Контейнер DI и состояние с настраиваемой областью действия в устаревшей системе

Я считаю, что понимаю основные концепции контейнеров DI / IoC, написав пару приложений, использующих их, и прочитав много ответов о переполнении стека, а также книгу Марка Симана. Есть еще несколько случаев, с которыми у меня возникают проблемы, особенно когда речь идет об интеграции DI-контейнера в большую существующую архитектуру, где принцип DI на самом деле не использовался (подумайте о большом комке грязи).

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

С (потенциально ошибочными, я мог бы слишком много думать об этом или быть совершенно неправым, предполагая, что такая гибридная архитектура является хорошей идеей) предположения, вот реальная проблема:

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

Ведение журнала является более серьезной проблемой. Подсистемы, вызываемые заданием, вероятно, также должны записывать данные в журнал, и обычно в примерах это делается путем простого запроса экземпляра ILog в конструкторе. Но как это работает в этом случае, когда мы не знаем деталей/реализации до времени выполнения? С:

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

Упрощенный пример, помогающий лучше понять проблему:

Class JobXImplementation : JobBase {
    // through constructor injection
    ILoggerFactory _loggerFactory;
    JobXExtraLogic _jobXExtras;

    public void Run(JobConfig configurationFromDatabase)
    {
        ILog log = _loggerFactory.Create(configurationFromDatabase.targets);
        // if there were no legacy parts in the call chain, I would register log as instance to a child container and Resolve next part of the call chain and everyone requesting ILog would get the correct logging targets
        // do stuff
        _jobXExtras.DoStuff(configurationFromDatabase, log);
    }
}

Class JobXExtraLogic {
    public void DoStuff(JobConfig configurationFromDatabase, ILog log) {
        // call to legacy sub-system
        var old = new OldClass(log, configurationFromDatabase.SomeRandomSetting);
        old.DoOldStuff();
    }
}

Class OldClass {
    public void DoOldStuff() {
        // moar stuff 
        var old = new AnotherOldClass();
        old.DoMoreOldStuff();
    }
}

Class AnotherOldClass {
    public void DoMoreOldStuff() {
        // call to a new subsystem 
        var newSystemEntryPoint = DIContainerAsServiceLocator.Resolve<INewSubsystemEntryPoint>();
        newSystemEntryPoint.DoNewStuff();
    }
}

Class NewSubsystemEntryPoint : INewSubsystemEntryPoint {
    public void DoNewStuff() {
        // want to log something...
    }
}

Я уверен, что вы получите картину к этому моменту.

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

Каковы мои варианты? Какие еще проблемы вы могли бы увидеть в подобной ситуации? Возможна ли попытка использовать только инъекцию конструктора в такой среде?


person Jukka Tykkyläinen    schedule 14.08.2012    source источник
comment
Мой первый вопрос о ведении журнала: вы не слишком много ?   -  person Steven    schedule 15.08.2012


Ответы (1)


Отличный вопрос. В общем, я бы сказал, что контейнер IoC теряет большую часть своей эффективности, когда только часть кода DI-дружественна.

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

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

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

person neontapir    schedule 14.08.2012