"Controladores delgados, modelos delgados", modelos de Laravel mantenibles.
Published: 2021-05-07 06:01:09

¿Alguna vez has escuchado la frase "Modelos delgados, Controladores gordos"?

Hablando en una arquitectura Modelo, vista, controlador (MVC), la frase se refiere a una filosofía cuyo objetivo es delegar mayor responsabilidad a los controladores e intentar dejar los modelos con la menor cantidad de líneas posibles. 

Al igual que esta filosofía, también existe su contraparte: "Modelos gordos, Controladores delgados" que como imaginarás, se refiere a la filosofía de delegar mayor carga a los modelos e intentar dejar los controladores con la menor cantidad de líneas de código 

Esta última ha sido adoptada por múltiples frameworks populares, ya que la mayoría de ellos cuentan con un ORM (Object-Relational Mapping) que permite explotar de manera muy práctica la base de datos. Un ejemplo de estos frameworks, cuyo ORM es una de las características más fuerte, es Laravel.

Gracias a las características de Laravel, en los modelos solemos mapear la tabla de la base de datos, definir relaciones, colocar accesores, modificadores, eventos, colecciones, alcances, etc... Teniendo al final clases enormes y poco mantenibles.

El objetivo de Laravel no es precisamente que los modelos sean poco mantenibles, simplemente se brindan las herramientas para volverlo muy práctico. Aquí es donde comienza nuestra labor como desarrolladores de software al ejercer las mejores prácticas para no llegar a ese punto.

Primero que nada, debemos entender que "modelo" no es sinónimo de "lógica de negocio". Un modelo es la representación abstracta de un objeto de dominio, toda la lógica de negocio debe ser manejada por otras clases, pudiendo ser "servicios" o "acciones". Entender esto es crucial para poder comenzar a delegar responsabilidades a clases independientes.

La idea en general de mantener modelos mantenibles en Laravel, es mantener el mapeo de la base de datos, definición de relaciones, casteos y accesores simples que no requieran cálculos.

### Delegando queries a un QueryBuilder personalizado.

Si requerimos crear fragmentos de query que puedan ser frecuentemente utilizados, también conocidos como "scopes", laravel nos provee una forma de hacerlo en 2 sencillos pasos:

1. Crear una clase que extienda de `Illuminate\Database\Eloquent\Builder` en dónde vivirán todos los queries reutilizables necesarios para el proyecto.
    ```php
        namespace App\QueryBuilders;

        use Illuminate\Database\Eloquent\Builder;

        class UserQueryBuilder extends Builder
        {
            public function whereActive(): self
            {
                return $this->where('status', User::ACTIVE_STATUS)
            }
        }
    ```
2. Indicar en nuestro modelo que utilice el `Builder` que acabamos de crear.
    ```php
        namespace App\Models;

        use  App\QueryBuilders\UserQueryBuilder;

        class User extends Model
        {
            public function newEloquentBuilder($builder): UserQueryBuilder
            {
                return new UserQueryBuilder($query);
            }
        }
    ```

De esta manera, Laravel sabrá que debe utilizar nuestro Builder personalizados y podremos hacer uso de él cuando sea necesario, ejemplo:

```php
User::whereActive()->get();
```

### Moviendo colecciones a Collection personalizada.

Si la intención es utilizar las colecciones para manipular y/o filtrar datos, Laravel nos provee la facilidad de utilizar colecciones personalizadas, nuevamente en 2 pasos:

1. Crear una clase que extienda de `Illuminate\Database\Eloquent\Collection` en el que colocaremos todas las funciones relacionadas a la colección.
    ```php
        namespace App\Collections;

        use Illuminate\Database\Eloquent\Collection;

        class UserCollection extends Collection
        {
            public function active(): self
            {
                return $this->filter(fn (User $user) => $user->email_verified_at !== null);
            }
        }
    ```
2. Indicar en nuestro modelo que la colección por defecto será la clase que acabamos de crear.
    ```php
        namespace App\Models;

        use  App\Collections\UserCollection;

        class User extends Model
        {
            public function newCollection(array $models): UserCollection
            {
                return new UserCollection($models);
            }
        }
    ```

Nuevamente, al hacer ésto podremos hacer uso de una maner más fluida y sin cargar demasiado nuestros modelos. Ejemplo:
```php
User::all()->active();
```

### Delegando lógica de eventos

Uno de los mecanismos más importantes a la hora de delegar responsabilidades, es utilizar eventos y subscriptores para llevar a cabo aquellos procesos de "efecto secundario". Por default, los modelos de laravel son capaces de emitir eventos genéricos en diversas acciones, como al ser creados, modificados o eliminados. Nosotros podemos tomar ventaja de estos para mantener nuestros procesos aislados.

La manera de lograrlo es a través de 4 pasos:

1. Crear un evento que funcionará como DTO, regularmente transportará el modelo afectado.
   ```php
       namespace App\Events;

       use App\Models\User;

       class UserSavingEvent
       {
           public User $user;

           public function __construct(User $user)
           {
               $this->user = $user;
           }
       }
   ```
2. Crear un subscriptor para dicho evento, nótese que la definición de qué evento se escucha se encuentra en el método `subscribe`:
   ```php
       namespace App\Subscribers;

       use App\Events\UserSavingEvent;
       use App\Services\NotifyUserCreation;
       use Illuminate\Events\Dispatcher;

       class UserSubscriber
       {
           private NotifyUserCreation $notifyUserCreation;

           public function __construct(NotifyUserCreation $notifyUserCreation)
           {
               $this->notifyUserCreation = $notifyUserCreation;
           }

           public function saving(UserSavingEvent $event): void
           {
               ($this->notifyUserCreation)($event->user);
           }
           
           public function subscribe(Dispatcher $dispatcher): void
           {
               $dispatcher->listen(
                   InvoiceSavingEvent::class,
                   self::class . '@saving'
               );
           }
       }
   ```
3. Ahora debemos registrar al suscriptor dentro de nuestro `EventServiceProvider`
   ```php
       use App\Subscribers\UserSubscriber;

       class EventServiceProvider extends ServiceProvider
       {
           protected $subscribe = [
               UserSubscriber::class,
           ];
       }
   ```
4. Finalmente debemos indicarle a nuestro modelo, en la propiedad `$dispatchesEvents` aquellos eventos que queremos despachar:
   ```php
       use App\Events\UserSavingEvent;

       class Invoice extends Model
       {
           protected $dispatchesEvents = [
               'saving' => UserSavingEvent::class,
           ];
       }
   ```


Esta solución parece un poco larga, sin embargo, es muy escalable, si en algún momento se requiere quitar o agregar funcionalidad, nuestro Modelo se va a ver afectado en menor escala. Además separaremos toda aquella lógica en Subscribers separados, haciendo piezas más pequeñas y mantenibles.

### Conclusión

Aunque realmente no parezca de gran impacto, comenzar a delegar responsabilidades de los modelos a clases con responsabilidades únicas nos va a ayudar a mantener nuestros modelos delgados y nuestro código en general mantenible. Y si seguimos con la idea de encapsular lógica de negocio en "servicios" o "acciones", podemos concluir que lo ideal es tener "controladores delgados y modelos delgados".

En este post hablamos de Laravel en particular, sin embargo, estoy casi seguro de que los frameworks en general tienden a brindar características para que desarrollemos software a toda velocidad, sin embargo, es necesario detenernos a pensar si vale la pena sacrificar practicidad por calidad.

Te invito a que pongas en práctica estas maneras de optimizar tus modelos y si vienes de un lenguaje o framework diferente, compártenos de qué manera solucionar estos problemas.

Hasta la próxima.