Recenzje pierwszego wydania

Realne przykłady pozwalają skupić się na tym co ważne… Prawdziwa gratka.

Glenn Block

Microsoft

Dobrze napisana, wnikliwa, łatwa do zastosowania i… ponadczasowa.

David Barkol

Neudesic

Zaspokaja wielką potrzebę dla projektantów .NET.

Paul Grebenc

PCA Services

Tłumaczy to co niewyjaśnione w tym temacie.

Rama Krishna

3C Software

Bardzo osobisty sposób, by dogłębnie nauczyć się współczesnych zasad rozwoju oprogramowania. Bardzo polecany!

Darren Neimke

HomeStart Finance

Wszystko, co musisz wiedzieć o wstrzykiwaniu zależności… i więcej!

Jonas Bandi

TechTalk

Lektura obowiązkowa na temat wstrzykiwania zależności.

Braj Panda

Capgemini India

Ta książka na pewno będzie kompletnym przewodnikiem do wstrzykiwania zależności w .NET.

Doug Ferguson

Improving Enterprise

Dane oryginału

Original edition copyright © 2019 by Manning Publications Co. All Rights reserved. Title of English-language original: Dependency Injection. Principles, Practices, and Patterns by Mark Seemann, Steven van Deursen, ISBN 9781617294730. Polish-language edition © 2020 by Polish Scientific Publishers PWN Wydawnictwo Naukowe PWN Spółka Akcyjna. All Rights reserved.

Przekład Anna Gurgul

Projekt okładki INT-MEDIA Dawid Mazur na podstawie oryginału

Wydawca Wioleta Szczygielska-Dybciak

Redaktor prowadzący Jolanta Kowalczuk

Redaktor Anna Marecka

Koordynator produkcji Anna Bączkowska

Zastrzeżonych nazw firm i produktów użyto w książce wyłącznie w celu identyfikacji.

Skład wersji elektronicznej na zlecenie Wydawnictwa Naukowego PWN

Monika Lipiec /Woblink

Książka, którą nabyłeś, jest dziełem twórcy i wydawcy. Prosimy, abyś przestrzegał praw, jakie im przysługują. Jej zawartość możesz udostępnić nieodpłatnie osobom bliskim lub osobiście znanym. Ale nie publikuj jej w internecie. Jeśli cytujesz jej fragmenty, nie zmieniaj ich treści i koniecznie zaznacz, czyje to dzieło. A kopiując jej część, rób to jedynie na użytek osobisty.

Szanujmy cudzą własność i prawo.

Więcej na www.legalnakultura.pl

Polska Izba Książki

Copyright © for the Polish edition by Wydawnictwo Naukowe PWN SA

Warszawa 2021

ISBN 978-83-01-21396-1

eBook został przygotowany na podstawie wydania papierowego z 2020 r., (wyd. I)

Warszawa 2021

Wydawnictwo Naukowe PWN SA

02-460 Warszawa, ul. Gottlieba Daimlera 2

tel. 22 69 54 321, faks 22 69 54 288

infolinia 801 33 33 88

e-mail: pwn@pwn.com.pl; reklama@pwn.pl

www.pwn.pl

Spis treści

przedmowa

podziękowania

o książce

o autorach

o ilustracji na okładce

Część 1. Umiejscowienie Wstrzykiwania zależności na mapie

1. Podstawy Wstrzykiwania zależności: co, jak i dlaczego

1.1. Pisanie utrzymywalnego kodu

1.1.1. Powszechne mity na temat DI

1.1.2. Rozumienie celu DI

1.2. Prosty przykład: Hello DI!

1.2.1. Kod Hello DI!

1.2.2. Korzyści (płynące) z DI

1.3. Co wstrzykiwać, a czego nie wstrzykiwać

1.3.1. stabilne zależności

1.3.2. niestabilne zależności

1.4. Zakres DI

1.4.1. kompozycja obiektowa

1.4.2. Cykl życia obiektu

1.4.3. Przechwytywanie

1.4.4. DI w trzech wymiarach

1.5. Konkluzje

2. Pisanie ściśle powiązanego kodu

2.1. Budowanie ściśle powiązanej aplikacji

2.1.1. Poznajmy Mary Rowan

2.1.2. Tworzenie warstwy danych

2.1.3. Tworzenie warstwy domeny

2.1.4. Tworzenie warstwy interfejsu użytkownika

2.2. Ocena aplikacji ściśle powiązanej

2.2.1. Ocena grafu zależności

2.2.2. Ocena tworzenia strukturalności

2.3. Analiza braku strukturalności

2.3.1. Analiza wykresu zależności

2.3.2. Analiza interfejsu dostępu do danych

2.3.3. Inne kwestie

2.4. Konkluzja

3. Pisanie luźno powiązanego kodu

3.1. Ponowne budowanie aplikacji e-commerce

3.1.1. Budowanie bardziej utrzymywanego UI

3.1.2. Budowanie niezależnego modelu domeny

3.1.3. Budowanie nowej warstwy dostępu do danych

3.1.4. Implementacja Adaptera IUserContext specyficznego dla ASP.NET Core

3.1.5. Tworzenie aplikacji w podstawie kompozycji

3.2. Analiza luźno powiązanej implementacji

3.2.1. Zrozumienie interakcji między komponentami

3.2.2. Analizowanie nowego grafu zależności

Część 2. Katalog

4. Wzorce DI

4.1. podstawa kompozycji

4.1.1. Jak działa PODSTAWA KOMPOZYCJI

4.1.2. Użycie KONTENERA DI w PODSTAWIE KOMPOZYCJI

4.1.3. Przykład: Implementowanie PODSTAWY KOMPOZYCJI przy użyciu CZYSTEGO DI

4.1.4. Pozorna eksplozja zależności

4.2. wstrzykiwanie konstruktorem

4.2.1. Jak działa WSTRZYKIWANIE KONSTRUKTOREM

4.2.2. Kiedy używać WSTRZYKIWANIA KONSTRUKTOREM

4.2.3. Znany sposób użycia WSTRZYKIWANIA KONSTRUKTOREM

4.2.4. Przykład: Dodawanie przeliczania walut do wyróżnionych produktów

4.2.5. Podsumowanie

4.3. wstrzykiwanie metodą

4.3.1. Jak działa WSTRZYKIWANIE METODĄ

4.3.2. Kiedy używać WSTRZYKIWANIA METODĄ

4.3.3. Znany sposób użycia WSTRZYKIWANIA METODĄ

4.3.4. Przykład: Dodawanie przewalutowania do ENCJI Product

4.4. WSTRZYKIWANIE WŁAŚCIWOŚCIĄ

4.4.1. Jak działa WSTRZYKIWANIE WŁAŚCIWOŚCIĄ

4.4.2. Kiedy używać WSTRZYKIWANIA WŁAŚCIWOŚCIĄ

4.4.3. Znane zastosowania WSTRZYKIWANIA WŁAŚCIWOŚCIĄ

4.4.4. Przykład: WSTRZYKIWANIE WŁAŚCIWOŚCIĄ jako model rozszerzalności biblioteki do ponownego użycia

4.5. Wybieranie wzorca (do pracy)

5. Antywzorce DI

5.1. ANTYWZORZEC CONTROL FREAK

5.1.1. Przykład: CONTROL FREAK przez nowo powstające ZALEŻNOŚCI

5.1.2. Przykład: CONTROL FREAK przez fabryki

5.1.3. Przykład: control freak przez przeciążone konstruktory

5.1.4. Analiza CONTROL FREAK

5.2. LOKALIZATOR USŁUG

5.2.1. Przykład: ProductService przy użyciu LOKALIZATORA USŁUG

5.2.2. Analiza LOKALIZATORA USŁUG

5.3. kontekst otoczenia

5.3.1. Przykład: Dostęp do czasu przez KONTEKST OTOCZENIA

5.3.2. Przykład: Logowanie przez kontekst otoczenia

5.3.3. Analiza KONTEKSTU OTOCZENIA

5.4. Antywzorzec OGRANICZONEJ KONSTRUKCJI

5.4.1. Przykład: Późne wiązanie ProductRepository

5.4.2. Analiza OGRANICZONEJ KONSTRUKCJI

6. Zapachy kodu

6.1. Radzenie sobie z zapachem kodu Przesadnego Wstrzykiwania Konstruktorem

6.1.1. Rozpoznawanie Przesadnego Wstrzykiwania Konstruktorem

6.1.2. Refaktorowanie z Przesadnego Wstrzykiwania Konstruktorem do Usług fasadowych

6.1.3. Refaktorowanie z Przesadnego Wstrzykiwania Konstruktorem do zdarzeń domeny

6.2. Nadużywanie Fabryk abstrakcyjnych

6.2.1. Nadużywanie Fabryk abstrakcyjnych do pokonania problemów cyklu życia

6.2.2. Nadużywanie Fabryk abstrakcyjnych do wybierania ZALEŻNOŚCI na podstawie danych czasu wykonania

6.3. Naprawianie cyklicznych ZALEŻNOŚCI

6.3.1. Przykład: Cykliczne ZALEŻNOŚCI spowodowane naruszeniem SRP

6.3.2. Analiza cyklu ZALEŻNOŚCI Mary

6.3.3. Refaktoring naruszeń SRP, aby rozwiązać cykl ZALEŻNOŚCI

6.3.4. Powszechne strategie przerywania cyklów ZALEŻNOŚCI

6.3.5. Ostateczność: przerwanie cyklu za pomocą WSTRZYKIWANIA WŁAŚCIWOŚCIĄ

Część 3. Czyste DI

7. Kompozycja aplikacji

7.1. Komponowanie aplikacji konsolowej

7.1.1. Przykład: Uaktualnienie kursu walut przy użyciu programu UpdateCurrency

7.1.2. Budowanie podstawy kompozycji programu UpdateCurrency

7.1.3. Komponowanie grafów obiektów w CreateCurrencyParser

7.1.4. Głębsze spojrzenie na warstwy UpdateCurrency

7.2. Komponowanie aplikacji UWP

7.2.1. Kompozycja UWP

7.2.2. Przykład: Podłączanie aplikacji typu gruby klient do zarządzania produktami

7.2.3. Implementowanie PODSTAWY KOMPOZYCJI w aplikacji UWP

7.3. Komponowanie aplikacji ASP.NET Core MVC

7.3.1. Tworzenie własnego aktywatora kontrolera

7.3.2. Konstruowanie własnej warstwy middleware przy użyciu CZYSTEGO DI

8. Cykl życia obiektu

8.1. Zarządzanie cyklem życia zależności

8.1.1. Wprowadzenie ZARZĄDZANIA CYKLEM ŻYCIA

8.1.2. Zarządzanie cyklem życia przy użyciu CZYSTEGO DI

8.2. Pracowanie z usuwalnymi zależnościami

8.2.1. Konsumowanie usuwalnych ZALEŻNOŚCI

8.2.2. Zarządzanie usuwalnymi ZALEŻNOŚCIAMI

8.3. Katalog STYLÓW ŻYCIA

8.3.1. styl życia singleton

8.3.2. przejściowy styl życia

8.3.3. zakresowy styl życia

8.4. Złe wybory STYLU ŻYCIA

8.4.1. pojmane zależności

8.4.2. Wybór STYLU ŻYCIA wyciekający do klas konsumujących przez CIEKNĄCE ABSTRAKCJE

8.4.3. Powodowanie błędów współbieżności przez łączenie instancji z cyklem życia jednego wątku

9. Przechwytywanie

9.1. Przedstawienie PRZECHWYTYWANIA

9.1.1. Wzorzec projektowy dekorator

9.1.2. Przykład: Implementowanie audytowania przy użyciu Dekoratora

9.2. Implementowanie ZAGADNIEŃ PRZEKROJOWYCH

9.2.1. Przechwytywanie za pomocą wzorca Bezpiecznik

9.2.2. Raportowanie wyjątków przy użyciu wzorca Dekorator

9.2.3. Zapobieganie nieautoryzowanemu dostępowi do wrażliwej funkcjonalności przy użyciu Dekoratora

10. Rozmyślne Programowanie Aspektowe

10.1. Wprowadzenie do AOP

10.2. Zasady SOLID

10.2.1. zasada pojedynczej odpowiedzialności (srp)

10.2.2. zasada otwarte/zamknięte (ocp)

10.2.3. zasada podstawienia liskov (lsp)

10.2.4. zasada segregacji interfejsów (isp)

10.2.5. zasada odwrócenia zależności (dip)

10.2.6. Zasady solid a przechwytywanie

10.3. SOLID jako sterownik dla AOP

10.3.1. Przykład: Implementowanie funkcjonalności związanych z produktem przy użyciu IProductService

10.3.2. Analiza IProductService z perspektywy SOLID

10.3.3. Ulepszanie projektu przez zastosowanie zasad solid

10.3.4. Dodanie większej liczby ZAGADNIEŃ PRZEKROJOWYCH

10.3.5. Wnioski

11. Programowanie Aspektowe oparte na narzędziach

11.1. Dynamiczne PRZECHWYTYWANIE

11.1.1. Przykład: PRZECHWYTYWANIE za pomocą Castle Dynamic Proxy

11.1.2. Analiza dynamicznego PRZECHWYTYWANIA

11.2. Tkanie w czasie kompilacji

11.2.1. Przykład: Zastosowanie aspektu transakcji przy użyciu tkania w czasie kompilacji

11.2.2. Analiza tkania w czasie kompilacji

Część 4. Kontenery DI

12. Wprowadzenie do Kontenera DI

12.1. Wprowadzenie do KONTENERÓW DI

12.1.1. Odkrywanie api kontenerów rozwiązującego ZALEŻNOŚCI

12.1.2. automatyczne podłączanie

12.1.3. Przykład: Implementowanie uproszczonego KONTENERA DI, który wspiera AUTOMATYCZNE PODŁĄCZANIE

12.2. Konfigurowanie KONTENERÓW DI

12.2.1. Konfigurowanie kontenerów za pomocą plików konfiguracyjnych

12.2.2. Kontenery konfiguracyjne przy użyciu KONFIGURACJI JAKO KODU

12.2.3. Konfigurowanie kontenerów przez konwencję przy użyciu AUTO-REJESTRACJI

12.2.4. Mieszanie i dopasowywanie podejść konfiguracji

12.3. Kiedy używać KONTENERA DI

12.3.1. Używanie bibliotek trzecich oznacza koszty i ryzyko

12.3.2. CZYSTE DI daje krótszy cykl feedbacku

12.3.3. Werdykt: kiedy używać kontenera di

13. Kontener DI Autofac

13.1. Wprowadzenie do Autofac

13.1.1. Rozwiązywanie obiektów

13.1.2. Konfigurowanie ContainerBuilder

13.2. Zarządzanie cyklem życia

13.2.1. Konfigurowanie zakresów instancji

13.2.2. Zwalnianie komponentów

13.3. Rejestrowanie trudnych API

13.3.1. Konfigurowanie prymitywnych ZALEŻNOŚCI

13.3.2. Rejestrowanie obiektów za pomocą bloków kodu

13.4. Praca z licznymi komponentami

13.4.1. Wybieranie wśród wielu kandydatów

13.4.2. Podłączenie sekwencji

13.4.3. Podłączanie Dekoratorów

13.4.4. Pisanie Kompozytów

14. Kontener DI Simple Injector

14.1. Wprowadzenie do Simple Injector

14.1.1. Tworzenie obiektów

14.1.2. Konfigurowanie kontenera

14.2. Zarządzanie cyklem życia

14.2.1. Konfigurowanie STYLÓW ŻYCIA

14.2.2. Zwalnianie komponentów

14.2.3. Zakresy otoczenia

14.2.4. Diagnozowanie kontenera pod kątem częstych problemów cyklu życia

14.3. Rejestrowanie trudnych API

14.3.1. Konfigurowanie prymitywnych zależności

14.3.2. Wyciąganie prymitywnych ZALEŻNOŚCI do Obiektów Parametrów

14.3.3. Rejestrowanie obiektów za pomocą bloków kodu

14.4. Praca z licznymi komponentami

14.4.1. Wybieranie spośród licznych kandydatów

14.4.2. Podłączanie sekwencji

14.4.3. Podłączanie Dekoratorów

14.4.4. Podłączanie Kompozytów

14.4.5. Sekwencje są strumieniami

15 . Kontener DI Microsoft.Extensions. DependencyInjection

15.1. Wprowadzenie do Microsoft.Extensions.DependencyInjection

15.1.1. Rozwiązywanie obiektów

15.1.2. Konfigurowanie ServcieCollection

15.2. Zarządzanie cyklem życia

15.2.1. Konfigurowanie STYLÓW ŻYCIA

15.2.2. Zwalnianie komponentów

15.3. Rejestrowanie trudnych API

15.3.1. Konfigurowanie prymitywnych ZALEŻNOŚCI

15.3.2. Wyciąganie prymitywnych ZALEŻNOŚCI do Obiektów Parametru

15.3.3. Rejestrowanie obiektów za pomocą bloków kodu

15.4. Praca z licznymi komponentami

15.4.1. Wybieranie wśród licznych kandydatów

15.4.2. Podłączanie sekwencji

15.4.3. Podłączanie Dekoratorów

15.4.4. Podłączanie Kompozytów

słowniczek terminologii

literatura

Pozycje drukowane

Pozycje online

Inne źródła

Przypisy

przedmowa

Istnieje osobliwy fenomen związany z Microsoftem, który nazywany jest komorą pogłosową Microsoft. Microsoft jest wielką organizacją, a przynależny ekosystem certyfikowanych partnerów Microsoftu zwiększa jeszcze tę wielkość o rzędy wielkości. Jeśli jest się mocno związanym z tym ekosystemem, może być ciężko zauważyć coś poza jego granicami. Zawsze, kiedy poszukuje się rozwiązania problemu z technologii lub produktu Microsoft, to prawdopodobnym rozwiązaniem będzie dorzucenie i wykorzystanie kolejnych produktów Microsoft. Nieważne, co wykrzyczy się w komorze pogłosowej, odpowiedzią jest Microsoft!

Ja (Mark) pracowałem przez lata dla certyfikowanych partnerów Microsoftu, więc kiedy zostałem zatrudniony przez sam Microsoft w 2003, byłem już od dawna osadzony w tej komorze. I przyznam, że ją uwielbiałem! Niedługo po moim zatrudnieniu wysłano mnie na wewnętrzną konferencję technologiczną do Nowego Orleanu. Miałem poznać najnowszą i najlepszą technologię Microsoftu.

Dzisiaj nie potrafię sobie przypomnieć ani jednej prelekcji dotyczącej produktów Microsoftu, w których brałem udział. Ale za to pamiętam ostatni dzień konferencji. Tego dnia głównie nie mogłem się doczekać lotu powrotnego do mojego domu w Danii, ponieważ żadna z prelekcji, na której byłem, nie mogła zaspokoić mojego głodu na ciekawą technologię. Moim priorytetem było znaleźć jakieś miejsce, gdzie mógłbym zająć się e-mailami. Dlatego wybrałem prelekcję, która wydawała mi się marginalnie istotna, i odpaliłem laptopa.

Prelekcja miała swobodną strukturę i brało w niej udział kilku mówców. Jednym z nich był brodaty facet, Martin Fowler, który opowiadał o Test-Driven Development (TDD) i o dynamic mocks. Nigdy wcześniej o nim nie słyszałem i też niezbyt uważnie go słuchałem, jednak coś musiało utkwić mi w głowie.

Niedługo po powrocie do Danii przydzielono mi zadanie przepisania dużego systemu ETL (extract, transform, load) od podstaw. Zdecydowałem się wypróbować TDD (okazało się to bardzo dobrą decyzją). Użycie dynamic mocks przyszło naturalnie jako kolejny etap, ale wskazało także potrzebę zarządzania zależnościami. Był to dla mnie bardzo trudny, ale i bardzo zajmujący problem, o którym nie mogłem przestać myśleć.

To, co rozpoczęło się jako efekt uboczny mojego zainteresowania TDD, stało się pasją samą w sobie. Zbierałem dużo informacji na ten temat, przeczytałem wiele wpisów blogowych, napisałem też kilka blogów sam, eksperymentowałem z kodem, a także rozmawiałem na ten temat z każdym, kto tylko chciał mnie słuchać. W coraz większym stopniu musiałem szukać inspiracji i wskazówek poza komorą pogłosową Microsoftu. W trakcie tych poszukiwań ludzie zaczęli kojarzyć mnie z ruchem ALT.NET, chociaż nigdy nie byłem w nim nazbyt aktywny. Popełniłem wszystkie błędy, których można było popełnić, ale wreszcie stopniowo byłem w stanie rozwinąć spójne zrozumienie Wstrzykiwania zależności (Dependency Injection, DI).

Kiedy wydawnictwo Manning po raz pierwszy skontaktowało się ze mną w sprawie książki na temat wstrzykiwania zależności w .NET, moja reakcja brzmiała „Czy to jest w ogóle potrzebne?”. Wydawało mi się, że wszystkie założenia/pojęcia, których twórca oprogramowania potrzebuje, by zrozumieć DI, zostały już opisane w wielu wpisach blogowych. Czy trzeba było cokolwiek dodawać? Naprawdę w moim odczuciu temat DI w .NET był już omówiony do granic możliwości.

Jednak po dłuższym zastanowieniu się olśniło mnie – wiedza na ten temat zdecydowanie jest dostępna, ale jest też bardzo rozproszona i wykorzystuje dużo sprzecznej terminologii. Przed pierwszym wydaniem tej książki nie istniały żadne publikacje o DI, które próbowałyby zaprezentować spójny opis tego zagadnienia. I kiedy pomyślałem o tym dogłębnie, uświadomiłem sobie, że Manning oferowało mi ogromne wyzwanie i niesamowitą możliwość do zebrania i usystematyzowania wszystkiego, co wiedziałem na temat DI.

Efektem jest ta książka oraz jej poprzednik – pierwsza edycja. Wykorzystano tu .NET Core i C# w celu przedstawienia pełnej terminologii, jak również wprowadzenia do DI, ale mam nadzieję, że wartość tej książki będzie mieć o wiele większy zasięg i dotrze poza platformę. W moim odczuciu język wzorców wyrażony na tych kartkach jest uniwersalny. I niezależnie od tego, czy jest się programistą .NET, czy używa się innej obiektowo-zorientowanej platformy, mam nadzieję, że ta książka pomoże komuś w byciu lepszym inżynierem oprogramowania.

podziękowania

Wdzięczność może wydawać się takim banałem, ale tylko dlatego, że jest to zasadnicza część ludzkiej natury. W trakcie pisania tej książki wielu ludzi dało nam powody, by być wdzięcznymi, i chcielibyśmy właśnie podziękować im wszystkim.

Po pierwsze, pisanie książki w naszym wolnym czasie pozwoliło nam zrozumieć, jak obciążający jest taki projekt dla naszego życia rodzinnego i małżeńskiego. Żona Marka, Cecilie, pozostała przy nim i aktywnie wspierała go w trakcie całego procesu tworzenia książki. Co najważniejsze, rozumiała, jak ważny to jest dla niego projekt. Nadal są razem, a Mark czeka z niecierpliwością na możliwość spędzenia więcej czasu z nią i ich dziećmi, Lineaą i Jarlem. Żona Stevena, Judith, dała mu przestrzeń potrzebną do ukończenia tak potężnego przedsięwzięcia, ale zdecydowanie jest zadowolona, że ten projekt wreszcie został zakończony.

W bardziej zawodowym kontekście, chcemy podziękować wydawnictwu Manning za daną nam możliwość. Inicjatorem projektu był Michael Stephens. Dan Maharry, Marina Michaels oraz Christina Taylor byli naszymi redaktorami i uważnie przyglądali się jakości tekstu. Pomogli nam w dostrzeżeniu słabszych punktów maszynopisu, a także udzielili nam obszernej konstruktywnej krytyki.

Karsten Strøbæk był naszym redaktorem technicznym. Przeczytał wiele wczesnych wersji tekstu i służył nam bardzo pomocnymi komentarzami. Karsten był także przy tym, gdy Mark napisał pierwszą edycję tej książki, i pomógł wtedy przy technicznej korekcie tekstu. W tej edycji korektę tekstu pod względem technicznym naniósł Chris Heneghan, który wyłapał dużo drobnych błędów i nieścisłości w całym tekście.

Po procesie pisania książki przeszliśmy do procesu jej produkcji. Ten etap był nadzorowany przez Anthony’ego Calcara, a redakcja tekstu przez Frances Buran, a wszystkim obrazom i diagramom w książce dokładnie przyglądała się Nichole Beard.

Chcielibyśmy podziękować z imienia i nazwiska wszystkim recenzentom, którzy w różnych stadiach powstawania tej książki udzielali nam swoich komentarzy i spostrzeżeń, a są to: Ajay Bhosale, Björn Nordblom, Cemre Mengu, Dennis Sellinger, Emanuele Origgi, Ernesto Cardenas Cangahuala, Gustavo Gomes, Igor Kochetov, Jeremy Caney, Justin Coulston, Mikkel Arentoft, Pasquale Zirpoli, Robert Morrison, Sergio Romero, Shawn Lam oraz Stephen Byrne. Recenzja książki była możliwa dzięki Ivanowi Martinovicowi, redaktorowi recenzji.

Wielu uczestników Manning Early Access Program (Programu wczesnego dostępu [do książki] w wydawnictwie Manning) zapewniło nam feedback i postawiło dużo trudnych pytań, które odsłoniły słabe części tekstu.

Szczególne podziękowania kierujemy do Jeremy’ego Caneya, który zaczynał jako uczestnik Manning Early Access Program i dostał awans na recenzenta. Dostarczył nam ogromną ilość feedbacku zarówno w warstwie językowej, jak i kontekstualnej. Jego głębokie zrozumienie DI i projektowania oprogramowania jest nieocenione.

Kolejne szczególne podziękowanie należy się Ricowi Slappendelowi. Ric doradzał nam, jak utworzyć aplikacje UWP przy użyciu DI. Jego znajomość WPF, UWP oraz XAML oszczędziła nam niezliczonych godzin i nieprzespanych nocy. To jego porady w całości ukształtowały sekcję 7.2 i przykłady kodu towarzyszącego. Bez pomocy Rica książka prawdopodobnie nie zawierałaby wcale omówienia zagadnienia UWP.

Alex Meyer-Gleaves i Travis Illig recenzowali wczesne wersje rozdziału 13 i dostarczyli nam feedbacku na temat użycia nowej konfiguracji Autofacu i wsparcia Decoratora. Jesteśmy wdzięczni za ich udział.

I wreszcie Mogens Heller Grabe kurtuazyjnie pozwolił nam użyć jego zdjęcia suszarki do włosów podpiętej bezpośrednio do gniazdka!

o książce

Przede wszystkim ta książka jest o Wstrzykiwaniu zależności (Dependency Injection, DI). Jest ona także o .NET, ale to mniej ważna część. I chociaż w przykładach kodu użyto C#, większość kwestii w tej książce może być łatwo zastosowanych w innych językach i na innych platformach. W rzeczywistości wiele podstawowych zasad i schematów poznaliśmy przez czytanie książek, w których w przykładach używano języków Java i C++.

DI jest zestawem pokrewnych wzorców i zasad. To bardziej sposób myślenia o kodzie i jego projektowania niż konkretna technologia. Ostatecznym celem wykorzystywania DI jest stworzenie utrzymywalnego oprogramowania w paradygmacie programowania obiektowego.

Wszystkie koncepcje wykorzystane w tej książce odnoszą się do programowania obiektowego. Problem, który DI rozwiązuje (utrzymywalny kod), jest uniwersalny, jednakże zaproponowane rozwiązanie jest podane w ramach zakresu programowania obiektowego w statycznie typowanych językach: C#, Java, Visual Basic, .NET, C++ itd. Nie można zastosować DI w programowaniu proceduralnym. DI może też nie być najlepszym rozwiązaniem dla języków funkcyjnych i dynamicznych.

DI samo w sobie to mały wzorzec, ale mocno wiąże się z wieloma zasadami i wzorcami obiektowego projektowania oprogramowania. Oprócz tego, że książka od początku do końca konsekwentnie skupia się na DI, omówiono w niej również te inne zagadnienia w świetle szczególnej perspektywy, którą umożliwia właśnie DI. Celem lektury jest więcej niż nauczenie specyfiki DI. To spowodowanie, że będzie się lepszym programistą obiektowym.

kto powinien przeczytać tę książkę?

Kuszące byłoby stwierdzenie, że jest to książka dla wszystkich programistów .NET. Ale obecnie społeczność .NET jest bardzo rozległa i dotyczy programistów pracujących z aplikacjami internetowymi, aplikacjami desktopowymi, mobilnymi, RIA, integracyjnymi, automatyką biurową, systemami zarządzania treścią, a nawet grami. Chociaż .NET jest obiektowy, to nie wszyscy jego programiści piszą kod obiektowy.

Ta książka jest o programowaniu obiektowym, więc chociaż w minimalnym stopniu jej czytelnicy powinni być zainteresowani aspektem obiektowym lub powinni rozumieć, czym jest interfejs. Na pewno przyda się kilka lat zawodowego doświadczenia, wiedza na temat wzorców projektowych lub też zasad SOLID. Tak naprawdę nie oczekujemy, że początkujący zrozumieją coś z tej książki; jest ona głównie skierowana do doświadczonych programistów i architektów oprogramowania.

Przykłady napisane zostały w języku C#, a więc czytelnicy pracujący z innymi językami .NET muszą potrafić czytać i rozumieć C#. Jednak osoby zaznajomione z różnymi językami obiektowymi spoza platformy .NET, takimi jak Java czy C++, mogą uznać tę książkę za wartościową. Dlaczego? Ponieważ treść charakterystyczna dla platformy .NET jest względnie lekka. Osobiście sami czytamy wiele książek o wzorcach z przykładami w Javie i dużo z nich zyskujemy, więc mamy nadzieję, że i ta pozycja będzie służyć nie tylko programistom .NET.

plan

Treść tej książki jest podzielona na cztery części. Idealnie byłoby przeczytać tę książkę od deski do deski, a potem używać jej jako odniesienia w swojej pracy, ale rozumiemy, że można mieć inne priorytety. Dlatego też większość rozdziałów została napisana tak, by móc w każdej chwili rozpocząć czytanie z dowolnego punktu.

Pierwsza część jest znaczącym wyjątkiem. Jest to ogólne wprowadzenie do DI i najlepsze byłoby czytanie tej części po kolei. Druga część to katalog wzorców i im podobnych, a trzecia największa część to przyjrzenie się DI pod trzema różnymi kątami. Czwarta część jest katalogiem trzech bibliotek będących kontenerami di.

W książce często będzie można znaleźć powiązane ze sobą pojęcia i w związku z tym niektóre terminy pojawiają się na długo przed wprowadzeniem swoich definicji. W skrócie napotyka się najpierw na sam termin, a dopiero za parę stron na jego definicję. Aby rozróżnić uniwersalne pojęcia od bardziej lokalnej terminologii, używamy wersalików, tak by łatwiej je wyróżnić w tekście. Wszystkie takie terminy są pobieżnie wytłumaczone w słowniczku pojęć, który składa się również z odniesień do bardziej obszernych opisów.

Część 1 jest ogólnym wprowadzeniem do DI. Jeśli ktoś nie ma wiedzy, czym jest DI, część I jest dobrym miejscem, aby rozpocząć lekturę. Ale nawet jeśli wie się, czym jest DI, można chcieć zaznajomić się z treścią tej części, gdyż pojawia się tam dużo terminologii i ugruntowuje ona kontekst zawarty w reszcie książki. W rozdziale 1 omówione zostają cel i korzyści DI, a także przedstawiony jest podstawowy zarys zagadnienia. W rozdziale 2 pojawia się obszerny i raczej rozległy przykład silnie powiązanego kodu, a w rozdziale 3 wytłumaczyliśmy, jak przekształcić i ponownie zaimplementować ten sam przykład, korzystając z DI. W porównaniu do innych części, część 1 ma bardziej linearny postęp w kwestii swojej treści. Najlepiej czytać każdy rozdział od początku, by zyskać z niego jak najwięcej.

Część 2 jest katalogiem wzorców, antywzorców i zapachów kodu. Tu znajdą się szczegółowe wskazówki, jak zaimplementować DI, oraz informacja o zagrożeniach, na które warto uważać. Rozdział 4 jest katalogiem wzorców projektowych Wstrzykiwania zależności, a rozdział 5 na odwrót – to katalog antywzorców. Rozdział 6 zawiera uogólnione rozwiązania dla często pojawiających się problemów. Tak jak w katalogu, każdy rozdział zawiera zbiór swobodnie powiązanych ze sobą sekcji, które zostały stworzone tak, by móc je czytać zarówno pojedynczo, jak i po kolei.

W części 3 analizujemy DI pod trzema różnymi kątami: kompozycji obiektowej, zarządzania cyklem życia (aplikacji) oraz przechwytywania. W rozdziale 7 omawiamy, jak zaimplementować DI na bazie już istniejących aplikacji opartych na ASP.NET Core i UWP, a także jak zaimplementować DI w aplikacji konsolowej.

W rozdziale 8 omawiamy, jak zarządzać cyklami życia zależności, tak by uniknąć wycieku zasobów. Znajduje się tu mniej ścisłą strukturę w porównaniu do wcześniejszych rozdziałów, ale za to duża część tego rozdziału może być wykorzystana jako katalog dobrze rozpoznanych cyklów życia. Trzy ostatnie rozdziały tej części skupiają się na tym, jak tworzyć aplikacje przy wykorzystaniu zagadnień przekrojowych. I tak rozdział 9 skupia się na podstawach przechwytywania przy użyciu Dekoratorów, a rozdziały 10 i 11 zagłębiają się w pojęciu programowania aspektowego. To w tym miejscu książki będzie można zauważyć wszystkie korzyści całej wcześniejszej pracy. Osobiście z wielu powodów uważamy to miejsce za najważniejsze w naszej książce.

Część 4 jest katalogiem bibliotek kontenerów di. Rozpoczynamy od omówienia, czym są kontenery di i w jaki sposób pasują one do całości. W trzech pozostałych rozdziałach prezentujemy indywidualnie i dość szczegółowo specjalne kontenery: Autofac, Simple Injector oraz Microsoft.Extensions.DependencyInjection. Każdy z rozdziałów wyjaśnia dany kontener raczej w skróconej wersji, by nie tracić miejsca. W związku z tym lepiej, gdyby ktoś przeczytał tylko o jednym lub dwóch kontenerach, które interesują go najbardziej. Te trzy ostatnie rozdziały w wielu kwestiach uważamy za jeden wielki zestaw załączników.

Aby omówienie zasad i schematów DI obyło się bez opierania się na API konkretnego kontenera, większość książki, z wyjątkiem części 4, została napisana bez odnoszenia się do jakiegokolwiek kontenera w szczególności. Dlatego też wymienione kontenery pojawiają się tak intensywnie w części 4. Mamy nadzieję, że właśnie przez utrzymanie dyskusji na poziomie ogólnym książka ta będzie przydatniejsza przez dłuższy czas.

Oczywiście można również użyć pojęć przewijających się od części 1 do części 3 i zastosować je w bibliotekach kontenerów nieopisanych w części 4. Istnieją naprawdę dobre kontenery, których, niestety, nie objaśniamy w tej książce. Niemniej jednak wierzymy, że ta książka może dużo zaoferować również użytkownikom nieopisanych przez nas kontenerów.

konwencja kodu i materiały do pobrania

Książka zawiera wiele przykładów kodu, w większości w C#, ale od czasu do czasu pojawia się też troszkę XML lub JSON. Kod źródłowy w listingach i tekście zapisany został czcionką o stałej szerokości, by odróżnić go od zwykłego tekstu.

Cały kod źródłowy w tej książce został napisany w C# i Visual Studio 2017. Aplikacje ASP.NET Core zostały napisane na podstawie ASP.NET Core v2.1.

Tylko niektóre z technik omówionych opierają się na nowych funkcjach języka. Chcieliśmy znaleźć równowagę między konserwatywnymi a nowoczesnymi stylami kodowania. Kiedy sami piszemy kod w życiu zawodowym, używamy nowoczesnych funkcji języka w o wiele wyższym stopniu, ale i tak w większości te najbardziej zaawansowane funkcje to typy parametryczne i LINQ. Nie chcemy, żeby ktoś sobie myślał, że DI może być używane jedynie z supernowoczesnymi językami.

Napisanie przykładów kodu dla jakiejkolwiek książki to wyzwanie samo w sobie. W porównaniu do dzisiejszych ekranów komputerów, książka pozwala jedynie na bardzo krótkie linijki kodu. Bardzo kusząca była myśl napisania kodu w lakonicznym stylu, gdzie metody i zmienne przyjęłyby krótkie i zagadkowe nazwy. Taki kod w prawdziwym życiu jest już trudny do zrozumienia, nawet jeśli używa się IDE i debuggera. Więc co tu mówić o nadążeniu za nim na papierze. Stwierdziliśmy, że ważne jest, aby nazwy były tak czytelne jak to tylko możliwe. Czasami musieliśmy się uciec do nieortodoksyjnych metod podziału linijek. Cały kod jest kompilowalny, ale czasami jego formatowanie wygląda trochę dziwacznie.

Kod wykorzystuje również słowo kluczowe var z języka C#. Podczas naszej pracy, kiedy szerokość linijki nie jest ograniczona stroną książki, często wykorzystujemy różne style kodowania w trakcie stosowania var. Na potrzeby tej publikacji, by oszczędzić miejsca, używamy var wtedy, kiedy sądzimy, że użycie pełnej deklaracji typu powoduje mniejszą czytelność kodu.

Słowo klasa jest często używane jako synonim słowa typ. W .NET, klasy, struktury, interfejsy, typy wyliczeniowe itd. to wszystko typy, ale ponieważ słowo typ jest przesycone różnymi znaczeniami w zwykłym języku, częste jego użycie powodowałoby niejasności.

Większość kodu w tej książce odnosi się do nadrzędnego przykładu przewijającego się przez rozdziały: sklep internetowy uzupełniony o wewnętrzne aplikacje zarządzające. Jest to chyba najmniej wymyślny przykład, jaki można znaleźć w jakimkolwiek tekście o oprogramowaniu, ale wybraliśmy go z m.in. dwóch powodów:

1. ■ jest to dobrze znany problem domeny dla większości czytelników. Mimo że może wydawać się on nudny, uważamy to za jego plus – nie odciąga on uwagi od najważniejszej kwestii, czyli DI.

2. ■ Ponadto musimy się przyznać, że nie mogliśmy wymyślić żadnej innej domeny, która byłaby na tyle złożona i pozwoliła na wykorzystanie w tych wszystkich różnych aspektach, które stworzyliśmy w naszych głowach.

Napisaliśmy naprawdę dużo kodu na poparcie tych przykładów, ale większość nie trafiła nawet do książki. Tak naprawdę napisaliśmy prawie cały kod na bazie Test-Driven Development (TDD), ale jako że nie jest to praca o TDD, to w zasadzie nie pokazujemy tu testów jednostkowych.

Kod źródłowy dla wszystkich przykładów zamieszczonych w tej książce jest dostępny na stronie wydawnictwa Manning: www.manning.com/books/dependency-injection-principles-practices-patterns.

Plik README.md w pobranym katalogu zawiera instrukcje do kompilacji i uruchomienia kodu.

forum dyskusyjne livebook

Wraz z zakupem Wstrzykiwanie zależności. Zasady, praktyki, wzorce otrzymuje się darmowy dostęp do prywatnego forum internetowego zarządzanego przez Manning Publications. Można tam dodawać komentarze na temat książki, zadawać techniczne pytania i otrzymać pomoc od samych autorów oraz innych użytkowników forum. Aby otrzymać prawo dostępu i subskrypcję, wystarczy wpisać adres https://livebook.manning.com/book/dependency-injection-principles-practices-%20patterns/discussion. Można również dowiedzieć się więcej o forum wydawnictwa i zasadach postępowania pod adresem https://livebook.manning.com/discussion.

Wydawnictwo Manning udostępnia platformę do konstruktywnego dialogu zarówno między samymi czytelnikami, jak i między czytelnikami a autorami. Jednak autorzy nie zobowiązują się do określonego w jakikolwiek sposób uczestnictwa, jako że ich udział na forum jest dobrowolny (i bezpłatny). Sugerujemy zadawanie trudnych i wymagających pytań, inaczej nie będą zainteresowani! Forum książki oraz archiwum wszystkich dyskusji będą dostępne na stronie wydawcy tak długo, jak książka pozostanie w druku i będzie pojawiać się jej reedycja.

o autorach

Steven van Deursen jest Duńczykiem. Pracuje jako niezależny programista .NET oraz architekt. Swoją karierę zawodową w tej dziedzinie rozpoczął w 2002 roku. Mieszka w Nijmegen i lubi pisać kod zarówno dla zabawy, jak i dla zarobku. Oprócz pisania kodu w wolnym czasie Steven trenuje sztuki walki, lubi zjeść na mieście i zdecydowanie ma słabość do dobrej whisky.

Mark Seemann jest programistą, architektem oprogramowania i wykładowcą mieszkającym w Kopenhadze. Pracuje z oprogramowaniem od 1995, a z TDD od 2003 roku, w tym przez 6 lat był konsultantem, programistą i architektem dla Microsoftu. Obecnie Mark zawodowo zajmuje się rozwojem oprogramowania i pracuje zdalnie z Kopenhagi. Lubi czytać, malować, grać na gitarze, pić wino i jeść wyszukane dania.

o ilustracji na okładce

Na okładce Wstrzykiwania zależności. Zasady, praktyki, wzorce zaprezentowano „Kobietę z Vodnjan” (A woman from Vodnjan) – Vodnjan to małe miasteczko w środkowej części chorwackiego półwyspu Istria na Morzu Adriatyckim. Ilustrację zaczerpnięto z reprodukcji albumu tradycyjnych chorwackich strojów z połowy XIX w. Reprodukcja została stworzona przez Nikola Arsenovica i wydana przez Muzeum Etnograficzne w Splicie, w Chorwacji w 2003 roku. Ilustracje otrzymano dzięki uprzejmemu bibliotekarzowi Muzeum Etnograficznego w Splicie. Samo muzeum usytuowane jest w rzymskiej części średniowiecznego centrum miasta: ruinach rezydencji rzymskiego cesarza Dioklecjana z około 304 r.n.e. W książce można znaleźć piękne ilustracje postaci z różnych regionów Chorwacji. Każdemu obrazkowi towarzyszy opis stroju oraz codziennego życia w tej epoce. Vodnjan jest kulturowo i historycznie ważnym miastem usytuowanym na wzgórzu. Pod miastem rozciąga się piękny widok Adriatyku, a samo Vodnjan znane jest z wielu kościołów i skarbów sztuki sakralnej. Kobieta na okładce książki ma na sobie długą czarną lnianą spódnicę oraz krótki czarny żakiet, spod którego wystaje biała lniana koszula. Żakiet zakończony jest niebieskim haftem. Niebieski lniany fartuszek dopełnia całego stroju. Kobieta ma także na głowie czarny kapelusz z dużym rondem, na szyi apaszkę w kwiatki, natomiast w uszach duże kolczyki koła. Jej elegancki strój wskazuje na to, że jest mieszczanką, a nie wieśniaczką. Stroje ludowe z tych okolic są bardziej kolorowe, wykonane z wełny i przyozdobione bogatymi haftami.

Ubiór i styl życia zmieniły się przez 200 lat, a różnorodność regionu, tak bujna w tamtych czasach, powoli odeszła w niepamięć. Dzisiaj trudno odróżnić mieszkańców różnych kontynentów, a co dopiero różnych osad i miasteczek położonych w odległości kilku mil od siebie. Możliwe, że różnorodność kulturową zamieniliśmy na bardziej zróżnicowane życie osobiste. Na pewno zamieniliśmy ją na bardziej zróżnicowane i dynamiczne życie technologiczne.

Wydawnictwo Manning docenia innowacyjność i inicjatywę przemysłu komputerowego za pomocą okładek, które prezentują bogatą różnorodność życia regionalnego sprzed dwóch wieków. Ta różnobarwność jest przywracana do życia przez ilustracje z zabytkowych książek i kolekcji takich jak ta okładka.

Część 1
Umiejscowienie Wstrzykiwania zależności na mapie

Wstrzykiwanie zależności (Dependency Injection, DI) jest jednym z najbardziej niezrozumiałych pojęć programowania obiektowego. Niejasności są liczne i obejmują: terminologię, cel i mechanizmy. Może powinniśmy nazywać ten wzorzec Wstrzykiwaniem zależności, Inwersją zależności, Odwróceniem sterowania albo nawet Połączeniem trzeciej strony? Czy celem DI jest tylko wsparcie testów jednostkowych, czy może istnieje jego szersze zastosowanie? Czy DI to lokalizacja usług? Czy potrzebujemy kontenerów di w celu zastosowana DI?

Powstało wiele wpisów blogowych, artykułów w magazynach, prezentacji podczas rozmaitych konferencji itp., które omawiały DI, ale niestety w wielu użyto sprzecznej terminologii i podzielono się złymi poradami. Klapa na całej linii. Nawet ci duzi i wpływowi na rynku, tak jak Microsoft, dorzucają swoje do tych niejasności.

Tak nie musi być. W tej książce konsekwentnie prezentujemy i używamy stałej terminologii. W większości zaadaptowaliśmy i objaśniliśmy istniejące już pojęcia, które stworzyli inni przed nami, ale czasami dodaliśmy odrobinę terminologii, której wcześniej nikt nie używał. To znacząco pomogło nam w rozwinięciu specyfikacji zakresu oraz granic DI.

Jednym z powodów takiej niespójności i nietrafionych porad jest fakt, że granice DI są dość nieoczywiste. Gdzie kończy się DI, a zaczynają inne obiektowe pojęcia? Naszym zdaniem niemożliwe jest oddzielenie wyraźną linią DI od innych aspektów tworzenia dobrego kodu obiektowego. Omawiając DI, nie możemy nie przywołać innych pojęć, takich jak: SOLID, Czysty kod, a nawet programowanie aspektowe. W naszym odczuciu nie można wiarygodnie opisać DI bez uwzględnienia niektórych ze wspomnianych tematów.

Pierwsza część tej książki pomoże w zrozumieniu, gdzie znajduje się DI w odniesieniu do innych aspektów inżynierii oprogramowania – w pewnym sensie chodzi o umiejscowienie wzorca na mapie. Rozdział 1 jest krótkim przewodnikiem po DI, gdzie opisany jest jego cel, zasady oraz korzyści. To również zarys zakresu reszty książki. W tym rozdziale skupiamy się na ogóle i nie wchodzimy zbytnio w szczegóły. Jeśli ktoś chce dowiedzieć się, czym jest DI i dlaczego powinien się nim interesować, to dobre miejsce, by zacząć czytać. Do rozdziału 1 nie potrzeba jakiejkolwiek wcześniejszej wiedzy na temat DI. A nawet jeśli już coś się wie o tym wzorcu, nadal warto pochylić się nad tym tekstem – może się okazać, że DI to coś całkiem innego, niż się wydawało.

Odnośnie do rozdziału 2 i 3 – są one w całości zarezerwowane i poświęcone jednemu dużemu przykładowi, którego zadaniem jest konkretniejsze wyczucie wzorca DI. W rozdziale 2 ukazaliśmy typową silnie powiązaną implementację przykładowej aplikacji e-commerce – bardziej tradycyjny styl programowania, który mocno kontrastuje z DI. Następnie, w rozdziale 3, ten sam przykład jest zaimplementowany ponownie przy użyciu DI.

W tej części omówimy DI na zasadach ogólnych. Oznacza to, że nie będziemy wykorzystywać żadnych tak zwanych kontenerów di. Istnieje możliwość pełnego zastosowania DI bez żadnego kontenera di, jako że jest to przydatne, ale wciąż opcjonalne, narzędzie. W związku z tym części 1, 2 oraz 3 pomijają mniej więcej w całości kontenery di i skupiają się na DI w kontekście niezależnym od kontenerów. Dopiero w części 4 powracamy do kontenerów di, by przeanalizować trzy specyficzne biblioteki.

Część 1 nadaje kontekst całej książce. Stworzona została w tej formie dla czytelników, którzy nie mieli wcześniej styczności z DI, ale doświadczeni użytkownicy tego wzorca mogą również skorzystać z tych rozdziałów, by zapoznać się z terminologią przewijającą się w całej książce. Pod koniec części 1 każdy powinien mieć gruntowną wiedzę na temat słownictwa i ogólnych pojęć, nawet jeśli niektóre ze szczegółów mogą nadal wydawać się troszkę niejasne. Takie odczucie jest w porządku, ponieważ książka staje się coraz bardziej konkretna z każdym kolejnym przeczytanym rozdziałem. A więc części 2, 3 i 4 powinny rozwiać wszelkie wątpliwości i odpowiedzieć na pytania, które pewnie pojawią się po przeczytaniu części 1.

1
Podstawy Wstrzykiwania zależności: co, jak i dlaczego

W rozdziale:

1. ■ Obalenie powszechnych mitów o wstrzykiwaniu zależności

2. ■ Zrozumienie celu wstrzykiwania zależności

3. ■ Ocena korzyści (płynących z) wstrzykiwania zależności

4. ■ Wskazanie, kiedy stosować wstrzykiwanie zależności

Zapewne niektórzy słyszeli, że zrobienie sosu berneńskiego to trudne zadanie. Nawet wielu z tych, którzy regularnie gotują, nigdy nie podjęło się próby przygotowania go. Szkoda, bo sos jest przepyszny. (Tradycyjnie podaje się go ze stekiem, ale świetnie pasuje też do białych szparagów, jajek w koszulkach i innych dań.) Niektórzy idą na łatwiznę i wybierają gotowe sosy albo mieszanki rozpuszczalne instant. Jednak żaden zamiennik nie jest ani odrobinę tak smaczny jak prawdziwy sos.

Sos berneński to zemulgowany sos na bazie żółtek i masła, doprawiony do smaku estragonem, trybulą, szalotką i octem winnym. Nie zawiera ani kropli wody, a największym wyzwaniem jest to, że jego przygotowanie może się nie udać. Sos może się zwarzyć lub rozwarstwić. Jeśli przytrafi się to nam – nie ma mowy o uratowaniu sosu. Sam proces przygotowania zajmuje około 45 minut, więc jeżeli polegnie się przy pierwszej próbie, może się okazać, że zabraknie czasu na kolejną. Jednak wszyscy szefowie kuchni potrafią przyrządzić sos berneński. Uczą się tego w trakcie przygotowań do swojego zawodu i zawsze powtarzają, że przygotowanie wspomnianego sosu nie jest takie trudne.

Nie trzeba być profesjonalnym kucharzem, żeby zrobić sos berneński. Każdy, kto uczy się go przyrządzać, musi chociaż raz zrobić go źle, ale, kiedy już załapie, o co chodzi, będzie przygotowywał pyszny sos za każdym razem. Naszym zdaniem Wstrzykiwanie zależności jest jak sos berneński. Wzorzec uważany jest za trudny i jeśli nie uda nam się próba jego użycia, możliwe, że nie będzie się miało więcej czasu na kolejną.

DEFINICJA Wstrzykiwanie zależności to zestaw zasad i wzorców projektowych, który pozwala na stworzenie luźno powiązanego kodu.

Mimo obaw, niepewności i wątpliwości (Fear, uncertainty, and doubt, FUD), które pojawiają się przy DI, łatwo się go nauczyć, porównywalnie jak w przypadku przyrządzania sosu berneńskiego. W trakcie nauki można popełniać błędy, ale kiedy opanuje się tę technikę, nigdy już ich nie powtórzysz przy stosowaniu DI.

Stack Overflow, strona z pytaniami i odpowiedziami dotyczącymi tworzenia oprogramowania, odpowiada na pytanie „Jak wytłumaczyć Wstrzykiwanie zależności pięciolatkowi?”. Najwyżej oceniana odpowiedź została zamieszczona przez Johna Munscha i oferuje zadziwiająco celną analogię skierowaną do (wymyślonego) pięcioletniego ciekawskiego[1]:

Kiedy idziesz sam coś wziąć z lodówki, możesz spowodować jakieś problemy. Możesz zostawić niedomknięte drzwi, możesz wyciągnąć coś, co mama lub tata nie chcą, żebyś jadł. Możesz też szukać czegoś, czego nie mamy w lodówce albo co straciło swoją datę przydatności.

Zamiast tego powinieneś powiedzieć o swojej potrzebie – „Chcę czegoś się napić do mojego obiadu” i wtedy to my zajmiemy się tym tak, że gdy usiądziesz do jedzenia, dostaniesz coś do picia.

W świecie tworzenia obiektowego oprogramowania oznacza to, że współpracujące klasy (wspomniany pięciolatek) powinny polegać na infrastrukturze (rodzicach) w celu zapewnienia niezbędnych usług.

UWAGA W terminologii DI często mówimy o usługach i komponentach. Usługa to z zasady abstrakcja, definicja czegoś, co świadczy usługę. Implementacja abstrakcji nazywana jest często komponentem – klasą, która zawiera własną logikę. Zarówno usługa, jak i komponent to nadużywane terminy, a więc w tej książce zamiast nich używamy pojęć abstrakcja i klasa.

Ten rozdział jest w miarę linearny w swojej strukturze. Na początku przedstawiamy DI oraz pokazujemy jego wykorzystanie i korzyści. I chociaż użyliśmy przykładów, to w porównaniu do pozostałych rozdziałów jest tu najmniej kodu. Zanim przedstawimy DI, omówimy podstawowy cel DI, czyli utrzymywalność. Jest to ważne, ponieważ nietrudno o złe zrozumienie DI bez wcześniejszego dobrego przygotowania. Następnie po przykładnie (Hello DI!) opisujemy korzyści i zakres, którymi wyznaczamy plan dla całej książki. Kiedy ktoś upora się z tym rozdziałem, powinien być przygotowany na bardziej zaawansowane pojęcia pojawiające się później w książce.

Dla większości deweloperów DI może wydawać się odwróconym sposobem tworzenia kodu źródłowego i, tak jak w przypadku sosu berneńskiego, pojawić się może wiele wcześniej wspomnianych FUD. Aby poznać DI, najpierw należy zrozumieć jego cel.

1.1. Pisanie utrzymywalnego kodu

Do jakiego celu wykorzystuje się DI? DI nie jest celem samym w sobie, ale raczej środkiem do osiągnięcia założenia. Ostatecznie celem większości technik programowania jest dostarczenie działającego oprogramowania w jak najbardziej efektywny sposób. Jednym z aspektów do osiągnięcia tego jest pisanie łatwo utrzymywalnego kodu.

Często pojawiają się takie sytuacje, kiedy to utrzymuje się i rozszerza istniejący kod. No chyba że pisze się tylko i wyłącznie prototypy i aplikacje, które nigdy nie doczekają się wydania. Mówiąc w skrócie, utrzymywalność kodu przekłada się na efektywność pracy z kodem.

Świetnym sposobem na tworzenie utrzymywalnego kodu jest stosowanie luźnych wiązań. Ta metoda była już znana dawno temu w 1994 roku, kiedy to Banda Czterech (The Gang of Four) napisała „Wzorce projektowe” (Design Patterns)[2].

Programuj do interfejsu, a nie implementacji.

Ta ważna porada nie jest konkluzją książki „Wzorce projektowe”, a raczej jej (głównym) założeniem. Luźne wiązanie sprawia, że kod jest łatwiej rozszerzalny. A rozszerzalność przekłada się na łatwość utrzymania. DI to nic innego jak technika pozwalająca na właśnie luźne wiązanie. Co więcej, istnieje wiele błędnych przekonań o DI i czasami zaburzają one zrozumienie tematu. Zanim ktoś nauczy się czegokolwiek, musi oduczyć się tego (co myśli), co już wie.

1.1.1. Powszechne mity na temat DI

Pewnie niejedna osoba spotkała się z DI lub o nim słyszała. Super! W takim razie tę sekcję można pominąć i przejść od razu do punktu 1.1.2. Ale jeśli ktoś czyta tę książkę, to prawdopodobnie co najmniej słyszał gdzieś o DI w rozmowie, natknął się na nie w bazie kodu, którą odziedziczył, lub przeczytał o tym w jakichś postach blogowych. Można było też zauważyć, że jakakolwiek wzmianka o DI niesie za sobą dużą ilość silnych opinii. W tej sekcji przyjrzymy się czterem najczęstszym niejasnościom na temat DI, które pojawiły się na przestrzeni lat, oraz odpowiemy na pytanie, dlaczego te opinie nie są prawdziwe. Mity, o których mowa, to:

1. ■ DI jest odpowiednie tylko dla późnego wiązania.

2. ■ DI jest odpowiednie tylko dla testów jednostkowych.

3. ■ DI jest w pewnym sensie Fabryką abstrakcyjną na sterydach.

4. ■ DI wymaga jakiegokolwiek kontenera di.

I chociaż żadne z tych mitów nie są prawdziwe, to jednak przeważają (w powszechnej opinii). Musimy się ich pozbyć, zanim rozpoczniesz naukę na temat DI.

PÓŹNE WIĄZANIE

W tym kontekście późne wiązanie odnosi się do możliwości zastąpienia części aplikacji bez rekompilacji naszego kodu. Aplikacja, która włącza zewnętrzne rozszerzenia (na przykład, Visual Studio), to jeden z przykładów. Kolejnym jest standardowe oprogramowanie, które wspiera różne środowiska uruchomieniowe.

Wyobraźmy sobie, że mamy aplikację, która działa na więcej niż jednym silniku bazy danych (na przykład taką, która wspiera zarówno Oracle, jak i SQL Server). W celu osiągnięcia tej funkcji reszta aplikacji komunikuje się z bazą danych przez jeden interfejs. Kod źródłowy zapewnia różne implementacje tego interfejsu pozwalające na uzyskanie dostępu odpowiednio do Oracle i SQL Server. W takim wypadku możemy użyć opcji konfiguracji do kontrolowania, która implementacja powinna zostać użyta do danej instalacji.

Powszechne błędne przekonanie brzmi, że DI jest jedynym odpowiednim wzorcem w wypadku takiego scenariusza. To zrozumiałe, ponieważ DI pozwala na obsługę takiego scenariusza. Jednak błędne jest uważanie tego związku za symetryczny. Fakt, iż DI pozwala na późne wiązanie nie oznacza, że jest on jedynym odpowiednim wzorcem takich scenariuszy. Jak ukazano to na rys. 1.1, późne wiązanie jest jednym z wielu aspektów DI.

Jeśli ktoś uważał do tej pory DI za wzorzec odpowiedni tylko dla scenariuszy z późnym wiązaniem, to musi się tego oduczyć. DI pozwala na o wiele więcej niż (tylko) uruchamianie późnego wiązania.

TESTOWANIE JEDNOSTKOWE

Niektórzy uważają DI za jedynie słuszny we wspieraniu testowania jednostkowego. To też nieprawda, mimo że DI to zdecydowanie ważna część tworzenia testowania. Prawdę powiedziawszy, nasze pierwotne wprowadzenie do DI wzięło się z borykania się z pewnymi aspektami Test-Driven Development (Programowanie sterowane testami, TDD). Wtedy właśnie odkryliśmy DI i dowiedzieliśmy się, że inni ludzie używali go już wcześniej w scenariuszach, które my poruszaliśmy.

I nawet jeśli nie pisze się testów jednostkowych (jeśli tak jest, to powinno się zacząć już teraz), DI jest nadal odpowiednim wzorcem z powodu wszystkich innych korzyści, które ma w zanadrzu. Twierdzenie, że DI jest jedynie odpowiednie do wsparcia testów jednostkowych, jest jak twierdzenie, że nadaje się tylko do osiągnięcia późnego wiązania. Rysunek 1.2 pokazuje, jak – mimo że to inny widok – jest to widok tak samo wąski jak ten z rys. 1.1. W tej książce postaramy się pokazać cały obraz.

Rysunek 1.2. Może do tej pory niektórzy twierdzili, że testowanie jednostkowe to jedyny cel di. Chociaż to założenie różni się od założenia o późnym wiązaniu, ono również jest przyjęciem fragmentu z o wiele szerszego widoku

Rysunek 1.1. di pozwala na późne wiązanie, ale zakładanie, że nadaje się ono tylko do scenariuszy późnego wiązania, jest przyjęciem fragmentu z o wiele szerszego widoku

Jeśli ktoś myślał, że DI ma zastosowanie tylko w testowaniu jednostkowym, musi oduczyć się tego założenia. DI pozwala na wiele więcej niż (tylko) testowanie jednostkowe.

fabryka abstrakcyjna na sterydach

Najprawdopodobniej najbardziej niebezpiecznym mitem jest ten o DI obejmującym jakiegoś rodzaju ogólny cel Fabryki abstrakcyjnej, którego można użyć do stworzenia przykładów zależności potrzebnych w aplikacjach.

Fabryka abstrakcyjna

Fabryka abstrakcyjna to z zasady abstrakcja zawierająca wiele metod, a każda z nich pozwala na stworzenie pewnego rodzaju obiektu[3].

Typowo Fabrykę abstrakcyjną wykorzystuje się w przypadku bibliotek interfejsu użytkownika (ui) lub aplikacji klienckiej, które muszą być uruchomione na wielu platformach. Aby osiągnąć wysoki stopień możliwości ponownego użycia kodu na wszystkich platformach, można, na przykład, zdefiniować abstrakcję IUIControlFactory pozwalającą na stworzenie pewnych rodzajów elementów interfejsu użytkownika, takich jak: ramki tekstowe oraz przyciski dla użytkowników:

public interface IUIControlFactory
{
IButton CreateButton();
ITextBox CreateTextBox();
}

Dla każdego systemu operacyjnego można mieć inną implementację takiego IUIControlFactory. W tym wypadku istnieją tylko dwie metody fabryki, ale w zależności od aplikacji lub biblioteki interfejsu użytkownika mogłoby być ich o wiele więcej. Ważną kwestią do zapamiętania jest to, że jedna Fabryka abstrakcyjna określa predefiniowaną listę metod fabryki.

We wstępie do tego rozdziału napisaliśmy „współpracujące klasy (…) powinny polegać na infrastrukturze (…) w celu zapewnienia niezbędnych usług”. Jaka może być pierwsza myśl na temat tego zdania? Czy można sobie wyobrazić infrastrukturę jako jakiegoś rodzaju usługę, którą odpytuje się, by otrzymać potrzebne zależności? Jest to bardzo powszechne myślenie. Wielu programistów i architektów widzi DI jako serwis, który może być użyty do namierzenia innych usług. Takie zastosowanie nazywa się lokalizatorem usług. Jednak jest to całkowite przeciwieństwo DI.

lokalizator usług nazywany jest często Fabryką abstrakcyjną na sterydach, ponieważ w porównaniu do zwykłej Fabryki abstrakcyjnej lista typów rozwiązywalnych jest niesprecyzowana i prawdopodobnie nieskończona. Z zasady ma ona jedną metodę pozwalającą na stworzenie wszystkich rodzajów typów, mniej więcej jak w tym przykładzie:

public interface IServiceLocator
{
object GetService(Type serviceType);
}

WAŻNE: Jeśli ktoś sądził, że DI to lokalizator usług (w znaczeniu fabryki ogólnego przeznaczenia), powinien się oduczyć takiego myślenia. DI jest przeciwieństwem lokalizatora usług. To możliwość układania kodu w taki sposób, by nigdy nie było konieczności zapytań o zależności. Raczej wymaga się od użytkownika kodu, aby to oni je dostarczali.

KONTENERY DI

Z poprzednim błędnym przekonaniem blisko związane jest myślenie, że DI potrzebuje kontenera di. Jeśli wierzyło się w błędny pogląd, że DI wykorzystuje lokalizator usług, łatwo jest też wyjść z założenia, że jeden kontener di może przejąć obowiązki lokalizatora usług. Może i jest to jakaś opcja, ale nie w taki sposób powinno się używać kontenera di.

kontener di to opcjonalna biblioteka, która ułatwia tworzenie instancji klas, podczas startu aplikacji. Nie jest to jednak wymagane narzędzie. Kiedy tworzymy aplikacje bez kontenera di, ten sposób nazywany jest czystym di. Taka technika może zająć trochę więcej czasu, ale nie wymaga poświęcenia innych zasad DI.

DEFINICJA Czyste di to praktyka aplikowania DI bez użycia kontenera di[4].

WAŻNE Jeśli ktoś myślał, że DI wymaga kontenera di, w takim razie jest to kolejny pogląd, którego musi się oduczyć. DI to zbiór zasad i wzorców, a kontener di to pomocne, ale opcjonalne narzędzie.

Do tej pory wytłumaczyliśmy dokładnie, czym jest kontener di oraz jak i kiedy powinno się go stosować. W szczegóły wejdziemy na końcu rozdziału 3, a część 4 książki jest w całości poświęcona temu zagadnieniu.

Wydawać się może, że ponieważ do tej pory wskazaliśmy na cztery mity na temat DI, przyszedł teraz czas na przekonujący argument przeciwko każdemu z nich. To prawda. W takim sensie, że ta książka to jeden wielki argument przeciwko takiemu błędnemu myśleniu, a więc na pewno jeszcze wrócimy do tej rozmowy w dalszej części pracy. Na przykład w rozdziale 5 sekcji 5.2 omówimy, dlaczego lokalizator usług jest antywzorcem.

Odnosząc się do naszego doświadczenia, oduczenie się błędnych poglądów jest bardzo ważne, ponieważ ludzie często starają się zmodernizować to, co mówmy im na temat DI, i dopasować do tego, co myślą, że wiedzą już na ten temat. Potrzeba potem czasu, żeby wreszcie zdali sobie sprawę, jak bardzo mylne są ich podstawowe założenia. Chcemy oszczędzić wszystkim takiego przeżycia. Jeśli ktoś może, niech przeczyta tę książkę tak, jakby nie wiedział nic o DI.

1.1.2. Rozumienie celu DI

DI nie jest celem końcowym – jest to środek do jego osiągnięcia. DI pozwala na luźne wiązania, a luźne wiązania powodują, że kod jest bardziej utrzymywalny. Jest to dość mocne stwierdzenie i moglibyśmy odesłać wszystkich do poczytania poważnych autorytetów, np. do Bandy Czterech, ale wydaje nam się w porządku wytłumaczenie, dlaczego to twierdzenie jest prawdą.

Żeby omówić temat dogłębnie, następna sekcja poświęcona jest porównaniu projektowania oprogramowania i kilku wzorcom projektowym z wykorzystaniem instalacji elektrycznej. Wydaje nam się to mocną analogią. Używamy jej nawet do wytłumaczenia projektowania oprogramowania osobom nietechnicznym.

W tej analogii używamy cztery specjalne wzorce projektowe, ponieważ pojawiają się one często w odniesieniu do DI. W książce można zauważyć wielokrotnie pojawiające się trzy z tych wzorców – Dekorator, Kompozyt oraz Adapter. (Czwartym wzorcem – Pustym Obiektem, zajmiemy się w rozdziale 4). Nie ma co się przejmować, jeśli nie zna się tych wzorców, będzie można je poznać w trakcie czytania tej książki.

Tworzenie oprogramowania to wciąż nowa profesja, a więc w wielu kwestiach wciąż odkrywamy, jak implementować dobrą architekturę. Jednak niektórzy specjalizujący się w bardziej tradycyjnych zawodach, na przykład w konstrukcjach, zrozumieli to już dawno temu.

zakwaterowanie się w tanim hotelu

Jeśli zatrzymujemy się w jakimś tanim hotelu, pewnie napotkamy na widok zaprezentowany na rys. 1.3.

Rysunek 1.3. W pokoju z taniego hotelu można znaleźć suszarkę podłączoną bezpośrednio do gniazdka ściennego. Jest to odpowiednik użycia powszechnie stosowanej praktyki pisania ściśle powiązanego kodu

Jak widać, hotel wychodzi nam naprzeciw i udostępnia suszarkę. Jednak nie do końca nam ufają, mają przypuszczenia, że urządzenie nie przywita kolejnego gościa, w związku z tym podłączono je bezpośrednio do gniazdka ściennego. Zarząd hotelu obliczył, że zastąpienie skradzionej suszarki to zbyt wysoki koszt, i wykorzystano rozwiązanie, które w każdej innej sytuacji wydaje się gorszą implementacją.

A co się stanie, kiedy suszarka przestanie działać? Hotel musi zadzwonić po wykwalifikowanego fachowca. Żeby naprawić takie urządzenie podłączone na stałe, trzeba odłączyć zasilanie w całym pokoju, przez co stanie się on czasowo bezużyteczny. Elektryk będzie musiał użyć specjalnych narzędzi do odłączenia suszarki i wymienić ją na nowy model. Jeśli będziemy mieć na tyle szczęścia, to ten elektrotechnik będzie pamiętać o podłączeniu zasilania do pokoju na nowo, a ponadto wykona test, żeby sprawdzić, czy nowa suszarka działa. Oczywiście jeśli będziemy mieć na tyle szczęścia. Czy ta cała procedura brzmi znajomo?

W taki sposób podeszlibyśmy do pracy ze ściśle powiązanym kodem. W tym scenariuszu to suszarka jest ściśle powiązana ze ścianą, a my nie możemy tak łatwo zmodyfikować jednego elementu bez wpływania na drugi.

porównywanie instalacji elektrycznej do wzorców projektowych

Zazwyczaj nie podłączamy do siebie urządzeń elektrycznych przez przymocowanie ich kabla bezpośrednio do ściany. W zamian za to, jak zostało to przedstawione na rys. 1.4, używamy wtyczek i gniazdek. Kształt tego drugiego decyduje o tym, jaka wtyczka będzie do niego pasować.

Rysunek 1.4. Przy użyciu gniazdek i wtyczek suszarka może zostać luźno powiązana z gniazdkiem ściennym

Pozostając przy analogii do tworzenia oprogramowania – gniazdko to interfejs, a wtyczka ze swoim urządzeniem to implementacja. Oznacza to, że pokój (aplikacja) ma jedno lub (przy odrobinie szczęścia) więcej gniazdek, a użytkownicy pomieszczenia (programiści) potrafią podłączyć urządzenia tak, jak chcą, nawet suszarkę przywiezioną przez klienta.

Dla kontrastu z suszarką podłączoną „na stałe” – wtyczki i gniazdka definiują luźno powiązany model połączenia urządzeń elektrycznych. Dopóki wtyczka (implementacja) pasuje do gniazdka (implementuje interfejs) i potrafi poradzić sobie z ilością woltów i herców (przestrzega wymagań interfejsu), możemy łączyć urządzenia na różne sposoby. Co szczególnie ciekawe, większość z tych częstych połączeń możemy porównać do dobrze znanych zasad i wzorców tworzenia oprogramowania.

Po pierwsze, nie jesteśmy już więcej ograniczeni tylko do suszarek. Jeśli ktoś jest typowym czytelnikiem tej książki, zakładamy, że bardziej potrzebuje prądu do komputera niż do suszarki. To żaden problem: odpina się suszarkę od prądu i w jej miejsce podpina komputer (zobacz rys. 1.5).

Rysunek 1.5. Używając gniazdka i wtyczki, można zamienić oryginalną suszarkę z rys. 1.4 na komputer. Taka zamiana odnosi się do zasady podstawiania liskov

ZASADA PODSTAWIANIA LISKOV

Zadziwiające, jak koncepcja gniazdka pojawiła się dekady wcześniej niż komputery, a jednak wciąż jest podstawą jego działania (zasilania). Twórcy oryginalnego projektu gniazdka nie mogli nawet w żaden sposób przewidzieć istnienia komputerów PC, ale dzięki wszechstronności designu potrzeby na początku nieuwzględnione mogą być teraz spełnione.

Możliwość zmiany wtyczek lub implementacji, bez potrzeby zmiany gniazdka lub interfejsu, przypomina główną zasadę tworzenia oprogramowania zwaną zasadą podstawiania liskov. Według tej zasady powinniśmy być w stanie zamienić jedną implementację interfejsu na inną bez zmian ani w kodzie klienta, ani w implementacji.

Kiedy mowa o di, zasada podstawiania liskov jest jedną z najważniejszych zasad tworzenia oprogramowania. To taka reguła, która pozwala na sprostanie wymaganiom pojawiającym się w przyszłości, nawet jeśli nie możemy ich dzisiaj przewidzieć.

Kiedy nie używa się komputera, można odłączyć go od prądu. I chociaż nic nie jest podłączone do prądu, to pokój nie eksploduje. Oznacza to, że jeśli odłączy się komputer z gniazdka, to ani gniazdko, ani samo urządzenie się nie zepsują.

W przypadku oprogramowania często klient oczekuje, że usługa będzie dostępna. Jeśli usuniemy usługę, otrzymamy NullReferenceException. Możemy stworzyć pojedynczą implementację interfejsu, która nic nie robi w celu uporania się z taką sytuacją. Taki wzorzec tworzenia nazywany jest Pustym Obiektem. Jest on analogiczny do zaślepek do gniazd sieciowych używanych przy dzieciach (albo sama wtyczka bez kabla blokująca gniazdko, albo specjalne urządzenie wkładane do środka). I ponieważ używa się luźnego wiązania, można zastąpić prawdziwą implementację czymś bezproblemowym i nic nierobiącym. Zostało to zilustrowane na rys. 1.6.

Rysunek 1.6. Odłączenie komputera nie powoduje wybuchu pokoju czy komputera, kiedy zamieni się wtyczkę na zaślepkę używaną przy dzieciach. Można to mniej więcej porównać do wzorca Pusty Obiekt

Można wykonać również inne czynności. Jeśli mieszka się gdzieś, gdzie zdarzają się okresowe awarie zasilania, pewnie wolimy mieć komputer podłączony do systemu zasilania awaryjnego (UPS). Jak zostało to pokazane na rys. 1.7, podłączamy UPS do gniazdka, a komputer łączymy z UPS.

Rysunek 1.7. System zasilania awaryjnego (ups) może zostać wykorzystany i w rezultacie otrzymamy komputer działający nawet w razie awarii zasilania. To odpowiada wzorcowi projektowemu Dekorator

I komputer, i UPS służą różnym celom. Każde z nich ma pojedynczą odpowiedzialność, która nie wpływa na drugi sprzęt. Obydwa elementy pewnie wyprodukowane zostały przez dwie różne firmy, zakupiono je w różnym czasie, a do tego podłączone zostały oddzielnie. Jak przedstawiliśmy to na rys. 1.5, możemy używać komputera bez UPS i tak samo możemy potencjalnie wykorzystać suszarkę w trakcie awarii prądu przez podłączenie jej do UPS.

W tworzeniu oprogramowania taki sposób przechwytywania jednej implementacji kolejną implementacją tego samego interfejsu nazywany jest wzorcem projektowym Dekorator[5]. To daje nam możliwość stopniowego wprowadzenia nowych funkcji i przekrojowych zagadnień bez potrzeby przepisywania lub zmiany dużej ilości istniejącego już kodu.

Kolejnym sposobem dodania nowej funkcjonalności do istniejącego kodu jest refaktorowanie istniejącej implementacji interfejsu do nowej implementacji. Zebranie kilku implementacji w jedną to użycie wzorca projektowego Kompozyt[6]. Rysunek 1.8 ilustruje, jak ten wzorzec odnosi się do podłączenia zróżnicowanych urządzeń do jednej listwy.

Rysunek 1.8. Rozgałęźnik elektryczny pozwala podłączyć kilka urządzeń do jednego gniazdka ściennego. To odpowiada wzorcowi projektowemu Kompozyt

Taki rozgałęźnik elektryczny ma pojedynczą wtyczkę, którą można wpiąć do pojedynczego gniazdka, a sam osprzęt elektrotechniczny zapewnia kilka gniazdek dla różnych urządzeń elektrycznych. W ten sposób można podłączyć i odłączyć (naszą) suszarkę, podczas gdy komputer jest podłączony do prądu. W ten sam sposób wzorzec Kompozyt ułatwia dodanie lub usunięcie funkcyjności przez modyfikowanie zbioru implementacji interfejsu.

I oto końcowy przykład. Czasami wtyczka nie pasuje do jakiegoś gniazdka. Zauważyć to można w trakcie podróży za granicę. Gniazdka wyglądają różnie w różnych krajach. Zabierając ze sobą w podróż na przykład aparat fotograficzny (patrz rys. 1.9), do naładowania go potrzebny będzie Adapter. Odpowiednio do sytuacji i do przykładu powstał wzorzec projektowy o takiej samej nazwie.

Rysunek 1.9. Podczas podróży często istnieje potrzeba użycia Adaptera, żeby podłączyć swoje urządzenia do zagranicznych gniazdek (na przykład żeby naładować aparat fotograficzny). Odpowiada to wzorcowi projektowemu Adapter. Czasami chodzi jedynie o prostą zmianę kształtu wtyczki, ale czasami jest to skomplikowana sprawa zmiany natężenia prądu z przemiennego (AC) na stały (DC)

Wzorzec projektowy Adapter działa dokładnie tak, jak wskazuje jego nazwa[7]. Można go użyć, by połączyć ze sobą dwa związane, a jednak oddzielne interfejsy. Jest to w szczególności przydatne, gdy ma się do czynienia z istniejącym niezależnym API, który chce się wystawić jako instancję interfejsu używaną przez aplikację. Tak jak w przypadku fizycznego Adaptera implementacje wzorca projektowego Adapter mogą obejmować proste do ekstremalnie skomplikowanych.

Fascynujące w modelu gniazdka i wtyczki jest to, że przez dekady udowodnił swoją prostotę i wszechstronność. Kiedy infrastruktura jest na miejscu, każdy może z niego korzystać i przystosowywać do zmieniających się i nieprzewidzianych potrzeb. A co jeszcze bardziej fascynujące, to fakt, iż możemy odnieść model gniazdko-wtyczka do tworzenia oprogramowania. Wszystkie części składowe są już gotowe w formie zasad i wzorców projektowych.

Zaleta luźnego wiązania jest taka sama i w tworzeniu oprogramowania, i w fizycznym modelu gniazdka oraz wtyczki: kiedy infrastruktura jest gotowa, może ona być użyta przez każdego i zaadaptowana do zmieniających się potrzeb, jak również nieprzewidzianych wymagań. Można to robić bez potrzeby dużych zmian w kodzie aplikacji i jej infrastrukturze. Oznacza to, że w idealnym świecie nowy wymóg powinien wprowadzić tylko nową klasę bez jakichkolwiek zmian w już istniejących klasach systemu.

Koncepcja mówiąca o rozszerzaniu aplikacji bez modyfikowania istniejącego kodu nazywa się zasadą otwarte/zamknięte. Sytuacja, gdy 100% kodu zawsze jest otwarte na rozszerzenia, a zamknięte na modyfikacje, jest niemożliwa. Jednak luźne wiązanie przybliża do takiego scenariusza.

Z każdym kolejnym krokiem łatwiej jest dodać nowe funkcje i wymagania do systemu. A dodawanie nowych elementów bez ruszania istniejących części systemu oznacza izolowanie problemów. W ten sposób tworzy się kod, który łatwiej zrozumieć i testować, co pozwala na zarządzanie złożonością tego systemu. W tym właśnie może pomóc luźne wiązanie i dlatego też może spowodować to lepszą utrzymywalność kodu. zasada otwarte/zamknięte zostanie przez nas omówiona dokładnie w rozdziale 4.

Jak na razie można było się zastanawiać, jak będzie wyglądała implementacja tych wzorców w kodzie. Nie warto się tym przejmować. Jak już wspominaliśmy o tym wcześniej, podamy wiele przykładów tych wzorców w tej książce. W dalszej części tego rozdziału pokażemy jedną implementację obydwu wzorców, Dekorator i Adapter.

Łatwą częścią luźnego wiązania jest programowanie do interfejsu zamiast implementacji. Pytanie brzmi – skąd pochodzą instancje? W pewnym sensie cała ta książka jest na ten temat: jest to główne pytanie, na które DI poszukuje odpowiedzi.

Nie można utworzyć nowej instancji interfejsu w ten sam sposób, w jaki tworzy się nową instancję konkretnego typu. Taki kod się nie skompiluje:

Interfejs nie zawiera żadnych implementacji, a więc taka operacja nie jest możliwa. Instancja writer musi zostać stworzona przez użycie innego mechanizmu. DI rozwiązuje ten problem. Przy takim nakreśleniu celu DI wydaje nam się, że czas na nowy przykład.

1.2. Prosty przykład: Hello DI!

W tradycji niezliczonych książek o programowaniu spójrzmy na prostą aplikację konsolową, która wypisuje „Hello DI!” na ekranie. Warto zauważyć, że pełen kod jest dostępny w materiałach do pobrania do tej książki (więcej informacji na początku książki w sekcji „Konwencja kodu i materiały do pobrania”).

W tej sekcji pokażemy, jak wygląda ten kod, i pokrótce omówimy niektóre z najważniejszych korzyści. W pozostałych rozdziałach będziemy już bardziej szczegółowi.

1.2.1. Kod Hello DI!

Pewnie większość czytelników zna dobrze przykłady Hello world, które tworzy się za pomocą pojedynczej linijki kodu. My weźmiemy coś, co jest super proste i bardziej to skomplikujemy. Dlaczego? Wkrótce wytłumaczymy, ale najpierw zobaczmy, jak Hello world wyglądałoby z DI.

klasy współpracujące

Żeby zrozumieć strukturę programu, zaczniemy od spojrzenia na metodę Main aplikacji konsolowej. Następnie pokażemy, ale na początek przedstawimy metodę Main aplikacji Hello DI!:

private static void Main()
{
IMessageWriter writer = new ConsoleMessageWriter();
var salutation = new Salutation(writer);
salutation.Exclaim();
}

Ponieważ program wymaga pisania w konsoli, tworzy on nową instancję ConsoleMessageWriter, która enkapsuluje tę funkcjonalność. Przekazuje on obiekt writer do klasy Salutation w taki sposób, że instancja obiektu salutation wie, gdzie wypisać swoją wiadomość. I skoro wszystko jest teraz poprawnie podpięte, możliwe jest wykonanie logiki metody Exclaim skutkującej wypisaniem wiadomości na ekranie.

Konstruowanie obiektów wewnątrz metody Main to podstawowy przykład czystego di. Żaden kontener di nie jest użyty do stworzenia Salutation i jego zależności ConsoleMessageWriter. Rysunek 1.10 ukazuje związek między klasami współpracującymi.

Rysunek 1.10. Związek między klasami współpracującymi w aplikacji Hello di!

implementowanie logiki aplikacji

Główna logika aplikacji jest zamknięta w klasie Salutation ukazanej w listingu 1.1.

Listing 1.1. Klasa Salutation obejmująca główną logikę aplikacji

public class Salutation
{
private readonly IMessageWriter writer;

public Salutation(IMessageWriter writer)

Zapewnia klasie Salutation zależność IMessageWriter przy użyciu wstrzykiwania konstruktorem

{
if (writer == null)
throw new ArgumentNullException("writer");

Zapis Wyrażenia Strażnika weryfikuje, czy przesyłany IMessageWriter nie jest wartością null.e

this.writer = writer;
}

public void Exclaim()
{
this.writer.Write("Hello DI!");

Wysyła wiadomość Hello di! do zależności IMessageWriter

}
}

Klasa Salutation zależy od własnego interfejsu nazwanego IMessageWriter (zdefiniowanego później). Wymaga ona przekazania jego instancji przez konstruktor. Taka technika nazywana jest wstrzykiwaniem konstruktorem. Wyrażenie Strażnika weryfikuje, czy dostarczony IMessageWriter nie jest równy wartości null przez rzucenie wyjątku, gdyby był[8]. I w końcu klasa Salutation wykorzystuje wcześniej wstrzykniętą instancję IMessageWriter wewnątrz implementacji metody Exclaim przez wywołanie jej metody Write. W ten sposób wysyłana jest wiadomość Hello DI! do zależności IMessageWriter.

DEFINICJA wstrzykiwanie konstruktorem jest działaniem, które statycznie definiuje listę wymaganych zależności przez sprecyzowanie ich jako parametry dla konstruktora klasy. (wstrzykiwanie konstruktorem jest opisane szczegółowo w rozdziale 4, który również zawiera bardziej szczegółową analizę podobnego przykładu kodu.)

Używając terminologii DI, mówimy, że zależność IMessageWriter jest wstrzykiwana do klasy Salutation przy użyciu argumentu konstruktora. Uwaga, Salutation nie wie nic o istnieniu ConsoleMessageWriter. Współpracuje ona z nią tylko i wyłącznie przez interfejs IMessageWriter. To prosty interfejs na potrzeby przykładu:

public interface IMessageWriter
{
void Write(string message);
}

Mogłyby się pojawić tu metody składowe, ale w tym prostym przykładzie potrzebna jest jedynie metoda Write. Interfejs jest implementowany przez klasę ConsoleMessageWriter, która przekazywana jest przez metodę Main do klasy Salutation:

public class ConsoleMessageWriter : IMessageWriter
{
public void Write(string message)
{
Console.WriteLine(message);
}
}

Klasa ConsoleMessageWriter implementuje IMessageWriter przez opakowanie klasy Console z Biblioteki klas podstawowych .NET (oryg. .NET Base Class Library, BCL). Jest to prosta aplikacja wzorca projektowego Adapter, o której mówiliśmy w sekcji 1.1.2.

1.2.2. Korzyści (płynące) z DI

Można zastanawiać się nad korzyścią zastąpienia pojedynczej linijki kodu dwoma klasami i jednym interfejsem, która w efekcie daje razem 28 linijek. Taki sam problem można łatwo rozwiązać w następujący sposób:

private static void Main()
{
Console.WriteLine("Hello DI!");
}

DI może wydawać się przesadą, ale płynie z niego kilka korzyści, które pojawiają się przy użyciu go/wykorzystaniu tego wzorca. Jak to się dzieje, że ten przykład jest lepszy niż standardowy jednolinijkowy kod, którego normalnie używa się do implementacji Hello World w C#? Tutaj DI dokłada narzut o 2800%, ale z racji zwiększenia złożoności kodu z jednej linijki do setek tysięcy narzut zmniejsza się, aby aż na końcu zniknąć całkiem. Rozdział 3 ukazuje bardziej rozbudowany przykład aplikowania DI. I chociaż ten przykład jest nadal super uproszczony w porównaniu do prawdziwych aplikacji, można dostrzec, że DI w takim przypadku jest o wiele mniej inwazyjne.

Zrozumiemy, jeśli ktoś będzie uważać przedstawiony przykład DI za zbyt skomplikowany pod kątem inżynierii, ale warto pomyśleć tylko nad tym: w swojej naturze, klasyczne Hello World jest prostym problemem z dobrze sprecyzowanymi i ograniczonymi wymaganiami. W prawdziwym życiu tworzenie oprogramowania nigdy nie wygląda w ten sposób. Wymagania zmieniają się i często są niejasne. A i funkcjonalności, które trzeba zaimplementować, bywają bardziej złożone. DI pomaga w rozwiązaniu tych problemów przez umożliwienie luźnego wiązania. W szczególności zyskuje się na korzyściach wypisanych w tab. 1.1.

Tabela 1.1. Korzyści uzyskiwane z luźnego wiązania. Każdy benefit jest zawsze dostępny, ale jego wartość zależy od okoliczności

Korzyść

Opis

Kiedy ma wartość?

Późne wiązanie

Usługi mogą być zamieniane na inne bez rekompilacji kodu.

Ma wartość w standardowym oprogramowaniu, ale możliwe, że jest mniej ważne w aplikacjach dla firm, gdzie środowisko uruchomieniowe bywa dobrze zdefiniowane.

Rozszerzalność

Kod może być rozszerzony i ponownie użyty w sposób pierwotnie nieprzewidziany.

Zawsze wartościowa.

Równoczesny rozwój

Kod może być rozwijany równolegle.

Ma wartość dla dużych złożonych aplikacji, ale nie za bardzo przy małych prostych aplikacjach.

Utrzymywalność

Klasy z dobrze zdefiniowanymi odpowiedzialnościami są łatwiejsze w utrzymaniu.

Zawsze wartościowa.

testowalność

Klasy mogą być poddawane testom jednostkowym.

Zawsze wartościowa.

Jako pierwszą korzyść wypisaliśmy tę o późnym wiązaniu, ponieważ zgodnie z naszym doświadczeniem jest to główna wartość według opinii większości ludzi. Kiedy architekci i programiści nie potrafią zrozumieć korzyści luźnego powiązania, najprawdopodobniej jest to spowodowane tym, że nigdy nie rozważali pozostałych korzyści.

PÓŹNE WIĄZANIE

Kiedy tłumaczymy korzyści płynące z programowania do interfejsów i DI, najbardziej widoczną dla większości osób jest ta o możliwości podmiany jednej usługi na inną. Często rozważają oni wady i zalety tylko na podstawie tej jednej wartości. Nie tak dawno sugerowaliśmy, żeby najpierw zapomnieć o czymś, by nauczyć się czegoś poprawnie, nieprawdaż? Ktoś może odpowiedzieć, że zna swoje wymagania tak dobrze i jest pewny tego, że nigdy nie będzie musiał zamienić, powiedzmy, bazy danych SQL (Server) na cokolwiek innego. Ale wymagania się zmieniają.

NoSQL, Microsoft Azure i dyskusja na temat możliwości kompozycji

Lata temu ja (Mark) często spotykałem się z obojętnością, kiedy próbowałem przekonać programistów i architektów do korzyści płynących z di. „Dobra, więc możesz zamienić mechanizm dostępu do bazy danych, którego ktoś używa na coś innego. Po co? Czy istnieje jakaś alternatywa dla relacyjnych baz danych?”

Pliki xml nigdy nie wydawały się przekonującą alternatywą w wysoce skalowalnych scenariuszach znanych z dużych firm. Ale w ciągu kilku ostatnich lat nastąpiła znacząca zmiana (w tej kwestii).

Na pdc 2008 zapowiedziano Azure i dużo zrobiono, by przekonać nawet najbardziej zatwardziałe organizacje pracujące tylko na bazie technologii Microsoftu. Musiały one ponownie ocenić swoje pozycje w kwestii przechowywania danych. Istnieje teraz prawdziwa alternatywa dla relacyjnych baz danych i jedyne, o co muszę pytać ludzi, to, czy chcą, żeby ich aplikacja była gotowa na chmurę. Argument z zamianą zyskał teraz na sile.

Podobny ruch można znaleźć w całym koncepcie NoSQL, gdzie modeluje się aplikacje, korzystając ze zdenormalizowanych danych – często baz dokumentowych. Ale pomysły takie jak Event Sourcing stają się coraz bardziej znaczące[9].

W sekcji 1.2.1 nie użyto późnego wiązania, ponieważ explicite stworzono nową instancję IMessageWriter przez kodowanie na sztywno i tworzenie nowej instancji ConsoleMessageWriter. Jednak można wprowadzić późne wiązanie przez zmianę tej jednej linijki kodu:

IMessageWriter writer = new ConsoleMessageWriter();

Aby pozwolić na późne wiązanie, możliwe, że trzeba zamienić tę linijkę kodu na podobną do tej.

Listing 1.2. Późne wiązanie implementacji IMessageWriter

IConfigurationRoot configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();

string typeName = configuration["messageWriter"];
Type type = Type.GetType(typeName, throwOnError: true);

IMessageWriter writer = (IMessageWriter)Activator.CreateInstance(type);

UWAGA Listing 1.2 upraszcza pewne kwestie, aby przedstawić ideę. Tak naprawdę cierpi na ten antywzorzec ograniczonej konstrukcji, o którym mowa w rozdziale 5.

Przez wyciągnięcie nazwy typu z pliku konfiguracji aplikacji i stworzenie z niego instancji Type można użyć mechanizmu refleksji do stworzenia instancji IMessageWriter bez znajomości konkretnego typu w czasie kompilacji. Żeby to zadziałało, wymagane jest określenie nazwy typu w parametrze konfiguracji messageWriter w pliku konfiguracyjnym aplikacji:

{
"messageWriter":
"Ploeh.Samples.HelloDI.Console.ConsoleMessageWriter, HelloDI.Console"
}

Luźne wiązanie pozwoli na późne wiązanie, ponieważ istnieje tylko jedno miejsce, gdzie można stworzyć instancję IMessageWriter. I dlatego że klasa Salution działa wyłącznie przeciwko interfejsowi IMessageWriter, nigdy nie zauważy różnicy. W przykładzie Hello DI! późne powiązanie mogłoby pozwolić na napisanie wiadomości w różnych innych miejscach niż konsola. Na przykład w bazie danych lub w pliku. Można dodać taką funkcję, chociaż początkowo nikt nie zamierzał jej uwzględniać.

rozszerzalność

Dobre oprogramowanie musi być gotowe na zmiany. Trzeba będzie dodawać nowe funkcjonalności i rozszerzać te już istniejące. Luźne wiązanie pozwala na efektywne zmiany w aplikacji w podobny sposób, tak jak wtyczka i gniazdko pozwalają na elastyczność.

Załóżmy, że ktoś chce zapewnić większe bezpieczeństwo w przykładzie Hello DI przez zezwolenie tylko uwierzytelnionym użytkownikom na pisanie wiadomości. Listing 1.3 pokazuje, jak można dodać taką funkcjonalność bez zmiany jakiejkolwiek istniejącej już funkcji – po prostu dodaje się nową implementację interfejsu IMessageWriter.

Listing 1.3. Rozszerzanie aplikacji Hello DI! o mechanizm uwierzytelnienia

public class SecureMessageWriter : IMessageWriter

Implementuje interfejs IMessageWriter, jak również z niego korzysta

{
private readonly IMessageWriter writer;
private readonly IIdentity identity;

public SecureMessageWriter(
IMessageWriter writer,

wstrzykiwanie konstruktorem wymaga instancji IMessageWriter

IIdentity identity)
{
if (writer == null)
throw new ArgumentNullException("writer");
if (identity == null)
throw new ArgumentNullException("identity");

this.writer = writer;

this.identity = identity;
}

public void Write(string message)
{

if (this.identity.IsAuthenticated)

Weryfikuje, czy użytkownik jest uwierzytelniony

{
this.writer.Write(message);

Jeśli użytkownik jest uwierzytelniony, wiadomość wykorzystuje wstrzyknięty obiekt typu IMessagerWriter

}
}
}

UWAGA To standardowa aplikacja wzorca projektowego Dekorator, o której wspomnieliśmy w sekcji 1.1.2. Więcej na temat Dekoratorów w rozdziale 9.

Oprócz instancji IMessageWriter konstruktor SecureMessageWriter wymaga instancji IIdentity. Metoda Write jest implementowana przez sprawdzenie w pierwszej kolejności, czy użytkownik jest uwierzytelniony przez użycie wstrzykniętego IIdentity. Jeśli jest, pozwala to polu dekorowanego obiektu writer na wywołanie metody Write. Jedynym miejscem, w którym trzeba będzie zmienić istniejący kod, jest metoda Main, ponieważ zmienia się proces tworzenia klas:

IMessageWriter writer =
new SecureMessageWriter(

ConsoleMessageWriter jest przechwycony przez Dekorator SecureMessageWriter

new ConsoleMessageWriter(),
WindowsIdentity.GetCurrent());

UWAGA W porównaniu do listingu 1.2 tutaj użyto sztywno zaprogramowanego ConsoleMessageWriter.

Warto zauważyć, że opakowuje się lub dekoruje starą instancję ConsoleMessageWriter przy wykorzystaniu nowej klasy SecureMessageWriter. Raz jeszcze klasa Salutation zostaje niezmodyfikowana, ponieważ korzysta z interfejsu IMessageWriter. Tak samo nie ma potrzeby ani modyfikować, ani duplikować funkcjonalności klasy ConsoleWriter. Wykorzystuje się klasę System.Security.Principal.WindowsIdentity, żeby uzyskać tożsamość użytkownika, w którego imieniu kod ten jest wykonywany[10].

Jak pisaliśmy wcześniej, luźne wiązanie pozwala na pisanie kodu, który jest otwarty na rozszerzenia, ale zamknięty na modyfikację. Jedynym miejscem, gdzie należy modyfikować ten kod, jest punkt wejścia do aplikacji. SecureMessageWriter implementuje funkcjonalność bezpieczeństwa aplikacji, a ConsoleMessageWriter odnosi się do interfejsu użytkownika. W ten sposób pojawia się możliwość różnicowania tych aspektów niezależnie od nich samych i łączenia ich w potrzebnej formie. Każda klasa ma własną pojedynczą odpowiedzialnoŚĆ.

równoległe rozwijanie oprogramowania

Separacja zagadnień umożliwia rozwijanie kodu równolegle. Kiedy projekt oprogramowania rośnie do pewnych rozmiarów, niezbędna jest wtedy praca wielu programistów jednocześnie na tym samym kodzie. W większej skali jest nawet potrzebne podzielenie zespołu tworzącego aplikację na kilka mniejszych podzespołów. Najczęściej każdy zespół zajmuje się jednym obszarem z całej aplikacji. Dzięki wytyczonym zadaniom indywidualny zespół rozwija jeden lub więcej modułów, które na końcu będą zintegrowane w jedną aplikację. Jeśli obszary są od siebie zależne, niektóre zespoły prawdopodobnie będą polegać na funkcjonalnościach stworzonych przez innych.

DEFINICJA W tworzeniu oprogramowania obiektowego modułem nazywa się grupę logicznie powiązanych klas (lub komponentów), gdzie jeden moduł jest niezależny od innych modułów i wymienny z nimi. Z zasady widać, że warstwa składa się z jednego lub więcej modułów.

Co do poprzedniego przykładu – ponieważ klasy SecureMessageWriter i ConsoleMessageWriter są bezpośrednio niezależne od siebie, można by tworzyć je w (dwóch) równoległych zespołach. Wymagane jest tylko ustalenie współdzielonego przez podzespoły interfejsu IMessageWriter.

utrzymywalność

Kiedy odpowiedzialność każdej z klas została jasno określona i ograniczona, utrzymanie całej aplikacji staje się o wiele łatwiejsze. Jest to następstwem zasady pojedynczej odpowiedzialności, która głosi, że każda klasa powinna mieć tylko pojedynczą odpowiedzialność. zasadę pojedynczej odpowiedzialności omówimy dokładnie w rozdziale 2.

Dodawanie nowych funkcji do aplikacji staje się łatwiejsze, ponieważ wiadomo, gdzie zmiany powinny być wprowadzone. Częściej zdarza się, że nie trzeba zmieniać istniejącego kodu, ale można za to dodać nowe klasy i ponownie zbudować aplikację. I znowu w praktyce używa się tu zasady otwarte/zamknięte.

Również rozwiązywanie problemów staje się mniej męczące, ponieważ zakres możliwych przyczyn się zmniejsza. Z dobrze określonymi odpowiedzialnościami ma się o wiele lepsze pojęcie, gdzie rozpocząć poszukiwania przyczyn problemu.

testowalność

Aplikację uważa się za testowalną, kiedy można poddać ją testom jednostkowym. Dla niektórych testowalność będzie najmniejszym zmartwieniem, a dla innych będzie to niezbędny wymóg. My należymy do tej drugiej kategorii. Mark w trakcie swojego życia zawodowego odrzucił już kilka ofert pracy z tego powodu, że musiałby pracować z pewnymi produktami, które nie były testowalne.

TESTOWALNOŚĆ

Określenie testowalne jest bardzo nieprecyzyjne, ale z drugiej strony można się z nim wszędzie spotkać w społeczności tworzenia oprogramowania. Ten termin jest głównie używany przez tych, którzy praktykują testowanie jednostkowe. Z zasady każda aplikacja może być przetestowana przez jej uruchomienie. Testy mogą być wykonane przez ludzi używających aplikacji przez jej interfejs użytkownika (ui) lub jakikolwiek inny dostępny interfejs. Takie testy manualne są długotrwałe i drogie, a więc preferuje się używanie zautomatyzowanych testów.

Napotkasz na wiele typów zautomatyzowanego testowania: testowanie jednostkowe, testowanie integracyjne, testowanie wydajnościowe, test warunków skrajnych itd. Ale ponieważ testowanie jednostkowe ma mniej wymogów dotyczących środowiska wykonania, z zasady jest ono najbardziej wydajne i niezawodne. W tym kontekście często ocenia się właśnie testowalność.

Testy jednostkowe pozwalają na szybką pętlę zwrotną na temat stanu aplikacji, ale są możliwe do napisania, jedynie jeśli dana jednostka może być odpowiednio odizolowana od swoich zależności. Istnieją niejasności co do tego, jakich rozmiarów może być jednostka, ale wszyscy jednak zgadzają się z tym, że zdecydowanie nie jest to coś, co rozciąga się na wiele modułów. Możliwość testowania modułów w odizolowaniu jest zasadniczą kwestią w testowaniu jednostkowym.

Jedynie kiedy aplikacja jest poddawana testom jednostkowym, uważa się ją za testowalną. Najbezpieczniejszym sposobem zapewnienia testowalności jest stworzenie jej za pomocą TDD.

Warto wspomnieć o tym, że testy jednostkowe same w sobie nie gwarantują działającej aplikacji. Testy całego systemu lub innego rodzaju testy integracyjne są nadal niezbędne do sprawdzenia, czy aplikacja działa tak, jak powinna.

Korzyść testowalności jest najprawdopodobniej najbardziej kontrowersyjną z tych, które wypisaliśmy. Niektórzy programiści i architekci nadal nie praktykują testów jednostkowych, a więc uważają tę korzyść za co najmniej zbędną. Jednak my uważamy ją za jedną z najważniejszych części tworzenia oprogramowania i dlatego też oceniliśmy ją jako „Zawsze wartościowa” w tab. 1.1. Michael Feathers definiuje nawet termin program przestarzały jako każda aplikacja, która nie jest pokryta testami jednostkowymi[11].

Luźne powiązanie prawie przypadkowo pozwala na testowanie jednostkowe, ponieważ programiści trzymają się zasady podstawiania liskov: nie przejmują się konkretnymi typami swoich zależności. Oznacza to, że można wstrzyknąć obiekt zastępczy do testowanego systemu (System Under Test, SUT), co znajduje się w listingu 1.4.

Obiekty typu Test double

Częstą techniką jest tworzenie implementacji zależności, które zachowują się jak dublerzy prawdziwych lub zaplanowanych implementacji. Nazywane są one Test Double [Obiekty zastępcze] i nigdy nie będą użyte w końcowej wersji aplikacji. Ich zadaniem jest bycie zastępstwem dla prawdziwych zależności, kiedy te nie są dostępne lub nie chcemy z nich korzystać.

Obiekt typu Test Double jest przydatny wtedy, gdy prawdziwa zależność jest powolna, droga, wymaga usunięcia lub po prostu jest poza zakresem danego testu. Istnieje pełen wzorzec językowy wokół obiektów typu Testu Double i wiele podtypów takich jak: Stubs, Mocks czy Fakes[12].

Możliwość zamienienia zaplanowanych zależności na zamienniki testowe jest produktem ubocznym luźnego powiązania, ale zdecydowaliśmy się go wyszczególnić na liście jako oddzielną korzyść, ponieważ wynikająca z tego wartość jest inna. Osobiście doświadczyliśmy tego, że DI przynosi korzyści nawet podczas testowania integracyjnego. Chociaż z zasady komunikują się one z prawdziwymi systemami zewnętrznymi (takimi jak baza danych), nadal wymagany jest jakiś stopień izolacji. Innymi słowy, nadal istnieją powody, by zamieniać, przechwytywać lub symulować niektóre zależności w testowanej aplikacji.

Przechwytywanie wiadomości tekstowych

Ja (Steven) pracowałem w wielu aplikacjach, które wysyłają SMS-y przez niezależny serwis. Nie chciałem, żeby nasze środowisko testowe wysyłało te wiadomości przez prawdziwe bramki, ponieważ koszt zależał od ilości wiadomości i ja zdecydowanie nie chciałem niechcący spamować telefonu takimi rzeczami.

Ale w trakcie testów manualnych wiadomości tekstowe były wysyłane na komórki. Jednak, w tym przypadku, użyto Dekoratora, który zamieniał numer telefonu wysyłany do bramki na taki, do którego miał dostęp tester. W ten sposób miał on możliwość otrzymania wiadomości na swój telefon i weryfikacji testowanego systemu.

W zależności od typu aplikacji, trzeba będzie nie przejmować się możliwością późnego wiązania, ale my zawsze dbamy o testowalność. Niektórzy programiści nie zwracają na nią uwagi, ale wciąż uważają późne wiązanie za ważne dla aplikacji, które tworzą. W każdym razie DI zapewnia opcje na przyszłość przy minimalnym dodatkowym narzucie dzisiaj.

przykład: test jednostkowy logiki hello di

W sekcji 1.2.1 widziałeś przykład Hello DI! Mimo że pokazaliśmy końcowy kod na początku, stworzyliśmy go przy użyciu TDD. Listing 1.4 pokazuje najważniejszy test jednostkowy.

UWAGA Brak doświadczenia w testach jednostkowych nie jest powodem do zmartwienia. Czasami pojawiają się w tej książce, ale w żaden sposób nie są niezbędne. Książkę można czytać bez tej wiedzy[13].

Listing 1.4. Testowanie jednostkowe klasy Salutation

[Fact]
public void ExclaimWillWriteCorrectMessageToMessageWriter()
{
var writer = new SpyMessageWriter();
var sut = new Salutation(writer);

zależność IMessageWriter jest podmieniona za pomocą obiektu typu Spy SpyMessageWriter

sut.Exclaim();

Assert.Equal(
expected: "Hello DI!",
actual: writer.WrittenMessage);
}

public class SpyMessageWriter : IMessageWriter
{
public string WrittenMessage { get; private set; }

public void Write(string message)
{
this.WrittenMessage += message;
}
}

Klasa Salutation potrzebuje instancji interfejsu IMessageWriter, więc trzeba go stworzyć. Można by użyć jakiejkolwiek implementacji, ale w testach jednostkowych obiektu zastępczego interfejs może być przydatny – w tym przypadku wprowadza się swoją własną implementację obiektu typu Spy[14].

W tym przypadku obiekt zastępczy jest używany tak jak produkcyjna implementacja. To skutek tego, że ten przykład jest prosty. W większości aplikacji obiekt typu Test Double jest znacznie prostszy niż konkretne produkcyjne implementacje, które zastępuje. Ważne, by przekazać testową implementację IMessageWriter w celu upewnienia się, że testuje się tylko jeden element naraz. Teraz testowana jest metoda Exclaim klasy Salutation, więc nie chcemy, żeby produkcyjna implementacja IMessageWriter zaśmiecała ten test. Żeby stworzyć klasę Salutation, przekazuje się instancję obiekt typu Spy dla IMessageWriter, używając wstrzykiwania konstruktorem.

Po wykonaniu testów na SUT można wywołać Assert . Equal, żeby zweryfikować, czy oczekiwany wynik pokrywa się z prawdziwym wynikiem. Jeśli wywołano metodę IMessageWriter.Write z łańcucha znaków „Hello DI!”, SpyMessageWriter mógł przechowywać ją w swojej właściwości WrittenMessage i asercja Equal kończy się bez błędu. Ale jeśli metoda Write nie była wywołana lub była, ale z inną wartością, wyrzuci ona wyjątek i w rezultacie test się nie powiedzie.

Luźne wiązanie przynosi wiele korzyści: kod jest o wiele łatwiej rozwijać, utrzymywać, rozszerzać i staje się on o wiele bardziej testowalny. Ta technika nie jest też zbytnio trudna. Programujemy do interfejsów, a nie do konkretnych implementacji. Jedynym dużym problemem jest wyczucie, jak zarządzać instancjami tych interfejsów. DI pokonuje takie przeszkody przez wstrzykiwanie zależności z zewnątrz. Mimo że wstrzykiwanie konstruktorem to preferowana metoda działania, zbadamy też kilka dodatkowych opcji w rozdziale 4.

1.3. Co wstrzykiwać, a czego nie wstrzykiwać

We wcześniejszej sekcji opisaliśmy kwestie, które mogą motywować do użycia DI w pierwszej kolejności. Jeśli ktoś jest przekonany co do korzyści luźnego powiązania, możliwe, że będzie chciał robić wszystko z jego wykorzystaniem. Generalnie jest to dobry pomysł. Kiedy trzeba zdecydować, jak zbudować moduły, luźne powiązanie potwierdza się być najbardziej przydatnym. Ale nie trzeba skupiać się na wyciąganiu abstrakcji ze wszystkich elementów i sprawiać, żeby implementacje były zastępowalne. W tej sekcji podamy niektóre z narzędzi decyzyjnych, które mogą pomóc zadecydować, jak modelować swoje zależności.

.NET BCL składa się z wielu podzespołów. Za każdym razem, gdy pisany jest kod z wykorzystaniem typu z BCL, dodawana jest jedna zależność do modułu. W poprzedniej sekcji omówiliśmy, jak luźne powiązanie jest ważne i jak programowanie do interfejsu jest kluczowe. Czy oznacza to, że nie można odnosić się do żadnych podzespołów BCL i używać ich typów bezpośrednio w aplikacji? Co, jeśli ktoś chciałby użyć jakiegoś XmlWriter, który jest zdefiniowany w podzespole System.Xml?

Nie trzeba traktować wszystkich zależności w ten sam sposób. Wiele typów w BCL może być użytych bez zagrożenia dla stopnia powiązania aplikacji – ale uwaga, nie wszystkie, tylko wiele z nich. Ważne jest, by wiedzieć, jak rozróżnić typy, które nie stanowią zagrożenie, od typów, które mogą ścieśnić stopień powiązania aplikacji. Warto skupić się głównie na tych drugich.

W trakcie poznawania DI przydatne może się okazać kategoryzowanie zależności na stabilne zależności i niestabilne zależności.

SZWY

Wszędzie, gdzie ktoś decyduje się programować do abstrakcji zamiast konkretnego typu, wprowadza się szew do aplikacji. szew to miejsce, gdzie aplikacja jest składana ze swoich wszystkich elementów w podobny sposób, jak ubranie jest zszywane na szwach[15]. Jest to także miejsce, gdzie można rozłożyć aplikację na czynniki (pierwsze) i pracować na oddzielnych izolowanych modułach.

Przykład Hello di! zbudowany przez nas w sekcji 1.2 zawiera szew między Salutation a ConsoleMessageWriter (poniżej zilustrowane). Klasa Salutation nie zależy bezpośrednio od klasy ConsoleMessageWriter, a raczej używa interfejsu IMessageWriter do pisania wiadomości. Można rozłożyć na części tę aplikację na tym szwie i złożyć ją ponownie z innym writerem wiadomości.

1.3.1. stabilne zależności

Wiele modułów w bibliotece BCL i poza nią stanowi zagrożenie dla stopnia modularności aplikacji. Zawierają one funkcjonalności, które nadają się do ponownego użycia i sprawiają, że kod jest bardziej zwięzły. Moduły BCL są zawsze dostępne dla aplikacji, ponieważ wymaga ona .NET Framework w celu uruchomienia. I też dlatego że już istnieją, problem równoległego rozwoju nie odnosi się do takich modułów. Zawsze można użyć ponownie jakiejś biblioteki BCL w innej aplikacji.

Domyślnie można założyć, że większość (ale nie wszystkie) typy definiowane w BCL są bezpieczne lub są stabilnymi zależnościami. Nazywamy je stabilnymi, ponieważ już są na miejscu, mają tendencję do bycia wstecznie kompatybilnymi oraz wywołanie ich skutkuje deterministycznymi rezultatami. Większość stabilnych zależności to typy BCL, ale inne zależności również mogą być stabilne. Ważnymi kryteriami dla stabilnych zależności są:

1. ■ Ich klasa lub moduł już istnieją.

2. ■ Oczekuje się, że nowe wersje nie będą zawierać zmian łamiących kompatybilność.

3. ■ Typy, o których mowa, zawierają algorytmy deterministyczne.

4. ■ Nigdy nie oczekuje się potrzeby zmiany, zwinięcia, dekorowania lub przechwytywania klasy lub modułu na inne.

Innymi przykładami, o których można wspomnieć, są wyspecjalizowane biblioteki, które enkapsulują algorytmy wykorzystywane w rozwijanej aplikacji. Na przykład jeśli tworzy się aplikację, która zajmuje się chemią, można odnosić się do niezależnej biblioteki zawierającej funkcjonalność związaną z chemią.

Odnoszenie się kontenera di

kontenery di same w sobie mogą dotyczyć albo stabilnych zależności, albo niestabilnych zależnych. Wytyczną jest to, czy ktoś chce je zmienić. Bazowanie swojej aplikacji na jakimś konkretnym kontenerze di wiąże się z ryzykiem utknięcia w swoim wyborze na cały czas życia aplikacji. Jest to kolejny z powodów, dla którego powinno się ograniczać do użycia kontenera tylko do miejsca startu aplikacji. Tylko początek aplikacji powinien odnosić się do kontenera di.

Ogólnie, zależności mogą być uważane za stabilne przez wykluczanie. Są stabilne, jeśli nie są niestabilne.

1.3.2. niestabilne zależności

Wprowadzanie szwów do aplikacji to dodatkowa praca, dlatego powinno się robić to tylko, kiedy jest to niezbędne. Może istnieć nawet więcej niż jeden powód potrzeby odizolowania jakiejś zależności za szwem, ale są one mocno związane z korzyściami luźnego powiązania (omówionymi w sekcji 1.2.1).

Takie zależności mogą być rozpoznane po ich tendencji do naruszania jednego lub więcej z tych korzyści. Nie są także stabilne, ponieważ nie zapewniają odpowiedniej podstawy dla aplikacji, a więc w związku z tym nazywamy je niestabilnymi zależnościami. zależność powinna być uważana za niestabilną, jeśli którykolwiek podpunkt z tego kryterium jest dla niej prawdziwy:

1. ■ Zależność wprowadza wymóg stworzenia i konfiguracji środowiska wykonania dla aplikacji. Nie jest to tak, że konkretne typy .NET są niestabilne, ale raczej niestabilne jest to, co one implikują na temat środowiska wykonania.

Bazy danych to dobry przykład typów BCL, które są również niestabilnymi zależnościami, a relacyjne bazy danych są archetypicznym przykładem. Jeśli ktoś nie schowa relacyjnej bazy danych za szwem, nie będzie mógł jej nigdy zamienić przy wykorzystaniu jakiejkolwiek technologii. Jest także trudno stworzyć i uruchomić automatycznie testy jednostkowe. (Nawet jeśli biblioteka klienta Microsoft SQL Server jest technologią zawartą w BCL, jej użycie implikuje relacyjną bazę danych). Inne zasoby typu „out-of-process”, takie jak: kolejki komunikatów, web serwisy, a nawet systemy plików, zaliczają się do tej kategorii. Symptomami tego typu zależności są: brak późnego powiązania i rozszerzalności, a także brak testowalności.

1. ■ Zależność nadal nie istnieje lub jest wciąż w tworzeniu.

2. ■ Zależność nie jest zainstalowana na wszystkich urządzeniach w organizacji tworzącej. Może to być spowodowane drogimi niezależnymi bibliotekami lub zależnościami, które nie mogą być zainstalowane na wszystkich systemach operacyjnych. Najczęstszym symptomem jest brak testowalności.

3. ■ Zależność zachowuje się niedeterministycznie. Ten aspekt jest w szczególności ważny przy testach jednostkowych, ponieważ wszystkie testy muszą być deterministyczne. Typowymi źródłami niedeterminizmu są liczby losowe i algorytmy, które polegają na aktualnych danych lub czasie.

Ponieważ BCL definiuje typowe źródła niedeterminizmu, takie jak System.Random, System.Security.Cryptography.RandomNumberGenerator albo System.DateTime.Now, nie można ominąć odnośników do podzespołu, w którym są zdefiniowane. Mimo to powinno się traktować je jako niestabilne zależności, ponieważ mają tendencję do degradacji testowalności.

WAŻNE niestabilne zależności są punktem centralnym DI. To z powodu niestabilnych zależności, a nie stabilnych zależności wprowadza się szwy do aplikacji. I znowu wymaga to stworzenia ich przy użyciu DI.

Teraz, kiedy zrozumiałe są różnice między stabilnymi a niestabilnymi zależnościami, można zacząć dostrzegać granice możliwości DI. Luźne wiązanie to powszechnie występująca zasada projektowania, a więc DI (jako czynnik to umożliwiający) powinno być wszechobecne w tworzonym kodzie. Nie istnieje jasna granica między tematem DI a dobrym projektowaniem oprogramowania, ale by zdefiniować zakres reszty tej książki, szybko przybliżymy to, co ten wzorzec (DI) obejmuje.

Przypisy

Rozdział 1

[1] Zob. J. Munsch et al. (2009), How to explain Dependency Injection to a 5-year old? [online]. Dostęp: https://stackoverflow.com/questions/1638919/how-to-explain-dependency-injection-to-a-5-year-old.

[2] E. Gamma et al., Design Patterns: Elements of Reusable Object-Oriented Software, Addison–Wesley, 1994, 18. Polskie tłumaczenie: Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku, Helion, 2010.

[3] E. Gamma et al., op.cit., 87.

[4] W pierwszej edycji tej książki „Wstrzykiwanie zależności w .NET” (Dependency Injection in .NET) wykorzystaliśmy termin DI biednego człowieka. czyste di zastąpiło ten termin, ale niech nikogo nie zdziwi stary zwrot w Internecie. Dlaczego zmieniliśmy terminologię, zob. M. Seemann (2014). Pure DI [online]. Dostęp: https://blog.ploeh.dk/2014/06/10/pure-di/.

[5] E. Gamma et al., op.cit., 175.

[6] E. Gamma et al., op.cit., 163.

[7] E. Gamma et al., op.cit., 139.

[8] M. Fowler et al., Refactoring: Improving the Design of Existing Code, Addison–Wesley, 1999, 250. Polskie tłumaczenie: Refaktoryzacja. Ulepszanie struktury istniejącego kodu, Helion, 2011.

[9] M. Fowler (2005). Event Sourcing [online]. Dostęp: https://martinfowler.com/eaaDev/EventSourcing.html.

[10] Klasa System.Security.Principal.WindowsIdentity znajduje się w pakiecie NuGet System.Security.Principal.Windows, który jest częścią .NET Core.

[11] M.C. Feathers, Working Effectively with Legacy Code, Prentice Hall 2004, xvi. Polskie tłumaczenie: Praca z zastanym kodem. Najlepsze techniki, Helion 2014.

[12] G. Meszaros, xUnit Test Patterns: Refactoring Test Code, Addison–Wesley 2007, 522.

[13] Jednak warto przeczytać The Art. Of Unit Testing Roya Osherove’a (polskie tłumaczenie: Testy jednostkowe. Świat niezawodnych aplikacji. Wydanie II, Helion 2014), a następnie xUnit Test Patterns Gerarda Meszarosa (op. cit.).

[14] Obiekt typu Spy jest obiektem typu Test Double, który wychwytuje pośrednie odwołania do innego komponentu przez SUT dla późniejszej weryfikacji przez test. Zob. G. Meszaros, op.cit., 538.

[15] M.C. Feathers, op.cit., 29–44.