Изменение интерфейса между версиями - как управлять? - PullRequest
6 голосов
/ 01 апреля 2012

Вот довольно неприятный рассол, с которым мы столкнулись на сайте клиента.Клиент имеет около 100 рабочих станций, на которых мы развернули версию 1.0.0 нашего продукта «MyApp».

Теперь одна из вещей, которые делает продукт, - это загружает надстройку (назовите ее «MyPlugIn»).", который он сначала ищет на центральном сервере, чтобы увидеть, есть ли более новая версия, и если это так, то он копирует этот файл локально, затем загружает надстройку, используя Assembly.Load, и вызывает определенный известный интерфейс.работал хорошо в течение нескольких месяцев.

Затем клиент захотел установить v1.0.1 нашего продукта на некоторые машины (но не на все). Это пришло с новой и обновленной версией MyPlugIn.

Но затем возникла проблема. Есть общая DLL, на которую ссылаются как MyApp, так и MyPlugIn, которая называется MyDLL, у которой есть метод MyClass.MyMethod. Между v1.0.0 и v1.0.1 изменена подпись MyClass.MyMethod (был добавлен параметр). И теперь новая версия MyPlugIn вызывает сбой клиентских приложений v1.0.0:

Метод не найден: MyClass.MyMethod (System.String)

Клиент явно не хочет развертывать v1.0.1 на всех клиентских станциях, поскольку исправление, включенное в v1.0.1, было необходимо только для нескольких рабочих станций, и нет необходимости развертывать его для всех клиентов.К сожалению, мы (пока) не используем ClickOnce или другие утилиты массового развертывания, поэтому развертывание v1.0.1 будет болезненным и в противном случае ненужным упражнением.

Есть ли какой-то способ написания кода в MyPlugin, чтобыон будет работать одинаково хорошо, независимо от того, имеет ли он дело с MyDLL v1.0.0 или v1.0.1?Возможно, есть какой-то способ поиска ожидаемого интерфейса с использованием отражения, чтобы увидеть, существует ли он, прежде чем его вызвать?

РЕДАКТИРОВАТЬ: Я должен также упомянуть - у нас есть довольно жесткие процедуры обеспечения качества.Поскольку v1.0.1 был официально выпущен QA, мы не можем вносить какие-либо изменения в MyApp или MyDLL.Единственная свобода передвижения, которую мы имеем, - это изменить MyPlugin, который представляет собой пользовательский код, написанный специально для этого клиента.

Ответы [ 7 ]

4 голосов
/ 01 апреля 2012

Дело в том, что сделанные вами изменения должны быть в основном в дополнение, а не изменение . Поэтому, если вы хотите обеспечить обратно совместимость в своем развертывании (насколько я понял в текущей стратегии развертывания, это единственный вариант), вам следует никогда изменить интерфейс, но добавить новые методы и избегайте тесных связей вашего плагина с общей DLL, но загружайте его динамически. В этом случае

  • вы добавите новую функциональность, не нарушая старую

  • вы сможете выбрать, какую версию DLL загружать во время выполнения.

3 голосов
/ 02 апреля 2012

Моя команда совершила ту же ошибку, что и вы, не раз. У нас похожая архитектура плагинов, и лучший совет, который я могу вам дать в долгосрочной перспективе, - это как можно скорее изменить эту архитектуру. Это кошмар ремонтопригодности. Матрица обратной совместимости растет нелинейно с каждым выпуском. Строгий анализ кода может дать некоторое облегчение, но проблема в том, что вам всегда нужно знать, когда были добавлены или изменены методы, чтобы вызывать их соответствующим образом. Если и разработчик, и рецензент не знают точно, когда метод был изменен в последний раз, вы рискуете возникнуть исключение времени выполнения, когда метод не найден. Вы НИКОГДА не можете безопасно вызывать новый метод в MyDLL в плагине, потому что вы можете работать на более старом клиенте, у которого нет новейшей версии MyDLL с методами.

В настоящее время вы можете сделать что-то вроде этого в 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);
   }   

}

Если MyMethod не является статичным, вы можете создать аналогичную нестатическую оболочку.

Что касается долгосрочных изменений, одна вещь, которую вы можете сделать со своей стороны, - предоставить интерфейсам ваших плагинов для связи. Вы не можете изменить интерфейсы после выпуска, но вы можете определить новые интерфейсы, которые будут использовать более поздние версии плагина. Кроме того, вы не можете вызывать статические методы в MyDLL из MyPlugIn. Если вы можете что-то изменить на уровне сервера (я понимаю, что это может быть вне вашего контроля), другой вариант - это обеспечить поддержку версий, чтобы новый плагин мог объявить, что он не работает со старым клиентом. Тогда старый клиент будет загружать только старую версию с сервера, в то время как новые клиенты загружают новую версию.

3 голосов
/ 01 апреля 2012

Я извлек этот код из приложения, которое написал некоторое время назад, и удалил некоторые части.
Здесь предполагается много вещей:

  1. Местоположение MyDll.dll является текущим каталогом
  2. Пространство имен для получения информации об отражении "MyDll.MyClass"
  3. В классе есть конструктор без параметров.
  4. Вы не ожидаете возвращаемого значения
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;
         }
     }
}

Что мы здесь делаем:

  1. Динамически загружать библиотеку и извлекать тип MyClass.
  2. Используя тип, запросите в подсистеме отражения список MethodInfo, представленных в этом типе.
  3. Проверьте каждое имя метода, чтобы найти нужный.
  4. Когда метод найден, создайте экземпляр типа.
  5. Получите количество параметров, ожидаемых методом.
  6. В зависимости от количества параметров, вызовите нужную версию, используя Invoke.
2 голосов
/ 01 апреля 2012

На самом деле звучит как плохая идея изменить контракт между выпусками.Находясь в объектно-ориентированной среде, вам лучше создать новый контракт, возможно, наследуя от старого.

public interface MyServiceV1 { }

public interface MyServiceV2 { }

Внутренне вы заставляете свой движок использовать новый интерфейс и предоставляете адаптер для перевода старогообъекты в новый интерфейс.

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

После загрузки сборки вы сканируете ее и:

  • когда вы найдете класс, реализующий новый интерфейс, вы используете его напрямую
  • когда вы найдете класс, реализующий старый интерфейс, вы используете над ним адаптер

Использование хаков (например, тестирование интерфейса) рано или поздно укусит вас или кого-либо еще, кто использует контракт -подробности взлома должны быть известны любому, кто полагается на интерфейс, который звучит ужасно с объектно-ориентированной точки зрения.

1 голос
/ 01 апреля 2012

Учитывая обстоятельства, я думаю, что единственное, что вы действительно можете сделать, - это запустить две версии MyDLL «бок о бок»,
и это означает что-то вроде того, что предложил Тигран, динамически загружая MyDLL - например, в качестве побочного примера, не связанного, но может помочь вам, взгляните на RedemptionLoader http://www.dimastr.com/redemption/security.htm#redemptionloader (это для плагинов Outlook, у которых часто возникают проблемы сбоя друг с другом, ссылаясь на разные версии dll-помощника, просто как предыстория - это более сложная причина COM, но здесь мало что меняется) -
это то, что вы можете сделать, нечто подобное. Динамически загружать dll по его местоположению, имени - вы можете указать это местоположение внутренне, жестко кодировать или даже настроить его из конфигурации или чего-то еще (или проверить и сделать это, если вы видите, что MyDll не подходящей версии),
а затем «обернуть» объекты, вызовы из динамически загруженной библиотеки DLL, чтобы соответствовать тому, что у вас обычно есть - или сделать какой-то трюк, подобный этому (вам нужно было бы что-то обернуть или «разветвлять» в реализации), чтобы все работало в обоих случаях .
Также добавить 'no-nos' и ваши печали QA:),
они не должны нарушать обратную совместимость с 1.0.0 до 1.0.1 - это (как правило) мелкие изменения, исправления - не ломать изменения, для этого нужна основная версия #.

1 голос
/ 01 апреля 2012

Не могли бы вы перегрузить MyMethod для приема MyMethod (строка) (совместима с версией 1.0.0) и MyMethod (строка, строка) (версия v1.0.1)?

1 голос
/ 01 апреля 2012

В MyDLL 1.0.1 устарели старые MyClass.MyMethod(System.String) и перегрузите их новой версией.

...