里氏替换原则(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 tradeFlow() {
List tradeList = new ArrayList();
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 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 tradeFlow() {
List tradeList = new ArrayList();
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图:
继承作为面向对象的重要特征,虽然给程序开发带来了非常大的便利,但也引入了一些弊端。继承的开发方式会给代码带来侵入性,可移植能力降低,类之间的耦合度较高。当对父类修改时,就要考虑一整套子类的实现是否有风险,测试成本较高。
里氏替换原则的目的是使用约定的方式,让使用继承后的代码具备良好的扩展性和兼容性。
使用了继承,就一定要遵从里氏替换原则,否则会让代码出现问题的概率变得更大。
在设计模式中体现里氏替换原则的有如下几个模式:
- 策略模式
- 组合模式
- 代理模式