C#/DDD : How to model entities with internal state objects not instantiable by the domain layer when using onion architecture?

标签 c# oop architecture domain-driven-design onion-architecture

我正在将类似“大泥球”(BBOM)的系统迁移到基于领域驱动设计思想的系统。

经过多次重构迭代后,领域聚合/实体目前使用内部状态对象建模,如 Vaughn Vernon 在本文中所述,例如:https://vaughnvernon.co/?p=879#comment-1896

所以基本上,一个实体可能看起来像这样:

public class Customer
{
    private readonly CustomerState state;

    public Customer(CustomerState state)
    {
        this.state = state;
    }

    public Customer()
    {
        this.state = new CustomerState();
    }

    public string CustomerName => this.state.CustomerName;

    [...]
}

到目前为止,该系统中的状态对象始终是来自应用程序当前使用的专有数据访问框架的数据库表包装器,类似于 Active Record 模式。因此,所有状态对象都继承自数据访问框架的基类部分。目前,不能将 POCO 用作状态对象、 Entity Framework 或任何其他对象。

该应用程序当前使用经典层架构,其中基础设施(包括提到的表包装器/状态对象)位于底部,然后是域。域知道基础设施,并且使用基础设施在域中实现存储库。正如您在上面看到的,大多数实体都包含一个公共(public)构造函数,用于在域内方便地创建新实例,它在内部只是创建一个新的状态对象(因为域知道它)。

现在,我们想进一步发展这一点,并逐渐扭转架构,从而产生更多的“洋葱”式架构。在该架构中,域将仅包含存储库接口(interface),而实际实现将由位于其之上的基础设施层提供。在这种情况下,域不再知道实际的状态对象/数据库表包装器。

解决这个问题的一个想法是让状态对象实现由域定义的接口(interface),这实际上似乎是一个很好的解决方案。这在技术上也是可能的,因为即使状态对象必须从特殊的数据访问基类继承,它们也可以自由地实现接口(interface)。
所以上面的例子会变成这样:
public class Customer
{
    private readonly ICustomerState state;

    public Customer(ICustomerState state)
    {
        this.state = state;
    }

    public Customer()
    {
        this.state= <<<-- what to do here??;
    }

    [...]
}

因此,当存储库(现在在基础架构中实现)实例化新客户时,它可以轻松传入实现 ICustomerState 的数据库包装器对象。到现在为止还挺好

但是,当在域中创建新实体时,不再可能同时创建内部状态对象,因为我们不再知道它的实际实现。

有几种可能的解决方案,但它们似乎都不是真正有吸引力的:
  • 我们总是可以使用抽象工厂来创建新实体,然后这些工厂将由基础设施实现。虽然在某些情况下,由于实体的复杂性,域工厂是合适的,但我不想在每种情况下都使用一个域工厂,因为它们会导致域中出现很多困惑并导致另一个依赖项被传递.
  • 我们可以使用另一个类(POCO),而不是直接使用数据库表包装器作为状态对象,它只保存值,然后由基础设施从/到数据库包装器进行转换。这可能有效,但最终会产生大量额外的映射代码,并导致每个数据库表(数据库包装器、状态对象、域实体)有 3 个或更多类,使维护复杂化。如果可能,我们希望避免这种情况。
  • 为了避免绕过工厂,实体内部的构造函数可以调用一些神奇的、类似单例的 StateFactory.Instance.Create<TState>()用于创建内部状态对象的方法。然后,基础设施有责任为其注册适当的实现。类似的方法是以某种方式获取 DI 容器并从那里解析工厂。我个人不太喜欢这种服务定位器方法,但在这种特殊情况下它可能是可以接受的。

  • 我还缺少什么更好的选择吗?

    最佳答案

    领域驱动设计不适合大泥球。尝试在大型系统中应用 DDD 不如面向对象的设计有效。尝试从相互协作的对象的角度来思考,隐藏数据的复杂性,并开始思考方法/行为,以通过行为来操纵对象内部。
    为了实现洋葱结构,我建议以下规则:

  • 尽量避免在业务规则中使用 Orm(EF、Hibernate 等),因为它在业务代码中增加了数据库(DataContext、DataSet、getter、setter、贫血模型、代码异味等)的复杂性。
  • 在业务规则使用组合中,关键是通过构造函数注入(inject)对象(系统中的参与者),尽量保持业务规则的纯度。
  • 要求对象对数据做一些事情
  • 在对象 API 的设计上投入时间。
  • 将实现细节留到最后(数据库、云、mongo 等)。你应该在类中实现细节,不要让代码的复杂性蔓延到它之外。
  • 尽量不要总是在你的代码中加入设计模式,只有在需要的时候。

  • 以下是我将如何使用对象设计业务规则以便具有可读性和维护性:
    public interface IProductBacklog
    {
        KeyValuePair<bool, int> TryAddProductBacklogItem(string description);
    
        bool ExistProductBacklogItem(string description);
    
        bool ExistProductBacklogItem(int backlogItemId);
    
        bool TryDeleteProductBacklogItem(int backlogItemId);
    }
    
    public sealed class AddProductBacklogItemBusinessRule
    {
        private readonly IProductBacklog productBacklog;
    
        public AddProductBacklogItemBusinessRule(IProductBacklog productBacklog)
        {
            this.productBacklog = productBacklog ?? throw new ArgumentNullException(nameof(productBacklog));
        }
    
        public int Execute(string productBacklogItemDescription)
        {
            if (productBacklog.ExistProductBacklogItem(productBacklogItemDescription))
                throw new InvalidOperationException("Duplicate");
            KeyValuePair<bool, int> result = productBacklog.TryAddProductBacklogItem(productBacklogItemDescription);
            if (!result.Key)
                throw new InvalidOperationException("Error adding productBacklogItem");
            return result.Value;
        }
    }
    
    public sealed class DeleteProductBacklogItemBusinessRule
    {
        private readonly IProductBacklog productBacklog;
    
        public DeleteProductBacklogItemBusinessRule(IProductBacklog productBacklog)
        {
            this.productBacklog = productBacklog ?? throw new ArgumentNullException(nameof(productBacklog));
        }
    
        public void Execute(int productBacklogItemId)
        {
            if (productBacklog.ExistProductBacklogItem(productBacklogItemId))
                throw new InvalidOperationException("Not exists");
            if(!productBacklog.TryDeleteProductBacklogItem(productBacklogItemId))
                throw new InvalidOperationException("Error deleting productBacklogItem");
        }
    }
    
    public sealed class SqlProductBacklog : IProductBacklog
    {
        //High performance, not loading unnesesary data
        public bool ExistProductBacklogItem(string description)
        {
            //Sql implementation
            throw new NotImplementedException();
        }
    
        public bool ExistProductBacklogItem(int backlogItemId)
        {
            //Sql implementation
            throw new NotImplementedException();
        }
    
        public KeyValuePair<bool, int> TryAddProductBacklogItem(string description)
        {
            //Sql implementation
            throw new NotImplementedException();
        }
    
        public bool TryDeleteProductBacklogItem(int backlogItemId)
        {
            //Sql implementation
            throw new NotImplementedException();
        }
    }
    
    public sealed class EntityFrameworkProductBacklog : IProductBacklog
    {
        //Use EF here
        public bool ExistProductBacklogItem(string description)
        {
            //EF implementation
            throw new NotImplementedException();
        }
    
        public bool ExistProductBacklogItem(int backlogItemId)
        {
            //EF implementation
            throw new NotImplementedException();
        }
    
        public KeyValuePair<bool, int> TryAddProductBacklogItem(string description)
        {
            //EF implementation
            throw new NotImplementedException();
        }
    
        public bool TryDeleteProductBacklogItem(int backlogItemId)
        {
            //EF implementation
            throw new NotImplementedException();
        }
    }
    
    public class ControllerClientCode
    {
        private readonly IProductBacklog productBacklog;
    
        //Inject from Services, IoC, etc to unit test
        public ControllerClientCode(IProductBacklog productBacklog)
        {
            this.productBacklog = productBacklog;
        }
    
        public void AddProductBacklogItem(string description)
        {
            var businessRule = new AddProductBacklogItemBusinessRule(productBacklog);
            var generatedId = businessRule.Execute(description);
            //Do something with the generated backlog item id
        }
    
        public void DeletePRoductBacklogItem(int productBacklogId)
        {
            var businessRule = new DeleteProductBacklogItemBusinessRule(productBacklog);
            businessRule.Execute(productBacklogId);
        }
    }
    

    关于C#/DDD : How to model entities with internal state objects not instantiable by the domain layer when using onion architecture?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47941019/

    相关文章:

    mysql - 数据库架构

    c# - 如何从 Unity 应用程序启动 Android Activity ?

    c# - 由于其保护级别而无法访问结构

    oop - OO 设计质量指标

    java - 有没有办法确定Java类中的方法是否已被覆盖

    asp.net-mvc - 在层之间复制模型

    java - 自己的物联网设备消息处理架构中的设计缺陷

    c# - 使用户控件显示在窗体边界之外

    c# - Visual Studio 2013 更新 2 - 删除 C# 导航栏中的项目下拉列表?

    javascript - 函数作为所有元素的方法