Lightweight Containers causan masivo cierre de Factories


"Manifestación", témpera del artista argentino Antonio Berni (1934)
 

 Orientación  a Objetos implica romper monolitos de código. Implica distribuir la lógica en clases (ojalá reusables) que cooperen de modo tal que algunas consuman lo que otras provean

Un problema elemental presente desde los primeros tiempos de este paradigma, es el llamado A-B-C:

  • Direccionamiento (Addressing): dónde se encuentra la clase proveedora?
  • Enlace (Binding): a cuál clase de la jerarquía es a quien se debe contactar?
  • Contrato (Contract): cómo hay que comunicarse con la clase proveedora?

Llamemos a este problema el Problema de la Dependencia (curioso parecido con la política). Voy a ilustrarlo con un ejemplo:

Somos una empresa que ofrece servicios de Internet. Tenemos varias políticas de consumo para satisfacer un rango amplio de necesidades de clientes:

  • por dial-up con un abono básico de hasta x minutos a costo fijo, y los excedentes a una determinada tasa
  • dial-up con un costo fijo un poco más caro pero sin límite de minutos
  • con banda ancha (mayor velocidad que dial-up) a un costo fijo mayor
  • con banda ancha durante la noche a un costo fijo intermedio, y durante el día por dial-up a cierta tasa
  • etc

Los distintos consumidores eligen el plan de su conveniencia hasta que llega el proceso de cierre mensual y debemos facturarle a todos sus respectivos consumos, aplicando a cada cliente la política de facturación que escogió

Acá se presenta el Problema de la Dependencia: el proceso facturador depende de las distintas políticas de facturación. Veamos cómo se ha ido abordando esta dependencia a lo largo del tiempo

Estadío 1 – Programación Estructurada: Festival de IF
Aún me topo con enormes segmentos de código (especialmente en COBOL ya que sigue siendo mainstream) que (no) resuelven el Problema de la Dependencia anidando secuencias de IF en el proceso facturador

FACTURAR-INTERNET
    IF WK-TIPO-ABONO = K-DIAL-UP
        PERFORM FACTURAR-DIAL-UP THRU FACTURAR-DIAL-UP-END
        GO TO FACTURAR-INTERNET-END.

    IF WK-TIPO-ABONO = K-DIAL-UP-ILIMIT
        PERFORM FACTURAR-DIAL-UP-ILIM THRU FACTURAR-DIAL-UP-ILIM-END
        GO TO FACTURAR-INTERNET-END.

    IF WK-TIPO-ABONO = K-BANDA-ANCHA
        PERFORM FACTURAR-BAN-ANCH THRU FACTURAR-BAN-ANCH-END
        GO TO FACTURAR-INTERNET-END.

    IF WK-TIPO-ABONO = K-BANCH-MIX
        PERFORM FACTURAR-BANCH-MIX THRU FACTURAR-BANCH-MIX-END
        GO TO FACTURAR-INTERNET-END.

    …
FACTURAR-INTERNET-END
    EXIT.

Y así podrían surgir nuevas políticas de facturación que este, más que monolito, esta "piedra Rosetta" va a expandirse sin límite. Alguien por casualidad quería un buen ejemplo de acoplamiento? Si bien COBOL permite llamadas (CALL) a programas externos, la declaración de parámetros formales (LINKAGE SECTION) es muy pesada y más pesada aún la ejecución de la invocación

Estadío 2 – Programación Orientada a Objetos: Festival de switch
Justamente para romper monolitos fue que surgió el paradigma Orientado a Objetos (OO). No obstante hubo que introducir el concepto de Interfaz (Interface). Una interfaz declara el contrato de las funcionalidades que las clases que implementen la interfaz deben proveer. De esta forma logramos beldades como la siguiente (pseudocódigo estilo C++/Java/C#/…)

public interface IPoliticaFacturacionInternet {
   // sólo establece el contrato para hablar con
   // las clases que implementen
   public double facturar(ConsumoInternet consumo);
}

public class PoliticaDialUp : IPoliticaFacturacionInternet {
   // da al contrato de la interfaz contenido específico
   // de Dial-Up
   public double facturar(ConsumoInternet consumo) { … }
}

public class PoliticaBandaAncha : IPoliticaFacturacionInternet {
   // idem pero de Banda Ancha
   public double facturar(ConsumoInternet consumo) { … }
}

// y así sucesivamente

public class FacturadorInternet {
   …

   public int facturarClientes(IList consumosInternet) {
      // iterando en toda la colección
      for (int i = 0;i<consumosInternet.length();i++) {
         ConsumoInternet consumo = consumosInternet.get(i);

         // obtenerPolitica se encarga de devolver
         // la clase adecuada
         IPoliticaFacturacionInternet politica = obtenerPolitica(consumo.tipoPolitica);

         // la que sea que se retorne, nos factura el 
         // consumo sin que sepamos cuál es
         double montoInternet = politica.facturar(consumo);

         …
      }
   }

   private IPoliticaFacturacionInternet obtenerPolitica(int tipo) {
      // suena conocido? Qué hay de nuevo, viejo?
      switch (tipo) {
         case K_DIAL_UP :
            return new PoliticaDialUp();
         case K_BANDA_ANCHA :
            return new PoliticaBandaAncha();
         case …
      }
   }
}

Después de todo es un avance. Ya no hay monolitos sino clases reusables y lógica reusable del facturador iterativo también (puede facturar no sólo internet sino otros tipos de bienes y servicios). Definitivamente es preferible pero aún no hemos resuelto del todo el Problema de la Dependencia, ya que sigue siendo el facturador el encargado de crear las versiones adecuadas de políticas de facturación

Estadio 3 – Patrones de Diseño: Festival de Fábricas (Factories)
En 1995 cuatro amigos, Erich Gamma, Richard Helm, Ralph Johnson y el fallecido John Vlissides, publicaron un libro destinado a resolver éste y varios otros tipos de problemas de diseño cuya presencia era recurrente en cualquier ámbito de desarrollo de aplicaciones modernas. El libro se llama Patrones de Diseño: Elementos Reusables de Software Orientado a Objetos (Dessign Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, 1995) y, no sé creo que haya sido el primero pero sí fue de los primeros en acuñar el concepto de Patrón de Diseño: solución reusable dentro del contexto en que cada uno de estos problemas recurrentes se manifiesta. Gamma tiene hoy un rol no menor en el proyecto Eclipse: el más exitoso IDE J2EE que hace temblar incluso a Visual Studio

Particularmente para este caso, lo que esta "banda" propone (así se los llama hoy: GoF por "gang of four" o "banda de los cuatro") es quitarle al facturador masivo la responsabilidad de crear el objeto que implementa la correspondiente política de facturación, encapsulando esa lógica en una Fábrica (Factory)

public class FabricaPoliticaFacturacionInternet {
   public IPoliticaFacturacionInternet obtenerPolitica(int tipo) {
      // contenido similar a
      // FacturadorInternet.obtenerPolitica(…) de Estadío 2
      …
   }
}

De esta manera, el facturador queda con una responsabilidad menos: no va a tener que crear per-sé cada política de facturación si bien a cambio debe contar con una fábrica a quien delegarle tal responsabilidad. La fábrica le va a enchufar (plug-in) las dependencias

public class FacturadorInternet {
   …

   public int facturarClientes(IList consumosInternet) {
      // en lugar de crear políticas, crea una fábrica de ellas
      FabricaPoliticaFacturacionInternet fabrica =
            new FabricaPoliticaFacturacionInternet();

      // iterando en toda la colección
      for (int i = 0;i<consumosInternet.length();i++) {
         ConsumoInternet consumo = consumosInternet.get(i);

         // ahora se delega a la fábrica
         // la creación de la política
         IPoliticaFacturacionInternet politica =
               fabrica.obtenerPolitica(consumo.tipoPolitica);

         // la que sea que se retorne, nos factura el consumo
         // sin que sepamos cuál es
         double montoInternet = politica.facturar(consumo);

         …
      }
   }

  
}

El principio rector de esta Inversión del Control en la creación de instancias (Inversion of Control o frecuentemente IoC) es el llamado Principio de Hollywood (Hollywood Principle): "No me llame, yo lo llamaré a Ud". En otras palabras, "no se preocupe de crear y configurar políticas de facturación, yo haré eso para Ud"

Esto tiene cuantiosos beneficios respecto del desacoplamiento del facturador:

  • Separación de Intereses (Separation of Concerns). Pueden surgir o desaparecer innumerables políticas de facturación sin que la lógica concreta del facturador se deba alterar. Sólo se altera la lógica específica, cohesiva del proceso de creación. A simple vista puede sonar a "nada se crea, nada se pierde, todo se transforma". Sin embargo aplicando este principio desaparecerán diálogos tensos y densos como el siguiente:
    -Quién fue el último que tocó el facturador? Fulano lo tocó?
    -Sí, pero yo lo toqué porque se agregó una política
    -No sé, la cosa es que ahora hay clientes que se les factura dos veces el mismo ítem
    -Pero yo no toqué esa parte!! Yo no tengo ni idea de esa lógica del facturador!!
    -No sé, arreglalo. Desde que se instaló tu cambio que dejó de andar. Si la facturación no sale hoy, el que va a salir sos vos
    … Creánme que este avance conviene
  • Mejor administración del equipo de desarrollo y evolución del software. Corolario de lo anterior. Podemos asignar un responsable del proceso de facturación y otro de la administración de políticas de facturación. Ante una alteración en las políticas vigentes, es claro quién es el responsable primario del upgrade del software, y quién el relacionado en segunda instancia
  • Centralización y reuso de la lógica de configuración de objetos creados. Es muy factible que al crearse una nueva política de facturación, antes de quedar disponible para ser usada, sea necesario asignar valores iniciales a ciertas propiedades. En otras palabras, es posible que cada clase de la que el facturador depende, tenga a su vez sus propias dependencias. Al encapsular en la Fábrica toda esa lógica de instanciación, aquellas clases o procesos dependientes de lo fabricado reusan esta facilidad
  • Mejor control de instancias. En estos ejemplos venimos creando una nueva instancia de una clase cada vez que es necesaria. No obstante en la práctica si estas creaciones son muy frecuentes la performance puede verse degradada. Para ello es útil reaprovechar instancias ya creadas de una clase, sobre todo si el estado de una política de facturación dada es independiente de cuántos clientes con esa política se hayan facturado. Limitar el número de instancias de una clase por este mecanismo es mucho mejor que hacerlo con el patrón Singleton, pero de esto nos vamos a ocupar en otro artículo
  • Mayor desacoplamiento de clases creadas. Supongamos que cada tipo de política de facturación "vive" en un proceso remoto ya que está implementado por una empresa externa a la que se le tercerizó (outsourcing) la implementación de políticas de facturación
    Usemos un poco la imaginación: esa empresa implementa un algoritmo de cálculo basado en las últimas tres cotizaciones de la onza de trigo en Chicago, el índice de precios al consumidor, la tasa de desocupación y la diferencia de cotización entre el dólar y el euro. Finalmente, además de todo eso, un cliente aceptó que le facturen internet en una forma dependiente de todos estos indicadores
    Si la política de facturación corre en un proceso remoto, la fábrica entonces nos devuelve un Proxy (agente) al mismo: nos devuelve un objeto que, tras la fachada de una IPoliticaFacturacionInternet no hace otra cosa que esconder la complejidad del proceso de requerimiento/respuesta (request/response) con el proceso remoto (el cuál podría saludablemente no conocer de la interfaz IPoliticaFacturacionInternet)

Existen otros beneficios no comentados sobre este patrón de diseño. Sin embargo el título de este artículo habla del cierre masivo de fábricas. Qué contras tienen las mismas? Ciertos escenarios donde las fábricas pueden complicar más de lo que resuelven. Veamos

  • Poca adaptación a escenarios híbridos. La instanciación y configuración de dependencias que la fábrica provée podría no ser tan reusable entre distintas clases o procesos clientes. Esto llevaría a sobrecargar los métodos de la fábrica para que acepten parámetros que se acomoden a las necesidades de sus clientes, lo que desembocaría en un oscurecimiento de su lógica
  • Inutilidad de la fábrica como solución. Una forma de evitar la consecuencia anterior es dejar que la fábrica fabrique instancias predecibles, y que luego cada cliente de la misma termine de configurar sus instancias. No obstante, ciertas configuraciones adicionales podrían terminar duplicándose entre varias clases clientes. Podemos hablar de subclasear la fábrica (algo que GoF le dio forma en el patrón Fábrica Abstracta, que no vamos a cubrir acá). No obstante, subclaseando la fábrica tenemos un nuevo foco de conflicto que pasamos a analizar en el punto siguiente
  • Acoplamiento en segunda instancia. Si bien el facturador se desacopló de las políticas, quedó acoplado en tiempo de compilación a la fábrica. Si como decíamos antes, nos vemos en la necesidad de crear una jerarquía de fábricas, qué impacto puede tener esto en las clases que dependen de la misma? Es cierto que entre depender de las distintas políticas de facturación y depender de la fábrica, esta última dependencia es preferible porque estamos achicando en forma mayúscula la superficie de acoplamiento. De todas formas, aún es responsabilidad del facturador el pedir políticas a la fábrica apropiada
  • Proliferación descontrolada de fábricas. Volvamos al punto donde la fábrica de políticas está configurando las propiedades de una política dada. Si estas propiedades dependen de otras clases, lo lógico sería no acoplar la fábrica a las mismas. Por ende, podemos reutilizar la solución que el patrón Fábrica nos propone, haciendo que cada fábrica le pida instancias de dependencias a otras fábricas. Como se puede apreciar, sigue el ciclo de festivales. Sin una buena herramienta de generación de código que nos alivie la aplicación del patrón Fábrica, este estadío es mejor que los anteriores en términos de mantenibilidad posterior, mas no en cuanto a implementación inicial

Estadío 4 – Inversión de Control: Festival de Inyección de Dependencias
Como respuesta contemporánea a los problemas derivados de la solución aplicada en el estadío 3, están floreciendo por doquier los así llamados "Contenedores Livianos" (Lightweight Containers). Más adelante veremos el por qué de tal denominación. Estos contenedores tienen un componente jugando el rol de Ensamblador universal. Mediante alguna implementación apropiada, sea por configuración o por código pero en ambos casos asociadas con el concepto de Reflexión (Reflection), el ensamblador se encarga de devolver instancias (las que fueren) completamente configuradas a cualquier clase cliente que se las pida. Al proceso completo de crear instancias configuradas, Martin Fowler lo bautizó como Inyección de Dependencias (Dependency Injection)
El orígen del concepto de "Contenedor Liviano" es por oposición a otros tipos de contenedores de objetos que estaban fuertemente comprometidos con API’s propietarias. Entre estos tenemos a COM+ (ex MTS), a los Enterprise JavaBeans (EJB), a los EnterpriseServices de .NET, etc. Potentes en cuanto a los servicios que brindan, pero que obligan a los desarrolladores a heredar de clases propias (en entornos OO que no permiten herencia múltiple, quitando la posibilidad de herencia de clases propias). Por consiguiente la decisión de meterse en estos contenedores pesados implica cadenas pesadas de las cuales luego no será fácil salirse

El desafío y éxito de los contenedores livianos radica en que, excepto el ensamblador (la fábrica universal) todas las clases consumidoras como las clases proveedoras no tienen dependencias respecto del contenedor: son nuestras propias clases planas

Existen numerosos ejemplos de contenedores livianos disponibles. El primero que voy a mencionar es el que viene incluído en la versión 2.0 de Enterprise Library. Su nombre es ObjectBuilder y al parecer será el ensamblador de todo lo que en el futuro próximo el comité de Patterns & Practices nos provea. No obstante, es importante saberlo, no se requiere conocer cómo trabaja este mecanismo para usar la Enterprise Library. Aún así, quienes lo conozcan, lo pueden aprovechar en sus propias aplicaciones (y vale la pena)

Para cerrar el caso del facturador de Internet, ahora la lógica del mismo se simplifica porque ya no será necesario generar una clase Fábrica

using Microsoft.Practices.EnterpriseLibrary.Common.Configuration.ObjectBuilder;

public class FacturadorInternet {
   …

   public int facturarClientes(IList consumosInternet) {
      // iterando en toda la colección
      for (int i = 0;i<consumosInternet.length();i++) {
         ConsumoInternet consumo = consumosInternet.get(i);

         // EnterpriseLibraryFactory es un ensamblador
         // universal basado en generics. No vamos a ver
         // acá sus argumentos
         IPoliticaFacturacionInternet politica = EnterpriseLibraryFactory.BuildUp<IPoliticaFacturacionInternet>(…);

         // la que sea que se retorne, nos factura el consumo
         // sin que sepamos cuál es
         double montoInternet = politica.facturar(consumo);

         …
      }
   }

  
}

Pienso dedicar pronto un artículo a comentar mejor cómo se usa ObjectBuilder ya que si bien no es nada complejo (todo lo contrario), explicar todas sus potencialidades y usos extenderían innecesariamente este artículo

Otro contenedor liviano muy reconocido es el Spring Framework. El más completo por su soporte a diferentes frameworks open source así como también a un entorno de programación orientada a aspectos (AOP). Rod Johnson, el creador de esta facilidad, viene escribiendo año tras año libros que documentan el último grado de evolución y mejores prácticas de uso de su invalorable "donación". El éxito de Spring hace temer por el futuro de la especificación estándar J2EE, ya que esta último promueve contenedores pesados. Spring está disponible tanto en J2EE como en .NET, aunque preveo difícil que en el entorno de Microsoft llegue a alcanzar uso masivo: Spring esconde habilmente complejidades del estándar J2EE que no están presentes en .NET. Por ende presumo que no habría una motivación a meter esta pieza en .NET. Quizás sí para aquellos desarrolladores J2EE que la hayan usado exitosamente en esa plataforma y quieran una transición suave a la plataforma de Microsoft. No obstante, como cuenta Johnson, la versión .NET no es una portación del código de la versión J2EE sino una forma de simplificar detalles de .NET no necesariamente presentes en J2EE

Existen varios contenedores más: Pico Container, HiveMind, XWork, etc. Lo que distingue a los mismos es el nivel de soporte que dan a los distintos tipos de IoC:

  • Tipo 1: Inyección para Interfaces
  • Tipo 2: Inyección mediante Setters
  • Tipo 3: Inyección mediante Constructores

Los Contenedores Livianos no adolescen de los problemas que suele enfrentar el patrón Fábrica (ver más arriba). Asimismo, las implementaciones basadas en archivos de configuración ha posibilitado a los desarrolladores poder testear segmentos extensos de código de lado servidor sin necesidad de montar un servidor completo. Mediante cambios adecuados de los archivos de configuración del contenedor, se instancian las clases necesarias en un entorno aislado y controlado, sin necesidad de recompilar nada

Epilogo
Windows Communication Foundation (WCF o Indigo) es un Contenedor Liviano orientado a servicios web. Su modelo de solución claramente encara la problemática A-B-C planteada al principio: Direccionamiento (Dónde?), Enlace (Con quién?) y Contrato (Cómo?)

Referencias de Inyección de Dependencias

Esta entrada fue publicada en Software Architecture. Guarda el enlace permanente.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s