You are on page 1of 422

Tytu oryginau: Clean Code: A Handbook of Agile Software Craftsmanship

Tumaczenie: Pawe Gonera


Projekt okadki: Mateusz Obarek, Maciej Pokoski
ISBN: 978-83-283-1401-6
Authorized translation from the English language edition, entitled: Clean Code:
A Handbook of Agile Software Craftsmanship, First Edition, ISBN 0132350882,
by Robert C. Martin, published by Pearson Education, Inc., publishing as Prentice Hall.
Copyright 2009 by Pearson Education, Inc.
Polish language edition published by Helion S.A.
Copyright 2014.
All rights reserved. No part of this book may be reproduced or transmitted in any form
or by any means, electronic or mechanical, including photocopying, recording or by any
information storage retrieval system, without permission from Pearson Education Inc.
Wszelkie prawa zastrzeone. Nieautoryzowane rozpowszechnianie caoci
lub fragmentu niniejszej publikacji w jakiejkolwiek postaci jest zabronione.
Wykonywanie kopii metod kserograficzn, fotograficzn, a take kopiowanie
ksiki na noniku filmowym, magnetycznym lub innym powoduje naruszenie
praw autorskich niniejszej publikacji.
Wszystkie znaki wystpujce w tekcie s zastrzeonymi znakami firmowymi
bd towarowymi ich wacicieli.
Autor oraz Wydawnictwo HELION dooyli wszelkich stara, by zawarte
w tej ksice informacje byy kompletne i rzetelne. Nie bior jednak adnej
odpowiedzialnoci ani za ich wykorzystanie, ani za zwizane z tym ewentualne
naruszenie praw patentowych lub autorskich. Autor oraz Wydawnictwo HELION
nie ponosz rwnie adnej odpowiedzialnoci za ewentualne szkody wynike
z wykorzystania informacji zawartych w ksice.
Materiay graficzne na okadce zostay wykorzystane za zgod iStockPhoto Inc.
Wydawnictwo HELION
ul. Kociuszki 1c, 44-100 GLIWICE
tel. 32 231 22 19, 32 230 98 63
e-mail: helion@helion.pl
WWW: http://helion.pl (ksigarnia internetowa, katalog ksiek)
Drogi Czytelniku!
Jeeli chcesz oceni t ksik, zajrzyj pod adres
http://helion.pl/user/opinie/czykov_ebook
Moesz tam wpisa swoje uwagi, spostrzeenia, recenzj.
Pliki z przykadami omawianymi w ksice mona znale pod adresem:
ftp://ftp.helion.pl/przyklady/czykov.zip

Pole ksik na Facebook.com


Kup w wersji papierowej
Oce ksik

Ksigarnia internetowa
Lubi to! Nasza spoeczno

SPIS TRECI

Sowo wstpne

13

Wstp

19

1. Czysty kod

23

Niech stanie si kod...


W poszukiwaniu doskonaego kodu...
Cakowity koszt baaganu
Rozpoczcie wielkiej zmiany projektu
Postawa
Najwiksza zagadka
Sztuka czystego kodu?
Co to jest czysty kod?
Szkoy mylenia
Jestemy autorami
Zasada skautw
Poprzednik i zasady
Zakoczenie
Bibliografia

2. Znaczce nazwy
Wstp
Uywaj nazw przedstawiajcych intencje
Unikanie dezinformacji
Tworzenie wyranych rnic
Tworzenie nazw, ktre mona wymwi
Korzystanie z nazw atwych do wyszukania
Unikanie kodowania
Notacja wgierska
Przedrostki skadnikw
Interfejsy i implementacje
Unikanie odwzorowania mentalnego
Nazwy klas
Nazwy metod
Nie bd dowcipny
Wybieraj jedno sowo na pojcie
Nie twrz kalamburw!
Korzystanie z nazw dziedziny rozwizania
Korzystanie z nazw dziedziny problemu
Dodanie znaczcego kontekstu
Nie naley dodawa nadmiarowego kontekstu
Sowo kocowe

24
24
25
26
27
28
28
28
34
35
36
36
36
37

39
39
40
41
42
43
44
45
45
46
46
47
47
47
48
48
49
49
49
50
51
52
5

3. Funkcje
Mae funkcje!
Bloki i wcicia
Wykonuj jedn czynno
Sekcje wewntrz funkcji
Jeden poziom abstrakcji w funkcji
Czytanie kodu od gry do dou zasada zstpujca
Instrukcje switch
Korzystanie z nazw opisowych
Argumenty funkcji
Czsto stosowane funkcje jednoargumentowe
Argumenty znacznikowe
Funkcje dwuargumentowe
Funkcje trzyargumentowe
Argumenty obiektowe
Listy argumentw
Czasowniki i sowa kluczowe
Unikanie efektw ubocznych
Argumenty wyjciowe
Rozdzielanie polece i zapyta
Stosowanie wyjtkw zamiast zwracania kodw bdw
Wyodrbnienie blokw try-catch
Obsuga bdw jest jedn operacj
Przyciganie zalenoci w Error.java
Nie powtarzaj si
Programowanie strukturalne
Jak pisa takie funkcje?
Zakoczenie
SetupTeardownIncluder
Bibliografia

4. Komentarze
Komentarze nie s szmink dla zego kodu
Czytelny kod nie wymaga komentarzy
Dobre komentarze
Komentarze prawne
Komentarze informacyjne
Wyjanianie zamierze
Wyjanianie
Ostrzeenia o konsekwencjach
Komentarze TODO
Wzmocnienie
Komentarze Javadoc w publicznym API
Ze komentarze
Bekot
Powtarzajce si komentarze
Mylce komentarze
Komentarze wymagane
Komentarze dziennika

SPIS TRECI

53
56
57
57
58
58
58
59
61
62
62
63
63
64
64
65
65
65
66
67
67
68
69
69
69
70
70
71
71
73

75
77
77
77
77
78
78
79
80
80
81
81
81
81
82
84
85
85

Komentarze wprowadzajce szum informacyjny


Przeraajcy szum
Nie uywaj komentarzy, jeeli mona uy funkcji lub zmiennej
Znaczniki pozycji
Komentarze w klamrach zamykajcych
Atrybuty i dopiski
Zakomentowany kod
Komentarze HTML
Informacje nielokalne
Nadmiar informacji
Nieoczywiste poczenia
Nagwki funkcji
Komentarze Javadoc w niepublicznym kodzie
Przykad
Bibliografia

86
87
88
88
88
89
89
90
91
91
91
92
92
92
95

5. Formatowanie

97

Przeznaczenie formatowania
Formatowanie pionowe
Metafora gazety
Pionowe odstpy pomidzy segmentami kodu
Gsto pionowa
Odlego pionowa
Uporzdkowanie pionowe
Formatowanie poziome
Poziome odstpy i gsto
Rozmieszczenie poziome
Wcicia
Puste zakresy
Zasady zespoowe
Zasady formatowania wujka Boba

98
98
99
99
101
101
105
106
106
107
109
110
110
111

6. Obiekty i struktury danych

113

Abstrakcja danych
Antysymetria danych i obiektw
Prawo Demeter
Wraki pocigw
Hybrydy
Ukrywanie struktury
Obiekty transferu danych
Active Record
Zakoczenie
Bibliografia

113
115
117
118
118
119
119
120
121
121

7. Obsuga bdw

123

Uycie wyjtkw zamiast kodw powrotu


Rozpoczynanie od pisania instrukcji try-catch-finally
Uycie niekontrolowanych wyjtkw
Dostarczanie kontekstu za pomoc wyjtkw
Definiowanie klas wyjtkw w zalenoci od potrzeb wywoujcego

124
125
126
127
127

SPIS TRECI

Definiowanie normalnego przepywu


Nie zwracamy null
Nie przekazujemy null
Zakoczenie
Bibliografia

8. Granice
Zastosowanie kodu innych firm
Przegldanie i zapoznawanie si z granicami
Korzystanie z pakietu log4j
Zalety testw uczcych
Korzystanie z nieistniejcego kodu
Czyste granice
Bibliografia

9. Testy jednostkowe
Trzy prawa TDD
Zachowanie czystoci testw
Testy zwikszaj moliwoci
Czyste testy
Jzyki testowania specyficzne dla domeny
Podwjny standard
Jedna asercja na test
Jedna koncepcja na test
F.I.R.S.T.
Zakoczenie
Bibliografia

10. Klasy
Organizacja klas
Hermetyzacja
Klasy powinny by mae!
Zasada pojedynczej odpowiedzialnoci
Spjno
Utrzymywanie spjnoci powoduje powstanie wielu maych klas
Organizowanie zmian
Izolowanie moduw kodu przed zmianami
Bibliografia

11. Systemy
Jak budowaby miasto?
Oddzielenie konstruowania systemu od jego uywania
Wydzielenie moduu main
Fabryki
Wstrzykiwanie zalenoci
Skalowanie w gr
Separowanie (rozcicie) problemw
Poredniki Java

SPIS TRECI

129
130
131
132
132

133
134
136
136
138
138
139
140

141
142
143
144
144
147
147
149
150
151
152
152

153
153
154
154
156
158
158
164
166
167

169
170
170
171
172
172
173
176
177

Czyste biblioteki Java AOP


Aspekty w AspectJ
Testowanie architektury systemu
Optymalizacja podejmowania decyzji
Korzystaj ze standardw, gdy wnosz realn warto
Systemy wymagaj jzykw dziedzinowych
Zakoczenie
Bibliografia

178
181
182
183
183
184
184
185

12. Powstawanie projektu

187

Uzyskiwanie czystoci projektu przez jego rozwijanie


Zasada numer 1 prostego projektu system przechodzi wszystkie testy
Zasady numer 2 4 prostego projektu przebudowa
Brak powtrze
Wyrazisto kodu
Minimalne klasy i metody
Zakoczenie
Bibliografia

187
188
188
189
191
192
192
192

13. Wspbieno

193

W jakim celu stosowa wspbieno?


Mity i nieporozumienia
Wyzwania
Zasady obrony wspbienoci
Zasada pojedynczej odpowiedzialnoci
Wniosek ograniczenie zakresu danych
Wniosek korzystanie z kopii danych
Wniosek wtki powinny by na tyle niezalene, na ile to tylko moliwe
Poznaj uywan bibliotek
Kolekcje bezpieczne dla wtkw
Poznaj modele wykonania
Producent-konsument
Czytelnik-pisarz
Ucztujcy filozofowie
Uwaga na zalenoci pomidzy synchronizowanymi metodami
Tworzenie maych sekcji synchronizowanych
Pisanie prawidowego kodu wyczajcego jest trudne
Testowanie kodu wtkw
Traktujemy przypadkowe awarie jako potencjalne problemy z wielowtkowoci
Na pocztku uruchamiamy kod niekorzystajcy z wtkw
Nasz kod wtkw powinien da si wcza
Nasz kod wtkw powinien da si dostraja
Uruchamiamy wicej wtkw, ni mamy do dyspozycji procesorw
Uruchamiamy testy na rnych platformach
Uzbrajamy nasz kod w elementy prbujce wywoa awarie i wymuszajce awarie
Instrumentacja rczna
Instrumentacja automatyczna
Zakoczenie
Bibliografia

194
195
196
196
197
197
197
198
198
198
199
199
200
200
201
201
202
202
203
203
203
204
204
204
205
205
206
207
208

SPIS TRECI

14. Udane oczyszczanie kodu

209

Implementacja klasy Args


Args zgrubny szkic
Argumenty typu String
Zakoczenie

210
216
228
261

15. Struktura biblioteki JUnit


Biblioteka JUnit
Zakoczenie

16. Przebudowa klasy SerialDate

263
264
276

277

Na pocztek uruchamiamy
Teraz poprawiamy
Zakoczenie
Bibliografia

278
280
293
294

17. Zapachy kodu i heurystyki

295

Komentarze
C1. Niewaciwe informacje
C2. Przestarzae komentarze
C3. Nadmiarowe komentarze
C4. le napisane komentarze
C5. Zakomentowany kod
rodowisko
E1. Budowanie wymaga wicej ni jednego kroku
E2. Testy wymagaj wicej ni jednego kroku
Funkcje
F1. Nadmiar argumentw
F2. Argumenty wyjciowe
F3. Argumenty znacznikowe
F4. Martwe funkcje
Oglne
G1. Wiele jzykw w jednym pliku rdowym
G2. Oczywiste dziaanie jest nieimplementowane
G3. Niewaciwe dziaanie w warunkach granicznych
G4. Zdjte zabezpieczenia
G5. Powtrzenia
G6. Kod na nieodpowiednim poziomie abstrakcji
G7. Klasy bazowe zalene od swoich klas pochodnych
G8. Za duo informacji
G9. Martwy kod
G10. Separacja pionowa
G11. Niespjno
G12. Zaciemnianie
G13. Sztuczne sprzenia
G14. Zazdro o funkcje
G15. Argumenty wybierajce
G16. Zaciemnianie intencji
G17. le rozmieszczona odpowiedzialno

10

SPIS TRECI

296
296
296
296
297
297
297
297
297
298
298
298
298
298
298
298
299
299
299
300
300
301
302
302
303
303
303
303
304
305
305
306

G18. Niewaciwe metody statyczne


G19. Uycie opisowych zmiennych
G20. Nazwy funkcji powinny informowa o tym, co realizuj
G21. Zrozumienie algorytmu
G22. Zamiana zalenoci logicznych na fizyczne
G23. Zastosowanie polimorfizmu zamiast instrukcji if-else lub switch-case
G24. Wykorzystanie standardowych konwencji
G25. Zamiana magicznych liczb na stae nazwane
G26. Precyzja
G27. Struktura przed konwencj
G28. Hermetyzacja warunkw
G29. Unikanie warunkw negatywnych
G30. Funkcje powinny wykonywa jedn operacj
G31. Ukryte sprzenia czasowe
G32. Unikanie dowolnych dziaa
G33. Hermetyzacja warunkw granicznych
G34. Funkcje powinny zagbia si na jeden poziom abstrakcji
G35. Przechowywanie danych konfigurowalnych na wysokim poziomie
G36. Unikanie nawigacji przechodnich

306
307
307
308
308
309
310
310
311
312
312
312
312
313
314
314
315
316
317
317
317
318
319
320
320
321
322
322
323
323
323
324
324
324
324
324
324
324
324
325
325
325
325

Java
J1. Unikanie dugich list importu przez uycie znakw wieloznacznych
J2. Nie dziedziczymy staych
J3. Stae kontra typy wyliczeniowe
Nazwy
N1. Wybr opisowych nazw
N2. Wybr nazw na odpowiednich poziomach abstrakcji
N3. Korzystanie ze standardowej nomenklatury tam, gdzie jest to moliwe
N4. Jednoznaczne nazwy
N5. Uycie dugich nazw dla dugich zakresw
N6. Unikanie kodowania
N7. Nazwy powinny opisywa efekty uboczne
Testy
T1. Niewystarczajce testy
T2. Uycie narzdzi kontroli pokrycia
T3. Nie pomijaj prostych testw
T4. Ignorowany test jest wskazaniem niejednoznacznoci
T5. Warunki graniczne
T6. Dokadne testowanie pobliskich bdw
T7. Wzorce bdw wiele ujawniaj
T8. Wzorce pokrycia testami wiele ujawniaj
T9. Testy powinny by szybkie
Zakoczenie
Bibliografia

A Wspbieno II

327

Przykad klient-serwer
Serwer
Dodajemy wtki
Uwagi na temat serwera
Zakoczenie

327
327
329
329
331

SPIS TRECI

11

Moliwe cieki wykonania


Liczba cieek
Kopiemy gbiej
Zakoczenie
Poznaj uywan bibliotek
Biblioteka Executor
Rozwizania nieblokujce
Bezpieczne klasy nieobsugujce wtkw
Zalenoci midzy metodami mog uszkodzi kod wspbieny
Tolerowanie awarii
Blokowanie na kliencie
Blokowanie na serwerze
Zwikszanie przepustowoci
Obliczenie przepustowoci jednowtkowej
Obliczenie przepustowoci wielowtkowej
Zakleszczenie
Wzajemne wykluczanie
Blokowanie i oczekiwanie
Brak wywaszczania
Cykliczne oczekiwanie
Zapobieganie wzajemnemu wykluczaniu
Zapobieganie blokowaniu i oczekiwaniu
Umoliwienie wywaszczania
Zapobieganie oczekiwaniu cyklicznemu
Testowanie kodu wielowtkowego
Narzdzia wspierajce testowanie kodu korzystajcego z wtkw
Zakoczenie
Samouczek. Peny kod przykadw
Klient-serwer bez wtkw
Klient-serwer z uyciem wtkw

12

331
332
333
336
336
336
337
338
339
340
340
342
343
344
344
345
346
346
346
346
347
347
348
348
349
351
352
352
352
355

B org.jfree.date.SerialDate

357

C Odwoania do heurystyk

411

Epilog

413

Skorowidz

415

SPIS TRECI

Sowo wstpne

s Ga-Jol ich silny zapach lukrecji jest doskoJ naym uzupenieniem naszego wilgotnego i chodnego
klimatu. Czci wizerunku Ga-Jol dla nas,
EDNYMI Z MOICH ULUBIONYCH DUSKICH CUKIERKW

Duczykw, jest mdre lub dowcipne zdanie zapisane na grze kadego pudeka. Kupiem dzi rano
dwupak tego delikatesu i odkryem, e jest na nim stare duskie powiedzenie:
rlighed i sm ting er ikke nogen lille ting.
Uczciwo w maych rzeczach nie jest ma rzecz. By to dobry znak wanie o tym chciaem
napisa we wprowadzeniu do niniejszej ksiki: mae rzeczy maj znaczenie. Jest to ksika o niewielkich zagadnieniach, ktrych znaczenie jest jednak niezwykle istotne.
Bg objawia si w szczegach mwi architekt Ludwig Mies van der Rohe. Zdanie to przypomina wspczesne argumenty na temat roli architektury w tworzeniu oprogramowania, a szczeglnie w wiecie Agile. Zdarzao si, e Bob i ja z pasj na ten temat dyskutowalimy. Faktycznie,
Mies van der Rohe przykada wielk wag do uytecznoci i ponadczasowej formy budynkw, bdcych
podstaw doskonaej architektury. Z drugiej strony, osobicie wybiera kad klamk w kadym
projektowanym przez siebie domu. Dlaczego? Poniewa mae rzeczy maj znaczenie.
W naszej debacie na temat TDD Bob i ja zgodzilimy si co do tego, e architektura oprogramowania to jeden z najwaniejszych aspektw jego tworzenia, ale chyba mamy inne wizje tego,
co to dokadnie oznacza. Jednak takie wtpliwoci s niezbyt istotne, poniewa moemy przyj, e
odpowiedzialni profesjonalici przez pewien czas obmylaj i planuj sposb realizacji projektu.
Notacje z pnych lat dziewidziesitych, w ktrych wynikiem procesu projektowania byy wycznie
testy i kod, dawno ju odeszy w zapomnienie. Jednak przywizanie do szczegw jest waniejsz
oznak profesjonalizmu ni jakakolwiek wielka wizja. Po pierwsze, przez praktyk w niewielkiej
13

skali profesjonalici zdobywaj biego i zaufanie niezbdne w wielkiej skali projektw. Po drugie,
najmniejszy fragment niewaciwej konstrukcji drzwi, ktre si nie domykaj, nieco wybrzuszona
deska w pododze, a nawet baagan na biurku cakowicie burz elegancj caoci. To uzmysawia
wag czystego kodu w procesie projektowania aplikacji.
Nadal jednak architektura jest metafor tworzenia oprogramowania, a w szczeglnoci tej czci
programowania, ktra pozwala dostarczy pocztkowy produkt, podobnie jak architekt dostarcza
oglny projekt budynku. W dzisiejszych czasach, gdy krluj metodyki Scrum i Agile, priorytetem
jest szybkie dostarczenie produktu na rynek. Chcemy, aby fabryka produkujca oprogramowanie
dziaaa z pen wydajnoci. Te fabryki stanowi koderzy, ktrzy pracuj nad stworzeniem produktu od sporzdzenia specyfikacji lub listy wymaga klienta. Metafora produkcyjna wietnie oddaje
ten sposb mylenia wszak sposb produkowania samochodw w japoskich fabrykach, w wiecie
linii produkcyjnych, zainspirowa powstanie metodologii Scrum.
Jednak nawet w przemyle samochodowym wikszo pracy nie stanowi produkowanie, ale utrzymanie pojazdw w stanie penej sprawnoci. W przypadku oprogramowania co najmniej 80% pracy mona nazwa utrzymaniem naprawianiem czego. Zamiast, jak typowy czowiek Zachodu,
skupia si na produkowaniu dobrego oprogramowania, powinnimy myle raczej jak kto, kto
remontuje budynki lub naprawia samochody. Co na ten temat mog powiedzie japoskie metody
zarzdzania?
Okoo 1951 roku w Japonii pojawia si idea Totalnego Zarzdzania Produkcj (Total Productive
Maintenance TPM). Skupiaa si ona na utrzymaniu zamiast na produkcji. Jednym z gwnych
elementw TPM by zbir tak zwanych zasad 5S. 5S to zbir dyscyplin i termin dyscyplina jest
tu uyty w znaczeniu dosownym. Te zasady 5S byy w rzeczywistoci podstawami metodyki Lean
coraz bardziej popularnego pojcia w przemyle oprogramowania. Zasady te nie s fakultatywne. Zgodnie z tym, co powiedzia wujek Bob, praktyka dobrego oprogramowania wymaga takiej
dyscypliny skupienia, wyobrani i mylenia. Nie zawsze jest to zwizane z dziaaniem, z nadawaniem
tamie produkcyjnej odpowiedniej prdkoci. Filozofia 5S skada si z nastpujcych koncepcji:

14

Seiri, czyli organizacja. Koniecznie trzeba wiedzie, jak znale potrzebne rzeczy przy uyciu
odpowiednich narzdzi, na przykad nazewnictwa. Uwaasz, e nazewnictwo nie jest wane?
Przeczytaj kolejne rozdziay.

Seiton, czyli porzdek. Istnieje stare amerykaskie powiedzenie: Miejsce dla wszystkiego
i wszystko na swoim miejscu. Fragmenty kodu powinny znajdowa si tam, gdzie si ich spodziewamy jeeli tak nie jest, powinnimy przebudowa kod, aby si tam znalazy.

Seiso, czyli czysto. W miejscu pracy nie powinno by zwisajcych przewodw, piasku, cinek
ani mieci. Tutaj autorzy mwi o zamiecaniu kodu komentarzami, ktre przechowuj histori
lub yczenia na przyszo. Usumy je.

Seiketsu, czyli standaryzacja. Grupa uzgadnia, w jaki sposb utrzymywa miejsce pracy w czystoci. Czy uwaasz, e w niniejszej ksice znajdziesz co na temat spjnego stylu kodowania
i zbioru praktyk w zespole? Skd bior si te standardy? Zachcam do lektury.

Shutsuke, czyli dyscyplina (samodyscyplina). Wymaga to zachowania dyscypliny w stosowaniu


tych praktyk, czstego zwracania uwagi na swoj prac i chci do zmian.

CZYSTY KOD. PODRCZNIK DOBREGO PROGRAMISTY

Jeeli podejmiesz wyzwanie tak, wyzwanie przeczytania tej ksiki i stosowania si do zamieszczonych w niej zalece, w kocu zrozumiesz i docenisz ostatni punkt. Docieramy wreszcie do
podstaw pracy odpowiedzialnego profesjonalisty, ktry powinien by skupiony na cyklu ycia produktu. Gdy utrzymujemy samochody lub inne maszyny zgodnie z zasadami TPM, to usuwanie
awarii jest wyjtkiem. Zamiast tego wchodzimy na wyszy poziom codziennie przegldamy maszyny i naprawiamy zuywajce si czci, zanim si zepsuj, wykonujemy wymian oleju co 15 000
kilometrw itp. Kod przebudowujemy bez skrupuw. Moemy poprawi kady z elementw. Inicjatywa TPM powstaa 50 lat temu, by mona byo budowa maszyny, ktre atwiej jest serwisowa. Zapewnienie czytelnoci kodu jest rwnie wane jak zapewnienie jego dziaania. Kolejn z praktyk
wdroonych w ramach TPM okoo roku 1960 byo skupienie si na wprowadzaniu nowych maszyn
lub zastpowaniu starych. Jak poucza Fred Brooks, naley co siedem lat ponownie konstruowa
gwne fragmenty oprogramowania, aby usun ujawnione bdy. Wydaje si, e powinnimy
skrci sta czasow Brooksa do tygodni, dni lub godzin, a nie lat. Tu wanie nabieraj znaczenia
szczegy.
W szczegach tkwi sia, ale jest co skromnego i gbokiego w takim podejciu do ycia, co, czego
mona si spodziewa w podejciu majcym japoskie korzenie. Jednak nie jest to tylko wschodnie
spojrzenie na ycie; mdro ludowa ludzi Zachodu jest rwnie pena takich przestrg. Zasada
seiton, wymieniona powyej w jednym z punktw, wysza spod pira ministra z Ohio, ktry postrzega schludno jako lekarstwo na zo kadego rodzaju. Co mona powiedzie o seiso? Czysto
jest bliska boskoci. Niezalenie od tego, jak pikny jest dom, baagan na biurku psuje dobre wraenie. Co mona znale o shutsuke wrd zotych myli? Ten, kto jest godzien zaufania w sprawach
niewielkich, jest godzien zaufania i w duych. Co moemy znale na temat gotowoci do przebudowy w odpowiednim czasie, na temat budowania fundamentw dla pniejszych wielkich decyzji, zamiast ich odkadania? Jak sobie pocielisz, tak si wypisz. Nie odkadaj na jutro tego, co
masz zrobi dzisiaj (taki by oryginalny sens wyraenia ostatni odpowiedni moment w Lean, gdy
powiedzenie to wpado w ucho konsultantom oprogramowania). Co mona powiedzie o maych,
indywidualnych staraniach, podejmowanych podczas zmierzania do osignicia wielkiego celu?
Wielki db ronie z maego odzia. Co na temat wykonywania dziaa prewencyjnych w codziennym yciu? Lepiej zapobiega, ni leczy. Jedno jabko dziennie trzyma lekarza z daleka ode mnie.
Czysty kod jest zainspirowany mdroci naszej szerokiej kultury; kultury, w ktrej wci jest
obecna dbao o szczegy.
Nawet w wielkiej literaturze dotyczcej architektury znajdziemy symptomy skupienia si na szczegach. Wemy jako przykad klamki van der Rohea. To seiri. Dlatego powinnimy zwraca uwag
na nazw kadej zmiennej. Powinnimy wybiera nazwy zmiennych z tak sam uwag, jak wybieramy imi swojego dziecka.
Kady waciciel domu wie, e konserwacja i cige usprawnienia nie maj koca. Architekt Christopher Alexander ojciec teorii wzorcw w architekturze postrzega kady akt projektowania
jako ma, lokaln napraw. Uwaa ponadto, e tworzenie precyzyjnej struktury jest jedynym zadaniem
architekta; wiksze formy mog by pozostawione wzorcom i ich stosowaniu przez mieszkacw.
Projektowanie jest staym zadaniem, nie tylko przy dodawaniu nowego pokoju do domu, ale rwnie
przy malowaniu, wymianie zniszczonych dywanw lub poprawianiu zlewu kuchennego. W sztuce
pojawiaj si analogiczne sentymenty. Szukajc innych, ktrzy opisuj Dom Boy jako peen detali,

SOWO WSTPNE

15

znalelimy dobre towarzystwo w dziewitnastowiecznym francuskim autorze, Gustawie Flaubercie. Francuski poeta, Paul Valry, przekonuje, e poemat nigdy nie jest skoczony i wymaga staej
pracy, a zaprzestanie pracy jest rezygnacj. Taka troska o szczegy jest waciwa wszelkiemu deniu do doskonaoci. By moe nie powiem nic nowego, ale bd w tej ksice zachca do przyjcia dyscypliny, ktra dawno temu zostaa zastpiona apati lub tylko odpowiadaniem na zmiany.
Niestety, zwykle nie uwaamy takich problemw za kluczowe w programowaniu. Porzucamy nasz
kod wczenie nie dlatego, e jest gotowy, ale dlatego, e nasz system wartoci skupia si na wygldzie, a nie treci tego, co dostarczamy. Taki brak uwagi w kocu drogo nas kosztuje: zy szelg zawsze
wraca. Badania, zarwno w przemyle, jak i na uniwersytetach, umniejszaj wag zachowania czystoci kodu. Gdy pracowaem w Bell Labs Software Production Research, odkrylimy, e spjny
system wci by statystycznie najwikszym wskanikiem niewielkiej liczby bdw. Chcielimy, aby
tym wskanikiem jakoci bya architektura, jzyk programowania albo inna zaawansowana notacja; jako osoby, ktre uwaay za profesjonalizm doskonao narzdzi i zaawansowane metody
projektowania, czulimy si zdeprecjonowani przez warto tych maszyn z najniszego poziomu
fabryki, koderw, stosujcych prosty i spjny styl wci. Odwoujc si do mojej ksiki sprzed 17
lat, mog powiedzie, e taki styl oddziela doskonao od zwykej kompetencji. Japoczycy doceniaj warto poszczeglnych pracownikw i, co wicej, systemw produkcji, opierajcych si na prostych, codziennych dziaaniach tych pracownikw. Jako jest wynikiem milionw aktw uwagi nie
tylko wspaniaych metod spywajcych z niebios. To, e te dziaania s proste, nie oznacza, e s
prostackie czy atwe do wykonania. S jednak bez wtpienia kanw wielkoci i pikna kadego
przedsiwzicia.
Oczywicie, niniejsza ksika obejmuje szerszy zakres zagadnie, odwoujc si do pogldw i dowiadcze prawdziwych pasjonatw dobrego kodu, takich jak Peter Sommerlad, Kevlin Henney
oraz Giovanni Asproni. Kod jest projektem i prosty kod to ich motta. Cho musimy pamita o tym, e interfejs jest programem i e struktura interfejsu wiele mwi o strukturze programu,
niezwykle wane jest odwoywanie si do zasady, ktra gosi, e projekt znajduje si w kodzie. Przebudowa w wiecie produkcji wie si z kosztami, ale przebudowa projektu podwysza jego warto. Powinnimy traktowa nasz kod jako pikn artykulacj powanych zada projektowych
projektu jako procesu, a nie statycznego punktu kocowego. To w kodzie znajduj si architektoniczne zasady czenia i spjnoci. Jeeli posuchamy Larryego Constantinea opisujcego czenie
i spjno, to zauwaymy, e mwi on z punktu widzenia kodowania, a nie wzniosych koncepcji
abstrakcyjnych, ktre mona znale w UML. Richard Gabriel przekonuje nas w swoim eseju Abstraction Descant, e abstrakcja to zo, chaos. Skoro tak, to kod, a szczeglnie czysty kod, jest zaprzeczeniem za, prb uporzdkowania chaosu.
Wracajc do mojego maego pudeka Ga-Jol, myl, e warto wspomnie tu o duskiej zasadzie,
aby zwraca uwag nie tylko na szczegy, ale rwnie by uczciwym w maych sprawach. Oznacza
to, e powinnimy by uczciwi w programowaniu, uczciwi wzgldem kolegw, gdy mowa o stanie
naszego kodu, a przede wszystkim uczciwi wzgldem siebie i wasnego kodu. Czy zrobilimy
wszystko, aby pozostawi obozowisko czyciejsze, ni je zastalimy? Czy ulepszylimy nasz kod
przed jego umieszczeniem w repozytorium? Nie s to zagadnienia peryferyjne stanowi sedno
zasad Agile. Metodologia Scrum zaleca, aby przebudowa kodu bya czci operacji zakoczenia
projektu. Ani architektura, ani czysty kod nie s doskonae, a jedynie najlepsze, jakie moglimy
16

CZYSTY KOD. PODRCZNIK DOBREGO PROGRAMISTY

wykona. Myli si jest rzecz ludzk; wybacza bosk. W Scrum wszystko jest widoczne niczego nie zamiatamy pod dywan. Jestemy uczciwi, jeli chodzi o stan kodu, poniewa kod nigdy
nie jest doskonay.
W naszym zawodzie desperacko potrzebujemy caej pomocy, jak moemy uzyska. Jeeli czysta
podoga w sklepie zmniejsza liczb wypadkw, a dobrze zorganizowany magazyn narzdzi zwiksza wydajno, to jestem za tym. Ksika ta jest najlepszym pragmatycznym wdroeniem zasad Lean
w programowaniu, jakie kiedykolwiek widziaem na papierze. Tego oczekiwaem od tej maej grupy indywidualistw, ktrzy przez lata starali si nie tylko sta si lepszymi, ale rwnie podarowa
swoj wiedz przemysowi, dziki czemu teraz znajduje si ona w Twoich rkach. Pozostawilimy ten
wiat nieco lepszym, ni by przedtem, zanim wujek Bob przesa mi rkopis.
Teraz, gdy napisaem te grnolotne stwierdzenia, zajm si posprztaniem biurka.
James O. Coplien
Mrdrup, Dania

SOWO WSTPNE

17

18

CZYSTY KOD. PODRCZNIK DOBREGO PROGRAMISTY

Wstp

Rysunek zamieszczony za zgod Thoma Holwerda

Ktre reprezentuj Twj zesp lub firm? Dlaczego jestemy


K w tym pokoju? Czy jest to zwyky przegld
kodu, czy moe tu po zainstalowaniu systemu pojawio
TRE DRZWI REPREZENTUJ

TWJ KOD?

si mnstwo powanych problemw? Czy debugujemy w panice, lczc nad kodem, nad ktrym
wczeniej pracowalimy? Czy klienci odchodz stadami, a kierownicy zabieraj nam premie? Czy
moemy by pewni, e znalelimy si za waciwymi drzwiami, gdy sprawy si komplikuj? Czy
mona jako nad tym zapanowa? Odpowied jest prosta: naley osign mistrzostwo.
Dochodzenie do mistrzostwa wymaga dwch elementw: wiedzy i pracy. Konieczne jest zdobywanie wiedzy na temat zasad, wzorcw i heurystyk; nastpnie naley przeku j w umiejtnoci
przez cik prac i praktyk.
19

Moesz nauczy si fizyki jazdy na rowerze. W rzeczywistoci klasyczna matematyka jest wzgldnie
prosta. Grawitacja, tarcie, moment ktowy, rodek masy i tak dalej mog by zademonstrowane
na niecaej stronie rwna. Majc te wzory, mog udowodni, e jazda na rowerze jest praktyczna,
i da Ci ca wiedz, jaka jest potrzebna. Pomimo tego nadal bdziesz spada podczas pierwszych
prb jazdy na rowerze.
Z kodowaniem jest podobnie. Moglibymy zapisa wszystkie dobre praktyki czystego kodu i zaufa Ci, e wykonasz swoj prac (inaczej mwic, pozwolilibymy Ci spa z roweru), ale jak by to
wiadczyo o nas jako nauczycielach i co daoby Tobie jako uczniowi?
Nie. W tej ksice zastosowalimy inne podejcie.
Nauka pisania czystego kodu jest cik prac. Wymaga czego wicej ni tylko wiedzy na temat
zasad i wzorcw. Musisz si przy tym spoci. Musisz to sam praktykowa i przeywa poraki. Musisz
patrze na wiczenia innych i ich poraki. Musisz widzie, jak d do doskonaoci. Musisz widzie, jak mcz si nad decyzjami i jak cen pac, gdy decyzje te s ze.
Bd przygotowany na cik prac przy czytaniu tej ksiki. To nie jest ksika atwa i przyjemna,
ktr mona przeczyta w samolocie i skoczy przed ldowaniem. Bdzie ona wymagaa pracy,
i to cikiej. Jakiego rodzaju bdzie to praca? Bdziesz czyta kod wiele kodu. Bdziesz proszony
o zastanowienie si, co jest dobre, a co ze w danym kodzie. Bdziesz proszony o ledzenie
zmian podczas rozdzielania moduw na czci i powtrnego ich skadania. To bdzie wymagao
czasu i pracy, ale zapewniamy ta inwestycja zwrci si z nawizk.
Ksika zostaa podzielona na trzy czci. W kilku pierwszych rozdziaach opisujemy zasady, wzorce i praktyki pisania czystego kodu. W rozdziaach tych znajduje si sporo kodu i ich przeczytanie
jest wyzwaniem. Przygotowuj one do lektury drugiej czci. Jeeli odoysz ksik po przeczytaniu pierwszej czci, yczymy szczcia!
Druga cz ksiki wymaga ciszej pracy. Zawiera kilka analiz przypadkw o coraz wikszej zoonoci. Kada analiza przypadku jest wiczeniem polegajcym na czyszczeniu pewnego kodu
poprawianiu kodu zawierajcego rnej wagi usterki. Liczba szczegw w tej czci jest ogromna.
Bdziesz musia skaka pomidzy tekstem i listingami kodu, analizowa tekst i ledzi nasze rozumowanie, aby poj kad wprowadzon zmian. Zarezerwuj sobie troch czasu, poniewa powinno
to zaj kilka dni.
Trzecia cz ksiki jest nagrod. Jest to jeden rozdzia zawierajcy list heurystyk i zapachw, jakie zebralimy przy tworzeniu analiz przypadkw. Gdy analizowalimy i czycilimy kod studiw
przypadkw, dokumentowalimy kady powd naszej akcji jako heurystyk lub zapach. Prbowalimy zrozumie nasze reakcje dotyczce czytanego oraz zmienianego kodu i uchwyci przesanki
naszych zaoe i decyzji. Wynikiem jest baza wiedzy opisujca sposb, w jaki mylimy, piszc,
czytajc i czyszczc kod.
Ta baza wiedzy ma ograniczon warto, jeeli nie przeczytae uwanie analiz przypadkw zawartych w drugiej czci ksiki. W tych analizach uwanie odnotowalimy kad zmian w postaci
odwoania do heurystyk. Te odwoania s umieszczone w nawiasach kwadratowych, np.: [H22].
20

CZYSTY KOD. PODRCZNIK DOBREGO PROGRAMISTY

Zwr uwag na kontekst, w jakim s stosowane i pisane te heurystyki. To nie same heurystyki s tak
wartociowe, ale relacje pomidzy tymi heurystykami a konkretnymi decyzjami, jakie podjlimy w czasie
czyszczenia kodu w analizie przypadku.
Aby pokaza te relacje, umiecilimy na kocu ksiki list odwoa do heurystyk z numerami stron.
Dziki temu mona znale kade miejsce, w ktrym zostaa zastosowana okrelona heurystyka.
Jeeli przeczytasz pierwsz i trzeci cz ksiki, pomijajc analizy przypadkw, bdzie to lektura
kolejnej atwej i przyjemnej ksiki o pisaniu dobrego oprogramowania. Jeeli jednak powicisz
czas na prac nad analiz przypadkw, przeledzisz kady may krok, kad decyzj jeeli postawisz si na naszym miejscu i przyjmiesz nasz sposb mylenia, to zdobdziesz znacznie lepsz
wiedz na temat tych zasad, wzorcw, praktyk i heurystyk. Nie bdziesz ju potrzebowa atwej
i przyjemnej wiedzy stanie si ona czci Twojej intuicji, czci Ciebie, w taki sam sposb, jak
rower staje si naszym przedueniem, gdy tylko opanujemy jazd na nim.

Podzikowania
Rysunki
Dzikuj moim dwm artystkom, Jeniffer Kohnke oraz Angeli Brooks. Jennifer jest autork wietnych, pomysowych rysunkw zamieszczonych na pocztku kadego rozdziau oraz portretw
Kenta Becka, Warda Cunninghama, Bjarnea Stroustrupa, Rona Jeffriesa, Gradyego Boocha,
Davea Thomasa, Michaela Feathersa oraz mojej podobizny.
Angela jest autork interesujcych rysunkw ozdabiajcych wntrze kadego rozdziau. Narysowaa dla
mnie sporo rysunkw przez ostatnie lata, w tym wiele ilustracji zamieszczonych w ksice Agile
Software Development: Principles, Patterns, and Practices. Angela jest moj najstarsz crk. Nie
ukrywam, e jestem z niej bardzo dumny.

WSTP

21

22

CZYSTY KOD. PODRCZNIK DOBREGO PROGRAMISTY

ROZDZIA 1.

Czysty kod

sign po t ksik z dwch powodw. Po pierwsze, jest programist.


S Po drugie, chce by lepszym programist.
Dobrze. Potrzebujemy lepszych programistw.
PODZIEWAMY SI, E CZYTELNIK

Jest to ksika o programowaniu, dlatego zamieszczono w niej sporo kodu. Przygldamy si temu
kodowi bardzo wnikliwie i z rnych stron, o czym przekonasz si w trakcie czytania. Dziki temu
bdziesz dostrzega rnic pomidzy dobrym kodem a zym. Wiemy, jak pisa dobry kod. Wiemy
rwnie, jak naprawi kiepski kod.

23

Niech stanie si kod...


Wielu Czytelnikw uwaa zapewne, e ksika na temat kodu jest anachronizmem wszak kod
nie stanowi dzi problemu; powinnimy skupi si na modelach i wymaganiach. Faktycznie,
niektre osoby sugeroway, e jestemy blisko koca epoki kodu. e wkrtce cay kod bdzie
generowany, a nie pisany. e programici po prostu nie bd potrzebni, poniewa specjalici
biznesowi bd generowali program na podstawie specyfikacji.
Nonsens! Nigdy nie unikniemy stosowania kodu, poniewa implementuje on szczegowe wymagania
uytkownikw. Na pewnym poziomie pracy nad programem szczegy te nie mog by zignorowane trzeba dokadnie je okreli. Specyfikacja wymaga na takim poziomie szczegowoci,
jaki maszyna moe wykona, jest wanie programowaniem. Specyfikacja ta jest kodem.
Oczekuj, e poziom abstrakcji naszych jzykw bdzie stale wzrasta, podobnie jak liczba jzykw
specyficznych dla domeny. Jest to korzystne zjawisko, jednak nie wyeliminujemy kodu. W rzeczywistoci wszystkie specyfikacje napisane w tych jzykach wyszego poziomu oraz waciwych dla
domeny bd kodem! Bd one nadal musiay by rygorystyczne, dokadne i tak formalne i szczegowe, aby maszyna moga je zinterpretowa i wykona.
Osoby, ktre uwaaj, e kod pewnego dnia zniknie, s podobne do matematykw, ktrzy maj
nadziej, e pewnego dnia odkryj matematyk... niesformalizowan. Maj oni nadziej, e pewnego
dnia stworz maszyny, ktre bd mogy wykonywa to, co bdziemy chcieli, a nie to, co im nakaemy. Maszyny te bd potrafiy przeksztaca oglnie wyraone potrzeby na doskonale dziaajce
programy, ktre precyzyjnie speni nasze wymagania.
To si nigdy nie zdarzy. Nawet ludzie, ze swoj intuicj i pomysowoci, nie s w stanie tworzy
udanych systemw na podstawie niejasnych odczu swoich klientw. Jeeli dyscyplina specyfikacji
wymaga czegokolwiek nas nauczya, to wanie koniecznoci tworzenia precyzyjnie okrelonych
wymaga, ktre s tak formalne jak kod i mog suy jako testy wykonania tego kodu!
Naley pamita, e kod jest w rzeczywistoci jzykiem, w ktrym ostatecznie wyraamy wymagania. Moemy tworzy jzyki, ktre s bliskie wymaganiom. Moemy tworzy narzdzia, ktre pomagaj nam analizowa i przetwarza wymagania na struktury formalne. Jednak nigdy nie wyeliminujemy niezbdnej precyzji przyszo kodu jest zatem niezagroona.

W poszukiwaniu doskonaego kodu...


Ostatnio czytaem przedmow do ksiki Kenta Becka Implementation Patterns1. Kent napisa:
Ksika ta bazuje na do kruchych podstawach na tym, e dobry kod ma znaczenie. Kruche
podstawy? Nie zgadzam si z tym! Uwaam, e jest to jedna z najistotniejszych przesanek w programowaniu (i myl, e Kent o tym wie). Wiemy, e dobry kod ma znaczenie, poniewa tak dugo
musielimy zmaga si z jego brakiem.
1

[Beck07].

24

ROZDZIA 1.

Znam pewn firm, ktra pod koniec lat osiemdziesitych ubiegego


wieku opracowaa niesamowit aplikacj. Bya ona bardzo popularna i wielu profesjonalistw j kupio. Jednak wkrtce cykle
produkcyjne zaczy si wydua. Bdy z jednej wersji nie byy
poprawiane w nastpnej. Czas uruchamiania aplikacji wydua
si, awarie pojawiay si coraz czciej. Pamitam, e pewnego
dnia po prostu wyczyem ten program i nigdy wicej go nie
uyem. Niedugo pniej firma ta wypada z rynku.
Dwadziecia lat pniej spotkaem jednego z pracownikw tej
firmy i zapytaem go, co si stao. Odpowied potwierdzia moje
obawy. pieszyli si z produktem i mieli ogromny baagan w kodzie. Dodawali coraz wicej funkcji, a kod stawa si coraz gorszy
i gorszy, a doszo do tego, e po prostu nie dao si nim zarzdza. By to przypadek zniszczenia firmy przez zy kod.
Czy Czytelnikowi zdarzyo si by wstrzymywanym przez zy kod? Kady programista o dowolnym
dowiadczeniu spotka si wielokrotnie z takim przypadkiem. Mamy nawet na to nazw. Jest to
tzw. brodzenie (ang. wading). Brodzimy w zym kodzie. Przedzieramy si przez bajoro pokrconych gazi i ukrytych puapek. Staramy si znale drog naprzd, majc nadziej na jak podpowied, jaki pomys na to, co si dzieje, ale rozwizanie niknie w bezmiarze bezsensu.
Oczywicie, kady z nas straci mnstwo czasu z powodu zego kodu. A zatem dlaczego go tworzymy?
Czy wynika to z popiechu? Prawdopodobnie tak. Czsto uwaamy, e nie mamy czasu, aby dobrze
wykona swoj prac; e nasz szef bdzie zy, jeeli bdziemy spdzali czas na usprawnianiu kodu.
By moe jestemy ju zmczeni prac nad programem i chcemy j zakoczy. Zdarza si rwnie, e
zagldamy do listy zada, jakie obiecalimy wykona, i zauwaamy, e trzeba szybko skleci jaki
modu, aby przej do nastpnego. Wszyscy tak robimy.
Nie zagldamy do baaganu, jaki wanie zrobilimy, i zostawiamy to na inny dzie. Uwaamy, e
najwaniejsze jest, e nasz zabaaganiony program dziaa, i uznajemy, e dziaajcy baagan jest lepszy ni nic. Obiecujemy sobie, e kiedy do niego wrcimy i go poprawimy. Oczywicie, znamy prawo
LeBlanca: Pniej znaczy nigdy.

Cakowity koszt baaganu


Kady programista z kilkuletnim dowiadczeniem w kodowaniu z pewnoci straci wiele czasu
pracy, borykajc si z czyim niechlujnym kodem. Zwykle jest on przyczyn znaczcego spowolnienia pracy. W czasie roku lub dwch zespoy, ktre dziaay bardzo szybko na pocztku projektu,
zauwaaj, e posuwaj si w wim tempie. Kada zmiana wprowadzona do kodu powoduje
bdy w dwch lub trzech innych fragmentach kodu. adna zmiana nie jest atwa. Kady dodatek
lub modyfikacja systemu wymaga zrozumienia skrzyowa, zakrtw i wzw, aby mona

CZYSTY KOD

25

byo doda kolejne skrzyowania, zakrty i wzy. Z czasem baagan staje si tak potny, e nie
mona si go pozby. Po prosu nie ma na to sposobu.
Wraz ze wzrostem baaganu efektywno zespou stale spada, dc do cakowitego paraliu projektu. Wobec spadku efektywnoci kierownictwo zwykle dodaje do projektu wicej osb w nadziei,
e podniesie to wydajno zespou. Jednak nowe osoby nie s zaznajomione z projektem systemu.
Nie wiedz, na jakie zmiany projektu mog sobie pozwoli, a jakie spowoduj jego naruszenie. Dodatkowo, tak jak wszyscy pozostali w zespole, s pod ogromn presj zwikszenia wydajnoci. Tak wic
tworz kolejne pokady baaganu, spychajc wydajno jeszcze bliej zera (patrz rysunek 1.1).

R Y S U N E K 1 . 1 . Wydajno a czas

Rozpoczcie wielkiej zmiany projektu


W kocu zesp buntuje si. Informuje kierownictwo, e nie moe kontynuowa programowania
na bazie tego okropnego kodu. da przeprojektowania. Kierownictwo nie chce kierowa wszystkich zasobw do penej zmiany projektu, ale nie moe zaprzeczy, e wydajno zespou jest
fatalna. W kocu ugina si pod daniami programistw i zatwierdza wielk zmian projektu.
Wybierany jest nowy zesp tygrysw. Kady chce by w tym zespole, poniewa jest to projekt tworzony od pocztku. Ludzie chc napisa co naprawd piknego. Jednak do tego zespou s wybierani tylko najlepsi i najbardziej byskotliwi. Reszta musi nadal utrzymywa biecy system.
Teraz te dwa zespoy zaczynaj si ze sob ciga. Zesp tygrysw musi zbudowa nowy system,
ktry realizuje wszystkie funkcje starego. Oprcz tego musi rwnie uwzgldnia zmiany wprowadzane do starego systemu. Kierownictwo nie zdecyduje si na wymian starego systemu, dopki
nowy nie bdzie realizowa tego, co poprzedni.
Wycig bdzie trwa bardzo dugo. Znam przypadek, gdy trwao to 10 lat. Po tym czasie pierwsi
czonkowie zespou tygrysw dawno odeszli, a pozostali zatrudnieni dali przeprojektowania nowego systemu, poniewa by on bardzo zabaaganiony.
Jeeli kto zetkn si z tym zjawiskiem, wie ju, e utrzymywanie wysokiej jakoci kodu jest nie
tylko efektywne finansowo, ale take jest warunkiem przetrwania firmy na rynku.

26

ROZDZIA 1.

Postawa
Czy zdarzyo si Czytelnikowi przedziera przez taki gszcz kodu, e zmiana, ktra wymagaa godzin, zaja tygodnie? Czy zdarzyo si, e co, co powinno by zmian w jednym wierszu, spowodowao zmian setek wierszy w rnych moduach? Objawy te wystpuj nazbyt czsto.
Dlaczego tak si dzieje? Dlaczego dobry kod tak szybko si psuje? Mamy na to wiele wyjanie.
Narzekamy, e zbyt daleko idce zmiany wymaga zniweczyy pocztkowy projekt. Stwierdzamy,
e plany byy zbyt napite, aby mona byo wykona wszystko prawidowo. Mwimy o gupich
kierownikach i nietolerancyjnych klientach, bezuytecznych marketingowcach i sesjach telefonicznych.
Jednak wina, drogi Dilbercie, jest nie w gwiazdach, ale w nas samych w braku profesjonalizmu.
By moe jest to gorzka piguka do przeknicia. Jak to nasza wina? A wymagania? Co z harmonogramem? Co z gupimi kierownikami i bezuytecznymi marketingowcami? Czy nie s oni po
czci winni?
Nie. Kierownictwo i marketing u nas szuka informacji, jakich potrzebuje, aby zoy obietnice
i przeprowadzi uzgodnienia, a jeli nawet o nic nas nie pytaj, nie powinnimy si wstydzi powiedzie, co mylimy. Uytkownicy oczekuj od nas sprawdzenia, czy wymagania bd pasowa do
systemu. Kierownicy projektu oczekuj, abymy pomogli im w planowaniu. Powinnimy by zainteresowani planowaniem projektu i bra znaczn cz odpowiedzialnoci za wszystkie bdy,
szczeglnie jeeli bdy te s zwizane ze zym kodem!
Powiesz: Zaraz, zaraz. Jeeli nie zrobi tego, co mi kaza mj kierownik, zostan zwolniony.
Prawdopodobnie nie. Wikszo kierownikw to ludzie, ktrzy chc prawdy, nawet jeeli tak si
nie zachowuj; chc dobrego kodu, nawet jeeli maj obsesj na punkcie harmonogramw. Mog
z pasj broni harmonogramw i wymaga to ich praca. Nasz prac jest obrona kodu
z rwnie wielk pasj.
Wyobramy sobie nastpujc sytuacj: przed operacj pacjent zdecydowanie da, aby lekarz
przesta wreszcie my rce, poniewa zabiera to zbyt wiele czasu 2. Jasne, e pacjent jest szefem,
ale kady lekarz absolutnie odrzuci takie dania. Dlaczego? Poniewa lekarze wiedz wicej ni
pacjent na temat ryzyka choroby i infekcji. Byoby to nieprofesjonalne (a oprcz tego karalne),
gdyby lekarz w tym przypadku zgodzi si z pacjentem.
Tak wic brakiem profesjonalizmu programisty jest podporzdkowanie si kierownikowi, ktry
nie rozumie zagroe zwizanych z baaganem w kodzie.

W roku 1847 Ignaz Semmelweis zaleca lekarzom mycie rk, ale oni nie stosowali si do tego, twierdzc, e s zbyt zajci
i nie maj na to czasu pomidzy wizytami u kolejnych pacjentw.

CZYSTY KOD

27

Najwiksza zagadka
Programici spotykaj si z zagadk podstawowych wartoci. Wszyscy programici majcy co najmniej kilka lat dowiadczenia wiedz, e baagan ich spowalnia. Jednoczenie wszyscy programici ulegaj presji tworzenia baaganu w celu dotrzymania terminw. W skrcie nie uznaj, e czas biegnie tak szybko!
Prawdziwi profesjonalici wiedz, e druga cz zagadki jest niewaciwa. Nie da si dotrzyma terminw, wprowadzajc baagan. W rzeczywistoci baagan natychmiast nas spowolni, co spowoduje przekroczenie terminu. Jedynym sposobem na dotrzymanie terminu jedynym sposobem na szybki rozwj
aplikacji jest utrzymywanie moliwie najwyszej czystoci kodu.

Sztuka czystego kodu?


Zamy, e uwaasz niedbay kod za znaczne utrudnienie. Zamy, e akceptujesz fakt, i jedynym sposobem na utrzymanie duej szybkoci tworzenia kodu jest zapewnienie jego czystoci.
Trzeba sobie zada pytanie: Czy pisz czysty kod?. Prba tworzenia czystego kodu nie ma wikszego sensu, jeeli nie wiadomo, co to stwierdzenie oznacza!
Tworzenie czystego kodu mona porwna do malowania obrazu. Wikszo z nas moe stwierdzi, czy obraz jest adny czy brzydki. Jednak moliwo odrnienia dobrego dziea od zego nie
oznacza, e wiemy, jak malowa. Podobnie zdolno do odrnienia czystego kodu od niedbaego
nie oznacza, e wiemy, jak pisa czysty kod!
Pisanie takiego kodu wymaga dyscypliny oraz wykorzystania wielu technik w kolejnych mozolnych
iteracjach pozwalajcych na osignicie czystoci. Takie czucie kodu jest kluczem do sukcesu.
Niektrzy z nas po prostu si z tym urodzili. Niektrzy musieli t umiejtno zdoby. Zdolno ta
nie tylko pozwala stwierdzi, czy kod jest dobry, czy zy, ale rwnie pokazuje strategie stosowania
technik naszej dyscypliny nauki do przeksztacenia zego kodu w czysty.
Programista pozbawiony czucia kodu moe patrze na baagan i rozpoznawa go, ale nie bdzie
mia pomysu, co z nim zrobi. Programista posiadajcy t umiejtno spojrzy na zabaaganiony
modu i zobaczy moliwoci i warianty. Takie czucie kodu pomaga programicie wybra najlepszy wariant i poprowadzi go (lub j) do naszkicowania sekwencji dziaa zapewniajcych transformacj kodu z postaci biecej do finalnej.
W skrcie programista piszcy czysty kod jest artyst, ktry po serii transformacji przeksztaca
pusty ekran edytora w elegancko zakodowany system.

Co to jest czysty kod?


Istnieje prawdopodobnie tyle definicji, ilu jest programistw. Dlatego poprosiem kilku znanych
i bardzo dowiadczonych programistw o opinie na ten temat.

28

ROZDZIA 1.

Bjarne Stroustrup, twrca C++


oraz autor The C++ Programming Language
Lubi, gdy mj kod jest elegancki i efektywny. Logika kodu
powinna by prosta, aby nie mogy si w niej kry bdy,
zalenoci minimalne dla uproszczenia utrzymania, obsuga
bdw kompletna zgodnie ze zdefiniowan strategi, a wydajno zbliona do optymalnej, aby nikogo nie kusio psucie kodu w celu wprowadzenia niepotrzebnych optymalizacji. Czysty kod wykonuje dobrze jedn operacj.
Bjarne uy sowa elegancki. To waciwe sowo! Sownik
jzyka polskiego podaje nastpujc definicj sowa elegancja:
dobry smak i wytworno w sposobie bycia; przyjemnie kunsztowny i prosty. Warto zwrci uwag na sowo przyjemny. Wydaje si, e Bjarne uwaa czysty
kod za przyjemny w czytaniu. Czytanie go powinno wywoywa umiech na twarzy, podobnie jak
dobrze skomponowana muzyka czy dobrze zaprojektowany samochd.
Bjarne wspomina rwnie o efektywnoci i to dwukrotnie. Ten pogld twrcy jzyka C++ nie
powinien nas dziwi, ale osobicie uwaam, e jest to co wicej ni tylko zwyka potrzeba szybkoci dziaania. Nastpne warte do odnotowania stwierdzenie opisuje konsekwencje braku elegancji.
Uy on sowa kusi. Jest to szczera prawda. Zy kod kusi do powikszenia baaganu! Zmiany
przeprowadzane w zym kodzie zwykle prowadz do tego, e staje si on jeszcze gorszy.
Pragmatyczni Dave Thomas oraz Andy Hunt powiedzieli to w inny sposb. Uyli oni metafory
rozbitego okna3. Budynek z rozbitymi oknami wyglda, jakby nikt si o niego nie troszczy. Dlatego
inni ludzie rwnie przestaj si o niego troszczy. W ten sposb coraz wicej okien jest rozbijanych.
W kocu sami je rozbijaj. Zaczynaj bazgra na fasadzie i pozwalaj na powstanie gr mieci. Jedno
rozbite okno rozpoczyna proces upadku.
Bjarne wspomnia rwnie, e obsuga bdw powinna by kompletna. Wymaga to skupienia si
na szczegach. Skrcona obsuga bdw jest jednym z przypadkw, gdy programici nie dbaj
o szczegy. Drugim s wycieki pamici, a trzecim wycigi. Kolejnym takim przypadkiem s niespjne nazwy. Wynika z tego, e w czystym kodzie zwracamy uwag na szczegy.
Bjarne zamyka swoj wypowied stwierdzeniem, e czysty kod wykonuje dobrze jedn operacj.
Nie jest przypadkiem, e tak wiele zasad projektowania oprogramowania mona sprowadzi do
jednego prostego stwierdzenia. W zym kodzie prbuje si wykona zbyt wiele zada i operacji,
mieszajc zamierzenia i ambicj z zastosowaniem. Czysty kod jest ukierunkowany. Kada funkcja,
kada klasa, kady modu realizuje jedno zadanie.

http://www.pragmaticprogrammer.com/booksellers/2004-12.html

CZYSTY KOD

29

Grady Booch, autor Object Oriented Analysis


and Design with Applications
Czysty kod jest prosty i bezporedni. Czysty kod czyta si
jak dobrze napisan proz. Czysty kod nigdy nie zaciemnia
zamiarw projektanta; jest peen trafnych abstrakcji i prostych cieek sterowania.
Grady powtrzy niektre spostrzeenia Bjarne, ale uj je
z perspektywy czytelnoci. Moim zdaniem bardzo trafne jest
stwierdzenie, e czysty kod powinien by podobny do dobrze
napisanej prozy. Przypomnijmy sobie naprawd dobr ksik,
jak ostatnio czytalimy sowa pynnie wytwarzay obrazy w naszych mylach. Byo to podobne
do ogldania filmu, prawda? Lepiej! Widzielimy bohaterw, syszelimy dwiki, dowiadczalimy patosu i humoru.
Jednak czytanie czystego kodu nie bdzie podobne do czytania Wadcy piercieni, mimo e metafora literacka jest do adekwatna. Podobnie jak w przypadku dobrej powieci, czysty kod jasno
przedstawia napicie w rozwizywanym problemie. Powinien on budowa to napicie, a do
punktu kulminacyjnego, w ktrym czytelnik krzyknie: Aha! Oczywicie!, gdy pojawi si oczywiste
rozwizanie.
Zauwayem, e Grady uywa stwierdzenia trafna abstrakcja jako fascynujcego oksymoronu!
W tym znaczeniu sowo trafna jest niemal synonimem konkretna. Mj sownik definiuje sowo
trafna jako energicznie stanowcza, rzeczowa, pozbawiona niezdecydowania i niepotrzebnych szczegw. Pomimo tych pozornie rnych znacze sowo to niesie ze sob mocny przekaz. Nasz kod
powinien by rzeczowy, a nie spekulacyjny. Powinien zawiera tylko to, co jest niezbdne. Nasi
czytelnicy powinni postrzega nas jako skutecznych.

Big Dave Thomas, zaoyciel OTI,


ojciec chrzestny strategii Eclipse
Czysty kod moe by czytany i rozszerzany przez innego
programist ni jego autor. Posiada on testy jednostkowe
i akceptacyjne. Zawiera znaczce nazwy. Oferuje jedn, a nie
wiele cieek wykonania jednej operacji. Posiada minimalne zalenoci, ktre s jawnie zdefiniowane, jak rwnie zapewnia jasne i minimalne API. Kod powinien by
opisywany przy jednoczesnej zalenoci od jzyka nie
wszystkie potrzebne informacje mog by wyraane bezporednio w kodzie.

30

ROZDZIA 1.

Big Dave rwnie wskazuje potrzeb czytelnoci przedstawion przez Gradyego, ale z wan
rnic. Dave zakada, e czysty kod pozwala na jego atwe rozszerzanie przez innych ludzi. Moe to si wydawa oczywiste, ale nie moe by niedocenione. Jest to w kocu rnica pomidzy kodem, jaki si atwo czyta, a kodem, jaki mona atwo zmienia.
Dave czy czysto z testami! Dziesi lat temu spowodowaoby to powszechne zdziwienie. Jednak
dyscyplina programowania sterowanego testami w znacznym stopniu wpyna na nasz przemys
i staa si jedn z najbardziej podstawowych zasad. Dave ma racj. Kod bez testw nie jest czysty.
Nie ma znaczenia, jak jest elegancki, nie ma znaczenia, jak jest czytelny i dostpny; jeeli brakuje
w nim testw, nie jest czysty.
Dave dwukrotnie uywa sowa minimalny. Najwyraniej docenia potrzeb minimalizowania kodu.
Faktycznie, jest to czsto spotykane twierdzenie w literaturze dotyczcej programowania. Mniejsze
jest lepsze.
Dave uwaa rwnie, e kod powinien by opisywany. Jest to odwoanie do programowania pimiennego4 Knutha. Wynikiem tego procesu powinien by kod skomponowany w taki sposb,
aby by czytelny dla ludzi.

Michael Feathers,
autor Working Effectively with Legacy Code
Mgbym wymienia wszystkie cechy, jakie zauwaam
w czystym kodzie, ale istnieje jedna, ktra prowadzi do
pozostaych. Czysty kod zawsze wyglda, jakby by napisany przez kogo, komu na nim zaley. Nie ma w nim nic
oczywistego, co mgby poprawi. Autor kodu pomyla
o wszystkim i jeeli prbujemy sobie wyobrazi usprawnienia, prowadz one nas tam, skd zaczlimy, co pozwala nam doceni kod, ktry kto dla nas napisa kod,
ktry napisa kto, kto naprawd przejmuje si swoimi
zadaniami.
Jedno wyraenie: przejmuje si. Tak naprawd jest to temat
tej ksiki. Prawdopodobnie odpowiednim podtytuem byoby
zdanie Jak przejmowa si kodowaniem.
Michael trafi w sedno. Czysty kod to taki, ktrym zaj si kto, komu na nim zaley. Kto, kto powici czas na jego uproszczenie i uporzdkowanie. Kto, kto zwrci uwag na szczegy kto
si przej.

[Knuth92].

CZYSTY KOD

31

Ron Jeffries, autor Extreme Programming Installed


oraz Extreme Programming Adventures in C#
Ron zacz swoj karier od programowania w jzyku Fortran
w Strategic Air Command i pisa kod w niemal wszystkich
jzykach i na niemal kadej maszynie. Warto uwanie zapozna si z jego sowami.
Ostatnio zaczem (i niemal skoczyem) od zasad Becka
dotyczcych prostego kodu. W podanej kolejnoci prosty kod:

przechodzi wszystkie testy;

nie zawiera powtrze;

wyraa wszystkie idee projektowe zastosowane w systemie;

minimalizuje liczb encji, takich jak klasy, metody, funkcje i podobne.

Spord wymienionych cech skupiam si gwnie na powtrzeniach. Gdy ta sama operacja jest
wykonywana wielokrotnie, jest to znak, e znajduje si tam idea, ktra nie jest wystarczajco dobrze
reprezentowana w kodzie. Prbuj okreli, co ni jest. Nastpnie prbuj wyrazi t ide znacznie
janiej.
Ekspresywno oznacza dla mnie uycie znaczcych nazw i osobicie zmieniam kilkakrotnie nazwy rnych elementw, zanim znajd waciw. Przy uyciu nowoczesnych narzdzi, takich jak
Eclipse, zamiana nazwy jest dosy prosta, wic nie stanowi to dla mnie problemu. Ekspresywno
nie ogranicza si jednak do nazw. Sprawdzam rwnie, czy obiekt lub metoda nie wykonuj wicej ni jednej operacji. Jeeli jest to obiekt, prawdopodobnie wymaga podziau na dwa lub wicej
obiektw. Jeeli jest to metoda, zawsze korzystam w niej z narzdzia Extract Method, dziki
czemu uzyskuj jedn metod, ktra janiej wyraa to, co wykonuje, i kilka podmetod mwicych,
jak jest to zrobione.
Powtrzenia i ekspresywno s pocztkiem bardzo dugiej drogi do tego, co uwaam za czysty kod,
a usprawnienie brudnego kodu pod wzgldem tylko tych dwch cech powoduje znaczn rnic.
Istnieje jednak jeszcze jedna rzecz, na jak zwracam uwag; jest ona nieco trudniejsza do wyjanienia.
Po latach wykonywania tej pracy wydaje mi si, e wszystkie programy skadaj si z bardzo podobnych elementw. Jednym z przykadw jest szukanie elementw w kolekcji. Niezalenie od
tego, czy mamy baz danych z informacjami o pracownikach, czy tablic mieszajc z kluczami
i wartociami, czy te tablic z pewnego rodzaju elementami, czsto chcemy uzyska okrelony
element z takiej kolekcji. Gdy znajd taki fragment, czsto przenosz okrelon implementacj do
bardziej abstrakcyjnej metody lub klasy. Daje mi to kilka interesujcych moliwoci.
Mog teraz zaimplementowa funkcj przy uyciu prostego mechanizmu, na przykad tablicy mechanizmu, ale poniewa w tym momencie wszystkie odwoania do tego wyszukiwania odwouj si
do mojej maej abstrakcji, mog zmieni implementacj w dowolnym momencie. Mog szybko
przej do kolejnych elementw, zachowujc moliwo pniejszej zmiany.

32

ROZDZIA 1.

Dodatkowo abstrakcja kolekcji czsto kieruje moje zainteresowanie do tego, co naprawd jest
wykonywane, i odsuwa mnie od cieki implementowania okrelonego dziaania kolekcji, gdy tak
naprawd musz mie kilka bardzo prostych sposobw na wyszukanie tego, czego potrzebuj.
Ograniczone powtrzenia, wysoka ekspresywno i wczesne budowanie prostych abstrakcji. Tym
jest dla mnie czysty kod.
W tych kilku akapitach Ron podsumowa zawarto caej ksiki. Brak powtrze, jedna operacja,
ekspresywno, mae abstrakcje. Mamy to wszystko.

Ward Cunningham, wynalazca Wiki, wynalazca Fit,


wspautor eXtreme Programming. Sia przewodnia
dla wzorcw projektowych. Lider Smalltalka
i programowania obiektowego. Ojciec chrzestny
wszystkich, ktrzy dbaj o kod
Wiemy, e pracujemy na czystym kodzie, jeeli kada
procedura okazuje si tak, jakiej si spodziewalimy.
Mona nazywa go rwnie piknym kodem, jeeli wyglda, jakby ten jzyk zosta stworzony do rozwizania
danego problemu.
Tego typu zdania s charakterystyczne dla Warda. Czytasz je,
kiwasz gow i przechodzisz do nastpnego tematu. Wyglda
to tak rozsdnie, e prawie nie odbierasz tego jako czego dogbnego. Moesz myle, e byo to
mniej wicej to, czego oczekiwae. Ale spjrzmy dokadniej.
Jakiej si spodziewalimy kiedy ostatnio widziae modu, ktry by w wikszoci tym, czego si
spodziewae? Czy nie trafiamy czciej na moduy, ktre wygldaj na zagmatwane, skomplikowane
i niejasne? Nie jest to pogwacenie tej zasady? 6Czy nie prbujesz czsto uchwyci i utrzyma wtki
wnioskowania, ktre wypywaj z caego systemu i toruj sobie drog przez czytany przez Ciebie modu?
Kiedy ostatnio czytae kod i kiwae gow z aprobat tak, jak podczas czytania zasad Warda?
Ward oczekuje, e czytajc czysty kod, nie bdziemy zaskakiwani. W rzeczywistoci nawet nie powinnimy zbytnio powica uwagi czytaniu. Po prostu powinnimy czyta i bdzie to mniej wicej
to, czego oczekiwalimy. Powinno to by oczywiste, proste i zachcajce. Kady modu powinien
przygotowywa grunt pod nastpny. Kady powinien mwi nam, jak bdzie napisany nastpny.
Programy, ktre s tak czyste, s tak dobrze napisane, e niemal tego nie zauwaamy. Projektant
spowodowa, e s niezwykle proste, jak wszystkie niezwyke projekty.
Co moemy powiedzie na temat uwagi Warda o piknie? Wszyscy borykamy si z tym, e jzyki,
ktrymi si posugujemy, nie zostay zaprojektowane specjalnie pod ktem naszych problemw
projektowych. Jednak stwierdzenie Warda znw zrzuca na nas cay ciar. Mwi on, e pikny kod
powoduje, e jzyk wyglda tak, jakby by przeznaczony do rozwizywania danego problemu! Wic
to na nas ciy odpowiedzialno, by doprowadzi do tego, aby jzyk by prosty! Uwaajcie bigoci

CZYSTY KOD

33

jzykowi z wszystkich stron. To nie jzyk powoduje, e program


jest prosty. To programista sprawia, e jzyk jest prosty!

Szkoy mylenia
Co ja (wujek Bob) mog powiedzie na ten temat? Czym wedug mnie jest czysty kod? Ksika ta przedstawia, w najmniejszych szczegach, co ja i moi przyjaciele mylimy na
temat czystego kodu. Powiemy Ci, co uwaamy za czyst nazw zmiennej, czyst funkcj, czyst klas i tak dalej. Przedstawimy te opinie jako bezwarunkowe i nie bdziemy przeprasza za nasze ostre opinie. Dla nas, z punktu widzenia
naszych karier, s one bezwarunkowe. S one nasz szko
mylenia o czystym kodzie.
Zwolennicy rnych sztuk walki nie zgadzaj si w tym, ktra z nich jest najlepsza lub jaka jest
najlepsza technika. Czsto mistrzowie sztuk walki tworz swoje szkoy mylenia i zbieraj
uczniw, aby ich w dany sposb uczy. Mamy wic Gracie Jiu Jitsu, zaoon i prowadzon przez
rodzin Gracie z Brazylii. Mamy Hakkoryu Jiu Jitsu, zaoon i prowadzon przez Okuyam Ryuho
z Tokio. Mamy Jeet Kune Do, zaoon przez Brucea Lee w USA.
Uczniowie tych szk zagbiaj si w naukach swoich mistrzw. Ucz si tego, co przedstawia okrelony mistrz, czsto wykluczajc nauki innych mistrzw. Pniej, gdy zdobywaj mistrzostwo w swojej
sztuce, mog sta si uczniami innych mistrzw, aby poszerzy swoj wiedz i praktyk. Niektrzy
w kocu doskonal swoje umiejtnoci, odkrywajc nowe techniki, i zakadaj nowe szkoy.
adna z tych rnych szk nie ma cakowitej racji. Jednak wewntrz okrelonej szkoy dziaamy
tak, jakby nauki i techniki byy prawidowe. W kocu jest to prawidowy sposb praktykowania
technik prezentowanych w Hakkoryu Jiu Jitsu lub Jeet Kune Do. Jednak prawidowo w danej
szkole nie uniewania nauki innych szk.
Zamy, e ksika ta ma podtytu Szkoa czystego kodu mentora obiektowego. Znajdujce si
w niej techniki i nauki pokazuj, jak my praktykujemy nasz sztuk. Chcemy powiedzie, e jeeli
bdziesz stosowa nasze techniki, bdziesz cieszy si korzyciami, ktrymi my si cieszymy, i nauczysz si pisa kod czysty i profesjonalny. Jednak nie popeniaj bdu, mylc, e jestemy nieomylni w bezwzgldnym sensie. Istniej inne szkoy i inni mistrzowie, ktrzy maj rwnie duo
cech profesjonalistw, co my. Uczenie si od nich rwnie da Ci wiele dobrego.
W istocie wiele zalece zamieszczonych w tej ksice jest kontrowersyjnych. Prawdopodobnie
nie zgodzisz si z wszystkimi. Moesz je kwestionowa. wietnie. Nie uwaamy si za ostateczny
autorytet. Z drugiej strony, zalecenia zamieszczone w tej ksice dotycz technik, ktrych poznanie
zajo nam duo czasu. Nauczylimy si ich przez dekady dowiadczenia oraz w wyniku wielokrotnych prb i bdw. Tak wic, niezalenie od tego, czy si zgadzasz, czy nie, byoby bdem, gdyby nie
przyj i nie doceni naszego punktu widzenia.

34

ROZDZIA 1.

Jestemy autorami
Pole @author w Javadoc mwi nam, kim jestemy. Jestemy autorami. Autorzy maj zwykle czytelnikw. W rzeczywistoci autorzy s odpowiedzialni za komunikacj ze swoimi czytelnikami.
Nastpnym razem, gdy napiszesz wiersz kodu, pamitaj, e jeste autorem piszcym dla czytelnikw,
ktrzy bd ocenia Twoje wysiki.
Mona zada pytanie, ile kodu naprawd si czyta? Czy wysiek w wikszoci nie jest skupiony na
jego pisaniu?
Czy kiedykolwiek odtwarzae sesj edycji? W latach osiemdziesitych i dziewidziesitych korzystalimy z takich edytorw jak Emacs, ktry zapamitywa kade nacinicie klawisza. Mona byo
popracowa przez godzin, a nastpnie odtworzy ca sesj edycji jak w przyspieszonym filmie.
Gdy to zrobiem, efekty byy fascynujce.
Ogromna wikszo procesu odtwarzania polegaa na przewijaniu i przechodzeniu do innych moduw!
Bob wszed do moduu.
Przewin w d, do funkcji wymagajcej zmiany.
Przerwa, rozwaajc moliwoci.
Przeszed do pocztku moduu w celu sprawdzenia inicjalizacji zmiennej.
Teraz przesun si w d i zacz pisa.
Ups, usun to, co napisa!
Wpisa to jeszcze raz.
Usun to jeszcze raz.
Wpisa poow czego innego, ale usun to!
Przesun si do innej funkcji, ktra wywoywaa zmienian funkcj, aby sprawdzi, jak bya
wywoywana.
Przeszed znw w gr i wpisa ten sam kod, ktry usun.
Zatrzyma si.
Ponownie usun ten kod.
Otworzy nowe okno i zajrza do klasy pochodnej. Czy ta funkcja jest przesonita?
Wiesz ju, o co chodzi. W rzeczywistoci stosunek czasu spdzanego na czytaniu do czasu spdzanego na pisaniu jest jak 10:1. Stale czytamy starszy kod, jest to cz pracy przy pisaniu nowego.
Poniewa wspczynnik jest tak wysoki, chcemy, aby czytanie kodu byo atwe, nawet jeeli powoduje, e jego pisanie jest trudniejsze. Oczywicie, nie ma sposobu na pisanie kodu bez jego czytania,
wic uatwienie czytania powoduje uatwienie pisania.
Nie ma od tego wyjtkw. Nie mona pisa kodu, jeeli nie przeczyta si kodu go otaczajcego.
Kod, jaki prbujemy dzi napisa, moe by trudny lub atwy do napisania, w zalenoci od tego,
jak atwy lub trudny do czytania jest kod otaczajcy go. Tak wic jeeli chcemy szybko i naprzd,
jeeli chcemy szybko i atwo pisa kod musi by on atwy w czytaniu.

CZYSTY KOD

35

Zasada skautw
Nie wystarczy dobrze pisa kod. Kod musi by zachowany w czystoci przez cay czas. Wiele razy
widzielimy kod, ktry z czasem psu si i degradowa. Dlatego naley aktywnie przeciwdziaa tej
degradacji.
Skauci amerykascy mieli prost zasad, jak mona zastosowa w naszym zawodzie.
Pozostaw obz czyciejszym, ni go zastae5.
Jeeli wszyscy bdziemy dodawali do repozytorium kod nieco czyciejszy, ni ten, ktry pobralimy, po prostu nie bdzie si psu. Czyszczenie nie musi by due. Wystarczy zmiana jednej nazwy
zmiennej na lepsz, podzia funkcji, ktra bya nieco za dua, wyeliminowanie maego powtrzenia, wyczyszczenie jednej zoonej instrukcji if.
Czy mona wyobrazi sobie prac w projekcie, w ktrym kod po prostu z czasem staje si lepszy?
Czy uwaasz, e jakakolwiek inna opcja jest profesjonalna? Czy staa poprawa nie jest niezbdn
czci profesjonalizmu?

Poprzednik i zasady
W wielu miejscach ksika ta jest poprzedniczk tej, ktr napisaem w roku 2002, zatytuowanej
Agile Software Development: Principles, Patterns, and Practices (PPP). Ksika ta bya powicona zasadom projektowania obiektowego i wielu praktykom stosowanym przez zawodowych programistw. Jeeli nie czytae PPP, moesz odnie wraenie, e kontynuuje ona histori tej
ksiki. Jeeli ju j przeczytae, moesz zauway, e wiele jej fragmentw odbija si echem w tej
ksice, na poziomie kodu.
W tej ksice znajduj si sporadyczne odwoania do rnych zasad projektowania. Mamy midzy
innymi zasad jednej odpowiedzialnoci (SRP), zasad otwarty-zamknity (OCP) oraz zasad odwrcenia zalenoci (DIP). Zasady te s dokadnie opisane w PPP.

Zakoczenie
Ksiki na temat sztuki nie obiecuj, e staniesz si artyst. Wszystko, co mog zrobi, to da nam
kilka narzdzi, technik i procesw mylenia wykorzystywanych przez artystw. Dlatego nie moemy obieca, e kady Czytelnik po przeczytaniu tej ksiki zostanie dobrym programist. Nie moemy obieca, e obdarzymy kadego intuicj potrzebn do tworzenia czystego kodu. Wszystko, co
moemy zrobi, to przedstawi proces mylenia dobrego programisty oraz sztuczki, techniki i narzdzia przez niego wykorzystywane.

Parafraza poegnania skautw autorstwa Roberta Stephensona Smyth Baden-Powellsa: Staraj si pozostawi ten wiat
nieco lepszym, ni go zastae.

36

ROZDZIA 1.

Podobnie jak w przypadku ksiki na temat sztuki, rwnie i ta jest wypeniona szczegami.
Jest w niej mnstwo kodu. Pokaemy zarwno dobry, jak i zy kod. Pokaemy, jak mona naprawi zy
kod. Przedstawimy list heurystyk, dyscyplin oraz technik. Pokaemy przykad po przykadzie.
Pniej wszystko bdzie w Twoich rkach.
Czy pamitasz stary art o skrzypku koncertowym? Zaczepi on starszego czowieka na skrzyowaniu, pytajc go, jak trafi do Carnegie Hall. Stary czowiek spojrza na skrzypka i trzymane
przez niego skrzypce i powiedzia: Trzeba wiczy, synu. wiczy!.

Bibliografia
[Beck07]: Kent Beck, Implementation Patterns, Addison-Wesley 2007.
[Knuth92]: Donald E. Knuth, Literate Programming, Center for the Study of Language and Information,
Leland Stanford Junior University 1992.

CZYSTY KOD

37

38

ROZDZIA 1.

ROZDZIA 2.

Znaczce nazwy
Tim Ottinger

Wstp
Nazwy peni fundamentaln rol w oprogramowaniu. Nazywamy nasze zmienne, funkcje, argumenty, klasy i pakiety. Nazywamy pliki rdowe i katalogi, w ktrych s umieszczone. Nazywamy pliki jar, war oraz ear. Nazywamy, nazywamy, nazywamy. Poniewa robimy to tak czsto,
powinnimy robi to dobrze. W niniejszym rozdziale przedstawione s proste zasady tworzenia
dobrych nazw.

39

Uywaj nazw przedstawiajcych intencje


atwo powiedzie, e nazwy powinny przedstawia zamiary. Chcemy podkreli, e traktujemy ten
temat naprawd powanie. Wybr odpowiedniej nazwy to dobra inwestycja zajmuje troch
czasu, ale pozwala oszczdzi go znacznie wicej. Dlatego warto przyglda si uywanym nazwom
i zmienia je, gdy znajdziemy lepsze. Kady, kto czyta Twj kod (wcznie z Tob), skorzysta na tym.
Nazwa zmiennej, funkcji lub klasy powinna by odpowiedzi na wszystkie wane pytania. Powinna
informowa, w jakim celu istnieje, co robi i jak jest uywana. Jeeli nazwa wymaga komentarza, to
znaczy, e nie ilustruje intencji.
int d; // Czas trwania w dniach

Nazwa d nic nam nie objania. Nie przedstawia w aden sensowny sposb czasu trwania ani dni. Powinnimy wybra nazw, ktra przedstawia mierzon wielko oraz jednostk pomiaru:
int
int
int
int

elapsedTimeInDays;
// czasTrwaniawDniach
daysSinceCreation;
// dniOdUtworzenia
daysSinceModification;
// dniOdModyfikacji
fileAgeInDays;
// wiekPlikuwDniach

Wybr nazw ilustrujcych zamiary uatwia zrozumienie i modyfikowanie kodu. Jakie jest przeznaczenie poniszego kodu?
public List<int[]> getThem() {
List<int[]> list1 = new ArrayList<int[]>();
for (int[] x : theList)
if (x[0] == 4)
list1.add(x);
return list1;
}

Dlaczego tak trudno stwierdzi, co robi ten kod? Nie ma tu zoonych wyrae. Odstpy i wcicia
s rozsdne. Uywane s w nim tylko trzy zmienne i dwie stae. Nie ma tu nawet uytych adnych
fantazyjnych klas ani metod polimorficznych, jest tylko lista tablic.
Problemem nie jest prostota kodu, ale jego niejawno kontekst kodu nie jest jasny na podstawie
analizy samego kodu. W kodzie tym niejawnie zakadamy, e znamy odpowiedzi na takie pytania, jak:
1. Jakiego rodzaju elementy znajduj si w theList?
2. Jakie jest znaczenie zerowego indeksu elementu w theList?
3. Jakie jest znaczenie wartoci 4?
4. Do czego mona uy zwracanej listy?
W przykadowym kodzie nie ma odpowiedzi na te pytania, ale mog si one tam znajdowa.
Zamy, e pracujemy nad gr w sapera. Odkrylimy, e pole gry jest list komrek o nazwie
theList. Zmienimy wic jej nazw na gameBoard.

40

ROZDZIA 2.

Kada komrka pola gry jest reprezentowana przez prost tablic. Na podstawie dalszej analizy
okazao si, e zerowy indeks lokacji zawiera warto statusu, a warto 4 oznacza, e komrka jest
zaznaczona. Wystarczy nada nazwy tym elementom i czytelno kodu znacznie si poprawi:
public List<int[]> getFlaggedCells() {
List<int[]> flaggedCells = new ArrayList<int[]>();
for (int[] cell : gameBoard)
if (cell[STATUS_VALUE] == FLAGGED)
flaggedCells.add(cell);
return flaggedCells;
}

Warto zwrci uwag, e prostota kodu zostaa zachowana nie zmienia si ani liczba operatorw i staych, ani liczba poziomw zagniedenia. Mimo to kod sta si znacznie bardziej czytelny.
Moemy rwnie pj nieco dalej i napisa prost klas dla komrek w miejsce tablicy liczb int.
Mona utworzy funkcj przedstawiajc intencje (nazwiemy j isFlagged), pozwalajc ukry
magiczne liczby. Moemy wic napisa now wersj funkcji:
public List<Cell> getFlaggedCells() {
List<Cell> flaggedCells = new ArrayList<Cell>();
for (Cell cell : gameBoard)
if (cell.isFlagged())
flaggedCells.add(cell);
return flaggedCells;
}

Po tych prostych zmianach nazw nietrudno zrozumie, o co chodzi w tym kodzie. W tym wanie
tkwi potga wyboru odpowiednich nazw elementw kodu.

Unikanie dezinformacji
Programici musz wystrzega si sugerowania faszywych wskazwek zaburzajcych znaczenie
kodu. Powinnimy unika sw, ktre wprowadzaj znaczenia rnice si od oczekiwanego. Na
przykad hp, aix czy sco bd nieodpowiednimi nazwami zmiennych, poniewa s to nazwy platform
Unix lub ich wariantw. Nawet jeeli kodujemy obliczanie przeciwprostoktnej (ang. hypotenuse)
i hp moe wydawa si odpowiednim skrtem, moe to by jednak dezinformujce.
Nie naley nazywa grupy kont mianem accountList, o ile nie jest to faktycznie zmienna typu List.
Sowo lista ma dla programisty specjalne znaczenie. Jeeli kontener przechowujcy konta nie jest
faktycznie typu List, moe to prowadzi do faszywych wnioskw1. Dlatego lepiej uy zmiennej
accountGroup, bunchOfAccounts lub po prostu accounts.
Naley unika nazw, ktre nieznacznie si od siebie rni. Ile czasu zajmie zauwaenie subtelnej
rnicy pomidzy zmienn XYZControllerForEfficientHandlingOfStrings w jednym module
i znajdujc si nieco dalej XYZControllerForEfficientStorageOfStrings? Sowa te maj zbyt
podobny wygld.

Jak si pniej okae, nawet jeeli kontener jest typu List, najlepiej nie wbudowywa typu kontenera w nazw.

ZNACZCE NAZWY

41

Przedstawianie podobnych koncepcji za pomoc podobnych sekwencji znakw to informacja.


Uywanie niespjnego nazewnictwa to dezinformacja. W nowoczesnych rodowiskach Java mona
korzysta z automatycznego dokaczania kodu. Piszemy kilka znakw z nazwy i naciskamy kombinacj klawiszy, co powoduje wywietlenie listy moliwych zakocze tej nazwy. Jest to bardzo
pomocne, jeeli nazwy bardzo podobnych elementw s posortowane alfabetycznie i jeeli rnice
s bardzo oczywiste, poniewa programista moe wybra obiekt z uyciem nazwy bez koniecznoci przegldania komentarzy, a nawet listy metod oferowanych przez klas.
Przykadem niezwykle dezinformujcych dziaa jest uycie w nazwach maej litery L oraz wielkiej O,
szczeglnie w poczeniach. Problem oczywicie polega na tym, e litery te wygldaj niemal tak
jak cyfry jeden i zero.
int a = l;
if ( O == l )
a = O1;
else
l = 01;

Czytelnik moe uwaa, e jest to wymylony przykad, ale osobicie wiele razy widzielimy kod,
w ktrym takie zapisy s powszechne. W jednym przypadku autor kodu zasugerowa uycie innej
czcionki, dziki ktrej rnice bd bardziej widoczne, rozwizanie to powinno by przekazane
wszystkim nowym programistom jako tradycja przekazywana ustnie lub w postaci dokumentacji.
Problem mona rozwiza ostatecznie bez dodatkowych nakadw wystarczy zwyka zmiana
nazwy.

Tworzenie wyranych rnic


Programici czsto sami sobie tworz problemy, gdy pisz
kod pozwalajcy jedynie speni oczekiwania kompilatora lub interpretera. Poniewa na przykad nie mona
uy tej samej nazwy do tego samego elementu w zakresie,
moemy ulec pokusie zmiany jednej nazwy w dowolny
sposb. Czasami zdarza si to przez literwk, co prowadzi do zaskakujcej sytuacji, w ktrej poprawienie
bdu powoduje, e nie mona skompilowa kodu2.
Nie wystarczy doda numer seryjny lub dodatkowe sowo, nawet jeeli wystarczy to kompilatorowi.
Jeeli nazwy musz by rne, to powinny one rwnie znaczy co innego.
Nazwy korzystajce z numerw kolejnych (a1, a2, aN) s przeciwiestwem nazw znaczcych.
Nazwy te nie s dezinformujce s one nieinformujce; nie nios ze sob adnej informacji
o intencjach autora. Przeanalizujmy taki przykad:

Wemy jako przykad fataln praktyk tworzenia zmiennej o nazwie


jest zajta.

42

ROZDZIA 2.

klass

tylko z tego powodu, e nazwa

class

public static void copyChars(char a1[], char a2[]) {


for (int i = 0; i < a1.length; i++) {
a2[i] = a1[i];
}
}

Funkcja ta bdzie znacznie czytelniejsza, jeeli jako nazw argumentw uyjemy source i destination.
Dodatkowe sowa s kolejnym sposobem rozrnienia nazw nienioscych adnego znaczenia. Jako
przykad wemy klas Product. Jeeli utworzymy inn klas, o nazwie ProductInfo lub ProductData,
otrzymamy rne nazwy, ale nie spowodujemy, e bd one znaczyy co innego. Sowa Info i Data
s rwnie mao znaczcymi nazwami, jak a, an i the.
Oczywicie nie ma nic zego w korzystaniu z konwencji zalecajcych stosowanie przedrostkw takich jak a czy the, pod warunkiem e zachowane s znaczce rnice. Na przykad moemy korzysta
z przedrostka a dla wszystkich zmiennych lokalnych i the dla wszystkich argumentw funkcji3.
Problem pojawia si w przypadku, gdy zdecydujemy si nazwa zmienn theZork, poniewa istnieje
ju inna zmienna o nazwie zork.
Dodatkowe sowa s nadmiarowe. Sowo variable (lub zmienna, jeeli przyjmiemy konwencj
stosowania polskich nazw w kodzie) nigdy nie powinno pojawia si w nazwie zmiennej. Sowo
table (tabela) nigdy nie powinno pojawia si w nazwie tabeli. W czym NameString jest lepsze
od Name? Czy Name (Nazwa) bdzie kiedykolwiek liczb zmiennoprzecinkow? Jeeli tak, zamana
zostanie wczeniejsza zasada dotyczca dezinformacji. Wyobramy sobie, e znajdziemy jedn klas
o nazwie Customer i inn CustomerObject. Jak powinnimy rozumie rnic pomidzy nimi?
Ktra reprezentuje najlepsz ciek do historii patnoci klienta?
Znamy aplikacj, w ktrej wystpuj ponisze nazwy. Zostay one zmienione, aby chroni winnych,
ale ponisze metody dokadnie ilustruj bd:
getActiveAccount();
getActiveAccounts();
getActiveAccountInfo();

Skd programici maj wiedzie, ktr z tych funkcji naley wywoa?


W przypadku braku specyficznych konwencji zmienna moneyAmount jest nieodrnialna od money,
customerInfo jest nieodrnialna od customer, accountData jest nieodrnialna od account,
a theMessage nie da si odrni od message. Nazwy powinny rni si w taki sposb, aby czytelnik
wiedzia, na czym polega rnica.

Tworzenie nazw, ktre mona wymwi


Ludzie dobrze sobie radz ze sowami. Znaczna cz naszego mzgu jest stworzona do interpretacji sw. Sowa z definicji daj si wymwi. Marnotrawstwem byoby nie skorzysta z duej czci
naszych mzgw, ktre wyewoluoway w celu umoliwienia nam korzystania z jzyka mwionego.
Dlatego powinnimy tworzy nazwy, ktre mona wyartykuowa.
3

Wujek Bob korzysta z tej konwencji w C++, ale zarzuci t praktyk, poniewa nowoczesne IDE spowodoway, e jest niepotrzebna.

ZNACZCE NAZWY

43

Jeeli nie bdziemy mogli ich wymwi, nie bdzie mona swobodnie dyskutowa o kodzie. C,
w bee cee arr three cee enn tee mamy pee ess zee kyew, widzisz?. Ma to znaczenie, poniewa programowanie jest aktywnoci spoeczn.
Znam firm korzystajc z nazw genymdhms (data generacji, rok, miesic, dzie, godzina, minuta
i sekunda), wic czsto mona byo spotka ludzi mamroczcych gen why emm dee aich emm ess.
Ja mam zwyczaj dla niektrych irytujcy wymawiania wszystkiego, co jest napisane, wic zaczem czyta te nazwy gen-yah-muddahims. Pniej zostao to podchwycone przez zesp projektantw i analitykw, ale nadal brzmiao to gupio. Traktowalimy to jako dobry art. art czy
nie, tolerowalimy ze nazewnictwo. Konieczne byo tumaczenie nazw zmiennych nowym programistom, a zaczli korzysta z dziwnie brzmicych sztucznych sw, zamiast mwi zwykymi
angielskimi sowami. Porwnajmy:
class DtaRcrd102 {
private Date genymdhms;
private Date modymdhms;
private final String pszqint = "102";

/* */
};

z:
class Customer {
private Date generationTimestamp;
private Date modificationTimestamp;;
private final String recordId = "102";

/* ... */
};

Moliwa staa si cakiem inteligentna konwersacja: Marek spjrz na ten rekord! Znacznik czasu
generacji jest ustawiony na jutrzejsz dat! Jak to si mogo sta?.

Korzystanie z nazw atwych do wyszukania


Nazwy jednoliterowe i stae numeryczne s szczeglnie niewygodne, poniewa nie mona ich atwo
zlokalizowa w tekcie.
Bardzo atwo mona wyszuka cig znakw MAX_CLASSES_PER_STUDENT, ale liczba 7 moe by problematyczna. Wyszukiwanie moe zwrci cyfry z nazw plikw, innych definicji staych lub wyrae, w ktrych byy uyte do innych celw. Jeszcze gorszym przypadkiem jest uywanie dugiej staej
numerycznej, w ktrej kto przestawi cyfry, poniewa bd ten jest trudny do znalezienia przez
programist.
Podobnie nazwa e jest zym wyborem, jeeli jest uywana do zmiennej, ktr programista bdzie
chcia kiedykolwiek znale. Jest to najczciej wystpujca litera w jzyku angielskim i najprawdopodobniej zostanie wyszukana w kadym fragmencie tekstu w kadym programie. Pod tym wzgldem
dusze nazwy przewyszaj krtsze i kada dajca si wyszuka nazwa jest lepsza od staej w kodzie.

44

ROZDZIA 2.

Osobicie uywam nazw jednoliterowych WYCZNIE jako zmiennych lokalnych wewntrz


krtkich metod. Dugo nazwy powinna odpowiada rozmiarowi zasigu [N5]. Jeeli zmienna lub
staa moe by widziana lub uywana w wielu miejscach w kodzie, niezwykle wane jest nadanie jej
nazwy, ktr mona wyszuka. Kolejny raz porwnajmy:
for (int j=0; j<34; j++) {
s += (t[j]*4)/5;
}

z:
int realDaysPerIdealDay = 4;
const int WORK_DAYS_PER_WEEK = 5;
int sum = 0;
for (int j=0; j < NUMBER_OF_TASKS; j++) {
int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;
int realTaskWeeks = (realdays / WORK_DAYS_PER_WEEK);
sum += realTaskWeeks;
}

Warto zauway, e sum w powyszym przykadzie nie jest szczeglnie uyteczn nazw, ale przynajmniej daje si wyszuka. Kod z odpowiednimi nazwami jest duszy, ale zauwamy, e znacznie
atwiej znale WORK_DAYS_PER_WEEK ni wszystkie miejsca, w ktrych zostaa uyta cyfra 5,
i wrd nich te, ktre nas interesuj.

Unikanie kodowania
Korzystamy na co dzie z wielu kodowa, wic nie ma potrzeby dodawania kolejnych. Kodowanie
w nazwach typu lub informacji o zakresie powoduje zwikszenie nakadu pracy przy odszyfrowaniu. Trudno uzna za rozsdne wymaganie, aby kady nowy pracownik musia nauczy si kolejnego jzyka kodowania oprcz zapoznania si z (zwykle rozsdn) treci kodu, nad ktrym
bdzie pracowa. Jest to niepotrzebne zakcenie procesu mylenia w czasie rozwizywania
problemu. Kodowane nazwy zwykle trudno wymawia, dlatego mona si pomyli w ich wpisywaniu.

Notacja wgierska
W dawnych czasach, gdy korzystalimy z jzykw o dugociach zmiennych stanowicych powane
wyzwania, z koniecznoci naruszalimy reguy. Fortran wymusza kodowanie przez uycie pierwszej litery nazwy na kod typu. Wczesne wersje jzyka Basic pozwalay tylko na korzystanie z litery
i jednej cyfry. Notacja wgierska (NW) pozwalaa przej na cakiem nowy poziom.
NW bya uwaana za bardzo wan w API Windows C, gdy wszystko byo uchwytem cakowitym,
dugim wskanikiem lub wskanikiem void albo jedn z kilku odmian string (o rnych zastosowaniach i atrybutach). W tym czasie kompilator nie sprawdza typw, wic programici potrzebowali sposobu na ich zapamitanie.
W nowoczesnych jzykach mamy znacznie bogatszy system typw, a kompilator pamita i wymusza typy. Co wicej, istnieje trend tworzenia mniejszych klas i krtszych funkcji, aby ludzie najczciej widzieli punkt deklaracji kadej uywanej zmiennej.

ZNACZCE NAZWY

45

Programici korzystajcy z jzyka Java nie potrzebuj kodowania typw. Obiekty maj silnie wymuszane typy, a rodowiska edycyjne s na tyle zaawansowane, e wykrywaj bdy typw na dugo przed uruchomieniem kompilatora! Tak wic NW i inne sposoby kodowania typw po prostu
przeszkadzaj. Powoduj one, e trudniej zmieni nazw lub typ zmiennej, funkcji lub klasy.
Powoduj one, e trudniej czyta si kod. Dodatkowo sprawiaj, e system kodowania moe myli
czytelnika.
PhoneNumber phoneString;

// Nazwa nie zostaa zmieniona po zmianie typu!

Przedrostki skadnikw
Nie potrzebujemy ju przedrostkw m_ dla zmiennych skadowych. Nasze klasy i funkcje powinny
by na tyle mae, aby nie byy one potrzebne. Powinnimy uywa rodowiska edycyjnego, ktre
wyrnia lub koloruje skadniki, dziki czemu mona je atwo odrni.
public class Part {
private String m_dsc; // Opis tekstowy
void setName(String name) {
m_dsc = name;
}
}
_________________________________________________
public class Part {
String description;
void setDescription(String description) {
this.description = description;
}
}

Oprcz tego ludzie szybko ucz si ignorowa przedrostki (lub przyrostki), aby widzie znaczc
cz nazwy. Im czciej czytamy kod, tym mniej widzimy przedrostkw. W kocu stan si one
niezauwaalnym szumem i znacznikiem starszego kodu.

Interfejsy i implementacje
Czasami stosowane s specjalne przypadki kodowania. Zamy, e budujemy fabryk abstrakcyjn
(ang. abstract factory) do tworzenia figur. Fabryka ta bdzie interfejsem implementowanym przez
konkretn klas. Jak powinnimy j nazwa? IShapeFactory i ShapeFactory? Osobicie pozostawiam nazwy interfejsw bez dekoracji. Pocztkowe I, tak czste w istniejcej bazie kodu, jest
w najlepszym przypadku zakceniem, a w najgorszym nonikiem zbyt duej iloci informacji.
Nie chc, aby moi uytkownicy wiedzieli, e przekazuj im interfejs. Chc, aby wiedzieli, e jest to
ShapeFactory. Dlatego, jeeli konieczne jest kodowanie interfejsu lub implementacji, wybieram kodowanie implementacji. Nazwanie tej klasy ShapeFactoryImp, a nawet w fatalny sposb CShapeFactory,
jest lepsze ni kodowanie nazwy interfejsu.

46

ROZDZIA 2.

Unikanie odwzorowania mentalnego


Czytelnicy nie powinni mentalnie przeksztaca naszych nazw na inne, ktre znaj. Problem ten
wynika zwykle z powodu uycia terminw spoza domeny problemu lub domeny rozwizania.
Jest to problem z jednoliterowymi nazwami zmiennych. Oczywicie, licznik ptli moe mie nazw
i, j lub k (ale nie l!), jeeli zakres jest bardzo may i nie ma nazw pozostajcych z nim w konflikcie.
Mona przyj to rozwizanie, poniewa jednoliterowe nazwy licznikw ptli s ju tradycj. Jednak
w wikszoci kontekstw nazwy jednoliterowe s zym wyborem; s to nazwy, ktre czytelnik musi
mentalnie odwzorowa na aktualn koncepcj. Nie ma gorszego powodu uycia nazwy c ni
ten, e a i b s ju zajte.
Programici s zwykle bystrymi ludmi. Bystrzy ludzie czasami lubi pokazywa swoje moliwoci
przez demonstrowanie moliwoci mentalnych. W kocu, jeeli moemy niezawodnie zapamita,
e r jest adresem URL zapisanym maymi literami z usunit nazw hosta i schematu, to jasno wida,
e jestemy bystrzy.
Jedn z rnic pomidzy bystrymi programistami a profesjonalistami jest to, e profesjonalici rozumiej, e czytelno jest wszystkim. Profesjonalici pisz kod zrozumiay dla innych.

Nazwy klas
Klasy i obiekty powinny by rzeczownikami lub wyraeniami rzeczownikowymi, takimi jak Customer,
WikiPage, Account czy te AddressParser. Naley unika w nazwach klas sw takich jak Manager,
Processor, Data lub Info. Nazwy klas nie powinny by czasownikami.

Nazwy metod
Metody naley opatrywa nazwami bdcymi czasownikami lub wyraeniami czasownikowymi,
takimi jak postPayment, deletePage lub save. Akcesory, mutatory i predykaty powinny mie
nazwy tworzone na podstawie nazwy obsugiwanej waciwoci i by poprzedzone przedrostkiem
get, set lub is, zgodnie ze standardem javabean4.
string name = employee.getName();
customer.setName("mike");
if (paycheck.isPosted())...

Gdy konstruktory s przecione, naley uywa metod fabryk o nazwach opisujcych argumenty.
Na przykad:
Complex fulcrumPoint = Complex.FromRealNumber(23.0);

jest zwykle lepsze od


Complex fulcrumPoint = new Complex(23.0);
4

http://java.sun.com/products/javabeans/docs/spec.html

ZNACZCE NAZWY

47

Warto rozway wymuszenie stosowania tej techniki


przez zadeklarowanie odpowiedniego konstruktora
jako prywatnego.

Nie bd dowcipny
Jeeli nazwy s dowcipnymi szaradami, mog by
zapamitane wycznie przez osoby z identycznym
co autor poczuciem humoru, i tylko dopty, dopki
bd one pamita dany art sowny. Czy wszyscy bd wiedzieli, do czego suy funkcja o nazwie
HolyHandGrenade? Oczywicie, jest to dowcipne, ale w tym przypadku DeleteItems bdzie lepsz
nazw. Czytelno jest zawsze waniejsza od wrae artystycznych.
Dowcipno w kodzie czsto przybiera form kolokwializmw lub slangu. Na przykad nie naley
uywa nazwy whack(), jeeli mona kill(). Nie naley stosowa artw znanych w maych spoecznociach, takich jak eatMyShorts() zamiast abort().
Nazwa obiektu powinna odzwierciedla jego znaczenie. Dowcipne hasa zachowajmy na spotkania
z przyjacimi.

Wybieraj jedno sowo na pojcie


Naley stosowa zasad jedno sowo na jedno abstrakcyjne pojcie i trzyma si jej. Na przykad
mylce jest stosowanie nazw fetch, retrieve i get do analogicznych metod w rnych klasach.
Jak mona zapamita, ktra metoda naley do ktrej klasy? Niestety, czsto trzeba pamita, ktra
firma, grupa lub osoba napisaa bibliotek lub klas, aby zapamita, ktre nazwy zostay
uyte. W przeciwnym razie spdzimy zbyt wiele czasu na przegldaniu nagwkw i przykadw
wczeniejszego kodu.
Nowoczesne rodowiska edycyjne, takie jak Eclipse i IntelliJ, dostarczaj podpowiedzi kontekstowych,
takich jak lista metod, jakie mona wywoa na danym obiekcie. Jednak warto pamita, e listy te nie
zawieraj zwykle komentarzy, jakie napisalimy obok nazw funkcji i list parametrw. Jeeli bdziemy
mieli szczcie, bd tam nazwy parametrw z deklaracji funkcji. Nazwy funkcji musz by niezalene i spjne, aby mona byo pniej wybra waciw metod bez dodatkowych poszukiwa.
Podobnie mylce jest wystpowanie nazw controller, manager oraz driver w tej samej bazie
kodu. Jaka jest najwaniejsza rnica pomidzy DeviceManager a ProtocolController? Dlaczego obie nazwy nie s menederami lub kontrolerami? Czy naprawd obie s sterownikami? Nazwy
te pozwalaj oczekiwa, e dwa obiekty maj bardzo rne typy, jak rwnie inne klasy.
Spjny leksykon jest ogromnym uatwieniem dla programistw, ktrzy musz korzysta z naszego kodu.

48

ROZDZIA 2.

Nie twrz kalamburw!


Naley unika uywania tego samego sowa do dwch celw. Uywanie tego samego terminu dla
dwch rnych zagadnie jest wanie kalamburem.
Jeeli stosujemy zasad jedno sowo na zagadnienie, powinnimy w efekcie otrzyma wiele klas,
ktre zawieraj na przykad metod add. Jeeli lista parametrw i zwracane wartoci tych rnych
metod add odpowiadaj sobie skadniowo, wszystko jest w najlepszym porzdku.
Jednak kto moe zdecydowa, aby uy dla spjnoci sowa add, gdy faktycznie wykonywana
operacja nie jest dodawaniem. Zamy, e mamy wiele klas, w ktrych add tworzy now warto
przez dodawanie lub czenie dwch istniejcych wartoci. Zamy teraz, e piszemy now klas,
ktra posiada metod wstawiajc jeden parametr do kolekcji. Czy powinnimy nazywa t metod add?
Moe si to wydawa spjne, poniewa mamy tak duo innych metod add, ale w tym przypadku
dziaanie jest inne, wic powinnimy uy nazwy takiej jak insert lub append. Nazwanie tej nowej
metody add byoby kalamburem.
Naszym celem jako autorw powinno by uczynienie kodu tak atwego do zrozumienia, jak jest to
moliwe. Chcemy, aby nasz kod dao si szybko zrozumie, a nie intensywnie studiowa. Chcemy
uy popularnego modelu szkicowanego na kartce, w ktrym autor jest odpowiedzialny za czytelno przekazu, a nie akademickiego modelu, w ktrym zadaniem ucznia jest wydobycie wiedzy
z papierowych tomw.

Korzystanie z nazw dziedziny rozwizania


Naley pamita, e ludzie czytajcy nasz kod bd programistami. Mona wic korzysta z terminw informatycznych, nazw algorytmw, nazw wzorcw, terminw matematycznych i tak dalej.
Niekoniecznie trzeba uywa kadej nazwy z dziedziny problemu, poniewa moe to spowodowa,
e nasi wsppracownicy bd musieli wielokrotnie pyta klienta, co oznacza dana nazwa, cho
znaj ten termin pod inn nazw.
Nazwa AccountVisitor niesie ze sob wiele informacji dla kadego programisty, ktry zna wzorzec visitor. Ktry programista nie bdzie wiedzia, co to jest JobQueue? Istnieje wiele technicznych
zagadnie, ktre programici musz zna. Wybr nazwy technicznej dla tych elementw jest zwykle
najlepszym rozwizaniem.

Korzystanie z nazw dziedziny problemu


Wszdzie tam, gdzie nie istniej terminy programistyczne dla wykonywanych operacji, naley
uywa nazw z dziedziny problemu. W najgorszym przypadku programista, ktry bdzie konserwowa kod, zapyta eksperta o znaczenie nazwy.

ZNACZCE NAZWY

49

Rozdzielenie koncepcji domeny problemu i rozwizania jest jednym z zada dobrego programisty
i projektanta. Kod, ktry jest bardziej zbliony do koncepcji dziedziny problemu, powinien zawiera
nazwy z tej dziedziny.

Dodanie znaczcego kontekstu


Istnieje niewiele nazw, ktre same w sobie nios wystarczajco duo treci w wikszoci przypadkw tak nie jest. Z tego powodu konieczne jest umieszczenie nazw we waciwym kontekcie
przez wstawienie ich w odpowiednio nazwane klasy, funkcje i przestrzenie nazw. Gdy wszystko inne
zawiedzie, ostatni desk ratunku moe by zastosowanie przedrostka nazwy.
Wyobramy sobie, e mamy zmienne o nazwach firstName, lastName, street, houseNumber,
city, state oraz zipcode. Jeeli s one zgrupowane, jasne jest, e tworz adres. Co w przypadku, gdy
w metodzie zobaczymy pojedyncz zmienn state? Czy automatycznie skojarzymy j z czci
adresu?
Mona utworzy kontekst przez zastosowanie przedrostka: addrFirstName, addrLastName,
addrState i tak dalej. Dziki temu czytelnicy bd wiedzieli, e zmienne te s czci wikszej
struktury. Oczywicie, lepszym rozwizaniem jest utworzenie klasy o nazwie Address. W takim
przypadku nawet kompilator bdzie wiedzia, e zmienna jest czci wikszej caoci.
Przeanalizujmy metod z listingu 2.1. Czy zmienne wymagaj lepszego kontekstu?
Nazwa funkcji tworzy tylko cz kontekstu; algorytm zapewnia pozosta cz. Gdy zapoznamy si
z funkcj, okae si, e trzy zmienne, number, verb oraz pluralModifier, s czci komunikatu
obliczanej statystyki. Niestety, konieczne jest wywnioskowanie kontekstu. Gdy po raz pierwszy
spojrzymy na funkcj, znaczenie tych zmiennych nie bdzie jasne.
L I S T I N G 2 . 1 . Zmienne o niejasnym kontekcie
private void printGuessStatistics(char candidate, int count) {
String number;
String verb;
String pluralModifier;
if (count == 0) {
number = "no";
verb = "are";
pluralModifier = "s";
} else if (count == 1) {
number = "1";
verb = "is";
pluralModifier = "";
} else {
number = Integer.toString(count);
verb = "are";
pluralModifier = "s";
}
String guessMessage = String.format(
"There %s %s %s%s", verb, number, candidate, pluralModifier);
print(guessMessage);
}

50

ROZDZIA 2.

Funkcja jest zbyt dua, a zmienne s intensywnie uywane. Aby podzieli funkcj na mniejsze
elementy, musimy utworzy klas GuessStatisticsMessage, ktrej skadowymi bd te trzy
zmienne. Zapewnia to jasny kontekst dla zmiennych. S one jednoznacznie czci klasy Guess
StatisticsMessage. Poprawienie kontekstu powoduje rwnie, e algorytm jest znacznie
bardziej czytelny, poniewa jest podzielony na mniejsze funkcje (patrz listing 2.2).
L I S T I N G 2 . 2 . Zmienne z kontekstem
public class GuessStatisticsMessage {
private String number;
private String verb;
private String pluralModifier;
public String make(char candidate, int count) {
createPluralDependentMessageParts(count);
return String.format(
"There %s %s %s%s",
verb, number, candidate, pluralModifier );
}
private void createPluralDependentMessageParts(int count) {
if (count == 0) {
thereAreNoLetters();
} else if (count == 1) {
thereIsOneLetter();
} else {
thereAreManyLetters(count);
}
}
private void thereAreManyLetters(int count) {
number = Integer.toString(count);
verb = "are";
pluralModifier = "s";
}
private void thereIsOneLetter() {
number = "1";
verb = "is";
pluralModifier = "";
}
private void thereAreNoLetters() {
number = "no";
verb = "are";
pluralModifier = "s";
}
}

Nie naley dodawa nadmiarowego kontekstu


W hipotetycznej aplikacji o nazwie Luksusowa stacja benzynowa nie naley prefiksowa kadej
klasy skrtem LSB. Po prostu bdziemy mieli przeciwko sobie uywane narzdzia. Wpisujemy L,
naciskamy klawisz dokaczania i otrzymujemy dug na kilometr list wszystkich klas w systemie.
Czy jest to mdre? Dlaczego utrudniamy sobie prac z IDE?

ZNACZCE NAZWY

51

Zamy, e w module ksigowym LSB utworzylimy klas MailingAddress i nazwalimy j


LSBAccountAddress. Pniej potrzebujemy adresu wysyki dla aplikacji obsugujcej kontakty
z klientami. Czy uyjemy klasy LSBAccountAddress? Czy bdzie to prawidowa nazwa? Dziesi
z siedemnastu znakw to znaki nadmiarowe lub nieodpowiednie.
Krtsze nazwy s zwykle lepsze ni dusze, o ile s one jasne. Nie naley opatrywa nazwy nadmiarowym kontekstem.
Nazwy accountAddress i customerAddress s doskonae dla instancji klasy Address, ale mog
by kiepskimi nazwami dla klas. Address jest wietn nazw dla klasy. Jeeli musz odrnia adresy MAC, adresy portw i adresy WWW, mog uywa nazw PostalAddress, MAC oraz URI.
Wynikowe nazwy s bardziej precyzyjne, co jest wane.

Sowo kocowe
Wybr dobrych nazw wymaga umiejtnoci opisywania i podstaw kulturowych. Trzeba si tego
nauczy, ale nie jest to problemem techniczny, biznesowy ani organizacyjny. Niestety, wiele osb
z brany nie posiada tych umiejtnoci.
Ludzie czsto nie zmieniaj nazw elementw z obawy, e inni programici bd mieli zastrzeenia.
My nie mamy takich obaw i uwaamy, e zmiany nazw na czytelniejsze to zawsze dobry pomys.
W wikszoci przypadkw nie pamitamy nazw klas i metod. Korzystamy z nowoczesnych narzdzi,
ktre obsuguj tego typu szczegy, dziki czemu moemy skupi si na tym, by kod mona byo
czyta jak akapity i zdania, a co najmniej jak tabele i struktury danych (zdania nie zawsze nadaj si
do wywietlania danych). Prawdopodobnie zaskoczymy kogo, gdy zmienimy nazw, podobnie jak
w przypadku innych usprawnie kodu, ale nie powstrzymuje to nas przed dokonywaniem zmian.
Skorzystaj z przedstawionych tu zasad i sprawd, jak wpywaj one na czytelno tworzonego kodu.
Jeeli konserwujesz kod napisany przez kogo innego, moesz skorzysta z narzdzi refactoringu,
ktre pomagaj rozwizywa problemy zwizane ze zmian nazwy.

52

ROZDZIA 2.

ROZDZIA 3.

Funkcje

tworzylimy systemy z programw i podprogramw. NaW stpnie, w erze Fortrana i PL/1, korzystalimy
z programw, podprogramw i funkcji. Tylko funkZAMIERZCHYCH CZASACH PROGRAMOWANIA

cje przeszy prb czasu. Funkcje s pierwsz lini organizacji kadego programu. Tematem
tego rozdziau jest wanie pisanie dobrych funkcji.

53

Przeanalizujmy kod z listingu 3.1. Trudno jest znale dug funkcj w FitNesse1, ale gdy zagbiem si w kod, znalazem tak. Nie tylko jest duga, ale take zawiera powielony kod, sporo dziwnych tekstw, wiele obcych i nieoczywistych typw danych oraz API. Sprawdmy, o ile bardziej sensowna moe by po powiceniu jej trzech minut.
L I S T I N G 3 . 1 . HtmlUtil.java (FitNesse 20070619)
public static String testableHtml(
PageData pageData,
boolean includeSuiteSetup
) throws Exception {
WikiPage wikiPage = pageData.getWikiPage();
StringBuffer buffer = new StringBuffer();
if (pageData.hasAttribute("Test")) {
if (includeSuiteSetup) {
WikiPage suiteSetup =
PageCrawlerImpl.getInheritedPage(
SuiteResponder.SUITE_SETUP_NAME, wikiPage
);
if (suiteSetup != null) {
WikiPagePath pagePath =
suiteSetup.getPageCrawler().getFullPath(suiteSetup);
String pagePathName = PathParser.render(pagePath);
buffer.append("!include -setup .")
.append(pagePathName)
.append("\n");
}
}
WikiPage setup =
PageCrawlerImpl.getInheritedPage("SetUp", wikiPage);
if (setup != null) {
WikiPagePath setupPath =
wikiPage.getPageCrawler().getFullPath(setup);
String setupPathName = PathParser.render(setupPath);
buffer.append("!include -setup .")
.append(setupPathName)
.append("\n");
}
}
buffer.append(pageData.getContent());
if (pageData.hasAttribute("Test")) {
WikiPage teardown =
PageCrawlerImpl.getInheritedPage("TearDown", wikiPage);
if (teardown != null) {
WikiPagePath tearDownPath =
wikiPage.getPageCrawler().getFullPath(teardown);
String tearDownPathName = PathParser.render(tearDownPath);
buffer.append("\n")
.append("!include -teardown .")
.append(tearDownPathName)
.append("\n");
}
if (includeSuiteSetup) {
WikiPage suiteTeardown =
PageCrawlerImpl.getInheritedPage(
SuiteResponder.SUITE_TEARDOWN_NAME,
wikiPage
);
if (suiteTeardown != null) {
1

Narzdzie testujce dostpne na zasadach open source, www.fitnese.org.

54

ROZDZIA 3.

WikiPagePath pagePath =
suiteTeardown.getPageCrawler().getFullPath (suiteTeardown);
String pagePathName = PathParser.render(pagePath);
buffer.append("!include -teardown .")
.append(pagePathName)
.append("\n");
}
}
}
pageData.setContent(buffer.toString());
return pageData.getHtml();
}

Czy mona zrozumie dziaanie tej funkcji po trzech minutach analizowania? Prawdopodobnie
nie. Zbyt wiele tu si dzieje na zbyt wielu poziomach abstrakcji. Mamy tu nieznane napisy i dziwne
wywoania funkcji wymieszane z podwjnie zagniedonymi instrukcjami if kontrolowanymi
przez znaczniki.
Jednak po wyodrbnieniu kilku metod, zmianie kilku nazw i niewielkiej modyfikacji struktury byem
w stanie zawrze przeznaczenie tej funkcji w dziewiciu wierszach przedstawionych na listingu 3.2.
Czy to da si zrozumie w trzy minuty?
L I S T I N G 3 . 2 . HtmlUtil.java (zmodyfikowany)
public static String renderPageWithSetupsAndTeardowns(
PageData pageData, boolean isSuite
) throws Exception {
boolean isTestPage = pageData.hasAttribute("Test");
if (isTestPage) {
WikiPage testPage = pageData.getWikiPage();
StringBuffer newPageContent = new StringBuffer();
includeSetupPages(testPage, newPageContent, isSuite);
newPageContent.append(pageData.getContent());
includeTeardownPages(testPage, newPageContent, isSuite);
pageData.setContent(newPageContent.toString());
}
return pageData.getHtml();
}

O ile nie spdzilimy wicej czasu na studiowaniu FitNesse, prawdopodobnie nie zrozumiemy
wszystkich szczegw. Jednak mona wywnioskowa, e ta funkcja docza pewne strony konfiguracji i rozbioru do testowanej strony, a nastpnie generuje t stron w HTML-u. Czytelnik znajcy
JUnit2 zapewne zorientuje si, e funkcja ta naley do pewnego rodzaju biblioteki testujcej bazujcej na WWW. Wydzielenie tej informacji z listingu 3.2 jest dosy atwe, ale na listingu 3.1 jest
ona znacznie bardziej ukryta.
Co powoduje, e funkcje takie jak na listingu 3.2 s czytelne i zrozumiae? Co mona zrobi, by
funkcja komunikowaa swoje przeznaczenie? Jakie atrybuty moemy nada funkcjom, aby przypadkowy czytelnik mg wywnioskowa, w jakiego rodzaju programie si znajduj?

Narzdzie do testowania jednostkowego dostpne na zasadach open source dla jzyka Java, www.junit.org.

FUNKCJE

55

Mae funkcje!
Pierwsza zasada dotyczca konstruowania funkcji jest taka, e powinny by mae. Druga zasada
mwi, e powinny by mniejsze, ni s. Nie jest to zaoenie, ktre mog udowodni. Nie mog
przedstawi przykadw bada pokazujcych, e bardzo mae funkcje s lepsze. Mog jedynie powiedzie, e przez niemal czterdzieci lat napisaem funkcje o bardzo rnych rozmiarach. Napisaem
kilka paskudnych 3000-wierszowych potworw. Napisaem rwnie wiele funkcji majcych od 100
do 300 wierszy. Pisaem te funkcje majce od 20 do 30 wierszy. Po wielu prbach i bdach mog
powiedzie, e funkcje powinny by bardzo mae.
W latach osiemdziesitych ubiegego wieku panowa pogld goszcy, e funkcja powinna mieci
si na jednym ekranie. Oczywicie, mwilimy tak, gdy terminal VT100 mia 24 wiersze po 80
kolumn, a nasze edytory zabieray cztery wiersze na cele administracyjne. Obecnie, przy zastosowaniu niewielkiej czcionki i duego monitora, mona zmieci 150 znakw w wierszu i co najmniej 100
wierszy na ekranie. Wiersze jednak nie powinny mie po 150 znakw. Funkcje nie powinny mie 100
wierszy dugoci. Funkcje powinny mie wanie nie wicej ni 20 wierszy.
Jak krtkie powinny by funkcje? W 1999 roku odwiedziem Kenta Becka w jego domu w Oregonie. Kent pokaza mi wwczas may program Java/Swing, ktry nazwa Sparkle. Tworzy on efekty
graficzne bardzo podobne do tych, jakie widzielimy przy rdce wrki w filmie Kopciuszek. Gdy
przesuwaem wskanik myszy, cign on za sob iskrzcy si lad, a iskry te, poddawane symulowanemu polu grawitacyjnemu, opaday na d okna. Gdy Kent pokaza mi kod realizujcy ten
efekt, byem zaszokowany wszystkie funkcje byy niezwykle krtkie. Byem przyzwyczajony do
tego, e funkcje w programach Swing miay cae kilometry dugoci. Kada funkcja w tym programie miaa dwa, trzy lub cztery wiersze dugoci. Kada bya przejrzysta i oczywista. Kada miaa do
opowiedzenia jak histori. Kada prowadzia do nastpnej we wzorowym porzdku. Tak krtkie
powinny by funkcje w naszych programach3!
A zatem, jakie rozmiary powinny mie tworzone przez nas funkcje? Powinny by zwykle krtsze
ni ta z listingu 3.2! Kod z listingu 3.2 powinien by skrcony do postaci z listingu 3.3.
L I S T I N G 3 . 3 . HtmlUtil.java (ponownie zmodyfikowany)
public static String renderPageWithSetupsAndTeardowns(
PageData pageData, boolean isSuite) throws Exception {
if (isTestPage(pageData))
includeSetupAndTeardownPages(pageData, isSuite);
return pageData.getHtml();
}

Pytaem Kenta, czy ma jeszcze ten plik, ale nie mg go znale. Przeszukiwaem rwnie moje stare komputery, ale nie
znalazem go. Program pozosta wic tylko w mojej pamici.

56

ROZDZIA 3.

Bloki i wcicia
Z powyszych zaoe wynika, e bloki w instrukcjach if, else, while i podobnych powinny mie
po jednym wierszu. Prawdopodobnie wiersze te powinny by wywoaniem funkcji. Nie tylko pozwala to zachowa niewielk objto caej funkcji, ale rwnie ma warto dokumentacyjn, poniewa funkcja wywoana w bloku moe mie sugestywn, opisow nazw.
Oznacza to, e funkcja nie moe by na tyle dua, aby zawieraa zagniedone struktury. Dlatego
poziom wci w funkcji nie powinien przekracza dwch. Dziki temu funkcje s czytelniejsze
i bardziej zrozumiae.

Wykonuj jedn czynno


Kod z listingu 3.1 wykonuje kilka czynnoci: tworzy bufory, pobiera strony, wyszukuje strony dziedziczone, generuje cieki, docza tajemnicze napisy i generuje kod HTML. Ogrom rnych
czynnoci tworzy wielki baagan. Z drugiej strony, kod z listingu 3.3
wykonuje jedn prost operacj. Docza strony konfiguracyjne
i rozbioru do testowanych stron.
Ponisza porada pojawia si w takiej lub innej formie ju co
najmniej od 30 lat.
FUNKCJE POWINNY WYKONYWA JEDN OPERACJ. POWINNY ROBI TO DOBRZE.
POWINNY ROBI TYLKO TO.
W przypadku tej porady problem polega na tym, e nie wiadomo, co to jest jedna operacja. Czy
kod z listingu 3.3 wykonuje jedn operacj? atwo wykaza, e wykonywane s trzy czynnoci:
1. Sprawdzenie, czy strona jest stron testow.
2. Jeeli tak, doczenie stron konfiguracyjnych i rozbioru.
3. Wygenerowanie strony w HTML-u.
Jak to w kocu jest? Czy ta funkcja wykonuje jedn operacj, czy trzy? Naley zwrci uwag, e te
trzy kroki z funkcji znajduj si o jeden poziom abstrakcji poniej zadeklarowanego przez nazw
funkcji. Moemy opisa nasz funkcj za pomoc krtkiego akapitu OTO4:
OTO RenderPageWithSetupsAndTeardowns, sprawdzamy w niej, czy strona jest stron testow,
i jeeli tak, doczamy konfiguracj i rozbir. W kadym z przypadkw generujemy stron
w HTML-u.

W jzyku programowania LOGO uywano sowa kluczowego OTO do tych samych celw, do jakich jzyki Ruby i Python
korzystay ze sowa def. Kada funkcja zaczynaa si od sowa OTO. Ma to interesujcy wpyw na sposb projektowania
funkcji.

FUNKCJE

57

Jeeli funkcja wykonuje tylko operacje znajdujce si o jeden poziom poniej zadeklarowanej nazwy funkcji, to wykonuje ona jedn operacj. W kocu powodem pisania funkcji jest dekompozycja duych zagadnie (inaczej mwic, nazwy funkcji) na zbir operacji znajdujcych si na niszych poziomach abstrakcji.
Z listingu 3.1 powinno jasno wynika, e zawiera on operacje z wielu rnych poziomw abstrakcji. Wynika z tego, e funkcja wykonuje wicej ni jedn operacj. Nawet na listingu 3.2
znajduj si dwa poziomy abstrakcji, co pozwolio nam na jego zmniejszenie. Jednak bardzo
trudno jest sensownie skrci listing 3.3. Moemy wyodrbni instrukcj if do funkcji o nazwie
includeSetupsAndTeardownsIfTestPage, ale to zmienia tylko struktur kodu, a nie liczb poziomw abstrakcji.
Tak wic innym sposobem sprawdzenia, czy funkcja wykonuje tylko jedn operacj, jest prba
wyodrbnienia z niej innej funkcji, ktra nie jest tylko reorganizacj jej implementacji [G34].

Sekcje wewntrz funkcji


Spjrzmy na listing 4.7 z nastpnego rozdziau. Naley zwrci uwag, e funkcja generatePrimes
jest podzielona na sekcje, takie jak deklaracje, inicjalizacja oraz sito. Jest to oczywisty symptom wykonywania wicej ni jednej operacji. Funkcji wykonujcej jedn operacj nie mona w rozsdny
sposb podzieli na sekcje.

Jeden poziom abstrakcji w funkcji


Aby upewni si, e nasze funkcje wykonuj jedn operacj, musimy sprawdzi, czy instrukcje
w funkcji s na tym samym poziomie abstrakcji. atwo zauway, e na listingu 3.1 regua ta nie jest
zachowana. S tu koncepcje znajdujce si na wysokim poziomie abstrakcji, takie jak getHTML(); inne
na rednim poziomie abstrakcji, takie jak String pagePathName = PathParser.render(pagePath);
jeszcze inne na wyranie niskim poziomie, takie jak append("\n").
Mieszanie poziomw abstrakcji w jednej funkcji zawsze jest mylce. Czytelnicy mog mie
problemy z rozpoznaniem, czy okrelone wyraenie jest wanym zagadnieniem, czy mao istotnym
szczegem. Co gorsza, gdy szczegy wymieszaj si z wanymi koncepcjami, rodzi si pokusa dodawania do funkcji coraz wikszej liczby szczegw.

Czytanie kodu od gry do dou zasada zstpujca


Chcemy, aby kod mona byo czyta od gry do dou5. Chcemy, aby po kadej funkcji znajdowaa
si kolejna, na nastpnym poziomie abstrakcji, dziki czemu mona czyta program, schodzc
o jeden poziom abstrakcji niej wraz z przejciem do kolejnej funkcji w pliku. Nazywam to zasad
zstpujc.

[KP78].

58

ROZDZIA 3.

Inaczej mwic, chcemy, aby mona byo czyta program tak, jakby by zbiorem akapitw ABY,
z ktrych kady opisuje biecy poziom abstrakcji odwoujcych si do kolejnych akapitw ABY
na nastpnym poziomie.
Aby doczy konfiguracje i rozbiory, doczamy konfiguracj, nastpnie zawarto strony
testowej, po czym rozbiory.
Aby doczy konfiguracj, doczamy konfiguracj zestawu, jeeli jest to zestaw, a nastpnie
zwyk konfiguracj.
Aby doczy konfiguracj zestawu, wyszukujemy w hierarchii nadrzdnej stron SuiteSetUp
i dodajemy instrukcj include ze ciek do tej strony.
Aby przeszuka hierarchi nadrzdn
Praktyka pokazuje, e programici maj problemy z przyswojeniem sobie tej reguy i pisaniem
funkcji dziaajcych na jednym poziomie abstrakcji. Programista, ktry chce by profesjonalist,
powinien jednak wypracowa sobie nawyk tworzenia krtkich funkcji, ktre na pewno wykonuj
jedn operacj. Konstruowanie kodu w taki sposb, aby mona byo czyta go od gry do dou jak
seri akapitw ABY, jest efektywn technik zachowywania spjnoci poziomu abstrakcji.
Zwrmy uwag na listing 3.7, znajdujcy si na kocu tego rozdziau. Zawiera on ca funkcj
testableHtml przebudowan zgodnie z opisanymi tu zasadami. Warto zwrci uwag, e kada
funkcja wprowadza nastpny poziom abstrakcji i kada pozostaje w obrbie jednego poziomu.

Instrukcje switch
Trudno jest napisa ma instrukcj switch6. Nawet jeeli zawiera ona tylko dwa przypadki, jest
wiksza ni oczekiwana przeze mnie wielko jednego bloku kodu lub funkcji. Rwnie trudno jest
napisa instrukcj switch wykonujc jedn operacj. Ze swojej natury instrukcje switch zawsze
wykonuj n operacji. Niestety, nie zawsze mona unikn stosowania instrukcji switch, ale moemy zapewni, e kada z nich jest umieszczona w niskopoziomowej klasie i nigdy nie jest powtarzana. Moemy to zrobi przy wykorzystaniu polimorfizmu.
Rozwamy kod z listingu 3.4. Mamy tu tylko jedn operacj, ktra moe zalee od typu pracownika.
L I S T I N G 3 . 4 . Payroll.java
public Money calculatePay(Employee e)
throws InvalidEmployeeType {
switch (e.type) {
case COMMISSIONED:
return calculateCommissionedPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return calculateSalariedPay(e);

Oczywicie dotyczy to rwnie acuchw if-else.

FUNKCJE

59

default:
throw new InvalidEmployeeType(e.type);
}
}

Z funkcj t wie si kilka problemw. Po pierwsze, jest dua, a po dodaniu kolejnego typu pracownika uronie jeszcze bardziej. Po drugie, jasne jest, e wykonuje wicej ni jedn operacj. Po
trzecie, narusza zasad pojedynczej odpowiedzialnoci7 (SRP), poniewa istnieje wicej ni jeden
powd uzasadniajcy zmian. Po czwarte, narusza zasad otwarty-zamknity8 (OCP), poniewa
musi si zmienia przy kadym dodaniu nowego typu. Jednak najprawdopodobniej najwikszym
problemem w przypadku tej funkcji jest to, e istnieje dua liczba innych funkcji majcych tak
sam struktur. Moemy mie na przykad:
isPayday(Employee e, Date date),

lub
deliverPay(Employee e, Money pay),

i wiele innych. Wszystkie te funkcje bd miay tak sam niewaciw struktur.


Rozwizaniem tego problemu jest ukrycie instrukcji switch w podstawie fabryki abstrakcyjnej9
(patrz listing 3.5), co spowoduje, e nigdy ju jej nie zobaczymy. Fabryka uyje instrukcji switch
do tworzenia odpowiednich obiektw typw bazujcych na Employee, a rne funkcje, takie jak
calculatePay, isPayday czy deliverPay, bd rozmieszczone polimorficznie przez interfejs
Employee.
Moj naczeln zasad dotyczc stosowania instrukcji switch jest tolerowanie wycznie jednego
wystpienia podczas tworzenia obiektw polimorficznych i ich ukrywanie w relacji dziedziczenia,
aby pozostae czci systemu ich nie widziay [G23]. Oczywicie kady przypadek jest inny i zdarza si,
e naruszam t zasad w jednym lub kilku miejscach.
L I S T I N G 3 . 5 . Pracownicy i fabryka
public abstract class Employee {
public abstract boolean isPayday();
public abstract Money calculatePay();
public abstract void deliverPay(Money pay);
}
----------------public interface EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
----------------public class EmployeeFactoryImpl implements EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {

a) http://en.wikipedia.org/wiki/Single_responsibility_principle
b) http://www.objectmentor.com/resources/articles/srp.pdf

a) http://en.wikipedia.org/wiki/Open/closed_principle
b) http://www.objectmentor.com/resources/articles/ocp.pdf

[GOF].

60

ROZDZIA 3.

switch (r.type) {
case COMMISSIONED:
return new CommissionedEmployee(r) ;
case HOURLY:
return new HourlyEmployee(r);
case SALARIED:
return new SalariedEmploye(r);
default:
throw new InvalidEmployeeType(r.type);
}
}
}

Korzystanie z nazw opisowych


Na listingu 3.7 zmienilimy nazw naszej przykadowej funkcji z testableHtml na SetupTear
downIncluder.render. Jest to lepsza nazwa, poniewa dokadniej opisuje przeznaczenie funkcji.
Kadej z metod prywatnych nadaem rwnie opisow nazw, tak jak isTestable czy include
SetupAndTeardownPages. Trudno nie docenia wartoci dobrych nazw. Warto zapamita
zasad Warda: Wiemy, e pracujemy na czystym kodzie, jeeli kada procedura okazuje si tak,
jakiej si spodziewalimy. Poow sukcesu w osigniciu tego stanu jest wybr dobrych nazw dla
maych funkcji wykonujcych jedn operacj. Im mniejsze i lepiej ukierunkowane s funkcje, tym
atwiej wybra dla nich opisow nazw.
Nie naley obawia si konstruowania dugich nazw. Duga opisowa nazwa jest lepsza ni krtka
enigmatyczna. Duga opisowa nazwa jest lepsza ni dugi opisowy komentarz. Naley uy konwencji nazewnictwa, ktra umoliwia atwe odczytywanie wielu sw w nazwie funkcji, a nastpnie
wykorzystanie tych wielu sw do nadania funkcji nazwy, ktra informuje o przeznaczeniu funkcji.
Czas powicony na wybr odpowiedniej nazwy jest dobr inwestycj. Warto wyprbowa rne
nazwy i przeczyta kod korzystajcy z kadej z nich. Nowoczesne rodowiska IDE, takie jak Eclipse
lub IntelliJ, znacznie uatwiaj zmian nazwy. Warto uy tych mechanizmw i wyprbowa rne
nazwy.
Wybr opisowych nazw pozwala na wyjanienie projektu moduu i pomaga w jego usprawnianiu.
Czsto zdarza si, e polowanie na dobr nazw skutkuje zmian struktury kodu na lepsz.
Nazwy powinny by spjne. W funkcjach naley uywa tych samych fraz, rzeczownikw i czasownikw, jakie wybralimy dla moduw. Jako przykad wemy nazwy includeSetupAndTear
downPages, includeSetupPages, includeSuiteSetupPage i includeSetupPage. Podobna
frazeologia w tych samych nazwach pozwala na utworzenie sekwencji opowiadajcej histori. Faktycznie, jeeli zobaczymy powysz sekwencj, zapytamy si: Co si dzieje w includeTeardownPages,
includeSuiteTeardownPage oraz includeTeardownPage?. Odpowied powinna brzmie:
W wikszoci to, czego si spodziewamy.

FUNKCJE

61

Argumenty funkcji
Idealn liczb argumentw dla funkcji jest zero (funkcja bezargumentowa). Nastpnie mamy jeden (jednoargumentowa) i dwa
(dwuargumentowa). Naley unika konstruowania funkcji
o trzech argumentach (trzyargumentowych). Wicej ni trzy
argumenty (funkcja wieloargumentowa) wymagaj specjalnego uzasadnienia a nawet wtedy takie funkcje nie powinny
by stosowane.
Argumenty s kopotliwe. Wymagaj one uycia sporej iloci
energii koncepcyjnej. Z tego powodu usun niemal wszystkie
argumenty z naszego przykadu. Wemy jako przykad String
Buffer. Moemy przekazywa ten obiekt jako argument
zamiast przechowywa go jako zmienn instancyjn, ale nasi
czytelnicy bd musieli go interpretowa za kadym razem, gdy go zobacz. Gdy czytamy histori,
jak opowiada modu, includeSetupPage() jest atwiejsze do zrozumienia ni includeSetup
PageInto(newPageContent). Argumenty znajduj si na innym poziomie abstrakcji ni funkcje
i wymusza to zapoznanie si ze szczegami (inaczej mwic, ze StringBuffer), co nie jest w tym
miejscu szczeglnie istotne.
Argumenty s jeszcze bardziej kopotliwe z punktu widzenia testowania. Trudno napisa wszystkie testy jednostkowe zapewniajce, e wszystkie kombinacje argumentw bd dziaay prawidowo. Jeeli nie ma argumentw, jest to bardzo proste. Jeeli mamy jeden argument, nie jest to zbyt
trudne. Przy dwch argumentach problem staje si bardziej kopotliwy. Jeeli mamy wicej ni dwa
argumenty, testowanie kadej kombinacji odpowiednich wartoci moe by nuce.
Argumenty wyjciowe s trudniejsze do zrozumienia ni argumenty wejciowe. Gdy czytamy funkcje, zwykle zakadamy, e dane wchodz do funkcji przez argumenty i wychodz z funkcji poprzez
zwracan warto. Zwykle nie oczekujemy, e dane bd wychodziy z funkcji przez argumenty.
Z tego powodu argumenty wyjciowe powoduj czsto konieczno powtarzania analizy.
Jeden argument jest prawie tak dobry jak brak argumentw. Funkcja SetupTeardownIncluder.
render(pageData) jest bardzo atwa do zrozumienia. Jasne jest, e mamy zamiar wygenerowa
dane z obiektu pageData.

Czsto stosowane funkcje jednoargumentowe


Istniej dwa czsto spotykane powody przekazania jednego argumentu do funkcji. Moemy zadawa
pytanie na temat tego argumentu, tak jak w boolean fileExists("MyFile"). Mona rwnie
operowa na argumencie, przeksztacajc go w co innego i zwracajc wynik. Na przykad InputStream
fileOpen("NazwaPliku") powoduje przeksztacenie nazwy pliku typu String na zwracany
obiekt InputStream. S to dwa zastosowania, jakich czytelnik oczekuje od funkcji. Naley uywa nazw, ktre jasno definiuj dziaanie, i zawsze uywa tych postaci w spjny sposb (patrz
Rozdzielanie polece i zapyta w dalszej czci rozdziau).
62

ROZDZIA 3.

Nieco rzadziej spotykan, ale rwnie przydatn odmian funkcji jednoargumentowej jest zdarzenie.
W tej postaci funkcja posiada argument wejciowy, ale nie ma wyjciowego. W programie interpretuje si takie funkcje jako wywoania zdarze i wykorzystuje argumenty do zmiany stanu systemu, na przykad void passwordAttemptFailedNtimes(int attempts). Nie naley naduywa
funkcji w tej postaci. Czytelnik musi by jasno poinformowany, e jest to zdarzenie. Trzeba uwanie
wybiera nazw i kontekst.
Naley unika funkcji jednoargumentowych, ktre nie nale do adnej z tych postaci, na przykad
void includeSetupPageInto(StringBuffer pageText). Uycie argumentu wyjciowego zamiast zwracanej wartoci dla transformacji jest mylce. Jeeli funkcja ma przeksztaca argument
wejciowy, wynik tej transformacji powinien by wartoci zwracan. Faktycznie, StringBuffer
transform(StringBuffer in) jest lepsza ni void transform(StringBuffer out), nawet jeeli
w implementacji pierwszej funkcji po prostu zwracamy argument wejciowy. Przynajmniej ma to
posta transformacji.

Argumenty znacznikowe
Argumenty znacznikowe s brzydkie. Przekazanie wartoci boolean do funkcji jest naprawd niepraktyczne. Od razu komplikuje to sygnatur metody, wyranie informujc, e funkcja wykonuje
wicej ni jedn operacj. Wykonuje jedn operacj, jeeli znacznik ma warto true, i inn, jeeli
znacznik ma warto false!
W funkcji z listingu 3.7 nie miaem wyboru, poniewa wywoujcy przesyali ju wczeniej znacznik, a ja chciaem ograniczy zakres przebudowy do tej funkcji i kolejnych. Nadal funkcja ta jest
wywoywana jako render(true), co po prostu myli biednego czytelnika. Zobaczenie sygnatury
funkcji, render(boolean isSuite), niewiele pomaga. Powinnimy podzieli t funkcj na dwie:
renderForSuite() oraz renderForSingleTest().

Funkcje dwuargumentowe
Funkcja z dwoma argumentami jest trudniejsza do zrozumienia ni jednoargumentowa. Na przykad writeField(name) jest atwiejsza do zrozumienia ni writeField(output-Stream, name)10.
Mimo e znaczenie obu jest jasne, tylko pierwsza wyranie informuje o swoim przeznaczeniu.
Druga wymaga krtkiego zastanowienia (trzeba zignorowa pierwszy parametr). A to oczywicie
w kocu spowoduje powstanie problemw nigdy nie powinnimy ignorowa adnej czci kodu.
W ignorowanych przez nas czciach zwykle kryj si bdy.
Istniej oczywicie przypadki, w ktrych dwa argumenty s odpowiednie. Na przykad Point p = new
Point(0,0); jest zupenie sensowne. Punkty kartezjaskie naturalnie korzystaj z dwch argumentw. W rzeczywistoci bybym bardzo zaskoczony, jeeli zobaczybym wywoanie new Point(0).

10

Wanie skoczyem przebudow moduu, ktry korzysta z funkcji dwuargumentowych. Byem w stanie zdefiniowa
jako pole klasy i zamieni wszystkie wywoania writeField na jednoargumentowe. Wynik by znacznie
bardziej czytelny.
outputString

FUNKCJE

63

Jednak dwa argumenty s w tym przypadku uporzdkowanymi skadnikami jednej wartoci! Z kolei
outputStream oraz name nie s naturalnie skojarzone ani nie maj naturalnego uporzdkowania.
Nawet oczywiste funkcje dwuargumentowe, takie jak assertEquals(expected, actual), s
problematyczne. Ile razy umiecie warto w actual, a powinna ona znajdowa si w expected?
Te dwa argumenty nie maj naturalnego uporzdkowania. Uporzdkowanie expected, actual
jest konwencj, ktra wymaga nauki i praktyki.
Funkcje dwuargumentowe nie s ze i oczywicie trzeba je pisa. Jednak powinnimy wiedzie, e s
one kosztowne, i zna dostpne mechanizmy pozwalajce na ich konwersj na jednoargumentowe.
Na przykad mona przenie metod writeField do klasy outputStream, co upraszcza wywoanie
do outputStream.writeField(name). Mona rwnie umieci outputStream w zmiennej skadowej
biecej klasy, dziki czemu nie trzeba jej przekazywa. Mona te wyodrbni now klas, na
przykad FieldWriter, ktra oczekuje outputStream w konstruktorze i posiada metod write.

Funkcje trzyargumentowe
Funkcje oczekujce trzech argumentw s znacznie trudniejsze do zrozumienia ni dwuargumentowe. Problemy z kolejnoci i ignorowaniem s znacznie wiksze. Warto dogbnie przemyle
problem przed utworzeniem funkcji trzyargumentowej.
Jako przykad wemy czsto stosowan przecion wersj assertEquals, ktra oczekuje trzech
argumentw: assertEquals(message, expected, actual). Ile razy odczytae message, mylc,
e jest to warto expected? Osobicie wiele razy potykaem si na tej wanie funkcji trzyargumentowej. W rzeczywistoci za kadym razem, gdy j widz, musz uczy si ignorowa parametr message.
Z drugiej strony, istnieje funkcja trzyargumentowa, ktra nie jest tak podstpna: assertEquals(1.0,
amount, .001). Cho nadal wymaga ona duszego zastanowienia, to jednak warto o niej wspomnie. Zawsze trzeba pamita, e rwno wartoci zmiennoprzecinkowych jest rzecz wzgldn.

Argumenty obiektowe
Gdy wydaje si, e funkcja wymaga wicej ni dwch lub trzech argumentw, najprawdopodobniej
niektre z nich powinny by umieszczone w osobnej klasie. Przeanalizujmy rnice pomidzy nastpujcymi deklaracjami:
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);

Zmniejszenie liczby argumentw przez utworzenie z nich obiektw moe wydawa si oszustwem,
ale tak nie jest. Gdy grupy zmiennych s przesyane wsplnie, tak jak x oraz y w powyszym przykadzie, najprawdopodobniej s czci obiektu, ktry zasuguje na wasn nazw.

64

ROZDZIA 3.

Listy argumentw
Czasami chcemy przekaza do funkcji rn liczb argumentw. Jako przykad wemy metod
String.format:
String.format("%s pracowa %.2f godzin.", name, hours);

Jeeli wszystkie argumenty bd traktowane w jednakowy sposb, tak jak w powyszym przykadzie, to s one odpowiednikiem jednego argumentu typu List. Stosujc takie wnioskowanie, mona
powiedzie, e String.format jest faktycznie metod dwuargumentow. W rzeczywistoci zamieszczona poniej deklaracja String.format jest dwuargumentowa.
public String format(String format, Object... args)

Tak wic obowizuj te same reguy. Funkcje z argumentami mog by jednoargumentowe, dwuargumentowe, a nawet trzyargumentowe. Jednak stosowanie wikszej liczby argumentw jest pomyk.
void monad(Integer... args);
void dyad(String name, Integer... args);
void triad(String name, int count, Integer... args);

Czasowniki i sowa kluczowe


Odpowiednia nazwa funkcji moe w znacznym stopniu wyjania jej przeznaczenie oraz porzdek
i przeznaczenie argumentw. W przypadku funkcji jednoargumentowych funkcja i argument mog
tworzy uyteczn par czasownik-rzeczownik. Na przykad zapis write(name) jest oczywisty. Niezalenie od tego, czym jest name (nazwa), jest to poddawane operacji write (zapis). Jeszcze lepsz nazw moe by writeField(name), z ktrej jednoznacznie wynika, e zapisywana jest nazwa pola.
Jest to przykad nazwy funkcji ze sowem kluczowym. Przy uyciu tej postaci kodujemy nazwy argumentw w nazwie funkcji. Na przykad assertEquals moe by lepiej nazwane jako
assertExpectedEqualsActual(expected, actual). W znacznym stopniu ogranicza to problem
koniecznoci pamitania kolejnoci argumentw.

Unikanie efektw ubocznych


Efekty uboczne s kamstwem. Funkcja obiecuje, e wykonuje jedn operacj, ale oprcz tego realizuje inn w sposb ukryty. Czasami w niespodziewany sposb modyfikuje zmienn ze swojej klasy.
Czasami zmienia parametry przekazane do funkcji lub globalne zmienne systemowe. W obu przypadkach s to nieodpowiedzialne i niszczce operacje, ktre czsto powoduj sprzenia i zalenoci
czasowe.
Jako przykad wemy pozornie niewinn funkcj z listingu 3.6. Korzysta ona ze standardowego algorytmu do dopasowania nazwy uytkownika i jego hasa. Zwraca ona true, jeeli podane wartoci
pasuj, i false, jeeli co pjdzie le. Ma ona rwnie efekt uboczny. Czy moesz go znale?

FUNKCJE

65

L I S T I N G 3 . 6 . UserValidator.java
public class UserValidator {
private Cryptographer cryptographer;
public boolean checkPassword(String userName, String password) {
User user = UserGateway.findByName(userName);
if (user != User.NULL) {
String codedPhrase = user.getPhraseEncodedByPassword();
String phrase = cryptographer.decrypt(codedPhrase, password);
if ("Haso prawidowe".equals(phrase)) {
Session.initialize();
return true;
}
}
return false;
}
}

Efektem ubocznym jest oczywicie wywoanie Session.initialize(). Funkcja checkPassword,


zgodnie ze swoj nazw, powinna sprawdza haso. Nazwa nie informuje o tym, e jest inicjalizowana sesja. Wywoujcy, ktry wierzy w to, co mwi nazwa funkcji, naraa si na ryzyko usunicia
istniejcych danych sesji przy sprawdzeniu hasa uytkownika.
Ten efekt uboczny wywouje sprzenie czasowe. Powoduje ono, e checkPassword moe by
wywoana w okrelonym momencie (inaczej mwic, gdy mona bezpiecznie zainicjowa sesj).
Jeeli jest wywoana w niewaciwym momencie, wszystkie dane sesji zostan utracone. Sprzenia
czasowe s mylce, szczeglnie jeeli s ukryte w efektach ubocznych. Jeeli konieczne jest uycie
sprzenia czasowego, naley jasno to przedstawi w nazwie funkcji. W tym przypadku wymaga to
zmiany nazwy funkcji na checkPasswordAndInitializeSession, cho powoduje to naruszenie
zasady wykonywania jednej operacji.

Argumenty wyjciowe
Argumenty s w naturalny sposb interpretowane jako dane wejciowe do funkcji. Jeeli kto programuje wicej ni kilka lat, to jestem pewien, e dwa razy duej zajmuje mu zrozumienie argumentw wyjciowych. Na przykad:
appendFooter(s);

Czy ta funkcja docza s jako stopk do czego? Czy docza stopk do s? Czy s jest wejciem, czy
wyjciem? Konieczne jest szybkie sprawdzenie sygnatury funkcji:
public void appendFooter(StringBuffer report)

Pozwala to rozwiza problem, ale tylko kosztem sprawdzenia deklaracji funkcji. Wszystko, co
wymusza na nas sprawdzenie sygnatury funkcji, jest odpowiednikiem dwukrotnego sprawdzania.
Jest to przerwa w rozumowaniu, ktrej naley unika.
W czasach przed programowaniem obiektowym nieraz niezbdne byo korzystanie z argumentw
wyjciowych. Jednak wikszo przyczyn wykorzystywania argumentw wyjciowych znikna
w jzykach obiektowych, poniewa zmienna this jest przewidziana do tego, aby dziaaa jak

66

ROZDZIA 3.

argument wyjciowy. Inaczej mwic, znacznie lepiej byoby, aby funkcja appendFooter bya wywoywana jako
report.appendFooter();

Zwykle daje si unikn stosowania parametrw wyjciowych. Jeeli funkcja musi zmienia stan
czegokolwiek, powinna zmienia stan wasnego obiektu.

Rozdzielanie polece i zapyta


Funkcje powinny co wykonywa lub odpowiada na jakie pytanie, ale nie powinny robi tych
dwch operacji jednoczenie. Nasza funkcja powinna zmienia stan obiektu lub zwraca pewne
informacje na temat tego obiektu. Wykonywanie obu tych operacji czsto prowadzi do pomyek.
Jako przykad wemy nastpujc funkcj:
public boolean set(String attribute, String value);

Ustawia ona warto nazwanego atrybutu i zwraca true w przypadku powodzenia i false, jeeli
taki atrybut nie istnieje. Powoduje to powstanie dziwnych instrukcji, takich jak:
if (set("username", "unclebob"))...

Przeanalizujmy je z punktu widzenia czytelnika. Co to oznacza? Czy sprawdzamy, czy atrybut


username mia wczeniej warto unclebob? Czy te sprawdzamy, czy udao si ustawi atrybut
username na warto unclebob? Trudno wywnioskowa znaczenie z wywoania, poniewa nie jest
jasne, czy sowo set jest czasownikiem, czy przymiotnikiem.
Autor mia zamiar, aby set byo czasownikiem, ale w kontekcie instrukcji if mamy wraenie, e
jest to przymiotnik. Z tego powodu czytamy ten kod nastpujco: jeeli atrybut username mia
wczeniej warto unclebob, a nie ustawiamy warto atrybutu username na unclebob i jeeli
to zadziaa, to. Moemy sprbowa rozwiza problem przez zmian nazwy funkcji set na
setAndCheckIfExists, ale nie poprawia to czytelnoci instrukcji if. Lepszym rozwizaniem jest
oddzielenie polecenia od zapytania, dziki czemu niejasno nie wystpuje.
if (attributeExists("username")) {
setAttribute("username", "unclebob");
...
}

Stosowanie wyjtkw zamiast zwracania kodw bdw


Zwracanie kodw bdw z funkcji polece jest subtelnym naruszeniem rozdzielenia polece i zapyta. Promuje to stosowanie polece jako wyrae w warunkach instrukcji if.
if (deletePage(page) == E_OK)

Nie powoduje to problemw zwizanych z myleniem czasownika z przymiotnikiem, ale prowadzi


do powstania gboko zagniedonych struktur. Gdy zwracamy kod bdu, powodujemy konieczno
jego natychmiastowej obsugi przez wywoujcego.

FUNKCJE

67

if (deletePage(page) == E_OK) {
if (registry.deleteReference(page.name) == E_OK) {
if (configKeys.deleteKey(page.name.makeKey()) == E_OK){
logger.log("strona usunita");
} else {
logger.log("configKey nie zosta usunity");
}
} else {
logger.log("deleteReference w rejestrze nieudane");
}
} else {
logger.log("usuwanie nieudane");
return E_ERROR;
}

Z drugiej strony, jeeli uyjemy wyjtkw zamiast zwracania kodw bdw, to przetwarzanie bdu
moe by oddzielone od prawidowej cieki wykonania kodu i przez to uproszczone:
try {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
catch (Exception e) {
logger.log(e.getMessage());
}

Wyodrbnienie blokw try-catch


Bloki try-catch naruszaj struktur kodu i mieszaj przetwarzanie bdw ze zwykym przetwarzaniem. Z tego powodu warto wyodrbnia tre blokw try i catch do osobnych funkcji.
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
}
catch (Exception e) {
logError(e);
}
}
private void deletePageAndAllReferences(Page page) throws Exception {
deletePage(page);
registry.deteleReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
private void logError(Exception e){
logger.log(e.getMessage());
}

W powyszym przypadku funkcja delete jest przeznaczona do obsugi bdw. atwo j zrozumie, a nastpnie zignorowa. Funkcja deletePageAndAllReferences jest odpowiedzialna za
przetwarzanie penego usuwania obiektu page. Obsuga bdw moe by zignorowana. Zapewnia
to eleganck separacj, ktra pozwala na atwiejsze zrozumienie i modyfikowanie kodu.

68

ROZDZIA 3.

Obsuga bdw jest jedn operacj


Funkcje powinny wykonywa jedn operacj. Obsuga bdw jest jedn operacj. Dlatego funkcja
obsugi bdw nie powinna wykonywa nic innego. Powoduje to (jak w powyszym przykadzie),
e jeeli w funkcji istnieje sowo kluczowe, try powinno by pierwszym sowem w funkcji i nie
powinno si w niej znajdowa nic poza blokami catch i finally.

Przyciganie zalenoci w Error.java


Zwracanie bdw zwykle powoduje, e istnieje wiele klas lub typw wyliczeniowych, w ktrych s
zdefiniowane kody bdw.
public enum Error {
OK,
INVALID,
NO_SUCH,
LOCKED,
OUT_OF_RESOURCES,
WAITING_FOR_EVENT;
}

Tego typu klasy s magnesami zalenoci wiele klas musi je importowa i z nich korzysta. Dlatego w momencie zmiany typu wyliczeniowego Error wszystkie te klasy musz by ponownie
skompilowane i zainstalowane11. Skutkuje to tym, e programici nie chc dodawa nowych bdw,
poniewa musz wszystko przekompilowa i instalowa. Dlatego korzystaj ze starych kodw
bdw, zamiast dodawa nowe.
Gdy korzystamy z wyjtkw zamiast kodw bdw, nowe wyjtki dziedzicz po klasie wyjtku.
Mog by one dodawane bez koniecznoci ponownej kompilacji i instalacji12.

Nie powtarzaj si13


Gdy uwanie przeanalizujemy kod z listingu 3.1, zauwaymy, e jest to algorytm powtarzajcy si cztery razy,
kolejno dla przypadkw SetUp, SuiteSetUp, TearDown
oraz SuiteTearDown. Nieatwo zauway to powtrzenie, poniewa te cztery przypadki s wymieszane z innym kodem i nie s powielone w identyczny sposb.
Powtarzanie to jest problemem, poniewa powoduje
rozdcie kodu i wymaga czterokrotnej modyfikacji
w przypadku jakiejkolwiek zmiany algorytmu, a zatem
czterokrotnie zwiksza moliwo popenienia bdu.

11

Ci, ktrzy uwaali, e da si to zrobi bez ponownej kompilacji i instalacji, zostali odnalezieni i poradzilimy sobie z nimi.

12

Jest to przykad zasady otwarty-zamknity (OCP) [PPP02].

13

Zasada DRY [PRAG].

FUNKCJE

69

Powielanie to zostao rozwizane przy uyciu metody include z listingu 3.7. Warto przeczyta
ponownie kod z pewnoci atwo mona zauway, e dziki ograniczeniu powtrek wzrosa
czytelno moduu.
Powtrzenia mog by rdem wszelkiego za w oprogramowaniu. W celu ich kontrolowania
i eliminowania powstao wiele zasad i praktyk. Jako przykad wemy pod uwag wszystkie postacie
normalne bazy danych zdefiniowane przez Codda, ktre su eliminowaniu danych. Wemy pod
uwag sposb, w jaki w programowaniu obiektowym koncentrujemy kod w klasach bazowych,
ktry w przeciwnym razie musiaby by nadmiarowy. Programowanie strukturalne, programowanie
aspektowe, programowanie komponentowe s po czci strategiami eliminowania powtrze.
Moe si wydawa, e od czasw wynalezienia podprogramw innowacje w tworzeniu oprogramowania s kolejnymi prbami eliminowania powtrze w kodzie rdowym.

Programowanie strukturalne
Niektrzy programici korzystaj z zasad programowania strukturalnego, opracowanych przez
Edserera Dijkstr14. Dijkstra powiedzia, e kada funkcja i kady blok funkcji powinien mie jedno
wejcie i jedno wyjcie. Zgodnie z tymi zasadami w funkcji powinna by jedna instrukcja return,
w ptli nie naley stosowa elementw break ani continue i nigdy, przenigdy nie wolno korzysta z instrukcji goto.
Cho sympatyzujemy z celami i dyscyplin programowania strukturalnego, zasady te niewiele
wnosz, jeeli funkcje s mae. Zasady te s przydatne wycznie w duych funkcjach.
Jeeli wic nasze funkcje bd mae, to okazjonalne wielokrotne instrukcje return, break i continue
nie powoduj problemw, a czasami mog przysuy si bardziej ni zasada jednego wejcia i jednego wyjcia. Z drugiej strony, instrukcja goto jest sensowna jedynie w duych funkcjach, wic
powinnimy jej unika.

Jak pisa takie funkcje?


Pisanie oprogramowania jest podobne do innych rodzajw pisania. Gdy piszemy ksik lub artyku,
najpierw zapisujemy swoje myli, a nastpnie cyzelujemy je, by byy czytelne i zrozumiae.
Pierwszy szkic moe by zagmatwany i mao zorganizowany, wic pracujemy nad sowami, przebudowujemy i dopracowujemy tekst do momentu, a bdzie nadawa si do czytania.
Gdy pisz funkcj, pierwsza wersja jest zwykle duga i skomplikowana. Ma wiele wci i zagniedonych ptli. Ma rwnie dug list argumentw. Nazwy s dowolne, a w funkcji znajduje si duo
powielonego kodu. Jednak mam rwnie zbir testw jednostkowych pokrywajcych kady z tych
zagmatwanych wierszy kodu.

14

[SP72].

70

ROZDZIA 3.

Zaczynam wic pracowa nad kodem, wydziela funkcje, zmienia nazwy i eliminowa powtrzenia. Zmniejszam metody i zmieniam ich kolejno. Czasami nawet wyodrbniam cae klasy, stale
zapewniajc poprawno wykonania testw.
Na kocu otrzymuj funkcje, ktre speniaj zasady przedstawione w tym rozdziale. Przedstawiona
metoda jest moe mao atrakcyjna, ale z pewnoci skuteczna.

Zakoczenie
Kady system jest budowany na podstawie jzyka specyficznego dla domeny, zaprojektowanego
przez programistw opisujcych system. Funkcje s czasownikami jzyka, a klasy jego rzeczownikami.
Nie jest przypadkiem, e w od dawna stosowanej metodologii rzeczowniki i czasowniki w dokumentach wymaga s pierwszymi kandydatami do klas i funkcji w systemie. Tak sztuka programowania jest i zawsze bya sztuk projektowania jzykw.
wietni programici postrzegaj systemy jako opowiadania, a nie jako programy do napisania.
Korzystaj oni z moliwoci wybranego jzyka programowania w celu skonstruowania znacznie
bogatszego jzyka, ktry moe by uyty do opowiedzenia danej historii. Czci tego jzyka specyficznego dla domeny jest hierarchia funkcji opisujcych wszystkie akcje, jakie s wykonywane
w systemie. Akcje te s w sposb rekurencyjny zapisywane przy uyciu definiowanego jzyka specyficznego dla domeny, aby byo moliwe opowiedzenie wasnej czci historii.
W niniejszym rozdziale przedstawiona zostaa rwnie mechanika pisania funkcji. Jeeli bdziemy
stosowa przedstawione tu zasady, funkcje bd krtkie, odpowiednio nazwane i adnie zorganizowane. Nigdy nie naley zapomina, e naszym celem jest opowiadanie historii na temat systemu
i pisane przez nas funkcje musz tworzy jasny i precyzyjny jzyk pomagajcy nam w tym opowiadaniu.

SetupTeardownIncluder
L I S T I N G 3 . 7 . SetupTeardownIncluder.java
package fitnesse.html;
import fitnesse.responders.run.SuiteResponder;
import fitnesse.wiki.*;
public class SetupTeardownIncluder {
private PageData pageData;
private boolean isSuite;
private WikiPage testPage;
private StringBuffer newPageContent;
private PageCrawler pageCrawler;
public static String render(PageData pageData) throws Exception {
return render(pageData, false);
}
public static String render(PageData pageData, boolean isSuite)
throws Exception {
return new SetupTeardownIncluder(pageData).render(isSuite);
}

FUNKCJE

71

private SetupTeardownIncluder(PageData pageData) {


this.pageData = pageData;
testPage = pageData.getWikiPage();
pageCrawler = testPage.getPageCrawler();
newPageContent = new StringBuffer();
}
private String render(boolean isSuite) throws Exception {
this.isSuite = isSuite;
if (isTestPage())
includeSetupAndTeardownPages();
return pageData.getHtml();
}
private boolean isTestPage() throws Exception {
return pageData.hasAttribute("Test");
}
private void includeSetupAndTeardownPages() throws Exception {
includeSetupPages();
includePageContent();
includeTeardownPages();
updatePageContent();
}
private void includeSetupPages() throws Exception {
if (isSuite)
includeSuiteSetupPage();
includeSetupPage();
}
private void includeSuiteSetupPage() throws Exception {
include(SuiteResponder.SUITE_SETUP_NAME, "-setup");
}
private void includeSetupPage() throws Exception {
include("SetUp", "-setup");
}
private void includePageContent() throws Exception {
newPageContent.append(pageData.getContent());
}
private void includeTeardownPages() throws Exception {
includeTeardownPage();
if (isSuite)
includeSuiteTeardownPage();
}
private void includeTeardownPage() throws Exception {
include("TearDown", "-teardown");
}
private void includeSuiteTeardownPage() throws Exception {
include(SuiteResponder.SUITE_TEARDOWN_NAME, "-teardown");
}
private void updatePageContent() throws Exception {
pageData.setContent(newPageContent.toString());
}
private void include(String pageName, String arg) throws Exception {
WikiPage inheritedPage = findInheritedPage(pageName);
if (inheritedPage != null) {

72

ROZDZIA 3.

String pagePathName = getPathNameForPage(inheritedPage);


buildIncludeDirective(pagePathName, arg);
}
}
private WikiPage findInheritedPage(String pageName) throws Exception {
return PageCrawlerImpl.getInheritedPage(pageName, testPage);
}
private String getPathNameForPage(WikiPage page) throws Exception {
WikiPagePath pagePath = pageCrawler.getFullPath(page);
return PathParser.render(pagePath);
}
private void buildIncludeDirective(String pagePathName, String arg) {
newPageContent
.append("\n!include ")
.append(arg)
.append(" .")
.append(pagePathName)
.append("\n");
}
}

Bibliografia
[KP78]: Kernighan i Plaugher, The Elements of Programming Style, McGraw-Hill 1978.
[PPP02]: Robert C. Martin, Agile Software Development: Principles, Patterns, and Practices, Prentice
Hall 2002.
[GOF]: Gamma, Design Patterns: Elements of Reusable Object Oriented Software, Addison-Wesley 1996.
[PRAG]: Andrew Hunt, Dave Thomas, The Pragmatic Programmer, Addison-Wesley 2000.
[SP72]: O.J. Dahl, E.W. Dijkstra, C.A.R. Hoare, Structured Programming, Academic Press, London 1972.

FUNKCJE

73

74

ROZDZIA 3.

ROZDZIA 4.

Komentarze

Nie komentuj zego kodu popraw go.


Brian W. Kernighan i P.J. Plaugher1

, jak dobrze umieszczony komentarz. Jednoczenie nic tak nie


N zaciemnia moduu, jak kilka zbyt dogmatycznych
komentarzy. Nic nie jest tak szkodliwe, jak stary
IEWIELE JEST RZECZY TAK POMOCNYCH

komentarz szerzcy kamstwa i dezinformacj.


Komentarze nie s jak Lista Schindlera. Nie s one czystym dobrem. W rzeczywistoci komentarze s w najlepszym przypadku koniecznym zem. Jeeli nasz jzyk programowania jest wystarczajco ekspresyjny lub mamy wystarczajcy talent, by wykorzystywa ten jzyk, aby wyraa
nasze zamierzenia, nie bdziemy potrzebowa zbyt wielu komentarzy.

[KP78], s. 144.

75

Prawidowe zastosowanie komentarzy jest kompensowaniem naszych bdw przy tworzeniu kodu.
Prosz zwrci uwag, e uyem sowa bd. Dokadnie to miaem na myli. Obecno komentarzy
zawsze sygnalizuje nieporadno programisty. Musimy korzysta z nich, poniewa nie zawsze wiemy,
jak wyrazi nasze intencje bez ich uycia, ale ich obecno nie jest powodem do witowania.
Gdy uznamy, e konieczne jest napisanie komentarza, naley pomyle, czy nie istnieje sposb na
wyraenie tego samego w kodzie. Za kadym razem, gdy wyrazimy to samo za pomoc kodu, powinnimy odczuwa satysfakcj. Za kadym razem, gdy piszemy komentarz, powinnimy poczu
smak poraki.
Dlaczego jestem tak przeciwny komentarzom? Poniewa one kami. Nie zawsze, nie rozmylnie,
ale nader czsto. Im starsze s komentarze, tym wiksze prawdopodobiestwo, e s po prostu
bdne. Powd jest prosty. Programici nie s w stanie utrzymywa ich aktualnoci.
Kod zmienia si i ewoluuje. Jego fragmenty s przenoszone w rne miejsca. Fragmenty te s rozdzielane, odtwarzane i ponownie czone. Niestety, komentarze nie zawsze za nimi podaj nie
zawsze mog by przenoszone. Zbyt czsto komentarze s odczane od kodu, ktry opisuj, i staj
si osieroconymi notatkami o stale zmniejszajcej si dokadnoci. Dla przykadu warto spojrze,
co si stao z komentarzem i wierszem, ktrego dotyczy:
MockRequest request;
private final String HTTP_DATE_REGEXP =
"[SMTWF][a-z]{2}\\,\\s[0-9]{2}\\s[JFMASOND][a-z]{2}\\s"+
"[0-9]{4}\\s[0-9]{2}\\:[0-9]{2}\\:[0-9]{2}\\sGMT";
private Response response;
private FitNesseContext context;
private FileResponder responder;
private Locale saveLocale;

// Przykad: "Tue, 02 Apr 2003 22:18:49 GMT"

Pozostae zmienne instancyjne zostay prawdopodobnie pniej dodane pomidzy sta HTTP_
DATE_REGEXP a objaniajcym j komentarzem.
Mona oczywicie stwierdzi, e programici powinni by na tyle zdyscyplinowani, aby utrzymywa komentarze w naleytym stanie. Zgadzam si, powinni. Wolabym jednak, aby powicona na
to energia zostaa spoytkowana na zapewnienie takiej precyzji i wyrazistoci kodu, by komentarze
okazay si zbdne.
Niedokadne komentarze s znacznie gorsze ni ich brak. Kami i wprowadzaj w bd. Powoduj
powstanie oczekiwa, ktre nigdy nie s spenione. Definiuj stare zasady, ktre nie s ju potrzebne lub nie powinny by stosowane.
Prawda znajduje si w jednym miejscu: w kodzie. Jedynie kod moe niezawodnie przedstawi to,
co realizuje. Jest jedynym rdem naprawd dokadnych informacji. Dlatego cho komentarze s
czasami niezbdne, powicimy spor ilo energii na zminimalizowanie ich liczby.

76

ROZDZIA 4.

Komentarze nie s szmink dla zego kodu


Jednym z czsto spotykanych powodw pisania komentarzy jest nieudany kod. Napisalimy modu
i zauwaamy, e jest le zorganizowany. Wiemy, e jest chaotyczny. Mwimy wwczas: Hm, bdzie
lepiej, jak go skomentuj. Nie! Lepiej go poprawi!
Precyzyjny i czytelny kod z ma liczb komentarzy jest o wiele lepszy ni zabaaganiony i zoony
kod z mnstwem komentarzy. Zamiast spdza czas na pisaniu kodu wyjaniajcego baagan, jaki
zrobilimy, warto powici czas na posprztanie tego baaganu.

Czytelny kod nie wymaga komentarzy


W wielu przypadkach kod mgby zupenie obej si bez komentarzy, a jednak programici wol
umieci w nim komentarz, zamiast zawrze objanienia w samym kodzie. Spjrzmy na poniszy
przykad. Co wolelibymy zobaczy? To:
// Sprawdzenie, czy pracownik ma prawo do wszystkich korzyci
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65))

czy to:
if (employee.isEligibleForFullBenefits())

Przeznaczenie tego kodu jest jasne po kilku sekundach mylenia. W wielu przypadkach jest to wycznie kwestia utworzenia funkcji, ktra wyraa to samo co komentarz, jaki chcemy napisa.

Dobre komentarze
Czasami komentarze s niezbdne lub bardzo przydatne. Przedstawimy kilka przypadkw, w ktrych
uznalimy, e warto powici im czas. Naley jednak pamita, e naprawd dobry komentarz to
taki, dla ktrego znalelimy powd, aby go nie pisa.

Komentarze prawne
Korporacyjne standardy kodowania czasami wymuszaj na nas pisanie pewnych komentarzy
z powodw prawnych. Na przykad informacje o prawach autorskich s niezbdnym elementem
umieszczanym w komentarzu na pocztku kadego pliku rdowego.
Przykadem moe by standardowy komentarz, jaki umieszczalimy na pocztku kadego pliku
rdowego w FitNesse. Na szczcie nasze rodowisko IDE ukrywa te komentarze przez ich automatyczne zwinicie.
// Copyright (C) 2003,2004,2005 by Object Mentor, Inc. All rights reserved.
// Released under the terms of the GNU General Public License version 2 or later.

KOMENTARZE

77

Tego typu komentarze nie powinny by wielkoci umw lub kodeksw. Tam, gdzie to moliwe,
warto odwoywa si do standardowych licencji lub zewntrznych dokumentw, a nie umieszcza
w komentarzu wszystkich zasad i warunkw.

Komentarze informacyjne
Czasami przydatne jest umieszczenie w komentarzu podstawowych informacji. Na przykad w poniszym komentarzu objaniamy warto zwracan przez metod abstrakcyjn.
// Zwraca testowany obiekt Responder.
protected abstract Responder responderInstance();

Komentarze tego typu s czasami przydatne, ale tam, gdzie to moliwe, lepiej jest skorzysta
z nazwy funkcji do przekazania informacji. Na przykad w tym przypadku komentarz moe sta si
niepotrzebny, jeeli zmienimy nazw funkcji: responderBeingTested.
Poniej mamy nieco lepszy przypadek:
// Dopasowywany format kk:mm:ss EEE, MMM dd, yyyy
Pattern timeMatcher = Pattern.compile(
"\\d*:\\d*:\\d* \\w*, \\w* \\d*, \\d*");

W tym przypadku komentarze pozwalaj nam poinformowa, e uyte wyraenie regularne ma


dopasowa czas i dat sformatowane za pomoc funkcji SimpleDateFormat.format z uyciem
zdefiniowanego formatu. Nadal lepiej jest przenie kod do specjalnej klasy pozwalajcej na konwertowanie formatw daty i czasu. Po tej operacji komentarz najprawdopodobniej stanie si zbdny.

Wyjanianie zamierze
W niektrych przypadkach komentarze zawieraj informacje nie tylko o implementacji, ale take
o powodach podjcia danej decyzji. W poniszym przypadku widzimy interesujc decyzj udokumentowan w postaci komentarza. Przy porwnywaniu obiektw autor zdecydowa o tym, e
obiekty jego klasy bd po posortowaniu wyej ni obiekty pozostaych klas.
public int compareTo(Object o)
{
if(o instanceof WikiPagePath)
{
WikiPagePath p = (WikiPagePath) o;
String compressedName = StringUtil.join(names, "");
String compressedArgumentName = StringUtil.join(p.names, "");
return compressedName.compareTo(compressedArgumentName);
}
return 1; // Jestemy wiksi, poniewa jestemy waciwego typu.
}

Poniej pokazany jest lepszy przykad. Moemy nie zgadza si z rozwizaniem tego problemu
przez programist, ale przynajmniej wiemy, co prbowa zrobi.
public void testConcurrentAddWidgets() throws Exception {
WidgetBuilder widgetBuilder =
new WidgetBuilder(new Class[]{BoldWidget.class});
String text = "'''bold text'''";

78

ROZDZIA 4.

ParentWidget parent =
new BoldWidget(new MockWidgetRoot(), "'''bold text'''");
AtomicBoolean failFlag = new AtomicBoolean();
failFlag.set(false);

//Jest to nasza prba uzyskania wycigu


//przez utworzenie duej liczby wtkw.
for (int i = 0; i < 25000; i++) {
WidgetBuilderThread widgetBuilderThread =
new WidgetBuilderThread(widgetBuilder, text, parent, failFlag);
Thread thread = new Thread(widgetBuilderThread);
thread.start();
}
assertEquals(false, failFlag.get());
}

Wyjanianie
Czasami przydatne jest wytumaczenie znaczenia niejasnych argumentw lub zwracanych wartoci.
Zwykle lepiej jest znale sposb na to, by ten argument lub zwracana warto byy bardziej czytelne,
ale jeeli s one czci biblioteki standardowej lub kodu, ktrego nie moemy zmienia, to
wyjanienia w komentarzach mog by uyteczne.
public void testCompareTo() throws Exception
{
WikiPagePath a = PathParser.parse("PageA");
WikiPagePath ab = PathParser.parse("PageA.PageB");
WikiPagePath b = PathParser.parse("PageB");
WikiPagePath aa = PathParser.parse("PageA.PageA");
WikiPagePath bb = PathParser.parse("PageB.PageB");
WikiPagePath ba = PathParser.parse("PageB.PageA");
assertTrue(a.compareTo(a) == 0); // a == a
assertTrue(a.compareTo(b) != 0); // a != b
assertTrue(ab.compareTo(ab) == 0); // ab == ab
assertTrue(a.compareTo(b) == -1); // a < b
assertTrue(aa.compareTo(ab) == -1); // aa < ab
assertTrue(ba.compareTo(bb) == -1); // ba < bb
assertTrue(b.compareTo(a) == 1); // b > a
assertTrue(ab.compareTo(aa) == 1); // ab > aa
assertTrue(bb.compareTo(ba) == 1); // bb > ba
}

Istnieje oczywicie spore ryzyko, e komentarze objaniajce s nieprawidowe. Warto przeanalizowa poprzedni przykad i zobaczy, jak trudno jest sprawdzi, czy s one prawidowe. Wyjania to,
dlaczego niezbdne s objanienia i dlaczego s one ryzykowne. Tak wic przed napisaniem tego
typu komentarzy naley sprawdzi, czy nie istnieje lepszy sposb, a nastpnie powici im wicej
uwagi, aby byy precyzyjne.

KOMENTARZE

79

Ostrzeenia o konsekwencjach
Komentarze mog rwnie suy do ostrzegania innych
programistw o okrelonych konsekwencjach. Poniszy
komentarz wyjania, dlaczego przypadek testowy jest
wyczony:
// Nie uruchamiaj, chyba e masz nieco czasu do zagospodarowania.
public void _testWithReallyBigFile()
{
writeLinesToFile(10000000);
response.setBody(testFile);
response.readyToSend(this);
String responseString = output.toString();
assertSubString("Content-Length:
1000000000", responseString);
assertTrue(bytesSent > 1000000000);
}

Obecnie oczywicie wyczamy przypadek testowy przez uycie atrybutu @Ignore z odpowiednim
tekstem wyjaniajcym. @Ignore("Zajmuje zbyt duo czasu"). Jednak w czasach przed JUnit 4
umieszczenie podkrelenia przed nazw metody byo czsto stosowan konwencj. Komentarz,
cho nonszalancki, dosy dobrze wskazuje powd.
Poniej pokazany jest inny przykad:
public static SimpleDateFormat makeStandardHttpDateFormat()
{

//SimpleDateFormat nie jest bezpieczna dla wtkw,


//wic musimy kady obiekt tworzy niezalenie.
SimpleDateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
df.setTimeZone(TimeZone.getTimeZone("GMT"));
return df;
}

Mona narzeka, e istniej lepsze sposoby rozwizania tego problemu. Mog si z tym zgodzi.
Jednak zastosowany tu komentarz jest cakiem rozsdny. Moe on powstrzyma nadgorliwego
programist przed uyciem statycznego inicjalizera dla zapewnienia lepszej wydajnoci.

Komentarze TODO
Czasami dobrym pomysem jest pozostawianie notatek do zrobienia w postaci komentarzy
//TODO. W zamieszczonym poniej przypadku komentarz TODO wyjania, dlaczego funkcja ma
zdegenerowan implementacj i jaka powinna by jej przyszo.
//TODO-MdM Nie jest potrzebna.
// Oczekujemy, e zostanie usunita po pobraniu modelu.
protected VersionInfo makeVersion() throws Exception
{
return null;
}

80

ROZDZIA 4.

Komentarze TODO oznaczaj zadania, ktre wedug programisty powinny by wykonane, ale
z pewnego powodu nie mona tego zrobi od razu. Moe to by przypomnienie o koniecznoci
usunicia przestarzaej funkcji lub proba do innej osoby o zajcie si problemem. Moe to by danie, aby kto pomyla o nadaniu lepszej nazwy, lub przypomnienie o koniecznoci wprowadzenia zmiany zalenej od planowanego zdarzenia. Niezalenie od tego, czym jest TODO, nie moe to by
wymwka dla pozostawienia zego kodu w systemie.
Obecnie wiele dobrych IDE zapewnia specjalne funkcje lokalizujce wszystkie komentarze TODO, wic
jest mao prawdopodobne, aby zostay zgubione. Nadal jednak nie jest korzystne, by kod by nafaszerowany komentarzami TODO. Naley wic regularnie je przeglda i eliminowa wszystkie, ktre si da.

Wzmocnienie
Komentarz moe by uyty do wzmocnienia wagi operacji, ktra w przeciwnym razie moe wydawa si niekonsekwencj.
String listItemContent = match.group(3).trim();

// Wywoanie trim jest naprawd wane. Usuwa pocztkowe


// spacje, ktre mog spowodowa, e element bdzie
// rozpoznany jako kolejna lista.
new ListItemWidget(this, listItemContent, this.level + 1);
return buildList(text.substring(match.end()));

Komentarze Javadoc w publicznym API


Nie ma nic bardziej pomocnego i satysfakcjonujcego, jak dobrze opisane publiczne API. Przykadem tego moe by standardowa biblioteka Java. Bez niej pisanie programw Java byoby trudne,
o ile nie niemoliwe.
Jeeli piszemy publiczne API, to niezbdne jest napisanie dla niego dobrej dokumentacji Javadoc.
Jednak naley pamita o pozostaych poradach z tego rozdziau. Komentarze Javadoc mog by
rwnie mylce, nie na miejscu i nieszczere jak wszystkie inne komentarze.

Ze komentarze
Do tej kategorii naley wikszo komentarzy. Zwykle s to podpory zego kodu lub wymwki albo
uzasadnienie niewystarczajcych decyzji znaczce niewiele wicej ni dyskusja programisty ze sob.

Bekot
Pisanie komentarza tylko dlatego, e czujemy, i powinien by napisany lub te e wymaga tego
proces, jest bdem. Jeeli decydujemy si na napisanie komentarza, musimy powici nieco czasu
na upewnienie si, e jest to najlepszy komentarz, jaki moglimy napisa.

KOMENTARZE

81

Poniej zamieszczony jest przykad znaleziony w FitNesse. Komentarz by faktycznie przydatny. Jednak
autor pieszy si lub nie powici mu zbyt wiele uwagi. Bekot, ktry po sobie zostawi, stanowi
nie lada zagadk:
public void loadProperties()
{
try
{
String propertiesPath = propertiesLocation + "/" + PROPERTIES_FILE;
FileInputStream propertiesStream = new FileInputStream(propertiesPath);
loadedProperties.load(propertiesStream);
}
catch(IOException e)
{

// Brak plikw waciwoci oznacza zaadowanie wszystkich wartoci domylnych.


}
}

Co oznacza komentarz w bloku catch? Jasne jest, e znaczy on co dla autora, ale znaczenie to nie
zostao dobrze wyartykuowane. Jeeli otrzymamy wyjtek IOException, najwyraniej oznacza to
brak pliku waciwoci, a w takim przypadku adowane s wszystkie wartoci domylne. Jednak kto
aduje te wartoci domylne? Czy byy zaadowane przed wywoaniem loadProperties.load?
Czy te loadProperties.load przechwytuje wyjtek, aduje wartoci domylne i przekazuje nam
wyjtek do zignorowania? A moe loadProperties.load aduje wszystkie wartoci domylne
przed prb zaadowania pliku? Czy autor prbowa usprawiedliwi przed samym sob fakt, e pozostawi pusty blok catch? By moe ta moliwo jest nieco przeraajca autor prbowa
powiedzie sobie, e powinien wrci w to miejsce i napisa kod adujcy wartoci domylne.
Jedynym sposobem, aby si tego dowiedzie, jest przeanalizowanie kodu z innych czci systemu
i sprawdzenie, co si w nich dzieje. Wszystkie komentarze, ktre wymuszaj zagldanie do innych
moduw w celu ich zrozumienia, nie s warte bitw, ktre zajmuj.

Powtarzajce si komentarze
Na listingu 4.1 zamieszczona jest prosta funkcja z komentarzem w nagwku, ktry jest cakowicie
zbdny. Prawdopodobnie duej zajmuje przeczytanie komentarza ni samego kodu.
L I S T I N G 4 . 1 . waitForClose
// Metoda uytkowa koczca prac, gdy this.closed ma warto true. Zgasza wyjtek,
// jeeli przekroczony zostanie czas oczekiwania.
public synchronized void waitForClose(final long timeoutMillis)
throws Exception
{
if(!closed)
{
wait(timeoutMillis);
if(!closed)
throw new Exception("MockResponseSender could not be closed");
}
}

Czemu suy ten komentarz? Przecie nie niesie wicej informacji ni sam kod. Nie uzasadnia on
kodu, nie przedstawia zamierze ani przyczyn. Nie jest atwiejszy do czytania od samego kodu.
82

ROZDZIA 4.

W rzeczywistoci jest mniej precyzyjny ni kod i wymusza na czytelniku zaakceptowanie braku


precyzji w imi prawdziwego zrozumienia. Jest on podobny do paplania sprzedawcy uywanych
samochodw, ktry zapewnia, e nie musisz zaglda pod mask.
Spjrzmy teraz na legion bezuytecznych i nadmiarowych komentarzy Javadoc pobranych z programu Tomcat i zamieszczonych na listingu 4.2. Komentarze te maj za zadanie wycznie zaciemni i popsu kod. Nie maj one adnej wartoci dokumentujcej. Co gorsza, pokazaem tutaj tylko
kilka pierwszych. W tym module znajduje si znacznie wicej takich komentarzy.
L I S T I N G 4 . 2 . ContainerBase.java (Tomcat)
public abstract class ContainerBase
implements Container, Lifecycle, Pipeline,
MBeanRegistration, Serializable {

/**
* The processor delay for this component.
*/
protected int backgroundProcessorDelay = -1;

/**
* The lifecycle event support for this component.
*/
protected LifecycleSupport lifecycle =
new LifecycleSupport(this);

/**
* The container event listeners for this Container.
*/
protected ArrayList listeners = new ArrayList();

/**
* The Loader implementation with which this Container is
* associated.
*/
protected Loader loader = null;

/**
* The Logger implementation with which this Container is
* associated.
*/
protected Log logger = null;

/**
* Associated logger name.
*/
protected String logName = null;

/**
* The Manager implementation with which this Container is
* associated.
*/
protected Manager manager = null;

/**
* The cluster with which this Container is associated.
*/
protected Cluster cluster = null;

KOMENTARZE

83

/**
* The human-readable name of this Container.
*/
protected String name = null;

/**
* The parent Container to which this Container is a child.
*/
protected Container parent = null;

/**
* The parent class loader to be configured when we install a
* Loader.
*/
protected ClassLoader parentClassLoader = null;

/**
* The Pipeline object with which this Container is
* associated.
*/
protected Pipeline pipeline = new StandardPipeline(this);

/**
* The Realm with which this Container is associated.
*/
protected Realm realm = null;

/**
* The resources DirContext object with which this Container
* is associated.
*/
protected DirContext resources = null;

Mylce komentarze
Czasami pomimo najlepszych intencji programista zapisuje w komentarzu nieprecyzyjne zdania.
Wrmy na moment do nadmiarowego, ale rwnie nieco mylcego komentarza zamieszczonego
na listingu 4.1.
Czy Czytelnik zauway, w czym ten komentarz jest mylcy? Metoda ta nie koczy si, gdy
this.closed ma warto true. Koczy si ona, jeeli this.closed ma warto true; w przeciwnym razie czeka okrelony czas, a nastpnie zgasza wyjtek, jeeli this.closed nadal nie ma
wartoci true.
Ta subtelna dezinformacja umieszczona w komentarzu, ktry czyta si trudniej ni sam kod, moe
spowodowa, e inny programista naiwnie wywoa t funkcj, oczekujc, e zakoczy si od razu,
gdy this.closed przyjmie warto true. Ten biedny programista moe zorientowa si, o co
chodzi, dopiero w sesji debugera, gdy bdzie prbowa zorientowa si, dlaczego jego kod dziaa
tak powoli.

84

ROZDZIA 4.

Komentarze wymagane
Wymaganie, aby kada funkcja posiadaa Javadoc lub aby kada zmienna posiadaa komentarz,
jest po prostu gupie. Tego typu komentarze tylko zaciemniaj kod i prowadz do powszechnych
pomyek i dezorganizacji.
Na przykad wymaganie komentarza Javadoc prowadzi do powstania takich potworw, jak ten
zamieszczony na listingu 4.3. Takie komentarze nie wnosz niczego, za to utrudniaj zrozumienie
kodu.
LISTING 4.3.
/**
*
* @param title Tytu pyty CD
* @param author Autor pyty CD
* @param tracks Liczba cieek na pycie CD
* @param durationInMinutes Czas odtwarzania CD w minutach
*/
public void addCD(String title, String author,
int tracks, int durationInMinutes) {
CD cd = new CD();
cd.title = title;
cd.author = author;
cd.tracks = tracks;
cd.duration = duration;
cdList.add(cd);
}

Komentarze dziennika
Czasami programici dodaj na pocztku kadego pliku komentarz informujcy o kadej edycji. Komentarze takie tworz pewnego rodzaju dziennik wszystkich wprowadzonych zmian. Spotkaem si
z moduami zawierajcymi kilkanacie stron z kolejnymi pozycjami dziennika.
* Changes (from 11-Oct-2001)
* -------------------------* 11-Oct-2001 : Re-organised the class and moved it to new package
* com.jrefinery.date (DG);
* 05-Nov-2001 : Added a getDescription() method, and eliminated NotableDate
* class (DG);
* 12-Nov-2001 : IBD requires setDescription() method, now that NotableDate
* class is gone (DG); Changed getPreviousDayOfWeek(),
* getFollowingDayOfWeek() and getNearestDayOfWeek() to correct
* bugs (DG);
* 05-Dec-2001 : Fixed bug in SpreadsheetDate class (DG);
* 29-May-2002 : Moved the month constants into a separate interface
* (MonthConstants) (DG);
* 27-Aug-2002 : Fixed bug in addMonths() method, thanks to N???levka Petr (DG);
* 03-Oct-2002 : Fixed errors reported by Checkstyle (DG);
* 13-Mar-2003 : Implemented Serializable (DG);
* 29-May-2003 : Fixed bug in addMonths method (DG);
* 04-Sep-2003 : Implemented Comparable. Updated the isInRange javadocs (DG);
* 05-Jan-2005 : Fixed bug in addYears() method (1096282) (DG);

KOMENTARZE

85

Dawno temu istniay powody tworzenia i utrzymywania takich dziennikw na pocztku kadego
moduu. Nie mielimy po prostu systemw kontroli wersji, ktre wykonyway to za nas. Obecnie
jednak takie dugie dzienniki tylko pogarszaj czytelno moduu. Powinny zosta usunite.

Komentarze wprowadzajce szum informacyjny


Czasami zdarza si nam spotka komentarze, ktre nie s niczym wicej jak tylko szumem informacyjnym. Przedstawiaj one oczywiste dane i nie dostarczaj adnych nowych informacji.
/**
* Konstruktor domylny.
*/
protected AnnualDateRule() {
}

No nie, naprawd? Albo co takiego:


/** Dzie miesica. */
private int dayOfMonth;

Nastpnie mamy doskonay przykad nadmiarowoci:


/**
* Zwraca dzie miesica.
*
* @return dzie miesica.
*/
public int getDayOfMonth() {
return dayOfMonth;
}

Komentarze takie stanowi tak duy szum informacyjny, e nauczylimy si je ignorowa. Gdy
czytamy kod, nasze oczy po prostu je pomijaj. W kocu komentarze te gosz nieprawd, gdy otaczajcy kod jest zmieniany.
Pierwszy komentarz z listingu 4.4 wydaje si waciwy2. Wyjania powd zignorowania bloku catch.
Jednak drugi jest czystym szumem. Najwyraniej programista by tak sfrustrowany pisaniem blokw
try-catch w tej funkcji, e musia sobie uly.
L I S T I N G 4 . 4 . startSending
private void startSending()
{
try
{
doSending();
}
catch(SocketException e)
{

// Normalne. Kto zatrzyma danie.


}
catch(Exception e)
2

Obecny trend sprawdzania poprawnoci w komentarzach przez rodowiska IDE jest zbawieniem dla wszystkich, ktrzy
czytaj duo kodu.

86

ROZDZIA 4.

{
try
{
response.add(ErrorResponder.makeExceptionString(e));
response.closeAll();
}
catch(Exception e1)
{

// Musz zrobi przerw!


}
}
}

Zamiast szuka ukojenia w bezuytecznych komentarzach, programista powinien zauway, e jego frustracja moe by rozadowana przez poprawienie struktury kodu. Powinien skierowa swoj
energi na wyodrbnienie ostatniego bloku try-catch do osobnej funkcji, jak jest to pokazane na
listingu 4.5.
L I S T I N G 4 . 5 . startSending (zmodyfikowany)
private void startSending()
{
try
{
doSending();
}
catch(SocketException e)
{

// Normalne. Kto zatrzyma danie.


}
catch(Exception e)
{
addExceptionAndCloseResponse(e);
}
}
private void addExceptionAndCloseResponse(Exception e)
{
try
{
response.add(ErrorResponder.makeExceptionString(e));
response.closeAll();
}
catch(Exception e1)
{
}
}

Warto zastpi pokus tworzenia szumu determinacj do wyczyszczenia swojego kodu. Pozwala to
sta si lepszym i szczliwszym programist.

Przeraajcy szum
Komentarze Javadoc rwnie mog by szumem. Jakie jest przeznaczenie poniszych komentarzy
Javadoc (ze znanej biblioteki open source)? Odpowied: adne. S to po prostu nadmiarowe komentarze stanowice szum informacyjny, napisane w le pojtej chci zapewnienia dokumentacji.

KOMENTARZE

87

/** Nazwa. */
private String name;

/** Wersja. */
private String version;

/** nazwaLicencji. */
private String licenceName;

/** Wersja. */
private String info;

Przeczytajmy dokadniej te komentarze. Czy czytelnik moe zauway bd kopiowania i wklejania? Jeeli autor nie powici uwagi pisaniu komentarzy (lub ich wklejaniu), to czy czytelnik moe
oczekiwa po nich jakiej korzyci?

Nie uywaj komentarzy, jeeli mona uy funkcji lub zmiennej


Przeanalizujmy poniszy fragment kodu:
// Czy modu z listy globalnej <mod> zaley
// od podsystemu, ktrego jest czci?
if (smodule.getDependSubsystems().contains(subSysMod.getSubSystem()))

Moe to by przeorganizowane bez uycia komentarzy:


ArrayList moduleDependees = smodule.getDependSubsystems();
String ourSubSystem = subSysMod.getSubSystem();
if (moduleDependees.contains(ourSubSystem))

Autor oryginalnego kodu prawdopodobnie napisa komentarz na pocztku (niestety), a nastpnie


kod realizujcy zadanie z komentarza. Jeeli jednak autor zmodyfikowaby kod w sposb, w jaki ja
to wykonaem, komentarz mgby zosta usunity.

Znaczniki pozycji
Czasami programici lubi zaznacza okrelone miejsca w pliku rdowym. Na przykad ostatnio
trafiem na program, w ktrym znalazem co takiego:
// Akcje //////////////////////////////////

Istniej rzadkie przypadki, w ktrych sensowne jest zebranie okrelonych funkcji razem pod tego
rodzaju transparentami. Jednak zwykle powoduj one chaos, ktry powinien by wyeliminowany
szczeglnie ten pocig ukonikw na kocu.
Transparent ten jest zaskakujcy i oczywisty, jeeli nie widzimy go zbyt czsto. Tak wic warto
uywa ich oszczdnie i tylko wtedy, gdy ich zalety s wyrane. Jeeli zbyt czsto uywamy tych
transparentw, zaczynaj by traktowane jako szum ta i ignorowane.

Komentarze w klamrach zamykajcych


Zdarza si, e programici umieszczaj specjalne komentarze po klamrach zamykajcych, tak jak
na listingu 4.6. Cho moe to mie sens w przypadku dugich funkcji, z gboko zagniedonymi
strukturami, w maych i hermetycznych funkcjach, jakie preferujemy, tworz tylko niead. Jeeli wic
Czytelnik bdzie chcia oznacza klamry zamykajce, niech sprbuje zamiast tego skrci funkcj.

88

ROZDZIA 4.

L I S T I N G 4 . 6 . wc.java
public class wc {
public static void main(String[] args) {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String line;
int lineCount = 0;
int charCount = 0;
int wordCount = 0;
try {
while ((line = in.readLine()) != null) {
lineCount++;
charCount += line.length();
String words[] = line.split("\\W");
wordCount += words.length;
} //while
System.out.println("wordCount = " + wordCount);
System.out.println("lineCount = " + lineCount);
System.out.println("charCount = " + charCount);
} // try
catch (IOException e) {
System.err.println("Error:" + e.getMessage());
} //catch
} //main
}

Atrybuty i dopiski
/* Dodane przez Ricka */

Systemy kontroli wersji wietnie nadaj si do zapamitywania, kto (i kiedy) doda okrelony
fragment. Nie ma potrzeby zamiecania kodu tymi maymi dopiskami. Mona uwaa, e tego typu komentarze bd przydatne do sprawdzenia, z kim mona porozmawia na temat danego fragmentu kodu. Rzeczywisto jest inna zwykle zostaj tam przez lata, tracc na dokadnoci i uytecznoci.
Pamitajmy systemy kontroli wersji s lepszym miejscem dla tego rodzaju informacji.

Zakomentowany kod
Niewiele jest praktyk tak nieprofesjonalnych, jak zakomentowanie kodu. Nie rb tego!
InputStreamResponse response = new InputStreamResponse();
response.setBody(formatter.getResultStream(), formatter.getByteCount());

// InputStream resultsStream = formatter.getResultStream();


// StreamReader reader = new StreamReader(resultsStream);
// response.setContent(reader.read(formatter.getByteCount()));

Inni programici, ktrzy zobacz taki zakomentowany kod, nie bd mieli odwagi go usun. Uznaj,
e jest tam z jakiego powodu i e jest zbyt wany, aby go usun. W ten sposb zakomentowany
kod zaczyna si odkada jak osad na dnie butelki zepsutego wina.

KOMENTARZE

89

Przeanalizujmy fragment z projektu Apache:


this.bytePos = writeBytes(pngIdBytes, 0);

//hdrPos = bytePos;
writeHeader();
writeResolution();

//dataPos = bytePos;
if (writeImageData()) {
writeEnd();
this.pngBytes = resizeByteArray(this.pngBytes, this.maxPos);
}
else {
this.pngBytes = null;
}
return this.pngBytes;

Dlaczego te dwa wiersze kodu s zakomentowane? Czy s wane? Czy jest to pozostao po wczeniejszych zmianach? Czy te s bdami, ktre kto przed laty zakomentowa i nie zada sobie trudu, aby to wyczyci?
W latach szedziesitych ubiegego wieku komentowanie kodu mogo by przydatne. Jednak od
bardzo dugiego czasu mamy ju dobre systemy kontroli wersji. Systemy te pamitaj za nas wczeniejszy kod. Nie musimy ju komentowa kodu. Po prostu moemy go usun. Nie stracimy go.
Gwarantuj.

Komentarze HTML
Kod HTML w komentarzach do kodu rdowego jest paskudny, o czym mona si przekona po
przeczytaniu kodu zamieszczonego poniej. Powoduje on, e komentarze s trudne do przeczytania w jedynym miejscu, gdzie powinny by atwe do czytania edytorze lub rodowisku IDE. Jeeli komentarze maj by pobierane przez jakie narzdzie (na przykad Javadoc), aby mogy by
wywietlone na stronie WWW, to zadaniem tego narzdzia, a nie programisty, powinno by opatrzenie ich stosownymi znacznikami HTML.
/**
* Zadanie uruchomienia testw sprawnoci.
* Zadanie uruchamia testy fitnesse i publikuje wyniki.
* <p/>
* <pre>
* Zastosowanie:
* &lt;taskdef name=&quot;execute-fitnesse-tests&quot;
* classname=&quot;fitnesse.ant.ExecuteFitnesseTestsTask&quot;
* classpathref=&quot;classpath&quot; /&gt;
* LUB
* &lt;taskdef classpathref=&quot;classpath&quot;
* resource=&quot;tasks.properties&quot; /&gt;
* <p/>
* &lt;execute-fitnesse-tests
* suitepage=&quot;FitNesse.SuiteAcceptanceTests&quot;
* fitnesseport=&quot;8082&quot;
* resultsdir=&quot;${results.dir}&quot;
* resultshtmlpage=&quot;fit-results.html&quot;
* classpathref=&quot;classpath&quot; /&gt;
* </pre>
*/

90

ROZDZIA 4.

Informacje nielokalne
Jeeli konieczne jest napisanie komentarza, to naley upewni si, e opisuje on kod znajdujcy si
w pobliu. Nie naley udostpnia informacji dotyczcych caego systemu w kontekcie komentarzy lokalnych. Wemy jako przykad zamieszczone poniej komentarze Javadoc. Pomijajc fakt, e
s zupenie zbdne, zawieraj one informacje o domylnym porcie. Funkcja jednak nie ma absolutnie adnej kontroli nad t wartoci domyln. Komentarz nie opisuje funkcji, ale inn cz
systemu, znacznie od niej oddalon. Oczywicie, nie ma gwarancji, e komentarz ten zostanie
zmieniony, gdy kod zawierajcy warto domyln ulegnie zmianie.
/**
* Port, na ktrym dziaa fitnesse. Domylnie <b>8082</b>.
*
* @param fitnessePort
*/
public void setFitnessePort(int fitnessePort)
{
this.fitnessePort = fitnessePort;
}

Nadmiar informacji
Nie naley umieszcza w komentarzach interesujcych z punktu widzenia historii dyskusji lub lunych opisw szczegw. Komentarz zamieszczony poniej zosta pobrany z moduu majcego za
zadanie sprawdzi, czy funkcja moe kodowa i dekodowa zgodnie ze standardem base64. Osoba
czytajca ten kod nie musi zna wszystkich szczegowych informacji znajdujcych si w komentarzu,
poza numerem RFC.
/*
RFC 2045 - Multipurpose Internet Mail Extensions (MIME)
Part One: Format of Internet Message Bodies
section 6.8. Base64 Content-Transfer-Encoding
The encoding process represents 24-bit groups of input bits as output
strings of 4 encoded characters. Proceeding from left to right,
a 24-bit input group is formed by concatenating 3 8-bit input groups.
These 24 bits are then treated as 4 concatenated 6-bit groups, each
of which is translated into a single digit in the base64 alphabet.
When encoding a bit stream via the base64 encoding, the bit stream
must be presumed to be ordered with the most-significant-bit first.
That is, the first bit in the stream will be the high-order bit in
the first 8-bit byte, and the eighth bit will be the low-order bit in
the first 8-bit byte, and so on.
*/

Nieoczywiste poczenia
Poczenie pomidzy komentarzem a kodem, ktry on opisuje, powinno by oczywiste. Jeeli mamy
problemy z napisaniem komentarza, to powinnimy przynajmniej doprowadzi do tego, by czytelnik patrzcy na komentarz i kod rozumia, o czym mwi dany komentarz.

KOMENTARZE

91

Jako przykad wemy komentarz zaczerpnity z projektu Apache:


/*
* Zaczynamy od tablicy, ktra jest na tyle dua, aby zmieci wszystkie piksele
* (plus filter bajtw) oraz dodatkowe 200 bajtw na informacje nagwka.
*/
this.pngBytes = new byte[((this.width + 1) * this.height * 3) + 200];

Co to s bajty filter? Czy ma to jaki zwizek z wyraeniem +1? A moe z *3? Z obydwoma? Czy
piksel jest bajtem? Dlaczego 200? Zadaniem komentarza jest wyjanianie kodu, ktry sam si nie objania. Jaka szkoda, e sam komentarz wymaga dodatkowego objanienia.

Nagwki funkcji
Krtkie funkcje nie wymagaj rozbudowanych opisw. Odpowiednio wybrana nazwa maej funkcji
realizujcej jedn operacj jest zwykle lepsza ni nagwek z komentarzem.

Komentarze Javadoc w niepublicznym kodzie


Komentarze Javadoc s przydatne w publicznym API, ale za to niemile widziane w kodzie nieprzeznaczonym do publicznego rozpowszechniania. Generowanie stron Javadoc dla klas i funkcji wewntrz systemu zwykle nie jest przydatne, a dodatkowy formalizm komentarzy Javadoc przyczynia
si jedynie do powstania bdw i rozproszenia uwagi.

Przykad
Kod zamieszczony na listingu 4.7 zosta przeze mnie napisany na potrzeby pierwszego kursu XP
Immersion. By on w zamierzeniach przykadem zego stylu kodowania i komentowania. Pniej
Kent Beck przebudowa go do znacznie przyjemniejszej postaci na oczach kilkudziesiciu entuzjastycznie reagujcych studentw. Pniej zaadaptowaem ten przykad na potrzeby mojej ksiki
Agile Software Development, Principles, Patterns, and Practices i pierwszych artykuw Craftman
publikowanych w magazynie Software Development.
Fascynujce w tym module jest to, e swego czasu byby on uznawany za dobrze udokumentowany.
Teraz postrzegamy go jako may baagan. Spjrzmy, jak wiele problemw z komentarzami mona
tutaj znale.
L I S T I N G 4 . 7 . GeneratePrimes.java
/**
* Klasa ta generuje liczby pierwsze do okrelonego przez uytkownika
* maksimum. Uytym algorytmem jest sito Eratostenesa.
* <p>
* Eratostenes z Cyrene, urodzony 276 p.n.e. w Cyrene, Libia -* zmar 194 p.n.e. w Aleksandrii. Pierwszy czowiek, ktry obliczy
* obwd Ziemi. Znany rwnie z prac nad kalendarzem
* z latami przestpnymi i prowadzenia biblioteki w Aleksandrii.
* <p>
* Algorytm jest dosy prosty. Mamy tablic liczb cakowitych
* zaczynajcych si od 2. Wykrelamy wszystkie wielokrotnoci 2. Szukamy

92

ROZDZIA 4.

* nastpnej niewykrelonej liczby i wykrelamy wszystkie jej wielokrotnoci.


* Powtarzamy dziaania do momentu osignicia pierwiastka kwadratowego z maksymalnej wartoci.
*
* @author Alphonse
* @version 13 Feb 2002 atp
*/
import java.util.*;
public class GeneratePrimes
{

/**
* @param maxValue jest limitem generacji.
*/
public static int[] generatePrimes(int maxValue)
{
if (maxValue >= 2) // Jedyny prawidowy przypadek.
{

// Deklaracje.
int s = maxValue + 1; // Rozmiar tablicy.
boolean[] f = new boolean[s];
int i;

// Inicjalizacja tablicy wartociami true.


for (i = 0; i < s; i++)
f[i] = true;

// Usuwanie znanych liczb niebdcych pierwszymi.


f[0] = f[1] = false;

// Sito.
int j;
for (i = 2; i < Math.sqrt(s) + 1; i++)
{
if (f[i]) // Jeeli i nie jest wykrelone, wykrelamy jego wielokrotnoci.
{
for (j = 2 * i; j < s; j += i)
f[j] = false; // Wielokrotnoci nie s pierwsze.
}
}

// Ile mamy liczb pierwszych?


int count = 0;
for (i = 0; i < s; i++)
{
if (f[i])
count++; // Licznik trafie.
}
int[] primes = new int[count];

// Przeniesienie liczb pierwszych do wyniku.


for (i = 0, j = 0; i < s; i++)
{
if (f[i]) // Jeeli pierwsza.
primes[j++] = i;
}
return primes; // Zwracamy liczby pierwsze.
}
else // maxValue < 2
return new int[0]; // Zwracamy pust tablic, jeeli niewaciwe dane wejciowe.
}
}

KOMENTARZE

93

Na listingu 4.8 zamieszczona jest przebudowana wersja tego samego moduu. Warto zauway, e
znacznie ograniczona jest liczba komentarzy. W caym module znajduj si tylko dwa komentarze.
Oba s z natury opisowe.
L I S T I N G 4 . 8 . PrimeGenerator.java (przebudowany)
/**
* Klasa ta generuje liczby pierwsze do okrelonego przez uytkownika
* maksimum. Uytym algorytmem jest sito Eratostenesa.
* Mamy tablic liczb cakowitych zaczynajcych si od 2.
* Wyszukujemy pierwsz nieokrelon liczb i wykrelamy wszystkie jej
* wielokrotnoci. Powtarzamy, a nie bdzie wicej wielokrotnoci w tablicy.
*/
public class PrimeGenerator
{
private static boolean[] crossedOut;
private static int[] result;
public static int[] generatePrimes(int maxValue)
{
if (maxValue < 2)
return new int[0];
else
{
uncrossIntegersUpTo(maxValue);
crossOutMultiples();
putUncrossedIntegersIntoResult();
return result;
}
}
private static void uncrossIntegersUpTo(int maxValue)
{
crossedOut = new boolean[maxValue + 1];
for (int i = 2; i < crossedOut.length; i++)
crossedOut[i] = false;
}
private static void crossOutMultiples()
{
int limit = determineIterationLimit();
for (int i = 2; i <= limit; i++)
if (notCrossed(i))
crossOutMultiplesOf(i);
}
private static int determineIterationLimit()
{

// Kada wielokrotno w tablicy ma podzielnik bdcy liczb pierwsz


// mniejsz lub rwn pierwiastkowi kwadratowemu wielkoci tablicy,
// wic nie musimy wykrela wielokrotnoci wikszych od tego pierwiastka.
double iterationLimit = Math.sqrt(crossedOut.length);
return (int) iterationLimit;
}
private static void crossOutMultiplesOf(int i)
{
for (int multiple = 2*i;
multiple < crossedOut.length;
multiple += i)
crossedOut[multiple] = true;

94

ROZDZIA 4.

}
private static boolean notCrossed(int i)
{
return crossedOut[i] == false;
}
private static void putUncrossedIntegersIntoResult()
{
result = new int[numberOfUncrossedIntegers()];
for (int j = 0, i = 2; i < crossedOut.length; i++)
if (notCrossed(i))
result[j++] = i;
}
private static int numberOfUncrossedIntegers()
{
int count = 0;
for (int i = 2; i < crossedOut.length; i++)
if (notCrossed(i))
count++;
return count;
}
}

Mona si spiera, e pierwszy komentarz jest nadmiarowy, poniewa czyta si go podobnie jak
sam funkcj genratePrimes. Uwaam jednak, e komentarz uatwia czytelnikowi poznanie algorytmu, wic zdecydowaem o jego pozostawieniu.
Drugi komentarz jest niemal na pewno niezbdny. Wyjania powody zastosowania pierwiastka
jako ograniczenia ptli. Mona sprawdzi, e adna z prostych nazw zmiennych ani inna struktura
kodu nie pozwala na wyjanienie tego punktu. Z drugiej strony, uycie pierwiastka moe by prnoci. Czy faktycznie oszczdzam duo czasu przez ograniczenie liczby iteracji do pierwiastka
liczby? Czy obliczenie pierwiastka nie zajmuje wicej czasu, ni uda si nam zaoszczdzi?
Warto o tym pomyle. Zastosowanie pierwiastka jako limitu ptli zadowala siedzcego we mnie
eksperta C i asemblera, ale nie jestem przekonany, e jest to warte czasu i energii osoby, ktra ma
za zadanie zrozumie ten kod.

Bibliografia
[KP78]: Kernighan i Plaugher, The Elements of Programming Style, McGraw-Hill 1978.

KOMENTARZE

95

96

ROZDZIA 4.

ROZDZIA 5.

Formatowanie

, chc by oczarowani elegancj, spjnoci i zauwaaln


G dbaoci o szczegy. Chcemy ich zaskoczy
uporzdkowaniem. Chcemy, aby ich brwi si uniosy,
DY LUDZIE ZAGLDAJ POD MASK PROJEKTU

gdy bd przeglda moduy kodu. Chcemy, aby rozpoznali prac profesjonalisty. Jeeli jednak zobacz wymieszan mas kodu, ktra wyglda, jakby kod by pisany przez zaog pijanych marynarzy,
mog doj do wniosku, e taki sam brak dbaoci o szczegy dotyczy innych aspektw projektu.
Pamitajmy, e kod powinien by adnie sformatowany. Naley wybra zbir prostych zasad, ktre
rzdz formatowaniem kodu, a nastpnie w konsekwentny sposb je stosowa. Jeeli pracujemy
w zespole, to wszyscy jego czonkowie powinni przyj zbir zasad formatowania i stosowa si do
nich. Pomocne s narzdzia automatyczne, ktre stosuj za nas te zasady formatowania.

97

Przeznaczenie formatowania
Na pocztku chciabym co ustali. Formatowanie kodu jest wane. Jest zbyt wane, aby je ignorowa, i zbyt wane, aby traktowa je dogmatycznie. Formatowanie kodu ma zapewni komunikacj,
a dobra komunikacja jest pierwsz zasad biznesu zawodowego programisty.
By moe kto stwierdzi, e pierwsz zasad biznesu zawodowego programisty jest zapewnienie
dziaania. Mam nadziej jednak, e ju teraz ksika ta dostatecznie skompromitowaa to zaoenie.
Funkcje, ktre tworzymy dzi, z duym prawdopodobiestwem mog si zmieni w nastpnej wersji, ale czytelno naszego kodu bdzie miaa zbawienny wpyw na wszystkie zmiany, jakie zostan
kiedykolwiek wykonane. Odpowiedni styl kodowania i czytelno kodu zapewniaj atwo utrzymania i rozszerzania kodu. Nasz styl i dyscyplina przetrwaj, nawet jeeli nasz kod ju nie.
Jakie s wic zasady formatowania, ktre zapewniaj uporzdkowanie i czytelno kodu?

Formatowanie pionowe
Zaczniemy od rozmiaru pionowego. Jak duy powinien by kod rdowy? W jzyku Java rozmiar
pliku jest cile zwizany z wielkoci klasy. Wielko klas przedstawialimy przy okazji dyskusji
o klasach. Teraz rozwamy tylko wielko plikw.
Jak due s zwykle pliki rdowe Java? Okazuje si, e rnice w zakresie wielkoci i stylu kodu s
znaczne (rys. 5.1).

R Y S U N E K 5 . 1 . Rozkad wielkoci plikw w skali logarytmicznej (wysoko prostokta = sigma)

98

ROZDZIA 5.

Przeanalizowano siedem rnych projektw Junit, FitNesse, testNG, Time and Money, JDepend,
Ant oraz Tomcat. Linie przechodzce przez prostokty pokazuj minimaln i maksymaln wielko pliku w kadym projekcie. Prostokt pokazuje rednio jedn trzeci (jedno standardowe odchylenie1) liczby plikw. rodek prostokta oznacza redni. Tak wic rednia wielko pliku
w projekcie FitNesse wynosi okoo 65 wierszy, a okoo jedna trzecia plikw ma od 40 do 100 i wicej
wierszy. Najwikszy plik w FitNesse ma okoo 400 wierszy, a najmniejszy 6 wierszy. Naley zwrci
uwag, e jest to skala logarytmiczna, wic maa rnica w pooeniu pionowym stanowi bardzo
du rnic w wielkoci pliku.
JUnit, FitNesse oraz Time and Money s zbudowane ze wzgldnie maych plikw. aden z nich nie
zawiera plikw powyej 500 wierszy, a wikszo plikw ma poniej 200 wierszy. Tomcat i Ant
maj z kolei cz plikw o dugoci kilku tysicy wierszy, a okoo poowa ma ponad 200 wierszy.
Co to oznacza? Wydaje si, e moliwe jest zbudowanie znaczcego systemu (FitNesse ma niemal
50 000 wierszy) z plikw majcych przecitnie 200 wierszy, a nieprzekraczajcych 500 wierszy.
Cho nie musi to by sztywna regua, to jednak powinna by bardzo podana. Mae pliki s zwykle
atwiejsze do zrozumienia ni due.

Metafora gazety
Pomylmy o dobrze napisanym artykule w gazecie. Czytamy go od gry do dou. Na grze oczekujemy nagwka, ktry informuje nas o temacie danego artykuu i pozwala zdecydowa, czy
chcemy przeczyta cao. Pierwszy akapit przedstawia zarys artykuu, nie zawiera szczegw. Gdy
kontynuujemy lektur w d kolumny, liczba szczegw zwiksza si s to daty, nazwiska, cytaty
i twierdzenia.
Chcemy, aby plik rdowy by podobny do takiego artykuu w gazecie. Nazwa ma by prosta i sugestywna. Powinna wystarczy nam do stwierdzenia, czy jestemy we waciwym module, czy nie.
Grne partie pliku rdowego maj zawiera algorytmy i koncepcje najwyszego poziomu. Liczba
szczegw powinna zwiksza si wraz ze schodzeniem w d pliku, a do funkcji oraz szczegw
na najniszym poziomie.
Gazeta skada si z wielu artykuw w wikszoci bardzo maych. Niektre s nieco wiksze.
Niewiele artykuw zawiera tyle tekstu, aby wypeni ca stron. Powoduje to, e gazety s uyteczne.
Jeeli gazeta przedstawiaaby jedn dug histori, zawierajc niezorganizowane konglomeraty
faktw, dat i nazwisk, po prostu nie daoby si jej czyta.

Pionowe odstpy pomidzy segmentami kodu


Niemal kady kod czyta si od lewej do prawej i od gry do dou. Kady wiersz reprezentuje wyraenie lub klauzul, a kada grupa wierszy reprezentuje kompletn myl. Myli te powinny by oddzielone od siebie pustymi wierszami.
1

Prostokt pokazuje wielko sigma/2 powyej i poniej redniej. Tak, wiem, e rozkad wielkoci pliku nie jest normalny, wic
odchylenie standardowe nie jest matematycznie dokadne. Jednak nie chodzi tutaj o precyzj chc przedstawi trend.

FORMATOWANIE

99

Jako przykad wemy kod z listingu 5.1. Znajduj si w nim puste wiersze oddzielajce deklaracj
pakietu, importy i kad z funkcji. Ta bardzo prosta zasada daje zbawienny efekt dla wizualnego
ukadu kodu. Kady pusty wiersz jest graficzn wskazwk identyfikujc now, osobn koncepcj.
Gdy przewijamy listing w d, oko zatrzymuje si na pierwszym wierszu znajdujcym si po pustym.
L I S T I N G 5 . 1 . BoldWidget.java
package fitnesse.wikitext.widgets;
import java.util.regex.*;
public class BoldWidget extends ParentWidget {
public static final String REGEXP = "'''.+?'''";
private static final Pattern pattern = Pattern.compile("'''(.+?)'''",
Pattern.MULTILINE + Pattern.DOTALL
);
public BoldWidget(ParentWidget parent, String text) throws Exception {
super(parent);
Matcher match = pattern.matcher(text);
match.find();
addChildWidgets(match.group(1));
}
public String render() throws Exception {
StringBuffer html = new StringBuffer("<b>");
html.append(childHtml()).append("</b>");
return html.toString();
}
}

Usunicie tych pustych wierszy, tak jak na listingu 5.2, ma fatalny wpyw na czytelno kodu.
L I S T I N G 5 . 2 . BoldWidget.java
package fitnesse.wikitext.widgets;
import java.util.regex.*;
public class BoldWidget extends ParentWidget {
public static final String REGEXP = "'''.+?'''";
private static final Pattern pattern = Pattern.compile("'''(.+?)'''",
Pattern.MULTILINE + Pattern.DOTALL);
public BoldWidget(ParentWidget parent, String text) throws Exception {
super(parent);
Matcher match = pattern.matcher(text);
match.find();
addChildWidgets(match.group(1));}
public String render() throws Exception {
StringBuffer html = new StringBuffer("<b>");
html.append(childHtml()).append("</b>");
return html.toString();
}
}

Efekt ten jest nawet bardziej zauwaalny, gdy spojrzymy na te bloki kodu z wikszej odlegoci.
W pierwszym przykadzie poszczeglne grupy wierszy bd dostrzegalne, natomiast drugi przykad
bdzie wyglda na wymieszany. Jedyn rnic pomidzy tymi dwoma listingami jest zastosowanie kilku odstpw w pionie.

100

ROZDZIA 5.

Gsto pionowa
Jeeli odstpy pozwalaj na rozdzielenie koncepcji, to pionowe zagszczenie pozwala wskaza
zwizki. Dlatego wiersze kodu majce ze sob cise zwizki powinny by zagszczone w pionie.
Zwrmy uwag, jak bezuyteczne komentarze z listingu 5.3 ami bliskie skojarzenie dwch
zmiennych instancyjnych.
LISTING 5.3.
public class ReporterConfig {

/**
* Nazwa klasy nasuchu raportu.
*/
private String m_className;

/**
* Waciwoci nasuchu raportu.
*/
private List<Property> m_properties = new ArrayList<Property>();
public void addProperty(Property property) {
m_properties.add(property);
}

Listing 5.4 jest znacznie atwiejszy do czytania. Mona go obj wzrokiem, przynajmniej ja tak to
odczuwam. Mona na niego spojrze i stwierdzi, e jest to klasa z dwoma zmiennymi i metodami,
bez potrzeby przemieszczania wzroku po kodzie. Aby osign ten sam poziom zrozumienia
w przypadku wczeniejszego listingu, nie wystarczy jedno spojrzenie na kod.
LISTING 5.4.
public class ReporterConfig {
private String m_className;
private List<Property> m_properties = new ArrayList<Property>();
public void addProperty(Property property) {
m_properties.add(property);
}

Odlego pionowa
Czy kiedykolwiek szukae koca klasy, skakae z jednej funkcji do nastpnej, przewijae plik rdowy w gr i w d, prbujc okreli, jak s powizane funkcje i jak dziaaj, aby nastpnie znw
zagubi si w labiryncie kodu? Czy kiedykolwiek ledzie acuch dziedziczenia dla definicji
zmiennej lub funkcji? Jest to frustrujce, poniewa chcemy zrozumie, co robi system, a tracimy
czas i energi na wyszukanie i zapamitanie, gdzie znajduj si jego skadniki.
Koncepcje, ktre s ze sob cile zwizane, powinny znajdowa si w niewielkiej odlegoci od
siebie [G10]. Jasne jest, e zasada ta nie moe by stosowana w przypadku koncepcji znajdujcych si
w osobnych plikach. Jednak cile powizane koncepcje nie powinny by umieszczane w rnych
plikach, o ile nie ma ku temu specjalnych przesanek. Jest to jeden z powodw, dla ktrych naley
unika zmiennych chronionych.
FORMATOWANIE

101

W przypadku tych koncepcji, ktre s ze sob tak cile powizane, e nale do tego samego pliku
rdowego, ich odlego od siebie jest miar tego, jak dana funkcja jest wana dla zrozumienia
drugiej. Nie powinnimy zmusza czytelnikw do skakania po pliku rdowym i klasach.
Deklaracje zmiennych. Zmienne powinny by zadeklarowane tak blisko miejsca ich uycia, jak
tylko to moliwe. Poniewa nasze zmienne s bardzo krtkie, zmienne lokalne powinny znajdowa
si na pocztku kadej z funkcji, tak jak w tych dugich funkcjach z Junit 4.3.1.
private static void readPreferences() {
InputStream is= null;
try {
is= new FileInputStream(getPreferencesFile());
setPreferences(new Properties(getPreferences()));
getPreferences().load(is);
} catch (IOException e) {
try {
if (is != null)
is.close();
} catch (IOException e1) {
}
}
}

Zmienne sterujce ptli powinny by deklarowane wewntrz instrukcji ptli, tak jak w maych
funkcjach z tego samego rda.
public int countTestCases() {
int count= 0;
for (Test each : tests)
count += each.countTestCases();
return count;
}

W rzadkich przypadkach zmienne mog by deklarowane na pocztku bloku lub w duszej funkcji,
bezporednio przed ptl. Mona spotka zmienne z poniszego fragmentu w rodku bardzo dugiej
funkcji z TestNG.
...
for (XmlTest test : m_suite.getTests()) {
TestRunner tr = m_runnerFactory.newTestRunner(this, test);
tr.addListener(m_textReporter);
m_testRunners.add(tr);
invoker = tr.getInvoker();
for (ITestNGMethod m : tr.getBeforeSuiteMethods()) {
beforeSuiteMethods.put(m.getMethod(), m);
}
for (ITestNGMethod m : tr.getAfterSuiteMethods()) {
afterSuiteMethods.put(m.getMethod(), m);
}
}
...

Zmienne instancyjne z kolei powinny by deklarowane na pocztku klasy. Nie naley zwiksza
odlegoci pionowej tych zmiennych, poniewa w dobrze zaprojektowanej klasie s uywane przez
wiele metod klasy, o ile nie przez wszystkie.
102

ROZDZIA 5.

Od dawna tocz si debaty na temat tego, gdzie powinny znajdowa si zmienne instancyjne. W jzyku C++ powszechnie stosowalimy tak zwan regu noyczek, zgodnie z ktr umieszczalimy
wszystkie zmienne instancyjne na kocu. Z kolei w jzyku Java powszechnie stosowan konwencj
jest umieszczanie wszystkich takich zmiennych na pocztku klasy. Nie widz powodw, dla ktrych
mielibymy stosowa si do ktrejkolwiek z tych konwencji. Najwaniejsze dla zmiennych instancyjnych jest to, aby byy zadeklarowane w jednym znanym miejscu. Kady powinien wiedzie, gdzie
zajrze, aby zobaczy deklaracje.
Jako przykad wemy dziwny przypadek klasy TestSuite z JUnit 4.3.1. Musiaem znacznie rozrzedzi t klas, aby zorientowa si, jak ona dziaa. Jeeli spojrzymy na rodkow cz listingu,
zauwaymy zadeklarowane tam dwie zmienne instancyjne. Trudno byoby schowa je w lepszym
miejscu. Osoba czytajca ten kod mogaby przypadkowo omin te deklaracje (tak jak ja).
public class TestSuite implements Test {
static public Test createTest(Class<? extends TestCase> theClass,
String name) {
...
}
public static Constructor<? extends TestCase>
getTestConstructor(Class<? extends TestCase> theClass)
throws NoSuchMethodException {
...
}
public static Test warning(final String message) {
...
}
private static String exceptionToString(Throwable t) {
...
}
private String fName;
private Vector<Test> fTests= new Vector<Test>(10);
public TestSuite() {
}
public TestSuite(final Class<? extends TestCase> theClass) {
...
}
public TestSuite(Class<? extends TestCase> theClass, String name) {
...
}
... ... ... ... ...
}

Funkcje zalene. Jeeli jedna funkcja wywouje inn, powinny by one pooone blisko siebie, a funkcja
wywoujca powinna by umieszczona powyej wywoywanej, o ile jest to moliwe. Pozwala to
osign naturalny przebieg programu. Jeeli konwencja ta jest stosowana konsekwentnie, czytelnik bdzie mg zaufa nam, e definicja funkcji znajduje si zaraz po jej wywoaniu. Jako przykad
wemy fragment z FitNesse z listingu 5.5. Warto zwrci uwag, e funkcje umieszczone na grze
wywouj te znajdujce si poniej, ktre z kolei wywouj pooone jeszcze niej. Powoduje to, e
atwiej mona znale wywoywane funkcje, i znacznie poprawia czytelno caego moduu.

FORMATOWANIE

103

L I S T I N G 5 . 5 . WikiPageResponder.java
public class WikiPageResponder implements SecureResponder {
protected WikiPage page;
protected PageData pageData;
protected String pageTitle;
protected Request request;
protected PageCrawler crawler;
public Response makeResponse(FitNesseContext context, Request request)
throws Exception {
String pageName = getPageNameOrDefault(request, "FrontPage");
loadPage(pageName, context);
if (page == null)
return notFoundResponse(context, request);
else
return makePageResponse(context);
}
private String getPageNameOrDefault(Request request, String defaultPageName)
{
String pageName = request.getResource();
if (StringUtil.isBlank(pageName))
pageName = defaultPageName;
return pageName;
}
protected void loadPage(String resource, FitNesseContext context)
throws Exception {
WikiPagePath path = PathParser.parse(resource);
crawler = context.root.getPageCrawler();
crawler.setDeadEndStrategy(new VirtualEnabledPageCrawler());
page = crawler.getPage(context.root, path);
if (page != null)
pageData = page.getData();
}
private Response notFoundResponse(FitNesseContext context, Request request)
throws Exception {
return new NotFoundResponder().makeResponse(context, request);
}
private SimpleResponse makePageResponse(FitNesseContext context)
throws Exception {
pageTitle = PathParser.render(crawler.getFullPath(page));
String html = makeHtml(context);
SimpleResponse response = new SimpleResponse();
response.setMaxAge(0);
response.setContent(html);
return response;
}
...

Przy okazji mamy w tym fragmencie przykad rozmieszczania staych na odpowiednim poziomie
[G35]. Staa FrontPage mogaby by umieszczona w funkcji getPageNameOrDefault, ale spowodowaoby to ukrycie znanej i oczekiwanej staej w funkcji znajdujcej si na zbyt niskim poziomie.
Lepiej byo przekazywa t sta w d, z miejsca, gdzie jej definicja ma sens, do miejsca, w ktrym
jest wykorzystywana.

104

ROZDZIA 5.

Koligacja koncepcyjna. Niektre fragmenty kodu chcemy


umieszcza niedaleko innych fragmentw. Maj one okrelon
koligacj koncepcyjn. Im silniejsza ta koligacja, tym mniejsza
powinna by odlego pomidzy tymi fragmentami.
Jak mona si przekona, koligacja ta moe bazowa na bezporedniej zalenoci, takiej jak wywoywanie jednej funkcji
przez inn lub uycie zmiennej w funkcji. Istniej rwnie inne
moliwoci koligacji. Koligacja moe wynika z tego, e funkcje wykonuj podobne operacje. Wemy pod uwag fragment
kodu zaczerpnity z Junit 4.3.1:
public class Assert {
static public void assertTrue(String message,
boolean condition) {
if (!condition)
fail(message);
}
static public void assertTrue(boolean condition) {
assertTrue(null, condition);
}
static public void assertFalse(String message, boolean condition) {
assertTrue(message, !condition);
}
static public void assertFalse(boolean condition) {
assertFalse(null, condition);
}
...

Funkcje te maj siln koligacj koncepcyjn, poniewa wspuytkuj ten sam schemat nazewnictwa i wykonuj odmiany tego samego zadania. Fakt, e wywouj si wzajemnie, jest mniej
wany. Nawet jeeli nie robiyby tego, nadal chcielibymy trzyma je razem.

Uporzdkowanie pionowe
Zwykle chcemy, aby w funkcji wywoania zalenoci byy kierowane w d. Inaczej mwic, wywoywana funkcja powinna znajdowa si poniej funkcji wywoujcej 2. Daje to efekt przepywu
sterowania w d kodu rdowego moduu, od wysokiego poziomu do niskiego.
W artykule prasowym oczekujemy, e najwaniejsze informacje znajd si na pocztku, a nastpnie
zostan opisane z najmniejsz liczb przeszkadzajcych detali. Oczekujemy, e najdrobniejsze
szczegy bd przedstawione na kocu. Podobnie w przypadku kodu pozwala to nam zapozna
si z plikami rdowymi i uzyskiwa ich obraz na podstawie kilku pierwszych funkcji, bez zagbiania si w szczegy. W taki sposb jest zorganizowany kod z listingu 5.5. Jeszcze lepszymi przykadami s listingi 15.5 i 3.7.
2

Jest to odwrotna sytuacja w stosunku do jzykw takich jak Pascal, C i C++, w ktrych funkcje musiay by zdefiniowane
lub co najmniej zadeklarowane przed ich uyciem.

FORMATOWANIE

105

Formatowanie poziome
Jak dugie powinny by wiersze? Aby odpowiedzie na to pytanie, sprawdzimy dugo wierszy
w typowych programach. Ponownie przeanalizujemy siedem rnych projektw. Na rysunku 5.2
przedstawiony jest rozkad dugoci wierszy wszystkich siedmiu projektw. Regularno jest imponujca, szczeglnie okoo 45 znakw. W rzeczywistoci kada warto od 20 do 60 reprezentuje
okoo 1 procent cakowitej liczby wierszy. To jest 40 procent! Kolejne 30 procent to wiersze krtsze ni
10 znakw. Naley pamita, e jest to skala logarytmiczna, wic liniowy spadek dugoci powyej
80 znakw jest naprawd bardzo znaczcy. Jasne jest, e programici preferuj krtkie wiersze.

R Y S U N E K 5 . 2 . Rozkad dugoci wierszy kodu Java

A zatem powinnimy tworzy krtkie wiersze. Stare ograniczenie Holleritha do 80 znakw jest zbyt
restrykcyjne i osobicie nie mam nic przeciwko wierszom majcym 100, a nawet 120 znakw. Jednak
wartoci powyej tej granicy s zbyt due.
Sam pisz programy tak, aby nigdy nie trzeba byo przewija ekranu edytora w prawo. Jednak
obecnie monitory s bardzo szerokie, a modsi programici mog tak zmniejszy czcionk, e uzyskuj wiersze o dugoci 200 znakw. Nie jest to dobra praktyka. Ja staram si nie przekracza dugoci 120 znakw.

Poziome odstpy i gsto


Odstpy poziome su do kojarzenia elementw cile powizanych i rozczania elementw, ktre
s ze sob luniej zwizane. Wemy pod uwag nastpujc funkcj:

106

ROZDZIA 5.

private void measureLine(String line) {


lineCount++;
int lineSize = line.length();
totalChars += lineSize;
lineWidthHistogram.addLine(lineSize, lineCount);
recordWidestLine(lineSize);
}

Operatory przypisania otoczyem odstpami w celu ich wyrnienia. Instrukcje przypisania maj
dwa osobne gwne elementy: lew i praw stron. Odstpy powoduj, e rozdzielenie to jest
oczywiste.
Z drugiej strony, nie dodaj odstpw pomidzy nazwami funkcji a otwierajcymi nawiasami.
Funkcje i argumenty s ze sob bardzo cile zwizane. Rozdzielenie ich spowodowaoby, e wygldayby na rozczone, a nie zczone. Rozdzielam argumenty wywoania funkcji, aby zaznaczy
przecinek i pokaza, e argumenty s osobne.
Innym zastosowaniem odstpw jest zaznaczenie kolejnoci wykonywania operatorw.
public class Quadratic {
public static double root1(double a, double b, double c) {
double determinant = determinant(a, b, c);
return (-b + Math.sqrt(determinant)) / (2*a);
}
public static double root2(int a, int b, int c) {
double determinant = determinant(a, b, c);
return (-b - Math.sqrt(determinant)) / (2*a);
}
private static double determinant(double a, double b, double c) {
return b*b - 4*a*c;
}
}

Warto zwrci uwag, jak atwo czyta si te wyraenia. Wykadniki nie s oddzielone odstpami, poniewa maj wysoki priorytet. Skadniki s rozdzielone, poniewa operacje dodawania i odejmowania maj niski priorytet.
Niestety, wikszo narzdzi formatujcych kod jest lepa na priorytety operatorw i stosuje wszdzie takie same odstpy. Tak wic subtelne odstpy, takie jak tu przedstawione, zwykle s tracone
po automatycznym sformatowaniu kodu.

Rozmieszczenie poziome
Gdy byem programist asemblera3, stosowaem rozmieszczenie poziome w celu wyrniania
okrelonych struktur. Gdy zaczem kodowa w C, C++ i w kocu w Javie, nadal staraem si wyrwnywa wszystkie nazwy zmiennych w zbiorze deklaracji lub wszystkie r-wartoci w zbiorze
instrukcji przypisania. Kod taki moe wyglda nastpujco:

Czy ja artuj? Nadal jestem programist asemblera. Mona odsun chopca od zabawek, ale trudno mu je zabra!

FORMATOWANIE

107

public class FitNesseExpediter implements ResponseSender


{
private
Socket
socket;
private
InputStream
input;
private
OutputStream
output;
private
Request
request;
private
Response
response;
private
FitNesseContext context;
protected long
requestParsingTimeLimit;
private
long
requestProgress;
private
long
requestParsingDeadline;
private
boolean
hasError;
public FitNesseExpediter(Socket
s,
FitNesseContext context) throws Exception
{
this.context =
context;
socket =
s;
input =
s.getInputStream();
output =
s.getOutputStream();
requestParsingTimeLimit = 10000;
}

Zauwayem jednak, e tego typu wyrwnywanie nie jest uyteczne. Wydaje si, e wyrwnanie
takie powoduje skupienie uwagi na niewaciwych elementach i odciga wzrok od waciwych.
Na przykad w przedstawionej powyej licie deklaracji mamy pokus czytania listy zmiennych
bez zwracania uwagi na ich typy. Podobnie w instrukcjach przypisania zwykle patrzymy na list
r-wartoci, bez zwracania uwagi na operator przypisania. Co gorsza, automatyczne narzdzia formatujce zwykle eliminuj tego rodzaju wyrwnanie.
Z tego powodu w kocu przestaem stosowa ten typ formatowania. Obecnie preferuj niewyrwnane deklaracje i przypisania, tak jak w poniszym przykadzie, poniewa wskazuj one na wan
niedoskonao. Jeeli mam dug list, ktra powinna by wyrwnana, wystpuje problem z szerokoci listy, a nie z brakiem wyrwnania. Dugo listy deklaracji w FitNesseExpediter, znajdujcej si poniej, sugeruje konieczno podziau klasy.
public class FitNesseExpediter implements ResponseSender
{
private Socket socket;
private InputStream input;
private OutputStream output;
private Request request;
private Response response;
private FitNesseContext context;
protected long requestParsingTimeLimit;
private long requestProgress;
private long requestParsingDeadline;
private boolean hasError;
public FitNesseExpediter(Socket s, FitNesseContext context) throws Exception
{
this.context = context;
socket = s;
input = s.getInputStream();
output = s.getOutputStream();
requestParsingTimeLimit = 10000;
}

108

ROZDZIA 5.

Wcicia
Plik rdowy stanowi pewnego rodzaju hierarchi, a nie szablon. Znajduj si w nim informacje
odnoszce si do pliku jako caoci, do poszczeglnych klas w pliku, do metod w klasach, do blokw w metodach oraz, rekurencyjnie, do blokw w blokach. Kady poziom hierarchii jest zakresem, w ktrym s zadeklarowane nazwy oraz w ktrym s interpretowane deklaracje i instrukcje wykonywalne.
Aby hierarchia ta bya widoczna, wcinamy wiersze kodu rdowego odpowiednio do pozycji
w hierarchii. Instrukcje na poziomie pliku, takie jak wikszo deklaracji klas, nie s wcinane. Metody
w klasach s wcinane jeden poziom w praw stron klasy. Implementacje tych metod znajduj si
jeden poziom w prawo w stosunku do deklaracji metody. Implementacje blokw znajduj si jeden
poziom w prawo w stosunku do bloku, w ktrym si znajduj, i tak dalej.
Programici polegaj w znacznym stopniu na tym schemacie wci. Wyrwnuj oni wizualnie
wiersze z lewej strony, aby sprawdzi, w ktrym zakresie si znajduj. Pozwala to szybko przeskakiwa zakresy, takie jak implementacje instrukcji if lub while, ktre nie odnosz si do biecej
sytuacji. Od lewej strony programici szukaj deklaracji metod, nowych zmiennych, a nawet
nowych klas. Bez wci programy byyby niemal nieczytelne dla ludzi.
Spjrzmy na ponisze programy, ktre s pod wzgldem skadniowym i semantycznym identyczne:
public class FitNesseServer implements SocketServer { private FitNesseContext
context; public FitNesseServer(FitNesseContext context) { this.context =
context; } public void serve(Socket s) { serve(s, 10000); } public void
serve(Socket s, long requestTimeout) { try { FitNesseExpediter sender = new
FitNesseExpediter(s, context);
sender.setRequestParsingTimeLimit(requestTimeout); sender.start(); }
catch(Exception e) { e.printStackTrace(); } } }
----public class FitNesseServer implements SocketServer {
private FitNesseContext context;
public FitNesseServer(FitNesseContext context) {
this.context = context;
}
public void serve(Socket s) {
serve(s, 10000);
}
public void serve(Socket s, long requestTimeout) {
try {
FitNesseExpediter sender = new FitNesseExpediter(s, context);
sender.setRequestParsingTimeLimit(requestTimeout);
sender.start();
}
catch (Exception e) {
e.printStackTrace();
}
}
}

FORMATOWANIE

109

Nasze oko moe szybko okreli struktur wci w pliku. Mona niemal natychmiast odszuka
zmienne, konstruktory, akcesory i metody. Tylko kilka sekund zajmuje okrelenie, e jest to jaki
interfejs do gniazda z obsug czasu oczekiwania. Wersja bez wci jest jednak niemal nieczytelna
bez intensywnego studiowania.
amanie wci. Czasami kuszce jest amanie zasad wci w przypadku krtkich instrukcji if,
krtkich ptli while lub krtkich funkcji. Wszdzie tam, gdzie ulegem tej pokusie, niemal zawsze
wracaem i dodawaem waciwe wcicia. Dlatego naley unika czenia zakresw w jeden wiersz,
tak jak w poniszym przykadzie:
public class CommentWidget extends TextWidget
{
public static final String REGEXP = "^#[^\r\n]*(?:(?:\r\n)|\n|\r)?";
public CommentWidget(ParentWidget parent, String text){super(parent, text);}
public String render() throws Exception {return ""; }
}

Polecam rozwinicie kodu i wcicie zakresw, tak jak poniej:


public class CommentWidget extends TextWidget {
public static final String REGEXP = "^#[^\r\n]*(?:(?:\r\n)|\n|\r)?";
public CommentWidget(ParentWidget parent, String text) {
super(parent, text);
}
public String render() throws Exception {
return "";
}
}

Puste zakresy
Czasami tre instrukcji while lub for nie zawiera kodu, tak jak jest to pokazane poniej. Nie lubi
tego typu struktur i staram si ich unika. Gdy nie mog ich unikn, upewniam si, e pusta tre
jest prawidowo wcita i otoczona klamrami. Nie zlicz, ile razy daem si oszuka przez rednik
siedzcy cichutko na kocu ptli while w tym samym wierszu. Jeeli nie zapewnimy, e rednik ten bdzie widoczny, przez zastosowanie wcicia w jego wasnym wierszu, trudno bdzie go zauway.
while (dis.read(buf, 0, readBufferSize) != -1)
;

Zasady zespoowe
Tytu tego podrozdziau jest gr sw. Kady programista ma wasne ulubione zasady formatowania, ale
jeeli pracuje w zespole, zasady ustala zesp.

110

ROZDZIA 5.

Zesp programistw powinien ustali jeden styl formatowania, a nastpnie kady czonek zespou
powinien stosowa ten styl. Oczekujemy, e program bdzie mia jednorodny styl. Nie chcemy, aby
wyglda, jakby by napisany przez band niezgadzajcych si ze sob indywiduw.
Gdy w roku 2002 zaczem prac nad projektem FitNesse, usiadem z zespoem w celu wypracowania stylu kodowania. Zajo to okoo 10 minut. Zdecydowalimy, gdzie bdziemy umieszcza
klamry, jaka powinna by wielko wcicia, jak bdziemy nazywa klasy, zmienne, metody i tak
dalej. Nastpnie wpisalimy te zasady w formater kodu naszego IDE i od tego momentu je stosowalimy. Nie byy to zasady, ktre preferuj; byy to zasady, o ktrych zdecydowa zesp. Jako
czonek zespou stosowaem je przy pisaniu kodu w projekcie FitNesse.
Naley pamita, e dobre oprogramowanie skada si ze zbioru dokumentw, ktre dobrze si
czyta. Musz mie one spjny i gadki styl. Musz budzi zaufanie czytelnika, ktry uzna, e formatowanie, jakie zobaczy w jednym pliku rdowym, bdzie znaczyo to samo w innym. Ostatni
rzecz, jak chcemy zrobi, jest zwikszenie skomplikowania kodu przez jego pisanie przy uyciu
kilku rnych odrbnych stylw.

Zasady formatowania wujka Boba


Zasady formatowania stosowane przeze mnie s bardzo proste i zostay przedstawione w kodzie na listingu 5.6. Mona go uzna za przykad tego, jak kod tworzy najlepszy dokument standardu kodowania.
L I S T I N G 5 . 6 . CodeAnalyzer.java
public class CodeAnalyzer implements JavaFileAnalysis {
private int lineCount;
private int maxLineWidth;
private int widestLineNumber;
private LineWidthHistogram lineWidthHistogram;
private int totalChars;
public CodeAnalyzer() {
lineWidthHistogram = new LineWidthHistogram();
}
public static List<File> findJavaFiles(File parentDirectory) {
List<File> files = new ArrayList<File>();
findJavaFiles(parentDirectory, files);
return files;
}
private static void findJavaFiles(File parentDirectory, List<File> files) {
for (File file : parentDirectory.listFiles()) {
if (file.getName().endsWith(".java"))
files.add(file);
else if (file.isDirectory())
findJavaFiles(file, files);
}
}
public void analyzeFile(File javaFile) throws Exception {
BufferedReader br = new BufferedReader(new FileReader(javaFile));

FORMATOWANIE

111

String line;
while ((line = br.readLine()) != null)
measureLine(line);
}
private void measureLine(String line) {
lineCount++;
int lineSize = line.length();
totalChars += lineSize;
lineWidthHistogram.addLine(lineSize, lineCount);
recordWidestLine(lineSize);
}
private void recordWidestLine(int lineSize) {
if (lineSize > maxLineWidth) {
maxLineWidth = lineSize;
widestLineNumber = lineCount;
}
}
public int getLineCount() {
return lineCount;
}
public int getMaxLineWidth() {
return maxLineWidth;
}
public int getWidestLineNumber() {
return widestLineNumber;
}
public LineWidthHistogram getLineWidthHistogram() {
return lineWidthHistogram;
}
public double getMeanLineWidth() {
return (double)totalChars/lineCount;
}
public int getMedianLineWidth() {
Integer[] sortedWidths = getSortedWidths();
int cumulativeLineCount = 0;
for (int width : sortedWidths) {
cumulativeLineCount += lineCountForWidth(width);
if (cumulativeLineCount > lineCount/2)
return width;
}
throw new Error("Nie mona tu wej");
}
private int lineCountForWidth(int width) {
return lineWidthHistogram.getLinesforWidth(width).size();
}
private Integer[] getSortedWidths() {
Set<Integer> widths = lineWidthHistogram.getWidths();
Integer[] sortedWidths = (widths.toArray(new Integer[0]));
Arrays.sort(sortedWidths);
return sortedWidths;
}
}

112

ROZDZIA 5.

ROZDZIA 6.

Obiekty i struktury danych

korzystamy ze zmiennych prywatnych. Nie chcemy, aby ktokolwiek


I na nich polega. Chcemy zachowa
sobie swobod zmian ich typu lub implementacji. Dlaczego
STNIEJE POWD, DLA KTREGO

wic tak wielu programistw automatycznie dodaje gettery i settery do swoich obiektw, udostpniajc zmienne prywatne tak, jakby byy one publiczne?

Abstrakcja danych
Zwrmy uwag na rnice pomidzy listingiem 6.1 a listingiem 6.2. Oba reprezentuj dane
punktu w ukadzie kartezjaskim. A jednak w pierwszym przypadku implementacja jest udostpniona, a w drugim cakowicie ukryta.

113

L I S T I N G 6 . 1 . Punkt konkretny
public class Point {
public double x;
public double y;
}

L I S T I N G 6 . 2 . Punkt abstrakcyjny
public interface Point {
double getX();
double getY();
void setCartesian(double x, double y);
double getR();
double getTheta();
void setPolar(double r, double theta);
}

Najlepsze w kodzie z listingu 6.2 jest to, e nie ma sposobu na stwierdzenie, czy implementacja korzysta ze wsprzdnych prostoktnych, czy ktowych. By moe cakowicie innych? A jednak interfejs niezawodnie reprezentuje struktur danych.
Co lepsze, reprezentuje wicej ni tylko struktur danych. Metody pozwalaj na wymuszenie zasad
dostpu. Mona niezalenie odczytywa poszczeglne wsprzdne, ale ustawianie wsprzdnych
musi by wykonywane jako atomowa (tzn. pojedyncza) operacja.
Z drugiej strony, na listingu 6.1 jest bardzo wyrazicie zaimplementowana klasa wsprzdnych
prostoktnych, ktra zmusza nas do niezalenego manipulowania tymi wsprzdnymi. Powoduje to ujawnienie implementacji. W rzeczywistoci implementacja ta byaby rwnie ujawniona,
jeeli zmienne byyby prywatne i uywalibymy setterw i getterw poszczeglnych zmiennych.
Ukrywanie implementacji nie sprowadza si do dodawania warstwy funkcji nad zmiennymi.
Ukrywanie implementacji polega na tworzeniu abstrakcji! Klasa nie powinna po prostu przepycha
zmiennych przez gettery i settery. Zamiast tego powinna udostpnia interfejs pozwalajcy uytkownikom na manipulowanie istot danych bez koniecznoci znajomoci jej implementacji.
Spjrzmy na listingi 6.3 i 6.4. W pierwszym uyte s konkretne terminy informujce o poziomie
paliwa w pojedzie, natomiast w drugim uyta jest abstrakcja poziomu procentowego. W konkretnym przypadku moemy by niemal pewni, e s to po prostu akcesory zmiennych. W przypadku abstrakcyjnym nie mamy pojcia o formie danych.
L I S T I N G 6 . 3 . Pojazd konkretny
public interface Vehicle {
double getFuelTankCapacityInGallons();
double getGallonsOfGasoline();
}

L I S T I N G 6 . 4 . Pojazd abstrakcyjny
public interface Vehicle {
double getPercentFuelRemaining();
}

114

ROZDZIA 6.

W obu przedstawionych przypadkach zalecane jest stosowanie drugiego z przedstawionych kodw.


Nie chcemy udostpnia szczegw naszych danych. Zamiast tego powinnimy opisywa dane
abstrakcyjnymi terminami. Nie mona tego osign przez proste uycie interfejsw oraz getterw
i setterw. Konieczna jest dogbna analiza w celu okrelenia najlepszego sposobu reprezentowania
danych znajdujcych si w obiektach. Najgorsz opcj jest lepe dodanie getterw i setterw.

Antysymetria danych i obiektw


Ponisze dwa przykady pokazuj rnice pomidzy obiektami i strukturami danych. Obiekty
ukrywaj dane, tworzc abstrakcje, i udostpniaj funkcje operujce na tych danych. Struktury
danych udostpniaj ich dane i nie maj znaczcych funkcji. Przeczytajmy to jeszcze raz. Zwrmy
uwag na komplementarn natur tych dwch definicji. S one waciwie swoimi przeciwiestwami. Rnica ta moe wydawa si nieznaczna, ale ma daleko idce implikacje.
Jako przykad wemy proceduralny kod ksztatu z listingu 6.5. Klasa Geometry operuje na trzech
klasach ksztatu. Klasy ksztatw s prostymi strukturami danych bez wasnych operacji. Wszystkie
operacje s zdefiniowane w klasie Geometry.
L I S T I N G 6 . 5 . Ksztat proceduralny
public class Square {
public Point topLeft;
public double side;
}
public class Rectangle {
public Point topLeft;
public double height;
public double width;
}
public class Circle {
public Point center;
public double radius;
}
public class Geometry {
public final double PI = 3.141592653589793;
public double area(Object shape) throws NoSuchShapeException
{
if (shape instanceof Square) {
Square s = (Square)shape;
return s.side * s.side;
}
else if (shape instanceof Rectangle) {
Rectangle r = (Rectangle)shape;
return r.height * r.width;
}
else if (shape instanceof Circle) {
Circle c = (Circle)shape;
return PI * c.radius * c.radius;
}
throw new NoSuchShapeException();
}
}

OBIEKTY I STRUKTURY DANYCH

115

Czytelnicy, ktrzy preferuj programowanie zorientowane obiektowo, mog zmarszczy brwi i narzeka, e jest to kod proceduralny i bd mieli racj. Jednak to szyderstwo moe by nie na miejscu. Zwrmy uwag, co si stanie, jeeli dodamy funkcj perimeter() do klasy Geometry. Klasy
ksztatw pozostan bez zmian! Wszystkie inne klasy korzystajce z klas ksztatw rwnie si nie
zmieni! Z drugiej strony, jeeli dodamy nowy ksztat, musimy zmieni wszystkie operujce na
nim funkcje z Geometry. Przeczytajmy to ponownie. Trzeba zwrci uwag, e te dwa warunki s
zupenie przeciwstawne.
Wemy teraz przykad obiektowy z listingu 6.6. Metoda area() jest tu polimorficzna. Nie jest potrzebna klasa Geometry. Jeeli wic dodamy nowy ksztat, adna z istniejcych funkcji nie ulegnie
zmianie, ale jeeli dodamy now funkcj, wszystkie ksztaty bd musiay by zmienione1!
L I S T I N G 6 . 6 . Ksztat polimorficzny
public class Square implements Shape {
private Point topLeft;
private double side;
public double area() {
return side*side;
}
}
public class Rectangle implements Shape {
private Point topLeft;
private double height;
private double width;
public double area() {
return height * width;
}
}
public class Circle implements Shape {
private Point center;
private double radius;
public final double PI = 3.141592653589793;
public double area() {
return PI * radius * radius;
}
}

Ponownie widzimy komplementarn natur tych dwch definicji s one swoimi przeciwiestwami! Pokazuj podstawow rnic pomidzy obiektami i strukturami danych:
Kod proceduralny (kod korzystajcy ze struktur danych) uatwia dodawanie nowych funkcji
bez zmiany istniejcych struktur danych. Z kolei kod obiektowy uatwia dodawanie nowych klas
bez zmiany istniejcych funkcji.

Istniej sposoby obejcia tego problemu, dobrze znane dowiadczonym projektantom obiektowym. Przykadem moe by
wzorzec Visitor lub podwjne rozsyanie. Jednak techniki te s obcione wasnymi kosztami i zwykle daj w wyniku
struktur znan z podejcia proceduralnego.

116

ROZDZIA 6.

Prawdziwe jest rwnie stwierdzenie odwrotne:


Kod proceduralny utrudnia dodawanie nowych struktur danych, poniewa musz zosta
zmienione wszystkie funkcje. Kod obiektowy utrudnia dodawanie nowych funkcji, poniewa
musz zosta zmienione wszystkie klasy.
Tak wic operacje, ktre s trudne do wykonania w sposb obiektowy, mona atwo zrealizowa przy
uyciu procedur, a operacje trudne do realizacji za pomoc procedur mona atwo wykona za pomoc obiektw!
A co zrobi w sytuacji, kiedy w zoonym systemie chcemy doda nowy typ danych zamiast nowych
funkcji? Dla takich przypadkw najwaciwsze jest zastosowanie technik obiektowych. Z drugiej
strony, zdarza si rwnie, e chcemy doda nowe funkcje. W takich przypadkach najwaciwszy
wydaje si kod proceduralny i struktury danych.
Dowiadczeni programici wiedz, e twierdzenie, i wszystko jest obiektem, to mit. Czasami faktycznie warto zastosowa proste struktury danych z operujcymi na nich procedurami.

Prawo Demeter
Dobrze znana zasada, nazywana prawem Demeter2, mwi, e modu powinien nie wiedzie nic
o wntrzu obiektw, ktrymi manipuluje. Jak przedstawilimy w poprzednim punkcie, obiekty
ukrywaj swoje dane i udostpniaj operacje. Oznacza to, e obiekt nie powinien udostpnia
swojej struktury wewntrznej przy uyciu akcesorw, poniewa powoduje to udostpnienie, a nie
ukrycie wewntrznej struktury.
Dokadniej rzecz ujmujc, prawo Demeter gosi, e metoda f klasy C powinna wywoywa tylko
metody z:
C,

obiektu utworzonego przez f,

obiektu przekazanego jako argument do f,

obiektu umieszczonego w zmiennej instancyjnej klasy C.

Metoda nie powinna wywoywa metod z obiektw zwracanych przez jakkolwiek z innych dozwolonych funkcji. Inaczej mwic, powinnimy rozmawia z przyjacimi, a nie z obcymi.
W poniszym kodzie3 nie jest zachowane prawo Demeter (midzy innymi), poniewa wywoywana
jest metoda getScratchDir() z obiektu zwracanego przez getOptions(), a nastpnie wywoywana jest metoda getAbsolutePatch() na obiekcie zwracanym przez getScratchDir().
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();

http://en.wikipedia.org/wiki/Law_of_Demeter

Znalezionym gdzie w bibliotekach Apache.

OBIEKTY I STRUKTURY DANYCH

117

Wraki pocigw
Przedstawiony powyej kod jest czsto nazywany
wrakiem pocigu, poniewa wyglda jak kilka wagonw kolejowych po zderzeniu. Tego rodzaju acuchy wywoa s uznawane za oznak sabego stylu
programowania i naley ich unika [G36]. Najlepiej
podzieli je w nastpujcy sposb:
Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();

Czy te dwa fragmenty kodu naruszaj prawo Demeter? Oczywicie wiemy, i w tym module
obiekt ctxt zawiera opcje, ktre zawieraj katalog poredni, ktry z kolei posiada ciek bezwzgldn. To sporo wiedzy jak na jedn funkcj. Wywoujca funkcja wie, w jaki sposb nawigowa
pomidzy wieloma rnymi obiektami.
To, czy naruszone jest prawo Demeter, zaley od tego, czy ctxt, Options i ScratchDir s obiektami, czy strukturami danych. Jeeli s obiektami, to ich wewntrzna struktura powinna by ukryta, a nie ujawniona, wic wiedza na temat jej budowy wewntrznej jest jawnym naruszeniem prawa
Demeter. Z drugiej strony, jeeli ctxt, Options i ScratchDir s po prostu strukturami danych
bez zdefiniowanych operacji, to w naturalny sposb ujawniaj swoj struktur wewntrzn i prawo
Demeter nie ma tu zastosowania.
Zastosowanie funkcji akcesorw zwykle utrudnia dostrzeenie problemu. Jeeli kod zostaby napisany tak jak poniej, to prawdopodobnie nie pytalibymy o naruszenie prawa Demeter.
final String outputDir = ctxt.options.scratchDir.absolutePath;

Problem ten byby znacznie mniej kopotliwy, jeeli struktury danych po prostu miayby zmienne
publiczne i nie zawieray funkcji, natomiast obiekty miayby zmienne prywatne i funkcje publiczne.
Jednak istniej biblioteki i standardy (na przykad beans), ktre wymagaj, aby nawet proste
struktury danych miay akcesory i mutatory.

Hybrydy
Przedstawione problemy czsto prowadz do powstania struktur hybrydowych bdcych w poowie obiektami, a w poowie strukturami danych. Maj one funkcje wykonujce wane operacje, ale
maj rwnie zmienne publiczne lub publiczne akcesory i mutatory, ktre oprcz tego, e realizuj
zamierzone operacje, udostpniaj zmienne prywatne, co pozwala zewntrznym funkcjom na uycie tych zmiennych w sposb, w jaki programy proceduralne uywaj struktur danych4.
Takie hybrydy utrudniaj dodawanie nowych funkcji, jak rwnie utrudniaj dodawanie nowych
struktur danych. Maj najgorsze cechy z obu wiatw. Naley unika ich tworzenia. S one charakterystyczne dla tych projektw, w ktrych autorzy nie s pewni lub co gorsza, nie maj pojcia
czy potrzebuj ochrony funkcji, czy typw.
4

Jest to czasami nazywane zazdroci o funkcje (ang. Feature Envy) [Refactoring].

118

ROZDZIA 6.

Ukrywanie struktury
Zamy, e ctxt, options oraz scratchDir s obiektami ze zdefiniowanymi operacjami. W takim
przypadku obiekty powinny ukrywa swoj wewntrzn struktur i nie powinnimy mie moliwoci nawigowania po nich. W jaki sposb moemy wic otrzyma ciek bezwzgldn do katalogu
poredniego?
ctxt.getAbsolutePathOfScratchDirectoryOption();

lub
ctx.getScratchDirectoryOption().getAbsolutePath()

Pierwsza opcja moe doprowadzi do eksplozji metod w obiekcie ctxt. W drugiej zakadamy, e
getScratchDirectoryOption() zwraca struktur danych, a nie obiekt. adna z tych opcji nie
wyglda dobrze.
Jeeli ctxt jest obiektem, powinnimy powiedzie mu, aby co zrobi; nie powinnimy pyta go
o szczegy wewntrzne. Dlaczego wic chcemy otrzyma ciek bezwzgldn do katalogu poredniego? Co mamy zamiar z ni zrobi? Moemy si dowiedzie tego z nastpujcego kodu
(znajdujcego si wiele wierszy dalej) z tego samego moduu:
String outFile = outputDir + "/" + className.replace('.', '/') + ".class";
FileOutputStream fout = new FileOutputStream(outFile);
BufferedOutputStream bos = new BufferedOutputStream(fout);

Wymieszanie rnych poziomw szczegowoci [G34][G6] powoduje problemy. Kropki, ukoniki


i obiekty File nie powinny by tak beztrosko mieszane ze sob oraz z otaczajcym je kodem. Ignorujc
ten problem, widzimy w kocu, e przeznaczeniem cieki bezwzgldnej do katalogu poredniego
byo utworzenie pliku roboczego o podanej nazwie.
Czy obiekt ctxt moe to zrobi za nas?
BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);

Wydaje si to rozsdn operacj, jak obiekt moe wykona! Pozwala to ukry szczegy wewntrzne obiektu ctxt i uniemoliwia funkcjom amanie prawa Demeter przez nawigowanie
pomidzy obiektami, o ktrych nie powinnimy wiedzie.

Obiekty transferu danych


Kwintesencj postaci struktur danych jest klasa ze zmiennymi publicznymi niezawierajca funkcji.
Czasami jest to nazywane obiektem transferu danych, czyli DTO. DTO s bardzo przydatnymi
strukturami, szczeglnie w przypadku komunikowania si z bazami danych, analizowania komunikatw z gniazd sieciowych i tak dalej. Czsto staj si pierwszym elementem serii przeksztace,
ktre konwertuj surowe dane z bazy danych na obiekty w kodzie aplikacji.
Nieco czciej spotykan jest posta bean, przedstawiona na listingu 6.7. Ma ona zmienne prywatne,
na ktrych operuje si za pomoc getterw i setterw. Ta quasi-hermetyzacja obiektw bean powoduje, e niektrzy puryci obiektowi czuj si lepiej, ale nie ma ona adnych dodatkowych zalet.
OBIEKTY I STRUKTURY DANYCH

119

L I S T I N G 6 . 7 . address.java
public class Address {
private String street;
private String streetExtra;
private String city;
private String state;
private String zip;
public Address(String street, String streetExtra,
String city, String state, String zip) {
this.street = street;
this.streetExtra = streetExtra;
this.city = city;
this.state = state;
this.zip = zip;
}
public String getStreet() {
return street;
}
public String getStreetExtra() {
return streetExtra;
}
public String getCity() {
return city;
}
public String getState() {
return state;
}
public String getZip() {
return zip;
}
}

Active Record
Active Record to specjalna forma DTO. S to struktury danych ze zmiennymi publicznymi (lub
dostpnymi w stylu bean), ale dodatkowo maj metody nawigacyjne, takie jak save i find. Zwykle
obiekty Active Record s bezporednim tumaczeniem z tabel bazy danych lub innych rde.
Niestety, czsto zdarza si, e programici traktuj te struktury danych, jakby byy penoprawnymi
obiektami, i umieszczaj w nich metody zasad biznesowych. Jest to dziwne, poniewa powoduje powstanie hybrydy struktury danych i obiektu.
Rozwizaniem jest oczywicie traktowanie Active Record jako struktury danych i tworzenie osobnych obiektw, ktre zawieraj zasady biznesowe i ukrywaj ich dane wewntrzne (ktrymi bd
prawdopodobnie obiekty Active Record).

120

ROZDZIA 6.

Zakoczenie
Obiekty udostpniaj operacje i ukrywaj dane. Uatwia to dodawanie nowych rodzajw obiektw
bez zmiany istniejcych operacji, ale utrudnia dodawanie nowych operacji do istniejcych
obiektw. Struktury danych udostpniaj dane i nie maj znaczcych operacji. Uatwia to dodawanie nowych operacji do istniejcych struktur danych, ale utrudnia dodawanie nowych struktur danych do istniejcych funkcji.
W kadym systemie potrzebujemy czasami elastycznoci w dodawaniu nowych typw danych,
wic w tych czciach systemu powinnimy korzysta gwnie z obiektw. W innych przypadkach
oczekujemy elastycznoci w dodawaniu nowych operacji, wic w tych czciach systemu powinnimy korzysta gwnie z typw danych i procedur. W programowaniu nie naley kierowa si
uprzedzeniami, ale wybiera takie rozwizanie, ktre umoliwi najlepsz realizacj zada.

Bibliografia
[Refactoring]: Martin Fowler i inni, Refactoring: Improving the Design of Existing Code, AddisonWesley 1999.

OBIEKTY I STRUKTURY DANYCH

121

122

ROZDZIA 6.

ROZDZIA 7.

Obsuga bdw
Michael Feathers

ksice na temat czystego kodu moe wydawa si nieporoR zumieniem. A jednak musimy zaj siwtym
tematem. Obsuga bdw jest po prostu czci proOZDZIA POWICONY OBSUDZE BDW

gramowania. Dane wejciowe mog by nieprawidowe, a urzdzenia mog ulec uszkodzeniu. Mwic krtko, wszystko moe pj le, a programista jest odpowiedzialny za takie skonstruowanie
kodu, aby wykonywa to, co powinien.
Poczenie tematu obsugi bdw z zagadnieniami czystego kodu powinno by oczywiste. Wiele
zbiorw kodu jest cakowicie zdominowanych przez obsug bdw. Mwic zdominowanych,
nie mam na myli tego, e kod ten realizuje wycznie obsug bdw. Mam na myli to, e niemal
niemoliwe jest okrelenie, co ten kod tak naprawd robi, poniewa wszystko jest usiane obsug
bdw. Obsuga bdw jest wana, ale jeeli utrudnia zrozumienie logiki kodu, jest niewaciwa.
W tym rozdziale przedstawi kilka technik i zaoe, z ktrych moemy skorzysta przy pisaniu
kodu, ktry jest zarwno czytelny, jak i solidny kodu, ktry obsuguje bdy elegancko i stylowo.

123

Uycie wyjtkw zamiast kodw powrotu


W odlegej przeszoci korzystalimy z wielu jzykw programowania nieposiadajcych wyjtkw.
W jzykach tych techniki obsugi i raportowania bdw byy ograniczone. Mona byo ustawi
znacznik bdu lub kod bdu, ktry nastpnie mg by sprawdzany przez funkcj wywoujc.
Podejcie to jest przedstawione na listingu 7.1.
L I S T I N G 7 . 1 . DeviceController.java
public class DeviceController {
...
public void sendShutDown() {
DeviceHandle handle = getHandle(DEV1);

// Sprawdzenie stanu urzdzenia.


if (handle != DeviceHandle.INVALID) {

// Zapisanie stanu urzdzenia w polu rekordu.


retrieveDeviceRecord(handle);

// Jeeli nie wstrzymane, wyczenie.


if (record.getStatus() != DEVICE_SUSPENDED) {
pauseDevice(handle);
clearDeviceWorkQueue(handle);
closeDevice(handle);
} else {
logger.log("Urzdzenie wstrzymane. Nie mona wyczy");
}
} else {
logger.log("Niewaciwy uchwyt dla: " + DEV1.toString());
}

}
...

Rozwizanie to jest jednak problematyczne, poniewa powoduje baagan w funkcji wywoujcej.


Musi ona sprawdzi bdy natychmiast po wykonaniu wywoania. Niestety, atwo o tym zapomnie. Z tego powodu w przypadku napotkania bdu lepiej zgosi wyjtek. Kod wywoujcy jest
prostszy. Jego logika nie jest zanieczyszczona obsug bdw.
Na listingu 7.2 przedstawiony jest wczeniejszy kod, w ktrym metody wykrywajce bd zgaszaj wyjtek.
L I S T I N G 7 . 2 . DeviceController.java (z wyjtkami)
public class DeviceController {
...
public void sendShutDown() {
try {
tryToShutDown();
} catch (DeviceShutDownError e) {
logger.log(e);
}
}
private void tryToShutDown() throws DeviceShutDownError {
DeviceHandle handle = getHandle(DEV1);
DeviceRecord record = retrieveDeviceRecord(handle);

124

pauseDevice(handle);
clearDeviceWorkQueue(handle);
closeDevice(handle);

ROZDZIA 7.

private DeviceHandle getHandle(DeviceID id) {


...
throw new DeviceShutDownError("Niewaciwy uchwyt dla: " + id.toString());
...
}
...
}

Jak wida, ta wersja kodu jest znacznie czytelniejsza. To nie jest jednak wycznie kwestia estetyki.
Kod jest lepszy, poniewa dwa problemy, ktre byy ze sob spltane, algorytm wyczenia urzdzenia i obsugi bdw, zostay rozdzielone. atwiej teraz zrozumie kad z nich.

Rozpoczynanie od pisania instrukcji try-catch-finally


Jedn z najbardziej interesujcych cech wyjtkw jest to, e definiuj one zakresy w naszym programie. Gdy wykonujemy kod w czci try instrukcji try-catch-finally, zakadamy, e wykonanie moe zosta w dowolnym momencie przerwane i wznowione w bloku catch.
Dziki temu bloki try s podobne do transakcji. Nasz blok catch ma za zadanie pozostawi program w stanie spjnym, niezalenie od tego, co stao si w try. Z tego powodu dobr praktyk jest
korzystanie z instrukcji try-catch-finally w przypadku tworzenia kodu, w ktrym mog zosta
zgoszone wyjtki. Pomaga to zdefiniowa operacje, jakich powinien oczekiwa uytkownik tego
kodu, niezalenie od tego, co zego wydarzy si w czasie wykonywania kodu z try.
Rozwamy nastpujcy przykad. Chcemy napisa kod odwoujcy si do pliku, z ktrego odczytuje serializowane obiekty.
Zaczynamy od testu jednostkowego, ktry pokazuje, e w przypadku braku pliku otrzymujemy
wyjtek.
@Test(expected = StorageException.class)
public void retrieveSectionShouldThrowOnInvalidFileName() {
sectionStore.retrieveSection("invalid - file");
}

Test prowadzi nas do utworzenia nastpujcego fragmentu:


public List<RecordedGrip> retrieveSection(String sectionName) {

// Zalepka zwracanej wartoci do momentu utworzenia implementacji.


return new ArrayList<RecordedGrip>();
}

Test zawiedzie, poniewa nie zgasza wyjtku. Nastpnie zmieniamy nasz implementacj, aby
sprbowa odwoa si do nieprawidowego pliku. Operacja ta zgasza wyjtek:
public List<RecordedGrip> retrieveSection(String sectionName) {
try {
FileInputStream stream = new FileInputStream(sectionName)
} catch (Exception e) {
throw new StorageException("retrieval error", e);
}
return new ArrayList<RecordedGrip>();
}

OBSUGA BDW

125

Test powiedzie si, poniewa przechwyci wyjtek. Od tego momentu moemy zacz modyfikowanie kodu. Moemy zawzi typ przechwytywanego wyjtku, aby pasowa do typu faktycznie
zgaszanego przez konstruktor FileInputStream: FileNotFoundException:
public List<RecordedGrip> retrieveSection(String sectionName) {
try {
FileInputStream stream = new FileInputStream(sectionName);
stream.close();
} catch (FileNotFoundException e) {
throw new StorageException("retrieval error, e);
}
return new ArrayList<RecordedGrip>();
}

Teraz, po zdefiniowaniu zakresu za pomoc struktury try-catch, moemy uy TDD do zbudowania


reszty potrzebnej logiki. Logika ta zostanie umieszczona pomidzy utworzeniem FileInputStream
a wywoaniem close i moemy w niej zaoy, e wszystko przebiegnie prawidowo.
Sprbujmy napisa testy wymuszajce wyjtki, a nastpnie doda do procedury obsugi operacje
pozwalajce speni te testy. Spowoduje to konieczno zbudowania zakresu transakcji za pomoc
bloku try, co pomoe nam utrzyma transakcyjn natur tego zakresu.

Uycie niekontrolowanych wyjtkw


Debata jest zakoczona. Przez lata programici Javy debatowali nad zaletami i odpowiedzialnoci
kontrolowanych wyjtkw. Gdy w pierwszej wersji jzyka Java zostay wprowadzone kontrolowane
wyjtki, wydawao si to dobrym pomysem. Sygnatura kadej metody powinna zawiera list
wszystkich wyjtkw, jakie moe przekazywa do wywoujcego. Co wicej, wyjtki te byy czci
typu metody. Kod po prostu nie skompilowa si, jeeli sygnatura nie odpowiadaa temu, co
robi kod.
Z czasem zaczlimy myle, e kontrolowane wyjtki byy wietnym pomysem daway one pewne
korzyci. Obecnie jednak jest jasne, e nie s one potrzebne do tworzenia solidnego oprogramowania. C# nie posiada kontrolowanych wyjtkw i, pomimo wielu prb, C++ te ich nie posiada. Nie
ma ich rwnie w jzykach Python czy Ruby. Jednak we wszystkich tych jzykach moliwe jest pisanie solidnego oprogramowania. Z powodu istnienia tak wielu przykadw musielimy zdecydowa naprawd czy kontrolowane wyjtki s warte ich ceny.
Jakiej ceny? Cen kontrolowanych wyjtkw jest naruszenie zasady otwarty-zamknity1. Jeeli
zgosimy kontrolowany wyjtek w metodzie naszego kodu, a instrukcja catch znajduje si trzy poziomy wyej, musimy zadeklarowa ten wyjtek w sygnaturze kadej metody pomidzy naszym kodem a catch. Oznacza to, e zmiana wykonana na niskim poziomie oprogramowania wymusza
zmiany na wielu wyszych poziomach. Zmienione moduy musz zosta ponownie skompilowane
i zainstalowane, nawet jeeli zmiana bezporednio ich nie dotyczy.

[Martin].

126

ROZDZIA 7.

Wemy pod uwag hierarchi wywoa w duym systemie. Funkcje znajdujce si na grnym poziomie
wywouj funkcje znajdujce si poniej, ktre wywouj kolejne funkcje poniej, i tak dalej. Zamy teraz, e jedna z funkcji najniszego poziomu zostaa zmodyfikowana w taki sposb, e
musi zgosi wyjtek. Jeeli ten wyjtek jest kontrolowany, to do sygnatury funkcji naley doda
klauzul throws. Jednak oznacza to, e kada funkcja, ktra wywouje nasz zmodyfikowan funkcj, musi by rwnie zmodyfikowana, aby przechwytywaa nowy wyjtek w przeciwnym wypadku naley doczy do jej sygnatury klauzul throws. Operacje te realizowane s w nieskoczono! Kocowym wynikiem s kaskadowe zmiany zaczynajce si na najniszym poziomie
oprogramowania i koczce na najwyszym! Naruszona jest rwnie hermetyzacja, poniewa
wszystkie funkcje w ciece musz pozna szczegy tego niskopoziomowego wyjtku. Jako e przeznaczeniem wyjtkw jest umoliwienie obsugi bdw na odlego, trzeba uzna za porak fakt,
e kontrolowane wyjtki niszcz w ten sposb hermetyzacj.
Kontrolowane wyjtki czasami mog by uyteczne, na przykad gdy piszemy wan bibliotek.
Konieczne jest wtedy ich przechwytywanie. Jednak w zwykym programowaniu aplikacji koszt
zalenoci jest wikszy ni ich zalety.

Dostarczanie kontekstu za pomoc wyjtkw


Kady zgaszany przez nas wyjtek powinien mie wystarczajc ilo informacji na temat kontekstu,
aby mona byo okreli rdo i lokalizacj bdu. W jzyku Java moemy uzyska lad stosu z dowolnego wyjtku; jednak lad stosu nie zawiera informacji o przeznaczeniu nieudanej operacji.
Trzeba tworzy komunikaty bdw zawierajce odpowiednie informacje i przekazywa je za pomoc wyjtkw. Naley zamieci w nich nieudan operacj i typ awarii. Jeeli w naszej aplikacji
stosujemy rejestrowanie, musimy przekaza wystarczajce informacje, aby mona byo zarejestrowa
bd w bloku catch.

Definiowanie klas wyjtkw


w zalenoci od potrzeb wywoujcego
Istnieje wiele sposobw klasyfikowania bdw. Moemy klasyfikowa je w zalenoci od rda:
czy pochodz one z tego komponentu, czy innego? Lub w zalenoci od typu: czy jest to awaria
urzdzenia, sieci, czy bd programowy? Jednak gdy definiujemy klasy wyjtkw w aplikacji,
najwiksz uwag powinnimy zwrci na sposb ich przechwytywania.
Spjrzmy na przykad sabej klasyfikacji wyjtkw. Mamy tu instrukcj try-catch-finally do
realizacji wywoania z zewntrznej biblioteki. Obejmuje ona wszystkie wyjtki, jakie mog zgosi
te wywoania:
ACMEPort port = new ACMEPort(12);
try {
port.open();
} catch (DeviceResponseException e) {

OBSUGA BDW

127

reportPortError(e);
logger.log("Wyjtek odpowiedzi urzdzenia", e);
} catch (ATM1212UnlockedException e) {
reportPortError(e);
logger.log("Wyjtek odblokowania", e);
} catch (GMXError e) {
reportPortError(e);
logger.log("Wyjtek odpowiedzi urzdzenia");
} finally {
...
}

Ta instrukcja zawiera wiele duplikatw, ale nie powinnimy by tym zaskoczeni. W wikszoci
przypadkw obsugi wyjtkw wykonywane operacje s wzgldnie standardowe, niezalenie od
faktycznej przyczyny. Musimy zarejestrowa bd i upewni si, e moemy kontynuowa.
W tym przypadku, poniewa wiemy, e wykonywane operacje s w przyblieniu identyczne, niezalenie od wyjtku, moemy znacznie uproci tworzony kod przez otoczenie wywoywanego API i upewnienie si, e zwraca wsplny typ wyjtku:
LocalPort port = new LocalPort(12);
try {
port.open();
} catch (PortDeviceFailure e) {
reportError(e);
logger.log(e.getMessage(), e);
} finally {
...
}

Nasza klasa LocalPort jest prost klas osonow, przechwytujc i przeksztacajc wyjtki zgaszane przez klas ACMEPort:
public class LocalPort {
private ACMEPort innerPort;
public LocalPort(int portNumber) {
innerPort = new ACMEPort(portNumber);
}

public void open() {


try {
innerPort.open();
} catch (DeviceResponseException e) {
throw new PortDeviceFailure(e);
} catch (ATM1212UnlockedException e) {
throw new PortDeviceFailure(e);
} catch (GMXError e) {
throw new PortDeviceFailure(e);
}
}

Tego rodzaju klasy osonowe, jakie zdefiniowalimy dla ACMEPort, mog by bardzo przydatne.
Faktycznie, osanianie API firm zewntrznych jest dobr praktyk. Gdy osaniamy API firmy zewntrznej, minimalizujemy zalenoci od niego w przyszoci mona bdzie zdecydowa si na
przejcie na inn bibliotek bez ponoszenia zbytnich kosztw. Osanianie uatwia rwnie tworzenie imitacji klas zewntrznych dla potrzeb testowania wasnego kodu.

128

ROZDZIA 7.

Ostatni zalet osaniania jest to, e nie jestemy przywizani do okrelonych decyzji projektowych
producenta API. Mona zdefiniowa API, ktre bdzie dla nas komfortowe. We wczeniejszym
przykadzie zdefiniowalimy jeden wyjtek dla awarii urzdzenia port i okazao si, e moemy pisa
znacznie czytelniejszy kod.
Czsto jedna klasa wyjtkw jest wystarczajca dla okrelonego obszaru kodu. Informacje wysyane
w wyjtku pozwalaj nam rozrnia bdy. Naley korzysta z rnych klas tylko w przypadkach,
gdy chcemy przechwyci jeden wyjtek, a inny chcemy przepuci.

Definiowanie normalnego przepywu


Jeeli stosujemy si do porad z wczeniejszych punktw,
uzyskamy wyrany rozdzia midzy logik biznesow
a obsug bdw. Kod zaczyna wyglda jak czysty,
schludny algorytm. Jednak proces dochodzenia do takiej
formy powoduje spychanie wykrywania bdw na
margines naszego programu. Osaniamy zewntrzne
API tak, aby zgaszay wyjtki, i definiujemy procedury
obsugi w kodzie, aby mona byo obsuy kad przerwan operacj. W wikszoci przypadkw jest to wietne
podejcie, ale zdarzaj si sytuacje, w ktrych moemy nie
chcie przerywa pracy.
Rozwamy nastpujcy przykad. Poniej zamieszczony jest dziwny kod, w ktrym sumujemy wydatki w aplikacji ksigowej:
try {
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();
} catch(MealExpensesNotFound e) {
m_total += getMealPerDiem();
}

W tej firmie wszystkie wydatki na posiki wchodz do podsumowania. Jeeli nie bdzie takiego
wydatku, pracownicy otrzymuj ryczat dzienny. Wyjtek zaciemnia logik kodu. Czy nie byoby
lepiej, gdybymy nie musieli obsugiwa specjalnego przypadku? Wwczas kod bdzie znacznie
prostszy. Mgby wyglda tak:
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();

Czy bdziemy w stanie tak uproci kod? Okazuje si, e tak. Zmienimy klas ExpenseReportDAO tak,
aby zawsze zwracaa obiekt MealExpense. Jeeli nie bd wystpoway wydatki na posiki, zwracany bdzie obiekt MealExpense zawierajcy warto ryczatu dziennego:
public class PerDiemMealExpenses implements MealExpenses {
public int getTotal() {

// Zwrcenie domylnej wartoci ryczatu dziennego.


}
}

OBSUGA BDW

129

Jest to przykad wzorca specjalnego przypadku (ang. Special Case Pattern) [Fowler]. Tworzymy
w nim klas lub konfigurujemy obiekt, aby obsugiwa szczeglny przypadek za nas. Po takiej operacji kod klienta nie bdzie musia obsugiwa sytuacji wyjtkowej. Obsuga ta jest hermetyzowana
w obiekcie przypadku specjalnego.

Nie zwracamy null


Uwaam, e kade omwienie obsugi bdw powinno zawiera przedstawienie operacji, ktre
powoduj powstanie bdw. Pierwsz pozycj na licie jest zwracanie wartoci null. Nie zlicz
widzianych przeze mnie aplikacji, w ktrych niemal kady wiersz kodu zawiera test wartoci null.
Poniej zamieszczony jest przykad takiego kodu:
public void registerItem(Item item) {
if (item != null) {
ItemRegistry registry = peristentStore.getItemRegistry();
if (registry != null) {
Item existing = registry.getItem(item.getID());
if (existing.getBillingPeriod().hasRetailOwner()) {
existing.register(item);
}
}
}
}

Gdy zwracamy warto null, w rzeczywistoci tworzymy sobie dodatkow prac i powodujemy
problemy w funkcjach wywoujcych. W takich przypadkach brak jednego testu wartoci null powoduje, e aplikacja wymyka si spod kontroli.
Czy Czytelnik zauway, e w drugim wierszu zagniedonej instrukcji if nie ma kontroli wartoci
null? Co si stanie w trakcie dziaania programu, jeeli persistentStore bdzie null? Otrzymamy
w takim przypadku wyjtek NullPointerException i albo kto przechwyci ten wyjtek na najwyszym poziomie, albo nie. W obu przypadkach jest to ze. Co mona zrobi w odpowiedzi na
wyjtek NullPointerException zgoszony w gbi naszej aplikacji?
atwo powiedzie, e problem w powyszym kodzie wynika z braku kontroli wartoci null, ale
w rzeczywistoci problem polega na tym, e jest ich zbyt duo. Jeeli mamy zamiar zwrci null
z metody, warto rozway zgoszenie wyjtku lub zwrcenie obiektu specjalnego przypadku. Jeeli
wywoujemy metod zwracajc null pochodzc z zewntrznego API, warto rozway osonicie
tej metody inn, ktra zgasza wyjtek lub zwraca obiekt specjalnego przypadku.
W wielu sytuacjach obiekty specjalnych przypadkw s prostym rozwizaniem. Wyobramy sobie,
e mamy nastpujcy kod:
List<Employee> employees = getEmployees();
if (employees != null) {
for(Employee e : employees) {
totalPay += e.getPay();
}
}

130

ROZDZIA 7.

Metoda getEmployees moe zwrci null, ale czy musi to robi? Jeeli zmienimy getEmployees
tak, aby zwracaa pust list, moemy wyczyci nasz kod:
List<Employee> employees = getEmployees();
for(Employee e : employees) {
totalPay += e.getPay();
}

Na szczcie Java posiada metod Collections.emptyList() zwracajc predefiniowan niezmienn list, ktra moe by wykorzystana do takich celw.
public List<Employee> getEmployees() {
if( .. brak pracownikw .. )
return Collections.emptyList();
}

Jeeli bdziemy tworzy kod w taki sposb, zmniejszymy moliwo powstawania wyjtkw
NullPointerException, a nasz kod bdzie czytelniejszy.

Nie przekazujemy null


Zwracanie wartoci null z metod jest niedobr praktyk, ale przekazywanie wartoci null do
metod jest jeszcze gorsze. O ile nie korzystamy z API, ktre oczekuje wartoci null, i o ile mamy
tak moliwo, powinnimy unika przekazywania null we wasnym kodzie. Dlaczego? Rozwamy
nastpujcy przykad. Mamy tu prost metod, ktra oblicza odlego midzy dwoma punktami:
public class MetricsCalculator
{
public double xProjection(Point p1, Point p2) {
return (p2.x p1.x) * 1.5;
}
...
}

Co si stanie, gdy kto przekae null jako argument?


calculator.xProjection(null, new Point(12, 13));

Oczywicie otrzymamy NullPointerException.


Jak mona to poprawi? Moemy utworzy nowy typ wyjtku i zgosi go:
public class MetricsCalculator
{
public double xProjection(Point p1, Point p2) {
if (p1 == null || p2 == null) {
throw InvalidArgumentException(
"Niewaciwy argument dla MetricsCalculator.xProjection");
}
return (p2.x p1.x) * 1.5;
}
}

Czy tak jest lepiej? By moe jest to lepsze ni wyjtek wskanika null, ale naley pamita, e musimy
zdefiniowa procedur obsugi zdarzenia InvalidArgumentException. Co powinna robi taka
procedura? Czy istnieje waciwa akcja?

OBSUGA BDW

131

Istnieje inna opcja. Moemy skorzysta ze zbioru asercji.


public class MetricsCalculator
{
public double xProjection(Point p1, Point p2) {
assert p1 != null : "p1 nie moe by null";
assert p2 != null : "p2 nie moe by null";
return (p2.x p1.x) * 1.5;
}
}

Jest to dobra dokumentacja, ale nie rozwizuje problemu. Jeeli kto przekae null, otrzyma bd
wykonania.
W wikszoci jzykw programowania nie istnieje dobra metoda obsugi wartoci null przypadkowo przekazanych przez wywoujc procedur. Z tego powodu racjonalnym podejciem jest zakazanie przekazywania wartoci null. W takim przypadku moemy tworzy nasz kod, wiedzc, e
null w licie argumentw jest symptomem problemu, co bdzie skutkowao znacznie mniejsz
liczb pomyek.

Zakoczenie
Czysty kod jest czytelny, ale musi by rwnie solidny. Nie s to cele pozostajce w konflikcie. Moemy
pisa solidny czysty kod, jeeli bdziemy traktowa obsug bdw jako osobne zadanie, co, co
jest widziane niezalenie od gwnej logiki. Takie podejcie do problemu uatwi konserwacj kodu
w przyszoci.

Bibliografia
[Martin]: Robert C. Martin, Agile Software Development: Principles, Patterns, and Practices,
Prentice Hall 2002.

132

ROZDZIA 7.

ROZDZIA 8.

Granice
James Grenning

naszym systemie. Czasami kupujemy pakiety


R firm zewntrznych lub korzystamy z kodu open wsource.
W innym przypadku polegamy na zespoZADKO KONTROLUJEMY WSZYSTKIE ELEMENTY KODU

ach z naszej firmy, ktre produkuj dla nas komponenty lub podsystemy. W jaki sposb musimy w czysty sposb zintegrowa ten obcy kod z naszym. W tym rozdziale zapoznamy si z praktykami i technikami pozwalajcymi na zachowanie czystych granic naszego oprogramowania.

133

Zastosowanie kodu innych firm


Istnieje naturalne napicie pomidzy dostawc interfejsu a jego uytkownikiem. Dostawcy pakietw i bibliotek staraj si o szeroki zakres zastosowa, wic dziaaj w wielu rodowiskach i odwouj si do szerokiej gamy odbiorcw. Z drugiej strony, uytkownicy daj interfejsu, ktry jest
przystosowany do ich konkretnych potrzeb. Takie napicia mog powodowa problemy na granicach naszych systemw.
Rozwamy przykad klasy java.util.Map. Jak wynika z analizy rysunku 8.1, Map ma bardzo szeroki interfejs z wieloma moliwociami. Oczywicie, sia i elastyczno to przydatne cechy, ale mog rwnie wiza si z du odpowiedzialnoci. Nasze aplikacje mog na przykad wykorzystywa obiekty Map oraz je przesya. Naszym zaoeniem moe by, e aden z odbiorcw naszych
obiektw Map nie bdzie z nich nic usuwa. Jednak bezporednio na pocztku listy metod mamy
clear(). Kady uytkownik obiektu Map ma moliwo wyczyszczenia go. By moe nasza konwencja projektowa dyktuje, e w obiekcie Map mona przechowywa obiekty okrelonego typu, ale
obiekty Map nie mog w niezawodny sposb ogranicza typw obiektw w nich umieszczanych.
Kady zdeterminowany uytkownik moe dodawa do Map elementy dowolnego typu.

R Y S U N E K 8 . 1 . Metody klasy Map

Jeeli nasza aplikacja potrzebuje obiektu Map dla obiektw Sensor, do tworzenia kontenera wykorzystamy nastpujcy kod:
Map sensors = new HashMap();

Nastpnie, gdy w innej czci kodu bdziemy chcieli odwoa si do jego zawartoci, uyjemy
nastpujcego kodu:
Sensor s = (Sensor)sensors.get(sensorId );

134

ROZDZIA 8.

Nie bdziemy uywa tego wywoania jednokrotnie, ale wiele razy w caym kodzie. Klient tego
kodu jest odpowiedzialny za pobranie obiektu Object z Map i rzutowanie go na waciwy typ.
To dziaa, ale nie jest czystym kodem. Dodatkowo kod ten nie opowiada historii, tak jak powinien.
Niezawodno tego kodu moe by znacznie poprawiona przez zastosowanie typw oglnych, jak
poniej:
Map<Sensor> sensors = new HashMap<Sensor>();
...
Sensor s = sensors.get(sensorId );

Nie rozwizuje to jednak problemu dostarczania przez Map<Sensor> wikszych moliwoci, ni


potrzebujemy.
Przekazywanie obiektu Map<Sensor> w caym systemie powoduje, e w przypadku jakiejkolwiek
zmiany interfejsu Map konieczne bdzie wprowadzenie zmian w wielu miejscach. Mona myle, e
taka zmiana jest mao prawdopodobna, ale naley pamita, e ju zdarzya si po dodaniu obsugi
typw oglnych w Java 5. Faktycznie, spotkaem si z systemami, w ktrych zrezygnowano z uycia
typw oglnych z powodu koniecznoci wprowadzenia olbrzymiej liczby zmian, ktre byy wymagane, aby mona byo skorzysta z interfejsu Map.
atwiejszy sposb zastosowania Map jest przedstawiony poniej. aden z uytkownikw klasy Sensors
nie bdzie wiedzia, czy typy oglne zostay w niej uyte, czy nie. Wybr ten sta si (i zawsze by)
szczegem implementacji.
public class Sensors {
private Map sensors = new HashMap();
public Sensor getById(String id) {
return (Sensor) sensors.get(id);
}

// Odcinamy
}

Interfejs graniczny (Map) zosta ukryty. Jego ewolucja bdzie miaa niewielki wpyw na pozostae
czci aplikacji. Zastosowanie typw oglnych nie stanowi ju duego problemu, poniewa rzutowanie i zarzdzanie typami jest obsuone wewntrz klasy Sensors.
Interfejs ten jest rwnie odpowiednio dostosowany i ograniczony, aby spenia potrzeby aplikacji.
Wynikowy kod jest przez to atwiejszy do zrozumienia i trudniej popeni pomyk. Klasa Sensors moe wymusza zasady projektowe i biznesowe.
Nie sugerujemy jednak, aby kade uycie Map byo w ten sposb hermetyzowane. Zamiast tego
radzimy nie przekazywa obiektw Map (ani innych interfejsw granicznych) po caym systemie.
Jeeli korzystamy z interfejsu granicznego, takiego jak Map, naley korzysta z niego w klasie lub
w bliskiej rodzinie klas, w ktrych jest wykorzystywany. Naley unika zwracania go lub wykorzystywania jako argument w publicznym API.

GRANICE

135

Przegldanie i zapoznawanie si z granicami


Kod firm zewntrznych pozwala na dostarczenie wikszej liczby funkcji w krtszym czasie. Od
czego zacz, gdy bdziemy chcieli wykorzysta obcy pakiet? Nie naszym zadaniem jest testowanie
obcego kodu, ale w naszym interesie jest napisanie testw dla tego kodu, z ktrego korzystamy
w projekcie.
Zamy, e nie jest jasne, jak uywa pewnej biblioteki obcej firmy. Moemy spdzi dzie lub dwa
(albo wicej), czytajc dokumentacj i podejmujc decyzj, jak bdziemy korzysta z tej biblioteki.
Nastpnie moemy napisa nasz kod wykorzystujcy kod firmy zewntrznej i sprawdzi, czy realizuje to, czego si spodziewalimy. Nie bdzie niespodziank, gdy spdzimy dugie godziny na
debugowaniu i prbach zorientowania si, czy bdy znajduj si w naszym kodzie, czy obcym.
Uczenie si obcego kodu jest zawsze trudne. Integrowanie kodu obcego rwnie jest trudne. Wykonywanie tych dwch czynnoci jednoczenie jest podwjnie trudne. By moe warto sprbowa
innego podejcia? Zamiast eksperymentowa z wykorzystaniem nowych funkcji w kodzie produkcyjnym, powinnimy napisa kilka testw w celu skontrolowania naszego zrozumienia analizowanego kodu. Jim Newkirk nazywa to testami uczcymi1.
W testach uczcych wywoujemy API zewntrzne tak, jak robilibymy to w naszej aplikacji. W rzeczywistoci wykonujemy kontrolowane eksperymenty, sprawdzajce nasz wiedz na temat API.
Testy skupiaj si na elementach, ktre chcemy uzyska z API.

Korzystanie z pakietu log4j


Zamy, e chcemy uy pakietu log4j z Apache zamiast naszego wasnego pakietu rejestrowania.
Pobieramy go i otwieramy stron wprowadzajc dokumentacji. Bez dokadnej lektury jestemy
w stanie napisa nasz pierwszy test, w ktrym oczekujemy wypisania sowa cze na konsoli.
@Test
public void testLogCreate() {
Logger logger = Logger.getLogger("MyLogger");
logger.info("cze");
}

Po jego uruchomieniu program zgasza bd informujcy, e potrzebujemy czego o nazwie


Appender. Po krtkiej lekturze znajdujemy informacj, e dostpna jest klasa ConsoleAppender.
Tworzymy wic ConsoleAppender i sprawdzamy, czy odkrylimy sekrety kierowania komunikatw
na konsol.
@Test
public void testLogAddAppender() {
Logger logger = Logger.getLogger("MyLogger");
ConsoleAppender appender = new ConsoleAppender();
logger.addAppender(appender);
logger.info("cze");
}
1

[BeckTDD], s. 136 137.

136

ROZDZIA 8.

Tym razem okazuje si, e Appender nie posiada strumienia wyjciowego. Dziwne wydaje si
logiczne, e go mamy. Z niewielk pomoc Google prbujemy czego takiego:
@Test
public void testLogAddAppender() {
Logger logger = Logger.getLogger("MyLogger");
logger.removeAllAppenders();
logger.addAppender(new ConsoleAppender(
new PatternLayout("%p %t %m%n"),
ConsoleAppender.SYSTEM_OUT));
logger.info("cze");
}

Tym razem program dziaa i na konsoli pojawia si komunikat cze. Wydaje si dziwne, e musimy
poinformowa ConsoleAdapter, e ma pisa na konsol.
Co ciekawe, gdy usuniemy argument ConsoleAppender.SystemOut, komunikat nadal bdzie wywietlany. Gdy usuniemy PatternLayout, program znw zacznie informowa o braku strumienia
wyjciowego. Jest to bardzo dziwne dziaanie.
Po dokadniejszym przyjrzeniu si dokumentacji zauwaamy, e domylny konstruktor ConsoleAdapter
jest nieskonfigurowany, co nie wydaje si zbyt oczywiste ani uyteczne. Wyglda to na bd lub
co najmniej na niekonsekwencj w pakiecie log4j.
Po dalszym szukaniu, czytaniu i testowaniu w kocu otrzymujemy kod z listingu 8.1. Wiele dowiedzielimy si na temat sposobu dziaania pakietu log4j i zawarlimy t wiedz w zbiorze prostych
testw jednostkowych.
L I S T I N G 8 . 1 . LogTest.java
public class LogTest {
private Logger logger;
@Before
public void initialize() {
logger = Logger.getLogger("logger");
logger.removeAllAppenders();
Logger.getRootLogger().removeAllAppenders();
}
@Test
public void basicLogger() {
BasicConfigurator.configure();
logger.info("basicLogger");
}

@Test
public void addAppenderWithStream() {
logger.addAppender(new ConsoleAppender(
new PatternLayout("%p %t %m%n"),
ConsoleAppender.SYSTEM_OUT));
logger.info("addAppenderWithStream");
}
@Test
public void addAppenderWithoutStream() {
logger.addAppender(new ConsoleAppender(
new PatternLayout("%p %t %m%n")));
logger.info("addAppenderWithoutStream");
}

GRANICE

137

Teraz wiemy ju, jak naley zainicjowa prosty system rejestrowania na konsoli, i moemy umieci t wiedz we wasnej klasie, aby pozostaa cz aplikacji bya izolowana od interfejsu granicznego log4j.

Zalety testw uczcych


Testy uczce nic nie kosztuj. I tak musimy nauczy si uywanego API, a pisanie takich testw jest
prostym sposobem uzyskania tej wiedzy. Testy uczce s precyzyjnymi eksperymentami, ktre
pomagaj nam zwikszy wiedz.
Testy uczce nie tylko s bezpatne, ale rwnie przynosz wymierne korzyci. Gdy pojawi si
nowa wersja pakietu zewntrznego, uruchamiamy testy uczce i sprawdzamy, czy wystpi rnice
w dziaaniu.
Testy te pozwalaj sprawdzi, czy uywane przez nas obce pakiety dziaaj tak, jak tego oczekujemy. Po zintegrowaniu obcego kodu z wasnym nie mamy gwarancji, e ten zewntrzny kod bdzie nadal spenia nasze oczekiwania. Jego autorzy mogli zmieni swj kod w taki sposb, aby
spenia inne oczekiwania. Poprawiaj oni bdy i dodaj nowe funkcje. Kada nowa wersja niesie
ze sob nowe zagroenia. Jeeli pakiet firmy zewntrznej zmieni si w taki sposb, e bdzie niezgodny z naszymi testami, natychmiast si o tym dowiemy.
Niezalenie od tego, czy potrzebujemy wiedzy zdobywanej za pomoc testw uczcych, czy nie,
powinnimy pokry kod graniczny testami, ktre sprawdzaj interfejs w taki sam sposb, jak dzieje
si to w kodzie produkcyjnym. Bez tych testw granicznych moemy np. korzysta ze starej wersji
biblioteki duej, ni jest to potrzebne.

Korzystanie z nieistniejcego kodu


Istnieje rwnie inny rodzaj granicy, ktra oddziela znane od nieznanego. Czsto spotykamy si
z takimi fragmentami kodu, w ktrych nasza wiedza jest wystawiona na prb. Czasami to, co
znajduje si po drugiej stronie granicy, jest nieznane (przynajmniej chwilowo). Czasami decydujemy si,
aby nie zaglda za granic.
Kilka lat temu byem czonkiem zespou tworzcego oprogramowanie dla systemu komunikacji
radiowej. Istnia w nim podsystem Transmitter, o ktrym niewiele wiedzielimy, a osoby odpowiedzialne za ten podsystem nie wpady jeszcze na pomys, aby zdefiniowa interfejs. Nie chcielimy by przez to blokowani, wic zaczlimy prac od odlegych czci kodu.
Wiedzielimy dokadnie, gdzie koczy si nasz, a zaczyna ich wiat. W czasie naszej pracy czasami
odbijalimy si od tej granicy. Pomimo e mga i chmury ignorancji przesaniay nasz widok poza
granic, w czasie pracy okrelilimy, jaki chcielibymy mie interfejs graniczny. Chcielimy przekaza do transmitera mniej wicej tak informacj:
Ustaw transmiter na przekazanej czstotliwoci i wyemituj analogow reprezentacj danych
przychodzcych w tym strumieniu.
138

ROZDZIA 8.

Nie wiedzielimy, jak powinno by to zrobione, poniewa API nie zostao jeszcze zaprojektowane.
Zdecydowalimy wic popracowa pniej nad szczegami.
Aby nie blokowa prac, zdefiniowalimy wasny interfejs. Nadalimy mu jak chwytliw nazw,
co jak Transmitter. Dodalimy do niej metod transmit, ktra oczekiwaa przekazania czstotliwoci i strumienia danych. By to interfejs, ktrego oczekiwalimy.
Gwn zalet tworzenia oczekiwanego interfejsu jest to, e pozostaje on pod nasz kontrol.
Pozwala to na zachowanie wikszej czytelnoci kodu i ukierunkowanie go na wykonywan akcj.
Na rysunku 8.2 mona zobaczy, e odizolowalimy klasy CommunicationsController od API
transmitera (ktry by poza nasz kontrol i nie by jeszcze zdefiniowany). Przez uycie interfejsu
specyficznego dla interfejsu nasz kod CommunicationsController pozosta czysty i komunikatywny. Po zdefiniowaniu API transmitera napisalimy klas TransmitterAdapter atajc dziur. Adapter2 hermetyzowa interakcj z API i zapewnia jedno miejsce zmian w przypadku
ewolucji API.

R Y S U N E K 8 . 2 . Przewidywanie klas transmitera

Projekt ten pozwoli na utworzenie bardzo wygodnego szwu3 w kodzie, co uatwia testowanie. Przy uyciu
odpowiedniej klasy FakeTransmitter moglimy przetestowa klasy CommunicationsController.
Moglimy rwnie utworzy testy graniczne po uzyskaniu TransmitterAPI, aby upewni si, e
prawidowo korzystamy z tego API.

Czyste granice
Na granicach dziej si interesujce zdarzenia. Wrd nich s m.in. zmiany. Dobry projekt oprogramowania przyjmuje zmiany bez ogromnych nakadw i ponownej pracy. Gdy korzystamy z kodu pozostajcego poza nasz kontrol, musimy uy specjalnych metod w celu ochrony naszych
inwestycji, ktre zapewniaj, e przysze modyfikacje nie bd zbyt kosztowne.
Kod na tych granicach wymaga jasnej separacji i testw definiujcych oczekiwania. Powinnimy
unika sytuacji, w ktrej zbyt wiele naszego kodu korzysta ze szczegw zdefiniowanych w obcym
kodzie. Lepiej polega na czym, co my moemy kontrolowa, ni na czym, czego nie moemy
kontrolowa, poniewa koczy si to... kontrolowaniem nas.
2

Patrz wzorzec Adapter w [GOF].

Wicej informacji na temat szww mona znale w [WELC].

GRANICE

139

Zarzdzamy granicami z obcym kodem przez ograniczenie do minimum miejsc w kodzie odwoujcych si do niego. Moemy hermetyzowa ten kod tak, jak zrobilimy to w Map, lub uy adaptera
w celu konwersji naszego perfekcyjnego interfejsu na dostarczony interfejs. W obu przypadkach
kod lepiej do nas przemawia, promuje wewntrzne spjne nazewnictwo na granicach i posiada
mniej punktw konserwacji w przypadku zmiany zewntrznego kodu.

Bibliografia
[BeckTDD]: Kent Beck, Test Driven Development, Addison-Wesley 2003.
[GOF]: Gamma, Design Patterns: Elements of Reusable Object Oriented Software, Addison-Wesley 1996.
[WELC]: Working Effectively with Legacy Code, Addison-Wesley 2004.

140

ROZDZIA 8.

ROZDZIA 9.

Testy jednostkowe

ostatnich dziesiciu lat. W roku 1997 nikt nie syN sza o programowaniu sterowanym testamiw czasie
(ang. Test Driven Development). Dla znacznej wikszoASZA PROFESJA PRZESZA ZNACZNE ZMIANY

ci z nas testy jednostkowe byy krtkimi fragmentami kodu, ktre pisalimy w celu upewnienia si,
e nasze programy dziaaj. Z mozoem pisalimy klasy i metody, a nastpnie tworzylimy fragmenty kodu do ich przetestowania. Zwykle wymagao to uycia jakiego programu sterujcego,
ktry pozwala rcznie sterowa napisanym kodem.
W latach dziewidziesitych pisaem program w jzyku C++ dla wbudowanego systemu czasu
rzeczywistego. Program by prostym stoperem o nastpujcej sygnaturze:
void Timer::ScheduleCommand(Command* theCommand, int milliseconds)

141

Pomys by dosy prosty, metoda execute obiektu Command wywoywaa nowy wtek po zdefiniowanej liczbie milisekund. Powsta problem, jak go przetestowa.
Skleciem wic prosty program sterujcy, ktry nasuchiwa zdarze klawiatury. Po kadym wpisaniu znaku planowane byo wykonanie polecenia wpisujcego ten sam znak pi sekund pniej.
Nastpnie wystukiwaem rytmiczn melodi na klawiaturze i melodia ta bya odtwarzana na ekranie po piciu sekundach.
I want-a-girl just like-the-girl-who-marr ied dear old dad.
Faktycznie nuciem t melodi, naciskajc klawisz ., a potem ponownie j nuciem, gdy kropki
pojawiay si na ekranie.
To by mj test! Gdy sprawdziem, e kod dziaa, i pokazaem to moim kolegom, skasowaem
kod testu.
Jak powiedziaem, nasza profesja przesza znaczne zmiany. Obecnie napisabym test, ktry upewniby mnie, e kady fragment i zaktek kodu dziaa tak, jak tego oczekuj. Wyizolowabym swj
kod z systemu operacyjnego, zamiast wywoywa standardowe funkcje stopera. Uybym imitacji
tych funkcji stopera, aby mie pen kontrol nad czasem. Zaplanowabym polecenia, ktre ustawiayby znaczniki, a nastpnie po upywie okrelonego czasu kontrolowabym te znaczniki, upewniajc si, e zmieniy swj stan na waciwy dla czasu, jaki bym ustawi.
Gdy testy byyby wykonywane prawidowo, upewnibym si, e moe je bez trudu uruchomi kady,
kto bdzie chcia korzysta z mojego kodu. Upewnibym si, e testy i sprawdzany przez nie kod
znajduj si w tym samym pakiecie kodu rdowego.
Tak, przeszlimy dug drog, ale musimy pj jeszcze dalej. Metodologie Agile oraz TDD zachciy
wielu programistw do pisania automatycznych testw jednostkowych i kadego dnia ich liczba
ronie. Jednak w owczym pdzie do korzystania z testw wielu programistw nie zdaje sobie sprawy
z kilku bardziej subtelnych i istotnych cech dobrych testw.

Trzy prawa TDD


Obecnie wszyscy wiedz, e TDD zakada pisanie testw jednostkowych na pocztku, przed napisaniem kodu produkcyjnego. Jednak zasada ta jest tylko wierzchokiem gry lodowej. Moemy
zdefiniowa nastpujce trzy prawa1:
Pierwsze prawo. Nie mona zacz pisa kodu produkcyjnego do momentu napisania niespenianego testu jednostkowego.
Drugie prawo. Nie mona napisa wicej testw jednostkowych, ktre s wystarczajce do niespenienia testu, a brak kompilacji jest jednoczenie nieudanym testem.
1

Robert C. Martin, Professionalism and Test-Driven Development, Object Mentor, IEEE Software, maj czerwiec 2007
(Vol. 24, No. 3), s. 32 36, http://doi.ieeecomputersociety.org/10.1109/MS.2007.85.

142

ROZDZIA 9.

Trzecie prawo. Nie mona pisa wikszej iloci kodu produkcyjnego, ni wystarczy do spenienia
obecnie niespenianego testu.
Te trzy prawa zamykaj nas w cyklu, ktry trwa prawdopodobnie trzydzieci sekund. Testy i kod
produkcyjny s pisane razem, a testy s wykonywane kilka sekund wczeniej ni kod produkcyjny.
Jeeli bdziemy w ten sposb pracowa, napiszemy kilkanacie testw kadego dnia, setki kadego
miesica i tysice testw co rok. Testy te bd pokrywa niemal cay kod produkcyjny. Ogromna
liczba tych testw, ktre bd podobnej objtoci co kod produkcyjny, moe prowadzi do powanych problemw z zarzdzaniem projektem.

Zachowanie czystoci testw


Kilka lat temu zostaem poproszony o kierowanie zespoem, ktry wczeniej zdecydowa, e jego
kod testujcy nie powinien by utrzymywany zgodnie z tymi samymi standardami jakoci co kod
produkcyjny. Programici dali sobie przyzwolenie na amanie regu w testach jednostkowych.
Myl przewodni byo szybki i brudny. Zmienne nie musiay mie odpowiednich nazw, funkcje
testujce nie musiay by krtkie i opisowe. Ich kod testujcy nie musia by odpowiednio zaprojektowany i waciwie podzielony. Dopki kod testu dziaa i pokrywa kod produkcyjny, by wystarczajco dobry.
Cz Czytelnikw moe sympatyzowa z tymi decyzjami. By moe w dalekiej przeszoci pisali
oni testy podobne do mojego testu klasy Timer. Jednak pomidzy tego rodzaju odrzucanymi
testami a automatycznymi testami jednostkowymi istnieje ogromna przepa. Tak wic podobnie
w kierowanym przeze mnie zespole moglibymy zdecydowa, e posiadanie brudnych testw jest
lepsze ni ich brak.
Jednak zesp ten nie zorientowa si, e korzystanie z takich testw odpowiada... brakowi testw,
o ile nie jest czym gorszym. Problem wynika z tego, e testy musz si zmienia wraz z ewoluowaniem kodu produkcyjnego. Im gorsze s testy, tym trudniej je zmienia. Im bardziej zagmatwany
jest kod testujcy, tym wiksze prawdopodobiestwo, e spdzimy wicej czasu na dodawaniu nowych testw do pakietu ni na pisaniu nowego kodu produkcyjnego. Wraz z modyfikowaniem kodu produkcyjnego stare testy przestaj si wykonywa, a baagan w kodzie testujcym utrudnia
doprowadzenie ich do stanu zgodnego z kodem produkcyjnym. Dlatego testy zaczynaj by postrzegane jako stale zwikszajca si odpowiedzialno.
W kolejnych wersjach koszt utrzymania zestawu testw w moim zespole stale wzrasta. W kocu
sta si najwiksz uciliwoci dla programistw. Gdy kierownicy pytali, dlaczego ich oszacowania s tak due, programici winili za to testy. W kocu zostali zmuszeni do cakowitego usunicia
zestawu testw.
Jednak bez zestawu testw utracilimy moliwo upewnienia si, e zmiany w bazie kodu dziaaj
w oczekiwany sposb. Bez zestawu testw nie moglimy upewni si, e zmiany w jednej czci
systemu nie spowoduj bdu w innych jego czciach. Dlatego liczba defektw zacza wzrasta.

TESTY JEDNOSTKOWE

143

Gdy liczba nieoczekiwanych defektw zacza wzrasta, programici zaczli obawia si wprowadzania zmian. Przestali czyci swj kod produkcyjny, poniewa obawiali si, e zmiany wprowadz
wicej szkody ni poytku. Ich kod produkcyjny zacz si psu. W kocu zostali bez testw, z zagmatwanym i zawierajcym bdy kodem produkcyjnym, sfrustrowanymi klientami oraz poczuciem, e
ich praca nad testami posza na marne.
W pewnym stopniu mieli racj. Ich praca przy testach zawioda. Jednak ich decyzja o pozwoleniu
na tworzenie zagmatwanych testw bya zalkiem poraki. Gdyby ich testy byy utrzymywane w odpowiednim stanie, praca przy testach nie poszaby na marne. Mog to powiedzie z du pewnoci, poniewa braem udzia w projektach i kierowaem wieloma zespoami, ktre z duym sukcesem korzystay z czystych testw jednostkowych.
Mora z tej historii jest prosty: kod testw jest tak samo wany jak kod produkcyjny. Nie jest to obywatel drugiej kategorii. Wymaga przemylenia, zaprojektowania i uwagi. Musi by utrzymywany
zgodnie z takimi samymi zasadami co kod produkcyjny.

Testy zwikszaj moliwoci


Jeeli nie bdziemy zachowywali czystoci testw, utracimy je. Bez nich utracimy jedyn rzecz, ktra
zapewnia elastyczno kodu produkcyjnego. Tak, dobrze przeczytalicie. To wanie testy jednostkowe zapewniaj elastyczno, atwo utrzymania i ponownego wykorzystania kodu. Powd jest
prosty. Jeeli mamy testy, nie musimy ba si wprowadzania zmian do kodu! Bez testw kada
zmiana jest potencjalnym bdem. Niezalenie od tego, jak elastyczna jest nasza architektura, niezalenie od tego, jak dobrze partycjonowany jest nasz projekt, bez testw bdziemy obawiali si
wprowadza zmiany z powodu strachu przed wprowadzeniem niewykrytych bdw.
Jednak gdy testy s uywane, obawa ta znika niemal zupenie. Im lepsze jest pokrycie kodu testami,
tym obawa jest mniejsza. Mona niemal bezkarnie wprowadza zmiany do kodu, ktrego architektura nie jest doskonaa, a projekt jest zagmatwany i niejasny. Mona nawet bez obawy poprawia t architektur i projekt!
Tak wic posiadanie zestawu testw automatycznych obejmujcych kod produkcyjny jest kluczowe
w utrzymaniu projektu i architektury w moliwie najlepszym stanie. Testy znacznie zwikszaj
moliwoci, poniewa pozwalaj na zmiany.
Jeeli nasze testy s zagmatwane, to moliwoci zmiany kodu s zmniejszone i zaczynamy traci
moliwo poprawiania struktury kodu. Im gorsze s nasze testy, tym bardziej zagmatwany staje
si kod. W kocu tracimy testy, a nasz kod zaczyna si psu.

Czyste testy
Co sprawia, e testy s czyste? Trzy elementy: czytelno, czytelno i jeszcze raz czytelno.
Czytelno jest prawdopodobnie waniejsza w przypadku testw jednostkowych ni w kodzie
produkcyjnym. Co sprawia, e testy s czytelne? Te same elementy, ktre powoduj, e kady

144

ROZDZIA 9.

kod jest czytelny klarowno, prostota i spjno kodu. W tecie chcemy wyrazi moliwie duo
przy uyciu moliwie niewielu wyrae.
Przeanalizujmy kod z listingu 9.1, pochodzcy z FitNesse. Testy te s trudne do zrozumienia i powinny by poprawione. Po pierwsze, mamy tu ogromn ilo powtarzanego kodu [G5] w powtarzanych wywoaniach addPage oraz assertSubString. Co waniejsze, kod ten jest przeadowany
szczegami, ktre niekorzystnie wpywaj na jasno wyrae w testach.
L I S T I N G 9 . 1 . SerializedPageResponderTest.java
public void testGetPageHieratchyAsXml() throws Exception
{
crawler.addPage(root, PathParser.parse("PageOne"));
crawler.addPage(root, PathParser.parse("PageOne.ChildOne"));
crawler.addPage(root, PathParser.parse("PageTwo"));
request.setResource("root");
request.addInput("type", "pages");
Responder responder = new SerializedPageResponder();
SimpleResponse response =
(SimpleResponse) responder.makeResponse(
new FitNesseContext(root), request);
String xml = response.getContent();
assertEquals("text/xml", response.getContentType());
assertSubString("<name>PageOne</name>", xml);
assertSubString("<name>PageTwo</name>", xml);
assertSubString("<name>ChildOne</name>", xml);
}
public void testGetPageHieratchyAsXmlDoesntContainSymbolicLinks()
throws Exception
{
WikiPage pageOne = crawler.addPage(root, PathParser.parse("PageOne"));
crawler.addPage(root, PathParser.parse("PageOne.ChildOne"));
crawler.addPage(root, PathParser.parse("PageTwo"));
PageData data = pageOne.getData();
WikiPageProperties properties = data.getProperties();
WikiPageProperty symLinks = properties.set(SymbolicPage.PROPERTY_NAME);
symLinks.set("SymPage", "PageTwo");
pageOne.commit(data);
request.setResource("root");
request.addInput("type", "pages");
Responder responder = new SerializedPageResponder();
SimpleResponse response =
(SimpleResponse) responder.makeResponse(
new FitNesseContext(root), request);
String xml = response.getContent();
assertEquals("text/xml", response.getContentType());
assertSubString("<name>PageOne</name>", xml);
assertSubString("<name>PageTwo</name>", xml);
assertSubString("<name>ChildOne</name>", xml);
assertNotSubString("SymPage", xml);
}
public void testGetDataAsHtml() throws Exception
{
crawler.addPage(root, PathParser.parse("TestPageOne"), "test page");

TESTY JEDNOSTKOWE

145

request.setResource("TestPageOne");
request.addInput("type", "data");
Responder responder = new SerializedPageResponder();
SimpleResponse response =
(SimpleResponse) responder.makeResponse(
new FitNesseContext(root), request);
String xml = response.getContent();
assertEquals("text/xml", response.getContentType());
assertSubString("test page", xml);
assertSubString("<Test", xml);
}

Dla przykadu spjrzmy na wywoania PathParser. Przeksztacaj one cig znakw w obiekt
PagePath wykorzystywany przez mechanizm przeszukiwania. Transformacja ta jest niepotrzebna
do testowania i jedynie zaciemnia nasze intencje. Szczegy konieczne do utworzenia obiektu
responder oraz zbierania i rzutowania response s rwnie tylko szumem. Nastpnie znajduje si
tam mechanizm budowania URL dania na podstawie obiektu resource oraz argumentu (pomagaem pisa ten kod, wic mog swobodnie go krytykowa).
W kocu kod ten nie by zaprojektowany do czytania. Biedny Czytelnik zostanie zarzucony wieloma
szczegami, jakie musi zrozumie, aby testy miay sens.
Popatrzmy teraz na ulepszone testy z listingu 9.2. Testy te wykonuj dokadnie te same zadania, ale
zostay przebudowane do czystszej i bardziej zrozumiaej postaci.
L I S T I N G 9 . 2 . SerializedPageResponderTest.java (przebudowany)
public void testGetPageHierarchyAsXml() throws Exception {
makePages("PageOne", "PageOne.ChildOne", "PageTwo");
submitRequest("root", "type:pages");

assertResponseIsXML();
assertResponseContains(
"<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>"
);

public void testSymbolicLinksAreNotInXmlPageHierarchy() throws Exception {


WikiPage page = makePage("PageOne");
makePages("PageOne.ChildOne", "PageTwo");
addLinkTo(page, "PageTwo", "SymPage");
submitRequest("root", "type:pages");

assertResponseIsXML();
assertResponseContains(
"<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>"
);
assertResponseDoesNotContain("SymPage");

public void testGetDataAsXml() throws Exception {


makePageWithContent("TestPageOne", "test page");
submitRequest("TestPageOne", "type:data");

146

assertResponseIsXML();
assertResponseContains("test page", "<Test");

ROZDZIA 9.

Struktura tych testw powoduje, e wzorzec BUILD-OPERATE-CHECK2 jest oczywisty. Kady


z tych testw jest podzielony na trzy czci. Pierwsza cz buduje dane testowe, druga operuje na
danych testowych, a trzecia kontroluje, by operacja daa oczekiwane wyniki.
Naley zwrci uwag, e wikszo irytujcych szczegw zostaa wyeliminowana. Testy id
w dobrym kierunku i korzystaj tylko z tych typw danych i funkcji, ktre s faktycznie potrzebne.
Kady, kto czyta te testy, powinien mc zorientowa si, co si dzieje, bez koniecznoci analizowania mnstwa szczegw.

Jzyki testowania specyficzne dla domeny


Testy z listingu 9.2 demonstruj technik budowania jzyka specyficznego dla domeny na potrzeby
naszych testw. Zamiast korzysta z API stosowanego przez programistw do manipulowania
systemem, budujemy zbir funkcji i narzdzi wykorzystujcych to API, ktre pozwalaj na wygodniejsze pisanie testw, ktre z kolei s atwiejsze do czytania. Te funkcje i narzdzia staj si specjalizowanym API wykorzystywanym przez testy. S one jzykiem testowania wykorzystywanym przez programistw przy pisaniu testw oraz wspierajcym osoby czytajce pniej testy.
Takie API nie jest zaprojektowane od pocztku; raczej ewoluowao ze stale przebudowywanego
kodu testowego, ktry sta si zbyt zatoczony przez szczegy. Podobnie jak przebudowalimy kod
z listingu 9.1 na ten z listingu 9.2, zdyscyplinowani programici przebudowuj swj kod testowy na
bardziej zwize i czytelne formy.

Podwjny standard
Wspomniany przeze mnie na pocztku tego rozdziau zesp mia racj w jednej sprawie. Kod w API
testowym faktycznie ma inny zbir standardw inynierskich ni kod produkcyjny. Musi nadal by
prosty, zwizy i czytelny, ale nie musi by tak wydajny jak kod produkcyjny. W kocu dziaa on w rodowisku testowym, a nie produkcyjnym, a te dwa rodowiska maj cakiem inne wymagania.
Przeanalizujmy kod z listingu 9.3. Napisaem te testy jako cz systemu kontrolowania rodowiska,
ktrego prototyp opracowaem. Bez wchodzenia w szczegy mona powiedzie, e testy te kontroluj,
czy alarm niskiej temperatury, ogrzewacz i wentylator s wczone, gdy temperatura jest wyranie
zbyt niska.
L I S T I N G 9 . 3 . EnvironmentControllerTest.java
@Test
public void turnOnLoTempAlarmAtThreashold() throws Exception {
hw.setTemp(WAY_TOO_COLD);
controller.tic();
assertTrue(hw.heaterState());
assertTrue(hw.blowerState());
assertFalse(hw.coolerState());
assertFalse(hw.hiTempAlarm());
assertTrue(hw.loTempAlarm());
}
2

http://fitnesse.org/FitNesse.AcceptanceTestPatterns

TESTY JEDNOSTKOWE

147

Mamy tu oczywicie wiele szczegw. Na przykad do czego suy funkcja tic? W rzeczywistoci
nie przejmowabym si tym w czasie czytania tych testw. Obawiabym si raczej tego, czy kocowy
stan systemu jest spjny, gdy temperatura jest wyranie zbyt niska.
Naley zauway, e gdy czytamy testy, nasze oko musi skaka pomidzy nazw sprawdzanego stanu
a czujnikiem sprawdzanego stanu. Widzimy heaterState i nasze oko przelizguje si w lewo do
assertTrue. Widzimy coolerState i nasze oko musi skierowa si w lewo do assertFalse.
Jest to nuce i zawodne. Powoduje, e testy s trudne do czytania.
Znacznie poprawiem czytelno tych testw przez przeksztacenie ich na posta z listingu 9.4.
L I S T I N G 9 . 4 . EnvironmentControllerTest.java (przebudowany)
@Test
public void turnOnLoTempAlarmAtThreshold() throws Exception {
wayTooCold();
assertEquals("HBchL", hw.getState());
}

Oczywicie, ukryem szczegy funkcji tic przez utworzenie funkcji wayTooCold. Zauwaamy
jednak dziwny cig znakw w assertEquals. Wielka litera oznacza wczony, a maa wyczony; litery
s zawsze w nastpujcej kolejnoci: {heater, blower, cooler, hi-temp-alarm, lo-temp-alarm}.
Pomimo e jest to bliskie naruszenia zasady odwzorowania mentalnego3, w tym przypadku wydaje si
dziaaniem waciwym. Naley zauway, e gdy znamy znaczenie, nasze oko przelizguje si po tym
cigu znakw i moemy szybko zinterpretowa wyniki. Czytanie testw staje si niemal przyjemnoci. Spjrzmy na listing 9.5 i przekonajmy si, jak atwo mona zrozumie znajdujce si tam
testy.
L I S T I N G 9 . 5 . EnvironmentControllerTest.java (wikszy fragment)
@Test
public void turnOnCoolerAndBlowerIfTooHot() throws Exception {
tooHot();
assertEquals("hBChl", hw.getState());
}
@Test
public void turnOnHeaterAndBlowerIfTooCold() throws Exception {
tooCold();
assertEquals("HBchl", hw.getState());
}
@Test
public void turnOnHiTempAlarmAtThreshold() throws Exception {
wayTooHot();
assertEquals("hBCHl", hw.getState());
}
@Test
public void turnOnLoTempAlarmAtThreshold() throws Exception {
wayTooCold();
assertEquals("HBchL", hw.getState());
}
3

Unikanie odwzorowania mentalnego w rozdziale 2.

148

ROZDZIA 9.

Funkcja getState jest zamieszczona na listingu 9.6. Warto zwrci uwag, e kod ten nie jest zbyt
efektywny. Aby poprawi jego efektywno, prawdopodobnie musiabym uy klasy StringBuffer.
L I S T I N G 9 . 6 . MockControlHardware.java
public String getState() {
String state = "";
state += heater ? "H" : "h";
state += blower ? "B" : "b";
state += cooler ? "C" : "c";
state += hiTempAlarm ? "H" : "h";
state += loTempAlarm ? "L" : "l";
return state;
}

Zastosowanie StringBuffer jest mao eleganckie. Nawet w kodzie produkcyjnym unikam ich,
jeeli koszt jest niewielki, a mona si zgodzi, e koszt w kodzie z listingu 9.6 jest niewielki. Jednak
aplikacja ta wchodzi w skad wbudowanego systemu czasu rzeczywistego, a najprawdopodobniej
zasoby komputera i pami s bardzo ograniczone. rodowisko testowe nie jest jednak tak ograniczone.
Taka jest natura podwjnych standardw. Istniej operacje, ktrych nigdy nie uylibymy w rodowisku produkcyjnym, ale s cakowicie dozwolone w rodowisku testowym. Zwykle jest to zwizane z efektywnoci obliczeniow i pamiciow. Jednak nigdy nie s to problemy z czystoci kodu.

Jedna asercja na test


Istnieje szkoa programowania4 twierdzca, e kada funkcja testowa w JUnit powinna mie jedn
instrukcj asercji. Zasada ta moe wydawa si drakoska, ale jej zalety s widoczne na listingu
9.5. Poszczeglne testy zawieraj pojedyncz konkluzj, ktr mona szybko i atwo zrozumie.
Co jednak z listingiem 9.2? Wydaje si nierozsdne, e moemy dosy atwo czy asercje, ktrych
wynikiem jest XML i ktre zawieraj okrelone podcigi. Moemy jednak podzieli test na dwa
osobne testy, z ktrych kady bdzie mia wasn asercj, tak jak na listingu 9.7.
L I S T I N G 9 . 7 . SerializedPageResponderTest.java (pojedyncze asercje)
public void testGetPageHierarchyAsXml() throws Exception {
givenPages("PageOne", "PageOne.ChildOne", "PageTwo");
whenRequestIsIssued("root", "type:pages");
thenResponseShouldBeXML();
}
public void testGetPageHierarchyHasRightTags() throws Exception {
givenPages("PageOne", "PageOne.ChildOne", "PageTwo");
whenRequestIsIssued("root", "type:pages");
thenResponseShouldContain(
"<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>"
);
}

Patrz wpis na blogu Davea Astela: http://www.artima.com/weblogs/viewpost.jsp?thread=35578.

TESTY JEDNOSTKOWE

149

Zwrmy uwag, e zmieniem nazwy funkcji, aby korzystay z czsto stosowanej konwencji givenwhen-then5. Powoduje to, e testy s jeszcze atwiejsze w czytaniu. Niestety, tego typu rozdzielenie
testw powoduje powstanie duej iloci powtrzonego kodu.
Moemy wyeliminowa te powtrzenia przez zastosowanie wzorca szablonu metody6 (ang. Template
Method) i umieszczenie czci given-then w klasie bazowej, a czci then w poszczeglnych klasach
pochodnych. Mona rwnie utworzy cakowicie osobne klasy testw i umieci czci given oraz
then w funkcji @Before, a czci when w kadej funkcji @Test. Jednak wydaje si nieracjonalne
uycie tak wielu mechanizmw do rozwizania niewielkiego problemu. Osobicie najbardziej lubi
wielokrotne asercje z listingu 9.2.
Uwaam, e zasada jednej asercji jest dobr wskazwk7. Zwykle staram si tworzy specyficzny dla
domeny jzyk testowania, ktry je wspiera, tak jak w przypadku kodu z listingu 9.5. Jednak nie
obawiam si umieszcza wicej ni jednej asercji w tecie. Uwaam, e moemy jedynie stwierdzi,
i liczba asercji w tecie powinna by zminimalizowana.

Jedna koncepcja na test


Lepsz zasad dotyczc testw jest obejmowanie jednej koncepcji w kadej funkcji testowej. Nie
potrzebujemy dugich funkcji testowych, ktre kolejno testuj jedn rzecz za drug. Przykad takiego testu jest zamieszczony na listingu 9.8. Test ten powinien by podzielony na trzy niezalene
testy, poniewa sprawdzane s trzy niezalene kwestie. Zczenie ich w tej samej funkcji wymusza
na Czytelniku konieczno sprawdzania, w ktrej sekcji si znajduje i co jest w niej testowane.
LISTING 9.8.
/**
* Rne testy metody addMonths().
*/
public void testAddMonths() {
SerialDate d1 = SerialDate.createInstance(31, 5, 2004);
SerialDate d2 = SerialDate.addMonths(1, d1);
assertEquals(30, d2.getDayOfMonth());
assertEquals(6, d2.getMonth());
assertEquals(2004, d2.getYYYY());
SerialDate d3 = SerialDate.addMonths(2, d1);
assertEquals(31, d3.getDayOfMonth());
assertEquals(7, d3.getMonth());
assertEquals(2004, d3.getYYYY());
SerialDate d4 = SerialDate.addMonths(1, SerialDate.addMonths(1, d1));
assertEquals(30, d4.getDayOfMonth());
assertEquals(7, d4.getMonth());
assertEquals(2004, d4.getYYYY());
}

[RSpec].

[GOF].

Trzymaj si kodu!.

150

ROZDZIA 9.

Te trzy funkcje testowe powinny prawdopodobnie wyglda nastpujco:

Mamy ostatni dzie miesica majcego 31 dni (tak jak maj):


1. Gdy dodamy jeden miesic, tak e ostatnim dniem tego miesica jest 30 (tak jak czerwiec),
to data powinna by 30 tego miesica, a nie 31.
2. Gdy dodamy do tej daty dwa miesice, tak aby wynikowy miesic mia 31 dni, to data
powinna by 31.

Mamy ostatni dzie miesica majcego 30 dni (tak jak czerwiec):


1. Gdy dodamy jeden miesic, tak aby ostatni dzie tego miesica mia 31 dni, to wynikowa
data powinna by 30, a nie 31.

Na podstawie takich zaoe mona zauway, e istnieje oglna zasada ukrywania testw. Gdy
zwikszymy miesic, data nie moe by wiksza ni ostatni dzie miesica. Powoduje to, e zwikszenie miesica w przypadku 28 lutego powinno da w wyniku 20 marca. Tego testu brakuje, a byby
on przydatny.
Tak wic to nie wielokrotne asercje z listingu 9.8 powoduj problem. Problemem jest raczej testowanie wicej ni jednej koncepcji. Dlatego najlepsz zasad jest minimalizacja liczby asercji dla
koncepcji i testowanie tylko jednej koncepcji na funkcj testow.

F.I.R.S.T.8
Czyste testy powinny spenia pi innych zasad, ktre tworz powyszy akronim:
Szybkie (Fast). Testy powinny by szybkie. Powinny dziaa szybko. Gdy testy dziaaj powoli, nie
chcemy ich uruchamia zbyt czsto. Jeeli nie uruchamiamy ich zbyt czsto, nie znajdziemy problemw wystarczajco szybko, aby mona byo atwo je poprawi. Nie czujemy si wystarczajco
pewnie, aby czyci nasz kod. W kocu kod zaczyna si psu.
Niezalene (Independent). Testy nie powinny zalee od siebie. Jeden test nie powinien konfigurowa warunkw do nastpnego testu. Powinnimy by w stanie uruchamia kady test niezalenie
i uruchamia testy w dowolnie wybranym porzdku. Jeli testy zale od siebie, to gdy nie uda si pierwszy test, powstaje kaskada awarii, co utrudnia diagnoz i ukrywa awarie na niszym poziomie.
Powtarzalne (Repeatable). Testy powinny by powtarzalne w kadym rodowisku. Powinnimy
by w stanie uruchomi testy w rodowisku produkcyjnym, rodowisku zapewnienia jakoci oraz
na naszym laptopie w trakcie podry pocigiem do domu. Jeeli nasze testy nie s powtarzalne
w kadym rodowisku, to zawsze bdziemy mieli wymwk, gdy si nie powiod. Okazuje si
rwnie, e moemy mie kopoty z uruchomieniem testw, gdy nie jest dostpne dane rodowisko.

Materiay szkoleniowe mentora obiektowego.

TESTY JEDNOSTKOWE

151

Samokontrolujce si (Self-Validating). Testy powinny mie jeden parametr wyjciowy typu


logicznego. Mog one si powie lub nie. Nie powinnimy musie czyta plikw dziennikw w celu
sprawdzenia, czy testy si powiody. Nie powinnimy rcznie porwnywa dwch plikw testowych w celu sprawdzenia, czy test si powid. Jeeli testy nie kontroluj si samodzielnie, to awaria
moe sta si subiektywna i uruchomienie testw bdzie wymaga dugiego procesu rcznej analizy.
O czasie (Timely). Testy powinny by pisane w odpowiednim momencie. Testy jednostkowe
powinny by pisane bezporednio przed tworzeniem testowanego kodu produkcyjnego. Jeeli piszemy testy po kodzie produkcyjnym, moe si okaza, e jest on trudny do przetestowania.
Mona zdecydowa, e pewna cz kodu produkcyjnego jest zbyt trudna do przetestowania. Nie
mona zaprojektowa kodu produkcyjnego tak, aby nie da si przetestowa.

Zakoczenie
Niniejszy rozdzia stanowi zaledwie zarys problematyki testowania kodu. Faktycznie, mona napisa ca ksik na temat czystych testw. Testy s tak wane dla jakoci projektu, jak sam kod produkcyjny. Prawdopodobnie s nawet waniejsze, poniewa zapewniaj i rozszerzaj elastyczno,
atwo utrzymania i ponownego uycia kodu produkcyjnego. Tak wic testy powinny by stale
utrzymywane w czystoci. Naley zapewni, e bd czytelne i zwize. Warto opracowa API
testujce, ktre posuy jako jzyk specyficzny dla domeny, pomagajcy w pisaniu testw.
Jeeli pozwolimy na zepsucie testw, nasz kod rwnie bdzie si psu. Testy naley utrzymywa
w czystoci.

Bibliografia
[RSpec]: Aslak Hellesy, David Chelimsky, RSpec: Behavior Driven Development for Ruby Programmers,
Pragmatic Bookshelf 2008.
[GOF]: Gamma, Design Patterns: Elements of Reusable Object Oriented Software, Addison-Wesley 1996.

152

ROZDZIA 9.

ROZDZIA 10.

Klasy
we wsppracy z Jeffem Langrem

skupialimy si na odpowiednim zapisywaniu wierszy i blokw kodu.


D Zajmowalimy si prawidow
kompozycj funkcji oraz ich wzajemnymi relacjami. Jednak pomimo
O TEJ PORY W KSICE

caego zaangaowania w zapewnienie ekspresywnoci instrukcji kodu, jak rwnie zbudowanych


z niego funkcji, nadal nie mamy czystego kodu; musimy powici uwag wyszym poziomom
organizacji kodu. Zajmijmy si czystoci klas.

Organizacja klas
Zgodnie ze standardow konwencj jzyka Java klasy powinny zaczyna si od listy zmiennych.
Publiczne stae statyczne, jeeli wystpuj, powinny by zapisane jako pierwsze. W dalszej kolejnoci
zapisywane s prywatne zmienne statyczne, a po nich prywatne zmienne instancyjne. Rzadko istnieje dobry powd korzystania ze zmiennych publicznych.
153

Po licie zmiennych powinny znajdowa si funkcje publiczne. Prywatne funkcje uytkowe wywoywane przez funkcje publiczne zwykle umieszczamy po funkcjach publicznych. Jest to zgodne
z zasad zstpujc i pomaga czyta program jak artyku w gazecie.

Hermetyzacja
Zazwyczaj nasze zmienne i funkcje uytkowe pozostaj prywatne, ale nie jestemy fanatykami tego
rozwizania. Czasami zmieniamy zmienn lub funkcj na chronion, aby bya dostpna dla testu.
Dla nas to testy ustalaj zasady. Jeeli test w tym samym pakiecie potrzebuje wywoa funkcj lub
odwoa si do zmiennej, zmieniamy jej zasig na chroniony lub dostpny w ramach pakietu. Jednak na pocztku zawsze szukamy sposobu na zachowanie prywatnoci. Rozlunianie zasady prywatnoci jest ostatnim moliwym rozwizaniem.

Klasy powinny by mae!


Pierwsza zasada dotyczca klas mwi, e powinny by mae. Druga zasada e powinny by
mniejsze, ni s. Nie, nie mamy zamiaru powtarza dokadnie takiego samego tekstu z rozdziau
Funkcje. Jednak podobnie jak w przypadku funkcji, podstawow zasad projektowania klas jest
zachowanie maych rozmiarw. Podobnie jak w przypadku funkcji, naszym natychmiastowym
pytaniem jest zawsze: Jak mae?.
W przypadku funkcji mierzylimy ich rozmiar, zliczajc linie fizyczne. W przypadku klas korzystamy z innej metryki. Zliczamy tzw. odpowiedzialnoci1.
Na listingu 10.1 przedstawiony jest szkielet klasy SuperDashboard, ktra udostpnia 70 metod
publicznych. Wikszo programistw zgodzi si, e jest ona zbyt dua. Niektrzy programici
mog okrela SuperDashboard jako klas bosk.
L I S T I N G 1 0 . 1 . Zbyt wiele odpowiedzialnoci
public class SuperDashboard extends JFrame implements MetaDataUser
public String getCustomizerLanguagePath()
public void setSystemConfigPath(String systemConfigPath)
public String getSystemConfigDocument()
public void setSystemConfigDocument(String systemConfigDocument)
public boolean getGuruState()
public boolean getNoviceState()
public boolean getOpenSourceState()
public void showObject(MetaObject object)
public void showProgress(String s)
public boolean isMetadataDirty()
public void setIsMetadataDirty(boolean isMetadataDirty)
public Component getLastFocusedComponent()
public void setLastFocused(Component lastFocused)
public void setMouseSelectState(boolean isMouseSelected)
public boolean isMouseSelected()

[RDD].

154

ROZDZIA 10.

public LanguageManager getLanguageManager()


public Project getProject()
public Project getFirstProject()
public Project getLastProject()
public String getNewProjectName()
public void setComponentSizes(Dimension dim)
public String getCurrentDir()
public void setCurrentDir(String newDir)
public void updateStatus(int dotPos, int markPos)
public Class[] getDataBaseClasses()
public MetadataFeeder getMetadataFeeder()
public void addProject(Project project)
public boolean setCurrentProject(Project project)
public boolean removeProject(Project project)
public MetaProjectHeader getProgramMetadata()
public void resetDashboard()
public Project loadProject(String fileName, String projectName)
public void setCanSaveMetadata(boolean canSave)
public MetaObject getSelectedObject()
public void deselectObjects()
public void setProject(Project project)
public void editorAction(String actionName, ActionEvent event)
public void setMode(int mode)
public FileManager getFileManager()
public void setFileManager(FileManager fileManager)
public ConfigManager getConfigManager()
public void setConfigManager(ConfigManager configManager)
public ClassLoader getClassLoader()
public void setClassLoader(ClassLoader classLoader)
public Properties getProps()
public String getUserHome()
public String getBaseDir()
public int getMajorVersionNumber()
public int getMinorVersionNumber()
public int getBuildNumber()
public MetaObject pasting(
MetaObject target, MetaObject pasted, MetaProject project)
public void processMenuItems(MetaObject metaObject)
public void processMenuSeparators(MetaObject metaObject)
public void processTabPages(MetaObject metaObject)
public void processPlacement(MetaObject object)
public void processCreateLayout(MetaObject object)
public void updateDisplayLayer(MetaObject object, int layerIndex)
public void propertyEditedRepaint(MetaObject object)
public void processDeleteObject(MetaObject object)
public boolean getAttachedToDesigner()
public void processProjectChangedState(boolean hasProjectChanged)
public void processObjectNameChanged(MetaObject object)
public void runProject()
public void setAowDragging(boolean allowDragging)
public boolean allowDragging()
public boolean isCustomizing()
public void setTitle(String title)
public IdeMenuBar getIdeMenuBar()
public void showHelper(MetaObject metaObject, String propertyName)

// ... Wiele kolejnych niepublicznych metod ...


}

Co mona powiedzie w przypadku, gdy klasa SuperDashboard bdzie zawieraa wycznie metody
z listingu 10.2?

KLASY

155

L I S T I N G 1 0 . 2 . Wystarczajco maa?
public class SuperDashboard extends JFrame implements MetaDataUser
public Component getLastFocusedComponent()
public void setLastFocused(Component lastFocused)
public int getMajorVersionNumber()
public int getMinorVersionNumber()
public int getBuildNumber()
}

Pi metod to niezbyt duo, prawda? W tym przypadku, pomimo maej liczby metod, SuperDashboard
ma zbyt wiele odpowiedzialnoci.
Nazwa klasy powinna opisywa penione przez ni odpowiedzialnoci. W rzeczywistoci nazewnictwo jest prawdopodobnie pierwszym sposobem na pomoc w okreleniu wielkoci klasy. Jeeli
nie moemy utworzy spjnej nazwy klasy, prawdopodobnie jest ona zbyt dua. Im bardziej oglna
jest nazwa klasy, tym wiksze prawdopodobiestwo, e ma wiele odpowiedzialnoci. Na przykad
nazwy klas zawierajce takie sowa, jak Processor, Manager lub Super, czsto wskazuj na niefortunn agregacj odpowiedzialnoci.
Powinnimy by w stanie napisa krtki opis klasy, uywajc okoo 25 sw, bez wykorzystania sw
jeeli, oraz, lub czy te ale. Jak mona opisa SuperDashboard? Klasa SuperDashboard
zapewnia dostp do komponentu majcego ostatnio fokus oraz pozwala ledzi wersj i numer
kompilacji. Sowo oraz jest podpowiedzi, e SuperDashboard ma zbyt duo odpowiedzialnoci.

Zasada pojedynczej odpowiedzialnoci


Zasada pojedynczej odpowiedzialnoci (SRP)2 zakada, e klasa lub modu powinny mie jeden
i tylko jeden powd do zmiany. Zasada ta zarwno definiuje odpowiedzialno, jak i daje wskazwki
odnonie do wielkoci klas. Klasy powinny mie jedn odpowiedzialno jeden powd do zmiany.
Pozornie maa klasa SuperDashboard z listingu 10.2 ma dwa powody do zmiany. Po pierwsze, ledzi
ona informacje o wersji, ktre bd prawdopodobnie aktualizowane przy kadym wydaniu oprogramowania. Po drugie, zarzdza komponentami Java Swing (dziedziczy po JFrame, reprezentacji
gwnego okna GUI w bibliotece Swing). Pewne jest, e bdziemy chcieli zmieni numer wersji po
zmianie fragmentu kodu Swing, ale w odwrotnej sytuacji niekoniecznie. Moemy zmienia
informacje o wersji w przypadku wprowadzania zmian w innych fragmentach kodu systemu.
Prba identyfikacji odpowiedzialnoci (powodw zmiany) czsto pomaga nam rozpozna i utworzy
lepsze abstrakcje w naszym kodzie. Mona bez trudu wydzieli trzy metody z klasy SuperDashboard,
operujce na informacjach o wersji, do zewntrznej klasy o nazwie Version (patrz listing 10.3).
Klasa Version jest konstrukcj, ktra z duym prawdopodobiestwem moe by ponownie wykorzystywana w innych aplikacjach!

Wicej na temat tej zasady mona przeczyta w [PPP].

156

ROZDZIA 10.

L I S T I N G 1 0 . 3 . Klasa o jednej odpowiedzialnoci


public class
public int
public int
public int
}

Version {
getMajorVersionNumber()
getMinorVersionNumber()
getBuildNumber()

SRP jest jedn z najwaniejszych koncepcji w projektowaniu obiektowym. Jest to rwnie jedna
z najprostszych koncepcji do zrozumienia oraz stosowania. Co jednak dziwne, jest jedn z najczciej amanych zasad projektowania. Regularnie spotykamy si z klasami, ktre wykonuj zbyt duo rzeczy. Dlaczego?
Doprowadzenie do tego, aby program dziaa, i tworzenie czystego oprogramowania to dwa bardzo
rne zadania. Nasz umys ma ograniczone moliwoci, wic bardziej skupiamy si na doprowadzeniu kodu do stanu, w ktrym dziaa, ni na jego organizacji i czystoci. Jest to cakowicie
waciwe. Utrzymanie rozdziau zada jest wane zarwno dla procesu programowania, jak i dla
naszych programw.
Problem wynika z tego, e wielu z nas uwaa prac za zakoczon po doprowadzeniu kodu do
dziaania. Nie zajmujemy si drugim zadaniem zapewnieniem organizacji i czystoci kodu.
Przechodzimy do kolejnego problemu, zamiast zajmowa si dzieleniem przecionych klas na
rozczone jednostki z jedn odpowiedzialnoci.
Jednoczenie wielu programistw boi si, e zbyt wiele maych i jednozadaniowych klas utrudni
zrozumienie caego systemu. Obawiaj si, e bd musieli przechodzi z klasy do klasy w celu sprawdzenia, w jaki sposb mona wykona wiksze zadanie.
Jednak system z wieloma maymi klasami nie ma wicej zmiennych czci ni system z kilkoma
duymi klasami. System z kilkoma duymi klasami wymaga dokadnie tyle samo nauki. Naley
wic postawi pytanie: Czy wolimy przechowywa nasze narzdzia w skrzynkach z wieloma maymi szufladkami zawierajcymi okrelone elementy, czy wolimy kilka szuflad, do ktrych po prostu
wrzucamy narzdzia?
Kady wikszy system bdzie zawiera du ilo kodu logiki i bdzie skomplikowany. Podstawowym celem zarzdzania t zoonoci jest jej organizacja, aby programista wiedzia, gdzie szuka
okrelonych elementw, i w danym momencie musia analizowa jedynie elementy bezporednio
zwizane z problemem. Dla porwnania, system z wikszymi, wielozadaniowymi klasami zawsze
zmusza nas do przedzierania si przez wiele elementw, ktrych w danym momencie nie potrzebujemy.
Powtrzmy to jeszcze raz: wolimy, gdy nasze systemy skadaj si z wielu maych klas, a nie kilku
duych. Kada maa klasa hermetyzuje jedn odpowiedzialno, ma jeden powd zmiany i dla
osignicia oczekiwanego dziaania systemu wsppracuje z niewielk liczb innych klas.

KLASY

157

Spjno
Klasy powinny mie niewielk liczb zmiennych instancyjnych. Kada z metod klasy powinna manipulowa jedn lub kilkoma tymi zmiennymi. Zwykle im wiksz liczb zmiennych manipuluje
klasa, tym bardziej spjna z klas jest ta metoda. Klasa, w ktrej kada zmienna jest wykorzystywana
w kadej metodzie, jest maksymalnie spjna.
Zwykle nie jest zalecane ani moliwe tworzenie takich maksymalnie spjnych klas; z drugiej strony,
spjno powinna by wysoka. Gdy spjno jest wysoka, oznacza to, e metody i zmienne klasy s
wzajemnie zalene i tworz logiczn cao.
Jako przykad wemy implementacj klasy Stack z listingu 10.4. Jest to tylko bardzo spjna klasa.
W przypadku jej trzech metod jedynie size() nie korzysta z obu zmiennych.
L I S T I N G 1 0 . 4 . Stack.java spjna klasa
public class Stack {
private int topOfStack = 0;
List<Integer> elements = new LinkedList<Integer>();
public int size() {
return topOfStack;
}
public void push(int element) {
topOfStack++;
elements.add(element);
}
public int pop() throws PoppedWhenEmpty {
if (topOfStack == 0)
throw new PoppedWhenEmpty();
int element = elements.get(--topOfStack);
elements.remove(topOfStack);
return element;
}
}

Strategia tworzenia maych funkcji oraz krtkich list parametrw czasami prowadzi do wykorzystywania zmiennych instancyjnych uywanych przez podzbiory metod. W takim przypadku niemal
zawsze oznacza to, e przynajmniej jedna nowa klasa chce si uwolni z biecej klasy. Powinnimy
sprbowa podzieli zmienne i metody na dwie lub wicej klas, aby nowe klasy byy bardziej spjne.

Utrzymywanie spjnoci powoduje powstanie wielu maych klas


Sam akt podziau duych funkcji na mniejsze powoduje zwikszenie liczby klas. Wemy pod uwag
du funkcj z zadeklarowanymi w niej wieloma zmiennymi. Zamy, e chcemy wydzieli ma
cz tej funkcji do osobnej funkcji. Jednak kod, jaki chcemy wydzieli, korzysta z czterech zmiennych zadeklarowanych w funkcji. Czy musimy przekaza wszystkie te zmienne do nowej funkcji
jako argumenty?

158

ROZDZIA 10.

Nie musimy! Jeeli zmienimy te cztery zmienne na zmienne instancyjne klasy, bdziemy mogli
wydzieli kod bez przekazywania jakichkolwiek zmiennych. Bardzo atwo bdzie podzieli funkcj
na mniejsze czci.
Niestety, oznacza to rwnie, e nasze klasy utrac spjno, poniewa bdzie si w nich gromadzio coraz wicej zmiennych, ktre istniej tylko po to, aby kilka funkcji wsplnie z nich korzystao. Czekaj, czekaj! Jeeli istnieje niewiele funkcji, ktre wspuytkuj okrelone zmienne, czy
nie naley utworzy z nich osobnej klasy? Oczywicie, e tak. Gdy klasy trac spjno, naley je
podzieli!
Tak wic podzia duej funkcji na mniejsze czsto daje nam rwnie moliwo wydzielenia kilku mniejszych klas. Pozwala to na lepsz organizacj programu i zapewnienie bardziej przejrzystej struktury.
Aby przedstawi, o co mi chodzi, skorzystam z od dawna znanego przykadu zamieszczonego
w doskonaej ksice Knutha, Literate Programming3. Na listingu 10.5 przedstawione jest tumaczenie
na jzyk Java programu Knutha PrintPrimes. Aby by uczciwym wobec Knutha, musz nadmieni, e program ten nie zosta przez niego napisany, ale jest wynikiem dziaania jego narzdzia
WWW. Korzystam z niego, poniewa jest to doskonay materia do podziau duej funkcji na
mniejsze funkcje i klasy.
L I S T I N G 1 0 . 5 . PrintPrimes.java
package literatePrimes;
public class PrintPrimes {
public static void main(String[] args) {
final int M = 1000;
final int RR = 50;
final int CC = 4;
final int WW = 10;
final int ORDMAX = 30;
int P[] = new int[M + 1];
int PAGENUMBER;
int PAGEOFFSET;
int ROWOFFSET;
int C;
int J;
int K;
boolean JPRIME;
int ORD;
int SQUARE;
int N;
int MULT[] = new int[ORDMAX + 1];
J = 1;
K = 1;
P[1] = 2;
ORD = 2;
SQUARE = 9;
while (K < M) {
do {
J = J + 2;
if (J == SQUARE) {
3

[Knuth92].

KLASY

159

ORD = ORD + 1;
SQUARE = P[ORD] * P[ORD];
MULT[ORD - 1] = J;
}
N = 2;
JPRIME = true;
while (N < ORD && JPRIME) {
while (MULT[N] < J)
MULT[N] = MULT[N] + P[N] + P[N];
if (MULT[N] == J)
JPRIME = false;
N = N + 1;
}
} while (!JPRIME);
K = K + 1;
P[K] = J;
}
{
PAGENUMBER = 1;
PAGEOFFSET = 1;
while (PAGEOFFSET <= M) {
System.out.println("Pierwsze " + M +
" liczb pierwszych --- strona " + PAGENUMBER);
System.out.println("");
for (ROWOFFSET = PAGEOFFSET; ROWOFFSET < PAGEOFFSET + RR; ROWOFFSET++){
for (C = 0; C < CC;C++)
if (ROWOFFSET + C * RR <= M)
System.out.format("%10d", P[ROWOFFSET + C * RR]);
System.out.println("");
}
System.out.println("\f");
PAGENUMBER = PAGENUMBER + 1;
PAGEOFFSET = PAGEOFFSET + RR * CC;
}
}
}
}

Program ten, zapisany w jednej funkcji, jest skomplikowany. Ma cile powizan, gboko zagniedon struktur i sporo dziwnych zmiennych. Co najwyej mona podzieli jedn du funkcj na
kilka mniejszych.
Na listingach od 10.6 do 10.8 przedstawiony jest wynik podziau kodu z listingu 10.5 na mniejsze
klasy i funkcje, jak rwnie wynik wyboru znaczcych nazw dla tych klas, funkcji i zmiennych.
L I S T I N G 1 0 . 6 . PrimePrinter.java (przebudowany)
package literatePrimes;
public class PrimePrinter {
public static void main(String[] args) {
final int NUMBER_OF_PRIMES = 1000;
int[] primes = PrimeGenerator.generate(NUMBER_OF_PRIMES);
final int ROWS_PER_PAGE = 50;
final int COLUMNS_PER_PAGE = 4;
RowColumnPagePrinter tablePrinter =
new RowColumnPagePrinter(ROWS_PER_PAGE,
COLUMNS_PER_PAGE,
"Pierwsze " + NUMBER_OF_PRIMES +
" liczb pierwszych");

160

ROZDZIA 10.

tablePrinter.print(primes);
}
}

L I S T I N G 1 0 . 7 . RowColumnPagePrinter.java
package literatePrimes;
import java.io.PrintStream;
public class RowColumnPagePrinter {
private int rowsPerPage;
private int columnsPerPage;
private int numbersPerPage;
private String pageHeader;
private PrintStream printStream;
public RowColumnPagePrinter(int rowsPerPage,
int columnsPerPage,
String pageHeader) {
this.rowsPerPage = rowsPerPage;
this.columnsPerPage = columnsPerPage;
this.pageHeader = pageHeader;
numbersPerPage = rowsPerPage * columnsPerPage;
printStream = System.out;
}
public void print(int data[]) {
int pageNumber = 1;
for (int firstIndexOnPage = 0;
firstIndexOnPage < data.length;
firstIndexOnPage += numbersPerPage) {
int lastIndexOnPage =
Math.min(firstIndexOnPage + numbersPerPage - 1,
data.length - 1);
printPageHeader(pageHeader, pageNumber);
printPage(firstIndexOnPage, lastIndexOnPage, data);
printStream.println("\f");
pageNumber++;
}
}
private void printPage(int firstIndexOnPage,
int lastIndexOnPage,
int[] data) {
int firstIndexOfLastRowOnPage =
firstIndexOnPage + rowsPerPage - 1;
for (int firstIndexInRow = firstIndexOnPage;
firstIndexInRow <= firstIndexOfLastRowOnPage;
firstIndexInRow++) {
printRow(firstIndexInRow, lastIndexOnPage, data);
printStream.println("");
}
}
private void printRow(int firstIndexInRow,
int lastIndexOnPage,
int[] data) {
for (int column = 0; column < columnsPerPage; column++) {
int index = firstIndexInRow + column * rowsPerPage;
if (index <= lastIndexOnPage)
printStream.format("%10d", data[index]);
}

KLASY

161

}
private void printPageHeader(String pageHeader,
int pageNumber) {
printStream.println(pageHeader + " --- Strona " + pageNumber);
printStream.println("");
}
public void setOutput(PrintStream printStream) {
this.printStream = printStream;
}
}

L I S T I N G 1 0 . 8 . PrimeGenerator.java
package literatePrimes;
import java.util.ArrayList;
public class PrimeGenerator {
private static int[] primes;
private static ArrayList<Integer> multiplesOfPrimeFactors;
protected static int[] generate(int n) {
primes = new int[n];
multiplesOfPrimeFactors = new ArrayList<Integer>();
set2AsFirstPrime();
checkOddNumbersForSubsequentPrimes();
return primes;
}
private static void set2AsFirstPrime() {
primes[0] = 2;
multiplesOfPrimeFactors.add(2);
}
private static void checkOddNumbersForSubsequentPrimes() {
int primeIndex = 1;
for (int candidate = 3;
primeIndex < primes.length;
candidate += 2) {
if (isPrime(candidate))
primes[primeIndex++] = candidate;
}
}
private static boolean isPrime(int candidate) {
if (isLeastRelevantMultipleOfNextLargerPrimeFactor(candidate)) {
multiplesOfPrimeFactors.add(candidate);
return false;
}
return isNotMultipleOfAnyPreviousPrimeFactor(candidate);
}
private static boolean
isLeastRelevantMultipleOfNextLargerPrimeFactor(int candidate) {
int nextLargerPrimeFactor = primes[multiplesOfPrimeFactors.size()];
int leastRelevantMultiple = nextLargerPrimeFactor * nextLargerPrimeFactor;
return candidate == leastRelevantMultiple;
}
private static boolean
isNotMultipleOfAnyPreviousPrimeFactor(int candidate) {

162

ROZDZIA 10.

for (int n = 1; n < multiplesOfPrimeFactors.size(); n++) {


if (isMultipleOfNthPrimeFactor(candidate, n))
return false;
}
return true;
}
private static boolean
isMultipleOfNthPrimeFactor(int candidate, int n) {
return
candidate == smallestOddNthMultipleNotLessThanCandidate(candidate, n);
}
private static int
smallestOddNthMultipleNotLessThanCandidate(int candidate, int n) {
int multiple = multiplesOfPrimeFactors.get(n);
while (multiple < candidate)
multiple += 2 * primes[n];
multiplesOfPrimeFactors.set(n, multiple);
return multiple;
}
}

Pierwsz rzecz, jak zauwaamy, jest fakt, e program sta si znacznie duszy. Wzrs z niewiele
ponad jednej strony do niemal trzech. Istnieje kilka przyczyn tego wzrostu objtoci kodu. Po
pierwsze, przebudowany program korzysta z duszych, bardziej opisowych nazw zmiennych. Po
drugie, przebudowany program korzysta z deklaracji funkcji i klas jako sposobu dodawania komentarzy do kodu. Po trzecie, wykorzystalimy odstpy i techniki formatowania pozwalajce
zachowa czytelno programu.
Warto zauway, e program zosta podzielony na trzy gwne odpowiedzialnoci. Gwny program znajduje si w samej klasie PrimePrinter. Jej odpowiedzialnoci jest obsuga rodowiska
wykonania. Zmienia si ona, gdy zmieniaj si wywoania metod. Jeeli program ten zostaby na
przykad skonwertowany na usug SOAP, zmieniona zostaaby ta klasa.
Klasa RowColumnPagePrinter obsuguje wszystkie operacje formatowania list liczb na stronach
o okrelonej liczbie wierszy i kolumn. Jeeli zmiany bdzie wymagao formatowanie danych wyjciowych, zostanie zmodyfikowana ta klasa.
Klasa PrimeGenerator obsuguje wszystkie operacje generowania listy liczb pierwszych. Naley
zauway, e nie jest przewidziane tworzenie tego obiektu. Klasa jest po prostu uytecznym zakresem,
w ktrym mona zadeklarowa zmienne i zapewni ich ukrycie. Klasa ta zostanie zmieniona, jeeli
zmieni si algorytm obliczania liczb pierwszych.
To nie byo ponowne napisanie programu! Nie zaczlimy od pustego ekranu i nie napisalimy
programu od podstaw. W rzeczywistoci, jeeli przyjrzymy si dokadniej tym dwm programom,
okae si, e korzystaj z tego samego algorytmu i tych samych mechanizmw.
Zmian, jak wprowadzilimy, byo dodanie zestawu testw precyzyjnie weryfikujcych dziaanie
pierwszego programu. Nastpnie wprowadzono wiele maych zmian, jedna po drugiej. Po kadej
zmianie program by uruchamiany w celu upewnienia si, e jego dziaanie nie zmienio si.

KLASY

163

Organizowanie zmian
W wikszoci systemw jedynym staym elementem s zmiany. Kada zmiana powoduje zagroenie,
e pozostaa cz systemu przestanie dziaa w zaplanowany sposb. W czystym systemie organizujemy swoje klasy w taki sposb, aby zredukowa ryzyko zmian.
Klasa Sql z listingu 10.9 jest wykorzystywana do generowania prawidowo sformatowanego zapytania SQL na podstawie metadanych. Jest ona w trakcie rozwoju i midzy innymi nie obsuguje
operacji SQL update. Gdy przyszed czas na dodanie do klasy Sql obsugi zapytania update, musielimy otworzy t klas, aby wprowadzi modyfikacje. Z otwieraniem klasy zawsze zwizane
jest pewne ryzyko. Kada modyfikacja klasy moe spowodowa bd w innym fragmencie kodu
klasy. Musi ona zosta w peni przetestowana.
L I S T I N G 1 0 . 9 . Klasa, ktra musi by otwarta na zmiany
public class Sql {
public Sql(String table, Column[] columns)
public String create()
public String insert(Object[] fields)
public String selectAll()
public String findByKey(String keyColumn, String keyValue)
public String select(Column column, String pattern)
public String select(Criteria criteria)
public String preparedInsert()
private String columnList(Column[] columns)
private String valuesList(Object[] fields, final Column[] columns)
private String selectWithCriteria(String criteria)
private String placeholderList(Column[] columns)
}

Gdy dodajemy nowy rodzaj wyraenia, klasa Sql musi zosta zmieniona. Musi ulec zmianie rwnie w momencie, gdy zmienimy szczegy jednego typu wyraenia na przykad jeeli musimy
zmodyfikowa zapytanie select, aby obsugiwao podzapytania. Dwa powody zmiany oznaczaj,
e klasa Sql narusza zasad SRP.
Moemy zauway to naruszenie SRP na podstawie prostego organizacyjnego punktu widzenia.
Metoda przedstawiona dla Sql pokazuje, e istniej metody prywatne, takie jak selectWithCriteria,
ktre odnosz si tylko do instrukcji select.
Metody prywatne, ktre odnosz si tylko do maej czci klasy, mog by przydatne do lokalizowania potencjalnych obszarw usprawnienia. Jednak podstawow zacht do podjcia akcji powinna by sama zmiana systemu. Jeeli klasa Sql jest uznana za logicznie kompletn, nie ma
koniecznoci rozdzielania odpowiedzialnoci. Jeeli nie potrzebujemy funkcji update w dajcej si
przewidzie przyszoci, powinnimy zostawi klas Sql. Jednak gdy zdecydujemy si otworzy
klas, powinnimy rozway poprawienie naszego projektu.
Przyjmijmy rozwizanie takie jak na listingu 10.10. Kada metoda interfejsu publicznego, zdefiniowana w poprzedniej klasie Sql z listingu 10.9, zostaa przeniesiona do osobnej klasy dziedziczcej po klasie Sql. Metody prywatne, takie jak valuesList, zostay przeniesione bezporednio tam,
gdzie s potrzebne. Wsplne operacje prywatne zostay wydzielone do pary klas narzdziowych,
Where oraz ColumnList.
164

ROZDZIA 10.

L I S T I N G 1 0 . 1 0 . Zbir zamknitych klas


abstract public class Sql {
public Sql(String table, Column[] columns)
abstract public String generate();
}
public class CreateSql extends Sql {
public CreateSql(String table, Column[] columns)
@Override public String generate()
}
public class SelectSql extends Sql {
public SelectSql(String table, Column[] columns)
@Override public String generate()
}
public class InsertSql extends Sql {
public InsertSql(String table, Column[] columns, Object[] fields)
@Override public String generate()
private String valuesList(Object[] fields, final Column[] columns)
}
public class SelectWithCriteriaSql extends Sql {
public SelectWithCriteriaSql(
String table, Column[] columns, Criteria criteria)
@Override public String generate()
}
public class SelectWithMatchSql extends Sql {
public SelectWithMatchSql(
String table, Column[] columns, Column column, String pattern)
@Override public String generate()
}
public class FindByKeySql extends Sql
public FindByKeySql(
String table, Column[] columns, String keyColumn, String keyValue)
@Override public String generate()
}
public class PreparedInsertSql extends Sql {
public PreparedInsertSql(String table, Column[] columns)
@Override public String generate() {
private String placeholderList(Column[] columns)
}
public class Where {
public Where(String criteria)
public String generate()
}
public class ColumnList {
public ColumnList(Column[] columns)
public String generate()
}

Kod w kadej klasie sta si niezwykle prosty. Wymagany czas skupienia w celu zrozumienia kadej
z klas skurczy si niemal do zera. Ryzyko, e jedna funkcja spowoduje bd w innej, wyranie
zmalao. Z punktu widzenia testw znacznie prostszym zadaniem stao si sprawdzenie kadego
fragmentu tego rozwizania, poniewa klasy s izolowane od siebie.

KLASY

165

Rwnie wane jest to, e gdy przyjdzie czas na dodanie instrukcji update, adna z istniejcych klas
nie bdzie wymagaa zmiany! Kod pozwalajcy na budowanie instrukcji update umiecimy w nowej
klasie dziedziczcej po Sql, o nazwie UpdateSql. aden inny kod systemu nie ulegnie uszkodzeniu
z powodu wprowadzenia tej zmiany.
Nasza zrestrukturyzowana logika klasy Sql czy najlepsze cechy obu wiatw. Wspiera ona SRP.
Wspiera rwnie inn wan zasad projektowania obiektowego, zasad otwarty-zamknity, czyli
OCP4. Klasy powinny by otwarte na rozszerzanie, ale zamknite dla modyfikacji. Nasza zmieniona klasa Sql jest otwarta na dodawanie nowych funkcji przez dziedziczenie, a zmiany te moemy wykonywa bez koniecznoci otwierania pozostaych klas. Po prostu dorzucamy nasz klas
UpdateSql do pakietu.
Powinnimy tak konstruowa systemy, aby przy ich aktualizowaniu o nowe lub zmienione funkcje
nie byo konieczne wprowadzanie zbyt wielu dodatkowych modyfikacji. W idealnym systemie
wprowadzamy nowe funkcje przez jego rozszerzanie, a nie wprowadzanie zmian do istniejcego
kodu.

Izolowanie moduw kodu przed zmianami


Potrzeby wymuszaj zmiany, dlatego kod podlega modyfikacji. Podstawy programowania obiektowego mwi, e wystpuj klasy konkretne, zawierajce szczegy implementacji (kod), oraz klasy abstrakcyjne, ktre reprezentuj wycznie koncepcje. W przypadku gdy klasa klienta zaley od
szczegw klasy konkretnej, istnieje ryzyko, e szczegy te mog ulec zmianie. Aby zabezpieczy
si przed wpywem tych szczegw, wprowadzamy interfejsy i klasy abstrakcyjne.
Zalenoci od szczegw klas konkretnych powoduj problemy z testowaniem systemu. Jeeli budujemy klas Portfolio, ktra zaley od zewntrznego API TokyoStockExchange, do dziedziczenia wartoci portfela, nasze przypadki testowe s obcione zmiennoci tego wyszukiwania. Trudno
napisa test, jeeli co pi minut otrzymujemy inn odpowied!
Zamiast projektowania klasy Portfolio, ktra bezporednio zaley od TokyoStockExchange,
powinnimy utworzy interfejs StockExchange, ktry zawiera jedn metod:
public interface StockExchange {
Money currentPrice(String symbol);
}

Klas TokyoStockExchange projektujemy tak, aby implementowaa ten interfejs. Musimy si rwnie
upewni, e konstruktor Portfolio jako argumentu oczekuje referencji do StockExchange:
public Portfolio {
private StockExchange exchange;
public Portfolio(StockExchange exchange) {
this.exchange = exchange;
}

//
}

[PPP].

166

ROZDZIA 10.

Teraz moemy utworzy testow implementacj interfejsu StockExchange, ktra emuluje


TokyoStockExchange. Taka testowa implementacja zwraca sta warto dla dowolnego przekazanego symbolu akcji. Jeeli nasz test demonstruje zakup piciu akcji firmy Microsoft do naszego
portfela, moemy uy implementacji testowej zwracajcej zawsze warto 100 dolarw za akcj. Nasza
testowa implementacja interfejsu StockExchange sprowadza si do zwykego wyszukania w tablicy.
Moemy teraz napisa test, ktry oczekuje, aby cakowita warto portfela wynosia 500 dolarw.
public class PortfolioTest {
private FixedStockExchangeStub exchange;
private Portfolio portfolio;
@Before
protected void setUp() throws Exception {
exchange = new FixedStockExchangeStub();
exchange.fix("MSFT", 100);
portfolio = new Portfolio(exchange);
}

@Test
public void GivenFiveMSFTTotalShouldBe500() throws Exception {
portfolio.add(5, "MSFT");
Assert.assertEquals(500, portfolio.value());
}

Jeeli moduy systemu s wystarczajco niezalene, aby mona byo je testowa w ten sposb, to
cay system jest rwnie bardziej elastyczny i uatwia ponowne wykorzystanie kodu. Brak sprze
oznacza, e elementy systemu s lepiej izolowane od siebie oraz od zmian. Izolacja ta uatwia zrozumienie kadego elementu systemu.
Przez minimalizacj sprze nasze klasy s zgodne z inn zasad projektowania klas, znan jako
zasada odwrcenia zalenoci (DIP)5. Zasada ta gosi, e klasy powinny zalee od abstrakcji, a nie
klas konkretnych.
Nasza klasa Portfolio nie zaley ju od szczegw implementacji klasy TokyoStockExchange,
ale od interfejsu StockExchange. Interfejs StockExchange reprezentuje abstrakcyjn koncepcj
odpytania o biec cen akcji. Abstrakcja ta izoluje wszystkie szczegy uzyskiwania tej ceny,
w tym rwnie tego, skd jest uzyskiwana cena.

Bibliografia
[RDD]: Rebecca Wirfs-Brock i inni, Object Design: Roles, Responsibilities, and Collaborations,
Addison-Wesley 2002.
[PPP]: Robert C. Martin, Agile Software Development: Principles, Patterns, and Practices, Prentice
Hall 2002.
[Knuth92]: Donald E. Knuth, Literate Programming, Center for the Study of language and Information,
Leland Stanford Junior University 1992.
5

[PPP].

KLASY

167

168

ROZDZIA 10.

ROZDZIA 11.

Systemy
Dr Kevin Dean Wampler

Zoono zabija. Zatruwa ycie programistw i utrudnia planowanie,


budowanie i testowanie produktw.
Ray Ozzie, CTO, Microsoft Corporation

169

Jak budowaby miasto?


Czy mona samodzielnie zarzdza wszystkimi szczegami jakiego duego systemu? Prawdopodobnie nie. Nawet zarzdzanie istniejcym miastem wykracza poza moliwoci jednej osoby. Jednak miasta funkcjonuj (w wikszoci przypadkw). Funkcjonuj, poniewa istniej zespoy ludzi,
ktrzy zarzdzaj okrelonymi czciami miasta, systemem wodocigowym, systemem zasilania,
sterowania ruchem, pilnowaniem prawa, numerami budynkw i tak dalej. Cz z tych ludzi jest
odpowiedzialna za ogln koncepcj, natomiast inni skupiaj si na szczegach.
Miasta funkcjonuj, poniewa wypracowane zostay odpowiednie poziomy abstrakcji i modularnoci,
ktre pozwalaj poszczeglnym osobom i zarzdzanym przez nie komponentom pracowa efektywnie, bez koniecznoci rozumienia dziaania wszystkich mechanizmw.
Cho zespoy programistyczne s rwnie podobnie zorganizowane, systemy, nad ktrymi pracuj,
czsto nie maj podobnej separacji zada i poziomw abstrakcji. Czysty kod pozwala nam osign
to na niszych poziomach abstrakcji. W tym rozdziale zajmiemy si sposobami zachowania czystoci
na wyszym poziomie abstrakcji na poziomie systemu.

Oddzielenie konstruowania systemu od jego uywania


Na pocztek trzeba zauway, e konstruowanie i uywanie to zupenie inne procesy. W czasie,
gdy pisz te sowa, z mojego okna w Chicago widz budow nowego hotelu. Obecnie jest to nagie
cementowe pudo z urawiami konstrukcyjnymi oraz windami przyczepionymi na jego cianach.
Wszyscy pracujcy tam ludzie nosz kaski i ubrania robocze. Po mniej wicej roku hotel zostanie
ukoczony. urawie i winda znikn. Budynek bdzie czysty, obudowany atrakcyjn wizualnie
szklan elewacj. Pracownicy i gocie hotelu rwnie bd wygldali inaczej.
Systemy oprogramowania powinny oddzieli proces uruchamiania, w ktrym s budowane
obiekty aplikacji i czone ze sob zalenoci, od logiki dziaania, ktra jest wykorzystywana
po uruchomieniu.
Proces uruchomienia jest problemem, ktry musi by rozwizany w kadej aplikacji. Jest to pierwszy problem, jaki przeanalizujemy w tym rozdziale. Jedn z najstarszych i najwaniejszych technik
projektowania jest rozdzielenie problemw.
Niestety, w wikszoci aplikacji problem ten nie jest wydzielony. Kod dla procesu uruchamiania jest
tworzony ad hoc i jest wymieszany z logik dziaania. Poniej przedstawiony jest typowy przykad:
public Service getService() {
if (service == null)
service = new MyServiceImpl(...); // Odpowiednie wartoci domylne dla wikszoci przypadkw?
return service;
}

Jest to idiom pnej inicjalizacji (wartociowania), ktry ma kilka zalet. Nie ponosimy kosztu utworzenia, jeeli aktualnie korzystamy z takiego obiektu, a w wyniku zastosowania tego mechanizmu czas uruchomienia moe by mniejszy. Zapewnia on rwnie, e nigdy nie zostanie zwrcona warto null.

170

ROZDZIA 11.

Jednak powoduje to wbudowanie zalenoci od MyServiceImpl i wszystkich elementw wymaganych


przez konstruktor (ktre tu pominem). Nie moemy skompilowa kodu bez rozwizania tych zalenoci, nawet jeeli w rzeczywistoci nie korzystamy w czasie jego dziaania z obiektw tego typu!
Problemem moe by testowanie. Jeeli MyServiceImpl jest cikim obiektem, musimy si upewni,
e przed wywoaniem metod w czasie testowania jednostkowego do pola usugi przypisane zostay
odpowiednie obiekty Test Double1 lub Mock. Poniewa kod konstrukcyjny jest wymieszany z normalnym przetwarzaniem, powinnimy przetestowa wszystkie cieki wykonania (na przykad test null
i jego bloku). Posiadanie obu tych odpowiedzialnoci oznacza, e metoda wykonuje wicej ni jedn
operacj, wic amie ona zasad pojedynczej odpowiedzialnoci.
Jednak chyba gorsze dla nas jest to, e nie wiemy, czy MyServiceImpl jest waciwym obiektem dla
wszystkich przypadkw. Zapisaem to w komentarzu. Dlaczego klasa zawierajca t metod musi zna
kontekst globalny? Czy kiedykolwiek bdziemy wiedzie, jaki jest waciwy obiekt do wykorzystania
w tym miejscu? Czy jest moliwe, e jeden typ bdzie odpowiedni dla wszystkich moliwych kontekstw?
Jedno wystpienie pnej inicjalizacji nie jest oczywicie powanym problemem. Jednak zwykle
w aplikacji tego typu idiom wystpuje wiele razy. Dlatego globalna strategia konfiguracji (jeeli istnieje) jest rozsiana w caej aplikacji, zachowujc niewielk modularno, i czsto jest wielokrotnie
powielana.
Jeeli aktywnie budujemy odpowiednio skonstruowane i solidne systemy, nie powinnimy pozwala
niewielkim, wygodnym idiomom na zamanie modularnoci. Proces tworzenia obiektw i jego konfiguracji nie jest wyjtkiem. Powinnimy zmodularyzowa ten proces, oddzielajc go od normalnej logiki
dziaania, oraz upewni si, e mamy globaln, spjn strategi rozwizywania gwnych zalenoci.

Wydzielenie moduu main


Jednym ze sposobw oddzielenia konstrukcji od uywania jest proste przeniesienie wszystkich
aspektw tworzenia do main lub moduw wywoywanych przez main oraz zaprojektowanie pozostaych czci systemu tak, aby zakaday, e wszystkie obiekty s odpowiednio utworzone i skonfigurowane (patrz rysunek 11.1).

R Y S U N E K 1 1 . 1 . Wydzielenie konstrukcji do main()

[Mezzaros07].

SYSTEMY

171

Przebieg sterowania jest atwy w ledzeniu. Funkcja main buduje wszystkie niezbdne obiekty systemu,
a nastpnie przekazuje je do aplikacji, ktra po prostu z nich korzysta. Naley zwrci uwag na kierunek strzaek zalenoci przechodzcych przez barier pomidzy main i aplikacj. Wszystkie wskazuj w jednym kierunku, od main na zewntrz. Oznacza to, e aplikacja nie ma adnych informacji
o funkcji main ani o procesie tworzenia. Po prostu oczekuje, e wszystko zostao prawidowo zbudowane.

Fabryki
Czasami konieczne jest, aby aplikacja bya odpowiedzialna za to, kiedy zostanie utworzony obiekt.
Na przykad w systemie przetwarzania zamwie aplikacja musi utworzy obiekty LineItem w celu
dodania ich do Order. W takim przypadku moemy skorzysta ze wzorca fabryki abstrakcyjnej 2
(ang. Abstract Factory) w celu przekazania aplikacji kontroli nad tym, kiedy naley zbudowa obiekty
LineItem, ale szczegy jego tworzenia pozostan oddzielone od kodu aplikacji (patrz rysunek 11.2).

R Y S U N E K 1 1 . 2 . Oddzielenie konstrukcji za pomoc fabryki abstrakcyjnej

I w tym przypadku powinnimy zauway, e wszystkie zalenoci s skierowane z main do aplikacji


OrderProcessing. Oznacza to, e aplikacja jest oddzielona od szczegw tworzenia obiektu LineItem.
Funkcja ta jest dostpna w LineItemFactoryImplementation, ktra znajduje si na rysunku po
stronie main. Dziki temu aplikacja ma pen kontrol nad momentem utworzenia obiektu LineItem,
a nawet moe przekazywa do konstruktora argumenty specyficzne dla aplikacji.

Wstrzykiwanie zalenoci
Zaawansowanym mechanizmem oddzielania konstrukcji od uytkowania jest wstrzykiwanie zalenoci (ang. Dependency Injection DI), bdce zastosowaniem odwrcenia sterowania (ang. Inversion
of Control IoC) do zarzdzania zalenociami3. Odwrcenie sterowania przenosi drugorzdne
odpowiedzialnoci z obiektu do innego obiektu, dedykowanego do tych zada, przez co zapewnia
si zachowanie zasady pojedynczej odpowiedzialnoci. W kontekcie zarzdzania zalenociami
2

[GOF].

Przykady mona znale w [Fowler].

172

ROZDZIA 11.

obiekt nie powinien by odpowiedzialny za samodzielne tworzenie zalenoci. Powinien przekaza


t odpowiedzialno do innego autorytarnego mechanizmu, odwracajc w ten sposb sterowanie. Poniewa konfiguracja jest problemem globalnym, ten autorytarny mechanizm zwykle bdzie
moduem main lub specjalizowanym kontenerem.
Wyszukiwanie JNDI jest czciow implementacj DI, w ktrej obiekt odpytuje serwer katalogu
w celu dostarczenia usugi odpowiadajcej okrelonej nazwie.
MyService myService = (MyService)(jndiContext.lookup("NameOfMyService"));

Obiekt wywoujcy nie steruje rodzajem zwracanego obiektu (o ile oczywicie implementuje on
odpowiedni interfejs), ale wywoujcy obiekt nadal aktywnie rozwizuje zalenoci.
Prawdziwe wstrzykiwanie zalenoci idzie o krok dalej. Klasa nie wykonuje bezporednich krokw
w celu rozwizania jej zalenoci jest cakowicie pasywna. Zamiast tego udostpnia metody
ustawiajce lub argumenty konstruktora (albo oba te mechanizmy), ktre s uywane do wstrzykiwania zalenoci. W czasie procesu konstrukcji kontener DI tworzy wymagane obiekty (zwykle na
danie) i korzysta z argumentw konstruktora lub metod ustawiajcych do dowizania zalenoci.
To, ktre zalene obiekty s w rzeczywistoci uyte, jest definiowane w pliku konfiguracyjnym lub
programowo w specjalizowanym module konstrukcyjnym.
Najlepszym znanym kontenerem DI dla jzyka Java jest Spring Framework4. W pliku XML definiujemy, ktre obiekty naley ze sob poczy, a nastpnie w kodzie Java pytamy o okrelone obiekty
z uyciem ich nazwy. Odpowiedni przykad zostanie przedstawiony w dalszej czci rozdziau.
Co jednak z zaletami pnej inicjalizacji? W przypadku DI idiom ten jest czasami przydatny.
Po pierwsze, wikszo kontenerw DI nie tworzy obiektw do momentu, a s potrzebne. Po drugie,
wiele z tych kontenerw udostpnia mechanizmy wywoywania fabryk abstrakcyjnych lub porednikw konstrukcji, ktre mog by uyte do pnego wartociowania lub podobnych optymalizacji5.

Skalowanie w gr
Miasta wyrastaj z miasteczek, ktre z kolei rozwijaj si z osad. Na pocztku drogi s wskie
i niewygodne, nastpnie s utwardzane, a z czasem poszerzane. Mae budynki i puste przestrzenie
s zastpowane wikszymi budynkami, ktrych cz w kocu zostanie zastpiona drapaczami
chmur.
Na pocztku niedostpne s takie usugi jak energia, woda, odprowadzanie ciekw, a nawet internet
(ech!). Usugi te s dodawane wraz ze wzrostem populacji i gstoci budynkw.
Wzrost ten nie przebiega bezbolenie. Ile razy jechalimy zderzak w zderzak przez miejsce, w ktrym by prowadzony projekt ulepszania drogi, i pytalimy si: Dlaczego nie zbudowano od razu
drogi o odpowiedniej szerokoci?.
4

Patrz [Spring]. Istnieje rwnie biblioteka Spring.NET.

Nie naley zapomina, e pne tworzenie i wartociowanie jest tylko optymalizacj, i to prawdopodobnie przedwczesn!

SYSTEMY

173

Jednak nie moe si to dzia w inny sposb. Kto uzasadni wydanie gry pienidzy na szeciopasmow autostrad przez rodek maego miasteczka, ktre prawdopodobnie kiedy si powikszy?
Kto chciaby mie tak drog w swoim miasteczku?
Mitem jest, e moemy otrzyma waciwe systemy za pierwszym razem. Zamiast tego powinnimy wdraa tylko obecne potrzeby, a nastpnie przebudowywa i rozszerza system, aby spenia
potrzeby w przyszoci. Jest to esencja wielokrotnej i iteracyjnej solidnoci. Programowanie sterowane testami, przebudowywanie oraz tworzenie czystego kodu pozwala na prac na poziomie kodu.
Co jednak mona powiedzie o poziomie systemowym? Czy architektura systemu wymaga wstpnego planowania? Czy nie moe ona zwiksza si przyrostowo od prostej do zoonej?
Systemy oprogramowania s inne ni fizyczne systemy. Ich architektury mog si zwiksza
przyrostowo, jeeli zapewnimy odpowiedni separacj problemw.
Jak si okazuje, ulotna natura systemw programowych pozwala na takie planowanie. Na pocztek przedstawimy negatywny przykad architektury, w ktrej problemy zostay nieodpowiednio
odseparowane.
Oryginalne architektury EJB1 oraz EJB2 nie rozdzielay odpowiednio problemw i przez to wprowadzay niepotrzebne bariery wzrostu organicznego. Wemy jako przykad Entity Bean dla trwaej
klasy Bank. Entity Bean jest pamiciow reprezentacj danych relacyjnych, czyli, mwic inaczej,
wiersza w tabeli.
Po pierwsze, konieczne byo zdefiniowanie lokalnych (wewntrz procesu) lub zdalnych (w osobnym
rodowisku JVM) interfejsw uywanych przez klientw. Na listingu 11.1 przedstawiony jest przykadowy interfejs lokalny.
L I S T I N G 1 1 . 1 . Lokalny interfejs EJB2 dla EJB Bank
package com.example.banking;
import java.util.Collections;
import javax.ejb.*;
public interface BankLocal extends java.ejb.EJBLocalObject {
String getStreetAddr1() throws EJBException;
String getStreetAddr2() throws EJBException;
String getCity() throws EJBException;
String getState() throws EJBException;
String getZipCode() throws EJBException;
void setStreetAddr1(String street1) throws EJBException;
void setStreetAddr2(String street2) throws EJBException;
void setCity(String city) throws EJBException;
void setState(String state) throws EJBException;
void setZipCode(String zip) throws EJBException;
Collection getAccounts() throws EJBException;
void setAccounts(Collection accounts) throws EJBException;
void addAccount(AccountDTO accountDTO) throws EJBException;
}

174

ROZDZIA 11.

Przedstawionych jest tu kilka atrybutw adresu banku oraz kolekcja kont prowadzonych przez
bank, z ktrych kade bdzie posiadao wasne dane obsugiwane przez osobny EJB Account.
Na listingu 11.2 przedstawiona jest odpowiednia implementacja klasy Bank.
L I S T I N G 1 1 . 2 . Implementacja Entity Bean w EJB2
package com.example.banking;
import java.util.Collections;
import javax.ejb.*;
public abstract class Bank implements javax.ejb.EntityBean {

// Logika biznesowa...
public abstract String getStreetAddr1();
public abstract String getStreetAddr2();
public abstract String getCity();
public abstract String getState();
public abstract String getZipCode();
public abstract void setStreetAddr1(String street1);
public abstract void setStreetAddr2(String street2);
public abstract void setCity(String city);
public abstract void setState(String state);
public abstract void setZipCode(String zip);
public abstract Collection getAccounts();
public abstract void setAccounts(Collection accounts);
public void addAccount(AccountDTO accountDTO) {
InitialContext context = new InitialContext();
AccountHomeLocal accountHome = context.lookup("AccountHomeLocal");
AccountLocal account = accountHome.create(accountDTO);
Collection accounts = getAccounts();
accounts.add(account);
}

// Logika kontenera EJB.


public
public
public
public

abstract void setId(Integer id);


abstract Integer getId();
Integer ejbCreate(Integer id) { ... }
void ejbPostCreate(Integer id) { ... }

// Pozostaa cz musi by zaimplementowana, ale z reguy metody s puste:


public
public
public
public
public
public
public

void
void
void
void
void
void
void

setEntityContext(EntityContext ctx) {}
unsetEntityContext() {}
ejbActivate() {}
ejbPassivate() {}
ejbLoad() {}
ejbStore() {}
ejbRemove() {}

Nie zamieszczamy tu odpowiedniego interfejsu LocalHome, czyli fabryki wykorzystywanej do tworzenia obiektw, ani moliwych metod wyszukujcych dla klasy Bank.
Na koniec musimy napisa w XML-u jeden lub wicej deskryptorw instalacyjnych, ktre definiuj
szczegy mapowania obiektowo-relacyjnego dla magazynu, oczekiwane dziaanie transakcji, ograniczenia bezpieczestwa i tak dalej.
Logika biznesowa jest cile zczona z kontenerem aplikacji EJB2. Konieczne jest dziedziczenie
po typach kontenera i trzeba napisa wiele metod cyklu ycia, wymaganych przez kontener.

SYSTEMY

175

Z powodu tego sprzenia z cikim kontenerem wyizolowanie testw jednostkowych jest trudne.
Konieczne jest napisanie imitacji kontenera, co jest trudne, lub marnowanie wiele czasu na instalowanie EJB i wykonywanie testw na rzeczywistym serwerze. Ponowne uycie kodu poza architektur EJB2 jest niemoliwe z powodu tego cisego sprzenia.
Nawet programowanie obiektowe jest ograniczone. Jeden bean nie moe dziedziczy po innym.
Zwrmy uwag na kod dodawania nowego konta. Standardem w EJB2 jest definiowanie obiektw transferu danych (DTO), ktre s w rzeczywistoci strukturami bez adnych operacji. Zwykle
prowadzi to do powstania nadmiarowych typw przechowujcych dokadnie te same dane i wymaga kodu zapewniajcego kopiowanie danych z jednego obiektu do drugiego.

Separowanie (rozcicie) problemw


Architektura EJB2 w niektrych obszarach zblia si do separacji problemw. Na przykad wymagane
dziaanie transakcji, zabezpiecze i niektre zachowania mechanizmw trwaoci s deklarowane
w deskryptorze instalacji, niezalenie od kodu rdowego.
Naley zauway, e problemy takie jak trwao obiektw zwykle maj tendencj do przechodzenia
przez naturalne granice obiektw domeny. Zazwyczaj chcemy zapisa wszystkie obiekty z uyciem
tej samej strategii, na przykad korzystajc z okrelonego systemu DBMS6 zamiast paskich plikw, korzystajc z okrelonej strategii nazewnictwa dla tabeli i kolumn, z uyciem spjnej semantyki obiektowej i tak dalej.
W teorii mona opisywa nasz strategi trwaoci w modularny, hermetyzowany sposb. Jednak
w praktyce musimy powieli waciwie ten sam kod, implementujcy strategi trwaoci, w wielu
obiektach. Do tego typu zagadnie stosujemy okrelenie rozcicie problemw. I w tym przypadku
biblioteka trwaoci moe by modularna, jak rwnie modularna moe by nasza logika domeny,
traktowana osobno. Problemy wynikaj z przecicia tych domen.
W rzeczywistoci sposb, w jaki architektura EJB obsuguje trwao obiektw, bezpieczestwo
aplikacji i transakcje, przewiduje programowanie zorientowane aspektowo (AOP)7, ktre jest
oglnym podejciem do przywracania modularnoci w przypadku rozcicia problemw.
W AOP konstrukcje modularne nazywane aspektami definiuj te punkty systemu, ktrych dziaanie naley zmieni w pewien spjny sposb, tak by obsugiway okrelone zagadnienie. Specyfikacja
ta jest wykonywana z uyciem zwizego mechanizmu deklaratywnego lub programowego.
W przypadku mechanizmu trwaoci obiektw moemy zadeklarowa, ktre obiekty i atrybuty (lub ich
wzorce) powinny by utrwalane, a nastpnie wydelegowa zadanie utrwalania do odpowiedniej biblioteki. Zadanie modyfikowania jest realizowane przez bibliotek AOP nieinwazyjnie8
dla docelowego kodu. Zapoznamy si teraz z trzema mechanizmami aspektowymi lub podobnymi
do aspektowych w jzyku Java.
6

Systemy zarzdzania baz danych.

Wicej informacji na temat aspektw mona znale w [AOSD], a informacje na temat Aspect-J w [AspectJ] i [Colyer].

Czyli nie ma potrzeby edycji docelowego kodu rdowego.

176

ROZDZIA 11.

Poredniki Java
Mechanizm porednikw Java jest odpowiedni dla prostych przypadkw, takich jak otaczanie wywoa metody w pojedynczych obiektach lub klasach. Jednak dynamiczne poredniki zapewniane
w JDK operuj tylko na interfejsach. Aby zapewni poredniczenie dla klas, naley skorzysta z bibliotek manipulujcych kodem wynikowym, takim jak CGLIB, ASM lub Javassist9.
Na listingu 11.3 przedstawiony jest szkielet porednika Java zapewniajcego obsug trwaoci dla
naszej aplikacji Bank, obsugujcy tylko metody pobierania i ustawiania list kont.
L I S T I N G 1 1 . 3 . Przykad porednika JDK
// Bank.java (pominite nazwy pakietw...).
import java.utils.*;

// Abstrakcja banku.
public interface Bank {
Collection<Account> getAccounts();
void setAccounts(Collection<Account> accounts);
}

// BankImpl.java
import java.utils.*;

// Obiekt "Plain Old Java Object" (POJO) implementujcy abstrakcj.


public class BankImpl implements Bank {
private List<Account> accounts;
public Collection<Account> getAccounts() {
return accounts;
}
public void setAccounts(Collection<Account> accounts) {
this.accounts = new ArrayList<Account>();
for (Account account: accounts) {
this.accounts.add(account);
}
}
}

// BankProxyHandler.java
import java.lang.reflect.*;
import java.util.*;

// "InvocationHandler" wymagany przez API porednika.


public class BankProxyHandler implements InvocationHandler {
private Bank bank;
public BankHandler (Bank bank) {
this.bank = bank;
}

// Metoda zdefiniowana w InvocationHandler


public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
String methodName = method.getName();
if (methodName.equals("getAccounts")) {
bank.setAccounts(getAccountsFromDatabase());
9

Patrz [CGLIB], [ASM] i [Javassist].

SYSTEMY

177

return bank.getAccounts();
} else if (methodName.equals("setAccounts")) {
bank.setAccounts((Collection<Account>) args[0]);
setAccountsToDatabase(bank.getAccounts());
return null;
} else {
...
}
}

// Tutaj sporo szczegw:


protected Collection<Account> getAccountsFromDatabase() { ... }
protected void setAccountsToDatabase(Collection<Account> accounts) { ... }
}

// W innym miejscu...
Bank bank = (Bank) Proxy.newProxyInstance(
Bank.class.getClassLoader(),
new Class[] { Bank.class },
new BankProxyHandler(new BankImpl()));

Zdefiniowalimy interfejs Bank, ktry zosta osonity za pomoc porednika, oraz Plain-Old Java
Object (POJO), BankImpl, implementujcy logik biznesow (obiekty POJO zostan przedstawione
w dalszej czci rozdziau).
API porednika wymaga zastosowania obiektu InvocationHandler, ktry przekazuje wywoania
metod do implementacji Bank. Nasz BankProxyHandler korzysta z API refleksji do odwzorowania
oglnych wywoa metod na metody w BankImpl.
Mamy tu bardzo duo kodu i jest on dosy skomplikowany nawet dla prostego przypadku10. Uycie
jednej z bibliotek manipulujcych kodem wynikowym jest podobnie zoone. Taka objto kodu
i jego skomplikowanie to dwie powane wady porednikw. Powoduj one, e trudniej tworzy
czysty kod! Dodatkowo poredniki nie zapewniaj mechanizmw specyfikowania dostpnych systemowo punktw obsugi, ktre s niezbdne w prawdziwym rozwizaniu AOP11.

Czyste biblioteki Java AOP


Na szczcie wikszo zada porednikw moe by obsuona automatycznie przez narzdzia.
Poredniki s uywane wewntrznie w kilku bibliotekach Java, na przykad Spring AOP oraz JBoss
AOP, do implementowania aspektw w czystym jzyku Java12. W Spring logik pisze si w postaci
obiektw Plain-Old Java Objects. Obiekty POJO s ukierunkowane na swoj domen. Nie maj one
zalenoci z bibliotekami typu enterprise (ani innymi domenami). Dziki temu s koncepcyjnie
prostsze i atwiejsze do testowania. Ich wzgldna prostota uatwia sprawdzenie, czy prawidowo
implementuj odpowiednie problemy uytkownika, oraz ich utrzymanie i ewolucj kodu do obsugi
kolejnych problemw w przyszoci.
10

Bardziej szczegowe przykady API porednikw i przykady jego zastosowania mona znale w [Goetz].

11

AOP jest czasami mylone z technikami jego implementacji, takimi jak przechwytywanie metod i osanianie za pomoc
porednikw. Prawdziw wartoci AOP jest moliwo specyfikowania zachowa systemowych w spjny i modularny sposb.

12

Patrz [Spring] oraz [JBoss]. Czysta Java oznacza, e nie zastosowano AspectJ.

178

ROZDZIA 11.

Wymagan infrastruktur aplikacji, w tym zagadnienia rozcicia problemw, takie jak trwao,
transakcje, bezpieczestwo, buforowanie, przeczanie w czasie awarii i tak dalej, doczamy przez
zastosowanie deklaratywnych plikw konfiguracyjnych lub API. W wielu przypadkach faktycznie
specyfikujemy aspekty bibliotek Spring lub JBoss, natomiast biblioteki obsuguj mechanik zastosowania porednikw Java lub bibliotek kodu wynikowego w sposb niewidoczny dla uytkownika.
Deklaracje te steruj kontenerem wstrzykiwania zalenoci (DI), ktry tworzy na danie wikszo
obiektw i czy je ze sob.
Na listingu 11.4 przedstawiony jest typowy fragment pliku konfiguracyjnego Spring V2.5, app.xml13.
L I S T I N G 1 1 . 4 . Plik konfiguracyjny Spring 2.X
<beans>
...
<bean id="appDataSource"
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"
p:driverClassName="com.mysql.jdbc.Driver"
p:url="jdbc:mysql://localhost:3306/mydb"
p:username="me"/>
<bean id="bankDataAccessObject"
class="com.example.banking.persistence.BankDataAccessObject"
p:dataSource-ref="appDataSource"/>
<bean id="bank"
class="com.example.banking.model.Bank"
p:dataAccessObject-ref="bankDataAccessObject"/>
...
</beans>

Kady bean jest podobny do jednej czci lalki matrioszki obiekt domeny Bank posiada jako
porednika (czyli jest opakowany przez) obiekt dostpu do danych (DAO), ktry z kolei ma jako
porednika sterownik JDBC rda danych (patrz rysunek 11.3).

R Y S U N E K 1 1 . 3 . Lalka matrioszka z dekoratorw

Klient uwaa, e wykonuje metod getAccounts() obiektu Bank, ale w rzeczywistoci komunikuje si z najbardziej zewntrznym ze zbioru dekoratorw14 rozszerzajcych podstawowe funkcje
POJO Bank. Moemy rwnie doda kolejne dekoratory dla transakcji, buforowania i innych
mechanizmw.

13

Zaadaptowany z http://www.theserverside.com/tt/articles/article.tss?l=IntrotoSpring25.

14

[GOF].

SYSTEMY

179

W aplikacji konieczne jest uycie tylko kilku wierszy kodu do odpytania kontenera DI o obiekty
najwyszego poziomu w systemie, zgodnie z definicj w pliku XML.
XmlBeanFactory bf =
new XmlBeanFactory(new ClassPathResource("app.xml", getClass()));
Bank bank = (Bank) bf.getBean("bank");

Dziki temu, e wymaganych jest tak niewiele wierszy kodu specyficznego dla Spring, aplikacja jest
niemal cakowicie odczona od Spring, co eliminuje problem cisego zczenia z systemami takimi
jak EJB2.
Cho XML moe by rozwleky i mao czytelny15, zasady zdefiniowane w tych plikach konfiguracyjnych s prostsze ni skomplikowana logika porednikw i aspektw, ktra jest ukrywana i tworzona automatycznie. Tego typu architektura jest tak kuszca, e biblioteki podobne do Spring doprowadziy do cakowitego przebudowania standardu EJB w wersji 3. EJB w wikszoci korzysta
z modelu Spring deklaratywnego wspierania rozcicia problemw, z uyciem plikw konfiguracyjnych XML oraz (lub) adnotacji Java 5.
Na listingu 11.5 przedstawiony jest nasz obiekt Bank zapisany w EJB316.
L I S T I N G 1 1 . 5 . EJB Bank w EJB3
package com.example.banking.model;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collection;
@Entity
@Table(name = "BANKS")
public class Bank implements java.io.Serializable {
@Id @GeneratedValue(strategy=GenerationType.AUTO)
private int id;
@Embeddable // Obiekt "wbudowany" w wiersz bazy danych dla obiektu Bank.
public class Address {
protected String streetAddr1;
protected String streetAddr2;
protected String city;
protected String state;
protected String zipCode;
}
@Embedded
private Address address;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER,
mappedBy="bank")
private Collection<Account> accounts = new ArrayList<Account>();
public int getId() {
return id;
}
public void setId(int id) {
15

Przykad moe by uproszczony z uyciem mechanizmw wykorzystujcych konwencj przed konfiguracj oraz adnotacje
Java 5 do redukcji iloci wymaganej logiki pocze.

16

Zaadaptowany z http://www.onjava.com/pub/a/onjava/2006/05/17/standardizing-with-ejb3-java-persistence-api.html.

180

ROZDZIA 11.

this.id = id;
}
public void addAccount(Account account) {
account.setBank(this);
accounts.add(account);
}
public Collection<Account> getAccounts() {
return accounts;
}
public void setAccounts(Collection<Account> accounts) {
this.accounts = accounts;
}
}

Ten kod jest znacznie czystszy od wczeniejszego kodu EJB2. Niektre szczegy encji nadal si
w nim znajduj, umieszczone w adnotacjach. Jednak dziki temu, e adna informacja nie znajduje
si poza adnotacjami, kod jest czysty, jasny i przez to atwy do testowania i utrzymania.
Jeeli bdzie to wymagane, niektre lub wszystkie informacje dotyczce trwaoci z adnotacji mog
by przeniesione do deskryptorw instalacji XML, co pozwoli pozostawi czyste obiekty POJO.
Jeeli szczegy odwzorowania trwaoci nie zmieniaj si czsto, wiele zespow wybiera korzystanie z adnotacji, poniewa ten mechanizm ma znacznie mniej wad w porwnaniu z inwazyjnym
sposobem programowania w EJB2.

Aspekty w AspectJ
Na koniec przedstawione zostanie najbogatsze narzdzie rozdzielania problemw za pomoc jzyka
AspectJ17 rozszerzenie jzyka Java, zapewniajce pierwszej klasy obsug aspektw do konstrukcji
modularnoci. Rozwizanie oparte na uyciu czystego jzyka Java, zapewniane przez Spring
AOP oraz JBoss AOP, w zupenoci wystarcza w znaczcej wikszoci przypadkw wykorzystania
aspektw. Jednak AspectJ zapewnia bardzo przydatny i zaawansowany zestaw narzdzi do rozdzielania problemw. Wad AspectJ jest potrzeba zastosowania kilku nowych narzdzi i nauczenia
si nowych konstrukcji jzyka i idiomw.
Problemy z adaptacj zostay czciowo ograniczone przez ostatnio wprowadzon posta adnotacji
dla AspectJ, w ktrej adnotacje Java 5 zostay uyte do definiowania aspektw z uyciem czystego
kodu Java. Dodatkowo Spring Framework posiada kilka funkcji, ktre znacznie uatwiaj zastosowanie aspektw bazujcych na adnotacjach w przypadku zespow majcych niewielkie dowiadczenie w AspectJ.
Pene omwienie rozszerzenia AspectJ wykracza poza zakres tej ksiki. Wicej informacji na ten
temat mona znale w [AspectJ], [Colyer] oraz [Spring].

17

Patrz [AspectJ] oraz [Colyer].

SYSTEMY

181

Testowanie architektury systemu


Sia rozdzielania problemw z uyciem metod aspektowych nie moe by niedoceniana. Jeeli moemy napisa logik domeny naszej aplikacji z uyciem obiektw POJO, odczonych na poziomie
kodu od problemw architektury, to moliwe jest testowanie naszej architektury. Mona rwnie
w razie potrzeby poddawa j zmianom, z prostej tworzc cakiem skomplikowan, przez adaptowanie nowych technologii. Nie jest konieczne wykonywanie wielkiego projektu od podstaw18 (ang. Big
Design Up Front BDUF). W rzeczywistoci projektowanie BDUF jest nawet szkodliwe, poniewa
uniemoliwia adaptowanie zmian m.in. z powodu psychologicznego oporu autora przed zaprzepaszczeniem wczeniejszej pracy oraz z powodu sposobu, w jaki wybory architektoniczne wpywaj
na dalsze mylenie o projekcie.
Architekci budynkw musz korzysta z BDUF, poniewa nie da si wprowadza radykalnych
zmian architektonicznych w duych strukturach fizycznych w czasie trwania ich budowy19. Jednak
oprogramowanie posiada wasn fizyk20, w ktrej moliwe jest wprowadzanie radykalnych zmian,
jeeli struktura oprogramowania efektywnie rozdziela problemy.
Oznacza to, e mona rozpocz projekt oprogramowania, korzystajc z naiwnie prostej, ale
rozczonej architektury, i dostarczajc szybko uytkownikom dziaajce rozwizania, a nastpnie
dodajc coraz wicej elementw infrastruktury w procesie skalowania w gr. Niektre z najwikszych na wiecie witryn WWW osigny wysok dostpno i wydajno przez zastosowanie
skomplikowanych mechanizmw buforowania danych, bezpieczestwa, wirtualizacji itp. w sposb
efektywny i elastyczny, poniewa minimalnie sprzone projekty byy odpowiednio proste na kadym
poziomie abstrakcji.
Oczywicie nie oznacza to, e idziemy w kierunku projektu bez steru. Mamy pewne oczekiwania co do
zakresu, celw i harmonogramu projektu, jak rwnie ogln wizj struktury wynikowego systemu. Jednak musimy utrzymywa moliwo zmiany kierunku w odpowiedzi na zmieniajce si okolicznoci.
Wczesna architektura EJB jest jednym z najlepiej znanych API, ktre s przeprojektowane i nie
umoliwiaj rozdzielenia problemw. Nawet dobrze zaprojektowane API moe by przesadne, jeeli nie jest ono naprawd potrzebne. Dobre API powinno w wikszoci przypadkw znika z widoku, aby zesp spdza wikszo swojego czasu na implementowaniu rozwiza dla uytkownika.
W przeciwnym razie ograniczenia architektury bd zmniejszay efektywno dostarczania klientom
optymalnych rozwiza.
Podsumujmy ten dugi opis:
Optymalna architektura systemu skada si z modularnych domen problemw; kada z nich jest
implementowana z uyciem zwykych obiektw, np. POJO. Osobne domeny s integrowane
ze sob przy uyciu minimalnie inwazyjnych narzdzi aspektowych lub podobnych. Architektura ta moe by testowana, podobnie jak kod.
18

Nie naley myli tego z dobr praktyk projektowania od podstaw BDUF jest praktyk projektowania wszystkiego od
pocztku, przed rozpoczciem implementowania czegokolwiek.

19

Nadal stosowane jest iteracyjne analizowanie i dyskusja nad detalami, nawet po rozpoczciu budowy.

20

Termin fizyka oprogramowania zosta po raz pierwszy uyty w [Kolence].

182

ROZDZIA 11.

Optymalizacja podejmowania decyzji


Modularno i rozdzielenie problemw umoliwiaj decentralizowane zarzdzanie i podejmowanie decyzji. W odpowiednio duym systemie, niezalenie od tego, czy jest to miasto, czy projekt
oprogramowania, jedna osoba nie moe podejmowa wszystkich decyzji.
Wszyscy wiemy, e najlepiej rozdziela odpowiedzialno midzy najbardziej kwalifikowane osoby.
Czsto zapominamy, e rwnie najlepiej opnia decyzj do ostatniego moliwego momentu. Nie
jest to lenistwo ani brak odpowiedzialnoci pozwala to nam dokona wyboru z uyciem najbardziej aktualnych informacji. Zbyt wczesna decyzja jest decyzj podjt na podstawie nieoptymalnych
informacji. Mamy w takim przypadku znacznie mniej informacji od klienta, wiedzy dotyczcej
projektu i dowiadczenia z naszymi wyborami implementacyjnymi.
Solidno zapewniana przez system POJO z modularnymi problemami pozwala na podejmowanie optymalnych decyzji na czas, przy czym decyzje te bazuj na najbardziej
aktualnej wiedzy. Zmniejszona jest rwnie zoono tych decyzji.

Korzystaj ze standardw, gdy wnosz realn warto


Fantastycznie jest patrze na plac budowy, zarwno ze wzgldu na szybko, z jak powstaj nowe
budynki (nawet w rodku zimy), jak i na nadzwyczajne projekty, jakie s moliwe do realizacji
z uyciem dzisiejszych technologii. Budownictwo jest dojrzaym przemysem korzystajcym z wysoce zoptymalizowanych czci, metod i standardw, ktre ewoluoway przez setki lat.
Wiele zespow uywao architektury EJB2, poniewa by to standard, nawet jeeli wystarczyo zastosowa prostsze projekty. Spotkaem si z zespoami, ktre miay obsesj na punkcie mocno przereklamowanych standardw i przestaway si skupia na dostarczaniu wartoci swoim klientom.
Standardy pozwalaj na wielokrotne wykorzystywanie pomysw i komponentw, zatrudnianie osb o odpowiednim dowiadczeniu, hermetyzacj dobrych pomysw i czenie ze sob
komponentw. Jednak proces tworzenia standardw moe zajmowa zbyt wiele czasu, przez co
cz z nich traci kontakt z faktycznymi potrzebami ich uytkownikw potrzebami, ktre te
standardy miay zaspokaja.

Systemy wymagaj jzykw dziedzinowych


Budownictwo, podobnie jak wikszo dziedzin technologii, wytworzyo bogaty jzyk ze sownictwem, idiomami oraz wzorcami21, ktre pozwalaj na przekazywanie istotnych informacji w jasny
i zwizy sposb. W przypadku oprogramowania ostatnio odnowio si zainteresowanie tworzeniem
jzykw dziedzinowych (DSL)22 osobnych, niewielkich jzykw skryptowych lub API w standardowych jzykach, pozwalajcych na napisanie kodu, ktry czyta si jak porzdnie skomponowan
proz.
21

Na informatyk najwikszy wpyw miaa praca [Alexander].

22

Patrz na przykad [DSL]. [JMock] jest dobrym przykadem API Java do tworzenia DSL.

SYSTEMY

183

Dobry jzyk DSL minimalizuje przepa komunikacyjn pomidzy koncepcj domeny a implementujcym j kodem, podobnie jak solidne praktyki optymalizuj komunikacj wewntrz zespou
oraz z udziaowcami projektu. Jeeli implementujemy logik domeny w tym samym jzyku, ktrego
uywa ekspert w zakresie domeny, zmniejszamy ryzyko niewaciwej konwersji domeny na implementacj.
Uywane efektywnie jzyki DSL podnosz poziom abstrakcji ponad idiomy kodu i wzorce projektowe. Pozwalaj programistom na odcyfrowanie przeznaczenia kodu na odpowiednim poziomie
abstrakcji.
Jzyki dziedzinowe pozwalaj wszystkim poziomom abstrakcji i wszystkim domenom w aplikacji na korzystanie z POJO poczwszy od zasad wysokiego poziomu, na szczegach niskiego poziomu skoczywszy.

Zakoczenie
Systemy rwnie musz by czyste. Inwazyjna architektura przytacza logik domeny i niekorzystnie wpywa na jej solidno. Gdy logika domeny jest zaburzona, spada jako dziaania systemu,
poniewa bdy znacznie atwiej ukrywaj si w aplikacji, a implementacja rozwiza staje si
trudniejsza. W efekcie spada wydajno i zalety TDD s tracone.
Intencje powinny by jasne na wszystkich poziomach abstrakcji. Zdarza si to tylko wtedy, gdy korzystamy z POJO oraz mechanizmw zblionych do aspektowych w celu nieinwazyjnego zastosowania innych implementacji problemw.
Niezalenie od tego, czy projektujemy systemy, czy indywidualne moduy, nigdy nie naley zapomina o korzystaniu z najprostszych skutecznych mechanizmw.

Bibliografia
[Alexander]: Christopher Alexander, A Timeless Way of Building, Oxford University Press,
New York 1979.
[AOSD]: Aspect-Oriented Software Development, http://aosd.net.
[ASM]: Strona gwna ASM, http://asm.objectweb.org.
[AspectJ]: http://eclipse.org/aspectj.
[CGLIB]: Code Generation Library, http://cglib.sourceforge.net.
[Colyer]: Adrian Colyer, Andy Clement, George Hurley, Mathew Webster, Eclipse AspectJ, Person
Education, Inc., Upper Saddle River, New York 2005.

184

ROZDZIA 11.

[DSL]: Jzyk dziedzinowy, http://en.wikipedia.org/wiki/Domainspecific_programming_language


lub http://pl.wikipedia.org/wiki/J%C4%99zyk_dziedzinowy.
[Fowler]: Inversion of Control Containers and the Dependency Injection pattern, http://martinfowler.com/
articles/injection.html.
[Goetz]: Brian Goetz, Java Theory and Practice: Decorating with Dynamic Proxies, http://www.ibm.com/
developerworks/java/library/j-jtp08305.html.
[Javassist]: Strona gwna Javassist, http://www.csg.is.titech.ac.jp/~chiba/javassist.
[JBoss]: Strona gwna JBoss, http://jboss.org.
[JMock]: Jmock A Lightweight Mock Object Library for Java, http://jmock.org.
[Kolence]: Kenneth W. Kolence, Software physics and computer performance measurements,
Proceedings of the ACM annual conference Volume 2, Boston, Massachusetts, s. 1024 1040, 1972.
[Spring]: The Spring Framework, http://www.springframework.org.
[Mezzaros07]: Gerard Mezzaros, XUnit Patterns, Addison-Wesley 2007.
[GOF]: Gamma i inni, Elements of Reusable Object Oriented Software, Addison-Wesley 1996.

SYSTEMY

185

186

ROZDZIA 11.

ROZDZIA 12.

Powstawanie projektu
Jeff Langr

Uzyskiwanie czystoci projektu przez jego rozwijanie


Co by byo, gdyby dostpne byy cztery proste zasady, ktre pomagayby tworzy dobre projekty?
Co by byo, gdyby korzystanie z tych zasad dawao wgld w struktur i projekt kodu, uatwiajc
stosowanie takich zasad, jak SRP i DIP? Co by byo, gdyby te cztery zasady uatwiay powstawanie
dobrych projektw?
Wielu z nas uwaa, e cztery zasady prostego projektu1 opracowane przez Kenta Becka s znaczc
pomoc przy tworzeniu dobrze zaprojektowanego oprogramowania.

[XPE].

187

Wedug Kenta projekt jest prosty, jeeli spenia nastpujce zasady:

Przechodzi wszystkie testy.

Nie zawiera powtrze.

Wyraa intencje programisty.

Minimalizuje liczb klas i metod.

Zasady te s zamieszczone zgodnie z hierarchi wanoci.

Zasada numer 1 prostego projektu


system przechodzi wszystkie testy
Po pierwsze i najwaniejsze, projekt musi da w wyniku system dziaajcy w zamierzony sposb.
System moe mie doskonay projekt na papierze, ale jeeli nie istnieje prosty sposb na weryfikowanie, czy dziaa on w oczekiwany sposb, to cay projekt papierowy jest niewiele warty.
System, ktry jest dokadnie testowany i przechodzi za kadym razem wszystkie testy, jest systemem testowalnym. Mona przytoczy oczywiste, ale wane zdanie: systemy, ktre nie s testowalne,
nie s rwnie weryfikowalne. Z kolei system, ktrego nie mona zweryfikowa, nie powinien by
nigdy instalowany.
Na szczcie zadanie doprowadzenia do testowalnoci systemu kieruje nas w stron projektu,
w ktrym klasy s mae i jednozadaniowe. Po prostu atwiej jest testowa klasy, ktre speniaj SRP.
Im wicej testw napiszemy, tym czciej bdziemy tworzy konstrukcje atwe w testowaniu. Tak
wic zapewnienie penej testowalnoci systemu pomaga w tworzeniu lepszych projektw.
cise sprzenie utrudnia pisanie testw. Podobnie, im wicej testw napiszemy, tym czciej bdziemy korzysta z zasad takich jak DIP i narzdzi takich jak interfejsy wstrzykiwania zalenoci
i abstrakcji zapewniajcych minimalizacj sprze. Nasze projekty stan si jeszcze lepsze.
Co ciekawe, dziaanie zgodnie z prost i oczywist zasad mwic, e musimy mie testy i stale je
wykonywa, wpywa na zgodno systemu z podstawowymi celami obiektowoci niskiego
sprzenia i wysokiej spjnoci. Pisanie testw prowadzi do tworzenia lepszych projektw.

Zasady numer 2 4 prostego projektu przebudowa


Gdy mamy zbudowane testy, jestemy przygotowani do zapewnienia czystoci kodu i klas. Realizujemy to przez przyrostow przebudow kodu. Po dodaniu kadego wiersza kodu powinnimy si
zatrzyma i pomyle o nowym projekcie. Czy wanie go nie zepsulimy? Jeeli tak, naley poprawi kod i wykona testy w celu pokazania, e niczego nie uszkodzilimy. W rzeczywistoci posiadanie
tych testw eliminuje obaw, e proces czyszczenia kodu uszkodzi go!

188

ROZDZIA 12.

W czasie operacji przebudowy moemy stosowa dowolne techniki z bogatej wiedzy na temat dobrych projektw oprogramowania. Moemy zwiksza spjno projektu, zmniejsza sprzenia,
rozdziela problemy, modularyzowa mechanizmy, skraca funkcje i klasy, tworzy lepsze nazwy
i tak dalej. Tu rwnie stosujemy trzy ostatnie zasady prostego projektu eliminujemy powtrzenia, zapewniamy wyrazisto kodu i minimalizujemy liczb klas i metod.

Brak powtrze
Powtrzenia s wrogiem publicznym w dobrze zaprojektowanym systemie. Reprezentuj one dodatkow prac, dodatkowe ryzyko i dodatkow niepotrzebn zoono. Powtrzenia pojawiaj si
w wielu formach. Wiersze kodu wygldajce dokadnie tak samo s oczywicie powtrzeniami.
Wiersze kodu, ktre s podobne, czsto mog by przeksztacone tak, aby wyglday bardziej podobnie,
dziki czemu mog by one atwiej przebudowane. Powtrzenia mog rwnie istnie w innych
formach, jak na przykad powtrzenia implementacji. Moemy mie dwie metody w kolekcji klas:
int size() {}
boolean isEmpty() {}

Moemy mie osobne implementacje dla kadej z tych metod. Metoda isEmpty moe zwraca
warto logiczn, natomiast size warto licznika. Mona rwnie wyeliminowa powtrzenia
przez zwizanie isEmpty z definicj size:
boolean isEmpty() {
return 0 == size();
}

Tworzenie czystego systemu wymaga eliminowania powtrze, nawet jeeli jest to tylko kilka wierszy
kodu. Wemy pod uwag nastpujcy kod:
public void scaleToOneDimension(
float desiredDimension, float imageDimension) {
if (Math.abs(desiredDimension - imageDimension) < errorThreshold)
return;
float scalingFactor = desiredDimension / imageDimension;
scalingFactor = (float)(Math.floor(scalingFactor * 100) * 0.01f);
RenderedOp newImage = ImageUtilities.getScaledImage(
image, scalingFactor, scalingFactor);
image.dispose();
System.gc();
image = newImage;
}
public synchronized void rotate(int degrees) {
RenderedOp newImage = ImageUtilities.getRotatedImage(
image, degrees);
image.dispose();
System.gc();
image = newImage;
}

Aby zachowa czysto systemu, powinnimy wyeliminowa mae powtrzenia pomidzy metodami scaleToDimension a rotate:

POWSTAWANIE PROJEKTU

189

public void scaleToOneDimension(


float desiredDimension, float imageDimension) {
if (Math.abs(desiredDimension - imageDimension) < errorThreshold)
return;
float scalingFactor = desiredDimension / imageDimension;
scalingFactor = (float)(Math.floor(scalingFactor * 100) * 0.01f);
replaceImage(ImageUtilities.getScaledImage(
image, scalingFactor, scalingFactor));
}
public synchronized void rotate(int degrees) {
replaceImage(ImageUtilities.getRotatedImage(image, degrees));
}
private void replaceImage(RenderedOp newImage) {
image.dispose();
System.gc();
image = newImage;
}

Gdy wyodrbniamy wsplne czci na tak niskim poziomie, zaczynamy rozpoznawa naruszenia
SRP. Moemy wic przenie nowe, wyekstrahowane metody do innej klasy. Pozwala to poprawi
przejrzysto kodu. Inna osoba z zespou moe rozpozna moliwo utworzenia dalszej abstrakcji
w nowej metodzie i ponownie j wykorzysta w innym kontekcie. Takie ponowne uycie w maej
skali moe spowodowa znaczce zmniejszenie zoonoci systemu. Jeeli bdziemy widzie, w jaki
sposb ponownie wykorzystywa kod w maej skali, to znacznie prostsze bdzie jego wykorzystanie
w wikszej skali.
Zastosowanie wzorca szablonu metody2 (ang. Template Method) jest czsto stosowan technik
usuwania powtrze na wysokim poziomie. Na przykad:
public class VacationPolicy {
public void accrueUSDivisionVacation() {

// Kod obliczajcy urlopy, bazujcy na godzinach przepracowanych do daty.


// ...
// Kod sprawdzajcy, czy urlop spenia minimalne wymagania z USA.
// ...
// Kod zapisujcy urlop w systemie pacowym.
//
}
public void accrueEUDivisionVacation() {

// Kod obliczajcy urlopy, bazujcy na godzinach przepracowanych do daty.


// ...
// Kod sprawdzajcy, czy urlop spenia minimalne wymagania przepisw Unii Europejskiej.
// ...
// Kod zapisujcy urlop w systemie pacowym.
// ...
}
}

Kod w metodach accrueUSDivisionVacation oraz accrueEuropeanDivisionVacation jest


w wikszoci taki sam, z wyjtkiem obliczania wartoci minimalnych. Ten fragment algorytmu
zmienia si w zalenoci od typu pracownika.

[GOF].

190

ROZDZIA 12.

Te oczywiste powtrzenia mona wyeliminowa przez zastosowanie wzorca szablonu metody.


abstract public class VacationPolicy {
public void accrueVacation() {
calculateBaseVacationHours();
alterForLegalMinimums();
applyToPayroll();
}

private void calculateBaseVacationHours() { /* ... */ };


abstract protected void alterForLegalMinimums();
private void applyToPayroll() { /* ... */ };

public class USVacationPolicy extends VacationPolicy {


@Override protected void alterForLegalMinimums() {

// Kod specyficzny dla USA.


}

public class EUVacationPolicy extends VacationPolicy {


@Override protected void alterForLegalMinimums() {

// Kod specyficzny dla UE.


}

Dziedziczenie wypenia dziur w algorytmie accrueVacation, dostarczajc jedyny fragment


informacji, ktry nie jest powtarzany.

Wyrazisto kodu
Wikszo z nas ma dowiadczenie w pracy z zawiym kodem. Wikszo z nas napisaa nieco takiego kodu. atwo napisa kod, ktry sami moemy zrozumie, poniewa w czasie pisania dokadnie rozumiemy problem, ktry prbujemy rozwiza. Inne osoby, ktre konserwuj kod, nie musz
mie tak gbokiego rozeznania.
Wikszo kosztu oprogramowania przypada na jego dugoterminowe utrzymanie. Aby zminimalizowa potencjalne defekty, jakie s wprowadzane przez zmiany, niezbdne jest zrozumienie tego,
co realizuje system. Wraz ze wzrostem stopnia skomplikowania systemu coraz wicej czasu zajmuje programistom zrozumienie jego dziaania, a dodatkowo istnieje stale zwikszajce si niebezpieczestwo niewaciwego zrozumienia. Dlatego kod powinien jasno wyraa intencje autora. Im
janiejszy jest kod napisany przez autora, tym mniej czasu inne osoby bd potrzeboway na jego
zrozumienie. Pozwala to na ograniczenie defektw i zmniejszenie kosztu utrzymania kodu.
Co zatem mona zrobi, by kod by czytelny?
Mona wybiera sugestywne nazwy. Chcemy mc usysze nazw klasy lub funkcji i nie by zaskoczonym, gdy odkryjemy ich odpowiedzialnoci.
Mona rwnie tworzy niewielkie funkcje i klasy. Mae klasy i funkcje s zwykle atwiejsze do nazwania, napisania i zrozumienia.
Mona rwnie korzysta ze standardowej nomenklatury. Wzorce projektowe dla przykadu s w wikszoci przeznaczone do poprawiania komunikacji i wyrazistoci kodu. Przez stosowanie standardowych nazw wzorcw, takich jak Command lub Visitor w nazwach klas implementujcych te wzorce,
moemy jasno opisywa nasz projekt dla innych programistw.
POWSTAWANIE PROJEKTU

191

Dobrze napisane testy jednostkowe s rwnie ekspresyjne. Podstawowym celem testw jest zapewnienie dokumentacji przez przykad. Ten, kto czyta nasze testy, powinien mc szybko zrozumie przeznaczenie klasy.
Jednak najwaniejszym sposobem na osignicie wyrazistoci kodu jest prbowanie. Zbyt czsto
zajmujemy si wycznie tym, aby kod dziaa prawidowo, i przechodzimy do nastpnego problemu, nie zaprztajc sobie gowy kwesti czytelnoci kodu. Naley pamita, e najprawdopodobniej t nastpn osob bdziemy my sami.
Dlatego warto by dumnym ze swojej pracy. Warto spdzi nieco czasu nad kad funkcj i klas
wybra lepsze nazwy, podzieli du funkcj na mniejsze i oglnie przywizywa wiksz wag do
tego, co tworzymy. Uwaga jest cennym zasobem.

Minimalne klasy i metody


Nawet tak podstawowe koncepcje, jak eliminacja powtrze, wyrazisto kodu czy te SRP, mog
by pojmowane zbyt dosownie. W procesie tworzenia maych klas i metod moemy utworzy zbyt
wiele niewielkich klas i metod. Dlatego zasada ta sugeruje, aby rwnoczenie zachowywa niewielk liczb funkcji i klas.
Dua liczba klas i metod jest czasami wynikiem bezcelowego dogmatyzmu. Wemy jako przykad
standard kodowania, ktry zakada tworzenie interfejsu dla kadej klasy, lub programist, ktry
uwaa, e pola i operacje musz by zawsze rozdzielone na klasy danych i klasy operacji. Nie tdy
droga.
Naszym celem jest zachowanie moliwie maego systemu, przy jednoczesnym ograniczaniu liczby
funkcji i klas. Naley jednak pamita, e zasada ta ma najniszy priorytet z czterech zasad prostego projektu. Tak wic cho utrzymanie niskiej liczby klas i funkcji jest wane, waniejsze jest posiadanie testw, eliminowanie powtrze i wyrazisto kodu.

Zakoczenie
Czy istnieje zbir prostych zasad, ktre mog zastpi dowiadczenie? Oczywicie nie. Z drugiej
strony, praktyki opisane w tym rozdziale i caej ksice s wykrystalizowan postaci wielu dekad
dowiadczenia zdobywanego przez autorw. Korzystanie z praktyki prostego projektu moe i powinno zachca i skania programistw do stosowania dobrych praktyk oraz wzorcw, ktrych
w innym przypadku uczyliby si latami.

Bibliografia
[XPE]: Kent Beck, Extreme Programming Explained: Embrace Change, Addison-Wesley 1999.
[GOF]: Gamma i inni, Elements of Reusable Object Oriented Software, Addison-Wesley 1996.

192

ROZDZIA 12.

ROZDZIA 13.

Wspbieno
Brett L. Schuchert

Obiekty s abstrakcj przetwarzania.


Wtki s abstrakcj harmonogramw.
James O. Coplien1

Z korespondencji prywatnej.

193

bardzo trudne. Znacznie atwiej pisa


P kod, ktry wykonuje si w jednym wtku. Rwnietrudne
atwo mona pisa kod wielowtkowy, ktry
ISANIE CZYSTYCH PROGRAMW WSPBIENYCH JEST

wyglda wietnie na pierwszy rzut oka, ale zawiera bdy na niszych poziomach. Taki kod dziaa
prawidowo do momentu, gdy system zostanie poddany wikszemu obcieniu.
W tym rozdziale przedstawimy potrzeb programowania wspbienego oraz zwizane z tym
trudnoci. Przedstawimy kilka zalece pozwalajcych poradzi sobie z tymi trudnociami oraz pisa czysty kod wspbieny. Na koniec zajmiemy si problemami zwizanymi z testowaniem kodu
wspbienego.
Wspbieno kodu jest zoonym zagadnieniem, ktre moe by tematem osobnej ksiki.
W tej ksice przedstawimy go w dwch etapach: w niniejszym rozdziale zawarte jest wprowadzenie do zagadnienia, a bardziej szczegowy samouczek zamiecilimy w dodatku A, Wspbieno
II. Jeeli Czytelnik jest po prostu ciekaw zagadnie zwizanych ze wspbienoci, to ten rozdzia
jest wystarczajcy. Jeeli konieczne jest dogbne zrozumienie wspbienoci, lektura samouczka jest
niezbdna.

W jakim celu stosowa wspbieno?


Wspbieno jest strategi rozdzielania. Pomaga nam oddzieli to, co jest wykonywane, od tego,
kiedy jest wykonywane. W aplikacji jednowtkowej co i kiedy s tak cile powizane, e stan caej
aplikacji moe by czsto okrelony wycznie na podstawie zapisu stosu. Programista debugujcy
taki system moe ustawi punkty zatrzymania lub ich sekwencj i bdzie wiedzia, jaki jest stan
systemu w czasie, gdy zostanie zatrzymany.
Rozczenie tego, co jest wykonywane, od tego, kiedy jest wykonywane, moe znakomicie poprawi
zarwno wydajno, jak i struktur aplikacji. Ze strukturalnego punktu widzenia aplikacja jest
podobna do wielu wsppracujcych ze sob komputerw zamiast jednej wielkiej ptli gwnej. Pozwala
to atwiej zrozumie system i oferuje wiele wydajnych sposobw na rozdzielenie problemw.
Jako przykad wemy standardowy model aplikacji WWW z uyciem serwletw. Systemy te dziaaj pod parasolem kontenera WWW lub EJB, ktry czciowo zarzdza za nas wspbienoci.
Serwlety s wykonywane asynchronicznie w momencie przyjcia dania WWW. Programista
serwletu nie musi zarzdza wszystkimi przychodzcymi daniami. W zasadzie kady serwlet wykonuje si w osobnym maym wiecie i jest odczony od dziaania innych serwletw.
Oczywicie, jeeli byoby to proste, rozdzia ten byby niepotrzebny. W rzeczywistoci oddzielenie
zapewniane przez kontenery WWW jest dalekie od doskonaoci. Programici serwletw musz
by uwani i ostroni przy programowaniu, aby ich wspbiene programy byy prawidowe.
Pomimo tego strukturalne zalety modelu serwletw s znaczne.
Jednak struktura nie jest jedynym motywem wykorzystywania wspbienoci. Niektre systemy
maj narzucone ograniczenia na czas odpowiedzi i przepustowo, ktre wymagaj rcznego kodowania rozwiza wspbienych. Dla przykadu wemy jednowtkowy agregator informacji,
ktry zbiera informacje z wielu rnych witryn WWW i czy te informacje w dzienne podsumowanie.
194

ROZDZIA 13.

Poniewa system jest jednowtkowy, odpytuje kolejne witryny, koczc przeszukiwanie jednej
przed rozpoczciem nastpnej. Jedno uruchomienie musi si zakoczy w czasie krtszym ni 24
godziny. Jednak gdy dodawanych jest coraz wicej witryn WWW, czas ten ronie, i w kocu zebranie wszystkich danych zajmuje ponad 24 godziny. Aplikacja jednowtkowa potrzebuje bardzo duo
czasu na oczekiwanie zakoczenia operacji wejcia-wyjcia przez gniazdo WWW. Moemy poprawi wydajno takiej aplikacji przez uycie algorytmu wielowtkowego, ktry jednoczenie przeglda wicej ni jedn witryn WWW.
Innym przykadem zastosowania wspbienoci moe by system obsugujcy jednego uytkownika jednoczenie, wymagajcy tylko sekundy na obsuenie uytkownika. Taki system dosy szybko
reaguje w przypadku wielu uytkownikw, ale wraz ze wzrostem ich liczby czas odpowiedzi systemu
zwiksza si. aden uytkownik nie ma ochoty czeka w kolejce za 150 innymi! Moemy poprawi
czas odpowiedzi takiego systemu przez obsug wielu uytkownikw jednoczenie.
Wspbieno moe rwnie znale zastosowanie w systemie interpretujcym due zbiory danych, ktry moe da pene rozwizanie po przetworzeniu wszystkich zbiorw. Prawdopodobnie
kady zbir danych jest przetwarzany przez inny komputer, wic wiele zbiorw danych jest przetwarzanych rwnolegle.

Mity i nieporozumienia
Mamy wic kuszce powody stosowania wspbienoci. Jednak jak wczeniej wspomnielimy,
wspbieno jest trudna. Jeeli nie bdziemy ostroni, moemy doprowadzi do wielu nieprzyjemnych sytuacji. Przeanalizujmy ponisze czsto spotykane mity i nieporozumienia:

Wspbieno zawsze poprawia wydajno.


Wspbieno moe czasami poprawi wydajno aplikacji, ale tylko w przypadkach, gdy program
traci duo czasu na oczekiwaniu na wykonanie pewnych procesw i gdy ten czas mona podzieli pomidzy wiele wtkw lub wiele procesorw.

Projekt nie zmienia si, jeeli piszemy programy wspbiene.


W rzeczywistoci projekt algorytmu wspbienego moe by cakowicie inny ni projekt systemu
jednowtkowego. Rozdzielenie co od kiedy zwykle ma ogromny wpyw na struktur systemu.

Zrozumienie problemw ze wspbienoci nie jest wane w przypadku korzystania z takiego


kontenera jak WWW lub EJB.
W rzeczywistoci powinnimy doskonale wiedzie, co robi nasz kontener i jak chroni si
przed problemami wspbienej modyfikacji oraz zakleszcze dziaania kodu, ktre zostan
przedstawione w dalszej czci rozdziau.

Dalej mamy kilka bardziej wywaonych stwierdze zwizanych z pisaniem programw wspbienych:

Wspbieno wymaga pewnych narzutw, zarwno na wydajno, jak i na pisanie dodatkowego kodu.

Prawidowa obsuga wspbienoci jest zoona, nawet w przypadku prostych problemw.


WSPBIENO

195

Bdy wspbienoci czsto nie s powtarzalne, wic zwykle s ignorowane jako jednorazowe2,
cho w rzeczywistoci s prawdziwymi defektami.

Wspbieno czsto wymaga podstawowych zmian w strategii projektu.

Wyzwania
Co powoduje, e programowanie wspbiene jest tak trudne? Wemy jako przykad tak prost
klas:
public class X {
private int lastIdUsed;
public int getNextId() {
return ++lastIdUsed;
}
}

Zamy, e tworzymy obiekt X, ustawiamy pole lastIdUsed na 42, a nastpnie wspdzielimy ten
obiekt midzy dwa wtki. Zamy teraz, e oba te wtki wywouj metod getNextId(); istniej
trzy moliwe wyniki:

wtek I otrzymuje warto 43, wtek II otrzymuje warto 44, lastUsedId jest rwne 44;

wtek I otrzymuje warto 44, wtek II otrzymuje warto 43, lastUsedId jest rwne 44;

wtek I otrzymuje warto 43, wtek II otrzymuje warto 43, lastUsedId jest rwne 43.

Zaskakujcy trzeci wynik3 wystpuje, gdy dwa wtki napotkaj si. Dzieje si to, poniewa istnieje
wiele moliwych cieek, ktre mog by wybrane przez te dwa wtki w czasie wykonywania jednego wiersza kodu Java, a cz z tych cieek generuje niewaciwe wyniki. Ile jest takich rnych cieek? Aby rzetelnie odpowiedzie na to pytanie, musimy zrozumie, co robi kompilator
JIT z wygenerowanym kodem i co w modelu pamici Java jest uznawane za atomowe.
Dla tych dwch wtkw wykonujcych metod getNextId na podstawie wanie wygenerowanego
kodu bajtowego wystpuje 12 870 rnych moliwych cieek wykonania 4. Jeeli zmienimy typ
zmiennej lastIdUsed z int na long, liczba moliwych cieek zwikszy si do 2 704 156. Oczywicie,
wikszo cieek wygeneruje prawidowe wyniki. Problemem jest jednak, e niektre nie dadz
prawidowych wynikw.

Zasady obrony wspbienoci


Przedstawimy teraz seri zasad i technik obrony naszych systemw przed problemami ze wspbienym kodem.
2

Promienie kosmiczne, szumy i tak dalej.

Patrz Kopiemy gbiej w dodatku A.

Patrz Moliwe cieki wykonania w dodatku A.

196

ROZDZIA 13.

Zasada pojedynczej odpowiedzialnoci


Zasada SRP5 mwi, e dana metoda (klasa, komponent) powinna mie jeden powd do zmiany.
Projekt wspbienoci jest na tyle zoony, aby by powodem do zmiany, i dlatego warto go oddzieli
od reszty kodu. Niestety, zbyt czsto implementacja szczegw wspbienoci jest wbudowywana
bezporednio w pozostay kod produkcyjny. Warto wzi pod uwag nastpujce zagadnienia:

Kod zwizany ze wspbienoci ma wasny cykl produkcji, zmian i dostrajania.

Kod zwizany ze wspbienoci ma wasne wyzwania, ktre rni si od tych z kodu niewspbienego i czsto s trudniejsze.

Liczba sposobw, na ktre le napisany kod wspbieny moe zawie, powoduje, e jest on
wystarczajcym wyzwaniem, nawet bez otaczajcego go kodu aplikacji.

Zalecenie: Kod zwizany ze wspbienoci powinnimy oddzieli od pozostaego kodu6.

Wniosek ograniczenie zakresu danych


Jak widzielimy, dwa wtki modyfikujce to samo pole wspuytkowanego obiektu mog wpywa
na siebie, powodujc nieoczekiwane dziaanie. Jednym z rozwiza jest zastosowanie sowa kluczowego synchronized w celu ochrony sekcji krytycznej kodu wykorzystujcego wspuytkowane
obiekty. Wane jest, aby ogranicza liczb takich sekcji krytycznych. Im wicej mamy miejsc,
w ktrych modyfikowane s wsplne dane, tym wiksze prawdopodobiestwo, e:

zapomnimy ochroni jedno lub wicej takich miejsc w efekcie powodujc uszkodzenie caego
kodu modyfikujcego te wsplne dane;

wystpi powtrzenie zada wymaganych do upewnienia si, e wszystko jest efektywnie chronione
(naruszenie zasady DRY7);

trudno bdzie okreli rdo awarii, ktre i tak jest wystarczajco trudne do znalezienia.

Zalecenie: Naley wzi sobie do serca zagadnienie hermetyzacji danych i w znacznym stopniu
ograniczy dostp do danych, ktre mog by wspuytkowane.

Wniosek korzystanie z kopii danych


Dobrym sposobem na eliminowanie wspuytkowanych danych jest... unikanie ich wspuytkowania. W niektrych przypadkach moliwe jest kopiowanie obiektw i traktowanie ich jako tylko
do odczytu. W innych przypadkach moe by moliwe kopiowanie obiektw, zbieranie wynikw
z wielu wtkw w tych kopiach, a nastpnie czenie wynikw w jednym wtku.

[PPP].

Patrz Przykad klient-serwer w dodatku A.

[PRAG].

WSPBIENO

197

Jeeli istnieje prosty sposb na uniknicie wspuytkowania obiektw, wynikowy kod bdzie
ze znacznie mniejszym prawdopodobiestwem powodowa bdy. Mona si jednak obawia o koszt
tworzenia dodatkowych obiektw. Warto poeksperymentowa w celu okrelenia, czy faktycznie
jest to problemem. Jednak jeeli wykorzystanie kopii obiektw pozwala na uniknicie synchronizacji,
oszczdnoci na unikniciu koniecznej blokady zwykle bd wiksze ni narzut wymagany na dodatkowe tworzenie i usuwanie obiektu.

Wniosek wtki powinny by na tyle niezalene,


na ile to tylko moliwe
Warto rozway pisanie kodu wtkw w taki sposb, aby kady wtek dziaa we wasnym wiecie,
nie wspuytkujc danych z pozostaymi wtkami. Kady wtek przetwarza jedno danie
klienta, a wszystkie wymagane dane pochodz z niewspuytkowanego rda i s przechowywane jako zmienne lokalne. Powoduje to, e wtki te dziaaj tak, jakby byy jedynym wtkiem
na wiecie, i dlatego nie potrzebuj synchronizacji.
Na przykad klasy dziedziczce po HttpSevlet otrzymuj wszystkie dane jako parametry przekazane do metod doGet oraz doPost. Powoduje to, e kady serwlet dziaa jako osobna maszyna.
Dopki kod w serwlecie korzysta wycznie ze zmiennych lokalnych, nie ma moliwoci, aby wystpiy bdy synchronizacji. Oczywicie, wikszo aplikacji korzystajcych z serwletw i tak korzysta ze wspuytkowanych zasobw, na przykad pocze z baz danych.
Zalecenie: Warto sprbowa podzieli dane na niezalene podzbiory, na ktrych mog dziaa niezalene wtki, by moe dziaajce na rnych procesorach.

Poznaj uywan bibliotek


W stosunku do poprzedniej wersji jzyk Java 5 oferuje wiele usprawnie programowania wspbienego. Piszc kod korzystajcy z wtkw za pomoc jzyka Java 5, naley rozway nastpujce zagadnienia:

Zastosowanie dostarczanych kolekcji bezpiecznych dla wtkw.

Uycie wykonawcw do uruchamiania niezwizanych ze sob zada.

Stosowanie rozwiza nieblokujcych wszdzie tam, gdzie jest to moliwe.

Wykorzystywanie kilku klas z biblioteki nie jest bezpieczne dla wtkw.

Kolekcje bezpieczne dla wtkw


Gdy Java bya modym jzykiem, Doug Lea napisa doskona ksik8 Concurrent Programming in Java.
W ksice tej przedstawi proces tworzenia kilku kolekcji bezpiecznych dla wtkw, ktre pniej
stay si czci JDK i zostay umieszczone w pakiecie java.util.concurrent. Kolekcje z tego
pakietu s bezpieczne w przypadkach operacji wielowtkowych i dziaaj odpowiednio szybko.
8

[Lea99].

198

ROZDZIA 13.

Faktycznie implementacja ConcurrentHashMap w niemal kadej sytuacji dziaa szybciej ni HashMap.


Pozwala ona na jednoczesny zapis i odczyt oraz udostpnia metody wspierajce czsto uywane
operacje zoone, ktre w pozostaych przypadkach nie s bezpieczne dla wtkw. Jeeli rodowiskiem instalacyjnym jest Java 5, naley korzysta z ConcurrentHashMap.
Dostpnych jest rwnie kilka innych klas pozwalajcych na obsug zaawansowanych projektw
wielowtkowych. Kilka przykadw przedstawiono poniej.
ReentrantLock

Blokada, ktra moe by naoona w jednej metodzie i zwolniona w innej.

Semaphore

Implementacja klasycznego semafora, czyli blokady z licznikiem.

CountDownLatch

Blokada, ktra oczekuje na okrelon liczb zdarze przed ponownym uruchomieniem


wszystkich wtkw oczekujcych na niej. Pozwala ona, aby wszystkie wtki miay moliwo
wystartowania w mniej wicej tym samym momencie.

Zalecenie: Warto zapozna si z dostpnymi klasami. W przypadku jzyka Java naley zapozna si
z java.util.concurrent, java.util.concurrent.atomic, java.util.concurrent.locks.

Poznaj modele wykonania


Istnieje kilka rnych sposobw na partycjonowanie dziaania w aplikacjach wspbienych. Przed
ich przedstawieniem musimy zapozna si z kilkoma podstawowymi definicjami.
Zasoby zwizane

Zasoby staej wielkoci i liczby uywane w rodowisku wspbienym. Przykadem mog by


poczenia z baz danych oraz bufory odczytu i zapisu o staej wielkoci.

Wzajemne wykluczanie

Tylko jeden wtek moe odwoywa si do danych wspuytkowanych w danym momencie.

Zagodzenie

Jeden wtek z grupy wtkw nie moe dziaa przez zbyt dugi czas lub cigle. Na przykad
dopuszczenie, aby szybko dziaajce wtki zawsze pierwsze przechodziy przez blokad,
powoduje zagodzenie wolniej dziaajcych wtkw, jeeli nie ma koca strumienia szybko
dziaajcych wtkw.

Zakleszczenie

Co najmniej dwa wtki czekaj na zakoczenie innego z oczekujcych wtkw. Kady wtek
posiada zasb, ktry jest wymagany przez inny wtek, i aden nie moe si zakoczy, dopki
nie uzyska innego zasobu.

Uwizienie

Wtki s wstrzymane, ale kady z nich prbuje znale inn drog. Z powodu rezonansu wtki
prbujce i dalej nie s w stanie zrobi tego przez nadmiernie dugi czas lub te nigdy.

Majc te definicje, moemy przedstawi rne modele wykonania kodu uywane w programowaniu wspbienym.

Producent-konsument9
Co najmniej jeden wtek producent wykonuje pewne zadania i umieszcza wyniki w buforze lub
kolejce. Co najmniej jeden wtek konsument odbiera te wyniki i wykonuje na nich operacje. Kolejka
pomidzy producentami i konsumentami jest zasobem zwizanym. Oznacza to, e producenci musz

http://pl.wikipedia.org/wiki/Problem_producenta_i_konsumenta

WSPBIENO

199

czeka na wolne miejsce w kolejce przed zapisem, a konsumenci musz czeka, a w kolejce bdzie
co do skonsumowania. Koordynacja pomidzy producentami i konsumentami poprzez kolejk
wymaga zastosowania sygnalizacji przez producenta i konsumenta. Producent zapisuje do kolejki
i sygnalizuje, e kolejka nie jest ju pusta. Konsumenci odczytuj z kolejki i sygnalizuj, e kolejka
nie jest ju pena. Obie strony oczekuj na informacj, e mog kontynuowa prac.

Czytelnik-pisarz10
Gdy mamy wspuytkowany zasb, ktry przede wszystkim suy jako rdo informacji dla czytelnikw, ale jest okazjonalnie aktualizowany przez pisarzy, problemem jest przepustowo. Pooenie nacisku na przepustowo moe powodowa zagodzenie i akumulacj oczekujcych informacji.
Umoliwienie aktualizacji moe zmniejsza przepustowo. Koordynacja czytelnikw, aby nie czytali
czego, co jest aktualizowane przez pisarza, i odwrotnie, jest zadaniem trudnym do wywaenia.
Pisarze zwykle blokuj wielu czytelnikw przez dugi czas, powodujc problemy z przepustowoci.
Wyzwaniem jest zbilansowanie potrzeb zarwno czytelnikw, jak i pisarzy, aby zapewni prawidowe dziaanie, rozsdn przepustowo i unikn zagodzenia. Prosta strategia wymaga, aby pisarze przed wykonaniem aktualizacji czekali, dopki nie bdzie adnego czytelnika. Jeeli bdzie wystpowa stay strumie czytelnikw, pisarz zostanie zagodzony. Z drugiej strony, jeeli pisarze
bd dziaali czsto i bd mieli duy priorytet, spadnie przepustowo. Rozwizanie tego problemu
polega na znalezieniu rwnowagi i unikniciu problemw ze wspbien aktualizacj.

Ucztujcy filozofowie11
Wyobramy sobie kilku filozofw siedzcych wok okrgego stou. Po lewej stronie kadego filozofa znajduje si widelec. Na rodku stou znajduje si dua misa spaghetti. Filozofowie spdzaj
swj czas na myleniu, o ile nie s godni. Gdy s godni, chwytaj za widelce lece po obu stronach i zaczynaj je. Filozofowie nie mog je, jeeli nie trzymaj dwch widelcw. Jeeli filozof
siedzcy po prawej stronie uywa jednego z potrzebnych widelcw, godny filozof musi poczeka,
dopki ten po prawej nie skoczy je i nie odoy widelcw. Gdy filozof jest najedzony, odkada
swoje widelce na st i czeka, a znw bdzie godny.
Gdy zamienimy filozofw na wtki i widelce na zasoby, problem stanie si podobny do wielu aplikacji korporacyjnych, w ktrych procesy rywalizuj o zasoby. Jeeli system taki nie bdzie odpowiednio zaprojektowany, tego rodzaju rywalizacja bdzie powodowaa zakleszczenia, uwizienia
oraz spadek przepustowoci i efektywnoci systemu.
Wikszo problemw wspbienych, z jakimi si spotykamy, jest odmian jednej z tych trzech sytuacji. Warto zapozna si z tymi algorytmami i napisa ich rozwizania, dziki czemu, gdy napotkamy na
konkretny problem wspbienoci, bdziemy lepiej przygotowani do jego rozwizania.
Zalecenie: Warto nauczy si podstawowych algorytmw i zrozumie ich rozwizania.
10

http://pl.wikipedia.org/wiki/Problem_czytelnik%C3%B3w_i_pisarzy

11

http://pl.wikipedia.org/wiki/Problem_ucztuj%C4%85cych_filozof%C3%B3w

200

ROZDZIA 13.

Uwaga na zalenoci
pomidzy synchronizowanymi metodami
Zalenoci pomidzy synchronizowanymi metodami powoduj subtelne bdy w kodzie wspbienym. Jzyk Java posiada notacj synchronized, ktra pozwala chroni poszczeglne metody.
Jeeli jednak mamy wicej metod synchronizowanych w tej samej klasie wspuytkowanej, to
system moe by napisany nieprawidowo12.
Zalecenie: Unikaj uywania wicej ni jednej metody w obiekcie wspuytkowanym.
Zdarzaj si jednak przypadki, gdy konieczne jest uycie wicej ni jednej metody we wspuytkowanym obiekcie. W takim przypadku istniej trzy sposoby na zapewnienie prawidowego dziaania kodu.

Blokowanie po stronie klienta klient blokuje serwer przed wywoaniem pierwszej metody
i blokada ta jest utrzymywana przez czas wykonywania kodu ostatniej metody.

Blokowanie po stronie serwera wewntrz serwera tworzona jest metoda, ktra blokuje
serwer, wywouje wszystkie potrzebne metody, a nastpnie zdejmuje blokad. Klient musi wywoa now metod.

Serwer adaptujcy tworzony jest porednik zapewniajcy blokowanie. Jest to przykad blokowania po stronie serwera, w ktrym oryginalny serwer nie moe by zmieniany.

Tworzenie maych sekcji synchronizowanych


Sowo kluczowe synchronized wprowadza blokad. Wszystkie sekcje kodu chronione przez
t sam blokad maj zagwarantowane, e w danym momencie tylko jeden wtek bdzie wykonywa chronion sekcj. Blokady s kosztowne, poniewa powoduj opnienia i dodaj narzut.
Dlatego nie naley naduywa instrukcji synchronized. Z drugiej strony, sekcje krytyczne13 musz
by chronione. Dlatego powinnimy projektowa nasz kod z minimaln moliw liczb sekcji
krytycznych.
Niektrzy naiwni programici prbuj to osign, tworzc bardzo due sekcje krytyczne. Jednak
rozciganie synchronizacji poza minimaln sekcj krytyczn zwiksza rywalizacj o zasoby i obnia
wydajno systemu14.
Zalecenie: Sekcje synchronizowane powinny by jak najmniejsze.

12

Patrz Zalenoci pomidzy metodami mog uszkodzi kod wspbieny w dodatku A.

13

Sekcja krytyczna jest dowoln sekcj kodu, ktra musi by chroniona przed jednoczesnym uyciem, aby dziaanie programu byo prawidowe.

14

Patrz Zwikszanie przepustowoci w dodatku A.

WSPBIENO

201

Pisanie prawidowego kodu wyczajcego jest trudne


Konstruowanie systemw, ktre maj dziaa w nieskoczono, rni si od tworzenia czego,
co dziaa przez chwil, a nastpnie elegancko si wycza.
Eleganckie wyczenie moe by trudne do osignicia. Czsto spotykanymi problemami s zakleszczenia15 wtkw oczekujcych na sygna kontynuowania, ktry nigdy nie nadejdzie.
Wyobramy sobie system z wtkiem gwnym, ktry generuje kilka wtkw potomnych i czeka na
ich zakoczenie, po czym zwalnia zasoby i koczy prac. Co si stanie, gdy wtek potomny ulegnie
zakleszczeniu? Wtek gwny bdzie oczekiwa bez przerwy i system nigdy nie zostanie wyczony.
Wemy te pod uwag podobny system, ktry zosta poinstruowany o koniecznoci zakoczenia
pracy. Wtek gwny powiadamia wszystkie wtki potomne, aby przerway swoje zadania i zakoczyy si. Co si stanie, jeeli dwa z tych wtkw potomnych dziaaj jako para producent-konsument?
Zamy, e producent otrzyma sygna z wtku gwnego i szybko zakoczy prac. Konsument
moe oczekiwa na komunikat od producenta i by zablokowany w stanie, w ktrym nie moe
odebra sygnau wyczenia. Wtek utknie, czekajc na producenta, i nie bdzie mg zakoczy
pracy, co uniemoliwi rwnie zakoczenie pracy wtku gwnego.
Tego typu sytuacje nie s niczym szczeglnym. Jeeli wic musimy napisa kod wspbieny, ktry
wymaga eleganckiego zakoczenia, naley oczekiwa, e spdzimy sporo czasu na prawidowym
zrealizowaniu takiego zakoczenia.
Zalecenie: Naley stosowa wczesne wyczanie i wczesne uruchamianie. Zwykle zajmuje to wicej
czasu, ni oczekujemy. Warto przejrze istniejce algorytmy, poniewa prawdopodobnie jest to trudniejsze, ni mylisz.

Testowanie kodu wtkw


Udowadnianie, e kod jest prawidowy, jest niepraktyczne. Testowanie nie gwarantuje poprawnoci.
Jednak dobre testy pozwalaj zminimalizowa ryzyko. To wszystko jest prawd w przypadku rozwiza jednowtkowych. Gdy tylko istniej co najmniej dwa wtki korzystajce z tego samego kodu
i pracujce na wspuytkowanych danych, wszystko staje si znacznie bardziej skomplikowane.
Zalecenie: Warto pisa testy, ktre maj moliwo ujawnienia problemw, a nastpnie naley je
czsto uruchamia, korzystajc z rnych konfiguracji programowych i systemowych oraz rnego
obcienia. Jeeli w jakichkolwiek okolicznociach test zawiedzie, naley zlokalizowa bd. Nie naley
ignorowa awarii tylko dlatego, e w nastpnym uruchomieniu test wykona si prawidowo.

15

Patrz Zakleszczenia w dodatku A.

202

ROZDZIA 13.

Naley tu wzi pod uwag wiele elementw. Poniej zamieszczone s bardziej precyzyjne zalecenia.

Traktujemy przypadkowe awarie jako potencjalne problemy z wielowtkowoci.

Na pocztku uruchamiamy kod niekorzystajcy z wtkw.

Nasz kod wtkw powinien da si wcza.

Nasz kod wtkw powinien da si dostraja.

Uruchamiamy wicej wtkw, ni mamy do dyspozycji procesorw.

Uruchamiamy testy na rnych platformach.

Uzbrajamy nasz kod w elementy prbujce wywoa awarie i wymuszajce awarie.

Traktujemy przypadkowe awarie


jako potencjalne problemy z wielowtkowoci
Kod wielowtkowy powoduje, e awarii ulegaj elementy, ktre po prostu nie mog zawie.
Wikszo programistw nie ma intuicyjnego poczucia, w jaki sposb wtki wspdziaaj z pozosta czci kodu (w tym autorzy). Bdy w kodzie wtkw mog si ujawnia raz na tysic lub
milion wykona. Prby ich powtrzenia mog by frustrujce. Czsto prowadzi to do tego, e programici kwalifikuj bd jako efekt promieniowania kosmicznego, wybryku sprztu lub innego
rodzaju sytuacji jednorazowej. Najlepiej zaoy, e sytuacje jednorazowe nie istniej. Im duej
s one ignorowane, tym wicej kodu korzysta z potencjalnie nieprawidowego podejcia.
Zalecenie: Nie naley ignorowa awarii systemu, kwalifikujc je jako jednorazowe.

Na pocztku uruchamiamy kod niekorzystajcy z wtkw


Moe to si wydawa oczywiste, ale warto to powtrzy. Naley upewni si, e kod dziaa poza
rodowiskiem wielowtkowym. Zwykle oznacza to, e tworzymy obiekty POJO, ktre bd wywoywane przez nasze wtki. Obiekty POJO nie maj informacji o wtkach i dziki temu mog by
testowane poza rodowiskiem wielowtkowym. Im wicej elementw systemu jest umieszczonych
w tych obiektach, tym lepiej.
Zalecenie: Nie warto w tym samym czasie szuka bdw w kodzie jednowtkowym i wielowtkowym.
Naley sprawdzi, czy kod dziaa poza wtkiem.

Nasz kod wtkw powinien da si wcza


Warto pisa kod wspierajcy wspbieno w taki sposb, e moe by uruchomiony w kilku
konfiguracjach:

Jeden wtek, kilka wtkw, rna liczba w zalenoci od sposobu wykonania.

Kod wtkw wspdziaajcy zarwno z rzeczywistym, jak i testowym rodowiskiem.

WSPBIENO

203

Wykonanie w rodowisku testowym dziaajcym szybko, powoli lub ze zmienn szybkoci.

Konfigurowanie testw w taki sposb, aby mona byo okrela liczb iteracji.

Zalecenie: Warto tak napisa kod wtkw, aby by dowolnie doczany, by mona byo go uruchamia w rnych konfiguracjach.

Nasz kod wtkw powinien da si dostraja


Uzyskanie waciwej rwnowagi wtkw zwykle wymaga wielu prb. Jak najwczeniej naley znale sposoby na sterowanie wydajnoci systemu w rnych konfiguracjach. Takim sposobem moe by atwa zmiana liczby wtkw. Warto rozway moliwo zmiany ich liczby w czasie dziaania systemu. Przydatny moe by rwnie mechanizm automatycznego dostrajania w zalenoci od
przepustowoci i obcienia systemu.

Uruchamiamy wicej wtkw,


ni mamy do dyspozycji procesorw
Gdy system przecza si pomidzy zadaniami, zdarzaj si rne sytuacje. Aby wymusi przeczanie zada, naley uruchomi wicej wtkw, ni mamy do dyspozycji procesorw lub rdzeni.
Im czciej s przeczane zadania, tym wiksze prawdopodobiestwo, e wykryjemy kod, w ktrym
brakuje sekcji krytycznej, lub kod powodujcy zakleszczenie.

Uruchamiamy testy na rnych platformach


W roku 2007 tworzylimy kurs programowania wspbienego. Sam kurs zakada wykorzystanie
przede wszystkim systemu OS X. W czasie lekcji korzystalimy te z Windows XP dziaajcego na
maszynie wirtualnej. Testy pisane w celu pokazania sytuacji bdnych nie byy tak czsto wykonywane nieprawidowo w rodowisku XP, jak w rodowisku OS X.
We wszystkich przypadkach wiedzielimy, e testowany kod by nieprawidowy. Ilustruje to fakt, e
rne systemy operacyjne maj rne zasady wielowtkowoci, ktre wpywaj na wykonywanie
kodu. Kod wielowtkowy dziaa inaczej w rnych rodowiskach16. Powinnimy wykonywa testy
w kadym potencjalnym rodowisku instalacyjnym.
Zalecenie: Kod wielowtkowy powinnimy uruchamia na wszystkich docelowych platformach
i to czsto.

16

Czy wiesz, e model wielowtkowoci w jzyku Java nie gwarantuje wielowtkowoci z wywaszczaniem? Nowoczesne
systemy operacyjne obsuguj wielowtkowo z wywaszczaniem, wic mamy j za darmo. Pomimo tego nie jest ona
gwarantowana przez JVM.

204

ROZDZIA 13.

Uzbrajamy nasz kod w elementy prbujce wywoa awarie


i wymuszajce awarie
Nie jest niczym niezwykym fakt, e w kodzie wspbienym ukrywaj si bdy. Proste testy po
prostu nie s w stanie ich ujawni. Czsto ukrywaj si w czasie normalnego przetwarzania. Mog
si one ujawnia jednokrotnie co kilka godzin, dni lub tygodni!
Powodem tego, e bdy wspbienoci mog by rzadkie, sporadyczne i trudne do powtrzenia,
jest to, e tylko kilka cieek wykonania z tysicy moliwych cieek przez wraliw sekcj moe
zawie. Dlatego prawdopodobiestwo wyboru niewaciwej cieki moe by bardzo niskie.
Powoduje to, e wykrywanie bdw i debugowanie jest bardzo trudne.
Jak mona zwikszy szanse przechwycenia takich rzadkich przypadkw? Mona uzbroi nasz kod
i wymusi dziaanie w rnej kolejnoci przez dodanie wywoa takich metod, jak Object.wait(),
Object.sleep(), Object.yield() oraz Object.priority().
Kada z tych metod moe wpywa na kolejno wykonania, zwikszajc moliwo wykrycia bdu.
Bdzie lepiej, gdy nieprawidowy kod zawiedzie wczeniej i moliwie czsto.
Istniej dwie moliwoci instrumentacji kodu:

rczna,

automatyczna.

Instrumentacja rczna
Do naszego kodu moemy rcznie wstawia wywoania wait(), sleep(), yield() oraz priority().
Moe to by waciwe dziaanie, gdy testujemy szczeglnie oporny fragment kodu.
Poniej mamy przykad takiego dziaania:
public synchronized String nextUrlOrNull() {
if(hasNext()) {
String url = urlGenerator.next();
Thread.yield(); // Wstawione do testowania.
updateHasNext();
return url;
}
return null;
}

Wstawione wywoanie yield() zmienia ciek wywoania kodu i by moe powoduje awari kodu w miejscu, w ktrym wczeniej wykonywa si prawidowo. Jeeli kod ulegnie awarii, nie jest to
spowodowane dodaniem wywoania yield()17. Kod ju wczeniej zawiera bd, a teraz po prostu go
ujawnilimy.
17

Jednak nie do koca. Poniewa JVM nie gwarantuje wielowtkowoci z wywaszczaniem, okrelony algorytm moe zawsze
dziaa w systemach niewywaszczajcych wtkw. Sytuacja odwrotna jest rwnie moliwa, ale z innych powodw.

WSPBIENO

205

Z podejciem tym wie si kilka problemw:

Konieczne jest rczne znalezienie odpowiednich miejsc.

Skd bdziemy wiedzieli, gdzie umieci wywoanie i jakiego rodzaju?

Pozostawienie takiego kodu w rodowisku produkcyjnym niepotrzebnie spowalnia kod.

Jest to metoda rutwki. Moemy znale bd lub nie. I tak nie mamy wielkich szans.

Potrzebujemy sposobu na korzystanie z tego podejcia w czasie testowania, a nie produkcji.


Potrzebujemy rwnie sposobu na modyfikowanie konfiguracji pomidzy uruchomieniami, co
zwiksza szanse znalezienia bdw.
Jasne jest, e gdy podzielimy nasz system na obiekty POJO, ktre nie maj informacji o wtkach,
i klasy sterujce wtkami, atwiej bdzie znale odpowiednie miejsca instrumentacji kodu. Co wicej,
moemy utworzy wiele rnych elementw testowych, ktre wywouj POJO w rnych reimach
wywoa sleep, yield i innych.

Instrumentacja automatyczna
Moemy skorzysta z narzdzi takich jak Aspect-Oriented Framework, CGLIB lub ASM do programowej instrumentacji naszego kodu. Moemy na przykad skorzysta z klasy z jedn metod:
public class ThreadJigglePoint {
public static void jiggle() {
}
}

Mona doda jej wywoania w rnych miejscach kodu:


public synchronized String nextUrlOrNull() {
if(hasNext()) {
ThreadJiglePoint.jiggle();
String url = urlGenerator.next();
ThreadJiglePoint.jiggle();
updateHasNext();
ThreadJiglePoint.jiggle();
return url;
}
return null;
}

Teraz mona uy prostego aspektu, ktry losowo wybiera jedn z moliwoci: wywoanie sleep,
yield lub niewywoywanie niczego.
Mona rwnie skorzysta z klasy ThreadJigglePoint majcej dwie implementacje. Pierwsza
implementacja jigle nic nie robi i jest uywana w produkcji. Druga generuje liczb losow wybierajc pomidzy sleep, yield lub niewywoywaniem niczego. Jeeli uruchomimy nasz test tysic
razy z losowym wytrzsaniem, moemy ujawni niektre bdy. Jeeli test zostanie wykonany prawidowo, bdziemy mogli powiedzie, e przynajmniej wykazalimy si naleyt starannoci. Cho to
rozwizanie jest dosy proste, moe by rozsdn opcj w porwnaniu z zastosowaniem bardziej skomplikowanych narzdzi.
206

ROZDZIA 13.

Dostpny jest program ConTest18 opracowany w IBM, ktry dziaa podobnie, ale jego uycie jest
znacznie bardziej skomplikowane.
Naszym celem jest wstrzsanie kodem tak, aby wtki wykonyway si za kadym razem w innej
kolejnoci. Kombinacja dobrze napisanych testw i wstrzsw moe zdecydowanie zwikszy
szanse znalezienia bdw.
Zalecenie: Warto korzysta ze strategii wstrzsania w celu upolowania bdw.

Zakoczenie
Trudno uzyska prawidowy kod wspbieny. Kod, ktry jest prosty do analizy, moe sta si
koszmarem, gdy wymieszamy go z wieloma wtkami i wspuytkowanymi danymi. Jeeli chcemy
zmierzy si z kodem wspbienym, musimy rygorystycznie przestrzega pisania czystego kodu,
poniewa w przeciwnym razie zderzymy si z subtelnymi i niezbyt czstymi awariami.
Po pierwsze i najwaniejsze, naley stosowa zasad pojedynczej odpowiedzialnoci. Naley podzieli nasz system na obiekty POJO, ktre oddzielaj kod obsugi wtkw od kodu nieobsugujcego
wtkw. Przy testowaniu kodu obsugi wtkw naley upewni si, e testujemy tylko ten problem.
Wynika z tego, e kod obsugi wtkw powinien by may i cile ukierunkowany.
Znane s moliwe rda problemw z wielowtkowoci: wiele wtkw dziaa na wsplnych danych lub korzysta ze wsplnej puli zasobw. Przypadki brzegowe, takie jak czyste wyczenie lub
zakoczenie iteracji ptli, mog by szczeglnie kopotliwe.
Warto zapozna si z bibliotek i podstawowymi algorytmami. Konieczne jest zrozumienie, w jaki
sposb funkcje oferowane przez bibliotek wspieraj rozwizywanie problemw podobnych do
podstawowych algorytmw.
Naley nauczy si rozpoznawania regionw kodu, ktre musz by odizolowane i zablokowane.
Nie naley blokowa regionw kodu, ktre nie musz by zablokowane. Naley unika wywoywania jednej blokowanej sekcji z innej. Wymaga to gbokiego zrozumienia tego, czy dany element
jest, czy nie jest wspuytkowany. Liczba wspuytkowanych obiektw i zakres wspuytkowania
powinny by moliwie mae. Projekt obiektw zawierajcych wspuytkowane dane powinien by
zmieniony tak, aby przyjmowa klienty, zamiast wymusza na nich zarzdzanie stanem wspuytkowanym.
Na pewno bd ujawniay si problemy. Te problemy, ktre nie zostan ujawnione odpowiednio
wczenie, czsto s interpretowane jako przypadki jednorazowe. Te tak zwane przypadki jednorazowe
zwykle ujawniaj si pod obcieniem systemu lub pozornie losowo. Z tego powodu musimy
zapewni, by nasz kod obsugi wtkw mona byo uruchamia w wielu konfiguracjach oraz na

18

http://www.alphaworks.ibm.com/tech/contest

WSPBIENO

207

wielu platformach w sposb cigy i powtarzalny. Moliwo testowania, ktra wynika naturalnie
z trzech praw TDD, zakada pewien poziom doczania i pozwala obsugiwa niezbdny kod
uruchomieniowy w szerokim zakresie konfiguracji.
Znacznie poprawimy nasze szanse znalezienia nieprawidowego kodu, jeeli powicimy troch
czasu na jego instrumentacj. Mona to zrobi rcznie lub skorzysta z technologii automatycznych. Warto w to zainwestowa moliwie wczenie. Nasz kod korzystajcy z wtkw powinien
przed przekazaniem go do produkcji dziaa tak dugo, jak tylko to moliwe.

Bibliografia
[Lea99]: Doug Lea, Concurrent Programming in Java: Design Principles and Patterns, wydanie drugie,
Prentice Hall 1999.
[PPP]: Robert C. Martin, Agile Software Development: Principles, Patterns, and Practices, Prentice
Hall 2002.
[PRAG]: Andrew Hunt, Dave Thomas, The Pragmatic Programmer, Addison-Wesley 2000.

208

ROZDZIA 13.

ROZDZIA 14.

Udane oczyszczanie kodu


Analiza przypadku procesora argumentw wiersza polecenia

oczyszczania kodu moduu, w ktrym wystpiy proR blemy ze skalowaniem. Przedstawimy,udanego


w jaki sposb modu ten zosta przebudowany i wyczyszOZDZIA TEN JEST ANALIZ PRZYPADKU

czony.
Wikszo z nas od czasu do czasu musi analizowa argumenty wiersza polecenia. Jeeli nie mamy
wygodnego narzdzia, to po prostu przegldamy tablic cigw przekazan do funkcji main.
Dostpnych jest kilka niezych narzdzi pochodzcych z rnych rde, ale adne nie oferowao
wszystkich oczekiwanych przez nas moliwoci. Z tego powodu zdecydowalimy si napisa wasny
modu. Nazwaem go Args.

209

Jest on bardzo prosty w uyciu. Tworzymy klas Args z argumentami wejciowymi oraz cigiem
formatujcym, a nastpnie odczytujemy wartoci argumentw z obiektu Args. Wemy pod uwag
nastpujcy prosty przykad:
L I S T I N G 1 4 . 1 . Proste zastosowanie klasy Args
public static void main(String[] args) {
try {
Args arg = new Args("l,p#,d*", args);
boolean logging = arg.getBoolean('l');
int port = arg.getInt('p');
String directory = arg.getString('d');
executeApplication(logging, port, directory);
} catch (ArgsException e) {
System.out.printf("Bd argumentw: %s\n", e.errorMessage());
}
}

Jak wida, jest to bardzo proste. Tworzymy obiekt klasy Args z dwoma parametrami. Pierwszym
jest cig formatu lub schematu: "l,p#,d*". Definiuje on trzy argumenty wiersza polecenia.
Pierwszy, -l, jest argumentem logicznym. Drugi, -p, jest argumentem cakowitym. Trzeci, -d, jest
argumentem tekstowym. Drugim parametrem konstruktora Args jest po prostu tablica argumentw
wiersza polecenia przekazana do main.
Jeeli konstruktor wykona si bez zgoszenia wyjtku ArgsException, to wiemy, e przekazany wiersz
polecenia zosta zanalizowany i e mona korzysta z obiektu Args. Metody getBoolean, getInteger
i getString pozwalaj na odczytywanie wartoci argumentw przy uyciu ich nazw.
Jeeli wystpi problem w cigu formatujcym albo w samych argumentach wiersza polecenia, zgaszany jest wyjtek ArgsException. Dokadny opis tego, co poszo le, mona odczyta za pomoc
metody errorMessage z wyjtku.

Implementacja klasy Args


Na listingu 14.2 przedstawiona jest implementacja klasy Args. Prosz o jego uwane przeczytanie.
Ciko pracowaem nad stylem i struktur, wic mam nadziej, e jest warta emulowania.
L I S T I N G 1 4 . 2 . Args.java
package com.objectmentor.utilities.args;
import static com.objectmentor.utilities.args.ArgsException.ErrorCode.*;
import java.util.*;
public class Args {
private Map<Character, ArgumentMarshaler> marshalers;
private Set<Character> argsFound;
private ListIterator<String> currentArgument;
public Args(String schema, String[] args) throws ArgsException {
marshalers = new HashMap<Character, ArgumentMarshaler>();
argsFound = new HashSet<Character>();

210

ROZDZIA 14.

parseSchema(schema);
parseArgumentStrings(Arrays.asList(args));
}
private void parseSchema(String schema) throws ArgsException {
for (String element : schema.split(","))
if (element.length() > 0)
parseSchemaElement(element.trim());
}
private void parseSchemaElement(String element) throws ArgsException {
char elementId = element.charAt(0);
String elementTail = element.substring(1);
validateSchemaElementId(elementId);
if (elementTail.length() == 0)
marshalers.put(elementId, new BooleanArgumentMarshaler());
else if (elementTail.equals("*"))
marshalers.put(elementId, new StringArgumentMarshaler());
else if (elementTail.equals("#"))
marshalers.put(elementId, new IntegerArgumentMarshaler());
else if (elementTail.equals("##"))
marshalers.put(elementId, new DoubleArgumentMarshaler());
else if (elementTail.equals("[*]"))
marshalers.put(elementId, new StringArrayArgumentMarshaler());
else
throw new ArgsException(INVALID_ARGUMENT_FORMAT, elementId, elementTail);
}
private void validateSchemaElementId(char elementId) throws ArgsException {
if (!Character.isLetter(elementId))
throw new ArgsException(INVALID_ARGUMENT_NAME, elementId, null);
}
private void parseArgumentStrings(List<String> argsList) throws ArgsException
{
for (currentArgument = argsList.listIterator(); currentArgument.hasNext();)
{
String argString = currentArgument.next();
if (argString.startsWith("-")) {
parseArgumentCharacters(argString.substring(1));
} else {
currentArgument.previous();
break;
}
}
}
private void parseArgumentCharacters(String argChars) throws ArgsException {
for (int i = 0; i < argChars.length(); i++)
parseArgumentCharacter(argChars.charAt(i));
}
private void parseArgumentCharacter(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
if (m == null) {
throw new ArgsException(UNEXPECTED_ARGUMENT, argChar, null);
} else {
argsFound.add(argChar);
try {
m.set(currentArgument);
} catch (ArgsException e) {
e.setErrorArgumentId(argChar);
throw e;
}

UDANE OCZYSZCZANIE KODU

211

}
}
public boolean has(char arg) {
return argsFound.contains(arg);
}
public int nextArgument() {
return currentArgument.nextIndex();
}
public boolean getBoolean(char arg) {
return BooleanArgumentMarshaler.getValue(marshalers.get(arg));
}
public String getString(char arg) {
return StringArgumentMarshaler.getValue(marshalers.get(arg));
}
public int getInt(char arg) {
return IntegerArgumentMarshaler.getValue(marshalers.get(arg));
}
public double getDouble(char arg) {
return DoubleArgumentMarshaler.getValue(marshalers.get(arg));
}
public String[] getStringArray(char arg) {
return StringArrayArgumentMarshaler.getValue(marshalers.get(arg));
}
}

Warto zauway, e mona czyta ten kod od gry do dou bez skakania po kodzie lub wyszukiwania
jakich elementw. Jedynym elementem, ktrego trzeba szuka, jest definicja ArgumentMarshaler,
ktr rozmylnie pozostawiem bez zmian. Po uwanym przeczytaniu kodu powinnimy rozumie,
jakie jest przeznaczenie interfejsu ArgumentMarshaler i klasy, ktra go implementuje. Kilka
z nich przedstawiono poniej (listingi od 14.3 do 14.6).
L I S T I N G 1 4 . 3 . ArgumentMarshaler.java
public interface ArgumentMarshaler {
void set(Iterator<String> currentArgument) throws ArgsException;
}

L I S T I N G 1 4 . 4 . BooleanArgumentMarshaler.java
public class BooleanArgumentMarshaler implements ArgumentMarshaler {
private boolean booleanValue = false;
public void set(Iterator<String> currentArgument) throws ArgsException {
booleanValue = true;
}
public static boolean getValue(ArgumentMarshaler am) {
if (am != null && am instanceof BooleanArgumentMarshaler)
return ((BooleanArgumentMarshaler) am).booleanValue;
else
return false;
}
}

212

ROZDZIA 14.

L I S T I N G 1 4 . 5 . StringArgumentMarshaler.java
import static com.objectmentor.utilities.args.ArgsException.ErrorCode.*;
public class StringArgumentMarshaler implements ArgumentMarshaler {
private String stringValue = "";
public void set(Iterator<String> currentArgument) throws ArgsException {
try {
stringValue = currentArgument.next();
} catch (NoSuchElementException e) {
throw new ArgsException(MISSING_STRING);
}
}
public static String getValue(ArgumentMarshaler am) {
if (am != null && am instanceof StringArgumentMarshaler)
return ((StringArgumentMarshaler) am).stringValue;
else
return "";
}
}

L I S T I N G 1 4 . 6 . IntegerArgumentMarshaler.java
import static com.objectmentor.utilities.args.ArgsException.ErrorCode.*;
public class IntegerArgumentMarshaler implements ArgumentMarshaler {
private int intValue = 0;
public void set(Iterator<String> currentArgument) throws ArgsException {
String parameter = null;
try {
parameter = currentArgument.next();
intValue = Integer.parseInt(parameter);
} catch (NoSuchElementException e) {
throw new ArgsException(MISSING_INTEGER);
} catch (NumberFormatException e) {
throw new ArgsException(INVALID_INTEGER, parameter);
}
}
public static int getValue(ArgumentMarshaler am) {
if (am != null && am instanceof IntegerArgumentMarshaler)
return ((IntegerArgumentMarshaler) am).intValue;
else
return 0;
}
}

Pozostae interfejsy dziedziczce po ArgumentMarshaler dla typw double i tablic String po


prostu powielaj ten wzorzec. Pozostawi je do wykonania jako wiczenie.
Problematyczny moe by jeszcze jeden fragment definicja staych kodw bdw. Znajduj si
one w klasie ArgsException (listing 14.7).

UDANE OCZYSZCZANIE KODU

213

L I S T I N G 1 4 . 7 . ArgsException.java
import static com.objectmentor.utilities.args.ArgsException.ErrorCode.*;
public class ArgsException extends Exception {
private char errorArgumentId = '\0';
private String errorParameter = null;
private ErrorCode errorCode = OK;
public ArgsException() {}
public ArgsException(String message) {super(message);}
public ArgsException(ErrorCode errorCode) {
this.errorCode = errorCode;
}
public ArgsException(ErrorCode errorCode, String errorParameter) {
this.errorCode = errorCode;
this.errorParameter = errorParameter;
}
public ArgsException(ErrorCode errorCode,
char errorArgumentId, String errorParameter) {
this.errorCode = errorCode;
this.errorParameter = errorParameter;
this.errorArgumentId = errorArgumentId;
}
public char getErrorArgumentId() {
return errorArgumentId;
}
public void setErrorArgumentId(char errorArgumentId) {
this.errorArgumentId = errorArgumentId;
}
public String getErrorParameter() {
return errorParameter;
}
public void setErrorParameter(String errorParameter) {
this.errorParameter = errorParameter;
}
public ErrorCode getErrorCode() {
return errorCode;
}
public void setErrorCode(ErrorCode errorCode) {
this.errorCode = errorCode;
}
public String errorMessage() {
switch (errorCode) {
case OK:
return "TILT: Niedostpne.";
case UNEXPECTED_ARGUMENT:
return String.format("Nieoczekiwany argument -%c.", errorArgumentId);
case MISSING_STRING:
return String.format("Nie mona znale parametru znakowego dla -%c.",
errorArgumentId);
case INVALID_INTEGER:

214

ROZDZIA 14.

return String.format("Argument -%c oczekuje liczby cakowitej, a by '%s'.",


errorArgumentId, errorParameter);
case MISSING_INTEGER:
return String.format("Nie mona znale parametru cakowitego dla -%c.",
errorArgumentId);
case INVALID_DOUBLE:
return String.format("Argument -%c oczekuje liczby double, a by '%s'.",
errorArgumentId, errorParameter);
case MISSING_DOUBLE:
return String.format("Nie mona znale parametru double dla -%c.",
errorArgumentId);
case INVALID_ARGUMENT_NAME:
return String.format("'%c' nie jest prawidow nazw argumentu.",
errorArgumentId);
case INVALID_ARGUMENT_FORMAT:
return String.format("'%s' nie jest prawidowym formatem argumentu.",
errorParameter);
}
return "";
}
public enum ErrorCode {
OK, INVALID_ARGUMENT_FORMAT, UNEXPECTED_ARGUMENT, INVALID_ARGUMENT_NAME,
MISSING_STRING,
MISSING_INTEGER, INVALID_INTEGER,
MISSING_DOUBLE, INVALID_DOUBLE}
}

Jak wida, sporo kodu potrzeba do zrealizowania szczegw tego prostego zagadnienia. Jednym
z powodw jest zastosowanie dosy rozwlekego jzyka. Java, jzyk o statycznych typach, wymaga
uycia wielu sw w celu usatysfakcjonowania systemu typw. W jzykach takich jak Ruby, Python lub Smalltalk program ten byby znacznie mniejszy1.
Prosz o ponowne przeczytanie kodu. Szczegln uwag proponuj zwrci na sposb nazywania
elementw programu, rozmiar funkcji oraz formatowanie kodu. Jeeli Czytelnik jest dowiadczonym
programist, moe mie uwagi do niektrych czci stylu i struktury. Jednak mona powiedzie,
e jako cao program ten jest adnie napisany i ma czyst struktur.
Na przykad powinno by oczywiste, w jaki sposb moemy doda nowy typ argumentu, np. argument
daty lub argument liczby zespolonej, a jego dodanie powinno zaj niewiele czasu i wysiku. Mwic
krtko, wymagaoby to napisania nowej klasy implementujcej ArgumentMarshaler oraz nowej
funkcji getXXX i nowego elementu case w funkcji parseSchemaElement. Prawdopodobnie konieczne
bdzie rwnie dodanie nowego ArgsException.ErrorCode i nowego komunikatu bdu.

Jak to napisaem?
Prosz teraz o szczegln uwag. Nie napisaem tego programu od pocztku do koca w jego
obecnej postaci. Co waniejsze, nie oczekuj, e ktokolwiek bdzie w stanie pisa czyste i eleganckie
programy w jednym przebiegu. Jeeli nauczylimy si czegokolwiek w kilku ostatnich dekadach,
to przede wszystkim tego, e programowanie jest bardziej rzemiosem ni sztuk. Aby pisa czysty
kod, musimy na pocztku napisa brudny kod, a nastpnie go oczyci.
1

Ostatnio przepisaem ten modu na Ruby. Mia on okoo 1/7 wielkoci przedstawionego tu moduu i nieco lepsz struktur.

UDANE OCZYSZCZANIE KODU

215

Nie powinno to by dla nas niespodziank. Nauczylimy si tej prawdy w szkole podstawowej, gdy
nasi nauczyciele prbowali (zwykle w mczarniach) nauczy nas pisania szkicw prac. Uczyli nas
procesu, w ktrym powinnimy napisa zgrubny szkic, nastpnie drugi szkic, a potem kilka kolejnych szkicw, a otrzymamy ostateczn wersj. Prbowali nam w ten sposb pokaza, e pisanie
czystych tekstw to dugi proces wielu poprawek.
Wikszo modych programistw (podobnie jak wikszo absolwentw) nie stosuje si do tej porady. Uwaaj oni, e ich podstawowym celem jest napisanie dziaajcego programu. Gdy ju
dziaa, przechodz do nastpnego zadania, pozostawiajc ten dziaajcy program w dowolnym
stanie, ktry doprowadzi do jego dziaania. Wikszo dowiadczonych programistw wie, e
jest to zawodowe samobjstwo.

Args zgrubny szkic


Na listingu 14.8 przedstawiona jest wczeniejsza wersja klasy Args. Ona dziaa. Jest jednak mocno
zagmatwana.
L I S T I N G 1 4 . 8 . Args.java (pierwszy szkic)
import java.text.ParseException;
import java.util.*;
public class Args {
private String schema;
private String[] args;
private boolean valid = true;
private Set<Character> unexpectedArguments = new TreeSet<Character>();
private Map<Character, Boolean> booleanArgs =
new HashMap<Character, Boolean>();
private Map<Character, String> stringArgs = new HashMap<Character, String>();
private Map<Character, Integer> intArgs = new HashMap<Character, Integer>();
private Set<Character> argsFound = new HashSet<Character>();
private int currentArgument;
private char errorArgumentId = '\0';
private String errorParameter = "TILT";
private ErrorCode errorCode = ErrorCode.OK;
private enum ErrorCode {
OK, MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER, UNEXPECTED_ARGUMENT}
public Args(String schema, String[] args) throws ParseException {
this.schema = schema;
this.args = args;
valid = parse();
}
private boolean parse() throws ParseException {
if (schema.length() == 0 && args.length == 0)
return true;
parseSchema();
try {
parseArguments();
} catch (ArgsException e) {
}
return valid;
}

216

ROZDZIA 14.

private boolean parseSchema() throws ParseException {


for (String element : schema.split(",")) {
if (element.length() > 0) {
String trimmedElement = element.trim();
parseSchemaElement(trimmedElement);
}
}
return true;
}
private void parseSchemaElement(String element) throws ParseException {
char elementId = element.charAt(0);
String elementTail = element.substring(1);
validateSchemaElementId(elementId);
if (isBooleanSchemaElement(elementTail))
parseBooleanSchemaElement(elementId);
else if (isStringSchemaElement(elementTail))
parseStringSchemaElement(elementId);
else if (isIntegerSchemaElement(elementTail)) {
parseIntegerSchemaElement(elementId);
} else {
throw new ParseException(
String.format("Argument: %c ma niewaciwy format: %s.",
elementId, elementTail), 0);
}
}
private void validateSchemaElementId(char elementId) throws ParseException {
if (!Character.isLetter(elementId)) {
throw new ParseException(
"Zy znak:" + elementId + "w formacie Args: " + schema, 0);
}
}
private void parseBooleanSchemaElement(char elementId) {
booleanArgs.put(elementId, false);
}
private void parseIntegerSchemaElement(char elementId) {
intArgs.put(elementId, 0);
}
private void parseStringSchemaElement(char elementId) {
stringArgs.put(elementId, "");
}
private boolean isStringSchemaElement(String elementTail) {
return elementTail.equals("*");
}
private boolean isBooleanSchemaElement(String elementTail) {
return elementTail.length() == 0;
}
private boolean isIntegerSchemaElement(String elementTail) {
return elementTail.equals("#");
}
private boolean parseArguments() throws ArgsException {
for (currentArgument = 0; currentArgument < args.length; currentArgument++)
{
String arg = args[currentArgument];
parseArgument(arg);
}

UDANE OCZYSZCZANIE KODU

217

return true;
}
private void parseArgument(String arg) throws ArgsException {
if (arg.startsWith("-"))
parseElements(arg);
}
private void parseElements(String arg) throws ArgsException {
for (int i = 1; i < arg.length(); i++)
parseElement(arg.charAt(i));
}
private void parseElement(char argChar) throws ArgsException {
if (setArgument(argChar))
argsFound.add(argChar);
else {
unexpectedArguments.add(argChar);
errorCode = ErrorCode.UNEXPECTED_ARGUMENT;
valid = false;
}
}
private boolean setArgument(char argChar) throws ArgsException {
if (isBooleanArg(argChar))
setBooleanArg(argChar, true);
else if (isStringArg(argChar))
setStringArg(argChar);
else if (isIntArg(argChar))
setIntArg(argChar);
else
return false;
return true;
}
private boolean isIntArg(char argChar) {return intArgs.containsKey(argChar);}
private void setIntArg(char argChar) throws ArgsException {
currentArgument++;
String parameter = null;
try {
parameter = args[currentArgument];
intArgs.put(argChar, new Integer(parameter));
} catch (ArrayIndexOutOfBoundsException e) {
valid = false;
errorArgumentId = argChar;
errorCode = ErrorCode.MISSING_INTEGER;
throw new ArgsException();
} catch (NumberFormatException e) {
valid = false;
errorArgumentId = argChar;
errorParameter = parameter;
errorCode = ErrorCode.INVALID_INTEGER;
throw new ArgsException();
}
}
private void setStringArg(char argChar) throws ArgsException {
currentArgument++;
try {
stringArgs.put(argChar, args[currentArgument]);
} catch (ArrayIndexOutOfBoundsException e) {
valid = false;
errorArgumentId = argChar;

218

ROZDZIA 14.

errorCode = ErrorCode.MISSING_STRING;
throw new ArgsException();
}
}
private boolean isStringArg(char argChar) {
return stringArgs.containsKey(argChar);
}
private void setBooleanArg(char argChar, boolean value) {
booleanArgs.put(argChar, value);
}
private boolean isBooleanArg(char argChar) {
return booleanArgs.containsKey(argChar);
}
public int cardinality() {
return argsFound.size();
}
public String usage() {
if (schema.length() > 0)
return "-[" + schema + "]";
else
return "";
}
public String errorMessage() throws Exception {
switch (errorCode) {
case OK:
throw new Exception("TILT: Niedostpne.");
case UNEXPECTED_ARGUMENT:
return unexpectedArgumentMessage();
case MISSING_STRING:
return String.format("Nie mona znale parametru znakowego dla -%c.",
errorArgumentId);
case INVALID_INTEGER:
return String.format("Argument -%c oczekuje liczby cakowitej, a by '%s'.",
errorArgumentId, errorParameter);
case MISSING_INTEGER:
return String.format("Nie mona znale parametru cakowitego dla -%c.",
errorArgumentId);
}
return "";
}
private String unexpectedArgumentMessage() {
StringBuffer message = new StringBuffer("Argument(y) -");
for (char c : unexpectedArguments) {
message.append(c);
}
message.append(" nieoczekiwany.");
return message.toString();
}
private boolean falseIfNull(Boolean b) {
return b != null && b;
}
private int zeroIfNull(Integer i) {
return i == null ? 0 : i;
}

UDANE OCZYSZCZANIE KODU

219

private String blankIfNull(String s) {


return s == null ? "" : s;
}
public String getString(char arg) {
return blankIfNull(stringArgs.get(arg));
}
public int getInt(char arg) {
return zeroIfNull(intArgs.get(arg));
}
public boolean getBoolean(char arg) {
return falseIfNull(booleanArgs.get(arg));
}
public boolean has(char arg) {
return argsFound.contains(arg);
}
public boolean isValid() {
return valid;
}
private class ArgsException extends Exception {
}
}

Mam nadziej, e pocztkow reakcj na t mas kodu byo: Mam nadziej, e nie zostawi tego
w takiej postaci!. Jeeli taka bya Twoja reakcja, to pamitaj, jak inne osoby reaguj na kod, ktry
pozostawisz w formie zgrubnego szkicu.
Zgrubny szkic to najagodniejsze okrelenie tego typu kodu. Jest to po prostu praca w toku.
Ogromna liczba zmiennych instancyjnych jest zniechcajca. Dziwne napisy, takie jak TILT,
obiekty HashSet oraz TreeSet, bloki try-catch-catch jeszcze bardziej pogarszaj spraw.
Nie chciaem pisa takiego stosu mieci. W rzeczywistoci staraem si utrzyma rozsdn organizacj.
Mona si o tym przekona na podstawie wyboru nazw funkcji i zmiennych oraz na podstawie tego,
e istnieje zgrubna struktura programu. Jednak jasne jest, e problem mi uciek.
Baagan stale przyrasta. Wczeniejsze wersje nie wyglday tak le. Na listingu 14.9 przedstawiona
jest wczeniejsza wersja, w ktrej dziaay tylko argumenty Boolean.
L I S T I N G 1 4 . 9 . Args.java (tylko Boolean)
package com.objectmentor.utilities.getopts;
import java.util.*;
public class Args {
private String schema;
private String[] args;
private boolean valid;
private Set<Character> unexpectedArguments = new TreeSet<Character>();
private Map<Character, Boolean> booleanArgs =
new HashMap<Character, Boolean>();
private int numberOfArguments = 0;

220

ROZDZIA 14.

public Args(String schema, String[] args) {


this.schema = schema;
this.args = args;
valid = parse();
}
public boolean isValid() {
return valid;
}
private boolean parse() {
if (schema.length() == 0 && args.length == 0)
return true;
parseSchema();
parseArguments();
return unexpectedArguments.size() == 0;
}
private boolean parseSchema() {
for (String element : schema.split(",")) {
parseSchemaElement(element);
}
return true;
}
private void parseSchemaElement(String element) {
if (element.length() == 1) {
parseBooleanSchemaElement(element);
}
}
private void parseBooleanSchemaElement(String element) {
char c = element.charAt(0);
if (Character.isLetter(c)) {
booleanArgs.put(c, false);
}
}
private boolean parseArguments() {
for (String arg : args)
parseArgument(arg);
return true;
}
private void parseArgument(String arg) {
if (arg.startsWith("-"))
parseElements(arg);
}
private void parseElements(String arg) {
for (int i = 1; i < arg.length(); i++)
parseElement(arg.charAt(i));
}
private void parseElement(char argChar) {
if (isBoolean(argChar)) {
numberOfArguments++;
setBooleanArg(argChar, true);
} else
unexpectedArguments.add(argChar);
}
private void setBooleanArg(char argChar, boolean value) {
booleanArgs.put(argChar, value);
}

UDANE OCZYSZCZANIE KODU

221

private boolean isBoolean(char argChar) {


return booleanArgs.containsKey(argChar);
}
public int cardinality() {
return numberOfArguments;
}
public String usage() {
if (schema.length() > 0)
return "-["+schema+"]";
else
return "";
}
public String errorMessage() {
if (unexpectedArguments.size() > 0) {
return unexpectedArgumentMessage();
} else
return "";
}
private String unexpectedArgumentMessage() {
StringBuffer message = new StringBuffer("Argument(y) -");
for (char c : unexpectedArguments) {
message.append(c);
}
message.append(" nieoczekiwany.");
return message.toString();
}
public boolean getBoolean(char arg) {
return booleanArgs.get(arg);
}
}

Cho mona w tym kodzie znale wiele miejsc wymagajcych zmiany, w caoci nie wyglda tak le.
Jest zwarty, prosty i atwy do zrozumienia. Jednak mona tu atwo zauway zalki pniejszej
sterty mieci. Dosy jasno wida, w jaki sposb rozrs si pniejszy baagan.
Warto zauway, e w pniejszym baaganie mamy tylko o dwa typy argumentw wicej: String
oraz integer. Dodanie tylko dwch typw miao negatywny wpyw na kod. Co, co byo moliwe do utrzymania, zmienio si w co, co byoby wypenione bdami.
Dodaem tylko dwa typy argumentw. Na pocztku dodaem argument String, co dao w efekcie
nastpujcy kod:
L I S T I N G 1 4 . 1 0 . Args.java (Boolean oraz String)
package com.objectmentor.utilities.getopts;
import java.text.ParseException;
import java.util.*;
public class Args {
private String schema;
private String[] args;
private boolean valid = true;

222

ROZDZIA 14.

private Set<Character> unexpectedArguments = new TreeSet<Character>();


private Map<Character, Boolean> booleanArgs =
new HashMap<Character, Boolean>();
private Map<Character, String> stringArgs =
new HashMap<Character, String>();
private Set<Character> argsFound = new HashSet<Character>();
private int currentArgument;
private char errorArgument = '\0';
enum ErrorCode {
OK, MISSING_STRING}
private ErrorCode errorCode = ErrorCode.OK;
public Args(String schema, String[] args) throws ParseException {
this.schema = schema;
this.args = args;
valid = parse();
}
private boolean parse() throws ParseException {
if (schema.length() == 0 && args.length == 0)
return true;
parseSchema();
parseArguments();
return valid;
}
private boolean parseSchema() throws ParseException {
for (String element : schema.split(",")) {
if (element.length() > 0) {
String trimmedElement = element.trim();
parseSchemaElement(trimmedElement);
}
}
return true;
}
private void parseSchemaElement(String element) throws ParseException {
char elementId = element.charAt(0);
String elementTail = element.substring(1);
validateSchemaElementId(elementId);
if (isBooleanSchemaElement(elementTail))
parseBooleanSchemaElement(elementId);
else if (isStringSchemaElement(elementTail))
parseStringSchemaElement(elementId);
}
private void validateSchemaElementId(char elementId) throws ParseException {
if (!Character.isLetter(elementId)) {
throw new ParseException(
"Zy znak:" + elementId + "w formacie Args: " + schema, 0);
}
}
private void parseStringSchemaElement(char elementId) {
stringArgs.put(elementId, "");
}
private boolean isStringSchemaElement(String elementTail) {
return elementTail.equals("*");
}
private boolean isBooleanSchemaElement(String elementTail) {

UDANE OCZYSZCZANIE KODU

223

return elementTail.length() == 0;
}
private void parseBooleanSchemaElement(char elementId) {
booleanArgs.put(elementId, false);
}
private boolean parseArguments() {
for (currentArgument = 0; currentArgument < args.length; currentArgument++)
{
String arg = args[currentArgument];
parseArgument(arg);
}
return true;
}
private void parseArgument(String arg) {
if (arg.startsWith("-"))
parseElements(arg);
}
private void parseElements(String arg) {
for (int i = 1; i < arg.length(); i++)
parseElement(arg.charAt(i));
}
private void parseElement(char argChar) {
if (setArgument(argChar))
argsFound.add(argChar);
else {
unexpectedArguments.add(argChar);
valid = false;
}
}
private boolean setArgument(char argChar) {
boolean set = true;
if (isBoolean(argChar))
setBooleanArg(argChar, true);
else if (isString(argChar))
setStringArg(argChar, "");
else
set = false;
return set;
}
private void setStringArg(char argChar, String s) {
currentArgument++;
try {
stringArgs.put(argChar, args[currentArgument]);
} catch (ArrayIndexOutOfBoundsException e) {
valid = false;
errorArgument = argChar;
errorCode = ErrorCode.MISSING_STRING;
}
}
private boolean isString(char argChar) {
return stringArgs.containsKey(argChar);
}
private void setBooleanArg(char argChar, boolean value) {
booleanArgs.put(argChar, value);
}

224

ROZDZIA 14.

private boolean isBoolean(char argChar) {


return booleanArgs.containsKey(argChar);
}
public int cardinality() {
return argsFound.size();
}
public String usage() {
if (schema.length() > 0)
return "-[" + schema + "]";
else
return "";
}
public String errorMessage() throws Exception {
if (unexpectedArguments.size() > 0) {
return unexpectedArgumentMessage();
} else
switch (errorCode) {
case MISSING_STRING:
return String.format("Nie mona znale parametru znakowego dla -%c.",
errorArgument);
case OK:
throw new Exception("TILT: Niedostpne.");
}
return "";
}
private String unexpectedArgumentMessage() {
StringBuffer message = new StringBuffer("Argument(y) -");
for (char c : unexpectedArguments) {
message.append(c);
}
message.append(" nieoczekiwany.");
return message.toString();
}
public boolean getBoolean(char arg) {
return falseIfNull(booleanArgs.get(arg));
}
private boolean falseIfNull(Boolean b) {
return b == null ? false : b;
}
public String getString(char arg) {
return blankIfNull(stringArgs.get(arg));
}
private String blankIfNull(String s) {
return s == null ? "" : s;
}
public boolean has(char arg) {
return argsFound.contains(arg);
}
public boolean isValid() {
return valid;
}
}

UDANE OCZYSZCZANIE KODU

225

Jak wida, kod zacz si wymyka spod kontroli. Nadal nie jest zy, ale jasne jest, e baagan zacz
narasta. Jest to ju stos, ale jeszcze niewypeniony mieciami. Wystarczyo dodanie obsugi argumentw typu integer, aby stos zacz ropie i fermentowa.

Zatrzymaem si
Miaem do dodania co najmniej dwa kolejne typy argumentw i od razu mogem stwierdzi, e
sprawy bd miay si jeszcze gorzej. Jeeli bez zastanowienia parbym naprzd, prawdopodobnie
bybym w stanie doprowadzi ten kod do prawidowego dziaania, ale pozostawiajc za sob zbyt
duy baagan, aby mona byo go atwo poprawi. Jeeli struktura tego kodu miaa by kiedykolwiek
moliwa do utrzymania, by to najwyszy czas, aby j poprawi.
Dlatego wstrzymaem dodawanie funkcji i zaczem przebudow. Po dodaniu obsugi argumentw
typu String i integer wiedziaem ju, e kady typ argumentu wymaga nowego kodu w trzech
gwnych miejscach. Po pierwsze, kady typ argumentu wymaga pewnego sposobu analizy elementw schematu w celu wybrania obiektu HashMap dla danego typu. Nastpnie kady typ argumentw musi by pobrany z cigu wiersza polecenia i skonwertowany na jego waciwy typ. Na
koniec kady typ argumentw wymaga metody getXXX, ktra zwraca wywoujcemu waciwy typ.
Wiele rnych typw i wszystkie maj podobne metody dla mnie to brzmi jak klasa. W ten sposb
powstaa koncepcja klasy ArgumentMarshaler.

O przyrostowoci
Jednym z najlepszych sposobw na zrujnowanie programu jest wprowadzenie do jego struktury
ogromnych zmian w imi usprawnie. Niektre programy nigdy nie podnosz si po takich
usprawnieniach. Problem wynika z tego, e bardzo trudno uzyska takie samo dziaanie, jak przed
usprawnieniami.
Aby tego unikn, korzystam z dyscypliny programowania sterowanego testami (TDD). Jedn
z podstawowych doktryn tego podejcia jest zachowanie dziaania systemu przez cay czas. Inaczej
mwic, przy uyciu TDD nie mog wprowadzi zmian do systemu, ktre przerywaj jego dziaanie.
Kada wprowadzona zmiana musi zachowa poprzednie dziaanie systemu.
Aby to osign, potrzebuj zbioru testw automatycznych, ktre mog uruchomi w celu sprawdzenia, czy dziaanie systemu nie zmienio si. Dla klasy Args napisaem zbir testw jednostkowych i akceptacyjnych ju w czasie budowania sterty mieci. Testy jednostkowe byy napisane w jzyku
Java i administrowane przez JUnit. Testy akceptacyjne byy napisane jako strony wiki w FitNesse.
Mogem uruchomi te testy w dowolnym momencie i jeeli wykonyway si prawidowo, byem
pewny, e system dziaa w zdefiniowany sposb.
Zaczem wic wprowadza sporo niewielkich zmian. Kada zmiana przybliaa struktur systemu
w kierunku koncepcji uycia ArgumentMarshaler. Jednak po wprowadzeniu kadej zmiany system
stale dziaa. Pierwsz zmian, jak wykonaem, byo wstawienie szkieletu ArgumentMarshaller na
szczyt sterty mieci (listing 14.11).

226

ROZDZIA 14.

L I S T I N G 1 4 . 1 1 . ArgumentMarshaller dodany do Args.java


private class ArgumentMarshaler {
private boolean booleanValue = false;
public void setBoolean(boolean value) {
booleanValue = value;
}
public boolean getBoolean() {return booleanValue;}
}
private class BooleanArgumentMarshaler extends ArgumentMarshaler {
}
private class StringArgumentMarshaler extends ArgumentMarshaler {
}
private class IntegerArgumentMarshaler extends ArgumentMarshaler {
}
}

Jasne jest, e nie spowodowao to powstania adnego bdu. Nastpnie wprowadziem jak najprostsz
modyfikacj, ktra nie powinna spowodowa duych usterek w kodzie. Zmieniem HashMap dla
argumentw Boolean, aby przechowywa obiekty ArgumentMarshaler.
private Map<Character, ArgumentMarshaler> booleanArgs =
new HashMap<Character, ArgumentMarshaler>();

Spowodowao to bd w kilku instrukcjach, ktre szybko poprawiem.


...
private void parseBooleanSchemaElement(char elementId) {
booleanArgs.put(elementId, new BooleanArgumentMarshaler());
}
...
private void setBooleanArg(char argChar, boolean value) {
booleanArgs.get(argChar).setBoolean(value);
}
...
public boolean getBoolean(char arg) {
return falseIfNull(booleanArgs.get(arg).getBoolean());
}

Warto zauway, e zmiany te znajduj si dokadnie w tych obszarach, ktre wczeniej wymieniem: metody parse, set oraz get dla danego typu argumentu. Niestety, pomimo tak niewielkich
zmian niektre testy przestay dziaa. Jeeli spojrzymy uwanie na getBoolean, zauwaymy, e
jeeli wywoamy j z argumentem 'y', a nie bdzie argumentu y, to booleanArgs.get('y')
zwrci null, a funkcja zgosi NullPointerException. Funkcja falseIfNull zostaa uyta do
ochrony przed tak sytuacj, ale wprowadzona przeze mnie zmiana spowodowaa, e funkcja ta
przestaa by wana.
Przyrostowo wymaga, abym szybko doprowadzi do dziaania kodu, przed wprowadzeniem
jakichkolwiek innych zmian. W rzeczywistoci poprawka nie bya taka trudna. Po prostu przesunem kontrol wartoci null. Nie byo ju wartoci boolean bdcej null, ktr musiaem
sprawdza; by teraz obiekt ArgumentMarshaller.

UDANE OCZYSZCZANIE KODU

227

Po pierwsze, usunem falseIfNull, zastpujc j funkcj getBoolean. Bya teraz bezuyteczna,


wic wyeliminowaem ca funkcj. Testy byy przerywane w ten sam sposb, wic byem pewny,
e nie wprowadziem adnych nowych bdw.
public boolean getBoolean(char arg) {
return booleanArgs.get(arg).getBoolean();
}

Nastpnie podzieliem funkcj na dwa wiersze i umieciem ArgumentMarshaler w osobnej


zmiennej o nazwie argumentMarshaler. Musiaem zaj si jednak dug nazw zmiennej; bya
ona mocno nadmiarowa i zaciemniaa funkcj. Skrciem j wic do am [N5].
public boolean getBoolean(char arg) {
Args.ArgumentMarshaler am = booleanArgs.get(arg);
return am.getBoolean();
}

Nastpnie dodaem kod wykrywania wartoci null.


public boolean getBoolean(char arg) {
Args.ArgumentMarshaler am = booleanArgs.get(arg);
return am != null && am.getBoolean();
}

Argumenty typu String


Dodanie argumentw String byo bardzo podobne do dodawania argumentw boolean. Musiaem
zmieni HashMap oraz doprowadzi do prawidowego dziaania funkcji parse, set oraz get. Nie
powinno tu by adnych niespodzianek, by moe poza umieszczeniem caej implementacji szeregowania w klasie bazowej ArgumentMarshaler zamiast rozproszenia jej w klasach pochodnych.
private Map<Character, ArgumentMarshaler> stringArgs =
new HashMap<Character, ArgumentMarshaler>();
...
private void parseStringSchemaElement(char elementId) {
stringArgs.put(elementId, new StringArgumentMarshaler());
}
...
private void setStringArg(char argChar) throws ArgsException {
currentArgument++;
try {
stringArgs.get(argChar).setString(args[currentArgument]);
} catch (ArrayIndexOutOfBoundsException e) {
valid = false;
errorArgumentId = argChar;
errorCode = ErrorCode.MISSING_STRING;
throw new ArgsException();
}
}
...
public String getString(char arg) {
Args.ArgumentMarshaler am = stringArgs.get(arg);
return am == null ? "" : am.getString();
}
...
private class ArgumentMarshaler {
private boolean booleanValue = false;

228

ROZDZIA 14.

private String stringValue;


public void setBoolean(boolean value) {
booleanValue = value;
}
public boolean getBoolean() {
return booleanValue;
}
public void setString(String s) {
stringValue = s;
}
public String getString() {
return stringValue == null ? "" : stringValue;
}
}

Ponownie zmiany te byy wprowadzane jedna po drugiej i w taki sposb, e testy stale dziaay,
cho nie przechodziy. Gdy test nie przechodzi, upewniaem si, e przed przejciem do nastpnej
zmiany bdzie on przechodzi prawidowo.
Teraz moje intencje powinny by ju widoczne. Gdy obecne funkcje szeregowania znajduj si
w klasie bazowej ArgumentMarshaler, zaczynam przesuwa je do klas pochodnych. Powoduje to,
e wszystko dziaa, przy jednoczesnej stopniowej zmianie ksztatu programu.
Oczywistym krokiem byo przeniesienie funkcji dla argumentu int do ArgumentMarshaler. I tym
razem nie byo adnych niespodzianek.
private Map<Character, ArgumentMarshaler> intArgs =
new HashMap<Character, ArgumentMarshaler>();
...
private void parseIntegerSchemaElement(char elementId) {
intArgs.put(elementId, new IntegerArgumentMarshaler());
}
...
private void setIntArg(char argChar) throws ArgsException {
currentArgument++;
String parameter = null;
try {
parameter = args[currentArgument];
intArgs.get(argChar).setInteger(Integer.parseInt(parameter));
} catch (ArrayIndexOutOfBoundsException e) {
valid = false;
errorArgumentId = argChar;
errorCode = ErrorCode.MISSING_INTEGER;
throw new ArgsException();
} catch (NumberFormatException e) {
valid = false;
errorArgumentId = argChar;
errorParameter = parameter;
errorCode = ErrorCode.INVALID_INTEGER;
throw new ArgsException();
}
}
...
public int getInt(char arg) {
Args.ArgumentMarshaler am = intArgs.get(arg);
return am == null ? 0 : am.getInteger();

UDANE OCZYSZCZANIE KODU

229

}
...
private class ArgumentMarshaler {
private boolean booleanValue = false;
private String stringValue;
private int integerValue;
public void setBoolean(boolean value) {
booleanValue = value;
}
public boolean getBoolean() {
return booleanValue;
}
public void setString(String s) {
stringValue = s;
}
public String getString() {
return stringValue == null ? "" : stringValue;
}
public void setInteger(int i) {
integerValue = i;
}
public int getInteger() {
return integerValue;
}
}

Gdy wszystkie funkcje szeregowania znalazy si w ArgumentMarshaler, zaczem przenosi odpowiednie funkcje do jego klas pochodnych. Pierwszym krokiem byo przeniesienie funkcji setBoolean
do BooleanArgumentMarshaller i upewnienie si, e jest wywoywana prawidowo. Utworzyem
wic abstrakcyjn metod set.
private abstract class ArgumentMarshaler {
protected boolean booleanValue = false;
private String stringValue;
private int integerValue;
public void setBoolean(boolean value) {
booleanValue = value;
}
public boolean getBoolean() {
return booleanValue;
}
public void setString(String s) {
stringValue = s;
}
public String getString() {
return stringValue == null ? "" : stringValue;
}
public void setInteger(int i) {
integerValue = i;
}

230

ROZDZIA 14.

public int getInteger() {


return integerValue;
}
public abstract void set(String s);
}

Nastpnie zaimplementowaem metod set w BooleanArgumentMarshaller.


private class BooleanArgumentMarshaler extends ArgumentMarshaler {
public void set(String s) {
booleanValue = true;
}
}

Na koniec zastpiem wywoanie setBoolean wywoaniem set.


private void setBooleanArg(char argChar, boolean value) {
booleanArgs.get(argChar).set("true");
}

Testy nadal byy wykonywane prawidowo. Poniewa zmiana ta spowodowaa, e set zostaa
przeniesiona do BooleanArgumentMarshaler, usunem metod setBoolean z klasy bazowej
ArgumentMarshaler.
Naley zwrci uwag, e abstrakcyjna funkcja set oczekuje argumentu String, ale implementacja
w BooleanArgumentMarshaller nie korzysta z niego. Umieciem tam ten argument, poniewa
wiedziaem, e StringArgumentMarshaller oraz IntegerArgumentMarshaller bd z niego
korzystay.
Nastpnie chciaem przenie metod get do BooleanArgumentMarshaler. Przenoszenie funkcji
get jest zawsze mao eleganckie, poniewa zwracanym typem zawsze musi by Object i w tym
przypadku jest on rzutowany na Boolean.
public boolean getBoolean(char arg) {
Args.ArgumentMarshaler am = booleanArgs.get(arg);
return am != null && (Boolean)am.get();
}

Aby kod si kompilowa, dodaem funkcj get do ArgumentMarshaler.


private abstract class ArgumentMarshaler {
...
public Object get() {
return null;
}
}

Kod ten kompiluje si i oczywicie nie przechodzi testw. Aby testy znw przechodziy, wystarczyo
utworzy abstrakcyjn metod get i zaimplementowa j w BooleanAgumentMarshaler.
private abstract class ArgumentMarshaler {
protected boolean booleanValue = false;
...
public abstract Object get();
}

UDANE OCZYSZCZANIE KODU

231

private class BooleanArgumentMarshaler extends ArgumentMarshaler {


public void set(String s) {
booleanValue = true;
}
public Object get() {
return booleanValue;
}
}

Kolejny raz testy dziaaj prawidowo. Tak wic get i set s przeniesione do BooleanArgument
Marshaler! Pozwolio mi to usun star funkcj getBoolean z ArgumentMarshaler, przenie
chronion zmienn booleanValue do BooleanArgumentMarshaler i zmieni jej widoczno
na private.
Ten sam szablon zmian zastosowaem dla typu String. Przeniosem funkcje set i get, usunem
nieuywane funkcje i przeniosem zmienne.
private void setStringArg(char argChar) throws ArgsException {
currentArgument++;
try {
stringArgs.get(argChar).set(args[currentArgument]);
} catch (ArrayIndexOutOfBoundsException e) {
valid = false;
errorArgumentId = argChar;
errorCode = ErrorCode.MISSING_STRING;
throw new ArgsException();
}
}
...
public String getString(char arg) {
Args.ArgumentMarshaler am = stringArgs.get(arg);
return am == null ? "" : (String) am.get();
}
...
private abstract class ArgumentMarshaler {
private int integerValue;
public void setInteger(int i) {
integerValue = i;
}
public int getInteger() {
return integerValue;
}
public abstract void set(String s);
}

public abstract Object get();


private class BooleanArgumentMarshaler extends ArgumentMarshaler {
private boolean booleanValue = false;
public void set(String s) {
booleanValue = true;
}
public Object get() {
return booleanValue;
}
}

232

ROZDZIA 14.

private class StringArgumentMarshaler extends ArgumentMarshaler {


private String stringValue = "";
public void set(String s) {
stringValue = s;
}
public Object get() {
return stringValue;
}
}
private class IntegerArgumentMarshaler extends ArgumentMarshaler {
public void set(String s) {
}
public Object get() {
return null;
}
}
}

Na koniec powtrzyem proces dla argumentw integer. Byo to nieco bardziej skomplikowane, poniewa liczby cakowite musz by analizowane, a operacja parse moe zgosi wyjtek.
Jednak wynik jest lepszy, poniewa caa obsuga NumberFormatException zostaa umieszczona
w IntegerArgumentMarshaler.
private boolean isIntArg(char argChar) {return intArgs.containsKey(argChar);}
private void setIntArg(char argChar) throws ArgsException {
currentArgument++;
String parameter = null;
try {
parameter = args[currentArgument];
intArgs.get(argChar).set(parameter);
} catch (ArrayIndexOutOfBoundsException e) {
valid = false;
errorArgumentId = argChar;
errorCode = ErrorCode.MISSING_INTEGER;
throw new ArgsException();
} catch (ArgsException e) {
valid = false;
errorArgumentId = argChar;
errorParameter = parameter;
errorCode = ErrorCode.INVALID_INTEGER;
throw e;
}
}
...
private void setBooleanArg(char argChar) {
try {
booleanArgs.get(argChar).set("true");
} catch (ArgsException e) {
}
}
...
public int getInt(char arg) {
Args.ArgumentMarshaler am = intArgs.get(arg);
return am == null ? 0 : (Integer) am.get();
}
...
private abstract class ArgumentMarshaler {

UDANE OCZYSZCZANIE KODU

233

public abstract void set(String s) throws ArgsException;


public abstract Object get();

}
...
private class IntegerArgumentMarshaler extends ArgumentMarshaler {
private int intValue = 0;
public void set(String s) throws ArgsException {
try {
intValue = Integer.parseInt(s);
} catch (NumberFormatException e) {
throw new ArgsException();
}
}
public Object get() {
return intValue;
}
}

Oczywicie, testy stale byy realizowane prawidowo. Nastpnie zajem si usuniciem rnych
odwzorowa z algorytmu. Dziki temu cay system sta si bardziej oglny. Jednak nie mogem ich
usun przez proste skasowanie, poniewa system przestaby dziaa. Zamiast tego dodaem now
zmienn Map do klasy ArgumentMarshaler i jedna po drugiej zmieniaem metody, aby korzystay
z niej zamiast z oryginalnych zmiennych.
public class Args {
...
private Map<Character, ArgumentMarshaler> booleanArgs =
new HashMap<Character, ArgumentMarshaler>();
private Map<Character, ArgumentMarshaler> stringArgs =
new HashMap<Character, ArgumentMarshaler>();
private Map<Character, ArgumentMarshaler> intArgs =
new HashMap<Character, ArgumentMarshaler>();
private Map<Character, ArgumentMarshaler> marshalers =
new HashMap<Character, ArgumentMarshaler>();
...
private void parseBooleanSchemaElement(char elementId) {
ArgumentMarshaler m = new BooleanArgumentMarshaler();
booleanArgs.put(elementId, m);
marshalers.put(elementId, m);
}
private void parseIntegerSchemaElement(char elementId) {
ArgumentMarshaler m = new IntegerArgumentMarshaler();
intArgs.put(elementId, m);
marshalers.put(elementId, m);
}
private void parseStringSchemaElement(char elementId) {
ArgumentMarshaler m = new StringArgumentMarshaler();
stringArgs.put(elementId, m);
marshalers.put(elementId, m);
}

Oczywicie, testy nadal byy wykonywane prawidowo. Nastpnie zmieniem isBooleanArg z takiej
postaci:
private boolean isBooleanArg(char argChar) {
return booleanArgs.containsKey(argChar);
}

234

ROZDZIA 14.

na tak:
private boolean isBooleanArg(char argChar) {
ArgumentMarshaler m = marshalers.get(argChar);
return m instanceof BooleanArgumentMarshaler;
}

Testy nadal byy wykonywane prawidowo. Tak wic wprowadziem t sam zmian do isIntArg
oraz isStringArg.
private boolean isIntArg(char argChar) {
ArgumentMarshaler m = marshalers.get(argChar);
return m instanceof IntegerArgumentMarshaler;
}
private boolean isStringArg(char argChar) {
ArgumentMarshaler m = marshalers.get(argChar);
return m instanceof StringArgumentMarshaler;
}

Testy nadal byy wykonywane prawidowo. Dziki temu wyeliminowaem powielone wywoania
marshalers.get:
private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
if (isBooleanArg(m))
setBooleanArg(argChar);
else if (isStringArg(m))
setStringArg(argChar);
else if (isIntArg(m))
setIntArg(argChar);
else
return false;
}

return true;

private boolean isIntArg(ArgumentMarshaler m) {


return m instanceof IntegerArgumentMarshaler;
}
private boolean isStringArg(ArgumentMarshaler m) {
return m instanceof StringArgumentMarshaler;
}
private boolean isBooleanArg(ArgumentMarshaler m) {
return m instanceof BooleanArgumentMarshaler;
}

Sprawio to, e nie byo powodu istnienia metod isxxxArgs. Wic wbudowaem je:
private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
if (m instanceof BooleanArgumentMarshaler)
setBooleanArg(argChar);
else if (m instanceof StringArgumentMarshaler)
setStringArg(argChar);
else if (m instanceof IntegerArgumentMarshaler)
setIntArg(argChar);
else
return false;
return true;
}

UDANE OCZYSZCZANIE KODU

235

Nastpnie zaczem korzysta ze zmiennych marshalers w funkcjach set, usuwajc jednoczenie kontenery Map. Zaczem od argumentw logicznych.
private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
if (m instanceof BooleanArgumentMarshaler)
setBooleanArg(m);
else if (m instanceof StringArgumentMarshaler)
setStringArg(argChar);
else if (m instanceof IntegerArgumentMarshaler)
setIntArg(argChar);
else
return false;
return true;
}
...
private void setBooleanArg(ArgumentMarshaler m) {
try {
m.set("true"); // Byo: booleanArgs.get(argChar).set("true");
} catch (ArgsException e) {
}
}

Testy nadal dziaay prawidowo, wic to samo zrobiem z typami String i integer. Pozwolio mi
to zintegrowa cz nieeleganckiego kodu zarzdzania wyjtkami w funkcji setArgument.
private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
try {
if (m instanceof BooleanArgumentMarshaler)
setBooleanArg(m);
else if (m instanceof StringArgumentMarshaler)
setStringArg(m);
else if (m instanceof IntegerArgumentMarshaler)
setIntArg(m);
else
return false;
} catch (ArgsException e) {
valid = false;
errorArgumentId = argChar;
throw e;
}
return true;
}
private void setIntArg(ArgumentMarshaler m) throws ArgsException {
currentArgument++;
String parameter = null;
try {
parameter = args[currentArgument];
m.set(parameter);
} catch (ArrayIndexOutOfBoundsException e) {
errorCode = ErrorCode.MISSING_INTEGER;
throw new ArgsException();
} catch (ArgsException e) {
errorParameter = parameter;
errorCode = ErrorCode.INVALID_INTEGER;
throw e;
}
}

236

ROZDZIA 14.

private void setStringArg(ArgumentMarshaler m) throws ArgsException {


currentArgument++;
try {
m.set(args[currentArgument]);
} catch (ArrayIndexOutOfBoundsException e) {
errorCode = ErrorCode.MISSING_STRING;
throw new ArgsException();
}
}

Byem bliski usunicia starych zmiennych Map. Musiaem jeszcze zmieni funkcj getBoolean
z postaci:
public boolean getBoolean(char arg) {
Args.ArgumentMarshaler am = booleanArgs.get(arg);
return am != null && (Boolean) am.get();
}

na tak:
public boolean getBoolean(char arg) {
Args.ArgumentMarshaler am = marshalers.get(arg);
boolean b = false;
try {
b = am != null && (Boolean) am.get();
} catch (ClassCastException e) {
b = false;
}
return b;
}

Ta ostatnia zmiana moga by niespodziank. Dlaczego nagle zdecydowaem si obsugiwa


ClassCastException? Powodem byo to, e miaem zestaw testw jednostkowych i osobne testy
akceptacyjne w FitNesse. Okazao si, e testy FitNesse sprawdzaj, i gdy wywoamy getBoolean
na argumencie innym ni logiczny, otrzymamy warto false. Testy jednostkowe tego nie robiy.
Do tego momentu uruchamiaem wycznie testy jednostkowe2.
Ta ostatnia zmiana pozwolia mi wyodrbni ostatnie zastosowanie mapy boolean:
private void parseBooleanSchemaElement(char elementId) {
ArgumentMarshaler m = new BooleanArgumentMarshaler();

booleanArgs.put(elementId, m);
marshalers.put(elementId, m);
}

Teraz mogem usun zmienn Map dla typu boolean.


public class Args {
...

private Map<Character, ArgumentMarshaler> booleanArgs =


new HashMap<Character, ArgumentMarshaler>();
private Map<Character, ArgumentMarshaler> stringArgs =
new HashMap<Character, ArgumentMarshaler>();
private Map<Character, ArgumentMarshaler> intArgs =
new HashMap<Character, ArgumentMarshaler>();
2

Aby zapobiec tego typu niespodziankom, dodaem nowy test jednostkowy wywoujcy wszystkie testy FitNesse.

UDANE OCZYSZCZANIE KODU

237

private Map<Character, ArgumentMarshaler> marshalers =


new HashMap<Character, ArgumentMarshaler>();
...

Nastpnie przeniosem argumenty String i integer w ten sam sposb i nieco wyczyciem kod
obsugi argumentw logicznych.
private void parseBooleanSchemaElement(char elementId) {
marshalers.put(elementId, new BooleanArgumentMarshaler());
}
private void parseIntegerSchemaElement(char elementId) {
marshalers.put(elementId, new IntegerArgumentMarshaler());
}
private void parseStringSchemaElement(char elementId) {
marshalers.put(elementId, new StringArgumentMarshaler());
}
...
public String getString(char arg) {
Args.ArgumentMarshaler am = marshalers.get(arg);
try {
return am == null ? "" : (String) am.get();
} catch (ClassCastException e) {
return "";
}
}
public int getInt(char arg) {
Args.ArgumentMarshaler am = marshalers.get(arg);
try {
return am == null ? 0 : (Integer) am.get();
} catch (Exception e) {
return 0;
}
}
...
public class Args {
...
private Map<Character, ArgumentMarshaler> stringArgs =
new HashMap<Character, ArgumentMarshaler>();
private Map<Character, ArgumentMarshaler> intArgs =
new HashMap<Character, ArgumentMarshaler>();
private Map<Character, ArgumentMarshaler> marshalers =
new HashMap<Character, ArgumentMarshaler>();
...

Wbudowaem trzy metody parse, poniewa nie realizoway ju zbyt wielu operacji:
private void parseSchemaElement(String element) throws ParseException {
char elementId = element.charAt(0);
String elementTail = element.substring(1);
validateSchemaElementId(elementId);
if (isBooleanSchemaElement(elementTail))
marshalers.put(elementId, new BooleanArgumentMarshaler());
else if (isStringSchemaElement(elementTail))
marshalers.put(elementId, new StringArgumentMarshaler());
else if (isIntegerSchemaElement(elementTail)) {
marshalers.put(elementId, new IntegerArgumentMarshaler());
} else {
throw new ParseException(String.format(
"Argument: %c ma niewaciwy format: %s.", elementId, elementTail), 0);
}
}

238

ROZDZIA 14.

Spjrzmy teraz, jak wyglda to w caoci. Na listingu 14.12 przedstawiona jest bieca posta klasy Args.
L I S T I N G 1 4 . 1 2 . Args.java (po pierwszej przebudowie)
package com.objectmentor.utilities.getopts;
import java.text.ParseException;
import java.util.*;
public class Args {
private String schema;
private String[] args;
private boolean valid = true;
private Set<Character> unexpectedArguments = new TreeSet<Character>();
private Map<Character, ArgumentMarshaler> marshalers =
new HashMap<Character, ArgumentMarshaler>();
private Set<Character> argsFound = new HashSet<Character>();
private int currentArgument;
private char errorArgumentId = '\0';
private String errorParameter = "TILT";
private ErrorCode errorCode = ErrorCode.OK;
private enum ErrorCode {
OK, MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER, UNEXPECTED_ARGUMENT}
public Args(String schema, String[] args) throws ParseException {
this.schema = schema;
this.args = args;
valid = parse();
}
private boolean parse() throws ParseException {
if (schema.length() == 0 && args.length == 0)
return true;
parseSchema();
try {
parseArguments();
} catch (ArgsException e) {
}
return valid;
}
private boolean parseSchema() throws ParseException {
for (String element : schema.split(",")) {
if (element.length() > 0) {
String trimmedElement = element.trim();
parseSchemaElement(trimmedElement);
}
}
return true;
}
private void parseSchemaElement(String element) throws ParseException {
char elementId = element.charAt(0);
String elementTail = element.substring(1);
validateSchemaElementId(elementId);
if (isBooleanSchemaElement(elementTail))
marshalers.put(elementId, new BooleanArgumentMarshaler());
else if (isStringSchemaElement(elementTail))
marshalers.put(elementId, new StringArgumentMarshaler());
else if (isIntegerSchemaElement(elementTail)) {
marshalers.put(elementId, new IntegerArgumentMarshaler());
} else {

UDANE OCZYSZCZANIE KODU

239

throw new ParseException(String.format(


"Argument: %c ma niewaciwy format: %s.", elementId, elementTail), 0);
}
}
private void validateSchemaElementId(char elementId) throws ParseException {
if (!Character.isLetter(elementId)) {
throw new ParseException(
"Zy znak:" + elementId + "w formacie Args: " + schema, 0);
}
}
private boolean isStringSchemaElement(String elementTail) {
return elementTail.equals("*");
}
private boolean isBooleanSchemaElement(String elementTail) {
return elementTail.length() == 0;
}
private boolean isIntegerSchemaElement(String elementTail) {
return elementTail.equals("#");
}
private boolean parseArguments() throws ArgsException {
for (currentArgument=0; currentArgument<args.length; currentArgument++) {
String arg = args[currentArgument];
parseArgument(arg);
}
return true;
}
private void parseArgument(String arg) throws ArgsException {
if (arg.startsWith("-"))
parseElements(arg);
}
private void parseElements(String arg) throws ArgsException {
for (int i = 1; i < arg.length(); i++)
parseElement(arg.charAt(i));
}
private void parseElement(char argChar) throws ArgsException {
if (setArgument(argChar))
argsFound.add(argChar);
else {
unexpectedArguments.add(argChar);
errorCode = ErrorCode.UNEXPECTED_ARGUMENT;
valid = false;
}
}
private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
try {
if (m instanceof BooleanArgumentMarshaler)
setBooleanArg(m);
else if (m instanceof StringArgumentMarshaler)
setStringArg(m);
else if (m instanceof IntegerArgumentMarshaler)
setIntArg(m);
else
return false;
} catch (ArgsException e) {

240

ROZDZIA 14.

valid = false;
errorArgumentId = argChar;
throw e;

}
return true;

private void setIntArg(ArgumentMarshaler m) throws ArgsException {


currentArgument++;
String parameter = null;
try {
parameter = args[currentArgument];
m.set(parameter);
} catch (ArrayIndexOutOfBoundsException e) {
errorCode = ErrorCode.MISSING_INTEGER;
throw new ArgsException();
} catch (ArgsException e) {
errorParameter = parameter;
errorCode = ErrorCode.INVALID_INTEGER;
throw e;
}
}
private void setStringArg(ArgumentMarshaler m) throws ArgsException {
currentArgument++;
try {
m.set(args[currentArgument]);
} catch (ArrayIndexOutOfBoundsException e) {
errorCode = ErrorCode.MISSING_STRING;
throw new ArgsException();
}
}
private void setBooleanArg(ArgumentMarshaler m) {
try {
m.set("true");
} catch (ArgsException e) {
}
}
public int cardinality() {
return argsFound.size();
}
public String usage() {
if (schema.length() > 0)
return "-[" + schema + "]";
else
return "";
}
public String errorMessage() throws Exception {
switch (errorCode) {
case OK:
throw new Exception("TILT: Niedostpne.");
case UNEXPECTED_ARGUMENT:
return unexpectedArgumentMessage();
case MISSING_STRING:
return String.format("Nie mona znale parametru znakowego dla -%c.",
errorArgumentId);
case INVALID_INTEGER:
return String.format("Argument -%c oczekuje liczby cakowitej, a by '%s'.",
errorArgumentId, errorParameter);
case MISSING_INTEGER:

UDANE OCZYSZCZANIE KODU

241

return String.format("Nie mona znale parametru cakowitego dla -%c.",


errorArgumentId);

}
return "";

private String unexpectedArgumentMessage() {


StringBuffer message = new StringBuffer("Argument(y) -");
for (char c : unexpectedArguments) {
message.append(c);
}
message.append(" nieoczekiwany.");
return message.toString();
}
public boolean getBoolean(char arg) {
Args.ArgumentMarshaler am = marshalers.get(arg);
boolean b = false;
try {
b = am != null && (Boolean) am.get();
} catch (ClassCastException e) {
b = false;
}
return b;
}
public String getString(char arg) {
Args.ArgumentMarshaler am = marshalers.get(arg);
try {
return am == null ? "" : (String) am.get();
} catch (ClassCastException e) {
return "";
}
}
public int getInt(char arg) {
Args.ArgumentMarshaler am = marshalers.get(arg);
try {
return am == null ? 0 : (Integer) am.get();
} catch (Exception e) {
return 0;
}
}
public boolean has(char arg) {
return argsFound.contains(arg);
}
public boolean isValid() {
return valid;
}
private class ArgsException extends Exception {
}
private abstract class ArgumentMarshaler {
public abstract void set(String s) throws ArgsException;
public abstract Object get();
}
private class BooleanArgumentMarshaler extends ArgumentMarshaler {
private boolean booleanValue = false;
public void set(String s) {
booleanValue = true;
}

242

ROZDZIA 14.

public Object get() {


return booleanValue;
}
}
private class StringArgumentMarshaler extends ArgumentMarshaler {
private String stringValue = "";
public void set(String s) {
stringValue = s;
}
public Object get() {
return stringValue;
}
}
private class IntegerArgumentMarshaler extends ArgumentMarshaler {
private int intValue = 0;
public void set(String s) throws ArgsException {
try {
intValue = Integer.parseInt(s);
} catch (NumberFormatException e) {
throw new ArgsException();
}
}
public Object get() {
return intValue;
}
}
}

Po tak duej iloci pracy efekt troch rozczarowuje. Struktura jest nieco lepsza, ale nadal mamy
te wszystkie zmienne na grze, nadal w setArgument znajduje si okropna konstrukcja case,
a wszystkie funkcje set s naprawd brzydkie. Nie wspominam tu nawet o przetwarzaniu bdw.
Przed nami wci sporo pracy.
Naprawd chciaem usun instrukcje case zalene od typu z setArgument [G23]. Chciabym, aby
setArgument by jedynym wywoaniem ArgumentMarshaler.set. Oznacza to, e powinienem
przenie setIntArg, setStringArg oraz setBooleanArg do odpowiednich klas dziedziczcych
po ArgumentMarshaler. Ale wtedy pojawi si problem.
Jeeli spojrzymy na setIntArg, okae si, e korzysta z dwch zmiennych instancyjnych: args
oraz currentArg. Aby przenie setIntArg do BooleanArgumentMarshaler, musiabym przekazywa zarwno args, jak i currentArgs jako argumenty funkcji. Jest to nieeleganckie rozwizanie
[F1]. Wol przekazywa jeden argument zamiast dwch. Na szczcie istnieje proste rozwizanie.
Moemy skonwertowa tablic args na list i przekaza do funkcji set jej Iterator. Poniszy
kod wykonuje dziesi krokw, pozwalajcych na spenienie testu po tecie. Zamieszcz tu tylko
ostateczny wynik. Czytelnik powinien zorientowa si, jakie byy poszczeglne mae kroki.
public class Args {
private String schema;

private String[] args;


private boolean valid = true;
private Set<Character> unexpectedArguments = new TreeSet<Character>();

UDANE OCZYSZCZANIE KODU

243

private Map<Character, ArgumentMarshaler> marshalers =


new HashMap<Character, ArgumentMarshaler>();
private Set<Character> argsFound = new HashSet<Character>();
private Iterator<String> currentArgument;
private char errorArgumentId = '\0';
private String errorParameter = "TILT";
private ErrorCode errorCode = ErrorCode.OK;
private List<String> argsList;
private enum ErrorCode {
OK, MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER, UNEXPECTED_ARGUMENT}
public Args(String schema, String[] args) throws ParseException {
this.schema = schema;
argsList = Arrays.asList(args);
valid = parse();
}
private boolean parse() throws ParseException {
if (schema.length() == 0 && argsList.size() == 0)
return true;
parseSchema();
try {
parseArguments();
} catch (ArgsException e) {
}
return valid;
}
--private boolean parseArguments() throws ArgsException {
for (currentArgument = argsList.iterator(); currentArgument.hasNext();) {
String arg = currentArgument.next();
parseArgument(arg);
}
return true;
}
--private void setIntArg(ArgumentMarshaler m) throws ArgsException {
String parameter = null;
try {
parameter = currentArgument.next();
m.set(parameter);
} catch (NoSuchElementException e) {
errorCode = ErrorCode.MISSING_INTEGER;
throw new ArgsException();
} catch (ArgsException e) {
errorParameter = parameter;
errorCode = ErrorCode.INVALID_INTEGER;
throw e;
}
}
private void setStringArg(ArgumentMarshaler m) throws ArgsException {
try {
m.set(currentArgument.next());
} catch (NoSuchElementException e) {
errorCode = ErrorCode.MISSING_STRING;
throw new ArgsException();
}
}

244

ROZDZIA 14.

Byy to proste zmiany, dziki ktrym testy nadal dziaay. Teraz zaczem przenosi funkcje set do
odpowiednich klas podrzdnych. Na pocztek konieczne byo wprowadzenie nastpujcej zmiany
w setArgument:
private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
if (m == null)
return false;
try {
if (m instanceof BooleanArgumentMarshaler)
setBooleanArg(m);
else if (m instanceof StringArgumentMarshaler)
setStringArg(m);
else if (m instanceof IntegerArgumentMarshaler)
setIntArg(m);

else
return false;

} catch (ArgsException e) {
valid = false;
errorArgumentId = argChar;
throw e;
}
return true;

Zmiana ta bya istotna, poniewa zamierzaem cakowicie wyeliminowa acuch if-else. Dlatego
chciaem wyodrbni z niego kontrol bdu.
Teraz moemy rozpocz przenoszenie funkcji set. Funkcja setBooleanArg jest najprostsza, wic
zaczniemy od niej. Naszym celem jest zmiana setBooleanArg na zwyke przekazanie wywoania
do BooleanArgumentMarshaler.
private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
if (m == null)
return false;
try {
if (m instanceof BooleanArgumentMarshaler)
setBooleanArg(m, currentArgument);
else if (m instanceof StringArgumentMarshaler)
setStringArg(m);
else if (m instanceof IntegerArgumentMarshaler)
setIntArg(m);
} catch (ArgsException e) {
valid = false;
errorArgumentId = argChar;
throw e;
}
return true;
}
--private void setBooleanArg(ArgumentMarshaler m,
Iterator<String> currentArgument)
throws ArgsException {

try {
m.set("true");

catch (ArgsException e) {
}
}

UDANE OCZYSZCZANIE KODU

245

Czy nie dodawalimy wczeniej tego przetwarzania wyjtku? Dodawanie elementw, aby je pniej
usun, jest dosy czste w procesie przebudowywania. Wykonywanie niewielkich krokw oraz
konieczno zachowania dziaania testw powoduj, e czsto przesuwa si fragmenty kodu. Przebudowa podobna jest do ukadania kostki Rubika. Do osignicia wikszego celu uywa si wielu
maych krokw. Kady krok pozwala na nastpny.
Dlaczego przekazywalimy iterator, gdy setBooleanArg wcale go nie potrzebuje? Poniewa
setIntArg oraz setStringArg bd go potrzeboway! Oprcz tego, poniewa chciaem uruchamia te funkcje przez metod abstrakcyjn w ArgumentMarshaler, musiaem przekaza j do
setBooleanArg.
Obecnie setBooleanArg jest bezuyteczna. Jeeli mielibymy funkcj set w ArgumentMarshaler,
moglibymy wywoywa j bezporednio. Czas wic napisa t funkcj! Pierwszym krokiem jest
napisanie nowej metody abstrakcyjnej w ArgumentMarshaler.
private abstract class ArgumentMarshaler {
public abstract void set(Iterator<String> currentArgument)
throws ArgsException;
public abstract void set(String s) throws ArgsException;
public abstract Object get();
}

Oczywicie, spowoduje to, e wszystkie klasy pochodne przestan dziaa. Dlatego konieczne bdzie
dodanie w kadej z nich nowej metody.
private class BooleanArgumentMarshaler extends ArgumentMarshaler {
private boolean booleanValue = false;
public void set(Iterator<String> currentArgument) throws ArgsException {
booleanValue = true;
}
public void set(String s) {

booleanValue = true;
}
public Object get() {
return booleanValue;
}
}
private class StringArgumentMarshaler extends ArgumentMarshaler {
private String stringValue = "";
public void set(Iterator<String> currentArgument) throws ArgsException {
}
public void set(String s) {
stringValue = s;
}
public Object get() {
return stringValue;
}
}
private class IntegerArgumentMarshaler extends ArgumentMarshaler {
private int intValue = 0;

246

ROZDZIA 14.

public void set(Iterator<String> currentArgument) throws ArgsException {


}

public void set(String s) throws ArgsException {


try {
intValue = Integer.parseInt(s);
} catch (NumberFormatException e) {
throw new ArgsException();
}
public Object get() {
return intValue;
}

Teraz moemy wyeliminowa setBooleanArg!


private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
if (m == null)
return false;
try {
if (m instanceof BooleanArgumentMarshaler)
m.set(currentArgument);
else if (m instanceof StringArgumentMarshaler)
setStringArg(m);
else if (m instanceof IntegerArgumentMarshaler)
setIntArg(m);
} catch (ArgsException e) {
valid = false;
errorArgumentId = argChar;
throw e;
}
return true;
}

Wszystkie testy wykonuj si prawidowo, a funkcja set zostaa przeniesiona do BooleanArgument


Marshaler! Teraz moemy wykona to samo dla typu String i integer.
private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
if (m == null)
return false;
try {
if (m instanceof BooleanArgumentMarshaler)
m.set(currentArgument);
else if (m instanceof StringArgumentMarshaler)
m.set(currentArgument);
else if (m instanceof IntegerArgumentMarshaler)
m.set(currentArgument);
} catch (ArgsException e) {
valid = false;
errorArgumentId = argChar;
throw e;
}
return true;
}
--private class StringArgumentMarshaler extends ArgumentMarshaler {
private String stringValue = "";
public void set(Iterator<String> currentArgument) throws ArgsException {

UDANE OCZYSZCZANIE KODU

247

try {
stringValue = currentArgument.next();
} catch (NoSuchElementException e) {
errorCode = ErrorCode.MISSING_STRING;
throw new ArgsException();
}
}
public void set(String s) {
}
public Object get() {
return stringValue;
}
}
private class IntegerArgumentMarshaler extends ArgumentMarshaler {
private int intValue = 0;
public void set(Iterator<String> currentArgument) throws ArgsException {
String parameter = null;
try {
parameter = currentArgument.next();
set(parameter);
} catch (NoSuchElementException e) {
errorCode = ErrorCode.MISSING_INTEGER;
throw new ArgsException();
} catch (ArgsException e) {
errorParameter = parameter;
errorCode = ErrorCode.INVALID_INTEGER;
throw e;
}
}
public void set(String s) throws ArgsException {
try {
intValue = Integer.parseInt(s);
} catch (NumberFormatException e) {
throw new ArgsException();
}
}
public Object get() {
return intValue;
}
}

I teraz coup de grace wybr w zalenoci od typu moe by usunity! Touch!


private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
if (m == null)
return false;
try {
m.set(currentArgument);
return true;
} catch (ArgsException e) {
valid = false;
errorArgumentId = argChar;
throw e;
}
}

248

ROZDZIA 14.

Moemy ju usun cz niepotrzebnych funkcji w IntegerArgumentMarshaler i nieco posprzta.


private class IntegerArgumentMarshaler extends ArgumentMarshaler {
private int intValue = 0
public void set(Iterator<String> currentArgument) throws ArgsException {
String parameter = null;
try {
parameter = currentArgument.next();
intValue = Integer.parseInt(parameter);
} catch (NoSuchElementException e) {
errorCode = ErrorCode.MISSING_INTEGER;
throw new ArgsException();
} catch (NumberFormatException e) {
errorParameter = parameter;
errorCode = ErrorCode.INVALID_INTEGER;
throw new ArgsException();
}
}
public Object get() {
return intValue;
}
}

Moemy rwnie przeksztaci ArgumentMarshaler w interfejs.


private interface ArgumentMarshaler {
void set(Iterator<String> currentArgument) throws ArgsException;
Object get();
}

Teraz zobaczymy, jak atwo mona doda nowy typ argumentu do naszej struktury. Powinno to
wymaga niewielu zmian, ktre to zmiany powinny by izolowane. Na pocztek zacznijmy od dodania nowego przypadku testowego, ktry sprawdza, czy argument double dziaa prawidowo.
public void testSimpleDoublePresent() throws Exception {
Args args = new Args("x##", new String[] {"-x","42.3"});
assertTrue(args.isValid());
assertEquals(1, args.cardinality());
assertTrue(args.has('x'));
assertEquals(42.3, args.getDouble('x'), .001);
}

Teraz wyczycimy kod analizy schematu i dodamy operacj wykrywania ## dla argumentw typu
double.
private void parseSchemaElement(String element) throws ParseException {
char elementId = element.charAt(0);
String elementTail = element.substring(1);
validateSchemaElementId(elementId);
if (elementTail.length() == 0)
marshalers.put(elementId, new BooleanArgumentMarshaler());
else if (elementTail.equals("*"))
marshalers.put(elementId, new StringArgumentMarshaler());
else if (elementTail.equals("#"))
marshalers.put(elementId, new IntegerArgumentMarshaler());
else if (elementTail.equals("##"))
marshalers.put(elementId, new DoubleArgumentMarshaler());
else

UDANE OCZYSZCZANIE KODU

249

throw new ParseException(String.format(


"Argument: %c ma niewaciwy format: %s.", elementId, elementTail), 0);
}

Nastpnie napiszemy klas DoubleArgumentMarshaler.


private class DoubleArgumentMarshaler implements ArgumentMarshaler {
private double doubleValue = 0;
public void set(Iterator<String> currentArgument) throws ArgsException {
String parameter = null;
try {
parameter = currentArgument.next();
doubleValue = Double.parseDouble(parameter);
} catch (NoSuchElementException e) {
errorCode = ErrorCode.MISSING_DOUBLE;
throw new ArgsException();
} catch (NumberFormatException e) {
errorParameter = parameter;
errorCode = ErrorCode.INVALID_DOUBLE;
throw new ArgsException();
}
}
public Object get() {
return doubleValue;
}
}

Wymusza to na nas dodanie nowej wartoci ErrorCode.


private enum ErrorCode {
OK, MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER, UNEXPECTED_ARGUMENT,
MISSING_DOUBLE, INVALID_DOUBLE}

Potrzebujemy rwnie funkcji getDouble.


public double getDouble(char arg) {
Args.ArgumentMarshaler am = marshalers.get(arg);
try {
return am == null ? 0 : (Double) am.get();
} catch (Exception e) {
return 0.0;
}
}

Wszystkie testy s wykonywane prawidowo! Poszo cakiem bezbolenie. Teraz moemy si


upewni, e kod przetwarzania bdw dziaa prawidowo. Nastpny przypadek testowy kontroluje,
czy zostanie zadeklarowany bd, jeeli do argumentu ## zostanie przekazany cig niedajcy si
zanalizowa.
public void testInvalidDouble() throws Exception {
Args args = new Args("x##", new String[] {"-x","Czterdzieci dwa"});
assertFalse(args.isValid());
assertEquals(0, args.cardinality());
assertFalse(args.has('x'));
assertEquals(0, args.getInt('x'));
assertEquals("Argument -x oczekuje wartoci double, a byo 'Czterdzieci dwa'.",
args.errorMessage());
}
---

250

ROZDZIA 14.

public String errorMessage() throws Exception {


switch (errorCode) {
case OK:
throw new Exception("TILT: Niedostpne.");
case UNEXPECTED_ARGUMENT:
return unexpectedArgumentMessage();
case MISSING_STRING:
return String.format("Nie mona znale parametru znakowego dla -%c.",
errorArgumentId);
case INVALID_INTEGER:
return String.format("Argument -%c oczekuje liczby cakowitej, a by '%s'.",
errorArgumentId, errorParameter);
case MISSING_INTEGER:
return String.format("Nie mona znale parametru cakowitego dla -%c.",
errorArgumentId);
case INVALID_DOUBLE:
return String.format("Argument -%c oczekuje liczby double, a by '%s'.",
errorArgumentId, errorParameter);
case MISSING_DOUBLE:
return String.format("Nie mona znale parametru double dla -%c.",
errorArgumentId);
}
return "";
}

Wszystkie testy s wykonywane prawidowo! Nastpny test sprawdza, czy prawidowo wykrywamy
brakujcy argument double.
public void testMissingDouble() throws Exception {
Args args = new Args("x##", new String[]{"-x"});
assertFalse(args.isValid());
assertEquals(0, args.cardinality());
assertFalse(args.has('x'));
assertEquals(0.0, args.getDouble('x'), 0.01);
assertEquals("Nie mona znale parametru double dla -x.",
args.errorMessage());
}

Ten rwnie dziaa, jak oczekiwalimy. Napisalimy go tylko dla zachowania kompletnoci kodu.
Kod wyjtkw jest dosy brzydki i nie naley naprawd do klasy Args. Zgaszamy rwnie
ParseException, ktry na pewno nie naley do nas. Poczymy wic wszystkie wyjtki w jedn
klas ArgsException i przeniesiemy j do osobnego moduu.
public class ArgsException extends Exception {
private char errorArgumentId = '\0';
private String errorParameter = "TILT";
private ErrorCode errorCode = ErrorCode.OK;
public ArgsException() {}
public ArgsException(String message) {super(message);}
public enum ErrorCode {
OK, MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER, UNEXPECTED_ARGUMENT,
MISSING_DOUBLE, INVALID_DOUBLE}
}
--public class Args {
...
private char errorArgumentId = '\0';

UDANE OCZYSZCZANIE KODU

251

private String errorParameter = "TILT";


private ArgsException.ErrorCode errorCode = ArgsException.ErrorCode.OK;
private List<String> argsList;
public Args(String schema, String[] args) throws ArgsException {
this.schema = schema;
argsList = Arrays.asList(args);
valid = parse();
}
private boolean parse() throws ArgsException {
if (schema.length() == 0 && argsList.size() == 0)
return true;
parseSchema();
try {
parseArguments();
} catch (ArgsException e) {
}
return valid;
}
private boolean parseSchema() throws ArgsException {
...
}
private void parseSchemaElement(String element) throws ArgsException {
...
else
throw new ArgsException(
String.format("Argument: %c ma niewaciwy format: %s.",
elementId,elementTail));
}
private void validateSchemaElementId(char elementId) throws ArgsException {
if (!Character.isLetter(elementId)) {
throw new ArgsException(
"Zy znak:" + elementId + "w formacie Args: " + schema);
}
}
...
private void parseElement(char argChar) throws ArgsException {
if (setArgument(argChar))
argsFound.add(argChar);
else {
unexpectedArguments.add(argChar);
errorCode = ArgsException.ErrorCode.UNEXPECTED_ARGUMENT;
valid = false;
}
}
...
private class StringArgumentMarshaler implements ArgumentMarshaler {
private String stringValue = "";
public void set(Iterator<String> currentArgument) throws ArgsException {
try {
stringValue = currentArgument.next();
} catch (NoSuchElementException e) {
errorCode = ArgsException.ErrorCode.MISSING_STRING;
throw new ArgsException();

252

ROZDZIA 14.

}
}
public Object get() {
return stringValue;
}
}
private class IntegerArgumentMarshaler implements ArgumentMarshaler {
private int intValue = 0;
public void set(Iterator<String> currentArgument) throws ArgsException {
String parameter = null;
try {
parameter = currentArgument.next();
intValue = Integer.parseInt(parameter);
} catch (NoSuchElementException e) {
errorCode = ArgsException.ErrorCode.MISSING_INTEGER;
throw new ArgsException();
} catch (NumberFormatException e) {
errorParameter = parameter;
errorCode = ArgsException.ErrorCode.INVALID_INTEGER;
throw new ArgsException();
}
}
public Object get() {
return intValue;
}
}
private class DoubleArgumentMarshaler implements ArgumentMarshaler {
private double doubleValue = 0;
public void set(Iterator<String> currentArgument) throws ArgsException {
String parameter = null;
try {
parameter = currentArgument.next();
doubleValue = Double.parseDouble(parameter);
} catch (NoSuchElementException e) {
errorCode = ArgsException.ErrorCode.MISSING_DOUBLE;
throw new ArgsException();
} catch (NumberFormatException e) {
errorParameter = parameter;
errorCode = ArgsException.ErrorCode.INVALID_DOUBLE;
throw new ArgsException();
}
}
public Object get() {
return doubleValue;
}
}
}

Teraz jest adnie. Jedynym wyjtkiem zgaszanym przez Args jest ArgsException. Przeniesienie
ArgsException do osobnego moduu spowodowao, e moemy przenie wiele dodatkowego kodu
obsugi bdw do tego moduu, osobnego wzgldem moduu Args. Zapewnia on naturalne i oczywiste miejsce na umieszczenie caego tego kodu i naprawd pomaga wyczyci modu Args.

UDANE OCZYSZCZANIE KODU

253

Dziki temu wyjtki i kod obsugi bdw pozostaj cakowicie oddzielone od moduu Args (patrz
listingi 14.13 do 14.16). Zostao to osignite w serii 30 maych krokw, pomidzy ktrymi testy
stale byy wykonywane prawidowo.
L I S T I N G 1 4 . 1 3 . ArgsTest.java
package com.objectmentor.utilities.args;
import junit.framework.TestCase;
public class ArgsTest extends TestCase {
public void testCreateWithNoSchemaOrArguments() throws Exception {
Args args = new Args("", new String[0]);
assertEquals(0, args.cardinality());
}
public void testWithNoSchemaButWithOneArgument() throws Exception {
try {
new Args("", new String[]{"-x"});
fail();
} catch (ArgsException e) {
assertEquals(ArgsException.ErrorCode.UNEXPECTED_ARGUMENT,
e.getErrorCode());
assertEquals('x', e.getErrorArgumentId());
}
}
public void testWithNoSchemaButWithMultipleArguments() throws Exception {
try {
new Args("", new String[]{"-x", "-y"});
fail();
} catch (ArgsException e) {
assertEquals(ArgsException.ErrorCode.UNEXPECTED_ARGUMENT,
e.getErrorCode());
assertEquals('x', e.getErrorArgumentId());
}
}
public void testNonLetterSchema() throws Exception {
try {
new Args("*", new String[]{});
fail("Konstruktor Args powinien zgosi wyjtek");
} catch (ArgsException e) {
assertEquals(ArgsException.ErrorCode.INVALID_ARGUMENT_NAME,
e.getErrorCode());
assertEquals('*', e.getErrorArgumentId());
}
}
public void testInvalidArgumentFormat() throws Exception {
try {
new Args("f~", new String[]{});
fail("Konstruktor Args powinien zgosi wyjtek");
} catch (ArgsException e) {
assertEquals(ArgsException.ErrorCode.INVALID_FORMAT, e.getErrorCode());
assertEquals('f', e.getErrorArgumentId());
}
}
public void testSimpleBooleanPresent() throws Exception {
Args args = new Args("x", new String[]{"-x"});
assertEquals(1, args.cardinality());

254

ROZDZIA 14.

assertEquals(true, args.getBoolean('x'));
}
public void testSimpleStringPresent() throws Exception {
Args args = new Args("x*", new String[]{"-x", "param"});
assertEquals(1, args.cardinality());
assertTrue(args.has('x'));
assertEquals("param", args.getString('x'));
}
public void testMissingStringArgument() throws Exception {
try {
new Args("x*", new String[]{"-x"});
fail();
} catch (ArgsException e) {
assertEquals(ArgsException.ErrorCode.MISSING_STRING, e.getErrorCode());
assertEquals('x', e.getErrorArgumentId());
}
}
public void testSpacesInFormat() throws Exception {
Args args = new Args("x, y", new String[]{"-xy"});
assertEquals(2, args.cardinality());
assertTrue(args.has('x'));
assertTrue(args.has('y'));
}
public void testSimpleIntPresent() throws Exception {
Args args = new Args("x#", new String[]{"-x", "42"});
assertEquals(1, args.cardinality());
assertTrue(args.has('x'));
assertEquals(42, args.getInt('x'));
}
public void testInvalidInteger() throws Exception {
try {
new Args("x#", new String[]{"-x", "Czterdzieci dwa"});
fail();
} catch (ArgsException e) {
assertEquals(ArgsException.ErrorCode.INVALID_INTEGER, e.getErrorCode());
assertEquals('x', e.getErrorArgumentId());
assertEquals("Czterdzieci dwa", e.getErrorParameter());
}
}
public void testMissingInteger() throws Exception {
try {
new Args("x#", new String[]{"-x"});
fail();
} catch (ArgsException e) {
assertEquals(ArgsException.ErrorCode.MISSING_INTEGER, e.getErrorCode());
assertEquals('x', e.getErrorArgumentId());
}
}
public void testSimpleDoublePresent() throws Exception {
Args args = new Args("x##", new String[]{"-x", "42.3"});
assertEquals(1, args.cardinality());
assertTrue(args.has('x'));
assertEquals(42.3, args.getDouble('x'), .001);
}
public void testInvalidDouble() throws Exception {
try {

UDANE OCZYSZCZANIE KODU

255

new Args("x##", new String[]{"-x", "Czterdzieci dwa"});


fail();
} catch (ArgsException e) {
assertEquals(ArgsException.ErrorCode.INVALID_DOUBLE, e.getErrorCode());
assertEquals('x', e.getErrorArgumentId());
assertEquals("Czterdzieci dwa", e.getErrorParameter());
}
}
public void testMissingDouble() throws Exception {
try {
new Args("x##", new String[]{"-x"});
fail();
} catch (ArgsException e) {
assertEquals(ArgsException.ErrorCode.MISSING_DOUBLE, e.getErrorCode());
assertEquals('x', e.getErrorArgumentId());
}
}
}

L I S T I N G 1 4 . 1 4 . ArgsExceptionTest.java
public class ArgsExceptionTest extends TestCase {
public void testUnexpectedMessage() throws Exception {
ArgsException e =
new ArgsException(ArgsException.ErrorCode.UNEXPECTED_ARGUMENT,
'x', null);
assertEquals("Argument -x nieoczekiwany.", e.errorMessage());
}
public void testMissingStringMessage() throws Exception {
ArgsException e = new ArgsException(ArgsException.ErrorCode.MISSING_STRING,
'x', null);
assertEquals("Nie mona znale parametru znakowego dla -x.", e.errorMessage());
}
public void testInvalidIntegerMessage() throws Exception {
ArgsException e =
new ArgsException(ArgsException.ErrorCode.INVALID_INTEGER,
'x', "Czterdzieci dwa");
assertEquals("Argument -x oczekuje liczby cakowitej 'Czterdzieci dwa'.",
e.errorMessage());
}
public void testMissingIntegerMessage() throws Exception {
ArgsException e =
new ArgsException(ArgsException.ErrorCode.MISSING_INTEGER, 'x', null);
assertEquals("Nie mona znale parametru cakowitego dla -x.", e.errorMessage());
}
public void testInvalidDoubleMessage() throws Exception {
ArgsException e = new ArgsException(ArgsException.ErrorCode.INVALID_DOUBLE,
'x', "Czterdzieci dwa");
assertEquals("Argument -x oczekuje liczby double, a by 'Czterdzieci dwa'.",
e.errorMessage());
}
public void testMissingDoubleMessage() throws Exception {
ArgsException e = new ArgsException(ArgsException.ErrorCode.MISSING_DOUBLE,
'x', null);
assertEquals("Nie mona znale parametru double dla -x.", e.errorMessage());
}
}

256

ROZDZIA 14.

L I S T I N G 1 4 . 1 5 . ArgsException.java
public class ArgsException extends Exception {
private char errorArgumentId = '\0';
private String errorParameter = "TILT";
private ErrorCode errorCode = ErrorCode.OK;
public ArgsException() {}
public ArgsException(String message) {super(message);}
public ArgsException(ErrorCode errorCode) {
this.errorCode = errorCode;
}
public ArgsException(ErrorCode errorCode, String errorParameter) {
this.errorCode = errorCode;
this.errorParameter = errorParameter;
}
public ArgsException(ErrorCode errorCode, char errorArgumentId,
String errorParameter) {
this.errorCode = errorCode;
this.errorParameter = errorParameter;
this.errorArgumentId = errorArgumentId;
}
public char getErrorArgumentId() {
return errorArgumentId;
}
public void setErrorArgumentId(char errorArgumentId) {
this.errorArgumentId = errorArgumentId;
}
public String getErrorParameter() {
return errorParameter;
}
public void setErrorParameter(String errorParameter) {
this.errorParameter = errorParameter;
}
public ErrorCode getErrorCode() {
return errorCode;
}
public void setErrorCode(ErrorCode errorCode) {
this.errorCode = errorCode;
}
public String errorMessage() throws Exception {
switch (errorCode) {
case OK:
throw new Exception("TILT: Niedostpne.");
case UNEXPECTED_ARGUMENT:
return String.format("Nieoczekiwany argument -%c.", errorArgumentId);
case MISSING_STRING:
return String.format("Nie mona znale parametru znakowego dla -%c.",
errorArgumentId);
case INVALID_INTEGER:
return String.format("Argument -%c oczekuje liczby cakowitej, a by '%s'.",
errorArgumentId, errorParameter);
case MISSING_INTEGER:

UDANE OCZYSZCZANIE KODU

257

return String.format("Nie mona znale parametru cakowitego dla -%c.",


errorArgumentId);
case INVALID_DOUBLE:
return String.format("Argument -%c oczekuje liczby double, a by '%s'.",
errorArgumentId, errorParameter);
case MISSING_DOUBLE:
return String.format("Nie mona znale parametru double dla -%c.",
errorArgumentId);
}
return "";
}
public enum ErrorCode {
OK, INVALID_FORMAT, UNEXPECTED_ARGUMENT, INVALID_ARGUMENT_NAME,
MISSING_STRING,
MISSING_INTEGER, INVALID_INTEGER,
MISSING_DOUBLE, INVALID_DOUBLE}
}

L I S T I N G 1 4 . 1 6 . Args.java
public class Args {
private String schema;
private Map<Character, ArgumentMarshaler> marshalers =
new HashMap<Character, ArgumentMarshaler>();
private Set<Character> argsFound = new HashSet<Character>();
private Iterator<String> currentArgument;
private List<String> argsList;
public Args(String schema, String[] args) throws ArgsException {
this.schema = schema;
argsList = Arrays.asList(args);
parse();
}
private void parse() throws ArgsException {
parseSchema();
parseArguments();
}
private boolean parseSchema() throws ArgsException {
for (String element : schema.split(",")) {
if (element.length() > 0) {
parseSchemaElement(element.trim());
}
}
return true;
}
private void parseSchemaElement(String element) throws ArgsException {
char elementId = element.charAt(0);
String elementTail = element.substring(1);
validateSchemaElementId(elementId);
if (elementTail.length() == 0)
marshalers.put(elementId, new BooleanArgumentMarshaler());
else if (elementTail.equals("*"))
marshalers.put(elementId, new StringArgumentMarshaler());
else if (elementTail.equals("#"))
marshalers.put(elementId, new IntegerArgumentMarshaler());
else if (elementTail.equals("##"))
marshalers.put(elementId, new DoubleArgumentMarshaler());
else
throw new ArgsException(ArgsException.ErrorCode.INVALID_FORMAT,
elementId, elementTail);

258

ROZDZIA 14.

}
private void validateSchemaElementId(char elementId) throws ArgsException {
if (!Character.isLetter(elementId)) {
throw new ArgsException(ArgsException.ErrorCode.INVALID_ARGUMENT_NAME,
elementId, null);
}
}
private void parseArguments() throws ArgsException {
for (currentArgument = argsList.iterator(); currentArgument.hasNext();) {
String arg = currentArgument.next();
parseArgument(arg);
}
}
private void parseArgument(String arg) throws ArgsException {
if (arg.startsWith("-"))
parseElements(arg);
}
private void parseElements(String arg) throws ArgsException {
for (int i = 1; i < arg.length(); i++)
parseElement(arg.charAt(i));
}
private void parseElement(char argChar) throws ArgsException {
if (setArgument(argChar))
argsFound.add(argChar);
else {
throw new ArgsException(ArgsException.ErrorCode.UNEXPECTED_ARGUMENT,
argChar, null);
}
}
private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
if (m == null)
return false;
try {
m.set(currentArgument);
return true;
} catch (ArgsException e) {
e.setErrorArgumentId(argChar);
throw e;
}
}
public int cardinality() {
return argsFound.size();
}
public String usage() {
if (schema.length() > 0)
return "-[" + schema + "]";
else
return "";
}
public boolean getBoolean(char arg) {
ArgumentMarshaler am = marshalers.get(arg);
boolean b = false;
try {
b = am != null && (Boolean) am.get();

UDANE OCZYSZCZANIE KODU

259

} catch (ClassCastException e) {
b = false;
}
return b;
}
public String getString(char arg) {
ArgumentMarshaler am = marshalers.get(arg);
try {
return am == null ? "" : (String) am.get();
} catch (ClassCastException e) {
return "";
}
}
public int getInt(char arg) {
ArgumentMarshaler am = marshalers.get(arg);
try {
return am == null ? 0 : (Integer) am.get();
} catch (Exception e) {
return 0;
}
}
public double getDouble(char arg) {
ArgumentMarshaler am = marshalers.get(arg);
try {
return am == null ? 0 : (Double) am.get();
} catch (Exception e) {
return 0.0;
}
}

public boolean has(char arg) {


return argsFound.contains(arg);
}

Zmiany w klasie Args w wikszoci polegay na usuniciu pewnych elementw. Sporo kodu przenielimy z Args do ArgsException. wietnie. Dodatkowo przenielimy klasy ArgumentMarshaler do
osobnych plikw. Jeszcze lepiej!
Wikszo dobrych projektw programowych dotyczy partycjonowania tworzenia odpowiednich miejsc, w ktrych mona umieci rne czci kodu. Takie rozdzielenie problemw powoduje, e kod jest znacznie prostszy do zrozumienia i utrzymania.
Szczeglnie interesujca jest metoda errorMessage z ArgsException. Jasne byo, e umieszczenie
komunikatw bdw formatowania w Args byo naruszeniem SRP. Klasa Args powinna dotyczy
przetwarzania argumentw, a nie formatowania komunikatw bdw. Jednak czy faktycznie ma
sens umieszczanie formatowania komunikatw bdw w ArgsException?
W zasadzie jest to kompromis. Uytkownicy, ktrzy nie chc korzysta z komunikatw bdw dostarczanych przez ArgsException, mog napisa wasne. Jednak wygoda posiadania gotowych
komunikatw bdw nie jest bez znaczenia.
Teraz powinno by jasne, e jestemy bardzo blisko kocowego rozwizania, przedstawionego na
pocztku tego rozdziau. Ostatnie przeksztacenia pozostawi do wykonania jako wiczenie.

260

ROZDZIA 14.

Zakoczenie
Nie wystarczy napisa dziaajcy kod. Dziaajcy kod jest czsto fatalnie napisany. Programici,
ktrym wystarcza jedynie dziaajcy kod, dziaaj nieprofesjonalnie. Mog oni obawia si, e nie
bd mieli czasu na poprawienie struktury i projektu swojego kodu, ale nie mog si z tym zgodzi.
Nic nie ma tak gbokiego i w dugim czasie degradujcego wpywu na prowadzenie projektu,
jak zy kod. Ze harmonogramy mog by zmodyfikowane, ze wymagania przedefiniowane. Z
dynamik zespou da si poprawi. Jednak zy kod psuje si, stajc si coraz wikszym ciarem dla
zespou. Widziaem wiele zespow, ktre si rozsypyway, poniewa w popiechu tworzyy zoliwe bagnisko kodu, ktry na zawsze zdeterminowa ich przeznaczenie.
Oczywicie, nieudany kod mona wyczyci. Jednak jest to bardzo kosztowne. Wraz z psuciem si
kodu moduy przeplataj si ze sob, tworzc wiele ukrytych i zagmatwanych zalenoci. Wyszukiwanie i przerywanie starych zalenoci jest czasochonnym i mudnym zadaniem. Z drugiej
strony, zachowanie czystoci kodu jest wzgldnie atwe. Jeeli narobimy baaganu w kodzie rano,
atwo moemy go posprzta po poudniu. Jednak jeszcze atwiej jest posprzta od razu, gdy tylko
nabaaganimy.
Tak wic rozwizaniem jest cige zachowywanie czystoci i maksymalnej prostoty kodu. Nigdy nie
pozwalajmy, by zacz si psu.

UDANE OCZYSZCZANIE KODU

261

262

ROZDZIA 14.

ROZDZIA 15.

Struktura biblioteki JUnit

bibliotek jzyka Java. Jest on oparty na prostej koncepcji, precyJ zyjnej definicji i eleganckiej implementacji.
Jak jednak wyglda jego kod? W tym rozdziale poddamy
UNIT JEST JEDN Z NAJSYNNIEJSZYCH

krytyce przykadowy kod biblioteki JUnit.

263

Biblioteka JUnit
JUnit ma wielu autorw, ale powsta na podstawie pracy Kent Becka i Erica Gamma w czasie ich
wsplnego lotu do Atlanty. Kent chcia si nauczy jzyka Java, a Eric chcia pozna bibliotek
testw dla jzyka Smalltalk, ktrej autorem by Kent. Co mogo by bardziej naturalnego dla maniakw uwizionych w fotelach, jak tylko wycign swoje laptopy i zacz kodowa?1. Po trzech
godzinach pracy na wysokim puapie napisali oni podstawy JUnit.
Modu, ktrym si zajmiemy, jest sprytnym kawakiem kodu pomagajcym identyfikowa bdy porwnania napisw. Nosi on nazw ComparisonCompactor. Analizujc dwa rnice si napisy, takie
jak ABCDE i ABXDE, wskazuje on rnice przez wygenerowanie cigu takiego jak <...B[X]D...>.
Omwimy to dokadniej nieco dalej, a najlepszym wyjanieniem bd przypadki testowe. Spjrzmy
wic na kod z listingu 15.1, ktry szczegowo wyjania wymagania dotyczce tego moduu. Moemy
oceni struktur testw. Czy nie mogy by one prostsze lub bardziej oczywiste?
L I S T I N G 1 5 . 1 . ComparisonCompactorTest.java
package junit.tests.framework;
import junit.framework.ComparisonCompactor;
import junit.framework.TestCase;
public class ComparisonCompactorTest extends TestCase {
public void testMessage() {
String failure= new ComparisonCompactor(0, "b", "c").compact("a");
assertTrue("a oczekiwane:<[b]> a byo:<[c]>".equals(failure));
}
public void testStartSame() {
String failure= new ComparisonCompactor(1, "ba", "bc").compact(null);
assertEquals("oczekiwane:<b[a]> a byo:<b[c]>", failure);
}
public void testEndSame() {
String failure= new ComparisonCompactor(1, "ab", "cb").compact(null);
assertEquals("oczekiwane:<[a]b> a byo:<[c]b>", failure);
}
public void testSame() {
String failure= new ComparisonCompactor(1, "ab", "ab").compact(null);
assertEquals("oczekiwane:<ab> a byo:<ab>", failure);
}
public void testNoContextStartAndEndSame() {
String failure= new ComparisonCompactor(0, "abc", "adc").compact(null);
assertEquals("oczekiwane:<...[b]...> a byo:<...[d]...>", failure);
}
public void testStartAndEndContext() {
String failure= new ComparisonCompactor(1, "abc", "adc").compact(null);
assertEquals("oczekiwane:<a[b]c> a byo:<a[d]c>", failure);
}
public void testStartAndEndContextWithEllipses() {
String failure=
1

Kent Beck, JUnit Pocket Guide, OReilly 2004, s. 43.

264

ROZDZIA 15.

new ComparisonCompactor(1, "abcde", "abfde").compact(null);


assertEquals("oczekiwane:<...b[c]d...> a byo:<...b[f]d...>", failure);

public void testComparisonErrorStartSameComplete() {


String failure= new ComparisonCompactor(2, "ab", "abc").compact(null);
assertEquals("oczekiwane:<ab[]> a byo:<ab[c]>", failure);
}
public void testComparisonErrorEndSameComplete() {
String failure= new ComparisonCompactor(0, "bc", "abc").compact(null);
assertEquals("oczekiwane:<[]...> a byo:<[a]...>", failure);
}
public void testComparisonErrorEndSameCompleteContext() {
String failure= new ComparisonCompactor(2, "bc", "abc").compact(null);
assertEquals("oczekiwane:<[]bc> a byo:<[a]bc>", failure);
}
public void testComparisonErrorOverlapingMatches() {
String failure= new ComparisonCompactor(0, "abc", "abbc").compact(null);
assertEquals("oczekiwane:<...[]...> a byo:<...[b]...>", failure);
}
public void testComparisonErrorOverlapingMatchesContext() {
String failure= new ComparisonCompactor(2, "abc", "abbc").compact(null);
assertEquals("oczekiwane:<ab[]c> a byo:<ab[b]c>", failure);
}
public void testComparisonErrorOverlapingMatches2() {
String failure= new ComparisonCompactor(0, "abcdde", "abcde").compact(null);
assertEquals("oczekiwane:<...[d]...> a byo:<...[]...>", failure);
}
public void testComparisonErrorOverlapingMatches2Context() {
String failure=
new ComparisonCompactor(2, "abcdde", "abcde").compact(null);
assertEquals("oczekiwane:<...cd[d]e> a byo:<...cd[]e>", failure);
}
public void testComparisonErrorWithActualNull() {
String failure= new ComparisonCompactor(0, "a", null).compact(null);
assertEquals("oczekiwane:<a> a byo:<null>", failure);
}
public void testComparisonErrorWithActualNullContext() {
String failure= new ComparisonCompactor(2, "a", null).compact(null);
assertEquals("oczekiwane:<a> a byo:<null>", failure);
}
public void testComparisonErrorWithExpectedNull() {
String failure= new ComparisonCompactor(0, null, "a").compact(null);
assertEquals("oczekiwane:<null> a byo:<a>", failure);
}
public void testComparisonErrorWithExpectedNullContext() {
String failure= new ComparisonCompactor(2, null, "a").compact(null);
assertEquals("oczekiwane:<null> a byo:<a>", failure);
}

public void testBug609972() {


String failure= new ComparisonCompactor(10, "S&P500", "0").compact(null);
assertEquals("oczekiwane:<[S&P50]0> a byo:<[]0>", failure);
}

STRUKTURA BIBLIOTEKI JUNIT

265

Uruchomiem analiz pokrycia kodu ComparisionCompactor przy uyciu tych testw. Kod jest
w stu procentach pokryty. Kady wiersz kodu, kada instrukcja if i ptla for jest wykonywana
w tych testach. Daje mi to duy stopie pewnoci, e kod dziaa, i wzbudza we mnie duy respekt
dla umiejtnoci autorw.
Kod ComparisionCompactor jest zamieszczony na listingu 15.2. Warto powici troch czasu na
zapoznanie si z tym kodem. Chyba wszyscy zauwa, e jest adnie podzielony, wyrazisty i ma
prost struktur. Po zakoczeniu tej analizy zbierzemy wszystkie uwagi.
L I S T I N G 1 5 . 2 . ComparisonCompactor.java (Oryginalny)
package junit.framework;
public class ComparisonCompactor {
private static final String ELLIPSIS = "...";
private static final String DELTA_END = "]";
private static final String DELTA_START = "[";
private
private
private
private
private

int fContextLength;
String fExpected;
String fActual;
int fPrefix;
int fSuffix;

public ComparisonCompactor(int contextLength,


String expected,
String actual) {
fContextLength = contextLength;
fExpected = expected;
fActual = actual;
}
public String compact(String message) {
if (fExpected == null || fActual == null || areStringsEqual())
return Assert.format(message, fExpected, fActual);
findCommonPrefix();
findCommonSuffix();
String expected = compactString(fExpected);
String actual = compactString(fActual);
return Assert.format(message, expected, actual);
}
private String compactString(String source) {
String result = DELTA_START +
source.substring(fPrefix, source.length() fSuffix + 1) + DELTA_END;
if (fPrefix > 0)
result = computeCommonPrefix() + result;
if (fSuffix > 0)
result = result + computeCommonSuffix();
return result;
}
private void findCommonPrefix() {
fPrefix = 0;
int end = Math.min(fExpected.length(), fActual.length());
for (; fPrefix < end; fPrefix++) {
if (fExpected.charAt(fPrefix) != fActual.charAt(fPrefix))

266

ROZDZIA 15.

break;
}
}
private void findCommonSuffix() {
int expectedSuffix = fExpected.length() - 1;
int actualSuffix = fActual.length() - 1;
for (;
actualSuffix >= fPrefix && expectedSuffix >= fPrefix;
actualSuffix--, expectedSuffix--) {
if (fExpected.charAt(expectedSuffix) != fActual.charAt(actualSuffix))
break;
}
fSuffix = fExpected.length() - expectedSuffix;
}
private String computeCommonPrefix() {
return (fPrefix > fContextLength ? ELLIPSIS : "") +
fExpected.substring(Math.max(0, fPrefix - fContextLength),
fPrefix);
}
private String computeCommonSuffix() {
int end = Math.min(fExpected.length() - fSuffix + 1 + fContextLength,
fExpected.length());
return fExpected.substring(fExpected.length() - fSuffix + 1, end) +
(fExpected.length() - fSuffix + 1 < fExpected.length() fContextLength ? ELLIPSIS : "");
}
private boolean areStringsEqual() {
return fExpected.equals(fActual);
}
}

Mona mie pewne zastrzeenia do tego moduu. Znajduje si tu nieco dugich wyrae, kilka
dziwnych operacji +1 i tak dalej. Jednak oglnie jest to cakiem niezy modu. Ostatecznie mgby
on wyglda jak na listingu 15.3.
L I S T I N G 1 5 . 3 . ComparisonCompator.java (uszkodzony)
package junit.framework;
public class ComparisonCompactor {
private int ctxt;
private String s1;
private String s2;
private int pfx;
private int sfx;
public ComparisonCompactor(int ctxt, String s1, String s2) {
this.ctxt = ctxt;
this.s1 = s1;
this.s2 = s2;
}
public String compact(String msg) {
if (s1 == null || s2 == null || s1.equals(s2))
return Assert.format(msg, s1, s2);
pfx = 0;
for (; pfx < Math.min(s1.length(), s2.length()); pfx++) {

STRUKTURA BIBLIOTEKI JUNIT

267

if (s1.charAt(pfx) != s2.charAt(pfx))
break;
}
int sfx1 = s1.length() - 1;
int sfx2 = s2.length() - 1;
for (; sfx2 >= pfx && sfx1 >= pfx; sfx2--, sfx1--) {
if (s1.charAt(sfx1) != s2.charAt(sfx2))
break;
}
sfx = s1.length() - sfx1;
String cmp1 = compactString(s1);
String cmp2 = compactString(s2);
return Assert.format(msg, cmp1, cmp2);
}
private String compactString(String s) {
String result =
"[" + s.substring(pfx, s.length() - sfx + 1) + "]";
if (pfx > 0)
result = (pfx > ctxt ? "..." : "") +
s1.substring(Math.max(0, pfx - ctxt), pfx) + result;
if (sfx > 0) {
int end = Math.min(s1.length() - sfx + 1 + ctxt, s1.length());
result = result + (s1.substring(s1.length() - sfx + 1, end) +
(s1.length() - sfx + 1 < s1.length() - ctxt ? "..." : ""));
}
return result;
}
}

Mimo e autorzy nadali temu moduowi bardzo dobry ksztat, zasada skautw2 mwi, e powinnimy pozostawi go czystszym, ni go zastalimy. W jaki sposb mona poprawi oryginalny kod z listingu 15.2?
Pierwsz rzecz, jakiej nie cierpi, jest prefiks f przed zmiennymi skadowymi [N6]. Dzisiejsze
rodowiska powoduj, e taki sposb kodowania zakresu jest nadmiarowy. Wyeliminujemy wic
wszystkie f.
private
private
private
private
private

int contextLength;
String expected;
String actual;
int prefix;
int suffix;

Nastpnie mamy niehermetyzowany warunek na pocztku funkcji compact [G28].


public String compact(String message) {
if (expected == null || actual == null || areStringsEqual())
return Assert.format(message, expected, actual);
findCommonPrefix();
findCommonSuffix();
String expected = compactString(this.expected);
String actual = compactString(this.actual);
return Assert.format(message, expected, actual);
}

Patrz Zasada skautw w rozdziale 1.

268

ROZDZIA 15.

Warunek ten powinien by hermetyzowany, aby nasze intencje byy jasne. Z tego powodu wyodrbnimy metod, ktra je wyjania.
public String compact(String message) {
if (shouldNotCompact())
return Assert.format(message, expected, actual);

findCommonPrefix();
findCommonSuffix();
String expected = compactString(this.expected);
String actual = compactString(this.actual);
return Assert.format(message, expected, actual);

private boolean shouldNotCompact() {


return expected == null || actual == null || areStringsEqual();
}

Nie podoba mi si notacja this.expected oraz this.actual w funkcji compact. Powstaa ona,
gdy zmieniem nazw z fExpected na expected. Dlaczego w tej funkcji wykorzystane s nazwy
majce takie same nazwy co zmienne skadowe? Czy nie reprezentuj czego innego [N4]? Powinnimy uy bardziej znaczcych nazw.
String compactExpected = compactString(expected);
String compactActual = compactString(actual);

Wyraenia negatywne s nieco trudniejsze do zrozumienia ni pozytywne [G29]. Postawimy wic


wyraenie if na gowie i zmienimy sens warunku.
public String compact(String message) {
if (canBeCompacted()) {
findCommonPrefix();
findCommonSuffix();
String compactExpected = compactString(expected);
String compactActual = compactString(actual);
return Assert.format(message, compactExpected, compactActual);
} else {
return Assert.format(message, expected, actual);
}
}
private boolean canBeCompacted() {
return expected != null && actual != null && !areStringsEqual();
}

Nazwa funkcji jest do dziwna [N7]. Cho wykonuje ona zmniejszanie cigw, w rzeczywistoci
moe tego nie robi, jeeli canBeCompacted zwrci false. Tak wic nazwanie tej funkcji compact
powoduje ukrycie efektu ubocznego kontroli bdw. Naley rwnie zauway, e funkcja zwraca
sformatowany komunikat, a nie po prostu zmniejszony cig. Dlatego nazwa tej funkcji powinna
brzmie formatCompactedComparison. Bdzie znacznie czytelniejsza, gdy zczymy j z argumentem funkcji:
public String formatCompactedComparison(String message) {

Faktyczne zmniejszanie oczekiwanego i otrzymanego cigu znajduje si w ciele instrukcji if. Powinnimy wyodrbni t operacj do osobnej metody o nazwie compactExpectedAndActual. Jednak chcemy,
aby funkcja formatCompactedComparison wykonywaa cae formatowanie. Funkcja compact...
nie powinna robi nic poza zmniejszaniem [G30]. Podzielmy kod w nastpujcy sposb:

STRUKTURA BIBLIOTEKI JUNIT

269

...
private String compactExpected;
private String compactActual;
...
public String formatCompactedComparison(String message) {
if (canBeCompacted()) {
compactExpectedAndActual();
return Assert.format(message, compactExpected, compactActual);
} else {
return Assert.format(message, expected, actual);
}
}
private void compactExpectedAndActual() {
findCommonPrefix();
findCommonSuffix();
compactExpected = compactString(expected);
compactActual = compactString(actual);
}

Trzeba zauway, e wymaga to wypromowania compactExpected oraz compactActual jako


zmiennych skadowych. Nie lubi sposobu, w jaki w ostatnich dwch wierszach nowej funkcji s
zwracane zmienne, a w dwch pierwszych nie. Nie jest tu uywana spjna konwencja [G11].
Powinnimy zmieni findCommonPrefix oraz findCommonSuffix, aby zwracay warto przedrostka i przyrostka.
private void compactExpectedAndActual() {
prefixIndex = findCommonPrefix();
suffixIndex = findCommonSuffix();
compactExpected = compactString(expected);
compactActual = compactString(actual);
}
private int findCommonPrefix() {
int prefixIndex = 0;
int end = Math.min(expected.length(), actual.length());
for (; prefixIndex < end; prefixIndex++) {
if (expected.charAt(prefixIndex) != actual.charAt(prefixIndex))
break;
}
return prefixIndex;
}
private int findCommonSuffix() {
int expectedSuffix = expected.length() - 1;
int actualSuffix = actual.length() - 1;
for (; actualSuffix >= prefixIndex && expectedSuffix >= prefixIndex;
actualSuffix--, expectedSuffix--) {
if (expected.charAt(expectedSuffix) != actual.charAt(actualSuffix))
break;
}
return expected.length() - expectedSuffix;
}

Powinnimy rwnie zmieni nazwy zmiennych skadowych na bardziej trafne [N1]; w kocu obie
s indeksami.
Dokadna analiza pokazuje, e findCommonSuffix ujawnia ukryte sprzenie czasowe [G31]; zmienna zaley od wyliczenia wartoci prefixIndex przez findCommonPrefix. Jeeli te dwie funkcje
zostayby wywoane w niewaciwej kolejnoci, mona by si spodziewa trudnej sesji debugowania.

270

ROZDZIA 15.

Tak wic, aby ujawni to sprzenie czasowe, funkcja findCommonSuffix bdzie oczekiwaa jako
argumentu prefixIndex.
private void compactExpectedAndActual() {
prefixIndex = findCommonPrefix();
suffixIndex = findCommonSuffix(prefixIndex);
compactExpected = compactString(expected);
compactActual = compactString(actual);
}
private int findCommonSuffix(int prefixIndex) {
int expectedSuffix = expected.length() - 1;
int actualSuffix = actual.length() - 1;
for (; actualSuffix >= prefixIndex && expectedSuffix >= prefixIndex;
actualSuffix--, expectedSuffix--) {
if (expected.charAt(expectedSuffix) != actual.charAt(actualSuffix))
break;
}
return expected.length() - expectedSuffix;
}

Nie jestem zadowolony z tego kodu. Przekazywanie argumentu prefixIndex jest zbyt dowolne [G32].
Pozwala okreli kolejno, ale nie wyjania potrzeby takiego uporzdkowania. Inny programista
moe wycofa wprowadzone przez nas zmiany, poniewa nie ma adnej wskazwki, e parametr
jest naprawd potrzebny. Przyjmijmy wic inne podejcie.
private void compactExpectedAndActual() {
findCommonPrefixAndSuffix();
compactExpected = compactString(expected);
compactActual = compactString(actual);
}
private void findCommonPrefixAndSuffix() {
findCommonPrefix();
int expectedSuffix = expected.length() - 1;
int actualSuffix = actual.length() - 1;
for (;
actualSuffix >= prefixIndex && expectedSuffix >= prefixIndex;
actualSuffix--, expectedSuffix-) {
if (expected.charAt(expectedSuffix) != actual.charAt(actualSuffix))
break;
}
suffixIndex = expected.length() - expectedSuffix;
}
private void findCommonPrefix() {
prefixIndex = 0;
int end = Math.min(expected.length(), actual.length());
for (; prefixIndex < end; prefixIndex++)
if (expected.charAt(prefixIndex) != actual.charAt(prefixIndex))
break;
}

Umiecilimy findCommonPrefix oraz findCommonSuffix z powrotem tam, gdzie byy wczeniej, zmieniajc nazw findCommonSuffix na findCommonPrefixAndSuffix i wywoujc findCommonPrefix
przed wykonaniem jakichkolwiek innych operacji. Uwypukla to czasow natur sprzenia tych
dwch funkcji w bardziej dramatyczny sposb ni w poprzednim rozwizaniu. Wskazuje rwnie,
jak nieudana jest funkcja findCommonPrefixAndSuffix. Zajmijmy si jej wyczyszczeniem.

STRUKTURA BIBLIOTEKI JUNIT

271

private void findCommonPrefixAndSuffix() {


findCommonPrefix();
int suffixLength = 1;
for (; !suffixOverlapsPrefix(suffixLength); suffixLength++) {
if (charFromEnd(expected, suffixLength) !=
charFromEnd(actual, suffixLength))
break;
}
suffixIndex = suffixLength;
}
private char charFromEnd(String s, int i) {
return s.charAt(s.length()-i);}
private boolean suffixOverlapsPrefix(int suffixLength) {
return actual.length() - suffixLength < prefixLength ||
expected.length() - suffixLength < prefixLength;
}

Teraz jest znacznie lepiej. Ujawnione zostao, e suffixIndex jest w rzeczywistoci dugoci
przyrostka, czyli nie jest dobrze nazwana. To samo dotyczy prefixIndex, czyli w tym przypadku
index i length s synonimami. Kod zyska na spjnoci, gdy bdziemy korzysta z length. Jednak
problem polega na tym, e suffixIndex nie zaczyna si od zera zaczyna si od 1, czyli nie jest
to prawdziwa dugo. Przyczyna si to rwnie do tego, e w computeCommonSuffix s uyte te
wszystkie +1 [G33]. Poprawmy wic to. Wynik jest zamieszczony na listingu 15.4.
L I S T I N G 1 5 . 4 . ComparisonCompactor.java (tymczasowy)
public class ComparisonCompactor {
...
private int suffixLength;
...
private void findCommonPrefixAndSuffix() {
findCommonPrefix();
suffixLength = 0;
for (; !suffixOverlapsPrefix(suffixLength); suffixLength++) {
if (charFromEnd(expected, suffixLength) !=
charFromEnd(actual, suffixLength))
break;
}
}
private char charFromEnd(String s, int i) {
return s.charAt(s.length() - i - 1);
}
private boolean suffixOverlapsPrefix(int suffixLength) {
return actual.length() - suffixLength <= prefixLength ||
expected.length() - suffixLength <= prefixLength;
}
...
private String compactString(String source) {
String result =
DELTA_START +
source.substring(prefixLength, source.length() - suffixLength) +
DELTA_END;

272

ROZDZIA 15.

if (prefixLength > 0)
result = computeCommonPrefix() + result;
if (suffixLength > 0)
result = result + computeCommonSuffix();
return result;
}
...
private String computeCommonSuffix() {
int end = Math.min(expected.length() - suffixLength +
contextLength, expected.length()
);
return
expected.substring(expected.length() - suffixLength, end) +
(expected.length() - suffixLength <
expected.length() - contextLength ?
ELLIPSIS : "");
}

Zamienilimy +1 w computeCommonSuffix na -1 w charFromEnd, gdzie jest to cakiem sensowne,


oraz dwa operatory <= w suffixOverlapsPrefix, gdzie rwnie jest to sensowne. Pozwolio to
nam zmieni nazw suffixIndex na suffixLength, co znacznie poprawio czytelno kodu.
Istnieje tu jednak problem. Po wyeliminowaniu +1 pozosta w compactString nastpujcy wiersz:
if (suffixLength > 0)

Spjrzmy na listing 15.4. Poniewa suffixLength ma teraz warto o jeden mniejsz ni wczeniej, powinnimy zmieni operator > na >=. Jednak nie miao to sensu. Teraz ma! Jeeli wczeniej
nie miao to sensu, by to prawdopodobnie bd. C, waciwie nie by to bd. Dokadna analiza
pokazuje, e instrukcja if zapobiega teraz doczaniu przyrostkw o zerowej dugoci. Przed
wprowadzeniem przez nas zmian instrukcja if nie dziaaa, poniewa zmienna suffixIndex nigdy
nie przyjmowaa wartoci mniejszej od jeden!
Dotyczy to obu instrukcji if w compactString! Wyglda na to, e mona je wyeliminowa.
Zakomentujemy je wic i uruchomimy testy. Zostay wykonane prawidowo! Moemy wic zmodyfikowa compactString, eliminujc nadmiarowe instrukcje if, co znacznie upraszcza t
funkcj [G9].
private String compactString(String source) {
return
computeCommonPrefix() +
DELTA_START +
source.substring(prefixLength, source.length() - suffixLength) +
DELTA_END +
computeCommonSuffix();
}

Teraz jest znacznie lepiej! Widzimy, e funkcja compactString po prostu czy ze sob fragmenty
tekstu. Z pewnoci moemy pokaza to jeszcze janiej, wykona jeszcze wiele maych usprawnie.
Jednak zamiast pokazywa pozostae zmiany, zamiecimy ostateczny wynik na listingu 15.5.

STRUKTURA BIBLIOTEKI JUNIT

273

L I S T I N G 1 5 . 5 . ComparisonCompactor.java (ostateczny)
package junit.framework;
public class ComparisonCompactor {
private static final String ELLIPSIS = "...";
private static final String DELTA_END = "]";
private static final String DELTA_START = "[";
private
private
private
private
private

int contextLength;
String expected;
String actual;
int prefixLength;
int suffixLength;

public ComparisonCompactor(
int contextLength, String expected, String actual
) {
this.contextLength = contextLength;
this.expected = expected;
this.actual = actual;
}
public String formatCompactedComparison(String message) {
String compactExpected = expected;
String compactActual = actual;
if (shouldBeCompacted()) {
findCommonPrefixAndSuffix();
compactExpected = compact(expected);
compactActual = compact(actual);
}
return Assert.format(message, compactExpected, compactActual);
}
private boolean shouldBeCompacted() {
return !shouldNotBeCompacted();
}
private boolean shouldNotBeCompacted() {
return expected == null ||
actual == null ||
expected.equals(actual);
}
private void findCommonPrefixAndSuffix() {
findCommonPrefix();
suffixLength = 0;
for (; !suffixOverlapsPrefix(); suffixLength++) {
if (charFromEnd(expected, suffixLength) !=
charFromEnd(actual, suffixLength)
)
break;
}
}
private char charFromEnd(String s, int i) {
return s.charAt(s.length() - i - 1);
}
private boolean suffixOverlapsPrefix() {
return actual.length() - suffixLength <= prefixLength ||
expected.length() - suffixLength <= prefixLength;
}

274

ROZDZIA 15.

private void findCommonPrefix() {


prefixLength = 0;
int end = Math.min(expected.length(), actual.length());
for (; prefixLength < end; prefixLength++)
if (expected.charAt(prefixLength) != actual.charAt(prefixLength))
break;
}
private String compact(String s) {
return new StringBuilder()
.append(startingEllipsis())
.append(startingContext())
.append(DELTA_START)
.append(delta(s))
.append(DELTA_END)
.append(endingContext())
.append(endingEllipsis())
.toString();
}
private String startingEllipsis() {
return prefixLength > contextLength ? ELLIPSIS : "";
}
private String startingContext() {
int contextStart = Math.max(0, prefixLength - contextLength);
int contextEnd = prefixLength;
return expected.substring(contextStart, contextEnd);
}
private String delta(String s) {
int deltaStart = prefixLength;
int deltaEnd = s.length() - suffixLength;
return s.substring(deltaStart, deltaEnd);
}
private String endingContext() {
int contextStart = expected.length() - suffixLength;
int contextEnd =
Math.min(contextStart + contextLength, expected.length());
return expected.substring(contextStart, contextEnd);
}
private String endingEllipsis() {
return (suffixLength > contextLength ? ELLIPSIS : "");
}
}

Kod ten jest cakiem adny. Modu ma oddzielon grup funkcji analitycznych i inn grup funkcji
syntetycznych. S one posortowane topologicznie, dziki czemu definicja kadej z funkcji znajduje
si zaraz po jej uyciu. Wszystkie funkcje analityczne znajduj si na pocztku, a funkcje syntetyczne na kocu.
Dokadniejsza analiza kodu pokazuje, e cofnem kilka decyzji, jakie podjem we wczeniejszej
czci tego rozdziau. Na przykad wbudowaem kilka wyodrbnionych metod z powrotem do
formatCompactedComparison i zmieniem sens wyraenia shouldNotBeCompacted. Jest to typowe. Czsto jedna przebudowa prowadzi do anulowania wczeniejszej. Przebudowa jest procesem
iteracyjnym penym prb i bdw procesem, ktry wynika z naszego denia do profesjonalizmu.

STRUKTURA BIBLIOTEKI JUNIT

275

Zakoczenie
Wypenilimy regu skauta. Pozostawilimy modu czyciejszy, ni go dostalimy. Nie mona powiedzie, e wczeniej nie by czysty. Jego autorzy wietnie si spisali. Jednak nie ma moduu, ktrego
nie mona poprawi, a kady z nas jest odpowiedzialny za pozostawienie kodu w lepszym stanie,
ni by wczeniej.

276

ROZDZIA 15.

ROZDZIA 16.

Przebudowa klasy SerialDate

http://www.jfree.org/jcommon/index.php, znajdziemy tam bibliotek


J JCommon. Gboko w tej bibliotece
znajdziemy pakiet o nazwie
. Z kolei w paEELI OTWORZYMY STRON

org.jfree.date

kiecie znajduje si klasa o nazwie SerialDate. W niniejszym rozdziale przeanalizujemy t klas.


Autorem klasy SerialDate jest David Gilbert. Jasne jest, e David to dowiadczony i kompetentny
programista. Jak moemy zauway, podczas tworzenia tego kodu wykaza si dyscyplin i profesjonalizmem. W przypadku wszystkich zastosowa jest to dobry kod. A ja zamierzam rozebra
go na czci.
Nie jest to z mojej strony adna zoliwo. Nie uwaam rwnie, e jestem znacznie lepszym programist ni David, wic mam prawo przeprowadzi sd nad jego kodem. Gdyby kto przeanalizowa mj kod, jestem pewny, e znalazby wiele fragmentw, do ktrych miaby uwagi.

277

Nie, nie jest to akt zoliwoci ani arogancji. Moim zamiarem jest tylko profesjonalna recenzja. Jest
to co, co kady z nas powinien wykonywa bez problemw. Jest to rwnie co, z czego powinnimy by zadowoleni, gdy kto to robi dla nas. Tylko przez tak krytyk moemy si czego nauczy.
Lekarze to robi. Piloci to robi. Prawnicy to robi. Rwnie my, programici, powinnimy si nauczy recenzowa dziea innych.
Jeszcze jedno sowo na temat Davida Gilberta. David to wicej ni tylko dobry programista. Mia
odwag i dobr wol, aby za darmo zaoferowa swj kod spoecznoci. Udostpni go dla wszystkich, umoliwiajc jego uycie i publiczne badanie. Jest to wietnie zrobiony kod!
Klasa SerialDate (listing B.1 w dodatku B) reprezentuje dat w jzyku Java. Po co nam kolejna
klasa reprezentujca dat, skoro Java posiada ju midzy innymi klasy java.util.Date oraz
java.util.Calendar? Autor napisa t klas w odpowiedzi na problemy, z jakimi czsto si spotykalimy. Otwierajcy komentarz Javadoc (wiersz 67.) jasno to wyjania. Moemy mie wtpliwoci na temat jego intencji, ale ja kiedy musiaem zmaga si z tym problemem i z radoci powitaem
klas, ktra operuje na datach zamiast na czasie.

Na pocztek uruchamiamy
W klasie SerialDateTests (listing B.2 w dodatku B) znajduje si kilka testw jednostkowych.
Wszystkie te testy s wykonywane prawidowo. Niestety, szybki przegld testw pokazuje, e nie
testuj wszystkiego [T1]. Na przykad wyszukanie zastosowa dla metody MonthCodeToQuarter
(wiersz 334.) wykazuje, e nie jest uywana [F4]. Zatem testy jednostkowe nie testuj jej.
Skorzystaem zatem z programu Clover, aby sprawdzi, co jest pokryte przez testy jednostkowe, a co nie.
Clover wykaza, e testy jednostkowe wykonuj tylko 91 ze 185 instrukcji wykonywalnych w klasie
SerialDate (~50 procent) [T2]. Mapa pokrycia wygldaa jak dziurawy koc, z wielkimi poaciami
niewykonywanego kodu rozsianymi po caej klasie.
Musiaem dokadnie zrozumie, jak przebudowa t klas. Nie mogem tego robi bez znacznie
wikszego pokrycia kodu testami. Napisaem wic wasny zestaw zupenie niezalenych testw jednostkowych (listing B.4 w dodatku B).
Przegldajc te testy, mona zauway, e wiele z nich jest zakomentowanych. Testy te nie wykonuj
si prawidowo. Reprezentuj one te funkcje, ktre wedug mnie powinna posiada klasa SerialDate.
Dlatego w czasie przebudowy SerialDate bdziemy dy do uruchomienia rwnie tych testw.
Nawet pomimo zakomentowania czci testw Clover raportuje, e nowe testy jednostkowe wykonuj 170 ze 180 (92 procent) instrukcji wykonywalnych. Jest to cakiem niezy wynik i uwaam, e
jestem w stanie uzyska wyszy.
Kilka pierwszych zakomentowanych testw (wiersze od 23. do 63.) byo z mojej strony nieco przesadzonych. Program nie by zaprojektowany, aby przeszed te testy, ale dziaanie takie wydawao
mi si oczywiste [G2]. Nie jestem pewien, dlaczego metoda testWeekdayCodeToString bya napisana

278

ROZDZIA 16.

jako pierwsza, ale poniewa ju jest, wydaje si oczywiste, e powinna rozpoznawa wielko
liter. Napisanie tych testw byo bardzo atwe [T3]. Spowodowanie, aby si wykonyway, byo
rwnie proste po prostu zmieniem wiersze 259. i 263., aby wykorzystywana bya metoda
equalsIgnoreCase.
Pozostawiem zakomentowane testy w wierszach 32. i 45., poniewa nie byo dla mnie jasne, czy
skrty tues oraz thurs powinny by obsugiwane.
Testy z wierszy 153. i 154. nie byy wykonywane prawidowo. Jasne jest, e powinny [G2]. Moemy
atwo to poprawi, razem z testami z wierszy od 163. do 213., przez wprowadzenie nastpujcych
zmian do funkcji stringToMonthCode:
457
458
459
460
461
462
463
464
465
466
467
468

if ((result < 1) || (result > 12)) {


result = -1;
for (int i = 0; i < monthNames.length; i++) {
if (s.equalsIgnoreCase(shortMonthNames[i])) {
result = i + 1;
break;
}
if (s.equalsIgnoreCase(monthNames[i])) {
result = i + 1;
break;
}
}
}

Zakomentowany test z wiersza 318. ujawnia bd w metodzie getFollowingDayOfWeek (wiersz


672.). 25 grudnia 2004 by sobot. Nastpn sobot by 1 stycznia 2005. Jednak po uruchomieniu
testw okazuje si, e getFollowingDayOfWeek zwraca 25 grudnia jako sobot nastpujc po 25
grudnia. Oczywicie jest to bd [G3], [T1]. Problem znajduje si w wierszu 685. Jest to typowy
bd warunku granicznego [T1]. Powinien on wyglda nastpujco:
685

if (baseDOW >= targetWeekday) {

Warto zauway, e funkcja ta bya celem wczeniejszej poprawki. Historia zmian wykazuje
(wiersz 43.), e te bdy byy poprawione w getPreviousDayOfWeek, getFollowingDayOfWeek
oraz getNearestDayOfWeek [T6].
Test jednostkowy testGetNearestDayOfWeek (wiersz 329.), ktry testuje metod getNearest
DayOfWeek (wiersz 705.), nie by tak dugi i dokadny, jak jest obecnie. Dodaem do niego wiele
przypadkw testowych, poniewa nie wszystkie moje pocztkowe przypadki testowe wykonyway
si prawidowo [T6]. Mona zauway wzorzec awarii przez sprawdzenie, ktre przypadki testowe
s zakomentowane. Wzorzec ten wiele nam mwi [T7]. Pokazuje, e algorytm nie dziaa prawidowo, jeeli najbliszy dzie jest w przyszoci. Jasne jest, e jest to jaki rodzaj bdu w warunku
granicznym [T5].
Rwnie interesujcy jest wzr pokrycia testami raportowany przez program Clover [T8]. Wiersz 719.
nigdy nie jest wykonywany! Oznacza to, e instrukcja if w wierszu 718. zawsze ma warto false.
Patrzc na kod, mona stwierdzi, e musi mie ona warto true. Zmienna adjust jest zawsze
ujemna, wic nie moe by wiksza lub rwna 4. A zatem algorytm ten jest nieprawidowy.

PRZEBUDOWA KLASY SERIALDATE

279

Prawidowy algorytm jest przedstawiony poniej:


int delta = targetDOW - base.getDayOfWeek();
int positiveDelta = delta + 7;
int adjust = positiveDelta % 7;
if (adjust > 3)
adjust -= 7;
return SerialDate.addDays(adjust, base);

Testy z wierszy 417. oraz 429. mog by wykonywane prawidowo przez zgoszenie wyjtku
IllegalArgumentException, zamiast zwraca komunikat bdu z weekInMonthToString
oraz relativeToString.
Po wprowadzeniu tych zmian wszystkie testy jednostkowe s wykonywane prawidowo i uwaam,
e SerialDate teraz dziaa. Czas wic, by bya napisana waciwie.

Teraz poprawiamy
Mam zamiar przej od pocztku do koca klasy SerialDate, ulepszajc kolejne konstrukcje.
Cho nie jest to pokazane, po kadej wprowadzonej zmianie uruchamiaem wszystkie testy
jednostkowe JCommon, razem z moimi ulepszonymi testami jednostkowymi SerialDate. Pozostae
testy upewniay mnie, e kada pokazana tu zmiana dziaa z pozostaym kodem JCommon.
Zaczynajc od wiersza 1., widzimy blok komentarzy z informacjami o licencji, autorach i historii
zmian. Zgadzam si, e okrelone problemy prawne powinny zosta rozwizane, wic informacje
o prawach autorskich i licencjach powinny zosta. Z drugiej strony, historia zmian jest pozostaoci
z lat szedziesitych ubiegego stulecia. Mamy teraz narzdzia kontroli wersji, ktre robi to za nas.
Historia ta powinna zosta usunita [C1].
Lista importw zaczynajca si od wiersza 61. powinna zosta skrcona przez uycie java.text.*
oraz java.util.* [J1].
Formatowanie HTML w Javadoc przyprawia mnie o bl gowy (wiersz 67.). Pliki rdowe z wicej
ni jednym jzykiem s dla mnie problemem. Ten komentarz korzysta z czterech jzykw: Java,
angielskiego, Javadoc oraz HTML [G1]. Przy tak duej liczbie zastosowanych jzykw trudno zachowa prostot. Na przykad adne pozycjonowanie w wierszach 71. i 72. jest tracone po wygenerowaniu Javadoc, a poza tym kto chce widzie w kodzie rdowym znaczniki <ul> i <li>. Lepsz
strategi jest objcie caego komentarza znacznikami <pre>, aby formatowanie naturalne dla kodu
rdowego zostao zachowane w Javadoc1.
W wierszu 86. znajduje si deklaracja klasy. Dlaczego klasa ta jest nazwana SerialDate? Jakie jest
znaczenie wartoci sowa serial? Czy pochodzi ono od Serializable? Raczej mao prawdopodobne.

Jeszcze lepszym rozwizaniem byoby, gdyby Javadoc prezentowa wszystkie komentarze w postaci wstpnie sformatowanej
te same komentarze wyglday identycznie w kodzie i dokumentacji.

280

ROZDZIA 16.

Nie chc trzyma dalej Czytelnikw w niepewnoci. Wiem (lub co najmniej uwaam, e wiem), dlaczego
zostao uyte sowo serial. Powodem jest uycie staych SERIAL_LOWER_BOUND i SERIAL_UPPER_BOUND
z wierszy 98. i 101. Jeszcze lepsz wskazwk jest komentarz zaczynajcy si w wierszu 830. Klasa ta
jest nazwana SerialDate, poniewa jest zaimplementowana z uyciem numeru seryjnego, ktry
jest liczb dni od 30 grudnia 1899.
S z tym zwizane dwa problemy. Po pierwsze, nazwa numer seryjny nie jest tak naprawd prawidowa. Moe to by wtpliwe, poniewa reprezentacja ta jest bardziej wzgldnym przesuniciem
ni numerem seryjnym. Nazwa numer seryjny ma wicej wsplnego ze znacznikami identyfikujcymi produkty ni z datami. Dlatego nie uwaam, aby nazwa ta bya odpowiednio opisowa [N1].
Waciwszym terminem moe by ordinal (kolejno).
Drugi problem jest waniejszy. Nazwa SerialDate zakada implementacj. Klasa ta jest klas abstrakcyjn. Nie ma potrzeby, aby zakadaa cokolwiek na temat jej implementacji. W rzeczywistoci
istniej powody, aby ukry implementacj! Uwaam wic, e nazwa ta znajduje si na niewaciwym
poziomie abstrakcji [N2]. Wedug mnie klasa ta powinna nazywa si po prostu Date.
Niestety, w bibliotece Java znajduje si zbyt wiele klas nazwanych Date, wic nie jest to najlepsza
nazwa. Poniewa klasa ta operuje na dniach, a nie na czasie, rozwaaem nazwanie jej Day, ale ta
nazwa jest rwnie intensywnie uywana w innych miejscach. W kocu wybraem DayDate jako
najlepszy kompromis.
Od teraz w rozdziale bd korzysta z nazwy DayDate. Czytelnik winien pamita, e listingi, na jakie bdzie patrzy, nadal korzystaj z nazwy SerialDate.
Rozumiem, dlaczego DayDate dziedziczy po Comparable i Serializable. Dlaczego jednak dziedziczy po MonthConstants? Klasa MonthConstants (listing B.3 w dodatku B) jest po prostu zbiorem statycznych staych final, ktre definiuj nazwy miesicy. Dziedziczenie po klasach ze staymi jest star sztuczk uywan przez programistw Java, aby unikn uywania wyrae takich jak
MonthConstants.January, ale jest to zy pomys [J2]. MonthConstants powinien by typem wyliczeniowym.
public abstract class DayDate implements Comparable,
Serializable {
public static enum Month {
JANUARY(1),
FEBRUARY(2),
MARCH(3),
APRIL(4),
MAY(5),
JUNE(6),
JULY(7),
AUGUST(8),
SEPTEMBER(9),
OCTOBER(10),
NOVEMBER(11),
DECEMBER(12);
Month(int index) {
this.index = index;
}

PRZEBUDOWA KLASY SERIALDATE

281

public static Month make(int monthIndex) {


for (Month m : Month.values()) {
if (m.index == monthIndex)
return m;
}
throw new IllegalArgumentException("Niewaciwy indeks miesica " + monthIndex);
}
public final int index;
}

Zmiana MonthConstants na enum wymusza niewiele zmian w klasie DayDate i klasach jej uywajcych. Wprowadzenie wszystkich tych zmian zajo mi godzin. Jednak kada funkcja, ktra wczeniej oczekiwaa int z miesicem, obecnie oczekuje enumeratora Month. Dziki temu mona usun metod isValidMonthCode (wiersz 326.) i wszystkie sprawdzenia kodu miesica, takie jak te
uywane w monthCodeToQuarter (wiersz 356.) [G5].
Nastpnie w wierszu 91. mamy serialVersionUID. Zmienna ta jest uywana do sterowania serializacj. Jeeli j zmienimy, to dowolny obiekt DayDate zapisany w starszej wersji oprogramowania
nie bdzie czytelny w nowszej i spowoduje zgoszenie wyjtku InvalidClassException. Jeeli nie
zadeklarujemy zmiennej serialVersionUID, kompilator automatycznie j dla nas wygeneruje i bdzie
ona miaa inn warto po kadej zmianie w module. Wiem, e we wszystkich dokumentach
zalecana jest rczna kontrola nad t zmienn, ale wydaje mi si, e automatyczne sterowanie serializacj jest znacznie bezpieczniejsze [G4]. W kocu raczej bd debugowa InvalidClass
Exception, ni podejm dziwne dziaanie, jakie bdzie wynikiem braku zmiany wartoci serial
VersionUID. Dlatego usun t zmienn przynajmniej na jaki czas2.
Uwaam, e komentarz z wiersza 93. jest nadmiarowy. Nadmiarowe komentarze s po prostu
dezinformujcymi miejscami gromadzcymi kamstwa [C2]. Zamierzam wic usun go.
Komentarze z wierszy 97. i 100. traktuj o numerach seryjnych, o czym wspominaem wczeniej [C1].
Zmienne, jakie opisuj, s najwczeniejsz i najpniejsz moliw dat, jak mona opisa za pomoc
DayDate. Moe to by nieco janiejsze [N1].
public static final int EARLIEST_DATE_ORDINAL = 2; // 1900.01.01
public static final int LATEST_DATE_ORDINAL = 2958465; // 9999.12.31

Nie jest dla mnie jasne, dlaczego EARLIEST_DATE_ORDINAL ma warto 2 zamiast 0. W wierszu
829. znajduje si sugestia, e ma to co wsplnego ze sposobem reprezentacji dat w programie
Microsoft Excel. Znacznie dokadniej jest to przedstawione w klasie SpreadsheetDate dziedziczcej
po DayDate (listing B.5 w dodatku B). Komentarz z wiersza 71. dobrze opisuje problem.
Problem, jaki mamy tutaj, wydaje si zwizany z implementacj SpreadsheetDate i nie ma nic wsplnego z DayDate. Wywnioskowaem z tego, e EARLIEST_DATE_ORDINAL oraz LATEST_DATE_ORDINAL
nie nale do DayDate i powinny zosta przeniesione do SpreadsheetDate [G6].
2

Kilku recenzentw tego rozdziau protestowao przeciwko tej decyzji. Przekonywali oni, e w bibliotece open source lepiej
zakada rczn kontrol nad identyfikatorem serializacji, aby pomniejsze zmiany w oprogramowaniu nie powodoway
uniewanienia starych serializowanych dat. Jest to zrozumiay powd. Jednak awarie, niezalenie od tego, jak mog by
niewygodne, maj jasn przyczyn. Z drugiej strony, jeeli autor klasy zapomni zaktualizowa identyfikator, to powd
awarii nie jest zdefiniowany i moe by dobrze ukryty. Uwaam, e moraem z tej historii jest to, e nie powinnimy oczekiwa deserializacji pomidzy rnymi wersjami.

282

ROZDZIA 16.

Faktycznie, przeszukanie kodu wykazuje, e zmienne te s uywane wycznie w SpreadsheetDate.


Nic w DayDate ani w innych klasach biblioteki JCommon nie korzysta z tych zmiennych. Dlatego
przeniosem je do SpreadsheetDate.
Nastpne zmienne, MINIMUM_YEAR_SUPPORTED oraz MAXIMUM_YEAR_SUPPORTED (wiersz 104. oraz
107.), spowodoway pewien dylemat. Wydaje si jasne, e jeeli klasa DayDate jest klas abstrakcyjn, ktra nie zapewnia implementacji, to nie powinna informowa nas o roku minimalnym
i maksymalnym. Kolejny raz miaem zamiar przenie te zmienne do SpreadsheetDate [G6].
Jednak wyszukanie uytkownikw tych zmiennych wykazao, e inna klasa z nich korzysta: Relative
DayOfWeekRule (listing B.6 w dodatku B). S one zastosowane w wierszu 177. i 178. w funkcji
getDate, gdzie s wykorzystywane do sprawdzenia, czy argument getDate ma prawidowy rok.
Dylemat jest zwizany z tym, e uytkownik klasy abstrakcyjnej potrzebuje informacji o jej implementacji.
Do dostarczenia tych informacji bez zanieczyszczania potrzebujemy klasy DayDate. Zwykle
powinnimy uzyska informacje o implementacji z obiektu klasy pochodnej. Jednak do funkcji
getDate nie jest przekazywany obiekt DayDate. Zwraca ona jednak taki obiekt, co oznacza, e musi
by gdzie utworzony. Podpowiedzi s wiersze od 187. do 205. Obiekt DayDate jest tworzony przez
jedn z trzech funkcji, getPreviousDayOfWeek, getNearestDayOfWeek lub getFollowingDayOfWeek.
Wracajc do listingu DayDate, moemy zauway, e te funkcje (wiersze od 638. do 724.) zwracaj
dat utworzon przez addDays (wiersz 571.), a ta wywouje createInstance (wiersz 808.), ktra
z kolei tworzy obiekt SpreadsheetDate! [G7].
Zwykle zym pomysem jest, gdy klasy bazowe wiedz o swoich klasach pochodnych. Aby to poprawi, powinnimy uy wzorca fabryki abstrakcyjnej3 i utworzy DayDateFactory. Fabryka ta
utworzy potrzebne nam instancje DayDate i dodatkowo odpowie na pytania o implementacj, takie
jak data minimalna i maksymalna.
public abstract class DayDateFactory {
private static DayDateFactory factory = new SpreadsheetDateFactory();
public static void setInstance(DayDateFactory factory) {
DayDateFactory.factory = factory;
}
protected
protected
protected
protected
protected
protected

abstract
abstract
abstract
abstract
abstract
abstract

DayDate _makeDate(int ordinal);


DayDate _makeDate(int day, DayDate.Month month, int year);
DayDate _makeDate(int day, int month, int year);
DayDate _makeDate(java.util.Date date);
int _getMinimumYear();
int _getMaximumYear();

public static DayDate makeDate(int ordinal) {


return factory._makeDate(ordinal);
}
public static DayDate makeDate(int day, DayDate.Month month, int year) {
return factory._makeDate(day, month, year);
}

[GOF].

PRZEBUDOWA KLASY SERIALDATE

283

public static DayDate makeDate(int day, int month, int year) {


return factory._makeDate(day, month, year);
}
public static DayDate makeDate(java.util.Date date) {
return factory._makeDate(date);
}
public static int getMinimumYear() {
return factory._getMinimumYear();
}
public static int getMaximumYear() {
return factory._getMaximumYear();
}
}

Ta klasa fabryki zastpuje metody createInstance za pomoc metod makeDate, co nieco poprawia nazewnictwo [N1]. Domylnie wykorzystywana jest klasa SpreadsheetDateFactory, ale moe to by zmienione w dowolnym momencie przez uycie innej fabryki. Metody statyczne, ktre
deleguj do metod abstrakcyjnych, korzystaj z poczenia wzorcw: singleton 4, dekorator5 i fabryka
abstrakcyjna (moim zdaniem, wzorcw bardzo uytecznych).
Kod SpreadsheetDateFactory wyglda nastpujco:
public class SpreadsheetDateFactory extends DayDateFactory {
public DayDate _makeDate(int ordinal) {
return new SpreadsheetDate(ordinal);
}
public DayDate _makeDate(int day, DayDate.Month month, int year) {
return new SpreadsheetDate(day, month, year);
}
public DayDate _makeDate(int day, int month, int year) {
return new SpreadsheetDate(day, month, year);
}
public DayDate _makeDate(Date date) {
final GregorianCalendar calendar = new GregorianCalendar();
calendar.setTime(date);
return new SpreadsheetDate(
calendar.get(Calendar.DATE),
DayDate.Month.make(calendar.get(Calendar.MONTH) + 1),
calendar.get(Calendar.YEAR));
}
protected int _getMinimumYear() {
return SpreadsheetDate.MINIMUM_YEAR_SUPPORTED;
}
protected int _getMaximumYear() {
return SpreadsheetDate.MAXIMUM_YEAR_SUPPORTED;
}
}

Ibid.

Ibid.

284

ROZDZIA 16.

Jak mona zauway, przeniosem ju zmienne MINIMUM_YEAR_SUPPORTED oraz MAXIMUM_YEAR_


SUPPORTED do SpreadsheetDate, do ktrej to klasy one faktycznie nale [G6].
Nastpnym problemem w DayDate s stae dni rozpoczynajce si w wierszu 109. Powinny by
one osobnym typem wyliczeniowym [J3]. Przedstawiaem ju ten wzorzec, wic nie bd si tutaj
powtarza. Mona go zobaczy w kocowym listingu.
Nastpnie mamy seri zmiennych zaczynajcych si od LAST_DAY_OF_MONTH w wierszu 140. Moim
pierwszym zastrzeeniem byo to, e opisujce je komentarze s nadmiarowe [C3]. Ich nazwy s
wystarczajce. Dlatego usun te komentarze.
Wydaje si, e nie ma powodu, aby ta tabela nie bya prywatna [G8], poniewa dostpna jest funkcja
statyczna lastDayOfMonth, ktra dostarcza tych samych danych.
Nastpna tabela, AGGREGATE_DAYS_TO_END_OF_MONTH, jest nieco tajemnicza, poniewa nie jest
uywana w bibliotece JCommon [G9]. Dlatego j usunem.
To samo dotyczy LEAP_YEAR_AGGREGATE_DAYS_TO_END_OF_MONTH.
Nastpna tabela, AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH, jest uywana wycznie
w SpreadsheetDate (wiersz 434. oraz 473.). Powstaje pytanie, czy nie powinna zosta przeniesiona
do SpreadsheetDate. Argument za nieprzenoszeniem jej jest taki, e tabela ta nie jest specyficzna
dla adnej okrelonej implementacji [G6]. Z drugiej strony, nie istnieje inna implementacja poza
SpreadsheetDate, wic tabela powinna zosta przeniesiona moliwie blisko miejsca uycia [G10].
Najlepszym dla mnie argumentem jest konieczno zachowania spjnoci [G11], wic powinnimy
oznaczy t tabel jako prywatn i udostpni j za pomoc funkcji, na przykad julianDateOf
LastDayOfMonth. Wydaje si, e nikt nie potrzebuje tej funkcji. Co wicej, tabela moe by atwo
przeniesiona do DayDate, jeeli nowa implementacja tej klasy bdzie jej potrzebowaa. Dlatego j
przeniosem.
To samo dotyczy LEAP_YEAR_AGGREGATE_DAYS_TO_END_OF_MONTH.
Nastpnie mamy trzy zestawy staych, ktre mog by zmienione na typy wyliczeniowe (wiersze od
162. do 205.). Pierwszy z nich pozwala na wybranie tygodnia w miesicu. Zamieniem go na typ
wyliczeniowy o nazwie WeekInMonth.
public enum WeekInMonth {
FIRST(1), SECOND(2), THIRD(3), FOURTH(4), LAST(0);
public final int index;
WeekInMonth(int index) {
this.index = index;
}
}

Drugi zestaw staych (wiersze 177. 187.) jest nieco brzydszy. Stae INCLUDE_NONE, INCLUDE_FIRST,
INCLUDE_SECOND oraz INCLUDE_BOTH s wykorzystywane do opisywania, czy zdefiniowane daty
kocowe zakresu powinny by doczone do tego zakresu. Matematycznie rzecz ujmujc, opisuj

PRZEBUDOWA KLASY SERIALDATE

285

terminy otwarty okres, potwarty okres oraz zamknity okres. Uwaam, e janiejsze bdzie uycie
nomenklatury matematycznej [N3], wic zmieniem je na typ wyliczeniowy o nazwie DateInterval
ze staymi CLOSED, CLOSED_LEFT, CLOSED_RIGHT oraz OPEN.
Trzeci zbir staych (wiersze 189. 205.) opisuje, czy wyszukiwanie okrelonego dnia tygodnia
powinno da w wyniku ostatnie, nastpne lub najblisze wystpienie. Wybr tej nazwy jest co najmniej trudny. W kocu zdecydowaem si na WeekdayRange ze staymi LAST, NEXT oraz NEAREST.
Niekoniecznie mona zgadza si z wybranymi przeze mnie nazwami. Maj one dla mnie sens, ale
niekoniecznie mog mie sens dla innych. Jednak s one teraz w takiej postaci, ktr mona bardzo
atwo zmienia [J3]. Nigdzie nie s przekazywane w postaci liczb s przekazywane jako symbole.
Mona uy funkcji IDE zmie nazw do zmiany nazwy lub korzysta z nich bez obawy, e w innym
miejscu kodu opucilimy -1 lub 2 albo e ktra z deklaracji argumentu int zostaa le opisana.
Pole opisowe z wiersza 208. nie jest ju nigdzie uywane. Usunem je wic razem z jego akcesorem
i mutatorem.
Dodatkowo usunem zdegenerowany konstruktor domylny z wiersza 213. [G12]. Kompilator
wygeneruje go za nas.
Moemy pomin metod isValidWeekdayCode (wiersze 216. 238.), poniewa usunlimy j
podczas tworzenia typu wyliczeniowego Day.
Dotarlimy wic do metody stringToWeekdayCode (wiersze 242. 270.). Komentarze Javadoc,
ktre nie dodaj informacji do sygnatury metody, s jedynie wypeniaczem [C3], [G12]. Jedyn
wartoci tego komentarza Javadoc jest opis zwracanej wartoci -1. Jednak poniewa skorzystalimy
z typu wyliczeniowego Day, komentarz ten jest teraz nieprawidowy [C2]. Obecnie metoda zgasza
wyjtek IllegalArgumentException. Usunem wic Javadoc.
Usunem rwnie sowa kluczowe final z deklaracji argumentw i zmiennych. Mona powiedzie, e nie dodaj one adnej rzeczywistej wartoci, a jedynie zaciemniaj kod [G12]. Eliminacja
sw kluczowych final kci si z pewnymi konwencjami. Na przykad Robert Simmons6 gorco
zachca nas do korzystania z final w caym kodzie. Nie zgadzam si z tym. Uwaam, e istnieje
troch dobrych zastosowa final, takich jak niektre stae final, ale w innych przypadkach to
sowo kluczowe niewiele daje poza zamiecaniem kodu. By moe uwaam tak, poniewa bdy,
przed jakimi zabezpiecza sowo kluczowe final, mog by wczeniej przechwycone przez pisane
przeze mnie testy jednostkowe.
Nie podobaj mi si powtrzone instrukcje if [G5] wewntrz ptli for (wiersze 259. oraz 263.),
wic poczyem je za pomoc operatora || w jedn instrukcj if. Wykorzystaem rwnie typ
wyliczeniowy Day do sterowania ptl i wprowadziem kilka innych kosmetycznych zmian.

[Simmons04], s. 73.

286

ROZDZIA 16.

Zauwayem rwnie, e metoda ta nie naley tak naprawd do DayDate. Jest to funkcja analizujca
warto Day. Dlatego przeniosem j do typu wyliczeniowego Day. Jednak spowodowao to, e typ
Day sta si dosy duy. Poniewa zgodnie z koncepcj Day nie zaley od DayDate, przeniosem typ
wyliczeniowy Day poza klas DayDate, do osobnego pliku rdowego [G13].
Przeniosem do niego rwnie nastpn funkcj, weekdayCodeToString (wiersze 272. 286.),
i nazwaem j toString.
public enum Day {
MONDAY(Calendar.MONDAY),
TUESDAY(Calendar.TUESDAY),
WEDNESDAY(Calendar.WEDNESDAY),s
THURSDAY(Calendar.THURSDAY),
FRIDAY(Calendar.FRIDAY),
SATURDAY(Calendar.SATURDAY),
SUNDAY(Calendar.SUNDAY);
public final int index;
private static DateFormatSymbols dateSymbols = new DateFormatSymbols();
Day(int day) {
index = day;
}
public static Day make(int index) throws IllegalArgumentException {
for (Day d : Day.values())
if (d.index == index)
return d;
throw new IllegalArgumentException(
String.format("Niedozwolony indeks dnia: %d.", index));
}
public static Day parse(String s) throws IllegalArgumentException {
String[] shortWeekdayNames =
dateSymbols.getShortWeekdays();
String[] weekDayNames =
dateSymbols.getWeekdays();

s = s.trim();
for (Day day : Day.values()) {
if (s.equalsIgnoreCase(shortWeekdayNames[day.index]) ||
s.equalsIgnoreCase(weekDayNames[day.index])) {
return day;
}
}
throw new IllegalArgumentException(
String.format("%s nie jest prawidow nazw dnia", s));

public String toString() {


return dateSymbols.getWeekdays()[index];
}

Nastpnie mamy dwie funkcje getMonths (wiersze 288. 316.). Pierwsza wywouje drug. Druga
nie jest wywoywana nigdzie poza pierwsz. Dlatego poczyem te funkcje, znacznie je upraszczajc
[G9], [G12], [F4]. Na koniec zmieniem nazw na bardziej sugestywn [N1].
public static String[] getMonthNames() {
return dateFormatSymbols.getMonths();
}

PRZEBUDOWA KLASY SERIALDATE

287

Funkcja isValidMonthCode (wiersze 326. 346.) przestaa by potrzebna przez zastosowanie typu
wyliczeniowego Month, wic j usunem [G9].
Metoda monthCodeToQuarter (wiersze 356. 375.) wyglda na przypadek zazdroci o funkcje7
(ang. Feature Envy) [G14] i prawdopodobnie naley do typu wyliczeniowego Month jako metoda
quarter. Dlatego j zastpiem.
public int quarter() {
return 1 + (index-1)/3;
}

Spowodowao to, e typ wyliczeniowy Month sta si tak duy, i powinien sta si osobn klas.
Przeniosem go wic poza DayDate, zachowujc spjno z typem wyliczeniowym Day [G11], [G13].
Nastpne dwie metody nosz nazw monthCodeToString (wiersze 377. 426.). Rwnie w tym
przypadku mamy wzorzec wywoania jednej metody z drugiej, z dodatkowo przekazanym znacznikiem. Zwykle przekazywanie znacznika jako argumentu funkcji jest zym pomysem, szczeglnie
gdy znacznik wybiera format danych wyjciowych [G15]. Zmieniem nazw tej funkcji, zmieniem
struktur i uprociem j, a nastpnie przeniosem do typu wyliczeniowego Month [N1], [N3],
[C3], [G14].
public String toString() {
return dateFormatSymbols.getMonths()[index - 1];
}
public String toShortString() {
return dateFormatSymbols.getShortMonths()[index - 1];
}

Kolejn metod jest stringToMonthCode (wiersze 428. 472.). Zmieniem nazw tej funkcji
i uprociem j, a nastpnie przeniosem do typu wyliczeniowego Month [N1], [N3], [C3],
[G14], [G12].
public static Month parse(String s) {
s = s.trim();
for (Month m : Month.values())
if (m.matches(s))
return m;
try {
return make(Integer.parseInt(s));
}
catch (NumberFormatException e) {}
throw new IllegalArgumentException("Niewaciwy miesic " + s);
}
private boolean matches(String s) {
return s.equalsIgnoreCase(toString()) ||
s.equalsIgnoreCase(toShortString());
}

[Refactoring].

288

ROZDZIA 16.

Metoda isLeapYear (wiersze 495. 513.) powinna by bardziej ekspresyjna [G16].


public static boolean isLeapYear(int year) {
boolean fourth = year % 4 == 0;
boolean hundredth = year % 100 == 0;
boolean fourHundredth = year % 400 == 0;
return fourth && (!hundredth || fourHundredth);
}

Nastpna funkcja, leapYearCount (wiersze 519. 536.), nie naley do DayDate. Nic jej nie wywouje poza dwoma metodami z SpreadsheetDate. Dlatego przeniosem j dalej.
Funkcja lastDayOfMonth (wiersze 538. 560.) korzysta z tablicy LAST_DAY_OF_MONTH. Tablica
naley naprawd do typu wyliczeniowego Month [G17], wic j tam przeniosem. Dodatkowo
uprociem j i zmieniem na bardziej ekspresyjn [G16].
public static int lastDayOfMonth(Month month, int year) {
if (month == Month.FEBRUARY && isLeapYear(year))
return month.lastDay() + 1;
else
return month.lastDay();
}

Kolejne funkcje s bardziej interesujce. Nastpn funkcj jest addDays (wiersze 562. 567.).
Po pierwsze, poniewa ta funkcja dziaa na zmiennych DayDate, nie powinna by statyczna [G18].
Dlatego zmieniem j na metod instancyjn. Po drugie, wywouje ona funkcj toSerial. Funkcja
ta powinna zosta nazwana toOrdinal [N1]. Dodatkowo metoda ta moe by uproszczona.
public DayDate addDays(int days) {
return DayDateFactory.makeDate(toOrdinal() + days);
}

To samo dotyczy addMonths (wiersze 578. 602.). Powinna to by metoda instancyjna [G18].
Algorytm jest do skomplikowany, wic uyem objaniajcych zmiennych tymczasowych8 [G19],
aby by bardziej przejrzysty. Dodatkowo zmieniem metod getYYY na getYear [N1].
public DayDate addMonths(int months) {
int thisMonthAsOrdinal = 12 * getYear() + getMonth().index - 1;
int resultMonthAsOrdinal = thisMonthAsOrdinal + months;
int resultYear = resultMonthAsOrdinal / 12;
Month resultMonth = Month.make(resultMonthAsOrdinal % 12 + 1);
int lastDayOfResultMonth = lastDayOfMonth(resultMonth, resultYear);
int resultDay = Math.min(getDayOfMonth(), lastDayOfResultMonth);
return DayDateFactory.makeDate(resultDay, resultMonth, resultYear);
}

W funkcji addYears (wiersze 604. 626.) nie ma adnych nowych niespodzianek.


public DayDate plusYears(int years) {
int resultYear = getYear() + years;
int lastDayOfMonthInResultYear = lastDayOfMonth(getMonth(), resultYear);
int resultDay = Math.min(getDayOfMonth(), lastDayOfMonthInResultYear);
return DayDateFactory.makeDate(resultDay, getMonth(), resultYear);
}

[Beck97].

PRZEBUDOWA KLASY SERIALDATE

289

Cichutki gosik w mojej gowie namawia mnie na zmian tych metod ze statycznych na instancyjne.
Czy wyraenie date.addDays(5) jasno pokazuje, e obiekt date nie zmienia si i zwracany jest
nowy obiekt DayDate? Czy te bdnie przyjmujemy, e do obiektu date zostanie dodanych pi
dni? Mona uwaa, e nie jest to duy problem, ale poniszy fragment kodu pokazuje, e moe to
by bardzo mylce [G20].
DayDate date = DateFactory.makeDate(5, Month.DECEMBER, 1952);
date.addDays(7); // Przesuwamy dat o tydzie.

Kto, kto czyta ten kod, najprawdopodobniej przyjmie, e addDays zmienia obiekt date. Musimy wybra
nazw, ktra rozwieje t niejasno [N4]. Zmieniem wic nazwy na plusDays oraz plusMonths.
Wydaje mi si, e przeznaczenie tej metody jest dobrze oddawane przez
DayDate date = oldDate.plusDays(5);

natomiast ponisze wywoanie nie da si przeczyta tak pynnie, aby czytelnik przyj, e zmieniany
jest obiekt date:
date.plusDays(5);

Algorytmy staj si coraz bardziej interesujce. Metoda getPreviousDayOfWeek (wiersze 628. 660.)
dziaa, ale jest skomplikowana. Po przeanalizowaniu, co naprawd jest tu wykonywane [G21], byem
w stanie uproci j i uy objaniajcych zmiennych tymczasowych [G19], aby j zapisa janiej.
Zmieniem j rwnie z metody statycznej na instancyjn [G18] i usunem powtrzon metod instancyjn [G5] (wiersze 997. 1008.).
public DayDate getPreviousDayOfWeek(Day targetDayOfWeek) {
int offsetToTarget = targetDayOfWeek.index - getDayOfWeek().index;
if (offsetToTarget >= 0)
offsetToTarget -= 7;
return plusDays(offsetToTarget);
}

Dokadnie tak sam analiz zastosowaem w getFollowingDayOfWeek (wiersze 662. 693.), uzyskujc analogiczne wyniki.
public DayDate getFollowingDayOfWeek(Day targetDayOfWeek) {
int offsetToTarget = targetDayOfWeek.index - getDayOfWeek().index;
if (offsetToTarget <= 0)
offsetToTarget += 7;
return plusDays(offsetToTarget);
}

Nastpn funkcj jest getNearestDayOfWeek (wiersze 695. 726.), ktr poprawilimy ju wczeniej. Jednak wprowadzone zmiany nie s spjne z biecym wzorcem zastosowanym w ostatnich
dwch funkcjach [G11]. Aby uzyska spjno, skorzystaem z kilku objaniajcych zmiennych
tymczasowych [G19] do wyjanienia algorytmu.
public DayDate getNearestDayOfWeek(final Day targetDay) {
int offsetToThisWeeksTarget = targetDay.index - getDayOfWeek().index;
int offsetToFutureTarget = (offsetToThisWeeksTarget + 7) % 7;
int offsetToPreviousTarget = offsetToFutureTarget - 7;

290

if (offsetToFutureTarget > 3)
return plusDays(offsetToPreviousTarget);
else
return plusDays(offsetToFutureTarget);

ROZDZIA 16.

Metoda getEndOfCurrentMonth (wiersze 728. 740.) jest dziwna, poniewa jest to zmienna instancyjna, ktra zazdroci [G14] wasnej klasie przez oczekiwanie argumentu DayDate. Zmieniem j na prawdziw metod instancyjn i rozjaniem kilka nazw.
public DayDate getEndOfMonth() {
Month month = getMonth();
int year = getYear();
int lastDay = lastDayOfMonth(month, year);
return DayDateFactory.makeDate(lastDay, month, year);
}

Przebudowa metody weekInMonthToString (wiersze 742. 761.) okazaa si jednak bardzo interesujca. Przy uyciu narzdzi rodowiska IDE na pocztek przeniosem metod do typu wyliczeniowego WeekInMonth, ktry zosta utworzony nieco wczeniej. Nastpnie zmieniem nazw tej
metody na toString. Potem zmieniem j z metody statycznej na instancyjn. Testy nadal byy
wykonywane prawidowo (czy wiesz, do czego zmierzam?).
Nastpnie zupenie j usunem! Pi asercji przestao dziaa (wiersze 411. 415., listing B.4
w dodatku B). Zmieniem te wiersze tak, aby wykorzystywane byy nazwy staych typu wyliczeniowego (FIRST, SECOND, ). Wszystkie testy byy wykonywane prawidowo. Czy moesz powiedzie,
dlaczego? Czy widzisz, dlaczego kady z tych krokw by potrzebny? Narzdzie przebudowy kontroluje, czy wszystkie poprzednie wywoania weekInMonthToString obecnie s zamienione na
toString ze staych typu wyliczeniowego weekInMonth, poniewa wszystkie stae wyliczeniowe
implementuj metod toString, ktra po prostu zwraca ich nazw.
Niestety, byo to zbyt proste. Niezalenie od tego, jak wspaniay by acuch przebudowy, ostatecznie zorientowaem si, e jedynymi uytkownikami tej funkcji byy zmodyfikowane przeze mnie
testy, wic je usunem.
Oszukasz mnie raz wstyd si. Gdy oszukasz mnie po raz drugi, ja powinienem si wstydzi.
Gdy zorientowaem si, e nic innego poza testami nie wywouje metody relativeToString
(wiersze 765. 781.), po prostu usunem funkcj i jej testy.
W kocu dotarlimy do metod abstrakcyjnych tej klasy abstrakcyjnej. Pierwsza jest najbardziej
waciwa: toSerial (wiersze 838. 844.). Na pocztku tego rozdziau zmieniem jej nazw na
toOrdinal. Patrzc na jej kontekst, zdecydowaem si na zmian jej nazwy na getOrdinalDay.
Nastpn metod abstrakcyjn jest toDate (wiersze 838. 844.). Konwertuje ona DayDate na
java.util.Date . Dlaczego ta metoda jest abstrakcyjna? Jeeli spojrzymy na implementacj
w SpreadsheetDate (wiersze 198. 207., listing B.5 w dodatku B), okae si, e nie zaley ona od
niczego w implementacji tej klasy [G6]. Dlatego przesunem j w gr.
Metody getYYYY, getMonth oraz getDayOfMonth tworz adn abstrakcj. Jednak metoda
gatDayOfWeek jest kolejn, ktra powinna by wyjta z SpreadSheetDate, poniewa nie zaley od
niczego innego, jak tylko od elementw znajdujcych si w DayDate [G6]. A moe nie?

PRZEBUDOWA KLASY SERIALDATE

291

Jeeli przyjrzymy si jej dokadnie (wiersz 247., listing B.5 w dodatku B), zauwaymy, e algorytm
niejawnie zaley od pooenia dni w kolejnoci (inaczej mwic, ktry dzie tygodnia ma indeks 0).
Tak wic, mimo e ta funkcja nie ma fizycznych zalenoci, ktre nie mog by przeniesione do
DayDate, posiada zalenoci logiczne.
Tego typu zalenoci logiczne budz moje wtpliwoci [G22]. Jeeli co zaley logicznie od implementacji, to powinno rwnie zalee fizycznie. Ponadto wydaje mi si, e sam algorytm moe
by oglny, a tylko niewielka jego cz powinna by zalena od implementacji [G6].
Dlatego utworzyem metod abstrakcyjn w DayDate, o nazwie getDayOfWeekForOrdinalZero,
i do SpreadsheetDate dodaem implementacj zwracajc Day.SATURDAY. Nastpnie przeniosem metod getDayOfWeek do DayDate i zmieniem j, aby wywoywaa getOrdinalDay
oraz getDayOfWeekForOrdinalZero.
public Day getDayOfWeek() {
Day startingDay = getDayOfWeekForOrdinalZero();
int startingOffset = startingDay.index - Day.SUNDAY.index;
return Day.make((getOrdinalDay() + startingOffset) % 7 + 1);
}

Proponuj przyjrze si uwaniej komentarzowi znajdujcemu si w wierszach od 895. do 899. Czy


takie powtrzenie jest naprawd potrzebne? Jak zwykle, usunem ten komentarz wraz z innymi.
Nastpn metod jest compare (wiersze 902. 913.). Po raz kolejny metoda ta jest na niewaciwym poziomie abstrakcji [G6], wic przeniosem jej implementacj do DayDate. Dodatkowo nazwa
nie jest wystarczajco komunikatywna [N1]. Metoda faktycznie zwraca rnic w dniach w stosunku do argumentu. Dlatego zmieniem jej nazw na daysSince. Zauwayem te, e nie ma dla
tej metody testw, wic je dopisaem.
Sze kolejnych funkcji (wiersze od 915. do 980.) to funkcje abstrakcyjne, ktre powinny by implementowane w DayDate. Dlatego wycignem je z SpreadsheetDate.
Ostatnia funkcja, isInRange (wiersze 982. 995.), rwnie wymaga wydobycia i przebudowy. Instrukcja switch jest niezbyt elegancka [G23] i moe by zamieniona przez przeniesienie przypadkw do typu wyliczeniowego DateInterval.
public enum DateInterval {
OPEN {
public boolean isIn(int d, int left, int right) {
return d > left && d < right;
}
},
CLOSED_LEFT {
public boolean isIn(int d, int left, int right) {
return d >= left && d < right;
}
},
CLOSED_RIGHT {
public boolean isIn(int d, int left, int right) {
return d > left && d <= right;
}
},
CLOSED {

292

ROZDZIA 16.

public boolean isIn(int d, int left, int right) {


return d >= left && d <= right;
}

};
public abstract boolean isIn(int d, int left, int right);

public boolean isInRange(DayDate d1, DayDate d2, DateInterval interval) {


int left = Math.min(d1.getOrdinalDay(), d2.getOrdinalDay());
int right = Math.max(d1.getOrdinalDay(), d2.getOrdinalDay());
return interval.isIn(getOrdinalDay(), left, right);
}

W ten sposb dotarlimy do koca klasy DayDate. Kolejnym krokiem bdzie ponowne przejrzenie
caej klasy i sprawdzenie, czy przepyw kodu jest waciwy.
Otwierajcy komentarz jest od dawna niewaciwy, wic skrciem go i poprawiem [C2].
Nastpnie przeniosem pozostae typy wyliczeniowe do osobnych plikw [G12].
Potem przeniosem zmienn statyczn (dateFormatSymbols) oraz trzy metody statyczne
(getMonthNames, isLeapYear, lastDayOfMonth) do nowej klasy o nazwie DateUtil [G6].
Przeniosem metody abstrakcyjne w gr, tam gdzie powinny by [G24].
Zmieniem Month.make na Month.fromInt [N1] i wykonaem to samo w przypadku pozostaych
typw wyliczeniowych. Dodatkowo we wszystkich typach wyliczeniowych utworzyem akcesor
toInt() i pole index zmieniem na prywatne.
Znalazem rwnie kilka interesujcych powtrze [G5] w plusYears oraz plusMonths, ktre
mogem wyeliminowa przez wyodrbnienie nowej metody o nazwie correctLastDayOfMonth,
co spowodowao, e wszystkie trzy metody s znacznie bardziej czytelne.
Usunem rwnie magiczn liczb 1 [G25], zastpujc j wywoaniami, odpowiednio: Month.JANU
ARY.toInt() lub Day.SUNDAY.toInt(). Powiciem rwnie troch czasu klasie SpreadsheetDate,
poprawiajc algorytm. Kocowy wynik znajduje si na listingach od B.7 do B.16 w dodatku B.
Interesujce jest, e pokrycie kodu w DayDate zmniejszyo si do 84,9 procent. Stao si tak nie
z powodu mniejszej liczby testowanych funkcji po prostu klasa na tyle si zmniejszya, e tych
kilka niepokrytych wierszy ma wiksz wag. DayDate ma obecnie pokrytych testami 45 z 53
instrukcji wykonywalnych. Niepokryte wiersze s tak proste, e nie warto ich testowa.

Zakoczenie
Kolejny raz zadziaalimy zgodnie z zasad skautw. Zwrcilimy kod nieco czyciejszy, ni go pobralimy. Zajo to troch czasu, ale si opacio. Pokrycie kodu testami zwikszyo si, zostao poprawionych kilka bdw, a kod zosta skrcony i jest bardziej zrozumiay. Mamy nadziej, e nastpna osoba, ktra zajrzy do tego kodu, uzna, i jest atwiejszy w uyciu ni wczeniej. Mamy
rwnie nadziej, e osoba ta usprawni kod w wikszym stopniu ni my.

PRZEBUDOWA KLASY SERIALDATE

293

Bibliografia
[GOF]: Gamma i inni, Elements of Reusable Object Oriented Software, Addison-Wesley 1996.
[Simmons04]: Robert Simmons Jr., Hardcore Java, OReilly 2004.
[Refactoring]: Martin Fowler i inni, Refactoring: Improving the Design of Existing Code, AddisonWesley 1999.
[Beck97]: Kent Beck, Smalltalk Best Practice Patterns, Prentice Hall 1997.

294

ROZDZIA 16.

ROZDZIA 17.

Zapachy kodu i heurystyki

Martin Fowler zidentyfikowa wiele rnych zapachw


W kodu. Na znajdujcej si dalej licie znajduje
si wiele zapachw zidentyfikowanych przez Martina
SWOJEJ WSPANIAEJ KSICE

REFACTORING1

i jeszcze wicej okrelonych przeze mnie. Dodatkowo doczyem inne pereki i heurystyki, ktrymi
chciabym si podzieli.

[Refactoring].

295

List t napisaem, gdy przegldaem kilka rnych programw i przebudowywaem je. Wprowadzajc zmian, zadawaem sobie pytanie, dlaczego j wprowadzam, a nastpnie zapisywaem powd.
Wynikiem jest dosy duga lista elementw, ktre brzydko mi pachn, gdy czytam kod rdowy.
Lista ta moe by czytana od pocztku do koca, jak rwnie uywana jako skorowidz. W dodatku C
znajduje si lista odwoa kadej heurystyki, zawierajca wyszczeglnienie miejsc w tekcie, w ktrych
zostaa uyta.

Komentarze
C1. Niewaciwe informacje
Niewaciwe jest, aby komentarze przechowyway informacje, ktre lepiej przechowywa w innych
systemach, takich jak system kontroli wersji, system ledzenia bdw lub inny system przechowywania danych. Na przykad historia zmian jedynie zaciemnia pliki rdowe wieloma wierszami historycznego i mao interesujcego tekstu. Zwykle metadane, takie jak autorzy, data ostatniej modyfikacji, numer SPR itd., nie powinny wystpowa w komentarzach. Komentarze powinny by
zarezerwowane dla informacji technicznych o kodzie i projekcie.

C2. Przestarzae komentarze


Komentarz, ktry sta si nieistotny i nieprawidowy, jest przestarzay. Komentarze bardzo szybko
si starzej. Najlepiej nie pisa komentarzy, ktre mog utraci aktualno. Jeeli znajdziemy przestarzay komentarz, najlepiej jak najszybciej go zaktualizowa lub usun. Przestarzae komentarze
maj tendencj do utraty cznoci z opisywanym kodem. Staj si one pywajcymi wyspami
nieistotnoci i bdnymi kierunkowskazami w kodzie.

C3. Nadmiarowe komentarze


Komentarz jest nadmiarowy, jeeli opisuje co, co wystarczajco dobrze samo si opisuje. Na przykad:
i++; // Zwikszenie i

Innym przykadem jest Javadoc niezawierajcy wicej informacji (a nawet mniej) ni sygnatura
funkcji:
/**
* @param sellRequest
* @return
* @throws ManagedComponentException
*/
public SellResponse beginSellItem(SellRequest sellRequest)
throws ManagedComponentException

Komentarze powinny informowa o tym, o czym nie moe powiedzie sam kod.

296

ROZDZIA 17.

C4. le napisane komentarze


Komentarz, ktry wart jest napisania, trzeba napisa dobrze. Jeeli decydujemy si na napisanie
komentarza, musimy powici troch czasu na upewnienie si, e jest to najlepszy komentarz, jaki
moglimy napisa. Naley uwanie dobiera sowa, przestrzega zasad odmiany i interpunkcji. Nie
pisa chaotycznie! Nie pisa o oczywistociach. Pisa zwile.

C5. Zakomentowany kod


Do szalestwa doprowadzaj mnie poacie zakomentowanego kodu. Kto bdzie wiedzia, jak stary
jest ten kod? Kto bdzie wiedzia, czy jest on znaczcy, czy nie? Dodatkowo nikt go nie skasuje, poniewa kady zakada, e kto inny go potrzebuje lub ma wzgldem niego jakie plany.
Komentarz taki tkwi w kodzie przez duszy czas, psujc si i z kadym dniem tracc na znaczeniu.
Wywouje funkcje, ktre ju nie istniej. Uywa zmiennych, ktrych nazwy zostay zmienione. Korzysta z konwencji, ktre ju od dawna nie obowizuj. Zanieczyszcza zawierajcy go modu i przeszkadza ludziom, ktrzy go czytaj. Zakomentowany kod jest potworny.
Gdy zobaczysz zakomentowany kod, usu go! Nie obawiaj si, system kontroli wersji nadal go pamita.
Jeeli ktokolwiek bdzie go potrzebowa, bdzie mg pobra wczeniejsz wersj. Nie toleruj
zachowywania zakomentowanego kodu.

rodowisko
E1. Budowanie wymaga wicej ni jednego kroku
Budowanie (kompilacja i konsolidacja) projektu powinno by jedn prost operacj. Niedopuszczalne jest pobieranie wielu elementw z systemu kontroli wersji. Nie powinnimy potrzebowa
sekwencji zaawansowanych polece lub skryptw zalenych od kontekstu przeznaczonych do
zbudowania poszczeglnych elementw. Nie powinnimy szuka dodatkowych plikw JAR, XML
i innych wymaganych przez system artefaktw. Powinnimy mie moliwo pobrania systemu za
pomoc jednego prostego polecenia, a nastpnie wykonania innego prostego polecenia w celu jego
zbudowania.
svn get mySystem
cd mySystem
ant all

E2. Testy wymagaj wicej ni jednego kroku


Powinnimy by w stanie uruchomi wszystkie testy jednostkowe za pomoc jednego polecenia.
Najlepszym przypadkiem jest uruchomienie wszystkich testw klikniciem jednego przycisku w IDE.
W najgorszym przypadku powinnimy to osign, wywoujc jedno proste polecenie powoki.
Moliwo wykonania wszystkich testw jest tak wana i podstawowa, e powinna by szybka,
atwa i oczywista.

ZAPACHY KODU I HEURYSTYKI

297

Funkcje
F1. Nadmiar argumentw
Funkcje powinny mie ma liczb argumentw. Najlepszym przypadkiem jest... brak argumentw, nastpnie mog by: jeden, dwa lub trzy argumenty. Wiksza liczba argumentw stanowi potencjalne rdo problemw (patrz Argumenty funkcji w rozdziale 3.).

F2. Argumenty wyjciowe


Argumenty wyjciowe s mao intuicyjne. Czytelnik oczekuje, e argumenty bd wejciem, a nie
wyjciem. Jeeli funkcja musi zmienia stan czegokolwiek, powinna zmienia stan wasnego obiektu
(patrz Argumenty wyjciowe w rozdziale 3.).

F3. Argumenty znacznikowe


Argumenty logiczne deklaruj, e funkcja wykonuje wicej ni jedn operacj. S one mylce
i powinny by eliminowane (patrz Argumenty znacznikowe w rozdziale 3.).

F4. Martwe funkcje


Metody, ktre nie s nigdy wywoywane, powinny by usuwane. Przechowywanie martwego kodu
jest rozrzutnoci. Nie trzeba si obawia usuwania funkcji system kontroli wersji nadal j pamita.

Oglne
G1. Wiele jzykw w jednym pliku rdowym
Stosowane obecnie rodowiska programowania umoliwiaj umieszczanie wielu rnych jzykw
w jednym pliku rdowym. Na przykad plik rdowy jzyka Java moe zawiera fragmenty
jzykw: XML, HTML, YAML, JavaDoc, polskiego, JavaScript i tak dalej. W przypadku plikw
JSP oprcz jzyka HTML mona znale kod Java, znaczniki biblioteki, komentarze w jzyku naturalnym, Javadoc, XML, JavaScript itd. Jest to co najmniej mylce.
W idealnym przypadku jeden plik rdowy powinien zawiera jeden i tylko jeden jzyk. Realistycznie
rzecz biorc, konieczne bdzie uycie wicej ni jednego. Jednak powinnimy podj prb ograniczenia liczby i zakresu dodatkowych jzykw w plikach rdowych.

298

ROZDZIA 17.

G2. Oczywiste dziaanie jest nieimplementowane


Zgodnie z zasad najmniejszego zaskoczenia2 kada funkcja lub klasa powinna by implementowana w taki sposb, aby inny programista mg si tego spodziewa. Przyjrzyjmy si funkcji przeksztacajcej nazw dnia na typ enum reprezentujcy ten dzie:
Day day = DayDate.StringToDay(String dayName);

Moemy oczekiwa, e cig Monday zostanie zamieniony na Day.MONDAY. Moemy rwnie


oczekiwa, e przeksztacane bd czsto stosowane skrty, a funkcja bdzie ignorowaa wielko liter.
Gdy takie oczywiste dziaania nie s implementowane, czytelnicy i uytkownicy kodu nie mog
polega na swojej intuicji i nazwach funkcji. Trac wwczas zaufanie do autora i musz wrci do
czytania szczegw kodu.

G3. Niewaciwe dziaanie w warunkach granicznych


Wydaje si oczywiste, e kod powinien dziaa prawidowo. Niestety, rzadko zdajemy sobie spraw,
jak skomplikowane jest prawidowe dziaanie. Programici czsto pisz funkcje, ktre wedug nich
dziaaj; ufaj oni swojej intuicji, zamiast udowodni, e kod dziaa we wszystkich sytuacjach
skrajnych.
Niczym nie mona zastpi dokadnej analizy. Kady warunek graniczny, kady problem i wyjtek
reprezentuje co, co moe zepsu elegancki i intuicyjny algorytm. Nie polegaj na swojej intuicji.
Naley wyszuka wszystkie warunki graniczne i napisa dla nich testy.

G4. Zdjte zabezpieczenia


Do wybuchu w Czarnobylu doszo dlatego, e dyrektor elektrowni wycza kolejne mechanizmy
zabezpieczajce. Zabezpieczenia te utrudniay przeprowadzanie eksperymentu. Eksperyment i tak si
nie uda, a wiat zobaczy pierwsz du cywiln katastrof nuklearn.
Zdejmowanie zabezpiecze jest ryzykowne. Przejmowanie rcznej kontroli nad wartoci
serialVersionUID moe by niezbdne, ale zawsze jest ryzykowne. Wyczanie okrelonych
ostrzee kompilatora (lub wszystkich ostrzee!) moe pomc nam w prawidowym skompilowaniu, ale niesie ryzyko nieskoczonej sesji debugowania. Wyczenie niewykonywanych testw
i obiecanie sobie, e zajmiemy si nimi pniej, jest rwnie ze, jak postrzeganie kart kredytowych jako niewyczerpanego rda darmowych pienidzy.

Lub zasad najmniejszego zdziwienia: http://en.wikipedia.org/wiki/Principle_of_least_astonishment.

ZAPACHY KODU I HEURYSTYKI

299

G5. Powtrzenia
Jest to jedna z najwaniejszych zasad z tej ksiki i powinnimy j traktowa bardzo powanie.
Niemal kady autor piszcy o projektowaniu oprogramowania wspomina o tej zasadzie. Dave
Thomas oraz Andy Hunt nazwali j zasad DRY3 (ang. Dont Repeat Yourself), czyli nie powtarzaj si.
Kent Beck uzna j za jedn z podstawowych zasad programowania ekstremalnego i nazwa j once,
and only once, czyli raz i tylko raz. Ron Jeffries uzna j za drug w hierarchii wanoci, zaraz za
wykonywaniem wszystkich testw.
Za kadym razem, gdy widzimy powtrzenie w kodzie, reprezentuje ono utracon moliwo abstrakcji. Powtrzenie takie moe prawdopodobnie sta si funkcj albo zupenie osobn klas. Przez
zamian powtrze na takie abstrakcje zwikszamy sownik projektowanego przez nas jzyka. Inni
programici mog skorzysta z tworzonych przez nas abstrakcji. Kodowanie staje si szybsze i bardziej
odporne na bdy, poniewa podwyszamy poziom abstrakcji.
Najbardziej oczywist form powtrzenia jest wystpowanie fragmentw identycznego kodu, co
wyglda, jakby ktry z programistw oszala i wielokrotnie wkleja ten sam kod. Powinny by one
zastpione prostymi metodami.
Bardziej subteln form jest acuch instrukcji switch-case lub if-else, ktry wystpuje wielokrotnie w rnych moduach i zawsze uywany jest ten sam zbir warunkw. Powinny by one zastpione polimorfizmem.
Jeszcze bardziej subtelnymi powtrzeniami s moduy, ktre maj podobne algorytmy, ale nie
maj podobnego kodu. Jest to rwnie powtrzenie, ktre powinno by wyeliminowane przez uycie wzorcw szablonu metody4 lub strategii5.
Faktycznie, wikszo wzorcw projektowych, jakie zostay zdefiniowane w ostatnich pitnastu latach, to dobrze znane sposoby na eliminowanie powtrze. Rwnie postacie normalne Codda s
strategi eliminowania powtrze ze schematu bazy danych. Samo programowanie obiektowe jest
strategi organizacji moduw i eliminowania powtrze. Nie dziwi zatem fakt, e to samo dotyczy
programowania strukturalnego.
Uwaam, e zasada zostaa ustanowiona. Znajd i wyeliminuj wszystkie powtrzenia.

G6. Kod na nieodpowiednim poziomie abstrakcji


Wane jest, aby tworzy abstrakcje, ktre oddzielaj oglne koncepcje na wyszym poziomie od
szczegw na niszym poziomie. Czasami robimy to przez tworzenie klas abstrakcyjnych, ktre
przechowuj koncepcje wyszego poziomu, a klasy dziedziczce po nich zawieraj koncepcje niszych poziomw. Gdy stosujemy taki podzia, musimy upewni si, e jest kompletny. Chcemy,
3

[PRAG].

[GOF].

[GOF].

300

ROZDZIA 17.

aby wszystkie koncepcje niszych poziomw znajdoway si w klasach pochodnych, a wszystkie


koncepcje wyszego poziomu byy w klasie bazowej.
Na przykad stae, zmienne lub funkcje uytkowe, ktre odnosz si tylko do szczegowej implementacji, nie powinny znajdowa si w klasie bazowej. Klasa bazowa nie powinna nic o nich wiedzie.
Zasada ta dotyczy rwnie plikw rdowych, komponentw i moduw. Dobry projekt oprogramowania wymaga, abymy rozdzielali koncepcje na rne poziomy i umieszczali je w rnych
kontenerach. Czasami kontenery te s klasami bazowymi lub pochodnymi, a czasami s plikami
rdowymi, moduami lub komponentami. Niezalenie od zastosowanego przypadku, rozdzielenie
powinno by pene. Nie powinnimy miesza ze sob koncepcji z niszych i wyszych poziomw.
Wemy pod uwag nastpujcy kod:
public interface Stack {
Object pop() throws EmptyException;
void push(Object o) throws FullException;
double percentFull();
class EmptyException extends Exception {}
class FullException extends Exception {}
}

Funkcja percentFull znajduje si na niewaciwym poziomie abstrakcji. Cho istnieje wiele implementacji Stack, w ktrych koncepcja wypenienia jest rozsdna, to jednak istniej inne implementacje, ktre po prostu nie powinny wiedzie, jak bardzo s wypenione. Dlatego funkcja ta
powinna by umieszczona w interfejsie dziedziczcym, na przykad BoundedStack.
Kto moe uwaa, e taka implementacja powinna zwraca zero, jeeli stos jest bez ogranicze.
Problem polega jednak na tym, e aden stos nie jest naprawd nieograniczony. Nie mona naprawd zapobiec wyjtkom OutOfMemoryException przez nastpujce sprawdzenia:
stack.percentFull() < 50.0.

Implementacja funkcji, ktra zwraca 0, bdzie po prostu wprowadzaa w bd.


Nie mona wprowadza w bd lub maskowa le umieszczonej abstrakcji. Izolacja abstrakcji jest
jednym z najtrudniejszych zada programistw i w przypadku, gdy zostanie przeprowadzona le,
nie ma szybkiego sposobu poprawienia kodu.

G7. Klasy bazowe zalene od swoich klas pochodnych


Najczstszym powodem podziau koncepcji na klas bazow i pochodne jest to, e klasa bazowa
wyszego poziomu moe by niezalena od klas pochodnych niszego poziomu. Dlatego, gdy widzimy
klas bazow korzystajc z nazw swoich pochodnych, oczekujemy problemu. Klasy bazowe nie
powinny nic wiedzie o swoich potomkach.
Istniej oczywicie wyjtki od tej zasady. Czasami liczba klas pochodnych jest cile ustalona i klasa
bazowa posiada kod wybierajcy pomidzy tymi klasami. Widzielimy takie rozwizanie w wielu
implementacjach automatw skoczonych. Jednak w takim przypadku klasy pochodne i klasa bazowa

ZAPACHY KODU I HEURYSTYKI

301

s cile poczone i zawsze udostpniane razem, w tym samym pliku jar. W oglnym przypadku
chcemy mie moliwo udostpniania klas bazowych i pochodnych w rnych plikach jar.
Udostpnianie klas pochodnych w rnych plikach jar i zapewnienie, e bazowe pliki jar nic nie
wiedz o zawartoci plikw jar z klasami pochodnymi, pozwala nam na instalowanie systemw
w indywidualnych i niezalenych komponentach. Gdy taki komponent zostanie zmodyfikowany,
moe by ponownie zainstalowany, bez koniecznoci instalowania komponentw bazowych. Oznacza
to, e wpyw zmian jest znacznie obniony, a utrzymanie dziaajcych systemw jest znacznie
prostsze.

G8. Za duo informacji


Dobrze zdefiniowane moduy maj bardzo mae interfejsy, ktre umoliwiaj wykonanie wielu
operacji. le zdefiniowane moduy maj szerokie i gbokie interfejsy, ktre wymuszaj na nas
tworzenie wielu rnych konstrukcji do wykonania prostych operacji. Dobrze zdefiniowane interfejsy nie posiadaj wielu funkcji, od ktrych zale, wic sprzenie jest niskie. le zdefiniowane
interfejsy maj wiele funkcji, ktre naley wywoa, wic sprzenie jest wysokie.
Profesjonalni twrcy oprogramowania ograniczaj liczb elementw udostpnianych w interfejsach ich klas i moduw. Im mniej metod ma klasa, tym lepiej. Im mniej zmiennych jest wykorzystywanych w funkcji, tym lepiej. Im mniej zmiennych instancyjnych ma klasa, tym lepiej.
Ukrywajmy nasze dane. Ukrywajmy funkcje narzdziowe. Ukrywajmy stae i dane tymczasowe. Nie
twrzmy klas z wieloma metodami i wieloma zmiennymi instancyjnymi. Nie twrzmy wielu
zmiennych i funkcji zabezpieczonych dla naszych klas bazowych. Skoncentrujmy si na zachowaniu maych i cisych interfejsw. Eliminujmy sprzenia przez ograniczanie informacji.

G9. Martwy kod


Martwy kod to taki, ktry nie jest wykonywany. Mona znale go w treci instrukcji if, ktre
kontroluj nigdy niespeniane warunki. Mona znale go w blokach catch, dla ktrych nigdy nie
zostanie wywoane odpowiednie throw. Mona go znale w niewielkich metodach narzdziowych,
ktre nie s nigdy wywoywane, lub w warunkach switch-case, ktre nigdy nie zostaj spenione.
Problem z martwym kodem jest taki, e szybko zaczyna wydziela zapach. Im jest starszy, tym ten
zapach staje si silniejszy i kwaniejszy. Dzieje si tak, poniewa martwy kod nie jest w peni aktualizowany w czasie zmian projektu. Nadal kompiluje si, ale nie jest zgodny z najnowszymi konwencjami i zasadami. By on pisany w czasie, gdy system by inny. Gdy znajdziemy martwy kod, moemy
zrobi tylko jedn waciw rzecz. Zapewnijmy mu godny pochwek, usuwajc go z systemu.

302

ROZDZIA 17.

G10. Separacja pionowa


Zmienne i funkcje powinny by definiowane blisko miejsca, gdzie s uyte. Zmienne lokalne powinny by deklarowane bezporednio ponad pierwszym uyciem i powinny mie may zakres pionowy. Nie chcemy zmiennych lokalnych deklarowanych setki wierszy od ich zastosowania.
Funkcje prywatne powinny by definiowane poniej pierwszego zastosowania. Funkcje prywatne
nale do zakresu caej klasy, ale i tak chcemy ograniczy odlego w pionie pomidzy wywoaniem a definicj. Znalezienie funkcji prywatnej powinno sprowadza si do przewinicia kodu
w d od pierwszego uycia.

G11. Niespjno
Jeeli wykonujemy co w okrelony sposb, wszystkie podobne zadania powinnimy wykonywa
w taki sam sposb. Odnosi si to do zasady najmniejszego zaskoczenia. Naley rozwanie wybiera
konwencje i po ich wybraniu konsekwentnie je stosowa.
Jeeli w okrelonej funkcji korzystamy ze zmiennej o nazwie response, zawierajcej HttpServlet
Response, to tej samej nazwy zmiennej powinnimy uywa we wszystkich innych funkcjach korzystajcych z obiektu HttpServletResponse. Jeeli nazwiemy metod processVerificationRequest,
to powinnimy uy podobnej nazwy, takiej jak processDeletionRequest, dla metod przetwarzajcych inne rodzaje da.
Tego typu prosta spjno, jeeli jest konsekwentnie stosowana, moe spowodowa, e kod bdzie
znacznie atwiejszy do czytania i modyfikacji.

G12. Zaciemnianie
Jakie jest zastosowanie domylnego konstruktora bez implementacji? Suy on jedynie do zaciemniania kodu fragmentami niemajcymi znaczenia. Zmienne, ktre nie s uywane, nigdy niewywoywane funkcje, komentarze, ktre nie dodaj adnej informacji i tym podobne elementy zaciemniaj kod i powinny zosta usunite. Pliki rdowe winny by uporzdkowane, dobrze
zorganizowane.

G13. Sztuczne sprzenia


Elementy, ktre nie s od siebie zalene, nie powinny by sztucznie czone. Na przykad oglny
typ enum nie powinien znajdowa si w bardziej specjalizowanej klasie, poniewa wymusza to na
caej aplikacji korzystanie ze zbyt szczegowych klas. To samo dotyczy funkcji statycznej oglnego
przeznaczenia zadeklarowanej w specjalizowanej klasie.
Sztuczne sprzenie to takie, ktre wystpuje pomidzy dwoma niepowizanymi bezporednio
moduami. Jest to wynik umieszczenia zmiennej, staej lub funkcji w tymczasowo wygodnej,
cho niewaciwej lokalizacji. Jest to wynik lenistwa i niedbalstwa.

ZAPACHY KODU I HEURYSTYKI

303

Naley powici troch czasu na okrelenie, w ktrym miejscu powinny by zadeklarowane nasze
funkcje, stae i zmienne. Nie naley ich po prostu wrzuca w najwygodniejsze w danej chwili miejsce
i pozostawia ich tam.

G14. Zazdro o funkcje


Jest to jeden z zapachw kodu6 Martina Fowlera. Klasa (lub metoda) powinna by zainteresowana
zmiennymi i funkcjami nalecymi do tej klasy, a nie zmiennymi i metodami innych klas. Gdy
metoda korzysta z akcesorw i mutatorw innego obiektu do manipulowania danymi wewntrz
tego obiektu, to zazdroci zakresu klasy tego obiektu. auje, e nie znajduje si wewntrz innej klasy,
by mie bezporedni dostp do zmiennych, ktrymi manipuluje. Na przykad:
public class HourlyPayCalculator {
public Money calculateWeeklyPay(HourlyEmployee e) {
int tenthRate = e.getTenthRate().getPennies();
int tenthsWorked = e.getTenthsWorked();
int straightTime = Math.min(400, tenthsWorked);
int overTime = Math.max(0, tenthsWorked - straightTime);
int straightPay = straightTime * tenthRate;
int overtimePay = (int)Math.round(overTime*tenthRate*1.5);
return new Money(straightPay + overtimePay);
}
}

Metoda calculateWeeklyPay siga do obiektu HourlyEmployee w celu uzyskania danych, na


ktrych operuje. Metoda calculateWeeklyPay zazdroci zakresu HourlyEmployee. auje ona,
e nie jest wewntrz HourlyEmployee.
Eliminujemy z kodu zazdro o funkcje, poniewa powoduje to ujawnienie jednej klasie wewntrznej
budowy innej klasy. Czasami jednak zazdro o funkcje jest zem koniecznym. Wemy pod uwag
nastpujcy kod:
public class HourlyEmployeeReport {
private HourlyEmployee employee ;
public HourlyEmployeeReport(HourlyEmployee e) {
this.employee = e;
}

String reportHours() {
return String.format(
"Nazwisko: %s\tGodziny:%d.%1d\n",
employee.getName(),
employee.getTenthsWorked()/10,
employee.getTenthsWorked()%10);
}

Oczywicie, metoda reportHours zazdroci klasie HourlyEmployee. Z drugiej strony, nie chcemy,
aby klasa HourlyEmployee znaa szczegy formatowania raportu. Przeniesienie tego cigu formatujcego do klasy HourlyEmployee zamaoby kilka zasad projektowania obiektowego7. Spowodowaoby sprzenie HourlyEmployee z formatem raportu, wystawiajc j na zmiany w formacie.
6

[Refactoring].

Dokadnie zasad pojedynczej odpowiedzialnoci, zasad otwarty-zamknity oraz zasad wsplnego zamknicia. Patrz [PPP].

304

ROZDZIA 17.

G15. Argumenty wybierajce


Trudno o co bardziej wstrtnego, jak wiszcy argument false na kocu wywoania funkcji. Co on
oznacza? Co si stanie, jeeli zmienimy go na true? Nie tylko trudno zapamita przeznaczenie
argumentu wybierajcego, ale rwnie kady argument wybierajcy czy kilka funkcji w jednej.
Argumenty wybierajce s sposobem na uniknicie podziau duej funkcji na kilka mniejszych,
wynikajcym z lenistwa. Przeanalizujmy taki przykad:
public int calculateWeeklyPay(boolean overtime) {
int tenthRate = getTenthRate();
int tenthsWorked = getTenthsWorked();
int straightTime = Math.min(400, tenthsWorked);
int overTime = Math.max(0, tenthsWorked - straightTime);
int straightPay = straightTime * tenthRate;
double overtimeRate = overtime ? 1.5 : 1.0 * tenthRate;
int overtimePay = (int)Math.round(overTime*overtimeRate);
return straightPay + overtimePay;
}

Mona wywoa t funkcj z wartoci true, jeeli nadgodziny s patne ze wspczynnikiem 1,5,
lub false, jeeli s pacone w zwyky sposb. Niewaciwe jest, e musimy pamita, co oznacza
wywoanie calculateWeeklyPay(false), gdy si na takie natkniemy. Jednak prawdziwym wstydem
jest to, e autor nie korzysta z moliwoci napisania nastpujcego kodu:
public int straightPay() {
return getTenthsWorked() * getTenthRate();
}
public int overTimePay() {
int overTimeTenths = Math.max(0, getTenthsWorked() - 400);
int overTimePay = overTimeBonus(overTimeTenths);
return straightPay() + overTimePay;
}
private int overTimeBonus(int overTimeTenths) {
double bonus = 0.5 * getTenthRate() * overTimeTenths;
return (int) Math.round(bonus);
}

Oczywicie, argument wybierajcy nie musi by typu boolean. Moe by to typ wyliczeniowy,
cakowity lub inny, ktry moe by uyty do wyboru zachowania funkcji. Zwykle lepiej mie wicej
funkcji, ni przekazywa pewien kod do funkcji w celu wybrania zachowania.

G16. Zaciemnianie intencji


Kod powinien by wyrazisty i zrozumiay. Rozwleke wyraenia, notacja wgierska i magiczne liczby
zaciemniaj intencje autora. Mamy tu na przykad funkcj overTimePay:
public int m_otCalc() {
return iThsWkd * iThsRte +
(int) Math.round(0.5 * iThsRte *
Math.max(0, iThsWkd - 400)
);
}

Wyglda ona na ma i zwiz, ale jest kompletnie enigmatyczna. Warto powici troch czasu na
ujawnienie czytelnikom zamierze naszego kodu.
ZAPACHY KODU I HEURYSTYKI

305

G17. le rozmieszczona odpowiedzialno


Jedn z najwaniejszych decyzji twrcy oprogramowania jest rozmieszczenie kodu. Gdzie na przykad powinna znale si staa PI? Czy powinna by w klasie Math? By moe naley do klasy
Trigonometry? A moe do klasy Circle?
Ma tu zastosowanie zasada najmniejszego zaskoczenia. Kod powinien by umieszczony tam,
gdzie spodziewa si go czytelnik. Staa PI powinna znale si tam, gdzie s zadeklarowane
funkcje trygonometryczne. Staa OVERTIME_RATE powinna znajdowa si w klasie HourlyPay
Calculator.
Czasami chcemy zastosowa sprytne rozwizanie okrelonych funkcji. Umieszczamy funkcj
tam, gdzie jest nam wygodnie, nie biorc pod uwag oczekiwa czytelnika. Moemy na przykad
potrzebowa raportu z sum godzin przepracowanych przez pracownika. Moemy zsumowa te
godziny w kodzie drukujcym raport albo sprbowa tworzy sum w kodzie akceptujcym karty
czasu pracy.
Decyzj t bdzie nam atwiej podj, gdy spojrzymy na nazwy funkcji. Zamy, e nasz modu
raportowy posiada funkcj o nazwie getTotalHours. Zamy rwnie, e modu akceptacji kart
czasu pracy posiada funkcj saveTimeCard. Ktra nazwa tych dwch funkcji informuje nas, e jest
w niej obliczana sumaryczna liczba godzin? Odpowied powinna by oczywista.
Czasami wzgldy wydajnociowe powoduj, e suma jest obliczana w czasie akceptacji kart pracy,
a nie w czasie drukowania raportu. Jest to do przyjcia pod warunkiem, e nazwy funkcji bd to
odzwierciedla. W takim przypadku w module kart czasu pracy powinna znajdowa si funkcja
computeRunningTotalOfHours.

G18. Niewaciwe metody statyczne


Przykadem dobrej metody statycznej jest Math.max(double a, double b). Nie dziaa ona na
jednym obiekcie, a w rzeczywistoci niewaciwe byoby korzystanie z new Math().max(a, b)
czy te a.max(b). Wszystkie dane wykorzystywane przez max pochodz z argumentw, a nie
z zawierajcego j obiektu. Co wicej, niemal nie ma szansy, abymy potrzebowali polimorficznej
metody Math.max.
Czasami jednak piszemy metody statyczne, ktre nie powinny by statyczne. Oto przykad:
HourlyPayCalculator.calculatePay(employee, overtimeRate).

Wydaje si, e jest to sensowna funkcja statyczna. Nie operuje ona na adnym okrelonym
obiekcie i pobiera wszystkie dane z argumentw. Jednak istnieje prawdopodobiestwo, e bdziemy potrzebowa, aby funkcja ta bya polimorficzna. Moemy chcie zaimplementowa kilka
rnych algorytmw obliczania pacy godzinowej, na przykad OvertimeHourlyPayCalculator,
czasami StraightTimeHourlyPayCalculator. Dlatego w tym przypadku funkcja nie powinna
by statyczna. Powinna by niestatyczn metod skadow klasy Employee.

306

ROZDZIA 17.

Powinnimy preferowa korzystanie z metod niestatycznych zamiast statycznych. Jeeli mamy


wtpliwoci, nie tworzymy funkcji statycznej. Jeeli faktycznie chcemy, aby funkcja bya statyczna,
musimy si upewni, e nie powinna ona dziaa polimorficznie.

G19. Uycie opisowych zmiennych


Kent Beck pisa o tym w swojej wspaniaej ksice Smalltalk Best Practice Patterns8 oraz pniej,
w rwnie dobrej pracy Implementation Patterns9. Jednym z najefektywniejszych sposobw zapewnienia czytelnoci programu jest podzielenie oblicze na kroki porednie, ktrych wyniki s przechowywane w zmiennych o znaczcych nazwach.
Przykadem moe by fragment kodu z FitNesse:
Matcher match = headerPattern.matcher(line);
if(match.find())
{
String key = match.group(1);
String value = match.group(2);
headers.put(key.toLowerCase(), value);
}

Dziki uyciu zmiennych opisowych od razu wiadomo, e pierwsza znaleziona grupa jest kluczem,
a druga wartoci.
Trudno zrobi to lepiej. Im wicej zmiennych opisowych, tym lepiej. Trudno uwierzy, jak szybko
nieczytelny modu staje si przejrzysty, gdy tylko podzielimy obliczenia na dobrze nazwane wartoci
porednie.

G20. Nazwy funkcji powinny informowa o tym, co realizuj


Spjrzmy na ten wiersz kodu:
Date newDate = date.add(5);

Czy moemy oczekiwa, e spowoduje to dodanie piciu dni do daty? Czy te tygodni lub godzin?
Czy obiekt date jest zmieniany, czy tylko funkcja zwraca nowy obiekt Date bez zmiany starego?
Nie jestemy w stanie stwierdzi na podstawie wywoania, co robi funkcja.
Jeeli funkcja dodaje pi dni do daty i zmienia j, to powinna by nazwana addDaysTo lub
increaseByDays. Jeeli jednak funkcja zwraca now dat pniejsz o pi dni i nie zmienia
bazowego obiektu daty, powinna by nazwana daysLater lub daysSince.
Jeeli musimy spojrze do implementacji (lub dokumentacji) funkcji w celu sprawdzenia, co ona
robi, to powinnimy znale jej lepsz nazw lub zmieni dziaanie tak, aby mogo by realizowane
przez funkcje o lepszych nazwach.

[Beck97], s. 108.

[Beck07].

ZAPACHY KODU I HEURYSTYKI

307

G21. Zrozumienie algorytmu


Dziki temu, e ludzie nie powicaj czasu na zrozumienie algorytmu, powstao wiele miesznego
kodu. Prbuj oni uzyska jakiekolwiek wyniki przez dodawanie odpowiedniej liczby instrukcji if
oraz znacznikw, zamiast zatrzyma si i sprawdzi, co naprawd powinni zrobi.
Programowanie czsto jest eksploracj. Uwaamy, e znamy odpowiedni algorytm czynnoci, ale
nastpnie mczymy si z nim, ugniatajc go i szturchajc, do momentu, a bdzie on dziaa.
Skd jednak wiemy, e dziaa? Poniewa realizuje wszystkie testy, o ktrych pomylelimy.
Nie ma nic zego w takim podejciu. W rzeczywistoci czsto jest to jedyny sposb na otrzymanie
potrzebnej funkcji. Jednak nie mona pozostawia cudzysoww wok dziaa.
Zanim uznamy, e funkcja jest zakoczona, upewnijmy si, e rozumiemy, jak ona dziaa. Nie wystarczy, e przechodzi wszystkie testy. Musimy wiedzie10, e rozwizanie jest prawidowe.
Najczciej najlepszym sposobem na uzyskanie tej wiedzy i zrozumienie algorytmu jest przebudowa
funkcji do postaci tak jasnej i ekspresywnej, e jest oczywiste, jak ona dziaa.

G22. Zamiana zalenoci logicznych na fizyczne


Jeeli jeden modu zaley od drugiego, to ta zaleno powinna by fizyczna, a nie tylko logiczna.
Modu zaleny nie powinien wykonywa zaoe (inaczej mwic, zalenoci logicznych) na temat
moduu, od ktrego zaley. Zamiast tego naley jawnie odpyta ten modu o informacje, na ktrych
nam zaley.
Zamy, e piszemy funkcj generujc tekstowy raport godzin przepracowanych przez pracownikw. Klasa o nazwie HourlyReporter zbiera wszystkie dane, a nastpnie przekazuje je do Hourly
ReportFormatter w celu ich wywietlenia (patrz listing 17.1).
L I S T I N G 1 7 . 1 . HourlyReporter.java
public class HourlyReporter {
private HourlyReportFormatter formatter;
private List<LineItem> page;
private final int PAGE_SIZE = 55;
public HourlyReporter(HourlyReportFormatter formatter) {
this.formatter = formatter;
page = new ArrayList<LineItem>();
}
public void generateReport(List<HourlyEmployee> employees) {
for (HourlyEmployee e : employees) {
addLineItemToPage(e);
if (page.size() == PAGE_SIZE)

10

Istnieje rnica pomidzy znajomoci dziaania kodu a pewnoci, e algorytm realizuje wymagane dziaanie. Brak pewnoci
co do algorytmu zdarza si, i czsto jest po prostu faktem. Brak pewnoci co do dziaania wasnego kodu jest po prostu lenistwem.

308

ROZDZIA 17.

printAndClearItemList();
}
if (page.size() > 0)
printAndClearItemList();
}
private void printAndClearItemList() {
formatter.format(page);
page.clear();
}
private void addLineItemToPage(HourlyEmployee e) {
LineItem item = new LineItem();
item.name = e.getName();
item.hours = e.getTenthsWorked() / 10;
item.tenths = e.getTenthsWorked() % 10;
page.add(item);
}
public class LineItem {
public String name;
public int hours;
public int tenths;
}
}

Kod ten zawiera zalenoci logiczne, ktre nie zostay fizycznie ujawnione. Czy moesz je znale?
Jest to staa PAGE_SIZE. Dlaczego klasa HourlyReporter zna wielko strony? Wielko ta powinna
by odpowiedzialnoci klasy HourlyReportFormatter.
Fakt deklaracji staej PAGE_SIZE w HourlyReporter jest le rozmieszczon odpowiedzialnoci [G17],
co powoduje, e HourlyReporter zakada, i wie, jaka powinna by wielko strony. Takie zaoenie
jest zalenoci logiczn. Klasa HourlyReporter zaley od tego, e HourlyReportFormatter moe
obsugiwa strony o dugoci 55 wierszy. Jeeli pewna implementacja HourlyReportFormatter
nie bdzie obsugiwaa takiej wielkoci, wystpi bd.
Mona ujawni t zaleno przez utworzenie w klasie HourlyReportFormatter nowej metody
o nazwie getMaxPageSize(). Klasa HourlyReporter powinna wywoa t funkcj, zamiast korzysta ze staej PAGE_SIZE.

G23. Zastosowanie polimorfizmu


zamiast instrukcji if-else lub switch-case
Wydaje si to dziwn sugesti po przedstawieniu zagadnienia w rozdziale 6. W kocu uznaem tam,
e instrukcje switch s lepsze w tych czciach systemu, w ktrych dodawanie nowych funkcji
jest bardziej prawdopodobne ni dodawanie nowych typw.
Po pierwsze, wikszo programistw uywa instrukcji switch, poniewa jest to rozwizanie
oczywiste, a nie dlatego, e jest to rozwizanie waciwe w danej sytuacji. Poruszamy ten temat, aby
przypomnie, e przed zastosowaniem instrukcji switch naley rozway uycie polimorfizmu.

ZAPACHY KODU I HEURYSTYKI

309

Po drugie, przypadki, w ktrych funkcje s bardziej ulotne od typw, s wzgldnie rzadkie. Dlatego
kada instrukcja switch powinna by dla nas podejrzana.
Osobicie korzystam z nastpujcej zasady jeden switch: W danej deklaracji typu moe znale si
najwyej jedna instrukcja switch. W takim przypadku instrukcja switch powinna tworzy obiekty
polimorficzne, ktre zastpuj instrukcje switch w pozostaych czciach systemu.

G24. Wykorzystanie standardowych konwencji


Kady zesp powinien korzysta ze standardw kodowania bazujcych na wsplnych normach
przemysowych. Standard kodowania powinien definiowa takie zagadnienia, jak miejsce deklaracji zmiennych instancyjnych, sposb nazewnictwa klas, metod i zmiennych, miejsce umieszczenia
klamer i tak dalej. Zesp nie powinien potrzebowa dokumentu opisujcego te konwencje, poniewa
przykady s zawarte w kodzie.
Kady czonek zespou powinien stosowa si do tych konwencji. Oznacza to, e kady czonek zespou powinien by na tyle dojrzay, aby zgodzi si, e nie ma znaczenia, gdzie umieszcza si
klamry, dopki wszyscy zgadzaj si na jedno miejsce.
Jeeli kto chciaby zna uywane przeze mnie konwencje, moe je zobaczy w przebudowanym
kodzie zamieszczonym w listingach od B.7 do B.14 w dodatku B.

G25. Zamiana magicznych liczb na stae nazwane


Jest to prawdopodobnie jedna z najstarszych zasad programowania. Pamitam j ju z podrcznikw
Cobola, Fortrana i PL/1 z lat szedziesitych ubiegego wieku. Zwykle zym pomysem jest korzystanie
z surowych liczb w kodzie. Powinnimy je ukrywa w dobrze nazwanych staych.
Na przykad liczba 86 400 powinna by ukryta w staej SECONDS_PER_DAY. Jeeli wywietlamy
55 wierszy na stron, to liczba 55 powinna by ukryta w staej LINES_PER_PAGE.
Niektre stae s tak proste do rozpoznania, e nie zawsze wymagaj staej nazwanej, o ile s poczone z bardzo dobrze opisujcym si kodem. Na przykad:
double milesWalked = feetWalked/5280.0;
int dailyPay = hourlyRate * 8;
double circumference = radius * Math.PI * 2;

Czy faktycznie w powyszych przykadach potrzebujemy staych FEET_PER_MILE, WORK_HOURS_PER_DAY


oraz TWO? Oczywicie, ostatni przypadek jest absurdalny. Istniej jednak wyraenia, w ktrych stae
s po prostu lepsze ni surowe liczby. Mona mie wtpliwoci co do staej WORK_HOURS_PER_DAY,
poniewa prawo i konwencje mog si zmienia. Z drugiej strony, wyraenie to czyta si tak dobrze
z cyfr, e bybym ostrony przed dodawaniem do niego 17 znakw. W przypadku FEET_PER_MILE
liczba 5280 jest tak dobrze znana i unikatowa, e czytelnik rozpozna j, nawet gdyby znajdowaa si
sama na stronie, bez adnego kontekstu.

310

ROZDZIA 17.

Stae, takie jak 3,141592653589793, s rwnie bardzo dobrze znane i atwo rozpoznawalne. Jednak
szansa popenienia bdu jest zbyt dua, aby pozostawi j w surowej postaci. Za kadym razem,
gdy kto zobaczy 3,1415927535890793, bdzie wiedzia, e jest to pi, i nie bdzie jej sprawdza (czy
zauwaye bd w jednej cyfrze?). Nie chcemy rwnie, aby uywane byy wartoci 3,14, 3,14159,
3,142 i tak dalej. Dlatego dobrze, e mamy ju zdefiniowan sta Math.PI.
Termin magiczna liczba nie odnosi si wycznie do liczb. Odnosi si do dowolnej wartoci, ktra
nie jest rozpoznawalna na pierwszy rzut oka. Na przykad:
assertEquals(7777, Employee.find("John Doe").employeeNumber());

W tej asercji mamy dwie magiczne liczby. Pierwsz jest 7777, poniewa jej znaczenie nie jest oczywiste.
Drug magiczn liczb jest "John Doe", poniewa i w tym przypadku intencje nie s jasne.
Wyglda to tak, jakby "John Doe" by nazw pracownika numer 7777 w testowej bazie danych,
utworzonej przez nasz zesp. Kady w zespole wie, e po podczeniu do tej bazy bdzie mia dostp
do danych kilku pracownikw majcych znane wartoci i atrybuty. Okazuje si wic, e "John Doe"
reprezentuje jedynego pracownika opacanego na godziny w tej testowej bazie danych. Dlatego test
ten powinien wyglda nastpujco:
assertEquals(
HOURLY_EMPLOYEE_ID,
Employee.find(HOURLY_EMPLOYEE_NAME).employeeNumber());

G26. Precyzja
Oczekiwanie, e pierwsze dopasowanie bdzie jedynym wynikiem zapytania, jest co najmniej naiwne. Uycie liczb zmiennoprzecinkowych do reprezentowania walut jest niemal przestpstwem.
Unikanie blokad i zarzdzania transakcjami, gdy nie uwaamy, e rwnolega aktualizacja jest
prawdopodobna, jest lenistwem. Deklarowanie zmiennej jako ArrayList, gdy wystarczy List, jest
nadmiernym ograniczeniem. Domylne oznaczanie wszystkich zmiennych jako protected jest
niewystarczajcym ograniczeniem.
Podejmujc decyzje dotyczce kodu, naley to robi precyzyjnie. Trzeba wiedzie, dlaczego s one
podjte i jak obsuy wszystkie wyjtki. Nie naley by leniwym, jeeli chodzi o precyzj decyzji.
Jeeli zdecydujemy, e funkcja moe zwraca null, naley upewni si, e warto null jest kontrolowana. Jeeli zadajemy zapytanie dotyczce elementu, ktry powinien by jedyny w bazie danych, nasz kod powinien sprawdzi, czy nie ma innych. Jeeli potrzebujemy obsugi walut, naley
uy liczb cakowitych11 i prawidowo obsugiwa zaokrglanie. Jeeli istnieje moliwo rwnolegej
aktualizacji, naley zaimplementowa mechanizm blokowania.
Niejednoznacznoci w kodzie s wynikiem niezgodnoci lub lenistwa. Bez wzgldu na ich przyczyn,
powinny zosta wyeliminowane.

11

Lub lepiej zastosowa klas Money wykorzystujc liczby cakowite.

ZAPACHY KODU I HEURYSTYKI

311

G27. Struktura przed konwencj


Decyzje projektowe naley wymusza przez zastosowanie struktury przed konwencj. Konwencje
nazewnictwa s dobre, ale s gorsze od struktur wymuszajcych zgodno. Na przykad instrukcje
switch-case z adnie nazwanymi wyliczeniami s gorsze od klas bazowych z metodami abstrakcyjnymi. Nikt nie jest zmuszany do implementowania instrukcji switch-case zawsze w taki sam
sposb, natomiast klasy bazowe mog wymusi, aby klasy konkretne miay zaimplementowane
wszystkie metody abstrakcyjne.

G28. Hermetyzacja warunkw


Wyraenia logiczne s wystarczajco trudne do zrozumienia, bez koniecznoci ogldania kontekstu
instrukcji if lub while. Warto wyodrbnia funkcj, ktra wyjania przeznaczenie warunku.
Na przykad:
if (shouldBeDeleted(timer))

jest lepsze ni
if (timer.hasExpired() && !timer.isRecurrent())

G29. Unikanie warunkw negatywnych


Warunki negatywne s nieco trudniejsze do zrozumienia ni pozytywne. Dlatego, jeeli jest to
moliwe, wyraenia powinny by formuowane jako warunki pozytywne. Na przykad:
if (buffer.shouldCompact())

jest lepsze ni
if (!buffer.shouldNotCompact())

G30. Funkcje powinny wykonywa jedn operacj


Czsto kuszce jest tworzenie funkcji, ktre maj wiele sekcji wykonujcych seri dziaa. Funkcje
tego rodzaju realizuj wicej ni jedn operacj i powinny zosta zamienione na mniejsze, z ktrych
kada wykonuje jedn operacj.
Na przykad:
public void pay() {
for (Employee e : employees) {
if (e.isPayday()) {
Money pay = e.calculatePay();
e.deliverPay(pay);
}
}
}

312

ROZDZIA 17.

Ten fragment kodu wykonuje trzy operacje. Przeglda dane pracownikw, sprawdza, czy kolejnemu pracownikowi naley zapaci, a nastpnie realizuje wypat. Lepiej zapisa go tak:
public void pay() {
for (Employee e : employees)
payIfNecessary(e);
}
private void payIfNecessary(Employee e) {
if (e.isPayday())
calculateAndDeliverPay(e);
}
private void calculateAndDeliverPay(Employee e) {
Money pay = e.calculatePay();
e.deliverPay(pay);
}

Teraz kada z tych funkcji wykonuje jedn operacj (patrz Wykonuj jedn czynno w rozdziale
3.).

G31. Ukryte sprzenia czasowe


Sprzenia czasowe s czsto potrzebne, ale nie powinny ukrywa zalenoci. Naley tak uporzdkowa argumenty funkcji, aby kolejno ich wywoywania bya oczywista. Wemy pod uwag nastpujcy kod:
public class MoogDiver {
Gradient gradient;
List<Spline> splines;
public void dive(String reason) {
saturateGradient();
reticulateSplines();
diveForMoog(reason);
}
...
}

Kolejno tych trzech funkcji jest wana. Naley nasyci gradient przed retykulacj splajnw i tylko
po tej operacji mona wykona ostatni funkcj. Niestety, kod ten nie wymusza tego sprzenia
czasowego. Inny programista moe wywoa reticulateSplines przed saturateGradient,
doprowadzajc do wyjtku UnsaturatedGradientException. Lepszym rozwizaniem jest nastpujca konstrukcja:
public class MoogDiver {
Gradient gradient;
List<Spline> splines;
public void dive(String reason) {
Gradient gradient = saturateGradient();
List<Spline> splines = reticulateSplines(gradient);
diveForMoog(splines, reason);
}
...
}

ZAPACHY KODU I HEURYSTYKI

313

Ujawnia to sprzenie czasowe przez utworzenie zestawu kubekw. Kada funkcja wytwarza wynik potrzebny nastpnej funkcji, wic nie ma sensownego sposobu na wywoanie ich w nieprawidowej kolejnoci.
Mona narzeka, e zwiksza to zoono funkcji, i trzeba si z tym zgodzi. Jednak dodatkowa
zoono skadniowa ujawnia prawdziwe sprzenie czasowe w tej sytuacji.
Naley zwrci uwag, e pozostawiem zmienne instancyjne. Zakadam, e bd one potrzebne
metodom prywatnym w tej klasie. Nawet pomimo tego chcemy, aby sprzenia czasowe byy
ujawnione.

G32. Unikanie dowolnych dziaa


Struktura kodu nie powinna by przypadkowa. Jeeli struktura wyglda na dowoln, inne osoby
czuj si zobowizane do jej zmiany. Jeeli struktura jest spjna w caym systemie, inne osoby bd jej
uywa w celu zachowania konwencji. Na przykad ostatnio czyem zmiany w FitNesse i odkryem,
e jeden z programistw doda taki kod:
public class AliasLinkWidget extends ParentWidget
{
public static class VariableExpandingWidgetRoot {
...
...
}

Problemem jest to, e VariableExpandingWidgetRoot nie musi by w zakresie AliasLinkWidget.


Co wicej, inne niezwizane klasy korzystay z AliasLinkWidget.VariableExpandingWidgetRoot.
Klasy te nie musiay nic wiedzie na temat AliasLinkWidget.
Prawdopodobnie programista wrzuci VariableExpandingWidgetRoot do AliasWidget dla wygody albo myla, e faktycznie bdzie potrzebowa operacji wewntrz AliasWidget. Niezalenie
od powodu, wynik wyglda na dosy dowolny. Klasy publiczne, ktre nie s narzdziami innej klasy,
nie powinny by zagbione w innej klasie. Zgodnie z konwencj, naley zadeklarowa je jako
publiczne na poziomie pakietu.

G33. Hermetyzacja warunkw granicznych


Warunki graniczne s trudne do obsugi. Naley umieci ich przetwarzanie w jednym miejscu.
Nie naley dopuszcza, aby rozlay si po caym kodzie. Nie chcemy stada +1 i -1 wygldajcych
zza kadego rogu. Przykadem moe by fragment kodu z FIT:
if(level + 1 < tags.length)
{
parts = new Parse(body, tags, level + 1, offset + endTag);
body = null;
}

Mona zauway, e level+1 wystpuje dwa razy. Jest to warunek graniczny, ktry powinien by
hermetyzowany wewntrz zmiennej o nazwie podobnej do nextLevel.

314

ROZDZIA 17.

int nextLevel = level + 1;


if(nextLevel < tags.length)
{
parts = new Parse(body, tags, nextLevel, offset + endTag);
body = null;
}

G34. Funkcje powinny zagbia si na jeden poziom abstrakcji


Instrukcje wewntrz funkcji powinny by pisane na tym samym poziomie abstrakcji, ktry powinien by o jeden poziom niszy od operacji opisanych w nazwie funkcji. Moe by to najtrudniejsza do interpretacji i stosowania z wszystkich opisanych tu heurystyk. Cho zagadnienie jest dosy
proste, ludzie s po prostu zbyt dobrzy w pynnym mieszaniu poziomw abstrakcji. Jako przykad
wemy nastpujcy kod z FitNesse:
public String render() throws Exception
{
StringBuffer html = new StringBuffer("<hr");
if(size > 0)
html.append(" size=\"").append(size + 1).append("\"");
html.append(">");
return html.toString();
}

Krtka analiza pozwala stwierdzi, co si tu dzieje. Funkcja ta tworzy znacznik HTML rysujcy poziom lini na stronie. Wysoko tej linii jest zdefiniowana w zmiennej size.
Teraz spjrzmy na niego jeszcze raz. Metoda ta miesza ze sob co najmniej dwa poziomy abstrakcji.
Pierwszym jest informacja, e linia pozioma posiada wielko. Drugim jest sama skadnia znacznika HR. Kod ten pochodzi z moduu HruleWidget z FitNesse. Modu ten wykrywa wiersz z czterema lub wicej minusami i konwertuje je na odpowiedni znacznik HR. Im wicej minusw, tym
wikszy rozmiar.
Poniej zamieszczony jest kod po przebudowie. Naley zauway, e zmieniem nazw pola
size, aby odzwierciedlaa jego prawdziwe przeznaczenie. Przechowuje ono liczb dodatkowych
minusw.
public String render() throws Exception
{
HtmlTag hr = new HtmlTag("hr");
if (extraDashes > 0)
hr.addAttribute("size", hrSize(extraDashes));
return hr.html();
}
private String hrSize(int height)
{
int hrSize = height + 1;
return String.format("%d", hrSize);
}

Zmiana ta pozwolia na eleganckie rozdzielenie dwch poziomw abstrakcji. Funkcja render tworzy
dla nas znacznik HR, bez koniecznoci znajomoci skadni HTML tego znacznika. Modu HtmlTag
zajmuje si wszystkimi problemami skadniowymi.

ZAPACHY KODU I HEURYSTYKI

315

Wprowadzajc t zmian, znalazem subtelny bd. Oryginalny kod nie umieszcza zamykajcego
ukonika znacznika HR, tak jak definiuje to standard XHTML (inaczej mwic, generowa <hr>
zamiast <hr/>). Modu HtmlTag zosta ju dawno tak zmieniony, aby by zgodny z XHTML-em.
Rozdzielenie poziomw abstrakcji jest jednym z najwaniejszych zada przebudowy, przy tym
najtrudniejszym do wykonania. Jako przykad wemy poniszy kod. Bya to moja pierwsza prba
rozdzielenia poziomw abstrakcji w metodzie HruleWidget.render.
public String render() throws Exception
{
HtmlTag hr = new HtmlTag("hr");
if (size > 0) {
hr.addAttribute("size", ""+(size+1));
}
return hr.html();
}

Moim celem byo zapewnienie odpowiedniej separacji i umoliwienie wykonywania testw. atwo
zrealizowaem te cele, ale wynikiem bya funkcja, ktra nadal mieszaa poziomy abstrakcji. W tym
przypadku wymieszanymi poziomami byy: tworzenie znacznika HR oraz interpretacja i formatowanie zmiennej size. W tym przypadku okazao si, e gdy podzielimy funkcj wzdu poziomw
abstrakcji, czsto wykrywamy nastpne, ukryte przez poprzedni struktur.

G35. Przechowywanie danych konfigurowalnych


na wysokim poziomie
Jeeli mamy stae, takie jak wartoci domylne lub konfiguracyjne, ktre s oczekiwane na wysokim poziomie abstrakcji, to nie naley ukrywa ich w funkcjach niskiego poziomu. Naley ujawni je
jako argumenty, z uyciem ktrych funkcje niskiego poziomu s wywoywane z funkcji wysokiego
poziomu. Wemy pod uwag nastpujcy kod z FitNesse:
public static void main(String[] args) throws Exception
{
Arguments arguments = parseCommandLine(args);
...
}
public class Arguments
{
public static final String DEFAULT_PATH = ".";
public static final String DEFAULT_ROOT = "FitNesseRoot";
public static final int DEFAULT_PORT = 80;
public static final int DEFAULT_VERSION_DAYS = 14;
...
}

Argumenty wiersza polece s analizowane w pierwszym wykonywalnym wierszu FitNesse.


Wartoci domylne tych argumentw s specyfikowane w klasie Arguments. Nie musimy zaglda
do niskich poziomw systemu za pomoc takich instrukcji:
if (arguments.port == 0) // Uywamy domylnie 80.

316

ROZDZIA 17.

Stae konfiguracji znajduj si na bardzo wysokim poziomie i atwo je zmieni. S one przekazywane w d, do pozostaych czci aplikacji. Nisze poziomy aplikacji nie zawieraj wartoci tych
staych.

G36. Unikanie nawigacji przechodnich


Zazwyczaj nie chcemy, aby jeden modu zna swoich wsppracownikw. Mwic dokadniej,
jeeli A wspdziaa z B, a B wsppracuje z C, to nie chcemy, aby moduy korzystajce z A wiedziay
co o C (na przykad nie chcemy instrukcji a.getB().getC().doSomething();).
Jest to czasami nazywane prawem Demeter. Pragmatyczni programici nazywaj to pisaniem
wstydliwego kodu12. W obu przypadkach sprowadza si to do upewnienia si, e moduy znaj
tylko swoich bezporednich wsppracownikw i nie znaj mapy nawigacji w caym systemie.
Jeeli wiele moduw korzysta z instrukcji takich jak a.getB().getC(), to trudno zmieni projekt
i architektur i wstawi Q pomidzy B a C. Konieczne bdzie wyszukanie wszystkich wystpie
a.getB().getC() i ich zamiana na a.getB().getQ().getC(). W ten sposb architektura staje si
sztywna. Zbyt wiele moduw wie zbyt duo o architekturze.
Zamiast tego oczekujemy, e poredni wsppracownicy bd oferowali wszystkie potrzebne usugi.
Nie powinnimy przeglda grafu obiektw w systemie, polujc na metod do wywoania. Zamiast
tego powinnimy by w stanie skorzysta z:
myCollaborator.doSomething().

Java
J1. Unikanie dugich list importu
przez uycie znakw wieloznacznych
Jeeli uywamy dwch lub wicej klas z pakietu, to warto zaimportowa go w caoci przez uycie:
import package.*;

Dugie listy importu s nuce dla czytelnika. Nie chcemy zamieca pocztku moduu ponad
80 wierszami importw. Chcemy raczej, aby importy byy zwizymi instrukcjami opisujcymi
pakiety, z ktrymi wsppracujemy.
Specyficzne importy s trwaymi zalenociami, natomiast importy ze znakami wieloznacznymi
nie. Jeeli zaimportujemy specyficzn klas, to musi ona istnie. Jeeli jednak zaimportujemy pakiet z uyciem znakw wieloznacznych, nie musi istnie adna okrelona klasa. Instrukcja importu
po prostu dodaje pakiet do cieki przeszukiwania, uywanej przy polowaniu na nazwy. Dlatego za
pomoc importu nie s tworzone konkretne zalenoci, dziki czemu moduy s mniej sprzone.
12

[PRAG], s. 138.

ZAPACHY KODU I HEURYSTYKI

317

Istniej przypadki, w ktrych dugie listy importw mog by przydatne. Na przykad, jeeli korzystamy z zastanego kodu i musimy dowiedzie si, ktrych klas potrzebujemy w celu zbudowania
imitacji i zrbw, moemy przeglda list specyficznych importw, szukajc nazw kwalifikowanych wszystkich tych klas, a nastpnie zastpi je zrbami. Jednak takie zastosowanie specyficznych
importw jest bardzo rzadkie. Dodatkowo wikszo nowoczesnych rodowisk IDE pozwala na
zamian importw ze znakami wieloznacznymi na list specyficznych importw po wykonaniu
jednego polecenia. Dlatego nawet w zastanym kodzie lepiej korzysta z importw wieloznacznych.
Importy wieloznaczne mog czasami powodowa konflikty nazw i niejednoznacznoci. Dwie klasy
o tej samej nazwie, ale znajdujce si w rnych pakietach, bd wymagay specyficznego importu
lub co najmniej kwalifikowania w czasie uycia. Moe to by niuans, ale jest tak rzadki, e najczciej
importowanie z uyciem znakw wieloznacznych jest lepsze od specyficznych importw.

J2. Nie dziedziczymy staych


Widziaem to kilka razy i zawsze wywouje to grymas na mojej twarzy. Programista umieszcza stae
w interfejsie, a nastpnie uzyskuje dostp do tych staych przez dziedziczenie interfejsu. Przyjrzyjmy
si nastpujcemu przykadowi kodu:
public class HourlyEmployee extends Employee {
private int tenthsWorked;
private double hourlyRate;
public Money calculatePay() {
int straightTime = Math.min(tenthsWorked, TENTHS_PER_WEEK);
int overTime = tenthsWorked - straightTime;
return new Money(
hourlyRate * (tenthsWorked + OVERTIME_RATE * overTime)
);
}
...
}

Skd pochodz stae TENTHS_PER_WEEK oraz OVERTIME_RATE? Mog pochodzi z klasy Employee,
wic rzumy na ni okiem:
public abstract class Employee implements PayrollConstants {
public abstract boolean isPayday();
public abstract Money calculatePay();
public abstract void deliverPay(Money pay);
}

Nie, nie ma ich tu. No to gdzie s? Spjrzmy dokadniej na klas Employee. Implementuje ona
PayrollConstants.
public interface PayrollConstants {
public static final int TENTHS_PER_WEEK = 400;
public static final double OVERTIME_RATE = 1.5;
}

Jest to fatalna praktyka! Stae s ukryte na szczycie hierarchii dziedziczenia. Okropno! Nie naley
uywa dziedziczenia jako sposobu oszukania zasad zakresu zdefiniowanych w jzyku. Zamiast tego
naley korzysta z importu statycznego.

318

ROZDZIA 17.

import static PayrollConstants.*;


public class HourlyEmployee extends Employee {
private int tenthsWorked;
private double hourlyRate;
public Money calculatePay() {
int straightTime = Math.min(tenthsWorked, TENTHS_PER_WEEK);
int overTime = tenthsWorked - straightTime;
return new Money(
hourlyRate * (tenthsWorked + OVERTIME_RATE * overTime)
);
}
...
}

J3. Stae kontra typy wyliczeniowe


Teraz, gdy do jzyka zostao dodane sowo kluczowe enum (Java 5), warto z niego korzysta! Nie
naley trzyma si starych sztuczek public static final int. Znaczenie wartoci int moe zosta
utracone. Znaczenie typu wyliczeniowego nie moe, poniewa naley do nazwanego wyliczenia.
Co wicej, warto uwanie przestudiowa skadni enum. Mog one posiada metody i pola. Powoduje to, e staj si bardzo potnym narzdziem i pozwalaj na uzyskanie znacznie wikszej
ekspresywnoci i elastycznoci ni liczby int. Przeanalizujmy odmian wczeniejszego kodu obliczania pac:
public class HourlyEmployee extends Employee {
private int tenthsWorked;
HourlyPayGrade grade;
public Money calculatePay() {
int straightTime = Math.min(tenthsWorked, TENTHS_PER_WEEK);
int overTime = tenthsWorked - straightTime;
return new Money(
grade.rate() * (tenthsWorked + OVERTIME_RATE * overTime)
);
}
...
}
public enum HourlyPayGrade {
APPRENTICE {
public double rate() {
return 1.0;
}
},
LEUTENANT_JOURNEYMAN {
public double rate() {
return 1.2;
}
},
JOURNEYMAN {
public double rate() {
return 1.5;
}
},
MASTER {
public double rate() {

ZAPACHY KODU I HEURYSTYKI

319

return 2.0;
}
};
public abstract double rate();
}

Nazwy
N1. Wybr opisowych nazw
Nie naley zbyt szybko wybiera nazwy. Naley upewni si, e jest opisowa. Trzeba pamita, e
znaczenie ma tendencj do dryfowania wraz z rozwojem oprogramowania, wic czste sprawdzanie
dokadnoci nazw jest wskazane.
To nie jest tylko rekomendacja dobrego samopoczucia. W 90 procentach nazwy w kodzie decyduj o jego czytelnoci. Nazwy s zbyt wane, aby je traktowa beztrosko.
Wemy pod uwag poniszy kod. Co on robi? Jeeli poka go z uytymi odpowiednimi nazwami,
bdzie dla nas zupenie sensowny, ale w tej postaci jest zlepkiem symboli i magicznych liczb.
public int x() {
int q = 0;
int z = 0;
for (int kk = 0; kk < 10; kk++) {
if (l[z] == 10)
{
q += 10 + (l[z + 1] + l[z + 2]);
z += 1;
}
else if (l[z] + l[z + 1] == 10)
{
q += 10 + l[z + 2];
z += 2;
} else {
q += l[z] + l[z + 1];
z += 2;
}
}
return q;
}

Poniej mamy ten sam kod zapisany we waciwy sposb. Ten fragment jest w rzeczywistoci mniej
kompletny ni pokazany powyej. Niemal natychmiast mona wywnioskowa, co on ma robi,
i z duym prawdopodobiestwem moglibymy od razu napisa brakujce funkcje, bazujc tylko na
wywnioskowanym znaczeniu. Magiczne liczby przestay by magiczne, a struktura algorytmu jest
zachcajco opisowa.
public int score() {
int score = 0;
int frame = 0;
for (int frameNumber = 0; frameNumber < 10; frameNumber++) {
if (isStrike(frame)) {
score += 10 + nextTwoBallsForStrike(frame);
frame += 1;

320

ROZDZIA 17.

} else if (isSpare(frame)) {
score += 10 + nextBallForSpare(frame);
frame += 2;
} else {
score += twoBallsInFrame(frame);
frame += 2;
}
}
return score;
}

Si dobrze dobranych nazw jest uzupenianie struktury kodu przy uyciu opisw. Pozwala to na
przystosowanie oczekiwa czytelnika do tego, co realizuj inne funkcje w module. Czy moemy
wywnioskowa implementacj isStrike(), spogldajc na powyszy kod? Gdy bdziemy czyta
metod isStrike, bdzie robia mniej wicej to, czego oczekiwalimy13.
private boolean isStrike(int frame) {
return rolls[frame] == 10;
}

N2. Wybr nazw na odpowiednich poziomach abstrakcji


Nie naley wybiera nazw informujcych o implementacji, ale raczej nazwy odzwierciedlajce poziom abstrakcji, na ktrym operuje klasa lub funkcja. Jest to trudne do realizacji. Przypomnijmy, e
ludzie s zbyt dobrzy w mieszaniu poziomw abstrakcji. Za kadym razem, gdy przejrzysz swj
kod, najprawdopodobniej znajdziesz kilka zmiennych, ktrych nazwy s na zbyt niskim poziomie.
Po znalezieniu takich nazw warto wykorzysta okazj i zmieni je. Proces tworzenia czytelnego kodu
wymaga chci do staej poprawy. Wemy jako przykad poniszy interfejs Modem:
public interface Modem {
boolean dial(String phoneNumber);
boolean disconnect();
boolean send(char c);
char recv();
String getConnectedPhoneNumber();
}

Na pierwszy rzut oka wyglda wietnie. Wszystkie funkcje wydaj si odpowiednie. Faktycznie, dla
wielu zastosowa s takie. Wemy pod uwag aplikacj, w ktrej modemy nie s czone z uyciem
wybierania. Zamiast tego s poczone na stae (tak jak modemy kablowe uywane obecnie do dostarczania internetu w wielu domach). By moe niektre s czone przez wysyanie numeru
portu do przecznika, z uyciem poczenia USB. Jasne jest, e notacja numerw telefonw znajduje si na niewaciwym poziomie abstrakcji. Lepsz strategi nazewnicz dla tego scenariusza
moe by:
public interface Modem {
boolean connect(String connectionLocator);
boolean disconnect();
boolean send(char c);
char recv();
String getConnectedLocator();
}
13

Patrz cytat wypowiedzi Warda Cunninghama z rozdziau 1.

ZAPACHY KODU I HEURYSTYKI

321

Teraz nazwy nie zawieraj adnych odwoa do numerw telefonicznych. Numery telefoniczne
mog by nadal uywane, mona te zastosowa inn strategi poczenia.

N3. Korzystanie ze standardowej nomenklatury tam,


gdzie jest to moliwe
Nazwy s atwiejsze do zrozumienia, jeeli bazuj na istniejcych konwencjach lub zastosowaniach.
Na przykad, jeeli korzystamy z wzorca Dekorator, to powinnimy uy sowa Decorator w nazwach klas dekorujcych. Nazw klasy dekorujcej Modem i zapewniajcej automatyczne rozczanie
na kocu sesji moe by AutoHangupModemDecorator.
Wzorce s po prostu jednym z rodzajw standardw. W jzyku Java funkcje konwertujce obiekty
na posta znakow s czsto nazywane toString. Lepiej zachowa zgodno z tego typu konwencjami, ni wymyla wasne.
Zespoy czsto wymylaj wasne standardy nazewnictwa dla okrelonych projektw. Eric Evans nazywa
to wszechobecnym jzykiem projektu14. W naszym kodzie powinnimy intensywnie uywa terminw
z tego jzyka. Mwic krtko, im wicej uywamy nazw przeadowanych znaczeniami odnoszcymi
si do naszego projektu, tym atwiej bdzie czytelnikowi zrozumie, do czego suy dany kod.

N4. Jednoznaczne nazwy


Naley wybiera nazwy, ktre nie pozostawiaj niejednoznacznoci co do dziaania funkcji lub
zmiennej. Spjrzmy na fragment kodu z FitNesse:
private String doRename() throws Exception
{
if(refactorReferences)
renameReferences();
renamePage();
pathToRename.removeNameFromEnd();
pathToRename.addNameToEnd(newName);
return PathParser.render(pathToRename);
}

Nazwa tej funkcji nie mwi nam, co funkcja realizuje, poza oglnym i niejasnym pojciem. Odczucie
to jest jeszcze powikszane przez fakt, e wewntrz funkcji doRename znajduje si wywoanie funkcji
renamePage! Co te nazwy mwi nam o rnicach pomidzy tymi dwoma funkcjami? Nic.
Lepsz nazw dla tej funkcji byaby renamePageAndOptionallyAllReferences. Moe si ona
wydawa duga (bo taka faktycznie jest), ale funkcja jest wywoywana z jednego miejsca w module,
wic warto informacyjna tej nazwy przewysza problemy wynikajce z jej dugoci.

14

[DDD].

322

ROZDZIA 17.

N5. Uycie dugich nazw dla dugich zakresw


Dugo nazwy powinna by zwizana z dugoci zakresu. Mona uywa bardzo krtkich nazw
dla niewielkich zakresw, ale dla duych zakresw powinny by uywane dugie nazwy.
Nazwy zmiennych takie jak i oraz j s wietne, o ile ich zakres ma dugo najwyej piciu wierszy.
Oto fragment kodu starej gry w krgle:
private void rollMany(int n, int pins)
{
for (int i=0; i<n; i++)
g.roll(pins);
}

Jest to zupenie jasne i jeeli zmienna i zostaaby zastpiona nieco irytujc, jak rollCount, kod byby
trudniejszy do zrozumienia. Z drugiej strony, zmienne i funkcje o krtkich nazwach trac swoje znaczenie na duszych dystansach. Dlatego im szerszy zakres nazwy, tym powinna by ona dusza i precyzyjniejsza.

N6. Unikanie kodowania


W nazwach nie powinny by kodowane informacje o typie i zakresie. Przedrostki, takie jak m_ czy
f, s w dzisiejszych rodowiskach bezuyteczne. Rwnie kodowanie projektu lub (i) podsystemu,
takiego jak vis_ (dla systemu przetwarzania wizualnego), jest rozpraszajce i nadmiarowe. Stosowane
obecnie rodowiska zapewniaj wszystkie te informacje bez koniecznoci doczania ich do nazw.
Naley zachowa nazwy wolne od skaenia notacj wgiersk.

N7. Nazwy powinny opisywa efekty uboczne


Nazwy powinny opisywa wszystko, co wykonuje dana funkcja, zmienna lub klasa. Nie naley
ukrywa efektw ubocznych w nazwie. Nie naley uywa prostego sowa do opisania funkcji, ktra
wykonuje wicej ni tylko t prost akcj. Przykadem moe by nastpujcy kod z TestNG:
public ObjectOutputStream getOos() throws IOException {
if (m_oos == null) {
m_oos = new ObjectOutputStream(m_socket.getOutputStream());
}
return m_oos;
}

Funkcja ta realizuje wicej ni tylko pobieranie oos; ona tworzy oos, jeeli nie by wczeniej
utworzony. Dlatego lepsz nazw moe by createOrReturnOos.

ZAPACHY KODU I HEURYSTYKI

323

Testy
T1. Niewystarczajce testy
Ile testw powinno znajdowa si w zestawie testowym? Niestety, wielu programistw stosuje miar
to powinno wystarczy. Zestaw testw powinien sprawdza wszystko, co moe zawie. Testy
s niewystarczajce dopty, dopki bd istnie warunki nieskontrolowane przez testy lub niesprawdzone obliczenia.

T2. Uycie narzdzi kontroli pokrycia


W naszej strategii testowania konieczne jest wykorzystanie narzdzi raportujcych pokrycie kodu.
Uatwiaj one znalezienie moduw, klas i funkcji, ktre s niewystarczajco testowane. Wikszo rodowisk IDE posiada wizualn prezentacj pokrycia, oznaczajc wiersze pokryte testami
kolorem zielonym, a niepokryte kolorem czerwonym. Pozwala to szybko i atwo znale instrukcje
if oraz catch, ktrych tre nie jest sprawdzana.

T3. Nie pomijaj prostych testw


S one bardzo proste do napisania, a ich warto dokumentalna jest wysza ni koszt produkcji.

T4. Ignorowany test jest wskazaniem niejednoznacznoci


Czasami jestemy niepewni szczegw dziaania, poniewa wymagania s niejasne. Moemy wyrazi nasze pytanie na temat wymagania w postaci zakomentowanego testu lub testu oznaczonego
za pomoc @Ignore. Metoda, jak wybierzemy, zaley od tego, czy niejednoznaczno dotyczy
czego, co si kompiluje, czy nie.

T5. Warunki graniczne


Szczegln uwag naley powici warunkom granicznym. Czsto prawidowo realizujemy rodkow
cz algorytmu, ale mamy bdy w warunkach granicznych.

T6. Dokadne testowanie pobliskich bdw


Bdy maj tendencj do czenia si. Gdy znajdziemy bd w funkcji, mdrze jest wykona dokadne
testy funkcji. Prawdopodobnie okae si, e ten bd nie by jedynym.

T7. Wzorce bdw wiele ujawniaj


Czasami moemy zdiagnozowa problem przez znalezienie wzorca bdw przypadkw testowych. Jest to
kolejny argument przemawiajcy za tworzeniem moliwie kompletnych przypadkw testowych. Kompletne przypadki testowe, ktre s uporzdkowane w sensowny sposb, pozwalaj ujawnia takie wzorce.

324

ROZDZIA 17.

Moe by to przypadek, gdy nie wszystkie testy s wykonywane z danymi wejciowymi duszymi
ni pi znakw. Innym przykadem moe by ze wykonywanie wszystkich testw, w ktrych drugi
argument jest liczb ujemn. Czasami tylko zobaczenie wzorw kolorw czerwonego i zielonego na
raporcie testw wystarczy nam do znalezienia rozwizania. Interesujce przykady mona znale
w rozdziale 16. przy okazji przedstawiania przykadu klasy SerialDate.

T8. Wzorce pokrycia testami wiele ujawniaj


Spojrzenie na kod, ktry jest lub nie jest wykonywany przez testy, daje wskazwki odnonie do
przyczyny bdu testowania.

T9. Testy powinny by szybkie


Powolne testy nie bd wykonywane. Gdy zacznie nam brakowa czasu, powolne testy bd usuwane
z zestawu. Dlatego naley robi wszystko, aby testy byy szybkie.

Zakoczenie
Trudno stwierdzi, czy ta lista zapachw i heurystyk jest kompletna. W rzeczywistoci nie jestem
pewien, czy taka lista kiedykolwiek bdzie kompletna. Jednak kompletno nie powinna by tu celem,
poniewa lista ta pozwala na ocen systemu.
W rzeczywistoci ocena systemu powinna by celem i tematem tej ksiki. Czysty kod nie jest tworzony przez zamieszczon tu list zasad. Nikt nie stanie si wietnym programist przez nauczenie
si listy heurystyk. Profesjonalizm i umiejtnoci bior si z dyscypliny ich stosowania.

Bibliografia
[Refactoring]: Martin Fowler i inni, Refactoring: Improving the Design of Existing Code, AddisonWesley 1999.
[PRAG]: Andrew Hunt, Dave Thomas, The Pragmatic Programmer, Addison-Wesley 2000.
[GOF]: Gamma i inni, Elements of Reusable Object Oriented Software, Addison-Wesley 1996.
[Beck97]: Kent Beck, Smalltalk Best Practice Patterns, Prentice Hall 1997.
[Beck07]: Kent Beck, Implementation Patterns, Addison-Wesley 2008.
[PPP]: Robert C. Martin, Agile Software Development: Principles, Patterns, and Practices, Prentice
Hall 2002.
[DDD]: Eric Evans, Domain Driven Design, Addison-Wesley 2003.

ZAPACHY KODU I HEURYSTYKI

325

326

ROZDZIA 17.

DODATEK A

Wspbieno II
Brett L. Schuchert

rozdzia 13., Wspbieno. Zosta on napisany w postaci serii niezaleD nych tematw, ktre mog
by czytane w dowolnej kolejnoci. Aby umoliwi taki sposb czytaODATEK TEN UZUPENIA

nia, wystpuje tu kilka powtrze midzy punktami.

Przykad klient-serwer
Wyobramy sobie prost aplikacj klient-serwer. Serwer nasuchuje na gniedzie, oczekujc na
podczenie si klienta. Klient podcza si i wysya danie.

Serwer
Poniej przedstawiona jest uproszczona wersja aplikacji serwera. Peny kod rdowy zaczyna si
w dalszej czci dodatku, w punkcie Klient-serwer bez wtkw.
ServerSocket serverSocket = new ServerSocket(8009);
while (keepProcessing) {
try {
Socket socket = serverSocket.accept();
process(socket);
} catch (Exception e) {
handle(e);
}
}

327

Ta prosta aplikacja oczekuje na poczenia, przetwarza przychodzce komunikaty i znw oczekuje


na przyjcie nastpnego dania klienta. Poniej mamy kod klienta podczajcego si do tego serwera:
private void connectSendReceive(int i) {
try {
Socket socket = new Socket("localhost", PORT);
MessageUtils.sendMessage(socket, Integer.toString(i));
MessageUtils.getMessage(socket);
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}

Jak szybko dziaa para klient-serwer? W jaki sposb moemy formalnie opisa jej wydajno? Mamy
tu test, ktry zakada, e wydajno jest akceptowalna:
@Test(timeout = 10000)
public void shouldRunInUnder10Seconds() throws Exception {
Thread[] threads = createThreads();
startAllThreadsw(threads);
waitForAllThreadsToFinish(threads);
}

Konfiguracja nie jest tu zamieszczona, aby uproci nasz test (patrz listing A4. ClientTest.java).
Test ten zakada, e powinien zakoczy si w czasie 10 000 milisekund.
Jest to klasyczny przykad kontroli przepustowoci systemu. System ten powinien zakoczy seri
da klientw w czasie dziesiciu sekund. Jeeli serwer bdzie w stanie przetworzy poszczeglne
dania klienta na czas, test si powiedzie.
Co si stanie, jeeli test si nie uda? Poza utworzeniem ptli odpytywania zdarze nie da si zrobi
zbyt wiele z jednym wtkiem w celu przyspieszenia dziaania tego kodu. Czy zastosowanie wielu
wtkw rozwie problem? By moe, ale musimy wczeniej wiedzie, gdzie jest marnowany czas.
Istniej dwie moliwoci:

Wejcie-wyjcie korzystanie z gniazd, podczanie si do bazy danych, oczekiwanie na


przeczenie pamici wirtualnej i tak dalej.

Procesor obliczenia numeryczne, przetwarzanie wyrae regularnych, zbieranie nieuytkw


i tak dalej.

Zwykle systemy maj kilka takich wskich garde, ale w przypadku danej operacji najczciej dominuje jedno. Jeeli kod jest zwizany z procesorem, to lepszy sprzt przetwarzajcy moe poprawi wydajno i testy si powiod. Jednak mamy dostpn tylko okrelon liczb cykli CPU, wic
dodanie wtkw do problemu zwizanego z procesorem nie przyspieszy dziaania programu.
Jeeli jednak proces jest ograniczany przez wejcie-wyjcie, to wspbieno moe poprawi efektywno. Gdy jedna cz systemu oczekuje na operacj wejcia-wyjcia, inna cz moe wykorzysta ten czas oczekiwania do przetworzenia czego innego, bardziej efektywnie korzystajc z dostpnych zasobw procesora.

328

DODATEK A

Dodajemy wtki
Zamy teraz, e test wydajnociowy si nie powid. Jak moemy poprawi przepustowo, aby
zaliczy ten test? Jeeli metoda serwera process jest ograniczana przez wejcie-wyjcie, to moemy
w serwerze zastosowa wtki (wystarczy zmieni processMessage):
void process(final Socket socket) {
if (socket == null)
return;
Runnable clientHandler = new Runnable() {
public void run() {
try {
String message = MessageUtils.getMessage(socket);
MessageUtils.sendMessage(socket, "Processed: " + message);
closeIgnoringException(socket);
} catch (Exception e) {
e.printStackTrace();
}
}
};
Thread clientConnection = new Thread(clientHandler);
clientConnection.start();
}

Zamy, e zmiana ta spowodowaa, i test zostanie wykonany1; kod jest kompletny, zgadza si?

Uwagi na temat serwera


Zaktualizowany serwer wykonuje test w nieco ponad sekund. Niestety, rozwizanie to jest troch
naiwne i wprowadza kilka nowych problemw.
Ile wtkw moe utworzy nasz serwer? W kodzie nie ma ogranicze, wic moemy bez trudu
doj do ograniczenia nakadanego przez maszyn wirtualn rodowiska Java (JVM). Dla wielu
prostych systemw moe to wystarczy. Co jednak, jeeli system jest przeznaczony do obsugi
wielu uytkownikw w sieci publicznej? Jeeli zbyt wielu uytkownikw podczy si w tym samym
czasie, system moe si zatrzyma.
Jednak pozostawmy na moment problem dziaania. Przedstawione rozwizanie ma problemy
z czystoci i struktur. Ile odpowiedzialnoci ma kod serwera? Oto one:

zarzdzanie podczaniem do gniazd,

obsuga klientw,

zasady wtkw,

zasady wyczenia serwera.

Mona to sprawdzi samemu, testujc obie wersje kodu. Warto zapozna si z kodem niekorzystajcym z wtkw,
z punktu Klient-serwer bez wtkw, a take z kodem korzystajcym z wtkw, z punktu Klient-serwer z wtkami.

WSPBIENO II

329

Niestety, wszystkie te odpowiedzialnoci s umieszczone w funkcji process. Dodatkowo kod przekracza wiele rnych poziomw abstrakcji. Dlatego funkcja przetwarzania wymaga podzielenia,
pomimo e jest tak maa.
Istnieje kilka potencjalnych powodw zmiany serwera, wic amie to zasad pojedynczej odpowiedzialnoci. Aby zachowa czysto systemw wspbienych, zarzdzanie wtkami powinno by
umieszczone w niewielu odpowiednio kontrolowanych miejscach. Co wicej, kady kod zarzdzajcy wtkami powinien nie robi nic poza zarzdzaniem wtkami. Dlaczego? Wystarczy powiedzie, e ledzenie problemw ze wspbienoci jest wystarczajco trudne bez koniecznoci jednoczesnego rozwizywania problemw niezwizanych z ni.
Jeeli utworzymy osobn klas dla kadej wymienionej wczeniej odpowiedzialnoci, w tym dla
odpowiedzialnoci zarzdzania wtkami, to gdy zmienimy strategi zarzdzania wtkami, bdzie
ona miaa wpyw na mniejsz cz kodu i nie bdzie przeszkadza w innych odpowiedzialnociach.
Dodatkowo pozwala to atwiej testowa wszystkie inne odpowiedzialnoci, bez obawy o wielowtkowo. Poniej przedstawiona jest zmieniona wersja kodu, ktra dziaa wanie w taki sposb:
public void run() {
while (keepProcessing) {
try {
ClientConnection clientConnection = connectionManager.awaitClient();
ClientRequestProcessor requestProcessor
= new ClientRequestProcessor(clientConnection);
clientScheduler.schedule(requestProcessor);
} catch (Exception e) {
e.printStackTrace();
}
}
connectionManager.shutdown();
}

Teraz wszystkie elementy zwizane z wtkami znajduj si w jednym miejscu, clientScheduler.


Jeeli wystpi problemy ze wspbienoci, mamy jedno miejsce do sprawdzenia:
public interface ClientScheduler {
void schedule(ClientRequestProcessor requestProcessor);
}

Biece zasady s atwe do zaimplementowania:


public class ThreadPerRequestScheduler implements ClientScheduler {
public void schedule(final ClientRequestProcessor requestProcessor) {
Runnable runnable = new Runnable() {
public void run() {
requestProcessor.process();
}
};
Thread thread = new Thread(runnable);
thread.start();
}
}

Wyizolowanie wszystkich elementw zarzdzania wtkami do jednego miejsca pozwala na znacznie


atwiejsze zmienianie sposobu sterowania wtkami. Na przykad przejcie na bibliotek Java 5 Executor
wymaga napisania nowej klasy i jej podczenia (listing A1).

330

DODATEK A

L I S T I N G A 1 . ExecutorClientScheduler.java
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class ExecutorClientScheduler implements ClientScheduler {
Executor executor;
public ExecutorClientScheduler(int availableThreads) {
executor = Executors.newFixedThreadPool(availableThreads);
}
public void schedule(final ClientRequestProcessor requestProcessor) {
Runnable runnable = new Runnable() {
public void run() {
requestProcessor.process();
}
};
executor.execute(runnable);
}
}

Zakoczenie
Wprowadzanie wspbienoci w tym okrelonym przykadzie demonstruje sposb poprawiania
przepustowoci systemu oraz jeden sposb kontrolowania przepustowoci za pomoc testw.
Umieszczenie caego kodu wspbienoci w maej liczbie klas jest przykadem zastosowania zasady pojedynczej odpowiedzialnoci. W przypadku programowania wspbienego staje si to szczeglnie istotne z powodu jego zoonoci.

Moliwe cieki wykonania


Przyjrzyjmy si jednowierszowej metodzie incrementValue napisanej w jzyku Java, niezawierajcej ptli ani instrukcji warunkowych.
public class IdGenerator {
int lastIdUsed;
public int incrementValue() {
return ++lastIdUsed;
}
}

Zignorujmy przepenienie liczby cakowitej i zamy, e tylko jeden wtek ma dostp do jednego
obiektu IdGenerator. W takim przypadku mamy jedn ciek wykonania i jeden gwarantowany
wynik:

Zwracana warto jest rwna wartoci lastIdUsed, ktra jest o jeden wiksza ni przed wywoaniem tej metody.

Co si stanie, jeeli uyjemy dwch wtkw i pozostawimy metod niezmienion? Jakie s moliwe
wyniki, jeeli kady wtek jeden raz wywoa incrementValue? Ile mamy moliwych cieek wykonania? Na pocztek wyniki (zakadamy, e lastIdUsed zaczyna od wartoci 93):

WSPBIENO II

331

Wtek 1. otrzymuje warto 94, wtek 2. otrzymuje warto 95, lastUsedId jest rwne 95.

Wtek 1. otrzymuje warto 95, wtek 2. otrzymuje warto 94, lastUsedId jest rwne 95.

Wtek 1. otrzymuje warto 94, wtek 2. otrzymuje warto 94, lastUsedId jest rwne 94.

Ostatni wynik, cho zaskakujcy, jest moliwy. Aby sprawdzi, w jaki sposb s moliwe te rne
wyniki, musimy pozna liczb moliwych cieek wykonania i sposb ich realizacji przez maszyn
wirtualn Java.

Liczba cieek
Aby obliczy liczb moliwych cieek wykonania, zaczniemy od wygenerowanego kodu bajtowego.
Jeden wiersz kodu Java (return ++lastIdUsed;) staje si omioma instrukcjami kodu bajtowego.
Moliwe jest, e dwa wtki przeplataj wykonanie tych omiu instrukcji, w sposb podobny do
tasowania kart przez krupiera2. Nawet jeeli w kadej rce bdzie mia osiem kart, to wynikowa
liczba przetasowa jest znaczna.
Dla tego prostego przypadku sekwencji N instrukcji, bez ptli i warunkw oraz T wtkw, cakowita
liczba moliwych cieek wykonania jest rwna:

( NT )!
N !T
Wyliczanie moliwych uporzdkowa
Metoda pochodzi z e-maila wujka Boba do Bretta:
Przy N krokach i T wtkach mamy razem TN krokw. Przed kadym krokiem wykonywane jest przeczenie kontekstu, ktre pozwala na wybranie spord T wtkw.
Kada cieka moe by reprezentowana jako cig cyfr okrelajcych przeczenia
kontekstu. Jeeli mamy kroki A i B oraz wtki 1. i 2., szecioma moliwymi
ciekami bd 1122, 1212, 1221, 2112, 2121 oraz 2211. Zapisujc to w kontekcie
krokw, uzyskujemy: A1B1A2B2, A1A2B1B2, A1A2B2B1, A2A1B1B2, A2A1B2B1
oraz A2B2A1B1. Dla trzech wtkw mamy nastpujce sekwencje: 112233,
112323, 113223, 113232, 112233, 121233, 121323, 121332, 123132, 123123, .
Cigi te maj jedn wan cech zawsze musi znajdowa si N wystpie kadego T.
Dlatego cig 111111 jest nieprawidowy, poniewa ma sze wystpie 1 i zero
wystpie 2 i 3.
Oczekujemy permutacji N jedynek, N dwjek, oraz N wystpie T. Jest to naprawd
permutacja N T elementw po N T jednoczenie, czyli (N T)!, ale z usunitymi
powtrzeniami. Musimy tylko policzy powtrzenia i odj je od (N T )!

Jest to niewielkie uproszczenie. Jednak na potrzeby tej dyskusji moemy uy takiego uproszczonego modelu.

332

DODATEK A

Majc dwa kroki i dwa wtki, ile znajdziemy powtrze? Kady czterocyfrowy cig
ma dwie 1 i dwie 2. Kada z tych par moe by zamieniona bez zmiany sensu cigu.
Mona zamieni jedynki lub dwjki. Mamy wic cztery rwnorzdne postacie dla
kadego cigu, co oznacza, e mamy tu trzy powtrzenia. Trzy z czterech opcji to
powtrzenia albo, inaczej mwic, jedna z czterech permutacji nie jest powtrzeniem.
4!0,25 = 6. Wnioskowanie to wydaje si prawidowe.
Ile mamy powtrze? W przypadku, gdy N = 2 i T = 2, mog zamieni jedynki,
dwjki lub obie. W przypadku, gdy N = 2, a T = 3, moemy zamieni jedynki,
dwjki, trjki, jedynki i dwjki, jedynki i trjki lub dwjki i trjki. Zamiana jest po prostu permutacj N. Zamy, e mamy P permutacji N. Liczba rnych sposobw uporzdkowania tych permutacji wynosi PT.
Dlatego liczba moliwych postaci rwnorzdnych wynosi N!T. Wic liczba cieek
wynosi (TN)!/(N!T). W naszym przypadku, T = 2 i N = 2, otrzymamy 6 (24/4).
Dla N = 2 oraz T = 3 otrzymamy 720/8 = 90.
Dla N = 3 oraz T = 3 otrzymamy 9!/3^3 = 1680.
Dla naszego prostego kodu Java zawierajcego jeden wiersz, ktry daje osiem wierszy kodu bajtowego, oraz dwch wtkw cakowita liczba moliwych cieek wykonania wynosi 12 870. Jeeli
zmienna lastIdUsed byaby typu long, to kady odczyt i zapis obejmowaby dwie operacje
zamiast jednej, wic liczba moliwych uporzdkowa wyniosaby 2 704 156.
Co si stanie, jeeli do tej metody wprowadzimy jedn zmian?
public synchronized void incrementValue() {
++lastIdUsed;
}

Liczba moliwych cieek wykonania byaby rwna 2 dla dwch wtkw i N! w oglnym
przypadku.

Kopiemy gbiej
Co to za zaskakujcy wynik, jeeli dwa wtki mog wywoa jednoczenie t sam metod (przed
dodaniem synchronized) i otrzymamy ten sam wynik numeryczny? Jak to moliwe? Na pocztek
podstawy.
Co jest operacj atomow? Moemy j zdefiniowa jako operacj, ktrej nie mona przerwa. Na
przykad w poniszym kodzie wiersz 5., gdzie jest przypisywana warto 0 do lastId, jest atomowy,
poniewa zgodnie z modelem pamici Java przypisanie wartoci 32-bitowej jest nieprzerywalne.
01: public class Example {
02:
int lastId;
03:

WSPBIENO II

333

04:
05:
06:
07:
08:
09:
10:
11:}

public void resetId() {


value = 0;
}
public int getNextId() {
++value;
}

Co si stanie, jeeli zmienimy typ lastId na long? Czy wiersz 5. jest nadal atomowy? Nie, zgodnie
ze specyfikacj JVM. Moe by on atomowy na okrelonym procesorze, ale zgodnie ze specyfikacj
JVM przypisanie dowolnej wartoci 64-bitowej wymaga dwch przypisa 32-bitowych. Oznacza
to, e pomidzy pierwszym przypisaniem 32-bitowym a drugim przypisaniem 32-bitowym inny
wtek moe si wlizgn i zmieni jedn z wartoci.
Co mona powiedzie o operatorze inkrementacji z wiersza 9.? Operator ten moe by przerwany,
wic nie jest atomowy. Aby to zrozumie, przejrzyjmy kod bajtowy obu tych metod.
Zanim bdziemy mogli kontynuowa, przytoczymy wane definicje:

Ramka kade wywoanie metody wymaga ramki. Ramka zawiera adres powrotu, wszystkie
parametry przekazane do metody oraz zmienne lokalne zdefiniowane w metodzie. Jest to standardowa technika uywana do definiowania stosu wywoa, ktry jest wykorzystywany przez
nowoczesne jzyki do podstawowego wywoania metod i funkcji oraz umoliwienia wywoywania rekurencyjnego.

Zmienna lokalna dowolna zmienna zdefiniowana w zakresie metody. Wszystkie niestatyczne


metody maj co najmniej jedn zmienn, this, ktra reprezentuje biecy obiekt obiekt, ktry
otrzyma ostatni komunikat (w biecym wtku), jaki spowodowa wywoanie metody.

Stos operandw wiele z instrukcji w maszynie wirtualnej Java posiada parametry. Parametry
te s odkadane na stos operandw. Stos jest struktur danych typu ostatni wszed, pierwszy
wyszed (LIFO).

Poniej zamieszczony jest kod bajtowy wygenerowany dla resetId().

334

Mnemonika

Opis

Kocowy stos operandw

ALOAD 0

Zaadowanie zerowej zmiennej na stos operandw. Co jest zerow


zmienn? Jest to this, biecy obiekt. Gdy zostaje wywoana
metoda, odbiorca komunikatu, obiekt Example, jest umieszczany
w tablicy zmiennych lokalnych ramki utworzonej dla wywoania
metody. Jest to zawsze pierwsza zmienna umieszczana dla kadej
metody instancyjnej.

this

ICONST_0

Umieszczenie wartoci staej 0 na stosie operandw.

this, 0

PUTFIELD
lastId

Zapamitanie wartoci ze szczytu stosu (czyli 0) w polu obiektu,


do ktrego odwouje si referencja obiektu ze szczytu stosu,
czyli this.

<pusty>

DODATEK A

Gwarantowane jest, e te trzy instrukcje s atomowe, poniewa pomimo moliwoci przerwania


wtku po kadej z nich, informacja dla instrukcji PUTFIELD (staa 0 ze szczytu stosu oraz odwoanie do this poniej szczytu, jak rwnie warto pola) nie moe by zmieniona przez aden wtek.
Dlatego, gdy nastpi przypisanie, gwarantowane jest, e w polu zostanie zapisana warto 0. Operacja jest atomowa. Wszystkie operandy dziaaj na informacjach lokalnych dla metody, wic nie
ma interferencji midzy wieloma wtkami.
Jeeli te trzy instrukcje s wykonywane przez dziesi wtkw, moe wystpi 4,38679733629e+24 uporzdkowa. Jednak jest tylko jeden moliwy wynik, wic rne uporzdkowania s bez znaczenia.
W tym przypadku ten sam wynik jest gwarantowany rwnie dla wartoci long. Dlaczego? Wszystkie
dziesi wtkw przypisuje sta warto. Nawet jeeli przeplataj si, ostateczny wynik bdzie taki sam.
W przypadku operacji ++ w metodzie getNextId zaczynaj si problemy. Zamy, e na pocztku
dziaania tej metody lastId zawiera warto 42. Poniej pokazany jest kod bajtowy dla tej nowej
metody.
Mnemonika

Opis

Kocowy stos operandw

ALOAD 0

aduje this na stos operandw.

this

DUP

Kopiuje szczyt stosu. Na stosie operandw mamy teraz dwie


kopie this.

this, this

GETFIELD
lastId

Pobiera warto pola lastId z obiektu wskazywanego przez


referencj na szczycie stosu (this) i zapisuje warto z powrotem
na stosie.

this, 42

ICONST_1

Umieszcza sta cakowit 1 na stosie.

this, 42, 1

IADD

Dodaje dwie wartoci cakowite na stosie operandw i zapisuje


wynik z powrotem na stosie operandw.

this, 43

DUP_X1

Powiela warto 43 i umieszcza j przed this.

43, this, 43

PUTFIELD
value

Zapisuje warto ze szczytu stosu operandw, 43, do pola biecego


obiektu, reprezentowanego przez drug od szczytu warto stosu
operandw, czyli this.

43

IRETURN

Zwraca warto ze szczytu stosu (tylko t).

<pusty>

Wyobramy sobie przypadek, gdy pierwszy wtek koczy pierwsze trzy instrukcje, a do GETFIELD
wcznie, i jest przerywany. Drugi wtek przejmuje sterowanie i wykonuje ca metod, zwikszajc lastId o jeden; otrzymamy warto 43. Nastpnie pierwszy wtek wznawia prac tam, gdzie
zosta przerwany, a na jego stosie operandw nadal znajduje si 42, poniewa w czasie wykonywania GETFIELD taka bya wanie warto lastId. Znw dodawana jest warto jeden i ponownie
zapisywany jest wynik 43. Warto 43 jest zwracana rwnie z pierwszego wtku. W wyniku tego
jedna inkrementacja jest utracona, poniewa pierwszy wtek zosta przerwany przez drugi.
Oznaczenie metody getNextId() jako synchronized rozwizuje problem.

WSPBIENO II

335

Zakoczenie
Szczegowa znajomo kodu bajtowego nie jest niezbdna do zrozumienia tego, jak wtki mog
si na siebie nakada. Jeeli zrozumiemy przedstawiony tu przykad, powinnimy wiedzie o moliwoci nakadania si dwch wtkw, co jest wystarczajc wiedz.
Ten prosty przykad mia za zadanie pokaza potrzeb zrozumienia modelu pamici na tyle, aby
wiedzie, co jest bezpieczne, a co nie. Czsto spotykan pomyk jest zaoenie, e operator ++
(zarwno pre-, jak i postinkrementujcy) jest atomowy. To nieprawda. Oznacza to, e powinnimy
wiedzie:

gdzie znajduj si wspuytkowane obiekty i wartoci,

ktry kod moe spowodowa problemy z rwnolegym odczytem i zapisem,

jak chroni si przed wystpieniem problemw ze wspbienoci.

Poznaj uywan bibliotek


Biblioteka Executor
Jak jest pokazane na listingu A1, ExecutorClientScheduler.java, biblioteka Executor, wprowadzona w Java 5, pozwala na zoone operacje wykorzystujce pule wtkw. Klasa ta znajduje si w pakiecie java.util.concurrent.
Jeeli tworzymy wtki i nie korzystamy z pul wtkw lub korzystamy z wasnych, powinnimy
rozway wykorzystanie klasy Executor. Pozwala to na tworzenie czystszego, atwiejszego do analizy
i mniejszego kodu.
Biblioteka Executor zarzdza pul wtkw, zmienia j automatycznie i w razie potrzeby odtwarza
wtki. Obsuguje ona obiekty future, czsto stosowan konstrukcj programowania wspbienego.
Biblioteka Executor operuje na klasach implementujcych Runnable i dodatkowo na klasach implementujcych interfejs Callable. Interfejs Callable jest podobny do Runnable, ale pozwala na
zwrcenie wyniku, co jest czsto potrzebne w rozwizaniach wielowtkowych.
Obiekt future jest wygodny, jeeli w naszym kodzie istnieje potrzeba wykonania wielu niezalenych
operacji i oczekiwania na ich zakoczenie.
public String processRequest(String message) throws Exception {
Callable<String> makeExternalCall = new Callable<String>() {
public String call() throws Exception {
String result = "";

// Wykonanie zewntrznego dania.


return result;
}
};
Future<String> result = executorService.submit(makeExternalCall);
String partialResult = doSomeLocalProcessing();
return result.get() + partialResult;
}

336

DODATEK A

W tym przykadzie metoda uruchamia wykonywanie obiektu makeExternalCall. Nastpnie metoda


kontynuuje przetwarzanie. W ostatnim wierszu wywoanie result.get() powoduje zablokowanie
dziaania do zakoczenia future.

Rozwizania nieblokujce
Maszyna wirtualna Java 5 wykorzystuje projekty nowoczesnych procesorw, ktre obsuguj niezawodne aktualizacje nieblokujce. Jako przykad wemy klas korzystajc z synchronizacji (a wic
i blokowania) do zapewnienia bezpiecznej dla wtkw aktualizacji wartoci:
public class ObjectWithValue {
private int value;
public void synchronized incrementValue() { ++value; }
public int getValue() { return value; }
}

Java 5 posiada zestaw nowych klas pozwalajcych na obsuenie takich sytuacji przykadami
mog by AtomicBoolean, AtomicInteger czy AtomicReference, ale dostpnych jest jeszcze kilka
innych. Moemy zmieni powyszy kod tak, aby korzysta z rozwizania nieblokujcego:
public class ObjectWithValue {
private AtomicInteger value = new AtomicInteger(0);
public void incrementValue() {
value.incrementAndGet();
}
public int getValue() {
return value.get();
}
}

Mimo e wykorzystany jest obiekt zamiast wartoci prostej i wysyany jest komunikat increment
AndGet() zamiast ++, wydajno dziaania tej klasy niemal zawsze przewysza wczeniejsz
wersj. W niektrych przypadkach kod ten jest tylko nieznacznie szybszy, ale przypadki, gdy
rozwizanie to jest wolniejsze, praktycznie nie istniej.
Jak to moliwe? Nowoczesne procesory udostpniaj operacj zwykle nazywan Compare and
Swap (CAS). Operacja ta jest analogiczna do blokowania optymistycznego w bazie danych, natomiast
wersja synchronizowana jest analogiczna do blokowania pesymistycznego.
Sowo kluczowe synchronized zawsze nakada blokad, nawet jeeli drugi wtek nie prbuje
zaktualizowa tej samej blokady. Mimo e wydajno koniecznych blokad z kad wersj si poprawia, nadal s one kosztowne.
Wersja nieblokujca wychodzi z zaoenia, e wiele wtkw zwykle nie modyfikuje tej samej wartoci na tyle czsto, aby powstay problemy. Zamiast tego efektywnie wykrywa moment wystpienia
takiej sytuacji i powtarza operacj do prawidowego zakoczenia aktualizacji. Takie wykrywanie
jest niemal zawsze mniej kosztowne ni nakadanie blokady, nawet w przypadku redniego i wysokiego obcienia.

WSPBIENO II

337

W jaki sposb realizuje to maszyna wirtualna? Operacja CAS jest atomowa. Logicznie rzecz biorc,
operacja CAS wyglda mniej wicej tak:
int variableBeingSet;
void simulateNonBlockingSet(int newValue) {
int currentValue;
do {
currentValue = variableBeingSet
} while(currentValue != compareAndSwap(currentValue, newValue));
}
int synchronized compareAndSwap(int currentValue, int newValue) {
if(variableBeingSet == currentValue) {
variableBeingSet = newValue;
return currentValue;
}
return variableBeingSet;
}

Gdy metoda prbuje zaktualizowa wspuytkowan zmienn, operacja CAS sprawdza, czy
zmienna ta nadal posiada znan warto. Jeeli tak, zmienna jest modyfikowana. Jeeli nie, zmienna nie jest ustawiana, poniewa inny wtek wszed nam w drog. Metoda wykonujca operacj
(przy uyciu CAS) zauwaa, e zmiana nie zostaa wykonana, i ponawia prb.

Bezpieczne klasy nieobsugujce wtkw


Niektre klasy z natury nie s bezpieczne dla wtkw. Kilka przykadw takich klas przedstawiono
poniej:
SimpleDateFormat,

poczenia z bazami danych,

kontenery w java.util,

serwlety.

Niektre klasy kolekcji posiadaj pojedyncze metody bezpieczne dla wtkw. Jednak kada operacja
wymagajca wywoania wicej ni jednej metody nie jest bezpieczna. Na przykad, gdy nie chcemy
zamienia elementu z HashTable, jeeli ju si tam znajduje, moemy napisa nastpujcy kod:
if(!hashTable.containsKey(someKey)) {
hashTable.put(someKey, new SomeValue());
}

Kada z tych metod jest bezpieczna dla wtkw. Jednak inny wtek moe doda warto pomidzy
wywoaniami containsKey oraz put. Istnieje kilka moliwoci rozwizania tego problemu:

Wczeniejsze zablokowanie HashTable i upewnienie si, e wszyscy uytkownicy zrobili to samo


blokowanie z poziomu klienta:
synchronized(map) {
if(!map.conainsKey(key))
map.put(key,value);
}

338

DODATEK A

Opakowanie HashTable we wasny obiekt i uycie innego API blokowanie z poziomu serwera
wykorzystujce wzorzec adapter:
public class WrappedHashtable<K, V> {
private Map<K, V> map = new Hashtable<K, V>();
public synchronized void putIfAbsent(K key, V value) {
if (map.containsKey(key))
map.put(key, value);
}
}

Zastosowanie kolekcji bezpiecznych dla wtkw:


ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<Integer,String>();
map.putIfAbsent(key, value);

Kolekcje z pakietu java.util.concurrent posiadaj metody, takie jak putIfAbsent(), pozwalajce obsuy takie operacje.

Zalenoci midzy metodami


mog uszkodzi kod wspbieny
Poniej przedstawiony jest prosty przykad wprowadzenia zalenoci midzy moduami:
public class IntegerIterator implements Iterator<Integer>
private Integer nextValue = 0;
public synchronized boolean hasNext() {
return nextValue < 100000;
}
public synchronized Integer next() {
if (nextValue == 100000)
throw new IteratorPastEndException();
return nextValue++;
}
public synchronized Integer getNextValue() {
return nextValue;
}
}

Poniej mamy kod wykorzystujcy t klas IntegerIterator:


IntegerIterator iterator = new IntegerIterator();
while(iterator.hasNext()) {
int nextValue = iterator.next();

// Wykonanie operacji na nextValue.


}

Jeeli kod ten jest wykonywany przez jeden wtek, nie bdzie adnych problemw. Co si stanie,
jeeli dwa wtki bd wspuytkoway jeden obiekt IntegerIterator przy takim zaoeniu, e
kady wtek przetwarza pobrane wartoci, ale kady z elementw na licie bdzie przetwarzany
tylko raz? W wikszoci przypadkw nie stanie si nic zego; wtki bd w zgodzie wspuytkoway
list, przetwarzay elementy uzyskane za pomoc iteratora i koczyy prac, gdy iterator nie bdzie

WSPBIENO II

339

mia kolejnych elementw. Jednak istnieje niewielka szansa, e na kocu iteracji dwa wtki bd
interferoway ze sob i mog spowodowa, e jeden wtek wyjdzie poza iterator i zgosi wyjtek.
Problem ten jest nastpujcy: wtek 1. wywouje hasNext(), ktra zwraca true. Wtek 1. zostaje
wywaszczony, a wtek 2. wykonuje to samo wywoanie, nadal otrzymujc true. Wtek 2. wywouje metod next(), ktra zwraca oczekiwan warto, ale efektem ubocznym jej dziaania jest
zmiana wartoci zwracanej przez hasNext() na false. Wtek 1. wznawia prac i zakada, e
hasNext() nadal ma warto true, wic wywouje next(). Mimo e poszczeglne metody s synchronizowane, klient uywa dwch metod.
Jest to rzeczywisty problem i takie przykady czsto zdarzaj si we wspbienym kodzie. W tej
sytuacji problem jest szczeglnie subtelny, poniewa jedynym miejscem, w ktrym moe zdarzy
si bd, jest ostatni krok iteratora. Jeeli wtki tam wanie si spotkaj, to jeden z nich wychodzi
poza iterator. Tego rodzaju bdy zdarzaj si najczciej ju po produkcyjnym wdroeniu systemu
i s bardzo trudne do wyledzenia.
Mamy wtedy trzy moliwoci:

tolerowa awari,

rozwiza problem przez zmian klienta blokowanie na kliencie,

rozwiza problem przez zmian serwera, co dodatkowo wymaga zmian w kliencie blokowanie na serwerze.

Tolerowanie awarii
Czasami mona tak zorganizowa prac, aby takie awarie nie powodoway problemw. Na przykad klient moe przechwyci wyjtek i wyczyci obiekt. Oczywicie, jest to kiepskie rozwizanie.
Jest podobne do wyczyszczenia wyciekw pamici przez przeadowanie systemu o pnocy.

Blokowanie na kliencie
Aby IntegerIterator mg dziaa prawidowo dla wielu wtkw, mona zmieni tego klienta
(i wszystkie inne) w nastpujcy sposb:
IntegerIterator iterator = new IntegerIterator();
while (true) {
int nextValue;
synchronized (iterator) {
if (!iterator.hasNext())
break;
nextValue = iterator.next();
}
doSometingWith(nextValue);
}

Kady klient wprowadza blokad przez uycie sowa kluczowego synchronized. Takie powtrzenie narusza zasad DRY, ale moe by niezbdne, jeeli kod korzysta z zewntrznych narzdzi, ktre
nie s bezpieczne dla wtkw.
340

DODATEK A

Strategia ta jest ryzykowna, poniewa wszyscy programici korzystajcy z serwera musz pamita
o naoeniu blokady przed jego uyciem i odblokowaniu po zakoczeniu. Wiele (naprawd wiele!)
lat temu pracowaem nad systemem, w ktrym zostaa zastosowana strategia blokowania zasobw na
kliencie. Zasb by uywany w setkach rnych miejsc w kodzie. Jeden biedny programista zapomnia o zablokowaniu zasobu w jednym z tych miejsc.
System by wieloterminalowym systemem ze wspuytkowaniem czasu, obsugujcym ksigowo
dla zwizku kierowcw ciarwek Local 705. Komputer znajdowa si w pokoju z podniesion
podog i klimatyzacj, 80 kilometrw na pnoc od siedziby Local 705. W siedzibie zainstalowanych byo kilkanacie stanowisk wprowadzania danych, na ktrych urzdnicy wprowadzali dokumenty do systemu. Terminale byy podczone do komputera z uyciem dedykowanej linii telefonicznej i pdupleksowych modemw o prdkoci 600 b/s (to byo bardzo, bardzo dawno).
Mniej wicej raz dziennie jeden z terminali zawiesza si. Nie mona byo okreli adnych prawidowoci blokada nie wykazywaa adnych preferencji co do terminala ani okrelonego czasu. Wygldao to tak, jakby kto rzuca kostkami, wybierajc czas i terminal do zablokowania.
Czasami blokowao si kilka terminali. W niektrych dniach nie byo adnych zawiesze.
Na pocztku jedynym rozwizaniem byo przeadowanie. Jednak operacja ta bya trudna do skoordynowania. Musielimy zadzwoni do centrali i poczeka na zakoczenie pracy na wszystkich terminalach. Wtedy moglimy wyczy system i ponownie go uruchomi. Jeeli kto robi co wanego,
co trwao godzin lub dwie, zablokowany terminal musia pozosta bezuyteczny.
Po kilku tygodniach debugowania odkrylimy, e przyczyn by licznik bufora cyklicznego, ktry
traci synchronizacj ze wskanikiem. Bufor ten kontrolowa wyjcie na terminal. Warto wskanika wskazywaa, e bufor by pusty, ale licznik wskazywa, e by peny. Poniewa by pusty, nie
byo nic do wywietlania, ale poniewa by jednoczenie peny, nic nie mona byo doda do bufora
wywietlania na ekranie.
Wiedzielimy wic, dlaczego terminal si blokowa, ale nie wiedzielimy, dlaczego bufor cykliczny
rozsynchronizowywa si. Dodalimy wic modyfikacj pozwalajc obej problem. Moliwe byo
odczytywanie stanu przecznikw na panelu kontrolnym komputera (to byo bardzo, bardzo,
bardzo dawno). Napisalimy ma funkcj przechwytujc, ktra w momencie przeczenia jednego
z przecznikw wyszukiwaa bufory cykliczne, ktre byy jednoczenie pene i puste. Jeeli go znalaza, ustawiaa bufor na pusty. Voila! Zablokowany terminal ponownie zaczyna wywietla dane.
Nie musielimy wic restartowa systemu po zablokowaniu terminala. Wystarczy telefon od klienta,
a kto podchodzi do komputera i naciska przycisk.
Czasami jednak Local 705 pracowa w weekendy, a my nie. Dodalimy wic funkcj do harmonogramu, ktra sprawdzaa wszystkie bufory cykliczne co minut i jeeli ktry by jednoczenie peny i pusty, resetowaa go. Dziki temu terminale odblokowyway si, zanim kto z Local podszed
do telefonu.
Po kilku kolejnych tygodniach przegldania strona po stronie monolitycznego kodu asemblera
znalelimy przyczyn. Wykonalimy obliczenia i wyliczylimy, e czstotliwo blokowania bya
spjna z jednym niechronionym uyciem bufora cyklicznego. Musielimy wic tylko znale jedno

WSPBIENO II

341

nieprawidowe uycie. Niestety, byo to bardzo dawno temu i nie mielimy narzdzi wyszukujcych
ani wzajemnych odwoa, ani te innych automatycznych podpowiedzi. Musielimy po prostu lcze nad listingami.
Tej chodnej zimy 1971 roku w Chicago nauczyem si czego wanego. Blokowanie na kliencie jest
do niczego.

Blokowanie na serwerze
Powtrzenia mog by usunite przez wprowadzenie do klasy IntegerIterator nastpujcych zmian:
public class IntegerIteratorServerLocked {
private Integer nextValue = 0;
public synchronized Integer getNextOrNull() {
if (nextValue < 100000)
return nextValue++;
else
return null;
}
}

Musi zmieni si rwnie kod klienta:


while (true) {
Integer nextValue = iterator.getNextOrNull();
if (next == null)
break;

// Wykonanie operacji na nextValue.


}

W tym przypadku tak zmieniamy API naszej klasy, aby obsugiwao wielowtkowo3. Klient musi
skontrolowa warto null, zamiast korzysta z hasNext().
Powinnimy korzysta z blokowania na serwerze z nastpujcych powodw:

Zmniejsza powtarzanie kodu blokowanie na kliencie wymusza prawidowe blokowanie na


kadym kliencie. Przeniesienie kodu blokujcego na serwer pozwala, aby klienty korzystay
z obiektw bez koniecznoci pisania dodatkowego kodu blokujcego.

Pozwala osign lepsz wydajno w przypadku instalacji jednowtkowej pozwala na zamian serwera bezpiecznego dla wtkw na serwer bez obsugi wtkw, dziki czemu mona
pozby si wszystkich narzutw.

Zmniejsza moliwo wystpienia bdu niemoliwe jest spowodowanie awarii przez jednego programist, ktry zapomnia o zastosowaniu prawidowej blokady.

Wymusza jedn zasad zasady s w jednym miejscu, na serwerze, a nie w wielu miejscach,
na kadym kliencie.

W rzeczywistoci interfejs Iterator jest z natury niebezpieczny dla wtkw. Nigdy nie by projektowany do pracy na
wielu wtkach, wic nie powinno to by niespodziank.

342

DODATEK A

Zmniejsza zasig wspuytkowanych zmiennych klient nie jest ich wiadom, jak rwnie
sposobu, w jaki s blokowane. Wszystko jest ukryte w serwerze. Jeeli wystpi bd, liczba miejsc
do sprawdzenia jest mniejsza.

Co moemy zrobi, jeeli nie mamy wpywu na kod serwera?

Skorzysta z wzorca adaptera do zmiany API i dodania blokowania.


public class ThreadSafeIntegerIterator {
private IntegerIterator iterator = new IntegerIterator();
public synchronized Integer getNextOrNull() {
if(iterator.hasNext())
return iterator.next();
return null;
}
}

Jeszcze lepszym rozwizaniem jest uycie kolekcji z rozszerzonym interfejsem, bezpiecznym


dla wtkw.

Zwikszanie przepustowoci
Zamy, e musimy skorzysta z sieci i odczyta zawarto zbioru stron z listy adresw URL. Gdy
strona jest odczytywana, analizujemy j i zbieramy pewne statystyki. Po przeczytaniu wszystkich
stron wywietlamy raport podsumowujcy.
Ponisza klasa zwraca zawarto strony o podanym adresie URL.
public class PageReader {

//...
public String getPageFor(String url) {
HttpMethod method = new GetMethod(url);
try {
httpClient.executeMethod(method);
String response = method.getResponseBodyAsString();
return response;
} catch (Exception e) {
handle(e);
} finally {
method.releaseConnection();
}
}
}

Nastpna klasa jest iteratorem, ktry udostpnia zawarto stron na podstawie iteratora adresw URL:
public class PageIterator {
private PageReader reader;
private URLIterator urls;
public PageIterator(PageReader reader, URLIterator urls) {
this.urls = urls;
this.reader = reader;
}
public synchronized String getNextPageOrNull() {

WSPBIENO II

343

if (urls.hasNext())
getPageFor(urls.next());
else
return null;
}
public String getPageFor(String url) {
return reader.getPageFor(url);
}
}

Obiekt PageIterator moe by wspuytkowany przez rone wtki, z ktrych kady korzysta
z wasnego obiektu PageReader do odczytywania i analizowania stron uzyskanych z iteratora.
Naley zauway, e bloki synchronized s bardzo mae. Zawieraj wycznie sekcje krytyczne
gboko wewntrz PageIterator. Zawsze lepiej synchronizowa moliwe mae fragmenty, ni
obejmowa synchronizacj wszystko, co jest moliwe.

Obliczenie przepustowoci jednowtkowej


Wykonajmy teraz proste obliczenia. Na nasze potrzeby zamy nastpujce wielkoci:

Czas wejcia-wyjcia potrzebny do pobrania strony (rednio): 1 sekunda.

Czas przetwarzania caej strony (rednio): 0,5 sekundy.

Operacje wejcia-wyjcia wymagaj 0 procent czasu procesora, natomiast przetwarzanie 100 procent.

Dla N stron przetwarzanych przez jeden wtek cakowity czas wykonania wynosi 1,5 sekundy N.
Na rysunku A1 przedstawione jest przetwarzanie 13 stron, czyli okoo 19,5 sekundy.

R Y S U N E K A 1 . Jeden wtek

Obliczenie przepustowoci wielowtkowej


Jeeli moliwe jest pobieranie stron w dowolnej kolejnoci i ich niezalene przetwarzanie, to moliwe jest uycie wielu wtkw do zwikszenia przepustowoci. Co si stanie, gdy uyjemy trzech
wtkw? Ile stron przetworzymy w tym samym czasie?
Jak jest pokazane na rysunku A2, rozwizanie wielowtkowe pozwala na to, aby przetwarzanie stron
zwizane z procesorem przeplatao si z odczytem stron zwizanym z operacj wejcia-wyjcia.
W idealnym przypadku oznacza to, e procesor bdzie w peni wykorzystany. Kade jednosekundowe
pobranie bdzie przeplatao si z analiz dwch stron. Dlatego moemy przetwarza dwie strony
na sekund, czyli trzy razy wicej ni przepustowo rozwizania jednowtkowego.

344

DODATEK A

R Y S U N E K A 2 . Trzy wtki wspbiene

Zakleszczenie
Wyobramy sobie aplikacj WWW z dwoma pulami zasobw o skoczonym rozmiarze:

Pula pocze do bazy danych dla pracy lokalnej przy przetwarzaniu danych.

Pula pocze MQ do repozytorium nadrzdnego.

Zamy, e w aplikacji mamy dwie operacje utworzenie i aktualizacj:

Utworzenie uzyskanie poczenia z gwnym repozytorium i baz danych. Wymiana danych


z usug gwnego repozytorium i zapisanie danych w lokalnej bazie danych.

Aktualizacja uzyskanie poczenia z baz danych, a nastpnie z gwnym repozytorium.


Odczytanie danych z lokalnej bazy danych i wysanie ich do gwnego repozytorium.

Co si stanie, jeeli liczba uytkownikw bdzie przekraczaa wielko pul? Zamy, e kada pula
ma wielko rwn 10.

Dziesiciu uytkownikw prbuje uy opcji utworzenia, wic wszystkie (dziesi) poczenia


do bazy danych s zajte, a kady wtek jest przerywany po uzyskaniu dostpu do bazy danych,
ale przed uzyskaniem poczenia z gwnym repozytorium.

Dziesiciu uytkownikw prbuje uy opcji aktualizacji, wic wszystkie poczenia z gwnym repozytorium s zajte, a kady wtek jest przerywany po uzyskaniu dostpu do gwnego repozytorium, ale przed uzyskaniem poczenia z baz danych.

Teraz wszystkie wtki tworzce musz poczeka na uzyskanie poczenia z repozytorium,


a jednoczenie dziesi wtkw aktualizujcych musi poczeka na uzyskanie poczenia
z baz danych.

Zakleszczenie. System nigdy nie ruszy dalej.


WSPBIENO II

345

Moe si to wydawa mao prawdopodobn sytuacj, ale kto chciaby mie system, ktry kadego tygodnia zupenie przestaje odpowiada? Kto chce debugowa system, majc objawy tak trudne do powtrzenia? Jeeli tego rodzaju problemy zdarzaj si po wdroeniu, to ich rozwizanie cignie si
tygodniami.
Typowym rozwizaniem jest dodanie instrukcji debugujcych, ktre pomagaj okreli przyczyny
problemu. Oczywicie instrukcje debugujce zmieniaj kod na tyle, e zakleszczenia pojawiaj si
w innej sytuacji i znw trzeba czeka miesice na ich wystpienie4.
Aby naprawd rozwiza problem zakleszczenia, musimy zrozumie jego przyczyn. Istniej cztery
warunki wymagane do wystpienia zakleszczenia:

wzajemne wykluczanie,

blokowanie i oczekiwanie,

brak wywaszczania,

cykliczne oczekiwanie.

Wzajemne wykluczanie
Wzajemne wykluczanie wystpuje, gdy wiele wtkw musi uy tych samych zasobw, a te zasoby:

nie mog by uywane przez wiele wtkw jednoczenie,

s ograniczone.

Czsto spotykanym przykadem takich zasobw s poczenia z baz danych, otwarte do zapisu
pliki, blokady rekordw lub semafory.

Blokowanie i oczekiwanie
Gdy wtek uzyska zasb, nie zwalnia go do momentu uzyskania pozostaych zasobw wymaganych
do zakoczenia zadania.

Brak wywaszczania
Jeden wtek nie moe zabra zasobw innemu wtkowi. Gdy wtek zablokuje zasb, drugi wtek
moe skorzysta z niego dopiero po jego zwolnieniu przez wtek blokujcy.

Cykliczne oczekiwanie
Jest to nazywane rwnie miertelnym objciem. Wyobramy sobie dwa wtki, T1 i T2, oraz dwa
zasoby, R1 i R2. T1 posiada R1, a T2 posiada R2. T1 wymaga rwnie R2, a T2 wymaga rwnie
R1. Daje to w wyniku sytuacj, ktrej schemat jest przedstawiony na rysunku A3.
4

Na przykad kto dodaje rejestrowanie diagnostyczne i problem znika. Kod debugujcy naprawia problem, wic jest
pozostawiany w systemie.

346

DODATEK A

RYSUNEK A3.

Wszystkie cztery warunki musz by spenione, aby doszo do zakleszczenia.

Zapobieganie wzajemnemu wykluczaniu


Jedn ze strategii unikania zakleszczenia jest zapobieganie warunkom wzajemnego wykluczania.
Mona to zrobi przez:

wykorzystywanie zasobw pozwalajcych na jednoczesne uywanie, na przykad Atomic


Integer;

zwikszenie liczby zasobw, aby bya rwna lub przekraczaa liczb rywalizujcych wtkw;

sprawdzanie, czy wszystkie zasoby s wolne przed zablokowaniem ktregokolwiek.

Niestety, wikszo zasobw jest ograniczona i nie pozwalaj one na jednoczesne wykorzystywanie.
Dodatkowo czsto drugi zasb jest okrelany na podstawie wynikw dziaania na pierwszym. Jednak
nie naley si zraa, pozostay jeszcze trzy warunki.

Zapobieganie blokowaniu i oczekiwaniu


Mona rwnie wyeliminowa zakleszczenia, jeeli nie bdziemy oczekiwa na zwolnienie blokady. Naley sprawdzi kady zasb przed jego zablokowaniem, zwolni wszystkie zasoby i zacz
od nowa, jeeli jeden z nich jest zajty.
Rozwizanie to wprowadza jednak kilka potencjalnych problemw:

Zagodzenie jeden wtek nie jest w stanie uzyska potrzebnych zasobw (na przykad jest to
unikatowa kombinacja zasobw, ktra rzadko jest dostpna).

Uwizienie kilka wtkw moe wpa w dynamiczne zakleszczenie, gdy wszystkie zajmuj
jeden zasb, a nastpnie zwalniaj go, i tak w nieskoczono. Jest to szczeglnie prawdopodobne dla uproszczonych algorytmw harmonogramowania CPU (w urzdzeniach wbudowanych lub prostych wasnorcznie napisanych algorytmach rwnowaenia wtkw).

Obie te sytuacje mog obnia przepustowo. Wynikiem pierwszej jest niskie uycie procesora,
natomiast drugiej bezuytecznie wysokie uycie procesora.

WSPBIENO II

347

Mimo e strategia ta wyglda na mao efektywn, jest lepsza ni nic. Posiada swoje zalety i moe
by niemal zawsze zastosowana, gdy wszystko inne zawiedzie.

Umoliwienie wywaszczania
Inn strategi zapobiegania zakleszczeniom jest umoliwienie wtkom zabierania zasobw innym
wtkom. Jest to zwykle realizowane przez prosty mechanizm da. Gdy wtek wykryje, e zasb
jest zajty, prosi waciciela o jego zwolnienie. Jeeli waciciel rwnie oczekuje na inny zasb,
zwalnia wszystkie i zaczyna od nowa.
Jest to podobne do poprzedniego podejcia, ale pozwala, aby wtek oczekiwa na zasb. Zmniejsza
to liczb restartw. Trzeba jednak pamita, e zarzdzanie tymi daniami moe by kopotliwe.

Zapobieganie oczekiwaniu cyklicznemu


Jest to najczciej stosowane podejcie do zapobiegania zakleszczeniom. W przypadku wikszoci
systemw nie wymaga niczego poza uzgodnieniem przez wszystkie strony prostej konwencji.
W wczeniej przedstawionym przykadzie z wtkiem 1., wymagajcym zasobw 1. i 2., oraz wtkiem 2., wymagajcym zasobw 2. i 1., zwyke wymuszenie na wtkach 1. i 2. blokowania zasobw
w tej samej kolejnoci spowoduje, e oczekiwanie cykliczne bdzie niemoliwe.
Co wicej, jeeli wszystkie wtki bd stosoway globalne uporzdkowanie zasobw i wszystkie
bd przydziela zasoby w tej kolejnoci, zakleszczenia bd niemoliwe. Jak w przypadku pozostaych strategii, moe to powodowa problemy:

Kolejno pobierania moe nie by zgodna z kolejnoci uywania; zasb zajty na pocztku
moe nie by uywany a do koca procesu. Moe to powodowa dusze zajcie zasobw, ni
s potrzebne.

Czasami nie mona wywnioskowa kolejnoci przydzielania zasobw. Jeeli identyfikator


kolejnego zasobu pochodzi z operacji wykonanej na pierwszym, to kolejno moe nie by
waciwa.

Istnieje wiele sposobw unikania zakleszcze. Niektre prowadz do zagodzenia, natomiast inne
do silnego obcienia procesora i zwikszenia czasw odpowiedzi. NMDO5!
Izolowanie czci rozwizania odpowiedzialnych za wtki pozwalajce na dostrajanie i eksperymentowanie jest efektywnym sposobem uzyskania wiedzy pozwalajcej na wybranie najlepszej
strategii.

Nie ma darmowych obiadw.

348

DODATEK A

Testowanie kodu wielowtkowego


Jak napisa test pokazujcy, e poniszy kod zawiera bd?
01: public class ClassWithThreadingProblem {
02:
int nextId;
03:
04:
public int takeNextId() {
05:
return nextId++;
06:
}
07: }

Poniej zamieszczony jest opis testu, ktry udowadnia, e w kodzie jest bd:

Zapamitaj biec warto nextId.

Utwrz dwa wtki, ktre jednoczenie wywouj takeNextId().

Sprawd, czy nextId ma warto o dwa wiksz ni na pocztku.

Uruchamiaj test do momentu, gdy nextId bdzie zwikszone o jeden zamiast dwa.

Test taki jest zamieszczony na listingu A2.


L I S T I N G A 2 . ClassWithThreadingProblemTest.java
01:
02:
03:
04:
05:
06:
07:
08:
09:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:

package example;
import static org.junit.Assert.fail;
import org.junit.Test;
public class ClassWithThreadingProblemTest {
@Test
public void twoThreadsShouldFailEventually() throws Exception {
final ClassWithThreadingProblem classWithThreadingProblem
= new ClassWithThreadingProblem();
Runnable runnable = new Runnable() {
public void run() {
classWithThreadingProblem.takeNextId();
}
};
for (int i = 0; i < 50000; ++i) {
int startingId = classWithThreadingProblem.lastId;
int expectedResult = 2 + startingId;
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t1.start();
t2.start();
t1.join();
t2.join();
int endingId = classWithThreadingProblem.lastId;
if (endingId != expectedResult)
return;
}

WSPBIENO II

349

34:
35:
36:
37: }

fail("Powinien pokaza problem z wtkami, ale si to nie udao.");


}

Wiersz

Opis

10

Utworzenie obiektu ClassWithThreadingProblem. Musielimy tu uy sowa kluczowego


final, poniewa uywamy go poniej wewntrznej klasy anonimowej.

12 16

Utworzenie anonimowej klasy wewntrznej, ktra korzysta z jednego obiektu


ClassWithThreadingProblem.

18

Uruchomienie tego kodu odpowiedni liczb razy w celu pokazania, e kod zawiedzie, ale nie tak
duo, aby test wykonywa si zbyt dugo. Jest to kwestia wywaenia; nie chcemy oczekiwa zbyt
dugo na pokazanie awarii. Ustalenie tej liczby jest trudne cho pniej pokaemy, e mona
znacznie j zmniejszy.

19

Zapamitanie wartoci pocztkowej. Test ten prbuje udowodni, e kod w ClassWith


ThreadingProblem jest nieprawidowy. Jeeli ten test zostanie wykonany, udowodnimy,
e kod zawiera bd. Jeeli test zawiedzie, nie bdzie w stanie pokaza bdu w kodzie.

20

Oczekujemy, e kocowa warto bdzie o dwa wiksza ni bieca warto.

22 23

Utworzenie dwch wtkw, oba s utworzone w wierszach 12. 16. Daje to nam potencjalnie dwa wtki
prbujce uy jednego obiektu ClassWithThreadingProblem, wpywajce wzajemnie na siebie.

24 25

Upewnienie si, e dwa wtki s gotowe do uruchomienia.

26 27

Oczekiwanie na zakoczenie obu wtkw przed sprawdzeniem wynikw.

29

Zapamitanie aktualnej wartoci kocowej.

31 32

Czy warto endingId rni si od oczekiwanej? Jeeli tak, koczymy test udowodnilimy,
e kod zawiera bd. Jeeli nie, prbujemy jeszcze raz.

35

Jeeli doszlimy do tego miejsca, nie jestemy w stanie udowodni w rozsdnym czasie, e kod
produkcyjny zawiera bd; nasz test zawid. Albo kod nie zawiera bdu, albo nie wykonalimy
wystarczajco duo iteracji, aby wystpia sytuacja bdna.

Test ten konfiguruje warunki wystpienia problemu rwnolegej aktualizacji. Jednak problem wystpuje tak rzadko, e w wikszoci przypadkw test go nie wykryje.
W rzeczywistoci, aby wykry bd, musimy ustawi liczb iteracji na ponad milion. Nawet wtedy
w dziesiciu uruchomieniach ptli wykonujcej 1 000 000 iteracji problem wystpuje tylko raz.
Oznacza to, e aby uzyska waciwe wyniki, powinnimy ustawi liczb iteracji na ponad sto milionw. Na jak dugie czekanie jestemy przygotowani?
Nawet jeeli ustawimy test na niezawodne wykrywanie bdu na jednym komputerze, prawdopodobnie bdziemy musieli zrobi to ponownie, aby wykry bd na innym komputerze, innym
systemie operacyjnym lub wersji JVM.
Pamitajmy, e jest to prosty problem. Jeeli nie moemy atwo zademonstrowa bdu w tym
przypadku, to jak wykry naprawd skomplikowane problemy?
Jakie podejcie moemy przyj, aby zademonstrowa taki prosty bd? Co waniejsze, jak pisa testy,
ktre demonstruj bdy w bardziej zoonym kodzie? W jaki sposb wykry bdy w kodzie, gdy
nie wiemy, gdzie trzeba ich szuka?

350

DODATEK A

Poniej przedstawiono kilka pomysw.

Testowanie Monte Carlo. Testy naley pisa elastycznie, aby mogy by dostrajane. Nastpnie
naley je wykonywa w ptli na przykad na serwerze testowym losowo zmieniajc wartoci dostrajajce. Jeeli kiedykolwiek test si nie powiedzie, to w kodzie jest bd. Naley zapewni, e testy te zostan napisane moliwie wczenie, aby serwer testowy zacz je wykonywa
moliwie szybko. Przy okazji naley pamita o precyzyjnym zarejestrowaniu sytuacji, w ktrej
test si nie powid.

Testy naley przeprowadza na kadej platformie docelowej. Wielokrotnie. Cigle. Im duej


testy dziaaj bezbdnie, tym bardziej prawdopodobne jest, e kod produkcyjny jest prawidowy lub testy nie s w stanie ujawni problemu.

Testy powinny by uruchamiane na komputerze o rnym obcieniu. Jeeli moemy zasymulowa obcienie zblione do wystpujcego w rodowisku produkcyjnym, powinnimy
to zrobi.

Jednak nawet jeeli wykonamy wszystkie te operacje, nadal nie mamy zbyt duej szansy na znalezienie problemw wielowtkowoci w naszym kodzie. W wikszoci przypadkw wystpuj one
w maych sekcjach i zdarzaj si raz na miliard wykona. Takie problemy s przeklestwem zoonych systemw.

Narzdzia wspierajce testowanie kodu


korzystajcego z wtkw
W firmie IBM zostao wyprodukowane narzdzie o nazwie ConTest6. Pozwala ono na instrumentacj klas w taki sposb, aby kod niebezpieczny dla wtkw z wikszym prawdopodobiestwem
wygenerowa bd.
Nie mamy adnych bezporednich zwizkw z firm IBM ani zespoem produkujcym ConTest.
Nasi koledzy nam go pokazali. Ju po kilku minutach uywania tego programu zauwaylimy
ogromn popraw moliwoci znalezienia bdw wielowtkowoci.
Sposb wykorzystania programu ConTest jest nastpujcy:

Napisanie testw i kodu produkcyjnego. Testy powinny by tak zaprojektowane, aby symulowa
wielu uytkownikw przy rnym obcieniu, tak jak wspomnielimy to wczeniej.

Instrumentacja kodu testw i kodu produkcyjnego za pomoc ConTest.

Uruchomienie testw.

Gdy przetworzylimy nasz kod za pomoc programu ConTest, wspczynnik trafie wzrs
z okoo jednej awarii na dziesi milionw iteracji do okoo jednej awarii na trzydzieci iteracji.
Po instrumentacji wartoci licznika ptli dla kilku kolejnych wykona wynosiy: 13, 23, 0, 54, 16,
6

http://www.haifa.ibm.com/projects/verification/contest/index.html

WSPBIENO II

351

14, 6, 69, 107, 49, 2. Jasno wida, e klasy po instrumentacji znacznie wczeniej i bardziej niezawodnie pozwalay wykry bdy.

Zakoczenie
Rozdzia ten by krtk przejadk przez ogromne i zdradzieckie terytorium programowania
wspbienego. Co najwyej zarysowalimy kilka zagadnie. Naszym celem byo skupienie si na
dyscyplinie zachowania czystoci kodu, ale jeeli zamierzamy pisa systemy wspbiene, powinnimy nauczy si znacznie wicej. Na pocztek polecamy wietn ksik Dougha Lea Concurrent
Programming in Java: Design Principles and Patterns7.
W tym rozdziale zajlimy si wspbien aktualizacj oraz dyscyplin czystej synchronizacji i blokowania. Wspominalimy o sposobie poprawiania przepustowoci systemw zwizanych z wejciemwyjciem za pomoc wtkw oraz pokazalimy techniki osigania takiej poprawy. Mwilimy o zakleszczeniach i sposobach ich unikania. Na koniec przedstawilimy strategie ujawniania problemw
z wielowtkowoci przez instrumentacj kodu.

Samouczek. Peny kod przykadw


Klient-serwer bez wtkw
L I S T I N G A 3 . Server.java
package com.objectmentor.clientserver.nonthreaded;
import
import
import
import

java.io.IOException;
java.net.ServerSocket;
java.net.Socket;
java.net.SocketException;

import common.MessageUtils;
public class Server implements Runnable {
ServerSocket serverSocket;
volatile boolean keepProcessing = true;
public Server(int port, int millisecondsTimeout) throws IOException {
serverSocket = new ServerSocket(port);
serverSocket.setSoTimeout(millisecondsTimeout);
}
public void run() {
System.out.printf("Serwer uruchamia si\n");
while (keepProcessing) {
try {
System.out.printf("przyjmowanie klienta\n");
Socket socket = serverSocket.accept();
System.out.printf("klient przyjty\n");
process(socket);

Patrz [Lea99], s. 191.

352

DODATEK A

} catch (Exception e) {
handle(e);
}
}
}
private void handle(Exception e) {
if (!(e instanceof SocketException)) {
e.printStackTrace();
}
}
public void stopProcessing() {
keepProcessing = false;
closeIgnoringException(serverSocket);
}
void process(Socket socket) {
if (socket == null)
return;
try {
System.out.printf("Serwer: pobieranie komunikatu\n");
String message = MessageUtils.getMessage(socket);
System.out.printf("Serwer: komunikat pobrany: %s\n", message);
Thread.sleep(1000);
System.out.printf("Serwer: wysyanie odpowiedzi: %s\n", message);
MessageUtils.sendMessage(socket, "Przetworzone: " + message);
System.out.printf("Serwer: wysane\n");
closeIgnoringException(socket);
} catch (Exception e) {
e.printStackTrace();
}
}
private void closeIgnoringException(Socket socket) {
if (socket != null)
try {
socket.close();
} catch (IOException ignore) {
}
}
private void closeIgnoringException(ServerSocket serverSocket) {
if (serverSocket != null)
try {
serverSocket.close();
} catch (IOException ignore) {
}
}
}

L I S T I N G A 4 . ClientTest.java
package com.objectmentor.clientserver.nonthreaded;
import
import
import
import

java.io.IOException;
java.net.ServerSocket;
java.net.Socket;
java.net.SocketException;

import common.MessageUtils;
public class Server implements Runnable {

WSPBIENO II

353

ServerSocket serverSocket;
volatile boolean keepProcessing = true;
public Server(int port, int millisecondsTimeout) throws IOException {
serverSocket = new ServerSocket(port);
serverSocket.setSoTimeout(millisecondsTimeout);
}
public void run() {
System.out.printf("Serwer uruchamia si\n");
while (keepProcessing) {
try {
System.out.printf("przyjmowanie klienta\n");
Socket socket = serverSocket.accept();
System.out.printf("klient przyjty\n");
process(socket);
} catch (Exception e) {
handle(e);
}
}
}
private void handle(Exception e) {
if (!(e instanceof SocketException)) {
e.printStackTrace();
}
}
public void stopProcessing() {
keepProcessing = false;
closeIgnoringException(serverSocket);
}
void process(Socket socket) {
if (socket == null)
return;
try {
System.out.printf("Serwer: pobieranie komunikatu\n");
String message = MessageUtils.getMessage(socket);
System.out.printf("Serwer: komunikat pobrany: %s\n", message);
Thread.sleep(1000);
System.out.printf("Serwer: wysyanie odpowiedzi: %s\n", message);
MessageUtils.sendMessage(socket, "Przetworzone: " + message);
System.out.printf("Serwer: wysane\n");
closeIgnoringException(socket);
} catch (Exception e) {
e.printStackTrace();
}
}
private void closeIgnoringException(Socket socket) {
if (socket != null)
try {
socket.close();
} catch (IOException ignore) {
}
}
private void closeIgnoringException(ServerSocket serverSocket) {
if (serverSocket != null)
try {
serverSocket.close();
} catch (IOException ignore) {

354

DODATEK A

}
}
}

L I S T I N G A 5 . MessageUtils.java
package common;
import
import
import
import
import
import

java.io.IOException;
java.io.InputStream;
java.io.ObjectInputStream;
java.io.ObjectOutputStream;
java.io.OutputStream;
java.net.Socket;

public class MessageUtils {


public static void sendMessage(Socket socket, String message)
throws IOException {
OutputStream stream = socket.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(stream);
oos.writeUTF(message);
oos.flush();
}
public static String getMessage(Socket socket) throws IOException {
InputStream stream = socket.getInputStream();
ObjectInputStream ois = new ObjectInputStream(stream);
return ois.readUTF();
}
}

Klient-serwer z uyciem wtkw


Aby serwer mg korzysta z wtkw, wystarczy zmieni sposb przetwarzania komunikatu (nowe
wiersze s dla uatwienia wyrnione):
void process(final Socket socket) {
if (socket == null)
return;
Runnable clientHandler = new Runnable() {
public void run() {
try {
System.out.printf("Serwer: pobieranie komunikatu\n");
String message = MessageUtils.getMessage(socket);
System.out.printf("Serwer: komunikat pobrany: %s\n", message);
Thread.sleep(1000);
System.out.printf("Serwer: wysyanie odpowiedzi: %s\n", message);
MessageUtils.sendMessage(socket, "Przetworzone: " + message);
System.out.printf("Serwer: wysane\n");
closeIgnoringException(socket);
} catch (Exception e) {
e.printStackTrace();
}
}
};
Thread clientConnection = new Thread(clientHandler);
clientConnection.start();
}

WSPBIENO II

355

356

DODATEK A

DODATEK B

org.jfree.date.SerialDate
L I S T I N G B 1 . SerialDate.Java
1 /* =================================================================
2 * JCommon : a free general purpose class library for the Java(tm) platform
3 * ================================================================
4 *
5 * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
6 *
7 * Project Info: http://www.jfree.org/jcommon/index.html
8 *
9 * This library is free software; you can redistribute it and/or modify it
10 * under the terms of the GNU Lesser General Public License as published by
11 * the Free Software Foundation; either version 2.1 of the License, or
12 * (at your option) any later version.
13 *
14 * This library is distributed in the hope that it will be useful, but
15 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
16 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
17 * License for more details.
18 *
19 * You should have received a copy of the GNU Lesser General Public
20 * License along with this library; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
22 * USA.
23 *
24 * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
25 * in the United States and other countries.]
26 *
27 * --------------28 * SerialDate.java
29 * --------------30 * (C) Copyright 2001-2005, by Object Refinery Limited.
31 *
32 * Original Author: David Gilbert (for Object Refinery Limited);
33 * Contributor(s): -;
34 *
35 * $Id: SerialDate.java,v 1.7 2005/11/03 09:25:17 mungady Exp $
36 *
37 * Changes (from 11-Oct-2001)
38 * -------------------------39 * 11-Oct-2001 : Re-organised the class and moved it to new package
40 *
com.jrefinery.date (DG);

357

41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101

358

* 05-Nov-2001 : Added a getDescription() method, and eliminated NotableDate


*
class (DG);
* 12-Nov-2001 : IBD requires setDescription() method, now that NotableDate
*
class is gone (DG); Changed getPreviousDayOfWeek(),
*
getFollowingDayOfWeek() and getNearestDayOfWeek() to correct
*
bugs (DG);
* 05-Dec-2001 : Fixed bug in SpreadsheetDate class (DG);
* 29-May-2002 : Moved the month constants into a separate interface
*
(MonthConstants) (DG);
* 27-Aug-2002 : Fixed bug in addMonths() method, thanks to N???levka Petr (DG);
* 03-Oct-2002 : Fixed errors reported by Checkstyle (DG);
* 13-Mar-2003 : Implemented Serializable (DG);
* 29-May-2003 : Fixed bug in addMonths method (DG);
* 04-Sep-2003 : Implemented Comparable. Updated the isInRange javadocs (DG);
* 05-Jan-2005 : Fixed bug in addYears() method (1096282) (DG);
*
*/
package org.jfree.date;
import
import
import
import
import

java.io.Serializable;
java.text.DateFormatSymbols;
java.text.SimpleDateFormat;
java.util.Calendar;
java.util.GregorianCalendar;

/**
* An abstract class that defines our requirements for manipulating dates,
* without tying down a particular implementation.
* <P>
* Requirement 1 : match at least what Excel does for dates;
* Requirement 2 : class is immutable;
* <P>
* Why not just use java.util.Date? We will, when it makes sense. At times,
* java.util.Date can be *too* precise - it represents an instant in time,
* accurate to 1/1000th of a second (with the date itself depending on the
* time-zone). Sometimes we just want to represent a particular day (e.g. 21
* January 2015) without concerning ourselves about the time of day, or the
* time-zone, or anything else. That's what we've defined SerialDate for.
* <P>
* You can call getInstance() to get a concrete subclass of SerialDate,
* without worrying about the exact implementation.
*
* @author David Gilbert
*/
public abstract class SerialDate implements Comparable,
Serializable,
MonthConstants {

/** For serialization. */


private static final long serialVersionUID = -293716040467423637L;

/** Date format symbols. */


public static final DateFormatSymbols
DATE_FORMAT_SYMBOLS = new SimpleDateFormat().getDateFormatSymbols();

/** The serial number for 1 January 1900. */


public static final int SERIAL_LOWER_BOUND = 2;

/** The serial number for 31 December 9999. */


public static final int SERIAL_UPPER_BOUND = 2958465;

DODATEK B

102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163

/** The lowest year value supported by this date format. */


public static final int MINIMUM_YEAR_SUPPORTED = 1900;

/** The highest year value supported by this date format. */


public static final int MAXIMUM_YEAR_SUPPORTED = 9999;

/** Useful constant for Monday. Equivalent to java.util.Calendar.MONDAY. */


public static final int MONDAY = Calendar.MONDAY;

/**
* Useful constant for Tuesday. Equivalent to java.util.Calendar.TUESDAY.
*/
public static final int TUESDAY = Calendar.TUESDAY;

/**
* Useful constant for Wednesday. Equivalent to
* java.util.Calendar.WEDNESDAY.
*/
public static final int WEDNESDAY = Calendar.WEDNESDAY;

/**
* Useful constant for Thrusday. Equivalent to java.util.Calendar.THURSDAY.
*/
public static final int THURSDAY = Calendar.THURSDAY;

/** Useful constant for Friday. Equivalent to java.util.Calendar.FRIDAY. */


public static final int FRIDAY = Calendar.FRIDAY;

/**
* Useful constant for Saturday. Equivalent to java.util.Calendar.SATURDAY.
*/
public static final int SATURDAY = Calendar.SATURDAY;

/** Useful constant for Sunday. Equivalent to java.util.Calendar.SUNDAY. */


public static final int SUNDAY = Calendar.SUNDAY;

/** The number of days in each month in non leap years. */


static final int[] LAST_DAY_OF_MONTH =
{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

/** The number of days in a (non-leap) year up to the end of each month. */
static final int[] AGGREGATE_DAYS_TO_END_OF_MONTH =
{0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365};

/** The number of days in a year up to the end of the preceding month. */
static final int[] AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH =
{0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365};

/** The number of days in a leap year up to the end of each month. */
static final int[] LEAP_YEAR_AGGREGATE_DAYS_TO_END_OF_MONTH =
{0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366};

/**
* The number of days in a leap year up to the end of the preceding month.
*/
static final int[]
LEAP_YEAR_AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH =
{0, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366};

/** A useful constant for referring to the first week in a month. */


public static final int FIRST_WEEK_IN_MONTH = 1;

ORG.JFREE.DATE.SERIALDATE

359

164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225

360

/** A useful constant for referring to the second week in a month. */


public static final int SECOND_WEEK_IN_MONTH = 2;

/** A useful constant for referring to the third week in a month. */


public static final int THIRD_WEEK_IN_MONTH = 3;

/** A useful constant for referring to the fourth week in a month. */


public static final int FOURTH_WEEK_IN_MONTH = 4;

/** A useful constant for referring to the last week in a month. */


public static final int LAST_WEEK_IN_MONTH = 0;

/** Useful range constant. */


public static final int INCLUDE_NONE = 0;

/** Useful range constant. */


public static final int INCLUDE_FIRST = 1;

/** Useful range constant. */


public static final int INCLUDE_SECOND = 2;

/** Useful range constant. */


public static final int INCLUDE_BOTH = 3;

/**
* Useful constant for specifying a day of the week relative to a fixed
* date.
*/
public static final int PRECEDING = -1;

/**
* Useful constant for specifying a day of the week relative to a fixed
* date.
*/
public static final int NEAREST = 0;

/**
* Useful constant for specifying a day of the week relative to a fixed
* date.
*/
public static final int FOLLOWING = 1;

/** A description for the date. */


private String description;

/**
* Default constructor.
*/
protected SerialDate() {
}

/**
* Returns <code>true</code> if the supplied integer code represents a
* valid day-of-the-week, and <code>false</code> otherwise.
*
* @param code the code being checked for validity.
*
* @return <code>true</code> if the supplied integer code represents a
*
valid day-of-the-week, and <code>false</code> otherwise.
*/
public static boolean isValidWeekdayCode(final int code) {

DODATEK B

226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288

switch(code) {
case SUNDAY:
case MONDAY:
case TUESDAY:
case WEDNESDAY:
case THURSDAY:
case FRIDAY:
case SATURDAY:
return true;
default:
return false;
}
}

/**
* Converts the supplied string to a day of the week.
*
* @param s a string representing the day of the week.
*
* @return <code>-1</code> if the string is not convertable, the day of
*
the week otherwise.
*/
public static int stringToWeekdayCode(String s) {
final String[] shortWeekdayNames
= DATE_FORMAT_SYMBOLS.getShortWeekdays();
final String[] weekDayNames = DATE_FORMAT_SYMBOLS.getWeekdays();
int result = -1;
s = s.trim();
for (int i = 0; i < weekDayNames.length; i++) {
if (s.equals(shortWeekdayNames[i])) {
result = i;
break;
}
if (s.equals(weekDayNames[i])) {
result = i;
break;
}
}
return result;
}

/**
* Returns a string representing the supplied day-of-the-week.
* <P>
* Need to find a better approach.
*
* @param weekday the day of the week.
*
* @return a string representing the supplied day-of-the-week.
*/
public static String weekdayCodeToString(final int weekday) {
final String[] weekdays = DATE_FORMAT_SYMBOLS.getWeekdays();
return weekdays[weekday];
}

/**

ORG.JFREE.DATE.SERIALDATE

361

289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350

362

* Returns an array of month names.


*
* @return an array of month names.
*/
public static String[] getMonths() {
return getMonths(false);
}

/**
* Returns an array of month names.
*
* @param shortened a flag indicating that shortened month names should
*
be returned.
*
* @return an array of month names.
*/
public static String[] getMonths(final boolean shortened) {
if (shortened) {
return DATE_FORMAT_SYMBOLS.getShortMonths();
}
else {
return DATE_FORMAT_SYMBOLS.getMonths();
}
}

/**
* Returns true if the supplied integer code represents a valid month.
*
* @param code the code being checked for validity.
*
* @return <code>true</code> if the supplied integer code represents a
*
valid month.
*/
public static boolean isValidMonthCode(final int code) {
switch(code) {
case JANUARY:
case FEBRUARY:
case MARCH:
case APRIL:
case MAY:
case JUNE:
case JULY:
case AUGUST:
case SEPTEMBER:
case OCTOBER:
case NOVEMBER:
case DECEMBER:
return true;
default:
return false;
}
}

/**
* Returns the quarter for the specified month.
*

DODATEK B

351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412

* @param code the month code (1-12).


*
* @return the quarter that the month belongs to.
* @throws java.lang.IllegalArgumentException
*/
public static int monthCodeToQuarter(final int code) {
switch(code) {
case JANUARY:
case FEBRUARY:
case MARCH: return 1;
case APRIL:
case MAY:
case JUNE: return 2;
case JULY:
case AUGUST:
case SEPTEMBER: return 3;
case OCTOBER:
case NOVEMBER:
case DECEMBER: return 4;
default: throw new IllegalArgumentException(
"SerialDate.monthCodeToQuarter: invalid month code.");
}
}

/**
* Returns a string representing the supplied month.
* <P>
* The string returned is the long form of the month name taken from the
* default locale.
*
* @param month the month.
*
* @return a string representing the supplied month.
*/
public static String monthCodeToString(final int month) {
return monthCodeToString(month, false);
}

/**
* Returns a string representing the supplied month.
* <P>
* The string returned is the long or short form of the month name taken
* from the default locale.
*
* @param month the month.
* @param shortened if <code>true</code> return the abbreviation of the
*
month.
*
* @return a string representing the supplied month.
* @throws java.lang.IllegalArgumentException
*/
public static String monthCodeToString(final int month,
final boolean shortened) {

// check arguments...
if (!isValidMonthCode(month)) {
throw new IllegalArgumentException(
"SerialDate.monthCodeToString: month outside valid range.");

ORG.JFREE.DATE.SERIALDATE

363

413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475

364

}
final String[] months;
if (shortened) {
months = DATE_FORMAT_SYMBOLS.getShortMonths();
}
else {
months = DATE_FORMAT_SYMBOLS.getMonths();
}
return months[month - 1];
}

/**
* Converts a string to a month code.
* <P>
* This method will return one of the constants JANUARY, FEBRUARY, ,
* DECEMBER that corresponds to the string. If the string is not
* recognised, this method returns -1.
*
* @param s the string to parse.
*
* @return <code>-1</code> if the string is not parseable, the month of the
*
year otherwise.
*/
public static int stringToMonthCode(String s) {
final String[] shortMonthNames = DATE_FORMAT_SYMBOLS.getShortMonths();
final String[] monthNames = DATE_FORMAT_SYMBOLS.getMonths();
int result = -1;
s = s.trim();

// first try parsing the string as an integer (1-12)


try {
result = Integer.parseInt(s);
}
catch (NumberFormatException e) {

// suppress
}

// now search through the month names


if ((result < 1) || (result > 12)) {
for (int i = 0; i < monthNames.length; i++) {
if (s.equals(shortMonthNames[i])) {
result = i + 1;
break;
}
if (s.equals(monthNames[i])) {
result = i + 1;
break;
}
}
}
return result;
}

/**
* Returns true if the supplied integer code represents a valid

DODATEK B

476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537

* week-in-the-month, and false otherwise.


*
* @param code the code being checked for validity.
* @return <code>true</code> if the supplied integer code represents a
*
valid week-in-the-month.
*/
public static boolean isValidWeekInMonthCode(final int code) {
switch(code) {
case FIRST_WEEK_IN_MONTH:
case SECOND_WEEK_IN_MONTH:
case THIRD_WEEK_IN_MONTH:
case FOURTH_WEEK_IN_MONTH:
case LAST_WEEK_IN_MONTH: return true;
default: return false;
}
}

/**
* Determines whether or not the specified year is a leap year.
*
* @param yyyy the year (in the range 1900 to 9999).
*
* @return <code>true</code> if the specified year is a leap year.
*/
public static boolean isLeapYear(final int yyyy) {
if ((yyyy % 4) != 0) {
return false;
}
else if ((yyyy % 400) == 0) {
return true;
}
else if ((yyyy % 100) == 0) {
return false;
}
else {
return true;
}
}

/**
* Returns the number of leap years from 1900 to the specified year
* INCLUSIVE.
* <P>
* Note that 1900 is not a leap year.
*
* @param yyyy the year (in the range 1900 to 9999).
*
* @return the number of leap years from 1900 to the specified year.
*/
public static int leapYearCount(final int yyyy) {
final int leap4 = (yyyy - 1896) / 4;
final int leap100 = (yyyy - 1800) / 100;
final int leap400 = (yyyy - 1600) / 400;
return leap4 - leap100 + leap400;
}

ORG.JFREE.DATE.SERIALDATE

365

538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599

366

/**
* Returns the number of the last day of the month, taking into account
* leap years.
*
* @param month the month.
* @param yyyy the year (in the range 1900 to 9999).
*
* @return the number of the last day of the month.
*/
public static int lastDayOfMonth(final int month, final int yyyy) {
final int result = LAST_DAY_OF_MONTH[month];
if (month != FEBRUARY) {
return result;
}
else if (isLeapYear(yyyy)) {
return result + 1;
}
else {
return result;
}
}

/**
* Creates a new date by adding the specified number of days to the base
* date.
*
* @param days the number of days to add (can be negative).
* @param base the base date.
*
* @return a new date.
*/
public static SerialDate addDays(final int days, final SerialDate base) {
final int serialDayNumber = base.toSerial() + days;
return SerialDate.createInstance(serialDayNumber);
}

/**
* Creates a new date by adding the specified number of months to the base
* date.
* <P>
* If the base date is close to the end of the month, the day on the result
* may be adjusted slightly: 31 May + 1 month = 30 June.
*
* @param months the number of months to add (can be negative).
* @param base the base date.
*
* @return a new date.
*/
public static SerialDate addMonths(final int months,
final SerialDate base) {
final int yy = (12 * base.getYYYY() + base.getMonth() + months - 1)
/ 12;
final int mm = (12 * base.getYYYY() + base.getMonth() + months - 1)
% 12 + 1;
final int dd = Math.min(
base.getDayOfMonth(), SerialDate.lastDayOfMonth(mm, yy)
);

DODATEK B

600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661

return SerialDate.createInstance(dd, mm, yy);


}

/**
* Creates a new date by adding the specified number of years to the base
* date.
*
* @param years the number of years to add (can be negative).
* @param base the base date.
*
* @return A new date.
*/
public static SerialDate addYears(final int years, final SerialDate base) {
final int baseY = base.getYYYY();
final int baseM = base.getMonth();
final int baseD = base.getDayOfMonth();
final int targetY = baseY + years;
final int targetD = Math.min(
baseD, SerialDate.lastDayOfMonth(baseM, targetY)
);
return SerialDate.createInstance(targetD, baseM, targetY);
}

/**
* Returns the latest date that falls on the specified day-of-the-week and
* is BEFORE the base date.
*
* @param targetWeekday a code for the target day-of-the-week.
* @param base the base date.
*
* @return the latest date that falls on the specified day-of-the-week and
*
is BEFORE the base date.
*/
public static SerialDate getPreviousDayOfWeek(final int targetWeekday,
final SerialDate base) {

// check arguments
if (!SerialDate.isValidWeekdayCode(targetWeekday)) {
throw new IllegalArgumentException(
"Invalid day-of-the-week code."
);
}

// find the date


final int adjust;
final int baseDOW = base.getDayOfWeek();
if (baseDOW > targetWeekday) {
adjust = Math.min(0, targetWeekday - baseDOW);
}
else {
adjust = -7 + Math.max(0, targetWeekday - baseDOW);
}
return SerialDate.addDays(adjust, base);
}

ORG.JFREE.DATE.SERIALDATE

367

662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723

368

/**
* Returns the earliest date that falls on the specified day-of-the-week
* and is AFTER the base date.
*
* @param targetWeekday a code for the target day-of-the-week.
* @param base the base date.
*
* @return the earliest date that falls on the specified day-of-the-week
*
and is AFTER the base date.
*/
public static SerialDate getFollowingDayOfWeek(final int targetWeekday,
final SerialDate base) {

// check arguments
if (!SerialDate.isValidWeekdayCode(targetWeekday)) {
throw new IllegalArgumentException(
"Invalid day-of-the-week code."
);
}

// find the date


final int adjust;
final int baseDOW = base.getDayOfWeek();
if (baseDOW > targetWeekday) {
adjust = 7 + Math.min(0, targetWeekday - baseDOW);
}
else {
adjust = Math.max(0, targetWeekday - baseDOW);
}
return SerialDate.addDays(adjust, base);
}

/**
* Returns the date that falls on the specified day-of-the-week and is
* CLOSEST to the base date.
*
* @param targetDOW a code for the target day-of-the-week.
* @param base the base date.
*
* @return the date that falls on the specified day-of-the-week and is
*
CLOSEST to the base date.
*/
public static SerialDate getNearestDayOfWeek(final int targetDOW,
final SerialDate base) {

// check arguments
if (!SerialDate.isValidWeekdayCode(targetDOW)) {
throw new IllegalArgumentException(
"Invalid day-of-the-week code."
);
}

// find the date


final int baseDOW = base.getDayOfWeek();
int adjust = -Math.abs(targetDOW - baseDOW);
if (adjust >= 4) {
adjust = 7 - adjust;
}
if (adjust <= -4) {
adjust = 7 + adjust;
}

DODATEK B

724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785

return SerialDate.addDays(adjust, base);


}

/**
* Rolls the date forward to the last day of the month.
*
* @param base the base date.
*
* @return a new serial date.
*/
public SerialDate getEndOfCurrentMonth(final SerialDate base) {
final int last = SerialDate.lastDayOfMonth(
base.getMonth(), base.getYYYY()
);
return SerialDate.createInstance(last, base.getMonth(), base.getYYYY());
}

/**
* Returns a string corresponding to the week-in-the-month code.
* <P>
* Need to find a better approach.
*
* @param count an integer code representing the week-in-the-month.
*
* @return a string corresponding to the week-in-the-month code.
*/
public static String weekInMonthToString(final int count) {
switch (count) {
case SerialDate.FIRST_WEEK_IN_MONTH : return "First";
case SerialDate.SECOND_WEEK_IN_MONTH : return "Second";
case SerialDate.THIRD_WEEK_IN_MONTH : return "Third";
case SerialDate.FOURTH_WEEK_IN_MONTH : return "Fourth";
case SerialDate.LAST_WEEK_IN_MONTH : return "Last";
default :
return "SerialDate.weekInMonthToString(): invalid code.";
}
}

/**
* Returns a string representing the supplied 'relative'.
* <P>
* Need to find a better approach.
*
* @param relative a constant representing the 'relative'.
*
* @return a string representing the supplied 'relative'.
*/
public static String relativeToString(final int relative) {
switch (relative) {
case SerialDate.PRECEDING
case SerialDate.NEAREST :
case SerialDate.FOLLOWING
default : return "ERROR :
}

: return "Preceding";
return "Nearest";
: return "Following";
Relative To String";

/**

ORG.JFREE.DATE.SERIALDATE

369

786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846

370

* Factory method that returns an instance of some concrete subclass of


* {@link SerialDate}.
*
* @param day the day (1-31).
* @param month the month (1-12).
* @param yyyy the year (in the range 1900 to 9999).
*
* @return An instance of {@link SerialDate}.
*/
public static SerialDate createInstance(final int day, final int month,
final int yyyy) {
return new SpreadsheetDate(day, month, yyyy);
}

/**
* Factory method that returns an instance of some concrete subclass of
* {@link SerialDate}.
*
* @param serial the serial number for the day (1 January 1900 = 2).
*
* @return a instance of SerialDate.
*/
public static SerialDate createInstance(final int serial) {
return new SpreadsheetDate(serial);
}

/**
* Factory method that returns an instance of a subclass of SerialDate.
*
* @param date A Java date object.
*
* @return a instance of SerialDate.
*/
public static SerialDate createInstance(final java.util.Date date) {
final GregorianCalendar calendar = new GregorianCalendar();
calendar.setTime(date);
return new SpreadsheetDate(calendar.get(Calendar.DATE),
calendar.get(Calendar.MONTH) + 1,
calendar.get(Calendar.YEAR));
}

/**
* Returns the serial number for the date, where 1 January 1900 = 2 (this
* corresponds, almost, to the numbering system used in Microsoft Excel for
* Windows and Lotus 1-2-3).
*
* @return the serial number for the date.
*/
public abstract int toSerial();

/**
* Returns a java.util.Date. Since java.util.Date has more precision than
* SerialDate, we need to define a convention for the 'time of day'.
*
* @return this as <code>java.util.Date</code>.
*/
public abstract java.util.Date toDate();

/**

DODATEK B

847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907

* Returns a description of the date.


*
* @return a description of the date.
*/
public String getDescription() {
return this.description;
}

/**
* Sets the description for the date.
*
* @param description the new description for the date.
*/
public void setDescription(final String description) {
this.description = description;
}

/**
* Converts the date to a string.
*
* @return a string representation of the date.
*/
public String toString() {
return getDayOfMonth() + "-" + SerialDate.monthCodeToString(getMonth())
+ "-" + getYYYY();
}

/**
* Returns the year (assume a valid range of 1900 to 9999).
*
* @return the year.
*/
public abstract int getYYYY();

/**
* Returns the month (January = 1, February = 2, March = 3).
*
* @return the month of the year.
*/
public abstract int getMonth();

/**
* Returns the day of the month.
*
* @return the day of the month.
*/
public abstract int getDayOfMonth();

/**
* Returns the day of the week.
*
* @return the day of the week.
*/
public abstract int getDayOfWeek();

/**
* Returns the difference (in days) between this date and the specified
* 'other' date.
* <P>
* The result is positive if this date is after the 'other' date and
* negative if it is before the 'other' date.

ORG.JFREE.DATE.SERIALDATE

371

908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968

372

*
* @param other the date being compared to.
*
* @return the difference between this and the other date.
*/
public abstract int compare(SerialDate other);

/**
* Returns true if this SerialDate represents the same date as the
* specified SerialDate.
*
* @param other the date being compared to.
*
* @return <code>true</code> if this SerialDate represents the same date as
*
the specified SerialDate.
*/
public abstract boolean isOn(SerialDate other);

/**
* Returns true if this SerialDate represents an earlier date compared to
* the specified SerialDate.
*
* @param other The date being compared to.
*
* @return <code>true</code> if this SerialDate represents an earlier date
*
compared to the specified SerialDate.
*/
public abstract boolean isBefore(SerialDate other);

/**
* Returns true if this SerialDate represents the same date as the
* specified SerialDate.
*
* @param other the date being compared to.
*
* @return <code>true<code> if this SerialDate represents the same date
*
as the specified SerialDate.
*/
public abstract boolean isOnOrBefore(SerialDate other);

/**
* Returns true if this SerialDate represents the same date as the
* specified SerialDate.
*
* @param other the date being compared to.
*
* @return <code>true</code> if this SerialDate represents the same date
*
as the specified SerialDate.
*/
public abstract boolean isAfter(SerialDate other);

/**
* Returns true if this SerialDate represents the same date as the
* specified SerialDate.
*
* @param other the date being compared to.
*
* @return <code>true</code> if this SerialDate represents the same date
*
as the specified SerialDate.
*/
public abstract boolean isOnOrAfter(SerialDate other);

DODATEK B

969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029

/**
* Returns <code>true</code> if this {@link SerialDate} is within the
* specified range (INCLUSIVE). The date order of d1 and d2 is not
* important.
*
* @param d1 a boundary date for the range.
* @param d2 the other boundary date for the range.
*
* @return A boolean.
*/
public abstract boolean isInRange(SerialDate d1, SerialDate d2);

/**
* Returns <code>true</code> if this {@link SerialDate} is within the
* specified range (caller specifies whether or not the end-points are
* included). The date order of d1 and d2 is not important.
*
* @param d1 a boundary date for the range.
* @param d2 the other boundary date for the range.
* @param include a code that controls whether or not the start and end
*
dates are included in the range.
*
* @return A boolean.
*/
public abstract boolean isInRange(SerialDate d1, SerialDate d2,
int include);

/**
* Returns the latest date that falls on the specified day-of-the-week and
* is BEFORE this date.
*
* @param targetDOW a code for the target day-of-the-week.
*
* @return the latest date that falls on the specified day-of-the-week and
*
is BEFORE this date.
*/
public SerialDate getPreviousDayOfWeek(final int targetDOW) {
return getPreviousDayOfWeek(targetDOW, this);
}

/**
* Returns the earliest date that falls on the specified day-of-the-week
* and is AFTER this date.
*
* @param targetDOW a code for the target day-of-the-week.
*
* @return the earliest date that falls on the specified day-of-the-week
*
and is AFTER this date.
*/
public SerialDate getFollowingDayOfWeek(final int targetDOW) {
return getFollowingDayOfWeek(targetDOW, this);
}

/**
* Returns the nearest date that falls on the specified day-of-the-week.
*
* @param targetDOW a code for the target day-of-the-week.
*
* @return the nearest date that falls on the specified day-of-the-week.
*/

ORG.JFREE.DATE.SERIALDATE

373

1030
1031
1032
1033
1034 }

public SerialDate getNearestDayOfWeek(final int targetDOW) {


return getNearestDayOfWeek(targetDOW, this);
}

L I S T I N G B 2 . SerialDateTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

374

/* ==================================================================
* JCommon : a free general purpose class library for the Java(tm) platform
* =================================================================
*
* (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
*
* Project Info: http://www.jfree.org/jcommon/index.html
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA.
*
* [Java is a trademark or registered trademark of Sun Microsystems, Inc.
* in the United States and other countries.]
*
* -------------------* SerialDateTests.java
* -------------------* (C) Copyright 2001-2005, by Object Refinery Limited.
*
* Original Author: David Gilbert (for Object Refinery Limited);
* Contributor(s): -;
*
* $Id: SerialDateTests.java,v 1.5 2005/10/18 13:15:28 mungady Exp $
*
* Changes
* ------* 15-Nov-2001 : Version 1 (DG);
* 25-Jun-2002 : Removed unnecessary import (DG);
* 24-Oct-2002 : Fixed errors reported by Checkstyle (DG);
* 13-Mar-2003 : Added serialization test (DG);
* 05-Jan-2005 : Added test for bug report 1096282 (DG);
*
*/
package org.jfree.date.junit;
import
import
import
import
import

java.io.ByteArrayInputStream;
java.io.ByteArrayOutputStream;
java.io.ObjectInput;
java.io.ObjectInputStream;
java.io.ObjectOutput;

DODATEK B

54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115

import java.io.ObjectOutputStream;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import org.jfree.date.MonthConstants;
import org.jfree.date.SerialDate;

/**
* Some JUnit tests for the {@link SerialDate} class.
*/
public class SerialDateTests extends TestCase {

/** Date representing November 9. */


private SerialDate nov9Y2001;

/**
* Creates a new test case.
*
* @param name the name.
*/
public SerialDateTests(final String name) {
super(name);
}

/**
* Returns a test suite for the JUnit test runner.
*
* @return The test suite.
*/
public static Test suite() {
return new TestSuite(SerialDateTests.class);
}

/**
* Problem set up.
*/
protected void setUp() {
this.nov9Y2001 = SerialDate.createInstance(9, MonthConstants.NOVEMBER, 2001);
}

/**
* 9 Nov 2001 plus two months should be 9 Jan 2002.
*/
public void testAddMonthsTo9Nov2001() {
final SerialDate jan9Y2002 = SerialDate.addMonths(2, this.nov9Y2001);
final SerialDate answer = SerialDate.createInstance(9, 1, 2002);
assertEquals(answer, jan9Y2002);
}

/**
* A test case for a reported bug, now fixed.
*/
public void testAddMonthsTo5Oct2003() {
final SerialDate d1 = SerialDate.createInstance(5, MonthConstants.OCTOBER, 2003);
final SerialDate d2 = SerialDate.addMonths(2, d1);
assertEquals(d2, SerialDate.createInstance(5, MonthConstants.DECEMBER, 2003));
}

/**
* A test case for a reported bug, now fixed.

ORG.JFREE.DATE.SERIALDATE

375

116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175

376

*/
public void testAddMonthsTo1Jan2003() {
final SerialDate d1 = SerialDate.createInstance(1, MonthConstants.JANUARY, 2003);
final SerialDate d2 = SerialDate.addMonths(0, d1);
assertEquals(d2, d1);
}

/**
* Monday preceding Friday 9 November 2001 should be 5 November.
*/
public void testMondayPrecedingFriday9Nov2001() {
SerialDate mondayBefore = SerialDate.getPreviousDayOfWeek(
SerialDate.MONDAY, this.nov9Y2001
);
assertEquals(5, mondayBefore.getDayOfMonth());
}

/**
* Monday following Friday 9 November 2001 should be 12 November.
*/
public void testMondayFollowingFriday9Nov2001() {
SerialDate mondayAfter = SerialDate.getFollowingDayOfWeek(
SerialDate.MONDAY, this.nov9Y2001
);
assertEquals(12, mondayAfter.getDayOfMonth());
}

/**
* Monday nearest Friday 9 November 2001 should be 12 November.
*/
public void testMondayNearestFriday9Nov2001() {
SerialDate mondayNearest = SerialDate.getNearestDayOfWeek(
SerialDate.MONDAY, this.nov9Y2001
);
assertEquals(12, mondayNearest.getDayOfMonth());
}

/**
* The Monday nearest to 22nd January 1970 falls on the 19th.
*/
public void testMondayNearest22Jan1970() {
SerialDate jan22Y1970 = SerialDate.createInstance(22, MonthConstants.JANUARY,
1970);
SerialDate mondayNearest = SerialDate.getNearestDayOfWeek(SerialDate.MONDAY,
jan22Y1970);
assertEquals(19, mondayNearest.getDayOfMonth());
}

/**
* Problem that the conversion of days to strings returns the right result. Actually, this
* result depends on the Locale so this test needs to be modified.
*/
public void testWeekdayCodeToString() {
final String test = SerialDate.weekdayCodeToString(SerialDate.SATURDAY);
assertEquals("Saturday", test);
}

/**
* Test the conversion of a string to a weekday. Note that this test will fail if the
* default locale doesn't use English weekday names...devise a better test!

DODATEK B

176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237

*/
public void testStringToWeekday() {
int weekday = SerialDate.stringToWeekdayCode("Wednesday");
assertEquals(SerialDate.WEDNESDAY, weekday);
weekday = SerialDate.stringToWeekdayCode(" Wednesday ");
assertEquals(SerialDate.WEDNESDAY, weekday);
weekday = SerialDate.stringToWeekdayCode("Wed");
assertEquals(SerialDate.WEDNESDAY, weekday);
}

/**
* Test the conversion of a string to a month. Note that this test will fail if the default
* locale doesn't use English month names...devise a better test!
*/
public void testStringToMonthCode() {
int m = SerialDate.stringToMonthCode("January");
assertEquals(MonthConstants.JANUARY, m);
m = SerialDate.stringToMonthCode(" January ");
assertEquals(MonthConstants.JANUARY, m);
m = SerialDate.stringToMonthCode("Jan");
assertEquals(MonthConstants.JANUARY, m);
}

/**
* Tests the conversion of a month code to a string.
*/
public void testMonthCodeToStringCode() {
final String test = SerialDate.monthCodeToString(MonthConstants.DECEMBER);
assertEquals("December", test);
}

/**
* 1900 is not a leap year.
*/
public void testIsNotLeapYear1900() {
assertTrue(!SerialDate.isLeapYear(1900));
}

/**
* 2000 is a leap year.
*/
public void testIsLeapYear2000() {
assertTrue(SerialDate.isLeapYear(2000));
}

/**
* The number of leap years from 1900 up-to-and-including 1899 is 0.
*/
public void testLeapYearCount1899() {
assertEquals(SerialDate.leapYearCount(1899), 0);
}

ORG.JFREE.DATE.SERIALDATE

377

238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298

378

/**
* The number of leap years from 1900 up-to-and-including 1903 is 0.
*/
public void testLeapYearCount1903() {
assertEquals(SerialDate.leapYearCount(1903), 0);
}

/**
* The number of leap years from 1900 up-to-and-including 1904 is 1.
*/
public void testLeapYearCount1904() {
assertEquals(SerialDate.leapYearCount(1904), 1);
}

/**
* The number of leap years from 1900 up-to-and-including 1999 is 24.
*/
public void testLeapYearCount1999() {
assertEquals(SerialDate.leapYearCount(1999), 24);
}

/**
* The number of leap years from 1900 up-to-and-including 2000 is 25.
*/
public void testLeapYearCount2000() {
assertEquals(SerialDate.leapYearCount(2000), 25);
}

/**
* Serialize an instance, restore it, and check for equality.
*/
public void testSerialization() {
SerialDate d1 = SerialDate.createInstance(15, 4, 2000);
SerialDate d2 = null;
try {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
ObjectOutput out = new ObjectOutputStream(buffer);
out.writeObject(d1);
out.close();
ObjectInput in = new ObjectInputStream(new ByteArrayInputStream
(buffer.toByteArray()));
d2 = (SerialDate) in.readObject();
in.close();
}
catch (Exception e) {
System.out.println(e.toString());
}
assertEquals(d1, d2);
}

/**
* A test for bug report 1096282 (now fixed).
*/
public void test1096282() {
SerialDate d = SerialDate.createInstance(29, 2, 2004);
d = SerialDate.addYears(1, d);
SerialDate expected = SerialDate.createInstance(28, 2, 2005);
assertTrue(d.isOn(expected));

DODATEK B

299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322 }

/**
* Miscellaneous tests for the addMonths() method.
*/
public void testAddMonths() {
SerialDate d1 = SerialDate.createInstance(31, 5, 2004);
SerialDate d2 = SerialDate.addMonths(1, d1);
assertEquals(30, d2.getDayOfMonth());
assertEquals(6, d2.getMonth());
assertEquals(2004, d2.getYYYY());
SerialDate d3 = SerialDate.addMonths(2, d1);
assertEquals(31, d3.getDayOfMonth());
assertEquals(7, d3.getMonth());
assertEquals(2004, d3.getYYYY());
SerialDate d4 = SerialDate.addMonths(1, SerialDate.addMonths(1, d1));
assertEquals(30, d4.getDayOfMonth());
assertEquals(7, d4.getMonth());
assertEquals(2004, d4.getYYYY());
}

L I S T I N G B 3 . MonthConstants.java
1 /* ==================================================================
2 * JCommon : a free general purpose class library for the Java(tm) platform
3 * ==================================================================
4 *
5 * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
6 *
7 * Project Info: http://www.jfree.org/jcommon/index.html
8 *
9 * This library is free software; you can redistribute it and/or modify it
10 * under the terms of the GNU Lesser General Public License as published by
11 * the Free Software Foundation; either version 2.1 of the License, or
12 * (at your option) any later version.
13 *
14 * This library is distributed in the hope that it will be useful, but
15 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
16 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
17 * License for more details.
18 *
19 * You should have received a copy of the GNU Lesser General Public
20 * License along with this library; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
22 * USA.
23 *
24 * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
25 * in the United States and other countries.]
26 *
27 * ------------------28 * MonthConstants.java
29 * ------------------30 * (C) Copyright 2002, 2003, by Object Refinery Limited.
31 *
32 * Original Author: David Gilbert (for Object Refinery Limited);
33 * Contributor(s): -;

ORG.JFREE.DATE.SERIALDATE

379

34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91

380

*
* $Id: MonthConstants.java,v 1.3 2005/10/18 13:15:08 mungady Exp $
*
* Changes
* ------* 29-May-2002 : Version 1 (code moved from SerialDate class) (DG);
*
*/
package org.jfree.date;

/**
* Useful constants for months. Note that these are NOT equivalent to the
* constants defined by java.util.Calendar (where JANUARY=0 and DECEMBER=11).
* <P>
* Used by the SerialDate and RegularTimePeriod classes.
*
* @author David Gilbert
*/
public interface MonthConstants {

/** Constant for January. */


public static final int JANUARY = 1;

/** Constant for February. */


public static final int FEBRUARY = 2;

/** Constant for March. */


public static final int MARCH = 3;

/** Constant for April. */


public static final int APRIL = 4;

/** Constant for May. */


public static final int MAY = 5;

/** Constant for June. */


public static final int JUNE = 6;

/** Constant for July. */


public static final int JULY = 7;

/** Constant for August. */


public static final int AUGUST = 8;

/** Constant for September. */


public static final int SEPTEMBER = 9;

/** Constant for October. */


public static final int OCTOBER = 10;

/** Constant for November. */


public static final int NOVEMBER = 11;

/** Constant for December. */


public static final int DECEMBER = 12;
}

DODATEK B

L I S T I N G B 4 . BobsSerialDateTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

package org.jfree.date.junit;
import junit.framework.TestCase;
import org.jfree.date.*;
import static org.jfree.date.SerialDate.*;
import java.util.*;
public class BobsSerialDateTest extends TestCase {
public void testIsValidWeekdayCode() throws Exception {
for (int day = 1; day <= 7; day++)
assertTrue(isValidWeekdayCode(day));
assertFalse(isValidWeekdayCode(0));
assertFalse(isValidWeekdayCode(8));
}
public void testStringToWeekdayCode() throws Exception {
assertEquals(-1, stringToWeekdayCode("Hello"));
assertEquals(MONDAY, stringToWeekdayCode("Monday"));
assertEquals(MONDAY, stringToWeekdayCode("Mon"));

//todo assertEquals(MONDAY,stringToWeekdayCode("monday"));
// assertEquals(MONDAY,stringToWeekdayCode("MONDAY"));
// assertEquals(MONDAY, stringToWeekdayCode("mon"));
assertEquals(TUESDAY, stringToWeekdayCode("Tuesday"));
assertEquals(TUESDAY, stringToWeekdayCode("Tue"));

// assertEquals(TUESDAY,stringToWeekdayCode("tuesday"));
// assertEquals(TUESDAY,stringToWeekdayCode("TUESDAY"));
// assertEquals(TUESDAY, stringToWeekdayCode("tue"));
// assertEquals(TUESDAY, stringToWeekdayCode("tues"));
assertEquals(WEDNESDAY, stringToWeekdayCode("Wednesday"));
assertEquals(WEDNESDAY, stringToWeekdayCode("Wed"));

// assertEquals(WEDNESDAY,stringToWeekdayCode("wednesday"));
// assertEquals(WEDNESDAY,stringToWeekdayCode("WEDNESDAY"));
// assertEquals(WEDNESDAY, stringToWeekdayCode("wed"));
assertEquals(THURSDAY, stringToWeekdayCode("Thursday"));
assertEquals(THURSDAY, stringToWeekdayCode("Thu"));

// assertEquals(THURSDAY,stringToWeekdayCode("thursday"));
// assertEquals(THURSDAY,stringToWeekdayCode("THURSDAY"));
// assertEquals(THURSDAY, stringToWeekdayCode("thu"));
// assertEquals(THURSDAY, stringToWeekdayCode("thurs"));
assertEquals(FRIDAY, stringToWeekdayCode("Friday"));
assertEquals(FRIDAY, stringToWeekdayCode("Fri"));

// assertEquals(FRIDAY,stringToWeekdayCode("friday"));
// assertEquals(FRIDAY,stringToWeekdayCode("FRIDAY"));
// assertEquals(FRIDAY, stringToWeekdayCode("fri"));
assertEquals(SATURDAY, stringToWeekdayCode("Saturday"));
assertEquals(SATURDAY, stringToWeekdayCode("Sat"));

// assertEquals(SATURDAY,stringToWeekdayCode("saturday"));
// assertEquals(SATURDAY,stringToWeekdayCode("SATURDAY"));
// assertEquals(SATURDAY, stringToWeekdayCode("sat"));
assertEquals(SUNDAY, stringToWeekdayCode("Sunday"));
assertEquals(SUNDAY, stringToWeekdayCode("Sun"));

// assertEquals(SUNDAY,stringToWeekdayCode("sunday"));

ORG.JFREE.DATE.SERIALDATE

381

62 // assertEquals(SUNDAY,stringToWeekdayCode("SUNDAY"));
63 // assertEquals(SUNDAY, stringToWeekdayCode("sun"));
64
}
65
66
public void testWeekdayCodeToString() throws Exception {
67
assertEquals("Sunday", weekdayCodeToString(SUNDAY));
68
assertEquals("Monday", weekdayCodeToString(MONDAY));
69
assertEquals("Tuesday", weekdayCodeToString(TUESDAY));
70
assertEquals("Wednesday", weekdayCodeToString(WEDNESDAY));
71
assertEquals("Thursday", weekdayCodeToString(THURSDAY));
72
assertEquals("Friday", weekdayCodeToString(FRIDAY));
73
assertEquals("Saturday", weekdayCodeToString(SATURDAY));
74
}
75
76
public void testIsValidMonthCode() throws Exception {
77
for (int i = 1; i <= 12; i++)
78
assertTrue(isValidMonthCode(i));
79
assertFalse(isValidMonthCode(0));
80
assertFalse(isValidMonthCode(13));
81
}
82
83
public void testMonthToQuarter() throws Exception {
84
assertEquals(1, monthCodeToQuarter(JANUARY));
85
assertEquals(1, monthCodeToQuarter(FEBRUARY));
86
assertEquals(1, monthCodeToQuarter(MARCH));
87
assertEquals(2, monthCodeToQuarter(APRIL));
88
assertEquals(2, monthCodeToQuarter(MAY));
89
assertEquals(2, monthCodeToQuarter(JUNE));
90
assertEquals(3, monthCodeToQuarter(JULY));
91
assertEquals(3, monthCodeToQuarter(AUGUST));
92
assertEquals(3, monthCodeToQuarter(SEPTEMBER));
93
assertEquals(4, monthCodeToQuarter(OCTOBER));
94
assertEquals(4, monthCodeToQuarter(NOVEMBER));
95
assertEquals(4, monthCodeToQuarter(DECEMBER));
96
97
try {
98
monthCodeToQuarter(-1);
99
fail("Nieprawidowy kod miesica powinien wygenerowa wyjtek");
100
} catch (IllegalArgumentException e) {
101
}
102
}
103
104
public void testMonthCodeToString() throws Exception {
105
assertEquals("January", monthCodeToString(JANUARY));
106
assertEquals("February", monthCodeToString(FEBRUARY));
107
assertEquals("March", monthCodeToString(MARCH));
108
assertEquals("April", monthCodeToString(APRIL));
109
assertEquals("May", monthCodeToString(MAY));
110
assertEquals("June", monthCodeToString(JUNE));
111
assertEquals("July", monthCodeToString(JULY));
112
assertEquals("August", monthCodeToString(AUGUST));
113
assertEquals("September", monthCodeToString(SEPTEMBER));
114
assertEquals("October", monthCodeToString(OCTOBER));
115
assertEquals("November", monthCodeToString(NOVEMBER));
116
assertEquals("December", monthCodeToString(DECEMBER));
117
118
assertEquals("Jan", monthCodeToString(JANUARY, true));
119
assertEquals("Feb", monthCodeToString(FEBRUARY, true));
120
assertEquals("Mar", monthCodeToString(MARCH, true));
121
assertEquals("Apr", monthCodeToString(APRIL, true));
122
assertEquals("May", monthCodeToString(MAY, true));
123
assertEquals("Jun", monthCodeToString(JUNE, true));
124
assertEquals("Jul", monthCodeToString(JULY, true));
125
assertEquals("Aug", monthCodeToString(AUGUST, true));

382

DODATEK B

126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187

assertEquals("Sep",
assertEquals("Oct",
assertEquals("Nov",
assertEquals("Dec",

monthCodeToString(SEPTEMBER, true));
monthCodeToString(OCTOBER, true));
monthCodeToString(NOVEMBER, true));
monthCodeToString(DECEMBER, true));

try {
monthCodeToString(-1);
fail("Nieprawidowy kod miesica powinien wygenerowa wyjtek");
} catch (IllegalArgumentException e) {
}
}
public void testStringToMonthCode() throws Exception {
assertEquals(JANUARY,stringToMonthCode("1"));
assertEquals(FEBRUARY,stringToMonthCode("2"));
assertEquals(MARCH,stringToMonthCode("3"));
assertEquals(APRIL,stringToMonthCode("4"));
assertEquals(MAY,stringToMonthCode("5"));
assertEquals(JUNE,stringToMonthCode("6"));
assertEquals(JULY,stringToMonthCode("7"));
assertEquals(AUGUST,stringToMonthCode("8"));
assertEquals(SEPTEMBER,stringToMonthCode("9"));
assertEquals(OCTOBER,stringToMonthCode("10"));
assertEquals(NOVEMBER, stringToMonthCode("11"));
assertEquals(DECEMBER,stringToMonthCode("12"));

//todo assertEquals(-1, stringToMonthCode("0"));


// assertEquals(-1, stringToMonthCode("13"));
assertEquals(-1,stringToMonthCode("Cze"));
for (int m = 1; m <= 12; m++) {
assertEquals(m, stringToMonthCode(monthCodeToString(m, false)));
assertEquals(m, stringToMonthCode(monthCodeToString(m, true)));
}

// assertEquals(1,stringToMonthCode("jan"));
// assertEquals(2,stringToMonthCode("feb"));
// assertEquals(3,stringToMonthCode("mar"));
// assertEquals(4,stringToMonthCode("apr"));
// assertEquals(5,stringToMonthCode("may"));
// assertEquals(6,stringToMonthCode("jun"));
// assertEquals(7,stringToMonthCode("jul"));
// assertEquals(8,stringToMonthCode("aug"));
// assertEquals(9,stringToMonthCode("sep"));
// assertEquals(10,stringToMonthCode("oct"));
// assertEquals(11,stringToMonthCode("nov"));
// assertEquals(12,stringToMonthCode("dec"));
// assertEquals(1,stringToMonthCode("JAN"));
// assertEquals(2,stringToMonthCode("FEB"));
// assertEquals(3,stringToMonthCode("MAR"));
// assertEquals(4,stringToMonthCode("APR"));
// assertEquals(5,stringToMonthCode("MAY"));
// assertEquals(6,stringToMonthCode("JUN"));
// assertEquals(7,stringToMonthCode("JUL"));
// assertEquals(8,stringToMonthCode("AUG"));
// assertEquals(9,stringToMonthCode("SEP"));
// assertEquals(10,stringToMonthCode("OCT"));
// assertEquals(11,stringToMonthCode("NOV"));
// assertEquals(12,stringToMonthCode("DEC"));

ORG.JFREE.DATE.SERIALDATE

383

188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249

384

// assertEquals(1,stringToMonthCode("january"));
// assertEquals(2,stringToMonthCode("february"));
// assertEquals(3,stringToMonthCode("march"));
// assertEquals(4,stringToMonthCode("april"));
// assertEquals(5,stringToMonthCode("may"));
// assertEquals(6,stringToMonthCode("june"));
// assertEquals(7,stringToMonthCode("july"));
// assertEquals(8,stringToMonthCode("august"));
// assertEquals(9,stringToMonthCode("september"));
// assertEquals(10,stringToMonthCode("october"));
// assertEquals(11,stringToMonthCode("november"));
// assertEquals(12,stringToMonthCode("december"));
// assertEquals(1,stringToMonthCode("JANUARY"));
// assertEquals(2,stringToMonthCode("FEBRUARY"));
// assertEquals(3,stringToMonthCode("MAR"));
// assertEquals(4,stringToMonthCode("APRIL"));
// assertEquals(5,stringToMonthCode("MAY"));
// assertEquals(6,stringToMonthCode("JUNE"));
// assertEquals(7,stringToMonthCode("JULY"));
// assertEquals(8,stringToMonthCode("AUGUST"));
// assertEquals(9,stringToMonthCode("SEPTEMBER"));
// assertEquals(10,stringToMonthCode("OCTOBER"));
// assertEquals(11,stringToMonthCode("NOVEMBER"));
// assertEquals(12,stringToMonthCode("DECEMBER"));
}
public void testIsValidWeekInMonthCode() throws Exception {
for (int w = 0; w <= 4; w++) {
assertTrue(isValidWeekInMonthCode(w));
}
assertFalse(isValidWeekInMonthCode(5));
}
public void testIsLeapYear() throws Exception {
assertFalse(isLeapYear(1900));
assertFalse(isLeapYear(1901));
assertFalse(isLeapYear(1902));
assertFalse(isLeapYear(1903));
assertTrue(isLeapYear(1904));
assertTrue(isLeapYear(1908));
assertFalse(isLeapYear(1955));
assertTrue(isLeapYear(1964));
assertTrue(isLeapYear(1980));
assertTrue(isLeapYear(2000));
assertFalse(isLeapYear(2001));
assertFalse(isLeapYear(2100));
}
public void testLeapYearCount() throws Exception {
assertEquals(0, leapYearCount(1900));
assertEquals(0, leapYearCount(1901));
assertEquals(0, leapYearCount(1902));
assertEquals(0, leapYearCount(1903));
assertEquals(1, leapYearCount(1904));
assertEquals(1, leapYearCount(1905));
assertEquals(1, leapYearCount(1906));
assertEquals(1, leapYearCount(1907));
assertEquals(2, leapYearCount(1908));
assertEquals(24, leapYearCount(1999));
assertEquals(25, leapYearCount(2001));

DODATEK B

250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308

assertEquals(49, leapYearCount(2101));
assertEquals(73, leapYearCount(2201));
assertEquals(97, leapYearCount(2301));
assertEquals(122, leapYearCount(2401));
}
public void testLastDayOfMonth() throws Exception {
assertEquals(31, lastDayOfMonth(JANUARY, 1901));
assertEquals(28, lastDayOfMonth(FEBRUARY, 1901));
assertEquals(31, lastDayOfMonth(MARCH, 1901));
assertEquals(30, lastDayOfMonth(APRIL, 1901));
assertEquals(31, lastDayOfMonth(MAY, 1901));
assertEquals(30, lastDayOfMonth(JUNE, 1901));
assertEquals(31, lastDayOfMonth(JULY, 1901));
assertEquals(31, lastDayOfMonth(AUGUST, 1901));
assertEquals(30, lastDayOfMonth(SEPTEMBER, 1901));
assertEquals(31, lastDayOfMonth(OCTOBER, 1901));
assertEquals(30, lastDayOfMonth(NOVEMBER, 1901));
assertEquals(31, lastDayOfMonth(DECEMBER, 1901));
assertEquals(29, lastDayOfMonth(FEBRUARY, 1904));
}
public void testAddDays() throws Exception {
SerialDate newYears = d(1, JANUARY, 1900);
assertEquals(d(2, JANUARY, 1900), addDays(1, newYears));
assertEquals(d(1, FEBRUARY, 1900), addDays(31, newYears));
assertEquals(d(1, JANUARY, 1901), addDays(365, newYears));
assertEquals(d(31, DECEMBER, 1904), addDays(5 * 365, newYears));
}
private static SpreadsheetDate d(int day, int month, int year) {return new
SpreadsheetDate(day, month, year);}
public void testAddMonths() throws Exception {
assertEquals(d(1, FEBRUARY, 1900), addMonths(1, d(1, JANUARY, 1900)));
assertEquals(d(28, FEBRUARY, 1900), addMonths(1, d(31, JANUARY, 1900)));
assertEquals(d(28, FEBRUARY, 1900), addMonths(1, d(30, JANUARY, 1900)));
assertEquals(d(28, FEBRUARY, 1900), addMonths(1, d(29, JANUARY, 1900)));
assertEquals(d(28, FEBRUARY, 1900), addMonths(1, d(28, JANUARY, 1900)));
assertEquals(d(27, FEBRUARY, 1900), addMonths(1, d(27, JANUARY, 1900)));
assertEquals(d(30, JUNE, 1900), addMonths(5, d(31, JANUARY, 1900)));
assertEquals(d(30, JUNE, 1901), addMonths(17, d(31, JANUARY, 1900)));
assertEquals(d(29, FEBRUARY, 1904), addMonths(49, d(31, JANUARY, 1900)));
}
public void testAddYears() throws Exception {
assertEquals(d(1, JANUARY, 1901), addYears(1, d(1, JANUARY, 1900)));
assertEquals(d(28, FEBRUARY, 1905), addYears(1, d(29, FEBRUARY, 1904)));
assertEquals(d(28, FEBRUARY, 1905), addYears(1, d(28, FEBRUARY, 1904)));
assertEquals(d(28, FEBRUARY, 1904), addYears(1, d(28, FEBRUARY, 1903)));
}
public void testGetPreviousDayOfWeek() throws Exception {
assertEquals(d(24, FEBRUARY, 2006), getPreviousDayOfWeek(FRIDAY, d(1, MARCH,
2006)));
assertEquals(d(22, FEBRUARY, 2006), getPreviousDayOfWeek(WEDNESDAY, d(1, MARCH,
2006)));
assertEquals(d(29, FEBRUARY, 2004), getPreviousDayOfWeek(SUNDAY, d(3, MARCH,
2004)));
assertEquals(d(29, DECEMBER, 2004), getPreviousDayOfWeek(WEDNESDAY, d(5,
JANUARY, 2005)));

ORG.JFREE.DATE.SERIALDATE

385

309
310
try {
311
getPreviousDayOfWeek(-1, d(1, JANUARY, 2006));
312
fail("Nieprawidowy kod tygodnia powinien wygenerowa wyjtek");
313
} catch (IllegalArgumentException e) {
314
}
315
}
316
317
public void testGetFollowingDayOfWeek() throws Exception {
318 // assertEquals(d(1, JANUARY, 2005),getFollowingDayOfWeek(SATURDAY, d(25, DECEMBER, 2004)));
319
assertEquals(d(1, JANUARY, 2005), getFollowingDayOfWeek(SATURDAY, d(26, DECEMBER,
2004)));
320
assertEquals(d(3, MARCH, 2004), getFollowingDayOfWeek(WEDNESDAY, d(28, FEBRUARY,
2004)));
321
322
try {
323
getFollowingDayOfWeek(-1, d(1, JANUARY, 2006));
324
fail("Nieprawidowy kod tygodnia powinien wygenerowa wyjtek");
325
} catch (IllegalArgumentException e) {
326
}
327
}
328
329
public void testGetNearestDayOfWeek() throws Exception {
330
assertEquals(d(16, APRIL, 2006), getNearestDayOfWeek(SUNDAY, d(16, APRIL, 2006)));
331
assertEquals(d(16, APRIL, 2006), getNearestDayOfWeek(SUNDAY, d(17, APRIL, 2006)));
332
assertEquals(d(16, APRIL, 2006), getNearestDayOfWeek(SUNDAY, d(18, APRIL, 2006)));
333
assertEquals(d(16, APRIL, 2006), getNearestDayOfWeek(SUNDAY, d(19, APRIL, 2006)));
334
assertEquals(d(23, APRIL, 2006), getNearestDayOfWeek(SUNDAY, d(20, APRIL, 2006)));
335
assertEquals(d(23, APRIL, 2006), getNearestDayOfWeek(SUNDAY, d(21, APRIL, 2006)));
336
assertEquals(d(23, APRIL, 2006), getNearestDayOfWeek(SUNDAY, d(22, APRIL, 2006)));
337
338 //todo assertEquals(d(17, APRIL, 2006), getNearestDayOfWeek(MONDAY, d(16, APRIL, 2006)));
339
assertEquals(d(17, APRIL, 2006), getNearestDayOfWeek(MONDAY, d(17, APRIL, 2006)));
340
assertEquals(d(17, APRIL, 2006), getNearestDayOfWeek(MONDAY, d(18, APRIL, 2006)));
341
assertEquals(d(17, APRIL, 2006), getNearestDayOfWeek(MONDAY, d(19, APRIL, 2006)));
342
assertEquals(d(17, APRIL, 2006), getNearestDayOfWeek(MONDAY, d(20, APRIL, 2006)));
343
assertEquals(d(24, APRIL, 2006), getNearestDayOfWeek(MONDAY, d(21, APRIL, 2006)));
344
assertEquals(d(24, APRIL, 2006), getNearestDayOfWeek(MONDAY, d(22, APRIL, 2006)));
345
346 // assertEquals(d(18, APRIL, 2006), getNearestDayOfWeek(TUESDAY, d(16, APRIL, 2006)));
347 // assertEquals(d(18, APRIL, 2006), getNearestDayOfWeek(TUESDAY, d(17, APRIL, 2006)));
348
assertEquals(d(18, APRIL, 2006), getNearestDayOfWeek(TUESDAY, d(18, APRIL, 2006)));
349
assertEquals(d(18, APRIL, 2006), getNearestDayOfWeek(TUESDAY, d(19, APRIL, 2006)));
350
assertEquals(d(18, APRIL, 2006), getNearestDayOfWeek(TUESDAY, d(20, APRIL, 2006)));
351
assertEquals(d(18, APRIL, 2006), getNearestDayOfWeek(TUESDAY, d(21, APRIL, 2006)));
352
assertEquals(d(25, APRIL, 2006), getNearestDayOfWeek(TUESDAY, d(22, APRIL, 2006)));
353
354 // assertEquals(d(19, APRIL, 2006), getNearestDayOfWeek(WEDNESDAY, d(16, APRIL, 2006)));
355 // assertEquals(d(19, APRIL, 2006), getNearestDayOfWeek(WEDNESDAY, d(17, APRIL, 2006)));
356 // assertEquals(d(19, APRIL, 2006), getNearestDayOfWeek(WEDNESDAY, d(18, APRIL, 2006)));
357
assertEquals(d(19, APRIL, 2006), getNearestDayOfWeek(WEDNESDAY, d(19, APRIL, 2006)));
358
assertEquals(d(19, APRIL, 2006), getNearestDayOfWeek(WEDNESDAY, d(20, APRIL, 2006)));
359
assertEquals(d(19, APRIL, 2006), getNearestDayOfWeek(WEDNESDAY, d(21, APRIL, 2006)));
360
assertEquals(d(19, APRIL, 2006), getNearestDayOfWeek(WEDNESDAY, d(22, APRIL, 2006)));
361
362 // assertEquals(d(13, APRIL, 2006), getNearestDayOfWeek(THURSDAY, d(16, APRIL, 2006)));
363 // assertEquals(d(20, APRIL, 2006), getNearestDayOfWeek(THURSDAY, d(17, APRIL, 2006)));
364 // assertEquals(d(20, APRIL, 2006), getNearestDayOfWeek(THURSDAY, d(18, APRIL, 2006)));
365 // assertEquals(d(20, APRIL, 2006), getNearestDayOfWeek(THURSDAY, d(19, APRIL, 2006)));
366
assertEquals(d(20, APRIL, 2006), getNearestDayOfWeek(THURSDAY, d(20, APRIL, 2006)));
367
assertEquals(d(20, APRIL, 2006), getNearestDayOfWeek(THURSDAY, d(21, APRIL, 2006)));
368
assertEquals(d(20, APRIL, 2006), getNearestDayOfWeek(THURSDAY, d(22, APRIL, 2006)));
369

386

DODATEK B

370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432

// assertEquals(d(14, APRIL, 2006), getNearestDayOfWeek(FRIDAY, d(16, APRIL, 2006)));


// assertEquals(d(14, APRIL, 2006), getNearestDayOfWeek(FRIDAY, d(17, APRIL, 2006)));
// assertEquals(d(21, APRIL, 2006), getNearestDayOfWeek(FRIDAY, d(18, APRIL, 2006)));
// assertEquals(d(21, APRIL, 2006), getNearestDayOfWeek(FRIDAY, d(19, APRIL, 2006)));
// assertEquals(d(21, APRIL, 2006), getNearestDayOfWeek(FRIDAY, d(20, APRIL, 2006)));
assertEquals(d(21, APRIL, 2006), getNearestDayOfWeek(FRIDAY, d(21, APRIL, 2006)));
assertEquals(d(21, APRIL, 2006), getNearestDayOfWeek(FRIDAY, d(22, APRIL, 2006)));

// assertEquals(d(15, APRIL, 2006), getNearestDayOfWeek(SATURDAY, d(16, APRIL, 2006)));


// assertEquals(d(15, APRIL, 2006), getNearestDayOfWeek(SATURDAY, d(17, APRIL, 2006)));
// assertEquals(d(15, APRIL, 2006), getNearestDayOfWeek(SATURDAY, d(18, APRIL, 2006)));
// assertEquals(d(22, APRIL, 2006), getNearestDayOfWeek(SATURDAY, d(19, APRIL, 2006)));
// assertEquals(d(22, APRIL, 2006), getNearestDayOfWeek(SATURDAY, d(20, APRIL, 2006)));
// assertEquals(d(22, APRIL, 2006), getNearestDayOfWeek(SATURDAY, d(21, APRIL, 2006)));
assertEquals(d(22, APRIL, 2006), getNearestDayOfWeek(SATURDAY, d(22, APRIL, 2006)));
try {
getNearestDayOfWeek(-1, d(1, JANUARY, 2006));
fail("Nieprawidowy kod tygodnia powinien wygenerowa wyjtek");
} catch (IllegalArgumentException e) {
}
}
public void testEndOfCurrentMonth() throws Exception {
SerialDate d = SerialDate.createInstance(2);
assertEquals(d(31, JANUARY, 2006), d.getEndOfCurrentMonth(d(1, JANUARY, 2006)));
assertEquals(d(28, FEBRUARY, 2006), d.getEndOfCurrentMonth(d(1, FEBRUARY, 2006)));
assertEquals(d(31, MARCH, 2006), d.getEndOfCurrentMonth(d(1, MARCH, 2006)));
assertEquals(d(30, APRIL, 2006), d.getEndOfCurrentMonth(d(1, APRIL, 2006)));
assertEquals(d(31, MAY, 2006), d.getEndOfCurrentMonth(d(1, MAY, 2006)));
assertEquals(d(30, JUNE, 2006), d.getEndOfCurrentMonth(d(1, JUNE, 2006)));
assertEquals(d(31, JULY, 2006), d.getEndOfCurrentMonth(d(1, JULY, 2006)));
assertEquals(d(31, AUGUST, 2006), d.getEndOfCurrentMonth(d(1, AUGUST, 2006)));
assertEquals(d(30, SEPTEMBER, 2006), d.getEndOfCurrentMonth(d(1, SEPTEMBER, 2006)));
assertEquals(d(31, OCTOBER, 2006), d.getEndOfCurrentMonth(d(1, OCTOBER, 2006)));
assertEquals(d(30, NOVEMBER, 2006), d.getEndOfCurrentMonth(d(1, NOVEMBER, 2006)));
assertEquals(d(31, DECEMBER, 2006), d.getEndOfCurrentMonth(d(1, DECEMBER, 2006)));
assertEquals(d(29, FEBRUARY, 2008), d.getEndOfCurrentMonth(d(1, FEBRUARY, 2008)));
}
public void testWeekInMonthToString() throws Exception {
assertEquals("First",weekInMonthToString(FIRST_WEEK_IN_MONTH));
assertEquals("Second",weekInMonthToString(SECOND_WEEK_IN_MONTH));
assertEquals("Third",weekInMonthToString(THIRD_WEEK_IN_MONTH));
assertEquals("Fourth",weekInMonthToString(FOURTH_WEEK_IN_MONTH));
assertEquals("Last",weekInMonthToString(LAST_WEEK_IN_MONTH));

//todo try {
// weekInMonthToString(-1);
// fail("Nieprawidowy kod tygodnia powinien wygenerowa wyjtek");
// } catch (IllegalArgumentException e) {
// }
}
public void testRelativeToString() throws Exception {
assertEquals("Preceding",relativeToString(PRECEDING));
assertEquals("Nearest",relativeToString(NEAREST));
assertEquals("Following",relativeToString(FOLLOWING));

//todo try {
// relativeToString(-1000);
// fail("Nieprawidowy kod wzgldny powinien wygenerowa wyjtek");
// } catch (IllegalArgumentException e) {

ORG.JFREE.DATE.SERIALDATE

387

433 // }
434
}
435
436
public void testCreateInstanceFromDDMMYYY() throws Exception {
437
SerialDate date = createInstance(1, JANUARY, 1900);
438
assertEquals(1,date.getDayOfMonth());
439
assertEquals(JANUARY,date.getMonth());
440
assertEquals(1900,date.getYYYY());
441
assertEquals(2,date.toSerial());
442
}
443
444
public void testCreateInstanceFromSerial() throws Exception {
445
assertEquals(d(1, JANUARY, 1900),createInstance(2));
446
assertEquals(d(1, JANUARY, 1901), createInstance(367));
447
}
448
449
public void testCreateInstanceFromJavaDate() throws Exception {
450
assertEquals(d(1, JANUARY, 1900),createInstance(new GregorianCalendar
(1900,0,1).getTime()));
451
assertEquals(d(1, JANUARY, 2006),createInstance(new GregorianCalendar
(2006,0,1).getTime()));
452
}
453
454
public static void main(String[] args) {
455
junit.textui.TestRunner.run(BobsSerialDateTest.class);
456
}
457 }

L I S T I N G B 5 . SpreadsheetDate.java
1 /* ==================================================================
2 * JCommon : a free general purpose class library for the Java(tm) platform
3 * =================================================================
4 *
5 * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
6 *
7 * Project Info: http://www.jfree.org/jcommon/index.html
8 *
9 * This library is free software; you can redistribute it and/or modify it
10 * under the terms of the GNU Lesser General Public License as published by
11 * the Free Software Foundation; either version 2.1 of the License, or
12 * (at your option) any later version.
13 *
14 * This library is distributed in the hope that it will be useful, but
15 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
16 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
17 * License for more details.
18 *
19 * You should have received a copy of the GNU Lesser General Public
20 * License along with this library; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
22 * USA.
23 *
24 * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
25 * in the United States and other countries.]
26 *
27 * -------------------28 * SpreadsheetDate.java
29 * -------------------30 * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
31 *
32 * Original Author: David Gilbert (for Object Refinery Limited);

388

DODATEK B

33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93

* Contributor(s): -;
*
* $Id: SpreadsheetDate.java,v 1.8 2005/11/03 09:25:39 mungady Exp $
*
* Changes
* ------* 11-Oct-2001 : Version 1 (DG);
* 05-Nov-2001 : Added getDescription() and setDescription() methods (DG);
* 12-Nov-2001 : Changed name from ExcelDate.java to SpreadsheetDate.java (DG);
*
Fixed a bug in calculating day, month and year from serial
*
number (DG);
* 24-Jan-2002 : Fixed a bug in calculating the serial number from the day,
*
month and year. Thanks to Trevor Hills for the report (DG);
* 29-May-2002 : Added equals(Object) method (SourceForge ID 558850) (DG);
* 03-Oct-2002 : Fixed errors reported by Checkstyle (DG);
* 13-Mar-2003 : Implemented Serializable (DG);
* 04-Sep-2003 : Completed isInRange() methods (DG);
* 05-Sep-2003 : Implemented Comparable (DG);
* 21-Oct-2003 : Added hashCode() method (DG);
*
*/
package org.jfree.date;
import java.util.Calendar;
import java.util.Date;

/**
* Represents a date using an integer, in a similar fashion to the
* implementation in Microsoft Excel. The range of dates supported is
* 1-Jan-1900 to 31-Dec-9999.
* <P>
* Be aware that there is a deliberate bug in Excel that recognises the year
* 1900 as a leap year when in fact it is not a leap year. You can find more
* information on the Microsoft website in article Q181370:
* <P>
* http://support.microsoft.com/support/kb/articles/Q181/3/70.asp
* <P>
* Excel uses the convention that 1-Jan-1900 = 1. This class uses the
* convention 1-Jan-1900 = 2.
* The result is that the day number in this class will be different to the
* Excel figure for January and February 1900...but then Excel adds in an extra
* day (29-Feb-1900 which does not actually exist!) and from that point forward
* the day numbers will match.
*
* @author David Gilbert
*/
public class SpreadsheetDate extends SerialDate {

/** For serialization. */


private static final long serialVersionUID = -2039586705374454461L;

/**
* The day number (1-Jan-1900 = 2, 2-Jan-1900 = 3, ..., 31-Dec-9999 =
* 2958465).
*/
private int serial;

/** The day of the month (1 to 28, 29, 30 or 31 depending on the month). */
private int day;

ORG.JFREE.DATE.SERIALDATE

389

94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156

390

/** The month of the year (1 to 12). */


private int month;

/** The year (1900 to 9999). */


private int year;

/** An optional description for the date. */


private String description;

/**
* Creates a new date instance.
*
* @param day the day (in the range 1 to 28/29/30/31).
* @param month the month (in the range 1 to 12).
* @param year the year (in the range 1900 to 9999).
*/
public SpreadsheetDate(final int day, final int month, final int year) {
if ((year >= 1900) && (year <= 9999)) {
this.year = year;
}
else {
throw new IllegalArgumentException(
"The 'year' argument must be in range 1900 to 9999."
);
}
if ((month >= MonthConstants.JANUARY)
&& (month <= MonthConstants.DECEMBER)) {
this.month = month;
}
else {
throw new IllegalArgumentException(
"The 'month' argument must be in the range 1 to 12."
);
}
if ((day >= 1) && (day <= SerialDate.lastDayOfMonth(month, year))) {
this.day = day;
}
else {
throw new IllegalArgumentException("Invalid 'day' argument.");
}

// the serial number needs to be synchronised with the day-month-year


this.serial = calcSerial(day, month, year);
this.description = null;
}

/**
* Standard constructor - creates a new date object representing the
* specified day number (which should be in the range 2 to 2958465.
*
* @param serial the serial number for the day (range: 2 to 2958465).
*/
public SpreadsheetDate(final int serial) {
if ((serial >= SERIAL_LOWER_BOUND) && (serial <= SERIAL_UPPER_BOUND)) {
this.serial = serial;
}
else {

DODATEK B

157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218

throw new IllegalArgumentException(


"SpreadsheetDate: Serial must be in range 2 to 2958465.");
}

// the day-month-year needs to be synchronised with the serial number


calcDayMonthYear();
}

/**
* Returns the description that is attached to the date. It is not
* required that a date have a description, but for some applications it
* is useful.
*
* @return The description that is attached to the date.
*/
public String getDescription() {
return this.description;
}

/**
* Sets the description for the date.
*
* @param description the description for this date (<code>null</code>
*
permitted).
*/
public void setDescription(final String description) {
this.description = description;
}

/**
* Returns the serial number for the date, where 1 January 1900 = 2
* (this corresponds, almost, to the numbering system used in Microsoft
* Excel for Windows and Lotus 1-2-3).
*
* @return The serial number of this date.
*/
public int toSerial() {
return this.serial;
}

/**
* Returns a <code>java.util.Date</code> equivalent to this date.
*
* @return The date.
*/
public Date toDate() {
final Calendar calendar = Calendar.getInstance();
calendar.set(getYYYY(), getMonth() - 1, getDayOfMonth(), 0, 0, 0);
return calendar.getTime();
}

/**
* Returns the year (assume a valid range of 1900 to 9999).
*
* @return The year.
*/
public int getYYYY() {
return this.year;
}

/**

ORG.JFREE.DATE.SERIALDATE

391

219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280

392

* Returns the month (January = 1, February = 2, March = 3).


*
* @return The month of the year.
*/
public int getMonth() {
return this.month;
}

/**
* Returns the day of the month.
*
* @return The day of the month.
*/
public int getDayOfMonth() {
return this.day;
}

/**
* Returns a code representing the day of the week.
* <P>
* The codes are defined in the {@link SerialDate} class as:
* <code>SUNDAY</code>, <code>MONDAY</code>, <code>TUESDAY</code>,
* <code>WEDNESDAY</code>, <code>THURSDAY</code>, <code>FRIDAY</code>, and
* <code>SATURDAY</code>.
*
* @return A code representing the day of the week.
*/
public int getDayOfWeek() {
return (this.serial + 6) % 7 + 1;
}

/**
* Tests the equality of this date with an arbitrary object.
* <P>
* This method will return true ONLY if the object is an instance of the
* {@link SerialDate} base class, and it represents the same day as this
* {@link SpreadsheetDate}.
*
* @param object the object to compare (<code>null</code> permitted).
*
* @return A boolean.
*/
public boolean equals(final Object object) {
if (object instanceof SerialDate) {
final SerialDate s = (SerialDate) object;
return (s.toSerial() == this.toSerial());
}
else {
return false;
}
}

/**
* Returns a hash code for this object instance.
*
* @return A hash code.
*/
public int hashCode() {
return toSerial();
}

DODATEK B

281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341

/**
* Returns the difference (in days) between this date and the specified
* 'other' date.
*
* @param other the date being compared to.
*
* @return The difference (in days) between this date and the specified
*
'other' date.
*/
public int compare(final SerialDate other) {
return this.serial - other.toSerial();
}

/**
* Implements the method required by the Comparable interface.
*
* @param other the other object (usually another SerialDate).
*
* @return A negative integer, zero, or a positive integer as this object
*
is less than, equal to, or greater than the specified object.
*/
public int compareTo(final Object other) {
return compare((SerialDate) other);
}

/**
* Returns true if this SerialDate represents the same date as the
* specified SerialDate.
*
* @param other the date being compared to.
*
* @return <code>true</code> if this SerialDate represents the same date as
*
the specified SerialDate.
*/
public boolean isOn(final SerialDate other) {
return (this.serial == other.toSerial());
}

/**
* Returns true if this SerialDate represents an earlier date compared to
* the specified SerialDate.
*
* @param other the date being compared to.
*
* @return <code>true</code> if this SerialDate represents an earlier date
*
compared to the specified SerialDate.
*/
public boolean isBefore(final SerialDate other) {
return (this.serial < other.toSerial());
}

/**
* Returns true if this SerialDate represents the same date as the
* specified SerialDate.
*
* @param other the date being compared to.
*
* @return <code>true</code> if this SerialDate represents the same date
*
as the specified SerialDate.
*/

ORG.JFREE.DATE.SERIALDATE

393

342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402

394

public boolean isOnOrBefore(final SerialDate other) {


return (this.serial <= other.toSerial());
}

/**
* Returns true if this SerialDate represents the same date as the
* specified SerialDate.
*
* @param other the date being compared to.
*
* @return <code>true</code> if this SerialDate represents the same date
*
as the specified SerialDate.
*/
public boolean isAfter(final SerialDate other) {
return (this.serial > other.toSerial());
}

/**
* Returns true if this SerialDate represents the same date as the
* specified SerialDate.
*
* @param other the date being compared to.
*
* @return <code>true</code> if this SerialDate represents the same date as
*
the specified SerialDate.
*/
public boolean isOnOrAfter(final SerialDate other) {
return (this.serial >= other.toSerial());
}

/**
* Returns <code>true</code> if this {@link SerialDate} is within the
* specified range (INCLUSIVE). The date order of d1 and d2 is not
* important.
*
* @param d1 a boundary date for the range.
* @param d2 the other boundary date for the range.
*
* @return A boolean.
*/
public boolean isInRange(final SerialDate d1, final SerialDate d2) {
return isInRange(d1, d2, SerialDate.INCLUDE_BOTH);
}

/**
* Returns true if this SerialDate is within the specified range (caller
* specifies whether or not the end-points are included). The order of d1
* and d2 is not important.
*
* @param d1 one boundary date for the range.
* @param d2 a second boundary date for the range.
* @param include a code that controls whether or not the start and end
*
dates are included in the range.
*
* @return <code>true</code> if this SerialDate is within the specified
*
range.
*/
public boolean isInRange(final SerialDate d1, final SerialDate d2,
final int include) {
final int s1 = d1.toSerial();
final int s2 = d2.toSerial();

DODATEK B

403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465

final int start = Math.min(s1, s2);


final int end = Math.max(s1, s2);
final int s = toSerial();
if (include == SerialDate.INCLUDE_BOTH) {
return (s >= start && s <= end);
}
else if (include == SerialDate.INCLUDE_FIRST) {
return (s >= start && s < end);
}
else if (include == SerialDate.INCLUDE_SECOND) {
return (s > start && s <= end);
}
else {
return (s > start && s < end);
}
}

/**
* Calculate the serial number from the day, month and year.
* <P>
* 1-Jan-1900 = 2.
*
* @param d the day.
* @param m the month.
* @param y the year.
*
* @return the serial number from the day, month and year.
*/
private int calcSerial(final int d, final int m, final int y) {
final int yy = ((y - 1900) * 365) + SerialDate.leapYearCount(y - 1);
int mm = SerialDate.AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH[m];
if (m > MonthConstants.FEBRUARY) {
if (SerialDate.isLeapYear(y)) {
mm = mm + 1;
}
}
final int dd = d;
return yy + mm + dd + 1;
}

/**
* Calculate the day, month and year from the serial number.
*/
private void calcDayMonthYear() {

// get the year from the serial date


final int days = this.serial - SERIAL_LOWER_BOUND;

// overestimated because we ignored leap days


final int overestimatedYYYY = 1900 + (days / 365);
final int leaps = SerialDate.leapYearCount(overestimatedYYYY);
final int nonleapdays = days - leaps;

// underestimated because we overestimated years


int underestimatedYYYY = 1900 + (nonleapdays / 365);
if (underestimatedYYYY == overestimatedYYYY) {
this.year = underestimatedYYYY;
}
else {
int ss1 = calcSerial(1, 1, underestimatedYYYY);
while (ss1 <= this.serial) {
underestimatedYYYY = underestimatedYYYY + 1;
ss1 = calcSerial(1, 1, underestimatedYYYY);

ORG.JFREE.DATE.SERIALDATE

395

466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495 }

}
this.year = underestimatedYYYY - 1;
}
final int ss2 = calcSerial(1, 1, this.year);
int[] daysToEndOfPrecedingMonth
= AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH;
if (isLeapYear(this.year)) {
daysToEndOfPrecedingMonth
= LEAP_YEAR_AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH;
}

// get the month from the serial date


int mm = 1;
int sss = ss2 + daysToEndOfPrecedingMonth[mm] - 1;
while (sss < this.serial) {
mm = mm + 1;
sss = ss2 + daysToEndOfPrecedingMonth[mm] - 1;
}
this.month = mm - 1;

// what's left is d(+1);


this.day = this.serial - ss2
- daysToEndOfPrecedingMonth[this.month] + 1;
}

L I S T I N G B 6 . RelativeDayOfWeekRule.java
1 /* ==================================================================
2 * JCommon : a free general purpose class library for the Java(tm) platform
3 * =================================================================
4 *
5 * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
6 *
7 * Project Info: http://www.jfree.org/jcommon/index.html
8 *
9 * This library is free software; you can redistribute it and/or modify it
10 * under the terms of the GNU Lesser General Public License as published by
11 * the Free Software Foundation; either version 2.1 of the License, or
12 * (at your option) any later version.
13 *
14 * This library is distributed in the hope that it will be useful, but
15 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
16 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
17 * License for more details.
18 *
19 * You should have received a copy of the GNU Lesser General Public
20 * License along with this library; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
22 * USA.
23 *
24 * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
25 * in the United States and other countries.]
26 *
27 * -------------------------28 * RelativeDayOfWeekRule.java
29 * --------------------------

396

DODATEK B

30 * (C) Copyright 2000-2003, by Object Refinery Limited and Contributors.


31 *
32 * Original Author: David Gilbert (for Object Refinery Limited);
33 * Contributor(s): -;
34 *
35 * $Id: RelativeDayOfWeekRule.java,v 1.5 2005/11/03 09:24:45 mungady Exp $
36 *
37 * Changes (from 26-Oct-2001)
38 * -------------------------39 * 26-Oct-2001 : Changed package to com.jrefinery.date.*;
40 * 03-Oct-2002 : Fixed errors reported by Checkstyle (DG);
41 *
42 */
43
44 package org.jfree.date;
45
46 /**
47 * An annual date rule that returns a date for each year based on (a) a
48 * reference rule; (b) a day of the week; and (c) a selection parameter
49 * (SerialDate.PRECEDING, SerialDate.NEAREST, SerialDate.FOLLOWING).
50 * <P>
51 * For example, Good Friday can be specified as 'the Friday PRECEDING Easter
52 * Sunday'.
53 *
54 * @author David Gilbert
55 */
56 public class RelativeDayOfWeekRule extends AnnualDateRule {
57
58
/** A reference to the annual date rule on which this rule is based. */
59
private AnnualDateRule subrule;
60
61
/**
62
* The day of the week (SerialDate.MONDAY, SerialDate.TUESDAY, and so on).
63
*/
64
private int dayOfWeek;
65
66
/** Specifies which day of the week (PRECEDING, NEAREST or FOLLOWING). */
67
private int relative;
68
69
/**
70
* Default constructor - builds a rule for the Monday following 1 January.
71
*/
72
public RelativeDayOfWeekRule() {
73
this(new DayAndMonthRule(), SerialDate.MONDAY, SerialDate.FOLLOWING);
74
}
75
76
/**
77
* Standard constructor - builds rule based on the supplied sub-rule.
78
*
79
* @param subrule the rule that determines the reference date.
80
* @param dayOfWeek the day-of-the-week relative to the reference date.
81
* @param relative indicates *which* day-of-the-week (preceding, nearest
82
*
or following).
83
*/
84
public RelativeDayOfWeekRule(final AnnualDateRule subrule,
85
final int dayOfWeek, final int relative) {
86
this.subrule = subrule;
87
this.dayOfWeek = dayOfWeek;
88
this.relative = relative;
89
}
90

ORG.JFREE.DATE.SERIALDATE

397

91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151

398

/**
* Returns the sub-rule (also called the reference rule).
*
* @return The annual date rule that determines the reference date for this
*
rule.
*/
public AnnualDateRule getSubrule() {
return this.subrule;
}

/**
* Sets the sub-rule.
*
* @param subrule the annual date rule that determines the reference date
*
for this rule.
*/
public void setSubrule(final AnnualDateRule subrule) {
this.subrule = subrule;
}

/**
* Returns the day-of-the-week for this rule.
*
* @return the day-of-the-week for this rule.
*/
public int getDayOfWeek() {
return this.dayOfWeek;
}

/**
* Sets the day-of-the-week for this rule.
*
* @param dayOfWeek the day-of-the-week (SerialDate.MONDAY,
*
SerialDate.TUESDAY, and so on).
*/
public void setDayOfWeek(final int dayOfWeek) {
this.dayOfWeek = dayOfWeek;
}

/**
* Returns the 'relative' attribute, that determines *which*
* day-of-the-week we are interested in (SerialDate.PRECEDING,
* SerialDate.NEAREST or SerialDate.FOLLOWING).
*
* @return The 'relative' attribute.
*/
public int getRelative() {
return this.relative;
}

/**
* Sets the 'relative' attribute (SerialDate.PRECEDING, SerialDate.NEAREST,
* SerialDate.FOLLOWING).
*
* @param relative determines *which* day-of-the-week is selected by this
*
rule.
*/
public void setRelative(final int relative) {
this.relative = relative;
}

DODATEK B

152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209 }

/**
* Creates a clone of this rule.
*
* @return a clone of this rule.
*
* @throws CloneNotSupportedException this should never happen.
*/
public Object clone() throws CloneNotSupportedException {
final RelativeDayOfWeekRule duplicate
= (RelativeDayOfWeekRule) super.clone();
duplicate.subrule = (AnnualDateRule) duplicate.getSubrule().clone();
return duplicate;
}

/**
* Returns the date generated by this rule, for the specified year.
*
* @param year the year (1900 &lt;= year &lt;= 9999).
*
* @return The date generated by the rule for the given year (possibly
*
<code>null</code>).
*/
public SerialDate getDate(final int year) {

// check argument
if ((year < SerialDate.MINIMUM_YEAR_SUPPORTED)
|| (year > SerialDate.MAXIMUM_YEAR_SUPPORTED)) {
throw new IllegalArgumentException(
"RelativeDayOfWeekRule.getDate(): year outside valid range.");
}

// calculate the date


SerialDate result = null;
final SerialDate base = this.subrule.getDate(year);
if (base != null) {
switch (this.relative) {
case(SerialDate.PRECEDING):
result = SerialDate.getPreviousDayOfWeek(this.dayOfWeek,
base);
break;
case(SerialDate.NEAREST):
result = SerialDate.getNearestDayOfWeek(this.dayOfWeek,
base);
break;
case(SerialDate.FOLLOWING):
result = SerialDate.getFollowingDayOfWeek(this.dayOfWeek,
base);
break;
default:
break;
}
}
return result;
}

ORG.JFREE.DATE.SERIALDATE

399

L I S T I N G B 7 . DayDate.java (ostateczny)
1 /* =====================================================================
2 * JCommon : a free general purpose class library for the Java(tm) platform
3 * ======================================================================
4 *
5 * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
...
36 */
37 package org.jfree.date;
38
39 import java.io.Serializable;
40 import java.util.*;
41
42 /**
43 * Klasa abstrakcyjna, ktra reprezentuje niezmienne daty z dokadnoci
44 * do jednego dnia. Implementacja odwzorowuje kady dzie na liczb integer
45 * reprezentujc kolejny dzie od pewnego dnia pocztkowego.
46 *
47 * Dlaczego nie skorzysta po prostu z java.util.Date? Robimy to, gdy ma to sens.
48 * Czasami java.util.Date moe by *zbyt* dokadna - reprezentuje czas
49 * z dokadnoci do 1/1000 sekundy (a sama data jest zalena od strefy czasowej).
50 * Czasami chcemy reprezentowa okrelony dzie (np. 21 stycznia 2015)
51 * bez zajmowania si czasem w tym dniu, stref czasow
52 * czy te innymi parametrami. Do tego wanie wykorzystujemy DayDate.
53 *
54 * Do utworzenia obiektu bdziemy uywa DayDateFactory.makeDate.
55 *
56 * @author David Gilbert
57 * @author Robert C. Martin znacznie przebudowa kod.
58 */
59
60 public abstract class DayDate implements Comparable, Serializable {
61
public abstract int getOrdinalDay();
62
public abstract int getYear();
63
public abstract Month getMonth();
64
public abstract int getDayOfMonth();
65
66
protected abstract Day getDayOfWeekForOrdinalZero();
67
68
public DayDate plusDays(int days) {
69
return DayDateFactory.makeDate(getOrdinalDay() + days);
70
}
71
72
public DayDate plusMonths(int months) {
73
int thisMonthAsOrdinal = getMonth().toInt() - Month.JANUARY.toInt();
74
int thisMonthAndYearAsOrdinal = 12 * getYear() + thisMonthAsOrdinal;
75
int resultMonthAndYearAsOrdinal = thisMonthAndYearAsOrdinal + months;
76
int resultYear = resultMonthAndYearAsOrdinal / 12;
77
int resultMonthAsOrdinal = resultMonthAndYearAsOrdinal % 12 + Month.JANUARY.toInt();
78
Month resultMonth = Month.fromInt(resultMonthAsOrdinal);
79
int resultDay = correctLastDayOfMonth(getDayOfMonth(), resultMonth, resultYear);
80
return DayDateFactory.makeDate(resultDay, resultMonth, resultYear);
81
}
82
83
public DayDate plusYears(int years) {
84
int resultYear = getYear() + years;
85
int resultDay = correctLastDayOfMonth(getDayOfMonth(), getMonth(), resultYear);
86
return DayDateFactory.makeDate(resultDay, getMonth(), resultYear);
87
}
88
89
private int correctLastDayOfMonth(int day, Month month, int year) {

400

DODATEK B

90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153

int lastDayOfMonth = DateUtil.lastDayOfMonth(month, year);


if (day > lastDayOfMonth)
day = lastDayOfMonth;
return day;
}
public DayDate getPreviousDayOfWeek(Day targetDayOfWeek) {
int offsetToTarget = targetDayOfWeek.toInt() - getDayOfWeek().toInt();
if (offsetToTarget >= 0)
offsetToTarget -= 7;
return plusDays(offsetToTarget);
}
public DayDate getFollowingDayOfWeek(Day targetDayOfWeek) {
int offsetToTarget = targetDayOfWeek.toInt() - getDayOfWeek().toInt();
if (offsetToTarget <= 0)
offsetToTarget += 7;
return plusDays(offsetToTarget);
}
public DayDate getNearestDayOfWeek(Day targetDayOfWeek) {
int offsetToThisWeeksTarget = targetDayOfWeek.toInt() - getDayOfWeek().toInt();
int offsetToFutureTarget = (offsetToThisWeeksTarget + 7) % 7;
int offsetToPreviousTarget = offsetToFutureTarget - 7;
if (offsetToFutureTarget > 3)
return plusDays(offsetToPreviousTarget);
else
return plusDays(offsetToFutureTarget);
}
public DayDate getEndOfMonth() {
Month month = getMonth();
int year = getYear();
int lastDay = DateUtil.lastDayOfMonth(month, year);
return DayDateFactory.makeDate(lastDay, month, year);
}
public Date toDate() {
final Calendar calendar = Calendar.getInstance();
int ordinalMonth = getMonth().toInt() - Month.JANUARY.toInt();
calendar.set(getYear(), ordinalMonth, getDayOfMonth(), 0, 0, 0);
return calendar.getTime();
}
public String toString() {
return String.format("%02d-%s-%d", getDayOfMonth(), getMonth(), getYear());
}
public Day getDayOfWeek() {
Day startingDay = getDayOfWeekForOrdinalZero();
int startingOffset = startingDay.toInt() - Day.SUNDAY.toInt();
int ordinalOfDayOfWeek = (getOrdinalDay() + startingOffset) % 7;
return Day.fromInt(ordinalOfDayOfWeek + Day.SUNDAY.toInt());
}
public int daysSince(DayDate date) {
return getOrdinalDay() - date.getOrdinalDay();
}
public boolean isOn(DayDate other) {
return getOrdinalDay() == other.getOrdinalDay();
}

ORG.JFREE.DATE.SERIALDATE

401

154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179 }

public boolean isBefore(DayDate other) {


return getOrdinalDay() < other.getOrdinalDay();
}
public boolean isOnOrBefore(DayDate other) {
return getOrdinalDay() <= other.getOrdinalDay();
}
public boolean isAfter(DayDate other) {
return getOrdinalDay() > other.getOrdinalDay();
}
public boolean isOnOrAfter(DayDate other) {
return getOrdinalDay() >= other.getOrdinalDay();
}
public boolean isInRange(DayDate d1, DayDate d2) {
return isInRange(d1, d2, DateInterval.CLOSED);
}
public boolean isInRange(DayDate d1, DayDate d2, DateInterval interval) {
int left = Math.min(d1.getOrdinalDay(), d2.getOrdinalDay());
int right = Math.max(d1.getOrdinalDay(), d2.getOrdinalDay());
return interval.isIn(getOrdinalDay(), left, right);
}

L I S T I N G B 8 . Month.java (ostateczny)
1 package org.jfree.date;
2
3 import java.text.DateFormatSymbols;
4
5 public enum Month {
6
JANUARY(1), FEBRUARY(2), MARCH(3),
7
APRIL(4),
MAY(5),
JUNE(6),
8
JULY(7),
AUGUST(8),
SEPTEMBER(9),
9
OCTOBER(10),NOVEMBER(11),DECEMBER(12);
10
private static DateFormatSymbols dateFormatSymbols = new DateFormatSymbols();
11
private static final int[] LAST_DAY_OF_MONTH =
12
{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
13
14
private int index;
15
16
Month(int index) {
17
this.index = index;
18
}
19
20
public static Month fromInt(int monthIndex) {
21
for (Month m : Month.values()) {
22
if (m.index == monthIndex)
23
return m;
24
}
25
throw new IllegalArgumentException("Niewaciwy indeks miesica " + monthIndex);
26
}
27
28
public int lastDay() {
29
return LAST_DAY_OF_MONTH[index];
30
}
31
32
public int quarter() {
33
return 1 + (index - 1) / 3;
34
}

402

DODATEK B

35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65 }

public String toString() {


return dateFormatSymbols.getMonths()[index - 1];
}
public String toShortString() {
return dateFormatSymbols.getShortMonths()[index - 1];
}
public static Month parse(String s) {
s = s.trim();
for (Month m : Month.values())
if (m.matches(s))
return m;
try {
return fromInt(Integer.parseInt(s));
}
catch (NumberFormatException e) {}
throw new IllegalArgumentException("Nieprawidowy miesic " + s);
}
private boolean matches(String s) {
return s.equalsIgnoreCase(toString()) ||
s.equalsIgnoreCase(toShortString());
}
public int toInt() {
return index;
}

L I S T I N G B 9 . Day.java (ostateczny)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

package org.jfree.date;
import java.util.Calendar;
import java.text.DateFormatSymbols;
public enum Day {
MONDAY(Calendar.MONDAY),
TUESDAY(Calendar.TUESDAY),
WEDNESDAY(Calendar.WEDNESDAY),
THURSDAY(Calendar.THURSDAY),
FRIDAY(Calendar.FRIDAY),
SATURDAY(Calendar.SATURDAY),
SUNDAY(Calendar.SUNDAY);
private final int index;
private static DateFormatSymbols dateSymbols = new DateFormatSymbols();
Day(int day) {
index = day;
}
public static Day fromInt(int index) throws IllegalArgumentException {
for (Day d : Day.values())
if (d.index == index)
return d;
throw new IllegalArgumentException(
String.format("Nieprawidowy indeks dnia: %d.", index));
}

ORG.JFREE.DATE.SERIALDATE

403

30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54 }

public static Day parse(String s) throws IllegalArgumentException {


String[] shortWeekdayNames =
dateSymbols.getShortWeekdays();
String[] weekDayNames =
dateSymbols.getWeekdays();
s = s.trim();
for (Day day : Day.values()) {
if (s.equalsIgnoreCase(shortWeekdayNames[day.index]) ||
s.equalsIgnoreCase(weekDayNames[day.index])) {
return day;
}
}
throw new IllegalArgumentException(
String.format("%s nie jest prawidow nazw dnia tygodnia", s));
}
public String toString() {
return dateSymbols.getWeekdays()[index];
}
public int toInt() {
return index;
}

L I S T I N G B 1 0 . DateInterval.java (ostateczny)
1 package org.jfree.date;
2
3 public enum DateInterval {
4
OPEN {
5
public boolean isIn(int d, int left, int right)
6
return d > left && d < right;
7
}
8
},
9
CLOSED_LEFT {
10
public boolean isIn(int d, int left, int right)
11
return d >= left && d < right;
12
}
13
},
14
CLOSED_RIGHT {
15
public boolean isIn(int d, int left, int right)
16
return d > left && d <= right;
17
}
18
},
19
CLOSED {
20
public boolean isIn(int d, int left, int right)
21
return d >= left && d <= right;
22
}
23
};
24
25
public abstract boolean isIn(int d, int left, int
26 }

L I S T I N G B 1 1 . WeekInMonth.java (ostateczny)

right);

1 package org.jfree.date;
2
3 public enum WeekInMonth {
4
FIRST(1), SECOND(2), THIRD(3), FOURTH(4), LAST(0);
5
private final int index;
6

404

DODATEK B

7
8
9
10
11
12
13
14 }

WeekInMonth(int index) {
this.index = index;
}
public int toInt() {
return index;
}

L I S T I N G B 1 2 . WeekdayRange.java (ostateczny)
1 package org.jfree.date;
2
3 public enum WeekdayRange {
4
LAST, NEAREST, NEXT
5 }

L I S T I N G B 1 3 . DateUtil.java (ostateczny)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

package org.jfree.date;
import java.text.DateFormatSymbols;
public class DateUtil {
private static DateFormatSymbols dateFormatSymbols = new DateFormatSymbols();
public static String[] getMonthNames() {
return dateFormatSymbols.getMonths();
}
public static boolean isLeapYear(int year) {
boolean fourth = year % 4 == 0;
boolean hundredth = year % 100 == 0;
boolean fourHundredth = year % 400 == 0;
return fourth && (!hundredth || fourHundredth);
}
public static int lastDayOfMonth(Month month, int year) {
if (month == Month.FEBRUARY && isLeapYear(year))
return month.lastDay() + 1;
else
return month.lastDay();
}
public static int leapYearCount(int year) {
int leap4 = (year - 1896) / 4;
int leap100 = (year - 1800) / 100;
int leap400 = (year - 1600) / 400;
return leap4 - leap100 + leap400;
}
}

L I S T I N G B 1 4 . DayDateFactory.java (ostateczny)
1 package org.jfree.date;
2
3 public abstract class DayDateFactory {
4
private static DayDateFactory factory = new SpreadsheetDateFactory();
5
public static void setInstance(DayDateFactory factory) {
6
DayDateFactory.factory = factory;
7
}
8
9
protected abstract DayDate _makeDate(int ordinal);

ORG.JFREE.DATE.SERIALDATE

405

10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39 }

protected
protected
protected
protected
protected

abstract
abstract
abstract
abstract
abstract

DayDate _makeDate(int day, Month month, int year);


DayDate _makeDate(int day, int month, int year);
DayDate _makeDate(java.util.Date date);
int _getMinimumYear();
int _getMaximumYear();

public static DayDate makeDate(int ordinal) {


return factory._makeDate(ordinal);
}
public static DayDate makeDate(int day, Month month, int year) {
return factory._makeDate(day, month, year);
}
public static DayDate makeDate(int day, int month, int year) {
return factory._makeDate(day, month, year);
}
public static DayDate makeDate(java.util.Date date) {
return factory._makeDate(date);
}
public static int getMinimumYear() {
return factory._getMinimumYear();
}
public static int getMaximumYear() {
return factory._getMaximumYear();
}

L I S T I N G B 1 5 . SpreadsheetDateFactory.java (ostateczny)
1 package org.jfree.date;
2
3 import java.util.*;
4
5 public class SpreadsheetDateFactory extends DayDateFactory {
6
public DayDate _makeDate(int ordinal) {
7
return new SpreadsheetDate(ordinal);
8
}
9
10
public DayDate _makeDate(int day, Month month, int year) {
11
return new SpreadsheetDate(day, month, year);
12
}
13
14
public DayDate _makeDate(int day, int month, int year) {
15
return new SpreadsheetDate(day, month, year);
16
}
17
18
public DayDate _makeDate(Date date) {
19
final GregorianCalendar calendar = new GregorianCalendar();
20
calendar.setTime(date);
21
return new SpreadsheetDate(
22
calendar.get(Calendar.DATE),
23
Month.fromInt(calendar.get(Calendar.MONTH) + 1),
24
calendar.get(Calendar.YEAR));
25
}
26
27
protected int _getMinimumYear() {
28
return SpreadsheetDate.MINIMUM_YEAR_SUPPORTED;
29
}
30

406

DODATEK B

31
protected int _getMaximumYear() {
32
return SpreadsheetDate.MAXIMUM_YEAR_SUPPORTED;
33
}
34 }

L I S T I N G B 1 6 . SpreadsheetDate.java (ostateczny)
1 /*
======================================================================
2 * JCommon : a free general purpose class library for the Java(tm) platform
3 *
======================================================================
4 *
5 * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
6 *
...
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97

*
*/
package org.jfree.date;
import static org.jfree.date.Month.FEBRUARY;
import java.util.*;

/**
* Reprezentuje daty przy uyciu liczb integer, podobnie jak w
* programie Microsoft Excel. Obsugiwany zakres dat: od
* 1 stycznia 1900 do 31 grudnia 9999.
* <p/>
* Trzeba zwrci uwag, e w Excelu wystpuje bd, powodujcy uznanie roku
* 1900 za rok przestpny, cho nim nie jest. Wicej informacji na ten temat
* mona znale w witrynie Microsoft, w artykule Q181370:
* <p/>
* http://support.microsoft.com/support/kb/articles/Q181/3/70.asp
* <p/>
* Excel korzysta z konwencji, w ktrej 1 stycznia 1900 = 1. Ta klasa korzysta
* z konwencji, w ktrej 1 stycznia 1900 = 2.
* W wyniku tego numer dnia w tej klasie jest inny ni w
* Excelu dla stycznia i lutego 1900..., ale wtedy Excel wprowadza dodatkowy dzie
* (29 lutego 1900, ktry faktycznie nie istnia!) i od tego momentu
* numery dni zgadzaj si.
*
* @author David Gilbert
*/
public class SpreadsheetDate extends DayDate {
public static final int EARLIEST_DATE_ORDINAL = 2; // 1/1/1900
public static final int LATEST_DATE_ORDINAL = 2958465; // 12/31/9999
public static final int MINIMUM_YEAR_SUPPORTED = 1900;
public static final int MAXIMUM_YEAR_SUPPORTED = 9999;
static final int[] AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH =
{0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365};
static final int[] LEAP_YEAR_AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH =
{0, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366};
private
private
private
private

int ordinalDay;
int day;
Month month;
int year;

public SpreadsheetDate(int day, Month month, int year) {


if (year < MINIMUM_YEAR_SUPPORTED || year > MAXIMUM_YEAR_SUPPORTED)

ORG.JFREE.DATE.SERIALDATE

407

98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161

408

throw new IllegalArgumentException(


"Argument 'year' musi zawiera si w zakresie " +
MINIMUM_YEAR_SUPPORTED + " do " + MAXIMUM_YEAR_SUPPORTED + ".");
if (day < 1 || day > DateUtil.lastDayOfMonth(month, year))
throw new IllegalArgumentException("Nieprawidowy argument 'day'.");
this.year = year;
this.month = month;
this.day = day;
ordinalDay = calcOrdinal(day, month, year);
}
public SpreadsheetDate(int day, int month, int year) {
this(day, Month.fromInt(month), year);
}
public SpreadsheetDate(int serial) {
if (serial < EARLIEST_DATE_ORDINAL || serial > LATEST_DATE_ORDINAL)
throw new IllegalArgumentException(
"SpreadsheetDate: warto 'serial' musi by z zakresu od 2 do 2958465.");
ordinalDay = serial;
calcDayMonthYear();
}
public int getOrdinalDay() {
return ordinalDay;
}
public int getYear() {
return year;
}
public Month getMonth() {
return month;
}
public int getDayOfMonth() {
return day;
}
protected Day getDayOfWeekForOrdinalZero() {return Day.SATURDAY;}
public boolean equals(Object object) {
if (!(object instanceof DayDate))
return false;
DayDate date = (DayDate) object;
return date.getOrdinalDay() == getOrdinalDay();
}
public int hashCode() {
return getOrdinalDay();
}
public int compareTo(Object other) {
return daysSince((DayDate) other);
}
private int calcOrdinal(int day, Month month, int year) {
int leapDaysForYear = DateUtil.leapYearCount(year - 1);
int daysUpToYear = (year - MINIMUM_YEAR_SUPPORTED) * 365 + leapDaysForYear;
int daysUpToMonth = AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH[month.toInt()];
if (DateUtil.isLeapYear(year) && month.toInt() > FEBRUARY.toInt())

DODATEK B

162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215 }

daysUpToMonth++;
int daysInMonth = day - 1;
return daysUpToYear + daysUpToMonth + daysInMonth + EARLIEST_DATE_ORDINAL;
}
private void calcDayMonthYear() {
int days = ordinalDay - EARLIEST_DATE_ORDINAL;
int overestimatedYear = MINIMUM_YEAR_SUPPORTED + days / 365;
int nonleapdays = days - DateUtil.leapYearCount(overestimatedYear);
int underestimatedYear = MINIMUM_YEAR_SUPPORTED + nonleapdays / 365;
year = huntForYearContaining(ordinalDay, underestimatedYear);
int firstOrdinalOfYear = firstOrdinalOfYear(year);
month = huntForMonthContaining(ordinalDay, firstOrdinalOfYear);
day = ordinalDay - firstOrdinalOfYear - daysBeforeThisMonth(month.toInt());
}
private Month huntForMonthContaining(int anOrdinal, int firstOrdinalOfYear) {
int daysIntoThisYear = anOrdinal - firstOrdinalOfYear;
int aMonth = 1;
while (daysBeforeThisMonth(aMonth) < daysIntoThisYear)
aMonth++;
return Month.fromInt(aMonth - 1);
}
private int daysBeforeThisMonth(int aMonth) {
if (DateUtil.isLeapYear(year))
return LEAP_YEAR_AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH[aMonth] - 1;
else
return AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH[aMonth] - 1;
}
private int huntForYearContaining(int anOrdinalDay, int startingYear) {
int aYear = startingYear;
while (firstOrdinalOfYear(aYear) <= anOrdinalDay)
aYear++;
return aYear - 1;
}
private int firstOrdinalOfYear(int year) {
return calcOrdinal(1, Month.JANUARY, year);
}
public static DayDate createInstance(Date date) {
GregorianCalendar calendar = new GregorianCalendar();
calendar.setTime(date);
return new SpreadsheetDate(calendar.get(Calendar.DATE),
Month.fromInt(calendar.get(Calendar.MONTH) + 1),
calendar.get(Calendar.YEAR));
}

ORG.JFREE.DATE.SERIALDATE

409

410

DODATEK B

DODATEK C

Odwoania do heurystyk

DWOANIA DO ZAPACHW KODU I HEURYSTYK.

Pozostae odwoania mog by usunite.

C1 ..................................................................................................................................................... 280, 282, 296


C2 . ............................................................................................................................................282, 286, 292, 296
C3 . ............................................................................................................................................284, 286, 288, 296
C4 ...................................................................................................................................................................... 297
C5 ...................................................................................................................................................................... 297
E1 ....................................................................................................................................................................... 297
E2 ....................................................................................................................................................................... 297
F1 ...............................................................................................................................................................242, 298
F2 ....................................................................................................................................................................... 298
F3 ....................................................................................................................................................................... 298
F4 ...................................................................................................................................................... 278, 287, 298
G ................................................................................................................................................................279, 283
G1 ..............................................................................................................................................................280, 298
G10 ............................................................................................................................................................101, 302
G11 . ..................................................................................................................................270, 285, 287, 290, 302
G12 . ..................................................................................................................................286, 287, 288, 292, 303
G13 ................................................................................................................................................... 286, 287, 303
G14 . ..........................................................................................................................................287, 288, 290, 303
G15 ............................................................................................................................................................287, 304
G16 ............................................................................................................................................................288, 305
G17 ............................................................................................................................................................288, 305
G18 ................................................................................................................................................... 288, 289, 306

411

G19 ................................................................................................................................................... 289, 290, 306


G2 ..............................................................................................................................................................278, 299
G20 ............................................................................................................................................................289, 307
G21 ............................................................................................................................................................289, 307
G22 ............................................................................................................................................................291, 308
G23 ............................................................................................................................................. 60, 242, 292, 309
G24 ............................................................................................................................................................292, 309
G25 ............................................................................................................................................................293, 309
G26 .................................................................................................................................................................... 310
G27 .................................................................................................................................................................... 311
G28 ............................................................................................................................................................268, 311
G29 ............................................................................................................................................................269, 311
G3 ..............................................................................................................................................................279, 299
G30 ............................................................................................................................................................269, 312
G31 ............................................................................................................................................................270, 312
G32 ............................................................................................................................................................271, 313
G33 ............................................................................................................................................................272, 314
G34 ............................................................................................................................................... 31, 57, 119, 314
G35 ............................................................................................................................................................104, 315
G36 ............................................................................................................................................................118, 316
G4 ..............................................................................................................................................................282, 299
G5 ............................................................................................................................ 144, 282, 286, 289, 292, 299
G6 . ...................................................................................................................119, 282, 284, 285, 291, 292, 300
G7 ..............................................................................................................................................................283, 301
G8 ..............................................................................................................................................................284, 302
G9 . ............................................................................................................................................273, 285, 287, 302
J1 ................................................................................................................................................................280, 316
J2 ................................................................................................................................................................281, 317
J3 ....................................................................................................................................................... 284, 285, 318
N1 . ..........................................................................................270, 281, 282, 284, 287, 288, 289, 291, 292, 319
N2 ..............................................................................................................................................................281, 320
N3 ..................................................................................................................................................... 285, 288, 321
N4 ..................................................................................................................................................... 269, 289, 321
N5 . ...............................................................................................................................................................44, 321
N6 ..............................................................................................................................................................268, 322
N7 ..............................................................................................................................................................269, 322
T1 ..................................................................................................................................................... 278, 279, 322
T2 ..............................................................................................................................................................278, 323
T3 ..............................................................................................................................................................278, 323
T4 ....................................................................................................................................................................... 323
T5 ..............................................................................................................................................................279, 323
T6 ..............................................................................................................................................................279, 323
T7 ..............................................................................................................................................................279, 323
T8 ..............................................................................................................................................................279, 323
T9 ...................................................................................................................................................................... 324

412

DODATEK C

Epilog

Agile w Denver, Elisabeth Hedrickson podaa mi zielon opask


Wna rk, podobn do tej,konferencji
ktr tak spopularyzowa Lance Armstrong. Moja miaa napis Test Obsessed
ROKU

2005,

W CZASIE

(ogarnity obsesj testw). Chtnie j zaoyem i nosiem z dum. Od momentu nauczenia si TDD od
Kenta Becka, w roku 1999, faktycznie staem si entuzjast programowania sterowanego testami.
Jednak wtedy stao si co dziwnego. Odkryem, e nie mog zdj tej opaski. Nie dlatego, e fizycznie
si zablokowaa. Opaska ta ujawniaa moj profesjonaln etyk. Bya widocznym wskanikiem
mojego zaangaowania w tworzenie najlepszego kodu, jaki mogem napisa. Jej zdjcie wydawao
si zdrad tej etyki, a na to nie mogem sobie pozwoli.
Dlatego nadal jest ona na moim nadgarstku. Gdy pisz kod, widz j ktem oka. Jest to stae przypomnienie celu, jaki sobie postawiem celem tym jest pisanie czystego kodu.

http://www.qualitytree.com

413

414

EPILOG

S KOROWI D Z

A
Abstract Factory, 46, 172
abstrakcja danych, 113
abstrakcje, 300
ABY, 59
ACMEPort, 128
Active Record, 120
Agile, 16, 142
akapity ABY, 59
akcesory, 47
analiza pokrycia kodu, 266
antysymetria danych i obiektw, 115
AOP, 176
API Windows C, 45
aplikacje
jednowtkowe, 194
WWW, 194
architektura EJB, 176
architektura EJB2, 176
Args, 210
implementacja klasy, 210
ArgsException, 253, 260
argumenty funkcji, 62
argumenty obiektowe, 64
argumenty typu String, 228
argumenty wybierajce, 305
argumenty wyjciowe, 62, 66, 298
argumenty znacznikowe, 63, 298
asercje, 149
ASM, 177, 206
AspectJ, 181
Aspect-Oriented Framework, 206
aspekty, 176
AspectJ, 181
assertEquals(), 64
atrybuty, 89
automatyczne sterowanie serializacj, 282

B
Basic, 45
BDUF, 182
bean, 119

Beck Kent, 24, 187, 264


biblioteka JUnit, 264
Big Design Up Front, 182
bloki, 57
bloki try-catch, 68
blokowanie po stronie klienta, 201
blokowanie po stronie serwera, 201
bdy, 123
Booch Grady, 30
break, 70
brodzenie, 25
budowanie, 297

C
CGLIB, 177, 206
Clover, 278
ConcurrentHashMap, 199
ConTest, 207
continue, 70
CountDownLatch, 199
Cunningham Ward, 33
czasowniki, 65
czyste biblioteki Java AOP, 178
czyste granice, 139
czyste testy, 144
zasady, 151
czysto, 14, 15, 31
czysto projektu, 187
czysto testw, 143
czysty kod, 16, 23, 28, 34
czytanie kodu, 35
od gry do dou, 58
czytelnik-pisarz, 200
czytelno, 30

D
dane, 115
wejciowe, 66
wyjciowe, 288
DAO, 179
DBMS, 176

415

definiowanie
klasy wyjtkw, 127
normalny przepyw, 129
deklaracje zmiennych, 102
dekorator, 179, 284
Dependency Injection, 172
dezinformacja, 42
DI, 172
Dijkstra Edserer, 70
DIP, 36, 167
dugie listy importu, 317
dugie nazwy, 323
dugo wierszy kodu, 106
dobre komentarze, 77
dobry kod, 16, 24
Dont Repeat Yourself, 300
dopiski, 89
dostarczenie produktu na rynek, 14
dostp do danych, 179
DRY, 69, 300
DSL, 183
DTO, 119, 176
dyscyplina, 14
dziedziczenie staych, 318

E
efekty uboczne, 65, 323
sprzenie czasowe, 66
efektywno, 29
EJB, 176, 194
EJB1, 174
EJB2, 174, 175, 176
EJB3, 180
eliminacja nadmiarowych instrukcji, 273
Entity Bean, 174
enum, 319
Error.java, 69
Evans Eric, 322

F
F.I.R.S.T., 151
fabryka abstrakcyjna, 46, 60, 172, 284
fabryki, 172, 284
Feathers Michael, 31
Feature Envy, 288
final, 286
fizyka oprogramowania, 182

416

SKOROWIDZ

format danych wyjciowych, 288


formatowanie, 97
deklaracje zmiennych, 102
funkcje zalene, 103
gazeta, 99
gsto pionowa, 101
koligacja koncepcyjna, 105
amanie wci, 110
odlego pionowa, 101
pionowe, 98
pionowe odstpy pomidzy segmentami kodu, 99
poziome, 106
poziome odstpy, 106
przeznaczenie, 98
puste zakresy, 110
rozmieszczenie poziome, 107
uporzdkowanie pionowe, 105
wcicia, 109
zasady zespoowe, 110
zmienne instancyjne, 102
formatowanie HTML, 280
Fortran, 45
Fowler Martin, 304
funkcje, 53, 298
argumenty, 62
argumenty obiektowe, 64
argumenty wyjciowe, 62, 66
argumenty znacznikowe, 63
bezargumentowe, 62
bloki, 57
bloki try-catch, 68
break, 70
continue, 70
czasowniki, 65
dane wejciowe, 66
dugo, 56
dwuargumentowe, 63
efekty uboczne, 65
goto, 70
jednoargumentowe, 62
kody bdw, 67
listy argumentw, 65
nagwki, 92
nazwy, 40, 61, 269, 307
Nie powtarzaj si, 69
obsuga bdw, 69
poziom abstrakcji, 58
return, 70
rozdzielanie polece i zapyta, 67

sekcje, 58
sowa kluczowe, 65
sprzenie czasowe, 66
switch, 59
trzyargumentowe, 64
uporzdkowane skadniki jednej wartoci, 64
wcicia, 57
wieloargumentowe, 62
wyjtki, 67
wykonywane czynnoci, 57
zalene funkcje, 103
zasada konstruowania, 56
zasady pisania, 70
zdarzenia, 63
zwracanie kodw bdw, 67
zwracanie wyniku, 62

G
Ga-Jol, 16
Gamm Eric, 264
gazeta, 99
gettery, 113
gsto pionowa, 101
Gilbert David, 277
given-when-then, 150
globalna strategia konfiguracji, 171
goto, 70
granice, 133
czyste granice, 139
korzystanie z nieistniejcego kodu, 138
przegldanie, 136
testy graniczne, 138
testy uczce, 138
uczenie si obcego kodu, 136
zastosowanie kodu innych firm, 134

H
hermetyzacja, 127, 154
hermetyzacja warunkw, 312
warunki graniczne, 314
HTML, 90
Hunt Andy, 29, 300
hybrydowe struktury danych, 118
hybrydy, 118
hypotenuse, 41

I
idiom pnej inicjalizacji, 170
if, 286, 300, 309
implementacja interfejsu, 46
include, 70
informacja, 42
informacje nielokalne, 91
instrukcje switch, 59
interfejsy, 46
Inversion of Control, 172
IoC, 172

J
jar, 302
Java, 46, 317
JUnit, 263
klasy, 153
poredniki, 177
wspbieno, 198
Java Swing, 156
java.util.Calendar, 278
java.util.concurrent, 198
java.util.Date, 278
java.util.Map, 134
Javadoc, 81, 280, 286
Javassist, 177
JBoss, 179
JBoss AOP, 178, 181
JCommon, 277, 280
JDBC, 179
JDK, 177
jedna asercja na test, 149
jedna koncepcja na test, 150
jedno sowo na jedno abstrakcyjne pojcie, 48
jednoznaczne nazwy, 322
Jeffries Ron, 32
jzyk, 298
jzyk DSL, 184
jzyk dziedzinowy, 183
jzyki testowania specyficzne dla domeny, 147
JFrame, 156
JNDI, 173
JUnit, 55, 149, 226, 263
analiza pokrycia kodu, 266
przypadki testowe, 264

SKOROWIDZ

417

K
klasy, 153
bazowe, 301
bazowe zalene od swoich klas pochodnych, 301
DIP, 167
hermetyzacja, 154
izolowanie moduw kodu przed zmianami, 166
Java, 153
liczba zmiennych instancyjnych, 158
metody prywatne, 164
nazwy, 40, 47, 156
OCP, 166
odpowiedzialnoci, 154
organizacja, 153, 157
organizacja zmian, 164
prywatne funkcje uytkowe, 154
prywatne zmienne statyczne, 153
przebudowa, 277
publiczne stae statyczne, 153
rozmiar, 154
spjno, 158
utrzymywanie spjnoci, 158
zasada odwrcenia zalenoci, 167
zasada otwarty-zamknity, 166
zasada pojedynczej odpowiedzialnoci, 156
zmiany, 164, 166
klasyfikacja wyjtkw, 127
kod, 24
kod innych firm, 134
kod na nieodpowiednim poziomie abstrakcji, 300
kod testw, 144
kod za przyjemny w czytaniu, 29
kody bdw, 67
kody powrotu, 124
kolekcje bezpieczne dla wtkw, 198
koligacja koncepcyjna, 105
komentarze, 75, 282, 293, 296
atrybuty, 89
bekot, 81
czytelny kod, 77
dopiski, 89
dziennik, 85
HTML, 90
informacje nielokalne, 91
informacyjne, 78
Javadoc, 81, 92
klamry zamykajce, 88

418

SKOROWIDZ

mylce komentarze, 84
nadmiar informacji, 91
nagwki funkcji, 92
nieoczywiste poczenia, 91
nieudany kod, 77
objaniajce, 79
ostrzeenia o konsekwencjach, 80
powody pisania, 77
powtarzajce si komentarze, 82
prawne, 77
szum, 87
TODO, 80
wprowadzanie szumu informacyjnego, 86
wyjanianie zamierze, 78, 79
wymagane komentarze, 85
wzmocnienie wagi operacji, 81
zakomentowany kod, 89
ze komentarze, 81
znaczniki pozycji, 88
komunikaty bdw, 127
konstruowanie systemu, 170
kontekst kodu, 40
kontekst nazwy, 50
kontener, 173
kontrolowane wyjtki, 126
korzystanie z nieistniejcego kodu, 138
korzystanie ze standardw, 183
koszt utrzymania zestawu testw, 143

L
listy argumentw, 65
listy importu, 317
log4j, 136

amanie wci, 110

M
magiczne liczby, 310
magnesy zalenoci, 69
main, 171
Map, 134
martwe funkcje, 298
martwy kod, 302
mechanika pisania funkcji, 71

metody, 117
abstrakcyjne, 293
instancyjne, 289
nazwy, 47
statyczne, 284, 306
minimalizowanie kodu, 31
minimalne klasy, 192
minimalne metody, 192
Mock, 171
moduy, 117
main, 171
mutatory, 47

N
nadmiar argumentw, 298
nadmiar informacji, 91
nadmiarowe instrukcje, 273
nadmiarowe komentarze, 83, 282, 296
nagwki funkcji, 92
narzdzia kontroli pokrycia, 324
nawigacja przechodnia, 317
nazwy, 39, 290, 320
akcesory, 47
argumenty, 43
dezinformacja, 42
dugo, 45
dodatkowe sowa, 43
dziedzina problemu, 49
dziedzina rozwizania, 49
efekty uboczne, 323
funkcje, 40, 61, 269, 307
ilustracja zamiarw, 40
implementacja interfejsu, 46
informacja, 42
interfejsy, 46
interpretacja sw, 43
jedno sowo na jedno abstrakcyjne pojcie, 48
jednoliterowe, 44, 47
kalambury, 49
klasy, 40, 47, 156
kodowanie, 45
kontekst, 50
atwo wyszukania, 44
metody, 47
mutatory, 47
nadmiarowy kontekst, 51
notacja wgierska, 45
odpowiedni poziom abstrakcji, 321

odwzorowanie mentalne, 47
parametry, 48
predykaty, 47
przedrostki skadnikw, 46
refactoring, 52
standardowa nomenklatura, 322
tworzenie wyranych rnic, 42
unikanie dezinformacji, 41
unikanie kodowania, 323
wymawianie, 43
zmienne, 40, 47
zmienne skadowe, 270
Newkirk Jim, 136
Nie powtarzaj si, 69
niejawno kodu, 40
niekontrolowane wyjtki, 126
niespjno, 303
niewaciwe dziaanie w warunkach granicznych, 299
niewaciwe informacje, 296
niewaciwe metody statyczne, 306
niewystarczajce testy, 324
notacja wgierska, 45
null, 130
NW, 45

O
obiekt dostpu do danych, 179
obiekty, 113, 115
obiekty Mock, 171
obiekty POJO, 178
obiekty Test Double, 171
obiekty transferu danych, 119, 176
objaniajce zmienne tymczasowe, 290
Object.priority(), 205
Object.sleep(), 205
Object.wait(), 205
Object.yield(), 205
obliczanie przeciwprostoktnej, 41
obsuga bdw, 69, 123, 254
definiowanie klas wyjtkw, 127
definiowanie normalnego przepywu, 129
dostarczanie kontekstu, 127
kody powrotu, 124
komunikaty bdw, 127
niekontrolowane wyjtki, 126
null, 130
przekazywanie wartoci null, 131
try-catch-finally, 125
wyjtki, 124

SKOROWIDZ

419

OCP, 36, 60, 166


oczyszczanie kodu, 209
oczywiste dziaanie, 299
odlego pionowa, 101
odpowiedzialnoci, 154
odwrcenie sterowania, 172
odwzorowanie mentalne nazw, 47
opisowe nazwy, 61, 320
opisowe zmienne, 307
optymalizacja, 173
optymalizacja podejmowania decyzji, 183
organizacja, 14
organizacja klas, 153
ostrzeenia o konsekwencjach, 80
OTO, 57

P
pakiet log4j, 136
pionowe odstpy pomidzy segmentami kodu, 99
Plain-Old Java Object, 178
pliki rdowe, 298
pocztkowe przypadki testowe, 279
podejmowanie decyzji, 183
POJO, 178, 182, 184, 206
polimorfizm, 309
porzdek, 14
poredniki Java, 177
powtrzenia, 189, 300
poziome odstpy, 106
pna inicjalizacja, 170, 173
PPP, 36
prawa TDD, 142
prawo Demeter, 117
precyzja, 311
predykaty, 47
priority(), 205
problem z szerokoci listy, 108
problemy, 170, 176
problemy z wielowtkowoci, 203
proces uruchomienia, 170
producent-konsument, 199
produkt, 14
program wspbieny, 194
programowanie, 24
pimienne, 31
sterowane testami, 141, 226
strukturalne, 70
wspbiene, 194, 196
zorientowane aspektowo, 176

420

SKOROWIDZ

projekt, 187
czysto, 187
minimalne klasy, 192
minimalne metody, 192
powtrzenia, 189
prosty projekt, 188
przebudowa, 188
rozwijanie, 187
system przechodzi wszystkie testy, 188
wyrazisto kodu, 191
projektowanie obiektowe, 304
prosty projekt, 188
prbowanie, 192
przebudowa klasy, 277
przebudowa projektu, 188
przechowywanie danych konfigurowalnych
na wysokim poziomie, 316
przedrostki skadnikw, 46
przekazywanie argumentw, 271
przekazywanie wartoci null, 131
przestarzae komentarze, 296
przyciganie zalenoci, 69
przypadki testowe, 264, 279
przyrostowo, 226, 227
puste zakresy, 110
Python, 126

R
ReentrantLock, 199
refactoring, 52
regua noyczek, 103
return, 70
rozbite okno, 29
rozcicie problemw, 176
rozdzielanie, 194
rozdzielanie polece i zapyta, 67
rozkad dugoci wierszy kodu, 106
rozmieszczenie kodu, 306
rozmieszczenie odpowiedzialnoci, 306
rozmieszczenie poziome, 107
rozwijanie projektu, 187
Ruby, 126

S
samodyscyplina, 14
Scrum, 16
seiketsu, 14

seiri, 14, 15
seiso, 14
seiton, 14, 15
sekcje krytyczne, 197
sekcje synchronizowane, 201
Semaphore, 199
separacja pionowa, 303
separowanie problemw, 176
SerialDate, 277
serwer adaptujcy, 201
serwlety, 194
settery, 113
SetupTeardownIncluder, 71
shutsuke, 14
singleton, 284
skalowanie w gr, 173
sleep(), 205
sowa kluczowe, 65
SOAP, 163
Sparkle, 56
Special Case Pattern, 130
specyfikacja, 24
specyfikacja wymaga, 24
spjno, 285
sprawdzanie pokrycia przez testy, 278
Spring, 179, 180
Spring AOP, 178, 181
Spring Framework, 173
sprzenie czasowe, 66, 270, 313
SRP, 36, 60, 156, 157, 164, 190, 197, 260
stae, 319
stae nazwane, 310
stae numeryczne, 44
standardowe konwencje, 310
standardy, 183
standaryzacja, 14
sterowanie serializacj, 282
strategia konfiguracji, 171
String, 228
String.format(), 65
Stroustrup Bjarne, 29
struktura przed konwencj, 312
struktury danych, 113, 114
ukrywanie struktury, 119
switch, 59, 292, 300, 309
synchronized, 197, 201
systemy, 169
BDUF, 182
czyste biblioteki Java AOP, 178
fabryki, 172

fizyka oprogramowania, 182


jzyk dziedzinowy, 183
konstruowanie, 170
modu main, 171
optymalizacja podejmowania decyzji, 183
podejmowanie decyzji, 183
problemy, 176
rozcicie problemw, 176
rozdzielanie problemw, 170
separowanie problemw, 176
skalowanie w gr, 173
standardy, 183
strategia konfiguracji, 171
testowanie architektury, 182
uywanie, 170
wstrzykiwanie zalenoci, 172
szkoa mylenia, 34
sztuczne sprzenia, 303
sztuka czystego kodu, 28
szum, 87

rodowisko, 297

T
TDD, 126, 184, 226
prawa, 142
Template Method, 150, 190
Test Double, 171
Test Driven Development, 141
TestNG, 323
testowanie
architektura systemu, 182
jednostkowe, 171
kod wtkw, 202
pobliskie bdy, 324
testy, 31, 278, 324
automatyczne, 226
graniczne, 138
uczce, 136, 138
testy jednostkowe, 141, 144, 297
asercje, 149
czyste testy, 144
czysto testw, 143
F.I.R.S.T., 151
jedna asercja na test, 149
jedna koncepcja na test, 150

SKOROWIDZ

421

testy jednostkowe
jzyki testowania specyficzne dla domeny, 147
koszt utrzymania zestawu testw, 143
moliwoci, 144
powtarzalno, 151
TDD, 142
Thomas Dave, 29, 30, 300
TODO, 80
Total Productive Maintenance, 14
Totalne Zarzdzanie Produkcj, 14
TPM, 14, 15
trafna abstrakcja, 30
trwao obiektw, 176
try, 125
try-catch, 68, 126
try-catch-finally, 125
tworzenie
czysty kod, 28
produkt, 14
typy wyliczeniowe, 319

U
uczenie si obcego kodu, 136
ucztujcy filozofowie, 200
udane oczyszczanie kodu, 209
ukryte sprzenie czasowe, 270, 313
ukrywanie implementacji, 114
ukrywanie struktury, 119
UML, 16
unikanie dezinformacji, 41
unikanie dowolnych dziaa, 314
unikanie kodowania, 323
unikanie nawigacji przechodnich, 317
unikanie warunkw negatywnych, 312
uporzdkowanie pionowe, 105
upraszczanie funkcji, 273
utrzymywanie moliwie najwyszej czystoci kodu, 28
uwizienie, 199
uzyskiwanie czystoci projektu, 187
uywanie systemu, 170

V
void, 45

422

SKOROWIDZ

W
wading, 25
wait(), 205
warto null, 130
warunki graniczne, 279, 299, 324
warunki negatywne, 312
wtki, 194, 198
testowanie kodu, 202
wcicia, 57, 109
wczesne uruchamianie, 202
wczesne wyczanie, 202
wspbieno, 193
atomowo, 196
awarie, 205
biblioteki, 198
blokowanie po stronie klienta, 201
blokowanie po stronie serwera, 201
CountDownLatch, 199
czytelnik-pisarz, 200
instrumentacja automatyczna, 206
instrumentacja rczna, 205
Java 5, 198
kolekcje bezpieczne dla wtkw, 198
kopie danych, 197
mity, 195
modele wykonania, 199
nieporozumienia, 195
ograniczanie zakresu danych, 197
problemy, 195
producent-konsument, 199
przypadkowe awarie, 203
ReentrantLock, 199
sekcje krytyczne, 197
sekcje synchronizowane, 201
Semaphore, 199
serwer adaptujcy, 201
serwlety, 194
SRP, 197
stosowanie, 194
synchronizowane metody, 201
testowanie kodu wtkw, 202
testy, 204
tworzenie sekcji synchronizowanych, 201
ucztujcy filozofowie, 200
uwizienie, 199
wtki, 198, 203
wczesne uruchamianie, 202

wczesne wyczanie, 202


wydajno, 195
wyzwania, 196
wzajemne wykluczanie, 199
zagodzenie, 199
zakleszczenie, 199
zalenoci pomidzy synchronizowanymi
metodami, 201
zasada pojedynczej odpowiedzialnoci, 197
zasady obrony wspbienoci, 196
zasoby zwizane, 199
wstrzykiwanie zalenoci, 172, 173, 179
wszechobecny jzyk projektu, 322
wybr nazw, 40
wybr opisowych nazw, 320
wydajno, 26
wydzielanie moduu main, 171
wyjanianie zamierze, 78
wyjtki, 67, 124, 125, 253
dostarczanie kontekstu, 127
klasy, 127
przechwytywanie, 127
wykonywanie wielkiego projektu od podstaw, 182
wymagane komentarze, 85
wymagania uytkownikw, 24
wyrazisto kodu, 191
wyszukiwanie JNDI, 173
wzajemne wykluczanie, 199
wzmocnienie wagi operacji, 81
wzorce bdw, 324
wzorce pokrycia testami, 279, 325
wzorzec awarii, 279
wzorzec fabryki abstrakcyjnej, 172
wzorzec specjalnego przypadku, 130
wzorzec szablonu metody, 150, 190

X
XHTML, 316

Y
yield(), 205

zakleszczenie, 199
zakomentowany kod, 89, 297
zalenoci, 172
fizyczne, 308
logiczne, 292, 308
pomidzy synchronizowanymi metodami, 201
zamiana zalenoci logicznych na fizyczne, 308
zarzdzanie zalenociami, 172
zasada DRY, 300
zasada jedno sowo na jedno abstrakcyjne pojcie, 48
zasada najmniejszego zaskoczenia, 299
zasada odwrcenia zalenoci, 36, 167
zasada otwarty-zamknity, 36, 60, 166
zasada pojedynczej odpowiedzialnoci, 36, 60, 156,
171, 172, 197
zasada skautw, 36, 268
zasada SRP, 197
zasada zstpujca, 58
zasady 5S, 14
zasady formatowania, 110, 111
zasoby zwizane, 199
zastosowanie kodu innych firm, 134
zazdro o funkcje, 288, 304
zdarzenia, 63
zdejmowanie zabezpiecze, 299
zesp tygrysw, 26
zy kod, 29
zmiana nazwy, 61
zmiany, 27, 166
zmiany projektu, 26
zmienne, 102
deklaracje, 102
instancyjne, 102
nazwy, 40
prywatne, 113
tymczasowe, 289
znaczniki HTML, 90
znaczniki pozycji, 88
zrozumienie algorytmu, 308
zwracanie kodw bdw z funkcji, 67
zwracanie wartoci null, 130

le napisane komentarze, 297


rdo danych, 179

zaciemnianie, 303
zaciemnianie intencji, 305
zagodzenie, 199

SKOROWIDZ

423