领域驱动设计里有好多概念不好理解,本篇主要记录一些自己看到的,自觉有助于自己理解的碎片知识。
聚合根(Aggreagte)
一种更大范围的封装,把一组有相同生命周期、在业务上不可分隔的实体和值对象放在一起考虑,只有根实体可以对外暴露引用,也是一种内聚性的表现。
确定聚合边界要满足固定规则(Invariant),是指在数据变化时必须保持的一致性规则,具体规则如下:
- 根实体具有全局标识,最终负责检查规定规则
- 聚合内的实体具有本地标识,这些标识在Aggregate内部才是唯一的
- 外部对象不能引用除根Entity之外的任何内部对象
- 只有Aggregate的根Entity才能直接通过数据库查询获取,其他对象必须通过遍历关联来发现
- Aggegate内部的对象可以保持对其他Aggregate根的引用
- Aggregate边界内的任何对象修改时,整个Aggregate的所有固定规则都必须满足
银行的例子,Account(账号)是CustomerInfo(客户信息)Entity和Address(值对象)的聚合根,Tansaction(交易)是流水(Journal)的聚合根,因为流水是因为交易才产生的,具有相同的生命周期。
这里的Transaction是当一个聚合根处理,牛!
领域服务
有些领域中的动作,它们是一些动词,看上去却不属于任何对象。它们代表了领域中的一个重要的行为,所以不能忽略它们或者简单地把它们合并到某个实体或者值对象中。当这样的行为从领域中被识别出来时,最佳实践是将它声明成一个服务。这样的对象不再拥有内置的状态。它的作用仅仅是为领域提供相应的功能。Service往往是以一个活动来命名,而不是Entity来命名。例如开篇转账的例子,转账(transfer)这个行为是一个非常重要的领域概念,但是它是发生在两个账号之间的,归属于账号Entity并不合适,因为一个账号Entity没有必要去关联他需要转账的账号Entity,这种情况下,使用MoneyTransferDomainService就比较合适了。
识别领域服务,主要看它是否满足以下三个特征:
- 服务执行的操作代表了一个领域概念,这个领域概念无法自然地隶属于一个实体或者值对象。
- 被执行的操作涉及到领域中的其他的对象。
- 操作是无状态的。
应用服务和领域服务如何划分
在领域建模中,我们一般将系统划分三个大的层次,即应用层(Application Layer),领域层(Domain Layer)和基础实施层(Infrastructure Layer)。可以看到在App层和Domain层都有服务(Service),这两个Service如何划分呢,什么样的功能应该放在应用层,什么样的功能应该放在领域层呢?
决定一个服务(Service)应该归属于哪一层是很困难的。如果所执行的操作概念上属于应用层,那么服务就应该放到这个层。如果操作是关于领域对象的,而且确实是与领域有关的、为领域的需要服务,那么它就应该属于领域层。
总的来说,涉及到重要领域概念的行为应该放在Domain层,而其它非领域逻辑的技术代码放在App层,例如参数的解析,上下文的组装,调用领域服务,消息发送等。还是银行转账的case为例,下图给出了划分的建议:
边界上下文(Bounded Context)
领域实体是有边界上下文的,比如Apple这个实体不同的上下文,表达的含义就完全不一样,在水果店它就是水果,在苹果专卖店它就是手机。
所以边界上下文(Bounded Context)在DDD里面是一个非常重要的概念,Bounded Context明确地限定了模型的应用范围,在Context中,要保证模型在逻辑上统一,而不用考虑它是不是适用于边界之外的情况。在其他Context中,会使用其他模型,这些模型具有不同的术语、概念、规则和Ubiquitous Language的行话。
这个第一次看很难理解。一般在一个项目中,很难抽象Bounded Context。可能自己对这个理解不够深入!
上下文映射(Context Mapping)
那么不同Context下的业务要互相通信怎么办呢?这就涉及跨边界的Context Mapping(上下文映射),首先不同上下文之间的通信可以是同步的,也可以是异步的,同步的话一般是RPC或者RESTFul,异步的话会推荐上文提到的Domain Event.
Mapping的方式有很多种,有Shared Kernal(共享内核),Conformist(追随者),以及Anti-Corruption(防腐层)等等。
比较推崇Domain Event + AC,这样可以将系统之间的耦合降到最低。
以我们真实的业务场景举个例子,比如会员这个概念在ICBU网站是指网站上的Buyer,但是在CRM领域是指Customer,虽然很多的属性都是一样的,但是二者在不同的Context下其语义和概念是有差别的,我们需要用AC做一下转换:
补充
模型重构
最后我想强调的是,建模不是一次性的工作,也不可能是一次性的工作,业务在演化,随之而来的模型也需要演化和重构,当模型和业务部匹配的时候,你还是要霸王硬上弓的往里面塞,其结果可想而知。
这个好难呀,现实项目,一个需求一个需求的迭代,可能没时间去模型重构,时间长了就变成了大泥球了。
模型演化
世界上唯一不变的就是变化,模型和代码一样也需要不断的重构和精化,每一次的精化之后,开发人员应该对领域知识有了更加清晰的认识。这使得理解上的突破成为可能,之后,一系列快速的改变得到了更符合用户需要并更加切合实际的模型。其功能性及说明性急速增强,而复杂性却随之消失。这种突破需要我们对业务有更加深刻的领悟和思考,然后再加上重构的勇气和能力,勇气是项目工期很紧你敢不敢重构,能力是你有没有完备的CI保证你的重构不破坏现有的业务逻辑。
下面举例很好(有帮助)
实体在演变
以银行账号为例,假如一开始账号都有withdraw(取钱)的行为,此时只需要在Account上加上withdraw方法就好了。
- 演变一:
随着业务的发展,我们需要支持ATM账号和Online账号,而Online账号是不能withdraw的,此时最差的做法是保持模型不变,而是在withdraw方法中判断如果是OnlineAccount则抛出异常。这种简单的代码堆砌可以满足业务功能,但是其业务语义完全被掩盖。更好的重构方法应该是将withdraw抽成一个接口IWithdrawable。 - 演变二:
好的,没有什么可以阻挡业务对变化的向往。现在公司出于安全性的考虑,为新开通的ATMAccount设置了取款上线,超过则不能支取。简单做法是在IWithdrawable中再加入一个setLimit行为,可是我们并不想改动影响到老的账号业务,所以更好的重构应该是重新写一个ILimitedWithdrawable接口,让其继承老接口,这样老的代码就可以保持不变了。
通过上面的例子,我们可以看到领域模型和面向对象是一对孪生兄弟,我们会用到大量的OO原则,比如上面的重构就用到了SOLID的SRP(单一职责)和OCP(开闭原则)。在实际工作中,我的确也有这样的体会,自从践行DDD以后,我们采用OOA和OOD的时候比以前明显多了很多,OO的能力也在不断的提升。
#####引入新抽象
还是以开篇的转账来举个例子,假如转账业务开始变的复杂,要支持现金,信用卡,支付宝,比特币等多种通道,且没种通道的约束不一样,还要支持一对多的转账。那么你还是用一个$transfer(fromAccount, toAccount)$就不合适了,可能需要抽象出一个专门的领域对象Transaction,这样才能更好的表达业务,其演化过程如下:
————————————————
版权声明:本文为CSDN博主「张建飞(Frank)」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/significantfrank/article/details/79614915