摘要
本文主要介绍了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 {
@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
)由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); // 错:使用 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);
}
问题:
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. 总结
规范点 |
理由 |
事件名后缀 |
明确事件类型,方便区分和维护 |
动词过去式命名 |
事件是“已经发生的事实”,语义准确 |