ITPub博客

首页 > 数据库 > 数据库开发技术 > 手把手教你用 DDD|从零开始掌握 DDD:构建高质量软件系统的实战经验(万字长文)

手把手教你用 DDD|从零开始掌握 DDD:构建高质量软件系统的实战经验(万字长文)

原创 数据库开发技术 作者:灸哥漫谈 时间:2024-07-16 00:09:54 0 删除 编辑

点击上方蓝字,关注灸哥



大家好,我是灸哥,今天看看我能不能手把手教会大家用 DDD,既挑战我自己也挑战你们,一起来呗~(内容有点超长,请做好心理准备)

在软件开发的世界里,构建高质量的软件一直是一项非常艰巨的任务。

领域驱动设计(Domain-Driven Design,DDD) 这几年就像一颗璀璨的星辰,以其独特的光芒引领着软件架构的新潮流,DDD 提供了一套系统的方法,可以帮助我们开发团队来更好地理解和解决复杂的业务问题。

DDD 不仅是一种技术实战,更是一种思维模式,它强调以领域为中心,通过与领域专家的深入对话,通过把业务需求和技术实现紧密结合,如此, DDD 不仅可以提高软件质量,还能增强在开发过程中的沟通和协作,构建出真正符合业务需求的高质量软件

本文灸哥会用一个项目实例,来带着大家一起用 DDD,看看我讲的如何?也看看你学的如何?欢迎你留言交流~

导读

DDD 概述

领域驱动设计,Domain-Driven Design,DDD,是由 Eric Evans 在其 2003 年《领域驱动设计:软件核心复杂性应对之道》 中首次提出并进行了系统性地阐述,作者认为 DDD 是一种开发复杂软件的方法。

DDD 强调 以业务领域为核心 来构建软件架构。DDD 是为了解决复杂业务领域的软件建模问题。

在软件工程里,DDD 属于软件开发方法学的范畴。它继承了面向对象和敏捷方法的精华,并提炼了一套更容易掌握的原则、模式和实践,特别适合复杂的企业应用的开发。

DDD 重要性

从上图中四个场景的描述,我们可以看到 DDD 可以:

  1. 帮助企业来解决实际问题

  2. 对企业内开发微服务、遗留系统改造、架构治理、企业架构设计、研发效能提升、架构师培养等很多方面起到很好的指导和实践作用

  3. 用好 DDD,可以让企业应对数字化时代的挑战,助力数字化转型

DDD 实践难点

但是我们在使用 DDD 的实践过程中,常常是难点频出,从我的经验来看,主要有以下几点,不知道你在用 DDD 的过程中有没有遇到过呢?

  1. 领域建模不好掌握:领域建模是软件研发的核心,需要有深厚的方法和技巧

  2. 领域模型不好实现:基于领域模型实现同样要求深厚的面向对象基础

  3. 概念混乱:DDD 概念太多,而且同一个概念不同的人讲的不一样

  4. 技术发展:DDD 提出已经 20 多年,理念和方法要跟随时代技术发展

  5. 落地问题:落地执行障碍多,比如新老兼容、DDD 融入日常开发流程、如何评估 DDD 的成效等

DDD 学习套路

虽然 DDD 实践起来难点很多,但是它也是可以掌握的,我推荐给大家的学习思路主要有四点:

  1. 基于认知规律:根据灸哥我这篇文章跟着我,在三个迭代中去模拟使用 DDD

  2. 立足原书夯实基础:原书一定要学,二十年内出现的新内容和新观点同样要学

  3. 补充面向对象基础:DDD 要求面向对象设计能力深厚,尤其是分析和设计、UML、模型到代码的转换等

  4. 面向实践避免空谈:学习的知识必须要动手实践才能转化为技能

DDD 与传统开发方法的比较

迭代一 DDD 基础:第一个实战项目

需求描述:

在迭代一中我将使用 DDD 的设计思想来进行,请大家跟着我一起走。

第一步 需求分析与事件风暴:与业务团队的有效沟通

DDD 的研发流程是从捕获行为需求开始,在这一步我们通常要做的事情有:

  1. 识别需求里有哪些流程、哪些功能

  2. 每个功能由什么人操作

  3. 操作之后产生什么结果

在传统软件开发方法论里,常用的方式是用例建模,而在 DDD 中,称之为事件风暴。事件风暴的过程如下图:

通过事件风暴,我们可以从头脑中挖掘需求,可以尽量把需求补充完全,同时以协作的方式来保证业务和技术人员对需求理解一致,当然,事件风暴可以辅助我们来识别领域对象。

这一步通过文字可能很难理解和掌握,我进我最大的努力,如果有不明白的读者朋友们,可以联系灸哥交流。

01 识别领域事件(以需求中的项目管理流程为例)

首先,领域专家和架构师按照自己的理解写出项目管理中各个领域事件,然后,对比两者的不一样并开始讨论,最终得出一致的结果。

这里用我课件中的一页 PPT 来给大家展示,静态的图片可能无法让大家体验这个事件风暴的过程,容我想想挑战以下自己,录一个讲解视频。

在这个过程有以下几点提醒给到大家:

  1. 不要把技术事件当做领域事件,因为领域事件一定是领域专家关注的,要用业务术语,不要出现“缓存已命中”类似的技术术语

  2. 查询功能不是领域事件,因为领域事件是对某事物产生影响并被记录,一般是被创建、修改和删除,还包括向目标方发消息

  3. 统一语言是 DDD 的核心模式之一,贯穿整个 DDD 的全过程

  4. DDD 需要业务人员和开发人员使用的语言要一致,就是要求所有项目中的人员对领域知识的理解一致

  5. 协作是事件风暴的精髓

02 识别命令

命令是指引发领域事件的操作,可以从对领域事件的分析得到,识别任务一般包括:

  1. 识别命令本身

  2. 识别谁执行的命令

  3. 识别为了执行命令要查询什么数据

比如,对于合同已签订这个领域事件,命令是签订合同,由销售人员执行,在执行这个操作的时候,需要先从系统中查询客户信息,才能和客户签订合同。

按照个分析思路,我们对这样一个系统中的命令识别完成,具体的见下图所示。


03 识别领域名词

领域名词是指从命令、领域事件、执行者、查询数据里找到名词性概念,识别领域名词的目的是为了找到领域模型中的对象。我们对这样一个示例需求,进行领域名词识别后的结果如下图所示。

第二步 领域建模:深入理解业务需求

第二阶段领域建模,是通过建立领域模型,把需求里的主要业务知识描述清楚,领域模型是 DDD 的核心。

领域模型是用领域模型图来表达的,通常用 UML 来绘制,关于 UML 的基础能力,本文不做介绍,各位看官自行准备。

我们进行领域建模是出于以下两个目的:

  1. 把知识可视化,准确、深刻地反应领域知识,并且在业务和技术人员之间达成一致

  2. 指导系统的设计和编码,领域模型要求比较容易地转化成数据库模式和代码实现

领域建模任务

领域建模主要是识别领域对象、领域对象之间的关系以及领域对象的关键属性,必要的时候还要把领域对象组织成模块。

领域对象是指系统中要处理的各种事物。比如需求中的项目、员工等。有些名词化了的动词也可能是领域对象,比如支付就是一个领域对象。

假设每个领域名词都是一个实体,从本次项目中我们初步识别出的实体有:租户、企业、开发中心、开发组、部门、员工、管理员、人事人员。

实体关系的识别

一般我们现在进行 DDD 设计的时候,涉及到实体之间的关系包括以下几种:

  1. 一对一:需求中的租户和企业就是典型的一对一关系,不过各位可以思考下,既然这两者是一对一的关系,是否可以合并为一个实体呢?

  2. 一对多:需求中的企业和开发中心是一对多的关系,但是在分析的过程中,随着直属部门、开发组、员工这些实体出现后,你会发现途中的线就多了起来,图越来越乱了,这时候应该怎么办呢?这时候就需要引入抽象,要求我们对企业、开发中心、开发组、直属部门这几个实体进行抽象,找到共性,比如我们可以用组织这个实体来表示这几个实体的共性,同时增加组织类别来区分这几个实体的个性特征

  3. 多对多:需求中员工和岗位之间的关系就是多对多的关系,但是多对多的关系有时候比较麻烦,我们一般会通过引入一个表示关联的实体来把多对多拆分成两个一对多的关联

对于实体关系识别完成之后,在领域建模的阶段我们还需要对操作进行识别。比如需求中的客户经理上级、销售人员上级、项目经理上级这些概念,他们不是独立的实体,更像某种关系,所以我们可以引入组织负责人的概念,通过操作来达到目的。

实体关系识别的这个过程我这里文字内容表述的相对简单,后续我会用视频的方式把这个过程整体讲解以便分解给大家。

到这里,其实我们领域建模的任务基本就完成,大家可以看看整个过程中我形成的结果也就是模型图是什么样子的

上面的这张模型图基本覆盖了迭代一事件风暴中的所有领域概念,但是乱了,“蜘蛛网”不容易理解,那应该怎么办呢?我们可以通过模块化来解决这个问题,模块化是把模型中的业务概念组织成多个高内聚的模块,模块之间尽量低耦合。

模型图进行模块化后,分为租户管理、组织管理、项目管理、工时管理四个模块,清晰了!有了模块之后就可以从两个层面去理解模型:

第一个是宏观层面,只关心模型中有哪些模块以及模块间的依赖关系,不需要关心模块内部的细节。

第二个是微观层面,深入到模块内部,了解实现和关联等细节。但是如果模型更加复杂,光靠包图模块化就不够了,需要限界上下文来处理,迭代三介绍。

在这个阶段结束之后,我们还有一个任务,那就是要完善产出业务规则表和词汇表,示例如下:

第三步 数据库设计:根据领域模型设计数据库

这个阶段是根据领域模型进行数据库设计,数据库设计属于模型的实现。主要是建立物理数据模型,任务是对数据库表进行设计:

  1. 建立哪些表

  2. 表中有哪些字段

  3. 表的主键和外键是什么

  4. 字段的数据类型以及约束是什么

这个部分不做案例说明了。但是大家要清楚,DDD 的数据库设计主要是根据领域模型来进行设计,保证数据库和领域模型一致,从而保证数据库和业务需求以及代码的一致性。

对数据表、字段等的命名要依据词汇表,保证统一语言。

第四步 架构设计:打造灵活稳固的系统蓝图

这个阶段是基于领域模型,开始做架构设计,包括组件内和组件间的架构,微服务的设计属于组件间架构,DDD 分层架构属于组件内架构。

你的代码中有没有这样的问题?

  1. 很难识别出反映领域逻辑的代码,难以保证与领域模型的一致性

  2. 应该内聚的逻辑分散在不同地方,应该解耦的逻辑又混在一起,代码难以理解

  3. 修改业务代码影响技术代码,修改技术代码影响业务代码,维护成本巨大

  4. 各种硬编码,代码日益混乱,“屎山”越来越严重,质量问题频出

如果出现这些问题,那么你的代码结构可以称之为“大泥球”,难以维护、毫无规范。解决大泥球的最佳方案就是分层架构,比如之前讲的六边形、洋葱、COLA。

分层架构体现了软件架构设计的一个重要原则:代码中不稳定的部分,应该依赖稳定的部分,表现上就是越是内层就越稳定,越是外层就越容易变化。

关于分层架构的设计思想、设计过程请各位参照我前几天刚发布的文章:

第五步 代码设计:将模型转化为运行代码

这个阶段是根据上面全部设计的结果开始进行代码实现。分层架构是搭建了代码的“骨架”,接下来就需要给他“添血加肉”,代码谁都会写,但是按照 DDD 的要求,代码应该怎么写?

贫血模型 VS 充血模型

贫血模型是指领域对象中只有数据没有行为,代表面向过程编程范式,充血模型是既包括数据也包括行为,代表面向对象编程范式。

我个人是中立派,我主张把面向对象、面向过程以及函数式等编程范式结合使用,因为这些编程范式本身并非非黑即白的关系。

架构师不能寻求唯一正确的方法,而是要在多种不同方法中做出权衡。建议:在领域对象不直接或间接访问数据库的前提下,尽量使用充血模型。

开卡 VS 验卡

敏捷开发中,在真正动手编码之前和研发在此确认需求称之为“开卡”,代码开发完成提交测试之前会和领域专家/测试人员确认大体上有没有方向上的错误,这称之为“验卡”。这两个步骤可以有效避免因为需求理解不一致导致的返工。

往往在开卡和验卡的实践中,是会发现遗漏规则或者新增规则的,这时候就要不停去更新前面各个过程的产物。这才是 DDD 和敏捷开发结合之后的常态。

表意接口模式

DDD 强调每个类和方法的命名应该尽量直观地反应领域知识,与统一语言保持一致,这种模式也称之为表意接口模式。

领域服务模式

按照面向对象的要求,领域逻辑本应该在领域对象中,不过有些逻辑不适合,就干脆放到一个只有方法,没有状态的类里面,这些类称之为领域服务。

工厂模式

领域对象的创建逻辑属于领域层的一部分,如果创建领域对象的逻辑比较简单,可以直接使用对象的构造起来实现,但如果比较复杂,就需要把创建逻辑放到一个专门的机制里去,来保证领域对象的简洁和聚焦,这种机制就是 DDD 的工厂模式。

迭代一总结

DDD 开发的基本流程以领域模型为核心,整个流程可以分为模型的建立和模型的实现两部分:

  1. 模型的建立使用业务语言

  2. 模型的实现使用技术语言

本次迭代我们对 DDD 应该要求掌握的如以下概念图:

迭代二 DDD 进阶:掌握领域知识

迭代二的目标是掌握一些 DDD 更高级的技能,开始处理更复杂的需求,主要包括聚合、值对象、限定和泛化。

首先我们来这个迭代的需求描述:本次实现的系统目标是一个 SaaS 系统,要求这个系统可以应对不同公司的需求,来提升增加系统的灵活性。具体需求要求如下图。

进阶技能一:聚合

来看这样一个具体的场景,两个 HR A 和 B 同时录入张工的技能,操作顺序:

  1. A 查询到张工没有 Java 技能,同一时刻,B 也查询到张工没有 Java 技能

  2. A 为张工添加 Java 技能并提交,还没有彻底保存到数据库的那一刻,B 也提交了

  3. A 和 B 的数据都保存到了数据库,张工存在两条 Java 技能,不变规则被破坏

这是一个典型的因为并发引起的问题,很多人会想到使用事务和锁来解决。但是在这个场景中锁什么呢?锁住一条记录确实就可以解决问题,但是新建的时候数据库还没有记录。问题的关键是因为从业务的角度,张工的员工信息是一个整体,不应该同时被两个人操作,从技术的角度看,不应该被两个线程同时操作,只有一个人做完了,另一个人才可以做,如何保证这一点呢?

这就需要在一个人操作时,用事务把包含技能在内的员工信息作为一个整体锁起来,等到这个事务提交后,锁被释放,另一个人才能操作。员工和技能都在同一个事务边界内。

类似于员工和技能这两个实体,他们具有整体和部分的关系,同时具有不变规则(类似技能这种在员工的整个生命周期内都是有效的规则,称之为不变规则),而且这种不变规则在并发的时候有可能被破坏,具有这样特点的多个领域对象,在 DDD 中称之为聚合(Aggregate)。工作经验和员工也是一个聚合。

在一个聚合里,类似员工这样代表整体的实体就是聚合根,一个聚合只有一个聚合根。

关于聚合你要知道:

  1. 作为部分的实体,只能属于一个聚合根,不可能属于多个聚合根

  2. 一个聚合的一部分不能再变成其他聚合根的一部分(我的腿不会离开我)

  3. 聚合根一旦被删除,聚合中的所有对象都会被删除

  4. 聚合根要有全局的唯一标识,而从属于聚合根的实体只需要在聚合内有唯一标识

聚合外部的对象对非聚合根对象只能读不能写,而且必须通过聚合根才能对非根对象进行访问。技术上一般是通过包级私有权限封装构造器和方法、返回不可变列表、用聚合根创建和访问非根对象等来实现。

进阶技能二:值对象

DDD 的领域对象分为两种:实体和值对象。该项目中的组织、员工都是实体,而值对象是用来描述实体的属性值的。

用前面的员工状态和员工来举例,对于员工我们有两个业务规则:

  1. 试用期的员工才能被转正

  2. 已经终止的员工不能再次终止

我们很多人在设计对象和编写代码的时候,很多会这么写,把改变员工状态的业务规则放在员工对象中实现。

package chapter18.unjuanable.domain.orgmng.emp;
// imports ...

public class Emp extends AggregateRoot {
    // 其他属性 ...
    protected EmpStatus status;

    //其他方法 ...

    public Emp becomeRegular() {
        onlyProbationCanBecomeRegular();
        status = REGULAR;
        return this;
    }

    public Emp terminate() {
        shouldNotTerminateAgain();
        status = TERMINATED;
        return this;
    }

    private void onlyProbationCanBecomeRegular() {
        if (status != PROBATION) {
            throw new BusinessException("试用期员工才能转正!");
        }
    }

    private void shouldNotTerminateAgain() {
        if (status == TERMINATED) {
            throw new BusinessException("已经终止的员工不能再次终止!");
        }
    }
}

在这个例子中员工状态就是一个值对象,肯定都已经猜到这两个业务规则的校验应该放到员工状态这个值对象里面,而非员工对象里面。修改后的代码我们再一起来看看:

package chapter18.unjuanable.domain.orgmng.emp;
// imports ...

public enum EmpStatus {
    REGULAR("REG"),           // 正式
    PROBATION("PRO"),         // 试用期
    TERMINATED("TER");        // 终止

    private final String code;

    EmpStatus(String code) {
        this.code = code;
    }

    public String code() {
        return code;
    }

    //其他方法 ...
    
    public EmpStatus becomeRegular() {
        if (this != PROBATION) {
            throw new BusinessException("试用期员工才能转正!");
        }
        return REGULAR;
    }

    public EmpStatus terminate() {
        if (this == TERMINATED) {
            throw new BusinessException("已经终止的员工不能再次终止!");
        }
        return TERMINATED;
    }
}

员工状态这个对象中,转正和终止的两个方法,都会先验证是否满足条件,满足的话就会返回对应的状态,否则抛出异常。

package chapter18.unjuanable.domain.orgmng.emp;
// imports ...

public class Emp extends AggregateRoot {
    // 其他属性 ...
    protected EmpStatus status;

    //其他方法 ...
    
    public Emp becomeRegular() {
        status = status.becomeRegular();
        return this;
    }

    public Emp terminate() {
        status = status.terminate();
        return this;
    }

    // 删除了 onlyProbationCanBecomeRegular() 
    // 删除了 shouldNotTerminateAgain() 
    
}

而修改后的代码中,在员工这个实体对象中就不再校验这两条业务规则了,委托给了员工状态的值对象来执行,代码更加简洁。

当然,需求中的时间段也是可以作为值对象来实现。类似这两个对象到底有什么共性呢?那就是他们存在同一性,Identity,如果表面上看起来有两个对象,但实际上指的是同一个东西就说明这两者具有同一性。

我们在一家公司,一般用工号来区分员工,有一个员工叫赵迪,工号是 10,去年 25 岁,今年 26 岁,年龄虽然变了,但是赵迪还是那个赵迪。公司还有另外一个赵迪员工,工号是 20,今年年龄也是 26 岁。但此赵迪非彼赵迪,因为工号不一样。

员工是实体,实体就是要靠独立于其他属性的标识来确定“同一性”的。但例子中的员工状态和时间段就没有这样的标识,他们是所有的属性加在一起才是自身的标识,没有独立于其他属性的标识。很多人会觉得,我给他们加一个 ID 不就可以了吗?

ID 是一个技术上数据库主键的属性,对于时间段来说,起始日期和结束日期组合在一起就是标识,是不需要其他单独的属性来作为标识的。也就是说:时间段的所有属性值作为一个整体确定了自身的同一性。只要其中的属性值变化了,那就是另外一个对象。时间段【2023 年 1 月 1 日 - 2023 年 2 月 1 日】的结束日期变成 2023 年 2 月 2 日,那新的时间段【2023 年 1 月 1 日 - 2023 年 2 月 2 日】就是一个新对象了。

在 DDD 中,员工这样有单独的标识,理论上可以改变的对象,就称之为实体(Entity),而像员工状态和时间段这样没有单独的标识,并且不可改变的对象,叫做值对象(Value Object)。

实体是一个真实存在的东西,值对象是一个值,用来描述一个实体的属性。

对于值对象的不变性,在代码实现上倒是比较简单的,只需要把这些对象的属性值作为构造器的参数传入来创建对象即可,且不提供任何方法来改变这个对象。

进阶技能三:限定

限定是一种建模技术,在原书中,专门强调了限定这个方法,因为限定可以起到丰富模型语义和简化关联的作用。我会用需求中的员工和工作经验的关系来给大家说明限定这个概念。

业务规则:对于一个员工而言,每个时间段只能有一条工作经验或者没有工作经验,但绝对不能有多条工作经验。换句话来讲:一个员工可以有多条工作经验,但限定在一个时间段的话,那最多就只能有一条工作经验。这种机制就叫做限定(Qualification)。

限定在数据库的实现策略上,相对比较简单,只需要在之前的工作经验表中增加一个唯一索引即可:

● 工作经验表:租户 ID、员工 ID、开始时间、结束时间四个字段的唯一索引

在代码中,限定一般使用 Map 来实现。

进阶技能四:泛化建模

泛化是领域建模的高阶技能,以在子项目中上报工时的需求来讨论泛化建模的实现。

首先我们增加子项目以及工时和子项目的关联。

你按照上图设计,就会发现这样一个问题:一个工时记录必须要关联一个项目,但有了子项目之后,就有可能工时记录只要报在子项目,而不用在项目中报,图中工时记录和项目的 1:1 就不对了,继续优化吧~

钢铁直男们,你们可以啊,就直接改成 0:1 之后解决了刚才的问题,但是也不对啊,图中的修改说明工时记录可以不关联项目也不关联子项目了,另外一条工时记录还可以既报在项目中,也可以报在子项目中。这里正确的逻辑应该是:一条工时记录要么关联一个项目,要么关联一个子项目,但不能同时关联两者,也不能两者都不关联。

逻辑要求:项目和子项目都是可以报工时的东西,而一条工时记录,必须要关联到一个这种东西上。暂时给这种东西取名为工时项,一个工时项可以关联 0 到多条工时记录,一条工时记录必须关联且仅关联一个工时项。

这里的工时项的含义比项目和子项目要更广泛,这就叫泛化,工时项是项目和子项目的父类,项目和子项目是工时项的子类。

自然语言中泛化的四种场景:

  1. A 和 B 可以统称为 C(统称),男人和女人统称为人。

  2. C 分成两类 A 和 B(分类),人可以分成男人和女人。

  3. 一个 A 是一个 C,一个 B 也是一个 C(is-a),一个男人是一个人,一个女人也是一个人。

  4. A 和 B 具有共性(共性/个性),男人和女人具有共性,表示共性的概念叫人

在 DDD 中,我们可以把这四种场景都可以用泛化来进行表达和建模,泛化是一种强大的抽象机制,能够同时表现出不同对象间的共性和个性。

泛化可以通过抽象思维得到更深刻的领域知识,可以让模型更简洁,但是也是因为抽象,有时比较难理解,同时还要考虑技术上是否容易实现,是否容易保证代码和模型一致性的问题。

那对于泛化来说,就不是一个非黑即白的问题了,而是一个权衡策略。泛化在实践中,确实是一个比较容易被过度使用的技术,最终结果就是过度设计,造成不必要的复杂度,甚至项目失败。

是否要采用泛化,没有一个唯一正确答案,需要架构师站在业务和技术的双视角进行权衡。

一般权衡是否使用泛化有两个视角:

  1. 业务视角:实际上是业务人员和技术人员都理解的视角。站在这个视角我们要考虑引入泛化后,有没有在模型里增加新的知识,有没有让模型更加简洁,更容易理解。

  2. 技术视角:是要考虑模型是否能够自然、直接地映射到设计模型和代码

泛化的数据库实现一般有三种策略:

  1. 每个类一个表,父类表存放公共属性,用一个标识类型来区分不同子类,子类中存放各自属性

  2. 每个子类一个表,父类不单独建表,公共属性分别都在子类中存储

  3. 整个泛化体系一个表,父类和子类的属性都放在这个表中,但需要区分不同子类的类型字段

这三种策略如何选择,其实没有答案,需要权衡!

泛化在代码中的实现方式:

  1. 用类的继承或接口的实现领域对象的代码

  2. 用仓库实现内存中的对象和数据库表中的数据之间的双向转换

到底用类继承还是接口实现,则要根据具体情况而定。

迭代二总结

本次迭代主要讲了聚合、值对象、限定和泛化。

迭代三 DDD 高阶:在大规模复杂场景中的应用

本次迭代主要是为了解决以下问题以及实现新的功能需求:

在这次迭代中,我们要掌握限界上下文、CQRS。

关于 CQRS,我这里就不做介绍了,各位可以参照我之前的文章:命令查询责任分离模式(CQRS):分离读写模型以提升性能

限界上下文

现代企业现状的这些问题,你们有吗?

  1. 沟通变得困难:人少的时候直接说一声就解决了,现在是不断开会,有时候以为沟通到位了,但是代码写完,就会发现没理解

  2. 代码质量变差:已经用了 DDD 的分层架构,但是难以避免代码的腐化,尤其是老员工离职,新人接手,特别容易出问题

  3. 外在质量问题多:新需求稍微复杂一点,就很容易牵一发而动全身,改一个地方,就要改很多其他地方,漏了就错

  4. 需求互相等待多:系统是单体架构,不同需求之间的互相等待

  5. 系统性能变差了:并发的人数越来越多,当前部署的几台机器不够

需要扩充但是部署成本高。从紧急程度看当前只有报工时的功能需要扩充,但是因为单体就必须同时扩展所有功能

这时候应该怎么办呢?拆分,是时候把系统拆分成几个子系统了,那怎么拆呢?这就是该考虑 DDD 中的限界上下文。

限界上下文可以简单理解为和划分模块、划分子系统类似,是一种分而治之的手段,可以起到分离关注点的作用。但限界上下文和他们不同的是,它的目的还在于维护概念一致性。正因为这点,造成限界上下文和传统的方法本质不同。

DDD 认为面对大规模的系统,全局概念一致性从根本上是不可能的。这是因为人的认知能力是有限的。由于大型软件是团队协作开发的,当系统规模增大时,团队规模也相应增大,沟通难度会加大,当系统达到一定规模,就超过了一个团队的认知能力,无法保证概念一致性。这就需要把大系统拆分成多个子系统,每个子系统对应一个领域模型。每个模型的规模都不超过一个开发团队的认知负载。在每个子系统的内部实现概念的严格一致性,而不同系统之间没有必要严格一致。

考虑到各种因素之后,我们把示例项目的限界上下文划分如下:

大家要明白,划分限界上下文之后,实际上就是要求我们把一个大领域模型且分成多个小领域模型。

按照上面划分的限界上下文,本次迭代涉及到多个上下文,那架构师在设计架构的时候,就要考虑是使用单体还是微服务了。现在虽然微服务流行,但是单体也没有原罪。到底是采用哪种架构是要综合考虑的。

一般满足以下这些条件时,是可以考虑决策使用微服务架构,否则还是单体比较好。

  1. 收益大于成本。微服务是好处很多,但也提高了运维的成本并且带来了数据一致性、远程调用的性能消耗等复杂度,在收益大于成本的时候

  2. 团队有足够的运维技能和基础设施支持

  3. 团队具有一定的开发微服务的技能和经验

迭代三的总结

关于 DDD 的技能通过三个迭代,基本全部讲完,希望大家以此为基础,进一步精进,就可以向 DDD 专家迈进了。

企业引进 DDD 的建议

提醒各位,即使你已经是 DDD 专家,也不意味着你可以 自然而然地在企业内部顺利推广 DDD。

切入场景

  1. 新建应用系统:随着企业业务的不停发展和创新,刚好有新项目,需要开发一个全新的系统,又或者是为现在的系统新加或者重写一个核心大模块,这时候,为了保证质量,降低风险,可以引入 DDD。

  2. 改造现有系统:长期发展的大企业内部,会存在维护了很多年的系统,系统架构和质量已经腐化,很难维护,不能满足快速变化的业务需求,这时候是可以引入 DDD 对系统进行比较大的重构。另外就是如果企业决策要进行服务化改造,那可以把一些现有的单体应用逐步拆分成微服务。

  3. 改造现有研发流程:企业现在资源和预算可能无法进行新建或者改造系统,但是作为技术领导层已经看到研发流程中的种种不足,在这么下去,可能会出大事,那这时候是可以引入 DDD 的一些方法,提高研发水平和效能。

试点项目和团队选择策略

这里要综合项目和团队一起考虑,不能孤立地考虑其中一个。

  1. 价值点:价值挖掘一定要站在企业角度,因为实施 DDD 成本较高,只有在较大价值点上才能带来足够的回报

  2. 痛点:痛而思变,如果不痛,哪怕是痒,先忍着,不要为了引入而引入 DDD

  3. 意愿点:团队愿意学习 DDD,项目经理、产品经理、开发负责人、架构师、技术人员等

  4. 时间点:引入 DDD 有不小的学习成本和试错成本,要平衡好时间点


领域驱动设计(DDD)提供了一套系统的方法,来帮助开发团队在构建高质量软件时更好地理解和解决复杂的业务问题。

通过深入理解领域、建立统一语言、使用实体和值对象等核心概念来建模、应用仓储模式和领域服务等实践,团队是可以更高效地开发和维护软件系统。

DDD 不仅适用于复杂的企业级应用,也适用于任何需求深入理解业务需求的场景,作为软件开发者和架构师,掌握 DDD 的原则和实践,将有助于我们构建出更加健壮、灵活和可维护的软件系统。

希望灸哥的这篇文章对你在实际项目中应用 DDD 有所帮助,助你构建出高质量的软件。


长按二维码识别关注

关注灸哥

了解更多


你的赞和在看,我统统都要

原文作者:灸哥漫谈;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2023-11-26

  • 博文量
    368
  • 访问量
    173296