Professional Documents
Culture Documents
Feathers M Praca Z Zastanym Kodem Najlepsze Techniki Compress
Feathers M Praca Z Zastanym Kodem Najlepsze Techniki Compress
Feathers M Praca Z Zastanym Kodem Najlepsze Techniki Compress
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
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
Michael Feathers
mfeathers@objectmentor.com
www.objectmentor.com
WSTĘP 17
Wstęp
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.
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.
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ęć.
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ę
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
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
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ą.
ż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Ą
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Ą
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.
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ą.
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.
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?
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Ą
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.
„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.
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
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ą.
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
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);
}
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 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();
}
}
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.
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);
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
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());
}
#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
...
} else {
db_update(account_no, record->backup_item);
}
}
db_update(MASTER_ACCOUNT, record->item);
}
#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
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.
// 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)
{
}
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;
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
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):
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 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;
}
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.
refaktoryzacja. Oto definicja Martina Fowlera z książki Refactoring: Improving the Design of
Existing Code (Addison-Wesley 1999):
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.
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
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.*;
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;
employee.addTimeCard(new TimeCard(cardDate,40));
}
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>
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
End Class
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.
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.
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.
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;
}
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
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Ć
...
}
}
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);
}
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
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ć.
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);
}
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
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ŚĆ
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.
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;
}
...
}
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.
Skompiluj go
W tym celu musimy zmienić deklarację metody firstMomentAbout, żeby zgłaszała wyjątek
InvalidBasisException.
public double firstMomentAbout(double point)
throws InvalidBasisException {
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");
Usuń duplikaty
W tym przypadku nie ma żadnych duplikatów.
PROGRAMOWANIE STEROWANE TESTAMI 107
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;
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ŚĆ?
if (elements.size() == 0)
throw new InvalidBasisException("brak elementów");
if (elements.size() == 0)
throw new InvalidBasisException("brak elementów");
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
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 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
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());
}
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.
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());
}
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).
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.
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.
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ę.
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.
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.
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.
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
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.
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
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);
}
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");
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
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
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
mailing_list_dispatcher::mailing_list_dispatcher(mail_service *service)
{
initialize(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())
{}
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);
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).
Permit associatedPermit =
PermitRepository.getInstance().findAssociatedPermit(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);
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() {}
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
„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
...
// sprawdź, czy mamy tylko jedno pasujące pozwolenie; jeśli nie, zgłoś błąd
...
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() {}
{
...
}
...
}
protected PermitRepository() {}
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”.
#include "Meeting.h"
#include "MailDaemon.h"
...
#include "SchedulerDisplay.h"
#include "DayTime.h"
class Scheduler
{
public:
Scheduler(const string& owner);
~Scheduler();
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"
TEST(create,Scheduler)
144 ROZDZIAŁ 9. NIE MOGĘ UMIEŚCIĆ TEJ KLASY W JARZMIE TESTOWYM
{
Scheduler scheduler("fred");
}
TEST(create,Scheduler)
{
Scheduler scheduler("fred");
}
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)
{
...
}
}
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;
...
};
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
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
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();
}
};
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.
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
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);
}
};
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.
}
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
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
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(...) { .. }
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.
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
String getAccountSymbol() {
return detailDisplay.getAccountSymbol();
}
String getAccountSymbol() {
return accountSymbol;
}
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;
}
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ść.
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.
Możemy połączyć wszystkie te szkice w jeden duży schemat (patrz rysunek 11.4).
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.
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();
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).
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
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).
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.
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 = "";
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
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.
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 = "";
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ć.
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ść.
}
...
}
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;
}
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.
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.
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
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
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.
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ć.
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
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.
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ą.
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ć.
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.
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
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.
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.
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ć.
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
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);
}
...
}
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);
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);
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ą.
...
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);
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.
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.
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.
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.*;
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 ();
}
}
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
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
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).
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
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.
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”).
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.
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.
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
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
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ę).
„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
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
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
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;
}
}
Przypadek trudny
Oto funkcja w C, którą chcemy zmienić. Dobrze byłoby poddać ją testom, zanim wpro-
wadzimy nasze zmiany:
include "ksrlib.h"
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)
{
}
#ifdef TESTING
#define ksr_notify(code,packet)
#endif
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"
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
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.
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));
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 = ...
}
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;
}
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);
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;
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;
}
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);
};
public:
Scanner();
Scanner(ResultNotifier& notifier);
// w pliku źródłowym
Scanner::Scanner()
: notifier(globalResultNotifier)
{}
Scanner::Scanner(ResultNotifier& notifier)
: notifier(notifier)
{}
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);
{
...
}
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ć.
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
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.
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
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.
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.
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();
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;
}
Pierwszym krokiem jest zakreślenie okręgów dookoła każdej ze zmiennych, jak poka-
zano na rysunku 20.4.
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.6 pokazuje schemat po dodaniu okręgów dla każdej metody oraz
strzałek dla wszystkich zmiennych, z których metody te korzystają.
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)?
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).
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
...
public void addFee(FeeRider fee) {
calculator.addFee(fee);
}
public getTotalFee() {
int baseFee = getPrincipalFee();
return calculator.getTotalFee(baseFee);
}
}
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.
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.
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.
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
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
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;
import java.io.OutputStream;
= {(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;
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
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);
}
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.
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);
}
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);
}
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;
...
}
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
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();
...
}
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;
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
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.
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;
}
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;
}
+ CMD_BYTE_LENGTH + footer.length
+ getBodySize();
}
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
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);
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 };
}
wyglądała tak:
public void write(OutputStream outputStream)
throws Exception {
writeHeader(outputStream);
writeBody(outputstream);
writeFooter(outputStream);
}
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
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.
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.
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.
...
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();
}
...
}
if (singleBrokerCommodity(commodity)) {
displayCommodity(current.getView());
}
}
}
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.
paraList.add(node);
nodeAdded = true;
}
...
}
...
}
...
}
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);
}
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
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();
}
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);
...
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ń.
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.
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);
}
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Ę?
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.
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
Rozdział 25.
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);
}
...
}
}
return value;
}
}
Z kolei produkcyjne źródło parametru wygląda następująco:
class ServletParameterSource implements ParameterSource
{
private HttpServletRequest request;
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 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
private:
void drawPoint(int x, int y, COLOR color);
...
};
}
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);
...
};
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]);
}
...
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 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
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.
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 ();
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
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
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
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.
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.
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;
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.
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.
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
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
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.
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
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
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
Spójrzmy na przykład:
{
public WorkflowEngine () {
Reader reader
= new ModelReader(
AppConfig.getDryConfiguration());
Persister persister
= new XMLStore(
AppConfiguration.getDryConfiguration());
Persister persister
= new XMLStore(
AppConfiguration.getDryConfiguration());
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
// WorkflowEngine.cpp
WorkflowEngine::WorkflowEngine()
{
Reader *reader
= new ModelReader(
AppConfig.getDryConfiguration());
Persister *persister
= new XMLStore(
AppConfiguration.getDryConfiguration());
A oto co uzyskaliśmy:
// WorkflowEngine.h
class WorkflowEngine
{
private:
TransactionManager *tm;
protected:
354 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI
public:
WorkflowEngine ();
...
}
// WorkflowEngine.cpp
WorkflowEngine::WorkflowEngine()
:tm (0)
{
...
}
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
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
// ModelNode.cpp
ModelNode::~ModelNode()
{}
358 ROZDZIAŁ 25. TECHNIKI USUWANIA ZALEŻNOŚCI
double m_weight;
void createSpanningLinks();
public:
void addExteriorNode(ModelNode *newNode);
void addInternalNode(ModelNode *newNode);
void colorize();
...
};
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
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.
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.
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
{
...
}
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.
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.
i ją zaimplementujemy:
class BondRegistry : public BondProvider { … };
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
...
}
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
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
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ą.
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();
}
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;
...
}
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;
}
...
}
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);
}
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
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.
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
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();
}
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
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);
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();
return SequenceHasGapFor(baseRepresentation,
patternRepresentation);
}
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
}
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() {
}
import junit.framework.*;
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
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)
{}
flag = true;
}
showMessage();
return flag;
}
...
};
public:
OffMarketTradeValidator(Trade& trade)
: trade(trade), flag(false) {}
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;
}
}
...
};
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
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);
void initializeEnvironment() {
db_store = db_store_production;
...
}
ZASTĄPIENIE FUNKCJI WSKAŹNIKIEM DO FUNKCJI 395
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
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);
}
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();
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
return forward;
}
...
}
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
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
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
class B : public A
{
C *c;
public:
B() {
c = new C;
}
setParamByName("cm", "blend");
}
m_param = newParameter;
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
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.
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
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
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
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
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
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;
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;
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
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;
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.
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
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
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 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