你装修过房子吗?电线预埋进墙、插座装好之后,日后想加一台空气净化器、换一款新台灯,你绝不会为了新电器敲掉一面墙、重新布一次线。你只会走到墙边,把插头插进那个早就留好的插座里——电器有千万种,墙上的标准却不变。这就是软件世界里“开闭原则”(Open-Closed Principle,简称 OCP)的日常原型:系统对扩展开放——欢迎新电器接入;对修改关闭——墙体和布线保持原样。 本文要讲的,正是这个原则如何让软件在需求滚滚而来时依然稳如磐石。你会看到“插座标准”对应的抽象与多态、如何用策略模式像换电器一样新增支付方式、怎样用数值指标给抽象设计“验房”,以及什么时候不该盲目在墙上留孔。读完你会发现,好的架构师,本质上是一位懂得在正确位置预埋插座的“装修师”。

岗位关联

对 AI 从业者而言,OCP 是设计可复用、易维护代码的底层方法论。无论是搭建模型服务网关、扩展特征工程管道,还是给智能体接入新工具,OCP 都是让系统经得起需求迭代的护城河。

目录
  1. 可视化演示
  2. 原则定义:一堵不开槽的墙
  3. 抽象与多态:插座标准的诞生
  4. 策略模式:把扩展变成换插头
  5. 工程落地:在变化点预埋插座
  6. 稳定抽象指标:给设计验房
  7. 权衡与边界:不是所有墙都要留孔
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

对比展示OCP违反与遵守的差异,通过策略模式和装饰器模式演示扩展开放修改关闭的实现,点击按钮切换各设计模式视图

本节配有交互动效(进入视口后自动加载,约 3KB)。

OCP核心对比 左侧违反OCP需修改既有代码 if-else分支膨胀维护困难 右侧遵守OCP通过扩展新增 抽象基类开放扩展关闭修改 多态分发替代条件分支 OCP: open for extension, closed for modification 扩展开放修改关闭是OCP核心

原则定义:一堵不开槽的墙

想象你是一位房东,房子已经租出去了。租客今天想装智能门锁,明天想换即热饮水机。聪明的做法是什么?是在装修时就留好标准插座,而不是每来一个新需求就把墙面凿开。1988 年,Bertrand Meyer 在《面向对象软件构造》中把这种智慧写进了软件工程:一个软件实体应当对扩展开放,对修改关闭。

“对扩展开放”,意思是新需求来临时,系统有能力容纳新增的行为;“对修改关闭”,指的是已有的、经过验证的代码不因新需求而被迫改动。两者合起来就是 OCP:用“加新零件”代替“拆旧零件”。违反 OCP 的典型场景是:支付模块里躺着一串 if-else,每接入一种新支付方式,就有人在这串分支里再添一处。第一次加没问题,第十次加时,谁也不敢保证没碰坏原有的转账逻辑——因为修改的是同一段被无数调用方依赖的代码。

反过来说,为什么“修改旧代码”如此危险?因为旧代码往往已被多个模块复用、被测试用例锁定、被线上流量验证。每改一次,回归风险的涟漪就向外扩散一圈。OCP 的价值,就是把这个“扩散半径”压缩到零:

所以,判断一段代码是否遵守 OCP,只需问一句:下一次需求到来时,我要不要回头改它?如果答案是“不用,加个新类就行”,那你就像那位提前留好插座的房东,墙面每一寸都安然无恙;如果答案是“又得去动那坨老逻辑”,说明插座留错了位置,或者压根没留。

新需求⇒新增实现且新需求⇏改动旧代码\text{新需求} \Rightarrow \text{新增实现} \quad \text{且} \quad \text{新需求} \nRightarrow \text{改动旧代码}
这个式子用一正一反两个箭头同时写出 OCP 的“开放”与“关闭”:需求到达时,唯一合法的动作是“新增”,旧代码绝不参与变动。
  1. 新增实现箭头表示新需求通过添加新的实现类来满足,这一侧是“开”。
    新需求⇒新增实现\text{新需求} \Rightarrow \text{新增实现}
  2. 否定箭头\nRightarrow 读作“推不出”,强调新需求不该导致旧代码被修改,这一侧是“关”。
    新需求⇏改动旧代码\text{新需求} \nRightarrow \text{改动旧代码}

抽象与多态:插座标准的诞生

墙上的插座为什么能接住千奇百怪的电器?因为国家统一了插脚标准——电压、频率、孔径,全都标准化。电器内部电路再复杂,插脚必须长成标准模样。这个“标准”,就是软件里的抽象:接口或抽象类。

抽象把“什么方法必须有”(接口契约)和“方法如何实现”(具体逻辑)分开。多态则让同一个调用点运行不同实现。比如定义一个 Payment 接口,声明 pay(amount) 方法;支付宝、微信、银行卡各自实现,但调用方眼里它们都是“一个能付钱的 Payment”。

正是这对组合支撑了 OCP。插座不认识具体电器,只认插脚标准;调用方不依赖具体实现类,只依赖抽象接口。于是,新增实现类不再触碰任何既有代码,只需“长成标准模样”就能被系统接纳。依赖关系从“调用方指向具体类”变成“调用方指向抽象、实现类也指向抽象”——这就是依赖抽象,也是多态背后的稳定关联:

只要你的电器插脚符合国标,插座永远不必认识电饭煲内部的加热管长什么样。软件亦然:只要实现类遵守接口契约,核心模块永远不必认识新来的支付策略内部长什么样。

Client⟶Interface⟵{Impl1,Impl2,…,Impln}\text{Client} \longrightarrow \text{Interface} \longleftarrow \{ \text{Impl}_1, \text{Impl}_2, \ldots, \text{Impl}_n \}
三个节点画出了稳定的依赖结构:调用方与实现类都只认识中间那堵“标准墙”,互不耦合。
  1. Client调用方,例如订单服务。它只依赖抽象的 Interface,不指名道姓依赖某个具体实现。
    Client⟶Interface\text{Client} \longrightarrow \text{Interface}
  2. Interface抽象契约,定义方法签名,相当于电器的“标准插脚”,自身不含业务逻辑。
    Interface\text{Interface}
  3. Impl_i 集合第 i 个具体实现,实现接口定义的方法。花括号表示“任意多个实现可以随时加入”。
    Interface⟵{Impl1,Impl2,…,Impln}\text{Interface} \longleftarrow \{ \text{Impl}_1, \text{Impl}_2, \ldots, \text{Impl}_n \}

策略模式:把扩展变成换插头

插座本身不产生任何功能,它的本事是“让不同的电器插上去就干活”——插上电水壶能烧水,插上充电器能充电。这就是策略模式(Strategy Pattern)的日常版本:定义一族策略接口,把可互换的算法各自封装成类,再由上下文(Context)持有并调用。

在支付场景中,策略模式把 if-else 的一锅乱炖彻底分解:PaymentStrategy 是“插座标准”,AlipayStrategy、WechatStrategy、BankCardStrategy 是插上去的“电器”,PaymentContext 是“墙上的插座”——它只负责持有当前策略、把支付请求委托出去。运行时想换支付方式?一行 setStrategy,就相当于拔下电水壶、插上充电器。

对比两种做法。传统 if-else 中,新增支付方式是“修改”:打开支付函数,找到分支,加一段,重新测试全部流程。策略模式下,新增支付方式是“扩展”:新建一个类实现策略接口,在注册表登记,所有调用方原封不动。改造前后的核心差异,可以用一句话概括——上下文对具体策略一无所知:

换电器不换插座,换策略不换上下文。策略模式把“变化”打包成一件件可拆卸的家电,让系统的扩展像插拔插头一样优雅。

∀ i,  Context(Strategyi)=同一套调用流程×不同行为i\forall\ i,\ \ \text{Context}(\text{Strategy}_i) = \text{同一套调用流程} \times \text{不同行为}_i
这个式子表示:无论注入哪一个策略,上下文的调用流程都不变,变化的只是被委托出去的具体行为。
  1. 任意策略 ∀i任意下标 i,指任意一种策略实现都能被上下文接纳,没有特殊分支。
    ∀ i\forall\ i
  2. Context(Strategy_i)上下文持有第 i 个策略并委托其执行核心动作。
    Context(Strategyi)\text{Context}(\text{Strategy}_i)
  3. 同一套流程 × 不同行为调用骨架固定,行为内容由策略决定——这正是“开”与“关”在运行时的体现。
    同一套调用流程×不同行为i\text{同一套调用流程} \times \text{不同行为}_i

工程落地:在变化点预埋插座

有经验的装修师傅会在厨房多留几个插座,但不会在吊顶上留一排用不上的五孔。工程落地的第一课就是:在需求真正变化之前,想清楚哪里可能装新电器——也就是“识别变化点”。

识别变化点有一套朴素的方法:找需求文档里“以后可能要支持”“目前先支持一种”之类的措辞;看代码里频繁被改动的函数;统计同一处 if-else 被追加过多少次。找到变化点后,第二步是“封装变化”——把会变的部分包进接口背后,让它与稳定部分隔离。支付方式在变,但“发起支付、等待结果、返回状态”的流程不变,所以把流程留在上下文,把变化交给策略。

落地时还需克制“最小扩展”:不为假设中的需求预埋豪华设计,而是让每个新需求都走“新增代码、零改旧码”的通道。假设今天要给支付系统加一种“数字人民币”支付,工作清单是这样的——新增一个类、注册一行配置、端侧传一个新策略名,全程没有一行旧代码被按下删除键:

好房东在装修时留足插座,好程序员在代码里留好扩展位。识别变化点、封装变化、最小扩展,这三板斧让“开”与“闭”从口号变成每天提交代码时的习惯。

接入成本=∣新增类∣+∣新增配置项∣,旧代码修改数=0\text{接入成本} = |\text{新增类}| + |\text{新增配置项}|, \quad \text{旧代码修改数} = 0
这个式子把 OCP 的工程收益量化了:接入新功能的成本收敛在“新增”一侧,旧代码修改数恒为 0。
  1. 接入成本完成一次新功能接入的总工作量,由新增类和新增配置的数量构成。
    接入成本=∣新增类∣+∣新增配置项∣\text{接入成本} = |\text{新增类}| + |\text{新增配置项}|
  2. 旧代码修改数为零OCP 的验收标准——新功能上线,版本管理的 diff 里看不到任何既有文件。
    旧代码修改数=0\text{旧代码修改数} = 0

稳定抽象指标:给设计验房

装修有验收标准:插座离地 30 厘米、承重墙不许开槽。软件设计也需要客观的“验房师”。Robert Martin 提出了一组度量:抽象程度 A、不稳定依赖 I 和主序列距离 D。

抽象程度 A 衡量一个组件里抽象类(含接口)占全部类的比例——A 越接近 1,组件越“抽象”。不稳定依赖 I 衡量组件被“拖下水”的程度:向外依赖越多、被内部依赖越少,I 越接近 1,组件就越容易因为别人的变化而震荡。两者合起来得到主序列距离 D——衡量组件距离“理想平衡线”的远近。抽象且稳定(A 大、I 小)的组件,才是 OCP 喜欢的“承重墙”:

用过这些指标,你就能像监理一样审视自己的模块:抽象度很高却没人依赖的接口,等于在杂物间里装了个豪华插座——中看不中用。指标不是教条,而是帮你在“抽象”与“稳定”之间找到平衡点的量尺。

A=NaNc,I=CoutCin+Cout,D=∣A+I−1∣A=\frac{N_a}{N_c}, \qquad I=\frac{C_{\text{out}}}{C_{\text{in}}+C_{\text{out}}}, \qquad D=|A+I-1|
三个公式分别回答了:组件有多抽象、有多容易被动摇摆、离理想设计有多远。
  1. 抽象程度 ANa 是抽象类与接口数量,Nc 是组件中类的总数,比值越接近 1 越抽象。
    A=NaNcA=\frac{N_a}{N_c}
  2. 不稳定依赖 ICout 是组件向外发出的依赖数,Cin 是外界指向它的依赖数。向外依赖越多,组件越不稳定。
    I=CoutCin+CoutI=\frac{C_{\text{out}}}{C_{\text{in}}+C_{\text{out}}}
  3. 主序列距离 DA 与 I 之和偏离 1 的绝对值。D 越小,组件越靠近“既抽象又稳定”的理想线。
    D=∣A+I−1∣D=|A+I-1|

权衡与边界:不是所有墙都要留孔

装修最忌讳两件事:插座留得太少,日后满屋插线板;插座留得太多,墙面千疮百孔。OCP 也有代价——每多一层抽象,就多一层间接调用,多一份理解成本。无脑给所有类加接口,等于在墙上装满了从未被使用的插座。

所以,成熟的设计者会先问三个问题:这个点真的会变吗?变化的频率有多高?修改一次旧代码代价有多大?三者相乘才是“抽象收益”。只有当收益大于成本时,抽象才值得做。这恰好呼应了 YAGNI(你不会需要它)原则:不要把宝押在想象中的需求上,而是让代码先简单、后进化。当第二个真实变化出现时,再抽出接口,做一次小步重构,成本最低也最安全:

真正的高手不是插座控,而是懂得在“留太多”和“留太少”之间拿捏分寸的装修师。OCP 不是口号式的“能抽象就抽象”,而是基于变化概率的工程决策——以及团队成员共同遵守的默契,比如在 Code Review 中约定:新增功能时,diff 里不许出现核心流程文件。

收益率=P(变化)×变化代价抽象成本>1 ⇒ 动手抽象\text{收益率} = \frac{P(\text{变化}) \times \text{变化代价}}{\text{抽象成本}} > 1 \ \Rightarrow\ \text{动手抽象}
这个式子用“期望收益 vs 抽象成本”的比值,帮你在过度设计与僵化设计之间找到落点。
  1. 期望收益变化发生的概率 P 乘以单次修改旧代码的代价,得到抽象带来的期望收益。
    P(变化)×变化代价P(\text{变化}) \times \text{变化代价}
  2. 抽象成本接口设计、间接调用、团队理解与维护这些抽象所需付出的成本。
    抽象成本\text{抽象成本}
  3. 比值大于 1 才动手收益盖过成本时再抽象;否则保持简单,等待真实变化信号。
    抽象收益抽象成本>1\frac{\text{抽象收益}}{\text{抽象成本}} > 1

AI 应用场景

场景一:LLM 统一网关的多供应商接入

阶段:模型服务接入与路由

解决的问题:传统做法中,业务代码直接调用某个大模型厂商的 SDK,一旦要切换或新增供应商(OpenAI → Claude → 国产大模型),就得逐个修改调用点,回归验证苦不堪言。AI 网关用 OCP 设计抽象出 LLMProvider 接口,每家厂商一个实现类,通过配置注册接入,业务方只依赖 Provider 接口,新增供应商时核心路由零改动。

具体案例:某 AI 应用团队在网关层定义统一的 chat() 接口,先后接入文心一言、通义千问、GPT-4 三个实现类。某供应商接口升级导致超时,团队新增一个带超时重试的包装类并注册,全站路由零改动完成容灾切换。

场景二:智能体工具生态的插件化扩展

阶段:Agent 工具调用与扩展

解决的问题:智能体需要不断挂载新工具(查天气、订机票、调数据库)。若维护一个巨型 if-else 路由表,每加一个工具就改一次调度器,指令冲突与回归风险随工具数量爆炸。基于 OCP 设计 Tool 接口,每个工具是独立实现类,通过注册表接入,调度器永不修改。

具体案例:某 Agent 平台定义了 execute(input) -> output 的 Tool 接口,新工具只需实现接口并在 tools.json 中登记,即可被大模型的函数调用机制发现与使用,核心调度代码自上线后未再改动。

场景三:特征管道中的特征处理器扩展

阶段:特征工程与训练数据流水线

解决的问题:风控模型的样本特征经常要新增(新设备指纹、新时序聚合)。若特征提取逻辑散落在训练脚本各处,新增特征就要重跑全部验证。基于 OCP 定义 FeatureExtractor 接口,每个特征一个处理器,管道按注册顺序编排执行,新特征即插即用。

具体案例:某反欺诈团队新增“30 分钟内交易频次”特征时,只新增了 FrequencyExtractor 类并在配置中登记,管道代码零修改,模型实验第二天就上线。

Python 演示

python79 行
from abc import ABC, abstractmethod  class PaymentStrategy(ABC):    """支付策略接口:所有支付方式都要遵守的插脚标准"""     @abstractmethod    def pay(self, amount):        """执行支付并返回描述文本"""  class AlipayStrategy(PaymentStrategy):    def pay(self, amount):        return "支付宝支付 {:.2f} 元".format(amount)  class WechatStrategy(PaymentStrategy):    def pay(self, amount):        return "微信支付 {:.2f} 元".format(amount)  class BankCardStrategy(PaymentStrategy):    def pay(self, amount):        return "银行卡支付 {:.2f} 元".format(amount)  class CryptoStrategy(PaymentStrategy):    def pay(self, amount):        return "数字币支付 {:.2f} 元".format(amount)  class PaymentContext:    """支付上下文:墙上的插座,负责接住任意策略"""     _registry = {}     @classmethod    def register(cls, name, strategy_cls):        cls._registry[name] = strategy_cls     @classmethod    def new_strategy(cls, name):        if name not in cls._registry:            raise ValueError("未知支付方式: " + name)        return cls._registry[name]()     @classmethod    def create(cls, name):        return cls(cls.new_strategy(name))     def __init__(self, strategy):        self._strategy = strategy     def set_strategy(self, strategy):        self._strategy = strategy     def execute(self, amount):        return self._strategy.pay(amount)  def main():    PaymentContext.register("alipay", AlipayStrategy)    PaymentContext.register("wechat", WechatStrategy)    PaymentContext.register("bank", BankCardStrategy)     ctx = PaymentContext.create("alipay")    print(ctx.execute(199.0))     ctx.set_strategy(PaymentContext.new_strategy("wechat"))    print(ctx.execute(99.5))     # 新支付方式:只新增一个类 + 一行注册,旧代码零改动    PaymentContext.register("crypto", CryptoStrategy)    ctx.set_strategy(PaymentContext.new_strategy("crypto"))    print(ctx.execute(1.0))  if __name__ == "__main__":    main()
目的:用策略模式搭建一个支付系统,演示“新增支付方式只加类、不改旧代码”的 OCP 落地过程。
  1. class PaymentStrategy(ABC):插座标准:声明所有支付方式必须具备的统一能力
  2. @abstractmethod强制每个子类实现 pay,防止“插脚缺失”的电器混进来
  3. PaymentContext._registry = {}注册表:记录“插头型号”与实现类的对应关系
  4. def register(cls, name, strategy_cls)开放给外部的登记口,新策略只需在此报到
  5. def new_strategy(cls, name)按名字创建策略,未知名字抛出异常,保护核心流程
  6. def execute(self, amount)核心调用流程,永远只做一件事:委托给当前策略
  7. ctx.set_strategy(PaymentContext.new_strategy("wechat"))运行时换插头,上下文零改动
  8. PaymentContext.register("crypto", CryptoStrategy)这就是 OCP 的缩影:新增能力 = 一行注册

常见误区

误区我给每个类都加了接口,项目肯定满足开闭原则了吧?

正解接口只是 OCP 的必要不充分条件。如果接口设计得糟糕——比如方法签名频繁变化,新增实现类时依然要修改调用方,就等同于插座位置不对,电器照样插不进去。OCP 的验收标准只有一个:新增功能时,旧代码的 diff 是否为零。

为何常见:接口是 OCP 最显眼的“装备”,初学者容易把“形式上的抽象”当成“行为上的开闭”。

误区既然要对修改关闭,那我以后一行旧代码都不能动了?重构也不行吗?

正解“对修改关闭”保护的是稳定的抽象层和被大量依赖的核心流程,不是冻结所有代码。业务实现、内部逻辑、新需求对应的全新逻辑,该改就改、该重构就重构。真正禁止的是:为了新扩展去修改“插座墙体”级别的稳定模块。

为何常见:“关闭”这个词太绝对,人们容易望文生义,把 OCP 理解成代码冻结令。

误区我把 if-else 改成 switch-case,或者用查表法,应该就满足 OCP 了吧?

正解换语法糖不等于换架构。改完 switch 后,新增支付方式依然要打开核心函数加一个 case——修改点还是旧代码。OCP 的关键是“变化是否移出了旧代码”,移到策略类加注册表里才算真正关闭。判断方法很简单:下次加功能时,被 diff 的文件里还有没有那个核心函数。

为何常见:面向过程的思维惯性让人关注“怎么写分支”而不是“分支要不要存在”。

误区OCP 说要对扩展开放,那我现在就把所有可能变化的点都抽象出来,一劳永逸。

正解过早抽象是负债不是资产——它带来理解成本、调试成本和团队认知负担。YAGNI 原则提醒我们:当第二个真实变化出现时再抽象,成本最低。就像装修不会在每面墙上都留电视插座,只会根据真实居住需求预埋。抽象要跟随变化信号,而不是跟随想象力。

为何常见:把“拥抱变化”误解为“预测变化”,而工程实践反复证明:预测往往不准,封装现实变化才有价值。

面试题

Q1. 开闭原则(OCP)的核心主张是?

单选题 · easy

  • A.对扩展开放、对修改关闭← 正确答案
  • B.对修改开放、对扩展关闭
  • C.仅对扩展关闭、修改任意
  • D.所有改动一律禁止

答案:A。OCP 的名字就藏在两个短语里:Open for extension, Closed for modification。就像房子留好插座标准,新电器(扩展)随便插,但墙体和布线(既有代码)不许动。B 把方向完全颠倒,C、D 否定了“开放”这一面。

Q2. 以下哪项做法最符合 OCP?

单选题 · easy

  • A.新增功能时修改核心流程函数
  • B.新增实现类并通过注册表接入← 正确答案
  • C.删除抽象接口直接依赖具体类
  • D.复制旧类改个名字立即使用

答案:B。B 是标准答案:新增实现类、在注册表登记,核心流程零改动,正是“对扩展开放、对修改关闭”。A 是典型违反——改的是核心函数;C 把抽象拆掉,未来扩展无门;D 虽不碰旧代码,却制造了重复,且复制品与旧类没有抽象契约约束。

Q3. 插座与电器的比喻中,“插座标准”(统一的插脚规格)对应软件中的哪个概念?

单选题 · easy

  • A.具体实现类与内部逻辑
  • B.抽象接口与统一契约← 正确答案
  • C.配置文件与运行环境
  • D.调用方与业务流程

答案:B。插脚标准是电器必须遵守的“契约”,对应抽象接口。具体实现类是电器本身,配置文件是“登记了哪些电器”的清单,调用方是来用电的人。记住:标准定契约,电器做实现。

Q4. 策略模式中,负责持有策略并委托调用的角色是?

单选题 · mid

  • A.定义策略方法签名的接口层
  • B.实现各自支付逻辑的策略类
  • C.持有策略并委托调用的上下文← 正确答案
  • D.扫描加载策略配置的启动器

答案:C。上下文 Context 是“墙上的插座”:持有策略引用,把请求委托给策略执行,自己既不写实现细节也不写分支。策略接口只定义契约,具体策略类负责干活,启动器负责装配,都不直接“持有并调用”。

Q5. 抽象程度指标 A = Na / Nc 中,Na 表示什么?

单选题 · mid

  • A.抽象类与接口的总数量← 正确答案
  • B.类中抽象方法的总数量
  • C.被外部组件依赖的总数量
  • D.所有具体实现类的总数量

答案:A。Na 是 abstract classes 与 interfaces 的数量,Nc 是组件内类的总数,比值衡量组件的“抽象浓度”。抽象方法数量与这个指标无关,外部依赖数量属于不稳定指标 I 的范畴,具体实现类数量是 Nc 的一部分。

Q6. 主序列距离 D = |A + I - 1|,当 D 接近 0 时说明什么?

单选题 · hard

  • A.组件抽象程度与稳定度严重失衡
  • B.组件实现了抽象与稳定的理想平衡← 正确答案
  • C.组件既无抽象也不被任何组件依赖
  • D.组件必然完全满足开闭原则要求

答案:B。D 衡量组件离“主序列线”(A+I=1)的距离。D 接近 0 说明“既足够抽象又足够稳定”——理想的承重墙。A 是 D 很大时的状态;C 是 D=1 的极端情况;D 中“必然”过于绝对,指标只能辅助判断,不能保证设计正确。

Q7. 以下哪种做法属于过度应用 OCP?

单选题 · hard

  • A.针对确认会变化需求点做策略抽象
  • B.为所有类无脑增加抽象接口层← 正确答案
  • C.用工厂隔离不稳定外部依赖关系
  • D.在第二个真实变化出现时提炼接口

答案:B。OCP 要求收益高于成本才动手。为所有类加接口,等于在每面墙上装插座——抽象成本爆炸、调用链膨胀,却几乎没有真实变化信号支撑。A、C 都有明确变化依据,D 是 YAGNI 倡导的渐进式抽象,都属合理。

Q8. 以下哪些是违反 OCP 的典型信号?

多选题 · mid

  • A.每次新增功能都要修改核心流程函数← 正确答案
  • B.新增实现类后调用方完全不受影响
  • C.同一类业务分支判断散落多个文件← 正确答案
  • D.抽象接口稳定且实现互相独立

答案:AC。A 是“修改关闭”被破坏的直接证据;C 说明变化点没有封装进单一抽象,散落的分支意味着每次扩展都要去多个文件打补丁。B、D 恰恰是 OCP 应有的健康状态。

Q9. 实现“支付方式可扩展”,哪些做法符合 OCP?

多选题 · mid

  • A.定义统一的支付策略抽象接口层← 正确答案
  • B.在核心支付函数内不断追加分支
  • C.用注册表登记各类策略实现类← 正确答案
  • D.新增支付直接改写支付核心代码

答案:AC。定义抽象接口(A)确定“插脚标准”,注册表(C)让新增策略只登记不改旧代码,二者配合实现开闭。B、D 的修改点都在核心代码,属于典型违反。

Q10. 关于稳定抽象指标,下列说法正确的是?

多选题 · extreme

  • A.不稳定性 I 越大组件越容易被波及← 正确答案
  • B.抽象程度 A 越大表示抽象级别越低
  • C.主序列距离 D 越小抽象稳定越平衡← 正确答案
  • D.指标数值越好设计质量就必然越高

答案:AC。I=Cout/(Cin+Cout),向外依赖越多 I 越大、越脆弱,A 对。A 越大抽象度越高,B 说反了。D 接近 0 对应主序列线,C 对。指标是参考而非判决,“必然”过于绝对,D 错。