Принцип DRY на примере Laravel

от автора

Рассмотрим простой модуль, отвечающий за добавление новых пользователей.
И на его примере увидим, какие возможности открывает применение принципа DRY.

Для меня принцип DRY (Don’t Repeat Yourself) всегда воплощался в двух основных определениях:

  1. Дублирование знаний — всегда нарушение принципа
  2. Дублирование кода — не всегда нарушение принципа

Начнем с контроллеров содержащих минимальное количество логики.

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);          }     } 

Вывод

Здесь приведен простой пример. Реальная польза начинает проявляться, как только начинает расти сложность. Мы всегда знаем, что код находится в одном месте и его границы четко определены.

Получаем следующие преимущества: предотвращает дублирование, упрощает тестирование
и открывает дорогу к применению других принципов и паттернов проектирования.

ссылка на оригинал статьи https://habr.com/ru/post/491728/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *