六大设计原则之里氏替换原则

里氏替换原则(Liskov Substitution Principle,LSP)是由麻省理工学院计算机科学系教授芭芭拉·利斯科夫(Barbara Liskov)于 1987 年在“面向对象技术的高峰会议”(OOPSLA)上发表的一篇文章《数据抽象和层次》(Data Abstractionand Hierarchy)里提出的,她提出:继承必须确保超类所拥有的性质在子类中仍然成立。

如果S是T的子类型,那么所有T类型的对象都可以在不破坏程序的情况下被S类型的对象替换。

简单来说,子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:当子类继承父类时,除添加新的方法且完成新增功能外,尽量不要重写父类的方法。这句话包括了四点含义(非常重要):

  • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
  • 子类可以增加自己特有的方法。
  • 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松。
  • 当子类的方法实现父类的方法(重写、重载或实现抽象方法)时,方法的后置条件(即方法的输出或返回值)要比父类的方法更严格或与父类的方法相等。

里氏替换原则的作用
·里氏替换原则是实现开闭原则的重要方式之一。
·解决了继承中重写父类造成的可复用性变差的问题。
·是动作正确性的保证,即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。
·加强程序的健壮性,同时变更时可以做到非常好的兼容性,提高程序的维护性、可扩展性,降低需求变更时引入的风险。

关于里氏替换的场景,最有名的就是“正方形不是长方形”。同时还有一些关于动物的例子,比如鸵鸟、企鹅都是鸟,但是却不能飞。这样的例子可以非常形象地帮助我们理解里氏替换中关于两个类的继承不能破坏原有特性的含义。

举例:
这里用不同种类的银行卡作为场景对象进行学习。储蓄卡和信用卡都具备一定的消费功能,但又有一些不同。例如信用卡不宜提现,如果提现可能会产生高额的利息。构建这样一个模拟场景,假设在构建银行系统时,储蓄卡是第一个类,信用卡是第二个类。为了让信用卡可以使用储蓄卡的一些方法,选择由信用卡类继承储蓄卡类
违背原则的方案:
储蓄卡类

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
public class CashCard {

    /**提现*/
    public String withdrawal(String orderId, BigDecimal amount){
        System.out.println("提现成功,单号:"+orderId+", 金额:"+amount);
        return "00001";
    }

    /**储蓄*/
    public String recharge(String orderId, BigDecimal amount){
        System.out.println("储蓄成功,单号:"+orderId+", 金额:"+amount);
        return "00001";
    }

    /**
     * 查询交易流水
     */
    public List<String> tradeFlow() {
        List<String> tradeList = new ArrayList<String>();
        tradeList.add("orderid:103423,amount:100000");
        tradeList.add("orderid:103425,amount:150000");
        tradeList.add("orderid:103428,amount:5050000");
        return tradeList;
    }
}

信用卡类:

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

public class CreditCard extends CashCard{

    /**提现*/
    @Override
    public String withdrawal(String orderId, BigDecimal amount){
        //信用卡对于提现有不同的逻辑
        if(amount.compareTo(new BigDecimal(2000)) >= 0){
            System.out.println("贷款金额校验(限额1000),单号:"+orderId+",金额:"+amount);
            return "4582342";
        }
        System.out.println("生成贷款单,单号:"+orderId+",金额:"+amount);
        System.out.println("贷款成功,单号:"+orderId+",金额:"+amount);
        return "00001";
    }

    /**信息卡储蓄,相当于还款,逻辑也完全不一样*/
    @Override
    public String recharge(String orderId, BigDecimal amount){
        System.out.println("生成还款单,单号:"+orderId+", 金额:"+amount);
        System.out.println("还款成功,单号:"+orderId+", 金额:"+amount);
        return "00001";
    }

    /**
     * 查询交易流水,可以直接使用父类逻辑
     */
    public List<String> tradeFlow() {
       return super.tradeFlow();
    }

}

信用卡的功能实现是在继承了储蓄卡类后,进行方法重写:支付withdrawal()、还款recharge()。其实交易流水可以复用,也可以不用重写这个类。这种继承父类方式的优点是复用了父类的核心功能逻辑,但是也破坏了原有的方法。此时继承父类实现的信用卡类并不满足里氏替换原则,也就是说,此时的子类不能承担原父类的功能,直接当作储蓄卡使用。

里氏替换原则改善代码:
储蓄卡和信用卡在功能使用上有些许类似,在实际的开发过程中也有很多共同可复用的属性及逻辑。实现这样的类的最好方式是提取出一个抽象类,由抽象类定义所有卡的共用核心属性、逻辑,把卡的支付和还款等动作抽象成正向和逆向操作。
抽象出银行卡类:

public abstract class BankCard {

    private String cardNo; //银行卡都有卡号属性
    private String createDate; //银行卡都有开卡时间

    public BankCard(String cardNo, String createDate){
        this.cardNo = cardNo;
        this.createDate = createDate;
    }

    protected abstract  boolean rule(BigDecimal amount);

    //正向入账,加钱
    public String positive(String orderId, BigDecimal amount){
        System.out.println("入款成功,卡号:"+cardNo+",单号:"+orderId+", 金额:"+amount);
        return "00001";
    }

    //逆向入账,减钱
    public String negative(String orderId, BigDecimal amount){
        System.out.println("出款成功,卡号:"+cardNo+",单号:"+orderId+", 金额:"+amount);
        return "00001";
    }

    /**
     * 查询交易流水
     */
    public List<String> tradeFlow() {
        List<String> tradeList = new ArrayList<String>();
        tradeList.add("orderid:103423,amount:100000");
        tradeList.add("orderid:103425,amount:150000");
        tradeList.add("orderid:103428,amount:5050000");
        return tradeList;
    }

    public String getCardNo() {
        return cardNo;
    }

    public String getCreateDate() {
        return createDate;
    }
}

储蓄卡类实现:

import java.math.BigDecimal;

public class CashCard extends BankCard {

    public CashCard(String cardNo, String createDate){
        super(cardNo,createDate);
    }

    /**规则过滤,储蓄卡默认通过*/
    @Override
    protected boolean rule(BigDecimal amount) {
        return true;
    }

    /**提现*/
    public String withdrawal(String orderId, BigDecimal amount){
        System.out.println("提现成功,单号:"+orderId+", 金额:"+amount);
        return super.negative(orderId,amount);
    }

    /**储蓄*/
    public String recharge(String orderId, BigDecimal amount){
        System.out.println("储蓄成功,单号:"+orderId+", 金额:"+amount);
        return super.positive(orderId,amount);
    }

    public boolean risk(String cardNo,String orderId,BigDecimal amount){
        System.out.println("风险检测,卡号:"+cardNo+",单号:"+orderId+",金额:"+amount);
        return true;
    }
}

储蓄卡类中继承抽象银行卡父类 BankCard,实现的核心功能包括规则过滤rule、提现withdrawal、储蓄recharge和新增的扩展方法,即风控校验 checkRisk。这样的实现方式满足了里氏替换的基本原则,既实现抽象类的抽象方法,又没有破坏父类中的原有方法。

信用卡类实现:

import java.math.BigDecimal;

public class CreditCard extends CashCard{

    public CreditCard(String cardNo, String orderId){
        super(cardNo,orderId);
    }

    boolean rule2(BigDecimal amount){
        return amount.compareTo(new BigDecimal(2000)) <= 0;
    }

    /**
     * 贷款,信用卡提现
     */
    public String loan(String orderId, BigDecimal amount){
        boolean rule = rule2(amount);
        if(!rule){
            System.out.println("贷款失败!!单号:"+orderId+",金额:"+amount);
            return "00002";
        }
        System.out.println("生成贷款单,单号:"+orderId+",金额:"+amount);
        System.out.println("贷款成功,单号:"+orderId+",金额:"+amount);
        return super.negative(orderId,amount);
    }

    /**
     * 还款,信用卡还款
     */
    public String repayment(String orderId, BigDecimal amount){
        System.out.println("生成还款单,单号:"+orderId+",金额:"+amount);
        System.out.println("还款成功,单号:"+orderId+",金额:"+amount);
        return  super.positive(orderId,amount);
    }
}

信用卡类在继承父类后,使用了公用的属性,即卡号 cardNo、开卡时间createDate,同时新增了符合信用卡功能的新方法,即贷款loan、还款repayment,并在两个方法中都使用了抽象类的核心功能。关于储蓄卡中的规则校验方法,新增了自己的规则方法 rule2,并没有破坏储蓄卡中的校验方法。子类随时可以替代储蓄卡类。信用卡类具备储蓄卡的所有功能
UML图:


LSP.jpg

继承作为面向对象的重要特征,虽然给程序开发带来了非常大的便利,但也引入了一些弊端。继承的开发方式会给代码带来侵入性,可移植能力降低,类之间的耦合度较高。当对父类修改时,就要考虑一整套子类的实现是否有风险,测试成本较高。

里氏替换原则的目的是使用约定的方式,让使用继承后的代码具备良好的扩展性和兼容性。

使用了继承,就一定要遵从里氏替换原则,否则会让代码出现问题的概率变得更大。

在设计模式中体现里氏替换原则的有如下几个模式:

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

推荐阅读更多精彩内容