设计模式:里氏替换原则
里氏替换原则(Liskov Substitution Principle ,LSP):
指的是任何基类可以出现的地方,子类一定可以出现。
定义1
如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都替换成o2时,程序p的行为没有发生变化,那么类型T2是类型T1的子类型。
定义2
所有引用基类的地方必须能透明地使用其子类对象。
问题由来
有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。
解决方案
当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。
里氏替换原则包含了四层含义
1.子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法。
实践,以枪为例,看一下类图
枪支类图
枪支的抽象类:
public abstract class AbstractGun {
public abstract void shoot();
}
手枪,步枪实现类:
public class HandGun extends AbstractGun {
public void shoot() {
System.out.println("手机射击");
}
}
public class Rifle extends AbstractGun {
public void shoot() {
System.out.println("步枪射击");
}
}
士兵实现类:
public class Soldier {
private AbstractGun gun;
public void setGun(AbstractGun gun) {
this.gun = gun;
}
public void killEnemy() {
System.out.println("士兵杀敌人");
gun.shoot();
}
}
场景类:
public class Client {
public static void main(String[] args) {
Soldier sanMao = new Soldier();
sanMao.setGun(new Rifle());
sanMao.killEnemy();
}
}
注意
在类中调用其他类时务必要使用父类或者接口(例如Solider类的setGun(AbstractGun gun)方法),否则说明类的设计已经违背了LSP原则。
现在有个玩具枪该怎么定义?直接继承AbstractGun类吗?如下:
public class ToyGun extends AbstractGun {
@Override
public void shoot() {
//玩具枪不能像真枪杀敌,不实现
}
}
现在的场景类:
public class Client {
public static void main(String[] args) {
Soldier sanMao = new Soldier();
sanMao.setGun(new ToyGun());
sanMao.killEnemy();
}
}
在这种情况下,士兵拿着玩具枪杀敌,发现业务调用类已经出现了问题,正常的业务逻辑运行结果是不正确的。(因为玩具枪并不能杀敌)ToyGun应该脱离继承,建立一个独立的类,可以与AbstractGun建立关联委托关系。类图如下:
玩具枪与真实枪分离
按照继承的原则,ToyGun继承AbstractGun是没有问题的,但是在具体应用场景中就需考虑:子类是否能够完整地实现父类的业务,否则就会出现上面的情况拿玩具枪杀敌。
注意
如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中发生重写或者重载,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承。
2 子类中可以增加自己特性
子类当然可有自己的方法和属性。里氏替换原则可以正着用,但是不能反着用,在子类出现的地方,父类未必可以胜任。
再说下面两层含义之前先要明白 重载 重写(覆盖) 的区别:
重写(覆盖)的规则:
重载的规则:
3 类的方法重载父类的方法时,方法的前置条件(形参)要比父类方法的输入参数更宽松.
实例:
public class Father {
public Collection doSomething(HashMap map){
System.out.println("父类被执行了");
return map.values();
}
}
public class Son extends Father{
public Collection doSomething(Map map){
System.out.println("子类被执行了");
return map.values();
}
}
public class Client{
public static void main(String[] args) {
invoker();
}
public static void invoker(){
Son son = new Son();//子类对象
HashMap map=new HashMap<>();
son.doSomething(map);
}
}
运行是”父类被执行了”,这是正确的,父类方法的参数是HashMap类型,而子类的方法参数是Map类型,子类的参数类型范围比父类大,那么子类的方法永远也不会执行。
4 覆写或者实现父类的方法时输出结果(返回值)可以被缩小
父类的一个方法的返回值是一个类型T,子类的相同方法(重载或者重写)的返回值为S,那么里氏替换原则就要求S必须小于等于T。
总结
有子类出现的地方父类未必就可以出现
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 智能桌面机器人:用.NET IoT库控制舵机并多方法播放表情
· Linux glibc自带哈希表的用例及性能测试
· 深入理解 Mybatis 分库分表执行原理
· 如何打造一个高并发系统?
· .NET Core GC压缩(compact_phase)底层原理浅谈
· 新年开篇:在本地部署DeepSeek大模型实现联网增强的AI应用
· DeepSeek火爆全网,官网宕机?本地部署一个随便玩「LLM探索」
· Janus Pro:DeepSeek 开源革新,多模态 AI 的未来
· 上周热点回顾(1.20-1.26)
· 【译】.NET 升级助手现在支持升级到集中式包管理
2018-10-03 MongoDB高可用集群搭建
2018-10-03 MongoDB高可用集群搭建
2018-10-03 MongoDB高可用集群搭建