Зарегистрируйтесь, чтобы продолжить обучение

Полиморфизм подтипов PHP: Полиморфизм

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

<?php

function doSomething($logger)
{
    $logger->info('process starting');
}

Утиная типизация автоматически вытекает из динамической типизации. В большинстве динамических языков понятия "интерфейс" даже не существует. PHP в этом смысле пошёл своим путём. Несмотря на его динамическую природу, классовая модель PHP взята из Java. И в PHP присутствуют почти все соответствующие атрибуты: интерфейсы, абстрактные классы и многое другое. Поэтому в PHP принято указывать интерфейсы ожидаемых объектов в сигнатурах функций.

<?php

function doSomething(Psr\Log\LoggerInterface $logger)
{
    $logger->info('process starting');
}

Теперь эта функция работает только с объектами тех классов, которые реализуют указанный интерфейс.

Как вы помните, в сигнатуре можно указывать и класс, и интерфейс. Что правильнее? Вооружившись принципом инверсии зависимостей, мы можем однозначно сказать, что правильнее использовать интерфейсы и завязываться на абстракцию вместо конкретной реализации. Только в этом случае появится возможность подменить реализацию. Ниже пример реального кода из фреймворка Symfony2, в котором активно используется полиморфизм подтипов с инъекцией через конструктор:

<?php

class Psr18Client implements ClientInterface
{
    private $client;
    private $responseFactory;
    private $streamFactory;

    // Зависимости приходят через конструктор
    public function __construct(HttpClientInterface $client = null, ResponseFactoryInterface $responseFactory = null, StreamFactoryInterface $streamFactory = null)
    {
        $this->client = $client ?? HttpClient::create();
        $this->responseFactory = $responseFactory;
        $this->streamFactory = $streamFactory ?? ($responseFactory instanceof StreamFactoryInterface ? $responseFactory : null);
        $psr17Factory = new Psr17Factory();
        $this->responseFactory = $this->responseFactory ?? $psr17Factory;
        $this->streamFactory = $this->streamFactory ?? $psr17Factory;
    }

    // another methods
}

В очередной раз подчеркну что не нужно возводить эту технику в абсолют. Если создавать интерфейсы на всё подряд, то код очень быстро распухнет и станет сложным. Вспомните, в ruby/python/js вообще нет интерфейсов и никто не умер. Попробуйте побродить по исходникам популярного микрофреймворка Lumen. Внутри него крайне тяжело найти места, где в сигнатурах методов встречаются явные указания интерфейсов

Для лучшей переносимости кода, в PHP вводятся стандартные интерфейсы для самых частых задач. Они описываются в PSR. В примере выше используется стандартный интерфейс Psr\Log\LoggerInterface. Со временем, всё большая часть библиотек начнёт его реализовывать, а это позволит безболезненно подменять логгер без необходимости менять свой код. Кроме логгера в PSR описаны множество других интерфейсов, среди них HTTP-клиент, HTTP-запрос, HTTP-ответ, кеш и другое.

Теперь мы можем ответить на вопрос, почему этот полиморфизм называется полиморфизмом подтипов. Типом в ООП языках принято называть интерфейсы (хотя это не совсем правда) и отношение подтипов определяется отношением интерфейсов.

Интерфейсы можно расширять другими интерфейсами, для этого используется ключевое слово extends. Пример из библиотеки DS (той, где описаны структуры данных на PHP в объектном синтаксисе):

<?php
// https://github.com/php-ds/polyfill/blob/master/src/Collection.php
interface Collection extends \Traversable, \Countable, \JsonSerializable
{
    // some code
}

Теперь любой класс, реализующий интерфейс Collection, будет обязан реализовать все интерфейсы, которые расширяет Collection. Это касается каждого интерфейса в цепочке.

Статические методы

Статические методы жёстко завязаны на имя класса, поэтому полиморфизм подтипов в них невозможен. Но можно схитрить и использовать вместо класса переменную с его именем:

<?php

Foo::aStaticMethod();
$classname = 'Foo';
$classname::aStaticMethod(); // Начиная с PHP 5.3.0

Дополнительные материалы

  1. Subtyping
  2. Принцип открытости/закрытости
  3. Принцип разделения интерфейса
  4. Полиморфизм простыми словами

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 25 000 ₸ в месяц
Разработка веб-приложений на Laravel
10 месяцев
с нуля
Старт 26 декабря

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»