Предупреждение, длинный пост впереди.
В последнее время я много думал об этом, и я изо всех сил стараюсь найти здесь удовлетворительное решение. Я буду использовать С# и autofac для примеров.
Проблема
IoC отлично подходит для построения больших деревьев служб без состояния. Я разрешаю услуги и передаю данные только вызовам метода. Отлично.
Иногда я хочу передать параметр данных в конструктор службы. Для чего нужны фабрики. Вместо разрешения службы я разрешаю его factory и вызываю метод create с параметром для получения моей услуги. Еще немного работы, но хорошо.
Время от времени я хочу, чтобы мои службы разрешали один и тот же экземпляр в определенной области. Autofac обеспечивает InstancePerLifeTimeScope()
, что очень удобно. Это позволяет мне всегда разрешать один и тот же экземпляр внутри подкаталога выполнения. Хорошо.
И бывают случаи, когда я хочу объединить оба подхода. Я хочу, чтобы параметр data в конструкторе и имел экземпляры в области. Я не нашел удовлетворительного способа выполнить это.
Решение
1. Метод инициализации
Вместо передачи данных в конструктор просто передайте его методу Initialize
.
Интерфейс:
interface IMyService
{
void Initialize(Data data);
void DoStuff();
}
Класс:
class MyService : IMyService
{
private Data mData;
public void Initialize(Data data)
{
mData = data;
}
public void DoStuff()
{
//...
}
}
Регистрация:
builder.RegisterType<MyService>().As<IMyService>().InstancePerLifetimeScope();
Применение:
var myService = context.Resolve<IMyService>();
myService.Init(data);
// somewhere else
var myService = context.Resolve<IMyService>();
После первого разрешения службы и вызова Initialize я могу с радостью разрешить в том же контексте и получить тот же инициализированный экземпляр. Мне не нравится тот факт, что перед вызовом Initialize
у меня есть непригодный для использования объект. Существует опасность того, что экземпляр будет разрешен и использован где-то еще, прежде чем я вызову Initialize().
2. Шаблон держателя
Это шаблон, который содержит ссылку на объект данных и вместо того, чтобы вводить сам объект данных, я вставляю объект-держатель.
Интерфейс:
interface IMyService
{
void DoStuff();
}
Класс:
class MyService : IMyService
{
private Data mData;
public MyService(IDataHolder dataHolder)
{
mData = dataHolder.Data;
}
public void DoStuff()
{
//...
}
}
Регистрация:
builder.RegisterType<MyService>().As<IMyService>();
builder.RegisterType<DataHolder>().As<IDataHolder>().InstancePerLifetimeScope();
Применение:
var holder = context.Resolve<IDataHolder>();
holder.Data = data;
// somewhere else
var myService = context.Resolve<IMyService>();
Это немного лучше, поскольку я перенес ответственность за экземпляр для другого класса. Теперь я могу использовать держатель и в других сервисах. Другим преимуществом является то, что в случае необходимости я могу использовать данные горячей замены в держателе. Мне не нравится тот факт, что он запутывает код и добавляет другой интерфейс, который я должен издеваться во время тестирования.
3. Пусть контейнер содержит экземпляр
Интерфейс:
interface IMyService
{
void DoStuff();
}
Класс:
class MyService : IMyService
{
private Data mData;
public MyService(Data data)
{
mData = dataHolder.Data;
}
public void DoStuff()
{
//...
}
}
Регистрация:
builder.RegisterType<MyService>().As<IMyService>().InstancePerLifetimeScope();
Применение:
var myServiceFactory = context.Resolve<Func<Data, IMyService>>();
myServiceFactory(data);
// somewhere else
var myService = context.Resolve<IMyService>();
Это правильно. Я не сохраняю результат вызова factory в любом месте, потому что autofac сохраняет его для меня. Это очень удивительно для любого, кто прочитает код. Я не уверен, что autofac даже предназначался для такого использования. Хорошая вещь в том, что мне не нужен ни дополнительный метод инициализации, ни дополнительный класс для хранения экземпляра.
Вопрос
Как вы относитесь к этому? Как вы справляетесь с ситуацией с параметрами данных во время выполнения и охватом времени? Не хватает ли лучшего подхода?