2.3.- 工场模式
另一方面,如果创建对象很简单,使用构造器或者控制反转/依赖注入容器足够创建对象的依赖。
一些工程增加值的场景如下:
如果创建的对象并不简单或者需要要做很多验证,在创建期间检查一些对象,建议使用工厂来创建对象,工厂自己没有领域模型的职责但也是领域设计的一部分。
良好工厂的要求:
1.- 每个创建方法都是原子的,要求所有创建对象的不变量。一个工厂应该只能生产透明状态的对象。对于实体,意味着创建整个聚合时满足所有的不变量。例如,一个订单工厂类应该创建整个聚合,包括根实体订单和子实体集合订单项。以后可能会添加订单项到聚合上。
2.- 如果可能使用抽象类型的参数而不是具体的类。工厂和生产的具体类耦合,但不需要和具体参数耦合。
一个单独的工厂通常生产整个聚合,传出一个根实体的引用,确保聚合的不变量都有。如果对象的内部聚合需要工厂,通常工厂方法的逻辑放在在聚合根上。这样对外部隐藏了聚合内聚的实现,同时赋予了根确保聚合完整的职责。如果聚合根不是子实体工厂的合适的家,那么继续创建一个单独的工厂。但是注意聚合内的访问限制。
下面的图显示了订单聚合的工厂例子:
是值对象唯一更新的地方。
2.3.1.- 领域层的仓储库契约/接口
仓储的实现不属于领域,输入基础结构层;然而,仓储库的契约应该属于仓储。那就是为什么我们在这里提到它。契约指定了仓储库应该提供什么,而不管它的内部实现。这些接口对技术无知。因此,仓储库接口应该在领域层定义。这也是基于 Martin Fowler定义的“接口分离模式”的面向领域驱动架构的建议方法。 为了实现这个模式,里关于实体和值对象需要是POCO。换句话说,它们应该对数据访问技术无知。我们要记住,领域实体是那些仓储库发送和接收的对象类型的参数。
总之,持久化透明的目标是领域类对仓储库实现一无所知。当编写领域层时,必须对仓储库的实现无知。这也提高了代码的可维护性和可读性。
Table 6.- 框架架构导则
Rule Nº: D12. 按照接口分离模式在领域层定义仓储库接口
o 建议
- 从在领域层和数据库访问层解耦的观点看,我们推荐在领域层定义仓储库接口,在数据持久化层实现该接口。
因此,一个领域模型类可能
- 这条规则完全符合基于控制反转容器的解耦技术。
参考
- Martin Fowler的“接口分离”模式
“在一个包中定义接口在另一个包中实现。这样客户端代码在接口中需要依赖时可以对实现完全无知。”
http://www.martinfowler.com/eaaCatalog/separatedInterface.html
2.3.2.- 领域模型服务
需要指出领域驱动设计中服务的概念不是分发服务。Web服务可能发布领域服务的实现,有可能Web应用只有领域服务没有Web服务。那些不是明确属于领域实体的操作一般是活动,不是领域实体的内部属性。但由于我们的编程模型是面向对象的,我们应该把它们搭建策划那个对象。这些对象就是我们所说的服务。 硬把那些领域操作作为领域对象的一部分会歪曲领域对象的定义。服务是一个或一组已由模型提供的操作。 服务这个词在服务模式中是这么定义的:“服务提供的操作是它提供给使用它的客户端,并突出领域对象的关系。” 高层的服务通常由活动命名。这种情况下,他们和用例的动词而不是名词相关,甚至有一个领域业务操作的抽象定义。 服务操作的名字应该从领域的语言中来。参数和返回值应该是领域对象。服务类也是领域组件,但这种情况下它们是领域层中的最高级别的对象。多数情况下领域服务覆盖了不同的概念,协调业务情景和用例中相关的实体。
当一个领域操作被视为一个重要的领域概念,一般就应该作为领域服务。 服务应该是无状态的。这不意味着实现它的类是静态的;它可能是一个实例化类。一个服务是无状态的意味着客户端程序可以使用服务的任何一个实例,而不管服务内部的状态。
此外,服务的执行可能使用或者修改全局信息。但是服务不应该有影响它行为的状态。
至于领域服务的类型,一个银行应用中的例子是,从一个账户到另一个账户转账,因为它需要调用账户类型的支付和入账操作。另外,转账这个行为/行为是银行领域的典型操作。这种情况下,服务本身不需要做很多,只是调用领域类的入账和支付方法。另一方面,把转账方法放在账户类中会是一个错误因为该操作设计两个账户,可能需要考虑其他的业务规则。
异常处理和业务异常抛出应该在领域服务和实体类两者的内部实现。
从领域的外面看,服务通常是可见的,为了完成每层相关任务/操作。
o 操作和领域概念相关而不是实体内部逻辑的部分。
- 《领域驱动设计》的“服务模式” - Eric Evans。
我们应该在我们需要时定义领域服务,确实有需要有实体领域逻辑的协调。
如下图所示,我们可以有一个领域服务(BankTransferService类)协调BankAccount实体的业务逻辑:
2.3.3.- 规格模式
该模式由 Martin Fowler and Eric Evans在下面的报告中在逻辑层面解释:
http://martinfowler.com/apsupp/spec.pdf
因此,决定获取什么数据和被查询的对象分隔开,从使用的机制中获取到。
规格模式的一个好处可以用下面的代码看出:
IEnumerable<Customer> customers = customerRepository.GetBySpec(IsPremiumCustomerSpecification);
使用这样的代码可以在查询方法时复用领域相关仓储库的规格。
我们将在理论上解释这种模式。然而在领域层实现的章节,我们会看到我们选择了不同的逻辑模式因为
.NET提供了更强大的语言。这指的是表达树,在处理内存中的对象时提供了更好的实现,而我们认为在这里
解释原始的定义来对该模式有更好的理解。
规格模式很有用的情况
规格在应用中有用户可以使用开放的组合查询,可以保存这样的查询使得以后可以使用。
归入模式(相关的模式)
一旦我们使用规格模式,另一个非常有用的模式是归入模式。归入指的是归并的作用和效果。这来自于拉丁文的
'sumĕre',意思是“to take”;包含某种东西,例如组件,在一个广泛的总结或分类或考虑更广泛的一部分或一个通用普遍
原则的特例或者规则。
换句话说,规格的一般使用是测试这些候选对象看是否复合规格的所有需要。归入可以比较规则来看是否符合一个
规格也就符合另一个规格。有时可能使用归入模式来实现这种灵活。如果候选对象可以生产一个规格,然后测试一个
规格就像是比较相似的规格。归入特别适合在复合应用中使用。
由于归入的逻辑概念始于使事情变得复杂,最好看Martin Fowler和Eric Evans提供的明细表,根据需要使用哪种模式:
Table 10.- 归入模式表 – By Martin Fowler和Eric Evans
需要根据规范选择对象的子集。 创建一个规格来确定是否满足规范。 规格模式
需要确定某个对象用来某个角色。 规格有IsSatisfiedBy(anObject)的方法
需要描述对象可以做什么而不用解释对象如何做, 返回是否所有的规范都由anObject满足。
描述候选对象如何构建以满足需求。
怎样实现规格? 我们把选择方法放到代码的方法中。 硬编码模式
在规格里为容易改变的值创建属性。
把IsSatisfiedBy()方法作为参数来整合 参数模式
这些参数。
为各种情况创建叶元素。创建"and","or", 复合规格
"not"复合节点的操作符。
如何比较两个规格来确定一个是另一个的特例 创建一个叫做IsGeneralizationOf来回应。 归入模式
或一个可以用另一个替代。
需要弄清楚需要做什么来满足需求。 增加一个RemainderUnsatisfiedBy的方法 部分满足规格模式
需要告诉用户为什么规格没被满足。 返回没被目标对象满足的需求。(最好和
复合规格一起使用)
规则 Nº: D16. 需要设计实现动态或复合查询时使用规格模式
o 规则
- 识别在应用使用模式是有用的,在设计实现领域组件时使用该模式,在仓储库内实现规格的执行。
什么时候使用规格模式
问题
- 选择: 我们需要基于特定的条件查询一组对象,并在一定的时间间隔刷新结果。
- 验证: 需要确保合适的对象用作特殊用途。
- 构建要求:需要描述对象可以做什么而不用解释对象如何做,但在某种程度上可以构建一个候选对象
来满足需求。
解决方案
- 创建一个规格看查看是否一个对象复合特定的规范。规格会有一个 IsSatisfiedBy(anObject)的方法,
当规范满足时返回True。
使用规格的优点
- 我们解耦了需求,灵活性和验证的设计。
- 查询的定义明确且明确
什么时候不使用规格模式
- 当过度使用规格模式时会陷入反模式,最终是对所有类型的对象过度使用。如果我们发现不用规格模式
的通用方法或者规格对象其实是领域实体,应该重新考虑该模式的使用。
- 无论如何,我们不应该使用到所有类型的查询上。不应该过度使用该模式。
参考
- Martin Fowler 和 Eric Evans的规格模式报告: http://martinfowler.com/apsupp/spec.pdf
Martin Fowler 和 Eric Evans的最处定义在下面的UML图中,说明了怎样对象和对象集合必须满足规格。
规格模式最初建议的。
上述声明的主要原因来自于模式本身的定义,其中涉及直接面对内存中的对象;IsSatisfiedBy()会有
对象的实例来检查是否满足特定的条件。因此,我们会稍微改变规格模式的定义来替代返回布尔值否定或
确认特定规格的结果,会返回一个声明。
这点会本章中领域层的实现部分,实现规格模式时讲到。