子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。
实现多态的条件:
1.继承:必须要有子类继承父类的继承关系。
2.重写:子类需要对父类中的一些方法进行重写,然后调用方法时就会调用子类重写的方法而不是原本父类的方法。
3.向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。
多态的核心概念是向上转型,这是实现多态的核心。
而里氏替换原则讲的是,子类不能改变父类原来的逻辑,子类完美继承父类的设计初衷,并做了增强。
这两者的出发点是不同的。
我们举个例子,LoginLog类继承了原来的Login类,并判断是否需要打印登录日志。
public class Login{
public Response doLogin(Request request) {
// ...登录逻辑
}
}
public class LoginLog extends Login {
private Boolean openDebug;
public LoginLog(Bookean openDebug) {
this.openDebug= openDebug;
}
@Override
public Response doLogin(Request request) {
if (openDebug == true) {
logger.log("xxxx进行了登录");
}
return super.sendRequest(request);
}
}
public class Demo {
public void demoFunction(Login login) {
Reuqest request = new Request();
//...省略设置request中数据值的代码...
Response response = login.doLogin(request);
//...省略其他逻辑...
}
}
// 里式替换原则
Demo demo = new Demo();
demo.demofunction(new LoginLog(true););
在上面的代码中,子类 LoginLog的设计完全符合里式替换原则,可以替换父类出现的任何位置,并且原来代码的逻辑行为不变且正确性也没有被破坏。
乍一看上去,这不就是多态嘛,其实,里氏替换原则跟多态的核心思想是不同的,我们继续往下看,假如说LoginLog的代码稍作修改,改成下面这个样子:
public class LoginLog extends Login {
private Boolean openDebug;
public LoginLog(Bookean openDebug) {
this.openDebug= openDebug;
}
@Override
public Response doLogin(Request request) {
if (openDebug == false) {
throw new RuntimeException(...);
}
logger.log("xxxx进行了登录");
return super.sendRequest(request);
}
}
LoginLog继承了Login之后,将doLogin核心代码做了一些改变,使其部分入参的响应结果与父类不同。虽然LoginLog并不会带来编译上的错误,但是在设计上,我们认为它不符合里氏替换原则的。
多态和里式替换有点类似,但它们关注的角度是不一样的。多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。而里式替换是一种设计原则,是用来指导继承关系中子类该如何设计的,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。
比如说,订单排序,父类是按照时间正序排列,子类按照时间倒叙排列。
比如说,父类查询出用户的机构,子类却给去掉了。
比如说,父类买卖商品时不允许超额售卖,子类却允许了超额售卖
比如说,父类对于特定条件下输出为null,子类修改了这个逻辑,输出不为null了。
比如说,父类对于输入一个正整数,抛出一个异常,子类将这个异常捕获了。
只要是父类的一切输入、输出、异常、逻辑,子类有任何的修改,就是违背里氏替换原则。
子类不能改变父类原来的逻辑,子类完美继承父类的设计初衷,并做了增强。
一、改进已有实现。例如程序最开始实现时采用了低效的排序算法,改进时使用LSP(里氏替换原则)实现更高效的排序算法。
二、指导程序开发。告诉我们如何组织类和子类(subtype),子类的方法(非私有方法)要符合contract。
三、改进抽象设计。如果一个子类中的实现违反了LSP(里氏替换原则),那么是不是考虑抽象或者设计出了问题。
里氏替换最终一句话还是对扩展开放,对修改关闭,不能改变父类的入参,返回,但是子类可以自己扩展方法中的逻辑。父类方法名很明显限定了逻辑内容,比如按金额排序这种,子类就不要去重写金额排序,改成日期排序之类的,而应该抽出一个排序方法,然后再写一个获取排序的方法,父类获取排序调用金额排序,子类就重写调用排序方法,获取日期排序。
也是为了避免“二意性”,这里是只父类的逻辑和子类逻辑差别太多,读代码的人会感觉模棱两可,父类一套,子类一套,到底应该读哪种。感觉会混乱。
总之就是,子类的重写最好是扩展父类,而不要修改父类。
王争老师《设计模式之美》