Aplica el patrón de diseño Factory Method en laravel

Spread the love

Ya hemos platicado sobre los casos en los que puedes utilizar el patrón factory, pero hoy vamos a ver con un ejemplo, como puedes emplear la versión mas común de este patrón.

Imagina que tienes en tu sistema la opción para que el usuario decida obtener reportes usando dompdf o snappy. algo como lo que se muestra en el código.

    if ($request->type == 'dompdf') {
        $pdf = App::make('dompdf.wrapper');
    }

    if ($request->type == 'snappy') {
        $pdf = App::make('snappy.pdf.wrapper');
    }

    $pdf->loadView('pdf.invoice', ['order' => $request->order]);
    return $pdf->download('report.pdf');

Como puedes estar pensando, es obvio que agregar otro tipo de reporte te obliga a modificar la condición.

Afortunadamente un caso de uso del Patrón Factory Method es precisamente crear instancia que se deciden en tiempo de ejecución como en este caso.

Veamos como solucionar este problema con el Patrón.

Patrón de diseño Factory Method

Define una interfaz para crear un objeto, pero deja que las subclases decidan qué clase instanciar. El Factory Method permite a una clase delegar la instanciación a subclases.

Design Patterns by Gamma

El termino interfaz en la definición, se refiere a una clase que tiene métodos que te permiten crear una instancia, pero donde el cliente no interactúa de forma directa con las subclases que se encargan de crear las instancias.

Si la definición te parece que no es muy clara, con el siguiente diagrama tendrá mas sentido.

Estructura de clases

La imagen 1 te muestra las clases que intervienen en este patrón.

Factory method figura 1
Figura 1

Cada una de estas clases tiene la siguiente función.

  • FactoryInterface:  devuelve un objeto del tipo Product. Este objeto puede ser una interfaz o una clase abstracta.
  • Factory: es una instancia concreta de FactoryInterface.
  • Product: Define una interfaz que representa a los objetos que se quieren crear.
  • ProductA y ProductB son instancias concretas de la interfaz Product.

La idea es tener dos interfaces; una de ellas es la que usara un “cliente” que puede ser otra clase o un controlador, la segunda interfaz representa los objetos que serán instanciados y solo quedan disponibles a través del método create.

Si pasamos este diagrama a código, esto es lo que veríamos del lado del cliente.

$factory = new Factory();
$product = $factory->create($type);

La clase Factory es la interfaz que usa el cliente, el cual no conoce nada acerca de como se va a instanciar las clases de tipo Product. El solo requiere solicitar un producto y utilizarlo, para este caso el producto que proporcione la variable $type.

Si sientes que esto se pone medio complicado, no te preocupes que te parece si cambiamos el diagrama de clases y vamos aplicando un poco de nuestro problema.

Si pasas esto a código vas a tener lo siguiente.

$factory = FactoryReport();
$report = FactoryReport('dompdf');

En esta caso el Factory Method se encarga de instanciar el reporte que vamos a necesitar (DompdfReport)

En la siguiente figura te muestro como funciona esto, usando nuestro ejemplo.

De izquierda a derecha, el cliente (Controller) crea una instancia de ReportFactory, el cual de acuerdo a un identificador (dompdf) se encarga de solicitar una instancia de DompdfReport para finalmente regresar esa instancia al cliente. Los mismos pasos se repiten si queremos obtener una instancia de SnappyReport.

Muy bien, ahora vemos como implementar esto en código.

Implementación

Primero tienes que crear la interfaz para el report factory

Interfaz ReportFactory

namespace App\Reports;

use InvalidArgumentException;

interface ReportFactoryInterface
{
    /**
     * @param $type
     * @return ReportInterface
     * @throws InvalidArgumentException
     */
    public function create($type): ReportInterface;
}

Como puedes observar, para este caso solo requieres el método create y este debe de regresar una instancia de ReportInterface, que vamos a ver a continuación.

Interfaz ReportInterface

namespace App\Reports;

use Illuminate\Http\Response;

interface ReportInterface
{
    /**
     * @param $data
     * @return ReportInterface
     */
    public function fromRequest($data): ReportInterface;

    /**
     * @param $filename
     * @return Response
     */
    public function download($filename): Response;
}

Para nuestro ejemplo, requerimos dos método uno donde vamos a pasar los datos que se requieren para el reporte y el otro que se encarga de proporcionar el reporte para descarga.

ya que tenemos las interfaces, es hora de implementarlas.

Crear la clase ReportFactory

namespace App\Reports;


use InvalidArgumentException;

class ReportFactory implements ReportFactoryInterface
{
    private $app;

    private $aliases = [
        'dompdf' => 'dompdf.wrapper',
        'snappy' => 'snappy.pdf.wrapper',
    ];

    public function __construct($app)
    {

        $this->app = $app;
    }

    /**
     * @param $type
     * @return ReportInterface
     * @throws InvalidArgumentException
     */
    public function create($type) : ReportInterface
    {
        $reportClass = __NAMESPACE__.'\\'.ucfirst($type).'Report';

        if (!array_key_exists($type, $this->aliases)) {
            throw new InvalidArgumentException("Report {$type} does not exist");
        }

        if (!class_exists($reportClass)) {
            throw new InvalidArgumentException("Class {$reportClass} does not exist");
        }

        return new $reportClass($this->app->make($this->aliases[$type]));
    }
}

Debido a que tanto Snappy y Dompdf se usan mediante sus alias, decidí pasar una instancia del contenedor de dependencias.

El método create, utiliza la cadena $type para construir el namespace de una clase con el formato Name+Report si la clase no existe se lanza una excepción.

por otro lado los aliases se pasan mediante un arreglo, si no existe el alias se dispara también una excepción.

Por ultimo si todo esta bien, se crea la instancia de la clase.

Muy bien ahora veamos como crear las clases que manejan los reportes.

Crear la clase DompdfReport

namespace App\Reports;


use Barryvdh\DomPDF\PDF;
use Illuminate\Http\Response;

class DompdfReport implements ReportInterface
{
    private $view = 'pdf.order';

    /**
     * @var PDF
     */
    private $pdf;

    /**
     * DompdfReport constructor.
     * @param PDF $pdf
     */
    public function __construct(PDF $pdf)
    {

        $this->pdf = $pdf;
    }

    /**
     * @param $data
     * @return ReportInterface
     */
    public function fromRequest($data) : ReportInterface
    {
        $this->pdf->loadView($this->view, ['order' => $data]);
        return $this;
    }

    /**
     * @param $filename
     * @return Response
     */
    public function download($filename): Response
    {
        return $this->pdf->download($filename);
    }
}

Crear la clase SnappyReport

namespace App\Reports;


use Barryvdh\Snappy\PdfWrapper;
use Illuminate\Http\Response;

class SnappyReport implements ReportInterface
{

    private $view = 'pdf.order';

    /**
     * @var PdfWrapper
     */
    private $pdf;

    public function __construct(PdfWrapper $pdf)
    {
        $this->pdf = $pdf;
    }

    /**
     * @param $data
     * @return ReportInterface
     */
    public function fromRequest($data): ReportInterface
    {
        $this->pdf->loadView($this->view, ['order' =>$data]);
        return $this;
    }

    /**
     * @param $filename
     * @return Response
     */
    public function download($filename): Response
    {
        return $this->pdf->download($filename);
    }
}

Si te diste cuenta las dos clases hacen exactamente lo mismo, pero debes de notar que en el constructor cada clase recibe un objeto diferente. Por esa razón tenemos dos clases y no una.

Para finalizar, nuestro ejemplo original queda de la siguiente forma.

    /** @var \App\Reports\ReportInterface $report */
    $report = (new ReportFactory(app()))->create('dompdf');
    return $report->fromRequest($request->order)->download('report.pdf');

Se eliminaron las condiciones, el código es mas compacto y sencillo de leer y la ventaja adicional de que ahora puedes extender para crear otro tipo de reportes.

Si tienes mas dudas de como y cuando utilizar este patrón, puedes ver este otro artículo, donde hablo de forma general sobre el patrón Factory.

Finalmente si tienes alguna duda o quieres aportar algo mas, no dudes en dejar tus comentarios.