工厂
在针对大型的复杂领域进行建模时,聚合、实体和值对象之间的依赖关系可能会变得十分复杂。在某个对象中为了确保其依赖对象的有效实例被创建,需要深入了解对象实例化逻辑,我们可能需要加载其他相关对象,且可能为了保持其他对象的领域不变性增加了额外的业务逻辑,这样即打破了领域的单一责任原则(SRP),又增加了领域的复杂性。
那如何去创建复杂的领域对象呢?因为复杂的领域对象的生命周期可能需要协调才能进行创建。 这个时候,我们就可以引入创建类模式——工厂模式来帮忙,将对象的使用与创建分开,将对象的创建逻辑明确地封装到工厂对象中去。
先理清工厂和工厂模式。
DDD中工厂的主要目标是隐藏对象的复杂创建逻辑;次要目标就是要清楚的表达对象实例化的意图。
而工厂模式是计模式中的创建类模式之一。借助工厂模式我们可以很好实现DDD中领域对象的创建。
而针对工厂模式的实现主要有四种方式:
- 简单工厂:简单实用,但违反开放封闭;
- 工厂方法:开放封闭,单一产品;
- 抽象工厂:开放封闭,多个产品;
- 反射工厂:可以最大限度的解耦。
为什么需要工厂
- 当创建一个复杂对象或聚合的过程很复杂并且暴露出了过多的内部结构时,我们则可以使用工厂进行封装。一个对象在它的声明周期中要承担大量的职责,如果再让复杂对象负责自身的创建,那么职责过载将会导致问题。
- 我们设计好领域模型供客户方调用,但如果客户方也必须使用如何装配这个对象,则必须知道对象的内部结构。好比你去驾校学车,却得先学会发动机的原理。对客户方开发来说这是很不友好的。其次,复杂对象或者聚合当中的领域知识(业务规则)需要得到满足,如果让客户方自己装配复杂对象或聚合的话,就会将领域知识泄露到客户方代码中去。
- 对象的创建本身可以是一个主要操作,但被创建的对象并不适合承担复杂的装配操作。将这些职责混在一起可能产生难以理解的拙劣设计。让客户直接负责创建对象又会使客户的设计陷入混乱,并且破坏被装配对象或聚合的封装,而且导致客户与被创建对象的实现之间产生过于紧密的耦合。
工厂的设计要点
1.每个创建方法都应该是原子的,并保证生成的对象处于一致的状态。
2.可以使用独立的工厂或者在聚合根上使用工厂方法。当 A 对象的创建主要使用了 B 对象的数据或者规则时,那么可以在 B 对象上创建一个工厂方法来生成 A 对象。
3.以下情况只需使用构造函数即可。
- 类仅仅是一种类型,没有其他子类,没有实现多态性。
- 客户关心的是实现类。
- 客户可以访问对象的所有属性,因此向客户公开的构造函数中没有嵌套的对象创建。
- 构造过程很简单。
- 公共构造函数必须遵守与工厂相同的规则,必须是原子操作且满足所有固定规则。
- 不要在构造函数中调用其他构造函数,应保持构造函数的简单。
4.工厂方法的参数应该是较低层的对象。比如装配一辆汽车,应该传入较低层抽象的轮胎,发动机等对象。
例子说明工厂
我们以一个论坛对象发起一个讨论为例
public class Forum {
public DiscussionstartDiscussion(DiscussionId aDiscussionId, Author anAuthor, String aSubject) {
if(this.isClosed()){
throw new IllegalStateException("Forum is closed!");
}
Discussion discussion = new Discussion(this.tenant(), this.forumId(), aDiscusstionId, anAuthor, aSubject);
// .. 发布领域事件
return discusstion;
}
}
客户端如何使用这个模型呢?
// 由论坛实体生成这个讨论
Discussion discussion = aForum.startDiscussion(this.discussionRepository.nextIdentity(),
new Author("jdoe", "John Doe", "jdoe@gmail.com"),
"Dealing with Aggregate Concurrency Issues");
// 保存这个讨论
this.discussionRepository.add(discussion);
这里 讨论实体封装了对象的实体,相当于在工厂就可以实现一切
工厂不仅达到了封装创建对象的细节,并有效的表达了限界上下文中的通用语言,减轻客户端在创建新聚合实例时的负担,确保所创建的实例处于正确的状态(符合业务规则)。
工厂作用
对象创建不是一个领域的关注点,但它确实存在于应用程序的领域层中。通过使用工厂可以有效的保证领域模型的干净整洁,以确保领域模型的对现实的准确表达。使用工厂具有以下好处:
- 工厂将领域对象的使用和创建分离。
- 通过使用工厂类,可以隐藏创建复杂领域对象的业务逻辑。
- 工厂类可以根据调用者的需要,创建相应的领域对象。
- 工厂方法可以封装聚合的内部状态。
资源库
资源库是啥
为什么使用资源库
-
如果完全按照领域模型的角度,完全通过遍历对象的方法来获取所有关联的对象。这种模型会过于错综复杂。对象嵌套的层级或者关联的层级非常深。例如通过 Customer.order.product.price 层层遍历来获取当时客户订单的商品的价格。
-
那如果完全按照数据库模型的角度,模型中的对象不需要完全连接起来,对象关系网就能保持在一个可控范围。但是这又会回到之前传统开发模式中,零散的使用各个 DAO 从各个表抽取数据自行拼凑出我们想要的模型。
这里就会出现一个问题,Customer 类需要保持客户所有已订的 Order,还是通过 CustomerID 在数据库中查找 Order 列表呢?
在所有持久化对象中,有一小部分必须通过基于对象属性的搜索来全局访问。当很难通过遍历的方式来访问某些聚合根的时候,就需要这种访问方式,它们通常是实体,有时可能是具有复杂内部结构的值对象。而其他对象则不适合使用这种访问方式,因为这会混淆它们之间的重要区别。随意的数据库查询会破坏领域对象的封装和聚合。技术基础设施的数据访问机制的暴露会增加客户的复杂度,并妨碍模型驱动的设计。
如何使用资源库
应该将资源库看作一个对象的集合。
客户使用查询方法向资源库请求对象,这些查询方法根据客户所指定的条件来挑选对象。资源库检索被请求的对象,并封装数据库查询和元数据映射机制。他们可以返回汇总的信息,如多少个实例满足条件。甚至返回汇总计算,如所有匹配对象的某个数值属性的总和。
这样一来客户就只需与一个简单的、易于理解的接口进行对话,并根据模型向这个接口提出它的请求。
为每种需要全局访问的对象类型创建一个资源库,这个资源库相当于该类型的所有对象在内存的一个集合的“替身”。通过一个众所周知的全局接口来提供访问。提供添加和删除对象的方法,用这些方法来封装在数据存储中实际插入和删除数据的操作。根据提供具体条件来挑选对象的方法,并返回属性值满足条件的对象或者对象集合,从而将实际的存储和查询技术封装起来。只为那些确实需要直接访问的聚合根提供资源库,让客户始终聚焦于模型,而将所有对象的存储和访问操作交给资源库来完成。
让用户无感知的,以为就在内存中使用一个集合一样。
资源库的方法设计:
方法参数:
- 唯一标识
- 复杂的参数组合
- 值域(日期范围)
返回值:
- 对象
- 对象集合
- 某些类型的汇总。
资源库的实现
- 对类型进行抽象,比如有奔驰车宝马车,不应有两种资源库,而应该是只有一个抽象车类的资源库。
- 充分利用与客户进行解耦。
- 将事务的控制权交给客户。
资源库的优点
- 为客户提供了一个简单的模型,可用来获取持久化对象并管理他们的生命周期。
- 他们将应用程序和领域设计与持久化技术进行解耦。
- 它们体现了有关对象访问的设计决策。
- 很容易测试,将利用集合直接替换资源库进行测试。
资源库中如何管理事务
对事务的管理绝对不应该放在领域模型和领域层中。通常来说,与领域模型相关的操作都非常的细粒度,以致于无法用于管理事务。另外,领域模型也不应该意识到事务的存在。通常我们将事务放在应用层,然后为每个主要的用例创建一个门面,一个用例对应一个门面业务方法,如果所有操作安全完成的话,门面中的业务方法提交事务,否则失败回滚。
面向集合资源库(java中的集合)
在这里 不止collection go中的集合 不可重复的也是一个道理
面向集合的资源库,接口跟集合接口设计类似,如提供add,addAll,remove,removeAll,看起来就像在操作集合对象一样面向集合资源库,需要持久化机制提供特殊支持,比如隐式的跟踪发生在某个持久化对象上的改变,这样才能支持“要修改其中一个对象,我们只需要先从集合中获取到该对象的引用,然后在该对象上执行行为方法即可”,为了实现隐式变化跟踪,有隐式读时复制和隐式写时复制,但都需要大量的内存和一些代理操作,都会有性能上的损耗,所以这是实现的一个考虑因素。另外参照集合的add方法行为,面向集合资源库add时,只要集合中存在该聚合实例,添加就会失败,不论实例的属性是不是有变化。这也另一方面说明,资源库应该模拟集合的接口和行为,屏蔽持久化机制。
面向持久化资源库(mongodb)
在ddd 中用mongodb持久化 https://blog.csdn.net/muzizongheng/article/details/85228945
面向持久化的资源库,接口风格和行为上,会照顾持久化机制的实现,比如提供save,saveAll,remove,removeAll,而重要是:对聚合实例的修改,需要显式的调用save方法,完成实例的持久化更新
资源库VS数据访问对象DAO
DAO主要是从数据库表的角度来看待问题,来提供CRUD操作。表模块,表数据网关和活动记录,是DAO相关模式,是数据库表的一层封装,用于事务脚本程序中,而资源库和数据映射器(Data Mapper)则更加倾向于对象,因此通常被用于领域模型中。而资源库的接口设计,也应该是通用语言的表达
管理事务
“对事务的管理绝对不应该放在领域模型和领域层中。通常来说,与领域模型相关的操作都非常细粒度的,以至于无法用于管理事务,另外,领域模型也不应该意识到事务的存在”,通常事务应该放在应用层,然后为每个主要的用例创建一个门面(facade),门面的业务方法都是粗粒度的,然后一个业务方法对应一个事务,比如使用声明式事务:
集成限界上下文
领域模型是以限界上下文为边界的,集成限界上下文是跨上下文的,所以领域模型不是重点,本章更多是技术实现的指导。
如前面章节所述,一个限界上下文很多时候就是一个代码工程,对应着一个服务进程,那么集成限界上下文,就是对应着平常说的进程间通信。在分布式系统中,就是分布式系统通信了。在多用户系统,也为了高可用,基本分布式集群部署,是必需的考虑。所以书中论述,其实也是多从分布式系统方面考虑。
分布式系统有什么需要注意?
- 网络是不可靠的;
- 总会存在时间延迟,有时甚至非常严重;
- 带宽是有限的;
- 不要假设网络是安全的;
- 网络拓扑结构将发生变化;
- 知识和政策在多个管理员之间传播;
- 网络传输是有成本的;
- 网络是异构的。
在聚合那一章中也提到,一个事务原则上只对一个聚合进行更新操作(一个事务可以做到强一致性),如果多个聚合的情况,那使用最终一致性的解决方案。那集成限界上下文呢,跨上下文的通信,更只能使用最终一致性了。所以在下面列出的集成限界上下文的技术方案以及长时处理过程,都是以最终一致性作为指导原则的。
集成限界上下文的方式
集成限界上下文有三种常用的方式:RPC,REST,消息机制
RPC
RESTFull
就是RESTFull http接口,好处是非常明显的,成熟的公开的广泛使用的协议。所以对企业外集成,一般使用REST。然后REST http一般是同步调用的,即调用后需要服务提供方马上返回结果的(书中说法是:服务的提供方必须直接参与),所以客户端不是完全自治的。虽然也可以使用一些技术手段来优先:比如增加代理层(nginx负载均衡其实就是方案),或使用回调方案,但其实还不如直接在客户端做一些容错措施。
注意这里的CollaboratorService、UserInRoleAdaptor和CollaboratorTranslator便组成了一个防腐层。那HttpClient呢?也是防腐层的一部分,它是Adapter的技术实现,不属于领域模型的概念。
防腐层方式
开放主机服务:当一个限界上下文以URI形式提供了大量的rest资源 比如说在springcloud中为当前限界上下文提供的领域接口
防腐层: 不同于分层架构中的应用层 领域层 基础设施层 interface层
防腐层针对的是不同上下文之间的交互:
获取上游上下文的对象服务接口CollaboratorService设置于领域层
其实现往往设计技术实现:比如feign等,所以我们将其实现设计于分层架构中的基础设施层[体现依赖倒置,面向抽象编程]
防腐层入参为上游的对象,出参为当前限界上下文的对象
由文中的上下文映射图可知 防腐层广义上实际上是两个上下文交互的一系列代码实现的总称 包含领域服务接口,适配器,技术实现如feign ,翻译层等等 ,侠义上可以更加关注于翻译层
消息
使用领域事件,结合消息机制,使得系统间很好的解耦,书中原话是“在使用消息进行集成时,任何一个系统都可以获得更高层次的自治性”。而下一句是“只要消息基础设施工作正常,即使其中一个交互系统不可用,消息依然可以得到发送和投递”,虽然业务系统的自治性提高了,但增加了对消息基础设施的依赖,而在工程实践中,其实这也是需要考虑的因素。
“在有可能的情况下,我们应该最小化不同限界上下文之间的信息复制,甚至彻底消除”,因为要保证两个上下文之间数据同步是很困难的,虽然我们的目标也只是最终一致性,但还是存在诸多挑战(这里说一下工作实践的一般做法:数据增加版本,增量同步+定期全量数据校正),所以建议的方案是:只同步实体或聚合的唯一标识。
长时处理过程
这是消息集成方式的典型用法,使用消息异步机制解耦多个上下文,因为是异步的,所以使用一个长时处理过程Process的技术来跟踪一个多步骤的异步任务,怎么跟踪呢?任务或过程有自己的Id(ProcessId),有自己的资源库(持久化)和状态机,需要实现超时跟踪器;然后各个上下文及各个聚合的操作实现幂等支持;然后靠着状态的异步检查和定期检查,然后重试,实现长时处理过程的最终成功。其实这也是一个分布式系统事务补偿和最终一致性的体现。
不过有一点想补充,在实践中,每个上下文自己处理增加高可用的措施,比如完善的异常机制和重试机制,然后整体处理过程失败的几率是很低的,然后处理任务又比较多,这种情况,如果每一个任务都注册了一个Process,每个任务都定时检查,整个系统的消耗还是太多了。所以有一种方案是:Write Ahead Log,即在每个处理环节都进行日志记录,然后各个环节处理最终失败会有错误报警,只有出现错误报警了,才有系统自动或人工启动的补偿重试程序,然后从日志中去恢复和重试一个任务。而大部分情况,任务是都可以成功结束的,不需额外处理。