DDD案例(2):从领域分析到代码实现

小编按:不少朋友到处找寻一个DDD的案例而不可得,这里独家提供一枚,只要你有耐心好好的读、好好的消化。别忘了转发、在看。

20.3.3EAS的架构映射

通过对EAS展开全局分析,我们已经获得了EAS系统的价值需求和业务需求。接下来,我将延续全局分析阶段输出的这些成果,开展架构映射,获得遵循领域驱动架构风格的架构映射战略设计方案。

1.映射系统上下文

全局分析阶段确定了EAS的利益相关者。通过对目标系统的分析,可以得出参与系统上下文的用户包括:集团决策者、市场部、人力资源部、项目管理部、子公司、服务中心、财务、员工。

整个目标系统就是EAS系统。在确定系统范围时,系统的当前状态告诉我们:集团已有OA系统提供部门之间的流程协作与消息通知,它是EAS系统的伴生系统。系统的当前状态虽然还告知软件学院和招聘网站的简历会作为集团的人才储备库,却并未确认EAS系统是否需要和软件学院与招聘网站集成。通过招聘流程服务蓝图可以确知,这些简历信息需要招聘专员手工录入EAS系统,因此,EAS系统的伴生系统并不包含软件学院和招聘网站。客户合作流程服务蓝图中的内部支持者包含了薪资管理系统,调用它提供的服务接口可以获得员工薪资,以便财务进行财务核算,这说明薪资管理系统也是EAS系统的伴生系统。EAS系统的系统上下文如图20-27所示。

56050c8ebcc4da2d27908e9366c2486e.png

确定了系统上下文,就确定了EAS系统的解空间,同时也确定了位于解空间之外的伴生系统。

2.映射限界上下文

在全局分析阶段,EAS系统的问题空间被分解为业务场景下的各个业务服务。遵循业务维度的V型映射过程,需要针对这些业务服务进行归类和归纳,获得各个业务主体,再根据亲密度和限界上下文的特征对业务主体的边界进行调整,并运用验证原则验证业务边界的合理性。之后,根据管理维度的工作边界、技术维度的应用边界逐步对限界上下文做进一步的调整。

1)归类与归纳

V型映射过程从识别业务服务的语义相关性功能相关性开始。

语义相关性主要针对业务服务名的名词。例如,在客户活动业务流程中,诸如“创建合同”“添加附加合同”“指定合同承担者”等业务服务都包含“合同”一词,可归类到同一个业务主体,如图20-28所示。

功能相关性则从业务服务的业务目标进行归类,如图20-29中的业务服务都与市场管理的业务目标有关,可归类到同一个业务主体。

通过语义相关性和功能相关性对业务服务进行归类后,可以进一步针对它们表达的概念建立抽象,寻找共同特征完成对类别的归纳。例如,图20-29中的业务服务涵盖了市场需求、需求订单、客户需求等领域概念,它们都可以提炼为一个更高的抽象层次——市场。这也是图中业务主体名称的由来。从中也可发现,虽然归类与归纳属于V型映射过程的两个环节,但这两个环节并没有清晰的界限,在进行归类时,可以同时进行归纳。

9eb2860512fb2910ba8a6cb2bac95803.png

无论是寻找领域概念的共同特征,还是识别领域行为的业务目标,都需要一种抽象能力。在进行抽象时,可能出现“向左走还是向右走”的困惑,因为抽象层次的不同,抽象的方向或依据亦有所不同。这时就需要做出设计上的决策

例如对识别出来的“员工”与“储备人才”领域概念,可以抽象出“人才”的共同特征,得到图20-30所示的人才业务主体。从共同的业务目标考虑,储备人才又是服务于招聘和面试的,似乎归入招聘业务主体才是合理的选择,如图20-31所示。

b8f4c2c4686d96a30752c49426aa024a.png

还有第三种选择,就是将储备人才单独抽离出来,形成自己的储备人才业务主体,如图20-32所示。

该如何抉择呢?我认为须得思考识别业务主体的目的。业务主体是架构映射过程的中间产物,并非最终的设计目标。业务主体是对业务服务的分类,为限界上下文的识别提供了参考。因此,选择人才主体,还是招聘主体,或者单独的储备人才主体,都需要从限界上下文与领域建模的角度去思考。如果暂时分辨不清楚,可以先做出一个初步选择,待所有的业务主体都归纳出来之后,在梳理业务主体的边界时再决定。

识别业务主体不是求平衡,更不是为了让设计的模型更加好看。业务主体是根据业务相关性进行归类和归纳的,必然会出现各个业务主体包含的业务服务数量不均等的情形。例如,与项目管理有关的业务主体,包含的业务服务数量非常不均匀。项目业务主体的业务服务数量最多,如图20-33所示。

705c645c549bb316a78a3e6fa812affe.png

图20-32 储备人才业务主体                                                  图20-33 项目业务主体

问题业务主体的业务服务数量也比较多,如图20-34所示。

项目成员业务主体的业务服务最少,如图20-35所示。

bf78de348a020008a6f886c01490dfc2.png

不用担心业务主体的这种不均等。遵循V型映射过程,针对业务服务进行业务相关性分析获得的业务主体只是候选的限界上下文,还需要我们根据业务服务的亲密度进一步梳理业务主体的边界。

通过对业务相关性的归类和归纳,初步获得图20-36所示的业务主体视图。

c06b57d71043626433daccc3a234fecb.png

2)亲密度分析

一旦确定了各个业务主体的业务服务,就可以通过分析亲密度的强弱来调整业务服务与业务主体之间的关系。例如,“从储备人才转为正式员工”业务服务究竟属于储备人才主体,还是属于员工主体?虽说该业务服务需要同时用到储备人才和员工的领域知识,但由于其服务价值是生成员工记录,储备人才的信息仅仅作为该领域行为的输入,因此它与员工业务主体的亲密度显然更高。

亲密度分析也可以判断业务服务的归类是否合理。例如,项目主体中的创建迭代、开始迭代等业务服务牵涉到迭代这一领域概念,它与项目概念固然存在较亲密的关系,但问题概念与迭代概念之间的亲密关系也不遑多让,进一步,项目成员与问题之间的亲密关系也有目共睹。划分业务主体时,项目计划、迭代等概念被放到了项目主体,问题却没有被一并放入,显得领域知识的分配有些失衡。用亲密度来解释,就是迭代与问题之间的亲密度几乎等于项目与项目计划、迭代之间的亲密度,而问题与项目之间的亲密度却要明显低于问题与迭代之间的亲密度。为了保证领域知识分配的均衡,可以考虑两种设计方案:

q将项目主体、问题主体和项目成员主体合并,形成项目上下文;

q将项目计划、迭代等造成亲密度不均匀的领域概念单独剥离出来,定义独立的项目计划业务主体。

3)判断限界上下文的特征

知识语境看,合同上下文与员工上下文都具有合同(contract)领域概念,二者在各自的业务主体边界内,代表了各自的领域概念。前者为商务合同,后者为劳务合同。

员工主体与储备人才主体存在几乎完全相同的领域模型,如图20-37所示。

10289b61d1b1a00fb9a5dff1a724b10a.png

6a3544049e4b5caa3367c3e398f0434b.png

两个模型除了员工Employee与储备人次Candidate的名称不同,几乎是一致的。我们是否可以把这两个概念抽象为Talent,由此来来统一领域模型?如图20-38所示。

面向对象设计思想鼓励这样的抽象,以避免代码的重复。然而,若从领域模型的知识语境看,这是两个完全不同的领域模型:员工属于员工管理的领域范畴,储备人才并非正式员工,是招聘的目标。以模型中的项目经验Project Experience为例,虽然员工和储备人才的项目经验具有完全相同的属性,但它们面向的关注点是迥然不同的,如市场人员就完全不关心储备人才的项目经验。

业务能力看,员工与储备人才之间存在清晰的界限,提供了各自独立的业务能力,一个服务于员工的日常管理,一个服务于人才的招聘。虽说“从储备人才转为正式员工”需要二者的结合,但在储备人才转为正式员工之后,二者就不存在任何关系了。

因此,员工和储备人才这两个领域模型应该放在不同的限界上下文。这也体现了领域驱动设计与面向对象设计之间的差异。

ps:在具体的业务语境中,员工涉及的相关业务远大于储备人才,在张老师举例的业务需求中未展开。比如员工对于办公的诉求,拥有电脑、内网访问权限、分配座位、付出劳动获得薪酬和福利等。员工属于HR域,但和办公域强相关,而储备人才显然只服务于招聘活动。

4)运用验证原则

运用正交原则单一抽象层次原则,可以进一步确定限界上下文业务边界的合理性。

例如,为何选择将问题而非项目成员归入项目上下文?除了因为项目成员与组织之间存在黏性,在概念上,问题其实属于项目的子概念,在层次上处于“劣势”地位。遵循单一抽象层次原则,项目与问题并不在同一个抽象层次。相反,以招聘业务主体和储备人才业务主体为例,二者就没有非常明显的“上下级”层次关系。它们之间的关系或许比较亲密,却处于平等的层次。

在运用单一抽象层次原则时,业务主体的命名会影响我们对主体关系的判断。如果命名过于抽象,就可能使得过高的抽象隐隐然包含别的主体。以市场业务主体和合同业务主体为例,市场的抽象层次明显高于合同(即合同的概念也应属于市场的范畴),故而带来两个设计选择:要么将合同业务主体纳入市场业务主体,进而形成一个市场上下文,要么将市场业务主体命名为订单,而订单与合同显然处于同一层次。

正交原则警醒了设计者:限界上下文之间不能存在重叠内容。为何需要单独分离出文件共享上下文与通知上下文?因为诸如员工、储备人才、合同等业务主体都需要调用文件上传下载功能,项目、合同、招聘等业务主体都需要调用消息通知功能。如果不分离出来,一旦文件上传下载或者消息通知的实现有变,就会影响到相关的业务主体,造成“霰弹式修改”[6]的代码坏味道,违背了正交原则。

运用单一抽象层次原则与正交原则,对前面获得的业务主体进一步梳理和验证,可初步获得如图20-39所示限界上下文的草案。

3774e616d5dda8b184696ad98c296ae0.png

在图20-39中,订单上下文与项目上下文就是在对业务主体的边界进行梳理,并通过验证原则验证后调整的结果。

5)工作边界的识别

从工作边界识别限界上下文是一个长期过程,其中,也牵涉到需求变更和新需求加入时的柔性设计[8]168

如前所述,限界上下文之间是否允许进行并行开发可以作为判断工作边界分配是否合理的依据。在EAS限界上下文草案中,我发现报表上下文与客户、合同、订单、项目、员工等上下文都存在非常强的依赖关系。如果这些上下文没有完成相关的特性功能,就很难实现报表上下文。由于报表上下文的诸多统计报表与各自的业务强相关,如查看项目统计报表用例只需统计项目的信息,因此可以考虑将这些用例放到与业务强相关的限界上下文中。

结合工作边界和业务边界,我认为工作日志业务主体的边界过小,且从业务含义看,可将其视为员工管理的一项子功能,因而决定将工作日志合并到员工上下文,同样地,也将考勤业务主体合并到员工上下文。这实际也遵循了验证原则中的奥卡姆剃刀原则

储备人才和招聘之间的关系类似于工作日志和员工之间的关系,我最初也想将储备人才合并到招聘上下文中。然而,客户对需求的反馈打消了这一决策考量。因为该软件集团旗下还有一家软件学院,集团负责人希望将软件学院培养的软件开发专业学生也纳入企业的储备人才库中。这一需求影响了储备人才的管理模式,也扩充了储备人才的领域内涵,使它与招聘领域形成了正交关系,为它的“独立”增加了有力的砝码。

一些限界上下文之间的依赖无法通过需求分析直观呈现出来,这就有赖于上下文映射对这种协作(依赖)关系的识别。一旦明确了这种协作关系,定义了服务契约,就可以利用Mock或Stub解除开发的依赖,实现并行开发。

通过工作边界识别限界上下文的一个重要出发点是激发团队成员对工作职责的主观判断。这种边界也就是第9章提及的针对团队的“渗透性边界”。团队成员需要对自己负责开发的需求抱有成见,尤其是在面对需求变更或新增需求的时候。

在EAS系统的设计开发过程中,客户提出了增加员工培训的需求。该需求要求人力资源部能够针对员工的职业规划制订培训计划,确定培训课程,实现对员工培训过程的全过程管理。考虑到这些功能与员工上下文有关,我最初考虑将这些需求直接分配给员工上下文的领域特性团队。然而,团队的开发人员提出:这些功能虽然看似与员工有关,但实际上是一个完全独立的培训领域,包括了培训计划制订、培训提名、培训过程管理等业务知识,与员工管理的业务是正交的。最终,我们选择为培训建立一个专门的领域特性团队,同时引入培训上下文。

类似文件共享和通知这样一些属于支撑子领域或者通用子领域的限界上下文,可能具有并不均匀的粒度,且互相之间又不存在关联。此时,可维持限界上下文的业务边界不变,然后视粒度酌情将它们分配给一个或多个领域特性团队。如果该支撑功能需要团队成员具备一定的专业知识,也可将它单独抽离出来,建立专门的组件团队。如果它提供的功能具有普遍适用性,不仅可以支撑目标系统,还可以支持组织内其他软件系统,就可以考虑将其演进为企业范围内的框架或平台。这些框架和平台就不再属于目标系统的范围了(在系统上下文边界之外)。

根据需求变化以及对团队开发工作的分配,我们调整了限界上下文,如图20-40所示。

在图20-40中,将工作日志与考勤合并到了员工上下文,同时为了应对新需求的变更,增加了培训上下文,并暂时去掉了报表上下文。之所以说“暂时”,是因为还需要对其做一些技术层面的判断。

1c638cce7ea4b85bb11dd1d18814232b.png

6)应用边界的识别

对应用边界的识别,就是从技术维度考量限界上下文,包括考虑系统的质量属性、模块的复用性、对需求变化的应对和处理遗留系统的集成等。

我们与客户决策层一起确认了报表功能的需求,客户希望统计报表能够准确及时地展现历史和当前的人才供需情况。统计报表功能直接影响了目标系统的愿景,是系统的核心功能之一,需要花费更多精力来明确设计方案。通观与统计报表有关的业务服务,除了与职能部门管理工作有关的统计日报、周报和月报,报表的统计结果实际上为集团领导进行决策提供了数据层面的辅助支持。要提供准确的数据统计,就需要对市场需求、客户需求、项目、员工、储备人才、招聘活动等数据做整体分析,也就需要整个系统核心限界上下文的数据支持。倘若EAS的每个限界上下文并未采用微服务这种零共享架构,整个系统的数据就可以存储在一个数据库中,无须进行数据的采集和同步即可支持统计分析。另一种选择是引入数据仓库,采用诸如ETL等形式完成对各个生产数据库和日志文件的采集,经过统一的数据治理后为统计分析提供数据支持。

在分析工作边界时,我考虑到报表上下文与其他限界上下文之间存在强依赖关系,无法支持并行开发,因而将该上下文的功能按照业务相关性分配给其他限界上下文。如今,通过技术分析得知,虽然依赖仍然存在,但该上下文更多地体现了“决策分析”的特定领域。最终,我决定保留该限界上下文,并将其更名为决策分析上下文。

在考虑通知上下文的实现时,基于之前确定的系统上下文,EAS系统要与集团现有的OA系统集成。我们了解了OA系统公开的服务接口,发现这些接口已经提供了多种消息通知功能,包括站内消息、邮件通知和短消息通知,没有必要在EAS系统中重复开发通知功能。那么,通知上下文是否就没有存在的必要呢?一旦去掉通知上下文,与OA系统集成的功能又该放在哪里?领域驱动设计建议将这种与第三方服务集成的功能放在防腐层,可EAS系统中的多个限界上下文都需要调用该功能,会形成防腐层的重复建设。为了满足功能的复用性,可以为它单独创建一个限界上下文。为了说明其意图,将它更名为OA集成上下文。

最终,得到图20-41所示的限界上下文。

5394d18b3882653d3449db86d97110a2.png

3.上下文映射

确定了目标系统的限界上下文之后,即可通过业务服务获得的服务序列图确定限界上下文之间的协作关系,从而确定上下文映射模式,设计出服务契约。

1)创建市场需求

创建市场需求业务服务属于订单上下文,它拥有的领域知识已经足以满足该业务服务的需求,无须求助于其他的限界上下文,因此在本业务服务中,没有上下文映射。服务序列图如图20-42所示。

92b036bd46da8d801f8a84c61f9c2683.png

虽然创建市场需求没有多个限界上下文参与协作,但为其绘制服务序列图仍有必要,因为可以通过它驱动出创建市场需求的服务契约。

2)归档合同

归档合同业务服务属于合同上下文,具备合同相关的领域知识,但不具备上传文件的业务能力,需要求助于文件共享上下文,服务序列图如图20-43所示。

文件共享上下文作为支撑子领域的限界上下文,主要提供了文件上传与下载的功能。它具有的领域知识还包括针对不同类型的文档维护了服务器文件存储的路径映射,故而参与协作的是北向网关的应用服务。形成的上下文映射图如图20-44所示。

480924698b002a03cffd3fe959d6cb14.png

3)添加项目成员

添加项目成员业务服务属于项目上下文,拥有与项目相关的所有领域知识和业务能力,但并不具备发送通知的业务能力,也不知道该如何将当前项目的信息添加到员工的项目经验中,因此需要分别求助于OA集成上下文和员工上下文。服务序列图如图20-45所示。

7d14a16ae7452d8baac9e4527d12e359.png

OA集成上下文要实现的功能都与通知有关。无论是短信通知、邮件通知还是站内通知,都没有副作用,且允许以异步形式调用,适合使用事件的调用机制,因而为其选择发布者/订阅者模式。选择该模式既可以解除OA集成上下文与大多数限界上下文之间的耦合,又能较好地保证EAS系统的响应速度,减轻主应用服务器的压力。不足是需要增加一台部署消息队列的服务器,并在一定程度增加了架构的复杂度。如图20-45所示,项目管理者在添加了项目成员之后,会向事件总线发布TeamMemberAdded应用事件,并由OA集成上下文的事件订阅者订阅该事件。形成的上下文映射图如图20-46所示。

为主要的业务服务绘制服务序列图,深层次地思考各个限界上下文如何参与到每个业务服务、它们又该如何协作、应该采用什么样的上下文映射模式,就可以得到整个EAS系统的上下文映射图,如图20-47所示。

6db95adb96302286873eebd7e9e1aa78.png

上下文映射图不仅说明了限界上下文之间的协作关系、彼此间采用的团队协作模式,也可以作为服务契约设计的参考和补充。

4.服务契约设计

服务序列图驱动我们获得了消息定义,由此可以驱动出服务契约。如果目标系统规模大,限界上下文数量多,可以为每个限界上下文定义一个服务契约表。服务契约表除了体现了整个项目的服务契约定义,同时也为领域特性团队提供了设计约束。

表20-5列出了EAS系统的部分服务契约。

d2f3d91b8081de34ce6229eb2b60b0f6.png

f364adda4c64c51f09a717b9edca4f45.png

除了UI作为下游发起服务请求,表20-5列出的服务契约都标记了上下文映射模式。表20-5的“服务方法”列给出了类和方法的明确定义,也指出了方法参数的形参名和类型。若有返回值,也需要给出返回值类型。

在确定EAS的限界上下文时,我并没有明确指出限界上下文的通信边界。边界取决于质量属性的要求,自然也需要权衡库和服务的优缺点。在无法给出必须跨进程通信的证据之前,应优先考虑进程内通信。EAS系统作为一个企业内部系统,对并发访问与低延迟的要求并不高。可用性固然是一个系统该有的特质,但EAS系统毕竟不是“生死攸关”的一线生产系统,即使短时间出现故障,也不会给企业带来致命的打击或难以估量的损失。既然如此,我们应优先考虑将限界上下文定义为进程内的通信边界。唯一的例外是OA集成上下文被定义为进程间通信,因为它需要跨进程调用OA系统。这种方式一方面解除了OA系统上下文与大多数限界上下文之间的耦合,另一方面也能够较好地保证EAS系统的响应速度,减轻主应用服务器的压力。

因此,对于采用客户方/供应方模式的限界上下文,作为供应方的上游只需通过应用服务对外公开服务契约,如员工上下文的EmployeeAppService应用服务公开了追加项目经验的服务契约,这一设计满足菱形对称架构北向网关的要求。对采用发布者/订阅者模式的限界上下文而言,作为发布者的限界上下文也通过应用服务发布事件,如项目上下文通过TeamAppService应用服务发布了TeamMemberAdded事件,该事件属于应用事件,需要支持分布式通信。

所有服务契约的方法参数和返回值都是消息契约的一部分,也需要按照消息契约模型的要求进行定义,尤其需要满足菱形对称架构的要求,不能直接将领域模型暴露在外。

5.映射系统分层架构

在系统上下文的约束下,确定限界上下文属于哪种类型的子领域,即可将它们分别映射到系统分层架构的业务价值层与基础层。显然,资源上下文、文件共享上下文和OA集成上下文都属于支撑子领域,组织上下文和认证上下文属于通用子领域,其余限界上下文属于核心子领域。

EAS系统需要为集团决策者提供移动端应用程序,满足他(她)们提出的决策分析要求,也有利于他(她)们实时了解市场动态、人员动态和项目进度。针对市场部、人力资源部、项目管理部以及子公司等职能部门,主要提供Web前端,方便用户在办公环境的使用。因此,有必要为EAS系统引入一个边缘层来应对不同UI前端的需求。

根据系统级映射的方法,可以为EAS设计出图20-48所示的系统分层架构。

我们为限界上下文建立了菱形对称架构。可将它们看作一个个封闭的架构单元,它们之间的关系由上下文映射确定,在系统分层架构中,只需要考虑它们所处的层次即可。分层架构作用于整个系统上下文,业务价值层和基础层的内部架构由各个限界上下文控制,边缘层则汇聚了每个限界上下文提供的业务能力,统一对外向前端或其他客户端公开服务。

虽然EAS的每个限界上下文都引入了菱形对称架构,不过在其内部,网关层与领域层的设计仍有细微的差异。如决策分析上下文的领域逻辑主要为统计分析,受技术决策的影响,通常可以直接针对数据进行操作,无须建立领域模型,形成弱化的菱形对称架构,内部只包含北向网关与南向网关。其中,南向网关是一个薄薄的数据访问层,从数据库获得的统计分析数据会直接转换为消息契约模型。

OA集成上下文是一个由防腐层发展起来的限界上下文。它与其他限界上下文的协作采用了发布者/订阅者模式,内部又需要调用OA系统的服务接口,因而它的领域层只包含了组装消息内容的领域模型。在网关层,定义了应用事件作为消息契约模型,事件订阅者为北向网关的远程服务,事件处理器为北向网关的应用服务,事件发布者则属于南向网关,分为端口与适配器。

79f7b952b3a4d77806913a74ccacc2ab.png

文件共享上下文的定义打破了惯有的设计方式。它负责的工作是文件上传和下载,通常会考虑将其作为基础设施层的一个公共组件。正如我们在第9章对模块、组件、库、服务等概念的澄清,一个限界上下文可以实现为库或者服务,但本质上仍然表达了对业务能力的纵向切分。由于不需要跨进程通信,可以将文件共享上下文实现为基础层(注意不是基础设施层)的库。它提供的业务能力为具备支撑功能的文件共享能力,封装的领域逻辑除了上传文件与下载文件的领域行为,还规定了属于不同类别的文件存放在文件服务器的不同位置。文件传输的实现由于操作了外部资源,因而属于南向网关适配器的内容。以归档合同为例,合同上下文调用文件共享上下文的FileAppService,其内部的协作序列如图20-49所示。

系统分层架构属于架构的逻辑视图,并没有确定限界上下文的通信边界。例如,OA集成上下文与其他限界上下文并不在一个进程中,但系统分层架构并不需要体现这一点。

3519b3025e7328d321d78a762a7e1169.png

遵循系统分层架构与菱形对称架构对代码模型的约束和规定,EAS系统的代码模型如图20-50所示。

ce28afb43b5a48a5a6ee917ec88c8394.png

所有限界上下文都采用了菱形对称架构规定的标准代码模型,只是根据具体情况作了少量调整。各个限界上下文在系统分层架构所处的层次,也通过包的命名空间清晰地呈现出来了。

20.3.4EAS的领域建模

在确定了EAS系统的限界上下文与系统上下文,并通过菱形对称架构和系统分层架构设计出EAS的整体架构后,接下来就进入了战术层面的领域建模阶段。考虑到篇幅原因,我仅选择了业务逻辑相对复杂的培训上下文,运用快速建模法对其进行领域分析建模,获得领域分析模型后,采用庖丁解牛的过程设计聚合,然后相继开展服务驱动设计与测试驱动开发获得最终的领域模型。

1.领域分析建模

领域分析建模阶段的关键是识别领域概念,为限界上下文建立领域分析模型。参考过程模型推荐使用快速建模法进行领域分析建模,它的基础是业务服务规约。以“提名候选人”业务服务为例,它的业务服务规约如下。

服务编号:EAS-0202

服务名:提名候选人

服务描述:

  作为一名协调者

  我想要提名候选人参加培训

  以便部门的员工得到技能培训的机会

触发事件:

  协调者选定候选人后,点击“报名”按钮

基本流程:

1.确定候选人是否已经参加过该课程

2.对培训票提名候选人

3.邮件通知获得提名的候选人

替换流程:

1a.候选人参加过该培训要学习的课程,提示员工已经学习过该课程

2a.提名操作失败,提示失败原因

验收标准:

1.被提名人属于候选名单中的员工

2.提名的票状态必须为Available

3.提名后的票状态为WaitForConfirm

4.候选人获得培训票

识别业务服务规约的名词,可以获得领域概念:候选人(Candidate)、协调者(Coordinator)、培训(Training)、课程(Course)、票(Ticket)、候选名单(CandidateList)、票状态(TicketStatus)、邮件(Mail)。

识别业务服务规约的动词,然后逐一检查该动词代表的领域行为是否需要产生过程数据。发现“提名候选人”,除了候选人获得培训票,还要记录票的变更历史,因而获得票历史(TicketHistory)领域概念;发现“员工学习过课程”,需要记录该员工的学习记录,因而获得学习记录(Learning)领域概念。

对识别出来的领域概念进行归纳和抽象,发现CandidateList实际上是Candidate的集合,用List<Candidate>即可表达,没有必要单独引入;邮件通知由专门的OA集成上下文发送邮件,在培训上下文中没有必要列出,故而可以删去Mail概念。

对于其他业务服务的领域分析建模,也如法炮制。由于培训上下文的业务服务皆位于同一个限界上下文,因而只需要考虑该上下文内部领域模型之间的关系。由此可获得图20-51所示的领域分析模型。

2.领域设计建模

领域设计建模牵涉两个重要的设计阶段:识别聚合和服务驱动设计。

1)识别聚合

首先梳理对象图。确定领域模型对象到底是实体还是值对象,并分别用不同的颜色表示。一些较容易识别的值对象可以最先标记出来,例如体现了单位、枚举、类型的内聚概念等,如图20-52所示。

44ddf468cf1b03aa2d765a473f7fc4aa.png

7740ebe4af251dc63d34a5692051fb53.png

一些容易识别的实体类也可以提前标记出来。这些实体类往往是业务服务中扮演主要作用的领域概念,体现了非常清晰的生命周期特征。

ProgramOwner、Coordinator、Nominee和Trainee都是参与培训上下文的角色,都拥有员工上下文的员工ID,如此即可建立这些角色与Training和Ticket等实体类之间的关联。它们对应的角色(role)来自认证上下文,用于安全认证和权限控制。角色具有的基本信息,如姓名、电子邮件等,又来自员工上下文。因此,这些领域模型类虽然定义了ID,但在培训上下文中不过是其主实体的一个属性值而已,并不需要管理它们的生命周期,应该定义为值对象。由于培训上下文并未要求为培训维护一个单独的教师信息,故而与Training相关的Teacher应定义为值对象。

Filter和ValidDate都与Training关联。它们看似具有值对象的特征。对过滤器而言,只要TrainingId的值以及类型与规则相同,就应视为同一个Filter对象;有效日期也是如此,只要公式、日期和时间相同,就是同一个ValidDate对象。但是,由于它们的生命周期需要单独管理,将它们定义为实体更加适合。同理,ValidDateAction与CancellingAction也需要单独管理生命周期,应定义为实体。TicketAction却不同,它的差异仅在于具体的活动内容,而它又不需要管理生命周期,应定义为值对象。于是,获得图20-53所示的领域设计类图。

5626040e4134c4e2df3a9000045ec4a7.png

在确定了值对象与实体后,可以简化对领域模型对象关系的确认,即只需梳理实体之间的关系。一个Course聚合了多个Training,一个Training聚合了多个Ticket,这三者之间的组合关系非常清晰。一个Training可以配置多个Filter与ValidDate,但它们之间并非必须有的关系,故而定义为OO聚合关系。同理,一个ValidDate聚合多个ValidDateAction、一个Ticket聚合多个CancellingAction和多个TicketHistory、一个Training聚合多个Candidate和多个Attendance,而BlackList则是完全独立的。确定了实体关系的领域设计类图如图20-54所示。

1c8d6ccef09d48f2f31a02a4b33ebe8d.png

                       图20-54 确定实体之间的关系

梳理之后的领域设计类图非常规范。除了合成关系,存在OO聚合关系的实体都分到不同的聚合中,更不用说完全独立的Backlist实体。如果多个聚合边界的实体依赖了相同的值对象,可以定义多个相同的值对象,然后将它们放到各自的聚合边界内。分解关系薄弱处确定的聚合边界如图20-55所示。

6993767a78b627a55add8cc07577f48e.png

                    图20-55 根据关系强弱确定聚合边界

考虑聚合设计原则,由于Learning聚合中的Course实体具有独立性,因此需要对图20-55稍做调整,将Course实体分离出来,定义为单独的聚合。除此之外,其余聚合边界都是合理的,不需再做调整。最终,确定了聚合边界的领域设计类图如图20-56所示。

由此得到的聚合包括:

qTraining聚合;

qCourse聚合;

qLearning聚合;

qTicket聚合;

qTicketHistory聚合;

qFilter聚合;

qValidDate聚合;

qValidDateAction聚合;

qCancellingAction聚合;

qCandidate聚合;

qAttendance聚合;

qBlacklist聚合。

6195716b01e3a0d3974a1179f1a170f8.png

即使在领域设计模型中,我们也无须为领域模型对象定义字段。每个聚合内的实体或值对象到底需要定义哪些字段,可以结合业务服务,通过测试驱动开发逐步驱动出来。领域设计类图最重要的要素是聚合。一旦确定了聚合,实际上也就确定了管理聚合生命周期的资源库。至于需要哪些领域服务和其他角色构造型,可以交由服务驱动设计来识别。

2)服务驱动设计

服务驱动设计的起点是业务服务。以提名候选人业务服务为例,将业务服务规约的基本流程转换为由动词短语组成的任务,然后通过向上归纳和向下分解获得由组合任务与原子任务组成的任务树:

q提名候选人(业务服务)

♦   确定候选人是否已经参加过该课程

○   获取该培训对应的课程

○   确定课程学习记录是否有该候选人

♦   如果未参加,则提名候选人

○   获得培训票

○   提名

○   保存票的状态

♦   发送提名通知

○   获取通知邮件模板

○   组装提名通知内容

○   发送通知

结合任务分解与角色构造型,它的序列图脚本如下:

NominationAppService.nominate(nominationRequest) {

   LearningService.beLearned(candidateId, trainingId) {

      TrainingRepository.trainingOf(trainingId);

      LearningRepository.isExist(candidateId, courseId);

   }

   TicketService.nominate(ticketId, candidate) {

      TicketRepository.ticketOf(ticketId);

      Ticket.nominate(candidate);

      TicketRepository.update(ticket);

   }

   NotificationService.notifyNominee(ticket, nominee) {

      MailTemplateRepository.templateOf(templateType);

      MailTemplate.compose(ticket, nominee);

      NotificationClient.notify(notificationRequest);

   }

}

该序列图脚本对应的序列图如图20-57所示。

图20-57中的NominationAppService应用服务承担了多个领域服务之间的协作职责,且需要根据beAttend()方法的返回结果决定提名的执行流程。这实际上属于领域逻辑的一部分,故而应该在NominationAppService应用服务内部引入一个领域服务来封装这些业务逻辑。新增的领域服务为NominationService,修改后的序列图如图20-58所示。

230e70e58684e114c012408829541102.png

6a53b4e834b10f51bb24e7a92ebf9cc3.png

图20-58中的MailTemplate是一个聚合,存储了不同类型操作需要通知的邮件模板。在前面的领域分析建模与领域设计建模时,未能发现该聚合。这也印证了领域建模很难一蹴而就,需要不断地迭代更新和演进。

本文摘录于张逸老师新书《解构领域驱动设计》。

❀❀❀

dc4b396ae0474c6ea4cafe3e97276d0c.png

☼ 本文为《解构领域驱动设计》第一章内容,可在京东、当当、淘宝等电商平台上搜索书名,购买本书。

本书全面阐释了领域驱动设计(domain-driven design,DDD)的知识体系,内容覆盖领域驱动设计的主要模式与主流方法,并在此基础上提出“领域驱动设计统一过程”(domain-driven design unified process,DDDUP),将整个软件构建过程划分为全局分析、架构映射和领域建模3个阶段。除给出诸多案例来阐释领域驱动设计统一过程中的方法与模式之外,本书还通过一个真实而完整的案例全面展现了如何进行领域驱动设计统一过程的实施和落地。为了更好地运用领域驱动设计统一过程,本书还开创性地引入了业务服务、菱形对称架构、领域驱动架构、服务驱动设计等方法与模式,总结了领域驱动设计能力评估模型与参考过程模型。本书提出的一整套方法体系已在多个项目中推广和落地。
本书适合希望领会软件架构本质、提高软件架构能力的软件架构师,希望提高领域建模能力、打磨软件设计能力的开发人员,希望掌握业务分析与建模方法的业务分析人员,希望学习领域驱动设计并将其运用到项目中的软件行业从业人员阅读参考。
加入技术琐话粉丝群,可在公众号回复:技术群

  往期推荐:

技术琐话 

以分布式设计、架构、体系思想为基础,兼论研发相关的点点滴滴,不限于代码、质量体系和研发管理。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值