PHP: Что такое Интерфейс
В предыдущей статье мы говорили о PHP классах. Исходя из этого, сегодня мы поговорим об интерфейсах.
Основы интерфейсов в PHP
В качестве примера основ PHP интерфейса рассмотрим что-то издающее звуки
. В реальном мире это может быть птица (чирикает
), собака (лает
), кошка (мяукает
) или человек (поёт
). Детали озвучивания специфичны для каждого типа, но каждый может издавать звуки.
Мы можем описать это следующим образом:
interface Vocalizer
{
public function vocalize(string $message): string;
}
То о чём мы говорили выше: полученную строку $message
, vocalize()
вернёт то, что слышно, как строку.
Теперь интерфейс ничего не делает сам по себе. Он действует как тип PHP. Значит вы можете указать его в качестве аргумента или даже вернуть что-то в этом типе из функции или метода.
Для создания чего-либо этого типа, нам нужна доступная реализация. Классы могут реализовывать интерфейс:
class Bird implements Vocalizer
{
public function vocalize(string $message): string
{
return sprintf('<tweet>%s</tweet>', $message);
}
}
Допустим у нас есть следующая функция:
function prepareMessage(string $message, Vocalizer $vocalizer): string
{
return $vocalizer->vocalize($message);
}
Вышеупомянутая функция может быть вызвана с любым $vocalizer
реализующим Vocalizer
:
$chickadee = new Bird();
echo prepareMessage('a song', $chickadee); // "<tweet>a song</tweet>"
Наследование и подстановка
Короче говоря, интерфейсы дают возможность предоставлять функцию не требуя наследования классов. Это может быть полезно для адаптации существующих классов для работы в других контекстах.
В качестве примера предположим, что у нас есть класс Bird
не реализующий Vocalizer
, но у нас есть практически эквивалентная функциональность через метод tweet()
:
class Bird
{
public function tweet(string $message): string
{
return sprintf('<tweet>%s</tweet>', $message);
}
}
На данный момент мы можем сделать одну из двух вещей, чтобы сделать этот класс Vocalizer
, сохранив при этом всю существующую функциональность.
Во-первых, мы могли бы обновить класс, чтобы напрямую реализовать интерфейс Vocalizer
:
class Bird implements Vocalizer
{
public function vocalize(string $message): string
{
return $this->tweet($message);
}
public function tweet(string $message): string
{
return sprintf('<tweet>%s</tweet>', $message);
}
}
В качестве альтернативы мы могли бы создать расширение Bird
, реализующее Vocalizer
:
class VocalizingBird extends Bird implements Vocalizer
{
public function vocalize(string $message): string
{
return $this->tweet($message);
}
}
Поскольку он является расширением Bird
, он соответствует типу Bird
для подсказок типа; поскольку он так же реализует Vocalizer
, он также соответствует этому типу. Вы можете выполнить вышеуказанное с помощью реализации анонимного класса:
$bird = new class extends Bird implements Vocalizer {
public function vocalize(string $message): string
{
return $this->tweet($message);
}
};
Это приводит нас к ключевому обоснованию использования интерфейсов: возможности замены одного типа другим.
Пять принципов объектно-ориентированного программирования
При реализации объектно-ориентированного программирования существует набор из пяти основных принципов проектирования рекомендуемых для создания гибких, удобных в сопровождении архитектур, известных как принципы SOLID:
- Single-responsibility principle — Принцип единой ответственности
- Open-closed principle — Принцип Открытости/Закрытости
- Liskov substitution principle — Принцип подстановки Барбары Лисков
- Interface segregation principle — Принцип разделения интерфейса
- Dependency inversion principle — Принцип инверсии зависимости
Принцип инверсии зависимости
На что я хочу обратить внимание, так это на принцип инверсии зависимости.
Принцип инверсии зависимости гласит, что мы должны зависеть от абстракций, а не от реализации. Что это значит?
Если нас беспокоит только озвучивание, нам не нужно беспокоиться о том, есть ли у нас Bird
, Human
или Animal
. Мы должны беспокоиться о том, есть ли у нас что-то способное издавать звуки.
Интерфейсы позволяют нам определять эти возможности, а затем позволяют коду подсказывать тип этих возможностей, а не более конкретный тип. Это, в свою очередь, позволяет нам заменять разные типы, когда они выполняют контракт, определённым интерфейсом.
Распространённая ошибка при объектно-ориентированном программировании
Одной из распространённых ошибок при начале объектно-ориентированного программирования является создание строгого дерева наследования: базовый класс, затем подтипы этого базового класса, затем реализация этих подтипов и так далее.
Это может привести к созданию класса, который технически имеет десяток или более различных вариантов поведения, но используется только для одного из них. Разделив это поведение на разные интерфейсы, мы можем создавать классы реализующие только определённое поведение, и использовать их везде, где это поведение необходимо.
Что можно определить в интерфейсе
Теперь, когда у нас есть общее представление, что такое интерфейс, зачем его использовать и как его реализовать, Что мы можем определить в интерфейсе?
Интерфейсы в PHP ограничены:
- Публичными методами.
- Публичными константами.
В предыдущей статье о PHP классах мы отметили, что видимость — более сложная тема. Это всё ещё так, но мы можем осветить некоторые основы. В двух словах, видимость помогает детализировать, что и где может использовать функциональность. К чему-то, что имеет публичную видимость, можно получить доступ как из методов класса, так и из экземпляров. Что подразумевается под последним? В следующем:
$chickadee = new Bird();
$chickadee
— это экземпляр.
Отступление: Константы класса
В статье о классах мы не рассматривали константы классов. Константа похожа на обычную PHP константу в том смысле, что она детализирует значение, которое остаётся постоянным. Это значение нельзя переназначить позже. Для определения константы класса используется ключевое слово const
в объявлении класса и, необязательно, оператор видимости (как у свойств и методов, видимость по умолчанию public
):
class Bird
{
public const TAXONOMY_CLASS = 'aves';
}
Ссылаясь на константу используют имя класса и имя константы разделённые двоеточием :
:
$taxonomy = Bird::TAXONOMY_CLASS;
Константа класса может быть любого скалярного типа или массивом, если ни один из членов массива не является объектом. При необходимости они могут даже ссылаться на другие константы.
По соглашению имена констант обычно пишут ЗАГЛАВНЫМИ БУКВАМИ с использованием подчёркивания _
в качестве разделителя слов.
Константы определённые в интерфейсе наследуются всеми реализациями.
При определении метода в интерфейсе вы опускаете тело и его фигурные скобки и вместо этого заканчиваете объявление точкой с запятой ;
; вы определяете только сигнатуру. Они указывают, что реализация должна определить, чтобы быть допустимой.
Мы уже видели определение метода ранее, когда определяли метод vocalize()
в интерфейсе Vocalizer
:
public function vocalize(string $message): string;
Таким образом, любая реализация должна определять этот метод и быть совместимой. Они могут добавлять дополнительные аргументы, но только если эти аргументы являются необязательными. Только так они могут отличаться.
Возможность реализации множественного наследования
Одной из возможностей, предлагаемых некоторыми языками, является множественное наследование. Эта возможность позволяет объекту расширять несколько других типов и, таким образом, выполнять их все. Например, класс Horse
может расширять как класс Animal
, так и класс Vehicle
.
PHP не предлагает множественного наследования, по крайней мере, напрямую. Однако он обеспечивает возможность реализации нескольких интерфейсов. Это может быть полезно, когда вы хотите описать подмножество функций, предоставляемых конкретным классом в данном контексте.
Пример реализации нескольких интерфейсов
Например, пакет laminas/laminas-cache
определяет множество интерфейсов, описывающих возможности адаптера хранилища, включая FlushableInterface
, OptimizableInterface
, ClearByPrefixInterface
, TaggableInterface
, и т.д. Отдельные адаптеры — это всё адаптеры хранения, но они могут указывать, что у них есть другие возможности, реализуя эти интерфейсы.
Для реализации нескольких интерфейсов, вы предоставляете список интерфейсов, разделённых запятыми, после ключевого слова implements
при объявлении вашего класса:
class MyCustomStorageAdapter extends AbstractAdapter implements
ClearByStorageInterface,
FlushableInterface,
OptimizableInterface,
TaggableInterface
{
// . . .
}
Экземпляр этого класса будет выполнять подсказки типов для каждого из этих интерфейсов.
Именование интерфейсов PHP
Как вы должны называть свой интерфейс? Вообще назовите его на основе поведения, которое интерфейс описывает. В нашем примере мы определяли что-то издающее звуки
, поэтому мы назвали интерфейс Vocalizer
.
Это может быть трудно описать, особенно если вы извлекаете интерфейс из класса, который вы ранее определили, где логическое имя уже является именем существующего класса (например, вы можете захотеть определить интерфейс Logger
, но класс Logger
уже существует).
Иногда трудности с присвоением имени возникают из-за того, что в команде разработчиков разный опят или страны (например, у некоторых членов команды может быть разный родной язык). Таким образом, многие проекты используют суффикс interface
для упрощения решения об именовании, а также указать, какие файлы классов в проекте являются контрактами, а какие реализациями.
Вы и ваша команда должны решить, какой подход наиболее подходящий для вас!
Заключение
PHP интерфейсы предоставляют повторно используемы типы, подробно описывающие конкретное поведение, которое может использовать ваше приложение. В частности, они позволяют создавать классы с известным ожидаемым поведением, которое соответствует вашей собственной архитектуре приложения. Интерфейсы PHP также позволяют заменять различные реализации, независимо от обычного наследования классов, что даёт больше гибкости в структуре приложения.