为什么要领域建模
维护过企业级业务系统的同学,基本上没有一个不抱怨业务代码烂的,过程式的面条代码充斥着屏幕,程序员的心力和体力都经受着极大的考验,怎么破?
DDD革命
DDD革命性在于,领域模型准确反映了业务语言,而传统J2EE或Spring+Hibernate等事务性编程模型只关心数据,这些数据对象除了简单setter/getter方法外,没有任何业务方法,被比喻成贫血模式。
以银行账号Account为案例,Account有“存款”,“计算利息”和“取款”等业务行为,但是传统经典的方式是将“存款”,“计算利息”和“取款”行为放在账号的服务AccountService中,而不是放在Account对象本身之中。我们不能因为用了计算机,用了数据库,用了框架,业务模型反而被技术框架给绑架了,就像人虽然是由母亲生的,但是人的吃喝拉撒母亲不能替代,更不能以母爱名义剥夺人的正常职责行为,如果是这样,这个人就是被母爱绑架了。
DDD不是银弹
软件的世界里没有银弹,是用事务脚本还是领域模型没有对错之分,关键看是否合适。就像自营和平台哪个模式更好?答案是都很好,所以亚马逊可以有三方入住,阿里也可以有自建仓嘛。
实际上,CQRS就是对事务脚本和领域模型两种模式的综合,因为对于Query和报表的场景,使用领域模型往往会把简单的事情弄复杂,此时完全可以用奥卡姆剃刀把领域层剃掉,直接访问Infrastructure。
我个人也是坚决反对过度设计的,因此对于简单业务场景,我强力建议还是使用事务脚本,其优点是简单、直观、易上手。但对于复杂的业务场景,你再这么玩就不行了,因为一旦业务变得复杂,事务脚本就很难应对,容易造成代码的“一锅粥”,系统的腐化速度和复杂性呈指数级上升。目前比较有效的治理办法就是领域建模,因为领域模型是面向对象的,在封装业务逻辑的同时,提升了对象的内聚性和重用性,因为使用了通用语言(Ubiquitous Language),使得隐藏的业务逻辑得到显性化表达,使得复杂性治理成为可能。
边界上下文和微服务
先来说一下微服务,抛开以Docker为代表的底层容器化技术不看,微服务和我们之前的SOA么有本质区别。
这不是我一个人的观点,关于这个想法我还专门求证了业界大牛Randy Shoup,问他微服务和SOA的区别,下面是他给我的回答
那么如何划分系统,才能得到一个比较合适的粒度,不会太粗,也不会太细呢。此时我们可以考虑DDD的战略设计,即从战略角度整体描述业务领域全貌,然后通过边界上下文将不同的实体归类到相对应的域里面。
比如在CRM领域,我们按照下面的战略设计图,我会自然的把CRM系统划分成销售服务,组织权限服务,营销服务,售卖服务。
模型重构
最后我想强调的是,建模不是一次性的工作,也不可能是一次性的工作,业务在演化,随之而来的模型也需要演化和重构,当模型和业务部匹配的时候,你还是要霸王硬上弓的往里面塞,其结果可想而知。
模型统一
建模的过程很像盲人摸象,不同背景人用不同的视角看同一个东西,其理解也是不一样的。比如两个盲人都摸到大象鼻子,一个人认为是像蛇(活的能动),而另一个人认为像消防水管(可以喷水),那么他们将很难集成。双方都无法接受对方的模型,因为那不符合自己的体验。事实上,他们需要一个新的抽象,这个抽象需要把蛇的“活着的特性”与消防水管的“喷水功能”合并到一起,而这个抽象还应该排除先前两个模型中一些不确切的含义和属性,比如毒牙,或者卷起来放到消防车上去的行为,这就是模型的统一。统一完的模型也许还不叫大象鼻子,但是已经很接近大象鼻子的属性和功能了,随着我们对模型对象、对业务理解的越来越深入、越来越透彻,我们会不断的调整演化我们的模型,所以建模不是一个one-time off的工作,而是一个持续不断演化重构的过程。
模型演化
世界上唯一不变的就是变化,模型和代码一样也需要不断的重构和精化,每一次的精化之后,开发人员应该对领域知识有了更加清晰的认识。这使得理解上的突破成为可能,之后,一系列快速的改变得到了更符合用户需要并更加切合实际的模型。其功能性及说明性急速增强,而复杂性却随之消失。这种突破需要我们对业务有更加深刻的领悟和思考,然后再加上重构的勇气和能力,勇气是项目工期很紧你敢不敢重构,能力是你有没有完备的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,这样才能更好的表达业务,其演化过程如下:
业务可视化和可配置化
好的领域建模可以降低应用的复杂性,而可视化和可配置化主要是帮助大家(主要是非技术人员,比如产品,业务和客户)直观地了解系统和配置系统,提供了一种“code free”的解决方案,也是SaaS软件的主要卖点。要注意的是可视化和可配置化难免会给系统增加额外的复杂度,必须慎之又慎,最好是能使可视化和配置化的逻辑与业务逻辑尽量少的耦合,否则破坏了原有的架构,把事情搞的更复杂就得不偿失了。
在可扩展设计中,我已经介绍了我们SOFA架构是如何通过扩展点的设计来支撑不同业务差异化的需求的,那么可否更进一步,我们将领域的行为(也叫能力)和扩展点用可视化的方式呈现出来,并对于一些不需要编码实现的扩展点用配置的方式去完成呢。当然是可以的,比如还是开篇转账的例子,对于透支策略OverdraftPolicy这个业务扩展点,新来一个业务说透支额度不能超过1000,我们可以完全结合规则引擎进行配置化完成,而不需要编码。
所以我能想到的一种还比较优雅的方式,是通过Annotation注解的方式对领域能力和扩展点进行标注,然后在系统bootstrap阶段,通过代码扫描的方式,将这些能力点和扩展点收集起来上传到中心服务器,然后再通过GUI的方式呈现出来,从而做到业务的可视化和可配置化。