领域驱动设计基础

领域驱动设计,也就是DDD,是为了正确处理复杂逻辑而诞生的方法论;在软件开发中,随着系统业务复杂度增长,我们会遇到很多问题:业务非常复杂时,新需求评估,大家仅

领域驱动设计,也就是DDD,是为了正确处理复杂逻辑而诞生的方法论;在软件开发中,随着系统业务复杂度增长,我们会遇到很多问题:

  1. 业务非常复杂时,新需求评估,大家仅靠脑子里的印象来评估大概的改动范围和造成的影响,造成评估有误差,设计不全面,最后开发出来的系统并不是全局最优的解,往往在测试或者上线之后才发现设计有缺陷。
  2. 当系统概念繁多时,业务方和技术方在讨论问题,会牛头不对马嘴,一个词在两个人脑中代表的含义不一样,或者同一个概念有多个词都可以代表,造成误解。
  3. 业务理解的模型和代码实现差距过大,一个很小的改动,系统可能要伤筋动骨,此时,业务不理解为什么小改动要花这么多时间,实际上是因为业务理解的模型是纯分析模型,与实现并无关联。

基础概念

1.	领域(Domain): 领域是指特定业务领域或问题领域,DDD 的目标是将软件系统与这个领域相匹配,以便更好地满足业务需求。
2.	模型(Model): 模型是对领域的抽象和表示。在DDD中,模型是通过实体、值对象、聚合等构建的,用于描述领域中的概念和关系。
3.	实体(Entity): 实体是具有唯一标识符的领域对象,其状态和行为随时间变化。实体通常代表领域中具有实际业务含义的对象,如客户、产品、订单等。
4.	值对象(Value Object): 值对象是没有唯一标识符的领域对象,其身份由其属性的值来确定。值对象通常用于表示概念上不可变的东西,如日期、时间、货币金额等。
5.	聚合(Aggregate): 聚合是一组相关的实体和值对象的集合,它们被视为一个整体单元进行管理。一个实体通常是聚合的根,负责维护聚合内的一致性和约束。
6.	仓储(Repository): 仓储是用于管理实体的存储和检索的机制。它提供了一种从数据库或其他数据存储中加载和保存实体的方式,以及根据标识符查找实体的方法。
7.	领域服务(Domain Service): 领域服务是一种无状态的领域对象,用于处理领域内的特定操作或逻辑。它们有助于将领域逻辑从实体和值对象中分离出来。
8.	工厂(Factory): 工厂是用于创建领域对象的对象,特别是创建复杂对象或具有复杂初始化逻辑的对象时非常有用。
9.	领域事件(Domain Event): 领域事件是描述领域内已发生事件的对象。它们用于在领域内传递信息,通知其他部分关于状态变化。
10.	限界上下文(Bounded Context): 限界上下文是领域驱动设计中的概念,用于划分领域模型的边界,以便不同部分的模型不会发生冲突。每个限界上下文有自己的语言、模型和规则。
11.	领域驱动设计层次结构: 领域驱动设计通常包括不同层次的概念,如领域层、应用层、基础设施层等,以支持不同类型的业务逻辑和交互。

两关联一循环

领域驱动设计提出两关联一循环:

如上图:
实现与模型关联,统一语言与模型关联,通过需求不断循环迭代模型,明确统一语言;

我们设想通过这个机制,不断精炼模型和统一语言,让其能够被技术和业务双方理解,并解决上面提到的几个问题。

实际遇到的困难

我设计的模型到底是好是坏

虽然领域驱动设计的方式是【迭代试错法】,但是过于宽松的标准也会导致模型设计千人千面,不同的角度去理解业务会产生不同的模型,我们只能期望于一个大佬,抽象能力极强,让他来理解业务并设计模型;

性能与模型的矛盾

我在实操过程中,也会碰到这个问题:我的对象富含业务知识,行为完整,没有逻辑泄漏,但是实际生产系统又有极高性能要求,需要我更贴近计算机模型去设计,然后我就纠结了。举个例子:我初始化一个AI账号,账号里有多个模型,每个模型下有多个版本,有时候,我只需要取出其中一个模型数据,但要我初始化整个AI账号对象对数据库太不友好了;再比如说分页问题,假如一个AI账号下有N个模型,必须分页展示,那这个模型内部就要包含分页逻辑,包含了不必要的技术细节。

为了解决这个问题,这里我比较推崇Eric的Domain、Repository、RepositoryImpl、Factory设计方式,每个Domain中包含自己的Repository,Repository是一个接口,可以有不同的持久化RepositoryImpl实现类,这个设计可以屏蔽分页、懒加载、批处理等为了性能而妥协的技术细节;而Domain只能通过Factory来构造,以此限制使用者对Domain对象的访问权限,来保证领域对象的构造不泄露。

重度聚合对象的僵化问题

任何系统里都会有一个User或者Tenant对象,这个对象往往是所有业务的主体,在领域设计过程中,我们为了让代码富含业务知识,往往会在这个User对象中塞入各种对象,保证其聚合关系,例如:一个用户往往有算法模型账号、各种来源的算法模型、算法任务、设备、门店、多个租户权限,那这个User对象里往往会聚合大量的字段,这个类也就变成了过大类,最后别人理解不了这个类的全部行为,也改不动这个类,也就与领域驱动设计背道而驰了。

这里推荐使用上下文来将一些隐含的概念显性化,如下图所示,都是User用户,但是在不同的上下文(也可以叫做场景)中,有不同的上下文对象,自然也有不同的行为。

在订阅时,它是阅读者、在盆友圈时是联系人、在支付时是买家。这种设计方式也容易理解,例如在公司上班时,你是否会做饭、是否能遛狗无关紧要,但是回家时,你必须有这些技能,一个人在不同场景中扮演不同的角色,正是上下文最好的诠释。

然后我们需要考虑代码怎么实现,这里推荐使用装饰器模式,将User放到Buyer类里,并将特殊的行为写在Buyer类里,在初始化时,用Repository来屏蔽User、Buyer、Reader等不同上下文对象的初始化方式(因为他们可能来自于不同的存储层)。

User user = repository.findById(....);

//repository负责将User转换成不同的上下文对象,实际是从不同的存储层中初始化该对象信息。
Buyer buyer = repository.asBuyer(user);
Reader reader = repository.asReader(user);
Contact Contact = repository.asContact(user);

领域驱动设计的分层中,领域层与基础设施层的矛盾

这章说实话就是介绍了一些知识:领域驱动分层理论、复习SOLID原则、领域层与基础设施层如何解耦

领域驱动常见分层:

展现层(Representation Layer):负责给最终用户展现信息,并接受用户的输入作为功能的触发点。如果不是人机交互系统,用户也可以是其他软件系统。
应用层(Application Layer):负责支撑具体的业务或者交互流程,将业务逻辑组织为软件的功能。
领域层(Domain Layer):核心的领域概念、信息与规则。它不随应用层的流程、展现层的界面以及基础设施层的能力改变而改变。
基础设施层(Infrastructure Layer):通用的技术能力,比如数据库、消息总线等等。
SOLID原则是面向对象编程中的五个设计原则,用于帮助开发人员编写可维护、可扩展和易于理解的代码。这些原则是:

	1.	单一职责原则(Single Responsibility Principle - SRP):一个类应该只有一个引起变化的原因,即一个类应该只有一个责任。这有助于确保类保持简单且易于维护。
	2.	开放-封闭原则(Open-Closed Principle - OCP):软件实体(类、模块、函数等)应该对扩展开放,但对修改关闭。这意味着应该通过添加新代码来扩展功能,而不是修改现有代码。
	3.	里氏替换原则(Liskov Substitution Principle - LSP):子类应该能够替换其父类而不会影响程序的正确性。子类应该继承父类的行为,并且可以通过特化而不是修改来扩展其功能。
	4.	接口隔离原则(Interface Segregation Principle - ISP):不应该强迫客户端依赖于它们不使用的接口。将大型接口拆分成小型、具体的接口,以便客户端只需实现其需要的部分。
	5.	依赖反转原则(Dependency Inversion Principle - DIP):高层模块不应该依赖于低层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。这有助于实现松耦合和可维护性。

这些原则有助于编写具有良好结构的面向对象代码,提高了代码的可维护性、可扩展性和可重用性。

领域层与基础设施层的关系很微妙,一般我们希望和具体实现相关的基础设施层不要设计到领域层中,但实际又很难做到,例如:用户支付后,需要发送Email、手机短信、调用银行支付接口等外部三方接口,这些与具体供应商有关,你短信可以用阿里云、腾讯云,支付可以用工农建三大行的接口。我们可以把基础设施层对象拟人化,也作为领域模型,用CustomService接口来包裹Email、短信等逻辑,用Cashier来包裹银行处理逻辑(所以万物皆可领域化)

↓↓↓↓↓↓↓↓↓↓

事件风暴法

事件风暴法其实就是从事件流的角度切入,去理解和梳理业务,并总结出关键的7个要素,这些要素能够组成并梳理出我们需要的领域模型。

以下是关键的7要素:

行动者(Actors)是系统的使用者。这里使用者是一个相对模糊的概念,可能是现实中的人也可能是别的系统;
命令(Command)是由行动者发起的行为。它代表了某种决定,通常是事件的起因,也称作行动者触发命令(AIC,Actor Initiated Command);
事件(Event)就是我们前文讨论过的事件;
聚集(Aggregate)就是领域驱动设计中的聚合,可以看作一组领域对象,在头脑风暴阶段可以泛指某些领域概念,不需要细化;
系统(System)指代的是不需要了解细节的三方系统。因为不需要了解细节,所以我们可以将它们看作一个整体;
阅读模型(Read Model)用以支撑决策的信息。通常与界面布局有关;
策略(Policy)是对于事件的响应,通常表示不属于某些聚集的逻辑。通过策略可以触发新的命令,由策略触发的命令,被称作系统触发命令(SIC,System Initiated Command)。

事件风暴建模的整体流程是这样的:

1.首先通过头脑风暴寻找领域事件;
2.根据事件寻找触发它的命令与行动者;
3.通过事件,寻找策略以及由策略触发的 SIC;
4.根据命令与事件,寻找产生了变化的聚合,以及新生成的阅读模型;
5.根据寻找到的聚合、阅读模型、事件,开始完善、细化领域模型。

实战演示

根据某个业务实战分析一下,演示一下熟悉流程,理解各个要素的含义和功能。
需求如下:
我们有一个算法任务,这个算法任务可能是中心任务、边缘任务、本地算法任务,用户可以禁用该任务,用户会看到任务是禁用状态,然后禁用时,若是中心任务则清除中心推理资源;若是边缘任务,则清除边缘设备上所有算法资源;若是本地专业算法任务则直接禁用即可;且这些任务在禁用过程中需要回退相应扣除的服务授权;

根据以上需求,我们逐句进行理解并分解过程:

1.首先通过头脑风暴寻找领域事件:
    这里场景比较简单,就是禁用某个任务;
2.根据事件寻找触发它的命令与行动者;
    命令是:禁用;行动者是:用户;
3.通过事件,寻找策略以及由策略触发的 SIC(系统产生的命令):
    策略:任务禁用策略,它包含任务本身禁用、根据任务类型清除中心推理资源、边缘推理资源、专业算法任务禁用、授权回退。
4.根据命令与事件,寻找产生了变化的聚合(一堆领域模型),以及新生成的阅读模型(阅读模型是指专门用于查询的模型):
    这里产生变化的领域模型有:算法任务模型、中心推理资源模型、边缘推理资源模型、授权模型;并未产生新的阅读模型,仍使用老的任务阅读模型;
5.根据寻找到的聚合、阅读模型、事件,开始完善、细化领域模型。
    所以我们可以梳理出以上的模型了:算法任务模型、中心推理资源模型、边缘推理资源模型、授权模型、任务阅读模型;


行动者(Actors):用户
命令(Command):禁用任务
事件(Event):任务被禁用
聚集(Aggregate):算法任务模型、中心资源模型、边缘资源模型、授权模型
系统(System):边缘设备
阅读模型(Read Model):算法任务阅读模型
策略(Policy):任务禁用策略、清除中心推理资源、清除边缘推理资源、任务被禁用、授权回退

通过以上的流程,我们可以得到以上的7要素和任务禁用的事件流程,并得到了准确的几个领域模型对象。

几种UML关系解释

我们这里仅列举常见的几种,也就是标准UML绘图工具里有的几种关系,而且在不同场景下,相同事物之间的关系也会发生变化。

  • 关联
    关联是指两个或者多个对象之间有关系,可以是单向的或者双向的,可以具有多重性;
    举个栗子:一个小伙拜了一位老人为师,然后他们就产生了关联,是师徒关联。

  • 依赖
    标识一个对象依赖于另一个对象的服务或者接口。如果供应商的变化会影响你发货,这就是依赖。
    举个栗子:小伙求师傅教他功夫,好为父报仇,此时小伙【依赖】于他的师傅,所以【关联】与【依赖】的都表示产生了联系,但是【依赖】是没你不行。

  • 聚合
    表示整体与部分之间的关系,其中整体对象包含部分对象。聚合是一种弱关联,部分对象可以独立于整体对象。
    举个栗子:一天小伙和师傅一起在街上采买,结果碰到一群恶霸,俩人三下五除二打饭了这群恶霸,街上的人叫他们英雄师徒,此时小伙与师傅就是聚合关系,俩人一起行侠仗义,组成了【英雄师徒】小团体。

  • 组合
    类似于聚合关系,但表示更强的整体-部分关系。在组合中,部分对象的生命周期依赖于整体对象。
    举个栗子:但是恶霸们叫来了他们的头头,武艺极高,小伙师徒两人轮番落败,绝境之下,两人配合使出了一招需要双人配合才能发挥威力的绝学:【双剑合璧】,此时他们就是组合关系,必不可分。

  • 继承/泛化
    表示一个类(子类或派生类)继承另一个类(父类或基类)的属性和行为。它用于创建类的层级结构。
    举个栗子:十年之后,徒弟学习了师傅的所有本领,还自创了一招平沙落雁,可以说徒弟继承了师傅的所有本领,还有自己的绝学。

  • 实现
    表示一个类实现了一个接口,或者一个用例实现了一个用例规约。它用于显示类或用例与其规范之间的关系。
    举个栗子:徒弟临走前,师傅对其语重心长的说:为师有一个绝学终其一生未能学成,它是一套从天而降的掌法,威力无穷,可以以一人敌千人,他叫【如来神掌】,徒弟默默记在心中,又苦练十年,终于练成了这个掌法,这个就是实现。

四色建模法

总结

总结来说,我们的使用领域驱动设计的初衷就是:寻找到一个在软件系统生命周期内稳固的不变点,由它构成架构、协同与交流的基础,帮助我们更好地应对软件中的不确定性。
后面一些课程有点跟不上,可能是理解不够,我想着先进行现有业务模块的实践,然后在问题中再回头来看专栏学习学习。

您可能有感兴趣的文章
领域驱动设计(1)认识了解什么是领域驱动

【读书笔记】【设计】如何实现领域驱动设计(DDD)笔记

领域驱动设计(DDD)架构演进和典型架构介绍

领域驱动设计 (DDD) 总结

领域驱动设计(精简版)