定义:
所有引用基类的地方必须能透明地使用其子类的对象。
通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。
但是这里我们需要注意的是:有子类出现的地方,父类未必就能适应。
优点:
代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
提高代码的重用性;
提高代码的可扩展性;
提高产品或项目的开放性。
缺点:降低代码的灵活性,子类必须拥有父类的属性和方法,让子类增加了约束;
增强了耦合性,当父类的常量、变量和方法被修改时,必须考虑子类的修改。
含义:
例:
如上图,我们知道枪有很多种,而且有一个共同的功能就是射击。Soldier中我们添加了一个KillEnemy()方法,这个方法使用枪来杀敌人,具体使用哪个枪取决于选用哪个 枪。
代码:
public abstract class AbstractGun { public abstract void Shoot(); }
class Handgun : AbstractGun { public override void Shoot() { Console.WriteLine("手枪射击"); } }
class Rifle:AbstractGun { public override void Shoot() { Console.WriteLine("步枪射击"); } }
class MachineGun : AbstractGun { public override void Shoot() { Console.WriteLine("机枪射击"); } }
public class Soldier { private AbstractGun gun; public AbstractGun Gun { set { this.gun = value; } get { return gun; } } public void KillEnemy() { Console.WriteLine("士兵开始射击"); gun.Shoot(); } }客户端:
static void Main(string[] args) { Soldier soldier = new Soldier(); soldier.Gun = new Handgun(); soldier.KillEnemy(); }
注意:在类中调用其他类时务必要用父类或接口,若不能使用,则说明类的设计已经违背LSP原则。
但是现在有一把玩具枪,但是我们知道玩具枪是不能像其它的枪一样射击并击杀敌人的,所以玩具枪不能真正实现Shoot方法。
class ToyGun : AbstractGun { public override void Shoot() { //玩具枪不能射击杀人,所以不能真正实现此方法 } }
在这种情况下,我们发现业务调用类已经出现问题了,正常的业务逻辑不能运行。这里有两种处理方法:
(1)在Soldier类中增加判断,如果是仿真枪,就不用来杀人。这个方法可以解决问题,但是在程序中,我们每增加一个类,所有与这个父类有关系的类都必须修改,这样就不可行了。所以这个方案被否定了。
(2)ToyGun脱离继承,建立一个独立的父类,可以与AbstractGun建立关联委托关系。类图如下:
2、子类可以有自己的个性
因为有这个规则,里氏替换原则不能反过来使用,在子类出现的地方,父类不一定可以胜任。
3、覆盖或者实现父类的方法的时候输入的参数可以被放大
子类的输入参数类型的范围要大于等于父类的参数。当子类代替父类传递到调用者中,父类执行,子类永远不会被执行,这是正确的。如果你想让子类执行,必须覆写父类的方法。但若反过来,子类参数类型的范围小于父类,那么父类存在的地方,子类就未必可以存在。则子类就会被执行,这会影发业务逻辑混乱,因为在实际应用中父类一般是抽像类,子类是实现类,这样的一个子类会歪曲父类的意图。
所以子类中方法方法的前置条件必须与父类中被覆写的方法的前置条件相同或者更宽松。
比如,父类的一个方法返回值是类型T,子类的相同方法(重载或覆盖)的返回值是S,那么里氏替换原则就要求S必须小于等于T,也就是说,要么S和T是同一个类型,要么S是T的子类。
总结:
采用里氏替换原则的目的就是增强程序的健壮性,版本升级时也可以保持非常好的兼容性。即使增加子类,原有的子类也可以继续运行。在实际项目中,每个子类对应不同的业务含义,使用父类作为参数,传递不同的子类完成不同的业务逻辑。