Интерфейс или абстрактный класс: что выбрать
Вопрос, который задают почти всегда — и почти всегда получают заученный ответ про «множественное наследование».
Интерфейс — это контракт: список публичных методов без реализации. Абстрактный класс — это частично готовая реализация, которую нельзя создать напрямую и у которой часть методов оставлена потомкам.
Вопрос 1: «Чем интерфейс отличается от абстрактного класса?»
Что проверяет интервьюер
Формальные различия кандидат обычно перечисляет. Интервьюеру же интересно, умеете ли вы выбирать: он ждёт критерия проектирования, а не таблицы из учебника. Сильный ответ — «интерфейс отвечает на вопрос что умеет объект, абстрактный класс — как он это делает частично».
| Признак | interface | abstract class |
| Сколько можно реализовать/унаследовать | сколько угодно | ровно один |
| Реализация методов | нет (только сигнатуры) | есть у неабстрактных методов |
| Свойства | нельзя | можно, любой видимости |
| Константы | можно | можно |
| Видимость методов | только public | public, 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. - По привычке строят глубокие иерархии наследования там, где хватило бы одного интерфейса и внедрения зависимости.
Как ответить кратко
«Интерфейс — контракт без реализации и без состояния, их можно реализовать сколько угодно; абстрактный класс — заготовка с общим кодом и свойствами, наследовать можно только один и создать экземпляр нельзя. Интерфейс отвечает на вопрос "что объект умеет", абстрактный класс — "как частично сделано". Тип-подсказки я ставлю по интерфейсам, общий код чаще выношу композицией, а абстрактный класс беру под шаблонный метод».