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

Ben*_*n J 5 c# architecture oop domain-driven-design onion-architecture

我正在将一个"大球泥"(BBOM)式系统迁移到基于域驱动设计思想的系统.

在重构的各种迭代之后,域聚合/实体当前使用内部状态对象建模,如本文中Vaughn Vernon所描述的,例如:https://vaughnvernon.co/?p = 889#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;

    [...]
}
Run Code Online (Sandbox Code Playgroud)

截至今天,该系统中的状态对象始终是来自当前使用的应用程序的专有数据访问框架的数据库表包装器,其类似于活动记录模式.因此,所有状态对象都从数据访问框架的基类部分继承.目前,无法将POCO用作状态对象,实体框架或其中任何一种.

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

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

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

public class Customer
{
    private readonly ICustomerState state;

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

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

    [...]
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

有没有更好的选择,我错过了?

小智 1

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

\n\n
    \n
  • 尽量避免在业务规则中使用 Orm\xc2\xb4s(EF、Hibernate 等),因为它会增加业务代码中数据库(DataContext、DataSet、getters、setters、贫乏模型、代码异味等)的复杂性。
  • \n
  • 在业务规则使用组合时,关键是通过构造函数注入对象(系统中的参与者),尽量保持业务规则的纯粹性。
  • \n
  • 要求对象对数据执行某些操作
  • \n
  • 投入时间来设计对象 API。
  • \n
  • 将实现细节留在最后(数据库、云、mongo 等)。您应该在类中实现细节,不要让代码的复杂性蔓延到类之外。
  • \n
  • 尽量不要总是在代码中使用设计模式,仅在需要时才使用。
  • \n
\n\n

以下是我如何使用对象设计业务规则,以便具有可读性和可维护性:

\n\n
public interface IProductBacklog\n{\n    KeyValuePair<bool, int> TryAddProductBacklogItem(string description);\n\n    bool ExistProductBacklogItem(string description);\n\n    bool ExistProductBacklogItem(int backlogItemId);\n\n    bool TryDeleteProductBacklogItem(int backlogItemId);\n}\n\npublic sealed class AddProductBacklogItemBusinessRule\n{\n    private readonly IProductBacklog productBacklog;\n\n    public AddProductBacklogItemBusinessRule(IProductBacklog productBacklog)\n    {\n        this.productBacklog = productBacklog ?? throw new ArgumentNullException(nameof(productBacklog));\n    }\n\n    public int Execute(string productBacklogItemDescription)\n    {\n        if (productBacklog.ExistProductBacklogItem(productBacklogItemDescription))\n            throw new InvalidOperationException("Duplicate");\n        KeyValuePair<bool, int> result = productBacklog.TryAddProductBacklogItem(productBacklogItemDescription);\n        if (!result.Key)\n            throw new InvalidOperationException("Error adding productBacklogItem");\n        return result.Value;\n    }\n}\n\npublic sealed class DeleteProductBacklogItemBusinessRule\n{\n    private readonly IProductBacklog productBacklog;\n\n    public DeleteProductBacklogItemBusinessRule(IProductBacklog productBacklog)\n    {\n        this.productBacklog = productBacklog ?? throw new ArgumentNullException(nameof(productBacklog));\n    }\n\n    public void Execute(int productBacklogItemId)\n    {\n        if (productBacklog.ExistProductBacklogItem(productBacklogItemId))\n            throw new InvalidOperationException("Not exists");\n        if(!productBacklog.TryDeleteProductBacklogItem(productBacklogItemId))\n            throw new InvalidOperationException("Error deleting productBacklogItem");\n    }\n}\n\npublic sealed class SqlProductBacklog : IProductBacklog\n{\n    //High performance, not loading unnesesary data\n    public bool ExistProductBacklogItem(string description)\n    {\n        //Sql implementation\n        throw new NotImplementedException();\n    }\n\n    public bool ExistProductBacklogItem(int backlogItemId)\n    {\n        //Sql implementation\n        throw new NotImplementedException();\n    }\n\n    public KeyValuePair<bool, int> TryAddProductBacklogItem(string description)\n    {\n        //Sql implementation\n        throw new NotImplementedException();\n    }\n\n    public bool TryDeleteProductBacklogItem(int backlogItemId)\n    {\n        //Sql implementation\n        throw new NotImplementedException();\n    }\n}\n\npublic sealed class EntityFrameworkProductBacklog : IProductBacklog\n{\n    //Use EF here\n    public bool ExistProductBacklogItem(string description)\n    {\n        //EF implementation\n        throw new NotImplementedException();\n    }\n\n    public bool ExistProductBacklogItem(int backlogItemId)\n    {\n        //EF implementation\n        throw new NotImplementedException();\n    }\n\n    public KeyValuePair<bool, int> TryAddProductBacklogItem(string description)\n    {\n        //EF implementation\n        throw new NotImplementedException();\n    }\n\n    public bool TryDeleteProductBacklogItem(int backlogItemId)\n    {\n        //EF implementation\n        throw new NotImplementedException();\n    }\n}\n\npublic class ControllerClientCode\n{\n    private readonly IProductBacklog productBacklog;\n\n    //Inject from Services, IoC, etc to unit test\n    public ControllerClientCode(IProductBacklog productBacklog)\n    {\n        this.productBacklog = productBacklog;\n    }\n\n    public void AddProductBacklogItem(string description)\n    {\n        var businessRule = new AddProductBacklogItemBusinessRule(productBacklog);\n        var generatedId = businessRule.Execute(description);\n        //Do something with the generated backlog item id\n    }\n\n    public void DeletePRoductBacklogItem(int productBacklogId)\n    {\n        var businessRule = new DeleteProductBacklogItemBusinessRule(productBacklog);\n        businessRule.Execute(productBacklogId);\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n