PHP Курс

Классы, объекты и наследование в PHP

Самоучитель Python 3, собранный из материалов данного сайта.Предназначен в основном для тех, кто хочет изучить язык программирования Python с нуля.

Урок 6: Наследование в PHP – как дети наследуют черты родителей

Привет, юный программист! 👋 Сегодня мы познакомимся с одной из самых крутых и мощных концепций в объектно-ориентированном программировании (ООП) – наследованием.

Представь себе семью. У родителей есть какие-то черты: цвет глаз, фамилия, привычки. Их дети наследуют эти черты. Но при этом у каждого ребёнка могут быть и свои собственные особенности, которых нет у родителей. Например, папа любит играть в футбол, мама – готовить, а ребёнок унаследовал и любовь к спорту, и кулинарные таланты, но ещё добавил своё увлечение – программированием.

В PHP наследование работает точно так же! У нас есть родительский класс (ещё его называют базовым или суперклассом), и есть дочерний класс (подкласс). Дочерний класс автоматически получает все свойства и методы родительского класса. А потом мы можем добавить в дочерний класс что-то своё или даже переопределить то, что досталось от родителя.

Это невероятно удобно. Представь, что ты пишешь игру, и у тебя есть много разных типов врагов. Вместо того чтобы писать для каждого врага одни и те же базовые вещи (здоровье, координаты, скорость), ты создаёшь один родительский класс Enemy, а затем наследуешь от него Zombie, Ghost и Robot. Каждый дочерний класс получает всё базовое автоматически, а ты пишешь только то, что делает их уникальными.

Как работает наследование в PHP?

Для того чтобы один класс унаследовал от другого, используется ключевое слово extends. Выглядит это так:

class Родитель {
    // свойства и методы родителя
}

class Ребёнок extends Родитель {
    // свойства и методы ребёнка
    // ребёнок уже имеет всё, что есть у родителя!
}

Давай сразу перейдём к живому примеру. Предположим, мы создаём класс Animal (Животное). У любого животного есть имя и возраст, и оно может издавать звук.

<?php

// Создаём родительский класс
class Animal {
    // Свойства (характеристики)
    public $name;
    public $age;

    // Конструктор – вызывается при создании объекта
    public function __construct($name, $age) {
        $this->name = $name;
        $this->age = $age;
        echo "Создано животное: {$this->name}<br>";
    }

    // Метод (действие)
    public function makeSound() {
        echo "{$this->name} издаёт какой-то звук.<br>";
    }

    // Ещё один метод
    public function getInfo() {
        return "Имя: {$this->name}, Возраст: {$this->age} лет.";
    }
}

// Теперь создадим дочерний класс Dog (Собака)
class Dog extends Animal {
    // У собаки есть своё свойство – порода
    public $breed;

    // Свой конструктор (он переопределяет родительский)
    public function __construct($name, $age, $breed) {
        // Вызываем конструктор родителя, чтобы не дублировать код!
        parent::__construct($name, $age);
        // А это уже новое свойство
        $this->breed = $breed;
    }

    // Переопределяем метод makeSound
    public function makeSound() {
        echo "{$this->name} говорит: Гав-гав! 🐶<br>";
    }

    // Новый метод, которого нет у родителя
    public function fetch() {
        echo "{$this->name} приносит палку! 🎾<br>";
    }
}

// Создаём объект класса Dog
$dog = new Dog("Бобик", 3, "Лабрадор");

// Вызываем методы
echo $dog->getInfo() . "<br>"; // Этот метод унаследован от Animal!
$dog->makeSound();              // Этот переопределён
$dog->fetch();                  // Этот свой, новый

// Создаём объект родительского класса
$animal = new Animal("Просто зверь", 5);
$animal->makeSound();           // Обычный звук животного

?>

Обрати внимание на важную деталь: parent::__construct($name, $age);. Это вызов конструктора родительского класса. Зачем это нужно? Представь, что у родителя в конструкторе выполняется 100 строк кода. Если ты в дочернем классе напишешь свой конструктор с нуля, тебе придётся повторять все эти 100 строк. А так ты просто говоришь: «Родитель, сделай свою часть работы, а я добавлю своё». Это экономит время и делает код чище.

Зачем нужно наследование? Реальные аналогии

Давай подумаем, где в реальной жизни мы видим наследование.

  1. Транспорт: Есть общий класс «Транспортное средство» (у него есть скорость, колёса, метод «ехать»). От него наследуются «Автомобиль», «Велосипед», «Самолёт». У каждого свои особенности: у самолёта есть крылья, у велосипеда – педали. Но все они умеют ехать (двигаться).
  2. Геометрические фигуры: Есть класс «Фигура» (у неё есть цвет, метод «нарисовать»). От него наследуются «Круг», «Квадрат», «Треугольник». Каждый рисуется по-своему, но все они фигуры.
  3. Работники в компании: Есть класс «Сотрудник» (имя, зарплата, метод «работать»). От него наследуются «Менеджер», «Программист», «Дизайнер». Программист пишет код, дизайнер рисует макеты, но оба они сотрудники и получают зарплату.

Наследование помогает ответить на вопрос: «Что общего у этих объектов?». Всё общее выносится в родительский класс, а уникальное остаётся в дочерних.

Модификаторы доступа: public, protected, private

Когда мы говорим о наследовании, очень важно понимать, какие свойства и методы видны (доступны) в дочерних классах. Для этого существуют модификаторы доступа.

Представь, что у тебя есть дневник. Записи могут быть:

  • public (публичные) – это как записи на обложке дневника, которые видят все: и друзья, и учителя, и родители. Доступны отовсюду.
  • protected (защищённые) – это записи внутри дневника, которые видишь только ты и твои родственники (в нашем случае – родительский класс и его «дети»). Снаружи к ним доступа нет.
  • private (приватные) – это твои самые секретные мысли, которые знаешь только ты сам. Даже дети (дочерние классы) не могут их увидеть. Только сам класс, в котором они объявлены.

Давай посмотрим на примере:

<?php

class ParentClass {
    public $publicVar = "Вижу все!";
    protected $protectedVar = "Вижу я и мои дети!";
    private $privateVar = "Вижу только я!";

    public function testAccess() {
        echo $this->publicVar . "<br>";     // ОК
        echo $this->protectedVar . "<br>"; // ОК
        echo $this->privateVar . "<br>";   // ОК (мы внутри класса)
    }
}

class ChildClass extends ParentClass {
    public function testChildAccess() {
        echo $this->publicVar . "<br>";     // ОК (public виден везде)
        echo $this->protectedVar . "<br>"; // ОК (protected виден в дочерних)
        // echo $this->privateVar . "<br>"; // ОШИБКА! private не виден в дочерних
    }
}

$obj = new ChildClass();
echo $obj->publicVar . "<br>"; // ОК, public виден снаружи
// echo $obj->protectedVar; // ОШИБКА! protected не виден снаружи
// echo $obj->privateVar;   // ОШИБКА! private не виден нигде, кроме своего класса

$obj->testChildAccess(); // Выведет public и protected

?>

Совет: В реальных проектах принято делать свойства protected или private, чтобы никто случайно не испортил их извне. А методы, которые нужны для работы с классом, делают public.

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

Мы уже видели это в примере с собакой. Когда дочерний класс хочет изменить поведение унаследованного метода, он просто объявляет метод с тем же именем. Это называется переопределением.

Важное правило: сигнатура метода (название и количество параметров) должна совпадать с родительской, иначе PHP будет считать, что это просто другой метод.

Иногда бывает нужно не полностью заменить родительский метод, а дополнить его. Для этого мы используем parent::имяМетода(). Вот пример:

<?php

class Car {
    protected $brand;
    protected $speed;

    public function __construct($brand) {
        $this->brand = $brand;
        $this->speed = 0;
    }

    public function start() {
        echo "Двигатель {$this->brand} запущен. Врум-врум!<br>";
        $this->speed = 10;
    }

    public function getSpeed() {
        return $this->speed;
    }
}

// Спортивная машина – тоже машина, но с турбонаддувом!
class SportsCar extends Car {
    private $turboMode = false;

    // Переопределяем метод start
    public function start() {
        // Сначала делаем то же, что и обычная машина
        parent::start();
        // А потом добавляем свою магию
        echo "Турборежим активирован! 🔥<br>";
        $this->turboMode = true;
        $this->speed = 100; // Разгоняемся быстрее
    }

    public function activateNitro() {
        if ($this->turboMode) {
            $this->speed += 200;
            echo "НИТРО! Скорость: {$this->speed} км/ч 🚀<br>";
        }
    }
}

$ferrari = new SportsCar("Феррари");
$ferrari->start();  // Сначала запустится двигатель, потом турбо
$ferrari->activateNitro();

?>

Видишь, как красиво? Мы не копировали код запуска двигателя, а просто вызвали родительский метод через parent::start(), а затем добавили новое поведение. Это называется расширение метода.

Ключевое слово final

Иногда ты хочешь сказать: «Этот метод нельзя переопределять!» или «От этого класса нельзя наследоваться!». Для этого есть ключевое слово final.

Представь, что ты создал класс MathUtils с методом calculatePi(). Ты не хочешь, чтобы кто-то случайно (или намеренно) переопределил этот метод и исказил вычисление числа Пи. Тогда ты пишешь:

<?php

class MathUtils {
    final public function calculatePi() {
        return 3.14159;
    }
}

class BadMath extends MathUtils {
    // Это вызовет ошибку!
    // public function calculatePi() {
    //     return 3; // Злой программист хочет испортить Пи
    // }
}

// Также можно запретить наследование от всего класса:
final class ImmutableConfig {
    // Этот класс нельзя наследовать
}

// class MyConfig extends ImmutableConfig { } // Ошибка!

?>

final полезен для защиты важных частей кода от случайных изменений.

Практический пример: Система персонажей для игры

Давай создадим небольшую систему для RPG-игры. У нас будут разные типы персонажей: Воин, Маг и Лучник. Все они – Персонажи.

<?php

// Базовый класс для всех персонажей
class Character {
    protected $name;
    protected $health;
    protected $damage;
    protected $level = 1;

    public function __construct($name, $health = 100, $damage = 10) {
        $this->name = $name;
        $this->health = $health;
        $this->damage = $damage;
        echo "Создан персонаж: {$this->name}<br>";
    }

    // Базовый метод атаки
    public function attack(Character $target) {
        echo "{$this->name} атакует {$target->name} на {$this->damage} урона!<br>";
        $target->takeDamage($this->damage);
    }

    // Получить урон
    public function takeDamage($damage) {
        $this->health -= $damage;
        echo "{$this->name} получает {$damage} урона. Осталось здоровья: {$this->health}<br>";
        if ($this->health <= 0) {
            echo "{$this->name} повержен! 💀<br>";
        }
    }

    // Повысить уровень
    public function levelUp() {
        $this->level++;
        $this->health += 20;
        $this->damage += 5;
        echo "{$this->name} повысил уровень до {$this->level}! ❤️‍🔥<br>";
    }

    // Получить информацию
    public function getInfo() {
        return "{$this->name} [Ур.{$this->level}] HP:{$this->health} DMG:{$this->damage}";
    }
}

// Класс Воина
class Warrior extends Character {
    private $armor;

    public function __construct($name, $health = 150, $damage = 15) {
        parent::__construct($name, $health, $damage);
        $this->armor = 20; // У воина есть броня
        echo "{$this->name} надевает тяжёлую броню! 🛡️<br>";
    }

    // Переопределяем получение урона: броня уменьшает урон
    public function takeDamage($damage) {
        $reducedDamage = max(0, $damage - $this->armor);
        echo "Броня {$this->name} поглощает {$this->armor} урона.<br>";
        parent::takeDamage($reducedDamage);
    }

    // Специальная атака воина
    public function powerStrike(Character $target) {
        $bonusDamage = $this->damage * 2;
        echo "{$this->name} использует СИЛОВОЙ УДАР на {$bonusDamage} урона! 💥<br>";
        $target->takeDamage($bonusDamage);
    }
}

// Класс Мага
class Mage extends Character {
    private $mana;

    public function __construct($name, $health = 80, $damage = 25) {
        parent::__construct($name, $health, $damage);
        $this->mana = 100;
        echo "{$this->name} наполняется магической энергией! 🔮<br>";
    }

    // Маг атакует магией, тратя ману
    public function castSpell(Character $target) {
        if ($this->mana >= 30) {
            $this->mana -= 30;
            $spellDamage = $this->damage * 3;
            echo "{$this->name} произносит заклинание! Огненный шар на {$spellDamage} урона! 🔥<br>";
            $target->takeDamage($spellDamage);
        } else {
            echo "Недостаточно маны! ({$this->mana}/100)<br>";
        }
    }

    // Восстановить ману
    public function meditate() {
        $this->mana = 100;
        echo "{$this->name} медитирует и восстанавливает ману. 🧘<br>";
    }
}

// Класс Лучника
class Archer extends Character {
    private $arrows;

    public function __construct($name, $health = 100, $damage = 20) {
        parent::__construct($name, $health, $damage);
        $this->arrows = 30;
        echo "{$this->name} заряжает лук стрелами! 🏹<br>";
    }

    // Переопределяем атаку: лучник тратит стрелы
    public function attack(Character $target) {
        if ($this->arrows > 0) {
            $this->arrows--;
            echo "{$this->name} выпускает стрелу! Осталось стрел: {$this->arrows}<br>";
            parent::attack($target);
        } else {
            echo "Стрелы кончились! {$this->name} атакует луком в ближнем бою (урон меньше).<br>";
            $target->takeDamage(intval($this->damage / 2));
        }
    }

    // Специальный выстрел
    public function criticalShot(Character $target) {
        if ($this->arrows >= 3) {
            $this->arrows -= 3;
            $critDamage = $this->damage * 4;
            echo "{$this->name} делает ТОЧНЫЙ ВЫСТРЕЛ! Критический урон: {$critDamage}! 🎯<br>";
            $target->takeDamage($critDamage);
        } else {
            echo "Недостаточно стрел для точного выстрела.<br>";
        }
    }
}

// --- Демонстрация боя ---

echo "<h3>⚔️ Битва героев начинается! ⚔️</h3>";

$warrior = new Warrior("Тор", 200, 20);
$mage = new Mage("Гендальф", 90, 30);
$archer = new Archer("Леголас", 120, 25);

echo "<br><strong>Информация о героях:</strong><br>";
echo $warrior->getInfo() . "<br>";
echo $mage->getInfo() . "<br>";
echo $archer->getInfo() . "<br>";

echo "<br><strong>Раунд 1: Маг атакует Воина</strong><br>";
$mage->castSpell($warrior);

echo "<br><strong>Раунд 2: Воин отвечает силовым ударом</strong><br>";
$warrior->powerStrike($mage);

echo "<br><strong>Раунд 3: Лучник делает точный выстрел</strong><br>";
$archer->criticalShot($mage);

echo "<br><strong>Раунд 4: Маг восстанавливает ману и снова атакует</strong><br>";
$mage->meditate();
$mage->castSpell($warrior);

echo "<br><strong>Итоги битвы:</strong><br>";
echo $warrior->getInfo() . "<br>";
echo $mage->getInfo() . "<br>";
echo $archer->getInfo() . "<br>";

?>

Посмотри, как элегантно всё работает! Базовый класс Character содержит всё общее: имя, здоровье, урон, метод атаки и получения урона. А каждый дочерний класс добавляет свою уникальную механику: у Воина – броня, у Мага – мана и заклинания, у Лучника – стрелы. И при этом все они могут делать базовые вещи – атаковать, получать урон, повышать уровень.

Это и есть сила наследования: ты пишешь один раз общую логику, а потом просто расширяешь её для конкретных случаев.

Ограничения и советы

Наследование – мощный инструмент, но использовать его нужно с умом. Вот несколько правил:

  • Не создавай слишком глубокие цепочки наследования. Класс, который наследует от класса, который наследует от класса... это может запутать. Обычно 2-3 уровня – это максимум.
  • Помни про принцип «is-a» (является). Дочерний класс должен быть разновидностью родительского. Собака является животным. Машина является транспортным средством. Если у тебя класс «Утка», а ты хочешь унаследовать от класса «Самолёт» – это неправильно (утка не является самолётом, хотя и то, и другое может летать). В таком случае лучше сделать общий интерфейс (мы изучим это позже).
  • Старайся делать родительские классы абстрактными (мы тоже это изучим). Это классы, от которых нельзя создать объект напрямую – они нужны только для наследования.
  • Исп