Oscar Coleto

SOLID en Laravel (4/5): Principio de Segregación de Interfaces con Stripe

Introducción

Bienvenido al cuarto artículo de SOLID en Laravel. Hoy exploraremos el Principio de Segregación de Interfaces (ISP), que establece:

“Los clientes no deben verse obligados a depender de interfaces que no usan”

En otras palabras: Las interfaces deben ser pequeñas y específicas, no grandes y genéricas.

El Problema: La Interface Gigante

Cuando empezamos a trabajar con múltiples gateways de pago, es tentador crear una interfaz “completa”:

<?php

namespace App\Contracts;

/**
 * Interface "todo en uno" para gateways de pago
 * ❌ Viola el Principio de Segregación de Interfaces
 */
interface PaymentGatewayInterface
{
    // Métodos de pago básicos
    public function charge(float $amount, array $data): PaymentResult;
    public function refund(string $transactionId, ?float $amount = null): RefundResult;

    // Gestión de clientes
    public function createCustomer(array $customerData): string;
    public function updateCustomer(string $customerId, array $data): bool;
    public function deleteCustomer(string $customerId): bool;
    public function getCustomer(string $customerId): Customer;

    // Suscripciones
    public function createSubscription(string $customerId, string $planId): Subscription;
    public function cancelSubscription(string $subscriptionId): bool;
    public function updateSubscription(string $subscriptionId, array $data): Subscription;
    public function pauseSubscription(string $subscriptionId): bool;
    public function resumeSubscription(string $subscriptionId): bool;

    // Planes de suscripción
    public function createPlan(array $planData): Plan;
    public function updatePlan(string $planId, array $data): Plan;
    public function deletePlan(string $planId): bool;

    // Webhooks
    public function verifyWebhookSignature(string $payload, string $signature): bool;
    public function handleWebhook(string $payload): WebhookEvent;

    // Reportes
    public function getTransactions(array $filters): array;
    public function getBalance(): Balance;
    public function getPayoutSchedule(): PayoutSchedule;

    // Métodos de pago guardados
    public function attachPaymentMethod(string $customerId, string $paymentMethodId): bool;
    public function detachPaymentMethod(string $paymentMethodId): bool;
    public function listPaymentMethods(string $customerId): array;
    public function setDefaultPaymentMethod(string $customerId, string $paymentMethodId): bool;

    // Disputas
    public function listDisputes(): array;
    public function respondToDispute(string $disputeId, array $evidence): bool;

    // Y más métodos...
}

¿Qué está mal con esta interfaz?

Problema 1: Implementaciones Forzadas

<?php

namespace App\Services\PaymentGateways;

/**
 * PayPal NO soporta muchas de estas funcionalidades
 */
class PayPalGateway implements PaymentGatewayInterface
{
    public function charge(float $amount, array $data): PaymentResult
    {
        // PayPal soporta esto
        return $this->processPayPalPayment($amount, $data);
    }

    public function pauseSubscription(string $subscriptionId): bool
    {
        // PayPal NO tiene pause, solo cancel
        throw new \BadMethodCallException('PayPal does not support pausing subscriptions');
    }

    public function getBalance(): Balance
    {
        // PayPal no expone el balance de esta forma
        throw new \BadMethodCallException('PayPal does not provide balance information');
    }

    public function createPlan(array $planData): Plan
    {
        // PayPal usa un sistema diferente
        throw new \BadMethodCallException('Use PayPal dashboard to create plans');
    }

    // 15+ métodos más que lanzan excepciones...
}

Problema 2: Implementaciones Parciales

<?php

/**
 * Un gateway simple de un solo uso
 */
class OneTimePaymentGateway implements PaymentGatewayInterface
{
    public function charge(float $amount, array $data): PaymentResult
    {
        // Esto es lo único que necesita
        return $this->processPayment($amount, $data);
    }

    // Pero está OBLIGADO a implementar 25+ métodos que no usa
    public function createSubscription(...) { throw new Exception('Not supported'); }
    public function cancelSubscription(...) { throw new Exception('Not supported'); }
    public function createCustomer(...) { throw new Exception('Not supported'); }
    public function createPlan(...) { throw new Exception('Not supported'); }
    // ... 20+ métodos más inútiles
}

Problema 3: Dependencias Innecesarias

<?php

class SimpleCheckoutController
{
    // Solo necesita charge(), pero recibe TODA la interfaz
    public function __construct(
        private PaymentGatewayInterface $gateway
    ) {}

    public function processCheckout(Request $request)
    {
        // Solo usa charge()
        return $this->gateway->charge($request->amount, $request->data);

        // NO usa: subscriptions, customers, plans, webhooks, disputes, etc.
        // Pero está acoplado a todos esos métodos
    }
}

La Solución: Interfaces Segregadas

Vamos a dividir la interfaz gigante en interfaces pequeñas y específicas:

1. Interfaces Base

<?php

namespace App\Contracts\Payment;

/**
 * Capacidad básica de procesamiento de pagos
 */
interface ChargeableInterface
{
    public function charge(float $amount, array $data): PaymentResult;
}

/**
 * Capacidad de reembolso
 */
interface RefundableInterface
{
    public function refund(string $transactionId, ?float $amount = null): RefundResult;
}

/**
 * Capacidad de consultar estado de transacciones
 */
interface TransactionQueryableInterface
{
    public function getTransactionStatus(string $transactionId): TransactionStatus;
    public function getTransaction(string $transactionId): Transaction;
}

2. Interfaces de Gestión de Clientes

<?php

namespace App\Contracts\Payment;

/**
 * Gestión de clientes en el gateway
 */
interface CustomerManageableInterface
{
    public function createCustomer(array $customerData): string;
    public function getCustomer(string $customerId): Customer;
    public function updateCustomer(string $customerId, array $data): bool;
}

/**
 * Gestión de métodos de pago de clientes
 */
interface PaymentMethodManageableInterface
{
    public function attachPaymentMethod(string $customerId, string $paymentMethodId): bool;
    public function detachPaymentMethod(string $paymentMethodId): bool;
    public function listPaymentMethods(string $customerId): array;
}

3. Interfaces de Suscripciones

<?php

namespace App\Contracts\Payment;

/**
 * Capacidad de crear y gestionar suscripciones
 */
interface SubscribableInterface
{
    public function createSubscription(string $customerId, string $planId): Subscription;
    public function cancelSubscription(string $subscriptionId): bool;
    public function getSubscription(string $subscriptionId): Subscription;
}

/**
 * Capacidad de pausar/reanudar suscripciones
 * (Solo algunos gateways lo soportan)
 */
interface SubscriptionPausableInterface
{
    public function pauseSubscription(string $subscriptionId): bool;
    public function resumeSubscription(string $subscriptionId): bool;
}

/**
 * Gestión de planes de suscripción
 */
interface PlanManageableInterface
{
    public function createPlan(array $planData): Plan;
    public function updatePlan(string $planId, array $data): Plan;
    public function listPlans(): array;
}

4. Interfaces de Webhooks

<?php

namespace App\Contracts\Payment;

/**
 * Procesamiento de webhooks
 */
interface WebhookHandlerInterface
{
    public function verifyWebhookSignature(string $payload, string $signature): bool;
    public function parseWebhook(string $payload): WebhookEvent;
}

5. Implementación de Stripe (Gateway Completo)

<?php

namespace App\Services\PaymentGateways;

use App\Contracts\Payment\{
    ChargeableInterface,
    RefundableInterface,
    CustomerManageableInterface,
    SubscribableInterface,
    SubscriptionPausableInterface,
    WebhookHandlerInterface
};

/**
 * Stripe implementa TODAS las interfaces porque lo soporta todo
 */
class StripeGateway implements
    ChargeableInterface,
    RefundableInterface,
    CustomerManageableInterface,
    SubscribableInterface,
    SubscriptionPausableInterface,
    WebhookHandlerInterface
{
    public function charge(float $amount, array $data): PaymentResult
    {
        // Implementación real
        return $this->processStripeCharge($amount, $data);
    }

    public function refund(string $transactionId, ?float $amount = null): RefundResult
    {
        // Implementación real
        return $this->processStripeRefund($transactionId, $amount);
    }

    public function createCustomer(array $customerData): string
    {
        // Implementación real
    }

    public function createSubscription(string $customerId, string $planId): Subscription
    {
        // Implementación real
    }

    public function pauseSubscription(string $subscriptionId): bool
    {
        // Stripe SÍ soporta pausar
        return $this->pauseStripeSubscription($subscriptionId);
    }

    // ... resto de métodos REALES
}

6. Implementación de PayPal (Gateway Limitado)

<?php

namespace App\Services\PaymentGateways;

use App\Contracts\Payment\{
    ChargeableInterface,
    RefundableInterface,
    CustomerManageableInterface
};

/**
 * PayPal solo implementa las interfaces que REALMENTE soporta
 */
class PayPalGateway implements
    ChargeableInterface,
    RefundableInterface,
    CustomerManageableInterface
{
    public function charge(float $amount, array $data): PaymentResult
    {
        // Implementación real de PayPal
    }

    public function refund(string $transactionId, ?float $amount = null): RefundResult
    {
        // Implementación real de PayPal
    }

    public function createCustomer(array $customerData): string
    {
        // PayPal tiene su propio concepto de "customer"
    }

    // NO implementa SubscriptionPausableInterface
    // NO implementa WebhookHandlerInterface
    // NO lanza excepciones por métodos no soportados
}

7. Implementación Simple (Gateway Mínimo)

<?php

namespace App\Services\PaymentGateways;

use App\Contracts\Payment\ChargeableInterface;

/**
 * Gateway simple de pagos únicos
 * Solo implementa lo mínimo
 */
class CashAppGateway implements ChargeableInterface
{
    public function charge(float $amount, array $data): PaymentResult
    {
        // Solo procesa pagos únicos
        return $this->processCashAppPayment($amount, $data);
    }

    // Eso es TODO. Sin métodos innecesarios.
}

8. Uso con Type Hints Específicos

<?php

namespace App\Services;

use App\Contracts\Payment\{
    ChargeableInterface,
    RefundableInterface,
    SubscribableInterface
};

/**
 * Servicio de checkout: Solo necesita cargos
 */
class CheckoutService
{
    public function __construct(
        private ChargeableInterface $gateway // Solo pide lo que usa
    ) {}

    public function processCheckout(float $amount, array $data): PaymentResult
    {
        return $this->gateway->charge($amount, $data);
    }
}

/**
 * Servicio de suscripciones: Necesita cargos Y suscripciones
 */
class SubscriptionService
{
    public function __construct(
        private ChargeableInterface&SubscribableInterface $gateway
        // Union type: debe implementar AMBAS interfaces
    ) {}

    public function subscribe(User $user, string $planId): Subscription
    {
        // Puede usar charge() y createSubscription()
    }
}

/**
 * Servicio de soporte: Necesita refunds
 */
class CustomerSupportService
{
    public function __construct(
        private RefundableInterface $gateway // Solo refunds
    ) {}

    public function processRefund(string $transactionId): RefundResult
    {
        return $this->gateway->refund($transactionId);
    }
}

9. Detector de Capacidades

<?php

namespace App\Services;

/**
 * Helper para detectar capacidades de un gateway
 */
class GatewayCapabilityDetector
{
    public function supports(object $gateway, string $capability): bool
    {
        return match($capability) {
            'charge' => $gateway instanceof ChargeableInterface,
            'refund' => $gateway instanceof RefundableInterface,
            'subscription' => $gateway instanceof SubscribableInterface,
            'subscription_pause' => $gateway instanceof SubscriptionPausableInterface,
            'webhooks' => $gateway instanceof WebhookHandlerInterface,
            default => false,
        };
    }

    public function getCapabilities(object $gateway): array
    {
        $capabilities = [];

        if ($gateway instanceof ChargeableInterface) {
            $capabilities[] = 'charge';
        }
        if ($gateway instanceof RefundableInterface) {
            $capabilities[] = 'refund';
        }
        if ($gateway instanceof SubscribableInterface) {
            $capabilities[] = 'subscription';
        }
        if ($gateway instanceof SubscriptionPausableInterface) {
            $capabilities[] = 'subscription_pause';
        }

        return $capabilities;
    }
}

10. Uso Dinámico con Verificación

<?php

namespace App\Http\Controllers;

use App\Contracts\Payment\{ChargeableInterface, SubscriptionPausableInterface};
use App\Services\GatewayCapabilityDetector;

class SubscriptionController extends Controller
{
    public function __construct(
        private GatewayCapabilityDetector $detector
    ) {}

    public function pause(Request $request, ChargeableInterface $gateway)
    {
        // Verificar capacidad antes de usar
        if (!$this->detector->supports($gateway, 'subscription_pause')) {
            return response()->json([
                'error' => 'This payment gateway does not support pausing subscriptions',
            ], 400);
        }

        // Safe cast porque ya verificamos
        /** @var SubscriptionPausableInterface $gateway */
        $gateway->pauseSubscription($request->subscription_id);

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

Comparación

Antes (Violando ISP)

// Interface gigante
interface PaymentGatewayInterface {
    // 30+ métodos
}

// Implementaciones llenas de excepciones
class PayPalGateway implements PaymentGatewayInterface {
    public function pauseSubscription() {
        throw new Exception('Not supported');
    }
    // ... 15+ métodos más con excepciones
}

// Dependencias innecesarias
class CheckoutService {
    public function __construct(
        PaymentGatewayInterface $gateway // Recibe 30+ métodos, usa 1
    ) {}
}

Después (Aplicando ISP)

// Interfaces pequeñas y específicas
interface ChargeableInterface {
    public function charge(...);
}

// Implementaciones solo con lo que soportan
class PayPalGateway implements ChargeableInterface, RefundableInterface {
    // Solo 2-3 métodos reales
}

// Dependencias exactas
class CheckoutService {
    public function __construct(
        ChargeableInterface $gateway // Solo lo que necesita
    ) {}
}

Beneficios

1. Clases Más Pequeñas

// Antes: 400 líneas con 30+ métodos (15 lanzando excepciones)
// Después: 50 líneas con 3 métodos reales

2. Testing Más Simple

public function test_checkout_processes_payment()
{
    // Solo mockear lo que usa
    $gateway = Mockery::mock(ChargeableInterface::class);
    $gateway->shouldReceive('charge')->once();

    $service = new CheckoutService($gateway);
    // ...
}

3. Composición Flexible

// Crear gateway personalizado combinando capacidades
class CustomGateway implements
    ChargeableInterface,
    RefundableInterface
{
    // Solo lo que necesitas
}

Casos de Uso Reales

Gateway con Fallback

class FallbackGateway implements ChargeableInterface
{
    public function __construct(
        private ChargeableInterface $primary,
        private ChargeableInterface $fallback
    ) {}

    public function charge(float $amount, array $data): PaymentResult
    {
        $result = $this->primary->charge($amount, $data);

        if (!$result->success) {
            return $this->fallback->charge($amount, $data);
        }

        return $result;
    }
}

Gateway con Logging

class LoggingGatewayDecorator implements
    ChargeableInterface,
    RefundableInterface
{
    public function __construct(
        private ChargeableInterface&RefundableInterface $gateway,
        private Logger $logger
    ) {}

    public function charge(float $amount, array $data): PaymentResult
    {
        $this->logger->info('Charging', ['amount' => $amount]);
        $result = $this->gateway->charge($amount, $data);
        $this->logger->info('Charge result', ['success' => $result->success]);

        return $result;
    }

    public function refund(string $transactionId, ?float $amount = null): RefundResult
    {
        $this->logger->info('Refunding', ['transaction' => $transactionId]);
        return $this->gateway->refund($transactionId, $amount);
    }
}

Conclusión

El Principio de Segregación de Interfaces nos enseña:

  • ✅ Interfaces pequeñas y enfocadas
  • ✅ Clientes solo dependen de lo que usan
  • ✅ Implementaciones sin métodos forzados
  • ✅ Mayor flexibilidad y composición

Regla de oro: Si tu interfaz tiene más de 5 métodos, probablemente deberías dividirla.

En el próximo y último artículo exploraremos el Principio de Inversión de Dependencias, donde veremos cómo depender de abstracciones en lugar de implementaciones concretas.


Artículo anterior: SOLID en Laravel (3/5): Principio de Sustitución de Liskov Próximo artículo: SOLID en Laravel (5/5): Principio de Inversión de Dependencias