PHP中的Liskov替换原则

PHP 中的 Liskov 替换原则

PHP 并不是作为一种面向对象的编程语言开始的,但多年来,PHP 通过类、命名空间、接口、特征、抽象类和其他改进来帮助开发人员编写SOLID代码。

一个流行的误解是 OOP 是关于重用代码。一个简单且看似无害的例子是那些经典的图表,您从父类继承,并逐步声明从船到轮船、自行车到卡车的所有内容。

面向对象的车辆

OOP 的一个明显优势是重用代码。一个类可以继承另一个类,这使得子类可以访问父类的属性和函数,而这个子类又可以被另一个孙子类继承,以此类推。OOP 中的一个不好的地方是子类需要一些技巧来覆盖父类:

class views_object {}
class views_handler extends views_object {}
class views_handler_area extends views_handler {}
class views_handler_area_text extends views_handler_area{}

class views_handler_area_text_custom extends views_handler_area_text {
  // ...

  public function options_submit(&$form, &$form_state) {
    // Empty, so we don't inherit options_submit from the parent.
  }
}

这是来自 Drupal 7 的 Views 模块。views_handler_area_text_custom从四个父类继承,在继承链的这个阶段,这个类必须覆盖父类才能使其工作。

这是一个简单的例子,说明过度使用继承最终会导致问题和混乱的代码。五个 SOLID 原则有助于编写更好的 OOP 代码,而 Liskov 替换原则是一个重要的原则:

让Φ(x)为关于对象的属性可证明的X类型T的然后Φ(Y)应为Y型的的S对象真正其中S是T的子类型
巴巴拉里氏– 1987 -主题上的数据的抽象和等级制度

唔?

让我们重新表述一下。

行为子类型

子类应该满足调用者通过超类的引用访问子类对象的期望。Liskov 替换原则确保调用者可以期望子类以与超类相同的方式运行和交互。这意味着可以子类的对象替换一个对象,并期望它以相同的方式运行并履行其契约。

结构正确的 OOP 代码不仅在语法上正确,而且在含义上也是正确的。确保子类不会导致“找不到方法”错误是不够的:它必须在语义上相同。

此外,类抽象一个任务或一条信息以提供语义。这可以从抽象发送电子邮件的任务,或提供一种统一的方式来访问作者和书中的页数。

然而,当父电子邮件类的子类变成推送通知客户端时,或者当一个简单的书籍信息类可以获取有关电影的信息时,抽象就太过分了。

抽象和语义

抽象的目的不是模糊,而是创造一个可以绝对精确的新语义层次。
— Edsger W. Dijkstra

正确抽象的类为任务赋予意义。创建子类来处理相同的任务,但以不同的形式,这使得它变得有意义。

您的电子邮件类可以抽象为接收收件人电子邮件地址、主题和正文,并且每个子类都可以处理实现细节,无论是使用 SMTP 服务器还是通过 HTTP API 发送。对于来电者而言,使用哪种交通工具并不重要,只要来电者能够提供信息,并获得电子邮件是否发送的反馈。

$email = new PlainTextEmail('foo@example.com', 'Subject', 'Hi Ayesh, ...');
$smtp = new SMTPTransport('smtp.example.com', 465);
$emailer = new Emailer();
$emailer->setTransport($smtp);
$emailer->send($email);

setTransport()方法将接受任何传输方法。我们在这里使用 SMTP,但它可以是任何传输方式,只要它履行其合同即可。你可以用 替换这个SMTPTransportMailGunTransport,它应该可以工作。

此外,您可以替换PlainTextEmail为可能包含 HTML 电子邮件或 DKIM 签名电子邮件的子类型,但传输器(无论是它SMTPTransport还是MailGunTransport)仍然可以使用该电子邮件。

PHP 中的行为子类型

PHP 强制执行此操作的方法是通过接口、抽象类和继承。每当一个类实现一个接口,或者从一个类或一个抽象类继承时,PHP 都会检查子类或实现是否仍然履行其契约:

协方差

协方差允许子类方法声明作为父方法返回类型的子类型的返回类型。

考虑这个例子:


class Image {}
class JpgImage extends Image {}

class Renderer {
     public function render(): Image;
}

class PhotoRenderer {
    public function render(): JpgImage;
}

所述PhotoRenderer类履行合同Renderer类,因为它仍然会返回一个JpgImage对象,它是一个子类型的Image。任何知道如何使用Renderer该类的代码都将继续工作,因为返回值仍然instanceof是预期的Image类。

PHP 8.0 的联合类型可以用更简单的方式展示一个例子:


class Foo {
    public function process(): string|int;
}

class Bar extends Foo {
    public function process(): int;
}

Bar::process()返回类型进一步缩小返回类型它的父类的,而是因为谁可以处理任何调用它不违反其合同Foo都知道,int是预期收益类型之一。

逆变

逆变是关于子类可以期望的函数参数。子类可以增加其参数范围,但它必须接受父类接受的所有参数。


class Foo {
    public function process(int|float $value);
}

class Bar {
    public function process(int|float|string $value);
}

这种逆变是允许的,因为Bar::process方法接受父方法接受的所有参数类型。对于类对象,这意味着子类可以通过扩展其联合类型或通过接受父类来“扩大”它接受的参数范围。

不变性

不变性只是说属性类型(在 PHP 7.4+ 中)不能进一步缩小或扩大。

大图

  • 返回类型可以更窄:子类可以返回子类型或更小的返回类型中的联合类型。
  • 参数可以扩展:子类必须接受并处理父方法处理的所有参数类型。但是可以扩大它以接受更多类型或父类型。
  • 无法更改属性类型

一张图表中的 PHP LSP

学习资料:https://php.watch/articles/php-lsp#contravariance

转载请注明出处:https://www.onexin.net/php-liskov/

相关文章:

1、用 PHP 编写更好的正则表达式regex
https://www.onexin.net/php-regex/

2、PHP 版本 8.2,8.1,8.0,7.4
https://www.onexin.net/php-8_2-8_1-8_0-7_4/

3、php实现自运行的do实例详解
https://www.onexin.net/php-do/

4、PHP 后端实现JWT认证方法示例
https://www.onexin.net/php-jwt/

5、Yii 2.0 框架中使用Smarty视图模板引擎
https://www.onexin.net/yii-2-0-smarty/

Leave a Reply