Czy zastrzyk zależności musi odbywać się kosztem enkapsulacji?


128

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.


7
to jest bardzo mądre pytanie imho
dfa

5
Odpowiedź na to pytanie wymaga najpierw sporu o to, co oznacza hermetyzacja . ;)
Jeff Sternal

2
Hermetyzacja jest utrzymywana przez interfejsy. Ujawniają podstawowe cechy obiektu i ukrywają szczegóły, takie jak zależności. To pozwala nam do pewnego stopnia „otworzyć” klasy, aby zapewnić bardziej elastyczną konfigurację.
Lawrence Wagerfield,

Odpowiedzi:


62

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 :)


18
Hermetyzacja to nie tylko wymyślna terminologia. To prawdziwa rzecz, która przynosi realne korzyści i nie ma znaczenia, czy uważasz, że Twój program jest „zorientowany na komponenty” czy „zorientowany obiektowo”. Hermetyzacja ma chronić stan twojego obiektu / komponentu / usługi / czegokolwiek przed zmianą w nieoczekiwany sposób, a IoC odbiera część tej ochrony, więc zdecydowanie jest kompromis.
Ron Inbar

1
Argumenty dostarczane przez konstruktor nadal mieszczą się w zakresie oczekiwanych sposobów, w jakie obiekt może zostać „zmieniony”: są jawnie ujawniane, a niezmienniki wokół nich są wymuszane. Ukrywanie informacji to lepsze określenie rodzaju prywatności, o którym się mówisz, @RonInbar, i niekoniecznie zawsze jest korzystne (utrudnia rozplątywanie makaronu ;-)).
Nicholas Blumhardt

2
Cały sens OOP polega na tym, że plątanina makaronu jest podzielona na oddzielne klasy i że musisz w ogóle z nią mieszać, jeśli jest to zachowanie tej konkretnej klasy, którą chcesz zmodyfikować (w ten sposób OOP zmniejsza złożoność). Klasa (lub moduł) hermetyzuje swoje elementy wewnętrzne, jednocześnie udostępniając wygodny interfejs publiczny (w ten sposób OOP ułatwia ponowne użycie). Klasa, która ujawnia swoje zależności za pośrednictwem interfejsów, tworzy złożoność dla swoich klientów i w rezultacie jest mniej wielokrotnego użytku. Jest również z natury bardziej delikatny.
Neutrino

1
Bez względu na to, jak na to patrzę, wydaje mi się, że DI poważnie podważa niektóre z najcenniejszych korzyści z OOP, a ja jeszcze nie spotkałem się z sytuacją, w której faktycznie znalazłem to używane w sposób, który rozwiązałby prawdziwy istniejący problem.
Neutrino

1
Oto inny sposób ujęcia problemu: wyobraź sobie, że zestawy .NET wybrałyby „hermetyzację” i nie zadeklarowały, na których innych zespołach polegały. To byłaby szalona sytuacja, czytając dokumenty i mając nadzieję, że po załadowaniu coś działa. Deklarowanie zależności na tym poziomie pozwala zautomatyzowanym narzędziom radzić sobie z kompozycją aplikacji na dużą skalę. Musisz zmrużyć oczy, aby zobaczyć analogię, ale podobne siły działają na poziomie komponentów. Są kompromisy i YMMV jak zawsze :-)
Nicholas Blumhardt

29

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ępnienie Fooobiektu.

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 IFooprzez DI, bez martwienia się o to, co konstruktor FooImplwskazuje, że musi wykonać swoją pracę, a nawet nie zdając sobie sprawy z FooImpljego 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.


2
Słuszne uwagi. Odradzam używanie @Autowired na polach prywatnych; to sprawia, że ​​klasa jest trudna do przetestowania; jak następnie wstrzyknąć kpiny lub niedopałki?
lumpynose

4
Nie zgadzam się. DI narusza hermetyzację i można tego uniknąć. Na przykład używając ServiceLocator, który oczywiście nie musi nic wiedzieć o klasie klienta; musi tylko wiedzieć o implementacjach zależności Foo. Jednak w większości przypadków najlepszym rozwiązaniem jest użycie operatora „nowy”.
Rogério,

5
@Rogerio - Prawdopodobnie każda struktura DI działa dokładnie tak, jak opisywany przez Ciebie ServiceLocator; klient nie wie nic konkretnego o implementacjach Foo, a narzędzie DI nie wie nic konkretnego o kliencie. Używanie „nowego” jest znacznie gorsze w przypadku naruszenia hermetyzacji, ponieważ musisz znać nie tylko dokładną klasę implementacji, ale także dokładne klasy i wystąpienia wszystkich potrzebnych zależności.
Andrzej Doyle

4
Używanie „new” do tworzenia instancji klasy pomocniczej, która często nie jest nawet publiczna, promuje hermetyzację. Alternatywą dla DI byłoby upublicznienie klasy pomocnika i dodanie publicznego konstruktora lub metody ustawiającej w klasie klienta; obie zmiany zerwałyby hermetyzację zapewnianą przez oryginalną klasę pomocniczą.
Rogério

1
„To dobre pytanie - ale w pewnym momencie hermetyzacja w jej najczystszej postaci musi zostać naruszona, jeśli obiekt ma kiedykolwiek spełnić swoją zależność” Podstawowa przesłanka twojej odpowiedzi jest po prostu nieprawdziwa. Jak stwierdza @ Rogério, tworzenie nowej zależności wewnętrznie, a każda inna metoda, w której obiekt wewnętrznie spełnia swoje własne zależności, nie narusza hermetyzacji.
Neutrino

17

To uwidacznia wstrzykiwaną zależność i narusza zasadę hermetyzacji OOP.

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 .

„Ponieważ dziedziczenie naraża podklasę na szczegóły implementacji jej rodzica, często mówi się, że 'dziedziczenie przerywa hermetyzację'”. (Gang of Four 1995: 19)

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.

switch (principle)
{
      case encapsulation:
           if (there_is_a_reason)
      break!
}

3
„To są moje zasady, a jeśli ich nie lubisz… cóż, mam inne”. (Groucho Marx)
Ron Inbar

2
Myślę, że o to właśnie chodzi. To zależne wstrzyknięcie vs hermetyzacja. Dlatego należy stosować zastrzyk zależny tylko wtedy, gdy daje znaczące korzyści. To DI wszędzie daje DI złą
sławę

Nie jestem pewien, co ta odpowiedź próbuje powiedzieć ... Że można naruszyć hermetyzację podczas wykonywania DI, że jest to „zawsze ok”, ponieważ i tak byłoby to naruszone, czy po prostu DI może być powodem do naruszenia hermetyzacji? Z drugiej strony, w dzisiejszych czasach nie ma już potrzeby polegania na publicznych konstruktorach lub właściwościach w celu wstrzykiwania zależności; zamiast tego możemy wstrzyknąć do prywatnych pól z adnotacjami , co jest prostsze (mniej kodu) i zachowuje hermetyzację. Dzięki temu możemy jednocześnie korzystać z obu zasad.
Rogério

Dziedziczenie w zasadzie nie narusza hermetyzacji, chociaż może się tak stać, jeśli klasa nadrzędna jest źle napisana. Inne kwestie, które poruszysz, to jeden dość skrajny paradygmat programowania i kilka funkcji języka, które mają niewiele wspólnego z architekturą lub projektem.
Neutrino

13

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?


Myślenie z wyprzedzeniem nie jest straszne, ale zgadzam się Keep It Simple, Stupid, zwłaszcza jeśli nie będziesz tego potrzebować! Widziałem, jak deweloperzy marnują cykle, ponieważ zbytnio projektują coś, co ma być zbyt przyszłościowe i opiera się na przeczuciach, nawet na znanych / podejrzewanych wymaganiach biznesowych.
Jacob McKay

5

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.


3
Jeśli masz sytuację, w której obiekt klienta MOŻE bezpośrednio utworzyć instancję swoich zależności, dlaczego tego nie zrobić? To zdecydowanie najłatwiejsza rzecz do zrobienia i niekoniecznie zmniejsza testowalność. Oprócz prostoty i lepszej enkapsulacji, ułatwia to również posiadanie obiektów stanowych zamiast bezstanowych singletonów.
Rogério,

1
Dodanie do tego, co powiedział @ Rogério, jest również potencjalnie znacznie bardziej wydajne. Nie każda klasa kiedykolwiek stworzona w historii świata musiała mieć instancję każdej ze swoich zależności przez cały okres istnienia obiektu będącego właścicielem. Obiekt korzystający z DI traci najbardziej podstawową kontrolę nad własnymi zależnościami, a mianowicie ich żywotnością.
Neutrino

5

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:

  1. Wszystko związane z obiektem jest metodą na obiekcie. Tak więc, Fileobiekt może mieć metod do Save, Print, Display, ModifyText, itd.
  2. Obiekt jest swoim własnym małym światkiem i nie jest zależny od zewnętrznego zachowania.

Te dwie definicje są ze sobą w bezpośredniej sprzeczności. Jeśli Fileobiekt 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ć ( IFilePrinterlub jakimś takim interfejsie), to Fileobiekt 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.


Zdecydowanie zgadzam się z Tobą co do pierwszej definicji. Narusza również SoC, który jest prawdopodobnie jednym z głównych grzechów programowania i prawdopodobnie jednym z powodów, dla których nie jest skalowany.
Marcus Stade

4

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ę.


1
Ponieważ to ... nie? Jeśli masz program rejestrujący i masz klasę, która go potrzebuje, przekazanie programu rejestrującego do tej klasy nie narusza hermetyzacji. I to wszystko jest zastrzykiem zależności.
jrockway

3
Myślę, że źle rozumiesz hermetyzację. Na przykład weź naiwną randkę. Wewnętrznie może mieć zmienne instancji dnia, miesiąca i roku. Gdyby zostały one ujawnione jako proste ustawiacze bez logiki, zerwałoby to hermetyzację, ponieważ mógłbym zrobić coś takiego, jak ustawić miesiąc na 2 i dzień na 31. Z drugiej strony, jeśli ustawiacze są sprytni i sprawdzają niezmienniki, wszystko jest w porządku . Zauważ również, że w drugiej wersji mogłem zmienić pamięć na dni od 01.01.1970 i nic, co korzystało z interfejsu, nie musiało być tego świadome, pod warunkiem, że odpowiednio przepiszę metody dzień / miesiąc / rok.
Jason Watkins

2
DI zdecydowanie narusza hermetyzację / ukrywanie informacji. Jeśli zmienisz prywatną wewnętrzną zależność w coś ujawnionego w publicznym interfejsie klasy, to z definicji złamałeś hermetyzację tej zależności.
Rogério,

2
Mam konkretny przykład, w którym myślę, że kapsułkowanie jest zagrożone przez DI. Mam FooProvider, który pobiera "dane foo" z bazy danych i FooManager, który je buforuje i oblicza rzeczy na wierzchu dostawcy. Konsumenci mojego kodu omyłkowo udawali się do dostawcy FooProvider w celu uzyskania danych, gdzie wolałbym, aby był on hermetyzowany, aby byli świadomi tylko FooManager. To jest w zasadzie przyczyna mojego pierwotnego pytania.
urig

1
@Rogerio: Twierdzę, że konstruktor nie jest częścią interfejsu publicznego, ponieważ jest używany tylko w katalogu głównym kompozycji. Zatem zależność jest „widziana” tylko przez korzeń kompozycji. Jedynym obowiązkiem katalogu głównego kompozycji jest połączenie tych zależności razem. Zatem użycie iniekcji konstruktora nie przerywa żadnej hermetyzacji.
Jay Sullivan,

4

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?


Aktualizacja: Właśnie zauważyłem, że Unity 2.0 obsługuje dostarczanie wartości dla parametrów konstruktora (takich jak inicjatory stanu), podczas gdy nadal używa normalnego mechanizmu dla IoC zależności podczas resetu (). Być może inne kontenery również to obsługują? To rozwiązuje techniczną trudność mieszania stanu init i DI w jednym konstruktorze, ale nadal narusza hermetyzację!
shawnT

Słyszę cię. Zadałem to pytanie, ponieważ ja również czuję, że dwie dobre rzeczy (DI i hermetyzacja) są jedna kosztem drugiej. BTW, w twoim przykładzie, gdzie tylko 2 z 4 metod wymagają IWidget, co wskazywałoby, że pozostałe 2 należą do innego komponentu IMHO.
urig

3

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.


3

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.


2

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.


2

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:

Kiedy jechałem moim starym Kombi z 1971 roku, mogłem wcisnąć pedał przyspieszenia i jechało (trochę) szybciej. Nie musiałem wiedzieć dlaczego, ale faceci, którzy zbudowali Kombi w fabryce, wiedzieli dokładnie dlaczego.

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.

To ja jeżdżę moim Kombi na plażę.

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.

Na plażę też mogę pojechać swoim nowym samochodem. 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ą.


Jak przetestowałbyś jednostkę w swojej fabryce? A to oznacza, że ​​klient musi wiedzieć o fabryce, aby otrzymać sprawny samochód, co oznacza, że ​​potrzebujesz fabryki dla każdego innego obiektu w swoim systemie.
Rodrigo Ruiz

2

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

„zapobieganie ustawianiu przez użytkowników wewnętrznych danych komponentu w stan nieprawidłowy lub niespójny”.

Jednocześnie nie można tego powiedzieć

„użytkownicy komponentu (innych części oprogramowania) muszą tylko wiedzieć, co robi komponent i nie mogą uzależniać się od szczegółów tego, jak to robi” .

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.


2

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.


1

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!


1
Tak długo, jak DI jest odpowiedzialne za cały łańcuch tworzenia instancji, zgadzam się, że występuje pewien rodzaj enkapsulacji. W pewnym sensie, ponieważ zależności są nadal publiczne i mogą być nadużywane. Ale kiedy gdzieś „na górze” łańcucha ktoś musi utworzyć instancję obiektu bez niego przy użyciu DI (może jest to „strona trzecia”), wtedy robi się bałagan. Są narażeni na twoje zależności i mogą ulec pokusie, aby je wykorzystać. Albo mogą w ogóle nie chcieć o nich wiedzieć.
urig

1

PS. Dostarczając Dependency Injection , niekoniecznie przerywasz Encapsulation . Przykład:

obj.inject_dependency(  factory.get_instance_of_unknown_class(x)  );

Kod klienta nadal nie zna szczegółów implementacji.


Jak w twoim przykładzie cokolwiek jest wstrzykiwane? (Z wyjątkiem nazewnictwa twojej funkcji ustawiającej)
foo

1

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.


Nie chodzi o stosunek wartości do usługi. Chodzi o odwrócenie kontroli - czy konstruktor klasy konfiguruje klasę, czy też ta usługa przejmuje konfigurację klasy. W przypadku których musi znać szczegóły implementacji tej klasy, więc masz inne miejsce do obsługi i synchronizacji.
foo

1

Myślę, że jest oczywiste, że DI przynajmniej znacząco osłabia hermetyzację. Oprócz tego, oto kilka innych wad DI do rozważenia.

  1. 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.

  2. 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.

  3. 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.


0

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.

@Test
public void creditCard_Charge()
{
    CreditCard c = new CreditCard("1234 5678 9012 3456", 5, 2008);
    c.charge(100);
}

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ła DatabaseInterfacew 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ść.

@Before
public void setUp()
{
    Database.setInstance(new MockDatabase());
}

@After
public void tearDown()
{
    Database.resetInstance();
}

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.


1
Testy jednostkowe są zwykle pisane przez autora klasy, więc z technicznego punktu widzenia można opisać zależności w przypadku testowym. Gdy później klasa karty kredytowej zmieni się, aby korzystać z internetowego interfejsu API, takiego jak PayPal, użytkownik musiałby zmienić wszystko, gdyby zostało to DIED. Testy jednostkowe są zwykle wykonywane z dokładną wiedzą na temat testowanego przedmiotu (czy nie o to chodzi?), Więc myślę, że testy są raczej wyjątkiem niż typowym przykładem.
kizzx2

1
Celem DI jest uniknięcie opisanych zmian. Jeśli przełączysz się z internetowego interfejsu API na PayPal, nie zmienisz większości testów, ponieważ korzystałyby one z usługi MockPaymentService, a karta kredytowa została zbudowana z usługą PaymentService. Miałbyś tylko kilka testów sprawdzających rzeczywistą interakcję między kartą kredytową a prawdziwą usługą płatności, więc przyszłe zmiany są bardzo odizolowane. Korzyści są jeszcze większe w przypadku głębszych wykresów zależności (takich jak klasa zależna od karty kredytowej).
Craig P. Motlin

@Craig p. Motlin Jak obiekt może CreditCardzmienić się z WebAPI na PayPal, bez konieczności zmiany czegokolwiek poza klasą?
Ian Boyd

@Ian Wspomniałem, że CreditCard powinien zostać refaktoryzowany, aby w konstruktorze wziął DatabaseInterface, który chroni ją przed zmianami w implementacjach DatabaseInterfaces. Może to powinno być jeszcze bardziej ogólne i wziąć interfejs StorageInterface, którego WebAPI mógłby być inną implementacją. Jednak PayPal jest na złym poziomie, ponieważ jest alternatywą dla karty kredytowej. Zarówno PayPal, jak i CreditCard mogą wdrożyć PaymentInterface, aby chronić inne części aplikacji przed tym przykładem.
Craig P. Motlin

@ kizzx2 Innymi słowy, mówienie, że karta kredytowa powinna używać PayPal, jest bezsensowne. W prawdziwym życiu są alternatywą.
Craig P. Motlin

0

Myślę, że to kwestia zakresu. Definiując hermetyzację (nie mówiąc jak), musisz zdefiniować, czym jest hermetyzowana funkcjonalność.

  1. 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.

  2. 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:

<bean id="clientSorter" class="QuickSort">
   <property name="comparator">
      <bean class="ClientComparator"/>
   </property>
</bean>

Oto jak używa go inny kod klienta:

<bean id="clientService" class"...">
   <property name="sorter" ref="clientSorter"/>
</bean>

Jest hermetyzowany, ponieważ jeśli zmienisz implementację (zmienisz clientSorterdefinicję 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.


0

Warto chyba wspomnieć, że Encapsulationjest to w pewnym stopniu zależne od perspektywy.

public class A { 
    private B b;

    public A() {
        this.b = new B();
    }
}


public class A { 
    private B b;

    public A(B b) {
        this.b = b;
    }
}

Z perspektywy osoby pracującej na Azajęciach w drugim przykładzie Adużo mniej wie o naturzethis.b

Natomiast bez DI

new A()

vs

new A(new B())

Osoba przeglądająca ten kod wie więcej o naturze Aw drugim przykładzie.

W przypadku DI przynajmniej cała ta wyciekła wiedza jest w jednym miejscu.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.