Kodokon kodokon.com

Наследование, интерфейсы, трейты и абстрактные классы

Выбирай подходящий инструмент проектирования - контракт, скелет алгоритма или горизонтальное переиспользование - понимая компромиссы каждого.

9 мин · 3 вопросов

Открыть этот урок в Kodokon

PHP даёт тебе четыре инструмента проектирования, которые частично перекрываются: конкретное наследование, абстрактные классы, интерфейсы и трейты. Выбор сводится к одному вопросу: что именно ты хочешь разделить? Контракт: интерфейс. Контракт плюс скелет реализации: абстрактный класс. Код, общий для классов без иерархической связи: трейт. А конкретное наследование оправдать всё сложнее: оно жёстко связывает, и родитель в 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: реализация остаётся взаимозаменяемой.

Трейты решают горизонтальное переиспользование: они внедряют одни и те же методы в классы без общего предка. Когда два трейта дают метод с одинаковым именем, 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');
Конфликт трейтов: insteadof решает, as сохраняет псевдоним.

Проверка знаний

Убедись, что запомнил ключевые моменты этого урока.

  1. Какое решающее преимущество у интерфейса перед абстрактным классом?
    • Он может хранить общие свойства
    • Класс может реализовать несколько интерфейсов, но унаследоваться только от одного класса
    • Его методы работают быстрее
  2. Два трейта, используемых одним классом, объявляют по методу log(). Что произойдёт?
    • PHP возьмёт метод из первого перечисленного трейта
    • Фатальная ошибка, если не разрешить конфликт явно через insteadof
    • PHP объединит два метода в один
  3. Что может содержать абстрактный класс, но не может интерфейс?
    • Константы
    • Свойства и реализованные методы
    • Сигнатуры публичных методов