Marcio Cunha

¿Código duplicado? Cómo aplicar principios SOLID en la práctica

Aprende a refactorizar fragmentos complejos de código procedimental en estructuras orientadas a objetos limpias, testeables y desacopladas usando los principios SOLID.

Marcio Cunha•5 min
También disponible en:PortuguêsEnglish
Resumen
  • La duplicación de código en rutinas procedimentales genera rigidez estructural que dificulta cambios futuros y multiplica puntos de fallo.
  • El principio de responsabilidad única aísla comportamientos distintos para asegurar que cada clase cambie por un solo motivo.
  • Inyectar dependencias directamente en las estructuras desacopla los módulos y posibilita la creación de pruebas unitarias aisladas.
  • La apertura para extensión y el cierre para modificación permiten añadir funcionalidades sin reescribir el código existente.
  • La refactorización gradual transforma bloques lineales complejos en clases cohesivas, legibles y fáciles de mantener.

El peligro silencioso del código procedimental repetido

Todo programador se ha topado con ese archivo gigantesco donde docenas de líneas de código se repiten con pequeñas variaciones. En la programación procedimental tradicional, el foco está en una secuencia lineal de instrucciones que ejecutan tareas paso a paso. Sin embargo, cuando un requisito de negocio cambia, este enfoque pasa factura, exigiendo modificaciones en múltiples lugares y aumentando el riesgo de errores. Para entender cómo mitigar este problema, la mantenibilidad del sistema depende directamente de la claridad estructural, un desafío similar al enfrentado al estructurar bases complejas o incluso al estructurar microservicios cuando se migra un monolito gigante sin dolor hacia arquitecturas modulares.

En la práctica, esto significa que cuanta más lógica procedimental acumulamos dentro de una sola función, más difícil se vuelve probar el sistema. Un error común es mezclar reglas de cálculo, acceso a bases de datos y envío de notificaciones en un único bloque monolítico. Cuando el código crece sin una división clara de roles, el software pierde flexibilidad y el equipo gasta más tiempo corrigiendo fallas colaterales que creando nuevas funcionalidades.

Identificando síntomas de acoplamiento excesivo

El acoplamiento excesivo ocurre cuando diferentes partes del software dependen fuertemente unas de otras de manera rígida. Si alteramos la estructura de una tabla en la base de datos y eso rompe la lógica de visualización en pantalla, tenemos un fuerte indicador de que las fronteras arquitectónicas se han roto. Este escenario suele venir acompañado de funciones largas, llenas de condicionales anidadas que intentan adivinar todas las posibilidades de ejecución en un solo flujo.

Para ilustrar este escenario, imagina una rutina procedimental que procesa pagos y dispara correos de confirmación en el mismo bloque de código. Si necesitamos cambiar el proveedor de correo o agregar una nueva regla de descuento, tendremos que tocar una línea de código altamente sensible que ya funciona. Es precisamente en este punto que la aplicación inteligente de los conceptos orientados a objetos se vuelve indispensable para desacoplar las responsabilidades.

Aplicando la responsabilidad única para separar tareas

El primer paso para organizar la casa es aplicar el concepto de que cada parte del sistema debe tener un solo motivo para cambiar. En la práctica, separa la lógica de negocio pura de las reglas de infraestructura, como conexiones de red o persistencia de datos. En vez de una función gigante que hace todo, creamos clases especializadas donde cada una ejecuta una sola tarea específica a la perfección.

A continuación tenemos un ejemplo clásico de código procedimental inicial, repleto de responsabilidades mezcladas:

// Código procedimental acoplado y difícil de probar
function procesarPedido($pedido) {
    // Validación
    if (empty($pedido['items'])) {
        throw new Exception('Pedido vacío');
    }
    
    // Cálculo de precio
    $total = 0;
    foreach ($pedido['items'] as $item) {
        $total += $item['precio'];
    }
    
    // Guarda en la base de datos
    $db = new PDO('mysql:host=localhost;dbname=tienda', 'user', 'pass');
    $stmt = $db->prepare('INSERT INTO pedidos (total) VALUES (?)');
    $stmt->execute([$total]);
    
    // Envía correo
    mail($pedido['email'], 'Confirmación', 'Su pedido ha sido procesado.');
}

Este bloque maneja validación, cálculo, persistencia y mensajería todo junto. Cualquier cambio en el formato de la base de datos o en el servidor de correo exige alterar esta misma función, violando la cohesión del sistema.

Refactorizando en clases cohesivas y desacopladas

Para resolver el problema anterior, separamos las responsabilidades en pequeñas clases bien definidas. De esta forma, la validación se queda en su propio componente, el cálculo de valores en otro, y la persistencia utiliza una interfaz que abstrae la base de datos. Esta separación drástica reduce la duplicidad y permite que cada clase sea probada de forma aislada.

Mira cómo queda la estructura refactorizada usando conceptos orientados a objetos e inyección de dependencias:

// Clases cohesivas y desacopladas
class ValidadorPedido {
    public function validar(array $pedido): void {
        if (empty($pedido['items'])) {
            throw new Exception('Pedido vacío');
        }
    }
}

class CalculadoraPrecio {
    public function calcular(array $items): float {
        $total = 0;
        foreach ($items as $item) {
            $total += $item['precio'];
        }
        return $total;
    }
}

class ProcesadorPedido {
    private $validador;
    private $calculadora;

    public function __construct(ValidadorPedido $validador, CalculadoraPrecio $calculadora) {
        $this->validador = $validador;
        $this->calculadora = $calculadora;
    }

    public function ejecutar(array $pedido): float {
        $this->validador->validar($pedido);
        return $this->calculadora->calcular($pedido['items']);
    }
}

Con este enfoque, si necesitamos alterar la regla de cálculo de impuestos o cambiar la biblioteca de mensajería, el impacto se restringe únicamente a la clase correspondiente. La mantenibilidad del software salta a un nuevo nivel.

Garantizando extensibilidad sin modificar el núcleo

Otro beneficio fundamental al aplicar estos principios es la capacidad de extender el comportamiento del sistema sin necesidad de reescribir el código existente. Cuando utilizamos interfaces para definir contratos de comunicación entre los módulos, podemos conectar nuevas funcionalidades simplemente creando nuevas clases que implementen esas mismas interfaces, manteniendo la estabilidad general de la aplicación.

Esta flexibilidad es esencial en entornos dinámicos donde las reglas de negocio cambian constantemente. En lugar de saturar los archivos con condicionales del tipo if/else para verificar qué comportamiento ejecutar, delegamos esa elección al polimorfismo, haciendo que el código sea más limpio, elegante y resiliente ante alteraciones inesperadas.

Consideraciones finales sobre la evolución del código

Refactorizar código procedimental antiguo exige paciencia y disciplina, pero los beneficios a mediano y largo plazo superan ampliamente el esfuerzo inicial. Al dividir grandes bloques de lógica en estructuras orientadas a objetos cohesivas, el equipo gana velocidad y seguridad para entregar nuevas características sin miedo a romper el sistema heredado. La adopción de estas prácticas transforma bases de código frágiles en ecosistemas de software robustos, testeables y preparados para el crecimiento sostenible.