1. 定义
第一种定义:如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。
第二种定义:所有引用基类的地方必须能透明地使用其子类的对象。通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。
2. 规则
里氏替换原则为良好的继承定义了一个规范。
2.1 子类必须完全实现父类的方法
我们在做系统设计时,经常会定义一个接口或抽象类,然后编码实现,调用类则直接传入接口或抽象类,其实这里已经使用了里氏替换原则。
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
namespace helloworld
{
class Program
{
static void Main(string[] args)
{
// 产生三毛这个士兵
Soldier sanMao = new Soldier();
//在类中调用其他类时务必要使用父类或接口,如果不能使用父类或接口,则说明类的设计已经违背了LSP原则。
sanMao.setGun(new Rifle());
sanMao.killEnemy();
}
}
/// <summary>
/// 定义了枪支的抽象类
/// </summary>
public abstract class AbstractGun
{
//枪用来干什么的?杀敌!
public abstract void shoot();
}
public class Handgun : AbstractGun
{
//手枪的特点是携带方便,射程短
public override void shoot()
{
Console.WriteLine("手枪射击...");
}
}
public class Rifle : AbstractGun
{
//步枪的特点是射程远,威力大
public override void shoot()
{
Console.WriteLine("步枪射击...");
}
}
public class MachineGun : AbstractGun
{
public override void shoot()
{
Console.WriteLine("机枪扫射...");
}
}
public class Soldier
{
//定义士兵的枪支
private AbstractGun gun;
//给士兵一支枪
public void setGun(AbstractGun _gun)
{
this.gun = _gun;
}
public void killEnemy()
{
Console.WriteLine("士兵开始杀敌人...");
gun.shoot();
}
}
}
我们再来想一想,如果我们有一个玩具手枪,该如何定义呢?我们先在类图2-1上增加一个类ToyGun,然后继承于AbstractGun类,修改后的类图如图2-2所示。
首先我们想,玩具枪是不能用来射击的,杀不死人的,这个不应该写在shoot方法中。
public class ToyGun : AbstractGun
{
//玩具枪是不能射击的,但是编译器又要求实现这个方法,怎么办?虚构一个呗!
public override void shoot()
{
//玩具枪不能射击,这个方法就不实现了
}
}
在Soldier类中增加instanceof的判断,如果是玩具枪,就不用来杀敌人。这个方法可以解决问题,但是你要知道,在程序中,每增加一个类,所有与这个父类有关系的类都必须修改,你觉得可行吗?
ToyGun应该脱离继承,建立一个独立的父类,为了实现代码复用,可以与AbastractGun建立关联委托关系。
如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承。
2.2 子类可以有自己的个性
2.3 覆盖或实现父类的方法时输入参数可以被放大
方法中的输入参数称为前置条件,这是什么意思呢?大家做过Web Service开发就应该知道有一个“契约优先”的原则,也就是先定义出WSDL接口,制定好双方的开发协议,然后再各自实现。里氏替换原则也要求制定一个契约,就是父类或接口,这种设计方法也叫做Design by Contract(契约设计),与里氏替换原则有着异曲同工之妙。契约制定了,也就同时制定了前置条件和后置条件,前置条件就是你要让我执行,就必须满足我的条件;后置条件就是我执行完了需要反馈,标准是什么。
2.4 覆写或实现父类的方法时输出结果可以被缩小
父类的一个方法的返回值是一个类型T,子类的相同方法(重载或覆写)的返回值为S,那么里氏替换原则就要求S必须小于等于T,也就是说,要么S和T是同一个类型,要么S是T的子类
3.实践
采用里氏替换原则时,尽量避免子类的“个性”,一旦子类有“个性”,这个子类和父类之间的关系就很难调和了,把子类当做父类使用,子类的“个性”被抹杀——委屈了点;把子类单独作为一个业务来使用,则会让代码间的耦合关系变得扑朔迷离——缺乏类替换的标准。