Moi koledzy lubią mówić „logowanie / buforowanie / itd. To problem przekrojowy”, a następnie wszędzie używają odpowiedniego singletonu. A jednak uwielbiają IoC i DI.
Czy to naprawdę słuszna wymówka, by złamać zasadę SOLI D.
Moi koledzy lubią mówić „logowanie / buforowanie / itd. To problem przekrojowy”, a następnie wszędzie używają odpowiedniego singletonu. A jednak uwielbiają IoC i DI.
Czy to naprawdę słuszna wymówka, by złamać zasadę SOLI D.
Odpowiedzi:
Nie.
SOLID istnieje jako wytyczne uwzględniające nieuniknioną zmianę. Czy naprawdę nigdy nie zamierzasz zmieniać biblioteki rejestrowania, celowania, filtrowania, formatowania lub ...? Czy naprawdę nie zamierzasz zmieniać swojej biblioteki buforowania, celu, strategii, zakresu, czy ...?
Oczywiście, że jesteś. Na każdym razie , będziesz chciał drwić te rzeczy w sposób rozsądny, aby odizolować je do testowania. A jeśli chcesz izolować je do testów, prawdopodobnie natkniesz się na powód biznesowy, w którym chcesz je izolować z prawdziwych powodów.
Otrzymasz argument, że sam rejestrator poradzi sobie ze zmianą. „Och, jeśli zmieni się cel / filtrowanie / formatowanie / strategia, po prostu zmienimy konfigurację!” To śmieci Nie tylko masz teraz Boski Obiekt, który obsługuje wszystkie te rzeczy, piszesz swój kod w XML (lub podobny), gdzie nie dostajesz analizy statycznej, nie dostajesz błędów czasu kompilacji i nie „ to naprawdę skuteczne testy jednostkowe.
Czy istnieją przypadki naruszenia wytycznych SOLID? Absolutnie. Czasami rzeczy się nie zmienią (i tak nie będą wymagały pełnego przepisania). Czasami lekkie naruszenie LSP jest najczystszym rozwiązaniem. Czasami utworzenie izolowanego interfejsu nie przynosi żadnej wartości.
Ale rejestrowanie i buforowanie (i inne wszechobecne problemy przekrojowe) nie są tymi przypadkami. Są to zwykle świetne przykłady problemów ze sprzężeniem i projektowaniem, które występują, gdy ignoruje się wytyczne.
String
lub Int32
nawet List
z modułu. Jest tylko taki stopień, w jakim rozsądne i rozsądne jest zaplanowanie zmiany. Poza najbardziej oczywistymi typami „rdzenia”, rozeznanie, co możesz zmienić, jest naprawdę kwestią doświadczenia i osądu.
tak
Jest to cały sens terminu „problem przekrojowy” - oznacza to coś, co nie pasuje dokładnie do zasady SOLID.
To tutaj idealizm spotyka się z rzeczywistością.
Ludzie częściowo nowi w SOLID i przekrojowi często spotykają się z tym mentalnym wyzwaniem. Jest OK, nie wariuj. Staraj się umieścić wszystko w kategoriach SOLID, ale jest kilka miejsc, takich jak rejestrowanie i buforowanie, w których SOLID po prostu nie ma sensu. Przekrój jest bratem SOLID, idą ze sobą w parze.
HttpContextBase
(która została wprowadzona właśnie z tego powodu). Wiem na pewno, że moja rzeczywistość byłaby naprawdę kwaśna bez tej klasy.
Myślę, że do logowania. Rejestrowanie jest wszechobecne i zasadniczo niezwiązane z funkcjonalnością usługi. Powszechne i dobrze zrozumiałe jest stosowanie wzorców singletonu w ramach rejestrowania. Jeśli nie, tworzysz i wstrzykujesz rejestratory wszędzie i nie chcesz tego.
Jednym z powyższych problemów jest to, że ktoś powie „ale jak mogę przetestować rejestrowanie?” . Moje myśli są takie, że zwykle nie testuję rejestrowania, poza stwierdzeniem, że rzeczywiście mogę odczytać pliki dziennika i je zrozumieć. Kiedy widziałem sprawdzanie rejestrowania, dzieje się tak zwykle dlatego, że ktoś musi stwierdzić, że klasa rzeczywiście coś zrobiła i używa komunikatów w dzienniku, aby uzyskać informację zwrotną. Wolałbym raczej zarejestrować jakiegoś słuchacza / obserwatora w tej klasie i zapewnić w moich testach, że zostanie on wywołany. Następnie możesz umieścić rejestrowanie zdarzeń w tym obserwatorze.
Myślę jednak, że buforowanie to zupełnie inny scenariusz.
Moje 2 centy ...
Tak i nie.
Nigdy nie powinieneś tak naprawdę naruszać przyjętych zasad; ale wasze zasady powinny być zawsze dopracowane i przyjęte w służbie wyższego celu. Zatem przy odpowiednio uwarunkowanym zrozumieniu niektóre pozorne naruszenia mogą nie być faktycznymi naruszeniami „ducha” lub „zbioru zasad jako całości”.
W szczególności zasady SOLID, poza wymaganiem wielu niuansów, są ostatecznie podporządkowane celowi „dostarczania działającego, możliwego do utrzymania oprogramowania”. Tak więc przestrzeganie jakiejkolwiek konkretnej zasady SOLID jest samowystarczalne i wewnętrznie sprzeczne, gdy jest to sprzeczne z celami SOLID. I tutaj często zauważam, że zapewnianie atutów w utrzymaniu .
A co z D w SOLID ? Cóż, przyczynia się do zwiększenia łatwości konserwacji, czyniąc moduł wielokrotnego użytku stosunkowo agnostycznym w stosunku do jego kontekstu. Możemy zdefiniować „moduł wielokrotnego użytku” jako „zbiór kodu, którego planujesz używać w innym odrębnym kontekście”. Dotyczy to pojedynczych funkcji, klas, zestawów klas i programów.
I tak, zmiana implementacji programu rejestrującego prawdopodobnie umieszcza moduł w „innym odrębnym kontekście”.
Pozwólcie, że przedstawię moje dwa duże zastrzeżenia :
Po pierwsze: narysowanie linii wokół bloków kodu, które stanowią „moduł wielokrotnego użytku”, jest kwestią profesjonalnego osądu. Twój osąd jest koniecznie ograniczony do twojego doświadczenia.
Jeśli nie planujesz obecnie używać modułu w innym kontekście, prawdopodobnie bezradne jest od niego uzależnienie. Zastrzeżenie do zastrzeżenia: Twoje plany są prawdopodobnie błędne - ale to również OK. Im dłużej piszesz moduł po module, tym bardziej intuicyjne i dokładne jest poczucie, czy „kiedyś będę tego znowu potrzebować”. Ale prawdopodobnie nigdy nie będziesz w stanie z mocą wsteczną powiedzieć: „Zmodularyzowałem i odsprzęgłem wszystko w możliwie największym stopniu, ale bez nadmiaru ”.
Jeśli czujesz się winny z powodu błędów w ocenie, idź do spowiedzi i przejdź dalej ...
Po drugie: Kontrola odwracania nie równa się wstrzykiwaniu zależności .
Jest to szczególnie prawdziwe, gdy zaczynasz wstrzykiwać zależności ad nauseam . Wstrzykiwanie zależności jest użyteczną taktyką dla nadrzędnej strategii IoC. Ale twierdzę, że DI ma mniejszą skuteczność niż niektóre inne taktyki - takie jak używanie interfejsów i adapterów - pojedyncze punkty ekspozycji na kontekst z poziomu modułu.
I skoncentrujmy się na tym przez chwilę. Ponieważ nawet jeśli wstrzykujesz Logger
reklamę na nudę , musisz napisać kod przeciwko Logger
interfejsowi. Nie można rozpocząć korzystania z nowego Logger
od innego dostawcy, który przyjmuje parametry w innej kolejności. Ta zdolność wynika z kodowania w module na interfejsie, który istnieje w module i który ma jeden podmoduł (Adapter) w celu zarządzania zależnością.
A jeśli piszesz przeciwko Adapterowi, to czy Logger
jest on wstrzykiwany do tego Adaptera, czy też wykrywany przez Adapter, jest ogólnie dość nieistotny dla ogólnego celu w zakresie konserwacji. A co ważniejsze, jeśli masz adapter na poziomie modułu, prawdopodobnie absurdem jest wstrzyknięcie go w cokolwiek. Jest napisany dla modułu.
tl; dr - Przestań się niepokoić o zasady, nie zastanawiając się, dlaczego je stosujesz. I, bardziej praktycznie, wystarczy zbudować Adapter
dla każdego modułu. Skorzystaj z własnego osądu, decydując, gdzie narysujesz granice „modułu”. Z każdego modułu przejdź bezpośrednio do Adapter
. I oczywiście , wstrzyknij prawdziwego loggera do Adapter
- ale nie do każdego drobiazgu, który może go potrzebować.
Pomysł, że rejestrowanie powinno być zawsze wdrażane jako singleton, jest jednym z tych kłamstw, które tak często opowiadano, że zyskały na popularności.
Dopóki chodziło o nowoczesne systemy operacyjne, uznano, że możesz chcieć zalogować się do wielu miejsc, w zależności od charakteru danych wyjściowych .
Projektanci systemów powinni stale kwestionować skuteczność wcześniejszych rozwiązań, zanim ślepo włączą je do nowych. Jeśli nie przeprowadzają takiej staranności, to nie wykonują swojej pracy.
Prawdziwe logowanie jest szczególnym przypadkiem.
@Telastyn pisze:
Czy naprawdę nigdy nie zamierzasz zmieniać biblioteki rejestrowania, celowania, filtrowania, formatowania lub ...?
Jeśli spodziewasz się, że konieczna może być zmiana biblioteki logowania, powinieneś użyć fasady; tj. SLF4J, jeśli jesteś w świecie Java.
Jeśli chodzi o resztę, porządna biblioteka rejestrowania zajmuje się zmianą miejsca, w którym odbywa się rejestrowanie, które zdarzenia są filtrowane, jak formatowanie zdarzeń dziennika jest przeprowadzane za pomocą plików konfiguracyjnych rejestratora i (jeśli to konieczne) niestandardowych klas wtyczek. Istnieje wiele gotowych alternatyw.
Krótko mówiąc, są to rozwiązane problemy ... do logowania ... i dlatego nie ma potrzeby używania Dependency Injection do ich rozwiązania.
Jedyny przypadek, w którym DI może być korzystny (w porównaniu ze standardowymi metodami rejestrowania), polega na tym, że chcesz poddać rejestrowanie aplikacji testom jednostkowym. Podejrzewam jednak, że większość programistów powiedziałaby, że rejestrowanie nie jest częścią funkcjonalności klas i nie jest czymś, co wymaga przetestowania.
@Telastyn pisze:
Otrzymasz argument, że sam rejestrator poradzi sobie ze zmianą. „Och, jeśli zmieni się cel / filtrowanie / formatowanie / strategia, po prostu zmienimy konfigurację!” To śmieci Nie tylko masz teraz Boski Obiekt, który obsługuje wszystkie te rzeczy, piszesz swój kod w XML (lub podobny), gdzie nie dostajesz analizy statycznej, nie dostajesz błędów czasu kompilacji i nie „ to naprawdę skuteczne testy jednostkowe.
Obawiam się, że to bardzo teoretyczny ripost. W praktyce większość programistów i integratorów systemów PODOBA fakt, że można skonfigurować rejestrowanie za pomocą pliku konfiguracyjnego. I podoba im się to, że nie oczekuje się od nich jednostkowego testowania rejestrowania modułu.
Oczywiście, jeśli upchniesz konfiguracje rejestrowania, możesz mieć problemy, ale pojawią się one albo jako awaria aplikacji podczas uruchamiania, albo za dużo / za mało rejestrowania. 1) Problemy te można łatwo naprawić, naprawiając błąd w pliku konfiguracyjnym. 2) Alternatywą jest pełny cykl kompilacji / analizy / testowania / wdrażania przy każdej zmianie poziomów rejestrowania. To nie do przyjęcia.
Tak i nie !
Tak: Sądzę, że uzasadnione jest, aby różne podsystemy (lub warstwy semantyczne lub biblioteki lub inne pojęcia pakietowania modułowego) akceptują (ten sam lub) potencjalnie inny program rejestrujący podczas inicjalizacji, a nie wszystkie podsystemy oparte na tym samym wspólnym wspólnym singletonie .
Jednak,
Nie: sparametryzowanie logowania w każdym małym obiekcie (przy użyciu metody konstruktora lub instancji) jest nieuzasadnione. Aby uniknąć niepotrzebnego i bezcelowego wzdęcia, mniejsze jednostki powinny używać rejestratora singletonów w swoim otaczającym kontekście.
Jest to jeden z wielu powodów, dla których należy myśleć o modułowości poziomów: metody są pogrupowane w klasy, a klasy w podsystemy i / lub warstwy semantyczne. Te większe pakiety są cennymi narzędziami abstrakcji; powinniśmy rozważyć inne rozważania w granicach modułowych niż przy ich przekraczaniu.
Najpierw zaczyna się od silnej pamięci podręcznej singletonów, następnie widać silne singletony dla warstwy bazy danych wprowadzające stan globalny, class
nieopisowe interfejsy API es i niestabilny kod.
Jeśli zdecydujesz się nie mieć singletonu dla bazy danych, prawdopodobnie nie jest dobrym pomysłem posiadanie singletona dla pamięci podręcznej, w końcu reprezentują one bardzo podobną koncepcję, przechowywanie danych, tylko przy użyciu różnych mechanizmów.
Korzystanie z singletonu w klasie zmienia klasę o określonej liczbie zależności w klasę, która teoretycznie ma ich nieskończoną liczbę, ponieważ nigdy nie wiadomo, co tak naprawdę kryje się za metodą statyczną.
W ostatnim dziesięcioleciu spędziłem programowanie, był tylko jeden przypadek, w którym byłem świadkiem wysiłku zmiany logiki rejestrowania (która została wówczas napisana jako singleton). Więc chociaż uwielbiam zastrzyki z zależności, rejestrowanie nie jest tak wielkim problemem. Z drugiej strony pamięć podręczna z pewnością zawsze byłaby zależna.
Tak i nie, ale głównie nie
Zakładam, że większość konwersacji oparta jest na instancji statycznej vs. Nikt nie proponuje, aby rejestrowanie złamało SRP, które zakładam? Mówimy głównie o „zasadzie inwersji zależności”. Tbh W większości zgadzam się z brakiem odpowiedzi Telastyna.
Kiedy można używać statyki? Ponieważ najwyraźniej czasami jest to w porządku. Odpowiedź „tak” dla zalet abstrakcji, a odpowiedź „nie” wskazuje, że są to coś, za co płacisz. Jednym z powodów, dla których praca jest trudna, jest to, że nie ma jednej odpowiedzi, którą można zapisać i zastosować we wszystkich sytuacjach.
Brać:
Convert.ToInt32("1")
Wolę to:
private readonly IConverter _converter;
public MyClass(IConverter converter)
{
Guard.NotNull(converter)
_converter = conveter
}
....
var foo = _converter.ToInt32("1");
Dlaczego? Zgadzam się, że będę musiał przefakturować kod, jeśli potrzebuję elastyczności, aby zamienić kod konwersji. Akceptuję, że nie będę w stanie wyśmiewać tego. Uważam, że prostota i zwięzłość jest warta tego handlu.
Patrząc na drugim końcu spektrum, jeśli IConverter
było SqlConnection
, to byłoby dość przerażony widząc, że jako statyczny rozmowy. Powody, dla których są oczywiste. Zwracam uwagę, że a SQLConnection
może być dość „przekrojowe” w aplacji, więc nie użyłbym tych dokładnych słów.
Czy rejestrowanie bardziej przypomina „ SQLConnection
lub” Convert.ToInt32
? Powiedziałbym bardziej jak „SQLConnection”.
Powinieneś kpić z Logowania . Mówi do świata zewnętrznego. Pisząc metodę za pomocą Convert.ToIn32
, używam jej jako narzędzia do obliczania innych osobno testowalnych danych wyjściowych klasy. Nie muszę sprawdzać, czy Convert
został poprawnie wywołany podczas sprawdzania, że „1” + „2” == „3”. Logowanie jest inne, jest to całkowicie niezależne wyjście klasy. Zakładam, że jest to wynik, który ma wartość dla Ciebie, zespołu wsparcia i firmy. Twoja klasa nie działa, jeśli rejestrowanie jest nieprawidłowe, więc testy jednostkowe nie powinny przejść pomyślnie. Powinieneś testować, co rejestruje Twoja klasa. Myślę, że to argument zabójcy, naprawdę mógłbym się tutaj zatrzymać.
Myślę też, że jest to coś, co może się zmienić. Dobre rejestrowanie nie tylko drukuje ciągi, ale pokazuje, co robi twoja aplikacja (jestem wielkim fanem rejestrowania na podstawie zdarzeń). Widziałem, jak podstawowe logowanie zmienia się w dość skomplikowane interfejsy raportowania. Oczywiście o wiele łatwiej jest podążać w tym kierunku, jeśli rejestrowanie wygląda tak, _logger.Log(new ApplicationStartingEvent())
a mniej podobnie Logger.Log("Application has started")
. Można argumentować, że tworzy to ekwipunek na przyszłość, która może się nigdy nie wydarzyć, to jest apel sądowy i wydaje mi się, że warto.
W rzeczywistości w moim osobistym projekcie stworzyłem interfejs użytkownika, który nie loguje się, używając wyłącznie _logger
do sprawdzenia, co robi aplikacja. Oznaczało to, że nie musiałem pisać kodu, aby dowiedzieć się, co robi aplikacja, i skończyło się na solidnym logowaniu. Wydaje mi się, że gdyby moje podejście do logowania było proste i niezmienne, ten pomysł by mi się nie przydarzył.
Zgadzam się więc z Telastyn w sprawie logowania.
Semantic Logging Application Block
. Nie używaj go, jak większość kodu utworzonego przez zespół MS „wzorce i praktyka”, jak na ironię, jest to antypattern.
Po pierwsze, problemy przekrojowe nie są głównymi elementami składowymi i nie powinny być traktowane jako zależności w systemie. System powinien działać, jeśli np. Logger nie został zainicjowany lub pamięć podręczna nie działa. Jak sprawisz, że system będzie mniej sprzężony i spójny? Właśnie tam pojawia się SOLID w projektowaniu systemu OO.
Utrzymywanie obiektu jako singletonu nie ma nic wspólnego z SOLID. To jest cykl życia twojego obiektu, jak długo ma on żyć w pamięci.
Klasa, która wymaga zależności do zainicjowania, nie powinna wiedzieć, czy podana instancja klasy jest singletonowa czy przejściowa. Ale tldr; jeśli piszesz Logger.Instance.Log () w każdej metodzie lub klasie, to jest to problematyczny kod (zapach kodu / twarde sprzężenie), to naprawdę bałagan. To jest moment, kiedy ludzie zaczynają nadużywać SOLID. Inni programiści tacy jak OP zaczynają zadawać takie prawdziwe pytania.
Rozwiązałem ten problem, używając kombinacji dziedziczenia i cech (w niektórych językach nazywanych także mixinami). Cechy są bardzo przydatne do rozwiązania tego rodzaju problemów przekrojowych. Zazwyczaj jest to jednak funkcja językowa, więc myślę, że prawdziwą odpowiedzią jest to, że zależy od funkcji językowych.