新来的技术总监,把DDD落地的那叫一个高级优雅!

前言

大家好,我是田螺.

我们在日常开发中,经常听到DDD,那么DDD到底是什么呢? 我之前也看过一些网络上的文章,都是写了一大堆的文字,比较羞涩难懂.本文打算跟大家聊聊DDD.让大家看清楚它的模样~

  • 公众号捡田螺的小男孩 (有田螺精心原创的面试PDF)
  • github地址,感谢每颗star:github

1. 什么是DDD

DDD(Domain-Driven Design,领域驱动设计)是一种通过聚焦业务领域来构建复杂系统的软件开发方法,核心思想是将代码结构与业务领域的实际需求深度结合

一句话简单概括就是:用代码还原业务本质,而非实现功能

  • 对于传统开发,就是对着PRD需求文档,写if-else(数据库怎么设计,代码就怎么写).
  • 而对于DDD,拉着业务方画领域模型 ,代码就是业务的镜子(业务怎么变,代码就怎么调

2. 传统的开发模式,一个简单的注册例子

其实这些概念上的东西,看完过会,还是很容易忘记对吧~ 我们来看一个代码例子吧~

假设我们做一个用户注册的例子,业务规则如下:

  • 用户名必须唯一
  • 密码必须满足复杂度要求
  • 注册后需记录日志

传统模式,于是可以快速写出以下代码:

typescript 复制代码
@Controller
public class UserController {
    public void register(String username, String password) {
        // 校验密码
        // 检查用户名
        // 保存数据库
        // 记录日志
        // 所有逻辑混在一起
    }
}

有些伙伴说,哪有所有代码混合在controller,肯定要分层的呀,比如分controller、service、dao层. 于是写出类似这样的代码:

typescript 复制代码
// Service层:仅有流程控制,业务规则散落在各处
public class UserService {
    public void register(User user) {
        // 校验规则1:写在工具类里
        ValidationUtil.checkPassword(user.getPassword()); 
        // 校验规则2:通过注解实现
        if (userRepository.exists(user)) { ... }
        // 数据直接传递到DAO
        userDao.save(user); 
    }
}

你还别说,这快代码,其实流程已经比较清晰了~ 有些伙伴,满怀激动地说,已经分层了,代码已经很优雅清晰了,这就是DDD了吧.

3. 分层就是DDD了吗?

答案是,NO!

以上代码虽然分层了,代码结构划分了,但是它还不是DDD.

其实对于传统的分层的那块代码,User对象仅是数据载体(贫血模型),业务逻辑被拆解到外部了.对于DDD,其实一些逻辑,可以内聚到领域User对象中的. 如密码规则的校验.

对于这个注册的例子, DDD的正确姿势(充血模型)如下:

arduino 复制代码
// 领域实体:业务逻辑内聚
public class User {
    public User(String username, String password) {
        // 密码规则内聚到构造函数
        if (!isValidPassword(password)) { 
            throw new InvalidPasswordException();
        }
        this.username = username;
        this.password = encrypt(password);
    }

    // 密码复杂度校验是实体的职责
    private boolean isValidPassword(String password) { ... }
}

其实把校验密码的下沉到User 领域实体对象里了.专业点说法,就是业务规则被封装在领域对象内部,对象不再只是"数据袋子"。

3. DDD的关键设计

所以,DDD 就是把一些逻辑下沉到领域对象中?

不全对~

其实处了分层,DDD的关键设计,体现在以下模式深化业务表达:

  • 聚合根
  • 领域服务 vs 应用服务
  • 领域事件

3.1 聚合根(Aggregate Root)

  • 场景:用户(User)和收货地址(Address)关联
  • 传统方式:在Service中分别管理User和Address
  • DDD方式:将User作为聚合根,控制Address的增删
csharp 复制代码
public class User {
    private List<Address> addresses;

    // 添加地址的逻辑由聚合根控制
    public void addAddress(Address address) {
        if (addresses.size() >= 5) {
            throw new AddressLimitExceededException();
        }
        addresses.add(address);
    }
}

3.2 领域服务 vs 应用服务

  • 领域服务:处理跨多个实体的业务逻辑(如转账涉及两个账户)
  • 应用服务:协调流程(如调用领域服务+发送消息)
typescript 复制代码
// 领域服务:处理核心业务逻辑
public class TransferService {
    public void transfer(Account from, Account to, Money amount) {
        from.debit(amount); // 账户扣款逻辑内聚在Account实体
        to.credit(amount);
    }
}

// 应用服务:编排流程,不包含业务规则
public class BankingAppService {
    public void executeTransfer(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepository.findById(fromId);
        Account to = accountRepository.findById(toId);
        transferService.transfer(from, to, new Money(amount));
        messageQueue.send(new TransferEvent(...)); // 基础设施操作
    

领域事件(Domain Events)

  • 用事件显式表达业务变化
  • 例:用户注册成功后触发UserRegisteredEvent
csharp 复制代码
public class User {
    public void register() {
        // ...注册逻辑
        this.events.add(new UserRegisteredEvent(this.id)); // 记录领域事件
    }
}

4. 传统开发和DDD的区别

简单总结一下传统开发和DDD的区别~

维度 传统开发 DDD
业务逻辑归属 散落在Service、Util、Controller 内聚在领域实体/领域服务
模型作用 数据载体(贫血模型) 携带行为的业务模型(充血模型)

| 技术实现影响 | 数据库表驱动设计 |业务需求驱动表结构设计 |

5. 电商下单的DDD例子

为了方便大家理解,再给大家来个DDD的案例.给大家解解渴、润润喉

假设有个需求:

用户下单需实现:校验库存、用优惠券、计算实付金额、生成订单。

  1. 传统写法(贫血模型)
scss 复制代码
/**
 * 
 * 公众号:捡田螺的小男孩
 **/
// Service层:大杂烩式下单
public class OrderService {
    @Autowired private InventoryDAO inventoryDAO;
    @Autowired private CouponDAO couponDAO;
    
    public Order createOrder(Long userId, List<ItemDTO> items, Long couponId) {
        // 1. 校验库存(散落在Service)
        for (ItemDTO item : items) {
            Integer stock = inventoryDAO.getStock(item.getSkuId());
            if (item.getQuantity() > stock) {
                throw new RuntimeException("库存不足");
            }
        }
        
        // 2. 计算总价
        BigDecimal total = items.stream()
                .map(i -> i.getPrice().multiply(i.getQuantity()))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        
        // 3. 应用优惠券(规则写在工具类)
        if (couponId != null) {
            Coupon coupon = couponDAO.getById(couponId);
            total = CouponUtil.applyCoupon(coupon, total); // 优惠逻辑隐藏在Util
        }
        
        // 4. 保存订单(纯数据操作)
        Order order = new Order();
        order.setUserId(userId);
        order.setTotalAmount(total);
        orderDAO.save(order);
        return order;
    }
}

传统方式存在的问题:

  • 库存校验、优惠计算散落在Service、Util、DAO
  • Order对象只是数据载体(贫血),业务规则无人认领
  • 改需求时,需在Service层"考古"
  1. DDD写法(充血模型):业务逻辑内聚到领域
scss 复制代码
/**
 * 
 * 更多干货,关注公众号:捡田螺的小男孩
 **/
// 聚合根:Order(承载核心逻辑)
public class Order {
    private List<OrderItem> items;
    private Coupon coupon;
    private Money totalAmount;

    // 构造函数内聚业务逻辑
    public Order(User user, List<OrderItem> items, Coupon coupon) {
        // 1. 校验库存(领域规则内聚)
        items.forEach(item -> item.checkStock());
        
        // 2. 计算总价(业务逻辑在值对象)
        this.totalAmount = items.stream()
                .map(OrderItem::subtotal)
                .reduce(Money.ZERO, Money::add);
        
        // 3. 应用优惠券(规则在实体内部)
        if (coupon != null) {
            validateCoupon(coupon, user); // 优惠券使用规则内聚
            this.totalAmount = coupon.applyDiscount(this.totalAmount);
        }
    }

    // 优惠券校验逻辑(业务归属清晰)
    private void validateCoupon(Coupon coupon, User user) {
        if (!coupon.isValid() || !coupon.isApplicable(user)) {
            throw new InvalidCouponException();
        }
    }
}

// 领域服务:协调下单流程
public class OrderService {
    public Order createOrder(User user, List<Item> items, Coupon coupon) {
        Order order = new Order(user, convertItems(items), coupon);
        orderRepository.save(order);
        domainEventPublisher.publish(new OrderCreatedEvent(order)); // 领域事件
        return order;
    }
}

改为DDD后的优点:

  • 库存校验:封装在OrderItem值对象中
  • 优惠券规则:内聚在Order实体内部方法
  • 计算逻辑:由Money值对象保证精度
  • 业务变化时,只改领域对象

假设产品又出了个新需求:优惠券需满足"订单满100减20",且仅限新用户使用。

传统开发的方式影响了Service层、Util类,因为需要修改

markdown 复制代码
1. 修改CouponUtil.applyCoupon()逻辑
2. 在Service层添加新用户校验

而DDD 只影响了领域层,因为只需要修改:

scss 复制代码
仅修改Order.validateCoupon()方法	

6. 什么场景该用DDD?

其实,是不是什么场景,都要使用DDD呢? 不是的,那就是小题大作啦~

  • ✅ 业务复杂(如电商、金融、ERP)
  • ✅ 需求频繁变更(90%的互联网业务)
  • ❌ 简单CRUD(管理后台、数据报表)

我觉得这句话有点道理:

当你发现修改业务规则时,只需调整领域层代码,而无需改动Controller或DAO,这才是DDD真正落地。

让代码和业务长成连体婴,改需求不再是程序员的噩梦 !!!

最后

一直坚持原创不易,大家给个三连支持一下哈. 如果你觉得本文有不对的地方,可以在评论区留言讨论哈~

相关推荐
最后一次遇见11 分钟前
weblog-module-admin(管理模块:后台的所有数据的物理逻辑都是在这里实现的)
后端
十分钟空间11 分钟前
Flask+Bootstrap高可用框架搭建方案
后端
代码吐槽菌14 分钟前
基于微信小程序的智慧乡村旅游服务平台【附源码】
java·开发语言·数据库·后端·微信小程序·小程序·毕业设计
界面开发小八哥15 分钟前
企业级Java开发工具MyEclipse v2025.1——支持AI编码辅助
java·ide·人工智能·myeclipse
阿里云云原生16 分钟前
为什么我需要AI助手辅助学习python
后端
AronTing19 分钟前
08-Java并发容器源码剖析:ConcurrentHashMap与CopyOnWriteArrayList
后端·面试
到账一个亿19 分钟前
【已经解决】java.security.InvalidAlgorithmParameterException: Prime size must be multi
后端
可问 可问春风36 分钟前
Java中的ArrayList方法
java
程序猿chen36 分钟前
JVM考古现场(十七):鸿蒙初辟——从太极二进到混沌原初的编译天道
开发语言·jvm·git·后端·程序人生·java-ee·改行学it
大苏打seven40 分钟前
Java学习笔记(多线程):ReentrantLock 源码分析
java·笔记·学习