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+ 在线开发工具,纯浏览器端运算 ---
相关推荐
剧中有戏16 小时前
抽象工厂模式从入门到精通:一个多云存储案例的完整剖析(修订版)
设计模式
MC皮蛋侠客19 小时前
Redis 系列(八):缓存设计模式与一致性——从 Cache Aside 到防雪崩
redis·缓存·设计模式
莫得感情 o21 小时前
设计模式 18 · 状态模式
设计模式·状态模式
莫得感情 o21 小时前
设计模式 17 · 责任链模式
设计模式·责任链模式
用户938515635071 天前
从零在浏览器里跑 DeepSeek-R1:WebGPU + Transformer.js 全链路实战
前端·设计模式·typescript
AI人工智能+电脑小能手1 天前
大白话说Java设计模式-04-工厂方法模式(业务实战篇)
java·设计模式·工厂方法模式·架构设计·代码解耦
今天的砖头有点烫手啊2 天前
AI Agent 开发实战(十):Agent 设计模式(ReAct / Plan-Execute / Reflection)
人工智能·react.js·设计模式
董员外2 天前
RAG 系统进化论(十):可运营 RAG,从原型到长期运行的知识系统
人工智能·后端·设计模式
莫得感情 o2 天前
设计模式 15 · 模板方法模式
设计模式·模板方法模式