2012-04-01 2 views
6

Вот довольно неприятный рассол, который мы получили на клиентском сайте. Клиент имеет около 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? Возможно, есть какой-то способ исследования ожидаемого интерфейса с использованием отражения, чтобы увидеть, существует ли он, прежде чем называть его?

EDIT: Следует также упомянуть - у нас есть довольно жесткие процедуры QA. Поскольку v1.0.1 был официально выпущен QA, нам не разрешено вносить какие-либо изменения в MyApp или MyDLL. Единственная свобода передвижения, которую мы имеем, - это изменить MyPlugin, который является специальным кодом, написанным специально для этого клиента.

+1

Почему бы не добавить в MyDll метод, ожидаемый старой версией плагина? Внутри этого метода можно вызвать версию нового метода, передающую значение по умолчанию для нового параметра метода. – Steve

+0

@Steve - см. Мое редактирование - не может вносить изменения в MyDLL –

+0

Является ли MyClass.MyMethod статическим? –

ответ

3

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

  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.
+0

Спасибо, я следовал за методом, используя отражение, точно так же, как вы это сделали, и это отлично поработало! –

2

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

public interface MyServiceV1 { } 

public interface MyServiceV2 { } 

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

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

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

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

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

+0

Согласен, это никогда не должно было быть разрешено. Но мы сейчас после этого, и мы должны решить, как исправить эту ситуацию, с гибкостью только для изменения кода в MyPlugin. –

1

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

+0

Ах да, это была бы хорошая идея ... ** если ** у нас не было действительно жестких процедур QA. QA официально выпустила v1.0.1, и теперь нам не разрешено вносить в нее какие-либо изменения ... единственное, что я могу изменить, это MyPlugIn, который является настраиваемым кодом, написанным для этого конкретного клиента. –

+3

Это не плотный QA, это жесткий QA, жесткий QA нашел бы проблему, прежде чем она погаснет. Лучшее решение - исправить это правильно, иначе оно вернется и снова преследует вас. –

1

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

+0

Нет, см. Мое редактирование - я не могу вносить какие-либо изменения в MyDLL. –

4

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

  • вы добавить новый funcionality, не мешая на старую

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

+0

Ваши баллы хорошо приняты на будущее - но мы сейчас после факта, и нам приходится иметь дело с беспорядком, который мы создали ... –

+0

@ Шой: это беспорядок, чтобы загрузить ссылку плагина динамически? – Tigran

+0

Я не уверен, что понимаю ваш вопрос? –

1

Учитывая обстоятельства, я думаю, единственное, что вы можете сделать, на самом деле есть две версии 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 - это (как правило) незначительные изменения, исправления - не нарушение для этого необходимы изменения, основная версия #.

3

Моя команда совершила ту же ошибку, что и у вас более одного раза. У нас есть аналогичная архитектура плагинов, и лучший совет, который я могу вам дать в долгосрочной перспективе, - это как можно скорее изменить эту архитектуру. Это кошмар ремонтируемости. Матрица обратной совместимости растет нелинейно с каждым выпуском. Строгие проверки кода могут дать некоторое облегчение, но проблема заключается в том, что вам всегда нужно знать, когда методы были добавлены или изменены, чтобы вызвать их соответствующим образом. Если и разработчик, и рецензент точно знают, когда последний метод был изменен, вы рискуете, что существует исключение во время выполнения, когда метод не найден. Вы никогда не можете безопасно вызывать новый метод в 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. Если вы можете изменить ситуацию на уровне сервера (я понимаю, что это может быть вне вашего контроля), другой вариант - предоставить некоторую поддержку версий, чтобы новый плагин мог заявить, что он не работает со старым клиентом. Тогда старый клиент будет загружать только старую версию с сервера, а новые клиенты загружают новую версию.

Смежные вопросы