SOLID en Laravel (4/5): Principio de Segregación de Interfaces con Stripe
16 Dec 2025
10 min read
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
☕ ¿Te ha sido útil este artículo?
Apóyame con un café mientras sigo creando contenido técnico
☕ Invítame un café