- 单一职责原则(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},我可以飞");
}
}
/// <summary>
/// 现在来了只比较大的鸟,叫企鹅,继承了鸟类
/// </summary>
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
{
/// <summary>
/// 鸟有自己可以飞的方法
/// </summary>
public void Fly()
{
Console.WriteLine($"我是{this.Name},我可以飞");
}
}
public class Penguin : Animal //企鹅
{
//do nothing
}
public class Sparrow : Bird //麻雀
{
//do nothing
}
public class Peacock : Bird //孔雀
{
/// <summary>
/// 孔雀可以开屏
/// </summary>
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层含义:
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
- 子类中可以增加自己特有的方法。
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
看上去很不可思议,因为我们会发现在自己编程中常常会违反里氏替换原则,程序照样跑的好好的。所以大家都会产生这样的疑问,假如我非要不遵循里氏替换原则会有什么后果?
后果就是:你写的代码出问题的几率将会大大增加。