【面向对象】面向对象设计原则:SOLID五大原则

考点频率:★★★★★(上午选择题必考,下午题设计类图时也常需体现这些原则)
难度:⭐⭐⭐
建议:重点理解每个原则的核心含义软考常考表述,能根据场景判断是否违反了某条原则

1️⃣ 为什么要学SOLID?

前面我们学了面向对象的三大特性(封装、继承、多态),但“会用”不等于“用好”。三个特性只能保证你写出面向对象的代码,但写出来的代码是不是容易维护、容易扩展、容易测试——那是另外一回事。

SOLID五大原则就是面向对象设计的“最佳实践清单”。它们告诉你:怎么用封装、继承、多态,才能写出“好”的代码。

这五个原则合起来,就是为了实现一个终极目标:高内聚、低耦合

2️⃣ 单一职责原则(SRP,Single Responsibility Principle)

定义:一个类应该只有一个引起它变化的原因。换句话说,一个类只负责一个功能领域的职责。

核心思想:一个类只做一件事。如果一件事做不好需要改,不会连累其他不相干的功能。

软考常考表述

  • “将不同的职责分离到不同的类中”
  • “一个类只承担一个职责”
  • “变化点分散到不同的类中”

反面示例(违反SRP):

class 员工 { 计算工资() // 财务部门关心 保存到数据库() // DBA关心 生成报表() // 管理层关心 }

三个不同的职责混在一起,改工资算法可能会影响数据库操作。

正确做法:拆成工资计算器员工仓库报表生成器三个独立的类。

一句话:一个类,一个职责,一个改动的理由。

3️⃣ 开闭原则(OCP,Open-Closed Principle)

定义:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭

核心思想:当需求变化时,你应该通过新增代码来实现新功能,而不是修改现有代码

软考常考表述

  • “不修改原有代码,通过扩展来实现新功能”
  • “抽象化是开闭原则的关键”
  • “面向接口编程”

反面示例(违反OCP):

class 绘图 { 画(形状) { if (形状是圆形) { 画圆() } else if (形状是方形) { 画方() } // 新增三角形时,要修改这个类 } }

正确做法(遵循OCP):

interface 形状 { 画() } class 圆形 implements 形状 { 画() { 画圆 } } class 方形 implements 形状 { 画() { 画方 } } // 新增三角形:只需新增类,不修改已有代码

一句话不改旧代码,只加新代码。抽象是钥匙,接口是锁芯。

本质:开闭原则是SOLID中最核心、最抽象的原则,其他几个原则都是在不同层面上支撑“对扩展开放、对修改封闭”的手段。单一职责保证修改影响范围小,里氏替换保证扩展不破坏继承体系,接口隔离和依赖倒置保证扩展不引入不必要的依赖。

4️⃣ 里氏替换原则(LSP,Liskov Substitution Principle)

定义子类型必须能够替换掉它们的父类型。凡是父类出现的地方,子类都应该能够代替父类出现,且程序行为不变。

核心思想:继承不能破坏父类的行为契约。子类可以扩展父类的功能,但不能改变父类原有的行为含义。

软考常考表述

  • “子类可以替换父类”
  • “子类不应重写父类中已定义的方法来改变其预期行为”
  • “继承关系应该符合 is-a 关系”

特别注意:里氏替换原则是五个原则中最容易混淆的一个。它的关键不在于“子类能不能替换父类”(语法上当然能),而在于“替换后程序行为是否正确”。

反面示例(违反LSP——经典长方形/正方形问题):

class 长方形 { 设置宽度(w) { 宽度 = w } 设置高度(h) { 高度 = h } } class 正方形 extends 长方形 { 设置宽度(w) { 宽度 = w; 高度 = w } // 破坏了父类的契约 设置高度(h) { 高度 = h; 宽度 = h } // 父类说宽高独立,子类说必须相等 }

使用场景中,如果某个函数要求宽高独立变化,传入正方形就会出错。

一句话子类可以替换父类,且替换后程序照样跑不改变父类行为的预期是底线。

5️⃣ 接口隔离原则(ISP,Interface Segregation Principle)

定义:客户端不应该被迫依赖它不使用的接口。应该将胖接口拆分成多个专一的小接口

核心思想:一个类实现了某个接口,就必须实现该接口的所有方法。如果接口里有这个类根本用不到的方法,就说明接口设计得太“胖”了。

软考常考表述

  • “将大接口拆分为多个小接口”
  • “不应该强迫用户依赖他们不需要的方法”
  • “接口的粒度要小”

反面示例(违反ISP):

interface 多功能设备 { 打印() 扫描() 传真() } class 普通打印机 implements 多功能设备 { 打印() { ... } 扫描() { 抛出异常 } // 普通打印机不支持扫描 传真() { 抛出异常 } // 普通打印机不支持传真 }

正确做法:拆分为可打印可扫描可传真三个独立接口。

一句话胖接口拆成小接口,别让实现类被迫实现它用不上的方法。

6️⃣ 依赖倒置原则(DIP,Dependency Inversion Principle)

定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。

核心思想:要面向接口编程,不要面向实现编程。依赖关系应该终止于抽象类或接口。

软考常考表述

  • “依赖抽象,不依赖具体”
  • “高层模块和低层模块都依赖接口”
  • “抽象不依赖具体实现”

反面示例(违反DIP):

class 支付服务 { 支付宝 支付工具; // 直接依赖具体类 支付() { 支付工具.扣款() } } // 如果要换成微信支付,必须修改支付服务代码

正确做法(遵循DIP):

interface 支付方式 { 扣款() } class 支付宝 implements 支付方式 { 扣款() { ... } } class 微信支付 implements 支付方式 { 扣款() { ... } } class 支付服务 { 支付方式 支付工具; // 依赖接口,不依赖具体实现 支付() { 支付工具.扣款() } }

一句话依赖接口,不依赖具体类。细节依赖抽象,抽象不依赖细节。

7️⃣ SOLID五原则速查表

原则缩写核心要点软考关键词
单一职责SRP一个类只有一个职责一个职责一个变化原因
开闭原则OCP对扩展开放,对修改关闭不修改原有代码扩展
里氏替换LSP子类可替换父类且行为不变子类替换父类不改变预期行为
接口隔离ISP胖接口拆成小接口不依赖不需要的方法拆分接口
依赖倒置DIP依赖抽象,不依赖具体面向接口编程依赖抽象

8️⃣ 经典例题

例题1:某系统在增加新功能时,只需要添加新的类而不需要修改现有类,这体现了( )原则。

A. 单一职责
B. 开闭原则
C. 里氏替换
D. 接口隔离

解析:“增加新功能而不修改现有类”是开闭原则的典型表述。选B


例题2:某系统定义了一个“员工”类,包含了计算工资、保存到数据库、生成报表三个方法。有开发人员建议将这个类拆分为三个独立的类。该建议主要依据( )原则。

A. 单一职责
B. 开闭原则
C. 接口隔离
D. 依赖倒置

解析:将不同职责拆分到不同类中,是单一职责原则的典型应用。选A

9️⃣ 记忆口诀

单开里接依——SOLID要牢记。
单一职责管好自己事,开闭扩展不修旧。
里氏子替父,行为不走样。
接口隔离拆胖接,依赖倒置面向接口留。

🔟 小测验(评论区对答案)

某系统中,类A直接依赖于类B的具体实现。系统引入了一个新功能,需要将类B替换为类C,结果导致类A的代码也必须修改。这最可能违反了( )原则。
A. 单一职责
B. 开闭原则
C. 里氏替换
D. 依赖倒置

🔔本专栏日更,点击头像 → 专栏《软考中级高频考点》订阅,第一时间接收新内容

#软考中级 #软件设计师 #SOLID #单一职责 #开闭原则 #里氏替换 #接口隔离 #依赖倒置 #软考备考