OOP: Принципы
Glossary overview

OOP: Принципы

Классические принципы ООП – это четыре базовых столпа.

ПринципКраткое определениеЧто решаетКлючевой механизм в PHPПростой пример
ИнкапсуляцияСокрытие внутреннего состояния объекта и управление доступом к немуЗащищает данные и логику от хаотичного измененияpublic / protected / privateprivate $email + getEmail()
АбстракцияВыделение существенного поведения без раскрытия деталей реализацииПозволяет работать через общий интерфейсinterface, abstract classinterface Logger { log(); }
НаследованиеПовторное использование кода через иерархию “is-a”Расширяет базовый классextendsclass 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;
    }
}

Что произошло?

  1. Свойство $age закрыто – private.
  2. Наружу мы даем только методы.
  3. Внутри 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 не знает, что именно за уведомление используется. И ему всё равно. Это и есть сила полиморфизма.

Ты пишешь код один раз, а поведение масштабируется через разные реализации.