想象你要开一家餐厅。如果老板、厨师、服务员、收银员全由同一个人兼任,第一天就会乱成一锅粥。于是你定下规矩:每人只干一件事、岗位职责写清楚;想加新菜时不拆厨房,只新增一页菜谱;主厨休假时副主厨能无缝顶上;服务员只看服务手册,不用研究后厨设备说明书;所有电器插在标准插座上,而不是焊死在墙里。 这些朴素的经营智慧,恰好就是软件工程里“设计原则”的核心。构建软件就像经营餐厅:代码会随需求变化不断生长,也可能迅速腐坏。设计原则是代码世界里的“餐厅守则”——它不规定具体做法,却告诉你什么样的架构能活得更久。本文用这间“理想餐厅”作比喻,带你理解五大 SOLID 原则以及 DRY、KISS、YAGNI 等辅助原则,并看到它们如何在你每天写的代码里起作用。

岗位关联

弄懂设计原则,你的代码才能从“能跑”走向“能改”——这是程序员承担大规模系统演进、迈向高级岗位的必修地基。

目录
  1. 可视化演示
  2. 原则总览:餐厅的经营哲学
  3. 单一职责原则:一人一岗
  4. 开闭原则:不砸墙也能加新菜
  5. 里氏替换原则:副主厨能顶班
  6. 接口隔离原则:菜单分成小册子
  7. 依赖倒置原则:标准插座
  8. DRY与KISS:母配方与一页纸菜单
  9. 原则综合运用:经营是权衡的艺术
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示SOLID五大设计原则的模块关系与耦合度评估:单一职责、开闭、里氏替换、接口隔离与依赖倒置,交互切换理解架构解耦。

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

SOLID五大原则架构 SRP单一职责:一个类一个变化方向 OCP开闭:扩展开放修改关闭 S+O+L+I+D 五原则协同降低系统耦合度

原则总览:餐厅的经营哲学

一家餐厅开业前,老板不会先买锅碗瓢盆,而是先定“经营哲学”:分工明确、流程清晰、设备通用、随手能加菜。这些哲学不规定具体菜谱,却决定了餐厅能开多大、能活多久。软件系统的“经营哲学”就是设计原则——它是经验法则,不是语法规则,编译器不报警,但违反它的系统会在需求变化的重压下迅速腐坏。

设计原则的雏形来自二十世纪八十年代面向对象设计的实践总结,其中最著名的是 Robert Martin 归纳的五条 SOLID 原则,后来又补充出 DRY、KISS、YAGNI 等辅助原则。设计原则与设计模式的关系,好比“菜谱的原理”与“具体的菜谱”:模式是前人验证过的固定解法,原则是模式的灵魂。同一个原则可以由多种模式实现,比如开闭原则既能用策略模式落地,也能用装饰器模式落地。

衡量设计好坏常看两个标尺:内聚是“模块内部零件咬合紧密”,耦合是“模块之间互相牵连的松散度”。高内聚、低耦合是所有原则的共同目标——就像餐厅里甜点组内部配合默契,但甜点组与收银台之间只靠一张小票交接,互不干扰。

所以,设计原则就是餐厅的“经营哲学”:它不告诉你具体做哪道菜,而是在招聘、排班、采购时给你判断依据。有了这套哲学,餐厅才能从小店长成连锁品牌,软件才能从小模块长成大系统。

M=1C\displaystyle M = \frac{1}{C}
可维护性 M 与耦合度 C 成反比:模块之间牵连越少,系统越经得起折腾。
  1. 符号 M表示系统的可维护性,即需求变化时改得动、改得稳的能力,是设计质量的核心指标
    MM
  2. 符号 C表示模块间的耦合度,模块越彼此依赖、彼此了解细节,C 就越大
    CC
  3. 反比关系可维护性与耦合度成反比,压低耦合、抬高内聚,是所有设计原则的公共目标
    M=1C\displaystyle M = \frac{1}{C}

单一职责原则:一人一岗

在理想餐厅里,每个员工只有一个岗位:洗碗工绝不兼职做账,厨师绝不亲自端菜。因为“做菜”和“端菜”是两个不同的变化来源——菜式要创新,服务要提速,若由同一人承担,任何一边的变动都会连累另一边。代码世界同理:把不相关的逻辑塞进同一个类,就是让洗碗工兼做账。

单一职责原则(SRP)把这条规矩写成硬约束:一个类应该只有一个引起它变化的原因。注意,“职责”不等于“功能”——一个类可以有好几个方法,但它们必须服务于同一类“变化源”。判断方法非常简单:问一句“谁会要求修改这个类?”如果答案是财务部和市场部两个业务方,那就是两个职责,应该拆开。

实践中,一个“读配置、解析数据、存库、发通知”的多面手类,往往会在一次紧急需求里被改三次,每一次都冒着弄坏其他功能的风险。拆分的依据是“变化轴线”:把同一时间、同一原因的变化放在一起,把不同节奏、不同发起方的变化拆开。

就像餐厅让收银员专注收银、甜点师专注甜点,SRP 让每个类都有清晰的“岗位牌”。之后任何一处改动都只牵动一个岗位,其他同事照常出餐,互不添乱。

F=∑i=1Rfi\displaystyle F = \sum_{i=1}^{R} f_i
类的总变更频率等于所有职责的变更频率之和:职责越多,被改的几率越大,出错的风险也越高。
  1. 职责数 R一个类同时承担的不同职责数量,R 越大,要求修改它的业务方越多
    RR
  2. 单项频率 f_i第 i 个职责引起类变更的频率,每个职责背后都有一个推动变化的业务方
    fif_i
  3. 累加求总总变更频率等于各职责变更频率之和,单一职责让 R=1,F 只来自一个方向
    F=∑i=1Rfi\displaystyle F = \sum_{i=1}^{R} f_i

开闭原则:不砸墙也能加新菜

理想餐厅加新菜时,不会砸墙改厨房。老板只需让厨师照着“标准化菜谱格式”再写一页,挂进菜单夹即可。菜品可以无限增加,而餐具、流程、培训体系纹丝不动。这就是软件世界梦寐以求的升级方式:加功能像贴页,而不是拆墙。

开闭原则(OCP)说:软件实体应该对扩展开放,对修改封闭。意思是系统升级时,应优先通过“新增代码”来应对变化,而不是回头修改已经稳定运行的核心代码。核心做法是抽象——用接口或抽象类定义“菜谱格式”,让新需求以“新实现”的方式加入系统,而不是改动老菜谱。

常见落地手段有策略模式(替换算法)、模板方法(固定流程骨架)、装饰器(动态增强功能)。它们都把“变化的细节”挡在“稳定的骨架”之外。要注意,开闭不是绝对的“一行旧代码都不许碰”,而是把修改控制在小范围、隔离在系统边缘,让核心模块长久稳定。

菜单夹每多一页,厨房墙皮一动不动;你的代码每新增一个子类,老模块的测试用例一行不改还能全部通过。这就是开闭原则带给系统的“添菜自由”。

E=1−NoldNnew\displaystyle E = 1 - \frac{N_{\text{old}}}{N_{\text{new}}}
扩展度 E 越接近 1,说明新增功能时“加的新代码”越多、“改的旧代码”越少,开闭做得越好。
  1. 分子 N_old新增功能时被修改的旧代码行数,理想情况下它应该趋近于零
    NoldN_{\text{old}}
  2. 分母 N_new新增功能的代码行数,代表系统吸收变化的主要方式
    NnewN_{\text{new}}
  3. 计算扩展度E 越接近 1,说明变化由“扩展”承载得越多,老模块越稳定
    E=1−NoldNnew\displaystyle E = 1 - \frac{N_{\text{old}}}{N_{\text{new}}}

里氏替换原则:副主厨能顶班

主厨休假时,副主厨必须能顶上去,出品的口味、出餐的节奏不能有差别。老板不需要知道今天是谁掌勺——这就是“可替换”的信任。可替换的前提不是资历,而是行为一致:副主厨守住了主厨的全部出品标准。

里氏替换原则(LSP)由 Barbara Liskov 提出:所有引用父类的地方,必须能透明地使用其子类对象。注意,继承只给了你“父子名分”,不会自动保证行为兼容。经典反例是正方形继承长方形:长方形设置宽和高是两件独立的事,正方形却要求两者永远相等,于是 setWidth 时 setHeight 被悄悄改写,父类契约就此破裂。

满足 LSP 要做到三点:子类不能削弱父类的前置条件(如“必须非空”变成“可以为空”)、不能增强后置条件(返回值范围不许扩大)、必须保持父类的不变式(如正方形边长相等这类内部约束)。LSP 是继承正确性的“行为契约”,也是“多用组合、少用继承”背后真正的理由。

就像轮换厨师不能砸招牌,LSP 要求“换成子类,系统还像原来一样工作”。任何“为了复用代码而继承”的念头,都要先过一遍替换测试:把父类换成子类,世界会乱吗?

S⊆T ⇒ ∀x∈S, PT(x)\displaystyle S \subseteq T \ \Rightarrow \ \forall x \in S,\ P_T(x)
若 S 是 T 的子类型,那么对 S 的每一个对象 x,父类型 T 承诺的性质 P_T 都必须成立。
  1. 子类型关系声明 S 是 T 的子类型,例如“正方形是长方形”在语义上似乎成立
    S⊆TS \subseteq T
  2. 全称量词对子类型 S 中的每一个对象 x,要求性质无条件成立,不许有例外
    ∀x∈S\forall x \in S
  3. 性质保持P_T(x) 表示对象 x 拥有父类型 T 承诺的全部行为,这是 LSP 的核心约束
    PT(x)P_T(x)
  4. 蕴涵箭头从“是子类型”可以推出“必须可替换”,这就是继承背后的设计契约
    ⇒\Rightarrow

接口隔离原则:菜单分成小册子

在理想餐厅,顾客只看菜单,后厨看设备手册,财务看账本。如果每位顾客进门都要通读一整摞《餐厅运营全手册》,找一道菜会累死。所以菜单拆成前菜、主菜、甜点几本小册子,各取所需。接口亦然:每个调用方只应看到自己关心的那几页。

接口隔离原则(ISP)说:客户端不应被迫依赖它用不到的接口方法。“胖接口”就是那摞全手册——一个接口塞进 20 个方法,每个实现类都得写满 20 个方法的空壳,哪怕只用其中 3 个;一个方法签名变动,所有无关实现都被迫重新编译。

拆分的准则是“按客户端角色”而非“按功能多少”:把接口拆成多个专注的小接口,每个对应一个业务角色,比如订单接口拆成“下单者接口”和“管理员接口”。必要时再用适配器(Adapter),把一个宽接口翻译成客户端需要的窄接口。但过犹不及——接口不是越细越好,拆到每个方法一个接口,就失去了聚合的意义。

菜单被拆成三本小册子,改动甜点菜单不会让前菜菜单重新印刷;接口被拆成角色小接口,改动一个角色的方法签名,其他角色一行代码都不用动。

U=NuNt\displaystyle U = \frac{N_u}{N_t}
接口使用率 U 等于客户端真正用到的方法数除以接口全部方法数;U 越小,说明胖接口污染越严重。
  1. 已用方法数 N_u某个客户端在实际业务中真正调用的接口方法数量
    NuN_u
  2. 接口方法总数 N_t该接口暴露给所有客户端的全部方法数量
    NtN_t
  3. 使用率计算U 越接近 1,接口与客户端越匹配;胖接口场景下 U 常常小于 10%
    U=NuNt\displaystyle U = \frac{N_u}{N_t}

依赖倒置原则:标准插座

理想餐厅的电器全走标准插座取电。电饭煲要更新,拔下旧插头,插上新电器即可,墙里的电线不用重排。餐厅的“业务目标”(供电)与“具体设备”(电饭煲)之间,隔着一层谁都认识的“标准插座”——它让两边的变化互不牵连。

依赖倒置原则(DIP)说:高层模块不应该依赖低层模块,两者都应该依赖抽象;抽象不应该依赖细节,细节应该依赖抽象。没有倒置时,业务层直接 new 一个 MysqlStorage、一个 EmailSender,换数据库或换短信供应商就要改写业务层;倒置之后,业务层只认识 Storage 接口和 Sender 接口,“谁来干活”由外部决定。

落地工具是依赖注入(Dependency Injection):把具体实现通过构造函数、setter 或工厂传入高层模块,而不是在高层模块内部 new 出来。像 Spring 这类框架只是把注入过程自动化,原则本身与框架无关——即便没有框架,手写构造函数注入同样是依赖倒置。

插座让厨房电器“即插即用”,DIP 让业务模块“即插即换”。招牌菜不变,灶台更新换代——双方都只认中间那个标准接口,这就是稳定架构的秘密。

Cdirect=Nh→lNall→0\displaystyle C_{\text{direct}} = \frac{N_{h \to l}}{N_{\text{all}}} \to 0
高层直接依赖低层的比例越低越好;理想情况下所有依赖都指向中间的抽象层,该比值趋近于零。
  1. 直接依赖数 N_{h→l}高层模块直接引用低层具体实现类的次数,每次 new 一个具体类都算一次
    Nh→lN_{h \to l}
  2. 依赖总数 N_all系统中模块间全部依赖关系的次数总和
    NallN_{\text{all}}
  3. 倒置后趋零依赖倒置后,高层不再直接认识低层,依赖方向全部指向抽象,比值逼近零
    Cdirect=Nh→lNall→0\displaystyle C_{\text{direct}} = \frac{N_{h \to l}}{N_{\text{all}}} \to 0

DRY与KISS:母配方与一页纸菜单

理想餐厅的酱汁只有一个母配方,锁在总厨文件柜里,所有菜品的酱汁都从它衍生,而不是每道菜各抄一遍——抄三遍就迟早会有两份配方改得不一致。菜谱永远是一页纸能写完的简单流程,因为复杂菜谱最容易做砸。老板也不会为“可能有一天会来的贵宾”提前布置一整层包间。

DRY(Don't Repeat Yourself)要求每个知识在系统中有且只有一个权威表达;KISS(Keep It Simple, Stupid)要求以最简单可用的方式解决问题;YAGNI(You Aren't Gonna Need It)警告你不要为臆想的需求提前设计。三者从“消除重复、控制复杂度、抑制超前”三个方向守护系统健康。

关键在于理解 DRY 反对的是“知识重复”而非“文本重复”——同一业务规则同时写在数据库约束和前端校验里,即使文字不同也是重复。KISS 可以用圈复杂度度量,每多一个 if 分支,测试路径就多一条,心智负担就重一分;YAGNI 与“抽象上瘾”是相反的两种病,正确姿势是用简单方案起步,等重复和复杂度真实出现时再按原则重构。

母配方只写一份,菜谱只有一页纸,不为幻想中的客人预留包间——DRY、KISS、YAGNI 让餐厅每一分精力都花在真实的经营上,而不是维护冗余和猜测。

V(G)=E−N+2P\displaystyle V(G) = E - N + 2P
圈复杂度是代码分支复杂度的度量:值越大,代码越难读、越难测,KISS 要求把它控制在低位。
  1. 边数 E控制流图中边的数量,代表程序执行路径的跳转次数
    EE
  2. 节点数 N控制流图中基本块的数量,每多一个 if 或循环就多出节点
    NN
  3. 连通分量 P对单个程序通常为 1,表示图中独立连通区域的个数
    PP
  4. 圈复杂度V(G) 越大,分支越多,测试与维护成本越高,KISS 让这个数尽量小
    V(G)=E−N+2P\displaystyle V(G) = E - N + 2P

原则综合运用:经营是权衡的艺术

一家餐厅真正成熟,不是把每条守则都执行到极致,而是老板懂得变通:节日客流爆满时允许服务员临时传菜——短暂打破单一职责,但常态依旧分工明确。原则不是教条,是经营智慧:它们之间有张力,需要按场景调配。

SOLID 与 DRY/KISS/YAGNI 之间确实存在冲突:为了满足 ISP 拆出的细接口,可能违背 KISS 的“简单”;过度追求 DIP 的抽象层,可能撞上 YAGNI 的“你现在用不到”。成熟的工程师把原则当“体检指标”,结合具体上下文决定优先级。实践上推荐“重构三步”:先让代码可测(testable),再在变化点引入抽象,最后用设计模式固化结构。

一个真实演进过程:报表模块初期只用 SRP 把取数、计算、导出拆成三个类;当报表种类增多,用策略模式让每种报表成为子类(OCP);各报表共享的接口由基类保证行为一致(LSP);上层服务只依赖抽象接口(DIP);客户端只拿自己需要的窄接口(ISP)。每一步都小步提交、测试守护。

经营餐厅的智慧,是在分工与简单、扩展与克制之间找平衡。设计原则是地图,不是锁链;用它们指导重构,而不是被它们束缚手脚。真正的高手,看的是系统的长期生命力。

Q=∑i=1nwiqi\displaystyle Q = \sum_{i=1}^{n} w_i q_i
整体设计质量是各原则得分按权重加权求和的结果——没有放之四海皆准的固定权重,只有上下文里的取舍。
  1. 单项得分 q_i第 i 条原则在当前场景下的符合程度,每一条都有发言权
    qiq_i
  2. 权重 w_i该原则在当下项目中的优先级,不同阶段、不同团队权重不同
    wiw_i
  3. 加权求和总分 Q 是权衡的结果,不是机械叠加,而是按项目现实配比
    Q=∑i=1nwiqi\displaystyle Q = \sum_{i=1}^{n} w_i q_i

AI 应用场景

统一模型网关:新增供应商不再改核心

阶段:AI 系统架构设计 / 模型服务化

解决的问题:自研大模型上线后又要接入开源模型与第三方 API,旧架构把“调用 OpenAI”直接写进业务代码,每次换模型都要改业务层并全量回归。参照依赖倒置原则在业务与实现之间插入 ModelProvider 抽象,再配合开闭原则,新增供应商等于“新增一个实现类 + 一行注册”,核心代码零改动。

具体案例:某 AI 应用团队的 LLM 网关起初只接一家模型厂商,重构后两天内接入第二家供应商,业务层代码一行未改,A/B 效果对比通过配置即可切换。

特征计算管线的职责拆分

阶段:数据工程 / 特征平台建设

解决的问题:一个 FeatureService 类同时承担“读源表、洗数据、算特征、写特征库”四件事,任何一路数据源变更都要全量回归。按单一职责与接口隔离原则拆成 SourceReader、FeatureComputer、FeatureStore 三个接口,各自独立演进、独立测试。

具体案例:某推荐系统特征平台完成拆分后,接入新数据源的平均工时从两天降到两小时,单个环节的故障也不再拖垮整条管线。

模型版本升级的兼容守护

阶段:模型部署 / 服务治理

解决的问题:模型从 v1 升到 v2,若输出分布或字段含义改变,下游排序服务会出现异常——这正是违反里氏替换原则的典型表现。引入契约测试,验证新模型在所有旧场景下保持关键行为不变,才能让下游调用方“透明无感”地替换。

具体案例:某搜索团队通过行为契约测试集守护模型升级,v2 模型上线时所有下游服务零改动,线上指标平稳过渡。

Python 演示

python48 行
from abc import ABC, abstractmethod  class LLMProvider(ABC):    """抽象层: 依赖倒置原则的锚点, 高层只认识这个接口"""     @abstractmethod    def chat(self, prompt: str) -> str:        pass  class OpenAIImpl(LLMProvider):    def chat(self, prompt: str) -> str:        return f"[OpenAI] 回复: {prompt}"  class WenxinImpl(LLMProvider):    def chat(self, prompt: str) -> str:        return f"[文心] 回复: {prompt}"  FACTORY: dict = {}  def register(name: str, provider: LLMProvider) -> None:    """新增供应商 = 新增一个实现类 + 一行注册, 核心代码零修改 (OCP)"""    FACTORY[name] = provider  class AIService:    """高层业务: 不依赖具体供应商, 只依赖抽象 LLMProvider"""     def __init__(self, provider: LLMProvider):        self.provider = provider     def ask(self, prompt: str) -> str:        return self.provider.chat(prompt)  if __name__ == "__main__":    register("openai", OpenAIImpl())    register("wenxin", WenxinImpl())     service = AIService(FACTORY["openai"])    print(service.ask("你好"))     service = AIService(FACTORY["wenxin"])    print(service.ask("你好"))
目的:演示依赖倒置与开闭原则——AI 服务不依赖任何具体模型厂商,新增供应商只需注册一个实现类。
  1. class LLMProvider(ABC):定义抽象层,高层与低层都依赖它,这是依赖倒置的锚点
  2. def register(name, provider):系统唯一的扩展点,新增厂商等于新增一行注册
  3. class AIService:高层业务类只认识抽象接口,不认识任何具体实现
  4. self.provider = provider依赖注入:具体实现从外部传入,而不是在内部 new
  5. service = AIService(FACTORY["wenxin"])运行时切换实现,行为一致可替换,体现 LSP

常见误区

误区我用了开闭原则,所以这个类以后一行代码都不能改,谁改谁就是违反原则。

正解开闭原则不禁止修改,它只是说“变化应优先由新增承载”。就像餐厅加新菜不拆墙,但灶台老化还是要换——核心稳定部分尽量不动,边缘和局部可以修改。真正的开闭是“新增为主、修改为辅”,而不是“零修改”。

为何常见:因为原则的名字太有画面感,“对修改封闭”四个字让人误以为修改本身就是罪过。

误区我让子类继承了父类,编译器没报错,那它就是安全的,可以随便替换。

正解继承关系只给了名分,不给行为保证。正方形继承长方形在语法上完全合法,运行时却破坏了“设宽不影响高”的契约。LSP 看的是行为不是语法,必须用契约测试验证替换后的行为一致性。

为何常见:人脑默认“父子继承”就等于“同类复用”,而忽略了语义兼容才是继承的真正目的。

误区接口隔离就是把接口拆到最细,一个方法一个接口,这样肯定最干净。

正解接口的粗细要按“客户端角色”来定,不是越细越好。一个方法一个接口会让聚合关系碎成渣,调用方反而要拼装多个接口,复杂度不降反升。就像菜单不是一页一道菜,而是按前菜、主菜、甜点来分册。

为何常见:把“隔离”误解成“拆分到极限”,忽略了原则背后的目标是降低客户端的无效依赖。

误区项目里引入了 Spring 的依赖注入,所以我们系统就已经符合依赖倒置原则了。

正解依赖注入框架只是自动完成“把实现送进高层”这件事,它不保证依赖方向正确。如果注入的仍然是一个具体类,高层依然依赖低层细节,原则照样被违反。先确认依赖箭头指向抽象,再谈框架加持。

为何常见:因为 DI 框架最显眼、最容易感知,而“依赖方向”这种抽象的东西需要刻意观察才能发现。

误区好代码就是五个原则加 DRY、KISS、YAGNI 全套齐上,越多越安全。

正解原则之间天然存在张力:过度拆接口违背 KISS,过度抽象撞上 YAGNI。真正的好设计是按上下文给每个原则配权重——简单项目少抽象,核心系统重契约。原则是体检指标,不是装饰徽章。

为何常见:初学者喜欢确定性规则,希望有一个“全中”的模板,但设计恰恰是一项权衡的艺术。

面试题

Q1. 下列哪句话最准确地描述了单一职责原则的核心要求?

单选题 · easy

  • A.一个类只有一个引起它变化的原因← 正确答案
  • B.软件实体应该对扩展开放对修改关闭
  • C.高层模块不应该依赖低层模块
  • D.客户端不应依赖它用不到的接口

答案:A。单一职责的核心是“变化的原因”只有一个,就像餐厅洗碗工不做账,避免多个业务方推动同一个类频繁变更。B 是开闭原则,C 是依赖倒置,D 是接口隔离。

Q2. “对扩展开放,对修改封闭”描述的是哪一条原则?

单选题 · easy

  • A.单一职责
  • B.开闭原则← 正确答案
  • C.里氏替换
  • D.接口隔离

答案:B。开闭原则要求实体对扩展开放、对修改封闭,好比餐厅加新菜只加菜谱不动厨房。其余三条分别关注职责、替换与接口拆分。

Q3. 正方形作为长方形子类,设置宽度时改变了高度导致程序行为异常,这说明该设计最可能违背了哪条原则?

单选题 · mid

  • A.单一职责原则
  • B.里氏替换原则← 正确答案
  • C.依赖倒置原则
  • D.接口隔离原则

答案:B。正方形破坏了长方形“宽高独立变化”的行为契约,替换父类后行为不一致,正是里氏替换原则的经典反例。其他三条关注的是职责、依赖方向和接口粒度。

Q4. 一个接口包含十个方法,调用方只用到其中两个,这种设计主要违背了哪条原则?

单选题 · mid

  • A.单一职责原则
  • B.依赖倒置原则
  • C.里氏替换原则
  • D.接口隔离原则← 正确答案

答案:D。客户端被迫依赖大量用不到的方法,这是典型胖接口,对应接口隔离原则。它要求按客户端角色拆分小接口,就像菜单分成前菜、主菜、甜点三本小册子。

Q5. 根据依赖倒置原则,高层模块与低层模块都应该依赖什么?

单选题 · mid

  • A.稳定的抽象接口← 正确答案
  • B.具体的实现类
  • C.全局单例对象
  • D.运行时的容器

答案:A。依赖倒置强调双方共同依赖抽象,抽象不依赖细节,细节依赖抽象。依赖注入框架只是实现手段,容器或单例都不是原则的落点。

Q6. 一个类同时负责数据库读写与界面渲染,它主要违背了哪条原则?

单选题 · easy

  • A.单一职责原则← 正确答案
  • B.依赖倒置原则
  • C.里氏替换原则
  • D.接口隔离原则

答案:A。数据库读写与界面渲染是两个不同的变化方向,对应两类业务方,塞进一个类会让任何一边的改动都危及另一边。这正是单一职责原则要拆开的场景。

Q7. 关于里氏替换原则,下列哪个说法是正确的?

单选题 · hard

  • A.子类可随意改变方法的返回值
  • B.继承关系本身就代表行为可替换
  • C.子类应保持父类的行为契约← 正确答案
  • D.父类必须拥有多个子类实现

答案:C。LSP 的核心是行为契约:子类不能削弱前置条件、不能增强后置条件、必须保持不变式。继承只给名分,行为兼容才让替换安全,父类也不要求必须有多个子类。

Q8. 下列属于 SOLID 设计原则的有哪些?

多选题 · easy

  • A.单一职责原则← 正确答案
  • B.里氏替换原则← 正确答案
  • C.快速失败原则
  • D.接口隔离原则← 正确答案

答案:ABD。SOLID 指单一职责、开闭、里氏替换、接口隔离、依赖倒置五条。快速失败是另一种工程策略,不属于 SOLID 范畴,注意别把原则清单记混。

Q9. 关于开闭原则的落实,下列做法正确的有哪些?

多选题 · mid

  • A.修改稳定的核心类来支持变化
  • B.用抽象接口隔离变化点← 正确答案
  • C.新增功能时优先新增代码← 正确答案
  • D.通过配置装配新增的实现← 正确答案

答案:BCD。开闭原则主张“新增优先、修改为辅”。用抽象隔离变化点、新增子类与配置装配都是扩展之道;修改稳定核心类恰恰是最该避免的做法。

Q10. 关于设计原则的综合运用,正确的理解有哪些?

多选题 · hard

  • A.原则之间冲突时需要权衡取舍← 正确答案
  • B.所有模块应该无脑套用原则
  • C.重构时小步引入新的抽象← 正确答案
  • D.先保证可测试再追求完备← 正确答案

答案:ACD。设计原则是权衡的艺术,不是越多越好;小步重构、测试守护、先可测再完备是推荐路径。无脑套用反而会制造过度设计,撞上 YAGNI 的警告。