文章目录
- 为什么要领域建模
-
- DDD革命
- DDD不是银弹
- DDD初体验
- 银行转账领域模型实现
- 领域建模的好处
-
- 面向对象
- 业务语义显性化
- 如何进行领域建模
- 领域事件(Domain Event)
-
- Event命名
- Event内容
- Event Sourcing
- Event Storming
- 聚合根(Aggregate)
- 领域服务(Domain Service)
-
- 什么是领域服务
- 领域服务陷阱
- 边界上下文(Bounded Context)
- 上下文映射(Context Mapping)
-
- 边界上下文和微服务
- 模型重构
-
- 模型统一
- 模型演化
- 实体在演变
-
- 演变一:
- 演变二:
- 引入新抽象
- 业务可视化和可配置化
为什么要领域建模
维护过企业级业务系统的同学,基本上没有一个不抱怨业务代码烂的,过程式的面条代码充斥着屏幕,程序员的心力和体力都经受着极大的考验,怎么破/p>
DDD革命
DDD革命性在于,领域模型准确反映了业务语言,而传统J2EE或Spring+Hibernate等事务性编程模型只关心数据,这些数据对象除了简单setter/getter方法外,没有任何业务方法,被比喻成贫血模式。
以银行账 Account为案例,Account有“存款”,“计算利息”和“取款”等业务行为,但是传统经典的方式是将“存款”,“计算利息”和“取款”行为放在账 的服务AccountService中,而不是放在Account对象本身之中。我们不能因为用了计算机,用了数据库,用了框架,业务模型反而被技术框架给绑架了,就像人虽然是由母亲生的,但是人的吃喝拉撒母亲不能替代,更不能以母爱名义剥夺人的正常职责行为,如果是这样,这个人就是被母爱绑架了。
DDD不是银弹
软件的世界里没有银弹,是用事务脚本还是领域模型没有对错之分,关键看是否合适。就像自营和平台哪个模式更好案是都很好,所以亚马逊可以有三方入住,阿里也可以有自建仓嘛。
实际上,CQRS就是对事务脚本和领域模型两种模式的综合,因为对于Query和 表的场景,使用领域模型往往会把简单的事情弄复杂,此时完全可以用奥卡姆剃刀把领域层剃掉,直接访问Infrastructure。
我个人也是坚决反对过度设计的,因此对于简单业务场景,我强力建议还是使用事务脚本,其优点是简单、直观、易上手。
但对于复杂的业务场景,你再这么玩就不行了,因为一旦业务变得复杂,事务脚本就很难应对,容易造成代码的“一锅粥”,系统的腐化速度和复杂性呈指数级上升。目前比较有效的治理办法就是领域建模,因为领域模型是面向对象的,在封装业务逻辑的同时,提升了对象的内聚性和重用性,因为使用了通用语言(Ubiquitous Language),使得隐藏的业务逻辑得到显性化表达,使得复杂性治理成为可能。
接下来,让我们看一个银行转账的实例,对比下事务脚本和领域模型两者编程模型的不同。
DDD初体验
银行转账事务脚本实现
在事务脚本的实现中,关于在两个账 之间转账的领域业务逻辑都被写在了MoneyTransferService的实现里面了,而Account仅仅是getters和setters的数据结构,也就是我们说的贫血模型:
上面的代码大家看起来应该比较眼熟,因为目前大部分系统都是这么写的。需求评审完,工程师画几张UML图完成设计,就开始向上面这样怼业务代码了,这样写基本不用太费脑,完全是面向过程的代码风格。有些同学可能会说,我这样写也可以实现系统功能啊。
还是那句话“just because you can, doesn’t mean you should”。说句不好听的,正是有这么多“没有追求”、“不求上进”的码农才造成了应用系统的混乱、败坏了应用开发的名声。这也是为什么很多应用开发工程师觉得工作没意思,技术含量低,觉得整天就是写if-else的业务逻辑代码,系统又烂,工作繁琐、无聊、没有成长、没有成就感,所以转向去做中间件啊,去写JDK啊,觉得那个NB。实际上,应用开发一点都不简单也不无聊,业务的变化比底层Infrastructure的变化要多得多,解决的难度也丝毫不比写底层代码容易,只是很多人选择了用无聊的方式去做。
其实我们是有办法做的更优雅的,这种优雅的方式就是领域建模,唯有掌握了这种优雅你才能实现从工程师向应用架构的转型。同样的业务逻辑,接下来就让我们看一下用DDD是怎么做的。
银行转账领域模型实现
如果用DDD的方式实现,Account实体除了账 属性之外,还包含了行为和业务逻辑,比如debit( )和credit( )方法。
而且透支策略OverdraftPolicy也不仅仅是一个Enum了,而是被抽象成包含了业务规则并采用了策略模式的对象。
而Domain Service只需要调用Domain Entity对象完成业务逻辑即可。
通过上面的DDD重构后,原来在事务脚本中的逻辑,被分散到Domain Service,Domain Entity和OverdraftPolicy三个满足SOLID的对象中,在继续阅读之前,我建议可以自己先体会一下DDD的好处。
领域建模的好处
DDD最大的好处是:接触到需求第一步就是考虑领域模型,而不是将其切割成数据和行为,然后数据用数据库实现,行为使用服务实现,最后造成需求的首肢分离。DDD让你首先考虑的是业务语言,而不是数据。DDD强调业务抽象和面向对象编程,而不是过程式业务逻辑实现。重点不同导致编程世界观不同。
面向对象
- 封装:Account的相关操作都封装在Account Entity上,提高了内聚性和可重用性。
- 多态:采用策略模式的OverdraftPolicy(多态的典型应用)提高了代码的可扩展性。
业务语义显性化
通用语言:“一个团队,一种语言”,将模型作为语言的支柱。
确保团队在内部的所有交流中,代码中,画图,写东西,特别是讲话的时候都要使用这种语言。例如账 ,转账,透支策略,这些都是非常重要的领域概念,如果这些命名都和我们日常讨论以及PRD中的描述保持一致,将会极大提升代码的可读性,减少认知成本。说到这,稍微吐槽一下我们有些工程师的英语水平,有些神翻译让一些核心领域概念变得面目全非。
显性化:就是将隐式的业务逻辑从一推if-else里面抽取出来,用通用语言去命名、去写代码、去扩展,让其变成显示概念,比如“透支策略”这个重要的业务概念,按照事务脚本的写法,其含义完全淹没在代码逻辑中没有突显出来,看代码的人自然也是一脸懵逼,而领域模型里面将其用策略模式抽象出来,不仅提高了代码的可读性,可扩展性也好了很多。
如何进行领域建模
初步建模
好的模型应该是建立在对业务深入理解的基础上。就我自己的经验而言,建模是一个不断迭代的过程,一开始可以简单点来。
首先抓住一些核心概念,这些业务知识和核心概念可以通过和业务专家沟通,也可以通过头脑风暴的形式从User Story或者Event Storming去扣。
然后假设一些业务场景走查一下,再写一些伪代码验证一下run一下,看看顺不顺,如果很顺滑,说明没毛病,否则就要看看是不是需要调整一下模型,随着项目的进行和对业务理解的不断深入,这种迭代将持续进行。
举个栗子,比如让你设计一个中介系统,一个典型的User Story可能是“小明去找工作,中介说你留个电话,有工作机会我会通知你”,这里面的关键名词很可能就是我们需要的领域对象:
- 小明是求职者。
- 电话是求职者的属性。
- 中介包含了中介公司,中介员工两个关键对象。
- 工作机会肯定也是关键领域对象;
- 通知这个动词暗示我们这里用观察者模式会比较合适。
然后再梳理一下领域对象之间的关系,一个求职者可以应聘多个工作机会,一个工作机会也可以被多个求职者应聘,M2M的关系,中介公司可以包含多个员工,O2M的关系。对于这样简单的场景,这个建模就差不多了。
当然我们的业务场景往往比这个要复杂,而且不是所有的名词都是领域对象也可能是属性,也不是所有的动词都是方法也可能是领域对象,再者,看的见实体好找,看不见的、隐藏的,需要深入理解业务,需要“无中生有”才能得到的抽象就没那么容易发现了,所以要具体问题具体对待,这个进化的过程需要我们有很好的业务理解力,抽象能力以及建模的经验(知道为什么公司的job model里那么强调技术人员的业务理解力和抽象能力了吧。
比如通常情况下,价格和库存只是订单和商品的一个属性,但是在阿里系电商业务场景下,价格计算和库存扣减的复杂程度可以让你怀疑人生,因此作为电商中台,把价格和库存单独当成一个域(Domain)去对待是很必要的。
当然这个只是最初级的模型,接下来我会通过DDD中的一些核心概念的介绍,让大家更清楚的了解建模的过程。
领域事件(Domain Event)
An event is something that has happened in the past. A domain event
is, logically, something that happened in a particular domain, and
something you want other parts of the same domain (in-process) or
domain in aonther bounded context to be aware of and potentially react to.
Domain Event是由一个特定领域触因为一个用户Command触发的发生在过去的行为产生的事件,而这个事件是系统中其它部分感兴趣的。
为什么Domain Event如此重要因为在现在的分布式环境下,没有一个业务系统是割裂的,而Messaging绝对是系统之间耦合度最低,最健壮,最容易扩展的一种通信机制。因此理论上它是分布式系统的必选项。
但是目前大部分系统的Event都设计的很随性,没有统一的指导和规范,导致Event滥用和无用的情况时有发生,而Domain Event给我们一个很好的方向,指引我们该如何设计我们系统的Event。
Event命名
Your Domain Event type names should be a statement of a past occurrence, that is, a verb in the past tense。The event is typically the result of a command.
因为表示的是过去事件的结果,所以推荐命名为Domain Name + 动词的过去式 + Event。这样比较可以确切的表达业务语义。
下面是几个举例:
- CustomerCreatedEvent,表示客户创建后发出的领域事件。
- OpportunityTransferedEvent,表示机会转移后发出的领域事件。
- LeadsCreatedEvent,表示线索创建后发出的领域事件。
Event内容
Event的内容有两种形式:
Enrichment(丰富):也就是在Event的payload中尽量多多放data,这样consumer就可以自恰(Autonomy)的处理消息了。
Query-Back:这种是在Event中通过回调拿到更多的data,这种形式会加重系统的负载,performance也会差一些。
所以如果要在Enrichment和Query-Back之间做选择的话,首先推荐使用Enrichment。
Event Sourcing
Event Sourcing是在Domain Event上面的一个扩展,是一个可选项。
也就是要有一个Event Store保存所有的Events,其实如果你是用MQ作为Event机制的话,这些Events都是存储在MQ当中的,只是MQ并没有提供很好的Event查询和回溯,所以如果决定使用Event Sourcing的话,最好还是自己单独建立一个Event Store。
使用Event Sourcing主要有以下好处,如果用不到的话,完全可以不用,但是Domain Event还是强烈建议要使用。
Event Sourcing存储了所有发生在Core Domain上面的事件。
基于这些事件,我们可以做系统回放,系统Debug,以及做用户行为的分析(类似于打点)
Event Storming
事件风暴是《DDD Distilled》书中提出的一个业务分析的方法论,其主要作用是从Domain事件出发,来分析用户Command,来找到Ubiquitous Languange,来抽象Domain Entity以及Bounded Context。
可以和User Story的方法论结合起来使用,其最大的优点是,这种分析方式即使是non-tech的人,比如产品,业务专家等也能听得懂,也能参与进来。相比较一上来就使用UML画领域模型图而言。
聚合根(Aggregate)
聚合根(aggregate Root)是DDD中的一个概念,是一种更大范围的封装,把一组有相同生命周期、在业务上不可分隔的实体和值对象放在一起考虑,只有根实体可以对外暴露引用,也是一种内聚性的表现。
确定聚合边界要满足固定规则(Invariant),是指在数据变化时必须保持的一致性规则,具体规则如下
- 根实体具有全局标识,最终负责检查规定规则
- 聚合内的实体具有本地标识,这些标识在Aggregate内部才是唯一的
- 外部对象不能引用除根Entity之外的任何内部对象
- 只有Aggregate的根Entity才能直接通过数据库查询获取,其他对象必须通过遍历关联来发现
- Aggregate内部的对象可以保持对其他Aggregate根的引用
- Aggregate边界内的任何对象修改时,整个Aggregate的所有固定规则都必须满足
还是看银行的例子,Account(账 )是CustomerInfo(客户信息)Entity和Address(值对象)的聚合根,Tansaction(交易)是流水(Journal)的聚合根,因为流水是因为交易才产生的,具有相同的生命周期。
这里需要注意的是从其他系统获取的数据是有界上下文(Bounded Context)下的数据,为了弥合Bounded Context下的语义Gap,通常有两种方式,一个是用大领域(Big Domain)把两边的差异都合起来,另一个是增加防腐层(Anticorruption Layer)做转换。
什么是Bounded Context简单阐述一下,就是我们的领域概念是有作用范围的(Context)的,例如摇头这个动作,在中国的Context下表示NO,但是在印度的Context下却是YES 。
服务如何分层/strong>
可以看到在App层和Domain层都有服务(Service),这两个Service如何划分呢,什么样的功能应该放在应用层,什么样的功能应该放在领域层呢/p>
决定一个服务(Service)应该归属于哪一层是很困难的。如果所执行的操作概念上属于应用层,那么服务就应该放到这个层。如果操作是关于领域对象的,而且确实是与领域有关的、为领域的需要服务,那么它就应该属于领域层。
总的来说,涉及到重要领域概念的行为应该放在Domain层,而其它非领域逻辑的技术代码放在App层,例如参数的解析,上下文的组装,调用领域服务,消息发送等。还是银行转账的case为例,
下图给出了划分的建议:
边界上下文和微服务
先来说一下微服务,抛开以Docker为代表的底层容器化技术不看,微服务和我们之前的SOA么有本质区别。
这不是我一个人的观点,关于这个想法我还专门求证了业界大牛Randy Shoup,问他微服务和SOA的区别,下面是他的回答
模型重构
最后我想强调的是,建模不是一次性的工作,也不可能是一次性的工作,业务在演化,随之而来的模型也需要演化和重构,当模型和业务部匹配的时候,你还是要霸王硬上弓的往里面塞,其结果可想而知。
模型统一
建模的过程很像盲人摸象,不同背景人用不同的视角看同一个东西,其理解也是不一样的。比如两个盲人都摸到大象鼻子,一个人认为是像蛇(活的能动),而另一个人认为像消防水管(可以喷水),那么他们将很难集成。双方都无法接受对方的模型,因为那不符合自己的体验。
事实上,他们需要一个新的抽象,这个抽象需要把蛇的“活着的特性”与消防水管的“喷水功能”合并到一起,而这个抽象还应该排除先前两个模型中一些不确切的含义和属性,比如毒牙,或者卷起来放到消防车上去的行为,这就是模型的统一。抽象共性,分别拥有差异性。
统一完的模型也许还不叫大象鼻子,但是已经很接近大象鼻子的属性和功能了,随着我们对模型对象、对业务理解的越来越深入、越来越透彻,我们会不断的调整演化我们的模型,所以建模不是一个one-time off的工作,而是一个持续不断演化重构的过程。
模型演化
世界上唯一不变的就是变化,模型和代码一样也需要不断的重构和精化,每一次的精化之后,开发人员应该对领域知识有了更加清晰的认识。这使得理解上的突破成为可能,之后,一系列快速的改变得到了更符合用户需要并更加切合实际的模型。其功能性及说明性急速增强,而复杂性却随之消失。
这种突破需要我们对业务有更加深刻的领悟和思考,然后再加上重构的勇气和能力,勇气是项目工期很紧你敢不敢重构,能力是你有没有完备的CI保证你的重构不破坏现有的业务逻辑。
实体在演变
以开篇的银行账 为例,假如一开始账 都有withdraw(取钱)的行为,此时只需要在Account上加上withdraw方法就好了。
演变一:
随着业务的发展,我们需要支持ATM账 和Online账 **,而Online账 是不能withdraw的**,此时最差的做法是保持模型不变,而是在withdraw方法中判断如果是OnlineAccount则抛出异常。
这种简单的代码堆砌可以满足业务功能,但是其业务语义完全被掩盖。更好的重构方法应该是将withdraw抽成一个接口IWithdrawable。
演变二:
好的,没有什么可以阻挡业务对变化的向往。现在公司出于安全性的考虑,为新开通的ATMAccount设置了取款上线,超过则不能支取。
简单做法是在IWithdrawable中再加入一个setLimit行为,可是我们并不想改动影响到老的账 业务,所以更好的重构应该是重新写一个ILimitedWithdrawable接口,让其继承老接口,这样老的代码就可以保持不变了。
业务可视化和可配置化
好的领域建模可以降低应用的复杂性,而可视化和可配置化主要是帮助大家(主要是非技术人员,比如产品,业务和客户)直观地了解系统和配置系统,提供了一种“code free”的解决方案,也是SaaS软件的主要卖点。
要注意的是可视化和可配置化难免会给系统增加额外的复杂度,必须慎之又慎,最好是能使可视化和配置化的逻辑与业务逻辑尽量少的耦合,否则破坏了原有的架构,把事情搞的更复杂就得不偿失了。
在可扩展设计中,我已经介绍了我们SOFA架构是如何通过扩展点的设计来支撑不同业务差异化的需求的,那么可否更进一步,我们将领域的行为(也叫能力)和扩展点用可视化的方式呈现出来,并对于一些不需要编码实现的扩展点用配置的方式去完成呢。
当然是可以的,比如还是开篇转账的例子,对于透支策略OverdraftPolicy这个业务扩展点,新来一个业务说透支额度不能超过1000,我们可以完全结合规则引擎进行配置化完成,而不需要编码。
所以我能想到的一种还比较优雅的方式,是通过Annotation注解的方式对领域能力和扩展点进行标注,然后在系统bootstrap阶段,通过代码扫描的方式,将这些能力点和扩展点收集起来上传到中心服务器,然后再通过GUI的方式呈现出来,从而做到业务的可视化和可配置化。大概的示意图如下:

同学可能会问流程要不要可视化,这里要分清楚两个概念,业务逻辑流和工作流,很多同学混淆了这两个概念。
业务逻辑流是响应一次用户请求的业务处理过程,其本身就是业务逻辑,对其编排和可视化的意义并不是很大,无外乎只是把代码逻辑可视化了,在我们的SOFA框架中,是通过扩展点和策略模式来处理业务的分支情况,而我看到我们阿里很多的内部系统将这种响应一次用户请求的业务逻辑用很重的工作流引擎来做,美其名曰流程可编排,实质上往往是把简单的事情复杂化了。
而工作流是指完成一项任务所需要不同节点的连接,节点主要分为自动节点和人工节点,其中每个人工节点都需要用户的参与,也就是响应一次用户的请求,比如审批流程中的经理审批节点,CRM销售过程的业务员的处理节点等等。此时可以考虑使用工作流引擎,特别是当你的系统需要让用户自定义流程的时候,那就不得不使用可视化和可配置的工作流引擎了,除此之外,最好不要自找麻烦。
我曾在银行工作过,亲眼看见过IBM是怎么忽悠银行使用它们的BPM系统,然后把系统弄的巨复杂无比,所以我对工作流引擎的印象并不好,当然也不排除有用的特别合适的案例,只是我还没看见,如果有看见的同学麻烦告诉我一声,学习一下。因为我们现在还没有让用户自定义流程的诉求,所以使用工作流引擎并不在我们现阶段的考虑范围之内。
点击阅读原文
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!