Рассмотрим простой модуль, отвечающий за добавление новых пользователей.
И на его примере увидим, какие возможности открывает применение принципа DRY.
Для меня принцип DRY (Don't Repeat Yourself) всегда воплощался в двух основных определениях:
Начнем с контроллеров содержащих минимальное количество логики.
На начальном этапе такое повторение кода кажется довольно безобидным.
Но мы уже имеем дублирование знаний, а знания дублировать запрещено.
Для этого обобщим создание пользователя в классе UserService
Переместив всю логику работы с моделью в сервис, избавляемся от ее дублирования в контроллере. Но у нас появляется другая проблема. Допустим, нам предстоит немного усложнить процесс создания пользователя.
Постепенно класс UserService начнет разрастаться и мы рискуем получить супер класс с огромным количеством зависимостей.
Для того чтобы избежать таких последствий, можно разбить сервис на классы единого действия.
Основные требования к такому классу:
У нас уже есть проверка поля email в классе CreateRequet, но логично добавить проверку и сюда. Это более точно отображает бизнес логику создания пользователя, а также упрощает отладку.
Контроллеры обретают следующий вид
В итоге имеем полностью изолированную логику создания пользователя. Ее удобно изменять и расширять.
Теперь посмотрим какие преимущества нам дает такой подход.
Например, есть задача импортировать пользователей.
Получаем возможность повторного использования кода, встраивая его в метод Collection::map(). А так же обработать под наши нужды пользователей, чьи email адреса не являются уникальными.
Допустим нам необходимо регистрировать каждого нового пользователя в файл.
Для этого мы не будем встраивать это действие в сам класс CreateUser, а воспользуемся паттерном Декоратор.
Затем, используя IoC-контейнер Laravel, мы можем связать класс LogCreateUser с классом CreateUser, и первый будет внедрен каждый раз, когда нам понадобиться экземпляр второго.
Мы также имеем возможность вынести настройку создания пользователя с помощью переменной в файле конфигурации.
Здесь приведен простой пример. Реальная польза начинает проявляться, как только начинает расти сложность. Мы всегда знаем, что код находится в одном месте и его границы четко определены.
Получаем следующие преимущества: предотвращает дублирование, упрощает тестирование
и открывает дорогу к применению других принципов и паттернов проектирования.
И на его примере увидим, какие возможности открывает применение принципа DRY.
Для меня принцип DRY (Don't Repeat Yourself) всегда воплощался в двух основных определениях:
- Дублирование знаний — всегда нарушение принципа
- Дублирование кода — не всегда нарушение принципа
Начнем с контроллеров содержащих минимальное количество логики.
class UserController { public function create(CreateRequest $request) { $user = User::create($request->all()); return view('user.created', compact('user')); } } class UserApiController { public function create(CreateRequest $request) { $user = User::create($request->all()); return response()->noContent(201); } }
На начальном этапе такое повторение кода кажется довольно безобидным.
Но мы уже имеем дублирование знаний, а знания дублировать запрещено.
Для этого обобщим создание пользователя в классе UserService
class UserService { public function create(array $data): User { $user = new User; $user->email = $data['email']; $user->password = $data['password']; $user->save(); return $user; } public function delete($userId): bool { $user = User::findOrFail($userId); return $user->delete(); } }
Переместив всю логику работы с моделью в сервис, избавляемся от ее дублирования в контроллере. Но у нас появляется другая проблема. Допустим, нам предстоит немного усложнить процесс создания пользователя.
class UserService { protected $blogService; public function __construct(BlogService $blogService) { $this->blogService = $blogService; } public function create(array $data): User { $user = new User; $user->email = $data['email']; $user->password = $data['password']; $user->save(); $blog = $this->blogService->create(); $user->blogs()->attach($blog); return $user; } //Other methods }
Постепенно класс UserService начнет разрастаться и мы рискуем получить супер класс с огромным количеством зависимостей.
Класс единого действия CreateUser
Для того чтобы избежать таких последствий, можно разбить сервис на классы единого действия.
Основные требования к такому классу:
- Имя отображающее действие которое предстоит выполнить
- Имеет единственный публичный метод (я буду использовать магический метод __invoke)
- Имеет внутри себя все необходимые зависимости
- Обеспечивает внутри себя соблюдение всех бизнес правил, генерирует исключение при их нарушении
class CreateUser { protected $blogService; public function __construct(BlogService $blogService) { $this->blogService = $blogService; } public function __invoke(array $data): User { $email = $data['email']; if (User::whereEmail($email)->first()) { throw new EmailNotUniqueException("$email should be unique!"); } $user = new User; $user->email = $data['email']; $user->password = $data['password']; $user->save(); $blog = $this->blogService->create(); $user->blogs()->attach($blog); return $user; } }
У нас уже есть проверка поля email в классе CreateRequet, но логично добавить проверку и сюда. Это более точно отображает бизнес логику создания пользователя, а также упрощает отладку.
Контроллеры обретают следующий вид
class UserController { public function create(CreateRequest $request, CreateUser $createUser) { $user = $createUser($request->all()); return view('user.created', compact('user')); } } class UserApiController { public function create(CreateRequest $request, CreateUser $createUser) { $user = $createUser($request->all()); return response()->noContent(201); } }
В итоге имеем полностью изолированную логику создания пользователя. Ее удобно изменять и расширять.
Теперь посмотрим какие преимущества нам дает такой подход.
Например, есть задача импортировать пользователей.
class ImportUser { protected $createUser; public function __construct(CreateUser $createUser) { $this->createUser = $createUser; } public function handle(array $rows): Collection { return collect($rows)->map(function (array $row) { try { return $this->createUser($row); } catch (EmailNotUniqueException $e) { // Deal with duplicate users } }); } }
Получаем возможность повторного использования кода, встраивая его в метод Collection::map(). А так же обработать под наши нужды пользователей, чьи email адреса не являются уникальными.
Декорирование
Допустим нам необходимо регистрировать каждого нового пользователя в файл.
Для этого мы не будем встраивать это действие в сам класс CreateUser, а воспользуемся паттерном Декоратор.
class LogCreateUser extends CreateUser { public function __invoke(array $data) { Log::info("A new user has registered: " . $data['email']); parent::__invoke($data); } }
Затем, используя IoC-контейнер Laravel, мы можем связать класс LogCreateUser с классом CreateUser, и первый будет внедрен каждый раз, когда нам понадобиться экземпляр второго.
class AppServiceProvider extends ServiceProvider { // ... public function register() { $this->app->bind(CreateUser::class, LogCreateUser::class); }
Мы также имеем возможность вынести настройку создания пользователя с помощью переменной в файле конфигурации.
class AppServiceProvider extends ServiceProvider { // ... public function register() { if (config("users.log_registration")) { $this->app->bind(CreateUser::class, LogCreateUser::class); } }
Вывод
Здесь приведен простой пример. Реальная польза начинает проявляться, как только начинает расти сложность. Мы всегда знаем, что код находится в одном месте и его границы четко определены.
Получаем следующие преимущества: предотвращает дублирование, упрощает тестирование
и открывает дорогу к применению других принципов и паттернов проектирования.
