Как, используя внедрение зависимостей, получить конфигурацию из нескольких источников?

Я использую 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));
Другие вопросы по тегам