里氏替换原则

309 阅读3分钟

来自:设计模式之美-王争

什么是里氏替换原则

子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。 看代码

<?php
class Controller {
    public function sendRequest(array $request):string
    {
        return json_encode($request);
    }
}
class UserController extends Controller
{
	@Override
    public function sendRequest(array $request):string
    {
        if (!isset($request['appId'])){
            $request['appId'] = '12333';
        }
        return parent::sendRequest($request);
    }
}

function demo(){
    $controller = new UserController();
    $res = $controller->sendRequest([1233]);
    var_dump($res);
}
demo();

在上面的代码中,子类 UserController 的设计完全符合里式替换原则,可以替换父类出现的任何位置,并且原来代码的逻辑行为不变且正确性也没有被破坏。

不过,你可能会有这样的疑问,刚刚的代码设计不就是简单利用了面向对象的多态特性吗?多态和里式替换原则说的是不是一回事呢?从刚刚的例子和定义描述来看,里式替换原则跟多态看起来确实有点类似,但实际上它们完全是两回事。为什么这么说呢?

接下来我们对UserController做一下改造,$request 数组中不存在 appId 抛出 Exception

class UserController extends Controller
{
	@Override
    public function sendRequest(array $request):string
    {
        if (!isset($request['appId'])){
            throw new Exception('appId 必须');
        }
        return parent::sendRequest($request);
    }
}

改造之后的代码中,整个程序的逻辑行为有了改变, 在子类 $request 中不存在 appId 抛出了错误,在 demo() 中并没有做捕获异常处理,所以整个程序的逻辑发生了变化。

虽然改造之后的代码仍然可以通过 PHP 的多态语法,动态地用子类 UserController 来替换父类 Controller,也并不会导致程序运行报错。但是,从设计思路上来讲,UserController 的设计是不符合里式替换原则的。

好了,我们稍微总结一下。虽然从定义描述和代码实现上来看,多态和里式替换有点类似,但它们关注的角度是不一样的。多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。而里式替换是一种设计原则,是用来指导继承关系中子类该如何设计的,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。

总结

实际上,里式替换原则还有另外一个更加能落地、更有指导意义的描述,那就是“Design By Contract”,中文翻译就是“按照协议来设计”。看起来比较抽象,我来进一步解读一下。子类在设计的时候,要遵守父类的行为约定(或者叫协议)。父类定义了函数的行为约定,那子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定。这里的行为约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。实际上,定义中父类和子类之间的关系,也可以替换成接口和实现类之间的关系。