10.3 面向对象设计
面向对象设计 OOD 把分析模型进一步转化为可实现、可维护的软件设计。软考中这一部分主要考设计原则,尤其是七大原则的名称与描述匹配。
从结构化原则到面向对象原则
结构化设计强调“高内聚、低耦合”。面向对象设计也继承这个方向,只是对象世界中关注点变成类、接口、继承、组合和包。
| 结构化表达 | 面向对象中的对应思路 |
|---|---|
| 高内聚 | 单一职责,类或接口目的单一 |
| 低耦合 | 接口隔离、组合复用、迪米特法则 |
| 减少修改影响 | 开闭原则、依赖倒置、里氏替换 |
OOD 的原则不是互相完全孤立的,它们都在服务同一目标:降低修改成本,提高复用性、扩展性和可维护性。
七大设计原则
| 原则 | 一句话 | 题干关键词 |
|---|---|---|
| 单一职责原则 SRP | 一个类最好只有一个引起它变化的原因 | 目的单一、职责单一 |
| 开闭原则 OCP | 对扩展开放,对修改封闭 | 新增功能靠扩展,少改旧代码 |
| 里氏替换原则 LSP | 子类可以替换父类出现的位置 | 子类替换父类、特殊替换一般 |
| 依赖倒置原则 DIP | 依赖抽象,不依赖具体实现 | 针对接口编程、高层抽象 |
| 接口隔离原则 ISP | 使用多个专门接口,避免大而全接口 | 接口目的单一、不要总接口 |
| 组合复用原则 CRP | 优先用组合实现复用,而不是继承 | 组合优于继承、降低耦合 |
| 迪米特法则 LoD | 一个对象应尽量少了解其他对象 | 最少知识法则、只和必要对象交互 |
这些原则中,题目常给一句定义,让你选原则名称。
开闭原则:为什么“扩展开放、修改封闭”
修改已有代码可能引入新缺陷,也会触发回归测试。开闭原则希望面对变化时,尽量通过新增类、新增实现、新增配置来扩展,而不是直接改动稳定代码。
| 做法 | 后果 |
|---|---|
| 每次新功能都修改旧类 | 影响范围大,回归成本高 |
| 把变化点抽象出来,通过新增子类或策略扩展 | 旧代码更稳定,扩展更安全 |
开闭原则不是“不准修改”,而是设计时尽量让常见变化通过扩展完成。
里氏替换:子类要守父类契约
里氏替换原则强调:凡是能使用父类对象的地方,都应能使用子类对象而不破坏程序正确性。
如果子类继承父类后大幅改变父类行为,使调用方原有假设失效,就违反了里氏替换。它隐含的建议是:子类扩展父类能力,但不要破坏父类已有语义。
依赖倒置:针对抽象编程
传统调用常表现为高层模块依赖低层具体实现。依赖倒置要求高层模块不要直接依赖低层具体类,而要依赖抽象接口;低层实现也围绕这个抽象来实现。
flowchart LR
A["高层业务"] --> I["抽象接口"]
B["具体实现A"] --> I
C["具体实现B"] --> I这样新增具体实现时,高层业务不需要知道每个子类细节,多态和扩展都会更自然。
接口隔离与组合复用
接口隔离原则反对把所有功能塞进一个大接口。大接口会让使用者被迫依赖自己不需要的能力,一处变化影响很多调用方。更好的方式是按职责拆成多个小接口。
组合复用原则强调优先通过对象组合来获得能力。继承关系耦合紧、层次一旦设计错就很难改;组合关系更灵活,可以在运行时替换组件,修改影响也更小。
| 复用方式 | 优点 | 局限 |
|---|---|---|
| 继承复用 | 简单直接,可复用父类实现 | 耦合紧,父类变化影响子类 |
| 组合复用 | 灵活、低耦合、可替换 | 需要设计清晰接口 |
这也是很多设计模式更偏向“组合优于继承”的原因。
迪米特法则
迪米特法则又叫最少知识法则。对象只应了解自己必须了解的对象,不要到处访问别人的内部细节。
如果一个类知道太多其他类的结构,任何被它了解的类发生变化,都可能牵连它修改。迪米特法则通过减少知识面来降低耦合。
包相关原则
字幕还补充了几条包层面的原则,考试频率较低,但要能对上名称。
| 原则 | 含义 |
|---|---|
| 重用发布等价原则 | 重用的粒度应与发布的粒度一致 |
| 共同封闭原则 | 一个包中的类应对同一类变化共同封闭 |
| 共同重用原则 | 一个包中的类应共同被重用 |
| 无环依赖原则 | 包之间依赖关系不要形成环 |
| 稳定依赖原则 | 依赖应指向更稳定的包 |
| 稳定抽象原则 | 包的抽象程度应与稳定程度匹配 |
这些原则主要服务包结构设计,不是本节最高频,但遇到名称配对题时要有印象。
例题
本节小结
面向对象设计的考试重点是原则匹配。单一职责看“一个变化原因”,开闭原则看“扩展开放、修改封闭”,里氏替换看“子类替换父类”,依赖倒置看“依赖抽象”,接口隔离看“小而专门的接口”,组合复用看“组合优于继承”,迪米特看“最少知识”。