El programador flanker: De la cancha de rugby a la arquitectura de software

Strategy, un patrón de diseño que convirtió el procesamiento de expedientes caóticos en código elegante

Posted by Daniel Arbelaez on Friday, February 28, 2025

El reto: Automatización flexible en expedientes judiciales

En el mundo del desarrollo de software, nos encontramos frecuentemente con situaciones que requieren soluciones elegantes y mantenibles. Recientemente, me enfrenté a un caso interesante, que trataba sobre la implementación de un sistema de procesamiento de carpetas manteniendo un equilibrio entre flexibilidad y desacoplamiento.

El escenario planteaba un desafío particular. Se necesitaba desarrollar una funcionalidad que permitiera procesar carpetas de diferentes maneras, donde cada método de procesamiento compartía una estructura común pero requería su propia implementación específica. Lo fascinante de este requisito era la necesidad explícita de mantener un bajo acoplamiento, asegurando que las diferentes maneras de procesar, pudieran evolucionar de manera independiente.

La inspiración: Lecciones desde la cancha de rugby

La ortogonalidad en software es uno de esos conceptos que, aunque fundamentales, pueden resultar abstractos hasta que encuentras la analogía perfecta. Para mí, esa analogía llegó mientras veía un partido de rugby del “Torneo de las Seis Naciones 2025” y recordé mi posición favorita de esta disciplina deportiva, el “flanker”.

alt

Un flanker (posición 6 o 7) es quizás el jugador más versátil del equipo. No está completamente especializado como un forward o un back, sino que participa en casi todas las fases del juego. Defiende, ataca, apoya, disputa balones y conecta las diferentes líneas de juego. Es ortogonal por naturaleza.

No sucede lo mismo con el resto de forwards, como los pilares (posiciones 1 y 3) o el hooker (posición 2), cuyos roles estan altamente especializados en las formaciones fijas, el contacto físico y la obtención de la pelota. Tampoco es como los backs, desde el medio scrum (9) hasta los wings (11 y 14), quienes se especializan en ataque, velocidad y manejo de balón.

alt

El flanker existe precisamente en ese límite entre ambos mundos, tiene la potencia física de un forward pero la movilidad de un back, permitiéndole adaptarse a cada situación y mantener la cohesión entre las diferentes líneas de juego sin perder su propia función en el equipo.

En programación informática, ortogonalidad significa que las operaciones cambian sólo una cosa sin afectar a otras. tomado de Wikipedia

Este concepto tiene un paralelo sorprendentemente directo con lo que en programación llamamos “ortogonalidad”. Un sistema es ortogonal cuando un cambio en un componente no afecta a otros componentes no relacionados. Al igual que el flanker opera en múltiples dimensiones del juego sin perder su esencia, un código ortogonal permite que cada módulo cumpla su función específica sin interferir con otros.

La ortogonalidad en software ofrece dos beneficios fundamentales que cualquier equipo de programación reconocerá inmedientamente y son la localización de problemas y adaptabilidad. Cuando un módulo falla, el error queda contenido. Cuando necesitas cambiar una estrategia, solo modificas ese componente específico. No es casualidad que los sistemas más robustos y mantenibles sean también los más ortogonales ya que pueden avanzar en su propia dirección desarrollando operaciones sin afectar las demás vias de crecimiento.

En términos matemáticos, al observar estos tres planos que se intersectan en ángulos rectos (X, Y, Z), se puede evidenciar que cada uno puede moverse a lo largo de su propio eje sin afectar a los demás.

alt

Tres planos ortogonales - Tomado de Wikipedia

Estos planos ortogonales no son solo una abstracción matemática, en software se manifiestan como interfaces bien definidas y responsabilidades claramente separadas, que fue exactamente lo que implementé, ahí estaba lo que necesitaba mi sistema de procesamiento de carpetas, era exactamente eso, componentes “flanker” con interfaces que permitan:

  • Mantener su independencia (bajo acoplamiento)
  • Ser altamente cohesivos en su función específica
  • Interactuar con múltiples partes del sistema sin volverse dependientes

Esta estructura requería preservar la ortogonalidad del sistema mientras permitiera distintas implementaciones para cada tipo de procesamiento de carpetas. Cada estrategia debía poder ejecutarse con la misma llamada externa, pero internamente actuar de forma completamente diferente según el contexto. Además, debía poder incorporar nuevas estrategias en el futuro sin modificar el código existente. Fue entonces cuando recordé uno de los patrones de diseño más versátiles del arsenal de la programación orientada a objetos.

Del caos al orden: Cómo el patrón Strategy transformó mi código

Al analizar este escenario, el patrón Strategy emergió como la solución natural. Este patrón, uno de los pilares del diseño orientado a objetos, nos permite encapsular diferentes algoritmos en clases separadas, permitiendo que sean intercambiables. Es como tener diferentes herramientas en una caja de herramientas, donde cada una está diseñada para un propósito específico pero todas comparten la misma manera de ser utilizadas.

La belleza de esta aproximación radica en su simplicidad y elegancia. Al definir una interfaz común para todos los procesadores de carpetas, creamos un contrato claro que cada implementación debe seguir. Esto no solo facilita la adición de nuevos métodos de procesamiento sino que también simplifica el mantenimiento del código existente. Es como establecer un protocolo estándar que todos los nuevos desarrollos deben seguir, asegurando consistencia y predictibilidad en el sistema.

El desafío no era trivial. Estaba desarrollando un sistema para gestionar expedientes judiciales electrónicos que debía procesar carpetas de diferentes maneras según su estructura:

  • Cuadernos simples
  • Expedientes completos
  • Múltiples expedientes

Cada tipo requería su propio tratamiento, pero compartían operaciones comunes. Y lo más importante que estuvieran completamente desacoplados de la lógica de procesamiento.

Mi primer intento fue un gran bloque de código con condicionales anidados. Funcionaba, sí, pero era imposible de mantener. Cualquier cambio en la interfaz requería revisar toda la lógica de procesamiento y viceversa. Era como un equipo de rugby donde todos los jugadores intentaban hacer todas las posiciones a la vez. La respuesta estaba en un patrón de diseño que, al igual que un buen flanker, debía ser extremadamente versátiles.

El patrón Strategy: Especialistas intercambiables

Implementé una interfaz abstracta llamada ProcessStrategy que definía el contrato común para todas las estrategias de procesamiento:

class ProcessStrategy(ABC):
    @abstractmethod
    def add_folder(self, processor):
        pass
        
    @abstractmethod
    def process(self, processor):
        pass

Luego creé las clases con las estrategias concretas para cada tipo de procesamiento:

  • SingleCuadernoStrategy
  • SingleExpedienteStrategy
  • MultiExpedienteStrategy

Así, cada estrategia sabía exactamente cómo manejar su caso específico, pero desde fuera, todas se usaban de la misma manera. Como un flanker que sabe cuándo atacar y cuándo defender, cambiando de rol según la situación del partido.

Para más detalles en código, visita el repositorio original que contiene la clase ProcessStrategy

El contexto: Un director técnico cuando se ejecuta el código

Para completar la implementación del patrón Strategy, necesitaba un “director técnico” que decidiera qué estrategia utilizar en cada momento. Esta es la función del ProcessingContext:

class ProcessingContext:
    """Gestiona el procesamiento de carpetas usando la estrategia apropiada."""
    
    def __init__(self, gui_notifier: GUINotifier, logger=None):
        self.notifier = gui_notifier
        self.logger = logger or logging.getLogger("ProcessingContext")
        self._strategies = {
            "1": SingleCuadernoStrategy(self.notifier, self.logger),
            "2": SingleExpedienteStrategy(self.notifier, self.logger),
            "3": MultiExpedienteStrategy(self.notifier, self.logger),
        }

El contexto es sorprendentemente simple pero extremadamente poderoso. Actúa como un intermediario que permite lo siguiente:

  1. Inicializa todas las estrategias posibles: Como un entrenador que tiene preparados a todos sus jugadores.
  2. Selecciona la estrategia adecuada: Basándose en el valor proporcionado por el usuario.
  3. Delega la ejecución: No realiza el trabajo directamente, sino que lo encomienda al especialista correcto.

Por ejemplo, cuando el usuario selecciona “Cuaderno” en la interfaz (valor “1”), el contexto simplemente elige la estrategia adecuada y le pasa el control:

def process_folder(self, selected_value: str, processor: FileProcessor):
    # Selecciona la estrategia según el valor
    strategy = self._strategies.get(selected_value)
    
    # Registra la acción (importante para depuración)
    self.logger.info(f"Procesando carpeta con estrategia {strategy.__class__.__name__}")
    
    # Ejecuta la estrategia seleccionada
    strategy.process(processor)

Para más detalles en código, visita el repositorio original que contiene la clase ProcessingContext

La estructura del patrón Strategy: Simplicidad y elegancia

El diagrama UML del patrón Strategy ilustra perfectamente su elegancia estructural. Como se observa en la imagen, el patrón se compone de tres elementos fundamentales:

alt

  1. Una interfaz Strategy que define el contrato común para todos los algoritmos
  2. Las implementaciones concretas (ConcreteStrategyA y ConcreteStrategyB) que encapsulan las diferentes lógicas
  3. La clase Context que utiliza las estrategias y delega la ejecución al algoritmo seleccionado

Esta separación de responsabilidades nos permite aprovechar el polimorfismo, uno de los pilares fundamentales de la programación orientada a objetos. Gracias a este mecanismo, el contexto puede intercambiar estrategias sin conocer sus implementaciones específicas, manteniendo así un bajo acoplamiento entre componentes.

La verdadera potencia de este patrón radica precisamente en su simplicidad: al encapsular cada algoritmo en su propia clase que implementa la misma interfaz, conseguimos un sistema extremadamente flexible donde añadir nuevas estrategias no requiere modificar el código existente, respetando así el principio Open/Closed de SOLID.

El diagrama UML de este patrón es muy sencillo, solo separamos las diferentes lógicas para realizar una acción sin olvidar que implemente de una interface para luego valernos del polimorfismo para que nuestro patrón funcione. Y pues la clase contexto que puede ser cualquier clase que utilice nuestras estrategias.

Lecciones aprendidas: La belleza de la ortogonalidad

Uno de los momentos más estimulantes fue cuando un colega me pidió cambiar el comportamiento para los cuadernos simples. En mi antiguo diseño, habría significado revisar todo el código e incluir más if/else en las funciones relacionadas como obtener_rutas y process. Con este nuevo enfoque, solo modifiqué una clase y el resto del sistema siguió funcionando perfectamente. Por ende, esta experiencia me dejó tres lecciones valiosas:

  1. Los patrones de diseño son herramientas, no dogmas: Strategy funcionó perfectamente para este caso, pero lo importante fue entender el problema antes de elegir el patrón.

  2. Las analogías son poderosas para comprender conceptos abstractos: La comparación con el rugby me ayudó a internalizar la ortogonalidad mejor que cualquier definición técnica.

  3. La ortogonalidad no solo mejora el código, sino la comunicación: Explicar el sistema a mis colegas no técnicos usando la analogía del flanker hizo que entendieran inmediatamente por qué nuestro nuevo enfoque era superior.

Pases maestros: Del rugby al código elegante

La implementación resultante no solo satisface los requisitos funcionales, sino que establece cimientos robustos para el futuro. Cada nuevo algoritmo de procesamiento puede integrarse como una estrategia adicional sin alterar el código existente, honrando así el principio Open/Closed de SOLID. Este principio, fundamental en el diseño de software moderno, nos permite extender sin modificar – exactamente como un equipo de rugby que incorpora nuevos jugadores sin cambiar sus tácticas esenciales.

Al igual que un flanker debe adaptarse constantemente sin perder su propósito fundamental, un buen diseño de software mantiene su flexibilidad sin comprometer su integridad estructural. La auténtica elegancia no reside en construir sistemas complejos, sino en la simplicidad con la que resolvemos problemas intrincados.

En conclusión, este caso ilustra vívidamente cómo los patrones de diseño, cuando se aplican con criterio, transforman requisitos complejos en soluciones elegantes y mantenibles. No basta con que el código funcione; lo verdaderamente valioso es crear arquitecturas que faciliten su evolución y mantenimiento a través del tiempo. La maestría está en comprender no solo cómo implementar estos patrones, sino también en reconocer cuándo son apropiados para cada escenario específico.

De ahora en adelante, cada vez que te enfrentes a un desafio de diseño similar, puedes recordar al flanker, versátil pero enfocado, conectado pero independiente. A veces, las mejores lecciones de software provienen de los lugares más inesperados.