PHP изначально не задумывался как объектно-ориентированный язык программирования, но с годами был усовершенствован внедрением классов, пространств имен, интерфейсов, трейтов, абстрактных классов и других улучшений, которые помогают разработчикам писать на нем код с соблюдением принципов SOLID.

Популярное заблуждение заключается в том, что основная идея ООП — это повторное использования кода. Простой и, казалось бы, безобидный пример — это классические диаграммы, где вы наследуете от родительского класса "транспорт" и прокладываете свой путь вниз по иерархии, чтобы объявить все — от лодок до кораблей, от велосипедов до грузовиков.

Повторное использование кода является одним из очевидных преимуществ ООП. Один класс может наследоваться от другого класса, что делает свойства и функции родительского класса доступными в дочернем классе, и этот дочерний класс, в свою очередь, может наследоваться другим "внучатым" классом, и так далее. Плохой стороной этого преимущества является то, что для переопределения родительского класса в дочернем классе нужно прибегать к хитростям:

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.
  }
}

Это код из модуля Views для Drupal 7. Класс views_handler_area_text_custom наследуется от четырех родителей, и на этом этапе в цепочке наследования этот класс должен переопределить родительский класс, чтобы все работало как нужно.

Это простой пример того, как чрезмерное использование наследования может привести к проблемам и, в итоге, к "грязному" коду. Пять принципов SOLID помогают писать более качественный ООП-код, и один из них — принцип подстановки Барбары Лисков, имеет важное значение:

Пусть Φ(x) является свойством, верным относительно объектов x некоторого типа T. Тогда Φ(y) также должно быть верным для объектов y типа S, где S является подтипом типа T.

Барбара Лисков - 1987 - Основной доклад об абстракциях данных и иерархиях

Шта?

Давайте это перефразируем.

Поведенческое подтипирование


Подклассы должны соответствовать ожиданиям вызывающих методов, получающих доступ к объектам подкласса через ссылки на суперкласс. Принцип подстановки Лисков позволяет удостовериться в том, что вызывающие методы могут ожидать от подклассов такого же поведения и взаимодействия как и от суперкласса. Это означает, что можно заменить объект суперкласса объектом подкласса и ожидать, что он будет вести себя так же и выполнять свой контракт.

ООП-код, структурированный должным образом, будет не только синтаксически правильным, но также по подходящим по смыслу. При этом недостаточно просто убедиться в том, что использование подклассов не приведет к ошибкам типа "метод не найден": подкласс должен быть семантически одинаковым с родителем.

Кроме того, классы абстрагируют задачу или часть информации для обеспечения семантики. Это может отличаться от абстрагирования задачи отправки электронного письма или предоставления единого способа доступа к автору и количеству страниц в книге.

Однако когда подкласс родительского класса электронной почты превращается в клиент push-уведомлений или когда простой класс информации о книге может получать информацию о фильмах — это означает, что абстракция зашла слишком далеко.

Абстракция и семантика

Цель абстракции не в том, чтобы быть расплывчатой, а в том, чтобы создать новый семантический уровень, на котором можно быть абсолютно точным.

Эдсгер Вибе Дейкстра

Правильно абстрагированный класс придает задаче смысл. Целесообразно создавать подклассы для выполнения одной и той же задачи, но в различных вариациях.

Ваш класс электронной почты может быть абстрагирован для получения адреса электронной почты получателя, темы и тела письма, и каждый подкласс может позаботиться о деталях реализации, будь то использование 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, но это может быть любой способ транспортировки, если он выполняет свой контракт. Вы можете заменить класс SMTPTransport на MailGunTransport, и он должен работать.

Кроме того, вы можете заменить 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, будет продолжать работать, потому что возвращаемое значение все еще является экземпляром ожидаемого класса 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+) не могут быть сужены или расширены.

Итого

  • Тип возвращаемого значения можно сузить: подклассы могут возвращать подтипы типов, возвращаемых родительским классом или более узкие объединененные типы.
  • Типы параметров метода могут быть расширены: подклассы должны принимать и обрабатывать все типы параметров, которые обрабатывает родительский метод. Но объединенный тип можно расширить таким образом, чтобы принимать больше типов или родительских типов.
  • Типы свойств не могут быть изменены.

Перевел scorp13

Статья является свободным переводом материала сайта https://php.watch