Microsoft .NET Framework

Migración de bibliotecas .NET heredadas hacia plataformas modernas

Josh Lane

Una de las fortalezas de Microsoft .NET Framework es la gran variedad de bibliotecas abiertas y comerciales de terceros que están disponibles para la plataforma. Es un testimonio de la madurez del ecosistema de desarrollo de .NET, que no solo contamos con unas API excelentes dentro de .NET Framework, sino que podemos encontrar miles de bibliotecas que no pertenecen a Microsoft, para responder a solicitudes HTTP, dibujar cuadrículas en una aplicación de escritorio, almacenar datos estructurados en el sistema de archivos y mucho más. De hecho, una consulta rápida en los repositorios de código .NET muestra más de 32.000 proyectos en CodePlex, más de 5.000 ejemplos de código en code.msdn.microsoft.com y más de 10.000 paquetes únicos en la galería de NuGet.

La emergencia de plataformas de software nuevas como Windows Phone 8 y Windows 8 tiene el potencial de revitalizar estas bases de código de calidad reconocida a través del tiempo. Las bibliotecas .NET que nos han servido durante años en el escritorio y el servidor pueden resultar igualmente útiles (y a veces incluso más) en estos entornos nuevos, ya que estamos dispuestos a llevar a cabo la labor necesaria para la migración a estas plataformas nuevas. Tradicionalmente, estas tareas pueden resultar difíciles y tediosas, pero aunque hay que ser cuidadoso y realizar una planeación explícita para garantizar el éxito, Visual Studio 2012 tiene varias características que minimizan las posibles dificultades y maximizan las oportunidades de reutilización en las diferentes plataformas.

En este artículo exploraré las dificultades que encontré durante la migración real de la base de datos NoSQL ­orientada a objetos del proyecto Sterling. Lo guiaré a través de un breve resumen de la biblioteca, compartiré los obstáculos que surgieron durante la migración y las soluciones para superarlos. Para concluir, indicaré algunos consejos sobre los patrones y procedimientos recomendados que podrían servirle en sus iniciativas de migración de bibliotecas.

¿Qué es Sterling?

Sterling es una biblioteca ligera de almacenamiento de datos NoSQL que ofrece una rápida recuperación indizada de instancias de clases .NET. También permite realizar actualizaciones, eliminaciones, copias de seguridad y restauración, truncamiento, entre otros; aunque, al igual que otras tecnologías NoSQL, no entrega un sistema de búsqueda de uso general basado en el lenguaje SQL. En vez de eso, la noción de “consulta” es un conjunto de operaciones definidas y ordenadas:

  • Primero, se recupera una colección de claves predefinidas o índices asignados a las instancias de clase que se cargaron en forma diferida.
  • Luego, se busca el índice en la colección de claves para realizar un filtrado inicial rápido de todo el conjunto de los resultados posibles.
  • Finalmente, se emplean consultas LINQ to Objects estándar en los pares de clave-valor que ya están filtrados, para refinar aún más los resultados.

El modelo de uso de Sterling (y de otras bases de datos NoSQL similares) difiere claramente del que ofrecen las bases de datos relacionales tradicionales como SQL Server. La ausencia de un lenguaje de consultas formal y definido puede resultar especialmente extraño para los novatos. De hecho, los defensores de NoSQL presentan esto como una fortaleza, debido a la posible complejidad y sobrecarga asociada a la asignación de entradas y salidas entre el mundo de las consultas y el mundo del código. Las soluciones NoSQL como Sterling no realizan ninguna asignación, ya que la consulta y el código son una misma cosa.

Un tratamiento completo de los pormenores de Sterling está fuera del alcance de este artículo (consulte el artículo de Jeremy Likness, “Sterling para el almacenamiento aislado en Windows Phone 7” en msdn.microsoft.com/magazine/hh205658 para obtener más información), pero destacaré algunas ventajas y desventajas claves que hay que tener en cuenta:

  • Tiene una huella pequeña (alrededor de 150 KB en disco) y se presta bien para el hosting dentro del proceso.
  • Funciona de inmediato con todo el conjunto corriente de tipos serializables de .NET.
  • El conjunto de conceptos necesario para la funcionalidad básica es pequeño; basta con cinco líneas de código en C# para comenzar a trabajar con Sterling.
  • Las características de las bases de datos tradicionales, tales como la seguridad pormenorizada; las actualizaciones y eliminaciones en cascada; la semántica de bloqueo configurable; las garantías de atomicidad, coherencia, aislamiento y durabilidad (ACID); entre otras, no están disponibles en Sterling. Si necesita estas características, tal vez le convenga considerar un motor relacional como SQL Server.

Desde el principio, el creador de Sterling (mi colega de Wintellect, Jeremy Likness) lo destinó a diferentes plataformas: creó binarios para .NET Framework 4, Silverlight 4 y 5 e incluso Windows Phone 7. Así que al evaluar el trabajo necesario para actualizar Sterling para .NET Framework 4.5, Windows Phone 8 y para las aplicaciones de la Tienda Windows, yo sabía que la arquitectura se prestaría para esta tarea, pero no conocía con exactitud la magnitud del proyecto.

El consejo que describo en este artículo es el resultado directo de mi experiencia al actualizar Sterling para .NET Framework 4.5, Windows Phone 8 y las aplicaciones de la Tienda Windows. Aunque algunos de los detalles de mi travesía son propios del proyecto Sterling, muchos resultan pertinentes para una gran diversidad de proyectos e iniciativas de conversión en el ecosistema de Microsoft.

Desafíos y soluciones

Al meditar sobre los obstáculos a los que me enfrenté al portar Sterling a las nuevas plataformas de destino, emergieron algunas categorías generales, en las que pude agrupar los problemas que encontré y proporcionar una orientación más general para todos los responsables de realizar este tipo de proyectos.

Acomodar filosofías de diseño divergentes El primer conjunto de posibles problemas es de naturaleza un tanto filosófica, aunque tiene un impacto real en la migración general. Pregúntese lo siguiente: “¿Hasta qué punto se alinean la arquitectura y el diseño de la biblioteca que quiero migrar con los patrones y modelos de uso común de las nuevas plataformas de destino?”

No es una pregunta que tenga una respuesta fácil y las soluciones claras y universales son esquivas. Nuestro ingenioso administrador de diseño para Windows Forms podría resultar difícil de portar a Windows Presentation Foundation (WPF). Las API son diferentes, pero son las diferentes filosofías de diseño y nociones de administración de control y posicionamiento de esos dos mundos las que probablemente nos harán tropezar a la larga. Otro ejemplo son los controles de entrada personalizados de la interfaz de usuario que funcionan bien con el método de entrada clásico del teclado y mouse, pero que podrían causar una experiencia de usuario deficiente en los entornos táctiles como Windows Phone o Windows 8. No basta con querer migrar una base de código; tiene que existir una compatibilidad del diseño subyacente entre la plataforma antigua y la nueva, además de la voluntad de conciliar cualquier diferencia menor que pudiera existir. En el caso de Sterling, tuve que ocuparme de algunos de estos dilemas.

El problema más destacado del diseño fue la discordancia entre la API de actualización de datos sincrónica de Sterling y la naturaleza asincrónica esperada de este comportamiento en las bibliotecas de Windows Phone 8 y en las aplicaciones para la Tienda Windows. Sterling se diseñó hace varios años, en un mundo donde las API asincrónicas eran algo raro y las herramientas y técnicas para crearlas eran, en el mejor de los casos, rudimentarias.

Esta es una firma típica del método Save de Sterling, antes de la migración:

object Save<T>(T instance) where T : class, new()

Lo que debemos tener en cuenta aquí es que este método se ejecuta en forma sincrónica; es decir, independientemente de lo que se tarde en guardar el argumento de la instancia, el autor de la llamada se bloquea y espera que el método finalice la tarea. Esto puede resultar en el conjunto habitual de problemas de bloqueo de subprocesos: interfaces de usuario que no responden, reducción importante en la escalabilidad del servidor, etcétera.

Las expectativas del usuario con respecto a la capacidad de respuesta del diseño de software han ido en aumento durante los últimos años; ya nadie tolera una interfaz de usuario que se inmoviliza durante varios segundos mientras esperamos que finalice una operación de guardado. A modo de respuesta, las directrices de diseño para las API de las plataformas nuevas como Windows Phone 8 y Windows 8 exigen que los métodos de las bibliotecas públicas, tales como Save, sean operaciones asincrónicas y no de bloqueo. Afortunadamente, las características como el modelo de programación del Patrón asincrónico basado en tareas (TAP) de .NET y las palabras clave async y await de C# facilitan las cosas. Esta es la firma actualizada de Save:

Task<object> SaveAsync<T>(T instance) where T : class, new()

Ahora Save se devuelve en forma inmediata y el autor de la llamada tiene un objeto al que se puede aplicar await (Task), que sirve para la posible recolección del resultado (en este caso, la clave única de la instancia recién guardada). El autor de la llamada no se bloquea y puede realizar otras tareas mientras la operación de guardado se lleva a cabo en segundo plano.

Para ser más claros, todo lo que vimos aquí son las firmas del método; la conversión propiamente tal de una implementación sincrónica a una asincrónica requería de una refactorización adicional y de un cambio a las API de archivo asincrónicas para cada plataforma de destino. Por ejemplo, la implementación sincrónica de Save empleó BinaryWriter para escribir en el sistema de archivos:

using ( BinaryWriter instanceFile = _fileHelper.GetWriter( instancePath ) )
{
  instanceFile.Write( bytes );
}

Pero como BinaryWriter no admite la semántica asincrónica, lo refactoricé para usar las API asincrónicas adecuadas para cada plataforma de destino. Por ejemplo, la Figura 1 muestra la apariencia de SaveAsync para el controlador de almacenamiento de tablas de Windows Azure de Sterling.

Figura 1 Apariencia de SaveAsync para el controlador de almacenamiento de tablas de Windows Azure de Sterling

using ( var stream = new MemoryStream() )
{
  using ( var writer = new BinaryWriter( stream ) )
  {
    action( writer );
  }
  stream.Position = 0;
  var entity = new DynamicTableEntity( partitionKey, rowKey )
  {
    Properties = new Dictionary<string, EntityProperty>
    {
      { "blob", new EntityProperty( stream.GetBuffer() ) }
    }
  };
  var operation = TableOperation.InsertOrReplace( entity );
  await Task<TableResult>.Factory.FromAsync(
    table.BeginExecute, table.EndExecute, operation, null );
}

Todavía uso BinaryWriter para escribir valores discretos en una secuencia en memoria, pero luego uso DynamicTableEntity de Windows Azure y CloudTable.BeginExecute junto con .EndExecute para almacenar el contenido de la matriz de bytes de la secuencia de manera asincrónica en el servicio de almacenamiento de tablas de Windows Azure. Tuve que realizar varios cambios similares para lograr el comportamiento de actualización de datos asincrónicos de Sterling. Pero el punto clave es el siguiente: la refactorización de la API en el nivel superficial podría ser solo el primero de muchos pasos necesarios para lograr una migración como esta. Planee sus tareas de trabajo y cálculos aproximados del esfuerzo adecuadamente y sea realista en cuanto a si este gran cambio es factible en primer lugar.

De hecho, mi experiencia con Sterling reveló una meta impracticable. Una característica de diseño central de Sterling es que todas las operaciones de almacenamiento operan con datos fuertemente tipados, al emplear API y extensiones de serialización de contrato de datos estándar de .NET. Esto funciona bien con los clientes de Windows Phone, .NET 4.5 y también con las aplicaciones basadas en C# para la Tienda Windows. Pero en el mundo de los clientes de la Tienda Windows con HTML5 y JavaScript no existe el concepto del tipado fuerte. Luego de realizar indagaciones y conversaciones con Likness, determiné que no existía ninguna forma sencilla de adaptar Sterling para estos clientes, así que opté por excluirlos de las opciones disponibles. Por supuesto, este tipo de discordancias se debe considerar caso por caso, pero debe tener claro que estas situaciones pueden surgir y tiene que ser realista sobre sus opciones.

Compartir código en diferentes plataformas de destino El siguiente desafío que enfrenté es uno que todos enfrentamos alguna vez: ¿cómo compartir código común en diferentes proyectos?

Identificar y compartir código común en diferentes proyectos es una estrategia comprobada para disminuir el plazo de comercialización y los problemas de mantenimiento más adelante. Hemos hecho esto durante años en .NET; un patrón típico es definir un ensamblado común y hacer referencia a este desde varios proyectos consumidores. Otra técnica favorita es la funcionalidad de “Agregar como vínculo” de Visual Studio, que permite compartir un archivo de origen maestro único entre varios proyectos, tal como se aprecia en la Figura 2.

The Visual Studio 2012 Add As Link Feature
Figura 2 Característica Agregar como vínculo de Visual Studio 2012

Incluso hoy en día estas opciones funcionan bien si todos los proyectos de consumidor se dirigen a la misma plataforma subyacente. Sin embargo, cuando queremos exponer funcionalidades comunes en varias plataformas diferentes (como en el caso de Sterling), la creación de un ensamblado común para este código se convierte en una carga de desarrollo enorme. La creación y mantención de varios destinos de compilación se vuelve absolutamente necesario y esto aumenta la complejidad de la configuración y del proceso de creación del proyecto. El uso de directivas de preprocesador (#if, #endif, etc.) para incluir comportamientos propios de cada plataforma en forma condicional para ciertas configuraciones de compilación es prácticamente inevitable; esto dificulta la lectura, navegación y comprensión del código. La energía que desperdiciamos en esas tareas de configuración nos distrae de la meta principal, que es resolver problemas reales mediante código.

Por suerte, Microsoft previó la necesidad de un desarrollo más fácil para varias plataformas y, a partir de .NET Framework 4, agregó una característica nueva, llamada Bibliotecas de clases portables (PCL). Las PCL nos permiten destinar el código en forma selectiva a diferentes versiones de .NET Framework, Silverlight y Windows Phone, así como también a la Tienda Windows y Xbox 360; todo desde un solo proyecto de Visual Studio .NET. Cuando elegimos una plantilla de proyecto PCL, Visual Studio se asegura automáticamente de que el código solo use bibliotecas existentes en cada plataforma objetivo elegida. Gracias a esto, las directivas de preprocesador y los destinos de generación difíciles de controlar ya no son necesarios. Por otro lado, sí impone ciertas restricciones sobre las API que podemos llamar desde la biblioteca; más adelante veremos cómo arreglárnoslas con estas restricciones. Consulte “Desarrollo multiplataforma con .NET Framework” (msdn.microsoft.com/library/gg597391) para obtener más información sobre las características y el uso de PCL.

Las PCL resultaron ser una solución natural para lograr mis metas multiplataforma para Sterling. Pude refactorizar más del 90 por ciento de la base de código de Sterling en una PCL común única y usable sin modificaciones en .NET Framework 4.5, Windows Phone 8 ni Windows 8. Esto es una ventaja enorme para la viabilidad a largo plazo del proyecto.

Una breve nota sobre los proyectos de pruebas unitarias: actualmente no existe ningún equivalente a PCL para el código de pruebas unitarias. El obstáculo principal para su creación es la falta de un marco de pruebas unitarias unificado que funcione en todas las plataformas. Ante esta realidad, definí para las pruebas unitarias de Sterling diferentes proyectos de prueba independientes para .NET 4.5, Windows Phone 8 y Windows 8. El proyecto para .NET 4.5 tiene la única copia del código de prueba, mientras que todos los otros proyectos comparten el código de prueba mediante la técnica del Agregar como vínculo, que vimos previamente. Cada proyecto de plataforma hace referencia a los ensamblados del marco de pruebas propio de la plataforma; afortunadamente, el espacio de nombres y los nombres de los tipos son idénticos en todos ellos, así que el mismo código se compila sin modificaciones en todos los proyectos de prueba. Consulte la base de código actualizado de Sterling en GitHub (bit.ly/YdUNRN) para ver el funcionamiento en ejemplos.

Aprovechar las API propias de cada plataforma Mientras que las PCL son una gran ayuda en la creación de bases de código unificadas para varias plataformas, también plantean un dilema: ¿cómo se usan las API propias de cada plataforma que no se pueden llamar desde el código PCL? Un ejemplo perfecto es la refactorización del código asincrónico que ya mencioné: aunque en .NET 4.5 y en las aplicaciones de la Tienda Windows podemos elegir entre una gran cantidad de API asincrónicas, ninguna de estas se puede llamar desde una PCL. ¿Podemos tener la chancha y los cuatro reales?

La respuesta es que sí se puede, con un poco de trabajo. La idea es definir dentro de la PCL una o más interfaces que modelen los comportamientos propios de las plataformas que no podemos llamar directamente y luego implementar el código basado en PCL en términos de esas abstracciones de interfaz. Luego creamos las implementaciones para cada interfaz en bibliotecas específicas para cada plataforma. Finalmente, creamos las instancias de los tipos de PCL en tiempo de ejecución para realizar alguna tarea, al integrar la implementación de interfaz específica adecuada para la plataforma de destino actual. La abstracción permite que el código de la PCL permanezca desacoplado de los detalles de la plataforma.

Si todo esto le parece conocido, es porque debería serlo: el patrón que acabo de describir se conoce como Inversión de control (IoC), una técnica de diseño de software de eficacia comprobada que sirve para lograr el desacoplamiento y el aislamiento modular. Puede leer más acerca de IoC en bit.ly/13VBTpQ.

Al convertir Sterling, solucioné varios problemas de incompatibilidad entre las API con este sistema. La mayoría de las API problemáticas provienen del espacio de nombres System.Reflection. Lo irónico es que aunque cada plataforma de destino expone todas las funcionalidades de reflexión que necesito para Sterling, cada una tiene sus propias peculiaridades y sutilezas, lo que hace que sea muy difícil usarlas de manera uniforme en las PCL. Es por esto que resulta necesaria esta técnica basada en IoC. Puede encontrar la abstracción de interfaz en C# resultante que definí para que Sterling evite estos problemas en bit.ly/13FtFgO.

Un consejo general

Ahora que ya describí mi estrategia de migración para Sterling, retrocederé un poco para ver cómo podría aplicar las lecciones de esta experiencia en el caso general.

Primero, y pienso que es necesario recalcarlo, use PCL. Las PCL son una ayuda inmensa para el desarrollo en varias plataformas, además ofrecen bastante flexibilidad en la configuración para adaptarse a cualquier necesidad. Si quiere migrar una biblioteca existente (o incluso escribir una nueva) y la destina a más de una plataforma, debería usar una PCL.

Luego, deberá prever cierto trabajo de refactorización necesario para adaptarse a los objetivos de diseño cambiantes. En otras palabras, no espere que su migración de código sea un proceso mecánico sencillo o que pueda reemplazar una llamada de API por otra. Es muy probable que los cambios que deba realizar vayan más profundo que el nivel superficial y tal vez tenga que cambiar una o más suposiciones centrales que se hicieron cuando se escribió la base de código original. Hay un límite práctico a la renovación total que podemos imponer al código existente sin provocar un impacto grave posterior; tendrá que decidir por su propia cuenta dónde está esa línea y si puede cruzarla o no. La migración de una persona, para otra es la bifurcación de código fuente en un proyecto completamente nuevo.

Finalmente, no deje de lado su caja de herramientas existente de patrones y técnicas de diseño. Ilustré cómo empleé el principio de IoC y la inserción de dependencias en Sterling, para aprovechar las API propias de cada plataforma. Otros enfoques similares también podrían resultarle de gran utilidad. Los patrones de diseño de software clásicos como estrategia (bit.ly/Hhms), adaptador (bit.ly/xRM3i), método de plantilla (bit.ly/OrfyT) y fachada (bit.ly/gYAK9) pueden ser muy útiles a la hora de refactorizar código existente para nuevos propósitos.

Mundos felices

El resultado final de mi trabajo es una implementación de Sterling NoSQL completamente funcional en las tres plataformas de destino .NET Framework 4.5, Windows 8 y Windows Phone 8. Resulta muy gratificante ver cómo se ejecuta Sterling en los últimos dispositivos basados en Windows, como mi tableta Surface y mi teléfono Nokia Lumia 920.

El proyecto Sterling se hospeda en el sitio Wintellect GitHub (bit.ly/X5jmUh) y contiene el código fuente completamente migrado, así como las pruebas unitarias y los proyectos de ejemplo para cada plataforma. También incluye una implementación del modelo de controlador de Sterling que emplea el almacenamiento de tablas de Windows Azure. Lo invito a clonar el repositorio de GitHub y a explorar los patrones y las opciones de diseño que describí en este artículo; espero que sirva como punto de partida útil para otras iniciativas similares.

Y recuerde, no se deshaga del código viejo… ¡mígrelo!

Josh Lane es consultor sénior de Wintellect LLC en Atlanta. Ha dedicado 15 años a la arquitectura, el diseño y la creación de software en las plataformas de Microsoft y ha desarrollado con éxito una amplia gama de soluciones tecnológicas, desde sitios web de centro de llamadas hasta compiladores personalizados de JavaScript, entre otros. Le gusta el desafío de derivar valor empresarial significativo mediante software. Puede ponerse en contacto con él en jlane@wintellect.com.

GRACIAS al siguiente experto técnico por su ayuda en la revisión de este artículo: Jeremy Likness (Wintellect)