里氏替换原则
我们在学习面向对象语言时,都会学到三大特征:封装、多态、继承。继承就是告诉你拥有父类的方法和属性,然后你也可以重写父类的方法。如此,问题产生了:“我们如何去度量继承关系的质量?”Liskov于1987年提出了一个关于继承的原则“Inheritance should ensure that any property proved about supertype objects also holds for ubtype objects.”——“继承必须确保超类所拥有的性质在子类中仍然成立。”也就是说,当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有is-A关系。通俗一点讲,就是父类出现的地方,子类一定可以出现,将父类替换为子类不会产生任何异常错误,也不会引发逻辑上的问题。
里氏替换原则为继承定义了一个规范,简单地概括为4层含义:
1.子类必须完全实现父类的方法,且方法对子类是有意义的
2.子类可以有自己的个性
3.覆盖或者实现父类方法时输入参数可以被放大
4.覆盖或者实现父类方法时输出参数可以被缩小
子类必须完全实现父类的方法,且方法对子类是有意义
2014年的选秀快到了,又是NBA的一届选秀大年,本文就以NBA球员为素材来举例,顺便回顾一下另一届选秀大年—96黄金一代。回顾96黄金一代时,要哪些签是划算的,哪些签又是亏大了的呢?我们根据能力对那一届进行排名,当选秀顺位大于能力排名,那说明就是划算的,例如科比。反之则是亏大了。
新建一个球员抽象类AbstractPlayer,包含两个方法GetDraftRanking和GetAbilityRanking,分别用来获取选秀顺位和能力排名,代码如下
/// <summary> /// 球员抽象类 /// </summary> public abstract class AbstractPlayer { /// <summary> /// 获取球员选秀时的顺位 /// </summary> /// <returns></returns> public abstract int GetDraftRanking(); /// <summary> /// 根据能力进行排名后的顺位 /// </summary> /// <returns></returns> public abstract int GetAbilityRanking(); }
新建一个具体的球员类KeBo,继承自AbstractPlayer,并实现GetDraftRanking和GetAbilityRanking两个方法,代码如下
/// <summary> /// 球员科比 /// </summary> public class Kebo : AbstractPlayer { /// <summary> /// 选秀顺位为13 /// </summary> /// <returns></returns> public override int GetDraftRanking() { return 13; } /// <summary> /// 按照能力排名,个人把科比排在第一位 /// </summary> /// <returns></returns> public override int GetAbilityRanking() { return 1; } }
应用场景,计算当时选择该球员是否划算
static void Main(string[] args) { AbstractPlayer kebo = new Kebo(); PlayerCostEffective(kebo); } /// <summary> /// 回顾当时选秀时选择该球员是否划算 /// </summary> private static void PlayerCostEffective(AbstractPlayer player) { if (player.GetAbilityRanking() <= player.GetDraftRanking()) { Console.WriteLine("划算"); } else { Console.WriteLine("不划算,亏大了!"); } }
运行结果:划算...(科比当然划算啦!)
然而,当我们统计另外一位球员本华莱士时,发现本华莱士没有参加选秀,代码如下
/// <summary> /// 获取选秀顺位 /// </summary> /// <returns></returns> public override int GetDraftRanking() { throw new Exception("我没有参加选秀,没有选秀顺位!"); } /// <summary> /// 根据能力排名,暂且排第8(排名不必认真) /// </summary> /// <returns></returns> public override int GetAbilityRanking() { return 8; }
修改应用场景代码,如下
static void Main(string[] args) { AbstractPlayer benWallace = new BenWallace(); PlayerCostEffective(benWallace); } /// <summary> /// 回顾当时选秀时选择该球员是否划算 /// </summary> private static void PlayerCostEffective(AbstractPlayer player) { if (player.GetAbilityRanking() <= player.GetDraftRanking()) { Console.WriteLine("划算"); } else { Console.WriteLine("不划算,亏大了!"); } }
运行结果:抛出异常;
在PlayerCostEffective中,子类BenWallance替换父类AbstractPlayer了,违背了里氏替换原则。有人会说,有些球员没有参加选秀正常啊,而且这样编写代码也能正常编译,只要在使用这个类的场景代码中里捕获异常或者BenWallance类中不抛出异常。但是,这就是问题所在!因为本华莱士没有参加选秀,获取选秀的父类方法对于他是没有意义的。
注:如果子类不能完整地实现父类的方法,或者父类中的方法对于子类没有意义或发生“畸变”,建议断开父子关系,采用依赖、聚集、组合等关系替代继承。
子类可以有自己的个性 |
子类可以有自己的个性,也就是方法和属性。在这里强调的原因是里氏替换原则可以正着用,但是反过来就不能用。也就是父类出现的地方,子类一定可以替换父类,而不引起错误或异常,但子类出现的地方,父类不一定能替换子类而保证其不出现错误或异常。
覆盖或者实现父类方法时输入参数可以被放大 |
在C#中,重载方法都是通过override关键字来声明,方法只有被声明为vritual时,才能够被重写,重写时方法的输入参数必须严格与被重写方法的参数相同,因此在C#中这条不适用。但是在Java等其他语言中,方法默认是可以被重写以及重写父类的方法,是遵循覆盖或者实现父类方法时输入参数可以被放大这一原则的,在这里就不多做说明,有兴趣的可以自己研究研究....
覆盖或者实现父类方法时输出参数可以被缩小 |
父类的一个方法的返回值是一个类型T,子类的相同方法的返回值为S,那么里氏替换原则就要求S必须小于等于T,也就是说要么S和T是用一个类型,要么S是T的子类。为什么呢?分两种情况,如果是覆写,父类和子类的同名方法的输入参数是相同的,两个方法的范围S小于等T,这是覆写的要企业,这才是重中之重,子类覆写父类的方法,天经地义。如果是重载,则要求方法的输入参数类型或数量不相同,在里氏替换原则要求下,就是子类的输入参数宽于或等于父类的输入参数,也就是说你写的这个方法是不会被调用的。
Summary |
再来回顾一下里氏替换原则4层含义:
1.子类必须完全实现父类的方法,且方法对子类是有意义的
2.子类可以有自己的个性
3.覆盖或者实现父类方法时输入参数可以被放大
4.覆盖或者实现父类方法时输出参数可以被缩小
简单一句话:所有引用父类的地方必须能够透明地被替换为子类。