小白学习领域驱动设计

DDD(领域驱动设计)是一种针对复杂系统进行定制化设计的指导思想,适用于组件多、业务流程复杂、需要长期维护的系统。它提倡所有相关人员共同参与系统生命周期,以减少沟通成本。系统被划分为核心领域(公司的核心业务)、通用领域(如日志、安全,可采购或二次开发)和支持型领域(特定行业的辅助系统)。通过这种方式,DDD帮助分解和管理复杂性。

一、核心思想:我们为什么要用DDD?

想象一下你们现在的Java项目,是不是有很多这样的代码?

  1. "胖"Service: 一个UserService里有注册、登录、改密码、查信息、推荐好友......所有方法都塞在一起,几千行代码。
  2. "贫血"Model: User类基本上就是一堆getter/setter,没有任何业务逻辑。业务逻辑全散落在各种Service里。
  3. "流水账"代码: 一个业务方法里,就是一行行的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 (仓储接口): 定义怎么存取实体,实现不在这一层
  • 基础设施层 (Infrastructure Layer): 最底层。实现领域层定义的接口(如RepositoryImpl),提供技术细节:数据库(MyBatis)、缓存(Redis)、RPC、消息队列等。

总结与行动建议

别想着一次性把DDD所有概念都用上,那会把你吓跑。从最小的改变开始,收获最大化的效果

  1. 第一步(本周就能做): 找一个新的或修改中的业务方法,尝试为它的参数创建1-2个Domain Primitive(如UserId, OrderId)。体验"构造即有效"和代码清晰度的提升。
  2. 第二步(下个月目标): 找一个"贫血"的Entity,给它添加一个富有业务含义的方法(比如order.cancel(Reason)),把原来Service里的相关逻辑搬进去。
  3. 第三步(长期重构): 在修改老代码或写新代码时,有意识地思考:"这个逻辑属于哪个对象?"。是属于Service(流程编排),还是属于Entity(自身行为)?

DDD不是一个严格的规范,而是一套让代码更好地映射现实业务的思维工具。通过这些文章和本总结,希望你能够理解其精髓,并开始在实践中尝试,写出更优雅、更健壮的Java代码。

相关推荐
双向3310 小时前
MySQL 慢查询 debug:索引没生效的三重陷阱
后端
XPJ10 小时前
消息中间件入门到精通
后端
XPJ10 小时前
何为里氏替换原则(LSP)
后端
Cache技术分享10 小时前
177. Java 注释 - 重复注释
前端·后端
用户67570498850211 小时前
Git合并选Rebase还是Merge?弄懂这3点,从此不再纠结
后端
码事漫谈11 小时前
现代C++性能陷阱:std::function的成本、异常处理的真实开销
后端
那些无名之辈11 小时前
03 docker搭建
后端
Swot11 小时前
Nuxt3 服务端调用其他 api 的方式
后端
SimonKing11 小时前
Chrome插件千万别乱装!手把手教你从官方渠道安全下载
java·后端·程序员