Как, используя внедрение зависимостей, получить конфигурацию из нескольких источников?
Я использую Simple Injector, но, возможно, мне нужен скорее концептуальный ответ.
Вот сделка, предположим, у меня есть интерфейс с настройками приложения:
public interface IApplicationSettings
{
bool EnableLogging { get; }
bool CopyLocal { get; }
string ServerName { get; }
}
Тогда, обычно, есть класс, который реализует IApplicationSettings, получая каждое поле из указанного источника, например:
public class AppConfigSettings : IApplicationSettings
{
private bool? enableLogging;
public bool EnableLogging
{
get
{
if (enableLogging == null)
{
enableLogging = Convert.ToBoolean(ConfigurationManager.AppSettings["EnableLogging"];
}
return enableLogging;
}
}
...
}
ТЕМ НЕ МЕНИЕ! Допустим, я хочу получить EnableLogging
из app.config, CopyLocal
из базы данных, и ServerName
из другой реализации, которая получает текущее имя компьютера. Я хочу иметь возможность сочетать конфигурацию моего приложения без необходимости создавать 9 реализаций, по одной для каждой комбинации.
Я предполагаю, что не могу передать никакие параметры, потому что интерфейсы разрешены инжектором (контейнером).
Сначала я думал об этом:
public interface IApplicationSettings<TEnableLogging,TCopyLocal,TServerName>
where TEnableLogging : IGetValue<bool>
where TCopyLocal : IGetValue<bool>
where TServerName : IGetValue<string>
{
TEnableLogging EnableLog{get;}
TCopyLocal CopyLocal{get;}
TServerName ServerName{get;}
}
public class ApplicationSettings<TEnableLogging,TCopyLocal,TServerName>
{
private bool? enableLogging;
public bool EnableLogging
{
get
{
if (enableLogging == null)
{
enableLogging = Container.GetInstance<TEnableLogging>().Value
}
return enableLogging;
}
}
}
Тем не менее, с этим у меня есть одна главная проблема: как я знаю, как создать экземпляр TEnableLogging
(который является IGetValue<bool>
)? О, предположим, что IGetValue<bool>
интерфейс, который имеет свойство Value, которое будет реализовано конкретным классом Но конкретному классу может потребоваться некоторая специфика (например, как называется ключ в app.config) или нет (я могу просто захотеть всегда возвращать true).
Я относительно новичок в внедрении зависимости, так что, возможно, я ошибаюсь. У кого-нибудь есть идеи, как этого добиться?
(Вы можете ответить, используя другую библиотеку DI, я не буду возражать. Я думаю, мне просто нужно понять концепцию этого.)
1 ответ
Вы определенно идете неправильным путем здесь.
Несколько лет назад я создал приложение, которое содержало интерфейс, очень похожий на ваш IApplicationSettings
, Я думаю, что я назвал это IApplicationConfiguration
, но он также содержал все значения конфигурации приложения.
Хотя поначалу это помогло мне сделать приложение тестируемым, через некоторое время дизайн начал мешать. От этого интерфейса зависело множество реализаций, но он постоянно менялся, а вместе с ним - реализация и тестовая версия.
Как и вы, я реализовал некоторую ленивую загрузку, но это имело ужасную обратную сторону. Когда одно из значений конфигурации отсутствовало, я обнаружил, что это произошло только при первом вызове значения. Это привело к конфигурации, которую было трудно проверить.
Мне потребовалось несколько итераций рефакторинга, в чем суть проблемы. Большие интерфейсы являются проблемой. мой IApplicationConfiguration
класс нарушал принцип сегрегации интерфейса, и результатом была плохая ремонтопригодность.
В конце концов я обнаружил, что этот интерфейс совершенно бесполезен. Помимо нарушения интернет-провайдера, эти значения конфигурации описывают детали реализации, и вместо того, чтобы делать абстракцию всего приложения, было гораздо лучше предоставить каждой реализации непосредственно требуемое значение конфигурации.
Когда вы делаете это, проще всего передать это значение конфигурации в качестве значения свойства. С простым инжектором вы можете использовать RegisterInitializer
метод для этого:
var enableLogging =
Convert.ToBoolean(ConfigurationManager.AppSettings["EnableLogging"]);
container.RegisterInitializer<Logger>(logger =>
{
logger.EnableLogging = enableLogging;
});
Делая это, enableLogging
значение считывается только один раз из файла конфигурации и выполняется во время запуска приложения. Это делает его быстрым и дает сбой при запуске приложения, когда значение отсутствует.
Если по какой-то причине вам нужно отложить чтение (например, из базы данных), вы можете использовать Lazy<T>
:
Lazy<bool> copyLocal = new Lazy<bool>(() =>
container.GetInstance<IDatabaseManager>().RunQuery(CopyLocalQuery));
container.RegisterInitializer<FileCopier>(copier =>
{
copier.CopyLocal = copyLocal.Value;
});
Вместо передачи значений с помощью свойств вы также можете использовать аргументы конструктора, но это немного сложнее достичь. Взгляните на эту статью для некоторых идей.
Если вы обнаружите, что регистрируете одно и то же значение конфигурации для многих регистраций, вы, вероятно, пропускаете абстракцию. Взгляните на это:
container.RegisterInitializer<UserRepository>(rep => {
rep.ConnectionString = connectionString; });
container.RegisterInitializer<OrderRepository>(rep => {
rep.ConnectionString = connectionString; });
container.RegisterInitializer<CustomerRepository>(rep => {
rep.ConnectionString = connectionString; });
container.RegisterInitializer<DocumentRepository>(rep => {
rep.ConnectionString = connectionString; });
В этом случае вы, вероятно, пропускаете IDatabaseFactory
или же IDatabaseManager
абстракция, и вы должны сделать что-то вроде этого:
container.RegisterSingle<IDatabaseFactory>(new SqlDatabaseFactory(connectionString));