我们如何从领域驱动开发当中获益--王德水
领域驱动设计,遇见你之前
我们公司推行和实践敏捷已经很多年了,SCRUM已经成功应用于大部分项目,得益与业界敏捷开发大师以及国内很多优秀工程师的分享和宣传,我们使用了很多优秀的软件开发实践,比如测试驱动开发(TDD),行为驱动开发(BDD), 持续集成(CI)等等为我们带来了很多收益。由于我们公司以做项目为主,虽然这些软件实践确实能很好的提高软件交付质量和效率,但是要想用好这些实践,涉及到的因素很多,常见的如下:
1. Scrum里需要Product Owner, 客户方很少能有一个比较符合Scrum里提到的Product Owner来定义需求为As a role I want to do something so that I can get some benefit.
2. 行为驱动开发BDD对客户方要求更高,客户需要写Specific, Scenaro, Given…When…Then. 我做过一个项目,客户开始写BDD,而且也能写出比较高质量的BDD, 但是客户后来就不写了,觉得写的麻烦。
3. 我们主要使用.NET,虽然大家都熟悉面向对象,熟悉类,接口,继承,封装等等,但是面对一个项目的时候,如何对业务就行合适的抽象,正确使用面向对象依然是非常大的挑战。
4. 由于使用敏捷,不再像之前传统开发过程中有详细的需求说明书(假定那个需求说明书及时更新,且描述准确易于理解), 强调的是可工作的软件,但是很多业务逻辑很难通过界面来体现,虽然有User story, 但是就像第一步PO很难写出符合SMART原则的用户故事,常出现的情况,就是PO说一个需求的大意,程序员就“秒懂”了,最后也确实开发出了经过验收测试的软件,但是因为有测试和Feedback的修复,这一类的需求就“丢失”了。
5. 人员变动,软件行业成员变动是很正常的现象,但是很多小团队一个萝卜一个坑,如果没有好的方法,项目的相关context就丢失了,且不说有的人走的时候没有心思做交接,就算想好好做交接,也只能是最大程度减少项目相关内容不能很好传递。
几年前,当我开始做分公司的时候,刚开始我一直在致力于敏捷的推行,好的软件开发实践的实践,很多同事的软件技能大幅提升,比如熟悉了很多Clean Code的东西,单元测试的重要性和好处,持续集成,Gitflow等的好处。但是有几个问题一直是我在考虑的问题:
1. 如何减少客户反馈的bug, 虽然敏捷强调客户频繁互动和反馈来让错误无限靠近开发的时间,但是能把事情一次做对而不是多次改对依然能大幅提高项目提交速度,也就节省了客户的成本,提高了开发程序员的效率。
2. 人员变更时候的知识传递,虽然清晰的软件架构,整洁的代码,高的单元测试覆盖率能大幅加快新的人员理解项目的速度,但是看用户故事,单元测试以及散落在很多类中的代码(单一职责的时候,我们会有大量类,注意:这里不是说不要单一职责)对中途加入项目的人依然是需要较长时间熟悉项目和代码。
3. 提高开发人员软件技能,提高开发效率从而提高对客户的产出,然后开发人员的薪水自然能有对应的提升是做为一个分公司负责人的第一责任,员工第一嘛!另一个方面,我认为相对简单的项目,比如一些CRUD项目,一些前端项目比如Angular/React的项目越来越没有竞争力。(注:这里并没有任何贬低前端的意思,只是说直接使用已有的这些框架门槛并不是非常高), 我们必须去接一些”不好做”的项目,也就是业务复杂,需求复杂类的,这类项目才能提升程序员的能力而且报价相对较高。
追求软件卓越,原来,我一直强调质量和效率的重要性,但是大家很难理解和应用,突然有一天,我想到(也许从别的地方看到的),软件最重要的是要解决两个问题:
- 做正确的事情 (Do Right Thing)
- 把事情做正确 (Do Thing Right)
初遇领域驱动设计
做正确的事情 (Do Right Thing)
把事情做正确 (Do Thing Right)
一直在我脑海里,然后简单的CRUD项目越来越没竞争力,程序员的报价难以提高,自然薪水就会有瓶颈,而我也想解决这样的问题,突然有一天我看到了另一句话:我选择做这件事,不是因为他简单,而是因为他难,这句话对我触动很大。我的脑海里就一直萦绕着“简单”,“复杂”,“麻烦”, 于是我要做的事情就是下面三个:
1. 做正确的事情 (Do Right Thing)
2. 把事情做正确 (Do Thing Right)
3. 做难的事情 (Do Hard Thing)
我就在网上搜索Complex, Software 等,一本叫做Domain Driven Design的这本书出现在我面前,Domain Driven Design这个词语早就听说过,但是更吸引我的是副标题 “Tackling complexity in the heart of software”, 我就大概看了一下书,里面的战略设计正好提供了解决复杂业务的方法,统一语言,Bounded Context, 界限上下文,设计就是代码,代码就是设计等等。 我相信这正是解决 做难的事情,做正确的事情 但是对战略有些了解,怎么去实现呢?一直没有一个好的例子来帮助大家如何使用领域驱动设计,直到一本《实现领域驱动设计》这本书的出现,才真的让我们有了打通任督二脉的机会。
实践领域驱动设计
有了《领域驱动设计》和《实现领域驱动设计》两大神器,也只是向美女要了个联系方式和家庭住址而已,中间还隔了一个漫长的日落和日出,没有正式的项目,我们永远是在岸上游泳,虽然我们也在项目里开始或多或少使用了领域的一些概念,但是我们应该知道基于数据驱动(Database Driven) 是很难成功应用领域驱动设计的。正好这个时候公司来了一个项目(客户是Fortune Global 500), 他们的架构师指定要求使用领域驱动设计,这让我非常兴奋,让我们有机会对复杂业务进行领域驱动开发的实践。
就像实现领域驱动力说的一样,领域驱动主要有两大块儿战略设计和战术设计。
战略设计 (Do Right Things)
Ubiquitous language
领域驱动开发让业务专家(Domain Expert)和开发人员一起来梳理业务,而双方有效沟通的方式是使用通用语言,在这个项目里,一开始我们就定义了很多词汇表, 就是我们自己的通用语言。
Bounded Context 和 Domain
有了通用语言,词汇表 每一个词汇一定是有边界的,不同的边界内是不一样,比如你爱人在你家这个Bounded Context是你的Wife, 但是如果她是一个老师,那么在学校这个边界里就是一个Teacher. 我们经过多次讨论,采取的方法是拆成多个子系统(Bounded Context,是不是很像现在的微服务?),每个子系统进行自治。
随后我们把一个个业务抽象为领域对象(Domain Model), 每一个Domain对领域进行自治。而模型里的属性和行为表达为业务专家都可以理解的代码,用比如Job.Publish(). 虽然这里面最终产生了聚合根、实体、值对象等,但是我们和业务专家沟通的时候尽量不要说这些词汇,比如我们可以说, 在招聘这块儿,职位是不是必须经过公司进行管理,那样我们就知道 Job是属于公司这个聚合根。 对领域进行“通用”(类名,方法名等都用自然语言表达)建模,业务人员可以直接读懂我们的代码,从而可以知道是否表达了业务需求。
战术设计 (Do Things Right)
在战术设计方面,由于业务行为和规则都在领域里,而且系统被拆分成多个子系统,这对技术实现上带来了非常大的挑战,尤其是大部分人都是有牢固的基于数据驱动开发的思想。 技术上有不同实现方式,但是一开始我们选择了“最佳实践”(实现领域驱动设计),也就是使用了Event Source和CQRS, 但是这条路是陡峭的。
Event Sourcing
Event Sourcing 就是我们不记录数据的最终状态,我们记录对数据的每一次改变(Event),而读取的时候我们把这些改变从头再来一遍来取得数据状态,比如你有100块钱,现在剩下10块了,我们记录的不是money.total=10, 而是记录你每一次取钱的记录,然后从100块开始一步步重放你取钱的过程,来得到10.
一开始,我们写的过程中,时常回想起数据驱动的好,(每次开始一个新东西的时候,是不是很熟悉的感觉?),觉得用Event Sourcing各种麻烦,直到后来随着系统的复杂性不断增加,我们才感觉到带来了非常大的好处, 这个随后单独来说。
CQRS
由于使用了EventSourcing, 对数据查询,尤其是跨业务(aggregate)的查询非常麻烦,很难像关系数据那样有查询优势,CQRS是解决这一问题非常好的方法,CQRS让查询和写入分开,把界面需要查询的数据进行原样写入,原样的意思就是界面显示什么样的,就提前保存成什么样的,类似于原来的缓存,没有任何join操作,这样查询是非常高效的。
实践领域驱动过程中面临的技术挑战
最大的挑战当然是战略设计部分 就是正确的划分Bounded Context和领域建模,这个部分这里难以几句话说清楚,只能多实践,多向大师学习,比如试试Event Storming的方式。
然后,如果团队没有任何领域驱动开发的经验,千万不要低估技术部分的挑战,并不是很多人说的技术部分不重要,如果实现不好,领域驱动很难落地。我们遇到一些典型问题,当然后来都很好的解决。
- 开发人员认为EventSourcing不重要,比如,原来你要发布一个Job, 你可能只需要改一个属性Job.Status=”Published”, 但是现在你需要定义一个JobPublishedEvent的事件,很多时候一次改变需要定义很多事件,比如CompanyNameChangedEvent, CompanyEmployeeAddeedEvent. 最重要的是事件的粒度如何定义?
- 由于根据Bounded Context拆分成一个个子系统,系统之间的交互比较麻烦。原来在一个Controller里直接调用不同的Repository来改变数据的方式就很不适用了。
- 由于使用CQRS,查询必须要单独保存QueryModel, 这相对传统的数据库驱动的开发方法,写和读都是同一个数据库更加麻烦。
- 事件的版本管理,比如事件改名,删除和增加都需要考虑重放事件重建领域对象的影响。
- CQRS如何保证数据的及时性和一致性,比如我在一个公司详细页面修改了一个公司名字,然后点击保存按钮导航到公司列表页面,这个时候QueryModel可能还没有更新过来,这些如何解决一致性的问题。
领域驱动开发如何让我们和客户共同获益的
1. 做正确的事情(Do Right Things): 领域专家高效的和团队沟通,确保建立了正确的反映业务规则的模型,而开发人员有了直接可以使用的代码,而且可以因为Domain有了数据和行为,非常方便的进行单元测试,因为Domain不依赖第三方的数据存储等,可以确保实现了业务。
2. 大大提高了沟通的效率,我们知道一图胜千言,而对开发人员来说,少废话,Show me the code! 不但代码对程序员更容易读,而且Code(领域对象)就是最新的需求. 可以跑起来的需求。
3. 大大提高新成员进入项目的速度,最主要的是看领域模型以及对领域模型的测试,几乎就知道了系统的所有的业务规则。
4. 领域驱动的技术部分给系统增加功能或扩展带来了极大的遍历,举几个例子:
a)由于使用了事件溯源,我们很容易查询历史数据。我们只需要指定一个时间点,我们重放事件的时候重放到这个时间点就可以了。
b)操作日志,原来如果我们想记录操作日志我们代码里遍布都是Log, 而现在我们只需要重发事件,想看什么日志就看什么日志,而这些就只需要我们回放数据库存储的事件流就可以了。
c)系统之间的通信,我们只需要发布事件就可以了,其它系统订阅我们的事件就可以,我们和其它系统之间没有直接依赖。
d)大大提高了系统增加新功能的方便性,很多时候增加新功能就是订阅事件就可以了。
e)CQRS, Query model极大的提高了系统的查询性能,而且当我需要新的界面的时候,我不需要对写入端代码进行任何修改,包括类文件都不用修改,是不是符合对修改关闭,对扩展开放(OCP)? 我们只需要建一个类似新的EventHandler,然后重放对应的事件就可以了。
f)因为使用了事件溯源,系统之间通过事件集成,比如通过消息队列发布和订阅事件,这可以大大增加系统的抗压能力,我们可以把事件放入队列,后续处理系统即使不能及时处理也不会让前端系统崩溃。
g)系统性能大福提高,在写入端只有插入操作,没有修改操作,在读取端只有Read操作,那么何须锁表,何须开启事务?由此一来,输出存储和读取的瓶颈可以大大缓解。
5. 开发人员可以更集中的处理业务,由于一切都是事件,实现玩基础库后,开发人员可以忽略数据存储,大部分时间都是在写业务代码,不关心数据怎么存储,数据存储部分就俩操作
AggregateRoot.Get(id),
AggregateRoot.Save(),
EventPublish.Pubish(CompanyNameChangedEvent),
而事件订阅端只需要增加一个EventHandler就可以了。
6. 系统很好进行了解耦,业务逻辑集中在领域中,不会像之前的开发里面业务逻辑充斥在很多地方,修改一些功能的时候,不得不如履薄冰,生怕哪里给破坏了,或者哪里没考虑全。
7. 不用过于担心开发人员,尤其初级开发人员不正确的代码遍布系统多个部分,对其它功能的影响可以大大减低,Review代码其实主要Review业务实现的单独的类,不用担心很多技术实现部分不正确,因为基础库写好了,这样可以适当均衡团队成员组成来降低项目开发成本。
最后,系统更加容易修改和增加新功能,不正好支持了敏捷开发的“拥抱变化”吗?
总结
领域驱动开发好处多多,概念比较多,门槛相对较高,对人员要求较高,团队里至少需要有领路人,不然代价会比较大。 尤其慎用Event Sourcing, 而领域驱动尤其适合业务相对复杂的项目。 对那些很小的项目,CRUD仍然是好的选择。
Add new comment