Skip to main content

Domain Driven Design

DDD是为了解决什么问题?

According to studies, approximately 70% of software projects are not delivered on time, on budget, or according to the client’s requirements

大约7成的软件最后都是失败的,有个专门的词形容这种情况,叫"software crisis"(软件危机)。这个词1968年就被提出了,中间人们提出了各种方式去尝试解决:

  • Agile Manifesto(敏捷开发)

    Principles behind the Agile Manifesto

  • Extreme programming(极限编程)

    Extreme Programming: A Gentle Introduction.

    相比传统的开发,更强调适应性,而不是可预测性。需求在不断变化,有能力在任何时候适应变化是更加现实有效的办法。

  • Test-driven develpment(测试驱动开发, TDD)

    先写测试程序,然后再编码实现功能。测试不仅驱动着开发过程,还驱动代码的再设计和重构。

  • High-level languages

    高级语言,相对一些较为底层的语言学习曲线更平滑,代码量更少。

  • DevOps

    CI/CD,Microservices等等,包括和敏捷开发相关的一些日常流程

然而,这些好像也都没能完全解决问题。

Business Domain

所谓业务,就是一家公司主要的活动区域,大体上可以说是这家公司给客户提供的服务。比如:

  • FedEx的邮递服务
  • 星巴克的咖啡
  • 沃尔玛的零售业务

当然,一家公司也可以拥有多种不同的业务,比如亚马逊就同时有零售和云计算平台等业务。

重要的是,公司通常会经常变更业务

Core Subdomain

核心子业务,通常指一家公司和竞争对手差异化的部分,比如新的产品、服务,降本增效等。

Google的ranking algorithm则不然,它是一个单独的业务,不是子业务

核心的子业务通常都是复杂的。

这里还举了一个更好的例子,方便大家理解:

假设有一家公司是专门人工检测欺诈的,然后公司需要一个软件系统供分析师们工作。这个系统是核心子业务吗?

并不是。因为分析师们做的事,才是核心的,而软件系统无非是提交、批注、追踪等功能。

Generic subdomains

通用的子业务,指所有公司都以相同/类似的方式完成对应的工作,然后市面上也有各种各样的可替代的方案,大家都用这些方案。

比如oa系统等

Supporting subdomains

支撑业务,用于支撑公司的业务,但不提供比较优势。比如ETL、CRUD的接口等

Competitive advantage

只有核心子业务才能带给公司足够的竞争优势。通用子业务并没有任何竞争的意义,而支撑子业务则门槛太低,没有技术壁垒。

越是能解决困难的问题,一家公司的商业价值就越大。

从技术的角度来说,我们需要分辨出哪些复杂的情况会影响到软件的设计。

image-20221216224231170

Volatility

易失性

如果一个问题,只要尝试一次就解决了,那么它肯定没办法用来作为比较优势。很简单,其他人也能解决。

因此,一家公司需要不断地在核心子业务上进行研究、创新和迭代,开发新的feature,或者对现有的场景进行优化,来一直保持优势。

Solution strategy

一般来说,核心子业务肯定不能外包,否则核心的东西一旦没人维护就难以满足公司的目标了。通常都是公司最有才能的大佬们负责最核心的部分。

这也对解决方案提出了一些要求,即解决方案必须是可维护的(maintainable),易革新的(easy to evolve)。

Subdomain typeCompetitive advantageComplexityVolatilityImplementationProblem
CoreYesHighHighIn-houseInteresting
GenericNoHighLowBuy/adoptSolved
SupportingNoLowLowIn-house/outsourceObvious

Identifying Subdomain Boundaries

如何划分出子业务的边界,是一个问题。比如我们可以从公司的部门、机构来入手:

比如一家零售电商,包括仓储、客服、物流、品控等。然而,这些对业务的划分都是粗粒度的划分。如果单独拿出一个“客服”业务,我们很容易根据直觉,得出这是一个通用的业务,毕竟客服几乎全部都是外包。

Distilling subdomains

然而,当我们细细地拆分这个看似无聊的客服业务,却发现实际不简单:

比如Help desk和电话系统,这些可能确实是通用的业务

岗位的轮换、排版,可能是基础支撑业务

Case Routing - 将客户的事件,和业务员匹配,可就没那么简单了。许多公司甚至会开发一些相当精妙的算法,比如将事件交给处理过相似案例的职员处理等等。这就需要对案例进行分析,然后再聚类,这些都不是简单的任务。既然这个精妙的Routing算法能够为客户提供更好的服务,带来比竞争对手更好的客服体验,那么这个算法就是公司的核心子业务。

image-20221216225454314

当然,继续往下也可以再拆分,但总有个限度。

Subdomains as coherent use cases

从技术的角度来说,一个子业务一定代表了一些互相关联的,内聚的用例。这些用例可能都涉及到同样的角色,同样的业务实体,修改相似的数据。

比如信用卡的支付业务,就是这样的例子:

image-20221216230150335

而对于一些通用的业务,可能没有太多必要向下深究,比如

image-20221216230355696

Helpdesk的几个子业务都是通用的业务,那么深究带来的收益就不大。

It’s developers’ (mis)understanding, not domain experts’ knowledge, that gets released in production.

-- Alberto Brandolini

Communication

image-20221217130510534

通常在传统的开发模式中,往往业务的需求会翻译成程序猿能理解的需求,而这种翻译本身就会造成信息的损失。

image-20221217130941311

Ubiquitous Language

正是由于翻译、转换,包括理解也会出现偏差,我们需要一种统一的语言来进行交流。

这种语言必须是精确的,始终如一的,避免歧义的。

What Is a Model?

A model is a simplified representation of a thing or phenomenon that intentionally emphasizes certain aspects while ignoring others. Abstraction with a specific use in mind.

Rebecca Wirfs-Brock

一个有用的模型,并不是事无巨细地去模拟现实世界,而是用来有效地解决问题的。比如一张世界地图上,就不需要出现地铁站。

All models are wrong, but some are useful.

-- George Box

Tools

Glossary:术语表

术语表是最好的帮助消除歧义的工具,而且能够帮助解释一些晦涩的概念。

Gherkin (cucumber)

cucumber/cucumber-js: Cucumber for JavaScript (github.com)

Feature: Greeting

Scenario: Say hello
When the greeter says hello
Then I should have heard "hello"

尽管这种Behavior-driven的测试看上去极其繁琐,但是对复杂的业务场景是有帮助的。

"jack of all trades, master of none."

image-20221217132820887

像图中这样的模型,就是典型的杂而不精,最终啥也做不好。

Bounded Context

image-20221217133210174

image-20221217133420918

模型的最终目的是有效,因此模型的规模并不是教条的,必须大还是小。当然,模型的规模确实会对业务的统一有一些影响,比如模型越大,就越容易出现概念的冲突,一致性就越困难。模型越小,集成的代价就越大,维护的成本也就越高。这是一个权衡的过程。

Bounded Contexts vs Subdomains

业务的场景则是相对被动地由公司和系统的要求决定的。作为工程师,肯定在公司中并不是拍板人,决定不了要做什么,而是分析当前的业务,识别子领域。

相对来说,Bounded Context就是人为定义的,是一种设计的抉择。如果愿意,甚至可以定义一个横跨公司所有业务的模型(Monolithic),对小系统也是有效的。

当相互冲突的模型出现,那么就需要将系统继续分解为小的Context。

一句话:

subdomains are discovered and bounded contexts are designed.

Architectural design is system design. System design is contextual design—it is inherently about boundaries (what’s in, what’s out, what spans, what moves between), and about trade-offs. It reshapes what is outside, just as it shapes what is inside.

-- Ruth Malan

重要的是,一个Bounded context只应该由一个Team维护。当然,一个Team可以拥有多个Bounded context

A model should omit the extraneous information irrelevant to the task at hand.

Partnership

不同的Context也需要由交互和合作。

image-20221217135514330

在Partnership model - 伙伴模型中,一个Team提醒另一个Team 自己的API发生了改动,然后第二个Team就会进行合作和适配。

Shared Kernel

image-20221217135835312

多个Bounding context分享一个共同的模型,这个模型有点类似于交集,只定义一些需要被大家共同定义的内容。一旦这个核心模型被修改,那么引用它的Bounding context也都要做相应的修改或适配。

当然,这个模型违背了一个Bounding Context被一个Team开发的原则,所以是一个例外,需要格外小心地使用。

当然,如果这几个Context都是同一个Team开发的,情况会相对好一些。

Customer–Supplier

image-20221217140337744

上游的Service就是Provider,而下游的Service就是Customer。

Conformist

下游的服务遵循上游的定义 -- Take it or leave it

上游的服务对定义占据主导权

Anticorruption Layer

image-20221217140638642

下游的Service可以增加一个Anticorruption Layer,相当于对上游的模型进行了一次转译,得到适合自己Context的模型

Open-Host Service

image-20221217140829850

image-20221217140923037

在这种模式下,Consumer占据主导权,让Supplier能够根据需要以不同的进度对公开模型进行更改,如上图2.

下图是整体的Context Map

image-20221217141119424

Transactional Behavior

所有的操作,都应该要么成功,要么失败,而不是停留在一个非法的状态下。

Transaction Script

将系统的操作整理为简单、直接的过程脚本,每个步骤都是事务性的。类似ETL的操作

Active Record

Acrive record就是一种能提供简单CRUD方法的数据结构

当然,以上这两种都是针对简单的业务,对复杂的业务可能并不太适用。

Domain Model

Value object

如颜色类:

class Color
{
int _red;
int _green;
int _blue;
}

很显然,在这样的类里,定义一个额外的Color-id是完全没必要的,反而会埋下潜在的bug,比如比较color-id,却无法得到两个color是相同的还是不同的。

image-20221217142926250

Ubiquitous language

primitive obsession

class Person
{
private int _id;
private string _firstName;
private string _lastName;
private string _landlinePhone;
private string _mobilePhone;
private string _email;
private int _heightMetric;
private string _countryCode;

public Person(...) {...}
}

vs

class Person {
private PersonId _id;
private Name _name;
private PhoneNumber _landline;
private PhoneNumber _mobile;
private EmailAddress _email;
private Height _height;
private CountryCode _country;

public Person(...) { ... }
}

value objects使得验证更加严格,判断逻辑更加有鲁棒性,而且也不需要再记忆一大堆的传统了。

Aggregates
public void AddMessage(UserId from, string body)
{
var message = new Message(from, body);
_messages.Append(message);
}

vs

public void Execute(AddMessage cmd)
{
var message = new Message(cmd.from, cmd.body);
_messages.Append(message);
}

我们可以把判断逻辑移到Aggregate中,这样应用层就只需要拿到Aggergate的值,然后执行所需要的任务就可以了。

所有能够接受最终一致性的数据都应该在Aggregate外部,而Aggregate的内部是强一致性的。

image-20221217144443210

因此,Aggregate应该尽可能地小,只包括必须要强一致的实体。

Domain Event

image-20221217144957753

Domain services

无状态的对象,负责不属于任何Value Objects或Aggregates的业务逻辑

Event Sourcing

Show me your flowchart and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won’t usually need your flowchart; it’ll be obvious.

-- Fred Brooks

如果在一张表中只记录了数据的现状,那么我们并不知道数据具体发生了哪些变化。

Source of Truth

image-20221217150420463

乐观的异步,最起码要对事件的版本号进行判断,如果已经有大于版本号的事件就抛异常。

Advantages

Event sourcing 的优势是显而易见的:能够使得系统的状态重建、恢复、回滚变的可能,并且提供了更多可供分析的数据,也提供了一种管理并行的方式。

Disadvantages

缺点就是模型的学习曲线较为陡峭,更新迭代富有挑战,也增加了复杂度。