viernes, 16 de noviembre de 2012

El pseudo-dilema del prisionero, o cómo echarle toda la culpa a los servicios web con C#

El código de ejemplo se encuentra aquí. La solución .NET (Visual Studio 2012) contiene cuatro proyectos:
  • Nimio.Entidades - Definición de las entidades del «negocio».
  • Nimio.Frontal.Servicio.Pedidos - El servicio Web de ejemplo cuyos métodos devuelven tipos complejos
  • Nimio.Consolas.UsandoProxyCompleto - Aplicación de consola que usa el servicio web tal como Visual Studio genera el proxy incluyendo las estructuras de datos que devuelve.
  • Nimio.Consolas.ModificandoProxyManualmente - Versión de consola que, aunque tiene el mismo código básico, ha modificado el archivo Reference.cs para que deserialice el resultado de la llamada en las mismas estructuras de datos (clases) que devuelve el servicio web.

Desde abril tengo dedicación exclusiva a un proyecto y estoy desplazado en el cliente para el que lo estamos desarrollando, aunque mantengo el canal abierto con los compañeros del proyecto anterior por si hay algún asunto con el que yo pudiera echarles una mano. Hace unos días me llamaba uno de los compañeros para plantearme un problema. Contrarreloj, como no puede ser de otra forma en este negocio, tenían que aplicar una nueva directriz venida de ultramar y por la que, una aplicación Web que llevaba bastante tiempo funcionando, tenía que ser despedazada divida en dos partes, siguiendo el nuevo, novísimo, y ultradeterminante estándar de empresa: la parte cliente debe estar en una máquina y la lógica de negocio en otra. Tachá-a-a-a-a-a-an. Uno puede tomar dos caminos ante una situación así: a) discutir con el cliente, intentar hacerle entender que eso es poco menos que una tontería infinita y perder un tiempo precioso; o b) dedicarse a solucionar el nuevo problema en cuerpo y alma. Por suerte existen los repositorios de código y cuando entren en razón se podrá retroceder al punto previo a la última decisión inteligente.

Como decía al principio, mi dedicación es exclusiva para el cliente, por lo que no puedo dedicar mucho tiempo a otros temas, y menos aún desplazarme para ver el código del otro cliente. Pero había que echar una mano. El planteamiento era claro:

  1. La aplicación actual está dividida en capas.
  2. Hay cuatro capas (en realidad tres, porque la de acceso a datos está entremezclada con la definición de entidades y la de negocio).
  3. Las tres capas principales son Interfaz de usuario, Lógica de negocio + Acceso a datos, Entidades.
  4. Aunque la capa de interfaz no accede nunca a la capa de datos, depende de la capa de negocio y, al mismo tiempo, recibe las entidades.
  5. Las entidades ofrecen funcionalidad adicional más allá de ser continentes de datos. Por ejemplo funciones de validación, de cálculo de totales, de gestión de colecciones internas, etcétera. Estas operaciones son utilizadas también desde la capa de interfaz.
  6. Aunque a priori parece poco relevante, el proyecto está hecho en .NET con la versión 2.0 del Framework (y no se puede cambiar). ¡Tócate los…!

De forma visual, la arquitectura actual sería la que pongo en el siguiente esquema. Las flechas indican visibilidad y, por tanto, referencia a —o dependencia de— ensamblados:


Todo ello, recordemos, se ejecuta dentro de un mismo proceso: La aplicación Web original.

Lo que se quiere, mejor dicho lo que se está obligado a hacer, pasa por partir, lo más limpiamente posible, pero a la vez en el menor tiempo posible (para ayer), la aplicación llevando toda la lógica de negocio a un servicio web:


Sin embargo esto plantea un problema: ¿Qué pasa con el uso que se realiza desde la capa de interfaz de las funcionalidad adicional que tienen las entidades? ¿Cómo evito tener que reescribir la mitad del código de la interfaz?

Empecemos por los servicios web. En .NET, desde sus orígenes, se podía definir un proyecto de tipo Servicio Web ASP.NET y que los métodos web, más allá de devolver los tipos básicos del lenguaje, devolvieran tipos complejos (clases o estructuras). Internamente, lo que hacía el motor de ASP.NET era construir un serializador XML, pasar la instancia por él y devolver una estructura XML representando dicho objeto. El inconveniente es que en este proceso de conversión se pierde completamente cualquier funcionalidad adicional que tuviese la clase, de forma que el resultado es un cascarón de datos, vacío de cualquier otra funcionalidad. Otro de los inconvenientes es que se pierden las referencias. Si se serializa una colección en donde distintas posiciones referencian al mismo objeto, se devolvería tantas réplicas del objeto como referencias hubiera. Por lo demás, lo bueno que tiene la serialización XML de los Servicios Web ASP.NET es que hasta mi abuela entiende lo que devuelven.

Pero veámoslo mejor con un ejemplo. En el enlace que ya indiqué al principio de la entrada (ese cuadro tan bonito de fondo azul claro), se pueden descargar el código que utilizaré para la explicación. En esencia se trata de un ensamblado (proyecto) que contiene la definición de dos clases (nuestras entidades del caso de estudio), que son las siguientes:


Bastante simple, pero lo donde quiero poner el acento será en los métodos de la clase Pedido y en la colección de ítems, propiedad desglose. Por ejemplo, el código del método ValorPedido sería:

 
  public double ValorPedido()
  {
     double valor = 0.0;
     foreach (ItemPedido item in Desglose)
        valor += item.ValorAlmacen;

     return valor;
  }

(Recuerden: NET 2.0. No hay cosas como LinQ y tal y tal).

Teniendo ya definido el dominio de la solución, pasamos al servicio web. Todo el mundo sabe cómo se crea un servicio web en .Net con Visual Studio. Simplemente comentar que aprovechando la capacidad de estos de poder devolver casi cualquier cosa, la definición de un método será tal que:

 
  [WebMethod]
  public Pedido UltimoPedido()
  {
     List<pedido> pedidos = ListarPedidos();
     return pedidos[pedidos.Count - 1];
  }

Donde se puede apreciar que el método devuelve un tipo Pedido, clase que definimos en el ensamblado de entidades. Supongo también que sobra comprobar el resultado, pero estableciendo el proyecto del servicio web como proyecto de inicio, y la clase donde se define el servicio en sí, como el elemento de inicio, desde el propio Visual Studio podremos lanzar el navegador, que mostrará los métodos disponibles, la especificación de cada uno y, como último paso, invocarlo.


Queda consumir el servicio web. Una vez más Visual Studio nos lo pone fácil. Basta con decirle al proyecto donde nos interesa interrogar al servicio, que agregue una referencia a un servicio web. Al ser dentro de la misma solución, bastará con pedirle que sea uno de los definidos en la misma. Definir un espacio de nombres y chimpún. Esto es lo que se ha hecho en los dos proyectos de consola que se incluyen en la solución.


El código de aplicación de ambas consolas es casi idéntico. Al final del mismo se solicita que nos devuelva el tipo interno (el que asigna el compilador a la definición de la clase) tanto de Pedido como de ItemPedido. Tras ejecutar Nimio.Consolas.UsandoProxyCompleto obtenemos como resultado que los tipos no se parecen en nada a los que originalmente creamos, dentro del espacio de nombres Nimio.Entidades.


Realmente se trata de los tipos definidos para la creación del proxy al servicio web, generado automáticamente por Visual Studio cuando añadimos la referencia al servicio web. De ahí que el espacio de nombres incorpore el de la aplicación de consola seguido del asignado a la referencia al servicio web durante el proceso de añadido.

La otra diferencia está en el pedazo de código siguiente:

 
  double valor = 0.0;
  int cantidad = 0;
  foreach (ItemPedido item in pedido.Desglose)
  {
     valor += item.ValorAlmacen;
     cantidad += item.Cantidad;
  }

Hemos perdido los métodos de la clase Pedido, por lo que estamos obligados a repetir ese código nuevamente.

Si ahora revisamos el código de la segunda aplicación de consola, Nimio.Consolas.ModificandoProxyManualmente, veremos que se hace uso de los métodos de la clase pedido y que al principio del archivo se ha incorporado una cláusula using Nimio.Entidades; (lo que implica que también hemos añadido la referencia a dicho proyecto). Si, además, la ejecutamos, en la parte donde se solicita el tipo de compilador de cada una de las clases involucradas, se obtiene:


En esta ocasión parece que sí que sí es lo que queremos. ¿Dónde está el secreto? Pues lo que tenemos que hacer justo después de agregar la referencia al servicio web. Realmente Visual Studio invoca a una utilidad de línea de comando, WSDL.EXE, que genera una serie de archivos, entre ellos uno con código C# (o del lenguaje para el que se haya solicitado), pero que queda oculto. El archivo en cuestión es Reference.cs y la forma de acceder es solicitar en el Explorador de soluciones que nos muestre todos los archivos y navegar en el contenido, ahora visible, de la referencia al servicio web:


Una vez seleccionado editarlo realizando dos cambios:

  1. Incluir el uso del espacio de nombres de las entidades al comienzo, antes de la definición del espacio de nombres del proxy; y
  2. Comentar todas las definiciones de tipos internas, que suelen caer a mitad del archivo.


Con esto, y recompilando, ya podemos hacer uso del proxy del servicio web, que se encargará de invocar al propio servicio web, pero que a la hora de deserializar la respuesta, lo hará devolviendo las clases que nos interesa usar.

El único (y gran) inconveniente con este método es que si solicitamos actualizar la referencia, porque hayamos cambiado algo en la interfaz del servicio web, perderemos cualquier modificación que hubiéramos hecho manualmente. Sin embargo, con unos pocos cambios, podemos recuperar el uso de nuestras clases y, lo que es más importante, podemos satisfacer la demanda del cliente con muy poco esfuerzo, confiando en que el 99% del código actual podrá utilizarse sin cambios, reduciendo considerablemente el número de fallos a detectar en fases posteriores.

Otro cliente satisfecho.

No hay comentarios: