02.面向对象特性思考


title: 2.面向对象特性思考

date: 2019-04-11 16:57:09

permalink: /pages/f9fcfa/

categories:

  • 通用技术的提升
  • 重学设计模式
  • 面向对象设计
    tags:

author:

name: 杨充
link: https://yccoding.com/

2.面向对象的特性

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

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);
}

看起来人畜无害?它至少违反了四大特性中的三个
#mermaid-svg-8R3R5GSNgZiyLklV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8R3R5GSNgZiyLklV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8R3R5GSNgZiyLklV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8R3R5GSNgZiyLklV .error-icon{fill:#552222;}#mermaid-svg-8R3R5GSNgZiyLklV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8R3R5GSNgZiyLklV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8R3R5GSNgZiyLklV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8R3R5GSNgZiyLklV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8R3R5GSNgZiyLklV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8R3R5GSNgZiyLklV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8R3R5GSNgZiyLklV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8R3R5GSNgZiyLklV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8R3R5GSNgZiyLklV .marker.cross{stroke:#333333;}#mermaid-svg-8R3R5GSNgZiyLklV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8R3R5GSNgZiyLklV p{margin:0;}#mermaid-svg-8R3R5GSNgZiyLklV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-8R3R5GSNgZiyLklV .cluster-label text{fill:#333;}#mermaid-svg-8R3R5GSNgZiyLklV .cluster-label span{color:#333;}#mermaid-svg-8R3R5GSNgZiyLklV .cluster-label span p{background-color:transparent;}#mermaid-svg-8R3R5GSNgZiyLklV .label text,#mermaid-svg-8R3R5GSNgZiyLklV span{fill:#333;color:#333;}#mermaid-svg-8R3R5GSNgZiyLklV .node rect,#mermaid-svg-8R3R5GSNgZiyLklV .node circle,#mermaid-svg-8R3R5GSNgZiyLklV .node ellipse,#mermaid-svg-8R3R5GSNgZiyLklV .node polygon,#mermaid-svg-8R3R5GSNgZiyLklV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8R3R5GSNgZiyLklV .rough-node .label text,#mermaid-svg-8R3R5GSNgZiyLklV .node .label text,#mermaid-svg-8R3R5GSNgZiyLklV .image-shape .label,#mermaid-svg-8R3R5GSNgZiyLklV .icon-shape .label{text-anchor:middle;}#mermaid-svg-8R3R5GSNgZiyLklV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-8R3R5GSNgZiyLklV .rough-node .label,#mermaid-svg-8R3R5GSNgZiyLklV .node .label,#mermaid-svg-8R3R5GSNgZiyLklV .image-shape .label,#mermaid-svg-8R3R5GSNgZiyLklV .icon-shape .label{text-align:center;}#mermaid-svg-8R3R5GSNgZiyLklV .node.clickable{cursor:pointer;}#mermaid-svg-8R3R5GSNgZiyLklV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-8R3R5GSNgZiyLklV .arrowheadPath{fill:#333333;}#mermaid-svg-8R3R5GSNgZiyLklV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-8R3R5GSNgZiyLklV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-8R3R5GSNgZiyLklV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8R3R5GSNgZiyLklV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-8R3R5GSNgZiyLklV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8R3R5GSNgZiyLklV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-8R3R5GSNgZiyLklV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8R3R5GSNgZiyLklV .cluster text{fill:#333;}#mermaid-svg-8R3R5GSNgZiyLklV .cluster span{color:#333;}#mermaid-svg-8R3R5GSNgZiyLklV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-8R3R5GSNgZiyLklV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-8R3R5GSNgZiyLklV rect.text{fill:none;stroke-width:0;}#mermaid-svg-8R3R5GSNgZiyLklV .icon-shape,#mermaid-svg-8R3R5GSNgZiyLklV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8R3R5GSNgZiyLklV .icon-shape p,#mermaid-svg-8R3R5GSNgZiyLklV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-8R3R5GSNgZiyLklV .icon-shape .label rect,#mermaid-svg-8R3R5GSNgZiyLklV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8R3R5GSNgZiyLklV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-8R3R5GSNgZiyLklV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-8R3R5GSNgZiyLklV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 写


没有
没有
没有
transfer 函数
from.balance
to.balance
recordLog
出错时

谁守约束
余额可以是负数
扣款成功+加款失败=钱凭空消失
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),读者会被"设计复杂度"吸走注意力,看不清每个特性本身的味道。

所以换一个最小的样本:一只钱包 ,只有 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 四大特性关系

#mermaid-svg-DpSkJg6rP3yNKWTB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DpSkJg6rP3yNKWTB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DpSkJg6rP3yNKWTB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DpSkJg6rP3yNKWTB .error-icon{fill:#552222;}#mermaid-svg-DpSkJg6rP3yNKWTB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DpSkJg6rP3yNKWTB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DpSkJg6rP3yNKWTB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DpSkJg6rP3yNKWTB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DpSkJg6rP3yNKWTB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DpSkJg6rP3yNKWTB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DpSkJg6rP3yNKWTB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DpSkJg6rP3yNKWTB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DpSkJg6rP3yNKWTB .marker.cross{stroke:#333333;}#mermaid-svg-DpSkJg6rP3yNKWTB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DpSkJg6rP3yNKWTB p{margin:0;}#mermaid-svg-DpSkJg6rP3yNKWTB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-DpSkJg6rP3yNKWTB .cluster-label text{fill:#333;}#mermaid-svg-DpSkJg6rP3yNKWTB .cluster-label span{color:#333;}#mermaid-svg-DpSkJg6rP3yNKWTB .cluster-label span p{background-color:transparent;}#mermaid-svg-DpSkJg6rP3yNKWTB .label text,#mermaid-svg-DpSkJg6rP3yNKWTB span{fill:#333;color:#333;}#mermaid-svg-DpSkJg6rP3yNKWTB .node rect,#mermaid-svg-DpSkJg6rP3yNKWTB .node circle,#mermaid-svg-DpSkJg6rP3yNKWTB .node ellipse,#mermaid-svg-DpSkJg6rP3yNKWTB .node polygon,#mermaid-svg-DpSkJg6rP3yNKWTB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DpSkJg6rP3yNKWTB .rough-node .label text,#mermaid-svg-DpSkJg6rP3yNKWTB .node .label text,#mermaid-svg-DpSkJg6rP3yNKWTB .image-shape .label,#mermaid-svg-DpSkJg6rP3yNKWTB .icon-shape .label{text-anchor:middle;}#mermaid-svg-DpSkJg6rP3yNKWTB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DpSkJg6rP3yNKWTB .rough-node .label,#mermaid-svg-DpSkJg6rP3yNKWTB .node .label,#mermaid-svg-DpSkJg6rP3yNKWTB .image-shape .label,#mermaid-svg-DpSkJg6rP3yNKWTB .icon-shape .label{text-align:center;}#mermaid-svg-DpSkJg6rP3yNKWTB .node.clickable{cursor:pointer;}#mermaid-svg-DpSkJg6rP3yNKWTB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DpSkJg6rP3yNKWTB .arrowheadPath{fill:#333333;}#mermaid-svg-DpSkJg6rP3yNKWTB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DpSkJg6rP3yNKWTB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DpSkJg6rP3yNKWTB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DpSkJg6rP3yNKWTB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DpSkJg6rP3yNKWTB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DpSkJg6rP3yNKWTB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DpSkJg6rP3yNKWTB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DpSkJg6rP3yNKWTB .cluster text{fill:#333;}#mermaid-svg-DpSkJg6rP3yNKWTB .cluster span{color:#333;}#mermaid-svg-DpSkJg6rP3yNKWTB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-DpSkJg6rP3yNKWTB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DpSkJg6rP3yNKWTB rect.text{fill:none;stroke-width:0;}#mermaid-svg-DpSkJg6rP3yNKWTB .icon-shape,#mermaid-svg-DpSkJg6rP3yNKWTB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DpSkJg6rP3yNKWTB .icon-shape p,#mermaid-svg-DpSkJg6rP3yNKWTB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DpSkJg6rP3yNKWTB .icon-shape .label rect,#mermaid-svg-DpSkJg6rP3yNKWTB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DpSkJg6rP3yNKWTB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DpSkJg6rP3yNKWTB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DpSkJg6rP3yNKWTB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 封装

建立边界
抽象

隐藏实现
继承

层级关系
多态

统一接口

封装是地基 :没有边界,其余三者无从谈起;

抽象是骨架 :定义"做什么";

继承+多态是肌肉:让骨架在不同形态下复用与变形。

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 不变量守卫

封装解决的核心问题有三类:
#mermaid-svg-oijatV8nnwL7wo5P{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-oijatV8nnwL7wo5P .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oijatV8nnwL7wo5P .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oijatV8nnwL7wo5P .error-icon{fill:#552222;}#mermaid-svg-oijatV8nnwL7wo5P .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oijatV8nnwL7wo5P .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oijatV8nnwL7wo5P .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oijatV8nnwL7wo5P .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oijatV8nnwL7wo5P .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oijatV8nnwL7wo5P .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oijatV8nnwL7wo5P .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oijatV8nnwL7wo5P .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oijatV8nnwL7wo5P .marker.cross{stroke:#333333;}#mermaid-svg-oijatV8nnwL7wo5P svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oijatV8nnwL7wo5P p{margin:0;}#mermaid-svg-oijatV8nnwL7wo5P .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-oijatV8nnwL7wo5P .cluster-label text{fill:#333;}#mermaid-svg-oijatV8nnwL7wo5P .cluster-label span{color:#333;}#mermaid-svg-oijatV8nnwL7wo5P .cluster-label span p{background-color:transparent;}#mermaid-svg-oijatV8nnwL7wo5P .label text,#mermaid-svg-oijatV8nnwL7wo5P span{fill:#333;color:#333;}#mermaid-svg-oijatV8nnwL7wo5P .node rect,#mermaid-svg-oijatV8nnwL7wo5P .node circle,#mermaid-svg-oijatV8nnwL7wo5P .node ellipse,#mermaid-svg-oijatV8nnwL7wo5P .node polygon,#mermaid-svg-oijatV8nnwL7wo5P .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oijatV8nnwL7wo5P .rough-node .label text,#mermaid-svg-oijatV8nnwL7wo5P .node .label text,#mermaid-svg-oijatV8nnwL7wo5P .image-shape .label,#mermaid-svg-oijatV8nnwL7wo5P .icon-shape .label{text-anchor:middle;}#mermaid-svg-oijatV8nnwL7wo5P .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-oijatV8nnwL7wo5P .rough-node .label,#mermaid-svg-oijatV8nnwL7wo5P .node .label,#mermaid-svg-oijatV8nnwL7wo5P .image-shape .label,#mermaid-svg-oijatV8nnwL7wo5P .icon-shape .label{text-align:center;}#mermaid-svg-oijatV8nnwL7wo5P .node.clickable{cursor:pointer;}#mermaid-svg-oijatV8nnwL7wo5P .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-oijatV8nnwL7wo5P .arrowheadPath{fill:#333333;}#mermaid-svg-oijatV8nnwL7wo5P .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-oijatV8nnwL7wo5P .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-oijatV8nnwL7wo5P .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oijatV8nnwL7wo5P .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-oijatV8nnwL7wo5P .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oijatV8nnwL7wo5P .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-oijatV8nnwL7wo5P .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-oijatV8nnwL7wo5P .cluster text{fill:#333;}#mermaid-svg-oijatV8nnwL7wo5P .cluster span{color:#333;}#mermaid-svg-oijatV8nnwL7wo5P div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-oijatV8nnwL7wo5P .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-oijatV8nnwL7wo5P rect.text{fill:none;stroke-width:0;}#mermaid-svg-oijatV8nnwL7wo5P .icon-shape,#mermaid-svg-oijatV8nnwL7wo5P .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oijatV8nnwL7wo5P .icon-shape p,#mermaid-svg-oijatV8nnwL7wo5P .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-oijatV8nnwL7wo5P .icon-shape .label rect,#mermaid-svg-oijatV8nnwL7wo5P .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oijatV8nnwL7wo5P .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-oijatV8nnwL7wo5P .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-oijatV8nnwL7wo5P :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 封装价值
不变量保护

状态永远合法
变更隔离

实现可换不影响调用
认知降负

只看接口不看内部

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 好坏封装对比

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 抽象的动机

封装藏数据,抽象藏实现,调用者只需知道"能做什么",无需知道"怎么做"。
#mermaid-svg-0TPevnvl9UYwmwnO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0TPevnvl9UYwmwnO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0TPevnvl9UYwmwnO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0TPevnvl9UYwmwnO .error-icon{fill:#552222;}#mermaid-svg-0TPevnvl9UYwmwnO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0TPevnvl9UYwmwnO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0TPevnvl9UYwmwnO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0TPevnvl9UYwmwnO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0TPevnvl9UYwmwnO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0TPevnvl9UYwmwnO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0TPevnvl9UYwmwnO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0TPevnvl9UYwmwnO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0TPevnvl9UYwmwnO .marker.cross{stroke:#333333;}#mermaid-svg-0TPevnvl9UYwmwnO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0TPevnvl9UYwmwnO p{margin:0;}#mermaid-svg-0TPevnvl9UYwmwnO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0TPevnvl9UYwmwnO .cluster-label text{fill:#333;}#mermaid-svg-0TPevnvl9UYwmwnO .cluster-label span{color:#333;}#mermaid-svg-0TPevnvl9UYwmwnO .cluster-label span p{background-color:transparent;}#mermaid-svg-0TPevnvl9UYwmwnO .label text,#mermaid-svg-0TPevnvl9UYwmwnO span{fill:#333;color:#333;}#mermaid-svg-0TPevnvl9UYwmwnO .node rect,#mermaid-svg-0TPevnvl9UYwmwnO .node circle,#mermaid-svg-0TPevnvl9UYwmwnO .node ellipse,#mermaid-svg-0TPevnvl9UYwmwnO .node polygon,#mermaid-svg-0TPevnvl9UYwmwnO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0TPevnvl9UYwmwnO .rough-node .label text,#mermaid-svg-0TPevnvl9UYwmwnO .node .label text,#mermaid-svg-0TPevnvl9UYwmwnO .image-shape .label,#mermaid-svg-0TPevnvl9UYwmwnO .icon-shape .label{text-anchor:middle;}#mermaid-svg-0TPevnvl9UYwmwnO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0TPevnvl9UYwmwnO .rough-node .label,#mermaid-svg-0TPevnvl9UYwmwnO .node .label,#mermaid-svg-0TPevnvl9UYwmwnO .image-shape .label,#mermaid-svg-0TPevnvl9UYwmwnO .icon-shape .label{text-align:center;}#mermaid-svg-0TPevnvl9UYwmwnO .node.clickable{cursor:pointer;}#mermaid-svg-0TPevnvl9UYwmwnO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0TPevnvl9UYwmwnO .arrowheadPath{fill:#333333;}#mermaid-svg-0TPevnvl9UYwmwnO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0TPevnvl9UYwmwnO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0TPevnvl9UYwmwnO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0TPevnvl9UYwmwnO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0TPevnvl9UYwmwnO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0TPevnvl9UYwmwnO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0TPevnvl9UYwmwnO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0TPevnvl9UYwmwnO .cluster text{fill:#333;}#mermaid-svg-0TPevnvl9UYwmwnO .cluster span{color:#333;}#mermaid-svg-0TPevnvl9UYwmwnO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0TPevnvl9UYwmwnO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0TPevnvl9UYwmwnO rect.text{fill:none;stroke-width:0;}#mermaid-svg-0TPevnvl9UYwmwnO .icon-shape,#mermaid-svg-0TPevnvl9UYwmwnO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0TPevnvl9UYwmwnO .icon-shape p,#mermaid-svg-0TPevnvl9UYwmwnO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0TPevnvl9UYwmwnO .icon-shape .label rect,#mermaid-svg-0TPevnvl9UYwmwnO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0TPevnvl9UYwmwnO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0TPevnvl9UYwmwnO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0TPevnvl9UYwmwnO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 关心 what
不关心 how
调用方
抽象接口
实现 A
实现 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 抽象的层级

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

复制代码
高层抽象:业务概念(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) 的数学本质:子类必须能替换父类,不破坏程序正确性。
#mermaid-svg-8nSNcaNbCHtRWOdC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8nSNcaNbCHtRWOdC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8nSNcaNbCHtRWOdC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8nSNcaNbCHtRWOdC .error-icon{fill:#552222;}#mermaid-svg-8nSNcaNbCHtRWOdC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8nSNcaNbCHtRWOdC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8nSNcaNbCHtRWOdC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8nSNcaNbCHtRWOdC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8nSNcaNbCHtRWOdC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8nSNcaNbCHtRWOdC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8nSNcaNbCHtRWOdC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8nSNcaNbCHtRWOdC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8nSNcaNbCHtRWOdC .marker.cross{stroke:#333333;}#mermaid-svg-8nSNcaNbCHtRWOdC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8nSNcaNbCHtRWOdC p{margin:0;}#mermaid-svg-8nSNcaNbCHtRWOdC g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-8nSNcaNbCHtRWOdC g.classGroup text .title{font-weight:bolder;}#mermaid-svg-8nSNcaNbCHtRWOdC .cluster-label text{fill:#333;}#mermaid-svg-8nSNcaNbCHtRWOdC .cluster-label span{color:#333;}#mermaid-svg-8nSNcaNbCHtRWOdC .cluster-label span p{background-color:transparent;}#mermaid-svg-8nSNcaNbCHtRWOdC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8nSNcaNbCHtRWOdC .cluster text{fill:#333;}#mermaid-svg-8nSNcaNbCHtRWOdC .cluster span{color:#333;}#mermaid-svg-8nSNcaNbCHtRWOdC .nodeLabel,#mermaid-svg-8nSNcaNbCHtRWOdC .edgeLabel{color:#131300;}#mermaid-svg-8nSNcaNbCHtRWOdC .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-8nSNcaNbCHtRWOdC .label text{fill:#131300;}#mermaid-svg-8nSNcaNbCHtRWOdC .labelBkg{background:#ECECFF;}#mermaid-svg-8nSNcaNbCHtRWOdC .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-8nSNcaNbCHtRWOdC .classTitle{font-weight:bolder;}#mermaid-svg-8nSNcaNbCHtRWOdC .node rect,#mermaid-svg-8nSNcaNbCHtRWOdC .node circle,#mermaid-svg-8nSNcaNbCHtRWOdC .node ellipse,#mermaid-svg-8nSNcaNbCHtRWOdC .node polygon,#mermaid-svg-8nSNcaNbCHtRWOdC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8nSNcaNbCHtRWOdC .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-8nSNcaNbCHtRWOdC g.clickable{cursor:pointer;}#mermaid-svg-8nSNcaNbCHtRWOdC g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-8nSNcaNbCHtRWOdC g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-8nSNcaNbCHtRWOdC .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-8nSNcaNbCHtRWOdC .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-8nSNcaNbCHtRWOdC .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-8nSNcaNbCHtRWOdC .dashed-line{stroke-dasharray:3;}#mermaid-svg-8nSNcaNbCHtRWOdC .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-8nSNcaNbCHtRWOdC #compositionStart,#mermaid-svg-8nSNcaNbCHtRWOdC .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-8nSNcaNbCHtRWOdC #compositionEnd,#mermaid-svg-8nSNcaNbCHtRWOdC .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-8nSNcaNbCHtRWOdC #dependencyStart,#mermaid-svg-8nSNcaNbCHtRWOdC .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-8nSNcaNbCHtRWOdC #dependencyStart,#mermaid-svg-8nSNcaNbCHtRWOdC .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-8nSNcaNbCHtRWOdC #extensionStart,#mermaid-svg-8nSNcaNbCHtRWOdC .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-8nSNcaNbCHtRWOdC #extensionEnd,#mermaid-svg-8nSNcaNbCHtRWOdC .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-8nSNcaNbCHtRWOdC #aggregationStart,#mermaid-svg-8nSNcaNbCHtRWOdC .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-8nSNcaNbCHtRWOdC #aggregationEnd,#mermaid-svg-8nSNcaNbCHtRWOdC .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-8nSNcaNbCHtRWOdC #lollipopStart,#mermaid-svg-8nSNcaNbCHtRWOdC .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-8nSNcaNbCHtRWOdC #lollipopEnd,#mermaid-svg-8nSNcaNbCHtRWOdC .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-8nSNcaNbCHtRWOdC .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-8nSNcaNbCHtRWOdC .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-8nSNcaNbCHtRWOdC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-8nSNcaNbCHtRWOdC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-8nSNcaNbCHtRWOdC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Wallet
-BigDecimal balance
-List<TxLog> txLogs
+consume(amt)
+balance() : BigDecimal
#isAllowed(after) : bool
CreditWallet
#isAllowed() : 覆写为「≥ -500」
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 内部流程,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 三种形态

#mermaid-svg-aSr1f8snKikCQ7Th{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aSr1f8snKikCQ7Th .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aSr1f8snKikCQ7Th .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aSr1f8snKikCQ7Th .error-icon{fill:#552222;}#mermaid-svg-aSr1f8snKikCQ7Th .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aSr1f8snKikCQ7Th .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aSr1f8snKikCQ7Th .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aSr1f8snKikCQ7Th .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aSr1f8snKikCQ7Th .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aSr1f8snKikCQ7Th .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aSr1f8snKikCQ7Th .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aSr1f8snKikCQ7Th .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aSr1f8snKikCQ7Th .marker.cross{stroke:#333333;}#mermaid-svg-aSr1f8snKikCQ7Th svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aSr1f8snKikCQ7Th p{margin:0;}#mermaid-svg-aSr1f8snKikCQ7Th .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-aSr1f8snKikCQ7Th .cluster-label text{fill:#333;}#mermaid-svg-aSr1f8snKikCQ7Th .cluster-label span{color:#333;}#mermaid-svg-aSr1f8snKikCQ7Th .cluster-label span p{background-color:transparent;}#mermaid-svg-aSr1f8snKikCQ7Th .label text,#mermaid-svg-aSr1f8snKikCQ7Th span{fill:#333;color:#333;}#mermaid-svg-aSr1f8snKikCQ7Th .node rect,#mermaid-svg-aSr1f8snKikCQ7Th .node circle,#mermaid-svg-aSr1f8snKikCQ7Th .node ellipse,#mermaid-svg-aSr1f8snKikCQ7Th .node polygon,#mermaid-svg-aSr1f8snKikCQ7Th .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-aSr1f8snKikCQ7Th .rough-node .label text,#mermaid-svg-aSr1f8snKikCQ7Th .node .label text,#mermaid-svg-aSr1f8snKikCQ7Th .image-shape .label,#mermaid-svg-aSr1f8snKikCQ7Th .icon-shape .label{text-anchor:middle;}#mermaid-svg-aSr1f8snKikCQ7Th .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-aSr1f8snKikCQ7Th .rough-node .label,#mermaid-svg-aSr1f8snKikCQ7Th .node .label,#mermaid-svg-aSr1f8snKikCQ7Th .image-shape .label,#mermaid-svg-aSr1f8snKikCQ7Th .icon-shape .label{text-align:center;}#mermaid-svg-aSr1f8snKikCQ7Th .node.clickable{cursor:pointer;}#mermaid-svg-aSr1f8snKikCQ7Th .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-aSr1f8snKikCQ7Th .arrowheadPath{fill:#333333;}#mermaid-svg-aSr1f8snKikCQ7Th .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-aSr1f8snKikCQ7Th .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-aSr1f8snKikCQ7Th .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aSr1f8snKikCQ7Th .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-aSr1f8snKikCQ7Th .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aSr1f8snKikCQ7Th .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-aSr1f8snKikCQ7Th .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-aSr1f8snKikCQ7Th .cluster text{fill:#333;}#mermaid-svg-aSr1f8snKikCQ7Th .cluster span{color:#333;}#mermaid-svg-aSr1f8snKikCQ7Th div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-aSr1f8snKikCQ7Th .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-aSr1f8snKikCQ7Th rect.text{fill:none;stroke-width:0;}#mermaid-svg-aSr1f8snKikCQ7Th .icon-shape,#mermaid-svg-aSr1f8snKikCQ7Th .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aSr1f8snKikCQ7Th .icon-shape p,#mermaid-svg-aSr1f8snKikCQ7Th .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-aSr1f8snKikCQ7Th .icon-shape .label rect,#mermaid-svg-aSr1f8snKikCQ7Th .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aSr1f8snKikCQ7Th .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-aSr1f8snKikCQ7Th .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-aSr1f8snKikCQ7Th :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 多态
子类型多态

运行时·虚函数
参数多态

编译时·泛型
特设多态

编译时·重载

形态 例子 时机
子类型多态 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/原型链
多态 消除条件分支 延迟绑定

四大特性不是平行关系,而是层层依赖
#mermaid-svg-0VLZkDNOITIK9KYW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0VLZkDNOITIK9KYW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0VLZkDNOITIK9KYW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0VLZkDNOITIK9KYW .error-icon{fill:#552222;}#mermaid-svg-0VLZkDNOITIK9KYW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0VLZkDNOITIK9KYW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0VLZkDNOITIK9KYW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0VLZkDNOITIK9KYW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0VLZkDNOITIK9KYW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0VLZkDNOITIK9KYW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0VLZkDNOITIK9KYW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0VLZkDNOITIK9KYW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0VLZkDNOITIK9KYW .marker.cross{stroke:#333333;}#mermaid-svg-0VLZkDNOITIK9KYW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0VLZkDNOITIK9KYW p{margin:0;}#mermaid-svg-0VLZkDNOITIK9KYW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0VLZkDNOITIK9KYW .cluster-label text{fill:#333;}#mermaid-svg-0VLZkDNOITIK9KYW .cluster-label span{color:#333;}#mermaid-svg-0VLZkDNOITIK9KYW .cluster-label span p{background-color:transparent;}#mermaid-svg-0VLZkDNOITIK9KYW .label text,#mermaid-svg-0VLZkDNOITIK9KYW span{fill:#333;color:#333;}#mermaid-svg-0VLZkDNOITIK9KYW .node rect,#mermaid-svg-0VLZkDNOITIK9KYW .node circle,#mermaid-svg-0VLZkDNOITIK9KYW .node ellipse,#mermaid-svg-0VLZkDNOITIK9KYW .node polygon,#mermaid-svg-0VLZkDNOITIK9KYW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0VLZkDNOITIK9KYW .rough-node .label text,#mermaid-svg-0VLZkDNOITIK9KYW .node .label text,#mermaid-svg-0VLZkDNOITIK9KYW .image-shape .label,#mermaid-svg-0VLZkDNOITIK9KYW .icon-shape .label{text-anchor:middle;}#mermaid-svg-0VLZkDNOITIK9KYW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0VLZkDNOITIK9KYW .rough-node .label,#mermaid-svg-0VLZkDNOITIK9KYW .node .label,#mermaid-svg-0VLZkDNOITIK9KYW .image-shape .label,#mermaid-svg-0VLZkDNOITIK9KYW .icon-shape .label{text-align:center;}#mermaid-svg-0VLZkDNOITIK9KYW .node.clickable{cursor:pointer;}#mermaid-svg-0VLZkDNOITIK9KYW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0VLZkDNOITIK9KYW .arrowheadPath{fill:#333333;}#mermaid-svg-0VLZkDNOITIK9KYW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0VLZkDNOITIK9KYW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0VLZkDNOITIK9KYW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0VLZkDNOITIK9KYW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0VLZkDNOITIK9KYW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0VLZkDNOITIK9KYW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0VLZkDNOITIK9KYW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0VLZkDNOITIK9KYW .cluster text{fill:#333;}#mermaid-svg-0VLZkDNOITIK9KYW .cluster span{color:#333;}#mermaid-svg-0VLZkDNOITIK9KYW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0VLZkDNOITIK9KYW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0VLZkDNOITIK9KYW rect.text{fill:none;stroke-width:0;}#mermaid-svg-0VLZkDNOITIK9KYW .icon-shape,#mermaid-svg-0VLZkDNOITIK9KYW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0VLZkDNOITIK9KYW .icon-shape p,#mermaid-svg-0VLZkDNOITIK9KYW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0VLZkDNOITIK9KYW .icon-shape .label rect,#mermaid-svg-0VLZkDNOITIK9KYW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0VLZkDNOITIK9KYW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0VLZkDNOITIK9KYW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0VLZkDNOITIK9KYW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 封装
抽象
继承
多态
设计模式

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

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


9.综合实战案例

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

9.1 钱包再升级需求

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

复制代码
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 类图与不变量清单

#mermaid-svg-dYl8X4bbpAAQnhG4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dYl8X4bbpAAQnhG4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dYl8X4bbpAAQnhG4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dYl8X4bbpAAQnhG4 .error-icon{fill:#552222;}#mermaid-svg-dYl8X4bbpAAQnhG4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dYl8X4bbpAAQnhG4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dYl8X4bbpAAQnhG4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dYl8X4bbpAAQnhG4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dYl8X4bbpAAQnhG4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dYl8X4bbpAAQnhG4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dYl8X4bbpAAQnhG4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dYl8X4bbpAAQnhG4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dYl8X4bbpAAQnhG4 .marker.cross{stroke:#333333;}#mermaid-svg-dYl8X4bbpAAQnhG4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dYl8X4bbpAAQnhG4 p{margin:0;}#mermaid-svg-dYl8X4bbpAAQnhG4 g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-dYl8X4bbpAAQnhG4 g.classGroup text .title{font-weight:bolder;}#mermaid-svg-dYl8X4bbpAAQnhG4 .cluster-label text{fill:#333;}#mermaid-svg-dYl8X4bbpAAQnhG4 .cluster-label span{color:#333;}#mermaid-svg-dYl8X4bbpAAQnhG4 .cluster-label span p{background-color:transparent;}#mermaid-svg-dYl8X4bbpAAQnhG4 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dYl8X4bbpAAQnhG4 .cluster text{fill:#333;}#mermaid-svg-dYl8X4bbpAAQnhG4 .cluster span{color:#333;}#mermaid-svg-dYl8X4bbpAAQnhG4 .nodeLabel,#mermaid-svg-dYl8X4bbpAAQnhG4 .edgeLabel{color:#131300;}#mermaid-svg-dYl8X4bbpAAQnhG4 .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-dYl8X4bbpAAQnhG4 .label text{fill:#131300;}#mermaid-svg-dYl8X4bbpAAQnhG4 .labelBkg{background:#ECECFF;}#mermaid-svg-dYl8X4bbpAAQnhG4 .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-dYl8X4bbpAAQnhG4 .classTitle{font-weight:bolder;}#mermaid-svg-dYl8X4bbpAAQnhG4 .node rect,#mermaid-svg-dYl8X4bbpAAQnhG4 .node circle,#mermaid-svg-dYl8X4bbpAAQnhG4 .node ellipse,#mermaid-svg-dYl8X4bbpAAQnhG4 .node polygon,#mermaid-svg-dYl8X4bbpAAQnhG4 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dYl8X4bbpAAQnhG4 .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-dYl8X4bbpAAQnhG4 g.clickable{cursor:pointer;}#mermaid-svg-dYl8X4bbpAAQnhG4 g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-dYl8X4bbpAAQnhG4 g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-dYl8X4bbpAAQnhG4 .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-dYl8X4bbpAAQnhG4 .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-dYl8X4bbpAAQnhG4 .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-dYl8X4bbpAAQnhG4 .dashed-line{stroke-dasharray:3;}#mermaid-svg-dYl8X4bbpAAQnhG4 .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-dYl8X4bbpAAQnhG4 #compositionStart,#mermaid-svg-dYl8X4bbpAAQnhG4 .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-dYl8X4bbpAAQnhG4 #compositionEnd,#mermaid-svg-dYl8X4bbpAAQnhG4 .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-dYl8X4bbpAAQnhG4 #dependencyStart,#mermaid-svg-dYl8X4bbpAAQnhG4 .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-dYl8X4bbpAAQnhG4 #dependencyStart,#mermaid-svg-dYl8X4bbpAAQnhG4 .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-dYl8X4bbpAAQnhG4 #extensionStart,#mermaid-svg-dYl8X4bbpAAQnhG4 .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-dYl8X4bbpAAQnhG4 #extensionEnd,#mermaid-svg-dYl8X4bbpAAQnhG4 .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-dYl8X4bbpAAQnhG4 #aggregationStart,#mermaid-svg-dYl8X4bbpAAQnhG4 .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-dYl8X4bbpAAQnhG4 #aggregationEnd,#mermaid-svg-dYl8X4bbpAAQnhG4 .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-dYl8X4bbpAAQnhG4 #lollipopStart,#mermaid-svg-dYl8X4bbpAAQnhG4 .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-dYl8X4bbpAAQnhG4 #lollipopEnd,#mermaid-svg-dYl8X4bbpAAQnhG4 .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-dYl8X4bbpAAQnhG4 .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-dYl8X4bbpAAQnhG4 .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dYl8X4bbpAAQnhG4 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dYl8X4bbpAAQnhG4 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dYl8X4bbpAAQnhG4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} <<abstract behavior>>
Wallet
-BigDecimal balance
-List<TxLog> txLogs
+balance() : BigDecimal
+txLogs() : List
+recharge(amt)
+consume(amt)
#isAllowed(after) : bool
CreditWallet
#isAllowed(after) : bool
FrozenWallet
#isAllowed(after) : bool

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

# 不变量 谁来守
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 的三道思考题。

相关推荐
红豆诗人12 天前
C++ 继承进阶梳理:权限、转换、隐藏和菱形继承
c++·多态·面向对象
rolt13 天前
PlantUML+EA描述《分析模式》第7章使用会计模型(1)
架构师·uml·面向对象
小肝一下21 天前
多态(上)
android·开发语言·c++·vscode·多态·面向对象·伊蕾娜
FreakStudio1 个月前
WIZnet 开源方案分享:W55MH32 开发板 + 小智 AI实现以太网版本的小智机器人
python·单片机·嵌入式·大学生·面向对象·并行计算·电子diy·电子计算机
FreakStudio2 个月前
W55MH32L-EVB 上手测评:硬件 TCP/IP 加持的以太网单片机,MicroPython 零门槛开发
python·单片机·嵌入式·大学生·面向对象·并行计算·电子diy·电子计算机
小bo波2 个月前
形式化方法 × UML
java·软件工程·uml·面向对象·形式化方法·tla+
FreakStudio2 个月前
大话电容传感器和电容SOC芯片,看这一篇就够了
python·单片机·嵌入式·面向对象·并行计算·电子diy·电子计算机
xiaobobo33303 个月前
面向对象:linux内核中函数转数据的用法
linux·面向对象·隔离·函数指针绑定
不会武功的火柴3 个月前
SystemVerilog语法(11)-面向对象编程下篇
面向对象·fpga·systemverilog·ic验证