Инъекция зависимостей и настройки AppSettings

Скажем, я определяю класс реализации браузера для своего приложения:

class InternetExplorerBrowser : IBrowser {
    private readonly string executablePath = @"C:\Program Files\...\...\ie.exe";

    ...code that uses executablePath
}

На первый взгляд это может показаться хорошей идеей, так как данные executablePath находятся рядом с кодом, который будет использовать его.

Проблема возникает, когда я пытаюсь запустить это же приложение на моем другом компьютере, на котором есть ОС на внешнем языке: executablePath будет иметь другое значение.

Я мог бы решить это через класс Singleton AppSettings (или один из его эквивалентов), но тогда никто не знает, что мой класс на самом деле зависит от этого класса AppSettings (что противоречит идеям DI). Это может также затруднить тестирование Unit-Testing.

Я мог бы решить обе проблемы, передав executablePath через конструктор:

class InternetExplorerBrowser : IBrowser {
    private readonly string executablePath;

    public InternetExplorerBrowser(string executablePath) {
        this.executablePath = executablePath;
    }
}

но это вызовет проблемы в моем Composition Root (метод запуска, который сделает все необходимые проводки классов), так как тогда этот метод должен знать, как подключаться к ним, и знать все эти данные небольших настроек:

class CompositionRoot {
    public void Run() {
        ClassA classA = new ClassA();

        string ieSetting1 = "C:\asdapo\poka\poskdaposka.exe";
        string ieSetting2 = "IE_SETTING_ABC";
        string ieSetting3 = "lol.bmp";

        ClassB classB = new ClassB(ieSetting1);
        ClassC classC = new ClassC(B, ieSetting2, ieSetting3);

        ...
    }
}

который легко превратится в большой беспорядок.

Я мог бы обратиться к этой проблеме, вместо этого передав интерфейс формы

interface IAppSettings {
    object GetData(string name);
}

для всех классов, для которых требуются какие-то настройки. Затем я мог либо реализовать это, либо как обычный класс со всеми встроенными в него параметрами, либо классом, который считывает данные из файла XML, что-то вроде строк. Если это необходимо, должен ли я иметь общий класс класса AppSettings для всей системы или иметь класс AppSettings, связанный с каждым классом, который может понадобиться? Это, безусловно, похоже на перебор. Кроме того, все настройки приложения в одном и том же месте облегчают просмотр и видят, какие могут быть все изменения, которые мне нужно сделать, когда tryign перемещает программу на разные платформы.

Что может быть лучшим способом приблизиться к этой общей ситуации?

Изменить:

А как насчет использования IAppSettings со всеми его настройками, жестко закодированными в нем?

interface IAppSettings {
    string IE_ExecutablePath { get; }
    int IE_Version { get; }
    ...
}

Это позволит обеспечить безопасность во время компиляции. Если бы я увидел, что классы интерфейса/конкретного класса слишком сильно растут, я мог бы создать другие меньшие интерфейсы формы IMyClassXAppSettings. Будет ли это слишком тяжелым бременем для медсёмочных проектов?

Я также читаю об АОП и его преимуществах, связанных с сквозными проблемами (я думаю, это один). Не могли бы также предложить решения этой проблемы? Возможно, помечают такие переменные:

class InternetExplorerBrowser : IBrowser {
    [AppSetting] string executablePath;
    [AppSetting] int ieVersion;

    ...code that uses executablePath
}

Затем, при компиляции проекта, у нас также была бы безопасность времени компиляции (когда компилятор проверял, что мы действительно реализовали код, который будет переплетаться с данными. Это, конечно же, связывает наш API с этим конкретным аспектом.

Ответ 1

Отдельные классы должны быть как можно более свободными от инфраструктуры - такие конструкции, как IAppSettings, IMyClassXAppSettings, и [AppSetting] описывают детали композиции для классов, которые в своих простейших действиях зависят только от исходных значений, таких как executablePath. Искусство "Инъекции зависимостей" относится к факторингу проблем.

Я реализовал этот точный шаблон с помощью Autofac, который имеет модули, подобные Ninject, и должен привести к аналогичному коду (я понимаю вопрос не упоминает Ninject, но OP делает в комментарии).

Модули организуют приложения подсистемой. Модуль предоставляет настраиваемые элементы подсистемы:

public class BrowserModule : Module
{
    private readonly string _executablePath;

    public BrowserModule(string executablePath)
    {
        _executablePath = executablePath;
    }

    public override void Load(ContainerBuilder builder)
    {
        builder
            .Register(c => new InternetExplorerBrowser(_executablePath))
            .As<IBrowser>()
            .InstancePerDependency();
    }
}

Это оставляет корень композиции с той же проблемой: он должен указать значение executablePath. Чтобы избежать суп конфигурации, мы можем написать автономный модуль, который считывает настройки конфигурации и передает их в BrowserModule:

public class ConfiguredBrowserModule : Module
{
    public override void Load(ContainerBuilder builder)
    {
        var executablePath = ConfigurationManager.AppSettings["ExecutablePath"];

        builder.RegisterModule(new BrowserModule(executablePath));
    }
}

Вы можете использовать пользовательский раздел конфигурации вместо AppSettings; изменения будут локализованы в модуле:

public class BrowserSection : ConfigurationSection
{
    [ConfigurationProperty("executablePath")]
    public string ExecutablePath
    {
        get { return (string) this["executablePath"]; }
        set { this["executablePath"] = value; }
    }
}

public class ConfiguredBrowserModule : Module
{
    public override void Load(ContainerBuilder builder)
    {
        var section = (BrowserSection) ConfigurationManager.GetSection("myApp.browser");

        if(section == null)
        {
            section = new BrowserSection();
        }

        builder.RegisterModule(new BrowserModule(section.ExecutablePath));
    }
}

Это хороший шаблон, потому что каждая подсистема имеет независимую конфигурацию, которая считывается в одном месте. Единственное преимущество здесь - более очевидное намерение. Однако для значений string или сложных схем мы можем позволить System.Configuration выполнять тяжелый подъем.

Ответ 2

Я бы воспользовался последней опцией - передать объект, соответствующий интерфейсу IAppSettings. Фактически, я недавно исполнил этот рефактор на работе, чтобы разобраться в некоторых модульных тестах, и он работал хорошо. Однако в этом проекте было несколько классов, зависящих от настроек.

Я хотел бы создать один экземпляр класса настроек и передать его во все, что зависит от него. Я не вижу принципиальной проблемы с этим.

Однако, я думаю, вы уже об этом подумали и видели, как это может быть больно, если у вас много классов, зависящих от настроек.

Если это проблема для вас, вы можете обойти ее, используя инфраструктуру инъекции зависимостей, такую ​​как ninject (извините, если вы уже знаете о таких проектах, как ninject - это может звучать немного покровительствовать - если вы незнакомы, зачем использовать ninject на github это хорошее место для изучения).

Используя ninject, для вашего основного проекта вы можете объявить, что хотите, чтобы какой-либо класс с зависимостью от IAppSettings использовал одноэлементный экземпляр вашего класса на основе AppSettings без необходимости явно передавать его конструкторам везде.

Затем вы можете настроить свою систему по-разному для своих модульных тестов, заявив, что вы хотите использовать экземпляр MockAppSettings везде, где используется IAppSettings, или просто явно передавая ваши макетные объекты напрямую.

Надеюсь, у меня есть суть вашего вопроса, и я помог - вы уже знаете, что знаете, что делаете:)