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→ 因为Activity是interface,新规则只需加一个实现类。根因:抽象+多态同时在场。 - 🔴 三个方案选什么 → 取决于"编排谁说了算":
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);
}
看起来人畜无害?它至少违反了四大特性中的三个:
| 现象 | 缺失的特性 |
|---|---|
调用方能直接改 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),读者会被"设计复杂度"吸走注意力,看不清每个特性本身的味道。
所以换一个最小的样本:一只钱包 ,只有 balance 和 balanceLastModifiedTime 两个字段。在这个玩具级的例子上,四个特性的"有没有"差别会像放大镜一样清楚。
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 四大特性关系
封装是地基 :没有边界,其余三者无从谈起; 抽象是骨架 :定义"做什么"; 继承+多态是肌肉:让骨架在不同形态下复用与变形。
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 不变量守卫
封装解决的核心问题有三类:
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 抽象的动机
封装藏数据,抽象藏实现,调用者只需知道"能做什么",无需知道"怎么做"。
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) 的数学本质:子类必须能替换父类,不破坏程序正确性。
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 内部流程,CreditWallet、FrozenWallet 全被牵连 |
| 层级爆炸 | 再加"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 三种形态
| 形态 | 例子 | 时机 |
|---|---|---|
| 子类型多态 | 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/原型链 |
| 多态 | 消除条件分支 | 延迟绑定 |
四大特性不是平行关系,而是层层依赖:
- 封装是地基,没有封装,其他三个都建在沙上;
- 抽象是骨架,没有抽象,继承和多态都失去意义;
- 继承是肌肉,它提供类型层级,但本身不是目的;
- 多态是灵魂,一切设计模式的终点都是"用统一接口处理一族变化"。
四大特性是面向对象的语法层基础,但只懂特性还不够。真正的工程难题是:
- 何时用接口、何时用抽象类? → 03.接口vs抽象类比较
- 如何让代码不锁死在某个实现上? → 04.接口而非实现编程
- 继承怎么用才不翻车? → 05.多用组合和少继承
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 类图与不变量清单
钱包的不变量清单,这才是封装真正"封装"的东西:
| # | 不变量 | 谁来守 |
|---|---|---|
| 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+ 在线开发工具,纯浏览器端运算 | --- |