领域驱动设计(DDD)【23】之泛化:从概念到实践
文章目录
- 一 泛化基础:理解DDD中的核心抽象机制
- 1.1 什么是泛化?
- 1.2 为什么泛化在DDD中重要?
- 1.3 泛化与特化的双向关系
- 二 DDD中泛化的实现形式
- 2.0 实现形式概览
- 2.1 类继承:最直接的泛化实现
- 2.2 接口实现:更灵活的泛化方式
- 2.3 组合优于继承:现代DDD的实践
- 2.4 特征值实现:马匹管理
- 三 DDD中泛化的高级应用
- 3.1 限界上下文中的泛化
- 3.2 泛化的层次设计
- 3.3 泛化与持久化的权衡
- 四 避免泛化的常见陷阱
- 4.1 过度泛化:当抽象变成负担
- 4.2 错误泛化:混淆本质差异
- 4.3 泛化导致的数据污染
- 五、实战案例:电商系统中的泛化设计
- 5.1 商品类别的泛化设计
- 5.2 订单处理的状态模式实现
- 六 总结与最佳实践
- 6.1 何时使用泛化?
- 6.3 泛化的替代方案
- 6.4 持续重构的视角
一 泛化基础:理解DDD中的核心抽象机制
- 在领域驱动设计(Domain-Driven Design, DDD)中,泛化(Generalization)是一种强大的抽象工具,它允许我们识别和表达领域概念之间的"是一种"(is-a)关系。
1.1 什么是泛化?
- 泛化是指从多个特定概念中提取其共性,形成更一般化的概念的过程。在面向对象编程中,这通常通过继承来实现,但在DDD中,泛化的含义更为广泛。现实世界类比:想象你在设计一个动物园管理系统,特定概念:狮子、老虎、长颈鹿->泛化概念:动物。这里,"狮子是一种动物"就体现了泛化关系。
1.2 为什么泛化在DDD中重要?
- 减少重复:共性行为可以放在父类中
- 提高可维护性:修改一处即可影响所有子类
- 增强表达力:更准确地反映业务领域的关系
- 支持多态:允许以统一方式处理不同子类
1.3 泛化与特化的双向关系
- 在这个类图中,Animal是泛化类,Lion和Tiger是特化类。所有动物都有name和birthDate属性,都能eat()和sleep(),但只有狮子能roar(),只有老虎能growl()。
二 DDD中泛化的实现形式
2.0 实现形式概览
第一种是使用类的继承。领域模型中的父类、子类,和实现中的父类、子类直接对应。它的特点在于必须有不同种类的特性或者不同的操作。如果仅仅是某个属性值不同造成的泛化,那么用继承就不合适了。
第二种是接口的实现。如果各个子类在属性和操作实现方面没有共性,但有相同的操作接口,就使用这种方式。
第三种是用特性的值来区分,这时候在实现层面,没有父类和子类之分。特别适用于那些行为完全相同,仅在某些属性值上存在差异的领域概念。
2.1 类继承:最直接的泛化实现
- 电商系统示例:
public abstract class Payment {private BigDecimal amount;private Currency currency;private LocalDateTime paymentDate;public abstract void process();
}public class CreditCardPayment extends Payment {private String cardNumber;private String cardHolder;private LocalDate expiryDate;@Overridepublic void process() {// 信用卡支付处理逻辑}
}public class PayPalPayment extends Payment {private String email;private String transactionId;@Overridepublic void process() {// PayPal支付处理逻辑}
}
优劣分析:
- 优点:直接、明确、编译器支持
- 缺点:Java等语言单继承限制、父类修改影响大
2.2 接口实现:更灵活的泛化方式
public interface Shipment {void schedule();void cancel();TrackingInfo track();
}public class StandardShipment implements Shipment {// 实现标准物流
}public class ExpressShipment implements Shipment {// 实现快递物流
}public class InternationalShipment implements Shipment {// 实现国际物流
}
适用场景:
- 当不同实现有完全不同的行为时
- 需要多重"泛化"时
- 强调能力而非共同结构时
2.3 组合优于继承:现代DDD的实践
public class Product {private String id;private String name;private PricingStrategy pricingStrategy;public BigDecimal calculatePrice(OrderContext context) {return pricingStrategy.calculate(this, context);}
}public interface PricingStrategy {BigDecimal calculate(Product product, OrderContext context);
}public class StandardPricing implements PricingStrategy {// 标准定价策略
}public class DiscountPricing implements PricingStrategy {// 折扣定价策略
}public class BundlePricing implements PricingStrategy {// 捆绑定价策略
}
优势:
- 运行时可替换策略
- 避免类爆炸
- 更符合"组合优于继承"原则
2.4 特征值实现:马匹管理
- 其中:所有马都有相同的属性和行为、唯一区别是颜色不同(枣红马、白马、黑马等)。
public class Horse {private String name;private int age;private HorseColor color; // 类型区分字段public Horse(String name, int age, HorseColor color) {this.name = name;this.age = age;this.color = color;}public void run() { /* 奔跑实现 */ }public void eat() { /* 进食实现 */ }// 特定于颜色的行为(如果需要)public String getDescription() {return color.getDescription() + "马";}
}public enum HorseColor {RED("枣红"),WHITE("白"),BLACK("黑"),PALOMINO("金鬃"),DAPPLE("花斑");private final String description;HorseColor(String description) {this.description = description;}public String getDescription() {return description;}
}
public class HorseService {public void demo() {Horse redHorse = new Horse("赤兔", 5, HorseColor.RED);Horse whiteHorse = new Horse("的卢", 4, HorseColor.WHITE);System.out.println(redHorse.getDescription()); // 输出: 枣红马System.out.println(whiteHorse.getDescription()); // 输出: 白马// 所有马统一处理List<Horse> horses = List.of(redHorse, whiteHorse);horses.forEach(Horse::run);}
}
三 DDD中泛化的高级应用
3.1 限界上下文中的泛化
- 在不同限界上下文中,同一概念的泛化可能不同。医院系统示例:
患者管理上下文:
预约系统上下文:
- 注意在不同上下文中,泛化层次和标准完全不同。
3.2 泛化的层次设计
银行账户系统示例:
设计考量:
- 抽象层次不宜过深(通常不超过3层)
- 每个层次都应增加明确的业务价值
- 避免过度设计,只为真正的业务差异创建子类
3.3 泛化与持久化的权衡
- JPA继承策略示例:
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "account_type")
public abstract class BankAccount {@Idprivate String accountNumber;private BigDecimal balance;// 公共字段
}@Entity
@DiscriminatorValue("SAVINGS")
public class SavingsAccount extends BankAccount {private BigDecimal interestRate;// 特有字段
}@Entity
@DiscriminatorValue("CHECKING")
public class CheckingAccount extends BankAccount {private BigDecimal overdraftLimit;// 特有字段
}
三种继承映射策略对比:
策略类型 | 数据库结构 | 优点 | 缺点 |
---|---|---|---|
SINGLE_TABLE | 单表,用区分字段标识子类 | 查询高效,无需连接 | 字段稀疏,可能有NULL |
JOINED | 父类子类分开表,主键关联 | 结构规范,无冗余 | 查询需连接,性能较差 |
TABLE_PER_CLASS | 每个具体类对应完整表 | 查询具体类高效 | 多态查询需UNION,设计复杂 |
四 避免泛化的常见陷阱
4.1 过度泛化:当抽象变成负担
反例:
public abstract class Entity {private Long id;// 所有实体都需要的字段
}public abstract class Person extends Entity {private String name;private String email;// 所有人共有的字段
}public abstract class User extends Person {private String username;private String password;// 所有用户共有的字段
}public class Customer extends User {// 客户特有字段
}public class Employee extends User {// 员工特有字段
}public class SystemAdmin extends Employee {// 系统管理员特有字段
}
问题分析:
- 层次过深,修改基类影响范围大
- 中间抽象类可能包含不相关功能
- 实际业务需求可能不需要如此复杂的层次
4.2 错误泛化:混淆本质差异
错误案例:
public abstract class PaymentMethod {private BigDecimal amount;public abstract void process();
}public class CreditCard extends PaymentMethod {// 信用卡支付
}public class Invoice extends PaymentMethod {// 发票支付
}public class BankTransfer extends PaymentMethod {// 银行转账
}public class Coupon extends PaymentMethod {// 优惠券
}
问题:
- 优惠券本质上不是支付方式,而是折扣机制
- 强行泛化导致业务逻辑混乱
- 更好的设计是将Coupon作为独立的折扣概念
4.3 泛化导致的数据污染
- 问题示例:
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public abstract class Notification {@Idprivate Long id;private String recipient;private String content;private LocalDateTime sentTime;// 其他公共字段
}@Entity
public class EmailNotification extends Notification {private String emailSubject;private String ccList;
}@Entity
public class SMSNotification extends Notification {private String phoneNumber;
}@Entity
public class PushNotification extends Notification {private String deviceToken;private String appId;
}
问题:
- 单表策略下,表会包含所有子类的字段
- 大多数记录会有大量NULL字段
- 随着子类增加,表会变得臃肿
解决方案:
- 考虑使用JOINED策略
- 或将完全不相关的通知类型拆分到不同限界上下文
五、实战案例:电商系统中的泛化设计
5.1 商品类别的泛化设计
- 初始设计:
演进设计(考虑更多业务需求后):
设计演进说明:
- 从简单的继承层次转变为更灵活的混合模式
- 使用接口(或mixin)表示横切关注点
- 将定价策略分离出来,符合单一职责原则
- 每个产品可以组合不同的特性
5.2 订单处理的状态模式实现
public class Order {private String orderId;private OrderState state;public void cancel() {state.cancel(this);}public void approve() {state.approve(this);}public void ship() {state.ship(this);}public void deliver() {state.deliver(this);}// 状态转移方法void changeState(OrderState newState) {this.state = newState;}
}public interface OrderState {void cancel(Order order);void approve(Order order);void ship(Order order);void deliver(Order order);
}public class DraftState implements OrderState {// 实现草案状态的行为
}public class ApprovedState implements OrderState {// 实现已批准状态的行为
}public class ShippedState implements OrderState {// 实现已发货状态的行为
}public class DeliveredState implements OrderState {// 实现已交付状态的行为
}public class CancelledState implements OrderState {// 实现已取消状态的行为
}
优势:
- 每个状态的行为集中在一处
- 避免大量的if-else条件判断
- 新状态易于添加
- 状态转换逻辑明确
六 总结与最佳实践
6.1 何时使用泛化?
适合使用泛化的情况:
- 存在明确的"是一种"业务关系
- 多个概念有显著共享的行为和属性
- 需要以统一接口处理不同概念时
- 业务领域本身有明显的分类体系
应避免泛化的情况:
- 仅为了代码复用而强行抽象
- 差异大于共性的情况
- 未来可能有根本性差异的概念
- 技术驱动而非业务驱动的分类
6.3 泛化的替代方案
当泛化不合适时,考虑:
- 组合:将共性部分提取为独立组件
- 策略模式:将变化的行为抽象为策略
- 装饰器模式:动态添加功能
- 角色模式:允许对象动态获得能力
6.4 持续重构的视角
泛化设计应是演进式的:
- 开始时可以扁平化设计
- 随着重复代码的出现识别共性
- 当业务规则差异明确时引入抽象
- 定期审视泛化层次是否仍然合理
- 记住Eric Evans的话:“深层次模型是逐步演进而非一次性设计出来的,它需要开发人员和领域专家持续协作,通过多次迭代精炼而成。”