编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 面向对象设计

    • 0.面向对象设计导航
    • 1.面向对象设计思想
    • 2.面向对象特性思考
      • 1.先回答上篇思考题
        • 1.1 上篇遗留三道题
        • 1.2 三道题的共同根
        • 1.3 一次余额对账事故
        • 1.4 灵魂五连问
      • 2.从一个 Bug 说起
        • 2.1 钱包翻车现场
        • 2.2 Bug 的根因
        • 2.3 四大特性登场
      • 3.特性全景图
        • 3.1 四大特性关系
        • 3.2 三大还是四大
      • 4.封装特性
        • 4.1 封装的动机
        • 4.2 钱包封装版
        • 4.3 不变量守卫
        • 4.4 封装的层次
        • 4.5 实现原理
        • 4.6 好坏封装对比
      • 5.抽象特性
        • 5.1 抽象的动机
        • 5.2 接口与抽象类
        • 5.3 抽象的层级
        • 5.4 命名即抽象
      • 6.继承特性
        • 6.1 继承的动机
        • 6.2 类型层级与里氏替换
        • 6.3 实现原理
        • 6.4 继承的代价
      • 7.多态特性
        • 7.1 多态的动机
        • 7.2 三种形态
        • 7.3 实现原理
        • 7.4 多态的代价
      • 8.特性总结与延展
      • 9.综合实战案例
        • 9.1 钱包再升级需求
        • 9.2 一行 public 引发的崩塌
        • 9.3 四大特性逐层加固
        • 9.4 类图与不变量清单
        • 9.5 留下三道思考题
      • 10.认知跃迁总结
    • 3.接口vs抽象类比较
    • 4.接口而非实现编程
    • 5.多用组合和少继承
    • 6.设计原则的全景图
    • 7.SOLID原则案例汇
    • 8.反模式与坏味道
    • 9.重构十二式的实战
    • 10.可测试性实战设计
    • 11.DDD与战术的建模
    • 12.综合实战图片框架
    • 13.面向对象快速总结
  • 常见设计原则

  • 巧学设计模式

  • 系统架构设计

  • 程序员精进路

  • 编程
  • 面向对象设计
杨充
2019-04-11
目录

2.面向对象特性思考

# 2.面向对象的特性

# 目录介绍

  • 1.先回答上篇思考题
    • 1.1 上篇遗留三道题
    • 1.2 三道题的共同根
    • 1.3 一次余额对账事故
    • 1.4 灵魂五连问
  • 2.从一个 Bug 说起
    • 2.1 钱包翻车现场
    • 2.2 Bug 的根因
    • 2.3 四大特性登场
  • 3.特性全景图
    • 3.1 四大特性关系
    • 3.2 三大还是四大
  • 4.封装特性
    • 4.1 封装的动机
    • 4.2 钱包封装版
    • 4.3 不变量守卫
    • 4.4 封装的层次
    • 4.5 实现原理
    • 4.6 好坏封装对比
  • 5.抽象特性
    • 5.1 抽象的动机
    • 5.2 接口与抽象类
    • 5.3 抽象的层级
    • 5.4 命名即抽象
  • 6.继承特性
    • 6.1 继承的动机
    • 6.2 类型层级
    • 6.3 实现原理
    • 6.4 继承的代价
  • 7.多态特性
    • 7.1 多态的动机
    • 7.2 三种形态
    • 7.3 实现原理
    • 7.4 多态的代价
  • 8.特性总结与延展
  • 9.综合实战案例
    • 9.1 钱包再升级需求
    • 9.2 一行 public 引发的崩塌
    • 9.3 四大特性逐层加固
    • 9.4 类图与不变量清单
    • 9.5 留下三道思考题
  • 09.认知跃迁总结

# 1.先回答上篇思考题

# 1.1 上篇遗留三道题

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

  • 🟢 把 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。

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

// 事故代码 · 脱敏后
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 灵魂五连问

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

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 还是上周的时间"。排查代码:

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 钱包封装版

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 封装的层次

封装不仅在类一级:

系统层:微服务以 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 好坏封装对比

// ✗ 坏封装: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 双写,底层三天一换,调用方不能跟着改。这就需要用接口把"持久化"这件事抽象掉:

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 抽象的层级

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

高层抽象:业务概念(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 类型层级与里氏替换

继承构造的是子类型关系:

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 为例:

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,冻结钱包直接拒绝:

// 没有多态:调用方扛着所有类型的判断
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 → 回来改这段,散弹式修改
    }
}

多态后:

// 多态:调用方一行都不需要认识子类型
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 实现原理

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

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

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

  • 何时用接口、何时用抽象类? → 03.接口vs抽象类比较 (opens new window)
  • 如何让代码不锁死在某个实现上? → 04.接口而非实现编程 (opens new window)
  • 继承怎么用才不翻车? → 05.多用组合和少继承 (opens new window)

# 9.综合实战案例

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

# 9.1 钱包再升级需求

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

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

# 9.2 一行 public 引发的崩塌

工程师 A 写了第一版:

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 层:封装,把"钱"从字段变成"有守门人的资源"

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 表示"信用账户"

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

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

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

# 9.4 类图与不变量清单

classDiagram
    class Wallet {
        <<abstract behavior>>
        -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 是这样写的:

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

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

一句话送给你:

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

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

上次更新: 2026/07/15, 11:27:34
1.面向对象设计思想
3.接口vs抽象类比较

← 1.面向对象设计思想 3.接口vs抽象类比较→

最近更新
01
audit
07-27
02
C++入门教程全章思考题汇编
07-24
03
12.技术团队建设能力
07-21
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式