Principio SECO con Laravel

Considere un módulo simple que se encarga de agregar nuevos usuarios.

Y con su ejemplo veremos qué posibilidades abre la aplicación del principio DRY.

Para mí, el principio de DRY (No te repitas) siempre se ha plasmado en dos definiciones básicas:

  1. La duplicación del conocimiento es siempre una violación del principio.
  2. La duplicación de código no siempre es una violación del principio

Comencemos con los controladores que contienen la cantidad 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);
    }
}

En la etapa inicial, tal repetición del código parece bastante inofensiva.
Pero ya tenemos duplicación de conocimiento, y la duplicación de conocimiento está prohibida.
Para hacer esto, generalizamos la creación de usuarios en la clase 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();
    }    
}

Habiendo trasladado toda la lógica de trabajar con el modelo al servicio, nos deshacemos de su duplicación en el controlador. Pero tenemos otro problema. Digamos que tenemos que complicar un poco el proceso de creación de un usuario.

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
}

Poco a poco, la clase UserService comenzará a crecer y corremos el riesgo de obtener una superclase con una gran cantidad de dependencias.

Clase de acción única CreateUser


Para evitar tales consecuencias, puede dividir el servicio en clases de una sola acción.

Requisitos básicos para esta clase:

  • Nombre que representa la acción a realizar
  • Tiene un único método público (utilizaré el método mágico __invoke )
  • Tiene todas las dependencias necesarias.
  • Garantiza el cumplimiento de todas las reglas comerciales dentro de sí mismo, genera una excepción cuando se violan

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

Ya tenemos una verificación de campo de correo electrónico en la clase CreateRequet, pero es lógico agregar una verificación aquí también. Esto refleja con mayor precisión la lógica empresarial de crear un usuario y también simplifica la depuración.

Los controladores toman la siguiente forma

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, tenemos una lógica completamente aislada para crear un usuario. Es conveniente modificar y ampliar.

Ahora veamos qué ventajas nos brinda este enfoque.

Por ejemplo, hay una tarea para importar usuarios.

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

Tenemos la oportunidad de reutilizar el código incrustándolo en el método Collection :: map (). Y también procesamos para nuestros usuarios con necesidades cuyas direcciones de correo electrónico no son únicas.

Vendaje


Supongamos que necesitamos registrar cada nuevo usuario en un archivo.

Para hacer esto, no integraremos esta acción en la clase CreateUser, sino que utilizaremos el patrón Decorator.

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

Luego, utilizando el contenedor Laravel IoC , podemos asociar la clase LogCreateUser con la clase CreateUser , y la primera se implementará cada vez que necesitemos una instancia de la segunda.

class AppServiceProvider extends ServiceProvider
{

    // ...

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

También tenemos la oportunidad de realizar la configuración de creación de usuarios utilizando una variable en el archivo de configuración.

class AppServiceProvider extends ServiceProvider
{

    // ...

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

Conclusión


Aquí hay un ejemplo simple. Los beneficios reales comienzan a aparecer a medida que la complejidad comienza a crecer. Siempre sabemos que el código está en un lugar y sus límites están claramente definidos.

Obtenemos las siguientes ventajas: evita la duplicación, simplifica las pruebas
y abre el camino a la aplicación de otros principios y patrones de diseño.

All Articles