*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.