DDD - 域模型,服务和存储库之间的依赖关系

时间:2009-04-16 16:07:19

标签: design-patterns architecture domain-driven-design

只是想知道其他人如何分层他们的架构。假设我的图层如下:

域名层 - 产品
--ProductService(imp应该进入这一层吗?)
--IProductService
--IProductRepository

基础设施层 --ProductRepository(我域中IProductRepository的Imp)

现在,当创建新产品时,我需要通过调用ProductService.GetNextProductId()方法来分配产品ID。

因为服务依赖于存储库,所以我使用IProductRepository接口设置ProductService ctor,可以在以后注入。像这样的东西:

    public class ProductService : IProductService
    {
        private IProductRepository _repository;

        public ProductService(IProductRepository repository)
        {
            _repository = repository;
        }

        public long GetNextProductId()
        {
            return _repository.GetNextProductId();
        }
    }

我的问题是,当我在Product Class中使用该服务时,我在实例化一个新的ProductService类时引用了ctor中的Repository。在DDD中,有一个很大的不,没有这样的参考。我甚至不确定我的产品域类是否正确设置以调用该服务,有人可以建议:

public class Product : Entity
    {
        private ProductService _svc;
        private IProductRepository _repository;

        public Product(string name, Address address) //It doesnt seem right to put parm for IProductRepository in the ctor?
            : base(_svc.GetNextProductId) // This is where i pass the id
        {
            // where to create an instance of IProductRepository?
        }
    }

我怎样才能优雅地解决这个设计问题?我愿意接受经验丰富的DDD的建议

编辑:

感谢您的评论。我还怀疑是否应该从产品类调用该服务。我还没有使用工厂模式,因为对象的构造仍然很简单。我不觉得它保证了工厂方法吗?

我很困惑......如果我的Product类需要来自服务的一些其他数据,例如GetSystemDateTime()(我知道,但是尝试演示非db调用的错误示例),将ProductId放在一边这里的服务方法是叫什么名字?

DDD中的服务是逻辑转储,其中逻辑对域对象不是天真的,对吧?那么它如何粘在一起?

6 个答案:

答案 0 :(得分:15)

到最后一点,DDD中的服务是一个放置我所描述的“笨拙”逻辑的地方。如果您有某种类型的逻辑或工作流程依赖于其他实体,那么这种逻辑类型通常不适合在域对象本身内部。示例:如果我的业务对象上有一个方法来执行某种类型的验证,那么服务类可能会执行此方法(仍然保持与其类中的实体相关的实际验证逻辑)

我经常提到的另一个很好的例子是资金转账方法。您不会将帐户对象从一个对象转移到另一个对象,而是您将拥有一个带有“到”帐户和“来自”帐户的服务。然后在服务中,您将在“来自”帐户和“到”帐户上的存款方式中调用提款方式。如果你试图把它放在帐户实体本身里,那就会觉得很尴尬。

可以找到一个很好的播客,深入讨论这个话题here。大卫拉里比做得非常好,现在只解释DDD的“如何”而不是“为什么”。

答案 1 :(得分:10)

您的域模型不应该引用ProductService或IProductRepository。如果您创建新产品,则必须通过工厂创建 - Factory可以使用ProductService获取产品ID。

实际上,我使用适当的接口(例如IProductIdGeneratorService)封装ProductService,以便您可以使用IoC容器将其注入工厂。

答案 2 :(得分:3)

以下是我如何构建您的问题。我相信这也是推荐的DDD方式。

public class ProductService : IProductService // Application Service class, used by outside components like UI, WCF, HTTP Services, other Bounded Contexts, etc.
{
    private readonly IProductRepository _prodRepository;
    private readonly IStoreRepository _storeRepository;

    public ProductService(IProductRepository prodRepository, IStoreRepository storeRepository) // Injected dependencies DI
    {
        if(prodRepository == null) throw new NullArgumentException("Prod Repo is required."); // guard
        if(storeRepository == null) throw new NullArgumentException("Store Repo is required."); // guard

        _prodRepository = prodRepository;
        _storeRepository = storeRepository;
    }

    public void AddProductToStore(string name, Address address, StoreId storeId) //An exposed API method related to Product that is a part of your Application Service. Address and StoreId are value objects.
    {
        Store store = _storeRepository.GetBy(storeId);
        IProductIdGenerator productIdGenerator = new ProductIdGenerator(_prodRepository);
        Product product = Product.MakeNew(name, address, productIdGenerator);
    }

    ... // Rest of API
}

public class Product : Entity
{
    public static MakeNew(string name, Address address, IProductIdGenerator productIdGenerator) // Factory to make construction behaviour more explicit
    {
        return new Product(name, address, productIdGenerator);
    }

    protected Product(string name, Address address, IProductIdGenerator productIdGenerator)
        : base(productIdGenerator.GetNextProductId())
    {
        Name = name;
        Address = address;
    }

    ... // Rest of Product methods, properties and fields
}

public class ProductIdGenerator : IProductIdGenerator
{
    private IProductRepository _repository;

    public ProductIdGenerator(IProductRepository repository)
    {
        _repository = repository;
    }

    public long GetNextProductId()
    {
        return _repository.GetNextProductId();
    }
}

public interface IProductIdGenerator
{
    long GetNextProductId();
}

基本上,ProductService是您的应用服务的一部分,即需要使用您的域或跨越其边界的所有内容的入口和出口点。它负责将每个用例委派给可以处理它的适当组件,并且如果需要很多组件来完成用例,则负责协调所有这些组件。

产品是您的AggregateRoot和域中的实体。它负责规定捕获企业域的UbiquitousLanguage的合同。因此,就其本身而言,这意味着您的域具有产品概念,其中包含数据和行为,无论您公开公开哪种数据和行为都必须是UbiquitousLanguage的概念。它的字段不应该在域模型之外具有外部依赖性,因此没有服务。但是,它的方法可以将域服务作为参数来帮助它执行行为逻辑。

ProductIdGenerator就是这种域服务的一个例子。域服务封装了跨越实体自身边界的行为逻辑。因此,如果您的逻辑需要其他聚合根或外部服务,如存储库,文件系统,加密等。基本上,任何您无法在实体内部进行锻炼的逻辑,而不需要任何其他东西,您可能需要域服务。如果逻辑是为了充实并且看起来概念上可能并不真正属于您的实体的方法,那么您可能需要一个全新的应用服务用例,或者您错过了设计中的实体。也可以使用非双重调度方式直接从Application Service使用域服务。这有点类似于C#扩展方法与普通静态方法。

===========回答你的编辑问题===============

  

我也怀疑是否应该从产品类调用该服务。

如果通过方法参数将域服务作为临时引用传递,则可以从产品类调用域服务。永远不应该从Product类调用Application Services。

  

我没有使用工厂模式(还)作为构造   对象仍然很简单。我不觉得它保证了工厂方法吗?

这取决于您所期望的将花费您更多时间,现在建立工厂,即使您没有多个构造逻辑,或者稍后重构。我认为对于那些不需要以多种方式构建的实体来说,它是不值得的。正如wikipedia所解释的那样,工厂用于使每个构造函数更明确和可区分。在我的示例中,MakeNew工厂解释了实体的这种特定结构用作目的:创建新产品。你可以有更多的工厂,如MakeExisting,MakeSample,MakeDeprecated等。这些工厂中的每一个都会创建一个产品,但用途不同,方式略有不同。如果没有Factory,所有这些构造函数都将命名为Product(),并且很难知道哪一个是什么,哪些是什么。缺点是当你扩展你的实体时工厂更难处理,子实体不能使用父工厂来创建一个孩子,这就是为什么我倾向于在构造函数中做所有构造逻辑的原因无论如何,只使用工厂为他们命名。

  

我'我很困惑...如果我的Product类将ProductId放在一边   需要来自服务的一些其他数据,例如GetSystemDateTime()(我知道,   糟糕的例子,但试图演示非数据库调用)这将是什么   服务方法叫什么?

假设您认为Date实施是基础架构的细节。您可以在它周围创建一个抽象,以便在您的应用程序中使用。它将从一个接口开始,可能就像IDateTimeProvider。这个接口有一个方法GetSystemDateTime()。

您的Application Services可以随时实例化IDateTimeProvider并调用其方法,而不是将结果传递给Aggregates,Entities,Domain Services或其他任何需要它的方法。

您的域名服务可以自由地将IDateTimeProvider作为类字段引用,但它不应该自己创建实例。它通过依赖注入接收它,或者它通过服务定位器请求它。

最后,您的Entites和Aggregate Roots和Value Objects可以自由调用GetSystemDateTime()和IDateTimeProvider的其他方法,但不能直接调用。它需要经过双重调度,您可以将域服务作为其中一种方法的参数,并使用该域服务查询所需的信息,或执行它需要的行为。它还可以将自身传递回域服务,域服务将在该域服务中进行查询和设置。

如果你认为你的IDateTimeProvider实际上是一个域服务,作为普适语言的一部分,你的实体和聚合根可以直接调用它上面的方法,它就不能把它作为一个类字段引用它,但方法参数的局部变量很好。

  

DDD中的服务是逻辑转储,其中逻辑不是天真的   域对象,对吗?那么它如何粘在一起?

我认为我的答案已经非常清楚了。基本上,你有三种可能性来粘合它(我至少可以想到这一点)。

1)应用程序服务实例化域服务,在其上调用方法,并将生成的返回值传递给需要它的其他东西(repo,实体,聚合根,值对象,其他域服务,工厂等。 )。

2)域服务由应用程序域实例化,并作为参数传递给将使用它的方法。无论使用什么,都没有永久引用它,它只是一个局部变量。

3)域服务由应用程序域实例化,并作为参数传递给将使用它的方法。无论使用什么,使用双重调度以非依赖方式使用域服务。这意味着它将域服务的方法传递给自己的引用,如DomainService.DoSomething(this,name,Address)。

希望这会有所帮助。 如果我做错了或违反DDD的最佳做法,欢迎提出意见。

答案 3 :(得分:1)

如果我正确理解您的问题,请说明您的Product类正在调用ProductService类。它不应该。您应该在负责创建和配置产品的工厂类中执行此操作。你调用这个方法的地方也可能取决于你想要发布ProductId的时间:我们可能有类似的情况,因为我们需要从我们的遗留会计系统中获取一个项目的数字。我推迟获取数字,直到项目持续存在,这样我们就不会浪费任何数字或有差距。如果您处于类似情况,则可能需要在存储库保存方法中发出ProductId,而不是在创建对象时。

顺便说一句,您是否真的认为您将拥有多个ProductService或ProductRepository?如果没有,那么我不会打扰接口。

编辑添加:

我建议从两个简单的类Product和ProductServices开始,从小做起并保持简单。 ProductServices将执行所有服务,包括工厂和存储库,因为您可以将其视为专业服务。

答案 4 :(得分:0)

为什么在内存中创建产品时需要产品ID?通常,在存储库中创建产品时会设置产品ID。

看看以下代码:

var id1 = _repository.GetNextProductId(); var id2 = _repository.GetNextProductId();

它会返回两个不同的产品ID吗?

如果答案是肯定的,那么它是安全的(但仍然很尴尬); 如果答案是否定的,那么你将遇到一个巨大的问题;

答案 5 :(得分:0)

与Marcelo达成一致,您应该首先确定产品ID是否真的是域模型概念。如果业务用户从不使用或不知道产品ID,并且通常通过名称,编号,SKU或由名称+维度组成的自然产品ID来引用产品,那么域模型应该注意这一点。

话虽如此,假设产品ID是数据库中的自动编号字段,这就是我如何构建我的DDD项目:

Project.Business (域名模型)

没有引用,因此也没有任何依赖。

public class Product : Entity
{
    private Product(string name, Address address)
    {
        //set values.
    }

    //Factory method, even for simple ctor is used for encapsulation so we don't have 
    //to publically expose the constructor.  What if we needed more than just a couple           
    //of value objects?
    public static CreateNewProduct(string name, Address address) 
    {
        return new Product(name, address);
    }

    public static GetAddress(string address, string city, string state, string zip) { }
}

public interface IProductRepository : IEnumerable<Product>
{
    void Add(Product product);
    //The following methods are extraneous, but included for completion sake.
    int IndexOf(Product product);
    Product this[int index] { get; set; }
}

<强> Project.Implementation

public SqlProductRepository : List<ProductDataModel>, IProductRepository
{
    public SqlProductRepository(string sqlConnectionString) { }

    public void Add(Product product)
    {
        //Get new Id and save the product to the db.
    }

    public int IndexOf(Product product)
    {
        //Get the index of the base class and convert to business object.
    }

    public Product this[int index]
    {
        get { //find instance based on index and return; }
        set { //find product ID based on index and save the passed in Business object to the database under that ID. }
    }
}

Project.ApplicationName (表示层)

public class Application
{
    IProductRepository repository = new SqlProductRepository(SqlConnectionString);

    protected void Save_Click(object sender, EventArgs e)
    {
        Product newProduct = Product.CreateNewProduct(name, Product.GetAddress(address,city,state,zip));
        repository.Add(newProduct);
    }
}    

如有必要,您可能有:

Project.Services (在自身和表示层之间使用DTO的应用服务层)