重学Java设计模式
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.3 里氏替换原则

2.3.1 里氏替换原则定义

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

1.里氏替换原则

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

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

·子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。

·子类可以增加自己特有的方法。

·当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松。

·当子类的方法实现父类的方法(重写、重载或实现抽象方法)时,方法的后置条件(即方法的输出或返回值)要比父类的方法更严格或与父类的方法相等。

2.里氏替换原则的作用

·里氏替换原则是实现开闭原则的重要方式之一。

·解决了继承中重写父类造成的可复用性变差的问题。

·是动作正确性的保证,即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。

·加强程序的健壮性,同时变更时可以做到非常好的兼容性,提高程序的维护性、可扩展性,降低需求变更时引入的风险。

2.3.2 模拟场景

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

为了从真实的开发场景感受里氏替换原则,这里选择不同种类的银行卡作为场景对象进行学习。

我们会使用各种类型的银行卡,例如储蓄卡、信用卡,还有一些其他特性的银行卡。储蓄卡和信用卡都具备一定的消费功能,但又有一些不同。例如信用卡不宜提现,如果提现可能会产生高额的利息。

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

2.3.3 违背原则方案

储蓄卡和信用卡在使用功能上类似,都有支付、提现、还款、充值等功能,也有些许不同,例如支付,储蓄卡做的是账户扣款动作,信用卡做的是生成贷款单动作。下面这里模拟先有储蓄卡的类,之后继承这个类的基本功能,以实现信用卡的功能。

1.储蓄卡

在储蓄卡的功能实现中包括了三个方法:提现、储蓄、交易流水查询,这些是模拟储蓄卡的基本功能。接下来通过继承储蓄卡的功能,实现信用卡服务。

2.信用卡

信用卡的功能实现是在继承了储蓄卡类后,进行方法重写:支付withdrawal()、还款recharge()。其实交易流水可以复用,也可以不用重写这个类。

这种继承父类方式的优点是复用了父类的核心功能逻辑,但是也破坏了原有的方法。此时继承父类实现的信用卡类并不满足里氏替换原则,也就是说,此时的子类不能承担原父类的功能,直接给储蓄卡使用。

2.3.4 里氏替换原则改善代码

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

1.抽象银行卡类

在抽象银行卡类中,提供了基本的卡属性,包括卡号、开卡时间及三个核心方法。正向入账,加钱;逆向入账,减钱。当然,实际的业务开发抽象出来的逻辑会比模拟场景多一些。接下来继承这个抽象类,实现储蓄卡的功能逻辑。

2.储蓄卡类实现

储蓄卡类中继承抽象银行卡父类 BankCard,实现的核心功能包括规则过滤rule、提现withdrawal、储蓄recharge和新增的扩展方法,即风控校验 checkRisk。

这样的实现方式满足了里氏替换的基本原则,既实现抽象类的抽象方法,又没有破坏父类中的原有方法。接下来实现信用卡的功能,信用卡的功能可以继承于储蓄卡,也可以继承抽象银行卡父类。但无论哪种实现方式,都需要遵从里氏替换原则,不可以破坏父类原有的方法。

3.信用卡类实现

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

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

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

4.功能测试

(1)功能测试:储蓄卡。

(2)功能测试:信用卡。

(3)功能测试:信用卡替换储蓄卡。

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

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

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

在日常开发中使用继承的地方并不多,在有些公司的代码规范中也不会允许多层继承,尤其是一些核心服务的扩展。而继承多数用在系统架构初期定义好的逻辑上或抽象出的核心功能里。如果使用了继承,就一定要遵从里氏替换原则,否则会让代码出现问题的概率变得更大。