面向对象的目标不是“每个名词建一个类”,而是把会一起变化的数据和规则放在清晰边界内。先从一个需要不断增加 促销条件的订单计价需求开始。
Table of contents
Open Table of contents
先写可替换的计价规则
import java.math.BigDecimal;
interface 计价规则 {
BigDecimal 计算(BigDecimal 原价);
}
record 无折扣() implements 计价规则 {
public BigDecimal 计算(BigDecimal 原价) { return 原价; }
}
record 比例折扣(BigDecimal 比例) implements 计价规则 {
比例折扣 {
if (比例.signum() < 0 || 比例.compareTo(BigDecimal.ONE) > 0) {
throw new IllegalArgumentException("比例必须在 0 到 1 之间");
}
}
public BigDecimal 计算(BigDecimal 原价) { return 原价.multiply(比例); }
}
record 订单(BigDecimal 原价, 计价规则 规则) {
BigDecimal 应付金额() { return 规则.计算(原价); }
}
订单 不需要知道促销的具体算法,只把计算委托给 计价规则。这就是组合(Composition):对象持有另一个
对象,并通过协作完成职责。增加满减规则时,不需要修改订单类。
**多态(Polymorphism)**表示调用方通过统一接口调用不同实现。它不是为了炫技,而是把容易变化的决策隔离在接口 后面。
封装不是多写 getter
**封装(Encapsulation)**是让对象自己维护合法状态。若 订单 暴露 set原价、set折扣,调用者可以在任意时刻
拼出非法组合。更好的做法是在创建时检查不变量,并只公开业务动作。
record 订单(BigDecimal 原价, 计价规则 规则) {
订单 {
if (原价 == null || 原价.signum() < 0) throw new IllegalArgumentException("原价非法");
if (规则 == null) throw new IllegalArgumentException("计价规则不能为空");
}
}
什么时候不要继承
继承表达“子类型可以在任何需要父类型的地方安全替换父类型”。如果只是为了复用几行代码,组合通常更稳妥。
危险信号包括:
- 子类需要禁用父类方法或抛出“不支持”异常。
- 父类新增字段会迫使所有子类理解它。
- 业务规则需要同时继承两个维度,例如“会员等级”和“地区税率”。
- 测试必须构造复杂继承树才能覆盖一个简单规则。
可以继承稳定抽象,例如框架定义的扩展点;不要继承具体业务类来共享工具方法。
用测试保护替换能力
为接口建立契约测试:同一组输入输出要求应用于每个实现。这样增加规则时,测试验证的不是类内部步骤,而是它能否 遵守计价契约。测试应至少覆盖零金额、正常金额、边界比例和非法比例。
架构层面的判断标准是:新增一种计价方式时,修改范围是否局限在新实现和装配代码。如果必须修改订单、控制器和多个 已有规则,边界仍然不清晰。
下一步
阅读异常设计与资源关闭,让失败也成为接口契约的一部分。