里氏替换原则
1296字约4分钟
2022-04-22
定义
里氏替换原则(Liskov Substitution Principle ,LSP).
所有引用积累的地方必须能透明地使用其派生类的对象.
简单来说, 在代码中任何使用到父类对象的地方, 都能够直接使用子类对象直接替代而功能正常.
作用
有一功能 P1, 由类 A 完成. 现需要将功能 P1 进行扩展, 扩展后的功能为 P, 其中 P 由原有功能 P1 与新功能 P2 组成. 新功能 P 由类 A 的子类 B 来完成, 则子类 B 在完成新功能 P2 的同时, 有可能会导致原有功能 P1 发生故障.
如果我们遵循里氏替换原则可以保证代码不会发生故障.
里氏替换原则四层含义
子类可以实现父类的抽象方法, 但不能覆盖父类的非抽象方法. 子类可以实现父类的抽象方法, 但不能覆盖父类的非抽象方法, 父类中凡是已经实现好的方法(相对于抽象方法而言), 实际上是在设定一系列的规范和契约, 虽然它不强制要求所有的子类必须遵从这些契约, 但是如果子类对这些非抽象方法任意修改, 就会对整个继承体系造成破坏.
子类可以有自己的个性. 在继承父类属性和方法的同时, 每个子类也都可以有自己的个性, 在父类的基础上扩展自己的功能. 前面其实已经提到, 当功能扩展时, 子类尽量不要重写父类的方法, 而是另写一个方法, 所以对上面的代码加以更改, 使其符合里氏替换原则.
覆盖或实现父类的方法时输入参数可以被放大. 当子类的方法重载父类的方法时, 方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松.
覆写或实现父类的方法时输出结果可以被缩小. 当子类的方法实现父类的抽象方法时, 方法的后置条件(即方法的返回值)要比父类更严格.
里氏替换原则优点
- 保证了父类的复用性.
- 降低系统出错误的故障, 防止误操作.
- 不会破坏继承的机制.
- 增强程序的健壮性, 版本升级是也可以保持非常好的兼容性. 即使增加子类, 原有的子类还可以继续运行. 在实际项目中, 每个子类对应不同的业务含义, 使用父类作为参数, 传递不同的子类完成不同的业务逻辑.
里氏替换原则缺点
继承是入侵性的.
只要继承, 就必须拥有父类的所有属性与方法.
降低了代码的灵活性.
子类拥有了父类的属性方法, 会增多约束.
增强了耦合性.
当父类的常量、变量、方法被修改时, 必须考虑子类的修改.
实现方法
如果两个类 A 和 B 之间的关系违反了里氏替换原则(假设 A 为父类), 则可以选择以下的重构方案:
- 创建新的抽象类 C 作为 A 和 B 的父类, 将 A 和 B 的共同欣慰移动到 C 中.
- 将继承关系改为关联关系.
示例
下面展示其中一种重构方法, 将两个类之间的继承关系改为关联关系.
我们考虑运动员和自行车之间的例子, 每个运动员都有一辆自行车.
反例
class Bike {
public:
void Move();
void Stop();
void Repair();
protected:
int ChangeColor(int);
private:
int mColor;
};
class Player : private Bike
{
public:
void StartRace();
void EndRace();
protected:
int CurStrength ();
private:
int mMaxStrength;
int mAge;
};
正例
class Bike {
public:
void Move();
void Stop();
void Repair();
protected:
int ChangeColor(int);
private:
int mColor;
};
class Player
{
public:
void StartRace( );
void EndRace( );
protected:
int CurStrength ( );
private:
int mMaxStrength;
int mAge;
Bike * abike;
};
很显然, 我们将 Bike
从 Player
的基类改为了其组成的一部分.
总结
继承作为面向对象三大特性之一, 在给程序设计带来巨大便利的同时, 也带来了弊端. 比如使用继承会给程序带来侵入性, 程序的可移植性降低, 增加了对象间的耦合性. 如果一个类被其他的类所继承, 则当这个类需要修改时, 必须考虑到所有的子类, 并且父类修改后, 所有涉及到子类的功能都有可能会产生故障.
里氏替换原则的目的就是增强程序健壮性, 版本升级时也可以保持非常好的兼容性.