系统设计——DDD领域模型驱动实践

299 阅读20分钟

摘要

本文主要介绍了DDD(领域驱动设计)在系统设计中的实践应用,包括其在编码规范、分层架构设计等方面的具体要求和建议。重点强调了应用层的命名规范,如避免使用模糊的Handler、Processor等命名,推荐使用动词加业务动作的清晰命名方式;区分命令和查询服务的命名规则;以及Repository层和防腐层的设计原则。此外,还探讨了DDD的价值和在实际系统中的应用思考。

1. DDD领域模型驱动Coding规范

1.1. 统一语言规范

与统一语言(英文)一致的代码命名,保证代码可读性和可沟通性、降低团队沟通成本和团的其他成员理解成本。

1.2. Domian层

1.2.1. Domain对象拒绝Getter、Setter、Constructor等注解?

在 DDD(领域驱动设计)中,Domain 对象(如 Entity、Value Object、Aggregate Root)确实应当尽量避免使用 Getter Setter Constructor 等 Lombok 或 IDE 自动生成的注解,这是出于“建模思想”与“封装业务规则”的考虑。以下是详细解释与建议。

  1. 🧠 破坏封装与建模思想:DDD 强调 通过代码表达领域模型的意图和业务规则。直接暴露 getter/setter,会让你的领域对象退化为一个贫血模型(Anemic Domain Model),只是一个数据容器,而不是业务的承载体。
// ❌ 错误写法:getter/setter + public 字段完全暴露
@Getter
@Setter
public class Order {
    private Long id;
    private String status;
}

这样写没有任何领域语义,Order 的状态可以随意被外部修改,违背领域封装原则。

  1. 🛡️ Setter 允许任意修改内部状态,打破一致性:领域对象的核心职责是:保障业务数据的完整性和一致性。Setter 让外部可以绕过业务规则,随意设置对象属性:
order.setStatus("已支付"); // 没有校验是否可以从“已取消”变成“已支付”

而正确的做法应是:

public void markAsPaid() {
    if (!canPay()) {
        throw new IllegalStateException("当前状态不可支付");
    }
    this.status = Status.PAID;
}

3. 🏗️ Constructor 注解(如 @AllArgsConstructor)缺乏表达力

@AllArgsConstructor 自动生成构造函数,但它不能表达“构建一个合法对象的业务意图”,也难以做校验。例如,构建一个 LoanApplication 时可能需要校验利率、贷款人信息、期限等,而这些必须在构造过程强校验。应该这样:

public LoanApplication(Applicant applicant, BigDecimal amount, Term term) {
    if (amount.compareTo(BigDecimal.ZERO) <= 0) {
        throw new IllegalArgumentException("贷款金额必须大于0");
    }
    this.applicant = applicant;
    this.amount = amount;
    this.term = term;
}

正确做法:用行为方法代替 Setter,用工厂方法代替构造器,示例:DDD 风格的 Order 聚合根

public class Order {
    private final OrderId id;
    private final List<OrderItem> items = new ArrayList<>();
    private OrderStatus status = OrderStatus.CREATED;

    // 构造器设为 protected 或 private,仅通过工厂创建
    protected Order(OrderId id) {
        this.id = id;
    }

    public static Order create(OrderId id) {
        return new Order(id);
    }

    public void addItem(Product product, int quantity) {
        if (status != OrderStatus.CREATED) {
            throw new IllegalStateException("只能在创建状态添加商品");
        }
        this.items.add(new OrderItem(product, quantity));
    }

    public void markAsPaid() {
        if (status != OrderStatus.CREATED) {
            throw new IllegalStateException("不能重复支付");
        }
        this.status = OrderStatus.PAID;
    }

    public OrderId getId() {
        return id;
    }

    public List<OrderItem> getItems() {
        // 可返回不可变副本
        return Collections.unmodifiableList(items);
    }
}

Domian对象code实践建议

项目DDD 建议
@Getter只在读取聚合标识、只读字段时局部使用
@Setter禁止在领域对象中使用
@AllArgsConstructor不建议使用
@NoArgsConstructor避免(除非 ORM 必须)
构造函数应包含业务校验逻辑
Builder 模式可用于构造复杂值对象

1.2.2. Domain仅包含领域模型定义的对象,且用plain object。

Domain层主要包含领域模型(Domain Model) ,比如:

  • 实体(Entity) :有唯一标识的业务对象,如“订单”、“用户”。
  • 值对象(Value Object) :无唯一标识,仅通过属性值定义的对象,如“地址”、“金额”。
  • 聚合根(Aggregate Root) :实体的集合边界,保证数据一致性。
  • 领域服务(Domain Service) :当业务逻辑不适合放在某个实体上时,用领域服务封装。
  • 领域事件(Domain Event) :业务状态变化的事件。

不包含技术层相关的类(比如 DAO、DTO、Controller、ServiceImpl等)。

Domain对象都用plain object

  • Plain Object 指的是简单的、纯粹的业务对象,即不依赖特定框架的特殊基类、注解或技术代码。
  • 这意味着领域模型类尽量只包含业务属性和行为,不引入持久化、网络、序列化等技术代码。
  • 例如,领域模型不要直接继承 JPA Entity,或带大量数据库注解;避免和框架耦合
  • 保持领域模型的纯粹性,方便单元测试和业务复用。
// 领域实体示例(Plain Object)
public class Order {
    private String orderId;
    private List<OrderItem> items;

    public void addItem(OrderItem item) {
        // 业务规则校验
        items.add(item);
    }

    // 省略Getter/Setter,聚焦行为
}

这里的 Order 是一个“纯领域对象”,没有任何持久化注解,也没有依赖框架特性。

  • 在实际项目中,领域对象和数据库映射对象通常会做分离,通过Repository 层进行转换。
  • 这样既保持了领域层的纯粹,也满足了持久化需求。

1.2.3. Domain层不依赖spring的AOP和lOC等三方包

Domain层应该保持“纯净”,不依赖 Spring、MyBatis、Hibernate、Lombok 等任何三方框架,尤其不能依赖 Spring 的 IoC、AOP 等容器机制。

DDD 要求领域模型:

  • 表达业务含义清晰(面向业务而非技术)
  • 可以脱离框架独立测试或演算(保持“领域独立性”)
  • 保持长生命周期可演进(与基础框架解耦)

所以:@Component@Autowired@Transactional@Service 等 Spring 注解 → ❌ 不应该出现在 Domain 层@Getter@Setter@Entity@Table 等 Lombok / ORM 注解 → ❌ 不应该污染领域模型

如果你的 Domain层用了一堆Spring注解,要小心:

  1. 说明你的领域模型很可能耦合了基础设施层逻辑(违背 DDD 分层)
  2. 可能导致业务逻辑难以复用或测试
说明是否可用框架注解
domain纯领域逻辑模型、实体、聚合、值对象、领域服务❌ 不依赖任何框架
infrastructure数据持久化、消息中间件、缓存等实现细节✅ 用 Spring 管理
application调用编排、流程协调、事务管理等✅ 用 Spring
interfacesController、API 接口层✅ 用 Spring MVC 注解等
1.2.3.1. ❌ 不推荐(污染领域模型)
@Entity
@Getter
@Setter
public class LoanApplication {
    @Id
    private Long id;

    @Autowired
    private CreditService creditService; // 依赖外部服务

    public boolean canApprove() {
        return creditService.checkCredit(id); // 不可测、强耦合
    }
}
1.2.3.2. ✅ 推荐(纯净的领域对象)
public class LoanApplication {

    private Long id;
    private int score;

    public LoanApplication(Long id, int score) {
        this.id = id;
        this.score = score;
    }

    public boolean canApprove() {
        return score >= 700;
    }
}
  • 外部服务(如 CreditService)由 DomainServiceApplicationService 在外部注入,传参给领域对象,不在对象中注入依赖。
1.2.3.3. 🧪 好处:
  • ✅ 易于单元测试:构造纯对象即可测试,不依赖 Spring 环境。
  • ✅ 解耦框架:更容易迁移、更少“技术污染”。
  • ✅ 聚焦业务:领域对象只关心业务含义,职责清晰。

1.2.4. Domain对象行为拒绝setter、update、modify、save、delete等无明确业务含义的方法?

这是 DDD(领域驱动设计)中对 Domain 对象(即领域模型) 的一种强烈编码规范:领域对象的方法必须具备明确的业务含义。这些方法通常只表示技术操作,而没有任何具体的业务语义,违背了 DDD 中“领域模型体现业务行为”的基本理念。

错误方式(技术性方法)正确方式(业务性方法)
user.setStatus("DISABLED")user.disable()
loan.updateStatus("APPROVED")loan.approve()
order.delete()order.cancel()
account.save()account.deposit(amount)/ account.withdraw(amount)
1.2.4.1. 领域模型是业务专家的语言映射
  1. setter/update/save 等是 面向 ORM 和数据库 的语言。
  2. approve()reject()cancel() 等方法是 业务专家能听懂的术语,符合“统一语言”的要求。
1.2.4.2. 降低贫血模型(Anemic Model)风险
  • 如果实体只有 getter/setter,就沦为了数据容器(贫血模型),逻辑散落在 service 中,失去了封装。
  • 加入业务行为,才能形成真正的充血模型,逻辑内聚,模型可维护、可扩展。
1.2.4.3. 那领域模型中应该怎么定义方法?

按照“行为驱动模型”的思路:

public class LoanApplication {

    private LoanStatus status;

    public void approve() {
        if (this.status != LoanStatus.PENDING) {
            throw new IllegalStateException("Loan cannot be approved in current state");
        }
        this.status = LoanStatus.APPROVED;
    }

    public void reject(String reason) {
        this.status = LoanStatus.REJECTED;
        // 记录拒绝原因,可能还要写审计日志
    }
}
方法名是否允许说明
setXxx()除非是纯值对象,如 DTO、配置类
updateXxx()太笼统,建议改为具体业务操作
approve()cancel()有明确业务语义
validate()calculate()计算、验证行为是业务的一部分
toDTO()toSnapshot()✅(只读转换)可接受,但可以考虑放到 assembler 或 factory
1.2.4.4. ✅ Domain对象行为总结
编码项DDD 推荐
是否写 setter❌ 尽量避免
方法是否用 update/save/delete 命名❌ 避免
方法是否要有业务含义✅ 强烈建议
领域对象职责封装业务状态 + 表达业务行为
对象类型推荐值对象不可变、实体对象充血

1.2.5. 值对象命名不用加上标识技术语言的Enum。

在很多项目中,我们习惯于写:

public enum LoanStatusEnum {
    PENDING, APPROVED, REJECTED;
}

但在 DDD 中,更推荐写为:

public enum LoanStatus {
    PENDING, APPROVED, REJECTED;
}
1.2.5.1. 领域语言优先:表达业务语义,而非技术语义

DDD 强调“领域语言”,即代码的命名要贴合业务、可读性强,而不是暴露技术细节。

  • LoanStatus是一个业务概念,用户、产品经理、风控人员都能理解。
  • LoanStatusEnum 是面向程序员的命名方式,暴露了技术实现细节(用的是 enum)。

💡 DDD 建议隐藏实现细节、突出业务意图。值对象本身就是“一个不可变、具备自我完整性的业务值”,不管你是用 enumclassrecord 实现的,业务只需要知道这是 LoanStatus,而不是“枚举”。

1.2.5.2. 值对象不仅限于 enum

很多值对象是用 class 定义的:

public class Amount {
    private BigDecimal value;
    private String currency;
}

如果你对 enum 加上 Enum 后缀,那你是不是也要对上面的 class 叫 AmountClass?显然没有这个习惯。所以统一叫“LoanStatus”这种业务术语,风格更一致、更干净。

1.2.5.3. ❌ 什么情况下不推荐简化命名?

以下场景你可能仍然需要保留Enum后缀(但这不再是纯粹 DDD 语境了):

  • 与其他类型冲突,如 LoanStatus既是实体字段又是类名时。
  • 和第三方库集成时,需要区分类型。
  • 较低层的工具包(非 DDD),用于统一标识枚举。
维度建议
命名方式推荐使用业务语言命名,不加 Enum 后缀
示例LoanStatus 而非 LoanStatusEnum
原因保持领域语言一致性、隐藏实现细节、业务表达自然
例外与类型冲突、集成第三方工具时可保留 Enum

1.3. application层

1.3.1. application层拒绝XXXHandler、XXXProcessor、XXXContext等含义不明确的命名?

1.3.1.1. ❌ 为什么要避免这种模糊命名?
命名问题
OrderHandler“处理订单”具体是创建、取消、派送、结算还是别的?看不出来
LoanProcessor“处理贷款”是审批?风控?放款?也看不出来
UserContext是用户上下文对象?Session?请求参数?环境变量?不清晰
XXXManager管理什么?责任不清

这些命名是技术导向的,不利于业务沟通和代码可维护性。

1.3.1.2. ✅ 推荐的命名方式:动词 + 业务动作或职责清晰的命名

命名应能直接反映业务操作的意图,建议使用如下格式:

建议命名职责
SubmitLoanApplicationService提交贷款申请
CancelOrderService取消订单
ApproveLoanApplicationUseCase审批贷款申请
TransferFundsApplicationService发起资金转账
GenerateReportService生成报表

这些命名都表达了 清晰的业务行为,更符合 DDD 中“应用层协调领域服务”的职责。

1.3.1.3. ✅ 如何重构?
原名重命名建议
LoanProcessorLoanApprovalService/ SubmitLoanService
OrderHandlerCreateOrderService/ CancelOrderUseCase
UserContextCurrentUserInfo/ LoggedInUserInfo
1.3.1.4. 🧱 补充说明:不同于领域层、基础设施层
层级命名推荐不推荐
Domain 层LoanApplication/Order/UserLoanEntity / OrderDTO
Application 层ApproveLoanApplicationService/CancelOrderUseCaseLoanHandler / OrderProcessor
Infrastructure 层KafkaMessageConsumer/HttpClientAdapter可容忍 HandlerProcessor命名
命名类别推荐所在层说明
XXXHandlerInfrastructure 层 或 Application 层中实现类用于技术处理(如消息消费、HTTP 请求处理等)
XXXProcessorInfrastructure 层 或 Application 层中实现类用于组合多个行为、任务编排
XXXContext✅ 可用于跨调用传递上下文对象(如流程上下文),但不作为核心业务对象放在 Application 层或跨层共享模块
1.3.1.5. ✅ 总结
命名原则说明
❌ 避免 XXXHandler / XXXProcessor / XXXContext业务语义不明确
✅ 使用 动作+对象+用途符合统一语言
✅ 命名体现职责和行为方便业务沟通、代码自解释
❌ 不建议应用层泛化职责(如一个类什么都管)导致职责混乱、难以维护

1.3.2. 区分命令和查询,命令推荐KXXCommandService,查询推荐XXXQueryService?

你提到的这个命名方式和区分 命令(Command)查询(Query) 的设计,是现代 DDD(领域驱动设计)中非常推荐的一种 CQRS(Command Query Responsibility Segregation,命令查询职责分离) 实践。

1.3.2.1. ✅ 命名规则推荐
类型命名规范示例说明
命令类XXXCommandServiceXXXCommandAppService代表状态变更操作(有副作用)
查询类XXXQueryServiceXXXQueryAppService代表数据读取操作(无副作用)
1.3.2.2. 🧠 为什么这么命名?

CQRS 的核心思想是:将“读操作”和“写操作”分离成两个服务接口,职责清晰,便于演进、扩展和性能优化。

1.3.2.3. XXXCommandService
  • 只包含“写”操作:新增、更新、删除、触发业务行为等
  • 会调用 领域服务 / 聚合根
  • 会有事务控制
  • 会影响系统状态
public interface UserCommandService {
    void registerUser(RegisterUserCommand command);
    void updateUser(UpdateUserCommand command);
    void disableUser(String userId);
}
1.3.2.4. XXXQueryService
  • 只包含“读”操作:查询详情、列表、分页等
  • 不包含任何副作用
  • 可返回 DTO/VO
  • 可对接读库(或搜索引擎缓存等)
public interface UserQueryService {
    UserDetailDTO getUserById(String userId);
    List<UserDTO> listUsers(UserQuery query);
}
1.3.2.5. ✅ 优点总结
优点说明
职责清晰读写逻辑分离,不会混淆
可单独优化查询可以走缓存、ES、分库;命令可以做幂等性、事务保障
更易测试Query 无副作用;Command 只测试状态变更
支持复杂业务扩展比如后续支持 Event Sourcing、审计日志、写扩展性等
1.3.2.6. 🚫 反面示例(混用):
public class UserService {
    public void createUser(...) {...}      // 写
    public User getUserById(...) {...}     // 读
}

这种 Service 混合读写职责,后续很容易导致复杂度上升、耦合增加,不易演进。

1.3.2.7. 👇 实战建议
  1. 应用服务层(Application Service)就应该按照 Command / Query 分开设计
  2. Controller 层调用时清晰地知道是读请求还是写请求
  3. 命名约定保持一致:
UserCommandService / UserCommandAppService
UserQueryService / UserQueryAppService

4. 不需要为了“统一”而把 Command/Query 合并回一个 Service

1.4. infrastructure层

1.4.1. Repositoryl的入参和出参除了原始改据类型,只能包含领域对象?

Repository 的职责是访问“领域模型”的持久化存储,其输入输出应围绕“领域对象”展开,而不是直接处理 DTO(数据传输对象)或 PO(数据库实体对象)。

内容说明
✅ ****只能包含领域对象(Domain Object)Repository 是领域层的一部分,它的作用是将 领域对象保存/加载到持久化介质中 ,所以它操作的对象应该是领域对象(如实体、值对象)
✅ ****避免 PO(Persistence Object)PO 是数据库结构的映射,属于基础设施(infrastructure)层,而 Repository 是领域层的一部分,它不应直接操作数据库结构的对象
✅ ****避免 DTO(Data Transfer Object)DTO 是服务层或接口层的数据格式,通常用于与外部系统或前端交互,不属于领域模型,因此不能作为 Repository 的输入输出
1.4.1.1. ❌ 错误理解示例(违反规范):
// 错误:传入和返回的是 DTO 或 PO,而不是领域对象
UserDTO findById(Long id);
void save(UserPO userPo);
1.4.1.2. ✅ 正确设计示例:
// 正确:传入和返回的都是领域对象(Entity 或 ValueObject)
User findById(UserId id);  // 返回领域实体
void save(User user);      // 传入领域实体
1.4.1.3. 🎯 为什么这样设计?
原因说明
分层清晰明确职责边界,Repository 专注于领域模型的持久化,DTO/PO 属于别的层
降低耦合避免领域模型对数据库结构或外部接口耦合,增强模型稳定性和可演进性
保持统一语言领域对象使用的是统一语言建模,符合业务语义,PO/DTO 通常是技术导向结构

1.4.2. Repository对外交互拒绝DTO、PO?

Repository 对外交互拒绝 DTO、PO”,可以从 架构职责分层、解耦性、建模一致性 等多个角度来理解。

概念说明
Repository是 DDD 中领域层的一部分,负责对领域对象(Entity、Value Object)的持久化操作,如存储、加载
DTO(Data Transfer Object)用于服务层、应用层与外部系统(如接口调用、RPC、Web)之间的数据传输对象,不包含业务逻辑
PO(Persistence Object)通常是 ORM 框架(如 JPA、MyBatis)映射的数据库实体,紧耦合于数据库结构
1.4.2.1. ❌ 错误设计(违反规范)
// 错误:直接传 PO、DTO
public interface UserRepository {
    void save(UserPO userPo);       // 错:使用 PO
    UserDTO findById(Long id);      // 错:返回 DTO
}
1.4.2.2. ✅ 正确设计(遵守规范)
public interface UserRepository {
    void save(User user);           // 入参是领域对象
    User findById(UserId id);       // 返回领域对象
}
1.4.2.3. 📌 为什么要拒绝 DTO 和 PO?
原因说明
✅ 职责单一Repository 是领域层的一部分,职责是“存取领域模型”,不是处理数据库结构或 API 数据格式。
✅ 分层解耦DTO 是接口层/应用层对象,PO 是基础设施层对象,而 Repository 是领域层对象 —— 应层层隔离,不应交叉
✅ 保持建模一致性领域对象才具备业务语义,DTO 和 PO 都只是结构化数据,不具备行为和语义
✅ 便于演进若数据库字段或接口结构变化,只需修改 PO/DTO,不影响领域模型与 Repository 交互逻辑
1.4.2.4. 📌 那 Repository 和数据库是怎么交互的?

通过“转换器(Assembler/Converter) ”在基础设施层完成对象转换:

        +---------------------+
        |  Domain Repository  |   ← 输入输出:User(领域对象)
        +---------------------++-------------|------------------+
     |      Infrastructure 层        |
     |   UserRepositoryImpl.java     |
     |   UserPO ↔ User 转换器        |
     +-------------------------------++-----------------+
         |   数据库(PO)   |
         +-----------------+

示例:

@Override
public void save(User user) {
UserPO userPO = UserPOAssembler.toPO(user);
userMapper.insert(userPO);
}

1.4.3. 对外接口访问的防腐层,统一命名为XXXAdaptor?

对外接口访问的防腐层,统一命名为 XXXAdaptor

1.4.3.1. 什么是“防腐层”Anti-Corruption Layer(ACL)

在DDD中,防腐层的作用是:

  • 保护领域模型不被外部系统污染或侵蚀
  • 实现外部系统模型 → 自己系统领域模型的隔离和转换
  • 防止外部系统设计不佳、耦合度高、变化频繁影响你的系统

🧱 举个例子:你需要调用第三方风控系统,它返回的接口数据结构是ThirdPartyRiskResponse,但你不希望这个结构在你的领域模型里出现。这时你应该:

  • 定义一个RiskEngineAdaptor接口/实现
  • 将外部数据结构 ThirdPartyRiskResponse 转换为你自己的领域模型 RiskResult
1.4.3.2. 为什么命名为 XXXAdaptor

统一命名为 XXXAdaptor(或 Adapter)是为了:

  • 一眼识别出它是 适配外部系统的类
  • 它的作用是“适配 + 转换 + 解耦 + 防腐”
  • XXX 是被适配的系统名,如 RiskEngineAdaptor, CreditPlatformAdaptor, OpenApiAdaptor
1.4.3.3. Adaptor和领域的边界关系
       +------------------------+
       |     你的领域模型        |
       |      (干净、高内聚)     |
       +------------------------+
                   ↑
                   |  ← 防腐转换(Adaptor)
                   ↓
       +------------------------+
       | 外部系统(如三方接口)  |
       | 数据格式不一致,模型低质 |
       +------------------------+
1.4.3.4. 🧩 示例说明:

外部系统返回结构

@Data
public class ThirdPartyRiskResponse {
    private String code;
    private String message;
    private Map<String, String> data;
}

Adaptor 接口定义

public interface RiskEngineAdaptor {
    RiskResult query(RiskRequest request);
}

Adaptor 实现类(防腐层)

@Component
public class RiskEngineAdaptorImpl implements RiskEngineAdaptor {

    @Override
    public RiskResult query(RiskRequest request) {
        ThirdPartyRiskResponse response = thirdPartyClient.call(request);
        return RiskResultAssembler.toDomain(response);
    }
}

转换器(Assembler)

public class RiskResultAssembler {
    public static RiskResult toDomain(ThirdPartyRiskResponse response) {
        // 适配字段、格式、含义
        return new RiskResult(response.getCode(), response.getData().get("score"));
    }
}
1.4.3.5. 🚫 如果没有防腐层会怎样?

如果直接在 Service 中使用 ThirdPartyRiskResponse

  • 你的领域模型、服务层会大量出现外部结构 → 强耦合
  • 外部系统改了字段,你系统大范围受影响
  • 业务含义模糊,代码可读性差
  • 不利于测试、演进、重构

Adaptor 就是你的系统与外部世界之间的“防护墙”,统一命名为 XXXAdaptor 是为了职责清晰、结构分明、易于管理和维护。

1.4.4. 禁止外部接口对象向上层透传?

“禁止外部接口对象向上层透传”的核心目的是:不让外部结构入侵系统内部,保持业务领域的纯洁性和独立性。 外部接口返回的对象(如三方 API、RPC、数据库 PO、Web 请求参数 DTO 等)不能直接透传到系统内部,尤其是不能传入领域层或直接暴露给上层。

1.4.4.1. 📦 透传的反例(错误示范)

假设你调用一个外部授信平台,它返回一个 CreditResponseDTO,你直接在服务层或控制器里透传这个对象:

// ❌ 错误做法:把外部系统返回对象直接透传到上层接口
public CreditResponseDTO checkCredit(String userId) {
    return creditPlatformClient.query(userId);
}

问题:

  1. CreditResponseDTO 是外部定义的结构,字段命名、含义不一定稳定
  2. 一旦外部结构发生变动,你的整个服务层、接口层都需要改
  3. 你的业务逻辑会被迫使用外部系统的定义,严重耦合
1.4.4.2. ✅ 正确做法:引入 转换层(Assembler) 和 防腐层(Adaptor)
// 对外暴露领域对象或自定义 VO,而非外部结构
public CreditResult checkCredit(String userId) {
    CreditResponseDTO responseDTO = creditPlatformClient.query(userId);
    return CreditAssembler.toCreditResult(responseDTO); // 转换为内部对象
}
  • CreditResponseDTO 只在 AdaptorAssembler 层使用
  • CreditResult 是你自己定义的领域对象或 VO,用于业务逻辑或接口输出
  • 这样无论外部系统怎么变,只需改 Adapter/Assembler,不影响核心业务
1.4.4.3. 🎯 目的总结
原则说明
防腐外部系统不稳定,不可信,要设“隔离层”防污染
解耦内部系统演化应与外部系统解耦
可维护变化控制在边界,便于测试和演进
语义清晰自定义对象语义明确,更符合业务语言
1.4.4.4. 🧩 实战建议
类型示例是否允许透传?正确做法
第三方接口返回对象AliyunRiskResponse❌ 禁止透传转换为 RiskResult
数据库查询的 POUserPO❌ 禁止透传转换为 UserEntity
前端提交的请求体 DTOUserRegisterDTO❌ 禁止透传转换为 RegisterCommand
领域模型UserEntity✅ 允许传递按照聚合设计使用

1.5. 事件层

1.5.1. 事件命名为事件+Event,且事件命名为动词过去时 ?

1.5.1.1. 为什么事件命名要加Event后缀?
  • 明确类型Event 后缀能清晰表示这是一个“事件对象”,区别于命令(Command)、DTO、实体(Entity)等。
  • 增强可读性:看到类名带 Event,一目了然该对象是用于描述某个事件发生。
  • 方便维护:在代码库中快速定位事件相关代码,便于事件管理和监控。

示例:

UserRegisteredEvent
OrderCancelledEvent
PaymentSucceededEvent
1.5.1.2. 为什么事件名用动词过去式?
  • 表示已发生的事实:事件描述的是“某件事已经发生了”,所以用过去时更符合语义。
  • 符合事件驱动语义:事件是对“发生事实”的记录或通知,而不是命令或请求。
  • 区分命令和事件
    • 命令(Command)通常用动词原形或祈使句(如:CreateOrderCommand
    • 事件(Event)用动词过去式,表明动作已完成(如:OrderCreatedEvent
1.5.1.3. 结合起来的示例
类型命名示例语义说明
命令CreateOrderCommand请求创建订单(动作指令)
事件OrderCreatedEvent订单已被创建(已发生的事实)
事件PaymentSucceededEvent支付成功事件(动作完成的结果)
1.5.1.4. 总结
规范点理由
事件名后缀 Event明确事件类型,方便区分和维护
动词过去式命名事件是“已经发生的事实”,语义准确

2. DDD领域驱动实战思考

2.1. 你对DDD的理解是什么? DDD怎么样应用在实践系统中,怎么发挥DDD的价值?

2.2. 什么项目场景使用DDD ,为什么使用DDD,现有技术框架满足不了吗,能不能不使用DDD ? 使用DDD 之后带来的好处和弊端是什么?

2.3. 实践过程中DDD遇到问题、怎么解决这些问题的?

博文参考