里氏替换原则定义
里氏替换原则(Liskov Substitution Principle, LSP):所有引用父类的地方必须能使用其子类的对象。子类必须完全实现父类的方法
我们在做系统设计时,经常会定义一个接口或抽象类,然后编码实现,调用类则直接传入接口或抽象类,其实这已经使用了里氏替换原则。
我们举个射击的例子,其类图如下:
注意:
如果子类不能完整地实现父类的方法,或者父类的一些方法在子类中已经发生畸变,则建议断开继承关系,采用依赖,聚集,组合等关系代替继承。
子类可以有自己的个性
子类当然可以有自己的行为和外观,也就是方法和属性。但是里氏替换原则可以正着用,但是不能反着用。在子类出现的地方,父类未必就可以胜任。还是以刚才枪支为例,在步枪中,有一些枪支比较有名,比如AK47,AUG狙击步枪等,我们把这二个型号的枪支引入后的类图如下:
我们定义一个父类叫Animal, 其包含一个方法叫Say如下:
public class Animal { private readonly string _sayContent; public Animal(string sayContent) { _sayContent = sayContent; } public virtual void Say() { Console.WriteLine($"Animal Say:{_sayContent}"); } }
再定义一个子类Pig 集成自Animal,并覆盖父类中的Say 方法如下:
public class Pig:Animal { private readonly string _sayContent; public Pig(string sayContent) : base(sayContent) { _sayContent = sayContent; } public override void Say() { Console.WriteLine($"Pig Say:{_sayContent}"); } }
现在我们在调用方创建一个Animal的对象并调用Say方法:
Animal animal = new Animal("This is a parent class."); animal.Say();
输出结果:
Animal Say:This is a parent class.
下来我们创建一个Pig对象赋给animal 对象并调用Say方法:
static void Main(string[] args) { Animal animal = new Animal("This is a parent class."); animal.Say(); animal = new Pig("This is a sub class."); animal.Say(); Console.ReadKey(); }
输出:
可以看出将子类的对象赋给父类的对象,并且得到了我们期望的结果。
在使用里氏替换原则时需要注意如下几个问题:
(1)子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法。根据里氏替换原则,为了保证系统的扩展性,在程序中通常使用父类来进行定义,如果一个方法只存在子类中,在父类中不提供相应的声明,则无法在以父类定义的对象中使用该方法。
(2) 我们在运用里氏替换原则时,尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。里氏替换原则是开闭原则的具体实现手段之一。这也就是我们应该更多的依赖抽象,尽量少的依赖实现细节, 其实就是我们下一篇要讲的依赖倒置原则(DIP)。