Pull to refresh

Готовимся к собеседованию по PHP: Всё, что вы хотели узнать об интерфейсах, совместимости сигнатур и не побоялись узнать

Reading time 6 min
Views 109K
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 помогают структурировать самообучение вебинары и краткосрочные курсы. Именно поэтому я рекомендую (и немного скромно рекламирую) даже опытным разработчикам посещать разовые вебинары и курсы повышения квалификации — результат при грамотном сочетании курсов и самоподготовки всегда налицо!

Успехов на собеседовании и в работе!
Tags:
Hubs:
+25
Comments 41
Comments Comments 41

Articles