如何建模基于type属性的多字段Expense对象关联关系?
Hey,针对你这种基于type字段区分不同费用类型、同时存在大量公共属性的场景,我来分享几个比当前“双互斥字段”更优雅的最优建模方案,都是实际项目里验证过的:
1. 抽象父类+子类继承(经典OOP方案)
既然有大量公共属性,先抽一个抽象父类统一存放所有公共字段,再让MileageExpense和PerDiemExpense作为子类,各自维护专属属性:
// 抽象父类:集中管理所有公共属性 abstract class Expense { String id; String commonAttribute1; // 其他公共属性... } // 里程费用子类:只保留专属属性 class MileageExpense extends Expense { int totalMiles; BigDecimal ratePerMile; } // 每日津贴子类:只保留专属属性 class PerDiemExpense extends Expense { int travelDays; BigDecimal dailyAllowance; }
优势:
- 彻底避免了互斥字段的
null问题,每个子类只持有自己需要的属性 - 公共属性集中维护,不用重复定义
- 后续扩展新费用类型时,直接新增子类即可,完全符合开闭原则
如果需要保留type标识,可以在父类定义抽象方法让子类返回固定值,或者用枚举类实现类型安全:
enum ExpenseType { MILEAGE, PERDIEM } abstract class Expense { String id; String commonAttribute1; abstract ExpenseType getType(); } class MileageExpense extends Expense { @Override public ExpenseType getType() { return ExpenseType.MILEAGE; } // 专属属性... }
2. 密封类(Sealed Class)+ 模式匹配(Java 17+/Kotlin专属)
如果用Java 17及以上版本,密封类是更优的选择——它能严格限制子类范围,保证类型安全,同时结合模式匹配可以写出非常简洁的业务逻辑:
// 密封父类:限定只能有MileageExpense和PerDiemExpense两个子类 sealed class Expense permits MileageExpense, PerDiemExpense { String id; String commonAttribute1; // 公共属性... } // 里程费用子类(必须是final) final class MileageExpense extends Expense { int totalMiles; BigDecimal ratePerMile; } // 每日津贴子类(必须是final) final class PerDiemExpense extends Expense { int travelDays; BigDecimal dailyAllowance; }
处理不同类型时,用switch表达式做模式匹配,完全不用判断type和null:
BigDecimal calculateTotal(Expense expense) { return switch (expense) { case MileageExpense m -> BigDecimal.valueOf(m.totalMiles).multiply(m.ratePerMile); case PerDiemExpense p -> BigDecimal.valueOf(p.travelDays).multiply(p.dailyAllowance); }; }
3. 接口+组合模式(灵活复用场景)
如果不想用继承,也可以把公共属性抽成独立类,再通过组合方式让不同费用类型实现Expense接口:
interface Expense { ExpenseCommon getCommonProperties(); ExpenseType getType(); BigDecimal calculateTotal(); } // 公共属性类 class ExpenseCommon { String id; String commonAttribute1; // 其他公共属性... } class Mileage implements Expense { private final ExpenseCommon common; private int totalMiles; private BigDecimal ratePerMile; public Mileage(ExpenseCommon common) { this.common = common; } @Override public ExpenseCommon getCommonProperties() { return common; } @Override public ExpenseType getType() { return ExpenseType.MILEAGE; } @Override public BigDecimal calculateTotal() { return BigDecimal.valueOf(totalMiles).multiply(ratePerMile); } }
这种方式适合公共属性需要单独复用的场景,不过代码量会比继承方案稍多。
对比你当前方案的问题
你现在“单类+互斥null字段”的写法,缺点很突出:
- 容易触发NullPointerException,每次调用都要先判断
type再取值,非常繁琐 - 不符合单一职责原则,一个类承担了两种费用的逻辑
- 后续扩展新费用类型时,只能不断新增null字段,代码会越来越臃肿
综上,优先推荐抽象父类+子类继承(Java 17+直接用密封类),既解决了null问题,又能高效管理公共属性,代码可读性和扩展性都拉满。
内容的提问来源于stack exchange,提问作者Ahmed Hegazy
相关产品推荐
相关产品推荐

