Feathers M Praca Z Zastanym Kodem Najlepsze Techniki Compress

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 434

Spis treści

Słowo wstępne . ............................................................. 9


Przedmowa . .............................................................. 11
Wstęp . ................................................................... 17

Część I: Mechanika zmian . ........................................... 19


Rozdział 1. Zmiany w oprogramowaniu ......................................... 21
Cztery powody wprowadzania zmian w oprogramowaniu .............................................21
Ryzykowna zmiana . ..............................................................................................................25
Rozdział 2. Praca z informacją zwrotną ......................................... 27
Co to jest testowanie jednostkowe? . ...................................................................................30
Testy wyższego poziomu . .....................................................................................................32
Pokrycie testami . ...................................................................................................................33
Algorytm dokonywania zmian w cudzym kodzie . ...........................................................36
Rozdział 3. Rozpoznanie i separowanie . ........................................ 39
Fałszywi współpracownicy ....................................................................................................41
Rozdział 4. Model spoinowy . ................................................. 47
Ogromny arkusz z tekstem . .................................................................................................47
Spoiny . ....................................................................................................................................48
Rodzaje spoin . ........................................................................................................................51
Rozdział 5. Narzędzia . ...................................................... 63
Narzędzia do automatycznej refaktoryzacji . .....................................................................63
Obiekty pozorowane . ............................................................................................................65
Jarzmo testowania jednostkowego . ....................................................................................66
Ogólne jarzmo testowe . ........................................................................................................71

Część II: Zmiany w oprogramowaniu . .................................. 73


Rozdział 6. Nie mam zbyt wiele czasu, a muszę to zmienić . ........................ 75
Kiełkowanie metody . ............................................................................................................77
Kiełkowanie klasy . .................................................................................................................80
Opakowywanie metody . .......................................................................................................85
Opakowywanie klasy . ...........................................................................................................88
Podsumowanie . .....................................................................................................................93
6 SPIS TREŚCI

Rozdział 7. Dokonanie zmiany trwa całą wieczność . .............................. 95


Zrozumienie . ..........................................................................................................................95
Opóźnienie . ............................................................................................................................96
Usuwanie zależności . ............................................................................................................97
Podsumowanie . .................................................................................................................. 102
Rozdział 8. Jak mogę dodać nową funkcjonalność? . ............................. 103
Programowanie sterowane testami . ................................................................................. 104
Programowanie różnicowe . .............................................................................................. 110
Podsumowanie . .................................................................................................................. 119
Rozdział 9. Nie mogę umieścić tej klasy w jarzmie testowym . ...................... 121
Przypadek irytującego parametru ..................................................................................... 121
Przypadek ukrytej zależności . ........................................................................................... 128
Przypadek konstrukcyjnego kłębowiska . ........................................................................ 131
Przypadek irytującej zależności globalnej . ...................................................................... 133
Przypadek straszliwych zależności dyrektyw include . .................................................. 141
Przypadek cebulowego parametru . .................................................................................. 144
Przypadek zaliasowanego parametru ............................................................................... 147
Rozdział 10. Nie mogę uruchomić tej metody w jarzmie testowym . ................. 151
Przypadek ukrytej metody . ............................................................................................... 152
Przypadek „pomocnych” funkcji języka . ........................................................................ 155
Przypadek niewykrywalnych skutków ubocznych . ....................................................... 158
Rozdział 11. Muszę dokonać zmian. Które metody powinienem przetestować? ........ 165
Myślenie o skutkach . .......................................................................................................... 166
Śledzenie w przód . .............................................................................................................. 171
Propagacja skutków . .......................................................................................................... 176
Narzędzia do wyszukiwania skutków . ............................................................................. 177
Wyciąganie wniosków z analizy skutków . ...................................................................... 179
Upraszczanie schematów skutków . ................................................................................. 180
Rozdział 12. Muszę dokonać wielu zmian w jednym miejscu. Czy powinienem
pousuwać zależności we wszystkich klasach, których te zmiany dotyczą? ......... 183
Punkty przechwycenia . ...................................................................................................... 184
Ocena projektu z punktami zwężenia . ............................................................................ 191
Pułapki w punktach zwężenia . ......................................................................................... 192
Rozdział 13. Muszę dokonać zmian, ale nie wiem, jakie testy napisać . ............... 195
Testy charakteryzujące . ..................................................................................................... 196
Charakteryzowanie klas . ................................................................................................... 199
Testowanie ukierunkowane . ............................................................................................. 200
Heurystyka pisania testów charakteryzujących .............................................................. 205
Rozdział 14. Dobijają mnie zależności biblioteczne . ............................. 207
Rozdział 15. Cała moja aplikacja to wywołania API . ............................. 209
SPIS TREŚCI 7

Rozdział 16. Nie rozumiem wystarczająco dobrze kodu, żeby go zmienić . ........... 219
Notatki i rysunki . ................................................................................................................ 220
Adnotowanie listingów . .................................................................................................... 221
Szybka refaktoryzacja . ....................................................................................................... 222
Usuwanie nieużywanego kodu . ........................................................................................ 223
Rozdział 17. Moja aplikacja nie ma struktury . .................................. 225
Opowiadanie historii systemu . ......................................................................................... 226
Puste karty CRC . ................................................................................................................ 230
Analiza rozmowy . .............................................................................................................. 232
Rozdział 18. Przeszkadza mi mój testowy kod . ................................. 235
Konwencje nazewnicze klas . ............................................................................................. 235
Lokalizacja testu . ................................................................................................................ 236
Rozdział 19. Mój projekt nie jest zorientowany obiektowo. Jak mogę bezpiecznie
wprowadzać zmiany? .................................................... 239
Prosty przypadek . ............................................................................................................... 240
Przypadek trudny . .............................................................................................................. 241
Dodawanie nowego zachowania . ..................................................................................... 244
Korzystanie z przewagi zorientowania obiektowego . ................................................... 247
Wszystko jest zorientowane obiektowo . ......................................................................... 250
Rozdział 20. Ta klasa jest za duża, a ja nie chcę, żeby stała się jeszcze większa . ........ 253
Dostrzeganie odpowiedzialności . .................................................................................... 257
Inne techniki . ...................................................................................................................... 269
Posuwanie się naprzód . ..................................................................................................... 270
Po wyodrębnieniu klasy . ................................................................................................... 273
Rozdział 21. Wszędzie zmieniam ten sam kod . ................................. 275
Pierwsze kroki . .................................................................................................................... 278
Rozdział 22. Muszę zmienić monstrualną metodę, lecz nie mogę napisać do niej testów .... 293
Rodzaje monstrów . ............................................................................................................ 294
Stawianie czoła monstrom przy wsparciu automatycznej refaktoryzacji ................... 297
Wyzwanie ręcznej refaktoryzacji . .................................................................................... 300
Strategia . .............................................................................................................................. 307
Rozdział 23. Skąd mam wiedzieć, czy czegoś nie psuję? . .......................... 311
Superświadome edytowanie . ............................................................................................ 312
Edytowanie jednego elementu naraz . .............................................................................. 313
Zachowywanie sygnatur . ................................................................................................... 314
Wsparcie kompilatora . ...................................................................................................... 317
Programowanie w parach . ................................................................................................ 318
Rozdział 24. Czujemy się przytłoczeni. Czy nie będzie chociaż trochę lepiej? .......... 321
8 SPIS TREŚCI

Część III: Techniki usuwania zależności ................................ 325


Rozdział 25. Techniki usuwania zależności . .................................... 327
Adaptacja parametru . ........................................................................................................ 328
Wyłonienie obiektu metody . ............................................................................................ 332
Uzupełnianie definicji . ...................................................................................................... 338
Hermetyzacja referencji globalnej . .................................................................................. 340
Upublicznienie metody statycznej . .................................................................................. 346
Wyodrębnienie i przesłonięcie wywołania . .................................................................... 349
Wyodrębnienie i przesłonięcie metody wytwórczej . .................................................... 351
Wyodrębnienie i przesłonięcie gettera . ........................................................................... 353
Wyodrębnienie implementera . ........................................................................................ 356
Wyodrębnienie interfejsu . .................................................................................................. 361
Wprowadzenie delegatora instancji . ............................................................................... 367
Wprowadzenie statycznego settera . ................................................................................. 370
Zastępowanie biblioteki . ................................................................................................... 375
Parametryzacja konstruktora . .......................................................................................... 377
Parametryzacja metody . .................................................................................................... 381
Uproszczenie parametru . .................................................................................................. 383
Przesunięcie funkcjonalności w górę hierarchii ............................................................. 386
Przesunięcie zależności w dół hierarchii . ........................................................................ 390
Zastąpienie funkcji wskaźnikiem do funkcji . ................................................................. 393
Zastąpienie referencji globalnej getterem . ...................................................................... 396
Utworzenie podklasy i przesłonięcie metody . ................................................................ 398
Zastąpienie zmiennej instancji . ........................................................................................ 401
Przedefiniowanie szablonu . .............................................................................................. 405
Przedefiniowanie tekstu . ................................................................................................... 409
Dodatek: Refaktoryzacja .................................................... 411
Wyodrębnianie metody ...................................................................................................... 411
Słownik . ................................................................. 415
Skorowidz . .............................................................. 417
Słowo wstępne

„…i wtedy się zaczęło…”


W swojej przedmowie do tej książki Michael Feathers używa tego zwrotu do opi-
sania początków swojej pasji związanej z programowaniem.
„…i wtedy się zaczęło…”
Czy znasz to uczucie? Czy potrafisz wskazać określony moment swojego życia i powie-
dzieć: „…i wtedy się zaczęło…”? Czy to było jedno wydarzenie, które zmieniło bieg
Twojego życia i w rezultacie doprowadziło Cię do sięgnięcia po tę książkę i rozpoczęcia
czytania tego słowa wstępnego?
Kiedy mnie się to przydarzyło, byłem w szóstej klasie. Interesowałem się nauką,
kosmosem i wszystkimi rzeczami technicznymi. Moja mama znalazła w katalogu pla-
stikowy komputer i zamówiła go dla mnie. Nazywał się Digi-Comp. Czterdzieści lat
później ten mały plastikowy komputer zajmuje honorowe miejsce na mojej półce. To
był katalizator, który rozpalił moją trwającą do dzisiaj pasję do programowania. Dzię-
ki niemu zacząłem przeczuwać, ile radości może sprawiać pisanie programów, które
rozwiązują problemy innych ludzi. Ten komputer to były zaledwie trzy przerzutniki typu
RS i sześć bramek AND, ale to wystarczyło — komputer działał. I wtedy… dla mnie…
się zaczęło…
Jednak radość, którą odczuwałem, wkrótce została stłumiona, gdy zdałem sobie sprawę,
że oprogramowanie prawie zawsze ulega degradacji w kierunku nieładu. To, co w umy-
słach programistów zaczyna się jako krystalicznie czysty projekt, wraz z upływem czasu
zaczyna się psuć, niczym kawałek marnej jakości mięsa. Ten śliczny, niewielki system,
który stworzyliśmy rok temu, w kolejnym roku przekształca się w przerażające bagno
splątanych funkcji i zmiennych.
Dlaczego tak się dzieje? Dlaczego systemy się psują? Dlaczego nie mogą pozostawać
czyste?
Czasami obwiniamy naszych klientów. Czasami oskarżamy ich o zmianę wymagań.
Pocieszamy się, wierząc, że gdyby tylko klientom wystarczyło to, czego potrzebowali
wcześniej, wówczas projekt byłby udany. To wina klientów — zmienili nam wymagania.
Proszę bardzo, oto odpowiedź: zmiana wymagań. Projekty, które nie tolerują zmiany
wymagań, są przede wszystkim słabymi projektami. Celem każdego kompetentnego
projektanta oprogramowania jest tworzenie programów, które są odporne na zmiany.
Wygląda na to, że problem ten jest wyjątkowo trudny do rozwiązania. Tak bardzo
trudny, że w istocie niemal każdy system, jaki został kiedykolwiek wyprodukowany,
10 SŁOWO WSTĘPNE

cierpi na skutek powolnego i osłabiającego rozkładu. Zepsucie jest do tego stopnia wszech-
obecne, że aż ukuliśmy specjalny termin na określenie zepsutych programów — kod
zastany (ang. legacy code).
Zastany, cudzy kod. Słowa te budzą niesmak w sercach programistów. Przywodzą
na myśl obrazy przedzierania się przez mroczne bagno pełne splątanych zarośli, z ata-
kującymi od dołu pijawkami i żądlącymi od góry komarami. Kojarzą się z odorem wilgo-
ci, szlamu, zastoju i padliny. Chociaż nasza początkowa radość z programowania mogła
być intensywna, to smutna konieczność radzenia sobie z cudzym kodem częstokroć
wystarcza do zduszenia tego pierwszego płomienia.
Wielu z nas próbowało odkryć sposoby na powstrzymanie kodu przed rozkładem.
Pisaliśmy książki o regułach, wzorcach i praktykach, które mogą pomóc programi-
stom w utrzymaniu ich systemów w czystości. Michael Feathers wykazuje się jednak
intuicją, której wielu z nas brakło. Zapobieganie jest niedoskonałe. Nawet najbardziej
zdyscyplinowany zespół programistów, znający najlepsze zasady, korzystający z naj-
lepszych wzorców i przestrzegający najlepszych praktyk, wytworzy od czasu do czasu
bałagan. Zepsucie ciągle narasta. Próby zapobiegania rozkładowi nie wystarczą; mu-
sisz mieć możliwość jego odwrócenia.
Właśnie o tym jest ta książka; o odwracaniu zepsucia. Traktuje ona o podjęciu spląta-
nego, nieprzejrzystego i zagmatwanego systemu i powolnym — kawałek po kawałku i krok
po kroku — przekształceniu go w prosty, dobrze ustrukturyzowany i poprawnie zapro-
jektowany system. Traktuje o odwracaniu entropii.
Zanim jednak zbytnio się ucieszysz, ostrzegam — odwracanie rozkładu nie jest łatwe
i nie przebiega szybko. Techniki, wzorce i narzędzia, które Michael przedstawia w tej
książce są skuteczne, ale wymagają pracy, czasu, cierpliwości i staranności. Ta książka to
nie zaczarowany pocisk. Nie dowiesz się z niej, jak w ciągu jednej nocy wyeliminować
nagromadzone w Twoich systemach zepsucie. Zamiast tego w książce tej opisano
zbiór elementów dyscypliny, koncepcji oraz postaw, które będą przy Tobie obecne
przez resztę Twojej kariery i które pomogą Ci zamienić systemy, które stopniowo się
degradują, w systemy, które stopniowo stają się coraz lepsze.

Robert C. Martin
29 czerwca 2004 r.
PRZEDMOWA 11

Przedmowa

Czy pamiętasz swój pierwszy program, jaki napisałeś? Ja pamiętam mój. To był mały
program graficzny, który napisałem na wczesnym pececie. Zacząłem programować
później niż większość moich kolegów. Rzecz jasna, widziałem komputery, kiedy by-
łem dzieckiem. Pamiętam, że byłem pod wielkim wrażeniem minikomputera, który
zobaczyłem kiedyś w biurze, ale przez całe lata nawet nie miałem szansy, żeby usiąść
przed czymś takim. Później, gdy byłem nastolatkiem, kilku moich przyjaciół kupiło
sobie pierwsze komputery TRS-80. Byłem nimi zainteresowany, ale odczuwałem też
trochę obaw. Wiedziałem, że jeśli zacznę bawić się komputerami, to przepadnę. Wy-
dawało się to nazbyt fajne. Nie wiem, skąd aż tak dobrze znałem samego siebie, ale
udało mi się przed tym powstrzymać. Potem, na studiach, kolega z pokoju miał kom-
puter, a ja kupiłem kompilator C, żebym mógł nauczyć się programowania. I wtedy
się zaczęło. Noc po nocy wypróbowywałem różne rzeczy, przekopując się przez kod
źródłowy edytora Emacs, który był dołączony do kompilatora. To było uzależniające,
to było wymagające, i ja to kochałem.
Mam nadzieję, że także doświadczyłeś czegoś podobnego — czystej radości spra-
wiania, że coś działa na komputerze. Prawie każdy programista, którego pytałem, tego
zaznał. Radość ta częściowo odpowiada za to, że wybraliśmy właśnie taką pracę, ale
gdzie podziewa się ona na co dzień?
Kilka lat temu, pewnego wieczora po skończonej pracy, zadzwoniłem do mojego
przyjaciela, Erika Meade’a. Wiedziałem, że Erik właśnie zaczął pracować jako konsul-
tant w nowym zespole, więc zapytałem go: „A jak oni sobie radzą?”. „Człowieku! Oni
pracują na cudzym kodzie”, odpowiedział. To był jeden z niewielu razy w moim życiu,
kiedy słowa kolegi sprawiły, że poczułem się, jakbym dostał obuchem po głowie.
Czułem to w samym środku siebie. Erik wyraził słowami właśnie te odczucia, które
często mnie nachodzą, gdy po raz pierwszy odwiedzam zespoły programistów. Chło-
paki bardzo się starają, ale pod koniec dnia, ze względu na presję czasu, zaszłości hi-
storyczne lub brak lepszego kodu, z którym mogliby porównać rezultaty swoich wy-
siłków, wielu z nich posługuje się cudzym kodem.
Czym jest cudzy kod lub inaczej, kod zastany? Użyłem tego określenia bez jego
zdefiniowania. Spójrzmy zatem na jego ścisłą definicję: kod zastany to kod, który dostałeś
od kogoś. Może nasza firma pozyskała kod od innej firmy, a może ludzie z pierwotne-
go zespołu przeszli do innych projektów. Zastany kod to kod kogoś innego, ale w termi-
nologii programistów słowa te mają o wiele szerszą wymowę. Określenie kod zastany
wraz z upływem czasu zyskało wiele odcieni znaczeń i zwiększyło swoją wagę.
12 PRZEDMOWA

O czym myślisz, kiedy słyszysz słowa kod zastany? Jeżeli jesteś chociaż trochę taki jak
ja, wyobrazisz sobie zagmatwane, nieczytelne struktury; kod, który musisz zmienić, ale
którego tak naprawdę nie rozumiesz. Pomyślisz o bezsennych nocach spędzonych na pró-
bach dodania funkcji, które powinny być łatwe w implementacji, oraz o demoralizacji;
poczuciu, że wszyscy w zespole mają dosyć bazy kodu do tego stopnia, że nawet im nie
zależy — kodu, któremu życzysz, żeby przepadł. Część Ciebie czuje się źle już na samą
myśl o jego ulepszeniu. Wydaje Ci się, że nie jest to warte Twoich wysiłków. Taka definicja
cudzego kodu nie ma nic wspólnego z osobą, która go napisała. Kod może ulegać pogor-
szeniu na różne sposoby, a wiele z nich wcale nie zależy od tego, czy pochodzi on od
innego zespołu programistów.
W branży kod zastany jest często używanym slangowym określeniem trudnego do
zmiany kodu, którego nie rozumiemy. Pracując jednak całymi latami w zespołach
i pomagając im w pokonywaniu poważnych problemów z kodem, doszedłem do innej
definicji.
Dla mnie kod zastany to po prostu kod bez testów. Niektórzy czuli do mnie urazę
z powodu takiej definicji. Co testy mają wspólnego z tym, czy kod jest zły? Dla mnie
odpowiedź jest prosta, a poniższą kwestię będę rozwijał w tej książce:

Kod bez testów to zły kod. Nie ma znaczenia, jak dobrze jest napisany; nie ma znaczenia, jaki
jest ładny, jak bardzo zorientowany obiektowo czy też jak mocno hermetyczny. Za pomocą
testów możemy zmienić zachowanie naszego kodu szybko i w sposób weryfikowalny. Bez nich
tak naprawdę nie wiemy, czy kod zmierza ku lepszemu, czy ku gorszemu.

Być może myślisz, że taka ocena jest sroga. A co z czystym kodem? Czy nie wystarczy,
gdy baza kodu jest bardzo przejrzysta i ma poprawną strukturę? Cóż, nie popełniaj takiego
błędu. Uwielbiam czysty kod. Uwielbiam go nawet bardziej niż większość osób, które
znam, ale chociaż czysty kod jest dobrym zjawiskiem, to jednak nie wystarcza. Zespoły
programistów podejmują poważne ryzyko, kiedy próbują wprowadzać duże zmiany bez
przeprowadzania testów. To jak uprawianie akrobatyki na trapezie bez siatki zabezpiecza-
jącej. Wymaga niesamowitej sprawności i doskonałego zrozumienia, co może się stać
na każdym kroku. Dokładne rozeznanie, co nastąpi, gdy podmienisz kilka zmiennych,
często jest jak pewność tego, czy inny akrobata pochwyci Twoje ramiona, kiedy już zakoń-
czysz salto. Jeżeli znalazłeś się w zespole, który ma taki czysty kod, to Twoja sytuacja
jest lepsza niż wielu innych programistów. W trakcie mojej pracy zauważyłem, że zespoły
dysponujące takim stopniem czystości w odniesieniu do całego swojego kodu należą
do rzadkości. Wydają się one anomalią statystyczną. I wiesz co? Jeśli nie prowadzą one
wspierających testów, zmiany w kodzie zdają się postępować wolniej niż w przypadku
zespołów, które to robią.
Tak, zespoły stają się lepsze i zaczynają tworzyć czystszy kod, ale oczyszczenie sta-
rego kodu zabiera mnóstwo czasu. W wielu przypadkach nigdy nie uda się zrobić tego do
końca. Z tego względu nie mam problemu, definiując cudzy kod jako kod bez testów.
Jest to całkiem dobra definicja robocza i wskazuje ona rozwiązanie.
PRZEDMOWA 13

Od jakiegoś już czasu całkiem sporo mówię o testach, ale książka ta nie jest o te-
stowaniu. Traktuje ona o umiejętności wprowadzania bez obaw zmian w dowolnej bazie
kodu. W kolejnych jej rozdziałach opiszę techniki, które możesz wykorzystać do zro-
zumienia kodu, przetestowania go, poddania go refaktoryzacji i dodania w nim nowych
funkcjonalności.
Jedną z rzeczy, które zauważysz podczas lektury tej książki, jest fakt, że nie opowiada
ona o ładnym kodzie. Przykłady, z których korzystam, są zmyślone, ponieważ zawarłem
z moimi klientami umowy o poufności. W wielu przypadkach starałem się jednak zachować
charakter kodu, który widziałem w rzeczywistości. Nie twierdzę, że przykłady te zawsze są
reprezentatywne. Rzecz jasna, istnieją gdzieś oazy wspaniałego kodu, ale — szczerze mó-
wiąc — istnieją też fragmenty kodu, które są znacznie gorsze niż cokolwiek, co mógł-
bym zamieścić w tej książce w ramach przykładu. Poza koniecznością zachowania po-
ufności względem klientów po prostu nie mogłem umieścić w tej książce takiego kodu bez
zanudzenia Cię na śmierć i zagrzebywania istotnych kwestii w grzęzawisku szczegółów.
W rezultacie liczne przykłady są stosunkowo krótkie. Jeśli spojrzysz na któryś z nich i po-
myślisz: „No nie, on tego nie rozumie — moje metody są o wiele większe i o wiele gorsze”,
proszę, abyś obiektywnie zapoznał się z poradą, jakiej udzielam, i sprawdził, czy ma ona
zastosowanie w Twoim przypadku, nawet jeśli przykład wydaje się prostszy.
Omawiane tu techniki były testowane na zdecydowanie większych fragmentach kodu;
to tylko ograniczenia związane z formatem książki sprawiły, że przykłady są mniejsze.
Gdy w przykładowym kodzie — jak poniżej — zobaczysz wielokropki (…), będziesz mógł
je przeczytać jako „tu wstaw 500 linii brzydkiego kodu”:
m_pDispatcher->register(listener);
...
m_nMargins++;

Książka ta zatem nie traktuje o ładnym kodzie, ale jeszcze mniej mówi o ładnym pro-
jekcie. Dobry projekt powinien być celem dla każdego z nas, ale w przypadku cudzego
kodu dochodzimy do niego w poszczególnych krokach. W niektórych rozdziałach opisuję
sposoby dodawania nowego kodu do istniejących baz kodu i pokazuję, jak to zrobić,
mając na względzie zasady dobrego projektowania. Możesz rozpocząć wprowadzanie
obszarów kodu wysokiej jakości w zastanych bazach kodu, ale nie zdziw się, jeśli nie-
które z czynności, jakie wykonasz w celu wprowadzenia zmian, sprawią, że kod stanie się
trochę brzydszy. Praca ta przypomina operację. Musimy zrobić kilka nacięć i pogrzebać
w trzewiach, powstrzymując nasze poczucie estetyki. Czy główne organy pacjenta i jego
wnętrzności mogą być lepsze, niż są teraz? Tak. Czy zapominamy zatem o naszym pro-
blemie, zaszywamy pacjenta i mówimy mu, żeby się dobrze odżywiał i trenował do mara-
tonu? Moglibyśmy, ale to, co tak naprawdę powinniśmy zrobić, to przyjąć pacjenta takim,
jakim jest, naprawić w nim to, co złe, i pozostawić go w lepszym stanie zdrowia. Być może
nigdy nie zostanie olimpijczykiem, ale nie możemy pozwolić, żeby „najlepsze” stało się
wrogiem „lepszego”. Bazy kodu mogą stać się zdrowsze i łatwiejsze w obsłudze. Kiedy
pacjent poczuje się już trochę lepiej, często jest to chwila, w której można pomóc mu
w podjęciu postanowień dotyczących zdrowszego stylu życia. Taki właśnie cel przyświeca
14 PRZEDMOWA

nam w przypadku cudzego kodu. Staramy się dotrzeć do miejsca, w którym będziemy mogli
się rozluźnić; oczekujemy go i aktywnie staramy się ułatwić wprowadzanie zmian w kodzie.
Jeśli damy radę podtrzymać to poczucie w zespole, projekt stanie się lepszy.
Techniki, o których piszę, odkryłem i nauczyłem się ich razem ze współpracownikami
oraz klientami w trakcie lat pracy spędzonych na próbach uzyskania kontroli nad nie-
sfornymi bazami kodu. Taką wagę do cudzego kodu zacząłem przywiązywać przypadkowo.
Kiedy zaczynałem pracę w firmie Object Mentor, większość moich zadań wiązała się
z udzielaniem pomocy zespołom — które miały poważne problemy — w rozwijaniu ich
umiejętności oraz zdolności do współpracy do chwili, w której mogły regularnie oddawać
dobrej jakości kod. Często korzystaliśmy z praktyk programowania ekstremalnego, aby
pomóc zespołom w uzyskaniu kontroli nad ich pracą, w rozwinięciu intensywniejszej
współpracy oraz w przekazywaniu rezultatów. Często mam poczucie, że programowanie
ekstremalne w mniejszym stopniu jest metodą na tworzenie oprogramowania, a w więk-
szym sposobem na uzyskanie dobrze zgranego zespołu, który co dwa tygodnie będzie
oddawać wspaniałe programy.
Od samego początku był jednak pewien problem. Wiele z pierwszych projektów ko-
rzystających z programowania ekstremalnego było projektami powstającymi od zera.
Klienci, z którymi się spotykałem, mieli dość duże bazy kodu i napotykali kłopoty. Potrze-
bowali jakiegoś sposobu, aby zapanować nad swoją pracą i rozpocząć oddawanie opro-
gramowania. Wraz z upływem czasu zdałem sobie sprawę, że razem z klientami robiliśmy
w kółko wciąż to samo. To poczucie miało swoją kulminację podczas pewnej pracy, którą
razem z zespołem programistów miałem do wykonania w branży finansowej. Zanim do
nich dołączyłem, zdali oni już sobie sprawę z tego, że testy jednostkowe to świetna rzecz, ale
testy, które prowadzili, były pełnymi testami scenariuszowymi, które wymagały wielo-
krotnych odwołań do bazy danych i korzystały z obszernych fragmentów kodu. Testy
te były trudne do napisania, a zespół nie przeprowadzał ich zbyt często, ponieważ ich
uruchomienie zabierało dużo czasu. Kiedy usiedliśmy razem, żeby pousuwać zależności
i poddać testom mniejsze fragmenty kodu, ogarnęło mnie silne poczucie déjà vu. Wy-
glądało na to, że wykonywałem te same czynności z każdym zespołem, z którym się spoty-
kałem, i że to był taki rodzaj pracy, o którym tak naprawdę nikt nie chciał myśleć. Była
to brudna robota, której się podejmujesz, kiedy chcesz w kontrolowany sposób rozpo-
cząć pracę ze swoim kodem, jeżeli wiesz, jak to robić. Wtedy zdecydowałem, że warto
zastanowić się nad tym, jak rozwiązujemy takie problemy, oraz zapisać swoje wnioski, aby
zespoły programistów zyskały przewagę i miały łatwiejsze życie ze swoimi bazami kodu.
Jeszcze tylko uwaga na temat przykładów. Korzystałem z kilku różnych języków
programowania. Większość z przykładów została napisana w Javie, C++ i C. Wybrałem
Javę, bo jest ona bardzo popularnym językiem, a uwzględniłem C++, ponieważ w za-
stanym środowisku stawia on pewne szczególne wymagania. Z kolei zdecydowałem się na
C, gdyż uwypukla on wiele problemów, które pojawiają się w cudzym kodzie procedural-
nym. Języki te obejmują całe spektrum problemów, które pojawiają się w związku z pracą
przy zastanym kodzie. Jeśli jednak język, którego używasz, nie został uwzględniony w przy-
PRZEDMOWA 15

kładach, zapoznaj się z nimi, tak czy inaczej. Z wielu technik, które omawiam, można
korzystać w innych językach, takich jak Delphi, Visual Basic, COBOL i FORTRAN.
Mam nadzieję, że techniki opisane w tej książce będą dla Ciebie przydatne i że umoż-
liwią Ci odzyskanie tego, co w programowaniu daje radość. Pisanie programów może
być bardzo satysfakcjonującym i przyjemnym zajęciem. Jeśli nie odczuwasz tej radości
w swojej codziennej pracy, chciałbym, żeby techniki, które oferuję w tej książce, po-
mogły Ci ją odnaleźć i zaszczepić w Twoim zespole.

Podziękowania
Przede wszystkim mam poważny dług wdzięczności wobec mojej żony Anny oraz
dzieci, Debory i Ryana. Ich miłość i wsparcie umożliwiły powstanie tej książki oraz
zdobywanie całej wiedzy, które miało miejsce wcześniej. Chciałbym też podziękować
„Wujkowi Bobowi” Martinowi, prezydentowi i założycielowi firmy Object Mentor.
Jego rygorystycznie pragmatyczne podejście do opracowywania i projektowania, od-
dzielanie krytycznego od błahego dało mi coś, czego mogłem uczepić się jakieś 10 lat
temu, kiedy wyglądało na to, że wkrótce utonę w falach oderwanych od rzeczywistości
rad. Dziękuję Ci też, Bob, za to, że przez ostatnie pięć lat dałeś mi możliwość zobaczenia
większej ilości kodu i pracy z większą liczbą ludzi, niż kiedykolwiek byłem w stanie to
sobie wyobrazić.
Muszę także podziękować Kentowi Beckowi, Martinowi Fowlerowi, Ronowi Jeffriesowi
i Wardowi Cunninghamowi za to, że od czasu do czasu oferowali mi porady i że sporo
mnie nauczyli o pracy zespołowej, projektowaniu oraz programowaniu. Specjalne po-
dziękowania należą się wszystkim osobom, które przeglądały szkice tej książki. Oficjalny-
mi recenzentami byli Sven Gorts, Robert C. Martin, Erik Meade i Bill Wake; nieoficjalnymi
dr Robert Koss, James Grenning, Lowell Lindstrom, Micah Martin, Russ Rufer i Silicon
Valley Patterns Group oraz James Newkirk.
Dziękuję też recenzentom wczesnych szkiców tej książki, które zamieściłem w in-
ternecie. Ich opinie znacząco wpłynęły na kierunek, w którym książka zmierzała, po tym
jak zmieniłem jej format. Z góry przepraszam wszystkich Was, których być może pomi-
nąłem. Wczesnymi recenzentami byli Darren Hobbs, Martin Lippert, Keith Nicholas,
Phlip Plumlee, C. Keith Ray, Robert Blum, Bill Burris, William Caputo, Brian Marick, Steve
Freeman, David Putman, Emily Bache, Dave Astels, Russel Hill, Christian Sepulveda
i Brian Christopher Robinson.
Moje podziękowania należą się także Joshui Kerievskiemu, który przeprowadził
wczesną, kluczową recenzję, oraz Jeffowi Langrowi, który dopomagał mi radami i punk-
towymi recenzjami podczas całego procesu pisania.
Recenzenci w znacznym stopniu pomogli mi udoskonalić szkic tej książki, a jeśli
pozostały w niej błędy, to są one wyłącznie moje.
16 PRZEDMOWA

Dziękuję Martinowi Fowlerowi, Ralphowi Johnsonowi, Billowi Opdyke’owi, Do-


nowi Robertowi i Johnowi Brandtowi za ich pracę wykonaną w zakresie refaktoryzacji
— była inspirująca.
Specjalny dług wdzięczności mam wobec Jaya Packlicka, Jacques’a Morela i Kelly Mo-
wer z Sabre Holdings oraz Grahama Wrighta z Workshare Technology za ich wsparcie
i opinie.
Specjalne podziękowania przekazuję Paulowi Petralii, Michelle Vincenti, Lori Lyons,
Kriście Hansing i pozostałym osobom załogi Prentice Hall. Dziękuję Ci, Paul, za całą
pomoc i zachętę, których potrzebował ten początkujący autor.
Specjalne podziękowania należą się także Garemu i Joan Feathersom, April Roberts,
dr Raimundowi Ege’owi, Davidowi Lopezowi de Quintanie, Carlosowi Perezowi, Carlo-
sowi M. Rodriguezowi i świętej pamięci dr. Johnowi C. Comfortowi za pomoc i słowa
zachęty udzielane mi przez te wszystkie lata. Muszę też podziękować Brianowi Buttonowi
za przykład z rozdziału 21., „Wszędzie zmieniam ten sam kod”. Napisał on zamieszczony
tam kod w ciągu mniej więcej godziny, kiedy wspólnie pracowaliśmy nad kursem z refak-
toryzacji. Kod ten stał się moim ulubionym przykładem, którego nauczam.
Specjalne podziękowania kieruję do Janika Topa, którego instrumentalny utwór De
Futura służył jako ścieżka dźwiękowa przez kilka ostatnich tygodni mojej pracy nad tą
książką.
Na koniec chciałbym podziękować wszystkim, z którymi pracowałem w ostatnich kilku
latach, a których wnikliwość i stawiane przez nich przede mną wyzwania wzmocniły ma-
teriał zamieszczony w tej książce.

Michael Feathers
mfeathers@objectmentor.com
www.objectmentor.com
WSTĘP 17

Wstęp

Jak korzystać z tej książki?


Wypróbowałem kilka różnych form pisania, zanim zdecydowałem się na tę, której
użyłem w tej książce. Wiele spośród rozmaitych technik i praktyk, które są przydatne
podczas pracy nad cudzym kodem, trudno jest wyjaśnić w oderwaniu od siebie. Naj-
prostsze zmiany dają się zwykle wprowadzać łatwiej, kiedy możesz znaleźć spoiny,
tworzyć fałszywe obiekty i eliminować zależności, korzystając z kilku technik ich usu-
wania. Zdecydowałem, że najłatwiejszym sposobem sprawienia, aby książka ta była
przystępna i wygodna, będzie zorganizowanie jej najobszerniejszego fragmentu (część II,
„Zmiany w oprogramowaniu”) w formie FAQ (ang. frequently asked questions, czyli
często zadawanych pytań). Ponieważ określone techniki często wymagają korzystania
także z innych technik, poszczególne rozdziały w postaci FAQ są ze sobą mocno po-
wiązane. Niemal w każdym rozdziale znajdziesz odwołania — łącznie z numerami
stron — do innych rozdziałów i fragmentów opisujących pewne techniki oraz re-
faktoryzację. Przepraszam, jeśli z tego powodu będziesz szaleńczo wertować tę książkę
w próbach znalezienia odpowiedzi na swoje pytania, ale zakładam, że będziesz postę-
pować raczej tak, zamiast czytać ją od deski do deski, starając się zrozumieć, jak dzia-
łają wszystkie opisane techniki.
W części „Zmiany w oprogramowaniu” postarałem się odpowiedzieć na często za-
dawane pytania, które pojawiają się przy okazji pracy nad cudzym kodem. Każdy z roz-
działów nawiązuje tytułem do pewnego problemu. Z tego powodu tytuły są raczej długie,
ale mam nadzieję, że dzięki temu szybko znajdziesz fragment, który pomoże Ci rozwiązać
określony problem, z którym masz do czynienia.
„Zmiany w oprogramowaniu” są uzupełnione rozdziałami wprowadzającymi (część I,
„Mechanika zmian”) oraz katalogiem refaktoryzacji, bardzo przydatnymi podczas pracy
nad cudzym kodem (część III, „Techniki usuwania zależności”). Proszę, żebyś zapoznał się
z rozdziałami wprowadzającymi, szczególnie z rozdziałem 4., „Model spoinowy”. W roz-
działach tych podany jest kontekst oraz nazewnictwo wszystkich technik, które zostaną
opisane w dalszej części. Ponadto, jeśli znajdziesz termin, który nie został opisany w treści
rozdziału, poszukaj go w słowniku.
18 WSTĘP

Sposoby refaktoryzacji, omówione w części „Techniki usuwania zależności”, są szcze-


gólne, gdyż zostały przygotowane z myślą o tym, by wykorzystywać je bez testów, które
należy przeprowadzić dopiero później. Zachęcam Cię, abyś zapoznał się z każdą techniką,
dzięki czemu otworzy się przed Tobą więcej możliwości, kiedy już zaczniesz ujarzmiać swój
zastany po kimś innym kod.
Część I

Mechanika zmian
20 CZĘŚĆ I MECHANIKA ZMIAN
CZTERY POWODY WPROWADZANIA ZMIAN W OPROGRAMOWANIU 21

Rozdział 1.

Zmiany w oprogramowaniu

Wprowadzanie zmian w kodzie jest czymś wspaniałym. Właśnie tym zarabiamy na życie.
Istnieją sposoby zmieniania kodu, które znacznie je upraszczają, chociaż są też i sposoby,
które je utrudniają. W branży nie mówiliśmy o tym zbyt wiele. Najbliżej tego tematu zna-
leźliśmy się przy okazji zapoznawania się z literaturą traktującą o refaktoryzacji. Myślę, że
możemy nieco poszerzyć to zagadnienie i wyjaśnić, jak radzić sobie z kodem w najtrud-
niejszych sytuacjach. W tym celu musimy bardziej zagłębić się w mechanikę zmian.

Cztery powody wprowadzania zmian w oprogramowaniu


Aby uprościć sprawy, przyjrzyjmy się czterem głównym powodom, dla których wprowadza
się zmiany w oprogramowaniu.
1. Dodawanie funkcji
2. Poprawianie błędów
3. Ulepszanie projektu
4. Optymalizacja wykorzystania zasobów

Dodawanie funkcji i poprawianie błędów


Dodawanie funkcji wydaje się najprostszym rodzajem zmiany, jaką można wprowadzić.
Program zachowuje się w pewien sposób, a użytkownicy mówią, że system powinien robić
jeszcze coś innego.
Załóżmy, że pracujemy nad aplikacją sieciową i szefowa mówi nam, że chciałaby, aby
przenieść logo firmy z lewej strony ekranu na prawą stronę. Rozmawiamy z nią na ten
temat i odkrywamy, że wcale nie jest to takie proste. Szefowa chce przenieść logo, ale też
chciałaby wprowadzić inne zmiany — w nowej wersji logo miałoby być animowane.
Czy jest to poprawienie błędu, czy też dodanie nowej funkcji? To zależy od punktu widze-
nia. Z perspektywy klienta zdecydowanie chodzi o poprawienie błędu. Być może szefowa
22 ROZDZIAŁ 1. ZMIANY W OPROGRAMOWANIU

zobaczyła stronę, udała się na spotkanie z osobami ze swojego działu, a one zdecydowały
o zmianie położenia logo i poprosiły o niewielkie rozszerzenie funkcjonalności. Z punktu
widzenia programistów modyfikację można potraktować jako zupełnie nową cechę:
„Gdyby tylko przestali zmieniać swoje zdanie, już dawno byśmy skończyli”. Jednak w nie-
których organizacjach przesunięcie logo jest traktowane jako jedynie poprawienie błędu,
bez względu na to, że zespół programistów będzie mieć z tym mnóstwo nowej roboty.
Kuszące jest stwierdzenie, że wszystko to jest po prostu subiektywne. Dla Ciebie to
poprawienie błędu, a dla mnie nowa funkcja, i tyle. Niestety, w wielu organizacjach popra-
wianie błędów i dodawanie funkcji muszą być traktowane i rozliczane oddzielnie ze wzglę-
du na kontrakty i kwestię jakości. Na poziomie interpersonalnym możemy bez końca
spierać się, czy dodajemy nowe funkcje, czy też poprawiamy błędy, ale i tak wszystko
sprowadza się do zmian w kodzie i w pozostałych elementach. Co gorsza, cała ta dys-
kusja o poprawianiu błędów i dodawaniu funkcji przesłania coś, co z technicznego
punktu widzenia jest dla nas o wiele ważniejsze, mianowicie zmiany w zachowaniu.
Istnieje ogromna różnica między dodaniem nowego zachowania a zmianą zachowania
dotychczasowego.

Zachowanie jest najważniejszym elementem oprogramowania. To właśnie na nim polegają


użytkownicy. Lubią, kiedy dodajemy zachowanie (pod warunkiem, że naprawdę tego
oczekiwali), ale kiedy zmieniamy lub usuwamy zachowanie (przy okazji wprowadzając
błędy), na którym polegają, przestają nam ufać.

Czy w przykładzie z firmowym logo dodajemy zachowanie? Tak. Po zmianie system


będzie wyświetlać logo z prawej strony ekranu. Czy usuwamy jakieś zachowanie? Tak
— z lewej strony logo nie będzie już obecne.
Spójrzmy na trudniejszy przypadek. Załóżmy, że klient chce dodać logo z prawej strony
ekranu, ale nie było go wcześniej z lewej strony. Tak, dodajemy nowe zachowanie, ale czy
jakieś usuwamy? Czy w miejscu, w którym ma się znaleźć logo, było wcześniej coś innego?
Czy zmieniamy zachowanie, dodajemy je, a może i jedno, i drugie?
Okazuje się, że możemy na nasze potrzeby zdefiniować takie rozróżnienie, które będzie
przydatniejsze dla nas — programistów. Jeżeli musimy zmodyfikować kod (a HTML
w pewnym sensie liczy się jako kod), być może będziemy też zmieniać zachowanie. Jeśli
tylko dodajemy kod i go wywołujemy, często dodajemy zachowanie. Spójrzmy na ko-
lejny przykład. Oto metoda w klasie Javy:
public class CDPlayer
{
public void addTrackListing(Track track) {
...
}
...
}

Klasa ta ma metodę, która umożliwia dodawanie list odtwarzania. Dorzućmy następną


metodę, która pozwoli nam zastępować listy.
CZTERY POWODY WPROWADZANIA ZMIAN W OPROGRAMOWANIU 23

public class CDPlayer


{
public void addTrackListing(Track track) {
...
}
public void replaceTrackListing(String name, Track track) {
...
}
...
}

Czy dorzuciwszy tę metodę, dodaliśmy do naszej aplikacji nowe zachowanie, czy może
je zmieniliśmy? Odpowiedź brzmi: ani jedno, ani drugie. Dodanie metody nie zmienia
zachowania programu, dopóki metoda ta nie zostanie w jakiś sposób wywołana.
Wprowadźmy jeszcze jedną zmianę. Dołóżmy w interfejsie użytkownika nowy przy-
cisk odtwarzacza CD. Przycisk umożliwi użytkownikom zastępowanie list odtwarzania.
Za pomocą tego posunięcia dodajemy zachowanie, które zdefiniowaliśmy w metodzie
replaceTrackListing, ale też w niewielkim stopniu zmieniamy istniejące zachowanie.
Interfejs użytkownika z tym nowym przyciskiem będzie się tworzył inaczej. Prawdopo-
dobnie wyświetlenie interfejsu potrwa o mikrosekundę dłużej. Dodanie zachowania bez
zmiany interfejsu w pewnym zakresie wydaje się prawie niemożliwe.

Ulepszanie projektu
Ulepszenie projektu stanowi inny rodzaj zmiany w oprogramowaniu. Kiedy chcemy
zmienić strukturę programu, aby był łatwiejszy w konserwacji, zwykle chcemy też po-
zostawić jego zachowanie bez zmian. Jeśli w tym procesie pominiemy jakieś zachowanie,
często nazywamy to błędem. Jednym z głównych powodów, dla których programiści często
nie podejmują się ulepszania projektów, jest względna łatwość utraty w tym procesie za-
chowania lub stworzenia zachowania niepożądanego.
Czynność ulepszania projektu bez zmiany jego zachowania zwana jest refaktoryzacją.
Zamysł kryjący się za refaktoryzacją polega na tym, że możemy sprawić, aby program
był prostszy w konserwacji bez zmieniania jego zachowania, jeżeli napiszemy testy
gwarantujące, że obecne zachowanie nie zmieni się, a w celu weryfikowania tego zało-
żenia w trakcie procesu będziemy poruszać się małymi krokami. Programiści oczysz-
czali kody od lat, ale dopiero w ostatnich latach refaktoryzacja ruszyła z miejsca. Refakto-
ryzacja różni się od ogólnego porządkowania tym, że podejmujemy się nie tylko działań
o niskim ryzyku, takich jak ponowne formatowanie kodu źródłowego, czy też inwazyj-
nych i ryzykownych technik, takich jak przepisywanie sporych jego fragmentów. Miano-
wicie wprowadzamy jeszcze serię niewielkich, strukturalnych zmian wspieranych te-
stami, aby kod był łatwiejszy w modyfikowaniu. Z tego punktu widzenia kluczowa sprawa
w refaktoryzacji polega na tym, że kiedy refaktorujesz, nie powinieneś wprowadzać
żadnych zmian funkcjonalnych (chociaż zachowanie może się w pewien sposób zmienić,
gdyż strukturalne zmiany, jakie wprowadzasz, mogą wpływać na wydajność — pozytywnie
albo negatywnie).
24 ROZDZIAŁ 1. ZMIANY W OPROGRAMOWANIU

Optymalizacja
Optymalizacja przypomina refaktoryzację, ale kiedy ją przeprowadzamy, mamy na celu
coś innego. Zarówno w przypadku refaktoryzacji, jak i optymalizacji mówimy: „Po wpro-
wadzeniu zmian zamierzamy zachować dokładnie taką samą funkcjonalność, ale za to
zmienimy coś innego”. W refaktoryzacji tym „czymś innym” jest struktura programu —
chcemy, aby był on łatwiejszy w konserwacji. W optymalizacji z kolei „czymś innym” jest
jakiś zasób używany przez program, zwykle czas albo pamięć.

Zebranie wszystkiego razem


Dziwne może wydawać się, że refaktoryzacja i optymalizacja są w pewnym sensie do siebie
podobne. Wydaje się, że są one do siebie zbliżone bardziej niż do poprawiania błędów albo
dodawania nowych funkcji. Ale czy tak jest w istocie? Wspólna cecha refaktoryzacji i opty-
malizacji polega na tym, że pozostawiamy niezmienne zachowanie, podczas gdy zmieniamy
coś innego.
W ogólności mogą zmienić się trzy różne elementy, kiedy pracujemy nad systemem:
struktura, funkcjonalność oraz wykorzystanie zasobów.
Spójrzmy, co się zazwyczaj zmienia, a co pozostaje mniej więcej takie samo, gdy
wprowadzamy cztery różne rodzaje zmian (to prawda, często zmieniają się wszystkie
trzy elementy, ale przyjrzyjmy się temu, co jest typowe):

Dodawanie Poprawianie
Refaktoryzacja Optymalizacja
funkcji błędów
Struktura Zmienia się Zmienia się Zmienia się —
Funkcjonalność Zmienia się Zmienia się — —
Użycie zasobów — — — Zmienia się

Powierzchniowo refaktoryzacja i optymalizacja wyglądają podobnie; obie pozostawiają


niezmienioną funkcjonalność. Co się jednak stanie, gdy w odrębnym wierszu wymienimy
nową funkcjonalność? Kiedy dodajemy nowe funkcje, często dodajemy też nową funkcjo-
nalność, tyle że bez zmieniania istniejącej już funkcjonalności.

Dodawanie
Poprawianie błędów Refaktoryzacja Optymalizacja
funkcji
Struktura Zmienia się Zmienia się Zmienia się —
Nowa
Zmienia się — — —
funkcjonalność
Funkcjonalność — Zmienia się — —
Użycie zasobów — — — Zmienia się
RYZYKOWNA ZMIANA 25

Dodawanie funkcji, refaktoryzacja oraz optymalizacja pozostawiają istniejącą funkcjo-


nalność bez zmian. Jeśli z bliska przyjrzymy się poprawianiu błędów, to zauważymy, że
w istocie zmienia ono funkcjonalność, ale zmiany te często są bardzo małe w porównaniu
z zakresem istniejącej funkcjonalności, która nie podlega zmianom.
Dodawanie funkcji i poprawianie błędów bardzo przypominają refaktoryzację oraz
optymalizację. We wszystkich czterech przypadkach chcemy zmienić jakąś funkcjonalność
i jakieś zachowanie, ale chcemy też o wiele więcej pozostawić (patrz rysunek 1.1).

Rysunek 1.1. Pozostawianie zachowania

To bardzo elegancki rysunek, pokazujący, co powinno się stać, kiedy dokonujemy


zmian. Ale jakie ma on dla nas znaczenie praktyczne? Dobra wiadomość jest taka, że zdaje
się on pokazywać, na czym powinniśmy się skupić. Musimy upewnić się, że niewielka
liczba elementów, które zmieniamy, została poprawnie zmodyfikowana. Z kolei zła wia-
domość jest taka, że — no cóż — nie jest to jedyna rzecz, na której powinniśmy się skon-
centrować. Musimy wymyślić sposób na pozostawienie bez zmian reszty zachowania.
Niestety, utrzymanie zachowania to więcej niż tylko zostawienie kodu w spokoju. Powin-
niśmy mieć pewność, że zachowanie nie ulega zmianom, a to może być trudne. Zakres
zachowania, który musimy pozostawić, jest zazwyczaj dość spory, ale to nic takiego. Rzecz
w tym, że często nie wiemy, jaka część zachowania staje się zagrożona, kiedy wprowadzamy
nasze zmiany. Gdybyśmy to wiedzieli, moglibyśmy skoncentrować się na tym właśnie za-
chowaniu i odpuścić sobie całą resztę. Zrozumienie pozostaje kluczową sprawą, jeśli
chcemy dokonywać zmian bezpiecznie.

Utrzymanie istniejącego zachowania bez zmian jest jednym z największych wyzwań podczas
tworzenia oprogramowania. Nawet gdy zmieniamy podstawowe funkcje, często mamy do
czynienia z bardzo dużymi zakresami zachowania, które chcemy pozostawić bez zmian.

Ryzykowna zmiana
Ocalenie zachowania stanowi prawdziwe wyzwanie. Jeśli musimy dokonać zmian i jedno-
cześnie pozostawić zachowanie, powinniśmy liczyć się z poważnym ryzykiem.
Aby ograniczyć to ryzyko, musimy zadać sobie trzy pytania:
1. Jakie zmiany musimy wprowadzić?
2. Skąd będziemy wiedzieć, że je prawidłowo przeprowadziliśmy?
26 ROZDZIAŁ 1. ZMIANY W OPROGRAMOWANIU

3. Skąd będziemy wiedzieć, że czegoś nie popsuliśmy?


Na jakie zmiany możesz sobie pozwolić, jeśli są one ryzykowne?
Większość zespołów, z którymi pracowałem, próbowała radzić sobie z tym ryzy-
kiem w bardzo ostrożny sposób. Programiści ograniczali liczbę zmian, które wprowadzali
w bazie kodu. Często taka właśnie jest polityka zespołu: „Jeśli się nie popsuło, nie popra-
wiaj”. Czasami nikt tego nawet nie artykułuje. Programiści są po prostu bardzo ostrożni,
kiedy wprowadzają zmiany. „Co? Po to miałbym tworzyć nową metodę? Nie, po prostu
wcisnę wiersze kodu właśnie tu — w tej metodzie, gdzie mogę je widzieć razem z całą resztą
kodu. Będzie z tym mniej edycji i tak jest bezpieczniej”.
Kuszące jest myślenie, że możemy ograniczać problemy z oprogramowaniem, obcho-
dząc je, ale — niestety — one zawsze nas dogonią. Kiedy unikamy tworzenia nowych klas
i metod, istniejące już elementy stają się coraz większe i trudniejsze do zrozumienia. Kiedy
wprowadzasz zmiany w jakimkolwiek dużym systemie, możesz oczekiwać, że będziesz mieć
mniej czasu na zaznajomienie się z obszarem, nad którym pracujesz. Różnica między do-
brymi systemami a złymi jest taka, że w przypadku dobrego systemu jesteś raczej spokojny
po zapoznaniu się z nim i masz pewność co do zmian, które będziesz wprowadzać. W źle
ustrukturyzowanym kodzie przejście od wgryzania się w szczegóły do wprowadzania zmian
w programie przypomina rzucenie się z klifu w celu ratowania się przed tygrysem. Wahasz
się i wahasz: „Czy jestem już na to gotów? No cóż, wydaje mi się, że muszę to zrobić”.
Unikanie zmian niesie ze sobą także inne, złe skutki. Kiedy ludzie nie wprowadzają
zmian, często pogrążają się w zastoju. Rozbijanie dużej klasy na mniejsze części może być
dość czasochłonną czynnością, jeżeli nie wykonujesz jej kilka razy w tygodniu. Kiedy już
zaczniesz to robić, staje się ona rutyną. Jesteś coraz lepszy w rozpoznawaniu, co możesz
rozbić, a czego nie, i staje się to coraz łatwiejsze.
Ostatnią konsekwencją unikania zmian jest strach. Niestety, wiele zespołów żyje w nie-
wiarygodnej obawie przed zmianami, a ich strach rośnie z każdym dniem. Często nie
zdają sobie sprawy z jego rozmiaru, aż do chwili, kiedy poznają lepsze techniki, a ich
strach zaczyna słabnąć.
Mówiliśmy, że unikanie zmian jest złe, ale jaki mamy wybór? Naszą alternatywą są
jeszcze większe starania. Być może moglibyśmy zatrudnić więcej osób, dzięki czemu każdy
miałby wystarczająco dużo czasu, żeby usiąść i przeanalizować kod, przyjrzeć się mu
dokładnie i wprowadzić zmiany we „właściwy” sposób. Oczywiście większa ilość czasu
i dokładność sprawią, że dokonywanie zmian będzie bezpieczniejsze. Ale czy na pew-
no? Czy po takich dokładnych analizach każdy będzie wiedzieć, że wszystko się udało?
Rozdział 2.

Praca z informacją zwrotną

Zmiany w systemie można wprowadzać na dwa główne sposoby. Lubię nazywać je odpo-
wiednio Edytuj i módl się oraz Kryj i modyfikuj. Niestety, sposób Edytuj i módl się
jest prawie branżowym standardem. Kiedy stosujesz Edytuj i módl się, starannie pla-
nujesz zmiany, upewniasz się, że zrozumiałeś kod, który masz zamiar zmodyfikować,
a następnie przystępujesz do wprowadzania zmian. Kiedy skończysz, uruchamiasz sys-
tem, aby zobaczyć, czy zmiana jest aktywna, po czym rozglądasz się jeszcze przez chwilę,
żeby upewnić się, że niczego nie popsułeś. To rozglądanie się jest ważne. Gdy wprowa-
dzasz zmiany, modlisz się i masz nadzieję, że dokonujesz ich prawidłowo, a kiedy już
skończysz, poświęcasz dodatkowy czas, aby upewnić się, że właśnie tak zrobiłeś.
Powierzchownie Edytuj i módl się przypomina pracę z dochowaniem „należytej
staranności”, co jest bardzo profesjonalnym podejściem. Ta „staranność”, której przestrze-
gasz, znajduje się na samym czele. Poświęcasz dodatkowy czas, kiedy zmiany są bardzo
inwazyjne, ponieważ o wiele więcej może się nie udać. Bezpieczeństwo nie wynika jednak
wyłącznie z zachowania staranności. Nie wydaje mi się, by ktokolwiek z nas tylko dlatego
wybrał chirurga operującego nożem do smarowania chleba masłem, że pracuje on ze sta-
rannością. Efektywne wprowadzanie zmian w programach, podobnie jak efektywna chirur-
gia, wymaga tak naprawdę szerszych umiejętności. Praca z dochowaniem należytej staran-
ności nie wystarczy Ci, jeśli nie posługujesz się właściwymi narzędziami oraz technikami.
Kryj i modyfikuj jest inną metodą wprowadzania zmian. Idea stojąca za tym roz-
wiązaniem polega na tym, że podczas wprowadzania zmian w oprogramowaniu można
pracować z siatką zabezpieczającą. Siatka zabezpieczająca, z której będziemy korzystać,
nie będzie rozpięta pod naszymi biurkami, żebyśmy w nią wpadli, kiedy spadniemy
z krzesła. Jest to raczej coś w rodzaju plandeki, którą przykrywamy kod, nad którym
pracujemy, aby zagwarantować, że złe zmiany nie wyciekną i nie skażą reszty naszego
oprogramowania. Przykrycie kodu oznacza pokrycie go testami. Kiedy dla danego frag-
mentu kodu mamy do dyspozycji dobry zestaw testów, możemy wprowadzać zmiany
i bardzo szybko sprawdzać, czy skutki tych zmian są dobre, czy też złe. Nadal dochowujemy
należytej staranności, ale mając informację zwrotną, możemy dokonywać zmian z większą
dbałością.
28 ROZDZIAŁ 2. PRACA Z INFORMACJĄ ZWROTNĄ

Jeżeli takie zastosowanie testów nie jest Ci znane, wszystko to może wydawać się
nieco dziwne. Tradycyjnie testy są pisane i przeprowadzane po napisaniu programu. Grupa
programistów opracowuje kod, a potem grupa testerów uruchamia na kodzie testy i spraw-
dza, czy jest on zgodny ze specyfikacją. W niektórych bardzo tradycyjnych zespołach
w taki właśnie sposób tworzone jest oprogramowanie. Zespół otrzymuje informację
zwrotną, ale zataczana przez nią pętla jest duża. Pracujesz przez kilka tygodni albo kilka
dni, a potem ludzie z innej grupy mówią, czy Ci się udało.
Tak przeprowadzane testy są w rzeczywistości „testami mającymi wykazać popraw-
ność”. Chociaż ich cel jest słuszny, to jednak z testów można też korzystać inaczej.
Możemy wykonywać „testy wykrywające zmianę”.
Tradycyjnie czynność taką nazywamy testowaniem regresyjnym. Cyklicznie uru-
chamiamy testy sprawdzające znane zachowanie, aby przekonać się, czy nasze opro-
gramowanie nadal działa tak samo jak wcześniej.
Kiedy wokół obszarów kodu, w których zamierzasz dokonać zmian, umieściłeś testy,
będą one działać jak imadło programistyczne. Większość zachowania możesz pozostawić
bez zmian i wiesz, że zmieniasz tylko to, co zamierzasz zmienić.

Imadło programistyczne
Imadło — przyrząd służący do mocowania przedmiotów poddawanych obróbce ręcznej lub
mechanicznej. Imadło zbudowane jest z dwu szczęk zaciskanych śrubą z pokrętłem (definicja
na podstawie Wikipedii).
Kiedy mamy do dyspozycji testy wykrywające zmianę, to tak, jakbyśmy mieli zaciśnięte na na-
szym kodzie imadło. Zachowanie kodu zostało unieruchomione w miejscu. Kiedy doko-
nujemy zmian, wiemy, że w danym czasie zmieniamy tylko fragment zachowania. Ujmując
sprawę krótko, mamy kontrolę nad naszą pracą.

Testowanie regresyjne to wspaniały pomysł. Dlaczego nie jest przeprowadzane częściej?


Z testami regresyjnymi wiąże się pewien problem. Kiedy ktoś je przeprowadza, często robi
to na poziomie interfejsu aplikacji. Nie ma znaczenia, czy jest to aplikacja sieciowa,
program uruchamiany w wierszu poleceń, czy też aplikacja bazująca na graficznym
interfejsie użytkownika — testy regresyjne są tradycyjnie postrzegane jako styl testo-
wania na poziomie aplikacji. Jest to niefortunne założenie. Informacje zwrotne, jakie
możemy z nich uzyskać, są bardzo przydatne. Testowanie takie opłaca się przeprowadzać
na wyższym poziomie szczegółowości.
Przeprowadźmy mały eksperyment myślowy. Zagłębiamy się w obszerną funkcję, która
zawiera dużą ilość skomplikowanej logiki. Analizujemy, zastanawiamy się, rozmawiamy
z ludźmi, którzy wiedzą o tym fragmencie kodu więcej niż my, a potem wprowadzamy
zmianę. Chcemy mieć pewność, że zmiana ta niczego nie popsuła, ale skąd mamy to
wiedzieć? Na szczęście jest do dyspozycji grupa ludzi od kontroli jakości, posiadająca
zbiór testów regresyjnych, które może przeprowadzić w nocy. Wołamy ich i prosimy,
CO TO JEST TESTOWANIE JEDNOSTKOWE? 29

żeby zaplanowali test, a oni mówią, że owszem, mogą puścić testy w nocy, ale byłoby dobrze,
gdybyśmy skontaktowali się z nimi odpowiednio wcześniej. Inne grupy zwykle starają się
zaplanować testowanie regresyjne na środek tygodnia, a jeśli będziemy czekać trochę
dłużej, może okazać się, że nie mają dla nas ani czasu, ani dostępnej maszyny. Wzdychamy
z ulgą i wracamy do pracy. Mamy jeszcze do wprowadzenia pięć zmian, podobnych do tej
ostatniej. Każda z nich znajduje się w równie skomplikowanym obszarze. Poza tym nie je-
steśmy sami. Wiemy, że kilka innych osób także dokonuje zmian.
Następnego ranka dzwoni telefon. Danuta od testów mówi nam, że nocne testy AE1021
i AE1029 się nie powiodły. Nie jest pewna, czy to były nasze zmiany, ale dzwoni, bo wie,
że my już się tym zajmiemy. Zdebugujemy kod i sprawdzimy, czy niepowodzenia zostały
spowodowane przez jedną z naszych zmian, czy też przez zmiany kogoś innego.
Czy taka sytuacja wygląda na rzeczywistą? Niestety, jest ona jak najbardziej prawdziwa.
Spójrzmy na inny scenariusz.
Potrzebujemy wprowadzić zmiany w raczej długiej i skomplikowanej funkcji. Na
szczęście znaleźliśmy w miejscu do tego przeznaczonym zestaw testów jednostkowych.
Ludzie, którzy jako ostatni dotknęli tego kodu, napisali mniej więcej 20 testów jednost-
kowych, dokładnie sprawdzających jego działanie. Uruchamiamy je i okazuje się, że
wszystkie przechodzą. Następnie przyglądamy się testom, aby się zorientować, jakie wła-
ściwie jest zachowanie kodu.
Przygotowujemy się do wprowadzenia naszej zmiany, ale dociera do nas, że bardzo
trudno jest ustalić, jak to zrobić. Kod jest nieczytelny, a my naprawdę chcielibyśmy go
lepiej zrozumieć, zanim dokonamy w nim zmian. Testy nie wyłapią wszystkiego, tak
więc wolelibyśmy poprawić czytelność kodu, aby zyskać więcej pewności co do powodzenia
naszej zmiany. Poza tym nie chcemy, żebyśmy musieli sami albo żeby musiał ktokol-
wiek inny zadawać sobie tyle trudu, próbując go zrozumieć. Co za strata czasu!
Zaczynamy nieco faktoryzować kod. Wyciągamy niektóre metody i przesuwamy
wybraną logikę warunkową. Po każdej małej zmianie uruchamiamy niewielki zestaw
testów jednostkowych. Przechodzą one niemal za każdym razem, gdy je puszczamy. Kilka
minut temu popełniliśmy błąd i odwróciliśmy logikę w warunku, ale test nie powiódł się
i poprawienie błędu zajęło nam jakąś minutę. Po zakończeniu refaktoryzacji kod jest o wiele
czytelniejszy. Wprowadzamy zmianę, do której się przymierzaliśmy, i mamy pewność, że
się powiedzie. Aby zweryfikować nowe zachowanie, dodaliśmy kilka testów. Kolejni pro-
gramiści, którzy będą pracować z tym fragmentem kodu, będą mieć łatwiejsze zadanie,
a także będą dysponować testami, które kryją jego funkcjonalność.
Czy informacje zwrotne chcesz otrzymywać po minucie, czy może po nocy? Który
scenariusz jest efektywniejszy?
Testowanie jednostkowe jest jednym z najważniejszych składników przy pracy nad
cudzym kodem. Testy regresyjne na poziomie systemu są świetne, ale małe, lokalne testy
są bezcenne. Mogą one przekazywać Ci informacje wtedy, kiedy programujesz, i umożli-
wiają Ci dokonywanie o wiele bezpieczniejszej refaktoryzacji.
30 ROZDZIAŁ 2. PRACA Z INFORMACJĄ ZWROTNĄ

Co to jest testowanie jednostkowe?


Określenie test jednostkowy ma długą historię w dziedzinie rozwoju oprogramowania.
Wspólna dla wielu koncepcji testowania jednostkowego jest myśl, że są to testy prowa-
dzone w izolacji od poszczególnych elementów oprogramowania. Czym są te elementy?
Tutaj definicje mogą się różnić, ale podczas testowania jednostkowego jesteśmy zwykle
zainteresowani najbardziej podstawowymi jednostkami behawioralnymi systemu. W kodzie
proceduralnym jednostkami są często funkcje. W kodzie zorientowanym obiektowo
jednostkami będą klasy.

Jarzmo testowe
W książce tej używam wyrażenia jarzmo testowe na ogólne określenie kodu testującego, który
piszemy w celu sprawdzenia jakiegoś fragmentu oprogramowania, a także w odniesieniu
do kodu potrzebnego do jego uruchomienia. Aby pracować z naszym kodem, możemy ko-
rzystać z wielu różnych rodzajów jarzm testowych. W rozdziale 5., „Narzędzia”, omawiam
platformę testową xUnit oraz platformę FIT. Z obu z nich można korzystać w celu prze-
prowadzania testów, które opisuję w tej książce.

Czy w ogóle możemy przetestować tylko jedną funkcję albo jedną klasę? W systemach
proceduralnych testowanie funkcji w izolacji często jest trudne. Funkcje wysokiego po-
ziomu wywołują inne funkcje, które z kolei wywołują kolejne funkcje itd., aż do poziomu
maszynowego. W systemach zorientowanych obiektowo testowanie klas w izolacji jest
nieco łatwiejsze, niemniej faktem jest, że klasy zazwyczaj nie „żyją” w izolacji. Pomyśl
o wszystkich klasach, które kiedykolwiek napisałeś i które nie korzystały z innych klas.
Są bardzo rzadkie, prawda? Zwykle są to małe klasy danych lub klasy struktur danych,
takie jak stosy albo kolejki (ale nawet one mogą używać innych klas).
Testowanie w izolacji stanowi istotną część definicji testu jednostkowego, ale czy jest
ważne? W końcu do wielu błędów dochodzi, gdy fragmenty oprogramowania są inte-
growane. Czy duże testy, które obejmują swoim zakresem szeroką funkcjonalność kodu,
nie powinny być ważniejsze? Fakt — nie przeczę — są ważne, ale oto kilka problemów
z dużymi testami:
 Lokalizacja błędów — gdy testy oddalają się od elementu, który powinny sprawdzić,
stwierdzenie, co oznacza niepowodzenie testu, staje się trudniejsze. Dokładne
wskazanie źródła porażki testu często wymaga sporego nakładu pracy. Musisz
spojrzeć na dane wejściowe testu, przyjrzeć się niepowodzeniu i gdzieś na linii od
danych wejściowych do danych wyjściowych określić miejsce wystąpienia błędu.
Tak, to samo musisz zrobić w przypadku testów jednostkowych, tylko że często
zadanie to jest trywialne.
CO TO JEST TESTOWANIE JEDNOSTKOWE? 31

 Czas wykonywania — większe testy wykonują się dłużej. Sprawia to, że urucha-
mianie testów jest raczej frustrujące. Testy, które trwają zbyt długo, kończą w taki
sposób, że w ogóle nie są przeprowadzane.
 Pokrycie — trudno stwierdzić, czy istnieje związek między fragmentem kodu
a wartościami, które go testują. Zazwyczaj możemy sprawdzić, czy fragment kodu
jest obejmowany testem, korzystając z narzędzi pokrycia, ale gdy dodajemy nowy
kod, może nas czekać sporo pracy w celu utworzenia testów wysokiego poziomu,
które uwzględnią ten nowy kod.

Jedna z najbardziej frustrujących cech dużych testów polega na tym, że możemy lokalizować
błędy, gdy testy przeprowadzamy częściej, chociaż jest to bardzo trudne do osiągnięcia. Jeśli
uruchomimy testy, a one przejdą, po czym wprowadzimy niewielką zmianę i testy nie powiodą
się, to dokładnie wiemy, gdzie pojawił się problem. Przyczyną jest coś, co zrobiliśmy w związku
z tą małą zmianą. Możemy się z niej wycofać i spróbować ponownie. Jeśli jednak nasze testy są
obszerne, czas ich wykonywania może być zbyt długi; będziemy skłonni unikać uruchamiania
testów wystarczająco często, przez co nie będziemy mogli skutecznie lokalizować błędów.

Testy jednostkowe wypełniają lukę, którą pozostawiają po sobie duże testy. Możemy
testować fragmenty kodu niezależnie od siebie. Możemy grupować testy, dzięki czemu
mamy sposobność wykonywania niektórych testów w jednych warunkach, a innych
testów w innych warunkach. Za ich pomocą możemy szybko lokalizować błędy. Jeśli
sądzimy, że w pewnym fragmencie kodu znajduje się błąd, i mamy możliwość skorzysta-
nia z jarzma testowego, zwykle szybko możemy napisać kod testu i przekonać się, czy
rzeczywiście błąd wystąpił właśnie tam.
Oto cechy dobrych testów jednostkowych:
1. Wykonują się szybko.
2. Pomagają w lokalizowaniu problemów.
Ludzie z branży często nie mogą się zdecydować, czy określone testy są testami jed-
nostkowymi. Czy test, który korzysta z innej klasy produkcyjnej, naprawdę jest testem
jednostkowym? Powrócę do wymienionych powyżej dwóch cech: Czy testy wykonują się
szybko? Czy pomagają w krótkim czasie lokalizować błędy? Oczywiście istnieje tu pewna
ciągłość. Niektóre testy są większe i korzystają łącznie z wielu klas. W rzeczy samej, mogą
przypominać one nieco testy integracyjne. Same w sobie być może nie działają zbyt szybko,
ale co się stanie, gdy uruchomisz je wszystkie razem? Kiedy dysponujesz testem, który
sprawdza klasę łącznie z wieloma klasami współpracującymi, ma on tendencję do roz-
rastania się. Jeśli nie poświęciłeś czasu na umożliwienie odrębnego tworzenia instancji
klasy w jarzmie testowym, to czy Twoje zadanie będzie prostsze, kiedy dołożysz więcej
kodu? Nigdy nie będzie łatwiej. Ludzie zniechęcają się. Wraz z upływem czasu wykonanie
testu może zabierać nawet 1/10 sekundy.
32 ROZDZIAŁ 2. PRACA Z INFORMACJĄ ZWROTNĄ

Test jednostkowy, którego wykonanie trwa 1/10 sekundy, to wolny test.

Tak, mówię poważnie. W chwili, gdy piszę te słowa, 1/10 sekundy w przypadku testu
jednostkowego to cała wieczność. Policzmy. Jeśli masz projekt z 3000 klas, a na każdą
klasę przypada mniej więcej 10 testów, daje to razem 30 000 testów. Ile czasu zabierze
wykonanie wszystkich testów w projekcie, jeżeli jeden test trwa 1/10 sekundy? Prawie
godzinę. To długi czas oczekiwania na informację zwrotną. Nie masz 3000 klas? Skróć
uzyskany czas o połowę. Nadal mamy pół godziny. Z drugiej strony, co by się stało, gdyby
testy trwały po 1/100 sekundy? Teraz mówimy o łącznym czasie wynoszącym między
5 a 10 minut. Kiedy testy zajmują tyle czasu, upewniam się, że pracuję tylko z niektórymi
z nich, ale nie mam przy tym oporów przed uruchamianiem ich co kilka godzin.
Dzięki prawu Moore’a mam nadzieję na ujrzenie jeszcze za mojego życia testów,
które niemal błyskawicznie przekazują informacje zwrotne, nawet w przypadku najwięk-
szych systemów. Podejrzewam, że praca z takimi systemami będzie jak praca z kodem,
który potrafi się odgryzać — będzie on w stanie poinformować nas, kiedy został zmie-
niony w niepożądany sposób.

Testy jednostkowe działają szybko. Jeśli nie są szybkie, nie są testami jednostkowymi.
Inne testy często przybierają maskę testu jednostkowego. Test nie jest testem jednostkowym, jeżeli:
1. Komunikuje się z bazą danych.
2. Komunikuje się poprzez sieć.
3. Kontaktuje się z systemem plików.
4. Musisz zrobić w swoim środowisku coś specjalnego (np. edytować pliki konfiguracyjne),
aby go uruchomić.
Testy, które robią powyższe rzeczy, nie są złe. Często warto je napisać i w ogólności bę-
dziesz je tworzyć w jarzmie testowym. Ważne jednak jest, aby odróżnić je od rzeczywistych
testów jednostkowych, dzięki czemu będziesz dysponować zestawem testów, które można
szybko uruchomić za każdym razem, kiedy dokonasz zmian w kodzie.

Testy wyższego poziomu


Testy jednostkowe są świetne, ale istnieje także miejsce na testy wyższego poziomu, które
obejmują scenariusze oraz interakcje zachodzące w aplikacji. Testy wyższego poziomu
mogą być używane do jednoczesnego weryfikowania zachowania wielu klas. Jeśli jesteś
w stanie to zrealizować, często będzie Ci łatwiej pisać testy dla poszczególnych klas.
POKRYCIE TESTAMI 33

Pokrycie testami
Jak więc przystępujemy do wprowadzania zmian w cudzym projekcie? Pierwszą sprawą,
na którą warto zwrócić uwagę, jest to, że mając możliwość wyboru, zawsze bezpieczniej
będzie umieszczać testy wokół zmian, które wprowadzamy. Kiedy zmieniamy kod, mo-
żemy przemycić w nim błędy — w końcu jesteśmy tylko ludźmi. Jeśli jednak pokryjemy
kod testami, zanim go zmienimy, będziemy mieć większe szanse na wyłapanie pomyłek,
które możemy popełnić.
Rysunek 2.1 pokazuje niewielki zbiór klas. Chcemy wprowadzić zmiany w metodzie
getResponseText klasy InvoiceUpdateResponder oraz w metodzie getValue klasy Invoice.
Metody te to nasze miejsca zmian. Możemy je pokryć, pisząc testy dla klas, w których się
one znajdują.

Rysunek 2.1. Klasy aktualizujące fakturę

Aby pisać i uruchamiać testy, musimy mieć możliwość tworzenia instancji klas
InvoiceUpdateResponder oraz Invoice w jarzmie testowym. Czy możemy to robić?
Cóż, wygląda na to, że utworzenie instancji klasy Invoice powinno być w miarę proste —
ma ona konstruktor, który nie przyjmuje żadnych argumentów. Z InvoiceUpdateResponder
możemy mieć jednak problem. Jej argumentem jest DBConnection, rzeczywiste połą-
czenie z prawdziwą bazą danych. W jaki sposób obsłużymy tę klasę podczas testu? Czy
na jego potrzeby musimy konfigurować bazę danych? Przecież to masa pracy. Czy te-
stowanie z wykorzystaniem bazy danych nie będzie przebiegać powoli? W tym momencie
34 ROZDZIAŁ 2. PRACA Z INFORMACJĄ ZWROTNĄ

baza danych nieszczególnie nas interesuje, chcemy tylko pokryć testami nasze zmiany
w klasach InvoiceUpdateResponder oraz Invoice. Mamy także większy problem. Kon-
struktor w klasie InvoiceUpdateResponder potrzebuje jako argumentu obiektu klasy
InvoiceUpdateServlet. Czy utworzenie go będzie łatwe? Moglibyśmy zmienić kod tak,
żeby ten serwlet nie był już potrzebny. Jeżeli InvoiceUpdateResponder wymaga tylko
fragmentu informacji z obiektu klasy InvoiceUpdateServlet, moglibyśmy przekazać wy-
łącznie ten fragment zamiast całego serwletu, ale czy nie powinniśmy umieścić w od-
powiednim miejscu testu, aby upewnić się, że dokonaliśmy tej zmiany poprawnie?
Wszystkie te problemy wiążą się z zależnościami. Kiedy klasy bezpośrednio zależą od
elementów, których użycie w testach nie jest łatwe, wówczas trudno je modyfikować i pra-
cować z nimi.

Zależność jest jednym z najbardziej krytycznych problemów występujących podczas roz-


wijania oprogramowania. Większość pracy nad cudzym kodem wiąże się z usuwaniem za-
leżności, dzięki czemu wprowadzanie zmian będzie prostsze.

Jak więc możemy to zrobić? W jaki sposób umieszczamy testy na swoim miejscu bez
wprowadzania zmian w kodzie? Zła wiadomość jest taka, że w wielu przypadkach nie
będzie to zbyt praktyczne, a w niektórych będzie wręcz niemożliwe. W przykładzie, który
właśnie zobaczyliśmy, moglibyśmy spróbować ominąć problem z klasą DBConnection,
korzystając z rzeczywistej bazy danych, ale co z serwletem? Czy będziemy musieli utworzyć
cały serwlet i przekazać go do konstruktora w klasie InvoiceUpdateResponder? Czy uda
nam się doprowadzić go do właściwego stanu? Być może jest to możliwe. Co byśmy zrobili,
gdybyśmy pracowali w aplikacji z interfejsem graficznym? Moglibyśmy nie mieć do dyspo-
zycji żadnego interfejsu programistycznego. Logika mogłaby być wbudowana bezpo-
średnio w klasy interfejsu graficznego. Co robimy w takich przypadkach?

Dylemat cudzego kodu


Kiedy zmieniamy kod, powinniśmy umieścić w nim testy. Aby w kodzie umieścić testy,
często musimy zmienić kod.

W przykładzie z fakturą możemy próbować przeprowadzić testy na wyższym pozio-


mie. Jeśli napisanie testu bez wprowadzania zmian w określonej klasie jest trudne, często
prostsze może być przetestowanie klasy, z której ona korzysta — tak czy inaczej, zwykle
będziemy musieli usunąć w jakimś miejscu zależności między klasami. W tym przypadku
możemy zerwać zależność w klasie InvoiceUpdateServlet poprzez przekazanie jedynego
elementu, którego tak naprawdę wymaga klasa InvoiceUpdateResponder. Potrzebuje ona
zbioru identyfikatorów faktur, które przechowuje obiekt klasy InvoiceUpdaterServlet.
Możemy też usunąć zależność łączącą klasy InvoiceUpdateResponder i DBConnection,
wprowadzając interfejs (IDBConnection) i zmieniając klasę InvoiceUpdateResponder w taki
sposób, aby korzystała z tego interfejsu. Na rysunku 2.2 pokazano stan tych klas po wpro-
wadzeniu opisanych zmian.
POKRYCIE TESTAMI 35

Rysunek 2.2. Klasy aktualizujące fakturę z pousuwanymi zależnościami

Czy bezpieczne jest dokonywanie tych refaktoryzacji bez testów? Istotnie może tak
być. Refaktoryzacje te nazywają się odpowiednio upraszczaniem typu parametru (383)
i wydzielaniem interfejsu (361). Zostały one opisane w katalogu technik usuwania zależ-
ności zamieszczonym na końcu tej książki. Kiedy usuwamy zależności, często możemy
pisać testy, dzięki którym bardziej inwazyjne zmiany będą bezpieczniejsze. Cała sztuczka
polega na tym, aby przeprowadzać te wstępne refaktoryzacje bardzo ostrożnie.
Zachowanie ostrożności to właściwa postawa, jeśli z pewną dozą prawdopodobieństwa
możemy spowodować błędy, ale czasami — kiedy usuwamy zależności, aby pokryć
kod — sprawy nie przybierają tak dobrego obrotu, jak w poprzednim przykładzie. Być
może dodamy w metodach parametry, które nie są bezwzględnie potrzebne w kodzie pro-
dukcyjnym, albo na dziwne sposoby porozbijamy klasy tylko po to, aby w odpowiednich
miejscach porozmieszczać testy. Kiedy tak robimy, skutkiem może być częściowe pogor-
szenie wyglądu kodu w tym miejscu. Gdybyśmy tylko byli mniej ostrożni, zaraz byśmy
to naprawili. Możemy właśnie tak robić, ale to zależy od ryzyka, jakie się z tym wiąże.
Kiedy błędy mają duże znaczenie, a z reguły właśnie tak jest, ostrożność się opłaca.

Kiedy usuwasz zależności w cudzym kodzie, często musisz odłożyć na bok swoje poczucie
estetyki. Niektóre zależności dają się usuwać elegancko, inne pozostawiają po sobie miejsca
dalekie od ideału z punktu widzenia poprawności projektu. Przypominają one ślady po na-
cięciach wykonane podczas operacji — po tym jak zakończysz pracę, w Twoim kodzie mo-
że pozostać blizna, ale wszystko pod nią może stać się lepsze.
Jeśli później będziesz w stanie zakryć kod wokół miejsca, w którym usunąłeś zależności,
będziesz mógł zaleczyć także i tę bliznę.
36 ROZDZIAŁ 2. PRACA Z INFORMACJĄ ZWROTNĄ

Algorytm dokonywania zmian w cudzym kodzie


Kiedy musisz wprowadzić zmiany w cudzej bazie kodu, możesz skorzystać z następu-
jącego algorytmu:
1. Zidentyfikuj miejsca zmian.
2. Odszukaj miejsca na wstawienie testów.
3. Usuń zależności.
4. Napisz testy.
5. Wprowadź zmiany i dokonaj refaktoryzacji.
Celem codziennej pracy nad cudzym kodem jest wprowadzanie zmian, ale nie ja-
kichkolwiek zmian. Chcemy dokonywać modyfikacji funkcjonalnych, które wnoszą nową
jakość, i jednocześnie poddawać spore fragmenty systemu testom. Na koniec naszej pracy
programistycznej powinniśmy być w stanie wskazać nie tylko kod, który udostępnia
nowe funkcjonalności, ale także testy, które go weryfikują. Wraz z upływem czasu pod-
dane testom obszary bazy kodu zaczną wypływać na powierzchnię niczym wyspy wy-
nurzające się z oceanu. Praca na tych wyspach stanie się o wiele łatwiejsza. Z czasem
wyspy przerodzą się w ogromne obszary lądowe. Na koniec będziesz mógł pracować
na kontynentach kodu pokrytego testami.
Spójrzmy na każdy z tych etapów i przekonajmy się, jak książka ta pomoże Ci w ich
realizacji.

Zidentyfikuj miejsca zmian


Miejsca, w których musisz wprowadzić swoje zmiany, ściśle zależą od architektury Two-
jego systemu. Jeśli nie znasz swojego projektu wystarczająco dobrze, aby mieć pewność,
że dokonujesz zmian we właściwych miejscach, zajrzyj do rozdziału 16., „Nie rozumiem
wystarczająco dobrze kodu, żeby go zmienić”, oraz rozdziału 17., „Moja aplikacja nie ma
struktury”.

Odszukaj miejsca na wstawienie testów


W niektórych przypadkach znalezienie miejsc, w których można przeprowadzić testy,
jest łatwe, ale w przypadku cudzego kodu może to być trudne. Zajrzyj do rozdziału 11.,
„Muszę dokonać zmian. Które metody powinienem przetestować?”, oraz rozdziału 12.,
„Muszę dokonać wielu zmian w jednym miejscu. Czy powinienem pousuwać zależności
we wszystkich klasach, których te zmiany dotyczą?”. W rozdziałach tych opisałem tech-
niki, z których możesz korzystać, aby określić, czy musisz pisać testy do określonych
rodzajów zmian.
ALGORYTM DOKONYWANIA ZMIAN W CUDZYM KODZIE 37

Usuń zależności
Zależności często są najbardziej oczywistą przeszkodą podczas testowania. Dwa sposoby,
na które przeszkoda ta się przejawia, to trudność w tworzeniu instancji obiektów oraz
uruchamianiu metod w jarzmie testowym. Często musisz pousuwać zależności w cudzym
kodzie, aby rozlokować testy. W sytuacji idealnej mielibyśmy testy, które pokazywałyby
nam, czy to, co robimy w celu usunięcia zależności, samo nie jest przyczyną problemów,
ale zwykle tak nie jest. Zajrzyj do rozdziału 23., „Skąd mam wiedzieć, czy czegoś nie psuję?”,
gdzie poznasz niektóre z praktyk, z jakich możesz korzystać, aby pierwsze „nacięcia”
w systemie — których dokonujesz, rozpoczynając jego testowanie — były bezpieczniejsze.
Gdy już to zrobisz, przejdź do rozdziału 9., „Nie mogę umieścić tej klasy w jarzmie testo-
wym”, oraz rozdziału 10., „Nie mogę uruchomić tej metody w jarzmie testowym”, gdzie
znajdziesz scenariusze, pokazujące, jak sobie radzić z tymi powszechnymi problemami
dotyczącymi zależności. Rozdziały te są ściśle związane z katalogiem technik usuwania
zależności, który znajduje się na końcu tej książki, chociaż nie opisują one wszystkich
tych technik. Poświęć trochę czasu, żeby zapoznać się z tym katalogiem i kolejnymi pomy-
słami na usuwanie zależności.
Zależności wypływają na wierzch także wtedy, gdy mamy pomysł na test, ale nie
możemy go łatwo napisać. Jeśli okaże się, że nie możesz utworzyć testu z powodu zależ-
ności obecnych w dużych metodach, zajrzyj do rozdziału 22., „Muszę zmienić mon-
strualną metodę, a nie mogę napisać do niej testów”. Jeśli jednak możesz pousuwać za-
leżności, ale tworzenie testów pochłania zbyt wiele czasu, zapoznaj się z rozdziałem 7.,
„Dokonanie zmiany trwa całą wieczność”. W rozdziale tym opisano dodatkowe czyn-
ności związane z usuwaniem zależności, które możesz wykonać, aby przeciętny czas
poświęcany na budowanie był krótszy.

Napisz testy
Według mnie testy, które piszę do cudzego kodu, różnią się nieco od testów, które piszę
do nowego kodu. Zajrzyj do rozdziału 13., „Muszę dokonać zmian, ale nie wiem, jakie
testy napisać”, aby dowiedzieć się więcej na temat roli, jaką odgrywają testy w pracy
nad cudzym kodem.

Wprowadź zmiany i dokonaj refaktoryzacji


W celu dodawania funkcjonalności w cudzym kodzie polecam korzystanie z techniki
programowania sterowanego testami. Opis tej techniki oraz parę innych technik dodawa-
nia funkcjonalności znajduje się w rozdziale 8., „Jak mogę dodać funkcjonalność?”. Po
wprowadzeniu zmian w cudzym kodzie często już lepiej znamy występujące w nim pro-
blemy, a testy, które napisaliśmy w celu dodania funkcjonalności, zwykle zapewniają
pewien stopień pokrycia, umożliwiając nam przeprowadzenie częściowej refaktoryzacji.
Rozdział 20., „Ta klasa jest za duża, a ja nie chcę, żeby stała się jeszcze większa”, rozdział 22.,
38 ROZDZIAŁ 2. PRACA Z INFORMACJĄ ZWROTNĄ

„Muszę zmienić monstrualną metodę, a nie mogę napisać do niej testów”, oraz rozdział 21.,
„Wszędzie zmieniam ten sam kod”, opisują wiele technik, z których możesz korzystać,
aby nakierować swój cudzy kod w stronę lepszej struktury. Pamiętaj, że metody, jakie
opisuję w tych rozdziałach, to tylko „dziecięce kroki”. Nie pokazują one, jak sprawić, aby
Twój projekt stał się idealny, czysty czy też wypełniony wzorcami. Mnóstwo książek
opisuje, jak to zrobić, i zachęcam Cię, abyś skorzystał z tych technik, kiedy będziesz
miał ku temu okazję. Rozdziały te pokazują, jak sprawić, żeby Twój projekt był lepszy,
gdzie ta „lepszość” zależy od kontekstu i bardzo często sprowadza się po prostu do uzy-
skania projektu trochę łatwiejszego w konserwacji, niż to było przedtem. Nie lekceważ
jednak tej pracy. Często nawet najprostsza czynność, taka jak rozbicie dużej klasy tylko
po to, aby praca z nią była łatwiejsza, może spowodować ogromną różnicę w aplikacji,
wbrew temu, że sama zmiana była poniekąd mechaniczna.

Reszta tej książki


Reszta tej książki pokaże Ci, jak wprowadzać zmiany w cudzym kodzie. Następne dwa
rozdziały zawierają nieco materiału wprowadzającego na temat trzech istotnych koncepcji
związanych z pracą nad cudzym kodem, którymi są rozpoznanie, separowanie oraz spoiny.
Rozdział 3.

Rozpoznanie i separowanie

W idealnej sytuacji nie musielibyśmy robić z klasą nic specjalnego, żeby zacząć nad nią
pracować. W idealnym systemie moglibyśmy utworzyć obiekty dowolnej klasy, poddać je
testom, po czym rozpocząć pracę. Moglibyśmy tworzyć obiekty, pisać dla nich testy,
a następnie przechodzić do innych zadań. Gdyby to wszystko było takie łatwe, nie trzeba
by wcale o tym pisać, ale — niestety — często jest to trudne. Zależności istniejące między
klasami mogą spowodować, że przetestowanie określonego zbioru obiektów może być
bardzo skomplikowane. Chcielibyśmy utworzyć obiekt pewnej klasy, ale aby go otrzy-
mać, potrzebujemy obiektów innej klasy, a te z kolei wymagają obiektów jeszcze jednej
klasy itd. Wreszcie kończymy, poddając testom niemal cały system. W niektórych języ-
kach nie stanowi to problemu. W innych, a w szczególności w C++, sam czas konsoli-
dacji może praktycznie uniemożliwić szybkie przeprowadzenie testów, jeżeli nie usu-
niemy zależności.
W przypadku systemów, które nie były rozwijane równolegle z testami jednostko-
wymi, często musimy usuwać zależności w celu testowania klas, ale nie jest to jedyny
powód do eliminowania zależności. Zdarza się, że klasa, którą chcemy sprawdzić, wywiera
wpływ na inne klasy, a nasze testy powinny być na to przygotowane. Czasami możemy
rozpoznać takie efekty poprzez interfejs innej klasy; kiedy indziej jest to niemożliwe.
Jedyny wybór, jaki mamy, to odegrać rolę innej klasy, abyśmy mogli doświadczyć tych
efektów bezpośrednio.
W ogólności, kiedy chcemy porozmieszczać testy, mamy dwa powody do usuwania
zależności: rozpoznanie i separowanie.
1. Rozpoznanie — usuwamy zależności, aby rozpoznać, kiedy nie możemy uzyskać
dostępu do wartości obliczanych w naszym kodzie.
2. Separowanie — usuwamy zależności, aby je od siebie odseparować, kiedy nie mo-
żemy uruchomić w jarzmie testowym nawet fragmentu kodu
40 ROZDZIAŁ 3. ROZPOZNANIE I SEPAROWANIE

Oto przykład. Mamy klasę o nazwie NetworkBridge w aplikacji służącej do zarządzania


siecią:
{
public NetworkBridge(EndPoint [] endpoints) {
...
}
public void formRouting(String sourceID, String destID) {
...
}
...
}

NetworkBridge przyjmuje tablicę końcówek EndPoints i zarządza ich konfiguracją


za pomocą jakiegoś lokalnego sprzętu. Użytkownicy klasy NetworkBridge mogą korzystać
z jej metod, aby przekierowywać ruch sieciowy z jednej końcówki na inną. NetworkBridge
wykonuje tę pracę, zmieniając ustawienia zapisane w klasie EndPoint. Każda instancja
klasy EndPoint otwiera gniazdo sieciowe i komunikuje się poprzez sieć z określonym
urządzeniem.
To tylko krótki opis tego, czym zajmuje się klasa NetworkBridge. Moglibyśmy wdać się
w dokładniejsze szczegóły, ale z perspektywy testów już teraz widać kilka oczywistych
problemów. Gdybyśmy mieli napisać testy dla klasy NetworkBridge, jak byśmy to zrobili?
Klasa ta, kiedy jest konstruowana, może przecież odwoływać się do jakiegoś rzeczywistego
sprzętu. Czy musimy mieć dostęp do tego sprzętu, aby utworzyć jej instancję? Co gorsza,
skąd, u licha, mamy wiedzieć, co ona robi ze sprzętem albo z końcówkami? Z naszego
punktu widzenia klasa ta jest zamkniętą skrzynką.
Może jednak nie jest aż tak źle. Być może napiszemy jakiś kod podglądający pakiety
przesyłane siecią. Może uda nam się zdobyć sprzęt, z którym NetworkBridge będzie mógł
się komunikować, dzięki czemu przynajmniej nie zawiesi się, kiedy spróbujemy utworzyć
jej instancję. Może uda nam się skonfigurować sieć, co pozwoli nam uzyskać lokalny
klaster końcówek, który wykorzystamy w testach. Rozwiązania te mogłyby zadziałać, ale
wymagają mnóstwa pracy. Być może logika, jaką chcemy zmienić w klasie NetworkBridge,
wcale nie wymaga takich zabiegów; to tylko my nie mamy czego się uchwycić. Nie
możemy uruchomić obiektu tej klasy i bezpośrednio go wypróbować, aby przekonać się,
jak działa.
Przykład ten ilustruje problemy zarówno z rozpoznaniem, jak i separowaniem. Nie
możemy sprawdzić wpływu naszych odwołań do metod w tej klasie i nie możemy uru-
chomić jej niezależnie od reszty całej aplikacji.
Który problem jest trudniejszy — rozpoznanie czy separowanie? Na to pytanie nie ma
jednoznacznej odpowiedzi. Zwykle potrzebujemy obu tych zabiegów, a żeby je przepro-
wadzić, musimy usunąć zależności. Jedno jednak jest jasne: istnieje wiele sposobów na
dzielenie oprogramowania. W rzeczy samej, na końcu tej książki znajduje się cały katalog
technik, które temu służą, chociaż istnieje jedna, dominująca metoda służąca do roz-
poznania.
FAŁSZYWI WSPÓŁPRACOWNICY 41

Fałszywi współpracownicy
Jednym z największych problemów, z jakimi mamy do czynienia w cudzym kodzie, są
zależności. Jeśli chcemy uruchomić fragment kodu w oderwaniu od reszty programu
i sprawdzić, co on robi, często musimy usunąć zależności łączące go z innym kodem.
Prawie nigdy jednak nie jest to proste. Często ten inny kod jest jedynym miejscem, w któ-
rym łatwo możemy przekonać się o skutkach naszych działań. Jeśli damy radę zamienić
ten kod na inny i go przetestować, to będziemy mieć możliwość napisania własnych
testów. W programowaniu zorientowanym obiektowo takie fragmenty kodu często
nazywane są fałszywymi obiektami.

Fałszywe obiekty
Fałszywy obiekt to taki obiekt, który odgrywa rolę współpracownika Twojej klasy podczas
jej testowania. Oto przykład. W systemie obsługi sprzedaży mamy klasę o nazwie Sale
(patrz rysunek 3.1). W klasie tej znajduje się metoda scan(), przyjmująca kody kreskowe
towarów, które chce kupić klient. Przy każdym wywołaniu metody scan() obiekt klasy
Sale musi wyświetlić na kasie fiskalnej nazwę zeskanowanego towaru łącznie z jego ceną.

Rysunek 3.1. Klasa Sale

W jaki sposób możemy przeprowadzić testy, aby sprawdzić, czy na wyświetlaczu


kasy pojawia się poprawna informacja? Jeśli odwołania do API wyświetlacza kasy fiskal-
nej są ukryte głęboko w klasie Sale, będzie to trudne. Sprawdzenie efektu bezpośrednio
na wyświetlaczu może nie być łatwe. Jeśli jednak uda nam się znaleźć w kodzie miejsce,
w którym następuje aktualizacja wyświetlacza, będziemy mogli przejść do projektu poka-
zanego na rysunku 3.2.

Rysunek 3.2. Klasa Sale komunikująca się z klasą wyświetlacza

W miejscu tym powołaliśmy nową klasę — ArtR56Display. Klasa ta mieści cały kod
niezbędny do porozumiewania się z tym konkretnym urządzeniem, z którego będziemy
korzystać. Wszystko, co musimy zrobić, to dostarczyć jej wiersz z tekstem zawierającym
informację, którą chcemy wyświetlić. Możemy przenieść cały kod obsługi wyświetlacza
z klasy Sale do ArtR56Display i uzyskać system, który robi dokładnie to samo co wcze-
śniej. Czy dzięki temu zabiegowi posunęliśmy się do przodu? Tak — kiedy już to zrobimy,
możemy przejść do projektu pokazanego na rysunku 3.3.
42 ROZDZIAŁ 3. ROZPOZNANIE I SEPAROWANIE

Rysunek 3.3. Klasa Sale z hierarchią wyświetlaczy

Klasa Sale może teraz odwołać się do klasy ArtR56Display albo jakiejś innej — fałszywej
FakeDisplay. Przyjemną cechą posiadania do dyspozycji fałszywego wyświetlacza jest
możliwość pisania dla niego testów, aby dowiedzieć się, co robi klasa Sale.
Jak to działa? Ano tak, że klasa Sale przyjmuje wyświetlacz, a wyświetlacz jest obiektem
dowolnej klasy, która implementuje interfejs Display.
public interface Display
{
void showLine(String line);
}

Zarówno klasa ArtR56Display, jak i FakeDisplay implementują interfejs Display.


Obiekt klasy Sale może przyjąć wyświetlacz poprzez konstruktor i odwołać się do niego
wewnętrznie:
public class Sale
{
private Display display;
public Sale(Display display) {
this.display = display;
}
public void scan(String barcode) {
...
String itemLine = item.name()
+ " " + item.price().asDisplayText();
display.showLine(itemLine);
...
}
}

W metodzie scan kod wywołuje metodę showLine dla zmiennej display. Co się stanie,
zależy jednak od rodzaju wyświetlacza, jaki przekażemy obiektowi klasy Sale podczas jego
tworzenia. Jeśli będzie to ArtR56Display, obiekt spróbuje wyświetlić informację na praw-
dziwej kasie fiskalnej. Jeżeli będzie to FakeDisplay, obiekt nie podejmie takiej próby, ale
za to będziemy mogli zobaczyć, co zostałoby wyświetlone. Oto test, z którego możemy
skorzystać, aby się o tym przekonać:
FAŁSZYWI WSPÓŁPRACOWNICY 43

import junit.framework.*;
public class SaleTest extends TestCase
{
public void testDisplayAnItem() {
FakeDisplay display = new FakeDisplay();
Sale sale = new Sale(display);
sale.scan("1");
assertEquals("Mleko 2.49 zł", display.getLastLine());
}
}

Klasa FakeDisplay jest trochę osobliwa. Rzućmy na nią okiem:


public class FakeDisplay implements Display
{
private String lastLine = "";
public void showLine(String line) {
lastLine = line;
}
public String getLastLine() {
return lastLine;
}
}

Metoda showLine przyjmuje wiersz tekstu i przypisuje go zmiennej lastLine. Metoda


getLastLine zwraca ten wiersz za każdym razem, kiedy jest wywoływana. Jest to dość
ograniczone zachowanie, ale bardzo dla nas przydatne. Za pomocą tego testu — który
właśnie napisaliśmy — możemy dowiedzieć się, czy na wyświetlaczu zostanie pokazana
właściwa informacja, kiedy stosowana jest klasa Sale.

Fałszywe obiekty wspomagają prawdziwe testy


Czasami, gdy ktoś zobaczy użycie fałszywych obiektów, mówi, że tak naprawdę to nie jest
testowanie. W końcu taki test nie pokazuje, co faktycznie wyświetla się na rzeczywistym ekra-
nie. Przypuśćmy, że jakiś element oprogramowania wyświetlacza kasy fiskalnej nie działa
prawidłowo; w tym teście nigdy byśmy się o tym nie dowiedzieli. No cóż — to prawda, ale
nie oznacza to wcale, że taki test nie jest prawdziwy. Nawet gdyby udało nam się wymyślić
test rzeczywiście pokazujący, które dokładnie piksele zostały zapalone na prawdziwym wy-
świetlaczu kasy fiskalnej, czy oznaczałoby to, że program będzie działać z wszystkimi urzą-
dzeniami? Wcale nie; ale to też nie oznacza, że nie mamy do czynienia z prawdziwym testem.
Kiedy piszemy testy, musimy dzielić i zwyciężać. Przykładowy test pokazuje nam, jaki wpływ na
wyświetlacz wywiera klasa Sale, i to wszystko. Nie jest to jednak trywialne zadanie. Jeśli
odkryjemy jakiś błąd, uruchomienie tego testu może dopomóc nam w stwierdzeniu, że
problem nie leży po stronie klasy Sale. Jeżeli potrafimy korzystać z takich informacji w celu lo-
kalizowania błędów, uda nam się zaoszczędzić niesamowicie dużo czasu.
Kiedy piszemy testy dla poszczególnych modułów, otrzymujemy niewielkie i proste do ogarnięcia
jednostki. Dzięki temu analiza naszego kodu może być łatwiejsza.
44 ROZDZIAŁ 3. ROZPOZNANIE I SEPAROWANIE

Dwie strony fałszywego obiektu


Fałszywe obiekty mogą budzić Twoją dezorientację, kiedy widzisz je po raz pierwszy.
Jednym z najdziwniejszych ich aspektów jest to, że w pewnym sensie mają one dwie strony.
Jeszcze raz spójrzmy na klasę FakeDisplay, pokazaną na rysunku 3.4.

Rysunek 3.4. Dwie strony fałszywego obiektu

Metoda showLine jest potrzebna w klasie FakeDisplay, ponieważ klasa ta implementuje


interfejs Display. Jest to jedyna metoda interfejsu Display i jedyna metoda, którą będzie
widzieć klasa Sale. Druga metoda, getLastLine, służy na potrzeby testu. To właśnie dlate-
go typ zmiennej display deklarujemy jako FakeDisplay, a nie jako Display:
import junit.framework.*;
public class SaleTest extends TestCase
{
public void testDisplayAnItem() {
FakeDisplay display = new FakeDisplay();
Sale sale = new Sale(display);
sale.scan("1");
assertEquals("Mleko 2.49 zł", display.getLastLine());
}
}

Klasa Sale będzie widzieć fałszywy wyświetlacz jako obiekt Display, ale w testach mu-
simy trzymać się obiektu klasy FakeDisplay. Jeśli tego nie zrobimy, nie uda nam się wy-
wołać metody getLastLine(), aby sprawdzić, co się wyświetli.

Esencja fałszowania
Przykład, który przedstawiłem w tym rozdziale, jest bardzo prosty, ale pokazuje główną
myśl kryjącą się za tworzeniem takich fałszywek. Można je implementować na wiele róż-
nych sposobów. W językach zorientowanych obiektowo często pisze się proste klasy,
takie jak FakeDisplay z poprzedniego przykładu. W językach nieobiektowych możemy
implementować fałszywki, definiując alternatywne funkcje — takie, które odczytują
wartości pochodzące z jakichś globalnych struktur, do których mamy dostęp podczas te-
stów. Więcej szczegółów znajdziesz w rozdziale 19., „Mój projekt nie jest zorientowany
obiektowo. Jak mogę bezpiecznie wprowadzać zmiany?”.
FAŁSZYWI WSPÓŁPRACOWNICY 45

Obiekty pozorowane
Fałszywki są proste w pisaniu i stanowią bardzo cenne narzędzie przydatne w rozpozna-
niu. Jeśli musisz utworzyć wiele fałszywek, możesz wziąć pod uwagę bardziej zaawan-
sowany ich rodzaj — obiekty pozorowane. Obiekty pozorowane to takie fałszywki,
które przeprowadzają wewnętrzne asercje. Oto przykład testu, w którym użyto pozorowa-
nego obiektu:
import junit.framework.*;
public class SaleTest extends TestCase
{
public void testDisplayAnItem() {
MockDisplay display = new MockDisplay();
display.setExpectation("showLine", "Mleko 2.49 zł");
Sale sale = new Sale(display);
sale.scan("1");
display.verify();
}
}

W powyższym teście tworzymy pozorowany obiekt wyświetlacza. Przyjemna cecha


obiektów pozorowanych polega na tym, że możemy im wskazać, jakich wywołań powinny
oczekiwać, a następnie wydać polecenie sprawdzania, czy te wywołania otrzymują.
Dokładnie coś takiego dzieje się w powyższym teście. Mówimy obiektowi display, że po-
winien spodziewać się wywołania metody showLine z argumentem Mleko 2.49 zł. Po
zdefiniowaniu oczekiwania działamy dalej i korzystamy z obiektu — w tym przypadku
wywołujemy metodę scan(). Następnie wywołujemy metodę verify(), która sprawdza,
czy nasze oczekiwania zostały spełnione. Jeśli nie, test kończy się niepowodzeniem.
Obiekty pozorowane są wydajnym narzędziem; dostępny jest również szeroki asorty-
ment platform programistycznych je wspierających. Nie dla wszystkich jednak języków
takie platformy są osiągalne, niemniej w większości sytuacji wystarczą zwykłe fałszywe
obiekty.
46 ROZDZIAŁ 3. ROZPOZNANIE I SEPAROWANIE
Rozdział 4.

Model spoinowy

Jedną z rzeczy, którą zauważa prawie każdy, kto próbuje pisać testy do istniejącego
kodu, jest słabe przystosowanie tego kodu do testowania. Problem nie leży w określo-
nych programach ani językach. Z reguły języki programowania po prostu zdają się nie
wspierać zbyt dobrze testowania. Wygląda na to, że jedynym sposobem na uzyskanie
programów, które łatwo poddają się testowaniu, jest pisanie testów podczas ich opra-
cowywania lub poświęcenie pewnego czasu na stworzenie „projektu przyjaznego te-
stowaniu”. Pierwsze podejście wzbudza ogromne nadzieje, z kolei drugie podejście —
jak świadczy spora ilość istniejącego kodu —nie odniosło zbyt wielkiego sukcesu.
Zauważyłem, że kiedy próbuję poddawać kod testom, zaczynam myśleć o nim w nieco
inny sposób. Mógłbym uważać to za pewnego rodzaju osobiste dziwactwo, ale przeko-
nałem się, że ten inny sposób patrzenia na kod pomaga mi, gdy pracuję w nowych i nie-
znanych dla mnie językach programowania. Ponieważ nie byłbym w stanie uwzględnić
w tej książce każdego języka, zdecydowałem, że przedstawię w zarysie to moje spoj-
rzenie z nadzieją, że pomoże ono Tobie w takim samym stopniu, w jakim pomaga mi.

Ogromny arkusz z tekstem


Kiedy zaczynałem programować, miałem to szczęście, że rozpoczynałem na tyle późno,
aby dysponować własnym komputerem i kompilatorem, który mogłem na nim urucha-
miać. Wielu z moich przyjaciół zaczynało programować jeszcze w czasach kart perfo-
rowanych. Kiedy zdecydowałem się poznawać programowanie na studiach, zacząłem
pracę na terminalu w laboratorium. Mogliśmy zdalnie kompilować nasz kod na ma-
szynie DEC VAX. Na miejscu znajdował się niewielki system podliczający — każda
kompilacja kosztowała nas pieniądze, które były ściągane z naszego konta, a w każdym
semestrze mieliśmy do dyspozycji stałą ilość czasu maszynowego.
W owych czasach program był po prostu listingiem. Co kilka godzin przechodziłem
z laboratorium do pokoju z drukarką, brałem wydruk mojego programu i analizowałem
go, starając się określić, co jest w nim dobre, a co złe. Nie miałem zbyt wielkiej wiedzy,
48 ROZDZIAŁ 4. MODEL SPOINOWY

aby dbać o modułowość. Musieliśmy pisać kod modułowy, aby pokazać, że potrafimy
to robić, ale wówczas tak naprawdę o wiele bardziej interesowało mnie to, czy kod
dostarczy prawidłowe odpowiedzi. Kiedy zacząłem pisać kod zorientowany obiektowo,
modułowość była już raczej zagadnieniem akademickim. W trakcie studiów nie miałem
też zamiaru zamieniać jednych zajęć na inne. Kiedy zacząłem pracować w branży, zaczą-
łem bardzo dbać o te sprawy, ale na studiach program był dla mnie tylko listingiem —
długim zbiorem funkcji, które musiałem po kolei napisać i zrozumieć.
Postrzeganie programu jako listingu wydaje się trafne, przynajmniej jeśli spojrzymy
na to, jak ludzie zachowują się w odniesieniu do programów, które piszą. Jeśli w ogóle nie
wiedzielibyśmy, czym jest programowanie, i zobaczylibyśmy pokój pełen pracujących
programistów, moglibyśmy pomyśleć, że są oni badaczami, którzy studiują i edytują
ważne dokumenty. Program może wyglądać jak wielki arkusz z tekstem. Modyfikacja jego
małego fragmentu może spowodować zmianę znaczenia całego dokumentu, tak więc
zmiany te są wprowadzane ostrożnie, aby uniknąć pomyłek.
Powierzchownie wszystko to jest prawdą, ale co z modułowością? Często mówi się
nam, że lepiej pisać programy, które składają się z małych fragmentów wielokrotnego
użycia, ale jak często te małe fragmenty są używane niezależnie od siebie? Niezbyt często.
Wielokrotne użycie jest trudne. Nawet gdy elementy oprogramowania wydają się nieza-
leżne, często są ze sobą wzajemnie powiązane na różne subtelne sposoby.

Spoiny
Kiedy próbujesz wyodrębnić pojedyncze klasy na potrzeby przeprowadzenia testów jed-
nostkowych, często musisz pousuwać wiele zależności. Co ciekawe, zwykle masz z tym
mnóstwo roboty, niezależnie od tego, jak „dobry” jest projekt. Wyciąganie klas z istniejących
programów w celu ich testowania naprawdę może zmienić Twój pogląd na znaczenie słowa
„dobry” w odniesieniu do projektu. Skłoni Cię także do myślenia o programach w zupełnie
inny sposób. Idea, że program jest kartką zapisaną tekstem, już nie przejdzie. Jak zatem
powinniśmy go postrzegać? Spójrzmy na przykład, którym jest funkcja w języku C++.
bool CAsyncSslRec::Init()
{
if (m_bSslInitialized) {
return true;
}
m_smutex.Unlock();
m_nSslRefCount++;

m_bSslInitialized = true;

FreeLibrary(m_hSslDll1);
m_hSslDll1=0;
FreeLibrary(m_hSslDll2);
m_hSslDll2=0;
SPOINY 49

if (!m_bFailureSent) {
m_bFailureSent=TRUE;
PostReceiveError(SOCKETCALLBACK, SSL_FAILURE);
}

CreateLibrary(m_hSslDll1,"syncesel1.dll");
CreateLibrary(m_hSslDll2,"syncesel2.dll");

m_hSslDll1->Init();
m_hSslDll2->Init();

return true;
}

Bez wątpienia wygląda to jak kartka zapisana tekstem. Czyż nie? Przypuśćmy, że
chcemy uruchomić całą tę metodę z wyjątkiem poniższego wiersza:
PostReceiveError(SOCKETCALLBACK, SSL_FAILURE);

Jak się do tego zabrać?


To będzie łatwe, prawda? Wszystko, co musimy zrobić, to przejść do kodu i ska-
sować ten wiersz.
No dobra, doprecyzujmy nasz problem nieco bardziej. Chcemy uniknąć wykonywania
się tego wiersza kodu, gdyż PostReceiveError jest globalną funkcją, komunikującą
się z innym podsystemem, a praca z tym podsystemem w ramach testu jest piekielnie
trudna. Tak więc problem sprowadza się do tego, jak można uruchomić naszą metodę bez
wywoływania funkcji PostReceiveError w czasie testu. W jaki sposób możemy to osiągnąć
i jednocześnie pozostawić wywołanie funkcji PostReceiveError w wersji produkcyjnej?
Dla mnie pytanie to ma wiele możliwych odpowiedzi i prowadzi do idei spoiny.
Oto definicja spoiny. Spójrzmy na nią, a następnie zapoznajmy się z kilkoma jej
przykładami.

Spoina
Spoina jest miejscem, w którym można zmienić zachowanie programu bez potrzeby edytowania
tego miejsca.

Czy w miejscu wywołania funkcji PostReceiveError znajduje się spoina? Tak. Możemy
pozbyć się niechcianego zachowania na kilka różnych sposobów. Oto jeden z najbar-
dziej bezpośrednich. PostReceiveError to funkcja globalna; nie jest ona częścią klasy
CAsyncSslRec. Co się stanie, jeśli w klasie tej dodamy metodę z dokładnie taką samą sy-
gnaturą, jaką ma PostReceiveError?
class CAsyncSslRec
{
...
virtual void PostReceiveError(UINT type, UINT errorcode);
...
};
50 ROZDZIAŁ 4. MODEL SPOINOWY

W pliku implementacyjnym możemy dodać dla niej takie ciało:


void CAsyncSslRec::PostReceiveError(UINT type, UINT errorcode)
{
::PostReceiveError(type, errorcode);
}

Taka zamiana powinna umożliwić pozostawienie zachowania. Korzystamy z tej nowej


metody, aby za pomocą operatora zasięgu języka C++ (::) odwołać się do globalnej
funkcji PostReceiveError. Mamy tu pewien brak bezpośredniości, ale w rezultacie wywo-
łujemy tę samą funkcję globalną.
No dobra, a co jeśli utworzymy podklasę klasy CAsyncSslRec i przesłonimy metodę
PostReceiveError?
class TestingAsyncSslRec : public CAsyncSslRec
{
virtual void PostReceiveError(UINT type, UINT errorcode)
{
}
};

Jeśli tak postąpimy, po czym powrócimy do miejsca, gdzie tworzymy naszą klasę
CAsyncSslRec, i zamiast niej utworzymy TestingAsyncSslRec, skutecznie uda nam się wy-
eliminować w tym kodzie zachowanie związane z wywołaniem metody PostReceiveError:
bool CAsyncSslRec::Init()
{
if (m_bSslInitialized) {
return true;
}
m_smutex.Unlock();
m_nSslRefCount++;

m_bSslInitialized = true;

FreeLibrary(m_hSslDll1);
m_hSslDll1=0;
FreeLibrary(m_hSslDll2);
m_hSslDll2=0;

if (!m_bFailureSent) {
m_bFailureSent=TRUE;
PostReceiveError(SOCKETCALLBACK, SSL_FAILURE);
}

CreateLibrary(m_hSslDll1,"syncesel1.dll");
CreateLibrary(m_hSslDll2,"syncesel2.dll");

m_hSslDll1->Init();
m_hSslDll2->Init();

return true;
}
RODZAJE SPOIN 51

Teraz dla powyższego kodu możemy pisać testy niepowodujące niepożądanych skut-
ków ubocznych.
Taką spoinę nazywam spoiną obiektową. Możemy zmieniać wywoływaną metodę bez
zmieniania metody, która ją wywołuje. Spoiny obiektowe są dostępne w językach zorien-
towanych obiektowo i stanowią zaledwie jeden z wielu rodzajów spoin.
Dlaczego spoiny? W jakich zastosowaniach sprawdza się ten pomysł?
Jednym z największych wyzwań napotykanych w czasie testowania cudzego kodu jest
usuwanie zależności. Gdy mamy szczęście, zależności są małe i lokalne, ale w przypadkach
patologicznych są one liczne i rozsiane po całej bazie kodu. Spojrzenie na program po-
przez spoiny pomaga nam zauważyć okazje istniejące już w kodzie. Jeżeli uda nam się
zastąpić zachowanie w miejscu spoiny, będziemy mogli w naszych testach selektywnie
wyłączać zależności. Gdy zechcemy rozpoznać w kodzie instrukcje warunkowe i napisać
dla nich testy, będziemy mogli uruchomić inny kod w miejscach, w których zależności te
występowały. Często praca ta pomoże nam rozmieścić akurat tyle testów, ile potrzeba,
abyśmy mieli wsparcie przy agresywniejszych działaniach.

Rodzaje spoin
Rodzaje dostępnych testów różnią się w zależności od języków programowania. Najlep-
szym sposobem na ich poznanie jest przyjrzenie się kolejnym etapom przekształcania
tekstu programu w kod działający na komputerze. Każdy możliwy do zidentyfikowania
etap ujawnia inne rodzaje spoin.

Spoiny preprocesowe
W większości środowisk programistycznych tekst programu jest odczytywany przez
kompilator. W następnej kolejności kompilator produkuje kod obiektowy albo instrukcje
w kodzie bajtowym. W zależności od języka mogą jeszcze występować kolejne etapy
przetwarzania, ale co z etapami wcześniejszymi?
Zaledwie w kilku językach przed kompilacją występuje jeszcze etap budowania. C i C++
należą wśród nich do najpopularniejszych.
W C i C++ przed kompilatorem uruchamiany jest preprocesor makr. Przez całe lata
preprocesor był nieustannie przeklinany i wyszydzany. Dzięki niemu możemy wziąć
wiersze tekstu, które wyglądają równie niewinnie, jak poniżej:
TEST(getBalance,Account)
{
Account account;
LONGS_EQUAL(0, account.getBalance());
}

I sprawić, że dla kompilatora będą wyglądać tak:


class AccountgetBalanceTest : public Test
52 ROZDZIAŁ 4. MODEL SPOINOWY

{ public: AccountgetBalanceTest () : Test ("getBalance" "Test") {}


void run (TestResult& result_); }
AccountgetBalanceInstance;
void AccountgetBalanceTest::run (TestResult& result_)
{
Account account;
{ result_.countCheck();
long actualTemp = (account.getBalance());
long expectedTemp = (0);
if ((expectedTemp) != (actualTemp))
{ result_.addFailure (Failure (name_, "c:\\seamexample.cpp", 24,
StringFrom(expectedTemp),
StringFrom(actualTemp))); return; } }
}
Możemy też zagnieżdżać kod w warunkowych instrukcjach kompilacji — jak poniżej
— aby wspierać debugowanie i różne platformy (aaaaaa!):
...
m_pRtg->Adj(2.0);

#ifdef DEBUG
#ifndef WINDOWS
{ FILE *fp = fopen(TGLOGNAME,"w");
if (fp) { fprintf(fp,"%s", m_pRtg->pszState); fclose(fp); }}
#endif

m_pTSRTable->p_nFlush |= GF_FLOT;
#endif
...

Używanie rozbudowanego preprocesowania w kodzie produkcyjnym nie jest dobrym


pomysłem, ponieważ prowadzi do zmniejszenia czytelności kodu. Dyrektywy kompilacji
warunkowej (#ifdef, #ifndef, #if itd.) w zasadzie zmuszają Cię do utrzymywania
wielu różnych programów w tym samym kodzie źródłowym. Makra (definiowane dy-
rektywą #define) mogą być używane do robienia rozmaitych pożytecznych rzeczy, ale
tak naprawdę realizują one tylko prostą zamianę tekstu. Bardzo łatwo utworzyć makra,
w których ukrywają się okropnie pogmatwane błędy.
Ale odsuńmy na bok te zastrzeżenia. Tak naprawdę to cieszę się, że C i C++ mają pre-
procesory, bo dzięki temu mamy do dyspozycji więcej spoin. Oto przykład. W programie
napisanym w C istnieją zależności w procedurze bibliotecznej o nazwie db_update. Funkcja
db_update porozumiewa się bezpośrednio z bazą danych. Dopóki nie podstawimy innej
implementacji tej procedury, nie będziemy mogli rozpoznać zachowania tej funkcji.
#include <DFHLItem.h>
#include <DHLSRecord.h>
extern int db_update(int, struct DFHLItem *);
void account_update(
int account_no, struct DHLSRecord *record, int activated)
{
if (activated) {
if (record->dateStamped && record->quantity > MAX_ITEMS) {
db_update(account_no, record->item);
RODZAJE SPOIN 53

} else {
db_update(account_no, record->backup_item);
}
}
db_update(MASTER_ACCOUNT, record->item);
}

Możemy użyć spoin preprocesowych, aby zastąpić odwołania do funkcji db_update.


W tym celu dołączymy plik nagłówkowy o nazwie localdefs.h.
#include <DFHLItem.h>
#include <DHLSRecord.h>

extern int db_update(int, struct DFHLItem *);

#include "localdefs.h"

void account_update(
int account_no, struct DHLSRecord *record, int activated)
{
if (activated) {
if (record->dateStamped && record->quantity > MAX_ITEMS) {
db_update(account_no, record->item);
} else {
db_update(account_no, record->backup_item);
}
}
db_update(MASTER_ACCOUNT, record->item);
}

Wewnątrz niego możemy udostępnić definicję procedury db_update oraz kilku zmien-
nych, które będą dla nas przydatne:
#ifdef TESTING
...
struct DFHLItem *last_item = NULL;
int last_account_no = -1;
#define db_update(account_no,item)\
{last_item = (item); last_account_no = (account_no);}
...
#endif

Gdy procedura zastępująca db_update znajdzie się już na swoim miejscu, możemy
napisać testy sprawdzające, czy została ona wywołana z właściwymi parametrami. Można
to zrobić, ponieważ dyrektywa #include preprocesora C udostępnia spoinę, z której
możemy skorzystać, aby zastępować teksty jeszcze przed kompilacją.
Spoiny preprocesowe mają całkiem spore możliwości. Nie wydaje mi się jednak, żebym
tak naprawdę potrzebował preprocesora w Javie czy też w innych nowoczesnych językach,
niemniej całkiem miło dysponować takim narzędziem w C i C++ jako rekompensatą za
niektóre z utrudnień w testowaniu, jakie są obecne w tych językach.
Nie wspominałem o tym wcześniej, ale jest jeszcze coś ważnego do zrozumienia, jeśli
chodzi o spoiny: każda spoina ma punkt dostępowy. Spójrzmy jeszcze raz na definicję
spoiny:
54 ROZDZIAŁ 4. MODEL SPOINOWY

Spoina
Spoina jest miejscem, w którym można zmienić zachowanie programu bez potrzeby edy-
towania tego miejsca.

Kiedy masz spoinę, dysponujesz tym samym miejscem, w którym możesz zmieniać
zachowanie programu. Tak naprawdę nie możemy jednak przejść do tego miejsca i zmie-
nić kodu tylko po to, aby go przetestować. Kod źródłowy powinien być taki sam zarówno
w wersji produkcyjnej, jak i testowej. We wcześniejszym przykładzie chcieliśmy zmienić
zachowanie w miejscu wywołania procedury db_update. Aby skorzystać z tej spoiny,
musiałeś wprowadzić zmiany w innym miejscu kodu. W tym przypadku punktem dostę-
powym jest zdefiniowana dla preprocesora nazwa TESTING. Kiedy nazwa TESTING jest zdefi-
niowana, plik localdefs.h definiuje makra zastępujące wywołania procedury db_update
w kodzie źródłowym.

Punkt dostępowy
Każda spoina ma punkt dostępowy — miejsce, w którym możesz podjąć decyzję dotyczącą
wyboru jakiegoś bądź też innego zachowania.

Spoiny konsolidacyjne
W wielu językach kompilacja nie jest ostatnim etapem procesu budowy. Kompilator tworzy
pośrednią reprezentację kodu, która zawiera wywołania kodu znajdującego się w innych
plikach. Reprezentacje te są scalane przez konsolidatory. Analizują one te wywołania, abyś
mógł otrzymać pełny program w wersji zdatnej do uruchomienia.
W językach takich jak C albo C++ istnieje odrębny konsolidator, który przeprowadza
opisane operacje. W Javie i podobnych językach kompilator przeprowadza konsolidację
za kulisami. Kiedy plik źródłowy zawiera polecenie import, kompilator sprawdza, czy im-
portowana klasa została już skompilowana. Jeśli nie, kompiluje ją i w razie konieczności
sprawdza, czy wszystkie jej wywołania zostaną poprawnie przeprowadzone w czasie
działania programu.
Niezależnie od schematu, według którego Twój język przetwarza odwołania, zwykle
będziesz mógł z niego korzystać, aby zamieniać fragmenty programu. Spójrzmy na przy-
padek w języka Java. Oto niewielka klasa o nazwie FitFilter:
package fitnesse;

import fit.Parse;
import fit.Fixture;

import java.io.*;
import java.util.Date;

import java.io.*;
import java.util.*;
RODZAJE SPOIN 55

public class FitFilter {

public String input;


public Parse tables;
public Fixture fixture = new Fixture();
public PrintWriter output;

public static void main (String argv[]) {


new FitFilter().run(argv);
}

public void run (String argv[]) {


args(argv);
process();
exit();
}

public void process() {


try {
tables = new Parse(input);
fixture.doTables(tables);
} catch (Exception e) {
exception(e);
}
tables.print(output);
}
...
}

W pliku tym importujemy klasy fit.Parse i fit.Fixture. W jaki sposób kompilator


i JVM je znajdują? W Javie możesz użyć zmiennej środowiskowej classpath, aby określić,
gdzie system Javy ma szukać tych klas. Możesz nawet tworzyć klasy o tych samych na-
zwach, umieszczać je w różnych ścieżkach i zmieniać wartość zmiennej classpath, aby
odwoływać się do różnych klas fit.Parse i fit.Fixture. Chociaż korzystanie z tej sztuczki
prowadziłoby do zamieszania w przypadku kodu produkcyjnego, to podczas testów
będzie ona dość wygodną metodą usuwania zależności.

Przypuśćmy, że na potrzeby testów chcemy podstawić inną wersję klasy Parse. Gdzie wów-
czas znalazłaby się spoina?
Spoiną będzie wywołanie new Parse w metodzie process.
Gdzie jest punkt dostępowy?
Punktem dostępowym jest zmienna classpath.

Taki rodzaj dynamicznej konsolidacji można realizować w wielu językach. W większo-


ści z nich istnieje sposób na wykorzystanie spoin konsolidacyjnych. Nie każda konso-
lidacja jest jednak dynamiczna. W wielu starszych językach prawie cały proces konso-
lidacji jest statyczny — przebiega po kompilacji.
56 ROZDZIAŁ 4. MODEL SPOINOWY

Wiele systemów konsolidujących w językach C i C++ w celu uzyskania wykonywal-


nych programów przeprowadza konsolidację statyczną. Często najprostszym sposobem
na użycie spoiny konsolidacyjnej jest utworzenie odrębnej biblioteki dla każdej z klas lub
funkcji, które chcesz zamienić. Kiedy to zrobisz, zawsze będziesz mógł podczas testowania
modyfikować swoje skrypty budujące, aby wskazywały właśnie na nie, zamiast na funkcje
produkcyjne. Taki zabieg może być nieco pracochłonny, ale dysponowanie bazą kodu
usianą odwołaniami do zewnętrznych bibliotek może się opłacać. Wyobraź sobie na
przykład aplikację CAD zawierającą mnóstwo odwołań do biblioteki graficznej. Oto
przykład typowego kodu:
void CrossPlaneFigure::rerender()
{
// narysuj etykietę
drawText(m_nX, m_nY, m_pchLabel, getClipLen());
drawLine(m_nX, m_nY, m_nX + getClipLen(), m_nY);
drawLine(m_nX, m_nY, m_nX, m_nY + getDropLen());
if (!m_bShadowBox) {
drawLine(m_nX + getClipLen(), m_nY,
m_nX + getClipLen(), m_nY + getDropLen());
drawLine(m_nX, m_nY + getDropLen(),
m_nX + getClipLen(), m_nY + getDropLen());
}

// narysuj figurę
for (int n = 0; n < edges.size(); n++) {
...
}
...
}

Kod ten zawiera wiele bezpośrednich odwołań do biblioteki graficznej. Niestety, jedy-
nym sposobem na rzeczywiste sprawdzenie tego, czy program robi to, co powinien robić,
jest spojrzenie na ekran komputera, kiedy rysowane są figury. W przypadku skompliko-
wanego kodu metoda ta jest bardzo podatna na błędy, nie wspominając już o jej pra-
cochłonności. Alternatywą jest użycie spoin konsolidacyjnych. Jeśli wszystkie funkcje
rysujące stanowią część określonej biblioteki, możesz utworzyć jej okrojoną wersję, do
której będzie odwoływać się reszta aplikacji. Jeśli jesteś zainteresowany wyłącznie od-
separowaniem zależności, funkcje będą mogły być puste:
void drawText(int x, int y, char *text, int textLength)
{
}

void drawLine(int firstX, int firstY, int secondX, int secondY)


{
}

Jeżeli funkcje zwracają wartości, także będziesz musiał coś zwrócić. Często dobrym
wyborem jest kod wskazujący domyślną wartość pewnego typu albo na sukces:
RODZAJE SPOIN 57

int getStatus()
{
return FLAG_OKAY;
}

Przypadek biblioteki graficznej jest trochę nietypowy. Powodem, dla którego jest ona
dobrym kandydatem do zastosowania tej techniki, jest to, że stanowi ona przykład
niemal czystego interfejsu „słuchającego”. Wywołujesz funkcje, aby im powiedzieć, że mają
coś zrobić, i nie domagasz się przy tym żadnej odpowiedzi z informacją. Pytanie o infor-
mację jest trudne, ponieważ zwracanie wartości domyślnych często nie jest dobrym wybo-
rem, kiedy próbujesz sprawdzać swój kod.
Separowanie często jest dobrym powodem do użycia spoiny konsolidacyjnej. Możesz
także przeprowadzać rozpoznanie, co wymaga tylko nieco więcej pracy. W przypadku
biblioteki graficznej, którą właśnie podrobiliśmy, można wprowadzić kilka dodatkowych
struktur danych, rejestrujących wywołania:
std::queue<GraphicsAction> actions;

void drawLine(int firstX, int firstY, int secondX, int secondY)


{
actions.push_back(GraphicsAction(LINE_DRAW,
firstX, firstY, secondX, secondY);
}

Za pomocą takich struktur danych możemy rozpoznawać w teście skutki wywoływania


funkcji:
TEST(simpleRender,Figure)
{
std::string text = "prosty";
Figure figure(text, 0, 0);

figure.rerender();
LONGS_EQUAL(5, actions.size());
GraphicsAction action;
action = actions.pop_front();
LONGS_EQUAL(LABEL_DRAW, action.type);

action = actions.pop_front();
LONGS_EQUAL(0, action.firstX);
LONGS_EQUAL(0, action.firstY);
LONGS_EQUAL(text.size(), action.secondX);
}

Schematy, których możemy używać do rozpoznania skutków, mogą być dość złożone,
ale najlepiej jest rozpocząć od bardzo prostego i pozwolić mu, aby stał się na tyle skompli-
kowany, na ile wymaga tego rozwiązanie bieżących potrzeb w tym obszarze.
Punkt dostępowy spoiny konsolidacyjnej zawsze występuje poza kodem programu.
Czasami znajduje się w skrypcie budującym lub wdrożeniowym. Z tego powodu użycie
spoiny może być nieco trudne do zauważenia.
58 ROZDZIAŁ 4. MODEL SPOINOWY

Wskazówka na temat użycia


Kiedy używasz spoin konsolidacyjnych, upewnij się, że różnica między środowiskiem te-
stowym a produkcyjnym jest oczywista.

Spoiny obiektowe
Spoiny obiektowe są prawdopodobnie najbardziej użytecznym rodzajem spoin dostępnych
w językach programowania zorientowanych obiektowo. Najważniejsza rzecz, z jakiej
należy zdać sobie sprawę, polega na tym, że gdy przyjrzymy się wywołaniu metody
w programie zorientowanym obiektowo, zauważymy, że nie definiuje ono metody, która
faktycznie zostanie wywołana. Spójrzmy na przykładowe wywołanie w Javie:
cell.Recalculate();

Gdy widzimy ten kod, wydaje się, że gdzieś musi być metoda o nazwie Recalculate,
która wykona się, kiedy nastąpi moment jej wywołania. Jeżeli program ma zostać uru-
chomiony, metoda o takiej nazwie musi istnieć, ale tak naprawdę może być więcej metod
z taką nazwą (patrz rysunek 4.1):

Rysunek 4.1. Hierarchia obiektów Cell

Która metoda zostanie wywołana w poniższej linii kodu?


cell.Recalculate();

Nie wiedząc, na co wskazuje obiekt cell, po prostu tego nie stwierdzimy. Może to być
metoda Recalculate obiektu ValueCell albo metoda Recalculate obiektu FormulaCell.
Może to nawet być metoda Recalculate jakiejś innej klasy, która nie dziedziczy po klasie
Cell. Jeżeli możemy decydować, która metoda Recalculate zostanie wywołana w danej li-
nii kodu bez wprowadzania zmian w okolicy tego kodu, to mamy do czynienia ze spoiną.
W językach zorientowanych obiektowo nie wszystkie metody są spoinami. Oto przy-
kład wywołania, które nie jest spoiną:
public class CustomSpreadsheet extends Spreadsheet
{
public Spreadsheet buildMartSheet() {
...
Cell cell = new FormulaCell(this, "A1", "=A2+A3");
...
RODZAJE SPOIN 59

cell.Recalculate();
...
}
...
}

W kodzie tym tworzymy obiekt klasy Cell, a następnie używamy go w tej samej
metodzie. Czy wywołanie metody Recalculate jest spoiną obiektową? Nie. Nie ma tu
punktu dostępowego. Nie możemy zdecydować, która metoda Recalculate zostanie
wywołana, ponieważ wybór zależy od klasy obiektu cell. Klasa ta została określona w chwili
tworzenia obiektu, a nie możemy jej zmienić bez modyfikowania metody.
A co, gdyby kod wyglądał tak:
public class CustomSpreadsheet extends Spreadsheet
{
public Spreadsheet buildMartSheet(Cell cell) {
...
cell.Recalculate();
...
}
...
}

Czy teraz wywołanie cell.Recalculate w buildMartSheet jest spoiną? Tak. Możemy


utworzyć obiekt CustomSpreadsheet podczas testu i funkcję buildMartSheet z dowolnym
obiektem Cell, jakiego tylko chcemy użyć. Udało nam się zróżnicować to, co wykonuje
wywołanie cell.Recalculate bez zmieniania metody, która to wywołanie zawiera.
Gdzie jest punkt dostępowy?
W przykładzie tym punktem dostępowym jest lista argumentów funkcji buildMartSheet.
Możemy decydować, jaki rodzaj obiektu przekazać, i zmieniać zachowanie metody
Recalculate w dowolny sposób, jaki tylko będzie nam potrzebny w testowaniu.
No dobra, spoiny obiektowe są w większości dość proste, ale oto i spoina podchwytliwa.
Czy wywołanie metody Recalculate w tej wersji funkcji buildMartSheet zawiera spoinę?
public class CustomSpreadsheet extends Spreadsheet
{
public Spreadsheet buildMartSheet(Cell cell) {
...
Recalculate(cell);
...
}

private static void Recalculate(Cell cell) {


...
}
...
}
60 ROZDZIAŁ 4. MODEL SPOINOWY

Metoda Recalculate jest statyczna. Czy wywołanie metody Recalculate w funkcji


buildMartSheet jest spoiną? Tak. Nie musimy edytować metody buildMartSheet, aby
zmienić zachowanie programu podczas tego wywołania. Jeśli usuniemy słowo kluczowe
static przy metodzie Recalculate i z metody prywatnej zmienimy ją w chronioną, bę-
dziemy mogli tworzyć jej podklasy i przesłaniać ją podczas testów:
public class CustomSpreadsheet extends Spreadsheet
{
public Spreadsheet buildMartSheet(Cell cell) {
...
Recalculate(cell);
...
}
protected void Recalculate(Cell cell) {
...
}
...
}

public class TestingCustomSpreadsheet extends CustomSpreadsheet {


protected void Recalculate(Cell cell) {
...
}
}

Czy nie zrobiliśmy tego wszystkiego w pewnym sensie „naokoło”? Jeśli nie chcemy mieć
zależności, dlaczego nie wejdziemy bezpośrednio w kod i go nie zmienimy? Czasami to
działa, ale zwykle w przypadku szczególnie paskudnego cudzego kodu najlepsze, co można
zrobić podczas rozmieszczania testów, to modyfikować kod w możliwie najmniejszym
stopniu. Jeżeli znasz spoiny, jakie oferuje Ci Twój język, i wiesz, jak z nich korzystać,
często bezpieczniej będzie przeprowadzać testy właśnie tak niż w inny sposób.
Typy spoin, które tu pokazałem, należą do najważniejszych. Możesz je znaleźć w wielu
językach programowania. Jeszcze raz spójrzmy na przykład, który przewijał się w tym
rozdziale, i sprawdźmy, jakie spoiny uda nam się zauważyć.
bool CAsyncSslRec::Init()
{
if (m_bSslInitialized) {
return true;
}
m_smutex.Unlock();
m_nSslRefCount++;

m_bSslInitialized = true;

FreeLibrary(m_hSslDll1);
m_hSslDll1=0;
FreeLibrary(m_hSslDll2);
m_hSslDll2=0;

if (!m_bFailureSent) {
m_bFailureSent=TRUE;
RODZAJE SPOIN 61

PostReceiveError(SOCKETCALLBACK, SSL_FAILURE);
}

CreateLibrary(m_hSslDll1,"syncesel1.dll");
CreateLibrary(m_hSslDll2,"syncesel2.dll");

m_hSslDll1->Init();
m_hSslDll2->Init();
return true;
}

Jakie spoiny są dostępne przy wywołaniu funkcji PostReceiveError? Wymieńmy je:


1. PostReceiveError jest funkcją globalną, możemy więc z łatwością użyć spoiny
konsolidacyjnej. Możemy utworzyć bibliotekę ze szczątkową funkcją i podłożyć ją
podczas konsolidacji, aby pozbyć się niechcianego zachowania. Punktem dostępo-
wym będzie plik programu make albo jakiś parametr w środowisku programistycz-
nym. Musimy też zmienić proces budowy programu, aby podczas testów następo-
wała konsolidacja z biblioteką testową, natomiast podczas budowania rzeczywistego
programu — z biblioteką produkcyjną.
2. Podczas testów możemy dołączyć do kodu instrukcję #include i skorzystać z pre-
procesora w celu zdefiniowania makra o nazwie PostReceiveError. W ten sposób
uzyskamy spoinę preprocesową. Gdzie będzie punkt dostępowy? Aby włączać
i wyłączać definicję makra, możemy korzystać z definiującej dyrektywy preprocesora.
3. Możemy także zadeklarować wirtualną funkcję PostReceiveError, jak to zrobili-
śmy na początku tego rozdziału, dzięki czemu uzyskamy także spoinę obiektową.
Gdzie jest punkt dostępowy? W tym przypadku będzie to miejsce, w którym zde-
cydowaliśmy się utworzyć obiekt. Możemy wygenerować obiekt klasy CAsyncSslRec
lub obiekt jakiejś testowej podklasy, przesłaniającej PostReceiveError.
Zdumiewające jest, że istnieje tyle sposobów na zastąpienie zachowania programu
w miejscu wywołania metody, bez konieczności edycji samej metody:
bool CAsyncSslRec::Init()
{
...
if (!m_bFailureSent) {
m_bFailureSent=TRUE;
PostReceiveError(SOCKETCALLBACK, SSL_FAILURE);
}
...

return true;
}

Ważne jest, aby dokonać wyboru właściwego typu spoiny, kiedy chcesz poddać kod
testom. W ogólności spoiny obiektowe są najlepszym wyborem w przypadku języków zo-
rientowanych obiektowo. Spoiny preprocesowe i konsolidacyjne mogą być czasami
przydatne, ale ich użycie nie jest tak oczywiste, jak spoin obiektowych. Ponadto testy, które
62 ROZDZIAŁ 4. MODEL SPOINOWY

na nich polegają, mogą być trudne w obsłudze. Lubię zachowywać spoiny preprocesowe
i konsolidacyjne na potrzeby sytuacji, w których zależności są porozsiewane w całym
kodzie i nie ma dla nich lepszych rozwiązań.
Kiedy nauczysz się postrzegać kod w kategoriach spoin, łatwiej będzie Ci decydować,
jak testować jego elementy i jak nowemu kodowi nadawać strukturę, aby testowanie było
łatwiejsze.
Rozdział 5.

Narzędzia

Jakich narzędzi potrzebujesz podczas pracy nad cudzym kodem? Edytora (albo zintegro-
wanego środowiska programistycznego) i systemu do budowy programu, ale potrzebna
będzie także platforma testowa. Jeśli Twój język udostępnia narzędzia do refaktoryzacji,
one też mogą okazać się bardzo pomocne.
W rozdziale tym opiszę część narzędzi, które są obecnie dostępne, oraz rolę, jaką mogą
one odegrać w Twojej pracy nad cudzym kodem.

Narzędzia do automatycznej refaktoryzacji


Ręczna refaktoryzacja jest zupełnie w porządku, ale jeśli dysponujesz narzędziem, które
potrafi trochę refaktoryzować za Ciebie, będziesz mógł oszczędzić sporo czasu. W 1990
roku Bill Opdyke w ramach swojej pracy magisterskiej o refaktoryzacji rozpoczął pracę nad
narzędziem refaktoryzującym kod w C++. Chociaż, o ile wiem, nigdy nie stało się ono do-
stępne komercyjnie, zainspirowało utworzenie wielu innych narzędzi w innych językach.
Jednym z najważniejszych była przeglądarka refaktoryzująca kod w Smalltalk, opracowana
przez Johna Brandta i Dona Robertsa na Uniwersytecie Illinois. Przeglądarka ta wspie-
rała wiele metod refaktoryzacji i przez dłuższy czas służyła jako nowoczesny przykład au-
tomatycznej technologii refaktoryzacji. Od tamtego czasu miało miejsce wiele prób doda-
nia wsparcia dla refaktoryzacji w różnych językach i udostępnienia go w szerszym zakresie.
W chwili, kiedy piszę te słowa, dostępnych jest wiele narzędzi refaktoryzujących Javę;
większość z nich jest zintegrowana ze środowiskiem programistycznym, a kilka nie. Ist-
nieją także narzędzia refaktoryzujące kod w Delphi oraz kilka względnie nowych na-
rzędzi dla C++. Kiedy to piszę, trwają aktywne prace nad narzędziami refaktoryzują-
cymi kod w C#.
Wydaje się, że z tymi wszystkimi narzędziami refaktoryzacja powinna być prostsza.
W niektórych środowiskach istotnie tak jest. Niestety, w przypadku wielu z tych na-
rzędzi wsparcie dla refaktoryzacji jest różne. Przypomnijmy sobie jeszcze raz, czym jest
64 ROZDZIAŁ 5. NARZĘDZIA

refaktoryzacja. Oto definicja Martina Fowlera z książki Refactoring: Improving the Design of
Existing Code (Addison-Wesley 1999):

Refaktoryzacja — zmiana w wewnętrznej strukturze oprogramowania, mająca na celu lep-


sze zrozumienie jego kodu i tańszą konserwację bez zmiany istniejącego zachowania.

Zmiana jest refaktoryzacją tylko wtedy, gdy nie powoduje zmiany w zachowaniu. Na-
rzędzia refaktoryzujące powinny sprawdzać, czy określona modyfikacja nie zmienia za-
chowania, i wiele z nich to robi. Była to główna reguła w przeglądarce refaktoryzującej kod
w Smalltalk, w pracy Billa Opdyke’a i w wielu wczesnych narzędziach refaktoryzujących
kod w Javie. Na obrzeżach jednak niektóre z narzędzi tak naprawdę nie sprawdzają —
a jeśli tego nie robią, podczas refaktoryzacji możesz wprowadzać subtelne błędy.
Opłaca się starannie wybrać swoje narzędzie do refaktoryzacji. Dowiedz się, co twórcy
narzędzia mówią o jego bezpieczeństwie. Przeprowadź własne testy. Kiedy mam do czy-
nienia z nowym narzędziem refaktoryzującym, często puszczam na nim niewielkie testy
poczytalności (ang. sanity checks). Czy narzędzie zgłasza błąd, kiedy próbujesz wy-
izolować metodę i nadać jej nazwę, która już istnieje w klasie? A co, jeśli jest to nazwa
metody w klasie bazowej — czy narzędzie potrafi to wykryć? Jeżeli nie, to mógłbyś przy-
padkowo przesłonić metodę i naruszyć kod.
W książce tej omawiam pracę zarówno z automatycznym wsparciem refaktoryzacji,
jak i bez takiego wsparcia. W przykładach piszę, czy założyłem dostępność narzędzia
refaktoryzującego.
We wszystkich przypadkach założyłem, że funkcje refaktoryzacji wspierane przez na-
rzędzie pozostawiają zachowanie bez zmian. Jeśli odkryjesz, że funkcje udostępnione
w Twoim narzędziu nie pozostawiają zachowania, nie korzystaj z automatycznej re-
faktoryzacji. Zastosuj się do rad opisujących, jak postępować bez narzędzia do refaktoryzacji
— tak będzie bezpieczniej.

Testy i automatyczna refaktoryzacja


Kiedy masz narzędzie wykonujące za Ciebie refaktoryzację, kuszące może być przyjęcie założe-
nia, że nie trzeba już pisać testów sprawdzających kod, który masz zamiar poddać refakto-
ryzacji. W niektórych przypadkach jest tak w istocie. Jeśli Twoje narzędzie przeprowadza
bezpieczną refaktoryzację, a Ty przechodzisz od jednej automatycznej refaktoryzacji do
kolejnej bez dokonywania dodatkowej edycji, to można przyjąć, że wprowadzone auto-
matycznie edycje nie zmieniają zachowania. Nie zawsze jednak ma to miejsce.
Oto przykład:
public class A {
private int alpha = 0;
private int getValue() {
alpha++;
return 12;
}
OBIEKTY POZOROWANE 65

public void doSomething() {


int v = getValue();
int total = 0;
for (int n = 0; n < 10; n++) {
total += v;
}
}
}

Istnieją przynajmniej dwa narzędzia refaktoryzujące kod w Javie, których można użyć do
usunięcia zmiennej v z funkcji doSomething. Po refaktoryzacji kod wygląda następująco:
public class A {
private int alpha = 0;
private int getValue() {
alpha++;
return 12;
}
public void doSomething() {
int total = 0;
for (int n = 0; n < 10; n++) {
total += getValue();
}
}
}

Czy widzisz problem? Zmienna została usunięta, ale teraz wartość alpha jest zwiększana
dziesięć razy zamiast tylko raz. Ta modyfikacja z całą pewnością nie pozostawiła zachowania
bez zmian.
Dobrym pomysłem jest przetestowanie kodu, zanim zaczniesz stosować automatyczną refakto-
ryzację. Możesz przeprowadzić niektóre z automatycznych refaktoryzacji bez testowania,
ale powinieneś wiedzieć, co sprawdza Twoje narzędzie, a czego nie sprawdza. Kiedy zaczy-
nam korzystać z nowego narzędzia, pierwsze, co robię, to przepuszczenie funkcji do wydo-
bywania metod przez wszystkie etapy jej działania. Jeżeli będę w stanie zaufać jej w stopniu
wystarczającym do korzystania z niej bez testów, to uzyskam kod w postaci znacznie bar-
dziej nadającej się do dalszego testowania.

Obiekty pozorowane
Jednym z większych problemów, z którym mamy do czynienia podczas pracy nad cudzym
kodem, są zależności. Kiedy chcemy uruchomić w oderwaniu fragment kodu i zobaczyć,
co robi, często musimy usunąć jego zależność od innego kodu. Rzadko jednak jest to
proste. Jeżeli usuniemy inny kod, musimy umieścić w jego miejscu coś, co dostarczy
właściwe wartości podczas testów, abyśmy mogli dokładnie zweryfikować nasz frag-
ment. W kodzie zorientowanym obiektowo taki element często jest nazywany obiektem
pozorowanym.
66 ROZDZIAŁ 5. NARZĘDZIA

Wiele obiektów pozorowanych jest dostępnych za darmo. Strona www.mockobjects.com


jest dobrym miejscem do szukania informacji na temat większości z nich.

Jarzmo testowania jednostkowego


Narzędzia do testowania mają długą i różnorodną historię. Nie ma roku, abym nie natknął
się na cztery czy też pięć zespołów, które zakupiły jakieś drogie narzędzie testujące, sprze-
dawane ze stanowiskowymi licencjami, które kończy, nie zapracowawszy na swoją cenę.
Aby oddać sprawiedliwość sprzedawcom tych narzędzi — testowanie to poważna sprawa,
a ludzie często są uwodzeni ideą, że mogą prowadzić testy w graficznym lub webowym
interfejsie użytkownika, bez potrzeby robienia czegoś dodatkowego w swojej aplikacji.
Jest to możliwe, ale zazwyczaj testowanie wiąże się z większym nakładem pracy, niż
ktokolwiek w zespole byłby to gotów przyznać. Poza tym interfejs użytkownika nie
jest najlepszym miejscem na pisanie testów. Interfejsy użytkownika często podlegają
zmianom i są zbyt oddalone od testowanych funkcjonalności. Kiedy testy bazujące na
interfejsie użytkownika nie powiodą się, trudne może być stwierdzenie, dlaczego tak się
stało. Niezależnie od wszystkiego ludzie często wydają znaczące kwoty pieniędzy, próbując
przeprowadzać wszystkie swoje testy za pomocą tego typu narzędzi.
Najefektywniejsze narzędzia do testowania, z jakimi miałem do czynienia, są darmowe.
Pierwszym z nich jest platforma testująca xUnit. Oryginalnie napisany w języku Smalltalk
przez Kenta Becka, a następnie przełożony na Javę przez Kenta Becka i Ericha Gammę
xUnit jest niewielkim, wydajnym systemem przeznaczonym do testowania jednostkowego.
Oto jego kluczowe cechy:
 Umożliwia programistom pisanie testów w językach, w których programują.
 Wszystkie testy przebiegają w izolacji.
 Testy można grupować w zestawy, dzięki czemu istnieje możliwość ich uru-
chamiania, a następnie powtarzania na żądanie.
Platforma xUnit została przystosowana do obsłużenia większości najważniejszych
języków programowania, a także kilku dziwnych i rzadko spotykanych.
Najbardziej rewolucyjnym aspektem xUnit jest jego prostota i ukierunkowanie.
Umożliwia on bezproblemowe pisanie testów. Chociaż był początkowo projektowany do
przeprowadzania testów jednostkowych, możesz z niego korzystać także do pisania więk-
szych testów, gdyż xUnit tak naprawdę nie zwraca uwagi na to, jak bardzo duży czy też
mały jest test. Jeśli tylko można napisać test w języku, z którego korzystasz, xUnit jest
w stanie go uruchomić.
W książce tej większość przykładów jest napisana w Javie i C++. W przypadku Javy
preferowanym jarzmem testowym xUnit jest JUnit i wygląda ono bardzo podobnie jak
większość pozostałych jarzm xUnit. Jeśli chodzi o C++, to często korzystam z jarzma
testowego o nazwie CppUnitLite, które sam napisałem. Wygląda ono zupełnie inaczej
JARZMO TESTOWANIA JEDNOSTKOWEGO 67

i również opisuję je w tym rozdziale. A tak przy okazji — nie okazuję lekceważenia
autorowi oryginalnego CppUnit, korzystając z CppUnitLite. Byłem tym facetem dawno
temu, i już po tym, jak wydałem CppUnitLite, odkryłem, że mogło być ono znacznie mniej-
sze, łatwiejsze w użyciu i przenośne w zdecydowanie większym stopniu, gdyby korzystało
z niektórych idiomów języka C i tylko niezbędnego podzbioru C++.

JUnit
W JUnit piszesz testy, tworząc podklasę klasy o nazwie TestCase.
import junit.framework.*;

public class FormulaTest extends TestCase {


public void testEmpty() {
assertEquals(0, new Formula("").value());
}

public void testDigit() {


assertEquals(1, new Formula("1").value());
}
}

Każda metoda klasy testowej definiuje test, jeśli ma sygnaturę o następującej postaci:
void testXXX(), gdzie XXX jest nazwą, jaką chcesz nadać testowi. Każda metoda testowa
może zawierać kod i asercje. W metodzie testEmpty znajduje się kod tworzący nowy obiekt
Formula i wywołujący jego metodę value. Jest tam także kod asercji, który sprawdza, czy
skutkiem tego wywołania jest wartość równa 0. Jeśli tak, test kończy się sukcesem; jeśli nie
— niepowodzeniem.
Oto skrótowe przedstawienie tego, co się dzieje, kiedy przeprowadzasz testy w JUnit.
Program uruchomieniowy JUnit wczytuje program (taki jak pokazano powyżej) i korzysta
z mechanizmu refleksji, aby odszukać wszystkie metody testowe. To, co zrobi w następnej
kolejności, jest w pewnym sensie przebiegłe. Tworzy on całkowicie odrębne obiekty dla
każdego obiektu z metod testowych. W przykładowym kodzie utworzy dwa takie obiekty:
zadaniem pierwszego z nich jest wyłącznie uruchomienie metody testEmpty, natomiast
jedynym zadaniem drugiego obiektu jest wywołanie obiektu testDigit. Jeśli chcesz
wiedzieć, jakiej klasy są te obiekty, to w obu przypadkach mamy do czynienia z tą samą
klasą — FormulaTest. Każdy obiekt został skonfigurowany tak, aby wywoływał dokładnie
jedną z metod testowych tej klasy. Kluczową sprawą jest fakt, że mamy zupełnie odrębne
obiekty dla każdej z metod. Nie ma możliwości, aby wywierały one na siebie wpływ.
Oto przykład.
public class EmployeeTest extends TestCase {
private Employee employee;

protected void setUp() {


employee = new Employee("Alfred", 0, 10);
TDate cardDate = new TDate(10, 10, 2000);
68 ROZDZIAŁ 5. NARZĘDZIA

employee.addTimeCard(new TimeCard(cardDate,40));
}

public void testOvertime() {


TDate newCardDate = new TDate(11, 10, 2000);
employee.addTimeCard(new TimeCard(newCardDate, 50));
assertTrue(employee.hasOvertimeFor(newCardDate));
}

public void testNormalPay() {


assertEquals(400, employee.getPay());
}
}

W klasie EmployeeTest mamy specjalną metodę o nazwie setUp. Metoda ta została


zdefiniowana w klasie TestCase i jest uruchamiana w każdym obiekcie testowym przed
wywołaniem metody testowej. Metoda setUp umożliwia nam tworzenie zestawu obiek-
tów, z których będziemy korzystać w teście. Taki zestaw obiektów jest tworzony w ten
sam sposób przed rozpoczęciem każdego z testów. W obiekcie wywołującym metodę
testNormalPay sprawdzane jest w kontekście pracownika utworzonego na potrzeby
tego obiektu przez metodę setUp, czy jego wynagrodzenie jest poprawnie obliczane na
podstawie jednej karty czasu pracy — karty dodanej w metodzie setUp. W obiekcie
wywołującym metodę testOvertime pracownik utworzony dla tego obiektu w metodzie
setUp otrzymuje dodatkową kartę czasu pracy, a także następuje sprawdzenie, czy ta
druga karta spełnia warunek występowania nadgodzin. Metoda setUp jest wywoływana dla
wszystkich obiektów klasy EmployeeTest, a każdy z nich otrzymuje własny zestaw obiek-
tów utworzonych przez tę metodę. Jeśli musisz zrobić jeszcze coś specjalnego po tym,
jak test się zakończy, możesz przesłonić kolejną metodę o nazwie tearDown, zdefiniowaną
w klasie TestCase. Jest ona uruchamiana dla każdego obiektu po metodzie testowej.
Kiedy po raz pierwszy widzisz jarzmo xUnit, z pewnością będzie Ci się wydawać
trochę dziwne. Dlaczego klasy testowe w ogóle mają metody setUp i tearDown? Dlaczego
nie możemy po prostu tworzyć potrzebnych nam obiektów w konstruktorze? Fakt,
moglibyśmy, ale pamiętaj, że testowy program uruchomieniowy pracuje na klasach testo-
wych. Przechodzi on po kolei do każdej klasy testowej i tworzy zbiór obiektów — po jed-
nym dla każdej metody testowej. To bardzo obszerny zbiór obiektów, ale nie jest tak źle,
jeżeli obiekty te nie przeprowadziły jeszcze potrzebnych alokacji. Umieszczając kod
w klasie setUp w celu utworzenia tego, czego potrzebujemy dokładnie wtedy, kiedy po-
trzebujemy, całkiem sporo oszczędzamy na zasobach. Ponadto, opóźniając metodę setUp,
możemy uruchamiać ją wówczas, gdy zaistnieje możliwość wykrycia i zaraportowania
problemów, które mogą wystąpić w momencie konfigurowania testu.

CppUnitLite
Kiedy tworzyłem pierwszą wersję CppUnit, starałem się, aby była tak bliska JUnit, jak to
tylko możliwe. Doszedłem do wniosku, że ułatwi to zadanie ludziom, którzy widzieli już
wcześniej architekturę xUnit; przyjęcie zatem takiego właśnie rozwiązania wydawało
JARZMO TESTOWANIA JEDNOSTKOWEGO 69

mi się lepsze. Niemal od razu natknąłem się na serię zagadnień, które były trudne lub
wręcz niemożliwe do bezproblemowego zaimplementowania w C++ ze względu na różne
cechy Javy i C++. Podstawowym problemem w C++ był brak mechanizmu refleksji.
W Javie możesz pracować na referencjach do metod klas pochodnych, odnajdować metody
podczas wykonywania programu itd. W C++ musisz pisać kod rejestrujący metodę, do
której chcesz mieć dostęp w czasie działania programu. W rezultacie CppUnit stał się
trochę trudniejszy w użyciu i w zrozumieniu. Musiałeś pisać własną funkcję obsługi dla
klasy testowej, żeby program uruchamiający testy mógł wywoływać obiekty dla poszcze-
gólnych metod.
Test *EmployeeTest::suite()
{
TestSuite *suite = new TestSuite;
suite.addTest(new TestCaller<EmployeeTest>("testNormalPay",
testNormalPay));
suite.addTest(new TestCaller<EmployeeTest>("testOvertime",
testOvertime));
return suite;
}

Nie muszę dodawać, że jest to dość żmudne. Trudno jest zachować impet podczas pi-
sania testów, kiedy musisz deklarować metody testowe w nagłówku klasy, definiować je
w pliku źródłowym oraz rejestrować w metodzie je obsługującej. Istnieje wiele schematów
makr, umożliwiających obejście tych problemów, ale ja zdecydowałem się zacząć od sa-
mego początku. Uzyskałem schemat, w którym można utworzyć test, zapisując po prostu
następujący plik źródłowy:
#include "testharness.h"
#include "employee.h"
#include <memory>

using namespace std;

TEST(testNormalPay,Employee)
{
auto_ptr<Employee> employee(new Employee("Alfred", 0, 10));
LONGS_EQUALS(400, employee->getPay());
}

W teście tym użyte zostało makro o nazwie LONGS_EQUAL, które sprawdza, czy dwie
długie liczby całkowite są sobie równe. Zachowuje się ono tak samo jak assertEquals
w JUnit, ale jest przystosowane do liczb typu long.
Makro TEST robi za kulisami kilka paskudnych rzeczy. Tworzy podklasę testowanej
klasy i nadaje jej nazwę, sklejając razem dwa argumenty (nazwę testu i nazwę testowanej
klasy). Następnie tworzy instancję tej podklasy, która jest skonfigurowana do uruchomie-
nia kodu znajdującego się między nawiasami klamrowymi. Instancja jest statyczna; kiedy
program się wczyta, dodaje sam siebie do listy testowanych obiektów. W dalszej kolejności
program uruchamiający testy może gnać przez listę i wywoływać każdy z testów.
70 ROZDZIAŁ 5. NARZĘDZIA

Po tym, jak napisałem tę niewielką platformę, zdecydowałem, że nie będę jej publiko-
wał, ponieważ kod w makrze nie był szczególnie przejrzysty, a ja przecież poświęcałem
mnóstwo czasu na przekonywanie innych, aby pisali przejrzysty kod. Mój przyjaciel, Mike
Hill, natknął się na niektóre z tych samych problemów, zanim jeszcze się poznaliśmy,
i utworzył dedykowaną dla produktów Microsoftu platformę testową o nazwie TestKit,
która obsługiwała rejestrowanie w taki sam sposób. Zachęcony przez Mike’a zacząłem
pozbywać się wielu późnych funkcji charakterystycznych dla C++, z których skorzystałem
w mojej małej platformie, po czym ją opublikowałem (funkcje te stanowiły poważny
problem w CppUnit; każdego dnia otrzymywałem e-maile od ludzi, którzy nie mogli
używać szablonów albo biblioteki standardowej lub ich kompilator C++ zgłaszał wyjątki).
Zarówno CppUnit, jak i CppUnitLite sprawdzają się w roli jarzm testowych. Testy
pisane przy użyciu CppUnitLite są nieco krótsze, dlatego też korzystam z niego w tej
książce przy okazji prezentowania przykładów w języku C++.

NUnit
NUnit jest platformą testową dla języków .NET. Można w niej pisać testy dla kodu w C#,
VB.NET lub dowolnego innego języka działającego na platformie .NET. NUnit bardzo
przypomina w działaniu JUnit. Jedyna zasadnicza różnica polega na tym, że wykorzystuje
ona atrybuty w celu oznaczania testowych metod i klas. Składnia atrybutów zależy od
języka .NET, w jakim dane testy są pisane.
Oto przykład testu na platformie NUnit, napisanego w VB.NET:
Imports NUnit.Framework

<TestFixture()> Public Class LogOnTest


Inherits Assertion

<Test()> Public Sub TestRunValid()


Dim display As New MockDisplay()
Dim reader As New MockATMReader()
Dim logon As New LogOn(display, reader)
logon.Run()
AssertEquals("Proszę wprowadzić kartę", display.LastDisplayedText)
AssertEquals("MainMenu",logon.GetNextTransaction().GetType.Name)
End Sub

End Class

<TestFixture()> i <Test()> są atrybutami oznaczającymi LogonTest i TestRunValid


odpowiednio jako klasę testową i metodę testową.

Inne platformy xUnit


Istnieje wiele odmian xUnit dla wielu różnych języków i platform. W ogólności
wspierają one specyfikowanie, grupowanie oraz uruchamianie testów jednostkowych.
OGÓLNE JARZMO TESTOWE 71

Jeśli chcesz odszukać wersję xUnit dla swojej platformy albo języka, przejdź na stronę
www.xprogramming.com i zajrzyj do sekcji Downloads. Witryna ta jest prowadzona
przez Rona Jeffriesa i de facto stanowi repozytorium wszystkich wersji xUnit.

Ogólne jarzmo testowe


Platformy rodziny xUnit, które opisałem w poprzednich podrozdziałach, zostały za-
projektowane na potrzeby testów jednostkowych. Można z nich korzystać, aby testować
wiele klas jednocześnie, ale taki rodzaj zadań jest domeną platform FIT i Fitnesse.

Framework for Integrated Tests (FIT)


FIT (Platforma dla Testów Zintegrowanych) jest zwięzłą i elegancką platformą, opra-
cowaną przez Warda Cunninghama. Idea kryjąca się za FIT jest prosta i zarazem wydajna.
Na temat swojego systemu możesz pisać dokumenty i osadzać w nich tabele opisujące
jego dane wejściowe i wyjściowe. Dokumenty te można zapisywać w formacie HTML,
a platforma FIT może uruchamiać je jako testy.
FIT przyjmuje HTML, uruchamia testy zdefiniowane w tabelach HTML i przedstawia
wyniki w HTML. Dane wyjściowe wyglądają tak samo jak dane wejściowe, a cały tekst
i tabele zostają zachowane. Komórki w tabelach są oznaczone kolorem zielonym, aby
wskazać wartości, które pomyślnie zaliczyły test, lub kolorem czerwonym, aby wskazać,
dla jakich wartości test się nie powiódł. Możesz także skorzystać z opcji, aby w wyni-
kowym kodzie HTML uzyskać informację podsumowującą.
Jedyne, co musisz zrobić, aby to wszystko zadziałało, to skonfigurowanie paru tabel
obsługujących kod, dzięki czemu system będzie wiedzieć, jak uruchamiać fragmenty
Twojego kodu i jak pobierać z nich dane wynikowe. Zazwyczaj jest to dość łatwe, ponie-
waż platforma udostępnia kod zapewniający wsparcie dla wielu różnych rodzajów tabel.
Jedna z wielu wydajnych funkcji platformy FIT polega na możliwości wspierania
komunikacji między osobami piszącymi oprogramowanie a osobami specyfikującymi,
co program powinien robić. Ci, którzy tworzą specyfikacje, mogą pisać dokumenty i osa-
dzać w nich rzeczywiste testy. Testy zostaną uruchomione, ale zakończą się niepowodze-
niem. W dalszej kolejności programiści mogą dodać nowe funkcjonalności, a testy się
powiodą. Zarówno użytkownicy, jak i programiści, dysponują wspólnym i aktualnym
obrazem możliwości systemu.
FIT ma o wiele większe możliwości, niż jestem w stanie tu opisać. Więcej informacji
o platformie FIT znajdziesz pod adresem http://fit.c2.com/.

Fitnesse
Fitnesse jest w zasadzie platformą FIT hostowaną na wiki. Większość tego systemu została
opracowana przez Roberta Martina i Micaha Martina. Pracowałem trochę z Fitnesse,
72 ROZDZIAŁ 5.

ale musiałem zrobić sobie przerwę, aby skupić się na pisaniu tej książki. Z niecierpliwością
czekam, aż znowu będę mógł przystąpić do pracy z tą platformą.
Fitnesse wspiera hierarchiczne strony web, które definiują testy na platformie FIT.
Strony zawierające tabele testowe mogą być uruchamiane indywidualnie albo w zesta-
wach, a bogactwo różnych opcji ułatwia współpracę w obrębie zespołu. Platforma Fitnesse
dostępna jest pod adresem http://www.fitnesse.org/. Tak samo jak pozostałe narzędzia opi-
sane w tym rozdziale, jest ona darmowa i wspierana przez społeczność programistów.
Część II

Zmiany w oprogramowaniu
74 CZĘŚĆ II ZMIANY W OPROGRAMOWANIU
KIEŁKOWANIE METODY 75

Rozdział 6.

Nie mam zbyt wiele czasu,


a muszę to zmienić

Spójrzmy prawdzie w oczy: książka, którą właśnie czytasz, opisuje dodatkową pracę —
pracę, której prawdopodobnie teraz nie wykonujesz i która może sprawić, że dokoń-
czenie pewnych zmian, jakie masz zamiar wprowadzić w swoim kodzie, pochłonie więcej
Twojego czasu. Być może zastanawiasz się, czy akurat teraz warto się tym zajmować.
Prawda jest taka, że praca, którą wykonujesz w celu usuwania zależności i pisania te-
stów na potrzeby wprowadzanych zmian, zabiera Ci czas, ale w większości przypadków jej
wynikiem będzie przyszła oszczędność czasu i frustracji. Kiedy? Cóż, to zależy od projektu.
W niektórych przypadkach być może przez dwie godziny będziesz pisać testy dla kodu,
który wymaga zmian. Zmiana, którą następnie wprowadzisz, zabierze Ci 15 minut.
Kiedy potem spojrzysz na to, co się stało, będziesz mógł powiedzieć: „Właśnie straci-
łem dwie godziny — czy było warto?”. To zależy. Nie wiesz, ile czasu zabrałaby Ci ta praca,
gdybyś nie napisał testów. Nie wiesz też, ile czasu musiałbyś poświęcić na debugowanie
kodu, gdybyś popełnił w nim błąd — czasu, który mógłbyś zaoszczędzić, gdybyś w odpo-
wiednich miejscach porozmieszczał testy. Nie mam na myśli tego czasu, który możesz
zaoszczędzić, gdy testy wyłapują pomyłkę, ani czasu, jaki oszczędzają Ci testy, kiedy
próbujesz zlokalizować błąd. Dzięki testom rozmieszczonym w kodzie często łatwiejsze
jest znalezienie problemów funkcjonalnych.
Przyjmijmy najgorszy scenariusz. Zmiana była prosta, ale my i tak przetestowaliśmy
kod w modyfikowanym obszarze — wszystkie nasze zmiany przeprowadziliśmy poprawnie.
Czy warto było testować? Nie wiemy, kiedy powrócimy do tego fragmentu kodu i doko-
namy następnych zmian. W najlepszym przypadku wrócimy w to miejsce przy następnej
iteracji i nasza inwestycja zacznie się szybko zwracać; w najgorszym — miną całe lata,
zanim ktokolwiek tu powróci i zmieni kod. Prawdopodobnie jednak będziemy czytać ten
kod okresowo, choćby tylko po to, aby sprawdzić, czy nie trzeba w tym — czy jakimś
innym — miejscu wprowadzić zmian. Czy kod byłby łatwiejszy do zrozumienia, gdyby
klasy były mniejsze, a testy jednostkowe zostały przeprowadzone? Prawdopodobnie tak.
76 ROZDZIAŁ 6. NIE MAM ZBYT WIELE CZASU, A MUSZĘ TO ZMIENIĆ

Ale to był najgorszy przypadek. Jak często ma on miejsce? Zmiany zazwyczaj nagroma-
dzają się w systemach. Jeśli zmieniasz coś dzisiaj, istnieje szansa, że już wkrótce nastąpi
kolejna zmiana.
Kiedy pracuję z zespołami, często rozpoczynam od zaproszenia ich do wzięcia udziału
w eksperymencie. Przez jedną iterację staramy się nie dokonywać ani jednej poprawki
w kodzie bez przeprowadzania sprawdzającego ją testu. Jeśli ktoś uważa, że nie może
napisać testu, powinien zwołać szybkie zebranie, podczas którego zapyta się grupy, czy
napisanie takiego testu jest możliwe. Początki tych iteracji bywają trudne. Ludzie mają
wrażenie, że nie wykonują całej pracy, jaką powinni wykonać. Jednak powoli zaczynają
odkrywać, że powracają do lepszego kodu. Wprowadzanie zmian staje się łatwiejsze,
a ludzie intuicyjnie odczuwają, że właśnie tego trzeba, aby w lepszym stylu posuwać się
naprzód. Grupa potrzebuje czasu, żeby pokonać tę barierę, a jeśli istnieje coś, co mógł-
bym bezzwłocznie zrobić dla każdego zespołu na świecie, to zagwarantowałbym im moż-
liwość doświadczenia takiego wspólnego przeżycia — przeżycia, które można odczytać
z ich twarzy: „O rany, już nigdy więcej nie wrócimy do tamtych metod”.
Jeśli jeszcze nie masz takich doświadczeń, powinieneś to nadrobić.
W rezultacie Twoja praca będzie przebiegać szybciej, co jest ważne prawie w każdej
firmie programistycznej. Szczerze mówiąc, jako programista jestem jednak najzwyczajniej
szczęśliwy, że dzięki temu moja praca jest o wiele mniej frustrująca.
Gdy pokonasz już tę barierę, Twoje życie nie będzie usiane różami, ale stanie się ła-
twiejsze. Kiedy poznasz wartość testowania i odczujesz różnicę, jedyną rzeczą, z jaką bę-
dziesz musiał się zmierzyć, to podjęcie chłodnej i wyrachowanej decyzji, co robić w każ-
dym konkretnym przypadku.

Każdego dnia gdzieś to się zdarza


Wchodzi Twój szef i mówi: „Klienci domagają się tej nowej funkcji. Czy damy radę zrobić
to dzisiaj?”.
„Nie wiem”.
Rozglądasz się. Czy testy są porozmieszczane na swoich miejscach? Nie.
Pytasz: „Jak bardzo jest ci to potrzebne?”.
Wiesz, że możesz wprowadzić poprawki bezpośrednio w kodzie we wszystkich 10 miejscach,
które wymagają zmiany, i że uporasz się z tym do 17.00. W końcu mamy sytuację krytyczną.
Jutro to naprawimy, prawda?
Pamiętaj — kod to Twój dom, a Ty musisz w nim mieszkać.

Najtrudniejszą kwestią dotyczącą podejmowania decyzji, czy pisać testy, gdy znaj-
dujesz się pod presją, jest fakt, że możesz po prostu nie wiedzieć, ile czasu zabierze Ci doda-
nie nowej funkcjonalności. W przypadku cudzego kodu szczególnie trudne jest przedsta-
wienia oszacowania, które będzie wiarygodne. Istnieją pewne techniki, które mogą być przy
KIEŁKOWANIE METODY 77

tym pomocne. Po szczegóły zajrzyj do rozdziału 16., „Nie rozumiem kodu wystarczająco
dobrze, żeby go zmienić”. Kiedy naprawdę nie wiesz, jak długo może potrwać dodanie
nowej funkcjonalności, a podejrzewasz, że zabierze to więcej czasu, niż masz do dyspozy-
cji, kuszące może być uporanie się z tym zadaniem tak szybko, jak tylko będziesz w stanie
to zrobić. Potem — jeśli znajdziesz wystarczająco dużo czasu — powrócisz, aby przepro-
wadzić testy i zająć się refaktoryzacją. Najtrudniejszym zadaniem jest właśnie ów powrót
i dokonanie testów oraz refaktoryzacji. Zanim ludzie pokonają barierę, bardzo często
unikają tej pracy, co może być problemem związanym z morale. Zajrzyj do rozdziału 24.,
„Czujemy się przytłoczeni. Czy nie będzie chociaż trochę lepiej?”, gdzie znajdziesz kilka
konstruktywnych rad, jak posunąć się do przodu.
To, co opisałem do tej pory, brzmi jak rzeczywisty dylemat: zapłacić już teraz czy za-
płacić więcej później. Albo będziesz pisać testy podczas wprowadzania zmian, albo będziesz
musiał pogodzić się z faktem, że wraz z upływem czasu będzie coraz trudniej. Może być
trudno, ale czasami tak się nie dzieje.
Jeśli musisz wprowadzić zmianę w klasie już teraz, spróbuj utworzyć egzemplarz
obiektu tej klasy w jarzmie testowym. Jeśli nie możesz, zajrzyj do rozdziału 9., „Nie mogę
umieścić tej klasy w jarzmie testowym”, albo 10., „Nie mogę uruchomić tej metody
w jarzmie testowym”. Poddanie kodu, który zmieniasz, testom w jarzmie może być ła-
twiejsze, niż się spodziewasz. Jeśli przeczytasz te rozdziały i dojdziesz do wniosku, że w da-
nej chwili istotnie nie możesz usunąć zależności ani porozmieszczać testów, dokładnie
przyjrzyj się zmianom, których chcesz dokonać. Dalsza część tego rozdziału zawiera
opis kilku technik, które można w tym celu wykorzystać.
Zapoznaj się z tymi technikami i weź pod uwagę ich użycie, ale pamiętaj, że należy
z nich korzystać z rozwagą. Kiedy je stosujesz, wprowadzasz do swojego systemu przete-
stowany kod, ale dopóki nie pokryjesz testami kodu, który go wywołuje, nie zweryfikujesz
jego użycia. Bądź ostrożny.

Kiełkowanie metody
Gdy potrzebujesz dodać do systemu funkcjonalność, którą można sformułować w postaci
całkowicie nowego kodu, zapisz ten kod w odrębnej metodzie. Będziesz ją wywoływać
z miejsc, w których ta nowa funkcjonalność powinna się znajdować. Być może poddanie
testom wszystkich tych miejsc nie będzie proste, ale przynajmniej będziesz mógł tworzyć
testy dla nowego kodu. Oto przykład.
public class TransactionGate
{
public void postEntries(List entries) {
for (Iterator it = entries.iterator(); it.hasNext(); ) {
Entry entry = (Entry)it.next();
entry.postDate();Method
}
transactionBundle.getListManager().add(entries);
78 ROZDZIAŁ 6. NIE MAM ZBYT WIELE CZASU, A MUSZĘ TO ZMIENIĆ

}
...
}

Musimy dodać kod, który sprawdzi, czy któryś z nowych wpisów nie znajduje się już
w obiekcie transactionBundle, zanim prześlemy daty tych wpisów i je tam dodamy.
Po spojrzeniu na kod wydaje się, że należy to zrobić na początku metody — jeszcze
przed pętlą, ale tak naprawdę sprawdzanie może odbywać się wewnątrz pętli. Moglibyśmy
zmienić kod następująco:
public class TransactionGate
{
public void postEntries(List entries) {
List entriesToAdd = new LinkedList();
for (Iterator it = entries.iterator(); it.hasNext(); ) {
Entry entry = (Entry)it.next();
if (!transactionBundle.getListManager().hasEntry(entry) {
entry.postDate();
entriesToAdd.add(entry);
}
}
transactionBundle.getListManager().add(entriesToAdd);
}
...
}

Zmiana wygląda na prostą, ale jest dość inwazyjna. Skąd będziemy wiedzieć, że prze-
prowadziliśmy ją poprawnie? Między nowym kodem, który dodaliśmy, a kodem dotych-
czasowym nie ma żadnego rozgraniczenia. Co gorsza, po zmianie kod stał się bardziej
zagmatwany. Teraz mieszamy w nim dwie operacje — przesyłanie daty i wykrywanie
powielonych wpisów. Metoda jest raczej mała, a już zrobiła się mniej czytelna i wpro-
wadziliśmy jeszcze tymczasową zmienną. Zmienne tymczasowe niekoniecznie są złe, ale
czasami przyciągają nowy kod. Jeżeli następna zmiana, jaką będziemy musieli wprowa-
dzić, polega na pracy z wszystkimi niepowielonymi wpisami przed ich dodaniem, to w takim
przypadku będzie istnieć tylko jedno miejsce w kodzie, w którym tego typu zmienna bę-
dzie mogła się znaleźć — właśnie w tej metodzie. Kuszące będzie po prostu umieszczenie
w tej metodzie także i tego kodu. Czy moglibyśmy zrobić to inaczej?
Tak. Możemy przyjąć, że usuwanie powielonych wpisów jest całkowicie odrębną ope-
racją. Aby utworzyć nową metodę o nazwie uniqueEntries, skorzystamy z techniki pro-
gramowania sterowanego testami (104):
public class TransactionGate
{
...
List uniqueEntries(List entries) {
List result = new ArrayList();
for (Iterator it = entries.iterator(); it.hasNext(); ) {
Entry entry = (Entry)it.next();
if (!transactionBundle.getListManager().hasEntry(entry) {
result.add(entry);
KIEŁKOWANIE METODY 79

}
}
return result;
}
...
}

Napisanie testów, które doprowadziłyby nas do uzyskania dla tej metody powyższego
kodu, byłoby łatwe. Kiedy już będziemy dysponować tą metodą, możemy powrócić do
wyjściowego kodu i dodać jej wywołanie.
public class TransactionGate
{
...
public void postEntries(List entries) {
List entriesToAdd = uniqueEntries(entries);
for (Iterator it = entriesToAdd.iterator(); it.hasNext(); ) {
Entry entry = (Entry)it.next();
entry.postDate();
}
transactionBundle.getListManager().add(entriesToAdd);
}
...
}

Nadal mamy zmienną tymczasową, ale kod jest o wiele mniej zagmatwany. Gdybyśmy
musieli dodać jeszcze więcej kodu, który działa na niepowielonych wpisach, moglibyśmy
napisać w tym celu metodę i wywołać ją z tego miejsca. Jeżeli potrzeba będzie jeszcze więcej
kodu obsługującego takie wpisy, będziemy mogli dodać nową klasę i przesunąć do niej
wszystkie te metody. W efekcie wyjściowa metoda pozostaje mała; uzyskujemy kolejne
metody, krótsze i łatwiejsze do zrozumienia.
Był to przykład na kiełkowanie metody. Oto czynności, które należy wykonać:
1. Określ miejsce, w którym trzeba zmienić kod.
2. Jeśli zmiany można sformułować w postaci sekwencji instrukcji do umieszczenia
w pewnym miejscu istniejącej metody, zapisz tam wywołanie nowej metody, która
wykonuje określone zadanie, a następnie przekształć je w komentarz (lubię tak robić,
zanim jeszcze napiszę nową metodę, dzięki czemu mogę zyskać wyobrażenie,
jak wywołanie metody będzie wyglądać w kontekście).
3. Określ, które zmienne lokalne z metody źródłowej będą potrzebne, i uwzględnij je
jako argumenty wywołania.
4. Określ, czy kiełkowana metoda powinna zwracać jakieś wartości metodzie źródłowej.
Jeśli tak, zmień wywołanie, aby zwracana wartość była przypisywana zmiennej.
5. Opracuj kiełkowaną metodę, korzystając z techniki programowania sterowanego
testami (104).
6. Usuń komentarz z metody źródłowej, aby uaktywnić wywołanie.
80 ROZDZIAŁ 6. NIE MAM ZBYT WIELE CZASU, A MUSZĘ TO ZMIENIĆ

Zalecam kiełkowanie metody za każdym razem, kiedy dodawaną metodę możesz po-
traktować jako odrębny fragment kodu albo nie masz jeszcze możliwości jej przetestowa-
nia. Takie rozwiązanie jest o wiele bardziej wskazane niż dodawanie wierszy z kodem.
Czasami chcesz skorzystać z kiełkowania metody, ale zależności w Twojej klasie są do
tego stopnia skomplikowane, że nie możesz utworzyć jej instancji bez fałszowania wielu
argumentów konstruktora. Jedną z możliwości jest przekazanie wartości pustej (126).
Kiedy to nie zadziała, rozważ przekształcenie wykiełkowanej metody w statyczną metodę
publiczną. Być może jako argumenty będziesz musiał przekazać zmienne instancji klasy
źródłowej, ale umożliwi Ci to dokonanie zmiany. Tworzenie w tym celu metody statycznej
może wydawać się dziwne, ale może też okazać się przydatne podczas pracy nad cudzym
kodem. Mam skłonność do postrzegania metod statycznych w klasach jako punktu zbor-
nego. Często zdarza się, że kiedy masz wiele metod statycznych i stwierdzasz, iż nie-
które z ich zmiennych są wspólne, dochodzisz do wniosku, że mógłbyś utworzyć nową
klasę i przesunąć do niej te metody, gdzie staną się one metodami instancji. Gdy rzeczywi-
ście zasługują one na to, aby stać się metodami instancji bieżącej klasy, można je będzie
przesunąć z powrotem do klasy, gdzie wreszcie poddasz je testom.

Zalety i wady
Kiełkowanie metody ma swoje zalety i wady. Najpierw przyjrzyjmy się wadom. Jakie
słabe strony ma kiełkowanie metody? Otóż kiedy z niej korzystasz, to tak, jakbyś na chwilę
zrezygnował z metody źródłowej i jej klas. Nie przetestujesz jej ani nie udoskonalisz —
dodasz po prostu jakąś nową funkcjonalność w nowej metodzie. Rezygnacja z metody albo
klasy jest czasami wyborem podyktowanym praktycznością, ale nadal w pewnym sensie
jest przykrą sprawą. Pozostawia to Twój kod w stanie zawieszenia. Metoda źródłowa
może zawierać mnóstwo skomplikowanego kodu i pojedynczy kiełek nowej metody.
Czasami nie jest jasne, dlaczego tylko ta praca odbywa się w jakimś innym miejscu, co
pozostawia metodę źródłową w dziwnym stanie. Przynajmniej wskazuje to na jakąś
dodatkową pracę, którą możesz wykonać, gdy później poddajesz testom klasę źródłową.
Chociaż sposób ten ma swoje wady, posiada on także kilka istotnych zalet. Kiedy
korzystasz z kiełkowania metody, czytelnie oddzielasz nowy kod od starego. Nawet jeśli
nie możesz bezzwłocznie poddać starego kodu testom, będziesz mógł przynajmniej w ode-
rwaniu zaobserwować wprowadzone przez siebie zmiany i uzyskać wyraźną granicę mię-
dzy nowym a starym kodem. Zobaczysz wszystkie zmienne, które zostały użyte, co może
ułatwić stwierdzenie, czy kod znajduje się we właściwym kontekście.

Kiełkowanie klasy
Kiełkowanie metody jest wydajną techniką, ale w niektórych sytuacjach — na przykład
przy skomplikowanych zależnościach — nie wystarcza.
KIEŁKOWANIE KLASY 81

Zastanów się nad sytuacją, w której musisz dokonać zmian w klasie, ale po prostu nie
masz możliwości utworzenia w rozsądnym czasie obiektów tej klasy w jarzmie testowym,
a tym samym nie możesz wykiełkować metody ani napisać dla niej testów w tej klasie. Być
może masz do czynienia z dużym zbiorem zależności tworzeniowych, które w poważnym
stopniu utrudniają tworzenie instancji Twojej klasy. Możesz także mieć poukrywane
zależności. Aby się ich pozbyć, musiałbyś przeprowadzić sporo inwazyjnej refaktoryzacji
w celu odseparowania ich w stopniu wystarczającym do skompilowania klasy w jarzmie
testowym.
W takich przypadkach możesz utworzyć kolejną klasę, która będzie przechowywać
wprowadzane przez Ciebie zmiany, i użyć jej w miejsce klasy źródłowej. Przyjrzyjmy się
uproszczonemu przykładowi.
Oto stareńka metoda w klasie C++, o nazwie QuaterlyReportGenerator:
std::string QuarterlyReportGenerator::generate()
{
std::vector<Result> results = database.queryResults(
beginDate, endDate);
std::string pageText;

pageText += "<html><head><title>"
"Raport kwartalny"
"</title></head><body><table>";
if (results.size() != 0) {
for (std::vector<Result>::iterator it = results.begin();
it != results.end();
++it) {
pageText += "<tr>";
pageText += "<td>" + it->wydział + "</td>";
pageText += "<td>" + it->kierownik + "</td>";
char buffer [128];
sprintf(buffer, "<td>$%d</td>", it->netProfit / 100);
pageText += std::string(buffer);
sprintf(buffer, "<td>$%d</td>", it->operatingExpense / 100);
pageText += std::string(buffer);
pageText += "</tr>";
}
} else {
pageText += "Brak wyników dla wskazanego okresu ";
}
pageText += "</table>";
pageText += "</body>";
pageText += "</html>";

return pageText;
}

Załóżmy, że zmiana, jaką musimy wprowadzić w kodzie, polega na wstawieniu wiersza


nagłówkowego do tabeli HTML, którą ten kod tworzy. Wiersz nagłówka powinien
wyglądać mniej więcej tak:
"<tr><td>Wydział</td><td>Kierownik</td><td>Zysk</td><td>Wydatki</td></tr>"
82 ROZDZIAŁ 6. NIE MAM ZBYT WIELE CZASU, A MUSZĘ TO ZMIENIĆ

Przyjmijmy ponadto, że klasa jest ogromna i że poddanie jej testom w jarzmie zabrało-
by cały dzień, a na coś takiego nie możemy sobie akurat teraz pozwolić.
Moglibyśmy sformułować naszą zmianę w postaci małej klasy o nazwie QuaterlyReport
TableHeaderProducer i opracować ją, korzystając z techniki programowania stero-
wanego testami (104).
using namespace std;

class QuarterlyReportTableHeaderProducer
{
public:
string makeHeader();
};

string QuarterlyReportTableProducer::makeHeader()
{
return "<tr><td>Wydział</td><td>Kierownik</td>"
"<td>Zysk</td><td>Wydatki</td>";
}

Gdy już ją mamy, będziemy mogli utworzyć jej instancję i wywołać ją bezpośrednio
w metodzie QuarterlyReportGenerator::generate():
...
QuarterlyReportTableHeaderProducer producer;
pageText += producer.makeHeader();
...

Jestem pewien, że patrzysz w tej chwili na to wszystko i myślisz: „On jest niepoważny.
To śmieszne, żeby do takiej zmiany tworzyć klasę. To jest tylko malutka klasa, która nie
wnosi żadnych korzyści do projektu. Wprowadza za to zupełnie nową koncepcję, która
zaśmieca kod”. Cóż, na tym etapie istotnie tak jest. Jedynym powodem, dla którego to ro-
bimy, jest pozbycie się ciężkiego przypadku zależności, ale przyjrzyjmy się temu bliżej.
A gdybyśmy nazwali naszą klasę QuarterlyReportTableHeaderGenerator i dali jej taki
interfejs:
class QuarterlyReportTableHeaderGenerator
{
public:
string generate();
};

Teraz klasa ta jest częścią koncepcji, z którą jesteśmy już zaznajomieni. QuarterlyReport
TableHeaderGenerator jest generatorem, tak samo jak QuarterlyReportGenerator.
Obie zawierają metodę generate(), która zwraca łańcuchy tekstowe. Możemy udoku-
mentować tę wspólną cechę za pomocą kodu, tworząc klasę interfejsową i pozwalając, aby
obie wspomniane klasy z niej dziedziczyły:
class HTMLGenerator
{
public:
virtual ~HTMLGenerator() = 0;
KIEŁKOWANIE KLASY 83

virtual string generate() = 0;


};

class QuarterlyReportTableHeaderGenerator : public HTMLGenerator


{
public:
...
virtual string generate();
...
};

class QuarterlyReportGenerator : public HTMLGenerator


{
public:
...
virtual string generate();
...
};

Jeśli jeszcze trochę popracujemy, być może uda nam się poddać klasę QuarterlyReport
Generator testom i zmienić jej implementację w taki sposób, aby wykonywała większość
swoich zadań za pomocą klas generatora.
W tym przypadku udało nam się szybko odwzorować klasę na zbiór koncepcji, które
już istniały w naszej aplikacji. W wielu innych sytuacjach nie mamy takiej możliwości,
ale nie oznacza to, że powinniśmy z tego rezygnować. Niektóre wykiełkowane klasy nigdy
nie przekładają się na główne koncepcje w aplikacji — zamiast tego stają się nowymi
koncepcjami. Możesz wykiełkować klasę i uważać, że nie ma ona większego znaczenia
w Twoim projekcie, dopóki nie zrobisz czegoś podobnego w innym miejscu i nie dostrze-
żesz tego podobieństwa. Czasami masz możliwość dokonania faktoryzacji powielonego
kodu na nowe klasy, a czasami musisz pozmieniać ich nazwy, ale nie oczekuj, że to
wszystko stanie się od razu.
Sposób, w jaki postrzegasz wykiełkowaną klasę, kiedy ją tworzysz, i Twoje postrzeganie
tej samej klasy kilka miesięcy później często znacznie się między sobą różnią. Fakt, że masz
w systemie nową, dziwną klasę, daje Ci sporo do myślenia. Kiedy musisz blisko niej
wprowadzić zmianę, możesz zacząć zastanawiać się, czy modyfikacja ta jest częścią nowej
koncepcji, czy też istniejąca koncepcja musi ulec nieznacznej zmianie. Wszystko to jest
częścią będącego w toku procesu projektowania.
Oba te przypadki prowadzą nas zasadniczo do kiełkowania klasy. W pierwszym przy-
padku zmiany spowodowały dodanie do jednej z Twoich klas całkowicie nowej funk-
cjonalności. Na przykład w programie podatkowym określone zmniejszenia podstawy
opodatkowania mogą być niemożliwe do wykonania w pewnych momentach roku.
Wiesz, jak do klasy TaxCalculator dodać sprawdzanie daty, ale czy taka funkcja nie leży
poza główną kompetencją tej klasy, którą jest obliczanie podatku? Może potrzebne jest
utworzenie nowej klasy? Inny przypadek pojawił się w tym rozdziale. Mamy niewielką
funkcjonalność, którą moglibyśmy umieścić w istniejącej klasie, ale nie możemy pod-
dać tej klasy testom w jarzmie. Gdybyśmy tylko mogli ją skompilować w jarzmie, mo-
glibyśmy spróbować kiełkowania metody, ale czasami nie mamy nawet tyle szczęścia.
84 ROZDZIAŁ 6. NIE MAM ZBYT WIELE CZASU, A MUSZĘ TO ZMIENIĆ

Tym, na co warto zwrócić uwagę w tych dwóch przypadkach, jest fakt, że chociaż mo-
tywacje były różne, to tak naprawdę nie widać znaczącej różnicy między uzyskanymi wy-
nikami. Podjęcie decyzji, czy nowa funkcjonalność jest wystarczająco silna, aby stała
się nową klasą, jest kwestią indywidualnej oceny. Co więcej, ze względu na fakt, że kod
zmienia się wraz z upływem czasu, decyzja o kiełkowaniu klasy często wygląda lepiej z per-
spektywy czasowej.
Oto kroki prowadzące do wykiełkowania klasy:
1. Zidentyfikuj miejsce, w którym należy zmienić kod.
2. Jeśli zmianę można sformułować w postaci pojedynczej sekwencji instrukcji,
umieszczonej w pewnym miejscu metody, zastanów się nad dobrą nazwą dla
klasy, która może wykonać to zadanie. Następnie napisz w tym właśnie miejscu
kod, który utworzy obiekt tej klasy i wywoła metodę wykonującą potrzebną pracę,
po czym przekształć wiersze tego kodu w komentarz.
3. Sprawdź, które lokalne zmienne ze źródłowej metody będą potrzebne, i zrób z nich
argumenty konstruktora klasy.
4. Określ, czy wykiełkowana klasa powinna zwracać wartości metodzie źródłowej.
Jeśli tak, umieść w tej klasie metodę, która dostarcza owe wartości, i dodaj w meto-
dzie źródłowej wywołanie je pobierające.
5. W pierwszej kolejności opracuj test dla wykiełkowanej klasy (patrz metoda pro-
gramowania sterowanego testami na stronie 104).
6. Usuń komentarze z metody źródłowej, aby umożliwić utworzenie obiektu i wy-
wołania.

Zalety i wady
Główna zaleta kiełkowania klasy wynika z faktu, że możesz posuwać się z pracą do
przodu z większą pewnością, niż mógłbyś ją mieć, gdybyś wprowadzał inwazyjne zmiany.
W przypadku C++ kiełkowanie klasy daje przewagę polegającą na tym, że nie musisz
modyfikować żadnych istniejących plików nagłówkowych w celu wprowadzenia zmiany.
Nagłówek nowej klasy możesz dołączyć do pliku implementacyjnego klasy źródłowej.
Ponadto dodawanie do projektu nowego pliku nagłówkowego ma dobre strony. Wraz
z upływem czasu umieścisz deklaracje, które mogłyby się znaleźć w nagłówku klasy
źródłowej, w nowym pliku nagłówkowym. W ten sposób zmniejsza się obciążenie
kompilacyjne tej klasy. Przynajmniej będziesz wiedzieć, że nie pogarszasz sytuacji,
która i tak już jest zła. Jakiś czas później będziesz mógł poprawić klasę źródłową i poddać
ją testom.
Najważniejszą wadą kiełkowania klasy jest koncepcyjna złożoność tej metody. W miarę
jak programiści poznają nowe bazy kodu, wyrabiają w sobie wizję wspólnego działania
kluczowych klas. Kiedy korzystasz z kiełkowania klasy, zaczynasz niszczyć abstrakcje,
a większość prac wykonujesz w innych klasach. Czasami jest to jak najbardziej właściwe
OPAKOWYWANIE METODY 85

rozwiązanie, ale kiedy indziej posuwasz się do tej metody tylko dlatego, że zostałeś przyci-
śnięty do muru. Wszystko to, co w idealnej sytuacji pozostałoby w jednej klasie, zostaje
rozkiełkowane tylko po to, aby możliwe stało się dokonywanie bezpiecznych zmian.

Opakowywanie metody
Dodawanie zachowania do istniejących metod jest łatwe, ale często nie jest właściwym po-
stępowaniem. Kiedy początkowo tworzysz metodę, zwykle robi ona dla swojego klienta
tylko jedną rzecz. Każdy kolejny kod, który do niej dodajesz jest w pewnym sensie podej-
rzany. Istnieje prawdopodobieństwo, że robisz to tylko dlatego, że metoda i dodawany
kod muszą wykonywać się w tym samym czasie. We wczesnych latach programowania
taki zabieg nazywał się chwilowym sprzężeniem i był dość paskudnym rozwiązaniem,
gdy stosowano go zbyt często. Kiedy grupujesz ze sobą elementy kodu tylko dlatego, że
mają wykonywać się w tym samym czasie, relacja między nimi nie jest zbyt silna. Później
może się okazać, że jedną z tych rzeczy należy wykonać w oderwaniu od drugiej, ale na
tym etapie są one już ze sobą zrośnięte. Rozdzielenie ich bez znalezienia spoiny może
być trudne.
Kiedy musisz dodać zachowanie, możesz to zrobić w mniej zawikłany sposób. Jedną
z technik, których możesz użyć, jest kiełkowanie metody, ale istnieje jeszcze jeden sposób,
przydatny w niektórych sytuacjach. Nazywam go opakowywaniem metody. Oto prosty
przykład:
public class Employee
{
...
public void pay() {
Money amount = new Money();
for (Iterator it = timecards.iterator(); it.hasNext(); ) {
Timecard card = (Timecard)it.next();
if (payPeriod.contains(date)) {
amount.add(card.getHours() * payRate);
}
}
payDispatcher.pay(this, date, amount);
}
}

W metodzie tej dodajemy do siebie dzienne karty czasu pracy pracownika, a następnie
wysyłamy informację o jego wynagrodzeniu do obiektu PayDispatcher. Załóżmy, że poja-
wiły się nowe wymagania. Za każdym razem, kiedy płacimy pracownikowi, musimy zapi-
sać w pliku jego nazwisko, dzięki czemu plik ten będzie można wysłać do jakiegoś progra-
mu raportującego. Najprościej nowy kod można umieścić w metodzie naliczającej płacę.
W końcu odbywa się to w tym samym czasie, prawda? A co, jeśli w zamian zrobimy
coś takiego:
86 ROZDZIAŁ 6. NIE MAM ZBYT WIELE CZASU, A MUSZĘ TO ZMIENIĆ

public class Employee


{
private void dispatchPayment() {
Money amount = new Money();
for (Iterator it = timecards.iterator(); it.hasNext(); ) {
Timecard card = (Timecard)it.next();
if (payPeriod.contains(date)) {
amount.add(card.getHours() * payRate);
}
}
payDispatcher.pay(this, date, amount);
}

public void pay() {


logPayment();
dispatchPayment();
}

private void logPayment() {


...
}
}

W powyższym kodzie zmieniłem nazwę metody pay() na dispatchPayment() i ją spry-


watyzowałem. Następnie utworzyłem nową metodę pay(), która ją wywołuje. Nasza nowa
metoda rejestruje wypłatę, po czym ją wysyła. Klienty, które wywoływały metodę pay(),
nie muszą wiedzieć o tej zmianie ani się nią przejmować. Po prostu dokonują swoich wy-
wołań i wszystko przebiega jak należy.
Jest to jedna z postaci opakowywania metody. Tworzymy metodę o nazwie, jaką ma
oryginalna metoda, i umieszczamy ją w naszym starym kodzie. Ze sposobu tego korzy-
stamy, gdy chcemy dodać nowe zachowanie do istniejących wywołań oryginalnej metody.
Jeśli chcemy, aby przy każdym wywołaniu metody pay() dokonywało się zapisywanie,
technika ta może być bardzo przydatna.
Oto kolejna postać opakowywania metody, której możemy użyć, kiedy chcemy dodać
nową metodę — taką, której jeszcze nikt nie wywołuje. Gdybyśmy w poprzednim przy-
kładzie chcieli, aby zapisywanie wypłat odbywało się jawnie, moglibyśmy dodać do klasy
Employee metodę makeLoggedPayment:
public class Employee
{
public void makeLoggedPayment() {
logPayment();
pay();
}

public void pay() {


...
}

private void logPayment() {


OPAKOWYWANIE METODY 87

...
}
}

Teraz użytkownicy mają możliwość wyboru sposobu płacenia. Przykład ten został
opisany przez Kenta Becka w książce Smalltalk Patterns: Best Practices (Pearson Edu-
cation 1996).
Opakowywanie metody jest świetnym sposobem na wprowadzanie spoin podczas
dodawania nowych funkcjonalności. Ma on tylko kilka wad. Pierwsza polega na tym, że
nowa funkcja, którą dodajesz, nie może przeplatać się z logiką starej funkcji. Należy ją
umieścić przed dotychczasową funkcjonalnością albo po niej. Chwileczkę! Czy powie-
działem, że to coś złego? Otóż nie. Zrób tak, kiedy możesz. Druga (i bardziej realna) wada
sprowadza się do tego, że musisz wymyślić nową nazwę dla starego kodu, który znajduje
się już w metodzie. W tym przypadku kodowi w metodzie pay() nadałem nazwę
dispatchPayment(), co można przetłumaczyć jako „wyślij wynagrodzenie”. Jest to trochę
naciągane i szczerze mówiąc, nie podoba mi się postać, jaką ostatecznie przybrał kod
w tym programie. Metoda dispatchPayment() tak naprawdę nie tylko wysyła wynagro-
dzenie, ale też je oblicza. Gdybym przeprowadzał testy, prawdopodobnie wydzieliłbym
z pierwszej części metody dispatchPayment() odrębną metodę o nazwie calculatePay(),
a metoda pay() wyglądałaby następująco:
public void pay() {
logPayment();
Money amount = calculatePay();
dispatchPayment(amount);
}

Teraz wygląda na to, że wszystkie kompetencje zostały rozdzielone poprawnie.


Oto czynności, które należy wykonać w pierwszej wersji opakowywania metody:
1. Zidentyfikuj metodę, którą musisz zmienić.
2. Jeśli zmianę można sformułować w postaci pojedynczej sekwencji instrukcji,
umieszczonej w pewnym miejscu, zmień nazwę starej metody, po czym utwórz
nową metodę o takiej samej nazwie i sygnaturze, jaką miała stara metoda. Pamiętaj
o zachowaniu sygnatur (314), gdy to robisz.
3. W nowej metodzie dodaj wywołanie starej metody.
4. Opracuj metodę realizującą nową funkcjonalność, przetestuj ją (patrz technika
programowania sterowanego testami (104)), a następnie wywołaj nową metodę.
W drugiej wersji nie dbamy o użycie takiej samej nazwy, jaką ma stara metoda, tak
więc czynności są następujące:
1. Zidentyfikuj metodę, którą musisz zmienić.
2. Jeśli zmianę można sformułować w postaci pojedynczej sekwencji instrukcji,
umieszczonej w pewnym miejscu, opracuj metodę realizującą nową funkcjonal-
ność, korzystając z techniki programowania sterowanego testami (104).
3. Utwórz kolejną metodę, która wywołuje zarówno starą, jak i nową metodę.
88 ROZDZIAŁ 6. NIE MAM ZBYT WIELE CZASU, A MUSZĘ TO ZMIENIĆ

Zalety i wady
Opakowywanie metody stanowi dobry sposób na utworzenie w aplikacji nowej, przete-
stowanej funkcjonalności, kiedy nie możemy w prosty sposób poddać testom kodu, który
ją wywołuje. Kiełkowanie metody i kiełkowanie klasy dodają do istniejących metod kod
i wydłużają je co najmniej o jeden wiersz, podczas gdy opakowywanie metody nie zwiększa
rozmiaru starych metod.
Kolejna zaleta opakowywania metody polega na tym, że nowa funkcjonalność jest
tworzona w sposób wyraźnie niezależny od istniejącej już funkcjonalności. Kiedy opa-
kowujesz metodę, nie przeplatasz kodu służącego do realizacji jednego celu z kodem słu-
żącym czemuś innemu.
Główną wadą opakowywania metody jest to, że prowadzi ona do kiepskich nazw.
W poprzednim przykładzie zmieniliśmy nazwę metody pay() na dispatchPay() tylko
dlatego, że potrzebowaliśmy innej nazwy dla kodu znajdującego się w starej metodzie.
Gdyby tylko nasz kod nie był wyjątkowo delikatny albo złożony lub gdybyśmy dyspono-
wali narzędziem do refaktoryzacji, które bezpiecznie wyodrębnia metody (411), mo-
glibyśmy powydzielać jeszcze trochę metod i uzyskać lepsze nazwy. Jednak w wielu przy-
padkach opakowujemy metody, gdyż nie mamy żadnych testów, kod jest kruchy i nie
mamy odpowiednich narzędzi.

Opakowywanie klasy
Odpowiednikiem opakowywania metody na poziomie klasy jest opakowywanie klasy.
Opakowywanie klasy bazuje na bardzo podobnym pomyśle. Jeżeli musimy dodać zacho-
wanie do systemu, możemy je wstawić do istniejącej metody, ale możemy je także dodać
do jakiegoś innego elementu, który korzysta z tej metody. W przypadku opakowywania
klasy elementem tym jest inna klasa.
Ponownie rzućmy okiem na kod w klasie Employee.
class Employee
{
public void pay() {
Money amount = new Money();
for (Iterator it = timecards.iterator(); it.hasNext(); ) {
Timecard card = (Timecard)it.next();
if (payPeriod.contains(date)) {
amount.add(card.getHours() * payRate);
}
}
payDispatcher.pay(this, date, amount);
}
...
}
OPAKOWYWANIE KLASY 89

Chcemy rejestrować fakt wypłaty wynagrodzenia konkretnemu pracownikowi. Jedna


z rzeczy, które możemy zrobić, to utworzenie kolejnej klasy zawierającej metodę pay().
Obiekty tej klasy będą mogły przyjmować pracownika, rejestrować wypłaty w metodzie
pay(), a następnie odwoływać się do klasy Employee w celu dokonania wypłaty. Często
najprostszym sposobem realizacji takiego zadania — gdy nie masz możliwości utworzenia
instancji oryginalnej klasy w jarzmie testowym — jest skorzystanie w odniesieniu do tej
klasy z techniki wyodrębniania implementera (356) albo wyodrębniania interfejsu (361)
i uzyskanie potrzebnego interfejsu za pomocą opakowywania klasy.
W poniższym kodzie użyliśmy techniki wydzielania implementera, aby przekształcić
klasę Employee w interfejs. Teraz klasę tę implementuje nowa klasa — LoggingEmployee.
Możemy do niej przekazać dowolny obiekt klasy Employee, dzięki czemu wynagrodzenie
zostanie zarówno zarejestrowane, jak i wypłacone.
class LoggingEmployee extends Employee
{
public LoggingEmployee(Employee e) {
employee = e;
}

public void pay() {


logPayment();
employee.pay();
}

private void logPayment() {


...
}
...
}

Technika ta nazywana jest wzorcem dekoratora. Tworzymy obiekty klasy, która


opakowuje inną klasę, i przekazujemy je dalej. Klasa, która opakowuje, powinna mieć taki
sam interfejs jak klasa opakowywana, dzięki czemu klienty nie będą wiedzieć, że pracują
z opakowaniem. W naszym przykładzie LoggingEmployee jest dekoratorem klasy Employee.
Musi zawierać metodę pay(), a także wszystkie inne metody klasy Employee, z których
korzystają klienty.

Wzorzec dekoratora
Dekorator umożliwia tworzenie złożonych zachowań poprzez łączenie obiektów w czasie
działania programu. Na przykład w przypadku przemysłowego systemu do kontroli proce-
sów moglibyśmy mieć klasę o nazwie ToolController z takimi metodami, jak: raise(),
lower(), step(), on() i off(). Gdybyśmy potrzebowali dodatkowych akcji w czasie działania
metod raise() albo lower() — takich jak na przykład włączenie słyszalnych alarmów, aby
pracownicy mogli zejść z drogi — moglibyśmy umieścić nowe funkcjonalności bezpośrednio
w metodach klasy ToolController. Istnieje jednak prawdopodobieństwo, że na tym poprawki
by się nie skończyły. Mogłaby na przykład zajść potrzeba rejestrowania, ile razy włączamy
90 ROZDZIAŁ 6. NIE MAM ZBYT WIELE CZASU, A MUSZĘ TO ZMIENIĆ

i wyłączamy kontroler. Może też okazać się konieczne powiadamianie innych kontrolerów
o tym, że wykonujemy krok, aby zapobiec sytuacji, w której wykonują one krok w tym samym
czasie co my. Lista czynności, które można zrealizować łącznie z naszymi pięcioma prostymi
operacjami — uniesieniem, opuszczeniem, krokiem, włączeniem i wyłączeniem, repre-
zentowanymi odpowiednio przez metody raise(), lower(), step(), on() i off() — jest nie-
skończona, a utworzenie podklasy dla każdej możliwej kombinacji zdarzeń jest nierealne.
Wzorzec dekoratora idealnie nadaje się do rozwiązywania tego rodzaju problemów. Kiedy
korzystasz z dekoratora, tworzysz abstrakcyjną klasę, która definiuje zbiór operacji wymagają-
cych wsparcia. Następnie definiujesz podklasę, która dziedziczy z tej klasy abstrakcyjnej,
przyjmuje jej instancję w swoim konstruktorze i udostępnia ciało dla każdej z metod. Oto
klasa rozwiązująca problem klasy ToolController.
abstract class ToolControllerDecorator extends ToolController
{
protected ToolController controller;
public ToolControllerDecorator(ToolController controller) {
this.controller = controller;
}
public void raise() { controller.raise(); }
public void lower() { controller.lower(); }
public void step() { controller.step(); }
public void on() { controller.on(); }
public void off() { controller.off(); }
}

Klasa ta być może nie wygląda na szczególnie przydatną, ale jednak taka jest. Możesz two-
rzyć jej podklasy i przesłaniać dowolną lub wszystkie jej metody w celu dodania nowego
zachowania. Jeżeli na przykład potrzebujemy powiadomić inne kontrolery, że wykonuje-
my krok, moglibyśmy utworzyć klasę StepNotifyController, która wygląda następująco:
public class StepNotifyingController extends ToolControllerDecorator
{
private List notifyees;
public StepNotifyingController(ToolController controller,
List notifyees) {
super(controller);
this.notifyees = notifyees;
}
public void step() {
// tu powiadom wszystkich odbiorców
...
controller.step();
}
}

Naprawdę miła cecha takiego rozwiązania polega na tym, że możemy zagnieżdżać podkla-
sy dekoratora ToolControllerDecorator:
ToolController controller = new StepNotifyingController(
new AlarmingController
(new ACMEController()), notifyees);
OPAKOWYWANIE KLASY 91

Kiedy w kontrolerze wykonujemy taką operację jak step(), powiadamia on wszystkich odbior-
ców, włącza alarm i wykonuje krok. Ta ostatnia czynność, polegająca na wykonaniu kroku,
zachodzi w ACMEController, która jest w rzeczywistości podklasą ToolConroller, a nie
ToolControllerDecorator. Nie przekazuje ona niczego nikomu; wykonuje po prostu każdą
z czynności kontrolera. Kiedy używasz wzorca dekoratora, musisz mieć przynajmniej jedną
z tych „podstawowych” klas, które opakowujesz.
Dekorator to przydatny wzorzec, ale należy z niego korzystać z umiarem. Poruszanie się po
kodzie zawierającym dekoratory dekorujące inne dekoratory bardzo przypomina obieranie
cebuli z kolejnych warstw. Taką pracę trzeba wykonać, ale będziesz przy niej płakać.

Jest to świetny sposób dodawania funkcjonalności, kiedy masz wiele istniejących


obiektów, wywołujących takie metody jak pay(). Jest jednak jeszcze inny sposób opa-
kowywania, który nie jest aż tak „dekoracyjny”. Spójrzmy na przypadek, w którym
potrzebujemy rejestrować wywołania metody pay() tylko w jednym miejscu. Zamiast
opakowywać tę funkcjonalność pod postacią dekoratora, możemy dodać kolejną klasę,
która przyjmuje pracownika, dokonuje płatności, po czym zapisuje o tym informację.
Oto niewielka klasa, która właśnie to robi:
class LoggingPayDispatcher
{
private Employee e;
public LoggingPayDispatcher(Employee e) {
this.e = e;
}

public void pay() {


employee.pay();
logPayment();
}

private void logPayment() {


...
}
...
}

Teraz możemy utworzyć metodę LogPayDispatcher tylko w tym miejscu, w którym


musimy rejestrować płatności.
Najważniejsze w technice opakowywania klas jest to, że możesz dodać w systemie
nowe zachowanie bez umieszczania go w istniejącej klasie. Jeżeli istnieje wiele wywołań
kodu, który chcesz opakować, często opłaca się wybrać opakowania bardziej podobne do
dekoratora. Gdy korzystasz ze wzorca dekoratora, możesz w przejrzysty sposób jedno-
cześnie dodać nowe zachowanie do istniejącego zbioru wszystkich wywołań takich jak
pay(). Z drugiej jednak strony, jeśli nowe zachowanie ma być dodane tylko w niektórych
miejscach, utworzenie opakowania, które nie będzie dekoratorem, może być bardzo
przydatne. Wraz z upływem czasu powinieneś zacząć zwracać uwagę na role odgrywane
92 ROZDZIAŁ 6. NIE MAM ZBYT WIELE CZASU, A MUSZĘ TO ZMIENIĆ

przez opakowanie i sprawdzać, czy mogłoby ono stać się kolejną, wysokopoziomową kon-
cepcją w Twoim systemie.
Oto czynności związane z opakowywaniem klasy:
1. Zidentyfikuj metodę, w której należy wprowadzić zmiany.
2. Jeśli zmianę można sformułować w postaci pojedynczej sekwencji instrukcji,
umieszczonej w pewnym miejscu, utwórz klasę przyjmującą jako argument
konstruktora klasę, którą zamierzasz opakować. Jeżeli masz problemy z utworze-
niem w jarzmie testowym klasy opakowującej oryginalną klasę, być może będziesz
musiał skorzystać w odniesieniu do opakowywanej metody z techniki wyodręb-
niania implementera (356) albo wyodrębniania interfejsu (361), dzięki czemu
będziesz mógł stworzyć instancję swojego opakowania.
3. Za pomocą techniki programowania sterowanego testami (104) utwórz w tej kla-
sie metodę, która wykonuje potrzebną pracę. Napisz jeszcze jedną metodę, któ-
ra wywołuje zarówno nową, jak i starą metodę dla opakowanej klasy.
4. W miejscu, w którym nowe zachowanie ma być aktywne, utwórz w kodzie instancję
klasy opakowującej.
Różnica między kiełkowaniem metody a opakowywaniem metody jest raczej błaha.
Kiełkowanie metody zastosujesz wtedy, gdy musisz napisać nową metodę i wywołać ją
z metody istniejącej. Z kolei z opakowywania metody korzystasz, jeśli wybrałeś zmianę
nazwy metody i zastąpienie jej nową metodą, która wykonuje nowe zadania i wywołuje
starą metodę. Zwykle korzystam z kiełkowania metody, kiedy kod, który mam w istniejącej
metodzie, przekazuje swojemu czytelnikowi czytelny algorytm. Przechodzę do opakowy-
wania metody wtedy, gdy sądzę, że nowa funkcja, którą dodaję, jest równie ważna jak ta,
która istniała tam wcześniej. W takiej sytuacji — kiedy już przeprowadzę opakowywanie
— często otrzymuję nowy algorytm wysokiego poziomu, taki jak na przykład ten:
public void pay() {
logPayment();
Money amount = calculatePay();
dispatchPayment(amount);
}

Podjęcie decyzji o skorzystaniu z opakowywania klasy to już zupełnie inna sprawa.


Dla tego wzorca istnieje wyższy próg jego stosowania. W zasadzie istnieją dwa przypadki,
przy których skłaniam się ku opakowywaniu klas:
1. Zachowanie, które chcę dodać, jest w pełni niezależne, a ja nie chcę zanieczyszczać
istniejącej klasy zachowaniem, które jest niskopoziomowe albo nie ma z nią żadnego
związku.
2. Klasa jest już tak rozrośnięta, że naprawdę nie zniósłbym, gdyby miała się zrobić
jeszcze większa. Wtedy opakowuję klasę tylko po to, aby położyć kres tej sytuacji
i wyznaczyć kierunek dla kolejnych zmian.
PODSUMOWANIE 93

Druga operacja jest dość ciężka do przeprowadzenia i trudno się z nią pogodzić. Jeśli
masz dużą klasę, która realizuje na przykład 10 albo 15 różnych zadań, może wydawać się
nieco dziwne opakowywanie jej tylko po to, aby dodać do niej jakąś błahą funkcjonalność.
W rzeczywistości, jeśli nie potrafisz przedstawić swoim kolegom jakiegoś przekonującego
argumentu, może się zdarzyć, że zostaniesz pobity na parkingu albo — co gorsza — bę-
dziesz ignorowany przez resztę swojej kariery zawodowej. Pozwól zatem, że pomogę
Ci wymyślić taki argument.
Największą przeszkodą we wprowadzaniu poprawek w dużej bazie kodu jest istniejący
w niej kod. „Naprawdę?”, mógłbyś zapytać. Nie mówię jednak o tym, jak ciężko jest
pracować w trudnym kodzie. Mówię o postawie, do której przyjęcia skłania Cię taki kod.
Jeśli większość swojego dnia spędzasz, przedzierając się przez nieprzyjemny kod, bardzo
łatwo przyjdzie Ci uwierzyć, że już zawsze taki on będzie i że nie warto wprowadzać w nim
nawet najdrobniejszych poprawek. Możesz pomyśleć: „Czy to ma jakieś znaczenie, że
poprawię ten malutki fragment, jeśli przez 90 procent czasu nadal będę się grzebać w tym
cuchnącym bagnie? Oczywiście, że mógłbym ulepszyć to miejsce, ale co z tego będę miał
dzisiaj albo jutro?”. Cóż, jeśli patrzysz na to w ten sposób, to muszę przyznać Ci rację.
Niewiele Ci z tego przyjdzie. Ale jeśli będziesz konsekwentnie wprowadzać niewielkie
poprawki, to w ciągu kilku miesięcy Twój system zacznie wyglądać zdecydowanie inaczej.
Nadejdzie wreszcie taki poranek, że zjawisz się w pracy gotów na zanurzenie rąk w błocku
i dokonasz pewnego odkrycia: „Hej, ten kod wygląda nieźle. Wydaje się, że ktoś tu coś
niedawno refaktoryzował”. W tym momencie, gdy głęboko w trzewiach poczujesz różnicę
między dobrym a złym kodem, staniesz się inną osobą. Być może nawet okaże się, że
chcesz refaktoryzować o wiele więcej, niż jest to potrzebne do ukończenia zadania, tylko
po to, aby ułatwić sobie życie. Wszystko to może to brzmieć dla Ciebie niedorzecznie, jeśli
nigdy czegoś takiego nie doświadczyłeś, ale wiele razy byłem świadkiem, jak to się odbywa
w zespołach. Najtrudniejszą częścią jest wykonanie kilku wstępnych czynności, ponieważ
czasami wydaje się, że są one głupie. „Co? Opakować klasę tylko w celu dodania tej małej
funkcji? Teraz to wygląda gorzej niż przedtem. Jest bardziej skomplikowane”. Tak, na razie
jest skomplikowane. Kiedy jednak naprawdę zaczniesz rozbijać te 10 lub 15 zakresów
odpowiedzialności, istniejących w opakowanej klasie, zacznie ona wyglądać o wiele lepiej.

Podsumowanie
W rozdziale tym przedstawiłem w zarysie techniki, z których możesz skorzystać, aby
wprowadzać zmiany w kodzie bez poddawania istniejących klas testom. Z perspektywy
projektu trudno stwierdzić, co o nich myśleć. W wielu przypadkach umożliwiają one
odsunięcie nowych, odrębnych funkcjonalności od już istniejących. Innymi słowy, za-
czynamy zmierzać w stronę lepszego projektu. Jednak w innych sytuacjach wiemy, że
utworzyliśmy klasę tylko dlatego, że chcieliśmy napisać nowy kod razem z testami, a nie
byliśmy przygotowani na to, żeby poświęcić czas na poddanie testom istniejącej klasy.
94 ROZDZIAŁ 6. NIE MAM ZBYT WIELE CZASU, A MUSZĘ TO ZMIENIĆ

Jest to całkiem realna sytuacja. Kiedy ludzie zaczynają postępować w ten sposób w swoich
projektach, widać, jak nowe klasy i metody wyrastają wokół trucheł starych i dużych klas.
Wtedy zaczyna się dziać coś ciekawego. Po jakimś czasie ludzie mają już dość omijania
tych trucheł i zaczynają poddawać je testom. Część tego zachowania wynika z zaznajo-
mienia się ze starym kodem. Jeśli co chwilę musisz przyglądać się tej dużej, nieprzete-
stowanej klasie, aby stwierdzić, gdzie można ją wykiełkować, lepiej ją poznajesz. Staje się
ona mniej straszna. Inna część bierze się ze zwykłego zmęczenia. Jesteś po prostu znużony
oglądaniem śmieci leżących w Twoim salonie i chcesz się ich pozbyć. Dobrym punktem
wyjściowym będą rozdziały 9., „Nie mogę umieścić tej klasy w jarzmie testowym”, i 20.,
„Ta klasa jest za duża, a ja nie chcę, żeby stała się jeszcze większa”.
Rozdział 7.

Dokonanie zmiany
trwa całą wieczność

Ile czasu zajmuje wprowadzenie zmiany? Odpowiedzi mogą być diametralnie różne.
W przypadku projektów z wyjątkowo nieczytelnym kodem wiele zmian pochłania mnó-
stwo czasu. Musimy przedrzeć się przez kod, zrozumieć wszystkie rozgałęzienia związane
ze zmianą, po czym ją wprowadzić. W czytelniejszych obszarach kodu może to się odby-
wać szybko, ale przejście przez miejsca naprawdę zagmatwane może zabrać dużo czasu.
Niektóre zespoły mają o wiele gorzej od innych. W ich przypadku implementacja nawet
najprostszej zmiany trwa bardzo długo. Ludzie w tych zespołach potrafią określić, jakie
funkcje należy dodać; dokładnie wskazać, gdzie wprowadzić zmiany; wejść w kod i go
zmodyfikować w ciągu pięciu minut, ale mimo tego przez wiele godzin nie są w stanie
opublikować dokonanych przez siebie zmian.
Spójrzmy na powody takiego stanu rzeczy i na możliwe rozwiązania.

Zrozumienie
Ilość kodu w projekcie zwiększa się i stopniowo przekracza granicę możliwości zrozumie-
nia. Ilość czasu potrzebnego do pojęcia, co należy zmienić, wciąż rośnie.
Części tego nie da się uniknąć. Kiedy dodajemy do systemu kod, możemy go dodać do
istniejących klas, metod albo funkcji lub też utworzyć nowe. W każdym z tych przypadków
zrozumienie, jak dokonać zmian, zabierze nieco czasu, jeśli nie znamy ich kontekstu.
Istnieje jednak różnica między systemem dobrze utrzymywanym (konserwowanym)
a systemem obcym. Zrozumienie, jak wprowadzić zmianę w systemie dobrze utrzymywa-
nym, może chwilę potrwać, ale samo dokonanie zmiany zwykle jest łatwe, a Ty dość
dobrze odnajdujesz się w systemie. Z kolei zrozumienie tego, co należy zrobić w cudzym
systemie, może trwać długo, a zmiana jest trudna. Możesz także mieć poczucie, że nie
dowiedziałeś się zbyt wiele ponad absolutne minimum niezbędne do przeprowadzenia
96 ROZDZIAŁ 7. DOKONANIE ZMIANY TRWA CAŁĄ WIECZNOŚĆ

modyfikacji. W najgorszych przypadkach wydaje się, że żadna ilość czasu nie będzie
wystarczająca do pojęcia wszystkiego, co potrzebne, aby wprowadzić zmianę, a Tobie
pozostanie już tylko wkroczyć na oślep w kod i mieć nadzieję, że poradzisz sobie ze
wszystkimi problemami, jakie napotkasz.
Systemy, które są podzielone na małe, dobrze nazwane i zrozumiałe fragmenty, umoż-
liwiają szybszą pracę. Jeśli zrozumienie stanowi w Twoim projekcie poważny problem, zaj-
rzyj do rozdziałów 16., „Nie rozumiem wystarczająco dobrze kodu, żeby go zmienić”, i 17.,
„Moja aplikacja nie ma struktury”, gdzie znajdziesz kilka pomysłów na to, jak w takiej
sytuacji przystąpić do działania.

Opóźnienie
Dokonywanie zmian nierzadko zabiera dużo czasu ze względu na kolejny często spotykany
powód, którym jest opóźnienie. Opóźnienie to czas, jaki upływa między wprowadzeniem
przez Ciebie zmiany a chwilą, w której otrzymujesz informację zwrotną na jej temat.
W chwili, w której piszę te słowa, marsjański łazik Spirit wlecze się po powierzchni Marsa,
robiąc zdjęcia. Potrzeba siedmiu minut, aby sygnały radiowe dotarły z Ziemi do Marsa.
Na szczęście Spirit jest wyposażony w oprogramowanie sterujące, które pomaga mu
w samodzielnym poruszaniu się. Wyobraź sobie, jak by to było, gdybyś musiał nim
ręcznie kierować z Ziemi. Dotykasz urządzeń sterowych i 14 minut później wiesz, jak
daleko zajechał łazik. Następnie decydujesz, co chcesz zrobić dalej, robisz to, i czekasz
kolejne 14 minut, aby się przekonać, co się stało. Proces ten wygląda na absurdalnie
wręcz nieefektywny, prawda? Kiedy jednak się nad tym zastanowisz, dojdziesz do wnio-
sku, że właśnie tak postępuje większość z nas podczas pracy nad kodem. Wprowadzamy
jakieś zmiany, rozpoczynamy budowanie, a następnie dowiadujemy się, co się stało.
Niestety, nie dysponujemy oprogramowaniem, które wie, jak poruszać się poprzez
przeszkody — takie jak niepowodzenia testów — pojawiające się podczas budowania.
Zamiast tego staramy się gromadzić zmiany razem i wprowadzać je wszystkie za jed-
nym zamachem, abyśmy nie musieli zbyt często budować. Jeśli nasze zmiany były dobre,
jedziemy dalej, chociaż równie wolno jak marsjański łazik. Jeśli uderzymy w przeszkodę,
poruszamy się nawet wolniej.
Smutnym aspektem takiej metody pracy jest fakt, że w wielu językach jest ona zu-
pełnie niepotrzebna. To całkowita strata czasu. W większości głównych języków progra-
mowania zawsze możesz usuwać zależności w sposób, który umożliwia rekompilację
i ponowne poddawanie testom kodu, nad którym akurat pracujesz, w mniej niż 10 se-
kund. Jeśli zespół jest dobrze motywowany, jego członkowie mogą w większości przypad-
ków skrócić ten czas tak, by wynosił mniej niż pięć sekund. Oto co jest do tego potrzebne:
powinieneś mieć możliwość skompilowania każdej klasy albo modułu swojego systemu
niezależnie od pozostałych elementów i w ich własnym jarzmie testowym. Jeśli możesz tak
robić, będziesz mógł otrzymywać błyskawiczną informację zwrotną, co najzwyczajniej
w świecie pomaga w szybszym programowaniu.
USUWANIE ZALEŻNOŚCI 97

Umysł ludzki posiada kilka interesujących właściwości. Jeśli powinniśmy zrealizować


krótkie zadanie (trwające od 5 do 10 sekund), a możemy zrobić tylko jeden krok co mi-
nutę, zwykle go wykonujemy, a następnie robimy przerwę. Jeżeli musimy przeprowadzić
pewne działania, aby określić, co należy zrobić w następnym kroku, zaczynamy planować.
Po planowaniu nasze umysły zaczynają swobodnie wędrować aż do chwili, w której
możemy wykonać kolejny krok. Jeśli uda nam się skrócić czas między poszczególnymi
krokami z minuty do kilku sekund, jakość naszej pracy umysłowej staje się inna. Możemy
korzystać z informacji zwrotnej, aby szybko wypróbowywać rozwiązania. Nasza praca
bardziej zaczyna przypominać prowadzenie samochodu niż oczekiwanie na przystanku
autobusowym. Koncentrujemy się intensywniej, ponieważ nie czekamy bezustannie na
kolejną okazję, żeby coś zrobić. Co ważniejsze, ilość czasu, jaką potrzebujemy, aby zauwa-
żyć i skorygować pomyłki, jest o wiele krótsza.
Co powstrzymuje nas przed możliwością pracy w taki właśnie sposób przez cały czas?
Niektórzy tak potrafią. Osoby programujące w językach interpretowanych mogą podczas
swojej pracy często otrzymywać niemal błyskawiczną informację zwrotną. Dla reszty z nas
— która pracuje w językach kompilowanych — główną przeszkodą są zależności; ko-
nieczność skompilowania czegoś, co nas nie interesuje, tylko dlatego, że musimy poddać
kompilacji coś innego.

Usuwanie zależności
Zależności mogą powodować problemy, ale na szczęście możemy je usuwać. W kodzie
zorientowanym obiektowo często pierwszym krokiem jest próba stworzenia w jarzmie
testowym instancji klas, które są nam potrzebne. W najprostszych przypadkach mo-
żemy to zrobić, importując lub dołączając deklaracje klas, od których jesteśmy zależni.
W przypadkach trudniejszych wypróbuj techniki opisane w rozdziale 9., „Nie mogę
umieścić tej klasy w jarzmie testowym”. Kiedy masz możliwość utworzenia obiektu danej
klasy w jarzmie testowym, może istnieć kolejna zależność do usunięcia, jeśli chcesz poddać
testom poszczególne metody. W takich przypadkach zajrzyj do rozdziału 10., „Nie mogę
uruchomić tej metody w jarzmie testowym”.
Jeżeli masz klasę, którą musisz zmienić w jarzmie testowym, możesz w ogólności sko-
rzystać z przewagi, jaką dają bardzo krótkie przebiegi edytuj-kompiluj-konsoliduj-testuj.
Zwykle koszt uruchomienia większości metod jest relatywnie niski w porównaniu z kosz-
tami metod, które są przez nie wywoływane, zwłaszcza jeśli wywołania te dotyczą za-
sobów zewnętrznych, takich jak bazy danych, sprzęt albo infrastruktura telekomunikacyjna.
Przypadki, kiedy tak nie jest, zdarzają się zwykle wtedy, gdy metody wykonują bardzo
dużo obliczeń. Techniki, które zarysowałem w rozdziale 22., „Muszę zmienić monstrualną
metodę, a nie mogę napisać do niej testów”, mogą być wówczas pomocne.
W wielu przypadkach zmiana może być dość prosta do przeprowadzenia, ale często
ludzie pracujący nad cudzym kodem są zatrzymywani w miejscu już na pierwszym etapie
98 ROZDZIAŁ 7. DOKONANIE ZMIANY TRWA CAŁĄ WIECZNOŚĆ

— próbie umieszczenia klasy w jarzmie testowym. W przypadku niektórych systemów


może to być sporym wyzwaniem. Niektóre klasy są ogromne, inne mają tyle zależności,
że wydają się one całkowicie przytłaczać funkcjonalność, nad którą chcesz pracować.
Wtedy warto sprawdzić, czy da się wyciąć większy fragment kodu i poddać go testom.
Zajrzyj do rozdziału 12., „Muszę dokonać wielu zmian w jednym miejscu. Czy powi-
nienem pousuwać zależności we wszystkich klasach, których te zmiany dotyczą?”.
Rozdział ten zawiera zbiór technik, których możesz użyć w celu odszukania punktów
zwężenia (190).
W dalszej części tego rozdziału opiszę, jak możesz zmienić sposób organizacji swojego
kodu, aby proces budowania był łatwiejszy.

Zależności podczas budowania


Kiedy w systemie zorientowanym obiektowo masz zbiór klas, które chcesz budować szyb-
ciej, pierwszym, czego musisz się dowiedzieć, jest to, które zależności staną temu na
przeszkodzie. W ogólności jest to dość łatwe: próbujesz po prostu użyć klasy w jarzmie
testowym. Niemal każdy problem, który napotkasz, będzie spowodowany przez jakąś
zależność, którą powinieneś usunąć. Po uruchomieniu klas w jarzmie testowym nadal
będą istnieć pewne zależności mogące mieć wpływ na czas kompilacji. Opłaca się przyj-
rzeć wszystkim elementom uzależnionym od klasy, której instancję udało Ci się utworzyć.
Podczas ponownej budowy systemu będą one musiały zostać powtórnie skompilowane.
Jak mógłbyś zminimalizować koszt tej rekompilacji?
Sposób, w jaki możesz sobie z tym poradzić, to wyodrębnienie tych interfejsów klas
w Twoim zbiorze, które są używane przez klasy spoza tego zbioru. W wielu zintegrowa-
nych środowiskach programistycznych możesz wyodrębnić interfejs, zaznaczając klasę,
a następnie wybierając z menu polecenie ukazujące listę wszystkich metod w klasie
i umożliwiające Ci wskazanie, które z nich mają stać się częścią nowego interfejsu.
W dalszej kolejności narzędzia te pozwalają określić nazwę nowego interfejsu. Udostęp-
niają one także opcję zastąpienia odwołań do klasy odwołaniami do interfejsu wszędzie,
gdzie jest to możliwe w bazie kodu. Jest to bardzo przydatna funkcja. W języku C++
wyodrębnienie implementera (356) jest trochę łatwiejsze do przeprowadzenia niż wyod-
rębnienie interfejsu (361). Nie musisz zmieniać nazw odwołań w całym kodzie, ale
powinieneś zmienić miejsca tworzące instancje starych klas — szczegóły znajdziesz
w punkcie „Wyodrębnianie implementera” (356).
Kiedy poddajemy już nasze zbiory klas testom, mamy możliwość zmiany fizycznej
struktury naszego projektu, aby proces budowania był prostszy. Robimy to, przesuwając te
zbiory do nowego pakietu lub nowej biblioteki. Po tym zabiegu budowanie staje się bar-
dziej skomplikowane, ale oto jego istota: kiedy usuwamy zależności i przesuwamy klasy
do nowych pakietów albo bibliotek, ogólny koszt przebudowy całego systemu wzrasta,
ale średni czas budowy może się skrócić.
Spójrzmy na przykład. Rysunek 7.1 pokazuje mały zbiór współpracujących ze sobą klas.
Wszystkie znajdują się w tym samym pakiecie.
USUWANIE ZALEŻNOŚCI 99

Rysunek 7.1. Klasa obsługująca okazje

Chcemy wprowadzić parę zmian w klasie AddOpportunityFormHandler, ale byłoby


miło, gdybyśmy przy okazji mogli także przyspieszyć proces budowy. Pierwszy etap polega
na próbie utworzenia instancji klasy AddOpportunityFormHandler. Niestety, wszystkie kla-
sy, od których ona zależy, są klasami właściwymi. AddOpportunityFormHandler potrzebuje
klas ConsultantSchedulerDB i AddOpportunityXMLGenerator. Równie dobrze mógłby to
być przypadek, w którym obie te klasy zależą od jeszcze innych klas, które nie są widoczne
na schemacie.
Jeżeli spróbujemy utworzyć instancję klasy AddOpportunityFormHandler, to kto wie,
ile klas ostatecznie użyjemy? Możemy ominąć ten problem, usuwając zależności. Pierwszą
zależnością, jaką napotykamy, jest ConsultantSchedulerDB. Musimy utworzyć jej instancję,
aby przekazać ją konstruktorowi klasy AddOpportunityFormHandler. Użycie tej klasy byłoby
niewygodne, ponieważ nawiązuje ona łączność z bazą danych, a my nie chcemy tego robić
podczas testów. Możemy jednak skorzystać z techniki wyodrębniania implementera
(356) i zerwać zależność, jak pokazano na rysunku 7.2.

Rysunek 7.2. Wyodrębnienie implementera w klasie ConsultantSchedulerDB


100 ROZDZIAŁ 7. DOKONANIE ZMIANY TRWA CAŁĄ WIECZNOŚĆ

Teraz, kiedy ConsultantSchedulerDB jest już interfejsem, możemy utworzyć instancję


klasy AddOpportunityFormHandler, korzystając z fałszywego obiektu, który implementuje
interfejs ConsultantSchedulerDB. Co ciekawe, usuwając tę zależność, przyspieszyliśmy
nasz proces budowy przy spełnieniu pewnych warunków. Następnym razem, kiedy bę-
dziemy wprowadzać zmiany w klasie ConsultantSchedulerDBImpl, nie będzie musiała być
rekompilowana klasa AddOpportunityFormHandler. Dlaczego? Ponieważ nie zależy już ona
bezpośrednio od kodu zawartego w ConsultantSchedulerDBImpl. Możemy wprowadzić
tyle zmian w pliku ConsultantSchedulerDBImpl, ile tylko chcemy, ale jeśli nie zrobimy cze-
goś, co zmusi nas do zmodyfikowania interfejsu ConsultantSchedulerDB, nie będziemy
musieli przebudowywać klasy AddOpportunityFormHandler.
Jeśli zechcemy, będziemy mogli odizolować się jeszcze bardziej od wymuszonej rekom-
pilacji, co pokazano na rysunku 7.3. To kolejny projekt systemu, uzyskiwany dzięki użyciu
techniki wyodrębniania implementera (356) w odniesieniu do klasy OpportunityItem.

Rysunek 7.3. Wyodrębnienie implementera w klasie OpportunityItem

Teraz klasa AddOpportunityFormHandler w ogóle nie zależy od oryginalnego kodu


w klasie OpportunityItem. W pewnym sensie umieściliśmy w kodzie kompilacyjny fire-
wall. W klasach ConsultantSchedulerDBImpl i OpportunityItemImpl możemy wprowadzić
tyle zmian, ile tylko chcemy, ale nie wymusi to rekompilacji na AddOpportunityFormHandler
ani na żadnym z jego użytkowników. Gdybyśmy chcieli w jawny sposób zdefiniować takie
rozwiązanie w strukturze pakietu aplikacji, moglibyśmy rozbić nasz projekt na odrębne
pakiety, pokazane na rysunku 7.4.
Mamy teraz pakiet OpportunityProcessing, który w ogóle nie jest zależny od imple-
mentacji bazy danych. Dowolne testy, jakie napiszemy i umieścimy w pakiecie, powinny
kompilować się szybko, a samego pakietu nie musimy rekompilować po zmianie kodu
w klasach implementujących bazę danych.
USUWANIE ZALEŻNOŚCI 101

Rysunek 7.4. Poddana refaktoryzacji struktura pakietu

Zasada odwrócenia zależności


Kiedy Twój kod zależy od interfejsu, zależność jest zwykle drobna i nie rzuca się w oczy.
Twój kod nie musi się zmieniać, chyba że zmieni się interfejs, a interfejs zazwyczaj podlega
zmianom o wiele rzadziej niż kod, który się za nim kryje. Jeśli nie masz interfejsu, możesz
dokonać edycji klas, które go implementują, albo dodać nowe klasy implementujące in-
terfejs — wszystko to bez ingerencji w kod, który z niego korzysta.
Z tego powodu lepsze są zależności od interfejsu albo klas abstrakcyjnych niż zależności od
klas właściwych. Jeśli zależności dotyczą mniej ulotnych elementów, minimalizujesz praw-
dopodobieństwo, że wprowadzenie pewnej zmiany pociągnie za sobą konieczność obszer-
nej rekompilacji.

Do tej pory przeprowadziliśmy kilka zabiegów mających na celu zapobieżenie rekom-


pilacji klasy AddOpportunityFormHandler po tym, gdy zmodyfikujemy klasy, od których
ona zależy. Dzięki temu proces budowy przebiega szybciej, ale to tylko połowa problemu.
Możemy także przyspieszyć budowanie w odniesieniu do kodu, który zależy od tej klasy.
Jeszcze raz spójrzmy na projekt pakietu, pokazany na rysunku 7.5.

Rysunek 7.5. Struktura pakietu


102 ROZDZIAŁ 7. DOKONANIE ZMIANY TRWA CAŁĄ WIECZNOŚĆ

AddOpportunityFormHandler jest jedyną publiczną, produkcyjną (czyli nietestową)


klasą w klasie OpportunityProcessing. Dowolne klasy zawarte w innych pakietach, które
od niej zależą, muszą zostać zrekompilowane, gdy ją zmienimy. Zależność tę możemy
usunąć, także wykorzystując względem AddOpportunityFormHandler technikę wyod-
rębniania interfejsu (361) albo wyodrębniania implementera (356). Po tym zabiegu
klasy w innych pakietach będą mogły zależeć od interfejsu. Kiedy już to zrobimy, skutecz-
nie powstrzymamy wszystkich użytkowników tego pakietu przed koniecznością rekom-
pilacji po dokonaniu większości zmian.
Możemy usuwać zależności i alokować klasy w różnych pakietach, aby skrócić proces
budowania, na co warto poświęcić trochę czasu. Jeśli będziesz mógł przebudowywać i bar-
dzo szybko uruchamiać testy, otrzymasz lepszą informację zwrotną podczas programo-
wania. W większości przypadków oznacza to mniejszą liczbę błędów i mniej irytacji. Nie
ma jednak nic za darmo. Istnieje pewien koncepcyjny narzut związany z większą liczbą
interfejsów i pakietów. Czy warto zapłacić tę cenę w porównaniu z alternatywą? Tak. Cza-
sami znalezienie czegoś może zabrać więcej czasu, kiedy masz więcej pakietów oraz
interfejsów, ale kiedy już znajdziesz to, czego szukasz, Twoja praca będzie łatwiejsza.

Kiedy w celu usunięcia zależności wprowadzasz w swoim systemie więcej interfejsów oraz klas,
czas poświęcany na przebudowanie całego systemu nieco się wydłuża, gdyż istnieje więcej
plików do skompilowania. Jednak przeciętny czas pracy narzędzia make — budowania uwzględ-
niającego to, co powinno zostać zrekompilowane — może ulec znaczącemu skróceniu.

Kiedy już zaczniesz pracować nad optymalizacją przeciętnego czasu budowy, otrzy-
masz w rezultacie fragmenty kodu, z którymi bardzo łatwo jest pracować. Wysiłku może
wymagać poddanie testom niewielkich zestawów klas, które kompilują się oddzielnie, ale
ważne jest, aby pamiętać, że musisz się tym zająć tylko raz w odniesieniu do danego zestawu.
Kiedy to zrobisz, będziesz mógł już zawsze zbierać plony swojej pracy.

Podsumowanie
Techniki, które zaprezentowałem w tym rozdziale, mogą być użyte do skrócenia czasu
potrzebnego na tworzenie małych zestawów klas, jednak jest to zaledwie niewielka część
tego, co możesz osiągnąć, używając interfejsów i pakietów do zarządzania zależnościami.
Książka Roberta C. Martina Agile Software Development: Principles, Patterns, and Practices
(Pearson Education 2002) przedstawia więcej technik, razem z kodem, który powinien
znać każdy programista.
Rozdział 8.

Jak mogę dodać nową


funkcjonalność?

Jest to chyba najbardziej abstrakcyjne i uzależnione od konkretnego problemu pytanie


w tej książce. Mało brakowało, abym z tego powodu go tutaj nie zamieścił. Faktem
jednak jest, że niezależnie od przyjętego w swoim projekcie podejścia czy też specyficznych
ograniczeń, jakie napotkałeś, istnieją pewne techniki, z których można skorzystać, aby
ułatwić sobie pracę.
Przyjrzyjmy się kontekstowi. W przypadku cudzego kodu jedną z najważniejszych
rzeczy, jakie musimy wziąć pod uwagę, jest fakt, że znaczna jego część nie jest objęta te-
stami. Co gorsza, ich rozmieszczenie może być trudne. Z tych powodów osoby pracu-
jące w wielu zespołach skłaniają się do korzystania z technik opisanych w rozdziale 6.,
„Nie mam zbyt wiele czasu, a muszę to zmienić”. Możemy korzystać z tych technik
(kiełkowanie i opakowywanie) w celu dodawania nowego kodu bez przeprowadzania
testów, ale oprócz oczywistego ryzyka z tym związanego istnieją też inne niebezpieczeń-
stwa. Otóż kiedy kiełkujemy lub opakowujemy, nie modyfikujemy w znaczącym stop-
niu istniejącego kodu, w związku z czym przez jakiś czas nie stanie się on ani trochę
lepszy. Kolejnym ryzykiem są duplikaty. Jeżeli kod, który dodajemy, powiela kod znajdu-
jący się w nieprzetestowanych obszarach kodu, może on tam po prostu zalegać i dalej
się psuć. Co gorsza, możemy nie zdawać sobie sprawy z powstania duplikatów, dopóki
nie zajdziemy daleko z naszymi modyfikacjami. Ostatnie ryzyko to strach i rezygnacja
— strach, że nie będziemy w stanie zmienić określonego fragmentu kodu, aby praca z nim
była łatwiejsza, oraz rezygnacja, ponieważ całe obszary kodu po prostu nie stają się nawet
w najmniejszym stopniu lepsze. Strach staje nam na przeszkodzie w podejmowaniu
dobrych decyzji. Kiełki i opakowania pozostawione w kodzie przypominają nam o tym.
Z zasady lepiej jest skonfrontować się z bestią, niż się przed nią ukrywać. Jeśli możemy
poddać kod testom, uzyskamy możliwość skorzystania z technik opisanych w tym roz-
dziale, aby w dobrym stylu posunąć się do przodu. Jeżeli potrzebujesz sposobów na
104 ROZDZIAŁ 8. JAK MOGĘ DODAĆ NOWĄ FUNKCJONALNOŚĆ?

umieszczenie testów na swoich miejscach, przejdź do rozdziału 13., „Muszę dokonać


zmian, ale nie wiem, jakie testy napisać”. Jeśli na Twojej drodze stoją zależności, zajrzyj
do rozdziałów 9., „Nie mogę umieścić tej klasy w jarzmie testowym”, i 10., „Nie mogę
uruchomić tej metody w jarzmie testowym”.
Kiedy testy są już na swoich miejscach, znajdujemy się w lepszej sytuacji wyjściowej,
aby dodać nową funkcjonalność. Dysponujemy solidnymi fundamentami.

Programowanie sterowane testami


Najwydajniejszą techniką dodawania nowych funkcjonalności, jaką znam, jest programo-
wanie sterowane testami. W skrócie działa ona następująco: wyobrażamy sobie metodę,
która pomoże nam rozwiązać część jakiegoś problemu, a następnie piszemy przypadek
testowy kończący się niepowodzeniem. Sama metoda jeszcze nie istnieje, ale jeśli będziemy
mogli dla niej napisać test, skonkretyzujemy nasze wyobrażenia dotyczące tego, co kod
— który mamy zamiar napisać — powinien robić.
Programowanie sterowane testami korzysta z algorytmu, który wygląda następująco:
1. Napisz przypadek testowy kończący się niepowodzeniem.
2. Skompiluj go.
3. Spraw, aby test się powiódł.
4. Usuń duplikaty.
5. Powtórz.
Oto przykład. Pracujemy nad aplikacją finansową i potrzebujemy klasy, która korzy-
stając z pewnych wyszukanych obliczeń matematycznych, sprawdzi, czy należy sprzedać
określony towar. Potrzebna jest nam klasa Javy, która wyliczy coś, co nazywa się pierw-
szym momentem statystycznym punktu. Nie dysponujemy jeszcze metodą, która to robi,
ale wiemy, że możemy napisać dla niej przypadek testowy. Znamy obliczenia, tak więc
wiemy, że dla danych zakodowanych w teście powinniśmy uzyskać wynik równy -0.5.

Napisz przypadek testowy kończący się niepowodzeniem


Oto przypadek testowy dla funkcjonalności, której potrzebujemy:
public void testFirstMoment() {
InstrumentCalculator calculator = new InstrumentCalculator();
calculator.addElement(1.0);
calculator.addElement(2.0);

assertEquals(-0.5, calculator.firstMomentAbout(2.0), TOLERANCE);


}
PROGRAMOWANIE STEROWANE TESTAMI 105

Skompiluj go
Test, który właśnie napisaliśmy, jest dobry, ale się nie kompiluje. Nie mamy w klasie
InstrumentCalculator metody o nazwie firstMomentAbout, dodamy ją jednak jako metodę
pustą. Chcemy, żeby test nie powiódł się, a zatem zwrócimy wartość NaN typu double
(co z pewnością nie jest oczekiwaną przez nas wartością -0.5).
public class InstrumentCalculator
{
double firstMomentAbout(double point) {
return Double.NaN;
}
...
}

Spraw, aby test się powiódł


Mając już ten test na miejscu, tworzymy kod, który umożliwi jego powodzenie.
public double firstMomentAbout(double point) {
double numerator = 0.0;
for (Iterator it = elements.iterator(); it.hasNext(); ) {
double element = ((Double)(it.next())).doubleValue();
numerator += element - point;
}
return numerator / elements.size();
}

Jest to niezwykle duża ilość kodu napisana w odpowiedzi na test prowadzony metodą pro-
gramowania sterowanego testami. Zwykle poszczególne kroki są o wiele mniejsze, chociaż
mogą być równie duże, jeśli masz pewność co do algorytmu, z którego musisz korzystać.

Usuń duplikaty
Czy mamy tu jakieś duplikaty? Raczej nie. Możemy przejść do następnego przypadku.

Napisz przypadek testowy kończący się niepowodzeniem


Kod, który właśnie napisaliśmy, sprawia, że test zakończy się powodzeniem, ale z pewno-
ścią nie sprawdzi się we wszystkich sytuacjach. W instrukcji zwracającej wartość możemy
przypadkowo podzielić przez 0. Co wówczas zrobimy? Co powinniśmy zwrócić, gdy nie
mamy żadnych elementów? W takim przypadku chcielibyśmy zgłosić wyjątek. Wyniki
będą dla nas niezrozumiałe, jeśli na naszej liście elementów nie będzie danych.
Następny test jest specyficzny. Kończy się niepowodzeniem, jeśli nie zostanie zgłoszony
wyjątek InvalidBasisException, natomiast przechodzi, kiedy nie ma żadnych wyjątków
bądź zostanie zgłoszony inny wyjątek. Gdy uruchomimy ten test, zakończy się on niepo-
wodzeniem, ponieważ gdy w metodzie firstMomentAbout dzielimy przez 0, zgłaszany jest
wyjątek ArithmeticException.
106 ROZDZIAŁ 8. JAK MOGĘ DODAĆ NOWĄ FUNKCJONALNOŚĆ?

public void testFirstMoment() {


try {
new InstrumentCalculator().firstMomentAbout(0.0);
fail("spodziewany InvalidBasisException");
}
catch (InvalidBasisException e) {
}
}

Skompiluj go
W tym celu musimy zmienić deklarację metody firstMomentAbout, żeby zgłaszała wyjątek
InvalidBasisException.
public double firstMomentAbout(double point)
throws InvalidBasisException {

double numerator = 0.0;


for (Iterator it = elements.iterator(); it.hasNext(); ) {
double element = ((Double)(it.next())).doubleValue();
numerator += element - point;
}
return numerator / elements.size();
}

Ten kod jednak się nie skompiluje. Błędy kompilatora mówią nam, że musimy zgłosić
ten wyjątek, jeśli jest on wymieniony w deklaracji. Dopisujemy zatem brakujący kod.
public double firstMomentAbout(double point)
throws InvalidBasisException {
if (element.size() == 0)
throw new InvalidBasisException("brak elementów");

double numerator = 0.0;


for (Iterator it = elements.iterator(); it.hasNext(); ) {
double element = ((Double)(it.next())).doubleValue();
numerator += element - point;
}
return numerator / elements.size();
}

Spraw, aby test się powiódł


Teraz nasz test przechodzi.

Usuń duplikaty
W tym przypadku nie ma żadnych duplikatów.
PROGRAMOWANIE STEROWANE TESTAMI 107

Napisz przypadek testowy kończący się niepowodzeniem


Następny fragment kodu, który musimy napisać, to metoda obliczająca drugi moment
statystyczny punktu. Tak naprawdę jest to odmiana pierwszego momentu. Oto test, który
przybliży nas do napisania potrzebnego nam kodu. W tym przypadku spodziewaną
wartością jest 0.5 zamiast -0.5. Piszemy test dla metody, która jeszcze nie istnieje:
secondMomentAbout.
public void testSecondMoment() throws Exception {
InstrumentCalculator calculator = new InstrumentCalculator();
calculator.addElement(1.0);
calculator.addElement(2.0);
assertEquals(0.5, calculator.secondMomentAbout(2.0), TOLERANCE);
}

Skompiluj go
Aby skompilować ten kod, musimy dodać definicję metody secondMomentAbout. Mogliby-
śmy skorzystać z tej samej sztuczki, której użyliśmy przy metodzie firstMomentAbout, ale
okazuje się, że kod dla drugiego momentu tylko nieco różni się od kodu pierwszego
momentu.
Poniższy wiersz dla pierwszego momentu:
numerator += element - point;

powinien wyglądać następująco w przypadku momentu drugiego:


numerator += Math.pow(element – point, 2.0);

Istnieje ogólny wzorzec do wykorzystania w takiej sytuacji. N-ty moment statystyczny


jest obliczany za pomocą następującego wyrażenia:
numerator += Math.pow(element – point, N);

Kod w metodzie firstMomentAbout działa, ponieważ element – point jest taki sam jak
w wyrażeniu Math.pow(element – point, 1.0).
W tym miejscu mamy wybór. Możemy zauważyć tę regularność i napisać ogólną
metodę, przyjmującą punkt oraz wartość N. Następnie moglibyśmy zastąpić wszystkie
użycia metody firstMomentAbout(double) odwołaniem do metody ogólnej. Możemy tak
postąpić, ale tym samym obciążylibyśmy użytkowników metody koniecznością dostar-
czania wartości N, a nie chcemy, aby mogli oni przedstawiać arbitralne wartości tej zmiennej.
Wygląda na to, że zaczynamy gubić w tym miejscu wątek. Powinniśmy trochę przystopo-
wać i dokończyć to, co do tej pory rozpoczęliśmy. Teraz naszym jedynym zadaniem
jest kompilacja. Uogólniać będziemy mogli później, jeśli okaże się, że nadal mamy na to
ochotę.
Aby skompilować kod, możemy skopiować metodę firstMomentAbout i zmienić jej
nazwę na secondMomentAbout:
108 ROZDZIAŁ 8. JAK MOGĘ DODAĆ NOWĄ FUNKCJONALNOŚĆ?

public double secondMomentAbout(double point)


throws InvalidBasisException {

if (elements.size() == 0)
throw new InvalidBasisException("brak elementów");

double numerator = 0.0;


for (Iterator it = elements.iterator(); it.hasNext(); ) {
double element = ((Double)(it.next())).doubleValue();
numerator += element - point;
}
return numerator / elements.size();
}

Spraw, aby test się powiódł


Kod nie przechodzi testu. Gdy tak się dzieje, możemy się cofnąć i sprawić, aby test został
zaliczony, zmieniając kod w następujący sposób:
public double secondMomentAbout(double point)
throws InvalidBasisException {

if (elements.size() == 0)
throw new InvalidBasisException("brak elementów");

double numerator = 0.0;


for (Iterator it = elements.iterator(); it.hasNext(); ) {
double element = ((Double)(it.next())).doubleValue();
numerator += Math.pow(element – point, 2.0);
}
return numerator / elements.size();
}

Być może zaszokowało Cię użycie operacji „wytnij/kopiuj/wklej”, jak to właśnie zrobi-
liśmy, ale już za chwilę usuniemy tę nadmiarowość. Kod, który tworzymy, jest świeży.
Sztuczka polegająca na prostym skopiowaniu kodu, który jest nam potrzebny, i prze-
kształceniu go w nową metodę jest dość wydajna w kontekście cudzego kodu. Często zdarza
się, że gdy chcemy w szczególnie okropnym kodzie dodać funkcjonalność, łatwiej nam
będzie zrozumieć nasze modyfikacje, jeśli umieścimy je w jakimś nowym miejscu i przyj-
rzymy się im, gdy znajdą się tuż obok starego kodu. Powielony kod będziemy mogli
usunąć później, aby bardziej elegancko umieścić go w klasie, lub po prostu pozbędziemy
się modyfikacji i wypróbujemy inny sposób, wiedząc, że dysponujemy starym kodem, który
możemy analizować i z którego możemy się uczyć.

Usuń duplikaty
Teraz, kiedy przechodzą już oba testy, musimy wykonać następny krok: usunąć duplikaty.
Jak się do tego zabrać?
PROGRAMOWANIE STEROWANE TESTAMI 109

Jeden ze sposobów polega na wyodrębnieniu całego ciała metody secondMomentAbout,


nadaniu mu nazwy nthMomentAbout i przydzieleniu parametru N:
public double secondMomentAbout(double point)
throws InvalidBasisException {
return nthMomentAbout(point, 2.0);
}

private double nthMomentAbout(double point, double n)


throws InvalidBasisException {
if (elements.size() == 0)
throw new InvalidBasisException(“brak elementów “);

double numerator = 0.0;


for (Iterator it = elements.iterator(); it.hasNext(); ) {
double element = ((Double)(it.next())).doubleValue();
numerator += Math.pow(element – point, n);
}
return numerator / elements.size();
}

Jeżeli teraz uruchomimy nasze testy, okaże się, że przechodzą. Możemy powrócić do
metody firstMomentAbout i zastąpić jej ciało wywołaniem metody nthMomentAbout:
public double firstMomentAbout(double point)
throws InvalidBasisException {
return nthMomentAbout(point, 1.0);
}

Ostatni etap — usunięcie duplikatów — jest bardzo ważny. Możemy szybko i bru-
talnie dodawać w kodzie funkcjonalności, wykonując takie operacje jak kopiowanie całych
bloków kodu, ale jeśli później nie usuniemy duplikatów, wywołamy problemy i sprawimy,
że konserwacja programu będzie uciążliwa. Z drugiej jednak strony, jeśli testy znajdują się
na swoich miejscach, będziemy mogli łatwo usuwać duplikaty. Z pewnością mieliśmy
z tym do czynienia w naszym przykładzie, ale jedynym powodem, dla którego przeprowa-
dzaliśmy testy, było korzystanie od samego początku z techniki programowania sterowa-
nego testami. W przypadku cudzego kodu testy, jakie piszemy, posługując się tą techniką,
są bardzo ważne. Kiedy znajdują się one na swoich miejscach, mamy swobodę pisania
takiego kodu, jaki tylko jest nam potrzebny w celu dodania nowej funkcjonalności. Wiemy
przy tym, że będziemy mogli go umieścić w reszcie kodu bez pogarszania naszej sytuacji.

Programowanie sterowane testami i cudzy kod


Jedna z najcenniejszych cech programowania sterowanego testami polega na tym, że technika
ta umożliwia nam skoncentrowanie się jednocześnie na jednym tylko zagadnieniu. Albo piszemy
kod, albo refaktoryzujemy — nigdy nie robimy obu tych rzeczy jednocześnie.
To rozdzielenie jest szczególnie ważne w odniesieniu do cudzego kodu, ponieważ możemy pisać
nowy kod niezależnie od kodu starego.
Po napisaniu nowego kodu możemy przeprowadzić refaktoryzację, aby pozbyć się dupli-
katów powstałych w starym i nowym kodzie.
110 ROZDZIAŁ 8. JAK MOGĘ DODAĆ NOWĄ FUNKCJONALNOŚĆ?

W przypadku cudzego kodu możemy rozszerzyć algorytm programowania sterowa-


nego testami następująco:
0. Poddaj testom klasę, którą chcesz zmienić.
1. Napisz przypadek testowy kończący się niepowodzeniem.
2. Skompiluj go.
3. Spraw, aby test się powiódł (postaraj się podczas tej czynności nie zmieniać
istniejącego kodu).
4. Usuń duplikaty.
5. Powtórz.

Programowanie różnicowe
Programowanie sterowane testami nie jest związane wyłącznie ze zorientowaniem
obiektowym. W rzeczy samej przykład z poprzedniego podrozdziału jest tak naprawdę
fragmentem kodu proceduralnego, który został owinięty w klasę. W przypadku zoriento-
wania obiektowego mamy inną opcję. Aby wprowadzać nowe funkcjonalności z pominię-
ciem bezpośredniego modyfikowania klas, możemy korzystać z mechanizmu dziedziczenia.
Po tym, jak już dodamy funkcjonalność, możemy dokładnie zastanowić się, jak naprawdę
chcemy ją zintegrować.
Główna technika wykorzystywana w tym celu nazywana jest programowaniem różni-
cowym. Jest to raczej wiekowa technika, która była dyskutowana i dość szeroko używana
w latach 80. XX wieku, ale wypadła z łask w latach 90., kiedy wiele osób związanych z pro-
gramowaniem obiektowym zauważyło, że dziedziczenie może sprawiać problemy, gdy
jest nadużywane. Sam fakt, że na początku skorzystaliśmy z dziedziczenia, nie oznacza
jednak, że musimy się go kurczowo trzymać. Za pomocą testów z łatwością możemy
przejść do innych struktur, jeśli dziedziczenie zacznie być kłopotliwe.
Oto przykład pokazujący, jak działa ta technika. Mamy przetestowaną klasę Javy o na-
zwie MailForwarder, która jest częścią programu zarządzającego listami mailingowymi.
Znajduje się w niej metoda o nazwie getFromAddress, która wygląda następująco:
private InternetAddress getFromAddress(Message message)
throws MessagingException {
Address [] from = message.getFrom ();
if (from != null && from.length > 0)
return new InternetAddress (from [0].toString ());
return new InternetAddress (getDefaultFrom());
}

Celem tej metody jest wydobycie z otrzymanej wiadomości adresu „od” i zwrócenie
go, aby mógł zostać użyty jako adres „od” wiadomości, która zostanie przesłana dalej
odbiorcom z listy.
PROGRAMOWANIE RÓŻNICOWE 111

Jest on wykorzystywany tylko w jednym miejscu — w poniższych wierszach, znaj-


dujących się w metodzie forwardMessage:
MimeMessage forward = new MimeMessage (session);
forward.setFrom (getFromAddress (message));

Co powinniśmy zrobić, kiedy otrzymamy nowe wymagania? Co się stanie, jeśli bę-
dziemy musieli wspierać anonimowe listy mailingowe? Członkowie tych list mogą wysyłać
maile, ale adres „od” ich wiadomości powinien zawierać określony adres e-mail, bazujący
na wartości zmiennej domain (instancji klasy MessageForwarder). Oto przypadek testowy
kończący się niepowodzeniem w odniesieniu do tej zmiany (kiedy wykonuje się test,
zmienna expectedMessage ustawia się na wiadomości przesyłanej dalej przez metodę
MessageForwarder):
public void testAnonymous () throws Exception {
MessageForwarder forwarder = new MessageForwarder();
forwarder.forwardMessage (makeFakeMessage());
assertEquals ("anonimowy@" + forwarder.getDomain(),
expectedMessage.getFrom ()[0].toString());
}

Czy istotnie musimy modyfikować metodę MessageForwarder w celu dodania tej funk-
cjonalności? Niezupełnie — możemy po prostu utworzyć podklasę klasy MessageForwarder,
nazwać ją AnonymousMessageForwarder i skorzystać z niej w testach.
public void testAnonymous () throws Exception {
MessageForwarder forwarder = new AnonymousMessageForwarder();
forwarder.forwardMessage (makeFakeMessage());
assertEquals ("anonimowy@" + forwarder.getDomain(),
expectedMessage.getFrom ()[0].toString());
}

Tworzymy podklasę (patrz rysunek 8.1).

Rysunek 8.1. Tworzenie podklasy MessageForwarder

Zamiast metody prywatnej utworzyliśmy w klasie MessageForwarder metodę chronioną


getFromAddress. Następnie przesłoniliśmy ją w klasie AnonymousMessageForwarder. Metoda
ta wygląda w niej następująco:
112 ROZDZIAŁ 8. JAK MOGĘ DODAĆ NOWĄ FUNKCJONALNOŚĆ?

protected InternetAddress getFromAddress(Message message)


throws MessagingException {
String anonymousAddress = "anonimowy" + listAddress;
return new InternetAddress(anonymousAddress);
}

Co dzięki temu zyskujemy? No cóż, rozwiązaliśmy problem, ale za to dodaliśmy do


naszego systemu nową klasę o bardzo prostym zachowaniu. Czy ma sens tworzenie pod-
klasy na podstawie całej klasy przesyłającej dalej wiadomości tylko po to, aby zmienić
w mailu adres „od”? W dłuższej perspektywie nie, ale dzięki temu mamy możliwość szyb-
kiego przeprowadzania testów. A kiedy już dysponujemy testem, który się udaje, możemy
z niego skorzystać, aby zagwarantować, że to nowe zachowanie pozostanie, gdy zdecy-
dujemy się na zmianę projektu.
public void testAnonymous () throws Exception {
MessageForwarder forwarder = new AnonymousMessageForwarder();
forwarder.forwardMessage (makeFakeMessage());
assertEquals ("anonimowy@" + forwarder.getDomain(),
expectedMessage.getFrom ()[0].toString());
}

To wszystko wydaje się zbyt proste. Gdzie jest haczyk? Proszę bardzo, oto on: kiedy
będziemy często korzystać z tej techniki, a nie zwrócimy uwagi na niektóre z kluczowych
aspektów naszego projektu, zacznie się on szybko degradować. Aby zobaczyć, jak to się
dzieje, rozważmy kolejną zmianę. Chcemy przesyłać wiadomości odbiorcom z listy ma-
ilingowej, ale chcielibyśmy wysyłać także ukryte maile do wiadomości (UDW) osób, które
nie mogą znaleźć się na oficjalnej liście mailingowej. Możemy nazwać ich odbiorcami
spoza listy.
Wydaje się to łatwe. Moglibyśmy utworzyć kolejną podklasę klasy MessageForwarder
i przesłonić jej metodę, dzięki czemu będzie ona wysyłać wiadomości na inny adres, co
pokazano na rysunku 8.2.

Rysunek 8.2. Tworzenie podklas dla dwóch różnych opcji

Takie rozwiązanie mogłoby działać całkiem dobrze z jednym wyjątkiem: a gdybyśmy


tak potrzebowali klasy MessageForwarder, która robi obie te rzeczy — przekazuje wszystkie
wiadomości do odbiorców spoza listy i wysyła je anonimowo?
PROGRAMOWANIE RÓŻNICOWE 113

Na tym polega poważny problem, który pojawia się przy szerokim korzystaniu z dzie-
dziczenia. Jeśli poszczególne funkcjonalności umieścimy w odrębnych podklasach, bę-
dziemy mogli dysponować w danym czasie tylko jedną z tych funkcjonalności.
Jak moglibyśmy pozbyć się tego ograniczenia? Jednym z rozwiązań jest zatrzymanie
się przed dodaniem funkcjonalności obsługującej odbiorców spoza listy i przeprowadzenie
refaktoryzacji, dzięki czemu operacja ta odbędzie się sprawniej. Na szczęście mamy na
miejscu test, który napisaliśmy wcześniej. Możemy z niego skorzystać, żeby zweryfikować,
czy potrzebne zachowanie zostanie pozostawione, zanim przejdziemy do następnego
schematu.
W przypadku funkcjonalności anonimowego przesyłania wiadomości istnieje sposób,
który możemy zastosować bez konieczności tworzenia podklasy. Moglibyśmy podjąć de-
cyzję, że przekazywanie dalej poczty będzie opcją do konfiguracji. Jednym ze sposobów,
w jaki można to zrealizować, polega na takiej zmianie konstruktora klasy, aby przyjmował
zbiór własności:
Properties configuration = new Properties();
configuration.setProperty("anonymous", "true");
MessageForwarder forwarder = new MessageForwarder(configuration);

Czy możemy sprawić, że nasz test powiedzie się, kiedy tak zrobimy? Jeszcze raz spójrz-
my na test:
public void testAnonymous () throws Exception {
MessageForwarder forwarder = new AnonymousMessageForwarder();
forwarder.forwardMessage (makeFakeMessage());
assertEquals ("anonimowy@" + forwarder.getDomain(),
expectedMessage.getFrom ()[0].toString());
}

Teraz test przechodzi. Metoda AnonymousMessageForwarder przesłania metodę getFrom


z klasy MessageForwarder. Co się stanie, kiedy zmienimy metodę getFrom w klasie
MessageForwarder w poniższy sposób?
private InternetAddress getFromAddress(Message message)
throws MessagingException {

String fromAddress = getDefaultFrom();


if (configuration.getProperty("anonymous").equals("true")) {
fromAddress = "anonimowy@" + domain;
}
Else {
Address [] from = message.getFrom ();
if (from != null && from.length > 0) {
fromAddress = from [0].toString ();
}
}
return new InternetAddress (fromAddress);
}
114 ROZDZIAŁ 8. JAK MOGĘ DODAĆ NOWĄ FUNKCJONALNOŚĆ?

Teraz w klasie MessageForwarder mamy metodę getFrom, która powinna obsługiwać


zarówno przypadki anonimowe, jak i zwykłe. Możemy ją zweryfikować, zamieniając w ko-
mentarz fragment kodu przesłaniający metodę getFrom w klasie AnonymousMessageForwarder
i sprawdzając, czy testy się powiodą:
public class AnonymousMessageForwarder extends MessageForwarder
{
/*
protected InternetAddress getFromAddress(Message message)
throws MessagingException {
String anonymousAddress = "anonimowy" + listAddress;
return new InternetAddress(anonymousAddress);
}
*/
}

Testy, rzecz jasna, przechodzą.


Nie potrzebujemy już klasy AnonymousMessageForwarder, więc możemy ją usunąć.
W następnej kolejności musimy znaleźć wszystkie miejsca, w których tworzone są obiekty
tej klasy i zastąpić wywołania jej konstruktora wywołaniem konstruktora, który przyjmuje
zbiór własności.
Ze zbioru własności możemy też skorzystać, aby dodać nową funkcjonalność. Mo-
glibyśmy mieć własność aktywującą obsługę odbiorców spoza listy.
Czy już skończyliśmy? Niezupełnie. Metoda getFrom w klasie MessageForwarder zro-
biła się nieco nieczytelna, ale ponieważ mamy testy, możemy bardzo szybko skorzystać
z techniki wyodrębniania metody i trochę ją uporządkować. Teraz wygląda ona nastę-
pująco:
private InternetAddress getFromAddress(Message message)
throws MessagingException {

String fromAddress = getDefaultFrom();


if (configuration.getProperty("anonymous").equals("true")) {
fromAddress = "anonimowy@" + domain;
}
else {
Address [] from = message.getFrom ();
if (from != null && from.length > 0)
fromAddress = from [0].toString ();
}
return new InternetAddress (fromAddress);
}

Po refaktoryzacji metoda będzie wyglądać tak:


private InternetAddress getFromAddress(Message message)
throws MessagingException {

String fromAddress = getDefaultFrom();


if (configuration.getProperty("anonymous").equals("true")) {
from = getAnonymousFrom();
}
else {
PROGRAMOWANIE RÓŻNICOWE 115

from = getFrom(Message);
}
return new InternetAddress (from);
}

Teraz metoda getFrom wygląda lepiej, ale funkcja wysyłki anonimowej oraz obsługi
odbiorców spoza listy znajduje się w klasie MessageForwarder. Czy to źle w świetle zasady
pojedynczej odpowiedzialności (254)? Możliwe, że tak. To zależy od tego, jak bardzo
duży i jak mocno splątany z resztą kodu zrobi się kod realizujący te funkcjonalności.
W tym przypadku stwierdzenie, czy lista jest anonimowa, nie jest skomplikowanym zada-
niem. Podejście związane z wykorzystaniem własności umożliwia nam posuwanie się do
przodu w elegancki sposób. Co zrobimy, kiedy pojawi się tyle własności, że kod klasy
MessageForwarder zacznie roić się od instrukcji warunkowych? Jedną z rzeczy, które
możemy wtedy zrobić, to rozpocząć korzystanie z klas zamiast ze zbiorów własności.
A gdybyśmy tak utworzyli klasę o nazwie MailingConfiguration i przechowywali w niej
zbiór własności (patrz rysunek 8.3).

Rysunek 8.3. Delegowanie do klasy MailingConfiguration

Wygląda dobrze, ale czy nie przesadziliśmy? Wydaje się, że klasa MailingConfiguration
robi dokładnie to samo co zbiór własności.
A gdybyśmy tak zdecydowali się przesunąć metodę getFromAddress do klasy
MailingConfiguration? Klasa ta mogłaby przyjmować wiadomości i decydować, jaki adres
„od” zwrócić. Jeśli konfiguracja będzie anonimowa, zostanie zwrócony anonimowy adres
„od”. W przeciwnym razie pobierany, a następnie zwracany byłby pierwszy adres z wia-
domości. Nasz projekt wyglądałby jak na rysunku 8.4. Zwróć uwagę, że nie musimy już
mieć metody, która by odczytywała i ustawiała własności. Klasa MailingConfiguration re-
alizuje teraz funkcjonalności wyższego poziomu.

Rysunek 8.4. Przesuwanie zachowania do klasy MailingConfiguration

Moglibyśmy także rozpocząć dodawanie kolejnych metod do klasy MailingConfiguration.


Gdybyśmy na przykład chcieli zaimplementować obsługę odbiorców spoza listy, mogliby-
śmy dodać metodę o nazwie buildRecipientList i pozwolić, aby korzystała z niej klasa
MessageForwarder, jak to pokazano na rysunku 8.5.
116 ROZDZIAŁ 8. JAK MOGĘ DODAĆ NOWĄ FUNKCJONALNOŚĆ?

Rysunek 8.5. Przesuwanie kolejnych zachowań do klasy MailingConfiguration

Po tych zmianach nazwa klasy nie odpowiada już funkcjom, jakie ona pełni. Kon-
figuracja to zwykle coś raczej pasywnego. Klasa ta natomiast aktywnie buduje i modyfikuje
dane dla obiektów klasy MessageForwarder na ich żądanie. Jeśli w systemie nie ma już innej
klasy z taką samą nazwą, to dobrym wyborem mogłaby być MailingList. Obiekty klasy
MessageForwarder proszą listy mailingowe o tworzenie list odbiorców na podstawie adre-
sów. Można powiedzieć, że do zadań listy mailingowej należy decydowanie, w jaki sposób
wiadomości będą modyfikowane. Rysunek 8.6 pokazuje nasz projekt po zmianach.

Rysunek 8.6. Klasa MailingConfiguration z nazwą zmienioną na MailingList

Istnieje wiele wydajnych metod refaktoryzacji, ale najwydajniejszą jest zmiana nazwy klasy.
Umożliwia ona zmianę sposobu, w jaki jest postrzegany kod, i pozwala zauważyć możliwo-
ści, które wcześniej mogły nie być brane pod uwagę.

Programowanie różnicowe to przydatna technika. Umożliwia nam szybkie wprowa-


dzanie zmian i pozwala na korzystanie z testów w celu uzyskania czytelniejszego projektu.
Aby jednak z niej korzystać, musimy uważać na kilka kruczków. Jednym z nich jest naru-
szenie zasady podstawienia Liskov.

Zasada podstawienia Liskov


Istnieje kilka subtelnych błędów, które możemy wywołać, gdy korzystamy z dziedziczenia.
Weź pod uwagę następujący kod:
public class Rectangle
{
...
public Rectangle(int x, int y, int width, int height) { … }
public void setWidth(int width) { ... }
public void setHeight(int height) { ... }
public int getArea() { ... }
}
PROGRAMOWANIE RÓŻNICOWE 117

Mamy klasę Rectangle (prostokąt). Czy możemy utworzyć podklasę o nazwie Square (kwadrat)?
public class Square extends Rectangle
{
...
public Square(int x, int y, int width) { ... }
...
}

Klasa Square dziedziczy metody setWidth i setHeight klasy Rectangle. Jaka powinna być
powierzchnia figury, kiedy uruchomimy następujący kod?
Rectangle r = new Square();
r.setWidth(3);
r.setHeight(4);

Jeśli wynikiem jest 12, to obiekt klasy Square tak naprawdę nie jest kwadratem, prawda? Mogli-
byśmy przesłonić metody setWidth i setHeight, aby obiekt klasy Square pozostał kwadratowy.
Moglibyśmy sprawić, aby metody setWidth i setHeight modyfikowały wartość zmiennej width,
widoczną w nawiasach, ale mogłoby to prowadzić do wyników sprzecznych z oczekiwaniami.
Każdy, kto spodziewa się, że prostokąt ma powierzchnię 12, kiedy jego szerokość wynosi 3,
a wysokość 4, byłby zaskoczony, ponieważ otrzymałby wynik równy 16.
Jest to klasyczny przykład naruszenia zasady podstawienia Liskov. W kodzie powinna istnieć
możliwość zastępowania obiektów i podklas obiektami ich klas nadrzędnych. Jeśli tak nie jest,
w naszym kodzie mogą pojawić się ciche błędy.

Z zasady podstawienia Liskov wynika, że klienty klasy powinny mieć możliwość korzy-
stania z obiektów jej podklasy bez konieczności dowiadywania się, czy obiekty te są instan-
cjami podklasy. Nie ma automatycznego sposobu na całkowite uniknięcie naruszeń tej za-
sady. To, czy klasa jest zgodna z zasadą podstawienia Liskov, zależy od jej klientów oraz
tego, czego oczekują. Istnieje jednak kilka ogólnych zasad, które mogą być pomocne:
1. Jeśli to możliwe, unikaj przesłaniania metod konkretnych.
2. Jeśli już tak się stało, sprawdź, czy możesz wywoływać przesłaniane metody z po-
ziomu metody przesłaniającej.
Chwileczkę! Żadnej z tych rzeczy nie zrobiliśmy w odniesieniu do klasy Message
Forwarder. W rzeczywistości zrobiliśmy coś wręcz przeciwnego. Przesłoniliśmy kon-
kretną metodę w podklasie (AnonymousMessageForwarder). I o co tyle hałasu?
Oto nasz problem: kiedy przesłaniamy konkretne metody, jak to zrobiliśmy, przesła-
niając metodę getFromAddress klasy MessageForwarder w AnonymousMessageForwarder,
możemy przy okazji zmienić sens części kodu, z którego korzysta klasa MessageForwarder.
Jeżeli w naszej aplikacji porozrzucane są odwołania do klasy MessageForwarder, a jedno
z tych odwołań odnosi się do klasy AnonymousMessageForwarder, osoby używające tej
aplikacji mogą sądzić, że jest to odwołanie do zwykłej klasy MessageForwarder, która
pobiera adres „od” z przetwarzanej wiadomości, a następnie korzysta z tego adresu podczas
dalszej pracy nad tą wiadomością. Czy dla osób korzystających z tej klasy będzie mieć
118 ROZDZIAŁ 8. JAK MOGĘ DODAĆ NOWĄ FUNKCJONALNOŚĆ?

znaczenie to, że klasa ta działa właśnie w taki sposób, zamiast używać jako adresu „od”
jakiegoś innego — specjalnego adresu? To zależy od aplikacji. Zazwyczaj kod staje się
zagmatwany, kiedy zbyt często przesłaniamy konkretne metody. Ktoś może zobaczyć
w kodzie odwołanie do klasy MessageForwarder, spojrzeć na tę klasę i pomyśleć, że wyko-
nywany jest kod metody getFromAddress, którą ona zawiera. Może nie zdawać sobie
sprawy, że odwołanie odnosi się do klasy AnonymousMessageForwarder i że to właśnie jej
metoda getFromAddress jest wykonywana. Gdybyśmy chcieli pozostać przy dziedziczeniu,
moglibyśmy utworzyć abstrakcyjną klasę MessageForwarder, dać jej abstrakcyjną metodę
getFromAddress i pozwolić, aby to podklasy udostępniały konkretne ciała. Rysunek 8.7
pokazuje, jak wyglądałby nasz projekt po wprowadzeniu tych zmian.

Rysunek 8.7. Hierarchia znormalizowana


Taki rodzaj hierarchii nazywam hierarchią znormalizowaną. W hierarchii znormali-
zowanej żadna z klas nie ma więcej niż po jednej implementacji metody. Innymi słowy,
w żadnej klasie nie istnieje metoda, która przesłaniałaby konkretną metodę, odziedziczoną
od klasy nadrzędnej. Gdy zadasz pytanie: „Jak ta klasa robi X?”, będziesz mógł uzyskać
odpowiedź, jeśli udasz się do klasy X i to sprawdzisz. Albo znajdziesz tam szukaną metodę,
albo metoda będzie abstrakcyjna i zaimplementowana w jednej z podklas. W hierarchii
znormalizowanej nie musisz przejmować się klasami przesłaniającymi zachowanie, jakie
odziedziczyły od swoich klas nadrzędnych.
Czy opłaca się tak postępować za każdym razem? Kilka przesłonięć konkretnych klas
od czasu do czasu nie zaszkodzi, o ile nie naruszają one zasady podstawienia Liskov. Warto
jednak czasem zastanowić się nad tym, jak bardzo klasy są oddalone od hierarchii
znormalizowanej, i zrobić krok w jej kierunku, kiedy przygotowujemy się do separo-
wania odpowiedzialności.
Programowanie różnicowe pozwala nam szybko wprowadzać wariacje w systemach.
Gdy z niego korzystamy, możemy stosować testy, aby dokładnie zlokalizować nowe za-
chowanie i przejść do odpowiedniejszej struktury, jeśli zajdzie taka potrzeba. Dzięki testom
przejście to może być bardzo szybkie.
PODSUMOWANIE 119

Podsumowanie
Opisanych w tym rozdziale technik możesz używać w celu dodawania funkcjonalności
w dowolnym kodzie, który można poddać testom. Literatura na temat programowania
sterowanego testami wzbogaciła się w ostatnich latach. W szczególności polecam książki
Kenta Becka Test-Driven Development by Example (Addison-Wesley 2002) oraz Dave’a
Astela Test-Driven Development: A Practical Guide (Prentice Hall Professional Technical
Reference 2003).
120 ROZDZIAŁ 8. JAK MOGĘ DODAĆ NOWĄ FUNKCJONALNOŚĆ?
Rozdział 9.

Nie mogę umieścić tej klasy


w jarzmie testowym

Będzie ciężko. Gdyby utworzenie instancji klasy w jarzmie testowym zawsze było łatwe,
książka ta byłaby o wiele krótsza. Niestety, często zadanie to jest trudne.
Oto cztery najczęściej występujące problemy, które napotykamy:
1. Nie da się w prosty sposób utworzyć obiektów klasy.
2. Nie da się w prosty sposób przeprowadzić procesu budowy jarzma testowego
z umieszczoną w nim klasą.
3. Korzystanie z konstruktora, którego potrzebujemy użyć, wywołuje skutki uboczne.
4. Konstruktor wykonuje sporo pracy, a my musimy ją rozpoznać.
W rozdziale tym zajmiemy się serią przykładów, które skupiają się na tych problemach,
z uwzględnieniem różnych języków. Istnieje więcej niż tylko jeden sposób poradzenia
sobie z każdym z tych problemów. Zaznajomienie się z tymi przykładami jest jednak
niezłą metodą na poznanie całego arsenału technik usuwania zależności i nauczenia
się, które z nich wybrać i jak je stosować w określonych sytuacjach.

Przypadek irytującego parametru


Kiedy muszę wprowadzić zmianę w cudzym systemie, zwykle początkowo jestem nasta-
wiony bardzo optymistycznie. Nie mam pojęcia dlaczego. Na ile tylko mogę, próbuję być
realistą, ale optymizm zawsze się przebija. „Hej”, mówię do siebie (albo do kolegi), „wy-
gląda na to, że będzie łatwo. Musimy tylko cośtam trochę stentegować, i po robocie”.
Wszystko to brzmi prosto, kiedy się o tym mówi, aż bierzemy się do klasy CośTam (czym-
kolwiek by ona była) i się jej przyglądamy. „No dobra. Musimy więc dodać metodę tutaj,
zmienić inną metodę tam i oczywiście wrzucić całość do jarzma testowego”. W tym
122 ROZDZIAŁ 9. NIE MOGĘ UMIEŚCIĆ TEJ KLASY W JARZMIE TESTOWYM

momencie zaczynam nieco wątpić. „Motyla noga! Wygląda na to, że najprostszy kon-
struktor w tej klasie przyjmuje trzy parametry, ale — dodaję optymistycznie — być może
utworzenie obiektu wcale nie będzie takie trudne”.
Zajmijmy się przykładem i zobaczmy, czy mój optymizm ma podstawy, czy jest tylko
mechanizmem obronnym.
W kodzie systemu realizującego płatności znajduje się nieprzetestowana klasa Javy
o nazwie CreditValidator.
public class CreditValidator
{
public CreditValidator(RGHConnection connection,
CreditMaster master,
String validatorID) {
...
}
Certificate validateCustomer(Customer customer)
throws InvalidCredit {
...
}
...
}

Jedną z wielu funkcji tej klasy jest informowanie nas, czy klienci mają otwarty kredyt.
Jeśli tak, otrzymujemy certyfikat informujący o wysokości kredytu. Jeśli nie, klasa zgłasza
wyjątek.
Nasza misja, o ile tylko ją przyjmiemy, polega na dodaniu do tej klasy nowej metody.
Metoda będzie nosić nazwę getValidationPercent, a jej zadaniem będzie informowanie
nas o odsetku udanych wywołań metody validateCustomer w czasie działania walidatora.
Od czego zaczniemy?
Kiedy musimy utworzyć obiekt w jarzmie testowym, często najlepszym podejściem jest
po prostu próba jego utworzenia. Moglibyśmy przeprowadzić rozległą analizę, aby dowie-
dzieć się, dlaczego będzie (albo też nie będzie) to łatwe bądź trudne, ale równie proste
będzie utworzenie klasy testowej jUnit, wprowadzenie do niej kodu i skompilowanie go.
public void testCreate() {
CreditValidator validator = new CreditValidator();
}

Najlepszym sposobem na stwierdzenie, czy będziesz mieć kłopoty podczas tworzenia in-
stancji klasy w jarzmie testowym, jest po prostu próba jej utworzenia. Napisz przypadek
testowy i spróbuj w jego ramach utworzyć obiekt. Kompilator powie Ci, czego potrzebu-
jesz, aby odnieść sukces.

To jest test konstrukcyjny. Testy konstrukcyjne wyglądają trochę dziwnie. Kiedy piszę
taki test, zwykle nie umieszczam w nim asercji. Po prostu próbuję skonstruować obiekt.
Później, gdy mam już możliwość tworzenia obiektów w jarzmie testowym, zwykle pozby-
wam się takiego testu albo zmieniam jego nazwę, żebym mógł go wykorzystać do spraw-
dzenia czegoś ważniejszego.
PRZYPADEK IRYTUJĄCEGO PARAMETRU 123

Wróćmy jednak do naszego przykładu.


Do konstruktora nie dodaliśmy jeszcze żadnych argumentów, więc kompilator narzeka.
Mówi nam, że dla klasy CreditValidator nie ma domyślnego konstruktora. Szperając
w kodzie, odkrywamy, że potrzebujemy klasy RGHConnection, klasy CreditMaster oraz
hasła. Klasy te mają tylko po jednym konstruktorze i wyglądają następująco:
public class RGHConnection
{
public RGHConnection(int port, String Name, string passwd)
throws IOException {
...
}
}

public class CreditMaster


{
public CreditMaster(String filename, boolean isLocal) {
...
}
}

Kiedy konstruowany jest obiekt klasy RGHConnection, łączy się on z serwerem.


Podczas połączenia pobierane są z serwera wszystkie dane potrzebne do zweryfikowania
kredytu klienta.
Druga klasa, CreditMaster, przekazuje nam informacje polityczne, z których korzy-
stamy, podejmując decyzje kredytowe. Podczas konstruowania obiekt klasy CreditMaster
wczytuje informacje z pliku i zapisuje je w pamięci, abyśmy mogli z nich skorzystać.
Wygląda więc na to, że umieszczenie tej klasy w jarzmie testowym będzie dość łatwe,
prawda? Nie tak szybko. Możemy napisać test, ale czy damy radę z nim pracować?
public void testCreate() throws Exception {
RGHConnection connection = new RGHConnection(DEFAULT_PORT,
"admin", "rii8ii9s");
CreditMaster master = new CreditMaster("crm2.mas", true);
CreditValidator validator = new CreditValidator(
connection, master, "a");
}

Okazuje się, że nawiązywanie przez metodę RGHConnection połączenia z serwerem


w czasie testu nie jest dobrym pomysłem. Zabiera to sporo czasu, a serwer nie zawsze
odpowiada. Z kolei klasa CreditMaster nie stwarza problemów. Kiedy tworzymy jej in-
stancję, plik jest wczytywany szybko, poza tym jest on tylko do odczytu, w związku z czym
nie musimy się obawiać, że testy go uszkodzą.
Kiedy chcemy utworzyć walidator, prawdziwą przeszkodę stanowi klasa RGHConnection
— jest ona irytującym parametrem. Gdybyśmy mogli utworzyć jakiś rodzaj fałszywego
obiektu tej klasy i sprawić, że CreditValidator uwierzy, iż komunikuje się z autentycznym
obiektem, moglibyśmy uniknąć całej masy problemów związanych z połączeniem. Spójrzmy
na metody, które udostępnia klasa RGHConnection (rysunek 9.1).
124 ROZDZIAŁ 9. NIE MOGĘ UMIEŚCIĆ TEJ KLASY W JARZMIE TESTOWYM

Rysunek 9.1. Klasa RGHConnection

Wygląda na to, że klasa RGHConnection zawiera zbiór metod, które obsługują mecha-
nizm nawiązywania połączenia: connect, disconnect i retry, a także kilka metod biz-
nesowych, takich jak RFDIReportFor i ACTIOReportFor. Pisząc naszą nową metodę dla klasy
CreditValidator, będziemy musieli wywołać metodę RFDIReportFor, aby uzyskać wszyst-
kie potrzebne nam informacje. Zazwyczaj dane te pochodzą z serwera, ale ponieważ
chcemy uniknąć nawiązywania rzeczywistego połączenia, będziemy musieli znaleźć jakiś
sposób na ich udostępnienie przez nas.
W tym przypadku najlepszą metodą na utworzenie fałszywego obiektu będzie wyod-
rębnienie interfejsu (361) w odniesieniu do klasy RGHConnection. Jeśli dysponujesz narzę-
dziem, które wspiera refaktoryzację, prawdopodobnie udostępnia ono wyodrębnianie
interfejsu. Jeżeli Twoje środowisko programistyczne nie wspiera tej techniki, pamiętaj,
że wyodrębnianie interfejsu jest na tyle proste, że można je wykonać ręcznie.
Po wyodrębnieniu interfejsu (361) otrzymujemy taką strukturę, jak pokazano na
rysunku 9.2.

Rysunek 9.2. Klasa RGHConnection po wyodrębnieniu interfejsu

Możemy przystąpić do pisania testów, tworząc niedużą fałszywą klasę, która udo-
stępnia potrzebne nam raporty:
public class FakeConnection implements IRGHConnection
{
PRZYPADEK IRYTUJĄCEGO PARAMETRU 125

public RFDIReport report;

public void connect() {}


public void disconnect() {}
public RFDIReport RFDIReportFor(int id) { return report; }
public ACTIOReport ACTIOReportFor(int customerID) { return null; }
}
Mając tę klasę, możemy rozpocząć tworzenie takich testów:
void testNoSuccess() throws Exception {
CreditMaster master = new CreditMaster("crm2.mas", true);
IRGHConnection connection = new FakeConnection();
CreditValidator validator = new CreditValidator(
connection, master, "a");
connection.report = new RFDIReport(...);
Certificate result = validator.validateCustomer(new Customer(...));

assertEquals(Certificate.VALID, result.getStatus());
}
Klasa FakeConnection jest trochę dziwna. Jak często w ogóle piszemy metody, które
nie mają żadnego ciała i tylko zwracają pustą wartość? Co gorsza, ma ona publiczną
zmienną, której każdy może nadać taką wartość, jaką tylko zechce. Wydaje się, że klasa ta
narusza wszelkie obowiązujące reguły. Otóż w rzeczywistości ich wcale nie narusza. Re-
guły dla klas umożliwiających przeprowadzanie testów są inne. Kod klasy FakeConnection
nie jest kodem produkcyjnym. Nigdy nie zostanie uruchomiony w naszej pełnej aplikacji
— będzie działać wyłącznie w jarzmie testowym.
Teraz, kiedy możemy już utworzyć walidator, mamy możliwość napisania metody
getValidationPercent. Oto test, który ją weryfikuje:
void testAllPassed100Percent() throws Exception {
CreditMaster master = new CreditMaster("crm2.mas", true);
IRGHConnection connection = new FakeConnection("admin", "rii8ii9s");
CreditValidator validator = new CreditValidator(
connection, master, "a");
connection.report = new RFDIReport(...);
Certificate result = validator.validateCustomer(new Customer(...));
assertEquals(100.0, validator.getValidationPercent(), THRESHOLD);
}

Kod testowy a kod produkcyjny


Kod testowy nie musi spełniać tych samych standardów co kod produkcyjny. Zazwyczaj
nie mam nic przeciwko naruszaniu zasady hermetyzacji poprzez tworzenie zmiennych pu-
blicznych, jeśli uprości to pisanie testów. Kod testowy powinien być jednak przejrzysty;
powinien być łatwy do zrozumienia i prosty w modyfikowaniu.
Spójrz na testy testNoSuccess i testAllPassed100Percent w tym przykładzie. Czy zawierają
one powielony kod? Tak. Powtórzone są trzy pierwsze wiersze. Powinny one zostać wyod-
rębnione i umieszczone w jednym miejscu — metodzie setUp() tej klasy testowej.
126 ROZDZIAŁ 9. NIE MOGĘ UMIEŚCIĆ TEJ KLASY W JARZMIE TESTOWYM

Test ten sprawdza, czy procent uwierzytelnienia wynosi mniej więcej 100.0, gdy
otrzymujemy pojedynczy, ważny certyfikat kredytu.
Test działa poprawnie, ale pisząc kod metody getValidationPercent, zauważamy coś
interesującego. Okazuje się, że nie będzie ona w ogóle używać metody CreditMaster.
Dlaczego więc ją piszemy i przekazujemy do obiektu klasy CreditValidator? Może
wcale nie musimy tego robić? Instancję klasy CreditValidator moglibyśmy utworzyć
w naszym teście następująco:
CreditValidator validator = new CreditValidator(connection, null, "a");

Czy jeszcze się nie pogubiłeś?


Sposób, w jaki ludzie reagują na tego typu kod, sporo mówi o systemach, z którymi oni
pracują. Jeżeli widząc ten kod, powiedziałeś: „O, świetnie. Do konstruktora przekazywana
jest wartość null. W naszym systemie ciągle tak robimy”, prawdopodobnie zajmujesz
się dość paskudnym systemem. Zapewne wszędzie masz w nim porozrzucane instrukcje
sprawdzające występowanie pustej wartości i pełno kodu warunkowego określającego, co
można, a co trzeba z nią zrobić. Z drugiej jednak strony, jeśli spojrzałeś na powyższy
kod i stwierdziłeś: „Z tym facetem chyba jest coś nie tak! Przekazywanie w systemie
wartości null? Czy on w ogóle ma o czymkolwiek pojęcie?” — otóż tym z Was z tej drugiej
grupy (a przynajmniej tym, którzy nadal to czytają i nie zamknęli z rozmachem tej
książki w księgarni) — chciałbym powiedzieć coś takiego: pamiętajcie, że robimy to tylko
w testach. Najgorsze, co się może stać, to to, że jakiś fragment kodu spróbuje skorzystać
z tej zmiennej. W naszym przypadku środowisko uruchomieniowe Javy zgłosi wyjątek.
Ponieważ jarzmo wyłapuje wszystkie wyjątki zgłaszane podczas testów, dość szybko
dowiemy się, czy jakiś parametr jest w ogóle używany.

Przekazywanie pustej wartości


Kiedy piszesz testy, a pewien obiekt wymaga parametru, który jest trudny do utworzenia,
rozważ przekazanie po prostu pustej wartości. Jeżeli parametr ten zostanie użyty podczas
wykonywania się testu, kod zgłosi wyjątek, który zostanie wychwycony przez jarzmo testowe.
Jeśli musisz mieć zachowanie, które istotnie wymaga obiektu, to będziesz mógł go skonstru-
ować i przekazać jako parametr.
Przekazywanie pustej wartości jest w niektórych językach bardzo wygodną techniką. Dobrze
się ona sprawdza w Javie i C# oraz niemal każdym języku, który zgłasza wyjątek, gdy w czasie
działania programu zostanie użyta pusta referencja. Z tego wynika, że przekazywanie pustej
wartości nie jest dobrym pomysłem w przypadku C i C++, chyba że masz pewność, iż program
uruchomieniowy wykryje błędy związane z pustymi wskaźnikami. W przeciwnym razie bę-
dziesz mieć do czynienia z testami, które się tajemniczo wysypują — o ile będziesz mieć szczęście.
Jeśli zabraknie Ci szczęścia, Twoje testy będą po prostu bezobjawowo i beznadziejnie złe.
W czasie działania będą niszczyć pamięć, a Ty nigdy się o tym nie dowiesz.

Kiedy pracuję w Javie, często zaczynam od następującego testu, a parametry uzupeł-


niam w miarę potrzeb.
PRZYPADEK IRYTUJĄCEGO PARAMETRU 127

public void testCreate() {


CreditValidator validator = new CreditValidator(null, null, "a");
}
Najważniejsze do zapamiętania jest to, aby nie przekazywać pustej wartości w kodzie
produkcyjnym, chyba że nie masz innego wyboru. Wiem, że niektóre biblioteki tego od
Ciebie oczekują, ale kiedy piszesz nowy kod, masz lepszą alternatywę. Jeśli kusi Cię użycie
pustej wartości w kodzie produkcyjnym, znajdź miejsca, w których są one zwracane i po-
bierane, po czym weź pod uwagę inne rozwiązanie. Zastanów się nad użyciem wzorca
pusty obiekt.

Wzorzec pusty obiekt


Wzorzec pusty obiekt to sposób na uniknięcie użycia pustych wartości w programach. Mamy
na przykład metodę, która zwraca dane pracownika po otrzymaniu jego numeru identyfikacyj-
nego. Co powinno zostać zwrócone, jeśli nie ma pracownika o podanym numerze?
for(Iterator it = idList.iterator(); it.hasNext(); ) {
EmployeeID id = (EmployeeID)it.next();
Employee e = finder.getEmployeeForID(id);
e.pay();
}

Mamy kilka możliwości. Możemy podjąć decyzję o zgłaszaniu wyjątków, dzięki czemu nie będzie
trzeba niczego zwracać, ale takie rozwiązanie zmusiłoby klienty do jawnej obsługi błędów. Mo-
glibyśmy także zwracać pustą wartość, lecz wówczas klienty musieliby jawnie sprawdzać, czy jej
nie otrzymują.
Jest jeszcze trzecie rozwiązanie. Czy powyższy kod tak naprawdę sprawdza, czy istnieje pra-
cownik, któremu należy zapłacić? Czy musi to robić? A gdybyśmy tak mieli klasę o nazwie
NullEmployee? Instancja tej klasy nie ma nazwiska ani adresu, a kiedy polecisz jej dokonanie
wypłaty, po prostu nic nie zrobi.
W takich kontekstach puste obiekty mogą być przydatne — pomagają one chronić klienty przed
jawną obsługą błędów. Chociaż puste obiekty są pomocne, powinieneś zachować ostrożność,
gdy z nich korzystasz. Oto przykład niewłaściwego sposobu liczenia pracowników, którym wy-
płacono wynagrodzenie:
int employeesPaid = 0;
for(Iterator it = idList.iterator(); it.hasNext(); ) {
EmployeeID id = (EmployeeID)it.next();
Employee e = finder.getEmployeeForID(id);
e.pay();
mployeesPaid++; // błąd!
}

Jeśli któryś ze zwróconych pracowników jest pracownikiem pustym, to zliczenie będzie błędne.
Puste obiekty są przydatne zwłaszcza wtedy, gdy klient nie musi sprawdzać, czy dana operacja się
powiodła. W wielu przypadkach możemy tak dopracować nasz projekt, abyśmy mieli do czy-
nienia z takim właśnie rozwiązaniem.
128 ROZDZIAŁ 9. NIE MOGĘ UMIEŚCIĆ TEJ KLASY W JARZMIE TESTOWYM

Przekazanie pustej wartości i wyodrębnienie interfejsu (361) to dwa sposoby na


poradzenie sobie z irytującymi parametrami. Czasami można jednak skorzystać z jeszcze
innej możliwości. Jeżeli sprawiająca problemy zależność w parametrze nie jest bezpo-
średnio zakodowana w swoim konstruktorze, w celu pozbycia się jej możemy skorzystać
z techniki tworzenia podklasy i przesłaniania metody (398). W tym przypadku rozwią-
zanie takie byłoby możliwe. Jeśli konstruktor klasy RGHConnection używa metody connect
w celu nawiązania połączenia, moglibyśmy pozbyć się zależności, przesłaniając wywołanie
connect() w testowanej podklasie. Tworzenie podklasy i przesłanianie metody (398) może
być bardzo przydatnym sposobem usuwania zależności, ale musimy mieć pewność, że nie
zmieniamy zachowania, które chcemy przetestować, gdy z niego korzystamy.

Przypadek ukrytej zależności


Niektóre klasy bywają podstępne. Patrzymy na nie, znajdujemy konstruktor, którego
chcemy użyć, i próbujemy go wywołać. Wtedy trach! Napotykamy przeszkodę. Jedną
z najczęściej występujących przeszkód jest ukryta zależność — konstruktor korzysta z za-
sobów, do których nie mamy łatwego dostępu w naszym jarzmie testowym. Zobaczymy
taką sytuację w następnym przykładzie; źle zaprojektowaną klasę w C++, która obsługuje
listę mailingową:
class mailing_list_dispatcher
{
public:
mailing_list_dispatcher ();
virtual ~mailing_list_dispatcher;

void send_message(const std::string& message);


void add_recipient(const mail_txm_id id,
const mail_address& address);
...

private:
mail_service *service;
int status;
};

Oto fragment konstruktora tej klasy. Alokuje ona obiekt mail_service za pomocą in-
strukcji new na liście inicjatora konstruktora. To kiepski styl, ale potem jest jeszcze gorzej.
Konstruktor wykonuje sporo szczegółowej pracy z tym obiektem; korzysta też z magicznej
liczby 12. Co ma oznaczać to 12?
mailing_list_dispatcher::mailing_list_dispatcher()
: service(new mail_service), status(MAIL_OKAY)
{
const int client_type = 12;
service->connect();
if (service->get_status() == MS_AVAILABLE) {
PRZYPADEK UKRYTEJ ZALEŻNOŚCI 129

service->register(this, client_type, MARK_MESSAGES_OFF);


service->set_param(client_type, ML_NOBOUNCE | ML_REPEATOFF);
}
else
status = MAIL_OFFLINE;
...
}

Podczas testu możemy utworzyć instancję tej klasy, ale chyba nie przyniesie nam ona
większych korzyści. Przede wszystkim musimy połączyć się z bibliotekami pocztowymi
i skonfigurować system pocztowy, aby obsługiwał rejestrowanie. Jeśli w trakcie testów
użyjemy funkcji send_message, to naprawdę wyślemy maile do ludzi. Automatyczne te-
stowanie tej funkcjonalności będzie trudne, chyba że skonfigurujemy specjalną skrzynkę
pocztową i będziemy się z nią regularnie łączyć, czekając na nadejście wiadomości. Takie
rozwiązanie byłoby dobre podczas całościowych testów systemu, ale wszystko, co chcemy
teraz zrobić, to tylko dodać do klasy kilka przetestowanych funkcjonalności, więc sposób
ten byłby lekką przesadą. Jak moglibyśmy utworzyć prosty obiekt w celu dodania jakiejś
nowej funkcjonalności?
Podstawowy problem w tym przypadku polega na tym, że zależność od obiektu
mail_service jest ukryta w konstruktorze mailing_list_dispatcher. Gdyby istniał jakiś
sposób na zastąpienie tego obiektu fałszywką, moglibyśmy dokonać rozpoznania, posługując
się fałszywym obiektem i uzyskać informację zwrotną podczas modyfikowania klasy.
Jedną z technik, które możemy wykorzystać, jest parametryzacja konstruktora (377).
Przy jej pomocy wyciągamy na zewnątrz zależność istniejącą w konstruktorze, przekazując
ją do konstruktora.
Oto jak wygląda kod konstruktora po sparametryzowaniu konstruktora (377):
mailing_list_dispatcher::mailing_list_dispatcher(mail_service *service)
: status(MAIL_OKAY)
{
const int client_type = 12;
service->connect();
if (service->get_status() == MS_AVAILABLE) {
service->register(this, client_type, MARK_MESSAGES_OFF);
service->set_param(client_type, ML_NOBOUNCE | ML_REPEATOFF);
}
else
status = MAIL_OFFLINE;
...
}

Jedyna różnica tak naprawdę sprowadza się do tego, że obiekt mail_service jest two-
rzony poza klasą i do niej przekazywany. Być może nie wygląda to na znaczne usprawnie-
nie, ale teraz mamy ogromne możliwości. Możemy skorzystać z wyodrębniania interfejsu
(361), aby uzyskać interfejs dla mail_service. Jeden z implementerów tego interfejsu może
być klasą produkcyjną, która faktycznie wysyła maile. Inny może być fałszywą klasą, rozpo-
znającą to, co robimy podczas testów, oraz informującą nas, że istotnie to się stało.
130 ROZDZIAŁ 9. NIE MOGĘ UMIEŚCIĆ TEJ KLASY W JARZMIE TESTOWYM

Parametryzacja konstruktora (377) jest bardzo wygodnym sposobem na przesu-


nięcie zależności z konstruktora na zewnątrz, jednak programiści nie biorą go zbyt często
pod uwagę. Jedną z przeszkód stanowi fakt, że wiele osób sądzi, iż w celu przekazania
nowego parametru konieczna będzie zmiana wszystkich klientów klasy, co jednak nie
jest prawdą. Możemy sobie z tym poradzić następująco. Najpierw wyodrębniamy ciało
konstruktora i tworzymy z niego nową metodę o nazwie initialize. W przeciwieństwie
do większości innych sposobów wyodrębniania metod możemy to całkiem bezpiecznie
zrobić bez testów, gdyż w trakcie tej czynności zachowujemy sygnatury (314).
void mailing_list_dispatcher::initialize(mail_service *service)
{
status = MAIL_OKAY;
const int client_type = 12;
service.connect();
if (service->get_status() == MS_AVAILABLE) {
service->register(this, client_type, MARK_MESSAGES_OFF);
service->set_param(client_type, ML_NOBOUNCE | ML_REPEATOFF);
}
else
status = MAIL_OFFLINE;
...
}

mailing_list_dispatcher::mailing_list_dispatcher(mail_service *service)
{
initialize(service);
}

Teraz możemy udostępnić konstruktor, który ma oryginalną sygnaturę. Testy mogą


wywoływać konstruktor sparametryzowany przez mail_service, natomiast klienty mogą
wywoływać go w poniższy sposób; nie muszą wiedzieć, że coś uległo zmianie.
mailing_list_dispatcher::mailing_list_dispatcher()
{
initialize(new mail_service);
}

Tego typu refaktoryzacja jest jeszcze łatwiejsza w takich językach jak C# i Java, po-
nieważ możemy w nich wywoływać konstruktory z poziomu innych konstruktorów.
Gdybyśmy na przykład robili coś podobnego w C#, wynikowy kod mógłby wyglądać
następująco:
public class MailingListDispatcher
{
public MailingListDispatcher()
: this(new MailService())
{}

public MailingListDispatcher(MailService service) {


...
}
}
PRZYPADEK KONSTRUKCYJNEGO KŁĘBOWISKA 131

Z zależnościami ukrytymi w konstruktorach można sobie radzić za pomocą wielu


technik. Zwykle możemy zastosować wyodrębnianie i przesłanianie gettera (353), wyod-
rębnianie i przesłanianie metody wytwórczej (351) oraz zastępowanie zmiennej instan-
cji (401), najbardziej jednak lubię korzystać z parametryzacji konstruktora (377).
Kiedy w konstruktorze tworzony jest obiekt i nie ma on żadnych zależności konstrukcyj-
nych, łatwą do zastosowania techniką będzie właśnie parametryzacja konstruktora.

Przypadek konstrukcyjnego kłębowiska


Parametryzacja konstruktora (377) jest jedną z najprostszych technik, z których mo-
żemy skorzystać w celu usunięcia zależności ukrytych w konstruktorze. Jest też techniką,
po którą często sięgam w pierwszej kolejności. Niestety, nie zawsze jest ona najlepszym
wyborem. Jeśli konstruktor tworzy sporą liczbę obiektów lub ma dostęp do wielu zmien-
nych globalnych, możemy w rezultacie uzyskać bardzo długą listę parametrów. W gorszych
sytuacjach konstruktor tworzy kilka obiektów, po czym używa ich do utworzenia kolej-
nych obiektów, tak jak poniżej:
class WatercolorPane
{
public:
WatercolorPane(Form *border, WashBrush *brush, Pattern *backdrop)
{
...
anteriorPanel = new Panel(border);
anteriorPanel->setBorderColor(brush->getForeColor());
backgroundPanel = new Panel(border, backdrop);
cursor = new FocusWidget(brush, backgroundPanel);
...
}
...
}

Gdybyśmy chcieli zrobić rozpoznanie, posługując się obiektem cursor, mielibyśmy


problem. Obiekt ten jest osadzony w kłębowisku tworzonych obiektów. Moglibyśmy spró-
bować przenieść poza klasę cały kod użyty do tworzenia kursora. W dalszej kolejności
klient mógłby utworzyć kursor i przekazać go jako argument. Nie będzie to jednak bez-
pieczne, jeśli nie mamy na miejscu testów, poza tym rozwiązanie takie byłoby sporym
utrudnieniem dla klientów tej klasy.
Jeśli dysponujemy narzędziem refaktoryzującym, które bezpiecznie wyodrębnia metody,
możemy skorzystać z techniki wyodrębniania i przesłaniania metody wytwórczej (351)
w odniesieniu do kodu konstruktora, co jednak nie sprawdzi się we wszystkich językach.
Możemy tak postąpić w Javie i C#, ale już C++ nie pozwala wywoływać funkcji wirtu-
alnych w konstruktorach w celu odwołania się do wirtualnych funkcji zdefiniowanych
w klasach pochodnych. Poza tym tak w ogóle to nie jest dobry pomysł. Funkcje w klasach
pochodnych często zakładają, że mogą korzystać ze zmiennych w ich klasie bazowej.
132 ROZDZIAŁ 9. NIE MOGĘ UMIEŚCIĆ TEJ KLASY W JARZMIE TESTOWYM

Dopóki konstruktor klasy bazowej nie zakończy w pełni swojej pracy, istnieje ryzyko, że
przesłonięta funkcja, która go wywołuje, może uzyskać dostęp do niezainicjalizowanej
zmiennej.
Inną opcją jest zastępowanie zmiennej instancji (401). Piszemy setter dla klasy,
który umożliwi nam podstawienie innej instancji po skonstruowaniu obiektu.
class WatercolorPane
{
public:
WatercolorPane(Form *border, WashBrush *brush, Pattern *backdrop)
{
...
anteriorPanel = new Panel(border);
anteriorPanel->setBorderColor(brush->getForeColor());
backgroundPanel = new Panel(border, backdrop);

cursor = new FocusWidget(brush, backgroundPanel);


...
}

void supersedeCursor(FocusWidget *newCursor)


{
delete cursor;
cursor = newCursor;
}
}

Musimy być bardzo ostrożni, gdy stosujemy tę technikę refaktoryzacji w C++. Kiedy
zastępujemy obiekt, powinniśmy pozbyć się jego starej instancji. Często oznacza to, że
musimy skorzystać z operatora delete w celu wywołania destruktora i zniszczenia pamięci
obiektu. Kiedy to robimy, musimy wiedzieć, co robi destruktor i czy niszczy on coś, co zo-
stało przekazane konstruktorowi obiektu. Jeśli nie będziemy ostrożni podczas czyszczenia
pamięci, możemy spowodować pewne subtelne błędy.
W większości innych języków zastępowanie zmiennej instancji (401) jest dość proste
w użyciu. Oto wynik zastosowania tej techniki zapisany w Javie. Nie musimy robić nic
szczególnego, aby pozbyć się obiektu, do którego odwołuje się cursor — proces odśmie-
cania pamięci i tak w końcu się go pozbędzie. Powinniśmy jednak być szczególnie uważni,
aby nie korzystać z tej metody w kodzie produkcyjnym. Jeżeli obiekty, które zastępujemy,
zarządzają innymi zasobami, możemy spowodować całkiem poważne problemy związane
z dostępem do zasobów.
void supersedeCursor(FocusWidget newCursor) {
cursor = newCursor;
}

Teraz, gdy mamy już zastępczą metodę, możemy podjąć się próby utworzenia instancji
FocusWidget poza klasą i przekazać go do obiektu po jego skonstruowaniu. Ponieważ
potrzebujemy przeprowadzić rozpoznanie, w odniesieniu do klasy FocusWidget możemy
skorzystać z techniki wyodrębniania interfejsu (361) albo wyodrębniania implementera
PRZYPADEK IRYTUJĄCEJ ZALEŻNOŚCI GLOBALNEJ 133

(356) i utworzyć fałszywy obiekt do przekazania. Z pewnością będzie to łatwiejsze niż two-
rzenie obiektu FocusWidget w konstruktorze.
TEST(renderBorder, WatercolorPane)
{
...
TestingFocusWidget *widget = new TestingFocusWidget;
WatercolorPane pane(form, border, backdrop);

pane.supersedeCursor(widget);

LONGS_EQUAL(0, pane.getComponentCount());
}

Nie lubię korzystać z techniki zastępowania zmiennej instancji (401). Używam jej,
kiedy nie mam już innego wyjścia. Prawdopodobieństwo pojawienia się problemów z za-
rządzaniem zasobami jest zbyt duże. Mimo to korzystam z niej od czasu do czasu w C++.
Często wolałbym zastosować wyodrębnianie i przesłanianie metody wytwórczej (351),
co jednak jest niemożliwe w przypadku konstruktorów w języku C++. Z tego też powodu
rzadko uciekam się do zastępowania zmiennej instancji (401).

Przypadek irytującej zależności globalnej


Od lat ludzie w branży programistycznej narzekają, że nie ma na rynku większej liczby
komponentów wielokrotnego użytku. Wraz z upływem czasu sytuacja polepszyła się
— istnieje wiele komercyjnych oraz otwartych platform, ale w zasadzie z wielu z nich tak
naprawdę nie korzystamy; to raczej one używają naszego kodu. Platformy te często zarzą-
dzają cyklem życia aplikacji, a my piszemy kod wypełniający puste miejsca. Możemy to
zaobserwować we wszystkich rodzajach platform, od ASP.NET aż do Java Struts. W taki
sposób działa nawet xUnit — piszemy klasy testowe, a on je wywołuje i wyświetla wyniki
ich działania.
Platformy rozwiązują wiele problemów i nadają nam impet, gdy rozpoczynamy nowe
projekty, ale to nie takiego rodzaju wielokrotnego wykorzystania oczekiwano we wcze-
snych latach rozwoju oprogramowania. Wielokrotne użycie w starym stylu ma miejsce
wtedy, gdy znajdujemy jakąś klasę lub zbiór klas, których chcemy użyć w naszej aplikacji,
i po prostu to robimy. Dodajemy je do naszego projektu i z nich korzystamy. Byłoby miło,
gdybyśmy mogli tak robić rutynowo, ale szczerze mówiąc — myślę, że sami siebie oszuku-
jemy nawet wtedy, gdy tylko myślimy o takim rodzaju wielokrotnego użycia, skoro nie
potrafimy wyciągnąć z pierwszej lepszej aplikacji dowolnej klasy i oddzielnie jej skompi-
lować w jarzmie testowym bez konieczności wykonania całej masy pracy (ale zrzędzę).
Wiele różnych rodzajów zależności może utrudniać tworzenie i używanie klas na
platformach testowych, a jedną z najgorszych, z jakimi możemy mieć do czynienia, jest
użycie zmiennych globalnych. W prostszych przypadkach w celu ominięcia takich zależ-
ności możemy posłużyć się parametryzacją konstruktora (377), parametryzacją metody
134 ROZDZIAŁ 9. NIE MOGĘ UMIEŚCIĆ TEJ KLASY W JARZMIE TESTOWYM

(381) oraz wyodrębnianiem i przesłanianiem wywołania (349), ale czasami zależności


globalne są tak wszechobecne, że prościej będzie rozprawić się z nimi u źródła. Z taką sy-
tuacją spotkamy się w następnym przykładzie, którym jest aplikacja w Javie, rejestrująca
pozwolenia na budowę w pewnej agencji rządowej. Oto jedna z jej głównych klas:
public class Facility
{
private Permit basePermit;

public Facility(int facilityCode, String owner, PermitNotice notice)


throws PermitViolation {

Permit associatedPermit =
PermitRepository.getInstance().findAssociatedPermit(notice);

if (associatedPermit.isValid() && !notice.isValid()) {


basePermit = associatedPermit;
}
else if (!notice.isValid()) {
Permit permit = new Permit(notice);
permit.validate();
basePermit = permit;
}
else
throw new PermitViolation(permit);
}
...
}

Chcielibyśmy utworzyć klasę Facility w jarzmie testowym, w związku z czym za-


czynamy od próby utworzenia jej obiektu:
public void testCreate() {
PermitNotice notice = new PermitNotice(0, "a");
Facility facility = new Facility(Facility.RESIDENCE, "b", notice);
}

Test kompiluje się prawidłowo, ale kiedy zaczynamy pisać kolejne testy, zauważamy
pewien problem. Konstruktor korzysta z klasy o nazwie PermitRepository i aby nasze testy
zostały poprawnie skonfigurowane, musi zostać zainicjalizowany określonym zestawem
pozwoleń. Przebiegłe, co? Oto ta problematyczna instrukcja w konstruktorze:
Permit associatedPermit =
PermitRepository.getInstance().findAssociatedPermit(notice);

Moglibyśmy ominąć tę przeszkodę, parametryzując konstruktor, ale w tej aplikacji nie


jest to odosobniony przypadek. Istnieje jeszcze 10 innych klas, które zawierają mniej więcej
taki sam wiersz kodu. Tkwi on w konstruktorach, metodach zwykłych i statycznych.
Możemy tylko wyobrazić sobie, ile czasu byśmy poświęcili, walcząc z tym problemem
w bazie kodu.
Jeżeli uczyłeś się kiedyś o wzorcach projektowych, być może rozpoznałeś w tym przy-
kładzie wzorzec projektowy singleton (370). Metoda getInstance klasy PermitRepository
PRZYPADEK IRYTUJĄCEJ ZALEŻNOŚCI GLOBALNEJ 135

jest metodą statyczną, której zadaniem jest zwrócenie jedynej instancji klasy Permit
Repository, która może istnieć w naszej aplikacji. Pole przechowujące tę instancję także
jest statyczne i znajduje się w tej klasie.
W Javie wzorzec singleton jest jednym z mechanizmów używanych do tworzenia
zmiennych globalnych. Zazwyczaj wykorzystywanie zmiennych globalnych jest złym
pomysłem z kilku powodów. Jednym z nich jest nieprzejrzystość. Kiedy oglądamy frag-
ment kodu, dobrze byłoby wiedzieć, na co może on wpływać. Na przykład w Javie —
jeśli chcemy zrozumieć, jaki wpływ wywiera na różne elementy poniższy kod — musimy
zajrzeć do kilku zaledwie miejsc.
Account example = new Account();
example.deposit(1);
int balance = example.getBalance();

Wiemy, że obiekt klasy Account może mieć wpływ na elementy, które przekazujemy
do konstruktora Account, ale my niczego nie przekazujemy. Obiekty klasy Account mogą
też wpływać na obiekty, które przekazujemy do metody jako parametry, ale w tym przy-
padku nie przekazujemy nic, co mogłoby ulec zmianie, a jedynie liczbę całkowitą. W miej-
scu tym przypisujemy zwracaną wartość getBalance zmiennej i tak naprawdę jest to jedyny
element, który może być zmieniany przez powyższe instrukcje.
Kiedy korzystamy ze zmiennych globalnych, sytuacja zostaje postawiona na głowie.
Możemy patrzeć na użycie takich klas jak Account i nie mieć pojęcia, czy ma ona dostęp
i modyfikuje zmienne zadeklarowane w jakimś innym miejscu programu. Nie trzeba
nadmieniać, że z tego powodu programy mogą być trudniejsze do zrozumienia.
Trudnym zadaniem w naszej sytuacji jest konieczność określenia, które zmienne
globalne zostały użyte w klasie, i nadanie im odpowiednich wartości na potrzeby testu.
Do tego musimy to robić przed każdym testem, jeśli konfiguracja testów ma się zmieniać.
Zadanie to jest dość żmudne; musiałem je wykonywać w odniesieniu do całej masy syste-
mów, aby można było je poddać testom, i wraz z upływem czasu nie stało się ono nawet
na jotę bardziej ekscytujące.
Powróćmy jednak do naszego przykładu.
PermitRepository jest singletonem. Z tego względu jest on szczególnie trudny do
sfałszowania. Cała koncepcja kryjąca się za wzorcem singletona polega na uniemożli-
wieniu tworzenia więcej niż jednej jego instancji w danej aplikacji. Takie rozwiązanie
może sprawdzać się w kodzie produkcyjnym, ale w przypadku testowania każdy test w ze-
stawie powinien być w pewnym sensie miniaplikacją — powinien być całkowicie odizolo-
wany od pozostałych testów. Aby zatem uruchomić w jarzmie testowym kod zawierający
singletony, musimy osłabić własność singletona. Oto jak to zrobimy.
Najpierw do klasy singletona dodamy nową metodę statyczną. Pozwoli nam ona za-
stąpić statyczną instancję w singletonie. Nazwiemy ją setTestingInstance.
public class PermitRepository
{
private static PermitRepository instance = null;
136 ROZDZIAŁ 9. NIE MOGĘ UMIEŚCIĆ TEJ KLASY W JARZMIE TESTOWYM

private PermitRepository() {}

public static void setTestingInstance(PermitRepository newInstance)


{
instance = newInstance;
}

public static PermitRepository getInstance()


{
if (instance == null) {
instance = new PermitRepository();
}
return instance;
}
public Permit findAssociatedPermit(PermitNotice notice) {
...
}
...
}

Teraz, gdy mamy już setter, możemy utworzyć testową instancję klasy PermitRepository
i nadać jej wartość. W naszej testowej metodzie setUp chcielibyśmy dodać następujący kod:
public void setUp() {
PermitRepository repository = new PermitRepository();
...
// w tym miejscu dodaj zezwolenia do repozytorium
...
PermitRepository.setTestingInstance(repository);
}

Wprowadzanie statycznego settera (370) nie jest jedynym sposobem na poradzenie sobie
z taką sytuacją. Oto inne podejście. Do metody resetForTesting() możemy dodać singleton,
który wygląda następująco:
public class PermitRepository
{
...
public void resetForTesting() {
instance = null;
}
...
}

Jeśli wywołamy tę metodę z naszej metody testowej setUp (a dobrym pomysłem będzie
wywołanie jej także z metody tearDown), będziemy tworzyć świeże singletony w każdym te-
ście. Za każdym razem singleton będzie inicjalizował się od nowa. Schemat ten dobrze się
sprawdza, kiedy metody publiczne w singletonie pozwalają konfigurować jego stan w do-
wolny sposób, jaki tylko będzie potrzebny podczas testowania. Jeśli singleton nie ma takich
publicznych metod albo korzysta z zewnętrznych zasobów, które wpływają na jego stan,
lepszym wyborem będzie wprowadzenie statycznego settera (370). Dzięki temu będziesz
mógł tworzyć podklasy singletona, przesłaniać metody, usuwać zależności i dodawać me-
tody publiczne do podklas, aby poprawnie konfigurować ich stan.
PRZYPADEK IRYTUJĄCEJ ZALEŻNOŚCI GLOBALNEJ 137

Czy to zadziała? Jeszcze nie. Kiedy programiści korzystają ze wzorca projektowego


singleton (370), często ich konstruktor klasy singletona jest prywatny, i mają ku temu
dobry powód. Jest to najbardziej przejrzysty sposób zagwarantowania, że nikt spoza tej
klasy nie będzie mógł utworzyć kolejnej instancji singletona.
W tym momencie pojawia się konflikt między dwoma założeniami naszego projektu.
Chcemy mieć pewność, że w systemie istnieje tylko jedna instancja klasy PermitRepository,
ale chcemy też dysponować systemem, w którym klasy można testować niezależnie od sie-
bie. Czy uda nam się osiągnąć jednocześnie oba te cele?
Cofnijmy się na chwilę. Dlaczego chcemy mieć w systemie tylko jedną instancję
klasy? Odpowiedź będzie się różnić w zależności od systemu, ale oto kilka najczęściej
spotykanych powodów:
1. Modelujemy rzeczywisty świat, a w rzeczywistym świecie istnieje tylko jedna
taka rzecz. Właśnie takie są niektóre systemy kontrolujące sprzęt. Programiści
tworzą klasę dla każdego urządzenia, które musi być kontrolowane. Wychodzą
z założenia, że jeśli istnieje tylko po jednym urządzeniu, każde z nich powinno być
singletonem. Podobnie sprawy się mają w przypadku baz danych. W naszej agencji
istnieje tylko jeden zbiór pozwoleń, a zatem element zapewniający do nich dostęp
powinien być singletonem.
2. Jeśli utworzymy dwie takie rzeczy, możemy znaleźć się w poważnych opałach.
Sytuacja taka również często ma miejsce w dziedzinie sterowania urządzeniami.
Wyobraź sobie przypadkowe utworzenie dwóch kontrolerów prętów uranowych
i umożliwienie dwóm różnym częściom programu sterowanie nimi w tym samym
czasie bez wzajemnej wiedzy o sobie.
3. Jeśli ktoś utworzy dwie takie rzeczy, będziemy zużywać zbyt wiele zasobów.
To zdarza się często. Zasoby mogą być obiektami fizycznymi, takimi jak miejsce
na dysku albo zużycie pamięci, ale mogą być także abstrakcyjne, jak na przykład
liczba licencji na oprogramowanie.
Takie są powody, dla których wymusza się istnienie pojedynczych instancji, ale nie są
to główne powody, dla których singletony są używane. Programiści często tworzą single-
tony, ponieważ chcą mieć zmienne globalne. Uważają, że przekazywanie zmiennych
do miejsc, w których będą one potrzebne, jest zbyt kłopotliwe.
Jeśli mamy singletona z tej drugiej przyczyny, to naprawdę nie ma powodu do zacho-
wania jego własności. Nasz konstruktor może mieć zakres chroniony, publiczny albo
pakietu, a przy tym nadal będziemy dysponować przyzwoitym, możliwym do testowania
systemem. W innym przypadku i tak warto poszukać alternatywy. Jeśli zajdzie taka potrzeba,
wprowadzimy inny rodzaj ochrony. Moglibyśmy dodać w naszym systemie kompilującym
sprawdzanie we wszystkich plikach źródłowych, czy metoda setTestingInstance
nie jest wywoływana przez kod nietestujący. Tak samo możemy postąpić w odniesieniu
do kontroli wykonywanej w czasie działania programu. Jeśli metoda setTestingInstance
zostanie wywołana podczas działania aplikacji, możemy podnieść alarm albo zawiesić
system i poczekać na działanie ze strony użytkownika. Faktem jest, że chociaż wymuszenie
138 ROZDZIAŁ 9. NIE MOGĘ UMIEŚCIĆ TEJ KLASY W JARZMIE TESTOWYM

„singletonowości” nie było możliwe w wielu językach przed pojawieniem się zorientowa-
nia obiektowego, to jednak programiści zdołali utworzyć wiele bezpiecznych syste-
mów. W końcu wszystko sprowadza się do odpowiedzialnego projektu i kodowania.
Jeżeli naruszenie własności singletona nie stanowi większego problemu, możemy zdać
się na regułę stosowaną przez zespół. Przykładowo każdy w zespole powinien zrozumieć,
że w aplikacji mamy jedną instancję bazy danych i że nie powinniśmy mieć kolejnej.
Aby osłabić właściwość singletona w klasie PermitRepository, możemy przekształcić
konstruktor w publiczny. Takie rozwiązanie będzie nas satysfakcjonować, dopóki publiczne
metody tej klasy pozwalają nam robić wszystko, czego potrzebujemy w celu skonfigurowa-
nia naszego repozytorium na potrzeby testów. Jeśli na przykład w klasie PermitRepository
znajduje się metoda o nazwie addPermit, umożliwiająca dodawanie pozwoleń, które będą
nam potrzebne w testach, być może wystarczy po prostu, że umożliwimy sobie tworzenie
repozytoriów i użyjemy ich w naszych testach. W innym przypadku możemy nie mieć
potrzebnego nam dostępu lub — co gorsza — singleton może robić rzeczy, co do których
nie chcielibyśmy, aby się działy w jarzmie testowym, takie jak komunikowanie się w tle
z bazą danych. W takich okolicznościach możemy utworzyć podklasę i przesłonić metodę
(398), po czym utworzyć klasy pochodne, które ułatwią nam testowanie.
Oto przykład z naszego systemu pozwoleń. Oprócz metod i zmiennych, które sprawiają,
że PermitRepository jest singletonem, mamy także następującą metodę:
public class PermitRepository
{
...
public Permit findAssociatedPermit(PermitNotice notice) {
// otwórz bazę danych z pozwoleniami
...

// wybierz na podstawie wartości w powiadomieniu


...

// sprawdź, czy mamy tylko jedno pasujące pozwolenie; jeśli nie, zgłoś błąd
...

// zwróć pasujące pozwolenie


...
}
}

Jeśli chcemy uniknąć komunikacji z bazą danych, możemy utworzyć następującą


podklasę klasy PermitRepository:
public class TestingPermitRepository extends PermitRepository
{
private Map permits = new HashMap();

public void addAssociatedPermit(PermitNotice notice, permit) {


permits.put(notice, permit);
}

public Permit findAssociatedPermit(PermitNotice notice) {


PRZYPADEK IRYTUJĄCEJ ZALEŻNOŚCI GLOBALNEJ 139

return (Permit)permits.get(notice);
}
}

Kiedy tak zrobimy, będziemy mogli zachować część własności singletona. Ponieważ
korzystamy z podklasy klasy PermitRepository, sprawimy, że będzie chroniony raczej
nasz konstruktor klasy PermitRepository niż publiczny. Dzięki temu zabezpieczymy się
przed utworzeniem więcej niż jednej instancji tej klasy, chociaż będziemy mogli tworzyć
jej podklasy.
public class PermitRepository
{
private static PermitRepository instance = null;

protected PermitRepository() {}

public static void setTestingInstance(PermitRepository newInstance)


{
instance = newInstance;
}

public static PermitRepository getInstance()


{
if (instance == null) {
instance = new PermitRepository();
}
return instance;
}
public Permit findAssociatedPermit(PermitNotice notice)

{
...
}
...
}

W wielu przypadkach możemy skorzystać z tworzenia podklasy i przesłaniania me-


tody (398) — takiego jak powyżej — aby wstawić na miejsce fałszywy singleton. Innym ra-
zem zależności będą do tego stopnia rozbudowane, że łatwiej będzie wyodrębnić interfejs
(361) względem singletona i zmienić wszystkie referencje w aplikacji, aby używały nazwy
interfejsu. Może to kosztować sporo pracy, ale w celu dokonania takich zmian mogliby-
śmy skorzystać ze wsparcia kompilatora (317). Po wyodrębnieniu klasa PermitRepository
będzie wyglądać następująco:
public class PermitRepository implements IPermitRepository
{
private static IPermitRepository instance = null;

protected PermitRepository() {}

public static void setTestingInstance(IPermitRepository newInstance)


{
instance = newInstance;
140 ROZDZIAŁ 9. NIE MOGĘ UMIEŚCIĆ TEJ KLASY W JARZMIE TESTOWYM

public static IPermitRepository getInstance()


{
if (instance == null) {
instance = new PermitRepository();
}
return instance;
}

public Permit findAssociatedPermit(PermitNotice notice)


{
...
}
...
}

Interfejs IPermitRepository będzie mieć sygnatury wszystkich publicznych, nie-


statycznych metod klasy PermitRepository.
public interface IPermitRepository
{
Permit findAssociatedPermit(PermitNotice notice);
...
}

Jeśli używasz języka, który jest wyposażony w narzędzie do refaktoryzacji, mógłbyś


wykonać takie wyodrębnienie interfejsu automatycznie. Jeśli Twój język nie ma tej
możliwości, łatwiej będzie skorzystać z techniki wyodrębniania implementera (356).
Cały ten proces refaktoryzacji nosi nazwę wprowadzania statycznego settera (370).
Jest to technika, z której możemy skorzystać, aby rozmieścić testy mimo istnienia rozle-
głych zależności globalnych. Niestety, niespecjalnie nadaje się ona do obchodzenia global-
nych zależności. Jeśli musisz poradzić sobie z tym problemem, posłuż się parametryzacją
metody (381) i parametryzacją konstruktora (377). Za pomocą tych technik refakto-
ryzacji zmienisz referencję globalną na zmienną tymczasową w metodzie lub na pole
w obiekcie. Wada parametryzacji metody (381) polega na tym, że w wyniku jej zasto-
sowania możesz uzyskać wiele dodatkowych metod, które będą rozpraszać osoby pró-
bujące zrozumieć klasy. Z kolei wada parametryzacji konstruktora (377) jest taka, że
każdy obiekt korzystający ze zmiennej globalnej otrzymuje w rezultacie dodatkowe pole.
Pole to będzie musiało zostać przekazane do konstruktora, przez co klasa tworząca obiekt
także będzie musiała mieć dostęp do instancji. Jeżeli to dodatkowe pole będzie potrzebne
w zbyt wielu obiektach, może to znacząco wpłynąć na ilość pamięci używanej przez
aplikację, chociaż często wskazuje to na inne problemy w projekcie.
Przyjrzyjmy się najgorszemu przypadkowi. Mamy aplikację z kilkoma setkami klas,
które podczas działania programu tworzą tysiące obiektów, a każdy z tych obiektów
wymaga dostępu do bazy danych. Pierwsze pytanie, które przychodzi mi na myśl, nawet
bez spojrzenia na aplikację, brzmi: dlaczego? Jeśli system robi jeszcze coś innego oprócz
komunikowania się z bazą danych, można przeprowadzić jego refaktoryzację, dzięki czemu
PRZYPADEK STRASZLIWYCH ZALEŻNOŚCI DYREKTYW INCLUDE 141

część klas zajmie się tymi innymi rzeczami, a pozostałe klasy będą zapisywać i pobierać
dane z bazy. Kiedy podejmujemy skoordynowane działania w celu rozdzielenia odpowie-
dzialności w aplikacji, zależności stają się lokalne — referencja do bazy danych w każdym
obiekcie nie będzie potrzebna. Niektóre obiekty będą zapełniane danymi pochodzącymi
z bazy, a inne będą przeprowadzać obliczenia na danych, które otrzymały za pośrednic-
twem swoich konstruktorów.
W ramach ćwiczeń wybierz sobie w dużej aplikacji zmienną globalną i poszukaj jej.
W większości przypadków zmienne globalne są globalnie dostępne, ale rzadko są globalnie
używane. Korzysta się z nich w relatywnie niewielu miejscach. Wyobraź sobie, jak mogliby-
śmy przekazać taki obiekt do obiektów, które go potrzebują, gdyby nie mógł on być
zmienną globalną. W jaki sposób dokonalibyśmy refaktoryzacji tego programu? Czy ist-
nieją odpowiedzialności, które moglibyśmy wydzielić ze zbiorów klas, aby ograniczyć ich
globalny zasięg?
Jeśli znajdziesz globalną zmienną, która rzeczywiście jest używana we wszystkich
miejscach, oznacza to, że w Twoim kodzie nie ma żadnego podziału na warstwy. Zajrzyj
do rozdziałów 15., „Cała moja aplikacja to wywołania API”, i 17., „Moja aplikacja nie ma
struktury”.

Przypadek straszliwych zależności dyrektyw include


C++ był moim pierwszym językiem zorientowanym obiektowo i muszę przyznać, że czu-
łem się bardzo dumny, kiedy nauczyłem się wielu jego szczegółów i zawiłości. Zdomi-
nował on branżę, gdyż w swoim czasie stanowił niezwykle praktyczne rozwiązanie wielu
dokuczliwych problemów. Komputery są zbyt wolne? Proszę bardzo, oto język, w którym
wszystko jest opcjonalne. Możesz mieć całą wydajność czystego C, jeśli będziesz używać
tylko jego funkcji. Nie możesz namówić swoich ludzi do korzystania z języka zorientowa-
nego obiektowo? Proszę bardzo, oto kompilator C++; w kodzie C możesz dopisać fragment
w C++ i uczyć się zorientowania obiektowego w trakcie programowania.
Chociaż C++ był przez pewien czas bardzo popularny, w końcu ustąpił miejsca na
rzecz Javy oraz paru innych, nowszych języków. W pewnym stopniu przyczyną była ko-
nieczność zachowania wstecznej zgodności z C, ale o wiele większy wpływ wywarł wymóg
uproszczenia pracy z językami programowania. Zespoły pracujące w C++ regularnie
przekonywały się, że domyślna konfiguracja tego języka nieszczególnie sprawdza się pod-
czas konserwacji i że muszą poza nią wykraczać, aby system był elastyczny i podatny na
wprowadzanie zmian.
Jeden z aspektów C++, wywodzący się z C, który jest szczególnie kłopotliwy, to sposób,
w jaki jedna część programu dowiaduje się o innej części. W Javie i C#, gdy klasa w jednym
pliku musi skorzystać z klasy w drugim pliku, stosujemy import lub posługujemy się dy-
rektywą using, aby definicja klasy stała się dostępna. Kompilator szuka tej klasy i sprawdza,
czy była już ona kompilowana. Jeśli nie, kompiluje ją. Jeżeli klasa była już kompilowana,
142 ROZDZIAŁ 9. NIE MOGĘ UMIEŚCIĆ TEJ KLASY W JARZMIE TESTOWYM

kompilator odczytuje ze skompilowanego pliku niewielki fragment, pobierając tylko tyle


informacji, ile jest potrzebnych do zagwarantowania, że wszystkie metody wymagane
przez nową klasę znajdą się na miejscu.
Kompilatory C++ zazwyczaj nie stosują tego typu optymalizacji. Jeśli w C++ klasa
musi coś wiedzieć o innej klasie, deklaracja tej drugiej klasy (w innym pliku) jest dołączana
w formie tekstowej do pliku, który potrzebuje tych informacji. Taki proces może być
powolny. Kompilator musi powtórnie przeanalizować deklarację i zbudować jej wewnętrzną
reprezentację za każdym razem, kiedy ją napotyka. Co gorsza, mechanizm dołączania
jest podatny na nadużycia. Plik może dołączać plik, który dołącza kolejny plik itd.
W przypadku projektów, przy których programiści nie unikali takiego rozwiązania, nie-
trudno o znalezienie małych plików, które koniec końców dołączają tysiące wierszy kodu.
Programiści zastanawiają się, dlaczego kompilacja trwa tak długo, ale ponieważ instrukcje
dołączające są rozsiane po całym systemie, trudno jest wskazać konkretny plik i zrozu-
mieć, dlaczego zajmuje to tyle czasu.
Można odnieść wrażenie, że czepiam się C++, ale tak nie jest. To ważny język i stwo-
rzono w nim niewiarygodnie dużo kodu, ale poprawna z nim praca wymaga szczególnej
staranności.
Uzyskanie instancji klasy C++ w jarzmie testowym może być trudne w przypadku
cudzego kodu. Jeden z problemów, które prawie od razu napotykamy, to zależności na-
główkowe. Które pliki nagłówkowe są nam potrzebne, aby w jarzmie testowym utwo-
rzyć klasę?
Oto część deklaracji obszernej klasy C++ o nazwie Scheduler. Zawiera ona ponad 200
metod, ale tutaj pokazałem jakieś 5 z nich. Nie dość, że klasa ta jest ogromna, to jeszcze
charakteryzuje się silnymi i złożonymi zależnościami od wielu innych klas. Jak mogliby-
śmy poddać klasę Scheduler testom?
#ifndef SCHEDULER_H
#define SCHEDULER_H

#include "Meeting.h"
#include "MailDaemon.h"
...
#include "SchedulerDisplay.h"
#include "DayTime.h"

class Scheduler
{
public:
Scheduler(const string& owner);
~Scheduler();

void addEvent(Event *event);


bool hasEvents(Date date);
bool performConsistencyCheck(string& message);
...
};
#endif
PRZYPADEK STRASZLIWYCH ZALEŻNOŚCI DYREKTYW INCLUDE 143

Poza innymi elementami klasa Scheduler korzysta też z plików Meeting, MailDemon,
Event, SchedulerDisplay i DayTime. Gdy chcemy utworzyć testy dla obiektów klasy
Scheduler, najprostsze, co możemy zrobić, to próba utworzenia ich w tym samym katalogu,
w nowym pliku o nazwie SchedulerTests. Dlaczego chcemy mieć testy w tym samym
katalogu? Przy obecności preprocesora tak będzie łatwiej. Jeżeli w projekcie nie użyto ście-
żek, umożliwiających dołączanie plików w spójny sposób, czekałoby nas sporo pracy,
gdybyśmy chcieli umieścić nasze testy w innych katalogach.
#include "TestHarness.h"
#include "Scheduler.h"

TEST(create,Scheduler)
{
Scheduler scheduler("fred");
}

Jeśli utworzymy plik i po prostu wpiszemy taką deklarację obiektu do testu, natkniemy
się na problem z dołączaniem plików nagłówkowych. Aby skompilować klasę Scheduler,
musimy mieć pewność, że kompilator i konsolidator wiedzą wszystko na temat ele-
mentów, które są tej klasie potrzebne, a także wszystko na temat elementów potrzebnych
tym elementom itd. Na szczęście system przekazuje nam sporą liczbę komunikatów o błę-
dach i szczegółowo nas o nich informuje.
W prostszych przypadkach plik Scheduler.h zawiera wszystko, co jest nam potrzebne
do utworzenia klasy Scheduler, ale w niektórych przypadkach plik nagłówkowy nie obej-
muje wszystkiego. Aby utworzyć obiekt i z niego korzystać, będziemy musieli dołączyć
dodatkowe pliki.
Moglibyśmy po prostu skopiować wszystkie dyrektywy #include z pliku źródłowego
z klasą Scheduler, ale być może nie będziemy potrzebować każdego z tych plików. Najlep-
szą taktyką byłoby dodawanie ich po jednym i podjęcie decyzji, czy ta konkretna za-
leżność jest nam tak naprawdę niezbędna.
W idealnym świecie najprostsze byłoby dołączanie po kolei wszystkich potrzebnych
nam plików, aż przestałyby się pojawiać błędy kompilacji, lecz takie postępowanie mogłoby
doprowadzić do zamętu w naszym kodzie. Jeśli istnieje długi ciąg przechodnich zależności,
zapewne w rezultacie dołączylibyśmy o wiele więcej, niż naprawdę potrzebujemy. Nawet
jeśli ciąg zależności nie jest zbyt długi, mogłoby się okazać, że zależymy od elementów,
z którymi praca w jarzmie testowym jest bardzo trudna. W naszym przykładzie klasa
SchedulerDisplay jest jedną z takich zależności. Nie pokazuję tego tutaj, ale sięga do niej
konstruktor w klasie Scheduler. Tego rodzaju zależności możemy się pozbyć następująco:
#include "TestHarness.h"
#include "Scheduler.h"

void SchedulerDisplay::displayEntry(const string& entyDescription)


{
}

TEST(create,Scheduler)
144 ROZDZIAŁ 9. NIE MOGĘ UMIEŚCIĆ TEJ KLASY W JARZMIE TESTOWYM

{
Scheduler scheduler("fred");
}

Wprowadziliśmy w tym miejscu alternatywną definicję SchedulerDisplay::display


Entry. Niestety, kiedy tak zrobimy, będziemy potrzebować odrębnej kompilacji przy-
padków testowych zawartych w tym pliku. W programie możemy mieć tylko po jednej
definicji każdej metody w klasie SchedulerDisplay, w związku z czym potrzebny nam
będzie oddzielny program dla naszych testów tej klasy.
Na szczęście w pewnym stopniu będziemy mogli wielokrotnie korzystać z fałszy-
wek, które utworzyliśmy w ten sposób. Zamiast umieszczać definicje klas — takich jak
SchedulerDisplay — bezpośrednio w pliku testowym, możemy zamieścić je w oddziel-
nym pliku, z którego będzie można skorzystać w plikach z testami:
#include "TestHarness.h"
#include "Scheduler.h"
#include "Fakes.h"

TEST(create,Scheduler)
{
Scheduler scheduler("fred");
}

Po kilkakrotnym wykonaniu takiego zabiegu tworzenie instancji klasy C++ w jarzmie


testowym stanie się całkiem łatwe i machinalne. Istnieje jednak kilka dość poważnych
wad tego rozwiązania. Musimy utworzyć odrębny program i tak naprawdę nie usuwamy
żadnych zależności na poziomie języka, w związku z czym kod nie staje się przejrzystszy,
gdy się ich pozbywamy. Co gorsza, powielone definicje, które umieszczamy w pliku testo-
wym (w naszym przykładzie SchedulerDisplay::displayEntry), muszą być utrzymywane
tak długo, jak długo zachowujemy dany zestaw testów na swoim miejscu.
Technikę tę zachowuję dla przypadków, w których mam do czynienia z bardzo dużą
klasą, wykazującą poważne problemy z zależnościami. Nie jest to technika, z której można
korzystać często lub w prosty sposób. Jeśli dana klasa ma zostać rozbita wraz z upływem
czasu na dużą liczbę mniejszych klas, korzystne może okazać się utworzenie dla niej od-
rębnego programu testowego. Może on odgrywać rolę poligonu doświadczalnego, słu-
żącego do rozległej refaktoryzacji. Wraz z upływem czasu, gdy coraz więcej klas zostanie
poddanych testom, będzie można pozbyć się tego programu.

Przypadek cebulowego parametru


Lubię proste konstruktory. Naprawdę. To wspaniałe, kiedy decydujesz się na utworzenie
klasy, po czym po prostu wpisujesz wywołanie konstruktora i otrzymujesz sympatyczny,
żywy, działający i gotowy do użycia obiekt. W wielu przypadkach tworzenie obiektów
może być jednak trudne. Każdy obiekt powinien zostać skonfigurowany we właściwym
stanie — stanie, który przygotuje go do dodatkowych zadań. W wielu przypadkach ozna-
PRZYPADEK CEBULOWEGO PARAMETRU 145

cza to, że musimy mu udostępnić inne obiekty, które także muszą być poprawnie skonfi-
gurowane. Obiekty te podczas konfiguracji mogą wymagać jeszcze innych obiektów
i w rezultacie dochodzimy do tworzenia obiektów potrzebnych do utworzenia obiektów
potrzebnych do utworzenia obiektów potrzebnych do utworzenia parametru dla kon-
struktora klasy, którą chcemy poddać testom. Obiekty wewnątrz innych obiektów — wy-
gląda to jak jakaś wielka cebula. Oto przykład tego rodzaju problemu.
Mamy klasę wyświetlającą obiekt typu SchedulingTask:
public class SchedulingTaskPane extends SchedulerPane
{
public SchedulingTaskPane(SchedulingTask task) {
...
}
}

Aby ją utworzyć, musimy przekazać jej obiekt SchedulingTask, ale w celu jego utwo-
rzenia potrzebujemy skorzystać z jego jedynego konstruktora:
public class SchedulingTask extends SerialTask
{
public SchedulingTask(Scheduler scheduler, MeetingResolver resolver)
{
...
}
}

Jeśli odkryjemy, że do utworzenia obiektów Scheduler i MeetingResolver potrze-


bujemy kolejnych obiektów, prawdopodobnie zaczniemy rwać sobie włosy z głowy. Jedyne,
co nas powstrzymuje od pogrążenia się w skrajnej rozpaczy, to fakt, że musi istnieć przy-
najmniej jedna klasa, która nie potrzebuje jako argumentów obiektów innej klasy. W prze-
ciwnym razie system nigdy nie dałby się skompilować.
Sposób na poradzenie sobie z taką sytuacją polega na bliższym zastanowieniu się nad
tym, co chcemy osiągnąć. Musimy napisać testy, ale czego tak naprawdę potrzebujemy od
parametrów przekazywanych do konstruktora? Jeśli na potrzeby naszych testów nie po-
trzebujemy niczego, to możemy przekazać wartość pustą (126). Jeżeli potrzebujemy tylko
pewnego, elementarnego zachowania, możemy z najbliższej zależności wyodrębnić
interfejs (361) albo wyodrębnić implementer (356) i skorzystać z otrzymanego interfejsu
w celu utworzenia fałszywego obiektu. W naszym przypadku najbliższą zależnością klasy
SchedulingTaskPane jest SchedulingTask. Jeśli uda nam się utworzyć fałszywy obiekt klasy
SchedulingTask, będziemy w stanie utworzyć instancję klasy SchedulingTaskPane.
Niestety, klasa SchedulingTask dziedziczy po klasie SerialTask, a jedyne, co robi, to
przesłonięcie kilku metod chronionych; wszystkie metody publiczne znajdują się w klasie
SerialTask. Czy w odniesieniu do klasy SchedulingTask możemy skorzystać z wyodręb-
niania interfejsu (361)? A może powinniśmy zastosować tę technikę także do klasy
SerialTask? W Javie nie musimy tego robić. Możemy utworzyć interfejs dla klasy
SchedulingTask, który zawiera również metody klasy SerialTask.
146 ROZDZIAŁ 9. NIE MOGĘ UMIEŚCIĆ TEJ KLASY W JARZMIE TESTOWYM

Nasza wynikowa hierarchia wygląda jak na rysunku 9.3.

Rysunek 9.3. Interfejs SchedulingTask

W tym przypadku mamy szczęście, że korzystamy z Javy. Niestety, w C++ nie mamy
możliwości obsługiwania takich przypadków, gdyż w języku tym nie istnieją samodzielne
interfejsy. Są one zwykle implementowane w klasach zawierających jedynie funkcje czysto
wirtualne. Gdyby przykład ten został przełożony na C++, interfejs SchedulingTask stałby
się klasą abstrakcyjną, ponieważ dziedziczy funkcje wirtualne po klasie SchedulingTask.
Aby utworzyć instancję klasy SchedulingTask, musielibyśmy udostępnić w niej ciało me-
tody run(), które odsyłałoby do metody run() w klasie SerialTask. Na szczęście będzie
to łatwe do wykonania. Oto jak teraz wygląda kod:
class SerialTask
{
public:
virtual void run();
...
};

class ISchedulingTask
{
public:
virtual void run() = 0;
...
};

class SchedulingTask : public SerialTask, public ISchedulingTask


{
public:
virtual void run() { SerialTask::run(); }
};

W dowolnym języku, w którym możemy tworzyć interfejsy lub klasy działające jak in-
terfejsy, możemy z nich systematycznie korzystać w celu usuwania zależności.
PRZYPADEK ZALIASOWANEGO PARAMETRU 147

Przypadek zaliasowanego parametru


Często, kiedy do konstruktorów przekazywane są parametry, które wchodzą nam w drogę,
możemy ominąć ten problem, stosując wyodrębnianie interfejsu (361) lub wyodrębnianie
implementera (356). Czasami jednak takie rozwiązanie nie jest praktyczne. Spójrzmy na
inną klasę z systemu pozwoleń na budowę, z którym mieliśmy do czynienia we wcze-
śniejszym podrozdziale:
public class IndustrialFacility extends Facility
{
Permit basePermit;

public IndustrialFacility(int facilityCode, String owner,


OriginationPermit permit) throws PermitViolation {
Permit associatedPermit =
PermitRepository.GetInstance()
.findAssociatedFromOrigination(permit);

if (associatedPermit.isValid() && !permit.isValid()) {


basePermit = associatedPermit;
}
else if (!permit.isValid()) {
permit.validate();
basePermit = permit;
}
else
throw new PermitViolation(permit);
}
...
}

Chcielibyśmy utworzyć instancję tej klasy w jarzmie, ale na przeszkodzie stoi nam
kilka problemów. Jeden z nich polega na tym, że znowu mamy do czynienia z singletonem
— PermitRepository. Możemy ominąć ten problem, stosując techniki, które poznaliśmy
we wcześniejszym podrozdziale „Przypadek irytującej zależności globalnej”. Zanim jednak
rozwiążemy ten problem, napotykamy kolejny. Uzyskanie źródłowego pozwolenia, które
musimy przekazać do konstruktora, jest trudne. Obiekty klasy OriginationPermit cechują
się okropnymi zależnościami. Pierwsze, co przychodzi mi na myśl to: „Aha, żeby ominąć
tę zależność, zastosuję wobec klasy OriginationPermit wyodrębnianie interfejsu”, ale nie
jest to takie proste. Na rysunku 9.4 pokazano hierarchię obiektów klasy Permit.
Konstruktor IndustrialFacility przyjmuje obiekt klasy OriginationPermit i przecho-
dzi do obiektu PermitRepository, aby zdobyć odpowiednie pozwolenie; w PermitRepository
korzystamy z metody przyjmującej obiekt klasy OriginationPermit i zwracającej obiekt
typu Permit. Jeśli repozytorium odnajdzie odpowiednie pozwolenie, zapisze je w polu
Permit. Jeśli nie, zapisze w tym polu obiekt OriginationPermit. Moglibyśmy utworzyć in-
terfejs dla klasy OriginationPermit, ale w niczym by nam to nie pomogło. Musielibyśmy
148 ROZDZIAŁ 9. NIE MOGĘ UMIEŚCIĆ TEJ KLASY W JARZMIE TESTOWYM

Rysunek 9.4. Hierarchia obiektów klasy Permit

polu Permit przypisać interfejs IOriginationPermit, co by nie zadziałało — w Javie interfejsy


nie mogą dziedziczyć po klasach. Najbardziej oczywistym rozwiązaniem będzie utworzenie
interfejsów od samej góry aż po dół i zamiana pola Permit na IPermit. Rysunek 9.5 poka-
zuje, jak to będzie wyglądać.

Rysunek 9.5. Hierarchia obiektów klasy Permit z wyodrębnionymi interfejsami

A fe! To absurdalnie dużo pracy i w ogóle nie podoba mi się kod, jaki byśmy otrzymali.
Interfejsy świetnie nadają się do usuwania zależności, ale kiedy zbliżamy się do mo-
mentu, gdy między klasami a interfejsami mamy już niemal relację „jeden do jednego”,
projekt robi się zagracony. Nie zrozum mnie źle — gdy jesteśmy przyparci do muru,
dobrze będzie zmierzać w stronę takiego projektu, ale jeśli istnieją inne możliwości, po-
winniśmy je rozpatrzyć. Na szczęście je mamy.
Wyodrębnianie interfejsu (361) jest tylko jednym ze sposobów na usuwanie zależności
w odniesieniu do parametru. Czasami opłaca się zadać pytanie, dlaczego zależność jest
niedobra. Czasem tworzenie obiektu jest uciążliwe. Niekiedy parametr wywołuje niepożą-
dane efekty uboczne; może komunikuje się z systemem plików albo bazą danych. Innym
razem wykonywanie się kodu może zabierać zbyt dużo czasu. Kiedy stosujemy wyodręb-
nianie interfejsu (361), możemy poradzić sobie z tymi wszystkimi problemami, chociaż
robimy to, brutalnie odcinając jego połączenie z całą klasą. Jeśli problem kryje się tylko
we fragmentach klasy, możemy przyjąć inne podejście i przeciąć połączenie tylko z kłopo-
tliwymi fragmentami.
PRZYPADEK ZALIASOWANEGO PARAMETRU 149

Przyjrzyjmy się bliżej klasie OriginationPermit. Nie chcemy jej używać w teście, po-
nieważ komunikuje się ona po kryjomu z bazą danych, kiedy każemy jej przeprowadzić
autoryzację:
{
...
public void validate() {
// połącz się z bazą danych
...
// pobierz informację o autoryzacji
...
// ustaw flagę autoryzacji
...
// zamknij bazę danych
...
}
}

Nie chcemy tego robić w teście — musielibyśmy wprowadzić do bazy danych jakieś
fałszywe wpisy, co zdenerwowałoby jej administratora. Gdyby się o tym dowiedział, przy-
szłoby nam postawić mu obiad, ale i tak byłby poirytowany. I bez tego jego praca jest
wystarczająco trudna.
Inną strategią, którą moglibyśmy przyjąć, jest utworzenie podklasy i przesłonięcie
metody (398). Możemy utworzyć klasę o nazwie FakeOriginationPermit, która udostęp-
nia metodę ułatwiającą zmianę flagi uwierzytelnienia. Następnie w podklasach mogli-
byśmy przesłonić metodę validate i podczas testowania klasy IndustrialFacility prze-
stawić flagę uwierzytelniania w taki sposób, jaki tylko będzie nam potrzebny. Oto całkiem
dobry, pierwszy test:
public void testHasPermits() {
class AlwaysValidPermit extends FakeOriginationPermit
{
public void validate() {
// ustaw flagę uwierzytelnienia
becomeValid();
}
};

Facility facility = new IndustrialFacility(Facility.HT_1, "b",


new AlwaysValidPermit());
assertTrue(facility.hasPermits());
}

W wielu językach możemy tworzyć takie klasy „w locie” za pomocą metod. Chociaż
nie lubię tego często robić w kodzie produkcyjnym, sposób ten jest całkiem wygodny pod-
czas testowania. Bardzo prosto możemy tworzyć przypadki specjalne.
Tworzenie podklasy i przesłanianie metody (398) jest pomocne podczas usuwania
zależności dotyczących parametrów, ale czasami faktoryzacja metod w klasie nie jest
idealnym rozwiązaniem tego problemu. Mieliśmy szczęście, że zależności, które nam
przeszkadzały, były odizolowane w metodzie validate. W najgorszym przypadku są
150 ROZDZIAŁ 9. NIE MOGĘ UMIEŚCIĆ TEJ KLASY W JARZMIE TESTOWYM

one splecione z potrzebną nam logiką, a my najpierw musimy wyodrębnić metody. Może
to być proste, gdy mamy narzędzie refaktoryzujące. Jeśli nie dysponujemy takim narzę-
dziem, przydatne mogą okazać się niektóre z technik opisanych w rozdziale 22., „Muszę
zmienić monstrualną metodę, lecz nie mogę napisać do niej testów”.
Rozdział 10.

Nie mogę uruchomić


tej metody w jarzmie testowym

Porozmieszczanie testów w celu dokonania zmian może stwarzać pewne problemy. Jeżeli
potrafisz utworzyć instancję swojej klasy w jarzmie testowym, powinieneś uważać się za
szczęściarza. Wiele osób nie może tego o sobie powiedzieć. Jeśli masz z tym jakieś problemy,
zajrzyj do rozdziału 9., „Nie mogę umieścić tej klasy w jarzmie testowym”.
Utworzenie instancji klasy często jest zaledwie pierwszym etapem bitwy. Drugi etap to
napisanie testów dla metod, które musimy zmienić. Czasami możemy to zrobić w ogóle bez
tworzenia instancji klasy. Jeśli metoda nie używa zbyt wielu danych instancji, możemy
dostać się do kodu za pomocą techniki upubliczniania metody statycznej (347). Jeżeli
metoda jest dość długa i trudna w obsłudze, możemy wyłamać obiekt metody (332), aby
przesunąć kod do klasy, której instancja jest prostsza do utworzenia.
Na szczęście w większości przypadków zakres prac, które musimy wykonać w celu
napisania testów weryfikujących metody, nie jest drastycznie duży. Oto niektóre z proble-
mów, na które możemy natrafić:
 Metoda może być niedostępna dla testów; może być prywatna lub wykazywać inne
problemy z dostępnością.
 Wywołanie metody może być trudne, ponieważ skonstruowanie potrzebnych do
jej wywołania parametrów jest skomplikowane.
 Działanie metody może przynosić skutki uboczne (modyfikowanie bazy danych,
wystrzelenie pocisków manewrujących itd.), co uniemożliwia nam uruchomienie jej
w jarzmie testowym.
 Może być potrzebne przeprowadzenie rozpoznania w niektórych obiektach, z któ-
rych dana metoda korzysta.
Dalsza część tego rozdziału zawiera kilka scenariuszy pokazujących różne sposoby
ominięcia powyższych problemów oraz niektóre z kompromisów, jakie się z tym wiążą.
152 ROZDZIAŁ 10. NIE MOGĘ URUCHOMIĆ TEJ METODY W JARZMIE TESTOWYM

Przypadek ukrytej metody


Potrzebujemy wprowadzić zmianę w metodzie klasy, ale metoda ta jest prywatna. Co
powinniśmy zrobić?
Pierwsze pytanie, które powinniśmy zadać, dotyczy tego, czy możemy przetestować
metodę publiczną. Jeśli tak, warto będzie to zrobić. Oszczędzimy sobie problemów zwią-
zanych z próbami odnalezienia sposobu na dotarcie do metody prywatnej. Jest jeszcze
jedna korzyść. Kiedy testujemy metody publiczne, mamy gwarancję, że testujemy je w takiej
postaci, w jakiej zostały użyte w kodzie, co może nam pomóc w ograniczeniu ilości pracy,
jaka nas czeka. W cudzym kodzie bardzo często zdarzają się porozrzucane w klasach me-
tody o wątpliwej jakości. Zakres refaktoryzacji, jaką musielibyśmy przeprowadzić, aby
metoda prywatna była przydatna dla wszystkich klientów, mógłby być całkiem spory.
Chociaż posiadanie bardzo ogólnych metod, które są użyteczne dla wielu klientów,
jest miłe, to faktem pozostaje, że każda metoda musi być funkcjonalna tylko w takim
stopniu, żeby obsługiwała swoje klienty, i wystarczająco przejrzysta, aby można ją było
łatwo zrozumieć i modyfikować. Jeśli testujemy metodę prywatną za pośrednictwem
metod publicznych, które z niej korzystają, nie stwarzamy zagrożenia, że będzie ona zbyt
ogólna. Jeżeli metoda ta któregoś dnia będzie musiała stać się publiczna, pierwszy jej
użytkownik spoza klasy powinien napisać przypadki testowe i dokładnie wyjaśnić, co
metoda robi i w jaki sposób klient może z niej prawidłowo korzystać.
Wszystko to brzmi pięknie, ale w niektórych przypadkach chcemy po prostu napisać
test sprawdzający metodę prywatną, której wywołanie zagrzebane jest gdzieś głęboko
w klasie. Potrzebujemy konkretnej informacji zwrotnej oraz testów wyjaśniających, jak tej
metody używać. Możliwe jednak — któż to wie — że testowanie tej metody za pośred-
nictwem metod publicznych w klasie jest po prostu uciążliwe.
W jaki więc sposób napiszemy test dla metody prywatnej? Jest to z pewnością jedno
z najczęściej zadawanych pytań, które są związane z przeprowadzaniem testów. Na szczę-
ście istnieje na nie bezpośrednia odpowiedź: jeżeli musimy przetestować metodę prywatną,
powinniśmy ją upublicznić. Jeśli upublicznienie metody sprawia nam problem, w więk-
szości przypadków znaczy to, że nasza klasa robi za dużo i że powinniśmy to naprawić.
Przyjrzyjmy się konkretnym przypadkom. Dlaczego upublicznianie metody prywatnej
może być dla nas problematyczne? Oto niektóre z powodów:
1. Metoda jest tylko metodą pomocniczą; nie jest czymś, na czym by klientom
zależało.
2. Gdy klienty korzystają z metody, mogą negatywnie wpłynąć na wyniki pocho-
dzące z innych metod danej klasy.
Pierwszy powód nie jest szczególnie istotny. Dodatkowa metoda publiczna w interfejsie
klasy da się przeboleć, niemniej powinniśmy spróbować zastanowić się, czy nie byłoby
lepsze umieszczenie jej w innej klasie. Drugi powód ma nieco poważniejszą naturę,
chociaż szczęśliwie mamy na to środek zaradczy: metody prywatne można przesunąć do
PRZYPADEK UKRYTEJ METODY 153

nowej klasy, w której będą publiczne, a nasza klasa może utworzyć jej wewnętrzną instancję.
Dzięki temu metody staną się testowalne, a sam projekt będzie lepszy.
Tak, wiem, że moja rada brzmi bezceremonialnie, ale takie rozwiązanie ma kilka
bardzo korzystnych skutków. Faktem pozostaje, że dobry projekt jest testowalny, a projekt
nietestowalny jest zły. Odpowiedzią na to jest rozpoczęcie korzystania z technik opisanych
w rozdziale 20., „Ta klasa jest za duża, a ja nie chcę, żeby stała się jeszcze większa”. Kiedy
jednak nie mamy na miejscu zbyt wielu testów, być może przyjdzie nam postępować
z rozwagą i wykonać jeszcze trochę pracy, zanim będziemy mogli przystąpić do rozbijania
kodu na mniejsze części.
Popatrzmy, jak można poradzić sobie z takim problemem na przykładzie realnego
przypadku. Oto fragment deklaracji klasy w C++:
class CCAImage
{
private:
void setSnapRegion(int x, int y, int dx, int dy);
...
public:
void snap();
...
};

Klasa CCAImage jest używana do robienia zdjęć w systemie ochronnym. Być może
zastanawiasz się, dlaczego klasa z wizerunkiem (image) w nazwie robi zdjęcia, ale nie za-
pominaj, że to cudzy kod. W klasie tej znajduje się metoda snap(), która korzysta z ni-
skopoziomowego API w C, aby kontrolować aparat i „wykonać” zdjęcie, z tym że jest
to zdjęcie bardzo specjalne. Pojedyncze wywołanie metody snap() może spowodować
kilka różnych akcji aparatu, z których każda wiąże się z wykonaniem fotografii i umiesz-
czeniem jej w innej części bufora obrazu, przechowywanego w tej klasie. Logika od-
powiadająca za podjęcie decyzji, gdzie wstawić każde ze zdjęć, jest dynamiczna; zależy
od ruchu obiektu, który jest fotografowany. W zależności od sposobu przemieszczania się
obiektu metoda snap() może wielokrotnie powtarzać wywoływanie metody setSnapRegion,
aby zdecydować, w którym miejscu bufora umieścić bieżące zdjęcie. Niestety, API apa-
ratu uległo zmianie, w związku z czym musimy zmodyfikować metodę setSnapRegion.
Co powinniśmy zrobić?
Jeden z zabiegów, które możemy przeprowadzić, to upublicznienie tej metody. Nie-
stety, czynność ta może pociągnąć za sobą pewne bardzo negatywne konsekwencje. Klasa
CCAImage polega na kilku zmiennych, które decydują o bieżącej lokalizacji miejsca, które
zostanie sfotografowane. Jeśli ktoś zacznie wywoływać metodę setSnapRegion w kodzie
produkcyjnym spoza metody snap(), spowoduje poważne problemy w systemie śledzą-
cym aparatu.
I tu właśnie pojawia się kłopot. Zanim przypatrzymy się niektórym rozwiązaniom,
porozmawiajmy o tym, jak się w to wpakowaliśmy. Prawdziwa przyczyna, sprawiająca, że
nie możemy dobrze przetestować klasy CCAImage, jest taka, że klasa ta ma zbyt wiele
odpowiedzialności. Najlepiej byłoby rozbić ją na wiele mniejszych klas, korzystając z technik
154 ROZDZIAŁ 10. NIE MOGĘ URUCHOMIĆ TEJ METODY W JARZMIE TESTOWYM

opisanych w rozdziale 20., chociaż powinniśmy dokładnie rozważyć, czy chcemy właśnie
teraz przeprowadzać aż tak rozległą refaktoryzację. Dobrze byłoby się tym zająć, ale to,
czy będziemy mogli to zrobić, zależy od miejsca w cyklu produkcyjnym, w którym akurat
się znajdujemy, czasu, jaki nam jeszcze pozostał, oraz związanych z tym niebezpieczeństw.
Jeśli teraz nie możemy sobie pozwolić na rozdzielenie odpowiedzialności, to czy bę-
dziemy mogli napisać testy sprawdzające metody, które zmieniamy? Na szczęście tak. Oto
jak się za to zabierzemy.
Pierwszym krokiem jest zmiana rodzaju metody setSnapRegion z prywatnej na
chronioną.
class CCAImage
{
protected:
void setSnapRegion(int x, int y, int dx, int dy);
...
public:
void snap();
...
};

Następnie tworzymy podklasę klasy CCAImage, aby uzyskać dostęp do metody set
SnapRegion:
class TestingCCAImage : public CCAImage
{
public:
void setSnapRegion(int x, int y, int dx, int dy)
{
// wywołaj metodę setSnapRegion klasy nadrzędnej
CCAImage::setSnapRegion(x, y, dx, dy);
}
};

W większości nowoczesnych kompilatorów C++ możemy także skorzystać z deklaracji using,


umieszczonej w podklasie testującej, aby automatycznie oddelegować wywołania.
class TestingCCAImage : public CCAImage
{
public:
// Oznacz wszystkie implementacje metody setSnapRegion
// w klasie CCAImage jako część mojego interfejsu publicznego.
// Oddeleguj wszystkie wywołania do klasy CCAImage.
using CCAImage::setSnapRegion;
}

Gdy już to zrobimy, będziemy mogli wywołać w ramach testu metodę setSnapRegion
klasy CCAImage, chociaż nie bezpośrednio. Czy jest to jednak dobry pomysł? Wcześniej nie
chcieliśmy, żeby metoda ta była publiczna, a teraz robimy coś podobnego — zmieniamy ją
na chronioną, przez co staje się ona bardziej dostępna.
PRZYPADEK „POMOCNYCH” FUNKCJI JĘZYKA 155

Szczerze mówiąc, nie mam nic przeciwko temu. Dla mnie umieszczanie testów w ko-
dzie jest uczciwą transakcją. Fakt, zmiana ta umożliwia nam łamanie zasady hermetyzacji.
Kiedy zastanawiamy się nad działaniem kodu, musimy wziąć pod uwagę, że metoda
setSnapRegion może być teraz wywoływana w podklasach, jednak jest to względnie mało
ważne. Być może ten niewielki fragment wystarczy, aby skłonić nas do przeprowadzenia
pełnej refaktoryzacji, kiedy następnym razem sięgniemy po tę klasę. Będziemy mogli
wtedy porozdzielać odpowiedzialności z klasy CCAImage między inne klasy i spowodować,
że będą testowalne.

Osłabianie ochrony dostępu


W wielu zorientowanych obiektowo językach programowania, nowszych od C++, możemy
korzystać z refleksji oraz specjalnych zezwoleń w celu uzyskania dostępu do zmiennych prywat-
nych podczas działania programu. Chociaż funkcje te mogą być wygodne, to jednak jest to
pewnego rodzaju oszustwo — naprawdę. Możliwość taka jest bardzo przydatna, gdy chcemy
usuwać zależności, ale nie lubię pozostawiać w projektach testów, które mają dostęp do
zmiennych prywatnych. Taki wybieg naprawdę powstrzymuje zespoły programistów przed
dostrzeżeniem, jak bardzo zły staje się kod. Pewnie zabrzmi to trochę sadystycznie, ale ból,
który odczuwamy podczas pracy nad cudzym kodem, może być niesamowitym bodźcem do
wprowadzenia zmian. Moglibyśmy się od tego wykręcić, ale dopóki nie rozprawimy się
z przyczynami leżącymi u źródła — klasy z nadmiernymi odpowiedzialnościami oraz splątane
zależności — będziemy tylko odkładać konieczność zapłaty na później. Kiedy wreszcie wszyscy
odkryją, jak bardzo zły stał się kod, koszty jego poprawienia będą absurdalnie wysokie.

Przypadek „pomocnych” funkcji języka


Projektanci języków często starają się ułatwić nam życie, ale ich zadanie jest trudne. Muszą
znaleźć punkt równowagi między łatwością programowania a zasadami bezpieczeństwa.
Niektóre funkcjonalności początkowo wydają się pełnym sukcesem, łączącym w sobie
wszystkie te zagadnienia, ale kiedy przystępujemy do wypróbowania ich w kodzie, odkry-
wamy brutalną prawdę.
Oto fragment kodu w C#, który przyjmuje zbiór plików pobranych od klienta web. Kod
iteruje po wszystkich tych plikach i zwraca listę skojarzonych z nimi strumieni, które
wykazują się pewnymi cechami.
public void IList getKSRStreams(HttpFileCollection files) {
ArrayList list = new ArrayList();
foreach(string name in files) {
HttpPostedFile file = files[name];
if (file.FileName.EndsWith(".ksr") ||
(file.FileName.EndsWith(".txt")
&& file.ContentLength > MIN_LEN)) {
...
list.Add(file.InputStream);
}
156 ROZDZIAŁ 10. NIE MOGĘ URUCHOMIĆ TEJ METODY W JARZMIE TESTOWYM

}
return list;
}

Chcielibyśmy wprowadzić w tym fragmencie kodu pewne zmiany i być może nieco go
zrefaktoryzować, ale napisanie testów będzie trudne. Zamierzaliśmy utworzyć obiekt klasy
HttpFileCollection i zapełnić go obiektami klasy HttpPostedFile, ale jest to niemożliwe.
Po pierwsze, klasa HttpPostedFile nie ma publicznego konstruktora, a po drugie, klasa ta
jest zapieczętowana. W C# oznacza to, że nie możemy utworzyć jej instancji ani podklasy.
HttpPostedFile jest częścią biblioteki .NET. Podczas działania programu jej instancje
tworzy jakaś inna klasa, do której nie mamy dostępu. Szybki rzut oka na klasę HttpFile
Collection przekonuje nas, że i tutaj napotykamy te same problemy: brak publicz-
nych konstruktorów i brak sposobu na tworzenie klas pochodnych.
Dlaczego Bill Gates nam to robi? Przecież odnawialiśmy nasze licencje i w ogóle. Nie
wydaje mi się, żeby nas nienawidził. Jeśli jednak tak jest, to cóż… Może Scott McNealy też
nas nienawidzi, gdyż problem ten nie występuje wyłącznie w językach Microsoftu. Sun ma
analogiczną składnię, która uniemożliwia tworzenie podklas. Aby oznaczyć klasy szcze-
gólnie wrażliwe pod względem bezpieczeństwa, w Javie używane jest słowo kluczowe
final. Gdyby tylko ktoś potrafił utworzyć podklasę klasy HttpPostedFile lub nawet takiej
klasy jak String, mógłby napisać jakiś złośliwy kod i przekazać go w programie korzysta-
jącym z tych klas. Jest to jak najbardziej rzeczywiste niebezpieczeństwo, chociaż sealed
i final to dość drastyczne narzędzia; pozostawiają one nas ze skrępowanymi rękami.
Co moglibyśmy zrobić, aby napisać testy dla metody getKSRStreams? Nie możemy
wyodrębnić interfejsu (361) ani wyodrębnić implementera (356) — nie mamy kon-
troli nad klasami HttpPostedFile ani HttpFileCollection; są one klasami bibliotecznymi
i nie możemy ich zmieniać. Jedyną techniką, z której możemy skorzystać, jest adaptacja
parametru (328).
W tym przypadku mamy szczęście, ponieważ jedyną czynnością, jaką wykonujemy
w odniesieniu do zbioru plików, jest jego iterowanie. Szczęśliwie zapieczętowana klasa
HttpFileCollection, z której korzysta nasz kod, ma niezapieczętowaną klasę nadrzędną
o nazwie NameObjectCollectionBase. Możemy utworzyć jej podklasę i przekazać obiekt tej
podklasy do metody getKSRStreams. Zmiana ta będzie bezpieczna i prosta do przeprowa-
dzenia, jeśli skorzystamy ze wsparcia kompilatora (317).
public void LList getKSRStreams(OurHttpFileCollection files) {
ArrayList list = new ArrayList();
foreach(string name in files) {
HttpPostedFile file = files[name];
if (file.FileName.EndsWith(".ksr") ||
(file.FileName.EndsWith(".txt")
&& file.ContentLength > MAX_LEN)) {
...
list.Add(file.InputStream);
}
}
return list;
}
PRZYPADEK „POMOCNYCH” FUNKCJI JĘZYKA 157

OurHttpFileCollection jest podklasą klasy NameObjectCollectionBase, która z kolei


jest klasą abstrakcyjną zestawiającą łańcuchy tekstowe z obiektami.
Tak więc jeden problem mamy już za sobą. Następny będzie gorszy. Aby w teście uru-
chomić metodę getKSRStreams, potrzebujemy obiektów klasy HttpPostedFile, ale nie
możemy ich utworzyć. Czego będziemy w tym celu potrzebować? Wygląda na to, że będzie
nam potrzebna klasa udostępniająca dwie własności: FileName i ContentLength. Aby odse-
parować się od klasy HttpPostedFile, moglibyśmy skorzystać z techniki odzwierciedlenia
i opakowania API (215). W tym celu wyodrębniamy interfejs (IHttpPostedFile) i piszemy
obiekt opakowujący (HttpPostedFileWrapper):
public class HttpPostedFileWrapper : IHttpPostedFile
{
public HttpPostedFileWrapper(HttpPostedFile file) {
this.file = file;
}

public int ContentLength {


get { return file.ContentLength; }
}
...
}

Ponieważ mamy interfejs, możemy też utworzyć klasę do testowania:


public class FakeHttpPostedFile : IHttpPostedFile
{
public FakeHttpPostedFile(int length, Stream stream, ...) { ... }

public int ContentLength {


get { return length; }
}
}

Jeśli teraz wesprzemy się kompilatorem (317) i zmienimy nasz kod produkcyjny, bę-
dziemy mogli użyć obiektów klasy HttpPostedFileWrapper albo FakeHttpPostedFile
w interfejsie IHttpPostedFile, nie wiedząc, który z nich został wykorzystany.
public IList getKSRStreams(OurHttpFileCollection) {
ArrayList list = new ArrayList();
foreach(string name in files) {
IHttpPostedFile file = files[name];
if (file.FileName.EndsWith(".ksr") ||
(file.FileName.EndsWith(".txt"))
&& file.ContentLength > MAX_LEN)) {
...
list.Add(file.InputStream);
}
}
return list;
}
158 ROZDZIAŁ 10. NIE MOGĘ URUCHOMIĆ TEJ METODY W JARZMIE TESTOWYM

Jedyna niedogodność polega na tym, że musimy iterować oryginalną klasę HttpFile


Collection w kodzie produkcyjnym, opakowywać każdy obiekt klasy HttpPostedFile,
który tam się znajduje, a następnie dodawać go do nowego zbioru, który przekazujemy
metodzie gestKSRStreams. Taka jest cena bezpieczeństwa.
Tak na poważnie — łatwo uwierzyć, że sealed i final są nieprzemyślanymi pomyłkami,
które nigdy nie powinny znaleźć się w językach programowania. Prawdziwą winę ponosi-
my jednak my sami. Kiedy bezpośrednio uzależniamy się od bibliotek, które leżą poza
naszą kontrolą, dopraszamy się wręcz o kłopoty.
Któregoś dnia główne języki programowania być może udostępnią specjalne zgody na
czas testów, ale do tego czasu lepiej oszczędnie używać takich mechanizmów jak sealed
i final. Jeżeli potrzebujemy skorzystać z klas bibliotecznych, w których je zastosowano,
dobrym pomysłem będzie odizolowanie ich pod jakimś opakowaniem, co da nam trochę
przestrzeni manewrowej, gdy będziemy wprowadzać zmiany. Zajrzyj do rozdziałów 14.,
„Dobijają mnie zależności biblioteczne”, oraz 15., „Cała moja aplikacja to wywołania
API”, gdzie znajdziesz szczegółowe informacje oraz techniki pozwalające radzić sobie
z tym problemem.

Przypadek niewykrywalnych skutków ubocznych


Teoretycznie napisanie testu sprawdzającego pewną funkcjonalność nie powinno być zbyt
trudne. Tworzymy instancję klasy, wywołujemy jej metody i sprawdzamy wyniki ich
działania. Co mogłoby pójść nie tak? No cóż, wszystko byłoby proste, gdyby tworzony
przez nas obiekt nie komunikował się z żadnymi innymi obiektami. Jeśli inne obiekty
używają naszego obiektu, a nasz obiekt nie korzysta już z niczego więcej, to nasze testy
także mogłyby z niego korzystać i działać w taki sposób, jak reszta programu. Obiekty,
które nie korzystają z innych obiektów, należą jednak do rzadkości.
Programy się kompilują. Często mamy obiekty z metodami, które nie zwracają wartości.
Wywołujemy te metody, one wykonują jakąś pracę, ale my (czyli wywołujący je kod)
nigdy się o tym nie dowiemy. Obiekt wywołuje metody w innych obiektach, a my nawet
nie mamy wskazówki dotyczącej tego, jaki obrót przybrały sprawy.
Oto klasa z tego typu problemem:
public class AccountDetailFrame extends Frame
implements ActionListener, WindowListener
{
private TextField display = new TextField(10);
...
public AccountDetailFrame(...) { ... }

public void actionPerformed(ActionEvent event) {


String source = (String)event.getActionCommand();
if (source.equals("aktywność projektu")) {
detailDisplay = new DetailFrame();
PRZYPADEK NIEWYKRYWALNYCH SKUTKÓW UBOCZNYCH 159

detailDisplay.setDescription(
getDetailText() + " " + getProjectionText());
detailDisplay.show();
String accountDescription
= detailDisplay.getAccountSymbol();
accountDescription += ": ";
...
display.setText(accountDescription);
...
}
}
...
}

Ta stara klasa w Javie robi wszystko. Tworzy komponenty graficznego interfejsu


użytkownika, otrzymuje od nich powiadomienia za pośrednictwem procedury obsługi
actionPerformed, oblicza, co powinno zostać wyświetlone, a następnie to wyświetla.
Wszystko robi w dość osobliwy sposób: najpierw generuje szczegółowy tekst, po czym
tworzy i wyświetla kolejne okno. Kiedy okno to wykona już swoje zadanie, metoda pobiera
bezpośrednio z niego informacje, przetwarza je, a następnie umieszcza w jednym ze swo-
ich pól tekstowych.
Moglibyśmy spróbować uruchomić tę metodę w jarzmie testowym, ale nie miałoby
to sensu. Utworzyłaby ona okno, pokazała je nam, poprosiła o wpisanie danych, po czym
wyświetliłaby coś w kolejnym oknie. Nie ma tu dogodnego miejsca, aby rozpoznać, czym
ten kod się zajmuje.
Co możemy w tej sytuacji zrobić? Przede wszystkim możemy rozpocząć wydzielanie
zadań, które są niezależne od graficznego interfejsu użytkownika. Ponieważ pracujemy
w Javie, możemy skorzystać z przewagi, jaką daje nam jedno z dostępnych narzędzi do
refaktoryzacji. Naszym pierwszym krokiem będzie przeprowadzenie serii wyodrębniania
metod (411), aby rozdzielić zadania wykonywane w tej metodzie.
Gdzie powinniśmy zacząć?
Metoda ta odgrywa przede wszystkim rolę haczyka na ogłoszenia pochodzące z fra-
meworka wyświetlającego okna. Pierwszą czynnością, którą wykonuje, jest pobranie
nazwy polecenia ze zdarzenia akcji, które zostało do niej przekazane. Jeśli wyodrębnimy
całe ciało tej metody, będziemy mogli odseparować się od wszelkich zależności od klasy
ActionEvent.
public class AccountDetailFrame extends Frame
implements ActionListener, WindowListener
{
private TextField display = new TextField(10);
...
public AccountDetailFrame(...) { ... }

public void actionPerformed(ActionEvent event) {


String source = (String)event.getActionCommand();
performCommand(source);
}
160 ROZDZIAŁ 10. NIE MOGĘ URUCHOMIĆ TEJ METODY W JARZMIE TESTOWYM

public void performCommand(String source) {


if (source.equals(“aktywność projektu“)) {
detailDisplay = new DetailFrame();
detailDisplay.setDescription(
getDetailText() + " " + getProjectionText());
detailDisplay.show();
String accountDescription
= detailDisplay.getAccountSymbol();
accountDescription += ": ";
...
display.setText(accountDescription);
...
}
}
...
}

To jednak nie wystarczy, aby kod stał się testowalny. Następnym krokiem jest wyod-
rębnienie metod dla kodu, który zapewnia dostęp do kolejnej ramki. Dzięki temu ramka
detailDisplay stanie się zmienną instancji klasy.
public class AccountDetailFrame extends Frame
implements ActionListener, WindowListener
{
private TextField display = new TextField(10);
private DetailFrame detailDisplay;
...
public AccountDetailFrame(...) { .. }

public void actionPerformed(ActionEvent event) {


String source = (String)event.getActionCommand();
performCommand(source);
}

public void performCommand(String source) {


if (source.equals("aktywność projektu")) {
detailDisplay = new DetailFrame();
detailDisplay.setDescription(
getDetailText() + " " + getProjectionText());
detailDisplay.show();
String accountDescription
= detailDisplay.getAccountSymbol();
accountDescription += ": ";
...
display.setText(accountDescription);
...
}
}
...
}

Teraz możemy wyodrębnić kod, który korzysta z tej ramki, i przekształcić go w zbiór
metod. Jak powinniśmy nazwać te metody? Aby wpaść na pomysły nazw, powinniśmy
przyjrzeć się, co robi każdy fragment kodu z perspektywy danej klasy albo jakie obliczenia
PRZYPADEK NIEWYKRYWALNYCH SKUTKÓW UBOCZNYCH 161

przeprowadza on w tej klasie. Ponadto nie powinniśmy stosować nazw, które mają coś
wspólnego z wyświetlaniem. W kodzie, który wyodrębniamy, możemy użyć składników
realizujących wyświetlanie, ale nasze nazwy powinny ten fakt zatajać. Mając to na uwadze,
dla każdego fragmentu kodu możemy utworzyć metodę, która jest albo komendą, albo
zapytaniem.

Oddzielenie komendy od zapytania


Oddzielenie komendy od zapytania (ang. command/query separation) jest regułą projektową
opisaną po raz pierwszy przez Bertranda Meyera. Prostymi słowami można ją wytłumaczyć
następująco: metoda powinna być albo komendą, albo zapytaniem, ale nigdy i jednym, i drugim.
Komenda to metoda, która potrafi zmienić stan obiektu, ale nie zwraca wartości, natomiast
zapytanie to metoda, która zwraca wartość, ale nie potrafi zmodyfikować obiektu.
Dlaczego reguła ta jest ważna? Istnieje ku temu wiele powodów, ale najważniejszym jest komu-
nikacja. Jeżeli metoda jest zapytaniem, nie powinniśmy być zmuszeni zaglądać do jej ciała, aby
dowiedzieć się, czy możemy z niej skorzystać wiele razy z rzędu bez wywołania jakichś skutków
ubocznych.

Oto jak wygląda metoda performCommand po przeprowadzeniu serii wyodrębnień:


public class AccountDetailFrame extends Frame
implements ActionListener, WindowListener
{
public void performCommand(String source) {
if (source.equals("aktywność projektu")) {
setDescription(getDetailText() + " " + getProjectionText());
...
String accountDescription = getAccountSymbol();
accountDescription += ": ";
...
display.setText(accountDescription);
...
}
}

void setDescription(String description) {


detailDisplay = new DetailFrame();
detailDisplay.setDescription(description);
detailDisplay.show();
}

String getAccountSymbol() {
return detailDisplay.getAccountSymbol();
}
...
}

Teraz, kiedy wyodrębniliśmy już cały kod, który zajmuje się ramką detailDisplay,
możemy wyodrębnić kod realizujący dostęp do składników klasy AccountDetailFrame.
162 ROZDZIAŁ 10. NIE MOGĘ URUCHOMIĆ TEJ METODY W JARZMIE TESTOWYM

public class AccountDetailFrame extends Frame


implements ActionListener, WindowListener {
public void performCommand(String source) {
if (source.equals("aktywność projektu")) {
setDescription(getDetailText() + " " + getProjectionText());
...
String accountDescription
= detailDisplay.getAccountSymbol();
accountDescription += ": ";
...
setDisplayText(accountDescription);
...
}
}

void setDescription(String description) {


detailDisplay = new DetailFrame();
detailDisplay.setDescription(description);
detailDisplay.show();
}

String getAccountSymbol() {
return detailDisplay.getAccountSymbol();
}

void setDisplayText(String description) {


display.setText(description);
}
...
}

Po przeprowadzeniu powyższego wyodrębniania możemy utworzyć podklasę


i przesłonić metodę (398), a także przetestować kod, który pozostał jeszcze w metodzie
performCommand. Jeśli na przykład utworzymy w następujący sposób podklasę klasy
AccountDetailFrame, będziemy mogli sprawdzić, czy przy poleceniu aktywność projektu
wyświetlacz otrzymuje poprawny tekst.
public class TestingAccountDetailFrame extends AccountDetailFrame
{
String displayText = "";
String accountSymbol = "";

void setDescription(String description) {


}

String getAccountSymbol() {
return accountSymbol;
}

void setDisplayText(String text) {


displayText = text;
}
}
PRZYPADEK NIEWYKRYWALNYCH SKUTKÓW UBOCZNYCH 163

Oto test, który angażuje metodę performCommand:


public void testPerformCommand() {
TestingAccountDetailFrame frame = new TestingAccountDetailFrame();
frame.accountSymbol = "SYM";
frame.performCommand("aktywność projektu");
assertEquals("SYM: konto podstawowe", frame.displayText);
}
Jeśli zależności będziemy separować w taki sposób — bardzo konserwatywnie, wyko-
rzystując zautomatyzowaną refaktoryzację techniką wyodrębniania metody, możemy
uzyskać kod, na którego widok ciarki przejdą nam po plecach. Na przykład metoda
setDescription, która tworzy ramkę, wygląda na wskroś paskudnie. Co się stanie, jeśli
wywołamy ją dwa razy z rzędu? Musimy jakoś sobie poradzić z tym problemem, niemniej
wykonanie takich topornych wyodrębnień to całkiem dobry, pierwszy krok. Później bę-
dziemy mogli sprawdzić, czy uda nam się przenieść tworzenie ramki w lepsze miejsce.
Gdzie znajdujemy się obecnie? Rozpoczęliśmy od klasy zawierającej klasę z jedną ważną
metodą — performAction, a zakończyliśmy, mając klasę pokazaną na rysunku 10.1.

Rysunek 10.1. Klasa AccountDetailFrame

Nie widać tego na diagramie UML, ale metody getAccountSymbol i setDescription


korzystają tylko z pola detailDisplay i z niczego więcej. Metoda setDisplayText używa
wyłącznie obiektu klasy TextField o nazwie display. Moglibyśmy założyć, że są to odrębne
odpowiedzialności. Jeśli tak zrobimy, będziemy mogli ostatecznie uzyskać efekt pokazany
na rysunku 10.2.

Rysunek 10.2. Klasa AccountDetailFrame po topornej refaktoryzacji


164 ROZDZIAŁ 10. NIE MOGĘ URUCHOMIĆ TEJ METODY W JARZMIE TESTOWYM

Jest to wyjątkowo toporny sposób na przeprowadzenie refaktoryzacji oryginalnego


kodu, ale przynajmniej separuje on w pewnym stopniu odpowiedzialności. Klasa Account
DetailFrame jest powiązana z graficznym interfejsem użytkownika (jest podklasą klasy
Frame) i nadal zawiera logikę biznesową. Stosując dodatkową refaktoryzację, będziemy
mogli ominąć i ten problem, a już teraz możemy zrealizować przypadek testowy dla me-
tody, która zawierała logikę biznesową. To całkiem dobry krok do przodu.
Klasa SymbolSource to konkretna klasa, która reprezentuje decyzję dotyczącą utworze-
nia kolejnego obiektu klasy Frame i pobrania od niego informacji. Nadaliśmy jej jednak
nazwę SymbolSource, ponieważ z punktu widzenia klasy AccountDetailFrame jej zadanie
polega na pobieraniu informacji o symbolu w sposób, jaki tylko będzie potrzebny. Nie
byłbym zaskoczony, gdyby SymbolSource stał się interfejsem, jeśli decyzja ta kiedykol-
wiek ulegnie zmianie.
Kroki, które podjęliśmy w tym przykładzie, są dość popularne. Kiedy mamy narzędzie
do refaktoryzacji, łatwo możemy wyodrębnić metody z klasy, a następnie rozpocząć
identyfikowanie grup metod, które można przesunąć do nowych klas. Dobre narzędzie
refaktoryzujące pozwoli na przeprowadzenie zautomatyzowanej refaktoryzacji techniką
wyodrębniania metody tylko wtedy, gdy jest to bezpieczne. Z tego też powodu najbardziej
ryzykowną częścią naszej pracy staje się edycja kodu, której dokonujemy między kolejnymi
użyciami tego narzędzia. Pamiętaj, że nie ma nic złego w wyodrębnianiu metod z kiep-
skimi nazwami lub o słabej strukturze w celu rozmieszczenia testów. Bezpieczeństwo
jest najważniejsze. Kiedy testy znajdą się już na swoich miejscach, będziesz mógł sprawić,
że kod stanie się o wiele przejrzystszy.
Rozdział 11.

Muszę dokonać zmian.


Które metody powinienem
przetestować?

Potrzebujemy wprowadzić kilka zmian, a także napisać testy charakteryzujące (196),


aby poznać zachowanie, które jest już zaimplementowane. Gdzie powinniśmy umieścić te
testy? Najprostsza odpowiedź jest taka, że powinniśmy je napisać dla każdej metody, którą
zmieniamy. Ale czy to wystarczy? Być może tak, jeśli kod jest prosty i łatwy do zrozumienia,
ale w przypadku cudzego kodu wszystko może się wydarzyć. Zmiana jednego fragmentu
może wpłynąć na zachowanie programu gdzieś indziej. Dopóki nie umieścimy testu
w odpowiednim miejscu, możemy się nigdy o tym nie dowiedzieć.
Kiedy muszę wprowadzić zmiany w szczególnie zagmatwanym kodzie, często poświę-
cam czas, próbując zrozumieć, gdzie powinienem rozmieścić testy. Wiąże się to z rozmy-
ślaniem o zmianie, którą zamierzam przeprowadzić; o skutkach, jakie ona przyniesie;
o tym, na co wpłyną zmieniane elementy itd. Tego typu rozważania nie są niczym nowym
— ludzie robili to od zarania ery komputerowej.
Programiści siedzą i dumają o swoich programach z wielu różnych powodów. Zabawne
jest to, że dużo o tym nie mówimy. Zakładamy, że każdy wie, jak myśleć, i że robienie tego
jest „po prostu częścią bycia programistą”. Niestety, w niczym nam to nie pomaga,
gdy stajemy w obliczu przeraźliwie poplątanego kodu, który zdecydowanie przekracza
nasze możliwości jasnego o nim myślenia. Wiemy, że powinniśmy przeprowadzić jego
refaktoryzację, aby stał się bardziej zrozumiały, tylko że wtedy znowu wychodzi na
wierzch kwestia testów. Jeżeli nie dysponujemy testami, skąd będziemy wiedzieć, że
poprawnie refaktorujemy?
O technikach zamieszczonych w tym rozdziale pisałem w celu wypełnienia luki.
Aby znaleźć najlepsze miejsca na testy, często musimy myśleć o skutkach zmian w niesza-
blonowy sposób.
166 ROZDZIAŁ 11. MUSZĘ DOKONAĆ ZMIAN. KTÓRE METODY POWINIENEM PRZETESTOWAĆ?

Myślenie o skutkach
Nie rozmawiamy o tym zbyt często w branży, ale każda funkcjonalna zmiana w oprogra-
mowaniu pociąga za sobą cały szereg skutków. Jeśli na przykład w poniższym kodzie w C#
zmienię 3 na 4, zmieni się wynik działania metody, gdy zostanie ona wywołana. Mogą
również ulec zmianie wyniki metod ją wywołujących itd., aż do samych granic systemu.
Mimo to wiele elementów kodu nie zmieni swojego zachowania. Ich użycie nie przyniesie
innych wyników, ponieważ nie wywołują bezpośrednio ani pośrednio metody getBalance
Point().
int getBalancePoint() {
const int SCALE_FACTOR = 3;
int result = startingLoad + (LOAD_FACTOR * residual * SCALE_FACTOR);
foreach(Load load in loads) {
result += load.getPointWeight() * SCALE_FACTOR;
}
return result;
}

Wsparcie zintegrowanego środowiska programistycznego w analizie skutków


Czasami chciałbym mieć środowisko programistyczne, które pomagałoby mi w dostrzeganiu
skutków zmian w cudzym kodzie. Mógłbym zaznaczyć fragment kodu i nacisnąć klawisz,
po czym interfejs środowiska wyświetliłby listę wszystkich zmiennych i metod, na które
będzie mieć wpływ modyfikacja zaznaczonego kodu.
Być może któregoś dnia ktoś opracuje tego typu narzędzie, ale do tego czasu musimy ana-
lizować skutki zmian bez tego rodzaju pomocy. Umiejętności tej łatwo można się nauczyć,
chociaż stwierdzenie, czy wszystko zrobiliśmy dobrze, jest trudne.

Najlepszy sposób zorientowania się, na czym polega przewidywanie skutków, to zapo-


znanie się z przykładem. Oto klasa Javy, która jest częścią aplikacji przetwarzającej kod
w C++. Wygląda na to, że problem ten jest ściśle związany z określoną dziedziną pro-
gramowania, prawda? Wiedza z danej dziedziny nie ma jednak znaczenia, gdy zasta-
nawiamy się nad skutkami działania kodu.
Wykonajmy małe ćwiczenie. Zrób listę wszystkich elementów, które mogą ulec zmia-
nie po utworzeniu obiektu klasy CppClass, a które wpłyną na wyniki zwracane przez jej
poszczególne metody.
public class CppClass {
private String name;
private List declarations;

public CppClass(String name, List declarations) {


this.name = name;
this.declarations = declarations;
}
MYŚLENIE O SKUTKACH 167

public int getDeclarationCount() {


return declarations.size();
}

public String getName() {


return name;
}

public Declaration getDeclaration(int index) {


return ((Declaration)declarations.get(index));
}

public String getInterface(String interfaceName, int [] indices) {


String result = "class " + interfaceName + " {\npublic:\n";
for (int n = 0; n < indices.length; n++) {
Declaration virtualFunction
= (Declaration)(declarations.get(indices[n]));
result += "\t" + virtualFunction.asAbstract() + "\n";
}
result += "};\n";
return result;
}
}

Twoja lista powinna wyglądać mniej więcej tak:


1. Ktoś mógłby dodać kolejne elementy do listy declarations po przekazaniu jej do
konstruktora. Ponieważ lista ta jest przechowywana przez referencję, zmiany w niej
wprowadzone mogą wpłynąć na wyniki metod getInterface, getDeclaration
i getDeclarationCount.
2. Ktoś może zmienić jeden z obiektów przechowywanych na liście declarations lub
zastąpić jeden z jej elementów, wywierając wpływ na te same metody.

Niektórzy, widząc metodę getName, podejrzewają, że mogłaby ona zwrócić inną wartość,
gdyby ktoś zmodyfikował zmienną łańcuchową name, ale w Javie obiekty typu String nie
podlegają zmianom. Nie można modyfikować ich wartości, gdy zostaną już utworzone. Po
utworzeniu obiektu klasy CppClass metoda getName zawsze będzie zwracać tę samą wartość.

Sporządziliśmy też schemat (patrz rysunek 11.1) pokazujący, że zmiany na liście


declarations mają wpływ na metodę getDeclarationCount().

Rysunek 11.1. Lista declarations wpływa na metodę getDeclarationCount


168 ROZDZIAŁ 11. MUSZĘ DOKONAĆ ZMIAN. KTÓRE METODY POWINIENEM PRZETESTOWAĆ?

Schemat pokazuje, że jeśli lista declarations ulegnie jakiejś zmianie — na przykład


wzrośnie jej rozmiar — metoda getDeclarationCount() może zwrócić inną wartość.
Możemy też wykonać schemat metody getDeclaration(int index) — patrz rysu-
nek 11.2.

Rysunek 11.2. Lista declarations i przechowywane w niej obiekty wpływają na metodę


getDeclarationCount

Wartości zwracane przez wywołania tej metody mogą się zmienić, jeśli coś spowoduje
zmianę listy declarations lub ulegną zmianie deklaracje w jej obrębie.
Rysunek 11.3 pokazuje, że podobne zdarzenia mogą mieć wpływ również na metodę
getInterface.

Rysunek 11.3. Elementy wpływające na metodę getInterface

Możemy połączyć wszystkie te szkice w jeden duży schemat (patrz rysunek 11.4).

Rysunek 11.4. Połączony schemat skutków


MYŚLENIE O SKUTKACH 169

Na wykresach tych nie ma zbyt wiele składni. Nazywam je po prostu schematami


skutków. Najważniejsze jest, aby mieć odrębny dymek na każdą zmienną, która może ulec
zmianie, oraz na każdą metodę, której zwracane wartości mogą się zmienić. Czasami
zmienne znajdują się w tym samym obiekcie, a czasami w różnych obiektach. Nie ma
to znaczenia. Rysujemy dymek dla każdego elementu, który się zmieni, oraz strzałkę
do każdego obiektu, którego wartość może pod wpływem tego elementu ulec zmianie
w czasie działania programu.

Jeśli Twój kod jest dobrze ustrukturyzowany, większość metod w Twoim programie będzie mieć
proste struktury efektów. W rzeczy samej, jedną z miar jakości oprogramowania jest to, że sto-
sunkowo złożone efekty widoczne na zewnątrz są sumą znacznie prostszych efektów w kodzie.
Prawie wszystko, co możesz zrobić w celu uproszczenia schematu skutków, ilustrującego pe-
wien fragment kodu, sprawi, że kod ten stanie się przystępniejszy i łatwiejszy w konserwacji.

Poszerzmy nasze spojrzenie na system, z którego pochodzi poprzednia klasa, i przyj-


rzyjmy się obrazowi skutków obejmującemu większy fragment kodu. Obiekty klasy
CppClass są tworzone w klasie o nazwie ClassReader. W istocie udało nam się stwierdzić,
że są one tworzone wyłącznie w tej klasie.
public class ClassReader {
private boolean inPublicSection = false;
private CppClass parsedClass;
private List declarations = new ArrayList();
private Reader reader;

public ClassReader(Reader reader) {


this.reader = reader;
}
public void parse() throws Exception {
TokenReader source = new TokenReader(reader);
Token classToken = source.readToken();
Token className = source.readToken();

Token lbrace = source.readToken();


matchBody(source);
Token rbrace = source.readToken();

Token semicolon = source.readToken();

if (classToken.getType() == Token.CLASS
&& className.getType() == Token.IDENT
&& lbrace.getType() == Token.LBRACE
&& rbrace.getType() == Token.RBRACE
&& semicolon.getType() == Token.SEMIC) {
parsedClass = new CppClass(className.getText(),
declarations);
}
}
...
}
170 ROZDZIAŁ 11. MUSZĘ DOKONAĆ ZMIAN. KTÓRE METODY POWINIENEM PRZETESTOWAĆ?

Pamiętasz, czego dowiedzieliśmy się o klasie CppClass? Czy mamy pewność, że lista
deklaracji nie ulegnie zmianie po jej utworzeniu? Informacje, jakie mamy o klasie CppClass,
tak naprawdę nie pozwalają nam tego stwierdzić. Powinniśmy dowiedzieć się, jak zapeł-
niana jest lista deklaracji. Jeżeli lepiej przyjrzymy się klasie CppClass, zauważymy, że dekla-
racje są dodawane tylko w jednej jej części — metodzie o nazwie matchVirtualDeclaration,
która jest wywoływana przez metodę matchBody.
private void matchVirtualDeclaration(TokenReader source)
throws IOException {
if (!source.peekToken().getType() == Token.VIRTUAL)
return;
List declarationTokens = new ArrayList();
declarationTokens.add(source.readToken());
while(source.peekToken().getType() != Token.SEMIC) {
declarationTokens.add(source.readToken());
}
declarationTokens.add(source.readToken());
if (inPublicSection)
declarations.add(new Declaration(declarationTokens));
}

Wygląda na to, że wszystkie zmiany, które zachodzą na liście, mają miejsce przed
utworzeniem obiektu klasy CppClass. Ponieważ dodajemy do listy nowe deklaracje i nie
przechowujemy żadnych referencji do nich, deklaracje te także nie ulegną zmianie.
Zastanówmy się nad elementami przechowywanymi na liście declarations. Metoda
readToken klasy TokenReader zwraca tokeny, zawierające tylko łańcuch tekstowy oraz
liczbę całkowitą, która nigdy nie ulegnie zmianie. Nie pokazałem tego w tym miejscu, ale
szybki rzut oka na klasę Declarations ujawnia, że nic nie może zmienić jej stanu po jej
utworzeniu. Tak więc z pewnością możemy stwierdzić, że po utworzeniu obiektu klasy
CppClass jego lista deklaracji oraz zawartość listy się nie zmienią.
W jaki sposób wiedza ta może nam pomóc? Gdybyśmy zaczęli dostawać od obiektów
klasy CppClass nieoczekiwane wartości, wiedzielibyśmy, że musimy sprawdzić tylko
kilka rzeczy. Aby dowiedzieć się, co się dzieje, z reguły możemy zajrzeć do miejsc, w któ-
rych tworzone są podobiekty klasy CppClass. Moglibyśmy również sprawić, że kod stanie
się bardziej zrozumiały, oznaczając za pomocą słowa kluczowego Javy final niektóre
z referencji w tej klasie jako stałe.
W przypadku programów, które nie są zbyt dobrze napisane, często trudno jest nam
się zorientować, dlaczego wyniki, które widzimy, są takie, a nie inne. Gdy znajdujemy się
w takiej sytuacji, mamy problem z debugowaniem i musimy rozumować wstecz — od
wystąpienia błędu do jego źródła. Kiedy jednak pracujemy z cudzym kodem, często mu-
simy zadać sobie pytanie innej natury: jeśli wprowadzimy określoną zmianę, to w jaki spo-
sób może ona wpłynąć na pozostałe wyniki działania programu?
Odpowiedź na to pytanie wiąże się z prześledzeniem działania programu w przód, po-
cząwszy od punktów zmian. Kiedy już oswoisz się z takim sposobem myślenia, uzyskasz
podstawy techniki służącej do szukania dobrych miejsc na umieszczanie testów.
ŚLEDZENIE W PRZÓD 171

Śledzenie w przód
W poprzednim przykładzie próbowaliśmy wyznaczyć zbiór obiektów, które wpływają na
wartości znajdujące się w określonym punkcie kodu. Kiedy piszemy testy charakteryzujące
(196), odwracamy ten proces. Patrzymy na zbiór obiektów i staramy się dowiedzieć, co
zmieni się na wcześniejszych etapach, gdy testy przestaną działać. Oto przykład. Poniższa
klasa jest częścią systemu plikowego, rezydującego w pamięci komputera. Nie dysponuje-
my dla niego żadnymi testami, ale chcielibyśmy wprowadzić w nim kilka zmian.
public class InMemoryDirectory {
private List elements = new ArrayList();

public void addElement(Element newElement) {


elements.add(newElement);
}

public void generateIndex() {


Element index = new Element("index");
for (Iterator it = elements.iterator(); it.hasNext(); ) {
Element current = (Element)it.next();
index.addText(current.getName() + "\n");
}
addElement(index);
}

public int getElementCount() {


return elements.size();
}

public Element getElement(String name) {


for (Iterator it = elements.iterator(); it.hasNext(); ) {
Element current = (Element)it.next();
if (current.getName().equals(name)) {
return current;
}
}
return null;
}
}

InMemoryDirectory jest niewielką klasą Javy. Możemy utworzyć obiekt tej klasy, dodać
do niego elementy, wygenerować indeks, a następnie uzyskać dostęp do tych elementów.
Obiekty klasy Element zawierają tekst, tak jak pliki. Kiedy generujemy indeks, tworzymy
element o nazwie index i dołączamy w jego treści nazwy wszystkich pozostałych elementów.
Jedna z osobliwych cech klasy InMemoryDirectory polega na tym, że nie możemy wy-
wołać metody generateIndex dwukrotnie bez namieszania w katalogu. Jeśli metoda ta
zostanie uruchomiona dwa razy, dostaniemy dwa indeksy (drugi indeks będzie zawierać
pierwszy indeks jako element katalogu).
Na szczęście nasza aplikacja korzysta z klasy InMemoryDirectory w bardzo uporządko-
wany sposób. Tworzy katalogi, wypełnia je elementami, wywołuje metodę generateIndex,
172 ROZDZIAŁ 11. MUSZĘ DOKONAĆ ZMIAN. KTÓRE METODY POWINIENEM PRZETESTOWAĆ?

po czym przekazuje katalog dalej, dzięki czemu pozostałe części aplikacji mogą uzyskać
dostęp do jej elementów. Obecnie wszystko działa bardzo dobrze, niemniej musimy
wprowadzić zmiany. Potrzebujemy zmodyfikować program w taki sposób, aby użytkow-
nicy mogli dodawać do katalogu elementy na dowolnym etapie jego cyklu działania.
W idealnej sytuacji chcielibyśmy, żeby tworzenie indeksu oraz jego konserwacja zacho-
dziły jako skutek uboczny dodawania elementów. Gdy ktoś doda element po raz pierwszy,
powinien zostać utworzony element indeksu zawierający nazwę dodawanego elementu.
Za drugim razem ten sam element indeksu powinien zostać zaktualizowany nazwą drugiego
elementu. Napisanie testów weryfikujących nowe zachowanie oraz kodu, który je realizuje,
będzie całkiem łatwe, nie mamy jednak testów sprawdzających obecne zachowanie. Skąd
będziemy wiedzieć, gdzie testy te umieścić?
W tym przykładzie odpowiedź będzie wystarczająco jasna: potrzebujemy serii testów,
które na różne sposoby wywołują metodę addElement, generują indeks, po czym pobierają
rozmaite elementy, żeby sprawdzić, czy są one poprawne. Skąd będziemy wiedzieć, że
wybraliśmy właściwy sposób na przeprowadzenie testów? W tym przypadku nasz problem
jest prosty. Testy stanowią tylko odwzorowanie naszych oczekiwań względem korzystania
z katalogu. Zapewne moglibyśmy je napisać, nie patrząc nawet na kod tworzący katalog,
ponieważ mamy dość dobre wyobrażenie na temat funkcji, które katalog powinien re-
alizować. Niestety, określenie miejsc, które należy poddać testom, nie zawsze jest tak samo
łatwe. Na potrzeby przykładu mógłbym skorzystać z dużej i skomplikowanej klasy —
takiej, jakie często czają się w cudzym kodzie — ale zapewne szybko byś się znudził i za-
mknął tę książkę. Udajmy zatem, że nasz problem jest trudny, i zastanówmy się, jak patrząc
na kod, możemy dowiedzieć się, co przetestować. Taki sam tok myślenia ma zastosowanie
także do bardziej złożonych problemów.
W naszym przykładzie pierwsze, co musimy zrobić, to ustalić, gdzie wprowadzimy
zmiany. Powinniśmy usunąć funkcjonalność z metody generateIndex i dodać funkcjonal-
ność do metody addElement. Kiedy już zidentyfikujemy je jako punkty zmian, będziemy
mogli wziąć się do rysowania schematu skutków.
Zacznijmy od metody generateIndex. Co ją wywołuje? Żadne inne metody klasy
InMemoryDirectory. Jest ona wywoływana wyłącznie przez klienty. Czy coś w niej
modyfikujemy? Tworzymy nowy element i dodajemy go do katalogu, tak więc metoda
generateIndex może wywierać wpływ na kolekcję elements w tej klasie (patrz rysunek 11.5).

Rysunek 11.5. Metoda generateIndex wpływa na listę elements

Teraz możemy przyjrzeć się kolekcji elements i sprawdzić, na co może ona mieć wpływ.
Gdzie jeszcze jest używana? Wygląda na to, że korzystają z niej metody getElementCount
i getElement. Kolekcja elements jest używana także przez metodę addElement, ale ten
przypadek możemy pominąć, gdyż metoda ta zawsze zachowuje się tak samo, bez względu
na to, co dzieje się z listą; obojętne, co zrobimy z kolekcją elements, nie wpłynie to na
użytkowników metody addElement (patrz rysunek 11.6).
ŚLEDZENIE W PRZÓD 173

Rysunek 11.6. Kolejne skutki zmian w metodzie generateIndex

Czy już skończyliśmy? Nie, nasze punkty zmian to metody generateIndex i addElement,
tak więc musimy jeszcze spojrzeć, jaki wpływ na program wywiera metoda addElement.
Wygląda na to, że metoda ta wpływa na kolekcję elements (patrz rysunek 11.7).

Rysunek 11.7. Metoda addElement wpływa na listę elements

Moglibyśmy sprawdzić, na które elementy ona wpływa, ale już to zrobiliśmy, gdyż
metoda generateIndex wywiera wpływ na kolekcję elements.
Cały schemat pokazano na rysunku 11.8.

Rysunek 11.8. Schemat skutków dla klasy InMemoryDirectory


174 ROZDZIAŁ 11. MUSZĘ DOKONAĆ ZMIAN. KTÓRE METODY POWINIENEM PRZETESTOWAĆ?

Jedynym sposobem, w jaki użytkownicy klasy InMemoryDirectory mogą rozpoznać


skutki jej działania, polega na skorzystaniu z metod getElementCount i getElement. Jeśli
będziemy w stanie napisać testy dla tych metod, wydaje się, że damy radę sprawdzić
wszystkie skutki wprowadzonej przez nas zmiany.
Czy jest jednak prawdopodobne, że coś pominęliśmy? A co z klasami nadrzędnymi
i podklasami? Jeśli jakiekolwiek dane w klasie InMemoryDirectory są publiczne, chronione
albo mają zakres pakietu, to metoda z podklasy mogłaby ją zmodyfikować w taki sposób,
o którym byśmy nie wiedzieli. W tym przypadku zmienne instancji w klasie InMemory
Directory są prywatne, tak więc nie musimy się tym przejmować.

Gdy rysujesz schemat skutków, upewnij się, że odnalazłeś wszystkie klienty klasy, którą spraw-
dzasz. Jeśli Twoja klasa ma klasy nadrzędne albo podklasy, mogą istnieć inne klienty, których
nie wziąłeś pod uwagę.

Czy to już wszystko? Cóż, jest jeszcze jedna rzecz, którą całkowicie przeoczyliśmy.
W katalogu używamy klasy Element, ale nie jest ona częścią naszego schematu skutków.
Przyjrzyjmy się jej z bliska.
Kiedy wywołujemy metodę generateIndex, tworzymy obiekt klasy Element i wielokrot-
nie wywołujemy w nim metodę addText. Spójrzmy na kod klasy Element:
public class Element {
private String name;
private String text = "";

public Element(String name) {


this.name = name;
}

public String getName() {


return name;
}

public void addText(String newText) {


text += newText;
}

public String getText() {


return text;
}
}

Na szczęście jest ona bardzo prosta. Narysujmy dymek dla nowego elementu, który
tworzy metoda generateIndex (patrz rysunek 11.9).
Kiedy mamy już nowy element i jest on wypełniony tekstem, metoda generateIndex
dodaje do niego kolekcję, przez co ten nowy element ma na nią wpływ (patrz rysunek
11.10).
ŚLEDZENIE W PRZÓD 175

Rysunek 11.9. Skutki wywierane przez klasę Element

Rysunek 11.10. Metoda generateIndex wpływająca na kolekcję elements

Na podstawie wykonanej przez nas wcześniej pracy wiemy, że metoda addText wywiera
wpływ na kolekcję elements, co z kolei wpływa na wartości zwracane przez metody
getElement i getElementCount. Jeśli chcemy sprawdzić, czy tekst jest generowany prawi-
dłowo, możemy wywołać metodę getText dla elementu zwróconego przez metodę
getElement. Są to jedyne miejsca, dla których musimy napisać testy, aby wykryć skutki
wprowadzonych przez nas zmian.
Jak już wspomniałem, jest to raczej mały przykład, ale bardzo reprezentatywny dla
tego rodzaju analizy, jaką musimy przeprowadzić, kiedy chcemy ocenić wpływ zmian
wprowadzonych w cudzym kodzie. Musimy znaleźć miejsca na testy, a pierwszy krok
polega na określeniu, gdzie nasza zmiana może zostać wykryta — jakie będą jej skutki.
Kiedy już wiemy, gdzie możemy wykryć skutki, w czasie pisania testów będziemy mogli
dokonać wyboru spośród tych właśnie miejsc.
176 ROZDZIAŁ 11. MUSZĘ DOKONAĆ ZMIAN. KTÓRE METODY POWINIENEM PRZETESTOWAĆ?

Propagacja skutków
Niektóre ze sposobów, na które przejawiają się skutki zmian, są łatwiejsze do zauważenia
niż inne. W przykładzie z metodą InMemoryDirectory, z poprzedniego podrozdziału,
znaleźliśmy ostatecznie metody, które zwracały wartości do obiektów je wywołujących.
Nawet gdy zaczynam śledzenie skutków, począwszy od punktów zmian — miejsc, w któ-
rych dokonałem modyfikacji — zazwyczaj zauważam najpierw metody z wartościami
zwrotnymi. O ile tylko zwracane wartości nie zostaną użyte, wywierają one wpływ na kod,
który je wywołał.
Skutki mogą rozprzestrzeniać się też cicho i podstępnie. Jeśli mamy obiekt, który
przyjmuje jako parametr jakiś inny obiekt, może zmodyfikować jego stan, a zmiana ta
zostanie odzwierciedlona w pozostałych częściach aplikacji.

Każdy język ma reguły określające, jak obsługiwane są parametry w metodach. W wielu


przypadkach domyślnym sposobem jest przekazywanie referencji do obiektów przez war-
tość. Tak jest w Javie i C#. Obiekty nie są przekazywane do metod. Zamiast tego przekazy-
wane są „uchwyty” do nich. W rezultacie dowolna metoda może zmienić stan obiektu za
pośrednictwem uchwytu, który został jej przekazany. W niektórych językach istnieją słowa
kluczowe, z których można skorzystać, aby uniemożliwić modyfikację stanu obiektu prze-
kazanego do metody. W C++ odpowiada za to słowo kluczowe const, gdy użyjesz go w deklaracji
parametru metody.

Najbardziej podstępny sposób, w jaki fragment kodu może wpłynąć na inny kod,
polega na użyciu danych globalnych albo statycznych. Oto przykład:
public class Element {
private String name;
private String text = "";

public Element(String name) {


this.name = name;
}

public String getName() {


return name;
}

public void addText(String newText) {


text += newText;
View.getCurrentDisplay().addText(newText);
}

public String getText() {


return text;
}
}
NARZĘDZIA DO WYSZUKIWANIA SKUTKÓW 177

Klasa ta jest niemal identyczna jak klasa Element, z którą mieliśmy do czynienia w przy-
kładzie z klasą InMemoryDirectory. W rzeczywistości inny jest tylko jeden wiersz kodu —
druga linia metody addText. Samo spojrzenie na sygnatury metod w klasie Element nie
pozwoli nam poznać wpływu, jaki wywierają te elementy na widoki. Ukrywanie in-
formacji to całkiem dobry pomysł, chyba że jest to informacja, którą chcemy poznać.

Skutki rozprzestrzeniają się w kodzie na trzy podstawowe sposoby:


1. Wartości zwrotne, które są wykorzystywane przez element wywołujący.
2. Modyfikowanie obiektów przekazywanych jako parametry, które zostaną później użyte.
3. Modyfikowanie danych statycznych lub globalnych, które zostaną później użyte.
Niektóre języki udostępniają dodatkowe mechanizmy. Na przykład w językach zorientowanych
aspektowo programiści mogą tworzyć konstrukcje zwane aspektami, mające wpływ na zacho-
wanie kodu w innych obszarach systemu.

Oto heurystyka, z której korzystam, kiedy szukam skutków:


1. Zidentyfikuj metodę, która się zmieni.
2. Jeśli metoda ma wartości zwrotne, przyjrzyj się obiektom, które ją wywołują.
3. Sprawdź, czy metoda modyfikuje jakieś wartości. Jeśli tak, spójrz na metody korzy-
stające z tych wartości oraz na metody korzystające z tych metod.
4. Nie zapomnij poszukać klas nadrzędnych i podklas, które mogą być użytkownikami
tych zmiennych instancji, a także metod.
5. Przypatrz się parametrom tych metod. Sprawdź, czy metody te lub zwracane
przez nie obiekty są używane przez kod, który chcesz zmienić.
6. Przyjrzyj się zmiennym globalnym i danym statycznym, które są modyfikowane
przez zidentyfikowane przez Ciebie metody.

Narzędzia do wyszukiwania skutków


Najważniejszym narzędziem, którym dysponujemy w naszym arsenale, jest znajomość
języka programowania. W każdym z języków istnieją pewne „firewalle” — funkcjonalności,
które zapobiegają propagacji skutków. Jeśli wiemy, czym one są, mamy pewność, że nie bę-
dziemy musieli ich poszukiwać.
Załóżmy, że właśnie mamy wprowadzić zmiany w poniższej klasie Coordinate. Chcemy
przybliżyć się do możliwości korzystania z wektora w celu przechowywania wartości x
i y, ponieważ mamy zamiar uogólnić tę klasę w taki sposób, żeby mogła reprezentować
trój- oraz czterowymiarowe współrzędne. W następującym kodzie, napisanym w Javie, nie
musimy zaglądać poza klasę Coordinate, aby zrozumieć skutki, jakie wywoła wprowadzona
przez nas zmiana:
178 ROZDZIAŁ 11. MUSZĘ DOKONAĆ ZMIAN. KTÓRE METODY POWINIENEM PRZETESTOWAĆ?

public class Coordinate {


private double x = 0;
private double y = 0;

public Coordinate() {}
public Coordinate(double x, double y) {
this.x = x; this.y = x;
}
public double distance(Coordinate other) {
return Math.sqrt(
Math.pow(other.x - x, 2.0) + Math.pow(other.y - y, 2.0));
}
}
A oto kod, w którego przypadku powinniśmy zajrzeć poza klasę:
public class Coordinate {
double x = 0;
double y = 0;

public Coordinate() {}
public Coordinate(double x, double y) {
this.x = x; this.y = x;
}
public double distance(Coordinate other) {
return Math.sqrt(
Math.pow(other.x - x, 2.0) + Math.pow(other.y - y, 2.0));
}
}

Czy widzisz różnicę? Jest subtelna. W pierwszej wersji klasy zmienne x i y były prywatne.
W drugiej mają zakres pakietu. Jeśli w pierwszej wersji zrobimy coś, co zmieni wartości
zmiennych x i y, wpłynie to na klienty tylko za pośrednictwem funkcji distance, bez
względu na to, czy klienty korzystają z klasy Coordinate, czy też jej podklasy. W wersji dru-
giej klienty w pakiecie mogą bezpośrednio sięgać do zmiennych. Powinniśmy rozejrzeć się
za czymś takim w kodzie albo spróbować przekształcić je w zmienne prywatne, aby tak się
nie działo. Podklasy klasy Coordinate także mogą korzystać ze zmiennych instancji,
w związku z czym musimy ich poszukać i sprawdzić, czy zmienne te zostały użyte w me-
todach którejkolwiek z podklas.
Znajomość używanego przez nas języka jest ważna, ponieważ pewne subtelne reguły
często mogą sprowadzić nas na manowce. Spójrzmy na przykład w języku C++:
class PolarCoordinate : public Coordinate {
public:
PolarCoordinate();
double getRho() const;
double getTheta() const;
};

Kiedy w języku C++ po deklaracji metody następuje słowo kluczowe const, metoda ta
nie może modyfikować zmiennych instancji obiektu. A może jednak? Przypuśćmy, że klasa
nadrzędna klasy PolarCoordinate wygląda następująco:
WYCIĄGANIE WNIOSKÓW Z ANALIZY SKUTKÓW 179

class Coordinate {
protected:
mutable double first, second;
};

Gdy w języku C++ w deklaracji zmiennych użyto słowa kluczowego mutable, oznacza
to, że zmienne te mogą być modyfikowane w metodach oznaczonych jako const. Co
prawda takie użycie słowa kluczowego mutable jest szczególnie dziwaczne, ale gdy przy-
chodzi do sprawdzenia, co może, a co nie może się zmieniać w programie, którego nie
znamy, powinniśmy szukać skutków, nie zwracając uwagi na to, jak bardzo mogą one
wydawać się nam dziwne. Przyjęcie, że w języku C++ const rzeczywiście oznacza const,
bez sprawdzenia, że tak jest istotnie, może być niebezpieczne. To samo dotyczy innych
konstrukcji języka, które można obejść.

Poznaj swój język.

Wyciąganie wniosków z analizy skutków


Postaraj się analizować skutki działania kodu przy każdej okazji. Czasami zapewne stwier-
dzisz, że gdy coraz lepiej poznajesz bazę kodu, pojawia się u Ciebie uczucie, że nie musisz
już szukać niektórych elementów. Kiedy tak się dzieje, oznacza to, że odnalazłeś w bazie
kodu pewną „podstawową poprawność”. W przypadku najlepszego kodu niezbyt często
nadchodzi chwila na zawołanie „mam cię!”. Niektóre „reguły” zapisane w bazie kodu —
obojętnie, czy zostały wyrażone otwarcie, czy też nie — powstrzymają Cię od popadania
w paranoję, kiedy szukasz możliwych do wystąpienia skutków działania kodu. Najlepszą
metodą na znalezienie tych reguł jest zastanowienie się, jak pewien fragment programu
może wpłynąć na inny fragment w sposób, którego nigdy wcześniej nie widziałeś
w bazie kodu. Po namyśle stwierdzisz: „Nie, przecież to byłoby głupie”. Kiedy w Twojej
bazie kodu znajdziesz wiele fragmentów spełniających tę regułę, o wiele lepiej będziesz
mógł sobie z nimi poradzić. W przypadku złego kodu nigdy nie wiadomo, czym są
„reguły”, albo w „regułach” tych aż roi się od wyjątków.
„Reguły” w bazie kodu niekoniecznie są wzniosłymi stwierdzeniami dotyczącymi stylu
programowania, takimi jak „nigdy nie używaj zmiennych chronionych”. Często wynikają
one z kontekstu. W przykładzie z klasą CppClass, na początku tego rozdziału, wykonaliśmy
niewielkie ćwiczenie, podczas którego próbowaliśmy określić, co wpłynie na użytkowników
obiektu klasy CppClass po jego utworzeniu. Oto fragment kodu tej klasy:
public class CppClass {
private String name;
private List declarations;

public CppClass(String name, List declarations) {


this.name = name;
this.declarations = declarations;
180 ROZDZIAŁ 11. MUSZĘ DOKONAĆ ZMIAN. KTÓRE METODY POWINIENEM PRZETESTOWAĆ?

}
...
}

Zwróciliśmy uwagę na to, że ktoś mógłby zmodyfikować listę deklaracji po przekazaniu


jej do konstruktora. Jest to idealny kandydat do reguły „nie, przecież to byłoby głupie”.
Gdybyśmy na początku analizy klasy CppClass wiedzieli, że mamy do czynienia z listą,
która nie ulegnie zmianie, nasze wnioskowanie byłoby prostsze.
Zazwyczaj programowanie staje się prostsze, gdy ograniczamy skutki występujące
w programie. Aby zrozumieć fragment kodu, musimy wiedzieć mniej. W skrajnym przy-
padku dochodzimy do programowania funkcjonalnego w takich językach jak Scheme albo
Haskell. Programy w nich pisane mogą być bardzo łatwe do zrozumienia, chociaż języki te
nie są powszechnie używane. Niemniej w językach zorientowanych obiektowo ogranicza-
nie skutków może w znacznym stopniu ułatwić testowanie, przy czym nie ma żadnych
przeszkód, aby tak postępować.

Upraszczanie schematów skutków


Książka ta traktuje o ułatwianiu sobie pracy z cudzym kodem, w związku z czym w wielu
przykładach mamy do czynienia w pewnym sensie z „rozlanym mlekiem”. Chciałbym
jednak skorzystać z okazji i w związku ze schematami skutków pokazać Ci coś bardzo
przydatnego, co może mieć wpływ na sposób, w jaki będziesz pisał swój kod w miarę
postępowania naprzód.
Czy pamiętasz pierwszy schemat skutków narysowany dla klasy CppClass (patrz
rysunek 11.11)?

Rysunek 11.11. Schemat skutków dla klasy CppClass

Wygląda na to, że mamy tu do czynienia ze skutkami rozbiegającymi się w wielu kie-


runkach. Dwa elementy danych — deklaracja oraz kolekcja declarations — wywierają
wpływ na kilka różnych metod. Na potrzeby naszych testów możemy sobie wybrać jedną
z nich. Najlepsza będzie getInterface, ponieważ trochę mocniej angażuje deklaracje.
UPRASZCZANIE SCHEMATÓW SKUTKÓW 181

Niektóre z zachowań możemy rozpoznać poprzez właśnie tę metodę, co nie będzie łatwe
w przypadku metod getDeclaration i getDeclarationCount. Nie miałbym nic przeciwko
napisaniu testów wyłącznie dla metody getInterface, gdybym musiał scharakteryzować
klasę CppClass, ale szkoda by było, gdyby metody getDeclaration i getDeclarationCount
nie zostały uwzględnione. Co by się jednak stało, gdyby metoda getInterfece wyglądała
następująco:
public String getInterface(String interfaceName, int [] indices) {
String result = "class " + interfaceName + " {\npublic:\n";
for (int n = 0; n < indices.length; n++) {
Declaration virtualFunction = getDeclaration(indices[n]);
result += "\t" + virtualFunction.asAbstract() + "\n";
}
result += "};\n";
return result;
}

Różnica jest subtelna — kod używa obecnie metody getDeclaration wewnętrznie.


Nasz schemat przekształca się zatem z wersji pokazanej na rysunku 11.12 na wersję
z rysunku 11.13.

Rysunek 11.12. Schemat skutków dla klasy CppClass

Rysunek 11.13. Schemat skutków dla klasy CppClass


182 ROZDZIAŁ 11. MUSZĘ DOKONAĆ ZMIAN. KTÓRE METODY POWINIENEM PRZETESTOWAĆ?

Zmiana jest niewielka, ale ma całkiem spore znaczenie. Metoda getInterface ko-
rzysta teraz z metody getDeclaration wewnętrznie. W rezultacie używamy metody
getDeclaration za każdym razem, gdy poddajemy testom metodę getInterface.
Kiedy usuwamy malutkie fragmenty powielonych elementów, zwykle otrzymujemy
schematy skutków z mniejszą liczbą punktów końcowych, co często przekłada się na ła-
twiejsze do podjęcia decyzje dotyczące testów.

Skutki i hermetyzacja
Jedną z często wymienianych zalet zorientowania obiektowego jest hermetyzacja. Często się
zdarza, że gdy pokazuję ludziom techniki usuwania zależności opisywane w tej książce, zwracają
oni uwagę, że naruszają one zasadę hermetyzacji. To prawda. Wiele z tych technik ma to
do siebie.
Hermetyzacja jest ważna, ale powód, dla którego jest ważna, jest jeszcze ważniejszy. Hermety-
zacja pomaga nam w myśleniu o naszym kodzie. W dobrze hermetyzowanym kodzie masz
mniej ścieżek do prześledzenia, kiedy starasz się go zrozumieć. Jeśli na przykład dodamy
nowy parametr do konstruktora w celu usunięcia zależności — jak to się robi podczas re-
faktoryzacji techniką parametryzacji konstruktora (377) — będziemy mieć o jedną ścieżkę
więcej do prześledzenia, gdy przystąpimy do analizowania skutków. Naruszenie hermetyzacji
może utrudnić rozmyślanie o naszym kodzie, ale może je także ułatwić, jeśli w rezultacie
otrzymamy testy dobrze wyjaśniające działanie programu. Gdy dysponujemy przypadkami te-
stowymi dla klasy, możemy z nich skorzystać w celu bardziej bezpośredniego przeanalizowania
naszego kodu. Możemy również napisać nowe testy odpowiadające na pytania, które mogliby-
śmy mieć w odniesieniu do zachowania się kodu.
Hermetyzacja i pokrycie testami nie zawsze stoją ze sobą w sprzeczności, ale gdy już tak jest,
skłaniam się raczej ku testom. Testy często pomagają mi uzyskać lepszą hermetyzację na
późniejszym etapie.
Hermetyzacja nie jest celem samym w sobie — jest narzędziem służącym do zrozumienia.

Kiedy musimy stwierdzić, gdzie umieścić testy, ważne jest, aby wiedzieć, na co mogą
mieć wpływ wprowadzane przez nas zmiany. Musimy zastanowić się nad skutkami. Mo-
żemy to robić nieformalnie albo też w sposób bardziej sformalizowany, za pomocą nie-
wielkich schematów; warto jednak ćwiczyć. W przypadku szczególnie zagmatwanego
kodu jest to jedna z umiejętności, na której możemy polegać, kiedy rozmieszczamy
w nim testy.
Rozdział 12.

Muszę dokonać wielu zmian


w jednym miejscu.
Czy powinienem
pousuwać zależności
we wszystkich klasach,
których te zmiany dotyczą?

Czasami łatwo jest rozpocząć pisanie testów sprawdzających klasę, jednak w przypadku
cudzego kodu często nie jest to łatwe. Zależności mogą być trudne do usunięcia. Jeżeli
w celu uproszczenia sobie pracy postanowiłeś umieścić klasy w jarzmie testowym,
jedną z najbardziej irytujących rzeczy, które możesz napotkać, jest kolejna zmiana w nie-
dalekiej perspektywie. Chcesz dodać do systemu nową funkcjonalność i okazuje się, że
musisz zmodyfikować trzy albo cztery blisko spokrewnione klasy. Poddanie każdej z nich
testom zabierze kilka godzin. Oczywiście wiesz, że koniec końców kod będzie lepszy,
ale czy naprawdę musisz pojedynczo usuwać wszystkie te zależności? Być może nie.
Czasami opłaca się przetestować „o jeden poziom niżej”, aby znaleźć lepsze miejsce
do przetestowania jednocześnie kilku zmian. Możemy napisać testy dla jednej metody
publicznej, sprawdzające zmiany wprowadzone w wielu metodach prywatnych, albo
utworzyć testy dla interfejsu jednego obiektu, weryfikujące współpracę kilku obiektów
w nim przechowywanych. Kiedy tak zrobimy, nie tylko będziemy mogli przetestować
wprowadzane przez nas zmiany, ale też przygotować sobie pole pod przeprowadzenie
refaktoryzacji w tym miejscu. Struktura kodu pokrytego testami może podlegać rady-
kalnym zmianom, pod warunkiem że testy dokładnie określają jego zachowanie.
184 ROZDZIAŁ 12. MUSZĘ DOKONAĆ WIELU ZMIAN W JEDNYM MIEJSCU

Testy wysokopoziomowe mogą być pomocne podczas refaktoryzacji. Często programiści wolą
je od szczegółowego testowania każdej klasy, ponieważ uważają, że wprowadzenie zmiany
jest trudniejsze, kiedy trzeba napisać wiele krótkich testów weryfikujących zmieniany in-
terfejs. W rzeczywistości dokonanie zmian może być łatwiejsze, niż się spodziewasz, gdyż
najpierw możesz wprowadzić modyfikacje w testach, a dopiero potem w kodzie, zmieniając je-
go strukturę w niewielkich i bezpiecznych przyrostach.
Chociaż testy wysokopoziomowe są ważnym narzędziem, nie należy ich stosować w za-
stępstwie testów jednostkowych; powinny stanowić pierwszy krok w kierunku ich roz-
mieszczenia w kodzie.

W jaki sposób możemy rozmieścić takie „testy pokrywające”? Najpierw musimy


ustalić, gdzie je umieścić. Jeśli jeszcze tego nie robiłeś, zajrzyj do rozdziału 11., „Muszę do-
konać zmian. Które metody powinienem przetestować?”. Rozdział ten opisuje schematy
skutków (167), będące przydatnym narzędziem, z którego można korzystać w celu
określenia, gdzie napisać testy. Z kolei w tym rozdziale omawiam pojęcie punktów prze-
chwycenia i sposoby ich odnajdowania. Opisałem także najlepszy rodzaj punktu prze-
chwycenia, na jaki możesz natrafić w kodzie — punkt zwężenia. Pokazałem, jak ich
szukać i w jaki sposób mogą Ci one pomóc, gdy chcesz napisać testy mające pokryć kod,
który masz zamiar zmienić.

Punkty przechwycenia
Punkt przechwycenia to po prostu miejsce w programie, w którym można wykryć skutki
określonej zmiany. W niektórych aplikacjach ich znalezienie może być trudniejsze niż
w przypadku innych programów. Jeśli masz aplikację, której poszczególne części są połą-
czone ze sobą z małą liczbą naturalnych spoin, znalezienie dobrego punktu przechwyce-
nia może być trudne. Często konieczne będzie przeprowadzenie analizy skutków oraz
usunięcie sporych zależności. Od czego zaczniemy?
Najlepiej na początek zidentyfikować miejsca, w których musisz wprowadzić zmiany,
i rozpocząć śledzenie skutków w kierunku prowadzącym na zewnątrz od tych punktów.
Każde miejsce, w którym możesz wykryć skutki, to punkt przechwycenia, chociaż
niekoniecznie najlepszy. W czasie tego procesu będziesz musiał kierować się subiektywną
opinią.

Prosty przypadek
Wyobraźmy sobie, że musimy zmodyfikować klasę Javy o nazwie Invoice, która oblicza
kwoty na fakturach, aby zmienić sposób wyliczania kosztów. Metoda kalkulująca koszty
w klasie Invoice ma nazwę getValue.
PUNKTY PRZECHWYCENIA 185

public class Invoice


{
...
public Money getValue() {
Money total = itemsSum();
if (billingDate.after(Date.yearEnd(openingDate))) {
if (originator.getState().equals("FL") ||
originator.getState().equals("NY"))
total.add(getLocalShipping());
else
total.add(getDefaultShipping());
}
else
total.add(getSpanningShipping());
total.add(getTax());
return total;
}
...
}

Musimy zmienić sposób obliczania kosztów dostawy do Nowego Jorku. Ustawodawca


wprowadził właśnie podatek, który wpływa na wysokość kosztów naszych usług trans-
portowych w tym rejonie, w związku z czym niestety musimy przerzucić koszty na klienta.
Podczas wprowadzania zmian wyodrębnimy logikę odpowiedzialną za koszty i utworzymy
z niej nową klasę o nazwie ShippingPricer. Kiedy już skończymy, nasz kod powinien
wyglądać następująco:
public class Invoice
{
public Money getValue() {
Money total = itemsSum();
total.add(shippingPricer.getPrice());
total.add(getTax());
return total;
}
}

Cała praca, która odbywała się w metodzie getValue, jest obecnie wykonywana w klasie
ShippingPricer. Będziemy musieli również zmodyfikować konstruktor w klasie Invoice,
aby tworzył obiekt klasy ShippingPricer znający daty z faktury.
Aby znaleźć nasze punkty przechwycenia, musimy zacząć śledzić skutki w przód,
począwszy do punktów zmian. W przypadku metody getValue otrzymamy inne wyniki.
Okazuje się, że żadna z metod klasy Invoice nie korzysta z metody getValue, chociaż sama
getValue jest używana w pewnej klasie — korzysta z niej metoda makeStatement klasy
BillingStatement, co pokazano na rysunku 12.1.
Przyjdzie nam także dokonać modyfikacji konstruktora, tak więc powinniśmy spojrzeć
na kod, który od niego zależy. W tym przypadku będziemy tworzyć w konstruktorze nowy
obiekt klasy shippingPricer. Obiekt ten nie wpłynie na nic z wyjątkiem metod, które
z niego korzystają, a jedyną metodą, która będzie z niego korzystać, jest getValue. Rysunek
12.2 pokazuje te zależności.
186 ROZDZIAŁ 12. MUSZĘ DOKONAĆ WIELU ZMIAN W JEDNYM MIEJSCU

Rysunek 12.1. Metoda getValue wpływa na metodę BillingStatement.makeStatement

Rysunek 12.2. Skutki wpływające na metodę getValue

Oba schematy możemy ze sobą połączyć, jak pokazano na rysunku 12.3.

Rysunek 12.3. Ciąg skutków

Gdzie zatem znajdują się nasze punkty przechwycenia? Tak naprawdę możemy w tej
roli użyć dowolnego dymku na schemacie, pod warunkiem że mamy dostęp do elementu,
który jest przez dymek reprezentowany. Moglibyśmy spróbować poddać testom zmienną
shippingPricer, ale jest to zmienna prywatna klasy Invoice, w związku z czym nie mamy
do niej dostępu. Nawet gdybyśmy podczas testów mogli z niej korzystać, to i tak jest ona
raczej ograniczonym punktem przechwycenia. Moglibyśmy rozpoznać, co zrobiliśmy za
pomocą konstruktora (utworzyliśmy obiekt shippingPricer), i upewnić się, że obiekt ten
robi, co do niego należy, ale nie mielibyśmy możliwości zweryfikowania za jego pomocą,
czy metoda getValue nie ulegnie zmianie w nieodpowiedni sposób.
Moglibyśmy napisać testy angażujące metodę makeStatement klasy BillingStatement
i sprawdzić jej wartości zwrotne w celu upewnienia się, że poprawnie dokonaliśmy
naszych zmian. Najlepiej jednak będzie, jeśli przeprowadzimy testy weryfikujące działa-
nie metody getValue w klasie Invoice. W związku z tym czeka nas nawet mniej pracy.
PUNKTY PRZECHWYCENIA 187

Rzecz jasna dobrze byłoby poddać testom klasę BillingStatement, ale akurat w tym mo-
mencie nie jest to konieczne. Jeśli przyjdzie nam kiedyś dokonać zmian w tej klasie, bę-
dziemy mogli ją wtedy przetestować.

Zazwyczaj dobrym pomysłem jest takie dobieranie punktów przechwycenia, aby znajdowały się
one blisko punktów zmian. Jest ku temu kilka powodów. Pierwszym z nich jest bezpieczeństwo.
Każdy krok dzielący punkt zmiany od punktu przechwycenia jest jak etap w logicznym wywo-
dzie. To jakby powiedzieć: „Możemy tu przeprowadzić test, ponieważ to ma wpływ na tamto,
a tamto oddziałuje na jeszcze coś innego, co z kolei wpływa na element, który testujemy”. Im
więcej etapów istnieje w wywodzie logicznym, tym trudniej stwierdzić, czy mamy rację. Czasami
jedynym sposobem na zdobycie pewności jest napisanie testów w punkcie przechwycenia i po-
wrót do punktu zmiany w celu nieznacznego zmodyfikowania kodu i sprawdzenia, czy test prze-
chodzi. Czasem będziesz musiał zdać się na tę technikę, ale nie powinieneś uciekać się do niej za
każdym razem. Innym powodem, dla którego bardziej odległe punkty przechwycenia są gorsze,
jest fakt, że często umieszczanie w nich testów jest trudniejsze. Nie zawsze tak jest — to zależy od
kodu. Tym, co sprawia, że testowanie jest trudniejsze, ponownie jest liczba kroków dzielących
zmianę od punktu przechwycenia. Często musisz w swojej głowie odgrywać rolę komputera, aby
przekonać się, że test obejmuje jakiś odległy fragment funkcjonalności.
W naszym przykładzie zmiany, które chcemy wprowadzić w klasie Invoice, prawdopo-
dobnie najlepiej będzie przetestować właśnie tam. Klasę tę możemy utworzyć w jarzmie te-
stowym, skonfigurować ją na różne sposoby i wywoływać metodę getValue w celu spraw-
dzenia jej zachowania podczas dokonywania zmian klasy.

Punkty przechwycenia wyższego poziomu


W większości przypadków najlepszym punktem przechwycenia, jaki możemy mieć dla
zmiany, jest metoda publiczna klasy, którą poddajemy zmianie. Punkty te są łatwe do
znalezienia i proste w użyciu, ale czasami nie są najlepszym wyborem. Możemy się o tym
przekonać, jeśli nieco poszerzymy przykład z klasą Invoice.
Przyjmijmy, że oprócz zmiany sposobu obliczania kosztów dostawy w klasie Invoice
musimy też zmodyfikować klasę o nazwie Item, aby przechowywała środek transportu.
W klasie BillingStatement potrzebujemy także wprowadzić podział na spedytorów.
Rysunek 12.4 przedstawia wygląd naszego obecnego projektu na schemacie UML.

Rysunek 12.4. Poszerzony system fakturujący


188 ROZDZIAŁ 12. MUSZĘ DOKONAĆ WIELU ZMIAN W JEDNYM MIEJSCU

Jeśli dla żadnej z tych klas nie ma testów, moglibyśmy rozpocząć ich pisanie odrębnie
dla każdej klasy wraz z wprowadzaniem potrzebnych zmian. Taki sposób mógłby się
sprawdzić, ale wydajniejsza może okazać się próba znalezienia punktu przechwycenia
wyższego poziomu, którego to punktu moglibyśmy użyć do scharakteryzowania tej części
kodu. Istnieją dwie korzyści płynące z przyjęcia takiego podejścia: być może będziemy
mieć mniej zależności do usunięcia, a poza tym obejmujemy testami większy fragment
programu. Dzięki testom charakteryzującym całą grupę klas uzyskujemy większe pole do
refaktoryzacji. Możemy zmienić strukturę klas Invoice oraz Item, korzystając z testów,
które posiadamy w klasie BillingStatement jako niezmiennik. Oto dobry początek testu
charakteryzującego łącznie klasy BillingStatement, Invoice oraz Item:
void testSimpleStatement() {
Invoice invoice = new Invoice();
invoice.addItem(new Item(0,new Money(10));
BillingStatement statement = new BillingStatement();
statement.addInvoice(invoice);
assertEquals("", statement.makeStatement());
}

Możemy dowiedzieć się, co tworzy klasa BillingStatement dla faktury składającej się
z jednej pozycji, i zmienić test w taki sposób, aby korzystał z tego obiektu. W dalszej kolej-
ności możemy dodać więcej testów, aby sprawdzić, jak działa formatowanie zestawienia
dostawy dla różnych kombinacji faktur i pozycji. Powinniśmy szczególnie pamiętać o napi-
saniu przypadków testujących obszary kodu, w których będziemy wykorzystywać spoiny.
Co sprawia, że klasa BillingStatement jest idealnym punktem przechwycenia? Jest
ona jedynym punktem, z którego możemy skorzystać w celu wykrycia skutków zmian
wprowadzonych w grupie klas. Rysunek 12.5 pokazuje schemat skutków zmian, które
mamy zamiar wprowadzić.

Rysunek 12.5. Schemat skutków dla systemu fakturującego


PUNKTY PRZECHWYCENIA 189

Zwróć uwagę, że wszystkie skutki można zaobserwować poprzez klasę makeStatement.


Wykrycie ich za pośrednictwem tej klasy może nie być łatwe, niemniej jest ona jedynym
miejscem, w którym istnieje możliwość wykrycia każdego z nich. Takie miejsce w projek-
cie nazywam punktem zwężenia. Punkt zwężenia to przewężenie na schemacie skutków
(167); miejsce, w którym istnieje możliwość pisania testów obejmujących szeroki za-
kres zmian. Jeśli uda Ci się znaleźć punkt zwężenia w projekcie, Twoja praca może
stać się o wiele prostsza.
Najważniejsze, co należy pamiętać o punktach zwężenia, to fakt, że są one determi-
nowane przez punkty zmian. Zestaw zmian, którym została poddana klasa, może mieć
dobry punkt zwężenia, nawet jeśli klasa ta ma wiele klientów. Aby to unaocznić,
spójrzmy szerzej na system fakturujący, pokazany na rysunku 12.6.

Rysunek 12.6. System fakturujący ze spisem towarów

Nie zauważyliśmy tego wcześniej, ale klasa Item ma również metodę o nazwie
needsReorder. Klasa InventoryControl wywołuje ją za każdym razem, gdy musi ustalić,
czy ma złożyć zamówienie. Czy wpływa to na konieczność zmiany schematu skutków
w zakresie zmian, które chcemy wprowadzić? W najmniejszym stopniu. Dodanie w klasie
Item pola shippingCarrier w ogóle nie ma wpływu metodę needsReorder, w związku
z czym klasa BillingStatement nadal pozostaje naszym punktem zwężenia — wąskim
miejscem, w którym możemy prowadzić testy.
Zmieńmy jeszcze trochę nasz scenariusz. Załóżmy, że musimy wprowadzić kolejną
zmianę. Do klasy Item powinniśmy dodać metody umożliwiające określenie dostawcy
towaru. Klasy InventoryControl oraz BillingStatement będą korzystać z nazwy dostawcy.
Na rysunku 12.7 pokazano, jak zmiana ta wpływa na nasz schemat skutków.
Sprawy nie wyglądają teraz różowo. Skutki naszych zmian mogą być wykrywane za
pośrednictwem metody makeStatement klasy BillingStatement oraz poprzez zmienne, na
które wpływa metoda run klasy InventoryControl, ale nie mamy już pojedynczego punktu
przechwycenia. Niemniej rozpatrywane wspólnie metody run i makeStatement mogą być
postrzegane jako punkt zwężenia. Są to tylko dwie metody, które tworzą węższe miejsce,
umożliwiające wykrywanie problemów, a nie osiem metod i zmiennych, do których należy
się zabrać w celu wprowadzenia zmian. Jeśli umieścimy tam testy, będziemy mieć pokrycie
dla całej masy pracy związanej z dokonywaniem zmian.
190 ROZDZIAŁ 12. MUSZĘ DOKONAĆ WIELU ZMIAN W JEDNYM MIEJSCU

Rysunek 12.7. Pełny schemat systemu fakturującego

Punkt zwężenia
Punkt zwężenia to przewężenie na schemacie skutków — miejsce, w którym testy obej-
mujące kilka metod mogą wykryć zmiany dotyczące wielu metod.

W niektórych programach znalezienie punktów zwężenia dotyczących zbioru zmian


jest dość łatwe, ale w wielu przypadkach będzie to prawie niemożliwe. Pojedyncza klasa
albo metoda może wywierać bezpośredni wpływ na dziesiątki elementów, a schemat
skutków narysowany dla takiej klasy będzie wyglądać jak wielkie, rozgałęzione drzewo. Co
możemy zrobić w takim przypadku? Jedną z rzeczy jest zweryfikowanie naszych punktów
zmian. Być może próbujemy osiągnąć zbyt wiele za jednym zamachem? Rozważmy poszu-
kanie punktów zwężenia jednocześnie tylko dla jednej lub dwóch zmian. Jeśli w ogóle
nie możesz znaleźć takiego punktu, po prostu spróbuj umieścić testy dla poszczególnych
zmian tak blisko nich, jak tylko możesz.
Inny sposób szukania punktu zwężenia polega na odnalezieniu na schemacie skutków
(167) wspólnie używanych elementów. Metoda lub zmienna może mieć trzech użytkow-
ników, ale nie znaczy to, że jest używana na trzy różne sposoby. Załóżmy, że musimy
przeprowadzić refaktoryzację metody needsReorder w klasie Item z poprzedniego przy-
kładu. Nie pokazałem Ci jej kodu, ale jeśli naszkicujemy skutki, zobaczymy, że możemy
uzyskać punkt zwężenia, który zawiera metodę run klasy InventoryControl oraz metodę
makeStatement klasy BillingStatement. Lepszego zawężenia już nie uzyskamy.
OCENA PROJEKTU Z PUNKTAMI ZWĘŻENIA 191

Czy słuszne będzie napisanie testów tylko dla jednej z tych klas, a dla drugiej już nie? Klu-
czowe pytanie, jakie w tej sytuacji należy zadać, brzmi: „Jeśli rozbiję tę metodę, to czy będę
w stanie sprawdzić ją w tym miejscu?”. Odpowiedź zależy od sposobu użycia danej
metody. Jeśli jest stosowana tak samo w odniesieniu do obiektów, które mają porówny-
walne wartości, to przetestowanie jej w jednym miejscu — z pominięciem drugiego —
może być słuszne. Analizę przeprowadź wspólnie ze swoim kolegą.

Ocena projektu z punktami zwężenia


W poprzednim podrozdziale mówiliśmy o tym, jak pożyteczne w testach są punkty zwęże-
nia. Mają one jednak również inne zastosowania. Jeśli zwrócisz uwagę, gdzie punkty te się
znajdują, będziesz mógł dzięki nim uzyskać wskazówki, jak ulepszyć swój kod.
Czym tak naprawdę jest punkt zwężenia? Jest to naturalna granica hermetyzacji. Kiedy
odszukasz punkt zwężenia, to tak, jakbyś natrafił na lejek zbierający wszystkie skutki po-
chodzące z większego fragmentu kodu. Jeśli metoda BillingStatement.makeStatement
jest punktem zwężenia dla wielu faktur i towarów, będziemy wiedzieć, gdzie szukać,
gdy zestawienie dostawy będzie inne, niż oczekujemy. Przyczyną problemu muszą być
klasa BillingStatement lub faktury i towary. Analogicznie nie musimy nic wiedzieć
o fakturach ani towarach, aby wywołać metodę makeStatement. Mniej więcej taka właśnie
jest definicja hermetyzacji: nie musimy interesować się wnętrzem, ale gdy już to zrobimy,
nie będziemy musieli wyglądać na zewnątrz, aby zrozumieć elementy wewnętrzne. Kiedy
szukam punktów zwężenia, często zaczynam dostrzegać, w jaki sposób można między
klasami przemieszczać odpowiedzialności, aby uzyskać lepszą hermetyzację.

Korzystanie ze schematów skutków w celu znajdowania ukrytych klas


Czasami, kiedy masz do czynienia z dużą klasą, możesz skorzystać ze schematów skutków
w celu odkrycia, jak rozbić klasę na fragmenty. Oto przykład w Javie. Mamy klasę o nazwie
Parser, w której znajduje się metoda publiczna parseExpression.
public class Parser
{
private Node root;
private int currentPosition;
private String stringToParse;
public void parseExpression(String expression) { .. }
private Token getToken() { .. }
private boolean hasMoreTokens() { .. }
}

Jeśli narysujemy schemat efektów dla tej klasy, okaże się, że metoda parseExpression zależy
od metod getToken i hasMoreTokens, ale nie zależy bezpośrednio od klasy stringToParse ani
currentPosition, chociaż metody getToken i hasMoreTokens od nich zależą. To, z czym ma-
my tutaj do czynienia, jest naturalną granicą hermetyzacji, nawet jeśli granica ta nie jest tak
192 ROZDZIAŁ 12. MUSZĘ DOKONAĆ WIELU ZMIAN W JEDNYM MIEJSCU

naprawdę zawężona (w dwóch metodach ukrywają się dwie informacje). Możemy wyodrębnić
te metody i pola do klasy o nazwie Tokenizer i w rezultacie otrzymać prostszą klasę Parser.
Nie jest to jedyny sposób na odkrycie, jak wyodrębnić odpowiedzialności w klasie. Czasa-
mi podpowiedzią mogą być nazwy, tak jak to jest w tym przypadku (mamy dwie metody ze
słowem Token w nazwie). Dzięki temu spojrzysz na klasę w inny sposób, co może dopro-
wadzić do pomyślnego wyodrębniania klas.
W ramach ćwiczeń narysuj schemat skutków obrazujący zmiany w dużej klasie i pomiń nazwy
dymków — po prostu popatrz, jak są pogrupowane. Czy widać jakieś naturalne granice
hermetyzacji? Jeśli tak, spójrz na dymki znajdujące się wewnątrz tych granic. Zastanów się
nad nazwą, jaką mógłbyś nadać grupie tych metod i zmiennych; mogłaby ona stać się na-
zwą nowej klasy. Pomyśl, czy zmiana którejkolwiek z nazw byłaby pomocna.
Zajmij się tym ćwiczeniem razem ze swoimi kolegami z zespołu. Dyskusje, które przepro-
wadzicie na temat nazw, przyniosą korzyści wykraczające daleko poza pracę, którą aktual-
nie realizujecie — pomogą Tobie i Twojemu zespołowi wypracować wspólny pogląd na to,
czym jest dany system i czym może się stać.

Pisanie testów w punktach zwężenia jest doskonałym sposobem na przystąpienie do


inwazyjnych prac nad pewnym fragmentem programu. Tworzysz inwestycję, wyodręb-
niając zbiór klas i doprowadzając je do stanu, w którym istnieje możliwość łącznego utwo-
rzenia ich instancji w jarzmie testowym. Po napisaniu testów charakteryzujących (196)
będziesz mógł bezkarnie wprowadzać zmiany. Udało Ci się założyć w aplikacji małą oazę,
gdzie praca właśnie stała się łatwiejsza. Ale uważaj — to może być pułapka.

Pułapki w punktach zwężenia


Kiedy piszemy testy jednostkowe, możemy wpakować się w kłopoty na kilka różnych
sposobów. Jeden z nich to dopuszczenie do sytuacji, w której testy jednostkowe powoli
stają się minitestami integracyjnymi. Musimy przetestować klasę, tak więc tworzymy in-
stancje kilku klas z nią współpracujących i przekazujemy je do naszej klasy. Sprawdzamy
parę wartości i możemy mieć pewność, że cały zestaw obiektów wspólnie działa dobrze.
Wada takiego rozwiązania polega na tym, że jeśli będziemy je przyjmować zbyt często,
uzyskamy w rezultacie mnóstwo okropnych i nieporęcznych testów jednostkowych, któ-
rych uruchomienie trwa całą wieczność. Sztuczka, którą możemy zastosować podczas
pisania testów jednostkowych dla nowego kodu, sprowadza się do testowania klas na tyle
niezależnie od siebie, na ile jest to tylko możliwe. Kiedy zaczynasz zauważać, że Twoje te-
sty stają się zbyt duże, powinieneś rozbić testowaną klasę w celu uzyskania mniejszych,
niezależnych od siebie fragmentów, które można łatwiej przetestować. Czasami będziesz
musiał sfałszować klasy współpracujące, ponieważ zadaniem testu jednostkowego nie jest
sprawdzenie, jak wspólnie zachowuje się grupa obiektów, ale raczej, jak zachowuje się po-
jedynczy obiekt. Testowanie za pomocą fałszywek jest łatwiejsze.
PUŁAPKI W PUNKTACH ZWĘŻENIA 193

Kiedy piszemy testy dla istniejącego kodu, sytuacja ulega odwróceniu. Czasami opłaca
się odłączyć fragment aplikacji i obudować ją testami. Kiedy testy te znajdują się już na
miejscu, łatwiej będzie nam pisać węższe testy jednostkowe dla każdej z klas, do której
bierzemy się podczas pracy. W końcu będziemy mogli pozbyć się testów w punktach
zwężenia.
Testy umieszczane w punktach zwężenia to trochę jak wejście kilka kroków w głąb lasu,
narysowanie linii i ogłoszenie: „Posiadam cały ten obszar”. Kiedy już wiesz, że posiadasz
cały ten obszar, możesz go rozwinąć poprzez refaktoryzację i pisanie kolejnych testów.
Wraz z upływem czasu będziesz mógł usunąć testy z punktów zwężenia i pozwolić, aby
testy w każdej z klas wspierały Twoją pracę programisty.
194 ROZDZIAŁ 12. MUSZĘ DOKONAĆ WIELU ZMIAN W JEDNYM MIEJSCU
Rozdział 13.

Muszę dokonać zmian,


ale nie wiem,
jakie testy napisać

Kiedy ludzie mówią o testach, zwykle mają na myśli testy, których używają w celu szuka-
nia błędów. Często są to testy wykonywane ręcznie. Przeprowadzanie testów automa-
tycznych w celu znajdowania błędów w cudzym kodzie nie jest zazwyczaj aż tak wydajne,
jak zwykłe wypróbowywanie kodu. Jeżeli dysponujesz jakimś sposobem na ręczne uru-
chamianie cudzego kodu, zwykle dość szybko znajdziesz błędy. Wadą takiego podejścia
jest konieczność wykonywania ręcznej pracy za każdym razem, kiedy wprowadzisz zmiany
w kodzie. Poza tym — bądźmy szczerzy — nikt tego po prostu nie robi. Niemal wszystkie
zespoły, z którymi współpracowałem, a które bazowały na ręcznym testowaniu zmian,
zarzuciły takie testy dawno temu. Poziom pewności siebie w zespole nie był taki, jaki
mógłby być.
Nie, szukanie błędów w cudzym kodzie zwykle nie stanowi problemu. Pod względem
strategii może to być wysiłek skierowany w niewłaściwą stronę. Zazwyczaj lepsze jest
zrobienie czegoś, co pomoże Twojemu zespołowi rozpocząć konsekwentne pisanie po-
prawnego kodu. Sposób, w jaki można osiągnąć ten cel, polega przede wszystkim na skon-
centrowaniu swoich wysiłków na niewprowadzaniu błędów do kodu.
Testowanie automatyczne stanowi bardzo ważne narzędzie, ale nie podczas szukania
błędów — w każdym razie nie bezpośrednio. Zazwyczaj testy automatyczne powinny
specyfikować cel, który chcemy osiągnąć, lub pomagać w utrzymaniu zachowania, które
istnieje. W procesie naturalnego rozwoju testy, które specyfikują, stały się testami, które
utrzymują. Znajdziesz błędy, ale zwykle nie podczas pierwszego uruchomienia testu. Błędy
pojawią się w kolejnych uruchomieniach, kiedy zmienisz zachowanie programu w spo-
sób, którego nie oczekiwałeś.
196 ROZDZIAŁ 13. MUSZĘ DOKONAĆ ZMIAN, ALE NIE WIEM, JAKIE TESTY NAPISAĆ

Jaki związek ma z tym cudzy kod? W cudzym kodzie możemy nie mieć do dyspozycji
żadnych testów weryfikujących zmiany, które chcemy wprowadzić, a zatem nie mamy
sposobu, aby naprawdę sprawdzić, czy podczas dokonywania zmian utrzymujemy za-
chowanie programu. Z tego też powodu najlepszym podejściem, które możemy przyjąć,
gdy potrzebujemy wprowadzić zmiany, jest wyposażenie modyfikowanego obszaru testami,
które zapewnią nam swego rodzaju siatkę zabezpieczającą. W trakcie prac znajdziemy
błędy i będziemy musieli sobie z nimi poradzić, ale jeżeli naszym celem uczynimy wyszu-
kanie i poprawienie wszystkich błędów, to nigdy nie skończymy.

Testy charakteryzujące
OK, potrzebujemy więc testów. Tylko jak je pisać? Jeden ze sposobów podejścia do tego
problemu polega na dowiedzeniu się, co program powinien robić, i utworzeniu testów
w oparciu o tę wiedzę. Możemy postarać się dotrzeć do dokumentacji wymagań oraz no-
tatek dotyczących projektu, po czym po prostu usiąść i zabrać się do pisania testów. No
tak, jest to jakaś metoda, ale niespecjalnie dobra. W prawie każdym zastanym systemie
ważniejsze jest to, co ten system robi, niż to, co powinien robić. Jeśli napiszemy testy,
bazując na naszych przypuszczeniach dotyczących tego, co system powinien robić, znowu
powrócimy do szukania błędów. Znajdowanie błędów jest ważne, ale naszym obecnym
celem jest umieszczenie na miejscu testów, które pomogą nam wprowadzanie zmian
w bardziej deterministyczny sposób.
Testy, których potrzebujemy, gdy musimy pozostawić zachowanie, nazywam testami
charakteryzującymi. Test charakteryzujący to test, który charakteryzuje bieżące zacho-
wanie pewnego fragmentu kodu. Nie ma tu miejsca na zdania w rodzaju: „No cóż, kod
powinien to zrobić” albo „Sądzę, że to robi”. Testy te dokumentują rzeczywiste zachowanie
systemu.
Oto krótki algorytm dotyczący pisania testów charakteryzujących:
1. Skorzystaj z fragmentu kodu w jarzmie testowym.
2. Napisz asercję, o której wiesz, że nie zostanie spełniona.
3. Na podstawie niepowodzenia asercji dowiedz się, jakie jest zachowanie.
4. Zmień test w taki sposób, aby spodziewał się zachowania kodu.
5. Powtórz.
W poniższym przykładzie mam podstawy oczekiwać, że obiekt klasy PageGenerator
nie utworzy łańcucha tekstowego "fred":
void testGenerator() {
PageGenerator generator = new PageGenerator();
assertEquals("fred", generator.generate());
}
TESTY CHARAKTERYZUJĄCE 197

Uruchom swój test i pozwól, aby zakończył się niepowodzeniem. Gdy tak się stanie,
dowiesz się, co robi kod przy tak określonym warunku. Na przykład w powyższym kodzie
nowo utworzony obiekt klasy PageGenerator wygeneruje po wywołaniu swojej metody
generate pusty łańcuch tekstowy:
.F
Time: 0.01
There was 1 failure:
1) testGenerator(PageGeneratorTest)
junit.framework.ComparisonFailure: expected:<fred> but was:<>
at PageGeneratorTest.testGenerator
(PageGeneratorTest.java:9)
at sun.reflect.NativeMethodAccessorImpl.invoke0
(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke
(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke
(DelegatingMethodAccessorImpl.java:25)
FAILURES!!!
Tests run: 1, Failures: 1, Errors: 0

Możemy zmienić test tak, aby się powiódł:


void testGenerator() {
PageGenerator generator = new PageGenerator();
assertEquals("", generator.generate());
}

Teraz test przechodzi. Co więcej, dokumentuje jedną z podstawowych cech klasy


PageGenerator: kiedy tworzymy jej instancję i od razu chcemy, aby coś wygenerowała,
tworzy ona pusty łańcuch tekstowy.
Możemy skorzystać z tej samej sztuczki, aby dowiedzieć się, jakie będzie zachowanie
tej klasy, kiedy przekażemy jej inne dane:
void testGenerator() {
PageGenerator generator = new PageGenerator();
generator.assoc(RowMappings.getRow(Page.BASE_ROW));
assertEquals("fred", generator.generate());
}

W tym przypadku komunikat o błędzie pochodzący z jarzma testowego informuje nas,


że wynikowym łańcuchem tekstowym jest "<node><carry>1.1 vectrai</carry></node>",
dzięki czemu z łańcucha tego możemy uczynić spodziewaną wartość w teście:
void testGenerator() {
PageGenerator generator = new PageGenerator();
assertEquals("<node><carry>1.1 vectrai</carry></node>",
generator.generate());
}

W takim postępowaniu jest coś zasadniczo dziwnego, jeśli jesteś przyzwyczajony do


myślenia, że testy to… no, testy. Jeżeli po prostu umieszczasz w testach wartości genero-
wane przez program, to czy nasze testy w ogóle cokolwiek testują? A co, jeśli w programie
198 ROZDZIAŁ 13. MUSZĘ DOKONAĆ ZMIAN, ALE NIE WIEM, JAKIE TESTY NAPISAĆ

znajduje się błąd? Spodziewane wartości, które wprowadzamy w testach, mogą być
najzwyczajniej w świecie złe.
Problem ten przestaje istnieć, gdy zaczynamy myśleć o testach inaczej. Nie są one tak
naprawdę testami pisanymi jako złoty standard, który musi być dotrzymany przez pro-
gram. W tej chwili nie szukamy błędów. Mechanizm znajdujący błędy postaramy się
uruchomić później. Będą to błędy objawiające się jako różnice w stosunku do obecnego
zachowania. Kiedy przyjmiemy taki punkt widzenia, nasz pogląd na testy stanie się inny:
nie mają one żadnego autorytetu moralnego; po prostu sobie tam tkwią i dokumentują to,
co rzeczywiście robią poszczególne fragmenty systemu. Kiedy już dowiemy się, co te frag-
menty robią, będziemy mogli skorzystać z tej wiedzy — łącznie z informacjami o tym,
co system powinien robić — w celu dokonania zmian. Szczerze mówiąc, bardzo ważne jest,
abyśmy wiedzieli, co system tak naprawdę robi. Zwykle możemy dowiedzieć się, jakie za-
chowanie jest potrzebne, rozmawiając z innymi ludźmi albo przeprowadzając rozmaite
obliczenia, ale z wyjątkiem testów nie mamy innego sposobu, aby poznać, co system tak
naprawdę robi. Możemy jedynie „odgrywać rolę komputera” w naszych głowach, czytać
kod i starać się wywnioskować, jakie wartości pojawią się w określonych momentach.
Niektórzy potrafią to robić szybciej niż inni, chociaż niezależnie od prędkości jest to
czynność dosyć żmudna i nieekonomiczna, jeśli musi być powtarzana wciąż na nowo.

Testy charakteryzujące odnotowują faktyczne zachowanie fragmentu kodu. Jeśli podczas


ich pisania natrafimy na coś niespodziewanego, warto będzie sprawę wyjaśnić, gdyż może
się okazać, że jest to błąd. Nie oznacza to jednak, iż nie dołączymy tego testu do naszego
zestawu testów. Przeciwnie, powinniśmy oznaczyć go jako test podejrzany i dowiedzieć się,
jaki będzie efekt jego poprawienia.

Z pisaniem testów charakteryzujących wiąże się o wiele więcej, niż przedstawiłem do


tej pory. W przykładzie z generatorem mogło się zdawać, że wartości testowe otrzymywa-
liśmy na ślepo, wrzucając łańcuchy tekstowe w kod i uzyskując je w asercjach. Możemy tak
postępować, jeśli mamy dobre pojęcie na temat tego, co kod powinien robić. Niektóre
przypadki, takie jak nierobienie niczego z obiektem i sprawdzenie, co utworzą jego metody,
łatwo jest obmyślić i warto je scharakteryzować, ale co zrobić w następnej kolejności? Jaka
jest ogólna liczba testów, które możemy przeprowadzić z takim obiektem, jak generator
stron? Nieskończona. Sporą część naszego życia moglibyśmy spędzić na pisaniu przypad-
ków testowych dla tej klasy. Kiedy powinniśmy się zatrzymać? Czy jest jakiś sposób,
aby dowiedzieć się, które przypadki testowe są ważniejsze od innych?
Ważne jest, aby zdać sobie sprawę z tego, że nie piszemy testów czarnej skrzynki —
mamy możliwość zajrzenia do kodu, który charakteryzujemy. Sam kod może nam podsu-
nąć pomysły na temat tego, co robi, a jeśli mamy jakieś pytania, testy będą idealnym
sposobem na ich zadanie. Pierwszym krokiem w teście charakteryzującym jest wzbudze-
nie w sobie ciekawości w odniesieniu do zachowania kodu. Na tym etapie piszemy po
prostu testy, aż stwierdzimy, że je dobrze zrozumieliśmy. Czy pokrywają one cokolwiek
w kodzie? Być może nie, ale mamy do zrobienia następny krok. Myślimy o zmianach, które
CHARAKTERYZOWANIE KLAS 199

chcemy wprowadzić w kodzie, i staramy się sprawdzić, czy testy, jakimi dysponujemy,
wykryją problemy, które możemy spowodować. Jeśli nie, dodajemy kolejne testy, aż do
uzyskania pewności, że błędy zostaną wykryte. Jeżeli jednak nie mamy tej pewności,
bezpieczniej będzie zastanowić się nad wprowadzeniem innych zmian w programie. Może
uda nam się zrealizować kod, który braliśmy pod uwagę na początku.

Reguła użycia metody


Zanim w cudzym systemie użyjesz metody, sprawdź, czy istnieją dla niej jakieś testy. Jeśli
nie, napisz je. Jeśli będziesz konsekwentny w takim postępowaniu, testy staną się dla Ciebie
środkiem porozumiewania się. Ludzie będą mogli na nie spojrzeć i dowiedzieć się, czego mogą,
a czego nie mogą spodziewać się po danej metodzie. Już sam proces przystosowywania klasy do
testów sprawia, że kod zyskuje na jakości. Ludzie mogą dowiadywać się, co i w jaki sposób
działa, zmieniać to, poprawiać błędy, a także posuwać się do przodu.

Charakteryzowanie klas
Dysponujemy klasą i chcemy dowiedzieć się, co przetestować. Jak możemy to zrobić?
Najpierw należy spróbować dowiedzieć się, co klasa ta robi na wysokim poziomie. Możemy
napisać testy dla najprostszych zadań, które zgodnie z naszymi wyobrażeniami są przez nią
realizowane, i pozwolić, aby dalej prowadziła nas nasza ciekawość. Oto nieco heurystyki,
która może nam w tym pomóc:
1. Poszukaj miejsc z zagmatwaną logiką. Jeśli nie rozumiesz pewnego fragmentu kodu,
rozważ wprowadzenie zmiennej rozpoznającej (300) w celu jego scharakteryzo-
wania. Korzystaj ze zmiennych rozpoznających, aby mieć pewność, że określone
obszary kodu są wykonywane.
2. Podczas odkrywania odpowiedzialności klasy lub metody zatrzymuj się, aby spo-
rządzić listę rzeczy, które mogą pójść źle. Sprawdź, czy potrafisz sformułować testy
mogące wywołać takie sytuacje.
3. Zastanów się nad wartościami wejściowymi, które wprowadzasz podczas testu.
Co dzieje się dla wartości skrajnych?
4. Czy któreś z warunków powinny pozostawać spełnione przez cały czas życia klasy?
Często warunki takie nazywane są inwariantami. Spróbuj napisać testy, które je
zweryfikują. Zwykle w celu wykrycia tych warunków będziesz musiał przeprowa-
dzić refaktoryzację. Jeżeli się na to zdecydujesz, może ona sprawić, że uzyskasz nowe
spojrzenie na to, jaki powinien być kod.
Testy pisane w celu scharakteryzowania kodu są bardzo ważne. Stanowią one doku-
mentację rzeczywistego zachowania systemu. Tak samo jak w przypadku każdej dokumen-
tacji, którą tworzysz, powinieneś zastanowić się nad tym, co będzie ważne dla użytkownika.
200 ROZDZIAŁ 13. MUSZĘ DOKONAĆ ZMIAN, ALE NIE WIEM, JAKIE TESTY NAPISAĆ

Postaw się na jego miejscu. Co chciałbyś wiedzieć na temat klasy, nad którą pracujesz,
gdybyś nigdy wcześniej jej nie widział? W jakim porządku wolałbyś otrzymywać o niej in-
formacje? Jeśli korzystasz z platformy xUnit, testy będą metodami zapisanymi w pliku.
Możesz umieszczać je w kolejności, która ułatwi innym osobom zrozumienie stosowa-
nego przez nie kodu. Rozpocznij od prostych przypadków, ukazujących główne zadanie
klasy, a następnie przejdź do przykładów ilustrujących jej osobliwości. Nie zapomnij
udokumentować ważnych faktów, które odkryjesz podczas przeprowadzania testów. Czę-
sto po przystąpieniu do wprowadzania zmian stwierdzisz, że napisane przez Ciebie te-
sty są bardzo przydatne w pracy, jaką masz zamiar wykonać. Bez względu na to, czy
myślimy o tym świadomie, czy też nie, nasza ciekawość będzie kierowana zmianą, którą
zamierzamy wprowadzić.

Gdy znajdujesz błędy


Podczas charakteryzowania cudzego kodu będziesz znajdował w nim błędy. Każdy cudzy
kod zawiera błędy, zwykle w liczbie wprost proporcjonalnej do stopnia jego niezrozumienia.
Co powinieneś zrobić, kiedy natrafisz na błąd?
Odpowiedź na to pytanie zależy od sytuacji. Jeśli system nigdy nie został wdrożony, odpowiedź
będzie prosta: popraw błąd. Jeżeli system został wdrożony, powinieneś sprawdzić ewentu-
alność, że ktoś polega na danej funkcjonalności, nawet jeśli postrzegasz ją jako błąd. Często
potrzebne będzie przeprowadzenie analizy w celu ustalenia, jak naprawić usterkę w kodzie
bez wywoływania efektu domina.
Osobiście skłaniam się ku poprawianiu błędów, gdy tylko zostaną znalezione. Jeżeli pewne
zachowanie jest niewątpliwie błędne, powinno zostać skorygowane. Jeśli tylko podejrzewasz, że
zachowanie jest nieprawidłowe, oznacz je w kodzie testowym jako podejrzane, po czym zinten-
syfikuj testy. Jak najszybciej dowiedz się, czy zachowanie to jest błędne i jaki jest najlepszy
sposób, aby dać sobie z nim radę.

Testowanie ukierunkowane
Po napisaniu testów pomagających nam zrozumieć fragment kodu powinniśmy spojrzeć
na elementy, które chcemy zmienić, i sprawdzić, czy nasze testy istotnie je pokrywają.
Oto przykład — metoda w Javie, która oblicza wartość paliwa znajdującego się w dzierża-
wionych zbiornikach.
public class FuelShare
{
private long cost = 0;
private double corpBase = 12.0;
private ZonedHawthorneLease lease;
...
public void addReading(int gallons, Date readingDate){
if (lease.isMonthly()) {
TESTOWANIE UKIERUNKOWANE 201

if (gallons < Lease.CORP_MIN)


cost += corpBase;
else
cost += 1.2 * priceForGallons(gallons);
}
...
lease.postReading(readingDate, gallons);
}
...
}

W klasie FuelShare chcemy wprowadzić bezpośrednią zmianę. Napisaliśmy już dla tej
klasy kilka testów, a zatem jesteśmy na to gotowi. Oto nasza zmiana: chcielibyśmy wyod-
rębnić wysokopoziomową instrukcję warunkową if, utworzyć z niej nową metodę i prze-
sunąć ją do klasy ZonedHawthorneLease. Zmienna lease jest instancją tej klasy.
Możemy sobie wyobrazić, jak będzie wyglądać kod po refaktoryzacji:
public class FuelShare

{
public void addReading(int gallons, Date readingDate){
cost += lease.computeValue(gallons,
priceForGallons(gallons));
...
lease.postReading(readingDate, gallons);
}
...
}

public class ZonedHawthorneLease extends Lease


{
public long computeValue(int gallons, long totalPrice) {
long cost = 0;
if (lease.isMonthly()) {
if (gallons < Lease.CORP_MIN)
cost += corpBase;
else
cost += 1.2 * totalPrice;
}
return cost;
}
...
}

Jakich testów potrzebujemy, aby upewnić się, że refaktoryzację przeprowadziliśmy


poprawnie? Jedno jest pewne: wiemy, że w najmniejszym nawet stopniu nie będziemy
modyfikować następującego fragmentu kodu:
if (gallons < Lease.CORP_MIN)
cost += corpBase;

Mając już test na miejscu, dobrze byłoby wiedzieć, jak obliczana jest wartość mniejsza od
minimalnego limitu zapisanego w polu Lease.CORP_MIN, chociaż nie jest to nam bezwzględ-
nie potrzebne. Z kolei ulegnie zmianie poniższa instrukcja else z oryginalnej wersji kodu:
202 ROZDZIAŁ 13. MUSZĘ DOKONAĆ ZMIAN, ALE NIE WIEM, JAKIE TESTY NAPISAĆ

else
valueInCents += 1.2 * priceForGallons(gallons);

Kiedy przesuniemy ten kod do nowej metody, przybierze on następującą postać:


else
valueInCents += 1.2 * totalPrice;

Zmiana jest mała, niemniej pozostaje zmianą. Gdybyśmy mogli upewnić się, że powyż-
sza instrukcja else wykonuje się w jednym z testów, nasza sytuacja stałaby się pewniejsza.
Jeszcze raz spójrzmy na wyjściową metodę:
public class FuelShare
{
public void addReading(int gallons, Date readingDate){
if (lease.isMonthly()) {
if (gallons < CORP_MIN)
cost += corpBase;
else
cost += 1.2 * priceForGallons(gallons);
}
...
lease.postReading(readingDate, gallons);
}
...
}

Jeśli uda nam się utworzyć obiekt klasy FuelShare z miesięczną ratą leasingu i spróbu-
jemy wywołać metodę addReading dla objętości paliwa większej niż zapisana w polu
Lease.CORP_MIN, to wykona się następujący fragment instrukcji else:
public void testValueForGallonsMoreThanCorpMin() {
StandardLease lease = new StandardLease(Lease.MONTHLY);
FuelShare share = new FuelShare(lease);

share.addReading(FuelShare.CORP_MIN +1, new Date());


assertEquals(12, share.getCost());
}

Pisząc test dla rozgałęzienia, zadaj sobie pytanie, czy istnieje jakiś inny sposób na przepro-
wadzenie testu oprócz uruchomienia kodu zawartego w rozgałęzieniu. Jeśli nie masz pew-
ności, skorzystaj ze zmiennej rozpoznającej (300) lub debugera, aby dowiedzieć się, czy
testy przechodzą.

Jedną z ważniejszych czynności, jakie należy przeprowadzić podczas charakteryzowania


rozgałęzień, jest sprawdzenie, czy wprowadzane przez Ciebie dane wejściowe mogą dopro-
wadzić do powodzenia testu, chociaż test powinien zakończyć się porażką. Oto przykład.
Załóżmy, że w poniższym kodzie do reprezentowania pieniędzy użyto liczb podwójnej
precyzji zamiast całkowitych.
public class FuelShare
{
private double cost = 0.0;
TESTOWANIE UKIERUNKOWANE 203

...
public void addReading(int gallons, Date readingDate){
if (lease.isMonthly()) {
if (gallons < CORP_MIN)
cost += corpBase;
else
cost += 1.2 * priceForGallons(gallons);
}
...
lease.postReading(readingDate, gallons);
}
...
}

Taki kod mógłby wpędzić nas w poważne tarapaty i wcale nie mam na myśli tego, że
na skutek błędów zaokrągleń liczb zmiennoprzecinkowych z aplikacji prawdopodobnie
wyciekają pojedyncze grosze. O ile tylko nie dobierzemy trafnych danych wejściowych,
możemy w trakcie wyodrębniania metody popełnić pomyłkę i nigdy się o tym nie dowiemy.
Jeden z błędów może wystąpić, gdy wyodrębnimy metodę i któryś z jej argumentów zde-
finiujemy jako int zamiast double. W Javie i wielu innych językach następuje automatycz-
na konwersja z liczb podwójnej precyzji na całkowite — podczas działania programu
liczby są po prostu obcinane. Jeśli tylko nie wybierzemy starannie danych wejściowych,
które wskażą nam błąd, przeoczymy go.
Spójrzmy na przykład. Jaki będzie efekt działania poprzedniego kodu, jeśli w chwili
uruchomienia testu wartość zmiennej LeaseCORP_MIN wyniesie 10, natomiast zmiennej
corpBase 12.0?
public void testValue () {
StandardLease lease = new StandardLease(Lease.MONTHLY);
FuelShare share = new FuelShare(lease);

share.addReading(1, new Date());


assertEquals(12, share.getCost());
}

Ponieważ 1 jest mniejsze niż 10, dodajemy po prostu 12.0 do początkowej wartości
zmiennej cost, która wynosi 0. Pod koniec obliczeń wartość tej zmiennej będzie równa
12.0 i właśnie tak powinno być. Co jednak się stanie, jeśli wyodrębnimy metodę w poniż-
szy sposób i zadeklarujemy zmienną cost jako long zamiast double?
public class ZonedHawthorneLease
{
public long computeValue(int gallons, long totalPrice) {
long cost = 0;
if (lease.isMonthly()) {
if (gallons < CORP_MIN)
cost += corpBase;
else
cost += 1.2 * totalPrice;
}
return cost;
}
}
204 ROZDZIAŁ 13. MUSZĘ DOKONAĆ ZMIAN, ALE NIE WIEM, JAKIE TESTY NAPISAĆ

Test, który napisaliśmy, nadal przechodzi, chociaż po cichu obcinamy wartość zmiennej
cost podczas jej zwracania. Konwersja z typu double na int nadal ma miejsce, ale tak
naprawdę nie jest w pełni realizowana. Dzieje się to samo, co miałoby miejsce, gdyby nie
było żadnej konwersji: przypisujemy wartość całkowitą zmiennej całkowitej.

Kiedy refaktorujemy, musimy zwykle sprawdzić dwie rzeczy: czy po refaktoryzacji istnieje
dane zachowanie oraz czy jest prawidłowo powiązane z kodem.
Wiele testów charakteryzujących przypomina testy „słonecznego dnia”. Nie sprawdzają one
specyficznych warunków, a weryfikują tylko, czy określone zachowania są obecne. Na ich
podstawie możemy wyciągnąć wniosek, że refaktoryzacja, jaką przeprowadziliśmy w celu
przesunięcia lub wyodrębnienia kodu, pozostawiła zachowanie.

Jak możemy sobie z tym poradzić? Istnieje kilka ogólnych strategii. Jedna z nich polega
na ręcznym obliczeniu wartości, które spodziewamy się uzyskać w pewnym fragmencie
kodu. Przy okazji każdej konwersji sprawdzamy, czy pojawia się problem z obcinaniem
liczb. Inna technika sprowadza się do skorzystania z debugera i przejścia krok po kroku
wszystkich operacji przypisania, co umożliwi nam stwierdzenie, jakie konwersje są wy-
woływane przez określony zestaw danych wejściowych. Trzecia technika to użycie zmien-
nych rozpoznających (300) w celu sprawdzenia, czy określony przebieg został uwzględnio-
ny, a konwersje są dokonywane.

Najcenniejsze są testy charakteryzujące, które obejmują określony przebieg i angażują wszystkie


konwersje mające miejsce w tym przebiegu.

Istnieje jeszcze czwarta opcja. Możemy podjąć decyzję o scharakteryzowaniu mniej-


szego fragmentu kodu. Jeśli mamy do dyspozycji narzędzie refaktoryzujące, które umożli-
wia bezpieczne wyodrębnianie metod, będziemy mogli podzielić metodę computeValue
i napisać testy dla mniejszych jej fragmentów. Niestety, nie dla wszystkich języków istnieją
narzędzia refaktoryzujące, a nawet jeśli są dostępne, to czasami nie wyodrębniają one
metod w taki sposób, jaki jest Ci potrzebny.

Osobliwości narzędzi refaktoryzujących


Dobre narzędzie refaktoryzujące jest bezcenne, ale często osoby, które dysponują tego typu
narzędziami, muszą dokonywać ręcznej refaktoryzacji. Oto jeden z często spotykanych
przypadków. Mamy klasę A z kodem, który chcielibyśmy wyodrębnić z metody b():
public class A
{
int x = 1;
public void b() {
int y = 0;
int c = x + y;
}
};
HEURYSTYKA PISANIA TESTÓW CHARAKTERYZUJĄCYCH 205

Chcemy z metody b wyodrębnić wyrażenie x + y i utworzyć z niego metodę o nazwie add.


Przynajmniej jedno z dostępnych narzędzi wyodrębni metodę add(y) zamiast add(x,y).
Dlaczego? Ponieważ x jest zmienną instancji i będzie dostępne dla wszystkich metod, które
wyodrębniamy.

Heurystyka pisania testów charakteryzujących


1. Napisz testy obejmujące obszar, w którym wprowadzisz zmiany. Opracuj tyle przy-
padków, ile potrzebujesz do zrozumienia zachowania kodu.
2. W następnej kolejności przyjrzyj się konkretnym elementom, które masz zamiar
zmienić, i postaraj się napisać dla nich testy.
3. Jeżeli próbujesz wyodrębnić albo przesunąć funkcjonalność, utwórz testy, które
przypadek po przypadku weryfikują istnienie zachowań oraz potwierdzają ich zwią-
zek z tą funkcjonalnością. Upewnij się, że testujesz kod, który zamierzasz przesunąć,
i że ma on związek z funkcjonalnością. Sprawdź dokonywane konwersje.
206 ROZDZIAŁ 13. MUSZĘ DOKONAĆ ZMIAN, ALE NIE WIEM, JAKIE TESTY NAPISAĆ
Rozdział 14.

Dobijają mnie zależności


biblioteczne

Techniką, która rzeczywiście pomaga w programowaniu, jest wielokrotne użycie kodu.


Jeśli możemy kupić bibliotekę, która rozwiązuje za nas jakiś problem (a także możemy
rozgryźć, jak z niej korzystać), często uda nam się zaoszczędzić sporo czasu przeznaczonego
na pracę nad projektem. Jedyna wada tego rozwiązania polega na tym, że uzależnienie się
od takiej biblioteki jest bardzo łatwe. Jeżeli w swoim kodzie zaczniesz szeroko z niej
korzystać, to praktycznie zostaniesz na nią skazany. Niektóre z zespołów, z którymi przy-
szło mi współpracować, mocno sparzyły się na pokładaniu zbyt wielkiego zaufania
w bibliotekach. W jednym z przypadków sprzedawca biblioteki tak bardzo podniósł
opłaty licencyjne, że aplikacja nie była w stanie na siebie zarobić. Zespół nie mógł w łatwy
sposób skorzystać z biblioteki innego producenta, ponieważ wyodrębnienie odwołań
do kodu dostarczonego przez poprzednika byłoby równoznaczne z przepisaniem całej
aplikacji na nowo.

Unikaj zaśmiecania swojego kodu bezpośrednimi odwołaniami do klas bibliotecznych.


Możesz myśleć, że nigdy nie będziesz musiał ich zmienić, ale takie przekonanie może prze-
rodzić się w samospełniającą się przepowiednię.

Kiedy piszę te słowa, większość świata programistycznego skupiła się wokół Javy
i platformy .NET. Zarówno Microsoft, jak i Sun, tworząc liczne biblioteki, postarały się,
aby ich środowiska były maksymalnie uniwersalne. Dzięki temu ich produkty w dalszym
ciągu będą używane przez programistów. W przypadku wielu projektów jest to jak wygrana
na loterii, chociaż nadal możesz za bardzo uzależnić się od określonej biblioteki. Każde
zapisane w kodzie użycie klasy bibliotecznej jest miejscem, w którym można utworzyć
spoinę. Niektóre biblioteki bardzo dobrze definiują interfejsy do wszystkich swoich kon-
kretnych klas. W innych przypadkach klasy są konkretne i zadeklarowane jako finalne albo
208 ROZDZIAŁ 14. DOBIJAJĄ MNIE ZALEŻNOŚCI BIBLIOTECZNE

zapieczętowane lub ich kluczowe funkcje nie są wirtualne, co uniemożliwia ich sfał-
szowanie podczas testów. W takich okolicznościach czasami najlepsze, co można zrobić,
to utworzyć cienkie opakowanie dla klasy, którą musisz wyodrębnić. Nie zapomnij na-
pisać do swojego dostawcy i wypomnieć mu, jak bardzo utrudnia Ci programowanie.

Projektanci bibliotek, którzy korzystają z funkcji języka w celu narzucenia ograniczeń w projek-
tach, często popełniają błąd. Zapominają, że dobry kod jest uruchamiany zarówno w śro-
dowisku produkcyjnym, jak i testowym. Ograniczenia wprowadzone w środowisku pro-
dukcyjnym mogą wręcz uniemożliwić pracę w środowisku testowym.

Istnieją zasadnicze konflikty między funkcjami języka mającymi na celu wymuszenie


dobrego projektu a czynnościami, jakie musisz wykonać w celu przetestowania kodu.
Jednym z najczęściej spotykanych jest dylemat jednorazowości. Jeżeli w bibliotece przy-
jęto, że w systemie ma prawo występować tylko jedna instancja danej klasy, użycie fałszy-
wych obiektów może okazać się trudne. Może nie być sposobu na wprowadzanie statycz-
nego settera (370) ani też jakiejkolwiek z wielu technik usuwania zależności, które
mogą być stosowane w odniesieniu do singletonów. Czasami opakowanie singletona
jest jedynym dostępnym rozwiązaniem.
Powiązanym problemem jest dylemat ograniczonego przesłaniania. W niektórych
językach zorientowanych obiektowo wszystkie metody są wirtualne. W innych metody są
wirtualne domyślnie, chociaż można je zmienić na niewirtualne. W jeszcze innych językach
metody należy jawnie definiować jako wirtualne. Z perspektywy projektu tworzenie
części metod jako niewirtualnych ma pewną wartość. Czasami niektóre osoby z branży
zalecają definiowanie jako niewirtualnych tylu metod, ile jest to tylko możliwe. Zdarza się,
że podają ku temu dobre powody, jednak trudno zaprzeczyć, że taka praktyka utrudnia
w bazach kodu przeprowadzanie rozpoznania oraz wyodrębniania. Trudno też zaprze-
czyć, że programiści często piszą bardzo dobry kod w języku Smalltalk, gdzie takie rozwią-
zanie jest niemożliwe; w Javie, gdzie zwykle tak się nie robi; a nawet w C++, gdzie sporo
kodu napisano bez uciekania się do tej techniki. Możesz całkiem dobrze dawać sobie radę
w kodzie produkcyjnym, udając po prostu, że metoda publiczna jest niewirtualna. Jeżeli
tak postąpisz, będziesz mógł ją wybiórczo przesłaniać w testach i dzięki temu upiec dwie
pieczenie przy jednym ogniu.

Czasami stosowanie konwencji kodowania jest równie dobre jak korzystanie z funkcjonalności
języka restrykcyjnego. Zastanów się, co będzie potrzebne w Twoich testach.
Rozdział 15.

Cała moja aplikacja


to wywołania API

Utwórz, kup albo pożycz. Taki jest wybór, jakiego wszyscy musimy dokonać podczas pi-
sania programu. Kiedy pracujemy nad aplikacją, wiele razy zdarza się, że podejrzewamy,
iż moglibyśmy zaoszczędzić sobie nieco czasu oraz wysiłku, kupując jakąś gotową bi-
bliotekę, korzystając z otwartego kodu lub nawet używając sporych fragmentów kodu po-
chodzącego z bibliotek dołączonych do naszych platform (J2EE, .NET itd.). Kiedy decydu-
jemy się na dołączenie kodu, którego nie możemy zmodyfikować, powinniśmy rozważyć
wiele różnych kwestii. Musimy wiedzieć, jak bardzo jest on stabilny, czy jest wystarczalny
i czy jest prosty w użyciu. Gdy wreszcie podejmiemy decyzję o użyciu cudzego kodu,
często napotykamy kolejny problem. Uzyskujemy aplikacje, które zdają się nie robić nic
innego, jak tylko dokonywać wielokrotnych odwołań do czyjejś biblioteki. W jaki sposób
możemy wprowadzać zmiany w takim kodzie?
Pierwsza pokusa każe nam powiedzieć, że tak naprawdę nie potrzebujemy testów.
W końcu nie robimy nic ważnego — po prostu tu i tam wywołujemy metodę, a nasz kod
jest prosty. To tyle. Co mogłoby pójść źle?
Wiele cudzych projektów miało właśnie takie skromne początki. Kod rósł i rósł, a spra-
wy przestały być już takie proste. Po jakimś czasie być może nadal bylibyśmy w stanie
wskazać fragmenty aplikacji, które nie tykają API, jednak są one osadzone w mozaice nie-
przetestowanego kodu. Za każdym razem, gdy coś zmienimy, musimy uruchamiać pro-
gram, aby przekonać się, czy nadal działa. Powracamy tym samym do głównej rozterki
programisty, który pracuje nad cudzym kodem. Nie mamy pewności co do zmian; nie
jesteśmy autorami całego kodu, ale musimy go konserwować.
Pod wieloma względami ciężej jest zajmować się systemami, które są usiane odwoła-
niami do bibliotek, niż systemami, które opracowało się we własnym zakresie. Pierw-
szy powód jest taki, że często trudno jest stwierdzić, jak można by poprawić strukturę
aplikacji, gdyż wszystko, co widać, to wywołania API. Drugi powód, dla którego sys-
temy mocno bazujące na API są trudne, jest taki, że nie jesteśmy właścicielami API.
210 ROZDZIAŁ 15. CAŁA MOJA APLIKACJA TO WYWOŁANIA API

Gdybyśmy mieli prawa do API, moglibyśmy zmieniać nazwy interfejsów, klas i metod,
aby były dla nas bardziej zrozumiałe. Moglibyśmy także dodać metody do klas i sprawić,
żeby były one dostępne w różnych częściach kodu.
Oto przykład. Poniżej znajduje się listing kiepsko napisanego kodu serwera listy ma-
ilingowej. Nie mamy nawet pewności, czy działa.
import java.io.IOException;
import java.util.Properties;

import javax.mail.*;
import javax.mail.internet.*;

public class MailingListServer


{
public static final String SUBJECT_MARKER = "[list]";
public static final String LOOP_HEADER = "X-Loop";

public static void main (String [] args) {


if (args.length != 8) {
System.err.println ("Użycie: java MailingList <popHost> " +
"<smtpHost> <pop3user> <pop3password> " +
"<smtpuser> <smtppassword> <listname> " +
"<relayinterval>");
return;
}

HostInformation host = new HostInformation (


args [0], args [1], args [2], args [3],
args [4], args [5]);
String listAddress = args[6];
int interval = new Integer (args [7]).intValue ();
Roster roster = null;
try {
roster = new FileRoster("roster.txt");
} catch (Exception e) {
System.err.println ("nie można otworzyć pliku roster.txt");
return;
}
try {
do {
try {
Properties properties = System.getProperties ();
Session session = Session.getDefaultInstance (
properties, null);
Store store = session.getStore ("pop3");
store.connect (host.pop3Host, -1,
host.pop3User, host.pop3Password);
Folder defaultFolder = store.getDefaultFolder();
if (defaultFolder == null) {
System.err.println("Nie można otworzyć folderu
domyślnego");
return;
}
Folder folder = defaultFolder.getFolder ("INBOX");
CAŁA MOJA APLIKACJA TO WYWOŁANIA API 211

if (folder == null) {
System.err.println("Nie można otworzyć: "
+ defaultFolder);
return;
}
folder.open (Folder.READ_WRITE);
process(host, listAddress, roster, session,
store, folder);
} catch (Exception e) {
System.err.println(e);
System.err.println ("(ponowne sprawdzanie poczty)");
}
System.err.print (".");
try { Thread.sleep (interval * 1000); }
catch (InterruptedException e) {}
} while (true);
}
catch (Exception e) {
e.printStackTrace ();
}
}

private static void process(


HostInformation host, String listAddress, Roster roster,
Session session,Store store, Folder folder)
throws MessagingException {
try {
if (folder.getMessageCount() != 0) {
Message[] messages = folder.getMessages ();
doMessage(host, listAddress, roster, session,
folder, messages);
}
} catch (Exception e) {
System.err.println ("błąd obsługi wiadomości");
e.printStackTrace (System.err);
}
finally {
folder.close (true);
store.close ();
}
}

private static void doMessage(


HostInformation host,
String listAddress,
Roster roster,
Session session,
Folder folder,
Message[] messages) throws
MessagingException, AddressException, IOException,
NoSuchProviderException {
FetchProfile fp = new FetchProfile ();
fp.add (FetchProfile.Item.ENVELOPE);
fp.add (FetchProfile.Item.FLAGS);
fp.add ("X-Mailer");
212 ROZDZIAŁ 15. CAŁA MOJA APLIKACJA TO WYWOŁANIA API

folder.fetch (messages, fp);


for (int i = 0; i < messages.length; i++) {
Message message = messages [i];
if (message.getFlags ().contains (Flags.Flag.DELETED))
continue;
System.out.println("wiadomość otrzymana: "
+ message.getSubject ());
if (!roster.containsOneOf (message.getFrom ()))
continue;
MimeMessage forward = new MimeMessage (session);
InternetAddress result = null;
Address [] fromAddress = message.getFrom ();
if (fromAddress != null && fromAddress.length > 0)
result =
new InternetAddress (fromAddress [0].toString ());
InternetAddress from = result;
forward.setFrom (from);
forward.setReplyTo (new Address [] {
new InternetAddress (listAddress) });
forward.addRecipients (Message.RecipientType.TO,
listAddress);
forward.addRecipients (Message.RecipientType.BCC,
roster.getAddresses ());
String subject = message.getSubject();
if (-1 == message.getSubject().indexOf (SUBJECT_MARKER))
subject = SUBJECT_MARKER + " " + message.getSubject();
forward.setSubject (subject);
forward.setSentDate (message.getSentDate ());
forward.addHeader (LOOP_HEADER, listAddress);
Object content = message.getContent ();
if (content instanceof Multipart)
forward.setContent ((Multipart)content);
else
forward.setText ((String)content);

Properties props = new Properties ();


props.put ("mail.smtp.host", host.smtpHost);

Session smtpSession =
Session.getDefaultInstance (props, null);
Transport transport = smtpSession.getTransport ("smtp");
transport.connect (host.smtpHost,
host.smtpUser, host.smtpPassword);
transport.sendMessage (forward, roster.getAddresses ());
message.setFlag (Flags.Flag.DELETED, true);
}
}
}

Jest to raczej mały fragment kodu, niemniej jest dość nieczytelny. Trudno znaleźć
w nim wiersze, które nie odwołują się do API. Czy kodowi temu można nadać lepszą
strukturę? Czy można mu nadać taką strukturę, aby wprowadzanie zmian było prostsze?
Oczywiście, że tak.
CAŁA MOJA APLIKACJA TO WYWOŁANIA API 213

Pierwszym krokiem będzie zidentyfikowanie obliczeniowego rdzenia kodu. Co tak


naprawdę robi dla nas ten kod?
Pomocna może okazać się próba napisania krótkiej charakterystyki działania kodu:
Powyższy kod wczytuje informacje konfiguracyjne z wiersza poleceń oraz adresy e-mailowe
z pliku. Okresowo sprawdza, czy są nowe wiadomości e-mail. Kiedy pojawi się nowa
wiadomość, przesyła ją na każdy z adresów zapisanych w pliku.
Wygląda na to, że program ten to przede wszystkim operacje wejścia i wyjścia, ale jest
jeszcze coś. W kodzie uruchomiony jest wątek; jest on uśpiony, ale budzi się okresowo,
aby sprawdzić pocztę. Poza tym nie ograniczamy się wyłącznie do ponownego wysyłania
nadchodzącej korespondencji — na podstawie otrzymanych wiadomości tworzymy
nowe. Musimy skonfigurować wszystkie pola wiadomości, a także sprawdzić oraz zmienić
jej temat, aby informował, że e-mail pochodzi z listy dyskusyjnej. Wykonujemy zatem
trochę najprawdziwszej roboty.
Gdybyśmy mieli rozdzielić odpowiedzialności kodu, moglibyśmy otrzymać coś w tym
rodzaju:
1. Potrzebujemy czegoś, co potrafi odbierać nadchodzące wiadomości i zasilać nimi
nasz system.
2. Potrzebujemy czegoś, co tylko rozsyła wiadomości e-mail.
3. Potrzebujemy czegoś, co bazując na wiadomościach nadchodzących, potrafi utwo-
rzyć nową wiadomość na podstawie naszej listy odbiorców.
4. Potrzebujemy czegoś, co przez większość czasu pozostaje w uśpieniu, ale okre-
sowo budzi się i sprawdza pocztę.
Czy teraz, kiedy spoglądamy na powyższe odpowiedzialności, wygląda na to, że niektóre
z nich są bardziej związane z Java Mail API niż inne? Odpowiedzialności nr 1 i 2 zdecy-
dowanie mają związek z API. Odpowiedzialność nr 3 to przypadek nieco bardziej skom-
plikowany. Potrzebne nam klasy wiadomości stanowią część pocztowego API, chociaż
prawdopodobnie moglibyśmy tę odpowiedzialność niezależnie od nich przetestować,
tworząc pozorowane wiadomości nadchodzące. Odpowiedzialność nr 4 tak naprawdę nie
ma nic wspólnego z pocztą; potrzebuje tylko wątku, który jest skonfigurowany do budze-
nia się w określonych odstępach czasu.
Na rysunku 15.1 pokazano projekt bazujący na rozdzieleniu powyższych odpowie-
dzialności.
Klasa ListDriver obsługuje cały system. Znajduje się w niej wątek, który przez więk-
szość czasu pozostaje uśpiony i budzi się okresowo w celu sprawdzenia poczty. Klasa
ta odczytuje wiadomości, nakazując klasie MailReceiver sprawdzenie korespondencji.
MailReceiver odbiera pocztę i przekazuje poszczególne e-maile klasie MailForwarder.
Klasa ta tworzy wiadomości dla każdego z odbiorców z listy i wysyła je za pomocą klasy
MailSender.
214 ROZDZIAŁ 15. CAŁA MOJA APLIKACJA TO WYWOŁANIA API

Rysunek 15.1. Lepszy serwer listy mailingowej

Taki projekt jest całkiem dobry. Interfejsy MessageProcessor i MailService są wy-


godne, ponieważ umożliwiają nam niezależne testowanie klas. W szczególności przydatna
jest możliwość pracowania nad klasą MessageForwarder w jarzmie testowym bez
faktycznego wysyłania wiadomości. Będzie to łatwe do osiągnięcia, jeśli utworzymy
klasę FakeMailSender implementującą interfejs MailService.
Prawie w każdym systemie znajduje się jakaś rdzenna logika, którą można oddzielić
od wywołań API. Chociaż nasz przypadek jest niewielki, tak naprawdę jest gorszy niż
większość rzeczywistych sytuacji. Klasa MessageForwarder to fragment systemu, którego
odpowiedzialność jest najbardziej niezależna w całej mechanice otrzymywania i wysyłania
wiadomości, ale i tak korzysta ona z klas wiadomości pochodzących z API Javy. Nie wygląda
na to, aby znalazło się tu wiele miejsca na stare, zwykłe klasy Javy, niemniej faktoryzacja
systemu na cztery klasy i dwa interfejsy — widoczne na rysunku — umożliwiła nam wy-
dzielenie warstw. Główna logika listy mailingowej znajduje się w klasie MessageForwarder,
którą możemy poddać testom. W kodzie wyjściowym była ona głęboko zagrzebana i nie-
osiągalna. Praktycznie niemożliwe jest rozbicie systemu na mniejsze fragmenty bez uzy-
skania elementów, które znajdują się na „wyższym poziomie” niż inne.
Kiedy mamy do czynienia z systemem, który wydaje się składać wyłącznie z wy-
wołań API, pomocne może być wyobrażenie sobie, że jest on jednym wielkim obiektem,
po czym zastosowanie heurystyki wyodrębniania odpowiedzialności, opisanej w roz-
dziale 20., „Ta klasa jest za duża, a ja nie chcę, żeby stała się jeszcze większa”. Być może
nie będziemy mieć możliwości szybkiego uzyskania lepszego systemu, ale już sama czyn-
ność identyfikowania odpowiedzialności może ułatwić nam podejmowanie lepszych decyzji
podczas prac nad systemem.
CAŁA MOJA APLIKACJA TO WYWOŁANIA API 215

No dobrze, a więc tak wygląda lepszy projekt. Dobrze wiedzieć, że istnieje taka moż-
liwość, ale powróćmy do rzeczywistości. Jak możemy posunąć się do przodu? Zasadniczo
istnieją dwa podejścia:
1. Odzwierciedlenie i opakowanie API.
2. Wyodrębnianie bazujące na odpowiedzialnościach.
Kiedy odzwierciedlamy i opakowujemy API, konstruujemy interfejs odzwierciedlający
API tak dokładnie, jak to jest tylko możliwe, po czym dookoła klas bibliotecznych tworzy-
my opakowania. Aby zminimalizować możliwość popełnienia pomyłek, możemy podczas
naszej pracy zachować sygnatury (314). Jedna z zalet odzwierciedlania i opakowywania
API polega na tym, że w rezultacie możemy pozbyć się zależności w bazowym kodzie
API. Nasze opakowania będą korzystać z prawdziwego API w kodzie produkcyjnym,
a podczas testów będziemy odwoływać się do fałszywek.
Czy z tej samej techniki możemy skorzystać w kodzie obsługującym listę mailingową?
Oto kod serwera listy mailingowej, który wysyła wiadomości e-mail:
...
Session smtpSession = Session.getDefaultInstance (props, null);
Transport transport = smtpSession.getTransport ("smtp");
transport.connect (host.smtpHost, host.smtpUser,
host.smtpPassword);
transport.sendMessage (forward, roster.getAddresses ());
...

Gdybyśmy chcieli usunąć zależności w klasie Transport, moglibyśmy utworzyć dla niej
opakowanie, ale w tym kodzie nie tworzymy obiektu tej klasy; otrzymujemy go z klasy
Session. Czy mamy możliwość utworzenia opakowania dla klasy Session? Niezupełnie
— Session jest klasą finalną, a w Javie nie można tworzyć podklas na podstawie klas final-
nych (wrrrrr).
Kod listy mailingowej jest tak naprawdę kiepskim kandydatem na przeprowadzenie
odzwierciedlenia. API jest dość złożone. Jeśli jednak nie dysponujemy żadnym narzę-
dziem do refaktoryzacji, taki zabieg może być najbezpieczniejszym rozwiązaniem.
Na szczęście w Javie dostępne są narzędzia do refaktoryzacji, w związku z czym może-
my zrobić coś innego, mianowicie przeprowadzić wyodrębnianie bazujące na odpowie-
dzialnościach. W procesie tym identyfikujemy odpowiedzialności w kodzie i na tej pod-
stawie rozpoczynamy wyodrębnianie metod.
Jakie odpowiedzialności istnieją w naszym fragmencie kodu? Cóż, jego ogólnym zada-
niem jest wysyłanie wiadomości. Co jest potrzebne, aby to robić? SMTP oraz aktywne
połączenie. W poniższym kodzie wyodrębniliśmy odpowiedzialność związaną z wysyła-
niem wiadomości, utworzyliśmy z niej metodę i dodaliśmy ją do nowej klasy o nazwie
MailSender.
import javax.mail.*;
import javax.mail.internet.InternetAddress;
import java.util.Properties;
216 ROZDZIAŁ 15. CAŁA MOJA APLIKACJA TO WYWOŁANIA API

public class MailSender


{
private HostInformation host;
private Roster roster;

public MailSender (HostInformation host, Roster roster) {


this.host = host;
this.roster = roster;
}

public void sendMessage (Message message) throws Exception {


Transport transport
= getSMTPSession ().getTransport ("smtp");
transport.connect (host.smtpHost,
host.smtpUser, host.smtpPassword);
transport.sendMessage (message, roster.getAddresses ());
}

private Session getSMTPSession () {


Properties props = new Properties ();
props.put ("mail.smtp.host", host.smtpHost);
return Session.getDefaultInstance (props, null);
}
}

Skąd będziemy wiedzieć, czy wybrać odzwierciedlanie i opakowywanie API, czy też
wyodrębnianie bazujące na odpowiedzialnościach? Oto kilka wskazówek:
Odzwierciedlanie i opakowywanie API sprawdza się w następujących okolicznościach:
 API jest względnie mały.
 Chcesz w całości usunąć zależności w bibliotece pochodzącej od osób trzecich.
 Nie dysponujesz testami i nie możesz ich napisać, ponieważ testowanie za po-
średnictwem API jest niemożliwe.
Kiedy odzwierciedlamy i opakowujemy API, mamy szanse, aby poddać testom cały
nasz kod z wyjątkiem cienkiej warstwy delegującej zadania z opakowania do rzeczywistych
klas API.
Wyodrębnianie bazujące na odpowiedzialnościach sprawdza się w następujących
okolicznościach:
 API jest bardziej złożony.
 Masz do dyspozycji narzędzie realizujące bezpieczną metodę wyodrębniania lub
jesteś pewien, że możesz w bezpieczny sposób wyodrębnić klasy ręcznie.
Porównanie wad i zalet obu tych technik jest nieco trudne. Z odzwierciedlaniem
i opakowywaniem API wiąże się więcej pracy, ale technika ta może być przydatna, kiedy
chcesz odizolować się od bibliotek pochodzących od osób trzecich, co zdarza się dość
często. Więcej szczegółów znajdziesz w rozdziale 14., „Dobijają mnie zależności biblio-
teczne”. Gdy korzystamy z wyodrębniania bazującego na odpowiedzialnościach, może-
my w rezultacie otrzymać nieco naszej logiki z kodem API, dzięki czemu będziemy mogli
CAŁA MOJA APLIKACJA TO WYWOŁANIA API 217

wyodrębnić metodę z wyższego poziomu. Gdy tak postąpimy, nasz kod będzie mógł zale-
żeć od wysokopoziomowych interfejsów zamiast od niskopoziomowych wywołań API,
chociaż być może nie uda nam się poddać wyodrębnionego kodu testom.
Wiele zespołów korzysta z obu tych technik — cienkiego opakowania na potrzeby te-
stów oraz wysokopoziomowego opakowania w celu zapewnienia w aplikacji lepszego
interfejsu.
218 ROZDZIAŁ 15. CAŁA MOJA APLIKACJA TO WYWOŁANIA API
Rozdział 16.

Nie rozumiem
wystarczająco dobrze kodu,
żeby go zmienić

Wkroczenie w nieznany kod, zwłaszcza kod obcy, może budzić grozę. Jednak wraz z upły-
wem czasu niektórzy mogą do pewnego stopnia uodpornić się na strach. Wciąż na nowo
stając twarzą w twarz z potworami czającymi się w kodzie i pokonując je, osoby takie wy-
kształcają w sobie pewność, chociaż trudno się nie bać. Każdy od czasu do czasu napotyka
demony, których nie może pokonać. Jeśli zaczniesz o tym rozmyślać, zanim nawet zaj-
rzysz do kodu, pogorszysz tylko swoją sytuację. Nigdy nie wiesz, czy zmiana w kodzie
będzie szybka, czy też przeobrazi się w trwające tydzień doświadczenie, przy którym
będziesz rwać sobie włosy z głowy, przeklinać system, swoją sytuację i prawie wszyst-
ko dookoła siebie. Gdybyśmy tylko rozumieli każdą rzecz potrzebną do wprowadzenia
naszych zmian, wszystko poszłoby gładko. Jak możemy pozyskać taką wiedzę?
Oto typowa sytuacja. Dowiadujesz się o nowej funkcjonalności, którą musisz dodać
do systemu. Siadasz i zaczynasz przeglądać kod. Czasami szybko możesz dowiedzieć się
wszystkiego, co jest Ci potrzebne, chociaż w przypadku cudzego kodu może to chwilę
potrwać. Przez cały ten czas sporządzasz w myślach spis rzeczy, które musisz zrobić,
rezygnując z jednych rozwiązań na rzecz innych. W pewnym momencie możesz poczuć,
że poczyniłeś postępy i że masz wystarczającą pewność siebie, by przystąpić do pracy.
W innych przypadkach zaczyna Ci się kręcić w głowie od wszystkich tych elementów,
które próbujesz sobie przyswoić. Czytanie kodu zdaje się nie pomagać i rozpoczynasz
swoją pracę od tego, co potrafisz zrobić, mając nadzieję, że będzie lepiej.
Istnieją inne sposoby na pozyskiwanie wiedzy, ale wiele osób z nich nie korzysta,
ponieważ są zbyt zajęte, starając się rozgryźć kod najszybciej, jak tylko jest to możliwe.
W końcu poświęcanie czasu na próby zrozumienia czegoś jest podejrzanie podobne do
nicnierobienia. Jeśli bardzo szybko uda im się przedrzeć przez proces zrozumienia kodu,
220 ROZDZIAŁ 16. NIE ROZUMIEM WYSTARCZAJĄCO DOBRZE KODU, ŻEBY GO ZMIENIĆ

będą mogły zacząć pracować na swoje pensje. Czyż to nie brzmi niedorzecznie? Dla mnie
tak, ale wielu postępuje w taki właśnie sposób, co niestety jest godne pożałowania, gdyż
po wykonaniu kilku bardzo nietechnicznych czynności możemy rozpocząć pracę na
solidniejszym gruncie.

Notatki i rysunki
Kiedy czytanie kodu sprawia, że stajesz się coraz bardziej zagubiony, opłaca się sporządzać
rysunki i notatki. Zapisz nazwę ostatniego ważnego elementu, jaki zauważyłeś, po czym
zanotuj nazwę kolejnego elementu. Jeśli widzisz między nimi związek, połącz je linią.
Takie rysunki nie muszą być w pełni rozwiniętymi schematami UML ani grafami obrazu-
jącymi wywoływanie metod, sporządzonymi z zachowaniem specjalnej notacji. Jeśli jednak
sprawy zaczną się mocniej komplikować, będziesz mógł sięgnąć po bardziej sformalizowany
lub usystematyzowany sposób organizowania swoich przemyśleń. Rysunki często poma-
gają dostrzec różne rzeczy w nowym świetle; są także doskonałym sposobem na zachowa-
nie świeżości umysłu, gdy próbujemy zrozumieć coś naprawdę skomplikowanego.
Na rysunku 16.1 odtworzyłem szkic, który pewnego dnia wykonałem wspólnie z innym
programistą, gdy razem przeglądaliśmy kod. Narysowaliśmy go na odwrocie innej notatki
(pozmieniałem nazwy na schemacie, aby chronić niewinne osoby).

Rysunek 16.1. Szkic

Teraz ten szkic nie jest szczególnie czytelny, ale pomógł nam w czasie tamtej rozmowy.
Trochę wówczas zrozumieliśmy i przyjęliśmy właściwe podejście do naszego zadania.
Czy nie wszyscy tak postępują? No cóż — i tak, i nie. Niewiele osób robi to regularnie.
Podejrzewam, że dzieje się tak dlatego, iż nie ma żadnego poradnika, jak się do tego za-
brać. Kiedy tylko sięgamy po długopis, odczuwamy pokusę pisania fragmentów kodu
albo stosowania składni UML. UML jest niezły, ale tak samo sprawdzają się dymki, kreski
i figury, które będą nieczytelne dla każdej osoby, która była nieobecna, gdy je rysowaliśmy.
ADNOTOWANIE LISTINGÓW 221

Na papierze nie musimy zachowywać dokładności. Papier to tylko narzędzie ułatwiające


rozmowę i pomagające nam zapamiętać pomysły, które omawialiśmy i poznawaliśmy.
Naprawdę wspaniałą cechą szkicowania fragmentów projektu, która objawia się, gdy
próbujesz je zrozumieć, jest jej nieformalność i zaraźliwość. Jeśli stwierdzisz, że technika ta
jest przydatna, nie będziesz musiał nakłaniać swojego zespołu, aby ją przyjął jako część
procesu programowania. Oto co musisz zrobić: poczekaj, aż zaczniesz pracować z kimś,
kto próbuje zrozumieć jakiś kod, po czym w czasie udzielania swoich wyjaśnień wykonaj
niewielki rysunek tego, co właśnie widzicie. Jeśli Twój kolega także jest zaangażowany
w zrozumienie tej części systemu, to podczas rozgryzania kodu co chwilę będziecie powra-
cać wzrokiem na rysunek.
Kiedy już przystąpisz do wykonywania lokalnych szkiców systemu, często będzie się
u Ciebie pojawiać pokusa poświęcenia czasu na zrozumienie jego całości. Zajrzyj do roz-
działu 17., „Moja aplikacja nie ma struktury”, gdzie znajdziesz zbiór technik, które
ułatwią Ci orientowanie się w dużych bazach kodu oraz zajmowanie się nimi.

Adnotowanie listingów
Sporządzanie szkiców nie jest jedynym sposobem, który pomaga w rozumieniu kodu.
Kolejną techniką, z której często korzystam, jest adnotowanie listingów. Jest ona szcze-
gólnie przydatna w odniesieniu do długich metod. Sam pomysł jest prosty i prawie każdy
korzystał z niego przy jakiejś okazji, chociaż — jeśli mam być szczery — uważam, że jest
on zbyt rzadko stosowany.
Sposób umieszczania adnotacji w listingach zależy od tego, co chcesz zrozumieć.
Pierwszy krok polega na wydrukowaniu kodu, nad którym będziesz pracować. Kiedy
masz już wydruk, będziesz mógł skorzystać z techniki adnotowania listingów podczas
czynności opisanych poniżej.

Wyodrębnianie odpowiedzialności
Jeśli chcesz wyodrębnić odpowiedzialności, użyj markera do pogrupowania poszczegól-
nych elementów, które wchodzą w ich skład. Jeśli wiele elementów należy wspólnie do
danej odpowiedzialności, umieść obok każdego z nich symbol, dzięki czemu będziesz
mógł je zidentyfikować. Jeżeli możesz, użyj kilku kolorów.

Zrozumienie struktury metody


Jeśli chcesz zrozumieć dużą metodę, obrysuj jej bloki. Często wcięcia w długiej meto-
dzie mogą uniemożliwić jej czytanie. Możesz obrysować bloki takiej metody, kreśląc linię
od początku bloku aż do jego końca albo umieszczając na końcach bloków komentarze
z pętlą lub warunkiem, które je otwierają.
222 ROZDZIAŁ 16. NIE ROZUMIEM WYSTARCZAJĄCO DOBRZE KODU, ŻEBY GO ZMIENIĆ

Najprostszy sposób obrysowywania bloków polega na pracy od wnętrza w kierunku


na zewnątrz. Jeśli na przykład pracujesz w jednym z języków z rodziny C, zacznij czytanie
kodu od góry listingu, pomiń wszystkie nawiasy klamrowe otwierające i dojdź do pierw-
szego nawiasu klamrowego zamykającego. Zaznacz go, po czym cofnij się i oznacz od-
powiadający mu nawias klamrowy otwierający. Kontynuuj czytanie kodu, aż natrafisz na
kolejny nawias zamykający, i ponownie zrób to samo; spójrz powyżej i odszukaj odpowia-
dający mu otwierający nawias klamrowy.

Wyodrębnianie metod
Jeżeli chcesz podzielić dużą metodę, obrysuj kod, który chcesz wyodrębnić, i odnotuj obok
jej liczbę powiązań (patrz rozdział 22., „Muszę zmienić monstrualną metodę, lecz nie
mogę napisać do niej testów”).

Zrozumienie skutków zmiany


Jeśli chcesz zrozumieć następstwa zmiany, jaką masz zamiar wprowadzić, zamiast rysować
schemat skutków (167), oznacz wiersze kodu, które chcesz zmodyfikować. Następnie
zaznacz każdą zmienną, której wartość może ulec zmianie w rezultacie tej modyfikacji,
oraz wszystkie wywołania metod, które mogą się zmienić. W dalszej kolejności zaznacz
wszystkie zmienne i metody, na jakie mają wpływ elementy, które przed chwilą oznaczyłeś.
Powtórz te czynności tyle razy, ile będzie potrzebne, aby ustalić, jak rozprzestrzeniają się
skutki wprowadzonej zmiany. Kiedy tak postąpisz, uzyskasz lepsze pojęcie na temat
elementów, które powinieneś poddać testom.

Szybka refaktoryzacja
Jedną z najlepszych technik umożliwiających poznawanie kodu jest refaktoryzacja. Po
prostu zabierz się do niej, rozpocznij przesuwanie elementów w obrębie kodu i spraw, że
będzie on czytelniejszy. Jedyny problem polega na tym, że jeśli nie masz do dyspozycji
testów, zajęcie to może być dość ryzykowne. Skąd będziesz wiedzieć, że czegoś nie psu-
jesz, kiedy w celu zrozumienia kodu przeprowadzasz tę całą refaktoryzację. Prawda jest
taka, że możesz pracować tak, jak chcesz, i niczym się nie przejmować — jest to całkiem
łatwe. Zawsze możesz sprawdzić kod w swoim systemie kontroli wersji. Zapomnij o pisaniu
testów. Wyodrębniaj metody, przesuwaj zmienne, refaktoruj w taki sposób, jaki jest Ci
potrzebny do lepszego zrozumienia kodu — tylko go nie pozostawiaj. Wyrzuć taki kod.
Właśnie na tym polega szybka refaktoryzacja.
Kiedy po raz pierwszy wspomniałem o szybkiej refaktoryzacji koledze, z którym
pracowałem, pomyślał on, że to strata czasu, ale w ciągu pół godziny przestawiania róż-
nych rzeczy w programie, nad którym pracowaliśmy, dowiedzieliśmy się bardzo dużo
o jego kodzie. Po tym doświadczeniu kolega zaakceptował tę metodę.
USUWANIE NIEUŻYWANEGO KODU 223

Szybka refaktoryzacja znakomicie sprawdza się przy docieraniu do istoty rzeczy i do-
wiadywaniu się, jak działa pewien fragment kodu, ale wiąże się z nią kilka zagrożeń. Pierwsze
z nich polega na tym, że możemy popełnić ogromny błąd, gdy dokonamy refaktoryzacji
prowadzącej nas do założenia, że system robi coś, czego tak naprawdę nie robi. Gdy tak
się stanie, będziemy mieć fałszywe wyobrażenie na temat systemu, co może wzbudzić
w nas niepokój, gdy przystąpimy do rzeczywistej refaktoryzacji. Drugie ryzyko wiąże się
z pierwszym. Możemy tak bardzo przyzwyczaić się do nowego kodu, że przez cały czas
będziemy myśleć w jego kategoriach. Nie wydaje się, aby mogło być w tym coś złego, ale
może okazać się, że istotnie tak jest. Istnieje wiele powodów, dla których możemy uzyskać
inną strukturę kodu, kiedy już przystąpimy do prawdziwej refaktoryzacji. Być może
dostrzeżemy lepszy sposób na ustrukturyzowanie kodu. Od chwili przeprowadzenia
pierwszej refaktoryzacji nasz kod może ulec zmianom i możemy mieć inne przemyślenia.
Jeśli będziemy zbyt przywiązani do ostatecznego efektu szybkiej refaktoryzacji, prze-
oczymy te inne rozwiązania.
Szybka refaktoryzacja to dobry sposób na sprawdzenie, czy zrozumiałeś najważniejsze
rzeczy dotyczące kodu, co samo w sobie może ułatwić Ci pracę. Uzyskasz pewność, że za
żadnym rogiem nie czai się nic strasznego — a jeśli nawet się czai, zostaniesz ostrzeżony,
zanim udasz się w to miejsce.

Usuwanie nieużywanego kodu


Jeśli kod, na który spoglądasz, jest zagmatwany, a potrafisz stwierdzić, że jakiś jego frag-
ment nie jest używany, usuń go. Fragment ten nie robi nic, z wyjątkiem tego, że stoi Ci na
drodze.
Czasami niektórzy mają poczucie, że usuwanie kodu to marnotrawstwo. W końcu ktoś
poświęcił czas na jego napisanie i być może da się z niego skorzystać w przyszłości. No cóż
— od tego są systemy kontroli wersji. Kod ten będzie obecny we wcześniejszych wersjach
aplikacji. Zawsze będziesz mógł go tam znaleźć, gdy zdecydujesz, że jest Ci potrzebny.
224 ROZDZIAŁ 16. NIE ROZUMIEM WYSTARCZAJĄCO DOBRZE KODU, ŻEBY GO ZMIENIĆ
Rozdział 17.

Moja aplikacja
nie ma struktury

Długo istniejące aplikacje mają tendencję do przeradzania się w chaos. Być może rozpo-
czynały z dobrze przemyślaną architekturą, ale wraz z upływem lat i pod presją terminów,
dotarły do punktu, w którym nikt tak naprawdę nie rozumie ich pełnej struktury. Pro-
gramiści latami mogą pracować nad projektem i nie mieć pojęcia, gdzie mają być do-
dawane nowe funkcjonalności; znają tylko obejścia, które ostatnio powstały w systemie.
Kiedy dodają nowe funkcjonalności, udają się do tych „obejść”, ponieważ miejsca te
znają najlepiej.
Nie ma łatwego sposobu zapobiegania takim sytuacjom, a ich powaga może być różna.
W niektórych przypadkach programiści nie mają wyjścia. Dodawanie nowych funkcjo-
nalności jest trudne, co powoduje „przełączenie” całej organizacji w tryb kryzysowy.
Pracowników obarcza się zadaniem sprawdzenia, czy lepszym rozwiązaniem nie będzie
zmiana architektury lub przepisanie systemu. W innych organizacjach system kuleje przez
całe lata. To fakt, dodawanie funkcjonalności pochłania więcej czasu, niż powinno, ale
przyjęto, że taka jest cena prowadzenia działalności. Nikt nie wie, czy mogłoby być lepiej
ani ile pieniędzy jest traconych z powodu złej struktury systemu.
Kiedy zespoły programistów nie znają architektury swoich systemów, zaczyna się ich
degradacja. Co stoi na przeszkodzie w zdobyciu tej wiedzy?
 System może być złożony do tego stopnia, że uzyskanie obrazu całości zabiera dużo
czasu.
 System może być złożony do tego stopnia, że obraz całości nie istnieje.
 Zespół może pracować w trybie szybkiego reagowania, zajmując się jedną pilną
sytuacją po drugiej do tego stopnia, że zupełnie utracił obraz całości.
W celu rozwiązania tego typu problemów wiele organizacji tradycyjnie korzystało
z roli architekta. Architektom powierzano zwykle zadanie wypracowania obrazu całości
i podjęcia takich decyzji, które umożliwiłyby przedstawienie go zespołom. Takie podejście
226 ROZDZIAŁ 17. MOJA APLIKACJA NIE MA STRUKTURY

może się sprawdzić, ale pod pewnym warunkiem. Architekt musi być częścią zespołu
i dzień w dzień pracować z jego członkami — w przeciwnym razie kod zacznie odbiegać
od obrazu całości. Może się to dziać na dwa różne sposoby: ktoś robi coś niewłaściwego
w kodzie albo obraz całości powinien ulec zmianie. W jednej z najgorszych sytuacji, z jaką
miałem do czynienia w zespole, architekt grupy miał zupełnie inną wizję systemu niż pro-
gramiści. Często jest tak dlatego, że architekt ma inny zakres obowiązków i nie może inge-
rować w kod albo komunikować się z zespołem wystarczająco często, aby tak naprawdę
wiedzieć, co się tam dzieje. W rezultacie komunikacja załamuje się w całej organizacji.
Brutalna prawda jest taka, że architektura jest zbyt ważna, aby pozostawić ją w gestii
zaledwie paru osób. Dobrze jest mieć architekta, ale najlepszym sposobem na zachowanie
nienaruszonej architektury jest zagwarantowanie, że każdy w zespole ją zna i ma w niej
udział. Każda osoba, która pracuje z kodem, powinna znać architekturę, a wszyscy inni,
którzy mają do czynienia z kodem, powinni odnosić korzyści z wiedzy tej osoby. Kiedy
każdy korzysta z przewagi, jaką daje dostęp do tych samych idei, ogólna wiedza zespołu
na temat systemu ulega wzmocnieniu. Jeśli masz, dajmy na to, zespół 20 osób, w którym
tylko 3 osoby szczegółowo znają architekturę, to albo te 3 osoby będą mieć sporo pracy,
aby pozostałą siedemnastkę utrzymać na właściwym torze, albo te 17 osób będzie po
prostu popełniać błędy spowodowane nieznajomością obrazu całości.
W jaki sposób możemy uzyskać obraz całości w przypadku dużego systemu? Istnieje
wiele możliwości. Książka Object-Oriented Reengineering Patterns, napisana przez Serge’a
Demeyera, Stephane’a Ducasse’a i Oscara M. Nierstrasza (Morgan Kaufmann Publishers
2002), zawiera katalog technik poświęconych wyłącznie temu zagadnieniu. Tutaj opiszę
kilka innych sposobów, które są dość skuteczne. Jeśli będziesz je często praktykował
w zespole, pomogą Ci one utrzymać w nim zainteresowanie architekturą, co prawdopo-
dobnie jest najważniejszą rzeczą, jaką możesz zrobić w celu jej zachowania. Trudno jest
zwracać uwagę na coś, o czym zbyt często nie myślisz.

Opowiadanie historii systemu


Kiedy pracuję z zespołami, często posługuję się techniką, którą nazwałem „opowiada-
niem historii systemu”. Aby była ona skuteczna, potrzebne są przynajmniej dwie osoby.
Jedna z nich zadaje pytanie drugiej: „Jaka jest architektura tego systemu?”. Wtedy druga
osoba próbuje wyjaśnić architekturę systemu, korzystając zaledwie z kilku pojęć — być
może dwóch albo trzech. Jeśli jesteś osobą, która wyjaśnia, musisz udawać, że ta druga
osoba nic nie wie na temat systemu. W zaledwie kilku zdaniach powinieneś wyjaśnić,
jakie są elementy projektu i jak one ze sobą współpracują. W zdaniach tych wyrazisz to,
co według Ciebie stanowi najważniejsze elementy systemu. Teraz wybierz kolejne pod
względem istotności rzeczy, które go dotyczą. Kontynuuj swoją wypowiedź, aż przeka-
żesz wszystkie ważne informacje dotyczące istoty projektu systemu.
OPOWIADANIE HISTORII SYSTEMU 227

Kiedy rozpoczniesz, zauważysz, jak nachodzi Cię dziwne uczucie. Aby rzeczywiście
opowiedzieć o architekturze systemu w tak krótkich słowach, będziesz musiał upraszczać.
Możesz na przykład powiedzieć: „Brama sieciowa otrzymuje zestawy reguł z aktywnej
bazy danych”, ale gdy będziesz wypowiadać te słowa, jakaś część Ciebie może zakrzyknąć:
„Nie! Brama sieciowa otrzymuje zestawy reguł z aktywnej bazy danych, ale też z bieżącego
zbioru roboczego”. Gdy opowiadasz o czymś w uproszczeniu, w pewnym sensie czujesz
się, jakbyś kłamał; po prostu nie mówisz całej prawdy. Przekazujesz jednak prostszą
historię, która opisuje łatwiejszą do zrozumienia architekturę. Dlaczego na przykład
brama sieciowa musi otrzymywać zestawy reguł z wielu miejsc zamiast z tylko jednego?
Czy nie byłoby prościej, gdyby ten proces zunifikować?
Względy natury praktycznej często powstrzymują nas od upraszczania spraw, ale
istnieje pewna wartość w przekazywaniu prostego obrazu. Pomaga on wszystkim zro-
zumieć, jaki byłby ideał i jakie elementy znajdują się już na swoich miejscach. Inny ważny
aspekt tej techniki polega na tym, że tak naprawdę zmusza ona do zastanowienia się, co
jest ważne w systemie. Jakie są najważniejsze rzeczy do zakomunikowania?
Zespół programistyczny może dotrzeć tylko do tego miejsca, jeśli system, nad którym
pracuje, stanowi dla niego tajemnicę. Może to wydawać się dziwne, ale dysponowanie
uproszczonym opisem działania systemu jest jak drogowskaz — pomaga Ci ustalić pozycję,
gdy poszukujesz właściwych miejsc na dodanie nowych funkcjonalności. Sprawia też, że
system staje się o wiele mniej przerażający.
W swoim zespole często opowiadaj historię systemu po to, aby mieć wspólne poglądy.
Opowiadaj ją na różne sposoby. Idź na kompromis, kiedy jedna z koncepcji staje się
ważniejsza niż inna. Kiedy weźmiesz pod uwagę wprowadzenie zmian w systemie, zauwa-
żysz, że niektóre z nich lepiej wpasowują się w historię systemu — to znaczy, że dzięki nim
krótka historia nie przywodzi już tak bardzo na myśl kłamstwa. Jeśli musisz wybrać między
dwoma sposobami zrobienia czegoś, opowiedzenie historii może się okazać dobrą metodą
stwierdzenia, który sposób doprowadzi do uzyskania łatwiejszego do zrozumienia systemu.
Oto przykład praktycznego zastosowania techniki opowiadania historii systemu — sesja
omawiająca platformę JUnit. Zakładam, że masz trochę wiedzy na temat architektury
JUnit. Jeśli nie, poświęć chwilę, aby zajrzeć do jej kodu źródłowego, który możesz pobrać
pod adresem www.junit.org.
Jaka jest architektura JUnit?
JUnit składa się z dwóch podstawowych klas. Pierwsza nosi nazwę Test, a druga
TestResult. Użytkownicy tworzą testy i uruchamiają je, przekazując im TestResult.
Kiedy test kończy się porażką, informuje o tym TestResult. Użytkownicy mogą pytać
TestResult o wszystkie niepowodzenia, które miały miejsce.
Wymieńmy uproszczenia:
1. W JUnit istnieje wiele innych klas. Stwierdziłem, że podstawowymi klasami są
Test i TestResult, ponieważ tak sądzę. Według mnie interakcja między nimi jest
podstawową interakcją zachodzącą w systemie. Inne osoby mogą mieć inny, równie
uzasadniony pogląd na jego architekturę.
228 ROZDZIAŁ 17. MOJA APLIKACJA NIE MA STRUKTURY

2. Użytkownicy nie tworzą obiektów testowych. Są one tworzone na podstawie klas


przypadków testowych za pomocą mechanizmu refleksji.
3. Test nie jest klasą; jest interfejsem. Testy uruchamiane w JUnit są zwykle pisane
w podklasach klasy TestCase, która implementuje interfejs Taste.
4. Użytkownicy zazwyczaj nie pytają obiektów klasy TestResult o porażki. Obiekty
te rejestrują obiekty nasłuchujące, które są powiadamiane za każdym razem, gdy
obiekt klasy TestResult otrzyma informację pochodzącą od testu.
5. Testy raportują nie tylko porażki. Informują także o liczbie przebiegów testu oraz
o liczbie błędów (błędy są problemami, które wystąpiły podczas testu, a które nie są
jawnie obserwowane; porażki są niepowodzeniami testów).
Czy te uproszczenia dają nam jakieś pojęcie na temat możliwości uproszczenia plat-
formy JUnit? W pewnym stopniu tak. W niektórych prostszych platformach testowych
xUnit przekształcono interfejs Test w klasę i zrezygnowano z klasy TestCase. W innych
platformach scalono błędy i porażki, dzięki czemu są one raportowane w taki sam sposób.
Powróćmy jednak do naszej opowieści.
Czy to już wszystko?
Nie. Testy można pogrupować w obiekty zwane zestawami. Taki zestaw możemy uru-
chomić w TestResult tak samo jak pojedynczy test. Każdy z testów znajdujących się
w zestawie działa oraz informuje TestResult, kiedy zakończy się porażką.
Z jakimi uproszczeniami mamy tu do czynienia?
1. Obiekty klasy TestSuite nie tylko przechowują i uruchamiają zestawy testów.
Tworzą też instancje klas pochodnych klasy TestCase, korzystając z mechanizmu
refleksji.
2. Jest jeszcze jedno uproszczenie; pewnego rodzaju pozostałość po uproszczeniu
pierwszym. Testy tak naprawdę nie uruchamiają się same. Przekazują się one do
klasy TestResult, która z kolei wywołuje dla testów metodę je uruchamiającą. Jest
to proces raczej niskopoziomowy. Myślenie o nim w uproszczeniu jest w pewnym
sensie wygodne. Po części to kłamstwo, ale właśnie tak działał jUnit, kiedy był
trochę prostszy.
Czy to już wszystko?
Nie. Tak naprawdę Test jest interfejsem. Istnieje klasa o nazwie TestCase, która
implementuje ten interfejs. Użytkownicy tworzą podklasę klasy TestCase, po czym
piszą swoje testy w postaci publicznych metod zadeklarowanych jako void, które
w swoich podklasach zaczynają się od słowa test. Klasa TestSuite korzysta z me-
chanizmu refleksji w celu utworzenia grupy testów, które można wywołać poje-
dynczym wywołaniem jej metody run.
Moglibyśmy w ten sposób kontynuować, ale to, co pokazałem do tej pory, daje pewne
wyobrażenie o stosowaniu tej techniki. Zaczynamy od sporządzenia krótkiego opisu
systemu. Kiedy w celu jego utworzenia dokonujemy uproszczeń i pozbywamy się szcze-
OPOWIADANIE HISTORII SYSTEMU 229

gółów, tworzymy tak naprawdę abstrakcję. Gdy zmuszamy się do zaprezentowania uprosz-
czonego obrazu systemu, często możemy znaleźć nowe abstrakcje.
Czy system jest zły, jeśli nie jest w takim stopniu prosty, jak najprostsza historia, którą
możemy o nim opowiedzieć? Otóż nie. Nieodmiennie wraz z rozrastaniem się systemów
stają się one coraz bardziej złożone, o czym informuje nas ich historia.
Załóżmy, że musimy dodać do JUnit nową funkcjonalność. Chcielibyśmy wygenerować
raport na temat wszystkich testów, które nie wywołały żadnych asercji, kiedy je urucho-
miliśmy. Jakie mamy możliwości, biorąc pod uwagę to, co zostało wcześniej opisane?
Jedną z opcji jest dodanie do klasy TestCase metody o nazwie buildUsageReport,
która uruchamia każdą metodę, po czym tworzy raport zawierający wszystkie metody,
które nie wywołały metody assert. Czy taki sposób na dodanie tej funkcjonalności bę-
dzie dobry? Jak wpłynie on na naszą historię? Cóż, wybór tego sposobu spowoduje dodanie
do naszego najkrótszego opisu systemu kolejnego „kłamstwa przemilczenia”:
JUnit składa się z dwóch podstawowych klas. Pierwsza nosi nazwę Test, a druga
TestResult. Użytkownicy tworzą testy i uruchamiają je, przekazując im TestResult.
Kiedy test kończy się porażką, informuje o tym TestResult. Użytkownicy mogą pytać
TestResult o wszystkie niepowodzenia, które miały miejsce.

Wygląda na to, że teraz obiekty klasy Test mają zupełnie nową odpowiedzialność,
generowanie raportów, o którym nie wspominamy w opisie.
A gdybyśmy tak inaczej dodali nową funkcjonalność? Moglibyśmy zmienić interakcje
zachodzące między klasami TestCase i TestResult w taki sposób, aby klasa TestResult
otrzymywała łączną liczbę wywołań asercji podczas każdego uruchomienia testu.
Wtedy moglibyśmy skonstruować klasę tworzącą raporty i zarejestrować ją w TestResult
jako obiekt nasłuchujący. W jaki sposób wpłynie to na historię naszego systemu? Przyjęcie
takiego rozwiązania może być dobrym powodem do odrobiny generalizacji. Obiekty klasy
Test nie tylko informują instancje klasy TestResult o liczbie porażek, ale także o liczbie
błędów, liczbie przebiegów testowych i liczbie wywołań asercji. Naszą krótką historię
moglibyśmy zmienić następująco:
JUnit składa się z dwóch podstawowych klas. Pierwsza nosi nazwę Test, a druga
TestResult. Użytkownicy tworzą testy i uruchamiają je, przekazując im TestResult.
Kiedy test trwa, przekazuje on informacje o swoim przebiegu do TestResult. Użyt-
kownicy mogą następnie pobierać z TestResult informacje o wszystkich przebiegach
testowych.
Czy teraz jest lepiej? Szczerze mówiąc, wolałem pierwszą wersję, w której jest mowa
o niepowodzeniach. Moim zdaniem opisuje ona podstawowe zachowania JUnit. Jeśli
zmienimy kod, dzięki czemu obiekty klasy TestResult będą rejestrować liczbę wywo-
łań asercji, nadal będziemy trochę kłamać, chociaż i tak ukrywamy już inne informacje,
które są przesyłane z obiektów klasy Test to obiektów TestResult. Alternatywne rozwią-
zanie, polegające na obarczeniu klasy TestCase odpowiedzialnością za uruchamianie
zestawu testów i tworzenie na ich podstawie raportów, byłoby przyczyną jeszcze większego
230 ROZDZIAŁ 17. MOJA APLIKACJA NIE MA STRUKTURY

kłamstwa, gdyż w ogóle nie wspominamy w historii o tej dodatkowej odpowiedzialności.


Lepiej, jeśli testy będą raportować liczbę wywołań asercji podczas swojego przebiegu. Na-
sza pierwsza historia jest nieco bardziej ogólna, ale przynajmniej w większej części po-
zostaje prawdziwa, co oznacza, że wprowadzane przez nas zmiany są zgodne z archi-
tekturą systemu.

Puste karty CRC


We wczesnych latach zorientowania obiektowego wiele osób zmagało się z zagadnieniami
związanymi z projektowaniem. Przyzwyczajenie się do zorientowania obiektowego może
być trudne, gdy większość doświadczeń programistycznych nabyło się podczas używania
języków proceduralnych. Mówiąc wprost, sposób myślenia o kodzie jest inny. Pamiętam,
jak po raz pierwszy ktoś próbował pokazać mi na kartce papieru projekt zorientowany
obiektowo. Patrzyłem na wszystkie te figury oraz linie i słuchałem wyjaśnień, ale przez
cały czas chciałem zapytać: „Gdzie jest main()? Gdzie znajduje się punkt wejściowy dla
każdego z tych obiektów?”. Przez chwilę byłem zdezorientowany, ale później zaskoczyłem.
Jednak nie tylko ja miałem takie problemy. Wygląda na to, że większość branży zmagała
się mniej więcej w tym samym czasie z tymi samymi zagadnieniami. Szczerze mówiąc,
każdego dnia nowe osoby w branży muszą stawić czoła tym problemom, gdy po raz
pierwszy mają do czynienia z kodem zorientowanym obiektowo.
W latach osiemdziesiątych XX wieku Ward Cunningham i Kent Beck zmagali się
z tymi właśnie kwestiami. Próbowali pomóc innym osobom zacząć myśleć o projekcie
w kategoriach obiektów. Ward korzystał wówczas z narzędzia o nazwie Hypercard, które
umożliwiało tworzenie na ekranie komputera kart oraz łączy między nimi. Nagle poja-
wiło się olśnienie. Dlaczego by nie wykorzystać prawdziwych kart indeksowych w celu
reprezentowania klas? Dzięki temu klasy stałyby się namacalne i łatwiej byłoby o nich
dyskutować. Mamy porozmawiać o klasie Transaction? Proszę bardzo, oto karta — są
na niej wszystkie odpowiedzialności klasy oraz jej współpracowników.
Skrót CRC pochodzi od słów Class, Responsibility i Collaborations (klasa, odpowie-
dzialność, współpraca). Na każdą kartę CRC nanosisz nazwę klasy, jej odpowiedzial-
ności oraz listę współpracowników (innych klas, z którymi dana klasa się komunikuje).
Jeśli sądzisz, że określona odpowiedzialność nie należy do danej klasy, wykreślasz ją
i zapisujesz na innej karcie lub tworzysz dla niej nową kartę.
Chociaż karty CRC były przez pewien czas dość popularne, zostały ostatecznie wyparte
przez diagramy. Prawie każda osoba nauczająca zorientowania obiektowego miała własną
notację na oznaczenie klas oraz relacji. W końcu poczyniono ogromny wysiłek w celu
scalenia tych różnych notacji. W wyniku powstał UML, a wiele osób zaczęło myśleć, że
położył on kres dyskusjom na temat metod projektowania systemów. Sądziły one, że to
notacja jest metodą; że UML jest sposobem projektowania systemów; że najpierw należy
narysować mnóstwo diagramów, a potem napisać kod. Minęło trochę czasu, zanim ludzie
PUSTE KARTY CRC 231

zdali sobie sprawę z faktu, że chociaż UML zapewnia dobrą notację, która jest przydatna
podczas dokumentowania systemów, to nie stanowi on jedynego sposobu na pracę z po-
mysłami, jakie wykorzystujemy w trakcie tworzenia nowych systemów. Teraz wiem, że
istnieje o wiele lepszy sposób informowania zespołu o projekcie. Jest to technika, którą
kilku moich kolegów od testów nazwało pustymi kartami CRC, ponieważ przypomina
ona korzystanie z kart CRC, z tym że nic się na nich nie pisze. Niestety, opisanie tej
techniki w książce nie jest łatwe. Niemniej postaram się to zrobić jak najlepiej.
Kilka lat temu spotkałem na konferencji Rona Jeffriesa. Obiecał, że pokaże mi, jak
można wytłumaczyć architekturę systemu za pomocą kart w taki sposób, który gwarantuje,
że wzajemne interakcje w systemie stają się wyraźne i zapadają w pamięć. Jak powiedział,
tak zrobił. Oto na czym polega ta metoda. Osoba opisująca system korzysta z kilku pu-
stych kart indeksowych i kładzie je na stole jedną po drugiej. Karty można przesuwać,
wskazywać je albo robić z nimi wszystko, co jest potrzebne do opisania typowych obiek-
tów istniejących w systemie oraz interakcji, jakie między nimi zachodzą.
Oto przykład — opis działającego online systemu do głosowania:
„Tak działa system do głosowania w czasie rzeczywistym. Tutaj jest sesja klienta”
(wskazuje na kartę).

„Każda sesja ma dwa połączenia — połączenie przychodzące i połączenie wycho-


dzące” (na leżącą kartę kładzie dwie nowe i po kolei je wskazuje).

„Kiedy zaczyna się sesja klienta, na serwerze — o tutaj — tworzona jest sesja” (kła-
dzie kartę z prawej strony).

„Sesje serwera także mają po dwa połączenia” (na kartę z prawej strony kładzie
dwie karty reprezentujące połączenia).

„Kiedy rozpoczyna się sesja serwera, jest ona rejestrowana przez menedżera gło-
sowania” (powyżej sesji serwera kładzie kartę reprezentującą menedżera głosowania).
232 ROZDZIAŁ 17. MOJA APLIKACJA NIE MA STRUKTURY

„Po stronie serwera możemy mieć wiele sesji” (kładzie kolejny zestaw kart repre-
zentujących nowe sesje serwera oraz ich połączenia).

„Kiedy klient oddaje głos, jest on przesyłany do sesji po stronie serwera” (przesuwa
palec z jednego z połączeń po stronie sesji klienta do połączenia po stronie sesji serwera).
„Sesja serwera odpowiada potwierdzeniem, a następnie odnotowuje głos za pomocą
menedżera głosowania” (przesuwa palec z sesji serwera z powrotem do sesji klienta, a na-
stępnie wskazuje sesję serwera, po czym menedżera głosowania).
„Po tym wszystkim menedżer głosowania mówi każdej sesji po stronie serwera, aby
przekazały swoim sesjom klienta nowy wynik głosowania” (przenosi palec z menedżera
głosowania po kolei na każdą sesję serwera).
Jestem pewien, że opisowi temu czegoś brakuje, ponieważ nie jestem w stanie przesuwać
kart ani ich pokazywać tak, jak mógłbym to robić, gdybyśmy razem siedzieli przy stole.
Niemniej technika ta jest bardzo skuteczna. Dzięki niej elementy systemu stają się nama-
calnymi obiektami. Nie musisz używać kart; wszystkie poręczne rzeczy mogą okazać się
przydatne. Najważniejsze jest, abyś mógł korzystać z ruchu i miejsca w celu pokazania
interakcji zachodzących w systemie. Bardzo często te dwa elementy ułatwiają zrozu-
mienie scenariuszy. Z tego samego powodu sesje z kartami lepiej też zapadają w pamięć.
Istnieją tylko dwie wskazówki dotyczące korzystania z pustych kart CRC:
1. Karty przedstawiają obiekty, a nie klasy.
2. Nakładające się karty reprezentują zbiory obiektów.

Analiza rozmowy
W przypadku cudzego kodu bardzo kuszące jest unikanie tworzenia abstrakcji. Kiedy
spoglądam na cztery albo pięć klas, z których każda ma mniej więcej po tysiąc linii kodu,
nie biorę pod uwagę dodania nowej klasy, tylko zastanawiam się, co należy zmienić.
ANALIZA ROZMOWY 233

Gdy próbujemy zorientować się w tym wszystkim, często pomijamy rzeczy, które mogą
podsunąć nam dodatkowe pomysły, ponieważ nasza uwaga jest rozproszona. Oto przy-
kład. Pracowałem kiedyś z kilkoma członkami zespołu, którzy próbowali uruchomić
spory fragment kodu wywoływany przez wiele wątków. Kod był dość skomplikowany
i znajdowało się w nim kilka okazji do zakleszczenia. Zdaliśmy sobie sprawę z tego, że
jeśli w odpowiedniej kolejności będziemy blokować i odblokowywać zasoby, to będziemy
mogli uniknąć zakleszczenia w kodzie. Zaczęliśmy zastanawiać się, jak można by w tym
celu zmodyfikować kod. Już po chwili rozmawialiśmy o nowej polityce blokowania zaso-
bów i rozważaliśmy, jak przechowywać w tabelach zliczenia, aby takie blokowanie zreali-
zować. Kiedy jeden z programistów zaczął dopisywać w kodzie odpowiedni kod, powie-
działem: „Poczekaj, przecież rozmawiamy o polityce blokowania, prawda? Dlaczego nie
utworzymy klasy o nazwie LockingPolicy i nie będziemy przechowywać tam zliczeń? Mo-
żemy używać nazw metod, które rzeczywiście opisują, co próbujemy osiągnąć, a poza tym
tak będzie czytelniej niż w przypadku kodu, który po prostu wrzuca zliczenia do tabeli”.
Najgorsze było to, że zespół nie należał do niedoświadczonych. W bazie kodu znajdo-
wały się dobrze wyglądające części, ale obszerne fragmenty kodu proceduralnego mają
w sobie coś hipnotyzującego — zdają się one błagać o więcej.
Przysłuchuj się rozmowom dotyczącym Twojego projektu. Czy koncepcje, o których
mówisz, są tymi samymi koncepcjami, które istnieją w Twoim projekcie? Nie oczekuję,
że tak będzie w całej rozciągłości. Oprogramowanie musi spełniać wyższe wymagania, niż
tylko umożliwiać prowadzenie o nim łatwych rozmów. Jeśli jednak między rozmową
o kodzie a samym kodem istnieje spora rozbieżność, ważne jest pytanie, dlaczego tak się
dzieje. Odpowiedź zwykle jest połączeniem dwóch przyczyn: albo kod nie miał szans do-
stosować się do sposobu, w jaki rozumie go zespół, albo też zespół powinien rozumieć go
inaczej. W każdym razie zwracanie szczególnej uwagi na koncepcje używane instynk-
townie przez ludzi opisujących projekty przynosi wiele pożytku. Kiedy ktoś mówi o pro-
jekcie, zwykle stara się, aby jego wypowiedź była przejrzysta. Umieść nieco tej przejrzy-
stości w kodzie.
W rozdziale tym opisałem kilka technik służących do poznawania architektury dużych
systemów oraz udzielania o nich informacji. Wiele z tych technik doskonale nadaje się do
opracowywania projektów nowych systemów. W końcu projekt jest projektem, niezależ-
nie od etapu, na jakim znajduje się w cyklu produkcyjnym. Jednym z najgorszych błę-
dów, jaki może popełnić zespół, jest przekonanie, że w pewnym momencie projekt można
uznać za zakończony. Jeśli projekt jest „zakończony”, a programiści nadal wprowadzają
w nim zmiany, istnieje spore prawdopodobieństwo, że nowy kod zostanie umieszczony
w nieprzemyślanych miejscach, a klasy będą się rozrastać, gdyż nikt nie czuje się komfor-
towo, wprowadzając nową abstrakcję. Nie ma skuteczniejszego sposobu na pogorszenie
zastanego systemu.
234 ROZDZIAŁ 17. MOJA APLIKACJA NIE MA STRUKTURY
Rozdział 18.

Przeszkadza mi
mój testowy kod

Kiedy po raz pierwszy przystępowałeś do pisania testów jednostkowych, mogłeś czuć


się nieswojo. Jednym z wrażeń, które często nawiedza wtedy ludzi, jest poczucie, że testy
najzwyczajniej w świecie im przeszkadzają. Przeglądają oni swój projekt i czasami za-
pominają, czy mają do czynienia z kodem testowym, czy też produkcyjnym. Świadomość,
że zaczynasz coś robić tylko po to, aby na koniec pozostać z całą masą kodu testowego,
w niczym nie pomaga. Jeśli nie ustanowisz jakiejś konwencji, pochłonie Cię trzęsawisko.

Konwencje nazewnicze klas


Jedną z pierwszych zasad, jakie należy przyjąć, jest konwencja nazewnicza klas. Zazwyczaj
będziesz mieć do czynienia z przynajmniej jedną klasą testu jednostkowego na każdą klasę,
nad którą pracujesz. W związku z tym sensowne jest nadawanie klasom testowym nazw,
które są odmianami nazw klas produkcyjnych. Istnieje kilka konwencji nazewniczych,
z których można przy tym skorzystać. Najczęściej spotykane jest użycie słowa Test jako
przedrostka lub przyrostka nazwy klasy. Jeśli zatem mamy klasę DBEngine, moglibyśmy
nazwać naszą klasę testową TestDBEngine albo DBEngineTest. Czy nasz wybór ma jakieś
znaczenie? Tak naprawdę to nie. Osobiście wolę jednak konwencję z przyrostkiem. Jeśli
pracujesz w środowisku programistycznym, które potrafi utworzyć alfabetyczną listę klas,
każda klasa produkcyjna zostanie wówczas umieszczona obok swojej klasy testowej,
co ułatwia orientowanie się wśród nich.
Jakie jeszcze klasy poddajemy testom? Często przydatne jest tworzenie fałszywych
klas, które odgrywają rolę klas współpracujących z klasami znajdującymi się w danym
pakiecie lub katalogu. Konwencja, z której korzystam w takim przypadku, polega na uży-
ciu przedrostka Fake. Rozwiązanie takie umożliwia alfabetyczne pogrupowanie wszystkich
fałszywek w przeglądarce klas, chociaż znajdą się one w pewnym oddaleniu od głównych
236 ROZDZIAŁ 18. PRZESZKADZA MI MÓJ TESTOWY KOD

klas pakietu. Jest to jednak wygodne, ponieważ często zdarza się, że fałszywe klasy są pod-
klasami klas znajdujących się w innych katalogach.
Kolejnym często używanym w testach rodzajem klasy jest podklasa testowa. Podklasa
testowa to klasa, którą tworzysz tylko dlatego, że chcesz poddać testom klasę z zależ-
nościami, które musisz wyodrębnić. Jest to podklasa, jaką piszesz, gdy korzystasz z tech-
niki tworzenia podklasy i przesłaniania metody (398). Konwencja nazewnicza, z której
wtedy korzystam, polega na poprzedzeniu nazwy klasy słowem Testing. Jeśli klasy w pa-
kiecie albo katalogu są wymienione w porządku alfabetycznym, wszystkie klasy testowe
zostaną zgrupowane razem.
Oto przykładowa lista klas znajdujących się w katalogu z niewielkim pakietem
księgowym:
 CheckingAccount
 CheckingAccountTest
 FakeAccountOwner
 FakeTransaction
 SavingsAccount
 SavingsAccountTest
 TestingCheckingAccount
 TestingSavingsAccount
Zauważ, że każda klasa produkcyjna znajduje się obok swojej klasy testowej. Klasy
fałszywe oraz podklasy testowe zostały zgrupowane razem.
Takie grupowanie nie stanowi mojego dogmatu. Sprawdza się ono w wielu sytu-
acjach, ale istnieją też powody, dla których można je różnicować. Warto pamiętać, że
najważniejsza jest ergonomia. Należy wziąć pod uwagę, jak łatwe będzie przemieszczanie
się od klas do testów i z powrotem.

Lokalizacja testu
Jak do tej pory, przyjąłem w tym rozdziale założenie, że zarówno swój kod testowy, jak
i kod produkcyjny umieszczasz w tym samym katalogu. Zazwyczaj jest to najprostszy
sposób na ustrukturyzowanie projektu, chociaż istnieją z pewnością zagadnienia, które
należy rozważyć, kiedy decydujesz się na takie rozwiązanie.
Najważniejsze, co należy wziąć pod uwagę, to ograniczenia wdrożeniowe Twojej
aplikacji. Program działający na serwerze, który znajduje się pod Twoją kontrolą, może
nie mieć zbyt wielu ograniczeń. Jeśli tylko jesteś w stanie zaakceptować podczas wdra-
żania systemu dwa razy większe zapotrzebowanie na miejsce (binaria kodu produk-
cyjnego oraz testowego), dość łatwe będzie przechowywanie kodu i testów w tym samym
katalogu, a także wdrożenie wszystkich binariów.
LOKALIZACJA TESTU 237

Z drugiej jednak strony, gdy oprogramowanie jest produktem komercyjnym i działa


na cudzym komputerze, rozmiar kodu wdrożeniowego może stanowić problem. Możesz
wziąć pod uwagę przechowywanie kodu testowego oddzielnie od jego produkcyjnego
źródła, ale powinieneś zastanowić się, w jaki sposób wpłynie to na Twoją orientację
w kodzie.
Czasami, jak pokażę na poniższym przykładzie, przyjęcie takiego rozwiązania nie spra-
wia większej różnicy. W Javie pakiet może znajdować się w dwóch różnych katalogach:
source
com
orderprocessing
dailyorders
test
com
orderprocessing
dailyorders

Klasy produkcyjne możemy umieścić w katalogu dailyorders, poniżej source, nato-


miast klasy testowe w katalogu dailyorders, poniżej test, i będą one traktowane jakby
znajdowały się w tym samym pakiecie. Niektóre zintegrowane środowiska programi-
styczne pokazują takie dwa katalogi w tym samym widoku, dzięki czemu nie musisz
pamiętać, gdzie znajdują się one fizycznie.
W wielu innych językach i środowiskach lokalizacja ma jednak znaczenie. Jeśli w celu
przełączania się między kodem produkcyjnym a testowym będziesz musiał przemieszczać
się w górę i w dół po strukturze katalogu, zaczniesz się czuć, jakbyś musiał od swojej pracy
płacić podatek. Ludzie po prostu przestaną pisać testy, a praca zacznie posuwać się wolniej.
Alternatywnym rozwiązaniem jest przechowywanie kodu produkcyjnego i testowego
w tym samym katalogu oraz skorzystanie ze skryptów albo ustawień kompilacji w celu
usunięcia testów podczas wdrażania aplikacji. Jeżeli dla swoich klas przyjąłeś dobrą
konwencję nazewniczą, taki zabieg całkiem dobrze może się sprawdzić.
Jeśli decydujesz się na oddzielenie kodu testowego od produkcyjnego, powinieneś
przede wszystkim upewnić się, że masz ku temu dobre powody. Bardzo często zespoły
separują kody ze względów estetycznych — po prostu nie mogą znieść idei trzymania
kodu produkcyjnego i testowego w tym samym miejscu. Później orientowanie się
w projekcie robi się problematyczne. Można jednak przywyknąć do przechowywania
testów łącznie ze źródłowym kodem produkcyjnym. Po jakimś czasie taki sposób pracy
wydaje się najzupełniej normalny.
238 ROZDZIAŁ 18. PRZESZKADZA MI MÓJ TESTOWY KOD
Rozdział 19.

Mój projekt nie jest


zorientowany obiektowo.
Jak mogę bezpiecznie
wprowadzać zmiany?

Tytuł tego rozdziału jest trochę prowokacyjny. Bezpieczne zmiany można wprowadzać
w dowolnym języku, chociaż w niektórych językach jest to łatwiejsze niż w innych. Nawet
jeśli zorientowanie obiektowe zdominowało branżę, to istnieją również inne języki oraz
metody programowania. Mamy języki bazujące na regułach, języki funkcjonalne, języki
bazujące na ograniczeniach — taką listę można by jeszcze kontynuować. Ze wszystkich
jednak języków żadne nie rozpowszechniły się w takim stopniu jak stare dobre języki
proceduralne, jakimi są C, COBOL, FORTRAN, Pascal i BASIC.
Zwłaszcza języki proceduralne stanowią szczególne wyzwanie w cudzym środowisku.
Przetestowanie kodu przed wprowadzeniem w nim zmian jest ważne, ale możliwości,
jakimi dysponujemy podczas przeprowadzenia testów jednostkowych w językach proce-
duralnych, są mocno ograniczone. Często najprostsze, co można zrobić, to mocno się
zastanowić, dokonać poprawek i mieć nadzieję, że wprowadzone zmiany były dobre.
Powyższy dylemat jest wręcz pandemiczny w przypadku cudzego kodu procedu-
ralnego. W językach proceduralnych najczęściej po prostu nie ma spoin, które występują
w językach zorientowanych obiektowo (a także w wielu językach funkcjonalnych). Zmyślni
programiści mogą obejść ten problem, uważnie zarządzając zależnościami (istnieje na
przykład sporo wspaniałego kodu napisanego w C), chociaż równie łatwo jest otrzymać
prawdziwą gmatwaninę, która trudno poddaje się zmianom i weryfikacji.
Ponieważ usuwanie zależności w kodzie proceduralnym jest tak trudne, często najlep-
sza strategia polega na próbach poddania testom dużego fragmentu kodu jeszcze przed
wprowadzeniem jakichkolwiek zmian i na skorzystaniu następnie z tych testów w celu
240 ROZDZIAŁ 19. MÓJ PROJEKT NIE JEST ZORIENTOWANY OBIEKTOWO

uzyskania informacji zwrotnych podczas programowania. Techniki opisane w rozdziale 12.,


„Muszę dokonać wielu zmian w jednym miejscu. Czy powinienem pousuwać zależności
we wszystkich klasach, których te zmiany dotyczą?”, mogą okazać się pomocne. Mają
one zastosowanie zarówno do kodu proceduralnego, jak i zorientowanego obiektowo.
Ujmując rzecz krótko, opłaca się poszukać punktu zwężenia (190), po czym skorzystać ze
spoiny konsolidacyjnej (54), aby usunąć zależności w stopniu wystarczającym do umiesz-
czenia kodu w jarzmie testowym. Jeśli Twój język programowania korzysta z prepro-
cesora, możesz także skorzystać ze spoiny preprocesowej (51).
Taki jest standardowy sposób postępowania, chociaż nie jest on jedyny. W dalszej
części tego rozdziału przyjrzymy się sposobom na lokalne usuwanie zależności w pro-
gramach proceduralnych; dowiemy się, jak ułatwić sobie wprowadzanie weryfikowalnych
zmian, oraz poznamy metody posuwania się naprzód, gdy korzystamy z języków umożli-
wiających obranie ścieżki w kierunku zorientowania obiektowego.

Prosty przypadek
Nie zawsze kod proceduralny stanowi problem. Oto przykład — funkcja w C, obecna
w systemie operacyjnym Linux. Czy napisanie testów dla tej funkcji byłoby trudne, gdyby-
śmy musieli wprowadzić w niej jakieś zmiany?
void set_writetime(struct buffer_head * buf, int flag)
{
int newtime;

if (buffer_dirty(buf)) {
/* Move buffer to dirty list if jiffies is clear */
newtime = jiffies + (flag ? bdf_prm.b_un.age_super :
bdf_prm.b_un.age_buffer);
if(!buf->b_flushtime || buf->b_flushtime > newtime)
buf->b_flushtime = newtime;
} else {
buf->b_flushtime = 0;
}
}

Aby przetestować tę funkcję, możemy nadać zmiennej jiffies wartość, utworzyć


zmienną buffer_head, przekazać ją do funkcji, po czym sprawdzić jej wartość po wy-
wołaniu. W przypadku wielu funkcji nie będziemy jednak mieć tyle szczęścia. Czasami
funkcja wywołuje funkcję, która wywołuje następną funkcję, po czym następuje wywoła-
nie, z którym trudno jest sobie poradzić — jest nim funkcja dokonująca w jakimś miejscu
operacji wejścia-wyjścia lub pochodząca z obcej biblioteki. Chcemy przetestować, co
robi taki kod, ale zbyt często dowiadujemy się, że „robi coś świetnego, ale o tym dowie się
tylko jakiś element na zewnątrz programu, a Ty już nie”.
PRZYPADEK TRUDNY 241

Przypadek trudny
Oto funkcja w C, którą chcemy zmienić. Dobrze byłoby poddać ją testom, zanim wpro-
wadzimy nasze zmiany:
include "ksrlib.h"

int scan_packets(struct rnode_packet *packet, int flag)


{
struct rnode_packet *current = packet;
int scan_result, err = 0;

while(current) {
scan_result = loc_scan(current->body, flag);
if(scan_result & INVALID_PORT) {
ksr_notify(scan_result, current);
}
...
current = current->next;
}
return err;
}

W kodzie tym wywoływana jest funkcja o nazwie ksr_notify, która cechuje się
pewnym przykrym skutkiem ubocznym. Przesyła ona powiadomienie do obcego sys-
temu, a my raczej wolelibyśmy, aby tego nie robiła podczas testów.
Jednym ze sposobów na poradzenie sobie z tą sytuacją jest skorzystanie ze spoiny
konsolidacyjnej (54). Jeśli chcemy przeprowadzać testy bez wywoływania skutków
ubocznych związanych ze stosowaniem funkcji bibliotecznych, możemy utworzyć bi-
bliotekę zawierającą fałszywki — funkcje mające takie same nazwy jak funkcje oryginal-
ne, ale nierobiące tak naprawdę tego, co powinny. W tym przypadku napiszemy ciało
funkcji ksr_notify, które wygląda następująco:
void ksr_notify(int scan_code, struct rnode_packet *packet)
{
}

Możemy tę funkcję wbudować do biblioteki, po czym taką bibliotekę dołączyć do


kodu. Funkcja scan_packets będzie robić dokładnie to samo co wcześniej, z jednym
wyjątkiem — nie będzie wysyłać powiadomień. Jest to jak najbardziej w porządku, jeśli
przed wprowadzeniem zmian do funkcji zechcemy poznać inne jej zachowania.
Czy to właśnie taką strategię powinniśmy obrać? To zależy. Jeśli w bibliotece ksrlib
znajduje się wiele funkcji, a naszym zdaniem ich wywołania odgrywają raczej poboczną
rolę w stosunku do głównej logiki systemu, to utworzenie biblioteki fałszywek i dołączanie
jej podczas testów będzie mieć sens. Z drugiej jednak strony, jeśli za pośrednictwem tych
funkcji chcemy przeprowadzić rozpoznanie albo chcielibyśmy trochę zróżnicować warto-
ści przez nie zwracane, użycie spoin konsolidacyjnych (54) nie będzie już tak korzystne.
W rzeczy samej będzie dość żmudnym zajęciem. Ponieważ podmiana odbywa się
242 ROZDZIAŁ 19. MÓJ PROJEKT NIE JEST ZORIENTOWANY OBIEKTOWO

w momencie konsolidacji, będziemy mogli udostępniać tylko po jednej definicji funkcji


dla każdego wykonywanego pliku, który kompilujemy. Jeśli zechcemy, aby fałszywa
funkcja ksr_notify zachowywała się w pewien sposób podczas jednego testu, a inaczej
w trakcie drugiego testu, będziemy musieli umieścić w jej ciele kod i odpowiednio
skonfigurować testy, aby wymusić odpowiednie zachowanie funkcji. Wszystko to będzie
dość pogmatwane. Niestety, wiele języków proceduralnych nie pozostawia nam innych
możliwości.
W języku C mamy inną alternatywę. C ma preprocesor umożliwiający pisanie makr,
z którego możemy skorzystać, aby ułatwić sobie przeprowadzanie testów z funkcją
scan_packets. Oto jak wygląda plik zawierający tę funkcję po dodaniu kodu testowego:
#include "ksrlib.h"

#ifdef TESTING
#define ksr_notify(code,packet)
#endif

int scan_packets(struct rnode_packet *packet, int flag)


{
struct rnode_packet *current = packet;
int scan_result, err = 0;

while(current) {
scan_result = loc_scan(current->body, flag);
if(scan_result & INVALID_PORT) {
ksr_notify(scan_result, current);
}
...
current = current->next;
}
return err;
}

#ifdef TESTING
#include <assert.h>
int main () {
struct rnode_packet packet;
packet.body = ...
...
int err = scan_packets(&packet, DUP_SCAN);
assert(err & INVALID_PORT);
...
return 0;
}
#endif

W kodzie tym mamy definicję preprocesora TESTING, która określa odwołanie do nie-
istniejącej na czas testów funkcji ksr_notify, a także zawiera małą zaślepkę z testami.
Mieszanie w taki sposób testów z kodem źródłowym nie jest najmądrzejszym po-
sunięciem z naszej strony. Często z tego powodu orientowanie się w kodzie jest trudniejsze.
PRZYPADEK TRUDNY 243

Alternatywne rozwiązanie polega na dołączeniu pliku, dzięki czemu testy i kod pro-
dukcyjny będą znajdować się w odrębnych plikach:
#include "ksrlib.h"

#include "scannertestdefs.h"

int scan_packets(struct rnode_packet *packet, int flag)


{
struct rnode_packet *current = packet;
int scan_result, err = 0;

while(current) {
scan_result = loc_scan(current->body, flag);
if(scan_result & INVALID_PORT) {
ksr_notify(scan_result, current);
}
...
current = current->next;
}
return err;
}

#include "testscanner.tst"

Po tej zmianie kod wygląda mniej więcej tak samo, jak wyglądałby bez infrastruktury
testującej. Jedyna różnica polega na dołączeniu instrukcji #include na końcu pliku. Jeśli
testowane funkcje zadeklarujemy za pomocą słowa kluczowego forward, będziemy mogli
przenieść całą zawartość z pliku dołączanego na końcu do pliku pierwszego.
Aby uruchomić testy, wystarczy po prostu, że zdefiniujemy stałą TESTING i skompilujemy
nasz plik. Kiedy zdefiniowana jest stała TESTING, funkcja main() z pliku testscanner.tst
zostanie skompilowana i dołączona do pliku wykonywalnego, który uruchamia testy.
Funkcja main(), znajdująca się w tym pliku, uruchamia wyłącznie testy dla procedur
skanujących. Definiując odrębne funkcje testowe dla każdego z naszych czterech te-
stów, możemy skonfigurować plik w taki sposób, aby w tym samym czasie testy były
uruchamiane grupowo.
#ifdef TESTING
#include <assert.h>
void test_port_invalid() {
struct rnode_packet packet;
packet.body = ...
...
int err = scan_packets(&packet, DUP_SCAN);
assert(err & INVALID_PORT);
}

void test_body_not_corrupt() {
...
}

void test_header() {
244 ROZDZIAŁ 19. MÓJ PROJEKT NIE JEST ZORIENTOWANY OBIEKTOWO

...
}
#endif

W kolejnym pliku możemy je wywołać z funkcji main:


int main() {
test_port_invalid();
test_body_not_corrupt();
test_header();

return 0;
}

Możemy nawet posunąć się dalej, dodając rejestrowanie funkcji, które ułatwiają grupo-
wanie testów. Szczegółowe informacje znajdziesz w różnych platformach wspomagających
testowanie jednostkowe w C, które są dostępne pod adresem www.xprogramming.com.
Chociaż nietrudno o nieprawidłowe zastosowanie preprocesora makr, to jednak jest
on bardzo przydatny w omawianym kontekście. Dołączanie plików i zastępowanie makr
może pomóc nam w ominięciu zależności nawet w najbardziej problematycznym kodzie.
Jeśli tylko powstrzymamy się od zbyt rozrzutnego stosowania makr w kodzie, który jest
poddawany testom, nie będziemy musieli martwić się, że nieumiejętnie użyte przez nas
makra wpłyną na kod produkcyjny.
C jest jednym z głównych języków programowania, które mają preprocesor makr.
Zwykle jednak w celu usunięcia zależności w innych językach należy skorzystać ze spoiny
konsolidacyjnej (54) i spróbować poddać testom większe fragmenty kodu.

Dodawanie nowego zachowania


W cudzym kodzie proceduralnym bardziej opłaca się dołączać nowe funkcje, niż dołączać
nowy kod do funkcji już istniejących. Przynajmniej będziemy mogli tworzyć testy dla
funkcji podczas ich pisania.
Jak możemy uniknąć wprowadzania pułapek zależności w kodzie proceduralnym?
Jednym ze sposobów (zarysowanych w rozdziale 8., „Jak mogę dodać funkcjonalność?”)
jest skorzystanie z programowania sterowanego testami. Programowanie sterowane
testami sprawdza się zarówno w kodzie zorientowanym obiektowo, jak i proceduralnym.
Często zdarza się, że próby sformułowania testu dla każdego z fragmentów kodu, które
planujemy poddać testom, prowadzą nas do zmiany jego projektu na lepsze. Koncentruje-
my się na pisaniu funkcji wykonujących zadania obliczeniowe, a następnie integrujemy je
z resztą aplikacji.
Aby to zrobić, często inaczej będziemy musieli myśleć o tym, co powinniśmy napisać.
Oto przykład. Potrzebna jest nam funkcja o nazwie send_command. Za pośrednictwem
funkcji mart_key_send będzie ona wysyłać do innego systemu identyfikator, nazwisko
oraz łańcuch z poleceniem. Kod tej funkcji nie jest zbyt skomplikowany. Możemy sobie
wyobrazić, że wygląda on mniej więcej tak:
DODAWANIE NOWEGO ZACHOWANIA 245

void send_command(int id, char *name, char *command_string) {


char *message, *header;
if (id == KEY_TRUM) {
message = ralloc(sizeof(int) + HEADER_LEN + ...
...
} else {
...
}
sprintf(message, "%s%s%s", header, command_string, footer);
mart_key_send(message);

free(message);
}

W jaki jednak sposób możemy napisać test dla takiej funkcji, szczególnie gdy jedyna
metoda na ustalenie, co się dzieje, polega na znalezieniu się dokładnie w tym miejscu,
w którym jest wywoływana funkcja mart_key_send? A może byśmy przyjęli nieco inne
rozwiązanie?
Moglibyśmy przetestować całą tę logikę przed wywołaniem funkcji mart_key_send,
gdyby znajdowała się ona w innej funkcji. Pierwszy nasz test moglibyśmy napisać
następująco:
char *command = form_command(1,
"Mike Ratledge",
"56:78:cusp-:78");
assert(!strcmp("<-rsp-Mike Ratledge><56:78:cusp-:78><-rspr>",
command));

Następnie możemy napisać funkcję form_command, która zwraca polecenie:


char *form_command(int id, char *name, char *command_string)
{
char *message, *header;
if (id == KEY_TRUM) {
message = ralloc(sizeof(int) + HEADER_LEN + ...
...
} else {
...
}
sprintf(message, "%s%s%s", header, command_string, footer);

return message;
}

Gdy już ją mamy, możemy napisać prostą funkcję send_command, która jest nam
potrzebna:
void send_command(int id, char *name, char *command_string) {
char *command = form_command(id, name, command_string);
mart_key_send(command);

free(message);
}
246 ROZDZIAŁ 19. MÓJ PROJEKT NIE JEST ZORIENTOWANY OBIEKTOWO

W wielu przypadkach takie przeformułowanie kodu jest dokładnie tym, czego po-
trzebujemy, aby posunąć się do przodu. Całą czystą logikę umieszczamy w jednym ze-
stawie funkcji, dzięki czemu możemy uwolnić je od kłopotliwych zależności. Kiedy tak ro-
bimy, otrzymujemy w wyniku niewielkie funkcje opakowujące, takie jak send_command,
która wiąże naszą logikę i nasze zależności. Technika ta nie jest idealna, ale sprawdza
się, kiedy zależności nie dominują w kodzie.
W innych przypadkach potrzebujemy napisać funkcje, które będą zaśmiecone wywo-
łaniami zewnętrznymi. W funkcjach tych nie ma wielu obliczeń, ale niezbędne jest ustale-
nie odpowiedniej kolejności wywołań, które są w nich dokonywane. Jeśli na przykład
staramy się napisać funkcję obliczającą oprocentowanie pożyczki, najprostszy sposób
jej zrealizowania może wyglądać mniej więcej tak:
void calculate_loan_interest(struct temper_loan *loan, int calc_type)
{
...
db_retrieve(loan->id);
...
db_retrieve(loan->lender_id);
...
db_update(loan->id, loan->record);
...
loan->interest = ...
}

Co robimy w podobnych przypadkach? W wielu językach proceduralnych najlepszym


wyborem będzie pominięcie w pierwszym rzędzie testów i napisanie funkcji najlepiej, jak
tylko będziemy w stanie to zrobić. Być może na wyższym poziomie będziemy mogli
sprawdzić, czy funkcja działa poprawnie. W C mamy jednak inną opcję. Język C wspiera
wskaźniki do funkcji, z których możemy skorzystać, aby otrzymać kolejną spoinę. Oto jak
to zrobić:
Możemy utworzyć strukturę zawierającą wskaźniki do funkcji:
struct database
{
void (*retrieve)(struct record_id id);
void (*update)(struct record_id id, struct record_set *record);
...
};

Wskaźniki te możemy zainicjalizować adresami funkcji dostępu do bazy danych. Na-


stępnie strukturę tę można przekazać do jakichkolwiek nowych funkcji potrzebujących
dostępu do bazy danych, które napiszemy. W kodzie produkcyjnym funkcje te mogą
wskazywać rzeczywiste funkcje dostępu do bazy, a podczas testów mogą wskazywać na
fałszywki.
W przypadku wcześniejszych kompilatorów może zajść konieczność użycia składni
wskaźnika do funkcji w starym stylu:
extern struct database db;
(*db.update)(load->id, loan->record);
KORZYSTANIE Z PRZEWAGI ZORIENTOWANIA OBIEKTOWEGO 247

W innych kompilatorach możemy wywoływać te funkcje w naturalnym, obiektowo


zorientowanym stylu:
extern struct database db;
db.update(load->id, loan->record);

Technika ta nie jest ograniczona do C. Można z niej korzystać w większości języków,


które wspierają wskaźniki do funkcji.

Korzystanie z przewagi zorientowania obiektowego


W językach zorientowanych obiektowo mamy dostęp do spoin obiektowych (58). Mają
one kilka interesujących właściwości:
 Są łatwe do zauważenia w kodzie.
 Można z nich skorzystać w celu rozbicia kodu na mniejsze, prostsze do zrozumie-
nia fragmenty.
 Umożliwiają uzyskanie większej elastyczności. Spoiny, które wprowadzasz na po-
trzeby testów, mogą być przydatne, kiedy będziesz musiał rozszerzyć swój program.
Niestety, nie każde oprogramowanie można przekształcić na obiekty, chociaż w niektó-
rych przypadkach jest to o wiele prostsze niż w innych. Wiele języków proceduralnych
wyewoluowało w języki zorientowane obiektowo. Visual Basic Microsoftu dopiero
niedawno stał się w pełni obiektowy. Dla COBOL-a i Fortrana istnieją zorientowane
obiektowo rozszerzenia, a większość kompilatorów języka C umożliwia również kom-
pilowanie kodu w C++.
Jeśli Twój język daje Ci możliwość przejścia na stronę zorientowania obiektowego, masz
więcej opcji. Pierwszym krokiem będzie zazwyczaj skorzystanie z hermetyzacji referencji
globalnej (340) w celu poddania testom modyfikowanych fragmentów kodu. Możemy
użyć tej techniki, aby pozbyć się trudnej zależności, z jaką mieliśmy do czynienia w funkcji
ksr_notify we wcześniejszej części tego rozdziału. Przypomnę, jaki problem mieliśmy
z tą funkcją: nie chcieliśmy, aby wysyłała powiadomienia w czasie przeprowadzania testów.
int scan_packets(struct rnode_packet *packet, int flag)
{
struct rnode_packet *current = packet;
int scan_result, err = 0;

while(current) {
scan_result = loc_scan(current->body, flag);
if(scan_result & INVALID_PORT) {
ksr_notify(scan_result, current);
}
...
current = current->next;
}
248 ROZDZIAŁ 19. MÓJ PROJEKT NIE JEST ZORIENTOWANY OBIEKTOWO

return err;
}

Pierwszy etap polega na skompilowaniu kodu jako C++ zamiast C. W zależności od


naszego podejścia będzie to mała albo też wielka zmiana. Możemy zacisnąć zęby i spró-
bować rekompilacji całego projektu w C++ albo dokonać tego kawałek po kawałku, co
jednak będzie wymagać czasu.
Jeśli kompilujemy kod jako C++, możemy rozpocząć od znalezienia deklaracji funkcji
ksr_notify i opakować ją w klasę:
class ResultNotifier
{
public:
virtual void ksr_notify(int scan_result,
struct rnode_packet *packet);
};

Możemy także założyć dla naszej klasy nowy plik źródłowy i umieścić w nim jej do-
myślną implementację:
extern "C" void ksr_notify(int scan_result,
struct rnode_packet *packet);

void ResultNotifier::ksr_notify(int scan_result,


struct rnode_packet *packet)
{
::ksr_notify(scan_result, packet);
}

Zwróć uwagę, że nie zmieniamy nazwy funkcji ani jej sygnatury. Stosujemy technikę
zachowywania sygnatur (314), dzięki czemu minimalizujemy ryzyko popełnienia błędu.
Następnie deklarujemy globalną instancję klasy ResultNotifier i umieszczamy ją
w pliku źródłowym:
ResultNotifier globalResultNotifier;

Teraz możemy przeprowadzić rekompilację kodu i pozwolić, aby komunikaty o błę-


dach poinformowały nas, gdzie musimy dokonać zmian. Ponieważ deklarację funkcji
ksr_notify umieściliśmy w klasie, kompilator nie widzi już deklaracji tej funkcji w zakre-
sie globalnym.
Oto wyjściowa funkcja:
#include "ksrlib.h"

int scan_packets(struct rnode_packet *packet, int flag)


{
struct rnode_packet *current = packet;
int scan_result, err = 0;

while(current) {
scan_result = loc_scan(current->body, flag);
if(scan_result & INVALID_PORT) {
ksr_notify(scan_result, current);
KORZYSTANIE Z PRZEWAGI ZORIENTOWANIA OBIEKTOWEGO 249

}
...
current = current->next;
}
return err;
}

W celu umożliwienia kompilacji możemy skorzystać z deklaracji zewnętrznej, aby


obiekt globalResultNotifier stał się widoczny, a także poprzedzić funkcję ksr_notify
nazwą obiektu:
include "ksrlib.h"

extern ResultNotifier globalResultNotifier;

int scan_packets(struct rnode_packet *packet, int flag)


{
struct rnode_packet *current = packet;
int scan_result, err = 0;

while(current) {
scan_result = loc_scan(current->body, flag);
if(scan_result & INVALID_PORT) {
globalResultNotifier.ksr_notify(scan_result, current);
}
...
current = current->next;
}
return err;
}

W tym momencie kod będzie działać tak samo. Metoda ksr_notify w klasie
ResultNotifier deleguje się do funkcji ksr_notify. Czy mamy z tego jakąś korzyść? Cóż,
jeszcze nie. Następny krok polega na znalezieniu jakiegoś sposobu na takie skonfigu-
rowanie kodu, żebyśmy mogli korzystać z obiektu ResultNotifier w kodzie produkcyj-
nym, natomiast z innego obiektu, gdy prowadzimy testy. Istnieje wiele sposobów na
osiągnięcie takiego wyniku, ale akurat ten, który poprowadzi nas dalej w obranym kierunku,
to ponownie hermetyzacja referencji globalnej (340) i umieszczenie funkcji scan_packets
w następnej klasie, którą możemy nazwać Scanner.
class Scanner
{
public:
int scan_packets(struct rnode_packet *packet, int flag);
};

Teraz możemy zastosować parametryzację konstruktora (377) i zmienić klasę


Scanner, aby korzystała z obiektu klasy ResultNotifier, który dostarczymy:
class Scanner
{
private:
ResultNotifier& notifier;
250 ROZDZIAŁ 19. MÓJ PROJEKT NIE JEST ZORIENTOWANY OBIEKTOWO

public:
Scanner();
Scanner(ResultNotifier& notifier);

int scan_packets(struct rnode_packet *packet, int flag);


};

// w pliku źródłowym

Scanner::Scanner()
: notifier(globalResultNotifier)
{}

Scanner::Scanner(ResultNotifier& notifier)
: notifier(notifier)
{}

Po wprowadzeniu powyższej zmiany możemy znaleźć miejsca, w których używana jest


funkcja scan_packets, utworzyć instancję klasy Scanner i skorzystać z niej.
Zmiany te są zupełnie bezpieczne i czysto mechaniczne. Nie stanowią one najlepszego
przykładu projektu zorientowanego obiektowo, ale są wystarczająco dobre, aby posłużyły
nam jako klin rozbijający zależności, który pozwoli nam na przeprowadzenie testów pod-
czas naszego marszu do przodu.

Wszystko jest zorientowane obiektowo


Niektórzy programiści proceduralni lubią czepiać się zorientowania obiektowego. Uwa-
żają, że jest ono niepotrzebne lub myślą, że jego złożoność nie przynosi żadnych korzyści.
Kiedy jednak głębiej się nad tym zastanowisz, zaczniesz sobie zdawać sprawę z faktu, że
wszystkie programy proceduralne są zorientowane obiektowo; szkoda tylko, że tak wiele
z nich zawiera tylko po jednym obiekcie. Aby to zauważyć, wyobraź sobie program zawie-
rający około 100 funkcji. Oto ich deklaracje:
...
int db_find(char *id, unsigned int mnemonic_id,
struct db_rec **rec);
...
...
void process_run(struct gfh_task **tasks, int task_count);
...

Wyobraź sobie, że umieszczamy wszystkie te deklaracje w jednym pliku i otaczamy je


deklaracją klasy:
class program
{
public:
...
int db_find(char *id, unsigned int mnemonic_id,
WSZYSTKO JEST ZORIENTOWANE OBIEKTOWO 251

struct db_rec **rec);


...
...
void process_run(struct gfh_task **tasks, int task_count);
...
};

Teraz możemy odszukać każdą definicję funkcji (tutaj pokazałem tylko jedną):
int db_find(char *id,
unsigned int mnemonic_id,
struct db_rec **rec);
{
...
}
i poprzedzić jej nazwę nazwą klasy:
int program::db_find(char *id,
unsigned int mnemonic_id,
struct db_rec **rec);
{
...
}

Teraz musimy w programie napisać nową funkcję main():


int main(int ac, char **av)
{
program the_program;

return the_program.main(ac, av);


}

Czy powyższe zabiegi zmieniają zachowanie systemu? Niezupełnie. Zmiana ta była


wyłącznie procesem mechanicznym i pozostawiła dokładnie takie samo znaczenie oraz
zachowanie programu. Stary system w C był tak naprawdę jednym wielkim obiektem.
Kiedy zaczynamy stosować hermetyzację referencji globalnej (340), tworzymy wiele
nowych obiektów i dzielimy system w sposób, który ułatwi nam w nim pracę.
Kiedy języki proceduralne mają zorientowane obiektowo rozszerzenia, możemy udać
się w tym kierunku. Nie jest to głębokie zorientowanie obiektowe; to tylko użycie obiek-
tów w celu rozbicia programu na potrzeby przeprowadzenia testów.
Co jeszcze oprócz wyodrębniania zależności możemy zrobić, gdy nasz język progra-
mowania wspiera zorientowanie obiektowe? Przede wszystkim możemy stopniowo zmie-
rzać w kierunku lepszego projektowania obiektów, co zazwyczaj oznacza, że należy po-
grupować powiązane ze sobą funkcje w klasy oraz wyodrębnić mnóstwo metod, dzięki
czemu będzie można powydzielać splątane odpowiedzialności. Więcej informacji na
ten temat znajdziesz w rozdziale 20., „Ta klasa jest za duża, a ja nie chcę, żeby stała się
jeszcze większa”.
Kod proceduralny nie daje nam tylu możliwości, ile kod zorientowany obiektowo,
niemniej nawet w zastanym kodzie proceduralnym możemy poczynić postępy. Spoiny,
jakie oferuje język proceduralny, w znaczący sposób wpływają na jakość naszej pracy.
252 ROZDZIAŁ 19. MÓJ PROJEKT NIE JEST ZORIENTOWANY OBIEKTOWO

Jeśli język proceduralny, jakiego używasz, ma swojego zorientowanego obiektowo następcę,


zalecam przestawienie się na niego. Spoiny obiektowe (40) mają o wiele szersze zastosowa-
nie niż tylko umieszczanie testów w kodzie. Spoiny konsolidacyjne i preprocesowe
świetnie sprawdzają się w czasie przygotowywania kodu do testów, ale tak naprawdę nie
wpływają w większym stopniu na poprawę projektu.
Rozdział 20.

Ta klasa jest za duża,


a ja nie chcę, żeby stała się
jeszcze większa

Wiele elementów, które są dodawane do systemów, to drobne poprawki. Wymagają


one dodania niewielkiej ilości kodu i być może kilku metod. Kuszące jest wprowadza-
nie takich zmian w klasach znajdujących się już w systemie. Istnieje prawdopodobień-
stwo, że kod, który musisz dodać, korzysta z danych udostępnianych przez jakąś klasę,
a najprostsze, co można zrobić, to wstawienie do niej nowego kodu. Niestety, taki pro-
sty sposób na dokonywanie zmian może prowadzić do poważnych problemów. Kiedy
kontynuujemy dodawanie kodu do istniejących klas, możemy w rezultacie otrzymać
długie metody i ogromne klasy. Nasz program przeistacza się w grzęzawisko i coraz
więcej czasu zabiera nam znalezienie sposobu na dodawanie nowych elementów albo
nawet zrozumienie, jak działają stare funkcjonalności.
Odwiedziłem kiedyś zespół, który miał rozrysowane na papierze coś, co wyglądało
na świetną architekturę. Ludzie z tego zespołu pokazali mi główne klasy oraz opowie-
dzieli, jak komunikują się one między sobą w standardowych warunkach. Następnie
wyciągnęli kilka ładnych diagramów UML, ukazujących strukturę systemu. Kiedy za-
cząłem oglądać kod, poczułem zaskoczenie. Każdą z klas można było rozbić na mniej
więcej 10 mniejszych klas, co pomogłoby zespołowi poradzić sobie z najpilniejszymi
problemami, z którymi się borykał.
Jakie problemy sprawiają duże klasy? Pierwszym z nich jest dezorientacja. Kiedy
masz w klasie 50 albo 60 metod, często trudno jest zrozumieć, co należy zmienić i czy ta
zmiana nie wpłynie jeszcze na coś innego. W najgorszych przypadkach obszerne klasy
zawierają niewiarygodnie dużo zmiennych instancji i nie wiadomo, jakie skutki przyniesie
modyfikacja zmiennej. Kolejnym problemem jest harmonogram zadań. Gdy klasa ma
jakieś 20 odpowiedzialności, prawdopodobnie będziesz mieć wiele powodów, aby ją
254 ROZDZIAŁ 20. TA KLASA JEST ZA DUŻA, A JA NIE CHCĘ, ŻEBY STAŁA SIĘ JESZCZE WIĘKSZA

zmienić. Podczas tej samej iteracji może nad nią pracować wielu programistów, z których
każdy będzie się zajmować czymś innym. Jeśli będą oni pracować równolegle, może to
doprowadzić do poważnych kłopotów, zwłaszcza że istnieje jeszcze trzeci problem —
duże klasy są trudne w testowaniu. Hermetyzacja to dobre rozwiązanie, prawda? Cóż,
nie zadawaj takiego pytania programistom, bo chętnie odgryzą Ci głowę. Zbyt duże
klasy często za wiele ukrywają. Hermetyzacja sprawdza się wtedy, gdy pomaga nam
analizować nasz kod i gdy wiemy, że pewne rzeczy można zmienić tylko w pewnych
okolicznościach. Kiedy jednak za bardzo hermetyzujemy, to, co znajdzie się w środku,
zaczyna się psuć i jątrzyć. Nie ma prostych sposobów na rozpoznanie skutków zmian,
dlatego też programiści zwracają się ku metodzie programowania edytuj i módl się (27).
Na tym etapie wprowadzanie zmian trwa zbyt długo albo wzrasta liczba błędów. Taka jest
cena za brak przejrzystości w kodzie.
Pierwszy problem, z jakim musimy się zmierzyć w przypadku dużych klas, jest na-
stępujący: w jaki sposób możemy pracować, aby nie pogorszyć spraw? Główną taktyką,
którą możemy obrać, jest kiełkowanie klasy (80) oraz kiełkowanie metody (77). Kiedy
musimy wprowadzić zmiany, powinniśmy rozważyć umieszczenie kodu w nowej klasie
lub nowej metodzie. Kiełkowanie klasy (80) istotnie zapobiega pogarszaniu się spraw.
Jeżeli nowy kod ulokujesz w nowej klasie, to — rzecz jasna — może zajść potrzeba dele-
gowania z klasy wyjściowej, ale przynajmniej nie spowodujesz jej powiększenia. Kiełko-
wanie metody (77) także może być pomocne, chociaż w bardziej subtelny sposób. Jeśli
dodasz kod w nowej metodzie, to oczywiście w rezultacie uzyskasz dodatkową metodę,
ale przynajmniej zidentyfikujesz nowe zadanie wykonywane przez klasę i nadasz mu nazwę.
Bardzo często nazwy metod mogą podpowiedzieć Ci, jak rozbić klasę na mniejsze elementy.
Najlepszym lekarstwem na duże klasy jest refaktoryzacja. Pomaga ona je rozbijać na
mniejsze klasy. Poważniejszym zagadnieniem jest jednak określenie, jak te mniejsze klasy
powinny wyglądać. Na szczęście dysponujemy pewnymi wskazówkami, jak to zrobić.

Zasada pojedynczej odpowiedzialności


Każda klasa powinna mieć pojedynczą odpowiedzialność — powinna ona spełniać w sys-
temie jedno zadanie i powinien być tylko jeden powód do jej zmiany.

Zasada pojedynczej odpowiedzialności jest trochę trudna do opisania, ponieważ sama


idea odpowiedzialności jest w pewnym sensie niewyraźna. Jeśli spojrzymy na ten problem
z naiwnością, będziemy mogli zapytać: „Aha, a więc oznacza to, że każda klasa powinna
zawierać tylko jedną metodę, prawda?”. No tak, metody można postrzegać jako odpowie-
dzialności. Klasa Task jest odpowiedzialna za uruchamianie zadań przy użyciu metody
run; za informowanie za pomocą metody taskCount, ile podzadań wykonuje itd. Co
jednak rozumiemy przez „odpowiedzialność”, okazuje się wtedy, gdy mówimy o głów-
nym zadaniu. Na rysunku 20.1 pokazano przykładową klasę.
DOSTRZEGANIE ODPOWIEDZIALNOŚCI 255

Rysunek 20.1. Analizator reguł

Mamy tu niewielką klasę, która analizuje łańcuchy tekstowe z regułami opisanymi


w jakimś nieokreślonym języku. Jakie odpowiedzialności zawiera ta klasa? Możemy spoj-
rzeć na angielską nazwę klasy i poznać jedną z jej odpowiedzialności: jest nią analizo-
wanie. Czy jednak jest to jej główne zadanie? Wcale na to nie wygląda. Wydaje się, że
klasa ta również coś oblicza.
Co jeszcze robi? Zajmuje się bieżącym łańcuchem tekstowym — tym, którego skład-
nię analizuje. Podczas analizowania odwołuje się także do pola wskazującego bieżącą
pozycję w łańcuchu. Obie te miniodpowiedzialności zdają się podchodzić pod katego-
rię analizy składni.
Spójrzmy teraz na inny element — pole variables. Przechowuje ono zbiór zmiennych,
z których korzysta analizator w celu obliczania wyrażeń arytmetycznych, takich jak na
przykład + 3, zapisanych w regułach. Jeśli ktoś wywoła metodę addVariable z argumen-
tami a oraz 1, wyrażenie a + 3 zostanie przekształcone na wynik równy 4. Wygląda
zatem na to, że klasa ta zawiera jeszcze jedną odpowiedzialność, którą jest zarządzanie
zmiennymi.
Czy istnieje jeszcze więcej odpowiedzialności? Kolejny sposób, aby to ustalić, polega
na przyjrzeniu się nazwom metod. Czy możemy w jakiś naturalny sposób je pogrupować?
Wydaje się, że metody można podzielić następująco:

evaluate branchingExpression nextTerm addVariable


causalExpression hasMoreTerms
variableExpression
valueExpression

Metoda evaluate jest punktem startowym klasy. Ma ona tylko dwie metody pu-
bliczne, a jej nazwa określa jej główną odpowiedzialność, jaką jest obliczanie. Wszystkie
metody kończące się przyrostkiem Expression są w pewnym sensie takie same. Nie tylko
mają podobne nazwy, ale też przyjmują jako argument obiekt klasy Node i zwracają liczbę
256 ROZDZIAŁ 20. TA KLASA JEST ZA DUŻA, A JA NIE CHCĘ, ŻEBY STAŁA SIĘ JESZCZE WIĘKSZA

całkowitą określającą wartość podwyrażenia. Metody nextTerm i hasMoreTerms także są


do siebie podobne. Wydaje się, że przeprowadzają one jakąś specjalną formę tokenizacji
łańcuchów tekstowych. Jak już wspomnieliśmy wcześniej, metoda addVariable zajmuje
się zarządzaniem zmiennymi.
Podsumowując, wygląda na to, że klasa RuleParser ma następujące odpowiedzialności:
 analizowanie składni,
 obliczanie wyrażeń,
 tokenizacja łańcuchów tekstowych,
 zarządzanie zmiennymi.
Gdybyśmy mieli stworzyć od podstaw projekt, w którym odpowiedzialności te byłyby
od siebie odseparowane, mógłby on wyglądać mniej więcej tak, jak na rysunku 20.2.

Rysunek 20.2. Klasy reguł z rozdzielonymi odpowiedzialnościami

Czy nie przesadziliśmy? Być może tak. Często osoby piszące niewielkie interpretery
języków łączą ze sobą analizę składni oraz obliczanie wyrażeń; ich programy po prostu
dokonują obliczeń podczas analizowania składni. Chociaż takie rozwiązanie może być
wygodne, to jednak nie sprawdza się, kiedy język się rozrasta. Kolejną odpowiedzial-
nością, która wydaje się być raczej wątła, jest rola klasy SymbolTable. Jeżeli jedyną od-
powiedzialnością tej klasy jest mapowanie nazw zmiennych na liczby całkowite, to ko-
rzystając z niej, nie zyskujemy większej przewagi niż w sytuacji, gdybyśmy użyli tablicy
mieszającej albo listy. Projekt ten wygląda całkiem przyjemnie. Ale wiesz co? Jest raczej
hipotetyczny. Jeśli tylko nie zdecydujemy się na przepisanie od nowa tej części systemu,
projekt wieloklasowy nie będzie zagrożony.
W przypadku prawdziwych dużych klas kluczem jest zidentyfikowanie różnych od-
powiedzialności oraz wypracowanie sposobu na stopniowe przejście w kierunku od-
powiedzialności bardziej skupionych.
DOSTRZEGANIE ODPOWIEDZIALNOŚCI 257

Dostrzeganie odpowiedzialności
W przykładzie z klasą RuleParser pokazałem pewien sposób rozbicia klasy na klasy mniej-
sze. Kiedy to robiłem, postępowałem w zasadzie bez namysłu. Utworzyłem listę wszyst-
kich metod i zastanowiłem się, jakie są ich odpowiedzialności. Kluczowe pytania, jakie
zadałem, brzmiały: „Dlaczego ta metoda tu się znajduje?” i „Jaką rolę odgrywa ona w kla-
sie?”. Następnie pogrupowałem je, zestawiając obok siebie metody, które znalazły się tam
z podobnych przyczyn.
Taki sposób postrzegania odpowiedzialności nazywam grupowaniem metod. Jest to
zaledwie jeden ze sposobów na znajdowanie odpowiedzialności w istniejącym już kodzie.
Nauczenie się dostrzegania odpowiedzialności jest ważną umiejętnością, która ma
związek z projektowaniem i wymaga praktyki. Mówienie o znajomości projektowania
może wydawać się dziwne w kontekście pracy z cudzym kodem, ale naprawdę nie ma
większej różnicy między odkrywaniem odpowiedzialności w istniejącym kodzie a for-
mułowaniem ich na potrzeby kodu, który nie został jeszcze napisany. Najważniejszą rzeczą
jest umiejętność dostrzegania odpowiedzialności oraz nauczenie się ich skutecznego
rozdzielania. W każdym razie cudzy kod oferuje o wiele więcej możliwości na wykorzy-
stanie umiejętności projektowania niż w przypadku tworzenia nowych funkcjonalności.
Łatwiej jest rozmawiać o kompromisach związanych z projektem, gdy widzisz kod, na
który on wpływa, a także łatwiej można stwierdzić, czy struktura jest odpowiednia
w danym kontekście, ponieważ kontekst jest jak najbardziej rzeczywisty i mamy go tuż
przed oczami.
W podrozdziale tym opisałem kilka heurystyk, z których możemy skorzystać w celu
dostrzeżenia odpowiedzialności w istniejącym kodzie. Zwróć uwagę, że ich nie wynajdu-
jemy, odkrywamy tylko takie odpowiedzialności, które już się tam znajdują. Bez względu
na strukturę, jaką ma cudzy kod, jego fragmenty realizują identyfikowalne zadania. Nie-
które z tych zadań mogą być trudne do zauważenia, ale opisane techniki będą pomocne
podczas ich odkrywania. Spróbuj je zastosować nawet w odniesieniu do kodu, którego już
wkrótce nie będziesz musiał zmieniać. Im szybciej zaczniesz zauważać odpowiedzialności
tkwiące w kodzie, tym lepiej je poznasz.

Heurystyka nr 1. Pogrupuj metody


Poszukaj podobnych nazw metod. Zapisz wszystkie metody istniejące w klasie, łącznie z ich
rodzajem (publiczna, prywatna itd.), i spróbuj znaleźć metody, które realizują podobne zadania.

Technika ta, tj. grupowanie metod, jest dobra na początek, szczególnie w przypadku
dużych klas. Ważne jest, aby pamiętać, że nie musisz każdej metody przydzielać do odręb-
nej grupy. Po prostu sprawdź, czy uda Ci się znaleźć metody, które zdają się tworzyć
część wspólnej odpowiedzialności. Jeśli zidentyfikujesz niektóre z odpowiedzialności
258 ROZDZIAŁ 20. TA KLASA JEST ZA DUŻA, A JA NIE CHCĘ, ŻEBY STAŁA SIĘ JESZCZE WIĘKSZA

wykraczających nieco poza główną odpowiedzialność klasy, poznasz kierunek, w któ-


rym będzie mógł zmierzać Twój kod wraz z upływem czasu. Poczekaj do chwili, w której
przyjdzie Ci dokonać modyfikacji podzielonych przez Ciebie na kategorie metod, i do-
piero wtedy podejmij decyzję, czy chcesz je wyodrębnić.
Grupowanie metod jest również doskonałym ćwiczeniem grupowym. W pokoju,
w którym pracuje zespół, umieść tablice z listami nazw metod znajdujących się w każdej
z głównych klas. Członkowie zespołu mogą nanosić na tych tablicach swoje oznaczenia,
pokazując różne grupowania metod. Cały zespół może dyskutować o tym, które gru-
powania są lepsze, i podjąć decyzję o kierunku, w którym powinien zmierzać kod.

Heurystyka nr 2. Rozejrzyj się za ukrytymi metodami


Zwróć uwagę na metody prywatne i chronione. Jeśli w klasie znajduje się wiele z nich, często
może to oznaczać, że zawiera ona jeszcze inną klasę, która aż prosi się o to, aby ją wydobyć.

Duże klasy mogą ukrywać zbyt wiele. Następujące pytanie jest wciąż na nowo zadawane
przez osoby dopiero rozpoczynające testowanie jednostkowe: „Jak mogę poddawać testom
metody prywatne?”. Wiele z tych osób poświęca mnóstwo czasu, próbując rozwiązać ten
problem, chociaż — jak już wspomniałem wcześniej w tym rozdziale — odpowiedź na nie
jest taka, że jeśli naprawdę odczuwasz potrzebę przetestowania metody prywatnej, to me-
toda ta w ogóle nie powinna być prywatna. Jeżeli upublicznienie tej metody niepokoi Cię,
jest tak prawdopodobnie dlatego, że stanowi ona część odrębnej odpowiedzialności i po-
winna znaleźć się w innej klasie.
Klasa RuleParser, wspomniana wcześniej w tym podrozdziale, jest typowym tego
przykładem. Zawiera ona dwie metody publiczne: evaluate i addVariable. Cała reszta jest
prywatna. Jaką klasą byłaby RuleParser, gdybyśmy upublicznili jej metody nextTerm oraz
hasMoreTerms? No cóż, byłaby dość dziwna. Użytkownicy analizatora składni mogliby
sądzić, że w celu przeanalizowania składni i wykonania obliczeń powinni skorzystać z obu
tych metod oraz metody evaluate. Upublicznienie tych metod w klasie RuleParser
byłoby dziwne, ale już takie nie jest upublicznienie ich w klasie TermTokenizer. Zabieg taki
nie powoduje, że klasa RuleParser jest w mniejszym stopniu hermetyzowana. Nawet jeśli
nextTerm i hasMoreTerms są publiczne w klasie TermTokenizer, to ich odczytywanie w klasie
RuleParser odbywa się prywatnie, co pokazano na rysunku 20.3.

Rysunek 20.3. Klasy RuleParser i TermTokenizer


DOSTRZEGANIE ODPOWIEDZIALNOŚCI 259

Heurystyka nr 3. Poszukaj decyzji, które można zmienić


Poszukaj decyzji, ale nie takich, które są podejmowane w kodzie, lecz takich, które już zapadły.
Czy jest jakiś inny sposób na zrealizowanie czegoś (jak komunikowanie się z bazą danych,
wymiana informacji z innymi obiektami itd.), co wydaje się zapisane bezpośrednio w kodzie?
Czy potrafisz sobie wyobrazić, że można to zmienić?

Kiedy usiłujesz rozbić dużą klasę, kuszące jest skierowanie swojej uwagi na nazwy
metod. W końcu stanowią one najbardziej zauważalny składnik klasy. Nazwy metod nie
przekazują jednak pełni obrazu. Często duże klasy zawierają metody, które robią wiele
różnych rzeczy na wielu różnych poziomach abstrakcji. Na przykład metoda o nazwie
updateScreen() może generować tekst do wyświetlenia, formatować go i przesyłać do
kilku różnych obiektów graficznego interfejsu użytkownika. Patrząc na samą nazwę
metody, nie dowiesz się, ile pracy ona wykonuje oraz ile odpowiedzialności upchnięto
w jej kodzie.
Z tych też powodów przed zabraniem się do wyodrębniania klas opłaca się prze-
prowadzenie niewielkiej refaktoryzacji techniką wyodrębniania metod. Które metody
powinienem wydobyć? Borykam się z takimi dylematami, szukając decyzji. Ile różnych
założeń przyjęto w kodzie? Czy kod wywołuje metody w jakimś określonym API? Czy
założono, że zawsze będzie odczytywał dane z tej samej bazy? Jeśli kod wykonuje te
czynności, dobrym pomysłem jest wyodrębnienie metod, które odzwierciedlają to, co
zamierzasz zrobić na wyższym poziomie. Jeżeli określone informacje są pobierane z bazy
danych, wyodrębnij metodę, która bierze swoją nazwę od tych informacji. Kiedy już do-
konasz takich wyodrębnień, będziesz mieć o wiele więcej metod, ale przekonasz się też, że
grupowanie ich jest prostsze. Co więcej, może okazać się, iż dokonałeś całkowitej herme-
tyzacji jakiegoś zasobu za zbiorem metod. Kiedy wyodrębnisz dla nich klasę, usuniesz
niektóre zależności zachodzące między niskopoziomowymi elementami.

Heurystyka nr 4. Poszukaj wewnętrznych relacji


Poszukaj zależności istniejących między zmiennymi instancji a metodami. Czy pewne zmienne
instancji są używane przez określone metody, ale już nie przez inne?

Naprawdę trudno jest znaleźć klasy, w których wszystkie metody korzystają ze wszyst-
kich zmiennych instancji. Zwykle w klasie można stwierdzić pewien stopień „zbrylenia”.
Dwie lub trzy metody mogą być jedynymi elementami korzystającymi z określonego zbio-
ru zmiennych. Bardzo często nazwy mogą być pomocne w zauważeniu tego zjawiska.
Na przykład w klasie RulerParser istnieje kolekcja o nazwie variables oraz metoda
addVariable, co pozwala nam zauważyć, że istnieje między nimi oczywisty związek.
Nie znaczy to, że nie ma innych metod odczytujących tę zmienną, ale przynajmniej mamy
miejsce, w którym możemy rozpocząć nasze poszukiwania.
260 ROZDZIAŁ 20. TA KLASA JEST ZA DUŻA, A JA NIE CHCĘ, ŻEBY STAŁA SIĘ JESZCZE WIĘKSZA

Kolejna technika, której możemy użyć w celu znalezienia tych „brył”, polega na sporzą-
dzeniu niewielkiego schematu relacji zachodzących wewnątrz klasy, nazywanego schema-
tem funkcjonalności. Pokazuje on, które metody oraz zmienne instancji są używane
przez poszczególne metody w klasie, i jest dość łatwy do sporządzenia. Oto przykład:
class Reservation
{
private int duration;
private int dailyRate;
private Date date;
private Customer customer;
private List fees = new ArrayList();

public Reservation(Customer customer, int duration,


int dailyRate, Date date) {
this.customer = customer;
this.duration = duration;
this.dailyRate = dailyRate;
this.date = date;
}

public void extend(int additionalDays) {


duration += additionalDays;
}

public void extendForWeek() {


int weekRemainder = RentalCalendar.weekRemainderFor(date);
final int DAYS_PER_WEEK = 7;
extend(weekRemainder);
dailyRate = RateCalculator.computeWeekly(
customer.getRateCode())
/ DAYS_PER_WEEK;
}

public void addFee(FeeRider rider) {


fees.add(rider);
}

int getAdditionalFees() {
int total = 0;lities
for(Iterator it = fees.iterator(); it.hasNext(); ) {
total += ((FeeRider)(it.next())).getAmount();
}
return total;
}

int getPrincipalFee() {
return dailyRate
* RateCalculator.rateBase(customer)
* duration;
}

public int getTotalFee() {


return getPrincipalFee() + getAdditionalFees();
}
}
DOSTRZEGANIE ODPOWIEDZIALNOŚCI 261

Pierwszym krokiem jest zakreślenie okręgów dookoła każdej ze zmiennych, jak poka-
zano na rysunku 20.4.

Rysunek 20.4. Zmienne klasy Reservation

Następnie szukamy wszystkich metod i wokół nich także zakreślamy okręgi. W dalszej
kolejności rysujemy linie od każdego okręgu z metodą do wszystkich okręgów ze zmien-
nymi instancji, które są przez te metody odczytywane lub modyfikowane. Zazwyczaj można
przy tym pominąć konstruktory, ponieważ modyfikują one każdą ze zmiennych instancji.
Rysunek 20.5 pokazuje schemat po dodaniu do niego okręgu z metodą extend.

Jeśli przeczytałeś już rozdziały opisujące schematy skutków (167), być może zauważyłeś,
że schematy funkcjonalności w znacznym stopniu je przypominają. Zasadniczo są one ze
sobą spokrewnione. Główna różnica między nimi polega na tym, że strzałki na obu typach
schematów są zwrócone w przeciwnych kierunkach. Na schemacie funkcjonalności strzałki
wskazują w stronę metody lub zmiennej, z której korzysta inna metoda lub zmienna. Na sche-
macie skutków strzałka jest zwrócona w kierunku metody lub zmiennej, na którą wywie-
rają wpływ inne metody albo zmienne.
Są to dwa różne sposoby obrazowania interakcji zachodzących w systemie, z których każdy ma
swoje zastosowanie. Schematy funkcjonalności znakomicie nadają się do obrazowania we-
wnętrznej struktury klas. Z kolei schematy skutków (167) doskonale sprawdzają się pod-
czas śledzenia efektów zmian w przód, począwszy od punktu zmiany.
Czy ich zbliżony wygląd może być mylący? Niezupełnie. Oba te schematy są narzędziami jed-
norazowego użytku. Tego typu pomoce sporządzasz razem ze swoim kolegą na 10 minut przed
wprowadzeniem zmian, a potem je wyrzucasz. Nie ma korzyści z ich zachowywania, stąd też
jest mało prawdopodobne, że zostaną ze sobą pomylone.
262 ROZDZIAŁ 20. TA KLASA JEST ZA DUŻA, A JA NIE CHCĘ, ŻEBY STAŁA SIĘ JESZCZE WIĘKSZA

Rysunek 20.5. Metoda extend korzysta ze zmiennej duration

Rysunek 20.6 pokazuje schemat po dodaniu okręgów dla każdej metody oraz
strzałek dla wszystkich zmiennych, z których metody te korzystają.

Rysunek 20.6. Schemat funkcjonalności klasy Reservation


DOSTRZEGANIE ODPOWIEDZIALNOŚCI 263

Czego możemy się dowiedzieć z tego schematu? Jedną z oczywistych jego cech są dające
się zauważyć w klasie skupiska. Zmienne duration, dailyRate, date i customer są używane
przede wszystkim przez metody getPrincipalFee, extend i extendForWeek. Czy któreś
z tych metod są publiczne? Tak, extend i extendForWeek są publiczne, ale getPrincipal już
nie. Jak wyglądałby nasz system, gdybyśmy z tego skupiska zrobili odrębną klasę
(patrz rysunek 20.7)?

Rysunek 20.7. Skupisko w klasie Reservation

Duży krąg na schemacie mógłby być nową klasą. Jej metody extend, extendForWeek
i getPrincipalFee musiałyby być publiczne, ale wszystkie pozostałe metody mogłyby
pozostać prywatne. Metody fees, addFee, getAdditionalFees oraz getTotalFee mogliby-
śmy zachować w klasie Reservation i delegować je do tej nowej klasy (patrz rysunek 20.8).

Rysunek 20.8. Klasa Reservation korzystająca z nowej klasy


264 ROZDZIAŁ 20. TA KLASA JEST ZA DUŻA, A JA NIE CHCĘ, ŻEBY STAŁA SIĘ JESZCZE WIĘKSZA

Najważniejsze, co powinniśmy wiedzieć przed próbą wprowadzenia tej modyfikacji,


jest to, czy ta nowa klasa będzie mieć wyraźnie określoną odpowiedzialność. Czy możemy
zaproponować dla niej jakąś nazwę? Wygląda na to, że ma ona dwa zadania: przedłużenie
rezerwacji oraz obliczenie opłaty za przedłużenie. Wydaje się, że nazwa Reservation była-
by dobra, ale jest już używana w odniesieniu do wcześniejszej klasy.
Oto inna możliwość. Moglibyśmy postąpić odwrotnie. Zamiast wyodrębniać kod
znajdujący się w dużym okręgu, możemy wyodrębnić pozostały kod, tak jak pokazano
na rysunku 20.9.

Rysunek 20.9. Inne spojrzenie na klasę Reservation

Klasie, którą teraz wyodrębniamy, możemy nadać nazwę FeeCalculator. Taki zabieg
mógłby się sprawdzić, ale metoda getTotalFee musi wywoływać metodę getPrincipalFee
w klasie Reservation. Czy jednak rzeczywiście musi?
A gdybyśmy tak wywołali metodę getPrincipalFee w klasie Reservation, po czym
przekazali otrzymaną wartość do klasy FeeCalculator? Oto zarys takiego kodu:
public class Reservation
{
...
private FeeCalculator calculator = new FeeCalculator();
DOSTRZEGANIE ODPOWIEDZIALNOŚCI 265

private int getPrincipalFee() {


...
}

public Reservation(Customer customer, int duration,


int dailyRate, Date date) {
this.customer = customer;
this.duration = duration;
this.dailyRate = dailyRate;
this.date = date;
}

...
public void addFee(FeeRider fee) {
calculator.addFee(fee);
}

public getTotalFee() {
int baseFee = getPrincipalFee();
return calculator.getTotalFee(baseFee);
}
}

Nasza struktura będzie wyglądać jak na rysunku 20.10.

Rysunek 20.10. Klasa Reservation korzystająca z klasy FeeCalculator

Możemy nawet zastanowić się nad przeniesieniem metody getPrincipalFee do klasy


FeeCalculator, aby odpowiedzialność lepiej pasowała do nazwy klasy, ale biorąc pod uwagę
to, że metoda ta korzysta z wielu zmiennych w klasie Reservation, może jednak będzie le-
piej pozostawić ją tam, gdzie jest obecnie.
Schematy funkcjonalności świetnie sprawdzają się w roli narzędzia służącego do szu-
kania odrębnych odpowiedzialności w klasach. Możemy podejmować próby grupo-
wania funkcjonalności i sprawdzać, które klasy uda nam się wyodrębnić na podstawie ich
nazw. Oprócz pomagania w szukaniu odpowiedzialności schematy funkcjonalności po-
zwalają nam również dostrzec strukturę zależności w obrębie klasy, co może być równie
ważne jak sama odpowiedzialność, gdy decydujemy, co powinno zostać wyodrębnione.
W omawianym przykładzie istniały dwie silne grupy zmiennych i metod. Jedynym łączą-
cym je elementem była metoda getPrincipalFee wewnątrz metody getTotalFee. W sche-
macie funkcjonalności często możemy zobaczyć takie powiązania jako niewielkie grupy
linii łączących większe zestawy elementów. Nazywam je punktami zwężenia (190); w roz-
dziale 12., „Muszę dokonać wielu zmian w jednym miejscu. Czy powinienem pousu-
wać zależności we wszystkich klasach, których te zmiany dotyczą?”, piszę o nich więcej.
266 ROZDZIAŁ 20. TA KLASA JEST ZA DUŻA, A JA NIE CHCĘ, ŻEBY STAŁA SIĘ JESZCZE WIĘKSZA

Zdarza się, że kiedy narysujesz schemat, nie znajdziesz żadnych punktów zwężenia.
Nie zawsze one się tam znajdują, ale przynajmniej pomocne może okazać się zobaczenie
wszystkich nazw i zależności łączących funkcjonalności.
Kiedy masz rozrysowany schemat, możesz wypróbowywać różne sposoby usuwania
zależności. W tym celu zakreśl grupy funkcjonalności. Gdy to zrobisz, linie, które narysu-
jesz, mogą zdefiniować interfejs nowej klasy. Podczas rysowania postaraj się nadawać
nazwy klasom utworzonym przez każdą z grup. Jeśli mam być szczery, to nie biorąc
pod uwagę decyzji, które podejmiesz lub nie podczas wyodrębniania klas, mogę po-
wiedzieć, że jest to świetny sposób na udoskonalenie własnych umiejętności tworzenia
nazw; stanowi także niezłą metodę na poznanie alternatywnych projektów systemu.

Heurystyka nr 5. Poszukaj głównej odpowiedzialności


Postaraj się w jednym zdaniu opisać odpowiedzialność klasy.

Zasada pojedynczej odpowiedzialności mówi, że klasa zawsze powinna mieć jedną


odpowiedzialność. Jeśli mamy do czynienia z takim właśnie przypadkiem, to opisanie jej
w jednym zdaniu powinno być łatwe. Spróbuj to zrobić dla którejś z dużych klas w swoim
systemie. Kiedy będziesz zastanawiać się, czego potrzebują klienty tej klasy i jakie mają
wobec niej oczekiwania, będziesz dodawać do swojego zdania kolejne człony. Klasa robi to
i to, i to, i jeszcze tamto. Czy jest jakieś zadanie, które wydaje się ważniejsze od wszystkich
innych zadań? Jeśli tak, to być może udało Ci się znaleźć główną odpowiedzialność klasy.
Pozostałe odpowiedzialności prawdopodobnie powinny zostać przeniesione do innych klas.
Istnieją dwa sposoby naruszenia zasady pojedynczej odpowiedzialności. Można ją
złamać na poziomie interfejsu oraz na poziomie implementacji. ZPO zostaje naruszona na
poziomie interfejsu, gdy interfejs klasy sugeruje, że jest ona odpowiedzialna za bardzo wiele
różnych zadań. Na przykład z interfejsu klasy pokazanej na rysunku 20.11 wynika, że można
by ją rozbić na trzy lub cztery mniejsze klasy.

Rysunek 20.11. Klasa ScheduledJob


DOSTRZEGANIE ODPOWIEDZIALNOŚCI 267

Naruszenie ZPO, które najbardziej nas interesuje, ma miejsce na poziomie imple-


mentacji. Mówiąc otwarcie, chcemy wiedzieć, czy klasa rzeczywiście robi wszystko to,
czego oczekujemy, czy też deleguje swoje zadania do kilku innych klas. Jeśli tak, to nie
mamy do czynienia z obszerną, monolityczną klasą, tylko z fasadą — przykrywką dla
wielu mniejszych klas, którą można łatwiej zarządzać.
Rysunek 20.12 pokazuje klasę ScheduledJob razem z jej odpowiedzialnościami od-
delegowanymi do kilku innych klas.

Rysunek 20.12. Klasa ScheduledJob z wyodrębnionymi klasami

Zasada pojedynczej odpowiedzialności nadal pozostaje naruszona na poziomie


interfejsu, ale na poziomie interfejsu sprawy mają się nieco lepiej.
Jak moglibyśmy rozwiązać problem występujący na poziomie interfejsu? Będzie to
trochę trudne. Ogólne podejście polega na sprawdzeniu, czy któraś z klas, do której dele-
gujemy, może być używana bezpośrednio przez swoje klienty. Jeśli na przykład niektóre
klienty są zainteresowane uruchamianiem klasy ScheduledJob, moglibyśmy dokonać re-
faktoryzacji i otrzymać mniej więcej coś, co pokazano na rysunku 20.13.
Teraz klienty, których zajęciem jest wyłącznie kontrolowanie zadań, mogą przyjmować
obiekty klasy ScheduledJob jako obiekty JobControllers. Taka technika, polegająca na
utworzeniu interfejsu dla określonej grupy klientów, pozwala na zachowanie projektu
w zgodzie z zasadą rozdzielania interfejsów.
268 ROZDZIAŁ 20. TA KLASA JEST ZA DUŻA, A JA NIE CHCĘ, ŻEBY STAŁA SIĘ JESZCZE WIĘKSZA

Rysunek 20.13. Specyficzny dla klienta interfejs klasy ScheduledJob

Zasada rozdzielania interfejsów


Gdy klasa jest obszerna, rzadko kiedy wszystkie jej klienty korzystają ze wszystkich jej metod.
Często możemy dostrzec różne grupy metod używanych przez poszczególne klienty. Jeśli
utworzymy interfejsy dla każdej z tych grup i zostaną one zaimplementowane w dużej klasie,
każdy z klientów będzie widział tę klasę poprzez swój interfejs. Takie rozwiązanie pomoże
nam ukryć informacje, a także zmniejszyć stopień zależności w systemie. Nie będzie już
potrzeby rekompilowania klientów przy okazji kompilowania dużej klasy.

Kiedy mamy do dyspozycji interfejsy dla określonego zbioru klientów, często możemy
rozpocząć przenoszenie kodu z dużej klasy do nowej klasy, która z niej korzysta, jak poka-
zano na rysunku 20.14.

Rysunek 20.14. Rozdzielanie interfejsu klasy ScheduledJob


INNE TECHNIKI 269

Zamiast delegować klasę ScheduledJob do obsługi interfejsu JobController, sprawiliśmy,


że JobController jest delegowany do ScheduledJob. Teraz, kiedy tylko klient zechce
uruchomić klasę ScheduledJob, tworzy obiekt klasy JobController, przekazuje go do klasy
ScheduledJob, po czym poprzez interfejs JobController obsługuje jej wykonanie się.
Taki rodzaj refaktoryzacji prawie zawsze jest trudniejszy do przeprowadzenia, niż się wy-
daje. Zwykle w tym celu będziesz musiał odkryć jeszcze więcej metod w interfejsie publicz-
nym wyjściowej klasy (ScheduledJob), dzięki czemu nowa fasada (StandardJobController)
będzie mieć dostęp do wszystkich elementów, które są jej potrzebne do pracy. Często
wprowadzenie takiej zmiany wiąże się z dużym nakładem pracy. Kod klientów musi
zostać zmieniony w taki sposób, żeby korzystał z nowej klasy zamiast ze starej. Aby taka
zmiana była bezpieczna, będą Ci potrzebne testy sprawdzające te klienty. Taki rodzaj
refaktoryzacji ma jednak swoją zaletę — umożliwia ograniczenie wielkości interfejsu dużej
klasy. Zwróć uwagę, że w klasie ScheduledJob nie ma już metod znajdujących się w inter-
fejsie JobController.

Heurystyka nr 6. Jeśli wszystko inne zawiodło,


przeprowadź szybką refaktoryzację
Jeżeli masz mnóstwo problemów z dostrzeżeniem odpowiedzialności w klasie, przeprowadź
szybką refaktoryzację.

Szybka refaktoryzacja (222) to wydajne narzędzie. Pamiętaj tylko, że efekty jej zasto-
sowania są sztuczne. Elementy, które widzisz, kiedy przeprowadzasz szybką refaktoryzację,
niekoniecznie będą elementami, jakie uzyskasz po dokonaniu „prawdziwej” refaktoryzacji.

Heurystyka nr 7. Skup się na bieżącej pracy


Zwracaj uwagę na to, co masz obecnie do zrobienia. Jeśli pracujesz nad innym sposobem na
wykonanie czegoś, być może udało Ci się zidentyfikować odpowiedzialność, którą powinieneś
wyodrębnić, a potem umożliwić jej zastąpienie.

Łatwo poczuć się przytłoczonym przez rozmaite odpowiedzialności, które udało Ci


się zidentyfikować w klasie. Pamiętaj, że zmiany, które akurat wprowadzasz, mówią Ci coś
o sposobie, w jaki może się zmienić system. Często już samo dostrzeżenie tego sposobu
wystarczy, aby uznać za odrębną odpowiedzialność nowy kod, który piszesz.

Inne techniki
Heurystyki służące do identyfikowania odpowiedzialności rzeczywiście mogą być pomocne
podczas wyszukiwania nowych abstrakcji w starych klasach, chociaż tak naprawdę są to
tylko sztuczki. Najlepszy sposób na udoskonalenie swoich umiejętności identyfikowania
270 ROZDZIAŁ 20. TA KLASA JEST ZA DUŻA, A JA NIE CHCĘ, ŻEBY STAŁA SIĘ JESZCZE WIĘKSZA

to poświęcenie większej ilości czasu na czytanie. Czytaj książki o wzorcach projektowych


i — co ważniejsze — czytaj kod autorstwa innych osób. Przyglądaj się projektom z otwar-
tych źródeł i poświęć trochę czasu na sprawdzenie, jak inni robią różne rzeczy. Zwracaj
uwagę na nazwy klas i na związek zachodzący między nazwami klas a nazwami metod.
Wraz z upływem czasu staniesz się lepszy w identyfikowaniu ukrytych odpowiedzialności
i po prostu zaczniesz je zauważać podczas przeglądania nieznanego Ci kodu.

Posuwanie się naprzód


Kiedy zidentyfikujesz już grupę różnych odpowiedzialności w dużej klasie, pozostaną
jeszcze dwa elementy, z którymi należy się zmierzyć: strategia i taktyka. Najpierw poroz-
mawiajmy o strategii.

Strategia
Co powinniśmy zrobić, kiedy już zidentyfikujemy wszystkie odrębne odpowiedzialności?
Czy mamy poświęcić tydzień i zabrać się za rozbijanie dużych klas istniejących w syste-
mie? Czy każdą z nich powinniśmy podzielić na małe fragmenty? Świetnie, jeśli masz na
to czas, ale rzadko tak bywa. Poza tym taki zabieg może być ryzykowny. W niemal każdym
przypadku, z jakim miałem do czynienia, kiedy zespół wpada w szał faktoryzacji, sys-
tem traci na krótką chwilę stabilność, nawet jeśli zmiany są wprowadzane ostrożnie i to-
warzyszą im testy. Jeżeli znajdujesz się na wczesnym etapie cyklu produkcyjnego, jesteś
gotów na podjęcie ryzyka i dysponujesz czasem, szał refaktoryzacji może przynieść wyniki.
Nie pozwól tylko, żeby błędy odwiodły Cię od przeprowadzenia pozostałej refaktoryzacji.
Najlepsze podejście do rozbicia dużej klasy polega na zidentyfikowaniu jej odpo-
wiedzialności, upewnieniu się, że wszyscy w zespole je zrozumieli, po czym rozbijaniu jej
w miarę potrzeb. Gdy tak postąpisz, rozciągniesz w czasie ryzyko związane z wprowa-
dzaniem zmian i w trakcie pracy będziesz mógł zająć się także innymi sprawami.

Taktyka
W przypadku większości cudzych systemów najlepsze, co możesz zrobić na początku, to
rozpocząć stosowanie zasady pojedynczej odpowiedzialności na poziomie implementacji
— po prostu wyodrębniaj klasy z dużych klas i deleguj do nich. Stosowanie ZPO na po-
ziomie interfejsu wymaga więcej pracy. Klienty Twojej klasy mogą ulec zmianie i konieczne
będzie poddanie ich testom. Szczęśliwie wprowadzenie ZPO na poziomie implementacji
ułatwia jej późniejsze stosowanie na poziomie interfejsu. Zajmijmy się jednak najpierw
przypadkiem związanym z implementacją.
Techniki, z których możesz korzystać w celu wyodrębniania klas, zależą od wielu
czynników. O zastosowaniu jednej z nich decyduje łatwość, z jaką można poddać testom
metody, na które wpłyną zmiany. W technice tej najpierw należy spojrzeć na klasę i wypisać
POSUWANIE SIĘ NAPRZÓD 271

wszystkie zmienne instancji oraz metody, które zamierzasz przenieść. Na tej podsta-
wie powinieneś uzyskać wiedzę co do metod, dla których należy napisać testy. Gdybyśmy
w przykładzie z klasą RuleParser, której przyglądaliśmy się wcześniej, zechcieli rozbić kla-
sę TermTokenizer, musielibyśmy przenieść pole tekstowe current, pole currentPosition
oraz metody hasMoreTerms i nextTerm. Fakt, że hasMoreTerms i nextTerm są metodami
prywatnymi, oznacza, że nie możemy bezpośrednio dla nich napisać testów. Moglibyśmy
je upublicznić (w końcu i tak zamierzamy je przenieść), ale równie łatwe mogłoby być
utworzenie klasy RuleParser w jarzmie testowym i przekazanie jej zestawu łańcuchów tek-
stowych do przetestowania. Jeśli tak postąpimy, otrzymamy testy obejmujące metody
hasMoreTerms oraz nextTerm i uzyskamy możliwość bezpiecznego ich przeniesienia do
nowej klasy.
Niestety, wiele dużych klas ma instancje trudne do utworzenia w jarzmie testowym.
W rozdziale 9., „Nie mogę umieścić tej klasy w jarzmie testowym”, znajdziesz podpowiedzi,
z których możesz skorzystać, aby posunąć się do przodu ze swoją pracą. Jeśli masz pro-
blemy z utworzeniem instancji klasy, będziesz mógł wykorzystać wskazówki zawarte
w rozdziale 10., „Nie mogę uruchomić tej metody w jarzmie testowym”, także w celu
rozmieszczenia testów na miejscu.
Jeśli masz możliwość utworzenia testów, od razu będziesz mógł rozpocząć wyodręb-
nianie klas, korzystając z refaktoryzacji metodą wyodrębniania klasy, opisaną przez
Martina Fowlera w książce Refactoring: Improving the Design of Existing Code (Addi-
son-Wesley 1999). Jeżeli jednak nie możesz umieścić testów na miejscu, nadal będziesz
mógł posuwać się naprzód, chociaż w nieco bardziej ryzykowny sposób. Jest to bardzo
ostrożne rozwiązanie i działa niezależnie od tego, czy masz do dyspozycji narzędzie do
refaktoryzacji. Oto czynności do wykonania:
1. Zidentyfikuj odpowiedzialność, którą chcesz wyodrębnić do innej klasy.
2. Sprawdź, czy do nowej klasy należy przenieść jakiekolwiek zmienne instancji. Jeśli
tak, przesuń je do oddzielnej części deklaracji klasy, z dala od pozostałych zmien-
nych instancji.
3. Jeśli istnieją całe metody, które chciałbyś przenieść do nowej klasy, wyodrębnij ciała
każdej z nich do nowych metod. Nazwy wszystkich metod powinny być takie same
jak nazwy starych metod, ale z niepowtarzalnym, pisanym wielkimi literami, wspól-
nym przedrostkiem, takim jak na przykład PRZENIES. Jeżeli podczas wyodrębniania
metod nie korzystasz z narzędzia refaktoryzującego, pamiętaj o zachowaniu sy-
gnatur (314). Każdą z wyodrębnionych metod umieść w odrębnej części deklaracji
klasy, obok zmiennych, które przenosisz.
4. Jeśli fragmenty pewnych metod powinny znaleźć się w innej klasie, wyodrębnij je
z metod źródłowych. W ich nazwach także zastosuj przedrostek PRZENIES i umieść
je w odrębnej sekcji.
5. W tym momencie powinieneś mieć dla swojej klasy oddzielną sekcję ze zmiennymi
instancji oraz grupą metod, które zamierzasz przenieść. Za pomocą funkcji wyszu-
kiwania tekstów odszukaj w kodzie bieżącą klasę oraz jej wszystkie podklasy, aby
272 ROZDZIAŁ 20. TA KLASA JEST ZA DUŻA, A JA NIE CHCĘ, ŻEBY STAŁA SIĘ JESZCZE WIĘKSZA

upewnić się, że żadne ze zmiennych, które chcesz przenieść, nie zostały użyte
poza metodami, które przenosisz. Ważne jest, aby na tym etapie nie korzystać ze
wsparcia kompilatora (317). W wielu zorientowanych obiektowo językach pro-
gramowania klasa pochodna może deklarować zmienne o takich samych nazwach
jak zmienne w klasie bazowej. Technika ta często jest nazywana zastępowaniem.
Jeśli Twoja klasa zastępuje jakiekolwiek zmienne, a inne zastosowania tych zmien-
nych przewijają się w kodzie, to ich przeniesienie może zmienić zachowanie
programu. Podobnie gdy w celu odszukania wystąpień zmiennej, która zastępuje
inną zmienną, skorzystasz ze wsparcia kompilatora (317), nie uda Ci się znaleźć
wszystkich miejsc, w których została ona użyta. Przekształcenie w komentarz
deklaracji zastąpionej zmiennej powoduje, że widoczna staje się tylko ta zmienna,
która ją zastępuje.
6. Na tym etapie możesz bezpośrednio do nowej klasy przenieść wszystkie zmienne
instancji oraz metody, które wyodrębniłeś. Utwórz instancję nowej klasy w starej
klasie i skorzystaj ze wsparcia kompilatora (317) w celu odszukania miejsc, w któ-
rych przenoszone metody powinny być wywoływane w nowej instancji zamiast
w starej klasie.
7. Kiedy już zakończysz przenoszenie i stwierdzisz, że kod się kompiluje, możesz
zacząć usuwać przedrostek PRZENIES ze wszystkich przeniesionych metod. Sko-
rzystaj ze wsparcia kompilatora (317) w celu przejścia do miejsc, w których
musisz zmienić nazwy.
Kroki w tej metodzie refaktoryzacji są dość skomplikowane, ale jeśli masz do czy-
nienia ze szczególnie złożonym kodem i musisz bezpiecznie wyodrębnić klasy bez prze-
prowadzania testów, są one konieczne.
Istnieje kilka rzeczy, które mogą pójść nie tak, kiedy wyodrębniasz klasy bez przepro-
wadzania testów. Najsubtelniejsze błędy, które możesz wprowadzić, są związane z dziedzi-
czeniem. Przenoszenie metod z jednej klasy do drugiej jest dość bezpieczne. Aby ułatwić
sobie pracę, możesz skorzystać ze wsparcia kompilatora (317), chociaż w większości
języków wszystko może się wydarzyć, gdy spróbujesz przenieść metodę, która przesłania
inną metodę. W takim przypadku obiekty wywołujące oryginalną klasę będą wywoły-
wać metodę o takiej samej nazwie w klasie bazowej. Podobna sytuacja może mieć miejsce
ze zmiennymi. Zmienna w podklasie może przesłaniać zmienną o takiej samej nazwie
w klasie nadrzędnej. Przesunięcie jej spowoduje uwidocznienie tylko tej zmiennej,
która była przesłonięta.
Aby obejść powyższe problemy, w ogóle nie przenosimy oryginalnych metod.
Tworzymy nowe metody, wyodrębniając ciała starych metod. Użycie przedrostka to tylko
mechaniczny sposób na wygenerowanie nowej nazwy i zagwarantowanie, że przed prze-
niesieniem nie będzie kolidować z innymi nazwami. Sprawa ze zmiennymi instancji
jest trochę bardziej skomplikowana — zanim skorzystamy ze zmiennych, musimy je
wyszukać ręcznie. Łatwo wtedy o pomyłkę. Bądź wówczas uważny i wykonaj tę czynność
razem z kolegą.
PO WYODRĘBNIENIU KLASY 273

Po wyodrębnieniu klasy
Wyodrębnianie klas z klasy większej często stanowi dobry pierwszy krok. W praktyce
największym zagrożeniem dla zespołów, które tak postępują, jest przerost ambicji. Być
może przeprowadziłeś szybką refaktoryzację (222) albo opracowałeś jakiś projekt
określający, jak system ma wyglądać. Powinieneś jednak pamiętać, że struktura istniejąca
w Twojej aplikacji działa. Wspiera określoną funkcjonalność i tylko może nie być przysto-
sowana do zmierzania naprzód. Czasami najlepsze, co możesz wówczas zrobić, to po-
kazać, jak duża klasa będzie wyglądać po refaktoryzacji, po czym po prostu o tym za-
pomnieć. Wykonałeś to tylko w celu pokazania, co jest możliwe. Aby posunąć się naprzód,
musisz być wrażliwy na to, co znajduje się w kodzie, i zmierzać niekoniecznie w stronę
idealnego projektu, ale przynajmniej w lepszym kierunku.
274 ROZDZIAŁ 20. TA KLASA JEST ZA DUŻA, A JA NIE CHCĘ, ŻEBY STAŁA SIĘ JESZCZE WIĘKSZA
Rozdział 21.

Wszędzie zmieniam
ten sam kod

Może to być najbardziej frustrujące doświadczenie w cudzych systemach. Masz do wpro-


wadzenia zmianę i myślisz sobie: „Aha, to tylko tyle”. Potem odkrywasz, że tę samą
zmianę musisz wprowadzać wciąż na nowo, ponieważ w systemie znajduje się kilkanaście
miejsc z podobnym kodem. Może Cię ogarnąć poczucie, że gdybyś tylko ponownie
zaprojektował albo zrestrukturyzował ten system, nie miałbyś tego problemu, ale kto
miałby czas na coś takiego? Pozostawiasz zatem ten bolesny punkt w systemie; coś, co
tylko wnosi swój wkład w jego ogólną paskudność.
Jeśli coś wiesz o refaktoryzacji, Twoja pozycja jest lepsza. Masz świadomość, że
usuwanie powielonego kodu nie musi być wielkim wysiłkiem, takim jak przeprojek-
towanie albo zmiana architektury systemu. Można je przeprowadzić małymi krokami,
podczas wykonywania swojej pracy. Wraz z upływem czasu system stanie się lepszy, o ile
nikt nie zacznie za Twoimi plecami wprowadzać duplikacji. W takim przypadku mógłbyś
podjąć wobec nich kroki bez uciekania się do fizycznej przemocy, ale to już inna kwestia.
Najważniejsze jest pytanie, czy warto. Co uzyskamy, gdy gorliwie zabierzemy się do eli-
minowania duplikacji z pewnego fragmentu kodu? Wyniki będą zaskakujące. Spójrzmy
na przykład.
Mamy niewielki, bazujący na Javie system sieciowy, w którym wysyłamy instrukcje do
serwera. Dwa polecenia, którymi się posługujemy, to AddEmployeeCmd oraz LogonCommand.
Kiedy musimy wydać jedno z nich, tworzymy jego instancję i przekazujemy strumień
wyjściowy do jego metody write.
Oto listingi tych dwóch klas z poleceniami. Czy widzisz powielony kod?
import java.io.OutputStream;

public class AddEmployeeCmd {


String name;
String address;
String city;
276 ROZDZIAŁ 21. WSZĘDZIE ZMIENIAM TEN SAM KOD

String state;
String yearlySalary;
private static final byte[] header = {(byte)0xde, (byte)0xad};
private static final byte[] commandChar = {0x02};
private static final byte[] footer = {(byte)0xbe, (byte)0xef};
private static final int SIZE_LENGTH = 1;
private static final int CMD_BYTE_LENGTH = 1;

private int getSize() {


return header.length +
SIZE_LENGTH +
CMD_BYTE_LENGTH +
footer.length +
name.getBytes().length + 1 +
address.getBytes().length + 1 +
city.getBytes().length + 1 +
state.getBytes().length + 1 +
yearlySalary.getBytes().length + 1;
}

public AddEmployeeCmd(String name, String address,


String city, String state,
int yearlySalary) {
this.name = name;
this.address = address;
this.city = city;
this.state = state;
this.yearlySalary = Integer.toString(yearlySalary);
}

public void write(OutputStream outputStream)


throws Exception {
outputStream.write(header);
outputStream.write(getSize());
outputStream.write(commandChar);
outputStream.write(name.getBytes());
outputStream.write(0x00);
outputStream.write(address.getBytes());
outputStream.write(0x00);
outputStream.write(city.getBytes());
outputStream.write(0x00);
outputStream.write(state.getBytes());
outputStream.write(0x00);
outputStream.write(yearlySalary.getBytes());
outputStream.write(0x00);
outputStream.write(footer);
}
}

import java.io.OutputStream;

public class LoginCommand {


private String userName;
private String passwd;
private static final byte[] header
PIERWSZE KROKI 277

= {(byte)0xde, (byte)0xad};
private static final byte[] commandChar = {0x01};
private static final byte[] footer
= {(byte)0xbe, (byte)0xef};
private static final int SIZE_LENGTH = 1;
private static final int CMD_BYTE_LENGTH = 1;

public LoginCommand(String userName, String passwd) {


this.userName = userName;
this.passwd = passwd;
}

private int getSize() {


return header.length + SIZE_LENGTH + CMD_BYTE_LENGTH +
footer.length + userName.getBytes().length + 1 +
passwd.getBytes().length + 1;
}

public void write(OutputStream outputStream)


throws Exception {
outputStream.write(header);
outputStream.write(getSize());
outputStream.write(commandChar);
outputStream.write(userName.getBytes());
outputStream.write(0x00);
outputStream.write(passwd.getBytes());
outputStream.write(0x00);
outputStream.write(footer);
}
}

Rysunek 21.1 pokazuje te klasy na diagramie UML.

Rysunek 21.1. Klasy AddEmployeeCmd i LoginCommand

Wygląda na to, że jest tu sporo powielonego kodu, ale co z tego? Kodu nie jest dużo.
Moglibyśmy przeprowadzić jego refaktoryzację, pozbyć się duplikacji i spowodować,
że będzie zajmował mniej miejsca, ale czy ułatwi nam to życie? Może tak, a może nie.
Trudno to stwierdzić, patrząc tylko na kod.
278 ROZDZIAŁ 21. WSZĘDZIE ZMIENIAM TEN SAM KOD

Spróbujmy zidentyfikować powielone fragmenty i je usunąć — zobaczymy, co nam


to da. Zdecydujemy wtedy, czy zlikwidowanie duplikacji rzeczywiście się nam na coś
przydało.
Pierwszą czynnością, jaką musimy wykonać, jest opracowanie testów, które będziemy
uruchamiać po każdej refaktoryzacji. Pominiemy je w naszym opisie, ale pamiętaj, że one
tam są.

Pierwsze kroki
Moją pierwszą reakcją, gdy mam do czynienia z duplikacją, jest odsunięcie się o krok do
tyłu, aby uzyskać lepszy widok na jej cały zakres. Kiedy już to zrobię, zaczynam zastana-
wiać się, jakie klasy ostatecznie uzyskam i jak będą wyglądać wyodrębnione fragmenty
powielonego kodu. Wtedy zdaję sobie sprawę z tego, że tak naprawdę za dużo myślę.
Usunięcie małych fragmentów powielonego kodu pomaga i ułatwia późniejsze dostrzeże-
nie dużych obszarów duplikacji. Na przykład w metodzie write klasy LoginCommand
występuje następujący kod:
outputStream.write(userName.getBytes());
outputStream.write(0x00);
outputStream.write(passwd.getBytes());
outputStream.write(0x00);

Kiedy wypisujemy łańcuch tekstowy, wysyłamy także pusty znak końca (0x00). Po-
wielenie takie możemy wyodrębnić następująco. Tworzymy metodę o nazwie writeField,
która przyjmuje łańcuchy tekstowe oraz strumień wyjściowy. Metoda ta zapisuje do
strumienia łańcuch tekstowy i kończy go pustym znakiem.
void writeField(OutputStream outputStream, String field) {
outputStream.write(field.getBytes());

outputStream.write(0x00);
}

Podjęcie decyzji, gdzie zacząć


Kiedy przeprowadzamy serię refaktoryzacji mających na celu usunięcie powielonego kodu,
możemy uzyskać różne struktury, w zależności od miejsca, w którym zaczęliśmy. Wyobraź
sobie na przykład, że mamy następującą metodę:
void c() { a(); a(); b(); a(); b(); b(); }

Można ją przekształcić tak:


void c() { aa(); b(); a(); bb(); }

albo tak:
void c() { a(); ab(); ab(); b(); }
PIERWSZE KROKI 279

Który sposób powinniśmy wybrać? Prawda jest taka, że pod względem struktury nie ma tu
żadnej różnicy. Oba grupowania są lepsze niż na początku i w razie potrzeby można prze-
prowadzić ich refaktoryzację. Nasza decyzja nie jest ostateczna. Kiedy podejmuję decyzje,
zwracam uwagę na nazwy, których użyję. Jeśli potrafię wskazać nazwę dla dwóch powtó-
rzonych wywołań a(), która ma w danym kontekście większy sens niż nazwa dla wywoła-
nia a(), po której następuje wywołanie b(), to korzystam z tej właśnie nazwy.
Inna heurystyka, z której korzystam, to rozpoczęcie od małych kroków. Jeżeli mogę usunąć
niewielkie fragmenty powielonego kodu, robię to na samym początku, ponieważ w ten
sposób często uzyskuję czytelniejszy obraz całości.

Kiedy mamy już tę metodę, możemy rozpocząć zastępowanie każdej pary poleceń
zapisujących łańcuch tekstowy i znak pusty, uruchamiając przy tym okresowo testy, aby
upewnić się, czy niczego nie popsuliśmy. Oto metoda write klasy LoginCommand po zmianie:
public void write(OutputStream outputStream)
throws Exception {
outputStream.write(header);
outputStream.write(getSize());
outputStream.write(commandChar);
writeField(outputstream, username);
writeField(outputStream, passwd);
outputStream.write(footer);
}

W ten sposób pozbyliśmy się problemu z klasą LoginCommand, ale w niczym nie pomo-
gło nam to z klasą AddEmployeeCmd. W jej metodzie write także występują podobne,
powtarzające się sekwencje zapisujące wartości tekstowe i znak pusty. Ponieważ obie
klasy są instrukcjami, moglibyśmy wprowadzić nadrzędną wobec nich klasę o nazwie
Command. Gdy już ją utworzymy, będziemy mogli umieścić w niej metodę writeField,
dzięki czemu będą z niej mogły korzystać obie klasy, co pokazano na rysunku 21.2.

Rysunek 21.2. Hierarchia klasy Command

Teraz możemy powrócić do metody AddsEmployeeCmd i zastąpić w niej operacje za-


pisywania łańcucha tekstowego oraz znaku pustego wywołaniami metody writeField.
Gdy skończymy, metoda Write w klasie AddEmployeeCmd będzie wyglądać następująco:
public void write(OutputStream outputStream)
throws Exception {
280 ROZDZIAŁ 21. WSZĘDZIE ZMIENIAM TEN SAM KOD

outputStream.write(header);
outputStream.write(getSize());
outputStream.write(commandChar);
writeField(outputStream, name);
writeField(outputStream, address);
writeField(outputStream, city);
writeField(outputStream, state);
writeField(outputStream, yearlySalary);
outputStream.write(footer);
}

Metoda write w klasie LoginCommand będzie wyglądać tak:


public void write(OutputStream outputStream)
throws Exception {
outputStream.write(header);
outputStream.write(getSize());
outputStream.write(commandChar);
writeField(outputstream, userName);
writeField(outputStream, passwd);
outputStream.write(footer);
}

Kod jest już trochę czytelniejszy, ale jeszcze nie skończyliśmy. Metody write klas
AddEmployeeCmd i LoginCommand mają taką samą postać: wypisanie nagłówka, rozmiaru
oraz łańcucha tekstowego z poleceniem, następnie wypisanie kilku różnych pól i na koniec
stopki. Jeśli uda nam się wyodrębnić różnicę występującą między nimi, czyli wypisywanie
pól, to metoda write w klasie LoginCommand będzie wyglądać następująco:
public void write(OutputStream outputStream)
throws Exception {
outputStream.write(header);
outputStream.write(getSize());
outputStream.write(commandChar);
writeBody(outputstream);
outputStream.write(footer);
}

Oto wyodrębniona metoda writeBody:


private void writeBody(OutputStream outputStream)
throws Exception {
writeField(outputstream, userName);
writeField(outputStream, passwd);
}

Metoda write w klasie AddEmployeeCmd jest dokładnie taka sama, ale jej metoda
writeBody wygląda tak:
private void writeBody(OutputStream outputStream) throws Exception {
writeField(outputStream, name);
writeField(outputStream, address);
writeField(outputStream, city);
writeField(outputStream, state);
writeField(outputStream, yearlySalary);
}
PIERWSZE KROKI 281

Jeżeli dwie metody wyglądają mniej więcej tak samo, wyodrębnij różnice do innych metod.
Gdy tak postąpisz, często uzyskasz dwie identyczne metody i będziesz mógł pozbyć się
jednej z nich.

Metody write obu klas wyglądają dokładnie tak samo. Czy moglibyśmy przenieść
metodę write do klasy Command? Jeszcze nie. Nawet jeśli obie metody write wyglądają
identycznie, to jednak korzystają z danych header, footer i commandChar pochodzących z ich
własnych klas. Gdybyśmy chcieli napisać jedną metodę write, w celu pobierania danych
musiałaby ona wywoływać metody z odpowiednich podklas. Przyjrzyjmy się zmiennym
w klasach AddEmployeeCmd i LoginCommand:
public class AddEmployeeCmd extends Command {
String name;
String address;
String city;
String state;
String yearlySalary;
private static final byte[] header
= {(byte)0xde, (byte)0xad};
private static final byte[] commandChar = {0x02};
private static final byte[] footer
= {(byte)0xbe, (byte)0xef};
private static final int SIZE_LENGTH = 1;
private static final int CMD_BYTE_LENGTH = 1;
...
}

public class LoginCommand extends Command {


private String userName;
private String passwd;

private static final byte[] header


= {(byte)0xde, (byte)0xad};
private static final byte[] commandChar = {0x01};
private static final byte[] footer
= {(byte)0xbe, (byte)0xef};
private static final int SIZE_LENGTH = 1;
private static final int CMD_BYTE_LENGTH = 1;
...
}

Obie te klasy mają wiele wspólnych danych. Do klasy Command możemy przenieść
zmienne header, footer, SIZE_LENGTH i CMD_BYTE_LENGTH, ponieważ mają te same wartości.
Chwilowo zadeklaruję je jako chronione, żebyśmy mogli je zrekompilować i przetestować:
public class Command {
protected static final byte[] header
= {(byte)0xde, (byte)0xad};
protected static final byte[] footer
= {(byte)0xbe, (byte)0xef};
protected static final int SIZE_LENGTH = 1;
282 ROZDZIAŁ 21. WSZĘDZIE ZMIENIAM TEN SAM KOD

protected static final int CMD_BYTE_LENGTH = 1;


...
}

Teraz w obu podklasach pozostała już tylko zmienna commandChar. W każdej z nich ma
ona inną wartość. Prosty sposób na poradzenie sobie z taką sytuacją polega na dodaniu
do klasy Command abstrakcyjnego gettera:
public class Command {
protected static final byte[] header
= {(byte)0xde, (byte)0xad};
protected static final byte[] footer
= {(byte)0xbe, (byte)0xef};
protected static final int SIZE_LENGTH = 1;
protected static final int CMD_BYTE_LENGTH = 1;
protected abstract char [] getCommandChar();
...
}

Możemy teraz zastąpić zmienne commandChar w każdej podklasie przesłaniającymi je


funkcjami getCommandChar:
public class AddEmployeeCmd extends Command {
protected char [] getCommandChar() {
return new char [] { 0x02};
}
...
}

public class LoginCommand extends Command {


protected char [] getCommandChar() {
return new char [] { 0x01};
}
...
}

Teraz możemy przenieść metodę write. Kiedy już to zrobimy, otrzymamy klasę
Command, która wygląda następująco:
public class Command {
protected static final byte[] header
= {(byte)0xde, (byte)0xad};
protected static final byte[] footer
= {(byte)0xbe, (byte)0xef};
protected static final int SIZE_LENGTH = 1;
protected static final int CMD_BYTE_LENGTH = 1;

protected abstract char [] getCommandChar();

protected abstract void writeBody(OutputStream outputStream);

protected void writeField(OutputStream outputStream,


String field) {
outputStream.write(field.getBytes());
outputStream.write(0x00);
PIERWSZE KROKI 283

public void write(OutputStream outputStream)


throws Exception {
outputStream.write(header);
outputStream.write(getSize());
outputStream.write(commandChar);
writeBody(outputstream);
outputStream.write(footer);
}
}

Zwróć uwagę, że musieliśmy wprowadzić abstrakcyjną metodę writeBody i także


umieścić ją w klasie Command, jak widać na rysunku 21.3.

Rysunek 21.3. Przeniesienie metody writeField

Jedyne, co po przeniesieniu metody write pozostaje w każdej podklasie, to metody


getSize, getCommandChar i konstruktory. Spójrzmy jeszcze raz na klasę LoginCommand:
public class LoginCommand extends Command {
private String userName;
private String passwd;

public LoginCommand(String userName, String passwd) {


this.userName = userName;
this.passwd = passwd;
}

protected char [] getCommandChar() {


return new char [] { 0x01};
}

protected int getSize() {


return header.length + SIZE_LENGTH + CMD_BYTE_LENGTH +
footer.length + userName.getBytes().length + 1 +
passwd.getBytes().length + 1;
}
}

Jest to raczej niewielka klasa. AddEmployeeCmd wygląda podobnie. Zawiera ona metody
getSize i getCommandChar i niewiele więcej. Przyjrzyjmy się bliżej obu metodom getSize.
284 ROZDZIAŁ 21. WSZĘDZIE ZMIENIAM TEN SAM KOD

Oto ta metoda w klasie LoginCommand:


protected int getSize() {
return header.length + SIZE_LENGTH +
CMD_BYTE_LENGTH + footer.length +
userName.getBytes().length + 1 +
passwd.getBytes().length + 1;
}

A oto ta sama metoda w klasie AddEmployeeCmd:


private int getSize() {
return header.length + SIZE_LENGTH +
CMD_BYTE_LENGTH + footer.length +
name.getBytes().length + 1 +
address.getBytes().length + 1 +
city.getBytes().length + 1 +
state.getBytes().length + 1 +
yearlySalary.getBytes().length + 1;
}

Co w nich jest takie samo, a co inne? Wygląda na to, że obie dodają do siebie zmienne
header.length, SIZE_LENGTH, CMD_BYTE_LENGTH i footer.length. Następnie dodają rozmiary
każdego z pól. A może byśmy tak wyodrębnili to, co jest obliczane na inne sposoby: roz-
miary pól? Otrzymaną metodę nazwiemy getBodySize().
private int getSize() {
return header.length + SIZE_LENGTH
+ CMD_BYTE_LENGTH + footer.length + getBodySize();
}

Jeśli to zrobimy, uzyskamy taki sam kod w każdej metodzie. Dodajemy do siebie
wielkości wszystkich zapisanych danych, po czym dodajemy rozmiar ciała, czyli sumę
rozmiarów wszystkich pól. Gdy już to zrobimy, będziemy mogli przenieść metodę getSize
do klasy Command i uzyskać różne implementacje metody getBodySize w każdej podklasie,
co pokazano na rysunku 21.4.

Rysunek 21.4. Przeniesienie metody getSize


PIERWSZE KROKI 285

Zobaczmy, gdzie znajdujemy się teraz. Mamy następującą implementację metody


getBody w klasie AddEmployeeCmd:
protected int getBodySize() {
return name.getBytes().length + 1 +
address.getBytes().length + 1 +
city.getBytes().length + 1 +
state.getBytes().length + 1 +
yearlySalary.getBytes().length + 1;
}

Zignorowaliśmy tutaj dość ewidentną duplikację. Nie jest zbyt wielka, ale wykażmy się
gorliwością i usuńmy ją całkowicie:
protected int getFieldSize(String field) {
return field.getBytes().length + 1;
}

protected int getBodySize() {


return getFieldSize(name) +
getFieldSize(address) +
getFieldSize(city) +
getFieldSize(state) +
getFieldSize(yearlySalary);
}

Jeśli przeniesiemy metodę getFieldSize do klasy Command, będziemy mogli z niej


skorzystać także w metodzie getBodySize klasy LoginCommand:
protected int getBodySize() {
return getFieldSize(name) + getFieldSize(password);
}

Czy pozostała tu jeszcze jakaś duplikacja? W zasadzie tak, ale jest jej bardzo mało.
Klasy LoginCommand i AddEmployeeCmd przyjmują listę parametrów, pobierają ich wielkości,
po czym je wypisują. Pozostała już tylko zmienna commandChar, która odpowiada za wszyst-
kie pozostałe różnice między dwoma klasami. A gdybyśmy tak usunęli duplikację, ge-
neralizując trochę tę zmienną? Jeśli zadeklarujemy listę w klasie bazowej, będziemy mogli
w każdej podklasie dodać do niej konstruktor w następujący sposób:
class LoginCommand extends Command
{
...
public AddEmployeeCmd(String name, String password) {
fields.add(name);
fields.add(password);
}
...
}

Gdy w każdej z podklas dodajemy elementy do listy fields, możemy skorzystać z tego
samego kodu, aby otrzymać rozmiar ciała:
286 ROZDZIAŁ 21. WSZĘDZIE ZMIENIAM TEN SAM KOD

int getBodySize() {
int result = 0;
for(Iterator it = fields.iterator(); it.hasNext(); ) {
String field = (String)it.next();
result += getFieldSize(field);
}
return result;
}

Podobnie metoda writeBody może wyglądać tak:


void writeBody(Outputstream outputstream) {
for(Iterator it = fields.iterator(); it.hasNext(); ) {
String field = (String)it.next();
writeField(outputStream, field);
}
}

Możemy przenieść powyższe metody do klasy nadrzędnej. Kiedy to zrobimy, rze-


czywiście pozbędziemy się powielonego kodu w całości. Oto jak będzie wyglądać klasa
Command. Aby nasz zabieg miał większy sens, zadeklarujemy jako prywatne wszystkie
metody, które nie są już wywoływane w podklasach.
public class Command {
private static final byte[] header
= {(byte)0xde, (byte)0xad};
private static final byte[] footer
= {(byte)0xbe, (byte)0xef};
private static final int SIZE_LENGTH = 1;
private static final int CMD_BYTE_LENGTH = 1;
protected List fields = new ArrayList();
protected abstract char [] getCommandChar();

private void writeBody(Outputstream outputstream) {


for(Iterator it = fields.iterator(); it.hasNext(); ) {
String field = (String)it.next();
writeField(outputStream, field);
}
}

private int getFieldSize(String field) {


return field.getBytes().length + 1;
}

private int getBodySize() {


int result = 0;
for(Iterator it = fields.iterator(); it.hasNext(); ) {
String field = (String)it.next();
result += getFieldSize(field);
}
return result;
}

private int getSize() {


return header.length + SIZE_LENGTH
PIERWSZE KROKI 287

+ CMD_BYTE_LENGTH + footer.length
+ getBodySize();
}

private void writeField(OutputStream outputStream,


String field) {
outputStream.write(field.getBytes());
outputStream.write(0x00);
}

public void write(OutputStream outputStream)


throws Exception {
outputStream.write(header);
outputStream.write(getSize());
outputStream.write(commandChar);
writeBody(outputstream);
outputStream.write(footer);
}
}

Klasy LoginCommand i AddEmployeeCmd są teraz niewiarygodnie małe:


public class LoginCommand extends Command {
public LoginCommand(String userName, String passwd) {
fields.add(username);
fields.add(passwd);
}

protected char [] getCommandChar() {


return new char [] { 0x01};
}
}

public class AddEmployeeCmd extends Command {


public AddEmployeeCmd(String name, String address,
String city, String state,
int yearlySalary) {
fields.add(name);
fields.add(address);
fields.add(city);
fields.add(state);
fields.add(Integer.toString(yearlySalary));
}

protected char [] getCommandChar() {


return new char [] { 0x02 };
}
}

Rysunek 21.5 przedstawia diagram UML pokazujący ostateczny efekt naszej pracy.
No dobra, gdzie zatem jesteśmy? Usunęliśmy tyle powielonego kodu, że zostały nam
praktycznie skorupy klas. Cała funkcjonalność znajduje się w klasie Command. Tak naprawdę
warto zastanowić się, czy rzeczywiście potrzebujemy oddzielnych klas dla tych dwu
poleceń. Czy istnieją jakieś inne możliwości?
288 ROZDZIAŁ 21. WSZĘDZIE ZMIENIAM TEN SAM KOD

Rysunek 21.5. Hierarchia klasy Command z usuniętą duplikacją

Moglibyśmy pozbyć się podklas i dodać do klasy Command metodę statyczną, która
umożliwi nam wysłanie polecenia:
List arguments = new ArrayList();
arguments.add("Mike");
arguments.add("asdsad");
Command.send(stream, 0x01, arguments);

Taki zabieg oznaczałby jednak sporo pracy do wykonania przez klienty. Jedno jest
pewne: istotnie musimy wysyłać dwa różne znaki z poleceniami i nie chcemy, aby użyt-
kownik był zmuszony je rejestrować.
Zamiast tego moglibyśmy dodać różne metody statyczne dla każdego z poleceń, które
chcemy wysłać:
Command.SendAddEmployee(stream,
"Mike", "122 Elm St", "Miami", "FL", 10000);

Command.SendLogin(stream, "Mike", "asdsad");

Rozwiązanie takie wymusiłoby jednak zmianę kodu we wszystkich naszych klientach.


Obecnie istnieje wiele miejsc w kodzie, gdzie konstruowane są obiekty klas AddEmployeeCMD
i LoginCommand.
Może postąpimy lepiej, pozostawiając kod taki, jakim jest teraz. To prawda, że klasy są
dość małe, ale czy to kogoś boli? Chyba nie.
Czy to już wszystko? Istnieje jeszcze coś, co musimy zrobić — coś, co powinniśmy
zrobić wcześniej. Możemy zmienić nazwę klasy z AddEmployeeCmd na AddEmployeeCommand.
Dzięki temu nazwy dwóch podklas będą spójne. Jeśli będziemy konsekwentni w stosowa-
niu nazw, z mniejszym prawdopodobieństwem będziemy popełniać błędy.
PIERWSZE KROKI 289

Skróty
Skróty w nazwach klas i metod są problematyczne. Wszystko może być w porządku, jeśli są
używane konsekwentnie, ale zwykle nie lubię ich stosować.
Jeden z zespołów, z którymi współpracowałem, próbował używać słów kierownik i kierowanie
w prawie każdej nazwie klasy w systemie. Przyjęta w nim konwencja nazewnicza nie była
zbyt pomocna, a sprawy pogarszało jeszcze skracanie tych słów na wszystkie możliwe spo-
soby. Niektóre klasy były na przykład nazwane XXXXKrw, a inne XXXXKrwn. Kiedy już byłem
gotowy do użycia jakiejś klasy, najczęściej musiałem ją odszukać, aby przekonać się, czy
zastosowałem poprawną nazwę. Myliłem się w połowie przypadków, gdy próbowałem od-
gadywać, czy określony przyrostek był właściwy dla danej klasy.

Pozbyliśmy się zatem całej duplikacji. Czy sprawy mają się teraz lepiej, czy gorzej?
Odegrajmy kilka scenariuszy. Co się stanie, gdy będziemy musieli dodać nowe polecenie?
Moglibyśmy po prostu utworzyć podklasę klasy Command. Porównajmy takie rozwiązanie
z pracą, którą musielibyśmy wykonać w wyjściowym systemie. Dodalibyśmy nowe pole-
cenie, po czym za pomocą kopiowania i wklejania kodu z innego polecenia utworzyli-
byśmy nowy kod, zmieniając przy tym każdą z jego zmiennych. Gdybyśmy tak postąpili,
wprowadzilibyśmy jeszcze więcej zduplikowanego kodu, co tylko pogorszyłoby sprawy.
Ponadto takie rozwiązanie sprzyja powstawaniu błędów. Moglibyśmy nie połapać się
w sposobie użycia zmiennych i zrobić to źle. Nie ma wątpliwości, że dodanie nowego po-
lecenia zabrałoby nam więcej czasu przed usunięciem powielonego kodu.
Czy z powodu tego, co zrobiliśmy, utraciliśmy elastyczność? A gdybyśmy tak musieli
wysyłać polecenia, które składają się z czegoś innego niż tylko łańcuchy tekstowe?
W pewnym sensie rozwiązaliśmy już ten problem. Klasa AddEmployeeCommand przyjmuje
także liczby całkowite; aby można było wysyłać je jako polecenia, są konwertowane na
łańcuchy tekstowe. To samo możemy zrobić z jakimkolwiek innym typem. Musimy
tylko w jakiś sposób dokonać jego konwersji, zanim go wyślemy. Będzie to możliwe
w konstruktorze nowej podklasy.
A co, jeśli dostaniemy polecenie o innym formacie? Załóżmy, że będzie nam potrzebny
nowy rodzaj polecenia, który w swojej treści może zawierać inne polecenia. Z łatwością
możemy rozwiązać taki problem, tworząc podklasę klasy Command i przesłaniając jej metodę
writeBody:
public class AggregateCommand extends Command
{
private List commands = new ArrayList();
protected char [] getCommandChar() {
return new char [] { 0x03 };
}

public void appendCommand(Command newCommand) {


commands.add(newCommand);
}
290 ROZDZIAŁ 21. WSZĘDZIE ZMIENIAM TEN SAM KOD

protected void writeBody(OutputStream out) {


out.write(commands.getSize());
for(Iterator it = commands.iterator(); it.hasNext(); ) {
Command innerCommand = (Command)it.next();
innerCommand.write(out);
}
}
}

Wszystko inne po prostu działa.


Wyobraź sobie, że musielibyśmy to zrobić, nie mając usuniętej duplikacji.
Ostatni przykład zwraca naszą uwagę na coś bardzo ważnego. Kiedy usuniesz powie-
lony kod z klas, uzyskasz bardzo małe i skupione metody. Każda z nich robi coś, czym
nie zajmuje się żadna inna metoda. Dzięki temu odnosimy ogromną korzyść, jaką jest
ortogonalność.
Ortogonalność to wyszukane słowo na oznaczenie niezależności. Jeśli chcesz zmienić
istniejące zachowanie w swoim kodzie i jest w nim dokładnie jedno miejsce, do którego
musisz przejść, aby to zrobić, masz do czynienia z ortogonalnością. To tak, jakby Twoja
aplikacja była dużym pudełkiem z wystającymi na zewnątrz gałkami. Jeśli w systemie
istnieje tylko jedna gałka odpowiadająca za jedno zachowanie, zmiany będą łatwe do
wprowadzenia. Kiedy w kodzie szerzy się duplikacja, za jedno zachowanie odpowiada
wiele gałek. Pomyśl o zapisywaniu pól. Gdybyś w wyjściowym projekcie musiał użyć
znaku końca pola 0x01 zamiast 0x00, przyszłoby Ci przejrzeć cały kod i dokonać zmian
w wielu miejscach. Wyobraź sobie, że ktoś poprosił nas o wypisywanie dwóch znaków
0x00 na końcu każdego pola. Z tym też byłoby raczej kiepsko — nie mamy jednozada-
niowych gałek. Jednak w kodzie, w którym przeprowadziliśmy refaktoryzację, możemy
dokonać edycji lub też przesłonić metodę writeBody za każdym razem, gdy będziemy
musieli obsłużyć przypadki specjalne, takie jak agregacja poleceń. Kiedy zachowanie
znajduje się w pojedynczej metodzie, łatwo je zastąpić albo dodać do niego nowe elementy.
W naszym przykładzie robiliśmy wiele rzeczy — przenosiliśmy metody i zmienne
z klasy do klasy, rozbijaliśmy metody — większość z tego była zabiegami czysto mecha-
nicznymi. Zwracaliśmy po prostu uwagę na powielony kod i go usuwaliśmy. Jedyną
twórczą czynnością, którą tak naprawdę wykonaliśmy, było nadanie nazw nowym meto-
dom. W oryginalnym kodzie nie było koncepcji pola czy też ciała polecenia, chociaż
w pewnym sensie znajdowały się one w kodzie. Na przykład niektóre zmienne były
traktowane inaczej — nazywaliśmy je polami. Pod koniec całego procesu uzyskaliśmy
o wiele przyjemniejszy, ortogonalny projekt, chociaż nie mieliśmy poczucia, że coś pro-
jektujemy. Bardziej było tak, jakbyśmy zwracali uwagę na to, co się w nim znajduje,
i starali się przybliżyć kod do jego właściwej istoty.
Jedną z zaskakujących rzeczy, które odkrywasz podczas gorliwego usuwania powielo-
nego kodu, jest wyłaniający się projekt. Nie musisz planować rozmieszczenia większości
gałek w swojej aplikacji — one po prostu się pojawiają. Projekt nie jest idealny. Byłoby na
przykład lepiej, gdyby następująca metoda w klasie Command:
PIERWSZE KROKI 291

public void write(OutputStream outputStream)


throws Exception {
outputStream.write(header);
outputStream.write(getSize());
outputStream.write(commandChar);
writeBody(outputstream);
outputStream.write(footer);
}

wyglądała tak:
public void write(OutputStream outputStream)
throws Exception {
writeHeader(outputStream);
writeBody(outputstream);
writeFooter(outputStream);
}

Teraz mamy gałkę odpowiedzialną za wypisywanie nagłówków i gałkę odpowiedzialną


za stopki. Możemy dodawać gałki, kiedy będą potrzebne, ale miło jest, kiedy dzieje się to
w naturalny sposób.
Usuwanie duplikacji to wydajny sposób na przedestylowanie projektu. Proces ten nie
tylko sprawia, że projekt staje się bardziej elastyczny, ale także ułatwia i przyspiesza
wprowadzanie zmian.

Zasada otwarte-zamknięte
Zasada otwarte-zamknięte jest regułą wyrażoną po raz pierwszy przez Bertranda Meyera.
Idea kryjąca się za tą zasadą polega na tym, że kod powinien być otwarty na rozszerzenia, ale
zamknięty na modyfikacje. Co to oznacza? Otóż kiedy mamy do czynienia z dobrym projek-
tem, nie musimy w szerokim zakresie zmieniać kodu, aby dodać nowe funkcjonalności.
Czy kod, który uzyskaliśmy w tym rozdziale, wykazuje takie właściwości? Tak. Właśnie przyj-
rzeliśmy się kilku scenariuszom zmian. W wielu z nich musieliśmy zmodyfikować zaledwie
parę metod. W niektórych przypadkach mogliśmy dodać nowe funkcjonalności poprzez
proste utworzenie podklas. Rzecz jasna, po utworzeniu podklasy należy usunąć powielony
kod (więcej informacji na temat dodawania funkcjonalności poprzez tworzenie podklas
oraz integrowania ich za pomocą refaktoryzacji znajduje się w omówieniu programowania
różnicowego na stronie 110).
Kiedy usuniemy duplikację, nasz kod często w naturalny sposób zaczyna być zgodny z zasadą
otwarte-zamknięte.
292 ROZDZIAŁ 21. WSZĘDZIE ZMIENIAM TEN SAM KOD
Rozdział 22.

Muszę zmienić
monstrualną metodę,
lecz nie mogę napisać
do niej testów

Jednym z najtrudniejszych doświadczeń związanych z pracą nad cudzym kodem jest


konieczność zajmowania się dużymi metodami. W wielu przypadkach możesz unik-
nąć refaktoryzacji takich metod, posługując się technikami kiełkowania metody (77)
i kiełkowania klasy (80), chociaż szkoda postępować w ten sposób, nawet jeśli masz taką
możliwość. Długie metody to obszary bagna występujące w bazie kodu. Za każdym ra-
zem, kiedy musisz je zmienić, powracasz do nich, ponownie usiłujesz je zrozumieć, po
czym wprowadzasz swoje zmiany. Często zabiera to więcej czasu niż wtedy, gdy kod
jest czystszy.
Długie metody są problematyczne, ale metody monstrualne są jeszcze gorsze. Mon-
strualna metoda to metoda tak długa i złożona, że czujesz się nieswojo na samą myśl o niej.
Może ona zawierać setki albo tysiące linii z chaotycznymi wcięciami, które prawie unie-
możliwiają orientowanie się w niej. Kiedy masz do czynienia z taką metodą, aż kusi
Cię, żeby wydrukować ją na kilku metrach papieru składanki i rozłożyć ten wydruk
w korytarzu, abyście Ty i Twoi koledzy mogli zorientować się, o co w niej chodzi.
Znajdowaliśmy się kiedyś w drodze na spotkanie i gdy wracaliśmy do hotelu, zawołał
mnie kolega: „Hej, musisz to zobaczyć”. Poszedł do swojego pokoju, wyciągnął laptop
i pokazał mi metodę, która ciągnęła się przez więcej niż tysiąc linii. Kolega wiedział, że
interesuję się refaktoryzacją, więc zapytał: „Jak, u licha, zrefaktoryzowałbyś coś takiego?”.
Zaczęliśmy się nad tym zastanawiać. Wiedzieliśmy, że kluczem do sukcesu jest testowanie,
ale gdzie je w ogóle zacząć przy tak dużej metodzie?
W tym rozdziale opiszę w zarysie, czego nauczyłem się od tamtej pory.
294 ROZDZIAŁ 22. MUSZĘ ZMIENIĆ MONSTRUALNĄ METODĘ, LECZ NIE MOGĘ NAPISAĆ DO NIEJ TESTÓW

Rodzaje monstrów
Monstrualne metody występują w kilku gatunkach, które niekoniecznie muszą się między
sobą wyraźnie różnić. Metody, na które można się napatoczyć, są w pewnym sensie
jak dziobaki — przypominają mieszankę kilku różnych typów.

Metoda punktowana
Metoda punktowana to metoda niemal zupełnie pozbawiona wcięć. Jest to po prostu
sekwencja kodu, który przywodzi Ci na myśl listę punktowaną. Niektóre z fragmen-
tów kodu mogą być powcinane, ale w samej metodzie nie ma zbyt wielu wcięć. Kiedy
spojrzysz na metodę punktowaną i zmrużysz oczy, zobaczysz mniej więcej coś takiego,
co widać na rysunku 22.1.

Rysunek 22.1. Metoda punktowana


RODZAJE MONSTRÓW 295

Tak wygląda ogólna postać metody punktowanej. Jeśli masz szczęście, między po-
szczególnymi jej sekcjami będą znajdować się dodatkowe wiersze albo komentarze,
wskazujące, że w sekcjach tych dzieją się różne rzeczy. W idealnej sytuacji miałbyś
możliwość wyodrębnienia metod z każdej z takich sekcji, ale często metody nie poddają się
tak łatwo refaktoryzacji. Odstępy między sekcjami mogą wprowadzać w błąd, ponieważ
często zmienne tymczasowe są deklarowane w jednej sekcji i wykorzystywane w następnej.
Rozbicie takiej metody nie będzie tak proste jak zwykłe przekopiowanie i wklejenie kodu.
Wbrew temu wszystkiemu metody punktowane nie są aż tak okropne jak inne, przede
wszystkim dlatego, że brak zdziczałych wcięć pozwala nam zachować orientację w kodzie.

Metoda wysunięta
W metodzie wysuniętej dominuje pojedyncza, obszerna i wysunięta sekcja. Najprostszym
przypadkiem tego typu metody jest jedna duża instrukcja warunkowa, taka jak pokazana
na rysunku 22.2.

Rysunek 22.2. Prosta metoda wysunięta

Taki rodzaj wysunięcia charakteryzuje się prawie identycznymi cechami, jakie ma


metoda punktowana. Wysunięcia, które wymagają od Ciebie pełnej uwagi, występują
w metodach o kształcie pokazanym na rysunku 22.3.
Najlepszym sposobem na ustalenie, czy masz do czynienia z rzeczywistym wysunię-
ciem, jest próba wyrównania bloków w długiej metodzie. Jeśli zaczniesz odczuwać za-
wroty głowy, to znaczy, że natrafiłeś na metodę wysuniętą.
296 ROZDZIAŁ 22. MUSZĘ ZMIENIĆ MONSTRUALNĄ METODĘ, LECZ NIE MOGĘ NAPISAĆ DO NIEJ TESTÓW

Rysunek 22.3. Bardzo wysunięta metoda

Metody zazwyczaj nie są ani w stu procentach punktowane, ani wysunięte, tylko
znajdują się gdzieś pomiędzy. Wiele wysunięć ma zaszyte głęboko w sobie sekcje punkto-
wane, ale trudno napisać dla nich testy weryfikujące ich zachowanie, ponieważ są one
głęboko zagnieżdżone. W związku z tym wysunięcia są wyjątkowymi wyzwaniami.
Kiedy refakturujesz długie metody, możliwość skorzystania z narzędzia do refaktory-
zacji sprawia sporą różnicę. Prawie każde takie narzędzie wspiera refaktoryzację techniką
wyodrębniania metod, gdyż takie wsparcie przynosi ogromne korzyści. Jeśli narzędzie
potrafi bezpiecznie wyodrębniać metody, nie będziesz potrzebował testów, aby je we-
ryfikować. Narzędzie przeprowadzi za Ciebie analizę, pozostawiając Ci nauczenie się,
STAWIANIE CZOŁA MONSTROM PRZY WSPARCIU AUTOMATYCZNEJ REFAKTORYZACJI 297

jak korzystać z mechanizmu wyodrębniania w taki sposób, aby metoda przybrała przy-
zwoitą postać, która nada się do dalszej pracy nad nią.
Jeżeli nie masz wsparcia przy wyodrębnianiu metod, czyszczenie monstrualnych
metod będzie trudniejsze. Często będziesz musiał być ostrożniejszy, ponieważ Twoja
praca będzie ograniczona testami, które można umieścić we właściwych miejscach.

Stawianie czoła monstrom


przy wsparciu automatycznej refaktoryzacji
Jeśli masz narzędzie, które wyodrębnia za Ciebie metody, powinieneś wiedzieć, co ono
potrafi, a czego nie potrafi zrobić. Większość współczesnych narzędzi refaktoryzujących
przeprowadza proste wyodrębnianie metod i wykonuje różne inne refaktoryzacje, ale
nie potrafi obsłużyć wszystkich pomocniczych technik refaktoryzacyjnych, które są po-
trzebne osobom rozbijającym duże metody. Często kusi nas na przykład zmiana kolej-
ności instrukcji w celu pogrupowania ich na potrzeby wyodrębniania. Żadne z dostępnych
obecnie narzędzi nie przeprowadza analizy potrzebnej do stwierdzenia, czy takie prze-
grupowanie będzie bezpieczne. Szkoda, ponieważ zabieg ten może być przyczyną po-
wstawania błędów.
Aby podczas pracy z dużymi metodami skutecznie korzystać z narzędzi refaktoryzują-
cych, opłaca się dokonać serii zmian wyłącznie przy użyciu danego narzędzia z pomi-
nięciem jakiejkolwiek edycji kodu źródłowego. Może to wyglądać jak dokonywanie
refaktoryzacji z jedną ręką schowaną za plecami, ale dzięki temu uzyskasz wyraźne
rozgraniczenie między zmianami, co do których wiadomo, że są bezpieczne, a zmianami,
które takie nie są. Kiedy refaktorujesz w ten sposób, powinieneś unikać nawet najprost-
szych zabiegów, takich jak zmiana kolejności instrukcji lub rozbijanie wyrażeń. Świetnie,
jeśli Twoje narzędzie wspiera zmienianie nazw zmiennych, ale jeśli nie ma takiej możliwo-
ści, odłóż tę czynność na później.

Jeśli przeprowadzasz automatyczną refaktoryzację bez testów, korzystaj wyłącznie z narzędzia.


Po serii zautomatyzowanych refaktoryzacji często będziesz mógł umieścić testy w kodzie,
dzięki czemu ręcznie zweryfikujesz zmiany, których dokonasz.

Podczas wyodrębniania kodu powinieneś mieć następujące cele:


1. Odseparowanie logiki od kłopotliwych zależności.
2. Wprowadzenie szwów, które ułatwią umieszczenie na miejscach testów umoż-
liwiających dalszą refaktoryzację.
Oto przykład:
class CommoditySelectionPanel
{
298 ROZDZIAŁ 22. MUSZĘ ZMIENIĆ MONSTRUALNĄ METODĘ, LECZ NIE MOGĘ NAPISAĆ DO NIEJ TESTÓW

...
public void update() {
if (commodities.size() > 0
&& commodities.GetSource().equals("local")) {
listbox.clear();
for (Iterator it = commodities.iterator();
it.hasNext(); ) {
Commodity current = (Commodity)it.next();
if (commodity.isTwilight()
&& !commodity.match(broker))
listbox.add(commodity.getView());
}
}
...
}
...
}

W metodzie tej można uporządkować wiele spraw. Jedno z dziwnych zjawisk polega
na tym, że takie filtrowanie zachodzi w klasie panelowej, która w idealnej sytuacji powinna
być odpowiedzialna wyłącznie za wyświetlanie. Uporządkowanie tego kodu będzie trudne.
Gdybyśmy chcieli napisać testy dla tej metody w takiej postaci, jaką ma ona teraz, mo-
glibyśmy rozpocząć od sprawdzania stanu pola listy, co jednak nie poprowadziłoby nas
zbyt daleko w kierunku uzyskania lepszego projektu.
Mając wsparcie dla refaktoryzacji, możemy rozpocząć nadawanie nazw wysokopo-
ziomowym fragmentom metod i w tym samym czasie rozbijać zależności. Po serii wyod-
rębnień kod będzie wyglądać następująco:
class CommoditySelectionPanel
{
...
public void update() {
if (commoditiesAreReadyForUpdate()) {
clearDisplay();
updateCommodities();
}
...
}

private boolean commoditiesAreReadyForUpdate() {


return commodities.size() > 0
&& commodities.GetSource().equals("local");
}

private void clearDisplay() {


listbox.clear();
}

private void updateCommodities() {


for (Iterator it = commodities.iterator(); it.hasNext(); ) {
Commodity current = (Commodity)it.next();)
STAWIANIE CZOŁA MONSTROM PRZY WSPARCIU AUTOMATYCZNEJ REFAKTORYZACJI 299

if (singleBrokerCommodity(commodity)) {
displayCommodity(current.getView());
}
}
}

private boolean singleBrokerCommodity(Commodity commodity) {


return commodity.isTwilight() && !commodity.match(broker);
}

private void displayCommodity(CommodityView view) {


listbox.add(view);
}
...
}

Szczerze mówiąc, kod w metodzie update nie wygląda specjalnie inaczej pod względem
struktury; nadal jest instrukcją if, w której wykonywana jest jakaś praca. Zadanie jednak
zostało oddelegowane do metod. Metoda update wygląda jak szkielet kodu, z którego
się wywodzi. A co z nazwami? Wyglądają nieco sztucznie, prawda? Są jednak dobre na
początek. Przynajmniej umożliwiają, aby kod komunikował się na wyższym poziomie,
i wprowadzają szwy, które pozwalają na usuwanie zależności. Możemy utworzyć podklasę
i przesłonić metodę (398) w celu przeprowadzenia rozpoznania za pomocą metod
displayCommodity i clearDisplay. Gdy już to zrobimy, będziemy mogli rozejrzeć się za
możliwością utworzenia klasy wyświetlacza i przeniesienia do niej tych metod, korzysta-
jąc z testów jako wsparcia. W tym przypadku jednak rozsądniej byłoby sprawdzić, czy
możemy przenieść metody update i updateCommodities do innej klasy i pozostawić na
miejscu metody clearDisplay oraz displayCommodity, dzięki czemu skorzystamy z prze-
wagi, jaką daje fakt, że klasa ta jest panelem, czyli wyświetlaczem. Kiedy metody znajdą
się już na swoich miejscach, będziemy mogli zmienić ich nazwy. Po przeprowadzeniu
dodatkowej refaktoryzacji nasz projekt będzie mógł wyglądać mniej więcej tak jak na
rysunku 22.4.

Rysunek 22.4. Klasa logiczna wyodrębniona z klasy CommoditySelectionPanel

Najważniejszą rzeczą do zapamiętania podczas korzystania z automatycznych na-


rzędzi w celu wyodrębniania metod jest to, że można bezpiecznie wykonać sporo prostej
pracy i zająć się szczegółami po umieszczeniu testów na miejscu. Nie przejmuj się meto-
dami, które zdają się nie pasować do klasy. Często wskazują one na potrzebę późniejszego
wyodrębnienia nowej klasy. Więcej informacji na ten temat znajdziesz w rozdziale 20.,
„Ta klasa jest za duża, a ja nie chcę, żeby stała się jeszcze większa”.
300 ROZDZIAŁ 22. MUSZĘ ZMIENIĆ MONSTRUALNĄ METODĘ, LECZ NIE MOGĘ NAPISAĆ DO NIEJ TESTÓW

Wyzwanie ręcznej refaktoryzacji


Kiedy masz wsparcie automatycznej refaktoryzacji, nie musisz robić nic specjalnego, aby
rozpocząć rozbijanie dużych metod. Dobre narzędzia refaktoryzujące sprawdzają każdą
refaktoryzację, którą chcesz przeprowadzić, i blokują takie, których nie mogą dokonać
bezpiecznie. Jeśli jednak nie dysponujesz narzędziem do refaktoryzacji, powinieneś zagwa-
rantować, że Twoja praca będzie poprawna, a testy będą najsilniejszym narzędziem,
które to umożliwiają.
Monstrualne metody znacznie utrudniają testowanie, refaktoryzację i dodawanie
funkcjonalności. Jeśli masz możliwość utworzenia w jarzmie testowym instancji klasy, która
obejmuje daną metodę, możesz spróbować opracować zestaw przypadków testowych,
które zagwarantują Ci pewność podczas rozbijania metody. Jeżeli logika w tej metodzie
jest szczególnie złożona, może okazać się, że będzie to prawdziwym koszmarem. Na
szczęście w takich przypadkach możemy skorzystać z kilku technik. Zanim jednak im
się przyjrzymy, sprawdźmy, co może pójść źle, gdy będziemy wyodrębniać metody.
Oto lista. Nie występują na niej wszystkie możliwe błędy, ale zawiera ona najczęściej
spotykane:
1. Możemy zapomnieć o przekazaniu zmiennej do wyodrębnionej metody. Często
kompilator informuje o brakującej zmiennej (o ile nie ma takiej samej nazwy
jak zmienna instancji), ale możemy pomyśleć, że zapewne jest to jakaś zmienna
lokalna i zadeklarować ją w nowej metodzie.
2. Możemy nadać wyodrębnionej metodzie nazwę, która ukrywa lub przesłania
metodę o takiej samej nazwie w klasie bazowej.
3. Możemy pomylić się, kiedy przekazujemy parametry albo przypisujemy wartości
zwrotne. Możemy zrobić coś naprawdę głupiego, jak na przykład zwrócić niewła-
ściwą wartość, lub coś bardziej subtelnego, jak na przykład zwrócić albo przyjąć
w nowej metodzie niepoprawny typ.
Całkiem sporo rzeczy może pójść źle. Techniki opisane w tym podrozdziale mogą
sprawić, że wyodrębnianie metod będzie mniej ryzykowne, gdy nie mamy do dyspo-
zycji testów.

Wprowadzenie zmiennej rozpoznającej


Kiedy przeprowadzamy refaktoryzację, możemy nie mieć ochoty na dodawanie funk-
cjonalności w kodzie produkcyjnym, co jednak nie znaczy, że nie możemy dodawać
żadnego kodu. Czasami pomocne może być dodanie do klasy zmiennej i wykorzystanie
jej do rozpoznania warunków w metodzie, którą chcemy poddać refaktoryzacji. Kiedy
już przeprowadzimy refaktoryzację, będziemy mogli pozbyć się zmiennej testowej, a nasz
kod pozostanie czysty. Taka technika nazywa się wprowadzeniem zmiennej rozpo-
znającej. Oto przykład. Zaczniemy od metody w klasie Javy o nazwie DOMBuilder. Chcemy
ją oczyścić, ale niestety nie mamy narzędzia refaktoryzującego.
WYZWANIE RĘCZNEJ REFAKTORYZACJI 301

public class DOMBuilder


{
...
void processNode(XDOMNSnippet root, List childNodes)
{
if (root != null) {
if (childNodes != null)
root.addNode(new XDOMNSnippet(childNodes));
root.addChild(XDOMNSnippet.NullSnippet);
}
List paraList = new ArrayList();
XDOMNSnippet snippet = new XDOMNReSnippet();
snippet.setSource(m_state);
for (Iterator it = childNodes.iterator();
it.hasNext();) {
XDOMNNode node = (XDOMNNode)it.next();
if (node.type() == TF_G || node.type() == TF_H ||
(node.type() == TF_GLOT && node.isChild())) {
paraList.addNode(node);
}
...
}
...
}
...
}

W powyższym przykładzie wygląda na to, że sporo pracy w tej klasie wykonuje


XDOMNSnippet. Oznacza to, że powinniśmy mieć możliwość przeprowadzenia potrzebnych
nam testów poprzez przekazywanie do tej metody różnych wartości jako jej argumentów.
W rzeczywistości jednak wiele pracy jest wykonywanej również na drugim planie; pracy,
którą możemy poznać tylko pośrednio. W takiej sytuacji możemy wprowadzać zmienne
rozpoznające, które ułatwią nam pracę. Moglibyśmy dodać zmienną instancji i stwier-
dzić, że gdy typ węzła jest prawidłowy, węzeł ten zostanie dodany do listy paraList.
public class DOMBuilder
{
public boolean nodeAdded = false;
...
void processNode(XDOMNSnippet root, List childNodes)
{
if (root != null) {
if (childNodes != null)
root.addNode(new XDOMNSnippet(childNodes));
root.addChild(XDOMNSnippet.NullSnippet);
}
List paraList = new ArrayList();
XDOMNSnippet snippet = new XDOMNReSnippet();
snippet.setSource(m_state);
for (Iterator it = childNodes.iterator();
it.hasNext(); ) {
XDOMNNode node = (XDOMNNode)it.next();
if (node.type() == TF_G || node.type() == TF_H ||
(node.type() == TF_GLOT && node.isChild())) {
302 ROZDZIAŁ 22. MUSZĘ ZMIENIĆ MONSTRUALNĄ METODĘ, LECZ NIE MOGĘ NAPISAĆ DO NIEJ TESTÓW

paraList.add(node);
nodeAdded = true;
}
...
}
...
}
...
}

Mając na miejscu tę zmienną, musimy jeszcze popracować nad danymi wejścio-


wymi w celu utworzenia przypadku obejmującego ten warunek. Kiedy już to zrobimy,
będziemy mogli wyodrębnić ten fragment logiki, a nasze testy nadal powinny kończyć
się sukcesem.
Oto test, który pokaże nam, że kiedy typ węzła to TF_G, dodany zostanie nowy węzeł:
void testAddNodeOnBasicChild()
{
DOMBuilder builder = new DomBuilder();
List children = new ArrayList();
children.add(new XDOMNNode(XDOMNNode.TF_G));
Builder.processNode(new XDOMNSnippet(), children);

assertTrue(builder.nodeAdded);
}

Z kolei poniższy test pokazuje, że gdy typ węzła jest niepoprawny, węzeł nie zostanie
dodany:
void testNoAddNodeOnNonBasicChild()
{
DOMBuilder builder = new DomBuilder();
List children = new ArrayList();
children.add(new XDOMNNode(XDOMNNode.TF_A));
Builder.processNode(new XDOMNSnippet(), children);
assertTrue(!builder.nodeAdded);
}

Mając na miejscach powyższe testy, powinniśmy mieć więcej pewności co do wy-


odrębnienia ciała instrukcji warunkowej, określającej, czy zostanie dodany węzeł. Ko-
piujemy cały warunek, a testy pokazują, że węzeł jest dodawany, gdy warunek zostanie
spełniony.
public class DOMBuilder
{
void processNode(XDOMNSnippet root, List childNodes)
{
if (root != null) {
if (childNodes != null)
root.addNode(new XDOMNSnippet(childNodes));
root.addChild(XDOMNSnippet.NullSnippet);
}
WYZWANIE RĘCZNEJ REFAKTORYZACJI 303

List paraList = new ArrayList();


XDOMNSnippet snippet = new XDOMNReSnippet();
snippet.setSource(m_state);
for (Iterator it = childNodes.iterator();
it.hasNext();) {
XDOMNNode node = (XDOMNNode)it.next();
if (isBasicChild(node)) {
paraList.addNode(node);
nodeAdded = true;
}
...
}
...
}
private boolean isBasicChild(XDOMNNode node) {
return node.type() == TF_G
|| node.type() == TF_H
|| node.type() == TF_GLOT && node.isChild());
}
...
}

Później będziemy mogli usunąć flagę oraz test.


W tym przypadku skorzystałem ze zmiennej logicznej. Chciałem po prostu sprawdzić,
czy węzeł nadal będzie dodawany po wyodrębnieniu warunku. Jestem już na tyle pewien,
że mogę wyodrębnić całe ciało instrukcji warunkowej bez powodowania błędów, iż nie
poddałem testom całej logiki tego warunku. Testy te umożliwiły mi szybkie przekonanie
się, że po dokonaniu wyodrębnienia warunek nadal pozostanie częścią wykonywanego
kodu. Więcej informacji o tym, ile testów przeprowadzić podczas wyodrębniania metod,
znajdziesz w omówieniu testowania ukierunkowanego (200), w rozdziale 13., „Muszę
dokonać zmian, ale nie wiem, jakie testy napisać”.
Kiedy korzystasz ze zmiennych rozpoznających, dobrym pomysłem jest zachowanie
ich w klasie na czas przeprowadzania serii refaktoryzacji i skasowanie ich już po zakoń-
czeniu sesji refaktoryzacyjnej. Często tak postępuję, dzięki czemu mogę widzieć wszystkie
testy, które piszę na potrzeby refaktoryzacji, i z łatwością mogę się z nich wycofać, gdy
zechcę przeprowadzić wyodrębnianie inaczej. Kiedy już skończę, kasuję te testy albo je
refaktoryzuję, przez co weryfikują one wyodrębniane przeze mnie metody zamiast metod
oryginalnych.
Zmienne rozpoznające są głównym narzędziem używanym podczas rozplątywania
monstrualnych metod. Możesz korzystać z nich w celu przeprowadzenia refaktoryzacji
głęboko wewnątrz wysuniętych metod, ale możesz ich używać także do stopniowego ich
wygładzania. Jeśli mamy na przykład metodę, która zagnieżdża większość swojego kodu
głęboko w instrukcjach warunkowych, możemy skorzystać ze zmiennych rozpoznających
w celu wyodrębnienia tych instrukcji lub ich ciał do nowych metod. Zmienne rozpozna-
jące przydadzą się także podczas pracy nad tymi nowymi metodami, aż uda nam się
wygładzić ich kod.
304 ROZDZIAŁ 22. MUSZĘ ZMIENIĆ MONSTRUALNĄ METODĘ, LECZ NIE MOGĘ NAPISAĆ DO NIEJ TESTÓW

Wyodrębniaj to, co znasz


Inną strategią, którą możemy przyjąć podczas pracy nad monstrualnymi metodami, jest
skromny początek polegający na odszukaniu małych fragmentów kodu, które możemy
spokojnie wyodrębnić bez testów, i późniejszym pokryciu ich testami. No dobra, wyrażę
się inaczej, ponieważ „mały” dla każdego znaczy coś innego. Kiedy mówię „mały”, mam
na myśli dwie albo trzy linie kodu, góra pięć. Ma to być fragment kodu, któremu z łatwo-
ścią możesz nadać nazwę. Najważniejszą rzeczą, na którą należy zwracać uwagę podczas
dokonywania takiego niewielkiego wyodrębnienia, jest jego liczba powiązań. Liczba
powiązań to liczba wartości przekazywanych do i z wyodrębnianej metody. Jeśli na przy-
kład z poniższej metody wyodrębnimy metodę max, jej liczba powiązań będzie równa 3:
void process(int a, int b, int c) {
int maximum;
if (a > b)
maximum = a;
else
maximum = b;
...
}
Oto powyższy kod po dokonaniu wyodrębnienia:
void process(int a, int b, int c) {
int maximum = max(a,b);
...
}

Liczba powiązań metody max wynosi 3: dwie zmienne na wejściu i jedna na wyjściu.
Dobrze jest preferować wyodrębnienia z małą liczbą powiązań, ponieważ popełnienie
pomyłki w takim przypadku nie jest aż tak łatwe. Kiedy próbujesz wybrać metodę do
wyodrębnienia, rozglądaj się za małą liczbą linii i licz zmienne na wejściu oraz na wyjściu.
Pomijaj zmienne instancji — one się nie liczą, ponieważ je tylko wycinamy i wklejamy.
Nie przechodzą one poprzez interfejs metody, którą wyodrębniamy.
Główne niebezpieczeństwo podczas wyodrębniania metod tkwi w błędach konwersji
typu. Mamy większe szanse, aby ich uniknąć, jeśli będziemy wyodrębniać wyłącznie me-
tody o małej liczbie powiązań. Kiedy już zidentyfikujemy możliwe do przeprowadzenia
wyodrębnienie, powinniśmy poszukać miejsc deklaracji każdej z przekazywanych zmien-
nych, aby upewnić się, że poprawnie ujęliśmy sygnaturę metody.
Jeśli wyodrębnienia o niskiej liczbie powiązań są bezpieczniejsze, to wyodrębnienia
o liczbie powiązań równej 0 muszą być najbezpieczniejsze — i tak jest w istocie. Możesz
poczynić ogromne postępy w monstrualnych metodach, po prostu wyodrębniając metody,
które nie przyjmują żadnych parametrów i nie zwracają żadnych wartości. Metody te są
tak naprawdę poleceniami zrobienia czegoś. Każesz obiektowi zrobić coś ze swoim
stanem czy też, wyrażając się mniej precyzyjnie, każesz obiektowi zrobić coś z jakimś
stanem globalnym. W każdym razie, kiedy próbujesz w ten sposób nadawać nazwy frag-
mentom kodu, często w końcu uzyskujesz lepszy wgląd w to, co dany fragment robi i jaki
ma on wpływ na obiekt. Taki rodzaj wejrzenia może wywołać kaskadę kolejnych przemy-
śleń i sprawić, że spojrzysz na swój projekt z innych, produktywniejszych perspektyw.
WYZWANIE RĘCZNEJ REFAKTORYZACJI 305

Kiedy korzystasz z techniki wyodrębniasz to, co znasz, pamiętaj, aby nie wybierać
za dużych fragmentów. Poza tym jeśli liczba powiązań jest większa od 0, często opłaca się
skorzystać ze zmiennej rozpoznającej. Po dokonaniu wyodrębnienia napisz kilka testów
weryfikujących wyodrębnioną metodę.
Kiedy stosujesz tę technikę w odniesieniu do małych fragmentów kodu, trudno jest
zauważyć postęp w wygładzaniu monstrualnej metody, jednak postęp ten skrada się
w Twoim kierunku. Za każdym razem, gdy powracasz do kodu i wyodrębniasz kolejny
drobny fragment, który znasz, oczyszczasz nieco metodę. Wraz z upływem czasu możesz
uzyskać lepsze wyczucie jej zakresu oraz poznać kierunek, w którym chciałbyś ją po-
prowadzić.
Gdy nie mam narzędzia do refaktoryzacji, zazwyczaj zaczynam od wyodrębniania
metod o zerowej liczbie powiązań, aby uzyskać wgląd w ogólną strukturę kodu. Często jest
to dobry wstęp do testowania i dalszej pracy.
Jeśli masz do czynienia z metodą punktowaną, możesz pomyśleć, że będziesz mógł
wyodrębnić wiele metod o zerowej liczbie powiązań i że każdy fragment kodu będzie
dobry. Czasami znajdziesz takie fragmenty, ale często będą one używać zmiennych
tymczasowych, które zostały zadeklarowane przed nimi. Zdarzać się będzie, że przyj-
dzie Ci zignorować „bryłkowatą strukturę” metody punktowanej i szukać metod o niskiej
liczbie powiązań wewnątrz bryłek kodu oraz pomiędzy nimi.

Gromadzenie zależności
Czasami w monstrualnej metodzie znajduje się kod, który w pewnym sensie odgrywa
drugorzędną rolę w stosunku do głównego zadania tej metody. Jest on tam niezbędny, ale
nie jest zbyt skomplikowany i jeśli przypadkiem go zepsujesz, będzie to widoczne. Chociaż
wszystko to jest prawdą, po prostu nie możesz podjąć ryzyka uszkodzenia głównej logiki
tej metody. W takich przypadkach możesz skorzystać z techniki o nazwie gromadzenie
zależności. Piszesz testy dla logiki, którą chcesz zachować. Następnie wyodrębniasz
elementy, które nie zostały objęte testami. Kiedy tak postępujesz, przynajmniej masz pew-
ność, że pozostawisz ważne zachowanie. Oto prosty przykład:
void addEntry(Entry entry) {
if (view != null && DISPLAY == true) {
view.show(entry);
}
...
if (entry.category().equals("single")
|| entry.category("dual")) {
entries.add(entry);
view.showUpdate(entry, view.GREEN);
}
else {
...
}
}
306 ROZDZIAŁ 22. MUSZĘ ZMIENIĆ MONSTRUALNĄ METODĘ, LECZ NIE MOGĘ NAPISAĆ DO NIEJ TESTÓW

Jeśli w kodzie wyświetlacza popełnimy błąd, zauważymy go dość szybko. Odkrycie


pomyłki w logice metody add może jednak zająć sporo czasu. W takim przypadku piszemy
testy dla wybranej metody i sprawdzamy, czy wywołania metody add zachodzą przy wła-
ściwych warunkach. Kiedy już przetestujemy całe jej zachowanie, będziemy mogli wy-
odrębnić kod wyświetlacza i mieć pewność, że nasz zabieg nie wpłynie na realizowane
w kodzie dodawanie kolejnych wpisów.
Technika gromadzenia zależności sprawia wrażenie wymigiwania się. Pozostawiamy
pewien zbiór zachowań i pracujemy na innym zbiorze bez zabezpieczeń. Nie wszystkie
jednak zachowania są sobie równoważne. Niektóre z nich są krytyczne, o czym będziemy
się mogli dowiedzieć podczas pracy.
Gromadzenie zależności jest szczególnie wydajne, gdy krytyczne zachowanie prze-
plata się z innymi zachowaniami. Jeśli dysponujesz solidnymi testami sprawdzającymi
ważne zachowanie, możesz dokonać obszernych edycji, które nie są w całości poparte
testami, ale pomagają pozostawić zachowanie.

Wyłonienie obiektu metody


Zmienne rozpoznające są w naszym arsenale bardzo wydajnym narzędziem, ale cza-
sami zauważysz, że masz już pod ręką zmienne, które doskonale nadają się do rozpo-
znawania, ale są one lokalne w danej metodzie. Gdyby były one zmiennymi instancji,
mógłbyś przy ich pomocy przeprowadzić rozpoznanie po wywołaniu metody. Zmienne
lokalne możesz przekształcić w zmienne instancji, ale w wielu przypadkach mogłoby to
prowadzić do pomyłek. Stan, który w ten sposób zaprowadzisz, będzie wspólny tylko dla
monstrualnej metody oraz dla metod, które z niej wyodrębnisz. Chociaż będzie on inicja-
lizowany przy każdym wywołaniu monstrualnej metody, trudne może być zrozumienie,
które ze zmiennych zostaną zachowane, jeśli wyodrębnione metody zechcesz wywoływać
niezależnie od siebie.
Jedną z możliwości jest wyłonienie obiektu metody (332). Technika ta została po raz
pierwszy opisana przez Warda Cunninghama i uosabia ona abstrakcję istniejącą w kodzie.
Kiedy wyłaniasz obiekt metody, tworzysz klasę, której jedyną odpowiedzialnością jest
wykonywanie pracy realizowanej przez monstrualną metodę. Parametry metody stają się
parametrami konstruktora nowej klasy, a kod monstrualnej klasy może stać się częścią
metody o nazwie run albo execute tej klasy. Gdy kod zostanie przeniesiony do nowej klasy,
mamy idealną sytuację do przeprowadzenia refaktoryzacji. Możemy zamienić zmienne
tymczasowe w metodzie na zmienne instancji i w czasie rozbijania metody dokonywać za
ich pomocą rozpoznania.
Wyłonienie obiektu metody jest posunięciem dość drastycznym, ale w przeciwieństwie
do wprowadzenia zmiennej rozpoznającej zmienne, z których korzystasz, są potrzebne
w kodzie produkcyjnym, co umożliwia tworzenie testów, które można pozostawić. Szcze-
gółowy przykład znajduje się w omówieniu wyłonienia obiektu metody (332).
STRATEGIA 307

Strategia
Techniki, które opisałem w tym rozdziale, mogą być pomocne podczas rozbijania mon-
strualnych metod na potrzeby przeprowadzenia dodatkowej refaktoryzacji lub dodawania
nowych funkcjonalności. Niniejszy podrozdział zawiera kilka dodatkowych wskazówek
na temat strukturalnych ustępstw, na które powinieneś być gotowy podczas swojej pracy.

Szkieletyzuj metody
Kiedy masz do czynienia z instrukcją warunkową i szukasz miejsc do wyodrębnienia
metody, masz dwie możliwości wyboru. Możesz wyodrębnić warunek i jego ciało ra-
zem lub osobno. Oto przykład:
if (marginalRate() > 2 && order.hasLimit()) {
order.readjust(rateCalculator.rateForToday());
order.recalculate();
}

Jeżeli wyodrębnisz warunek i ciało do dwóch osobnych metod, łatwiej Ci będzie


w późniejszym czasie przeorganizować logikę metody:
if (orderNeedsRecalculation(order)) {
recalculateOrder(order, rateCalculator);
}

Taką technikę nazywam szkieletyzacją, ponieważ po jej zastosowaniu z metody pozo-


staje wyłącznie szkielet: struktura kontrolna oraz delegacje do innych metod.

Szukaj sekwencji
Kiedy masz do czynienia z instrukcją warunkową i szukasz miejsc do wyodrębnienia me-
tody, masz dwie możliwości wyboru. Możesz wyodrębnić warunek i jego ciało razem lub
osobno. Oto kolejny przykład:
...
if (marginalRate() > 2 && order.hasLimit()) {
order.readjust(rateCalculator.rateForToday());
order.recalculate();
}
...

Jeżeli wyodrębnisz warunek i ciało do tej samej metody, łatwiej Ci będzie zidenty-
fikować wspólną sekwencję poleceń:
...
recalculateOrder(order, rateCalculator);
...

void recalculateOrder(Order order,


RateCalculator rateCalculator) {
308 ROZDZIAŁ 22. MUSZĘ ZMIENIĆ MONSTRUALNĄ METODĘ, LECZ NIE MOGĘ NAPISAĆ DO NIEJ TESTÓW

if (marginalRate() > 2 && order.hasLimit()) {


order.readjust(rateCalculator.rateForToday());
order.recalculate();
}
}

Może okazać się, że pozostała część metody jest po prostu sekwencją powtarzających
się po sobie operacji. Byłoby lepiej, gdybyśmy mogli tę sekwencję dostrzec.
Chwileczkę! Czy nie udzieliłem Ci właśnie dwóch sprzecznych ze sobą rad? Tak,
zrobiłem to. W rzeczywistości sam często waham się między szkieletyzacją metod a szu-
kaniem sekwencji. Prawdopodobnie z Tobą też tak będzie. Szkieletyzuję, kiedy mam
przeczucie, że po oczyszczeniu metody konieczna będzie refaktoryzacja jej struktury kon-
trolnej. Z kolei szukam sekwencji, gdy czuję, że zidentyfikowanie najważniejszych z nich
przyczyni się do oczyszczenia kodu.
Metody punktowane bardziej skłaniają mnie do szukania sekwencji, podczas gdy me-
tody wysunięte — do szkieletyzacji. Wybór Twojej strategii tak naprawdę zależy od orien-
tacji w projekcie, jaką uzyskasz podczas dokonywania wyodrębnień.

Wyodrębniaj najpierw do bieżącej klasy


Kiedy zaczniesz wyodrębniać mniejsze metody z metody monstrualnej, prawdopodobnie
zauważysz, że niektóre wyodrębniane przez Ciebie fragmenty kodu tak naprawdę należą
do innych klas. Jedną z ważnych wskazówek pod tym względem jest nazwa, jaką chciałbyś
zastosować. Jeśli patrzysz na fragment kodu i kusi Cię, aby skorzystać z nazwy zmiennej,
która tam się znajduje, najprawdopodobniej kod należy do klasy zawierającej tę zmienną.
Z takim przypadkiem mamy do czynienia w poniższym kodzie:
if (marginalRate() > 2 && order.hasLimit()) {
order.readjust(rateCalculator.rateForToday());
order.recalculate();
}

Wygląda na to, że moglibyśmy nazwać ten fragment recalculateOrder. To byłaby


całkiem dobra nazwa, ale skoro w nazwie metody używamy słowa order, być może po-
winniśmy przenieść powyższy kod do klasy Order i nadać mu nazwę recalculate. To
prawda — istnieje już metoda recalculate, powinniśmy więc zastanowić się, pod jakim
względem to przeliczanie jest inne, i odzwierciedlić nasze wnioski w nowej nazwie lub
też zmienić nazwę metody recalculate, która już tam jest. W każdym razie wydaje
się, że powyższy fragment kodu powinien należeć do tej klasy.
Chociaż kuszące jest wyodrębnienie kodu bezpośrednio do innej klasy, nie powi-
nieneś tego robić. Najpierw zajmij się metodą o niezdarnej nazwie. Taka właśnie jest
recalculateOrder — umożliwia nam ona przeprowadzenie kilku łatwych do wycofania
wyodrębnień i sprawdzenie, czy wybraliśmy właściwy fragment kodu, który pozwoli
nam posunąć się do przodu. Zawsze możemy przenieść metodę do innej klasy później,
kiedy wyłoni się najlepszy kierunek dla naszych zmian. Do tego czasu wyodrębnianie
do bieżącej klasy pomaga nam posuwać się naprzód i jest mniej podatne na błędy.
STRATEGIA 309

Wyodrębniaj małe fragmenty


Wspominałem już o tym wcześniej, ale chciałbym to podkreślić: wyodrębniaj najpierw
małe fragmenty. Zanim z monstrualnej metody wyodrębnisz taki właśnie mały kawałek,
może się wydawać, że niczego to nie zmieni. Po wyodrębnieniu większej liczby frag-
mentów prawdopodobnie inaczej spojrzysz na wyjściową metodę. Być może dojrzysz
sekwencję, która była wcześniej nieczytelna, lub spostrzeżesz lepszy sposób na zorga-
nizowanie jej metod. Jeśli otworzą się przed Tobą takie możliwości, powinieneś z nich
skorzystać. Jest to bez porównania lepsza strategia niż bezpośrednie próby rozbicia
metody na duże kawałki. Zbyt często nie jest to tak proste, jak się wydaje, i nie jest tak
bezpieczne. Łatwiej jest pogubić szczegóły, a to właśnie one sprawiają, że kod działa.

Bądź gotów na powtórne wyodrębnianie


Istnieje wiele sposobów na pokrojenie tortu i wiele przepisów na rozbicie monstrualnej
metody. Kiedy już dokonasz kilku wyodrębnień, zwykle znajdziesz lepsze sposoby na
proste dodawanie nowych funkcjonalności. Czasami najlepszą metodą umożliwiającą
posunięcie się naprzód jest wycofanie się z jednego czy też dwóch wyodrębnień i ponowne
ich przeprowadzenie. Gdy tak zrobisz, nie będzie to oznaczać, że pierwsze wyodrębnienia
poszły na marne. Pozostawiły one po sobie coś bardzo ważnego: zrozumienie starego
projektu oraz wgląd w lepszy sposób dokonywania postępów.
310 ROZDZIAŁ 22. MUSZĘ ZMIENIĆ MONSTRUALNĄ METODĘ, LECZ NIE MOGĘ NAPISAĆ DO NIEJ TESTÓW
Rozdział 23.

Skąd mam wiedzieć,


czy czegoś nie psuję?

Kod to dziwny materiał budowlany. Większość materiałów, z których możesz wytwarzać


przedmioty, zużywa się. Kiedy korzystasz z nich przez długi czas, rozpadają się. Kod jest
inny. Jeśli pozostawisz go samego sobie, nigdy się nie popsuje. Oprócz zabłąkanej wiązki
promieniowania kosmicznego przelatującej przez Twój nośnik z danymi jedyne, co może
uszkodzić kod, to jego edycja. Jeśli będziesz wciąż na nowo uruchamiać urządzenie
zbudowane z metalu, w końcu się popsuje. Jeśli będziesz wciąż na nowo uruchamiać kod,
to… no cóż, będzie działać wciąż na nowo.
Z tego też powodu na nas, programistach, spoczywa ogromny ciężar. Nie tylko stano-
wimy główny czynnik wprowadzający do oprogramowania błędy, ale też przychodzi nam
to bardzo łatwo. Jak prosta jest modyfikacja kodu? Z mechanicznego punktu widzenia
— bardzo prosta. Każdy może otworzyć edytor tekstu i wrzucić do niego najbardziej
niezrozumiałą bzdurę. Spróbuj wpisać wiersz. Niektóre z takich programów nawet się
kompilują (przejdź na stronę www.ioccc.org i zajrzyj do sekcji międzynarodowego kon-
kursu na niezrozumiały kod w C — International Obfuscated C Code Contest). Żarty na
bok. Naprawdę zdumiewające jest, jak łatwo można popsuć program. Czy śledziłeś kiedyś
tajemniczy błąd tylko po to, aby odkryć, że jego przyczyną był przypadkowy znak, który
niechcący wprowadziłeś? Jakaś litera, która dostała się do kodu, kiedy otworzyła się
książka, którą podawałeś komuś nad klawiaturą. Kod to bardzo delikatny budulec.
W tym rozdziale omówię różne sposoby na zmniejszenie ryzyka, jakie istnieje, gdy
edytujemy kod. Niektóre z nich mają podłoże mechaniczne, a inne psychologiczne (auć!),
niemniej ważne jest skupienie się na nich, zwłaszcza podczas usuwania zależności w cu-
dzym kodzie w celu rozmieszczenia testów.
312 ROZDZIAŁ 23. SKĄD MAM WIEDZIEĆ, CZY CZEGOŚ NIE PSUJĘ?

Superświadome edytowanie
Co tak naprawdę robimy, kiedy edytujemy kod? Co staramy się osiągnąć? Zwykle mamy
przed sobą ambitne cele. Chcemy dodać funkcjonalność albo poprawić błąd. Dobrze
jest wiedzieć, jakie są nasze cele, ale jak możemy je przełożyć na działania?
Kiedy siadamy przy klawiaturze, z łatwością możemy podzielić każde uderzenie w kla-
wisz na dwie kategorie. Nasze uderzenia zmieniają zachowanie programu albo i nie.
Wpisujesz tekst komentarza? Zachowanie nie zostanie zmienione. Wpisujesz tekst do
literału łańcuchowego? W większości przypadków zachowanie się zmieni. Jeśli literał
łańcuchowy znajduje się w kodzie, który nigdy nie zostanie uruchomiony, zachowanie nie
ulegnie zmianie. Uderzenia w klawisze, którymi później dokończysz wywołanie metody
korzystającej z tego literału, zmieniają zachowanie. Z technicznego punktu widzenia
przytrzymywanie spacji podczas formatowania kodu jest refaktoryzacją w skali mikro.
Czasami wpisanie kodu również jest refaktoryzacją. Zmiana literału numerycznego na
wyrażenie, które zostanie użyte w kodzie, nie stanowi refaktoryzacji; jest zmianą
funkcjonalną, o czym warto wiedzieć, gdy korzystasz z klawiatury.
Taka jest istota programowania — dokładnie wiedzieć, co robi każde nasze uderzenie
w klawisz. Nie znaczy to, że musimy być wszechwiedzący, ale wszystko, dosłownie wszyst-
ko, co pomaga nam zrozumieć, jaki wpływ wywieramy na kod, gdy go wprowadzamy,
pomaga nam ograniczać błędy. Bardzo wydajną techniką w tym kontekście jest pro-
gramowanie sterowane testami (104). Jeżeli możesz umieścić kod w jarzmie testo-
wym i poddać go testom w czasie krótszym niż jedna sekunda, będziesz mógł niewiary-
godnie szybko przeprowadzać testy, kiedy tylko przyjdzie taka potrzeba, i rzeczywiście
poznawać efekty, jakie przynoszą Twoje zmiany.

Jeśli jeszcze go nie ma w chwili wydania tej książki, podejrzewam, że już wkrótce ktoś opracuje
zintegrowane środowisko programistyczne umożliwiające zdefiniowanie zestawu testów, jakie
muszą zostać przeprowadzone po każdym naciśnięciu klawisza. Byłby to wręcz niewiarygodny
sposób na domknięcie pętli z informacją zwrotną.
To musi się wydarzyć. Wydaje się to nieuniknione. Istnieją już środowiska sprawdzające
składnię po każdym naciśnięciu klawisza i zmieniające kolor czcionki w przypadku wykrycia
błędu w kodzie. Testowanie uruchamiane edycją jest kolejnym etapem.

Testy wspierają superświadome edytowanie. Programowanie parami również. Czy na


dźwięk słów „superświadome edytowanie” nie czujesz się wyczerpany? No cóż, zbyt
wiele wszystkiego wyczerpuje, najważniejsze jednak, że technika ta nie jest frustrująca.
Superświadome edytowanie to stan przepływu, w którym możesz odciąć się od świata
i uważnie pracować nad kodem. W rzeczywistości może być ono nawet odświeżającym
doświadczeniem. Osobiście czuję się bardziej zmęczony, kiedy nie dostaję żadnych
informacji zwrotnych. W takiej sytuacji zaczynam się bać, że psuję kod, nie wiedząc o tym.
EDYTOWANIE JEDNEGO ELEMENTU NARAZ 313

Walczę o zachowanie stanu systemu w głowie, zapamiętując, co zmieniłem, a czego


jeszcze nie. Zastanawiam się też nad tym, jak będę mógł później przekonać samego
siebie, że istotnie zrobiłem to, co zamierzałem zrobić.

Edytowanie jednego elementu naraz


Nie oczekuję, że pierwsze doświadczenia wszystkich osób z branży komputerowej są
takie same, ale gdy po raz pierwszy pomyślałem o zostaniu programistą, byłem ocza-
rowany opowieściami o superprogramistach — facetach i babkach — którzy potrafili
przechowywać w swoich głowach stan całego systemu, pisać z marszu poprawny kod
i błyskawicznie stwierdzać, czy jakaś zmiana była dobra, czy zła. To prawda, że ludzie
dysponują różnymi możliwościami zapamiętywania sporej liczby tajemniczych szcze-
gółów. Do pewnego stopnia też potrafię tak robić. Znałem kiedyś wiele trudnych części
języka C++, a w pewnym momencie potrafiłem sobie przypomnieć liczne szczegóły
metajęzyka UML, zanim dotarło do mnie, że bycie programistą z tak dobrą znajomością
UML jest bezcelowe i w pewnym sensie smutne.
Prawda jest taka, że istnieje wiele różnych rodzajów „sprytu”. Umiejętność obejmo-
wania umysłem wielu stanów może być przydatna, ale tak naprawdę nie pomaga nam
w podejmowaniu lepszych decyzji. W obecnym momencie mojej kariery zawodowej
uważam się za lepszego programistę, niż byłem kiedyś, chociaż znam mniej szczegółów
na temat każdego języka, w którym pracuję. Osąd stanowi kluczową umiejętność pro-
gramistyczną i możemy wpędzić się w tarapaty, jeśli będziemy próbować zachowywać się
jak supersprytni programiści.
Czy kiedykolwiek coś takiego przytrafiło się Tobie? Zaczynasz pracować nad pew-
nym problemem i wtedy nachodzi Cię myśl: „Hm, może powinienem to posprzątać?”.
Zatrzymujesz się więc, żeby przeprowadzić refaktoryzację, ale wtedy zaczynasz zasta-
nawiać się nad tym, jak ten kod powinien naprawdę wyglądać, i robisz sobie przerwę.
Funkcjonalność, nad którą pracowałeś, musi zostać zaimplementowana, w związku z czym
powracasz do pierwotnego miejsca, w którym edytowałeś kod. Decydujesz, że musisz wy-
wołać pewną metodę, więc przeskakujesz właśnie tam, gdzie jest ta metoda, ale odkry-
wasz, że powinna ona robić coś innego, zatem zaczynasz zmieniać ją, podczas gdy pier-
wotna zamiana nadal oczekuje, a (muszę złapać oddech) Twój kolega stoi obok Ciebie
i krzyczy: „Tak, tak, tak! Napraw to, a potem zajmiemy się tamtym”. Czujesz się jak koń
wyścigowy pędzący po torze, a Twój kolega tak naprawdę w niczym nie jest pomocny. Ujeż-
dża Cię jak dżokej albo — co gorsza — traktuje Cię jak hazardzista czekający na trybunach.
Właśnie tak to się odbywa w niektórych zespołach. Dwóch programistów przeżywa
ekscytującą przygodę programistyczną, a pozostali (trzy czwarte zespołu) są zaangażowani
w naprawianie kodu, który popsuli w poprzednim kwartale. Brzmi okropnie, prawda?
Ale jednak nie — czasami może to być zabawne. Ty i Twój kolega odchodzicie od
komputera niczym bohaterowie. Stawiliście czoła bestii w jej kryjówce i zabiliście ją.
Teraz jesteście ważniakami.
314 ROZDZIAŁ 23. SKĄD MAM WIEDZIEĆ, CZY CZEGOŚ NIE PSUJĘ?

Czy warto było? Spójrzmy na inny sposób osiągnięcia tego samego.


Musisz wprowadzić zmianę w metodzie. Masz już tę klasę w jarzmie testowym i roz-
począłeś dokonywanie poprawek. Wtedy jednak nachodzi Cię myśl: „Ej, powinienem
zmienić jeszcze jedną metodę, o tam”. Zatrzymujesz się więc i przechodzisz do tego
miejsca. Metoda wygląda niechlujnie, więc formatujesz w niej linię albo dwie, żeby
zobaczyć, o co w niej chodzi. Twój kolega spogląda na Ciebie i pyta: „Co robisz?”.
Odpowiadasz: „Chciałem tylko sprawdzić, czy będziemy musieli zmienić metodę X”.
Twój kolega mówi: „Hej, zajmujmy się tylko jedną rzeczą w danym czasie”, zapisuje
nazwę metody X na kartce obok Twojego komputera, a Ty powracasz do edycji i ją koń-
czysz. Przeprowadzasz testy i stwierdzasz, że wszystkie przechodzą. Następnie spoglą-
dasz na drugą metodę. Oczywiście musisz ją zmienić. Rozpoczynasz pisać kolejny test.
Po chwili programowania uruchamiasz testy i zaczynasz integrować. Ty i Twój kolega
patrzycie na drugą stronę biurka. Widzicie tam dwóch młodych programistów. Jeden
z nich krzyczy: „Tak, tak, tak! Napraw to, a potem zajmiemy się tamtym”. Pracują nad
swoim problemem już od kilku godzin i wyglądają na całkiem wyczerpanych. Jeśli
z historii możemy wyciągać jakieś wnioski, to integracja im się nie powiedzie i kilka
kolejnych godzin poświęcą na dalszą wspólną pracę.
Mam swoją małą mantrę, którą powtarzam sobie, gdy pracuję: „Programowanie to
sztuka robienia jednej rzeczy w danym czasie”. Kiedy pracuję w parze, zawsze proszę
mojego kolegę, aby mnie pod tym względem kontrolował i pytał: „Co robisz?”. Jeśli
odpowiem, że kilka rzeczy, wybieramy jedną z nich. O to samo pytam swojego kolegę.
Szczerze mówiąc, tak jest po prostu szybciej. Kiedy programujesz, dość łatwo jest za-
brać się w danym momencie do zbyt wielu zadań. Gdy to Ci się przydarzy, skończysz,
miotając się i wypróbowując różne rozwiązania tylko po to, żeby doprowadzić kod do
działania, zamiast pracować metodycznie i naprawdę wiedzieć, co robi Twój kod.

Zachowywanie sygnatur
Istnieje wiele różnych możliwości popełniania błędów podczas edytowania kodu. Możemy
coś źle napisać, użyć niewłaściwego typu danych, wpisać jedną zmienną, chociaż chcieli-
śmy skorzystać z innej — taką listę można ciągnąć w nieskończoność. Szczególnie podatna
na błędy jest refaktoryzacja. Często wiąże się ona z dokonywaniem obszernej edycji.
Kopiujemy różne elementy w różne miejsca i tworzymy nowe klasy oraz metody. Skala
tych zmian jest znacznie większa w porównaniu z dodaniem nowej linii kodu.
Zwykle możemy sobie radzić w takich sytuacjach, przeprowadzając testy. Gdy mamy
już porozmieszczane testy, będziemy mogli wykryć wiele błędów, które popełniamy
podczas edytowania kodu. Niestety, w wielu systemach musimy przeprowadzić małą re-
faktoryzację tylko po to, aby system stał się na tyle podatny na testy, żeby można było
dokonać większej refaktoryzacji. Taka wstępna refaktoryzacja (techniki usuwania za-
leżności są opisane w katalogu w rozdziale 25.) odbywa się bez testów i należy ją prze-
prowadzać ze szczególną ostrożnością.
ZACHOWYWANIE SYGNATUR 315

Kiedy po raz pierwszy zacząłem korzystać z tych technik, kusiło mnie, aby robić
zbyt wiele. Gdy musiałem wyodrębnić całe ciało metody, zamiast po prostu skopiować
i wkleić argumenty w deklaracji metody, porządkowałem jeszcze inne rzeczy. Jeśli na
przykład miałem wyodrębnić ciało następującej metody i przekształcić ją w metodę
statyczną (upublicznienie metody statycznej (346)):
public void process(List orders,
int dailyTarget,
double interestRate,
int compensationPercent) {
...
// tu następuje skomplikowany kod
...
}

dokonywałem wyodrębnienia jak poniżej, tworząc przy okazji kilka klas pomocniczych.
public void process(List orders,
int dailyTarget,
double interestRate,
int compensationPercent) {
processOrders(new OrderBatch(orders),
new CompensationTarget(dailyTarget,
interestRate * 100,
compensationPercent));
}

Moje intencje były dobre. Podczas usuwania zależności chciałem poprawić projekt, ale
nie wyszło mi to zbyt dobrze. Skończyło się popełnieniem głupich błędów, a bez testów,
które by je wskazały, często odkrywałem je o wiele później, niż powinienem.
Kiedy usuwasz zależności na potrzeby testów, musisz zachować szczególną ostrożność.
Jedną z technik, które stosuję, kiedy tylko mogę, jest zachowywanie sygnatur. Kiedy cał-
kowicie unikasz zmieniania sygnatur, możesz wycinać lub kopiować z miejsca na miejsce
sygnatury metod i minimalizować możliwość popełniania błędów.
W poprzednim przykładzie uzyskałbym następujący kod:
public void process(List orders,
int dailyTarget,
double interestRate,
int compensationPercent) {
processOrders(orders, dailyTarget, interestRate,
compensationPercent);
}

private static void processOrders(List orders,


int dailyTarget,
double interestRate,
int compensationPercent) {
...
}
316 ROZDZIAŁ 23. SKĄD MAM WIEDZIEĆ, CZY CZEGOŚ NIE PSUJĘ?

Edycja argumentów, którą musiałem przeprowadzić w celu osiągnięcia powyższego


efektu, była bardzo prosta. Zasadniczo wiązała się z wykonaniem kilku czynności:
1. Przekopiowałem całą listę argumentów do bufora kopiowania:
List orders,
int dailyTarget,
double interestRate,
int compensationPercent
2. Następnie wpisałem nową deklarację metody:
private void processOrders() {
}
3. Wkleiłem zawartość bufora do deklaracji nowej metody:
private void processOrders(List orders,
int dailyTarget,
double interestRate,
int compensationPercent) {
}
4. Wpisałem wywołanie nowej metody:
processOrders();
5. Wkleiłem zawartość bufora do wywołania:
processOrders(List orders,
int dailyTarget,
double interestRate,
int compensationPercent);
6. Na koniec usunąłem typy, pozostawiając nazwy argumentów:
processOrders(orders,
dailyTarget,
interestRate,
compensationPercent);

Jeśli będziesz wykonywał te czynności wciąż na nowo, staną się one automatyczne,
a Ty uzyskasz większą pewność siebie podczas wprowadzania zmian. Będziesz mógł skon-
centrować się na innych, utrzymujących się problemach, które mogą powodować błędy
podczas usuwania zależności. Czy Twoja nowa metoda na przykład ukrywa metodę o tej
samej sygnaturze w klasie bazowej?
W przypadku techniki zachowywania sygnatur istnieje kilka możliwych scenariuszy.
Możesz z niej korzystać podczas tworzenia deklaracji nowych metod. Możesz także
zastosować ją w celu utworzenia zbioru metod instancji dla wszystkich argumentów
przekazywanych do metody, kiedy posługujesz się techniką refaktoryzacji o nazwie wyła-
nianie obiektu metody. Więcej szczegółów znajdziesz w sekcji Wyłanianie obiektu
metody (330).
WSPARCIE KOMPILATORA 317

Wsparcie kompilatora
Głównym zadaniem kompilatora jest przetłumaczenie kodu źródłowego na jakąś inną
postać, ale w językach typowanych statycznie możesz za pomocą kompilatora zrobić
znacznie więcej. Możesz skorzystać z przewagi, jaką daje sprawdzanie przez kompilator
typów, i użyć tej jego właściwości w celu zidentyfikowania zmian, które powinieneś wpro-
wadzić. Praktykę taką nazywam wsparciem kompilatora. Oto przykład jej zastosowania.
W programie w C++ mam parę zmiennych globalnych:
double domestic_exchange_rate;
double foreign_exchange_rate;

Kilka metod w tym samym pliku używa tych zmiennych, ale chciałbym znaleźć jakiś
sposób umożliwiający ich zmianę na czas testów, tak więc korzystam z techniki hermety-
zacji zmiennej globalnej (340) z katalogu.
W tym celu wprowadzam powyższe deklaracje do nowej klasy i deklaruję zmienną tej
klasy.
class Exchange
{
public:
double domestic_exchange_rate;
double foreign_exchange_rate;
};

Exchange exchange;

Teraz kompiluję kod, aby odszukać wszystkie miejsca, w których kompilator nie może
znaleźć zmiennych domestic_exchange_rate i foreign_exchange_rate, oraz przekształ-
cam je w taki sposób, aby były dostępne poprzez swoją klasę. Oto jak to robię na przykła-
dzie jednej ze zmian:
total = domestic_exchange_rate * instrument_shares;

staje się:
total = exchange.domestic_exchange_rate * instrument_shares;

Najważniejsze w tej technice jest pozwolenie kompilatorowi, aby prowadził Cię w kie-
runku zmian, których powinieneś dokonać. Nie oznacza to, że możesz przestać myśleć
o tym, co należy zmienić, ale że w niektórych przypadkach kompilator może wykonać za
Ciebie część pracy fizycznej. Bardzo ważne jest, abyśmy wiedzieli, co kompilator znajdzie,
a czego nie znajdzie, dzięki czemu nie uśpi Cię nasza fałszywa pewność siebie.
Ze wsparciem kompilatora wiąże się wykonanie dwóch kroków:
1. Modyfikacja deklaracji w celu wywołania błędów kompilatora.
2. Przejście do tych błędów i dokonanie zmian.
318 ROZDZIAŁ 23. SKĄD MAM WIEDZIEĆ, CZY CZEGOŚ NIE PSUJĘ?

Wsparciem kompilatora posługujesz się w celu dokonywania zmian strukturalnych


w swoim programie, jak to robiliśmy w przykładzie z hermetyzacją referencji global-
nej (340). Możesz także korzystać z tej techniki, aby inicjować zmiany typów. Jednym
z często spotykanych przypadków jest zmiana typu deklaracji zmiennej z klasy na in-
terfejs i posłużenie się błędami kompilacji w celu określenia metod, które powinny
znaleźć się w interfejsie.
Wsparcie kompilatora nie zawsze jest praktyczne. Jeśli kompilacje zabierają dużo czasu,
sensowniejsze może okazać się szukanie miejsc, w których należy wprowadzić zmiany.
W rozdziale 7., „Dokonanie zmiany trwa całą wieczność”, znajdziesz kilka sposobów na
poradzenie sobie z tym problemem. Kiedy jednak możesz posłużyć się wsparciem kom-
pilatora, będzie ono bardzo przydatnym narzędziem, chociaż powinieneś zachować
ostrożność; posługując się nim na oślep, możesz wprowadzić do kodu subtelne błędy.
Cechą języka, która daje najwięcej możliwości popełnienia błędów podczas wspierania
się kompilatorem, jest dziedziczenie. Oto przykład.
Mamy metodę klasową o nazwie getX() w klasie Javy:
public int getX() {
return x;
}

Chcemy znaleźć wszystkie jej wystąpienia, w związku z czym komentujemy ją:


/*
public int getX() {
return x;
} */

Teraz rekompilujemy kod.


Zgadnij, co się stało. Nie pojawiły się żadne błędy. Czy oznacza to, że getX() jest metodą
nieużywaną? Niekoniecznie. Jeśli getX() została zadeklarowana jako konkretna metoda
w klasie nadrzędnej, przekształcenie w komentarz metody getX w naszej bieżącej klasie
spowoduje tylko, że zostanie użyta metoda z klasy nadrzędnej. Podobna sytuacja może
mieć miejsce w przypadku zmiennych oraz dziedziczenia.
Wsparcie kompilatora jest skuteczną techniką, ale powinieneś znać jej ograniczenia.
W przeciwnym razie możesz popełnić dość poważne błędy.

Programowanie w parach
Prawdopodobnie słyszałeś już o programowaniu w parach. Jeśli stosowaną przez Ciebie
metodyką jest programowanie ekstremalne, to zapewne tak działasz. To dobrze. Jest to
znakomity sposób na poprawienie jakości kodu i szerzenie wiedzy w zespole.
Jeśli nie programujesz w parach, sugeruję, abyś tego spróbował. W szczególności nale-
gam, żebyś utworzył z kimś parę podczas stosowania technik usuwania zależności, które
opisałem w tej książce.
PROGRAMOWANIE W PARACH 319

Łatwo jest popełnić błąd i w ogóle nie zauważyć, że popsuło się program. Druga para
oczu z pewnością będzie wówczas pomocna. Spójrzmy prawdzie w oczy; praca z cudzym
kodem to operacja, a chirurdzy nigdy nie operują w pojedynkę.
Więcej informacji o programowaniu w parach znajdziesz w książce Laurie Williams
i Roberta Kesslera Pair Programming Illuminated (Addison-Wesley 2002) oraz na stronie
www.pairprogramming.com.
320 ROZDZIAŁ 23. SKĄD MAM WIEDZIEĆ, CZY CZEGOŚ NIE PSUJĘ?
Rozdział 24.

Czujemy się przytłoczeni.


Czy nie będzie
chociaż trochę lepiej?

Praca nad cudzym kodem jest trudna. Nie ma sensu temu zaprzeczać. Chociaż każda
sytuacja jest inna, jedno zawsze sprawi, że zajęcie to będzie dla Ciebie — jako programisty
— ważne lub nie: zrozumienie, co z tego masz. Dla niektórych osób jest to wypłata, i nie
ma w tym nic złego. Każdy z nas musi zarabiać na życie, chociaż w rzeczywistości po-
winien być jeszcze jakiś inny powód, dla którego programujesz.
Jeśli masz szczęście, zacząłeś zawodowo pisać kod, ponieważ uważałeś, że to niezła
zabawa. Usiadłeś przed swoim pierwszym komputerem zachwycony tymi wszystkimi
możliwościami, niesamowitymi rzeczami, które można zrobić, programując kompu-
ter. To było coś, czego można się nauczyć i w czym można się doskonalić. Myślałeś:
„Hej, to dopiero zabawa! Jeśli będę w tym dobry, mogę zrobić wielką karierę”.
Nie każdy dochodzi do programowania w taki sposób, ale nawet osoby, których droga
była inna, nadal mają możliwość zdobycia tego, co w pisaniu programów daje frajdę.
Jeśli powiedzie się Tobie — a także niektórym z Twoich kolegów — tak naprawdę nie
będzie mieć znaczenia, w jakim systemie pracujesz; będziesz mógł robić z nim ciekawe
rzeczy. Alternatywą jest zniechęcenie, i nie będzie w tym nic zabawnego. Szczerze
mówiąc, zasługujemy na lepszy los.
Często osoby poświęcające swój czas na pracę nad cudzym kodem wolałyby działać
w systemach projektowanych od podstaw. Budowanie systemu od zera jest całkiem przy-
jemne, ale szczerze mówiąc, takie „zielone” systemy borykają się z własnymi proble-
mami. Wciąż na nowo widuję, jak odgrywa się następujący scenariusz: istniejący system
wraz z upływem czasu staje się mętny i coraz trudniej jest wprowadzać w nim zmiany.
Ludzie w firmie są sfrustrowani długim czasem, jaki pochłania dokonywanie w nim zmian.
Przenoszą swoich najlepszych pracowników (a czasami najbardziej kłopotliwych!) do
322 ROZDZIAŁ 24. CZUJEMY SIĘ PRZYTŁOCZENI. CZY NIE BĘDZIE CHOCIAŻ TROCHĘ LEPIEJ?

nowego zespołu, który jest obarczony zadaniem „utworzenia zastępczego systemu o lep-
szej architekturze”. Na początku wszystko idzie dobrze. Zespół wie, na czym polega pro-
blem ze starą architekturą, i poświęca czas na opracowanie nowego projektu. W tym
samym czasie reszta programistów pracuje nad starym systemem. Jest on nadal w użyciu,
tak więc otrzymują oni zlecenia dotyczące usuwania błędów i okazjonalnie dodawania
nowych funkcjonalności. Zarząd trzeźwym okiem spogląda na każdą nową funkcjonal-
ność i decyduje, czy powinna się ona znaleźć w starym systemie, czy też klient może po-
czekać na nowy system. W wielu przypadkach klient nie może czekać, tak więc zmiana
jest wprowadzana w obu systemach. Ludzie od nowego systemu mają podwójną robotę,
próbując zastąpić system, który podlega nieustannym zmianom. Wraz z upływem
miesięcy staje się coraz bardziej jasne, że nie uda się im zastąpić starego systemu — tego,
którym zarządzasz. Presja narasta. Programiści pracują dniami, nocami oraz w weekendy.
Coraz częściej reszta firmy odkrywa, że praca, którą wykonujesz, jest krytyczna i że opie-
kujesz się inwestycją, na której wszyscy będą musieli polegać w przyszłości.
Tak naprawdę wcale nie jest dobrze tam, gdzie nas nie ma.
Kluczem do rozkwitu przy pracy w cudzym kodzie jest odkrycie, co Cię motywuje.
Chociaż wielu z nas, programistów, to istoty samotne, tak naprawdę nic nie zastąpi pracy
w ciekawym środowisku z ludźmi, których szanujesz i którzy wiedzą, jak czerpać ra-
dość ze swojego zajęcia. W pracy poznałem niektórych z moich najlepszych przyjaciół
i do dziś pozostają oni osobami, z którymi rozmawiam, gdy podczas programowania
dowiem się czegoś ciekawego lub zabawnego.
Następne rozwiązanie, które pomaga, to wejście w kontakt z większą społecznością.
W dzisiejszych czasach nawiązanie kontaktu z innymi programistami w celu zdobycia
wiedzy i podzielenia się swoimi doświadczeniami jest łatwiejsze niż kiedykolwiek wcze-
śniej. Możesz zapisać się na listy mailingowe w internecie, uczęszczać na konferencje,
korzystać z przewagi, jaką daje obecność w sieci, dzielić się strategiami i technikami
i ogólnie pozostawać w pierwszej linii rozwoju oprogramowania.
Jeśli nawet projektem zajmuje się wiele osób, którym zależy na pracy i które starają się
ulepszać stan rzeczy, może wedrzeć się inny rodzaj zniechęcenia. Czasami ludzie tracą
wiarę, ponieważ ich baza kodu jest tak duża, że mogą oni pracować nad nią przez 10 lat,
a ona i tak nie staje się nawet o 10 procent lepsza. Czyż nie jest to dobry powód na po-
padnięcie w depresję? No cóż, odwiedzałem zespoły dysponujące bazami kodu liczącymi
miliony wierszy i dla których każdy dzień w pracy stanowił kolejne wyzwanie, szansę
wyprowadzenia na prostą kolejnych rzeczy oraz okazję do rozerwania się. Widywałem
także zespoły wprawiane w przygnębienie przez o wiele lepsze bazy kodu. Nastawienie,
z jakim przychodzimy do pracy, jest ważne.
Wypróbuj programowanie sterowane testami poza pracą. Poprogramuj trochę dla
zabawy. Zacznij odczuwać różnicę między swoimi niewielkimi projektami a dużym pro-
jektem w Twojej pracy. Istnieje prawdopodobieństwo, że zyskasz takie samo nastawienie
wobec projektu w pracy, jeżeli uda Ci się uruchomić jego fragmenty w jarzmie testowym.
Jeśli morale w Twoim zespole jest niskie z powodu marnej jakości kodu, podpowiem
Ci, co powinniście spróbować zrobić. Wybierzcie najpaskudniejszy, najbardziej odrażający
CZUJEMY SIĘ PRZYTŁOCZENI. CZY NIE BĘDZIE CHOCIAŻ TROCHĘ LEPIEJ? 323

zestaw klas, jaki istnieje w Waszym projekcie, i poddajcie go testom. Jeżeli jako zespół
poradzicie sobie z najgorszym problemem, poczujecie, że panujecie nad sytuacją. Wiele
razy widziałem, jak to się dzieje.
Gdy zaczniecie przejmować kontrolę nad swoją bazą kodu, zapoczątkujecie rozwój oaz
dobrego kodu. Praca w takich oazach może dawać dużo radości.
324 ROZDZIAŁ 24. CZUJEMY SIĘ PRZYTŁOCZENI. CZY NIE BĘDZIE CHOCIAŻ TROCHĘ LEPIEJ?
Część III

Techniki usuwania zależności


326 CZĘŚĆ III TECHNIKI USUWANIA ZALEŻNOŚCI
ADAPTACJA PARAMETRU 327

Rozdział 25.

Techniki usuwania zależności

W rozdziale tym opiszę zbiór technik usuwania zależności. Lista ta nie jest wyczerpu-
jąca; są to jedynie niektóre z technik, z jakich korzystałem razem z zespołami pro-
gramistów w celu wydzielenia klas w takim stopniu, aby można je było poddawać testom.
Z technicznego punktu widzenia techniki te są refaktoryzacją — każda z nich pozostawia
niezmienione zachowanie. W przeciwieństwie jednak do większości napisanych do tej
pory w naszej branży refaktoryzacji wymagających rozmieszczenia testów są one przezna-
czone do stosowania bez testów. Jeśli zastosujesz się dokładnie do opisanych kroków,
w większości przypadków prawdopodobieństwo popełnienia pomyłki będzie znikome,
co jednak nie oznacza, że techniki te są całkowicie bezpieczne. Możliwość popełnienia
błędu podczas ich stosowania nadal istnieje, tak więc powinieneś zachować ostrożność.
Zanim skorzystasz z tych technik, zajrzyj do rozdziału 23., „Skąd mam wiedzieć, czy
czegoś nie psuję?”. Wskazówki w nim zawarte mogą być pomocne w bezpiecznym
stosowaniu tych refaktoryzacji, dzięki czemu będziesz mógł porozmieszczać testy.
Kiedy już to zrobisz, będziesz mieć możliwość przeprowadzania bardziej inwazyjnych
zmian, mając większą pewność, że niczego nie popsujesz.
Opisane techniki nie poprawią automatycznie Twojego projektu. W rzeczy samej,
jeśli tylko masz w sobie poczucie dobrego projektu, to wzdrygniesz się podczas ich
poznawania. Techniki te mogą być pomocne podczas poddawania testom metod, klas
oraz zbiorów klas i dzięki nim Twój system stanie się łatwiejszy w konserwacji. Na tym
etapie będziesz mógł skorzystać z refaktoryzacji wspierających testowanie, aby uczynić
swój projekt czytelniejszym.

Kilka z refaktoryzacji zamieszczonych w tym rozdziale zostało opisanych przez Martina Fowlera
w książce Refaktoryzacja. Ulepszanie struktury istniejącego kodu (Helion 2011). Techniki te
uwzględniłem tutaj z innymi etapami. Dostosowałem je w taki sposób, aby można z nich
było korzystać bez testów.
328 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

Adaptacja parametru
Kiedy wprowadzam zmiany w metodach, parametry metod często wywołują u mnie
zależnościowe bóle głowy. Czasami okazuje się, że utworzenie potrzebnego mi parametru
jest trudne. Kiedy indziej muszę przetestować wpływ metody na parametr. W wielu
przypadkach klasa parametru wcale tego nie ułatwia. Jeśli mam możliwość zmodyfiko-
wania klasy, w celu zerwania zależności korzystam z techniki wyodrębniania interfejsu
(361). Wyodrębnianie interfejsu często jest najlepszym wyborem, gdy przychodzi do
usuwania zależności związanych z parametrami.
Zwykle w celu usunięcia zależności uniemożliwiających przeprowadzenie testów
chcemy zrobić coś prostego, coś, co nie stwarza nam możliwości popełnienia błędów.
W niektórych jednak przypadkach wyodrębnianie interfejsu (361) nie będzie najlepszym
wyborem. Jeśli typ parametru jest raczej niskopoziomowy lub specyficzny dla pewnej
technologii implementacji, wyodrębnianie interfejsu może przynieść niepożądane
skutki albo być niemożliwe.

Skorzystaj z adaptacji parametru, gdy możesz użyć wyodrębniania interfejsu (361) w odnie-
sieniu do klasy parametru lub gdy parametr jest trudny do sfałszowania.

Oto przykład:
public class ARMDispatcher
{
public void populate(HttpServletRequest request) {
String [] values
= request.getParameterValues(pageStateName);
if (values != null && values.length > 0)
{
marketBindings.put(pageStateName + getDateStamp(),
values[0]);
}
...
}
...
}

W klasie tej metoda populate przyjmuje jako parametr obiekt klasy HttpServletRequest.
HttpServletRequest jest interfejsem będącym częścią standardu Javy J2EE firmy Sun.
Gdybyśmy chcieli przetestować metodę populate w takiej postaci, jaką ma teraz, mu-
sielibyśmy utworzyć klasę implementującą interfejs HttpServletRequest i zapewnić
jakiś sposób przekazania jej wartości parametrów, które będą potrzebne do urucho-
mienia jej na potrzeby testów. Dokumentacja SDK Javy informuje, że istnieją około 23
deklaracje metod w interfejsie HttpServletRequest, nie licząc deklaracji z interfejsu
nadrzędnego, które także musielibyśmy zaimplementować. Dobrze byłoby skorzystać
z techniki wyodrębniania interfejsu (361) w celu otrzymania węższego interfejsu,
ADAPTACJA PARAMETRU 329

który wspierałby tylko potrzebne nam metody, jednak nie można wyodrębnić interfejsu
z innego interfejsu. W Javie interfejs HttpServletRequest musiałby rozszerzyć interfejs,
który wyodrębniamy, a nie możemy w ten sposób modyfikować interfejsu standardowego.
Na szczęście mamy inne opcje.
Dla J2EE istnieje kilka bibliotek obiektów pozorowanych. Jeśli pobierzemy jedną z nich,
będziemy mogli zastąpić HttpServletRequest takim obiektem i przeprowadzić testy,
które są nam potrzebne. Dzięki temu możemy zaoszczędzić na czasie. Jeśli wybierzemy
takie rozwiązanie, nie będziemy musieli poświęcać czasu na ręczne udawanie żądań
serwletu. Zdaje się więc, że znaleźliśmy nasze rozwiązanie, ale czy aby na pewno?
Kiedy usuwam zależności, zawsze staram się patrzeć naprzód i przewidywać, jakie bę-
dą wyniki moich działań. Dopiero wtedy decyduję, czy mogę pogodzić się z takimi
konsekwencjami. W tym przypadku nasz kod produkcyjny będzie wyglądać prawie tak
samo. Wykonamy sporo pracy, aby zachować na miejscu interfejs HttpServletRequest
oraz interfejs API. Czy istnieje jakiś sposób na poprawienie wyglądu kodu oraz uproszcze-
nie usuwania zależności? Okazuje się, że tak. Możemy opakować przychodzący para-
metr i całkowicie usunąć zależność od interfejsu API. Kiedy już to zrobimy, nasz kod
będzie wyglądać następująco:
public class ARMDispatcher
public void populate(ParameterSource source) {
String values = source.getParameterForName(pageStateName);
if (value != null) {
marketBindings.put(pageStateName + getDateStamp(),
value);
}
...
}
}

Co takiego zrobiliśmy? Wprowadziliśmy nowy interfejs o nazwie ParameterSource.


W tym momencie jedyną metodą, którą parametr ten zawiera, jest getParameterForName.
W przeciwieństwie jednak do metody getParameterSource interfejsu HttpServletRequest
metoda getParameterForName zwraca tylko jeden łańcuch tekstowy. Napisaliśmy naszą
metodę w taki właśnie sposób, ponieważ w tym kontekście interesuje nas tylko pierwszy
parametr.

Podążaj raczej w stronę interfejsów komunikujących odpowiedzialności niż szczegóły im-


plementacji. Dzięki temu kod będzie łatwiejszy w czytaniu i konserwacji.

Oto fałszywa klasa implementująca interfejs ParameterSource. Możemy z niej skorzy-


stać w naszych testach:
{
public String value;

public String getParameterForName(String name) {


330 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

return value;
}
}
Z kolei produkcyjne źródło parametru wygląda następująco:
class ServletParameterSource implements ParameterSource
{
private HttpServletRequest request;

public ServletParameterSource(HttpServletRequest request) {


this.request = request;
}

String getParameterValue(String name) {


String [] values = request.getParameterValues(name);
if (values == null || values.length < 1)
return null;
return values[0];
}
}

Z pozoru może wydawać się, że nasze poprawianie kodu to tylko sztuka dla sztuki,
ale jednym z często powtarzających się problemów w zastanych bazach kodu jest brak
jakichkolwiek warstw abstrakcji. Najważniejszy kod systemu często jest poprzeplatany
z niskopoziomowymi wywołaniami API. Zobaczyliśmy już, jak może to utrudnić prze-
prowadzanie testów, ale problemy dotyczą nie tylko testowania. Kod jest trudniejszy do
zrozumienia, gdy jest zaśmiecony obszernymi interfejsami zawierającymi dziesiątki nie-
używanych metod. Jeżeli utworzysz wąskie abstrakcje ukierunkowane na Twoje po-
trzeby, kod będzie objaśniać się lepiej, a Ty uzyskasz lepszą spoinę.
Jeżeli w naszym przykładzie skłonimy się do skorzystania z interfejsu ParameterSource,
odłączymy w rezultacie logikę zapisującą dane od ich konkretnych źródeł. Nie będziemy
już uzależnieni od interfejsów specyficznych dla J2EE.

Adaptacja parametru stanowi przypadek, w którym nie korzystamy z zachowywania sy-


gnatur (314) — bądź szczególnie uważny.

Adaptacja parametru może być ryzykowna, jeśli uproszczony interfejs, który two-
rzysz dla klasy parametrycznej, za bardzo różni się od bieżącego interfejsu parametru.
Jeżeli nie zachowasz ostrożności podczas wprowadzania zmian, możesz w rezultacie
wprowadzić do kodu subtelne modyfikacje. Jak zwykle pamiętaj, że naszym celem jest
usunięcie zależności w stopniu wystarczającym do rozmieszczenia testów. Powinieneś
skłaniać się bardziej w stronę zmian, co do których odczuwasz większą pewność, niż
zmian, które zapewniają lepszą strukturę — te drugie będziesz mógł wprowadzić po
przeprowadzeniu testów. Przykładowo w naszym przypadku możemy chcieć zmienić
interfejs ParameterSource w taki sposób, aby jego klienty nie musiały sprawdzać, czy
otrzymują wartość pustą, gdy wywołują jego metody — szczegóły znajdują się w omówie-
niu wzorca pustego obiektu (127).
ADAPTACJA PARAMETRU 331

Bezpieczeństwo przede wszystkim. Kiedy testy znajdują się już na swoich miejscach, bę-
dziesz mógł dokonywać bardziej inwazyjnych zmian z większą pewnością siebie.

Czynności
Aby skorzystać z adaptacji parametru, wykonaj następujące czynności:
1. Utwórz nowy interfejs, który zostanie użyty w metodzie. Niech będzie on maksy-
malnie prosty i przejrzysty, ale postaraj się nie tworzyć interfejsu, który będzie
wymagać czegoś więcej niż tylko dokonania trywialnych zmian w metodzie.
2. Utwórz implementer produkcyjny dla nowego interfejsu.
3. Utwórz fałszywy implementer dla interfejsu.
4. Napisz prosty przypadek testowy, uwzględniający przekazanie fałszywki do metody.
5. Wprowadź w metodzie zmiany, które sprawią, że będzie ona korzystać z nowych
parametrów.
6. Przeprowadź testy, aby sprawdzić, czy za pomocą fałszywki możesz poddać metodę
testom.
332 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

Wyłonienie obiektu metody


Z długimi metodami bardzo trudno pracuje się w wielu aplikacjach. Jeśli możesz utworzyć
instancję klasy, która je zawiera, i umieścić je w jarzmie testowym, będziesz mógł zabrać
się do pisania testów. W niektórych przypadkach trzeba sporo się napracować, aby
instancję takiej klasy można było utworzyć w oderwaniu od pozostałych metod. Nakład
takiej pracy może być zbyt wielki jak na zmiany, które chcesz wprowadzić. Jeżeli metoda,
której potrzebujesz, jest mała i nie korzysta z danych instancji, w celu przetestowania
swoich zmian użyj upublicznienia metody statycznej (346). Z drugiej jednak strony,
jeśli Twoja metoda jest duża albo korzysta ze zmiennych i metod instancji, weź pod uwagę
wyłonienie obiektu metody. W skrócie — idea kryjąca się za tą techniką refaktoryzacji
polega na przeniesieniu długiej metody do nowej klasy. Obiekty, które tworzysz, korzy-
stając z tej nowej klasy, nazywane są obiektami metody, ponieważ obejmują one kod
pojedynczej metody. Po zastosowaniu wyłonienia obiektu metody często pisanie testów
dla nowej klasy będzie prostsze, niż było w przypadku starej metody. Zmienne lokalne
ze starej metody mogą stać się zmiennymi instancji w nowej klasie, co często ułatwia
usunięcie zależności i przekształcenie kodu na lepszą postać.
Oto przykład w języku C++ (spore fragmenty klasy oraz metody zostały pominięte,
aby ocalić drzewa):
class GDIBrush
{
public:
void draw(vector<point>& renderingRoots,
ColorMatrix& colors,
vector<point>& selection);
...

private:
void drawPoint(int x, int y, COLOR color);
...
};

void GDIBrush::draw(vector<point>& renderingRoots,


ColorMatrix& colors,
vector<point>& selection)
{
for(vector<points>::iterator it = renderingRoots.begin();
it != renderingRoots.end();
++it) {
point p = *it;
...

drawPoint(p.x, p.y, colors[n]);


}
...

}
WYŁONIENIE OBIEKTU METODY 333

W klasie GDIBrush znajduje się długa metoda o nazwie draw. Nie możemy w prosty
sposób napisać dla niej testów i trudno będzie utworzyć instancję klasy GDIBrush w jarz-
mie testowym. Skorzystajmy z techniki wyłonienia obiektu metody w celu przeniesienia
metody draw do nowej klasy.
Pierwszy krok polega na utworzeniu nowej klasy, która będzie wykonywać pracę
metody draw. Możemy nazwać ją Renderer. Po jej utworzeniu dostanie ona publiczny
konstruktor. Argumenty tego konstruktora powinny być referencją do oryginalnej klasy,
natomiast argumenty referencją do oryginalnej metody. Musimy zachować sygnatury
(314) tej metody.
class Renderer
{
public:
Renderer(GBIBrush *brush,
vector<point>& renderingRoots,
ColorMatrix &colors,
vector<point>& selection);
...
};

Po utworzeniu konstruktora dodajemy zmienne instancji do każdego z argumen-


tów konstruktora i je inicjalizujemy. Aby zachować sygnatury (314), robimy to za po-
mocą wycinania, kopiowania i wklejania.
class Renderer
{
private:
GDIBrush *brush;
vector<point>& renderingRoots;
ColorMatrix& colors;
vector<point>& selection;

public:
Renderer(GDIBrush *brush,
vector<point>& renderingRoots,
ColorMatrix& colors,
vector<point>& selection)
: brush(brush), renderingRoots(renderingRoots),
colors(colors), selection(selection)
{}
};

Być może patrzysz na to wszystko i mówisz: „Hm, wygląda na to, że nie ruszymy z miej-
sca. Przyjmujemy referencję do GDIBrush i nie możemy utworzyć jej instancji w naszym
jarzmie testowym. Co będziemy z tego mieć?”. Poczekaj, skończymy jednak w innym
miejscu.
Po utworzeniu konstruktora możemy dodać kolejną metodę do tej klasy; metodę,
która wykona pracę wykonywaną przez draw(). Metodę tę także nazwiemy draw().
class Renderer
{
private:
334 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

GDIBrush *brush;
vector<point>& renderingRoots;
ColorMatrix& colors;
vector<point>& selection;

public:
Renderer(GDIBrush *brush,
vector<point>& renderingRoots,
ColorMatrix& colors,
vector<point>& selection)
: brush(brush), renderingRoots(renderingRoots),
colors(colors), selection(selection)
{}

void draw();
};

Teraz dodajemy ciało metody draw() do klasy Renderer. Kopiujemy ciało starej metody
draw() do metody nowej i korzystamy ze wsparcia kompilatora (317).
void Renderer::draw()
{
for(vector<points>::iterator it = renderingRoots.begin();
it != renderingRoots.end();
++it) {
point p = *it;
...
drawPoint(p.x, p.y, colors[n]);
}
...

Jeśli metoda draw() w klasie Renderer zawiera jakiekolwiek referencje do zmiennych


lub metod instancji w klasie GDIBRush, nasza kompilacja się nie powiedzie. Aby kompilacja
się udała, możemy utworzyć gettery dla zmiennych oraz upublicznić metody, od których
klasa ta zależy. W tym przypadku istnieje tylko jedna zależność — prywatna metoda o na-
zwie drawPoint. Jeśli upublicznimy ją w klasie GDIBrush, będziemy mogli uzyskać do niej
dostęp poprzez referencję do klasy Renderer i nasz kod się skompiluje.
Teraz możemy oddelegować metodę draw z klasy GDIBrush do nowej klasy Renderer.
void GDIBrush::draw(vector<point>& renderingRoots,
ColorMatrix &colors,
vector<point>& selection)
{
Renderer renderer(this, renderingRoots,
colors, selection);
renderer.draw();
}

Powróćmy do zależności związanej z klasą GDIBrush. Jeżeli mamy problem z utwo-


rzeniem instancji tej klasy w jarzmie testowym, możemy skorzystać z techniki wyod-
rębniania interfejsu, aby całkowicie pozbyć się związanej z nią zależności. W sekcji
WYŁONIENIE OBIEKTU METODY 335

wyodrębnianie interfejsu (361) opisano szczegóły, ale w skrócie chodzi o to, że tworzymy
pustą klasę interfejsu oraz implementujemy ją poprzez klasę GDIBrush. W tym przypadku
możemy nazwać ją PointRenderer, ponieważ drawPoint jest metodą z klasy GDIBrush,
do której tak naprawdę potrzebny jest nam dostęp z poziomu klasy Renderer. Następnie
zmieniamy referencję znajdującą się w klasie Renderer z GDIBrush na PointRenderer,
kompilujemy i pozwalamy, aby kompilator powiedział nam, które metody powinny zna-
leźć się w interfejsie. Oto jak pod koniec wygląda nasz kod:
class PointRenderer
{
public:
virtual void drawPoint(int x, int y, COLOR color) = 0;
};

class GDIBrush : public PointRenderer


{
public:
void drawPoint(int x, int y, COLOR color);
...
};

class Renderer
{
private:
PointRender *pointRenderer;
vector<point>& renderingRoots;
ColorMatrix& colors;
vector<point>& selection;

public:
Renderer(PointRenderer *renderer,
vector<point>& renderingRoots,
ColorMatrix& colors,
vector<point>& selection)
: pointRenderer(pointRenderer),
renderingRoots(renderingRoots),
colors(colors), selection(selection)
{}

void draw();
};

void Renderer::draw()
{
for(vector<points>::iterator it = renderingRoots.begin();
it != renderingRoots.end();
++it) {
point p = *it;
...
pointRenderer->drawPoint(p.x,p.y,colors[n]);
}
...

}
336 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

Rysunek 25.1 pokazuje wygląd naszych metod na diagramie UML.

Rysunek 25.1. Klasa GDIBrush po zastosowaniu techniki wyłaniania obiektu metody

Nasz ostateczny rezultat jest trochę dziwny. Mamy klasę (GDIBrush), która implemen-
tuje nowy interfejs (PointRenderer). Interfejs ten jest stosowany wyłącznie przez obiekt
(Renderer) tworzony przez klasę. Możesz czuć się nieswojo, ponieważ upubliczniliśmy
szczegóły, które były prywatne w pierwotnej klasie po to, abyśmy mogli skorzystać z tej
techniki. Teraz metoda drawPoint, która była prywatna w klasie GDIBrush, jest otwarta
na cały świat. Najważniejsze, na co powinniśmy zwrócić uwagę, to fakt, że tak naprawdę
to nie jest jeszcze koniec.
Wraz z upływem czasu coraz mniej będzie Ci się podobać, że nie możesz utworzyć
instancji oryginalnej klasy w jarzmie testowym i że usuwasz zależności, aby to zrobić.
Rozejrzysz się za innymi możliwościami. Na przykład, czy PointRenderer musi być inter-
fejsem? Czy może być klasą zawierającą GDIBrush? Jeśli tak, może będziesz mógł rozpocząć
przekształcanie projektu na zgodny z tą nową koncepcją obiektów klasy Renderer.
Jest to tylko jedna z prostych refaktoryzacji, jakie możemy przeprowadzić, gdy klasę
poddajemy testom. Uzyskana struktura może nadać się do wielu innych refaktoryzacji.

Wyłonienie obiektu metody występuje w kilku odmianach. W najprostszym przypadku


oryginalna metoda nie korzysta z żadnych zmiennych ani metod instancji pochodzących
z oryginalnej klasy. Nie musimy przekazywać referencji do klasy oryginalnej.
W innych przypadkach metoda korzysta z danych pochodzących wyłącznie od oryginalnej
klasy. Czasami sensowne jest wówczas umieszczenie tych danych w nowej, przechowującej
dane klasie i przekazanie jej w formie argumentu do obiektu metody.
Przypadek, który tu pokazałem, jest najgorszy. Potrzebujemy użyć metod z oryginalnej
klasy, w związku z czym skorzystaliśmy z wyodrębniania interfejsu (361) i rozpoczęliśmy
tworzenie abstrakcji między obiektem metody a oryginalną klasą.
WYŁONIENIE OBIEKTU METODY 337

Czynności
Możesz bezpiecznie korzystać z wyłaniania obiektu metody bez przeprowadzania testów,
wykonując następujące czynności:
1. Utwórz klasę, która będzie zawierać kod metody.
2. Utwórz konstruktor tej klasy i zachowaj sygnatury (314), aby otrzymał on dokład-
ną kopię argumentów, z których korzysta metoda. Jeżeli metoda korzysta z danych
instancji albo metod z oryginalnej klasy, jako pierwszy argument konstruktora
podaj referencję do tej klasy.
3. Dla każdego z argumentów konstruktora zadeklaruj zmienną instancji i nadaj jej
dokładnie taki sam typ, jaki ma zmienna. Zachowaj sygnatury (314), kopiując
wszystkie argumenty bezpośrednio do klasy i formatując je jako deklaracje
zmiennych instancji. Przypisz wszystkie argumenty zmiennym instancji w kon-
struktorze.
4. W nowej klasie utwórz pustą metodę uruchomieniową. Często metoda taka nosi
nazwę run(). W naszym przykładzie użyliśmy nazwy draw.
5. Skopiuj ciało starej metody do metody uruchomieniowej i ją skompiluj, aby uzy-
skać wsparcie kompilatora (317).
6. Komunikaty o błędach kompilatora powinny Ci wskazać, gdzie metoda w dalszym
ciągu korzysta z metod albo zmiennych pochodzących ze starej klasy. W każdym
z tych przypadków dokonaj poprawek koniecznych do skompilowania się metody.
Czasami będzie to prosta zmiana wywołania polegająca na użyciu referencji do
oryginalnej klasy. Innym razem być może będziesz musiał upublicznić metody
w klasie oryginalnej lub wprowadzić gettery, dzięki czemu nie będzie konieczne
upublicznianie zmiennych instancji.
7. Po skompilowaniu nowej klasy powróć do oryginalnej metody i zmień ją w taki
sposób, aby tworzyła instancję nowej klasy i delegowała do niej swoje zadania.
8. W razie potrzeby wyodrębnij interfejs (361), aby usunąć zależność od klasy
oryginalnej.
338 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

Uzupełnianie definicji
W niektórych językach można zadeklarować typ w jednym miejscu i zdefiniować go
w innym. Językami, w których możliwość taka jest najbardziej widoczna, są C i C++.
W obu tych językach można zadeklarować funkcję lub metodę w jednym miejscu i zdefi-
niować ją gdzieś indziej, zwykle w pliku implementacyjnym. Dysponując taką możliwością,
możemy z niej skorzystać w celu usuwania zależności.
Oto przykład:
class CLateBindingDispatchDriver : public CDispatchDriver
{
public:
CLateBindingDispatchDriver ();
virtual ~CLateBindingDispatchDriver ();

ROOTID GetROOTID (int id) const;

void BindName (int id,


OLECHAR FAR *name);
...

private:
CArray<ROOTID, ROOTID& > rootids;
};

Jest to deklaracja niewielkiej klasy w aplikacji C++. Użytkownicy tworzą obiekty klasy
CLateBindingDispatchDriver, po czym korzystają z metody BindName w celu powiązania
nazw z identyfikatorami. Chcemy udostępnić inny sposób wiązania nazw na czas korzy-
stania z tej klasy w testach. W C++ możemy to zrealizować za pomocą uzupełnienia
definicji. Metoda BindName została zadeklarowana w pliku nagłówkowym klasy. W ja-
ki sposób moglibyśmy ją zdefiniować inaczej na potrzeby testów? Przed rozpoczęciem
testów dołączymy w pliku testowym nagłówek zawierający deklarację tej klasy i udo-
stępnimy alternatywne definicje metod.
#include "LateBindingDispatchDriver.h"
CLateBindingDispatchDriver::CLateBindingDispatchDriver() {}
CLateBindingDispatchDriver::~CLateBindingDispatchDriver() {}
ROOTID GetROOTID (int id) const { return ROOTID(-1); }
void BindName(int id, OLECHAR FAR *name) {}

TEST(AddOrder,BOMTreeCtrl)
{
CLateBindingDispatchDriver driver;
CBOMTreeCtrl ctrl(&driver);

ctrl.AddOrder(COrderFactory::makeDefault());
LONGS_EQUAL(1, ctrl.OrderCount());
}
UZUPEŁNIANIE DEFINICJI 339

Kiedy definiujemy te metody bezpośrednio w pliku testowym, udostępniamy defi-


nicje, które zostaną użyte w testach. W metodach, na których nam nie zależy, możemy
pozostawić puste ciała lub możemy umieścić w nich metody objaśniające, z których
będziemy mogli korzystać we wszystkich naszych testach.
Kiedy uzupełniamy definicje w C albo C++, jesteśmy praktycznie zobowiązani do
utworzenia na potrzeby testów osobnego pliku wykonywalnego, który będzie korzy-
stać z uzupełnionych definicji. Jeśli tego nie zrobimy, podczas konsolidacji będą one
kolidować z rzeczywistymi definicjami. Kolejna wada takiego rozwiązania polega na
tym, że mamy teraz dwa różne zestawy definicji metod obecnych w klasie; jedną w te-
stowym pliku źródłowym i drugą w produkcyjnym pliku źródłowym. Stwarza to spory
problem związany z pielęgnacją kodu i jeśli poprawnie nie skonfigurujemy środowi-
ska, może dezorientować debugery. Z tych też powodów nie zalecam uzupełniania
definicji z wyjątkiem najtrudniejszych przypadków zależności, ale nawet wtedy radzę
stosowanie tej techniki jedynie w celu pozbycia się początkowych zależności. Zaraz
potem powinieneś szybko poddać klasę testom, dzięki czemu powielone definicje bę-
dzie można usunąć.

Czynności
W celu uzupełnienia definicji w C++ wykonaj następujące czynności:
1. Zidentyfikuj klasę zawierającą definicje, które chcesz zastąpić.
2. Upewnij się, że definicje metod znajdują się w pliku źródłowym, a nie w nagłówku.
3. Dołącz nagłówek do źródłowego pliku testowego lub testowanej klasy.
4. Upewnij się, że pliki źródłowe klasy nie są częścią kompilowanego kodu.
5. Skompiluj w celu znalezienia brakujących metod.
6. Dodawaj definicje metod do źródłowego pliku testowego, aż uda Ci się prze-
prowadzić pełną kompilację.
340 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

Hermetyzacja referencji globalnej


Kiedy próbujesz poddać testom kod, w którym występują problematyczne zależności
od elementów globalnych, zasadniczo masz trzy możliwości wyboru. Możesz postarać
się, aby elementy globalne zachowywały się inaczej podczas testów, możesz odwołać
się do innych elementów globalnych albo też przeprowadzić ich hermetyzację, dzięki
czemu będziesz mógł odseparować je jeszcze bardziej. Ostatnia opcja nazywa się her-
metyzacją referencji globalnej. Oto jej przykład w C++:
bool AGG230_activeframe[AGG230_SIZE];
bool AGG230_suspendedframe[AGG230_SIZE];
void AGGController::suspend_frame()
{
frame_copy(AGG230_suspendedframe,
AGG230_activeframe);
clear(AGG230_activeframe);
flush_frame_buffers();
}

void AGGController::flush_frame_buffers()
{
for (int n = 0; n < AGG230_SIZE; ++n) {
AGG230_activeframe[n] = false;
AGG230_suspendedframe[n] = false;
}
}

W przykładzie tym mamy trochę kodu, który pracuje z kilkoma tablicami global-
nymi. Metoda suspend_frame potrzebuje dostępu do aktywnych oraz zawieszonych
tablic. Na pierwszy rzut oka wygląda na to, że tablice mogłyby być elementami skła-
dowymi klasy AGGController, ale korzystają z nich inne (niepokazane tu) klasy. Co
możemy zrobić?
Naszą pierwszą myślą jest skorzystanie z parametryzacji metody (381) i przekazanie
tablic jako parametrów do metody suspend_frame, ale wówczas musielibyśmy przekazy-
wać je jako parametry do wszystkich metod wywoływanych przez suspend_frame,
które korzystają z nich jako tablic globalnych. Tutaj stoi nam na przeszkodzie metoda
flush_frame_buffer.
Następną opcją jest przekazanie do klasy AGGController obu tablic jako argumen-
tów konstruktora. Moglibyśmy tak postąpić, ale czy warto przyglądać się innym miejscom,
w których są one używane? Jeśli wygląda na to, że kiedy korzystamy z jednej tablicy,
używamy też i drugiej, moglibyśmy powiązać je ze sobą.

Jeżeli kilka elementów globalnych zawsze jest używanych lub modyfikowanych blisko siebie,
należą one do tej samej klasy.
HERMETYZACJA REFERENCJI GLOBALNEJ 341

Najlepszym sposobem poradzenia sobie z taką sytuacją jest spojrzenie na dane


oraz aktywne i zawieszone tablice, po czym zastanowienie się, czy możemy wymyślić
jakąś dobrą nazwę dla nowej „inteligentnej” klasy, zawierającą obie tablice. Czasami może
to być nieco trudne. Musimy zastanowić się, jakie znaczenie mają te dane w projekcie
i dlaczego się tam znalazły. Jeśli utworzymy nową klasę i w końcu przeniesiemy do niej
metody, prawdopodobnie okaże się, że ich kod istnieje już w innym miejscu, w którym
używane są te dane.

Nadając nazwę klasie, pomyśl o metodach, które ostatecznie w niej się znajdą. Nazwa po-
winna być dobra, ale nie musi być doskonała. Pamiętaj, że zawsze będziesz mógł zmienić
nazwę później.

W poprzednim przykładzie spodziewałem się, że wraz z upływem czasu metody


frame_copy i clear mogą zostać przeniesione do nowej klasy, którą utworzymy. Czy
jest jakaś praca wykonywana zarówno dla tablicy aktywnej, jak i zawieszonej? Wydaje się,
że tak. Funkcja suspend_frame klasy AGGController mogłaby prawdopodobnie zostać prze-
niesiona do nowej klasy, o ile będzie zawierać tablice suspended_frame oraz active_frame.
Jak moglibyśmy nazwać taką nową klasę? Na przykład po prostu Frame i stwierdzić, że
każdy obiekt tej klasy zawiera aktywny bufor oraz bufor zawieszony. Takie rozwiązanie
wymaga od nas zmiany naszej koncepcji i zmiany nazw zmiennych, jednak dzięki temu
dostaniemy inteligentniejszą klasę, która kryje więcej szczegółów.

Nazwa, którą wymyślisz dla klasy, może być już w użyciu. W takim przypadku zastanów
się, czy możesz zmienić nazwę elementu, który korzysta z wymyślonej przez Ciebie nazwy.

Oto jak to zrobimy krok po kroku.


Najpierw tworzymy klasę, która wygląda następująco:
class Frame
{
public:
// zadeklaruj AGG230_SIZE jako stałą
enum { AGG230_SIZE = 256 };

bool AGG230_activeframe[AGG230_SIZE];
bool AGG230_suspendedframe[AGG230_SIZE];
};

Nazwy danych specjalnie pozostawiliśmy takie same tylko po to, aby następny krok był
łatwiejszy. Teraz deklarujemy globalną instancję klasy Frame:
Frame frameForAGG230;

Następnie przekształcimy w komentarz oryginalne deklaracje danych i spróbujemy


skompilować kod:
// bool AGG230_activeframe[AGG230_SIZE];
// bool AGG230_suspendedframe[AGG230_SIZE];
342 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

W tym momencie otrzymamy wszelkie rodzaje błędów kompilacji, informujące nas, że


AGG230_activeframe i AGG230_suspendedframe nie istnieją, i grożące nam przeraźliwymi
konsekwencjami. Jeśli system kompilujący jest dostatecznie drażliwy, da sobie spokój
podczas próby konsolidacji i pozostawi nas z mniej więcej 10 stronami niepoprawionych
błędów konsolidacyjnych. Moglibyśmy się zdenerwować, ale przecież tego właśnie
oczekiwaliśmy, prawda?
Możemy poradzić sobie z tymi błędami, zatrzymując się przy każdym z nich i dopisując
frameForAGG230. przed każdą referencją powodującą problem.
void AGGController::suspend_frame()
{
frame_copy(frameForAGG230.AGG230_suspendedframe,
frameForAGG230.AGG230_activeframe);
clear(frameForAGG20.AGG230_activeframe);
flush_frame_buffer();
}

Kiedy już skończymy, nasz kod będzie brzydszy, ale da się skompilować i będzie
działać prawidłowo, tak więc nasze przekształcenie pozostawiło zachowanie. Teraz mo-
żemy przekazać obiekt klasy Frame konstruktorowi klasy AGGController i uzyskać od-
separowanie, które było nam potrzebne, aby przejść dalej.

Referencja do elementu składowego klasy zamiast do elementu globalnego to tylko pierw-


szy krok. W następnym etapie zastanów się, czy powinieneś skorzystać z wprowadzenia
statycznego settera (370), sparametryzować kod za pomocą parametryzacji konstruktora
(377), czy też sparametryzować metodę (381).

Wprowadziliśmy zatem nową klasę, dodając zmienne globalne do nowej klasy i upu-
bliczniając je. Dlaczego zrobiliśmy to w taki sposób? W końcu poświęciliśmy trochę
czasu na wymyślenie jej nazwy oraz zdecydowanie, jaki rodzaj metod w niej umieścić.
Moglibyśmy rozpocząć, tworząc fałszywy obiekt klasy Frame, do którego moglibyśmy
delegować w klasie AGGController, a całą logikę, która korzysta z tych zmiennych, prze-
nieść do rzeczywistej klasy Frame. Moglibyśmy tak postąpić, ale to bardzo dużo, jak na
jeden raz. Co gorsza, gdy nie mamy porozmieszczanych testów, a próbujemy wyko-
nać minimum pracy, aby je umieścić na miejscu, najlepiej pozostawić logikę w spokoju,
na ile jest to tylko możliwe. Powinniśmy unikać przenoszenia jej i podejmowania
prób uzyskania odseparowania poprzez umieszczanie spoin umożliwiających wywo-
ływanie jednej metody zamiast drugiej lub pobieranie pewnych danych zamiast in-
nych. Później, gdy na miejscu znajdzie się już więcej testów, będziemy mogli bezkar-
nie przenieść zachowanie z jednej klasy do innej.
Kiedy już przekażemy tablicę do klasy AGGController, będziemy mogli pozmieniać
niektóre nazwy, aby kod był czytelniejszy. Oto stan końcowy, jaki uzyskaliśmy po tej
refaktoryzacji:
class Frame
{
HERMETYZACJA REFERENCJI GLOBALNEJ 343

public:
enum { BUFFER_SIZE = 256 };
bool activebuffer[BUFFER_SIZE];
bool suspendedbuffer[BUFFER_SIZE];
};

Frame frameForAGG230;

void AGGController::suspend_frame()
{
frame_copy(frame.suspendedbuffer,
frame.activebuffer);
clear(frame.activeframe);
flush_frame_buffer();
}

Początkowo nie wygląda to na wielki postęp, ale ten pierwszy krok jest bardzo
cenny. Po przeniesieniu danych do klasy uzyskaliśmy separację i jesteśmy gotowi do
znacznej poprawy kodu w przyszłości. Być może w którymś momencie nawet zechcemy
mieć klasę FrameBuffer.

Kiedy chcesz skorzystać z hermetyzacji referencji globalnej, zacznij od danych albo ma-
łych klas. Więcej istotnych metod będziesz mógł przenieść do nowej klasy, kiedy na miej-
scu znajdzie się więcej testów.

We wcześniejszym przykładzie pokazałem, jak przeprowadzić hermetyzację refe-


rencji globalnej w odniesieniu do danych globalnych. To samo można zrobić z funk-
cjami nieskładowymi w programach napisanych w C++. Często zdarza się, że gdy pra-
cujesz w C z jakimś API, masz do czynienia z odwołaniami do funkcji globalnych
porozrzucanych w pewnym obszarze kodu, w którym chcesz pracować. Jedyną spoiną,
jaką masz do dyspozycji, są powiązania wywołań z ich odpowiednimi funkcjami. Aby
uzyskać separację, mógłbyś skorzystać z zastąpienia biblioteki (375), ale uzyskasz lepiej
ustrukturyzowany kod, jeśli zastosujesz hermetyzację referencji globalnej w celu otrzy-
mania kolejnej spoiny. Oto przykład.
We fragmencie kodu, który chcemy poddać testom, istnieją wywołania dwóch funkcji:
GetOption GetOption(const string optionName) oraz setOption(string name, Option
option). Są to funkcje wolne, nieprzypisane do żadnej klasy, ale często są wywoływane
w kodzie w następujący sposób:
void ColumnModel::update()
{
alignRows();
Option resizeWidth = ::GetOption("ResizeWidth");
if (resizeWidth.isTrue()) {
resize();
} else {
resizeToDefault();
}
}
344 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

W takim przypadku moglibyśmy rozważyć zastosowanie pewnych starych rozwią-


zań awaryjnych, takich jak parametryzacja metody (381) albo wyodrębnienie i prze-
słonięcie gettera (353). Jeśli jednak wywołania są dokonywane z wielu metod i wielu klas,
elegantszym rozwiązaniem będzie użycie hermetyzacji referencji globalnej. W tym celu
należy utworzyć nową klasę — taką jak ta:
class OptionSource
{
public:
virtual ~OptionSource() = 0;
virtual Option GetOption(const string& optionName) = 0;
virtual void SetOption(const string& optionName,
const Option& newOption) = 0;
};

Klasa ta zawiera metody abstrakcyjne dla każdej z wolnych funkcji, które są nam
potrzebne. Następnie tworzymy podklasę w celu uzyskania dla tej klasy fałszywki.
W tym przypadku moglibyśmy mieć w fałszywce mapę albo wektor umożliwiający
przechowywanie zestawu opcji, które zostaną użyte podczas testów. Moglibyśmy
mieć również metodę add lub zwyczajny konstruktor przyjmujący mapy — coś, co
byłoby wygodne podczas testów. Mając fałszywkę, moglibyśmy utworzyć rzeczywiste
źródło opcji:
class ProductionOptionSource : public OptionSource
{
public:
Option GetOption(const string& optionName);
void SetOption(const string& optionName,
const Option& newOption) ;
};

Option ProductionOptionSource::GetOption(
const string& optionName)
{
::GetOption(optionName);
}

void ProductionOptionSource::SetOption(
const string& optionName,
const Option& newOption)
{
::SetOption(optionName, newOption);
}

Aby dokonać hermetyzacji referencji do wolnych funkcji, utwórz klasę interfejsową z pod-
klasami fałszywymi oraz produkcyjnymi. Żadna z funkcji w kodzie produkcyjnym nie po-
winna robić nic więcej, jak tylko delegować do funkcji globalnej.
HERMETYZACJA REFERENCJI GLOBALNEJ 345

Nasza refaktoryzacja przyniosła całkiem dobre skutki. Wprowadziliśmy spoinę, a na


koniec przeprowadziliśmy prostą delegację do funkcji API. Teraz — gdy już to zrobiliśmy
— możemy przeprowadzić parametryzację klasy w taki sposób, aby przyjmowała obiekt
klasy OptionSource, dzięki czemu będziemy mogli wykorzystać fałszywkę w testach
oraz klasę rzeczywistą w produkcji.
W poprzednim przykładzie umieszczaliśmy funkcje w klasie i je wirtualizowaliśmy.
Czy możemy to zrobić w jakiś inny sposób? Tak, moglibyśmy utworzyć wolne funkcje,
które delegują do innych wolnych funkcji albo dodać je jako funkcje statyczne do no-
wej klasy, ale żadne z tych rozwiązań nie dałoby nam dobrych spoin. Aby zastąpić
jedną implementację inną, musielibyśmy użyć spoiny konsolidacyjnej (54) albo spoiny
preprocesowej (51). Gdy sięgamy po rozwiązanie korzystające z klasy i funkcji wirtualnej
oraz parametryzacji klasy, spoiny, które uzyskujemy są wyraźne i proste w użyciu.

Czynności
Aby przeprowadzić hermetyzację referencji globalnej, wykonaj następujące czynności:
1. Zidentyfikuj elementy globalne, które chcesz zahermetyzować.
2. Utwórz klasę, z której chcesz się do nich odwoływać.
3. Przekopiuj elementy globalne do tej klasy. Jeśli niektóre z nich są zmiennymi,
przeprowadź w tej klasie ich inicjalizację.
4. Przekształć w komentarz oryginalne deklaracje tych elementów globalnych.
5. Zadeklaruj globalną instancję nowej klasy.
6. Skorzystaj ze wsparcia kompilatora (317) w celu odszukania wszystkich nieob-
sługiwanych referencji do starych elementów globalnych.
7. Poprzedź każdą nieobsłużoną referencję nazwą globalnej instancji nowej klasy.
8. W miejscach, w których chcesz skorzystać z fałszywek, wprowadź statyczny setter
(370), dokonaj parametryzacji konstruktora (377), parametryzacji metody (381)
albo zastąp referencję globalną getterem (396).
346 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

Upublicznienie metody statycznej


Praca z klasami, których instancji nie można utworzyć w jarzmie testowym, wymaga
sprytu. Oto technika, którą stosuję w niektórych przypadkach. Jeśli masz metodę, która
nie korzysta z danych ani metod instancji, możesz przekształcić ją w metodę statyczną.
Kiedy będzie już statyczna, będziesz mógł poddać ją testom bez konieczności tworzenia jej
instancji. Oto przykład w Javie.
Mamy klasę z metodą validate i potrzebujemy dodać nowy warunek walidacji.
Niestety, utworzenie instancji tej klasy w oderwaniu od reszty kodu byłoby bardzo trudne.
Oszczędzę Ci traumy oglądania całej tej klasy — oto metoda, którą chcemy zmienić:
class RSCWorkflow
{
...
public void validate(Packet packet)
throws InvalidFlowException {
if (packet.getOriginator().equals( "MIA")
|| packet.getLength() > MAX_LENGTH
|| !packet.hasValidCheckSum()) {
throw new InvalidFlowException();
}
...
}
...
}

Co możemy zrobić, aby poddać tę metodę testom? Gdy przyjrzymy się jej bliżej,
widzimy, że korzysta z mnóstwa metod w klasie Packet. W rzeczywistości naprawdę
sporo sensu miałoby przeniesienie metody validate do klasy Packet, ale taki manewr
nie jest najmniej ryzykowną operacją, jaką możemy teraz przeprowadzić. Z pewnością
nie uda się nam zachować sygnatur (314). Jeśli nie masz automatycznego wsparcia do
przenoszenia metod, często najpierw będzie lepiej porozmieszczać testy. Pomocne
może w tym być upublicznienie metody statycznej. Mając już testy na miejscu, będziesz
mógł wprowadzić potrzebne zmiany i z o wiele większą pewnością siebie poprzenosić
metody później.

Gdy usuwasz zależności bez przeprowadzania testów, zachowuj sygnatury (314), jeżeli to
tylko będzie możliwe. Kiedy wycinasz lub kopiujesz i wklejasz całe sygnatury metod, za-
chodzi mniejsze prawdopodobieństwo, że popełnisz błędy.

Powyższy kod nie zależy od żadnych zmiennych ani metod instancji. Jak by wyglą-
dał, gdyby metoda validate była publiczna i statyczna? Ktokolwiek mógłby gdziekolwiek
w kodzie umieścić następującą instrukcję i dokonać walidacji pakietu:
RSCWorkflow.validate(packet);
UPUBLICZNIENIE METODY STATYCZNEJ 347

Prawdopodobnie ktoś, kto tę klasę utworzył, nigdy nie wyobrażał sobie, że ktoś inny
któregoś dnia mógłby tę klasę przekształcić w statyczną, nie wspominając nawet o jej
upublicznianiu. Czy to źle — zrobić coś takiego? Niekoniecznie. Hermetyzacja to świetne
rozwiązanie w odniesieniu do klas, ale statyczna część klasy nie jest tak naprawdę jej
częścią. W rzeczy samej, w niektórych językach stanowi ona część innej klasy, czasami
nazywanej metaklasą danej klasy.
Kiedy metoda jest statyczna, wiesz, że nie ma ona dostępu do żadnych prywatnych
danych klasy — jest to po prostu metoda pomocnicza. Jeśli ją upublicznisz, będziesz mógł
napisać dla niej testy. Testy te dadzą Ci wsparcie, jeśli później zdecydujesz się przenieść te
metody do innej klasy.

Statyczne metody i dane w rzeczywistości zachowują się, jakby były częściami innej klasy.
Dane statyczne żyją przez całe życie programu, a nie tylko przez życie instancji, i można
mieć do nich dostęp bez instancji.
Statyczne części klasy mogą być postrzegane jako „punkt zborny” dla elementów, które do
niej nie należą. Jeśli zobaczysz metodę, która nie korzysta z żadnych danych instancji, do-
brym pomysłem będzie przekształcenie jej w metodę statyczną, aby była lepiej widoczna
do czasu, kiedy stwierdzisz, do której klasy tak naprawdę ona należy.

Oto klasa RSCWorkflow po wyodrębnieniu statycznej metody validate.


public class RSCWorkflow {
public void validate(Packet packet)
throws InvalidFlowException {
validatePacket(packet);
}

public static void validatePacket(Packet packet)


throws InvalidFlowException {
if (packet.getOriginator() == "MIA"
|| packet.getLength() <= MAX_LENGTH
|| packet.hasValidCheckSum()) {
throw new InvalidFlowException();
}
...
}
...
}

W niektórych językach istnieje prostszy sposób na przeprowadzenie upublicznienia


metody statycznej. Zamiast wyodrębniać metodę statyczną z metody oryginalnej, możesz
po prostu przekształcić metodę oryginalną na statyczną. Jeżeli metoda ta jest używana przez
inne klasy, nadal będzie można mieć do niej dostęp z instancji tych klas. Oto przykład:
RSCWorkflow workflow = new RCSWorkflow();
...
// wywołanie statyczne, które wygląda jak niestatyczne
workflow.validatePacket(packet);
348 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

W niektórych językach jednak dostaniesz za coś takiego ostrzeżenie od kompilatora.


Najlepiej próbować doprowadzić kod do takiego stanu, w którym podczas kompilacji nie
ma żadnych ostrzeżeń.
Gdybyś martwił się, że ktoś mógłby zacząć używać metod statycznych w taki sposób,
który mógłby spowodować późniejsze problemy z zależnościami, mógłbyś odkryć metodę
statyczną, korzystając z któregoś niepublicznego trybu dostępu. W językach takich jak
Java i C#, które mają pakiety lub wewnętrzną widoczność, możesz ograniczyć dostęp do
metod statycznych lub przekształcić je w metody chronione i zapewnić do nich dostęp
poprzez podklasę testową. W C++ masz takie same możliwości: możesz przekształcić
metodę statyczną w chronioną albo skorzystać z przestrzeni nazw.

Czynności
Aby upublicznić metodę statyczną, wykonaj następujące czynności:
1. Napisz test sięgający do metody, którą chcesz odkryć jako publiczną i statyczną
metodę danej klasy.
2. Wyodrębnij ciało tej metody do metody statycznej. Pamiętaj o zachowaniu sy-
gnatur (314). Dla metody tej będziesz musiał użyć innej nazwy. Aby pomóc so-
bie w wymyśleniu nazwy, często będziesz mógł skorzystać z nazw parametrów. Je-
śli na przykład metoda o nazwie validate przyjmuje obiekty klasy Packet,
będziesz mógł wyodrębnić jej ciało jako metodę statyczną o nazwie validatePacket.
3. Przeprowadź kompilację.
4. Jeśli występują błędy związane z dostępem do danych lub metod instancji, przyjrzyj
się im i zastanów się, czy one także mogłyby być statyczne. Jeśli tak, przekształć
je na statyczne, aby system mógł się skompilować.
WYODRĘBNIENIE I PRZESŁONIĘCIE WYWOŁANIA 349

Wyodrębnienie i przesłonięcie wywołania


Czasami zależności, które przeszkadzają podczas testów mają charakter raczej lokalny.
Możemy mieć pojedyncze wywołanie metody, które potrzebujemy zastąpić. Jeśli uda się
nam usunąć zależność od tego wywołania, unikniemy podczas testowania dziwnych
efektów ubocznych albo przekazywania w wywołaniu wartości wyjaśniających.
Spójrzmy na przykład:
public class PageLayout
{
private int id = 0;
private List styles;
private StyleTemplate template;
...
protected void rebindStyles() {
styles = StyleMaster.formStyles(template, id);
...
}
...
}

Klasa PageLayout wywołuje funkcję statyczną o nazwie formStyles w klasie


StyleMaster, a wartość zwrotną przypisuje zmiennej instancji styles. Co możemy
zrobić, gdy chcemy dokonać rozpoznania poprzez funkcję formStyles albo usunąć naszą
zależność od klasy StyleMaster? Jedną z możliwości jest wyodrębnienie wywołania do
nowej metody i przesłonięcie jej w podklasie testowej. Taki zabieg jest nazywany wyod-
rębnieniem i przesłonięciem wywołania.
Oto kod po wyodrębnieniu:
public class PageLayout
{
private int id = 0;
private List styles;
private StyleTemplate template;
...
protected void rebindStyles() {
styles = formStyles(template, id);
...
}

protected List formStyles(StyleTemplate template,


int id) {
return StyleMaster.formStyles(template, id);
}
...
}

Teraz, gdy mamy już naszą lokalną metodę formStyles, możemy przesłonić ją w celu
usunięcia zależności. Style nie są nam potrzebne w testach, które teraz przeprowadzamy,
w związku z czym możemy po prostu zwrócić pustą listę.
350 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

public class TestingPageLayout extends PageLayout {


protected List formStyles(StyleTemplate template,
int id) {
return new ArrayList();
}
...
}

Kiedy zaczniemy opracowywać testy wymagające różnych stylów, będziemy mogli


zmienić tę metodę w taki sposób, aby można było konfigurować zwracane przez nią
elementy.
Wyodrębnienie i przesłonięcie wywołania jest bardzo przydatną techniką refak-
toryzacji. Bardzo często z niej korzystam. Idealnie nadaje się do usuwania zależności od
zmiennych globalnych i metod statycznych. Zwykle sięgam właśnie po nią, chyba że
istnieje wiele różnych odwołań do tego samego elementu globalnego. W takim przypadku
korzystam z zastąpienia referencji globalnej getterem (396).
Jeśli dysponujesz narzędziem do automatycznej refaktoryzacji, użycie wyodrębnienia
i przesłonięcia wywołania jest trywialnie proste. Możesz je przeprowadzić za pomocą
refaktoryzacji techniką wyodrębniania metody (411). Jeśli jednak takiego narzędzia nie
masz, wykonaj poniższe czynności. Umożliwiają one bezpieczne dokonanie wyodrębnienia,
nawet jeśli nie masz porozmieszczanych testów.

Czynności
Aby wyodrębnić i przesłonić wywołanie, wykonaj następujące czynności:
1. Zidentyfikuj wywołanie, które chcesz wyodrębnić. Odszukaj deklarację jego metody.
Skopiuj sygnaturę tej metody, aby umożliwić zachowanie sygnatur (314).
2. Utwórz nową metodę w bieżącej klasie. Nadaj jej sygnaturę, którą skopiowałeś.
3. Skopiuj wywołanie nowej metody i zastąp wywołanie starej metody wywołaniem
nowej metody.
WYODRĘBNIENIE I PRZESŁONIĘCIE METODY WYTWÓRCZEJ 351

Wyodrębnienie i przesłonięcie metody wytwórczej


Tworzenie obiektów w konstruktorach może być problematyczne, jeśli chcesz poddać
klasę testom. Czasami praca wykonywana w takich obiektach nie powinna mieć miejsca
w jarzmie testowym. Innym razem chciałbyś jedynie ulokować na miejscu obiekt ob-
jaśniający, ale nie możesz tego zrobić, ponieważ tworzenie tego obiektu jest na stałe
zapisane w kodzie.

Zapisana bezpośrednio w kodzie praca inicjalizacyjna konstruktorów może być trudna do


obejścia podczas przeprowadzania testów.

Spójrzmy na przykład:
{
public WorkflowEngine () {
Reader reader
= new ModelReader(
AppConfig.getDryConfiguration());

Persister persister
= new XMLStore(
AppConfiguration.getDryConfiguration());

this.tm = new TransactionManager(reader, persister);


...
}
...
}

WorkflowEngine tworzy w swoim konstruktorze obiekt klasy TransactionManager.


Gdyby proces ten zachodził w innym miejscu, łatwiej byłoby nam przeprowadzić se-
parację. Jedną z opcji, którą mamy do dyspozycji, jest wyodrębnienie i przesłonięcie
metody wytwórczej.

Wyodrębnienie i przesłonięcie metody wytwórczej jest całkiem skuteczną techniką, ale


możliwość jej zastosowania zależy od konkretnego języka. Na przykład nie można jej prze-
prowadzić w C++, ponieważ język ten nie pozwala, aby wywołania funkcji wirtualnych
odnosiły się do funkcji w klasach pochodnych. Z kolei Java i wiele innych języków na to
pozwala. Dobre rozwiązania w C++ to zastąpienie zmiennej instancji (401) oraz wyod-
rębnienie i przesłonięcie gettera (353). Problem ten został opisany przy okazji omawiania
zastępowania zmiennej instancji.

public class WorkflowEngine


{
public WorkflowEngine () {
this.tm = makeTransactionManager();
...
352 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

protected TransactionManager makeTransactionManager() {


Reader reader
= new ModelReader(
AppConfiguration.getDryConfiguration());

Persister persister
= new XMLStore(
AppConfiguration.getDryConfiguration());

return new TransactionManager(reader, persister);


}
...
}

Gdy mamy już naszą metodę wytwórczą, będziemy mogli utworzyć podklasę i ją
przesłonić, dzięki czemu będziemy mogli zwrócić nowego menedżera transakcji, gdy tylko
będzie potrzebny:
public class TestWorkflowEngine extends WorkflowEngine
{
protected TransactionManager makeTransactionManager() {
return new FakeTransactionManager();
}
}

Czynności
Aby wyodrębnić i przesłonić metodę wytwórczą, wykonaj następujące czynności:
1. Zidentyfikuj tworzenie obiektu w konstruktorze.
2. Wyodrębnij do metody wytwórczej całą pracę związaną z tworzeniem.
3. Utwórz podklasę testową i przesłoń w niej metodę wytwórczą, aby podczas testów
uniknąć zależności związanych z problematycznymi typami.
WYODRĘBNIENIE I PRZESŁONIĘCIE GETTERA 353

Wyodrębnienie i przesłonięcie gettera


Wyodrębnienie i przesłonięcie metody wytwórczej (351) jest skutecznym sposobem na
pozbycie się zależności od typów, ale nie działa on we wszystkich przypadkach. Sporą
„wyrwę” w możliwościach jego zastosowania stanowi C++. W języku tym nie można
wywoływać funkcji wirtualnych w klasach pochodnych z poziomu konstruktora klasy
bazowej. Na szczęście istnieje obejście tego problemu polegające wyłącznie na utworzeniu
obiektu w konstruktorze, bez wykonywania z nim żadnych dodatkowych prac.
Istotą tej techniki jest wprowadzenie gettera dla zmiennej instancji, którą chcesz
zastąpić fałszywym obiektem. Następnie należy przeprowadzić refaktoryzację, aby getter
był używany w każdym miejscu klasy. W dalszej kolejności tworzona jest podklasa,
a getter jest przesłaniany w celu poddania testom alternatywnych obiektów.
W naszym przykładzie tworzymy w konstruktorze menedżera transakcji. Chcemy
skonfigurować poszczególne elementy w taki sposób, aby klasa korzystała z tego me-
nedżera w wersji produkcyjnej, natomiast podczas testów z menedżera objaśniającego.
Oto z czym rozpoczynamy:
// WorkflowEngine.h
class WorkflowEngine
{
private:
TransactionManager *tm;
public:
WorkflowEngine ();
...
}

// WorkflowEngine.cpp
WorkflowEngine::WorkflowEngine()
{
Reader *reader
= new ModelReader(
AppConfig.getDryConfiguration());

Persister *persister
= new XMLStore(
AppConfiguration.getDryConfiguration());

tm = new TransactionManager(reader, persister);


...
}

A oto co uzyskaliśmy:
// WorkflowEngine.h
class WorkflowEngine
{
private:
TransactionManager *tm;

protected:
354 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

TransactionManager *getTransaction() const;

public:
WorkflowEngine ();
...
}

// WorkflowEngine.cpp
WorkflowEngine::WorkflowEngine()
:tm (0)
{
...
}

TransactionManager *getTransactionManager() const


{
if (tm == 0) {
Reader *reader
= new ModelReader(
AppConfig.getDryConfiguration());

Persister *persister
= new XMLStore(
AppConfiguration.getDryConfiguration());

tm = new TransactionManager(reader,persister);
}
return tm;
}
...

Pierwszą rzeczą, jaką robimy, jest wprowadzenie leniwego gettera — funkcji, która
przy pierwszym wywołaniu tworzy menedżera transakcji. W dalszej kolejności zastępujemy
użycie zmiennych wywołaniami tego gettera.

Leniwy getter to metoda, która dla wywołujących go funkcji wygląda jak zwykły getter.
Najważniejsza różnica jest taka, że leniwe gettery podczas pierwszego wywołania tworzą
obiekty, które powinny być przez nie zwracane. W tym celu zwykle zawierają one logikę,
która wygląda następująco:
Thing getThing() {
if (thing == null) {
thing = new Thing();
}
return thing;
}

Zwróć uwagę, jak inicjalizowana jest zmienna instancji thing. Leniwe gettery są używane
także w technice refaktoryzacji wzorzec projektowy singleton (372).

Kiedy już mamy ten getter, możemy utworzyć podklasę i przesłonić go w celu pod-
stawienia innego obiektu:
WYODRĘBNIENIE I PRZESŁONIĘCIE GETTERA 355

class TestWorkflowEngine : public WorkflowEngine


{
public:
TransactionManager *getTransactionManager()
{ return &transactionManager; }
FakeTransactionManager transactionManager;
};

Gdy korzystasz z wyodrębniania i przesłaniania gettera, powinieneś mieć na względzie


zagadnienia związane z czasem życia gettera, zwłaszcza w przypadku języków niesprzątających
pamięci, jakim jest na przykład C++. Nie zapomnij o skasowaniu instancji testowej w sposób
zgodny z tym, jak kod usuwa instancję produkcyjną.

W teście z łatwością możemy uzyskać dostęp do fałszywego menedżera transakcji, jeśli


zajdzie taka potrzeba:
TEST(transactionCount, WorkflowEngine)
{
auto_ptr<TestWorkflowEngine> engine(new TestWorkflowEngine);
engine.run();
LONGS_EQUAL(0,
engine.transactionManager.getTransactionCount());
}

Jedna z wad wyodrębniania i przesłaniania gettera polega na tym, że istnieje praw-


dopodobieństwo, iż ktoś mógłby użyć zmiennej przed jej inicjalizacją. Z tego powodu
dobrze byłoby upewnić się, że cały kod w klasie korzysta z gettera.
Wyodrębnianie i przesłanianie gettera jest techniką, z której korzystam bardzo
rzadko. Jeśli w obiekcie znajduje się tylko jedna problematyczna metoda, o wiele łatwiej
będzie przeprowadzić wyodrębnienie i przesłonięcie wywołania (349). Wyodrębnianie
i przesłanianie gettera będzie jednak lepszym wyborem, gdy w jednym obiekcie ist-
nieje wiele problematycznych metod. Jeśli możesz pozbyć się wszystkich problemów, wy-
odrębniając getter i go przesłaniając, zwycięstwo masz w kieszeni.

Czynności
Aby wyodrębnić i przesłonić getter, wykonaj następujące czynności:
1. Zidentyfikuj obiekt, dla którego potrzebujesz gettera.
2. Wyodrębnij do gettera całą logikę, jaka jest potrzebna do utworzenia obiektu.
3. Zastąp wszystkie użycia tego obiektu wywołaniami gettera oraz we wszystkich
konstruktorach zainicjalizuj pustą wartością referencję, która go przechowuje.
4. Dodaj do gettera logikę pierwszego wywołania, dzięki czemu obiekt zostanie
skonstruowany oraz przypisany do referencji, gdy referencja będzie pusta.
5. Utwórz podklasę klasy i przesłoń getter, aby udostępnić do testów alternatywny
obiekt.
356 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

Wyodrębnienie implementera
Wyodrębnianie interfejsu (361) jest wygodną techniką, ale jedna z jej części jest trudna:
wymyślenie nazwy. Często mam do czynienia z sytuacjami, w których chcę wyodrębnić
interfejs, ale nazwa, z jakiej chciałbym skorzystać, jest już nazwą klasy. Jeśli pracuję
w środowisku programistycznym, które wspiera zmiany nazw klas oraz wyodrębnianie
interfejsu, łatwo jest się tym zająć. W przeciwnym wypadku mam kilka możliwości
wyboru:
 Mogę wymyślić głupawą nazwę.
 Mogę przyjrzeć się metodom, których potrzebuję, i sprawdzić, czy stanowią one
podzbiór metod publicznych klasy. Jeśli tak, mogą one zasugerować nazwę dla
nowego interfejsu.
Jednym z rozwiązań, na które zwykle się nie decyduję podczas tworzenia nazw dla
nowych interfejsów, jest umieszczenie przedrostka „I” przed nazwą klasy, chyba że taka
konwencja została już przyjęta w bazie kodu. Nie ma nic gorszego niż praca nad niezna-
nym fragmentem kodu, w którym połowa nazw zaczyna się od I, a druga połowa nie.
Wpisując z klawiatury nazwę typu, w połowie przypadków popełnisz pomyłkę. Albo
pominiesz potrzebne I, albo i nie.

Nazewnictwo jest częścią projektu. Jeśli wybierzesz dobre nazwy, wprowadzisz do systemu
jasność i ułatwisz w nim pracę. Jeśli z kolei dobierzesz kiepskie nazwy, osłabisz zrozumia-
łość projektu, a życie programistów, którzy nadejdą po Tobie, obrócisz w piekło.

Jeśli nazwa klasy stanowi idealną nazwę dla interfejsu i nie mam narzędzi do au-
tomatycznej refaktoryzacji, korzystam z wyodrębniania implementera w celu uzyskania
potrzebnego mi odseparowania. Aby wyodrębnić implementer klasy, przekształcamy klasę
w interfejs, tworząc jej podklasę i przenosząc do niej wszystkie jej konkretne metody. Oto
przykład w C++:
// ModelNode.h
class ModelNode
{
private:
list<ModelNode *> m_interiorNodes;
list<ModelNode *> m_exteriorNodes;
double m_weight;
void createSpanningLinks();

public:
void addExteriorNode(ModelNode *newNode);
void addInternalNode(ModelNode *newNode);
void colorize();
...
};
WYODRĘBNIENIE IMPLEMENTERA 357

Pierwszy krok polega na przekopiowaniu deklaracji klasy ModelNode do innego pliku


nagłówkowego i zmianie nazwy tej kopii na ProductionModelNode. Oto fragment deklaracji
skopiowanej klasy:
// ProductionModelNode.h
class ProductionModeNode
{
private:
list<ModelNode *> m_interiorNodes;
list<ModelNode *> m_exteriorNodes;
double m_weight;
void createSpanningLinks();
public:
void addExteriorNode(ModelNode *newNode);
void addInternalNode(ModelNode *newNode);
void colorize();
...
};

Następny krok to powrót do nagłówka klasy ModelNode i pozbycie się wszystkich


niepublicznych deklaracji zmiennych oraz deklaracji metod. W dalszej kolejności prze-
kształcamy wszystkie pozostałe metody publiczne na metody czysto wirtualne (abs-
trakcyjne):
// ModelNode.h
class ModelNode
{
public:
virtual void addExteriorNode(ModelNode *newNode) = 0;
virtual void addInternalNode(ModelNode *newNode) = 0;
virtual void colorize() = 0;
...
};
Na tym etapie ModelNode jest czystym interfejsem — zawiera wyłącznie metody
abstrakcyjne. Pracujemy w C++, a zatem powinniśmy zadeklarować także czysto wirtual-
ny destruktor i zdefiniować go w pliku implementacyjnym:
// ModelNode.h
class ModelNode
{
public:
virtual ~ModelNode () = 0;
virtual void addExteriorNode(ModelNode *newNode) = 0;
virtual void addInternalNode(ModelNode *newNode) = 0;
virtual void colorize() = 0;
...
};

// ModelNode.cpp
ModelNode::~ModelNode()
{}
358 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

Teraz możemy powrócić do klasy ProductionModelNode i spowodować, aby dziedzi-


czyła nową klasę interfejsową:
#include "ModelNode.h"
class ProductionModelNode : public ModelNode
{
private:
list<ModelNode *> m_interiorNodes;
list<ModelNode *> m_exteriorNodes;

double m_weight;
void createSpanningLinks();

public:
void addExteriorNode(ModelNode *newNode);
void addInternalNode(ModelNode *newNode);
void colorize();
...

};

Teraz klasa ProductionModelNode powinna dać się elegancko skompilować. Jeśli


postanowisz skompilować także resztę systemu, znajdziesz miejsca, w których zacho-
dzą próby utworzenia obiektów klasy ModelNode. Możesz je zmienić, aby tworzone były
obiekty klasy ProductionModelNode. Podczas tej refaktoryzacji zastępujemy tworzenie
obiektów pewnej konkretnej klasy obiektami klasy innej, tak więc nasza ogólna sytuacja
związana z zależnościami w rzeczywistości wcale nie jest lepsza. Dobrze jednak byłoby
przyjrzeć się fragmentom, w których tworzone są obiekty, i postarać się zrozumieć,
czy możemy z nich skorzystać, aby ograniczyć zależności jeszcze bardziej.

Czynności
Aby wyodrębnić implementer, wykonaj następujące czynności:
Utwórz kopię źródła deklaracji klasy i nadaj jej inną nazwę. Warto mieć konwencję
nazewniczą dla wyodrębnianych klas. Osobiście często stosuję przyrostek Production,
aby wskazać, że nowa klasa jest kodem produkcyjnym implementera interfejsu.
1. Przekształć klasę źródłową w interfejs, usuwając wszystkie niepubliczne metody
oraz wszystkie zmienne.
2. Wszystkie pozostałe metody publiczne przekształć w metody abstrakcyjne. Jeśli
pracujesz w C++, upewnij się, że żadna z przekształcanych metod nie jest prze-
słaniana przez metody niewirtualne.
3. Przyjrzyj się wszystkim importom oraz dołączeniom plików realizowanym
w pliku interfejsu i sprawdź, czy są one niezbędne. Często będziesz mógł usunąć
wiele z nich. W celu ich odszukania możesz posłużyć się wsparciem kompilatora
(317) — po prostu kasuj je po kolei i rekompiluj kod, aby sprawdzić, czy usunięty
element był potrzebny.
WYODRĘBNIENIE IMPLEMENTERA 359

4. Dostosuj klasę produkcyjną w taki sposób, aby implementowała nowy interfejs.


5. Skompiluj klasę produkcyjną, aby upewnić się, że wszystkie sygnatury metod
w interfejsie zostały zaimplementowane.
6. Skompiluj pozostałą część systemu, aby znaleźć wszystkie miejsca, w których
tworzone są instancje klasy źródłowej. Zastąp je tworzeniem instancji nowej klasy
produkcyjnej.
7. Zrekompiluj kod i przetestuj go.

Przykład bardziej skomplikowany


Wyodrębnianie implementera jest względnie łatwe, kiedy klasa źródłowa w swojej
strukturze dziedziczenia nie ma żadnych klas nadrzędnych ani podrzędnych. Jeśli takie
klasy istnieją, będziemy musieli wykazać się odrobiną sprytu. Na rysunku 25.2 ponownie
pokazano klasę ModelNode, ale tym razem w Javie oraz z klasą nadrzędną i podrzędną.

Rysunek 25.2. Klasa ModelNode z klasami nadrzędną i podrzędną

W projekcie tym Node, ModelNode i LinkageNode są klasami konkretnymi. ModelNode


korzysta z metod chronionych z klasy Node, a także udostępnia metody używane przez
swoją podklasę, LinkageNode. Wyodrębnianie implementera wymaga konkretnej klasy,
którą można przekształcić na interfejs. Po takim zabiegu będziesz mieć zarówno interfejs,
jak i konkretną klasę.
Oto co możemy w takiej sytuacji zrobić — przeprowadzić wyodrębnienie imple-
mentera w stosunku do klasy Node, po umieszczeniu w hierarchii dziedziczenia klasy
ProductionNode poniżej klasy Node. Możemy także zmienić relację dziedziczenia w taki
sposób, aby klasa ModelNode dziedziczyła po klasie ProductionNode zamiast po Node.
Rysunek 25.3 pokazuje, jak nasz projekt będzie wyglądać po tych czynnościach.
360 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

Rysunek 25.3. Po wyodrębnieniu implementera z klasy Node

Następnie wyodrębniamy implementer z klasy ModelNode. Ponieważ klasa ta ma


swoją podklasę, w hierarchii dziedziczenia między klasy ModelNode a LinkageNode wpro-
wadzamy klasę ProductionModelNode. Kiedy już to zrobimy, będziemy mogli przeprowadzić
w interfejsie ModelNode dziedziczenie po klasie Node, co pokazano na rysunku 25.4.

Rysunek 25.4. Wyodrębnianie implementera z interfejsu ModelNode

Kiedy masz do czynienia z klasami funkcjonującymi w takiej hierarchii, naprawdę


powinieneś zastanowić się, czy nie będzie lepiej skorzystać z wyodrębniania interfejsu
(361) i wymyślenia nazw dla otrzymanych interfejsów. Jest to o wiele bardziej bezpośrednia
metoda refaktoryzacji.
WYODRĘBNIENIE INTERFEJSU 361

Wyodrębnienie interfejsu
W wielu językach wyodrębnianie interfejsu jest jedną z najbezpieczniejszych technik
usuwania zależności. Jeśli coś zrobisz źle, kompilator natychmiast Cię ostrzeże, tak
więc istnieje niewielkie prawdopodobieństwo wprowadzenia błędów. Technika ta po-
lega na utworzeniu interfejsu klasy z deklaracjami wszystkich metod, z których chcesz
skorzystać w pewnym kontekście. Kiedy już to zrobisz, będziesz mógł zaimplementować
interfejs w celu przeprowadzenia rozpoznania lub odseparowania, przekazując fałszywy
obiekt do klasy, którą chcesz przetestować.
Istnieją trzy sposoby na wyodrębnienie interfejsu oraz kilka kruczków, na które należy
zwrócić uwagę. Pierwszy sposób polega na skorzystaniu z automatycznego wsparcia
refaktoryzacji, które — jeśli masz szczęście — istnieje w Twoim środowisku. Narzędzia
wspierające refaktoryzację zwykle udostępniają jakiś sposób na dokonanie wyboru metod
w klasie i wpisanie nazw nowego interfejsu. Naprawdę dobre narzędzia zapytają Cię, czy
mają poszukać metod w całym kodzie i znaleźć miejsca, w których można zastąpić refe-
rencje w taki sposób, aby korzystały z nowego interfejsu. Takie narzędzie może zaosz-
czędzić Ci mnóstwo czasu.
Jeśli nie dysponujesz automatycznym wsparciem w wyodrębnianiu interfejsu, będziesz
mógł skorzystać z drugiej możliwości wyodrębniania metod — przeprowadzić wyodręb-
nianie stopniowo, postępując zgodnie z krokami, które opiszę poniżej.
Trzeci sposób na wyodrębnianie interfejsów polega na jednoczesnym wycięciu lub
skopiowaniu wielu metod z klasy i umieszczeniu ich deklaracji w interfejsie. Nie jest to
równie bezpieczne, jak dwie pierwsze metody, niemniej pozostaje w miarę bezpieczne
i często jest jedyną praktyczną metodą wyodrębnienia interfejsu, kiedy nie dysponujesz
automatycznym wsparciem, a kompilacje zabierają dużo czasu.
Wyodrębnijmy zatem interfejs, posługując się drugą metodą. Kiedy będziemy się
tym zajmować, omówię parę spraw, na które należy zwrócić uwagę.
Potrzebujemy wyodrębnić interfejs w celu poddania testom klasy PaydayTransaction.
Rysunek 25.5 pokazuje tę klasę oraz jedną z jej zależności — klasę TransactionLog.

Rysunek 25.5. Klasa PaydayTransaction zależna on klasy TransactionLog


362 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

Tak wygląda nasz przypadek testowy:


void testPayday()
{
Transaction t = new PaydayTransaction(getTestingDatabase());
t.run();

assertEquals(getSampleCheck(12),
getTestingDatabase().findCheck(12));
}

Aby jednak przypadek ten mógł się skompilować, musimy przekazać w nim jakiś
obiekt klasy TransactionLog. Utwórzmy zatem odwołanie do klasy FakeTransactionLog,
która jeszcze nie istnieje.
void testPayday()
{
FakeTransactionLog aLog = new FakeTransactionLog();
Transaction t = new PaydayTransaction(
getTestingDatabase(),
aLog);
t.run();
assertEquals(getSampleCheck(12),
getTestingDatabase().findCheck(12));
}

Aby kod ten skompilował się, musimy dla klasy TransactionLog wyodrębnić in-
terfejs, spowodować, aby klasa FakeTransactionLog zaimplementowała ten interfejs,
a następnie umożliwić w klasie PaydayTransaction przyjmowanie obiektów klasy
FakeTransactionLog.
Zacznijmy od początku: wyodrębnimy interfejs. Tworzymy pustą klasę o nazwie
TransactionRecorder. Jeśli zastanawiasz się, skąd wzięła się ta nazwa, rzuć okiem na
poniższe wskazówki.

Nadawanie nazw interfejsom


Interfejsy są względnie nowymi konstrukcjami programistycznymi. Istnieją one w Javie
oraz w wielu językach .NET. W C++ musisz je symulować, tworząc klasę, która zawiera
wyłącznie czyste funkcje wirtualne.
Kiedy interfejsy po raz pierwszy zostały wprowadzone w językach programowania, niektóre
osoby zaczęły nadawać im nazwy poprzez umieszczenie litery I przed nazwą klasy, z której
zostały wywiedzione. Jeśli na przykład miałeś klasę Account, a potrzebny był Ci interfejs, mogłeś
go nazwać IAccount. Zaleta takiego sposobu nazywania polega na tym, że tak naprawdę nie mu-
sisz myśleć o nazwach, kiedy dokonujesz wyodrębniania. Nadanie nazwy jest tak proste,
jak dodanie przedrostka. Wada polega na tym, że otrzymujesz mnóstwo kodu, który musi
wiedzieć, czy ma do czynienia z interfejsem. W idealnej sytuacji wcale nie powinno go to
obchodzić. Masz również bazę kodu, w której część nazw zaczyna się na I, a część nie.
Usunięcie I, gdybyś chciał powrócić do zwykłych klas, okazuje się ogromną zmianą. Jeśli
jej nie przeprowadzisz, poprzednia nazwa pozostanie w kodzie jako subtelne kłamstwo.
WYODRĘBNIENIE INTERFEJSU 363

Kiedy opracowujesz klasy, najłatwiej będzie tworzyć dla nich proste nazwy, nawet w przy-
padku dużych abstrakcji. Jeśli na przykład piszemy pakiet księgowy, możemy zacząć od klasy,
która nazywa się po prostu Account. Następnie możemy rozpocząć tworzenie testów w celu do-
dania nowej funkcjonalności. W pewnym momencie możesz zechcieć, żeby klasa Account
stała się interfejsem. W takim przypadku będziesz mógł utworzyć jej podklasę, przenieść w dół
wszystkie dane oraz metody i przekształcić Account w interfejs. Gdy tak postąpisz, nie będziesz
musiał przedzierać się przez cały kod w celu zmiany typu każdego odwołania do Account.
W przypadkach takich jak w przykładzie z klasą PaydayTransaction, gdy mamy już dobrą
nazwę dla interfejsu (TransactionLog), możemy zrobić to samo. Wadą takiego rozwiązania
jest duża liczba kroków, która wiąże się ze spychaniem danych i metod w dół, do nowej
podklasy. Jeśli jednak ryzyko jest dostatecznie małe, korzystam od czasu do czasu z tego
sposobu. Jest on nazywany wyodrębnianiem implementera (356).
Jeżeli nie mam wielu testów, a chcę wyodrębnić interfejs, aby mieć więcej w jednym miejscu,
często próbuję utworzyć dla niego nową nazwę. Czasami wymyślenie nazwy może trochę po-
trwać. Jeśli nie masz narzędzi zmieniających nazwy klas za Ciebie, opłaca się ustalić nazwę,
z której będziesz korzystać, zanim liczba miejsc, w których jest ona używana, stanie się zbyt duża.

interface TransactionRecorder
{
}
Teraz możemy powrócić i sprawić, że klasa TransactionLog zaimplementuje nowy
interfejs.
public class TransactionLog implements TransactionRecorder
{
...
}

Następnie tworzymy FakeTransactionLog również jako pustą klasę.


public class FakeTransactionLog implements TransactionRecorder
{
}

Kod powinien się skompilować bez problemów, ponieważ wszystkim, co zrobili-


śmy, było jedynie wprowadzenie kilku nowych klas oraz zmiana istniejącej klasy, aby
implementowała pusty interfejs.
Teraz zaczynamy refaktoryzować z pełną mocą. Zmieniamy typ każdej referencji
w miejscach, w których chcemy użyć interfejsu. Klasa PaydayTransaction korzysta z klasy
TransactionLog. Musimy ją zmienić w taki sposób, aby używała interfejsu Transaction
Recorder. Kiedy już to zrobimy i skompilujemy kod, znajdziemy parę przypadków,
w których metody są wywoływane z poziomu interfejsu TransactionRecorder. Będziemy
mogli pozbyć się błędów jeden po drugim, dodając deklaracje metod do tego interfejsu
oraz puste definicje metod do klasy FakeTransactionLog.
Oto przykład:
364 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

public class PaydayTransaction extends Transaction


{
public PaydayTransaction(PayrollDatabase db,
TransactionRecorder log) {
super(db, log);
}

public void run() {


for(Iterator it = db.getEmployees(); it.hasNext(); ) {
Employee e = (Employee)it.next();
if (e.isPayday(date)) {
e.pay();
}
}
log.saveTransaction(this);
}
...
}

W tym przypadku jedyną metodą, którą wywołujemy w interfejsie Transaction


Recorder, jest saveTransaction. Ponieważ w interfejsie tym nie ma jeszcze metody
saveTransaction, pojawi się błąd kompilacji. Możemy skompilować nasz test, dodając po
prostu tę metodę do interfejsu TransactionRecorder oraz klasy FakeTransactionLog.
interface TransactionRecorder
{
void saveTransaction(Transaction transaction);
}

public class FakeTransactionLog implements TransactionRecorder


{
void saveTransaction(Transaction transaction) {
}
}

I to by było tyle. Nie musimy już tworzyć w naszych testach prawdziwych obiek-
tów klasy TransactionLog.
Możesz na to wszystko spojrzeć i powiedzieć: „Hej, tak naprawdę to jeszcze nie
koniec. Nie dodaliśmy jeszcze metody recordError do interfejsu i do fałszywki”. To
prawda, metoda recordError znajduje się w klasie transactionLog. Gdybyśmy potrzebo-
wali wyodrębnić cały interfejs, moglibyśmy ją wprowadzić również do interfejsu, ale tak
naprawdę nie jest nam ona potrzebna w teście. Chociaż dobrze byłoby mieć interfejs
obejmujący wszystkie publiczne metody klasy, to gdybyśmy podążyli tą drogą, czekałoby
nas o wiele więcej pracy, niż potrzeba do przetestowania danego fragmentu aplikacji.
Jeśli czeka Cię praca z projektem, w którym określone kluczowe abstrakcje mają interfejsy
obejmujące w całości zbiór metod publicznych w ich klasach, pamiętaj, że możesz
stopniowo dotrzeć do potrzebnego stanu. Nieraz przed dokonaniem poważniejszej
zmiany lepiej jest chwilowo się wstrzymać do czasu, gdy będziesz mieć lepsze pokrycie
kodu testami.
WYODRĘBNIENIE INTERFEJSU 365

Kiedy wyodrębniasz interfejs, nie musisz wyodrębniać wszystkich publicznych metod z danej
klasy. Skorzystaj ze wsparcia kompilatora (317), aby dowiedzieć się, które z nich są używane.

Jedyna trudność ma miejsce wtedy, gdy mamy do czynienia z metodami niewirtu-


alnymi. W Javie mogą być nimi metody statyczne. Języki takie jak C# i C++ także zezwa-
lają na niewirtualne metody instancji. Więcej informacji na temat radzenia sobie w tego ty-
pu sytuacjach znajdziesz w następnej ramce.

Czynności
Aby wyodrębnić interfejs, wykonaj następujące czynności:
1. Utwórz nowy interfejs o nazwie, której chciałbyś użyć. Nie dodawaj do niego
jeszcze żadnych metod.
2. Zmień klasę, z której wyodrębniasz w taki sposób, aby implementowała interfejs.
Niczego nie popsujesz, ponieważ w interfejsie nie ma jeszcze żadnych metod. Mi-
mo wszystko dobrze będzie teraz skompilować kod i przeprowadzić test tylko po
to, aby się o tym przekonać.
3. Zmień miejsce, w którym chcesz użyć obiektu w taki sposób, aby zamiast z ory-
ginalnej klasy skorzystać z interfejsu.
4. Skompiluj system i wprowadź deklarację nowej metody z interfejsu do każdej
z metod, której użycie zostanie zgłoszone przez kompilator jako błędne.

Wyodrębnianie interfejsu i funkcji niewirtualnych


Jeśli masz w swoim kodzie takie wywołanie: bondRegistry.newFixedYield(client), to wyłącz-
nie patrząc na nie, trudno będzie w wielu językach stwierdzić, czy mamy do czynienia z metodą
statyczną, czy też z wirtualną lub niewirtualną metodą instancji. W językach, które zezwalają na
niewirtualne metody instancji, możesz narobić sobie problemów, jeśli wyodrębnisz interfejs
i dodasz do niego sygnaturę jednej z niewirtualnych metod klasy. Jeżeli Twoja klasa nie ma
podklas, zwykle będziesz mógł utworzyć metodę wirtualną, po czym wyodrębnić interfejs.
Wszystko się uda. Jeśli jednak Twoja klasa ma podklasy, przeniesienie sygnatury metody do
interfejsu może uszkodzić kod. Oto przykład w C++. Mamy klasę z metodą niewirtualną:
class BondRegistry
{
public:
Bond *newFixedYield(Client *client) { ... }
};

Mamy także podklasę z metodą o takiej samej nazwie i sygnaturze:


class PremiumRegistry : public BondRegistry
{
public:
Bond *newFixedYield(Client *client) { ... }
};
366 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

Jeśli wyodrębnimy interfejs z klasy BondRegistry:


class BondProvider
{
public:
virtual Bond *newFixedYield(Client *client) = 0;
};

i ją zaimplementujemy:
class BondRegistry : public BondProvider { … };

możemy uszkodzić następujący kod, przekazując obiekt klasy PremiumRegistry:


void disperse(BondRegistry *registry) {
...
Bond *bond = registry->newFixedYield(existingClient);
...
}

Przed wyodrębnieniem interfejsu metoda newFixedYield klasy BondRegistry była wywoływana,


ponieważ typem zmiennej rejestru w czasie kompilacji jest BondRegistry. Jeżeli w procesie
wyodrębniania przekształcimy metodę newFixedYield na wirtualną, zmienimy zachowanie
kodu. Wywoływana będzie metoda w klasie PremiumBondRegistry. Kiedy w C++ przekształca-
my metodę na wirtualną w klasie bazowej, metody, które ją przesłaniają w podklasie, stają
się wirtualne. Zwróć uwagę, że takiego problemu nie mamy w Javie ani w C#. W Javie
wszystkie metody instancji są wirtualne. Z kolei w C# sprawy mają się lepiej, gdyż dodanie
interfejsu nie wpływa na istniejące wywołania metod niewirtualnych.
Zwykle utworzenie w klasie pochodnej metody o takiej samej sygnaturze, jaką ma niewirtualna
metoda w klasie bazowej, nie jest dobrym pomysłem w C++, ponieważ zabieg taki może
prowadzić do nieporozumień. Jeśli chcesz mieć dostęp do funkcji niewirtualnej poprzez interfejs,
a nie znajduje się ona w klasie bez podklas, najlepsze, co można zrobić, to dodać nową metodę
wirtualną o nowej nazwie. Taka metoda może delegować do metody niewirtualnej albo nawet
statycznej. Musisz tylko upewnić się, że metoda ta wykonuje właściwe zadania dla każdej z podklas
znajdujących się poniżej klasy, z której wyodrębniasz.
WPROWADZENIE DELEGATORA INSTANCJI 367

Wprowadzenie delegatora instancji


Metody statyczne używane są w klasach z wielu różnych powodów. Jednym z najczęściej
spotykanych jest implementacja wzorca projektowego singleton (370). Innym powodem
do zastosowania metod statycznych jest tworzenie klas pomocniczych.
Klasy pomocnicze łatwo można znaleźć w wielu projektach. Są to klasy, które nie
zawierają żadnych zmiennych ani metod instancji. Zamiast tego znajduje się w nich
zestaw statycznych metod i stałych.
Programiści tworzą klasy pomocnicze z różnych powodów. W większości przypadków
robią to, gdy trudno jest znaleźć wspólną abstrakcję dla zbioru metod. Takim przykładem
jest klasa Math w JDK. Zawiera ona metody statyczne dla funkcji trygonometrycznych
(cos, sin, tan) oraz wiele innych metod. Kiedy projektanci języków budują je „w całej
rozciągłości” z obiektów, upewniają się, że numeryczne typy proste będą wiedzieć, jak
przeprowadzać takie operacje. Na przykład powinieneś mieć możliwość wywołania
metody sin() dla obiektu 1 lub dowolnego innego obiektu i uzyskania poprawnego
wyniku. W chwili, w której piszę te słowa, Java nie wspiera metod matematycznych
w odniesieniu do typów podstawowych, tak więc klasa pomocnicza jest rozsądnym roz-
wiązaniem, chociaż stanowi także przypadek specjalny. Aby wykonać swoją pracę, prawie
zawsze możesz korzystać ze zwykłych, starych klas z danymi instancji oraz metodami.
Jeśli w swoim projekcie masz metody statyczne, najprawdopodobniej nie popadniesz
z ich powodu w kłopoty, chyba że zawierają one coś, na czym trudno polegać w teście
(techniczna nazwa tego to przywieranie statyczne). W takich przypadkach chciałbyś
mieć możliwość użycia spoiny obiektowej (58) w celu zastąpienia określonego zachowa-
nia, gdy wywoływane są metody statyczne. Co można wówczas zrobić?
Jednym z rozwiązań, które można zastosować, jest rozpoczęcie wprowadzania w klasie
delegacji metod instancji. Kiedy to zrobisz, będziesz musiał znaleźć sposób na zastąpienie
w obiekcie wywołań statycznych wywołaniami metod. Oto przykład:
public class BankingServices
{
public static void updateAccountBalance(int userID,
Money amount) {
...
}
...
}

Mamy tu klasę, która zawiera wyłącznie metody statyczne. Pokazałem tylko jedną
z nich, ale chyba rozumiesz, co mam na myśli. Możemy dodać do takiej klasy metodę
instancji i delegować ją do metody statycznej:
public class BankingServices
{
public static void updateAccountBalance(int userID,
Money amount) {
368 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

...
}

public void updateBalance(int userID, Money amount) {


updateAccountBalance(userID, amount);
}
...
}

W tym przypadku dodaliśmy metodę instancji o nazwie updateBalance, która de-


leguje do metody statycznej updateAccountBalance.
Teraz możemy w kodzie wywołującym zastąpić takie referencje:
public class SomeClass
{
public void someMethod() {
...
BankingServices.updateAccountBalance(id, sum);
}
}

następującymi:
public class SomeClass
{
public void someMethod(BankingServices services) {
...
services.updateBalance(id,sum);
}

Zwróć uwagę, że odniesiemy sukces tylko wtedy, gdy uda nam się znaleźć jakiś
sposób na zewnętrzne utworzenie obiektu klasy BankingServices, z którego korzystamy.
Jest to dodatkowy etap refaktoryzacji, a w językach typowanych statycznie możemy sko-
rzystać ze wsparcia kompilatora (317), aby obiekt znalazł się na miejscu.
Technikę tę można bezpośrednio stosować w odniesieniu do wielu metod statycznych,
ale kiedy zaczniesz ją stosować do metod pomocniczych, możesz poczuć się niepewnie.
Klasa z 5 albo 10 metodami statycznymi i dwoma metodami instancji istotnie wygląda
dziwnie. Wygląda jeszcze dziwniej, gdy są to tylko proste metody delegujące do metod
statycznych. Kiedy jednak korzystasz z tej techniki, możesz łatwo rozmieszczać spoiny
obiektowe i na czas testów zastępować różne zachowania. Wraz z upływem czasu możesz
doprowadzić do sytuacji, w której każde odwołanie do klasy pomocniczej przechodzi
przez metody delegujące. Będziesz mógł wtedy przenieść ciała metod statycznych do
metod instancji i usunąć metody statyczne.

Czynności
Aby wprowadzić delegator instancji, wykonaj następujące czynności:
1. Zidentyfikuj metodę statyczną, której użycie w testach stwarza problemy.
WPROWADZENIE DELEGATORA INSTANCJI 369

2. Dla metody w klasie utwórz metodę instancji. Pamiętaj o zachowaniu sygnatur


(314). Oddeleguj metodę instancji do metody statycznej.
3. Odszukaj miejsca, w których metody statyczne są używane w testowanej klasie.
W celu przekazania instancji do miejsca, w którym nastąpiło wywołanie metody
statycznej, skorzystaj z parametryzacji metody (381) albo innej techniki usuwania
zależności.
370 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

Wprowadzenie statycznego settera


Może jestem purystą, ale nie lubię zmieniających się danych globalnych. Kiedy odwie-
dzam zespoły programistów, jest to najczęściej spotykana przeszkoda utrudniająca spraw-
dzenie fragmentów ich systemu w jarzmie testowym. Chcesz umieścić w jarzmie testowym
zbiór klas, ale odkrywasz, że niektóre z nich powinny być skonfigurowane w określony spo-
sób, żeby w ogóle można było z nich korzystać. Po skonfigurowaniu jarzma musisz zająć się
listą danych globalnych, aby zagwarantować, że każda z nich znajduje się w stanie odpo-
wiednim dla warunku, których chcesz przetestować. Fizycy kwantowi nie odkryli „upiorne-
go działania na odległość”, ale w oprogramowaniu mamy z nim do czynienia od lat.
Przestańmy jednak narzekać na dane globalne; występują one w wielu systemach.
Czasami są bardzo bezpośrednie i same siebie nieświadome — po prostu ktoś gdzieś
zadeklarował zmienną. Czasem są poprzebierane za singletony w pełni zgodne ze
wzorcem projektowym singleton. W każdym przypadku umieszczenie na miejscu fał-
szywki w celach testowych jest bardzo łatwe. Jeśli zmienna jest znajdującą się poza
klasą bezwstydną daną globalną albo istniejącą gdzieś w kodzie statyczną zmienną pu-
bliczną, możesz po prostu ją zastąpić. Jeśli typ referencji to const albo final, być może
będziesz musiał tę ochronę usunąć. Pozostaw w kodzie komentarz, że zrobiłeś to tylko
na potrzeby testów i że inni programiści nie powinni korzystać z przewagi, jaką daje
taki rodzaj dostępu w kodzie produkcyjnym.

Wzorzec projektowy singleton


Wzorzec projektowy singleton jest wzorcem używanym przez wiele osób w celu zagwa-
rantowania, że w programie będzie mogła istnieć wyłącznie jedna instancja danej klasy.
Wyróżniamy trzy właściwości wspólne dla wielu singletonów:
1. Konstruktory klasy singletonowej są zwykle prywatne.
2. Statyczny element składowy klasy przechowuje jedyną jej instancję, jaka w ogóle zosta-
nie utworzona w programie.
3. W celu zapewnienia dostępu do instancji wykorzystywana jest metoda statyczna. Zwy-
kle metoda ta nosi nazwę instance.
Chociaż singletony powstrzymują programistów od tworzenia więcej niż tylko jednej in-
stancji klasy w kodzie produkcyjnym, to powstrzymują ich także przed tworzeniem kilku
instancji klasy w jarzmie testowym.

Zastępowanie singletonów wiąże się z niewielkim nakładem pracy. Dodajesz do


singletona statyczny setter, aby zastąpić instancję, a następnie zmieniasz rodzaj konstruk-
tora na chroniony. Teraz możesz już założyć podklasę singletona, utworzyć świeży obiekt
i przekazać go do settera.
WPROWADZENIE STATYCZNEGO SETTERA 371

Konieczność rezygnacji z ochrony dostępu podczas korzystania ze statycznego settera


może budzić w Tobie niesmak, ale pamiętaj, że celem takiej ochrony jest zapobieganie
powstawaniu błędów. Przeprowadzanie testów ma taki sam cel. Okazuje się, że w tym
przypadku będziemy potrzebować silniejszego narzędzia.
Oto przykład na wprowadzenie statycznego settera w C++:
void MessageRouter::route(Message *message) {
...
Dispatcher *dispatcher
= ExternalRouter::instance()->getDispatcher();
if (dispatcher != NULL)
dispatcher->sendMessage(message);
}

W klasie MessageRouter korzystamy w kilku miejscach z singletonów w celu uzyskania


dyspozytorów. Jednym z takich singletonów jest klasa ExternalRouter. Korzysta on ze
statycznej metody o nazwie instance w celu zapewnienia dostępu do jedynej instancji
klasy ExternalRouter. W klasie tej znajduje się getter dla dyspozytora. Możemy zastąpić
jeden dyspozytor innym, zastępując zewnętrzny router, który go obsługuje.
Przed wprowadzeniem statycznego settera klasa ExternalRouter wygląda nastę-
pująco:
class ExternalRouter
{
private:
static ExternalRouter *_instance;
public:
static ExternalRouter *instance();
...
};

ExternalRouter *ExternalRouter::_instance = 0;

ExternalRouter *ExternalRouter::instance()
{
if (_instance == 0) {
_instance = new ExternalRouter;
}
return _instance;
}

Zauważ, że router jest tworzony podczas pierwszego wywołania metody instance. Aby
zastąpić go innym routerem, musimy zmienić to, co zwraca metoda instance. Pierwszy
krok polega na wprowadzeniu nowej metody, która ją zastąpi.
{
delete _instance;
_instance = newInstance;
}
372 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

Oczywiście założyliśmy, że mamy możliwość utworzenia nowej instancji. Kiedy


programiści korzystają z wzorca singleton, często tworzą w klasie prywatny konstruktor,
aby uniemożliwić tworzenie wielu instancji. Jeśli konstruktor będzie chroniony, będziesz
mógł utworzyć podklasę singletona w celu rozpoznania albo odseparowania i przekazania
nowej instancji do metody setTestingInstance. W poprzednim przykładzie tworzyliśmy
podklasę klasy ExternalRouter o nazwie TestingExternalRouter oraz przesłanialiśmy
metodę getDispatcher, dzięki czemu zwracała ona dyspozytor, o który nam chodziło
— dyspozytor fałszywy.
class TestingExternalRouter : public ExternalRouter
{
public:
virtual void Dispatcher *getDispatcher() const {
return new FakeDispatcher;
}
};

Może się wydawać, że taki sposób zastępowania dotychczasowego dyspozytora nowym


jest raczej okrężny. Tworzymy nową podklasę klasy ExternalRouter tylko po to, aby za-
stępować dyspozytory. Moglibyśmy pójść na skróty, ale z każdym z nich wiążą się róż-
ne kompromisy. Kolejnym rozwiązaniem, które możemy przyjąć, jest dodanie do klasy
ExternalRouter flagi logicznej i umożliwienie zwrócenia innego dyspozytora, kiedy flaga
ta będzie ustawiona. W C++ i C# w celu wybierania różnych dyspozytorów możemy także
korzystać z kompilacji warunkowej. Techniki te mogą sprawdzić się całkiem dobrze, ale są
one inwazyjne i mogą stać się trudne w obsłudze, jeśli będą używane w wielu miejscach
aplikacji. Z reguły lubię utrzymywać rozdział między kodem produkcyjnym a testowym.
Użycie metody settera i konstruktora chronionego w singletonie to technika inwazyjna
w średnim stopniu, ale pomaga ona w rozmieszczeniu testów. Czy programiści mogliby
nadużyć konstruktora publicznego i utworzyć w systemie produkcyjnym wiele singleto-
nów? Tak, ale jeśli ważne jest istnienie tylko jednej instancji obiektu w systemie, to moim
zdaniem najlepszym sposobem realizacji tego ograniczenia będzie jego zrozumienie
przez zespół.

Jednym z działań wobec osłabienia ochrony konstruktora i utworzenia jego podklasy jest
wyodrębnienie interfejsu (361) z klasy singletona i utworzenie settera, który przyjmuje obiekt
z tym interfejsem. Wada takiego rozwiązania polega na tym, że musisz zmienić typ referencji,
której używasz w celu przechowywania singletona w klasie, oraz typ wartości zwracanej
przez metodę instance. Modyfikacje te mogą być dość skomplikowane i tak naprawdę nie
zmieniają naszej sytuacji na lepszą. Docelowa „lepsza sytuacja” polega na zredukowaniu
referencji globalnych w singletonie w takim stopniu, aby mógł się on stać zwykłą klasą.

W poprzednim przykładzie zastąpiliśmy singleton za pomocą statycznego settera.


Singleton był obiektem podającym kolejny obiekt, jakim był dyspozytor. Czasami
w systemie możemy napotkać inny rodzaj elementu globalnego — globalną wytwórnię.
WPROWADZENIE STATYCZNEGO SETTERA 373

Zamiast przechowywać instancję, służą one świeżym obiektem za każdym razem, kiedy
zostanie wywołana jedna z ich metod statycznych. Podstawienie innego obiektu do zwrotu
jest trochę trudne, ale często można to zrobić poprzez delegowanie jednej wytwórni do
innej wytwórni. Spójrzmy na przykład w Javie:
public class RouterFactory
{
static Router makeRouter() {
return new EWNRouter();
}
}

RouteFactory jest prostą wytwórnią globalną. W takiej postaci nie pozwala nam
w warunkach testowych zastępować routerów, które serwuje, ale możemy zmodyfikować
ją w taki sposób, aby to umożliwić.
interface RouterServer
{
Router makeRouter();
}

public class RouterFactory implements RouterServer


{
static Router makeRouter() {
return server.makeRouter();
}

static setServer(RouterServer server) {


this.server = server;
}

static RouterServer server = new RouterServer() {


public RouterServer makeRouter() {
return new EWNRouter();
}
};
}
Podczas testu możemy zrobić coś takiego:
protected void setUp() {
RouterServer.setServer(new RouterServer() {
public RouterServer makeRouter() {
return new FakeRouter();
}
});
}

Ważne jest jednak pamiętanie o tym, że w każdym z powyższych wzorców statycznego


settera modyfikujesz stan, który jest dostępny w każdym z testów. W platformach te-
stowych xUnit możesz skorzystać z metody tearDown, aby przywrócić stan rzeczy do
jakiejś znanej postaci przed wykonaniem się reszty testów. Zwykle robię tak, gdy użycie
nieprawidłowego stanu w kolejnym teście może wprowadzać w błąd. Jeżeli we wszystkich
moich testach podstawiam fałszywą klasę MailSender, poddanie testom innego stanu nie
374 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

miałoby sensu. Z drugiej jednak strony, jeśli mam element globalny, który zachowuje stan
mający wpływ na wynik działania systemu, często tak samo konfiguruję metody setUp
i tearDown, aby zagwarantować, że pozostawiam czysty stan rzeczy:
protected void setUp() {
Node.count = 0;
...
}

protected void tearDown() {


Node.count = 0;
}

W tej chwili widzę Cię oczami wyobraźni. Siedzisz zdegustowany masakrą, jakiej
dopuściłem się na systemie tylko po to, abym mógł umieścić na miejscu kilka testów. Masz
rację — wzorce takie mogą w znacznym stopniu oszpecić fragmenty systemu. Chirurgia
nigdy nie wygląda ładnie, zwłaszcza na początku. Co można zrobić, aby z powrotem
doprowadzić system do przyzwoitego stanu?
Jedną z możliwości do rozważenia jest przekazanie parametru. Przyjrzyj się klasom,
które potrzebują dostępu do elementów globalnych, i zastanów się, czy możesz im nadać
wspólną klasę nadrzędną. Jeśli tak, będziesz mógł przekazać im właściwości globalne
podczas ich tworzenia i stopniowo zupełnie odejść od posiadania elementów globalnych.
Często programiści boją się, że każda klasa w systemie będzie wymagać jakiegoś elementu
globalnego. Uważaj, bo będziesz zaskoczony. Pracowałem kiedyś nad systemem osadzo-
nym, który pod postacią klas hermetyzował zarządzanie pamięcią oraz raportowanie
błędów, przekazując obiekt pamięci lub sprawozdanie z błędu wszędzie, gdzie tylko
było to potrzebne. Wraz z upływem czasu wykształcił się czysty podział na klasy, które
potrzebowały tych usług, oraz klasy, które ich nie potrzebowały. Klasy korzystające ze
wspomnianych usług miały po prostu wspólną klasę nadrzędną. Obiekty przekazywa-
ne w systemie były tworzone na starcie programu, co było niemal niezauważalne.

Czynności
Aby wprowadzić statyczny setter, wykonaj następujące czynności:
1. Zmniejsz ochronę konstruktora, dzięki czemu będziesz mógł dodać fałszywkę
poprzez utworzenie podklasy singletona.
2. Do klasy singletona dodaj statyczny setter. Setter powinien przyjmować referencję
do klasy singletona. Przed utworzeniem nowego obiektu upewnij się, że setter pra-
widłowo niszczy instancję singletona.
3. Jeśli potrzebny jest dostęp do prywatnych lub chronionych metod singletona w celu
poprawnego skonfigurowania go na potrzeby testów, rozważ utworzenie jego
podklasy albo wyodrębnienie interfejsu i przechowywanie w singletonie jego in-
stancji jako referencji, której typ jest zgodny z typem interfejsu.
ZASTĘPOWANIE BIBLIOTEKI 375

Zastępowanie biblioteki
Zorientowanie obiektowe daje wspaniałe możliwości zastępowania jednych obiektów
innymi. Jeśli dwie klasy implementują ten sam interfejs albo mają te same klasy nad-
rzędne, można dość łatwo zamienić jedną z nich na drugą. Niestety, programiści pra-
cujący z językami proceduralnymi, takimi jak na przykład C, nie mają tej możliwości.
Jeśli masz do czynienia z tego typu funkcją, z wyjątkiem użycia preprocesora nie istnieje
sposób na zastąpienie podczas kompilacji jednej funkcji inną:
void account_deposit(int amount);

Czy jest jakaś alternatywa? Tak — w celu zastąpienia jednej funkcji inną możesz
zastąpić bibliotekę. W tym celu tworzysz fałszywą bibliotekę, której funkcje mają takie
same sygnatury jak funkcje, które chcesz zastąpić. Jeśli rozpoznajesz działanie kodu,
powinieneś ustanowić jakiś mechanizm zapisujący powiadomienia i tworzący z nich
zapytania. Możesz korzystać z plików, zmiennych globalnych lub innych elementów,
które będą wygodne w użyciu podczas testów.
Oto przykład:
{
struct Call *call =
(struct Call *)calloc(1, sizeof (struct Call));
call->type = ACC_DEPOSIT;
call->arg0 = amount;
append(g_calls, call);
}

W tym przypadku interesuje nas rozpoznanie, tak więc utworzymy globalną listę
wywołań, aby rejestrować wszystkie wywołania tej lub jakiejkolwiek innej funkcji, którą
fałszujemy. Podczas testu będziemy mogli sprawdzać tę listę po wykorzystaniu obiektów,
aby stwierdzić, czy sfałszowane funkcje były wywoływane we właściwej kolejności.
Nigdy nie próbowałem korzystać z zastępowania biblioteki w przypadku klas C++,
ale przypuszczam, że jest to możliwe. Jestem pewny, że poprzekręcane nazwy, jakie
tworzą kompilatory C++, mogą utrudnić to zadanie, ale rozwiązanie takie jest bardzo
praktyczne przy wywoływaniu funkcji w C. Technika ta jest najprzydatniejsza podczas
udawania bibliotek zewnętrznych, a biblioteki, które najlepiej się do tego nadają, to
takie, w których znajdują się niemal wyłącznie odbiorcy danych — wywołujesz w nich
funkcje, ale wartości zwrotne rzadko kiedy Cię interesują. Podczas korzystania z techniki
zastępowania biblioteki szczególnie dobrze sprawdzają się na przykład biblioteki
graficzne.
Zastępowanie biblioteki można wykorzystywać również w Javie. Utwórz klasy o takich
samych nazwach oraz metodach, po czym zmień ścieżkę dostępu do tych klas, żeby
wywołania odnosiły się do nich zamiast do klas obciążonych zależnościami.
376 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

Czynności
Aby skorzystać z zastępowania biblioteki, wykonaj następujące czynności:
1. Zidentyfikuj funkcje lub klasy, które chcesz podrobić.
2. Utwórz ich alternatywne definicje.
3. Skonfiguruj proces kompilacji w taki sposób, aby zamiast wersji produkcyjnych
zostały dołączone wersje alternatywne.
PARAMETRYZACJA KONSTRUKTORA 377

Parametryzacja konstruktora
Jeśli za pomocą konstruktora tworzysz obiekt, często najprostszym sposobem na za-
stąpienie tego obiektu jest przeniesienie jego tworzenia na zewnątrz — utworzenie obiektu
poza klasą i przekazanie go jako parametru poprzez klienty do konstruktora.
Oto przykład:
public class MailChecker
{
public MailChecker (int checkPeriodSeconds) {
this.receiver = new MailReceiver();
this.checkPeriodSeconds = checkPeriodSeconds;
}
...
}

Następnie w taki sposób wprowadzamy nowy parametr:


public class MailChecker
{
public MailChecker (MailReceiver receiver,
int checkPeriodSeconds) {
this.receiver = receiver;
this.checkPeriodSeconds = checkPeriodSeconds;
}
...
}

Programiści niezbyt często biorą pod uwagę tę technikę, ponieważ zakładają, że wymu-
sza ona na wszystkich klientach przekazywanie dodatkowego argumentu. Możesz jednak
napisać konstruktor, który zachowuje wyjściową sygnaturę:
public class MailChecker
{
public MailChecker (int checkPeriodSeconds) {
this(new MailReceiver(), checkPeriodSeconds);
}

public MailChecker (MailReceiver receiver,


int checkPeriodSeconds) {
this.receiver = receiver;
this.checkPeriodSeconds = checkPeriodSeconds;
}
...
}

Jeśli tak postąpisz, będziesz mógł poddawać testom różne obiekty, a klienty klasy nie
będą musiały wiedzieć, że występuje tu jakaś różnica.
Zróbmy to krok po kroku. Oto nasz wyjściowy kod:
public class MailChecker
{
public MailChecker (int checkPeriodSeconds) {
378 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

this.receiver = new MailReceiver();


this.checkPeriodSeconds = checkPeriodSeconds;
}
...
}

Tworzymy kopię konstruktora:


public class MailChecker
{
public MailChecker (int checkPeriodSeconds) {
this.receiver = new MailReceiver();
this.checkPeriodSeconds = checkPeriodSeconds;
}

public MailChecker (int checkPeriodSeconds) {


this.receiver = new MailReceiver();
this.checkPeriodSeconds = checkPeriodSeconds;
}
...
}

Następnie dodajemy do niej parametr MailReceiver:


public class MailChecker
{
public MailChecker (int checkPeriodSeconds) {
this.receiver = new MailReceiver();
this.checkPeriodSeconds = checkPeriodSeconds;
}

public MailChecker (MailReceiver receiver,


int checkPeriodSeconds) {
this.receiver = new MailReceiver();
this.checkPeriodSeconds = checkPeriodSeconds;
}
...
}

Teraz przypisujemy ten parametr do zmiennej instancji, pozbywając się wyrażenia


z instrukcją new:
public class MailChecker
{
public MailChecker (int checkPeriodSeconds) {
this.receiver = new MailReceiver();
this.checkPeriodSeconds = checkPeriodSeconds;
}

public MailChecker (MailReceiver receiver,


int checkPeriodSeconds) {
this.receiver = receiver;
this.checkPeriodSeconds = checkPeriodSeconds;
}
...
}
PARAMETRYZACJA KONSTRUKTORA 379

Powracamy do oryginalnego konstruktora i usuwamy jego ciało, zastępując je wy-


wołaniem nowego konstruktora. Konstruktor ten korzysta z instrukcji new w celu utwo-
rzenia parametru, który musi przekazać.
public class MailChecker
{
public MailChecker (int checkPeriodSeconds) {
this(new MailReceiver(), checkPeriodSeconds);
}

public MailChecker (MailReceiver receiver,


int checkPeriodSeconds) {
this.receiver = receiver;
this.checkPeriodSeconds = checkPeriodSeconds;
}
...
}

Czy technika ta ma jakieś wady? Tak — jedną. Kiedy dodajemy nowy parametr do
konstruktora, umożliwiamy powstawanie kolejnych zależności w klasie parametru.
Użytkownicy tej klasy mogą skorzystać z nowego konstruktora w kodzie produkcyjnym
i spowodować wzrost zależności w całym systemie. Zwykle jednak nie jest to zbyt wielki
problem. Parametryzacja konstruktora stanowi refaktoryzację bardzo łatwą do przepro-
wadzenia i należy do technik, z których korzystam bardzo często.

W językach zezwalających na domyślne argumenty parametryzację konstruktora można


przeprowadzić jeszcze prościej. Do istniejącego konstruktora dodajemy po prostu argument
domyślny.
Oto konstruktor sparametryzowany w taki sposób w C++:
class AssemblyPoint
{
public:
AssemblyPoint(EquipmentDispatcher *dispatcher
= new EquipmentDispatcher);
...
};

Kiedy realizujemy to rozwiązanie w C++, pojawia się pewien problem. Plik nagłówkowy za-
wierający powyższą deklarację klasy musi zawierać nagłówek klasy EquipmentDispatcher.
Gdyby nie wywołanie konstruktora, mielibyśmy możliwość użycia deklaracji wyprzedzającej tej
klasy. Z tego powodu nie stosuję zbyt często argumentów domyślnych.

Czynności
Aby skorzystać z parametryzacji konstruktora, wykonaj następujące czynności:
1. Zidentyfikuj konstruktor, który chcesz sparametryzować, i wykonaj jego ko-
pię.
380 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

2. Dodaj do konstruktora parametr dotyczący obiektu, którego tworzenie chcesz


zastąpić. Usuń kod tworzący obiekt i dodaj do zmiennej instancji obiektu przy-
pisanie z parametru.
3. Jeśli w używanym przez Ciebie języku możesz wywołać konstruktor z poziomu
innego konstruktora, usuń ciało starego konstruktora i zastąp je wywołaniem
starego konstruktora. W starym konstruktorze dodaj wywołanie nowego kon-
struktora. Jeżeli w Twoim języku nie możesz wywoływać konstruktora z innego
konstruktora, być może będziesz musiał wyodrębnić do nowej metody wszystkie
duplikacje występujące wśród konstruktorów.
PARAMETRYZACJA METODY 381

Parametryzacja metody
Masz metodę, która tworzy wewnętrznie obiekt, i chcesz zastąpić ten obiekt, aby prze-
prowadzić rozpoznanie albo dokonać separacji. Często najprostsze, co można zrobić
w takiej sytuacji, to przekazać taki obiekt z zewnątrz. Oto przykład w C++:
void TestCase::run() {
delete m_result;
m_result = new TestResult;
try {
setUp();
runTest(m_result);
}
catch (exception& e) {
result->addFailure(e, this);
}
tearDown();
}

Mamy tu metodę tworzącą obiekt klasy TestResult o jakiejś tam nazwie. Jeśli chcemy
przeprowadzić rozpoznanie albo dokonać separacji, możemy przekazać go jako parametr.
void TestCase::run(TestResult *result) {
delete m_result;
m_result = result;
try {
setUp();
runTest(m_result);
}
catch (exception& e) {
result->addFailure(e, this);
}
tearDown();
}

Możemy skorzystać z niewielkiej metody wyprzedzającej, która zachowuje niezmie-


nioną oryginalną sygnaturę:
void TestCase::run() {
run(new TestResult);
}

W C++, Javie, C# i wielu innych językach można mieć w klasie dwie metody o tej samej nazwie,
o ile ich sygnatury są różne. W naszym przykładzie korzystamy z tej możliwości i stosujemy tę
samą nazwę zarówno dla nowej, sparametryzowanej metody, jak i dla metody oryginalnej.
Chociaż dzięki temu oszczędzamy sobie trochę pracy, czasami takie rozwiązanie może po-
wodować dezorientację. Alternatywne rozwiązanie polega na użyciu w nazwie nowej metody typu
parametru. W naszym przypadku moglibyśmy pozostawić run() jako nazwę oryginalnej meto-
dy, natomiast nowej metodzie nadalibyśmy nazwę runWithTestResut(TestResult).
382 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

Tak samo jak w przypadku parametryzacji konstruktora (377), parametryzacja


metody może spowodować, że jej klienty mogą stać się zależne od nowych typów,
które były wcześniej używane w klasie, ale nie było ich w interfejsie. Jeśli dochodzę do
wniosku, że taka sytuacja będzie problematyczna, biorę pod uwagę wyodrębnienie
i przesłonięcie metody wytwórczej (351).

Czynności
Aby dokonać parametryzacji metody, wykonaj następujące czynności:
1. Zidentyfikuj metodę, którą chcesz zastąpić, i wykonaj jej kopię.
2. Dodaj do metody parametr dotyczący obiektu, którego tworzenie chcesz zastąpić.
Usuń kod tworzący obiekt i dodaj do zmiennej przechowującej obiekt przypisanie
z parametru.
3. Usuń ciało skopiowanej metody i dodaj wywołanie metody sparametryzowanej,
korzystając z wyrażenia tworzącego oryginalny obiekt.
UPROSZCZENIE PARAMETRU 383

Uproszczenie parametru
Zazwyczaj najlepszym sposobem dokonania zmiany w klasie jest utworzenie jej instancji
w jarzmie testowym, napisanie testu weryfikującego tę zmianę, a następnie wprowadze-
nie zmiany, która przejdzie test. Czasami jednak ilość pracy, jaką musiałbyś wykonać
w celu przetestowania klasy, jest absurdalnie duża. Jeden z zespołów, które kiedyś od-
wiedziłem, odziedziczył system, w którym klasy domenowe wzajemnie zależały od
prawie wszystkich innych klas tego systemu. Jakby jeszcze tego było mało, wszystkie te
klasy były powiązane z frameworkiem trwałości. Poddanie testom jednej z tych klas
byłoby wykonalne, ale zespół nie mógłby poczynić żadnych postępów w pracach nad
funkcjonalnościami, gdyby spędzał cały swój czas, walcząc z klasami domeny. W celu
uzyskania separacji użyliśmy właśnie tej strategii. Aby uchronić niewinne osoby, zmieni-
łem okoliczności opisane w przykładzie.
W programie muzycznym służącym do komponowania utwór składa się z kilku
sekwencji zdarzeń muzycznych. W każdej sekwencji musimy znaleźć „czas martwy”,
dzięki czemu będzie go można wypełnić okresowym wzorcem muzycznym. Potrzebna
jest nam metoda bool Sequence::hasGapFor(Sequence& pattern) const. Metoda ta zwraca
wartość informującą o tym, czy dany wzorzec zmieści się w sekwencji.
W idealnej sytuacji wspomniana metoda znalazłaby się w klasie Sequence, ale klasa
ta zalicza się do tych okropnych klas, które wciągnęłyby do naszego jarzma testowego
cały świat, gdybyśmy tylko spróbowali utworzyć ich instancje. Zanim zaczniemy pisać
tę metodę, musimy dowiedzieć się, jak można dla niej utworzyć test. Napisanie testu
jest możliwe, ponieważ sekwencje mają swoją wewnętrzną reprezentację, którą można
uprościć. Każda sekwencja składa się z wektora zdarzeń. Niestety, zdarzenia borykają
się z tym samym problemem co sekwencje, którymi są przeraźliwe zależności prowa-
dzące do problemów z kompilacją. Na szczęście do przeprowadzenia obliczeń potrze-
bujemy tylko czasów trwania każdego ze zdarzeń. Możemy napisać kolejną metodę,
która przeprowadzi takie obliczenia na liczbach całkowitych. Gdy już będziemy je mieć,
będziemy mogli napisać metodę hasGapFor i pozwolić jej na wykonywanie swojej roli
poprzez delegację do innej metody.
Zacznijmy tworzyć pierwszą metodę. Oto jej test:
TEST(hasGapFor, Sequence)
{
vector<unsigned int> baseSequence;
baseSequence.push_back(1);
baseSequence.push_back(0);
baseSequence.push_back(0);

vector<unsigned int> pattern;


pattern.push_back(1);
pattern.push_back(2);

CHECK(SequenceHasGapFor(baseSequence, pattern));
}
384 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

Funkcja SequenceHasGapFor jest funkcją wolną. Nie stanowi części żadnej klasy i ope-
ruje na reprezentacji składającej się z typów prostych — w tym przypadku są to liczby
całkowite bez znaku. Jeżeli utworzymy funkcjonalność dla tej funkcji w jarzmie testowym,
będziemy mogli napisać w klasie Sequence dość prostą funkcję, która deleguje do nowej
funkcjonalności:
bool Sequence::hasGapFor(Sequence& pattern) const
{
vector<unsigned int> baseRepresentation
= getDurationsCopy();

vector<unsigned int> patternRepresentation


= pattern.getDurationsCopy();

return SequenceHasGapFor(baseRepresentation,
patternRepresentation);
}

Funkcja ta w celu otrzymania tablicy z czasami trwania potrzebuje kolejnej funkcji,


w związku z czym ją piszemy:
vector<unsigned int> Sequence::getDurationsCopy() const
{
vector<unsigned int> result;
for (vector<Event>::iterator it = events.begin();
it != events.end(); ++it) {
result.push_back(it->duration);
}
return result;
}

W tej chwili mieliśmy możliwość dodania funkcjonalności, ale w bardzo nieelegancki


sposób. Sporządźmy listę wszystkich okropnych rzeczy, które do tej pory zrobiliśmy:
1. Odsłoniliśmy wewnętrzną reprezentację klasy Sequence.
2. Utrudniliśmy zrozumienie implementacji klasy Sequence, przenosząc jej część
do wolnej funkcji.
3. Dodaliśmy nieprzetestowany kod (tak naprawdę nie mogliśmy napisać testu
dla metody getDurationsCopy()).
4. Powieliliśmy dane w systemie.
5. Odłożyliśmy problem na później. Nie przystąpiliśmy jeszcze do trudnego zadania
polegającego na usuwaniu zależności między naszymi klasami domeny a infra-
strukturą (usunięcie zależności wprowadzi ogromną różnicę, gdy zaczniemy posu-
wać się do przodu, ale to jeszcze przed nami).
Na przekór tym wszystkim niepomyślnym okolicznościom udało nam się dodać
funkcjonalność testującą. Nie lubię tego rodzaju refaktoryzacji, ale korzystam z niej,
kiedy jestem przyciśnięty do muru. Często spisuje się ona dobrze jako wstęp do kieł-
kowania klasy (63). Aby się o tym przekonać, wyobraź sobie opakowanie funkcji
SequenceHasGapFor w klasę o nazwie GapFinder.
UPROSZCZENIE PARAMETRU 385

Uproszczenie parametru (383) pozostawia kod w dość kiepskim stanie. Zazwyczaj w celu
utworzenia nowych abstrakcji, które będą mogły służyć jako podstawa do dalszej pracy, lepiej
jest dodać do oryginalnej klasy nowy kod albo wykiełkować klasę (80). Z uproszczenia para-
metru korzystam wyłącznie wtedy, gdy mam pewność, że przeznaczę później czas na poddanie
klasy testom. Wtedy funkcja będzie mogła zostać dodana do klasy jako prawdziwa metoda.

Czynności
Aby skorzystać z uproszczenia parametru, wykonaj następujące czynności:
1. Opracuj wolną funkcję, która realizuje pracę potrzebną do wykonania w klasie.
Podczas tego procesu napisz przejściową jej reprezentację, która umożliwi Ci
wykonanie zadania.
2. Dodaj funkcję do klasy, która tworzy reprezentację i deleguje ją do nowej funkcji.
386 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

Przesunięcie funkcjonalności w górę hierarchii


Czasami musisz pracować w klasie z grupą metod, a zależności, które uniemożliwiają
Ci utworzenie instancji klasy, nie mają związku z tą grupą. Pisząc „nie mają związku”,
mam na myśli to, że metody, z którymi chcesz pracować, nie są ograniczone bezpośrednio
ani pośrednio żadną z trudnych zależności. Gdyby tak było, mógłbyś kilkakrotnie prze-
prowadzić upublicznienie metody statycznej (346) albo wyłonienie obiektu metody
(332), chociaż niekoniecznie byłby to najbardziej bezpośredni sposób na poradzenie
sobie z istniejącą zależnością.
W takiej sytuacji możesz przesunąć grupę metod (funkcjonalność) do abstrakcyjnej
klasy nadrzędnej. Kiedy już będziesz mieć abstrakcyjną klasę nadrzędną, będziesz mógł
utworzyć jej podklasę i na potrzeby testów utworzyć instancje tej podklasy. Oto przykład:
public class Scheduler
{
private List items;

public void updateScheduleItem(ScheduleItem item)


throws SchedulingException {
try {
validate(item);
}
catch (ConflictException e) {
throw new SchedulingException(e);
}
...
}

private void validate(ScheduleItem item)


throws ConflictException {
// odwołanie do bazy danych
...
}

public int getDeadtime() {


int result = 0;
for (Iterator it = items.iterator(); it.hasNext(); ) {
ScheduleItem item = (ScheduleItem)it.next();
if (item.getType() != ScheduleItem.TRANSIENT
&& notShared(item)) {
result += item.getSetupTime() + clockTime();
}
if (item.getType() != ScheduleItem.TRANSIENT) {
result += item.finishingTime();
}
else {
result += getStandardFinish(item);
}
}
return result;
}
}
PRZESUNIĘCIE FUNKCJONALNOŚCI W GÓRĘ HIERARCHII 387

Załóżmy, że chcemy wprowadzić zmiany w metodzie getDeadtime, a metoda update


ScheduleItem nas nie interesuje. Byłoby dobrze, gdybyśmy w ogóle nie musieli mieć
do czynienia z zależnością od bazy danych. Moglibyśmy spróbować upublicznić metodę
statyczną (346), ale korzystamy z wielu niestatycznych elementów klasy Scheduler.
Kolejną możliwość tworzy wyłonienie obiektu metody (332), ale nasza metoda jest raczej
mała, a zależności od innych metod i pól klasy spowodują, że praca, którą musielibyśmy
wykonać, byłaby zbyt skomplikowana jak na zwykłe poddanie metody testom.
Kolejną możliwością jest przesunięcie interesującej nas metody do klasy nadrzędnej.
Kiedy już to zrobimy, będziemy mogli pozostawić szkodliwe zależności w tej klasie, gdzie
nie będą nam one przeszkadzać w testach. Oto jak klasa ta będzie wtedy wówczas wyglądać:
public class Scheduler extends SchedulingServices
{
public void updateScheduleItem(ScheduleItem item)
throws SchedulingException {
...
}

private void validate(ScheduleItem item)


throws ConflictException {
// odwołanie do bazy danych
...
}
...
}

Przenieśliśmy metodę getDeadtime (funkcjonalność, którą chcemy poddać testom) oraz


wszystkie funkcjonalności, z których ona korzysta, do klasy abstrakcyjnej.
public abstract class SchedulingServices
{
protected List items;

protected boolean notShared(ScheduleItem item) {


...
}

protected int getClockTime() {


...
}

protected int getStandardFinish(ScheduleItem item) {


...
}

public int getDeadtime() {


int result = 0;
for (Iterator it = items.iterator(); it.hasNext(); ) {
ScheduleItem item = (ScheduleItem)it.next();
if (item.getType() != ScheduleItem.TRANSIENT
&& notShared(item)) {
result += item.getSetupTime() + clockTime();
388 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

}
if (item.getType() != ScheduleItem.TRANSIENT) {
result += item.finishingTime();
}
else {
result += getStandardFinish(item);
}
}
return result;
}
...
}

Teraz możemy napisać podklasę testową, która pozwoli nam uzyskać dostęp do tych
metod w jarzmie testowym:
public class TestingSchedulingServices extends SchedulingServices
{
public TestingSchedulingServices() {
}

public void addItem(ScheduleItem item) {


items.add(item);
}
}

import junit.framework.*;

class SchedulingServicesTest extends TestCase


{
public void testGetDeadTime() {
TestingSchedulingServices services
= new TestingSchedulingServices();
services.addItem(new ScheduleItem("a",
10, 20, ScheduleItem.BASIC));
assertEquals(2, services.getDeadtime());
}
...
}

Wykonaliśmy tu następujący zabieg: przenieśliśmy metody, które chcemy przete-


stować, w górę hierarchii, do abstrakcyjnej klasy nadrzędnej, i utworzyliśmy konkretną
podklasę, której możemy użyć, aby metody te poddać testom. Czy takie rozwiązanie jest
dobre? Z punktu widzenia projektu sporo brakuje mu do ideału. Wiele funkcjonalności
rozmieściliśmy w dwóch różnych klasach tylko po to, aby ich testowanie było łatwiejsze.
Rozłożenie funkcjonalności w dwóch klasach może wprowadzać w błąd, jeśli związki
między tymi funkcjonalnościami w każdej z klas nie są zbyt silne, co właśnie ma miejsce
w naszym przypadku. Mamy klasę Scheduler, która jest odpowiedzialna za aktualizację
elementów planujących, oraz klasę SchedulingServices, odpowiedzialną za różne rzeczy,
łącznie z uzyskiwaniem domyślnych czasów dla poszczególnych elementów oraz oblicza-
niem czasu martwego. Lepsza faktoryzacja polegałaby na spowodowaniu, aby klasa
Scheduler delegowała do jakiegoś obiektu weryfikującego, który potrafi komunikować się
PRZESUNIĘCIE FUNKCJONALNOŚCI W GÓRĘ HIERARCHII 389

z bazą danych, ale jeśli krok ten wydaje się zbyt ryzykowny do natychmiastowego wy-
konania albo istnieją inne poważne zależności, przesunięcie funkcjonalności w górę
hierarchii zdaje się być dobrym pierwszym krokiem. Będzie on mniej ryzykowny, jeśli
zachowasz sygnatury (314) oraz skorzystasz ze wsparcia kompilatora (317). Delegacjami
będziemy mogli zająć się później, gdy rozmieścimy więcej testów.

Czynności
Aby przesunąć funkcjonalność w górę hierarchii, wykonaj następujące czynności:
1. Zidentyfikuj metody, które chcesz przesunąć w górę hierarchii.
2. Utwórz abstrakcyjną klasę nadrzędną dla klasy zawierającej te metody.
3. Przekopiuj metody do klasy nadrzędnej i skompiluj kod.
4. Przekopiuj do nowej klasy nadrzędnej każdą referencję, przed której brakiem
ostrzega kompilator. Pamiętaj przy tym o zachowaniu sygnatur (314), aby ograni-
czyć prawdopodobieństwo wystąpienia błędów.
5. Jeśli obie klasy kompilują się prawidłowo, utwórz podklasę klasy abstrakcyjnej
i dodaj do niej wszystkie metody, które są potrzebne, aby ją skonfigurować na
potrzeby przeprowadzenia testów.

Być może zastanawiasz się, dlaczego klasę nadrzędną zdefiniowałem jako abstrakcyjną.
Chciałem, żeby taka była. Dzięki temu kod będzie łatwiejszy do zrozumienia. Dobrze jest
spojrzeć na kod aplikacji i wiedzieć, że została w nim użyta każda konkretna klasa. Jeżeli
przeszukasz kod i znajdziesz konkretne klasy, których instancje nie są nigdzie tworzone,
może okazać się, że stanowią one „martwy kod”.
390 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

Przesunięcie zależności w dół hierarchii


W niektórych klasach występuje zaledwie kilka problematycznych zależności. Jeśli
zależności te zawarte są w paru wywołaniach metod, w celu pozbycia się ich na czas
pisania testów możesz utworzyć podklasę i przesłonić metodę (398). Jeśli jednak za-
leżności są wszechobecne, utworzenie podklasy i przesłonięcie metody może się nie
sprawdzić. W celu usunięcia zależności od niektórych typów być może będziesz musiał
kilkakrotnie skorzystać z wyodrębniania interfejsu (361). Kolejnym działaniem jest
przesunięcie zależności w dół hierarchii. Technika ta jest pomocna podczas oddzielania
zależności od innych części klasy, dzięki czemu łatwiej jest pracować z nią w jarzmie
testowym.
Kiedy przesuwasz zależność w dół hierarchii, przekształcasz swoją bieżącą klasę na
abstrakcyjną. Następnie tworzysz podklasę, która będzie Twoją nową klasą produkcyjną,
i przenosisz do niej wszystkie problematyczne zależności. W tym momencie będziesz
mógł utworzyć podklasę swojej oryginalnej klasy, aby jej metody stały się dostępne w te-
stach. Oto przykład tej techniki w C++:
class OffMarketTradeValidator : public TradeValidator
{
private:
Trade& trade;
bool flag;

void showMessage() {
int status = AfxMessageBox(makeMessage(),
MB_ABORTRETRYIGNORE);
if (status == IDRETRY) {
SubmitDialog dlg(this,
"Kliknij OK, jeśli transakcja jest wiążąca.");
dlg.DoModal();
if (dlg.wasSubmitted()) {
g_dispatcher.undoLastSubmission();
flag = true;
}
}
else
if (status == IDABORT) {
flag = false;
}
}

public:
OffMarketTradeValidator(Trade& trade)
: trade(trade), flag(false)
{}

bool isValid() const {


if (inRange(trade.getDate())
&& validDestination(trade.destination)
&& inHours(trade) {
PRZESUNIĘCIE ZALEŻNOŚCI W DÓŁ HIERARCHII 391

flag = true;
}
showMessage();
return flag;
}
...
};

Możemy napotkać problemy, gdy będziemy musieli wprowadzić zmiany w naszej


logice uwierzytelniającej, a nie będziemy chcieli uruchamiać w jarzmie testowym funkcji
ani klas specyficznych dla interfejsu użytkownika. W takim przypadku dobrym rozwiąza-
niem będzie przesunięcie zależności w dół hierarchii.
Oto jak będzie wyglądać nasz kod po przesunięciu zależności w dół hierarchii:
class OffMarketTradeValidator : public TradeValidator
{
protected:
Trade& trade;
bool flag;
virtual void showMessage() = 0;

public:
OffMarketTradeValidator(Trade& trade)
: trade(trade), flag(false) {}

bool isValid() const {


if (inRange(trade.getDate())
&& validDestination(trade.destination)
&& inHours(trade) {
flag = true;
}
showMessage();
return flag;
}
...
};

class WindowsOffMarketTradeValidator
: public OffMarketTradeValidator
{
protected:
virtual void showMessage() {
int status = AfxMessageBox(makeMessage(),
MB_ABORTRETRYIGNORE);
if (status == IDRETRY) {
SubmitDialog dlg(this,
"Kliknij OK, jeśli transakcja jest wiążąca.");
dlg.DoModal();
if (dlg.wasSubmitted()) {
g_dispatcher.undoLastSubmission();
flag = true;
}
}
else
392 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

if (status == IDABORT) {
flag = false;
}
}
...
};

Kiedy już przeniesiemy w dół hierarchii (do nowej podklasy WindowsOffMarket


Validator) całą pracę typową dla interfejsu użytkownika, będziemy mogli utworzyć
na potrzeby testów nową podklasę. Całe jej zadanie polega na wyzerowaniu zachowania
metody showMessage:
class TestingOffMarketTradeValidator
: public OffMarketTradeValidator
{
protected:
virtual void showMessage() {}
};

Teraz mamy klasę, którą możemy testować i która nie zależy od interfejsu użytkownika.
Czy takie użycie dziedziczenia jest rozwiązaniem idealnym? Nie, ale pomaga nam w przete-
stowaniu fragmentu logiki klasy. Jeśli mamy testy dla klasy OffMarketTradeValidator,
możemy przystąpić do porządkowania logiki ponownej próby i przesunąć ją z klasy
WindowsOffMarketTradeValidator w górę hierarchii. Gdy pozostaną nam już wyłącznie
wywołania interfejsu użytkownika, będziemy mogli zabrać się do oddelegowania ich do
nowej klasy. Ta nowa klasa będzie zawierać wyłącznie zależności od interfejsu użytkownika.

Czynności
Aby przesunąć zależności w dół hierarchii, wykonaj następujące czynności:
1. Spróbuj utworzyć w jarzmie testowym klasę, w której występują problemy z zależ-
nościami.
2. Zidentyfikuj zależności, które podczas kompilacji stwarzają problemy.
3. Utwórz nową podklasę o nazwie, która informuje o środowisku mającym związek
z tymi zależnościami.
4. Skopiuj zmienne instancji oraz metody zawierające problematyczne zależności do
nowej podklasy, pamiętając o zachowaniu sygnatur. Metody w oryginalnej klasie
powinny być chronione i abstrakcyjne, a sama klasa powinna być abstrakcyjna.
5. Utwórz podklasę testową i zmień swoje testy w taki sposób, aby następowała w nich
próba utworzenia jej instancji.
6. Skompiluj testy, aby sprawdzić, czy można utworzyć instancję nowej klasy.
ZASTĄPIENIE FUNKCJI WSKAŹNIKIEM DO FUNKCJI 393

Zastąpienie funkcji wskaźnikiem do funkcji


Kiedy musisz usunąć zależności w językach proceduralnych, nie masz tylu możliwości, ile
miałbyś w językach zorientowanych obiektowo. Nie możesz przeprowadzić hermetyzacji
referencji globalnej (340) ani utworzyć podklasy i przesłonić metody (398). Oba te
rozwiązania są wykluczone. Mógłbyś skorzystać z zastąpienia biblioteki (375) albo
uzupełnienia definicji (338), ale często zdarza się, że techniki te są zbyt skomplikowane,
jak na niewielkie zależności, które trzeba usunąć. Jedyną alternatywą w językach
wspierających wskaźniki do funkcji jest zastąpienie funkcji wskaźnikiem do funkcji.
Najbardziej znanym językiem udostępniającym taki rodzaj wsparcia jest C.
Różne zespoły programistów mają różne poglądy na wskaźniki do funkcji. W nie-
których zespołach są one uważane za okropnie niebezpieczne, ponieważ istnieje moż-
liwość uszkodzenia ich zawartości i wywołania jakiegoś nieprzewidzianego miejsca pamięci.
Inne zespoły uważają je za przydatne narzędzie, które należy stosować z rozwagą. Jeśli
skłaniasz się bardziej ku obozowi „stosowania z rozwagą”, będziesz mógł odseparować
zależności, które byłyby niemożliwe do usunięcia innymi metodami.
Zacznijmy jednak od tego, co najważniejsze. Spójrzmy na wskaźnik do funkcji w jego
naturalnym środowisku. W poniższym przykładzie pokazano deklarację kilku wskaźników
do funkcji w języku C oraz parę wywołań przeprowadzonych z ich udziałem:
struct base_operations
{
double (*project)(double,double);
double (*maximize)(double,double);
};

double default_projection(double first, double second) {


return second;
}

double maximize(double first, double second) {


return first + second;
}

void init_ops(struct base_operations *operations) {


operations->project = default_projection;
operations->maximize = default_maximize;
}

void run_tesselation(struct node *base,


struct base_operations *operations) {
double value = operations->project(base.first, base.second);
...
}

Za pomocą wskaźników do funkcji możesz do pewnego stopnia programować obiek-


towo, ale jak bardzo możliwość ta będzie przydatna podczas usuwania zależności?
Rozważmy następujący scenariusz:
394 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

Masz aplikację sieciową, która przechowuje informacje o pakietach w bazie danych


online. Współpraca z bazą danych następuje poprzez wywołania, które wyglądają na-
stępująco:
void db_store(
struct receive_record *record,
struct time_stamp receive_time);
struct receive_record * db_retrieve(time_stamp search_time);

Aby w funkcjach tych umieścić nowe ciała, moglibyśmy zastąpić bibliotekę (375),
ale czasami korzystanie z tej techniki wymaga wprowadzenia nietuzinkowych zmian.
Być może musielibyśmy rozbić bibliotekę w celu odseparowania funkcji, które chcemy
sfałszować. Co ważniejsze, spoiny, które otrzymujemy, zastępując bibliotekę, nie na-
leżą do tego rodzaju spoin, z którego chciałbyś korzystać w celu różnicowania zachowań
w kodzie produkcyjnym. Jeżeli chcesz poddać swój kod testom i zachować jego elastycz-
ność, na przykład w celu zmiany rodzaju bazy danych, z którą komunikuje się Twój kod,
przydatne może być zastąpienie funkcji wskaźnikiem do funkcji. Poznajmy etapy tej
techniki:
Najpierw odszukujemy deklarację funkcji, którą chcemy zastąpić.
// db.h
void db_store(struct receive_record *record,
struct time_stamp receive_time);

Następnie deklarujemy wskaźnik do funkcji o tej samej nazwie.


// db.h
void db_store(struct receive_record *record,
struct time_stamp receive_time);

void (*db_store)(struct receive_record *record,


struct time_stamp receive_time);

Zmieniamy nazwę oryginalnej deklaracji.


// db.h
void db_store_production(struct receive_record *record,
struct time_stamp receive_time);

void (*db_store)(struct receive_record *record,


struct time_stamp receive_time);

Teraz inicjalizujemy wskaźnik w źródłowym pliku w C.


// main.c
extern void db_store_production(
struct receive_record *record,
struct time_stamp receive_time);

void initializeEnvironment() {
db_store = db_store_production;
...
}
ZASTĄPIENIE FUNKCJI WSKAŹNIKIEM DO FUNKCJI 395

int main(int ac, char **av) {


initializeEnvironment();
...
}

Znajdujemy definicję funkcji db_store i zmieniamy jej nazwę na db_store_production.


// db.c
void db_store_production(
struct receive_record *record,
struct time_stamp receive_time) {
...
}

Teraz możemy skompilować kod i przetestować go.


Kiedy wskaźniki do funkcji znajdą się już na swoich miejscach, testy będą mogły
zapewnić alternatywne definicje tych funkcji na potrzeby rozpoznania i separowania.

Zastępowanie funkcji wskaźnikiem do funkcji to dobry sposób na usuwanie zależności. Jedna


z zalet tego rozwiązania polega na tym, że zastępowanie to w całości dokonuje się podczas
kompilacji, w związku z czym technika ta wywiera minimalny wpływ na Twój system. Jeśli
jednak stosujesz ją w C, zastanów się nad przejściem do C++, dzięki czemu będziesz mógł
skorzystać z przewagi, jaką dają pozostałe spoiny dostępne w tym języku. Kiedy piszę te
słowa, wiele kompilatorów C oferuje przełączniki umożliwiające dokonywanie mieszanej
kompilacji kodu w C i C++. Korzystając z tej funkcjonalności, będziesz mógł stopniowo
przełożyć swój projekt z C na C++, zajmując się początkowo tylko tymi plikami, w których
chcesz usunąć zależności.

Czynności
Aby zastąpić funkcję wskaźnikiem do funkcji, wykonaj następujące czynności:
1. Odszukaj deklaracje funkcji, które chcesz zastąpić.
2. Przed każdą deklaracją funkcji utwórz wskaźnik do funkcji o takiej samej nazwie.
3. Zmień nazwy oryginalnych deklaracji funkcji, dzięki czemu nie będą one takie
same jak nazwy wskaźników, które właśnie zadeklarowałeś.
4. W pliku C zainicjalizuj wskaźniki adresami starych funkcji.
5. Przeprowadź kompilację, aby odszukać ciała starych funkcji. Nadaj im nazwy
nowych funkcji.
396 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

Zastąpienie referencji globalnej getterem


Zmienne globalne mogą być prawdziwym utrapieniem, kiedy chcesz pracować z frag-
mentami kodu niezależnie od reszty systemu. To wszystko, co w tym miejscu na ten
temat napiszę. W miarę pełną tyradę przeciw elementom globalnym wygłosiłem przy
okazji omawiania techniki wprowadzania statycznego settera (370). Teraz oszczędzę
Ci powtórki.
Jednym ze sposobów na ominięcie zależności od elementów globalnych w klasie jest
wprowadzenie gettera dla każdego takiego elementu klasy. Kiedy już będziesz mieć gettery,
będziesz mógł utworzyć podklasę i przesłonić metodę (398), aby zwracały one coś
odpowiedniego. W niektórych przypadkach możesz posunąć się nawet do wyodrębnienia
interfejsu (361) w celu usunięcia globalnych zależności w klasie. Oto przykład w Javie:
public class RegisterSale
{
public void addItem(Barcode code) {
Item newItem =
Inventory.getInventory().itemForBarcode(code);
items.add(newItem);
}
...
}

W kodzie tym dostęp do klasy Inventory odbywa się na zasadach globalnych. „Mo-
ment!”, słyszę, jak mówisz. „Globalnych? Przecież to tylko wywołanie metody statycznej
w klasie”. Dla naszych potrzeb uznajemy, że jest to metoda globalna. W Javie sama klasa
jest obiektem globalnym i wygląda na to, że musi się odwoływać do jakiegoś stanu, aby
mogła wykonywać swoje zadanie (zwracanie towarów o zadanych kodach kreskowych).
Czy możemy obejść ten problem za pomocą zastąpienia referencji globalnej getterem?
Spróbujmy.
Najpierw piszemy getter. Zwróć uwagę, że jest on chroniony, dzięki czemu będziemy
mogli przesłonić go podczas testów.
public class RegisterSale
{
public void addItem(Barcode code) {
Item newItem = Inventory.getInventory().itemForBarcode(code);
items.add(newItem);
}

protected Inventory getInventory() {


return Inventory.getInventory();
}
...
}
ZASTĄPIENIE REFERENCJI GLOBALNEJ GETTEREM 397

Następnie zastępujemy wszystkie odwołania do klasy globalnej getterem.


public class RegisterSale
{
public void addItem(Barcode code) {
Item newItem = getInventory().itemForBarcode(code);
items.add(newItem);
}

protected Inventory getInventory() {


return Inventory.getInventory();
}
...
}

Teraz możemy utworzyć podklasę klasy Inventory, której będziemy mogli użyć
w teście. Ponieważ Inventory jest singletonem, jej konstruktor powinien być chroniony,
a nie prywatny. Gdy już to zrobimy, będziemy mogli utworzyć jej podklasę i umieścić
w niej logikę potrzebną do przekształcenia podczas testu kodów kreskowych na nazwy
towarów.
public class FakeInventory extends Inventory
{
public Item itemForBarcode(Barcode code) {
...
}
...
}
Następnie będziemy mogli napisać klasę, z której skorzystamy podczas testu.
class TestingRegisterSale extends RegisterSale
{
Inventory inventory = new FakeInventory();

protected Inventory getInventory() {


return inventory;
}
}

Czynności
Aby zastąpić referencję globalną getterem, wykonaj następujące czynności:
1. Zidentyfikuj globalną referencję, którą chcesz zastąpić.
2. Dla globalnej referencji napisz getter. Upewnij się, że ochrona dostępu do me-
tody jest wystarczająco luźna, byś mógł przesłonić getter w podklasie.
3. Zastąp referencje globalne wywołaniami gettera.
4. Utwórz podklasę testową i przesłoń getter.
398 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

Utworzenie podklasy i przesłonięcie metody


Utworzenie podklasy i przesłonięcie metody jest główną techniką usuwania zależności
w programach zorientowanych obiektowo. Tak naprawdę wiele innych technik usuwania
zależności to jej odmiany.
Główna idea tworzenia podklasy i przesłaniania metody polega na założeniu, że
w kontekście testów można skorzystać z dziedziczenia w celu pozbycia się zachowania,
które Cię nie interesuje, albo uzyskania dostępu do zachowania, na którym Ci zależy.
Spójrzmy na metodę obecną w pewnej małej aplikacji:
class MessageForwarder
{
private Message createForwardMessage(Session session,
Message message)
throws MessagingException, IOException {
MimeMessage forward = new MimeMessage (session);
forward.setFrom (getFromAddress (message));
forward.setReplyTo (
new Address [] {
new InternetAddress (listAddress) });
forward.addRecipients (Message.RecipientType.TO,
listAddress);
forward.addRecipients (Message.RecipientType.BCC,
getMailListAddresses ());
forward.setSubject (
transformedSubject (message.getSubject ()));
forward.setSentDate (message.getSentDate ());
forward.addHeader (LOOP_HEADER, listAddress);
buildForwardContent(message, forward);

return forward;
}
...
}

Klasa MessageForwarder ma całkiem sporo metod, których tu nie pokazano. Jedna


z metod publicznych wywołuje metodę prywatną createForwardMessage w celu utwo-
rzenia nowej wiadomości. Załóżmy, że w czasie testów nie chcemy być zależni od klasy
MimeTesting. Klasa ta korzysta ze zmiennej o nazwie session, a my podczas testów nie
chcemy otwierać prawdziwej sesji. Jeśli chcemy pozbyć się zależności od klasy MimeTesting,
możemy przekształcić createForwardMessage w metodę chronioną i przesłonić ją nową
podklasą, którą napiszemy wyłącznie na potrzeby testów:
class TestingMessageForwarder extends MessageForwarder
{
protected Message createForwardMessage(Session session,
Message message) {
Message forward = new FakeMessage(message);
UTWORZENIE PODKLASY I PRZESŁONIĘCIE METODY 399

return forward;
}
...
}

W tej nowej podklasie możemy zrobić wszystko, czego tylko potrzebujemy w celu
uzyskania separacji lub przeprowadzenia rozpoznania. W tym przypadku pozbywamy
się w zasadzie większości zachowań metody createForwardMessage, ale jeśli tylko nie
jest nam ona potrzebna ze względu na jakąś konkretną funkcjonalność, którą właśnie
testujemy, to w niczym nam to nie przeszkodzi.
W kodzie produkcyjnym tworzymy instancje klasy MessageForwarder; w testach —
TestingMessageForwarder. Udało nam się uzyskać separację kosztem minimalnej mo-
dyfikacji kodu produkcyjnego. Wszystko, co zrobiliśmy, sprowadziło się do zmiany
zakresu metody z prywatnego na chroniony.
Zazwyczaj faktoryzacja, jaką masz w klasie, decyduje o stopniu, w jakim będziesz
mógł skorzystać z dziedziczenia w celu odseparowania zależności. Czasami zależność,
której chcesz się pozbyć, jest odizolowana w małej metodzie. Innym razem w celu od-
separowania zależności będziesz musiał przesłonić większą metodę.
Utworzenie podklasy i przesłonięcie metody jest wydajną techniką, ale podczas
jej stosowania powinieneś zachować ostrożność. W poprzednim przykładzie mógłbym
wygenerować wiadomość bez tematu, adresu itd., co miałoby sens tylko wtedy, gdybym na
przykład sprawdzał, czy mogę przesłać wiadomość z jednego miejsca systemu do innego
bez przejmowania się jej treścią i adresami.
Dla mnie programowanie jest przede wszystkim czynnością wzrokową. Kiedy pracuję,
widzę w wyobraźni rozmaite rodzaje obrazów, co pomaga mi w podejmowaniu decyzji.
Szkoda, że żaden z tych obrazów nie jest tak naprawdę diagramem UML, chociaż tak czy
inaczej są one pomocne.
Jeden z obrazów, który często sobie wyobrażam, nazwałem widokiem papierowym.
Patrzę na metodę i zaczynam dostrzegać wszystkie sposoby, w jakie mogę pogrupować jej
instrukcje oraz wyrażenia. Zaczynam zdawać sobie sprawę z tego, że jeśli będę mógł
wyodrębnić do innej metody każdy najdrobniejszy jej fragment, który jestem w stanie
zidentyfikować, to będę mógł zastąpić te fragmenty czymś innym. To tak, jakbym na
kartce papieru z kodem położył drugą, przezroczystą kartkę. Ta druga kartka może
zawierać inny kod, którym zastąpię fragment oryginalnego kodu. Plik kartek to test,
a metody, które widzę, począwszy od górnej kartki, będę mógł wykonywać podczas
testów. Na rysunku 25.6 spróbowałem przedstawić papierowy widok klasy.
Widok papierowy pomaga mi zobaczyć, co jest możliwe, ale gdy zaczynam tworzyć
podklasę i przesłaniać metodę, próbuję przesłaniać metody, które już istnieją. W końcu
moim celem jest umieszczenie na miejscach testów, a wyodrębnianie metod bez po-
rozmieszczanych testów może być czasami ryzykowne.
400 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

Rysunek 25.6. Klasa TestingAccount nałożona na klasę Accounr

Czynności
Aby utworzyć podklasę i przesłonić metodę, wykonaj następujące czynności:
1. Zidentyfikuj zależności, które chcesz odseparować, albo miejsce, w którym chcesz
przeprowadzić rozpoznanie. Postaraj się dobrać najmniejszy zbiór metod, które
możesz przesłonić, aby osiągnąć swój cel.
2. Przekształć każdą metodę w taki sposób, aby można ją było przesłonić. Sposób,
w jaki można to zrobić, zależy od języka programowania. W C++ metody po-
winny być wirtualne (chyba że już takie są). W Javie metody nie mogą być finalne.
W wielu językach platformy .NET musisz jawnie zdefiniować metody jako przesła-
nialne.
3. Jeśli Twój język tego wymaga, dostosuj widzialność metod, które będziesz prze-
słaniać, w taki sposób, aby można je było przesłonić w podklasie. W tym celu
w Javie i C# widoczność metod musi być co najmniej chroniona. W C++ metody
mogą pozostawać prywatne, a mimo to będzie je można przesłaniać w podklasach.
4. Utwórz podklasę przesłaniającą metody. Sprawdź, czy możesz ją skompilować
w jarzmie testowym.
ZASTĄPIENIE ZMIENNEJ INSTANCJI 401

Zastąpienie zmiennej instancji


Tworzenie obiektów w konstruktorach może być problematyczne, zwłaszcza jeśli na
obiektach takich trzeba polegać podczas testów. W większości przypadków, aby ominąć
ten problem, można wyodrębnić i przesłonić metodę wytwórczą (351). W językach, które
nie pozwalają na przesłanianie wywołań funkcji wirtualnych w konstruktorach, musimy
jednak poszukać innej możliwości. Jedną z nich jest zastąpienie zmiennej instancji.
Oto przykład ukazujący problem z funkcją wirtualną w C++:
class Pager
{
public:
Pager() {
reset();
formConnection();
}

virtual void formConnection() {


assert(state == READY);
// tutaj znajduje się paskudny kod gadający ze sprzętem
...
}

void sendMessage(const std::string& address,


const std::string& message) {
formConnection();
...
}
...
};

Metoda formConnection jest wywoływana z konstruktora. Nie ma nic złego w kon-


struktorach, które delegują pracę do innych funkcji, ale w kodzie tym jest coś, co może
nas wprowadzić w błąd. Metoda formConnection została zadeklarowana jako wirtualna,
wygląda więc na to, że moglibyśmy po prostu utworzyć podklasę i przesłonić metodę
(398). Nie tak szybko. Najpierw spróbujmy to zrobić:
class TestingPager : public Pager
{
public:
virtual void formConnection() {
}
};

TEST(messaging,Pager)
{
TestingPager pager;
pager.sendMessage("5551212",
"Hej, chcesz pójść na imprezę? XXXOOO");
LONGS_EQUAL(OKAY, pager.getStatus());
}
402 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

Kiedy przesłaniamy funkcję wirtualną w C++, zastępujemy zachowanie tej funkcji


w klasach pochodnych, tak jak się tego spodziewaliśmy, ale z jednym wyjątkiem. Język
C++ nie pozwala na przesłanianie, gdy odwołanie do funkcji wirtualnej następuje w kon-
struktorze. W naszym przykładzie oznacza to, że kiedy wywołujemy funkcję sendMessage,
użyta zostanie funkcja TestingPager::formConnection. To dobrze, bo tak naprawdę nie
chcieliśmy wysyłać flirciarskiej wiadomości do operatora informacji, ale niestety, właśnie
to zrobiliśmy. Kiedy konstruowaliśmy obiekt klasy TestingPager, w czasie inicjalizacji
została wywołana metoda Page::formConnection, gdyż C++ nie pozwala na przesłanianie
w konstruktorze.
W C++ obowiązuje taka zasada, ponieważ wywoływanie przesłoniętych funkcji
wirtualnych przez konstruktor może być niebezpieczne. Wyobraź sobie następujący
scenariusz:
class A
{
public:
A() {
someMethod();
}

virtual void someMethod() {


}
};

class B : public A
{
C *c;
public:

B() {
c = new C;
}

virtual void someMethod() {


c.doSomething();
}
};

Mamy tu metodę someMethod klasy B przesłaniającą metodę klasy A. Pamiętaj jednak


o kolejności wywoływania konstruktorów. Kiedy tworzymy instancję klasy B, konstruktor
klasy A zostanie wywołany przed konstruktorem klasy B. Tak więc konstruktor klasy A
wywołuje metodę someMethod, która jest przesłaniana, dzięki czemu użyta zostanie
metoda z klasy B. Próbuje ona wywołać metodę doSomething z obiektem klasy C. I co się
dzieje? Jej instancja nie została utworzona, ponieważ konstruktor w klasie B nie został
jeszcze uruchomiony.
C++ zapobiega powstawaniu takich sytuacji. Inne języki są bardziej tolerancyjne.
Na przykład przesłonięte metody można wywoływać z konstruktorów w Javie, chociaż
nie zalecam takich praktyk w kodzie produkcyjnym.
ZASTĄPIENIE ZMIENNEJ INSTANCJI 403

W C++ taki mechanizm ochronny uniemożliwia zastępowanie zachowania w kon-


struktorach. Na szczęście możemy to zrobić w inny sposób. Jeśli zastępowany obiekt nie jest
używany w konstruktorze, w celu usunięcia zależności możemy wyodrębnić i prze-
słonić getter (353). Jeśli korzystasz z obiektu, ale chcesz mieć pewność, że można go za-
stąpić przed wywołaniem innej metody, będziesz mógł skorzystać z zastąpienia zmiennej
instancji. Oto przykład:
BlendingPen::BlendingPen()
{
setName("BlendingPen");
m_param = ParameterFactory::createParameter(
"cm", "Fade", "Aspect Alter");
m_param->addChoice("blend");
m_param->addChoice("add");
m_param->addChoice("filter");

setParamByName("cm", "blend");
}

W tym przypadku konstruktor tworzy parametr za pomocą wytwórni. Moglibyśmy


wprowadzić statyczny setter (370) i przejąć kontrolę nad kolejnym obiektem, który
zwróci wytwórnia, ale takie rozwiązanie byłoby dość inwazyjne. Jeśli nie mamy nic prze-
ciwko dodaniu do klasy jeszcze jednej metody, moglibyśmy zastąpić parametr, który
utworzyliśmy w konstruktorze:
void BlendingPen::supersedeParameter(Parameter *newParameter)
{
delete m_param;

m_param = newParameter;

W testach możemy tworzyć elementy, kiedy są one nam potrzebne, i wywoływać


metodę supersedeParameter, gdy musimy wstawić obiekt rozpoznający.
Powierzchniowo zastąpienie zmiennej instancji wygląda jak marny sposób wstawie-
nia na miejsce obiektu rozpoznającego, chociaż w C++, gdy parametryzacja konstruktora
(377) jest zbyt niewygodna ze względu na skomplikowaną logikę konstruktora, zastąpienie
zmiennej instancji (401) może okazać się najlepszym wyborem. W językach pozwalają-
cych na wirtualne wywołania w konstruktorach zwykle lepszym wyborem będzie wy-
odrębnienie i przesłonięcie metody wytwórczej (351).

Udostępnianie setterów zmieniających bazowe obiekty, z których korzysta dany obiekt,


zwykle jest złą praktyką. Settery takie umożliwiają klientom wprowadzanie drastycznych
zmian w zachowaniu obiektu w czasie jego życia. Jeżeli ktoś ma możliwość wprowadzania
takich zmian, powinieneś znać historię tego obiektu, aby zrozumieć, co się dzieje, gdy wy-
wołujesz jedną z jego metod. Jeśli nie masz setterów, kod będzie łatwiejszy do zrozumienia.
404 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

Korzystanie z wyrazu supersede jako przedrostka w nazwie metody ma swoją dobrą


stronę, gdyż wyraz ten jest wymyślny i niezwykły. Jeżeli kiedykolwiek będziesz chciał
wiedzieć, czy programiści używają takich metod w kodzie produkcyjnym, będziesz
mógł szybko przeszukać kod i przekonać się, czy istotnie to robią.

Czynności
Aby zastąpić zmienną instancji, wykonaj następujące czynności:
1. Zidentyfikuj zmienną instancji, którą chcesz zastąpić.
2. Utwórz metodę o nazwie supersedeXXX, gdzie XXX jest nazwą zmiennej, którą
chcesz zastąpić.
3. W metodzie tej umieść kod potrzebny do zniszczenia poprzedniej instancji
zmiennej i nadaj jej nową wartość. Jeśli zmienna jest referencją, sprawdź, czy w kla-
sie nie ma żadnych innych referencji wskazujących obiekt, do którego odwołuje
się ta zmienna. Jeśli takie referencje istnieją, czeka Cię dodatkowa praca do wy-
konania w metodzie zastępującej, mająca na celu upewnienie się, że zastąpienie
obiektu jest bezpieczne i że przyniesie pożądany efekt.
PRZEDEFINIOWANIE SZABLONU 405

Przedefiniowanie szablonu
Wiele z technik usuwania zależności opisanych w tym rozdziale bazuje na mechani-
zmach typowych w zorientowaniu obiektowym, takich jak interfejsy oraz implementacja
dziedziczenia. Niektóre z nowszych języków udostępniają dodatkowe możliwości. Jeśli na
przykład dany język wspiera generyczność oraz możliwość aliasowania typów, będziesz
mógł usuwać zależności, korzystając z techniki o nazwie przedefiniowanie szablonu. Oto
przykład jej zastosowania w C++:
// AsyncReceptionPort.h

class AsyncReceptionPort
{
private:
CSocket m_socket;
Packet m_packet;
int m_segmentSize;
...

public:
AsyncReceptionPort();
void Run();
...
};

// AsynchReceptionPort.cpp

void AsyncReceptionPort::Run() {
for(int n = 0; n < m_segmentSize; ++n) {
int bufferSize = m_bufferMax;
if (n = m_segmentSize - 1)
bufferSize = m_remainingSize;
m_socket.receive(m_receiveBuffer, bufferSize);
m_packet.mark();
m_packet.append(m_receiveBuffer,bufferSize);
m_packet.pack();
}
m_packet.finalize();
}

Jeśli mamy taki kod i chcemy wprowadzić zmiany w logice metody, okaże się, że nie
możemy uruchomić jej w jarzmie testowym bez wysłania czegoś poprzez gniazdo. W C++
możemy całkowicie uniknąć takiej sytuacji, jeżeli AsyncReceptionPort nie będzie zwykłą
klasą, tylko szablonem. Tak będzie wyglądać nasz kod po wprowadzeniu zmian (za chwilę
przejdziemy do dalszych czynności):
// AsynchReceptionPort.h

template<typename SOCKET> class AsyncReceptionPortImpl


{
private:
406 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

SOCKET m_socket;
Packet m_packet;
int m_segmentSize;
...

public:
AsyncReceptionPortImpl();
void Run();
...
};

template<typename SOCKET>
void AsyncReceptionPortImpl<SOCKET>::Run() {
for(int n = 0; n < m_segmentSize; ++n) {
int bufferSize = m_bufferMax;
if (n = m_segmentSize - 1)
bufferSize = m_remainingSize;
m_socket.receive(m_receiveBuffer, bufferSize);
m_packet.mark();
m_packet.append(m_receiveBuffer,bufferSize);
m_packet.pack();
}
m_packet.finalize();
}
typedef AsyncReceptionPortImpl<CSocket> AsyncReceptionPort;

Kiedy już dokonamy powyższych zmian, będziemy mogli utworzyć w pliku testowym
instancję szablonu o innym typie:
// TestAsynchReceptionPort.cpp

#include "AsyncReceptionPort.h"

class FakeSocket
{
public:
void receive(char *, int size) { ... }
};

TEST(Run,AsyncReceptionPort)
{
AsyncReceptionPortImpl<FakeSocket> port;
...
}

Najlepsze w tej technice jest to, że możemy korzystać z dyrektywy typedef, aby uniknąć
konieczności zmiany referencji w całej naszej bazie kodu. Bez niej musielibyśmy zastąpić
wszystkie referencje do AsyncReceptionPort referencjami do AsyncReceptionPort
<CSocket>. Byłoby to bardzo żmudne zajęcie, ale łatwiejsze, niż może się wydawać. Aby
upewnić się, że zmieniliśmy każdą potrzebną referencję, moglibyśmy skorzystać ze wspar-
cia kompilatora (317). W językach wspierających generyczność, ale nieoferujących
mechanizmu aliasowania typów (takiego jak dyrektywa typedef), będziesz zmuszony
do skorzystania ze wsparcia kompilatora.
PRZEDEFINIOWANIE SZABLONU 407

W C++ możesz użyć tej techniki, aby zamiast danych udostępnić alternatywne de-
finicje metod, chociaż takie rozwiązanie nie jest zbyt eleganckie. Reguły C++ obligują
Cię do określenia parametru szablonu, w związku z czym możesz wybrać dowolną
zmienną i wskazać jej typ jako parametr tego szablonu albo dodać nową zmienną tylko
po to, aby umożliwić parametryzację klasy na podstawie jakiegoś typu, chociaż postąpił-
bym w ten sposób, gdybym nie miał już żadnego innego wyjścia. Przede wszystkim
bardzo starannie sprawdziłbym, czy nie mam możliwości zastosowania technik bazu-
jących na dziedziczeniu.

Przedefiniowanie szablonu ma w C++ jedną zasadniczą wadę. Kiedy przekształcasz w szablon


kod, który znajdował się w plikach implementacyjnych, jest on przenoszony do nagłówków,
co może rozbudować zależności istniejące w systemie. Za każdym razem, gdy zmieni się kod
szablonu, konieczne będzie ponowne kompilowanie jego klientów.
Zwykle podczas usuwania zależności w C++ skłaniam się w stronę technik bazujących na
dziedziczeniu. Przedefiniowanie szablonu może być jednak przydatne, kiedy zależności, które
chcesz usunąć, znajdują się w kodzie szablonu. Oto przykład:
template<typename ArcContact> class CollaborationManager
{
...
ContactManager<ArcContact> m_contactManager;
...
};

Gdybyśmy chcieli usunąć zależność od zmiennej m_contactManager, moglibyśmy z łatwością za-


stosować wobec niej technikę wyodrębniania interfejsu (361), ze względu na sposób, w jaki
korzystamy tu z szablonów. Możemy jednak inaczej sparametryzować szablon:
template<typename ArcContactManager> class CollaborationManager
{
...
ArcContactManager m_contactManager;
...
};

Czynności
Oto opis, jak przedefiniować szablon w C++. W pozostałych językach wspierających
generyczność czynności te mogą być inne, ale opis ten da Ci posmak stosowania tej
techniki.
1. W klasie, którą chcesz przetestować, zidentyfikuj funkcjonalności, które chcesz
zastąpić.
2. Przekształć klasę w szablon, parametryzując ją za pomocą zmiennych, które
chcesz zastąpić, i kopiując ciała metod do nagłówka.
3. Nadaj szablonowi inną nazwę. Możesz zrobić to w sposób mechaniczny, dodając
do jego oryginalnej nazwy przyrostek Impl.
408 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

4. Po definicji szablonu dodaj instrukcję typedef definiującą szablon z jego orygi-


nalnymi argumentami oraz oryginalną nazwą klasy.
5. W pliku testowym dołącz definicję szablonu oraz utwórz jego instancję bazującą
na nowych typach, które znajdą się na miejscu typów, które musisz zastąpić
w testach.
PRZEDEFINIOWANIE TEKSTU 409

Przedefiniowanie tekstu
Niektóre z nowszych języków interpretowanych udostępniają bardzo przyjemny sposób
usuwania zależności. Podczas interpretacji metoda może zostać przedefiniowana „w locie”.
Oto przykład w języku Ruby:
# Account.rb
class Account
def report_deposit(value)
...
end

def deposit(value)
@balance += value
report_deposit(value)
end

def withdraw(value)
@balance -= value
end
end

Jeśli nie chcemy, aby funkcja report_deposit wykonała się podczas testu, możemy
przedefiniować ją w pliku testowym i umieścić testy już po tej zmianie:
# AccountTest.rb
require "runit/testcase"
require "Account"

class Account
def report_deposit(value)
end
end

# tutaj zaczynają się testy


class AccountTest < RUNIT::TestCase
...
end

Ważne jest zwrócenie uwagi na fakt, że nie zmieniamy definicji całej klasy Account,
a jedynie modyfikujemy metodę report_deposit. Interpreter Ruby interpretuje wszystkie
wiersze w pliku jako polecenia do wykonania. Polecenie class Account otwiera definicję
klasy Account, dzięki czemu można dodać do niej dodatkowe definicje. Polecenie def
report_deposit(value) rozpoczyna proces dodawania definicji do otwartej klasy. Inter-
preter Ruby nie dba o to, czy istnieje już definicja tej metody. Jeśli tak, po prostu ją za-
stępuje.

Przedefiniowanie tekstu w Ruby ma pewną wadę. Nowa metoda zastępuje starą na cały czas
działania programu, co może spowodować problemy, jeśli zapomnisz, że definicja określonej
metody została zmieniona we wcześniejszym teście.
410 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI

Z przedefiniowania tekstu można również korzystać w C i C++, posługując się preproce-


sorem. Aby zobaczyć przykład zastosowania tej techniki, zajrzyj do przykładu ze spoiną
preprocesową (51) w rozdziale 4., „Model spoinowy”.

Czynności
Aby przedefiniować tekst w języku Ruby, wykonaj następujące czynności:
1. Zidentyfikuj klasę z definicjami, które chcesz zastąpić.
2. Na początku testowego pliku źródłowego dodaj klauzulę require z nazwą mo-
dułu zawierającego tę klasę.
3. Na początku testowego pliku źródłowego dodaj alternatywne definicje wszystkich
metod, które chcesz zastąpić.
Dodatek

Refaktoryzacja

Refaktoryzacja jest najważniejszą techniką ulepszania kodu. Kanoniczną publikacją


poświęconą refaktoryzacji jest książka Martina Fowlera Refaktoryzacja. Ulepszanie struktu-
ry istniejącego kodu (Helion 2011). Znajdziesz w niej więcej informacji na temat rodzaju
refaktoryzacji, którą możesz przeprowadzić, gdy w swoim kodzie masz już poroz-
mieszczane testy.
W tym dodatku opiszę jedną z kluczowych technik refaktoryzacji, jaką jest wyod-
rębnianie metody. Dzięki niej poczujesz przedsmak mechaniki związanej z refaktory-
zacją wspieraną testami.

Wyodrębnianie metody
Ze wszystkich technik refaktoryzacji prawdopodobnie najprzydatniejsze jest wyod-
rębnianie metody. Pomysł kryjący się za wyodrębnianiem metody polega na rozbijaniu
istniejących dużych metod na mniejsze. Kiedy tak postępujemy, sprawiamy, że nasz
kod jest łatwiejszy do zrozumienia. Ponadto często możemy ponownie zastosować
fragmenty kodu i uniknąć powielania tej samej logiki w innych obszarach systemu.

W kiepsko zarządzanych bazach kodu metody mają tendencję do rozrastania się. Progra-
miści dodają logikę do istniejących metod, a one po prostu robią się coraz większe. Gdy tak
się stanie, metody mogą ostatecznie wykonywać dwa albo trzy różne zadania dla wywołu-
jących je klientów. W przypadkach patologicznych takich zadań mogą być dziesiątki albo
setki. Wyodrębnianie metody jest lekarstwem na taki stan rzeczy.

Kiedy chcesz wyodrębnić metodę, pierwsze, czego potrzebujesz, to zestaw testów. Jeśli
dysponujesz testami, które dokładnie weryfikują istniejącą metodę, będziesz mógł z niej
wyodrębnić metody, wykonując następujące czynności:
1. Zidentyfikuj kod, który chcesz wyodrębnić, i przekształć go w komentarz.
412 DODATEK REFAKTORYZACJA

2. Wymyśl nazwę dla nowej metody i utwórz ją jako metodę pustą.


3. Umieść wywołanie nowej metody w starej metodzie.
4. Przekopiuj wyodrębniany kod do nowej metody.
5. Zastosuj technikę wsparcia kompilatora (317), aby dowiedzieć się, jakie para-
metry należy przekazać i jakie wartości zwrócić.
6. Popraw deklarację metody, aby przyjmowała parametry, oraz uwzględnij war-
tość zwrotną (jeśli taka występuje).
7. Przeprowadź testy.
8. Usuń kod oznaczony jako komentarz.
Oto przykład w Javie:
public class Reservation
{
public int calculateHandlingFee(int amount) {
int result = 0;

if (amount < 100) {


result += getBaseFee(amount);
}
else {
result += (amount * PREMIUM_RATE_ADJ) + SURCHARGE;
}
return result;
}
...
}

Logika w instrukcji else oblicza opłatę manipulacyjną dla uprzywilejowanych rezer-


wacji. Takiej samej logiki potrzebujemy użyć w jakimś innym miejscu naszego systemu.
Zamiast powielać kod, możemy go wyodrębnić z istniejącego już miejsca i zastosować
gdzie indziej.
Oto pierwszy krok:
public class Reservation
{
public int calculateHandlingFee(int amount) {
int result = 0;

if (amount < 100) {


result += getBaseFee(amount);
}
else {
// result += (amount * PREMIUM_RATE_ADJ) + SURCHARGE;
}
return result;
}
...
}
WYODRĘBNIANIE METODY 413

Nowej metodzie chcemy nadać nazwę getPremiumFee, tak więc dodajemy ją oraz jej
wywołanie:
public class Reservation
{
public int calculateHandlingFee(int amount) {
int result = 0;

if (amount < 100) {


result += getBaseFee(amount);
}
else {
// result += (amount * PREMIUM_RATE_ADJ) + SURCHARGE;
result += getPremiumFee();
}
return result;
}

int getPremiumFee() {
}
...
}

Następnie kopiujemy stary kod do nowej metody i sprawdzamy, czy się kompiluje:
public class Reservation
{
public int calculateHandlingFee(int amount) {
int result = 0;

if (amount < 100) {


result += getBaseFee(amount);
}
else {
// result += (amount * PREMIUM_RATE_ADJ) + SURCHARGE;
result += getPremiumFee();
}
return result;
}

int getPremiumFee() {
result += (amount * PREMIUM_RATE_ADJ) + SURCHARGE;
}
...
}

Nie kompiluje się. W kodzie używane są zmienne result i amount, które nie zostały
zadeklarowane. Ponieważ obliczamy jedynie część wyniku, moglibyśmy zwracać tylko to,
co uzyskujemy. Ze zmienną amount możemy sobie poradzić, robiąc z niej parametr metody
i dodając ją do wywołania:
public class Reservation
{
public int calculateHandlingFee(int amount) {
int result = 0;
414 DODATEK REFAKTORYZACJA

if (amount < 100) {


result += getBaseFee(amount);
}
else {
// result += (amount * PREMIUM_RATE_ADJ) + SURCHARGE;
result += getPremiumFee(amount);
}
return result;
}

int getPremiumFee(int amount) {


return (amount * PREMIUM_RATE_ADJ) + SURCHARGE;
}
...
}

Teraz znowu możemy uruchomić testy i sprawdzić, czy działają. Jeśli tak, będziemy
mogli powrócić do kodu i pozbyć się kodu oznaczonego jako komentarz:
public class Reservation
{
public int calculateHandlingFee(int amount) {
int result = 0;

if (amount < 100) {


result += getBaseFee(amount);
}
else {
result += getPremiumFee(amount);
}
return result;
}

int getPremiumFee(int amount) {


return (amount * PREMIUM_RATE_ADJ) + SURCHARGE;
}
...
}

Chociaż nie jest to bezwzględnie konieczne, lubię przekształcać w komentarz kod, który
mam zamiar wyodrębnić. Dzięki temu, jeśli popełnię błąd i test nie powiedzie się, z łatwością
będę mógł powrócić do wcześniejszego stanu, doprowadzić test do wykonania się, po czym
spróbować ponownie.

Przykład, który pokazałem, to tylko jeden ze sposobów na przeprowadzenie wyod-


rębniania metody. Jeżeli dysponujesz testami, jest to względnie prosta i bezpieczna opera-
cja. Jest ona jeszcze łatwiejsza, gdy masz narzędzie do refaktoryzacji. Wszystko, co bę-
dziesz musiał zrobić, to wybrać fragment metody i wybrać polecenie z menu. Narzędzie
sprawdzi, czy kod można wyodrębnić jako metodę, i poprosi Cię o podanie jej nazwy.
Wyodrębnianie metody jest najważniejszą techniką stosowaną podczas pracy nad cu-
dzym kodem. Można z niej korzystać w celu wyodrębniania powielonego kodu, roz-
dzielania odpowiedzialności oraz rozbijania długich metod.
Słownik

fałszywy obiekt — obiekt udający w czasie testów współpracownika klasy.


funkcja wolna — funkcja, która nie jest częścią żadnej klasy. W C oraz innych języ-
kach proceduralnych są one nazywane po prostu funkcjami. W C++ są nazywane funk-
cjami nieskładowymi. Funkcje wolne nie występują w Javie ani C#.
jarzmo testowe — oprogramowanie umożliwiające przeprowadzanie testów jednost-
kowych.
liczba powiązań — liczba wartości przekazywanych do metody i od niej wychodzących
podczas jej wywołania. Jeśli metoda nie ma wartości zwrotnej, jest ona równa liczbie jej
parametrów. Jeśli wartość zwrotna występuje, to jest to liczba parametrów powiększona
o jeden. Określenie liczby powiązań może być bardzo przydatne w przypadku małych me-
tod, które chcesz wyodrębnić, gdy musisz wyodrębniać bez przeprowadzania testów.
obiekt pozorowany — fałszywy obiekt, który zachowuje wewnętrzne warunki obiektu
prawdziwego.
podklasa testowa — podklasa utworzona w celu umożliwienia na czas testów dostępu
do klasy nadrzędnej.
programowanie różnicowe — sposób wykorzystania dziedziczenia w celu dodawania
funkcjonalności w systemach zorientowanych obiektowo. Często można z niego korzy-
stać, aby szybko wstawić do systemu nową funkcjonalność. Testy, które są pisane w celu
zweryfikowania nowej funkcjonalności, mogą być w dalszej kolejności użyte do przepro-
wadzenia refaktoryzacji kodu i pozostawienia go w lepszym stanie.
programowanie sterowane testami — proces programowania polegający na napisaniu
przypadków testowych, które kończą się niepowodzeniem, po czym doprowadzaniu
ich kolejno do sukcesu. W trakcie tego procesu przeprowadzana jest refaktoryzacja, aby
kod pozostawał możliwie najprostszy. Kod opracowany przy zastosowaniu techniki
programowania sterowanego testami jest z definicji pokryty testami.
416 SŁOWNIK

punkt przechwycenia — miejsce, w którym można umieścić test, aby poznać pewien
warunek występujący w programie.
punkt zmiany — miejsce w kodzie, w którym należy wprowadzić zmianę.
punkt zwężenia — przewężenie na schemacie skutków wskazujące idealne miejsce na
przetestowanie wielu funkcjonalności.
schemat funkcjonalności — niewielki, ręcznie rysowany schemat, pokazujący, w jaki
sposób metody w klasie korzystają z innych metod oraz zmiennych instancji. Schematy
funkcjonalności mogą być przydatne, kiedy próbujesz zdecydować, jak rozbić na części
dużą klasę.
schemat skutków — niewielki, ręcznie rysowany schemat, pokazujący, na które zmienne
i wartości zwracane przez metody może mieć wpływ zmiana w kodzie. Schematy skutków
mogą być przydatne, kiedy próbujesz zdecydować, w którym miejscu umieścić testy.
spoina — miejsce, w którym można zmienić zachowanie kodu bez konieczności dokony-
wania w nim edycji. Spoiną jest na przykład wywołanie funkcji polimorficznej w obiekcie,
ponieważ można utworzyć podklasę tego obiektu i spowodować, że jego zachowanie
będzie inne.
spoina konsolidacyjna — miejsce, w którym można zmienić zachowanie programu
poprzez dołączenie biblioteki. W językach kompilowanych można w czasie testowania
zastępować biblioteki produkcyjne, biblioteki DLL, pliki assembly albo JAR innymi
plikami w celu pozbycia się zależności lub poznania pewnych warunków, które mogą
zachodzić w testach.
spoina obiektowa — miejsce, w którym można zmieniać zachowanie programu, za-
stępując jeden obiekt innym. W językach zorientowanych obiektowo zwykle robi się to
poprzez utworzenie podklasy danej klasy w kodzie produkcyjnym i przesłonięcie różnych
metod klasy nadrzędnej.
test charakteryzujący — test napisany w celu udokumentowania bieżącego zachowania
oprogramowania i jego pozostawienia podczas wprowadzania zmian w kodzie.
test jednostkowy — test, którego przeprowadzenie trwa krócej niż 1/10 sekundy; wystar-
czająco mały, by umożliwić zlokalizowanie problemu, gdy zakończy się niepowodzeniem.
Skorowidz

#define, 52 aplikacja
#include, 53, 61 bez struktury, 225
jako wywołania API, 209
A odpowiedzialność kodu, 213
zidentyfikowanie obliczeniowego rdzenia
abstrakcyjna klasa nadrzędna, 386, 389 kodu, 213
Account, 135, 409 architekt, 225
AccountDetailFrame, 158, 161, 162, 163 architektura systemu, Patrz struktura aplikacji
po topornej refaktoryzacji, 163 ArithmeticException, 105
ACMEController, 91 arkusz z tekstem, 47
adaptacja parametru, 328 ArtR56Display, 41
czynności, 331 asercje, 196
pomocne funkcje języka, 156 aspekty, 177
ryzyko, 330 assertEquals, 69
AddEmployeeCmd, 275, 281 AsyncReceptionPort, 405
AddOpportunityFormHandler, 99, 100 automatyczna refaktoryzacja, 63, 64
AddOpportunityXMLGenerator, 99 bez testów, 65, 297
addPermit, 138 długie metody, 297
AddsEmployeeCmd, 279
addText, 177
B
adnotowanie listingów, 221
obrysowywanie bloków, 222 BankingServices, 368
wyodrębnianie metod, 222 bezpieczeństwo, 331
wyodrębnianie odpowiedzialności, 221 bezpieczne zmiany, 239
zrozumienie skutków zmiany, 222 biblioteka, 207
zrozumienie struktury metody, 221 fałszywek, 241
AGGController, 340 graficzna, 57
algorytm pisania testów charakteryzujących, 196 BillingStatement, 188, 189
analiza rozmowy, 232 BindName, 338
opisywanie projektów, 233 błędy, 200
rozbieżność między rozmową a kodem, 233 BondRegistry, 365
analiza skutków, 179 budowanie systemu, 321
punkty przechwycenia, 184 buildMartSheet, 59
wsparcie zintegrowanego środowiska
programistycznego, 166 C
analizator reguł, 255
AnonymousMessageForwarder, 111, 113 C++, 141
calculatePay(), 87
CAsyncSslRec, 50
418 SKOROWIDZ

CCAImage, 153 słabe przystosowanie do testowania, 47


cell, 58 szukanie błędów, 195
charakterystyka działania kodu, 213 umieszczenie klasy w jarzmie testowym, 97
charakteryzowanie klas, 199 usuwanie zależności, 35, 37
heurystyka, 199 utworzenie obiektu klasy, 122
przekazywane informacje, 200 wprowadzanie zmian, 33
znajdowanie błędów, 200 wyodrębnianie metody, 414
charakteryzowanie rozgałęzień, 202 zaśmiecenie interfejsami, 330
chwilowe sprzężenie, 85 znajdowanie błędów, 200
ciąg zniechęcenie, 322
skutków, 186 CustomSpreadsheet, 59
zależności, 143 czas wprowadzenia zmiany, 75
classpath, 55 długość, 95
ClassReader, 169 kiełkowanie
CLateBindingDispatchDriver, 338 klasy, 80
Command, 279, 282 metody, 77
z usuniętą duplikacją, 288, 289 opakowywanie
command/query separation, 161 klasy, 88
CommoditySelectionPanel, 297 metody, 85
const, 176, 178, 179, 370 opóźnienie, 96
ConsultantSchedulerDB, 99 usuwanie zależności, 97
Coordinate, 177 zrozumienie kodu, 95
CppClass, 166, 170, 179 czysty kod, 12
CppUnitLite, 68
CRC, 230 D
createForwardMessage, 398
CreditMaster, 123 dane statyczne, 347
CreditValidator, 122 db_update, 52
cudzy kod, 10, 11 declarations, 167
algorytm dodawania zmian, 36 Declarations, 170
brak warstw abstrakcji, 330 deklaracja
dołączanie, 209 using, 154
dylemat, 34 deklarowanie typu, 338
identyfikacja miejsca zmian, 36 dekorator, 89
język proceduralny, 239 delegator instancji, 367
kontakt z większą społecznością, 322 delegowanie do klasy, 115
kopiowanie kodu, 108 delete, 132
miejsca na wstawienie testów, 36 destruktor, 132
motywacja do pracy, 322 wirtualny, 357
myślenie o skutkach, 170 detailDisplay, 160
narzędzia pracy, 63 diagramy, 230
objęcie testami, 103 dispatchPayment(), 86, 87
pisanie testów, 37 Display, 42
podstawowa poprawność, 179 długie metody, 293, 411
praca nad kodem, 321 automatyczna refaktoryzacja, 297
programowanie sterowane testami, 109 zmiana kolejności instrukcji, 297
refaktoryzacja, 37 nadawanie nazw wysokopoziomowym
reguły w bazie kodu, 179 fragmentom metod, 298
skutki zmian, 166 narzędzia refaktoryzujące, 297
SKOROWIDZ 419

przeniesienie do nowej klasy, 332 dostrzeganie odpowiedzialności, 257


przenoszenie metod, 299 double, 203
refaktoryzacja, 296 draw(), 333
ręczna refaktoryzacja, 300 drawPoint, 334
gromadzenie zależności, 305 drugi moment statystyczny, 107
wprowadzenie zmiennej rozpoznającej, 300 duplikaty, 103
wyłonienie obiektu metody, 306 eliminacja, 275
wyodrębniaj to, co znasz, 304 usuwanie, 108
rodzaje, 294 duże klasy, 253
strategia, 307 decyzje, które można zmienić, 259
bądź gotów na powtórne wyodrębnianie, dezorientacja, 253
309 edytuj i módl się, 254
szkieletyzuj metody, 307 główna odpowiedzialność, 266
szukaj sekwencji, 307 grupowanie metod, 257
wyodrębniaj małe fragmenty, 309 kiełkowanie
wyodrębniaj najpierw do bieżącej klasy, klasy, 254
308 metody, 254
wyłonienie obiektu metody, 332 po wyodrębnieniu klasy, 273
wyodrębnianie przenoszenie kodu, 268
kodu, 297 refaktoryzacja, 254
poleceń, 304 szybka, 269
wyodrębnienia o niskiej liczbie powiązań, 304 wyodrębnianie klasy, 271
zachowanie głównej logiki, 305 rozdzielanie interfejsu, 268
złożona logika, 300 skupienie na bieżącej pracy, 269
zmienne rozpoznające, 303 strategia, 270
dodawanie taktyka, 270
funkcji, 21, 24, 25 testowanie, 254
nowej funkcjonalności, 103 ukryte metody, 258
do klasy, 83 wewnętrzne relacje, 259
duplikaty, 103 wyodrębnianie klas, 259
kiełkowanie, 103 wybór techniki, 270
nowy kod, 77 zasada rozdzielania interfejsów, 268
opakowywanie, 103 zidentyfikowanie odpowiedzialności, 256
oszacowanie czasu, 77 dylemat
poddawanie kodu testom, 103 jednorazowości, 208
programowanie sterowane testami, 104 ograniczonego przesłaniania, 208
umieszczanie w podklasach, 113 dynamiczna konsolidacja, 55
uproszczenie parametru, 384 dyrektywy
usuwanie duplikatów, 109 #include, 143
w jednym miejscu, 91 include, 141
wiele istniejących obiektów, 91 kompilacji warunkowej, 52
zbiór własności, 114 typedef, 406
zmiany w wielu miejscach, 183 using, 141
zachowania dziedziczenie, 110
do istniejących metod, 85 problemy, 113
dołączanie kodu, 209 wywołanie błędów, 116
DOMBuilder, 300
dostęp do kodu, 151
420 SKOROWIDZ

E FocusWidget, 132
form_command, 245
edytowanie kodu, 311 formConnection, 401
błędy, 314 formStyles, 349
edytowanie jednego elementu naraz, 313 Formula, 67
programowanie w parach, 318 FormulaCell, 58
superświadome, 312 FormulaTest, 67
wsparcie kompilatora, 317 forward, 243
zachowania, 312 forwardMessage, 111
zachowywanie sygnatur, 314 Frame, 341
zmiany w metodach, 314 Framework for Integrated Tests, 71
edytuj i módl się, 27 frequently asked questions, 17
edytuj-kompiluj-konsoliduj-testuj, 97 FuelShare, 201
Element, 174 funkcja
elements, 172 niewirtualna, 365
elementy globalne, 340 dostęp przez interfejs, 366
ominięcie zależności, 396 opakowująca, 246
wytwórnia, 372 wirtualna
Employee, 88 C++, 353
EndPoint, 40 wywoływanie przesłoniętych funkcji, 402
expectedMessage, 111 zastąpienie zmiennej instancji, 401
ExternalRouter, 371 wolna, 343, 384, 415
funkcje
F buildMartSheet, 59
db_update, 52
Facility, 134 form_command, 245
FakeConnection, 125 formStyles, 349
FakeDisplay, 42, 44 GetOption, 343
FakeOriginationPermit, 149 ksr_notify, 241, 247
FakeTransactionLog, 362 leniwy getter, 354
fałszowanie, 44 mart_key_send, 244
fałszywa biblioteka, 375 PostReceiveError, 49, 61
fałszywe klasy, 235 report_deposit, 409
fałszywe obiekty, 41, 145, 415 scan_packets, 241, 242
dwie strony, 44 send_command, 244
tworzenie, 124 SequenceHasGapFor, 384
wspomaganie testowania, 43 setOption, 343
fałszywki, 44, 129, 192 zastępowanie inną, 375
biblioteka, 241 funkcjonalność, 386
korzystanie, 144 rozłożenie w klasach, 388
fasada, 267
final, 156, 158, 170, 370
firewall, 177
G
firstMomentAbout, 105, 106 GDIBrush, 333
FIT, 71 generate(), 82
fit.Fixture, 55 generateIndex, 171, 172
fit.Parse, 55 getBodySize(), 284
FitFilter, 54 getDeadtime, 387
Fitnesse, 71 getDeclaration(int index), 168
SKOROWIDZ 421

getFrom, 113 rozpoczynanie, 343


getFromAddress, 110, 115, 117 źródło opcji, 344
getInstance, 134 zmiennej globalnej, 317
getInterface, 168, 181, 182 hierarchia obiektów klasy Permit, 148
getKSRStreams, 156 z wyodrębnionymi interfejsami, 148
getLastLine, 43 hierarchia znormalizowana, 118
getName, 167 HttpFileCollection, 156, 158
GetOption, 343 HttpPostedFile, 156
getParameterForName, 329 HttpPostedFileWrapper, 157
getSize, 283 HttpServletRequest, 328
przeniesienie metody, 284
getter, 353, 396 I
czas życia, 355
leniwy, 354 identyfikowanie
getValidationPercent, 122, 125, 126 obliczeniowego rdzenia kodu, 213
getValue, 184 odpowiedzialności, 257
globalna wytwórnia, 372 inne techniki, 269
globalResultNotifier, 249 IHttpPostedFile, 157
główne zadanie, 254 imadło programistyczne, 28
grafy, 220 import, 54
granica hermetyzacji, 191 index, 171
gromadzenie IndustrialFacility, 147
zależności, 305 informacja zwrotna, 28, 29
zmian, 96 algorytm dokonywania zmian w cudzym
grupowanie metod, 257 kodzie, 36
ćwiczenie grupowe, 258 błyskawiczna, 96
heurystyka, 257 opóźnienie, 96
grupy metod, 386 pokrycie testami, 33
przebudowywanie testu, 102
szybkie uruchamianie testu, 102
H
testowanie jednostkowe, 30
hasGapFor, 383 testy wyższego poziomu, 32
hermetyzacja, 182, 347 initialize, 130
duże klasy, 254 InMemoryDirectory, 171
granica, 191 instance, 370, 371
referencji do wolnych funkcji, 344 instancja testowa, 355
referencji globalnej, 247, 249, 251, 340 instrukcja warunkowa, 295
błędy kompilacji, 342 int, 203
czynności, 345 interakcje w systemie, 261
fałszywe obiekty, 342 interfejs, 148
fałszywki, 344 czysty, 357
funkcje nieskładowe, 343 Display, 42
nazywanie klasy, 341 IHttpPostedFile, 157
nowa klasa, 341 IPermitRepository, 140
odseparowanie, 342 komunikujący odpowiedzialności, 329
refaktoryzacja, 342 MailService, 214
referencja do elementu składowego klasy, MessageProcessor, 214
342 nazywanie, 356, 362
422 SKOROWIDZ

interfejs K
ParameterSource, 329
PointRenderer, 336 karta CRC, 230
SchedulingTask, 146 kiełkowanie, 103
interpretery języków, 256 kiełkowanie klasy, 80, 83
InvalidBasisException, 105 czynności, 84
Inventory, 396 długie metody, 293
InventoryControl, 189 duże klasy, 254
Invoice, 184, 188 uproszczenie parametru, 384
inwarianty, 199 zalety i wady, 84
IPermitRepository, 140 kiełkowanie metody, 77
irytujący parametr, 121, 123 czynności, 79
Item, 187 długie metody, 293
iteracje, 76 duże klasy, 254
przekazanie wartości pustej, 80
przekształcenie w statyczną metodę
J
publiczną, 80
jarzmo testowania jednostkowego, 66 stosowanie, 80
jarzmo testowe, 30, 126, 415 zalety i wady, 80
CppUnitLite, 68 zastosowanie, 92
duże klasy, 271 klasa
inne platformy xUnit, 70 abstrakcyjna, 146, 390
JUnit, 66, 67 biblioteczna, 207
kod zawierający singletony, 135 charakteryzowanie, 199
NUnit, 70 duża, 253
ogólne, 71 elementy globalne, 340
Fitnesse, 71 finalna, 207, 215
Framework for Integrated Tests, 71 główne zadanie, 254
problemy z uruchamianiem metody, 151 interfejsowa, 344
separowanie, 39 konwencja nazewnicza, 358, 363
tworzenie instancji klasy, 97, 121, 122, 334 logiczna, 299
C++, 144 nadawanie nazwy, 341
umieszczanie klasy, 183 nowa funkcjonalność, 84
utworzenie obiektu, 122 odpowiedzialność, 153, 192
zmiana klasy, 97 pojedyncza, 254
jednostki behawioralne systemu, 30 odrębny program testowy, 144
języki proceduralne, 239 odwzorowanie na zbiór koncepcji, 83
a zorientowanie obiektowe, 247 panelowa, 298
projektowanie obiektów, 251 pojedyncze instancje, 137
usuwanie zależności, 393 pomocnicza, 367
wyodrębnianie zależności, 251 produkcyjna, 102
zorientowane obiektowo rozszerzenia, 251 przekształcenie w interfejs, 89, 356
JUnit, 67, 122, 227 reguły z rozdzielonymi
architektura, 227 odpowiedzialnościami, 256
zestaw obiektów, 68 rozbijanie na fragmenty, 191
schemat funkcjonalności, 260
skróty w nazwach, 289
SKOROWIDZ 423

szablony, 405 fit.Parse, 55


testowa, 68, 122 FitFilter, 54
nazewnictwo, 235 FocusWidget, 132
testowanie niezależnie od siebie, 137 FormulaTest, 67
ukryta, 191 Frame, 341
wprowadzanie zmian, 99 FuelShare, 201
wzajemne zależności, 265 GDIBrush, 333
zależność od interfejsu, 102 HttpFileCollection, 156, 158
zapieczętowana, 156, 208 HttpPostedFile, 156
zbrylenie, 259 HttpServletRequest, 328
zgodność z zasadą podstawienia Liskov, 117 InMemoryDirectory, 171
klasy Inventory, 396
Account, 135, 409 InventoryControl, 189
AccountDetailFrame, 158, 161, 162, 163 Invoice, 184, 188
AddEmployeeCmd, 275, 281 Item, 187
AddOpportunityFormHandler, 99, 100 LinkageNode, 359
AddOpportunityXMLGenerator, 99 ListDriver, 213
AddsEmployeeCmd, 279 LoggingEmployee, 89
AGGController, 340 LoginCommand, 279, 281, 283
AnonymousMessageForwarder, 111 LogonCommand, 275
ArtR56Display, 41 MailForwarder, 110, 213
AsyncReceptionPort, 405 MailingConfiguration, 115, 116
BankingServices, 368 MailReceiver, 213
BillingStatement, 188, 189 MailSender, 213
BondRegistry, 365 MessageForwarder, 113, 398
CAsyncSslRec, 50 MessageRouter, 371
CCAImage, 153 MimeTesting, 398
ClassReader, 169 ModelNode, 357, 359
CLateBindingDispatchDriver, 338 NameObjectCollectionBase, 156
Command, 279, 282 NetworkBridge, 40
CommoditySelectionPanel, 297 Node, 359
ConsultantSchedulerDB, 99 OffMarketTradeValidator, 391
Coordinate, 177 OpportunityItem, 100
CppClass, 166, 170, 179 OptionSource, 344
CreditMaster, 123 OriginationPermit, 147, 149
CreditValidator, 122 OurHttpFileCollection, 157
Declarations, 170 Packet, 346
DOMBuilder, 300 PageGenerator, 196
Element, 174 PageLayout, 349
Employee, 88 Parser, 191
EndPoint, 40 PaydayTransaction, 361
ExternalRouter, 371 Permit, 148
Facility, 134 PermitRepository, 134, 138
FakeConnection, 125 PointRenderer, 335
FakeDisplay, 42 PremiumRegistry, 365
FakeOriginationPermit, 149 ProductionModelNode, 357
FakeTransactionLog, 362 QuarterlyReportTableHeaderGenerator, 82
FeeCalculator, 265 QuaterlyReportTableHeaderProducer, 82
fit.Fixture, 55 Renderer, 333
424 SKOROWIDZ

klasy kod testowy, 125, 235


Reservation, 260 konwencje nazewnicze klas, 235
ResultNotifier, 248 lokalizacja testu, 236
RGHConnection, 123 oddzielenie od kodu produkcyjnego, 237
RouteFactory, 373 komendy, 161
RSCWorkflow, 347 komentarze, 79
RuleParser, 256, 258 kod do wyodrębnienia, 414
Sale, 41 kompilacja, 54
Scanner, 249 kompilator, 317
ScheduledJob, 266 C++, 142
Scheduler, 142, 387 komponenty wielokrotnego użytku, 133
SchedulerDisplay, 143 konfiguracja, 116
SchedulingServices, 388 konsolidacja
SchedulingTask, 145 dynamiczna, 55
SerialTask, 145 statyczna, 56
Session, 215 konsolidator, 54
ShippingPricer, 185 konstrukcyjne kłębowisko, 131
StepNotifyController, 90 konstruktor
StyleMaster, 349 IndustrialFacility, 147
SymbolSource, 164 tworzenie obiektu, 377
Task, 254 ukryte zależności, 128, 131
TermTokenizer, 258 zależność od obiektu, 129
TestCase, 67, 68 konwencja kodowania, 208
TestingAsyncSslRec, 50 konwencje nazewnicze klas, 235
TestingExternalRouter, 372 Fake, 235
TestingMessageForwarder, 399 Test, 235
TestResult, 381 Testing, 236
ToolController, 90 konwersja
TransactionLog, 361 problemy, 204
TransactionRecorder, 362 z liczb podwójnej precyzji na całkowite, 203
WindowsOffMarketTradeValidator, 391 koszt rekompilacji, 98
kod koszty metod, 97
asercji, 67 kryj i modyfikuj, 27
bez testów, 12 ksr_notify, 241, 247
testujący, 30
zastany, 11 L
kod proceduralny
a zorientowanie obiektowe, 247 legacy code, 10
dodanie nowego zachowania, 244 leniwy getter, 354
funkcje wykonujące zadania liczba powiązań, 304, 415
obliczeniowe, 244 liczby podwójnej precyzji, 202
funkcje z wywołaniami zewnętrznymi, 246 LinkageNode, 359
możliwości, 251 lista
pułapki zależności, 244 declarations, 167
przypadki zmian, 240, 241 elements, 172
kod produkcyjny, 125 ListDriver, 213
funkcje, 344 LoggingEmployee, 89
zastępowanie zmiennej instancji, 132 LoginCommand, 279, 281, 283
SKOROWIDZ 425

LogonCommand, 275 metoda


lokalizacja testu, 236 abstrakcyjna, 118, 357
a wdrażanie aplikacji, 237 delegowanie do funkcji, 249
ograniczenia wdrożeniowe aplikacji, 236 funkcjonalność, 152
rozmiar kodu wdrożeniowego, 237 klasy testowej, 67
lokalizowanie zachowań, 118 komenda a zapytanie, 161
long, 203 monstrualna, 293
LONGS_EQUAL, 69 nazewnictwo, 381
odpowiedzialność, 255
Ł pomocnicza, 347
przesłanianie, 117
łączenie obiektów, 89 punktowana, 294
sekcje, 295
M zmienne tymczasowe, 295
reguła użycia, 199
MailForwarder, 213 szkieletyzacja, 307
MailingConfiguration, 115, 116 statyczna, 80, 346, 347
MailReceiver, 213 ograniczenie dostępu, 348
MailSender, 213 zastosowanie, 367
MailService, 214 testowalna, 153
makeLoggedPayment, 86 upublicznianie, 152
makra, 52 wirtualna, 208, 357
LONGS_EQUAL, 69 wysunięta, 295
TEST, 69 wytwórcza, 351
mart_key_send, 244 zmiana na chronioną, 154
martwy kod, 389 metody
mechanika zmian, 21 addPermit, 138
model spoinowy, 47 addText, 177
narzędzia, 63 AnonymousMessageForwarder, 113
praca z informacją zwrotną, 27 BindName, 338
rozpoznanie i separowanie, 39 calculatePay(), 87
testy, 28 createForwardMessage, 398
unikanie zmian, 26 dispatchPayment(), 86, 87
w dużym systemie, 26 draw, 333
w oprogramowaniu, 21 draw(), 333
w zachowaniu, 22 drawPoint, 334
zmiany w systemie, 27 firstMomentAbout, 105, 106
mechanizm formConnection, 401
dołączania deklaracji klasy do pliku, 142 forwardMessage, 111
refleksji, 67 generate(), 82
menedżer generateIndex, 171, 172
objaśniający, 353 getBodySize(), 284
transakcji, 353 getDeadtime, 387
MessageForwarder, 113, 398 getDeclaration(int index), 168
MessageProcessor, 214 getFrom, 113
MessageRouter, 371 getFromAddress, 110, 115, 117
metaklasa, 347 getInstance, 134
getInterface, 168 181, 182
426 SKOROWIDZ

metody mieszana kompilacja, 395


getKSRStreams, 156 MimeTesting, 398
getLastLine, 43 minitesty integracyjne, 192
getName, 167 model spoinowy, 47
getParameterForName, 329 ModelNode, 357, 359
getSize, 283 modułowość, 48
getValidationPercent, 122, 125, 126 modyfikacje w testach, 184
getValue, 184 modyfikowanie
hasGapFor, 383 danych statycznych lub globalnych, 177
initialize, 130 obiektów przekazywanych jako parametry, 177
instance, 370, 371 monstrualna metoda, 293
makeLoggedPayment, 86 mutable, 179
newFixedYield, 366
nthMomentAbout, 109
parseExpression, 191 N
pay(), 86, 89 nadawanie nazw interfejsom, 362
performCommand, 159, 161 należyta staranność, 27
populate, 328
NameObjectCollectionBase, 156
QuaterlyReportGenerator, 81
narzędzia, 63
readToken, 170
do automatycznej refaktoryzacji, 63
Recalculate, 58, 60
wybór, 64
replaceTrackListing, 23
zachowania, 64
resetForTesting(), 136
do refaktoryzacji
run(), 146, 337
saveTransaction, 364 długie metody, 297
scan(), 41 osobliwości, 204
secondMomentAbout, 107 do wyszukiwania skutków, 177
setDescription, 163 jarzmo testowania jednostkowego, 66
setSnapRegion, 153 make, 102
setTestingInstance, 137 obiekty pozorowane, 65
setUp, 68 ogólne jarzmo testowe, 71
showLine, 42, 44 nazewnictwo, 356, 362
snap(), 153 metod, 381, 404
someMethod, 402 NetworkBridge, 40
suspend_frame, 340 newFixedYield, 366
tearDown, 68, 373 niepowodzenia testów, 96
testEmpty, 67 niewykrywalne skutki uboczne, 158
uniqueEntries, 78 Node, 359
updateAccountBalance, 368 notatki, 220
updateBalance, 368 nthMomentAbout, 109
validate, 149, 346, 347 N-ty moment statystyczny, 107
void testXXX(), 67 NUnit, 70
WorkflowEngine, 351
write, 275, 278, 279
writeBody, 280, 286
O
writeField, 278 obiekt
miejsca metody, 332
deklaracji, 304 nie korzystający z innych obiektów, 158
na wstawienie testów, 36, 175 objaśniający, 351
zmian, 36 opakowujący, 157
SKOROWIDZ 427

pozorowany, 45, 65, 415 operator


biblioteki, 329 delete, 132
wewnątrz innych obiektów, 145 zasięgu, 50
wprowadzanie zmian w zachowaniu, 403 opowiadanie historii systemu, 226
obiekty, 144 sesja JUnit, 227
CustomSpreadsheet, 59 opóźnienie, 96
Formula, 67 OpportunityItem, 100
globalResultNotifier, 249 OpportunityProcessing, 100
HttpPostedFileWrapper, 157 OptionSource, 344
ResultNotifier, 249 optymalizacja, 24
SchedulingTask, 145 a nowa funkcjonalność, 24
testDigit, 67 zmieniane elementy, 24
obrysowywanie bloków, 222 OriginationPermit, 147, 149
obsługiwanie parametrów, 176 ortogonalność, 290
oddzielenie komendy od zapytania, 161 osłabianie ochrony dostępu, 155
odpowiedzialności, 213, 254 OurHttpFileCollection, 157
delegowanie, 267
dostrzeganie, 257 P
główna, 257, 266
Packet, 346
grupowanie metod, 257
PageGenerator, 196
klasy, 199
PageLayout, 349
metoda prywatna, 258
pakiety, 100
nazwa klasy, 255
refaktoryzacja struktury, 101
nazwy metod, 255
struktura, 101
schematy funkcjonalności, 265
papierowy widok, 399
skuteczne rozdzielanie, 257 ParameterSource, 329
strategia rozbijania klasy, 270 parametry, 176
wydzielona klasa, 264 cebulowy, 144
wyłonienie obiektu metody, 306 zaliasowany, 147
odwołania do klas bibliotecznych, 207 parametryzacja konstruktora, 129, 140, 377
odwracanie rozkładu, 10 czynności, 379
odzwierciedlanie i opakowywanie API, 215, 216 dodanie parametru, 378
odseparowanie od klasy, 157 hermetyzacja, 182
OffMarketTradeValidator, 391 referencji globalnej, 342
ograniczenia w projektach, 208 kod, 129
opakowywanie, 103 kopia konstruktora, 378
opakowywanie klasy, 88 nowy konstruktor, 379
czynności, 92 problemy, 130
dodawanie zachowania, 91 użycie zmiennych globalnych, 133
wzorzec dekoratora, 89 w językach zezwalających na domyślne
zastosowanie, 92 argumenty, 379
opakowywanie metody, 85 wady, 379
czynności, 87 zmienna instancji, 378
dodawanie nowej metody, 86 zorientowanie obiektowe, 249
umieszczanie zmienionej metody w starym parametryzacja metody, 140, 381
kodzie, 86 czynności, 382
wprowadzanie spoin, 87 hermetyzacja referencji globalnej, 342
zalety i wady, 88 użycie zmiennych globalnych, 133
zastosowanie, 92 wada, 140
428 SKOROWIDZ

parseExpression, 191 pokrycie testami, 33, 182


Parser, 191 pomocne funkcje języka, 155
pay(), 86, 89 popagacja skutków, 176
PaydayTransaction, 361 poprawianie błędów, 21, 24
performCommand, 159, 161 a nowa funkcjonalność, 25
PermitRepository, 134, 138, 147 populate, 328
testowa instancja klasy, 136 porządkowanie, 23
pierwszy moment statystyczny, 104 PostReceiveError, 49, 61
pisanie testów, 37, 195 poszukaj decyzji, które można zmienić, 259
charakteryzujących, 196 poukrywane zależności, 81
heurystyka, 205 powielony kod, 275
klasy, 199 generalizowanie zmiennej, 285
dla istniejącego kodu, 193 klasa nadrzędna, 279
dla metody, 199 opracowanie testów po refaktoryzacji, 278
prywatnej, 152 pierwsze kroki, 278
dla rozgałęzienia, 202 po usunięciu duplikacji, 289
efekty, 75 skupione metody, 290
fałszywa klasa, 124 utrata elastyczności, 289
interfejsy użytkownika, 66 wyłaniający się projekt, 290
ogólna liczba, 198 przenoszenie metod, 281, 286
pod presją czasu, 76 refaktoryzacja, 275
podczas opracowywania projektu, 47 rozpoczynanie, 278
podczas wprowadzania zmian, 77 rozpoczęcie od małych kroków, 279
programowanie sterowane testami, 109 zasada otwarte-zamknięte, 291
sprawdzających powtórne wyodrębnianie, 309
funkcjonalność, 158 pozorowany obiekt, 45
klasę, 183 pozostawianie zachowania, 25
testowanie ukierunkowane, 200 praca inicjalizacyjna konstruktorów, 351
w punkcie przechwycenia, 187 praca z informacją zwrotną, 27
w punktach zwężenia, 192 PremiumRegistry, 365
weryfikujących metody, 151 preprocesor, 61, 143, 242
problemy, 151 makr, 51
wyłonienie obiektu metody, 332 przedefiniowanie tekstu, 410
Platforma dla Testów Zintegrowanych, 71 preprocesowanie, 52
platforma testowa proces budowania
CppUnitLite, 68 alokowanie klas w pakietach, 102
Fitnesse, 71 optymalizacja przeciętnego czasu budowy, 102
Framework for Integrated Tests, 71 pakiety, 100
JUnit, 67 przyspieszenie, 100
NUnit, 70 w odniesieniu do kodu, 101
TestKit, 70 rekompilacja, 98
xUnit, 66 struktura pakietu, 101
platformy, 133 średni czas budowy, 98
plik nagłówkowy, 53 usuwanie zależności, 98, 102
podklasy wyodrębnienie
dla dwóch różnych opcji, 112 implementera, 98, 99, 100
testowe, 236, 348, 388, 415 interfejsu, 98
PointRenderer, 335 zmiana fizycznej struktury projektu, 98
pojedyncze instancje, 137, 139
SKOROWIDZ 429

ProductionModelNode, 357 przekazywanie pustej wartości, 126


programowanie ekstremalne, 14 kiełkowanie metody, 80
programowanie różnicowe, 110, 415 parametr cebulowy, 145
kluczowe aspekty projektu, 112 w kodzie produkcyjnym, 127
korzystanie przenoszenie metod, 272, 346
z dziedziczenia, 113 do abstrakcyjnej klasy nadrzędnej, 388
z klas, 115 przepisanie systemu, 225
tworzenie podklas, 112 przesłanianie metod, 117
zalety, 118 wywoływanie, 402
zasada podstawienia Liskov, 116 przesunięcie
zastosowanie, 116 funkcjonalności w górę hierarchii, 386
zbiór własności, 113 czynności, 389
zmiana konstruktora klasy, 113 zachowania, 115
programowanie sterowane testami, 104, 110, 415 zależności w dół hierarchii, 390
algorytm, 104 czynności, 392
dla cudzego kodu, 110 przewidywanie skutków, 166
edytowanie kodu, 312 przywieranie statyczne, 367
kiełkowanie punkt dostępowy, 53, 54
klasy, 82 spoiny konsolidacyjnej, 57
metody, 78 spoiny obiektowe, 59
kod punkt przechwycenia, 184, 416
cudzy, 109 dobieranie, 187
odpowiedzi, 105 ograniczony, 186
proceduralny, 244 śledzenie skutków w przód, 185
kompilowanie testu, 105, 106, 107 wyższego poziomu, 187
powodzenie testu, 105, 106, 108 punkt zmiany, 172, 187, 189, 416
próbowanie, 322 punkt zwężenia, 98, 184, 189, 190, 416
przypadek testowy kończący się ocena projektu, 191
niepowodzeniem, 104, 105, 107 pułapki, 192
usuwanie duplikatów, 105, 106, 108 schemat funkcjonalności, 265
programowanie w parach, 318 testy, 193
projekt w kodzie proceduralnym, 240
przyjazny testowaniu, 47 znajdowanie, 190
w kategorii obiektów, 230 puste karty CRC, 230
przedefiniowanie opis systemu do głosowania, 231
szablonu, 405 wskazówki korzystania, 232
czynności, 407 pusty obiekt, 127
udostępnianie alternatywnych definicji
metod, 407 Q
w C++, 407
tekstu, 409 QuarterlyReportTableHeaderGenerator, 82
czynności, 410 QuaterlyReportGenerator, 81
wady, 409
w locie, 409 R
przeformułowanie kodu, 246
przeglądarka refaktoryzująca kod, 63 rdzenna logika, 214
przekazanie parametru, 374 readToken, 170
Recalculate, 58, 60
430 SKOROWIDZ

refaktoryzacja, 23, 35, 64, 411 RouteFactory, 373


a nowa funkcjonalność, 24 rozgałęzienia
automatyczna, 63 charakteryzowanie, 202
bez testów, 327 rozkład kodu, 10
charakterystyka elementów, 204 rozmieszczanie testów, 36, 60, 151
długie metody, 296 rozległe zależności globalne, 140
duże klasy, 254 skutki zmian, 165
klasy, 144 rozpoznanie, 39
metody, 116 fałszywki, 45
podatność na błędy, 314 parametryzacja metody, 381
powielony kod, 275, 278 spoina konsolidacyjna, 57
przygotowanie pola, 183, 188 warunków w metodzie, 300
ręczna, 63, 204, 300 RSCWorkflow, 347
rozdzielenie interfejsu, 269 RuleParser, 256, 258
sparametryzowanie konstruktora, 130 run(), 146, 337
sprawdzenie zachowania, 204 rysunki, 220
struktury pakietu, 101
szybka, 222 S
techniki usuwania zależności, 327
testy, 201 Sale, 41
wysokopoziomowe, 184 z hierarchią wyświetlaczy, 42
toporna, 164 sanity checks, 64
upraszczanie typu parametru, 35 saveTransaction, 364
w skali mikro, 312 scan(), 41
wsparcie, 63 scan_packets, 241, 242
wspierająca testowanie, 327 Scanner, 249
wydzielanie interfejsu, 35 ScheduledJob, 266
wyodrębnianie metody, 164 Scheduler, 142, 387
zasada pojedynczej odpowiedzialności, 254 tworzenie klasy, 143
zmieniane elementy, 24 Scheduler.h, 143
zmienne rozpoznające, 303 SchedulerDisplay, 143
referencja globalna, 340 SchedulingServices, 388
zastąpienie getterem, 396 SchedulingTask, 145, 146
refleksje, 155 SchedulingTaskPane
reguły, 179 tworzenie instancji klasy, 145
użycia metody, 199 schemat funkcjonalności, 260, 416
rekompilacja, 98 skupiska, 263
klas zależnych od klasy produkcyjnej, 102 zastosowanie, 261, 265
zapobieganie, 100 schemat skutków, 168, 172, 416
Renderer, 333 a schemat funkcjonalności, 261
replaceTrackListing, 23 dla klasy CppClass, 180
report_deposit, 409 dla systemu fakturującego, 188
Reservation, 260 punkt zwężenia, 189
schemat funkcjonalności, 262 rysowanie, 174
resetForTesting(), 136 upraszczanie, 169, 180
ResultNotifier, 248 wspólnie używane elementy, 190
ręczna refaktoryzacja, 300 zastosowanie, 261
RGHConnection, 123 znajdowanie ukrytych klas, 191
metody, 124
SKOROWIDZ 431

sealed, 156, 158 wyciąganie wniosków z analizy, 179


secondMomentAbout, 107 znajomość języka programowania, 177
sekwencje, 307 słowa kluczowe, 176
send_command, 244 Smalltalk, 63
separowanie, 39, 56 snap(), 153
parametryzacja metody, 381 someMethod, 402
spoina konsolidacyjna, 57 spoiny, 48, 49, 251, 416
uproszczenie parametru, 383 konsolidacyjne, 54, 61, 345, 416
SequenceHasGapFor, 384 kod proceduralny, 241
seria testów, 172 środowisko testowe i produkcyjne, 58
SerialTask, 145 obiektowe, 51, 58, 61, 252, 416
serwer listy mailingowej, 214 właściwości, 247
Session, 215 wprowadzenie delegatora instancji, 367
setDescription, 163 preprocesowe, 51, 61, 345
setOption, 343 punkt dostępowy, 53, 57
setSnapRegion, 153 rodzaje, 51
setter, 132 w języku zorientowanym obiektowo, 58
zmieniający bazowe obiekty, 403 w kodzie proceduralnym, 240
setTestingInstance, 135, 137 właściwy wybór, 61
wprowadzanie podczas dodawania
setUp, 68
funkcjonalności, 87
ShippingPricer, 185
zastosowania, 51
showLine, 42, 44
statyczne części klasy, 347
siatka zabezpieczająca, 27
statyczny setter, 370
singleton, 135, 137, 370, 372
StepNotifyController, 90
osłabienie
strategia, 270
wartości, 136
strukturalne ustępstwa dla długich metod,
własności, 135, 138
307
powody używania, 137
String, 167
właściwości wspólne, 370
struktura aplikacji, 225
zastępowanie, 370 analiza rozmowy, 232
składnia UML, 220 architekt, 225
skróty, 289 diagramy, 230
skupienie na bieżącej pracy, 269 historia systemu, 226
skutki zmian, 165 obraz całości, 226
adnotowanie listingów, 222 prosty obraz, 227
hermetyzacja, 182 przeszkody poznania, 225
lista elementów, 167 puste karty CRC, 230
myślenie o skutkach, 166 wzrost złożoności, 229
narzędzia do wyszukiwania, 177 zachowanie nienaruszonej architektury, 226
ograniczanie, 180 znajdowanie nowych abstrakcji, 229
określanie miejsca testów, 175 StyleMaster, 349
po użyciu danych globalnych i statycznych, 176 supersede, 404
propagacja, 176, 177 superświadome edytowanie, 312
punkt przechwycenia, 184 suspend_frame, 340
schemat skutków, 168 SymbolSource, 164
szukanie, 177 system
śledzenie w przód, 171 bazujący na API, 209
upraszczanie schematów, 180 dobrze utrzymywany a system obcy, 95
w języku C++, 178 konserwowany, 95
432 SKOROWIDZ

szablony metody
parametryzacja, 407 prywatnej, 152, 258
przedefiniowanie, 405 publicznej, 152
w C++, 407 statycznej, 346
szkicowanie fragmentów projektu, 221 obiektów, 377
szkieletyzacja metody, 307 obiekty pozorowane, 45
szukanie odwołania do biblioteki graficznej, 56
błędów, 195 podstawianie innej wersji klasy, 55
sekwencji, 307 regresyjne, 28
szwy, 297 tworzenie odrębnej biblioteki dla klasy, 56
szybka refaktoryzacja, 222, 269 ukierunkowane, 200
zagrożenia, 223 wyodrębnianie metody, 303
uruchamiane edycją, 312
Ś uruchamianie metody bez wywoływania
funkcji, 49
śledzenie skutków, 176 wiązanie nazw, 338
śledzenie w przód, 171 wyodrębnianie klas, 48
seria testów, 172 zmiana metody w chronioną, 60
zmienianych metod, 154
T zmienne globalne, 135
TestResult, 381
taktyka, 270 testy, 28
Task, 254 a automatyczna refaktoryzacja, 64
tearDown, 68, 373 automatyczne, 195
TermTokenizer, 258 czas trwania, 96
TEST, 69 dla klas, 33
TestCase, 68 dla metod ukrytych, 152
testDigit, 67 dokumentujące, 198
testEmpty, 67 dołączenie pliku, 243
TESTING, 54, 242 informacje zwrotne, 29
TestingAsyncSslRec, 50 integracyjne, 31
TestingExternalRouter, 372 konstrukcyjne, 122
TestingMessageForwarder, 399 mieszanie z kodem źródłowym, 242
TestKit, 70 modyfikowalnych fragmentów kodu, 247
testowanie, 66 na wyższym poziomie, 34
alternatywne funkcje, 44 pisanie, 37
automatyczne, 195 poczytalności, 64
cudzego kodu, 51 podejrzane, 198
fałszywe obiekty, 41, 43 pokrywające, 184
fałszywki, 44 pokrywanie kodu, 33
jednocześnie kilku zmian, 183 praca inicjalizacyjna konstruktorów, 351
jednostkowe, 29 słonecznego dnia, 204
grupowanie, 31 specyfikujące, 195
testowanie w izolacji, 30 spodziewane wartości, 198
xUnit, 66 umieszczanie, 33
języków .NET, 70 usuwanie zależności, 35
klas, 192 utrzymujące, 195
klasy Scheduler, 142 wykonywane ręcznie, 195
logiki, 245 wykrywające zmianę, 28
SKOROWIDZ 433

wysokopoziomowe, 184 parametru zaliasowanego, 147


wyższego poziomu, 32 parametryzacja konstruktora, 131
zmiany w kodzie, 34 ukryta zależność, 128
testy charakteryzujące, 165, 192, 196, 198, 416 zależności dyrektyw include, 141
grupę klas, 188 UML, 220, 230
konwersja, 204 unikanie zmian, 26
śledzenie w przód, 171 uniqueEntries, 78
testy duże, 192 updateAccountBalance, 368
czas wykonywania, 31 updateBalance, 368
lokalizacja błędów, 30, 31 uproszczenie parametru, 383
pokrycie, 31 czynności, 385
problemy, 30 typu parametru, 35
testy jednostkowe, 30, 416 upublicznienie metody, 152
cechy, 31, 32 statycznej, 315, 332, 346, 347
wolne, 32 czynności, 348
zagrożenia, 192 przekształcanie metody oryginalnej
thing, 354 na statyczną, 347
ToolController, 90 dostęp do kodu, 151
TransactionLog, 361 uruchamianie metody w jarzmie testowym
TransactionRecorder, 362, 363 adaptacja parametru, 156
tworzenie niewykrywalne skutki uboczne, 158
indeksu, 172 osłabianie ochrony dostępu, 155
instancji klasy pomocne funkcje języka, 155
C++, 144 problemy, 151
w jarzmie testowym, 121 ukryta metoda, 152
obiektów, 144 upublicznanie metody, 152
w konstruktorach, 351, 401 using, 141, 154
zmiennych globalnych, 135 usuwanie
tworzenie podklasy i przesłanianie metody, 128, 398 duplikatów, 108, 109, 291
automatyczna refaktoryzacja, 299 generalizowanie zmiennej, 285
czynności, 400 zasada otwarte-zamknięte, 291
fałszywy singleton, 139 nieużywanego kodu, 223
niewykrywalne skutki uboczne, 162 usuwanie zależności, 35, 37, 39, 51
ostrożność, 399 cel, 330
papierowy widok, 399 czas wprowadzenia zmiany, 97
parametr zaliasowany, 149 efekty, 75
stosowanie, 390 lokalnych, 349
typedef, 406, 408 na potrzeby testów, 315
od elementów globalnych, 340
U od typów, 353
opakowywanie parametru, 329
udostępnianie setterów, 403 osłabianie ochrony dostępu, 155
ukryta metoda, 152, 258 patrzenie naprzód, 329
ukryta zależność, 128 preprocesor, 242
ulepszanie projektu, 23 programowanie w parach, 318
umieszczanie klasy w jarzmie testowym, 121 rozpoznanie, 39
irytująca zależność globalna, 133 separowanie, 39
irytujący parametr, 121
parametr cebulowy, 144
434 SKOROWIDZ

usuwanie zależności uzupełnianie definicji, 338


techniki, 325 czynności, 339
adaptacja parametru, 328 osobny plik wykonywalny, 339
hermetyzacja referencji globalnej, 340 zestawy definicji, 339
parametryzacja konstruktora, 377 uzyskanie źródłowego pozwolenia, 147
parametryzacja metody, 381
przedefiniowanie szablonu, 405 V
przedefiniowanie tekstu, 409
przesunięcie funkcjonalności w górę validate, 149, 346, 347
hierarchii, 386 ValueCell, 58
przesunięcie zależności w dół hierarchii, void testXXX(), 67
390
uproszczenie parametru, 383 W
upublicznienie metody statcznej, 346
utworzenie podklasy i przesłonięcie wariacje w systemach, 118
metody, 398 wartości zwrotne, 177
uzupełnianie definicji, 338 wewnętrzne relacje, 259
wprowadzenie delegatora instancji, 367 wiązanie nazw, 338
wprowadzenie statycznego settera, 370 widok papierowy, 399
wyłonienie obiektu metody, 332 wiele zmian w jedym miejscu, 183
wyodrębnienie i przesłonięcie gettera, 353 punkty przechwycenia, 184
wyodrębnienie i przesłonięcie metody wyższego poziomu, 187
wytwórczej, 351 punkty zwężenia, 191
wyodrębnienie i przesłonięcie pułapki, 192
wywołania, 349 wielokrotne użycie, 133
wyodrębnienie implementera, 356 kodu, 207
wyodrębnienie interfejsu, 328, 361 WindowsOffMarketTradeValidator, 391
zastąpienie funkcji wskaźnikiem do wirtualna funkcja, 61
funkcji, 393 własności, 115
zastąpienie referencji globalnej getterem, wnioski z analizy skutków, 179
396 WorkflowEngine, 351
zastąpienie zmiennej instancji, 401 wprowadzenie
zastępowanie biblioteki, 375 delegatora instancji, 367
tworzenie interfejsu, 146 czynności, 368
tworzenie podklasy i przesłanianie metody, 149 statycznego settera, 136, 140, 370
w C++, 407 czynności, 374
w językach proceduralnych, 393 globalna wytwórnia, 372
w kodzie proceduralnym, 239 hermetyzacja referencji globalnej, 342
wiele zmian w jednym miejscu, 183 wyodrębnienie interfejsu, 372
wprowadzanie więcej interfejsów i klas, 102 write, 275, 278, 279
zachowywanie sygnatur, 315 writeBody, 280, 286
znajdowanie klas, 55 writeField, 278
związanych z parametrami, 328 przeniesienie metody, 283
utrzymanie zachowania, 25 wskaźniki do funkcji, 246, 393
ryzyko, 25 deklaracje, 393
utworzenie usuwanie zależności, 393
abstrakcji, 385 wsparcie kompilatora, 156, 317
podklasy i przesłanianie metody, 138 pomocne funkcje języka, 157
przenoszenie metod, 272, 334
SKOROWIDZ 435

wykonywane kroki, 317 wyodrębnianie implementera, 356


wyodrębnianie metod, 365, 412 czynności, 358
zastępowanie referencji, 406 klasy w hierarchii dziedziczenia, 359
zastosowanie, 318 opakowywanie klasy, 89
zastrzeżenia stosowania, 272 parametr
zmiana referencji w aplikacji, 139 cebulowy, 145
wsparcie zintegrowanego środowiska zaliasowany, 147
programistycznego w analizie skutków, 166 przekazywanie instancji klasy do obiektu, 132
wstępna refaktoryzacja, 314 w klasie
wybór metody do testowania, 165 ConsultantSchedulerDB, 99
myślenie o skutkach, 166 OpportunityItem, 100
narzędzia do wyszukiwania skutków, 177 wyodrębnienie interfejsu, 140
propagacja skutków, 176 zależności podczas budowania, 98, 99, 100, 102
śledzenie w przód, 171 wyodrębnianie interfejsu, 98, 129, 328, 361
upraszczanie schematów skutków, 180 automatyczne wsparcie refaktoryzacji, 361
wnioski z analizy skutków, 179 czynności, 365
wydorębnianie metody i funkcji niewirtualnych, 365
zestaw testów weryfikujących, 411 Java, 145
wydzielenie klasy w hierarchii dziedziczenia, 360
interfejsu, 35 nadawanie nazw interfejsom, 362
klas, 327 opakowywanie klasy, 89
wyłonienie obiektu metody, 306, 332 osłabienie ochrony konstruktora, 372
czynności, 337 parametr
dostęp do kodu, 151 cebulowy, 145
odmiany, 336 zaliasowany, 147, 148
publiczny konstruktor, 333 przekazywanie instancji klasy do obiektu, 132
schemat, 336 stopniowe wyodrębnianie, 361
zachowywanie sygnatur, 316 tworzenie
zmienne instancji, 333 fałszywego obiektu, 124
wyodrębniaj to, co znasz, 304 instancji klasy, 334
wyodrębnianie bazujące na wycięcie metod, 361
odpowiedzialnościach, 215, 216 względem singletona, 139
wyodrębnianie i przesłanianie gettera, 131, 351, 353 zależności podczas budowania, 102
czas życia gettera, 355 wyodrębnianie klas
czynności, 355 a dziedziczenie, 272
leniwy getter, 354 bez przeprowadzania testów, 271, 272
menedżer transakcji, 353 duże klasy, 271
wady, 355 refaktoryzacja, 259
wyodrębnianie i przesłanianie metody wyodrębnianie kodu
fabrycznej, 131 cele, 297
ukryte zależności konstruktora, 131 wyodrębnianie metod, 88, 114, 222, 411
wytwórczej, 351 automatyczne narzędzia, 299
czynności, 352 błędy, 315
możliwości zastosowania, 351 konwersji typu, 304
wyodrębnianie i przesłanianie wywołania, 349 czynności, 411
czynności, 350 do bieżącej klasy, 308
kod po wyodrębnieniu, 349 długie metody, 296
użycie zmiennych globalnych, 134 gromadzenie zależności, 305
wyodrębnianie metody, 350 liczba powiązań, 304
436 SKOROWIDZ

wyodrębnianie metod usuwanie


małe fragmenty kodu, 304, 309 biblioteka ze szczątkową funkcją, 61
możliwe błędy, 300 zastąpienie w miejscu spoiny, 51
powtórne wyodrębnianie, 309 zmiana, 22
proste, 297 zachowanie sygnatur, 87, 130, 215, 248, 271,
przykład w Javie, 412 314, 346
rozdzielanie zadań, 159 adaptacja parametru, 330
typ węzła, 301 wyłonienie obiektu metody, 333
wprowadzenie zmiennej rozpoznającej, 300 zastosowanie, 316
wyłonienie obiektu metody, 306 zagmatwana logika, 199
zachowanie sygnatur, 271 zależności, 34
zestaw przypadków testowych, 300 biblioteczne, 207
zmienne instancji, 301 dyrektyw include, 141
wyodrębnianie odpowiedzialności, 221 fałszywe obiekty, 41
wyodrębnianie różnic między metodami, 281 globalne, 133
wysunięcia, 295 rozdzielanie odpowiedzialności
wzorzec w aplikacji, 141
dekoratora, 89, 91 zmiana na pole w obiekcie, 140
projektowy singleton, 134, 135, 370 zmiana na zmienną tymczasową, 140
konstruktor klasy singletona, 137 kodu od interfejsu, 101
leniwy getter, 354 między klasami, 39
prywatny konstruktor, 372 nagłówkowe, 142
pustego obiektu, 127, 330 oddzielanie od innych części klasy, 390
pisanie testu, 37
X podczas budowania, 98
poukrywane, 81
xUnit, 66 separowanie, 56
cechy, 66 tworzeniowe, 81
inne platformy, 70 usuwanie, 39
prostota i ukieruknowanie, 66 zasada odwrócenia, 101
zapytania, 161
Z zasada
hermetyzacji, 125
zachowanie, 22 odwrócenia zależności, 101
charakterystyka, 196 otwarte-zamknięte, 291
dodawanie, 22 podstawienia Liskov, 116
do istniejących metod, 85 pojedynczej odpowiedzialności, 115, 254, 266
metody, 23 na poziomie implementacji, 270
w kodzie proceduralnym, 244 naruszenie, 266
narzędzia refaktoryzujące, 64 rozdzielania interfejsów, 267, 268
pozostawienie, 25, 196 zasoby, 137
przesuwanie do klasy, 115 zastępowanie, 272
systemu, 199 biblioteki, 375
testowanie pozostawienia, 113 a hermetyzacja referencji globalnej, 343
testy czynności, 376
charakteryzujące, 198 dyspozytora, 371
słonecznego dnia, 204 funkcji wskaźnikiem do funkcji, 393
czynności, 395
zalety, 395
SKOROWIDZ 437

obiektu, 377, 381 utrzymanie zachowania, 25


referencji globalnej getterem, 396 wiele zmian w jedym miejscu, 183
czynności, 397 wielokrotne odwołania do czyjejś biblioteki, 209
zachowania, 403 wybór metody do testowania, 165
zmiennej instancji, 131, 351, 401 wywołania API, 209
czynności, 404 zależności biblioteczne, 207
konstrukcyjne kłębowisko, 132 zrozumienie kodu, 219
setter, 132 zmiany w systemie, 27, 76
w Javie, 132 dobrze utrzymanym, 95
zbiór własności, 113 edytuj i módl się, 27
zbrylenie, 259 kryj i modyfikuj, 27
zezwolenia, 155 należyta staranność, 27
zmiany siatka zabezpieczająca, 27
architektury, 225 testowanie regresyjne, 28
funkcjonalne, 312 zmienne
nazwy klasy, 116 instancji, 306, 332
wymagań, 9 wprowadzenie gettera, 353
strukturalne, 318 zastępowanie, 401
typów, 318 globalne, 135, 396
w klasie, 77, 99, 383 singletony, 137
w kodzie bez poddawania istniejących klas szukanie, 141
testom, 77 testowe, 300
w metodach, 152, 328 tymczasowe, 78
edytowanie kodu, 314 rozpoznające, 300
zmiany w oprogramowaniu, 21, 73 charakteryzowanie klas, 199
bezpieczne zmiany, 239 konwersja automatyczna, 204
czas, 95 sesja refaktoryzacji, 303
długie metody, 293 testy dla rozgałęzienia, 202
dodawanie funkcji, 21 wyodrębniaj to, co znasz, 305
instancja klasy w jarzmie testowym, 121 zorientowanie obiektowe, 230, 247, 250
irytujący parametr, 121 hermetyzacja referencji globalnej, 247
kiełkowanie programy proceduralne, 250
klasy, 80 spoiny obiektowe, 247
metody, 77 zastępowanie biblioteki, 375
opakowywanie zrozumienie, 95
klasy, 88 kodu, 219
metody, 85 adnotowanie listingów, 221
optymalizacja, 24 notatki i rysunki, 220
pisanie testów, 195 pozyskiwanie wiedzy, 219
poprawianie błędów, 21 szybka refaktoryzacja, 222
powielony kod, 275 usuwanie nieużywanego kodu, 223
powody wprowadzania, 21 skutków zmiany, 222
problemy z uruchamianiem metody struktury metody, 221
w jarzmie testowym, 151
ryzyko podczas edycji kodu, 311 Ź
skutki zmian, 165
ukryta zależność, 128 źródłowe pozwolenie, 147
ulepszanie projektu, 23

You might also like