⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 技术琐话 「张建飞」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

你可以,不代表你应该。

(Just because you can, doesn’t mean you should.)

——施莉琳•凯尼恩

在第6章中,我们简要介绍了什么是模型、模型在软件开发中的重要性,以及一些常用的建模方式在软件工程中的应用。本章将重点讲解领域驱动设计(Domain Driven Design,DDD),包括DDD的重要概念,以及如何进行领域建模。

7.1 什么是DDD

DDD是Eric Evans在2003年出版的《领域驱动设计:软件核心复杂性应对之道》(Domain-Driven Design: Tackling Complexity in the Heart of Software)一书中提出的具有划时代意义的重要概念,是指通过统一语言、业务抽象、领域划分和领域建模等一系列手段来控制软件复杂度的方法论。

DDD的革命性在于领域驱动设计是面向对象分析的方法论,它可以利用面向对象的特性(封装、多态)有效地化解复杂性,而传统J2EE或Spring+Hibernate等事务性编程模型只关心数据。这些数据对象除了简单的setter/getter方法外,不包含任何业务逻辑,业务逻辑都是以过程式的代码写在Service中。这种方式极易上手,但随着业务的发展,系统也很容易变得混乱复杂。

7.2 初步体验DDD

在介绍DDD之前,我喜欢用这个银行转账的案例来做一个DDD和事务脚本(Transaction Script)的简单对比。我们要实现一个银行转账的功能,如果用传统的事务脚本方式实现,业务逻辑通常会被写在MoneyTransferService中,而Account仅仅是getters和setters的数据结构,也就是所谓的“贫血模式”。其代码如下所示:

public class MoneyTransferServiceTransactionScriptImpl
implements MoneyTransferService {
private AccountDao accountDao;
private BankingTransactionRepository bankingTransactionRepository;
. . .
@Override
public BankingTransaction transfer(
String fromAccountId, String toAccountId, double amount) {
Account fromAccount = accountDao.findById(fromAccountId);
Account toAccount = accountDao.findById(toAccountId);
. . .
double newBalance = fromAccount.getBalance() - amount;
switch (fromAccount.getOverdraftPolicy()) {
case NEVER:
if (newBalance < 0) {
throw new DebitException("Insufficient funds");
}
break;
case ALLOWED:
if (newBalance < -limit) {
throw new DebitException(
"Overdraft limit (of " + limit +") exceeded: " + newBalance);
}
break;
}
fromAccount.setBalance(newBalance);
toAccount.setBalance(toAccount.getBalance() + amount);
BankingTransaction moneyTransferTransaction =
new MoneyTranferTransaction(fromAccountId,toAccountId,amount);
bankingTransactionRepository.addTransaction(moneyTransferTransaction);
return moneyTransferTransaction;
}}

上述代码有些读者可能会比较眼熟,因为大部分系统都是这么写的。评审完需求,工程师画几张UML图完成设计,就开始像上面这样写业务代码了,这样写基本不用太动脑筋,完全是过程式的代码风格。

同样的业务逻辑,接下来看使用领域建模是怎么做的。在使用DDD之后,Account实体除账号属性之外,还包含了行为和业务逻辑,比如debit()和credit()方法。

public class Account {
private String id;
private double balance;
private OverdraftPolicy overdraftPolicy;
. . .
public double balance() { return balance; }
public void debit(double amount) {
this.overdraftPolicy.preDebit(this, amount);
this.balance = this.balance - amount;
this.overdraftPolicy.postDebit(this, amount);
}
public void credit(double amount) {
this.balance = this.balance + amount;
}}

透支策略OverdraftPolicy也不仅仅是一个Enum了,而是被抽象成包含业务规则并采用策略模式的对象。

public interface OverdraftPolicy {
void preDebit(Account account, double amount);
void postDebit(Account account, double amount);}public class NoOverdraftAllowed implements OverdraftPolicy {
public void preDebit(Account account, double amount) {
double newBalance = account.balance() - amount;
if (newBalance < 0) {
throw new DebitException("Insufficient funds");
}
}
public void postDebit(Account account, double amount) {
}}public class LimitedOverdraft implements OverdraftPolicy {
private double limit;
. . .
public void preDebit(Account account, double amount) {
double newBalance = account.balance() - amount;
if (newBalance < -limit) {
throw new DebitException(
"Overdraft limit (of " + limit + ") exceeded: "+newBalance);
}
}
public void postDebit(Account account, double amount) {
}}

而Domain Service只需要调用Domain Entity对象完成业务逻辑。

public class MoneyTransferServiceDomainModelImpl
implements MoneyTransferService {
private AccountRepository accountRepository;
private BankingTransactionRepository bankingTransactionRepository;
. . .
@Override
public BankingTransaction transfer(
String fromAccountId, String toAccountId, double amount) {
Account fromAccount = accountRepository.findById(fromAccountId);
Account toAccount = accountRepository.findById(toAccountId);
. . .
fromAccount.debit(amount);
toAccount.credit(amount);
BankingTransaction moneyTransferTransaction =
new MoneyTranferTransaction(fromAccountId,toAccountId,amount);
bankingTransactionRepository.addTransaction(moneyTransferTransaction);
return moneyTransferTransaction;
}}

通过DDD重构后,虽然类的数量比以前多了一些,但是每个类的职责更加单一,代码的可读性和可扩展性也随之提高。

7.3 数据驱动和领域驱动

7.3.1 数据驱动

目前主流的开发模式是由数据驱动的。数据驱动的开发很容易上手,

有了业务需求,创建数据库表,然后编写业务逻辑,开发过程如图7-1所示。数据驱动以数据库为中心,其中最重要的设计是数据模型,但随着业务的增长和项目的推进,软件开发和维护的难度会急剧增加。

图7-1 数据驱动研发过程

以客户关系管理(Customer Relationship Management,CRM)为例,其中很重要的概念有销售、机会、客户、私海、公海,实体的定义分别如下。

  • 销售(Sales):公司的销售人员,一个销售可以拥有多个销售机会。
  • 机会(Opportunity):销售机会,每个机会包含至少一个客户信息,且归属于一个销售人员。
  • 客户(Customer):客户,也就是销售的对象。
  • 私海(Private sea):专属于某个销售人员的领地(Territory),私海里面的客户,其他销售人员不能触碰。
  • 公海(Public sea):公共的领地,所有销售人员都可以从公海里捡入客户到其私海。

按照我们曾经学习的数据库建模理论,对于上面的场景,不难画出图7-2所示的实体联系(Entity Relationship,ER)图。

图7-2 CRM的ER图

可以看到,图7-2所示的ER图中不存在公海和私海,因为所谓的机会在私海,就是这个机会是不是归属某个销售,这样我们只需要看机会上是否有salesId。如果有,说明机会被某个销售占有,也就是在私海中;反之,这个机会就在公海中。

在这种开发模式下,最后的产出是几张数据库表,以及针对表中数据进行操作的事务脚本,如图7-3所示。

图7-3 事务脚本实现

7.3.2 领域驱动

领域驱动设计关心的是业务中的领域划分(战略设计)和领域建模(战术设计),其开发过程不再以数据模型为起点,而是以领域模型为出发点,研发过程如图7-4所示。领域模型对应的是业务实体,在程序中主要表现为类、聚合根和值对象,**它更加关注业务语义的显性化表达,而不是数据的存储和数据之间的关系。**这是“领域驱动设计”和“数据驱动设计”之间显著的区别。

图7-4 领域驱动研发过程

仍以上面的CRM为例。假如我们先不考虑数据模型,而是采用面向对象分析(Object Oriented Analysis,OOA)对这个场景进行领域建模,那么可以得到图7-5所示的领域模型。

图7-5 CRM的领域模型

可以看到,在图7-5中,领域模型的描述更加贴近业务,一些重要的业务术语和概念没有丢失,更完整地表达了业务语义。即使是产品经理或者业务人员,也不难看懂这样的领域模型,甚至他们可以和技术人员一起参与到梳理领域模型和创建活动中来。

通过DDD的战略设计和战术设计,我们可以为问题域划分出合适的子域,并对域中的业务进行建模。图7-6所示是我们在实际工作中为CRM进行的领域战略设计。

图7-6 CRM的领域划分

7.3.3 ORM

很明显,领域模型和数据模型并不是一一对应的关系,但也不排除,有些情况领域模型和数据模型是趋同的,但是大部分情况都需要做一层映射(Mapping)。为了弥补二者之间的差异,行业先驱们做了很多关于映射工作的尝试,这种技术有一个名称叫作对象关系映射(Object Relationship Mapping,ORM),如图7-7所示。

图7-7 对象关系映射

ORM曾经非常火,记得当年Hibernate才出现时,我用尽了其中的高级技巧,比如继承关系映射、多对多关系映射……结果弄出来的东西却变成了“四不像”,既不像Entity,也不像数据对象(Data Object,DO)。

ORM的问题在于它太理想化,期望通过工具把数据建模和领域建模合一,这样的尝试注定是很难成功的。仍以上述的CRM案例为例,在数据模型中根本就没有私海和公海这两个实体,工具是无法映射的。因此,Hibernate和JPA的衰落是可以预见的。现在使用最多的是MyBatis,它很简单,完全不理会复杂的关系和对象之间的复杂关系映射,只做数据库表和DO之间的简单映射。

复杂的数据库关系和对象关系之间的差异,其本质是数据模型和领域模型之间的差异,而这种差异的多样性和灵活性是很难通过规则预先定义的,这也是为什么工具的作用会很有限。现在的互联网大厂大多使用MyBatis,原因也在于此。因此,如果你打算实践DDD,请一定不要让工具帮你去建模,工具不会抽象,也不会思考,还是要老老实实自己动手去建。

文章目录
  1. 1. 7.1 什么是DDD
  2. 2. 7.2 初步体验DDD
  3. 3. 7.3 数据驱动和领域驱动
    1. 3.1. 7.3.1 数据驱动
    2. 3.2. 7.3.2 领域驱动
    3. 3.3. 7.3.3 ORM