组合优于继承

共 19 题
📑 题目列表 19 题
#
★★★

1. Decorator 模式作为组合的动态扩展(Java IO 流、JS HOC)

装饰器(Decorator)模式如何作为组合的动态扩展?举 Java IO 流与 JS HOC 的例子?

  • Decorator 模式的结构
  • 动态扩展能力
  • Java IO 与 JS HOC 实例

装饰器(Decorator)模式通过组合动态地为对象添加职责:装饰器持有被装饰对象的引用,实现相同接口,在调用时先/后增强行为。它不修改原对象、不改变接口,可在运行时按需组合——这是"组合优于继承"的典型:继承是静态的(编译期固定),装饰器是动态的(运行时叠加)。Java IO 流是经典实例:new BufferedInputStream(new FileInputStream(f)) 层层包装,Buffer 装饰器为文件流加缓冲,不改变 InputStream 接口。JS 的 HOC(Higher-Order Component)也是:withAuth(withLogging(Component)) 用函数组合包装组件,为组件动态加认证、日志能力。

装饰器把"扩展"从继承的多层子类改为"组合的层层包装",灵活且避免类爆炸。它让职责可自由叠加、可复用,且符合 OCP(新增装饰器即扩展)。是"组合优于继承"最直观的模式。

#
★★★

2. Effective Java 的"组合优于继承"原则;继承的破坏性(脆弱基类问题、菱形继承)

解释 Effective Java 的"组合优于继承"原则,以及继承的破坏性(脆弱基类、菱形继承)?

  • 组合优于继承的核心观点
  • 脆弱基类问题
  • 菱形继承问题

Effective Java 第 18 条"组合优于继承":继承在"非 is-a 关系"或"跨包继承"时是危险的,应优先用组合(包含 + 委托)。继承的破坏性:脆弱基类问题——基类内部实现细节变化会静默改变子类行为,子类覆写与基类新方法冲突时可能意外覆盖;菱形继承——多继承中基类被共享,方法/字段歧义,C++ 需用虚继承解决,Java 通过接口默认方法避免冲突但仍有规则。组合通过"持有对象 + 委托方法"避免子类与基类内部耦合,只依赖接口,更安全、更可测试。

组合优于继承的论据是"继承泄漏封装"。子类依赖基类实现细节,基类一变子类就破。组合把依赖限制在接口上,通过委托实现复用,隔离变化。Effective Java 明确指出:除非是真正的 is-a 且基类设计为可继承,否则用组合。

#
★★★

3. Fragile Base Class Problem(脆弱基类问题)中基类修改导致子类行为不可预期

解释脆弱基类问题(Fragile Base Class Problem),并说明它如何导致子类行为不可预期?

  • 脆弱基类问题定义
  • 基类修改如何影响子类
  • 规避手段

脆弱基类问题(Fragile Base Class Problem)指:基类的内部实现细节(未公开但被继承的成员)一旦修改,子类行为可能发生不可预期的变化。因为子类通过继承与基类内部耦合,基类重构、新增方法、改变字段或方法签名,都可能让子类覆写意外失效、方法覆盖冲突、或行为改变。例如基类新增一个 public 方法,恰好与子类已有同名方法碰撞,子类方法被意外覆写。规避手段:用组合替代继承(子类只依赖基类接口)、把基类设计成 final 或明确"可继承契约"、用接口默认方法、避免子类访问 protected 内部实现。

脆弱基类问题说明"继承打破封装":基类是"实现细节被子类依赖"的脆弱共享。组合能避免该问题,因为组合只依赖接口,不依赖内部实现。这也是"组合优于继承"的核心论据。

#
★★★

4. Mixin / Trait 模式作为"受限组合"(Ruby、Scala、Rust)

说明 Mixin / Trait 模式作为"受限组合"在 Ruby、Scala、Rust 中的实现?

  • Mixin/Trait 的概念
  • 各语言实现差异
  • 作为受限组合的价值

Mixin/Trait 是"受限组合":把一组可复用的行为封装成可被多个类"混入"的模块,类通过 include/mixin 复用行为,而不建立继承层级。Ruby 的模块(include/Module)可混入方法;Scala 的 trait 可混入且可带状态与实现,自底向上线性化;Rust 的 trait 通过 impl 实现并支持默认方法,可组合到多个类型。它们比继承更可控:混入是"横向"复用行为,而非"纵向"建立 is-a 层级,避免深层继承与菱形问题。Trait 提供默认实现,类可覆写,实现"受限组合"。

Mixin/Trait 介于"继承"与"组合"之间:它们复用行为但不建立强耦合的继承层级,是"受限组合"。相比多继承,线性化/单一来源规则避免菱形歧义。相比手写组合,语法更简洁。

#
★★★

5. State 模式作为组合的状态机替换

State 模式如何作为组合的状态机替换 if-else 嵌套?

  • State 模式结构
  • 用组合替换 if-else
  • 状态转换管理

State 模式用组合把"状态及其行为"封装成独立对象:上下文对象持有当前状态实例,把行为委托给状态对象;状态改变时切换持有的状态对象。这样把"根据状态用 if-else 分支"的代码替换为"面向状态对象的多态分派"。每个状态是一个类,实现同一状态接口,行为内聚;新增状态只需新增类并配置转换,符合 OCP。相比 if-else 嵌套,状态模式可读性、可扩展性、可测试性都更好。是"组合"实现状态机(行为随状态变化)的典型。

State 模式是"组合"的思想:上下文组合状态对象,行为委托。它把状态机从"控制流判断"变成"对象组合"。状态类的多态分派替代 if-else,让状态独立演化、状态转换清晰。

#
★★★

6. 组合优于继承的例外场景中何时继承框架基类仍是唯一现实选择(如 ORM 实体、框架 Controller 基类),如何用工程规则约束其滥用边界?

组合优于继承的例外场景是什么?如 ORM 实体、框架 Controller 基类,如何用工程规则约束滥用?

  • 继承的合理例外
  • 框架强制继承
  • 工程约束规则

组合优于继承,但存在例外:当继承框架基类是"唯一现实选择"时,如 ORM 实体类(JPA 实体需继承基类或实现接口)、框架 Controller 基类(Spring MVC 的 Controller、工具类基类)、某些框架强制要求继承以提供回调/生命周期。这些场景下继承是框架契约,无法用组合替代。约束滥用的工程规则:限定继承于框架边界(业务代码不继承业务代码)、继承层级深度上限(如 ≤3 层)、凡继承必须明确"是否 is-a + 是否覆写 protected 内部"、禁止跨层继承(如持久层实体不被业务继承)、用接口抽象调用方、代码评审把关防止"为复用而继承"。

例外是"框架契约强制继承"。此时继承是约束而非选择。工程规则的核心是"把继承限制在框架边界、控制层级、明确 is-a",防止业务代码间滥用继承。即使继承框架类,也应通过接口向外部暴露,隔离框架耦合。

#
★★

7. final class 的设计意图(不可继承)vs open class 的扩展空间

对比 final class(不可继承)与 open class(可扩展)的设计意图?

  • final class 的意图
  • open class 的扩展空间
  • 默认设计倾向

final class(Java 的 final、Kotlin 默认 final、C# 的 sealed)设计意图是"冻结类型,禁止继承",用于保证类型不变性、安全性与可预测性,防止子类破坏契约或带来脆弱基类问题。open class(Kotlin 显式 open、Java 默认 open、C# 的 virtual)设计意图是"留出扩展空间",允许子类覆写以扩展行为。现代趋势(如 Kotlin)默认 final、显式 open,因为"默认不可继承"更安全,只有明确需要扩展时才开放。final 类让委托/组合更安全,open 类则承担扩展点职责,需精心设计契约。

final vs open 是"默认封闭 vs 默认开放"的设计取向。final 保证安全与稳定,open 提供扩展空间。Kotlin 默认 final 反映"少继承、多组合"的现代理念。设计时:不明确需要扩展的类应 final,确需扩展的类 open 并明确契约。

#
★★

8. protected 字段/方法的耦合泄漏风险与替代(package-private、protected accessor)

protected 字段/方法的耦合泄漏风险是什么?有哪些替代方案?

  • protected 的耦合泄漏
  • 替代:package-private、protected accessor
  • 封装保护

protected 字段/方法被子类直接访问,导致子类与基类内部实现强耦合——子类依赖基类受保护成员的实现细节,一旦基类内部变化,子类行为不可预期(脆弱基类问题)。且 protected 暴露的范围(子类)难以控制,泄漏了封装。替代方案:用 package-private(包内可见)限制暴露范围,仅同包可访问;用 protected accessor(受保护的访问器方法)而非直接暴露字段,让子类通过方法访问,隔离实现变化;若无需子类访问,用 private 完全封装。核心原则是"最小暴露":只暴露子类真正需要的最小接口。

protected 是"半封装",在继承场景下泄漏实现。最小暴露原则要求:能 private 就 private,需包内共享用 package-private,需子类用才用 protected accessor 而非常量字段。这样隔离内部变化,减少脆弱基类问题。

#
★★

9. 组合 vs 继承的对比中复用、扩展与脆弱性?

从复用、扩展、脆弱性角度对比组合与继承?

  • 复用方式差异
  • 扩展方式差异
  • 脆弱性差异

复用:继承是"静态复用"——通过继承基类获得基类方法;组合是"动态复用"——持有对象并通过委托调用其方法。扩展:继承扩展是"覆写/新增子类",编译期固定、易产生深层层级;组合扩展是"注入不同的策略/委托对象",运行时可变、更灵活。脆弱性:继承脆弱——子类依赖基类内部实现,基类变化波及子类(脆弱基类);组合稳健——只依赖接口,对象可替换,隔离变化。总体:组合在扩展灵活性与脆弱性上优于继承,继承在"is-a 且骨架稳定"时简洁。现代实践倾向组合。

三者对比的核心是"依赖面向接口还是面向实现"。继承绑定内部实现,组合绑定接口。因此组合在灵活(运行时替换)、稳健(隔离变化)上胜出,继承在简单直接(稳定骨架)时胜出。工程上"组合优先、继承谨慎"。

#
★★

10. 「组合的过度使用」反模式中仅为了复用而强行组合导致的对象图爆炸与间接调用链,何时应回归简单继承、接口默认方法或静态工具函数?

组合的过度使用反模式是什么?何时应回归简单继承、接口默认方法或静态工具函数?

  • 组合过度使用的反模式
  • 对象图爆炸与间接调用链
  • 回归的时机

组合过度使用反模式:仅为"复用"而强行组合,把每个小行为都包成对象并注入,导致对象图爆炸(大量嵌套对象、构造复杂)、间接调用链过长(一个操作要穿透多层委托,可读性差、调试难)、以及无意义的包装层。应回归的时机:当组合层只是"转发"而无额外行为时,用简单继承或接口默认方法更简洁;当逻辑是纯函数、无状态、无多态需求时,用静态工具函数(如 StringUtils)而非包装对象;当只是复用一组方法而不需要运行时替换时,接口默认方法可提供实现。核心是"组合服务于变化与扩展,而非为复用而包装"。

组合的代价是间接性。过度组合把简单逻辑复杂化,违背 KISS。回归判断:无状态纯函数 → 静态工具;无运行时替换需求 → 接口默认方法/简单继承;逻辑简单单点 → 直接调用。组合应服务于"变化的隔离",而非机械复用。

#
★★

11. Template Method 模式中继承复用算法骨架的经典场景,与策略或组合方案相比的取舍与滥用边界?

模板方法(Template Method)模式如何用继承复用算法骨架?与策略/组合相比的取舍与滥用边界?

  • 模板方法结构
  • 继承复用骨架
  • 与策略/组合的取舍

模板方法(Template Method)在基类中定义算法骨架(固定步骤),把可变步骤声明为抽象方法,由子类实现,从而复用骨架、定制步骤。这是"继承复用算法骨架"的经典场景。与策略/组合相比:模板方法用继承(子类覆写步骤),策略用组合(注入算法对象)。模板方法适合"骨架稳定、步骤变化且固属于该类"的场景;策略适合"算法整体可替换"的场景。取舍:模板方法继承耦合基类,若步骤变化频繁或多变体,用策略更灵活;模板方法滥用边界:当子类只为覆写一个步骤而存在、或骨架不稳定、或继承层级加深时,应改用策略或组合。模板方法用于"固化流程"是合理的,但不宜过度子类化。

模板方法用继承复用"固定骨架",策略用组合复用"可换算法"。骨架稳定用模板方法,算法可换用策略。滥用边界:子类层级过深、覆写步骤过多变体、骨架频繁变化时,策略/组合更优。模板方法应控制在"稳定的流程 + 明确的变异点"。

#
★★

12. 扩展第三方类时的继承覆写 vs 组合包装(Adapter/委托)中对升级兼容性与可测试性的影响如何取舍?

扩展第三方类时,继承覆写与组合包装(Adapter/委托)对升级兼容性与可测试性有何影响?如何取舍?

  • 继承覆写第三方类的风险
  • 组合包装的优势
  • 升级兼容性与可测试性

扩展第三方类时,继承覆写有风险:第三方类升级时可能改变内部实现、新增方法冲突、或覆写方法与基类新版本不兼容,导致升级崩溃;且无法测试第三方基类内部行为。组合包装(Adapter/委托)持有第三方实例并封装所需接口,只依赖其公开 API,升级时第三方内部变化不影响包装层(只要 API 稳定);可测试性更好(可 mock 第三方、包装层隔离);兼容性更强(控制暴露面)。取舍:继承覆写仅在第三方明确支持继承且扩展点稳定时可选;多数情况用组合包装,尤其当第三方升级频繁、需要控制依赖面时。组合包装是"委托"不依赖内部实现,更稳健。

升级兼容性上,继承绑定第三方内部实现(易碎),组合包装只依赖公开 API(稳定)。可测试性上,组合包装可 mock 隔离。因此扩展第三方类优先组合包装,继承覆写只在第三方"设计为可继承"时谨慎使用。

#

13. 继承层级深度(≤3-4 层)的项目约束与可视化工具

如何约束继承层级深度(≤3-4 层),有哪些可视化工具?

  • 继承层级深度的约束
  • 深度过深的危害
  • 可视化工具

继承层级深度约束(常见 ≤3-4 层)用于防止深继承带来的脆弱性、可读性下降与理解困难。深度过深时,子类间接依赖多层基类,行为难以追踪,任何基类变化都可能穿透多层影响。工程上通过静态分析工具检查类继承深度(DIT, Depth of Inheritance Tree),超限即告警。可视化工具:IDEA 的 Type Hierarchy 视图、Java 工具(如 javap、JArchitect、SonarQube 的 DIT 指标)、Go 的 godepgraph、Python 的继承可视化、UML 工具(StarUML、PlantUML)可绘制继承树。约束深度的同时,应优先用组合/接口减少层级。

继承深度是设计健康的信号。DIT 指标衡量继承深度,超限提示"可能过度继承"。可视化工具帮助工程师看清层级,识别深继承与上帝类。约束深度 + 组合优先是控制继承复杂度的组合拳。

#

14. 继承的陷阱中深层继承层次与菱形问题?

继承的陷阱包括深层继承层次与菱形问题,具体指什么?

  • 深层继承的陷阱
  • 菱形问题的本质
  • 规避方式

深层继承陷阱:层级过深导致子类依赖多层基类,行为晦涩、脆弱基类风险放大、调试困难、可读性差。菱形问题:多继承(B、C 都继承 A,D 继承 B 和 C)时,A 的成员被 D 继承两次,产生歧义与方法冲突。C++ 用虚继承解决,Java 用接口 + 默认方法(冲突时强制覆写或通过规则解决),Ruby mixin 用线性化。规避:控制继承深度(≤3-4 层)、优先组合/接口、单继承时注意范围,多继承/混入注意冲突规则。深层继承与菱形都是"继承结构复杂化"的代价,应尽量用组合替代。

深层继承放大脆弱性,菱形多继承产生共享状态歧义。两者都源于"继承结构过度复杂"。规避核心是"减少继承、多用组合"。理解语言对多继承的解决机制(虚继承、线性化、接口默认)有助于跨语言设计。

#

15. 组合的实现中委托与策略注入?

组合如何通过委托与策略注入实现?

  • 委托机制
  • 策略注入
  • 组合的两种实现

组合通过"持有对象 + 委托"实现复用:一个类持有另一个对象的引用,把操作委托给它,而不是继承它。委托是组合的基础——类调用被持有对象的方法来复用行为。策略注入是组合的另一种形式:把"算法/行为"作为策略对象注入到类中,类在运行时委托给策略,实现行为可替换。二者结合:委托实现复用,策略注入实现变化。例如支付类持有 PaymentStrategy 并委托其 pay(),不同支付方式注入不同策略,运行时切换。组合的优点(解耦、可替换、可测试)正是通过委托 + 策略注入实现的。

委托与策略是组合的两大实现。委托复用"能力",策略注入替换"行为"。相比继承,组合用"引用 + 委托"达到复用,用"注入 + 替换"达到扩展,灵活且低耦合。

#

16. 何时继承仍合适,is-a 关系的确认?

何时继承仍合适?如何确认 is-a 关系?

  • is-a 关系的确认
  • 继承的合适场景
  • 判断方法

继承合适的前提是"成立真正的 is-a 关系":子类在行为上能完全替换父类(LSP),且父类设计为可继承(开放扩展点、契约稳定)。确认 is-a 除了逻辑语义(狗是动物),更要验证"行为可替换"——子类应强化而非改变父类契约,且没有"空实现/抛异常"的子类方法。合适的继承场景:框架强制的基类继承、模板方法复用稳定骨架、is-a 且层级浅、父类契约明确且稳定。判断方法:用 LSP 测试——任何使用父类的地方替换成子类是否行为不变;若需要 instanceof 或类型判断,则 is-a 不成立。

is-a 不仅是语义上,更是行为上(LSP)。语义 is-a 但行为 is-a 不成立(正方形-矩形)时不应继承。确认 is-a 的可靠方法是"替换测试":子类能否安全替换父类。若替换不安全,用组合。

#

17. Mixin/接口默认方法的组合?

Mixin 与接口默认方法如何实现组合?

  • 接口默认方法的作用
  • Mixin 组合
  • 横向复用

接口默认方法(Java 8+ 的 default 方法)与 Mixin 实现"横向组合":把可复用行为作为接口的默认实现,类通过实现接口自动获得该行为,无需继承。这样多个行为可"混入"同一类,实现"组合优于继承"的横向复用。例如接口 Identifiable 提供默认的 getId(),任何实现该接口的类都获得身份能力,多个这种"行为接口"可组合到一个类。相比继承,接口默认方法/混入:不建立强耦合层级、可在多个类间复用、避免菱形歧义(冲突时需覆写)。Mixin 是"受限组合"的体现,边界是默认方法不应携带过多状态,复杂状态仍用组合。

接口默认方法把"行为"从实现中剥离,通过接口混入复用,是组合的语法级支持。它避免继承层级,横向复用行为。注意默认方法适合"无状态行为",有状态行为仍用对象组合。

#

18. 组合的运行时替换中策略的动态切换?

组合如何实现运行时替换,即策略的动态切换?

  • 运行时替换机制
  • 策略动态切换
  • 组合的优势

组合实现运行时替换的关键是"持有接口引用 + 运行时替换实例"。类持有策略接口,通过 setter 或构造器在运行时更换策略对象,行为随之改变,无需修改类代码。例如排序类持有 Comparator 接口,运行时可切换不同比较器。动态切换让系统在运行时改变行为(如切换支付方式、切换算法、切换策略),这是继承做不到的(继承在编译期固定)。实现注意:策略接口要稳定、替换要线程安全(如用 volatile 或不可变引用)、切换逻辑要清晰。这是组合优于继承的重要优势——运行时灵活性。

组合通过"接口引用 + 运行时注入"实现行为替换,继承则编译期固定。动态切换源于组合的"对象可换"特性。策略模式(Strategy)正是此机制的典型应用,让算法族可运行时切换。

#

19. 多重继承的语言机制差异中 C++ 多继承、Java 接口默认方法与 Ruby mixin 各自的问题与组合替代?

对比 C++ 多继承、Java 接口默认方法、Ruby mixin 在多重继承上的机制差异与各自问题,以及组合替代?

  • 三种语言机制
  • 各自问题
  • 组合替代

C++ 多继承:可直接继承多个类,能力最强但易产生菱形问题(共享基类歧义)、状态冲突、复杂,需虚继承解决,认知负担高。Java 接口默认方法:单继承类 + 多实现接口,默认方法提供混入行为,冲突时(两个接口同名默认方法)需覆写解决,规避了菱形状态问题但能力受限。Ruby mixin:模块 include 混入方法,用线性化(祖先链)解决同名冲突,简单但无编译期检查,潜在冲突运行期才暴露。三者共同问题都是"多来源行为冲突"。组合替代:用对象持有 + 委托,把多个行为作为独立对象组合,避免冲突与歧义,是更稳妥的复用方式。

多重继承的机制差异在于"如何解决冲突":C++ 虚继承、Java 接口默认覆写、Ruby 线性化。但都增加复杂度。组合把行为作为独立对象委托,天然避免冲突,是"组合优于继承"在多重继承场景的体现。