Oscar Coleto

Patrón Factory en Laravel: Crea Objetos Complejos de Forma Elegante

Introducción

El patrón Factory es uno de los patrones de diseño creacionales más utilizados en el desarrollo de software. Su objetivo es encapsular la lógica de creación de objetos, permitiéndote crear instancias de clases de forma flexible sin acoplar tu código a implementaciones concretas.

En este artículo, exploraremos cómo implementar el patrón Factory en Laravel con ejemplos prácticos y reales que podrás aplicar inmediatamente en tus proyectos.

Nota importante: Este artículo trata sobre el patrón de diseño Factory, no sobre las Eloquent Factories de Laravel que se usan para testing y seeders.

¿Qué es el Patrón Factory?

El patrón Factory es un patrón de diseño creacional que proporciona una interfaz para crear objetos sin especificar la clase exacta del objeto que se creará.

En lugar de usar new directamente en tu código, delegas la creación de objetos a una clase Factory que decide qué clase instanciar según ciertos criterios.

Diagrama en Excalidraw del patrón Factory aplicado en Laravel para desacoplar servicios

Problema que Resuelve

Imagina que tienes código como este:

class NotificationController
{
    public function send(Request $request)
    {
        $type = $request->input('type');

        if ($type === 'email') {
            $notifier = new EmailNotifier();
        } elseif ($type === 'sms') {
            $notifier = new SmsNotifier();
        } elseif ($type === 'push') {
            $notifier = new PushNotifier();
        } elseif ($type === 'slack') {
            $notifier = new SlackNotifier();
        } else {
            throw new Exception("Tipo de notificación no válido");
        }

        $notifier->send($request->input('message'));
    }
}

Problemas con este código:

Viola el principio Open/Closed: Cada nuevo tipo de notificación requiere modificar el controlador

Alto acoplamiento: El controlador conoce todas las implementaciones concretas

Difícil de testear: No puedes inyectar fácilmente un mock

Lógica duplicada: Si necesitas crear notificadores en otros lugares, duplicas este código

Tipos de Factory

Hay dos variantes principales del patrón Factory:

1. Factory Method (Método Fábrica)

Define una interfaz para crear un objeto, pero permite que las subclases decidan qué clase instanciar.

2. Abstract Factory (Fábrica Abstracta)

Proporciona una interfaz para crear familias de objetos relacionados sin especificar sus clases concretas.

En este artículo nos enfocaremos principalmente en Factory Method con ejemplos prácticos en Laravel.

Implementando el Patrón Factory en Laravel

Vamos a refactorizar el ejemplo anterior usando el patrón Factory.

Paso 1: Crear la Interfaz

Primero, definimos un contrato común para todos los notificadores:

<?php

namespace App\Contracts;

interface NotifierInterface
{
    public function send(string $recipient, string $message): bool;
}

Paso 2: Implementar las Clases Concretas

Cada tipo de notificación implementa la interfaz:

<?php

namespace App\Services\Notifications;

use App\Contracts\NotifierInterface;

class EmailNotifier implements NotifierInterface
{
    public function send(string $recipient, string $message): bool
    {
        // Lógica para enviar email
        Mail::to($recipient)->send(new NotificationMail($message));
        return true;
    }
}
<?php

namespace App\Services\Notifications;

use App\Contracts\NotifierInterface;

class SmsNotifier implements NotifierInterface
{
    public function send(string $recipient, string $message): bool
    {
        // Lógica para enviar SMS (Twilio, etc.)
        return Twilio::sendSms($recipient, $message);
    }
}
<?php

namespace App\Services\Notifications;

use App\Contracts\NotifierInterface;

class PushNotifier implements NotifierInterface
{
    public function send(string $recipient, string $message): bool
    {
        // Lógica para enviar notificación push
        return FCM::sendNotification($recipient, $message);
    }
}

Paso 3: Crear la Factory

Ahora creamos la clase Factory que encapsula la lógica de creación:

<?php

namespace App\Factories;

use App\Contracts\NotifierInterface;
use App\Services\Notifications\EmailNotifier;
use App\Services\Notifications\SmsNotifier;
use App\Services\Notifications\PushNotifier;
use App\Services\Notifications\SlackNotifier;
use InvalidArgumentException;

class NotifierFactory
{
    /**
     * Crear un notificador según el tipo especificado.
     */
    public function make(string $type): NotifierInterface
    {
        return match ($type) {
            'email' => new EmailNotifier(),
            'sms' => new SmsNotifier(),
            'push' => new PushNotifier(),
            'slack' => new SlackNotifier(),
            default => throw new InvalidArgumentException("Tipo de notificador no soportado: {$type}")
        };
    }
}

Paso 4: Usar la Factory

Ahora el controlador queda mucho más limpio:

<?php

namespace App\Http\Controllers;

use App\Factories\NotifierFactory;
use Illuminate\Http\Request;

class NotificationController extends Controller
{
    public function __construct(
        private NotifierFactory $notifierFactory
    ) {}

    public function send(Request $request)
    {
        $validated = $request->validate([
            'type' => 'required|string|in:email,sms,push,slack',
            'recipient' => 'required|string',
            'message' => 'required|string'
        ]);

        $notifier = $this->notifierFactory->make($validated['type']);
        $notifier->send($validated['recipient'], $validated['message']);

        return response()->json(['success' => true]);
    }
}

Beneficios de esta implementación:

Código desacoplado: El controlador no conoce las implementaciones concretas

Fácil extensión: Agregar un nuevo notificador solo requiere modificar la Factory

Testeable: Puedes mockear la Factory fácilmente

Reutilizable: La Factory puede usarse en cualquier parte de la aplicación

Caso Práctico: Factory para Procesadores de Pago

Veamos un ejemplo más complejo: una Factory para diferentes pasarelas de pago.

La Interfaz

<?php

namespace App\Contracts;

interface PaymentGatewayInterface
{
    public function charge(float $amount, array $paymentData): PaymentResult;
    public function refund(string $transactionId, float $amount): RefundResult;
}

La Factory

<?php

namespace App\Factories;

use App\Contracts\PaymentGatewayInterface;
use App\Services\Payments\StripeGateway;
use App\Services\Payments\PayPalGateway;
use App\Services\Payments\MercadoPagoGateway;
use InvalidArgumentException;

class PaymentGatewayFactory
{
    /**
     * Crear una pasarela de pago según el tipo especificado.
     */
    public function make(string $gateway): PaymentGatewayInterface
    {
        return match ($gateway) {
            'stripe' => new StripeGateway(),
            'paypal' => new PayPalGateway(),
            'mercadopago' => new MercadoPagoGateway(),
            default => throw new InvalidArgumentException("Pasarela de pago no soportada: {$gateway}")
        };
    }

    /**
     * Crear la pasarela de pago por defecto.
     */
    public function makeDefault(): PaymentGatewayInterface
    {
        return $this->make(config('payments.default', 'stripe'));
    }
}

Uso en un Servicio

<?php

namespace App\Services;

use App\Factories\PaymentGatewayFactory;

class CheckoutService
{
    public function __construct(
        private PaymentGatewayFactory $gatewayFactory
    ) {}

    public function processPayment(Order $order, string $gateway = null)
    {
        // Usar pasarela especificada o la por defecto
        $paymentGateway = $gateway
            ? $this->gatewayFactory->make($gateway)
            : $this->gatewayFactory->makeDefault();

        $result = $paymentGateway->charge(
            amount: $order->total,
            paymentData: $order->paymentData
        );

        if ($result->isSuccess()) {
            $order->markAsPaid($result->transactionId);
        }

        return $result;
    }
}

Factory con Parámetros Complejos

A veces necesitas pasar parámetros específicos al crear objetos. Veamos cómo manejarlo:

<?php

namespace App\Factories;

use App\Services\Reports\PdfReportGenerator;
use App\Services\Reports\ExcelReportGenerator;
use App\Services\Reports\CsvReportGenerator;
use App\Contracts\ReportGeneratorInterface;

class ReportFactory
{
    public function make(string $format, array $options = []): ReportGeneratorInterface
    {
        return match ($format) {
            'pdf' => new PdfReportGenerator(
                orientation: $options['orientation'] ?? 'portrait',
                pageSize: $options['pageSize'] ?? 'A4',
                includeHeaders: $options['includeHeaders'] ?? true
            ),
            'excel' => new ExcelReportGenerator(
                sheetName: $options['sheetName'] ?? 'Report',
                includeFormulas: $options['includeFormulas'] ?? false
            ),
            'csv' => new CsvReportGenerator(
                delimiter: $options['delimiter'] ?? ',',
                enclosure: $options['enclosure'] ?? '"'
            ),
            default => throw new InvalidArgumentException("Formato no soportado: {$format}")
        };
    }
}

Uso:

$reportFactory = app(ReportFactory::class);

// Crear PDF con opciones personalizadas
$pdfGenerator = $reportFactory->make('pdf', [
    'orientation' => 'landscape',
    'pageSize' => 'A3',
    'includeHeaders' => true
]);

$report = $pdfGenerator->generate($data);

Testing con Factories

El patrón Factory hace que el testing sea mucho más sencillo:

<?php

namespace Tests\Unit;

use App\Factories\NotifierFactory;
use App\Contracts\NotifierInterface;
use Tests\TestCase;
use Mockery;

class NotificationServiceTest extends TestCase
{
    public function test_sends_email_notification()
    {
        // Crear un mock del notificador
        $mockNotifier = Mockery::mock(NotifierInterface::class);
        $mockNotifier->shouldReceive('send')
            ->once()
            ->with('user@example.com', 'Test message')
            ->andReturn(true);

        // Crear un mock de la factory
        $mockFactory = Mockery::mock(NotifierFactory::class);
        $mockFactory->shouldReceive('make')
            ->with('email')
            ->once()
            ->andReturn($mockNotifier);

        // Inyectar el mock en el servicio
        $service = new NotificationService($mockFactory);

        $result = $service->notify('email', 'user@example.com', 'Test message');

        $this->assertTrue($result);
    }
}

Cuándo Usar el Patrón Factory

Usa Factory cuando:

  • Tienes múltiples implementaciones de una interfaz que se seleccionan en runtime
  • La creación de objetos es compleja y necesita encapsularse
  • Quieres desacoplar tu código de implementaciones concretas
  • Necesitas flexibilidad para agregar nuevas implementaciones sin modificar código existente
  • Quieres facilitar el testing con mocks

No uses Factory cuando:

  • Solo tienes una implementación y no esperas más
  • La creación del objeto es trivial (un simple new es suficiente)
  • Estás agregando complejidad innecesaria
  • El objeto no tiene variaciones ni configuraciones complejas

Ventajas del Patrón Factory

Desacoplamiento: El código cliente no depende de clases concretas

Extensibilidad: Agregar nuevas implementaciones es fácil

Single Responsibility: La lógica de creación está en un solo lugar

Open/Closed: Cumple con el principio Open/Closed de SOLID

Testeable: Facilita el uso de mocks y stubs

Mantenibilidad: Los cambios están localizados en la Factory

Desventajas y Consideraciones

Complejidad adicional: Introduces más clases al sistema

Overhead: Para casos simples puede ser overkill

⚠️ Cuidado con: God Factories que hacen demasiadas cosas

Mejores Prácticas

1. Usa Type Hints

public function make(string $type): NotifierInterface
{
    // El tipo de retorno garantiza que siempre devuelves la interfaz correcta
}

2. Valida el Input

public function make(string $type): NotifierInterface
{
    if (!isset($this->notifiers[$type])) {
        throw new InvalidArgumentException("Tipo no soportado: {$type}");
    }

    return $this->container->make($this->notifiers[$type]);
}

3. Usa Configuración para Flexibilidad

public function __construct(Container $container)
{
    $this->notifiers = config('notifications.channels', []);
}

4. Proporciona Métodos Helper

public function makeDefault(): NotifierInterface
{
    return $this->make(config('notifications.default'));
}

public function getAvailableTypes(): array
{
    return array_keys($this->notifiers);
}

5. Documenta Tu Factory

/**
 * Factory para crear instancias de notificadores.
 *
 * Esta factory soporta los siguientes tipos:
 * - email: Envío vía correo electrónico
 * - sms: Envío vía SMS usando Twilio
 * - push: Notificaciones push con FCM
 * - slack: Mensajes a canales de Slack
 *
 * @example
 * $notifier = $factory->make('email');
 * $notifier->send('user@example.com', 'Hello!');
 */
class NotifierFactory
{
    // ...
}

Conclusión

El patrón Factory es una herramienta esencial en tu arsenal de patrones de diseño. Te permite crear objetos de forma flexible y mantenible, siguiendo los principios SOLID y facilitando el testing.

En Laravel, el patrón Factory se integra perfectamente con el contenedor de inyección de dependencias, permitiéndote crear Factories potentes y fáciles de usar.

Recapitulación:

El patrón Factory encapsula la lógica de creación de objetos

Desacopla tu código de implementaciones concretas

Facilita la extensión y el mantenimiento

Se integra perfectamente con el contenedor de Laravel

Mejora significativamente la testabilidad

No se trata de aplicar el patrón Factory en todos lados, sino de usarlo cuando realmente aporta valor. Cuando tengas múltiples implementaciones de una interfaz y necesites flexibilidad para elegir cuál usar, el patrón Factory es tu mejor aliado.


¿Quieres más?

Si te gustó este artículo y quieres llevar tus Factories al siguiente nivel, no te pierdas la segunda parte:

Patrón Factory Avanzado en Laravel: Container, Service Locator y Abstract Factory

Aprenderás técnicas avanzadas como:

  • Usar el Container de Laravel para inyección automática
  • Implementar Abstract Factory para familias de objetos
  • Factory con caché para optimización
  • Evitar anti-patrones como Service Locator
  • Y mucho más…

Artículos Relacionados

Si te gustó este artículo, te recomiendo leer:

Happy coding! 🚀

C O M E N T A R I O S

Deja un comentario

0/2000 caracteres

Tu email no será publicado. Los campos marcados con * son obligatorios.

Cargando comentarios...

☕ ¿Te ha sido útil este artículo?

Apóyame con un café mientras sigo creando contenido técnico

☕ Invítame un café