Tratando de no romper la compatibilidad hacia atrás de nuestros ensamblados
La siguiente entrada, tiene que ver con la compatibilidad de nuestros ensamblados o mejor dicho, con no romper la compatibilidad de nuestros ensamblado ante el planteamiento de modificaciones obligadas. El objetivo de no romper la compatibilidad de los ensamblados hacia atrás es algo que a veces resulta bastante complejo, sobre todo porque conviene tener un control de qué parte o partes del ensamblado o ensamblados vamos a cambiar, modificar, etc, y sobre todo, que impacto tienen esas modificaciones.
La solución que expongo en esta entrada es una de tantas, así que tampoco quiero que nadie la adopte como la ideal. A mí me resulta cómoda y me parece muy evidente, pero como dice el dicho, cada maestrillo tiene su librillo, y cada escenario puede obligarnos a cambiar de idea, así que cada uno que decida.
El caso es que muchas veces, desarrollamos un ensamblado con un conjunto de funcionalidades que son puestas ahí a veces de forma muy pensada y meditada, en otras ocasiones casi de mala manera, y así un montón de situaciones muy diversas. Lo más importante, es que las funcionalidades funcionen como esperamos y que sin pensarlo mucho más, decidamos tirar para adelante.
El problema surge cuando el día a día nos hace ver que la pequeña solución implementada para resolver nuestros grandes problemas, no está del todo bien, ya sea porque hemos metido la zarpa o por la razón que sea (excusas y culpas indemostrables hay siempre mil y una).
El caso es que vamos a suponer un ejemplo muy tonto de desarrollo.
Creamos un ensamblado llamado TestEnsamblado en versión 1.0.0.0, y cuyo código es el siguiente:
/// <summary>
/// Class to get the details of the a person.
/// </summary>
public class PersonDetails
{
/// <summary>
/// Constructor of the class.
/// </summary>
public PersonDetails()
{
}
/// <summary>
/// Constructor of the class.
/// </summary>
/// <param name=»name»>Name of the person.</param>
/// <param name=»department»>Department of the person</param>
public PersonDetails(string name, string department)
{
// Assign the name of the person
this.Name = name;
// Assign the department of the person
this.Department = department;
}
/// <summary>
/// Name property.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Department property.
/// </summary>
public string Department { get; set; }
}
Hasta aquí perfecto.
Compilamos nuestro ensamblado y lo consumimos en nuestros desarrollos.
Tiempo más adelante, nos encontramos con que la clase PersonDetails debería ser llamada (por cuestiones varias que nos ha dejado sin dormir durante un par de días) EmployeeDetails, y que el campo Department se denominará ahora Area.
¿Cómo podríamos hacer los cambios en nuestro ensamblado TestEnsamblado 2.0.0.0 sin romper la compatibilidad hacia atrás con TestEnsamblado 1.0.0.0?.
Aunque hay varios refritos y cada uno utiliza el que le conviene o le convence, uno de ellos nos invita a incluir en la clase del ensamblado la etiqueta Obsolete que creará la marca de obsoleto a dicha clase antigua, dejando perenne a la nueva clase del ensamblado. De esa manera, un usuario que tiene una aplicación que utiliza el ensamblado 1.0.0.0, podría recompilar la aplicación con el ensamblado 2.0.0.0 sin problemas y seguir funcionando la aplicación. O al menos, esa es la teoría.
No obstante, un desarrollador nuevo que utilice el ensamblado 2.0.0.0 debe saber que si utiliza la clase «obsoleta», estará cometiendo un sacrilegio mediante el cual, Bill Gates podría aparecer vestido de 666 y hacer conjuras maléficas varias… vamos,… que sin exagerar ni delirar, lo suyo es no utilizar la clase marcada como obsoleta. Si se marca como obsoleta será para que vayamos acostumbrándonos a no usarla. Digo yo que es de sentido común.
Para llevar a cabo esta tarea, podríamos hacer las siguientes acciones.
En primer lugar, crear la nueva clase.
/// <summary>
/// Class to get the details of the a person.
/// </summary>
public class EmployeeDetails
{
/// <summary>
/// Constructor of the class.
/// </summary>
public EmployeeDetails()
{
}
/// <summary>
/// Constructor of the class.
/// </summary>
/// <param name=»name»>Name of the person.</param>
/// <param name=»area»>Area of the person</param>
public EmployeeDetails(string name, string area)
{
// Assign the name of the person
this.Name = name;
// Assign the department of the person
this.Area = area;
}
/// <summary>
/// Name property.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Department property.
/// </summary>
public string Area { get; set; }
}
Y una vez hecho esto y sin querer complicar el asunto (formas diferentes de realizar lo que os voy a mostrar, herencia, etc etc etc), prepararemos la clase obsoleta para que no rompa la compatibilidad o al menos, para que la permita «tragar» adecuadamente en las aplicaciones que utilicen el ensamblado versión 1.0.0.0.
La clase obsoleta quedaría de esta forma:
using System;
/// <summary>
/// Class to get the details of the a person.
/// </summary>
[Obsolete(«This class is obsolete; Please, use EmployeeDetails instead.»)]
public class PersonDetails
{
private EmployeeDetails employeeDetails;
/// <summary>
/// Constructor of the class.
/// </summary>
public PersonDetails()
{
employeeDetails = new EmployeeDetails();
}
/// <summary>
/// Constructor of the class.
/// </summary>
/// <param name=»name»>Name of the person.</param>
/// <param name=»department»>Department of the person</param>
public PersonDetails(string name, string department)
{
employeeDetails = new EmployeeDetails(name, department);
}
/// <summary>
/// Name property.
/// </summary>
public string Name { get { return employeeDetails.Name; } set { employeeDetails.Name = value; } }
/// <summary>
/// Department property.
/// </summary>
public string Department { get { return employeeDetails.Area; } set { employeeDetails.Area = value; } }
}
Aún y así, cuando nos pongamos a trabajar con nuestra aplicación, es bastante probable que sin querer o por inercia, utilicemos la clase que hemos marcado como obsoleta.
El que una clase esté marcada como obsoleta, no nos impide utilizar la clase como vemos en la imagen anterior, ya que por defecto, solo muestra un aviso o warning.
Para lograr que el compilador nos marque un error y no un warning o aviso cuando utilizamos la clase obsoleta PersonDetails, deberemos cambiar al principio de la definición de la clase la siguiente instrucción:
/// <summary>
/// Class to get the details of the a person.
/// </summary>
[Obsolete(«This class is obsolete; Please, use EmployeeDetails instead.», true)]
public class PersonDetails
{
…
Marcando el valor true en Obsolete, forzamos a que la aplicación que utilice la clase obsoleta, marque un error y no compile.
Esto es muy útil para evitar que se utilice la clase obsoleta dentro del desarrollo de nuestra aplicación, pero… ¿qué ocurre si actualizamos el ensamblado y nuestra aplicación ya utilizaba la versión 1.0.0.0?.
Es decir,… supongamos que generamos nuestro ensamblado como TestEnsamblado v1.0.0.0.
Creamos el proyecto consumidor de nuestro ensamblado TestEnsamblado v1.0.0.0.
La aplicación funciona perfectamente. La implantamos y cerramos esa petición de funcionalidad.
Más adelante y en el tiempo, decidimos crear un nuevo proyecto de nuestros ensamblados y en concreto TestEnsamblado v2.0.0.0. Aquí podríamos hacer un branch o lo que haga falta, tampoco vamos a complicar mucho la historia ahora mismo.
A la clase obsoleta la marcamos con el atributo true como hemos visto anteriormente para que impida que ningún desarrollador pueda juguetear accidentalmente con la clase obsoleta. Además, tenemos previsto eliminar la clase obsoleta para TestEnsamblado v3.0.0.0, así que marcar a Obsolete como true tiene su sentido.
En otro orden de cosas, nos aseguramos de que la firma del segundo ensamblado es la misma que la del primer ensamblado. No queremos romper compatibilidades.
Creamos un nuevo proyecto consumidor de nuestro ensamblado y por «accidente» utilizamos la clase obsoleta. Compilamos o pulsamos F5, y el entorno nos indica un error impidiéndonos el uso de esta clase obsoleta,… pero… ¿os acordais que habíamos desarrollado una aplicación que utiliza TestEnsamblado v1.0.0.0 y que está en producción?. ¿Qué pasaría si sobreescribiéramos el ensamblado TestEnsamblado v1.0.0.0 con TestEnsamblado v2.0.0.0?.
Imaginemos que hemos agregado además de la nueva clase TestEnsamblado v2.0.0.0, un par de modificaciones que resuelven un par de bugs detectados.
¿Sobreescibimos el ensamblado?. Recordemos que el uso de la clase obsoleta, arroja en Visual Studio un error por estar marcada con el atributo true.
Pues efectivamente, no pasa nada de lo que «lógicamente» podríamos pensar, es decir, acepta sin rechistar TestEnsamblado v2.0.0.0 y se ejecuta sin problemas, sin embargo, en el entorno de desarrollo sí se queja.
Adicionalmente, también podemos utilizar el atributo Obsolete, no solo en clases, sino también en métodos, funciones, etc.
Esto es justamente lo que nos permite «jugar» con nuestros ensamblados, actualizarlos, modificarlos, y no romper compatibilidades, asunto éste más frecuente de lo que la mayoría cree.
Eso de amputar código de los ensamblados como Pedro por su casa, como que no, todo requiere un poco de meditación y planteamiento lógico (que muchas veces es el menos lógico) antes de cortar por lo sano.
Espero que os haya gustado.
Saludos.
2 Responsesso far
Buen post Jorge!
Obsolete es una buena medida de control.
Funciona bien en este escenario que comentas, donde la aplicación que usa TestEnsamblado v1.0.0.0 está terminada. El problema viene más cuando la aplicación que usa v1.0.0.0 está viva (al modificarla debes readaptar todo el código) y también si en lugar de aplicaciones separadas tenemos dlls que formen parte de una aplicación compuesta, o bien deben compartirse datos entre ellos: Para el CLR PersonDetails y EmployeeDetails son dos clases sin relación.
De todos modos como digo, gran post 🙂
Saludos!
Hola Eduard,
haz una cosa.
Crea un ensamblado v1.0.0.0 con una firma determinada.
Crea un proyecto ejecutable que utilice ese ensamblado y que muestre en pantalla por ejemplo los datos de sus propiedades o lo que quieras.
Crea un ensamblado v2.0.0.0 como he indicado y con la misma firma que el primero.
Sobreescribe el ensamblado v2.0.0.0 en el v1.0.0.0 de la aplicación del proyecto ejecutable que está implantada.
Ejecuta la aplicación que se lanzaba con el ensamblado v1.0.0.0 y que ahora se lanza con el ensamblado v2.0.0.0 (esta aplicación ni se ha tocado ni recompilado).
¿Resultados?.