Antipatrones en arquitecturas de varios niveles

Cuando se trabaja con arquitecturas de varios niveles, hay ciertas consideraciones a tener en cuenta que tienen que ver con la consistencia de los datos, el uso de objetos distribuidos y la concurrencia, que de no estudiar apropiadamente pueden dar más de un quebradero de cabeza durante el mantenimiento de la aplicación.

Lo primero a tener en cuenta es la diferencia entre capa y nivel. Habitualmente una aplicación bien diseñada tendrá varias capas con dependencias cuidadosamente definidas. Dichas capas pueden estar ubicadas en un sólo nivel o estar diseminadas en varios niveles. Digamos que una capa es un concepto organizacional de la aplicación, mientras que un nivel denota una separación de componentes que pueden estar (o de hecho están) separados en distintos soportes físicos.

Toda aplicación que interactúa con una base de datos tiene más de una capa, a no ser que la base de datos esté embebida en el propio proceso de la aplicación. Otro ejemplo de arquitectura multinivel sería una aplicación web que involucra a una base de datos, un servidor y un navegador.

Sin embargo, no nos referiremos a estos ejemplos tan simples como arquitecturas multinivel. El primero de los ejemplos es un caso muy simple y común, y no se suele considerar a efectos prácticos un caso multinivel por estar presente casi siempre. El segundo caso plantea una situación también muy común, además a todos los efectos el servidor web y el cliente pueden ser entendidos como un nivel de cliente.

¿A qué casos nos referimos, pues? A casos donde participan servicios web u otro tipo de servicios que se proveen entre distintos módulos. Típicamente, una arquitectura multinivel constará de, como mínimo, un nivel de base de datos, un nivel intermedio que expone un servicio y un nivel de cliente.

Planteados estos conceptos, el primer consejo sobre la distribución de objetos en arquitecturas multinivel viene de la mano de Martin Fowler en su libro “Patterns of Enterprise Application Architecture” (Addison-Wesley, 2002): ¡No distribuyas tus objetos!. Bien; si tomamos esta afirmación como cierta cerramos el artículo y nos vamos a tomar un café.

No, no nos vamos a tomar un café, y no es que vengamos a afirmar que Martin Fowler está equivocado, sino que simplemente hay situaciones donde esta ley debe quedar a un lado. Sí es cierto que es muy recomendable considerar y controlar los objetos que se distribuyen entre los distintos niveles y reducirlos en cantidad y tipología en la medida de lo posible, pero reducirlos a cero lo dejaremos como una utopía que no es viable en muchas situaciones. Vamos, pues, con los antipatrones:

Fuerte acoplamiento

Todos sabemos que el acoplamiento entre subsistemas, paquetes y en general módulos de nuestros sistemas debe ser el mínimo posible y lo llevamos a la práctica, ¿verdad? De acuerdo. Sin embargo, aunque seamos tan buenos arquitectos software hemos de reconocer que mantener un acoplamiento bajo conduce a soluciones complejas y en ocasiones poco eficientes. ¿Por qué introducir una dependencia mediante una interfaz cuando podemos simplemente crear una instancia del objeto de la clase directamente? Sin embargo, los beneficios de un acoplamiento bajo vienen a la larga, cuando nos movemos en los términos de mantenibilidad y extensibilidad.

Cuando trasladamos el problema del acoplamiento a sistemas multinivel, el problema se hace más grande. Imaginemos que tenemos que cambiar un nivel, pongamos el nivel medio. ¿Qué ocurre si el acoplamiento de nuestro sistema afecta al nivel de cliente? Habremos de cambiar todos los clientes… El truco aquí está en identificar grupos de módulos que cambian con una frecuencia similar; es imposible evitar que haya dependencia entre niveles, pero si agrupamos correctamente los niveles, podremos mantener una afectación entre niveles mínima.

Asumir requisitos estáticos

Bueno, ya conocemos que los requisitos son cambiantes, pero tampoco podemos vivir con fobia a los requisitos. ¿Qué es susceptible de cambiar?

Según el autor, hay dos aspectos fundamentales que suelen hacerse mal cuando se considera la estabilidad de los requisitos: pensar que el cliente es fiable e inamovible por un lado; por otro, hacer que el servicio que provee el nivel intermedio asuma que el cliente será implementado utilizando una determinada tecnología.

El primer problema se hace patente cuando, por ejemplo, se validan datos únicamente en el cliente, asumiendo el nivel intermedio que lo que le llega es pescado fresco del día, procesándolo y enviándolo a la base de datos. Esta consideración acarrea más problemas de los que se pueda pensar, más aún cuando el servicio que ofrece el nivel intermedio puede ser utilizado por más clientes de los cuales no teníamos constancia previamente.

En cuanto al segundo caso, sabemos que la tecnología siempre cambia. Protege tu arquitectura para que si el día de mañana el nivel de cliente deja de ser un navegador web para ser una aplicación para teléfono móvil o una aplicación Silverlight no trastoque todo tu sistema.

Concurrencia no manejada

Un error en la concurrencia es el típico problema que sólo se detecta cuando el sistema ya está en producción. De los divertidos. Y si el problema se manifiesta como un fallo con luces de colores y pantallazos con mensajes de error, vamos listos. Lo mejor es cuando estos fallos producen corrupción en los datos y el problema perdura en el tiempo hasta que finalmente es detectado.

La gestión de la concurrencia suele hacerse de forma optimista: en el supuesto caso de que varios clientes tuvieran que acceder a la base de datos simultáneamente, el número de veces que tienen que modificar la misma entidad surgiendo conflictos de consistencia es muy pequeño.

El Entity Framework da un soporte de concurrencia bastante optimista; mediante el seguimiento del valor original del valor a modificar cuando dicho valor es consultado y buscando conflictos antes de realizar un update. El problema en una arquitectura multinivel estriba en que este proceso es transparente siempre y cuando se utilice una misma instancia de ObjectContext desde el cual se consulta el valor hasta que éste es almacenado con SaveChanges. Si se serializan entidades de un nivel a otro lo recomendable es pasar el contexto entre niveles mientras dure la llamada al servicio que provee el nivel intermedio. De esto se desprende que debería crearse un nuevo contexto para cada llamada al servicio.

El problema viene porque mientras se trabaja en este modo “en-desconexión” (desconectado en el sentido de que las entidades están desconectadas de su contexto tras la consulta, enviadas a otra capa y después reconectadas cuando hay que modificar los datos mediante un update) ocurre lo siguiente:

1.- Se consulta la entidad y se serializa al nivel de cliente.

2.- El cliente recibe la entidad, realiza los cambios que tenga que hacer y envía de vuelta la versión modificada a la capa intermedia.

3.- Ya que nadie mantiene una traza de la concurrencia o de las propiedades que han sido modificadas, el servicio vuelve a consultar a la base de datos para conocer el estado actual de la entidad, creando un nuevo contexto para dicha consulta.

Después se comparan los valores de la entidad guardada y la que llega del cliente.

4.- El servicio llama a SaveChanges, que verifica si ha de hacer persistentes los cambios o no.

De este flujo surgen dos problemas fundamentales:

El primero es que cada vez que se modifica una entidad, hay que leer de la base de datos dos veces su valor, con el coste que eso supone.

El segundo y más importante es que el valor “original” utilizado por el Entity Framework para verificar si la entidad ha sido modificada en la base de datos viene de una segunda consulta en lugar de la primera consulta. ¿Qué ocurre si entre los pasos 1.- y 3.- alguien modifica el valor?

El sistema no detectará esta situación, detectará un cambio en el valor leído y tratará de almacenar el nuevo valor.

La solución para ambos problemas pasa por realizar una copia del valor leído en el cliente y enviarlo de vuelta junto con el valor modificado al nivel intermedio. Cuando dicho nivel recibe ambos valores, compara y decide.

Servicios con estado

Del antipatrón anterior se deduce que una buena idea para solucionar algunos problemas de concurrencia es pasar el contexto entre las distintas capas.

Lo que ocurre es que esta solución puede convertirse en una pesadilla rápidamente cuando se traslada a la práctica. Colisiones, manejo de múltiples contextos… Cada vez que se realiza una llamada al servicio, el nivel intermedio debe procesar la llamada, solicitar los recursos necesarios, y después liberar dichos recursos. Si podemos hacer que dicha información se mantenga en el cliente en lugar de en el nivel intermedio, de modo que no haya problemas con el manejo de múltiples sesiones (de múltiples llamadas) el cliente se encarga de liberar los recursos y gestionar sus propias llamadas, y además no se consumen recursos del servidor para dar servicio a un cliente.

Dos capas que pretenden ser tres

Simplificar el proceso: Si creamos un ObjectContext en el nivel de cliente, ejecutamos una consulta para cargar las entidades en dicho contexto, modificamos dichas entidades y después las guardamos mediante SaveChanges (lo que provocará un update), ¿para qué tener el nivel intermedio?

Ten siempre en cuenta la ley de Fowler. La única razón para tener una arquitectura multinivel es porque realmente es necesario: bien porque necesitas más seguridad o la posibilidad de escalar en múltiples servidores, o lo que sea. Si no, no utilices arquitecturas multinivel.

Puedes estar perdiendo todo el tiempo que quieras modelando una arquitectura meticulosamente diseñada, con múltiples niveles, totalmente independientes y todo lo que se te ocurra. Pierdes todo el tiempo creando una infraestructura superior y te das cuenta de que no has dedicado un segundo en trasladar un valor realmente útil a los usuarios.

Céntrate en tus objetivos y considera si es necesario una arquitectura multinivel. En ocasiones una arquitectura de dos niveles es suficiente.


Referencias
“Anti-Patterns To Avoid In N-Tier Applications”
Danny Simmons
MSDN Magazine
Junio 2009

Solución al problema de los filósofos comensales con la biblioteca Asynchronous Agents

La librería Asynchronous Agents de Visual C++ 2010 permite dar solución al problema de los filósofos comensales sin utilizar semáforos, monitores, bloqueos ni nada de esto directamente, sino simplemente utilizando un sistema de paso de mensajes síncronos y asíncronos. Esto facilita mucho la implementación puesto que podemos centrarnos en las abstracciones del problema y despreocuparnos bastante de los problemas de concurrencia y bloqueo mutuo.

Los filósofos comensales

Empecemos repasando el problema de los filósofos comensales. Recordemos que este problema planteado por Djikstra planteaba una mesa a la cual se sienta un determinado número de filósofos que se pasan el tiempo pensando o comiendo, de forma alternativa, de un gran bol de arroz que se encuentra situado en el centro de la mesa. Para ello, los filósofos necesitan tomar un par de palillos, uno que se encuentra a su izquierda y otro a su derecha. El problema es que no hay palillos para todos, de manera que a cada lado de cada filósofo se encuentra solamente un palillo. Hasta que un filósofo no tiene los dos palillos no puede empezar a comer. Cuando un filósofo desea pensar, deja cada palillo a cada lado, y deja de comer. El problema del bloqueo mutuo en este planteamiento viene si todos los filósofos cogen uno de los palillos, puesto que jamás podrán coger el otro palillo, que está ocupado por otro filósofo, produciéndose una espera indefinida. ¿Cómo coordinamos a los filósofos para que puedan comer y pensar sin problema?

Cómo resolver el problema con Asynchronous Agents

En este problema, los filósofos serían hilos independientes que tratan de acceder a un recurso compartido, representado por los palillos. Cada filósofo solamente tiene constancia de los dos palillos que hay a ambos lados de su plato, siendo el resto invisibles para él.

El problema así puede ser resuelto con cuatro entidades:

  • Una clase Chopstick, que representa a cada uno de los palillos situados en la mesa.
  • Una clase ChopstickProvider, que se encarga de mantener los palillos en la mesa y asignárselos a los filósofos.
  • Una clase Philosopher que es responsable de pensar y comer y es consciente solamente de dos ChopstickProviders.
  • Una clase Table que representa a la mesa y que contiene un conjunto de Philosopher, otro de Chopstick y otro de ChopstickProvider.

Es posible que a la hora de la implementación nos demos cuenta de que alguna de estas clases puede ser colapsada en una estructura de datos o algo más sencillo, dependiendo de la funcionalidad que pueda proveer.

Evitaremos de aquí en adelante todo detalle de implementación, limitándonos simplemente a decir cómo podemos utilizar la biblioteca Asynchronous Agents para resolver el problema de los filósofos comensales.

Envío de mensajes

Los ChopstickProviders pueden ser implementados utilizando colas de llamadas:

  • Concurrency::send para enviar un mensaje a una cola o buffer de mensajes; se espera un ACK del envío.
  • Concurrency::receive para recibir un mensaje; se bloquea hasta que recibe un mensaje.
  • Concurrency::asend, como send pero asíncrono.
  • Concurrency::try_receive, como receive pero no se bloquea hasta que haya un mensaje en la cola.

Los filósofos

Pueden ser implementados como agentes, derivar de una clase abstracta llamada Concurrency::agent, e implementar el método run(). Durante el ciclo de Comer() y Pensar() se puede realizar una espera aleatoria o cualquier otra cosa.

La sincronización

Viene provista de la llamada Concurrency::join, que puede soportar distintos tipos de fuentes de mensajes o solamente el mismo tipo de fuente; además puede realizar un uso glotón de los mensajes que llegan (consumiéndolos a medida que llegan) o no (esperando a que todos los mensajes requeridos lleguen). Es fácil ver que para esta solución necesitamos utilizar la alternativa de un simple tipo de fuente y no glotona, precisamente para evitar el problema del bloqueo mutuo.

Los detalles de la implementación de esta solución se encuentran en el artículo que se referencia debajo.


Referencias:

“Solving the Dining Philosophers Problem With Asynchronous Agents”
Rick Molloy
MSDN Magazine
June 2009

Moby Dick

Por fin terminé esta novela de Herman Melville que me ha llevado meses terminar.
Lamentablemente tengo que decir que la excusa de semejante tardanza es lo tremendamente aburrida que me ha parecido. Y es que a veces los clásicos a veces son un tostón, por muy trascendentales que hayan sido después en la historia de la literatura. De este libro lo único que destaca, bajo mi ignorante punto de vista, es su enorme influencia en obras posteriores, destacando sobre todas ellas la de Ernest Hemingway, El viejo y el mar, una obrita que no va de policías y ladrones, códigos y da Vincis, sino de sentimientos, anhelos y metas que uno se impone en la vida; cómo se lucha por llegar a dichas metas, y cómo a veces todo se acaba yendo a la mierda.

Comencé precisamente a leer esta obra por el enorme poder que ha tenido sobre obras posteriores, el omnipresente personaje de Ahab (¿quizá un juego de palabras con el rey Acab, uno de los muchos pérfidos reyes hebreos que surgieron tras la época dorada del rey David y su hijo, Salomón?) como el del viejo chiflado, pero un chiflado peligroso, pues arrastra al resto de la tripulación del Pequod a su trágico destino, del cual un marinero poco más que grumete, Ismael, reflejo del propio autor, se salva. La intención es buena, pero el libro se convierte en un tostón de casi quinientas páginas y cientoveinticinco capítulos en los cuales se explica las diferencias entre la ballena y el cachalote, su caza, su fisonomía,  su comportamiento y hasta la forma que tiene el chorro de agua que emite de su válvula dorsal; encuentros con otros barcos balleneros, tradiciones balleneras en otros países y demás eventos sin la mayor relevancia.

Si algo positivo de ello saco es que he aprendido cómo se cazaban ballenas en la época; probablemente no me vaya nunca a sacar de un apuro, y tampoco te importe un comino, amable lector, pero te vas a joder y te lo voy a contar: resulta que no se lanzaban arponazos desde el propio barco ballenero, como pensaba yo, sino que cuando se avistaba una ballena se bajaban unos botes empujados por un grupo de remeros, liderados por un piloto que dirigía el remo de timón y exhortaba al resto, y uno o dos arponeros. Los arpones, por tanto, se lanzaban desde los propios botes, e iban atados con una cuerda cuyo otro extremo iba a parar al propio bote, de manera que una vez el arpón alcanzaba al animal, éste remolcaba al bote durante unos cuantos cientos de metros hasta que finalmente moría. Tras su muerte el animal flotaba, y la tripulación esperaba al barco ballenero para poder remolcar e izar el enorme bicho al barco, como buenamente podían, para evitar lo que de otro modo sería un festín de tiburones.
Como se puede imaginar, cada fase de la caza de la ballena era tremendamente complicada y arriesgada, desde remar a más no poder para alcanzar al titánido marino, sin olvidar el peligro de remar entre tiburones que solían circundar por tales lares, mordisqueando los remos en ocasiones, como ofrecer una buena posición desde el bote para que el arponero pueda atacar a su presa, sin olvidar los posibles coletazos del animal que en innumerables ocasiones acababa con más de un marinero nadando entre tiburones o con el bote partido en dos. Después había que descuartizarlo en cubierta, con la bestia levantada por una especie de grúa, mientras un marinero experimentado desollaba al animal sujeto en vilo por una cuerda enrollada alrededor de su cuerpo que era sujetada por otro marinero en cubierta, el cual, a su vez tenía la cuerda enrollada a su cintura. Si la polea fallaba, los dos acababan en el mar, desdedonde tendrían seguramente el placer de nadar entre tiburones, siempre presentes ante la sangre y la carroña. De este modo se aseguraban que cada uno hiciera su cometido con la mayor presteza posible y no se le ocurriera hacer el gracioso.

Sin embargo, a pesar de que el hilo argumental es lento a más no poder, hay otra cosa a destacar del libro, aparte de lo que he aprendido sobre el mundo marino: los tres últimos capítulos: “La caza, primer día“, “La caza, segundo día” y “La caza, tercer día“, donde por fin la acción se desencadena cual vehículo de carreras sin frenos, sucediéndose todo el meollo de la novela vertiginosamente, magistralmente, en un cúmulo de descripciones que nos transporta mágicamente al lugar, a sentir lo que los personajes sienten, arrepentimientos, miedos, osadías, valentías, respeto y muerte. Para un lector que desee conocer la obra, realmente no tiene más que leer estos tres últimos capítulos. No conocerá a los personajes, pero creo que no será un inconveniente demasiado importante. Merece la pena realmente leer estos tres últimos capítulos porque vienen a significar, realmente, por qué Moby Dick está entre los grandes clásicos.

Por cierto, una nota para la gente de Edicomunicación S.A.: revisen mínimamente sus publicaciones, hablen con la traductora responsable o con quien sea; es rotundamente inaceptable que un libro de menos de 500 páginas tenga más de 3000 erratas, algunas sin sentido en absoluto. Lamentable.