Инверсия зависимости: как лучше управлять версиями ваших абстракций

Как создавать версии абстракций в.Net при применении инверсии зависимости в среде с высоким повторным использованием кода

Я заинтересован в переходе на использование Dependency Inversion в.Net, но натолкнулся на то, что меня озадачило. Я не верю, что это связано с определенным методом или поставщиком DIP, но более фундаментальная проблема, которую, возможно, решили другие. Проблема, которую я решаю, лучше всего изложена постепенно, как показано ниже.

Предположение / Ограничение

Существенным предположением или ограничением, которое стоит упомянуть, является то, что моя команда разработчиков придерживалась правила сохранения наших развернутых сборок в одной и только одной версии сборки, а именно в версии 1.0.0.0. До сих пор мы не поддерживали размещение более одной версии сборки любой данной сборки, которую мы разработали, на сервере для простоты. Это может быть ограничением, и может быть много веских причин отойти от этого, но тем не менее, в настоящее время мы работаем с этим правилом. Так что с этой практикой, продолжайте ниже.

сценарий

  • У вас есть интерфейс IDoStuff, содержащийся в сборке абстракции Stuff.Abstractions.dll с двумя методами.
  • Вы компилируете компонент A.dll с классом, явно реализующим IDoStuff с 2 методами.
  • Вы перемещаете A.dll в производственное использование, Assembly Version 1.0.0.0, Assembly File version 1.0.0.0.
  • Вы перемещаете Interface.dll в prod, версия сборки 1.0.0.0, версия файла сборки 1.0.0.0.
  • Все отлично работает Время проходит.
  • Вы добавляете другой метод (например, "DoMoreStuff") в интерфейс IDoStuff, чтобы его мог вызвать другой компонент B. (Принимая во внимание принцип OO сегрегации интерфейса, допустим, что метод DoMoreStuff имеет смысл находиться в этом относительно небольшом интерфейсе IDoStuff.)
  • Теперь у вас есть IDoStuff с 3 методами в Stuff.Abstractions.dll, и вы создали компонент B, чтобы использовать новый 3-й метод.
  • Переместите Stuff.Abstractions.dll в производственное использование (обновите его), версия сборки 1.0.0.0, версия файла сборки 1.0.0.1. (обратите внимание, что версия файла увеличивается, но версия сборки и, следовательно, строгое имя остаются неизменными)
  • Вы перемещаете B.dll в производственное использование, Assembly Version 1.0.0.0, Assembly File version 1.0.0.17.
  • Вы ничего не делаете с A.dll. Вы полагаете, что в это время не требуется никаких изменений.

Теперь вы вызываете код, который пытается выполнить A.dll на том же рабочем сервере, где он работал раньше. Во время выполнения инфраструктура Dependency Inversion разрешает интерфейс IDoStuff для класса внутри A.dll и пытается его создать. Проблема заключается в том, что в классе A.dll реализован исчезнувший теперь 2-методный интерфейс IDoStuff. Как и следовало ожидать, вы получите исключение, подобное этому:

Метод 'DoMoreStuff' в типе 'Класс IDoStuff внутри A.dll' из сборки 'строгое имя сборки A.dll' не имеет реализации.

Мне предлагаются два способа решения этого сценария, когда мне нужно добавить метод в существующий интерфейс:

1) Обновите каждую сборку, предоставляющую функциональность, которая использует Stuff.Abstractions.dll, для реализации нового метода DoMoreStuff. Кажется, что делать что-то нелегко, но грубой силой мучительно работать.

2) Измените приведенное выше предположение / ограничение и начните разрешать существование более чем одной версии сборки (по крайней мере, для сборок определения абстракции). Это могло бы быть немного по-другому и сделать еще несколько сборок на наших серверах, но это должно учитывать следующее конечное состояние:

A.dll зависит от stuff.abstractions.dll, версии сборки 1.0.0.0, версии файла сборки 1.0.0.22 (AFV не имеет значения, кроме идентификации сборки). B.dll зависит от stuff.abstractions.dll, версии сборки 1.0.0.1, сборочный файл версии 1.0.0.23 (AFV не имеет значения, кроме идентификации сборки) Оба с удовольствием могут выполняться на одном сервере.

Если на сервере установлены обе версии stuff.abstractions.dll, то все должно быть в порядке. A.dll не должен быть изменен либо. Всякий раз, когда ему нужны моды, у вас будет возможность реализовать заглушку и обновить интерфейс или ничего не делать. Возможно, было бы лучше ограничить 2 метода, к которым у него был доступ, если они когда-либо были нужны. В качестве дополнительного преимущества мы бы знали, что все, что ссылается на stuff.abstractions.dll версии 1.0.0.0, имеет доступ только к двум методам интерфейса, тогда как пользователи версии 1.0.0.1 имеют доступ к трем методам.

Есть ли лучший способ или принятый шаблон развертывания для версий абстракций?

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

2 ответа

Решение

Частично проблема может заключаться в том, что вы напрямую зависите от интерфейсов, которые были разработаны с более широкой целью. Вы можете смягчить проблему, если ваши классы зависят от абстракций, которые были созданы для них.

Если вы определяете интерфейсы по мере необходимости для представления зависимостей ваших классов, а не зависимости от внешних интерфейсов, вам никогда не придется беспокоиться о реализации элементов интерфейса, которые вам не нужны.

Предположим, я пишу класс, который включает в себя доставку заказа, и я понимаю, что мне нужно будет проверить адрес. У меня может быть библиотека или служба, которая выполняет такие проверки. Но я не обязательно хочу просто внедрить этот интерфейс прямо в мой класс, потому что теперь мой класс имеет внешнюю зависимость. Если этот интерфейс растет, я потенциально нарушаю принцип разделения интерфейсов, завися от интерфейса, который я не использую.

Вместо этого я мог бы остановиться и написать интерфейс:

public interface IAddressValidator
{
    ValidationResult ValidateAddress(Address address);
}

Я внедряю этот интерфейс в свой класс и продолжаю писать свой класс, откладывая написание реализации на потом.

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

public class MyOtherServiceAddressValidator : IAddressValidator
{
    private readonly IOtherServiceInterface _otherService;

    public MyOtherServiceAddressValidator(IOtherServiceInterface otherService)
    {
         _otherService = otherService;
    }

    public ValidationResult ValidateAddress(Address address)
    {
        // adapt my address to whatever input the other service
        // requires, and adapt the response to whatever I want
        // to return.
    }
}

IAddressValidator существует потому, что я определил его, чтобы делать то, что мне нужно для моего класса, поэтому мне никогда не придется беспокоиться о необходимости реализации элементов интерфейса, которые мне не нужны. Там никогда не будет.

Всегда есть возможность версии интерфейсов; например, если есть

public interface IDoStuff
{
    void GoFirst();

    void GoSecond();
}

Тогда может быть

public interface IDoStuffV2 : IDoStuff
{
    void GoThird();
}

Тогда ComponentA может ссылаться IDoStuff и ComponentB может быть написано против IDoStuffV2, Некоторые люди недовольны наследованием интерфейса, но я не вижу другого способа легко создавать версии интерфейсов.

Другие вопросы по тегам