读书笔记 - 实现领域驱动设计
书中其实也在权衡技术细节与DDD的实现,有非常多的取舍的地方,所以完美的DDD实现在现有技术组件下几乎是不存在的。这反而有种为了实现DDD而实现DDD的感觉,而关于DDD到底能带给我们什么,由于我没有实际DDD的经验,所以我并不能很真实的感觉到。
第1章:DDD入门
1、将领域专家引入到团队
领域专家并不是一个职位,他可以是精通业务的任何人。他们可能了解更多的关于业务领域的背景知识,他们可能是软件产品的设计者,甚至有可能是销售员。
在实施DDD的过程中,最好将那些不怎么使用技术语言的人加进自己的团队。就像你会向他们学习一样,他们也会向你学习。
2、什么是领域模型
领域模型是关于某个特定业务领域的软件模型。通常,领域模型通过对象模型来实现,这些对象同时包含了数据和行为,并且表达了准确的业务含义。
3、为什么我们需要DDD
使领域专家和开发者在一起工作,这样开发出来的软件能够准确的传达业务规则。当然,对于领域专家和开发者来说,这并不表示单单的包容对方,而是将他们组成一个密切协作的团队。
“准确传达业务规则”的意思是说,此时的软件就像如果领域专家是编码人员时所开发出来的一样。
可以帮助业务人员自我提高。没有任何一个领域专家或者管理者敢说他对业务已经了如指掌,业务知识也需要一个长期的学习过程。在DDD中,每个人都在学习,同时每个人又是知识的贡献者。
关键在于对知识的集中,因为这样可以确保软件知识并不只是掌握在少数人手中。
在领域专家、开发者和软件本身之间不存在“翻译”,意思是当大家都使用相同的语言进行交流时,每人都能听懂他人所说。
设计就是代码,代码就是设计。设计是关于软件如何工作的,最好的编码设计来自于多次试验,这得益于敏捷的发现过程。
DDD同时提供了战略设计和战术设计两种方式。战略设计帮助我们理解哪些投入是最重要的;那些既有软件资产是可以重新拿来使用的;哪些人应该被加到团队中?战术设计则帮助我们创建DDD模型中各个部件。
4、难以捉摸的业务价值:P6
5、DDD如何帮助我们:P7
6、处理领域复杂性:P8
4、什么样的软件系统值得做出DDD投入
应该将DDD应用在最重要是业务场景下,值得投入的是那些最重要的、复杂的东西,因为这些东西将带来可观的回报。DDD的作用是简化,而不是复杂化。在使用DDD时,我们应该采用最简单的方式对复杂领域进行建模,而不是使问题变得更加复杂。
5、如何评价业务领域的复杂性
可参考书中的DDD计分卡。(P8)
6、什么是通用语言
通用语言是团队共享的语言。领域专家和开发者使用相同的通用语言进行交流。事实上,团队中每个人都使用相同的通用语言。不管你在团队中的角色如何,只要你是团队的一员,都将使用通用语言。团队成员通过讨论、参考资料、引用标准、查阅字典等对通用语言进行改进。有时我们发现,有些我们曾经认为能很好表达业务的词汇不再适用了,而另外的一些词汇具有更好的效果。
7、使用通用语言的几点注意事项
这里的“通用”意思是“普遍的”,或者“到处都存在的”。通用语言在团队范围内使用,并且只表达一个单一的领域模型。
“通用语言”并不表示全企业、全公司或者全球性的万能的领域语言。
限界上下文和通用语言间存在一对一的关系。在一个限界上下文中使用其专属的通用语言,对于那些不包含在通用语言中的概念,应该拒绝使用。
限界上下文是一个相对较小的概念,通常比我们起初想象的要小。限界上下文刚好能够容纳下一个独立的业务领域所使用的通用语言。
只有当团队工作在一个独立的限界上下文中时,通用语言才是“通用”的,
虽然我们只工作在一个限界上下文中,但是通常我们还需要和其他限界上下文打交道,这时可以通过上下文映射图对这些限界上下文进行集成。每个限界上下文都有自己的通用语言,而有时语言间的术语可能有重叠的地方。
如果你试图将某个通用语言运用在整个企业范围之内,或者更大、跨企业的范围内,你将失败。
8、DDD的业务价值
获得了一个非常有用的领域模型
业务得到了更准确的定义和理解
领域专家可以为软件设计做出贡献
更好的用户体验
清晰的模型边界
更好的企业架构
敏捷、迭代式和持续建模
使用战略和战术新工具
9、实施DDD所面临的挑战
为创建通用语言腾出时间和精力
持续的将领域专家引入项目
改变开发者对领域的思考方式
(自己额外增加的)现有技术生态多是为贫血模型和设计的,要在这样的情况下实现DDD,是有难度的。
10、如何选择开发方式
团队是否有领域专家,如果有,你如何围绕领域专家组织自己的团队?
虽然目前来说,你的业务领域是简单的,但它将来会变得复杂吗?对于复杂的系统来说,使用事务脚本是存在风险的。当领域变得复杂时,是否有可能将系统重构到富含行为的领域模型?
DDD的战术模式是否可用简化与其他限界上下文的集成,不管是第三方的还是定制开发的?
使用事务脚本是否的确可用减少代码量?(经验表明,不管对于哪种开发方式,事务脚本都不能减少代码量。这可能是由于在项目计划阶段,领域复杂性并没有得到正确的认识所致。因此,我们需要在领域复杂性上下足功夫。)
你项目的进度安排是否允许在战术建模上有所投入?
你核心域上的战术投入能否消除架构变化所带来的影响?事务脚本是做不到这一点的(和领域模型层相比,其他层更容易受到架构变化的影响)
客户是否的确能从这种持续设计和开发的方式中获益,或者有现成的产品能满足他们的需求?换句话说,我们是否应该一开始就考虑定制化开发?你最终取悦你的客户,而不是技术开发者,所以得慎重的做成选择。
使用DDD的战术开发模式会比其他开发方式更加困难吗,比如事物脚本?(这个问题很大程度上取决于团队成员的技能水平和是否有领域专家)
如果团队已经具备了实施DDD的条件,我们还会可以的选择另一种开发方式吗?有些开发者已经将模型的持久化变得很实用了,比如使用ORM、全聚合序列化和持久化、事件存储(Event Store)、或者战术DDD框架等。但是我们也不能排除还有热衷于其他开发方式的开发者。
第2章:领域、子域和限界上下文
1、什么是领域
从广义上讲,领域即是一个组织所做的事情以及其中所包含的一切。商业机构通常会确定一个市场领域模型,然后在这个市场中销售产品和服务。每个组织都有它自己的业务范围和做事方式。这个业务范围以及其中所进行的活动便是领域。当你为某个组织开发软件时,你面对的便是这个组织的领域。这个领域对于你来说应该是明晰的,因为你在这个领域中工作。
2、核心域、支撑子域、通用子域
3、问题空间
问题空间是领域的一部分,对问题空间的开发将产生一个新的核心域。对问题空间的评估应该同时考虑已有子域和额外所需子域。因此,问题空间是核心域和其他子域的组合。问题空间中的子域通常随着项目的不同而不同,他们各自关注于当前的业务问题,这使得子域对于问题空间的评估非常有用。子域允许我们快速的浏览领域中的各个方面,这些方面对于解决特定的问题是必要的。
4、解决方案空间
解决方案空间包括一个或多个限界上下文,即一组特定的软件模型。这是因为限界上下文即是一个特定的解决方案,它通过软件的方式来实现解决方案。
5、实施解决方案之前,需要对问题空间和解决方案空间进行评估
这个战略核心域的名字是什么,它的目标是什么?
这个战略核心域中包含哪些概念?
这个核心域的支撑子域和通用子域是什么?
如何安排项目人员?
你能组建出一支合适的团队吗?
6、解决方案空间在很大程度上受到现有系统和技术的影响,我们应该根据分离的限界上下文仔细的思考。考虑的问题:P51
7、限界上下文
限界上下文是一个显式边界,领域模型便存在于边界之内。在边界内,通用语言中的所有术语和词组都有特定的含义,而模型需要准确地反映通用语言。
8、如何划分限界上下文
限界上下文并不只局限于容纳模型,它通常标定了一个系统、一个应用程序或者一种业务服务。模块、聚合、领域事件、领域服务等基础部件都属于限界上下文。限界上下文主要用来封装通用语言和领域对象,但同时它也包含了那些为领域模型提供交互手段和辅助功能的内容。
限界上下文的划分不因因为一些平台、框架、基础设施或者开发任务拆分等因素来创建,有时,我们可以使用模块来避免创建一些微小的限界上下文。模块可以将多个限界上下文减少到一个,因为我们可以用战术化的手段来管理团队任务分配,而不是限界上下文。
实施DDD的底线是,采用语言驱动,而不是技术手段。
应该尽量保证一个团队,一个限界上下文。
第3章:上下文映射图
1、上下文映射图的表达方式
一个项目的上下文映射图可以用两种方式来表示。比较容易的一种是画一个简单的框图来表示两个或多个限界上下文之间的映射关系。该框图表示了不同的限界上下文在解决方案空间中是如何通过集成相互关联的。另一种更详细的方式是通过限界上下文集成的源代码实现来表示。
2、多个限界上下文之间的关系,各个团队之间的关系(P79)
合作关系
共享内核
客户方-供应方开发
遵奉者
防腐层(ACL)
开放主机服务(OHS)
发布语言(PL)
另谋他路
大泥球
3、对于一个非常详细的上下文映射图,我们很有可能无法对其进行实时更新。将映射图贴在墙上是有好处的,这样可以方便团队成员之间的交流。保持简单性和敏捷性,拒绝繁文缛节,这样我们所创建的上下文映射图将对项目起推动作用,而不是阻碍作用。
第4章:架构
1、DDD的一大好处便是它并不需要使用特定的架构。
由于核心域位于限界上下文中,我们可以在整个系统中使用多种风格的架构。有些架构包围着领域模型,能够全局性的影响系统,而有些架构则满足了某些特定的需求。
2、选择合适的架构风格和架构模式
架构风格阐述如何实现某种架构,架构风格之于架构就像设计模式之于设计一样,它将不同架构实现所共有的东西抽象出来,例如客户端-服务器架构风格、分布式对象风格;而架构模式则关注一种架构中的某个方面,架构模式比设计模式更加宽泛。
在选择架构风格和架构模式时,我们应将软件质量考虑在内,而同时,避免滥用架构风格和架构模式,采用的架构是用来减少风险的,而不是增加失败风险。架构风格和模式的选择受到功能需求的限制,比如用例和用户故事,用例驱动架构在当今软件开发中依然适用。
3、非常精彩的实际项目的架构演进流程(P100)
关键词:DI、前后端分离、REST、六边形架构(端口与适配器)、SOA、CQRS、事件驱动架构、long-running process、Sagas、Event Sourcing
4、分层架构
原则:每层只能与位于其下方的层发生耦合。(较低层也是可以与较高层发生耦合的,但只局限于采用观察者模式或者调停者模式)
严格分层架构:某层只能与直接位于其下方的层发生耦合;
松散分层架构:允许任意上方层与任意下方层发生耦合。
5、依赖倒置原则结合DDD的分层架构(P108)
结合后的现象:当我们在分层架构中采用依赖倒置原则时,可能会发现,事实上已经不存在分层的概念了。无论是高层还是低层,他们都只依赖于抽象,好像把整个分层架构给推平了一样。
6、六边形架构(端口与适配器)(P110)
核心是,系统的输入、输出均通过适配器实现,内部应用层的统一的公共API接受适配器的请求。应用层的功能是根据应用程序的功能需求来创建用例,而不是客户数量或输出机制。
六边形架构可以用来支持系统中的其他架构。比如可能采用SOA架构、REST或者事件驱动架构;也有可能采用CQRS;或者数据网织或基于网格的分布式缓存;还有可能采用Map-Reduce这种分布式并行处理方式。
7、SOA的精神
业务价值高于技术策略
战略目标高于项目利益
8、CQRS的指导原则
如果一个方法修改了对象的状态,该方法便是一个命令,它不应该返回数据。
如果一个方法返回了数据,该方法便是一个查询,此时它不应该通过直接的或间接的手段修改对象的状态。
9、事件驱动架构:基于消息的管道和过滤器处理过程的基本特征
管道是消息通道。过滤器通过输入管道接收数据,通过输出管道发送数据。实际上,管道即是一个消息通道
端口连接过滤器和管道。过滤器通过端口连接到输入和输出管道。端口使得六边形架构成为首选的架构。
过滤器即是处理器。过滤器可以对消息进行处理,而不见得一定对消息进行过滤。
分离处理器。每个过滤处理器都是一个分离的组件。
松耦合。每个过滤处理器都相对独立的参与处理过滤,处理器组合可以通过配置完成。
可换性。根据用例需求,我们可以重新组织不同处理器的执行顺序,这同样是通过配置完成。
过滤器可以使用多个管道。在命令行例子中,过滤器只从一个管道中读写数据,但是消息过滤器可以从不同的管道中读写数据,这表示了一种并行的处理过程。
并行使用同种类型的过滤器。对于最繁忙的和最慢的过滤器来说,我们可以并行的采用多个相同类型的过滤器来增加处理量。
10、事件驱动架构:设计长时处理过程(Saga)的几种方法(已支持Saga的消息机制:NServiceBus、MassTransit)
将处理过程设计成一个组合任务,使用一个执行组件对任务进行跟踪,并对各个步骤和任务完成情况进行持久化。
将处理过程设计成一组聚合,这些聚合在一系列的活动中相互协作。一个或多个聚合实例充当执行组件并维护整个处理过程的状态。这种方式被Amazon的Pat Helland所提倡。
设计一个无状态的处理过程,其中每一个消息处理组件都将对所接收到的消息进行扩充——即向其中加入额外的数据信息——然后再将消息发送到下一个处理组件。在这种方法中,整个处理过程的状态包含在每条消息中。
11、事件源技术上的优势(附录A详尽的阐述了如何在聚合上实现事件源)
事件历史可以用来消除系统中的bug,对调试也有很大的益处。
事件源有助于获得高吞吐量的领域模型,从而极大的提高事务处理效率。比如,向单张数据库表中追加事件是非常快的。
另外,事件源还有助于提高CQRS查询模型的伸缩性,因为此时查询模型的数据源可以在事件存储更新之后得到静默更新。
可以复制多个查询模型的数据源实例以满足更多的新增客户。
12、事件源业务上的优势
用新的或者修改后的事件向事件存储打补丁可以修正许多问题。这对于业务来说可能不那么显而易见,但是这些补丁可以在很大程度上减少由模型中的bug所带来的系统问题。
除了补丁之外,我们可以通过重放一组事件的方式来重做或撤销对模型的修改。
有了所有事件的历史消息,业务层便可以考虑很多诸如“如果....会怎么样”的问题。即通过重放一组发生在聚合上的事件,业务层可以得到很多问题的答案。通过模拟这些虚拟的业务场景,业务层可以从中获得不少好处,而这也是实现业务智能化的一种方式。
13、数据网织和基于网格的分布式计算(P143)
第5章:实体
1、为什么要使用实体
DDD并不总能满足我们的业务需求,有时,一个基于CRUD的系统会更加合适,节约了时间和金钱。但是,随着软件复杂性的增加,我们就越能体会到由错误的工具选择所带来的限制,这时维护一个基于CUD的系统可能是非常昂贵的。由于只从数据开发,CRUD系统是不能创建出好的业务模型的。
2、唯一标识:创建实体身份标识的策略
用户提供一个或多个初始唯一值作为程序输入,程序应该保证这些初始值是唯一的。
程序内部通过某种算法自动生成身份标识,此时可以使用一些类库或框架,当然程序自身也可以完成这样的功能。(例如UUID、GUID、Apache Commons Id)
程序依赖于持久化存储,比如数据库,来生成唯一标识。
另一个限界上下文(系统或程序)已经决定出了唯一标识,这作为程序的输入,用户可用在一组标识中进行选择。
3、委托标识
有些ORM工具,比如Hibernate,通过自己的方式来处理对象的身份标识。Hibernate更倾向于使用数据库提供的机制,比如使用一个数值序列来生成实体标识。如果我们自己的领域需要另外一种实体标识,此时这两者将产生冲突。为了解决这个问题,我们需要使用两种标识,一种为领域所用,一种为ORM所使用,在Hibernate中,这被称为委派标识。
4、发现实体及其本质特征(这方面书中的方法论还是少了些,从书中并不能很好的知道分析的方法,还是得从实操中获得积累)
实体及其本质特征
挖掘实体的关键行为
基于角色和职责来建模实体的优缺点
如何创建实体
采用哪种验证方式
跟踪实体的状态变化
第6章:值对象
1、值对象的优点
值对象用于度量和描述事物,我们应该尽量使用值对象来建模而不是实体对象。即便一个领域概念必须建模成实体,在设计时也应该更偏向于将其作为值对象容器,而不是子实体容器。因为我们可以非常容易的对值对象进行创建、测试、使用、优化和维护。
2、如何确定一个领域概念应该建模成一个值对象?
当你只关心某个对象的属性时,该对象便可作为一个值对象。为其添加有意义的属性,并赋予它相应的行为。我们需要将值对象看成不变对象,不要给它任何身份标识,还应该尽量避免像实体对象一样的复杂性。
3、值对象的特征
它度量或者描述了领域中的一件东西。
它可以作为不变量(不变性,当需要变更属性时,应采取替换而不是更改 该值对象)。
它将不同的相关的属性组合成一个概念整体(比如{500美元}具有两个属性,一个是500,一个是美元,单独一个500可能表达另外的意思,单独一个美元更不可能表达成值对象,只有这两个者联合起来才是一个表达货币度量的概念整体)。
当度量和描述改变时,可以用另一个值对象予以替换。
它可以和其他值对象进行相等性比较。
它不会对协作对象造成副作用。(传给值对象方法的参数依然应该是值对象,如果一个值对象方法将一个实体对象作为参数时,最好的方法是,让实体对象使用该方法的返回结果来修改其自身的状态)
4、划分值对象范围的方法
如果你视图将多个属性加在一个实体上,但这样却弱化了各个属性之间的关系,那么此时你便应该考虑将这些相互关联的属性组合在一个值对象中了。每个值对象都是一个内聚的概念整体,它表达了通用语言中的一个概念。如果其中一个属性表达了一种描述性概念,那么我们应该把与该概念相关的所有属性集中起来。如果其中一个或多个属性发生了改变,那么可以考虑对整体值对象进行替换。
5、最小化集成
可能的情况下尽量使用值对象来完成限界上下文之间的集成,这对于许多需要消费标准类型的上下文来说都是适用的。这样的好处是可以达到最小化集成,即可以最小化下游模型中用于管理职责的属性数目。使用不变的值对象使得我们做更少的职责假设。
6、根据领域模型来设计数据模型,而不是根据数据模型来设计领域模型。
无论你使用上面技术来完成数据建模,数据库实体、主键、引用完整性和索引都不能用来驱动你对领域概念的建模。DDD不是关于如何根据范式来组织数据的,而是在一个一致的限界上下文中建模一套通用语言。在这个过程中,应该尽量避免数据模型从领域模型中泄漏到客户端中。
7、持久化值对象
文中只举例了Hibernate的相关方法,其他ORM框架需要再进行查阅。
第7章:领域服务
1、领域服务(领域服务不是应用服务)
领域中的服务表示一个无状态的操作,它用于实现特定于某个领域的任务。当某个操作不适合放在聚合和值对象上时,最好的方式便是使用领域服务了。
不要滥用领域服务,滥用领域服务将导致贫血领域模型这种反模式。
2、应用服务
应用服务只用于协调任务,调用单个业务操作(领域服务),而由该业务操作去处理所有的业务细节。我们绝不能将业务逻辑放到应用层,即使业务逻辑非常简单,但它依然是业务逻辑。虽然我们不会讲业务逻辑放在应用层,但是应用层却可以作为领域服务的客户端。
3、领域服务命名?采用独立接口?
P245(不一定要遵循 Java EE规范)
第8章:领域事件
1、领域事件
领域专家所关心的发生在领域中的一些事件。将领域中所发生的活动建模成一系列的离散事件,每个事件都用领域对象来标识。领域事件是领域模型的组成部分,表示领域中所发生的事情。
2、领域专家和领域事件
虽然领域专家在起初可能意识不到所有类型的领域事件,但是通过讨论之后,他们是应该能够了解到其中的原因的。当团队成员对领域事件达成一致之后,领域事件便是“通用语言”的正式组成部分了。
3、应用服务控制着事物。不要在事件通知过程中修改另外一个聚合试了,因为这样破坏了聚合的一大原则:在一个事务中,只对一个聚合进行修改。
4、相对分布式事务来讲,领域事件是一种更轻量级的最终一致性的实现方式。
5、如何保证领域模型存储和事件存储(消息设施所使用的持久化存储)之间的一致性?
领域模型和消息设施共享持久化存储(比如数据源)。在这种情况下,对模型的修改和对事件的提交发生在同一个本地事务中。这种方式的优点在于性能很高,而缺点在于消息系统的存储区域(比如数据库表)必须和领域模型位于同一个数据库中。当然,如果你的领域模型和消息机制不能共享持久化存储,这种方式便不合适了。
领域模型的持久化存储和消息持久化存储由全局的XA事务(两阶段提交)所控制。这种方式的优点在于模型和消息所使用的持久化存储可以分开;缺点在于全局事务需要额外的支持,但不见得所有的存储机制都支持全局事务。全局事务的成本是很高的,而性能却很差。有可能出现的情况是,要么领域模型存储不支持XA事务,要么消息存储不支持XA事务,要么两者都不支持。
领域模型的持久化存储中,创建一个特殊的存储区域(比如一张数据库表),该区域用于存储领域事件,这便是一个事件存储(Event Store)。这种方式和方式1相似,但是此时的事件存储区域不再由消息机制所拥有和控制,而是你的限界上下文。同时,你需要创建一个消息外发组件将事件存储中的所有消息机制发送出去。这种方式的优点在于:模型修改和事件提交可以同时位于单个本地事物中。另一个额外的优点是,我们可以发布基于Rest的事件通知。使用这种方式时,消息机制所使用的消息存储是完全私有的。因此,这种方式的缺点是,我们可能需要定制开发一个消息转发组件来发送消息,同时客户需要对消息进行消重处理。
6、时延与自治服务和系统
有些业务服务可能需要更高的吞吐量,此时我们需要好好的考虑最大容许时延,系统的架构应该满足在事件时延上的需求。对于自治服务和支持它们的消息设施来说,我们应该在可用性和可伸缩性上下足功夫,以便更好地完成那些非功能性的需求。
7、事件存储的方式
将事件存储作为一个消息队列来使用,该消息队列的作用是将所有的领域事件通过消息设施发布出去。这种方法是本书中首要使用的方法,它允许在不同的限界上下文之间进行集成,此时远程的订阅方将对领域事件做出反应以满足自身上下文的需求。
将相同的事件存储用于基于REST的事件通知。
检查由模型的命令方法所产生的所有结果的历史记录。这可以用于跟踪bug,不只是跟踪自己模型中的bug,还可以跟踪客户方中的bug。因此,此时的事件存储不再只是一个简单的审计日志。审计日志对于调试来说是有用的,但是却很少包含由聚合命令方法所产生的完整结果。
使用事件存储中的数据来进行业务预测和分析。很多时候,业务人员只有在需要使用这些数据的时候才能意识到这些历史数据的重要性,而在没有事件存储来维护这些数据的时候,他们便捉襟见肘了。
当从资源库中获取一个聚合实例时,使用事件来重建该聚合实例。对于事件源来说,这是一个必要的组成部分。重建聚合通过顺序的应用发生在该聚合上的所有事件来完成。你可以将任意数量的事件用于聚合重建。
8、REST风格事件通知的优缺点
如果多个客户方都可以通过单个URI来请求相同的事件通知,那么此时REST便是合适的。一个事件通知可以拥有任意多的消费方。虽然REST使用的是“拉”的方式,而不是“推的”的方式。
如果一个或多个消费方需要从多个发布方中获取资源以顺序的完成一系列任务,那么此时你便会感到REST所带来的痛苦了。这实际上描述了一个消息队列,许多发送方同时为一个或多个消费方服务,此时事件的接收顺序是重要的。对于实现消息队列来说,“拉”的方式并不是一个好的选择。
9、通过消息中件发布事件通知
第9章:模块
1、模块的作用
在DDD中,模型中的模块表示了一个命名的容器,用于存放领域中内聚在一起的类,将类放在不同的模块中的目的在于达到松耦合性。由于DDD中的模块并不是一个通用的存储区域,因此对其进行适当的命名是重要的。事实上,模块名是通用语言的重要组成部分,应该反映出它们在领域中的概念。
2、设计模块的简单原则
模块应该和领域概念保持一致。通常,对于一个或一组内聚的聚合来说,我们都相应的创建一个模块。
根据通用语言来命名模块。这也是DDD的一个基本目标。
不要机械式的根据通用的组件类型和模式来创造模块。如果我们将所有的聚合放在一个模块中,将所有的领域服务放在一个模块中,又将所有的工厂放在另一个模块中,那么我们是得不到什么好处的。这有悖于DDD设计原则,同时还会限制我们创建富含行为的领域模型。此时,我们的关注点不是在领域上,而是在当前的组件和模式上。
设计松耦合的模块。模块间的松耦合性与类间的松耦合性具有相同的好处。这样有利于我们维护和重构一些模块层面上的概念。
当同层模块间出现耦合时,我们应该杜绝循环依赖。要使不同的模块完全独立是不可能的。但是,如果我们消除同层模块之间的双向依赖,我们便可以减少它们之间的耦合度(比如产品依赖于开发团队,但开发团队却不依赖于产品)
在父模块和子模块之间放松原则(父模块即位于较高层次的模块,子模块即位于较低层次的模块)。要消除父模块和子模块之间的依赖的确是困难的。但是在有可能的情况下,我们依然应该避免它们之间的循环依赖,只有在无法避免时才引入循环依赖(比如,父模块中的对象创建一个子模块中的对象,而子模块对象又需要维护对父模块对象的引用)
不要将模块设计成一个静态的概念,而是与模型中的对象一道进行建模。如果模型概念将随时间而改变,这往往意味着对应的模块也应该随之而变。当你发现概念名和模块名不再匹配时,你应该对模块进行重构。诚然,这是痛苦的,但是和那些糟糕的模块命名相比,这些痛苦是值得的。
3、先考虑模块,再是限界上下文
有时,通用语言可以很好的帮助我们做出正确的选择。但是另外的时候,其中的术语将变得非常含糊。在这种情况下,我们并不清楚如何划分上下文边界。此时,我们可以首先将它们放在一起,使用模块来对模型进行划分,而不是限界上下文。
但是,这并不意味着我们就应该限制对限界上下文的创建。我们应该通过通用语言的需求来划分模型边界。你应该知道,限界上下文不是用来代替模块的。使用模块的目的在于组织那些内聚在一起的领域对象,对于那些内聚性不强或者没有内聚性的领域对象来说,我们应该将它们划分在不同的模块中。
第10章:聚合
1、设计聚合的原则
一个事务中只修改一个聚合实例。试图保持多个聚合实例间的一致性通常意味着我们缺少了某些聚合不变条件。
设计聚合时主要关注的是聚合的一致性边界,而不是创建一个对象树。
设计小聚合,擅用根实体、值对象;设计时考虑下系统性能和可伸缩性。
新的用例可能引导我们重新对聚合进行建模,甚至产生了一个大的聚合(几个聚合构成),但并不意味着要在单个事物中维持聚合的一致性,业务目标可以通过聚合间的最终一致性来实现。
通过唯一标识引用其他聚合
在聚合边界之外使用最终一致性。任何跨聚合的业务规则都不能总是保持处于最新状态。通过事件处理(消息队列?)、批处理或者其他更新机制,我们可以在一定时间之内处理好他方依赖。
2、一致性处理的指导原则
对于一个用例,问问是否应该由执行该用例的用户来保证数据的一致性。如果是,请使用事务一致性,当然此时依然需要遵循其他聚合原则。如果需要其他用户或者系统来保证数据一致性,请使用最终一致性。
3、打破原则的理由
方便用户界面
缺乏技术机制(没有消息机制、分布式事物、定时器、后台线程等等技术组件)
全局事务
查询性能
4、迪米特法则
强调了“最小知识”原则。考虑一个客户端对象需要调用系统中其他对象的行为方法的场景,此时我们可以将后者称为服务对象。在客户端对象使用服务对象时,它应该尽量少的知道服务对象的内部结构。客户端对象不应该知道任何关于服务对象属性的信息。客户端对象可以根据表层接口调用服务对象上的命令方法。然而,客户端对象不应该渗入到服务对象的内部。如果客户端所需服务位于服务对象的内部,那么此时客户端对象不应该访问这样的服务。对于服务对象来说,它只应该提供表层接口,在接口方法被调用时,它将操作委派给内部方法以完成功能。
对迪米特法则做一个简单的总结:任何对象的任何方法只能调用以下对象中的方法:①该对象自身;②所传入的参数对象;③它所创建的对象;④自身所包含的其他对象,并且对那些对象有直接访问权。
5、“告诉而非询问”原则
一个对象不应该被告知如何执行操作。对于客户端来说,这里的“非询问”表示:客户端对象不应该首先询问服务对象,然后根据询问结果调用服务对象中的方法,而是应该通过调用服务对象的公共接口的方式来“告诉”服务对象所要执行的操作。该原则和迪米特原则存在相似之处,但是使用起来更加简单。
6、实现原则
创建具有唯一标识的根实体
优先使用值对象
使用迪米特法则和“告诉而非询问”原则
乐观并发
不要在聚合中注入资源库和领域服务,我们应该在聚合命令方法执行之前进行查找,然后将其传入命令方法。(否则将会有很多额外的对象引用产生,系统内存吃紧、垃圾回收周期漫长的问题会更加严重)
第11章:工厂
1、使用工厂的主要动机:
将创建复杂对象和聚合的职责分配给一个单独的对象,该对象本身并不承担领域模型中的职责,但是依然是领域设计的一部分。工厂应该提供一个创建对象的接口,该接口封装了所有创建对象的复杂操作过程,同时,它并不需要客户去引用那个实际被创建的对象。对于聚合来说,我们应该一次性地创建整个聚合,并且确保它的不变条件得到满足。
2、聚合根中的工厂方法的好处
有效的表达限界上下文中的通用语言
减轻客户端在创建新聚合实例时的负担
确保所创建的实例处于正确的状态
第12章:资源库
1、严格来讲,只有聚合才拥有资源库。存在两种类型的资源库设计:面向集合设计和面向持久化设计。
2、面向集合资源库
我们可以将面向集合的资源库看成是一种传统的方式,因为它体现了原生DDD资源库模式的基本思想。这种资源库模拟了一个集合,或者至少模拟了集合上的标准接口。此时,从资源库的接口来看,我们根本看不出其背后还存在着持久化机制,也感觉不到我们是在向存储区域中保存数据。
3、面向集合资源库精要
一个资源库应该模拟一个set集合。无论采用什么类型的持久化机制,我们都不应该允许多次添加同一个聚合实例。另外,当从资源库中获取到一个对象并对其进行修改时,我们并不需要“重新保存”该对象到资源库中。考虑下集合的情形,要修改其中的一个对象,我们只需要先从集合中获取到该对象的引用,然后在该对象上执行行为方法即可。
4、持久化机制支持面向集合资源库的方法:
隐式读时复制(Implicit Copy-on-Read):在从数据存储中读取一个对象时,持久化机制隐式的对该对象进行复制,在提交时,再将该复制对象与客户端中的对象进行比较。详细过程如下:当客户端请求持久化机制从数据存储中读取一个对象时,该持久化机制一方面将获取到的对象返回给客户端,一方面立即创建一份该对象的备份(除去延迟加载部分,这些部分可以在之后实际加载时再进行复制)。当客户端提交事务时,持久化机制把该复制对象与客户端中的对象进行比较。所有的对象修改都将更新到数据存储中。
隐式写时复制(Implicit Copy-on-Write):持久化机制通过委派来管理所有被加载的持久化对象。在加载每个对象时,持久化机制都会为其创建一个微小的委派并将其交给客户端。客户端并不知道自己调用的是委派对象中行为方法,委派对象会调用真实对象中的行为方法。当委派对象首次接受到方法调用时,它将创建一份对真实对象的备份。委派对象将根据发生在真实对象上的改变,并将其标记为“肮脏的”。当事务提交时,该事务检查所有的“肮脏”对象并将对它们的修改更新到数据存储中。
5、面向集合资源库的高性能ORM工具:TopLink、EclipseLink
6、面向持久化资源库精要
在向数据存储中添加新建对象或修改既有对象时,我们都必须显示的调用put方法,该方法将以新的值来替换先前关联在某个键上的原值。这种类型的数据存储可以极大的简化对聚合的读写。正因如此,这种数据存储也称为聚合存储或面向聚合数据库。
7、额外的行为
一个资源库应该尽可能的模拟一个集合,因此命名也应该类似。比如count可以换为size。
在数据存储过程中,可能需要执行一些计算过程来满足某些非功能性需求。但是这些功能最好放在领域服务中,因为领域服务正是用于处理那些无状态的、特定于领域的操作。
可以出于性能的考虑,只查找聚合根的一部分,但是谨慎使用这种方式。
获取的数据如果来源于多个聚合,可以使用用例优化查询的方法直接查询所需要的数据,直接在持久化机制上执行查询,然后将查询结果放在一个“值对象”中予以返回。
在使用用例优化查询时,如果发现必须创建多个查询方法,那么这可能是一种坏味道,一般意味着对聚合边界的划分是错误的。如果的确发生了这种情况,并且你确认对聚合边界的设计是正确的,此时应该考虑使用CQRS了。
8、管理事务的位置
通常来说,我们将事务放在应用层中。然后为每个主要的用例创建一个门面,门面中的业务方法通常都是粗粒度的,常见的情况是每一个用例流对应一个业务方法。业务方法对用例所需操作进行协调
对事务的管理绝对不应该放在领域模型和领域层中。通常来说与,与领域模型相关的操作都非常细粒度的,以至于无法用于管理事务,领域模型也不应该意识到事务的存在。
9、使用事务的警告
不要过度的在领域模型上使用事务。我们必须慎重的设计聚合以保证正确的一致性边界。有时,在测试环境下,在单个事务中修改多个聚合可能工作得很好,但是在产品环境下,却有可能出现由并发所导致的事务失败。
10、资源库VS数据访问对象(DAO)
一个DAO主要从数据库表的角度来看待问题,并且提供CRUD操作。表模块、表数据网关、活动记录这样的模式应该用于事务脚本程序中,需要与领域模型分离开来对待,这些与DAO相关的模式通常只是对数据库表的一层封装。
资源库和数据映射器则更加偏向于对象,因此通常被用于领域模型中。通常来说,你可以将资源库当做DAO来看待。但是注意一点,在设计资源库时,我们应该采用面向集合的方式,而不是面向数据访问的方式。这有助于将领域当作模型来看待,而不是CRUD操作。
第13章:集成限界上下文
1、集成限界上下文的方式
RPC
消息机制,通过发布-订阅机制
RESTful HTTP。作者认为这并不是一种形式的RPC。
2、分布式系统原则
网络是不可靠的
总会存在时间延迟,有时甚至非常严重
带宽是有限的
不要假设网络是安全的
网络拓扑结构将发生变化
知识和政策在多个管理员之间传播
网络传输是有成本的
网络是异构的
3、通过消息集成限界上下文,长时处理过程的状态机和超时跟踪器。
4、防腐层
第14章:应用程序
1、应用程序与领域模型的关系
领域模型通常位于应用程序的中心位置。应用程序通过用户界面向外展示领域模型的概念,并且允许用户在模型上执行各种操作。用户界面使用应用服务来协调用例任务,管理事务,并执行一些必要的安全授权。另外,用户界面、应用服务和领域模型依赖于企业级的特定平台设施的支持。这些基础设施的实现细节通常包含组件容器、应用程序管理、消息系统和数据库等。
2、什么是应用程序
书中使用的“应用程序”标识那些支撑核心域模型的最近,通常包括领域模型本身、用户界面、内部使用的应用服务和基础设施组件等。至于这些组件中应该包含些什么,这是根据应用程序的不同而不同的,并且有可能受到所有架构的影响。
3、用户界面系统的类型(P469)
纯粹请求-应答式web用户界面,也称为web1.0。典型框架有Struts、SpringMvc和Web Flow、ASP.NET等。
基于web的富互联网应用(Rich Internet Application,RIA)用户界面,包括那些使用DHTML和Ajax的系统,也称为Web2.0。Google GWT、Yahoo YUI、Ext JS、Adobe Flex和Microsoft Silverlight均属于这个范畴。
本地客户端GUI(比如Windows、Mac和Linux的桌面用户界面)
4、应用服务(P478)
将应用服务于领域服务等同起来是错误的,它们并不相同。我们应该将所有的业务领域逻辑放在领域模型中,不管是聚合、值对象或者领域服务;而将应用服务做成很薄的一层,并且只使用它们来协调对模型的任务操作。
5、基础设施(P489)
基础设施的职责是为应用程序的其他部分提供技术支持。这里,虽然我们避免对分层的讨论,但是保持着依赖倒置原则的心态依然是有用的。因此,从架构上讲,无论基础设施位于什么地方,只要它的组件依赖于用户界面、应用服务和领域模型中的接口,而这些接口又需要特殊的技术支持,那么它都能工作得很好。这样,在应用服务获取资源库时,它只会依赖于领域模型中的接口,而实际使用的则是基础设施中的实现类。
资源库的实现被放在了基础设施层中,因为它们负责处理数据存储,而这些不属于模型的职责。你可以使基础设施层实现那些与消息相关的接口,比如消息队列和E-mail等。如果还有一些特殊的用户界面组件来处理诸如图表之类的展现,那么它们也应该放在基础设施层中。
如有侵权,请联系删除!
~~~ END ~~~