IoC с зависимостями типа значения и типа объекта
Я ищу предложения о том, как лучше проектировать объекты для IoC
Предположим, у меня есть объект (Service), который имеет зависимость от DataContext, который зарегистрирован в Ioc.
Но это также требует свойства name, я мог бы спроектировать объект так:
class Service
{
public Service(IDataContext dataContext,
string name)
{
this._dataContext = dataContext;
this._name = name
}
public string Name
{
get
{
return _name;
}
}
}
Проблема в том, что использование с контейнерами Ioc становится очень сложным, поскольку строковый объект, такой как имя, нелегко зарегистрировать, а использование контейнера Ioc усложняется. Таким образом, разрешение становится запутанным:
var service = Ioc.Resolve<Service>( ?? )
Другой подход заключается в следующем:
class Service
{
public Service(IDataContext dataContext)
{
this._dataContext = dataContext;
}
public string Name { get; set; }
}
Разрешение теперь проще:
var service = Ioc.Resolve<Service>();
service.Name = "Some name";
Единственный недостаток - указание имени больше не требуется. Я хотел бы услышать от экспертов DI или IoC, как они пойдут на разработку этого и все еще будут оставаться довольно независимыми от технологии контейнеров для конкретного Ioc.
Я знаю, что многое зависит от того, как вы хотите использовать это, вариант 2 был бы идеальным, если бы имя действительно было необязательным. Но в случае, когда требуется имя, вы можете добавить шаг проверки в другой точке кода, а вместо этого перейти к дизайну, чтобы сделать Ioc проще.
Мысли?
5 ответов
Ваш выбор DI-контейнера не должен диктовать дизайн вашего API. Если name
не является обязательным, он должен быть частью подписи конструктора (что делает его обязательным).
Затем возникает следующий вопрос: как сконфигурировать контейнер, не неся при этом тонны накладных расходов? Как это сделать, зависит от контейнера. Вот как реализовать соглашение вокруг строкового аргумента в Castle Windsor:
public class NameConvention : ISubDependencyResolver
{
public bool CanResolve(CreationContext context,
ISubDependencyResolver contextHandlerResolver,
ComponentModel model, DependencyModel dependency)
{
return dependency.TargetType == typeof(string)
&& dependency.DependencyKey == "name";
}
public object Resolve(CreationContext context,
ISubDependencyResolver contextHandlerResolver,
ComponentModel model, DependencyModel dependency)
{
return "foo"; // use whatever value you'd like,
// or derive it from the provided models
}
}
Затем зарегистрируйте NameConvention
с таким контейнером:
container.Kernel.Resolver.AddSubResolver(new NameConvention());
Если ваш контейнер не имеет подходящих точек расширения, выберите контейнер, который имеет.
Проблема в том, что его становится очень сложно использовать с контейнерами Ioc, так как строковый объект, такой как имя, не легко зарегистрировать, а использование становится сложным с контейнером Ioc.
Большинство хороших IoC-контейнеров обеспечат простые способы предоставления аргументов конструктора при настройке.
Ваш первый пример - внедрение в конструктор - обычно считается предпочтительным способом. Думайте о своем конструкторе как о контракте, которому нужно следовать, который, будучи удовлетворенным, отображает действительный объект.
Ваш второй пример кода - внедрение свойства - обычно считается менее предпочтительным, чем внедрение конструктора. В любом случае, контейнеры IoC обычно дают вам возможность предоставлять значения для параметров или свойств конструктора при конфигурации, которые будут предоставляться каждый раз, когда вы запрашиваете у IoC создание для вас этого объекта.
Я не уверен, какой контейнер IoC вы хотите использовать, но вот пример кода, используемого для настройки StructureMap и предоставления строковых значений для различных сервисов. Если я не читаю ваш вопрос, похоже, это то, что вы хотите сделать.
ObjectFactory.Initialize(x =>
{
x.For<ICatalogAdminService>().Use<DLinkCatalogAdminService>()
.Ctor<string>("catalogConnectionString").Is(ConfigurationManager.ConnectionStrings["myCompany_AdminConnectionString"].ConnectionString)
.Ctor<string>("contentConnectionString").Is(ConfigurationManager.ConnectionStrings["myCompany_AdminConnectionString"].ConnectionString)
.Ctor<string>("webCatalogConnectionString").Is(ConfigurationManager.ConnectionStrings["myCompany_WebConnectionString"].ConnectionString)
.Ctor<string>("dlinkPromotionAdminConnectionString").Is(ConfigurationManager.ConnectionStrings["myCompany_AdminConnectionString"].ConnectionString);
x.For<IContentManagementAdminService>().Use<DLinkContentManagementAdminService>()
.Ctor<string>("contentConnectionString").Is(ConfigurationManager.ConnectionStrings["myCompany_AdminConnectionString"].ConnectionString)
.Ctor<string>("webCatalogConnectionString").Is(ConfigurationManager.ConnectionStrings["myCompany_WebConnectionString"].ConnectionString)
.Ctor<string>("dlinkPromotionConnectionString").Is(ConfigurationManager.ConnectionStrings["myCompany_AdminConnectionString"].ConnectionString);
x.For<IPromotionAdminService>().Use<DLinkPromotionAdminService>()
.Ctor<string>("catalogConnectionString").Is(ConfigurationManager.ConnectionStrings["myCompany_AdminConnectionString"].ConnectionString)
.Ctor<string>("promotionConnectionString").Is(ConfigurationManager.ConnectionStrings["myCompany_AdminConnectionString"].ConnectionString);
x.For<ISearchService>().Use<Extractor>();
x.For<IImporter>().Use<Importer>();
x.For<IOrderAdminService>().Use<DLinkOrderAdminService>()
.Ctor<string>("contentConnectionString").Is(ConfigurationManager.ConnectionStrings["myCompany_AdminConnectionString"].ConnectionString)
.Ctor<string>("orderConnectionString").Is(ConfigurationManager.ConnectionStrings["myCompany_OrdersConnectionString"].ConnectionString);
});
РЕДАКТИРОВАТЬ
Отвечая на комментарий, если вы на самом деле хотите указать аргумент конструктора вручную, это будет выглядеть так:
ObjectFactory.GetInstance<ICatalogAdminService>(new ExplicitArguments(
new Dictionary<string, object>() { { "parameter1", "someValue" } }));
Очевидно, что это может быть ужасно быстро, поэтому вы можете захотеть использовать некоторые фабричные / вспомогательные методы, если вы обнаружите, что делаете это часто.
Подход, который я обычно применяю в такой ситуации, заключается в том, что вместо строки вводится объект настроек, а затем в конструкторе запрашивается свойство, представляющее эту строку. Или, в некоторых случаях, даже лучше, когда мне нужно это строковое свойство, я убираю его из этих настроек, чтобы его можно было изменить (полезно, если это действительно настройка программы).
Другой вариант - использовать что-то вроде привязки аннотации. Я не знаю, какую платформу внедрения зависимостей вы используете, но вот как это можно сделать в guice (java) фреймворке, с которым я сейчас работаю.
Если вы используете Castle Windsor, вы можете использовать печатные фабрики, о которых вы можете прочитать здесь. По сути, типизированные фабрики позволяют создавать интерфейс, который выглядит следующим образом.
public interface IServiceFactory
{
IService Create(string name);
}
Впрыскивать это и звонить Create()
с именем по вашему выбору Виндзор вернет построенный IService
реализация.
Проектируйте его так, как вы всегда делаете, помня о хороших инженерных практиках (SOLID и т. Д.) Тогда, если ваш контейнер выбора ограничивает вас, вы либо используете его неправильно, либо используете неправильный контейнер.
В случае Windsor вы можете легко предоставлять встроенные, жестко запрограммированные зависимости компонентам во время регистрации:
container.Register(Component.For<Foo>().DependsOn(new{name = "Stefan"});
Вы также можете предоставить больше динамических зависимостей или зависеть от значений из вашей конфигурации XML, если вам нужно изменить их после компиляции.
Если значение является необязательным, сделайте его необязательным, либо с помощью конструктора, который указывает для него значение по умолчанию, либо с помощью перегрузок конструктора, либо в качестве свойства. Опять же, хороший контейнер будет обрабатывать все эти случаи, если тот, который вы сейчас используете, возможно, не переключится на лучший.