里氏替换原则(Liskov Substitutiion Principle,LSP)被称作继承复用的基石,它的提出甚至要早于OCP。不过遗憾的是,由于对这一原则的理解各不相同,经过多次的翻译、转述,LSP成了OOD设计原则中争议最多的话题之一。
其实早在1987年的OOPSLA大会上,麻省理工学院(MIT)计算机科学实验室的Liskov女士就发表了经典文章Data Abstraction and Hierarchy,其中提出了以她名字命名的Liskov替换原则(The Liskov Substitution Principle),简称LSP。该原则说明了什么时候该使用继承,什么时候不该使用以及为什么。一年后此文发表在ACM的SIGPLAN Notices杂志上,更是影响深远。Liskov在这篇文章中写到:
A type hierarchy is composed of subtypes and supertypes. The intuitive idea of a subtype is one whose objects provide all the behavior of objects of another type (the supertype) plus something extra.What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.
意思是:“类型层次由子类型和超类型(也就是父类)组成,直觉告诉我们,子类型的含义就是该类型的对象提供了另外一个类型(超类型)的对象的所有行为功能,并有所扩充。这里需要如下的替换性质:若对于每一个类型S的对象o1,都存在一个类型T的对象o2,使得在所有针对T编写的程序P中,用o1替换o2后,程序P的行为功能不变,则S是T的子类型。”这就是LSP的最初含义。
而著名技术作家Robert Martin在1996年为《C++ Reporter》写了一篇题为《The The Liskov Substitution Principle》的文章,专门介绍LSP。在Martin的文章中,他给了LSP一个解释:
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
意思是:“使用指向基类的指针或引用的函数,必须能够在不知道具体派生类对象类型的情况下使用它们。”在2002年,Martin在他出版的《Agile Software Development Principles Patterns and Practices》一书中,又进一步简化为:
Subtypes must be substitutable for their base types。子类必须能替换成它们的父类。
也是正是由于这许多版本的存在在加上翻译,转述等等问题,最终导致了LSP的多种理解(不深究了)。按照我的理解,上面Liskov的表述虽然拗口,但是也不难明白。再加上Martin的解释,LSP原则就很清楚了。
LSP说的其实是在构建类的继承结构的过程中需要遵循的基本原则,什么时候改用,什么时候不能用,避免继承的滥用。LSP和OCP是相关联的,也可以说是OCP的基本保证。试想,如果某个函数使用了指向基类的指针或引用,但是类的设计却违背了LSP原则,那么这个函数就必须了解该基类的所有派生类。这个函数显然违背开放-封闭原则OCP,因为一旦新构建该基类的子类,此函数就需要修改。
我也来说那个经典的”正方形不是矩形“的问题。在数学的世界里,正方形当然是矩形。用OO的数据,正方形和矩形之间是IS-A的关系——这个关系正好是OO初级教程里说的确定继承关系的依据。因此,理所当然的,正方形类然Square应该继承长方形类Rectangle:
... {
private long width;
private long height;
public void setWidth(long width)...{
this.width=width;
}
public void setHeight(long height)...{
this.height=height;
}
public long getWidth()...{
return width;
}
public long getHeight()...{
return height;
}
} ;
public class Square : Rectangle
... {
public void setWidth(long width)...{
this.width=width;
this.height=width;
}
public void setHeight(long height)...{
this.width=width;
this.height=width;
}
} ;
假设有这么一个函数:
... {
while(r.getHeight()<r.getWidth()))
...{
r.setHeight(r.getHeight()++)
}
return r;
}
如果传递给IncreaseHeight的是一个Rectangle(长宽不同)的对象的话,没问题;如果传递一个Square对象。。。谁都知道会是个什么结果!出现这个问题的原因就是这个继承结构的设计违反了LSP原则:Square类对Height和Weight的处理和Rectangle逻辑不同不同,Rectangle单独改变Widtht和Height,而Square必须同时改变Width和Height。所以,Square和Rectangle之间的继承关系是不能成立的。可以增加一个抽象类Quadrangle,定义四边形的公共方法,Square和Rectangle都从Quadrangle继承这些方法,同时可以添加自己特有的方法:
其实,不只是这个“经典”的问题反映了现实世界中概念和OO概念的区别,很多情况都需要在做OOD的时候仔细考虑。只有符合了LSP的规定,才可能实现OCP。以下几条可作为实践经验:
1.从抽象类继承,不要从实体类继承。因为实体类会具有和特定实体相关的方法,这些方法在子类中可能不会有用。
2.使用契约式编程方法。DBC(Design by Contract)把类和其客户之间的关系看作是一个正式的协议,明确各方的权利和义务。在父类里定义好子类需要实现的功能,而子类只要实现这些功能即可。
3.所有派生类的行为功能必须和客户程序对其基类所期望的保持一致。上面的例子就是违背了这一条。其实,这是OO在继承和重写时的基本要求。