里氏替换原则
转载请注明出处!!!http://blog.csdn.net/zhonghuan1992 ???????? 所有配套代码均在github上:https://github.com/ZHONGHuanGit/DesignPattern 跟着ZHONGHuan学习设计模式里氏替换原则? ? ? ? ?这节中我们会聊聊里氏替换原则,聊它之前,我们先看看定义。 ???????? 定义:如果对每一个类型为T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。(摘自java与模式一书) ???????? 如果你觉得定义说的模糊了点,不太清楚,没关系,我们慢慢说明白。里氏替换原则的另一个简短的定义是“所有引用基类的地方必须能透明地使用其子类的对象”。这个可能更清楚点。如果你熟悉的掌握一门面向对象的语言,你应该都可以明白面向对象的继承,子类继承自父类的话,自然的就会继承父类的所有方法(当然前提是父类不要把方法声明为private)。???? 在面向对象中,继承有很多优点: ?????? 1代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性; ?????? 2提高代码的重用性; ?????? 3子类可以形似父类,但又异于父类,“龙生龙,凤生凤,老鼠生来会打洞”是说子拥有父的“种”,“世界上没有两片完全相同的叶子”是指明子与父的不同; ?????? 4提高代码的可扩展性,实现父类的方法就可以“为所欲为”了,君不见很多开源框架的扩展接口都是通过继承父类来完成的; ?????? 5提高产品或项目的开放性。 ?????? ?????? 而里氏替换原则希望,你在写一个类继承自原有的类的同时,尽量不要去更改原有的方法。话说那么多,最好的方式就是举个例子了。 ?????? 假设你要做一个枪战游戏,我们省去一些其它的细节,我们关注下,人物在开枪的时候的动作,大家应该玩过CF或CS,或者至少听过枪击类游戏。每个人物一般都有一把枪,所以,这里,我们让选手自己持有枪,也就是让枪称为选手的一个属性。请看下面的代码: ? 例子代码:
/* * 虚拟的抢,后面的枪不管如何实现的,都必须保证,shoot这个接口,实现的就是枪的射击,不同的枪的射击方式是不一样的,但是枪的射击理论上有一个共同的步骤 * 就是子弹出来,射向对方,具体如何我们不管,总之,子类在实现枪这个shoot这个射击方法的接口的时候,不能变成是装子弹,这个就违反了里氏替换原则了 * 这里是关键,就是射击的方法还是射击,虽然过程细节不一样,但还是射击 * 如果不明白为什么违反,那么请你再仔细思考下,还是不懂,留言吧。 * */ interface Gun{ public void shoot(); } //手枪实现Gun接口 class HandGun implements Gun{ public void shoot() { System.out.println("手枪射击"); } } //AK47实现Gun接口 class AK47 implements Gun{ public void shoot() { System.out.println("AK47射击"); } } //机枪实现Gun接口 class MachineGun implements Gun{ public void shoot() { System.out.println("机枪在装子弹");//这里就是违反了里氏替换原则原则 } } class Hero{ String name; Gun gun; public Hero(String name) { this.name=name; } public void setGun(Gun gun) { this.gun=gun; } public Gun getGun(Gun gun) { return gun; } public void shoot() { if(gun==null) { System.out.println("没抢啦,快去拿把枪吧"); return ; } gun.shoot(); } } public class Main{ public static void main(String[] args) { Hero hero=new Hero("神枪手"); //给枪手配上AK47 hero.setGun(new AK47()); hero.shoot(); //给枪手配上机枪 hero.setGun(new MachineGun()); hero.shoot();//本来想设计的,结果却变成了装子弹 //这个还是属于比较明显的问题 hero.setGun(new HandGun()); hero.shoot(); } } ?????? 上面的例子还是比较明显地违反了里氏替换原则,现在我们来看一下,感觉上可以这样设计,看上去没有问题,但是在某种情况下,会遇到问题的。 ?????? 来看一看现实与程序设计的矛盾。 ?????? 现实中,正方形是特殊的矩形,这个接触过数学的都不陌生了吧。所以,在设计中我们把正方形当做是矩形的子类,似乎是合理的(我们暂且这样,请继续看下求) ? ???????? 这样的设计会出什么问题呢? ?????? 让我们看代码示例吧! //普通的矩形 class Rectangle { int height,width; public int getWidth() { return width; } public void setWidth(int width) { this.width = width; } public int getHight() { return height; } public void setHeight(int height) { this.height = height; } } /* * 因为Square的长宽相同,用一个size来统一 所以改写了父类的方法,因为它们实际上都是对size进行修改 */ class Square extends Rectangle { int size; public int getWidth() { return size; } public void setWidth(int width) { this.size = width; } public int getHight() { return size; } public void setHeight(int height) { this.size = height; } public int getSize() { return size; } public void setSize(int size) { this.size = size; } } public class Main { /** * 这个方法主要作用就是调整长方形的长宽,让长方形的width调整至比height更长。 * 可是就是这样的方法,出问题了,因为如果穿进去的rec实际上是一个正方形的话,那么,程序会无限制的运行下去 * * @param rec * @return */ static void testRec(Rectangle rec) { while (rec.getWidth() <= rec.getHight())// { rec.setWidth(rec.getWidth()+1); } } public static void main(String[] args) { Rectangle rec = new Square(); rec.setWidth(5); rec.setHeight(6); testRec(rec);// 就是这里出现问题了,这个方法会无限的运行下去,不信的话,你试试就ok了 } } ?????? 为什么会出现这样的情况,就是因为子类Square在修改父类的setLength等方法时,看上去是没有问题,但是存在的潜在的问题就是面对testRec这样的方法,就会出错,这就是我们需要遵守里氏替换原则的原因之一,不能修改父类。但是不修改父类,我们怎么应对正方形和矩形之间的区别呢?解决方法之一就是抽象一个类(我们在这里称为四边形),正方形和矩形都继承自它,这样就可以解决问题了。 ?????? ?????? 之所以说问题解决了,是因为在运行的时候不会再出现刚刚那样的错误,这样的错误在编译的时候就被解决了。 ? ?????? 实例代码请参考github网址https://github.com/ZHONGHuanGit/DesignPattern (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |