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?
źródło
Odpowiedzi:
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 .
źródło
Segreguję grupy powiązanych ustawień za pomocą interfejsów.
Coś jak:
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.
Uważam, że ta mieszanka jest najlepsza ze wszystkich światów.
źródło
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.
źródło
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.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.
źródło
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ć:
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.
źródło
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.
źródło
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.
źródło
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.
źródło