C#设计模式六大原则 - 里氏替换

  • 单一职责原则(Single Reponsibility Principle,SRP)
  • 里氏替换原则(Liskov Substitution Principle,LSP)
  • 依赖倒置原则(Dependence Inversion Principle,DIP)
  • 接口隔离原则(Interface Segregation Principe,ISP)
  • 迪米特法则(Law of Demeter,LOD)
  • 开闭原则(Open Closed Principle,OCP)

里氏替换原则 LSP:

Liskov Substitution Principle,简称:LSP。

所有使用基类的地方,都可以使用其子类来代替,而且行为不会有任何变化

肯定有不少人跟我刚看到这项原则的时候一样,对这个原则的名字充满疑惑。其实原因就是这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的。

问题由来:有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。

解决方案:当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。


继承包含这样一层含义:父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。

面向对象语言的继承是项很牛的设计,普通类间父子继承,抽象类以及接口,它们之间的相互关联与纠缠,看似复杂,实则给我们带来很多好处:代码共享,减少创建类的工作量,提高了代码的复用性;提高了代码的可扩展性与项目的开放性,实现父类方法后,子类可任意扩展,想想一些框架的扩展接口不都是通过继承来完成的么。里氏替换原则就是为良好的继承定义了一个规范。主要如下:

  • 子类必须完全实现父类有的方法,如果子类没有父类的某项东西,就断掉继承;
  • 子类可以有父类没有的东西,所以子类的出现的地方,不一定能用父类来代替;
  • 透明,就是安全,父类的东西换成子类后不影响程序
    • a、父类已经实现的东西,子类不要去new
    • b、父类已经实现的东西,想改的话,就必须用virtual+override 避免埋雷

这里继续那鸟来举例子。先看个反例,鸟类都需要吃东西,都需要喝水,还可以飞,代码如下:

public class Bird
{
    public string Name => this.GetType().Name;

    public void Eat()
    {
        Console.WriteLine($"我是{this.Name},我需要吃东西");
    }

    public void Drink()
    {
        Console.WriteLine($"我是{this.Name},我需要喝水");
    }

    public void Fly()
    {
        Console.WriteLine($"我是{this.Name},我可以飞");
    }
}

/// 
/// 现在来了只比较大的鸟,叫企鹅,继承了鸟类
/// 
public class Penguin : Bird
{
    //Do nothing
}

调用一下

class Program
{
    static void Main(string[] args)
    {
        try
        {
            Bird bird = new Penguin();
            bird.Eat();
            bird.Drink();
            bird.Fly();
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
        }
        Console.Read();
    }
}

运行结果:
我是Penguin,我需要吃东西
我是Penguin,我需要喝水
我是Penguin,我可以飞

这就出问题了,企鹅显然是不会飞的啊,也继承了鸟类,这就违背了里氏替换原则。企鹅属于海鸟,虽然是鸟类,可不能飞,比较特珠,就需要断掉继承。企鹅这是说话了,我不能飞,我也要吃和喝啊,怎么办?那你都不属于动物吗,我们来使用里氏替换原则重构一下:

企鹅、麻雀、孔雀

代码如下:

public class Animal
{
    public string Name => this.GetType().Name;

    public void Eat()
    {
        Console.WriteLine($"我是{this.Name},我需要吃东西");
    }

    public void Drink()
    {
        Console.WriteLine($"我是{this.Name},我需要喝水");
    }
}

public class Bird : Animal
{
    /// 
    /// 鸟有自己可以飞的方法
    /// 
    public void Fly()
    {
        Console.WriteLine($"我是{this.Name},我可以飞");
    }
}

public class Penguin : Animal	//企鹅
{
    //do nothing
}

public class Sparrow : Bird		//麻雀
{
    //do nothing
}

public class Peacock : Bird		//孔雀
{
    /// 
    /// 孔雀可以开屏
    /// 
    public void Open()
    {
        Console.WriteLine($"我是{this.Name},我可以开屏,开着屏抖抖更漂亮!");
    }
}

调用如下:

class Program
{
    static void Main(string[] args)
    {
        try
        {
            {
                Bird bird = new Sparrow();
                bird.Fly();  //可以飞
            }

            {
                //Bird bird = new Peacock(); //子类出现的地方父类不能代替
                Peacock bird = new Peacock();
                bird.Fly();
                bird.Open();
            }
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
        }
        Console.Read();
    }
}

里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下4层含义:

  • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
  • 子类中可以增加自己特有的方法。
  • 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
  • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

看上去很不可思议,因为我们会发现在自己编程中常常会违反里氏替换原则,程序照样跑的好好的。所以大家都会产生这样的疑问,假如我非要不遵循里氏替换原则会有什么后果?

后果就是:你写的代码出问题的几率将会大大增加。

你可能感兴趣的:(#,设计模式,设计模式,c#)