浅析 DDD - 一个 PHP 程序员的自述

XuDing

DDD 是什么(首先它不是什么)

DDD 是 Domain Driven Design 的缩写,在中文中常被翻译为领域驱动设计。在我们了解 DDD 是什么之前,首先讨论下它不是什么。

  • DDD 不是一个软件框架。但是基于 DDD 思想的框架是存在的,比如 Axon,它是以 DDD 为指导思想,使用 Java 实现的一个微服务软件框架。
  • DDD 不是一种软件设计模式。它不是像工厂,单例这样子的设计模式。但是 DDD 思想中提出了诸如资源库(Repository)之类的设计模式。
  • DDD 不是一种系统架构模式。它不是像 MVC 之类的架构模式。但是 DDD 思想中提出了诸如事件溯源(Event Souring),读写隔离(Command Query Responsibility Segregation) 之类的架构模式。

那么 DDD 到底是什么

软件是服务于人类,为提高人类生产效率而产生的一种工具, 每一个软件都服务于某一个特定的领域。比如一个 CRM,它是以管理客户数据为核心,帮助商户与客户保持联系的工具。

而软件的实质是计算机中运行的代码,如何将抽象的代码更准确地映射到人类所关心的领域中,这是软件开发者一直在探寻的话题,不管是函数式编程(FP)还是面向对象编程(OOP)也好,都是为了帮助开发者开发出更贴近于领域中的软件模型。

在传统的软件开发方法中,我们常常会遇到一系列影响软件质量的技术以及非技术问题:

  • 开发者热衷于技术,但缺乏设计和业务思考。开发人员在不完全了解业务需求的情况下,闭门造车,即使功能上线也无人问津。
  • 代码输入而非业务输入。技术人员对技术实现情有独钟,出现杀鸡焉用牛刀的情况。
  • 过于重视数据库。以数据库设计为中心,而非业务来进行开发,结果往往是,软件无法适应一直在变动的业务逻辑。

DDD 是一种设计思想,一种以领域(业务)为出发点,以解决软件建模复杂度为目的设计思想,我们也可以将其理解为一种建模的方法论。

DDD 的设计思想分为战略和战术两部分。

我们可以将战略设计理解为宏观层面的设计,它的目的包括,分析业务的复杂程度,拆分业务的领域范畴,指导业务整合方式等等 。我们可以将战术设计理解为微观层面(代码层面)的设计,它为我们在实现业务逻辑提供一系列工具。

战略设计

通用语言(Ubiquitous Language)

语言本是人类沟通的基本工具,然而开发人员习惯了使用技术术语,领域专家(领域专家在此泛指精通业务的专家,比如用户,客户等等)对技术术语毫不关心,于是造成了不可避免的沟通问题,一旦沟通出现问题,开发出来的软件便很难解决领域专家的真正痛点。

通用语言是 DDD 思想的基石,它是开发人员和领域专家共同创建一套沟通语言,一套在团队中流行的,通用的沟通语言,团队的组员之间可使用通用语言进行无障碍交流。

这要求开发人员摒弃技术味浓重的技术术语,与领域专家合作,像领域专家一样关注业务问题,共同挖掘并打磨业务中的术语,创建一套通用语言。

通用语言往往可以直接应用于代码中,它可以直接被写成一个类或者一个类的方法。

比如在开发一个购物车时,与其使用技术术语:

  • Cart::create(): 创建一个购物车。
  • Cart::updateStatus():更新购物车状态。
  • Cart::remove():移除购物车。

我们不妨使用更贴近业务的通用语言:

  • Cart::init(): 创建一个购物车。
  • Cart::addItemToCart():添加商品。
  • Cart::removeItemFromCart():移除商品。
  • Cart::empty():清空购物车。

在使用后者时,开发人员不用解释每一个类方法的意义,领域专家可以直接看懂每一个类方法的目的。开发人员甚至可以和领域专家坐在一起使用代码来打磨业务流程。

限界上下文(Bounded Context)

在实现了通用语言自由以后,我们需要使用限界上下文来规定每一套通用语言的使用边界。限界上下文是语义和语境的边界,在其内的每一个元素都有自己特定的含义,也就是说每一个概念在一个限界上下文中都是独一无二,不可以出现一词多义的情况。

我们可以用一个简单的例子来解释限界上下文。比如在一个购物车的限界上下文中,我们可以用 User 一词来代表购买商品的客户。而在一个注册系统中,我们可以用 User 一词指的是带有用户名和密码的账号。虽然词汇一样,但是在不同的限界上下文中,它们的含义不同。

我们使用限界上下文和通用语言,对业务进行语言层面的拆分。限界上下文为领域中的每一个元素赋予清晰的概念,开发人员也就不会将情不自禁的在脑海中闪现多个支撑一个元素的概念,避免写出“大泥球”(big ball of mud)代码。

子域(Subdomain)

如果说限界上下文是对业务进行语言层面拆分的话,那么子域便是对业务进行商业价值的拆分。每一个商业都有自己的关注点,即便是看起来一样的电商平台,淘宝是开放平台模式,京东是价值链整合模式,一个明显的区别是,淘宝使用第三方物流而京东自建物流体系。

那么作为一个开发人员,为何要关心看起来似乎与自己无关的商业模式呢?恰恰相反,只有当我们了解一个商业的结构时,才能开发出一个主次分明的系统来支撑一个商业的飞速发展。

子域便是这样一个帮助我们划分主次的工具。

有三种类型的子域:

  • 核心域(Core Domain):这是系统中需要最大投资的领域,它代表着整个商业的核心竞争力。我们需要花大量资源以及资源来打磨核心域,这关乎一个企业的存亡。比如京东的自建物流系统。
  • 支撑域(Supporting Domain):此领域并非一个企业的核心业务,但是核心域却离不开它,它可以采用外包定制方案实现。比如认证上下文,权限上下文。
  • 通用域(Generic Domain):如果已有成熟的解决方案,通用域可以采购现成方案来,如果没有,也可以采用外包,在通用域上的投资应该是最小的。比如对于淘宝而言,物流便是其通用域。

限界上下文和子域的关系众说纷纭,有专家提倡1:1,也有专家提倡1:N。个人比较提倡 1:1, 由于受实现领域驱动设计一书作者影响比较深。

上下文映射(Context Mapping)

在一个庞大的系统中,限界上下文之间必定存在一定的依赖关系。如何将一个上下文中的概念映射到另一个上下文中?我们使用上下文映射。

以下是几种上下文映射的关系类型:

  • 合作关系(Partnership)
  • 共享内核(Shared Kernel)
  • 客户方-供应方开发(Customer-Supplier Development)
  • 遵奉者(Conformist)
  • 防腐层(Anticorruption Layer)
  • 开放主机服务(Open Host Service)
  • 发布语言(Published Language)
  • 另谋他路(SeparateWay)
  • 大泥球(Big Ball of Mud)

以上都是比较抽象的概念,有时两个限界上下文之间也可以存在多重关系。

以上便是 DDD 战略设计的几个核心概念。

战术设计

如何为限界上下文中的概念建立模型,我们将使用的是 DDD 所提供的战术设计。

实体(Entity)

首先我们讲到的是,实体。

实体是领域中独立事物的模型,每个实体都拥有一个唯一的标识符,比如 ID, UUID,Username 等等。大多数情况下,实体是可变的,它的状态会随着时间的迁移改变,不过,一个实体不一定必须可变。

实体的最大的特征是它的个体性,唯一性。比如在一个简单的购物车上下文中,订单(Order) 便是一个实体,ID 是它的标识符,它的状态可以在提交(placed),确认(confirmed) 以及已退 (refunded) 之间变化。

值对象(Value Object)

值对象是领域中用来描述,量化或者测量实体的模型。和实体不同,值对象没有唯一的标识符,两个对等的值对象是可以替换的。值对象具有不变性(Immutability),一旦创建以后,一个值对象的属性就定型了,不可更改。

理解值对象的最直接的方法是,想象我们现实生活中的钞票,在日常生活中,甲的十块钱人民币和乙的十块钱人民币是可以对等交换的。

在上文的购物车上下文中,金额(Money)便是一个值对象,金额由货币(currency)和数目(amount):

class Money {
  public $currency;
  public $amount;

  function __construct($currency, $amount) {
    $this->currency = $currency;
    $this->amount = $amount;
  }  
}

使用值对象进行建模,能帮助我们用代码更精确地建立领域模型。

聚合(Aggregate)

聚合是什么?聚合是上下文中对业务领域更精细的划分,每一个聚合保证自己的业务一致性。

那么什么是业务不变性?业务不变性表示一个业务规则,该规则在业务领域中不可违背,必须保证其一致性。比如,在进行订单退款时,退款金额不可以超过已付金额。

聚合的组成部分是实体和值对象,有时候也只有实体。为了保护聚合的业务一致性,每个聚合只可以通过某一个实体对其进行操作,该实体被称为聚合根。

领域事件(Domain Event)

领域事件是通过通用语言分析出来的事件,与常见的事务事件不同的是它与业务息息相关,所以它的命名往往夹带业务名词,而不应该与数据库挂钩。比如购物车增添商品,对应的领域事件应该是 ProductAddedToCart, 而不是 CartUpdated

总结

DDD 还提供了诸如应用服务(Application Service),领域服务(Domain Service) 等战术设计,DDD 还提出了文章开头就提过的事件溯源,六边形等架构模式,在此我们将不一一介绍。

DDD 的核心是从业务的角度为软件建立模型,其目的是打造更贴近业务的代码,能更直观的从代码理清业务流程。 然而实现 DDD 并非一日之举,它需要不断的实践,不断的打磨。

本文转载自【何以解耦】:https://codedecoupled.com/intro-ddd.html,如果你也对 TDD,DDD以及简洁代码感兴趣,欢迎关注公众号【何以解耦】,一起探索软件开发之道。

阅读 577

全职编程,兼职跑步,临时健身。

7 声望
0 粉丝
0 条评论

全职编程,兼职跑步,临时健身。

7 声望
0 粉丝
文章目录
宣传栏