2019-12 ThoughtWorks洞见 - 领域驱动设计文集

2020-03-23 294浏览

  • 1.ThoughtWorks洞见 领域驱动设计 insights.thoughtworks.cn/tag/ddd
  • 2.综述 01 DDD战略篇:架构设计的响应力···························· 2 DDD战术篇:领域模型的应用······························ 7 DDD实战篇:分层架构的代码结构· ························ 14 DDD的终极大招——By Experience· ······················· 22 通用语言、 领域、 限界上下文 25 重读领域驱动设计——如何说好一门通用语言················ 26 当Subdomain遇见Bounded Context······················· 30 架构 36 从三明治到六边形····································· 37 端口和适配器架构——DDD好帮手 ························· 45 领域事件 59 识别领域事件········································· 60 在微服务中使用领域事件································ 63 当提到“事件驱动”时,我们在说什么?· ··················· 73
  • 3.微服务 77 DDD & Microservices· ·································· 78 服务拆分与架构演进· ·································· 83 溯源微服务:企业分布式应用的一次回顾···················· 94 示例实现 100 后端开发实践系列——开发者的第0个迭代·················· 101 后端开发实践系列——领域驱动设计(DDD)编码实践· ········· 124 后端开发实践系列——事件驱动架构(EDA)编码实践··········· 151 后端开发实践系列——简单可用的CQRS编码实践············· 177 用DDD实现打卡系统··································· 196 扩展阅读 199 DDD该如何学?······································ 200 领域驱动设计(DDD)实现之路 · ·························· 203 从“四色建模法”到“限界纸笔建模法”···················· 211 可视化架构设计——C4介绍 ····························· 221 从架构可视化入门到抽象坏味道· ························ 230 技术债治理的四条原则 · ······························· 235 目录
  • 4.ThoughtWorks洞见 领域驱动设计 综述 1 ThoughtWorks 洞见领域驱动设计
  • 5.DDD战略篇:架构设计的响应力 作者:肖然 当敏捷宣言的17位签署者在2001年喊出“响应变化胜 会造成整个服务生态宕机的作法更多的是在测试系统 话当回事儿,甚至很多经验丰富的管理者会认为好的 瘫痪。 于遵循计划”这样的口号时,鲜有组织会真正把这句 计划是成功的一半,遵循计划就是另外一半。然而在 时下的第四次工业革命浪潮中,可能很多管理者已经 不会简单满足于“响应”,而是选择主动发起变化了。 不确定性管理成了这个时代的主旋律,企业的响应力 成了成败的关键。 随着这种趋势的深入,架构设计这个技术管理领域也 被推到了风暴边缘。 “稳定”这个过去我们用来形容好 系统的词语似乎已经失去原有的含义,很多人开始用“ 健壮”这个词语来形容好的系统。比如Netflix公司采 用的Chaos Monkey机制随机主动关停线上服务而不 的健壮性,保证不会因为某个局部的问题而造成全身 然而架构的健壮性却比较难于定义和测试,以至于很 多时候咱们在架构设计上还是在追求稳定性。在一个 典型的企业IT组织里,当你询问一位资深工程师架构 设计时,往往会得到一张搭积木一样的“架构图”。 图的底层是各种数据存储(从经典的Oracle到大数据 标配的Hadoop),图的中间是类似Kafka这样的消息 管道和传统的ESB(消息总线),上层则是各种业务应 用(包括各种Web应用和移动的APP)。 仿佛这是一个流行的“稳定”架构设计。 (示意:典型的IT系统架构图)
  • 6.当询问这样的架构是否合理时,不少人会告诉你问题可大了:这不是云时代的服务化架构。原因是这个架构的大部 分组件,如数据存储,都已经可以完全“托管”给云平台了。于是乎,很多企业架构师又开始寻找像过去ESB一样能 够对接各种云平台的PaaS了,然后抱怨现在的PaaS没有当年的ESB“稳定”。 两个核心问题却很少被提及: 1. 当年基于ESB集成的SOA服务化架构解耦出的组件不但没有提升效率,反而增加了系统后续修改的复杂度。 2. 看似“以不变应万变”的架构并不能支撑多样化的业务需求,最后各个业务部门仍然有一套自己的IT系统,即 3. 画出来的架构图惊人的相似(多少次有人惊呼“这就是我们之前那个工作流系统~”)。 就这两个核心痛点,让我们一起来谈谈架构设计面临的挑战和应对方式。 什么是架构设计? 由于软件设计是一个复杂度很高的活动, “通过组件化完成关注点分离从而降低局部复杂度”很早就成为了咱 们这个行业的共识。前面提到的数据存储、消息管道等“模块”在某种意义上都是组件化的产物。这样的好处是 在不同系统里遇到同样的功能需求时可以复用。在云服务崛起的今天,这样的组件以“服务”的形式更容易为我们 所采用。 当然技术出身的架构师们在架构设计的时候或多或少都有一种“搭积木”的感觉。大家都非常关注Kafaka有哪些 功能,K8S是不是比Mesos功能更全,以及Akka是不是稳定。就像走进一个家装公司,在选择了“套餐”之后有工 程人员给你介绍地砖和木地板用哪个品牌更好。
  • 7.回到咱们的第二个核心痛点,如果只是这样的搭积木,为什么咱们总是在面对新变化、新需求的时候发现需要新 的组装方式或新的组件呢?这样的架构设计对比直接按照需求实现(不考虑架构)有什么优势呢? 这里我们应该回到架构设计的本质,即为什么我们要在代码实现前做设计。显然如果去掉设计这个过程,大家会 说问题这么复杂,如何下手啊?所以设计首先是要解决问题的复杂度。于是有人做了一个架构,交给了一个团队去 实现,很快发现实现的架构和设计完全是两张皮。当然原因很明确——缺少了交流和沟通,所以设计其次是要建立 团队协作沟通的共识。 假设我们产生了一个团队都达成共识的架构设计,大家都兢兢业业把设计变成了现实。一个长期困扰软件行业的 问题出现了,需求总是在变化,无论预先设计如何“精确”,总是发现下一个坑就在不远处。相信很多技术人员都 有这样的经历,结果往往是情况越来越糟糕,也就是我们常说的架构腐化了,最后大家不得不接受重写。这些经历 让我们逐步明确了软件架构设计的实质是让系统能够更快地响应外界业务的变化,并且使得系统能够持续演进。 在遇到变化时不需要从头开始,保证实现成本得到有效控制。 面向业务变化而架构 基于上面的架构设计定义,关键因素就是业务变化。显然这个时代的业务变化是很快的,甚至很多业务主动在变, 不变则亡是很多行业目前的共识。变化速度给架构设计带来了很大挑战,一个移动APP可能需要在一周内上线, 然而为了支撑这个移动APP的后台服务,平台发布窗口是每两个月一次。这样的不匹配在IT领域里是随处可见的现 实,我们习惯性地认为后台天然就很重因此很慢,只可能在牺牲质量的情况下满足这样的速度。 然而事实上这样的健壮架构确实是存在的,看看身边现在无处不在的互联网,又有哪一个企业的架构比之复杂 呢。互联网系统的组件是一个个网站,每个网站完成着自己的业务功能更新,从新闻发布到在线聊天。而各个站点 又是紧密互联的,聊天网站可能把新闻网站拿到的信息实时推送给在线的用户。每个网站都是独立的小单元,面 向互联网用户提供着一定的业务服务。好的网站也根据用户的反馈在不停升级和变化,但这样的变化并不影响用 户使用其它的网站。
  • 8.从互联网架构我们可以学到什么呢?从架构设计角度我认为以下三点是关键。 1.让我们的组件划分尽量靠近变化的原点,对于互联网来说就是用户和业务,这样的划分能够让我们将变化“隔 离”在一定的范围(组件)内,从而帮助我们有效减少改变点。 2.组件之间能够互相调用,但彼此之间不应该有强依赖,即各自完成的业务是相对独立的,不会因为一方掉线而牵 连另外一方,比如新闻网站挂掉了,聊天网站应该继续正常提供服务,可能提示用户暂时无法提供新闻信息而已。 3.组件在业务上是鼓励复用的,正是这样的复用才成就了今天的互联网,我们不会每个网站都去实现一个强大的 搜索引擎。而被“复用”最多的网站显然会受到追捧,成为明星业务。当然架构上这样的网站必然是健壮的。 上面的三点毫无疑问都指向了业务,从业务出发、面向业务变化是我们现代架构设计成功的关键。架构设计的核 心实质是保证面对业务变化时我们能够有足够快的响应能力。 这种响应力体现在新需求(变化)的实现速度上,也体现在我们组件的复用上,在实现过程中现有架构和代码变化 点的数量也是技术人员能够切身体会到的。面对日新月异的数字化时代,组织的整体关注点都应该集中到变化的 原点,即业务上,而架构应该服务于这种组织模式,让这样的模式落地变得自然。 对比之前的传统SOA架构,这个思路的变化是本质性的。类似工业总线(ESB)这样的组件化其实是面向技术的, 希望通过技术平台的灵活性来解决业务变化的多样性。虽然短时间能够收到一定的成效,长期看必然把自身做成 瓶颈,因为所有业务的变化最后都堆积到了这个技术组件来解决。这也回答了为什么实施了传统SOA架构的企业 最后都发现响应速度其实并没有提升起来。 面向业务变化而架构就要求首先理解业务的核心问题,即有针对性地进行关注点分离来找到相对内聚的业务活动 形成子问题域。子问题域内部是相对稳定的,即未来的变化频率不会很高,而子问题边界是很容易变化的,比如在 一个物流系统中:计算货物从A地到B地的路径是相对固定的,计算包裹的体积及归类也是相对固定的,但根据包 裹的体积优化路径却经常会根据业务条件而变化。 (子问题域的划分)
  • 9.打造架构响应力的方法 如果认同了上述现代架构的真正意义,大家一定会问怎么才能打造这样的高响应力架构呢? 领域驱动设计方法DDD(Domain Driven Design)为我们提供了很好的切入点。这个2003年就总结出来的方法终于 在10多年后重新走入了架构师的视野,而这一次大家已经意识到了这种方法在这个快速变化时代的重要性。DDD 通过以下两个模式去有效解决了文章开始提到的两大痛点: 1.让团队中各个角色(从业务到开发测试)都能够采用统一的架构语言,从而避免组件划分过程中的边界错位。 2.让业务架构和系统架构形成绑定关系,从而建立针对业务变化的高响应力架构。 这两点是DDD的核心,也是为什么时下全球架构圈在进一步向DDD这个方向靠拢的原因。DDD明确了业务和系统 架构上的绑定关系,并提供了一套元语言来帮助各个角色有效交流架构设计。 (DDD的基本方法) 在战略层面,DDD非常强调针对业务问题的分析和分解,通过识别核心问题域来降低分析的复杂度。在战术层 面,DDD强调通过识别问题域里的不同业务上下文来进行面向业务需求的组件化。最后在实现层面利用成熟的技 术模式屏蔽掉技术细节的复杂度。 在这里我们也希望通过第一届DDD China建立起一个架构设计人员的交流平台。期待更多的中国技术人员能够 通过这个平台和世界一流架构大师们建立起沟通的渠道,不仅在战略层面,也在战术层面和所有人一起分享讨论 关于DDD的一切。
  • 10.DDD战术篇:领域模型的应用 作者:肖然 领域驱动设计DDD在战术建模(后文简称建模,除非特别说明)上提供了一个元模型体系(如下图),通过这个元 模型我们会对战略建模过程中识别出来的问题子域进行抽象,而通过抽象来指导最后的落地实现。 (DDD构建的元模型元素脑图) 这里我们谈的战术阶段实际就是这样一个抽象过程。这个抽象过程由于元模型的存在实际是一定程度模式化的。 这样的好处是并非只能技术人员参与建模,业务人员经过一定的培训也是完全可以理解的。在带领不少团队实践 建模的过程中,业务人员参与战术设计也是我要求的。 由于已经有不少书籍介绍DDD的元模型,这里我们就不再赘述,转而谈谈这个抽象过程中大家经常遇到的一些困 惑。这些比较常见的问题可能是DDD元模型未来演进需要解决的,但我们仍然要注意业务问题和架构设计的多样 性,不要过度规范,以至于过犹不及。
  • 11.业务对象的抽象 通过对业务问题的子域划分,我们找到了一些关键的 业务对象。在开始进行抽象前一个必须的步骤就是“ 讲故事”! 讲什么故事呢?关于这个子域解决的业务问题或者提 供的业务能力的故事。既然是故事,就必须有清晰的 业务场景和业务对象之间的交互。这件事情看起来是 如此自然和简单,然则一个团队里能够站起来有条不 紊陈述清楚的却没有几人。读到这里的读者不妨停下 来试试,你是否能够把现在你所做的业务在两三分钟 内场景化地描述出来? 这么做显然目的是让我们能够比较完整地思考我们所 要提炼和抽象的业务对象有哪些。只有当我们能够“ 讲”清楚业务场景的时候,才应该开始抽象的步骤。 对于一个业务对象,我们常见的抽象可以是“实体” (Entity)和“值对象” (Value Object)。 这两个抽象方式在定义上的区别是,实体需要给予 一个唯一标识,而值对象不需要(可以通过属性集 合标识)。当然另外一个经常引用的区别是,实体 应该是有一个连续的生命周期的,比如我们在一个 订单跟踪领域里抽象订单为一个实体,那么每个订 单应该有一个唯一识别号,订单也应该有从下单创 建到最后交货完成的生命周期。 显然,如果不增加其它约束条件,值对象的抽象是 没有意义的,都用实体不就行了?但如果我们稍微 思考一下一个实体的管理成本,比如需要保证生命 周期中实体状态的一致性,那么我们就会发现值对 象变得很简单很可爱。当一个对象在我们(抽象) 的世界里不能改变的时候,一切都变得简单了,这 个对象被创建后只能被引用,当没有引用时我们可 以把它交给垃圾回收自动处理。
  • 12.随着高并发、分布式系统的普及,实际上我们在对业务对象抽象的第一步思考是能否用值对象。如果大家实现的技 术架构采用函数范式的语言(类似Closure),那么首先考虑值对象抽象可能就是一个建模原则了。 对象抽象初步完成后,一定要再重复一次之前的故事来审视一下我们的建模。经历这个抽象过程后,参与讨论的每 个人都应该发现自己更清晰业务的需求和需要提供的能力了。 聚合的封装 DDD元模型中一个核心概念叫“聚合” (Aggregate)。这个从建筑学来的名词非常形象,建筑学上我们翻译为“骨 料”,是形成混凝土的重要元素,也是为什么混凝土如此坚固的基础。 (混凝土里的一种骨料) 同理,在DDD建模中,聚合也是我们构建领域模型的基础,并且每个聚合都是内聚性很高的组合。聚合本身完成了 我们对骨干业务规则的封装,减小了我们实现过程中出错的可能。 以上面那个订单跟踪领域为例,假设我们允许一个订单下存在多个子订单,而每个子订单也是可以独立配送的,这 种情况下我们抽象出“子订单”这个实体。显然订单和子订单存在业务逻辑上的一致性,没有订单的时候不应该创 建子订单,更新子订单的时候应该同时“通知”所属的订单。这个时候如果采用把订单和子订单聚合起来的封装就 很有必要了。
  • 13.采用聚合抽象的结果就是访问每个子订单都需要从相关的订单入口(i.e., 订单为聚合根),存取时我们都是以这个 聚合为基本单位,即包含了订单和订单下面的所有子订单。显然这样的好处是在订单跟踪这个领域模型里,订单作 为一个聚合存在,我们只需要一次性梳理清楚订单和子订单的逻辑关系,就不需要在未来每次引用时都考虑这里 面的业务规则了。 (订单跟踪领域的订单聚合) 在建模过程中,很多团队并没有努力思考聚合的存在。封装这个在技术实现领域的基本原则在建模时却很少被重 视起来。开篇提到在战术建模过程中强调业务领域人员的参与也是为了解决这个问题,聚合的识别实际是针对业 务规则的封装,当我们不理解业务规则的时候是无法做出是否封装的判断的。 一言以蔽之,识别聚合是认知潜在核心业务规则的过程,而定义出来的聚合是在大家共识基础上对核心业务规则 的封装。 领域服务的定义 在最初的元模型定义里,领域服务让不少人纠结,一个经典的例子是在账户管理领域里对“转账”这个业务行为的 抽象。由于转账本身是作用在至少两个账户上的,所以把转账作为一个账户的行为显然是不合适的。那么如果我们 把转账名词化抽象成一个实体呢?感觉也是比较别扭,毕竟转账是依附于账户存在的。 这个时候DDD在元模型里提出了服务(Service)这个抽象,转账被抽象为一个服务感觉就顺畅多了。同样道理,在
  • 14.我们上面的订单跟踪领域里,如果跟踪的过程中需要进行短信的通知,一个比较好的建模就是抽象出一个“通知” 服务来完成。 我经常会用静态方法来帮助技术人员理解服务的抽象(虽然服务并不一定用静态方法来实现)。服务本身就像一 个静态方法一样,拥有一定的逻辑但不持有任何的信息,从整个领域来看也不存在不同“版本”的同一个服务。 一个经常困扰大家的问题是对Service这个词语的限定,有的分层架构设计里会出现领域服务(Domain Service) 和应用服务(Applicaiton Service)。大多数时候应用服务在领域服务的上层,直接对外部提供接口。如果存在这 样的分层,那么领域服务就不应该直接对外,而应该通过应用服务。 举个例子,前面的订单消息通知如果是一个领域服务,在完成订单状态变化时创建通知消息,而最后的通知以短 信的方式发给设定的人群,这样就应该有一个相应的应用服务,包含了具体的业务场景处理逻辑。之后也可能有一 个邮件通知的应用服务,同样调用了这个通知领域服务,但通过邮件渠道来完成最终的业务场景。 由于微服务架构的流行,每个子领域的粒度已经相当细了,很多时候已经没有这样的领域服务和应用服务的区分 了。当然从简单性角度出发这是好事情。在整个建模过程中,服务的抽象往往是最不确定的,也是最值得大家反复 斟酌的地方。
  • 15.Repositories的使用 Repositories是一个非常容易被误解的抽象,很多人会直接联想到具体的数据存储。在初期采用DDD建模的时候, 我经常刻意回避这个抽象,避免让大家陷入思考紊乱。 这个抽象概念实际可以追溯到Martin Fowler的Object Query模式。另外一个相关概念是DAO(Data Access Object),都是用来简化需要存储的数据和对应的业 务对象之间的映射关系。不同的是Repositories针对 更加粗颗粒度的抽象,在DDD这个方法里我们可以认 为映射对象是我们的聚合。针对每个实体在实现时候 也可能创造出对应的DAO(比如采用Hibernate这样 的ORM框架),但显然在建模过程中不是我们需要关 注的。 那么Repositories的抽象为什么是必要的呢?让我们 再回到订单跟踪这个例子,通知订单状态发生变化的 服务在发出通知前,需要定位到订单的信息(可能包 括订单的相关干系人和子订单的信息)。通知作为一 个服务是不应该持有具体订单信息的,这个时候我们 就需要通过Repositories的抽象来建立对订单这个聚 合的查询,即有一个订单的repo,而具体的查询逻辑 应该在这个repo中。 这样的抽象在需要存储和查询值对象的时候也是必要 的。假设我们分析订单查询这个领域,在这个领域里订 单记录显然已经不允许修改了,自然的抽象方式就是 值对象。同时一个查询的服务来持有具体的查询逻辑 (比如按时间或用户)是合理的。外部应用直接调取 了查询服务(接口)并给出规定的参数,我们就需要 一个订单记录的repo来持有跟存储相关的查询逻辑。 当然这并不是说有一个查询就一定有一个repo与之对 应,如果查询的逻辑非常简单,未尝不可以让服务直 接针对数据存储实现。记住我们抽象的目标是让建模 更简单,抽象过程中应该保持灵活。
  • 16.限界上下文的意义 经过最近10多年的演进,我们在如何支撑一个组织的规模化上达成了一些基本的共识。我们知道微服务架构 (Microservices)能够帮助我们把成百上千的工程师们组织起来,而小团队的自组织性是至关重要的。我们也逐 步就如何能够在技术和业务团队之间明确沟通“架构”这个难题上找到了DDD。那么DDD和微服务架构的关系是 什么呢?很多人会提到限界上下文(Bounded Context)。 我曾经就这个话题专门撰文一篇(DDD&Microservices)。一个限界上下文封装了一个相对独立子领域的领域模型 和服务。限界上下文地图描述了各个子领域之间的集成调用关系。这个定义某种意义上和我们的微服务划分不谋 而合:以提供业务能力为导向的、自治的、独立部署单元。所以虽然我们不能百分百依据限界上下文划分服务,但 限界上下文,或者说是DDD,绝对是我们设计微服务架构的重要方法之一。 如果我们再追溯到DDD的战略设计,我们会发现在问题域上,DDD通过子问题域(subdomain)的划分就已经进行 了针对业务能力的分解,而限界上下文在解决方案域中完成了进一步分解。当然我们不能完全认为子问题域和限 界上下文有严格意义上的一对一关系,但大多数情况下一个子问题域是会被设计成一个或多个限界上下文的。子域 subdomain和限界上下文某种意义上是互相印证的,重点在区分问题域和解决方案域,这是落地DDD最困难的地 方,也是判断一个架构师能力进阶的分水岭。 战术建模小结 DDD的建模元素比较简洁,本文中叙述的元模型应该是满足了大多数场景下的建模。毛主席曾经有 一句名言“战略上要藐视敌人 战术上要重视敌人”,就架构设计来说我们没有敌人,业务需求是我们 的朋友。所以在领域驱动的架构设计方面,咱们需要的是“战略上要重视朋友,战术上要简化建模”。 希望这句话能够帮助正在实践DDD的团队重新思考自己在战略问题域的投入和重视程度,不要挥舞 着战术模型的大锤到处寻找实际不存在的钉子。 在这里我们也希望通过第一届DDD China建立起一个架构设计人员的交流平台。期待更多的中国技 术人员能够通过这个平台和世界一流架构大师们建立起沟通的渠道,不仅在战略层面,也在战术层 面和所有人一起分享讨论关于DDD的一切。
  • 17.DDD实战篇:分层架构的代码结构 作者:肖然 不同于其它的架构方法,领域驱动设计DDD(Domain Driven Design)提出了从业务设计到代码实现一致性的要 求,不再对分析模型和实现模型进行区分。也就是说从代码的结构中我们可以直接理解业务的设计,命名得当的 话,非程序人员也可以“读”代码。 然而在整个DDD的建模过程中,我们更多关注的是核心领域模型的建立,我们认为完成业务的需求就是在领域模 型上的一系列操作(应用)。这些操作包括了对核心实体状态的改变,领域事件的存储,领域服务的调用等。在良 好的领域模型之上,实现这些应用应该是轻松而愉快的。
  • 18.笔者经历过很多次DDD的建模工作坊,在经历了数天一轮又一轮激烈讨论和不厌其烦的审视之后,大家欣慰地看 着白板上各种颜色纸贴所展示出来的领域模型,成就感写满大家的脸庞。就在这个大功告成的时刻,往往会有人 问:这个模型我们怎么落地呢?然后大家脸上的愉悦消失了,换上了对细节就是魔鬼的焦虑。但这是我们不可避免 的实现细节,DDD的原始方法论中虽然给出了“分层架构” (Layered Architecture)的元模型,但如何分层却没有 明确定义。 分层架构 在DDD方法提出后的数年里,分层架构的具体实现也经历了几代演进,直到Martin Fowler提炼出下图的分层实现 架构后,才逐步为大家所认可。DDD的方法也得到了有效的补充,模型落地的问题也变得更容易,核心领域模型的 范围也做出了比较明确的定义:包括了Domain,Service Layer和Repositories。 (Martin Fowler总结提出的分层架构实现,注意“Resources”是基于RESTful架构的抽象,我们也可以理解为 更通用的针对外界的接口Interface。而HTTP Client主要是针对互联网的通信协议,Gateways实际才是交换过程 中组装信息的逻辑所在。) 我们的核心实体(Entity)和值对象(Value Object)应该在Domain层,定义的领域服务(Domain Service)在
  • 19.Service Layer,而针对实体和值对象的存储和查询逻辑都应该在Repositories层。值得注意的是,不要把Entity 的属性和行为分离到Domain和Service两层中去实现,即所谓的贫血模型,事实证明这样的实现方式会造成很大 的维护问题。DDD战术建模中的元模型定义不应该在实现过程中被改变,作为元模型中元素之一的实体本身就应 该包含针对自身的行为定义。 基于这个模型,下面我们来谈谈更具体的代码结构。对于这个分层架构还有疑惑的读者可以精读一下Martin的原 文。有意思的一点是,这个模型的叙述实际是在微服务架构的测试文章中,其中深意值得大家体会。 这里需要明确的是,我们谈论代码结构的时候,针对的是一个经过DDD建模后的子问题域(参见战略设计篇),这 是我们明确的组件化边界。是否进一步组件化,比如按照限界上下文(Bounded Context)模块化,或采用微服 务架构服务化,核心实体都是进一步可能采用的组件化方法。从抽象层面讲,老马提炼的分层架构适用于面向业 务的服务化架构,所以如果要进一步组件化也是可以按照这个代码结构来完成的。 总体的代码目录结构如下: - DDD-Sample/src/ domain gateways interface repositories services 这个目录结构一一对应了前文的分层架构图。完整的案例代码请从GitHub下载。 可以看到实际上我们并没有建立外部存储(Data Mappers/ORM)和对外通信(HTTP Client)的目录。从领域模型 和应用的角度,这两者都是我们不必关心的,能够验证整个领域模型的输入和输出就足够了。至于什么样的外部存 储和外部通信机制是可以被“注入”的。这样的隔离是实现可独立部署服务的基础,也是我们能够测试领域模型实 现的要求。
  • 20.模型表达 这个 实 现首先 申 明 了元 模 型 实体 E n t i t y 和 值 对 象 从战术建模过程中得到的核心实体和服务的定义。 AggregateRoot继承了Entity。 根据分层架构确立了代码结构后,我们需要首先定义 清楚我们的模型。如前面讲到的,这里主要涉及的是 我们利用C++头文件(.h文件)来展示一个Domain模 型的定义,案例灵感来源于DDD原著里的集装箱货运 例子。 namespace domain{ struct Entity { int getId();protected:int id; }; structAggregateRoot:Entity { }; struct ValueObject { }; struct Provider { }; structDelivery:ValueObject { Delivery(int); int AfterDays; }; structCargo:AggregateRoot { Cargo(Delivery*, int); ~Cargo(); void Delay(int);private:Delivery* delivery; }; } ValueObject。实体一定会有一个标识id。在实体的基 础上声明了DDD中的重要元素聚合根AggregateRoot。 根 据 定 义 ,聚 合 根 本 身 就 应 该 是 一 个 实 体 ,所 以 这个案例中我们定义了一个实体Cargo,同时也是一 个聚合根。Delivery是一个值对象。虽然这里为了实 现效率采用的是struct,在C++里可以理解为定义一个 class类。 依赖关系 代码目录结构并不能表达分层体系中各层的依赖关 系,比如Domain层是不应该依赖于其它任何一层的。 维护各层的依赖关系是至关重要的,很多团队在实施 的过程中都没有能够建立起这样的工程纪律,最后造 成代码结构的混乱,领域模型也被打破。 根据分层架构的规则,我们可以看到示例中的代码结 构如下图。
  • 21.Domain是不依赖于任何的其它对象的。Repositories 是依赖于Domain的,实现如下:引用了model.h。 #include “model.h” #include using namespace domain; namespace repositories { struct Repository { }; ... Services是依赖于Domain和Repositories的,实现如 下:引用了model.h和repository.h #include “model.h” #include “repository.h” using namespace domain; using namespace repositories; namespace services { struct CargoProvider : Provider { virtual void Confirm(Cargo* cargo){}; }; struct CargoService { ... ... }; ... auto provider =std::make_shared< StubCargoProvider >();api::Api*createApi() { ContainerBuilder builder; builder.registerType< CargoRepository >().singleInstance(); builder.registerInstance(provider).as(); builder.registerType< CargoService >(). singleInstance(); builder.registerType().singleInstance(); auto container = builder.build();std::shared_ptrapi = container>resolve(); return api.get(); } 为了维护合理的依赖关系,依赖注入(Depedency Injection)是需要经常采用的实现模式,它作为解耦合的一种方 法相信大家都不会陌生,具体定义参见这里。 在 测 试 构 建 时,我 们 利 用了一 个 I o C 框 架(依 赖 注 入 的 实 现 )来 构 造了一 个 A p i,并且 把 相 关 的 依 赖( 如 CargoService)注入给了这个Api。这样既没有破坏Interface和Service的单向依赖关系,又解决了测试过程中Api 的实例化要求。
  • 22.测试实现 有了领域模型,大家自然会想着如何去实现业务应用了,而实现应用的过程中一定会考虑到单元测试的设计。在构 建高质量软件过程中,单元测试已经成为了标准规范,但高质量的单元测试却是困扰很多团队的普遍问题。很多时 候设计测试比实现应用本身更加困难。 这里很难有一个固定标准来评判某个时间点的单元测试质量,但一个核心的原则是让用例尽量测试业务需求而不 是实现方式本身。满足业务需求是我们的目标,实现方式可能有多种,我们不希望需要持续重构的实现代码影响到 我们的测试用例。比如针对实现过程中的某个函数进行入参和出参的单元测试,当这个函数发生一点改变(即使是 重命名),我们也需要改动测试。 测试驱动开发TDD无疑是一种好的实践,如果应用得当,它确实能够实现我们上述的原则,并且能够帮助我们交 流业务的需求。比较有意思的是,在基于DDD建立的核心模型之上应用TDD似乎更加顺理成章。类比DDD和TDD 虽然是不恰当的,但我们会发现两者在遵循的原则上是一致的,即都是面向业务做分解和设计:DDD就整个业务 问题域进行了分解,形成子问题域;TDD就业务需求在实现时进行任务分解,从简单场景到复杂场景逐步通过测 试驱动出实现。下面的测试用例展现了在核心模型上的TDD过程。
  • 23.TEST(bc_demo_test, create_cargo) {api::CreateCargoMsg*'>api::CreateCargoMsg*