里氏替换原则

       我们在学习面向对象语言时,都会学到三大特征:封装多态继承继承就是告诉你拥有父类的方法和属性,然后你也可以重写父类的方法。如此,问题产生了:“我们如何去度量继承关系的质量?”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.覆盖或者实现父类方法时输出参数可以被缩小

     简单一句话:所有引用父类的地方必须能够透明地被替换为子类。

原文地址:https://www.cnblogs.com/ForOne/p/3803157.html