Introducción: Por qué SOLID no es solo un dogma, sino una herramienta
La mayoría de los desarrolladores saben que SOLID son cinco principios de la programación orientada a objetos: Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation y Dependency Inversion. Sin embargo, en la práctica, la comprensión a menudo se reduce a seguir las reglas mecánicamente. En este artículo iremos más allá: analizaremos escenarios avanzados de aplicación de SOLID en PHP, veremos antipatrones que violan los principios y mostraremos cómo la refactorización teniendo en cuenta SOLID hace que el código sea flexible y testeable.
El material está dirigido a desarrolladores de nivel Middle+ y Senior que ya escriben en PHP y desean mejorar la arquitectura de sus proyectos.
1. Principio de responsabilidad única (SRP): cuando la clase «sabe demasiado»
La formulación clásica: «Una clase debe tener solo una razón para cambiar». A nivel avanzado, es importante entender que «razón para cambiar» es un requisito de negocio. Si tu clase cambia debido a cambios en las reglas de validación, el formato de salida y la lógica de persistencia, es una violación de SRP.
Ejemplo de violación (God Object)
class OrderProcessor { public function process(array $data): void { // 1. Validación de datos if (empty($data['user_id']) || empty($data['items'])) { throw new InvalidArgumentException('Invalid order data'); } // 2. Cálculo del total $total = 0; foreach ($data['items'] as $item) { $total += $item['price'] * $item['quantity']; } // 3. Guardar en BD $sql = "INSERT INTO orders (user_id, total, status) VALUES (?, ?, 'pending')"; // ... ejecución de la consulta // 4. Envío de email mail($data['user_email'], 'Order confirmation', "Total: $total"); }}Refactorización con SRP
Dividimos en tres clases con áreas de responsabilidad claras:
class OrderValidator { public function validate(array $data): void { if (empty($data['user_id']) || empty($data['items'])) { throw new InvalidArgumentException('Invalid order data'); } }}
class OrderCalculator { public function calculateTotal(array $items): float { return array_reduce($items, function ($carry, $item) { return $carry + $item['price'] * $item['quantity']; }, 0.0); }}
class OrderRepository { public function save(int $userId, float $total, string $status = 'pending'): int { // Lógica de guardado en BD return 42; // ID del pedido }}
class OrderProcessor { public function __construct( private OrderValidator $validator, private OrderCalculator $calculator, private OrderRepository $repository, private Mailer $mailer ) {} public function process(array $data): void { $this->validator->validate($data); $total = $this->calculator->calculateTotal($data['items']); $orderId = $this->repository->save($data['user_id'], $total); $this->mailer->send($data['user_email'], 'Order #' . $orderId); }}2. Principio de abierto/cerrado (OCP): extensión sin modificación
La esencia de OCP: las clases deben estar abiertas para la extensión, pero cerradas para la modificación. En la práctica, esto significa usar polimorfismo y abstracciones en lugar de estructuras condicionales.
Violación de OCP: switch-case para cada nuevo tipo
class PaymentProcessor { public function pay(string $type, float $amount): void { switch ($type) { case 'credit_card': // procesar tarjeta break; case 'paypal': // procesar PayPal break; // ¡Al agregar un nuevo tipo, modificamos la clase! } }