Практическая работа: Принципы SOLID
Цель работы: Научиться выявлять нарушения принципов SOLID в объектно-ориентированном программном коде и выполнять рефакторинг программы с использованием абстракций, интерфейсов и внедрения зависимостей.
Основные теоретические положения
Section titled “Основные теоретические положения”1. Принципы SOLID
SOLID — набор из пяти принципов объектно-ориентированного проектирования, направленных на снижение связанности программных компонентов и повышение расширяемости кода.
| Принцип | Расшифровка | Основная идея |
|---|---|---|
S | Single Responsibility Principle | У класса должна быть одна основная ответственность |
O | Open/Closed Principle | Программные компоненты должны быть открыты для расширения, но закрыты для изменения |
L | Liskov Substitution Principle | Объект производного типа должен корректно заменять объект базового типа |
I | Interface Segregation Principle | Лучше несколько небольших интерфейсов, чем один перегруженный |
D | Dependency Inversion Principle | Высокоуровневый код должен зависеть от абстракций, а не от конкретных реализаций |
2. Single Responsibility Principle
Принцип единственной ответственности означает, что класс должен отвечать за одну логически связанную область поведения.
Например, класс оформления заказа не должен одновременно:
- проверять входные данные;
- рассчитывать стоимость;
- сохранять заказ;
- выполнять оплату;
- отправлять уведомления.
Чем больше различных причин для изменения у класса, тем сложнее его сопровождать.
3. Open/Closed Principle
Программный компонент должен позволять добавлять новое поведение без изменения уже работающего кода.
Плохой пример:
if ($paymentType === 'card') { // оплата картой} elseif ($paymentType === 'sbp') { // оплата через СБП}При появлении нового способа оплаты приходится изменять существующую конструкцию.
Более гибкий подход — общий интерфейс:
interface PaymentMethod{ public function pay(float $amount): void;}Новый способ оплаты реализует этот интерфейс, а существующий код остаётся без изменений.
4. Liskov Substitution Principle
Объект дочернего класса должен использоваться вместо объекта родительского класса без нарушения работы программы.
Если базовый тип обещает определённое поведение, наследник не должен неожиданно запрещать это поведение или выбрасывать исключение в обычной ситуации.
5. Interface Segregation Principle
Класс не должен зависеть от методов, которые ему не нужны.
Плохо:
interface Storage{ public function save(array $data): void; public function delete(int $id): void; public function export(): void; public function backup(): void; public function sendReport(): void;}Если конкретному классу нужны только save() и delete(), интерфейс содержит лишние обязанности.
6. Dependency Inversion Principle
Высокоуровневые классы не должны создавать конкретные зависимости самостоятельно.
Плохо:
class OrderService{ private MySqlOrderRepository $repository;
public function __construct() { $this->repository = new MySqlOrderRepository(); }}Лучше передавать зависимость через интерфейс:
class OrderService{ public function __construct( private OrderRepositoryInterface $repository ) { }}В этом случае конкретную реализацию можно заменить без изменения OrderService.
Задания для выполнения
Section titled “Задания для выполнения”Часть 1. Анализ и рефакторинг исходного кода
Задание 1.1. Анализ исходного класса
Создайте файл index.php и поместите в него следующий код:
<?php
class OrderService{ public function createOrder( array $items, string $email, string $paymentType ): void { if (empty($items)) { throw new Exception('Корзина пуста'); }
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new Exception('Некорректный email'); }
$total = 0;
foreach ($items as $item) { $total += $item['price'] * $item['quantity']; }
echo "Заказ сохранён в MySQL\n";
if ($paymentType === 'card') { echo "Оплата банковской картой: {$total} руб.\n"; } elseif ($paymentType === 'sbp') { echo "Оплата через СБП: {$total} руб.\n"; } elseif ($paymentType === 'cash') { echo "Оплата наличными: {$total} руб.\n"; } else { throw new Exception('Неизвестный способ оплаты'); }
echo "Email отправлен на {$email}\n"; }}
$service = new OrderService();
$service->createOrder( [ ['name' => 'Клавиатура', 'price' => 3500, 'quantity' => 1], ['name' => 'Мышь', 'price' => 1500, 'quantity' => 2], ], 'student@example.com', 'card');Запустите программу.
Ответьте письменно:
- Какие действия выполняет
OrderService? - Сколько разных обязанностей можно выделить у класса?
- Какие части класса придётся изменить при:
- смене способа хранения заказов;
- добавлении нового способа оплаты;
- переходе с Email на Telegram?
- Какие принципы SOLID нарушены в исходной реализации?
Задание 1.2. Применение SRP
Разделите обязанности исходного класса.
Создайте классы:
OrderValidatorOrderCalculatorOrderRepositoryEmailNotifierКласс OrderValidator должен проверять:
- наличие товаров;
- корректность email.
Класс OrderCalculator должен рассчитывать общую стоимость товаров.
Пример:
class OrderCalculator{ public function calculate(array $items): float { $total = 0;
foreach ($items as $item) { $total += $item['price'] * $item['quantity']; }
return $total; }}После рефакторинга OrderService не должен самостоятельно выполнять валидацию, вычисление стоимости, сохранение и отправку уведомлений.
Проверьте, что результат работы программы не изменился.
Задание 1.3. Применение OCP
Создайте интерфейс:
interface PaymentMethod{ public function pay(float $amount): void;}Создайте реализации:
CardPaymentSbpPaymentCashPaymentПример:
class CardPayment implements PaymentMethod{ public function pay(float $amount): void { echo "Оплата банковской картой: {$amount} руб.\n"; }}Измените OrderService так, чтобы способ оплаты передавался объектом PaymentMethod.
После этого добавьте новый способ оплаты:
CryptoPaymentили собственный вариант.
Условие: добавление нового способа оплаты не должно требовать изменения существующих классов оплаты.
Задание 1.4. Применение LSP
Рассмотрите код:
abstract class Delivery{ abstract public function deliver(): void;
abstract public function getTrackingNumber(): string;}
class CourierDelivery extends Delivery{ public function deliver(): void { echo "Заказ передан курьеру\n"; }
public function getTrackingNumber(): string { return 'TRACK-123'; }}
class StorePickup extends Delivery{ public function deliver(): void { echo "Заказ ожидает покупателя в магазине\n"; }
public function getTrackingNumber(): string { throw new Exception( 'Для самовывоза номер отслеживания отсутствует' ); }}Ответьте:
- Почему
StorePickupнельзя безопасно подставить вместо любогоDelivery? - Какое обещание базового типа нарушается?
- Почему выбрасывание исключения в
getTrackingNumber()здесь является архитектурной проблемой?
Создайте отдельный интерфейс:
interface TrackableDelivery{ public function getTrackingNumber(): string;}Измените код так, чтобы отслеживание реализовывали только виды доставки, которые действительно его поддерживают.
Задание 1.5. Применение ISP
Дан интерфейс:
interface OrderStorage{ public function save(array $order): void;
public function delete(int $id): void;
public function exportToCsv(): void;
public function createBackup(): void;
public function sendReport(): void;}Разделите его на несколько небольших интерфейсов.
Обязательно должен присутствовать интерфейс:
interface OrderRepositoryInterface{ public function save(array $order): void;
public function delete(int $id): void;}Самостоятельно создайте интерфейсы для:
- экспорта;
- резервного копирования;
- формирования или отправки отчёта.
Создайте минимум по одной реализации необходимых интерфейсов.
Задание 1.6. Применение DIP
Исходная зависимость:
class OrderService{ private MySqlOrderRepository $repository; private EmailNotifier $notifier;
public function __construct() { $this->repository = new MySqlOrderRepository(); $this->notifier = new EmailNotifier(); }}Создайте интерфейсы:
interface OrderRepositoryInterface{ public function save(array $order): void;}interface NotifierInterface{ public function send( string $recipient, string $message ): void;}Измените OrderService, передавая зависимости через конструктор:
class OrderService{ public function __construct( private OrderRepositoryInterface $repository, private NotifierInterface $notifier ) { }}Создайте реализации:
MySqlOrderRepositoryEmailNotifierДополнительно реализуйте один вариант:
FileOrderRepositoryTelegramNotifierПосле замены реализации класс OrderService изменяться не должен.
Часть 2. Задания для самостоятельного выполнения
Задание 2.1. Итоговая сборка приложения
Соберите созданные компоненты в одну программу.
Минимальная структура проекта:
solid-practice/├── index.php└── src/ ├── OrderService.php ├── OrderValidator.php ├── OrderCalculator.php ├── Payment/ │ ├── PaymentMethod.php │ ├── CardPayment.php │ ├── SbpPayment.php │ └── CashPayment.php ├── Repository/ │ ├── OrderRepositoryInterface.php │ └── MySqlOrderRepository.php ├── Notification/ │ ├── NotifierInterface.php │ └── EmailNotifier.php └── Delivery/ ├── Delivery.php └── TrackableDelivery.phpДопускается другая структура проекта при условии логичного разделения классов по назначению.
Задание 2.2. Проверка расширяемости
Не изменяя основной класс OrderService:
- добавьте новый способ оплаты;
- добавьте новый способ хранения заказов или уведомления.
Например:
CryptoPaymentFileOrderRepositoryTelegramNotifierПродемонстрируйте работу новых компонентов.
Задание 2.3. Анализ выполненного рефакторинга
Заполните таблицу:
| Принцип | Что было до рефакторинга | Что изменено |
|---|---|---|
| SRP | ||
| OCP | ||
| LSP | ||
| ISP | ||
| DIP |
После заполнения ответьте:
- Какие классы появились после рефакторинга?
- Почему увеличение количества классов в данном случае не является недостатком?
- Какой компонент теперь проще всего заменить?
- Какой принцип SOLID сильнее всего уменьшил связанность программы?
Контрольные вопросы
Section titled “Контрольные вопросы”- Что означает принцип единственной ответственности?
- В чём заключается принцип Open/Closed?
- Почему большое количество
if/elseifпри выборе поведения может усложнять расширение программы? - В чём заключается принцип подстановки Барбары Лисков?
- Почему наследник не должен нарушать контракт базового типа?
- Для чего применяется принцип разделения интерфейсов?
- Что означает зависимость от абстракции?
- Что такое внедрение зависимостей?
- Чем передача зависимости через конструктор лучше создания объекта внутри класса?
- Как применение SOLID влияет на сопровождение и расширение информационной системы?
Рекомендуемая литература
Section titled “Рекомендуемая литература”- Роберт Мартин. Чистая архитектура. Искусство разработки программного обеспечения.
- Роберт Мартин. Чистый код. Создание, анализ и рефакторинг.
- PHP Manual — Classes and Objects.
- PHP Manual — Object Interfaces.