Урок 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 строк. А так ты просто говоришь: «Родитель, сделай свою часть работы, а я добавлю своё». Это экономит время и делает код чище.
Зачем нужно наследование? Реальные аналогии
Давай подумаем, где в реальной жизни мы видим наследование.
- Транспорт: Есть общий класс «Транспортное средство» (у него есть скорость, колёса, метод «ехать»). От него наследуются «Автомобиль», «Велосипед», «Самолёт». У каждого свои особенности: у самолёта есть крылья, у велосипеда – педали. Но все они умеют ехать (двигаться).
- Геометрические фигуры: Есть класс «Фигура» (у неё есть цвет, метод «нарисовать»). От него наследуются «Круг», «Квадрат», «Треугольник». Каждый рисуется по-своему, но все они фигуры.
- Работники в компании: Есть класс «Сотрудник» (имя, зарплата, метод «работать»). От него наследуются «Менеджер», «Программист», «Дизайнер». Программист пишет код, дизайнер рисует макеты, но оба они сотрудники и получают зарплату.
Наследование помогает ответить на вопрос: «Что общего у этих объектов?». Всё общее выносится в родительский класс, а уникальное остаётся в дочерних.
Модификаторы доступа: 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» (является). Дочерний класс должен быть разновидностью родительского. Собака является животным. Машина является транспортным средством. Если у тебя класс «Утка», а ты хочешь унаследовать от класса «Самолёт» – это неправильно (утка не является самолётом, хотя и то, и другое может летать). В таком случае лучше сделать общий интерфейс (мы изучим это позже).
- Старайся делать родительские классы абстрактными (мы тоже это изучим). Это классы, от которых нельзя создать объект напрямую – они нужны только для наследования.
- Исп