Gdzie załadować i zapisać ustawienia z pliku?

9

Myślę, że to pytanie powinno dotyczyć większości programów, które ładują ustawienia z pliku. Moje pytanie jest z punktu widzenia programowania i naprawdę chodzi o to, jak poradzić sobie z ładowaniem ustawień z pliku pod względem różnych klas i dostępności. Na przykład:

  • Jeśli program ma prosty settings.iniplik, czy jego zawartość powinna zostać załadowana do load()metody klasy, a może konstruktora?
  • Czy wartości powinny być przechowywane w public staticzmiennych, czy też powinny istnieć staticmetody uzyskiwania i ustawiania właściwości?
  • Co powinno się stać, jeśli plik nie istnieje lub nie można go odczytać? Jak poinformowałbyś resztę programu, że nie może uzyskać tych właściwości?
  • itp.

Mam nadzieję, że pytam o to we właściwym miejscu. Chciałem, aby pytanie było jak najbardziej agnostyczne, ale koncentruję się głównie na językach, które mają takie dziedziczenie - zwłaszcza Java i C # .NET.

Andy
źródło
1
W przypadku platformy .NET najlepiej jest użyć App.config i klasy System.Configuration.ConfigurationManager, aby uzyskać ustawienia
Gibson
@Gibson Właśnie zaczynam w .NET, więc czy mógłbyś połączyć mnie z jakimikolwiek dobrymi tutorialami dla tej klasy?
Andy
Stackoverflow zawiera wiele odpowiedzi. Szybkie wyszukiwanie ujawnia: stackoverflow.com/questions/114527/... i stackoverflow.com/questions/13043530/... Będzie też wiele informacji na temat MSDN, zaczynając tutaj msdn.microsoft.com/en-us/library/ms228063(v = vs.100) .aspx i msdn.microsoft.com/en-us/library/ms184658(v=vs.110).aspx
Gibson
@Gibson Dziękujemy za te linki. Będą bardzo przydatne.
Andy,

Odpowiedzi:

8

To jest naprawdę bardzo ważne pytanie i często jest robione źle, ponieważ nie ma dostatecznego znaczenia, mimo że jest to podstawowa część prawie każdej aplikacji. Oto moje wytyczne:

Twoja klasa konfiguracji, która zawiera wszystkie ustawienia, powinna być zwykłym starym typem danych, struct / class:

class Config {
    int prop1;
    float prop2;
    SubConfig subConfig;
}

Nie powinno to wymagać metod i nie powinno obejmować dziedziczenia (chyba że jest to jedyny wybór w twoim języku dla implementacji pola odmiany - patrz następny akapit). Może i powinien używać kompozycji do grupowania ustawień w mniejsze określone klasy konfiguracji (np. SubConfig powyżej). Jeśli zrobisz to w ten sposób, idealnie będzie przekazywać testy jednostkowe i ogólnie aplikację, ponieważ będzie miała minimalne zależności.

Prawdopodobnie będziesz musiał użyć typów wariantów, w przypadku gdy konfiguracje dla różnych konfiguracji są niejednorodne w strukturze. Przyjmuje się, że będziesz musiał włączyć dynamiczny rzut w pewnym momencie, gdy czytasz wartość, aby rzucić ją na właściwą (pod-) klasę konfiguracji, i bez wątpienia będzie to zależało od innego ustawienia konfiguracji.

Nie powinieneś być leniwy, wpisując wszystkie ustawienia jako pola, wykonując następujące czynności:

class Config {
    Dictionary<string, string> values;
};

Jest to kuszące, ponieważ oznacza, że ​​możesz napisać uogólnioną klasę serializacji, która nie musi wiedzieć, z jakimi polami ma do czynienia, ale jest błędna i wyjaśnię to za chwilę.

Serializacja konfiguracji odbywa się w całkowicie oddzielnej klasie. Niezależnie od tego, jakiego interfejsu API lub biblioteki użyjesz do tego, treść funkcji serializacji powinna zawierać wpisy, które w zasadzie oznaczają mapę od ścieżki / klucza w pliku do pola na obiekcie. Niektóre języki zapewniają dobrą introspekcję i mogą to zrobić dla Ciebie od razu po wyjęciu z pudełka, inne musisz jawnie napisać mapowanie, ale najważniejsze jest, aby napisać mapowanie tylko raz. Weźmy na przykład ten fragment, który zaadaptowałem z dokumentacji parsera opcji programu zwiększającego c ++:

struct Config {
   int opt;
} conf;
po::options_description desc("Allowed options");
desc.add_options()
    ("optimization", po::value<int>(&conf.opt)->default_value(10);

Zauważ, że ostatni wiersz w zasadzie mówi „optymalizacja” odwzorowuje na Config :: opt, a także, że istnieje deklaracja typu, którego oczekujesz. Chcesz, aby odczyt konfiguracji nie powiódł się, jeśli typ nie jest zgodny z oczekiwaniami, jeśli parametr w pliku nie jest tak naprawdę zmiennoprzecinkowy lub int, albo nie istnieje. Tzn. Błąd powinien wystąpić podczas odczytu pliku, ponieważ problem dotyczy formatu / sprawdzania poprawności pliku i należy zgłosić kod wyjątku / powrotu i zgłosić dokładny problem. Nie należy opóźniać tego później w programie. Dlatego nie powinieneś kusić się, aby złapać cały styl słownika Conf, jak wspomniano powyżej, który nie zawiedzie, gdy plik zostanie odczytany - ponieważ rzutowanie jest opóźnione, dopóki wartość nie jest potrzebna.

Powinieneś uczynić klasę Config tylko do odczytu w pewien sposób - ustawiając zawartość klasy raz podczas jej tworzenia i inicjowania z pliku. Jeśli potrzebujesz w swojej aplikacji ustawień dynamicznych, które się zmieniają, a także stałych, które tego nie robią, powinieneś mieć osobną klasę do obsługi tych dynamicznych, zamiast starać się, aby bity twojej klasy konfiguracji nie były tylko do odczytu .

Idealnie czytasz w pliku w jednym miejscu programu, tzn. Masz tylko jedno wystąpienie „ ConfigReader”. Jeśli jednak masz problem z przekazaniem instancji Config tam, gdzie jest ona potrzebna, lepiej mieć drugi ConfigReader niż wprowadzić konfigurację globalną (co, jak sądzę, jest tym, co OP rozumie przez „statyczny” ”), co prowadzi mnie do następnego punktu:

Unikaj uwodzicielskiej syreny singletona: „Uratuję cię, abyś musiał omijać tę klasę, wszyscy twoi konstruktorzy będą piękni i czyści. Dalej, to będzie takie proste”. Prawda ma dobrze zaprojektowaną, testowalną architekturę, w której prawie nie trzeba przekazywać klasy Config lub jej części przez tak wiele klas aplikacji. To, co znajdziesz, w klasie najwyższego poziomu, funkcji main () lub cokolwiek innego, rozwiążesz conf na poszczególne wartości, które podasz klasom komponentów jako argumenty, które następnie złożysz ponownie (zależność ręczna iniekcja). Pojedynczy / globalny / statyczny conf sprawi, że testowanie aplikacji będzie znacznie trudniejsze do wdrożenia i zrozumienia - np. Wprowadzi w błąd nowych programistów w zespole, którzy nie będą wiedzieć, że muszą ustawić stan globalny, aby przetestować różne rzeczy.

Jeśli twój język obsługuje właściwości, powinieneś ich użyć do tego celu. Powodem jest to, że bardzo łatwo będzie dodać „wyprowadzone” ustawienia konfiguracji, które zależą od jednego lub więcej innych ustawień. na przykład

int Prop1 { get; }
int Prop2 { get; }
int Prop3 { get { return Prop1*Prop2; }

Jeśli Twój język natywnie nie obsługuje idiomu właściwości, może to obejść, aby osiągnąć ten sam efekt, lub po prostu utworzysz klasę opakowania, która zapewnia ustawienia premii. Jeśli nie możesz w inny sposób nadać korzyści właściwościom, w innym przypadku strata czasu polega na ręcznym pisaniu i korzystaniu z getters / setters po prostu w celu zadowolenia jakiegoś boga OO. Lepiej będzie z prostym, starym polem.

Może być potrzebny system do łączenia i pobierania wielu konfiguracji z różnych miejsc w kolejności pierwszeństwa. Ta kolejność pierwszeństwa powinna być dobrze zdefiniowana i zrozumiała dla wszystkich programistów / użytkowników, np. Rozważ rejestr Windows HKEY_CURRENT_USER / HKEY_LOCAL_MACHINE. Powinieneś zrobić ten funkcjonalny styl, abyś mógł zachować swoje konfiguracje tylko do odczytu, tj .:

final_conf = merge(user_conf, machine_conf)

zamiast:

conf.update(user_conf)

Powinienem na koniec dodać, że oczywiście, jeśli wybrany framework / język zapewnia własne wbudowane, dobrze znane mechanizmy konfiguracji, powinieneś rozważyć korzyści z korzystania z niego zamiast z rozwijania własnego.

Więc. Wiele aspektów do rozważenia - popraw to, a to głęboko wpłynie na architekturę aplikacji, redukując błędy, ułatwiając testowanie i zmuszając cię do użycia dobrego projektu w innym miejscu.

Benedykt
źródło
+1 Dziękuję za odpowiedź. Wcześniej, kiedy zapisywałem ustawienia użytkownika, właśnie czytałem z pliku takiego .ini, aby był czytelny dla człowieka, ale czy sugerujesz, żebym powinien serializować klasę ze zmiennymi w?
Andy,
.ini można łatwo przeanalizować, a także istnieje wiele interfejsów API, które zawierają .ini jako możliwy format. Sugeruję, abyś używał takich interfejsów API, aby pomóc ci w tandetnej analizie tekstu, ale końcowym rezultatem powinno być zainicjowanie klasy POD. Używam terminu serializacja w ogólnym znaczeniu kopiowania lub odczytywania w polach klasy z jakiegoś formatu, a nie bardziej szczegółowej definicji szeregowania klasy bezpośrednio do / z pliku binarnego (jak to zwykle rozumie się np. Java.io. ).
Benedykt
Teraz rozumiem trochę lepiej. Więc zasadniczo mówisz, że masz jedną klasę ze wszystkimi polami / ustawieniami i masz inną, aby ustawić wartości w tej klasie z pliku? Jak zatem uzyskać wartości z pierwszej klasy w innych klasach?
Andy,
W poszczególnych przypadkach. Niektóre klasy najwyższego poziomu mogą wymagać przekazania odniesienia do całej instancji Config, jeśli są zależne od tak wielu parametrów. Inne klasy mogą potrzebować tylko jednego lub dwóch parametrów, ale nie ma potrzeby łączenia ich z obiektem Config (co czyni je jeszcze łatwiejszymi do przetestowania), wystarczy przekazać kilka parametrów w dół przez aplikację. Jak wspomniano w mojej odpowiedzi, jeśli zbudujesz architekturę zorientowaną na testowanie / DI, uzyskanie wartości tam, gdzie ich potrzebujesz, na ogół nie będzie trudne. Korzystaj z globalnego dostępu na własne ryzyko.
Benedykt
Jak więc zasugerowałbyś, że obiekt config jest przekazywany do klas, jeśli nie trzeba brać udziału w dziedziczeniu? Przez konstruktora? Tak więc w głównej metodzie programu inicjowana jest klasa czytnika, która przechowuje wartości w klasie Config, a następnie główna metoda przekazuje obiekt Config lub poszczególne zmienne do innych klas?
Andy,
4

Ogólnie (moim zdaniem) najlepiej pozwolić aplikacji radzić sobie ze sposobem przechowywania konfiguracji i przekazać konfigurację do modułów. Umożliwia to elastyczność w jaki sposób ustawienia są zapisywane tak, że można kierować pliki lub webservices lub baz danych lub ...

Dodatkowo nakłada na aplikację ciężar „co się stanie, gdy coś się nie powiedzie”, kto najlepiej wie, co oznacza ta awaria.

A to sprawia, że ton łatwiejsze do testów jednostkowych, kiedy można po prostu przejść w obiekcie konfiguracyjnym zamiast dotykać systemu plików lub radzić sobie z kwestiami współbieżności wprowadzonych dostępu statycznego.

Telastyn
źródło
Dziękuję za odpowiedź, ale nie do końca ją rozumiem. Mówię o tym, pisząc aplikację, więc nie mam nic do czynienia z konfiguracją, dlatego jako programista wiem, co oznacza błąd.
Andy,
@andy - jasne, ale jeśli moduły mają bezpośredni dostęp do konfiguracji, nie wiedzą, w jakim kontekście pracują, więc nie mogą ustalić, jak poradzić sobie z problemami z konfiguracją. Jeśli aplikacja ładuje, może poradzić sobie z dowolnymi problemami, ponieważ zna kontekst. Niektóre aplikacje mogą chcieć
przestawić
Aby to wyjaśnić, przez moduły odnosisz się do klas lub rzeczywistych rozszerzeń programu? Ponieważ pytam o to, gdzie najlepiej przechowywać ustawienia wewnątrz kodu głównego programu i jak zezwolić innym klasom na dostęp do ustawień. Chcę więc załadować plik konfiguracyjny, a następnie zapisać gdzieś / w jakiś sposób ustawienie, aby nie musiałem ciągle odwoływać się do pliku.
Andy,
0

Jeśli używasz .NET do programowania klas, masz różne opcje, takie jak Zasoby, web.config, a nawet plik niestandardowy.

Jeśli korzystasz z Resources lub web.config, dane są faktycznie przechowywane w pliku XML, ale ładowanie jest szybsze.

Pobieranie danych z tych plików i przechowywanie ich w innej lokalizacji będzie przypominało podwójne użycie pamięci, ponieważ są one domyślnie ładowane do pamięci.

W przypadku każdego innego pliku lub języka programowania zadziała powyższa odpowiedź Benedykta.

Kiran
źródło
Dziękuję za odpowiedź i przepraszam za powolną odpowiedź. Korzystanie z zasobów byłoby dobrą opcją, ale właśnie zdałem sobie sprawę, że prawdopodobnie nie jest to najlepsza opcja, ponieważ planuję opracować ten sam program w innych językach, więc wolałbym mieć plik XML lub JSON, ale przeczytaj go we własnej klasie, aby ten sam plik mógł być odczytany w innych językach programowania.
Andy,