2017编程提高第2节课——面向对象设计(2)
LanyuanXiaoyao's Blog ヽ(✿゚▽゚)ノ

2017编程提高第2节课——面向对象设计(2)


面向对象设计的原则

  • 单一职责原则 (SRP)
  • 开闭原则 (OCP)
  • Liskov 替换原则 (LSP)
  • 接口隔离原则 (ISP)
  • 依赖倒置原则 (DIP)  
  • SOLID

开闭原则(OCP)

可复用的“基石”

  • 软件模块对扩展是开放的
    • 当需求发生改变时,可以对模块进行扩展
  • 软件模块对修改是封闭的
    • 对模块进行扩展时, 无须改动模块的源代码。
  • 似乎是矛盾的 ?

缺点

  • 对扩展开放: 可以添加新的水果类
  • 但是每次添加新的水果类,就需要修改ShopCart中的逻辑

修改后

  • 对扩展开放
    • 可以任意的添加新的水果类:香蕉,西瓜…
    • 对修改是封闭的
  • 对于ShopCart中的计算逻辑不用修改。
  • 关键是抽象 !

开闭原则的关键在于抽象出一个概念,然后针对接口编程,但是不要过度抽象,抽象到一定合适的层次即可
继承和多态都是OCP的一种具体的实现方式

三分钟练习

简单的答案

public class Employee{
	abstract void work();
}

Liskov替换原则(LSP)

子类型能够完全替换父类型,而不会让调用父类型的客户程序从行为上有任何改变
难道多态不就是为了达到这个目标吗?

正方形 is a 长方形?


违反了替换原则,如果在testArea方法中传入了Square对象,那么结果将会是16,即使两个set方法调换,结果也是25,不会等于20,这就有了矛盾,这是因为长方形默认隐含了隐藏的客观条件——长宽独立变化,不互相影响

鸟都会飞吗

某个程序员创建了一个鸵鸟类

processAll是运行良好的代码,由于鸵鸟类的加入, 变得岌岌可危了。
由于新加入鸵鸟(Ostrich)类,方法就会出错

非常恶心的方法: 明明是传进来的是父类Bird,  还得单独对鸵鸟这个子类做判断!
产生Bug 的一大源泉!

解决鸵鸟问题的简单方式就是建立“飞行”接口,让可以飞的鸟实现飞行接口即可

小结

  • 继承的目的
    • 重用父类的代码
    • 更重要的是, 复用那些使用父类的代码(例如processAll)!!
  • LSP实际上是确保我们做的抽象不会被子类破坏

如果我们发现了程序被某个不安分守己的子类破坏了,那么首先就考虑替换原则被违反了,就考虑使用组合来代替继承

LSP和契约式设计

Bertrand Meyer 在 1988 年阐述了 LSP 原则与契约式设计之间的关系。使用契约式设计,类中的方法需要声明前置条件和后置条件。前置条件为真,则方法才能被执行。而在方法调用完成之前,方法本身将确保后置条件也成立。

  • Rectangle.setWidth的后置条件
    • Assert (( width==w ) && (height == old.height)) w为传进的参数,old表示老对象/先前的对象
    • 很明显, Square 违反了这个后置条件

虽然契约式设计好像很厉害的样子,但是因为后置条件在复杂的情况下非常难设计,所以往往被放弃

  • 当通过基类(父类)的接口使用对象时, 用户只知道基类的前置条件和后置条件
  • 派生类(子类) 只能使用相等或者更弱的前置条件类替换父类的前置条件
  • 派生类(子类)只能使用相等或者更强的后置条件来替换父类的后置条件

接口隔离原则(ISP)

  • 客户端不应该依赖它不需要的接口
  • 类间的依赖关系应该建立在最小的接口上
    • 使用多个专门的接口比使用单一的总接口要好。
  • 防止接口污染

ATM例子

实线为继承,虚线为实现

三分钟练习: 咖啡机


有什么问题?

依赖倒置原则(DIP)

  • 高层模块不应该依赖于低层模块,二者都应该依赖于抽象
  • 结构化编程: 高层依赖于底层

框架对抽象概念进行操作

熔炉的例子

改进后的代码中的ht都是对温度调节器和控温器的抽象,实际上换成任意型号都可以,而不是局限于一个型号,这就是抽象,从代码中找出抽象的概念,然后让代码对抽象进行操作

三分钟练习

作业

重构该程序,使其符合OCP


评论