6大设计原则-里氏替换原则

里氏替换原则

定义:所有引用基类的地方必须能透明地使用其子类的对象,通俗点讲,将父类替换为子类后,程序不会产生错误或异常。
里氏替换原则的定义包含了四层含义

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

可以通过父类来对子类进行限制,起到一定的规范子类的作用
举个栗子,描述cs中的枪,图2-1:

2-1

那这个类图就很明显的表述了里氏原则,士兵获得枪,可以是手枪,步枪,机枪,代码如下:

public  abstract class AbstractGun{
    //枪的方法,射击
    public abstract 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 MechinaGun extends AbstractGun {
    @Override
    public void shoot() {
        System.out.println("机枪射击");
    }
}

//士兵类
public class Soldier {
    private AbstractGun gun;

    public AbstractGun getGun() {
        return 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();
    }
}

运行结果:
士兵开始设计
步枪射击


这里父类可以用子类替换,并且不同子类的射击方式不一样,这不就是java的继承和多态的特性;
现在继续想一下,如果有一把玩具枪,怎么定义,现在类图2-1上增加一个类ToyGun,继承AbstractGun类,修改后的类图如图2-2:


2-2

代码如下:

public class ToyGun extends AbstractGun {
    @Override
    public void shoot() {
        System.out.println("这是一把玩具枪,打不死敌人。。。");
    }
}
public class Client {
    public static void main(String[] args) {
        Soldier sanmao = new Soldier();
        sanmao.setGun(new ToyGun ());
        sanmao.killEnemy();
    }
}

发现玩具枪无法击杀敌人,那就玩完了,只能等着被爆头。。。
感觉这个例子很牵强,哈哈
这个业务场景就是玩具枪不满足条件,但他又确实继承abstractGun类,这样可以将toyGun与abstractGun断开继承关系,建立一个独立的父类AbstractToy,让两个基类下的子类自由发展,互不影响,如图2-3:

2-3

注意:如果子类不能完整的实现父类方法,或者父类的么些方法在子类中发生"畸变",则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承
(这个好像在图2-3里面看不出什么来)

2子类可以有自己的个性.

子类当然可以有自己的方法和属性,由于这个原因,子类能胜任的地方,父类不一定能适用,例如,步枪有几个比较响亮的型号,比如ak47,aug狙击步枪等,类图2-4:


2-4
//ak47
public class Ak47 extends Rifle {
}
//aug
public class Aug extends Rifle {
    public boolean zoomOut(){
        System.out.println("瞄准敌人");
        return true;
    }
}
//狙击手
public class Snipper {
    private Aug aug;
    public Aug getAug() {
        return aug;
    }
    public void setAug(Aug aug) {
        this.aug = aug;
    }
 
    public void killEnemy() {
        if(aug.zoomOut()){
            aug.shoot();
        }
    }
}
public class Client {
    public static void main(String[] args) {
        Snipper sanmao = new Snipper();
        sanmao.setAug(new Aug());
        sanmao.killEnemy();
    }
}

其实这个我写代码的时候想这个snipper是不是应该继承soldier,狙击手也是士兵的一种,然后让他拥有狙击枪的属性,然后重写killEnemy的具体实现。但是做完后好像很混乱,如果用父类的setGun方法会导致报错,因为abstractGun没有瞄准的方法
而且向下转型是不安全的。

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

方法中的输入参数称为前置条件,就是你要让我执行,就必须满足我的条件,可以理解为执行方法必须按我要求的参数数量个类型传入,后置条件就是执行完了返回的类型return,这种方式也叫契约设计。

//父类
public class Father {
    public Collection doSomething(HashMap map){
        System.out.println("父类执行了");
        return map.values();
    }
}
//子类
public class Son extends Father{
    //注意看这是一个重载方法
    public Collection doSomething(Map map) {
        System.out.println("子类被执行了");
        return map.values();
    }
}
//测试
public class Client {
    public static void main(String[] args) {
        Father father = new Father();
        father.doSomething(new HashMap());
        Son son = new Son();
        son.doSomething(new HashMap());
    }
}

运行结果:
父类执行了
父类执行了

这就是我们要的效果,父类出现的地方,子类可以胜任,在没有覆写父类方法的情况下并不会改变父类的执行逻辑


现在改一下,让子类的方法前置条件范围小于父类方法的前置条件

//父类
public class Father {
    public Collection doSomething(Map map){
        System.out.println("父类执行了");
        return map.values();
    }
}
//子类
public class Son extends Father{
    //注意看这是一个重载方法
    public Collection doSomething(HashMap map) {
        System.out.println("子类被执行了");
        return map.values();
    }
}
//测试
public class Client {
    public static void main(String[] args) {
        Father father = new Father();
        father.doSomething(new HashMap());
        Son son = new Son();
        son.doSomething(new HashMap());
    }
}

运行结果:
父类执行了
子类执行了

咋一看好像没啥毛病,但是注意这个子类执行的方法并不是覆写父类的方法,它是重载方法被执行了,那么这个地方就有可能改变了父类的意图,父类一般是抽象方法,但是这里并不是实现了这个抽象方法,而是重载了一个方法,这有可能造成逻辑混乱,所以子类中方法的前置条件一定要与超类中被覆写的方法的范围相同或更大,(这个描述有点不准确,参数的范围改了是重载而不是覆写)


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

父类的一个方法返回值是类型T,那么子类覆写方法的返回值S,那么S必须小于等于T,也就是说,要么S和T是同一个类型要么S是T的子类。(原书中好像表述有点问题,返回值类型不同时,应该是只能是覆写,重载的定义是返回值相同,参数类型和数量可以不同

里氏替换原则应该是很好的阐述了多态这一特性,基本就是父类做参数,传递子类去对应不同的业务逻辑,非常完美。
内容来自《设计模式之禅》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容