领域驱动设计与 Clean Code 的实践

前言:超越 CRUD,拥抱真正的业务建模

在现代软件开发中,我们常常被 CRUD(创建、读取、更新、删除)操作所束缚。当我们面对一个新需求时,第一反应往往是:"数据库表怎么设计?"、"API 接口需要哪些字段?"。这种以数据为中心的思维方式,在简单场景下或许足够,但当业务复杂度增加时,代码很快就会变成难以维护的"意大利面条"。

领域驱动设计(Domain-Driven Design, DDD)Clean Code 原则 提供了一套完整的解决方案,帮助我们将复杂的业务逻辑转化为清晰、可维护、可演进的代码结构。


一、反面教材:典型的贫血模型实现

让我们先看看一个常见的购物车实现:

java 复制代码
// ❌ 反面示例:贫血模型
public class ShoppingCart {
    private Long id;
    private List<CartItem> items;
    
    // getters and setters
}

public class CartItem {
    private Long productId;
    private String productName;
    private BigDecimal price;
    private Integer quantity;
    
    // getters and setters
}

@Service
public class ShoppingCartService {
    
    public void addItem(ShoppingCart cart, Long productId, Integer quantity) {
        // 1. 检查商品是否存在
        Product product = productRepository.findById(productId);
        if (product == null) {
            throw new IllegalArgumentException("商品不存在");
        }
        
        // 2. 检查库存
        if (product.getStock() < quantity) {
            throw new IllegalArgumentException("库存不足");
        }
        
        // 3. 检查是否已存在
        CartItem existingItem = cart.getItems().stream()
            .filter(item -> item.getProductId().equals(productId))
            .findFirst()
            .orElse(null);
            
        if (existingItem != null) {
            existingItem.setQuantity(existingItem.getQuantity() + quantity);
        } else {
            CartItem newItem = new CartItem();
            newItem.setProductId(productId);
            newItem.setProductName(product.getName());
            newItem.setPrice(product.getPrice());
            newItem.setQuantity(quantity);
            cart.getItems().add(newItem);
        }
    }
    
    public BigDecimal calculateTotal(ShoppingCart cart) {
        return cart.getItems().stream()
            .map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())))
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}

这个实现的问题在哪里?

  1. 贫血模型(Anemic Model)
    ShoppingCartCartItem 仅仅是数据容器,没有任何行为。所有业务逻辑都挤在 ShoppingCartService 中。

  2. 业务规则泄露

    "库存检查"、"重复商品合并"这些属于购物车自身的规则,却被放在了外部服务中。

  3. 对象状态不一致风险

    通过 setter 直接修改数量,可能导致对象处于非法状态(例如数量为负数)。

  4. 难以测试和复用

    想单独测试"购物车能否添加某个商品"?做不到!因为逻辑分散在服务层。

  5. 违反单一职责原则
    ShoppingCartService 既要处理商品添加,又要计算总价,还要处理促销,职责过于混杂。


二、正确姿势:基于 DDD 与 Clean Code 的重构

步骤 1:建立统一语言(Ubiquitous Language)

与领域专家(产品经理、运营人员)对话,提炼核心概念:

业务术语 代码模型
购物车(Shopping Cart) ShoppingCart 聚合根
购物车项(Cart Item) CartItem 值对象
商品(Product) Product 实体
商品ID(Product ID) ProductId 值对象
价格(Price) Money 值对象
数量(Quantity) Quantity 值对象

DDD 核心思想:代码中的类名、方法名必须与业务语言一致,消除沟通歧义。


步骤 2:设计不可变的值对象(Value Object)

首先,将基础概念建模为值对象:

java 复制代码
// ✅ 值对象:ProductId
@Value
@EqualsAndHashCode
public class ProductId {
    private final Long value;
    
    public ProductId(Long value) {
        if (value == null || value <= 0) {
            throw new IllegalArgumentException("商品ID必须大于0");
        }
        this.value = value;
    }
    
    @Override
    public String toString() {
        return value.toString();
    }
}

// ✅ 值对象:Quantity
@Value
@EqualsAndHashCode
public class Quantity {
    private final int value;
    
    public Quantity(int value) {
        if (value <= 0) {
            throw new IllegalArgumentException("数量必须大于0");
        }
        this.value = value;
    }
    
    public Quantity add(Quantity other) {
        return new Quantity(this.value + other.value);
    }
    
    public boolean isLessThan(Quantity other) {
        return this.value < other.value;
    }
}

// ✅ 值对象:Money
@Value
@EqualsAndHashCode
public class Money {
    private final BigDecimal amount;
    private final String currency;
    
    public Money(BigDecimal amount, String currency) {
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("金额不能为负");
        }
        this.amount = amount;
        this.currency = currency;
    }
    
    public Money multiply(int multiplier) {
        return new Money(amount.multiply(BigDecimal.valueOf(multiplier)), currency);
    }
    
    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("货币类型不匹配");
        }
        return new Money(this.amount.add(other.amount), this.currency);
    }
}

值对象的设计原则

  • 不可变性 :所有字段都是 final,创建后不可更改
  • 完整性校验:构造函数中验证参数合法性
  • 行为内聚 :提供相关操作方法(如 add, multiply
  • 相等性基于值 :重写 equalshashCode,基于字段值而非引用

步骤 3:构建充血的聚合根(Aggregate Root)

现在,让 ShoppingCart 成为一个真正的聚合根:

java 复制代码
// ✅ 聚合根:ShoppingCart
public class ShoppingCart {
    private final CartId id;
    private final List<CartItem> items;
    
    // 私有构造函数,强制通过静态工厂创建
    private ShoppingCart(CartId id) {
        this.id = id;
        this.items = new ArrayList<>();
    }
    
    // 静态工厂方法:封装创建逻辑
    public static ShoppingCart createNew() {
        return new ShoppingCart(new CartId(UUID.randomUUID()));
    }
    
    // 核心业务行为:添加商品
    public void addItem(Product product, Quantity quantity) {
        // 1. 参数校验
        if (product == null) {
            throw new IllegalArgumentException("商品不能为空");
        }
        if (quantity == null) {
            throw new IllegalArgumentException("数量不能为空");
        }
        
        // 2. 库存检查(委托给 Product)
        if (!product.hasSufficientStock(quantity)) {
            throw new IllegalArgumentException("库存不足");
        }
        
        // 3. 查找是否已存在
        Optional<CartItem> existingItem = findItemByProductId(product.getId());
        
        if (existingItem.isPresent()) {
            // 更新现有项
            CartItem updatedItem = existingItem.get().increaseQuantity(quantity);
            // 替换列表中的项(保持不可变性)
            int index = items.indexOf(existingItem.get());
            items.set(index, updatedItem);
        } else {
            // 添加新项
            items.add(CartItem.create(product, quantity));
        }
    }
    
    // 核心业务行为:移除商品
    public void removeItem(ProductId productId) {
        items.removeIf(item -> item.getProductId().equals(productId));
    }
    
    // 核心业务行为:计算总价
    public Money calculateTotal() {
        return items.stream()
            .map(CartItem::getTotalPrice)
            .reduce(Money::add)
            .orElse(new Money(BigDecimal.ZERO, "CNY"));
    }
    
    // 查询方法:获取所有项
    public List<CartItem> getItems() {
        // 返回不可变视图,防止外部修改
        return Collections.unmodifiableList(items);
    }
    
    // 内部辅助方法
    private Optional<CartItem> findItemByProductId(ProductId productId) {
        return items.stream()
            .filter(item -> item.getProductId().equals(productId))
            .findFirst();
    }
    
    // 只读访问器
    public CartId getId() {
        return id;
    }
}
购物车项(CartItem)作为值对象:
java 复制代码
// ✅ 值对象:CartItem
@Value
@EqualsAndHashCode
public class CartItem {
    private final ProductId productId;
    private final String productName;
    private final Money unitPrice;
    private final Quantity quantity;
    
    // 私有构造函数
    private CartItem(ProductId productId, String productName, Money unitPrice, Quantity quantity) {
        this.productId = productId;
        this.productName = productName;
        this.unitPrice = unitPrice;
        this.quantity = quantity;
    }
    
    // 静态工厂方法
    public static CartItem create(Product product, Quantity quantity) {
        return new CartItem(
            product.getId(),
            product.getName(),
            product.getPrice(),
            quantity
        );
    }
    
    // 行为方法:增加数量
    public CartItem increaseQuantity(Quantity additionalQuantity) {
        Quantity newQuantity = this.quantity.add(additionalQuantity);
        return new CartItem(productId, productName, unitPrice, newQuantity);
    }
    
    // 计算总价
    public Money getTotalPrice() {
        return unitPrice.multiply(quantity.value());
    }
}

步骤 4:完善 Product 实体

java 复制代码
// ✅ 实体:Product
public class Product {
    private final ProductId id;
    private final String name;
    private final Money price;
    private final Quantity stock;
    
    // 私有构造函数
    private Product(ProductId id, String name, Money price, Quantity stock) {
        this.id = id;
        this.name = name;
        this.price = price;
        this.stock = stock;
    }
    
    // 静态工厂方法
    public static Product create(ProductId id, String name, Money price, Quantity stock) {
        return new Product(id, name, price, stock);
    }
    
    // 业务方法:检查库存是否充足
    public boolean hasSufficientStock(Quantity requestedQuantity) {
        return !requestedQuantity.isLessThan(stock);
    }
    
    // 只读访问器
    public ProductId getId() { return id; }
    public String getName() { return name; }
    public Money getPrice() { return price; }
    public Quantity getStock() { return stock; }
}

步骤 5:服务层的职责澄清

重构后的服务层变得极其简洁:

java 复制代码
// ✅ 简洁的服务层
@Service
@Transactional
public class ShoppingCartApplicationService {
    
    private final ProductRepository productRepository;
    private final ShoppingCartRepository shoppingCartRepository;
    
    public ShoppingCartApplicationService(
        ProductRepository productRepository,
        ShoppingCartRepository shoppingCartRepository) {
        this.productRepository = productRepository;
        this.shoppingCartRepository = shoppingCartRepository;
    }
    
    public void addItemToCart(String cartId, Long productId, int quantity) {
        // 1. 获取领域对象
        ShoppingCart cart = shoppingCartRepository.findById(new CartId(cartId));
        Product product = productRepository.findById(new ProductId(productId));
        Quantity qty = new Quantity(quantity);
        
        // 2. 触发业务行为
        cart.addItem(product, qty);
        
        // 3. 保存变更
        shoppingCartRepository.save(cart);
    }
    
    public Money getCartTotal(String cartId) {
        ShoppingCart cart = shoppingCartRepository.findById(new CartId(cartId));
        return cart.calculateTotal();
    }
}

服务层的新角色

  • 协调者:获取领域对象,触发其行为
  • 事务边界 :用 @Transactional 保证操作原子性
  • 防腐层:将外部请求(如 API 参数)转换为领域对象

三、设计原则深度解析

1. 不可变性(Immutability)的力量

值对象必须不可变
  • 安全性 :避免意外修改。例如,如果 Money 可变,外部代码可能修改购物车项的价格
  • 线程安全:不可变对象天然线程安全
  • 缓存友好:可以安全地缓存和共享
实体部分可变
  • 身份标识不可变CartId 一旦创建就不能改变
  • 业务状态可变items 列表可以修改,但只能通过行为方法

📌 经验法则:能用不可变就用不可变;必须可变时,严格控制变更入口。

2. 静态工厂方法 vs 构造函数

特性 构造函数 静态工厂方法
命名 固定(类名) 自由(createNew, fromSnapshot
返回类型 必须是当前类 可返回子类或缓存实例
对象有效性 无法保证 可在校验后创建,确保有效
语义清晰度 较低 较高(Product.create(...)new Product(...) 更清晰)

在领域对象中,静态工厂方法是首选

3. 充血模型:让对象拥有"智慧"

  • 贫血模型:对象是" dumb data holder ",逻辑在服务层
  • 充血模型:对象是" smart actor ",知道如何响应业务事件

充血模型的优势

  • 高内聚:相关数据和行为在一起
  • 易于测试:可以单独测试对象的行为
  • 业务表达力强:代码即文档

💡 Eric Evans(DDD 之父):"领域对象应该封装状态和行为,这是面向对象的核心。"

4. 聚合根的设计原则

  • 一致性边界:聚合根负责维护内部对象的一致性
  • 外部只能引用根 :其他对象只能持有 ShoppingCart 的引用,不能直接持有 CartItem
  • 事务一致性:对聚合的修改必须在单个事务中完成

在我们的例子中,ShoppingCart 是聚合根,CartItem 是内部值对象。

5. 防御性编程与 Fail-Fast

  • 构造时校验:值对象在构造时验证参数合法性
  • 方法参数校验:业务方法验证输入参数
  • 早期失败:在问题发生时立即抛出异常,而不是让错误传播

这确保了对象始终处于有效状态。


四、Clean Code 原则的具体体现

1. 有意义的命名

  • addItemadd 更清晰
  • hasSufficientStockcheckStock 更准确
  • calculateTotalgetTotal 更准确(因为涉及计算)

2. 函数只做一件事

每个方法都有单一职责:

  • addItem:处理商品添加逻辑
  • calculateTotal:计算总价
  • hasSufficientStock:检查库存

3. 抽象层次一致

同一方法中的代码应该在同一抽象层次:

java 复制代码
// ✅ 好的抽象层次
public void addItem(Product product, Quantity quantity) {
    validateInputs(product, quantity);           // 高层抽象
    checkInventory(product, quantity);           // 高层抽象  
    updateCartItems(product, quantity);          // 高层抽象
}

// ❌ 混合抽象层次
public void addItem(Product product, Quantity quantity) {
    if (product == null) throw ...               // 低层细节
    if (!product.hasSufficientStock(quantity))   // 高层抽象
        throw ...
    // ... 混合细节和抽象
}

4. 避免副作用

  • 查询方法无副作用calculateTotal()getItems() 不修改对象状态
  • 命令方法有明确意图addItem()removeItem() 明确表示会修改状态

5. 使用描述性异常

  • 抛出有意义的异常信息:"库存不足" 而不是 "Validation failed"
  • 在合适的位置抛出异常:在业务规则违反的地方,而不是在调用方

五、如何将此模式应用到你的项目?

无论你的业务是金融、医疗、物流还是 IoT,只要涉及复杂状态变更多条件规则,都可以套用此模式:

1. 识别核心聚合根与值对象

  • 聚合根:有唯一身份,是外部访问的入口点(如订单、用户、设备)
  • 值对象:无身份,通过属性定义(如地址、价格、时间段、配置)

2. 将业务规则移入领域对象

问自己:"这个规则属于谁的职责?"

  • "订单能否取消?" → 应该是 Order.canBeCancelled()
  • "用户密码是否符合复杂度要求?" → 应该是 Password.isValid()

3. 用行为方法替代 setter

  • 禁止直接 setXXX
  • 提供 placeOrder(), cancel(), ship() 等业务语义方法

4. 服务层只做协调

  • 获取领域对象
  • 触发其行为
  • 处理事务和跨聚合操作

5. 逐步重构

如果你的代码已经是贫血模型,不要试图一次性重构:

  1. 先识别核心业务规则
  2. 将规则移到相关对象中
  3. 逐步移除服务层中的逻辑
  4. 最终形成充血模型

六、常见误区与最佳实践

误区 1:过度设计

问题 :为简单场景引入复杂的 DDD 模式
解决方案 :DDD 适用于复杂业务逻辑。对于简单的 CRUD,保持简单即可。

误区 2:聚合过大

问题 :将太多对象放入一个聚合,导致性能问题
解决方案:聚合应该尽可能小。只包含需要强一致性的对象。

误区 3:忽略 ORM 兼容性

问题 :JPA/Hibernate 需要无参构造函数和 setter
解决方案

  • 使用 Builder 模式
  • 或接受在持久化层有轻微妥协,但领域层保持纯净
  • 考虑使用支持不可变对象的 ORM(如 Spring Data JDBC)

误区 4:所有逻辑都放进聚合根

问题 :聚合根变得过于庞大
解决方案 :以下逻辑适合放在领域服务(Domain Service)

  • 涉及多个聚合根的操作
  • 无法自然归属到单一实体的复杂算法
  • 需要外部依赖的业务逻辑(如调用第三方服务)

七、测试策略

充血模型极大地简化了测试:

单元测试购物车

java 复制代码
@Test
void shouldAddNewItemWhenProductNotInCart() {
    // Given
    ShoppingCart cart = ShoppingCart.createNew();
    Product product = Product.create(
        new ProductId(1L),
        "iPhone",
        new Money(new BigDecimal("999.00"), "CNY"),
        new Quantity(10)
    );
    Quantity quantity = new Quantity(2);
    
    // When
    cart.addItem(product, quantity);
    
    // Then
    assertThat(cart.getItems()).hasSize(1);
    assertThat(cart.getItems().get(0).getQuantity()).isEqualTo(quantity);
    assertThat(cart.calculateTotal()).isEqualTo(new Money(new BigDecimal("1998.00"), "CNY"));
}

@Test
void shouldThrowExceptionWhenInsufficientStock() {
    // Given
    ShoppingCart cart = ShoppingCart.createNew();
    Product product = Product.create(
        new ProductId(1L),
        "iPhone",
        new Money(new BigDecimal("999.00"), "CNY"),
        new Quantity(1) // 只有1个库存
    );
    Quantity quantity = new Quantity(2); // 请求2个
    
    // When & Then
    assertThatThrownBy(() -> cart.addItem(product, quantity))
        .isInstanceOf(IllegalArgumentException.class)
        .hasMessage("库存不足");
}

测试优势

  • 无需 mock:纯内存操作,快速可靠
  • 业务导向:测试用例直接反映业务场景
  • 高覆盖率:容易覆盖各种边界条件
相关推荐
Mr. zhihao1 小时前
[特殊字符] 从 Redis 缓存穿透到布隆过滤器,再到布谷鸟过滤器:一次穿透防护的进化之旅
数据库·redis·缓存
@小匠1 小时前
Redis 7 持久化机制
数据库·redis·缓存
Geoffwo1 小时前
Oracle MySQL8.0升级8.4,无感升级数据库
数据库·oracle
u0110225121 小时前
如何自定义查询历史记录面板的展示风格_时间轴样式设计
jvm·数据库·python
2301_769340671 小时前
HTML怎么实现快捷跳转顶部_HTML固定悬浮锚点按钮【介绍】
jvm·数据库·python
老马95271 小时前
opencode7-桌面应用实战2
java·人工智能·后端
m0_609160491 小时前
MySQL如何限制触发器递归调用的深度_防止触发器死循环方法
jvm·数据库·python
呼Lu噜1 小时前
基于C#的ASP.NET Core中分析async、await的使用场景
数据库·c#·asp.net
李白的天不白1 小时前
大规模请求数据并发问题
java·前端·数据库