前言
我们学习了 SOLID 原则中的单一职责原则和开闭原则。今天,我们再来学习 SOLID 中的 “L” 对应的原则:里式替换原则。
整体上来讲,这个设计原则是比较简单、容易理解和掌握的。今天我主要通过几个反例,带你看看,哪些代码是违反里式替换原则的?我们该如何将它们改造成满足里式替换原则?除此之外,这条原则从定义上看起来,跟我们之前讲过的 “多态” 有点类似。
如何理解 “里式替换原则”?
里式替换原则的英文翻译是: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) {
// ...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(/* 省略参数 */););
在上面的代码中,子类 SecurityTransporter
的设计完全符合里式替换原则,可以替换父类出现的任何位置,并且原来代码的逻辑行为不变且正确性也没有被破坏。
不过,你可能会有这样的疑问,刚刚的代码设计不就是简单利用了面向对象的多态特性吗?多态和里式替换原则说的是不是一回事呢?从刚刚的例子和定义描述来看,里式替换原则跟多态看起来确实有点类似,但实际上它们完全是两回事。为什么这么说呢?
我们还是通过刚才这个例子来解释一下。不过,我们需要对 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
的设计是不符合里式替换原则的。
回顾
里式替换原则是用来指导,继承关系中子类该如何设计的一个原则。理解里式替换原则,最核心的就是理解 design by contract,按照协议来设计” 这几个字。父类定义了函数的 “约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数原有的 “约定”。
这里的约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。
更多java原创阅读:https://javawu.com