Интерфейсы, впервые появившись в PHP 5, давно уже заняли прочное место в объектно-ориентированной (или всё-таки правильнее «класс-ориентированной»?) части языка.Казалось бы — что может быть проще интерфейса? "Как бы класс, но и не класс, нельзя создать экземпляр, скорее контракт для будущих классов, содержит в себе заголовки публичных методов" — не правда ли, именно такими словами вы чаще всего отвечаете на собеседовании на дежурный вопрос о том, что такое интерфейс?
Однако не всё так просто, как может показаться начинающему программисту на PHP. Привычные аналогии не работают, руководство по языку вводит вас в заблуждение, в коде таятся неожиданные «подводные камни»…
Три предыдущие части:
- Готовимся к собеседованию по PHP: ключевое слово «static»
- Готовимся к собеседованию по PHP: псевдотип «callable»
- Готовимся к собеседованию по PHP: Всё об итерации и немного про псевдотип «iterable»
Что может содержать интер��ейс?
Очевидно, что публичные методы, причем без реализации: сразу после заголовка (сигнатуры) метода следует закончить его точкой с запятой:
interface SomeInterface { public function foo(); public static function bar(Baz $baz); }
Чуть менее очевиден (хотя и описан в мануале) тот факт, что интерфейс может содержать константы (разумеется, только публичные!):
interface SomeInterface { public const STATUSES = [ 'OK' => 0, 'ERROR' => 1, ]; } if (SomeInterface::STATUSES['OK'] === $status) { // ... }
Почему же константы в интерфейсах не получили широкого распространения в промышленном коде, хотя и используются иногда? Причина в том, что их невозможно переопределить в интерфейсе-наследнике или в классе, реализующем данный интерфейс. Константы интерфейсов — самые константные константы в мире :)
Чего не может содержать интерфейс?
Больше ничего не может. Кроме заголовков публичных методов и публичных констант.
Нельзя включать в интерфейс:
- Любые свойства
- Непубличные методы
- Методы с реализацией
- Непубличные константы
На то, собственно говоря, он и интерфейс!
Совместимость сигнатур методов
Для дальнейшего изучения интерфейсов нам с вами нужно узнать о важнейшем понятии, которое незаслуженно обойдено вниманием в мануале по PHP: о понятии «совместимости сигнатур».
Сигнатура — это описание функции (метода), включающее в себя:
- Модификатор доступа
- Имя функции (метода)
- Список аргументов, где для каждого аргумента указано:
- Тип
- Имя
- Значение по умолчанию
- либо оператор «три точки»
- Тип возвращаемого значения
Примеры:
function (); public function foo($arg = null); protected function sum(int $x, int $y, ...$args): int;
Предположим, что у нас есть две функции, A и B.
Сигнатура функции B считается совместимой с A (порядок важен, отношение несимметрично!) в строгом смысле, если:
Они полностью совпадают
Тривиальный случай, комментировать тут нечего.
B добавляет к A аргументы по умолчанию
A:
function foo($x);
совместимые B:
function foo($x, $y = null); function foo($x, ...$args);
B сужает область значений A
A:
function foo(int $x);
совместимые B:
// В A допускался возврат любых значений, в B эта область сужена только до целых чисел function foo(int $x): int;
Теперь, когда мы ввели эти три простых правила совместимости определений, станет гораздо проще понять дальнейшие тонкости, связанные с интерфейсами.
Наследование интерфейсов
Интерфейсы могут наследоваться друг от друга:
interface First { public const PI = 3.14159; public function foo(int $x); } interface Second extends First { public const E = 2.71828; public function bar(string $s); } assert(3.14159 === First::PI); assert(true === method_exists(First::class, 'foo')); assert(3.14159 === Second::PI); assert(2.71828 === Second::E); assert(true === method_exists(Second::class, 'foo')); assert(true === method_exists(Second::class, 'bar'));
Интерфейс-наследник получает от интерфейса-предка в наследство все определенные в предке методы и константы.
В интерфейсе-наследнике можно переопределить метод из родительского интерфейса. Но только при условии, что либо его сигнатура будет в точности совпадать с сигнатурой родительского, либо будет совместима (см. предыдущий раздел):
interface First { public function foo(int $x); } interface Second extends First { // Так можно, но бессмысленно public function foo(int $x); // Так нельзя, фатальная ошибка Declaration must be compatible public function foo(int $x, int $y); // Так можно, потому что эта сигнатура совместима с родительской - мы просто добавили необязательный аргумент public function foo(int $x, int $y = 0); // Так тоже можно, все аргументы после "..." являются необязательными public function foo(int $x, ...$args); // И так тоже можно public function foo(int $x, ...$args): int; }
Если ли в PHP множественное наследование?
Если вам зададут такой вопрос, смело отвечайте: «да». Интерфейс может наследоваться от нескольких других интерфейсов.
Теперь вы видели всё:
interface First { public function foo(int $x); } interface Second { public function bar(string $s); } interface Third extends First, Second { public function baz(array $a); } assert(true === method_exists(Third::class, 'foo')); assert(true === method_exists(Third::class, 'bar')); assert(true === method_exists(Third::class, 'baz'));
Правила решения конфликтов сигнатур методов при множественном наследовании точно такие же, как мы уже видели выше:
— либо сигнатуры совпадают полностью
— либо сигнатура метода интерфейса, упомянутого в списке предков первым, должна быть совместима с сигнатурой из второго предка (да, порядок упоминания имеет значение, но это очень редкий кейс, просто не принимайте его никогда во внимание)
Тонкости реализации интерфейс��в
Собственно, после всего, что вы уже видели, это уже и не тонкости, а так, мелкие нюансы.
Во-первых действительно, наследование класса от интерфейса называется реализацией. Смысл в том, что вы не просто получаете в наследство методы и константы, но обязаны реализовать те методы, которые заданы сигнатурами, наполнить их кодом:
interface IntSumInterface { public function sum(int $x, int $y): int; } interface IntMultInterface { public function mult(int $x, int $y): int; } class Math implements IntSumInterface, IntMultInterface { public function sum(int $x, int $y): int { return $x + $y; } public function mult(int $x, int $y): int { return $x * $y; } }
Важный аспект, который отличает реализацию интерфейса от наследования от другого класса — это возможность реализовать в одном классе несколько интерфейсов сразу.
Как быть, если в разных интерфейсах, которые реализует класс, будет один и тот же метод (с одинаковым названием)? Смотри выше — также, как и при наследовании интерфейсов друг от друга должен соблюдаться принцип совместимости сигнатур.
И да. Не верьте мануалу, который провозглашает:
Сигнатуры методов в классе, реализующем интерфейс, должны точно совпадать с сигнатурами, используемыми в интерфейсе, в противном случае будет вызвана фатальная ошибка.
The class implementing the interface must use the exact same method signatures as are defined in the interface. Not doing so will result in a fatal error.
Всё не так, действует тоже самое правило совместимости:
interface SomeInterface { public function sum(int $x, int $y); } class SomeClass implements SomeInterface { public function sum(int $x, int $y): int или public function sum(int $x, int $y, int $z = 0): int или даже public function sum(int $x, int $y, ...$args): int { // реализация метода } }
Интерфейс — это класс? Pro et Contra
Вообще-то нет. Интерфейс — это интерфейс, он отличается от класса хотя бы тем, что нельзя создать «экземпляр интерфейса».
И вообще-то да, у них в PHP очень много общего:
- Интерфейсы, как и классы, могут находиться в пространстве имён.
- Интерфейсы, как и классы, можно загружать через механизм автозагрузки. Функции автозагрузки будет передано полное имя интерфейса (с пространством имён).
- В каждом интерфейсе есть предопределенная константа ThisInterface::class, содержащая его полное имя
- Интерфейс, как и класс, может участвовать справа в операторе instanceof
- Интерфейс, как и класс, может быть указан в качестве типа в тайп-хинтинге (указание типа аргумента либо возвращаемого значения функции)
Что почитать в ночь перед ответственным собеседованием?
Разумеется, мануал по языку:
- php.net/manual/ru/language.oop5.interfaces.php
- php.net/manual/ru/language.oop5.constants.php
- php.net/manual/ru/language.constants.predefined.php
Но гораздо лучше читать тяжелые технические тексты не в последнюю ночь, а заранее.
Системный подход к самообразованию в программировании очень важен. И, по моему мнению, неплохо в начале пути в IT помогают структурировать самообучение вебинары и краткосрочные курсы. Именно поэтому я рекомендую (и немного скромно рекламирую) даже опытным разработчикам посещать разовые вебинары и курсы повышения квалификации — результат при грамотном сочетании курсов и самоподготовки всегда налицо!
Успехов на собеседовании и в работе!