← 返回题目列表

Python 里「组合优于继承」是什么意思?什么时候用组合、什么时候用继承?

高频 中等 第 7 / 21 题 更新于 2026/07/31
Python组合继承设计原则

简化版

**「组合优于继承(composition over inheritance)」是一条面向对象设计原则——当你想「复用别的类的功能」时,优先考虑「让一个类『持有』另一个类的对象(组合)」,而不是「继承那个类」。**两种复用方式:① 继承(is-a)——class Dog(Animal),Dog「是一个」Animal,直接获得 Animal 的所有方法(子类和父类强耦合);② 组合(has-a)——class Car: def __init__(self): self.engine = Engine(),Car「有一个」Engine,通过持有对象来用它的功能(松耦合)。为什么组合更好:① 松耦合——组合只依赖对方的「公开接口」,继承依赖父类的「内部实现」(父类改动容易连累子类);② 更灵活——组合能运行时替换(换个 Engine)、能组合多个对象;继承的关系是编译期固定的;③ 避免继承的坑——深继承层次难懂、脆弱基类问题、菱形继承。什么时候用继承:确实是「is-a」关系(正方形是矩形)、要用多态(统一接口)、框架要求(继承某基类)。判断口诀:能说「A 是一个 B」用继承(is-a),只是「A 用到了 B 的功能」用组合(has-a)。核心记忆:组合(持有对象、has-a、松耦合、灵活)优于继承(is-a、强耦合),复用功能优先组合;真正的 is-a 关系 + 多态才用继承。

详细版

组合 vs 继承

维度继承(is-a)组合(has-a)
关系「是一个」「有一个」
复用方式直接获得父类方法持有对象、调其方法
耦合强(依赖父类实现)松(依赖公开接口)
灵活性低(编译期固定)高(运行时可替换)
语法class B(A)self.a = A()
# 继承(is-a):Dog 是一个 Animal
class Animal:
    def eat(self): print("吃")
    def sleep(self): print("睡")
class Dog(Animal):              # Dog 继承 Animal(是一个)
    def bark(self): print("汪")
d = Dog()
d.eat()                         # 直接用继承的方法

# 组合(has-a):Car 有一个 Engine
class Engine:
    def start(self): print("引擎启动")
    def stop(self): print("引擎停止")
class Car:
    def __init__(self):
        self.engine = Engine()  # Car 持有 Engine(有一个)
    def drive(self):
        self.engine.start()     # 通过持有的对象用功能
        print("行驶")

# 组合的灵活:运行时替换
class ElectricEngine:
    def start(self): print("电机启动")
    def stop(self): print("电机停止")
class Car2:
    def __init__(self, engine):
        self.engine = engine    # 注入引擎(依赖注入)
    def drive(self):
        self.engine.start()
gas_car = Car2(Engine())
ev = Car2(ElectricEngine())     # 换个引擎(组合的灵活)

# 继承滥用的坑(不是真正的 is-a)
class Stack(list):              # ✗ Stack 继承 list(不好)
    def push(self, x): self.append(x)
    # 问题:Stack 意外暴露了 list 的所有方法(insert、sort、[]...)
    #       用户可以 stack.insert(0, x) 破坏栈的语义

class Stack2:                   # ✓ Stack 组合 list(好)
    def __init__(self):
        self._items = []        # 持有 list,不暴露它
    def push(self, x): self._items.append(x)
    def pop(self): return self._items.pop()
    # 只暴露栈该有的接口(push/pop),不暴露 list 的其他方法

# 用组合实现"功能复用"(代替 Mixin/多继承)
class Logger:
    def log(self, msg): print(f"[LOG] {msg}")
class Service:
    def __init__(self):
        self.logger = Logger()  # 组合一个 logger,而非继承 LoggingMixin
    def process(self):
        self.logger.log("处理中")

⚠️ 核心洞察:继承是「最强的耦合关系」——子类不仅依赖父类的「公开接口」,还依赖父类的「内部实现细节」(父类改了内部逻辑,子类可能莫名其妙坏掉,这叫『脆弱基类问题』);而组合只依赖对方的「公开接口」(把对方当黑盒用),耦合松得多、更能抵御变化。所以设计原则是「优先组合」:需要「复用某个类的功能」时,先想「能不能『持有』它(组合)而不是『继承』它」。经典反例是「Stack 继承 list」——虽然 Stack 想复用 list 的 append/pop,但继承让 Stack「意外地是一个 list」,暴露了 list 的所有方法(insertsort[] 索引……),用户能用这些方法破坏栈的「后进先出」语义;用组合(Stack 持有一个 list、只暴露 push/pop)就干净——只暴露该暴露的接口。组合还有两个大优势:① 运行时灵活——Car(engine) 可以在创建时注入不同的引擎(依赖注入),继承的父类是写死的;② 避免深继承和多继承的复杂——一层层继承的类难懂、菱形继承(多继承)的 MRO 微妙。但继承不是坏东西——当关系「真的是 is-a」(CircleShape)、需要「多态」(一组对象走统一接口)、或框架要求继承某基类时,继承是自然且正确的选择。判断标准:「A 真的『是一个』B 吗?」是就继承,只是「A 要用 B 的功能」就组合

完整版教学

一、两种复用方式

先理解继承和组合两种复用:

面向对象里"复用别的类的功能"有两种方式:

继承(inheritance):is-a(是一个)
  class Dog(Animal): ...
  → Dog "是一个" Animal,直接获得 Animal 的所有方法
  → 子类扩展/覆盖父类
  → 编译期确定的、强的关系

组合(composition):has-a(有一个)
  class Car:
      def __init__(self):
          self.engine = Engine()   # Car "有一个" Engine
  → Car 持有 Engine 的对象,通过它用功能
  → 运行时确定、松的关系

关系判断(关键):
  "A 是一个 B 吗?"(A is a B)→ 继承
    Dog 是一个 Animal ✓ → 继承
  "A 有一个 B / A 用到 B 的功能吗?"(A has a B)→ 组合
    Car 有一个 Engine ✓ → 组合

复用的本质:
  继承:白盒复用(子类看得到父类内部、依赖实现)
  组合:黑盒复用(只用对方公开接口、不看内部)

"组合优于继承":
  设计原则——复用功能时优先组合、慎用继承
  (不是"永远别用继承",而是"默认组合、有 is-a 才继承")

所以复用有继承(is-a、白盒、强耦合)和组合(has-a、黑盒、松耦合);优先组合

面向对象里「复用别的类的功能」有两种方式继承(inheritance):is-a(是一个)class Dog(Animal)、Dog「是一个」Animal 直接获得所有方法、子类扩展/覆盖父类、编译期确定的强关系);组合(composition):has-a(有一个)class Car: self.engine = Engine()、Car 持有 Engine 对象通过它用功能、运行时确定的松关系)。关系判断(关键):「A 是一个 B 吗?」→ 继承(Dog 是一个 Animal)、「A 有一个 B / A 用到 B 的功能吗?」→ 组合(Car 有一个 Engine)。复用的本质:继承是白盒复用(子类看得到父类内部、依赖实现)、组合是黑盒复用(只用对方公开接口、不看内部)。「组合优于继承」:设计原则——复用功能时优先组合、慎用继承(不是永远别用继承、而是默认组合、有 is-a 才继承)。理解「复用有继承(is-a、白盒、强耦合、编译期)和组合(has-a、黑盒、松耦合、运行时);判断 A 是一个 B 用继承、A 有一个 B/用到 B 功能用组合;组合优于继承(默认组合、有 is-a 才继承)」,就理解了两种复用方式。

二、为什么组合更好

理解组合的优势:

组合优于继承的原因:

① 松耦合(最重要):
   继承:子类依赖父类的"内部实现"(白盒)
     父类改内部逻辑 → 子类可能坏(脆弱基类问题)
   组合:只依赖对方的"公开接口"(黑盒)
     对方改内部实现 → 只要接口不变、我不受影响
   → 组合的耦合松得多、更能抵御变化

② 更灵活(运行时可替换):
   继承:父类是"写死的"(编译期固定)
   组合:可以运行时注入/替换持有的对象
     Car(GasEngine()) / Car(ElectricEngine())
   → 依赖注入、策略模式都靠组合

③ 避免继承的坑:
   ✗ 深继承层次:A→B→C→D,难懂、改一处影响一串
   ✗ 脆弱基类:父类改动连累所有子类
   ✗ 菱形继承(多继承):MRO 微妙、super 复杂
   ✗ 意外暴露:继承 list → 暴露 list 所有方法
   → 组合都能避免

④ 接口清晰:
   组合只暴露"你想暴露的"(封装内部对象)
   继承暴露父类的"所有 public 方法"(可能不想暴露)

⑤ 单一职责 + 可测试:
   组合的各部分职责单一、可独立测试、可 mock
   继承的父子逻辑纠缠、难单独测

一句话:
  继承是"最强的耦合",组合是"松散的协作"
  → 松耦合的系统更好维护、更能变化

所以组合优于继承因:松耦合(依赖接口非实现)+灵活(运行时替换)+避坑(深继承/脆弱基类/意外暴露)

组合优于继承的原因① 松耦合(最重要)——继承子类依赖父类的「内部实现」(白盒、父类改内部子类可能坏、脆弱基类问题)、组合只依赖对方的「公开接口」(黑盒、对方改内部只要接口不变我不受影响);② 更灵活(运行时可替换)——继承父类写死(编译期固定)、组合可运行时注入/替换(Car(GasEngine())/Car(ElectricEngine())、依赖注入/策略模式);③ 避免继承的坑——深继承层次难懂、脆弱基类、菱形继承 MRO 微妙、意外暴露(继承 list 暴露所有方法);④ 接口清晰——组合只暴露想暴露的、继承暴露父类所有 public 方法;⑤ 单一职责 + 可测试——组合各部分职责单一可独立测试可 mock。一句话:继承是「最强的耦合」、组合是「松散的协作」。理解「组合优于继承因:①松耦合(依赖接口非实现、避免脆弱基类)②灵活(运行时替换、依赖注入)③避坑(深继承/菱形/意外暴露)④接口清晰(只暴露想暴露的)⑤可测试;继承是最强耦合、组合是松散协作」,就理解了为什么组合更好。

三、继承的坑

理解继承容易出的问题:

继承的常见坑:

坑1:脆弱基类问题(fragile base class):
  子类依赖父类的内部实现
  父类改了内部逻辑(哪怕接口没变)→ 子类可能坏
  例:父类的方法 A 内部调了方法 B,子类覆盖了 B
      → 父类改 A 不再调 B → 子类行为莫名变化

坑2:意外暴露父类接口:
  class Stack(list): ...   # 想复用 append/pop
  → Stack 暴露了 list 的全部方法(insert/sort/[]/extend...)
  → 用户 stack.insert(0, x) 破坏栈语义(后进先出没了)
  → 继承"是一个"太强,暴露了不该暴露的

坑3:深继承层次:
  A → B → C → D → E
  → 一个方法在哪定义的?MRO 一层层找、难懂
  → 改 A 影响整条链、牵一发动全身

坑4:菱形继承(多继承):
      A
     / \
    B   C
     \ /
      D
  → D 继承 B、C,B、C 都继承 A
  → MRO 复杂、super 调用顺序微妙、易出 bug

坑5:不是真正的 is-a:
  为了"复用几个方法"就继承 → 关系不对
  → 复用是目的、继承是手段,但继承强加了 is-a 关系

坑6:违反 Liskov 替换原则:
  子类不能完全替代父类用(如正方形继承矩形,
  设置宽高会互相影响,破坏矩形的契约)

→ 这些坑组合基本都能避免

所以继承坑:脆弱基类/意外暴露接口/深继承/菱形继承/假 is-a/违反里氏替换

继承的常见坑坑1:脆弱基类问题(子类依赖父类内部实现、父类改内部逻辑子类可能坏,如父类方法 A 内部调 B、子类覆盖了 B、父类改 A 不再调 B 则子类行为变);坑2:意外暴露父类接口class Stack(list) 暴露了 list 全部方法、用户 stack.insert(0, x) 破坏栈语义);坑3:深继承层次(A→B→C→D→E、方法在哪定义难懂、改 A 影响整条链);坑4:菱形继承(多继承)(D 继承 B、C、B、C 都继承 A、MRO 复杂 super 微妙);坑5:不是真正的 is-a(为复用几个方法就继承、关系不对);坑6:违反 Liskov 替换原则(子类不能完全替代父类、如正方形继承矩形设宽高互相影响)。这些坑组合基本都能避免。理解「继承坑:①脆弱基类(父类改内部子类坏)②意外暴露接口(Stack(list)暴露 insert 破坏语义)③深继承难懂④菱形继承 MRO 复杂⑤假 is-a(为复用而继承)⑥违反里氏替换(正方形继承矩形);组合能避免」,就掌握了继承的坑。

四、什么时候用继承

理解继承合适的场景:

继承合适的场景(真正的 is-a + 多态):

① 真正的 is-a 关系:
   Circle 是一个 Shape ✓
   Dog 是一个 Animal ✓
   SavingsAccount 是一个 Account ✓
   → 子类"确实是"父类的一种,语义正确

② 需要多态(统一接口):
   class Shape:
       def area(self): ...
   shapes = [Circle(), Square(), Triangle()]
   total = sum(s.area() for s in shapes)   # 统一当 Shape 用
   → 一组对象走统一接口、可互换 → 继承+多态自然

③ 框架/库要求继承:
   class MyView(django.views.View): ...    # Django 要求
   class MyModel(models.Model): ...
   class MyException(Exception): ...        # 自定义异常
   → 框架的扩展点是"继承某基类"

④ 抽象基类定义接口:
   class Serializer(ABC):
       @abstractmethod
       def serialize(self): ...
   → 用继承 + 抽象方法强制接口

⑤ 复用 + 扩展(真的是特化):
   子类"是父类的一种特化",加特有行为
   class AdminUser(User):   # 管理员是一种用户
       def ban_user(self): ...

判断"该继承":
  ① 能说"A 是一个 B"(is-a,语义正确)
  ② 子类能完全替代父类用(里氏替换)
  ③ 需要多态、或框架要求

不该继承(改用组合):
  ① 只是想复用几个方法(不是 is-a)
  ② 关系是"A 有 B / A 用 B"(has-a)
  ③ 不想暴露父类的全部接口

所以继承用于:真 is-a+多态+框架要求+抽象接口;满足里氏替换、语义正确才继承

继承合适的场景(真正的 is-a + 多态)① 真正的 is-a 关系(Circle 是一个 Shape、Dog 是一个 Animal、子类确实是父类的一种语义正确);② 需要多态(统一接口)shapes = [Circle(), Square()] 统一当 Shape 用、sum(s.area() for s in shapes)、一组对象走统一接口可互换);③ 框架/库要求继承class MyView(django.views.View)class MyException(Exception)、框架的扩展点是继承某基类);④ 抽象基类定义接口class Serializer(ABC): @abstractmethod、用继承+抽象方法强制接口);⑤ 复用 + 扩展(真的是特化)class AdminUser(User) 管理员是一种用户)。判断「该继承」:① 能说「A 是一个 B」(语义正确)、② 子类能完全替代父类用(里氏替换)、③ 需要多态或框架要求。不该继承(改用组合):只是想复用几个方法(不是 is-a)、关系是 has-a、不想暴露父类全部接口。理解「继承用于:①真 is-a(Circle 是 Shape)②多态(统一接口可互换)③框架要求(继承 View/Exception)④抽象接口(ABC)⑤特化(AdminUser 是 User);判断能说 A 是一个 B+满足里氏替换+需要多态才继承」,就掌握了继承的合适场景。

五、组合的实践模式

理解组合的常见用法:

组合的实践模式:

① 持有对象(最基本):
   class Car:
       def __init__(self):
           self.engine = Engine()   # 持有
       def drive(self):
           self.engine.start()      # 委托给持有的对象

② 依赖注入(更灵活):
   class Car:
       def __init__(self, engine):  # 从外面注入
           self.engine = engine
   Car(GasEngine())      # 运行时决定用哪个引擎
   Car(ElectricEngine())
   → 解耦、可测试(注入 mock)

③ 委托/转发(组合 + 暴露部分接口):
   class Stack:
       def __init__(self):
           self._items = []
       def push(self, x): self._items.append(x)   # 委托给 list
       def pop(self): return self._items.pop()
       # 只暴露 push/pop,不暴露 list 的其他方法

④ 策略模式(组合可替换的行为):
   class Sorter:
       def __init__(self, strategy):
           self.strategy = strategy
       def sort(self, data):
           return self.strategy(data)
   Sorter(quicksort) / Sorter(mergesort)

⑤ 用组合替代 Mixin/多继承:
   # 不用 class Service(LoggingMixin, CacheMixin):
   class Service:
       def __init__(self):
           self.logger = Logger()   # 组合
           self.cache = Cache()
   → 各功能是独立对象、松耦合、可替换

组合 + 继承结合(常见):
  用继承表达 is-a(Shape → Circle)
  用组合复用功能(Circle 组合一个 Renderer)
  → 两者不是对立、各司其职

所以组合模式:持有对象/依赖注入/委托转发/策略模式/替代 Mixin;各部分松耦合可替换

组合的实践模式① 持有对象(最基本)self.engine = Engine() + self.engine.start() 委托给持有的对象);② 依赖注入(更灵活)def __init__(self, engine) 从外面注入、Car(GasEngine())/Car(ElectricEngine()) 运行时决定、解耦可测试注入 mock);③ 委托/转发(组合 + 暴露部分接口)(Stack 持有 _items list、只暴露 push/pop 不暴露 list 其他方法);④ 策略模式(组合可替换的行为)Sorter(strategy) + Sorter(quicksort)/Sorter(mergesort));⑤ 用组合替代 Mixin/多继承self.logger = Logger(); self.cache = Cache() 各功能独立对象松耦合可替换)。组合 + 继承结合(常见):用继承表达 is-a(Shape → Circle)、用组合复用功能(Circle 组合一个 Renderer)(两者不对立、各司其职)。理解「组合模式:①持有对象+委托②依赖注入(注入 engine、可测试)③委托转发(只暴露部分接口)④策略模式(可替换行为)⑤替代 Mixin(各功能独立对象);组合+继承结合(继承表 is-a、组合复用功能)」,就掌握了组合的实践模式。

六、总结与实践

总结组合与继承:

核心原则:组合优于继承
  复用功能时优先组合、有真正 is-a 才继承

两种复用:
  继承(is-a):class B(A),直接获得父类方法,强耦合
  组合(has-a):self.a = A(),持有对象用功能,松耦合

组合更好的原因:
  ① 松耦合(依赖接口非实现,抗脆弱基类)
  ② 灵活(运行时替换、依赖注入)
  ③ 避坑(深继承/菱形/意外暴露)
  ④ 接口清晰、可测试

继承的坑:
  脆弱基类、意外暴露接口、深继承、菱形继承、
  假 is-a、违反里氏替换

什么时候用继承:
  ① 真正的 is-a(Circle 是 Shape)
  ② 需要多态(统一接口可互换)
  ③ 框架要求(继承 View/Exception/Model)
  ④ 抽象基类定义接口

判断口诀:
  "A 是一个 B" → 继承(is-a)
  "A 有一个 B / A 用 B 的功能" → 组合(has-a)

实践建议:
  ① 默认用组合,有 is-a + 多态才继承
  ② 别为"复用几个方法"就继承(用组合)
  ③ 继承要满足里氏替换(子类能替代父类)
  ④ 组合 + 依赖注入 → 松耦合、可测试
  ⑤ 组合和继承可结合(继承表 is-a、组合复用)

核心总结:
  组合(持有对象、has-a、松耦合、灵活)优于继承(is-a、强耦合)
  复用功能优先组合,真 is-a + 多态才继承

所以组合(has-a、松耦合、灵活)优于继承(is-a、强耦合),复用优先组合、真 is-a+多态才继承

核心原则:组合优于继承(复用功能时优先组合、有真正 is-a 才继承)。两种复用:继承(is-a、class B(A)、直接获得父类方法、强耦合)、组合(has-a、self.a = A()、持有对象用功能、松耦合)。组合更好的原因:松耦合、灵活、避坑、接口清晰可测试。继承的坑:脆弱基类、意外暴露接口、深继承、菱形继承、假 is-a、违反里氏替换。什么时候用继承:真正的 is-a、需要多态、框架要求、抽象基类定义接口。判断口诀:「A 是一个 B」→ 继承、「A 有一个 B/A 用 B 的功能」→ 组合。理解「组合优于继承;继承 is-a 强耦合、组合 has-a 松耦合灵活;组合好因松耦合+灵活+避坑+可测试;继承坑脆弱基类/意外暴露/菱形;继承用于真 is-a+多态+框架;A 是一个 B 用继承、A 用 B 功能用组合」,就掌握了总结与实践。

记忆钩子:「『组合优于继承(composition over inheritance)』是一条 OOP 设计原则:想『复用别的类的功能』时优先『让一个类持有另一个类的对象(组合)』而不是『继承那个类』;两种复用:①继承(is-a、是一个):class Dog(Animal),Dog 是一个 Animal 直接获得所有方法(子类和父类强耦合)②组合(has-a、有一个):class Car:self.engine=Engine(),Car 有一个 Engine 通过持有对象用功能(松耦合);★为什么组合更好:①松耦合(最重要)——组合只依赖对方的『公开接口』(黑盒),继承依赖父类的『内部实现』(白盒、父类改内部子类可能莫名坏掉=脆弱基类问题)②更灵活——组合能运行时替换(Car(GasEngine())/Car(ElectricEngine())依赖注入),继承的关系编译期写死③避坑——深继承层次难懂/菱形继承 MRO 微妙/意外暴露(class Stack(list)会暴露 list 所有方法、用户能 stack.insert(0,x)破坏栈的后进先出语义,用组合只暴露 push/pop 就干净);★什么时候用继承:确实是 is-a 关系(Circle 是 Shape)、需要多态(一组对象走统一接口 sum(s.area()for s in shapes))、框架要求(继承 View/Exception/Model)、抽象基类定义接口;★判断口诀:能说『A 是一个 B』用继承(is-a)、只是『A 用到了 B 的功能』用组合(has-a);继承还要满足里氏替换(子类能完全替代父类);组合和继承可结合(继承表 is-a、组合复用功能)」

七、常见误区与追问

  • 误区:「组合优于继承」意思是永远不要用继承。 不是——它是说「复用功能时『优先』考虑组合、慎用继承」,而不是「禁止继承」;继承在「真正的 is-a 关系 + 需要多态」的场景是自然且正确的(Circle 是 Shape、需要把一堆形状统一当 Shape 用求面积)、框架也常要求继承某基类;这条原则针对的是「滥用继承」——为了复用几个方法就继承、把 has-a 关系硬做成 is-a;判断标准是「A 真的是一个 B 吗」,是就继承,只是「A 要用 B 的功能」就组合。
  • 误区:继承和组合只是语法不同、效果一样。 效果差别很大——继承是「最强的耦合」:子类不仅依赖父类的公开接口、还依赖父类的内部实现(父类改了内部逻辑,子类可能莫名其妙坏掉,即「脆弱基类问题」)、且子类会「意外继承并暴露父类的所有方法」;组合是「松散的协作」:只依赖对方的公开接口(把对方当黑盒)、能运行时替换持有的对象、只暴露你想暴露的接口;所以组合的系统更松耦合、更灵活、更能抵御变化,两者的设计影响完全不同。
  • 误区:想复用一个类的几个方法,就应该继承它。 不应该——「只是想复用几个方法」不构成「is-a 关系」,为此继承会带来问题:① 强加了「是一个」的语义(明明只是想用功能);② 意外暴露父类的所有方法(可能破坏你的类的语义,如 Stack 继承 list 会暴露 insert/sort 等破坏栈语义的方法);③ 与父类强耦合(父类变动连累你);正确做法是用组合——持有那个类的对象、只调用你需要的方法、只暴露你想暴露的接口(如 Stack 持有一个 list、只提供 push/pop);这样干净、松耦合、语义清晰。
  • 误区:正方形是矩形,所以 Square 应该继承 Rectangle。 数学上是 is-a、但代码继承会出问题(经典的 Liskov 替换违反案例)——如果 Rectangle 有 set_widthset_height(可独立设置),而 Square 继承它,Square 为了保持「正方形」不变式必须让 set_width 同时改 height(反之亦然),这就破坏了 Rectangle 的契约(「设置宽度不应影响高度」)——任何期望 Rectangle 行为的代码用 Square 替换就会出错,违反了里氏替换原则;这说明「继承要满足里氏替换(子类能完全替代父类用)」不只是「语义上是 is-a」;这类情况要么不用继承(组合、或都继承一个更抽象的 Shape)、要么让类不可变(避免 setter 的冲突)。
  • 追问:为什么说「组合优于继承」,组合到底好在哪? 核心是「耦合度」和「灵活性」:① 松耦合——继承让子类依赖父类的「内部实现」(白盒复用,父类改内部逻辑子类可能坏,即脆弱基类问题),而组合只依赖对方的「公开接口」(黑盒复用,把对方当黑盒用,对方改内部只要接口不变我就不受影响),组合的耦合松得多、系统更能抵御变化;② 更灵活——组合持有的对象可以在运行时注入/替换(依赖注入,Car(engine) 能传不同引擎),而继承的父类是编译期写死的、无法运行时改变;③ 避免继承特有的坑——深继承层次难以理解、菱形(多重)继承的 MRO 和 super 调用微妙易错、继承会「意外暴露父类的全部方法」(可能破坏子类语义);④ 接口清晰、可测试——组合只暴露你想暴露的接口(封装内部对象)、各部分职责单一可独立测试和 mock;所以在需要「复用功能」时,优先用组合(持有对象)而非继承,能得到更松耦合、更灵活、更易维护的设计。
  • 追问:什么时候应该用继承而不是组合? 当满足这些条件时用继承:① 真正的「is-a」关系——能自然地说「A 是一个 B」且语义正确(Circle 是一个 Shape、Dog 是一个 Animal、AdminUser 是一种 User),而不只是「A 想用 B 的功能」;② 满足里氏替换原则——子类能完全替代父类使用,任何期望父类的地方都能安全地传子类而不出错;③ 需要多态——你想把一组不同的对象「当作同一个基类」统一处理(shapes = [Circle(), Square()]; sum(s.area() for s in shapes)),让它们走统一接口、可互换,这时继承 + 覆盖方法是最自然的实现;④ 框架/库要求——很多框架的扩展点就是「继承某个基类」(Django 的 View/Model、自定义 Exception 继承 Exception、实现 ABC 抽象基类);简单说:「真的是 is-a、需要多态、或框架要求」时用继承,「只是要复用功能、是 has-a 关系」时用组合;实际项目中两者常结合——用继承表达 is-a 的类型层次和多态,用组合复用具体的功能实现。
  • 追问:Stack 继承 list 和 Stack 组合 list,有什么区别,哪个更好? 组合更好。class Stack(list)(继承):Stack 复用了 list 的 append/pop,但同时「意外地是一个 list」、暴露了 list 的全部方法——用户可以调用 stack.insert(0, x)stack.sort()stack[2]stack.remove(x) 等等,这些操作会破坏栈的「后进先出(LIFO)」语义(栈只应该从顶部 push/pop),Stack 无法阻止用户用这些方法搞乱它;class Stack: def __init__(self): self._items = [](组合):Stack 持有一个私有的 list _items、只对外暴露 push/pop(以及 peek、is_empty 等栈该有的接口),内部用 _items.append/_items.pop 实现,用户拿不到底层的 list、只能通过栈的接口操作,栈的语义被完好封装;这正是「组合优于继承」的经典例证——继承会「意外暴露不该暴露的接口」、破坏封装和语义,组合能「精确控制暴露什么」;所以实现一个有明确语义约束的容器(栈、队列、有序集合),应该用组合包一个内部容器、只暴露该暴露的接口,而不是继承内置容器。

八、加强记忆

「组合优于继承(composition over inheritance)」是一条 OOP 设计原则:想「复用别的类的功能」时优先「让一个类持有另一个类的对象(组合)」而不是「继承那个类」。两种复用① 继承(is-a、是一个)class Dog(Animal),Dog「是一个」Animal 直接获得所有方法(子类和父类强耦合);② 组合(has-a、有一个)class Car: self.engine = Engine(),Car「有一个」Engine 通过持有对象用功能(松耦合)。为什么组合更好① 松耦合(最重要)——组合只依赖对方的「公开接口」(黑盒),继承依赖父类的「内部实现」(白盒、父类改内部子类可能莫名坏掉 = 脆弱基类问题);② 更灵活——组合能运行时替换(Car(GasEngine())/Car(ElectricEngine()) 依赖注入),继承的关系编译期写死;③ 避坑——深继承层次难懂/菱形继承 MRO 微妙/意外暴露(class Stack(list) 会暴露 list 所有方法、用户能 stack.insert(0, x) 破坏栈的后进先出语义,用组合只暴露 push/pop 就干净)。什么时候用继承:确实是 is-a 关系(Circle 是 Shape)、需要多态(一组对象走统一接口 sum(s.area() for s in shapes))、框架要求(继承 View/Exception/Model)、抽象基类定义接口。判断口诀:能说「A 是一个 B」用继承(is-a)、只是「A 用到了 B 的功能」用组合(has-a);继承还要满足里氏替换(子类能完全替代父类)。一句话「组合(持有对象、has-a、松耦合、灵活、只依赖公开接口)优于继承(is-a、强耦合、依赖内部实现);复用功能优先组合、真正 is-a+多态+框架要求才继承;继承要满足里氏替换、别为复用几个方法就继承」。