跳到正文
Elaine Blog
返回

面向对象不是堆类:封装、组合与多态

Java 基础

面向对象的目标不是“每个名词建一个类”,而是把会一起变化的数据和规则放在清晰边界内。先从一个需要不断增加 促销条件的订单计价需求开始。

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("计价规则不能为空");
    }
}

什么时候不要继承

继承表达“子类型可以在任何需要父类型的地方安全替换父类型”。如果只是为了复用几行代码,组合通常更稳妥。

危险信号包括:

可以继承稳定抽象,例如框架定义的扩展点;不要继承具体业务类来共享工具方法。

用测试保护替换能力

为接口建立契约测试:同一组输入输出要求应用于每个实现。这样增加规则时,测试验证的不是类内部步骤,而是它能否 遵守计价契约。测试应至少覆盖零金额、正常金额、边界比例和非法比例。

架构层面的判断标准是:新增一种计价方式时,修改范围是否局限在新实现和装配代码。如果必须修改订单、控制器和多个 已有规则,边界仍然不清晰。

下一步

阅读异常设计与资源关闭,让失败也成为接口契约的一部分。


分享这篇文章:

上一篇
类型系统、值传递与对象引用
下一篇
异常设计与资源关闭