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.