当前位置: 首页 > news >正文

系统设计——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)确实应当尽量避免使用 GetterSetterConstructor 等 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;
}
  1. 🏗️ 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

interfaces

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)由 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. ✅ 如何重构?

原名

重命名建议

LoanProcessor

LoanApprovalService/ SubmitLoanService

OrderHandler

CreateOrderService/ CancelOrderUseCase

UserContext

CurrentUserInfo/ LoggedInUserInfo

1.3.1.4. 🧱 补充说明:不同于领域层、基础设施层

层级

命名推荐

不推荐

Domain 层

LoanApplication/Order/User

LoanEntity / OrderDTO

Application 层

ApproveLoanApplicationService

/CancelOrderUseCase

LoanHandler / OrderProcessor

Infrastructure 层

KafkaMessageConsumer/HttpClientAdapter

可容忍 HandlerProcessor命名

命名类别

推荐所在层

说明

XXXHandler

Infrastructure 层 或 Application 层中实现类

用于技术处理(如消息消费、HTTP 请求处理等)

XXXProcessor

Infrastructure 层 或 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
  1. 不需要为了“统一”而把 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);
}

问题:

  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

数据库查询的 PO

UserPO

❌ 禁止透传

转换为 UserEntity

前端提交的请求体 DTO

UserRegisterDTO

❌ 禁止透传

转换为 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遇到问题、怎么解决这些问题的?

博文参考

http://www.dtcms.com/a/331431.html

相关文章:

  • 深入浅出词向量(Word2Vec):从理论到实践
  • 数据结构初阶(13)排序算法-选择排序(选择排序、堆排序)(动图演示)
  • 【Java 后端】Spring Boot 集成 JPA 全攻略
  • HTTPS 工作原理
  • 电池充放电测试仪厂家:技术深耕与场景驱动的行业进阶
  • Java使用Apache POI读取Excel文件
  • Vue浅学
  • 深入解析 GitHub Actions 工作流文件编写:从入门到实战
  • 简单的 HTTPS 学习
  • 第四天-创建一个Classic CAN(经典CAN2.0)/CANFD的系统描述ARXML文件
  • 读From GPT-2 to gpt-oss: Analyzing the Architectural Advances
  • IPv6互联网地址解析
  • 从合规到卓越:全星QMS如何成为制造企业的质量战略引擎
  • linux 软硬链接详解
  • 《算法导论》第 25 章:所有结点对的最短路径问题
  • 计算机视觉CS231n学习(8)
  • 12 ABP Framework 租户管理
  • 介绍一下 自动驾驶 感知多任务训练模型设计
  • 面试题:如何用Flink实时计算QPS
  • 第4节 神经网络从公式简化到卷积神经网络(CNN)的进化之路
  • 第三章、GRU(门控循环网络,Gated Recurrent Unit)
  • redis中分布式锁的应用
  • 【js】让项目支持倾听和朗读AI技术
  • RTC时钟倒计时数码管同步显示实现(STC8)
  • AI模型选型:租快艇还是造航母?
  • 协作同步问题的深度分析与解决方案:结合表单汇总与AI技术提升运维效能
  • Git报错:Unlink of file ‘src/global/env.ts‘ failed. Should I try again? (y/n) y
  • AI对话框海量消息渲染优化:告别卡顿与跳动
  • 5.从零开始写LINUX内核--从实模式到保护模式的过渡实现
  • 嵌入式LINUX——————网络2