ООП в PHP: Абстрактные классы и интерфейсы

ООП в PHP: Абстрактные классы и интерфейсы

Одной из вещей, которая делает код более расширяемым, является полиморфизм. Я уже упоминал про него в нескольких предыдущих статьях. Напомню, что полиморфизм проявляется, когда версия переопределённого метода в цепочке наследования определяется прямо при его вызове исходя из типа объекта, от которого этот метод вызывается. Для того чтобы убедиться в наличии нужного нам метода у объекта можно проверять его тип при помощи оператора instanceof или уточнить тип аргумента метода. В этом случае, конечно, метод должен быть реализован хотя бы в том классе, имя которого мы указываем при использовании оператора instanceof или задаём в качестве типа аргумента. Теперь представьте, что нам нужно задать, что нужно уметь делать экземплярам заданного типа, но мы пока не знаем как они это будут делать. То есть нам необходимо обязать иметь реализацию некоторого метода в ветке наследования.
Давным-давно, когда абстрактных классов и интерфейсов в php не было, это можно было сделать примерно вот так:

class Animal {
function say() {
die ("Этот метод должен иметь реализацию в наследниках класса " . __CLASS__);
}
}

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

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

Абстрактные классы могут содержать описание абстрактных методов. Для таких методов указывается лишь заголовок с ключевым словом abstract и всеми прочими атрибутами, указываемыми при объявлении метода. Абстрактные методы не имеют тела или реализации, они лишь описывают, что должен уметь делать объект, а как он это будет делать – проблема наследников абстрактного класса.
Экземпляр абстрактного класса создавать нельзя, так как в противном случае могла произойти попытка вызвать от этого экземпляра абстрактный метод, что абсурдно, так как он не имеет реализации.
Объявление абстрактного класса начинается с ключевого слова abstract. Давайте сделаем наш класс Animal и его метод say абстрактными.

abstract class Animal {
abstract public function say();
}

Теперь класс, унаследованный от класса Animal, обязан будет содержать реализацию метода say или должен быть объявлен абстрактным, в противном случае ещё до начала выполнения скрипта произойдёт ошибка.

/*
* Fatal error: Class Cat contains 1 abstract method 
* and must therefore be declared abstract or implement 
* the remaining methods (Animal::say)
*/
class Cat extends Animal {  
public function __construct($name) {
$this->name = $name;
}
}

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

abstract class Animal {
private $name;
 
public function __construct($name) {
$this->name = $name;
}
 
abstract public function say();
 
public function getName() {
return $this->name;
}
}

Давайте унаследуем от нашего абстрактного класса Animal ещё и класс Dog, описывающий собаку, и добавим в него, и в класс Cat реализации метода Say. Обратите внимание, что в конструкторах этих классов вызываются конструктор абстрактного класса-предка.

class Cat extends Animal {
 
public function __construct($name) {
parent::__construct($name);
}
 
public function say() {
echo "meow-meow";
}
}
 
class Dog extends Animal {
 
public function __construct($name) {
parent::__construct($name);
}  
 
public function say() {
echo "woof-woof!";
}
}

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

Интерфейсы

Интерфейс, в отличие от абстрактного класса, не может содержать поля и методы, имеющие реализацию – он описывает только чистый функционал в виде абстрактных методов, которые должны реализовать его наследники. Имя интерфейса, так же как и имя абстрактного класса, можно использовать для уточнения типа параметра метода или указывать в качестве правого операнда оператора instanceof. Для объявления интерфейса используется ключевое слово interface. Давайте переместим абстрактный метод say из класса Animal в интерфейс.

interface СanSay {
public function say();
}

В отличие от абстрактных классов про интерфейсы чаще говорят, что классы их не наследуют, а имплементируют или реализуют. Если в классе, который реализует интерфейс, не реализованы все методы интерфейса, то он должен быть абстрактным.
Пусть наш класс Animal, который уже не содержит метод say, будет имплементировать интерфейс CanSay. Классы Dog и Cat при этом останутся его наследниками класса Animal.

abstract class Animal implements CanSay {
private $name;
 
public function __construct($name) {
$this->name = $name;
}
 
public function getName() {
return $this->name;
}
}

Теперь для того, чтобы проверить, что объект может говорить, нужно удостовериться что класс, которому он принадлежит, реализует интерфейс CanSay. Сделать это можно указав в качестве типа нужного аргумента метода имя интерфейса, или использовав оператор instanceof.

Перенос абстрактного метода say в отдельный класс сделал наш код более расширяемым. Теперь можно реализовать интерфейс CanSay и в других классах, которые не являются наследниками класса Animal, и тот функционал, которому было нужно, чтобы объекты, которыми он оперирует, умели говорить (реализовывали интерфейс CanSay), не изменится.

Стоит так же отметить ещё одно отличие интерфейсов от абстрактных классов – один класс может реализовывать сколь угодно много интерфейсов. Для этого их имена просто нужно перечислить через запятую после ключевого слова implements. Унаследовать же несколько абстрактных классов нельзя. Это связанно с тем, что в абстрактных классах могут содержаться различные реализации не абстрактных методов с одинаковыми именами, и при наследовании этих классов не ясно, какую реализацию унаследовать потомку. В интерфейсах же реализованные методы отсутствуют и, если класс имплементирует несколько интерфейсов, в которых содержаться абстрактные методы с одинаковыми именами, то какой из этих методов будет реализован в классе безразлично.

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *