05.多用组合和少继承


title: 5.多用组合和少继承

date: 2019-04-08 05:18:23

permalink: /pages/3b28c3/

categories:

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

author:

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

5.多用组合和少继承

目录介绍
  • 1.先回答上篇思考题
    • [1.1 上篇遗留三道题](#1.1 上篇遗留三道题)
    • [1.2 企鹅会飞的决策事故](#1.2 企鹅会飞的决策事故)
    • [1.3 维度正交的爆炸](#1.3 维度正交的爆炸)
    • [1.4 灵魂五连问](#1.4 灵魂五连问)
  • 2.从一个翻车说起
    • [2.1 鸟类继承谜题](#2.1 鸟类继承谜题)
    • [2.2 层级越拆越乱](#2.2 层级越拆越乱)
    • [2.3 问题的根因](#2.3 问题的根因)
  • 3.继承的三宗罪
    • [3.1 强耦合](#3.1 强耦合)
    • [3.2 层级膨胀](#3.2 层级膨胀)
    • [3.3 静态绑定](#3.3 静态绑定)
  • 4.组合解题
    • [4.1 用接口拆能力](#4.1 用接口拆能力)
    • [4.2 用组合复用代码](#4.2 用组合复用代码)
    • [4.3 鸟类组合方案](#4.3 鸟类组合方案)
  • 5.绘图程序对比
    • [5.1 继承的爆炸](#5.1 继承的爆炸)
    • [5.2 组合的优雅](#5.2 组合的优雅)
    • [5.3 类图对比](#5.3 类图对比)
  • 6.如何选择
    • [6.1 三大判据](#6.1 三大判据)
    • [6.2 继承适用场景](#6.2 继承适用场景)
    • [6.3 组合适用场景](#6.3 组合适用场景)
  • 7.设计模式映射
    • [7.1 装饰者模式](#7.1 装饰者模式)
    • [7.2 策略模式](#7.2 策略模式)
    • [7.3 模板方法](#7.3 模板方法)
  • 8.总结与延伸
  • 9.综合实战案例
    • [9.1 商品体系需求](#9.1 商品体系需求)
    • [9.2 继承版类爆炸](#9.2 继承版类爆炸)
    • [9.3 能力组合重构](#9.3 能力组合重构)
    • [9.4 类图与动态能力](#9.4 类图与动态能力)
    • [9.5 留下三道思考题](#9.5 留下三道思考题)
  • 10.认知跃迁总结

1.先回答上篇思考题

1.1 上篇遗留三道题

上一篇 04.接口而非实现编程 末尾留下了三道题:

  • 🟢 Map<Carrier, CarrierTracker> vs List<CarrierTracker>本质区别?
  • 🟡 SfTracker 应该 extends SfApiClient 还是 含有 SfApiClient 字段?
  • 🔴 三家快递 80% 共同逻辑如何复用?

本篇要回答的是后两道题:

本篇答案
🟢 Map = 根据快递公司路由List = 多实现同时生效。这是「路由 vs 广播」两种多态语义
🟡 本篇答 含有字段(组合)。理由看§02 「继承的三宗罪」
🔴 本篇答 提炼能力、用组合 ,三家都拼装 [认证, 限流, 重试, HttpCaller],而不是控制一个 RestApiBase 抽象类

这三道题几乎在问同一个问题:当你看到两个类"很像"时,是该走继承还是该走组合?

1.2 企鹅会飞的决策事故

某动物园业务系统中发生了一起「企鹅起飞」的荐题。

java 复制代码
public abstract class AbstractBird {
    public void fly() { /* 默认会飞 */ }
    public void layEgg() { ... }
    public void chirp() { ... }
}

public class Penguin extends AbstractBird { /* 使用默认 fly */ }

代码主人以为「企鹅不会飞」是常识,谁都不会调。直到一名实习生写了:

java 复制代码
zoo.allBirds().forEach(Bird::fly);   // 果然,企鹅也"飞"起来了

后果是企鹅馆的展示屏出现了"企鹅正在飞行"的字样。这不是笑话,是在 GitHub 能搜到的真实事故调例

这里骨子里不是「Penguin 应该重写 fly() 让它 throw 异常」,那只是「补丁」。

骨子里的错误是「把 fly() 放进了所有鸟都能看到的地方」,也就是继承体系的"公共套餐"设计。不适合的能力被「继承」这个机制强加到了子类身上。

企鹅会飞是"继承给了不该有的能力",单一能力错配。但工程中更常见的噩梦是反面:一个对象天然拥有多种能力(会飞、会走、会游),继承树却无法在单一层级里同时容纳它们。单一能力错配是尴尬,多重能力正交是爆炸。下面看一个更极端的案例。

1.3 维度正交的爆炸

企鹅的问题是「继承给了不该有的能力」。但继续沿着鸟类往下想,问题会从"尴尬"升级为"爆炸"。

现实中的鸟的能力矩阵,每只鸟拥有的能力都不同:

复制代码
企鹅:      会游泳、会叫、(不会飞)
麻雀:会飞、        会叫、(不会游)
鸭子:会飞、 会游泳、会叫
鸵鸟:             会叫、(不会飞、不会游)

如果坚持用继承树表达这三种能力的所有合法组合:

复制代码
AbstractBird
 ├── FlyableBird
 │    ├── Sparrow                  (飞+叫)
 │    └── ...
 ├── SwimmableBird
 │    ├── Penguin                  (游+叫)
 │    └── ...
 ├── TweetingBird                  (只叫)
 ├── FlySwimBird
 │    └── Duck                     (飞+游+叫)
 ├── FlyTweetBird                  (飞+叫、不游)
 ├── SwimTweetBird                 (游+叫、不飞)
 └── FlySwimTweetBird              (飞+游+叫)

3 种能力,产生 2³ = 7 种可能的中间抽象类。再加「会潜水」「会上树」?每多一种能力维度,中间类的数量翻一倍。

这不是"设计得不够好",是用 is-a(继承)去建模正交能力组合,数学上注定爆炸。

实际重构中,开发者经历了五次挣扎:

重构 1:为 DuckFlySwimBird → 出现了 SwimTweetBird(企鹅场景),继续抽

重构 2:当 Sparrow 需要时抽 FlyTweetBird → 中间类开始膨胀

重构 3:表面能跑,但 PM 说"加一种'会潜水'的鸟" → 继承树重来

重构 4:改为 interface CanFly / CanSwim / CanTweet → 发现 3 种能力的实现代码里,普遍存在重复逻辑(如所有飞行实现都要做「起飞前高度校验」)

重构 5:最终落定为 「能力接口 + 能力对象组合」class Duck { flyer; swimmer; tweeter; }

五个版本走完,结论只有一句:单一能力错配用继承可以打补丁,正交能力组合用继承是死路

1.4 灵魂五连问

复制代码
Q1 ── 为什么继承、这么学院派的工具,在工程里却如此危险?
       └─→ §02 三宗罪
Q2 ── 组合不也是依赖吗?为什么耦合更低?
       └─→ §03.1 / §03.2 能力维度划分
Q3 ── 什么时候继承反而是最佳选择?
       └─→ §05.2 继承适用场景
Q4 ── 设计模式中哪些是"伪装成继承的组合"?
       └─→ §06 三个模式反面例
Q5 ── 「能力组合」与「领域驱动」有什么关系?
       └─→ §08 末尾伏笔,对接第 11 篇 DDD

2.从一个翻车说起

2.1 鸟类继承谜题

定义抽象类 AbstractBird,所有鸟都继承它,并提供 fly() 方法:

java 复制代码
public abstract class AbstractBird {
    public void fly() { /* 默认会飞 */ }
}
public class Sparrow extends AbstractBird { /* 麻雀,会飞 */ }
public class Ostrich extends AbstractBird {  // 鸵鸟,不会飞
    @Override public void fly() {
        throw new UnsupportedOperationException("鸵鸟不会飞");
    }
}

第一处尴尬出现:鸵鸟仍然继承了 fly() 方法,只能靠抛异常"撇清" 。这违反里氏替换原则,客户端拿到一个 AbstractBird 调用 fly(),结果 RuntimeException。

2.2 层级越拆越乱

为了精确建模,再加一层:

复制代码
AbstractBird
   ├── AbstractFlyableBird(会飞)
   │      ├── Sparrow
   │      └── Crow
   └── AbstractUnflyableBird(不会飞)
          ├── Ostrich
          └── Penguin

需求继续叠加:"是否会叫?是否会游泳?",继续按维度切,类的数量呈笛卡尔积爆炸

复制代码
能力组合 = 飞? × 叫? × 游? = 2 × 2 × 2 = 8 个抽象类
再加哺乳/卵生 = 16 个
再加......

2.3 问题的根因

#mermaid-svg-T4ef3NgGxcj7LMgl{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-T4ef3NgGxcj7LMgl .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-T4ef3NgGxcj7LMgl .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-T4ef3NgGxcj7LMgl .error-icon{fill:#552222;}#mermaid-svg-T4ef3NgGxcj7LMgl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-T4ef3NgGxcj7LMgl .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-T4ef3NgGxcj7LMgl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-T4ef3NgGxcj7LMgl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-T4ef3NgGxcj7LMgl .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-T4ef3NgGxcj7LMgl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-T4ef3NgGxcj7LMgl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-T4ef3NgGxcj7LMgl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-T4ef3NgGxcj7LMgl .marker.cross{stroke:#333333;}#mermaid-svg-T4ef3NgGxcj7LMgl svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-T4ef3NgGxcj7LMgl p{margin:0;}#mermaid-svg-T4ef3NgGxcj7LMgl .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-T4ef3NgGxcj7LMgl .cluster-label text{fill:#333;}#mermaid-svg-T4ef3NgGxcj7LMgl .cluster-label span{color:#333;}#mermaid-svg-T4ef3NgGxcj7LMgl .cluster-label span p{background-color:transparent;}#mermaid-svg-T4ef3NgGxcj7LMgl .label text,#mermaid-svg-T4ef3NgGxcj7LMgl span{fill:#333;color:#333;}#mermaid-svg-T4ef3NgGxcj7LMgl .node rect,#mermaid-svg-T4ef3NgGxcj7LMgl .node circle,#mermaid-svg-T4ef3NgGxcj7LMgl .node ellipse,#mermaid-svg-T4ef3NgGxcj7LMgl .node polygon,#mermaid-svg-T4ef3NgGxcj7LMgl .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-T4ef3NgGxcj7LMgl .rough-node .label text,#mermaid-svg-T4ef3NgGxcj7LMgl .node .label text,#mermaid-svg-T4ef3NgGxcj7LMgl .image-shape .label,#mermaid-svg-T4ef3NgGxcj7LMgl .icon-shape .label{text-anchor:middle;}#mermaid-svg-T4ef3NgGxcj7LMgl .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-T4ef3NgGxcj7LMgl .rough-node .label,#mermaid-svg-T4ef3NgGxcj7LMgl .node .label,#mermaid-svg-T4ef3NgGxcj7LMgl .image-shape .label,#mermaid-svg-T4ef3NgGxcj7LMgl .icon-shape .label{text-align:center;}#mermaid-svg-T4ef3NgGxcj7LMgl .node.clickable{cursor:pointer;}#mermaid-svg-T4ef3NgGxcj7LMgl .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-T4ef3NgGxcj7LMgl .arrowheadPath{fill:#333333;}#mermaid-svg-T4ef3NgGxcj7LMgl .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-T4ef3NgGxcj7LMgl .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-T4ef3NgGxcj7LMgl .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-T4ef3NgGxcj7LMgl .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-T4ef3NgGxcj7LMgl .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-T4ef3NgGxcj7LMgl .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-T4ef3NgGxcj7LMgl .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-T4ef3NgGxcj7LMgl .cluster text{fill:#333;}#mermaid-svg-T4ef3NgGxcj7LMgl .cluster span{color:#333;}#mermaid-svg-T4ef3NgGxcj7LMgl 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-T4ef3NgGxcj7LMgl .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-T4ef3NgGxcj7LMgl rect.text{fill:none;stroke-width:0;}#mermaid-svg-T4ef3NgGxcj7LMgl .icon-shape,#mermaid-svg-T4ef3NgGxcj7LMgl .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-T4ef3NgGxcj7LMgl .icon-shape p,#mermaid-svg-T4ef3NgGxcj7LMgl .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-T4ef3NgGxcj7LMgl .icon-shape .label rect,#mermaid-svg-T4ef3NgGxcj7LMgl .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-T4ef3NgGxcj7LMgl .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-T4ef3NgGxcj7LMgl .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-T4ef3NgGxcj7LMgl :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 继承用来表达

具有某种能力
每加一种能力维度
就要再切一层继承
层级爆炸
可读性差

耦合度高

根本错误 :用 is-a 关系(继承)去建模 has-a 关系(能力)。鸵鸟"是一种鸟"成立,但"是否会飞"是能力维度,不该用类型层级表达。


3.继承的三宗罪

3.1 强耦合

子类与父类是编译期绑定

java 复制代码
// 父类改一行:
public abstract class AbstractBird {
    public void fly() {
        System.out.println("拍翅膀");   // 后来改为日志埋点
        flap();
    }
}
// 所有子类的 fly() 行为都被影响,但子类作者可能毫不知情

子类透视到了父类的内部实现,这正好破坏了封装。

3.2 层级膨胀

#mermaid-svg-vPQ5tSTgWkB4760g{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-vPQ5tSTgWkB4760g .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vPQ5tSTgWkB4760g .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vPQ5tSTgWkB4760g .error-icon{fill:#552222;}#mermaid-svg-vPQ5tSTgWkB4760g .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vPQ5tSTgWkB4760g .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vPQ5tSTgWkB4760g .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vPQ5tSTgWkB4760g .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vPQ5tSTgWkB4760g .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vPQ5tSTgWkB4760g .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vPQ5tSTgWkB4760g .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vPQ5tSTgWkB4760g .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vPQ5tSTgWkB4760g .marker.cross{stroke:#333333;}#mermaid-svg-vPQ5tSTgWkB4760g svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vPQ5tSTgWkB4760g p{margin:0;}#mermaid-svg-vPQ5tSTgWkB4760g .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-vPQ5tSTgWkB4760g .cluster-label text{fill:#333;}#mermaid-svg-vPQ5tSTgWkB4760g .cluster-label span{color:#333;}#mermaid-svg-vPQ5tSTgWkB4760g .cluster-label span p{background-color:transparent;}#mermaid-svg-vPQ5tSTgWkB4760g .label text,#mermaid-svg-vPQ5tSTgWkB4760g span{fill:#333;color:#333;}#mermaid-svg-vPQ5tSTgWkB4760g .node rect,#mermaid-svg-vPQ5tSTgWkB4760g .node circle,#mermaid-svg-vPQ5tSTgWkB4760g .node ellipse,#mermaid-svg-vPQ5tSTgWkB4760g .node polygon,#mermaid-svg-vPQ5tSTgWkB4760g .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vPQ5tSTgWkB4760g .rough-node .label text,#mermaid-svg-vPQ5tSTgWkB4760g .node .label text,#mermaid-svg-vPQ5tSTgWkB4760g .image-shape .label,#mermaid-svg-vPQ5tSTgWkB4760g .icon-shape .label{text-anchor:middle;}#mermaid-svg-vPQ5tSTgWkB4760g .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vPQ5tSTgWkB4760g .rough-node .label,#mermaid-svg-vPQ5tSTgWkB4760g .node .label,#mermaid-svg-vPQ5tSTgWkB4760g .image-shape .label,#mermaid-svg-vPQ5tSTgWkB4760g .icon-shape .label{text-align:center;}#mermaid-svg-vPQ5tSTgWkB4760g .node.clickable{cursor:pointer;}#mermaid-svg-vPQ5tSTgWkB4760g .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vPQ5tSTgWkB4760g .arrowheadPath{fill:#333333;}#mermaid-svg-vPQ5tSTgWkB4760g .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vPQ5tSTgWkB4760g .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vPQ5tSTgWkB4760g .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vPQ5tSTgWkB4760g .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vPQ5tSTgWkB4760g .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vPQ5tSTgWkB4760g .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vPQ5tSTgWkB4760g .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vPQ5tSTgWkB4760g .cluster text{fill:#333;}#mermaid-svg-vPQ5tSTgWkB4760g .cluster span{color:#333;}#mermaid-svg-vPQ5tSTgWkB4760g 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-vPQ5tSTgWkB4760g .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vPQ5tSTgWkB4760g rect.text{fill:none;stroke-width:0;}#mermaid-svg-vPQ5tSTgWkB4760g .icon-shape,#mermaid-svg-vPQ5tSTgWkB4760g .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vPQ5tSTgWkB4760g .icon-shape p,#mermaid-svg-vPQ5tSTgWkB4760g .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vPQ5tSTgWkB4760g .icon-shape .label rect,#mermaid-svg-vPQ5tSTgWkB4760g .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vPQ5tSTgWkB4760g .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vPQ5tSTgWkB4760g .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vPQ5tSTgWkB4760g :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Bird
Flyable
Unflyable
FlyTweet
FlyMute
UnflyTweet
UnflyMute
...
...

每多一个正交维度 ,层级就要乘上一倍。这不是"复用",是自虐

3.3 静态绑定

继承结构在编译期就钉死,运行时不可改

java 复制代码
// 想让一只鸵鸟暂时"会飞"(魔法状态)→ 不可能
ostrich.fly();   // 永远抛异常,除非改类层级重新编译

4.组合解题

4.1 用接口拆能力

把"能力"用接口表达:

java 复制代码
public interface Flyable   { void fly();   }
public interface Tweetable { void tweet(); }
public interface Swimmable { void swim();  }

每只鸟按需实现:

java 复制代码
public class Sparrow implements Flyable, Tweetable { /* ... */ }
public class Ostrich implements Tweetable, Swimmable { /* 不实现 Flyable */ }
public class Penguin implements Swimmable { /* ... */ }

接口让能力维度正交,加一种能力只需加一个接口,不动任何已有类。

4.2 用组合复用代码

接口只声明,没复用代码。把行为实现抽成"能力对象",再组合 + 委托

java 复制代码
public class FlyAbility {
    public void fly() { /* 拍翅膀飞行的通用逻辑 */ }
}
public class TweetAbility {
    public void tweet() { /* 鸣叫逻辑 */ }
}

public class Sparrow implements Flyable, Tweetable {
    private final FlyAbility   fly   = new FlyAbility();    // 组合
    private final TweetAbility tweet = new TweetAbility();  // 组合

    @Override public void fly()   { fly.fly();     }       // 委托
    @Override public void tweet() { tweet.tweet(); }
}

4.3 鸟类组合方案

#mermaid-svg-UP9rcvS3ySNsGoKv{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-UP9rcvS3ySNsGoKv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UP9rcvS3ySNsGoKv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UP9rcvS3ySNsGoKv .error-icon{fill:#552222;}#mermaid-svg-UP9rcvS3ySNsGoKv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UP9rcvS3ySNsGoKv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UP9rcvS3ySNsGoKv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UP9rcvS3ySNsGoKv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UP9rcvS3ySNsGoKv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UP9rcvS3ySNsGoKv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UP9rcvS3ySNsGoKv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UP9rcvS3ySNsGoKv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UP9rcvS3ySNsGoKv .marker.cross{stroke:#333333;}#mermaid-svg-UP9rcvS3ySNsGoKv svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UP9rcvS3ySNsGoKv p{margin:0;}#mermaid-svg-UP9rcvS3ySNsGoKv g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-UP9rcvS3ySNsGoKv g.classGroup text .title{font-weight:bolder;}#mermaid-svg-UP9rcvS3ySNsGoKv .cluster-label text{fill:#333;}#mermaid-svg-UP9rcvS3ySNsGoKv .cluster-label span{color:#333;}#mermaid-svg-UP9rcvS3ySNsGoKv .cluster-label span p{background-color:transparent;}#mermaid-svg-UP9rcvS3ySNsGoKv .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UP9rcvS3ySNsGoKv .cluster text{fill:#333;}#mermaid-svg-UP9rcvS3ySNsGoKv .cluster span{color:#333;}#mermaid-svg-UP9rcvS3ySNsGoKv .nodeLabel,#mermaid-svg-UP9rcvS3ySNsGoKv .edgeLabel{color:#131300;}#mermaid-svg-UP9rcvS3ySNsGoKv .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-UP9rcvS3ySNsGoKv .label text{fill:#131300;}#mermaid-svg-UP9rcvS3ySNsGoKv .labelBkg{background:#ECECFF;}#mermaid-svg-UP9rcvS3ySNsGoKv .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-UP9rcvS3ySNsGoKv .classTitle{font-weight:bolder;}#mermaid-svg-UP9rcvS3ySNsGoKv .node rect,#mermaid-svg-UP9rcvS3ySNsGoKv .node circle,#mermaid-svg-UP9rcvS3ySNsGoKv .node ellipse,#mermaid-svg-UP9rcvS3ySNsGoKv .node polygon,#mermaid-svg-UP9rcvS3ySNsGoKv .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UP9rcvS3ySNsGoKv .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-UP9rcvS3ySNsGoKv g.clickable{cursor:pointer;}#mermaid-svg-UP9rcvS3ySNsGoKv g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-UP9rcvS3ySNsGoKv g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-UP9rcvS3ySNsGoKv .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-UP9rcvS3ySNsGoKv .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-UP9rcvS3ySNsGoKv .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-UP9rcvS3ySNsGoKv .dashed-line{stroke-dasharray:3;}#mermaid-svg-UP9rcvS3ySNsGoKv .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-UP9rcvS3ySNsGoKv #compositionStart,#mermaid-svg-UP9rcvS3ySNsGoKv .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-UP9rcvS3ySNsGoKv #compositionEnd,#mermaid-svg-UP9rcvS3ySNsGoKv .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-UP9rcvS3ySNsGoKv #dependencyStart,#mermaid-svg-UP9rcvS3ySNsGoKv .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-UP9rcvS3ySNsGoKv #dependencyStart,#mermaid-svg-UP9rcvS3ySNsGoKv .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-UP9rcvS3ySNsGoKv #extensionStart,#mermaid-svg-UP9rcvS3ySNsGoKv .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-UP9rcvS3ySNsGoKv #extensionEnd,#mermaid-svg-UP9rcvS3ySNsGoKv .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-UP9rcvS3ySNsGoKv #aggregationStart,#mermaid-svg-UP9rcvS3ySNsGoKv .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-UP9rcvS3ySNsGoKv #aggregationEnd,#mermaid-svg-UP9rcvS3ySNsGoKv .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-UP9rcvS3ySNsGoKv #lollipopStart,#mermaid-svg-UP9rcvS3ySNsGoKv .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-UP9rcvS3ySNsGoKv #lollipopEnd,#mermaid-svg-UP9rcvS3ySNsGoKv .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-UP9rcvS3ySNsGoKv .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-UP9rcvS3ySNsGoKv .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UP9rcvS3ySNsGoKv .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UP9rcvS3ySNsGoKv .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UP9rcvS3ySNsGoKv :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 组合
组合
组合
组合
Flyable
<> +fly()
Tweetable
<> +tweet()
Swimmable
<> +swim()
FlyAbility
+fly()
TweetAbility
+tweet()
SwimAbility
+swim()
Sparrow
-FlyAbility fly
-TweetAbility tweet
Ostrich
-TweetAbility tweet
-SwimAbility swim

继承的三个职能,类型表达、多态、代码复用,被分别交给:

职能 替代方案
类型表达(is-a) 接口(has-a / can-do)
多态 接口多态
代码复用 组合 + 委托

5.绘图程序对比

上面用鸟类展示了「能力维度正交」的解法,用接口 + 组合取代继承树。下面换个场景再看一遍:当两个维度完全正交且同等重要时,继承的爆炸会更快。绘图系统里「形状」和「颜色」就是这样的正交维度。

5.1 继承的爆炸

绘图系统:形状 × 颜色 → 类爆炸:

java 复制代码
class RedCircle extends Circle { /* ... */ }
class BlueCircle extends Circle { /* ... */ }
class RedRectangle extends Rectangle { /* ... */ }
class BlueRectangle extends Rectangle { /* ... */ }
// 加一种颜色 → 形状数 × 1 个新类
// 加一种形状 → 颜色数 × 1 个新类

5.2 组合的优雅

把"颜色"作为正交维度,组合进形状:

java 复制代码
interface Color { void apply(); }
class Red  implements Color { public void apply() { /* 红 */ } }
class Blue implements Color { public void apply() { /* 蓝 */ } }

abstract class Shape {
    protected final Color color;     // 组合
    Shape(Color color) { this.color = color; }
    abstract void draw();
}
class Circle extends Shape {
    Circle(Color c) { super(c); }
    void draw() { color.apply(); /* 画圆 */ }
}
class Rectangle extends Shape {
    Rectangle(Color c) { super(c); }
    void draw() { color.apply(); /* 画矩形 */ }
}

// 使用
Shape s = new Circle(new Red());

类的数量从 N×M 降为 N+M。这正是桥接模式的精髓。

5.3 类图对比

#mermaid-svg-KyTVrll6FLTig8T8{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-KyTVrll6FLTig8T8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-KyTVrll6FLTig8T8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-KyTVrll6FLTig8T8 .error-icon{fill:#552222;}#mermaid-svg-KyTVrll6FLTig8T8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-KyTVrll6FLTig8T8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-KyTVrll6FLTig8T8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-KyTVrll6FLTig8T8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-KyTVrll6FLTig8T8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-KyTVrll6FLTig8T8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-KyTVrll6FLTig8T8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-KyTVrll6FLTig8T8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-KyTVrll6FLTig8T8 .marker.cross{stroke:#333333;}#mermaid-svg-KyTVrll6FLTig8T8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-KyTVrll6FLTig8T8 p{margin:0;}#mermaid-svg-KyTVrll6FLTig8T8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-KyTVrll6FLTig8T8 .cluster-label text{fill:#333;}#mermaid-svg-KyTVrll6FLTig8T8 .cluster-label span{color:#333;}#mermaid-svg-KyTVrll6FLTig8T8 .cluster-label span p{background-color:transparent;}#mermaid-svg-KyTVrll6FLTig8T8 .label text,#mermaid-svg-KyTVrll6FLTig8T8 span{fill:#333;color:#333;}#mermaid-svg-KyTVrll6FLTig8T8 .node rect,#mermaid-svg-KyTVrll6FLTig8T8 .node circle,#mermaid-svg-KyTVrll6FLTig8T8 .node ellipse,#mermaid-svg-KyTVrll6FLTig8T8 .node polygon,#mermaid-svg-KyTVrll6FLTig8T8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-KyTVrll6FLTig8T8 .rough-node .label text,#mermaid-svg-KyTVrll6FLTig8T8 .node .label text,#mermaid-svg-KyTVrll6FLTig8T8 .image-shape .label,#mermaid-svg-KyTVrll6FLTig8T8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-KyTVrll6FLTig8T8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-KyTVrll6FLTig8T8 .rough-node .label,#mermaid-svg-KyTVrll6FLTig8T8 .node .label,#mermaid-svg-KyTVrll6FLTig8T8 .image-shape .label,#mermaid-svg-KyTVrll6FLTig8T8 .icon-shape .label{text-align:center;}#mermaid-svg-KyTVrll6FLTig8T8 .node.clickable{cursor:pointer;}#mermaid-svg-KyTVrll6FLTig8T8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-KyTVrll6FLTig8T8 .arrowheadPath{fill:#333333;}#mermaid-svg-KyTVrll6FLTig8T8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-KyTVrll6FLTig8T8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-KyTVrll6FLTig8T8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KyTVrll6FLTig8T8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-KyTVrll6FLTig8T8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KyTVrll6FLTig8T8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-KyTVrll6FLTig8T8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-KyTVrll6FLTig8T8 .cluster text{fill:#333;}#mermaid-svg-KyTVrll6FLTig8T8 .cluster span{color:#333;}#mermaid-svg-KyTVrll6FLTig8T8 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-KyTVrll6FLTig8T8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-KyTVrll6FLTig8T8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-KyTVrll6FLTig8T8 .icon-shape,#mermaid-svg-KyTVrll6FLTig8T8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KyTVrll6FLTig8T8 .icon-shape p,#mermaid-svg-KyTVrll6FLTig8T8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-KyTVrll6FLTig8T8 .icon-shape .label rect,#mermaid-svg-KyTVrll6FLTig8T8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KyTVrll6FLTig8T8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-KyTVrll6FLTig8T8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-KyTVrll6FLTig8T8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 组合-正交
组合
组合
Shape
Circle
Rect
Color 接口
Red
Blue
继承-类爆炸
Shape
Circle
Rectangle
RedCircle
BlueCircle
RedRect
BlueRect


6.如何选择

6.1 三大判据

#mermaid-svg-OqGZxMvrx00Xy3bG{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-OqGZxMvrx00Xy3bG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-OqGZxMvrx00Xy3bG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-OqGZxMvrx00Xy3bG .error-icon{fill:#552222;}#mermaid-svg-OqGZxMvrx00Xy3bG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-OqGZxMvrx00Xy3bG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-OqGZxMvrx00Xy3bG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OqGZxMvrx00Xy3bG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OqGZxMvrx00Xy3bG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-OqGZxMvrx00Xy3bG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OqGZxMvrx00Xy3bG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OqGZxMvrx00Xy3bG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-OqGZxMvrx00Xy3bG .marker.cross{stroke:#333333;}#mermaid-svg-OqGZxMvrx00Xy3bG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OqGZxMvrx00Xy3bG p{margin:0;}#mermaid-svg-OqGZxMvrx00Xy3bG .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-OqGZxMvrx00Xy3bG .cluster-label text{fill:#333;}#mermaid-svg-OqGZxMvrx00Xy3bG .cluster-label span{color:#333;}#mermaid-svg-OqGZxMvrx00Xy3bG .cluster-label span p{background-color:transparent;}#mermaid-svg-OqGZxMvrx00Xy3bG .label text,#mermaid-svg-OqGZxMvrx00Xy3bG span{fill:#333;color:#333;}#mermaid-svg-OqGZxMvrx00Xy3bG .node rect,#mermaid-svg-OqGZxMvrx00Xy3bG .node circle,#mermaid-svg-OqGZxMvrx00Xy3bG .node ellipse,#mermaid-svg-OqGZxMvrx00Xy3bG .node polygon,#mermaid-svg-OqGZxMvrx00Xy3bG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-OqGZxMvrx00Xy3bG .rough-node .label text,#mermaid-svg-OqGZxMvrx00Xy3bG .node .label text,#mermaid-svg-OqGZxMvrx00Xy3bG .image-shape .label,#mermaid-svg-OqGZxMvrx00Xy3bG .icon-shape .label{text-anchor:middle;}#mermaid-svg-OqGZxMvrx00Xy3bG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-OqGZxMvrx00Xy3bG .rough-node .label,#mermaid-svg-OqGZxMvrx00Xy3bG .node .label,#mermaid-svg-OqGZxMvrx00Xy3bG .image-shape .label,#mermaid-svg-OqGZxMvrx00Xy3bG .icon-shape .label{text-align:center;}#mermaid-svg-OqGZxMvrx00Xy3bG .node.clickable{cursor:pointer;}#mermaid-svg-OqGZxMvrx00Xy3bG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-OqGZxMvrx00Xy3bG .arrowheadPath{fill:#333333;}#mermaid-svg-OqGZxMvrx00Xy3bG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-OqGZxMvrx00Xy3bG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-OqGZxMvrx00Xy3bG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OqGZxMvrx00Xy3bG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-OqGZxMvrx00Xy3bG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OqGZxMvrx00Xy3bG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-OqGZxMvrx00Xy3bG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-OqGZxMvrx00Xy3bG .cluster text{fill:#333;}#mermaid-svg-OqGZxMvrx00Xy3bG .cluster span{color:#333;}#mermaid-svg-OqGZxMvrx00Xy3bG 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-OqGZxMvrx00Xy3bG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-OqGZxMvrx00Xy3bG rect.text{fill:none;stroke-width:0;}#mermaid-svg-OqGZxMvrx00Xy3bG .icon-shape,#mermaid-svg-OqGZxMvrx00Xy3bG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OqGZxMvrx00Xy3bG .icon-shape p,#mermaid-svg-OqGZxMvrx00Xy3bG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-OqGZxMvrx00Xy3bG .icon-shape .label rect,#mermaid-svg-OqGZxMvrx00Xy3bG .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OqGZxMvrx00Xy3bG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-OqGZxMvrx00Xy3bG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-OqGZxMvrx00Xy3bG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

且层级浅



要建模 is-a?
继承层级是否稳定?
组合
父类是否完全可控?
继承

继承的安全姿势:层级 ≤ 2 层,关系稳定,父类自己写。三者缺一就改组合。

6.2 继承适用场景

场景 例子
模板方法骨架 Wallet 父类提供 consume 流程,CreditWallet 覆写 isAllowed
框架钩子 抽象类定义生命周期,子类填空(如之前文章中的 AbstractPaymentGateway.doPay
浅层稳定层级 一条继承链 ≤ 2 层,且未来不会出现正交维度

6.3 组合适用场景

场景 例子
能力维度正交 鸟(飞/叫/游)、商品(可发货/预售/限购/组合)
行为可运行时切换 Order 持有 DiscountPolicy,随时换策略
可叠加多个能力 装饰者模式,给商品动态叠加限购+预售
跨业务领域复用 RateLimiter 被支付网关和商品查询各自持有

7.设计模式映射

组合优于继承,并不是抽象口号,23 种设计模式中绝大多数都基于组合

7.1 装饰者模式

动态叠加能力,以商品系统为例,同一个 SKU 在不同阶段可能需要动态附加"限购"、"预售"标签:

java 复制代码
Product p = new BaseProduct("SKU-101", price);
p = new PurchaseLimitedDecorator(p, maxPerUser=2);   // 叠加限购
p = new PreSaleDecorator(p, deliveryDate=...);        // 再叠加预售
// p 仍然是一个 Product,调用方完全不知它被装饰了几层

每层都"组合"前一层,行为可在运行时拼装。靠继承做不到

7.2 策略模式

java 复制代码
public class Order {
    private DiscountPolicy discount;   // 组合策略
    public void setDiscount(DiscountPolicy p) { this.discount = p; }   // 运行时切换
}

策略对象作为字段被持有,热替换毫无压力。

7.3 模板方法

模板方法是继承的合法用法,父类定义骨架,子类填空:

java 复制代码
abstract class HttpClient {
    public final Response send(Request req) {
        beforeSend(req);
        Response r = doSend(req);   // 子类填
        afterSend(r);
        return r;
    }
    protected abstract Response doSend(Request req);
}

这种"骨架 + 钩子"的固定流程,正是继承的擅长场景。你在 03 篇AbstractPaymentGateway.doPay 里见过完全相同的结构 ,支付网关的公共骨架用抽象类,差异化的 doPay 留给子类填空。
#mermaid-svg-kiUp2KSxUc6lz0o9{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-kiUp2KSxUc6lz0o9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kiUp2KSxUc6lz0o9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kiUp2KSxUc6lz0o9 .error-icon{fill:#552222;}#mermaid-svg-kiUp2KSxUc6lz0o9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kiUp2KSxUc6lz0o9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kiUp2KSxUc6lz0o9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kiUp2KSxUc6lz0o9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kiUp2KSxUc6lz0o9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kiUp2KSxUc6lz0o9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kiUp2KSxUc6lz0o9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kiUp2KSxUc6lz0o9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kiUp2KSxUc6lz0o9 .marker.cross{stroke:#333333;}#mermaid-svg-kiUp2KSxUc6lz0o9 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kiUp2KSxUc6lz0o9 p{margin:0;}#mermaid-svg-kiUp2KSxUc6lz0o9 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-kiUp2KSxUc6lz0o9 .cluster-label text{fill:#333;}#mermaid-svg-kiUp2KSxUc6lz0o9 .cluster-label span{color:#333;}#mermaid-svg-kiUp2KSxUc6lz0o9 .cluster-label span p{background-color:transparent;}#mermaid-svg-kiUp2KSxUc6lz0o9 .label text,#mermaid-svg-kiUp2KSxUc6lz0o9 span{fill:#333;color:#333;}#mermaid-svg-kiUp2KSxUc6lz0o9 .node rect,#mermaid-svg-kiUp2KSxUc6lz0o9 .node circle,#mermaid-svg-kiUp2KSxUc6lz0o9 .node ellipse,#mermaid-svg-kiUp2KSxUc6lz0o9 .node polygon,#mermaid-svg-kiUp2KSxUc6lz0o9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kiUp2KSxUc6lz0o9 .rough-node .label text,#mermaid-svg-kiUp2KSxUc6lz0o9 .node .label text,#mermaid-svg-kiUp2KSxUc6lz0o9 .image-shape .label,#mermaid-svg-kiUp2KSxUc6lz0o9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-kiUp2KSxUc6lz0o9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-kiUp2KSxUc6lz0o9 .rough-node .label,#mermaid-svg-kiUp2KSxUc6lz0o9 .node .label,#mermaid-svg-kiUp2KSxUc6lz0o9 .image-shape .label,#mermaid-svg-kiUp2KSxUc6lz0o9 .icon-shape .label{text-align:center;}#mermaid-svg-kiUp2KSxUc6lz0o9 .node.clickable{cursor:pointer;}#mermaid-svg-kiUp2KSxUc6lz0o9 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-kiUp2KSxUc6lz0o9 .arrowheadPath{fill:#333333;}#mermaid-svg-kiUp2KSxUc6lz0o9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-kiUp2KSxUc6lz0o9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-kiUp2KSxUc6lz0o9 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kiUp2KSxUc6lz0o9 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kiUp2KSxUc6lz0o9 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kiUp2KSxUc6lz0o9 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-kiUp2KSxUc6lz0o9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kiUp2KSxUc6lz0o9 .cluster text{fill:#333;}#mermaid-svg-kiUp2KSxUc6lz0o9 .cluster span{color:#333;}#mermaid-svg-kiUp2KSxUc6lz0o9 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-kiUp2KSxUc6lz0o9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kiUp2KSxUc6lz0o9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-kiUp2KSxUc6lz0o9 .icon-shape,#mermaid-svg-kiUp2KSxUc6lz0o9 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kiUp2KSxUc6lz0o9 .icon-shape p,#mermaid-svg-kiUp2KSxUc6lz0o9 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-kiUp2KSxUc6lz0o9 .icon-shape .label rect,#mermaid-svg-kiUp2KSxUc6lz0o9 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kiUp2KSxUc6lz0o9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kiUp2KSxUc6lz0o9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kiUp2KSxUc6lz0o9 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 设计模式
装饰者-组合
策略-组合
适配器-组合
桥接-组合
组合模式-组合
模板-继承


8.总结与延伸

维度 继承 组合
关系 is-a(编译期) has-a(运行期)
耦合度 强(父类暴露给子类) 弱(仅依赖接口)
灵活性 静态、不可换 动态、可热插拔
复用粒度 整块复用 细粒度组合
复杂度 易爆炸 类多但关系扁平

"多用组合少用继承" 不是"杜绝继承",而是把继承用在对的场景,浅层、稳定、可控。其他时候,组合永远是更安全的默认选项。

到此为止,前 5 篇构成了一条主线:
#mermaid-svg-ro2EVGye2NLEy8gD{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-ro2EVGye2NLEy8gD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ro2EVGye2NLEy8gD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ro2EVGye2NLEy8gD .error-icon{fill:#552222;}#mermaid-svg-ro2EVGye2NLEy8gD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ro2EVGye2NLEy8gD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ro2EVGye2NLEy8gD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ro2EVGye2NLEy8gD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ro2EVGye2NLEy8gD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ro2EVGye2NLEy8gD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ro2EVGye2NLEy8gD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ro2EVGye2NLEy8gD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ro2EVGye2NLEy8gD .marker.cross{stroke:#333333;}#mermaid-svg-ro2EVGye2NLEy8gD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ro2EVGye2NLEy8gD p{margin:0;}#mermaid-svg-ro2EVGye2NLEy8gD .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ro2EVGye2NLEy8gD .cluster-label text{fill:#333;}#mermaid-svg-ro2EVGye2NLEy8gD .cluster-label span{color:#333;}#mermaid-svg-ro2EVGye2NLEy8gD .cluster-label span p{background-color:transparent;}#mermaid-svg-ro2EVGye2NLEy8gD .label text,#mermaid-svg-ro2EVGye2NLEy8gD span{fill:#333;color:#333;}#mermaid-svg-ro2EVGye2NLEy8gD .node rect,#mermaid-svg-ro2EVGye2NLEy8gD .node circle,#mermaid-svg-ro2EVGye2NLEy8gD .node ellipse,#mermaid-svg-ro2EVGye2NLEy8gD .node polygon,#mermaid-svg-ro2EVGye2NLEy8gD .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ro2EVGye2NLEy8gD .rough-node .label text,#mermaid-svg-ro2EVGye2NLEy8gD .node .label text,#mermaid-svg-ro2EVGye2NLEy8gD .image-shape .label,#mermaid-svg-ro2EVGye2NLEy8gD .icon-shape .label{text-anchor:middle;}#mermaid-svg-ro2EVGye2NLEy8gD .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ro2EVGye2NLEy8gD .rough-node .label,#mermaid-svg-ro2EVGye2NLEy8gD .node .label,#mermaid-svg-ro2EVGye2NLEy8gD .image-shape .label,#mermaid-svg-ro2EVGye2NLEy8gD .icon-shape .label{text-align:center;}#mermaid-svg-ro2EVGye2NLEy8gD .node.clickable{cursor:pointer;}#mermaid-svg-ro2EVGye2NLEy8gD .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ro2EVGye2NLEy8gD .arrowheadPath{fill:#333333;}#mermaid-svg-ro2EVGye2NLEy8gD .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ro2EVGye2NLEy8gD .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ro2EVGye2NLEy8gD .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ro2EVGye2NLEy8gD .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ro2EVGye2NLEy8gD .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ro2EVGye2NLEy8gD .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ro2EVGye2NLEy8gD .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ro2EVGye2NLEy8gD .cluster text{fill:#333;}#mermaid-svg-ro2EVGye2NLEy8gD .cluster span{color:#333;}#mermaid-svg-ro2EVGye2NLEy8gD 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-ro2EVGye2NLEy8gD .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ro2EVGye2NLEy8gD rect.text{fill:none;stroke-width:0;}#mermaid-svg-ro2EVGye2NLEy8gD .icon-shape,#mermaid-svg-ro2EVGye2NLEy8gD .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ro2EVGye2NLEy8gD .icon-shape p,#mermaid-svg-ro2EVGye2NLEy8gD .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ro2EVGye2NLEy8gD .icon-shape .label rect,#mermaid-svg-ro2EVGye2NLEy8gD .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ro2EVGye2NLEy8gD .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ro2EVGye2NLEy8gD .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ro2EVGye2NLEy8gD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 01 思想

对象 vs 过程
02 特性

四大基石
03 接口vs抽象类

选哪个
04 接口编程

不锁死实现
05 组合优于继承

避开继承陷阱

有了对象、特性、契约、依赖与组合,就具备了写出"高内聚低耦合"代码的全部砖头。但砖头怎么砌成稳健的房子?这需要更高层的指导原则,SOLID 五大原则与 23 种设计模式。

下一篇 06.设计原则全景图 将以此前案例为线索,串联 SOLID 与设计模式,把面向对象设计推向工程落地。


9.综合实战案例

主线接力,04 篇货运推送用接口解了「调用方锁死」,本篇要解「能力拼装」:商品类型体系。

9.1 商品体系需求

电商上架需求:

复制代码
1. 普通实物商品:需要发货
2. 虚拟商品(课程/会员卡):不发货,不运费
3. 亲邮商品:走包裹个量限制
4. 预售商品:需推迟发货
5. 组合商品:含多个子商品
6. 后续会不断增加「会员专享」「限购」「需预约」等能力...

9.2 继承版类爆炸

工程师以为「这不是类的经典选择题吗」,当场画了一棵类继承树:

复制代码
Product
 ├─ PhysicalProduct
 │    ├─ NormalPhysical
 │    ├─ PreSalePhysical
 │    ├─ OverseasPhysical
 │    └─ BundlePhysical
 └─ VirtualProduct
      ├─ Course
      └─ Membership

第一次迫重构出现在 PM 说「虚拟会员卡也能预售」:

复制代码
PreSaleVirtualProduct?
OverseasVirtualProduct?  // 如果也要呢?
BundleVirtualProduct?    // 组合 × 虚拟...

在添加第 4 个能力「限购 」后,以简单乘法计算:4 个能力×2 个大类 = 16 个叶子类型。「加一项能力」的代价是「叶子类型翻倍」。

这是「能力中多选」场景用继承的必然下场:正交能力的乘积爆炸

9.3 能力组合重构

重构的核心是三个判断

  1. 这些变化点是"is-a"还是"can"?,都是 "can"(可发货/可限购/可预售)
  2. 有多少维度?,有3+个独立变化维度
  3. 是否运行期可变?,同一 SKU 在不同阶段可能被动态加上「限购」

三个都是 "是",「能力组合」是唯一答案。

java 复制代码
// 能力接口(只表达「能干什么」)
public interface Shippable      { ShippingPlan plan(); }
public interface PreSalable     { LocalDateTime deliveryDate(); }
public interface PurchaseLimited{ int maxPerUser(); }
public interface Bundling       { List<Product> children(); }

// 商品仅是"能力代理人"
public class Product {
    private final String id;
    private final Money  price;
    private final List<ProductCapability> caps;   // 组合

    public boolean is(Class<? extends ProductCapability> c) {
        return caps.stream().anyMatch(c::isInstance);
    }
    public <T extends ProductCapability> T as(Class<T> c) {
        return caps.stream().filter(c::isInstance).map(c::cast).findFirst().orElseThrow();
    }
}

// 调用例
if (product.is(Shippable.class)) {
    ShippingPlan p = product.as(Shippable.class).plan();
    ...
}

深刻变化

场景 继承版 组合版
加一个能力 叶子类×2 加1个接口
动态上架「限购」 需重新创建实例 caps.add(new PurchaseLimited(...))
测试 继承树越深 mock 越难 能力独立 mock
业务表达力 OverseasPreSaleBundlePhysical [跨境, 预售, 组合, 可发货]

9.4 类图与动态能力

#mermaid-svg-uPzi2hRwlkFdbf1J{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-uPzi2hRwlkFdbf1J .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-uPzi2hRwlkFdbf1J .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-uPzi2hRwlkFdbf1J .error-icon{fill:#552222;}#mermaid-svg-uPzi2hRwlkFdbf1J .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-uPzi2hRwlkFdbf1J .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-uPzi2hRwlkFdbf1J .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-uPzi2hRwlkFdbf1J .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-uPzi2hRwlkFdbf1J .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-uPzi2hRwlkFdbf1J .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-uPzi2hRwlkFdbf1J .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-uPzi2hRwlkFdbf1J .marker{fill:#333333;stroke:#333333;}#mermaid-svg-uPzi2hRwlkFdbf1J .marker.cross{stroke:#333333;}#mermaid-svg-uPzi2hRwlkFdbf1J svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-uPzi2hRwlkFdbf1J p{margin:0;}#mermaid-svg-uPzi2hRwlkFdbf1J g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-uPzi2hRwlkFdbf1J g.classGroup text .title{font-weight:bolder;}#mermaid-svg-uPzi2hRwlkFdbf1J .cluster-label text{fill:#333;}#mermaid-svg-uPzi2hRwlkFdbf1J .cluster-label span{color:#333;}#mermaid-svg-uPzi2hRwlkFdbf1J .cluster-label span p{background-color:transparent;}#mermaid-svg-uPzi2hRwlkFdbf1J .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-uPzi2hRwlkFdbf1J .cluster text{fill:#333;}#mermaid-svg-uPzi2hRwlkFdbf1J .cluster span{color:#333;}#mermaid-svg-uPzi2hRwlkFdbf1J .nodeLabel,#mermaid-svg-uPzi2hRwlkFdbf1J .edgeLabel{color:#131300;}#mermaid-svg-uPzi2hRwlkFdbf1J .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-uPzi2hRwlkFdbf1J .label text{fill:#131300;}#mermaid-svg-uPzi2hRwlkFdbf1J .labelBkg{background:#ECECFF;}#mermaid-svg-uPzi2hRwlkFdbf1J .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-uPzi2hRwlkFdbf1J .classTitle{font-weight:bolder;}#mermaid-svg-uPzi2hRwlkFdbf1J .node rect,#mermaid-svg-uPzi2hRwlkFdbf1J .node circle,#mermaid-svg-uPzi2hRwlkFdbf1J .node ellipse,#mermaid-svg-uPzi2hRwlkFdbf1J .node polygon,#mermaid-svg-uPzi2hRwlkFdbf1J .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-uPzi2hRwlkFdbf1J .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-uPzi2hRwlkFdbf1J g.clickable{cursor:pointer;}#mermaid-svg-uPzi2hRwlkFdbf1J g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-uPzi2hRwlkFdbf1J g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-uPzi2hRwlkFdbf1J .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-uPzi2hRwlkFdbf1J .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-uPzi2hRwlkFdbf1J .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-uPzi2hRwlkFdbf1J .dashed-line{stroke-dasharray:3;}#mermaid-svg-uPzi2hRwlkFdbf1J .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-uPzi2hRwlkFdbf1J #compositionStart,#mermaid-svg-uPzi2hRwlkFdbf1J .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-uPzi2hRwlkFdbf1J #compositionEnd,#mermaid-svg-uPzi2hRwlkFdbf1J .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-uPzi2hRwlkFdbf1J #dependencyStart,#mermaid-svg-uPzi2hRwlkFdbf1J .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-uPzi2hRwlkFdbf1J #dependencyStart,#mermaid-svg-uPzi2hRwlkFdbf1J .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-uPzi2hRwlkFdbf1J #extensionStart,#mermaid-svg-uPzi2hRwlkFdbf1J .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-uPzi2hRwlkFdbf1J #extensionEnd,#mermaid-svg-uPzi2hRwlkFdbf1J .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-uPzi2hRwlkFdbf1J #aggregationStart,#mermaid-svg-uPzi2hRwlkFdbf1J .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-uPzi2hRwlkFdbf1J #aggregationEnd,#mermaid-svg-uPzi2hRwlkFdbf1J .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-uPzi2hRwlkFdbf1J #lollipopStart,#mermaid-svg-uPzi2hRwlkFdbf1J .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-uPzi2hRwlkFdbf1J #lollipopEnd,#mermaid-svg-uPzi2hRwlkFdbf1J .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-uPzi2hRwlkFdbf1J .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-uPzi2hRwlkFdbf1J .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-uPzi2hRwlkFdbf1J .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-uPzi2hRwlkFdbf1J .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-uPzi2hRwlkFdbf1J :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Product
-id String
-price Money
-caps List<Capability>
+is(Class) : bool
+as(Class) : T
Shippable
<> +plan() : ShippingPlan
PreSalable
<> +deliveryDate()
PurchaseLimited
<> +maxPerUser() : int
Bundling
<> +children() : List

注意关系使用了菱形空心o-- (组合/聚合)而非三角<|--(继承)。这不是 UML 语义的细节,是设计哲学的本质差别

9.5 留下三道思考题

答案在第 06 篇开头揭晓。

🟢 易 :上面的商品设计里,Product.caps 我写了类型为 List<ProductCapability>。为什么不写成 SetMap<Class, Capability>?答案其实是"可以都写,但三者的语义不同",你能说出区别吗?

🟡 中 :「限购」能力可以被运行期动态加上 。但这样一来,Product 变得可变。怎么扣住「不变量」又保证「运行期可加能力」?写下你的技术方案。

🔴 难 :现在设计中「Product.is(Shippable.class)」这种调用依然是「类型检查」。你认为这是设计问题吗?如果是,能不能不需要类型检查也能表达『可发货』这件事? 提示:这道题是「设计原则」与「多态」的交叉,是下一篇《设计原则全景图》的入口问题。


10.认知跃迁总结

回到开篇企鹅会飞、鸟类能力正交爆炸、商品体系类爆炸三件事。它们表面不同,本质都是「谁都不能预言『未来会出现哪些能力组合』」。继承需要设计者提前看准层级表,而组合允许心安理得说「以后再说」。

一句话:继承是面向过去的联姻,组合是面向未来的联姻。代码的生命周期里"未来不可预言"是常态,于是在多数场合里"多用组合」胜在了"少用继承"。

到这里,你已经拥有了面向对象的全部「砖块」:对象、特性、契约、依赖、组合。但砖块怎么砥成稳健的房子?这需要一份带骨架的"施工手册",下一篇 06.设计原则全景图 会从「吃 8000 行上帝类」的事故进入 SOLID 与设计模式的全景。

相关推荐
傻啦嘿哟1 小时前
王者荣耀爬虫:爬取全英雄皮肤数据,用Python做可视化分析
开发语言·爬虫·python
AI直播技术杂谈2 小时前
直播画面突然模糊了,排查了三个小时
开发语言·php
菜冻鱼2 小时前
Python-pytorch-数据加载
开发语言·人工智能·pytorch·python·深度学习·机器学习
heimeiyingwang2 小时前
【架构实战】可观测性三支柱实战:Metrics、Logging、Tracing 如何统一落地
开发语言·架构·php
caimouse2 小时前
ReactOS 图形系统分析(53):系统库存对象 — stockobj.c
c语言·开发语言·spring
ZJU_统一阿萨姆3 小时前
【推理优化进阶】通信关键路径:NCCL、RDMA 与计算通信重叠
开发语言·人工智能·语言模型·架构·系统架构
我不是疯子是傻子3 小时前
Qt 程序打包部署全攻略:从 windeployqt 到 Inno Setup 的静默安装与版本迭代实战
开发语言·qt
城管不管4 小时前
重生——第十次面试之开源中国一面挂
java·linux·开发语言·算法·面试·职场和发展·开源
暖核4 小时前
Linux实战入门:从零编写第一个Bash Shell脚本
linux·运维·bash