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