里氏代换原则是什么意思
1 里氏替换原则(LSP)
如何理解“里氏替换原则”
里式替换原则的英文翻译是:Liskov Substitution Principle,缩写为 LSP。它的英文描述有两个版本分别为
1986 年由 Barbara Liskov 提出 If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program。 1996 年,Robert Martin 在他的 SOLID 原则中,重新描述了这个原则 Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it。
我们综合两者的描述,将这条原则用中文描述出来,是这样的:子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。
为了让你更好地理解这个原则,我举一个例子来进一步解释一下。 如下代码中,父类 Transporter 使用 org.apache.http 库中的 HttpClient 类来传输网络数据。子类 SecurityTransporter 继承父类 Transporter,增加了额外的功能,支持传输 appId 和 appToken 安全认证信息。
public class Transporter { private HttpClient httpClient; public Transporter(HttpClient httpClient) { this.httpClient = httpClient; } public Response sendRequest(Request request) { } } public class SecurityTransporter extends Transporter { private String appId; private String appToken; public SecurityTransporter(HttpClient httpClient, String appId, String appToken) { super(httpClient); this.appId = appId; this.appToken = appToken; } @Override public Response sendRequest(Request request) { if (StringUtils.isNotBlank(appId) && StringUtils.isNotBlank(appToken) ) { request.addPayload("app-id", appId); request.addPayload("app-token", appToken); } return super.sendRequest(request); } } public class Demo { public void demoFunction(Transporter transporter) { Reuqest request = new Request(); Response response = transporter.sendRequest(request); } } // 里式替换原则 Demo demo = new Demo(); demo.demofunction(new SecurityTransporter(...););
在上面的代码中,子类 SecurityTransporter 的设计完全符合里式替换原则,可以替换父类(Transporter)出现的任何位置,并且原来代码的逻辑行为不变且正确性也没有被破坏。
不过,只要你接触过面向对象的语言,你会发现,这不就是多态吗?多态和里式替换原则说的是不是一回事呢?从刚刚的例子和定义描述来看,里式替换原则跟多态看起来确实有点类似,但实际上它们完全是两回事。 让我们带着这个思考继续往下看吧!
我们还是通过刚才这个例子来解释一下。不过,我们需要对 SecurityTransporter 类中 sendRequest() 函数稍加改造一下。
改造前,如果 appId 或者 appToken 没有设置,我们就不做校验; 改造后,如果 appId 或者 appToken 没有设置,则直接抛出 NoAuthorizationRuntimeException 未授权异常。改造前后的代码对比如下所示:
// 改造前: public class SecurityTransporter extends Transporter { //...省略其他代码.. @Override public Response sendRequest(Request request) { if (StringUtils.isNotBlank(appId) && StringUtils.isNotBlank(appToken) ) { request.addPayload("app-id", appId); request.addPayload("app-token", appToken); } return super.sendRequest(request); } } // 改造后: public class SecurityTransporter extends Transporter { //...省略其他代码.. @Override public Response sendRequest(Request request) { if (StringUtils.isBlank(appId) || StringUtils.isBlank(appToken)) { throw new NoAuthorizationRuntimeException(...); } request.addPayload("app-id", appId); request.addPayload("app-token", appToken); return super.sendRequest(request); } }
在改造之后的代码中,如果传递进 demoFunction() 函数的是父类 Transporter 对象,那 demoFunction() 函数并不会有异常抛出,但如果传递给 demoFunction() 函数的是子类 SecurityTransporter 对象,那 demoFunction() 有可能会有异常抛出。尽管代码中抛出的是运行时异常(Runtime Exception),我们可以不在代码中显式地捕获处理,但子类替换父类传递进 demoFunction 函数之后,整个程序的逻辑行为有了改变。
很明显改造之后的代码仍然可以通过 Java 的多态语法,动态地用子类 SecurityTransporter 来替换父类 Transporter,也并不会导致程序编译或者运行报错。但从设计思路上来讲,SecurityTransporter 已经不符合里式替换原则了。
现在我们可以稍微得出一些结论。虽然从定义描述和代码实现上来看,多态和里式替换有点类似,但它们关注的角度是不一样的。
多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。 里式替换是一种设计原则,是用来指导继承关系中子类该如何设计的,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。
哪些代码违背了里氏替换原则?
实际上,里氏替换原则有个更容易理解,接地气的描述,那就是
按照协议来实现
具体解释一下:子类在设计的时候,要遵守父类的行为约定(或者叫协议)。父类定义了函数的行为约定,那子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定。这里的行为约定包括:
函数声明要实现的功能;对输入、输出、异常的约定 甚至包括注释中所罗列的任何特殊说明
实际上,定义中父类和子类之间的关系,也可以替换成接口和实现类之间的关系。
下面来看几个反例加深理解。
1. 子类违背父类声明要实现的功能
父类中提供的 sortOrdersByAmount() 订单排序函数,是按照金额从小到大来给订单排序的,而子类重写这个 sortOrdersByAmount() 订单排序函数之后,是按照下单时间来给订单排序。那子类的设计就违背里式替换原则。
2. 子类违背父类对输入、输出、异常的约定
输入: 在父类中,某个函数约定,输入数据可以是任意整数,但子类实现的时候,只允许输入数据是正整数,负数就报错,也就是说,子类对输入的数据的校验比父类更加严格,那子类的设计就违背了里式替换原则。
输出:在父类中,某个函数约定:运行出错的时候返回 null;获取数据为空的时候返回空集合。而子类重载函数之后,实现变了,运行出错抛异常,获取不到数据返回 null。那子类的设计就违背里式替换原则。
异常约定:在父类中,某个函数约定,只会抛出 ArgumentNullException 异常,那子类的设计实现中只允许抛出 ArgumentNullException 异常,任何其他异常的抛出,都会导致子类违背里式替换原则。
3. 子类违背父类注释中所罗列的任何特殊说明
父类中定义的 withdraw() 提现函数的注释是这么写的:“用户的提现金额不得超过账户余额……”,而子类重写 withdraw() 函数之后,针对 VIP 账号实现了透支提现的功能,也就是提现金额可以大于账户余额,那这个子类的设计也是不符合里式替换原则的。
小技巧
如果你的团队有编写单元测试的习惯那么拿父类的单元测试去验证子类的代码。如果某些单元测试运行失败,就有可能说明,子类的设计实现没有完全地遵守父类的约定,子类有可能违背了里式替换原则。
2 总结
多态和里氏替换原则的区别:多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法,它是一种代码实现的思路。而里式替换是一种设计原则,用来指导继承关系中子类该如何设计,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑及不破坏原有程序的正确性。
父类定义了函数的“约定”,那子类可以改变函数的内部实现逻辑,但不能改变函数原有的“约定”。约定包括:函数声明要实现的功能,对输入、输出、异常的约定甚至包括注释中所罗列的任何特殊说明。
本文地址:百科问答频道 https://www.neebe.cn/wenda/916629.html,易企推百科一个免费的知识分享平台,本站部分文章来网络分享,本着互联网分享的精神,如有涉及到您的权益,请联系我们删除,谢谢!