2.面向对象的特性

2.面向对象的特性

目录介绍
  • 1.先回答上篇思考题
    • [1.1 上篇遗留三道题](#1.1 上篇遗留三道题 "#11-%E4%B8%8A%E7%AF%87%E9%81%97%E7%95%99%E4%B8%89%E9%81%93%E9%A2%98")
    • [1.2 三道题的共同根](#1.2 三道题的共同根 "#12-%E4%B8%89%E9%81%93%E9%A2%98%E7%9A%84%E5%85%B1%E5%90%8C%E6%A0%B9")
    • [1.3 一次余额对账事故](#1.3 一次余额对账事故 "#13-%E4%B8%80%E6%AC%A1%E4%BD%99%E9%A2%9D%E5%AF%B9%E8%B4%A6%E4%BA%8B%E6%95%85")
    • [1.4 灵魂五连问](#1.4 灵魂五连问 "#14-%E7%81%B5%E9%AD%82%E4%BA%94%E8%BF%9E%E9%97%AE")
  • [2.从一个 Bug 说起](#2.从一个 Bug 说起 "#2%E4%BB%8E%E4%B8%80%E4%B8%AA-bug-%E8%AF%B4%E8%B5%B7")
    • [2.1 钱包翻车现场](#2.1 钱包翻车现场 "#21-%E9%92%B1%E5%8C%85%E7%BF%BB%E8%BD%A6%E7%8E%B0%E5%9C%BA")
    • [2.2 Bug 的根因](#2.2 Bug 的根因 "#22-bug-%E7%9A%84%E6%A0%B9%E5%9B%A0")
    • [2.3 四大特性登场](#2.3 四大特性登场 "#23-%E5%9B%9B%E5%A4%A7%E7%89%B9%E6%80%A7%E7%99%BB%E5%9C%BA")
  • 3.特性全景图
    • [3.1 四大特性关系](#3.1 四大特性关系 "#31-%E5%9B%9B%E5%A4%A7%E7%89%B9%E6%80%A7%E5%85%B3%E7%B3%BB")
    • [3.2 三大还是四大](#3.2 三大还是四大 "#32-%E4%B8%89%E5%A4%A7%E8%BF%98%E6%98%AF%E5%9B%9B%E5%A4%A7")
  • 4.封装特性
    • [4.1 封装的动机](#4.1 封装的动机 "#41-%E5%B0%81%E8%A3%85%E7%9A%84%E5%8A%A8%E6%9C%BA")
    • [4.2 钱包封装版](#4.2 钱包封装版 "#42-%E9%92%B1%E5%8C%85%E5%B0%81%E8%A3%85%E7%89%88")
    • [4.3 不变量守卫](#4.3 不变量守卫 "#43-%E4%B8%8D%E5%8F%98%E9%87%8F%E5%AE%88%E5%8D%AB")
    • [4.4 封装的层次](#4.4 封装的层次 "#44-%E5%B0%81%E8%A3%85%E7%9A%84%E5%B1%82%E6%AC%A1")
    • [4.5 实现原理](#4.5 实现原理 "#45-%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86")
    • [4.6 好坏封装对比](#4.6 好坏封装对比 "#46-%E5%A5%BD%E5%9D%8F%E5%B0%81%E8%A3%85%E5%AF%B9%E6%AF%94")
  • 5.抽象特性
    • [5.1 抽象的动机](#5.1 抽象的动机 "#51-%E6%8A%BD%E8%B1%A1%E7%9A%84%E5%8A%A8%E6%9C%BA")
    • [5.2 接口与抽象类](#5.2 接口与抽象类 "#52-%E6%8E%A5%E5%8F%A3%E4%B8%8E%E6%8A%BD%E8%B1%A1%E7%B1%BB")
    • [5.3 抽象的层级](#5.3 抽象的层级 "#53-%E6%8A%BD%E8%B1%A1%E7%9A%84%E5%B1%82%E7%BA%A7")
    • [5.4 命名即抽象](#5.4 命名即抽象 "#54-%E5%91%BD%E5%90%8D%E5%8D%B3%E6%8A%BD%E8%B1%A1")
  • 6.继承特性
    • [6.1 继承的动机](#6.1 继承的动机 "#61-%E7%BB%A7%E6%89%BF%E7%9A%84%E5%8A%A8%E6%9C%BA")
    • [6.2 类型层级](#6.2 类型层级 "#62-%E7%B1%BB%E5%9E%8B%E5%B1%82%E7%BA%A7")
    • [6.3 实现原理](#6.3 实现原理 "#63-%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86")
    • [6.4 继承的代价](#6.4 继承的代价 "#64-%E7%BB%A7%E6%89%BF%E7%9A%84%E4%BB%A3%E4%BB%B7")
  • 7.多态特性
    • [7.1 多态的动机](#7.1 多态的动机 "#71-%E5%A4%9A%E6%80%81%E7%9A%84%E5%8A%A8%E6%9C%BA")
    • [7.2 三种形态](#7.2 三种形态 "#72-%E4%B8%89%E7%A7%8D%E5%BD%A2%E6%80%81")
    • [7.3 实现原理](#7.3 实现原理 "#73-%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86")
    • [7.4 多态的代价](#7.4 多态的代价 "#74-%E5%A4%9A%E6%80%81%E7%9A%84%E4%BB%A3%E4%BB%B7")
  • 8.特性总结与延展
  • 9.综合实战案例
    • [9.1 钱包再升级需求](#9.1 钱包再升级需求 "#91-%E9%92%B1%E5%8C%85%E5%86%8D%E5%8D%87%E7%BA%A7%E9%9C%80%E6%B1%82")
    • [9.2 一行 public 引发的崩塌](#9.2 一行 public 引发的崩塌 "#92-%E4%B8%80%E8%A1%8C-public-%E5%BC%95%E5%8F%91%E7%9A%84%E5%B4%A9%E5%A1%8C")
    • [9.3 四大特性逐层加固](#9.3 四大特性逐层加固 "#93-%E5%9B%9B%E5%A4%A7%E7%89%B9%E6%80%A7%E9%80%90%E5%B1%82%E5%8A%A0%E5%9B%BA")
    • [9.4 类图与不变量清单](#9.4 类图与不变量清单 "#94-%E7%B1%BB%E5%9B%BE%E4%B8%8E%E4%B8%8D%E5%8F%98%E9%87%8F%E6%B8%85%E5%8D%95")
    • [9.5 留下三道思考题](#9.5 留下三道思考题 "#95-%E7%95%99%E4%B8%8B%E4%B8%89%E9%81%93%E6%80%9D%E8%80%83%E9%A2%98")
  • 09.认知跃迁总结

# 章节 主题 关键案例 关键图表
01 面向对象设计思想 范式之别 双 11 雪崩订单系统 网状 vs 线性流程图
02 面向对象的特性 封装/抽象/继承/多态 钱包余额对不上排查 vtable 内存布局图
03 接口vs抽象类比较 何时选哪个 日志器 47 次改动复盘 决策树、模板时序图
04 接口而非实现编程 依赖抽象不依赖具体 云迁移 6 万行连锁修改 三步重构演化图
05 多用组合和少继承 继承陷阱与组合优雅 企鹅会飞继承翻车 类爆炸 vs 正交组合
06 设计原则全景图 SOLID + 23 模式 8000 行上帝类失控 SOLID 关系图
07 SOLID 原则案例汇 SOLID 在战场上 团队三年 SOLID 之争 滥用现场扫描图
08 反模式与坏味道 30+ 嗅觉清单 PR 被驳回 17 次 坏味道地图
09 重构十二式实战 12 个最常用手法 1200 行结算函数救援 重构节奏循环
10 可测试性设计 OOP 的硬验收 95% 覆盖率上线即崩 测试金字塔
11 DDD 与战术建模 从代码到业务 需求文档 70% 失真 限界上下文图
12 综合实战图片框架 完整系统实现 图片处理系统实战 完整架构图

1.先回答上篇思考题

1.1 上篇遗留三道题

上一篇 01.面向对象设计思想 末尾留下了三道题:

  • 🟢 把 Order.items 改成 public 会发生什么坏事?
  • 🟡 怎样改 BuyTwoGetOneActivity 而不动 Order 一行?
  • 🔴 多个活动叠加,List<Activity> / 装饰器 / 管道,三种方案各有什么代价?

它们看似分别对应「字段访问」「类的扩展」「调用顺序」,但答案都同一个根,本篇的四大特性。

1.2 三道题的共同根

看似考的 实际考的 对应特性
🟢 易 字段可见性 不变量被谁守护 封装
🟡 中 类的扩展点 细节被谁隐藏 抽象 + 多态
🔴 难 数据结构选型 行为如何编排 继承 / 组合 / 多态

快速推导一下:

  • 🟢 items 改成 public → 外部可以 order.items.add(newItem) 绕过 Order.total()总价不变量失效 。根因:缺乏封装,没有守门人。
  • 🟡 不改 Order 一行,只改 BuyTwoGetOne → 因为 Activityinterface,新规则只需加一个实现类。根因:抽象+多态同时在场。
  • 🔴 三个方案选什么 → 取决于"编排谁说了算":List<Activity> 责任在 Activity 外部、装饰器链在调用点、管道在 Activity 内部。这是第 05 篇「组合 vs 继承」的核心议题。

如果你在上一篇能直觉地写出"items 应该 private、Activity 应该是接口",说明你的脑子里已经埋着这四大特性,本篇要做的,是把它们从直觉变成可解释的工程语言。

1.3 一次余额对账事故

让"四大特性"不再是教科书名词,先看一个真实事故。2024 年 3 月,某公司财务系统:

凌晨 2 点对账:用户表与流水表余额相差 ¥1273.45 ,不是大数,但对账不能容忍任何差额。 排查发现:流水表显示用户 A 充值 100,但用户表余额没动;用户 B 没操作,余额却减少 100。

复盘根因:一段「转账」代码,

java 复制代码
// 事故代码 · 脱敏后
public void transfer(Account from, Account to, BigDecimal amount) {
    from.balance = from.balance.subtract(amount);
    to.balance   = to.balance.add(amount);
    recordLog(from, to, amount);
}

看起来人畜无害?它至少违反了四大特性中的三个

flowchart TD A[transfer 函数] -.写.-> B[from.balance] A -.写.-> C[to.balance] A -.调.-> D[recordLog] B & C ==> E{出错时<br/>谁守约束} E -->|没有| F[余额可以是负数] E -->|没有| G[扣款成功+加款失败=钱凭空消失] E -->|没有| H[recordLog 失败=对账缺日志]
现象 缺失的特性
调用方能直接改 balance 字段 封装 缺失,不变量"余额≥0"无人守
业务规则散在 transfer 函数里 抽象 缺失,「转账」该是 Account 自己的方法
切到"信用账户"(允许负余额)要改所有调用方 多态 缺失,账户类型没有统一接口

真相:四大特性不是为了"OOP 看起来更面向对象",而是为了让"约束有人守、变化有人扛"。

1.4 灵魂五连问

读到这里你应该有一连串疑问,本篇就靠这五问串起:

arduino 复制代码
Q1 ── 封装到底封装了什么?只是字段 private 吗?
       └─→ §03 封装的不变量守卫
Q2 ── 抽象和封装到底差在哪?听起来很像
       └─→ §04 抽象 = 隐藏实现复杂度
Q3 ── 继承不是经常被骂吗?为什么还要学?
       └─→ §05 继承的代价与价值
Q4 ── 多态究竟"动态"在哪?编译器看到了什么?
       └─→ §06 vtable / 内联缓存的实现原理
Q5 ── 四大特性中,最重要的是哪一个?
       └─→ §07 层层依赖关系图

2.从一个 Bug 说起

为什么换案例? 上篇我们造了一单 Order(含 Activity、Customer),足够复杂,用来展示「数据+行为绑定」的威力很到位。

但本篇要讲四大特性,如果一上来就用 Order(5 个类、多态 Activity),读者会被"设计复杂度"吸走注意力,看不清每个特性本身的味道。

所以换一个最小的样本:一只钱包 ,只有 balancebalanceLastModifiedTime 两个字段。在这个玩具级的例子上,四个特性的"有没有"差别会像放大镜一样清楚。

2.1 钱包翻车现场

接续上一篇的电商系统。线上突然出现"用户余额变了,但 balanceLastModifiedTime 还是上周的时间"。排查代码:

java 复制代码
public class Wallet {
    public BigDecimal balance;
    public long balanceLastModifiedTime;
}

// 某处业务代码
wallet.balance = wallet.balance.add(amount);  // 偷偷加了钱
// 忘了更新 balanceLastModifiedTime

这种"忘记同步"在百万行代码库里几乎无法靠纪律堵住。

2.2 Bug 的根因

字段 public、行为外置 → 不变量(balance 与时间必须同步变更)的守卫被分散到了 N 个调用点,任何一处疏忽都会全局污染。

2.3 四大特性登场

OOP 的四大特性,每一个都直接对应一类复杂度治理问题:

特性 对治的问题
封装 数据被任意修改
抽象 实现细节泄漏到调用方
继承 重复代码与类型层级
多态 调用方写满 if-else

3.特性全景图

3.1 四大特性关系

flowchart TB 封装[封装<br/>建立边界] --> 抽象[抽象<br/>隐藏实现] 封装 --> 继承[继承<br/>层级关系] 抽象 --> 多态[多态<br/>统一接口] 继承 --> 多态

封装是地基 :没有边界,其余三者无从谈起; 抽象是骨架 :定义"做什么"; 继承+多态是肌肉:让骨架在不同形态下复用与变形。

3.2 三大还是四大

业内常争论"抽象"是否独立,因为抽象不依赖特殊语法,函数本身就是一种抽象。记住一句话即可:四大特性争的是分类法,工程价值在于解决了什么问题,名字只是标签。


4.封装特性

4.1 封装的动机

根本目的:管理复杂度。人脑同时处理信息约 7±2 单元,百万行系统若全暴露,没人能理解。

封装的本质是把复杂度藏起来,只留必要的操作面板

复制代码
没有封装的车:油门控制喷油量、变速箱档位、ABS 频率...... → 没人能开
有封装的车:方向盘 + 油门 + 刹车 + 挡位 → 人人能开

4.2 钱包封装版

java 复制代码
public class Wallet {
    private final String id;
    private final long createTime;
    private BigDecimal balance;
    private long balanceLastModifiedTime;

    public Wallet() {
        this.id = IdGenerator.gen();
        this.createTime = System.currentTimeMillis();
        this.balance = BigDecimal.ZERO;
        this.balanceLastModifiedTime = this.createTime;
    }

    public BigDecimal getBalance() { return balance; }

    public void increase(BigDecimal amount) {
        if (amount.compareTo(BigDecimal.ZERO) <= 0)
            throw new InvalidAmountException("金额必须 > 0");
        this.balance = this.balance.add(amount);
        this.balanceLastModifiedTime = System.currentTimeMillis();  // ← 同步更新
    }

    public void decrease(BigDecimal amount) {
        if (amount.compareTo(BigDecimal.ZERO) <= 0)
            throw new InvalidAmountException("金额必须 > 0");
        if (amount.compareTo(this.balance) > 0)
            throw new InsufficientAmountException("余额不足");
        this.balance = this.balance.subtract(amount);
        this.balanceLastModifiedTime = System.currentTimeMillis();
    }
}

字段 private + 关键约束写在方法内部 → 不变量被强制守卫,调用方再也无法绕过。

4.3 不变量守卫

封装解决的核心问题有三类:

flowchart LR A[封装价值] --> B[不变量保护<br/>状态永远合法] A --> C[变更隔离<br/>实现可换不影响调用] A --> D[认知降负<br/>只看接口不看内部]

4.4 封装的层次

封装不仅在类一级:

java 复制代码
系统层:微服务以 API 交互
模块层:Java module / Go package
类  层:private 字段 + public 方法
函数层:局部变量对外不可见

四个层次原理一致:隐藏实现,暴露接口,降低耦合

4.5 实现原理

不同语言的封装机制差异巨大:

语言 机制 强度
C++ 编译期访问检查 + 名称查找 编译期君子协定
Java 字节码 access_flags,JVM 持续校验 编译期+运行时
JavaScript 闭包 / # 私有字段(Symbol 隔离) 引擎级隔离
Go 包级(首字母大小写) 编译期
Python 约定(_/__ 不强制

关键洞察 :C++ 的 private 在二进制里没有任何痕迹,靠 reinterpret_cast 可以"破";Java 的 private 写在字节码里,反射能"破",但模块系统能进一步堵死;JS 的闭包是真正不可达的,三种封装强度递进。

4.6 好坏封装对比

java 复制代码
// ✗ 坏封装:getter/setter 满天飞,等于没封装
class Bad {
    private int x;
    public int getX() { return x; }
    public void setX(int x) { this.x = x; }
}

// ✓ 好封装:方法背后有规则
class Good {
    private List<Item> items;
    private Money totalPrice = Money.ZERO;
    public void addItem(Item item) {
        if (items.size() >= MAX) throw new CapacityException();
        items.add(item);
        totalPrice = totalPrice.add(item.amount());   // 派生状态同步
        notifyObservers();                            // 副作用触发
    }
}

封装的灵魂不是"加 private",而是让方法承担"必须如此"的业务约束


5.抽象特性

5.1 抽象的动机

封装藏数据,抽象藏实现,调用者只需知道"能做什么",无需知道"怎么做"。

flowchart LR Caller[调用方] -->|关心 what| Iface(抽象接口) Iface -->|不关心 how| Impl1[实现 A] Iface --> Impl2[实现 B]

5.2 接口与抽象类

回到钱包的持久化需求。同一只 Wallet 的数据,开发期用内存、测试期用 H2、生产环境用 MySQL + Redis 双写,底层三天一换,调用方不能跟着改。这就需要用接口把"持久化"这件事抽象掉:

java 复制代码
public interface WalletRepository {
    void save(Wallet wallet);
    Wallet findById(String walletId);
}

public class InMemoryWalletRepo implements WalletRepository { /* 内存版 · 开发用 */ }
public class MySqlWalletRepo    implements WalletRepository { /* MySQL 版 · 生产用 */ }

PaymentService 只面向 WalletRepository底层切实现,PaymentService 零感知

注意:抽象不必依赖 interface/abstract 语法。函数本身就是抽象,malloc(size) 没人去看它的伙伴算法。

Wallet 对调用方暴露 increase / decrease / balance,不暴露它背后是 MySQL、Redis 还是文件,这正是日常 OOP 中最朴素也最强大的抽象落地。

5.3 抽象的层级

抽象有不同层级,仍在钱包体系内分层:

scss 复制代码
高层抽象:业务概念(Wallet、Transaction)
中层抽象:持久化接口(WalletRepository)、消息接口(TxEventPublisher)
低层抽象:函数(validateAmount(amt)、isAllowed(after))

越靠上越稳定,越靠下越易变。设计原则之一:让稳定的东西被依赖,让易变的东西被替换,这就是后两篇的主题。

5.4 命名即抽象

saveToMysqlWallet(...)save(...) 的差别:前者把"MySQL"这一实现细节焊死到了名字里。一旦存储切到 Redis 或文件,名字就要改,连锁拖累所有调用点。

getRedisBalance() 同理,名字暴露了存储引擎,调用方被迫知道不该知道的细节。

好命名只描述能力,不暴露策略,这是抽象思维最日常的体现。


6.继承特性

6.1 继承的动机

回到钱包。PM 在第 01 篇提出多种钱包:普通钱包(余额 ≥ 0)、信用钱包(可透支至 -500)、冻结钱包(只读)、花呗钱包(可透支至 -800)。它们共享同一套基本行为(充值、消费、查余额),但各自的"约束规则"不同。

这种"同骨架、不同肌肉"的场景,正是继承的用武之地:

复制代码
Wallet(约束规则:余额 ≥ 0)
 ├── NormalWallet(约束规则:余额 ≥ 0)
 ├── CreditWallet(约束规则:余额 ≥ -500)
 ├── HuabeiWallet(约束规则:余额 ≥ -800)
 └── FrozenWallet(约束规则:任何写操作都拒绝)

继承把"共享的骨架代码"放在父类,子类只覆写差异化的部分(规则)。

6.2 类型层级与里氏替换

继承构造的是子类型关系

arduino 复制代码
CreditWallet 的集合 ⊂ Wallet 的集合
→ 任何"Wallet"位置放一只"CreditWallet"都合法

这就是里氏替换原则(LSP) 的数学本质:子类必须能替换父类,不破坏程序正确性。

classDiagram Wallet <|-- CreditWallet Wallet <|-- FrozenWallet class Wallet { -BigDecimal balance -List~TxLog~ txLogs +consume(amt) +balance() BigDecimal #isAllowed(after) bool } class CreditWallet { #isAllowed() 覆写为「≥ -500」 } class FrozenWallet { #isAllowed() 覆写为「永远 false」 }

PaymentService 只知道 Wallet,给它 CreditWallet 照样 consume换子类不需要改调用方,这是继承 + 多态组合出的最强扩展力。

6.3 实现原理

C++/Java 的继承落到底层就是虚函数表(vtable) 。以 CreditWallet 为例:

arduino 复制代码
CreditWallet 对象内存布局:
┌──────────────┐
│ vptr ───────────→ CreditWallet_vtable
├──────────────┤    ┌──────────────────────────────────┐
│ balance      │    │ isAllowed → CreditWallet::isAllowed  │
│ txLogs       │    │ consume   → Wallet::consume         │
│ ...          │    │ balance   → Wallet::balance          │
└──────────────┘    └──────────────────────────────────┘

Wallet* w = new CreditWallet();
w->consume(100);
  ├─ ① 查 vptr[1] → Wallet::consume
  ├─ ② consume 内调 this->isAllowed(after)
  └─ ③ this 指向 CreditWallet,再次查 vptr[0] → CreditWallet::isAllowed ✓

子类对象前半段与父类布局一致 → 父类指针可以安全指向子类对象。这就是多态的物理基础,两次查表的开销,换来了"加一种钱包不改调用方"的扩展力。

6.4 继承的代价

继承也是双刃剑。以钱包为例就能看清每一条:

代价 钱包场景的实例
强耦合 父类 Wallet 改了 consume 内部流程,CreditWalletFrozenWallet 全被牵连
层级爆炸 再加"VIP 信用钱包"(叠加 VIP 规则 + 信用规则),层级立刻缠绕
破坏封装 CreditWallet 依赖父类 balance 字段的内部表示,改字段类型子类就崩
静态绑定 new CreditWallet() 写死具体类,运行时不能把普通钱包"升级成"信用钱包

这正是后续第 5 篇 "多用组合少用继承" 要破的题,用组合方式给 Wallet 注入一个 BalanceRule 策略,而不是派生子类。


7.多态特性

7.1 多态的动机

没多态时,调用方必须认识每种具体类型。以钱包消费为例,普通钱包余额 ≥ 0 才允许扣款,信用钱包允许透支至 -500,冻结钱包直接拒绝:

java 复制代码
// 没有多态:调用方扛着所有类型的判断
class PaymentService {
    void pay(Wallet w, BigDecimal amt) {
        // 判断①:是什么类型
        if (w instanceof NormalWallet) {
            NormalWallet nw = (NormalWallet) w;
            if (nw.balance().compareTo(amt) < 0)            // 判断②:普通钱包检查
                throw new InsufficientBalanceException();
            nw.decrease(amt);
        } else if (w instanceof CreditWallet) {
            CreditWallet cw = (CreditWallet) w;
            if (cw.balance().subtract(amt).compareTo(new BigDecimal("-500")) < 0)  // 判断③:信用钱包检查
                throw new InsufficientBalanceException();
            cw.decrease(amt);
        }
        // 判断④:明天新加 FrozenWallet → 回来改这段,散弹式修改
    }
}

多态后:

java 复制代码
// 多态:调用方一行都不需要认识子类型
class PaymentService {
    void pay(Wallet w, BigDecimal amt) {
        w.consume(amt);   // 不管你是普通、信用、冻结,永远这一行
    }
}

把两张图放一起,多态的优势立刻浮现:

场景 没多态 有多态
加一种新钱包(如 VIP 钱包) 搜全项目所有 instanceof Wallet,逐一加分支 新增一个 VIPWallet 子类,零处改动调用方
改一条业务规则(如信用额度 -500 → -1000) 找到对应 else if 分支,改判定条件 只改 CreditWallet.isAllowed 一处
删一种钱包 删类 + 删所有 instanceof 分支(漏一处 = bug) 删类即可,调用方编译不通过会主动提醒你
测试 每个 if 分支要单独造 state 才能触发 new CreditWallet().consume(600),一行就是一条用例

多态消除的不只是分支,是"类型差异向调用方的泄漏"。 调用方从"认识每一类钱包的细节"退回到"只认识 Wallet 这一个抽象概念",新增和删除钱包的影响面收敛到 0。

7.2 三种形态

flowchart TB 多态 --> 子类型多态[子类型多态<br/>运行时·虚函数] 多态 --> 参数多态[参数多态<br/>编译时·泛型] 多态 --> 特设多态[特设多态<br/>编译时·重载]
形态 例子 时机
子类型多态 Animal a = new Cat(); a.speak(); 运行时
参数多态 List<T>、C++ 模板 编译时
特设多态 函数重载、运算符重载 编译时

7.3 实现原理

子类型多态依赖延迟绑定,把"调哪个函数"推到运行时:

makefile 复制代码
早绑定: call 0x401000   ← 编译期写死
晚绑定: call [vptr+0]   ← 运行时查表

JIT/V8 还能进一步优化:内联缓存(Inline Cache) 把"高频路径"的虚调用降到接近直接调用的开销。

7.4 多态的代价

代价 C++ Java JS
内存 每对象多 8B vptr 对象头自带类型 隐藏类自带原型
调用 2 次内存间接寻址 类似,可 JIT 内联 首次慢,IC 后接近直调
分支预测 不友好 JIT 反馈优化 IC 可预测

没有银弹:多态用一次次间接跳转换来了扩展性;当性能极致敏感(紧内层循环、SIMD 段),刻意去多态也是合理工程选择。


8.特性总结与延展

特性 核心问题 关键机制
封装 不变量守卫 访问控制 + 业务方法
抽象 隐藏实现 接口/抽象类/函数
继承 类型层级与复用 vtable/原型链
多态 消除条件分支 延迟绑定

四大特性不是平行关系,而是层层依赖

flowchart LR 封装 --> 抽象 抽象 --> 继承 抽象 --> 多态 继承 --> 多态 多态 --> 设计模式
  • 封装是地基,没有封装,其他三个都建在沙上;
  • 抽象是骨架,没有抽象,继承和多态都失去意义;
  • 继承是肌肉,它提供类型层级,但本身不是目的;
  • 多态是灵魂,一切设计模式的终点都是"用统一接口处理一族变化"。

四大特性是面向对象的语法层基础,但只懂特性还不够。真正的工程难题是:


9.综合实战案例

延续主线案例 ,01 篇我们造了 Order,本篇要造一只 Wallet,作为后续支付环节的基石。

9.1 钱包再升级需求

电商系统接入"账户钱包"。PM 给的需求清单:

markdown 复制代码
1. 用户可以充值、消费、退款
2. 余额不能为负
3. 必须能追溯每一笔流水(不可丢失日志)
4. 后期要支持"信用账户"(允许临时透支至 -500)
5. 后期要支持"冻结账户"(只能查看,不能动钱)

9.2 一行 public 引发的崩塌

工程师 A 写了第一版:

java 复制代码
public class Wallet {
    public BigDecimal balance;          // ① public
    public List<TxLog> txLogs;          // ① public

    public void recharge(BigDecimal amt) { balance = balance.add(amt); txLogs.add(...); }
    public void consume(BigDecimal amt) { balance = balance.subtract(amt); txLogs.add(...); }
}

两周后:

接连发生的事 根因
客服直接 wallet.balance = wallet.balance.add(refund),绕过日志 ① 字段 public,没人守"流水必须同步"
月底某条流水偷偷被删,wallet.txLogs.remove(0) ① 集合内部状态泄漏
接 SDK 时崩溃,SDK 期望"余额变化必发事件",钱包压根没设计事件机制 没有抽象层,扩展点缺失
加"信用账户"要写 if (isCreditAccount) 满世界改 没有多态,类型差异散在调用方

四个事故,四个特性都缺席

9.3 四大特性逐层加固

第 1 层:封装,把"钱"从字段变成"有守门人的资源"

java 复制代码
public class Wallet {
    private BigDecimal balance;
    private final List<TxLog> txLogs = new ArrayList<>();

    public BigDecimal balance() { return balance; }
    public List<TxLog> txLogs() { return List.copyOf(txLogs); }   // 防御式只读

    private void recordAndApply(TxType type, BigDecimal amt) {
        // 不变量校验集中在这里
        BigDecimal after = type == TxType.IN ? balance.add(amt) : balance.subtract(amt);
        if (!isAllowed(after)) throw new InsufficientBalanceException();
        balance = after;
        txLogs.add(new TxLog(type, amt, Instant.now()));
    }
    protected boolean isAllowed(BigDecimal after) { return after.signum() >= 0; }
}

关键变化:balance 不再是被改的对象,而是"被守护的不变量";txLogs 通过防御性拷贝杜绝外部破坏。至此,第 ① 类事故彻底消失

第 2 层:抽象,把"扩展点"从 if-else 变成方法重写

isAllowed 设为 protected,这是一个抽象层级 :子类可以改写规则,但主流程被锁死

第 3 层:继承,派生 CreditWallet 表示"信用账户"

java 复制代码
public class CreditWallet extends Wallet {
    private static final BigDecimal CREDIT_LIMIT = new BigDecimal("-500");
    @Override protected boolean isAllowed(BigDecimal after) {
        return after.compareTo(CREDIT_LIMIT) >= 0;
    }
}

注意:不是为了"复用代码"才继承 ,是因为 CreditWallet 在概念上就是 Wallet 的子类型 (满足里氏替换:能装进 Wallet 的地方都能装它)。

第 4 层:多态,让支付服务永远只看到 Wallet

java 复制代码
public class PaymentService {
    public void pay(Wallet wallet, BigDecimal amt) {
        wallet.consume(amt);   // 不知道是普通钱包还是信用钱包,也无需知道
    }
}

新增 FrozenWallet(冻结账户)只需再写一个子类,PaymentService 一行不改,这就是上一篇思考题🟡的答案。

9.4 类图与不变量清单

classDiagram class Wallet { <> -BigDecimal balance -List~TxLog~ txLogs +balance() BigDecimal +txLogs() List +recharge(amt) +consume(amt) #isAllowed(after) bool } class CreditWallet { #isAllowed(after) bool } class FrozenWallet { #isAllowed(after) bool } Wallet <|-- CreditWallet Wallet <|-- FrozenWallet

钱包的不变量清单,这才是封装真正"封装"的东西:

# 不变量 谁来守
I1 余额变化必须有对应流水 recordAndApply 私有方法
I2 普通账户余额 ≥ 0 Wallet#isAllowed
I3 信用账户余额 ≥ -500 CreditWallet#isAllowed
I4 流水日志只增不删 txLogs() 返回不可变拷贝
I5 任何外部访问都不能直接改 balance 字段 private 关键字

写下这张表的瞬间,你会突然发现:这就是 11 篇 DDD 里"聚合根"的雏形,本篇的封装+抽象,是第 11 篇的伏笔。

9.5 留下三道思考题

答案在第 03 篇开头揭晓。

🟢 易 :第 8.3 节里 isAllowed 我用了 protected。如果改成 private,会出现什么问题?

🟡 中CreditWallet 用了继承 实现"信用规则"。如果换成组合 ,给 Wallet 注入一个 BalanceRule 策略,你倾向哪种?说出取舍标准。

🔴 难 :当前 recordAndApply 是同步的。如果"流水"必须事务性写入数据库,封装应该如何演化?需要在抽象层引入什么?这道题就是第 03 篇要回答的"接口 vs 抽象类"的真实战场。


10.认知跃迁总结

回到开篇的转账事故。如果当初 Account 是这样写的:

java 复制代码
public class Account {
    private Money balance;
    public void transferTo(Account other, Money amount) {
        // 校验、扣款、加款、记日志,全在一个边界内
    }
}

那 ¥1273.45 的对账差额根本没机会发生,不是因为代码更优雅,而是因为不变量有了守门人

一句话送给你:

OOP 不是"用类组织代码",而是"用类划清责任边界"。封装是边界,抽象是边界的语义,继承是边界的层级,多态是边界外的统一视角。

下一篇 03.接口vs抽象类比较 将从一次"日志器被改了 47 次"的事故出发,回答抽象到底该用接口还是抽象类,并揭晓本篇 §8.5 的三道思考题。

推荐 编程进阶网 按 10 大板块组织技术内容:

板块 核心主题 直达
计算机 计算机原理、操作系统、网络协议、数据库原理 12 篇
编程 面向对象、SOLID 原则、23 种设计模式、系统架构 40+ 篇
算法 数据结构导论 → 线性结构详解 → 树哈希结构论 → 容器设计实战 → 经典算法思想 → 实战 30+ 篇
内功 序卷方法论 → 数据的本质 → 运行时模型 → 并发的设计 → 内存的真相 → 交互和系统 50+ 篇
CodeX C / C++ / Java / Go / JavaScript 五语言从入门到精通 100+ 篇
Apps Android / iOS / Web / Linux QML 实战开发 60+ 篇
ScriptHub Python / Shell-Bash 脚本与自动化 20+ 篇
工具 18+ 在线开发工具,纯浏览器端运算 ---
相关推荐
杨充1 小时前
7.SOLID原则案例汇
设计模式·开源·全栈
杨充1 小时前
8.反模式与坏味道
设计模式·开源·代码规范
杨充1 小时前
3.接口vs抽象类比较
设计模式
咖啡八杯4 小时前
文法、BNF与AST
java·设计模式·解释器模式·ast·文法
咖啡八杯13 小时前
GoF设计模式——解释器模式
java·后端·spring·设计模式
大数据新鸟15 小时前
设计模式四大基本原则
设计模式
南一Nanyi21 小时前
依赖注入和控制反转
前端·设计模式·nestjs
tiankong12131 天前
如何拓展多态下的子类
设计模式·c#