转载请注明出处!!!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