Является ли один объект конфигурации плохой идеей?


43

В большинстве моих приложений у меня есть одноэлементный или статический объект «config», отвечающий за чтение различных настроек с диска. Почти все классы используют его для различных целей. По сути, это просто хеш-таблица пар имя / значение. Это только для чтения, поэтому меня не слишком беспокоит тот факт, что у меня так много глобального состояния. Но теперь, когда я начинаю с модульного тестирования, это начинает становиться проблемой.

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

  • Дайте объекту конфигурации установщик, который используется ТОЛЬКО для тестирования, чтобы вы могли передавать различные настройки.
  • Продолжайте использовать один объект конфигурации, но измените его с одиночного на экземпляр, который вы передаете везде, где это необходимо. Затем вы можете создать его один раз в своем приложении и один раз в своих тестах с различными настройками.

Но в любом случае, у вас все еще есть вторая проблема: почти любой класс может использовать объект конфигурации. Поэтому в тесте вам нужно настроить конфигурацию для тестируемого класса, а также ВСЕ его зависимости. Это может сделать ваш тестовый код безобразным.

Я начинаю приходить к выводу, что такого рода объект конфигурации является плохой идеей. Что вы думаете? Какие есть альтернативы? И как начать рефакторинг приложения, которое везде использует конфигурацию?


Конфиг получает свои настройки, читая файл на диске, верно? Так почему бы просто не иметь файл "test.config", который он может прочитать?
Анон.

Это решает первую проблему, но не вторую.
JW01

@ JW01: Все ли ваши тесты нуждаются в заметно другой конфигурации? Вам придется настроить эту конфигурацию где-то во время теста, не так ли?
Анон.

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

«Таким образом, в тесте вам нужно настроить конфигурацию для тестируемого класса, а также ВСЕ его зависимости. Это может сделать ваш тестовый код уродливым». Есть пример? У меня такое ощущение, что не структура всего вашего приложения, соединяющегося с классом config, а скорее структура самого класса config делает вещи "уродливыми". Разве настройка конфигурации класса не должна автоматически настраивать его зависимости?
AndrewKS

Ответы:


35

У меня нет проблем с одним объектом конфигурации, и я вижу преимущество в хранении всех настроек в одном месте.

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

Один из способов справиться с этим - создать несколько интерфейсов, которые предоставляют части объекта конфигурации, необходимые для различных частей приложения. Вместо того, чтобы позволять другим классам обращаться к объекту config, вы передаете экземпляры интерфейса тем классам, которые в этом нуждаются. Таким образом, части приложения, которые используют config, зависят от меньшего набора полей в интерфейсе, а не от всего класса config. Наконец, для модульного тестирования вы можете создать собственный класс, который реализует интерфейс.

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


7

Я разделяю группы связанных настроек с интерфейсами.

Что-то типа:

public interface INotificationEmailSettings {
   public string To { get; set; }
}

public interface IMediaFileSettings {
    public string BasePath { get; set; }
}

и т.п.

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

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

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

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

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

public static class AllSettings {
    public INotificationEmailSettings NotificationEmailSettings {
        get {
            return ServiceLocator.Get<INotificationEmailSettings>();
        }
    }
}

Я считаю этот микс лучшим из всех миров.


3

Да, как вы поняли, глобальный объект конфигурации затрудняет модульное тестирование. Наличие «секретного» установщика для модульного тестирования - это быстрый взлом, который, хотя и не очень приятен, может быть очень полезен: он позволяет вам начинать писать модульные тесты, чтобы вы могли со временем реорганизовать свой код для улучшения дизайна.

(Обязательная ссылка: Эффективная работа с устаревшим кодом . Он содержит этот и многие другие бесценные приемы для модульного тестирования унаследованного кода.)

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

Это уже приближается к настоящему решению для внедрения зависимостей, такому как Spring for Java. Если вы можете перейти на такие рамки, хорошо. Но в реальной жизни, особенно при работе с устаревшими приложениями, зачастую медленный и тщательный рефакторинг в сторону DI является наилучшим достижимым компромиссом.


Мне очень понравился ваш подход, и я бы не советовал, чтобы идея сеттера оставалась "секретной" или считалась "быстрым взломом" в этом отношении. Если вы придерживаетесь позиции, что модульные тесты являются важным пользователем / потребителем / частью вашего приложения, то наличие test_атрибутов и методов уже не так уж плохо, верно? Я согласен с тем, что подлинным решением проблемы является использование структуры DI, но в примерах, подобных вашему, при работе с унаследованным кодом или в других простых случаях не отчуждение тестового кода применяется корректно.
Филипп Дупанович

1
@kRON, это очень полезные и практичные приемы для того, чтобы сделать устаревший код тестируемым, и я также широко их использую. Однако в идеале рабочий код не должен содержать каких-либо функций, введенных исключительно для целей тестирования. В конечном счете, это то, к чему я отношусь с рефакторингом.
Петер Тёрёк

2

По моему опыту, в мире Java это то, для чего предназначен Spring. Он создает и управляет объектами (bean-компонентами) на основе определенных свойств, заданных во время выполнения, и в остальном прозрачен для вашего приложения. Вы можете пойти с файлом test.config, который Anon. упоминает или вы можете включить некоторую логику в конфигурационный файл, который Spring будет обрабатывать для установки свойств на основе некоторого другого ключа (например, имени хоста или среды).

Что касается вашей второй проблемы, вы можете обойти ее с помощью некоторой реархитектуры, но она не так серьезна, как кажется. В вашем случае это означает, что у вас не будет, например, глобального объекта Config, который используют различные другие классы. Вы просто сконфигурируете эти другие классы через Spring, а затем не получите объект конфигурации, потому что все, что требовало настройки, получили его через Spring, так что вы можете использовать эти классы непосредственно в своем коде.


2

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

Паттерн «Синглтон» считается «вредным». Это означает, что это не всегда неправильно, но обычно так и есть. Если вы когда - либо планируете использовать одноплодный шаблон для ничего , остановки рассмотреть следующие вопросы:

  • Вам когда-нибудь понадобится создать подкласс?
  • Вам когда-нибудь понадобится программировать на интерфейс?
  • Вам нужно запустить юнит-тесты против него?
  • Вам нужно будет часто его менять?
  • Ваше приложение предназначено для поддержки нескольких производственных платформ / сред?
  • Вы даже немного обеспокоены использованием памяти?

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

Если вы хотите получить практически все преимущества синглтона без каких-либо трудностей, используйте среду внедрения зависимостей, такую ​​как Spring или Castle, или любую другую, доступную для вашей среды. Таким образом, вам нужно всего лишь объявить его один раз, и контейнер автоматически предоставит экземпляр тому, что ему нужно.


0

Один из способов, которым я справился с этим в C #, - иметь одноэлементный объект, который блокируется во время создания экземпляра и инициализирует все данные сразу для использования всеми клиентскими объектами. Если этот синглтон может обрабатывать пары ключ / значение, вы можете хранить любые данные, включая сложные ключи, для использования многими различными клиентами и типами клиентов. Если вы хотите сохранить его динамическим и загружать новые данные клиента по мере необходимости, вы можете проверить существование главного ключа клиента и, в случае его отсутствия, загрузить данные для этого клиента, добавив его в основной словарь. Также возможно, что первичный конфигурационный синглтон может содержать наборы клиентских данных, где мультипликаторы одного и того же типа клиента используют одни и те же данные, все из которых доступны через первичный конфигурационный синглтон. Большая часть этой организации объекта конфигурации может зависеть от того, как ваши клиенты конфигурации должны получить доступ к этой информации и является ли эта информация статической или динамической. Я использовал как конфиги ключ / значение, так и конкретные объектные API, в зависимости от того, что было необходимо.

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

При загрузке из конфигурационных файлов вы можете иметь файловую иерархию. Настройки в одном файле могут указывать, какие из других файлов загружать. Я использовал этот механизм со службами C # Windows, которые имеют несколько дополнительных компонентов, каждый со своими настройками конфигурации. Шаблоны структуры файлов были одинаковыми для всех файлов, но были загружены или не основаны на настройках первичного файла.


0

Класс, который вы описываете, звучит как антипаттерн объекта God, за исключением данных вместо функций. Это проблема. Вероятно, вы должны прочитать данные конфигурации и сохранить их в соответствующем объекте, при этом читая данные снова, только если это необходимо по какой-то причине.

Кроме того, вы используете Singleton по причине, которая не подходит. Синглтоны следует использовать только тогда, когда существование нескольких объектов создаст плохое состояние. Использование Singleton в этом случае неуместно, так как наличие более одного считывателя конфигурации не должно сразу вызывать состояние ошибки. Если у вас их больше одного, вы, вероятно, делаете это неправильно, но не обязательно, чтобы у вас был только один из считывателей конфигурации.

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


0

Я думаю, что если есть много настроек с «скоплениями» связанных, имеет смысл разделить их на отдельные интерфейсы с соответствующими реализациями, а затем внедрить эти интерфейсы там, где это необходимо, с помощью DI. Вы получаете тестируемость, низкую связь и SRP.

Меня вдохновил этот вопрос Джошуа Фланаган из Los Techies; он написал статью по этому вопросу некоторое время назад.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.