当前课程知识点:软件工程与软件自动化 > 第三章 OO与UML > 3.2 面向对象设计基本原则 > 面向对象设计基本原则
嗨,大家好
下面我们一起讨论面向对象设计的基本原则
这些原则对我们进行系统开具有十分重要的意思
遵守这些原则可以让我们的代码更加灵活
更易于复用,易于拓展,灵活而优雅
就如Arthur在那本《OOD启示录》中所说的
“你并不必严格遵守这些原则
违背它们也不会被处以宗教刑罚
但你应当把这些原则看做警铃
若违背了其中的一条
那么警铃就会响起”
下面我们简单的讨论一下这七个基本原则
首先我们来看第一个原则SRP,单一职责原则
这个原则被称为最基本的一个原则
它的本意是说,对一个类而言
应该仅有一个引起它变化的原因
更好理解可以这么说,每个类应该专注于
做一件事情,承担一个职责
如果一个类承担的职责过多
那么这些职责就会相互依赖
一个职责的变化可能会影响另一个职责的履行
其实OOD的本质,就是要合理地进行类的职责划分
通过聚焦单一职责,提高了类的内聚性
有利于对象的复用和扩展
第二个原则是开闭原则
它要求我们在设计系统的时候
对扩展开放,对修改关闭
对于开放封闭原则,它是面向对象所有原则的核心
软件设计说到底追求的目标
就是封装变化、降低耦合
而开放封闭原则就是这一目标的最直接的体现
开放封闭原则的核心思想是
软件实体应该是可扩展的,而不可修改的
因此,开放封闭原则首先体现在两个方面
一. 对扩展开放,意味着有新的需求或变化时
可以对现有代码进行扩展以适应新的情况
第二. 对修改封闭,意味着类一旦设计完成
就可以独立完成它的工作
而不需要对它进行任何修改
实现开闭原则的核心思想就是对抽象编程
而不对具体编程
因为抽象相对稳定
让类依赖于固定的抽象,所以修改就是封闭的
而通过面向对象的继承和多态机制
又可以实现对抽象类的继承
通过覆盖父类的方法来改变其固有的行为
实现新的拓展方法,所以它是开放的
第三个原则是依赖倒置
这里有两个含义
一是高层模块不应该依赖于低层模块
也就是说高层和低层都应该依赖于抽象
二是抽象不应该依赖于具体,具体应该依赖于抽象
从编程角度来说,就是针对接口编程
不要对实现编程
我们知道,依赖一定会存在于类与类
模块与模块之间
当两个模块之间存在紧密的耦合关系的时候
最好的方法就是分离接口和实现
在依赖之间定义一个抽象的接口
使得高层模块调用接口
而低层模块来实现接口
以此来有效的控制耦合关系
达到依赖于抽象的设计目标
抽象的稳定性决定了系统的稳定性
因为抽象是不变的
依赖于抽象是面向对象设计的精髓
也是依赖倒置原则的核心
依赖倒置看起来很美,用起来威力也很强大
但是实现起来成本并不低
因为依赖的这种倒置
对象的创建就不能直接用new了
需要用对象工厂这些比较复杂的设计模式
以避免对具体类的直接引用
这样的结果就会产生大量的辅助类
这就增加了代码的复杂度
给维护带来了不必要的麻烦
所以,正确的做法是只对程序中
那些频繁变化的部分进行依赖倒置
第四个原则是里氏替换原则
这个原则是以美国科学家芭芭拉•利斯科夫命名的
里斯科夫是2008年图灵奖得主
对于Liskov替换原则,它的核心思想是
子类必须能够替换其基类
这一思想体现为对继承机制的约束规范
只有子类能够替换父类
这种情况下才能保证系统在运行期内识别子类
这是保证继承复用的基础
在父类和子类的具体行为当中
必须严格把握继承层次中的关系和特征
将基类替换为子类
程序的行为不会发生任何变化
同时,这个约束反过来是不成立的
子类可以代替父类,但是父类就不能替换子类
Liskov替换原则主要着眼于在继承的基础上
建立抽象和多态
因此只有遵循了这个原则
才能保证继承复用是可靠的
实现里氏替换原则的方法是面向接口编程
将公共部分抽象成基类或抽象类
通过抽取抽象类的方法,在子类中覆写父类的方法
实现新的方式来达到同样的职责
Liskov替换原则是关于继承机制的设计原则
违反了Liskov替换原则就必然导致违反开闭原则
Liskov替换原则能够保证系统具有良好的拓展性
同时实现基于多态的抽象机制
能够减少代码冗余,避免运行期的类型判别
提高系统性能
第五个原则是CARP
也就是说要优先使用对象的组合和聚合
而不是使用继承
从本质上来说,对象的合成
这里说的合成包括聚合和组合
对象的合成和继承都是用于复用的
那两者之间有什么区别呢?
在继承方式中,父类的内部细节对子类是可见的
所以可以理解为这种复用是白盒复用
对象合成时对象的内部细节是不可见的
所以可以理解为是黑盒复用
父类的内部细节对子类可见
我们认为破坏了封装性
子类中实现了与它父类中的方法,产生了依赖
以至于父类实现中的任何变化必然会导致子类发生变
化
当需要复用子类的时候
这种实现上的依赖就会产生一些问题
如果继承下来之后发现不适合解决新的问题
这时候怎么办呢?
父类就必须重写或被其他适合的类所取代
这种依赖关系限制了灵活性并最终限制了复用性
一个可用的解决方法就是只继承抽象类
因为抽象类通常提供较少的实现
反过来我们看对象组合和聚合
对象间通过接口关联
对象组合是通过获得对其他对象的引用
得到了一个运行时的对象
组合要求对象遵守彼此的接口约定
进而要求仔细地定义接口
而这些接口并不妨碍你将一个对象
和其他对象一起使用
这会产生良好的结果
也就是说对象只能通过接口访问
所以我们并没有破坏封装性
只要类型一致
运行时刻还可以用另外一个对象来替代
更进一步,因为对象的实现是基于接口写的
所以在实现上存在着很少的依赖关系
第六个原则是接口隔离
它的核心思想是
使用多个小的专门的接口
而不要使用一个大的总接口
具体来说,接口隔离原则体现在
接口应该是内聚的,应该避免“胖”接口
一个类对另外一个类的依赖应该建立在最小的接口上
不要强迫它依赖那些不用的方法
我们把这一点叫接口污染
接口有效地将细节和抽象隔离出来
体现了抽象编程的优点
接口隔离强调接口的单一性
而胖接口有明显的弊端
会导致实现的类型必须完全实现接口的所有方法
而某些时候,实现这种类型并非需要所有的接口定义
这在设计上是一种浪费
而且在实施上会带来一些潜在的问题
对胖接口的修改会导致一连串的
客户端程序都需要修改
有时候这是一种很可怕的灾难
在这种情况下,将胖接口分解为
多个特定的定制化接口
使得客户端仅仅依赖于它们实际调用的方法
客户端就不会依赖于它们不需要的那些方法
怎么分离呢?怎么把胖客户端
把这种胖接口变成瘦接口呢?
有两种方法
第一是委托分离
增加一个新的类型来委托客户的请求
隔离客户和接口的直接依赖关系
但这样会增加系统的开销
第二种就是多重继承这种分离方法
通过接口多继承来实现客户的需求
这种方式是比较常用的
第七个原则是LoD,迪米特法则
这个法则又叫最少知识法则,或者叫原则
还有一个称呼叫不和陌生人说话原则
据说这个原则是第一次在Demeter系统
中得到正式运用,所以被称为迪米特原则
我们知道,Demeter是希腊神话中掌管农业的女神
这个原则的核心思想就是建议
一个类尽可能少的和其他实体发生关系
从编程角度来看
在一个对象的方法中
只能给熟人对象发消息
常见的熟人对象包括以下五种
这里展示的是一段严重违反Demeter原则的代码
大家看,在Client类的func方法中
有一长串不认识的对象
最后调用了一个GetTimer()方法
如果B、C或D这三个类中的任何一个类发生变动
都可能会影响到Client类的功能
造成严重的依赖
怎么解决这个问题呢?
我们可以为ClassA添加一个GetTimer()职责
就是一个方法
然后对于Client类来说
ClassA是它的一个私有属性,是一个熟人
Client类只需要知道ClassA就行了
ClassA里面有一个GetTimer
至于这个GetTimer是如何实现的
并不会影响到Client类
这样就减少了不必要的耦合
本节的最后,我们一起来总结一下
如果我们遵守这些设计原则
可以让软件更灵活更强壮
但是,灵活的代价可能就是系统代码
更加复杂,性能降低
因此,我们在设计系统的时候
要在灵活性和性能之间做出权衡
同时,我们再次强调
无论设计原则还是设计模式
都是为了应对需求变更而生的
所以要根据需求的变化可能性
变化的频率,变化的程度等等这些因素
来决定在多大程度上
在多大范围内应用哪些基本原则
本节就 讨论到这里,谢谢大家
-1.1 软件工程的前生今世
--开篇阅读
--授课视频
-第一章 软件工程基础--1.1 软件工程的前生今世
-1.2 万变不离其宗
--授课视频1/3
--授课视频2/3
--授课视频3/3
-第一章 软件工程基础--1.2 万变不离其宗
-1.3 唯一不变的是变化
--授课视频1/3
--授课视频2/3
--授课视频3/3
--外部链接
-第一章 软件工程基础--1.3 唯一不变的是变化
-1.4 亡羊补牢为时不晚
--授课视频1/2
--授课视频2/2
-第一章 软件工程基础--1.4 亡羊补牢为时不晚
-扩展阅读与话题讨论
--扩展阅读
--话题讨论
-2.1 方法论来源于恐惧
--授课视频
-第二章 敏捷开发--2.1 方法论来源于恐惧
-2.2 敏捷是什么
--授课视频
-第二章 敏捷开发--2.2 敏捷是什么
-2.3 典型敏捷开发方法
--XP敏捷开发方法
-第二章 敏捷开发--2.3 典型敏捷开发方法
-2.4 敏捷不是万能药
--授课视频
-第二章 敏捷开发--2.4 敏捷不是万能药
-专家谈敏捷
-扩展阅读与话题讨论
--外部链接
--话题讨论
-3.1 面向对象核心概念和基本特性
-第三章 OO与UML--3.1 面向对象核心概念和基本特性
-3.2 面向对象设计基本原则
-第三章 OO与UML--3.2 面向对象设计基本原则
-3.3 通用职责分配模式(GRASP)
--通用职责分配模式
-3.3 通用职责分配模式(GRASP)--作业
-3.4 从重构到模式
--模式和设计模式
-第三章 OO与UML--3.4 从重构到模式
-3.5 使用UML设计面向对象系统
--UML综述
-第三章 OO与UML--3.5 使用UML设计面向对象系统
-3.6 主要UML模型图绘制技巧
--UML用例图
--UML类图
-第三章 OO与UML--3.6 主要UML模型图绘制技巧
-扩展阅读与话题讨论
--设计模式有毒么?
--话题讨论
-4.1 案例简介
--书籍参考
--案例说明
-4.2 对象模型之一
--授课视频1/2
--授课视频2/2
-第四章 对象模型分析--4.2 对象模型之一
-4.3 对象模型之二
--授课视频1/2
--授课视频2/2
-第四章 对象模型分析--4.3 对象模型之二
-4.4 对象模型之交互
--授课视频
-第四章 对象模型分析--4.4 对象模型之交互
-扩展阅读与话题讨论
--图书推荐
--话题讨论
-5.1 软件自动化概述
--软件自动化概述
-第五章 软件自动化技术--5.1 软件自动化概述
-5.2 典型自动化方法和工具
-第五章 软件自动化技术--5.2 典型自动化方法和工具
-5.3 文档自动化
--文档自动化视频
-第五章 软件自动化技术--5.3 文档自动化
-5.4 测试自动化
--测试自动化视频
-第五章 软件自动化技术--5.4 测试自动化
-专家访谈
-扩展阅读与话题讨论
--话题讨论
-6.1 持续集成
-第六章 CI/CD与DevOps--6.1 持续集成
-6.2 持续交付和部署
-第六章 CI/CD与DevOps--6.2 持续交付和部署
-6.3 DevOps
-第六章 CI/CD与DevOps--6.3 DevOps
-专家访谈
-扩展阅读与话题讨论
--DevOps专题
--话题讨论
-7.1 质量和质量保证
--授课视频
-第七章 软件质量保证--7.1 质量和质量保证
-7.2 软件质量模型
--授课视频
-第七章 软件质量保证--7.2 软件质量模型
-7.3 SQA组织与职责
--授课视频
-第七章 软件质量保证--7.3 SQA组织与职责
-7.4 全面软件质量管理
--授课视频
-第七章 软件质量保证--7.4 全面软件质量管理
-专家访谈
--专家访谈
-扩展阅读与话题讨论
--外部链接
--话题讨论
-8.1 软件过程综述
--授课视频
-第八章 软件过程改进--8.1 软件过程综述
-8.2 软件过程改进
--授课视频
-第八章 软件过程改进--8.2 软件过程改进
-8.3 能力成熟度模型
--授课视频
-第八章 软件过程改进--8.3 能力成熟度模型
-8.4 过程改进标准框架
--授课视频
-第八章 软件过程改进--8.4 过程改进标准框架
-扩展阅读与话题讨论
--话题讨论
-9.1软件复用综述
--授课视频
-第九章 软件复用--9.1软件复用综述
-9.2 软件构件技术
--授课视频
-第九章 软件复用--9.2 软件构件技术
-9.3 软件复用实施
--授课视频
-第九章 软件复用--9.3 软件复用实施
-9.4 微服务架构
--授课视频
-第九章 软件复用--9.4 微服务架构
-扩展阅读与话题讨论
--微服务扩展
--话题讨论
-文档提交处--文档提交