Где загрузить и сохранить настройки из файла?

9

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

  • Если у программы был простой settings.iniфайл, следует ли загружать его содержимое в load()метод класса или, возможно, в конструктор?
  • Должны ли значения храниться в public staticпеременных или должны быть staticметоды для получения и установки свойств?
  • Что должно произойти, если файл не существует или не читается? Как бы вы сообщили остальной части программы, что она не может получить эти свойства?
  • и т.п.

Я надеюсь, что я спрашиваю это в нужном месте здесь. Я хотел сделать вопрос как можно более независимым от языка, но я в основном сосредоточился на языках, имеющих такие вещи, как наследование, особенно на Java и C # .NET.

Энди
источник
1
Для .NET лучше всего использовать App.config и класс System.Configuration.ConfigurationManager для получения настроек
Гибсон,
@Gibson Я только начинаю работать в .NET, поэтому не могли бы вы дать мне ссылку на какие-нибудь хорошие учебники для этого класса?
Энди
Stackoverflow имеет много ответов на него. Быстрый поиск показывает: stackoverflow.com/questions/114527/… и stackoverflow.com/questions/13043530/… Там также будет много информации о MSDN, начиная здесь msdn.microsoft.com/en-us/library/ms228063(v = vs.100) .aspx и msdn.microsoft.com/en-us/library/ms184658(v=vs.110).aspx
Гибсон,
@Gibson Спасибо за эти ссылки. Они будут очень полезны.
Энди

Ответы:

8

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

Ваш конфигурационный класс, который содержит все настройки, должен быть просто старого типа данных 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)

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

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

Бенедикт
источник
+1 Спасибо за ответ. Ранее, когда я сохранил пользовательские настройки, я только что прочитал из файла, такого как, .iniчтобы он был удобочитаемым для человека, но вы предлагаете мне сериализовать класс с переменными в?
Энди
.ini легко разбирать, и есть также много API, которые включают .ini в качестве возможного формата. Я предлагаю вам использовать такие API, чтобы помочь вам выполнить основную работу по синтаксическому анализу текста, но в конечном итоге вы должны инициализировать класс POD. Я использую термин сериализация в общем смысле копирования или чтения в полях класса из какого-либо формата, а не в более конкретном определении сериализации класса непосредственно в / из двоичного файла (как обычно понимается, скажем, для java.io.Serializable). ).
Бенедикт
Теперь я понимаю немного лучше. То есть, по сути, вы говорите, что у вас есть один класс со всеми полями / настройками, а у другого - установить значения в этом классе из файла? Тогда как мне получить значения из первого класса в других классах?
Энди
От случая к случаю. Некоторые из ваших классов верхнего уровня могут просто нуждаться в ссылке на весь экземпляр Config, передаваемый в них, если они полагаются на очень много параметров. Другим классам, возможно, нужен только один или два параметра, но нет необходимости связывать их с объектом Config (что делает их еще проще тестировать), просто передайте несколько параметров через ваше приложение. Как упоминалось в моем ответе, если вы строите с помощью тестируемой / DI-ориентированной архитектуры, получить значения там, где они вам нужны, обычно не составит труда. Используйте глобальный доступ на свой страх и риск.
Бенедикт
Итак, как бы вы предположили, что объект config передается классам, если не нужно задействовать наследование? Через конструктор? Итак, в основном методе программы инициируется класс считывателя, который сохраняет значения в классе Config, затем основной метод передает объект Config или отдельные переменные другим классам?
Энди
4

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

Кроме того, на приложение ложится бремя «что происходит, когда что-то не получается», кто лучше знает, что означает этот сбой.

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

Telastyn
источник
Спасибо за ваш ответ, но я не совсем понимаю. Я говорю о том, когда пишу приложение, поэтому ничего не нужно иметь дело с конфигурацией, и поэтому, как программист, я знаю, что означает сбой.
Энди
@andy - конечно, но если модули обращаются к конфигурации напрямую, они не знают, в каком контексте они работают, поэтому они не могут определить, как решать проблемы конфигурации. Если приложение выполняет загрузку, оно может решать любые проблемы, поскольку оно знает контекст. Некоторые приложения могут захотеть
переключиться
Просто чтобы уточнить здесь, под модулями вы ссылаетесь на классы или на реальные расширения программы? Потому что я спрашиваю, где лучше всего хранить настройки внутри кода в основной программе и как разрешить доступ к настройкам другим классам. Поэтому я хочу загрузить файл конфигурации и затем сохранить настройку где-нибудь / каким-то образом, чтобы мне не приходилось ссылаться на файл.
Энди
0

Если вы используете .NET для программирования классов, у вас есть разные опции, такие как Resources, web.config или даже пользовательский файл.

Если вы используете Resources или web.config, данные на самом деле хранятся в файле XML, но загрузка происходит быстрее.

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

Для любого другого файла или языка программирования ответ выше Бенедикт будет работать.

Киран
источник
Спасибо за ваш ответ и извините за медленный ответ. Использование ресурсов было бы хорошим вариантом, но я только что понял, что это, вероятно, не лучший вариант для меня, поскольку я планирую разработать ту же программу на других языках, поэтому я бы предпочел иметь файл XML или JSON, но Прочитайте его в моем собственном классе, чтобы этот же файл можно было прочитать на других языках программирования.
Энди