领域驱动设计:从三层架构到领域分层
把常见三层架构问题拆开,看清领域分层真正解决什么。
领域驱动设计:从三层架构到领域分层
前言
《领域驱动设计:软件核心复杂性应对之道》的作者是 Eric Evans。
以前我面对的多是简单需求,很难真正理解 DDD。最近开始重构复杂业务模块,再读这本书,感受完全不同:谜底就在谜面上,复杂性才是 DDD 的土壤。
成长路线
1) 面向过程编程
很多程序员第一阶段都从《C程序设计》开始。掌握 struct 与函数,通过函数组合实现复杂功能。
2) 面向对象编程
随后接触 C++、Java、Python,开始理解封装、继承、多态,学会用对象组织状态与行为。
3) 设计模式
再往后学习工厂、策略等模式,尝试把“模板化写法”用于实际业务。
4) SOLID 原则
只会招式还不够,SOLID 更像心法:让你知道什么时候该用什么设计。理解 SOLID 后,组合模式会更自然,代码质量也更稳定。
5) 模块设计
当你对代码细节足够熟悉后,下一步是抬高视角:关注模块边界、职责划分和协作方式,而不是继续堆砌实现。
6) 架构设计
这是更高一层的话题,本文先不展开。
为什么要做领域分层
核心目的只有一个:关注点分离。
让每一层只关注自己的职责,减少“牵一发而动全身”的连锁变更。
传统三层架构的问题
以“注册用户”为例,常见流程是:
- 检查入参
- 按手机号做实名认证
- 落库
- 如果是学生,发放新用户奖励
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:扩展困难
如果新增规则:
- 大学生发书包
- 高中生发试卷
那就只能继续在应用层堆 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; }}这个改造里有三个关键动作:
- 用值对象替代基础类型(
PhoneNumber、IdCard) - 为第三方调用加防腐层(隔离外部 DTO)
- 把“注册后动作”抽成事件(解耦奖励、积分等后续行为)
领域对象(Domain Entity)到底在解决什么
领域对象的核心不是“写成 class”,而是“保证对象始终处于合法状态”。
典型特征:
- 在内存中表达业务对象
- 构造完成即处于可用状态
- 可变行为受领域规则约束
一个常见反例
@Datapublic 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;}这个对象在构建过程中长期不完整,调用方很难知道它何时可用。
更稳妥的做法
@Getterpublic 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 也会引入额外建模成本,需要团队共同理解并持续维护。否则,分层只会停留在目录结构,系统复杂度反而更高。
