Эта проблема

У нас есть много кода C #, который хочет получить доступ к какой-то конфигурации, и все это делается с помощью:

ConfigurationManager.AppSettings["SomeSettingOrOther"]

Например, у нас был класс CurrencyConversion со следующим кодом.

public class CurrencyConversion
{
    Currency GetDefaultCurrency() {
        // get the config setting
        string configCurrency = ConfigurationManager.AppSettings["MarketCurrency"];
        // return the equivalent Enum
        return CurrencyFromString(configCurrency);
    }
}

В этом коде есть несколько проблем.

  • Его зависимость от параметра MarketCurrency неявна (т. Е. Мы не можем узнать это, посмотрев на открытый интерфейс класса, мы должны заглянуть внутрь).
  • Он может генерировать исключения, если параметр конфигурации отсутствует или неправильно отформатирован, и мы узнаем это только при запуске этого конкретного фрагмента кода.
  • Могут быть другие части кода, которые также используют этот параметр конфигурации и будут повторять преобразование строки в Enum и любую обработку ошибок.
  • Возможно, он использует неправильную настройку конфигурации (или повторно использует существующую настройку для удобства), и правильнее было бы использовать DefaultCurrency. Мы не можем узнать это, глядя на код, а знания находятся только в головах разработчиков.
  • Тестирование затруднено, так как нам нужно настроить ConfigurationManager, и, вероятно, мы прибегаем к методам проб и ошибок при определении того, какая конфигурация требуется.
  • Источник конфигурации (ConfigurationManager) жестко запрограммирован и может варьироваться в зависимости от кодовой базы (то есть с некоторыми методами, использующими ConfigurationManager, и некоторыми методами, считывающими переменные среды)

Были и проблемы более высокого уровня. Эти вызовы распространялись по всему коду, иногда глубоко в разделяемых библиотеках, и было невозможно узнать, какие фрагменты кода и какие настройки требовали (без чтения всего кода).

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

Решение

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

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

public interface ICurrencyConversionConfiguration
{
    Currency DefaultCurrency;
}
public class CurrencyConversion
{
    readonly ICurrencyConversionConfiguration configuration;
    public CurrencyConversion(ICurrencyConversionConfiguration configuration) {
        Contract.Requires(configuration != null);
        this.configuration = configuration;
    }
    Currency GetDefaultCurrency() {
        return configuration.DefaultCurrency;
    }
}

В этом коде есть следующие улучшения

  • Его зависимость от DefaultCurrency теперь очевидна. Без него невозможно создать класс.
  • Зависимость от DefaultCurrency удовлетворяется посредством внедрения конструктора.
  • Mock ICurrencyConversionConfiguration при тестировании легко.
  • Название последовательное.
  • Снята ответственность за чтение и анализ конфигурации.

Чтобы взять на себя ответственность за чтение и анализ конфигурации, мы добавляем код ниже. Это зависит от простого класса Configuration, который вы можете увидеть на GitHub по адресу https://github.com/resgroup/configuration.

public class EconomicModelConfiguration : ICurrencyConversionConfiguration 
{
    readonly Configuration configuration;
    
    public EconomicModelConfiguration(Configuration configuration) {
        Contract.Requires(configuration != null);
        this.configuration = configuration;
    
        Validate();
    }
    
    void Validate() =>
        using (var validator = configuration.CreateValidator)
            validator.Check(() => DefaultCurrency);
    public string DefaultCurrency => 
        configuration.GetEnum<Currency>(MethodBase.GetCurrentMethod());
}

Сам класс конфигурации создается с помощью источника конфигурации, который считывается из переменных среды в приведенном ниже примере. Это позволяет легко придерживаться рекомендаций 12-факторного приложения (https://12factor.net/config).

new Configuration(new GetFromEnvironment());

Это имеет следующие улучшения

  • Параметры конфигурации проверяются при создании класса (который должен находиться в точке входа приложения), поэтому любые проблемы с конфигурацией немедленно удаляются.
  • Код для преобразования строки в валюту централизован.
  • Источник конфигурации инкапсулирован.

Если мы переместим другой класс для использования новой системы конфигурации, мы получим что-то вроде этого.

public class EconomicModelConfiguration : ICurrencyConversionConfiguration, IConcreteCostConfiguration 
{
    readonly Configuration configuration;
    
    public EconomicModelConfiguration(Configuration configuration) {
        Contract.Requires(configuration != null);
        this.configuration = configuration;
    
        Validate();
    }
    
    void Validate() {
        using (var validator = configuration.CreateValidator) {
            validator.Check(() => DefaultCurrency);
            validator.Check(() => DefaultConcreteCost);
        }
    }
    public string DefaultCurrency => 
        configuration.GetEnum<Currency>(MethodBase.GetCurrentMethod());
    public double DefaultConcreteCost => 
        configuration.GetDouble(MethodBase.GetCurrentMethod());
}

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

Устаревший код

Как и у любой зрелой команды разработчиков программного обеспечения, у нас есть унаследованный код, часть которого еще не готова для создания с помощью инверсии управления.

Для этих классов мы создаем статический класс конфигурации.

public static class EconomicModelConfigurationStatic
{
    readonly static EconomicModelConfiguration base = new EconomicModelConfiguration();
    public static IEconomicModelConfiguration Settings =>
        base;
}

Затем в устаревшем коде мы заменяем

ConfigurationManager.AppSettings["SomeSettingOrOther"]

с участием

EconomicModelConfigurationStatic.Settings.SomeSettingOrOther

Это дает нам множество преимуществ новой системы с очень незначительными и легкими изменениями в существующем коде.

Выводы

Такая инкапсуляция логики конфигурации и предоставление конфигурации через интерфейс дает следующие преимущества.

  • Здесь нет волшебных строк, поэтому любые орфографические ошибки будут обнаружены во время компиляции.
  • Можно использовать инструменты рефакторинга (например, rename) и гарантировать, что все экземпляры обновлены
  • Все ссылки на элемент конфигурации можно легко найти с помощью Visual Studio.
  • Код явно указывает на требуемую конфигурацию и может определять только ту часть конфигурации, которая ему нужна.
  • Файлы конфигурации можно проверить, чтобы убедиться, что они содержат всю необходимую информацию.
  • Файлы конфигурации можно проверить, чтобы увидеть, содержат ли они лишнюю информацию.
  • Логика конфигурации, такая как значения по умолчанию и преобразование, обрабатывается централизованно.
  • Элементы конфигурации гарантированно имеют одно и то же имя в файле конфигурации и в коде.

Если вы хотите его использовать, имеется пакет nuget, а исходный код находится на GitHub.