Перейти к содержимому
Теория · Наследование и интерфейсы · урок 3 из 8

Абстрактные классы и интерфейсы

В прошлом уроке дочерний класс получал от родителя готовые методы. Но бывает, что родитель сам не знает, как сделать часть работы. Возьмём фигуры: у квадрата и у круга есть площадь, но считают её по-разному, а построить «фигуру вообще» нельзя.

В семействе чертежей это чертёж-заготовка: стены и крыша нарисованы, а на месте кухни написано «дорисуйте сами». По заготовке дом не строят, её сначала дополняют.

Есть и другой документ — должностная инструкция. В ней перечислено, что работник обязан уметь, но не сказано, как именно это делать.

В PHP заготовка — это абстрактный класс, а инструкция — интерфейс.

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

Абстрактный класс — класс с пометкой abstract, объект которого создать нельзя: он существует только как родитель для других классов.

PHP
<?php

abstract class Shape
{
    public function __construct(protected float $size) {}

    abstract public function area(): float;

    public function describe(): string
    {
        return $this::class . ' ' . $this->area();
    }
}

Разберём по шагам:

  1. abstract class Shape объявляет заготовку: построить объект Shape нельзя.
  2. Конструктор и свойство $size такие же, как у обычного класса: данные абстрактный класс хранить может.
  3. abstract public function area(): float; — абстрактный метод. У него есть только сигнатура, то есть имя, параметры и тип результата, а вместо тела стоит точка с запятой. Каждый дочерний класс обязан его написать.
  4. describe() — готовый метод. Он вызывает $this->area(), хотя в Shape у этого метода нет тела: как вы уже знаете, $this найдёт версию из класса объекта. А $this::class даёт строкой имя класса объекта.

Здесь картинка с заготовкой неточна: саму заготовку в PHP не дорисовывают, а пишут дочерний класс с недостающими методами.

Интерфейс

Интерфейс — список методов, которые класс обещает иметь, без их кода. Его объявляют словом interface, а класс подписывается на него словом implements:

PHP
interface HasCorners
{
    public function corners(): int;
}
class Square extends Shape implements HasCorners
{
    public function area(): float
    {
        return $this->size ** 2;
    }
    public function corners(): int
    {
        return 4;
    }
}
  1. interface HasCorners объявляет один метод corners() без тела. Методы интерфейса всегда public.
  2. Square наследует Shape и реализует HasCorners, поэтому пишет оба метода: area() для родителя и corners() для интерфейса.
  3. Родитель у класса один, а интерфейсов может быть сколько угодно: implements A, B.

Кроме методов, интерфейс может объявлять константы. Свойств со значениями и готового кода в нём не бывает. С PHP 8.4 интерфейс может требовать и свойство, но значения у такого свойства в интерфейсе всё равно нет.

Если в классе нет одного из методов интерфейса, PHP остановит скрипт с фатальной ошибкой на объявлении класса:

TEXT
Fatal error: Class Square contains 1 abstract method and must therefore be declared abstract or implement the remaining method (HasCorners::corners)

Абстрактный класс или интерфейс

Разницу между абстрактным классом и интерфейсом часто спрашивают на собеседовании по PHP. Ответ удобно держать в голове таблицей:

Абстрактный классИнтерфейс
Свойства со значениямиможнонельзя
Методы с теломможнонельзя
Сколько у одного классаодин, через extendsсколько угодно, через implements
Объект через newнельзянельзя

Абстрактный класс берут, когда у семейства классов общие данные и общий код. Интерфейс — когда разным классам нужно одно и то же умение, а код у каждого свой.

instanceof и полиморфизм

Добавим круг и посмотрим на фигуры вместе:

PHP
class Circle extends Shape
{
    public function area(): float
    {
        return 3.14 * $this->size ** 2;
    }
}

$sq = new Square(3);
foreach ([$sq, new Circle(1)] as $shape) {
    echo $shape->describe(), '; '; // Square 9; Circle 3.14;
}
echo "\n";
var_dump($sq instanceof Shape); // bool(true)
var_dump(new Circle(1) instanceof HasCorners); // bool(false)

Давайте разберём, что напечаталось:

  1. Цикл вызывает у каждой фигуры один и тот же describe(), а площадь каждая считает по-своему. Это полиморфизм из прошлого урока: код цикла не знает, квадрат перед ним или круг.
  2. area() у квадрата возвращает 9.0 типа float, а echo печатает такое число без .0 — как 9.
  3. instanceof проверяет, относится ли объект к классу или интерфейсу. $sq instanceof Shape даёт true, хотя $sq — объект Square: проверка проходит по всей цепочке родителей и по всем интерфейсам.
  4. Circle не реализует HasCorners, поэтому второй ответ — false. Если слева от instanceof стоит не объект, ответ тоже false, а не ошибка.

Здесь аналогия с инструкцией неточна. Человек может уметь всё, что в ней написано, и без подписи. А в PHP instanceof HasCorners даёт true только для класса, который написал implements HasCorners: одного метода corners() мало.

Частая ошибка

Пытаются создать объект абстрактного класса:

PHP
$shape = new Shape(2); // ✗
TEXT
Fatal error: Uncaught Error: Cannot instantiate abstract class Shape

Абстрактный класс — заготовка, и PHP не даёт построить по ней объект. С интерфейсом то же самое: new HasCorners() даёт Cannot instantiate interface HasCorners. Исправление — создать объект дочернего класса, например new Square(2).

Главное

  • Абстрактный класс — заготовка: объект из него не создать, но в нём бывают свойства, конструктор, готовые и абстрактные методы.
  • Интерфейс перечисляет методы без тел; класс подписывается через implements и обязан написать каждый метод.
  • Родитель у класса один, а интерфейсов может быть сколько угодно.
  • instanceof истинен для класса объекта, всех его родителей и всех реализованных интерфейсов.