2012-04-01 14 views
6

Ecco un sottaceto piuttosto sgradevole che abbiamo trovato su un sito cliente. Il client ha circa 100 workstation, su cui abbiamo distribuito la versione 1.0.0 del nostro prodotto "MyApp".Cambio di interfaccia tra versioni - come gestire?

Ora, una delle cose che il prodotto fa è caricare un componente aggiuntivo (chiamalo "MyPlugIn", che prima cerca su un server centrale per vedere se c'è una versione più recente, e se è così copia quel file localmente, quindi carica il componente aggiuntivo utilizzando Assembly.Load e invoca una determinata interfaccia nota. Funziona bene da diversi mesi

Quindi il client voleva installare v1.0.1 del nostro prodotto su alcuni macchine (ma non tutte) .Questo è venuto con una versione nuova e aggiornata di MyPlugIn.

Ma poi è arrivato il problema. C'è una DLL condivisa, a cui fa riferimento sia MyApp che MyPlugIn, chiamato MyDLL, che ha un metodo MyClass.MyMethod. Tra la v1.0.0 e la v1.0.1, la firma di MyClass.MyMethod è cambiata (è stato aggiunto un parametro). E ora la nuova versione del myplugin fa sì che le applicazioni client v1.0.0 di crash:

Metodo non trovato: MyClass.MyMethod (System.String)

Il client volutamente non vuole distribuire v1 .0.1 su tutte le stazioni client, poiché la correzione inclusa in v1.0.1 era necessaria solo per poche workstation e non è necessario eseguirla su tutti i client. Purtroppo, non stiamo ancora utilizzando ClickOnce o altre utilità di distribuzione di massa, quindi implementare v1.0.1 sarà un esercizio doloroso e altrimenti inutile.

C'è un modo per scrivere il codice in MyPlugin in modo che funzioni ugualmente bene, indipendentemente dal fatto che si tratti di MyDLL v1.0.0 o v1.0.1? Forse c'è un modo per sondare un'interfaccia prevista usando la riflessione per vedere se esiste, prima di chiamarla effettivamente?

MODIFICA: Vorrei anche menzionare: abbiamo alcune procedure di QA piuttosto strette. Dal momento che v1.0.1 è stato ufficialmente rilasciato da QA, non è consentito apportare modifiche a MyApp o MyDLL. L'unica libertà di movimento che abbiamo è quella di cambiare MyPlugin, che è un codice personalizzato scritto appositamente per questo cliente.

+1

Perché non aggiungere nuovamente a MyDll il metodo previsto dalla vecchia versione del plug-in? Internamente questo metodo potrebbe chiamare la nuova versione del metodo che passa un valore predefinito per il nuovo parametro param. – Steve

+0

@Steve - vedere la mia modifica - non è possibile apportare modifiche a MyDLL –

+0

MyClass.MyMethod è statico? –

risposta

3

Ho estratto questo codice da un'applicazione che ho scritto qualche tempo fa e ho rimosso alcune parti.
Molte cose si assumono qui:

  1. posizione del MyDll.dll è la directory corrente
  2. Il Namespace per ottenere informazioni riflessione è "MyDll.MyClass"
  3. La classe ha un costruttore senza parametri.
  4. che non ti aspetti un valore di ritorno
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; 
     } 
    } 
} 

Quello che facciamo qui:

  1. caricare dinamicamente la biblioteca ed estrarre il tipo di MyClass.
  2. Utilizzando il tipo, chiedere al sottosistema di riflessione l'elenco di MethodInfo presente in quel tipo.
  3. Controllare ogni nome di metodo per trovare quello richiesto.
  4. Quando si trova il metodo, creare un'istanza del tipo.
  5. Ottieni il numero di parametri previsti dal metodo.
  6. In base al numero di parametri, chiamare la versione corretta utilizzando Invoke.
+0

Grazie, ho seguito un approccio usando il riflesso, esattamente come hai fatto qui, e questo ha funzionato alla grande! –

2

In realtà, sembra una cattiva idea per cambiare il contratto tra le versioni. Essendo in un ambiente orientato agli oggetti, dovresti piuttosto creare un nuovo contratto, possibilmente ereditando da quello vecchio.

public interface MyServiceV1 { } 

public interface MyServiceV2 { } 

Internamente fate il vostro motore per utilizzare la nuova interfaccia e vi fornirà un adattatore di tradurre vecchi oggetti alla nuova interfaccia.

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

Una volta caricato un assieme, è la scansione e:

  • quando si trova una classe che implementa la nuova interfaccia, lo si utilizza direttamente
  • quando si trova una classe che implementa la vecchia interfaccia, si utilizza l'adattatore su di esso

L'utilizzo di hack (come testare l'interfaccia) prima o poi morderà voi o chiunque altro utilizzando il contratto: i dettagli dell'hack devono essere noto a chiunque basandosi sull'interfaccia che suona terribile dalla prospettiva orientata agli oggetti.

+0

Concordato, questo non dovrebbe mai essere permesso. Ma ora siamo dietro al fatto e dobbiamo capire come risolvere questa situazione, con la sola flessibilità di cambiare il codice in MyPlugin. –

1

In MyDLL 1.0.1, deprecare il vecchio MyClass.MyMethod(System.String) e sovraccaricarlo con la nuova versione.

+0

Ah sì, sarebbe una buona idea ... ** se ** non avessimo procedure di QA molto strette. QA ha ufficialmente rilasciato la v1.0.1, e non ci è permesso apportare alcuna modifica ad esso ora ... l'unica cosa che sono libero di cambiare è MyPlugIn, che è un codice personalizzato scritto per questo specifico cliente. –

+3

Non è un QA stretto, è un QA rigido, un QA stretto avrebbe trovato il problema prima che fosse uscito. La soluzione migliore è quella di correggerlo correttamente, altrimenti tornerà e ti perseguiterà di nuovo. –

1

È possibile sovraccaricare MyMethod per accettare MyMethod (stringa) (versione 1.0.0 compatibile) e MyMethod (stringa, stringa) (versione v1.0.1)?

+0

No, vedi la mia modifica - Non posso apportare modifiche a MyDLL. –

4

Il fatto è che le modifiche apportate devono essere fondamentalmente in aggiunta e non il cambiamento . Quindi, se vuoi essere compatibile con la tua implementazione (per quanto ho capito nella strategia di implementazione corrente, questa è l'unica opzione) devi maicambiare l'interfaccia ma aggiungere un nuovo metodo ed evitare collegamenti stretti del tuo plugin con DLL condivisa, ma caricarlo in modo dinamico. In questo caso

  • si aggiungere una nuova funzionalita 'senza disturbare un vecchio

  • si sarà in grado di scegliere quale versione di DLL da caricare in fase di esecuzione.

+0

I tuoi punti sono ben presi per il futuro - ma ora siamo dietro al fatto e dobbiamo affrontare il casino che abbiamo fatto ... –

+0

@Shaul: è un casino caricare il riferimento di un plugin in modo dinamico? – Tigran

+0

Non sono sicuro di aver capito la tua domanda? –

1

Date le circostanze, credo che l'unica cosa che puoi fare veramente è avere due versioni di MyDLL disattivato 'fianco a fianco',
e questo significa che qualcosa di simile a ciò che Tigran ha suggerito, il caricamento del MyDLL in modo dinamico - per esempio come esempio di lato non correlato ma che potrebbe aiutarti, dai un'occhiata al RedemptionLoader http://www.dimastr.com/redemption/security.htm#redemptionloader (che è per un plugin di Outlook che spesso si blocca a vicenda facendo riferimento a diverse versioni di una dll helper, proprio come una storia di fondo - è un un po 'più complessa causa della COM coinvolta ma non cambia molto qui) -
è quello che puoi fare, qualcosa di simile. Carica dinamicamente la dll in base alla sua posizione, nome - puoi specificare quella posizione internamente, hard-code, o persino configurarla da config o qualcosa (o controllare e farlo se vedi che MyDll non è della versione corretta),
e poi "avvolgere" gli oggetti, le chiamate formano la DLL dinamicamente caricata per corrispondere a ciò che si ha normalmente - o fare qualche trucco del genere (dovresti avvolgere qualcosa o "fork" nell'implementazione) per far funzionare tutto in entrambi casi.
Anche per aggiungere "no-nos" e il tuo QA dispiacere :),
non dovrebbero rompere la compatibilità con le versioni precedenti da 1.0.0 a 1.0.1 - quelle sono (di solito) le modifiche minori, correzioni - non si rompono modifiche, la versione principale # è necessaria per questo.

3

La mia squadra ha commesso lo stesso errore più di una volta. Abbiamo un'architettura plug-in simile e il miglior consiglio che posso darti a lungo termine è quello di cambiare questa architettura il prima possibile. Questo è un incubo per la manutenzione. La matrice di retrocompatibilità cresce in modo non lineare con ogni versione. Revisioni rigorose del codice possono dare qualche sollievo, ma il problema è che devi sempre sapere quando i metodi sono stati aggiunti o modificati per chiamarli nel modo appropriato. A meno che lo sviluppatore e il revisore non sappiano esattamente quando un metodo è stato modificato per l'ultima volta, si corre il rischio che si verifichi un'eccezione di runtime quando il metodo non viene trovato. Non è MAI possibile chiamare in modo sicuro un nuovo metodo in MyDLL nel plug-in, poiché è possibile eseguire su un client precedente che non dispone della più recente versione MyDLL con i metodi.

Per il momento, si può fare qualcosa di simile in 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); 
    } 

} 

Se MyMethod non è statica si può fare un involucro non statico simile.

Per quanto riguarda i cambiamenti a lungo termine, una cosa che puoi fare da parte tua è quella di comunicare le interfacce dei tuoi plugin. Non è possibile modificare le interfacce dopo il rilascio, ma è possibile definire nuove interfacce che verranno utilizzate dalle versioni successive del plug-in. Inoltre, non è possibile richiamare metodi statici in MyDLL da MyPlugIn. Se è possibile modificare le cose a livello di server (mi rendo conto che questo potrebbe essere al di fuori del tuo controllo), un'altra opzione è fornire un supporto per il controllo delle versioni in modo che un nuovo plugin possa dichiarare che non funziona con un vecchio client. Quindi il vecchio client scaricherà la versione precedente dal server, mentre i nuovi client scaricheranno la nuova versione.

Problemi correlati