Интерфейс или абстрактный класс: что выбрать

Вопрос, который задают почти всегда — и почти всегда получают заученный ответ про «множественное наследование».

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

Вопрос 1: «Чем интерфейс отличается от абстрактного класса?»

Что проверяет интервьюер

Формальные различия кандидат обычно перечисляет. Интервьюеру же интересно, умеете ли вы выбирать: он ждёт критерия проектирования, а не таблицы из учебника. Сильный ответ — «интерфейс отвечает на вопрос что умеет объект, абстрактный класс — как он это делает частично».

Признакinterfaceabstract class
Сколько можно реализовать/унаследоватьсколько угодноровно один
Реализация методовнет (только сигнатуры)есть у неабстрактных методов
Свойстванельзяможно, любой видимости
Константыможноможно
Видимость методовтолько publicpublic, protected, private
Конструкторобычно не объявляютможно, в том числе с промоушеном свойств

Как это выглядит в коде

<?php
interface Exportable {
    public function export(): string;
}

abstract class Report implements Exportable {
    public function __construct(protected string $title) {}

    public function export(): string {
        return $this->header() . " | " . $this->body();
    }

    protected function header(): string {
        return "Отчёт: {$this->title}";
    }

    abstract protected function body(): string;
}

class SalesReport extends Report {
    protected function body(): string { return "продажи: 120"; }
}

$r = new SalesReport('за июль');
echo $r->export(), "\n";
var_dump($r instanceof Exportable, $r instanceof Report);

try {
    new Report('x');
} catch (Error $e) {
    echo "Error: ", $e->getMessage(), "\n";
}

Вывод:

Отчёт: за июль | продажи: 120
bool(true)
bool(true)
Error: Cannot instantiate abstract class Report

Здесь видно разделение ролей. Интерфейс Exportable — то, чем отчёт является для внешнего мира: любой код может принять Exportable и не знать про Report вовсе. Абстрактный класс задаёт скелет алгоритма (шаблонный метод export()) и требует от потомка только одну недостающую деталь — body(). Попытка создать абстрактный класс напрямую даёт Error, а не Exception: это ошибка программиста, а не ситуация времени выполнения.

Вопрос 2: «Когда что применять на практике?»

  • Интерфейс — когда нужно описать возможность, которую могут дать совершенно разные классы: LoggerInterface, CacheInterface, PaymentGateway. Тип-подсказка в конструкторе должна быть интерфейсом — это то, что делает код тестируемым и позволяет подменить реализацию.
  • Абстрактный класс — когда у нескольких классов есть общий, действительно одинаковый кусок кода и общее состояние, а различается один-два шага. Классика — шаблонный метод, как в примере выше.
  • Ни то, ни другое — если общий код можно просто вынести в отдельный сервис и передать в конструктор. Композиция гибче наследования: наследоваться можно один раз, а внедрить зависимость — сколько угодно.

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

Вопрос 3: «Может ли интерфейс наследовать интерфейс?»

Да, причём сразу несколько: interface A extends B, C {} — это законно, в отличие от классов. Кроме того, с PHP 8.0 интерфейсы могут содержать константы, а с PHP 8.1 доступны readonly-свойства и enum, которые тоже умеют реализовывать интерфейсы. На всякий случай стоит помнить: класс, реализующий интерфейс, не имеет права сужать видимость или менять сигнатуру несовместимым образом — иначе фатальная ошибка на этапе компиляции файла.

interface Identifiable {
    const PREFIX = 'ID-';
    public function getId(): string;
}

interface Nameable {
    public function getName(): string;
}

// интерфейс может расширять несколько интерфейсов
interface Entity extends Identifiable, Nameable {}

final class User implements Entity {
    public function __construct(
        private int $id,
        private string $name,
    ) {}

    public function getId(): string { return self::PREFIX . $this->id; }
    public function getName(): string { return $this->name; }
}

Типичные ошибки кандидатов

  • Говорят «интерфейс нужен для множественного наследования» и на этом останавливаются — не объясняя, зачем вообще нужен контракт.
  • Утверждают, что в интерфейсе можно объявлять свойства. Нельзя: только методы и константы.
  • Считают, что абстрактный класс обязан иметь хотя бы один абстрактный метод. Не обязан — достаточно ключевого слова abstract, чтобы запретить создание экземпляров.
  • Забывают, что методы интерфейса всегда public, и пытаются сделать реализацию protected.
  • По привычке строят глубокие иерархии наследования там, где хватило бы одного интерфейса и внедрения зависимости.

Как ответить кратко

«Интерфейс — контракт без реализации и без состояния, их можно реализовать сколько угодно; абстрактный класс — заготовка с общим кодом и свойствами, наследовать можно только один и создать экземпляр нельзя. Интерфейс отвечает на вопрос "что объект умеет", абстрактный класс — "как частично сделано". Тип-подсказки я ставлю по интерфейсам, общий код чаще выношу композицией, а абстрактный класс беру под шаблонный метод».

Проверьте себя
1. Что можно объявить в интерфейсе PHP?
AМетоды, константы и приватные свойства
BТолько сигнатуры public-методов и константы
CЛюбые методы с реализацией по умолчанию
DМетоды и типизированные свойства без значений
2. Обязан ли абстрактный класс содержать хотя бы один абстрактный метод?
AДа, иначе будет фатальная ошибка
BДа, но только если он реализует интерфейс
CНет, ключевого слова abstract достаточно, чтобы запретить создание экземпляров
DНет, но тогда его нельзя наследовать
3. Что произойдёт при попытке выполнить new Report('x'), если Report объявлен как abstract class?
AБудет создан объект с null-полями
BВернётся null
CБудет вызван конструктор ближайшего потомка
DБудет брошен Error: Cannot instantiate abstract class