在程序设计领域, SOLID(单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)是由 Robert Martin 在21世纪早期 引入的记忆术首字母缩略字,指代了面向对象编程和面向对象设计的五个基本原则。
“SOLID”中的 L 指代了里氏替换原则,英文是 Liskov Substitution principle
。
在面向对象的程序设计中,里氏替换原则是对子类型的特别定义,它由 Barbara Liskov 在1987年在一次会议上名为“数据的抽象与层次”的演说中首先提出。内容可以描述为:“派生类(子类)对象可以在程序中代替其基类(超类)对象。”,原文为:
Barbara Liskov 与 Jeannette Wing 在1994年发表论文并提出以上的 Liskov代换原则。
在 1996 年,Robert Martin 在他的 SOLID 原则中,重新描述了这个原则,英文原话是这样的: FUNCTIONS THAT USE POINTERS OR 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)不变及正确性不被破坏。
如何理解里氏替换?
经过上面的描述,XDM可能还是会觉得有点懵,不知道什么是里氏替换原则,我们一起来看下例子:
public abstract class Action {
//...
public Response handle(Request request) {
//... handle request and response
}
}
public class CustomizeAction extends Action {
public Response handle(Request request) {
String userId = request.get("userId");
String token = request.get("token");
// create auth key
if(StringUtil.isNotEmpty(userId) && StringUtil.isNotEmpty(token)) {
// use userId and token create authKey
String authKey = ...;
request.put("authKey", authKey);
}
return super.handle(request);
}
}
public class ActionContext {
public void resolve(Action action, Request request) {
Response response = action.handle(request);
// do something on response...
}
}
public class Main {
public static void main(String[] args) {
Request request = createRequest(args);
Action action = ActionSupport.getAction(request);
ActionContext context = new ActionContext();
context.resolve(action, request);
}
}
在上面的代码中,子类 CustomizeAction 的设计完全符合里式替换原则,可以替换父类出现的任何位置,并且原来代码的逻辑行为不变且正确性也没有被破坏。
或许,你会有疑问,这不就是面对对象程序的多态吗?多态与里氏替换原则确实不是一回事,我们先带着这个疑问,继续往下看。
上面的例子,我们改造一下:
public class CustomizeAction extends Action {
public Response handle(Request request) {
String userId = request.get("userId");
String token = request.get("token");
if(StringUtil.isEmpty(userId) || StringUtil.isEmpty(token)) {
throw new UnAuthException();
}
// create auth key
// use userId and token create authKey
String authKey = ...;
request.put("authKey", authKey);
return super.handle(request);
}
}
如果 userId 或者 token 获取失败时,我们认为是没有进行权限认证。改造之前,不进行处理;改造之后,会向上层抛出 UnAuthException 异常。改造之后的代码,违反了里氏替换原则,因为向上层抛异常,上层并没有进行显式的异常捕获处理,子类 CustomizeAction 替换父类 Action 传递给方法 resolve 后,程序的逻辑行为与预期发生偏差。
上面的疑问,在这里做一下解答,为什么多态和里氏替换原则不是一回事呢?
多态和里氏替换原则关注的角度不一样,多态是面向对象编程的特性,也是一种面向对象编程的语法,同时也是一种代码实现的思路;而里氏替换原则是一种设计原则,用来指导继承关系中子类该如何设计的,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。
里氏替换原则如何落地?
里氏替换原则,有一个更为落地的方针:按照协议来设计
。
子类在设计的时候,要遵循父类的行为规约,子类只允许修改函数内部实现的具体逻辑,但不能改变函数原本的行为约定。约定,我们可以简单概括一下:
函数声明要实现的功能;
假如父类方法约定实现的逻辑时删除列表List里面的空字符串,但是子类实现删除空字符串的同时并对列表的元素进行了排序,这就违反了里氏替换原则;对输入、输出、异常的约定;
如上面的例子,异常的抛出改变了父类程序的行为约定,违反了里氏替换原则;注释中所罗列的任何特殊说明
(如业务的特殊说明)。