Урок 05

Наследование и переопределение методов

Что такое наследование

Наследование — класс получает свойства и методы другого класса.

class Animal
{
    public function eat(): void
    {
        echo 'Ест';
    }
}

class Dog extends Animal
{
    public function bark(): void
    {
        echo 'Гав';
    }
}

$dog = new Dog();
$dog->eat();    // Ест (унаследовано)
$dog->bark();   // Гав

extends — наследование.

Родитель, потомок

Другие названия:

  • Родитель = base, super, parent.
  • Потомок = child, derived, subclass.

Что наследуется

Всё, кроме:

  • private — видны только в родителе.
  • Конструктор — если у родителя нет аргументов, вызывается автоматически.

Наследуется:

  • public и protected методы.
  • public и protected свойства.
  • Константы.

Переопределение методов

Метод с тем же именем:

class Animal
{
    public function speak(): string
    {
        return 'Звук';
    }
}

class Dog extends Animal
{
    public function speak(): string
    {
        return 'Гав';
    }
}

$dog = new Dog();
echo $dog->speak();   // Гав

Переопределение — замена поведения.

parent::

Вызвать метод родителя:

class Animal
{
    public function speak(): string
    {
        return 'Звук';
    }
}

class Dog extends Animal
{
    public function speak(): string
    {
        return parent::speak() . ' (но гав)';
    }
}

$dog = new Dog();
echo $dog->speak();   // Звук (но гав)

parent::method() — расширение, не замена.

Конструктор родителя

Вызывать явно:

class BaseEntity
{
    public function __construct(
        public readonly int $id,
    ) {}
}

class User extends BaseEntity
{
    public function __construct(
        int $id,
        public readonly string $email,
    ) {
        parent::__construct($id);
    }
}

Если родитель имеет обязательные параметры — обязательно.

Сигнатуры методов

Правила (LSP):

  • Параметры можно расширить (контравариантность).
  • Возврат можно сузить (ковариантность).

Пример возврата:

class Animal
{
    public function clone(): Animal { /* ... */ }
}

class Dog extends Animal
{
    public function clone(): Dog { /* ... */ }   // ок
}

Пример параметров:

class ParentClass
{
    public function process(Dog $dog): void { }
}

class ChildClass extends ParentClass
{
    public function process(Animal $animal): void { }   // ок — шире
}

Сузить нельзя:

class ChildClass extends ParentClass
{
    public function process(SpecificDog $dog): void { }   // Error
}

Наследование от нескольких

⚠️ В PHP нет множественного наследования классов.

Один класс — один родитель:

class A {}
class B {}
class C extends A, B { }   // Error

Множественное поведение — через трейты (следующий урок) или интерфейсы.

Иерархия

Многоуровневое наследование:

class A { }
class B extends A { }
class C extends B { }
class D extends C { }

Работает, но избегай глубоких иерархий — тяжело поддерживать.

Правило: максимум 2–3 уровня.

Абстрактные методы

В абстрактном классе:

abstract class Shape
{
    abstract public function area(): float;
}

class Circle extends Shape
{
    public function __construct(private float $radius) {}

    public function area(): float
    {
        return M_PI * $this->radius ** 2;
    }
}

Наследник обязан реализовать.

Полиморфизм

Один интерфейс, разное поведение:

abstract class Shape
{
    abstract public function area(): float;
}

class Circle extends Shape
{
    public function __construct(private float $r) {}
    public function area(): float { return M_PI * $this->r ** 2; }
}

class Square extends Shape
{
    public function __construct(private float $side) {}
    public function area(): float { return $this->side ** 2; }
}

function totalArea(array $shapes): float
{
    $total = 0.0;

    foreach ($shapes as $shape) {
        $total += $shape->area();   // вызов нужного метода
    }

    return $total;
}

$shapes = [new Circle(1.0), new Square(2.0)];
echo totalArea($shapes);   // ~7.14

Полиморфизм — ключевая сила ООП.

final

Запретить наследование:

final class User { }

class Admin extends User { }   // Error

Или метод:

class Foo
{
    final public function bar(): void { }
}

class Child extends Foo
{
    public function bar(): void { }   // Error
}

Правило: final по умолчанию, открывай осознанно.

parent:: в разных местах

Только в наследнике:

class Child extends Parent
{
    public function method(): void
    {
        parent::method();   // ok
    }
}

parent — статический контекст.

static:: vs self::

self:: — текущий класс в коде:

class Foo
{
    public static function name(): string
    {
        return self::class;   // всегда Foo
    }
}

static:: — вызывающий класс:

class Base
{
    public static function name(): string
    {
        return static::class;
    }
}

class Child extends Base {}

echo Child::name();   // Child

Позднее статическое связывание.

Шаблонный метод

Классический паттерн:

abstract class ReportGenerator
{
    final public function generate(): string
    {
        $data = $this->getData();
        $html = $this->render($data);
        return $this->wrap($html);
    }

    abstract protected function getData(): array;
    abstract protected function render(array $data): string;

    private function wrap(string $html): string
    {
        return "<html><body>$html</body></html>";
    }
}

class SalesReport extends ReportGenerator
{
    protected function getData(): array
    {
        return ['sales' => 100];
    }

    protected function render(array $data): string
    {
        return "<h1>Sales: {$data['sales']}</h1>";
    }
}

Алгоритм в родителе, шаги — в потомках.

Sealed classes (будущее)

В PHP нет sealed, но есть final и enum.

Практика: Notification

<?php

declare(strict_types=1);

abstract class Notification
{
    public function __construct(
        protected readonly string $message,
    ) {}

    public function getMessage(): string
    {
        return $this->message;
    }

    abstract public function send(string $to): string;
}

final class EmailNotification extends Notification
{
    public function send(string $to): string
    {
        return "Email на $to: {$this->message}";
    }
}

final class SmsNotification extends Notification
{
    public function send(string $to): string
    {
        return "SMS на $to: " . mb_substr($this->message, 0, 70);
    }
}

final class PushNotification extends Notification
{
    public function send(string $to): string
    {
        return "Push для $to: {$this->message}";
    }
}

$notifications = [
    new EmailNotification('Привет'),
    new SmsNotification('Длинное сообщение...'),
    new PushNotification('Уведомление'),
];

foreach ($notifications as $n) {
    echo $n->send('+79001234567') . "\n";
}

Наследование vs композиция

«Favor composition over inheritance» — принцип ООП.

Наследование — «is-a»:

class Dog extends Animal { }   // Dog is-a Animal

Композиция — «has-a»:

class Car
{
    public function __construct(
        private Engine $engine,   // Car has-a Engine
    ) {}
}

Обычно композиция гибче.

Проблема наследования

Хрупкий базовый класс:

class Base
{
    public function process(array $items): void
    {
        foreach ($items as $item) {
            $this->beforeItem($item);
            // ...
        }
    }

    protected function beforeItem(mixed $item): void { }
}

Если потомок переопределит process — сломает логику. Или если родитель поменяет порядок вызовов — сломает потомков.

Композиция решает эту проблему.

Итог

  • extends — наследование.
  • parent:: — вызов родителя.
  • Переопределение — своя реализация.
  • final — запрет наследования.
  • Контравариантность параметров, ковариантность возврата.
  • Полиморфизм — разное поведение через общий интерфейс.
  • Композиция лучше наследования в большинстве случаев.

Дальше — abstract классы.