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

Un pensamiento en “Antipatrones en arquitecturas de varios niveles

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