imageИнтерфейсы, впервые появившись в PHP 5, давно уже заняли прочное место в объектно-ориентированной (или всё-таки правильнее «класс-ориентированной»?) части языка.

Казалось бы — что может быть проще интерфейса? "Как бы класс, но и не класс, нельзя создать экземпляр, скорее контракт для будущих классов, содержит в себе заголовки публичных методов" — не правда ли, именно такими словами вы чаще всего отвечаете на собеседовании на дежурный вопрос о том, что такое интерфейс?

Однако не всё так просто, как может показаться начинающему программисту на PHP. Привычные аналогии не работают, руководство по языку вводит вас в заблуждение, в коде таятся неожиданные «подводные камни»…

Три предыдущие части:


Что может содержать интер��ейс?


Очевидно, что публичные методы, причем без реализации: сразу после заголовка (сигнатуры) метода следует закончить его точкой с запятой:

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 очень много общего:

  1. Интерфейсы, как и классы, могут находиться в пространстве имён.
  2. Интерфейсы, как и классы, можно загружать через механизм автозагрузки. Функции автозагрузки будет передано полное имя интерфейса (с пространством имён).
  3. В каждом интерфейсе есть предопределенная константа ThisInterface::class, содержащая его полное имя
  4. Интерфейс, как и класс, может участвовать справа в операторе instanceof
  5. Интерфейс, как и класс, может быть указан в качестве типа в тайп-хинтинге (указание типа аргумента либо возвращаемого значения функции)

Что почитать в ночь перед ответственным собеседованием?


Разумеется, мануал по языку:


Но гораздо лучше читать тяжелые технические тексты не в последнюю ночь, а заранее.

Системный подход к самообразованию в программировании очень важен. И, по моему мнению, неплохо в начале пути в IT помогают структурировать самообучение вебинары и краткосрочные курсы. Именно поэтому я рекомендую (и немного скромно рекламирую) даже опытным разработчикам посещать разовые вебинары и курсы повышения квалификации — результат при грамотном сочетании курсов и самоподготовки всегда налицо!

Успехов на собеседовании и в работе!