子类行为不能超过父类行为
——由模式谈面向对象的基本原则里氏替换原则
——由模式谈面向对象的基本原则里氏替换原则
原则:子类型必须能够替换它的基类型。
这就是著名的 Liskov替换原则(LSP),又称 里氏替换原则。
对于这个原则,通俗一些的理解就是,父类的方法都要在子类中实现或者重写,不允许子类出现父类所没有定义的方法。
我们前面说过依赖颠倒原则,说的是父类不能依赖子类,它们都要依赖抽象类。我们说这种依赖是我们实现代码扩展和运行期内绑定(多态)的基础。因为一旦类的使用者依赖某个具体的类,那么对该依赖的扩展就无从谈起;而依赖某个抽象类,则只要实现了该抽象类的子类,都可以被类的使用者使用,从而实现了系统的扩展。
但是,光有依赖颠倒原则,并不一定就使我们的代码真正具有良好的扩展性和运行期内绑定。请看下面的代码:
public class Animal
{
private String name;
public Animal(String name)
{
this.name = name;
}
public void descriptiion()
{
System.out.println(“This is a(an) ”+name);
}
}
下面是它的子类:
public class Cat extends Animal
{
public Cat(String name)
{
super(name);
}
public void mew()
{
System.out.println(“The cat is saying like ‘mew’”);
}
}
然后我们来看看Dog的实现:
public class Dog extends Animal
{
public Dog(String name)
{
super(name);
}
public void bark()
{
System.out.println(“The dog is saying like ‘bark’”);
}
}
最后,我们来看客户端的调用:
public decriptionTheAnimal(Animal animal)
{
if(animal instanceof Cat)
{
Cat cat = (Cat)animal;
Cat.decription();
Cat.mew();
}
else if(animal instanceof Dog)
{
Dog dog = (Dog)animal;
Dog.decription();
Dog.bark();
}
}
通过上面的代码,我们可以看到虽然客户端的依赖是对抽象的依赖,但依然这个设计的扩展性不好,运行期绑定没有实现。
是什么原因呢?其实就是因为不满足里氏替换原则,子类如Cat有mew()方法父类根本没有,Dog类有bark()方法父类也没有,两个子类都不能替换父类。这样导致了系统的扩展性不好和没有实现运行期内绑定。
现在看来,一个系统或子系统要拥有良好的扩展性和实现运行期内绑定,有两个必要条件:第一是依赖颠倒原则;第二是里氏替换原则。这两个原则缺一不可。
里氏替换原则的好处:
第一、保证系统或子系统有良好的扩展性。只有子类能够完全替换父类,才能保证系统或子系统在运行期内识别子类就可以了,因而使得系统或子系统有了良好的扩展性。
第二、实现运行期内绑定,即保证了面向对象多态性的顺利进行。这节省了大量的代码重复或冗余。避免了类似instanceof这样的语句,或者getClass()这样的语句,这些语句是面向对象所忌讳的。
第三、有利于实现契约式编程。契约式编程有利于系统的分析和设计,指我们在分析和设计的时候,定义好系统的接口,然后再编码的时候实现这些接口即可。在父类里定义好子类需要实现的功能,而子类只要实现这些功能即可。
我们知道,在我们的大多数的模式中,我们都有一个共同的接口,然后子类和扩展类都去实现该接口。这里,我们以命令模式为例。
下面是一段原始代码:
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 implements Action
{
public void doAction()
{
//do add action
……
}
}
public class ViewAction implements Action
{
public void doAction()
{
//do view action
……
}
}
public class deleteAction implements Action
{
public void doAction()
{
//do delete action
……
}
}
public class ModifyAction implements Action
{
public void doAction()
{
//do modify action
……
}
}
这样,客户端的调用大概如下:
public void execute(Action action)
{
action.doAction();
}
看,上面的客户端代码再也没有出现过instanceof这样的语句,扩展性良好,也有了运行期内绑定的优点。
在这里,只拿出命令模式一个模式来作为例子来看看模式是怎么遵从里氏替换原则的。其他的模式的例子就不再拿出来说。我们可以看到,几乎所有的模式都遵从了该原则,不信可以自行找来看看。
这就是著名的 Liskov替换原则(LSP),又称 里氏替换原则。
对于这个原则,通俗一些的理解就是,父类的方法都要在子类中实现或者重写,不允许子类出现父类所没有定义的方法。
我们前面说过依赖颠倒原则,说的是父类不能依赖子类,它们都要依赖抽象类。我们说这种依赖是我们实现代码扩展和运行期内绑定(多态)的基础。因为一旦类的使用者依赖某个具体的类,那么对该依赖的扩展就无从谈起;而依赖某个抽象类,则只要实现了该抽象类的子类,都可以被类的使用者使用,从而实现了系统的扩展。
但是,光有依赖颠倒原则,并不一定就使我们的代码真正具有良好的扩展性和运行期内绑定。请看下面的代码:
public class Animal
{
private String name;
public Animal(String name)
{
this.name = name;
}
public void descriptiion()
{
System.out.println(“This is a(an) ”+name);
}
}
下面是它的子类:
public class Cat extends Animal
{
public Cat(String name)
{
super(name);
}
public void mew()
{
System.out.println(“The cat is saying like ‘mew’”);
}
}
然后我们来看看Dog的实现:
public class Dog extends Animal
{
public Dog(String name)
{
super(name);
}
public void bark()
{
System.out.println(“The dog is saying like ‘bark’”);
}
}
最后,我们来看客户端的调用:
public decriptionTheAnimal(Animal animal)
{
if(animal instanceof Cat)
{
Cat cat = (Cat)animal;
Cat.decription();
Cat.mew();
}
else if(animal instanceof Dog)
{
Dog dog = (Dog)animal;
Dog.decription();
Dog.bark();
}
}
通过上面的代码,我们可以看到虽然客户端的依赖是对抽象的依赖,但依然这个设计的扩展性不好,运行期绑定没有实现。
是什么原因呢?其实就是因为不满足里氏替换原则,子类如Cat有mew()方法父类根本没有,Dog类有bark()方法父类也没有,两个子类都不能替换父类。这样导致了系统的扩展性不好和没有实现运行期内绑定。
现在看来,一个系统或子系统要拥有良好的扩展性和实现运行期内绑定,有两个必要条件:第一是依赖颠倒原则;第二是里氏替换原则。这两个原则缺一不可。
里氏替换原则的好处:
第一、保证系统或子系统有良好的扩展性。只有子类能够完全替换父类,才能保证系统或子系统在运行期内识别子类就可以了,因而使得系统或子系统有了良好的扩展性。
第二、实现运行期内绑定,即保证了面向对象多态性的顺利进行。这节省了大量的代码重复或冗余。避免了类似instanceof这样的语句,或者getClass()这样的语句,这些语句是面向对象所忌讳的。
第三、有利于实现契约式编程。契约式编程有利于系统的分析和设计,指我们在分析和设计的时候,定义好系统的接口,然后再编码的时候实现这些接口即可。在父类里定义好子类需要实现的功能,而子类只要实现这些功能即可。
我们知道,在我们的大多数的模式中,我们都有一个共同的接口,然后子类和扩展类都去实现该接口。这里,我们以命令模式为例。
下面是一段原始代码:
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 implements Action
{
public void doAction()
{
//do add action
……
}
}
public class ViewAction implements Action
{
public void doAction()
{
//do view action
……
}
}
public class deleteAction implements Action
{
public void doAction()
{
//do delete action
……
}
}
public class ModifyAction implements Action
{
public void doAction()
{
//do modify action
……
}
}
这样,客户端的调用大概如下:
public void execute(Action action)
{
action.doAction();
}
看,上面的客户端代码再也没有出现过instanceof这样的语句,扩展性良好,也有了运行期内绑定的优点。
在这里,只拿出命令模式一个模式来作为例子来看看模式是怎么遵从里氏替换原则的。其他的模式的例子就不再拿出来说。我们可以看到,几乎所有的模式都遵从了该原则,不信可以自行找来看看。