找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3792

积分

0

好友

500

主题
发表于 17 小时前 | 查看: 3| 回复: 0

你可以,不代表你应该。
(Just because you can, doesn’t mean you should.)
——施莉琳•凯尼恩

上一章我们聊了模型的重要性,本章我们聚焦于领域驱动设计(Domain Driven Design, DDD),深入探讨其核心概念与建模方法。

7.1 什么是DDD

DDD是Eric Evans在2003年出版的经典著作《领域驱动设计:软件核心复杂性应对之道》中提出的划时代方法论。它旨在通过统一语言、业务抽象、领域划分与建模等一系列手段,有效管理软件的复杂核心。

为什么说它是革命性的呢?因为DDD是一套面向对象的分析与设计方法,它能充分利用面向对象的封装、多态等特性来化解复杂性。相比之下,我们熟悉的传统开发模式,无论是早期的J2EE还是现在的Spring + MyBatis/Hibernate组合,其本质更像是“事务性编程”。在这种模式下,我们创建的对象(Entity或POJO)常常只是一堆属性的集合,仅有简单的getter/setter,业务逻辑则被写成过程式的代码,散落在各个Service中。这种方式上手极快,但随着业务迭代,系统往往会迅速陷入混乱,变得难以理解和维护。

7.2 初步体验DDD

在深入理论前,我们先通过一个经典的银行转账案例,直观感受DDD与传统“事务脚本”模式的差异。

假设我们要实现一个转账功能。如果用传统的事务脚本方式,业务逻辑会集中在一个MoneyTransferService中,而Account对象仅仅是承载数据的“贫血模型”。代码大概长这样:

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也不再是一个简单的枚举,而是被抽象成一个包含业务规则、采用策略模式的对象。

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) {
  }
}

如此一来,领域服务MoneyTransferService的职责就变得非常清晰和简单:只需协调领域实体完成业务逻辑。

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重构,虽然类的数量可能增加了,但每个类的职责高度单一和内聚,代码的可读性、可维护性和可扩展性都得到了显著提升。这对于长期维护复杂业务系统的Java程序员而言,价值巨大。

7.3 数据驱动和领域驱动

7.3.1 数据驱动

当前主流的开发模式其实是数据驱动的。这种方式上手门槛低:接到需求,创建数据库表,然后围绕这些表编写业务逻辑。整个开发流程可以概括为下图:

数据驱动研发流程图

数据驱动以数据库为中心,整个设计中最重要的是数据模型。但随着业务增长,这种模式的弊端会凸显,软件开发和维护的难度呈指数级上升。

我们以一个客户关系管理(CRM)系统的场景为例。其中涉及几个核心概念:

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

如果采用传统的数据库建模理论,我们很容易画出下面的实体关系(ER)图:

CRM系统ER图

你会发现,ER图中并没有“公海”和“私海”这两个实体。因为在这个模型里,“机会在私海”这个业务概念,被简化为“机会记录上是否关联了某个salesId”。有关联,即在私海;无关联,即在公海。

在这种模式下,最终的产出物就是几张数据库表,以及一系列直接操作这些表数据的事务脚本(Transaction Script),它们之间的关系如下图所示:

事务脚本与数据库表关系图

7.3.2 领域驱动

领域驱动设计则把关注点放在了业务本身。它关注的是业务领域的划分(战略设计)和领域模型的构建(战术设计)。其研发起点不再是数据模型,而是领域模型,流程见下图:

领域驱动研发流程图

领域模型对应的是业务实体,在代码中表现为类、聚合根、值对象等。它的核心目标是显性化地表达业务语义,而不仅仅是描述数据的存储和关系。 这是“领域驱动”与“数据驱动”最根本的区别。

还是上面那个CRM的例子。如果我们暂时抛开数据库,采用面向对象分析(OOA)的方法进行领域建模,会得到如下模型:

CRM领域模型图

可以看到,这个模型更加贴近真实的业务语言。“领地”、“公海”、“私海”、“经理库”这些重要的业务概念得以保留,完整地传递了业务语义。即便是产品经理或业务方,也能大致看懂这个模型,从而能够和技术人员一起参与模型的梳理与构建。

通过DDD的战略设计,我们可以为复杂的问题域划分子域。下图是我们为一个实际CRM项目进行的领域划分示意:

CRM领域划分架构图

7.3.3 ORM的局限

显然,领域模型和数据模型并非一一对应。当然,也存在两者趋同的情况,但大多数时候,我们需要在它们之间做一层映射(Mapping)。这项技术就是大家熟悉的对象关系映射(ORM),其理想模型如下图所示:

对象关系映射示意图

ORM曾风靡一时。记得Hibernate刚出现时,我也曾沉迷于它的高级特性,如继承映射、多对多映射等,但最终构建出来的东西却成了“四不像”,既不像纯粹的领域实体,也不像单纯的数据对象。

ORM的问题在于它过于理想化,试图用一个工具统一数据建模和领域建模,这种尝试注定面临巨大挑战。还是以CRM案例为例,在数据模型中根本没有“公海”、“私海”实体,工具如何进行映射呢?因此,Hibernate、JPA等全自动ORM框架的式微是可以预见的。现在更流行的是像MyBatis这样的半自动或手动映射框架,它足够简单,不处理复杂的对象关系网,只做数据库表和简单数据对象之间的映射。

复杂的数据关系与对象关系之间的差异,本质上是数据模型与领域模型之间的差异。这种差异的多样性和灵活性很难通过预定义的规则来涵盖,这也是为什么通用工具的作用有限。现今许多互联网公司青睐MyBatis,这也是原因之一。

因此,如果你决心实践DDD,请务必记住:不要期望工具替你完成建模。工具不会抽象,也不会思考。构建能深刻反映业务、易于演进的领域模型,始终是需要开发者亲力亲为、持续精进的核心工作。这背后考验的是我们对业务的理解深度和系统架构设计能力。


本文由云栈社区整理发布,聚焦于分享实用的架构与开发知识。欢迎访问社区,探讨更多技术实践。




上一篇:创业公司技术选型实录:从Egg.js到团队解散的踩坑记
下一篇:微软创始人比尔·盖茨承认婚外情,与俄桥牌选手合影细节曝光
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-2 20:46 , Processed in 0.496028 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表