DDD(领域驱动设计)是一种针对复杂系统进行定制化设计的指导思想,适用于组件多、业务流程复杂、需要长期维护的系统。它提倡所有相关人员共同参与系统生命周期,以减少沟通成本。系统被划分为核心领域(公司的核心业务)、通用领域(如日志、安全,可采购或二次开发)和支持型领域(特定行业的辅助系统)。通过这种方式,DDD帮助分解和管理复杂性。
一、核心思想:我们为什么要用DDD?
想象一下你们现在的Java项目,是不是有很多这样的代码?
- "胖"Service: 一个
UserService
里有注册、登录、改密码、查信息、推荐好友......所有方法都塞在一起,几千行代码。 - "贫血"Model:
User
类基本上就是一堆getter/setter,没有任何业务逻辑。业务逻辑全散落在各种Service里。 - "流水账"代码: 一个业务方法里,就是一行行的
dao.insert()
,redis.set()
,rpc.call()
,mq.send()
,像记流水账一样。读代码时,根本看不出业务的核心规则是什么。
DDD的核心目标就是解决这些问题! 它不是一套全新的技术框架,而是一套设计思想,教你怎么更好地组织代码,让代码:
- 更清晰: 业务意图明确,一读就懂。
- 更健壮: 业务规则被牢牢守住,不会写出错误的数据。
- 更好维护: 需求变更时,知道代码该改哪里,不会牵一发而动全身。
二、给Java开发的"DDD降维学习指南"(重点技巧篇)
你不用一次性掌握所有DDD概念。可以从下面这几个最实用、最易上手的技巧开始,它们都来自你提到的文章。
技巧一:告别Set/Get,使用 Domain Primitive (DP) - 【第一讲核心】
这是最重要、最推荐优先使用的技巧,能立竿见影地提升代码质量。
-
是什么? DP是一个"小而美"的值对象,代表你业务领域中最基础、最原始的概念。
-
旧习惯(问题很大):
java
typescript
// ❌ 坏味道:所有校验逻辑都散落在Service方法里
public void createUser(String name, String phoneNumber, String address) {
if (name == null || name.length() > 20) {
throw new IllegalArgumentException("name invalid");
}
if (phoneNumber == null || !isValidPhone(phoneNumber)) { // 需要自己写校验逻辑
throw new IllegalArgumentException("phone invalid");
}
// ... 一堆set操作
User user = new User();
user.setName(name);
user.setPhoneNumber(phoneNumber); // String类型太宽泛,可能被误设为地址
// ...
}
-
新技巧(使用DP):
java
typescript
// ✅ 好代码:业务规则封装在DP内部
public class PhoneNumber {
private final String number; // final, immutable(不可变)的!
public PhoneNumber(String number) {
// 构造即有效!创建对象时完成所有校验
if (number == null || !isValid(number)) {
throw new IllegalArgumentException("手机号格式错误");
}
this.number = number;
}
public String getNumber() { return number; }
// 可以有自己的行为方法!
public String getAreaCode() { return number.substring(0, 3); }
}
public class UserName { ... } // 同理
// Service方法变得极其清爽、安全
public void createUser(UserName name, PhoneNumber phone, Address address) {
// 不需要再校验参数了!因为能传进来的PhoneNumber等对象一定是合法的
User user = new User(name, phone, address); // 通过构造器或工厂创建
userRepository.save(user);
}
-
你的收获:
- 杜绝垃圾数据: 业务规则(校验逻辑)被封装在DP内部,只要对象存在,数据就是有效的。
- 代码即文档: 方法签名
createUser(UserName, PhoneNumber, Address)
比createUser(String, String, String)
清晰一万倍。 - 避免意外: 因为有final和私有字段,避免了被其他地方乱set的风险。
行动指南: 找出你项目中的String
/int
/long
参数,尤其是需要校验的(如用户ID、手机号、金额、订单号等),尝试把它们封装成DP类。
技巧二:告别"上帝Service",使用领域对象(Entity)封装行为 - 【第四、五讲核心】
不要把所有操作都放到Service里,而是让你的领域对象(如Order
, User
)"活"起来。
-
旧习惯(贫血模型):
java
scss
// ❌ 坏味道:User是个"哑巴",业务逻辑全在Service里
@Service
public class UserService {
public void changeUserPassword(Long userId, String oldPass, String newPass) {
User user = userRepository.findById(userId);
// 校验旧密码是否正确
if (!user.getPassword().equals(encrypt(oldPass))) {
throw new Exception("旧密码错误");
}
// 设置新密码
user.setPassword(encrypt(newPass));
userRepository.update(user);
// 发送密码修改通知邮件
emailService.sendPasswordChangeAlert(user.getEmail());
}
}
-
新技巧(富血模型):
java
typescript
// ✅ 好代码:行为封装在User实体内部
@Entity
public class User {
private UserId userId;
private Password password;
private Email email;
// ...
// 一个富含业务意义的方法
public void changePassword(Password oldPass, Password newPass, PasswordEncoder encoder, EmailService emailService) {
// 规则1:旧密码必须匹配
if (!this.password.equals(encoder.encode(oldPass))) {
throw new BusinessException("旧密码错误");
}
// 规则2:新密码不能和旧密码一样
if (this.password.equals(encoder.encode(newPass))) {
throw new BusinessException("新密码不能与旧密码相同");
}
// 执行变更
this.password = encoder.encode(newPass);
// 触发后续领域事件
DomainEventPublisher.publish(new UserPasswordChangedEvent(this.userId, this.email));
}
}
// Service变得很薄,只是协调者和"脚手架"
@Service
@Transactional
public class UserService {
public void changeUserPassword(UserId userId, Password oldPass, Password newPass) {
User user = userRepository.findById(userId);
// 核心业务逻辑委托给领域对象
user.changePassword(oldPass, newPass, passwordEncoder, emailService);
userRepository.save(user); // 保存变更
}
}
-
你的收获:
- 高内聚: 修改密码的所有规则都集中在
User
类里,而不是散落在各处。 - 可测试: 单独测试
user.changePassword(...)
方法非常容易,不需要复杂的Spring环境。 - Service变轻: Service不再关心"怎么改",只关心"找谁改"和"改完后存盘"。
- 高内聚: 修改密码的所有规则都集中在
行动指南: 审视你的Service类,找一个比较独立的方法,把其中的if/else
校验逻辑和设置状态的逻辑,尝试挪到对应的Entity对象中去,并给它起一个业务方法名。
技巧三:用Repository模式抽象持久化 - 【第三讲核心】
-
是什么? 定义一个清晰的接口,来说明你能怎么存/取某个聚合根(可以简单理解为你的核心Entity),而不关心底层是用MyBatis、JPA还是Redis。
-
好处:
- 持久化细节隔离: 业务代码不关心数据库实现,方便替换(比如从MySQL换到OceanBase)。
- 测试友好: 业务层代码测试时,可以很容易Mock一个
UserRepository
。
-
示例:
java
java
// 在领域层定义接口(核心!)
public interface UserRepository {
User findById(UserId id);
User findByPhoneNumber(PhoneNumber phone);
void save(User user);
void delete(UserId id);
}
// 在基础设施层(如infra模块)用MyBatis/JPA实现这个接口
@Repository
public class UserRepositoryMyBatisImpl implements UserRepository {
@Autowired
private UserMapper userMapper; // MyBatis的Mapper
@Override
public User findById(UserId id) {
UserDO userDO = userMapper.selectById(id.getValue());
return userDataConverter.toEntity(userDO); // 数据对象转领域实体
}
// ... 其他实现
}
行动指南: 为你的核心Entity定义一个清晰的Repository接口,将现有的DAO/Mapper注入到其实现类中。
三、整体架构是什么样的?- 【第二讲核心】
当你用了以上技巧后,你的代码结构会自然演进成下图的样子,这就是DDD推崇的分层架构:
-
用户接口层 (Interface Layer): 对外提供各种接口(HTTP API, RPC, 消息监听等)。它只负责接收请求、解析参数、调用应用服务、返回结果。
-
应用层 (Application Layer): 包含应用服务(就是你上面看到的变薄了的Service)。它不包含核心业务规则,只负责:
- 事务控制(
@Transactional
) - 权限校验
- 调用领域层的能力来编排业务流程(像导演指挥演员)
- 发送邮件、消息等跨领域协作
- 事务控制(
-
领域层 (Domain Layer): 最核心的一层。包含:
- Entity (实体): 有唯一ID,有生命周期,有业务行为的对象(如
User
,Order
)。 - Domain Primitive (DP): 无ID的值对象,是最基础的构建块(如
UserId
,Money
,Address
)。 - Domain Service (领域服务): 当一个行为不适合放在任何一个Entity内时(如"资金转账"涉及两个账户),放在这里。
- Repository Interface (仓储接口): 定义怎么存取实体,实现不在这一层。
- Entity (实体): 有唯一ID,有生命周期,有业务行为的对象(如
-
基础设施层 (Infrastructure Layer): 最底层。实现领域层定义的接口(如
RepositoryImpl
),提供技术细节:数据库(MyBatis)、缓存(Redis)、RPC、消息队列等。
总结与行动建议
别想着一次性把DDD所有概念都用上,那会把你吓跑。从最小的改变开始,收获最大化的效果:
- 第一步(本周就能做): 找一个新的或修改中的业务方法,尝试为它的参数创建1-2个
Domain Primitive
(如UserId
,OrderId
)。体验"构造即有效"和代码清晰度的提升。 - 第二步(下个月目标): 找一个"贫血"的
Entity
,给它添加一个富有业务含义的方法(比如order.cancel(Reason)
),把原来Service里的相关逻辑搬进去。 - 第三步(长期重构): 在修改老代码或写新代码时,有意识地思考:"这个逻辑属于哪个对象?"。是属于
Service
(流程编排),还是属于Entity
(自身行为)?
DDD不是一个严格的规范,而是一套让代码更好地映射现实业务的思维工具。通过这些文章和本总结,希望你能够理解其精髓,并开始在实践中尝试,写出更优雅、更健壮的Java代码。