Принцип единой ответственности (SRP) гласит, что никогда не должно быть более одной причины для изменения класса. Другими словами, каждый класс должен иметь только одну ответственность. Модуль должен быть ответственным перед одним и только одним актером. Термин «субъект» относится к группе (состоящей из одного или нескольких заинтересованных сторон или пользователей), которой требуется изменение в модуле.
Проще говоря: Делайте одно дело и делайте его хорошо.
Пример из реальной жизни: представьте, что вы нанимаете шеф-повара в ресторан.
- Плохой дизайн (шеф-повар «делает все»): вы нанимаете повара, который должен готовить еду, мыть посуду, рассчитывать налоги ресторана и ремонтировать сантехнику, когда лопается труба.
- Проблема: если налоговое законодательство изменится, ваш повар будет занят изучением бухгалтерского учета, а не готовкой. Если они испортят водопровод, кухню затопит, и весь ресторан закроется. Одно изменение портит все.
- Хороший дизайн (применяется SRP): вы нанимаете повара для приготовления пищи, посудомоечную машину для уборки, бухгалтера для уплаты налогов и сантехника для ремонта.
- Преимущество: если налоговое законодательство изменится, только бухгалтер изменит свой рабочий процесс. Шеф-повар продолжает готовить без перерыва.
❌ Неправильный подход: класс со слишком большим количеством заданий.
Этот класс User обрабатывает пользовательские данные, подключения к базе данных и доставку электронной почты. У него есть три причины для изменения.
class User {
private $name;
private $email;
public function __construct($name, $email) {
$this->name = $name;
$this->email = $email;
}
// Job 1: Manage User Data
public function getName() { return $this->name; }
// Job 2: Database Operations (Reason to change: Switching database types)
public function saveToDatabase() {
echo "Saving user to database...\n";
}
// Job 3: Notification Logic (Reason to change: Switching from Email to SMS)
public function sendWelcomeEmail() {
echo "Sending email to " . $this->email . "\n";
}
}
✅ Правильный подход: разделение обязанностей
Мы разбиваем большой класс на три крошечных специализированных класса. У каждого ровно одно задание.
// 1. Responsible ONLY for user data representation
class User {
private $name;
private $email;
public function __construct($name, $email) {
$this->name = $name;
$this->email = $email;
}
public function getName() { return $this->name; }
public function getEmail() { return $this->email; }
}
// 2. Responsible ONLY for saving data
class UserRepository {
public function save(User $user) {
echo "Saving " . $user->getName() . " to the database.\n";
}
}
// 3. Responsible ONLY for sending messages
class EmailService {
public function sendWelcome(User $user) {
echo "Sending email to " . $user->getEmail() . "\n";
}
}
Почему это вам поможет
- Удобство сопровождения. Когда классы имеют единую, четко определенную ответственность, их легче понять и изменить.
- Тестируемость: легче писать модульные тесты для классов с одним фокусом.
- Гибкость: изменения одной ответственности не затрагивают несвязанные части системы.
- Возможность повторного использования: вы можете использовать EmailService для отправки электронных писем с заказами, сбросами настроек или другими функциями.
Раскрыты три основные путаницы при внедрении SRP
1. Путаница: «Означает ли «одна ответственность» что класс может иметь только один метод?»
- Реальность: Нет. Ответственность — это целостная деловая роль, а не отдельное действие.
- Пример. Класс UserRepository может иметь save(), delete(), findById() и update(). Это еще одна обязанность: управление хранилищем пользовательских данных.
2. Путаница: «Что именно определяет «причину перемен»?»
- Реальность: Роберт К. Мартин (создатель SOLID) пояснил, что «причина для перемен» относится к людям, а не к технологиям. Ответственность определяется тем, кто запрашивает изменение (актеры).
- Пример: если бухгалтерия просит об изменении, а отдел кадров просит об изменении, это два разных участника. Они никогда не должны использовать один и тот же класс PHP.
3. Путаница: «Когда я перестану делить занятия?»
- Реальность: инженеры часто разделяют код преждевременно, создавая UserEmailValidator, UserPasswordValidator и UserAgeValidator еще до того, как они им понадобятся. Это вызывает «аналитический паралич».
Трехшаговая матрица решений (как выбрать)
Рассматривая класс PHP, задайте себе эти три строгих вопроса, чтобы решить, нужно ли вам его разделить:
Обслуживает ли этот класс более одного бизнес-отдела/актера?
START SRP_Decision_Process
IF Class_Serves_Multiple_Departments_Or_Actors IS True THEN
EXECUTE Split_Class_Immediately
ELSE
IF Class_Mixes_Different_Technical_Layers IS True THEN
EXECUTE Split_Class
ELSE
EXECUTE Leave_It_Alone
END IF
END IF
END SRP_Decision_Process
- Кто просит эту функцию? Если исправление ошибки в макете электронной почты маркетинговой команды может случайно нарушить расчет счетов бухгалтерской командой, у вашего класса слишком много обязанностей.
- Смешаны ли разные технические уровни? Если один класс PHP содержит необработанные SQL-запросы (SELECT *), HTML-разметку (
<div>) и логику проверки (если (пусто)), это нарушает SRP. Разделите их. - Могу ли я описать класс, не используя слово «И»?
- Плохо: «Этот класс хранит пользовательские данные, сохраняет их в MySQL и отправляет предупреждение Slack». (3 обязанности)
- Хорошо: «Этот класс отправляет оповещения Slack». (1 ответственность)
Реальное правило принятия решения: «Не расставайтесь, пока не станет больно»
Чтобы избежать чрезмерного проектирования, следуйте правилу тактического SRP:
- Если функция небольшая, начните с написания кода в одном классе.
- В тот момент, когда вам нужно повторно использовать его часть (например, использовать логику электронной почты где-то еще), извлеките ее в отдельный класс.
- Как только класс превысит 200 строк кода, проведите его аудит, используя приведенную выше матрицу решений.
Схема Laravel: освоение SRP
Давайте рассмотрим классическую ошибку Laravel: размещение логики регистрации пользователей, вставки базы данных, манипулирования фотографиями профиля и приветственных уведомлений в одном методе контроллера.
❌ Младший подход: контроллер «делай все» (нарушает SRP)
Этот метод контроллера имеет четыре различные причины для изменения: изменения базы данных, правила проверки, настройки оптимизации изображения и изменения носителя уведомлений.
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Intervention\Image\Facades\Image;
use Illuminate\Support\Facades\Mail;
class RegisterController extends Controller
{
public function store(Request $request)
{
// Reason to change 1: Validation rules change
$request->validate([
'name' => 'required',
'email' => 'required|email|unique:users',
'avatar' => 'required|image'
]);
// Reason to change 2: Handling image manipulation
$avatar = $request->file('avatar');
$filename = time() . '.' . $avatar->getClientOriginalExtension();
Image::make($avatar)->resize(300, 300)->save(public_path('/uploads/' . $filename));
// Reason to change 3: Database architecture updates
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'avatar' => $filename,
]);
// Reason to change 4: Switching from Email to Slack/SMS
Mail::to($user->email)->send(new \App\Mail\WelcomeMail($user));
return response()->json(['message' => 'User created successfully!'], 201);
}
}
Подход Laravel Mastery (соответствует SRP)
Чтобы построить это как архитектор инфраструктуры, мы разделяем задачи, используя специальные функции Laravel: запросы форм, классы обслуживания и события/прослушиватели.
Шаг 1. Обработка проверки через форму запроса
Контроллеру не обязательно знать правила проверки. Мы делегируем это в специальный файл запроса.
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class RegisterRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
'avatar' => 'required|image|max:2048'
];
}
}
Шаг 2. Извлечение базовой логики в класс действия/сервиса
Мы создаем специальный класс PHP, единственная задача которого — организовать регистрацию пользователей и обработку файлов.
namespace App\Actions;
use App\Models\User;
use App\Events\UserRegistered;
use Illuminate\Http\UploadedFile;
class RegisterNewUser
{
public function execute(array $data, UploadedFile $avatar): User
{
// Handle the file upload logic cleanly
$path = $avatar->store('avatars', 'public');
$user = User::create([
'name' => $data['name'],
'email' => $data['email'],
'avatar' => $path,
]);
// Fire an event! This class doesn't care HOW notifications are sent.
event(new UserRegistered($user));
return $user;
}
}
Шаг 3. Обработка сообщений через события и прослушиватели
Когда пользователь сохраняется, происходит событие. Выделенный прослушиватель перехватывает его и отправляет почту асинхронно.
namespace App\Listeners;
use App\Events\UserRegistered;
use App\Mail\WelcomeMail;
use Illuminate\Support\Facades\Mail;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendWelcomeNotification implements ShouldQueue
{
public function handle(UserRegistered $event): void
{
// This class only changes if the notification delivery method changes
Mail::to($event->user->email)->send(new WelcomeMail($event->user));
}
}
Шаг 4. Чистый и тонкий главный контроллер
Теперь посмотрим на контроллер. У него ровно одна обязанность: получить HTTP-запрос и вернуть HTTP-ответ. Он хорошо читаем и закрыт для внешних ошибок.
namespace App\Http\Controllers;
use App\Http\Requests\RegisterRequest;
use App\Actions\RegisterNewUser;
class RegisterController extends Controller
{
public function store(RegisterRequest $request, RegisterNewUser $registerAction)
{
// Execution is offloaded to specialized workers
$user = $registerAction->execute(
$request->validated(),
$request->file('avatar')
);
return response()->json(['user' => $user], 201);
}
}
Почему это делает вас мастером Laravel
- Простота тестирования: вы можете написать модульный тест для RegisterNewUser, передав поддельный загруженный файл, даже не отображая HTTP-маршрутизатор и не действуя как веб-браузер.
- Безупречное повторное использование: если вам нужно зарегистрировать пользователя через конечную точку API, веб-форму Blade или команду Artisan терминала, вы просто внедрите класс действия RegisterNewUser и запустите его. Вам никогда не придется дублировать код.
Комментарии (0)
Пока нет комментариев — будьте первым.