La arquitectura y el diseño de software son temas que generan mucho debate polémico, pero pocas conclusiones concretas. La Arquitectura Evolutiva y el Diseño Emergente son técnicas ágiles para posponer las decisiones hasta el último momento responsable. En este artículo definimos la arquitectura y el diseño, y luego identificamos preocupaciones comunes a estos temas.
La arquitectura y el diseño de software han resistido tener una definición firme durante mucho tiempo porque el desarrollo de software como disciplina no logró todavía comprender todas sus dificultades e implicancias. Pero para crear cualquier discusión razonable sobre este tema tenemos que empezar por algún lado.
Definiendo a la arquitectura
La arquitectura de software es uno de los temas más hablados y menos comprendido entre los desarrolladores. Se habla de la arquitectura en conferencias, charlas y reuniones informales, y sin embargo todavía tenemos definiciones muy vagas sobre lo que significa. Cuando discutimos sobre la arquitectura, en realidad estamos hablando de muchas cosas diferentes pero relacionadas que generalmente caen en las categorías más amplias de arquitectura de aplicación y arquitectura organizacional.
Arquitectura de aplicación
La arquitectura de aplicación describe las piezas de alto nivel que componen una aplicación. Por ejemplo, en el mundo Java, una arquitectura de aplicación describe dos cosas: la combinación de frameworks que se usan para construir una aplicación en particular (arquitectura a nivel de fraemework) y la separación tradicional de conceptos más lógicos (arquitectura de aplicación). Tratamos a la arquitectura de nivel de framework como algo diferente porque la mayoría de los desarrolladores de lenguajes de orientación a objetos descubren que las clases individuales no son suficientes como mecanismo de reutilización (¿cuándo fue la última vez que se bajaron una única clase para usar en un proyecto?). La unidad de reutilización de los lenguajes orientados a objetos modernos es el framework. Cuando iniciamos un proyecto con algún lenguaje rico en frameworks como Java, uno de los principales temas a resolver es la arquitectura a nivel de framework de la aplicación. Este estilo de reutilización es tan fuerte en el mundo Java que deberiamos dejar de decir que Java es orientado a objetos, y empezar a hablar de una orientación a frameworks. De esta manera, la arquitectura a nivel de framework representa una arquitectura física, compuesta por bloques de construcción.
El otro aspecto interesante de la arquitectura de aplicaciones describe cómo encajan juntas todas las piezas lógicas de una aplicación. Este es el dominio de los patrones de diseño y otras descripciones estructurales, y por lo tanto tienden a ser más abstractas y lógicas en vez de físicas. Por ejemplo, podríamos decir que una aplicación web sigue el patrón Model-View-Controller (MVC) sin especificar qué framework se usa para lograr este arreglo lógico. Este arreglo lógico es el que seguramente pase a adornar pizarras y salas de trabajo a medida que pasemos a trabajar en distntas partes de la aplicación.
Arquitectura organizacional
La arquitectura organizacional trata sobre cómo la organización en su totalidad (lo cual usualmente se refiere a las aplicaciones que corren en grandes organizaciones) consume otras aplicaciones. Podemos usar una metáfora y pensar en la organización como la planificación de una ciudad, y en la aplicación como la construcción de edificios. La planificación de la ciudad se encarga de obtener agua, electricidad, desagües y otros servicios para que la ciudad funcione. No podemos tener un edificio que consuma más que su parte del suministro de agua. La arquitectura organizacional considera estas mismas cosas para sus aplicaciones: no podemos tener una aplicación que consuma todo el ancho de banda.
La arquitectura organizacional logró una gran atención en los últimos años por la Arquitectura Orientada a Servicios (SOA). SOA es un tema enorme por si mismo que escapa a este artículo, pero vale destacar que tiene varios aspectos interesantes porque diluye la frontera entre las arquitecturas de organización y de aplicación al dictar características para la construcción de aplicaciones.
Hasta aquí dimos algunas definiciones superficiales de estos conceptos importantes, que nos servirán como base para otras definiciones más interesantes de arquitectura.
Otras definiciones
Hay muchos que ya intentaron definir el concepto de arquitectura de software, así que podemos repasar algunos de sus ideas. En su ensayo "Who needs an architect?" (¿Quién necesita un Arquitecto?), Martin Fowler muestra varias definiciones. Una de las primeras que menciona proviene de una lista de emails de Extreme Programming:
"RUP, partiendo de la definición de la IEEE, define la arquitectura como el más alto nivel conceptual de un sistema en su ambiente. La arquitectura de un sistema de software (en cualquier momento en el tiempo) es la organización o estructura de componentes significativos interactuando a través de interfaces, estos componentes están compuestos de componentes e interfaces sucesivamente más chicos".
Esta definición es muy parecida a la definición de arquitectura de aplicación que vimos más arriba. Aunque un poco vaga, captura la esencia de la responsabilidad de la arquitectura: el concepto de más alto nivel.
Fowler luego cita a Ralph Johnson, quien cuestiona la definición anterior en una respuesta a la lista:
"Una mejor definición sería: en la mayoría de los proyectos exitosos, los desarrolladores expertos que trabajan en ese proyecto tienen un conocimiento compartido del diseño del sistema. Este conocimiento compartido se llama arquitectura. Este conocimiento incluye cómo se divide al sistema en componentes y cómo estos componentes interactuan a través de interfaces".
Johnson hace un punto excelente en el cual el desarrollo de software necesita más de la comunicación que de la tecnología, y que la arquitectura en realidad representa este conocimiento compartido sobre el sistema, y no algo específico de lenguajes, frameworks, y otros temás tecnológicos secundarios.
En su ensayo, Fowler brinda una de las mejores definiciones de arquitectura:
"La arquitectura trata sobre temas importantes. Cualquiera que estos sean."
Esta definición es deliveradamente vaga, y muy descriptiva a la vez. Muchas de las discusiones sobre la arquitectura y el diseño giran alrededor de malos entendidos sobre lo que es importante. Lo que resulta importante para los analistas de negocio es distinto de lo que resulta importante para el arquitecto organizacional. Esta definición encapsula muy bien el hecho de que debemos definir los términos dentro del entorno antes de intentar definir otras cosas.
La definición de Fowler también resalta otro aspecto importante de definir algo tan complicado como la arquitectura. "Las cosas importantes" no sólo varian entre distintas personas y grupos; estas diferencias pueden ser mutuamente excluyentes. Por ejemplo, virtualmente todas las arquitecturas SOA hacen una conseción entre flexibilidad y velocidad. El sistema antiguo de cliente/servidor seguro que es más rápido que uno basado en la Web, un motor de portlets, basado en servicios o cualquier otra cosa que lo reemplace. A menos que la aplicación nueva fuera escrita por desarrolladores muy malos, estas capas extra proveen flexibilidad que, a su vez, hacen que los tiempos de respuesta aumenten. Quizás el arquitecto sea quien tenga que decirle a los usuarios "Ah, de paso, esta nueva cosa de SOA que estamos instalando va a ser que el trabajo sea mucho mejor para nosotros, pero su trabajo va a estar un poco peor. Perdón.". Tal vez por esto le pagan más a los arquitectos que a los desarrolladores.
Lo que nos lleva a mi definición favorita de arquitectura:
"La arquitectura es las cosas que son dificiles de cambiar más tarde"
Esta definición es la que mejor encaja dentro de la idea de Arquitectura Evolutiva. Una de las ideas fundamentales de la arquitectura evolutiva es posponer las decisiones lo más posible, lo que nos permite contar con alternativas que la experiencia nos muestre que son superiores.
Antes de terminar con la discusión sobre la arquitectura, veamos el título de "arquitecto" como rol laboral. A los de recursos humanos les molesta que como industria tengamos títulos tan pobremente definidos. Muchas organizaciones quieren promover a sus mejores desarrolladores -los que toman decisiones importantes sobre cosas que son dificiles de cambiar más tarde- pero no existe ningún término más allá del de "arquitecto". Tampoco existe una descripción común del trabajo, así que cada empresa define lo que significa este rol. Algunos arquitectos se parecen al Arquitecto de la segunda parte de Matrix (a los que Fowler cataloga como Architectus Reloadus). Estos arquitectos escribieron código por última vez hace décadas, y ahora toman decisiones importantes para la organización. La única herramienta de desarrollo de software que usan es el Visio.
El rol alternativo de arquitecto es uno que Fowler llama Architectus Oryus. Estos arquitectus contribuyen activamente con código para los proyectos, y trabajan como pares junto a otros desarrolladores en las partes más dificiles. Su responsabilidad incluye interactuar con los interesados de otros proyectos para asegurarse que todos están hablando el mismo idioma, usando las mismas definiciones, y entiendiendo las partes del sistema que necesitan entender. Obviamente, este rol activo es crítico para lograr los objetivos de una arquitectura evolutiva.
Definiendo el diseño
La mayoría de los desarrolladores saben bien lo que es el diseño, asi que no vamos a hablar mucho de esto. El diseño representa las tuercas y tornillos que ensanblan a una pieza de software. Trata de temas muy conocidos como patrones de diseño, refactor, frameworks y otros temas de desarrollo diario. El diseño puede encontrarse en un espectro entre BDUF (Big Design Up Front - Gran Diseño Al Principo) y Cowboy Hacking.
El lado izquierdo de la imagen sugiere que podemos anticipar todos los cientos y miles de temas que van a aparecer cuando desarrollamos software e intenta anticipar las respuestas a estos problemas.
Temas de arquitectura y diseño
Teniendo ya a mano las definiciones de arquitectura y diseño, podemos ver otras áreas de interés. Todos estos temas se intersectan con la arquitectura y el diseño en su nivel fundamental. Primero discutiremos la deuda técnica, luego la complejidad, y por último la generalidad descontrolada.
Capital e interés
Cada desarrollador conoce el concepto de deuda técnica, en donde se compromete el diseño por algún factor externo, como ser la presión por entregar. La deuda técnica es parecido a la deuda de la tarjeta de crédito: no tenemos los fondos suficientes en un momento dado, así que hacemos un préstamo a futuro. De forma similar, el proyecto no tiene tiempo ahora para hacer las cosas bien, así que se codifica una solución justo-a-tiempo y esperamos poder usar algo de tiempo en el futuro para arreglarlo. Desafortunadamente, la mayoría de los líderes y gerentes no comprenden la deuda técnica, y tienen resistencia a reveer el trabajo pasado.
Construir software no es como cavar un pozo. Si cometemos errores al cavar un pozo, a lo sumo terminamos con un pozo desparejo en profundidad. El pozo mal cavado de hoy no nos impide cavar un buen pozo mañana. En cambio, los errores que comentes hoy por apurar un proyecto causan entropía que se agrega a nuestro software. En el libro The Pragmatic Programmer, Andy Hunt y Dave Thomas hablan sobre la entropia del software y porqué tiene un efecto tan negativo. La entropía es una medida de la complejidad, y si agregamos complejidad hoy por hacer una solución justo-a-tiempo, vamos a tener que pagar algún precio durante el resto de la vida del proyecto.
Digamos que queremos agregar algunas características nuevas a un proyecto existente. Estas nuevas características tienen cierta complejidad asociada. Sin embargo, si ya tenemos una deuda técnica, tenemos que trabajar alrededor de estas partes comprometidas del sistema para agregar la nueva funcionalidad. La siguiente figura muestra la diferencia entre el esfuerzo requerido para agregar una nueva característica en un sistema limpio (por ejemplo, uno que tenga poca deuda técnica) versus un sistema típico que tiene una alta deuda técnica:
Podemos pensar en la complejidad inherente como en el capital, y el esfuerzo extra impuesto por compromisos anteriores como en el interés. La complejidad es todo un tema interesante por si mismo.
Complejidad esencial vs. accidental
Los problemas que resolvemos en el software tienen una complejidad inherente, que llamamos complejidad esencial. La complejidad que surge por los "atajos" que tomamos y que generan deuda técnica es diferente. Esta complejidad consiste en todas las formas externas impuestas que hacen que el software se vuelva complejo, que no deberían existir en un mundo perfecto. Esta es la complejidad accidental. Estos términos no son tan exactos, y como el diseño existen dentro de un espectro. Algunos ejemplos va a clarificar esta distinción.
Uno de mis colegas trabajó en un sistema de pagos para un sindicato. Uno de los beneficios que el sindicato aseguraba para algunos de sus miembros era un día libre extra para el inicio de la temporada de caza (de en serio que eran buenos negociadores). Los empleados en cuestión trabajaban en una única fábrica, pero acomodar esta día libre extra era parte del sistema de pagos de la organización entera. Este cambio añadía un montón de complejidad al software, pero se trataba de complejidad esencial porque era parte del problema de negocio a resolver.
Otro ejemplo más lejos en el espectro que surge todo el tiempo: seguridad a nivel de campos en los formularios de carga de datos. Muchas personas de negocio creen que quieren un control super fino sobre cada una de las características de seguridad por cada campo. En realidad, casi siempre odian la implementación porque genera un montón de trabajo extra para los usuarios porque necesitan definir y mantener todos los meta-datos. Las personas de negocio de uno de nuestros proyectos querían esta característica, así que la implementamos en una de las pantallas. Una vez que vieron todo el esfuerzo que requería hacerlo funcionar, decidieron que, cómo sólo accedian a la aplicación desde una oficina cerrada, podían usar una seguridad de más alta granularidad. Este es un excelente ejemplo de una decisión de diseño que emergió una vez que el negocio vio la realidad de lo que pensaba que quería.
Al otro extremo del espectro, hacia la complejidad accidental, se encuentran tareas de configuración y pruebas como en las dos primeras versiones de Enterprise JavaBeans (EJB) y herramientas como BizTalk. Muy pocos proyectos necesitaban todo el trabajo extra que agregaban estas herramientas, y sólo sumaban más complejidad.
Hay tres cosas que tienden a generar complejidad accidental. Ya discutimos la primera: los arreglos justo-a-tiempo que se codifican por presiones de agenda u otras. La segunda es la duplicación, lo que un Programador Pragmático llama una violación al principio DRY (Don't Repet Yourself - No te repitas). La duplicación es una de las fuerzas negativas más insidiosas en el desarrollo de software porque logra infiltrarse en muchos lugares sin que los desarrolladores se den cuenta. El ejemplo obvio incluye el copy-paste, pero hay ejemplos más sofisticados. Por ejemplo, casi todos los proyectos que usan alguna herramienta de mapeo objeto-relacional (como Hibernate o iBatis) tienen duplicación. El esquema de la base de datos, el mapeo XML, y el POJO asociado tienen información diferente pero solapada. La duplicación afecta a los proyectos porque genera resistencia para realizar cambios estructurales o refactors para lograr un mejor código. Si sabemos que necesitamos realizar un cambio en tres lugares, vamos a evitar hacerlo aunque sepamos que a la larga tendremos un mejor código.
El tercer generador de la complejidad accidental es la irreversabilidad. Cualquier decisión que hacemos que no puede revertirse va a crear algún nivel de complejidad accidental. La irreversabilidad afecta tanto a la arquitectura como al diseño, aunque sus efectos son más comunes y graves al nivel de arquitectura. Hay que intentar evitar las decisiones que son imposibles o muy dificiles de revertir. Uno de los mantras más útiles es esperar hasta el último momento responsable para tomar una decisión. Esto no significa postergar las decisiones al infinito, sino lo suficiente. ¿Cuál es el último momento responsable en el que podemos tomar una decisión de arquitectura? Mientras más podamos evitar tomar la decisión, más probabilidad de estar abiertos a otras alternativas. Preguntate: "¿tengo que tomar esta decisión ahora?" y "¿qué puedo hacer para poder posponer la decisión?". Nos podemos sorprender por todas las cosas que podemos posponer hasta más tarde si aplicamos algo de ingenio al proceso de toma de decisiones.
La distinción que hicimos al prinicipio entre la arquitectura a nivel de framework y la arquitectura de aplicación se unen al principio de Último Momento Responsable. La arquitectura de aplicación tiende a ser una arquitectura lógica. Por ejemplo, supongamos que sabemos que queremos una separación de responsabilidades con Model-View-Controller. A menudo, saltamos a una implementación física de esta arquitectura lógica al elegir el framework que satisface algunos de estos requerimientos. Tenemos que ver si podemos posponer esta decisión porque una vez que elegimos la implementación física, esta misma va a restringir otras decisiones que tendremos que considerar. Al posponer la decisión de frameworks lo más posible creamos espacio para tener mejores opciones que están menos contaminadas por la realidad.
Generalización descontrolada
El última tema de interés en la arquitectura y el diseño es la generalización descontrolada. Pareciera que sufrimos de una enfermedad en el mundo Java: sobre-ingenierizar soluciones intentando hacerlas lo más genéricas posibles. El motivo es claro: si construimos muchas capas para poder extender, vamos a poder construir más facil en el futuro. Sin embargo, hay una trampa peligrosa. Como la generalización añade entropía, dañamos nuestra habilidad de evolucionar el diseño de maneras interesantes en las primeras etapas del proyecto. Agregar demasiada flexibilidad hace que cada cambio a la base de código sea más complejo.
Por supuesto que no podemos ignorar la extensibilidad. El movimiento ágil tiene una excelente frase que resume el proceso de decisiones para implementar características: YAGNI (You Ain't Gonna Need It - No lo vas a necesitar). Este es el mantra para evitar sobrediseñar características simples. Implementamos sólo lo que necesitamos, y si más tarde necesitamos algo más, lo agregamos entonces. He visto varios proyectos Java tan complejos en la arquitectura y el diseño haciendo honor a la máxima generalización y la extensibilidad que terminaban fallando. Es hasta irónico, porque lo que truncó la vida de estos proyectos fue planificarlos para que vivieran por siempre. Claro que no es facil aprender a navegar esta delgada línea entre la extensibilidad y la sobre-ingeniería.