Aplicando el Patrón de Diseño Builder en Laravel

Spread the love

Los patrones de diseño son herramientas que debes de tener como parte de tu día a día y no quiero decir con esto que los debes de utilizar siempre.

Pero déjame decirte que no solo sirven para construir, también sirven para refactorizar código.

y en muchos escenarios son increíblemente buenos para reducir la complejidad.

Por ejemplo es muy común usar el paquete Laravel-Datatable y es útil para lidiar con los casos sencillos pero cuando lo empleas mas a fondo, de forma rápida puedes tener controladores de este estilo.

class AuditLogsController extends Controller
{
    public function index(Request $request)
    {
        if ($request->ajax()) {
            $query = AuditLog::query()->select(sprintf('%s.*', (new AuditLog)->table));
            $table = Datatables::of($query);

            $table->addColumn('placeholder', ' ');
            $table->addColumn('actions', ' ');

            $table->editColumn('actions', function ($row) {
                $viewGate      = 'audit_log_show';
                $editGate      = 'audit_log_edit';
                $deleteGate    = 'audit_log_delete';
                $crudRoutePart = 'audit-logs';

                return view('partials.datatablesActions', compact(
                    'viewGate',
                    'editGate',
                    'deleteGate',
                    'crudRoutePart',
                    'row'
                ));
            });

            $table->editColumn('id', function ($row) {
                return $row->id ? $row->id : "";
            });
            $table->editColumn('description', function ($row) {
                return $row->description ? $row->description : "";
            });
            $table->editColumn('subject_id', function ($row) {
                return $row->subject_id ? $row->subject_id : "";
            });
            $table->editColumn('subject_type', function ($row) {
                return $row->subject_type ? $row->subject_type : "";
            });
            $table->editColumn('user_id', function ($row) {
                return $row->user_id ? $row->user_id : "";
            });
            $table->editColumn('host', function ($row) {
                return $row->host ? $row->host : "";
            });

            $table->rawColumns(['actions', 'placeholder']);

            return $table->make(true);
        }

        return view('admin.auditLogs.index');
    }
    
    //...
}

Creo que se va entendiendo la idea, de que este controlador esta apunto de volverse un problema, Ahora imagina que pasa si estas usando esta clase en otros métodos o controladores!.

Los sorprendente es que este paquete ya es un Builder, así que solo tienes que hacer unos cambios para aprovecharlo y dejar un controlador mas limpio.

Algo como esto:

DatatableDirector::makeFrom(new AuditLogBuilder($query));

Si ya capte tu atención no te distraigas, que vamos a ver como usar el Patrón Builder para refactorizar nuestro código.

Implementación en vídeo

Explicación del problema y creación del Director

Refactorización para adaptar el Patrón Builder

¿Qué es el patrón de diseño Builder? (Pattern Builder)

Abstrae el proceso de creación de un objeto complejo, centralizando dicho proceso en un único punto, de tal forma que el mismo proceso de construcción pueda crear representaciones diferentes.

GOF

El patrón de diseño Builder intenta crear objetos complejos separando el proceso de creación, de tal forma que puedas reutilizar el builder para crear diferentes representaciones del mismo objeto a este objeto se le conoce como Producto.

Sé que no se entiende mucho, pero no te preocupes vamos a verlo mediante un diagrama para que sea mas claro y luego vamos a aplicar la idea a nuestro problema y vas a ver que sencillo es usarlo ya que le agarras el hilo.

Estructura del patrón builder

Este patrón se ve la siguiente forma:

Diagrama de estructura

Figura 1

Si observas la interfaz BuilderInterface separa la solicitud de construcción lo que permite implementar diferentes builders para variar el objecto Producto, sin necesidad de que tengas que cambiar el Director, de esta forma lo podemos reutilizar en otras partes de nuestra aplicación.

Este diagrama lo podemos representar como código de esta forma:

$builder = new Builder();
$product = (new Director($builder))->make();

Como puedes ver el Patrón Builder simplifica mucho el código!.

Ahora voy a adaptar nuestro problema y de esta forma vas a ver como es que vamos a refactorizar nuestro problema.

Adaptando el patrón builder

Bien, acomodando nuestras clases el diagrama queda de la siguiente forma:

Bien, de acuerdo al diagrama debes de crear una clase DatatableDirector que se encargara de crea clases de tipo Concrete, para nuestro ejemplo esta clase la llamaremos AgentLogBuilder, Esta clase tiene que implementar la interfaz DatatableBuilderInterface (Wow creo que se me fue la mano con el nombre). De esta forma el Director puede usar mas de una implementación de Builders concretos.

A nivel de código todo esto lo veremos algo parecido a esto:

$builder = AuditLogBuilder();
$datatable = DirectorDatatable::makeFrom($builder);

¿Interesante?

Veamos como hacerlo.

Implementación del Patrón Builder

Muy bien lo primero que tenemos que hacer es crear la interfaz que se va a llamar DatatableBuilderInterface.

Crear la interfaz

Cada método de esta interfaz agrupara las llamadas correspondientes para los métodos de la clase EloquentDatatable de la siguiente forma.

interface DatatableBuilderInterface
{
    public function buildColumns();

    public function buildEditColumns();

    public function buildRawColumns();
}

Crear el Builder Concreto.

El siguiente paso es crear nuestro Builder concreto que se va a llamar AuditLogBuilder. y va a implementar la interfaz y a extender de EloquentDatatable.

Despues de esto vamos a mover todo el código original a nuestro builder concreto.

Asi que el código quedara de la siguiente forma.

class AuditLogBuilder extends EloquentDataTable implements DatatableBuilderInterface
{

    public function buildColumns()
    {
        $this->addColumn('placeholder', ' ');
        $this->addColumn('actions', ' ');
    }

    public function buildEditColumns()
    {
        $this->editColumn('actions', function ($row) {
            $viewGate      = 'audit_log_show';
            $editGate      = 'audit_log_edit';
            $deleteGate    = 'audit_log_delete';
            $crudRoutePart = 'audit-logs';

            return view('partials.datatablesActions', compact(
                'viewGate',
                'editGate',
                'deleteGate',
                'crudRoutePart',
                'row'
            ));
        });

        $this->editColumn('id', function ($row) {
            return $row->id ? $row->id : "";
        });
        $this->editColumn('description', function ($row) {
            return $row->description ? $row->description : "";
        });
        $this->editColumn('subject_id', function ($row) {
            return $row->subject_id ? $row->subject_id : "";
        });
        $this->editColumn('subject_type', function ($row) {
            return $row->subject_type ? $row->subject_type : "";
        });
        $this->editColumn('user_id', function ($row) {
            return $row->user_id ? $row->user_id : "";
        });
        $this->editColumn('host', function ($row) {
            return $row->host ? $row->host : "";
        });
    }

    public function buildRawColumns()
    {
        $this->rawColumns(['actions', 'placeholder']);
    }
}

Crear el Director

Muy bien ya casi terminamos,

Para implementar el Director solo creamos la clase DirectorDatatable y pasamos como dependencia de su constructor la interfaz DatatableBuilderInterface.

class DatatableDirector
{
    private $builder;

    public function __construct(DatatableBuilderInterface $builder)
    {
        $this->builder = $builder;
    }

    public static function makeFrom(DatatableBuilderInterface $builder)
    {
        return (new static($builder))->build();
    }

    private function build()
    {
        $this->builder->buildColumns();
        $this->builder->buildEditColumns();
        $this->builder->buildRawColumns();

        return $this->builder->make(true);
    }
}

Para finalizar solo tenemos que actualiza el código original de nuestro controlador y listo!.

class AuditLogsController extends Controller
{
    public function index(Request $request)
    {
        if ($request->ajax()) {
            $query = AuditLog::query()->select(sprintf('%s.*', (new AuditLog)->table));

            return DatatableDirector::makeFrom(new AuditLogBuilder($query));
        }

        return view('admin.auditLogs.index');
    }

    //...
}

De esta forma terminamos, y como puedes ver el código es mucho mas sencillo que antes.

Conclusión

Creo que es importante comentar que no siempre es necesario usar las clases como las proporciona el creador, dependiendo de las necesidades debemos hacer las cosas de forma que cumplamos con la solución del problema.

Y es en ese punto donde puedes aplicar los patrones si es que el caso lo requiere.

La ventaja de usar el builder es la de proteger contra cambios inesperados de EloquentDatatable ya que el Director depende de la interfaz DatatableBuilderInterface, de esa forma podemos tener código mas sencillo de mantener.

Otra ventaja es la de reducir la complejidad de uso de Datatable ya que a nivel del controlador el único objeto con el que se comunica de forma directa es con el Director.

Eso permite reutilizar el Director en otro método o controlador simplemente creando un nuevo Builder.

Para finalizar toma esta idea como base ya que este ejemplo se puede mejorar o implementar de otra forma; por ejemplo es posible usar composición en lugar de herencia.

Si quieres ver un ejemplo usando composición lo puedes hacer en el libro Refactoring to Patterns, Que es de donde aprendí esta técnica que acabamos de ver.

Bien espero tus comentarios y comparte el articulo!.