面向对象设计原则
- 单一职责原则 (SRP)
- 开闭原则 (OCP)
- Liskov 替换原则 (LSP)
- 接口隔离原则 (ISP)
依赖倒置原则 (DIP)
SOLID
OCP: 开闭原则
- 软件模块对扩展是开放的
- 当需求发生改变时,可以对模块进行扩展
- 软件模块对修改是封闭的
- 对模块进行扩展时, 无须改动模块的源代码。
- 似乎是矛盾的 ?
面向对象设计的原则 (2) : 开闭原则
缺点:
- 对扩展开放: 可以添加新的水果类
- 但是每次添加新的水果类,就需要修改ShopCart中的逻辑 !
重构后
- 对扩展开放
- 可以任意的添加新的水果类:香蕉,西瓜…
- 对修改是封闭的
- 对于ShopCart中的计算逻辑不用修改。
- 模块依赖于一个固定抽象体,所以对于更改是关闭的。同时通过这个抽象体派生,也可以扩展此模块的行为。所以关键是抽象 !
例子
重构后
- 在许多方面,OCP都是面向对象设计的核心所在。遵循这个原则可以带来面向对象技术所声称的巨大好处(也就是 灵活性,可重用性,以及可维护性)。然而,并不是说只要使用一种面向对象语言遵循了这个原则。对于应用程序中的每个部分都肆意的进行抽象并不是一个好的主意。正确的做法是,开发人员应该仅仅对程序中频繁出现的变化的那部分做出抽象。拒接不成熟的抽象和抽象本身一样重要。
Liskov 替换原则 (LSP)
- 子类型能够完全替换父类型,而不会让调用父类型的客户程序从行为上有任何改变
- 难道多态不就是为了达到这个目标吗?
LSP: 正方形 is a 长方形?
LSP: 鸟都会飞吗
某个程序员创建了一个鸵鸟类
继承的目的
- 重用父类的代码
- 更重要的是, 复用那些使用父类的代码(例如processAll)!!
LSP实际上是确保我们做的抽象不会被子类破坏
LSP和契约式设计
Bertrand Meyer 在 1988 年阐述了 LSP 原则与契约式设计之间的关系。使用契约式设计,类中的方法需要声明前置条件和后置条件。前置条件为真,则方法才能被执行。而在方法调用完成之前,方法本身将确保后置条件也成立。
Rectangle.setWidth的后置条件
- Assert (( width==w ) && (height == old.height))
- 很明显, Square 违反了这个后置条件
当通过基类(父类)的接口使用对象时, 用户只知道基类的前置条件和后置条件
派生类(子类) 只能使用相等或者更弱的前置条件类替换父类的前置条件
接口隔离原则(ISP)
客户端不应该依赖它不需要的接口
类间的依赖关系应该建立在最小的接口上
- 使用多个专门的接口比使用单一的总接口要好。
防止接口污染
ISP: ATM例子
ISP: 咖啡机例子
重构后
依赖倒置原则(DIP)
- 高层模块不应该依赖于低层模块,二者都应该依赖于抽象
- 抽象不应该依赖于细节。细节应该依赖于抽象。
DIP: 熔炉的例子
DIP: 打印机的例子
重构后
使用传统的过程化程序设计所创建出来的依赖关系结构,策略是依赖于细节的。这样会导致策略受到细节改变的影响。面向对象的程序设计倒置了依赖关系结构,使得细节和策略都依赖于抽象,并且常常是客户拥有服务接口。
依赖关系的倒置正好是面向对象设计的标志所在。如果程序的的依赖关系是倒置的,他就是面向对象的设计。如果程序的依赖关系不是倒置的它就是过程化的设计。
依赖倒置原则对于创建可重用框架来说是必须的。同时对于构建在变化面前富有弹性的代码也是非常重要的。由于抽象和字节被彼此隔离,所以代码也非常容易维护。