← Blog

领域驱动设计:从三层架构到领域分层

把常见三层架构问题拆开,看清领域分层真正解决什么。

System Design

领域驱动设计:从三层架构到领域分层

前言

《领域驱动设计:软件核心复杂性应对之道》的作者是 Eric Evans。

以前我面对的多是简单需求,很难真正理解 DDD。最近开始重构复杂业务模块,再读这本书,感受完全不同:谜底就在谜面上,复杂性才是 DDD 的土壤

成长路线

1) 面向过程编程

很多程序员第一阶段都从《C程序设计》开始。掌握 struct 与函数,通过函数组合实现复杂功能。

2) 面向对象编程

随后接触 C++、Java、Python,开始理解封装、继承、多态,学会用对象组织状态与行为。

3) 设计模式

再往后学习工厂、策略等模式,尝试把“模板化写法”用于实际业务。

4) SOLID 原则

只会招式还不够,SOLID 更像心法:让你知道什么时候该用什么设计。理解 SOLID 后,组合模式会更自然,代码质量也更稳定。

5) 模块设计

当你对代码细节足够熟悉后,下一步是抬高视角:关注模块边界、职责划分和协作方式,而不是继续堆砌实现。

6) 架构设计

这是更高一层的话题,本文先不展开。


为什么要做领域分层

核心目的只有一个:关注点分离

让每一层只关注自己的职责,减少“牵一发而动全身”的连锁变更。

传统三层架构的问题

以“注册用户”为例,常见流程是:

  1. 检查入参
  2. 按手机号做实名认证
  3. 落库
  4. 如果是学生,发放新用户奖励
public class UserServiceBadExample {
private UserDAO userDAO;
private TelecomService telecomService;
private RewardService rewardService;
private void validateIdCard(String idCard) {
if (idCard == null || idCard.length() != 18) {
throw new IllegalArgumentException("身份证号码长度不正确");
}
for (int i = 0; i < 17; i++) {
if (!Character.isDigit(idCard.charAt(i))) {
throw new IllegalArgumentException("身份证号码中包含非数字字符");
}
}
}
private void validatePhoneNumber(String phoneNumber) {
if (phoneNumber == null || !StringUtils.isNumeric(phoneNumber)) {
throw new IllegalArgumentException("手机号码格式不正确");
}
if (phoneNumber.length() != 11) {
throw new IllegalArgumentException("手机号码长度不正确");
}
}
public void registerUser(RegisterUserRequest request) {
validateIdCard(request.getIdCard());
validatePhoneNumber(request.getPhoneNumber());
RealNameInfoDTO realNameInfoDTO = telecomService.getRealNameInfo(request.getPhoneNumber());
if(realNameInfoDTO == null){
throw new IllegalArgumentException("实名信息不存在");
}
if(!StringUtils.equals(realNameInfoDTO.getIdCard(), request.getIdCard())){
throw new IllegalArgumentException("实名信息不正确");
}
String userLabel = getUserLabel(request);
UserPO userPO = new UserPO();
userPO.setUserName(request.getUserName());
userPO.setPassword(encrypt(request.getPassword()));
userPO.setIdCard(request.getIdCard());
userPO.setLabel(userLabel);
if (StringUtils.equals(userLabel, "student")) {
RewardDTO rewardDTO = new RewardDTO();
rewardDTO.setUserId(userPO.getId());
rewardDTO.setRewardName("apple phone");
rewardService.sendAward(rewardDTO);
}
userDAO.save(userPO);
}
private String encrypt(String password) {
return password + "123";
}
private String getUserLabel(RegisterUserRequest request) {
return "";
}
}

优点

  1. 开发快
  2. 逻辑直观

在项目初期,这种方式常常很高效,确实符合“先快后优”的现实。

问题 1:应用逻辑与业务逻辑耦合

像“标签计算”“实名认证规则”“新用户判定”其实都是业务规则,不应该全部堆在应用层。

应用层应该更关注:

  1. 接收请求与参数编排
  2. 调度领域对象完成业务

问题 2:扩展困难

如果新增规则:

  • 大学生发书包
  • 高中生发试卷

那就只能继续在应用层堆 if/else。随着业务演进,应用层会越来越重,最终变成“头重脚轻”。

问题 3:外部依赖侵入严重

应用层直接依赖 telecomService 的 DTO 与调用方式,一旦三方接口变化,应用层被迫频繁变更,单测和维护成本都升高。


DDD 四层架构:先拆应用层

DDD 落地的第一步,通常是把传统应用层拆成:应用层 + 领域层

如何理解两层职责

领域层:承载业务规则与业务一致性。

  • 例如:小于 12 岁不能注册
  • 根据标签发放新客权益

应用层:负责编排流程。

  • 获取入参
  • 调用领域服务
  • 持久化与发消息
public class UserService {
private IUserRepository userRepository;
private IThirdIdentificationService thirdIdentificationService;
private UserEventHandler userEventHandler;
@Autowired
public UserService(IUserRepository userRepository) {
this.userRepository = userRepository;
}
public boolean registerUser(RegisterUserRequest request) {
PhoneNumber phoneNumber = new PhoneNumber(request.getPhoneNumber());
IdCard idCard = new IdCard(request.getIdCard());
RealNameInfo realNameInfo = thirdIdentificationService.getRealNameInfo(phoneNumber);
realNameInfo.check(idCard);
UserEntity userEntity = Builder.newBuilder()
.idCard(realNameInfo.getIdCard())
.phone(realNameInfo.getPhoneNumber())
.realName(realNameInfo.getRealName())
.userName(request.getUserName())
.password(request.getPassword())
.build();
userRepository.save(userEntity);
userEventHandler.publish("registerUser");
return true;
}
}

这个改造里有三个关键动作:

  1. 用值对象替代基础类型PhoneNumberIdCard
  2. 为第三方调用加防腐层(隔离外部 DTO)
  3. 把“注册后动作”抽成事件(解耦奖励、积分等后续行为)

领域对象(Domain Entity)到底在解决什么

领域对象的核心不是“写成 class”,而是“保证对象始终处于合法状态”。

典型特征:

  1. 在内存中表达业务对象
  2. 构造完成即处于可用状态
  3. 可变行为受领域规则约束

一个常见反例

@Data
public class Student{
public Student(){}
private String name;
private String sex;
private Long studentId;
private String phoneNumber;
}
public Student createStudent(){
Student stu = new Student();
processMethod1(stu);
processMethod2(stu);
processMethod3(stu);
return stu;
}

这个对象在构建过程中长期不完整,调用方很难知道它何时可用。

更稳妥的做法

@Getter
public class Student{
public Student(String name, String sex, Long studentId, String phoneNumber){
this.name = name;
this.sex = sex;
this.studentId = studentId;
this.phoneNumber = phoneNumber;
}
private String name;
private String sex;
private Long studentId;
private String phoneNumber;
public void setPhoneNumber(String phoneNumber){
if(! isDigit(phoneNumber)){
throw new RuntimeException("phoneNumber must be digit");
}
this.phoneNumber = phoneNumber;
}
public boolean hasPhoneNumber(){
return StringUtils.isNotBlank(phoneNumber);
}
}

如果字段很多,可以用 Builder 提升可读性,但原则不变:对象在对外可见前,必须先满足业务约束


DDD 落地时的一个现实建议

DDD 不是让所有东西都“面向对象到底”。

在很多真实系统里:

  • **Command(增删改)**复杂、规则多,适合领域模型保证正确性
  • **Query(查)**常常是读模型直出,过度对象化会徒增成本

换句话说,重点不是“教条分层”,而是在复杂度高的地方引入模型约束,在简单读路径上保持务实


写在最后

DDD 不是银弹。

它不是消除依赖,而是转移依赖、隔离变化。通过应用层与领域层的职责切分,尽量减少外部变更对核心业务规则的侵入。

但 DDD 也会引入额外建模成本,需要团队共同理解并持续维护。否则,分层只会停留在目录结构,系统复杂度反而更高。