架构方法论-DDD

222 阅读42分钟

一、DDD的本质与复杂系统治理的核心

1.1 为什么需要DDD?

兄弟,你有没有接过那种“祖传代码”?就是那种一看就知道,经了九九八十一代程序员的手,每个人都往上堆了点自己的“想法”,最后堆成了一座谁也不敢动、谁也动不了的“屎山”。

这种系统,一般都有几个通病,你对号入座一下:

  • 东一榔头,西一棒槌:一个简单的下单流程,它的逻辑能像天女散花一样,散落在Controller、Service、Helper、Util……各个角落。想理清整个脉络?行,先打开二十个文件再说。
  • 鸡同鸭讲,不在一个频道:业务同学跟你聊“用户生命周期”,你代码里写的却是UserStatusFlag;业务说“我们要调整风控策略”,你翻开代码,看到了RiskRuleCheckPolicyValidationHandler,哪个是哪个?猜吧。
  • 牵一发动全身,改代码像拆炸弹:加个小功能,评估下来要改几十个文件,影响范围大到吓人,测试回归的成本高到让产品经理想哭。至于重构?那更是想都不敢想的“史诗级工程”。

病根在哪?说白了,就是从一开始,业务和技术就没拧成一股绳!  咱们的代码,没有在“说业务的人话”。

图片

而DDD这门功夫,就是要当这个“翻译官”和“粘合剂”,让咱们的技术实现,能精准地翻译业务的意图,让代码真正成为业务的镜子。

1.2 DDD的核心哲学

记住,DDD不是一个具体的技术框架(像Spring、MyBatis那样),它是一种设计思想,一套心法。它的核心心法,浓缩成三点:

第一,业务核心驱动

软件是干嘛的?是为了解决业务问题,不是为了让我们秀技术肌肉的。DDD死死地抓住这一点,强调一切设计都要围绕着“业务核心”来转。

这意味着什么?意味着你的代码结构、模块划分、类和方法命名,都应该像镜子一样,直接映射出业务的样子。比如你做电商,“商品”、“订单”、“支付”、“优惠券”这些业务世界的核心概念,就必须是你代码世界里最闪亮的明星对象,而不是被一堆xxxManagerxxxService的技术细节给淹没了。

第二,模型就是代码,代码就是模型。

这是DDD的灵魂所在!领域模型不是画在PPT上、锁在文档里,仅供瞻仰的“艺术品”,它必须是活在你的代码库里,能跑、能测试、能呼吸的“活物”。

当业务专家告诉你:“订单必须先支付才能发货”。这个业务规则,就不应该零散地写在某个if-else里,而应该成为Order这个类自身的一个行为方法(比如order.ship())。当调用这个方法时,Order对象内部会自己检查“我支付了吗?”,如果没支付,它会理直气壮地抛出一个异常,告诉你“不行,按规矩来!”。这,就叫高内聚!

第三,拥抱变化,持续演进。

业务是活的,市场在变,用户在变,所以业务逻辑也必然在变。指望一次性设计出一个“完美”模型,用到天荒地老,那是在做梦。

DDD鼓励的,是一种“小步快跑、持续迭代”的演进式设计。我们先根据当前的理解,建立一个够用的模型,然后在业务发展的过程中,不断地和业务专家沟通,不断地重构、优化我们的代码模型,让它越来越精准,越来越能反映业务的本质。这就像养孩子,得天天陪着,看着他成长,不断给他买新衣服,而不是一生下来就给他穿上西装。

1.3 复杂系统治理的挑战

物理学里有个词,叫“熵增定律”,说的是在一个孤立的系统里,混乱程度(熵)只会增加,不会减少。软件系统也一样,如果你放任不管,它必然会从一个有序的“小甜甜”,慢慢变成一个混乱的“牛夫人”。

这种“软件熵增”体现在:

图片

  • 代码腐化:原本清晰的架构逐渐模糊,各种临时方案和"创可贴"式补丁越来越多,最后变成了"补丁摞补丁"的怪物。
  • 知识流失:老员工带着业务知识离职,新人看着代码一脸懵逼,业务逻辑变成了无人能解的"黑盒"。
  • 变更困难:牵一发而动全身,任何改动都得小心翼翼,生怕触发"蝴蝶效应"。
  • 性能下降:系统越来越臃肿,响应越来越慢,用户体验直线下降。

咱们当工程师的,就得像个"老中医",不仅要会治病,还得会"养生",让系统能长久地保持青春活力。DDD就是这么一套"养生秘籍",通过良好的设计和持续的治理,有效对抗系统的熵增。

二、DDD的核心概念与设计方法

DDD 这套理论体系,概念确实不少,初学者一看就容易被绕晕。但别急,这玩意儿得多品品,联系实际去理解。我把它比作治理一个“软件王国”的法则,这样理解起来就容易多了。DDD 的“法”分为两个层面:战略设计战术设计

2.1 战略设计:治理"软件王国"的大法

如果把一个复杂系统比作一个王国,那么战略设计就是制定这个王国的“宪法”和“行政区划”。它关注的是宏观层面的架构决策,决定了你的系统“骨架”是否健康,别上来就去纠结某个村子里的路该怎么修。

2.1.1 通用语言(Ubiquitous Language)

这是 DDD 的基石中的基石!通用语言就是咱们王国的“官方语言”,所有人——不管是业务专家、产品经理,还是架构师、开发、测试——在讨论同一个业务概念时,都得说一样的话,用一样的词,才不会鸡同鸭讲,互相猜忌。

我给你讲个真事儿。有一次,我们团队在做一个金融系统,业务专家火急火燎地跑来说要做“账户冻结”功能。技术团队一听,简单啊,不就是把账户状态改成“frozen”吗?啪啪啪代码写完,上线后业务炸了锅——原来业务专家说的“冻结”,是指冻结账户里的部分金额,账户本身还能正常使用,只是有一部分钱不能动!

你看,这就是没有通用语言造成的恶果!一个词,两种理解,最后写出来的代码跟业务需求南辕北辙!

建立通用语言的关键是:

  • 业务和技术要“坐下来好好聊”:  不是业务提需求、技术照着做,而是双方一起探讨、定义每个业务概念。把它写下来,形成一个“词汇表”。
  • 语言要体现在代码里:  如果业务叫“冻结金额”,代码里就应该是 frozenAmount,而不是 lockedMoney 或者 blockedBalance代码即文档,代码即语言!
  • 持续维护和更新:  业务在变,语言也要跟着变。这个“词汇表”得是个活的东西。

图片

2.1.2 限界上下文(Bounded Context)

限界上下文就像是把庞大、复杂的软件王国,划分成一个个边界清晰的“行省”。每个行省有自己的“地方法规”,也就是自己的领域模型和通用语言。

为什么要划分限界上下文?因为在一个大系统里,同一个概念在不同的场景下可能有完全不同的含义和关注点。比如“用户”这个概念:

  • 账户上下文里,“用户”可能更关注他的余额、交易记录、银行卡信息。
  • 营销上下文里,“用户”可能被看作一个营销对象,我们关注的是他的偏好、标签、活跃度。
  • 风控上下文里,“用户”又变成了风险评估对象,我们关注的是他的信用分、风险等级。

如果你试图用一个大而全的 User 类来满足所有这些场景,最后只会得到一个包含了几百个字段和方法的、臃肿不堪的“上帝类”,谁都不敢碰!

正确的做法是,在不同的限界上下文中,根据具体场景,定义不同的、更专注的用户模型。账户上下文里有个 AccountUser,营销上下文里有个 MarketingUser,它们各自演进,互不干扰。

限界上下文,是 DDD 控制复杂性的核心武器!  它也是微服务划分最重要的理论依据。

2.1.3 上下文映射(Context Mapping)

不同的“行省”之间总要打交道,这就需要建立“交流关系”。上下文映射图就是用来描述这些关系的“地图”和“协议”。

图片

常见的上下文关系模式包括:

  • 共享内核 (Shared Kernel):  两个上下文共享一部分核心模型,就像两个省共管一个经济开发区。耦合度最高,得非常小心。
  • 客户-供应商 (Customer-Supplier):  一个上下文为另一个提供服务,有明确的上下游关系。这是最常见的。
  • 防腐层 (Anti-Corruption Layer, ACL):  当你要集成一个外部的、或者遗留的、“不太干净”的系统时,在你的边界上建立一个转换层,就像“海关”一样,把外部的“脏东西”过滤和转换成自己内部能理解的模型,避免污染自己的核心领域。
  • 开放主机服务 (Open Host Service, OHS):  对外提供一套稳定的、公开的 API,就像设立一个“对外贸易窗口”,让别人能方便地跟你打交道。
2.1.4 领域划分:找到你的"核心竞争力"

不是所有的业务都同等重要。DDD 把业务领域分为三类,让你能把好钢用在刀刃上:

  • 核心域 (Core Domain):  这是你的核心竞争力所在,是公司挣钱的关键!比如对淘宝来说,商品搜索和推荐就是核心域。这里必须投入最优秀的人才和最多的时间!
  • 支撑域 (Supporting Subdomain):  支持核心业务,但本身不是核心竞争力。比如订单管理、物流跟踪。需要做好,但可以不用那么创新。
  • 通用域 (Generic Subdomain):  通用的、跟业务关系不大的功能,市面上有成熟方案。比如用户认证、消息通知。别自己造轮子,直接买或者用开源的!

识别出核心域后,就应该把最好的工程师、最多的精力,投入到核心域的设计和实现上。

2.2 战术设计:雕琢"领域模型"的手艺

如果说战略设计是“谋篇布局”,那么战术设计就是深入到每个限界上下文内部,用一系列的设计模式和构件,“精雕细琢”你的领域模型。

2.2.1 实体(Entity)

实体是有身份的。就像每个人都有身份证号,实体也有唯一标识。比如订单有订单号,用户有用户ID。

实体的特点是:

● 有唯一标识:通过ID来区分不同的实体,即使其他属性都相同
● 有生命周期:会被创建、修改、删除
● 有状态变化:订单从"待支付"到"已支付"到"已发货"

public class Order {
    private OrderId id;  // 唯一标识
    private OrderStatus status;  // 状态
    private Money totalAmount;
    
    public void pay(PaymentInfo payment) {
        // 状态变化的业务逻辑
        if (this.status != OrderStatus.PENDING) {
            thrownew IllegalStateException("只有待支付订单才能支付");
        }
        // 处理支付逻辑
        this.status = OrderStatus.PAID;
    }
}
2.2.2 值对象(Value Object)

值对象没有独立的身份,它纯粹是用来描述事物属性的。比如金额地址颜色。判断两个值对象是否相等,不是看它们是不是同一个对象,而是看它们的所有属性值是否都相同

值对象有一个非常重要的特点:不可变 (Immutable) 。一旦创建,就不能修改。任何修改操作,都应该返回一个新的值对象实例。这让它们天生就是线程安全的,可以被随意共享和传递。

// 值对象是不可变的,通常只有 final 字段和构造函数
publicfinalclass Money {
    privatefinal BigDecimal amount;
    privatefinal Currency currency;

    public Money(BigDecimal amount, Currency currency) {
        // ... 构造时进行校验 ...
        this.amount = amount;
        this.currency = currency;
    }

    // 任何操作都返回一个新的 Money 实例
    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            thrownew IllegalArgumentException("不同币种不能相加");
        }
        returnnew Money(this.amount.add(other.amount), this.currency);
    }
    
    // 需要重写 equals() 和 hashCode() 方法
}
2.2.3 聚合(Aggregate)与聚合根(Aggregate Root)

聚合就像一个“小王国”,把业务上密切相关、需要一起保证数据一致性的实体和值对象圈在一起。而聚合根,就是这个小王国的“国王”!

外界要跟这个小王国打交道,必须通过国王,不能绕过国王直接找下面的大臣或百姓。聚合根是唯一的对外入口,它负责维护整个聚合内部所有对象的不变性规则。

比如,一个订单聚合,可能包含订单(聚合根)、订单项(实体)、收货地址(值对象)。

public class Order {  // 聚合根
    private OrderId id;
    private List<OrderItem> items;  // 只能通过Order访问
    
    public void addItem(Product product, int quantity) {
        // 通过聚合根来维护业务规则
        if (items.size() >= 100) {
            throw new BusinessException("订单项不能超过100个");
        }
        items.add(new OrderItem(product, quantity));
    }
}

图片

聚合设计的黄金法则:

  • 保证事务一致性:一个聚合就是一个事务边界,要么全成功,要么全失败
  • 聚合要小:太大的聚合会导致并发冲突和性能问题,就像一个国家太大了不好管理
  • 通过ID引用其他聚合:不要直接持有其他聚合的对象引用,用ID就够了
2.2.4 领域事件(Domain Event)

领域事件就像新闻广播,记录了"重要的事情发生了"。当订单支付成功时,不仅订单状态要改变,可能还要通知库存系统减库存,通知物流系统准备发货,通知营销系统更新用户积分。

public class OrderPaidEvent {
    privatefinal OrderId orderId;
    privatefinal Money amount;
    privatefinal Instant occurredAt;
    privatefinal String userId;
    
    public OrderPaidEvent(OrderId orderId, Money amount, String userId) {
        this.orderId = orderId;
        this.amount = amount;
        this.userId = userId;
        this.occurredAt = Instant.now();
    }
    
    // 事件是不可变的,只记录发生了什么
    // getter方法...
}

领域事件的好处多多:

在这里插入图片描述

在这里插入图片描述

  • 解耦:发布者不需要知道谁在监听,各干各的活
  • 审计:事件本身就是一份完整的审计日志
  • 最终一致性:通过事件在不同聚合间优雅地传播变化

三、DDD实践的具体方法

3.1 事件风暴:让业务知识"风暴"起来

这是我最最推荐的 DDD 实践起点,没有之一!如果你不知道怎么开始 DDD,那就从一场事件风暴开始!

啥是事件风暴?它是一种协作式的、游戏化的、快速的业务探索工作坊。这就像开“群英会”,把各路英雄好汉(业务、产品、开发、测试)都请到一间屋子里,大家围着一面巨大的白板(或者在线协作白板),七嘴八舌地把事情的来龙去脉都给“抖落”出来,把那些隐藏在业务流程里的领域事件一个个“炸”出来!

3.1.1 事件风暴的基本流程

事件风暴不是瞎聊,它有一套行之有效的流程和规则。

  1. 准备阶段:
    • 找一面巨大的白板或墙(越大越好!),或者用 Miro、FigJam 这类在线协作工具。
    • 准备好不同颜色的便签纸和马克笔(橙、蓝、黄、粉是标配)。
    • 邀请对的人!  业务专家、产品经理、架构师、核心开发者、测试工程师,一个都不能少!
  2. 风暴阶段:

在这里插入图片描述

在这里插入图片描述

  • 第一轮:罗列领域事件(橙色便签):  这是核心!让所有人(特别是业务专家)一起头脑风暴,把自己知道的、在业务流程中已经发生的、重要的事,用 “...已...” 的过去式格式,写在橙色便签上,贴到墙上。比如:“订单创建”、“款项支付”、“商品发货”、“优惠券使用”...
    • 关键:  别怕重复,别怕不准确,先发散!把所有能想到的都写出来!
  • 第二轮:排序和补充:  把墙上所有的事件便签,按照业务发生的时间顺序,从左到右排列起来。在这个过程中,大家会自然地开始讨论:“是先锁定库存,还是先创建订单?”、“支付失败算不算一个事件?”... 很多隐藏的业务流程和逻辑分支,就在这个排序和讨论的过程中浮现出来了!这时候经常会有“啊,还有这个!”的惊喜。
  • 第三轮:识别命令和角色(蓝色便签表示命令,黄色表示角色):  接下来,对每个事件,反向追溯:
    • 这个事件是由**什么行为(命令)**触发的?用蓝色便签写下命令(动词形式),贴在对应事件的前面。比如,触发“订单已创建”的命令是“ 提交订单”。
    • 这个命令是由**谁(角色/用户)****发起的?用黄色小人便签写下角色,贴在命令旁边。比如,“提交订单”这个命令是由“买家”发起的。
  • 第四轮:发现聚合和限界上下文:  当墙上布满了事件、命令、角色后,你会发现,有些东西是天然地“扎堆”的。把那些操作同一个核心业务对象(比如都跟“订单”相关)的命令和事件圈在一起,这就是聚合的雏形!用粉色大便签把它们圈起来,并给这个聚合命名(比如“订单聚合”)。当墙上的聚合越来越多时,你会发现它们可以被自然地分成几个大的区域,这些区域就是限界上下文的雏形!
  1. 收敛阶段:
    • 整理和统一通用语言词汇表
    • 初步划分和命名限界上下文
    • 识别出贯穿全局的核心业务流程

我记得有一次给一个物流公司做事件风暴,场面那叫一个热闹!仓库主管说“货物已入库”,配送经理说“不对,应该是‘已签收入库’”,财务说“我们这叫‘入库确认’”。吵了半天才发现,原来他们说的是三个完全不同的业务节点发生的事件!这就是事件风暴的价值——把那些隐藏在团队成员脑子里的、不一致的认知,全都暴露在阳光下,然后达成共识!

3.1.2 从事件风暴到领域模型

事件风暴结束后,你会得到一面贴满便签的墙,这是你们团队集体智慧的结晶,是业务的全景图!接下来就要从这些便签中提炼出领域模型

  1. 识别聚合:  看哪些命令和事件总是围绕同一个主题名词。比如“创建订单”、“添加商品”、“修改数量”都是围绕“订单”的。
  2. 找出聚合根:  谁是这个聚合的“话事人”?通常是拥有唯一标识、生命周期最长的那个实体。
  3. 定义实体和值对象:  需要追踪身份的是实体(OrderOrderItem);只关心属性的是值对象(MoneyAddress)。
  4. 明确业务规则:  从讨论中提炼出的各种“必须”、“不能”、“只有...才能...”,这些规则应该封装在聚合根的行为方法里,而不是暴露在 Service 层。

3.2 限界上下文集成:搞好"外交关系"

不同的限界上下文之间总要打交道,这就像不同国家之间的外交。你得根据具体情况选择合适的“外交策略”。

3.2.1 防腐层(ACL):建立"海关"

当你需要集成一个外部系统,而这个系统的模型又很"脏"(比如字段命名混乱、结构不合理)时,就需要建立防腐层。

// 外部系统的"脏"模型
publicclass ExternalOrder {
    private String ord_id;  // 命名不规范
    privateint stat;  // 用数字表示状态
    private String cust_nm;  // 缩写看不懂
}

// 防腐层转换
publicclass OrderAdapter {
    public Order convert(ExternalOrder external) {
        OrderId id = new OrderId(external.getOrd_id());
        OrderStatus status = OrderStatus.fromCode(external.getStat());
        CustomerName customer = new CustomerName(external.getCust_nm());
        
        returnnew Order(id, status, customer);
    }
}

防腐层就像海关,所有进来的"货物"都要检查、转换,确保符合本国标准。这样,即使外部系统的模型变化,也只需要修改防腐层,而不会影响到你的核心领域模型。

图片

3.2.2 开放主机服务(OHS):设立"贸易窗口"

当你的服务需要被多个下游调用时,与其为每个调用方定制接口,不如提供一套标准的、稳定的、与内部实现解耦的 API

// 对外提供的数据传输对象 (DTO),与内部领域模型解耦
publicclass OrderDTO {
    private String orderId;
    private String status;
    private BigDecimal totalAmount;
    // ... 只暴露外部需要的数据
}

// Controller 层作为开放主机服务,提供稳定的 RESTful API
@RestController
@RequestMapping("/api/v1/orders")
publicclass OrderController {

    privatefinal OrderApplicationService applicationService;

    // ... 构造函数注入

    @PostMapping
    public OrderDTO createOrder(@RequestBody CreateOrderRequest request) {
        // 内部可能用了复杂的领域模型和业务逻辑
        OrderId newOrderId = applicationService.createOrder(request);
        // 但对外接口保持简洁稳定,返回 DTO
        return applicationService.getOrderById(newOrderId);
    }
}

这就像设立一个统一的贸易窗口,大家都按照公布的规则来办事,省得一对一谈判。

3.2.3 共享内核:建立"经济特区"

有时候两个上下文确实需要共享一些核心概念,这时可以建立共享内核。但要注意,共享内核是把双刃剑,它能减少重复,但也会增加耦合

// 比如一个共享的 "shared-kernel" 模块或包
package com.company.sharedkernel;

// 商品 ID 在多个上下文中(订单、库存、营销)都可能需要
// 把它定义成一个共享的值对象
public final class ProductId {
    private final String value;
    // ...
}

共享内核要遵循几个原则:

  • 只共享那些最核心、最稳定的部分(通常是值对象)。
  • 任何对共享内核的修改,都需要所有相关方团队的同意
  • 保持共享内核尽可能小

3.3 聚合设计的"内功修炼"

设计聚合就像装修房子,很多人总想搞个"大套间",把所有东西都塞进去。结果呢?住起来反而不舒服。聚合设计也是这个道理——宜小不宜大

3.3.1 聚合边界:小而美的哲学

聚合应该多大?这是个经常困扰大家的问题。我的经验是:宜小不宜大

为什么?因为:

  • 大聚合容易产生并发冲突:聚合是事务边界,太大会导致锁竞争
  • 大聚合加载成本高:每次都要加载整个聚合到内存
  • 大聚合违背单一职责:一个聚合管太多事,容易乱

来看个活生生的例子,很多兄弟刚开始都会这么设计:

// ❌ 错误示范:贪心的大聚合
public class Product {
    private ProductId id;
    private String name;
    private Money price;
    private List<Inventory> inventories;  // 所有仓库的库存都往里塞
    private List<Comment> comments;       // 所有评论也不放过
    private List<PriceHistory> priceHistories;  // 历史价格也要
    private List<Image> images;           // 图片也来
    // 这简直是个"垃圾场"!
}

这种设计会带来什么问题?

  • 用户只是想看个商品名称,结果把几千条评论都加载出来了
  • 改个库存,整个商品都被锁住了,其他人想改价格都不行
  • 一个类几千行代码,新人看了想离职

正确的做法:分而治之

在这里插入图片描述

在这里插入图片描述

看到没?拆分后的好处:

  • 性能提升:查商品信息不会加载库存和评论
  • 并发友好:改库存的改库存,写评论的写评论,互不影响
  • 职责清晰:每个聚合只管自己的一亩三分地
3.3.2 聚合根的职责设计:业务规则的守门员

很多兄弟把聚合根当成一个"数据袋子",只有getter/setter。这就大错特错了!聚合根应该是业务规则的守护者,就像足球队的守门员,把守着最后一道防线。

职责一:维护业务不变量

什么是不变量?就是无论如何都必须成立的业务规则。比如购物车的例子:

public class ShoppingCart {
    private List<CartItem> items;
    private Money totalAmount;  // 不变量:总金额必须等于所有商品金额之和
    
    public void addItem(Product product, int quantity) {
        // 守护规则1:数量必须合理
        if (quantity <= 0) {
            thrownew IllegalArgumentException("兄弟,数量得大于0啊!");
        }
        
        // 守护规则2:购物车不能太满
        if (items.size() >= 50) {
            thrownew BusinessException("购物车最多放50件商品,再多就装不下了!");
        }
        
        // 守护规则3:同一商品不重复添加
        Optional<CartItem> existingItem = findItem(product.getId());
        if (existingItem.isPresent()) {
            existingItem.get().increaseQuantity(quantity);
        } else {
            items.add(new CartItem(product, quantity));
        }
        
        // 维护不变量:重新计算总金额
        recalculateTotalAmount();
    }
    
    private void recalculateTotalAmount() {
        this.totalAmount = items.stream()
            .map(item -> item.getPrice().multiply(item.getQuantity()))
            .reduce(Money.ZERO, Money::add);
    }
}

看到没?聚合根不是简单地往列表里加东西,而是:

  • 检查业务规则
  • 维护数据一致性
  • 保证不变量始终成立

职责二:协调内部交互

聚合根就像一个"大管家",协调聚合内部各个部分的互动:

在这里插入图片描述

在这里插入图片描述

public class Order {
    private OrderId id;
    private List<OrderItem> items;
    private OrderStatus status;
    private ShippingAddress address;
    
    public void ship() {
        // 1. 检查前置条件
        if (status != OrderStatus.PAID) {
            thrownew BusinessException("兄弟,订单还没付款呢,不能发货!");
        }
        
        if (address == null) {
            thrownew BusinessException("送货地址都没有,往哪儿发?");
        }
        
        // 2. 协调内部状态变化
        this.status = OrderStatus.SHIPPING;
        
        // 3. 通知所有订单项
        items.forEach(item -> item.markAsShipping());
        
        // 4. 记录发货时间
        this.shippedAt = Instant.now();
        
        // 5. 发布领域事件,通知其他系统
        DomainEvents.publish(new OrderShippedEvent(
            this.id, 
            this.address,
            this.items.size()
        ));
    }
}

职责三:提供业务行为接口

这是最重要的一点:聚合根对外提供的应该是业务方法,而不是技术方法。

// ❌ 错误示范:暴露技术细节
publicclass Account {
    private Money balance;
    
    // 这是setter思维,不是业务思维
    public void setBalance(Money balance) {
        this.balance = balance;
    }
    
    public Money getBalance() {
        returnthis.balance;
    }
}

// ✅ 正确示范:提供业务行为
publicclass Account {
    private Money balance;
    
    // 存钱 - 这是业务行为
    public void deposit(Money amount) {
        if (amount.isNegativeOrZero()) {
            thrownew BusinessException("存款金额必须大于0");
        }
        this.balancethis.balance.add(amount);
        DomainEvents.publish(new MoneyDepositedEvent(this.id, amount));
    }
    
    // 取钱 - 这也是业务行为
    public void withdraw(Money amount) {
        if (amount.isGreaterThan(this.balance)) {
            thrownew BusinessException("余额不足");
        }
        this.balancethis.balance.subtract(amount);
        DomainEvents.publish(new MoneyWithdrawnEvent(this.id, amount));
    }
    
    // 转账 - 复杂的业务行为
    public void transferTo(Account target, Money amount) {
        this.withdraw(amount);
        target.deposit(amount);
        DomainEvents.publish(new MoneyTransferredEvent(
            this.id, target.id, amount
        ));
    }
}

看到区别了吗?

  • setter/getter是技术视角:我有什么数据
  • 业务方法是业务视角:我能做什么事
3.3.3 领域事件:聚合间的"快递小哥"

聚合之间不要搞得像连体婴儿一样,你调我、我调你。正确的做法是通过领域事件来通信,就像发快递一样——我把包裹(事件)发出去,谁要收谁签收,我不管。

图片

实现示例:

// 订单聚合:只管发布事件
publicclass Order {
    private OrderId id;
    private Money totalAmount;
    private OrderStatus status;
    
    public void confirmPayment(PaymentId paymentId) {
        // 1. 更新自己的状态
        if (this.status != OrderStatus.PENDING_PAYMENT) {
            thrownew BusinessException("订单状态不对,不能确认支付");
        }
        
        this.status = OrderStatus.PAID;
        this.paymentId = paymentId;
        this.paidAt = Instant.now();
        
        // 2. 发布事件,不关心谁会处理
        DomainEvents.publish(new OrderPaidEvent(
            this.id,
            this.totalAmount,
            this.customerId,
            this.items.stream()
                .map(item -> new OrderItemSnapshot(
                    item.getProductId(),
                    item.getQuantity()
                ))
                .collect(toList())
        ));
    }
}

// 库存聚合:监听事件,自主处理
@Component
publicclass InventoryEventHandler {
    
    @Autowired
    private InventoryRepository inventoryRepo;
    
    @EventListener
    @Async// 异步处理,不阻塞订单流程
    public void handleOrderPaid(OrderPaidEvent event) {
        log.info("收到订单支付事件,准备扣减库存: {}", event.getOrderId());
        
        for (OrderItemSnapshot item : event.getItems()) {
            try {
                Inventory inventory = inventoryRepo.findByProductId(item.getProductId());
                inventory.decrease(item.getQuantity());
                inventoryRepo.save(inventory);
            } catch (InsufficientStockException e) {
                // 库存不足,发布补偿事件
                DomainEvents.publish(new StockDeductionFailedEvent(
                    event.getOrderId(),
                    item.getProductId(),
                    e.getMessage()
                ));
            }
        }
    }
}

// 积分聚合:也在监听,各干各的
@Component
publicclass PointsEventHandler {
    
    @EventListener
    @Async
    public void handleOrderPaid(OrderPaidEvent event) {
        // 根据订单金额计算积分
        int points = calculatePoints(
        // 给用户增加积分
        UserPoints userPoints = pointsRepo.findByUserId(event.getCustomerId());
        userPoints.add(points, "订单支付奖励");
        pointsRepo.save(userPoints);
    
        log.info("用户{}获得{}积分", event.getCustomerId(), points);
}

领域事件的好处:

  1. 解耦彻底:订单不需要知道库存、积分的存在
  2. 扩展方便:想加个新功能?再加个监听器就行
  3. 容错性好:某个监听器挂了不影响主流程
  4. 性能更优:异步处理,不阻塞主流程

设计领域事件的小技巧:

// 事件要包含足够的信息,让监听者不用再查询
publicclass OrderPaidEvent {
    privatefinal OrderId orderId;
    privatefinal CustomerId customerId;
    privatefinal Money totalAmount;
    privatefinal List<OrderItemSnapshot> items;  // 快照,不是引用
    privatefinal Instant occurredAt;
    
    // 事件是不可变的,只有构造函数,没有setter
    public OrderPaidEvent(OrderId orderId, CustomerId customerId, 
                         Money totalAmount, List<OrderItemSnapshot> items) {
        this.orderId = orderId;
        this.customerId = customerId;
        this.totalAmount = totalAmount;
        this.items = new ArrayList<>(items);  // 防御性复制
        this.occurredAt = Instant.now();
    }
    
    // 只有getter,保证不可变
    public OrderId getOrderId() { return orderId; }
    // ... 其他getter
}

3.4 技术债务与系统演进

兄弟,咱们得承认一个现实:技术债是躲不掉的。就像人会老,系统也会老。关键是怎么"养生",让系统老得慢一点,老了还能焕发第二春。

3.4.1 技术债的识别与管理

技术债不是洪水猛兽,怕的是你假装看不见它!关键是要:

  1. 建立债务清单:  团队内部应该有一个公开的地方(比如 Jira, Confluence),记录每一笔技术债:什么时候欠的、为什么欠(是当时为了赶进度,还是设计考虑不周)、影响范围、潜在风险。
  2. 评估债务成本:  这笔债现在不还,将来要付多少“利息”?是会拖慢新功能开发,还是会增加线上故障风险?给债务评个级。
  3. 制定还债计划:  不是所有债都要立即还。结合业务优先级,把偿还技术债也作为正常的开发任务排入迭代计划。优先还那些“利息”高的债!
3.4.2 渐进式重构:绞杀者模式

面对一个庞大而腐化的遗留系统,千万不要想着推倒重来!那样风险太大,周期太长,业务也不会给你时间。

更聪明的做法是**“绞杀者模式” (Strangler Fig Pattern)**。

这个名字来自热带雨林中的绞杀榕:它的种子落在其他树上,慢慢生根发芽,长出气根包裹住宿主树,最终完全取代宿主树,自己长成参天大树。

我们的重构策略也是这样:用新的服务,像藤蔓一样,逐步包裹和替换旧系统的功能

图片

  1. 第一步:建立“绞杀者外立面” (Strangler Facade):  在遗留系统前面包一层代理或网关,所有对遗留系统的调用都先经过这个外立面。
  2. 第二步:逐步迁移功能:  当有新需求或要修改某个旧功能时,不要在旧系统里改!而是在外立面后面,用新的技术、新的架构(比如微服务+DDD)实现这个功能。然后,修改外立面的路由规则,把对这个功能的调用,从指向旧系统,切换到指向新系统
  3. 第三步:最终完全替代:  一点点地迁移,就像藤蔓越长越茂盛。终有一天,旧系统的所有功能都被新服务替代了,那个被“包裹”在里面的旧系统就可以安心地“退休”了
// 伪代码示例:绞杀者外立面
publicclass OrderServiceFacade {
    private LegacyOrderSystem legacySystem; // 旧系统
    private NewOrderService newMicroservice; // 新的微服务

    public Order getOrder(String orderId) {
        // 比如,查询功能已经迁移到新服务
        if (featureToggle.isNewOrderQueryEnabled()) {
            return newMicroservice.getOrder(orderId);
        } else {
            return legacySystem.getOrder(orderId);
        }
    }

    public void createOrder(OrderRequest request) {
        // 创建功能还在旧系统
        return legacySystem.createOrder(request);
    }
}

这种方式风险小、见效快、对业务无感知,是改造遗留系统的上上策!

四、DDD实践的工具箱

工欲善其事,必先利其器。但兄弟,记住一句话:工具是仆人,不是主人。我见过太多团队,工具玩得贼溜,领域模型却还是一团糟。

4.1 事件风暴工具:从便签纸到AI助手

别看现在工具满天飞,我最喜欢的还是大白板+便签纸。为啥?因为撕下来贴上去的感觉,比拖拽鼠标爽多了!

在这里插入图片描述

在这里插入图片描述

物理工具:

  • 大白板 + 便签纸:最原始也最有效,面对面讨论时的首选
  • 不同颜色代表不同元素:橙色=事件,蓝色=命令,黄色=角色,粉色=问题

在线协作工具:

  • Miro:功能强大,有专门的事件风暴模板
  • FigJam:Figma出品,界面简洁,适合远程协作
  • Mural:老牌协作工具,稳定可靠

AI助手的新玩法

2024年了,不用点AI都不好意思说自己搞技术。我最近在事件风暴中尝试了些新玩法:

# 用AI帮助整理事件风暴的结果
def analyze_event_storm_with_ai(events, commands, actors):
    prompt = f"""
    基于以下事件风暴的结果,帮我:
    1. 识别可能的聚合边界
    2. 发现遗漏的业务规则
    3. 建议限界上下文划分
    
    事件:{events}
    命令:{commands}
    角色:{actors}
    """
    
    # AI会给出建议,但最终决策还是人做
    suggestions = ai_model.analyze(prompt)
    return suggestions

4.2 建模可视化:让模型"活"起来

PlantUML:程序员的画图神器

为啥我喜欢PlantUML?因为它是用代码画图,可以版本管理,可以自动生成,简直是程序员的福音!

@startuml
skinparam backgroundColor transparent
skinparam defaultFontName "Microsoft YaHei"
skinparam handwritten true
skinparam classBackgroundColor #E8F5E9
skinparam classBorderColor #4CAF50

title 一个典型的DDD类图

package "订单限界上下文" #F5F5F5 {

  class Order <<Aggregate Root>> {
    - orderId: OrderId
    - status: OrderStatus
    - items: List<OrderItem>
    --
    + placeOrder()
    + addItem()
    + cancel()
  }

  class OrderItem <<Entity>> {
    - productId: ProductId
    - quantity: int
    - price: Money
  }

  class Money <<Value Object>> {
    - amount: BigDecimal
    - currency: String
  }

  enum OrderStatus {
    PENDING
    CONFIRMED
    SHIPPED
    COMPLETED
  }

  Order "1" *-- "n" OrderItem : contains
  OrderItem --> Money : uses
  Order --> OrderStatus : has
}

note right of Order
  聚合根是访问入口
  维护业务规则
end note

@enduml

在这里插入图片描述

在这里插入图片描述

AI辅助建模:从文字到图形

最近我发现了个好玩的:让AI根据业务描述直接生成PlantUML代码!当然要注意数据保密。

// 输入业务描述
const businessDescription = `
  电商系统中,用户可以创建订单,订单包含多个商品项。
  每个商品项有数量和价格。订单有待支付、已支付、已发货等状态。
  支付成功后要扣减库存,发送通知。
`;

// AI生成PlantUML
const plantUmlCode = generateDomainModel(businessDescription);
// 结果就是上面那个图的代码!

4.3 代码实践:让IDE成为你的DDD助手

IDE重构功能:

  • 提取方法:把复杂的业务逻辑提取成独立的方法
  • 移动方法:把方法移到更合适的类中
  • 重命名:让代码使用通用语言

框架和库(点到为止):

  • Java生态
    • Axon Framework:专门支持DDD和CQRS的框架
    • Spring Modulith:Spring的模块化方案,天然支持限界上下文
  • 通用实践
    • 不一定需要专门的框架
    • 关键是理解DDD思想,用普通的OOP也能实践DDD

4.4 AI赋能DDD:未来已来

AI在DDD各阶段的应用

图片

实际应用场景:

  1. 智能命名建议
// 你写的:
class OrderManager {  // AI提示:考虑使用OrderService或OrderRepository
    void updateOrderStatus() {}  // AI提示:changeStatus()更符合DDD风格
}
  1. 自动识别聚合边界
# 基于事件相关性分析
events = ["订单创建""商品添加""价格修改""库存扣减""支付完成"]
ai_suggestion"""
建议聚合划分:
- 订单聚合:订单创建、商品添加、价格修改
- 库存聚合:库存扣减
- 支付聚合:支付完成
"""
  1. 代码异味检测
// AI检测到贫血模型
public class Order {
    private String status;  // AI: 只有数据没有行为
    
    // AI建议添加:
    public void confirm() {
        if (!"PENDING".equals(status)) {
            throw new IllegalStateException("只能确认待处理订单");
        }
        this.status = "CONFIRMED";
    }
}

记住,所有工具都是为了帮助我们更好地思考和交流。如果工具反而成了负担,那就果断抛弃。

我有个朋友,花了三个月研究各种DDD框架,结果业务模型还是没搞清楚。这就是典型的本末倒置。先把思想搞通,再考虑工具

就像我常说的:"给我一支笔一张纸,我也能把领域模型设计出来。工具只是让这个过程更高效,而不是替你思考。"

五、例:DDD实战 - 电商交易系统的领域建模

兄弟们,光说不练假把式。今天,我就带你们真刀真枪地干一场,复盘一个我当年亲手操盘的DDD重构项目——一个复杂的电商交易系统。

5.1 背景与痛点

那是2013年,我接手了一个日订单量百万级的电商项目。别看数据光鲜,后台代码简直就是一场灾难。我给你形容一下当时的惨状:

业务背景

  • 商品类型五花八门:实物、虚拟卡、预售、秒杀……规则各不相同。
  • 营销活动像大姨妈,月月有,周周新,规则复杂到产品自己都得拿个小本本记。
  • 系统像个八爪鱼,对接了十几个外部系统(库存、支付、物流、风控……)。

技术痛点

图片

  • 究极大泥球:所有核心逻辑,几万行代码,全都堆在一个叫OrderService的类里。那不是一个类,那是一坨巨大的、冒着热气的“意大利面条”,谁动谁烫手。
  • 概念黑洞:啥是“交易”?啥是“订单”?啥是“支付”?业务、产品、开发,每个人都有自己的理解,开会基本靠吼,代码里全是order1trade2这种让人抓狂的命名。
  • 脆弱得像个渣男:想加个简单的“满减”活动?对不起,得在OrderService里那坨面条里找十几个地方小心翼翼地改,测试两天,上线前还得烧香拜佛,因为你永远不知道会搞崩哪个犄角旮旯的逻辑。
  • 性能瓶颈:查一个订单,好家伙,恨不得把用户祖宗十八代的信息都从数据库里捞出来,内存动不动就告急。

当时团队的士气非常低落,大家每天不是在救火,就是在去救火的路上。我们都清楚,再这么下去,这套系统迟早要崩。

这时候,我拍板决定:用DDD(领域驱动设计)这把“屠龙刀”,来砍断这团乱麻!

5.2 事件风暴过程(简化版)

要动刀,得先找准筋脉。我们搞了一场“事件风暴”(Event Storming)。我把业务方(懂交易的老司机)、产品、架构师、核心开发全拉到一个会议室里。

图片

第一天上午:鸡同鸭讲

场面一度很混乱:

  • 老王(业务专家):"订单创建了就要扣库存!"
  • 小李(产品经理):"不对,用户可能不付款,不能扣!"
  • 小张(开发):"代码里是先创建订单,再异步扣库存..."
  • 我:"等等,咱们说的'订单创建'是同一个时间点吗?"

大家面面相觑,原来各自理解的"订单创建"完全不是一回事!

第一天下午:拨云见日

经过激烈讨论(差点打起来),我们终于理清了几个关键点:

在这里插入图片描述

在这里插入图片描述

原来,从用户点击"提交订单"到系统真正创建订单,中间有这么多步骤!

第二天:模型浮现

基于梳理出的事件流,我们开始识别限界上下文:

图片

5.3 核心域建模:交易上下文的重生

确定了交易上下文是核心域后,我们开始深入建模。

5.3.1 通用语言的建立

这个过程特别有意思,就像在编写一本"业务词典":

在这里插入图片描述

在这里插入图片描述

有了通用语言,沟通效率提升了10倍不止。以前要解释半天的概念,现在一个词就搞定。

5.3.2 聚合设计:分而治之

最关键的设计决策:把交易(Trade)和订单(Order)设计成两个独立的聚合

为啥要这样?我给你看看之前的设计:

// ❌ 之前的设计:一个巨无霸Order
public class Order {
    private Long orderId;
    private Long userId;
    private List<OrderItem> items;        // 所有商品
    private List<Payment> payments;       // 所有支付记录
    private List<Logistics> logistics;    // 所有物流信息
    private List<Refund> refunds;        // 所有退款记录
    private List<Comment> comments;       // 所有评价
    // 还有20多个List...
}

这种设计的问题:

  • 用户买了10个商家的商品,要创建10个这样的大对象
  • 查个订单状态,把所有历史记录都加载出来
  • 并发修改?想都别想,全锁上!

新的设计:小而美的聚合

在这里插入图片描述

在这里插入图片描述

来看看代码实现——

交易聚合(Trade Aggregate):

// 交易聚合:轻量级的流程管理者
publicclass Trade {
    private TradeId id;
    private BuyerId buyerId;
    private Money totalAmount;
    private TradeStatus status;
    private List<OrderId> orderIds;  // 只存ID,不存对象!
    
    // 创建交易的工厂方法
    public static Trade place(BuyerId buyerId, List<OrderRequest> requests) {
        // 参数校验
        if (requests.isEmpty()) {
            thrownew IllegalArgumentException("兄弟,买点东西再下单啊!");
        }
        
        Trade trade = new Trade();
        trade.id = TradeId.generate();
        trade.buyerId = buyerId;
        trade.status = TradeStatus.PENDING;
        
        // 按商家分组,创建订单
        Map<SellerId, List<OrderRequest>> groupedRequests = 
            groupRequests(requests);
        
        // 创建订单并收集ID
        for (Entry<SellerId, List<OrderRequest>> entry : groupedRequests.entrySet()) {
            Order order = createOrder(trade.id, entry.getKey(), entry.getValue());
            trade.orderIds.add(order.getId());
            trade.totalAmount = trade.totalAmount.add(order.getAmount());
        }
        
        // 发布事件
        DomainEvents.publish(new TradePlacedEvent(trade));
        
        return trade;
    }
    
    // 确认交易
    public void confirm() {
        // 状态检查
        if (this.status != TradeStatus.PENDING) {
            thrownew BusinessException(
                "只有待确认的交易才能确认,当前状态:" + this.status
            );
        }
        
        this.status = TradeStatus.CONFIRMED;
        
        // 发布事件,让订单聚合自己去确认
        DomainEvents.publish(new TradeConfirmedEvent(this.id, this.orderIds));
    }
}

订单聚合(Order Aggregate):

// 订单聚合:专注于单个商家的订单管理
publicclass Order {
    private OrderId id;
    private TradeId tradeId;
    private SellerId sellerId;
    private List<OrderItem> items;
    private Money amount;
    private OrderStatus status;
    
    // 添加商品
    public void addItem(ProductId productId, Quantity quantity, Money price) {
        // 业务规则1:检查商品是否已存在
        Optional<OrderItem> existingItem = findItem(productId);
        if (existingItem.isPresent()) {
            // 已存在就增加数量
            existingItem.get().increaseQuantity(quantity);
        } else {
            // 不存在就新增
            OrderItem newItem = new OrderItem(productId, quantity, price);
            this.items.add(newItem);
        }
        
        // 重新计算金额
        recalculateAmount();
        
        // 发布事件
        DomainEvents.publish(new OrderItemAddedEvent(this.id, productId, quantity));
    }

    // 应用优惠
public void applyPromotion(PromotionResult promotion) {
    Money originalAmount = this.amount;
    this.amount = promotion.calculate(originalAmount);
    
    log.info("订单{}应用优惠,原价:{},现价:{}", 
        this.id, originalAmount, this.amount);
    
    // 记录优惠信息
    this.appliedPromotions.add(promotion.getPromotionId());
}

// 发货
public void ship(ShippingInfo shippingInfo) {
    // 只有已支付的订单才能发货
    if (this.status != OrderStatus.PAID) {
        thrownew BusinessException("订单未支付,不能发货!");
    }
    
    this.status = OrderStatus.SHIPPING;
    this.shippingInfo = shippingInfo;
    this.shippedAt = Instant.now();
    
    // 通知物流系统
    DomainEvents.publish(new OrderShippedEvent(this));
}

5.4 上下文集成:让各个系统协同工作

不同的限界上下文之间怎么协作?这就像不同部门之间的合作,需要建立清晰的接口和协议。

5.4.1 防腐层:对接"脏"系统的保护伞

库存系统是个老系统,接口设计得... 怎么说呢,很有"年代感":

图片

防腐层的实现:

@Component
publicclass InventoryServiceAdapter {
    
    privatefinal LegacyInventoryClient legacyClient;
    
    // 锁定库存 - 对外提供清晰的接口
    public boolean lockInventory(Order order) {
        log.info("开始锁定库存,订单:{}", order.getId());
        
        try {
            // 转换为老系统的请求格式
            List<StockLockRequest> requests = convertToLegacyFormat(order);
            
            // 调用老系统
            for (StockLockRequest request : requests) {
                // 老系统的返回值:1=成功,2=库存不足,3=系统错误
                // 谁设计的???
                int result = legacyClient.lockStock(
                    request.getSku_id(),  // 还是下划线命名
                    request.getNum()
                );
                
                // 翻译返回值
                if (result == 2) {
                    thrownew InsufficientStockException(
                        "商品" + request.getSku_id() + "库存不足"
                    );
                } elseif (result != 1) {
                    thrownew InfrastructureException(
                        "库存系统返回异常:" + result
                    );
                }
            }
            
            returntrue;
            
        } catch (Exception e) {
            // 老系统的异常不能污染我们的领域
            log.error("库存锁定失败", e);
            thrownew InfrastructureException("库存服务暂时不可用", e);
        }
    }
    
    // 转换数据格式
    private List<StockLockRequest> convertToLegacyFormat(Order order) {
        return order.getItems().stream()
            .map(item -> {
                StockLockRequest request = new StockLockRequest();
                // ProductId -> sku_id 的映射
                request.setSku_id(mapToSkuId(item.getProductId()));
                request.setNum(item.getQuantity().getValue());
                return request;
            })
            .collect(toList());
    }
}
5.4.2 领域事件:优雅的异步协作

支付成功后,需要通知多个系统。以前的做法是在PaymentService里直接调用各个系统,结果代码耦合得一塌糊涂。

现在我们用领域事件来解耦:

在这里插入图片描述

在这里插入图片描述

代码实现:

// 交易聚合 - 只管发布事件
publicclass Trade {
    
    public void confirmPayment(PaymentId paymentId, Money paidAmount) {
        // 验证支付金额
        if (!paidAmount.equals(this.totalAmount)) {
            thrownew BusinessException(
                String.format("支付金额不匹配,应付:%s,实付:%s", 
                    this.totalAmount, paidAmount)
            );
        }
        
        // 更新状态
        this.status = TradeStatus.PAID;
        this.paymentId = paymentId;
        this.paidAt = Instant.now();
        
        // 发布事件 - 不关心谁会处理
        PaymentConfirmedEvent eventnew PaymentConfirmedEvent(
            this.id,
            this.buyerId,
            this.totalAmount,
            this.orderIds
        );
        
        DomainEvents.publish(event);
        
        log.info("交易{}支付成功,金额:{}", this.id, this.totalAmount);
    }
}

// 库存服务 - 监听事件,自主处理
@Component
@Slf4j
publicclass InventoryEventHandler {
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private InventoryService inventoryService;
    
    @EventListener
    @Async// 异步处理,不阻塞主流程
    @Transactional
    public void handlePaymentConfirmed(PaymentConfirmedEvent event) {
        log.info("收到支付确认事件,开始扣减库存:{}", event.getTradeId());
        
        try {
            // 加载相关订单
            List<Order> orders = orderRepository.findByIds(event.getOrderIds());
            
            // 逐个扣减库存
            for (Order order : orders) {
                inventoryService.deductStock(order);
                order.markStockDeducted();
                orderRepository.save(order);
            }
            
            log.info("库存扣减成功,交易:{}", event.getTradeId());
            
        } catch (InsufficientStockException e) {
            // 库存不足,发布补偿事件
            log.error("库存不足:{}", e.getMessage());
            DomainEvents.publish(new StockDeductionFailedEvent(
                event.getTradeId(),
                e.getProductId(),
                e.getMessage()
            ));
        }
    }
}

// 积分服务 - 也在监听,各忙各的
@Component
@Slf4j
publicclass PointsEventHandler {
    
    @EventListener
    @Async
    public void handlePaymentConfirmed(PaymentConfirmedEvent event) {
        try {
            // 计算积分(1元=1积分)
            int points = event.getPaidAmount().toYuan().intValue();
            
            // 给用户加积分
            pointsService.addPoints(
                event.getBuyerId(),
                points,
                "订单支付奖励",
                event.getTradeId()
            );
            
            log.info("用户{}获得{}积分", event.getBuyerId(), points);
            
        } catch (Exception e) {
            // 积分失败不影响主流程
            log.error("积分处理失败", e);
        }
    }
}

5.5 实施效果:脱胎换骨的系统

经过6个月的渐进式重构(没错,我们是边开发新功能边重构的),系统发生了翻天覆地的变化。

  1. 代码结构清晰了
    • 每个限界上下文都有明确的职责
    • 业务逻辑集中在领域模型中,不再散落各处
  2. 沟通效率提高了
    • 大家都用通用语言交流,不再鸡同鸭讲
    • 新人看代码就能理解业务
  3. 系统更容易修改了
    • 加营销规则只需要改营销上下文
    • 不同团队可以并行开发不同的上下文
  4. 性能也提升了
    • 聚合设计合理,加载数据量大幅减少
    • 通过事件异步处理,响应时间缩短50%

5.6 经验总结:DDD实践的"坑"与"宝"

兄弟们,这个项目让我收获很多,也踩了不少坑。分享几点心得:

踩过的坑:

  1. 过度设计
    • 刚开始太激动,恨不得每个类都设计成聚合根
    • 后来发现,简单的就该简单处理
  2. 一步到位
    • 想一次性重构整个系统,结果差点翻车
    • 正确做法:小步快跑,逐步演进
  3. 忽视团队
    • 光顾着自己爽,忘了带着团队一起成长
    • DDD需要全员参与,不是架构师的独角戏

收获的宝:

  1. 业务优先
    • 真正理解了"让技术为业务服务"
    • 代码质量的提升带来了业务价值的提升
  2. 持续演进
    • 模型不是一成不变的,要随业务发展而演进
    • 今天的最佳实践,可能就是明天的技术债
  3. 团队成长
    • 整个团队的设计能力都上了一个台阶
    • 大家开始主动思考业务,而不是被动接需求

一个小插曲

项目结束后,有个刚毕业一年的小伙子找我聊天:

"哥,以前看代码像看天书,现在感觉像在读故事。原来代码真的可以'说话'!"

六、写在最后

好了,兄弟们,聊到这,关于DDD这趟硬核的旅程就差不多到站了。

我知道,DDD这套“内功心法”,初看之下,确实有点劝退:概念多、门槛高,刚上手时可能会感觉自己像个无头苍蝇,处处碰壁,甚至会怀疑人生:“搞这么复杂,到底图个啥?”

请相信我这个过来人:这道坎,迈过去,就是一片新天地。

当你不再把DDD当成一堆死记硬背的术语,而是真正开始用它的思想去审视你手里的业务,去和产品、业务方“掰扯”每一个细节时,你会发现,你看待软件设计的视角,会发生质的飞跃。它就像一把锋利的解剖刀,能帮你精准地划开业务的“五脏六腑”,找到问题的核心。

为了让大家下山之后不迷路,我再送你们几个锦囊妙计,这可是我多年踩坑换来的心法,一定记牢了:

  1. 技术是锤子,业务才是钉子。  永远别为了用DDD这把新买的“锤子”,而满世界去找“钉子”。你的出发点,永远应该是“如何更好地解决眼前的业务难题”。
  2. 罗马不是一天建成的,好模型也不是。  别指望一次事件风暴就能搞出完美的模型。建模是一个不断试错、不断迭代、持续演进的过程。拥抱变化,保持谦逊。
  3. “普通话”是地基,地基不牢,地动山摇。  如果团队连最基本的业务术语都无法达成共识,那后面的一切架构、代码,都只是浮沙之上建高塔,早晚要塌。
  4. 先开一枪,再瞄准。  别想着一口吃成胖子,一上来就要在整个系统里全面铺开。先找一个独立的、没那么核心的模块“开一枪”试试水,积累了手感和信心,再逐步扩大战果。

最后,老哥我得再啰嗦一句:DDD不是银弹,它治不了百病。对于那些业务逻辑简单、 CRUD为主的系统,硬上DDD反而会自讨苦吃。但对于那些让你夜不能寐、牵一发而动全身的复杂核心系统,它绝对是当今软件工程界最锋利的武器之一。

记住那句老话:“路要一步一步走,饭要一口一口吃”。学习和实践DDD,同样需要耐心和坚持。

DDD的智慧,是咱们应对复杂性的“定海神针”。但时代在变,技术在飞,咱们也得抬头看路。下一章,我将带大家打开一扇新世界的大门——如何用AI技术为我们的架构设计插上翅膀。届时,我会跟你分享,如何将DDD这份“传统手艺”与最新的AI能力结合起来,打造出更智能、更懂业务的未来系统。


本章主要参考及推荐阅读:

[1] Evans, Eric. 《领域驱动设计:软件核心复杂性应对之道》. 人民邮电出版社, 2010. (DDD的开山之作,必读经典)

[2] Vernon, Vaughn. 《实现领域驱动设计》. 电子工业出版社, 2014. (实践指南,手把手教你怎么做)

[3] 张逸. 《领域驱动设计:软件核心复杂性应对之道》. 机械工业出版社, 2018. (国内DDD专家的实践总结)

[4] 欧创新. 《领域驱动设计模式、原理与实践》. 机械工业出版社, 2019. (结合国内实际情况的DDD实践)

[5] Brandolini, Alberto. 《事件风暴:快速理解复杂业务领域的协作建模方法》. 人民邮电出版社, 2021. (事件风暴的权威指南)

[6] Richardson, Chris. 《微服务架构设计模式》. 机械工业出版社, 2019. (微服务与DDD的完美结合)

[7 钟敬. 《领域驱动设计精粹》. 电子工业出版社, 2020. (DDD核心概念的精炼总结)

[8 Fowler, Martin. "限界上下文". martinfowler.com, 2014. (大师对限界上下文的经典阐述)

参考:

第9章:架构思维与设计能力(下):领域驱动设计(DDD)与复杂系统治理