系统设计——DDD领域模型驱动实践
摘要
本文主要介绍了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 自动生成的注解,这是出于“建模思想”与“封装业务规则”的考虑。以下是详细解释与建议。
- 🧠 破坏封装与建模思想:DDD 强调 通过代码表达领域模型的意图和业务规则。直接暴露 getter/setter,会让你的领域对象退化为一个贫血模型(Anemic Domain Model),只是一个数据容器,而不是业务的承载体。
// ❌ 错误写法:getter/setter + public 字段完全暴露
@Getter
@Setter
public class Order {private Long id;private String status;
}
❗这样写没有任何领域语义,Order 的状态可以随意被外部修改,违背领域封装原则。
- 🛡️ Setter 允许任意修改内部状态,打破一致性:领域对象的核心职责是:保障业务数据的完整性和一致性。Setter 让外部可以绕过业务规则,随意设置对象属性:
order.setStatus("已支付"); // 没有校验是否可以从“已取消”变成“已支付”
而正确的做法应是:
public void markAsPaid() {if (!canPay()) {throw new IllegalStateException("当前状态不可支付");}this.status = Status.PAID;
}
- 🏗️ 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 建议 |
| ❌ 只在读取聚合标识、只读字段时局部使用 |
| ❌ 禁止在领域对象中使用 |
| ❌ 不建议使用 |
| ❌ 避免(除非 ORM 必须) |
| ✅ 应包含业务校验逻辑 |
| ✅ 可用于构造复杂值对象 |
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注解,要小心:
- 说明你的领域模型很可能耦合了基础设施层逻辑(违背 DDD 分层)
- 可能导致业务逻辑难以复用或测试
层 | 说明 | 是否可用框架注解 |
| 纯领域逻辑模型、实体、聚合、值对象、领域服务 | ❌ 不依赖任何框架 |
| 数据持久化、消息中间件、缓存等实现细节 | ✅ 用 Spring 管理 |
| 调用编排、流程协调、事务管理等 | ✅ 用 Spring |
| Controller、API 接口层 | ✅ 用 Spring MVC 注解等 |
1.2.3.1. ❌ 不推荐(污染领域模型)
@Entity
@Getter
@Setter
public class LoanApplication {@Idprivate Long id;@Autowiredprivate 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
)由DomainService
或ApplicationService
在外部注入,传参给领域对象,不在对象中注入依赖。
1.2.3.3. 🧪 好处:
- ✅ 易于单元测试:构造纯对象即可测试,不依赖 Spring 环境。
- ✅ 解耦框架:更容易迁移、更少“技术污染”。
- ✅ 聚焦业务:领域对象只关心业务含义,职责清晰。
1.2.4. Domain对象行为拒绝setter、update、modify、save、delete等无明确业务含义的方法?
这是 DDD(领域驱动设计)中对 Domain 对象(即领域模型) 的一种强烈编码规范:领域对象的方法必须具备明确的业务含义。这些方法通常只表示技术操作,而没有任何具体的业务语义,违背了 DDD 中“领域模型体现业务行为”的基本理念。
错误方式(技术性方法) | 正确方式(业务性方法) |
|
|
|
|
|
|
|
|
1.2.4.1. 领域模型是业务专家的语言映射
- setter/update/save 等是 面向 ORM 和数据库 的语言。
- 而
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;// 记录拒绝原因,可能还要写审计日志}
}
方法名 | 是否允许 | 说明 |
| ❌ | 除非是纯值对象,如 DTO、配置类 |
| ❌ | 太笼统,建议改为具体业务操作 |
| ✅ | 有明确业务语义 |
| ✅ | 计算、验证行为是业务的一部分 |
| ✅(只读转换) | 可接受,但可以考虑放到 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 建议隐藏实现细节、突出业务意图。值对象本身就是“一个不可变、具备自我完整性的业务值”,不管你是用 enum
、class
、record
实现的,业务只需要知道这是 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),用于统一标识枚举。
维度 | 建议 |
命名方式 | 推荐使用业务语言命名,不加 |
示例 |
|
原因 | 保持领域语言一致性、隐藏实现细节、业务表达自然 |
例外 | 与类型冲突、集成第三方工具时可保留 |
1.3. application层
1.3.1. application层拒绝XXXHandler、XXXProcessor、XXXContext等含义不明确的命名?
1.3.1.1. ❌ 为什么要避免这种模糊命名?
命名 | 问题 |
| “处理订单”具体是创建、取消、派送、结算还是别的?看不出来 |
| “处理贷款”是审批?风控?放款?也看不出来 |
| 是用户上下文对象?Session?请求参数?环境变量?不清晰 |
| 管理什么?责任不清 |
这些命名是技术导向的,不利于业务沟通和代码可维护性。
1.3.1.2. ✅ 推荐的命名方式:动词 + 业务动作或职责清晰的命名
命名应能直接反映业务操作的意图,建议使用如下格式:
建议命名 | 职责 |
| 提交贷款申请 |
| 取消订单 |
| 审批贷款申请 |
| 发起资金转账 |
| 生成报表 |
这些命名都表达了 清晰的业务行为,更符合 DDD 中“应用层协调领域服务”的职责。
1.3.1.3. ✅ 如何重构?
原名 | 重命名建议 |
|
|
|
|
|
|
1.3.1.4. 🧱 补充说明:不同于领域层、基础设施层
层级 | 命名推荐 | 不推荐 |
Domain 层 |
|
|
Application 层 |
/ |
|
Infrastructure 层 |
| 可容忍 |
命名类别 | 推荐所在层 | 说明 |
| ✅ Infrastructure 层 或 Application 层中实现类 | 用于技术处理(如消息消费、HTTP 请求处理等) |
| ✅ Infrastructure 层 或 Application 层中实现类 | 用于组合多个行为、任务编排 |
| ✅ 可用于跨调用传递上下文对象(如流程上下文),但不作为核心业务对象 | 放在 Application 层或跨层共享模块 |
1.3.1.5. ✅ 总结
命名原则 | 说明 |
❌ 避免 | 业务语义不明确 |
✅ 使用 | 符合统一语言 |
✅ 命名体现职责和行为 | 方便业务沟通、代码自解释 |
❌ 不建议应用层泛化职责(如一个类什么都管) | 导致职责混乱、难以维护 |
1.3.2. 区分命令和查询,命令推荐KXXCommandService,查询推荐XXXQueryService?
你提到的这个命名方式和区分 命令(Command) 与 查询(Query) 的设计,是现代 DDD(领域驱动设计)中非常推荐的一种 CQRS(Command Query Responsibility Segregation,命令查询职责分离) 实践。
1.3.2.1. ✅ 命名规则推荐
类型 | 命名规范示例 | 说明 |
命令类 |
| 代表状态变更操作(有副作用) |
查询类 |
| 代表数据读取操作(无副作用) |
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. 👇 实战建议
- 应用服务层(Application Service)就应该按照 Command / Query 分开设计
- Controller 层调用时清晰地知道是读请求还是写请求
- 命名约定保持一致:
UserCommandService / UserCommandAppService
UserQueryService / UserQueryAppService
- 不需要为了“统一”而把 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); // 错:使用 POUserDTO 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 {@Overridepublic 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);
}
问题:
CreditResponseDTO
是外部定义的结构,字段命名、含义不一定稳定- 一旦外部结构发生变动,你的整个服务层、接口层都需要改
- 你的业务逻辑会被迫使用外部系统的定义,严重耦合
1.4.4.2. ✅ 正确做法:引入 转换层(Assembler) 和 防腐层(Adaptor)
// 对外暴露领域对象或自定义 VO,而非外部结构
public CreditResult checkCredit(String userId) {CreditResponseDTO responseDTO = creditPlatformClient.query(userId);return CreditAssembler.toCreditResult(responseDTO); // 转换为内部对象
}
CreditResponseDTO
只在Adaptor
或Assembler
层使用CreditResult
是你自己定义的领域对象或 VO,用于业务逻辑或接口输出- 这样无论外部系统怎么变,只需改 Adapter/Assembler,不影响核心业务
1.4.4.3. 🎯 目的总结
原则 | 说明 |
防腐 | 外部系统不稳定,不可信,要设“隔离层”防污染 |
解耦 | 内部系统演化应与外部系统解耦 |
可维护 | 变化控制在边界,便于测试和演进 |
语义清晰 | 自定义对象语义明确,更符合业务语言 |
1.4.4.4. 🧩 实战建议
类型 | 示例 | 是否允许透传? | 正确做法 |
第三方接口返回对象 |
| ❌ 禁止透传 | 转换为 |
数据库查询的 PO |
| ❌ 禁止透传 | 转换为 |
前端提交的请求体 DTO |
| ❌ 禁止透传 | 转换为 |
领域模型 |
| ✅ 允许传递 | 按照聚合设计使用 |
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
)
- 命令(Command)通常用动词原形或祈使句(如:
1.5.1.3. 结合起来的示例
类型 | 命名示例 | 语义说明 |
命令 |
| 请求创建订单(动作指令) |
事件 |
| 订单已被创建(已发生的事实) |
事件 |
| 支付成功事件(动作完成的结果) |
1.5.1.4. 总结
规范点 | 理由 |
事件名后缀 | 明确事件类型,方便区分和维护 |
动词过去式命名 | 事件是“已经发生的事实”,语义准确 |