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 {