DDD是什么?如何正确使用DDD(附代码示例)

后端 潘老师 2周前 (04-07) 19 ℃ (0) 扫码查看

我们常常会听到领域驱动设计(DDD,Domain-Driven Design )这个术语,但很多人对它的理解还停留在表面,网络上的相关文章大多晦涩难懂。今天,咱们就深入浅出地聊聊DDD,看看它究竟是什么,以及如何在实际项目中应用。

一、DDD究竟是什么?

简单来说,DDD是一种构建复杂系统的软件开发方法,它的核心在于将代码结构与业务领域的实际需求紧密结合。和传统开发对着需求文档写代码不同,DDD强调拉着业务方一起绘制领域模型,让代码能够精准地反映业务本质。用大白话讲,就是用代码还原业务的真实情况,而不只是单纯实现功能。

二、从用户注册案例看传统开发模式

为了让大家更好地理解,我们以一个简单的用户注册功能为例。在这个案例中,业务规则规定用户名必须唯一,密码要满足复杂度要求,注册成功后还得记录日志。

在传统开发模式下,可能会写出这样的代码:

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

有些开发者可能会说,代码肯定不能这么写,得进行分层,比如分成controller、service、dao层。于是,代码就变成了这样:

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

虽然代码进行了分层,流程看起来也清晰了不少,但这并不意味着就是DDD。在这段代码里,User对象只是用来承载数据的“贫血模型”,业务逻辑被分散到了各个外部模块。

三、DDD的正确打开方式——以用户注册为例

在DDD中,一些业务逻辑会被内聚到领域对象中。还是以用户注册为例,密码规则的校验就可以放到User对象里。用专业的话来说,就是业务规则被封装在领域对象内部,对象不再仅仅是“数据袋子” ,而是“充血模型”。具体代码如下:

// 领域实体:业务逻辑内聚
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领域实体对象中,这就是DDD和传统开发的一个重要区别。

四、DDD的关键设计要素

(一)聚合根(Aggregate Root)

在实际业务场景中,比如用户(User)和收货地址(Address)有关联的情况。按照传统方式,会在Service中分别管理User和Address。而在DDD里,会将User作为聚合根,由它来控制Address的增删操作。代码示例如下:

public class User {
    private List<Address> addresses;

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

通过这种方式,业务逻辑更加集中,管理也更加方便。

(二)领域服务与应用服务

  1. 领域服务:主要处理跨多个实体的业务逻辑。比如说转账操作,涉及到两个账户,这种核心业务逻辑就由领域服务来处理。
  2. 应用服务:负责协调流程,比如调用领域服务,再加上发送消息等操作。具体代码示例如下:
// 领域服务:处理核心业务逻辑
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事件。代码示例如下:

public class User {
    public void register() {
        // ...注册逻辑
        this.events.add(new UserRegisteredEvent(this.id)); // 记录领域事件
    }
}

这样,当业务发生变化时,可以通过这些事件进行后续的处理。

五、传统开发与DDD的差异对比

下面我们通过表格来直观地对比一下传统开发和DDD的区别:

维度 传统开发 DDD
业务逻辑归属 分散在Service、Util、Controller等多个地方 内聚在领域实体或领域服务中
模型作用 主要作为数据载体,即贫血模型 是携带行为的业务模型,也就是充血模型
技术实现影响 数据库表结构驱动代码设计 业务需求驱动数据库表结构设计

六、电商下单案例:DDD的实际应用

为了进一步加深大家对DDD的理解,我们再来看一个电商下单的案例。假设业务需求是用户下单时要校验库存、使用优惠券、计算实付金额并生成订单。

(一)传统写法(贫血模型)


// 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对象只是数据载体,当需求变更时,修改代码就像“考古”一样困难。

(二)DDD写法(充血模型)


// 聚合根: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类,而DDD只需要修改Order.validateCoupon()方法。

七、DDD的适用场景

DDD虽然强大,但并非适用于所有场景。在业务复杂的系统,像电商、金融、ERP系统,以及需求频繁变更的互联网业务中,DDD能够发挥出巨大的优势。但对于简单的CRUD操作,比如管理后台、数据报表这类功能,使用DDD反而会增加开发成本,有些小题大做。

可以这样判断是否适合使用DDD:当修改业务规则时,只需要调整领域层代码,而不需要改动Controller或DAO,那就说明DDD在这个项目中得到了较好的落地。

总之,希望通过本文,大家对DDD有了更清晰的认识。如果在阅读过程中有任何疑问,欢迎在评论区留言讨论。


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/back/16802.html
喜欢 (0)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】