Классические принципы ООП – это четыре базовых столпа.
| Принцип | Краткое определение | Что решает | Ключевой механизм в PHP | Простой пример |
|---|---|---|---|---|
| Инкапсуляция | Сокрытие внутреннего состояния объекта и управление доступом к нему | Защищает данные и логику от хаотичного изменения | public / protected / private | private $email + getEmail() |
| Абстракция | Выделение существенного поведения без раскрытия деталей реализации | Позволяет работать через общий интерфейс | interface, abstract class | interface Logger { log(); } |
| Наследование | Повторное использование кода через иерархию “is-a” | Расширяет базовый класс | extends | class Dog extends Animal |
| Полиморфизм | Возможность объектов с общим типом вести себя по-разному | Позволяет писать универсальный код | переопределение методов | $animal->speak() → разный результат |
Инкапсуляция
Инкапсуляция в ООП – это не просто “прячем свойства и делаем геттеры-сеттеры”. Это шире. Это принцип, по которому объект сам управляет своим состоянием и не дает внешнему коду лезть внутрь как в ящик с инструментами.
class User
{
private int $age;
public function __construct(int $age)
{
$this->setAge($age);
}
public function setAge(int $age): void
{
if ($age < 0) {
throw new InvalidArgumentException('Age cannot be negative');
}
$this->age = $age;
}
public function getAge(): int
{
return $this->age;
}
}
Что произошло?
- Свойство
$ageзакрыто –private. - Наружу мы даем только методы.
- Внутри
setAgeконтролируем валидность данных.
Теперь никто не может присвоить -100 напрямую. Только через метод, который проверяет правила. Вот это и есть инкапсуляция: контроль доступа + защита инвариантов объекта.
Но важный момент. Геттеры и сеттеры сами по себе не делают код хорошим. Если ты просто напишешь:
public function setAge($age) {
$this->age = $age;
}
и никакой логики там нет – ты ничего не инкапсулировал. Ты просто сделал прокладку.
Настоящая инкапсуляция начинается там, где объект начинает защищать свою внутреннюю логику.
Например, еще интереснее:
class BankAccount
{
private float $balance = 0;
public function deposit(float $amount): void
{
if ($amount <= 0) {
throw new InvalidArgumentException('Invalid amount');
}
$this->balance += $amount;
}
public function getBalance(): float
{
return $this->balance;
}
}
Обрати внимание: здесь вообще нет setBalance. Потому что напрямую менять баланс нельзя. Только через поведение – deposit. Это уже более зрелая модель. Мы инкапсулируем не только данные, но и бизнес-логику.
Если упростить до определения:
Инкапсуляция – это сокрытие внутреннего состояния объекта и предоставление контролируемого интерфейса для работы с ним.
И да, в 80% учебных примеров это объясняют через геттеры/сеттеры. Но в реальном коде это скорее про контроль инвариантов, защиту логики и минимизацию прямого доступа.
Абстракция
Абстракция – это когда ты скрываешь детали реализации и оставляешь только суть поведения.
В PHP это чаще всего проявляется через интерфейсы и абстрактные классы.
Простой пример. Есть разные способы оплаты: карта, PayPal, крипта.
Мы можем описать общий контракт:
interface PaymentMethod
{
public function pay(float $amount): void;
}
Мы не говорим, как именно проходит платеж. Мы говорим только одно: любой способ оплаты обязан уметь pay().
Теперь конкретная реализация:
class CardPayment implements PaymentMethod
{
public function pay(float $amount): void
{
echo "Оплата картой на сумму $amount";
}
}
class CryptoPayment implements PaymentMethod
{
public function pay(float $amount): void
{
echo "Оплата в криптовалюте на сумму $amount";
}
}
Код, который использует оплату, вообще не знает, что внутри:
// мы тут ожидаем любой класс, у которого будет метод pay
function processPayment(PaymentMethod $method)
{
$method->pay(100);
}
Вот здесь магия: processPayment работает с абстракцией PaymentMethod, а не с конкретным CardPayment.
Мы отделили “что должно быть сделано” от “как это делается”.
Это и есть абстракция.
Если идти чуть глубже. Абстракция помогает уменьшить зависимость от конкретных реализаций. Ты работаешь с идеей, с контрактом, а не с деталями.
Иногда абстракция реализуется через abstract class:
abstract class Animal
{
abstract public function makeSound(): string;
}
Мы не знаем, какой звук. Просто говорим: любое животное должно уметь издавать звук.
Конкретика появляется позже:
class Dog extends Animal
{
public function makeSound(): string
{
return "Woof";
}
}
Наследование
Наследование – это когда один класс берет поведение и свойства другого класса и расширяет или уточняет их.
Если по-простому: мы создаем базовый шаблон, а потом делаем более конкретные версии.
В PHP это делается через extends.
Пример. Есть базовый класс:
class Animal
{
protected string $name;
public function __construct(string $name)
{
$this->name = $name;
}
public function eat(): void
{
echo $this->name . " ест\n";
}
}
Теперь создаем конкретное животное:
class Dog extends Animal
{
public function bark(): void
{
echo $this->name . " гавкает\n";
}
}
Dog автоматически получил:
- свойство
$name - метод
eat() - конструктор
И добавил свое поведение bark().
Вот это и есть наследование – повторное использование кода через иерархию классов.
Но есть более интересный момент – переопределение.
class Cat extends Animal
{
public function eat(): void
{
echo $this->name . " ест аккуратно и с презрением\n";
}
}
Здесь метод eat() переопределен. Родитель дал общее поведение, потомок уточнил его.
Это мощно. Но здесь же и ловушка.
Наследование – это сильная связка. Потомок жестко привязан к родителю. Если ты меняешь родителя, можешь поломать всех детей. Поэтому в современной архитектуре часто говорят “предпочитай композицию вместо наследования“.
Композиция
Композиция – это другой подход. Вместо “является” ты используешь “содержит” или “использует”.
Пример.
Через наследование:
class Logger
{
public function log(string $message): void
{
echo $message;
}
}
class FileLogger extends Logger
{
public function log(string $message): void
{
file_put_contents('log.txt', $message);
}
}
Здесь FileLogger привязан к Logger. Иерархия.
А теперь композиция:
interface LoggerInterface
{
public function log(string $message): void;
}
class FileLogger implements LoggerInterface
{
public function log(string $message): void
{
file_put_contents('log.txt', $message);
}
}
class UserService
{
private LoggerInterface $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function register(): void
{
$this->logger->log('User registered');
}
}
Композиция в PHP OOP — это способ проектирования, при котором сложный объект («целое») создается из более простых объектов («частей»), причем «части» создаются внутри «целого» и не существуют без него. Это отношение «имеет» (has-a), обеспечивающее гибкость, повторное использование кода и лучшую структуру, чем при наследовании.
Ключевые особенности композиции:
- «Часть» не существует без «целого»: Если уничтожается основной объект (например,
Car), уничтожаются и его части (например,Engine). - Связь «Имеет»: Класс
CarимеетEngine. - Создание внутри: Объект-часть часто создается прямо внутри конструктора или методов основного класса.
Final
Есть еще важный момент – ключевое слово final.
Если ты напишешь:
final class BaseService
{
}
То от него нельзя наследоваться. Это способ запретить расширение.
Полиморфизм
В PHP полиморфизм — это принцип ООП, который позволяет объектам разных классов иметь одинаковые имена методов, но выполнять их по-разному.
Проще говоря: это возможность работать с разными типами данных (объектами), используя один и тот же «интерфейс» (набор правил), не вдаваясь в детали того, как именно они устроены внутри.
Как это работает в PHP
Полиморфизм обычно реализуется двумя способами:
- Через интерфейсы (Interface): Вы создаете общий «контракт», который обязует классы реализовать определенные методы. Например, у классов
CircleиSquareможет быть общий методcalculateArea(), но формулы внутри будут разными. - Через наследование (Inheritance): Дочерний класс может переопределить метод родителя (Method Overriding), чтобы изменить его поведение под свои нужды.
interface Shape
{
public function getArea(): float;
}
// Circle
class Circle implements Shape
{
private float $radius;
public function __construct(float $radius)
{
$this->radius = $radius;
}
public function getArea(): float
{
return pi() * $this->radius ** 2;
}
}
// Rectangle
class Rectangle implements Shape
{
private float $width;
private float $height;
public function __construct(float $width, float $height)
{
$this->width = $width;
$this->height = $height;
}
public function getArea(): float
{
return $this->width * $this->height;
}
}
А теперь самое интересное:
// Просто ожидаем класс, которые реализует интерфейс Shape
// Нам все рано будет ли это Circle или Rectangle
function printArea(Shape $shape): void
{
echo $shape->getArea();
}
Мы передаем Circle. Работает. Передаем Rectangle. Работает.
Один и тот же код. Разное поведение.
Вот это и есть полиморфизм.
Мы работаем не с конкретным классом, а с абстракцией. А конкретная реализация выбирается в рантайме.
Это возможность обращаться к разным объектам через единый интерфейс.
Более прикладной пример, ближе к реальному коду.
Допустим, у тебя есть разные способы отправки уведомлений:
interface Notifier
{
public function send(string $message): void;
}
EmailNotifier, SmsNotifier, TelegramNotifier – все реализуют send().
И есть сервис:
class OrderService
{
private Notifier $notifier;
public function __construct(Notifier $notifier)
{
$this->notifier = $notifier;
}
public function completeOrder(): void
{
$this->notifier->send('Order completed');
}
}
// Use
$notifier = new EmailNotifier();
$orderService = new OrderService($notifier);
$orderService->completeOrder();
OrderService не знает, что именно за уведомление используется. И ему всё равно. Это и есть сила полиморфизма.
Ты пишешь код один раз, а поведение масштабируется через разные реализации.