Marzo de 2017

Volumen 32, número 3

Patrones. Active Events: un patrón de diseño en lugar de una docena

Por Thomas Hansen | Marzo de 2017

Nota del editor

Cuando me acerqué al editor colaborador sénior de MSDN Magazine James McCaffrey para revisar el borrador inicial de este artículo, se alejó bastante disgustado por algunas de las opiniones e ideas que había propuesto el autor. Cualquier día, eso supondría el fin de un borrador de artículo. Pero como McCaffrey apunta, es del todo común que las nuevas ideas de ingeniería de software se rechacen simplemente por ser nuevas. Aunque McCaffrey afirma seguir indignado por muchas de las declaraciones de este artículo, ambos estamos de acuerdo en que puede dar lugar a muchas ideas sobre paradigmas de diseño y metodología de software. —Michael Desmond

Desde que existe el software, existe software con errores. Según los estudios, un 25 % de todos los proyectos fallará completamente, mientras que un 50 % se puede describir como "cuestionado". Imagine que un fabricante de automóviles tuviera las mismas estadísticas y un 25 % de sus modelos no arrancara o explotara al encender las luces, mientras que un 50 % solo pudiera circular a 40 kilómetros por hora o consumiera 55 litros de gasolina cada 2 kilómetros. Y aún más importante, qué sucedería si los clientes no supieran si el coche funciona hasta después de pagarlo. Esta situación se parece bastante a la que se da actualmente en el desarrollo de software.

Se han propuesto distintas soluciones para mejorar esta situación, pero ninguna de ellas ataca la causa principal: el código. En mi opinión, el motivo por el que los proyectos de software fallan se podría reducir en general al mismo problema: el código espagueti. El motivo por el que el código tiene un estado tan lamentable es porque no tiene manera de adaptarse a cambios. Por eso existen metodologías que han sido recomendadas, tales como la programación extrema y Agile Software Development. No obstante, ninguna de ellas puede ayudar a crear código mejor en realidad. Ser ágil no ayuda a evitar el código espagueti por arte de magia. Para ser ágil, debe poder separar sus preocupaciones en bloques de construcción atómicos.

Aquí presento una alternativa al método ágil, entre otros. El método ágil es, después de todo, un marco de administración y no un patrón de diseño arquitectónico. En su lugar, propongo una metodología de diseño que le permita ser ágil más fácilmente porque puede anticiparse a los cambios. Durante los últimos años he desarrollado un patrón de diseño que denomino "Active Events". Este patrón de diseño permite crear pequeños componentes reutilizables que se pueden extender, modificar, reutilizar e intercambiar con otros elementos fácilmente. Permite pensar de manera diferente en el software y así "orquestar" el código, casi como si los componentes individuales fueran piezas de Lego. Podría pensar que esto se parece bastante a la escritura de código mediante un paradigma de programación orientada a objetos (OOP). No obstante, Active Events es muy diferente al paradigma OOP tradicional.

Consultar el propio código

Hágame un favor: abra la última solución que compiló en Visual Studio y consulte el gráfico de dependencias. ¿Cuántos proyectos y clases se pueden extraer de su solución? ¿Cuántas clases se podrían reutilizar en proyectos futuros? ¿Cuántas podría reemplazar por otras clases sin interrumpir el software?

Mi opinión es que sus dependencias de clases y del proyecto son iguales que en la mayoría de los demás proyectos. Es altamente probable que el gráfico de dependencias de su proyecto parezca espagueti. Siento ser tan honesto, pero es lo que me dice mi experiencia. Pero no se preocupe, no está solo. Aunque no tengo datos firmes, me atrevería a decir que un 98 % del código de todo el mundo sufre el mismo problema. Además, no le obligaría a enfrentarse a este problema si no pudiera ofrecerle una solución.

Si pudiera de algún modo desenmarañar esas dependencias y crear componentes reutilizables, de manera que ninguno de los elementos individuales del código hiciera referencia a otros elementos, su experiencia de programación mejoraría sustancialmente. Si sus proyectos y clases no estuvieran tan enredados, podría intercambiar más fácilmente componentes individuales. Si sus clases estuvieran desenredadas, podría reutilizarlas en proyectos futuros. Si sus proyectos tuvieran las mínimas dependencias, tendría una variedad de componentes con más probabilidades de reutilizarse, en lugar de un monstruo monolítico. Active Events permite eliminar las dependencias en sus clases y proyectos. Si hablamos de enredos en el software, cero es el único número aceptable.

¿Cómo funciona Active Events?

En realidad, el patrón de diseño Active Events es increíblemente fácil de entender. En lugar de crear interfaces para separar la implementación y el consumidor de un fragmento de lógica, solo tiene que marcar sus métodos con un atributo. Aquí se muestra un ejemplo:

[ActiveEvent (Name = “foo.bar”)]
protected static void foo_bar(ApplicationContext context, ActiveEventArgs e)
{
  var input = e.Args[“input”].Value as string;
  e.Args.Add(“output”, “Hello there ” + var);
}

Esta sintaxis se toma de Phosphorus Five, una implementación de código abierto de Active Events que yo creé. Consulte "Software prácticamente inexistente" para obtener más información.

Invocar un método que esté definido mediante el paradigma Active Events es igual de simple. En lugar de invocar el método directamente, puede invocarlo indirectamente a través de su nombre de Active Events:

/* ... some method in another assembly ... */
var args = new Node ();
args.Add ("input", "John Doe");
/* context is your ApplicationContext instance, which keeps
   track of your Active Events */
var result = context.Raise ("foo.bar", args)["output"].Value as string;

La invocación de Raise invoca el evento activo foo.bar. Tenga en cuenta que no existe ninguna dependencia entre el punto donde invoca el evento activo y su implementación. Además, no comparten clases comunes. Solo los datos se pasan dentro y fuera de los eventos. Observe también que todos los eventos activos tienen exactamente la misma signatura. Por tanto, las clases, las interfaces y los métodos se quedan un poco entre bastidores y dejan de ser el foco del código fuente.

La carga de los componentes de Active Events es completamente dinámica y se puede realizar bien por medio de una lista de ensamblados a los que se hace referencia en el archivo de configuración de la aplicación, o bien mediante la carga automática de todas las DLL de una carpeta y su registro como controladores de Active Events. Internamente, el "kernel" de Active Events cruzará todos los tipos de la DLL durante la carga y el almacenamiento de una referencia de MethodInfo a sus métodos Active Events en un diccionario de cadenas o MethodInfo. Este diccionario se usará más adelante durante la invocación del evento. Si desea evaluar un evento activo o asociarle un estado, puede agregar de forma dinámica una instancia de una clase como un agente de escucha de Active Events y usar un método de instancia en lugar de un método estático como su receptor de Active Events, que es lo que se hace, por ejemplo, en el proyecto plugins/p5.web en Phosphorus Five.

De este modo, se crea un entorno en el que los distintos componentes son bloques de construcción simples que se invocan entre sí indirectamente, lo que permite intercambiar fácilmente cualquier ensamblado de Active Events único con otra implementación. Cambiar completamente la aplicación resultante es tan fácil como cambiar una opción de configuración o reemplazar una DLL por otra. Asimismo, es tan fácil como cambiar la cadena "foo.bar" por "foo.bar-2", si lo desea. Al mismo tiempo, cada ensamblado de su solución puede seguir invocando métodos en sus otros ensamblados, sin compartir tanto como una estructura de datos antiguos sin formato (POD) con la otra parte. Básicamente, tiene una "capa de abstracción adicional" constante a su disposición. Dicho de otro modo, sus componentes se han atomizado.

Vamos a compararlo con lo que tendría que hacer en un paradigma OOP tradicional. En primer lugar, suponiendo que usa un enfoque de diseño por interfaz, tendría una interfaz y, por lo menos, una implementación de esta. En función del escenario, podría usar un esquema de fábrica abstracta para crear objetos. El resultado son dos clases y una interfaz, lo que podría suponer fácilmente 30 o más líneas de código en total. Además, si quisiera crear un complemento real a partir de su implementación, acabaría con un proyecto de elementos de consumidor (que consume su complemento) y un proyecto de elementos de implementación (el proyecto con su clase que implementa la interfaz). Si eligiera un enfoque completamente modular, podría usar un tercer proyecto que incluyera su interfaz y, posiblemente, su fábrica abstracta. Luego, al observar el resultado, se daría cuenta de la existencia de, como mínimo, tres referencias entre componentes entre los tres proyectos. Además, no puede agregar ni cambiar los elementos de entrada ni de salida de la interfaz sin modificar obligatoriamente los tres proyectos. Compare este enfoque con la solución de una línea, sin ninguna referencia, que proporciona el ejemplo de Active Events, que ni tan solo se preocupa por los argumentos de entrada o salida que le proporciona. El enfoque tradicional podría tener el doble de líneas de código, o incluso más, lo que produciría, según mi experiencia, un gráfico de dependencias mucho más complejo, un resultado mucho más rígido y un código mucho más complejo.

OOP no es lo mismo que OO

Me encanta OOP. En un atractivo paradigma que permite encapsular lógica, métodos y datos de código fácilmente. Sin embargo, uno puede objetar que OOP no resuelve completamente todos los problemas de codificación. En algunas situaciones, al usar OOP, debe definir varias clases, que a menudo están demasiado acopladas y crean dependencias.

En OOP, debe conocer la signatura de un método, debe tener acceso a una interfaz y debe saber cómo crear una instancia de esta interfaz. Además, a menudo se deben compartir tipos entre el consumidor y la implementación de la interfaz. Esto crea dependencias entre los bloques de construcción. Al OOP, no es raro tener que crear varias clases para crear un simple complemento "Hola mundo". Si cambia una interfaz de OOP, salvo que la haya diseñada con mucho esmero, es posible que deba recompilar varios ensamblados. Podría acabar con gran parte del código como interfaces abstractas y código reutilizable, o bien teniendo que aceptar que un cambio en una interfaz podría tener un efecto dominó y requerir una cantidad considerable de cambios de código adicionales. Siempre que algo sea excesivamente complejo o requiera una gran cantidad de trabajo repetitivo y aburrido, su instinto debería indicarle que no es correcto.

Pregúntese esto: "Si OOP ofrece la posibilidad de crear software orientado a objetos (OO), ¿por qué es tan difícil usarlo correctamente?" En mi opinión, OOP no es el equivalente de OO. Si lo fuera, no necesitaría conocer tantos patrones de diseño para sacarle el máximo partido.

Creo que OOP, como se usa actualmente, presenta varias limitaciones fundamentales y que todos esos patrones de diseño que debe implementar para usar OOP adecuadamente son el síntoma de un problema mucho más básico de su arquitectura: la incapacidad de entregar software OO. Active Events corrige estos problemas, no con la implementación de fábricas abstractas mejores ni nuevas construcciones de clases, sino con la modificación de los numerosos métodos y funciones que se invocan, lo que elimina el enfoque OOP de la escena. Para crear software OO, no se necesitan nuevas maneras de crear instancias de clases abstractas e interfaces, sino un mecanismo diferente para invocar la funcionalidad, así como la posibilidad de tener una signatura común para cada método de interfaz que se invoque.

Software prácticamente inexistente

Para ver un ejemplo de lo lejos que puede llevar Active Events, consulte Phosphorus Five en github.com/polterguy/phosphorusfive. Phosphorus Five es un gran proyecto de software, pero la implementación de Active Events solo contiene unas 1000 líneas de código. La implementación de Active Events se puede encontrar en core/p5.core.

En este proyecto, creé un nuevo lenguaje de programación, Hyperlambda, para la creación de enlaces de Active Events. Esto me permite cambiar la implementación de mis instrucciones for-each y while si lo deseo. También puedo escalar horizontalmente mis instrucciones else para que se ejecuten en otro servidor. Además, puedo extender fácilmente el lenguaje de programación a fin de crear mis propias palabras clave específicas del dominio, para hacer todo aquello que sea necesario para resolver los problemas de mi dominio.

También creé un formato de archivo que me permite declarar una estructura de nodos de forma dinámica, como un fragmento de texto, y almacenarla en mi disco o base de datos. Asimismo, creé mi propia sintaxis de las expresiones, lo que me permite hacer referencia a cualquier nodo en cualquier parte de mi árbol. El resultado es lo que denomino lenguaje que no es de programación, pero 100 % Turing completo. Mi lenguaje que no es de programación no se interpreta de forma dinámica ni se compila de forma estática, y, en ocasiones, es capaz de hacer con cinco líneas de código lo que otros lenguajes de programación hacen con cientos de ellas.

Con Active Events, junto con Hyperlambda y una biblioteca de AJAX administrada (que contiene un solo widget de AJAX), me las apañé para crear algo que no sé ni cómo explicar, sin introducir términos como "sistema operativo web". Existen aproximadamente 30 proyectos en Phosphorus Five y no existen referencias entre los distintos complementos. Todos los proyectos hacen referencia simplemente a p5.core, que es la implementación de Active Events, así como a p5.exp, el motor de expresiones. Eso es todo. El proyecto del sitio web principal, p5.website en la carpeta principal, solo contiene un widget contenedor simple y prácticamente ninguna lógica. Todos los complementos se cargan de forma dinámica en mi archivo Global.asax durante el inicio de la aplicación. Aún así, todos los proyectos invocan de forma dinámica la funcionalidad que incluyen otros proyectos. Ninguna referencia, ninguna dependencia, ningún problema.

Regreso a lo básico

La solución siempre está en el problema. Algunos de los mismos problemas por los que se inventó OOP (funciones y datos globales) son la solución de los problemas que OOP generó involuntariamente. Si echa un vistazo al patrón de diseño Active Events, lo primero que observará es que, en cierta medida, vuelve a lo básico, aunque con funciones globales en lugar de métodos y clases. No obstante, dado que no es necesario conocer signaturas ni tipos, ni compartirlos entre el consumidor y la implementación de un evento activo, tiene un entorno de caja negra que no tendría con OOP. Esto permite, por ejemplo, el intercambio sencillo de SaveToDatabase con InvokeWebService o SaveToFile. Sin interfaces, sin tipos, sin estructuras POD, sin clases y solo con una signatura comúnmente compartida. Solo datos antiguos simples y correctos. Datos dentro, datos fuera.

El polimorfismo es algo tan simple como cambiar una cadena. A continuación se muestra un ejemplo de implementación de polimorfismo con Active Events:

string myEvent = "some-active-event";
if (usePolymorphism) {
  myEvent = "some-other-active-event";
}
context.Raise (myEvent);

Admito que esta construcción de polimorfismo debe parecer ridículamente ingenua y simple para un arquitecto experto. No obstante, su simplicidad es el motivo por el cual funciona. Con Active Events, puede capturar el nombre de un método o una función de una base de datos, de un archivo de configuración o incluso de un usuario que proporcione su nombre a través de un cuadro de texto del formulario. Puede considerarlo una variación de polimorfismo que no necesita clases explícitas. Es polimorfismo sin tipos. Este polimorfismo se determina de forma dinámica durante el tiempo de ejecución. Al eliminar las ideas tradicionales sobre el polimorfismo de la escena y refactorizar la verdadera esencia del polimorfismo, obtiene polimorfismo que funciona realmente: encapsulación y polimorfismo, sin clases, tipos, interfaces ni patrones de diseño. Encadenar los eventos activos pasa a ser tan fácil como combinar átomos en moléculas. Eso es software ágil.

Node.cs, el último tipo de gráfico que necesitará

Con Active Events, solo envía datos dentro y fuera de sus eventos activos. Esto es lo que le permite acoplar sus componentes de manera flexible. Para ello, necesita una clase de datos, el único tipo de gráfico que necesitará cuando use un paradigma Active Events. Su clase debe poder encapsular todos los campos y propiedades posibles de todas las clases posibles. En Phosphorus Five, esta clase se denomina Node.cs y es simplemente un objeto de gráfico, con un diseño de clave, valor o elemento secundario.

La clave para implementar correctamente Active Events es que la clase Node sea el único argumento que sus eventos activos puedan tomar como entrada y devolver como salida. Lo que sucede es que casi todas las clases se pueden reducir de manera eficaz a un objeto POD de clave, valor o gráfico secundario. Esto es lo que, combinado con la carga dinámica de los ensamblados de Active Events, reduce considerablemente el número de dependencias entre los proyectos.

La implementación de Node.cs debe poder contener una clave o un nombre, un valor (que puede ser cualquier objeto) y una colección de "elementos secundarios" de Nodes. Si la clase Node cumple estas restricciones, puede transformar fácilmente casi todos los objetos posibles en una instancia de Node. Para todos aquellos que estén familiarizados con JSON o XML, las similitudes pueden ser obvias llegado este punto. A continuación se incluye pseudocódigo simplificado que muestra la estructura de la clase Node:

class Node
{
  public string Name;
  public object Value;
  public List<Node> Children;
}

Internamente, dentro de sus componentes, puede usar la cantidad de OOP que desee. No obstante, si un componente debe invocar lógica en otro componente, todos los datos de entrada deben transformarse de algún modo en una instancia de Node. Cuando se devuelve información de Active Events, se debe llevar a cabo el mismo proceso. No obstante, dentro de sus componentes, es libre de usar las clases, las interfaces, las fábricas abstractas, los componentes de fachada, los objetos singleton y los patrones de diseño que desee. Externamente, sin embargo, solo la instancia de Node y Active Events hacen de puente entre los componentes. Piense en Active Events como el protocolo y en Nodes como sus datos, si ello le ayuda a hacerse una imagen mental de la relación.

Resumen

Aunque su objetivo es reemplazar gran parte de los demás patrones de diseño existentes actualmente, Active Events no es una bala de plata. Entre otras cosas, esta tecnología viene con alguna sobrecarga. Por ejemplo, en lugar de invocar métodos directamente, se lleva a cabo una consulta de diccionario. Además, los métodos usan alguna reflexión. Estas invocaciones de métodos indirectos son, probablemente, órdenes de magnitudes más costosas que la invocación de métodos virtuales tradicionales. Además, debe transformar los objetos en nodos y datos siempre que interactúe con otros componentes.

No obstante, los eventos activos no están destinados a reemplazar todo lo que haga. La idea es proporcionar una mejor interacción entre los componentes, y es por eso que la sobrecarga de rendimiento no es una de las principales preocupaciones. Que se tarden 5 ciclos de la CPU o 500 ciclos de la CPU en invocar el método SaveToDatabase es totalmente irrelevante si la implementación real tarda 5 000 000 de ciclos de la CPU. Donald Knuth dijo en una ocasión: "La optimización prematura es la raíz de todos los males".

Cada vez que piense en escribir una interfaz, debe preguntarse si sería mejor crear un evento activo en su lugar. Una vez que se gana algo de experiencia con Active Events, la respuesta a esa pregunta suele ser: "probablemente, sí". Con Active Events, el énfasis de las interfaces y las clases abstractas se ha reducido en gran parte.

Sé que suena como una declaración irracionalmente atrevida, pero le recomiendo que examine el proyecto de Phosphorus Five explicado en "Software prácticamente inexistente". En Hyperlambda, el "lenguaje" que creé para Active Events, un objeto puede ser un archivo, una carpeta, una devolución de llamada lambda, un subelemento de un árbol de nodos de gráfico, un fragmento de texto tomado de una base de datos o un fragmento de datos enviado a través de HTTP. Todos los objetos se pueden ejecutar, como su fueran árboles de ejecución comprensibles para una máquina. En teoría, en Hyperlambda podría ejecutar el número 42.

Inicialmente, pensé en Active Events hace más de siete años y, de manera intuitiva, sentí que su belleza era intrínseca. El problema es que se enfrenta a 60 años de sabiduría de programación convencional. Active Events se convierte en el punto donde incluso nuestros procedimientos recomendados se deben refactorizar. Me costó siete años desaprender lo que había aprendido y confiar en mi intuición. El mayor problema de Active Events no está en realidad en la implementación, sino en su cabeza. El hecho de que he vuelto a crear completamente Phosphorus Five cinco veces lo prueba.

En el primer borrador de este artículo, creé decenas de analogías. Al acceder a sus conocimientos preexistentes sobre otros temas, como física y biología, intenté convencerle de la superioridad de Active Events. En mi segundo borrador, intenté provocarle. La idea era animarle a demostrarme lo contrario, de manera que buscara defectos en mis instrucciones, sin llegar a encontrar nada, por supuesto. Sin embargo, en mi tercer y último borrador, decidí hablarle simplemente de Active Events. Si puede pensar en continuar, Active Events será el último patrón de diseño que tendrá que aprender.


Thomas Hansen crea software desde que tenía 8 años, cuando empezó a escribir código con un equipo Oric-1 en 1982. Ocasionalmente, crea código que hace más bien que mal. Entre sus pasiones se incluyen la arquitectura del software y las metodologías Web, AJAX y Agile.

Gracias al siguiente experto técnico de Microsoft por revisar este artículo: James McCaffrey