IDDD 实现领域驱动设计-一个简单业务用例的回顾和理解

Fredrica ·
更新时间:2024-11-10
· 788 次阅读

  这篇博文是对《实现领域驱动设计》第一章后半部分内容的理解。   Domain Experts-领域专家   这节点内容是昨天的一个讨论引发的思考。   什么是领域专家?简单来说,是对某一业务领域精通的人,这个人可以是医生、学者、作家、艺术家等等,不管是什么职业,什么身份,只要对某一业务领域精通,都可以称之为领域专家。这样说可能会让你感到茫然,我举一个例子,比如你们软件公司要开发一套快递行业的业务系统,然后你需要到实际企业去了解业务流程等等,暂时把这个实际企业想象成很小(非三通一达),那么你到这个企业第一时间找的是谁呢?准确来说,应该是这个公司的 CEO,因为只有他了解他们公司的业务,毕竟是他创办的公司,CEO 不了解,还有谁还了解呢,那么,这个公司的 CEO 可以看作是领域专家。CEO 一般是蛮忙的,有很多的琐事需要处理,所以,在你和他聊天了解业务的时候,好是先准备一杯咖啡!   当我们开发人员自己开发一套系统的时候,在开发团队之间,领域专家的概念慢慢淡化了,为什么?因为领域专家变成了我们开发人员自己,自己给自己布置业务,然后自己再去完成,这样虽然很高效,因为没有非技术人员的参与沟通,但是这样会造成一些问题,比如,开发人员在思考业务流程的时候,会按照开发人员的思路去理解,比如,一个简单的业务操作描述,开发人员会首先想到的什么呢?一个表单和一个 Button,然后是对这个表单和 Button 操作的具体实现了,等项目开发完成后,需要交付真正的客户去检验,客户让你演示这个业务操作,然后你开始对表单和 Button 进行操作了,说这是业务操作,但是,客户突然来一句:我们不要表单和 Button 操作,UI 需要重新搞,这时候,你傻眼了,因为你所有的内容代码实现都是围绕着表单和 Button。说了这么多,到底是什么意思呢?在这个过程中,你并不了解这个业务操作背后所蕴含的业务含义,首先,业务不是 UI,UI 只不过是业务的一部分体现,有时候,业务仅仅只是领域专家的一段描述,开发人员需要对这个业务描述,进行一点一点的抽离,把术语和操作分离开,然后再和领域专家进行深入的探讨,这个过程可能会花很多的时间,但是是非常重要的,做完这些前期工作,你再去实现业务操作,你会发现,不管 UI 如何变化,这个业务操作的本质是没有发生变化的,也是说你的内部代码不需要进行修改,UI 修改那交给前端工程师可以了,和你没太大关系。总的来说,是不要让 UI 驱动你开发,而是让业务驱动你开发。   对上面的内容,我还需要补充一点,是开发人员需要领域专家,开发人员和领域专家的身份好不要重叠,要不然会造成一系列的问题,还有是,在整个领域驱动设计的过程中,开发人员和领域专家的地位是相同的,不要有任何的轻视心态,要用平等的心态去沟通交流。领域专家的概念,让我想到一个很相似的事,是苹果在开发一个产品的时候,会请很多的非技术人员参与,这些人遍布各行各业,医生、学者、作家、艺术家等等,苹果为什么要请他们,是想让他们参与产品的设计,因为他们是产品的使用者,他们提出的想法是实实在在的用户建议,这个产品开发过程,其实可以看作是领域(产品)驱动设计,这些参与产品设计的非技术人员,可以看作是领域(产品)专家。   一个简单业务用例的回顾和理解   这个简单业务用例描述是这样的:一个 Scrum 模型,我们需要将一个待定项(Backlog Item)提交到冲刺(Sprint)中去。   这是简答的描述,没有经过和领域专家进行深入沟通的,Scrum 是敏捷开发中的概念,这个不说明了,因为我也不懂,你只需要知道上面的操作可以了,一般的实现方式(属性访问): public class BacklogItem extends Entity { private SprintId sprintId; private BacklogItemStatusType status; ... public void setSprintId(SprintId sprintId) { this.sprintId = sprintId; } public void setStatus(BacklogItemStatusType status) { this.status = status; } ... }   客户端调用:   // client commits the backlog item to a sprint   // by setting its sprintId and status   backlogItem.setSprintId(sprintId);   backlogItem.setStatus(BacklogItemStatusType.COMMITTED);   上面的实现过程,完全和上一篇 saveCustomer 的实现方式一样,这样做没什么不可以,因为我也这样干过,只是你会总感觉有哪些不对劲的地方,首先,在实现待定项提交到冲刺这个操作的时候,你首先查看的是 BacklogItem 中的属性,然后是对这个属性进行设置,在这个过程中,你忘记了你实现的是一个行为操作,而不是一个属性赋值操作,这样说来,是不是有点脚本模式开发,还有是如果客户端第二个属性赋值 setStatus 出现了错误,因为第一个 setSprintId 已经成功完成,这个该怎么进行处理,即使有处理,这个操作也完全放在了客户端去完成,像 saveCustomer 一样,如果再增加一个属性赋值操作,你的实现将越改越乱,重要的是,再客户端暴露了 BacklogItem 模型的具体结构,这个应该是要避免的。   我们再来看另一种实现方式: public class BacklogItem extends Entity { private SprintId sprintId; private BacklogItemStatusType status; ... public void commitTo(Sprint aSprint) { if (!this.isScheduledForRelease()) { throw new IllegalStateException( "Must be scheduled for release to commit to sprint."); } if (this.isCommittedToSprint()) { if (!aSprint.sprintId().equals(this.sprintId())) { this.uncommitFromSprint(); } } this.elevateStatusWith(BacklogItemStatusType.COMMITTED); this.setSprintId(aSprint.sprintId()); DomainEventPublisher .instance() .publish(new BacklogItemCommitted( this.tenant(), this.backlogItemId(), this.sprintId())); } ... }客户端调用:   // client commits the backlog item to a sprint   // by using a domain-specific behavior   backlogItem.commitTo(sprint);   将第一种是实现方式出现的问题,再和第二种方式进行比较,你会发现,第二种实现方式完全避免掉了,在开始的时候,我们说了,这是一个简单的业务操作描述,没有和领域专家进行深入探讨和交流,如果进行探讨和交流的话,后详细、准确的业务操作描述,应该是这样:   允许将每一个待定项提交到冲刺中,只有在一个待定项位于发布计划(Release)中时才能进行提交,如果一个待定项已经提交到了另外一个冲刺中,那么需要先将其回收,提交完成时,通知相关客户方。   对于一个详细、准确的业务操作描述,如何进行确定下来,作者进行了如下总结:   对于你目前正在工作的业务领域,思考一下模型中的通用术语和业务操作。   将术语写在白板上。   然后,将项目中所用到的短语也写下来。   与真正的领域专家交流一下,看看哪些词汇是可以改善的(记得带上咖啡哦)。   我们再来分析一下上面第二种实现方式,希望可以抽离出一些对自己有所帮助的理解,首先,读上面的业务操作描述,然后再和实现代码进行对比,你会发现,它们之间的关系是完全契合的,在上一篇中,我们说过,设计是代码,代码是设计,这种设计是一种通用语言,开发人员和领域专家都能懂的通用语言。   在第二种实现的方式中,有两个关键词:commitTo 和 DomainEventPublisher,DomainEventPublisher 是领域事件(Domain Event),这个不要和领域服务(Domain Service)混淆,领域事件我没有使用过,后面再进行学习,你暂时可以把它看作是操作完成后的消息推送者。commitTo 是 BacklogItem 模型中的一个行为,意为提交,你可能会这样想:待定项怎么会有行为呢?它又不是人,我觉得这个很有意思,记得在之前做消息模型设计的时候,一直不确定的一点是发消息这个操作该如何设计?是消息实体的一个行为操作,还是发件人的一个行为操作,又或者是独立出来的一个领域服务(后结果),在这个设计确定的过程中,我们会进行多次讨论,但有一点需要进行明确的是,不只是具有“生命”的实体,才具有行为操作,像消息模型中的操作人,你自然会联想到现实生活中的发件人、收件人等等,认为只有人才会有一些行为操作,但是实际上,在软件系统中,一切的模型都有可能是行为操作,你要摒弃现实生活对你的影响,像上面待定项的提交操作,如果是我设计的话,我会创建一个领域服务进行行为操作,因为,在我的认知中,待定项不具有行为操作,但显然并不是这样,为什么要这样设计?现在还说不出个所以然,以后再慢慢体会。   DDD 并不笨重(测试驱动)   DDD(领域驱动设计)和 TDD(测试驱动开发),这两者有什么关系?我记得在之前的博文中有提到这一点,我的观点是,DDD 和 TDD 可以之间可以产生一些微妙的化学反应,并不一定要强制的去区分它们之间的关系,比如,如果你的 DDD 项目中,使用了 TDD,并不能说明你的项目不是 DDD 模式了,其实,TDD 可以对 DDD 进行一些补充,或者可以让你的项目,在使用 DDD 的时候,变得如鱼得水。关于它们两者的关系,作者简单说明了一下观点:DDD 也倾向于“测试先行,逐步改进”的设计思路,他们可能有细微的区别,但是基本思路是一样的,DDD 采用的是一种“敏捷的”方式进行软件开发的。   可以采取的步骤:   编写测试代码以模拟客户代码是如何使用该领域对象的。   创建该领域对象以使测试代码能够编译通过。   同时对测试和领域对象进行重构,直到测试代码能够正确地模拟客户代码,同时领域对象拥有能够表明业务行为的方法签名。   实现领域对象的行为,直到测试通过为止,再对实现代码进行重构。   向你的团队成员展示代码,包括领域专家,以保证领域对象能够正确地反映通用语言。   具体再说明一下,像上面的待定项提交业务操作,可以完全先写一个测试代码,如下:   [test]   public void  backlogItemCommit() {   ...   }   这个测试代码,其实是领域专家想要的,他不管你是如何具体实现的,他关心的是有没有这个业务操作,以及这个业务操作完成的结果,也是说,测试代码可以很好的反应领域专家所描述的业务操作,那有人可能会说了:你这不是 DDD 了,而是 TDD,表明看上去,好像确实如此,但是不能说写个测试代码是 TDD 开发,而去测试代码并不能反映领域模型,他只是一种辅助方式,你可以把它看作是通用语言的一种,可以帮助你和领域专家进行沟通,也可以加快你的开发速度,又或者可以帮助你完善你的领域模型设计。对应某一业务操作的测试代码,也不是一成不变的,它需要开发人员和领域专家的持续沟通和改进,测试代码是他们进行通用语言的一种表现形势,使用测试代码的好处是,它可以很好的表现业务需求,当然你也可以使用 UI,这些都不过是通用语言的一种罢了。   在读《DDD 并不笨重》这一小节点内容的时候,我是很有感触和共鸣的,因为我在之前短消息开发的时候,曾这样搞过,比如,新建一个与 Domain 对应的 Domain.Tests 项目,这个 Domain.Tests 是你和领域专家进行沟通的一个桥梁。   对于这个节点内容,可能每个人都有自己的理解,如果大家有不同的想法,欢迎探讨交流,记录到这!



领域 领域驱动设计

需要 登录 后方可回复, 如果你还没有账号请 注册新账号
相关文章