初始化域对象 - 观察SOLID,告诉,不要问

时间:2017-02-11 20:11:49

标签: domain-driven-design solid-principles tell-dont-ask

我试图遵循一些更新的设计原则,包括SOLID和Domain Driven Design。我的问题是关于人们如何处理"初始化"域对象。

这是一个简单的例子:

基于SOLID,我不应该依赖于具体结构,所以我创建了一个接口和一个类。由于我利用了域驱动设计,因此我使用相关方法创建了一个对象。 (即不贫血)。

Interface IBookstoreBook
{
   string Isbn {get; set;} 
   int Inventory {get; set;}
   void AddToInventory(int numBooks);
   void RemoveFromInventory(int numBooks);
}

public class BookstoreBook : IBookstoreBook
{
   public string Isbn {get; set;} 
   public int Inventory {get; private set;}
   public void AddToInventory(int numBooks);
   public void RemoveFromInventory(int numBooks);       
}

为了帮助进行测试并使其更松散耦合,我还使用IoC容器来创建本书。所以当创建这本书时,它总是被创建为空。但是,如果一本书没有ISBN和库存则无效。

BookstoreBook(string bookISBN, int bookInventory) {..} // Does not exist

我可以有4或5个使用BookstoreBook的不同类。一个人,

public class Bookstore : IBookstore
{
   ...
   public bool NeedToIncreaseInventory(BookstoreBook book) { ...}
   ...
}

任何方法如何知道获得有效的书籍?我的下面的解决方案似乎违反了“告诉不要问”的问题。原理。

a)每个使用Bookstore书籍测试的方法是否应该有效? (即,NeedToIncreaseInventory测试书的有效性吗?我不确定它应该知道是什么使BookstoreBook有效。)

b)我应该有一个" CreateBook"在IBookstoreBook对象上,只是"假设"客户知道他们想要初始化BookstoreBook时必须调用它吗?这样,NeedToIncreaseInventory就会相信那个" CreateBook"已经在BookstoreBook上调用了。

我对推荐的appreach在这里感兴趣。

4 个答案:

答案 0 :(得分:2)

首先,我认为您的BookstoreBook没有任何真正相关的方法,这意味着它没有任何相关行为,根本没有业务规则。由于它不包含任何业务规则,因此它实际上是贫血的。它只有一堆Getters和Setters。我认为让AddToInventory这样的方法最终只是为一个属性添加+1是没有意义的行为。

另外,为什么BookstoreBook知道Bookstore中有多少类型?我觉得这可能是Bookstore本身应该跟踪的东西。

关于a)点:不,如果您要从用户输入创建书籍,则应在创建新书之前检查提供的数据。这可以防止您的系统中出现无效的图书。

至于对象的创建,问题是你会有多种书籍类型吗?如果答案是否定的,您可以删除界面,只是在负责从用户输入创建新书的类中实例化一本书。如果您需要更多书籍类型,** keyword argument syntax可能会有用。

答案 1 :(得分:0)

  

为了帮助进行测试并使其更松散耦合,我还使用IoC容器来创建本书。

为什么您的IoC容器会创建书籍?这有点奇怪。您的域模型应该是容器不可知的(将接口和实现连接在一起是composition root的关注点)。

  

任何方法如何知道获得有效的书?

域模型知道它正在获得一本有效的书,因为它在界面中就是这么说的。

data 模型知道它正在生成一本有效的书,因为构造函数/工厂方法接受了它的参数而没有抛出异常。

  

每个使用Bookstore书籍测试的方法是否应该有效?

不,一旦你有了Book,它就会保持有效(你的域模型中不应该定义任何会创建无效数据模型的动词)。

  

我应该在IBookstoreBook对象上有一个“CreateBook”并且“假设”客户知道他们必须在他们想要初始化BookstoreBook的时候调用它吗?这样,NeedToIncreaseInventory就会相信已经在BookstoreBook上调用了“CreateBook”。

拥有一个用于创建对象的工厂是正常的。见Evans, chapter 6.

  

可以从数据库和许多其他地方创建书籍。我假设其他人必须解决这个问题,如果他们使用DDD,我想知道他们的方法。我们是否应该使用工厂 - 您建议将所需数据作为输入?

实际上只有两个数据源 - 您自己的记录簿(在这种情况下,您通过存储库加载数据),以及其他任何地方(您需要确保数据符合您模型的假设。

答案 2 :(得分:0)

首先,确保只能通过行为(方法)设置实体状态的好方法,以便使所有属性设置器都是私有的。它还允许您确保在状态更改时设置所有相关属性。

  

但是,如果一本书没有ISBN和库存,那么它就是无效的。

你有两个商业规则。让我们从ISBN开始吧。如果没有它,书籍无效,则必须在构造函数中指定。否则,完全有可能创建一本无效的书。 ISBN也具有指定的格式(至少长度)。所以这种格式也必须经过验证。

关于库存,我认为这不是真的。您可能拥有售罄的书籍或可在发布之前预订的书籍。对?因此,如果没有库存,可以存在一本书,这是不可能的。 如果从域视角查看库存和书籍之间的关系,它们是两个具有不同职责的独立实体。

一本书代表用户可以阅读的内容,并使用该信息来决定是否应该出租或购买。

库存用于确保您的应用程序可以满足您的客户请求。通常情况下,可以通过直接交货(减少库存)或延期交货(从供应商处订购更多副本然后交付账簿)来完成。

因此,应用程序的库存部分并不需要知道有关该书的所有信息。因此,我建议库存只知道书籍身份(根据Martin Fowler的书,这通常是根聚合如何相互引用)。

控制容器的反转通常用于管理服务(在DDD中,应用程序服务和域服务)。它的工作不是作为域实体的工厂。它只会使事情变得复杂而没有任何好处。

答案 3 :(得分:0)

  

基于SOLID,我不应该依赖于具体结构

如果您指的是依赖性倒置原则,那么它并没有完全说明。

- 高级模块不应该依赖于低级模块。两者都应该依赖于抽象。

- 抽象不应该依赖于细节。细节应取决于抽象。

没有域实体比其他实体更高级别,并且域层中通常没有对象是"详细信息",因此 DIP通常不适用于域实体

  

我还使用IoC容器来创建这本书

考虑到BookstoreBook没有依赖关系,我不确定你为什么这样做。

  

任何方法如何知道获得有效的书?

假设该书为Always Valid,则始终保持一致。这通常需要一个Book构造函数在创建时检查所有相关规则,以及状态更改方法强制执行有关Book的不变量。

  

a)...

     

b)......

您在这里混淆了两个问题 - 确保Book在任何地方都处于一致状态,并初始化Book。我不确定你的问题到底是什么,但是如果你应用"总是有效的"接近并忘记Book是一个接口/更高级别的抽象,你应该好好去。