🏭 Patrón Factory Method: la fábrica de objetos más elegante
¿Qué es?
El Factory Method es un patrón de diseño creacional del catálogo Gang of Four (GoF). Esto quiere decir que nos va a ayudar a crear objetos sin tener que escribir directamente la lógica de creación en varios lugares del código.

Imagínate que quieres fabricar coches Cupra desde varias partes del código. Esto se puede volver un auténtico coñazo de mantener, pero con un Factory Method se soluciona todo.
Pero cuidadito shavá, no es solo meter todo en una clase y ya está. El Factory Method es más elegante: defines una “fábrica abstracta” que dice “oye, aquí se van a crear coches, pero no sé cuáles exactamente”. Luego tienes fábricas específicas que heredan de esta y cada una sabe crear su rollo.
Por ejemplo, tienes una CupraFactory abstracta que dice “aquí se crean coches Cupra” pero no especifica cuáles. Luego tienes CupraLeonFactory que hereda de la anterior y, cuando le pides un coche, te fabrica un León flamante. También tienes CupraFormentorFactory que hace lo mismo, pero con Formentors.
Lo guapo de esto es que si mañana Cupra saca un modelo nuevo, solo creas una nueva fábrica (CupraAtecaFactory por ejemplo) sin tocar ni una línea del código que ya tienes. Tu código que pide coches no tiene ni idea de si está pidiendo un León o un Formentor, solo sabe que está pidiendo a una fábrica Cupra y le van a dar un coche Cupra.
Pues esto es el Factory Method: cada fábrica sabe crear su producto específico, pero todas comparten la misma interfaz para pedírselo.
Este POST no lo patrocina Cupra.
La estructura del patrón
Define la interfaz que comparten todos los productos que las fábricas van a crear. En nuestro caso: CupraCoche.
Las implementaciones concretas del producto. Cada una con su lógica: CupraLeon, CupraFormentor, CupraAteca.
Declara el método crearCoche() que retorna un Product. No sabe qué producto concreto crear, eso lo deciden las hijas.
Cada fábrica concreta sobreescribe el factory method y crea su producto específico: CupraLeonFactory, CupraFormentorFactory...
La clave está en que el código cliente trabaja con la fábrica abstracta y la interfaz del producto. No sabe ni le importa qué producto concreto se está creando. Eso lo decide la fábrica concreta.
¿Qué problema resuelve?
Imagínate que Cupra va sacando varios modelos de coche y nosotros tenemos un switch gigante repartido por el código con la lógica de creación mezclada:
// ❌ El switch infernal que crece sin parar
$modelo = $_GET['modelo'] ?? 'leon';
switch ($modelo) {
case 'leon':
$coche = new CupraLeon();
break;
case 'formentor':
$coche = new CupraFormentor();
break;
case 'ateca':
$coche = new CupraAteca();
break;
default:
throw new Exception("Modelo desconocido");
}
$coche->arrancar();
¿Qué problemas tiene esto?
- Cada vez que Cupra saca un modelo nuevo, hay que abrir este código y meter un case más (viola el OCP).
- Si la lógica de creación está en varios sitios, hay que modificarlos todos.
- El código que usa el coche está acoplado a las clases concretas.
- Difícil de testear: no puedes inyectar un mock de la fábrica.
Implementación con Factory Method
Primero, los productos (los coches):
<?php
interface CupraCoche
{
public function arrancar(): string;
public function getModelo(): string;
public function getPotencia(): int;
}
class CupraLeon implements CupraCoche
{
public function arrancar(): string
{
return "León: ¡Brrrum! 300 CV";
}
public function getModelo(): string
{
return "Cupra León";
}
public function getPotencia(): int
{
return 300;
}
}
class CupraFormentor implements CupraCoche
{
public function arrancar(): string
{
return "Formentor: ¡Zas! 310 CV";
}
public function getModelo(): string
{
return "Cupra Formentor";
}
public function getPotencia(): int
{
return 310;
}
}
class CupraAteca implements CupraCoche
{
public function arrancar(): string
{
return "Ateca: ¡Prrr! 300 CV";
}
public function getModelo(): string
{
return "Cupra Ateca";
}
public function getPotencia(): int
{
return 300;
}
}
Ahora la fábrica abstracta y sus fábricas concretas:
<?php
abstract class CupraFactory
{
// El Factory Method: las subclases deciden qué crear
abstract public function crearCoche(): CupraCoche;
// Lógica compartida que usa el factory method
public function entregarCoche(): string
{
$coche = $this->crearCoche();
return "Entregando: " . $coche->getModelo()
. " (" . $coche->getPotencia() . " CV)";
}
}
class CupraLeonFactory extends CupraFactory
{
public function crearCoche(): CupraCoche
{
return new CupraLeon();
}
}
class CupraFormentorFactory extends CupraFactory
{
public function crearCoche(): CupraCoche
{
return new CupraFormentor();
}
}
class CupraAtecaFactory extends CupraFactory
{
public function crearCoche(): CupraCoche
{
return new CupraAteca();
}
}
Ahora nos podemos cargar el switch y dejarlo más limpio. ¡Mira qué guapo pavo!
// index.php
$modelo = $_GET['modelo'] ?? 'leon';
// Fábrica "mapeada" (array asociativo)
$fabricas = [
'leon' => new CupraLeonFactory(),
'formentor' => new CupraFormentorFactory(),
'ateca' => new CupraAtecaFactory(),
];
if (!isset($fabricas[$modelo])) {
throw new Exception("Modelo desconocido");
}
$coche = $fabricas[$modelo]->crearCoche(); // ¡Una sola línea!
echo $coche->arrancar();
// O usando la lógica compartida del Creator:
echo $fabricas[$modelo]->entregarCoche();
// "Entregando: Cupra León (300 CV)"
Si aparece un nuevo modelo de coche, es fácil: creas la clase del coche, creas su fábrica, la añades al array y listo. Cero líneas modificadas en el código existente. OCP cumplido.
Ejemplo del mundo real: sistema de notificaciones
Vamos a ver un ejemplo que seguro te encuentras en proyectos reales. Un sistema que envía notificaciones por diferentes canales:
<?php
// Product: la interfaz de notificación
interface Notification
{
public function send(string $recipient, string $message): bool;
public function getChannel(): string;
}
// Concrete Products
class EmailNotification implements Notification
{
public function send(string $recipient, string $message): bool
{
// Configurar SMTP, construir email, enviar...
echo "Enviando email a $recipient: $message\n";
return true;
}
public function getChannel(): string
{
return "email";
}
}
class SmsNotification implements Notification
{
public function send(string $recipient, string $message): bool
{
// Conectar con API de SMS, enviar...
echo "Enviando SMS a $recipient: $message\n";
return true;
}
public function getChannel(): string
{
return "sms";
}
}
class PushNotification implements Notification
{
public function send(string $recipient, string $message): bool
{
// Conectar con servicio de push...
echo "Enviando push a $recipient: $message\n";
return true;
}
public function getChannel(): string
{
return "push";
}
}
// Creator: fábrica abstracta
abstract class NotificationFactory
{
abstract public function createNotification(): Notification;
public function notify(string $recipient, string $message): bool
{
$notification = $this->createNotification();
echo "[" . $notification->getChannel() . "] ";
return $notification->send($recipient, $message);
}
}
// Concrete Creators
class EmailNotificationFactory extends NotificationFactory
{
public function createNotification(): Notification
{
return new EmailNotification();
}
}
class SmsNotificationFactory extends NotificationFactory
{
public function createNotification(): Notification
{
return new SmsNotification();
}
}
class PushNotificationFactory extends NotificationFactory
{
public function createNotification(): Notification
{
return new PushNotification();
}
}
El código cliente trabaja con la fábrica abstracta:
function alertarUsuario(NotificationFactory $factory, string $user): void
{
// Esta función no sabe ni le importa si es email, SMS o push
$factory->notify($user, "Tienes una alerta importante");
}
// Usar email
alertarUsuario(new EmailNotificationFactory(), "juan@email.com");
// [email] Enviando email a juan@email.com: Tienes una alerta importante
// Usar SMS
alertarUsuario(new SmsNotificationFactory(), "+34612345678");
// [sms] Enviando SMS a +34612345678: Tienes una alerta importante
// Mañana añades Telegram sin tocar nada de lo anterior
alertarUsuario(new TelegramNotificationFactory(), "@juancho");
La función alertarUsuario recibe una fábrica abstracta y trabaja con ella sin saber qué canal concreto hay debajo. Si mañana necesitas Telegram, Slack o paloma mensajera, solo creas la clase y su fábrica.
Otro ejemplo: generación de documentos
Imagina que tu aplicación necesita generar facturas en diferentes formatos:
<?php
interface Document
{
public function generate(array $data): string;
public function getExtension(): string;
}
class PdfDocument implements Document
{
public function generate(array $data): string
{
// Lógica de generación de PDF...
return "Factura generada en PDF";
}
public function getExtension(): string
{
return "pdf";
}
}
class CsvDocument implements Document
{
public function generate(array $data): string
{
// Lógica de generación de CSV...
return implode(",", $data);
}
public function getExtension(): string
{
return "csv";
}
}
class HtmlDocument implements Document
{
public function generate(array $data): string
{
return "<table><tr><td>" . implode("</td><td>", $data) . "</td></tr></table>";
}
public function getExtension(): string
{
return "html";
}
}
// Creator
abstract class DocumentFactory
{
abstract public function createDocument(): Document;
public function exportInvoice(array $invoiceData): string
{
$doc = $this->createDocument();
$content = $doc->generate($invoiceData);
$filename = "factura_" . date('Ymd') . "." . $doc->getExtension();
// Guardar en disco, enviar por email, etc.
return "Exportado: $filename";
}
}
class PdfDocumentFactory extends DocumentFactory
{
public function createDocument(): Document
{
return new PdfDocument();
}
}
class CsvDocumentFactory extends DocumentFactory
{
public function createDocument(): Document
{
return new CsvDocument();
}
}
class HtmlDocumentFactory extends DocumentFactory
{
public function createDocument(): Document
{
return new HtmlDocument();
}
}
Fíjate en que exportInvoice() está en la fábrica abstracta y usa el factory method. Toda la lógica compartida (nombre del archivo, guardado en disco…) vive en el Creator. Los Concrete Creators solo se preocupan de crear el tipo de documento correcto. Eso es separación de responsabilidades.
Factory Method vs Simple Factory vs Abstract Factory
Es fácil confundir estos tres. Aquí van las diferencias:
| Patrón | Qué hace | Cuándo usarlo |
|---|---|---|
| Simple Factory | Un solo método estático con un switch que crea el objeto según un parámetro | Casos simples, pocos productos, no necesitas extender |
| Factory Method | Delega la creación a subclases. Cada subclase (fábrica concreta) crea un producto concreto | Cuando necesitas extensibilidad y cumplir OCP |
| Abstract Factory | Crea familias de productos relacionados. No un solo producto, sino un conjunto | Cuando necesitas crear objetos que van juntos (ej: UI dark theme + light theme) |
El Simple Factory no es un patrón GoF, es más bien un “idiom” de programación. El Factory Method y el Abstract Factory sí son patrones GoF oficiales.
Relación con los principios SOLID
El Factory Method es un patrón que nace naturalmente cuando aplicas SOLID:
Añadir un producto nuevo = crear una clase y una fábrica nueva. Sin tocar el código existente. Es el principio que más directamente cumple.
Cada fábrica tiene una sola responsabilidad: crear su producto. La lógica de creación está separada de la lógica de uso.
El código cliente depende de abstracciones (CupraFactory, CupraCoche), no de clases concretas.
Cualquier fábrica concreta puede sustituir a la abstracta sin romper nada. Todas cumplen el mismo contrato.
Ventajas y desventajas
- OCP: Nuevos productos sin tocar código existente.
- Desacoplamiento: El cliente no conoce las clases concretas.
- SRP: La lógica de creación está encapsulada en cada fábrica.
- Testeable: Puedes inyectar fábricas mock en los tests.
- Lógica compartida: El Creator puede tener métodos comunes que usan el factory method.
- Más clases: Una fábrica por cada producto puede generar muchas clases.
- Complejidad: Para problemas simples es matar moscas a cañonazos.
- Jerarquía paralela: La jerarquía de fábricas crece en paralelo con la de productos.
¿Cuándo usarlo y cuándo no?
Úsalo cuando:
- No sabes de antemano qué tipos de objetos va a necesitar tu código.
- Quieres que el sistema sea fácilmente extensible con nuevos tipos de productos.
- La lógica de creación es compleja o varía entre productos.
- Quieres desacoplar la lógica de creación de la lógica de uso.
NO lo uses cuando:
- Solo tienes 2-3 productos y no van a crecer. Un simple
if/elsees más claro. - La creación del objeto es trivial (un
newsin configuración). - No necesitas extensibilidad. No sobre-ingenieres por anticipar un futuro que nunca llega.
Recuerda: El Factory Method es una herramienta para cuando la complejidad lo justifica. Si un
newdirecto funciona y el código es claro, no metas un Factory Method solo porque queda bonito.
Conclusión
El Factory Method es uno de los patrones más útiles y más usados del catálogo GoF. Su idea es sencilla pero potente: delega la creación de objetos a subclases especializadas para que el código cliente trabaje con abstracciones y no se preocupe por los detalles.
Elimina los switches gigantes de creación, cumple el OCP de SOLID y hace que añadir productos nuevos sea tan fácil como crear una clase y su fábrica. Y lo más importante: el código que usa los productos no cambia nunca, independientemente de cuántos tipos de productos tengas.
¡Ea! Nos vemos en los bares. 🍻
Pon a prueba lo aprendido
1. ¿Qué tipo de patrón de diseño es el Factory Method?
2. ¿Qué problema principal resuelve el Factory Method?
3. En el ejemplo de Cupra, ¿quién decide qué coche concreto se crea?
4. ¿Qué principio SOLID cumple más directamente el Factory Method?
5. ¿Cuál es la diferencia entre Factory Method y Simple Factory?
6. ¿Qué pasa cuando Cupra saca un modelo nuevo usando Factory Method?
7. ¿Cuál es una desventaja del Factory Method?
8. ¿Cuándo NO deberías usar Factory Method?