C#/ DDD:在使用洋葱架构时,如何使用域层无法实例化的内部状态对象的实体建模?

时间:2017-12-22 11:47:44

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

我正在迁移一个"大泥球#34;基于域驱动设计思想的系统(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用作状态对象,实体框架或其中任何一种。

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

现在,我们希望进一步发展这个并逐渐转变架构,从而产生更多的洋葱"一种建筑。在该体系结构中,域只包含存储库接口,实际的实现将由位于其上的基础结构层提供。在这种情况下,域无法再知道实际的状态对象/数据库表包装器。

解决这个问题的一个想法是让状态对象实现域定义的接口,这实际上似乎是一个很好的解决方案。它在技术上也是可行的,因为即使状态对象必须从特殊的数据访问基类继承,它们也可以自由地实现接口。 所以上面的例子会改为:

public class Customer
{
    private readonly ICustomerState state;

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

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

    [...]
}

因此,当存储库(现在在基础结构中实现)实例化一个新的Customer时,它可以轻松地传入实现ICustomerState的数据库包装器对象。到目前为止一切顺利

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

有几种可能的解决方案,但它们似乎都没有吸引力:

  • 我们总是可以使用抽象工厂来创建新实体,然后这些工厂将由基础设施实施。虽然在某些情况下由于实体的复杂性,域工厂是合适的,但我不希望在每种情况下都使用一个,因为它们会导致域中的大量混乱,并且还会传递另一个依赖项。
  • 我们不是直接使用数据库表包装器作为状态对象,而是使用另一个类(POCO),它只保存值,然后由基础结构从/向数据库包装器转换。这可能会起作用,但最终会产生大量额外的映射代码,并导致每个数据库表(DB包装器,状态对象,域实体)有3个或更多类,这使得维护变得复杂。如果可能的话,我们希望避免这种情况。
  • 为了避免在工厂周围传递,实体内部的构造函数可以调用一些魔术的,类似单例的StateFactory.Instance.Create<TState>()方法来创建内部状态对象。然后,基础设施负责为其注册适当的实施。类似的方法是以某种方式获得DI容器并从那里解决工厂。我个人并不喜欢这种服务定位器方法,但在这种特殊情况下可能是可以接受的。

我有没有更好的选择?

1 个答案:

答案 0 :(得分:0)

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

  • 尽量避免在业务规则中使用Orm(EF,Hibernate等),因为它会在业务代码中增加数据库(DataContext,DataSet,getter,setter,贫血模型,代码异味等)的复杂性。
  • 在业务规则中使用组合,关键是通过构造函数注入对象(系统中的actor),尝试在业务规则中保持纯洁。
  • 让对象对数据执行某些操作
  • 投入时间设计对象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);
    }
}