设计模式之美-课程笔记7-设计原则3-里氏替换(LSP)
理论三:里式替换(LSP)跟多态有何区别?哪些代码违背了LSP?
如何理解里氏替换原则?
-
LSP: Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it.
- 这个原则来源于更早另一个哥们提出的 Liskov Substitution principle: If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking program.
-
子类对象能够替换程序中父类对象出现的地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。(那子类重写父类的方法,替换后不就不行了?多态?)
-
看个例子:父类 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) { // ...use httpClient to send 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(); //...省略设置request中数据值的代码... Response response = transporter.sendRequest(request); //...省略其他逻辑... } } // 里式替换原则 Demo demo = new Demo(); demo.demofunction(new SecurityTransporter(/*省略参数*/););
上述代码看起来不过是利用了多态,正如我在第二点中的疑惑一样。 但是多态只是面向对象的一个特性 能力,但是我们在设计子类的时候,要遵循里氏替换原则。
-
如果我们在SecurityTranspoter中sendRequest没有设置我们会抛出异常。
// 改造前: 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); } }
此时就违背了里氏替换原则,子类是无法替换父类方法的。因为原来的父类方法本身就没有appId和token,必然会报异常。这个程序的逻辑有了变化。
哪些代码明显违背了里氏替换原则
- LSP有另外一个更落地的描述:Design by contract.
- 子类在设计的时候,要遵循父类的行为约定。父类定义了函数的行为约定,那子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定。这里的行为约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。实际上,定义中父类和子类之间的关系,也可以替换成接口和实现类之间的关系。
1. 子类违背父类要实现的功能
比如父类有一个方法是sortOrdersByAmount()
,是按照金额从小到大来给订单排序的,儿子累充写这个函数后按照创建日期排序,那就是违背了LSP。
2. 子类违背父类对输入、输出、异常的约定
父类某个函数运行出错返回null,数据为空返回空集合;子类中重载之后运行出错返回异常,获取不到数据返回null。这也是违背LSP。
父类输入要求是整数,但是子类只允许正整数。对输入更加严格,也是违背了。
在父类的函数约定种,只会抛出ArgumentNullException
但是在子类中如果抛出其他异常的话也是违背LSP。
3. 子类违背父类注释中的说明
父类中定义的 withdraw() 提现函数的注释是这么写的:“用户的提现金额不得超过账户余额……”,而子类重写 withdraw() 函数之后,针对 VIP 账号实现了透支提现的功能,也就是提现金额可以大于账户余额,那这个子类的设计也是不符合里式替换原则的。
一个小方法:用子类的方法去跑父类的单测,没跑过的话可能说明有的子类违背了LSP。
LSP是用来指导继承中子类如何实现。