DDD/TDD/SDD三件套在Framework项目中的实战落地与工程化整合

📅 2026/8/12 9:49:15
DDD/TDD/SDD三件套在Framework项目中的实战落地与工程化整合
1. 项目概述当“方法论”遇上“真实代码”在软件工程领域我们总是不乏听到各种听起来很“高大上”的方法论DDD领域驱动设计教你如何用代码精准表达业务TDD测试驱动开发让你通过测试来驱动设计保证代码质量SDD软件设计文档则强调在编码前用文档理清思路统一认知。这些概念单独拿出来任何一个资深开发者都能侃侃而谈。但问题来了当这些“屠龙之术”要真正落地到一个具体的、正在迭代的、有历史包袱的framework仓库时会发生什么我最近就主导了这样一个项目在一个中等规模、服务于多个核心业务线的内部framework中系统性地引入并整合 DDD、TDD 和 SDD 实践。这不是一个从零开始的绿田项目而是一个“旧城改造”工程。目标很明确不是做一次性的“运动式”改进而是将这些工程实践变成团队日常研发的“肌肉记忆”最终提升framework的健壮性、可维护性和对业务的支持能力。这个过程充满了挑战也收获了大量一线实战经验。今天我就抛开理论教科书以一个亲历者的身份和你聊聊 DDD/TDD/SDD 这三件套在一个真实的framework项目里是如何从“纸上谈兵”到“真枪实弹”落地的。你会发现真正的难点往往不在于理解概念而在于如何在复杂的现实约束下做出合理的取舍与适配。2. 工程骨架重构为三件套打造“基础设施”在动任何一行业务代码之前我们花了近两周的时间来重构整个项目的“工程骨架”。这是整个落地过程的基础如果地基打歪了后面所有的方法论都会变形。我们的framework原本是一个典型的“大泥球”架构所有代码都堆在一个模块里单元测试覆盖率不足20%文档更是停留在远古的 README 阶段。2.1 模块化与界限上下文的映射我们做的第一件事就是运用 DDD 中的“界限上下文”思想对framework进行模块化拆分。这不是简单的按功能分文件夹而是根据业务能力的内聚性和变更频率来划分物理模块。例如我们的framework主要提供用户认证、支付流程编排和消息通知三大核心能力。我们将其拆分为三个独立的 Maven 模块如果是其他语言生态则是相应的包或库framework-auth-context负责所有与身份认证、权限校验相关的逻辑。framework-payment-context封装了与不同支付渠道的对接、订单状态机、资金流水的核心逻辑。framework-notification-context统一处理短信、邮件、站内信等各类消息的发送。注意模块化的粒度是关键。拆得太细模块间依赖会变得极其复杂增加构建和理解的负担拆得太粗又无法达到隔离变化的目的。我们的经验是一个界限上下文应对应一个团队或一个明确的业务子域其内部修改不应频繁波及其他模块。每个上下文模块内部我们严格遵循 DDD 的分层架构src/main/java ├── application/ # 应用层编排领域服务处理事务、权限等 ├── domain/ # 领域层核心包含实体、值对象、聚合根、领域服务、仓库接口 ├── infrastructure/ # 基础设施层实现仓库接口、集成外部服务如数据库客户端、消息队列 └── interfaces/ # 接口层可选对外暴露的API如REST Controller、RPC Stub这种结构强制性地将业务核心逻辑domain与技术实现细节infrastructure分离。framework中那些与具体数据库如MySQL表结构、缓存如Redis命令强耦合的代码被全部赶到了infrastructure里。domain层只依赖自身的抽象接口这使得核心业务逻辑变得极其纯净且可测试。2.2 测试框架与CI/CD流水线改造TDD 要落地一个高效、可靠的测试环境是前提。我们统一了测试技术栈单元测试JUnit 5 Mockito。这是 TDD 的主战场。我们要求所有domain层的核心逻辑实体、值对象、领域服务必须100%通过单元测试覆盖并且优先编写测试。集成测试Spring Boot Test用于测试application层服务、infrastructure层与真实数据库使用Testcontainers启动一个真实的MySQL Docker容器或外部API使用WireMock进行打桩的集成。契约测试Pact用于保障framework作为服务提供者其API变更不会破坏消费者使用该framework的业务方。我们在 GitLab CI/CD 流水线中设置了严格的关卡编译与单元测试关卡任何提交必须先通过所有单元测试覆盖率不能低于预设阈值我们设为85%。集成测试关卡合并请求Merge Request在合并前必须通过全套集成测试。契约测试关卡当interfaces层的API发生变更时自动运行契约测试确保向后兼容或明确提示不兼容变更。这套流水线是 TDD 实践的“守护神”。它让“测试先行”不再依赖于个人的自觉而是变成了一个无法绕过的、自动化的质量门禁。开发者很快养成了习惯红测试失败- 绿测试通过- 重构这个循环变得自然而然。2.3 文档即代码SDD的工程化集成传统的 Word 或 Confluence 文档极易与代码脱节。我们采用了“文档即代码”的理念将 SDD 无缝集成到工程骨架中。技术设计文档使用 Markdown 格式存放在每个模块的docs/design目录下。任何新功能或重大重构必须先提交设计文档评审通过后才能编码。文档需用 PlantUML 绘制关键的类图、序列图。API文档对于interfaces层暴露的 REST API我们使用 SpringDoc OpenAPI 自动生成 OpenAPI 3.0 规范并集成 Swagger UI。API的修改直接体现在代码注解中文档自动同步。决策记录我们引入了 ADRArchitecture Decision Record记录所有重要的架构决策、技术选型的背景和原因。这些ADR存放在项目根目录的docs/adr下例如001-use-axon-framework-for-event-sourcing.md。通过将文档与代码放在同一个仓库并用同样的版本控制工具管理我们确保了文档的可追溯性和时效性。在代码评审时评审者可以很方便地对照设计文档来检查实现是否偏离了初衷。3. DDD核心模式在Framework中的具象化有了坚实的工程骨架DDD的核心模式才能真正在framework的代码中生长出来。这个过程不是生搬硬套概念而是让概念为framework的“通用性”和“业务表达能力”服务。3.1 聚合根与实体从“贫血模型”到“富血模型”我们framework中有一个经典的“订单”概念。旧代码是一个典型的“贫血模型”Order类只是一堆属性和getter/setter的集合所有业务逻辑都散落在各种名为OrderService、OrderManager的“上帝类”中。我们将其重构成了一个富血模型的聚合根public class Order extends AbstractAggregateRootOrder { private OrderId id; private Money totalAmount; private OrderStatus status; private ListOrderLine orderLines; // 值对象集合 // 核心业务命令创建订单 public static Order create(OrderId id, ListOrderItem items, CustomerId customerId) { // 校验参数 Objects.requireNonNull(items); if (items.isEmpty()) { throw new IllegalArgumentException(Order must have at least one item); } // 计算总价业务逻辑内聚 Money total items.stream() .map(OrderItem::calculateSubTotal) .reduce(Money.ZERO, Money::add); // 创建订单实体 Order order new Order(id, total, OrderStatus.CREATED); order.orderLines items.stream() .map(item - new OrderLine(item.getProductId(), item.getQuantity(), item.getUnitPrice())) .collect(Collectors.toList()); // 发布领域事件 order.registerEvent(new OrderCreatedEvent(order.getId(), customerId, total)); return order; } // 核心业务命令支付订单 public void pay(PaymentId paymentId, Money paidAmount) { if (this.status ! OrderStatus.CREATED) { throw new IllegalOrderStateException(Only CREATED order can be paid.); } if (!this.totalAmount.equals(paidAmount)) { throw new PaymentAmountMismatchException(...); } this.status OrderStatus.PAID; this.paymentId paymentId; registerEvent(new OrderPaidEvent(this.id, paymentId)); } // 将状态判断逻辑封装在实体内部 public boolean canBeCancelled() { return this.status OrderStatus.CREATED || this.status OrderStatus.PAID; } }这个Order聚合根自己负责其核心生命周期规则如“只有已创建的订单才能支付”、“支付金额必须匹配”。它不再是一个被动的数据容器而是一个拥有行为和数据的主动业务对象。这样做最大的好处是任何使用我们framework的业务方在操作订单时都不可能绕过这些核心业务规则极大降低了业务错误的风险。3.2 领域服务与领域事件并非所有业务逻辑都适合放在实体里。当一个操作涉及多个聚合的协作或者是一个无状态的复杂计算时我们就使用领域服务。例如我们有一个FundTransferService领域服务它协调Account账户和Transaction交易记录两个聚合完成转账操作。这个服务本身不持有状态但封装了“检查余额、扣款、创建交易记录、发布转账成功事件”这一系列不可分割的领域逻辑。领域事件是我们实现framework内部各上下文之间以及framework与外部业务系统之间松耦合通信的关键。上面Order聚合发布的OrderPaidEvent事件可以被本上下文内NotificationHandler监听并发送支付成功通知。其他上下文InventoryContext的监听器消费该事件触发库存扣减。外部业务系统通过消息中间件如Kafka发布出去供业务方订阅处理。通过事件驱动我们将framework从一个“被动调用”的库部分转变为一个“主动通知”的平台架构的响应性和扩展性得到了提升。3.3 仓储模式的实践统一数据访问抽象在domain层我们只定义仓储接口例如OrderRepositorypublic interface OrderRepository { Order findById(OrderId orderId); OrderId save(Order order); void delete(OrderId orderId); // 根据业务定义查询方法避免暴露底层细节 ListOrder findPendingOrders(Instant since); }而在infrastructure层我们有基于 JPA 的JpaOrderRepository实现或者基于 MyBatis 的实现。这种模式带来了两个巨大优势可测试性在单元测试domain逻辑时我们可以轻松地用 Mockito 模拟OrderRepository完全隔离数据库。可移植性如果未来需要更换持久化技术比如从 MySQL 迁移到另一种数据库只需在infrastructure层提供新的实现domain核心业务代码一行都不用改。4. TDD驱动下的开发闭环实战TDD 是我们保证代码质量和设计方向的“指南针”。在framework开发中TDD 循环尤其重要因为我们的代码会被众多业务方依赖必须极度可靠。4.1 一个完整的TDD循环示例新增“订单折扣”功能假设我们需要在Order聚合中增加一个“应用折扣”的功能。第一步写一个失败的单元测试红我们先在OrderTest中写下我们期望的行为Test void shouldApplyDiscountAndUpdateTotalAmount() { // Given: 一个总价为100元的订单 Order order Order.create(... with total 100 ...); Discount discount new Discount(PROMO10, new BigDecimal(0.1)); // 10%折扣 // When: 应用折扣 order.applyDiscount(discount); // Then: 订单总价应变为90元且折扣信息被记录 assertThat(order.getTotalAmount()).isEqualTo(Money.of(90)); assertThat(order.getAppliedDiscount()).isEqualTo(discount); // 还可以验证领域事件是否发布 assertThat(domainEvents()).containsInstanceOf(OrderDiscountAppliedEvent.class); }运行测试显然会失败因为applyDiscount方法还不存在。第二步实现最简单代码让测试通过绿我们以最快的方式修改Order实体添加必要字段和方法让测试通过。此时的实现可能很粗糙比如直接修改了totalAmount字段。第三步重构在测试通过的保护下我们开始审视代码。我们发现直接修改totalAmount破坏了“总价应由订单项计算得出”的不变性。于是我们重构引入一个discount字段并修改getTotalAmount()方法使其返回原始项总价 - 折扣。同时确保applyDiscount方法包含业务规则校验如“已支付的订单不能修改折扣”。这个“红-绿-重构”的循环强迫我们在写业务逻辑前就思考其使用方式测试即文档并持续打磨代码设计避免过度设计或设计不足。4.2 测试策略与测试替身的使用在framework的测试中我们大量使用测试替身Mock用于模拟外部依赖的行为和验证交互。例如在测试FundTransferService时我们 MockAccountRepository和TransactionRepository验证save方法是否被以正确的参数调用。Stub为测试提供预设的、确定的响应。例如Stub 一个汇率服务始终返回固定的汇率以保证测试的确定性。Fake创建一个轻量级的、可用于测试的实现。例如我们有一个InMemoryOrderRepository用于那些需要真实仓储交互但又不想启动数据库的集成测试。实操心得不要滥用 Mock。一个常见的反模式是过度 Mock导致测试变成了验证“如何实现”而不是“实现了什么”。我们的原则是只 Mock 真正的跨进程或外部不稳定依赖如数据库、HTTP客户端对于同一个进程内、自己维护的类优先使用真实对象或Fake。这能更好地测试类之间的集成和协作。5. SDD连接设计与实现的桥梁SDD 不是一份写完就扔的文档而是贯穿整个开发周期的活文档。它在framework项目中的作用尤为突出因为framework的设计直接影响所有下游业务方。5.1 设计文档的结构化与评审我们为每个重要特性或模块定义了一个标准的设计文档模板背景与目标为什么要做解决什么问题非功能性需求性能QPS、延迟、可用性、扩展性要求。领域模型分析核心聚合、实体、值对象、领域事件的定义及其关系图PlantUML。API设计新增或变更的接口定义OpenAPI片段。架构设计模块划分、数据流图、与现有系统的集成方式。测试策略计划如何进行单元、集成、端到端测试。发布与回滚计划如何灰度、如何监控、出现问题如何回滚。这份文档在编码开始前必须经过团队核心成员和可能受影响方的技术评审。评审的重点不是挑语法错误而是挑战设计假设、发现潜在风险、对齐各方认知。很多时候一场激烈的设计评审能避免后期数周的返工。5.2 文档与代码的同步演进我们严格遵循一个规则任何代码提交如果涉及到设计决策的变更必须同步更新对应的设计文档或ADR。在合并请求Merge Request中评审者会同时检查代码变更和文档变更。Git的历史记录使得文档的每一次演变都有迹可循。例如当我们决定将某个同步调用改为异步事件驱动时我们不仅修改了代码还更新了架构设计图和数据流图并在ADR中补充了“从同步到异步演进的决策记录”解释了性能瓶颈的发现过程、异步方案的选择比较以及引入的最终一致性问题的应对措施。6. 整合过程中的挑战与应对策略将三件套整合落地绝非一帆风顺我们遇到了许多教科书里没写的坑。6.1 挑战一历史代码的“改造阻力”问题旧代码结构混乱直接应用DDD分层和聚合根概念阻力巨大牵一发而动全身。策略我们采用“绞杀者模式”和“适配器模式”相结合的策略。对于完全重写不现实的核心模块我们在其外部包裹一层“领域层适配器”。新的业务逻辑调用这层适配器适配器内部再去调用老代码。同时逐步将老代码中的业务逻辑向适配器内迁移。对于新增功能或重构代价较小的模块坚决采用新的DDD架构与旧模块通过清晰的接口防腐层进行交互。让新代码成为“绿洲”逐渐扩大范围。6.2 挑战二TDD带来的初期效率下降问题团队不熟悉TDD感觉写测试浪费时间拖慢了开发速度。策略培训与结对编程组织TDD工作坊让有经验的同事带领大家结对编程亲身感受“测试先行”如何减少调试时间、如何驱动出更好的设计。展示长期收益收集数据展示在引入TDD后生产环境缺陷率的下降比例、重构自信心的提升。让大家看到前期多花的1小时测试时间可能避免了后期10小时的线上排查和修复时间。提供高质量模板为常见的模式如对聚合根的操作、领域服务的测试提供测试代码模板降低大家的学习和起步成本。6.3 挑战三文档的维护成本与“僵尸文档”问题大家忙于编码文档更新不及时逐渐变成“僵尸文档”。策略工具自动化将能自动化的文档如API文档、依赖图全部自动化减少手动维护负担。流程卡点将“文档更新”作为代码合并请求的必选项在CI流水线中甚至可以加入简单的检查如检查设计文档的修改时间是否晚于相关代码文件。文化倡导在团队内强调维护文档是维护代码的一部分一份过时的文档比没有文档危害更大因为它传递错误信息。6.4 挑战四与业务方团队的认知摩擦问题业务方团队习惯了直接调用framework里细粒度的、过程式的“Service”不理解我们为什么要封装出“聚合根”和“领域事件”。策略沟通与培训举办分享会向业务方团队解释DDD带来的好处更强的业务语义、更好的封装性、更低的接入错误率。提供渐进式迁移指南编写详细的指南和示例代码展示如何从旧的调用方式迁移到新的领域模型API。建立反馈渠道积极收集业务方在使用新API时的痛点快速迭代改进。让他们感受到新架构确实在解决他们的实际问题而不是在制造麻烦。7. 效果评估与未来展望经过近半年的推行和实践DDD/TDD/SDD三件套在我们的framework项目中已经深深扎根。可量化的收益缺陷逃逸率从生产环境反馈的、由framework导致的严重缺陷数量下降了约70%。代码复用度由于清晰的界限上下文和领域模型新业务功能的接入速度平均提升了40%很多通用能力可以直接复用。团队认知负载新成员 onboarding 的时间缩短了因为代码结构和设计文档提供了清晰的地图。更重要的无形收益设计自信团队在进行重大重构或添加复杂功能时信心更足因为有测试套件和领域模型作为安全网。沟通效率团队内和跨团队沟通时“订单”、“支付”、“事件”这些术语有了统一且精准的代码对应物减少了歧义。技术债务可控通过持续的重构和良好的设计技术债务的增长被有效遏制代码库保持活力。当然这不是终点。我们仍在持续探索事件溯源对于核心的财务、交易链路我们正在小范围试点事件溯源以获得更强大的审计和回放能力。CQRS在读写负载差异巨大的场景下探索命令查询职责分离进一步提升查询性能。更智能的测试探索基于属性测试如jqwik来发现边缘情况以及利用代码覆盖率分析来识别未被测试覆盖的业务逻辑。回头看将 DDD、TDD、SDD 整合落地最大的感触是这从来不是单纯的技术问题而是一个系统工程和人因工程问题。它需要坚定的技术领导力、持续的团队磨合以及对工程卓越的不懈追求。最有效的起点往往不是最宏大的蓝图而是从一个清晰的模块、一个核心的聚合根、一组严格的测试用例开始一步步地构建起一个高质量、可演进的软件系统。我们的framework仓库就是这段旅程最好的见证。