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