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

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

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

Начать курс

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

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

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

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

Нарушение SRP:

class Order {
    public function calculateTotal() { /* ... */ }
    public function saveToDatabase() { /* ... */ }
    public function sendEmailNotification() { /* ... */ }
}

Здесь класс Order отвечает и за расчет, и за сохранение, и за отправку уведомлений. Это нарушает SRP.

Решение:

class Order {
    public function calculateTotal() { /* ... */ }
}

class OrderRepository {
    public function save(Order $order) { /* ... */ }
}

class EmailNotificationService {
    public function sendOrderConfirmation(Order $order) { /* ... */ }
}

Теперь каждый класс имеет одну четкую ответственность, что упрощает тестирование и модификацию.

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

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

Нарушение OCP:

class PaymentProcessor {
    public function process($type) {
        if ($type == 'credit_card') {
            // обработка кредитной карты
        } elseif ($type == 'paypal') {
            // обработка PayPal
        }
    }
}

Чтобы добавить новый способ оплаты, придется изменить класс PaymentProcessor.

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

interface PaymentMethod {
    public function process();
}

class CreditCardPayment implements PaymentMethod {
    public function process() { /* ... */ }
}

class PayPalPayment implements PaymentMethod {
    public function process() { /* ... */ }
}

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

Теперь для добавления нового способа оплаты достаточно создать новый класс, реализующий интерфейс PaymentMethod, без изменения существующего кода.

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

Суть: Объекты подклассов должны быть взаимозаменяемы с объектами базового класса без нарушения корректности программы. Подкласс не должен изменять поведение, ожидаемое от базового класса.

Нарушение LSP:

class Rectangle {
    protected $width;
    protected $height;

    public function setWidth($w) { $this->width = $w; }
    public function setHeight($h) { $this->height = $h; }
    public function getArea() { return $this->width * $this->height; }
}

class Square extends Rectangle {
    public function setWidth($w) {
        $this->width = $w;
        $this->height = $w; // нарушение принципа
    }
    public function setHeight($h) {
        $this->height = $h;
        $this->width = $h; // нарушение принципа
    }
}

Если клиентский код ожидает, что изменение ширины не повлияет на высоту, то Square нарушит это ожидание.

Решение: Избегайте наследования, если подкласс не полностью соответствует поведению родителя. Лучше использовать композицию или отдельные классы:

interface Shape {
    public function getArea();
}

class Rectangle implements Shape {
    public function __construct(private $width, private $height) {}
    public function getArea() { return $this->width * $this->height; }
}

class Square implements Shape {
    public function __construct(private $side) {}
    public function getArea() { return $this->side * $this->side; }
}

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

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

Нарушение ISP:

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

class HumanWorker implements Worker {
    public function work() { /* ... */ }
    public function eat() { /* ... */ }
}

class RobotWorker implements Worker {
    public function work() { /* ... */ }
    public function eat() { throw new Exception('Robot does not eat'); }
}

Робот вынужден реализовывать метод eat(), который ему не нужен.

Решение:

interface Workable {
    public function work();
}

interface Eatable {
    public function eat();
}

class HumanWorker implements Workable, Eatable {
    public function work() { /* ... */ }
    public function eat() { /* ... */ }
}

class RobotWorker implements Workable {
    public function work() { /* ... */ }
}

Теперь каждый класс реализует только те интерфейсы, которые ему действительно нужны.

5. Dependency Inversion Principle (DIP) — Принцип инверсии зависимостей

Суть: Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций. Абстракции не должны зависеть от деталей; детали должны зависеть от абстракций.

Нарушение DIP:

class MySQLDatabase {
    public function query($sql) { /* ... */ }
}

class UserRepository {
    private $database;
    public function __construct() {
        $this->database = new MySQLDatabase(); // жесткая зависимость
    }
}

Класс UserRepository жестко привязан к конкретной реализации базы данных.

Решение с внедрением зависимостей (Dependency Injection):

interface DatabaseInterface {
    public function query($sql);
}

class MySQLDatabase implements DatabaseInterface {
    public function query($sql) { /* ... */ }
}

class PostgreSQLDatabase implements DatabaseInterface {
    public function query($sql) { /* ... */ }
}

class UserRepository {
    private $database;
    public function __construct(DatabaseInterface $database) {
        $this->database = $database;
    }
}

// Использование
$repository = new UserRepository(new MySQLDatabase());
$repository = new UserRepository(new PostgreSQLDatabase()); // легко переключить

Теперь UserRepository зависит от абстракции (интерфейса), а не от конкретной реализации. Это упрощает замену базы данных и тестирование.

Заключение

SOLID

Блоги

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