Factory Pattern en PHP y como usarlo

Factory Pattern en PHP y como usarlo

*Importante: No confundir el Factory Pattern con el patrón Abstract Factory, aunque del mismo tipo, resuelven distintos problemas de distinta forma. A considerar…*

El patrón de diseño Factory viene a solucionar los inconvenientes con la creación o instanciación de nuestros objetos. La idea es encapsular u ocultar al cliente los detalles internos para la creación de ciertos objetos, o dicho de una mejor manera:

Desacoplar la lógica de creación de la lógica de negocio, evitando al cliente conocer detalles de la instanciación de los objetos de los que depende. Propósito del patrón

Implementación del Factory Pattern

Caso de ejemplo

Supongamos que necesitamos desarrollar un sistema para una tienda apple el cual permita mostrar la ficha informativa de cada ordenador fabricado por Apple. Una primera aproximación del código sería:

class AppleTechnicalSheet

class AppleTechnicalSheet
{
    const MACBOOK = 'MAC';
    const MACBOOK_AIR = 'MACAIR';
    const MACBOOK_PRO = 'MACPRO';
    const IMAC = 'IMAC';

    public function getTechnicalSheet(string $model)
    {
        switch ($model) {
            case self::MACBOOK:
                $processor = 'Intel Core i5';
                $model = 'Macbook';
                $cores = 4;
                $ssd = '256GB';
                $memory = '4GB'; 
                break;

            case self::MACBOOK_AIR:
                $processor = 'Intel Core i3';
                $model = 'Macbook Air';
                $cores = 2;
                $ssd = '128GB';
                $memory = '4GB';
                break;

            case self::MACBOOK_PRO:
                $processor = 'Intel Core i7';
                $model = 'Macbook Pro';
                $cores = 6;
                $ssd = '512GB';
                $memory = '16GB';
                break;

            case self::IMAC:
                $processor = 'Intel Core i7';
                $model = 'iMac';
                $cores = 6;
                $ssd = '512GB';
                $memory = '8GB';
                break;

            default:
            throw new InvalidArgumentException("The model you requested doesn't exist");
        }

        $format = 'Model: %s \n Processor: %s \n Cores: %d \n SSD: %s \n Memory: %s';

        return sprintf($format, $model, $processor, $cores, $ssd, $memory);
    }
}

Como se puede observar, esta implementación contiene evidentes problemas y el principal es el uso del switch case encargado de gestionar el modelo a mostrar.

Ya que si a futuro necesitamos añadir nuevos modelos, habra que estar editando el switch para añadirlos por lo que esta implementación no es limpia pero si dependiente de muchísimo mantenimiento.

Ademas de que visto de otra forma, estaríamos rompiendo el principio de responsabilidad única (Single Responsability Principle) de SOLID puesto que una misma clase esta gestionando lo que debería estar separado en varias clases, en nuestro ejemplo, una clase por modelo.

Implementando Factory Pattern

Los pasos a seguir para implementar en este caso el patrón Factory son:

  • Encapsular en objetos la información relativa a cada modelo de ordenador. No tiene sentido el tener esas constantes y variables dentro del switch.

  • En pro de estandarizar las acciones a realizar, crear una interfaz que todos los objetos implementen. Debido a que los rubros de información a mostrar son iguales, es preferible que todos implementen una misma interfaz que le indique a cada clase qué métodos debe definir.

  • Extraer la lógica de creación a una factoría. El cliente (la ficha técnica en este caso) no tiene por qué conocer cómo se instancian estos objetos, no es su responsabilidad, y además no es reutilizable. Saquemos ese código a una clase nueva.

  • Usar la factoría en la ficha técnica. Hagamos uso de la factoría en el cliente.

ModelFactory Class

ModelFactory Class

class ModelFactory
{
    const MACBOOK = 'MAC';
    const MACBOOK_AIR = 'MACAIR';
    const MACBOOK_PRO = 'MACPRO';
    const IMAC = 'IMAC';

    public function create(string $model)
    {
        switch ($model) {
            case self::MACBOOK:
                return new Macbook();

            case self::MACBOOK_AIR:
                return new MacbookAir();

            case self::MACBOOK_PRO:
                return new MacbookPro();

            case self::IMAC:
                return new Imac();

            default:
                throw new InvalidArgumentException("The model you requested doesn't exist");
        }
    }
}

TechnicalSheetInterface Interface

interface TechnicalSheetInterface
{
    public function getModel(): string;

    public function getProcessor(): string;

    public function getCores(): int;

    public function getSsd(): string;

    public function getMemory(): string;
}

TechnicalSheet Class

class TechnicalSheet
{
    private $modelFactory;

    public function __construct(ModelFactory $modelFactory)
    {
        $this->modelFactory = $modelFactory;
    }

    public function getTechnicalSheet(string $model)
    {
        /** @var TechnicalSheetInterface $model */
        $model = $this->modelFactory->create($model);

        $format = 'Model: %s \n Processor: %s \n Cores: %d \n SSD: %s \n Memory: %s';

        return sprintf(
            $format,
            $model->getModel(),
            $model->getProcessor(),
            $model->getCores(),
            $model->getSsd(),
            $model->getMemory()
        );
    }
}

Macbook Class

class Macbook implements TechnicalSheetInterface
{
    public function getModel(): string
    {
        return 'Macbook';
    }

    public function getProcessor(): string
    {
        return 'Intel Core i5';
    }

    public function getCores(): int
    {
        return 4;
    }

    public function getSsd(): string
    {
        return '256GB';
    }

    public function getMemory(): string
    {
        return '4GB';
    }
}

MacbookAir Class

class MacbookAir implements TechnicalSheetInterface
{
    public function getModel(): string
    {
        return 'Macbook Air';
    }

    public function getProcessor(): string
    {
        return 'Intel Core i3';
    }

    public function getCores(): int
    {
        return 2;
    }

    public function getSsd(): string
    {
        return '128GB';
    }

    public function getMemory(): string
    {
        return '4GB';
    }
}

Macbook Pro Class

class MacbookPro implements TechnicalSheetInterface
{
    public function getModel(): string
    {
        return 'Macbook Pro';
    }

    public function getProcessor(): string
    {
        return 'Intel Core i7';
    }

    public function getCores(): int
    {
        return 6;
    }

    public function getSsd(): string
    {
        return '512GB';
    }

    public function getMemory(): string
    {
        return '16GB';
    }
}

IMac Class

class IMac implements TechnicalSheetInterface
{
    public function getModel(): string
    {
        return 'iMac';
    }

    public function getProcessor(): string
    {
        return 'Intel Core i7';
    }

    public function getCores(): int
    {
        return 6;
    }

    public function getSsd(): string
    {
        return '256GB';
    }

    public function getMemory(): string
    {
        return '8GB';
    }
}

Resumamos un poco lo implementado

  • Definimos la interfaz TechnicalSheetInterface, que permite definir los getters necesarios para generar la información técnica de cada modelo.

  • Se ha creado una clase por cada uno de los modelos de ordenadores disponibles.

  • Hemos creado una clase ModelFactory que se encarga de construir el modelo según el código que se envíe y de lanzar una excepción en caso de no existir el modelo que se intenta mostrar.

  • El cliente TechnicalSheet ahora recibe en el constructor la factoría y hace uso de ella para obtener un objeto que implementa TechnicalSheetInterface con el que muestra la ficha técnica correspondiente.

Ventajas del Factory Pattern

  • La factoría es altamente reutilizable, solo hay que pasarla como dependencia.

  • El testing del cliente es mucho más sencillo, podemos usar mocks para la factoría y simular cualquier tipo de repuesta por parte de la misma, permitiendo testear todos los casos de uso posibles.

  • Si queremos añadir un nuevo modelo, solo hay que editar la factoría y añadirlo ahí, pasará a estar disponible para todos los usuarios de la factoría sin tener que replicar código.

Contras del Factory Pattern

  • En este ejemplo, si la cantidad de opciones crece en exceso, la factoría puede llegar a ser una clase difícil de mantener y habría que encontrar otras alternativas al switch como por ejemplo utilizar la clase ReflectionClass de PHP para instanciar la clase correspondiente basado en el nombre del modelo de ordenador a mostrar.

Consideraciones finales

Abstraer al cliente de la creación de los objetos de los que depende es un principio básico en la creación de software de calidad. Esto nos permite cumplir con dos principios SOLID de un plumazo y además seguir uno de los principios de la programación orientada a objetos: la encapsulación de los datos en objetos.

Recuerda que si tienes alguna sugerencia o pregunta, no dudes en dejar tus comentarios al final del post.

Si te gustó este post, ayúdame a que pueda servirle a muchas más personas, compartiendo mis contenidos en tus redes sociales.

Espero que este post haya sido de gran ayuda para ti, y como siempre, cualquier inquietud o duda que tengas, puedes contactarme por cualquiera de las vías disponibles, o dejando tus comentarios al final de este post. También puedes sugerir que temas o post te gustaría leer a futuro.

Did you find this article valuable?

Support Francisco Ugalde by becoming a sponsor. Any amount is appreciated!