[设计模式]<<设计模式之禅>>关于里氏替换原则
在面向对象的语言中,继承是必不可少的、非常优秀的语言机制,它有如下优点:
● 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
● 提高代码的重用性;
● 子类可以形似父类,但又异于父类,“龙生龙,凤生凤,老鼠生来会打洞”是说子拥有
父的“种”,“世界上没有两片完全相同的叶子”是指明子与父的不同;
● 提高代码的可扩展性,实现父类的方法就可以“为所欲为”了,君不见很多开源框架的扩展接口都是通过继承父类来完成的;
● 提高产品或项目的开放性。
自然界的所有事物都是优点和缺点并存的,即使是鸡蛋,有时候也能挑出骨头来,继承的缺点如下:
● 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;
● 降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束;
● 增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大段的代码需要重构。
Java使用extends关键字来实现继承,它采用了单一继承的规则,C++则采用了多重继承的规则,一个子类可以继承多个父类。从整体上来看,利大于弊,怎么才能让“利”的因素发
挥最大的作用,同时减少“弊”带来的麻烦呢?解决方案是引入里氏替换原则(LiskovSubstitution Principle,LSP),什么是里氏替换原则呢?它有两种定义:
第二种定义:Functions that use pointers or references to base classes must be able to useobjects of derived classes without knowing it.(所有引用基类的地方必须能透明地使用其子类的对象。
第二个定义是最清晰明确的,通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。
里氏替换原则为良好的继承定义了一个规范,一句简单的定义包含了4层含义。
1.子类必须完全实现父类的方法
2.子类可以有自己的个性
3.覆盖或实现父类的方法时输入参数可以被放大
4. 覆写或实现父类的方法时输出结果可以被缩小
下面对每一层含义进行介绍:
1.子类必须完全实现父类的方法
我们举个例子来说明这个原则,大家都打过CS吧,非常经典的FPS类游戏,我们来描述一下里面用到的枪,类图如下所示:
枪的主要职责是射击,如何射击在各个具体的子类中定义,手1枪是单发射程比较近,步枪威力大射程远,机枪用于扫射。在士兵类中定义了一个方法killEnemy,使用枪来杀敌人,
具体使用什么枪来杀敌人,调用的时候才知道,AbstractGun类的源程序如代码如下:
public abstract class AbstractGun {
//枪用来干什么的?杀敌!
public abstract void shoot();
} 手1枪、步枪、机枪的实现类如代码如下:
手1枪、步枪、机枪的实现类
public class Handgun extends AbstractGun {
//手1枪的特点是携带方便,射程短
@Override
public void shoot() {
System.out.println("手1枪射击...");
}
}
public class Rifle extends AbstractGun{
//步枪的特点是射程远,威力大
public void shoot(){
System.out.println("步枪射击...");
}
}
public class MachineGun 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();
}
}
注意粗体部分,定义士兵使用枪来杀敌,但是这把枪是抽象的,具体是手1枪还是步枪需要在上战场前(也就是场景中)前通过setGun方法确定。场景类Client的源代码如代码如下:
public class Client {
public static void main(String[] args) {
//产生三毛这个士兵
Soldier sanMao = new Soldier();
//给三毛一支枪
sanMao.setGun(new Rifle());
sanMao.killEnemy();
}
}
程序输出:
士兵开始杀敌人...
步枪射击...
在这个程序中,我们给三毛这个士兵一把步枪,然后就开始杀敌了。如果三毛要使用机枪,当然也可以,直接把sanMao.setGun(new Rifle())修改为sanMao.setGun(new MachineGun())
即可,在编写程序时Solider士兵类根本就不用知道是哪个型号的枪(子类)被传入。
注意 在类中调用其他类时务必要使用父类或接口,如果不能使用父类或接口,则说明类的设计已经违背了LSP原则。
我们再来想一想,如果我们有一个玩具手1枪,该如何定义呢?我们先在类图上增加一个类ToyGun,然后继承于AbstractGun类,修改后的类图如下所示:
首先我们想,玩具枪是不能用来射击的,杀不死人的,这个不应该写在shoot方法中。新增加的ToyGun的源代码如代码所示:
public class ToyGun extends AbstractGun {
//玩具枪是不能射击的,但是编译器又要求实现这个方法,怎么办?虚构一个呗!
@Override
public void shoot() {
//玩具枪不能射击,这个方法就不实现了
}
}
由于引入了新的子类,场景类中也使用了该类,Client稍作修改,源代码如代码清单2-6所示。
public class Client {
public static void main(String[] args) {
//产生三毛这个士兵
Soldier sanMao = new Soldier();
sanMao.setGun(new ToyGun());
sanMao.killEnemy();
}
}
修改了粗体部分,把玩具枪传递给三毛用来杀敌,代码运行结果如下所示:
士兵开始杀敌人...
坏了,士兵拿着玩具枪来杀敌人,射不出子弹呀!如果在CS游戏中有这种事情发生,那你就等着被人爆头吧,然后看着自己凄惨地倒地。在这种情况下,我们发现业务调用类已
经出现了问题,正常的业务逻辑已经不能运行,那怎么办?好办,有两种解决办法:
● 在Soldier类中增加instanceof的判断,如果是玩具枪,就不用来杀敌人。这个方法可以解决问题,但是你要知道,在程序中,每增加一个类,所有与这个父类有关系的类都必须修
改,你觉得可行吗?如果你的产品出现了这个问题,因为修正了这样一个Bug,就要求所有与这个父类有关系的类都增加一个判断,客户非跳起来跟你干架不可!你还想要客户忠诚于
你吗?显然,这个方案被否定了。
● ToyGun脱离继承,建立一个独立的父类,为了实现代码复用,可以与AbastractGun建立关联委托关系,如下图:
例如,可以在AbstractToy中声明将声音、形状都委托给AbstractGun处理,仿真枪嘛,形状和声音都要和真实的枪一样了,然后两个基类下的子类自由延展,互不影响。
在Java的基础知识中都会讲到继承,Java的三大特征嘛,封装、继承、多态。继承就是告诉你拥有父类的方法和属性,然后你就可以重写父类的方法。按照继承原则,我们上面的
玩具枪继承AbstractGun是绝对没有问题的,玩具枪也是枪嘛,但是在具体应用场景中就要考虑下面这个问题了:子类是否能够完整地实现父类的业务,否则就会出现像上面的拿枪杀敌
人时却发现是把玩具枪的笑话。
注意 如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承
2.子类可以有自己的个性
子类当然可以有自己的行为和外观了,也就是方法和属性,那这里为什么要再提呢?是因为里氏替换原则可以正着用,但是不能反过来用。在子类出现的地方,父类未必就可以胜
任。还是以刚才的关于枪支的例子为例,步枪有几个比较“响亮”的型号,比如AK47、AUG狙击步枪等,把这两个型号的枪引入后的Rifle子类图如下所示:
很简单,AUG继承了Rifle类,狙击手(Snipper)则直接使用AUG狙击步枪,源代码如代码清单如下所示:
public class AUG extends Rifle {
//狙击枪都携带一个精准的望远镜
public void zoomOut(){
System.out.println("通过望远镜察看敌人...");
}
public void shoot(){
System.out.println("AUG射击...");
}
}
有狙击枪就有狙击手,狙击手类的源代码如下所示:
public class Snipper {
public void killEnemy(AUG aug) {
// 首先看看敌人的情况,别杀死敌人,自己也被人干掉
aug.zoomOut();
// 开始射击
aug.shoot();
}
}
狙击手,为什么叫Snipper?Snipe翻译过来就是鹬,就是“鹬蚌相争,渔人得利”中的那只鸟,英国贵族到印度打猎,发现这个鹬很聪明,人一靠近就飞走了,没办法就开始伪装、
远程精准射击,于是乎Snipper就诞生了。
狙击手使用AUG杀死敌人,代码如下:
public class Client {
public static void main(String[] args) {
// 产生三毛这个狙击手
Snipper sanMao = new Snipper();
sanMao.setRifle(new AUG());
sanMao.killEnemy();
}
}
运行结果:
通过望远镜察看敌人...
AUG射击...
在这里,系统直接调用了子类,狙击手是很依赖枪支的,别说换一个型号的枪了,就是换一个同型号的枪也会影响射击,所以这里就直接把子类传递了进来。这个时候,我们能不
能直接使用父类传递进来呢?修改一下Client类,如下所示:
使用父类Rifle作为参数:
public class Client {
public static void main(String[] args) {
// 产生三毛这个狙击手
Snipper sanMao = new Snipper();
sanMao.setRifle((AUG) (new Rifle()));
sanMao.killEnemy();
}
}
显示是不行的,会在运行期抛出java.lang.ClassCastException异常,这也是大家经常说的向下转型(downcast)是不安全的,从里氏替换原则来看,就是有子类出现的地方父类未必
就可以出现。
3.覆盖或实现父类的方法时输入参数可以被放大
方法中的输入参数称为前置条件,这是什么意思呢?大家做过Web Service开发就应该知道有一个“契约优先”的原则,也就是先定义出WSDL接口,制定好双方的开发协议,然后再
各自实现。里氏替换原则也要求制定一个契约,就是父类或接口,这种设计方法也叫做Design by Contract(契约设计),与里氏替换原则有着异曲同工之妙。契约制定了,也就同
时制定了前置条件和后置条件,前置条件就是你要让我执行,就必须满足我的条件;后置条件就是我执行完了需要反馈,标准是什么。这个比较难理解,我们来看一个例子,我们先定
义一个Father类,代码如下所示:
Father.java
import java.util.Collection;
import java.util.HashMap; public class Father {
public Collection doSomething(HashMap hashmap) {
System.out.println("父类被执行");
return hashmap.values();
}
}
这个类非常简单,就是把HashMap转换为Collection集合类型,然后再定义一个子类,源代码如下所示。
Son.java
import java.util.Collection;
import java.util.Map; public class Son extends Father {
public Collection doSomething(Map map) {
System.out.println("子类被执行了");
return map.values();
}
}
请注意粗体部分,与父类的方法名相同,但又不是覆写(Override)父类的方法。你加个@Override试试看,会报错的,为什么呢?方法名虽然相同,但方法的输入参数不同,就
不是覆写,那这是什么呢?是重载(Overload)!不用大惊小怪的,不在一个类就不能是重载了?继承是什么意思,子类拥有父类的所有属性和方法,方法名相同,输入参数类型又不
相同,当然是重载了。父类和子类都已经声明了,场景类的调用如代码如下所示:
Client.java
import java.util.HashMap; public class Client {
private static void invoker() {
Father f = new Father();
HashMap map = new HashMap();
f.doSomething(hashmap); }
public static void main(String[] args) {
invoker();
}
}
上面代码的输出结果是:
父类被执行....
根据里氏替换原则,父类出现的地方子类就可以出现,我们把上面的粗体部分修改为子类,如代码如下所示:
public class Client {
public static void invoker(){
//有父类的地方就有子类
Father f= new Father(); //这种情况下输出:"父类被执行了"
//Son f =new Son();//这种情况下也是输出:"父类被执行了"
HashMap map = new HashMap();
f.doSomething(map);
} public static void main(String[] args) {
invoker();
}
}
运行结果还是一样,看明白是怎么回事了吗?父类方法的输入参数是HashMap类型,子类的输入参数是Map类型,也就是说子类的输入参数类型的范围扩大了,子类代替父类传递
到调用者中,子类的方法永远都不会被执行。这是正确的,如果你想让子类的方法运行,就必须覆写父类的方法。大家可以这样想,在一个Invoker类中关联了一个父类,调用了一个父
类的方法,子类可以覆写这个方法,也可以重载这个方法,前提是要扩大这个前置条件,就是输入参数的类型宽于父类的类型覆盖范围。这样说可能比较难理解,我们再反过来想一
下,如果Father类的输入参数类型宽于子类的输入参数类型,会出现什么问题呢?会出现父类存在的地方,子类就未必可以存在,因为一旦把子类作为参数传入,调用者就很可能进入
子类的方法范畴。我们把上面的例子修改一下,扩大父类的前置条件,源代码如代码如下所示:
父类的钳子条件较大:
Father.java
public class Father { public Collection doSomething(Map map){
System.out.println("父类被执行...");
return map.values(); }
}
把父类的前置条件修改为Map类型,我们再修改一下子类方法的输入参数,相对父类缩小输入参数的类型范围,也就是缩小前置条件,源代码如代码如下所示:
子类的前置条件较小:
Son.java
public class Son extends Father {
//缩小输入参数范围
public Collection doSomething(HashMap map){
System.out.println("子类被执行...");
return map.values();
}
}
在父类的前置条件大于子类的前置条件的情况下,业务场景的源代码如代码如下所示:
public class Client {
public static void invoker(){
//有父类的地方就有子类
//Father f= new Father(); //这种情况下输出:"父类被执行了"
Son f =new Son();//这种情况下也是输出:"子类被执行"
HashMap map = new HashMap();
f.doSomething(map);
} public static void main(String[] args) {
invoker();
}
}
完蛋了吧?!子类在没有覆写父类的方法的前提下,子类方法被执行了,这会引起业务逻辑混乱,因为在实际应用中父类一般都是抽象类,子类是实现类,你传递一个这样的实现
类就会“歪曲”了父类的意图,引起一堆意想不到的业务逻辑混乱,所以子类中方法的前置条件必须与超类中被覆写的方法的前置条件相同或者更宽松。
4. 覆写或实现父类的方法时输出结果可以被缩小
这是什么意思呢,父类的一个方法的返回值是一个类型T,子类的相同方法(重载或覆写)的返回值为S,那么里氏替换原则就要求S必须小于等于T,也就是说,要么S和T是同一
个类型,要么S是T的子类,为什么呢?分两种情况,如果是覆写,父类和子类的同名方法的输入参数是相同的,两个方法的范围值S小于等于T,这是覆写的要求,这才是重中之重,子
类覆写父类的方法,天经地义。如果是重载,则要求方法的输入参数类型或数量不相同,在里氏替换原则要求下,就是子类的输入参数宽于或等于父类的输入参数,也就是说你写的这
个方法是不会被调用的,参考上面讲的前置条件。
采用里氏替换原则的目的就是增强程序的健壮性,版本升级时也可以保持非常好的兼容性。即使增加子类,原有的子类还可以继续运行。在实际项目中,每个子类对应不同的业务
含义,使用父类作为参数,传递不同的子类完成不同的业务逻辑,非常完美!
在项目中,采用里氏替换原则时,尽量避免子类的“个性”,一旦子类有“个性”,这个子类和父类之间的关系就很难调和了,把子类当做父类使用,子类的“个性”被抹杀——委屈了
点;把子类单独作为一个业务来使用,则会让代码间的耦合关系变得扑朔迷离——缺乏类替换的标准。
[设计模式]<<设计模式之禅>>关于里氏替换原则的更多相关文章
- ZT 设计模式六大原则(2):里氏替换原则
设计模式六大原则(2):里氏替换原则 分类: 设计模式 2012-02-22 08:46 23330人阅读 评论(41) 收藏 举报 设计模式class扩展string编程2010 肯定有不少人跟我刚 ...
- "围观"设计模式(2)--里氏替换原则(LSP,Liskov Substitution Principle)
在面向对象的程序设计中.里氏替换原则(Liskov Substitution principle)是对子类型的特别定义.它由芭芭拉·利斯科夫(Barbara Liskov)在1987年在一次会议上名为 ...
- 北风设计模式课程---里氏替换原则(Liskov Substitution Principle)
北风设计模式课程---里氏替换原则(Liskov Substitution Principle) 一.总结 一句话总结: 当衍生类能够完全替代它们的基类时:(Liskov Substitution P ...
- Java设计模式(4:里氏替换原则和合成复用原则详解
一.里氏替换原则 如果说实现开闭原则的关键步骤就是抽象化,那么基类(父类)和子类的继承关系就是抽象化的具体实现,所以里氏替换原则就是对实现抽象化的具体步骤的规范.即:子类可以扩展基类(父类)的功能,但 ...
- 【设计模式六大原则2】里氏替换原则(Liskov Substitution Principle)
肯定有不少人跟我刚看到这项原则的时候一样,对这个原则的名字充满疑惑.其实原因就是这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的. 定义1:如果对 ...
- 设计模式值六大原则——里氏替换原则(LSP)
里氏替换原则(Liskov Substitution Principel)是解决继承带来的问题. 继承的优点: 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性: 提高代码的重用性: 子类 ...
- 设计模式六大原则(2):里氏替换原则(Liskov Substitution Principle)
肯定有不少人跟我刚看到这项原则的时候一样,对这个原则的名字充满疑惑.事实上原因就是这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的. 定义1:假设对每 ...
- 设计模式原则(2)--Liskov Substitution Principle(LSP)--里氏替换原则
1.定义: 所有引用基类(父类)的地方必须能透明地使用其子类的对象.这一原则与继承紧密相关.如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的 ...
- 设计模式之里氏替换原则(LSP)
在java等面向对象编程语言里面,我想继承性应该是一大特色吧!所以今天所要讲解的里氏替换原则主要是针对这一特性而提出来的,当我们定义对象的时候,尽量找出对象之间的相同点,然后将其抽象成基类对象.比如水 ...
随机推荐
- CodeForces 455C Civilization (并查集+树的直径)
Civilization 题目链接: http://acm.hust.edu.cn/vjudge/contest/121334#problem/B Description Andrew plays a ...
- How to include JavaScript file in JSF
In JSF 2.0, you can use <h:outputScript /> tag to render a HTML "script" element, an ...
- eclipse中如何设置tomcat启动时间
现象:在eclipse中启动tomcat总是提示“Server Tomcat v5.5 Server @ localhost was unable to start within 45 seconds ...
- 转载.net泛型理解说明
net泛型理解 泛型简介: 泛型(Generic Type)是.NET Framework2.0最强大的功能之一.泛型的主要思想是将算法与数据结构完全分离开,使得一次定义的算法能作用于多种数据结构,从 ...
- MVC神韵---你想在哪解脱!(十)
增加追加数据的方法和视图 现在我们将要在数据库中追加并保存一些数据.我们将要创建一个表单以及一些表单输入控件,用来输入数据信息.当用户提交表单时将把这些用户输入的信息保存在数据库中.我们可以通过在浏览 ...
- sgu - 274 - Spam-filter
题意:判断一个字符串是不是有效的邮件地址. 题目链接:http://acm.sgu.ru/problem.php?contest=0&problem=274 ——>>照着题目做. ...
- securecrt 连接vmware ubuntu
折腾了好几天,我只想说shit,吃一堑长一智,和大家分享. SecureCRT连接Linux是使用Ubuntu下的SSH服务,ssh包括客户端和服务端即openssh-client,openssh-s ...
- java中匿名类的讲解
匿名内部类也就是没有名字的内部类 正因为没有名字,所以匿名内部类只能使用一次,它通常用来简化代码编写 但使用匿名内部类还有个前提条件:必须继承一个父类或实现一个接口 实例1:不使用匿名内部类来实现抽象 ...
- const 成员方法
1.使用场景 代码提供者告诉客户(代码使用者),这个方法不会修改对象的内容,请客户放心使用. 2.代码提供者,尝试在const成员方法中修改对象内容,编译报错.在成员方法中,this是const 指针 ...
- SAP CRM 最新简介文字(2007年、中英文)
以下内容是SAP CRM功能的精简描述,摘自SAP官方文档,附上中英文版本,可以对SAP CRM的主要功能有大致了解. 营销 - 使用营销资源管理.客户细分及列表管理.营销活动管理.线索管理.贸易促销 ...