领域驱动设计,到底在讲什么?

1. 业务系统和非业务系统

领域驱动设计(DDD),我们现如今讲的非常多,好像在业务软件开发中,哪哪儿都在用,但是就是说不清楚 DDD 到底是什么意思。

这其实与软件的发展有关,年轻的开发者没有接触过早期的软件开发,对很多技术的演变不甚了解,因此一些在老开发看来水到渠成的概念,对年轻的开发者而言,却似是而非,比如 DDD,现如今的业务软件开发,基本都离不开这个概念,年轻开发者一开始开发,就沿着前辈们搭好的框架一顿开发,对他们而言,业务软件开发,似乎天生就应该是这样的架构,就应该遵循先需求评审、再代码实现,因此虽然他们一直在按照 DDD 的方式开发软件,却对 DDD 不甚了解,犹如雾里看花。

其实 DDD 很简单,他不是一种新的编码技术或编码风格,他只是一种模型驱动的设计方法:通过领域模型捕捉领域知识,使用领域模型构建更易维护的软件

1.1 非业务系统(早期软件开发)

上面定义有点拗口哈,这里举个例子,在 DDD 之前,软件开发一般是定义问题,解决问题,在解决问题中,一般是从开发者视角来看待如何解决问题的,因此开发者习惯于利用自己熟悉的工具来解决问题------数据结构

开发者对于数据结构非常了解,并且知道数据结构关联的各种算法,比如树关联着深度优先(DFS)、广度优先(BFS)等,开发者在面对问题时,通常会思考这个问题可以拆分为哪些小问题,每个小问题可以用什么数据结构结合什么算法来解决,就跟我们刷算法题一样,你看这种思考问题的方式对于开发者而言非常自然,并且这也算一种模型解决问题的方式,只不过这里的模型(数据结构)跟业务没有关联,即构造与领域无关的模型

1.2 业务系统

上面解释了非业务系统(早期软件)开发的方式,这种方式对开发者友好且自然,但对业务方而言,就是灾难,你难道指望业务方跟你提需求的时候利用数据结构跟你沟通吗?相信我,业务方会打死你的,这根本没法玩,业务方只会按照自己对业务的理解,直接定义问题而不会思考这个问题利用当前的数据结构是否能解,是否好解。

观众朋友们应该发现问题了,如何协调业务方跟开发者一起讨论需求,解决问题呢?答案是使用一套业务和开发都能理解的语言来进行沟通,而这,也就是 DDD 要干的事。

DDD 首先会对领域进行建模 ,有了模型后,会基于该模型,创建一套统一语言 ,业务方和开发者利用统一语言进行需求沟通,然后对模型进行优化提炼 ,开发者根据模型来实现业务功能,将模型跟实现关联在一起 ,因此模型的改动就是实现的改动,就能达到业务方也能轻松定义实现的目的。

这看起来是不是很熟悉?没错,这就是我们现在开发正在用的方式,构造一种专用模型(领域模型),将相关的业务流程与功能转化成模型行为,就能避免开发者与业务方出现认知偏差

这种方式在现在看来,天经地义,这也是为什么我前面说其实现在我们基本都在用 DDD,但是大家对 DDD 却似是而非,脑袋里有些模糊的想法,但就是无法对 DDD 做出准确定义的原因。

DDD 很难定义还有另外一个原因,在不同的领域中应该使用什么领域模型,并没有现成做法,对于某些领域,比如电商,已经有成熟的领域模型,比如用户、订单、商品等,我们经常在用却不会去思考为什么这样划分,因为从实践来看这样划分就是非常成熟的划分方式,但是对一些新兴领域,如何划分领域确定边界就是一个比较难的问题,因此成熟领域不用我们动脑筋,并且目前大家做的基本都是较为成熟的领域,因此一旦需要对新兴领域进行 DDD,我们就会感到无从下手。

其实对于如何实现 DDD,如何提炼领域模型,也是有方法的,那就是知识消化

2. 提炼模型:知识消化

知识消化分为五个步骤:

  • 将模型和实现关联(模型即代码)
  • 基于模型提炼统一语言(业务与开发沟通的桥梁)
  • 创建富含知识的模型(定义模型)
  • 提炼模型(修改模型)
  • 头脑风暴和尝试(需求分析和实现)

在这五步中,将模型和实现关联 起来,是前提和基础,如果模型和实现割裂分离,就会变成更接近业务的分析模型 和更接近实现的设计模型,这种时候,分析模型就会退化成纯粹的沟通需求的工具,很容易脱离实现的约束而变得天马行空,比如"根据手机壳的颜色变换主题颜色",不着边际。

基于模型提炼统一语言,将业务方变为模型的使用者,通过统一语言对模型进行需求讨论,实际就是基于模型对需求进行讨论。

后三步构成了提炼知识(模型优化)的循环,定义->修改->实践->定义,通过统一语言讨论需求,发现模型中缺失或者缺点,精炼修改模型,然后实现验证,进而重新定义新模型,然后更新统一语言,整个流程循环起来后,就能将模型不断完善。

3. 模型和实现关联

如何将模型和实现关联起来?对于用惯了面向对象的程序员而言,似乎不言而喻,封装即可,即对领域模型对象,封装其行为,使得对外界而言,只需要了解领域对象及其行为(方法)即可,这也是我们常说的充血模型

3.1 贫血和充血

贫血对象模型:对象仅仅是对数据进行简单封装,而关联关系和业务的逻辑都散落在对象范围之外,整体跟过程式代码风格类似,一个方法仅仅解决一个问题,并且方法跟对象没有关联。

充血对象模型:与某个概念相关的行为和逻辑,都被封装到领域对象中,跟面向对象的封装非常相似,对象不仅有数据,还有行为,这就是富含知识的模型,或者称之为充血模型。

上面对于贫血和充血,如果还不太理解,那我下面举一个例子:

java 复制代码
class UserDAO {
  
  ...
  public User find(long id) {
    try(PreparedStatement query = connection.createStatement(...)) {
      ResultSet result = query.executeQuery(....);
      if (rs.next) 
         return new User(rs.getLong(1), rs.getString(2), ....);
      ....
    } catch(SQLException e) {
       ...
    }
  }
}

class SubscriptionDAO {
  ...
  // 根据用户Id寻找其所订阅的专栏
  public List findSubscriptionsByUserId(long userId) {
     ...
  }
  
  // 根据用户Id,计算其所订阅的专栏的总价
  public double calculateTotalSubscriptionFee(long userId) {
     ...
  }
}

上面就是典型的贫血模型,User 和订阅是分割的概念,都能通过数据库进行 crud(可能会有外键的关联),与之相反的是充血模型,如下:

java 复制代码
class User {

   // 获取用户订阅的所有专栏
   public List getSubscription() {
     ...
   }
   
   // 计算所订阅的专栏的总价
   public double getTotalSubscriptionFee() {
     ...
   }
}

class UserRepository {
  ...
  public User findById(long id) {
    ...
  }
}

上面就是典型的充血模型,是不是很熟悉,跟我们现在写的面向对象的代码非常相似,订阅是 User 的一部分,无法脱离 User 存在,因此相关的订阅行为都被封装到 User 中。

从上面的代码中,我们也能解释一些 DDD 中的比较抽象的概念:

  • 实体:具有唯一标识和生命周期的业务对象,比如上面的 User
  • 值对象:描述业务属性的不可变对象,需要依附实体才能存在,比如上面订阅的课程
  • 聚合根:领域模型的完整性守护者

实体和值对象都还好理解,但是聚合根就比较抽象了,我一开始单纯的认为聚合根就是实体行为的集合,但后面发现这个定义不准确,聚合根应该是包含实体状态、行为方法、边界控制 三者于一体的概念,为什么这么说呢,因为聚合根的核心使命是保护内部一致性,行为只是实现手段:

  • 没有状态的行为 → 退化为无意义的工具方法
  • 没有行为的状态 → 退化为贫血模型
  • 状态+行为+边界 → 完整的领域守护者

外部只能通过聚合根来访问和操作领域实体。

3.2 现行 DDD 实现

上面有提过,DDD 不是一种编码方式或者风格,而是一种模型驱动的设计方法:通过领域模型捕捉领域知识,使用领域模型构建更易维护的软件

核心是提炼模型,对应代码中的领域对象,只要使用充血模型(合理使用),就能将模型和实现关联起来,也就是按照 DDD 的设计方式进行了实现,因此 DDD 和充血模型我理解是强关联的,任何贫血模型理论上讲都不算是 DDD,但这跟我们现有的开发模式似乎并不一致,比如我们会在领域 module 中定义好领域实体,然后并不将所有的实体方法都放在实体中,而是会写一些 service 用来实现领域实体的行为,下面以我们项目中使用的一种架构来进行说明:

如上,领域实体我们是单独定义了一个 module,外部可以依赖领域实体,比如 infra 层能将跟数据相关的对象转换为领域实体对外暴露,这就对领域实体 module 有一个要求,不能引用其他层或者其他 module,不能注入 bean 来实现某些逻辑,否则就会很容易发生循环引用等问题,那在这个模型中,似乎领域实体仅仅是单纯的贫血模型,聚合根不存在,分散在各个 service 中,但为什么我们还将我们的实现称为符合 DDD 的实现呢?

这是因为纯粹的充血也会有问题------实体膨胀,一个实体可能会变成上帝实体,所有功能包含在内,使其膨胀非常迅速,因此一般很少使用纯粹的充血模型,而是会使用一个折衷的方案,部分充血模型有限行为领域对象,这种方式能巧妙地平衡领域纯度和工程实用性。

在这种折衷的实现中,领域实体对外部无任何依赖,但是其也不是单纯的贫血对象,其内部会封装一些状态转移、状态校验、自包含的计算、不变条件的维护等行为,将对外部服务有依赖的行为放入服务中,也就是上面 domain-core 中,这样既避免了"贫血模型的知识碎片化",又规避了"充血模型的实体膨胀",是大多数业务系统的最佳实践选择。

相关推荐
晚风吹长发5 分钟前
Docker使用——Docker容器及相关命令
linux·运维·服务器·docker·容器·架构
电子科技圈14 分钟前
先进封装、芯粒架构和3D集成——先进异构集成亟需兼具标准化与定制化能力的互联及总线IP解决方案
tcp/ip·设计模式·架构·软件构建·代码规范·设计规范
止语Lab1 小时前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活1 小时前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
程序员天天困1 小时前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn1 小时前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端
AI小白Lin1 小时前
33 个 AI 专家全票通过?那问题才刚开始
人工智能·架构
用户298698530141 小时前
Python 实现 Excel 与 Markdown 互转的实用指南
后端·python·excel
用户77283104908401 小时前
krono-job:零侵入、单二进制交付的分布式任务调度平台(开源)
后端
Conan在掘金1 小时前
鸿蒙报错速查:Function lacks ending return statement,返回路径漏 return 就炸,根因 + 真解法
后端