重新温习软件设计之路(5)

本文是我学习课程《软件设计之美》的学习总结第五部分,记录对于DDD领域驱动设计方法的整体理解。

上一篇:体会软件设计之美(4)

1 关于领域驱动设计

前面温习了软件设计的分析步骤、设计原则与模式,道(理念、原则)已经有了,需要一个具体的术(落地方法论)来实践了,这就需要我们掌握一个好的设计方法。

在业务需求并不复杂的年代,我们一般都在围绕数据做文章。但当下软件变得越来越复杂,一种新的设计方法逐渐脱颖而出,它虽然不是万能药,但是对大部分人面对的业务场景而言,都是能够有效应对的。

领域驱动设计的发展简史

领域驱动设计似乎从Eric Evans的著作《领域驱动设计》出版开始登上舞台的,但是由于这本书的确太晦涩难懂,大多数人都看不懂,也就无法品味其好处,当然也就无法大范围地推广开来。

直到后来微服务的兴起,我们发现微服务的难度主要在于如何有效划分微服务,这时DDD又被重新请上了舞台中央,因为它是一个恰当的指引。所以,大家被逼着硬着头皮开始啃DDD了。

不过,DDD虽好,当下的各种资料也比较多,但由于DDD存在着大量的名词,对于初学者而言还是比较难以快速掌握的。好在,郑晔老师是个善于引导开发者抓住主线再抓细节的人,他在《软件设计之美》中提到了:学习DDD,需要首先理解DDD的根基:通用语言 和 模型驱动设计。而DDD的过程,也正是建立起通用语言 和 识别模型 的过程。

领域驱动设计的两个根基

第一个根基:通用语言(Ubiquitous Language)

所谓通用语言,是在业务人员和开发人员之间建立起的共有的语言,是将业务概念在业务和技术人之间达成共识,从而避免概念混淆和人为鸿沟。

建立通用语言最简单的做法就是让业务和开发一起,在白板上讲各种涉及到的业务概念列举出来,双方一起进行分类和整理。

而业界也探索出了一种达成通用语言的实践方式,即事件风暴。事件风暴是一种工作坊,它建议业务人员和开发人员在一起,找一面白板墙然后用不同颜色的便利贴将各种业务相关的东西等都贴上去。

事件风暴也有一个三步走:

(1)将领域事件识别出来

eg. 订单已支付、订单已下达

(2)找出引发领域事件的动作或命令

eg. 产品已上架、产品已下架

(3)找出与事件和命令相关的实体或聚合

eg. 产品、订单

关于如何开好事件风暴这个工作坊,我们可以去参考更多的相关资料。这里,我们只需要记住让不同的角色参与其中,大家一起通过讨论(切忌一言堂)达成共识即可。

第二个根基:模型驱动设计(Model-Driven Design)

有了通用语言后,如何将业务模型进行组织和落地,就属于模型设计阶段了。DDD将这个模型设计的过程分成了两个阶段:

(1)战略设计

(2)战术设计

所谓战略设计,也称为高层设计,指将系统拆分成不同的领域,换句话说:哪些是核心域,哪些是支撑域,哪些又是通用域。

所谓战术设计,也称为低层设计,指如何具体地组织不同的业务模型,换句话说:这些模型是啥角色(实体、值对象)?这些模型之间是啥关系(聚合、聚合根)?这些模型如何协作和演变(领域事件、领域服务、工厂、仓库、应用服务)?

初步理解了DDD的两个根基,也就对DDD有了一个初步的了解。

整体脑图

最后,整理了整体认知这部分学习内容的脑图如下所示:

整理自《软件设计之美》

2 战略设计

战略设计,是高层设计,将系统拆分成不同的领域。这一过程涉及到领域、子域、限界上下文、上下文映射图等众多概念,我们初学的时候往往会望而却步。

还好,郑晔老师善于引导我们抓主线,他说道,我们可以将战略设计分为两个部分:

(1)业务概念的划分

(2)业务概念的落地

业务概念的划分

通过和业务人员的讨论和共识,已经建立起了一套通用语言,也就有了各种各样的词汇。然后,我们就需要将其进行分类,即将它们划分到不同的子域中去。可以看出,这也是一个分离关注点的过程。

DDD中将子域分为了三个类型:核心域、支撑域 和 通用域

所谓核心域,就是整个系统中最重要的子域,换句话说,它是企业的核心竞争力,需要全力投入。

关于核心域,Eric Evans曾经提出过三个问题,可以用来指导我们识别是否搞划为核心域:

  • 为什么这个系统值得写?

  • 为什么不直接买一个?

  • 为什么不外包?

所谓支撑域,就是非核心竞争力的子域,但是市面上又找不到一个现成的满足需求的方案,需要一定投入。

所谓通用域,就是行业里通常都是这么做的,即使自己不做,也并不影响业务运行,可以花钱购买。

业务概念的落地

通过划分子域,接下来就要将其落地到代码实现上,那么,这些子域如何组织,是放一起还是分开放?DDD给出了一个限界上下文的概念,指导我们去组织子域。所谓限界上下文,就是用来限定通用语言使用的边界的,一旦出界,就无法保证其语义的一致性了。子域和限界上下文不一定一一对应,有可能一个限界上下文中包含多个子域,也有可能一个子域横跨了多个限界上下文。在目前的实践中,通常一个限界上下文就对应一个独立的系统,可以将其拆分为一个独立的微服务。有了对于限界上下文的理解,就可以将拆分后的不同的业务子域分解到不同的限界上下文中。但是,另外一个问题也随之出现:尽管划分了不同系统,但它们终究还是一个完整的大系统,它们之间需要有交互。DDD又给出了一个上下文映射图的概念,它描述了不同限界上下文之间的交互。目前,业界提供了如下一些描述的交互方式:

  • 合作关系

  • 共享内核

  • 客户-供应商

  • 跟随者

  • 防腐层

  • 开放主机服务

  • 发布语言

  • 各行其道

  • 大泥球

当然,这么多的交互方式,我们也一时半会记不住,可以在对DDD的主线有了初步理解之后通过学习参考其他的DDD资料去学习。

温馨提示:本文底部有推荐的DDD学习资料/课程。

明确了不同的限界上下文之间使用何种交互方式之后,这些交互方式最终会落地为不同的通信协议如REST、RPC或MQ(消息队列)等,我们根据实际需求选择即可。

当我们将不同限界上下文的交互描述出来之时,就得到了一幅上下文映射图。通过上下文映射图,可以帮助团队建立起全局性的认知。

整体脑图

最后,整理了战略设计这部分学习内容的脑图如下所示:

整理自《软件设计之美》

3 战术设计

战略设计阶段,通过业务概念的划分和落地,我们已经将识别出来的不同业务模型放到了不同的限界上下文中,也知道了这些限界上下文的交互方式。

而战术设计,是低层设计,需要组织这些不同的模型落地到实现方案中,这一过程又涉及实体、值对象、聚合、领域服务、应用服务等众多概念。

还好,感谢郑晔老师,又指导我们抓主线:如果将战略设计和战术设计比作写故事的过程,那么战略设计就是梳理故事大概的背景和剧本,战术设计就是根据故事背景去写具体的故事。写故事也是有套路的,先设定不同的角色,再设置不同角色之间的关系,最后将他们互动起来形成一个故事。

因此,可以看出,战术设计也是一个三步走:

(1)设定不同的角色

(2)设定角色之间的关系

(3)让角色之间互动起来

不同的角色

在战术设计中,要识别的名词有实体和值对象。

实体(Entity)是能够通过某个唯一标识符确认出来的一个对象。比如,订单的唯一标识符是订单号,无论这个订单是已支付还是已取消,都可以通过订单号识别出来。实体的属性是可以变的,比如订单的状态,但是只要标识符不变,实体就还是那个实体。

值对象(ValueObject)顾名思义就是一个值,它由多个属性组合而成。和实体不同,值对象的属性是不能变的,变了就不再是那个值对象了。比如,订单地址是由省市区和具体地址组成的,一旦其中一个属性变了,这个订单地址也就不是原来那个订单地址了。

区分实体和值对象的意义在于,将变的对象 和 不变的对象区分开

设定角色之间的关系

识别出实体和值对象之后,就需要考虑它们之间的关系了,DDD给了我们聚合和聚合根的概念。

聚合(Aggregate)是紧密关联的实体和值对象的组合,这个组合必须“同生共死”。比如,一个订单往往包含多个订单项,如果订单作废,那么里面的所有订单项都需要作废。因此,订单和订单项就是一个聚合。

聚合根(Aggregate Root)是从外部访问某个聚合的起点,是这个聚合的负责人或管理者,也是这个聚合对外的接口人。比如,在订单和订单项组成的聚合之中,订单就是聚合根。

郑晔老师强调,通过组合成聚合,我们也可以明确:是聚合的,可以一次都拿出来,比如拿订单的时候可以顺带一次性地将其所有的订单项都拿出来;不是聚合的,则靠标识符按需提取拿出来。

角色之间如何互动

通过事件风暴,已经识别出了事件和动作,相当于故事的大体剧本已经有了,接下来就要安排这些有着不同关系的不同角色去执行这些动作完成一些事件,最终形成整个故事。

这些动作的结果,就是领域事件(Domain Event),比如,订单已下单,订单已支付、产品已上架等事件。

而这些动作,其实就是各种动词,这里主要是指领域服务(Domain Service),它操作着领域对象,比如,下单、收款等动作。

这些动作之中,有一类特殊的动作:创建对象,它也在领域服务中,这种动作一般对应的是工厂(Factory)。对,没错,它就是设计模式中我们理解的工厂,比如订单项的创建应该从订单上的订单项工厂方法中创建出来,从而保证二者之间的关联。

对于这些领域对象的变更,仓库(Repository)负责将变更的结果保存下来。这里,我们可以暂时简单理解为持久化操作,比如我们熟知的CRUD操作。

当构建好了领域服务,核心的业务逻辑就已成型,但可能还缺少一些协调者,它们负责协调不同的领域对象和服务来完成不同客户端的不同业务动作,这些其实属于一些非核心的额外的工作,就可以放到外围去做。

因此,DDD又给出了一个应用服务(Application Service)的概念,它就是一个协调者的角色。实际开发中,我们往往会将一些与业务逻辑无关的内容放到应用服务中去,比如鉴权、认证、日志等。所以,可以看出,应用服务其实是不包含业务逻辑的。

下图是一个典型的DDD分层架构示意图,我们可以看到应用服务和领域服务所在的位置,图片来自欧创新老师的《DDD实战课》。

整体脑图

最后,整理了战术设计这部分学习内容的脑图如下所示:

整理自《软件设计之美》

4 小结

本文我们学习了领域驱动设计的整体概览 及 模型驱动设计的两大阶段 战略设计 和 战术设计 的基本过程。相信有了整体上的初步理解,我们再去学习DDD的相关资料时,不会再望而却步了。

最后,感谢郑晔老师的这门《软件设计之美》课程,让我受益匪浅!我也诚心把它推荐给关注Edison的各位童鞋!

 

参考资料

郑晔,《软件设计之美》(极客时间课程,推荐订阅学习)

延伸学习

欧创新,《DDD实战课》(极客时间课程,推荐订阅学习)

张逸,《领域驱动设计实践》(GitChat课程,推荐订阅学习)

[美] Scott Millett & Nick Tune,《领域驱动设计模式、原理与实践》(C#语言描述示例)

👇扫码订阅《软件设计之美》

👇扫码订阅《DDD实战课》

 

posted @ 2021-01-18 10:21  EdisonZhou  阅读(145)  评论(0编辑  收藏  举报