Princípio DRY com o Laravel

Considere um módulo simples que é responsável por adicionar novos usuários.

E, por seu exemplo, veremos quais possibilidades a aplicação do princípio DRY abre.

Para mim, o princípio de DRY (não se repita) sempre foi incorporado em duas definições básicas:

  1. A duplicação de conhecimento é sempre uma violação do princípio
  2. A duplicação de código nem sempre é uma violação do princípio

Vamos começar com os controladores que contêm a quantidade mínima de lógica.

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

No estágio inicial, essa repetição do código parece bastante inofensiva.
Mas já temos duplicação de conhecimento, e a duplicação de conhecimento é proibida.
Para fazer isso, generalizamos a criação do usuário na classe 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();
    }    
}

Tendo movido toda a lógica de trabalhar com o modelo para o serviço, nos livramos de sua duplicação no controlador. Mas temos outro problema. Digamos que tenhamos que complicar um pouco o processo de criação de um usuário.

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
}

Gradualmente, a classe UserService começará a crescer e corremos o risco de obter uma superclasse com um grande número de dependências.

Classe de Ação Única CreateUser


Para evitar essas consequências, você pode dividir o serviço em classes de uma única ação.

Requisitos básicos para esta classe:

  • Nome representando a ação a ser executada
  • Tem um único método público (usarei o método mágico __invoke )
  • Tem todas as dependências necessárias
  • Garante a conformidade com todas as regras de negócios em si, gera uma exceção quando elas são violadas

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

Já temos uma verificação de campo de email na classe CreateRequet, mas é lógico adicionar uma verificação aqui também. Isso reflete com mais precisão a lógica comercial da criação de um usuário e também simplifica a depuração.

Os controladores assumem o seguinte formato

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

Como resultado, temos uma lógica completamente isolada para criar um usuário. É conveniente modificar e expandir.

Agora vamos ver quais vantagens essa abordagem nos oferece.

Por exemplo, há uma tarefa para importar usuários.

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

Temos a oportunidade de reutilizar o código incorporando-o no método Collection :: map (). E também processe para as nossas necessidades usuários cujos endereços de email não sejam exclusivos.

Vestir


Suponha que precisamos registrar cada novo usuário em um arquivo.

Para fazer isso, não incorporaremos esta ação na própria classe CreateUser, mas usaremos o padrão Decorator.

class LogCreateUser extends CreateUser 
{
    public function __invoke(array $data)
    {
        Log::info("A new user has registered: " . $data['email']);
        
        parent::__invoke($data);
    }
}

Em seguida, usando o contêiner Laravel IoC , podemos associar a classe LogCreateUser à classe CreateUser , e a primeira será implementada toda vez que precisarmos de uma instância da segunda.

class AppServiceProvider extends ServiceProvider
{

    // ...

    public function register()
    {
        $this->app->bind(CreateUser::class, LogCreateUser::class);
    }

Também temos a oportunidade de fazer a configuração de criação do usuário usando uma variável no arquivo de configuração.

class AppServiceProvider extends ServiceProvider
{

    // ...

    public function register()
    {
         if (config("users.log_registration")) {
             $this->app->bind(CreateUser::class, LogCreateUser::class);
         }
    }

Conclusão


Aqui está um exemplo simples. Benefícios reais começam a aparecer à medida que a complexidade começa a crescer. Sempre sabemos que o código está em um único lugar e seus limites estão claramente definidos.

Temos as seguintes vantagens: evita duplicação, simplifica o teste
e abre caminho para a aplicação de outros princípios e padrões de design.

All Articles