面向对象的原则、模式、语言及框架(五)Liskov替换原则我们前面说了开闭原则OCP,其背后的主要机制是抽象和多
面向对象的原则、模式、语言及框架(五)
Liskov替换原则
我们前面说了开闭原则OCP,其背后的主要机制是抽象和多态,但在静态语言中(如c++,java),支持抽象和多态的关键机制之一便是继承,正是有了继承,我们才能够
抽象出接口/基类,然后在子类中实现继承而来的抽象方法,或覆写基类已实现的方法
来定制子类。这样我们才能只通过扩展来实现新增的功能。
但是按照什么规则,我们才能设计出最佳的继承层次呢,以及什么样才是最佳的继承体
系呢?Liskov替换原则回答了这个问题。
下面我们就看看什么是Liskov替换原则(LSP):
LSP:子类型必须能够替换成它们的基类型。
LSP的重要性是不言而喻的,例如:
void f(BaseType bt){ //使用bt来做事情}
这时候如果我们传递一个子类型SubType的对象st:f(st);
如果这时候f出现一个错误的行为,那么SubType就违反了LSP,同时也导致了对
OCP的违反,因为我们要想这个方法对SubType也产生正确的行为,我们需要重新
修改f,对特定SupType进行定制操作,以便得到正确的行为,这样f对BaseType
的子类就不封闭了
我们看看Bob大叔举的一个违反LSP的例子:
public class Rectangle{ protected int width; protected int hight; public void setWidth(int width){ this.width = width; } public void setHight(int hight){ this.hight = hight; } public int getWidth(){ return width; } public int getHight(){ return hight; }}
我们经常说继承是"Is-a",而组合是"has-a".这样如果一个的对象对于另一个类的对象
满足"is-a"关系,那么就应该把这个新类从原来那个类继承而来。
正方形是个矩形,因此把Square类视为Rectangle类的子类应该是合理的。"Is"被认为是
面向对象设计(OOA)的基本技术之一。但这会产生微妙但极为应该重视的问题。
但我们首先看到width,height两个变量对于Square来说是一种浪费,一般情况下,这种浪费是无关紧要的。但是setHight和setWidth对于Square来说是不合适的,因为正方形的长和宽应该相同的,我们为了确保这点,我们可以复写这两个方法:
public class Square extends Rectangle{//others public void setWidth(int width){ super.setWidth(width); super.setHight(width); } public void setHight(int hight){ super.setHight(hight); super.setWidth(hight); }}
这样似乎就满足了数学意义上的正方形了吧,但是我们考虑下面的函数
void testArea(Rectangle r){ r.setWidth(2); r.setHeight(3); assert(r.area()==6);}
当我们向testArea传递Square对象时,断言就会失败,因为testArea的编写者
不会认为高度的改变,会影响宽度。方法testArea表明Rectangle和Square的结构
是脆弱的,Square不能够替换掉Rectangle,因此Square和Rectagle之间的关系是违反了
LSP。
LSP让我们得出了一个非常重要的结论:一个模型,如果孤立的看,并不能发现问题,模型
的有效性只能通过它的客户程序来表现。如果孤立的看,最后那个版本的模型时自相容的,
但是如果从基类做出一些合理假设的程序员的角度来看,这个模型是有问题的。
但是有谁能知道使用者会做出怎样的假设呢?大多数的假设是很难预测的。事实上如果我们
试图去预测所有这种假设,我们所得到的系统将充满不必要复杂性的Bad Smell.所以通常最好的方法是之预测那些最明显的对于LSP违反情况而推迟其他的预测。
真正原因:
IS-A是关于行为的Square和Rectangle这个显然合理的模型为什么会出现问题?毕竟Square就是Rectangle,
难道它们之间不存在is-a关系么?
对于不是testArea的编写者来说Square就是Rectangle是没有问题的,但对于testArea得编写者而言,Square对象绝对不是Square,应为Square的行为方式和testArea所期望的行为方式是不相容的。对象的行为方式才是软件真正关注的问题,LSP清楚地告诉我们,OOD中
is-a是对于行为方式而言的,行为方式的合理假设是客户程序所依赖的。
另外一个问题:
我们都知道Java的异常,子类不能比超类抛出更多的异常,这其实就是LSP原则。
结论:
OCP是OOD的核心原则,如果这个原则应用的有效,应用程序就会有更好的维护性、可重用性和健壮性,而LSP是OCP成为可能的主要原则之一,正是子类型的可替换性,才使得使用基类型的模块无需修改的情况下就能进行扩展。这种可替换性是开发人员可以隐式依赖的东西。