java设计模式6大设计原则之里氏替换原则

里氏替换原则

1.1 里氏替换原则定义

里氏替换原则由麻省理工学院计算机系教授芭芭拉•利斯科夫(Barbara Liskov)提出,她提出:继承必须保证超类所拥有的特性在子类中任然成立。

1.2 里氏替换原则

如果S是T的子类,那么S类型的对象在不破换程序的情况下都应该可以替换T类型的对象。
简单来说,就是子类可以扩展父类的功能,但是不能改变父类原有的功能。也就是说,子类在完成继承父类是除了添加新的方法和完成新的功能外,尽量不要重写父类非抽象方法,这句话的意思是:

  1. 子类可以实现父类抽象方法,但不能覆盖父类非抽象方法
  2. 子类可以新增自己特有的方法
  3. 当子类重载父类方法时,方法的输入参数要比父类方法更宽松
  4. 当子类的方法实现父类的方法(重写,重载或实现抽象方法)时,方法的输出参数要比父类的方法更严格或与父类的方法相等。

1.3 里氏替换原则的作用

  1. 里氏替换原则是实现开闭原则的总要方式之一。
  2. 解决了继承中子类重写父类造成的可复用性变差问题。
  3. 类的扩展不会给已有系统引入新的错误,降低了代码出错的可能。
  4. 加强程序的复用性,提高了代码的可维护性、可扩展性、降低需求更变时引入的风险。

2.1 模拟场景

通过不同种类的银行卡作为场景的模拟对象,我们会使用各种类型的银行卡,如信用卡、储蓄卡,还有一些其他类型的银行卡,信用卡和储蓄卡都具备一定的消费功能,但有有一些不同,例如信用卡不宜提现,如果体现会产生高额的利息。

下面构建一个这样的模拟场景,假设在构建银行系统时,储蓄卡是第一个类,信用卡是第二个类。为了让信用卡可以使用储蓄卡的一些功能,将有信用卡继承储蓄卡,讨论是否满足里氏替换原则的一些要点。

2.2 违背原则方案

储蓄卡和信用卡都有类似的功能,但又有所不同,如都有支付、体现、还款、充值等功能,但不同点是,例如支付,储蓄卡做的是账户扣款动作,信用卡则是生成贷款单动作。下面模拟现有储蓄卡类,之后继承这个类实现信用卡的功能。

2.2.1 储蓄卡
/**
 * 模拟储蓄卡功能
 */
public class CashCard {

    /**
     * 提现
     * @param orderId 单号
     * @param amount 金额
     * @return 状态码
     */
    public String withdrawal(String orderId, BigDecimal amount){
        //模拟支付成功
        System.out.printf("提现成功----->单号:%s  金额:%s",orderId,amount);
        return "0000";
    }

    /**
     * 储蓄
     * @param orderId 单号
     * @param amount 金额
     * @return 状态码
     */
    public String recharge(String orderId,BigDecimal amount){
        //模拟充值成功
        System.out.printf("储蓄成功----->单号:%s  金额:%s",orderId,amount);
        return "0000";
    }
}

储蓄卡主要实现了两个功能:充值、储蓄。这是模拟储蓄卡的基本功能。接下来通过继承储蓄卡实现信用卡功能。

2.2.2 信用卡

/**
 * 模拟信用卡功能
 */
public class CreditCard extends CashCard {
    @Override
    public String withdrawal(String orderId, BigDecimal amount) {
        //校验
        if (amount.compareTo(new BigDecimal(1000))>0){
            System.out.printf("贷款金额校验(限额1000元),单号:%s  金额:%s",orderId,amount);
            return "0001";
        }
        //模拟生成贷款单
        System.out.printf("生成贷款单,单号:%s  金额:%s",orderId,amount);
        //模拟支付成功
        System.out.printf("贷款成功,单号:%s  金额:%s",orderId,amount);
        return "0000";
    }

    @Override
    public String recharge(String orderId, BigDecimal amount) {
        //模拟生成还款单
        System.out.printf("生成还款单,单号:%s  金额:%s",orderId,amount);
        //模拟还款成功
        System.out.printf("还款成功,单号:%s  金额:%s",orderId,amount);
        return "0000";
    }
}

信用卡继承了储蓄卡后,重写了父类支付、还款方法。这种继承的有点是服用了父类的核心逻辑,但也破环了原有方法。此时继承的父类实现的信用卡类不满足里氏替换原则,也就是说此时的子类不能承担原父类的功能,直接给储蓄卡使用。

2.3 里氏替换原则改善代码

储蓄卡和信用卡在功能的使用上有些许类似,此时可以抽取出可复用的属性和核心逻辑提取出一个抽象类,由抽象类定义所有卡的公用的核心属性、逻辑,把卡的支付和还款等动作抽象成一个正向和逆向操作。

2.3.1 抽象银行卡类
/**
 * 银行卡抽象类
 */
public abstract class BankCard {
    private String cardNo; //卡号
    private String cardDate; //开卡时间

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

    abstract boolean rule(BigDecimal amount);

    //正向入账,加钱
    public String positive(String orderId, BigDecimal amount) {
        //入账成功,存款、还款
        System.out.printf("卡号%s 入款成功,单号:%s  金额:%s", cardNo, orderId, amount);
        return "0000";
    }

    //逆向入账,减钱
    public String negative(String orderId, BigDecimal amount) {
        //入账成功,存款、还款
        System.out.printf("卡号%s 出款成功,单号:%s  金额:%s", cardNo, orderId, amount);
        return "0000";
    }

    public String getCardNo() {
        return cardNo;
    }

    public String getCardDate() {
        return cardDate;
    }
}

在抽象银行卡类中,提供了基本的卡属性,包括卡号、开卡时间,以及正向入账,加钱;逆向入账减钱操作;接下来继承这个抽象类实现储蓄卡的功能逻辑。

2.3.2 储蓄卡类
/**
 * 模拟储蓄卡功能
 */
public class CashCard extends BankCard {
    
    public CashCard(String cardNo,String cardDate){
        super(cardNo,cardDate);
    }
    //过滤规则,储蓄卡直接返回true
    @Override
    boolean rule(BigDecimal amount) {
        return true;
    }

    /**
     * 提现
     * @param orderId 单号
     * @param amount 金额
     * @return 状态码
     */
    public String withdrawal(String orderId,BigDecimal amount){
        //模拟支付成功
        System.out.printf("提现成功----->单号:%s  金额:%s",orderId,amount);
        return super.negative(orderId,amount);
    }

    /**
     * 储蓄
     * @param orderId 单号
     * @param amount 金额
     * @return 状态码
     */
    public String recharge(String orderId,BigDecimal amount){
        //模拟充值成功
        System.out.printf("储蓄成功----->单号:%s  金额:%s",orderId,amount);
        return super.positive(orderId,amount);
    }

    /**
     * 风控校验
     * @param cardNo 卡号
     * @param orderId 单号
     * @param amount 金额
     * @return 状态码
     */
    public boolean checkRisk(String cardNo,String orderId,BigDecimal amount){
        System.out.printf("风控校验,卡号%s 单号:%s  金额:%s",cardNo,orderId,amount);
        return true;
    }
}

储蓄卡继承抽象银行卡BankCard类,实现核心功能包括规则过滤rule,提现withdrawal,储蓄recharge和新增的扩展方法,即风控校验checkRisk。
这样实现方式满足了里氏替换原则的基本原则,即实现了抽象类的抽象方法,有没有破环父类中的原有方法。接下来实现信用卡功能,信用卡功能可以继承储蓄卡也可以继承抽象银行卡类,无论使用哪种方式都需要遵从里氏替换原则,不可以破环父类原有的方法。

2.3.3 信用卡类
/**
 * 模拟信用卡功能
 */
public class CreditCard extends CashCard {

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

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

    /**
     * 提现,信用卡提现
     * @param orderId 单号
     * @param amount 金额
     * @return 状态码
     */
    public String load(String orderId, BigDecimal amount) {
        //校验
        if (!rule2(amount)){
            System.out.printf("生成贷款单失败,金额超限,单号:%s  金额:%s",orderId,amount);
            return "0001";
        }
        //模拟生成贷款单
        System.out.printf("生成贷款单,单号:%s  金额:%s",orderId,amount);
        //模拟支付成功
        System.out.printf("贷款成功,单号:%s  金额:%s",orderId,amount);
        return super.negative(orderId,amount);
    }

    /**
     * 还款,信用卡还款
     * @param orderId 单号
     * @param amount 金额
     * @return 状态码
     */
    public String repayment(String orderId, BigDecimal amount) {
        //模拟生成还款单
        System.out.printf("生成还款单,单号:%s  金额:%s",orderId,amount);
        //模拟还款成功
        System.out.printf("还款成功,单号:%s  金额:%s",orderId,amount);
        return super.positive(orderId,amount);
    }
}

信用卡在继承父类后,使用了公用的属性,即卡号、开卡时间,并新增了符合信用卡功能的新方法,即贷款load、还款repayment,并在两个方法中都使用了抽象类的核心功能。

另外,关于储蓄卡中的校验规则,新增了自己的rule2方法,并没有破环储蓄卡中的校验方法。

以上的实现方式都是在遵从里氏替换原则下完成的,子类随时可以替代储蓄卡类。

3 功能测试

3.1 功能测试:储蓄卡。
        CashCard cashCard=new CashCard(UUID.randomUUID().toString().substring(20),"2021-09-28");
        //提现
        cashCard.withdrawal("100001",new BigDecimal(100));
        //储蓄
        cashCard.recharge("100001",new BigDecimal(100));

		//测试结果
		提现成功----->单号:100001  金额:100 
		卡号a3c-31e3eef99b40 出款成功,单号:100001  金额:100 
		储蓄成功----->单号:100001  金额:100 
		卡号a3c-31e3eef99b40 入款成功,单号:100001  金额:100 
3.2 功能测试:信用卡。
        CreditCard creditCard=new CreditCard(UUID.randomUUID().toString().substring(20),"2021-09-28");
        //支付,贷款
        creditCard.load("10001",new BigDecimal(100));
        //还款
        creditCard.repayment("100001",new BigDecimal(100000));
		
		//测试结果
		生成贷款单,单号:10001  金额:100 
		贷款成功,单号:10001  金额:100 
		卡号2f0-2a7c70ae4c40 出款成功,单号:10001  金额:100 
		生成还款单,单号:100001  金额:100000 
		还款成功,单号:100001  金额:100000 
		卡号2f0-2a7c70ae4c40 入款成功,单号:100001  金额:100000 		
3.3 功能测试:信用卡替换储蓄卡
        CashCard creditCard=new CreditCard(UUID.randomUUID().toString().substring(20),"2021-09-28");
        //提现
        creditCard.withdrawal("100001",new BigDecimal(100));
        //储蓄
        creditCard.recharge("100001",new BigDecimal(100));

		//测试结果
		提现成功----->单号:100001  金额:100 
		卡号a70-13a07862bb8d 出款成功,单号:100001  金额:100 
		储蓄成功----->单号:100001  金额:100 
		卡号a70-13a07862bb8d 入款成功,单号:100001  金额:100 

通过以上测试结果可以看出,储蓄卡功能正常,继承储蓄卡的信用卡功能也是正常的。同时,原有储蓄卡类的功能也能有信用卡类支持,即 CashCard creditCard=new CreditCard(…);

继承作为面向对象的重要特性,虽然给程序带来很大的便利性,但也引入了一些弊端,继承的方式给代码带来侵入性,可移值能力降低,类之间的耦合度较高。

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

在日常开发的过程中使用继承的方式并不多,有些公司的规范中也不允许出现多层继承,尤其一些核心业务的扩展。如果使用继承,就一定要遵从里氏替换原则,否则会让代码出现问题的概率变大。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值