Jeśli dobrze rozumiem, typowym mechanizmem wstrzykiwania zależności jest wstrzyknięcie albo przez konstruktor klasy, albo przez właściwość publiczną (składową) klasy.
To uwidacznia wstrzykiwaną zależność i narusza zasadę hermetyzacji OOP.
Czy mam rację w identyfikacji tego kompromisu? Jak radzisz sobie z tym problemem?
Zobacz także moją odpowiedź na moje własne pytanie poniżej.
Odpowiedzi:
Istnieje inny sposób spojrzenia na ten problem, który może Cię zainteresować.
Kiedy używamy IoC / iniekcji zależności, nie używamy koncepcji OOP. Wprawdzie używamy języka OO jako „hosta”, ale idee stojące za IoC pochodzą z inżynierii oprogramowania zorientowanej na komponenty, a nie z OO.
Oprogramowanie składowe polega na zarządzaniu zależnościami - przykładem często używanym jest mechanizm tworzenia .NET. Każdy zestaw publikuje listę zestawów, do których się odwołuje, a to znacznie ułatwia zbieranie (i sprawdzanie poprawności) elementów potrzebnych do uruchomionej aplikacji.
Stosując podobne techniki w naszych programach obiektowych za pośrednictwem IoC, dążymy do ułatwienia konfiguracji i utrzymania programów. Zależności publikowania (jako parametry konstruktora lub cokolwiek innego) są kluczową częścią tego. Hermetyzacja tak naprawdę nie ma zastosowania, ponieważ w świecie zorientowanym na komponenty / usługi nie ma „typu implementacji” szczegółów, z których można by wyciekać.
Niestety, nasze języki nie oddzielają obecnie drobnoziarnistych, zorientowanych obiektowo koncepcji od gruboziarnistych, zorientowanych na komponenty, więc jest to różnica, o której musisz pamiętać tylko :)
źródło
To dobre pytanie - ale w pewnym momencie hermetyzacja w jej najczystszej formie musi zostać naruszona, jeśli obiekt ma kiedykolwiek spełnić swoją zależność. Pewien dostawca zależności musi wiedzieć zarówno, że dany obiekt wymaga a
Foo
, a dostawca musi mieć sposób na udostępnienieFoo
obiektu.Klasycznie ten drugi przypadek jest obsługiwany, jak mówisz, za pomocą argumentów konstruktora lub metod ustawiających. Jednak niekoniecznie jest to prawdą - wiem, że najnowsze wersje frameworka Spring DI w Javie pozwalają na przykład dodawać adnotacje do pól prywatnych (np. Z
@Autowired
), a zależność zostanie ustawiona przez odbicie bez konieczności ujawniania zależności przez dowolna z publicznych metod / konstruktorów klas. To może być rozwiązanie, którego szukałeś.To powiedziawszy, nie sądzę, aby iniekcja konstruktora była dużym problemem. Zawsze uważałem, że obiekty powinny być w pełni poprawne po skonstruowaniu, tak że wszystko, czego potrzebują, aby pełnić swoją rolę (tj. Być w prawidłowym stanie), powinno i tak zostać dostarczone przez konstruktora. Jeśli masz obiekt, który wymaga pracy współpracownika, wydaje mi się dobrze, że konstruktor publicznie ogłasza to wymaganie i zapewnia, że zostanie ono spełnione, gdy zostanie utworzona nowa instancja klasy.
Idealnie, gdy mamy do czynienia z obiektami, i tak wchodzisz z nimi w interakcje za pośrednictwem interfejsu, a im częściej to robisz (i masz zależności podłączone przez DI), tym mniej masz do czynienia z konstruktorami. W idealnej sytuacji Twój kod nie zajmuje się ani nawet nie tworzy konkretnych instancji klas; więc po prostu otrzymuje
IFoo
przez DI, bez martwienia się o to, co konstruktorFooImpl
wskazuje, że musi wykonać swoją pracę, a nawet nie zdając sobie sprawy zFooImpl
jego istnienia. Z tego punktu widzenia hermetyzacja jest idealna.To jest oczywiście opinia, ale moim zdaniem DI niekoniecznie narusza hermetyzację i faktycznie może temu pomóc, scentralizując całą niezbędną wiedzę o wewnętrznych elementach w jednym miejscu. Nie tylko jest to dobra rzecz sama w sobie, ale nawet lepiej, że to miejsce znajduje się poza twoją własną bazą kodów, więc żaden z piszącego kodu nie musi wiedzieć o zależnościach klas.
źródło
Cóż, szczerze mówiąc, wszystko narusza hermetyzację. :) To rodzaj delikatnej zasady, którą należy dobrze traktować.
Więc co narusza hermetyzację?
Dziedziczenie tak .
Programowanie zorientowane aspektowo tak . Na przykład rejestrujesz wywołanie zwrotne onMethodCall () i daje to świetną okazję do wstrzyknięcia kodu do normalnej oceny metody, dodania dziwnych efektów ubocznych itp.
Deklaracja znajomego w C ++ robi .
Klasa rozbudowa w Ruby robi . Po prostu zredefiniuj metodę string gdzieś po pełnym zdefiniowaniu klasy string.
Cóż, wiele rzeczy tak .
Hermetyzacja to dobra i ważna zasada. Ale nie jedyny.
źródło
Tak, DI narusza hermetyzację (znaną również jako „ukrywanie informacji”).
Ale prawdziwy problem pojawia się, gdy programiści używają go jako wymówki do złamania zasad KISS (Keep It Short and Simple) i YAGNI (You Ain't Gonna Need It).
Osobiście wolę proste i skuteczne rozwiązania. Najczęściej używam operatora „new”, aby tworzyć instancje zależności stanowych, kiedykolwiek i gdziekolwiek są potrzebne. Jest prosty, dobrze zamknięty, łatwy do zrozumienia i łatwy do przetestowania. Więc czemu nie?
źródło
Dobry pojemnik / system do iniekcji pozwoli na iniekcję konstruktora. Obiekty zależne zostaną zamknięte i w ogóle nie będą musiały być ujawniane publicznie. Ponadto, używając systemu DP, żaden z twoich kodów nawet nie „zna” szczegółów budowy obiektu, być może nawet włączając konstruowany obiekt. W tym przypadku jest więcej hermetyzacji, ponieważ prawie cały kod nie tylko jest chroniony przed wiedzą o hermetyzowanych obiektach, ale nawet nie uczestniczy w ich konstrukcji.
Teraz zakładam, że porównujesz z przypadkiem, w którym utworzony obiekt tworzy własne zamknięte obiekty, najprawdopodobniej w swoim konstruktorze. W moim rozumieniu DP jest to, że chcemy zdjąć tę odpowiedzialność z obiektu i przekazać ją komuś innemu. W tym celu „ktoś inny”, który w tym przypadku jest pojemnikiem DP, posiada intymną wiedzę, która „narusza” hermetyzację; korzyścią jest to, że wyciąga tę wiedzę z samego przedmiotu. Ktoś musi to mieć. Reszta aplikacji nie.
Pomyślałbym o tym w ten sposób: kontener / system iniekcji zależności narusza hermetyzację, ale twój kod nie. W rzeczywistości Twój kod jest bardziej „hermetyzowany” niż kiedykolwiek.
źródło
Jak zauważył Jeff Sternal w komentarzu do pytania, odpowiedź jest całkowicie zależna od tego, jak zdefiniujesz hermetyzację .
Wydaje się, że istnieją dwa główne obozy dotyczące tego, co oznacza hermetyzacja:
File
obiekt może mieć metod doSave
,Print
,Display
,ModifyText
, itd.Te dwie definicje są ze sobą w bezpośredniej sprzeczności. Jeśli
File
obiekt może sam się wydrukować, będzie to w dużym stopniu zależeć od zachowania drukarki. Z drugiej strony, jeśli po prostu wie o czymś, co może dla niego drukować (IFilePrinter
lub jakimś takim interfejsie), toFile
obiekt nie musi nic wiedzieć o drukowaniu, więc praca z nim przyniesie mniej zależności w obiekcie.Tak więc iniekcja zależności przerwie hermetyzację, jeśli użyjesz pierwszej definicji. Ale, szczerze mówiąc, nie wiem, czy podoba mi się pierwsza definicja - najwyraźniej nie jest skalowana (gdyby tak było, MS Word byłby jedną wielką klasą).
Z drugiej strony iniekcja zależności jest prawie obowiązkowa, jeśli używasz drugiej definicji hermetyzacji.
źródło
Nie narusza hermetyzacji. Zapewniasz współpracownika, ale klasa decyduje, jak będzie używany. Dopóki podążasz za Tell, nie pytaj, że wszystko jest w porządku. Uważam, że preferowany jest wtrysk konstruktora, ale setery mogą być w porządku, o ile są inteligentne. Oznacza to, że zawierają logikę do utrzymania niezmienników reprezentowanych przez klasę.
źródło
Jest to podobne do odpowiedzi, za którą głosowano, ale ja chcę myśleć głośno - być może inni też widzą to w ten sposób.
Klasyczny obiekt obiektowy używa konstruktorów do definiowania publicznego kontraktu "inicjującego" dla konsumentów klasy (ukrywanie WSZYSTKICH szczegółów implementacji; aka hermetyzacja). Ten kontrakt może zapewnić, że po utworzeniu instancji będziesz mieć gotowy do użycia obiekt (tj. Żadnych dodatkowych kroków inicjalizacyjnych do zapamiętania (eee, zapomnienia) przez użytkownika).
(konstruktor) DI niezaprzeczalnie przerywa hermetyzację przez wykrwawianie szczegółów implementacji za pośrednictwem tego publicznego interfejsu konstruktora. Dopóki nadal uważamy, że konstruktor publiczny jest odpowiedzialny za zdefiniowanie kontraktu inicjalizacyjnego dla użytkowników, stworzyliśmy straszne naruszenie hermetyzacji.
Przykład teoretyczny:
Klasa Foo ma 4 metody i potrzebuje do inicjalizacji liczby całkowitej, więc jej konstruktor wygląda jak Foo (rozmiar int) i od razu jest jasne dla użytkowników klasy Foo , że muszą podać rozmiar w instancji, aby Foo działało.
Powiedzmy, że ta konkretna implementacja Foo może również potrzebować widgetu Widget do wykonania swojej pracy. Wstrzyknięcie konstruktora tej zależności spowodowałoby utworzenie konstruktora takiego jak Foo (rozmiar int, widżet IWidget)
Irytuje mnie to, że mamy teraz konstruktor, który miesza dane inicjalizacyjne z zależnościami - jedno wejście jest interesujące dla użytkownika klasy ( rozmiar ), drugie jest wewnętrzną zależnością, która służy tylko zmyleniu użytkownika i jest implementacją szczegół ( widżet ).
Parametr size NIE jest zależnością - to po prostu wartość inicjująca dla instancji. IoC jest dobre dla zależności zewnętrznych (jak widget), ale nie dla inicjalizacji stanu wewnętrznego.
Co gorsza, co jeśli Widget jest potrzebny tylko dla 2 z 4 metod tej klasy; Mogę ponosić narzut na tworzenie instancji dla Widget, nawet jeśli nie może być używany!
Jak to skompromitować / pogodzić?
Jedną z metod jest przełączenie się wyłącznie na interfejsy w celu zdefiniowania umowy operacyjnej; i zaprzestanie używania konstruktorów przez użytkowników. Aby zachować spójność, dostęp do wszystkich obiektów musiałby być możliwy tylko za pośrednictwem interfejsów, a ich instancje byłyby tworzone tylko przez jakąś formę mechanizmu rozstrzygającego (np. Kontener IOC / DI). Tylko kontener może tworzyć instancje.
To rozwiązuje kwestię zależności Widget, ale jak zainicjować „rozmiar” bez uciekania się do oddzielnej metody inicjalizacji w interfejsie Foo? Korzystając z tego rozwiązania, straciliśmy możliwość zapewnienia pełnego zainicjowania instancji Foo przed jej uzyskaniem. Szkoda, bo bardzo podoba mi się pomysł i prostota wtrysku konstruktora.
Jak osiągnąć gwarantowaną inicjalizację w tym świecie DI, kiedy inicjalizacja to WIĘCEJ niż TYLKO zewnętrzne zależności?
źródło
Czyste kapsułkowanie jest ideałem, którego nigdy nie można osiągnąć. Gdyby wszystkie zależności były ukryte, w ogóle nie potrzebowałbyś DI. Pomyśl o tym w ten sposób, jeśli naprawdę masz prywatne wartości, które można zinternalizować w obiekcie, na przykład wartość całkowitą prędkości obiektu samochodu, to nie masz żadnej zewnętrznej zależności i nie ma potrzeby odwracania lub wstrzykiwania tej zależności. Tego rodzaju wewnętrzne wartości stanu, które są obsługiwane wyłącznie przez funkcje prywatne, są tym, co chcesz zawsze hermetyzować.
Ale jeśli budujesz samochód, który potrzebuje określonego rodzaju obiektu silnika, masz zależność zewnętrzną. Możesz utworzyć instancję tego silnika - na przykład nową GMOverHeadCamEngine () - wewnętrznie w konstruktorze obiektu samochodu, zachowując hermetyzację, ale tworząc znacznie bardziej podstępne połączenie z konkretną klasą GMOverHeadCamEngine, lub możesz wstrzyknąć go, umożliwiając działanie obiektu Car agnostycznie (i znacznie bardziej niezawodnie) na przykład na interfejsie IEngine bez konkretnej zależności. Nie chodzi o to, czy używasz kontenera IOC, czy prostego DI, aby to osiągnąć - chodzi o to, że masz samochód, który może korzystać z wielu rodzajów silników bez łączenia się z żadnym z nich, dzięki czemu Twoja baza kodów jest bardziej elastyczna i mniej podatne na skutki uboczne.
DI nie stanowi naruszenia hermetyzacji, jest to sposób na zminimalizowanie sprzężenia, gdy hermetyzacja jest koniecznie zerwana, co jest oczywiste w praktycznie każdym projekcie OOP. Wstrzyknięcie zależności do interfejsu zewnętrznie minimalizuje efekty uboczne sprzęgania i pozwala klasom pozostać niezależnymi od implementacji.
źródło
Zależy to od tego, czy zależność jest naprawdę szczegółem implementacji, czy czymś, o czym klient chciałby / musiałby wiedzieć w taki czy inny sposób. Istotną rzeczą jest to, do jakiego poziomu abstrakcji jest kierowana klasa. Oto kilka przykładów:
Jeśli masz metodę, która używa buforowania pod maską do przyspieszenia wywołań, to obiekt pamięci podręcznej powinien być Singletonem lub czymś podobnym i nie powinien być wstrzykiwany. Fakt, że pamięć podręczna jest w ogóle używana, jest szczegółem implementacyjnym, o który klienci Twojej klasy nie powinni się martwić.
Jeśli twoja klasa musi wyprowadzać strumienie danych, prawdopodobnie sensowne jest wstrzyknięcie strumienia wyjściowego, aby klasa mogła łatwo wyprowadzić wyniki do tablicy, pliku lub gdziekolwiek indziej ktoś mógłby chcieć wysłać dane.
Dla szarego obszaru załóżmy, że masz klasę, która przeprowadza symulację Monte Carlo. Potrzebuje źródła losowości. Z jednej strony fakt, że jest to potrzebne, jest szczegółem implementacji, ponieważ klient naprawdę nie dba o to, skąd pochodzi losowość. Z drugiej strony, ponieważ generatory liczb losowych w świecie rzeczywistym wymagają kompromisów między stopniem losowości, szybkością itp., Które klient może chcieć kontrolować, a klient może chcieć kontrolować wypełnianie, aby uzyskać powtarzalne zachowanie, wstrzyknięcie może mieć sens. W tym przypadku sugerowałbym zaoferowanie sposobu tworzenia klasy bez określania generatora liczb losowych i domyślnego użycia Singletona lokalnego dla wątku. Jeśli / kiedy pojawi się potrzeba dokładniejszej kontroli, zapewnij inny konstruktor, który pozwala na wstrzyknięcie źródła losowości.
źródło
Wierzę w prostotę. Zastosowanie IOC / Dependecy Injection w klasach domeny nie powoduje żadnych ulepszeń, poza tym, że kod jest znacznie trudniejszy do mainowania poprzez posiadanie zewnętrznych plików xml opisujących relację. Wiele technologii, takich jak EJB 1.0 / 2.0 i Struts 1.1, cofa się, redukując zawartość XML i próbując umieścić je w kodzie jako adnotacje itp. Zatem zastosowanie IOC do wszystkich rozwijanych klas sprawi, że kod będzie pozbawiony sensu.
IOC ma zalety, gdy obiekt zależny nie jest gotowy do utworzenia w czasie kompilacji. Może się to zdarzyć w większości komponentów architektury na poziomie abstrakcyjnym infrasture, próbując ustanowić wspólne ramy podstawowe, które mogą wymagać pracy w różnych scenariuszach. W tych miejscach użycie IOC ma większy sens. Mimo to nie czyni to kodu prostszym / łatwiejszym w utrzymaniu.
Podobnie jak wszystkie inne technologie, ta również ma zalety i wady. Martwię się, że wdrażamy najnowsze technologie we wszystkich miejscach, niezależnie od ich najlepszego kontekstu wykorzystania.
źródło
Hermetyzacja jest zepsuta tylko wtedy, gdy klasa jest zarówno odpowiedzialna za utworzenie obiektu (co wymaga znajomości szczegółów implementacji), jak i używa klasy (która nie wymaga znajomości tych szczegółów). Wyjaśnię dlaczego, ale najpierw szybka anaologia samochodu:
Ale wracając do kodowania. Hermetyzacja to „ukrywanie szczegółów implementacji przed czymś, co używa tej implementacji”. Hermetyzacja to dobra rzecz, ponieważ szczegóły implementacji mogą ulec zmianie bez wiedzy użytkownika klasy.
W przypadku korzystania z iniekcji zależności iniekcja konstruktora służy do konstruowania obiektów typu usługi (w przeciwieństwie do obiektów jednostki / wartości, które modelują stan). Wszystkie zmienne składowe w obiekcie typu usługi reprezentują szczegóły implementacji, które nie powinny wyciekać. np. numer portu gniazda, referencje bazy danych, inna klasa do wywołania w celu wykonania szyfrowania, pamięć podręczna itp.
Konstruktor jest istotne, gdy klasa jest początkowo tworzony. Dzieje się tak na etapie budowy, gdy kontener DI (lub fabryka) łączy ze sobą wszystkie obiekty usługowe. Kontener DI wie tylko o szczegółach implementacji. Wie wszystko o szczegółach implementacji, tak jak ludzie w fabryce Kombi wiedzą o świecach zapłonowych.
W czasie wykonywania obiekt usługi, który został utworzony, jest nazywany apon, aby wykonać prawdziwą pracę. W tym momencie obiekt wywołujący obiekt nie wie nic o szczegółach implementacji.
Wróćmy teraz do hermetyzacji. Jeśli szczegóły implementacji ulegną zmianie, klasa używająca tej implementacji w czasie wykonywania nie musi się zmieniać. Hermetyzacja nie jest zepsuta.
Jeśli szczegóły implementacji ulegną zmianie, kontener DI (lub fabryka) musi ulec zmianie. Przede wszystkim nigdy nie próbowałeś ukrywać szczegółów wdrożenia przed fabryką.
źródło
Po dłuższym zmaganiu się z tym problemem jestem teraz zdania, że Dependency Injection (w tej chwili) narusza w pewnym stopniu hermetyzację. Nie zrozum mnie jednak źle - myślę, że użycie wstrzykiwania zależności jest w większości przypadków warte kompromisu.
Powód, dla którego DI narusza hermetyzację, staje się jasny, gdy komponent, nad którym pracujesz, ma zostać dostarczony stronie „zewnętrznej” (pomyśl o napisaniu biblioteki dla klienta).
Gdy mój składnik wymaga wstrzyknięcia podskładników za pośrednictwem konstruktora (lub właściwości publicznych), nie ma gwarancji
Jednocześnie nie można tego powiedzieć
Oba cytaty pochodzą z Wikipedii .
Aby podać konkretny przykład: muszę dostarczyć bibliotekę DLL po stronie klienta, która upraszcza i ukrywa komunikację z usługą WCF (zasadniczo zdalna fasada). Ponieważ zależy to od 3 różnych klas proxy WCF, jeśli zastosuję podejście DI, jestem zmuszony ujawnić je za pośrednictwem konstruktora. W ten sposób odsłaniam wnętrze mojej warstwy komunikacyjnej, którą staram się ukryć.
Generalnie jestem dla DI. W tym konkretnym (skrajnym) przykładzie wydaje mi się to niebezpieczne.
źródło
DI narusza hermetyzację obiektów niewspółdzielonych - kropka. Obiekty współużytkowane mają żywotność poza tworzonym obiektem i dlatego muszą być AGREGOWANE w tworzonym obiekcie. Obiekty, które są prywatne dla tworzonego obiektu, powinny być KOMPONOWANE do tworzonego obiektu - gdy utworzony obiekt zostanie zniszczony, zabiera ze sobą skomponowany obiekt. Weźmy na przykład ciało ludzkie. Co się składa, a co jest zagregowane. Gdybyśmy mieli użyć DI, konstruktor ciała ludzkiego miałby setki obiektów. Na przykład wiele narządów można (potencjalnie) wymienić. Ale nadal są wkomponowane w ciało. Komórki krwi powstają w organizmie (i są niszczone) każdego dnia, bez konieczności stosowania czynników zewnętrznych (innych niż białko). W ten sposób komórki krwi są tworzone wewnętrznie przez organizm - nowa komórka krwi ().
Zwolennicy DI twierdzą, że obiekt NIGDY nie powinien używać nowego operatora. To „purystyczne” podejście nie tylko narusza hermetyzację, ale także zasadę substytucji Liskova dla każdego, kto tworzy obiekt.
źródło
Z tym pojęciem też się zmagałem. Początkowo „wymaganie” użycia kontenera DI (takiego jak Spring) do utworzenia instancji obiektu sprawiało wrażenie przeskakiwania przez obręcze. Ale w rzeczywistości to tak naprawdę nie jest obręcz - to po prostu kolejny „opublikowany” sposób tworzenia potrzebnych mi obiektów. Jasne, hermetyzacja jest „zepsuta”, ponieważ ktoś „spoza klasy” wie, czego potrzebuje, ale tak naprawdę to nie reszta systemu to wie - to kontener DI. Nic magicznego nie dzieje się inaczej, ponieważ DI „wie”, że jeden przedmiot potrzebuje innego.
W rzeczywistości jest jeszcze lepiej - skupiając się na fabrykach i repozytoriach, nie muszę nawet wiedzieć, że DI jest w ogóle zaangażowane! To dla mnie stawia wieczko na hermetyzacji. Uff!
źródło
PS. Dostarczając Dependency Injection , niekoniecznie przerywasz Encapsulation . Przykład:
Kod klienta nadal nie zna szczegółów implementacji.
źródło
Może jest to naiwny sposób myślenia o tym, ale jaka jest różnica między konstruktorem, który przyjmuje parametr będący liczbą całkowitą, a konstruktorem, który przyjmuje usługę jako parametr? Czy to oznacza, że zdefiniowanie liczby całkowitej poza nowym obiektem i wprowadzenie jej do obiektu przerywa hermetyzację? Jeśli usługa jest używana tylko w nowym obiekcie, nie widzę, jak mogłoby to zepsuć hermetyzację.
Ponadto, używając jakiejś funkcji automatycznego okablowania (na przykład Autofac dla C #), sprawia, że kod jest wyjątkowo czysty. Budując metody rozszerzające dla konstruktora Autofac, byłem w stanie wyciąć DUŻO kodu konfiguracyjnego DI, który musiałbym utrzymywać w miarę upływu czasu, gdy lista zależności rosła.
źródło
Myślę, że jest oczywiste, że DI przynajmniej znacząco osłabia hermetyzację. Oprócz tego, oto kilka innych wad DI do rozważenia.
To sprawia, że kod jest trudniejszy do ponownego wykorzystania. Moduł, z którego klient może korzystać bez konieczności jawnego dostarczania zależności, jest oczywiście łatwiejszy w użyciu niż taki, w którym klient musi w jakiś sposób odkryć zależności tego komponentu, a następnie w jakiś sposób je udostępnić. Na przykład komponent pierwotnie utworzony do użycia w aplikacji ASP może oczekiwać, że jego zależności będą dostarczane przez kontener DI, który zapewnia wystąpienia obiektów z okresami istnienia związanymi z żądaniami HTTP klienta. Może to nie być łatwe do odtworzenia na innym kliencie, który nie ma tego samego wbudowanego kontenera DI, co oryginalna aplikacja ASP.
Może sprawić, że kod będzie bardziej kruchy. Zależności zapewniane przez specyfikację interfejsu mogą być implementowane w nieoczekiwany sposób, co prowadzi do powstania całej klasy błędów w czasie wykonywania, które nie są możliwe w przypadku statycznie rozwiązanej konkretnej zależności.
Może to uczynić kod mniej elastycznym w tym sensie, że możesz mieć mniej możliwości wyboru tego, jak chcesz, aby działał. Nie każda klasa musi istnieć przez cały okres istnienia instancji będącej jej właścicielem, jednak w przypadku wielu implementacji DI nie ma innej opcji.
Mając to na uwadze, myślę, że najważniejsze pytanie brzmi: „ czy w ogóle trzeba określić zewnętrznie jakąś zależność? ”. W praktyce rzadko stwierdzałem, że konieczne jest tworzenie zależności dostarczanej zewnętrznie tylko w celu obsługi testów.
Tam, gdzie zależność naprawdę musi być dostarczona z zewnątrz, zwykle sugeruje to, że relacja między obiektami jest współpracą, a nie wewnętrzną zależnością, w którym to przypadku odpowiednim celem jest hermetyzacja każdej klasy, a nie hermetyzacja jednej klasy wewnątrz drugiej .
Z mojego doświadczenia wynika, że głównym problemem związanym z używaniem DI jest to, że niezależnie od tego, czy zaczynasz od frameworka aplikacji z wbudowanym DI, czy dodajesz obsługę DI do swojej bazy kodu, z jakiegoś powodu ludzie zakładają, że skoro masz obsługę DI, to musi być poprawne sposób na utworzenie instancji wszystkiego . Po prostu nigdy nie zadają sobie trudu, aby zadać pytanie „czy ta zależność musi być określana zewnętrznie?”. Co gorsza, zaczynają też próbować zmusić wszystkich do korzystania ze wsparcia DI do wszystkiego .
Rezultatem tego jest to, że Twoja baza kodu nieuchronnie zaczyna przechodzić do stanu, w którym tworzenie dowolnej instancji czegokolwiek w bazie kodu wymaga ogromnej konfiguracji kontenera DI, a debugowanie czegokolwiek jest dwa razy trudniejsze, ponieważ masz dodatkowe obciążenie pracą, próbując zidentyfikować, jak i gdzie wszystko zostało utworzone.
Więc moja odpowiedź na to pytanie jest taka. Użyj DI, jeśli możesz zidentyfikować rzeczywisty problem, który rozwiązuje za Ciebie, a którego nie możesz rozwiązać po prostu w inny sposób.
źródło
Zgadzam się, że doprowadzone do skrajności, DI może naruszać hermetyzację. Zwykle DI ujawnia zależności, które nigdy nie zostały naprawdę hermetyzowane. Oto uproszczony przykład zapożyczony z książki Miško Hevery's Singletons are Pathological Liars :
Zaczynasz od testu karty kredytowej i piszesz prosty test jednostkowy.
W przyszłym miesiącu otrzymasz rachunek na 100 $. Dlaczego zostałeś obciążony? Test jednostkowy wpłynął na produkcyjną bazę danych. Wewnętrznie dzwoni CreditCard
Database.getInstance()
. Refaktoryzacja karty kredytowej tak, aby pobierałaDatabaseInterface
w konstruktorze, ujawnia fakt, że istnieje zależność. Ale argumentowałbym, że zależność nigdy nie została hermetyzowana, ponieważ klasa CreditCard powoduje zewnętrznie widoczne skutki uboczne. Jeśli chcesz przetestować CreditCard bez refaktoryzacji, z pewnością możesz zaobserwować zależność.Myślę, że nie warto się martwić, czy ujawnienie bazy danych jako zależności zmniejsza hermetyzację, ponieważ jest to dobry projekt. Nie wszystkie decyzje DI będą tak proste. Jednak żadna z pozostałych odpowiedzi nie zawiera kontrprzykładu.
źródło
CreditCard
zmienić się z WebAPI na PayPal, bez konieczności zmiany czegokolwiek poza klasą?Myślę, że to kwestia zakresu. Definiując hermetyzację (nie mówiąc jak), musisz zdefiniować, czym jest hermetyzowana funkcjonalność.
Klasa taka, jaka jest : jedyną odpowiedzialnością klasy jest to, co hermetyzujesz. Co wie, jak to zrobić. Na przykład sortowanie. Jeśli wstrzykniesz jakiś komparator do zamawiania, powiedzmy, klientów, to nie jest częścią hermetyzowanej rzeczy: szybkiego sortowania.
Skonfigurowana funkcjonalność : jeśli chcesz zapewnić gotową do użycia funkcjonalność, nie udostępniasz klasy QuickSort, ale instancję klasy QuickSort skonfigurowaną z komparatorem. W takim przypadku kod odpowiedzialny za tworzenie i konfigurację musi być ukryty przed kodem użytkownika. I to jest hermetyzacja.
Kiedy programujesz klasy, to implementując pojedyncze obowiązki do klas, używasz opcji 1.
Kiedy programujesz aplikacje, robisz coś, co wymaga użytecznej, konkretnej pracy, a następnie regularnie używasz opcji 2.
Oto implementacja skonfigurowanej instancji:
Oto jak używa go inny kod klienta:
Jest hermetyzowany, ponieważ jeśli zmienisz implementację (zmienisz
clientSorter
definicję fasoli), nie zakłóci to użytkowania klienta. Być może, gdy używasz plików xml ze wszystkimi zapisanymi razem, widzisz wszystkie szczegóły. Ale uwierz mi, kod klienta (ClientService
) nie wie nic o swoim sorterze.źródło
Warto chyba wspomnieć, że
Encapsulation
jest to w pewnym stopniu zależne od perspektywy.Z perspektywy osoby pracującej na
A
zajęciach w drugim przykładzieA
dużo mniej wie o naturzethis.b
Natomiast bez DI
vs
Osoba przeglądająca ten kod wie więcej o naturze
A
w drugim przykładzie.W przypadku DI przynajmniej cała ta wyciekła wiedza jest w jednym miejscu.
źródło