Внедрение зависимостей и AppSettings - PullRequest
11 голосов
/ 27 августа 2010

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

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

    ...code that uses executablePath
}

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

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

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

Я мог бы решить обе проблемы, передав 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, связанный с каждым классом, который может понадобиться? Это, конечно, кажется излишним. Кроме того, наличие всех настроек приложения в одном месте облегчает поиск и просмотр возможных изменений, которые необходимо выполнить при попытке перенести программу на другие платформы.

Как лучше всего подойти к этой общей ситуации?

Edit:

А как насчет использования 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 к этому конкретному аспекту.

Ответы [ 2 ]

14 голосов
/ 29 августа 2010

Отдельные классы должны быть максимально свободны от инфраструктуры - такие конструкции, как 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 голосов
/ 28 августа 2010

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

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

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

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

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

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

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

...