SOLID принципы в PHP: теория и практика — полное руководство

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

SOLID принципы в PHP: теория и практика

SOLID — это пять фундаментальных принципов объектно-ориентированного программирования, сформулированных Робертом Мартином. Они помогают создавать гибкий, поддерживаемый и масштабируемый код. В этой статье мы разберем каждый принцип на примерах PHP, от простых концепций до реальных практических сценариев.

1. Single Responsibility Principle (SRP) — Принцип единственной ответственности

Формулировка: Класс должен иметь только одну причину для изменения. Другими словами, каждый класс отвечает только за одну задачу или функциональность.

Нарушение SRP часто приводит к "божественным объектам", которые знают и делают слишком много. Рассмотрим плохой пример:

class Report {
    public function generate($data) {
        // Генерация отчета
        return "Отчет: " . json_encode($data);
    }

    public function saveToFile($filename, $content) {
        file_put_contents($filename, $content);
    }

    public function sendEmail($to, $subject, $body) {
        mail($to, $subject, $body);
    }
}

Здесь класс Report отвечает за генерацию, сохранение и отправку — три разные ответственности. Исправим это:

class ReportGenerator {
    public function generate($data) {
        return "Отчет: " . json_encode($data);
    }
}

class FileSaver {
    public function save($filename, $content) {
        file_put_contents($filename, $content);
    }
}

class EmailSender {
    public function send($to, $subject, $body) {
        mail($to, $subject, $body);
    }
}

// Использование
$report = new ReportGenerator();
$content = $report->generate($data);

$saver = new FileSaver();
$saver->save('report.txt', $content);

$sender = new EmailSender();
$sender->send('admin@example.com', 'Отчет готов', $content);

Практический совет: Если вы видите в названии класса союз "и" (например, UserAndLogger), это верный признак нарушения SRP.

2. Open-Closed Principle (OCP) — Принцип открытости/закрытости

Формулировка: Классы должны быть открыты для расширения, но закрыты для модификации. Вы должны иметь возможность добавлять новую функциональность без изменения существующего кода.

Нарушение OCP — это когда добавление нового типа требует изменения существующих классов. Пример плохого кода:

class PaymentProcessor {
    public function process($type, $amount) {
        if ($type === 'credit_card') {
            // Обработка кредитной карты
        } elseif ($type === 'paypal') {
            // Обработка PayPal
        } elseif ($type === 'crypto') {
            // Обработка криптовалюты
        }
        // Добавление нового типа = изменение этого класса
    }
}

Решение — использовать полиморфизм через интерфейсы:

interface PaymentMethod {
    public function process($amount);
}

class CreditCardPayment implements PaymentMethod {
    public function process($amount) {
        echo "Обработка кредитной карты на сумму $amount";
    }
}

class PayPalPayment implements PaymentMethod {
    public function process($amount) {
        echo "Обработка PayPal на сумму $amount";
    }
}

class PaymentProcessor {
    public function process(PaymentMethod $method, $amount) {
        $method->process($amount);
    }
}

// Использование
$processor = new PaymentProcessor();
$processor->process(new CreditCardPayment(), 100);
$processor->process(new PayPalPayment(), 200);

Теперь добавление нового способа оплаты (например, ApplePayPayment) не требует изменения PaymentProcessor.

3. Liskov Substitution Principle (LSP) — Принцип подстановки Лисков

Формулировка: Объекты подклассов должны быть взаимозаменяемы с объектами базового класса без нарушения корректности программы. Если у вас есть функция, работающая с базовым классом, она должна работать и с любым его подклассом.

Классическое нарушение LSP — наследование от Rectangle к Square:

class Rectangle {
    protected $width;
    protected $height;

    public function setWidth($width) {
        $this->width = $width;
    }

    public function setHeight($height) {
        $this->height = $height;
    }

    public function getArea() {
        return $this->width * $this->height;
    }
}

class Square extends Rectangle {
    public function setWidth($width) {
        $this->width = $width;
        $this->height = $width; // Нарушение: меняет поведение
    }

    public function setHeight($height) {
        $this->height = $height;
        $this->width = $height; // Нарушение
    }
}

function printArea(Rectangle $rect) {
    $rect->setWidth(5);
    $rect->setHeight(10);
    // Ожидаем 50, но для Square получим 100
    echo $rect->getArea();
}

Решение — не наследовать Square от Rectangle, а создать общий интерфейс:

interface Shape {
    public function getArea();
}

class Rectangle implements Shape {
    private $width;
    private $height;

    public function __construct($width, $height) {
        $this->width = $width;
        $this->height = $height;
    }

    public function getArea() {
        return $this->width * $this->height;
    }
}

class Square implements Shape {
    private $side;

    public function __construct($side) {
        $this->side = $side;
    }

    public function getArea() {
        return $this->side * $this->side;
    }
}

function printArea(Shape $shape) {
    echo $shape->getArea();
}

4. Interface Segregation Principle (ISP) — Принцип разделения интерфейса

Формулировка: Клиенты не должны зависеть от методов, которые они не используют. Лучше иметь несколько узкоспециализированных интерфейсов, чем один "толстый".

Плохой пример — один интерфейс для всех типов работников:

interface Worker {
    public function work();
    public function eat();
    public function sleep();
}

class HumanWorker implements Worker {
    public function work() { /* работает */ }
    public function eat() { /* ест */ }
    public function sleep() { /* спит */ }
}

class RobotWorker implements Worker {
    public function work() { /* работает */ }
    public function eat() { throw new Exception("Роботы не едят"); }
    public function sleep() { throw new Exception("Роботы не спят"); }
}

Решение — разделить интерфейсы:

interface Workable {
    public function work();
}

interface Eatable {
    public function eat();
}

interface Sleepable {
    public function sleep();
}

class HumanWorker implements Workable, Eatable, Sleepable {
    public function work() { /* работает */ }
    public function eat() { /* ест */ }
    public function sleep() { /* спит */ }
}

class RobotWorker implements Workable {                

Блоги

Рекомендаций по книгам