分层架构、DDD、整洁代码与重构
前言
架构设计的价值不在于堆砌分层和模式,而在于让业务规则、外部依赖和团队协作能够随需求演进保持清晰。本文从分层、DDD、依赖倒置到渐进式重构,讨论企业 Java 服务在复杂业务中保持可维护性的实践方法。
架构题考察长期演进能力
企业 Java 服务的难点往往不在第一次实现功能,而在规则增加、团队扩张、依赖变多后如何保持可修改性。面试回答分层、DDD 或整洁架构时,应结合真实边界:Controller 不承载交易规则,Service 不直接散落 SQL 和第三方 SDK,领域规则不依赖 HTTP/ORM 框架,跨系统事件要可追踪和可补偿。
分层的目的与边界
一个实用的分层可以是:接口层负责协议与输入校验,应用层编排用例和事务边界,领域层表达核心规则,基础设施层实现数据库、MQ、Redis、第三方渠道。不是每个小项目都必须有四层目录,而是依赖方向应清晰:业务规则不应依赖外部细节。
| 层 | 应承担的职责 | 不应承担的职责 |
|---|---|---|
| Interface/Controller | DTO、协议转换、输入校验、HTTP 状态 | 核心状态机、复杂事务和直接拼 SQL |
| Application Service | 用例编排、事务、权限上下文、调用端口 | 堆积所有领域判断成为巨型 Service |
| Domain | 实体、值对象、聚合、领域服务、规则 | 直接依赖 Spring MVC、JPA Repository、MQ Client |
| Infrastructure | Repository 实现、消息发布、缓存、SDK | 决定订单能否退款等核心业务规则 |
DDD 中真正有价值的概念
DDD 不是给类加 Entity、Aggregate 后缀。它先划分限界上下文,例如订单、库存、支付、营销各自拥有模型和数据,不共享一张“万能订单表”;上下文之间用 API、事件和防腐层交互。
聚合是一组需要在同一一致性边界内维护不变量的对象。订单聚合可保证“已关闭订单不能支付”“订单项数量不能为零”,但不应把库存、优惠券、支付流水全部塞进同一数据库事务。跨聚合/跨服务动作通过本地事务、领域事件、Outbox、幂等消费和补偿达到最终一致。
值对象应不可变且按值相等,例如 Money、Address、SkuId;实体有身份和生命周期,例如 Order。把金额表示为 long cents 或明确的 Money 值对象,避免 double 金额计算和到处散落的币种规则。
依赖倒置与可测试设计
应用层依赖端口接口,基础设施层实现接口:
1 | public interface PaymentGateway { |
这不是为了多建接口。稳定的核心规则、可能替换的外部渠道、需要隔离测试的依赖适合抽象;只有一个简单且稳定的本地实现时,过度抽象会增加认知负担。接口名称应表达业务能力,而不是基础设施细节,例如 PaymentGateway 好于 AlipayClientInterface。
常见坏味道与渐进式重构
| 坏味道 | 风险 | 重构起点 |
|---|---|---|
| 巨型 Controller/Service | 协议、规则、IO、事务耦合,难测难改 | 先提取一个明确用例或规则对象,不做一次性大重写 |
| if/else 状态爆炸 | 新状态、新渠道不断修改同一类 | 用状态机、策略或规则表,但保留可追踪的状态转换 |
| Entity 直接作为 API DTO | 字段越权、懒加载、兼容性和持久化耦合 | 明确 Request/Response DTO 与映射 |
| 分布式调用嵌套过深 | 超时级联、故障边界不清 | 定义同步必要路径,其余改事件异步并设置超时/熔断 |
| 共享数据库跨服务写表 | 领域边界被绕过,升级困难 | 逐步收敛写入所有权,通过 API/事件同步 |
重构前先补覆盖关键行为的测试和观测指标,再采用 strangler/旁路方式逐步迁移。一次“重写全部微服务”往往难以验证等价性。每一步要有数据迁移、双写/校验、回滚和删除旧路径的计划。
面试回答模板
回答“如何设计订单创建”时,可以先说明订单上下文负责订单不变量和状态机,库存/支付属于独立上下文;应用服务在本地事务中写订单和 Outbox 事件;事务提交后发布事件,库存和支付消费者按事件 ID 幂等处理;失败由重试、对账和补偿治理。然后说明 Controller 仅负责 DTO 与认证上下文,不把规则放进 HTTP 层。
这种回答同时体现了分层、聚合边界、事务边界、事件驱动和生产可恢复性,比只说“用 MVC 三层架构”更完整。
高频面试题
| 问题 | 回答要点 |
|---|---|
| DDD 的聚合是什么? | 一致性边界和事务修改单元,由聚合根对外维护不变量,不是任意对象集合。 |
| 为什么不让 Controller 直接调 Repository? | 会把协议、事务、领域规则和持久化耦合,难测且复用困难;简单 CRUD 也应保持边界不过度复杂。 |
| 领域事件一定异步吗? | 不一定。进程内同步事件可用于同一事务规则;跨服务事件通常异步且要 Outbox/幂等/补偿。 |
| 如何避免过度设计? | 从变化频繁、外部依赖和核心规则处抽象,用测试和实际变更验证,不为每个类创建多层接口。 |
| 重构如何保证安全? | 先建立测试和观测,渐进迁移、双写校验、灰度与可回滚,避免大爆炸式替换。 |


