在决定是否需要用继承的方法来解决一个问题的时候,里氏替换原则可能会给出一些指导。
三条描述:
关于里氏替换原则,有一个很有意思的表述:
描述一:你不能通过给一条狗增加4条假腿来创造一只章鱼(Mario Fusco).
但是单独看这条表述,还是很难理解里氏替换原则到底在讲什么,里面说的狗指什么,假腿指代什么,章鱼又是指代什么。
另一些大牛给出过一本正经的阐述:
描述二:如果S类型是T类型的一个子类型,并假设q(x)是T类型对象相当一个可证的属性,那么同样的,q(y)应该是S类型对象y的一个可证的属性(Barbara Liskov, Jeanette Wing).
似乎仍然不太容易理解。
还有更通俗的说法:
描述三:使用基类指针或基类引用的函数,必须在不知道派生类的情况下使用它(Robert C. Martin).
这就清楚多了,在使用多态属性的时候,应该有所体会。我们提倡在定义时,或者作为函数的前置条件(也就是方法的形参)时使用基类,而在应用时再决定调用哪种派生类。这也是开闭原则和依赖倒置原则的基础。也就是说,派生类必须完全可以替代其基类。如果不能,就违反了里氏替换原则。因此里氏替换原则是符合开闭原则和依赖倒置原则的基础。
回过头再看开头那句关于章鱼和狗的描述,就不难理解:你不能通过继承一个描述狗的类,通过增加腿的数量得到一个描述章鱼的类。因为章鱼和狗完全不是同一种事物,即使他们在某些方面似乎有些共性(比如他们都包含了大于或等于四条腿)。
在通过继承创建一个新类的时候,我们可以扩展属性,但是不能增加属性的约束。
另一个很好的描述里氏替换原则的例子是《C++代码整洁之道》中列举的关于矩形和正方形的例子。
在数学上,我们说正方形是矩形的一个特例,但是在用C++类对其进行抽象的时候,我们能不能通过继承描述矩形的类来得到一个描述正方形的类呢?
当然,在实现上可能很简单,但这样就违反了里氏替换原则。
换句话说,由矩形到正方形,我们并不是扩展了属性,而是对现有属性进行了约束,那就是必须要求长和宽必须相等。
如果非要通过继承,并且不能违反里氏替换原则的基础上实现这一场景,就应该将矩形和正方形放在并列的位置上,共同继承更高一层的抽象。就像章鱼和狗可以共同继承自更高的抽象,比如一个描述动物的类。
于是我们就可以说,矩形和正方形都是几何图形,狗和章鱼都是动物。