您的位置:首页 > 其它

设计模式笔记(二)设计六大原则之二--里氏替换原则

2017-11-06 23:56 417 查看
里氏替换原则

LiskoSubstitution Principle LSP

定义:

只要父类出现的地方,子类就可以出现,而且替换为子类也不会有任何错误或异常,使用者可能根本不需要知道是父类还是子类。但是反过来就不行了,有子类出现的地方,父类未必能够适应。

此原则包含了4个含义:

子类必须完全实现父类的方法

1.因为里氏替换要求,父类出现的地方子类一定能出现。所以在写方法的时候如果用父类的参数,子类的功能可以根据需要传入。这样既得到了代码的复用,又可以多功能的实现功能。

举个栗子:

/*
* 父类:枪
* 有一个抽象方法:杀敌
*/
public abstract class AbstractGun {
//枪用来干什么的?杀敌
public abstract void shoot();
}


/*
* 手枪的特点是携带方便,射程短
*/
public class Handgun extends AbstractGun{

@Override
public void shoot() {
System.out.println("手枪射击...");
}

}


/*
* 步枪的特点是射程远,威力大
*/
public class Rifle extends AbstractGun{

@Override
public void shoot() {
System.out.println("步枪射击...");
}

}


/*
* 使用枪支的士兵
*/
public class Soldier {
//定义士兵的枪支 - 这把枪具体是手枪还是步枪等,需要上战场前通过setGun确定
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();
}
}


士兵开始杀敌…

步枪射击…

即使这个时候我们不想用步枪了,想要用手枪,完全不用更改类设计得代码,只需要在使用的时候给士兵步枪即可:

//三毛用手枪杀敌
sanMao.setGun(new Handgun());
sanMao.killEnemy();


士兵开始杀敌…

手枪射击…

在类中调用其他类时务必要使用父类或接口,如果不能使用父类或接口,说明类的设计已经违背了LSP原则。

2.如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖,聚集,组合等关系代替继承。

举个栗子:

这个时候我们又有了玩具枪,想一想,玩具枪也是枪啊,理所当然我们又继承了AbstractGun

/*
* 玩具枪
*/
public class ToyGun extends AbstractGun{

//可是玩具枪并不能杀人啊。。。
@Override
public void shoot() {
System.out.println("玩具枪射击...");
}

}


这个时候如果三毛给的是玩具枪,哦哦

//三毛获得了玩具枪
sanMao.setGun(new ToyGun());
sanMao.killEnemy();


士兵开始杀敌…

玩具枪射击…

发现并没有达到该有的效果。

这个时候我们就需要重新设计一下类的关系。比如玩具枪的声音和形状模拟的枪支,但是它并不属于真枪的一种。

/*
* 我是玩具枪,不是射击杀敌的真枪
*/
public abstract class AbstractToyGun {
protected AbstractGun gun;

//可以射击,但是属于我自己的射击,杀不死人的哦
public abstract void shoot() ;

public void beauty() {
System.out.println("我模拟了真枪的外观...");
gun.beauty();
};

}


/*
* 父类:枪
* 有一个抽象方法:杀敌
*/
public abstract class AbstractGun {
//枪用来干什么的?杀敌
public abstract void shoot();

//外观
public abstract void beauty();
}


/*
* 手枪的特点是携带方便,射程短
*/
public class Handgun extends AbstractGun{

@Override
public void shoot() {
System.out.println("手枪射击...");
}

@Override
public void beauty() {
System.out.println("我有手枪外观...");
}

}


/*
* 步枪的特点是射程远,威力大
*/
public class Rifle extends AbstractGun{

@Override
public void shoot() {
System.out.println("步枪射击...");
}

//步枪
@Override
public void beauty() {
System.out.println("我有步枪外观...");
}

}


/*
* 手枪玩具枪
*/
public class ToyGun2 extends AbstractToyGun{

public ToyGun2(AbstractGun gun) {
super.gun = gun;
}

@Override
public void shoot() {
System.out.println("玩具枪射击...");
}

}


//我有一个玩具枪,是一个手枪样式的
ToyGun2 toyGun = new ToyGun2(new Handgun());
toyGun.beauty();


子类可以有自己的个性

但是子类出现的地方,父类未必就可以胜任。也就是常说的向下转型是不安全的。

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

因为加入父类的参数范围比子类大,那么很有可能调用时没有实现正确的方法。

举个栗子:

/*
* 之前说士兵可以有一把枪用来杀敌,士兵什么枪都可以有,手枪和步枪
* 这里假设一个普通的小小兵,只可以拿手枪,当然他拿刀什么的,这里只关注枪的部分
*/
public class GeneralPerson extends Soldier{

public void killEnemy(Handgun _gun) {
System.out.println("小兵开始杀敌...");
_gun.shoot();
}
}


/*
* 使用枪支的士兵
*/
public class Soldier {
//士兵杀敌
public void killEnemy(AbstractGun gun) {
System.out.println("士兵开始杀敌...");
gun.shoot();
}
}


//产生三毛这个士兵
Soldier sanMao = new Soldier();
//手枪杀敌
sanMao.killEnemy(new Handgun());

//产生四毛这个小兵
GeneralPerson siMao = new GeneralPerson();
//手枪杀敌
siMao.killEnemy(new Handgun());


士兵开始杀敌…

手枪射击…

小兵开始杀敌…

手枪射击…

我们发现执行了子类的方法,明明我们希望所有的士兵不管用什么枪支杀敌都用士兵这个方法,但是子类由于参数范围比父类的小,导致这个时候扭曲了父类的意图,达到了不一样的功能。

覆写或实现父类的方法时输出结果可以被缩小。

意思是说,父类的一个方法返回值是一个类型T,子类的相同方法(重写或覆写)的返回值是S,那么里氏替换原则要求s必须小于等于T。也就是说要么S和T是同一个类型,要么S是T的子类。

覆写的时候,父类和子类的同名方法的输入参数是相同的,两个方法的返回值S小于等于T。这是覆写的要求所在。

重载的话,则要求输入参数类型或数量不同,里氏替换中要求,子类的输入参数宽于或等于父类的输入参数,也就是说这个方法时不会被调用的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: