Skip to content

Практическая работа: Принципы SOLID

Цель работы: Научиться выявлять нарушения принципов SOLID в объектно-ориентированном программном коде и выполнять рефакторинг программы с использованием абстракций, интерфейсов и внедрения зависимостей.

Основные теоретические положения

Section titled “Основные теоретические положения”

1. Принципы SOLID

SOLID — набор из пяти принципов объектно-ориентированного проектирования, направленных на снижение связанности программных компонентов и повышение расширяемости кода.

ПринципРасшифровкаОсновная идея
SSingle Responsibility PrincipleУ класса должна быть одна основная ответственность
OOpen/Closed PrincipleПрограммные компоненты должны быть открыты для расширения, но закрыты для изменения
LLiskov Substitution PrincipleОбъект производного типа должен корректно заменять объект базового типа
IInterface Segregation PrincipleЛучше несколько небольших интерфейсов, чем один перегруженный
DDependency 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'
);

Запустите программу.

Ответьте письменно:

  1. Какие действия выполняет OrderService?
  2. Сколько разных обязанностей можно выделить у класса?
  3. Какие части класса придётся изменить при:
    • смене способа хранения заказов;
    • добавлении нового способа оплаты;
    • переходе с Email на Telegram?
  4. Какие принципы SOLID нарушены в исходной реализации?

Задание 1.2. Применение SRP

Разделите обязанности исходного класса.

Создайте классы:

OrderValidator
OrderCalculator
OrderRepository
EmailNotifier

Класс 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;
}

Создайте реализации:

CardPayment
SbpPayment
CashPayment

Пример:

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(
'Для самовывоза номер отслеживания отсутствует'
);
}
}

Ответьте:

  1. Почему StorePickup нельзя безопасно подставить вместо любого Delivery?
  2. Какое обещание базового типа нарушается?
  3. Почему выбрасывание исключения в 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
) {
}
}

Создайте реализации:

MySqlOrderRepository
EmailNotifier

Дополнительно реализуйте один вариант:

FileOrderRepository
TelegramNotifier

После замены реализации класс 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:

  1. добавьте новый способ оплаты;
  2. добавьте новый способ хранения заказов или уведомления.

Например:

CryptoPayment
FileOrderRepository
TelegramNotifier

Продемонстрируйте работу новых компонентов.


Задание 2.3. Анализ выполненного рефакторинга

Заполните таблицу:

ПринципЧто было до рефакторингаЧто изменено
SRP
OCP
LSP
ISP
DIP

После заполнения ответьте:

  1. Какие классы появились после рефакторинга?
  2. Почему увеличение количества классов в данном случае не является недостатком?
  3. Какой компонент теперь проще всего заменить?
  4. Какой принцип SOLID сильнее всего уменьшил связанность программы?
  1. Что означает принцип единственной ответственности?
  2. В чём заключается принцип Open/Closed?
  3. Почему большое количество if/elseif при выборе поведения может усложнять расширение программы?
  4. В чём заключается принцип подстановки Барбары Лисков?
  5. Почему наследник не должен нарушать контракт базового типа?
  6. Для чего применяется принцип разделения интерфейсов?
  7. Что означает зависимость от абстракции?
  8. Что такое внедрение зависимостей?
  9. Чем передача зависимости через конструктор лучше создания объекта внутри класса?
  10. Как применение SOLID влияет на сопровождение и расширение информационной системы?

Рекомендуемая литература

Section titled “Рекомендуемая литература”
  1. Роберт Мартин. Чистая архитектура. Искусство разработки программного обеспечения.
  2. Роберт Мартин. Чистый код. Создание, анализ и рефакторинг.
  3. PHP Manual — Classes and Objects.
  4. PHP Manual — Object Interfaces.