小白学习领域驱动设计

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代码。

相关推荐
vipbic8 分钟前
Strapi 5 怎么用才够爽?这款插件带你实现“建站自由”
后端·node.js
苏三的开发日记1 小时前
linux搭建hadoop服务
后端
sir7611 小时前
Redisson分布式锁实现原理
后端
大学生资源网1 小时前
基于springboot的万亩助农网站的设计与实现源代码(源码+文档)
java·spring boot·后端·mysql·毕业设计·源码
苏三的开发日记2 小时前
linux端进行kafka集群服务的搭建
后端
苏三的开发日记2 小时前
windows系统搭建kafka环境
后端
爬山算法2 小时前
Netty(19)Netty的性能优化手段有哪些?
java·后端
Tony Bai2 小时前
Cloudflare 2025 年度报告发布——Go 语言再次“屠榜”API 领域,AI 流量激增!
开发语言·人工智能·后端·golang
想用offer打牌2 小时前
虚拟内存与寻址方式解析(面试版)
java·后端·面试·系统架构
無量2 小时前
AQS抽象队列同步器原理与应用
后端