Czy pojedynczy obiekt konfiguracji to zły pomysł?

43

W większości moich aplikacji mam singleton lub statyczny obiekt „config”, który odpowiada za odczytywanie różnych ustawień z dysku. Prawie wszystkie klasy używają go do różnych celów. Zasadniczo jest to tylko tablica skrótów par nazwa / wartość. Jest tylko do odczytu, więc nie martwiłem się zbytnio faktem, że mam tak dużo globalnego stanu. Ale teraz, gdy zaczynam od testowania jednostkowego, zaczyna to stanowić problem.

Jednym z problemów jest to, że zwykle nie chcesz testować z tą samą konfiguracją, z którą korzystasz. Istnieje kilka rozwiązań tego:

  • Daj obiektowi konfigurującemu narzędzie, które TYLKO jest używane do testowania, abyś mógł przekazać inne ustawienia.
  • Kontynuuj korzystanie z pojedynczego obiektu konfiguracji, ale zmień go z singletonu na instancję przekazywaną wszędzie tam, gdzie jest to potrzebne. Następnie możesz zbudować go raz w aplikacji i raz w testach, z różnymi ustawieniami.

Ale tak czy inaczej, nadal masz drugi problem: prawie każda klasa może korzystać z obiektu config. Tak więc w teście musisz skonfigurować konfigurację testowanej klasy, ale także WSZYSTKIE jej zależności. Może to spowodować, że kod testowy będzie brzydki.

Zaczynam dochodzić do wniosku, że tego rodzaju obiekt konfiguracji to zły pomysł. Co myślisz? Jakie są alternatywy? Jak rozpocząć refaktoryzację aplikacji używającej konfiguracji wszędzie?

JW01
źródło
Konfiguracja pobiera ustawienia, odczytując plik na dysku, prawda? Dlaczego więc nie mieć pliku „test.config”, który można odczytać?
Anon.
To rozwiązuje pierwszy problem, ale nie drugi.
JW01
@ JW01: Czy zatem wszystkie twoje testy wymagają zupełnie innej konfiguracji? Będziesz musiał skonfigurować tę konfigurację gdzieś podczas testu, prawda?
Anon.
Prawdziwe. Ale jeśli nadal będę korzystać z jednej puli ustawień (z inną pulą do testowania), wtedy wszystkie moje testy będą miały te same ustawienia. A ponieważ mogę chcieć przetestować zachowanie przy różnych ustawieniach, nie jest to idealne.
JW01
„W teście musisz skonfigurować konfigurację testowanej klasy, ale także WSZYSTKIE jej zależności. Może to spowodować, że kod testowy będzie brzydki.” Masz przykład? Mam wrażenie, że to nie struktura całej aplikacji łączącej się z klasą config, ale struktura samej klasy config czyni rzeczy „brzydkimi”. Czy konfigurowanie klasy nie powinno automatycznie konfigurować jej zależności?
AndrewKS

Odpowiedzi:

35

Nie mam problemu z pojedynczym obiektem konfiguracji i widzę zaletę w utrzymywaniu wszystkich ustawień w jednym miejscu.

Jednak używanie tego pojedynczego obiektu wszędzie spowoduje wysoki poziom sprzężenia między obiektem config a klasami, które go używają. Jeśli musisz zmienić klasę konfiguracji, być może będziesz musiał odwiedzić każdą instancję, w której jest używana i sprawdzić, czy niczego nie zepsułeś.

Jednym ze sposobów na poradzenie sobie z tym jest utworzenie wielu interfejsów, które ujawniają części obiektu config, które są potrzebne przez różne części aplikacji. Zamiast pozwalać innym klasom na dostęp do obiektu config, przekazujesz instancje interfejsu do klas, które tego potrzebują. W ten sposób części aplikacji korzystające z config zależą raczej od mniejszej kolekcji pól w interfejsie niż od całej klasy config. Wreszcie, do testów jednostkowych możesz stworzyć niestandardową klasę, która implementuje interfejs.

Jeśli chcesz dalej zgłębiać te pomysły, polecam lekturę na temat zasad SOLID , w szczególności zasady segregacji interfejsu i zasady odwracania zależności .

Kramii Przywróć Monikę
źródło
7

Segreguję grupy powiązanych ustawień za pomocą interfejsów.

Coś jak:

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

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

itp.

Teraz, w prawdziwym środowisku, jedna klasa zaimplementuje wiele z tych interfejsów. Ta klasa może pobierać dane z bazy danych lub konfiguracji aplikacji lub co masz, ale często wie, jak uzyskać większość ustawień.

Ale segregacja według interfejsu sprawia, że ​​rzeczywiste zależności są bardziej wyraźne i drobnoziarniste, i jest to oczywista poprawa dla testowalności.

Ale .. Nie zawsze chcę być zmuszany do wstrzykiwania lub dostarczania ustawień jako zależności. Można argumentować, że powinienem, dla spójności lub jednoznaczności. Ale w prawdziwym życiu wydaje się to niepotrzebnym ograniczeniem.

Wykorzystam więc klasę statyczną jako fasadę, zapewniającą łatwy dostęp z dowolnego miejsca do ustawień, aw ramach tej klasy statycznej będę obsługiwać lokalizacje implementacji interfejsu i uzyskać ustawienie.

Wiem, że lokalizacja usługi szybko przesuwa kciuki w dół, ale spójrzmy prawdzie w oczy, pod warunkiem, że zależność od konstruktora jest ciężarem, a czasami ta waga jest większa niż mi zależy. Lokalizacja usługi to rozwiązanie pozwalające zachować testowalność poprzez programowanie interfejsów i pozwalające na wiele implementacji, jednocześnie zapewniając wygodę (w dokładnie zmierzonych i odpowiednio niewielu sytuacjach) statycznego punktu wejścia singletonu.

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

Uważam, że ta mieszanka jest najlepsza ze wszystkich światów.

Quentin-Starin
źródło
3

Tak, jak sobie uświadomiłeś, globalny obiekt konfiguracji utrudnia testy jednostkowe. Posiadanie „tajnego” setera do testowania jednostkowego to szybki hack, który choć nie jest miły, może być bardzo przydatny: umożliwia rozpoczęcie pisania testów jednostkowych, dzięki czemu możesz z czasem zmienić kod na lepszy projekt.

(Obowiązkowe odniesienie: Skuteczna praca ze starszym kodem. Zawiera on i wiele innych bezcennych sztuczek do testowania jednostek starszego kodu.)

Z mojego doświadczenia wynika, że ​​najlepiej jest mieć jak najmniej zależności od globalnej konfiguracji. Co niekoniecznie jest równe zero - wszystko zależy od okoliczności. Posiadanie kilku klas „wysokiego poziomu” organizatora, które uzyskują dostęp do globalnej konfiguracji i przekazują rzeczywiste właściwości konfiguracji do obiektów, które tworzą i używają, mogą działać dobrze, pod warunkiem, że organizatorzy tylko to robią, np. Nie zawierają zbyt wiele testowalnego kodu. Umożliwia to przetestowanie większości lub wszystkich ważnych funkcji aplikacji, które można przetestować, przy jednoczesnym niezakłóceniu istniejącego schematu konfiguracji fizycznej.

To już zbliża się do oryginalnego rozwiązania do wstrzykiwania zależności, takiego jak Spring for Java. Jeśli możesz migrować do takich ram, w porządku. Ale w prawdziwym życiu, a zwłaszcza w przypadku starszych aplikacji, często powolne i drobiazgowe przekształcanie w kierunku DI jest najlepszym osiągalnym kompromisem.

Péter Török
źródło
Naprawdę podobało mi się twoje podejście i nie sugerowałbym, że idea setera powinna być utrzymywana w „tajemnicy” lub uważana za „szybki hack” w tej sprawie. Jeśli przyjmiesz stanowisko, że testy jednostkowe są ważnym użytkownikiem / konsumentem / częścią twojej aplikacji, to posiadanie test_atrybutów i metod nie jest już takie złe, prawda? Zgadzam się, że prawdziwym rozwiązaniem tego problemu jest zastosowanie frameworka DI, ale w przykładach takich jak twój, podczas pracy ze starszym kodem lub innymi prostymi przypadkami, nie ma alienacji kodu testowego.
Filip Dupanović
1
@ kRON, są to niezwykle przydatne i praktyczne sztuczki, dzięki którym starsze jednostki kodu mogą być testowane, i używam ich również szeroko. Jednak idealnie kod produkcyjny nie powinien zawierać żadnych funkcji wprowadzonych wyłącznie w celu testowania. W dłuższej perspektywie właśnie tutaj dokonuję refaktoryzacji.
Péter Török
2

Z mojego doświadczenia wynika, że ​​w świecie Java właśnie do tego służy Spring. Tworzy obiekty (komponenty) i zarządza nimi na podstawie określonych właściwości ustawianych w czasie wykonywania, a poza tym jest przezroczysty dla aplikacji. Możesz przejść z plikiem test.config, który Anon. wspomina lub możesz dołączyć logikę do pliku konfiguracyjnego, który Spring zajmie się, aby ustawić właściwości w oparciu o inny klucz (np. nazwę hosta lub środowisko).

Jeśli chodzi o twój drugi problem, możesz obejść go przez jakąś rearchitekturę, ale nie jest tak poważny, jak się wydaje. W twoim przypadku oznacza to, że nie miałbyś na przykład globalnego obiektu Config, którego używają różne inne klasy. Po prostu skonfiguruj te inne klasy za pomocą Springa, a następnie nie będziesz mieć obiektu config, ponieważ wszystko, co wymagało konfiguracji, dostało się przez Springa, dzięki czemu możesz używać tych klas bezpośrednio w kodzie.

Justin Beal
źródło
2

To pytanie naprawdę dotyczy bardziej ogólnego problemu niż konfiguracji. Gdy tylko przeczytałem słowo „singleton”, natychmiast pomyślałem o wszystkich problemach związanych z tym wzorcem, a wśród nich o słabej testowalności.

Wzór Singletona jest „uważany za szkodliwy”. Oznacza to, że nie zawsze jest to coś złego, ale zwykle tak jest. Jeśli kiedykolwiek zastanawiasz się nad użyciem wzorca singletonu do czegoś , przestań rozważać:

  • Czy kiedykolwiek będziesz musiał go podklasować?
  • Czy kiedykolwiek będziesz musiał zaprogramować interfejs?
  • Czy chcesz przeprowadzić testy jednostkowe?
  • Czy będziesz musiał go często modyfikować?
  • Czy Twoja aplikacja ma obsługiwać wiele platform / środowisk produkcyjnych?
  • Czy jesteś trochę zaniepokojony wykorzystaniem pamięci?

Jeśli twoja odpowiedź brzmi „tak” na którekolwiek z nich (i prawdopodobnie kilka innych rzeczy, o których nie myślałem), prawdopodobnie nie chcesz używać singletonu. Konfiguracje często wymagają znacznie większej elastyczności niż singleton (lub, w tym przypadku, każda instancja bez instancji).

Jeśli chcesz uzyskać prawie wszystkie korzyści singletonu bez żadnego bólu, skorzystaj z platformy wstrzykiwania zależności, takiej jak Spring lub Castle lub cokolwiek, co jest dostępne dla twojego środowiska. W ten sposób będziesz musiał zadeklarować to tylko raz, a kontener automatycznie zapewni instancję dla dowolnej potrzeby.

Aaronaught
źródło
0

Jednym ze sposobów, w jaki poradziłem sobie z tym w C #, jest posiadanie obiektu singleton, który blokuje się podczas tworzenia instancji i inicjuje wszystkie dane jednocześnie, aby mogły być używane przez wszystkie obiekty klienta. Jeśli ten singleton może obsługiwać pary klucz / wartość, możesz przechowywać dowolny rodzaj danych, w tym złożone klucze do użytku przez wielu różnych klientów i typy klientów. Jeśli chcesz zachować dynamikę i w razie potrzeby ładować nowe dane klienta, możesz zweryfikować istnienie klucza głównego klienta, a jeśli go brakuje, załaduj dane tego klienta, dołączając je do głównego słownika. Możliwe jest również, że singleton konfiguracji podstawowej może zawierać zestawy danych klienta, gdzie wielokrotności tego samego typu klienta używają tych samych danych, do których dostęp uzyskuje się za pośrednictwem singletonu konfiguracji podstawowej. Duża część tej organizacji obiektów konfiguracyjnych może zależeć od tego, w jaki sposób klienci konfiguracji potrzebują dostępu do tych informacji i czy informacje te są statyczne czy dynamiczne. Użyłem zarówno konfiguracji klucza / wartości, jak i określonych interfejsów API obiektów, w zależności od tego, które były potrzebne.

Jeden z moich singletonów konfiguracji może odbierać komunikaty do przeładowania z bazy danych. W takim przypadku ładuję do drugiej kolekcji obiektów i blokuję kolekcję główną tylko w celu zamiany kolekcji. Zapobiega to blokowaniu odczytów w innych wątkach.

Jeśli ładujesz z plików konfiguracyjnych, możesz mieć hierarchię plików. Ustawienia w jednym pliku mogą określać, który z pozostałych plików ma zostać załadowany. Użyłem tego mechanizmu z usługami Windows C #, które mają wiele opcjonalnych składników, każdy z własnymi ustawieniami konfiguracji. Wzorce struktury plików były takie same we wszystkich plikach, ale zostały załadowane lub nie w oparciu o podstawowe ustawienia plików.

Tim Butterfield
źródło
0

Klasa, którą opisujesz, brzmi jak anty-wzorzec obiektu Bożego, z wyjątkiem danych zamiast funkcji. To jest problemem. Prawdopodobnie powinieneś odczytać i zapisać dane konfiguracyjne w odpowiednim obiekcie, a dane odczytywać tylko ponownie, jeśli z jakiegoś powodu będą potrzebne.

Ponadto używasz Singletona z niewłaściwego powodu. Singletonów należy używać tylko wtedy, gdy istnienie wielu obiektów spowoduje zły stan. Używanie Singletona w tym przypadku jest niewłaściwe, ponieważ posiadanie więcej niż jednego czytnika konfiguracji nie powinno powodować natychmiastowego wystąpienia błędu. Jeśli masz więcej niż jeden, prawdopodobnie robisz to źle, ale nie jest absolutnie konieczne, abyś miał tylko jeden czytnik konfiguracji.

Wreszcie, utworzenie takiego globalnego stanu jest naruszeniem enkapsulacji, ponieważ zezwalasz na więcej klas niż potrzeba bezpośredniego dostępu do danych.

indyk1ng
źródło
0

Myślę, że jeśli istnieje wiele ustawień z „grupami” powiązanych, sensowne jest podzielenie ich na osobne interfejsy z odpowiednimi implementacjami - a następnie wstrzyknięcie tych interfejsów w razie potrzeby za pomocą DI. Zyskujesz testowalność, niższe sprzężenie i SRP.

Inspiracją w tej sprawie był Joshua Flanagan z Los Techies; jakiś czas temu napisał artykuł na ten temat.

Grant Palin
źródło