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

Источник: «Interfaces vs Abstract Classes in PHP»
Недавно я опубликовал статью об улучшении PHP кода с помощью интерфейсов. Она охватывает основы того что такое интерфейс, что он может делать. И как вы можете использовать его сделав свой PHP код более расширяемым и поддерживаемым. Один из вопросов, заданных в комментариях к статье был от разработчиков, которые хотели знать "когда я должен использовать интерфейс вместо абстрактного класса?". Я подумал и решил написать статью, что бы объяснить различия между абстрактными классами и интерфейсами в PHP и дать краткий обзор того, когда вы должны использовать каждый из них.

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

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

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

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

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

interface HomeInterface
{
const MATERIAL = 'Brick';

public function openDoor(): void;

public function getRooms(): array;

public function hasGarden(): bool;
}

а не как в следующем примере:

interface HomeInterface
{
public string $material = 'Brick';

public function openDoor(): void
{
// Открываем дверь...
}

public function getRooms(): array
{
// Даём информацию о комнатах...
}

public function hasGarden(): bool
{
// Задаём есть ли у дома сад...
}
}

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

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

Используя наш выше описанный интерфейс и придерживаясь аналогии с домом, мы могли бы создавать различные классы реализующие HomeInterface такие, как House, Flat или Caravan. Используя интерфейс, мы можем быть уверены, что наш класс содержит три необходимых метода и все используют правильную сигнатуру метода. Например, у нас может быть класс House, который выглядит так:

class House implements HomeInterface
{
public function openDoor(): void
{
// Открываем дверь здесь...
}

public function getRooms(): array
{
// Даём информацию о комнатах...
}

public function hasGarden(): bool
{
// Задаём есть ли у дома сад...
}
}

Что такое Абстрактные классы

Абстрактные классы PHP очень похожи на интерфейсы PHP; они не предназначены быть классами сами по себе и представляют базовые методы без реализации.

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

Абстрактный класс может:

Абстрактный класс не может:

Что бы понять, что это означает, рассмотрим пример абстрактного класса:

abstract class House
{
const MATERIAL = 'Brick';

abstract public function openDoor(): void;

public function getRooms(): array
{
return [
'Bedroom',
'Bathroom',
'Living Room',
'Kitchen',
];
}

public function hasGarden(): bool
{
return true;
}
}

Наш класс House объявлен abstract — это означает, что мы не можем использовать его напрямую. Для использования нужно его унаследовать. Например, давайте создадим класс MyHouse, который расширяет абстрактный класс House:

class MyHouse extends House
{
public function openDoor(): void
{
// Открываем дверь...
}

public function getRooms(): array
{
return [
'Bedroom One',
'Bedroom Two',
'Bathroom',
'Living Room',
'Kitchen',
];
}
}
// Это не будет работать:
$house = new House();

// Это будет работать:
$house = new MyHouse();

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

В этом случае дочерний класс мог бы переопределить методы getRooms() и hasGarden(), но не должен был бы их включать. Что продемонстрировать это, мы переопределили метод getRooms() показав, как мы можем изменить его поведение в дочернем классе.

Как решить, что использовать

Это зависит от вашей цели. Сохранив аналогию с домом, если вы создаёте чертежи, которые в дальнейшем можно использовать для проектирования домов разных типов, вам нужен интерфейс.

Если вы построили дом и вам нужно создать копии с улучшениями, то вам нужен абстрактный класс.

Приведу несколько примеров:

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

Чтобы помочь понять, когда нужно использовать интерфейс, давайте рассмотрим пример. Допустим, у нас ест класс ConstructionCompany включающий метод buildHome(), который выглядит следующим образом:

class ConstructionCompany
{
public function buildHome($home)
{
// Строим дом...

return $home;
}
}

Теперь предположим, что у нас есть 3 разных класса, которые мо хотим создать и передать методу buildHome():

  1. class MyHouse implements HomeInterface extends House
  2. class MyCaravan implements HomeInterface
  3. class MyFlat implements HomeInterface

Как мы видим, класс MyHouse расширяет абстрактный класс House; и это имеет смысл к концептуальной точки зрения, потому что дом есть дом. Однако класс MyCaravan или MyFlat не имеет смысла расширять от абстрактного класса, потому что ни один из них не является домом.

Итак, поскольку наша строительная компания может строить дома, фургоны и квартиры, это правило исключает указание параметра $home в методе buildHome() как экземпляра House.

Однако это было бы идеальным местом для указания типа нашему методу, что бы разрешить принимать только классы реализующие HomeInterface. В качестве примера мы могли бы обновить метод следующим образом:

class ConstructionCompany
{
public function buildHome(HomeInterface $home)
{
// Строим дом...

return $home;
}
}

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

Вы могли подумать: Почему бы нам просто не создать абстрактный класс Home вместо интерфейса?. Однако важно помнить, что PHP поддерживает только одиночное наследование и что класс не может расширять более одного родителя. Таким образом, это будет довольно сложно, если вы захотите расширить один из своих классов в будущем.

Когда использовать Абстрактный класс

Возьмём сценарий аналогичный приведённому выше. Давайте представим, что у нас есть HouseConstructionCompany, похожая на нашу ConstructionCompany. Но в этом примере предположим, что компания HouseConstructionCompany строит только дома и ничего больше.

Поскольку мы знаем, что нам нужно уметь строить только дома, мы можем указать, что бы наш метод принимал только те классы, которые являются расширением абстрактного класса Home. Это может быть действительно полезно, так как мы всегда будем уверены, что передаём типы только тех домов которые строительная компания строит. Например:

class HouseConstructionCompany
{
public function buildHouse(House $house)
{
// Build the house here...

return $house;
}
}

Вывод

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

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

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

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

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

Laravel: Использование транзакций