首页 学海无涯 程序设计 面向对象程序设计六大原则:里式替换原则
面向对象程序设计六大原则:里式替换原则
摘要 在面向对象的程序设计中,里氏替换原则(Liskov Substitution principle)是对子类型的特别定义。它由芭芭拉·利斯科夫(Barbara Liskov)在1987年在一次会议上名为“数据的抽象与层次”的演说中首先提出。

里式替换原则(Liskove Substitution Principle)(LSP)

定义

如果S是T的子类型,对于S类型的任意对象,如果将他们看作是T类型的对象,则对象的行为也理应与期望的行为一致。

里式替换原则的官方定义似乎太过复杂,说的通俗一点就是:任何父类出现的地方都可以用其子类代替(或者说:子类可以在程序中代替其父类),并且不改变原有的程序功能

意义

封装、继承、多态是面向对象的三大基本特征,在程序设计中无处不在,继承是面向对象语言中一种优秀的机制,是实现多态的基础。

先来看看继承的优缺点:

优点:

1.代码复用,减少代码重复量。

2.实现多态,提高可扩展性。

缺点:

1.父类和子类高度耦合,降低可维护性。

不管多优秀的机制,也会存在缺点,有些优点亦是缺点,里氏替换原则正是为了弥补滥用继承带来的危害,增强程序的稳定性

案例讲解

从继承的角度来看,子类能够替换其父类是理所当然的,似乎里氏替换原则是多余的?答案是否定的。

要明白里氏替换原则的真正含义,最好是从违反了里氏替换原则的设计说起。先看以下案例:

/// <summary>
/// 枪
/// </summary>
public class Gun
{
/// <summary>
/// 子弹
/// </summary>
public string Bullet { get; set; } = "子弹";

/// <summary>
/// 射击
/// </summary>
public void Shoot()
{
Console.WriteLine($"发射 {Bullet}");
}

/// <summary>
/// 杀鬼子
/// </summary>
public void KillDevil()
{
Console.WriteLine($"使用 {Bullet} 射杀了鬼子");
}
}

/// <summary>
/// 玩具枪
/// </summary>
public class ToyGun : Gun
{
public ToyGun()
{
Bullet = "橡胶弹";
}
}

/// <summary>
/// Kar98k毛瑟步枪
/// </summary>
public class Kar98K : Gun
{
public Kar98K()
{
Bullet = "7.92×57毫米毛瑟步枪弹";
}
}

按照里氏替换原则的定义:子类可以完全替换父类,那么以下3种调用结果应该没问题:

Gun gun = new Gun();
gun.KillDevil();

Gun gun1 = new Kar98K();
gun1.KillDevil();

Gun gun2 = new ToyGun();
gun2.KillDevil();

而实际上3种调用结果是:

使用 子弹 射杀了鬼子
使用 7.92×57毫米毛瑟步枪弹 射杀了鬼子
使用 橡胶弹 射杀了鬼子

很明显问题来了:橡胶弹能杀死鬼子吗?玩具枪能杀死鬼子吗?使用 橡胶弹 射杀了鬼子,肯定是不合理的。

有人可能会说,杀鬼子的行为不能由父类来决定,子类要自己实现杀鬼子,那么我们将父类方法声明为抽象方法(abstract)或虚方法(virtual),由子类重写:

public abstract class Gun
{
public string Bullet { get; set; } = "子弹";
public abstract void KillDevilAbstract();
}

public class Kar98K : Gun
{
public Kar98K() { Bullet = "7.92×57毫米毛瑟步枪弹"; }

public override void KillDevilAbstract()
{
Console.WriteLine("发射7.92×57毫米毛瑟步枪弹");
Console.WriteLine("打中鬼子头部");
Console.WriteLine("鬼子倒地不起");
Console.WriteLine("成功杀死鬼子");
}
}

public class ToyGun : Gun
{
public ToyGun() { Bullet = "橡胶弹"; }
public override void KillDevilAbstract()
{
Console.WriteLine("发射橡胶弹");
Console.WriteLine("打中鬼子头部");
Console.WriteLine("鬼子毫发无损");
throw new Exception("杀鬼子出现异常");
}
}

public class WaterGun : Gun
{
public WaterGun() { Bullet = "水"; }
public override void KillDevilAbstract()
{
Console.WriteLine("发射水");
Console.WriteLine("打中鬼子头部");
Console.WriteLine("鬼子毫发无损");
throw new Exception("杀鬼子出现异常");
}
}

这里Kar98K可以顺利实现父类KillDevil方法并且达到预期行为,而ToyGun在实现KillDevil方法过程中就会发生异常,达不到预期行为。即使由子类自己来决定如何杀鬼子,某些子类也是无法实现杀鬼子的行为。

这个案例讲到了里式替换原则的一个要点:子类必须完全实现父类的方法。这里的意思不仅仅是简单的通过继承、重写(override)或覆盖(new)来实现父类的方法就完了,而是子类实现父类的方法所产生的行为与父类本身定义的方法产生的预期行为一致。

这句话什么意思?就拿上面的案例来说,Gun父类的KillDevil方法预期的行为是杀鬼子(具体点就是扣动扳机,发射子弹,子弹射中鬼子要害部位,将鬼子击毙),而在设计其子类时,就要考虑到子类必须完全实现这个方法,并且与预期行为一致。上述案例中的ToyGun子类即使继承后获得了父类的KillDevil方法,或者是它覆盖了KillDevil方法,由自己实现杀鬼子的行为。但它始终杀不了鬼子(因为它的玩具枪),达不到父类KillDevil方法所预期的行为,这时候就不满足里式替换原则。

正确的做法是,ToyGun不应该继承Gun类,准确来说是不应该继承带有KillDevil方法的Gun类(又叫断掉继承)。

结语

关于里式替换原则,其实挺难懂得,博主目前只总结出一个要点,也是一知半解。还是得靠经验积累才能理解,但是我们可以牢记一点,子类能够完全替代其父类,才叫满足里式替换原则。

版权声明:本文由不落阁原创出品,转载请注明出处!

本文链接:http://www.leo96.com/article/detail/84

广告位

来说两句吧
最新评论

暂无评论,大侠不妨来一发?