PHP: Интерфейсы vs Абстрактные классы
Что такое Интерфейсы
В общих чертах, Интерфейс должен описывать, как будет построен класс реализующий его, это похоже на план, описывающий публичные методы и константы, которые должны быть реализованы.
Интерфейсы должны:
- Использоваться для определения публичных методов класса.
- Использоваться для определения констант класса.
Интерфейсы не должны:
- Использоваться сами по себе.
- Использоваться для определения приватных или защищённых методов класса.
- Использоваться для определения свойств класса.
Интерфейсы используются для определения публичных методов, которые должен включать класс. Важно помнить, что интерфейс всегда предназначен для реализации классом, поэтому в нём вы определяете только сигнатуру метода, например:
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, интерфейсы служат нескольким целям:
- Позволить разработчикам создавать объекты разных классов, которые могут использоваться взаимозаменяемо, поскольку они реализуют один и тот же интерфейс или интерфейсы. Типичный пример — несколько служб доступа к базе данных, несколько платёжных шлюзов или разные стратегии кэширования. Различные реализации могут быть заменены, не требуя каких-либо изменений в коде, который их использует.
- Позволить функции или методу принимать и оперировать параметром, который соответствует интерфейсу, не заботясь о том, что ещё может делать объект или как он реализован. Эти интерфейсы часто называют
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
методов (аналогично интерфейсам). - Использоваться для определения методов.
- Использоваться для определения констант класса.
- Использоваться для определения свойств класса.
- Расширятся дочерним классом.
Абстрактный класс не может:
- Использоваться самостоятельно
Что бы понять, что это означает, рассмотрим пример абстрактного класса:
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()
:
class MyHouse implements HomeInterface extends House
class MyCaravan implements HomeInterface
class MyFlat implements HomeInterface
Как мы видим, класс MyHouse
расширяет абстрактный класс House
; и это имеет смысл к концептуальной точки зрения, потому что дом есть дом. Однако класс MyCaravan
или MyFlat
не имеет смысла расширять от абстрактного класса, потому что ни один из них не является домом.
Итак, поскольку наша строительная компания может строить дома, фургоны и квартиры, это правило исключает указание параметра $home
в методе buildHome()
как экземпляра House
.
Однако это было бы идеальным местом для указания типа нашему методу, что бы разрешить принимать только классы реализующие HomeInterface
. В качестве примера мы могли бы обновить метод следующим образом:
class ConstructionCompany
{
public function buildHome(HomeInterface $home)
{
// Строим дом...
return $home;
}
}
В результате мы можем быть уверенными, передавая значение дом, фургон или квартира, что наш класс ConstructionCompany получит необходимую информацию. Переданный объект home
всегда будет содержать необходимые нам методы.
Вы могли подумать: Почему бы нам просто не создать абстрактный класс
. Однако важно помнить, что PHP поддерживает только одиночное наследование и что класс не может расширять более одного родителя. Таким образом, это будет довольно сложно, если вы захотите расширить один из своих классов в будущем.Home
вместо интерфейса?
Когда использовать Абстрактный класс
Возьмём сценарий аналогичный приведённому выше. Давайте представим, что у нас есть HouseConstructionCompany
, похожая на нашу ConstructionCompany
. Но в этом примере предположим, что компания HouseConstructionCompany
строит только дома и ничего больше.
Поскольку мы знаем, что нам нужно уметь строить только дома, мы можем указать, что бы наш метод принимал только те классы, которые являются расширением абстрактного класса Home
. Это может быть действительно полезно, так как мы всегда будем уверены, что передаём типы только тех домов которые строительная компания строит. Например:
class HouseConstructionCompany
{
public function buildHouse(House $house)
{
// Build the house here...
return $house;
}
}
Вывод
Надеюсь эта статья даст представление о различиях между интерфейсами и абстрактными классами в PHP. А так же различных сценариев того, когда следует их использовать.
- PHP: Разница между Трейтом, Интерфейсом и Абстрактным классом
- PHP: Используем Интерфейсы для улучшения кода