面向对象设计原则实践:之四.里氏代换原则

五、里氏代换原则(LSP--Liskov Substitution Principle)

1. 定义

a). 如果对每一个类型为S的对象o1,都有类型为T的对象o2,

使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有变化,

那么类型S是类型T的子类型。

b). 子类型必须能够替换它的基类型。LSP又称里氏替换原则。 

对于这个原则,通俗一些的理解就是,父类的方法都要在子类中实现或者重写。 。

2.分析

a). 在软件中如果能够使用基类对象,那么一定能够使用其子类对象。

把基类都替换成它的子类,程序将不会产生任何错误和异常,

反过来则不成立,如果一个软件实体使用的是一个子类的话,那么它不一定能够使用基类。

b). 里氏代换原则是实现开闭原则的重要方式之一,

由于使用基类对象的地方都可以使用子类对象,因此,

在程序定义中,尽量使用基类类型来对对象进行定义,

在程序运行时,再确定其子类类型,用子类对象来替换父类对象。 

3.      实例一

某系统需要实现对重要数据(如用户密码)的加密处理,

在数据操作类(DataOperator)中需要调用加密类中定义的加密算法,

系统提供了两个不同的加密类,CipherA和CipherB,它们实现不同的加密方法,

在DataOperator中可以选择其中的一个实现加密操作。

如图所示:

里氏代换原则-图-1

里氏代换原则-图-2

图(一)和图(二)分析:

(图一)中,

如果需要更换一个加密算法类或者增加并使用一个新的加密算法类,

如将CipherA改为CipherAA, 增加一个CipherC,则需要修改客户类Client和数据操作类DataOperator的源代码,违背了开闭原则。

现使用里氏代换原则对其进行重构,使得系统可以灵活扩展,符合开闭原则。

(图二)中,

在程序定义时:

子类CipherB继承了基类CipherA,

并在DataOperator类中定义了基类对象cipherA

在程序运行时,

客户端类Client使用了配置文件config.xml,配置文件指明了运行时使用的子类加密算法CipherB

4. 实现二

对于依赖倒置原则,说的是父类不能依赖子类,它们都要依赖抽象类。

这种依赖是我们实现代码扩展和运行期内绑定(多态)的基础。

因为一旦类的使用者依赖某个具体的类,那么对该依赖的扩展就无从谈起;

而依赖某个抽象类,则只要实现了该抽象类的子类,都可以被类的使用者使用,从而实现了系统的扩展。 

但是,光有依赖倒置原则,并不一定就使我们的代码真正具有良好的扩展性和运行期内绑定。

请看下面的代码: 

public class Animal{

private string name;

public Animal(string name){

this.name = name;

}

public void Description(){

Console.WriteLine("This is a(an) " + name);

}

}

//下面是它的子类猫类:

public class Cat : Animal{

public Cat(string name){

}

public void Mew(){

Console.WriteLine("The cat is saying like 'mew'");

}

}

//下面是它的子类狗类:

public class Dog : Animal{

public Dog(string name){

}

public void Bark(){

Console.WriteLine("The dog is saying like 'bark'");

}

}

//最后,我们来看客户端的调用:

public void DecriptionTheAnimal(Animal animal){

if (typeof(animal) is Cat){

Cat cat = (Cat)animal;

Cat.Decription();

Cat.Mew();

}

else if (typeof(animal) is Dog){

Dog dog = (Dog)animal;

Dog.Decription();

Dog.Bark();

}

}

通过上面的代码,我们可以看到虽然客户端的依赖是对抽象的依赖,

但依然这个设计的扩展性不好,运行期绑定没有实现。 

是什么原因呢?

其实就是因为不满足里氏替换原则:

子类如Cat有Mew()方法父类根本没有,Dog类有Bark()方法父类也没有,

两个子类都不能替换父类。

这样导致了系统的扩展性不好和没有实现运行期内绑定。 

现在看来,一个系统或子系统要拥有良好的扩展性和实现运行期内绑定,有两个必要条件:

第一是依赖倒置原则;

第二是里氏替换原则。

这两个原则缺一不可。 

我们知道,在我们的大多数的模式中,我们都有一个共同的接口,然后子类和扩展类都去实现该接口。 

下面是一段原始代码:

if(action.Equals(“add”)){

//do add action

}

else if(action.Equals(“view”)){

//do view action

}

else if(action.Equals(“delete”)){

//do delete action

}

else if(action.Equals(“modify”)){

//do modify action

}

我们首先想到的是把这些动作分离出来,就可能写出如下的代码: 

public class AddAction{

public void add(){

//do add action

}

}

public class ViewAction{

public void view(){

//do view action

}

}

public class deleteAction{

public void delete(){

//do delete action

}

}

public class ModifyAction{

public void modify(){

//do modify action

}

}

我们可以看到,这样代码将各个行为独立出来,满足了单一职责原则,

但这远远不够,因为它不满足依赖颠倒原则和里氏替换原则。 

下面我们来看看命令模式对该问题的解决方法: 

public interface Action{

public void doAction();

}

//然后是各个实现:

public class AddAction : Action{

public void doAction(){

//do add action

}

}

public class ViewAction : Action{

public void doAction(){

//do view action

}

}

public class deleteAction : Action{

public void doAction(){

//do delete action

}

}

public class ModifyAction : Action{

public void doAction(){

//do modify action

}

}

//这样,客户端的调用大概如下:

public void execute(Action action){

action.doAction();

}

看,上面的客户端代码再也没有出现过typeof这样的语句,扩展性良好,也有了运行期内绑定的优点。 

5. 总结

子类型必须能够替换掉它们的父类。

LSP优点: 

1)、保证系统或子系统有良好的扩展性。

只有子类能够完全替换父类,才能保证系统或子系统在运行期内识别子类就可以了,

因而使得系统或子系统有了良好的扩展性。 

2)、实现运行期内绑定,即保证了面向对象多态性的顺利进行。

这节省了大量的代码重复或冗余。

避免了类似instanceof这样的语句,或者getClass()这样的语句,这些语句是面向对象所忌讳的。 

3)、有利于实现契约式编程。

契约式编程有利于系统的分析和设计,指我们在分析和设计的时候,定义好系统的接口,然后再编码的时候实现这些接口即可。

在父类里定义好子类需要实现的功能,而子类只要实现这些功能即可。

使用LSP注意点: 

1)、此原则和OCP的作用有点类似,其实这些面向对象的基本原则就2条:

A:面向接口编程,而不是面向实现;

B:用组合而不主张用继承 

2)、LSP是保证OCP的重要原则 

3)、这些基本的原则在实现方法上也有个共同层次,就是使用中间接口层,以此来达到类对象的低偶合,也就是抽象偶合! 

4)、派生类的退化函数:

派生类的某些函数退化(变得没有用处),Base的使用者不知道不能调用f,会导致替换违规。

在派生类中存在退化函数并不总是表示违反了LSP,但是当存在这种情况时,应该引起注意。 

5)、从派生类抛出异常:

如果在派生类的方法中添加了其基类不会抛出的异常。

如果基类的使用者不期望这些异常,那么把他们添加到派生类的方法中就可以能会导致不可替换性。 

猜你喜欢

转载自blog.csdn.net/fireroll/article/details/82107343
今日推荐