[设计模式原则]里氏代换原则(Liskov Substitution Principle ,LSP)

里氏替换原则

里氏是这个原则提出者的名字

关键在替换

 

我们一直强调

设计是开闭的

对于扩展开放,对于修改关闭

扩展就是新的类型

其中之一就是子类

 

好的设计会把握住抽象

后期扩展用子类具体化扩展

这其中一个默认的前提就是

子类不能改变抽象父类的业务规则

这样才能在后期扩展的时候

用子类来覆盖父类

达到扩展业务的目的

 

现在可以说说

什么是里氏替换

就是父类出现的地方

可以用符合里氏替换的子类来替换父类

---java编译器对里氏替换原则的支持表现在

---子类的访问权限要不小于父类的访问权限

---用里氏替换的原则来思考

---从访问权限角度,保证父类出现的地方能用子类来替换

---如果子类访问权限变低了

---替换了也访问不了

 

注意这里加了个前缀

符合里氏替换

因为并不是所有的子类都符合里氏替换的

或者说

从一个业务角度看上去符合里氏替换的子类

从另外一个角度看上去并不符合 

这有点绕口

需要例子说明

 

比如

鸵鸟是不是鸟这个业务继承关系

鸵鸟到底是不是鸟的子类呢?

从两个业务角度来看

1,从生物学角度来看

鸵鸟符合所有鸟类的生物学特征

所以

鸵鸟是鸟

2,从飞行这个业务角度来说

鸟都能飞行

鸵鸟不能飞行

所以

鸵鸟非鸟

 

那么到底鸵鸟是不是鸟呢

从上面两个角度我们看到

关键还是要看业务环境

如果从飞行上来说

鸵鸟和鸟是不符合里氏替换的

 

如果从飞行上让鸵鸟继承鸟

假定父类鸟定义了飞行速度

子类鸵鸟继承飞行速度并置为0,因为不会飞行

现在假设有业务环境,给定距离,计算各种鸟类的飞行时间

用数学式是: 飞行时间=距离/飞行速度

业务端得到鸵鸟这只鸟的实例

并且调用它的速度进行计算

现在问题来了,鸵鸟飞行速度为零,造成除零bug

对于业务来说这会是个很诡异的问题

因为数学式没有问题

为什么这只鸟会出bug呢?

因为在飞行这个业务角度看来飞行速度不能为零

而我们为了把鸵鸟归为鸟类强制速度为零

这和飞行这个业务规则是冲突的

把一个不会飞行的鸵鸟强行塞到飞行的继承体系

将会造成各种莫名其妙的问题

 

还有个正方形非长方形的例子

学过数学我们都知道

正方形是特殊的长方形

怎么到这里就说正方形非长方形了呢

我们只能说

环境决定一切

 

正方形为什么不是长方形呢

因为正方形和长方形的长度设置规则不一样

长方形设置长和设置宽是独立的

对于正方形来说

长是宽

宽是长

设置长的时候就要设置宽

那么在考虑长宽设置规则的时候

用正方形来替代长方形就会出现问题

比如设置长宽数值到一个规则的时候触发事件

因为正方形永远长宽同步

你不知道会触发什么

 

怎么确认需要里氏替换

或者什么地方违背了里氏替换呢

 

因为里氏替换说的是父子类的类型替换

所以

如果出现了在继承链上的 is-a 的判断逻辑

那可能就是违背了里氏替换原则

比如这个:

if (animal instanceof Cat ){
//do cat action
} if (animal instanceof Dog ){
//do dog action
}

比如这个:

if(action.Equals(“add”)){
  //do add action
}
else if(action.Equals(“view”)){
  //do view action
}

 

接下来的问题就是

发生这种情况的时候

可以做些什么呢

 

// TODO:

可以做些什么呢

 

对于网上流传的里氏变换规范的四个含义,这里稍微理解一下:

  • 一,子类必须完全实现父类的方法,如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承。
  • 二,子类可以有自己的个性,这里表示父类向子类的转变是不安全的
  • 三,重载父类的方法时输入参数可以被放大,这个请查看 面向对象之 继承关系中的关系,这一条在很多情况中都是不成立的。简单说明一下:
  •     0,首先,按照里氏替换,在父类出现的地方,替换成子类,那么在C#中和Java中的表现分别是:
  •     1,在C#中呢,先从当前类(这里是子类,因为已经里氏替换了)寻找有没有可以兼容的方法,没找到再从父类中寻找。所以是子类优先,所以除非子类中的入参是实际参数的子代类,否则总是调用子类的方法。比如实际参数是TSon,父类入参类型是TSon,子类入参类型是TFather,首先从子类找,TSon -> TFahter,这是安全的转变,所以调用子类的方法,这跟里氏替换相悖了,因为替换后的行为从父类转到了子类,而且没有发生重写 - 很多地方都说是重写方法(比如引用三),应该是重载,这里纠正一下。 另外如果实际参数是TSon,父类入参是TFather,子类入参是TSon,首先从子类找,发现TSon -> TSon,于是调用子类的方法。如果实际参数是TSon,父类入参是TFather,子类入参是TGrandson,首先从子类找,发现TSon -> TGrandson是不安全的,所以从父类找,发现TSon -> TFather是安全的,于是调用父类的方法。
  •     2,在Java中呢,直接在整个对象的方法表中寻找兼容的方法,所以你也没办法确定会选择父类还是子类的,比如实际参数是TSon,父类入参是TSon,子类入参是TFather,那么会选择父类的方法;如果实际参数是TSon,父类入参是TFahter,子类入参是TSon,那么会选择子类的。如果实际参数是TSon,父类入参是TSon,子类入参是TGrandson,那么会选择父类的方法。
  •     3,我们看到,#1,#2这两种调用根本没有规律,因为从继承规范上来说,子类重载父类(而不是重写或者隐藏父类的方法)这种跨类的重载不是正规操作,也就是说,你可以这么写,语法没问题,但是我不确保你会发生什么,因为这都不是规范关心范围内的。
  • 四,重写父类的方法时输出结果可以被缩小,这个好理解,因为返回的是子类,然后赋值给一个父类,这种转型是安全的。

 

总结下里氏替换原则的好处:
     第一、保证系统或子系统有良好的扩展性。只有子类能够完全替换父类,才能保证系统或子系统在运行期内识别子类就可以了,因而使得系统或子系统有了良好的扩展性。
     第二、实现运行期内绑定,即保证了面向对象多态性的顺利进行。这节省了大量的代码重复或冗余。避免了类似instanceof这样的语句,或者getClass()这样的语句,这些语句是面向对象所忌讳的。
     第三、有利于实现契约式编程。契约式编程有利于系统的分析和设计,指我们在分析和设计的时候,定义好系统的接口,然后再编码的时候实现这些接口即可。在父类里定义好子类需要实现的功能,而子类只要实现这些功能即可。

 

对应里氏替换的设计模式有不少

策略模式,在实际业务中,使用各种子类算法替换父类算法

合成模式,在层次业务中,简单元素和复合元素继承同一个父类,简单元素和复合元素可以出现在父类出现的地方形成树状结构

代理模式,代理类和实际类继承同一个父类,父类出现的地方可以用实际类和代理类。新增代理类的更多因素在于控制实际类的使用,比如缓存实际类,比如更多数据控制

 

参考:

1,里氏替换原则

2,里氏替换原则

3,覆盖或实现父类的方法时输入参数可以被放大

4,爱恨纠葛的父子关系

5,由模式谈面向对象的基本原则Liskov替换原则

 

 

转载于:https://www.cnblogs.com/tirestay/p/3734462.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值