17 мая в 17:57

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

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

Успехов на собеседовании и в работе!
Альберт Степанцев @AlexLeonov
карма
146,0
рейтинг 38,1
Программист. Team-Lead. Преподаватель.
Похожие публикации
Самое читаемое Разработка

Комментарии (30)

  • +5
    Оо… далеко не первый год в PHP, но не знал про наследование интерфейсов. Всмысле, вообще не задумывался, что в интерфейсе можно делать extends :) Как-то не встречалось и не пригодилось. Отложу в копилку супер-нужных знаний, вдруг понадобится.

    Кстати, указывайте о какой версии PHP идет речь. А то начнут указывать тип int в аргументах метода ниже 7-й версии и сильно удивятся.
    • +1
      Спасибо, что обратили внимание. Везде, где не указано обратное, речь идет об актуальной стабильной версии PHP. В данный момент это, как известно, PHP 7.1

      Если требуется акцентировать внимание на поведение в старых версиях — я это обычно оговариваю отдельно.

      И на курсах такой же принцип.
      • 0
        Просто в мире PHP «последняя» и «актуальная» версия, к сожалению сильно отличаются.

        Я сейчас не так активно работаю с клиентскими серверами, но еще ни разу не встречал стороннего сервера с 7-й веткой. Только если ставили специально, а это, увы, не всегда возможно. У самого большой проект на дебиане под который просто нет 7-й версии, приходится жить на 5.6 :(

        P.S. Я знаю про некую статистику по которой бОльшая часть серверов на 7-й версии. В реальном мире статистика не работает, к сожалению.
        • +1
          Мы с вами живем в каких-то разных «реальных мирах».

          Я тоже разработчик, активно практикующий (в том числе на основной работе). Встретить где-то PHP 5 для меня — это как увидеть на улице динозавра. Теоретически возможно, конечно, но на практике не встречается :)

          Позвольте полюбопытствовать, зачем вы так? Какие скрытые фобии перед PHP 7 вами движут?
          • 0
            Не не, вы не так поняли :)
            Я лично — всецело за новые версии, тем более когда они привносят столько полезного.

            Может быть я такой, но реально не встречал «обычных» проектов, созданных еще во времена 5-й ветки, которые бы сознательно обновлялись до 7-й. Они просто работают… и все тут :)

            «Обычных» — имею ввиду блоги, небольшие магазины и пр. проекты до 10к/сутки.
            • 0
              Так а что мешает-то?
              $ sudo apt update
              и вуаля — вы на заветной «семерке».

              Если раньше у вас не было классов, которые назывались Int или String, никаких проблем не будет. Всё просто будет работать, как и раньше, просто в 3 раза быстрее и в 2 раза экономнее по памяти.

              Если такие классы были — переименуйте. Дел на 10 минут.
              Ну ОК, поднять тестовый стенд клонированием боевого, дать ему другой домен и прогнать все приемочные тесты. 20 минут, хорошо.

              Или я чего-то не понимаю?
              • +2
                Для начала — любое обновление требует проверки работоспособности и, возможно, некоторых исправлений. Согласен, 7-я ветка относительно совместима с предыдущей, но все же. Я не раз обновлял версии PHP и думаю вы тоже, так что должны знать, что бывают такие внезпные казусы, что проще оставить как есть.

                Затраты времени должны кем-то оплачиваться. По крайней мере я ценю свое время и усилия и обновлять бесплатно мало кого буду. А зачем клиенту платить за кота в мешке? У него и так VDS за абстрактную 1000 руб./мес простаивает.

                Собственно, это основная причина по которой «простые» проекты могут оставаться на 5-й версии очень долго. Пока обновление сильно не понадобится — оно никого не волнует.

                Второй момент: обновление не всегда возможно. Например, около года назад я хотел так же обновить PHP 5.6 > 7 на Debian 7 на собственном проекте. Но оказалось, что под эту ОС его просто нет. Возможны танцы с бубнами, вроде как, но зачем оно мне? Обновление не критично, производительности мне с головой хватает. Можно обновить ОС и потом обновить PHP, но, снова, а зачем мне эти проблемы?

                Третий момент вам описали ниже: если вы думаете, что класс Int и String — это все, что может помешать обновиться, то перечитайте еще раз вот эту страницу: http://php.net/manual/ru/migration70.incompatible.php

                Я как-то обновлял PHP 5.3 > 5.4 (казалось бы) на одном сервере веб-студии с кучей-кучей мелких проектов. И это был такой ад, что в итоге мы просто вернули все обратно. Иногда обновление просто нецелесообразно.

                Ну и, да, не стоит делать просто update, лучше все же осмысленно обновлять пакеты поочереди, если хочется обновить все и сразу. Вдруг что-то полетит и ищи потом причину.

                P.S. Кто придумал эту глупость с ограничением комментов на 1 в час? Ну высказал свое мнение, нахватал минусов — теперь не напишешь ничего толком (
                • –2
                  перечитайте еще раз вот эту страницу: http://php.net/manual/ru/migration70.incompatible.php

                  Слушайте, я перевел с 5 на 7 уже порядка двух десятков проектов разной сложности. Включая совершенно уникальные вещи. И нигде не было у меня проблем и нигде не было оффлайна более трех минут и трудозатрат больше условных пары человеко-дней.

                  Повторю свой вопрос: ну что я делаю не так? В чем ошибаюсь? Чего не знаю еще о PHP, что знаете вы? Расскажите, интересно же!

                  UPD Плюсанул вам в карму, может поможет :) Но пожалуйста больше никогда не ставьте реф-ссылки…
                  • 0
                    При большом легаси в несколько MLOC потребовалось значительное время на перевод. Сначала сделаны были git hooks с запретом на коммиты кода с несовместимыми с php7 конструкциями — иначе переводить проект, где код пишут сотни php программистов не представлялось возможным. После последовательный перевод кода на 7. Не скажу что много работы, но и не мало получилось. Определенную сложность доставили расширения, поменявшие свою версию — привалило работы для qa на тестирования функционала, связанного с этими расширениями. И баги находились.
                    В целом для большого проекта получился быстрый переход. Но конечно не человекодни.
                  • 0
                    Переход на php 7 в зависимости от проекта требует разных усилий.
                    На моей памяти летом прошлого года был случай когда переходили с 5.4 на 5.6 чтобы развернуть докер и с него перейти на 7ку. Весь код проверили и привели к совместимости. Но на тестах бились юзеры один раз запускаешь и потом все не работает. Ушло примерно 2 недели осмысления в фоновом режиме на то чтобы понять, что используемая библиотека шифрования в новой версии работает иначе и оттуда проблемы. ПРишлось пробегаться по всем юзерам и пересчитывать перед обновлением. Так что увы не все так однозначно.
                    • 0
                      Никто с вами не спорит, что усилия нужны.
                      Просто я считаю, что многие комментаторы тут преувеличивают «масштаб бедствия».
                      • 0
                        Согласен, но хорошо бы уточнить, какого размера в строках кода были те проекты, где перевод занял человеко-дни и участвовали ли QA в переводе проектов, или было большое покрытие тестами, или помолясь…
                        • 0
                          Размер: скорее 10 в пятой, чем в шестой
                          Без QA я не работаю уже много лет, поэтому ответ «да, конечно же». Как вообще можно деплоить без регресса?
              • 0
                Немного некорректно покупать следующую модель телефона, когда тебе хватает и текущей. Должна быть прямая цель или необходимость. Обновление просто ради обновления.
                • 0
                  Мы не про телефоны говорим, это какая-то ложная аналогия.

                  Стоимость нового телефона обычно не меньше, чем цена старого, в случае же обновления версии PHP нет никакой необходимости заново переписывать всё приложение.
          • 0
            Есть проекты, живущие 5, 6, 7, 10(!) лет. В них просто тонна кода, адаптация под php7 требует ресурсов, которых всегда не хватает. Планы по пееходу конечно есть, но это сложно. т.к. ресурсы, время, деньги… Просто так сказать заказчикам по всей России, что нужно перейти на новую версию не поможет)
            • +1
              Ну пусть живет 10 лет. Это же прекрасно!

              Вы в этом проекте деньги получаете от заказчика за поддержку? Включите в план поддержки обновление софта на сервере. Не получаете денег? Ну тогда нет и вопроса для обсуждения.

              Не считайте бизнес идиотами — они всё понимают не хуже вас. И что такое «почти бесплатно поднять производительность в 3 раза, просто один раз пройдясь по списку из трех возможных несовместимостей» тоже поймут.

              Мне было бы стыдно получать деньги за проект, на котором я впариваю заказчику PHP 5.6, честное слово. Это пахнет каким-то обманом: «чувак, ты нам платишь, но мы тебе впариваем за твои же деньги устаревшее фуфло и скрываем от тебя информацию о том, что можно обновиться».

              P.S. Если речь не идет о товарно-денежных отношениях — возможно всё. Свой домашний проект можете хоть на PHP 3 ради интереса поддерживать.

              P.P.S.
              Если вы так уверены в своей точке зрения — предлагаю пари. С вас репозиторий проекта, который вы считаете сложным для перехода и сценарий его развертывания (или образ какой-нибудь виртуалки, мне всё равно). С меня честный отчет и хронометраж времени, которое я затрачу на «адаптацию» кода. С отчетом потом можете делать что угодно, в том числе использовать, как план для обновления.
        • +1
          У самого большой проект на дебиане под который просто нет 7-й версии, приходится жить на 5.6 :(

          То есть про backports & dotdeb он не знает, да?
          • –1
            Выше уже несколкьо раз сказали, что обновить можно все, но зачем? Обновление ради обновления не делается.
            • 0
              Вас уже давно все услышали. А вот вы противоположную точку зрения — не хотите слышать.

              Обновление делается не ради обновления, а ради того, чтобы затратив усилия на X рублей, получить от этого преимуществ на Y рублей. Желательно, чтобы Y >> X

              Переход на PHP 7 обычно соответствует этому условию.
            • 0
              То есть прирост производительности в 30%-70% лишний да?
              Вот реально 7 работает быстрее на том же железе чем 5.6. Если код был написан нормально, без совсем уж устаревших вещей (ну к примеру, написанный под свежие на момент выхода 5.3 вещи), то он переносится под 7 практически без проблем.
            • 0
              Более того, сейчас переносим проект, который все еще работает на 5.3, но с пачкой устаревшего кода, времен чуть ли не php 4. Да, приходится старые куски со спагетти кодом заменять новыми, но их не так уж много (в основном это всякие крон задачки), но оно стоит того. Остальное в PHP7 заводится к удивлению даже лучше, чем если бы мы перетаскивали его на 5.6.
              В итоге то, что работало на 8-ми хардверных ядрах с LA 2-3, работает в виртуалке с одним ядром с LA 0.3
    • 0
      Имеется костыль на PHP 5.6 (через переопределение обработчика ошибок), чтобы заставить типизацию работать и там.
  • –1
    Интерфейс, если не брать множественное наследование, фактически по смыслу близнец абстрактного класса без защищенных и приватных элементов.

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

    Давно заметил, что тесты проходят, если реализация интерфейса добавляет дефолтное значение. Но я ругаюсь на такое — это говнокодерство, пользоваться лазейками совместимости, чтобы расширять функциональность. Интерфейс должен скрупулезно совпадать с реализацией, иначе очень быстро накапливается путаница. Если недостает возможностей старого интерфейса — надо добавить дополнительный интерфейс.
    • 0
      это говнокодерство, пользоваться лазейками совместимости, чтобы расширять функциональность

      Интерфейс должен скрупулезно совпадать с реализацией


      Понимаете, тут такое дело: или вы знаете язык, за знание которого вам собираются платить деньги, или нет. Если нет — собеседование не пройдено. Если да — имеете право на своё мнение.

      Я ни в коем случае не хочу оспаривать ваше право иметь то мнение о PHP, которое вы имеете. Просто хочу заметить: чтобы иметь мнение о языке, рекомендуется его (язык) знать.

      И это не «лазейки». Это так устроен PHP. Как бы вам это не нравилось :)
      • 0
        Обычно я нанимаю людей. Знание языка вообще ничего не гарантирует. Более того, в реальной работе чем меньше малоизвестных «особенностей», тем надежнее проект. Платить есть смысл за реализацию задач, а не знание языка. Более того, если у чела есть ряд качеств, на пробелы в знании языка можно закрыть глаза.
    • +1
      Давно заметил, что тесты проходят, если реализация интерфейса добавляет дефолтное значение. Но я ругаюсь на такое

      И зря — интерфейс это декларация соглашения о вызовах, поэтому под интерфейс может подпадать любой метод который может быть вызван как написано в интерфейсе
      Если в нескольких местах у вас требуется именно implements A,B,C то следует его объединить в один интерфейс, т.к. будет момент когда вам захочется написать function foo(A+B+C $var)
      Заодно это сразу подскажет что в данном месте это именно связанные реализации
      • –1
        Возможность пропихнуть «прицеп» из необязательных параметров в любой интерфейс вообще обнуляет ценность интерфейса. Смотришь на красивую декларацию, а там на самом деле скрытые зависимости. Это недочет в дизайне движка, чтобы сохранить совместимость с пятой версией. Можно сгладить недочеты языка, если ограничивать себя и следовать строгим принципам кодописания.

        В контексте собеседования — это вообще запредельное буквоедство. Если чел педант и любит строгое соответствие декларациям, но не знает, что есть совместимость методов? Экзамен в универе может и не пройдет, но для работы очень даже привлекательный персонаж.
        • +1
          Откуда скрытые зависимости? Это же не обязательные параметры, любой вызов метода из интерфейса будет гарантированно вызван.
          Совместимость с РНР5 тут вообще не причём, это утиная типизация и такой подход не только в РНР
          • –1

            Ну ок, например есть интерфейс отрисовки "фигур", некий упрощенный кусок проекта с планировками зданий:


            interface DrawShapesInterface {
            
                public function drawCircle(int $x, int $y);
                // ... прочие фигуры аналогично
            }
            

            Потом некий умник делает следующее, ведь PHP позволяет:


            class SimpleDrawer implements DrawShapesInterface {
            
                public function drawCircle(int $x, int $y, ...$args)
                {
                   // отрисовка, которая зависит от дополнительных аргументов:
                   // кисти, параметры цвета и т.п.
                   // ведь так просто подсунуть в интерфейс еще и прицеп    
                }
            }
            

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

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