一文读懂DDD
何为DDDDDD不是架构设计方法,不能把每个设计细节具象化,DDD是一套体系,决定了其开放性,体系中可以用任何一种方法来解决这些问题,但是如果一些关键问题没有具体方案落地,可能让团队无所适从。 DDD没有解决的三个问题
DDD术语战略建模
战术建模
Bound Context(BC)领域中的BC被封装为高内聚的模块,这种特性让DDD对架构并没有太大侵入性。架构可以应用于领域内部的结构,也可以包围着领域模型,系统中可以采用多种风格的架构。 DDD的战略设计上提出了BC(Bounded Context,界限上下文)。UL(Ubiquitous Language,通用语言)是团队的共享语言,只要是团队的一员,就需要使用UL,可以保证各个概念在各自上下文中无歧义。BC和UL是DDD的两大支柱,相辅相成。 一个业务领域划分成多个BC,BC之间通过Context Map进行集成,BC是一个显示边界,领域模型在这个边界之内,领域模型是关于某个特定业务领域的软件模型,领域模型通过对象模型来实现,这些对象同时包含了数据和行为,并表达了准确的业务含义。 通过BC隔离系统复杂性,将复杂度内聚于边界之内。 一个大型系统的领域模型完全统一是不可行的,也不是一种经济有效的方式。任何一个大型项目都会存在多个模型,不同模型代码组织在一起软件可能会出现bug,同时更加不可靠并且难以理解。团队之间沟通也会变的混乱。 当划分为多个模型之后,在模型之内,团队可以自由工作,直到自己的界限并且恪守界限。所以需要确保模型纯洁,一致和统一。 Context Map 上下文图多个系统之间会发生关系,存在交互,需要在项目中创建一个所有模型上下文的全局视图,减少混乱。一般通过Context Map表示系统关系总体视图。 U表示上游(Upstream)的被依赖方,D表示下游(Downstream)的依赖方。防腐层(ACL)放在下游,将上游的消息转化为下游的领域模型。 Context Map通过下面几种方式表征界限上下文之间的关系:
共享内核-Shared Kernel 当不同团队开发一些紧密相关的应用程序时,团队之间需要进行协调,通常可以将两个团队共享的子集剥离出来形成共享内核(Shared Kernel),双方进行持续集成(Continuous Integration)。共享内核(Shared Kernel)是业务领域中公共的部分,同时也是团队间容易达成且必须达成共识的领域部分。 客户/供应商-Customer/Supplier 不同系统之间存在依赖关系时,下游系统依赖上游系统,下游系统是客户,上游系统是供应商,双方协定好需求,由上游系统完成模型的构建和开发,并交付给下游系统使用,之后进行联调、测试。这种模式建立在团队之间友好合作和支持的情况下。 追随者-Conformist 当两个开发团队具有上/下游关系时,如果上游团队没有动机来满足下游团队的需求,那么下游团队将无能为力。出于利他主义的考虑,上游开发人员可能会做出承诺,但他们可能不会履行承诺。下游团队出于良好的意愿会相信这些承诺,从而根据一些永远不会实现的特性来制定计划。下游项目只能被搁置.直到团队最终学会利用现有条件自力更生为止。下游团队不会得到根据他们的需求而量身定做的接口。 防腐层-Anticorruption Layer 前面介绍了在两个BC之间集成时可以进行的各种合作,从高度合作的 Shared Kernel模式或 Customer/Supplier Team到单方面的Conformist模式。如果是一种更悲观的关系,假设一个团队既不可能与另一个团队合作也无法利用他们的设计时,该如何应对。 公开主机服务-Open Host Service 当一个子系统必须与大量其他系统进行集成时,为每个集成都定制一个转换层可能会减慢团队的工作速度。如果一个子系统有某种内聚性,那么或许可以把它描述为一组 Service,这组 Service满足了其他子系统的公共需求。 各行其道-Separate Way 当两个系统之间的关系并非必不可少时,两者完全可以彼此独立,各自独立建模,独立发展,互不影响。 领域事件
一个领域事件可以理解为是发生在一个特定领域中的事件,是你希望在同一个领域中其他部分知道并产生后续动作的事件。但是并不是所有发生过的事情都可以成为领域事件。一个领域事件必须对业务有价值,有助于形成完整的业务闭环,也即一个领域事件将导致进一步的业务操作。 领域事件可以是业务流程的一个步骤,例如订单提交,客户付费100元,订单完工等。领域事件也可以是定时发生的事情,例如每晚对账完成。或者是一个事件发生后引发的后续动作,例如客户输错密码三次后发生锁定账户的事件。 领域事件也是一种基于事件的架构(EDA)。事件架构的好处可以把处理的流程解耦,实现系统可扩展性,提高主业务流程的内聚性。 如果改为事件驱动模式,把订单提交后触发一个事件,在订单保存后,触发订单提交事件。通知和后续的各种服务动作可以通过订阅这个事件,在自己的实现空间内实现对应的逻辑,这样就把订单提交和后续其他非主要活动从订单提交业务中剥离,实现了订单提交业务高内聚和低耦合性。
领域事件可以通过观察者模式和订阅模式进行实现。比较常见的实现方式是事件总线(Event Bus)。 事件风暴事件风暴是一项团队活动,旨在通过领域事件识别出聚合根,进而划分微服务的限界上下文。在活动中,团队先通过头脑风暴的形式罗列出领域中所有的领域事件,整合之后形成最终的领域事件集合,然后对于每一个事件,标注出导致该事件的命令(Command),再然后为每个事件标注出命令发起方的角色,命令可以是用户发起,也可以是第三方系统调用或者是定时器触发等。最后对事件进行分类整理出聚合根以及限界上下文。 举个例子 在我们的一次产品的重构活动中也采用了事件风暴方法。系统代码维护了10几年,代码中存在大量的“坏味道”:重复代码,过长函数,过大的类,过长的参数列表,发散式变化,霰弹式修改,镀金问题,注释不清等问题。实际研发过程中也是经常出现一点改动都可能会引起不可预测的结果,重构势在必行。
实体和值对象实体不仅需要知道它是什么?而且还需要知道它是哪个?而值对象只需要知道它是什么?
实体对象相对容易理解,我们常见的类的都可以看成是实体对象。值对象在DDD中相对而言是难以理解并且容易误用的。 为什么需要使用值对象,书中给了一个解释:
使用值对象在不同的BC中进行数据交换,可以避免不同BC对实体对象的状态变更而引发的数据依赖关系,实现最小化的集成。 值类型用于度量和描述事物,DDD中建议应尽量使用值对象来建模而不是实体对象,因为值对象非常容易地对值对象进行创建、测试、使用、优化和维护。 领域服务 领域中的服务表示一个无状态的操作,它用于实现特定于某个领域的任务。 《实现领域驱动设计》书中给出了一个例子,对User进行认证的例子。例子中给出的需求是:
对以上的需求,我们可以把认证的方法写在User类或者Tenant类中,不过对于以上解决方案,似乎都给模型带来了太多的问题。 对于后一种方案, 我们必须从以下回种解决办法中选择一种:
UserDescriptor userDescriptor = DomainRegistry .authenticationService() .authenticate(tenantID,userName,password); 模块在DDD中,模块表示了一个命名的容器,用于存放领域中内聚在一起的类。
模块的设计是基于领域模型的,要符合通用语言的表述。其次,模块的设计要符合高内聚低耦合的设计思想。 模块和BC的关系模块与子域和限界上下文并不是一致的概念,模块也是一种独立的建模方法。对于何时应该对领域模型进行分离,何时将领域模型建模成一个整体,应该仔细地思考与对待。有时通用语言可以很好地帮助我们做出正确的选择。但是另外的时候,其中的术语将变得非常含糊。在这种情况下,我们并不清楚如何划分上下文边界。此时,我们可以首先将它们放在一起,使用模块来对模型进行划分,面不是限界上下文。 但是,这并不意味着我们就应该限制对限界上下文的创建。我们应该通过通用语言的需求来划分模型边界。但限界上下文不是用来代替模块的。使用摸块的目的在于组织那些内聚在一起的领域对象,对于那些内聚性不强或者没有内聚性的领域对象来说,我们应该将它们划分在不同的模块中。 集成BC(界限上下文)一个项目中会存在多个BC,业务需要对它们进行集成。有多种直接的方法进行集成。最简单的方式就是一个BC中暴露API,然后在另外一个BC中通过RPC进行调用。 另外我们也可以通过消息机制进行集成,系统通过消息队列或者发布-订阅机制进行通讯。 第三种方式是通过使用RESTful的方式进行集成。当然,还存在有其他的集成方式。 欢迎加微信交流: (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |