2012-04-01 13 views
6

Aquí hay un pickle bastante desagradable que entramos en el sitio de un cliente. El cliente tiene aproximadamente 100 estaciones de trabajo, en las cuales implementamos la versión 1.0.0 de nuestro producto "MyApp".Cambio de interfaz entre versiones: ¿cómo gestionarlo?

Ahora, una de las cosas que hace el producto es cargar un complemento (llámalo "MyPlugIn", que primero busca en un servidor central para ver si hay una versión más nueva, y si es entonces copia ese archivo localmente, luego carga el complemento usando Assembly.Load e invoca una cierta interfaz conocida. Esto ha funcionado bien durante varios meses.

Luego el cliente quería instalar v1.0.1 de nuestro producto en algunos máquinas (pero no todas). Eso vino con una versión nueva y actualizada de MyPlugIn.

Pero luego vino el problema. Hay una DLL compartida, a la que hacen referencia MyApp y MyPlugIn, llamada MyDLL, que tiene un método MyClass.MyMethod. Entre v1.0.0 y v1.0.1, la firma de MyClass.MyMethod cambió (se agregó un parámetro). Y ahora la nueva versión de miPlugin hace que las aplicaciones cliente v1.0.0 a chocar:

Método no encontrado: MyClass.MyMethod (System.String)

El cliente deliberadamente no quiere desplegar v1 .0.1 en todas las estaciones cliente, ya que la corrección que se incluía en v1.0.1 era necesaria solo para algunas estaciones de trabajo, y no es necesario implementarla en todos los clientes. Tristemente, no estamos (todavía) usando ClickOnce u otras utilidades de despliegue masivo, por lo que desplegar v1.0.1 será un ejercicio doloroso y de otro modo innecesario.

¿Hay alguna forma de escribir el código en MyPlugin para que funcione igual de bien, independientemente de si se trata de MyDLL v1.0.0 o v1.0.1? Quizás haya alguna manera de explorar una interfaz esperada utilizando la reflexión para ver si existe, antes de llamarla realmente.

EDIT: Debo mencionar también: tenemos algunos procedimientos de control de calidad bastante estrictos. Desde que v1.0.1 ha sido lanzado oficialmente por QA, no podemos realizar ningún cambio en MyApp o MyDLL. La única libertad de movimiento que tenemos es cambiar MyPlugin, que es un código personalizado escrito específicamente para este cliente.

+1

¿Por qué no volver a agregar a MyDll el método esperado por la versión del complemento anterior? Internamente, este método podría llamar a la nueva versión del método pasando un valor predeterminado para el nuevo método param. – Steve

+0

@Steve - ver mi edición - no puede hacer ningún cambio en MyDLL –

+0

¿MyClass.MyMethod estático? –

Respuesta

3

He extraído este código de una aplicación que escribí hace algún tiempo y eliminé algunas partes.
Hay muchas cosas que se supone aquí:

  1. Localización de Mydll.dll es el directorio actual
  2. el espacio de nombres para obtener información reflexión es "MyDll.MyClass"
  3. La clase tiene un constructor sin parámetros.
  4. usted no espera un valor de retorno
using System.Reflection; 

private void CallPluginMethod(string param) 
{ 
    // Is MyDLL.Dll in current directory ??? 
    // Probably it's better to call Assembly.GetExecutingAssembly().Location but.... 
    string libToCheck = Path.Combine(Environment.CurrentDirectory, "MyDLL.dll"); 
    Assembly a = Assembly.LoadFile(libToCheck); 
    string typeAssembly = "MyDll.MyClass"; // Is this namespace correct ??? 
    Type c = a.GetType(typeAssembly); 

    // Get all method infos for public non static methods 
    MethodInfo[] miList = c.GetMethods(BindingFlags.Public|BindingFlags.Instance|BindingFlags.DeclaredOnly); 
    // Search the one required (could be optimized with Linq?) 
    foreach(MethodInfo mi in miList) 
    { 
     if(mi.Name == "MyMethod") 
     { 
      // Create a MyClass object supposing it has an empty constructor 
      ConstructorInfo clsConstructor = c.GetConstructor(Type.EmptyTypes); 
      object myClass = clsConstructor.Invoke(new object[]{}); 

      // check how many parameters are required 
      if(mi.GetParameters().Length == 1) 
       // call the new interface 
       mi.Invoke(myClass, new object[]{param}); 
      else 
       // call the old interface or give out an exception 
       mi.Invoke(myClass, null); 
      break; 
     } 
    } 
} 

Lo que hacemos aquí:

  1. carga dinámicamente la biblioteca y extraer el tipo de MyClass.
  2. Usando el tipo, solicite al subsistema de reflexión la lista de MethodInfo presente en ese tipo.
  3. Compruebe el nombre de cada método para encontrar el requerido.
  4. Cuando se encuentre el método, cree una instancia del tipo.
  5. Obtenga la cantidad de parámetros esperados por el método.
  6. Dependiendo del número de parámetros, llame a la versión correcta usando Invoke.
+0

Gracias, seguí un enfoque usando la reflexión, exactamente como lo has hecho aquí, ¡y funcionó muy bien! –

2

En realidad, suena como una mala idea para cambiar el contrato entre versiones. Al estar en un entorno orientado a objetos, debería crear un nuevo contrato, posiblemente heredando el anterior.

public interface MyServiceV1 { } 

public interface MyServiceV2 { } 

Internamente se hacen a su motor a utilizar la nueva interfaz y le proporcionará un adaptador para convertir objetos antiguos a la nueva interfaz.

public class V1ToV2Adapter : MyServiceV2 { 
    public V1ToV2Adapter(MyServiceV1) { ... } 
} 

Al cargar un ensamblaje, escanearlo y:

  • cuando se encuentra una clase que implementa la nueva interfaz, que lo utilice directamente
  • cuando se encuentra una clase que implementa la interfaz de edad, utiliza el adaptador sobre él

El uso de hacks (como la prueba de la interfaz) tarde o temprano morderá usted o cualquier otra persona que utilice el contrato - los detalles del truco deben ser conocido por cualquiera que confíe en la interfaz que suena terrible desde la perspectiva orientada a objetos.

+0

De acuerdo, esto nunca debería haber sido permitido. Pero ahora estamos detrás del hecho, y tenemos que encontrar la forma de solucionar esta situación, con flexibilidad solo para cambiar el código en MyPlugin. –

1

En MyDLL 1.0.1, desaprobar el anterior MyClass.MyMethod(System.String) y sobrecargarlo con la nueva versión.

+0

Ah, sí, sería una buena idea ... ** si ** no teníamos procedimientos de control de calidad realmente estrictos. QA ha lanzado oficialmente v1.0.1, y no podemos hacer ningún cambio ahora ... lo único que puedo cambiar es MyPlugIn, que es un código personalizado escrito para este cliente específico. –

+3

No es un control de calidad estricto, es un control de calidad rígido, un control de calidad estrecho habría encontrado el problema antes de que se hubiera apagado. La mejor solución es arreglar esto correctamente, de lo contrario volverá y te atormentará nuevamente. –

1

¿Podría sobrecargar MyMethod para aceptar MyMethod (cadena) (versión 1.0.0 compatible) y MyMethod (cadena, cadena) (versión v1.0.1)?

+0

No, ver mi edición - No puedo hacer ningún cambio en MyDLL. –

4

La cosa es que los cambios realizados tienen que ser básicamente además y no el cambio . Por lo tanto, si desea volver compatible en su implementación (por mucho que entendí en la estrategia de implementación actual, tiene esta es una opción única) debe nuncacambiar la interfaz pero agregarle un nuevo método y evitar enlaces estrechos de su complemento con DLL compartido, pero cárguelo de forma dinámica. En este caso

  • va a añadir una nueva funcionalidad sin molestar a un anciano

  • usted será capaz de elegir qué versión de DLL para cargar en tiempo de ejecución.

+0

Tus puntos están bien preparados para el futuro, pero ahora estamos detrás del hecho, y tenemos que lidiar con el desastre que hemos creado ... –

+0

@Shaul: ¿es un desastre cargar dinámicamente la referencia de un complemento? – Tigran

+0

No estoy seguro de entender su pregunta? –

1

Dadas las circunstancias, creo que la única cosa que puede hacer realmente es tener dos versiones de MyDLL corriendo 'lado a lado',
y que significa algo así como lo que sugiere Tigran, cargar el MyDLL dinámicamente - por ejemplo, como un ejemplo adicional no relacionado, pero que podría ayudarlo, eche un vistazo al RedemptionLoader http://www.dimastr.com/redemption/security.htm#redemptionloader (esto es para un plugin de Outlook que a menudo tiene problemas estrellándose entre sí haciendo referencia a diferentes versiones de un dll de ayuda, como una historia de fondo, eso es la causa un poco más compleja del COM involucrado pero no cambia mucho aquí) -
es lo que puedes hacer, algo similar. Cargue dinámicamente el dll por su ubicación, nombre: puede especificar esa ubicación internamente, código de hardware o incluso configurarlo desde config o algo así (o verifique y haga eso si ve que MyDll no tiene la versión correcta),
y luego 'envuelve' los objetos, las llamadas forman el dll dinámicamente cargado para que coincida con lo que normalmente tiene, o haga algún truco como ese (tendría que ajustar algo o 'fork' en la implementación) para que todo funcione en ambos casos.
Además de agregar los "no-nos" y sus dolores de control de calidad :),
no deberían romper la compatibilidad hacia atrás de 1.0.0 a 1.0.1 - esos son (generalmente) los cambios menores, arreglos - no se rompen cambios, la versión principal # es necesaria para eso.

3

Mi equipo ha cometido el mismo error que usted tiene más de una vez. Tenemos una arquitectura de complementos similar y el mejor consejo que puedo darle a largo plazo es cambiar esta arquitectura lo antes posible. Esta es una pesadilla de mantenimiento. La matriz de compatibilidad con versiones anteriores crece de forma no lineal con cada versión. Las revisiones de código estrictas pueden proporcionar algún alivio, pero el problema es que siempre debe saber cuándo se agregaron o cambiaron los métodos para llamarlos de la manera adecuada. A menos que el desarrollador y el revisor sepan exactamente cuándo se modificó por última vez el método, se corre el riesgo de que exista una excepción de tiempo de ejecución cuando no se encuentre el método. NUNCA puede llamar de manera segura a un nuevo método en MyDLL en el complemento, ya que puede ejecutar en un cliente anterior que no tenga la versión más reciente de MyDLL con los métodos.

por el momento, se puede hacer algo como esto en MyPlugin:

static class MyClassWrapper 
{ 
    internal static void MyMethodWrapper(string name) 
    { 
     try 
     { 
     MyMethodWrapperImpl(name); 
     } 
     catch (MissingMethodException) 
     { 
     // do whatever you need to to make it work without the method. 
     // this may go as far as re-implementing my method. 
     } 
    } 

    private static void MyMethodWrapperImpl(string name) 
    { 
     MyClass.MyMethod(name); 
    } 

} 

Si MiMetodo no es estática se puede hacer una envoltura no estático similar.

En cuanto a los cambios a largo plazo, una cosa que puede hacer en su extremo es dar a sus complementos interfaces para comunicarse a través de. No puede cambiar las interfaces después del lanzamiento, pero puede definir nuevas interfaces que usarán las versiones posteriores del complemento. Además, no puede llamar a métodos estáticos en MyDLL desde MyPlugIn. Si puede cambiar las cosas en el nivel del servidor (me doy cuenta de que esto puede estar fuera de su control), otra opción es proporcionar algún tipo de soporte de versiones para que un nuevo complemento pueda declarar que no funciona con un cliente anterior. Luego, el cliente anterior solo descargará la versión anterior del servidor, mientras que los clientes más nuevos descargarán la nueva versión.

Cuestiones relacionadas