Эффективное использование интерфейсов в PHP

Вчера на Reddit появился вопрос о цели интерфейсов в PHP. Пока я слишком опаздывал на вечеринку, чтобы дать ответ в этой теме (по крайней мере, чтобы быть замеченным кем-нибудь), я подумал, что это была большая тема для разговора.

Итак, давайте взглянем на интерфейсы в PHP.

Интерфейсы

Интерфейсы похожи на классы, за исключением гораздо меньших возможностей. На самом деле, есть только две вещи, которые вы можете сделать в интерфейсе:

  1. Определить заглушки публичных методов или подписать публичные методы без тел, и
  2. Наследовать другой интерфейс

Это заканчивается чем-то вроде:

<?php

interface QueryInterface {
  public function whereId($id);
}

interface CustomerQueryInterface extends QueryInterface {
  public function whereType($type);
}

После обычных правил наследования, CustomerQueryInterface определяет два метода:

  1. whereId($id) из исходного QueryInterface
  2. whereType($type) от себя

Опять же, эти методы не могут иметь тел в интерфейсе. Так какая же польза в этом?

Обратите внимание: вы не обязаны называть интерфейс с суффиксом Interface. Здесь, я просто следую удобным правилам именования, используемых внутри PHP Framework Interop Group (PHP-FIG). Вы можете называть их так, как душе угодно.

Реализация интерфейсов

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

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

Так что, если мы хотели использовать CustomerQueryInterface?

<?php

class CustomerQuery implements CustomerQueryInterface {
  public function whereId($id) {
    // сделать что-то, чтобы получить Customer по $id
  }

  public function whereType($type) {
    // сделать что-то, чтобы получить Customer по $type
  }
}

Мы используем интерфейс с помощью ключевого слова implements. Это очень похоже на наследование других классов, но работает только для интерфейсов. Ключевое слово implements также позволяет реализовать несколько интерфейсов, в то время как наследование в PHP позволяет расширить только один класс (можно воспользоваться трейтами для множественного наследования, прим. перев.).

Класс CustomerQuery включает реализацию методов whereId($id) и whereType($type) интерфейса CustomerQueryInterface. Он должен придерживаться описания интерфейса.

Но, опять же: чем это полезно?

Внедрение зависимости

Чтобы увидеть один из самых частых вариантов использования интерфейсов, нам необходимо сначала отвлечься и обсудить тему внедрения зависимостей.

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

По сути, с внедрением зависимостей, зависимости даются нам, а не мы их создаём.

Так что, если вы ранее имели класс, который выглядит следующим образом:

<?php

class CustomersController {
  private $customerQuery;

  public function __construct() {
    $this->customerQuery = new CustomerQuery();
  }
}

С внедрением зависимостей, этот класс будет переработан, чтобы выглядеть следующим образом:

<?php

class CustomersController {
  private $customerQuery;

  public function __construct(CustomerQuery $customerQuery) {
    $this->customerQuery = $customerQuery;
  }
}

Теперь, вместо создания экземпляра нашей зависимости CustomerQuery, мы её получаем или внедряем эту зависимость. Очевидно, что напрашивается вопрос: «Как получить экземпляр CustomerQuery и передать в CustomersController, тогда?» Об этом в другой раз, но очевидно то, что независимо от того, что ответственно за создание контроллера, оно же ответственно и за создание запроса.

Это, казалось бы, незначительно изменение, но оно открывает целый мир возможностей для нашего кода:

  1. Теперь мы можем менять реализацию CustomerQuery без изменения кода, использующего его (почти, у нас есть ещё одно изменение, сделаем его через секунду). А из-за этого,
  2. Мы можем протестировать CustomersController гораздо проще, в изоляции, с помощью mock’ов, создавая псевдообъекты CustomerQuery, вместо их экземпляров и, вероятно, используя экземпляр базы данных только для проверки нашего контроллера.
  3. Наш код в настоящее время более выразителен. Проще сказать, что класс зависит от CustomerQuery, потому что мы должны был передать его при создании экземпляра CusomersController. Ранее, без копания в коде, мы понятия не имели, что у этого класса вообще есть какие-то зависимости.

Так в третий раз: как сделать интерфейсы полезными?

Интерфейс Контроля Типа

В нашем предыдущем примере внедрения зависимостей мы использовали подсказку типа CustomerQuery в конструкторе. Вы помните, что CustomerQuery — это конкретная реализация интерфейса CustomerQueryInterface.

Мы можем применить это для рефакоринга на шаг вперёд и вместо типа, намекающего на конкретную реализацию, мы можем указать намёк на CustomerQueryInteface:

<?php

class CustomersController {
  private $customerQuery;

  public function __construct(CustomerQueryInterface $customerQuery) {
    $this->customerQuery = $customerQuery;
  }
}

Это то место, где концепция следования контракту интерфейса действительно вступает в игру: наша CustomersController заявляет, что требует экземпляр CustomerQueryInterface. Это всё равно, что получить конкретный экземпляр этого интерфейса, поскольку мы получаем те методы, которые ожидаем.

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

Это делает интерфейсы исключительно полезными и мощными в PHP.

Интерфейсы — Контракты

Проще говоря: интерфейсы — это контракты, которые реализуют некоторый код и поддерживают его, а другой код зависит от него. Это позволяет нам иметь уверенность, что любой зависимости будут соответствовать методы, которые мы ожидаем. Далее, с предстоящим PHP 7, мы сможем использовать также подсказки типа (объекты и скалярные величины) и возвращаемые типы для дальнейшего обеспечения конкретной реализации, придерживающейся контрактов.

Посмотрите мою статью «Реализация Луковой Архитектуры на PHP», где есть больше примеров того, как можно использовать интерфейсы для достижения чистой архитектуры и разделяемого кода.

Это перевод статьи «Using Interfaces Effectively in PHP».

Комментарии

  1. Includen пишет:

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

      • Includen пишет:

        Это перевод, а не моя статья.

  2. Kroll пишет:

    Для простоты понимания, стоит написать, что обычный класс — это интерфейс (публичные функции) + реализация (тела функций).
    Когда разработчику, например, нужно несколько реализаций, то оставить в одном классе получается только интерфейс. Далее производные классы будут для своего интерфейса наследовать его, а все тела этим функциям обязательно доопределять на свой лад.

    • Includen пишет:

      По-моему статья довольно доходчиво объяснила.

    • Александр пишет:

      Чтобы выделить логику взаимодействия частей приложения и дать больше возможностей в управлении сложностью.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *