PHP: Используем Интерфейсы для улучшения кода

Источник: «Using Interfaces to Write Better PHP Code»
В программировании важно убедится, что ваш код легко читается, поддерживается, расширяется и тестируется. Один из способов улучшить эти факторы — использовать интерфейсы.

Целевая аудитория

Эта статья предназначена для разработчиков, которые имеют базовые представления о концепциях ООП (объектно-ориентированного программирования) и использовании наследования в PHP. Если вы знаете, как использовать наследование в своём PHP коде, мы надеемся, что эта статья будет понятной.

Что такое Интерфейсы

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

Интерфейсы должны:

Интерфейсы не должны:

Интерфейсы используются для определения публичных методов, которые должен включать класс. Важно помнить, что определены только сигнатуры методов и что они не включают тело метода (как вы обычно видите в методе класса). Это связано с тем, что интерфейсы используются только для определения взаимодействия между объектами, а не для определения взаимодействия и поведения, как в классе. Что дать немного контекста, в этом примере показан пример интерфейса, который определяет несколько общедоступных методов:

interface DownloadableReport
{
public function getName(): string;

public function getHeaders(): array;

public function getData(): array;
}

Согласно php.net, интерфейсы служат нескольким целям:

  1. Позволить разработчикам создавать объекты разных классов, которые могут использоваться взаимозаменяемо, поскольку они реализуют один и тот же интерфейс или интерфейсы. Типичный пример — несколько служб доступа к базе данных, несколько платёжных шлюзов или разные стратегии кэширования. Различные реализации могут быть заменены, не требуя каких-либо изменений в коде, который их использует.
  2. Позволить функции или методу принимать и оперировать параметром, который соответствует интерфейсу, не заботясь о том, что ещё может делать объект или как он реализован. Эти интерфейсы часто называют Iterable, Cacheable, Renderable и т.д., чтобы описать поведение.

Использование Интерфейсов в PHP

Интерфейсы могут быть бесценной частью кодовых баз ООП (объектно-ориентированного программирования). Они позволяют нам отделить наш код и улучшить расширяемость. Для иллюстрации этого, давайте рассмотрим следующий класс:

class BlogReport
{
public function getName(): string
{
return 'Blog report';
}
}

Как вы видите, мы определили класс с методом возвращающим строку. Таким образом, мы определили поведение метода, мы можем увидеть как getName() формирует возвращаемую строку. Однако допустим, что мы вызываем этот метод в нашем коде внутри другого класса. Другой класс не заботит то, как была сформирована эта строка, его заботит только то, что бы она была возвращена. Например, давайте посмотрим, как мы могли бы вызвать этот метод в другом классе:

class ReportDownloadService
{
public function downloadPDF(BlogReport $report)
{
$name = $report->getName();

// Download the file here...
}
}

Хотя вышеприведённый код работает, давайте представим, что мы хотим добавить функциональность для загрузки пользовательского отчёта из класса UserReport. Конечно, мы не можем использовать существующий метод в нашем ReportDownloadService, потому что мы установили, что можно передавать только класс BlogReport. Итак, нам нужно переименовать существующий метод, а затем добавить новый метод, как показано ниже:

class ReportDownloadService
{
public function downloadBlogReportPDF(BlogReport $report)
{
$name = $report->getName();

// Download the file here...
}

public function downloadUsersReportPDF(UsersReport $report)
{
$name = $report->getName();

// Download the file here...
}
}

Хотя вы этого не видите, давайте предположим, что остальные методы в приведённом выше классе используют идентичный код для создания загрузки. Мы могли бы превратить их общий код в методы, но, вероятно, у нас всё равно будет какой-то общий код. Кроме того, у нас будет несколько точек входа в класс, выполняющий почти идентичный код. Это потенциально может привести к дополнительной работе в будущем при попытке расширить код или добавить тесты.

Представим, что мы создаём новый AnalyticsReport; нам нужно добавить в класс новый метод downloadAnalyticsReportPDF(). Как вы видите этот файл может начать быстро расти. Это может быть идеальным местом для использования интерфейсов.

Давайте создадим один; назовём его DownloadableReport и определим так:

interface DownloadableReport
{
public function getName(): string;

public function getHeaders(): array;

public function getData(): array;
}

Теперь вы можете обновить BlogReport и UsersReport для реализации интерфейса DownloadableReport, как показано в примере ниже. Но обратите внимание, я намеренно неправильно написал код для UsersReport, что бы кое-что продемонстрировать!

class BlogReport implements DownloadableReport
{
public function getName(): string
{
return 'Blog report';
}

public function getHeaders(): array
{
return ['The headers go here'];
}

public function getData(): array
{
return ['The data for the report is here.'];
}
}
class UsersReport implements DownloadableReport
{
public function getName()
{
return ['Users Report'];
}

public function getData(): string
{
return 'The data for the report is here.';
}
}

Если бы мы попытались запустить наш код, мы бы получили ошибки по следующим причинам:

  1. Пропущен метод getHeaders().
  2. У метода getName() не задан тип возвращаемого значения. Но в сигнатуре метода интерфейса тип определён.
  3. У метода getData() задан тип возвращаемого значения, он не соответствует определённому в сигнатуре метода интерфейса.

Итак, что бы обновить UsersReport, для правильной реализации интерфейса DownloadableReport, нужно заменить его на следующий код:

class UsersReport implements DownloadableReport
{
public function getName(): string
{
return 'Users Report';
}

public function getHeaders(): array
{
return [];
}

public function getData(): array
{
return ['The data for the report is here.'];
}
}

Теперь, когда оба класса отчётов реализуют один и тот же интерфейс, мы можем обновить ReportDownloadService следующим образом:

class ReportDownloadService
{
public function downloadReportPDF(DownloadableReport $report)
{
$name = $report->getName();

// Download the file here...
}
}

Теперь мы можем передать объект UsersReport или BlogReport в метод downloadReportPDF() без каких либо ошибок. Потому что мы знаем, что необходимые методы нужные для классов отчёта существуют и возвращают данные ожидаемого типа.

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

Если бы мы захотели создать новый AnalyticsReport, мы могли бы заставить его реализовать тот же интерфейс, и это позволило бы нам передать объект отчёта в тот же метод downloadReportPDF() без необходимости добавлять какие-либо новые методы. Это может быть полезно, если вы хотите создать свой пакет или фреймворк, и хотите дать разработчику возможность создавать свой собственный класс. Вы можете просто сказать им, какой интерфейс реализовать, и они смогут создать собственный класс. Например, в Laravel вы можете создать собственный класс драйвера кэша, реализовав интерфейс Illuminate\Contracts\Cache\Store.

Помимо использования интерфейсов для улучшения актуального кода, мне нравятся интерфейсы, потому что они действуют как концепция «Документация — это код». Например, если я пытаюсь выяснить что может класс, а что не может, я обычно сначала смотрю на интерфейс, а затем на класс, который его реализует. Он сообщает вам все методы, которые могут быть вызваны, и не нужно заботиться о том, что у них под капотом.

Как заметили многие мои читатели Laravel-разработчик, довольно часто термины «контракт» и «интерфейс» используются как взаимозаменяемые. Согласно документации Laravel, контракты Laravel — это наборы интерфейсов, которые определяют основные сервисы предоставляемые фреймворком. Итак, важно помнить, что контракт — это интерфейс, но интерфейс необязательно является контрактом. Обычно контракт — это просто интерфейс, предоставляемый фреймворком. Для получения информации об использовании контрактов я рекомендую почитать документацию поскольку в ней хорошо разбирается то как их использовать и когда их использовать.

Вывод

Надеюсь, что прочитав эту статью вы получили краткий обзор того, что такое интерфейсы, как их использовать в PHP, об их преимуществах и использовании.

Дополнительные материалы

Предыдущая Статья

Laravel: Как создать функцию хелпер

Следующая Статья

PHP: Интерфейсы vs Абстрактные классы