新来的技术总监,把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真正落地。

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

最后

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

相关推荐
xcLeigh3 分钟前
HTML5好看的水果蔬菜在线商城网站源码系列模板8
java·前端·html5
Alsn8615 分钟前
11.Spring Boot 3.1.5 中使用 SpringDoc OpenAPI(替代 Swagger)生成 API 文档
java·spring boot·后端
liyongjun631622 分钟前
Java List分页工具
java·后端
猎人everest1 小时前
Spring Boot集成Spring Cloud 2024(不使用Feign)
java·spring boot·spring cloud
茂桑1 小时前
日常开发小Tips:后端返回带颜色的字段给前端
java·状态模式
佩奇的技术笔记1 小时前
Java学习手册:Spring 中常用的注解
java·spring
一键三联啊1 小时前
GC的查看
java·jvm·python
howard20052 小时前
项目三 - 任务2:创建笔记本电脑类(一爹多叔)
java·接口·继承·抽象类
药尘师2 小时前
低版的spring boot 1.X接入knife4j
java·spring boot·后端
淋过很多场雨2 小时前
现代c++获取linux所有的网络接口名称
java·linux·c++