На самом деле это действительно важный вопрос, и он часто делается неправильно, поскольку ему не уделяется достаточного внимания, хотя он является основной частью практически каждого приложения. Вот мои рекомендации:
Ваш конфигурационный класс, который содержит все настройки, должен быть просто старого типа данных struct / class:
class Config {
int prop1;
float prop2;
SubConfig subConfig;
}
Он не должен иметь методов и не должен включать наследование (если это не единственный выбор, который у вас есть на вашем языке для реализации варианта поля - см. Следующий параграф). Он может и должен использовать композицию для группировки настроек в более мелкие конкретные классы конфигурации (например, subConfig выше). Если вы сделаете это таким образом, то будет идеальным переходить в модульных тестах и в приложении в целом, поскольку оно будет иметь минимальные зависимости.
Скорее всего, вам придется использовать типы вариантов, в случае, если конфигурации для разных настроек неоднородны по структуре. Принято считать, что вам нужно поместить динамическое приведение в какой-то момент, когда вы прочитаете значение, чтобы привести его к нужному (под) классу конфигурации, и, без сомнения, это будет зависеть от другого параметра конфигурации.
Вам не следует лениться вводить все настройки в виде полей, просто выполнив это:
class Config {
Dictionary<string, string> values;
};
Это заманчиво, так как это означает, что вы можете написать обобщенный класс сериализации, которому не нужно знать, с какими полями он имеет дело, но это неправильно, и я объясню почему через минуту.
Сериализация конфига производится в совершенно отдельном классе. Какой бы API или библиотеку вы не использовали для этого, тело вашей функции сериализации должно содержать записи, которые в основном равносильны отображению пути / ключа в файле к полю на объекте. Некоторые языки обеспечивают хороший самоанализ и могут сделать это для вас из коробки, другие вам придется явно написать отображение, но главное, что вам нужно будет написать отображение только один раз. Например, рассмотрим этот фрагмент, который я адаптировал из документации синтаксического анализатора опций программы c ++ boost:
struct Config {
int opt;
} conf;
po::options_description desc("Allowed options");
desc.add_options()
("optimization", po::value<int>(&conf.opt)->default_value(10);
Обратите внимание, что в последней строке в основном говорится, что «оптимизация» отображается на Config :: opt, а также что есть объявление того типа, который вы ожидаете. Вы хотите, чтобы чтение конфигурации не выполнялось, если тип не соответствует ожидаемому, если параметр в файле на самом деле не является float или int или не существует. Т.е. сбой должен произойти, когда вы читаете файл, потому что проблема связана с форматом / проверкой файла, и вы должны выбросить код исключения / возврата и сообщить точную проблему. Вы не должны откладывать это позже в программе. Вот почему у вас не должно быть соблазна перехватить весь словарь в стиле Conf, как упомянуто выше, который не потерпит неудачу при чтении файла - поскольку приведение откладывается до тех пор, пока не понадобится значение.
Вы должны сделать класс Config доступным только для чтения некоторым способом - устанавливая содержимое класса один раз, когда вы создаете его и инициализируете его из файла. Если вам необходимо иметь динамические настройки в вашем приложении, которые изменяются, а также постоянные, которые этого не делают, у вас должен быть отдельный класс для обработки динамических, а не пытаться разрешить битам вашего класса конфигурации быть не только для чтения ,
В идеале вы читаете файл в одном месте вашей программы, т.е. у вас есть только один экземпляр " ConfigReader
". Однако, если вы боретесь с передачей экземпляра Config туда, где вам это нужно, лучше иметь второй ConfigReader, чем вводить глобальный конфиг (который, я предполагаю, означает, что OP означает «статический» "), что подводит меня к следующему пункту:
Избегайте соблазнительной сирены: «Я избавлю вас от необходимости проходить этот урок, все ваши конструкторы будут красивыми и чистыми. Продолжайте, это будет так просто». Правда в том, что с хорошо разработанной тестируемой архитектурой вам вряд ли понадобится проходить класс Config или его части через множество классов вашего приложения. То, что вы найдете в своем классе верхнего уровня, своей функции main () или что-то еще, вы распутаете conf в отдельные значения, которые вы предоставите своим классам компонентов в качестве аргументов, которые вы затем соберете вместе (зависимость вручную) инъекции). Одноэтапный / глобальный / статический conf значительно усложнит реализацию и понимание модульного тестирования вашего приложения - например, он запутает новых разработчиков в вашей команде, которые не будут знать, что им нужно устанавливать глобальное состояние для тестирования.
Если ваш язык поддерживает свойства, вы должны использовать их для этой цели. Причина в том, что это означает, что будет очень легко добавить «производные» параметры конфигурации, которые зависят от одного или нескольких других параметров. например
int Prop1 { get; }
int Prop2 { get; }
int Prop3 { get { return Prop1*Prop2; }
Если ваш язык изначально не поддерживает идиому свойства, возможно, для достижения того же эффекта есть обходной путь, или вы просто создаете класс-оболочку, который предоставляет настройки бонусов. Если вы не можете иным образом использовать преимущества свойств, в противном случае это будет пустой тратой времени на написание вручную и использование методов получения / установки просто для того, чтобы угодить некоему ОО-богу. Вам будет лучше с простым старым полем.
Вам может понадобиться система для объединения и получения нескольких конфигов из разных мест в порядке приоритета. Этот порядок приоритета должен быть четко определен и понятен всем разработчикам / пользователям, например, рассмотреть реестр Windows HKEY_CURRENT_USER / HKEY_LOCAL_MACHINE. Вы должны сделать этот функциональный стиль, чтобы ваши конфиги оставались только для чтения, т.е.
final_conf = merge(user_conf, machine_conf)
скорее, чем:
conf.update(user_conf)
Наконец, я должен добавить, что, конечно, если выбранный вами фреймворк / язык предоставляет собственные встроенные, хорошо известные механизмы конфигурации, вы должны рассмотреть преимущества использования этого вместо того, чтобы использовать свой собственный.
Так. Множество аспектов для рассмотрения - сделайте это правильно, и это окажет глубокое влияние на архитектуру вашего приложения, уменьшая количество ошибок, делая вещи легко тестируемыми и заставляя вас использовать хороший дизайн в другом месте.