里氏替换原则
在六大设计原则中,里式替换原则,是一个关于父类和子类关系的原则,这个原则规定了,在软件开发中,父类出现的地方,把父类替换成子类,系统的功能应该不能受到影响。
里氏替换原则的主要作用如下。
它克服了继承中重写父类造成的可复用性变差的缺点。
它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。
加强程序的健壮性,同时变更时可以做到非常好的兼容性,提高程序的维护性、可扩展性,
降低需求变更时引入的风险。
里式替换原则为良好的基础关系定义了一个规范,里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含了4层含义。
1、子类必须完全实现父类的方法
2、子类可以有自己的个性
3、覆盖或者实现父类时的输入参数可以被放大
4、覆写或实现父类方法时输出参数可以被缩小
一、子类必须完全实现父类的方法
我们在做系统设计的时候,一般都会根据需求,首先定义一个接口或者是抽象类,然后再对接口或者是抽象类进行编码实现,调用类以接口或者抽象类作为函数的参数,接收接口或者是抽象类的实现,在这里,就已经使用了里氏替换原则了。里氏替换原则规定,在父类作为参数的方法中,传入任何一个子类,其功能不应该受任何影响。
让我们来举个例子说明一下,比如说,大家都玩过CS吧,我们可以使用不同的枪去和敌人突突突,用类图来描述这个场景:
士兵装备好枪,然后用枪进行射击。
代码如下:
枪抽象类:
public abstract class AbstractGun {
public abstract void shoot();
}
手枪实现类:
public class HandGun extends AbstractGun {
@Override
public void shoot() {
System.out.println("手枪射击");
}
}
步枪实现类
public class Rifle extends AbstractGun {
@Override
public void shoot() {
System.out.println("步枪射击");
}
}
士兵类:
public class Soldier {
private AbstractGun gun;
void setGun(AbstractGun gun){
this.gun = gun;
}
void killEnemy(){
System.out.println("士兵上战场");
gun.shoot();
}
}
game类主方法
public class Game {
public static void main(String[] args) {
Soldier soldier = new Soldier();
soldier.setGun(new HandGun());
soldier.killEnemy();
soldier.setGun(new Rifle());
soldier.killEnemy();
}
}
类的结构很简单,我们可以为士兵设置不同的枪,用不同的枪去射击,之后如果有新的类型的枪,再去定义新的枪,只要遵循完全实现父类(完成射击的需求),代码都不会出问题。
但是如果我们再去定义的枪是玩具枪呢?
玩具枪类:
public class ToyGun extends AbstractGun {
//玩具枪也是枪,但是玩具枪无法射击呀?
@Override
public void shoot() {
System.out.println("玩具枪不能发射子弹");
}
}
这个时候,如果把玩具枪给士兵,玩具枪也是枪的子类,所以士兵会去接收玩具枪,然后用玩具枪上战场,那会是什么后果,直接被敌人爆头了。
那应该怎么办?
1、在soldier类中判断,如果是玩具枪,就被用来杀敌,做特殊处理,但是这样会有什么后果,这样做,意味着程序中所有AbstractGun作为参数的函数,全部都要做玩具枪枪的判断,这简直太可怕了,显然,这个方法被否决了。
2、断开继承关系,建立一个独立的父类,如下图。
所以,里氏替换原则约束我们:子类是否可以完整的实现父类的业务,如果不能,那就得做其他考虑了。
二、子类可以有自己的个性。
子类当然会有自己独有的行为和特征,所以在第一条的基础上,里氏替换原则规定了,在子类出现的地方,父类未必能出现,换句话说就是,用子类作为参数的函数,传递父类进去,程序有可能会出错。
还是用枪来举例子:
步枪分为普通步枪和狙击步枪,普通步枪就比如AK-47,直接就能突突突,而狙击步枪,就是那种带瞄准镜的,可以先瞄准,再突突突,而狙击手,可以用狙击枪进行射击。
类图如下:
狙击手需要一把AUG狙击步枪进行射击
AUG类:
public class AUG extends Rifle {
public void zoomOut(){
System.out.println("用望远镜观察敌人");
}
}
狙击手类:
public class Snipper {
private AUG aug;
void setAug(AUG aug){
this.aug = aug;
}
void killEnemy(){
System.out.println("AUG射击。。。。");
}
}
main方法:
public class Client {
public static void main(String[] args) {
Snipper snipper = new Snipper();
snipper.setAug(new AUG());
snipper.killEnemy();
snipper.setAug((AUG) new Rifle());
snipper.killEnemy();
}
}
运行结果:
很明显,这个方法只能使用子类,如果用父类去作为参数,会报转换异常,从里氏替换原则来看,有子类出现的地方,父类未必可以出现。
三、覆盖或者实现父类时的输入参数可以被放大
子类在重写父类方法的时候,如果父类的参数是HashMap,那么子类的参数就必须更加的宽松,比如Map或者Map,如果父类是Map,那子类只能是Map.
举个例子:
我有一个father类和son类,father类是son的父类
public class Father {
public Collection doSomething(HashMap map){
System.out.println("父类被执行。。。。");
return map.values();
}
}
class Son extends Father {
public Collection doSomeThing(Map map){
System.out.println("子类被执行。。。。");
return map.values();
}
}
public class Client {
public static void main(String[] args) {
//里氏替换原则中,父类出现的地方,子类也可以出现
// Father f = new Father();
Son f = new Son();
HashMap hashMap = new HashMap();
f.doSomething(hashMap);
}
}
上面的代码,定义了三个类,在father类中,父类的参数是Map,子类的参数是HashMap,我们知道Map是HashMap的父类,所以给父类的参数是hashmap,依然能正常运行,上面的代码,注释的代码
// Father f = new Father();
Son f = new Son();
不管用那个,执行结果都是一样,都是
因为子类的参数比父类宽松,所以把父类替换成子类,结果不会受到影响。
如果父类参数比子类宽松(父类是Map,子类是HashMap)
public class Father {
public Collection doSomething(Map map){
System.out.println("父类被执行。。。。");
return map.values();
}
}
class Son extends Father {
public Collection doSomeThing(HashMap map){
System.out.println("子类被执行。。。。");
return map.values();
}
}
public class Client {
public static void main(String[] args) {
//里氏替换原则中,父类出现的地方,子类也可以出现
// Father f = new Father();
Son f = new Son();
HashMap hashMap = new HashMap();
f.doSomething(hashMap);
}
}
那么当把父类替换成子类后,执行结果便是:
这和里氏替换原则中,父类替换成子类,执行结果不变相违背。
四、覆写或实现父类方法时输出参数可以被缩小
在子类覆写或实现父类方法时,子类返回的参数,应该和父类的参数保持一致,或者是父类参数的子类。
五、总结
采样里氏替换原则的目的,就是增加程序的健壮性,让项目在版本升级的同时,也可以保持非常好的兼容性,即使增加子类,原有的子类还可以继续运行。在实际项目中,每个子类对应不同的业务,使用父类作为参数,传递不同的子类去实现业务逻辑,非常完美!
- 点赞
- 收藏
- 关注作者
评论(0)