里氏替换原则

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

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

 

 

 

 

你可能感兴趣的:(替换)