Kodokon kodokon.com

继承、接口、trait 与抽象类

通过了解各自的取舍,选择正确的设计工具 - 一份契约、一个算法骨架,还是横向复用。

9 分钟 · 3 题

在 Kodokon 中打开本课

PHP 给了你四种彼此部分重叠的设计工具:具体继承、抽象类、接口和 trait。选择归结为一个问题:你想共享什么?一份契约:接口。一份契约再加一个实现骨架:抽象类。在没有层级关系的类之间共享的代码:trait。至于具体继承,则越来越难说得通:它耦合紧密,而且 PHP 只允许一个父类。

PHP
<?php

declare(strict_types=1);

abstract class Exporter
{
    public function export(array $rows): string
    {
        return $this->head() . $this->body($rows);
    }

    abstract protected function head(): string;

    abstract protected function body(
        array $rows
    ): string;
}

final class CsvExporter extends Exporter
{
    protected function head(): string
    {
        return "id;name\n";
    }

    protected function body(array $rows): string
    {
        $lines = array_map(
            fn (array $r): string => implode(';', $r),
            $rows,
        );

        return implode("\n", $lines);
    }
}

$csv = new CsvExporter();
echo $csv->export([[1, 'Ada'], [2, 'Linus']]);
模板方法:父类固定算法,子类提供细节。

接口是纯粹的契约:只有签名和常量,没有状态也没有实现。它相对抽象类的决定性优势:一个类可以实现多个接口,而且任何现有的类都可以事后采纳这份契约。相比之下,抽象类携带状态和共享代码,但会占用你唯一的继承名额。实践中:用接口(Cache,而不是 ArrayCache)来标注你的参数类型,这样你就能替换实现,无论是在测试中还是在生产中。

PHP
<?php

declare(strict_types=1);

interface Cache
{
    public function get(string $key): ?string;

    public function set(
        string $key,
        string $value
    ): void;
}

final class ArrayCache implements Cache
{
    private array $items = [];

    public function get(string $key): ?string
    {
        return $this->items[$key] ?? null;
    }

    public function set(
        string $key,
        string $value
    ): void {
        $this->items[$key] = $value;
    }
}

$cache = new ArrayCache();
$cache->set('lang', 'PHP');
echo $cache->get('lang');
客户端代码依赖 Cache:实现可以随时替换。

trait 解决的是横向共享:把相同的方法注入到没有共同祖先的类里。当两个 trait 提供了同名方法时,PHP 拒绝替你做选择,并要求一个显式的解决方案:insteadof 指定要保留的版本,as 创建一个别名以便另一个仍然可以访问。这种冗长是刻意为之的 - 冲突必须在代码中保持可见。

PHP
<?php

declare(strict_types=1);

trait FileLogger
{
    public function log(string $message): void
    {
        echo "[file] $message\n";
    }
}

trait AuditLogger
{
    public function log(string $message): void
    {
        echo "[audit] $message\n";
    }
}

final class PaymentService
{
    use FileLogger, AuditLogger {
        FileLogger::log insteadof AuditLogger;
        AuditLogger::log as audit;
    }
}

$service = new PaymentService();
$service->log('Payment received');
$service->audit('Payment received');
trait 冲突:insteadof 做出决定,as 保留一个别名。

知识检测

确认你已牢记本课的重点内容。

  1. 接口相对抽象类有什么决定性的优势?
    • 它可以持有共享的属性
    • 一个类可以实现多个接口,但只能继承一个类
    • 它的方法运行得更快
  2. 同一个类使用的两个 trait 各自声明了一个 log() 方法。会发生什么?
    • PHP 会使用列在最前面的那个 trait 的方法
    • 致命错误,除非用 insteadof 显式解决
    • PHP 会把这两个方法合并成一个
  3. 抽象类可以包含哪些接口不能包含的东西?
    • 常量
    • 属性和已实现的方法
    • 公开的方法签名