И на его примере увидим, какие возможности открывает применение принципа 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);
}
}
Вывод
Здесь приведен простой пример. Реальная польза начинает проявляться, как только начинает расти сложность. Мы всегда знаем, что код находится в одном месте и его границы четко определены.
Получаем следующие преимущества: предотвращает дублирование, упрощает тестирование
и открывает дорогу к применению других принципов и паттернов проектирования.
DExploN
Хорошая, простая и понятная статья. По сути вы пришли к паттерну команда и архитетуре CQRS. Последнее время чем чаще думаю об архитетуре, тем больше приходит идей переходить на команды.
Нужно сделать декоратор? Проще сделать с командой, так как не нужно будет 100500 методов заглушек, если бы делали декоратор для большого сервиса и его одного метода.
Нужно выполнить очередь (или перевести какой то фунцкионал на нее) — команда как раз для этого отлично подходит.
Единственное но — это больше кода, а если проект небольшой, то плюсы которые выходят в статье — просто могут не понадобиться в небольшом проекте.
Неплохой доклад на тему CQRS, как раз с разбором нарастающих проблем.
www.youtube.com/watch?v=mvIXCgwGf9E