<? phpukraine СПІВБЕСІДИ
Пошук по платформі
CORE PHP · JUNIOR ЧАСТО ПИТАЮТЬ

Чим інтерфейс відрізняється від абстрактного класу і коли що обирати?

Інтерфейс — це публічний контракт без стану, і їх можна реалізувати скільки завгодно; абстрактний клас — частково готова реалізація зі станом, конструктором і protected-методами, але успадкувати можна лише один.

Навіщо інтерфейс, якщо абстрактний клас теж уміє оголошувати методи без тіла?
Скільки інтерфейсів можна реалізувати і скільки класів успадкувати?
Чи можна в інтерфейсі оголосити властивість або protected-метод?
У вас два класи з однаковим кодом усередині — зробите спільного батька чи інтерфейс?
ООП interface abstract class наслідування PHP 8.4 SOLID

На рівні мови інтерфейс — це оголошення публічного API без жодної реалізації: список сигнатур методів, константи і (з PHP 8.4) вимоги до властивостей у формі хуків public string $name { get; }. Усі методи інтерфейсу автоматично публічні, спроба написати protected function дає фатальну помилку «Access type for interface method must be public». Абстрактний клас — це звичайний клас, якому заборонено new: у ньому вільно живуть властивості, конструктор, private/protected-методи, готові реалізації і поряд з ними методи з модифікатором abstract, тобто без тіла. Обидва не інстанціюються, обидва створюють повноцінний тип, який можна вказати в type hint і перевірити через instanceof.

Найважливіша практична різниця — не «є тіло чи немає», а кількість. Клас має рівно одного батька (extends), а інтерфейсів реалізує скільки завгодно (implements A, B, C); сам інтерфейс теж може успадкувати одразу кілька інтерфейсів. Тому наслідування — це ресурс, який витрачається один раз, і витрачати його варто на головну вісь класифікації. Саме звідси випливає критерій вибору: інтерфейс відповідає на питання «що обʼєкт уміє» і легко комбінується (Arrayable, JsonSerializable, PaymentGateway), абстрактний клас відповідає «як він це частково робить» і задає жорстке відношення «is-a».

Найчастіше їх не протиставляють, а поєднують, як у прикладі: інтерфейс PaymentGateway описує контракт, абстрактний HttpGateway implements PaymentGateway тримає спільний стан (HTTP-клієнт, ключ) і спільний алгоритм, а нащадок дописує лише те, що справді відрізняється — це шаблонний метод (Template Method). Абстрактний клас має право реалізувати інтерфейс частково: PHP вимагатиме повноти лише від першого конкретного нащадка. Типізувати в решті коду треба інтерфейс, а не абстрактний клас — тоді контейнер Laravel біндить PaymentGateway::class на потрібну реалізацію, а в тесті ви підставляєте фейк, не тягнучи за собою конструктор батька.

Обмеження абстрактного класу проявляються швидко. Він не може бути final одночасно з abstract, його private-методи не бачать нащадки, а якщо нащадок оголошує власний конструктор і забуває parent::__construct(), типізовані властивості батька лишаються неініціалізованими і код падає з «must not be accessed before initialization». Ще одна пастка — спокуса скласти в базовий клас усі спільні хелпери: чим більше туди потрапляє, тим менше шансів, що нащадок згодом переїде в іншу ієрархію, бо батько єдиний. Коли потрібне саме перевикористання коду без спільного типу, беруть трейт; коли потрібні дві незалежні осі — композицію і два інтерфейси, які з PHP 8.1 можна вимагати разом через intersection type A&B.

Компроміс на боці інтерфейсів теж є, і про нього рідко згадують на джуніорських співбесідах: інтерфейс — це публічна обіцянка, яку дорого міняти. Додавання нового методу ламає всі існуючі реалізації, зокрема чужі, тоді як новий метод із тілом в абстрактному класі просто успадковується. Тому інтерфейси тримають вузькими (принцип Interface Segregation: краще PaymentGateway і RefundsPayments окремо, ніж один роздутий), а розширення оформлюють новим інтерфейсом з перевіркою instanceof. З версійних деталей варто памʼятати: перевизначати константи інтерфейсу в класі дозволено з PHP 8.1, типізовані константи зʼявилися у 8.3, а оголошення властивостей в інтерфейсі — лише у 8.4 і лише через хуки.

interface PaymentGateway            // контракт: лише public-методи, жодного стану
{
    public function charge(int $amountUah, string $token): string;
}

interface RefundsPayments           // окремий вузький контракт: вміє не кожен шлюз
{
    public function refund(string $paymentId): void;
}

// Абстрактний клас реалізує контракт частково: спільний код тут, специфіка — у нащадках
abstract class HttpGateway implements PaymentGateway
{
    public function __construct(
        protected readonly HttpClient $http,   // стан, якого в інтерфейсі бути не може
        private readonly string $apiKey,
    ) {
    }

    abstract protected function endpoint(): string;  // нащадок зобовʼязаний дати URL

    public function charge(int $amountUah, string $token): string
    {
        $response = $this->post($this->endpoint(), ['amount' => $amountUah, 'token' => $token]);

        return $response['id'];
    }

    protected function post(string $url, array $payload): array  // protected в інтерфейсі неможливий
    {
        return $this->http->post($url, $payload + ['key' => $this->apiKey]);
    }
}

final class StripeGateway extends HttpGateway implements RefundsPayments
{
    protected function endpoint(): string
    {
        return 'https://api.stripe.com/v1/charges';
    }

    public function refund(string $paymentId): void
    {
        $this->post('https://api.stripe.com/v1/refunds', ['charge' => $paymentId]);
    }
}

new HttpGateway($http, 'key');   // Error: Cannot instantiate abstract class HttpGateway
new StripeGateway($http, 'key') instanceof PaymentGateway;  // true: інтерфейс успадкувався від батька

function payAndAllowRefund(PaymentGateway&RefundsPayments $gateway): void {}  // intersection type, PHP 8.1+
Ключове обмеження: `extends` — тільки один клас, `implements` — скільки завгодно інтерфейсів, а інтерфейс може успадковувати одразу кілька інтерфейсів.
Що інтерфейс не має стану: до PHP 8.4 властивості в ньому взагалі заборонені, а в 8.4 оголошення `public string $name { get; }` — це вимога до хука, а не поле.
Що методи інтерфейсу можуть бути лише `public`, а абстрактний клас вільно тримає `protected`/`private`, конструктор і готові методи.
Правило вибору: спільний код і відношення «is-a» → абстрактний клас; точка підміни, яку типізуємо й мокаємо → інтерфейс. Часто разом: інтерфейс як контракт, абстрактний клас як зручна база.
Що обидва не інстанціюються, обидва працюють у type hint та `instanceof`, а DI-контейнер біндять саме на інтерфейс.
Що додати метод у published-інтерфейс — це BC break для всіх реалізацій, а додати метод з тілом в абстрактний клас — ні.
Казати «інтерфейс — це просто клас без реалізації, різниці немає»: різниця в тому, що абстрактних батьків може бути лише один.
Оголошувати `protected function` чи `private function` в інтерфейсі — фатальна помилка: «Access type for interface method must be public».
Пробувати покласти в інтерфейс властивість `public array $items;` — до PHP 8.4 це фатальна помилка, у 8.4 працює тільки форма з хуками `{ get; }`.
Вважати, що абстрактний клас зобовʼязаний мати хоч один `abstract`-метод: клас без жодного абстрактного методу теж може бути `abstract`, просто його не можна створити через `new`.
Звужувати видимість або типи в нащадку (`protected` замість `public`, вужчий тип параметра) — PHP відмовиться завантажити клас.
Забути `parent::__construct()` у нащадка абстрактного класу — властивості батька лишаться неініціалізованими і впадуть на typed property.
Ліпити абстрактний клас-«помийку» зі спільними хелперами замість трейта або окремого сервіса — потім із нього неможливо вилізти, бо батько лише один.
ПОРАДА

Скажіть формулою: «інтерфейс відповідає на питання *що вміє* обʼєкт, абстрактний клас — *як він це частково робить*». І одразу додайте практичний критерій: якщо в спільній частині зʼявляється стан або protected-метод — це абстрактний клас; якщо потрібна лише точка підміни для контейнера і тестів — інтерфейс.

оновлено 3 вересня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

Головні відмінності — кількість (один батько проти багатьох контрактів) і наявність стану та непублічних членів; в type hint і в instanceof однаково працюють обидва.