Uploaded by skydwarf

pragmatyczny-programista-od-czeladnika-do-mistrza-wydanie-ii-david-thomas-andrew-hunt-helion

advertisement
W YD A N IE J U B IL E U SZOW E Z OKAZJI
RO C ZN IC Y P IER WSZEJ EDYCJI
Wydanie II
Pragmatyczny
programista
Od czeladnika
do mistrza
Tytuł oryginału: The Pragmatic Programmer: Your Journey To Mastery,
20th Anniversary Edition (2nd Edition)
Tłumaczenie: Radosław Meryk
na podstawie Pragmatyczny programista. Od czeladnika do mistrza
w przekładzie Mikołaja Szczepaniaka
ISBN: 978-83-283-7140-8
Authorized translation from the English language edition, entitled
THE PRAGMATIC PROGRAMMER: YOUR JOURNEY TO MASTERY,
20TH ANNIVERSARY EDITION, 2nd Edition by DAVID THOMAS; ANDREW HUNT,
published by Pearson Education, Inc, publishing as Addison-Wesley Professional,
Copyright © 2020 Pearson Education, Inc.
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.
Polish language edition published by Helion SA, Copyright © 2021.
Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości
lub fragmentu niniejszej publikacji w jakiejkolwiek postaci jest zabronione.
Wykonywanie kopii metodą kserograficzną, fotograficzną, a także kopiowanie
książki na nośniku filmowym, magnetycznym lub innym powoduje naruszenie
praw autorskich niniejszej publikacji.
Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi
bądź towarowymi ich właścicieli.
Autorzy oraz Helion SA dołożyli wszelkich starań, by zawarte w tej książce
informacje były kompletne i rzetelne. Nie biorą jednak żadnej odpowiedzialności
ani za ich wykorzystanie, ani za związane z tym ewentualne naruszenie praw
patentowych lub autorskich. Autorzy oraz Helion SA nie ponoszą również żadnej
odpowiedzialności za ewentualne szkody wynikłe z wykorzystania informacji
zawartych w książce.
Helion SA
ul. Kościuszki 1c, 44-100 Gliwice
tel. 32 231 22 19, 32 230 98 63
e-mail: helion@helion.pl
WWW: http://helion.pl (księgarnia internetowa, katalog książek)
Drogi Czytelniku!
Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres
http://helion.pl/user/opinie/pragp2_ebook
Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję.
 Poleć książkę na Facebook.com
 Księgarnia internetowa
 Kup w wersji papierowej
 Lubię to! » Nasza społeczność
 Oceń książkę
4337ebf6db5c7cc89e4173803ef3875a
4
Opinie na temat drugiego wydania książki
Pragmatyczny programista
Niektórzy twierdzą, że w książce Pragmatyczny programista Andy i Dave schwytali błyskawicę do butelki. Jest mało prawdopodobne, aby ktoś wkrótce napisał książkę, która poruszy branżę w taki sposób, w jaki oni to zrobili. Czasami
jednak błyskawica uderza dwa razy. Ta książka jest dowodem, że to możliwe.
Dzięki zaktualizowanej treści pozostanie ona na szczycie list najlepszych publikacji dotyczących rozwoju oprogramowania przez 20 kolejnych lat. To miejsce jej się należy.
VM (Vicky) Brasseur
Dyrektor strategii open source, Juniper Networks
Jeśli chcesz, aby Twoje oprogramowanie było łatwe do aktualizacji i utrzymania, miej pod ręką egzemplarz książki Pragmatyczny programista. Jest ona pełna
praktycznych porad, zarówno technicznych, jak i zawodowych, które będą służyły Tobie i Twoim projektom przez długie lata.
Andrea Goulet
Dyrektor naczelny firmy Corgibytes; założyciel witryny LegacyCode.Rocks
Pragmatyczny programista to książka, która całkowicie zmieniła tor mojej kariery
w tworzeniu oprogramowania i wskazała kierunek do sukcesu. Jej lektura
otworzyła mój umysł na możliwości bycia rzemieślnikiem, a nie tylko trybikiem w wielkiej maszynie. To jedna z najważniejszych książek w moim życiu.
Obie Fernandez
Autor książki The Rails Way
Czytelnicy, którzy sięgają po tę książkę po raz pierwszy, mogą spodziewać się
fascynującego wprowadzenia do nowoczesnego świata praktyk tworzenia oprogramowania — świata, w którego kształtowaniu pierwsze wydanie tej książki
odegrało ważną rolę. Czytelnicy pierwszej edycji na nowo odkryją trafne spostrzeżenia i praktyczną mądrość, dzięki którym ta publikacja stała się tak ważna.
Drugie wydanie zostało fachowo uzupełnione i uaktualnione dużą ilością nowego
materiału.
David A. Black
Autor książki The Well-Grounded Rubyist
4337ebf6db5c7cc89e4173803ef3875a
4
Mam na półce starą, papierową kopię pierwszego wydania Pragmatycznego programisty. Książkę tę czytałem wiele razy i stale do niej wracam. Dawno temu
zmieniła ona wszystko w sposobie, w jaki podchodziłem do zawodu programisty. W nowym wydaniu zmieniło się wszystko, a jednocześnie nie zmieniło
się nic: teraz czytam ją na moim iPadzie, a w przykładach kodu wykorzystano
nowoczesne języki programowania, ale opisywane pojęcia, pomysły i sposoby
postępowania są ponadczasowe i mają uniwersalne zastosowanie. Dwadzieścia
lat po pierwszym wydaniu książka jest tak samo aktualna, jak kiedyś. Jestem
szczęśliwy, że obecni i przyszli programiści będą mieć taką samą okazję uczenia
się od Andy’ego i Dave’a ich głębokich spostrzeżeń, jaką miałem ja czytając
pierwsze wydanie.
Sandy Mamoli
Trener Agile, autor książki How Self-Selection Lets People Excel
Dwadzieścia lat temu pierwsze wydanie Praktycznego programisty całkowicie
zmieniło trajektorię mojej kariery. Niniejsza nowa edycja może zrobić to samo
dla Was.
Mike Cohn
Autor książek Succeeding with Agile, Agile Estimating and Planning
oraz User Stories Applied
4337ebf6db5c7cc89e4173803ef3875a
4
Dla Julii i Ellie, Zachary’ego i Elizabeth, Henry’ego i Stuarta
4337ebf6db5c7cc89e4173803ef3875a
4
4337ebf6db5c7cc89e4173803ef3875a
4
Spis treści
SŁOWO WSTĘPNE
PRZEDMOWA DO
Z
9
DRUGIEGO WYDANIA
PRZEDMOWY DO PIERWSZEGO WYDANIA
To jest Twoje życie .......................................................................... 26
Kot zjadł mój kod źródłowy ............................................................. 27
Entropia oprogramowania .............................................................. 30
Zupa z kamieni i gotowane żaby ..................................................... 33
Odpowiednio dobre oprogramowanie .............................................. 36
Portfolio wiedzy .............................................................................. 39
Komunikuj się!............................................................................... 45
2. POSTAWA PRAGMATYCZNA
8.
9.
10.
11.
12.
13.
14.
15.
53
Istota dobrego projektu .................................................................. 54
DRY — przekleństwo powielania..................................................... 56
Ortogonalność ................................................................................ 65
Odwracalność ................................................................................ 74
Pociski smugowe ............................................................................ 78
Prototypy i karteczki samoprzylepne............................................... 84
Języki dziedzinowe ......................................................................... 88
Szacowanie .................................................................................... 94
3. PODSTAWOWE NARZĘDZIA
16.
17.
18.
19.
20.
21.
22.
19
25
1. FILOZOFIA PRAGMATYCZNA
1.
2.
3.
4.
5.
6.
7.
13
101
Potęga zwykłego tekstu................................................................. 103
Powłoki ........................................................................................ 107
Efektywna edycja ......................................................................... 109
Kontrola kodu źródłowego ............................................................ 112
Debugowanie ............................................................................... 117
Operowanie na tekście ................................................................. 127
Dzienniki inżynierskie .................................................................. 130
4337ebf6db5c7cc89e4173803ef3875a
4
8 
Spis treści
4. PRAGMATYCZNA PARANOJA
23.
24.
25.
26.
27.
133
Projektowanie kontraktowe........................................................... 134
Martwe programy nie kłamią ........................................................ 143
Programowanie asertywne ............................................................ 145
Jak zrównoważyć zasoby .............................................................. 149
Nie prześcigaj swoich świateł ........................................................ 156
5. ZEGNIJ LUB ZŁAM
28.
29.
30.
31.
32.
161
Eliminowanie sprzężeń ................................................................. 162
Żonglerka realnym światem.......................................................... 170
Programowanie transformacyjne................................................... 180
Podatek od dziedziczenia .............................................................. 191
Konfiguracja................................................................................. 199
6. WSPÓŁBIEŻNOŚĆ
33.
34.
35.
36.
37.
203
Wszystko jest współbieżne ............................................................ 203
Eliminowanie związków czasowych............................................... 204
Współdzielony stan jest zły ........................................................... 209
Aktorzy i procesy.......................................................................... 216
Czarne tablice .............................................................................. 222
7. KIEDY KODUJEMY…
38.
39.
40.
41.
42.
43.
44.
45.
227
Słuchaj swojego jaszczurczego mózgu........................................... 228
Programowanie przez koincydencję............................................... 233
Szybkość algorytmu ..................................................................... 239
Refaktoryzacja.............................................................................. 245
Kod łatwy do testowania............................................................... 250
Testowanie na podstawie właściwości ........................................... 261
Pozostań w bezpiecznym miejscu.................................................. 267
Nazewnictwo ................................................................................ 275
8. PRZED PROJEKTEM
46.
47.
48.
49.
281
Kopalnia wymagań ....................................................................... 282
Rozwiązywanie niemożliwych do rozwiązania łamigłówek .............. 290
Praca zespołowa ........................................................................... 294
Istota zwinności ........................................................................... 297
9. PRAGMATYCZNE PROJEKTY
50.
51.
52.
53.
54.
303
Pragmatyczne zespoły................................................................... 304
Nie próbuj przecinać kokosów ...................................................... 310
Zestaw startowy pragmatyka ........................................................ 314
Wpraw w zachwyt użytkowników .................................................. 322
Duma i uprzedzenie ..................................................................... 324
POSŁOWIE
326
BIBLIOGRAFIA
329
MOŻLIWE ODPOWIEDZI DO ĆWICZEŃ
331
4337ebf6db5c7cc89e4173803ef3875a
4
Słowo wstępne
Pamiętam, kiedy Dave i Andy po raz pierwszy ogłosili na Twitterze informację
o nowym wydaniu tej książki. To był wielki news. Obserwowałem entuzjastyczną reakcję społeczności programistów. Efekt spełnił oczekiwania. Po dwudziestu
latach książka Pragmatyczny programista jest tak samo aktualna, jak jej pierwsze wydanie.
To, że książka z taką historią spotkała się z taką reakcją, jest bardzo znaczące.
Miałem przywilej czytania nieopublikowanej kopii po to, by napisać słowo
wstępne i zrozumiałem, dlaczego ta publikacja wywołała takie poruszenie. Chociaż jest to książka techniczna, nazywanie jej w taki sposób robi jej niedźwiedzią przysługę. Książki techniczne często odstraszają czytelników. Są wypełnione
po brzegi wielkimi słowami, niejasnymi terminami, zawiłymi przykładami. Możesz
czuć się zagubiony. Im bardziej doświadczony autor, tym łatwiej jest mu zapomnieć jak to jest, kiedy uczysz się nowych pojęć jako początkujący.
Pomimo dziesięcioleci doświadczeń w programowaniu Dave i Andy podołali trudnemu wyzwaniu pisania z emocjami właściwymi ludziom, którzy dopiero niedawno poznali temat. Nie traktują czytelników „z góry”. Nie zakładają, że jesteś
ekspertem. Nie zakładają nawet, że czytałeś pierwsze wydanie. Traktują Cię tak
jak powinni — jak programistę, który po prostu chce być lepszy. Poświęcili karty
tej książki, aby Ci pomóc takim się stać — krok po kroku.
Szczerze mówiąc, zrobili już to wcześniej. Pierwsze wydanie było pełne konkretnych przykładów, nowych pomysłów i praktycznych wskazówek, które budują
Twoją „programistyczną muskulaturę” i rozwijają Twój umysł — te przykłady
są nadal aktualne. Ale w niniejszej, zaktualizowanej edycji wprowadzono dwa
ulepszenia.
Pierwsze jest oczywiste: usunięto niektóre starsze odnośniki, przestarzałe przykłady i zastąpiono je świeżą, nowoczesną zawartością. Nie znajdziesz tu przykładów niezmienników pętli lub maszyn budowania. Dave i Andy zaktualizowali
wspaniałą zawartość pierwszego wydania i zadbali o to, aby opisywane zagadnienia były wolne od starych przykładów. Odkurzono w niej stare pomysły,
4337ebf6db5c7cc89e4173803ef3875a
4
10 
Słowo wstępne
takie jak zasada DRY (don’t repeat yourself — dosłownie: nie powtarzaj się)
i nałożono na nie świeżą warstwę farby, tak by na nowo zyskały połysk.
Ale to dzięki drugiemu ulepszeniu ta edycja jest naprawdę ekscytująca. Po napisaniu pierwszego wydania autorzy mieli okazję zastanowić się nad tym, co starali się powiedzieć, co chcieli, aby ich czytelnicy wyciągnęli z tej lektury oraz jaki
był tego odbiór. Na tej podstawie wyciągnęli właściwe wnioski. Zobaczyli, co nie
było najlepiej przekazane, co potrzebowało modyfikacji, jakie treści nie zostały
właściwie zrozumiane. W ciągu dwudziestu lat, przez które książka przeszła przez
ręce i serca programistów na całym świecie, Dave i Andy studiowali przekazywane im opinie i formułowali nowe pomysły oraz nowe koncepcje.
Nauczyli się znaczenia sprawczości i doszli do przekonania, że programiści mają
w sobie większe możliwości sprawcze w porównaniu z większością innych profesjonalistów. Zaczynają tę książkę od prostego, ale głębokiego komunikatu: „To
jest Twoje życie”. To przypomina o możliwościach tkwiących w naszej bazie
kodu, w naszej pracy oraz w naszych karierach. To zdanie ustawia ton dla pozostałej treści tej książki. Przypomina o tym, że jest ona czymś więcej niż tylko
kolejną książką techniczną wypełnioną przykładami kodu.
Tym, co naprawdę wyróżnia tę publikację spośród innych książek technicznych
jest to, że ta książka pozwala zrozumieć, co to znaczy być programistą. Programowanie to próba doprowadzenia do tego, aby przyszłość stała się mniej uciążliwa. Chodzi w nim o to, aby ułatwiało wykonywanie pracy naszym kolegom.
Chodzi o to, aby można było poprawić to, co jest robione źle. O tworzenie dobrych
nawyków. O zrozumienie zestawu narzędzi, jakim się posługujemy. Kodowanie
to tylko fragment świata programistów. Ta książka ten świat odkrywa.
Poświęciłem dużo czasu na myślenie o mojej „podróży” po krainie kodowania.
Nie wyssałem kodowania z mlekiem matki i nie nauczyłem się go na studiach.
Nie zajmowałem się zgłębianiem nauk technicznych, kiedy byłem nastolatkiem.
Wkroczyłem do świata kodowania kiedy byłem po dwudziestce i musiałem
nauczyć się, co to znaczy być programistą. Ta społeczność bardzo różni się od
innych, których byłem częścią. Obowiązuje w niej unikatowe poświęcenie się
nauce i praktyce; w obu przypadkach jest to zarówno odświeżające, jak i zatrważające.
Dla mnie wejście do społeczności programistów to tak, jakby wejść do nowego
świata. Albo przynajmniej zamieszkać w nowym mieście. Musiałem poznać
sąsiadów, znaleźć ulubiony sklep spożywczy, dowiedzieć się, gdzie są najlepsze
kawiarnie. Poznanie nowego krajobrazu, znalezienie najbardziej efektywnych
dróg, aby uniknąć zakorkowanych ulic, poznanie godzin, w których ruch osiąga
szczyt, zajęło mi trochę czasu. W nowym mieście pogoda jest inna, więc potrzebowałem nowej szafy.
Pierwsze tygodnie — a nawet miesiące — w nowym mieście mogą być zatrważające. Czy nie byłoby wspaniale mieć przyjaźnie nastawionego, znajomego sąsiada,
który mieszka tam już od jakiegoś czasu? Kogoś, kto mógłby zabrać Cię na
wycieczkę i pokazać najlepsze kawiarnie? Kto mieszka tam na tyle długo, że
4337ebf6db5c7cc89e4173803ef3875a
4
Słowo wstępne

11
zna kulturę, rozumie puls miasta. Dzięki komu mógłbyś nie tylko czuć się tam
tak, jak u siebie, ale mógłbyś także stać się aktywnym członkiem społeczności
mieszkańców? Dave i Andy są właśnie takimi sąsiadami.
Nowicjusza może łatwo przytłaczać nie sam akt programowania, ale proces
stawania się programistą. Musi nastąpić zmiana sposobu myślenia, a także
nawyków, zachowań i oczekiwań. Proces stawania się lepszym programistą nie
dzieje się dlatego, że wiesz, jak kodować; musisz świadomie dążyć do stosowania dobrych praktyk. Ta książka jest przewodnikiem wiodącym do wydajnego
stawania się lepszym programistą.
Niech Cię to jednak nie zmyli — nie znajdziesz tu odpowiedzi na pytanie o to,
jakie powinno być programowanie. Ta książka nie pokazuje filozofii programowania, nie przedstawia też tego rodzaju opinii. Powie Ci zaledwie, w prosty
i czytelny sposób, kim jest pragmatyczny programista — jak działa oraz jakie
stosuje podejście do kodu. Do Ciebie należy decyzja, czy chcesz zostać takim
programistą. Jeśli czujesz, że To nie jest dla Ciebie, nikt Cię do niczego nie zmusi.
Ale jeśli uznasz, że autorzy tej książki są Twoimi przyjaźnie nastawionymi sąsiadami, to oni pokażą Ci drogę.
Saron Yitbarek
Założyciel i dyrektor wykonawczy firmy CodeNewbie
Gospodarz podcastu „Command Line Heroes”
4337ebf6db5c7cc89e4173803ef3875a
4
12 
Słowo wstępne
4337ebf6db5c7cc89e4173803ef3875a
4
Przedmowa
do drugiego wydania
W latach dziewięćdziesiątych pracowaliśmy z firmami, które miały problemy ze
swoimi projektami. Mówiliśmy wszystkim to samo: może powinniście przetestować kod przed opublikowaniem? Dlaczego kod buduje się tylko na maszynie
Marii? Dlaczego nikt nie zapytał o zdanie użytkowników?
Aby zaoszczędzić czas spędzony z nowymi klientami, zaczęliśmy robić notatki.
Z tych notatek powstała książka Pragmatyczny programista. Ku naszemu zaskoczeniu okazała się strzałem w dziesiątkę. Przez 20 lat od jej wydania nadal jest
popularna.
Ale 20 lat to w przypadku oprogramowania wiele życiorysów. Weźcie programistę
z roku 1999 i włączcie go do współczesnego zespołu — będzie czuć się nieswojo
w tym dziwnym, nowym świecie. Ale świat lat dziewięćdziesiątych jest równie
obcy dla dzisiejszych programistów. Odniesienia w książce do takich technologii, jak CORBA, do narzędzi CASE oraz indeksowanych pętli byłyby w najlepszym razie jedynie ciekawostką, a co bardziej prawdopodobne, byłyby mylące.
Jednocześnie trzeba przyznać, że 20 minionych lat nie miało w ogóle wpływu
na sposób zdroworozsądkowego myślenia. Być może zmieniła się technologia,
ale nie zmienili się ludzie. Praktyki, które były dobre wtedy, pozostają dobre także
dziś. Ten aspekt książki dobrze się starzeje.
Kiedy nadszedł czas, aby przygotować niniejsze jubileuszowe wydanie, musieliśmy podjąć decyzję. Mogliśmy przejrzeć książkę i zaktualizować technologie,
do których się odwoływaliśmy. Drugim podejściem mogło być ponowne przyjęcie założeń dotyczących zalecanych praktyk w świetle dwóch dekad nowych
doświadczeń.
4337ebf6db5c7cc89e4173803ef3875a
4
14 
Przedmowa do drugiego wydania
Ostatecznie zrobiliśmy i jedno, i drugie.
W rezultacie ta książka jest czymś w rodzaju statku Tezeusza1. Mniej więcej
jedna trzecia tematów w niej jest całkowicie nowa. Większość z reszty została
przepisana — częściowo lub całkowicie. Naszym zamiarem było przedstawienie
tematów w sposób bardziej czytelny, wyraźniejszy i — mamy nadzieję — bardziej
ponadczasowy.
Podjęliśmy kilka trudnych decyzji. Usunęliśmy dodatek „Zasoby”, zarówno dlatego, że utrzymanie jego aktualności byłoby niemożliwe, jak i z tego powodu,
że łatwiej jest szukać tego, czego potrzebujemy. Zreorganizowaliśmy i przepisaliśmy tematy dotyczące współbieżności. Wzięliśmy przy tym pod uwagę obecną
obfitość współbieżnego sprzętu, jak i brak dobrych sposobów jego obsługi.
Dodaliśmy treści uwzględniające zmianę postaw i środowisk — począwszy od
ruchu Agile, do którego upowszechnienia się przyczyniliśmy, do rosnącej akceptacji idiomów programowania funkcyjnego oraz coraz większej potrzeby uwzględniania aspektów prywatności i bezpieczeństwa.
Co ciekawe, było między nami znacznie mniej dyskusji co do zawartości tego
wydania, w porównaniu z pierwszą edycją. Obaj czuliśmy, że łatwiej jest teraz
zidentyfikować tematy, które są ważne.
Niniejsza książka jest rezultatem naszej pracy. Życzymy przyjemnej lektury. Być
może warto zacząć stosować kilka nowych praktyk. Być może stwierdzicie, że
niektóre rzeczy, które proponujemy, są złe. Zachęcamy do zaangażowania się
w rozwój naszego rzemiosła. Podzielcie się z nami Waszymi opiniami.
Ale najważniejsze jest to, abyście mieli z tego frajdę.
36
Jak zorganizowano tę książkę
Ta książka jest zbiorem stosunkowo krótkich podrozdziałów. Każdy podrozdział
jest autonomiczny i dotyczy konkretnego zagadnienia. Poszczególne podrozdziały
zawierają też liczne odwołania, które znacznie ułatwiają postrzeganie prezentowanych zagadnień w szerszym kontekście. Zachęcamy do swobodnej lektury
tych podrozdziałów w dowolnej kolejności — tej książki nie trzeba czytać od
pierwszej do ostatniej strony.
Od czasu do czasu można natrafić na ramkę oznaczoną tytułem Wskazówka nr…
(na przykład Wskazówka nr 2, „Należy myśleć o tym, co się robi”). Oprócz zwracania szczególnej uwagi na pewne sugestie mamy wrażenie, że wskazówki w tej
1
Jeżeli w ciągu wielu lat napraw wadliwych części wymieniono wszystkie oryginalne
elementy statku, to czy w dalszym ciągu jest to ten sam statek?
4337ebf6db5c7cc89e4173803ef3875a
4
Co to z naczy?
 15
formie żyją własnym życiem — sami uwzględniamy je w codziennej pracy. Podsumowanie wszystkich wskazówek można znaleźć na wyciąganej karcie na końcu
tej książki.
Wszędzie tam, gdzie to było możliwe, proponowaliśmy dodatkowe ćwiczenia
i wyzwania. O ile odpowiedzi dla ćwiczeń w większości są stosunkowo proste,
wyzwania wymagają głębszego zastanowienia. Aby lepiej zilustrować nasz sposób myślenia, odpowiedzi do ćwiczeń zawarliśmy w dodatku, jednak chcemy
podkreślić, że tylko niewielka część tych zadań ma tylko jedno poprawne rozwiązanie. Wyzwania mogą stanowić podstawę dla dyskusji w szerszym gronie
lub być tematami rozpraw zadawanych słuchaczom zaawansowanych kursów
programowania.
Zamieściliśmy także krótką bibliografię, w której wymieniliśmy książki i artykuły, do których wyraźnie się odwołujemy.
37
Co to znaczy?
„Kiedy używam jakiegoś słowa — powiedział Humpty Dumpty dość
nonszalanckim tonem — oznacza ono tyle, co moim zdaniem ma
oznaczać, ani więcej, ani mniej”.
Lewis Carroll, Alicja po tamtej stronie lustra
W rozmaitych miejscach tej książki można znaleźć najróżniejsze przykłady żargonu — niektóre z nich są zupełnie prawidłowymi słowami języka polskiego,
którym całkiem niedawno nadano jakieś techniczne znaczenie, inne są raczej
przerażającymi zlepkami wyrazów wymyślonymi przez informatyków, którzy
zdają się nie zważać na piękno swojego języka. Kiedy po raz pierwszy używamy
każdego z tych określeń, staramy się je zdefiniować lub przynajmniej dać jakieś
wskazówki co do jego znaczenia. Mimo to jesteśmy pewni, że część niejasnych
określeń niezauważenie przedostała się przez to sito, a inne, jak „obiekt” czy
„relacyjna baza danych”, są na tyle popularne, że ich definiowanie byłoby po
prostu nudne. W razie napotkania nieznanego terminu w żadnym razie nie
należy go ignorować. Warto poświęcić trochę czasu na odnalezienie jego znaczenia, czy to w internecie, czy w innej książce informatycznej. Zachęcamy też
do informowania nas o podobnych niedopatrzeniach, tak aby w następnym
wydaniu nie zabrakło odpowiednich definicji.
Po tym wstępie możemy przyznać, że postanowiliśmy zrewanżować się informatykom. Istnieją doskonałe określenia stosowane w żargonie informatyków
i dobrze opisujące pewne pojęcia czy zjawiska, a mimo to zdecydowaliśmy o ich
ignorowaniu. Dlaczego? Ponieważ istniejący żargon zwykle jest kojarzony z konkretną dziedziną problemu lub fazą wytwarzania. Jednym z najważniejszych
założeń, które przyświecało nam podczas pisania tej książki, było proponowanie możliwie uniwersalnych technik — na przykład podział na moduły ma
4337ebf6db5c7cc89e4173803ef3875a
4
16 
Przedmowa do drugiego wydania
zastosowanie w kodzie, projekcie, dokumentacji i organizacji zespołu. Kiedy
próbowaliśmy używać typowego słowa żargonowego w szerszym kontekście,
nasze intencje stawały się niejasne — nie mogliśmy poradzić sobie z bagażem
oryginalnego kontekstu. W każdym takim przypadku postanawialiśmy zwiększyć swój wkład w upadek języka i wymyślaliśmy własne terminy.
38
Kod źródłowy i inne zasoby
Większość kodu źródłowego prezentowanego w tej książce pochodzi z plików
źródłowych gotowych do kompilacji, które są dostępne do pobrania pod adresem:
ftp://ftp.helion.pl/przyklady/pragp2.zip
Znajdziecie także łącza do zasobów, które uważamy za przydatne, a także aktualizacje książki i wiadomości dotyczące innych przedsięwzięć związanych
z książką Pragmatyczny programista.
39
Prześlij nam opinię
Będziemy wdzięczni za przesyłanie opinii na temat książki. Można wysłać do nas
e-mail pod adres redakcja@helion.pl.
40
Podziękowania do wydania drugiego
W ciągu ostatnich 20 lat przeprowadziliśmy dosłownie tysiące ciekawych rozmów o programowaniu. Spotykaliśmy ludzi na konferencjach, na kursach, a czasami nawet na pokładach samolotów. Każda z tych rozmów poprawiła nasze
zrozumienie procesu programowania i przyczyniła się do aktualizacji wprowadzonych w tym wydaniu. Dziękujemy Wam wszystkim (a jeśli jesteśmy w błędzie, dalej mówcie nam o tym).
Dziękujemy uczestnikom procesu beta książki. Wasze pytania i komentarze
pomogły nam lepiej wyjaśnić niektóre zagadnienia.
Zanim doszliśmy do wersji beta, udostępniliśmy książkę kilku osobom z prośbą
o opinie. Dziękujemy VM (Vicky) Brasseur, Jeffowi Langrowi i Kim Shrier za
szczegółowe komentarze, a także José Valimowi i Nickowi Cuthbertowi za opinie
techniczne.
Dziękujemy Ronowi Jeffriesowi za pozwolenie skorzystania z przykładu Sudoku.
Jesteśmy wdzięczni pracownikom Wydawnictwa Pearson, którzy zgodzili się,
abyśmy stworzyli tę książkę na swój sposób.
4337ebf6db5c7cc89e4173803ef3875a
4
Podziękowan ia do wydania drugiego
 17
Specjalne podziękowania kierujemy dla nieocenionej Janet Furlow, która w mistrzowski sposób kieruje wszystkim, co robi, i która dopilnowała, żebyśmy
dotrzymali terminu.
Na koniec dziękujemy wszystkim pragmatycznym programistom, dzięki którym programowanie w ciągu ostatnich dwudziestu lat stało się lepsze dla
wszystkich. Mamy nadzieję, że niniejsze wydanie zapoczątkuje kolejne takie
dwadzieścia lat.
4337ebf6db5c7cc89e4173803ef3875a
4
18 
Przedmowa do drugiego wydania
4337ebf6db5c7cc89e4173803ef3875a
4
Z przedmowy
do pierwszego wydania
Ta książka pomoże czytelnikowi zostać lepszym programistą.
Nie ma znaczenia, czy czytelnik jest wolnym strzelcem, członkiem wielkiego
zespołu projektowego, czy konsultantem równocześnie współpracującym z wieloma klientami. Ta książka pomoże każdemu w lepszym wykonywaniu swojej
pracy. Ta książka nie jest zbiorem teorii — koncentrujemy się raczej na tematach praktycznych, na efektywnym wykorzystywaniu własnych doświadczeń do
podejmowania lepszych decyzji. Słowo pragmatyczny pochodzi od łacińskiego
wyrazu pragmaticus („sprawny w działaniu”), który z kolei pochodzi od greckiego
słowa pragmatikós oznaczającego „do zrobienia”.
Ta książka jest właśnie o robieniu.
Programowanie jest rzemiosłem. W najprostszej postaci sprowadza się do zmuszania komputera do robienia tego, czego chcemy (lub czego chce użytkownik).
Jako programiści jesteśmy po części słuchaczami, po części doradcami, po części tłumaczami i po części dyktatorami. Próbujemy gromadzić ulotne, trudne do
sformułowania wymagania i znajdować sposoby ich wyrażania w sposób zrozumiały dla zwykłej maszyny. Staramy się tak dokumentować naszą pracę, aby
inni mogli ją zrozumieć, i jednocześnie próbujemy stosować metody inżynierskie, tak aby na bazie naszych dokonań inni mogli budować własne rozwiązania. Co więcej, próbujemy robić to wszystko wbrew bezlitosnym wskazówkom
zegara bieżącego projektu. Każdego dnia dokonujemy małych cudów.
To trudna praca.
Wiele osób oferuje nam pomoc. Twórcy narzędzi przekonują o niewiarygodnych
możliwościach swoich produktów. Specjaliści od metodyk obiecują, że ich techniki gwarantują doskonałe efekty. Każdy twierdzi, że jego język programowania
4337ebf6db5c7cc89e4173803ef3875a
4
20 
Z przedmowy do pierwszego wydania
jest najlepszy i że jego system operacyjny jest pierwszą skuteczną odpowiedzią
na wszystkie znane choroby.
Żadne z tych zapewnień oczywiście nie jest prawdziwe. Nie istnieją proste
odpowiedzi. Nie istnieje jedno najlepsze rozwiązanie, czy to w formie narzędzia,
języka programowania bądź systemu operacyjnego. Mogą co najwyżej istnieć
systemy, które w konkretnych okolicznościach sprawdzają się lepiej od konkurencyjnych produktów.
Właśnie tutaj potrzebny jest pragmatyzm. Nie powinniśmy wiązać swojej kariery
z żadną konkretną technologią — musimy raczej dbać o stały rozwój swojej wiedzy i gromadzenie doświadczeń niezbędnych do wybierania właściwych rozwiązań w różnych sytuacjach. Nasza wiedza wynika z rozumienia podstawowych
zasad informatyki, zaś nasze doświadczenie bierze się z wielu różnych praktycznych projektów. O naszej sile decyduje połączenie teorii i praktyki.
Musimy dostosowywać swoje postępowanie do bieżących okoliczności i środowiska, w którym aktualnie pracujemy. Musimy rozstrzygać względne znaczenie wszystkich czynników wpływających na projekt i wybierać najwłaściwsze
rozwiązania na podstawie swoich doświadczeń. Co więcej, musimy robić to nieustannie wraz z postępem prac nad projektem. Pragmatyczni programiści doprowadzają sprawy do końca i robią to dobrze.
36
Kto powinien przeczytać tę książkę?
Książka jest kierowana do programistów zainteresowanych poprawą swojej efektywności i produktywności. Część programistów jest sfrustrowana wrażeniem
niepełnego wykorzystania swojego potencjału. Inni programiści z zazdrością
obserwują kolegów po fachu, którzy sprawiają wrażenie, jakby korzystali
z narzędzi zapewniających wyższą wydajność. Jeszcze inni używają obecnie
starszych technologii i chcą wiedzieć, jak nowe rozwiązania i koncepcje sprawdziłyby się w ich pracy.
Nie udajemy, że znamy wszystkie (ani nawet większość) odpowiedzi. Nie twierdzimy też, że nasze pomysły sprawdzają się we wszystkich sytuacjach. Możemy
za to zagwarantować, że postępowanie według naszych zaleceń pozwoli błyskawicznie zdobywać nowe doświadczenia, podniesie produktywność programisty
i umożliwi lepsze rozumienie całego procesu wytwarzania. Czytelnik będzie
też pisał lepsze oprogramowanie.
4337ebf6db5c7cc89e4173803ef3875a
4
Co decyduje o byciu pragmatycznym programistą?
37
 21
Co decyduje o byciu
pragmatycznym programistą?
Każdy programista jest inny, ma własne mocne strony i słabości, preferencje
i uprzedzenia. Z czasem każdy programista wypracowuje także własne środowisko pracy. Wspomniane środowisko odzwierciedla indywidualne cechy programisty równie mocno jak jego hobby, ubiór czy fryzura. Pragmatyczni programiści
mają jednak pewne cechy wspólne (a przynajmniej większość z wymienionych
poniżej):

Szybkie sprawdzanie nowinek, błyskawiczne dostosowywanie warsztatu. Pragmatyczni programiści instynktownie poszukują nowych technologii i technik — wprost uwielbiają eksperymentować z nowinkami. Kiedy
tylko trafi w ich ręce coś nowego, potrafią błyskawicznie opanować nowe
rozwiązania i zintegrować je z resztą swojej wiedzy. Ocena poszczególnych
nowości wynika z doświadczenia.

Dociekliwość. Pragmatyczni programiści zadają pytania. To ciekawe — jak
to zrobiłeś? Miałeś jakieś problemy z tą biblioteką? Czym właściwie jest
ten BeOS, o którym tyle słyszałem? Jak zaimplementowano dowiązania
symboliczne? Pragmatyczny programista jest prawdziwym kolekcjonerem
faktów — każda taka informacja może wpłynąć na jego decyzję wiele lat po
jej zdobyciu.

Krytyczne myślenie. Pragmatyczni programiści rzadko akceptują
otrzymywane informacje bez zapoznania się z faktami. Kiedy nasi koledzy mówią „tak to jest zrobione i już” lub kiedy jakiś producent obiecuje
rozwiązanie wszystkich naszych problemów, od razu wiemy, że trzeba to
dobrze sprawdzić.

Realizm. Pragmatyczni programiści próbują zrozumieć naturę każdego
problemu, z którym muszą się zmierzyć. Realizm pozwala nam dość dobrze
szacować trudność poszczególnych zadań i — tym samym — czas trwania
planowanych czynności. Świadomość poziomu złożoności procesu i czasu
potrzebnego do jego zakończenia pozwala nam wytrwale dążyć do celu.

Gotowość do nowych wyzwań. Pragmatyczni programiści starają się
poznawać najróżniejsze technologie i środowiska. Robią, co w ich mocy, aby
na bieżąco poznawać nowe technologie i modele wytwarzania. Nawet jeśli
aktualny projekt wymaga specjalizacji w określonej dziedzinie, pragmatyczni programiści zawsze są gotowi do pracy w odmiennych obszarach
i przyjmowania nowych wyzwań.
Najbardziej podstawowe cechy zostawiliśmy sobie na koniec. Te dwie cechy łączą
wszystkich pragmatycznych programistów. Są na tyle proste, że można je wyrazić w formie wskazówek:
4337ebf6db5c7cc89e4173803ef3875a
4
22 
Z przedmowy do pierwszego wydania
WSKAZÓWKA NR 1
Należy dbać o swoje rzemiosło.
Uważamy, że tworzenie oprogramowania nie ma najmniejszego sensu, jeśli programista nie dba o jakość swoich produktów.
WSKAZÓWKA NR 2
Należy myśleć o tym, co się robi.
Warunkiem bycia pragmatycznym programistą jest ustawiczne myślenie o tym,
co się robi, przede wszystkim w trakcie tych czynności. Nie chodzi o jednorazowy audyt bieżących praktyk — powinniśmy raczej krytycznie oceniać każdą podejmowaną decyzję w codziennej pracy i podczas wszystkich czynności
związanych z wytwarzaniem. Nigdy nie możemy pozwolić sobie na lot z włączonym autopilotem. Musimy stale myśleć i krytycznie oceniać swoją pracę w czasie
rzeczywistym. Stare motto korporacyjne obowiązujące w firmie IBM, MYŚL!,
jest też mantrą pragmatycznych programistów.
Jeśli ustawiczne ocenianie własnych decyzji wydaje nam się trudne, możemy
być niemal pewni, że spełniamy warunek realizmu. Myślenie rzeczywiście będzie
wymagało trochę cennego czasu — czasu, który już teraz jest przedmiotem
poważnych nacisków. Nagrodą będzie jeszcze większe zaangażowanie w pracę,
którą kochamy, świadomość świetnej znajomości coraz większej liczby zagadnień oraz przyjemne uczucie ciągłego doskonalenia umiejętności. Z czasem
zainwestowany czas zwróci się z nawiązką, kiedy my i nasz zespół staniemy
się bardziej efektywni, tworzony przez nas kod będzie łatwiejszy w konserwacji,
a my sami będziemy tracili dużo mniej czasu na nudnych spotkaniach.
38
Pojedynczy pragmatycy,
wielkie zespoły
Niektórzy sądzą, że w wielkich zespołach lub podczas realizacji złożonych projektów nie ma miejsca na indywidualności. „Tworzenie oprogramowania to
zadanie typowo inżynierskie, którego realizacja jest niemożliwa, jeśli poszczególni członkowie zespołu sami podejmują decyzje”.
To nieprawda.
Budowa oprogramowania rzeczywiście powinna być przedsięwzięciem inżynierskim. Inżynierski charakter projektu nie wyklucza jednak rzemiosła członków
zespołu projektowego. Warto przywołać przykład wielkich katedr budowanych
w średniowiecznej Europie. Każda z nich wymagała tysięcy roboczolat, a budowa
4337ebf6db5c7cc89e4173803ef3875a
4
To proces bez końca
 23
jednego obiektu nierzadko zajmowała wiele dekad. Lekcje z kolejnych etapów
były wykorzystywane przez następne zastępy budowniczych, których osiągnięcia stopniowo przyczyniały się do rozwoju dziedziny mechaniki konstrukcji.
Stolarze, kamieniarze, rzeźbiarze i szklarze byli jednak rzemieślnikami, którzy
na swój sposób interpretowali wymagania inżynierskie, aby na tej podstawie
stworzyć pewną całość — dzieło nieporównanie ciekawsze od czysto mechanicznego aspektu konstrukcji. O sukcesie całych projektów decydowała wiara
budowniczych w znaczenie ich indywidualnego wkładu: My, którzy wydobywamy zwykłe kamienie, zawsze musimy mieć przed oczami katedry.
W ramach ogólnej struktury projektu zawsze istnieje przestrzeń dla indywidualności i rzemiosła. Możliwości w tym względzie są szczególnie widoczne na bieżącym etapie rozwoju inżynierii oprogramowania. Nawet jeśli za sto lat nasze
współczesne techniki będą wyglądały równie archaicznie co metody stosowane
przez średniowiecznych budowniczych katedr w oczach współczesnych inżynierów budownictwa, nasze rzemiosło wciąż będzie doceniane.
39
To proces bez końca
Turysta zwiedzający angielski Eton College zapytał ogrodnika, jak to możliwe, że trawa jest zawsze tak równo skoszona.
— To proste — odpowiedział — wystarczy lekko podlewać codziennie rano,
kosić co drugi dzień i walcować raz w tygodniu.
— To wszystko? — zapytał zdziwiony turysta.
— Oczywiście — odrzekł ogrodnik. — Rób tak przez 500 lat, a też będziesz
miał piękny trawnik.
Piękne trawniki wymagają prostych codziennych, choćby niewielkich, nakładów
pracy — tak samo jest ze świetnymi programistami. Konsultanci zajmujący
się zarządzaniem lubią mówić o ciągłym doskonaleniu (jap. kaizen). Kaizen to
japoński termin określający ustawiczne wprowadzanie drobnych udoskonaleń.
Uważa się, że właśnie ta filozofia jest jednym z powodów ogromnego wzrostu
produktywności i jakości japońskiego przemysłu, stąd jej powszechne powielanie na całym świecie. Filozofia kaizen ma zastosowanie także w przypadku
jednostek. Wystarczy codziennie pracować nad doskonaleniem swoich dotychczasowych umiejętności i uzupełniać swój warsztat o nowe narzędzia. Inaczej
niż w przypadku trawników w Eton, pierwsze efekty będą widoczne już w ciągu
kilku dni. Po latach ze zdziwieniem odkryjemy wprost niewiarygodny wzrost
swojego doświadczenia i poprawę umiejętności.
4337ebf6db5c7cc89e4173803ef3875a
4
24 
Z przedmowy do pierwszego wydania
4337ebf6db5c7cc89e4173803ef3875a
4
Rozdział 1.
Filozofia pragmatyczna
Ta książka jest o Tobie.
Nie popełnij błędu. Tu chodzi o Twoją karierę, a co ważniejsze, to jest Twoje
życie. Należy do Ciebie. Czytasz tę książkę, bo wiesz, że możesz stać się lepszym
programistą, a także pomagać innym, aby również stali się lepszymi. Możesz stać
się pragmatycznym programistą.
Co wyróżnia pragmatycznych programistów? Czujemy, że pragmatyzm to postawa, styl, filozofia postrzegania i rozwiązywania problemów. Pragmatyczni programiści wykraczają myślami poza bieżące, aktualnie rozwiązywane problemy,
stale próbując sytuować te problemy w szerszym kontekście, aby dysponować
pełnym obrazem analizowanej rzeczywistości. Czy bez świadomości tego szerszego kontekstu w ogóle możemy być pragmatyczni? Jak w takiej sytuacji mielibyśmy wypracowywać inteligentne kompromisy i podejmować świadome decyzje?
Innym kluczem do sukcesu pragmatycznych programistów jest gotowość do brania odpowiedzialności za wszystko, co robią — to zagadnienie zostanie omówione w podrozdziale „Kot zjadł mój kod źródłowy”. Odpowiedzialność oznacza,
że pragmatyczni programiści nie siedzą bezczynnie, obserwując, jak ich projekty
zmierzają ku nieuchronnej klęsce. W podrozdziale „Entropia oprogramowania”
zostaną omówione sposoby dbania o nieskazitelność projektów.
Większość ludzi z trudem akceptuje zmiany — niechęć do zmian w pewnych
przypadkach jest w pełni uzasadniona, ale nierzadko wynika ze zwykłego marazmu. W podrozdziale „Zupa z kamieni i gotowane żaby” przeanalizujemy strategię inspirowania zmian i przedstawimy (dla równowagi) pouczającą opowieść
o płazie, który ignorował niebezpieczeństwa związane ze stopniowymi zmianami.
Jedną z korzyści wynikających ze znajomości i rozumienia kontekstu, w którym pracujemy, jest łatwiejsza ocena tego, na ile dobre musi być tworzone przez
nas oprogramowanie. W pewnych przypadkach jedynym akceptowanym stanem
jest jakość bliska perfekcji, ale często możliwe są daleko idące kompromisy.
4337ebf6db5c7cc89e4173803ef3875a
4
26 
Rozdział 1.
Filozofia pragmatyczna
Tym zagadnieniem zajmiemy się w podrozdziale „Odpowiednio dobre oprogramowanie”.
Zapanowanie nad tym wszystkim wymaga, oczywiście, szerokiej wiedzy i sporego doświadczenia. Uczenie się jest typowym przykładem procesu ciągłego, który
nigdy się nie kończy. W podrozdziale „Portfolio wiedzy” omówimy pewne strategie zachowywania właściwego tempa zdobywania wiedzy i umiejętności.
I wreszcie, nikt z nas nie pracuje w próżni. Wszyscy spędzamy znaczną część
swojego czasu na interakcji ze współpracownikami. W podrozdziale „Komunikuj się!” zostaną omówione sposoby doskonalenia zasad współpracy.
Programowanie pragmatyczne rozciąga się od filozofii do myślenia pragmatycznego. W tym rozdziale skoncentrujemy się na podstawach filozofii.
1
36
To jest Twoje życie
Nie żyję w tym świecie, aby sprostać Waszym oczekiwaniom, a Wy nie
żyjecie w nim po to, by sprostać moim.
Bruce Lee
To jest Twoje życie. Należy do Ciebie. Ty sam je przeżywasz. Ty sam je tworzysz.
Rozmawiamy z wieloma sfrustrowanymi programistami. Mają różne punkty
widzenia. Niektórzy odczuwają, że w ich pracę wkradła się stagnacja, inni —
że technologia przeszła obok nich. Ludzie czują się niedostatecznie doceniani
albo niedostatecznie opłacani, albo że ich zespoły są toksyczne. Niektórzy chcieliby przenieść się do Azji lub Europy, albo pracować z domu.
Nasza odpowiedź jest zawsze taka sama.
„Dlaczego tego nie zmienisz?”.
Wytwarzanie oprogramowania powinno znaleźć się blisko szczytu listy karier,
nad którymi masz kontrolę. Nasze umiejętności są poszukiwane, nasza wiedza
przekracza granice geograficzne, możemy pracować zdalnie. Jesteśmy dobrze
opłacani. Naprawdę możemy zrobić niemal wszystko, czego chcemy.
Ale z jakiegoś powodu programiści są niechętni zmianom. Zamykają się w sobie
w nadziei, że sytuacja się poprawi. Biernie godzą się na to, że ich umiejętności
stają się przestarzałe i skarżą się, że ich firmy nie wysyłają ich na szkolenia.
Jadąc autobusem przyglądają się ogłoszeniom z egzotycznych miejsc, a następnie wychodzą z autobusu na chłód i deszcz i brną do pracy.
Oto najważniejsza wskazówka w tej książce.
4337ebf6db5c7cc89e4173803ef3875a
4
Kot zjadł mój kod źródłowy
 27
WSKAZÓWKA NR 3
Masz w sobie możliwości sprawcze.
Denerwuje Cię środowisko, w którym pracujesz? Twoja praca jest nudna? Spróbuj temu zaradzić. Ale nie próbuj w nieskończoność. Jak mówi Martin Fowler:
„Można zmienić firmę lub zmienić swoją firmę”1.
Jeśli wydaje Ci się, że technologia przechodzi obok Ciebie, znajdź czas (w swoim
czasie wolnym) na studiowanie nowych zagadnień, które wydają się interesujące.
Inwestujesz w siebie, więc robienie tego w czasie wolnym jest bardzo rozsądne.
Chcesz pracować zdalnie? A czy zapytałeś? Jeśli powiedzą Ci „nie”, to znajdź
kogoś, kto powie Ci „tak”.
Ta branża daje bardzo wiele szans. Bądź proaktywny i z nich skorzystaj.
Pokrewne podrozdziały
2
37

Temat 4., „Zupa z kamieni i gotowane żaby”.

Temat 6., „Portfolio wiedzy”.
Kot zjadł mój kod źródłowy
Największą słabością jest strach przed wyglądaniem na słabego.
J.B. Bossuet, Politics from Holy Writ, 1709
Jednym z największych elementów filozofii pragmatycznej jest idea brania odpowiedzialności zarówno za siebie, jak i za skutki podejmowanych przez siebie
działań (w wymiarze całej kariery, bieżącego projektu i codziennej pracy). Pragmatyczny programista bierze we własne ręce losy swojej kariery i nie boi się
przyznać do braku wiedzy czy popełnionego błędu. Przyznawanie się do usterek
z pewnością nie jest najprzyjemniejszym aspektem programowania, ale błędy są
nieodłączną częścią tej pracy (nawet w najlepszych projektach). Błędy zdarzają
się mimo gruntownych testów, dobrej dokumentacji i właściwie zaplanowanej
automatyzacji. Dotrzymanie terminu bywa niewykonalne. Programiści napotykają nieprzewidywalne problemy techniczne.
Wspomniane zjawiska po prostu się zdarzają, a rolą programistów jest możliwie profesjonalne radzenie sobie w trudnych sytuacjach. W tym przypadku
profesjonalizm wymaga uczciwego i bezpośredniego stawiania sprawy. Mimo
dumy ze swoich umiejętności musimy mieć odwagę uczciwego przyznawania się
do słabszych stron, w tym braku wiedzy oraz popełnianych błędów.
1
http://wiki.c2.com/?ChangeYourOrganization
4337ebf6db5c7cc89e4173803ef3875a
4
28 
Rozdział 1.
Filozofia pragmatyczna
Zaufanie zespołu
Przede wszystkim Twój zespół musi być w stanie Ci zaufać i na Tobie polegać.
Ty także powinieneś móc polegać na każdym członku Twojego zespołu. Według
literatury naukowej2 zaufanie w zespole jest absolutnie niezbędne dla zapewnienia kreatywności i współpracy. W zdrowym, opartym na zaufaniu środowisku, można bezpiecznie mówić to, co się myśli, przedstawiać swoje pomysły
i polegać na członkach zespołu, którzy mogą z kolei polegać na Tobie. Co by
było, gdyby nie było zaufania? Cóż…
Wyobraźmy sobie, że tajny zespół high-tech ninja pracuje zawzięcie nad infiltracją siedziby złoczyńcy. Po miesiącach planowania i prób uruchamiania kodu
wreszcie wszystko jest gotowe. Teraz Twoja kolej, aby skonfigurować siatkę
naprowadzania laserowego. Mówisz: „Przepraszam was, ale nie mam lasera. Kot
bawił się czerwoną kropką i zostawiłem laser w domu”.
Tego rodzaju nadużycie zaufania może być trudne do naprawienia.
Bierz odpowiedzialność
Odpowiedzialność to cecha powszechnie uznawana za pożądaną. Mimo zaangażowania i dbałości o możliwie najlepszą realizację zadania nie zawsze mamy
pełną kontrolę nad wszystkimi aspektami naszej pracy. Oprócz jak najlepszego
wykonywania własnych działań musimy więc analizować sytuację pod kątem
czynników ryzyka wykraczających poza naszą kontrolę. Odrzucenie odpowiedzialności jest usprawiedliwione tylko wtedy, gdy sytuacja uniemożliwia nam
prawidłową realizację zadania lub gdy czynniki ryzyka są zbyt poważne. Ocena
należy do samego programisty i zależy od jego zasad etycznych i osądu sytuacji.
Jeśli bierzemy na siebie odpowiedzialność za efekt podejmowanych działań,
musimy być przygotowani na wszelkie konsekwencje. W razie popełnienia błędu
(a wszyscy je popełniamy) lub błędnej oceny sytuacji musimy uczciwie przyznać
się do porażki i zaproponować rozwiązanie.
Nie należy winić za własne niedociągnięcia współpracowników ani innych czynników. Nie powinniśmy też szukać usprawiedliwień. Nie należy zrzucać winy
za wszystkie problemy na producentów narzędzi, język programowania, przełożonych ani współpracowników. Każdy z tych elementów mógł, oczywiście, przyczynić się do powstałej sytuacji, jednak rolą programisty nie jest szukanie
usprawiedliwień, tylko tworzenie rozwiązań.
Jeśli istnieje ryzyko niedostarczenia niezbędnych składników przez kogoś innego,
należy przygotować odpowiedni plan awaryjny. Jeśli wskutek awarii dysku programista traci cały kod źródłowy i jeśli nie dysponuje kopią zapasową, wina
2
Wartościową metaanalizę dotyczącą zaufania i wydajności zespołów można znaleźć
w artykule: A meta-analysis of main effects, moderators, and covariates, http://dx.doi.
org/10.1037/apl0000110.
4337ebf6db5c7cc89e4173803ef3875a
4
Kot zjadł mój kod źródłowy
 29
leży wyłącznie po jego stronie. Problemu nie da się rozwiązać, wmawiając szefowi, że kod źródłowy został zjedzony przez kota.
WSKAZÓWKA NR 4
Proponuj rozwiązania, zamiast posługiwać się kiepskimi wymówkami.
Zanim udamy się do kogokolwiek, aby tłumaczyć, dlaczego wykonanie jakiejś
czynności jest niemożliwe, dlaczego nie możemy dotrzymać terminu lub dlaczego
coś nie spełnia początkowych wymagań, warto zatrzymać się na chwilę i wsłuchać się we własne wyjaśnienia. Warto porozmawiać z gumową kaczką na
monitorze, kotem lub czymkolwiek innym. Czy te wyjaśnienia brzmią logicznie,
czy po prostu głupio? Jak będą brzmiały dla naszego przełożonego?
Warto przeprowadzić tę rozmowę we własnej głowie. Jakiej odpowiedzi spodziewamy się po rozmówcy? Czy zapyta: „Próbowałeś tego? Nie pomyślałeś o tym”?
Jak wtedy odpowiemy? Czy możemy zrobić coś jeszcze, zanim udamy się do
przełożonego ze złymi nowinami? W pewnych przypadkach z góry wiadomo,
co powie przełożony, zatem warto oszczędzić mu zmartwień.
Zamiast usprawiedliwień należy raczej przygotować propozycje rozwiązań. Nie
należy mówić o tym, czego nie można zrobić — powinniśmy raczej koncentrować
się na tym, co zrobić, aby wyjść z kłopotliwej sytuacji. Czy porzucenie dotychczasowego kodu rzeczywiście jest konieczne? Warto przedstawić rozmówcy zalety
refaktoryzacji (patrz temat 40., „Refaktoryzacja”).
Czy planujemy poświęcić czas na przygotowanie prototypu, który ułatwi nam
wypracowanie najlepszego rozwiązania (patrz temat 13., „Prototypy i karteczki
samoprzylepne”)? Czy w przyszłości będzie można uniknąć podobnych sytuacji, jeśli zostaną wdrożone lepsze procedury testowania (patrz temat 41., „Kod
łatwy do testowania” i podrozdział „Bezlitosne testy”) lub rozwiązania w zakresie
automatyzacji.
Być może właściwym rozwiązaniem będzie zapewnienie dodatkowych zasobów.
A może trzeba poświęcić więcej czasu użytkownikom? A może wszystko leży
w Twoich rękach: być może powinieneś dokładniej nauczyć się jakiejś techniki lub technologii? Może trzeba sięgnąć po książkę lub zapisać się na kurs?
Programista nie powinien obawiać się zadawania pytań ani przyznawania do
tego, że potrzebuje pomocy.
Warto próbować eliminować kiepskie wymówki jeszcze przed ich głośnym wypowiedzeniem. A jeśli odczuwamy nieodpartą potrzebę ich wyrażenia, przedstawmy
te usprawiedliwienia raczej swojemu kotu. Skoro mały Mruczek jest skłonny
wziąć winę na siebie, dlaczego nie skorzystać z tej okazji…
Pokrewne podrozdziały

Temat 49., „Pragmatyczne zespoły”.
4337ebf6db5c7cc89e4173803ef3875a
4
30 
Rozdział 1.
Filozofia pragmatyczna
Wyzwania
3
38

Jak reagujemy na kiepskie wymówki stosowane przez innych (pracowników banku, mechaników samochodowych, urzędników itp.) podczas rozmowy z nami? Jaki jest wpływ tych wymówek na naszą ocenę rozmówców
i organizacji, w których pracują?

Gdy kiedykolwiek znajdziesz się w sytuacji, że będziesz zmuszony odpowiedzieć: „Nie wiem”, pamiętaj, aby dodać „ale się dowiem”. To świetny sposób, aby się przyznać, że czegoś się nie wie, a następnie by przyjąć na siebie
odpowiedzialność, tak jak powinien postąpić profesjonalista.
Entropia oprogramowania
Chociaż wytwarzanie oprogramowania jest odporne na niemal wszystkie prawa
fizyki, akurat zjawisko entropii jest dla programistów aż nadto odczuwalne.
Entropia to termin zaczerpnięty z fizyki i opisujący stopień nieokreśloności,
chaotyczności w systemie. Prawa termodynamiki mówią, niestety, że entropia
we wszechświecie zmierza do osiągnięcia pewnego maksimum. Kiedy zjawisko
chaosu zaczyna nasilać się w świecie oprogramowania, mamy do czynienia ze
zjawiskiem określanym mianem rozkładu oprogramowania (ang. software rot).
Niektórzy to samo zjawisko określają bardziej optymistycznym terminem „dług
techniczny”, z domniemanym założeniem, że pewnego dnia go spłacą. Prawdopodobnie im się to nie uda.
Niezależnie od nazwy, zarówno dług techniczny, jak i rozkład oprogramowania,
mogą nasilać się w niekontrolowany sposób.
Istnieje wiele czynników, które mogą się składać na rozkład oprogramowania.
Bodaj najważniejszym czynnikiem są cechy psychologiczne (lub kulturowe) osób
zaangażowanych w realizację projektu. Nawet w przypadku jednoosobowego
„zespołu” psychologia projektu może okazać się bardzo delikatna. Nawet najlepsze plany i najlepsi ludzie nie wystarczą do zabezpieczenia projektu przed
ruiną i upadkiem w całym czasie życia. Co ciekawe, istnieją projekty, które
wbrew licznym utrudnieniom i mimo pasma niepowodzeń zadziwiająco
skutecznie radzą sobie z naturalnym dążeniem do nieporządku i doskonale
znoszą upływ czasu.
Co decyduje o tej różnicy?
W centrach miast tylko część budynków jest pięknych i zadbanych, podczas
gdy pozostałe to rozpadające się rudery. Dlaczego? Badacze zjawiska przestępczości i degradacji życia społecznego w miastach odkryli bardzo ciekawy mechanizm, który może sprawić, że czysty, zadbany, zamieszkany budynek błyskawicznie pustoszeje i zaczyna niszczeć3.
3
Patrz The police and neighborhood safety [WH82].
4337ebf6db5c7cc89e4173803ef3875a
4
Entropia oprogramowania
 31
Wystarczy jedna wybita szyba.
Jedna wybita szyba pozostawiona nienaprawiona przez dłuższy czas powoduje, że mieszkańcy zaczynają traktować swój dom jako opuszczone, porzucone miejsce, które nie może liczyć na właściwą opiekę wyznaczonych do tego
podmiotów. Niedługo potem zostaje zbita kolejna szyba. Ludzie zaczynają pozostawiać śmieci w przypadkowych miejscach. Na murach pojawia się graffiti.
Po jakimś czasie budynek jest już narażony na poważne uszkodzenia strukturalne. W stosunkowo krótkim okresie budynek znajduje się w stanie, w którym przywrócenie dawnej świetności traci sens ekonomiczny, zatem pierwotne
wrażenie porzucenia staje się rzeczywistością.
Dlaczego ta jedna wybita szyba stwarza tak wielką różnicę? Z badań psychologów4 wynika, że pokazanie stanu beznadziejności może być zaraźliwe. Weźmy
za przykład rozprzestrzenianie się wirusa grypy w dużych skupiskach ludzkich.
Ignorowanie wyraźnie nieprawidłowej sytuacji wzmacnia pogląd, że prawdopodobnie nic nie można naprawić, że nikogo to nie interesuje, wszystko jest przesądzone. Pojawiają się negatywne myśli, które mogą rozprzestrzeniać się między
członkami zespołu, tworząc błędne koło.
WSKAZÓWKA NR 5
Nie akceptuj żadnej wybitej szyby.
Nie należy pozostawiać nienaprawionej żadnej wybitej szyby (złego projektu,
niewłaściwej decyzji, kiepskiego kodu itp.). Każdą taką „szybę” należy wymienić
zaraz po odkryciu problemu. Jeśli brakuje czasu na wstawienie nowej szyby,
należy wstawić w jej miejsce choćby dyktę. W większości przypadków można
sobie pozwolić na umieszczenie problematycznego kodu w komentarzu, wyświetlenie komunikatu „Do zaimplementowania” lub celowe wstawienie błędnych
danych, które na pewno zwrócą uwagę programistów. Należy też podjąć działania nie tylko na rzecz ograniczenia ryzyka wywołania dalszych szkód przez
wykrytą usterkę, ale też w celu dowiedzenia, że panujemy nad sytuacją.
Każdy z nas widział piękne, w pełni funkcjonalne systemy, które rozsypywały się
jak domek z kart, kiedy tylko pojawiły się „wybite szyby”. Istnieją też inne czynniki prowadzące do rozkładu oprogramowania (wrócimy do nich w dalszej części
tej książki), ale nic tak nie przyspiesza tego zjawiska jak zaniedbania.
Część programistów sądzi, że konsekwentne sprzątanie rozbitego szkła i wstawianie nowych szyb jest niewykonalne — że nikt nie ma na to czasu podczas
realizacji projektu. Programiści, którzy podzielają ten pogląd, powinni albo
zaopatrzyć się w duży kubeł na śmieci, albo zmienić sąsiedztwo. Nie pozwólmy
entropii wygrać.
4
Patrz Contagious depression: Existence, specificity to depressed symptoms, and the role
of reassurance seeking [Joi94].
4337ebf6db5c7cc89e4173803ef3875a
4
32 
Rozdział 1.
Filozofia pragmatyczna
Po pierwsze nie szkodzić
Andy miał kiedyś znajomego, który był nieprzyzwoicie bogaty. Jego dom był pełen
bezcennych antyków, dzieł sztuki itp. Pewnego dnia zapalił się gobelin zawieszony zbyt blisko kominka w salonie. Wezwano na pomoc straż pożarną, aby
zapobiec katastrofie. Mimo szalejącego pożaru strażacy poprzedzili rozwinięcie
swoich wielkich, brudnych węży starannym rozłożeniem mat pomiędzy drzwiami
wejściowymi a źródłem ognia.
W ten sposób zabezpieczyli dywan przed niepotrzebnym zabrudzeniem.
Opisany scenariusz z pewnością jest mocno przesadzony. Zapewne najważniejszym priorytetem straży pożarnej powinno być ugaszenie pożaru, bez względu
na inne szkody. Jednak straż pożarna najwyraźniej właściwie oceniła sytuację.
Strażacy byli przekonani o zdolności do zapanowania nad ogniem i zastosowali
środki ostrożności w celu uniknięcia niepotrzebnych zniszczeń w budynku.
W podobny sposób należy postępować z oprogramowaniem: nie należy powodować dodatkowych szkód tylko dlatego, że powstał jakiś kryzys. Jedna wybita
szyba to o jedna za dużo.
Jedna wybita szyba (źle zaprojektowany fragment kodu, błędna decyzja kierownictwa utrudniająca pracę zespołu w całym okresie realizacji projektu itp.)
w zupełności wystarczy do rozpoczęcia procesu degradacji. Programista zaangażowany w projekt, w którym aż roi się od takich „wybitych szyb”, nie może
pogodzić się z sytuacją i przyjąć postawy: „Skoro cała reszta kodu nadaje się
do kosza, po co mój kod miałby się czymś wyróżniać?”. Jakość projektu do
momentu, w którym trafia do rąk programisty, nie powinna mieć żadnego znaczenia. W oryginalnym projekcie, który doprowadził do powstania teorii wybitej
szyby, porzucony samochód początkowo stał nietknięty przez tydzień. Wystarczyło jednak wybicie jednej szyby, aby w ciągu zaledwie paru godzin samochód został rozebrany niemal do gołej karoserii.
Podobnie, programista dołączający do zespołu realizującego projekt, którego kod
jest przykładem nieskazitelnego piękna (jest wyjątkowo przejrzysty, doskonale
zaprojektowany i elegancki), najprawdopodobniej będzie robił wszystko, aby
niczego nie zepsuć (jak strażacy w przytoczonej anegdocie). Nawet w warunkach szalejącego pożaru (zbliżającego się terminu, nadchodzącej daty wydania, planowanej prezentacji itp.) nikt nie chce być pierwszą osobą, która zakłóci
dotychczasową harmonię.
Po prostu sobie powiedz: „nie ma przyzwolenia na wybite szyby”.
Pokrewne podrozdziały

Temat 10., „Ortogonalność”.

Temat 40., „Refaktoryzacja”.

Temat 44., „Nazewnictwo”.
4337ebf6db5c7cc89e4173803ef3875a
4
Zupa z kamieni i gotowane żaby
 33
Wyzwania
4
39

Warto pomóc we wzmocnieniu swojego zespołu poprzez dokonanie gruntownego przeglądu otoczenia, w którym pracuje. Należy wybrać dwie lub
trzy „wybite szyby”, po czym omówić ze współpracownikami źródła poszczególnych problemów i możliwe rozwiązania.

Czy potrafimy wskazać szybę, która została wybita jako pierwsza? Jaka
była nasza reakcja po jej pierwszym wykryciu? Jeśli ta „wybita szyba”
wynika z decyzji kogoś innego lub polecenia kierownictwa, jak możemy
temu zaradzić?
Zupa z kamieni i gotowane żaby
Trzej głodni żołnierze wracali z wojny. Kiedy zobaczyli na swojej drodze wioskę,
byli pełni nadziei — byli przekonani, że mieszkańcy osady zaproszą ich na
posiłek. Po dotarciu na miejsce odkryli jednak, że wszystkie drzwi i okna są
pozamykane. Po wieloletniej wojnie mieszkańcy wsi dysponowali niewielką ilością jedzenia i pilnie strzegli swoich zapasów.
Zdeterminowani żołnierze zagotowali kocioł wody i ostrożnie włożyli do wrzątku
trzy kamienie. Zaskoczeni wieśniacy zaczęli wychodzić ze swoich domów i obserwować poczynania żołnierzy.
— To jest zupa z kamieni — wyjaśnili żołnierze.
— To wszystko, co do niej włożycie? — dopytywali mieszkańcy osady.
— Oczywiście, chociaż są tacy, którzy twierdzą, że zupa jest jeszcze lepsza
z kilkoma marchewkami. — Jeden z wieśniaków natychmiast pobiegł do swojego
domu, by po chwili przynieść kosz pełen marchwi.
Kilka minut później wieśniacy znowu zaczęli dopytywać:
— To naprawdę wszystko?
— Cóż — odpowiedzieli żołnierze — kilka ziemniaków na pewno by nie zaszkodziło. — Po chwili inny wieśniak przybiegł z workiem ziemniaków.
Po godzinie żołnierze dysponowali składnikami, które w zupełności wystarczyły
do przyrządzenia wymarzonej zupy: wołowiną, porem, solą i ziołami. Za każdym
razem inny mieszkaniec wsi plądrował własną spiżarnię w poszukiwaniu składnika wskazanego przez żołnierzy.
Ostatecznie udało się ugotować całkiem spory kocioł doskonałej zupy. Żołnierze
wyjęli z zupy kamienie, po czym usiedli wraz z mieszkańcami wsi, aby wspólnie celebrować pierwszy pełnowartościowy posiłek od wielu miesięcy.
4337ebf6db5c7cc89e4173803ef3875a
4
34 
Rozdział 1.
Filozofia pragmatyczna
Z historii o zupie z kamieni płynie wiele wniosków. Wieśniacy zostali oszukani
przez żołnierzy, którzy wykorzystali ich ciekawość do zdobycia niezbędnych
składników. Ważniejsze jest jednak coś zupełnie innego — żołnierze zadziałali
jak katalizator skupiający społeczność wokół jednego celu i umożliwiający ugotowanie posiłku, którego poszczególni mieszkańcy wsi nie mogliby sporządzić
na bazie własnych zapasów. Mamy więc do czynienia z pożądanym skutkiem
synergii. Ostatecznie wszyscy odnieśli korzyści.
Warto więc spróbować zastosować metodę tych żołnierzy we własnym środowisku.
Możemy znaleźć się w sytuacji, w której doskonale wiemy, co należy zrobić i jak
to osiągnąć. Wyobraźmy sobie, że mamy w głowie kompletną koncepcję systemu, o której z całą pewnością wiemy, że jest słuszna. Przyjmijmy jednak, że
musimy uzyskać zgodę na to przedsięwzięcie i że spotykamy się z niechęcią
i brakiem zrozumienia. Musimy przekonać rozmaite komitety, zadbać o akceptację zaproponowanego budżetu — projekt już na tym etapie bardzo się komplikuje. Każdy zazdrośnie strzeże swoich zasobów. Opisane zjawisko bywa określane mianem próby sił na początku działalności (ang. start-up fatigue).
To dobry moment, by sięgnąć po kamienie. Należy dobrze przemyśleć, o które
składniki warto prosić. Warto dobrze wykorzystać każdy otrzymany składnik.
Po otrzymaniu półproduktu należy zaprezentować efekt wszystkim zainteresowanym i pozwolić, by zachwycali się otrzymanym dziełem. Kiedy powiedzą:
„byłoby jeszcze lepsze, gdyby dodać…”, można udawać, że proponowane ulepszenia są nieistotne. Wystarczy teraz rozsiąść się wygodnie i czekać, aż sami
zaczną sugerować dodawanie funkcji, które od początku chcieliśmy zaimplementować w tym systemie. Wielu ludziom łatwiej przychodzi dołączanie do udanego projektu niż praca na niepewny sukces od podstaw. Warto więc tak opisać
perspektywy projektu, aby byli przekonani o jego świetlanej przyszłości5.
WSKAZÓWKA NR 6
Bądź katalizatorem zmian.
Mieszkańcy wsi
Jeśli spojrzymy na historię zupy z kamieni z punktu widzenia wieśniaków,
otrzymamy opowieść o subtelnym i postępującym oszustwie. To historia o zbyt
wąskiej perspektywie. Mieszkańcy osady są tak zaintrygowani kamieniami, że
zapominają o bożym świecie. To samo zdarza się każdemu, codziennie. Pewne
rzeczy potrafią zadziwiająco skutecznie przykuwać naszą uwagę.
5
Pewnym pokrzepieniem mogą być słowa przypisywane kontradmirał dr Grace Hopper:
„Łatwiej prosić o wybaczenie, niż uzyskać pozwolenie”.
4337ebf6db5c7cc89e4173803ef3875a
4
Zupa z kamieni i gotowane żaby
 35
Opisane symptomy są nam doskonale znane. Projekty powoli, ale nieuchronnie
wymykają nam się z rąk. Większość katastrof w świecie oprogramowania rozpoczyna się od zjawisk tak niepozornych, że wręcz trudnych do dostrzeżenia.
Co więcej, większość projektów nie jest realizowana w terminie. Systemy oddalają się od swoich oryginalnych specyfikacji funkcja po funkcji. Co więcej, kolejne
łatki eliminujące usterki powodują, że z czasem produkt nie ma nic wspólnego
z oryginałem. Na spadek morale i rozpad zespołu często składa się wiele drobnych zjawisk, tyle że kumulowanych w dłuższym okresie.
WSKAZÓWKA NR 7
Pamiętaj o szerszym kontekście.
Nigdy tego nie próbowaliśmy. Naprawdę. Mówi się, że żywa żaba wrzucona do
gotującej się wody natychmiast wyskoczy z garnka. Jeśli jednak wrzucimy żabę
do zimnej wody i zaczniemy tę wodę stopniowo nagrzewać, żaba nie zwróci uwagi
na powolny wzrost temperatury i pozostanie w garnku, aż zostanie ugotowana.
Łatwo zauważyć, że w przypadku żaby mamy do czynienia z zupełnie innym
problemem od tego, który omówiono we wcześniejszym podrozdziale na przykładzie zbitej szyby. Zgodnie z teorią wybitej szyby ludzie tracą zainteresowanie walką z entropią, kiedy odkrywają, że byliby w tej walce osamotnieni — że
otaczająca ich społeczność bagatelizuje problem. Żaba nawet nie dostrzega
zmiany.
Nie możemy upodabniać się do tej żaby. Musimy mieć na uwadze szerszy obraz
sytuacji. Powinniśmy stale obserwować to, co dzieje się w otaczającym nas świecie, zamiast koncentrować się tylko na swoich zadaniach.
Pokrewne podrozdziały

Temat 1., „To jest Twoje życie”.

Temat 38., „Programowanie przez koincydencję”.
Wyzwania

Podczas recenzowania wstępnej wersji tej książki John Lakos zwrócił uwagę
na ciekawe zjawisko. Żołnierze stopniowo oszukiwali mieszkańców wioski,
ale powodowana przez to oszustwo zmiana postawy wieśniaków była dla
wszystkich korzystna. Stopniowe oszukiwanie żaby ostatecznie prowadzi
do jej skrzywdzenia. Czy potrafimy stwierdzić, kiedy przyspieszanie zmian
odbywa się metodą zupy z kamieni, a kiedy zupy z żaby? Czy ta ocena
będzie subiektywna, czy obiektywna?

Odpowiedz szybko, bez spoglądania, ile lamp jest na suficie nad Tobą?
Ile jest ich wszystkich w pokoju? Ile osób w nim przebywa? Czy jest coś
spoza kontekstu — coś, co wygląda jakby nie należało do wystroju? Jest to
ćwiczenie na świadomość sytuacyjną — technikę praktykowaną zarówno
4337ebf6db5c7cc89e4173803ef3875a
4
36 
Rozdział 1.
Filozofia pragmatyczna
przez skautów, jak i oddziały marynarki wojennej Stanów Zjednoczonych.
Nabierz nawyku obserwowania otoczenia i spostrzegania tego, co się w nim
znajduje. Następnie stosuj ten sam nawyk w swoim projekcie.
5
40
Odpowiednio dobre oprogramowanie
Kto lepsze goni, często w gorsze wpadnie.
Król Lear, akt I, scena IV
Istnieje stary dowcip o amerykańskiej firmie, która zamówiła 100 tysięcy układów scalonych u japońskiego producenta. Specyfikacja zawierała błąd, który
powodował usterkę w jednym na 10 tysięcy układów. Kilka tygodni po złożeniu zamówienia do firmy dotarło wielkie pudło z tysiącami układów scalonych
oraz małe pudełko z zaledwie dziesięcioma układami. W pudełku znajdowała
się kartka z napisem: „Tutaj są te układy z błędami”.
Możemy, oczywiście, tylko pomarzyć o podobnej kontroli nad jakością swoich
produktów. Świat nigdy nie pozwoli nam tworzyć naprawdę doskonałego, w pełni
wolnego oprogramowania. Przeciwko nam sprzysięgły się takie siły jak czas,
technologia i temperament.
Nasza sytuacja wcale nie musi być frustrująca. Jak napisał Ed Yourdon w swoim
artykule w „IEEE Software”, When good-enough software is best [You95], przy
odrobinie samodyscypliny możemy pisać oprogramowanie, które będzie wystarczająco dobre — wystarczająco dobre dla użytkowników, dla osób w przyszłości
odpowiedzialnych za jego konserwację i dla naszego świętego spokoju. Szybko
odkryjemy, że taki model pracy zapewnia nam maksymalną produktywność
i w pełni satysfakcjonuje użytkowników. Co więcej, z zadowoleniem stwierdzimy,
że nasze programy są lepsze także dzięki skróconemu procesowi produkcji.
Zanim przystąpimy do dalszych rozważań, musimy doprecyzować, co naprawdę
oznaczają te propozycje. Określenie „wystarczająco dobre” nie oznacza niechlujnego czy źle opracowanego kodu. W przypadku każdego systemu warunkiem
sukcesu jest zgodność z wymaganiami użytkowników. W tym podrozdziale proponujemy koncepcję, w której użytkownicy końcowi mają szansę udziału w procesie podejmowania decyzji o tym, kiedy nasze dzieło jest wystarczająco dobre.
Angażowanie użytkowników
w rozstrzyganie o jakości
W typowych okolicznościach piszemy oprogramowanie z myślą o innych użytkownikach. Często pamiętamy nawet o potrzebie uzyskania wymagań od tych
użytkowników6. Jak często pytamy użytkowników, na ile dobrego oprogramo6
To miał być żart!
4337ebf6db5c7cc89e4173803ef3875a
4
Odpowiednio dobre oprogramowanie

37
wania oczekują? W pewnych przypadkach taki wybór oczywiście jest niemożliwy. Jeśli pracujemy nad rozrusznikiem serca, promem kosmicznym lub niskopoziomową biblioteką używaną później przez tysiące innych programistów,
wymagania będą bardziej restrykcyjne, a nasze pole manewru stosunkowo
niewielkie.
Jeśli jednak pracujemy nad zupełnie nowym produktem, podlegamy całkiem
innym ograniczeniom. Pracownicy działu marketingu składają przyszłym użytkownikom pewne obietnice, a sami użytkownicy mogą mieć plany związane
z zapowiedzianym terminem wydania. Co więcej, nasza firma na pewno podlega ograniczeniom związanym z przepływem środków finansowych. Ignorowanie
wymagań użytkowników w zakresie implementacji nowych funkcji lub tylko nieznacznego udoskonalenia gotowego kodu byłoby dalece nieprofesjonalne. Nie
oznacza to jednak, że należy panicznie bać się negatywnej oceny użytkownika
końcowego — równie nieprofesjonalne jest składanie nierealnych obietnic co do
terminu i rezygnowanie z kolejnych elementów produktu wyłącznie z myślą
o dotrzymaniu tych nieprzemyślanych deklaracji.
Zakres i jakość tworzonego systemu powinny być precyzyjnie określone w wymaganiach tego systemu.
WSKAZÓWKA NR 8
Jakość powinna być uwzględniona w wymaganiach.
W wielu przypadkach nie da się uniknąć wyboru części funkcji kosztem innych.
Co ciekawe, wielu użytkowników jest skłonnych zgodzić się na korzystanie
z okrojonej wersji już dzisiaj, zamiast czekać na przykład rok na wersję uzupełnioną o elementy multimedialne. Wiele działów IT dysponujących skromnym
budżetem zapewne się z tym zgodzi. Dobre oprogramowanie dzisiaj często jest
lepsze niż doskonałe oprogramowanie jutro. Jeśli z odpowiednim wyprzedzeniem
przekażemy użytkownikom produkt, z którym będą mogli swobodnie eksperymentować, ich opinie i wskazówki najprawdopodobniej umożliwią nam wypracowanie lepszego produktu docelowego (patrz temat 12., „Pociski smugowe”).
Warto wiedzieć, kiedy przestać
Programowanie pod pewnymi względami przypomina malowanie. W obu przypadkach praca rozpoczyna się od pustego płótna i kilku prostych materiałów.
Dopiero połączenie nauki, sztuki i rzemiosła pozwala prawidłowo używać dostępnych środków. Należy zacząć od naszkicowania ogólnego kształtu, by następnie
namalować otoczenie i wreszcie wypełnić szczegóły. Powinniśmy też stale odchodzić parę kroków od obrazu, by z większej odległości spojrzeć krytycznym okiem
na dotychczasowe dokonania. Od czasu do czasu musimy nawet wyrzucić całe
płótno do kosza i zacząć wszystko od nowa.
4337ebf6db5c7cc89e4173803ef3875a
4
38 
Rozdział 1.
Filozofia pragmatyczna
Każdy artysta potwierdzi jednak, że cała ta ciężka praca nie ma najmniejszego
sensu, jeśli twórca nie wie, kiedy przestać. Jeśli bez końca nanosimy kolejne
warstwy i domalowujemy kolejne szczegóły, artystyczna wartość obrazu ginie
gdzieś pod nadmiarem farby.
Nie należy niszczyć dobrego programu przesadną liczbą upiększeń i wyrafinowanych dodatków. Powinniśmy raczej pozwolić, by nasz kod sprawdził się w działaniu bez naszego udziału. Być może nie jest doskonały. Nie ma jednak powodu
do zmartwień — i tak nigdy taki by nie był. (W rozdziale 7. „Kiedy kodujemy…”
omówimy filozofie tworzenia kodu w niedoskonałym świecie).
Pokrewne podrozdziały

Temat 45., „Kopalnia wymagań”.

Temat 46., „Rozwiązywanie niemożliwych do rozwiązania łamigłówek”.
Wyzwania

Warto przyjrzeć się producentom narzędzi i systemów operacyjnych, których sami używamy. Czy potrafimy wskazać dowody sugerujące, że te firmy
oferują oprogramowanie, o którym same wiedzą, że jest niedoskonałe? Czy
jako użytkownicy wolelibyśmy raczej (1) czekać na wydanie oprogramowania pozbawionego wszystkich błędów, (2) otrzymać złożone oprogramowanie przy akceptacji pewnych niedoróbek, czy (3) korzystać z prostszego
oprogramowania z nieco większą liczbą usterek?

Zastanówmy się nad skutkami ewentualnej modularyzacji procesu dostarczania oprogramowania. Czy uzyskanie monolitycznego bloku oprogramowania w określonej jakości zajęłoby więcej, czy mniej czasu niż w przypadku projektowania systemu w formie zbioru modułów? Czy potrafimy
wskazać jakieś komercyjne przykłady?

Czy potrafisz wskazać popularne oprogramowanie, którego wadą jest nadmiar funkcjonalności? Oznacza to oprogramowanie zawierające znacznie
więcej funkcji niż kiedykolwiek będziemy używać. Każda funkcjonalność
wprowadza więcej możliwości popełnienia błędów i luk w zabezpieczeniach
i sprawia, że trudniej jest znaleźć to, czego potrzebujemy, a także zarządzać funkcjonalnościami. Czy istnieje niebezpieczeństwo, że sam wpadniesz
w tego rodzaju pułapkę?
4337ebf6db5c7cc89e4173803ef3875a
4
Portfolio wiedzy
6
41

39
Portfolio wiedzy
Inwestycja w wiedzę zawsze przynosi największe zyski.
Benjamin Franklin
Jak widać, zawsze można liczyć na celne i zwięzłe wskazówki starego, dobrego
Bena Franklina. Czy do zostania doskonałymi programistami wystarczy wczesne chodzenie spać i wstawanie o świcie? Ranny ptaszek może, oczywiście, pierwszy dopaść dorodnego robaka, ale co na rannym wstawaniu zyskuje robak?
W tym przypadku Ben trafił w sedno. Wiedza i doświadczenie to zdecydowanie
najważniejsze atuty w naszej profesji.
Okazuje się jednak, że wymienione aktywa nie są wieczne7. Nasza wiedza
dezaktualizuje się wraz z powstawaniem nowych technik, języków i środowisk.
Zmieniające się warunki rynkowe mogą powodować, że nasze dotychczasowe
doświadczenia stają się wręcz bezwartościowe. Zważywszy na tempo zmian
w erze internetu, opisane zjawiska mogą zachodzić wyjątkowo szybko.
Wraz ze spadkiem wartości naszej wiedzy spada wartość nas samych z perspektywy pracodawcy lub klienta. Naturalnym rozwiązaniem jest więc dążenie
do zapobieżenia temu spadkowi.
Twoja zdolność do uczenia się nowych rzeczy, to najważniejszy zasób o strategicznym znaczeniu. Ale w jaki sposób dowiedzieć się, jak się uczyć oraz skąd
wiedzieć, czego się uczyć?
Portfolio wiedzy
Wszyscy lubimy postrzegać wszystkie znane programiście fakty o przetwarzaniu
komputerowym, wszystkie dziedziny, w których pracował ten programista, oraz
jego łączne doświadczenie jako tzw. portfolio wiedzy programisty. Zarządzanie
portfolio wiedzy pod wieloma względami przypomina zarządzanie portfelem
instrumentów finansowych:
1. Poważni inwestorzy inwestują regularnie (to dla nich swoisty nawyk).
2. Dywersyfikacja jest kluczem do sukcesu w dłuższym terminie.
3. Najlepsi inwestorzy właściwie równoważą swoje portfele, dzieląc inwestycje na bezpieczne, konserwatywne oraz ryzykowne, ale dające szansę szybkiego pomnożenia kapitału.
4. Inwestorzy starają się kupować tanio i sprzedawać drogo, aby osiągać
maksymalny zwrot z inwestycji.
7
Tzw. aktywa wygasające (ang. expiring assets) to takie, których wartość maleje w czasie.
Innymi przykładami takich aktywów są magazyny pełne bananów czy bilet na mecz
koszykówki.
4337ebf6db5c7cc89e4173803ef3875a
4
40 
Rozdział 1.
Filozofia pragmatyczna
5. Portfele powinny być poddawane analizie i korygowane w regularnych
odstępach czasu.
Aby osiągnąć sukces w karierze, musimy zarządzać portfolio swojej wiedzy,
postępując według tych samych wskazówek.
Dobra wiadomość jest taka, że zarządzanie tego rodzaju inwestycjami jest taką
samą umiejętnością, jak każda inna — można się jej nauczyć. Sztuką jest,
aby zacząć to robić, a następnie stworzyć w sobie taki nawyk. Stwórz rutynę
i postępuj zgodnie z nią tak długo, aż Twój mózg uzna ją za swoją. Od tej pory
będziesz automatycznie „zasysał” nową wiedzę.
Budowa własnego portfolio wiedzy

Regularne inwestowanie. Tak jak w świecie inwestycji finansowych,
musimy inwestować w swoje portfolio wiedzy możliwie regularnie. Nawet
jeśli przedmiotem inwestycji są niewielkie kwoty, odpowiedni nawyk jest
równie ważny jak inwestowane sumy. Kilka przykładowych celów zostanie
opisanych w następnym podrozdziale.

Różnorodność. Im więcej różnych zagadnień znamy, tym większa jest nasza
wartość. Absolutnym minimum jest znajomość podstawowych cech technologii, której aktualnie używamy w swojej pracy. Nie możemy jednak na
tym poprzestać. Świat komputerów zmienia się na tyle gwałtownie, że technologia bijąca dzisiaj rekordy popularności jutro może być niemal bezużyteczna (a przynajmniej skazana na zapomnienie). Im więcej technologii
opanujemy, tym łatwiej będziemy mogli dostosowywać się do zachodzących zmian.

Zarządzanie ryzykiem. Z technologiami wiążą się bardzo różne czynniki
ryzyka — istnieją zarówno technologie cechujące się dużym zyskiem przy
małym ryzyku, jak i rozwiązania oferujące stosunkowo niewiele w warunkach wysokiego ryzyka. Inwestowanie wszystkich środków w ryzykowne
udziały, które mogą z dnia na dzień okazać się bezwartościowe, z pewnością nie byłoby rozsądnym posunięciem. Nie należy też inwestować wszystkich zasobów w najbardziej bezpieczne, zachowawcze rozwiązania, ponieważ można w ten sposób przegapić najlepsze okazje. Nie powinniśmy więc
umieszczać wszystkich technicznych jajek w jednym koszyku.

Kupuj tanio, sprzedawaj drogo. Nauka nowych technologii jeszcze przed
zyskaniem większej popularności bywa równie trudna jak odnajdywanie
niedoszacowanych papierów wartościowych, jednak potencjalna nagroda
w obu przypadkach będzie bardzo kusząca. Nauka Javy zaraz po jej pierwszym wydaniu być może była ryzykowna, ale zapewne zwróciła się z nawiązką pionierom tej technologii, którzy mają dzisiaj status najlepiej opłacanych ekspertów.

Przeglądy i korekty. Mamy do czynienia z branżą podlegającą wyjątkowo
dynamicznym zmianom. Popularna technologia, której używamy zaledwie
od miesiąca, już jutro może okazać się całkowitym przeżytkiem. Być może
4337ebf6db5c7cc89e4173803ef3875a
4
Portfolio wiedzy

41
warto wrócić do technologii bazy danych, której nie używaliśmy od jakiegoś
czasu. Niewykluczone, że uzyskamy nieporównanie lepszą propozycję pracy,
jeśli spróbujemy jeszcze opanować język…
Spośród wszystkich tych wskazówek najważniejsza jest ta, która wydaje się
najprostsza:
WSKAZÓWKA NR 9
Regularnie inwestuj w swoje portfolio wiedzy.
Cele
Skoro dysponujemy już pewnymi wskazówkami, jak i kiedy uzupełniać nasze
portfolio wiedzy, warto zastanowić się nad najlepszymi sposobami pozyskiwania kapitału intelektualnego niezbędnego do wypełnienia tego portfolio. Poniżej
przedstawiono kilka sugestii.
8

Warto uczyć się przynajmniej jednego nowego języka rocznie. Różne
języki programowania pozwalają rozwiązywać te same problemy na różne
sposoby. Stałe poznawanie nowych rozwiązań ułatwia szersze postrzeganie
rozwiązywanych problemów i zmniejsza ryzyko wybierania utartych sposobów postępowania. Co więcej, uczenie się nowych języków jest teraz
dużo prostsze dzięki materiałom i oprogramowaniu dostępnym za darmo
w internecie.

Należy czytać jedną książkę techniczną na miesiąc. Chociaż w internecie znajduje się mnóstwo krótkich artykułów oraz można w nim znaleźć
wartościowe odpowiedzi na nurtujące nas pytania, dokładne zrozumienie
tematu wymaga przeczytania książki. Warto poszukać w księgarniach książek technicznych poświęconych zagadnieniom w ten czy inny sposób związanym z aktualnie realizowanym projektem8. Kiedy już nabierzemy odpowiednich przyzwyczajeń, powinniśmy sięgać po nową książkę przynajmniej
raz w miesiącu. Po opanowaniu aktualnie używanych technologii warto
poświęcić trochę czasu na poznawanie rozwiązań niezwiązanych z aktualnie realizowanym projektem.

Powinniśmy też sięgać po książki inne niż techniczne. Musimy pamiętać, że komputery są używane przez ludzi — ludzi, których potrzeby próbujemy zaspokoić, tworząc odpowiednie oprogramowanie. Nie wolno nam
zapominać o stronie równania, po której występuje żywy człowiek.

Powinniśmy brać udział w szkoleniach. Warto sprawdzić, czy lokalne
uczelnie nie organizują wartościowych kursów. Niewykluczone, że cenną
wiedzę będzie można zdobyć na przykład podczas zbliżających się targów.
Być może to tylko nasze zdanie, ale wartościowa lista znajduje się pod adresem
https://praprog.com.
4337ebf6db5c7cc89e4173803ef3875a
4
42 
Rozdział 1.
Filozofia pragmatyczna

Należy zaangażować się w funkcjonowanie lokalnych grup użytkowników. Nie wystarczy tylko iść na spotkanie i słuchać, co inni mają do
powiedzenia — chodzi o aktywny udział. Izolacja jest śmiertelnym zagrożeniem dla kariery. Warto więc szukać kontaktów z osobami pracującymi
poza naszą firmą.

Należy eksperymentować z różnymi środowiskami. Jeśli pracujemy
wyłącznie w systemie Windows, warto spróbować poznać system Unix
w domu (wprost doskonałym wyborem będzie któraś z darmowych dystrybucji Linuksa). Jeśli do tej pory korzystaliśmy tylko z plików makefile
i zwykłego edytora, koniecznie powinniśmy sprawdzić możliwości środowisk IDE (i odwrotnie).

Należy trzymać rękę na pulsie. Warto czytać artykuły i posty online dotyczące technologii innych niż te, których używasz w swoim bieżącym projekcie. W ten sposób można się dowiedzieć, jakie doświadczenia mają inni
z tymi technologiami, jakiego używają słownictwa itp.
Niezwykle ważne jest ustawiczne szukanie i pogłębianie wiedzy. Kiedy uznajemy,
że opanowaliśmy nowy język lub technologię w dostatecznym stopniu, powinniśmy iść dalej. To doskonały moment, by nauczyć się czegoś nowego.
Nie ma znaczenia, czy kiedykolwiek używaliśmy którejś z tych technologii
w ramach jakiegoś projektu ani nawet czy wspominaliśmy o tej technologii
w swoich CV. Proces uczenia się poszerza nasze horyzonty myślowe, stwarzając
nowe możliwości i wskazując nowe drogi osiągania celów. W tym fachu bardzo
ważne jest umiejętne łączenie wiedzy z różnych źródeł. Warto więc próbować
wykorzystywać nowe umiejętności już w trakcie bieżącego projektu. Nawet jeśli
ten projekt jest realizowany w innej technologii, być może istnieje możliwość
zastosowania przynajmniej niektórych pomysłów. Wystarczy opanować na przykład programowanie obiektowe, aby nieco zmienić styl programowania. Zrozumienie paradygmatu programowania funkcyjnego pozwoli pisać kod obiektowy
inaczej.
Okazje do nauki
Przyjmijmy, że łapczywie sięgamy po wszystkie dostępne materiały i doskonale
orientujemy się w nowinkach w naszej dziedzinie (co nie jest proste). Załóżmy, że
nagle ktoś zadaje nam jakieś pytanie. Nie mamy zielonego pojęcia, jak na nie
odpowiedzieć, do czego od razu przyznajemy się swojemu rozmówcy.
Nie możemy na tym poprzestać. Powinniśmy raczej wykorzystać sytuację i traktować znalezienie odpowiedzi jako swoiste wyzwanie. Możemy zapytać innych,
poszukać w internecie — warto czytać także artykuły naukowe, nie tylko opinie
użytkowników.
Jeśli sami nie możemy znaleźć odpowiedzi, powinniśmy poszukać osoby, która
poradzi sobie z tym zadaniem. Nie wolno nam tak tego zostawić. Rozmowy
z innymi ułatwią nam budowę sieci osobistych relacji. W ten sposób nierzadko
4337ebf6db5c7cc89e4173803ef3875a
4
Portfolio wiedzy

43
można odnajdywać — ku własnemu zdziwieniu — rozwiązania innych problemów (niezwiązanych z początkowym tematem rozmów). Wszystko to sprawia,
że nasze portfolio stale jest powiększane…
Lektura wszystkich tych materiałów i poszukiwanie wiedzy z natury rzeczy
wymaga czasu. Właśnie czas jest tutaj zasobem deficytowym. Oznacza to, że
musimy planować swoje poczynania z wyprzedzeniem. Warto zawsze mieć pod
ręką coś do przeczytania na wypadek, aby nigdy nie siedzieć bezczynnie. Czas
spędzany w poczekalniach u lekarzy lub dentystów to wprost doskonała okazja
do nadrobienia zaległości w czytaniu. Nie możemy jednak liczyć na innych —
jeśli nie mamy ze sobą interesujących nas materiałów, możemy skończyć, trzymając w dłoniach pomięty miesięcznik z 1973 roku pełen artykułów o Papui-Nowej Gwinei.
Krytyczne myślenie
Ostatnim ważnym punktem jest krytyczna ocena tego, co czytamy i słyszymy.
Musimy mieć pewność, że wiedza, która trafia do naszego portfolio, jest prawidłowa i nie została zniekształcona przez marketingowy przekaz producenta
czy mediów. Należy wystrzegać się fanatyków dogmatycznie przywiązanych do
swoich racji — ich poglądy mogą, ale nie muszą potwierdzić się w naszym przypadku (i w ramach realizowanego projektu).
Nigdy nie powinniśmy lekceważyć siły komercjalizacji. To, że wyszukiwarka
internetowa wyświetla coś na pierwszym miejscu, nie oznacza jeszcze, że trafiliśmy na najlepszą stronę; być może jej właściciel po prostu zapłacił za wyższą
pozycję w wynikach. To, że księgarnia wystawia jakąś książkę na witrynie, nie
oznacza jeszcze, że książka jest dobra (ani nawet popularna); być może jej
wydawca zapłacił za eksponowanie swojego tytułu.
WSKAZÓWKA NR 10
Patrz krytycznym okiem na to, co czytasz i słyszysz.
Krytyczne myślenie to oddzielna dyscyplina. Zachęcamy do przeczytania i przestudiowania wszystkiego, co uda Ci się zdobyć na ten temat. Zanim rozpoczniesz
lekturę, oto lista z kilkoma pytaniami, które warto sobie zadać i przeanalizować
odpowiedzi.

Zadawaj pytania: „pięć razy dlaczego”. Ulubiona sztuczka konsultingowa: zadaj pytanie „dlaczego?” co najmniej pięć razy. Zadaj pytanie
i uzyskaj odpowiedź. Przeanalizuj problem dokładniej, dalej pytając „dlaczego?”. Powtarzaj to pytanie tak, jakbyś był czterolatkiem (ale grzecznym).
Być może w ten sposób uda Ci się zbliżyć do poznania głównej przyczyny
problemu.
4337ebf6db5c7cc89e4173803ef3875a
4
44 
Rozdział 1.
Filozofia pragmatyczna

Kto na tym skorzysta? Choć może się to wydawać cyniczne, podążanie za
pieniędzmi może być bardzo pomocną ścieżką do analizy. Korzyści kogoś
innego lub innej organizacji mogą być tożsame z Twoimi własnymi —
bądź nie.

Jaki jest kontekst? Weźmy za przykład artykuł lub książkę reklamującą
„najlepsze praktyki”. Dobre pytanie do rozważenia brzmi: „najlepsze dla
kogo?”. Jakie są warunki wstępne, jakie są konsekwencje, zarówno krótko-,
jak i długoterminowe?

Kiedy i w jakich warunkach rozwiązanie się sprawdza? Zastanów się
nad okolicznościami. Czy jest za późno? A może za wcześnie? Nie poprzestawaj na myśleniu wyłącznie o następnym kroku (co wydarzy się za
chwilę), ale myśl także długoterminowo (co zdarzy się w dalszej kolejności).

Dlaczego to jest problem? Czy istnieje model bazowy? W jaki sposób ten
model działa?
Praktyka pokazuje, że prostych odpowiedzi jest bardzo niewiele. Odpowiednio
bogate portfolio i krytyczne spojrzenie na otaczającą nas masę publikacji powinny
jednak umożliwić nam wypracowywanie odpowiedzi nawet na najtrudniejsze
pytania.
Pokrewne podrozdziały

Temat 1., „To jest Twoje życie”.

Temat 22., „Dzienniki inżynierskie”.
Wyzwania
9

Naukę nowego języka powinniśmy rozpocząć już w tym tygodniu. Od zawsze
programujesz w tym samym, starym języku? Spróbuj więc napisać coś
w językach Clojure, Elixir, Elm, F#, Go, Haskell, Python, R, ReasonML,
Ruby, Rust, Scala, Swift, TypeScript lub dowolnym innym, który zwrócił
Twoją uwagę i (lub), który Ci się spodobał9.

Sięgnijmy po nową książkę (ale dopiero po skończeniu tego wydania). Jeśli
aktualnie zajmujemy się szczegółową implementacją i kodowaniem, powinniśmy przeczytać książkę o projektowaniu i architekturze. Jeśli natomiast
pracujemy nad wysokopoziomowym projektem, dla odmiany powinniśmy
sięgnąć po książkę o technikach kodowania.
Nigdy nie słyszałeś o żadnym z tych języków? Pamiętaj, że wiedza, podobnie jak popularna technologia, jest wygasającym atutem. Lista nowych i eksperymentalnych języków była zupełnie inna w pierwszym wydaniu i prawdopodobnie będzie inna w chwili,
gdy będziesz czytać tę książkę. Tym bardziej masz powód, aby nie przestawać się uczyć.
4337ebf6db5c7cc89e4173803ef3875a
4
Komunikuj się!

7
42

45
Powinniśmy wyjść i porozmawiać o technologiach z osobami, które nie są
zaangażowane w bieżący projekt lub które w ogóle nie pracują w naszej
firmie. Warto zawierać nowe znajomości w firmowej kafejce lub poszukać
entuzjastów podobnych do nas na lokalnym spotkaniu grupy użytkowników.
Komunikuj się!
Wierzę, że lepiej być przedmiotem krytycznego osądu niż
niezauważonym.
Mae West, Piękność lat dziewięćdziesiątych, 1934
Być może możemy się czegoś nauczyć od pani West. Nie chodzi tylko o to, co
mamy, ale też o to, jak to zapakujemy. Nawet najlepsze pomysły, najdoskonalszy kod i najbardziej pragmatyczne myślenie będą jałowe, jeśli nie nauczymy
się komunikacji z innymi ludźmi. Bez efektywnej komunikacji nawet dobra
idea staje się sierotą.
Jako programiści musimy komunikować się na wielu poziomach. Spędzamy całe
godziny na spotkaniach, podczas których słuchamy i mówimy. Pracujemy z użytkownikami końcowymi, próbując zrozumieć ich potrzeby. Piszemy kod, którego
zadaniem jest zarówno komunikowanie naszych intencji maszynie (komputerowi), jak i dokumentowanie naszych przemyśleń przyszłym pokoleniom programistów. Piszemy propozycje i notatki dołączane do wniosków o zasoby i wyjaśniających ich stosowanie, raportujące postępy prac oraz sugerujące nowe
kierunki. Co więcej, codziennie pracujemy z naszymi zespołami, próbując przekonywać je do naszych pomysłów, zmieniać dotychczasowe praktyki ich członków i sugerować nowe rozwiązania. Ponieważ znaczną część czasu pracy poświęcamy właśnie na komunikację, musimy robić to naprawdę dobrze.
Traktuj angielski (lub dowolny język naturalny, którym się posługujesz) jak
specjalny język programowania. Pisz w języku naturalnym tak, jak pisze się kod,
stosuj zasady DRY, ETC, narzędzia automatyzacji itp. (zasady projektowania
DRY i ETC omówimy w następnym rozdziale).
WSKAZÓWKA NR 11
Język naturalny to po prostu kolejny język programowania.
W tym podrozdziale sporządzimy listę sugestii, które mogą być pomocne podczas doskonalenia umiejętności komunikacyjnych.
4337ebf6db5c7cc89e4173803ef3875a
4
46 
Rozdział 1.
Filozofia pragmatyczna
Poznaj swoich odbiorców
Komunikujesz się tylko wtedy, gdy przekazujesz to, co chcesz przekazać, samo
mówienie nie wystarczy. Aby to robić, musisz rozumieć potrzeby, zainteresowania i możliwości swoich odbiorców. Wszyscy uczestniczyliśmy w spotkaniach,
w czasie których jakiś maniakalny programista z błyskiem w oczach wygłaszał
wiceprezesowi ds. marketingu długi monolog na temat meritum jakiejś tajemnej
technologii. To nie jest komunikacja: to tylko monolog, i do tego denerwujący10.
Załóżmy, że chcesz zmienić zdalny system monitorowania, aby używać zewnętrznego brokera komunikatów do publikowania powiadomień o stanie. W zależności od odbiorców informacje można przedstawić na wiele różnych sposobów.
Użytkownicy końcowi docenią, że ich systemy będą teraz współdziałać z innymi
usługami korzystającymi z brokera. Twój dział marketingu będzie mógł wykorzystać ten fakt, aby zwiększyć sprzedaż. Menedżerowie działów programowania
i operacyjnego będą zadowoleni, ponieważ utrzymanie tej części systemu będzie
odtąd problemem kogoś innego. Wreszcie programiści mogą korzystać z doświadczeń związanych z posługiwaniem się nowymi interfejsami API, a może nawet
będą w stanie znaleźć nowe zastosowania dla brokera komunikatów. Dzięki
odpowiedniemu sposobowi dotarcia do każdej grupy, członkowie każdej z nich
będą podekscytowani Twoim projektem.
Podobnie jak w przypadku wszystkich form komunikacji, sztuką jest zebranie
opinii. Nie czekaj biernie na pytania: poproś o ich stawianie. Zwracaj uwagę na
język ciała i mimikę odbiorców. Jednym z założeń programowania neurolingwistycznego jest zasada „Sensem Twojej komunikacji jest odpowiedź, którą otrzymujesz”. Gdy się komunikujesz, stale poszerzaj wiedzę o swoich odbiorcach.
Należy wiedzieć, co powiedzieć
Bodaj najtrudniejszym aspektem oficjalnych form komunikacji obowiązujących
w biznesie jest precyzyjne określanie, co naprawdę chcemy powiedzieć. O ile
autorzy beletrystyki mogą pozwolić sobie na szczegółowe planowanie swoich
dzieł przed przystąpieniem do ich tworzenia, ludzie odpowiedzialni za tworzenie
dokumentacji technicznej nierzadko muszą w jednej chwili usiąść przed komputerem i niezwłocznie zacząć spisywać (począwszy od „Rozdział 1. Wprowadzenie”) wszystko, co przyjdzie im do głowy.
Musimy zaplanować, co chcemy powiedzieć. Powinniśmy przygotować szkic
przyszłej wypowiedzi. Warto też odpowiedzieć sobie na pytanie, czy planowana
wypowiedź rzeczywiście wyraża to, co chcemy przekazać rozmówcom. Należy
doskonalić plan wypowiedzi tak długo, aż odpowiedź na to pytanie będzie satysfakcjonująca.
10
Angielskie słowo annoy (denerwujący) pochodzi od starofrancuskiego enui, które
oznacza również „przynudzać”.
4337ebf6db5c7cc89e4173803ef3875a
4
Komunikuj się!

47
Proponowane rozwiązania nie sprawdzają się podczas pisania dokumentów.
Przed pójściem na ważne spotkanie lub odbyciem rozmowy telefonicznej z ważnym klientem warto zanotować sobie pomysły, które mamy zakomunikować,
i zaplanować kilka strategii ich prezentacji.
Teraz, gdy już wiesz, czego chcą Twoi odbiorcy, spróbuj spełnić ich oczekiwania.
Należy wybrać właściwy moment
Jest piątek, godzina 18. Wszyscy uczestnicy spotkania mają za sobą trudny
tydzień. Najmłodsze dziecko szefa jest w szpitalu, na zewnątrz leje jak z cebra,
a powrót do domu w piątkowy wieczór będzie prawdziwym koszmarem. Prawdopodobnie nie jest to najlepszy moment na rozmowę z szefem o konieczności
rozbudowy komputera.
Aby dobrze zrozumieć, co powinni usłyszeć nasi rozmówcy, musimy zidentyfikować ich priorytety. Na przykład pomysły dotyczące repozytoriów kodu źródłowego
najlepiej zaprezentować menedżerowi, który właśnie odbył przykrą rozmowę
z własnym przełożonym po utracie części kodu źródłowego — mamy wówczas
spore szanse, że nasze propozycje trafią na podatny grunt. Zarówno treść
naszych propozycji, jak i moment ich prezentacji muszą odpowiadać na bieżące
problemy firmy. Nierzadko wystarczy po prostu zadać sobie pytanie: Czy to dobry
moment, aby o tym rozmawiać?
Należy wybrać odpowiedni styl
Styl przekazu należy dostosować do charakteru jego odbiorców. Część ludzi oczekuje formalnych prezentacji ograniczających się wyłącznie do faktów. Inni wolą
poprzedzać właściwe rozmowy biznesowe długimi pogawędkami na najróżniejsze
tematy. Podobnie jest w przypadku dokumentów pisanych — niektórzy wolą
długie raporty z szerokimi wyjaśnieniami, inni oczekują raczej krótkich notatek
lub zwięzłych wiadomości poczty elektronicznej. W razie wątpliwości warto
zapytać.
Należy jednak pamiętać, że sami stanowimy połowę tej swoistej transakcji komunikacyjnej. Jeśli ktoś oczekuje akapitu opisującego jakiś aspekt i jeśli wiemy,
że do opisania tego złożonego aspektu będziemy potrzebowali kilku stron, należy
o tym po prostu powiedzieć. Musimy pamiętać, że także reakcje na propozycje
same w sobie stanowią formę komunikacji.
Należy zadbać o warstwę estetyczną
Nasze pomysły są ważne. Zasługują więc na odpowiednią oprawę, aby lepiej
trafić do odbiorców.
4337ebf6db5c7cc89e4173803ef3875a
4
48 
Rozdział 1.
Filozofia pragmatyczna
Zbyt wielu programistów (wraz ze swoimi menedżerami) koncentruje się wyłącznie na treści tworzonych przez siebie dokumentów. Uważamy to za błąd. Każdy
dobry kucharz (lub widz programów kulinarnych) wie, że można zamknąć się
w kuchni na wiele godzin, by następnie w jednej chwili zepsuć cały efekt wskutek kiepskiej prezentacji.
W dzisiejszych czasach nic nie może usprawiedliwić kiepskiego wyglądu drukowanych dokumentów. Współczesne edytory tekstu umożliwiają tworzenie wprost
doskonałych dokumentów niezależnie od tego, czy piszesz używając notacji Markdown, czy stosujesz procesor tekstu. Wystarczy opanować zaledwie kilka prostych poleceń. Jeśli używany przez nas edytor obsługuje arkusze stylów, koniecznie powinniśmy skorzystać z tej możliwości. (Być może nasza firma zdefiniowała
już arkusze stylów, których możemy używać w swoich dokumentach). Warto
nauczyć się ustawiania nagłówków i stopek stron. Jeśli brakuje nam pomysłów
i koncepcji układu dokumentu, wystarczy zajrzeć do przykładowych dokumentów dołączonych do danego edytora. Należy jeszcze sprawdzić pisownię (najpierw
przy użyciu automatycznego narzędzia, a następnie ręcznie). Istnieją kończ
obłędy, których nie morze wychwyci żadne automatyczny mechanizmu.
Należy zaangażować odbiorców
Nierzadko odkrywamy, że proces pracy nad dokumentem jest pod wieloma
względami cenniejszy niż sam dokument. Jeśli to możliwe, warto zaangażować
przyszłych czytelników w prace już nad wczesnymi wersjami dokumentu. Warto
zebrać ich opinie i skorzystać z ich rad. Opisany tryb przygotowywania materiałów pozwoli nie tylko zbudować lepsze relacje ze współpracownikami, ale też
tworzyć lepsze dokumenty.
Należy słuchać innych
Istnieje prosta technika, którą musimy stosować, jeśli chcemy być słuchani przez
innych — musimy sami ich słuchać. Nawet w sytuacji, w której dysponujemy
wszystkimi informacjami, nawet jeśli uczestniczymy w formalnym spotkaniu
z dwudziestoma wysoko postawionymi menedżerami — jeśli nie słuchamy
innych, oni nie będą słuchali nas.
Warto zachęcać ludzi do mówienia, zadając im pytania lub prosząc o streszczenie tego, o czym sami mówiliśmy. Przekształcenie spotkania w dialog znacznie
podniesie efektywność naszego przekazu. Kto wie, być może nawet czegoś się
nauczymy.
Należy wracać do rozmówców
Kiedy zadajemy komuś pytanie, brak jakiejkolwiek odpowiedzi traktujemy jako
przejaw złego wychowania. Czy jednak sami nie ignorujemy wiadomości poczty
elektronicznej lub notatek otrzymywanych od osób proszących o jakieś infor-
4337ebf6db5c7cc89e4173803ef3875a
4
Komunikuj się!

49
macje lub oczekujących jakichś czynności z naszej strony? W dzisiejszym świecie
łatwo o tym zapomnieć. Zawsze powinniśmy odpowiadać na wiadomości poczty
elektronicznej i nagrania na automatycznej sekretarce, nawet jeśli ta odpowiedź będzie brzmiała „wrócę do tego później”. Informowanie innych o zainteresowaniu podnoszonymi problemami przekłada się na dużo większą wyrozumiałość w przypadku sporadycznych błędów i upewnia współpracowników
w przekonaniu o tym, że ktoś pamięta o ich sprawach.
WSKAZÓWKA NR 12
Ważne jest nie tylko to, co mówimy, ale też to, jak to mówimy.
Jeśli nie pracujemy w próżni, musimy opanować sztukę komunikacji. Im bardziej efektywna będzie nasza komunikacja ze współpracownikami, tym większy będzie nasz wpływ na otaczającą nas rzeczywistość.
Dokumentacja
Wreszcie pozostaje kwestia komunikowania się za pośrednictwem dokumentacji. Zazwyczaj programiści nie poświęcają dokumentacji zbyt wiele czasu.
W najlepszym razie jest to nieprzyjemna konieczność; w najgorszym przypadku
jest traktowana jako zadanie o niskim priorytecie, a opracowujący dokumentację mają nadzieję, że kierownictwo o niej zapomni pod koniec projektu.
Pragmatyczni programiści uwzględniają dokumentację jako integralną część
całego cyklu życia projektu. Jej pisanie można ułatwić. Nie trzeba powielać
pracy lub marnować czasu. Dokumentacja może być cały czas pod ręką —
w kodzie źródłowym.
WSKAZÓWKA NR 13
Buduj dokumentację wraz z projektem zamiast ją do niego „przytwierdzać”.
Dobrze wyglądającą dokumentację można łatwo stworzyć na podstawie komentarzy w kodzie źródłowym. Zachęcamy do dodawania komentarzy do modułów
i eksportowanych funkcji, aby ułatwić innym programistom korzystanie
z naszego kodu.
Nie oznacza to jednak, że zgadzamy się z osobami, które mówią, że każda funkcja, struktura danych, deklaracja typu itp., potrzebuje osobnego komentarza.
Pisanie tego rodzaju mechanicznych komentarzy faktycznie utrudnia utrzymanie kodu: po wprowadzeniu zmian są dwie rzeczy do aktualizacji — zarówno
kod, jak i komentarze. Zatem ogranicz komentarze niezwiązane z API do wyjaśniania dlaczego coś zostało zrobione — jaki jest tego cel i przeznaczenie. To
kod powinien pokazywać jak zadanie zostało wykonane, więc zbyt szczegółowe
jego komentowanie jest zbędne i stanowi naruszenie zasady DRY.
4337ebf6db5c7cc89e4173803ef3875a
4
50 
Rozdział 1.
Filozofia pragmatyczna
Komentowanie kodu źródłowego daje doskonałą okazję do udokumentowania
tych nieuchwytnych fragmentów projektu, których nie można udokumentować
nigdzie indziej: kompromisów inżynierskich, powodów podejmowania decyzji,
odrzuconych alternatyw itp.
Podsumowanie

Należy wiedzieć, co powiedzieć.

Należy wiedzieć coś o rozmówcach.

Należy wybrać właściwy moment.

Należy wybrać odpowiedni styl.

Należy zadbać o warstwę estetyczną.

Należy zaangażować odbiorców.

Należy słuchać innych.

Należy wracać do rozmówców.

Należy utrzymywać kod razem z dokumentacją.
Pokrewne podrozdziały

Temat 15., „Szacowanie”.

Temat 18., „Efektywna edycja”.

Temat 45., „Kopalnia wymagań”.

Temat 49., „Pragmatyczne zespoły”.
Wyzwania

Istnieje wiele dobrych książek, których fragmenty poświęcono komunikacji
w ramach zespołów projektowych. Należą do nich The Mythical Man-Month:
Essays on Software Engineering [Bro96] oraz Peopleware: Productive Projects
and Teams [DL13]. Warto przeczytać te pozycje w ciągu najbliższych 18 miesięcy. W książce Trudni współpracownicy [BR89] dodatkowo omówiono problem bagażu emocjonalnego, który każdy z nas wnosi do swojego środowiska pracy.

Kiedy przy najbliższej okazji będziemy przygotowywali prezentację lub pisali
notatkę przekonującą odbiorcę do jakiegoś stanowiska, powinniśmy raz
jeszcze przestudiować przedstawione w tym rozdziale rady. Należy wyraźnie
zidentyfikować odbiorców oraz to, co chcemy im przekazać. Jeśli to możliwe, warto porozmawiać z odbiorcami po prezentacji, aby dowiedzieć się,
na ile słuszne były założenia dotyczące ich potrzeb i oczekiwań.
4337ebf6db5c7cc89e4173803ef3875a
4
Komunikuj się!

Komunikacja online
Wszystko, co do tej pory napisano o komunikacji przy użyciu dokumentów, ma zastosowanie także w przypadku poczty elektronicznej, postów w mediach społecznościowych, blogach itp. W szczególności poczta elektroniczna osiągnęła status podstawowej platformy komunikacji w ramach korporacji i pomiędzy korporacjami. Poczta
elektroniczna służy dzisiaj do negocjowania kontraktów i prowadzenia sporów,
a nierzadko stanowi ważny dowód w sądzie. Okazuje się jednak, że z jakiegoś powodu
ludzie, którzy nigdy nie wysłaliby papierowego dokumentu z najmniejszym niedociągnięciem, beztrosko wysyłają w świat niechlujne, wręcz odpychające wiadomości
poczty elektronicznej.
Wskazówki dotyczące poczty elektronicznej są dość proste:







Należy przeczytać tekst przed kliknięciem przycisku Wyślij.
Należy sprawdzić pisownię.
Należy zachować prosty format.
Należy ograniczać liczbę cytatów. Nikt nie lubi otrzymywać w całości własnej stuwierszowej wiadomości z lakoniczną odpowiedzią „Masz rację”!
Jeśli już cytujemy cudzą wiadomość poczty elektronicznej, koniecznie powinniśmy
odpowiednio wyróżnić cytat i umieścić go w tekście (nie w załączniku). To samo
dotyczy cytowania w mediach społecznościowych.
Nie należy przeklinać; w przeciwnym razie nasze przekleństwa będą nas jeszcze
długo prześladowały.
Przed wysłaniem wiadomości warto jeszcze raz sprawdzić listę adresatów. Byłoby
niezręcznie, gdybyś skrytykował swojego przełożonego w wiadomości rozesłanej do pracowników własnego działu, zapominając, że na liście odbiorców jest
także krytykowany szef. Jeszcze lepiej będzie, jeśli w ogóle nie będziemy krytykować szefa w wysyłanym e-mailu.
Jak przekonało się wielu pracowników licznych korporacji oraz polityków, wiadomości poczty elektronicznej i posty w mediach społecznościowych są wieczne. Należy
przykładać do nich taką samą wagę jak do tradycyjnych notatek i raportów.
4337ebf6db5c7cc89e4173803ef3875a
51
4
52 
Rozdział 1.
Filozofia pragmatyczna
4337ebf6db5c7cc89e4173803ef3875a
4
Rozdział 2.
Postawa pragmatyczna
Istnieją pewne wskazówki i zabiegi, które można z powodzeniem stosować na
wszystkich poziomach wytwarzania oprogramowania. Pewne koncepcje mają
niemal status aksjomatów, a niektóre procesy są wręcz uniwersalne. Co ciekawe, wspomniane rozwiązania rzadko są prezentowane w ten sposób — w większości przypadków są zapisywane raczej w formie dziwacznych, niezrozumiałych
przemyśleń w ramach szerszego omawiania projektowania, zarządzania projektami czy kodowania.
Pierwszy i być może najważniejszy podrozdział: „Istota dobrego projektu” opisuje
sedno wytwarzania oprogramowania. Od projektu wszystko się zaczyna.
Dwa kolejne podrozdziały, zatytułowane „DRY — Przekleństwo powielania”
i „Ortogonalność”, są ze sobą ściśle powiązane. Pierwszy z nich zawiera ostrzeżenie przed powielaniem wiedzy w całych systemach; w drugim podrozdziale przestrzegamy przed dzieleniem jednego elementu wiedzy pomiędzy wiele komponentów systemu.
Coraz większe tempo zmian powoduje, że dotrzymywanie pierwotnych założeń
dotyczących tworzonych aplikacji jest coraz trudniejsze. W podrozdziale „Odwracalność” omówimy pewne techniki ułatwiające zabezpieczenie projektów przed
wpływem zmieniającego się otoczenia.
Także dwa kolejne podrozdziały są ze sobą powiązane. W podrozdziale „Pociski
smugowe” omówimy styl programowania umożliwiający jednoczesne gromadzenie wymagań, testowanie projektów i implementowanie właściwego kodu.
To jedyny sposób dotrzymania tempa dzisiejszemu światu.
W podrozdziale „Prototypy i karteczki samoprzylepne” opisano, jak efektywnie
używać prototypów do testowania architektur, algorytmów, interfejsów i pomysłów. W nowoczesnym świecie kluczowe znaczenie ma testowanie pomsyłów
i zbieranie opinii przed pełnym zaangażowaniem się w wykorzystywanie wybranej koncepcji.
4337ebf6db5c7cc89e4173803ef3875a
4
54 
Rozdział 2.
Postawa pragmatyczna
Informatyka to dziedzina, która powoli dojrzewa. Programiści tworzą swoje aplikacje w językach coraz wyższego poziomu. Chociaż nie wymyślono jeszcze kompilatora, który działałby na podstawie lakonicznego polecenia „zrób to czy tamto”,
w podrozdziale „Języki dziedzinowe” można znaleźć kilka prostych rozwiązań,
które każdy programista może zaimplementować samodzielnie.
I wreszcie, wszyscy pracujemy w warunkach braku czasu i ograniczonych zasobów. Okazuje się, że z oboma problemami można radzić sobie nieporównanie
sprawniej (wprawiając przy okazji w zachwyt przełożonych), jeśli tylko potrafimy
trafnie przewidywać czas potrzebny do realizacji poszczególnych zadań (patrz
podrozdział „Szacowanie”).
Konsekwentne przestrzeganie tych prostych zasad podczas programowania
pozwala pisać lepszy, szybszy i bardziej niezawodny kod. Co więcej, proponowane
rozwiązania mogą sprawić, że nasz kod będzie nawet wyglądał na łatwiejszy.
8
36
Istota dobrego projektu
Świat jest pełen guru i ekspertów. Wszyscy oni są chętni do przekazywania swojej
z trudem zdobytej wiedzy o tym, jak projektować oprogramowanie. Istnieją
akronimy, listy (zwykle złożone z pięciu elementów), wzorce, wykresy, filmy,
rozmowy oraz (jak to w internecie) seria różnych wyjaśnień prawa Demeter.
A my, skromni autorzy tej książki, także jesteśmy trochę temu winni. Chcielibyśmy jednak, w ramach zadośćuczynienia, wyjaśnić coś, co dotarło do nas całkiem niedawno. Najpierw ogólna reguła:
WSKAZÓWKA NR 14
Dobry projekt jest łatwiejszy do modyfikacji niż zły.
Przedmiot jest dobrze zaprojektowany, jeśli dostosowuje się do osób, które
z niego korzystają. W przypadku kodu oznacza to konieczność dostosowania się
do zmian. Dlatego stosujemy zasadę ETC (ang. Easier to Change — dosłownie:
łatwiejsze do zmiany). Zasada ETC. To jest to!
Naszym zdaniem każda zasada projektowania opisana w tej książce jest szczególnym przypadkiem zasady ETC.
Dlaczego eliminowanie sprzężeń jest dobre? Ponieważ dzięki wyizolowaniu
przedmiotów zainteresowania kod staje się łatwiejszy do zmiany. ETC.
Dlaczego zasada pojedynczej odpowiedzialności jest przydatna? Ponieważ zmiana
wymagań może być zaimplementowana przez zmianę tylko w jednym module.
ETC.
4337ebf6db5c7cc89e4173803ef3875a
4
Istota dobrego projektu
 55
Dlaczego jest ważne odpowiednie nazewnictwo? Ponieważ dzięki stosowaniu
odpowiednich nazw kod staje się łatwiejszy do czytania, a żeby zmieniać kod,
trzeba go czytać. ETC!
ETC jest wartością, a nie regułą
Wartości to elementy, które pomagają Ci podjąć decyzje: czy powinienem zrobić to, czy tamto? W przypadku oprogramowania zasada ETC jest przewodnikiem, który pomaga Ci wybrać właściwą ścieżkę. Podobnie jak wszystkie inne
wartości, powinna być stosowana podświadomie, w subtelny sposób prowadzić
Cię we właściwym kierunku.
Ale jak sprawić, aby tak było? Z naszego doświadczenia wynika, że wymaga to
pewnego wstępnego „uzbrojenia” świadomości. Być może powinieneś poświęcić
tydzień na to, by celowo zadawać sobie pytanie: „Czy to, co właśnie zrobiłem,
sprawi, że cały system stanie się łatwiejszy do zmiany, czy trudniejszy?”. Zadaj
sobie to pytanie podczas zapisywania pliku. Zadawaj je podczas pisania testu.
Zadawaj również wtedy, gdy poprawiasz błąd.
W zasadzie ETC istnieje pewna niejawna przesłanka. Zakłada ona, że człowiek
potrafi odpowiedzieć na pytanie o to, która z wielu dostępnych ścieżek pozwoli
w przyszłości na łatwiejsze wprowadzanie zmian. W większości przypadków
wystarczy zastosować zasady zdroworozsądkowe, dzięki którym można w inteligentny sposób odgadnąć właściwą ścieżkę.
Czasem jednak nie mamy żadnych wskazówek. To jest w porządku. Uważamy,
że w takich przypadkach można zrobić dwie rzeczy.
Po pierwsze, biorąc pod uwagę, że nie jesteśmy pewni, jaką formę będą miały
zmiany, zawsze możemy zastosować złotą ścieżkę „łatwości wprowadzania
zmian”: postaraj się, aby kod, który piszesz, można było zastąpić. Dzięki temu,
niezależnie od tego, co się stanie w przyszłości, ten fragment kodu nie będzie
przeszkodą. Choć stosowanie tej zasady wydaje się być czymś ekstremalnym,
w rzeczywistości podczas pisania kodu należy ją stosować zawsze. W praktyce
sprowadza się to do eliminowania sprzężeń z kodu i dbania o jego spójność.
Po drugie, potraktuj stosowanie zasady ETC jako sposób na rozwijanie intuicji.
Opisz sytuację w swoim notatniku inżynierskim: wymień opcje, które masz do
wyboru, oraz kilka możliwości przyszłych zmian. Oznacz kod źródłowy tagiem.
Następnie, gdy będziesz później zmuszony do wprowadzenia zmian w kodzie,
będziesz mógł zajrzeć do notatek i przekazać dla siebie opinię. Może ona pomóc
Ci następnym razem, gdy będziesz zmuszony do podjęcia podobnej decyzji.
W pozostałych podrozdziałach w tym rozdziale opisano konkretne poglądy dotyczące projektowania, ale motywacją dla nich wszystkich jest wymieniona w tym
podrozdziale zasada ETC.
4337ebf6db5c7cc89e4173803ef3875a
4
56 
Rozdział 2.
Postawa pragmatyczna
Pokrewne podrozdziały

Temat 9., „DRY — Przekleństwo powielania”.

Temat 10., „Ortogonalność”.

Temat 11., „Odwracalność”.

Temat 14., „Języki dziedzinowe”.

Temat 28., „Eliminowanie sprzężeń”.

Temat 30., „Programowanie transformacyjne”.

Temat 31., „Podatek od dziedziczenia”.
Wyzwania

Pomyśl o zasadzie projektowania, którą regularnie stosujesz. Czy ta zasada
przyczynia się do łatwości wprowadzania zmian?

Pomyśl również o językach i paradygmatach programowania (OO, FP, Programowanie reaktywne i tak dalej). Czy którykolwiek z tych języków lub
paradygmatów ma określone plusy bądź minusy związane z pisaniem kodu
ETC? Czy któryś z języków bądź paradygmatów ma zarówno plusy, jak
i minusy?
Co możesz zrobić podczas kodowania, aby wyeliminować negatywy i zaakcentować pozytywy1?

9
37
Wiele edytorów zawiera wsparcie (wbudowane albo poprzez rozszerzenia)
do uruchamiania poleceń podczas zapisywaniu pliku. Skonfiguruj swój
edytor w taki sposób, aby przypominał Ci o zasadzie ETC za każdym razem,
gdy zapisujesz plik2. Stosuj to przypomnienie jako wskazówkę do zastanowienia się nad kodem, który właśnie napisałeś. Czy będzie on łatwy do
zmiany?
DRY — przekleństwo powielania
Dostarczanie komputerowi dwóch sprzecznych elementów wiedzy było ulubionym sposobem kapitana Jamesa T. Kirka radzenia sobie z wrogą sztuczną
inteligencją. Okazuje się, niestety, że ten sam schemat może uniemożliwić prawidłowe działanie naszego kodu.
Jako programiści gromadzimy, organizujemy, pielęgnujemy i wykorzystujemy
swoją wiedzę. Wiedzę dokumentujemy też w specyfikacjach, ożywiamy ją, pisząc
działający kod, oraz używamy jej do opracowywania mechanizmów sprawdzających na etapie testowania.
1
Parafrazując stary utwór Arlena i Mercera (Ac-Cent-Tchu-Ate the Positive)…
2
Albo żeby zachować zdrowie psychiczne, na przykład co 10 godzin…
4337ebf6db5c7cc89e4173803ef3875a
4
DRY — przekleństwo powielania

57
Okazuje się jednak, że wiedza nie jest stabilna. Wiedza podlega zmianom, które
nierzadko mają gwałtowny charakter. Nasze rozumienie jakiegoś wymagania
może ulec zmianie po spotkaniu z klientem. Zmianom podlegają regulacje
prawne. Pewne elementy logiki biznesowej z czasem okazują się nieaktualne.
Testy mogą wykazać, że wybrany algorytm nie zdaje egzaminu. Cała ta niestabilność oznacza, że znaczną część swojego czasu pracujemy w trybie konserwacji, próbując reorganizować i ponownie wyrażać wiedzę w swoich systemach.
Większość ludzi zakłada, że konserwacja rozpoczyna się w momencie wydania aplikacji i że sprowadza się do naprawiania błędów i rozbudowy dotychczasowych funkcji. To przekonanie jest naszym zdaniem błędne. Programiści
stale pracują w trybie konserwacji. Nasze rozumienie otoczenia zmienia się praktycznie codziennie. Nowe wymagania pojawiają się w trakcie projektowania lub
kodowania oprogramowania. Nierzadko zmiany dotyczą także otaczającego nas
środowiska. Niezależnie od przyczyn, konserwacja nie jest czynnością dyskretną,
tylko rutynowym elementem całego procesu wytwarzania oprogramowania.
Podczas wykonywania czynności konserwacyjnych musimy odnajdywać i zmieniać reprezentacje pewnych elementów wiedzy — swoistych kapsułek wiedzy
osadzonych w kodzie aplikacji. Problem w tym, że wiedzę można łatwo powielać w tworzonych specyfikacjach, procesach i programach, a każde takie powielenie jest jak zaproszenie do koszmaru konserwacji — problem ujawnia się
jeszcze przed dostarczeniem aplikacji.
Wydaje nam się, że jedynym sposobem tworzenia niezawodnego oprogramowania, który w dodatku ułatwia współpracującym z nami programistom rozumienie i konserwację kodu, jest przestrzeganie zasady DRY:
Każdy wycinek wiedzy musi mieć dokładnie jedną, jednoznaczną i oficjalną
reprezentację w ramach systemu.
Dlaczego mówi się o zasadzie DRY?
WSKAZÓWKA NR 15
Nie powtarzaj się (ang. Don’t Repeat Yourself — DRY).
Alternatywnym rozwiązaniem jest wyrażanie tej samej wiedzy w co najmniej
dwóch miejscach. Zmiana reprezentacji w jednym miejscu oznacza, że musimy
pamiętać o zmianie we wszystkich pozostałych wystąpieniach; w przeciwnym
razie, wzorem komputerów obcych, sprzeczność zawarta w systemie uniemożliwi
jego działanie. To, czy będziemy o tym pamiętać, nie budzi najmniejszych wątpliwości — powinniśmy się raczej zastanowić, kiedy o tym zapomnimy.
Zasada DRY będzie jeszcze wielokrotnie przywoływana w tej książce, często
w kontekstach, które nie mają nic wspólnego z kodowaniem aplikacji. Mamy
wrażenie, że właśnie ta zasada należy do najważniejszych narzędzi w zestawie
pragmatycznego programisty.
4337ebf6db5c7cc89e4173803ef3875a
4
58 
Rozdział 2.
Postawa pragmatyczna
W tym podrozdziale wprowadzimy problemy związane z powielaniem wiedzy
i zasugerujemy ogólne strategie ich rozwiązywania.
DRY to więcej niż kod
Na początek coś wyjaśnijmy. W pierwszym wydaniu tej książki niezbyt dobrze
opisaliśmy, co mamy na myśli pisząc o zasadzie DRY. Wiele osób odnosiło tę
zasadę wyłącznie do kodu. Zrozumieli oni, że DRY oznacza „nie kopiuj i nie
wklejaj wierszy kodu źródłowego”.
Istotnie, to jest część zasady DRY, ale niewielka i dość trywialna.
Zasada DRY dotyczy powielania wiedzy i zamiarów. Dotyczy wyrażania tego
samego w dwóch różnych miejscach, czasami na dwa zupełnie różne sposoby.
Oto „papierek lakmusowy”: czy kiedy jakiś pojedynczy aspekt kodu musi się
zmienić, musisz uwzględnić tę zmianę w wielu miejscach oraz w wielu różnych
formatach? Czy musisz zmienić kod i dokumentację lub schemat bazy danych
i jej strukturę, czy też nie musisz tego robić? Jeśli odpowiedziałeś twierdząco
na to pytanie, to Twój kod nie spełnia zasady DRY.
Zatem przyjrzyjmy się kilku typowym przykładom powielania.
Powielanie w kodzie
Być może to stwierdzenie jest trywialne, ale powielanie kodu jest bardzo
powszechne. Oto przykład:
def print_balance(account)
printf "Obciążenia: %10.2f\n", account.debits
printf "Uznania: %10.2f\n", account.credits
if account.fees < 0
printf "Opłaty: %10.2f-\n", -account.fees
else
printf "Opłaty: %10.2f\n", account.fees
end
printf " ———-\n"
if account.balance < 0
printf "Saldo: %10.2f-\n", -account.balance
else
printf "Saldo: %10.2f\n", account.balance
end
end
Na razie zignorujemy fakt, że popełniamy błąd typowy dla nowicjuszy: zapisywanie waluty w zmiennych typu float. Zamiast tego sprawdź, czy potrafisz
dostrzec w tym kodzie symptomy powielania (my widzimy co najmniej trzy takie
oznaki, ale być może jest ich więcej).
Co udało Ci się dostrzec? Oto nasza lista.
4337ebf6db5c7cc89e4173803ef3875a
4
DRY — przekleństwo powielania

59
Po pierwsze wyraźnie widać stosowanie techniki kopiuj i wklej przy obsłudze
liczb ujemnych. Możemy to poprawić poprzez dodanie nowej funkcji:
def format_amount(value)
result = sprintf("%10.2f", value.abs)
if value < 0
result + "-"
else
result + " "
end
end
def print_balance(account)
printf "Obciążenia: %10.2f\n", account.debits
printf "Uznania: %10.2f\n", account.credits
printf "Opłaty: %s\n", format_amount(account.fees)
printf " ———-\n"
printf "Saldo: %s\n", format_amount(account.balance)
end
Innym objawem powielania jest powtarzanie szerokości pola we wszystkich
wywołaniach funkcji printf. Możemy rozwiązać ten problem poprzez wprowadzenie stałej i przekazanie jej do każdego wywołania, ale możemy także użyć
istniejącej funkcji.
def format_amount(value)
result = sprintf("%10.2f", value.abs)
if value < 0
result + "-"
else
result + " "
end
end
def print_balance(account)
printf "Obciążenia: %s\n", format_amount(account.debits)
printf "Uznania: %s\n", format_amount(account.credits)
printf "Opłaty: %s\n", format_amount(account.fees)
printf " ———-\n"
printf "Saldo: %s\n", format_amount(account.balance)
end
Czy coś jeszcze? A co się stanie, jeśli klient prosi o dodatkową spację pomiędzy
etykietami a liczbami? Musielibyśmy wprowadzić zmiany w pięciu wierszach.
Usuńmy to powtórzenie:
def format_amount(value)
result = sprintf("%10.2f", value.abs)
if value < 0
result + "-"
else
result + " "
end
end
def print_line(label, value)
printf "%-9s%s\n", label, value
end
4337ebf6db5c7cc89e4173803ef3875a
4
60 
Rozdział 2.
Postawa pragmatyczna
def report_line(label, amount)
print_line(label + ":", format_amount(amount))
end
def print_balance(account)
report_line("Obciążenia", account.debits)
report_line("Uznania", account.credits)
report_line("Opłaty", account.fees)
print_line("", "———-")
report_line("Saldo", account.balance)
end
Jeśli będziemy zmuszeni do zmiany formatowania kwot, zmienimy funkcję
format_amount. Jeśli będziemy chcieli zmienić format etykiety, zmienimy funkcję
report_line.
W tym kodzie nadal istnieje niejawne naruszenie zasady DRY: liczba kresek
w wierszu separatora jest związana z szerokością pola amount. Ale nie jest to
dokładne dopasowanie: wiersz separatora jest o jeden znak krótszy, więc wszystkie końcowe znaki minus wykraczają poza kolumnę. Taka była intencja klienta,
która różni się od zasad prawidłowego formatowania kwot.
Nie każde powielenie kodu jest powieleniem wiedzy
W aplikacji online do zamawiania wina pobierasz i weryfikujesz wiek użytkownika oraz zamawianą ilość butelek. Według wymagań właściciela witryny, obie
te wartości powinny być liczbami i obie powinny być większe od zera. Oto kod
walidacji wprowadzanych wartości:
def validate_age(value):
validate_type(value, :integer)
validate_min_integer(value, 0)
def validate_quantity(value):
validate_type(value, :integer)
validate_min_integer(value, 0)
Podczas przeglądu kodu, wszystkowiedzący recenzent odrzucił ten kod, twierdząc, że jest to naruszenie zasady DRY: treść obu funkcji jest identyczna.
Nie ma racji. Kod jest taki sam, ale wiedza, którą reprezentują funkcje, różni się.
Te dwie funkcje sprawdzają poprawność dwóch oddzielnych rzeczy, dla których
w pewnym momencie obowiązują takie same zasady poprawności. To zbieg okoliczności, a nie powielanie.
Powielanie w dokumentacji
W jakiś sposób powstał mit, że należy komentować wszystkie funkcje. Osoby,
które popierają to szaleństwo, tworzą coś takiego:
# Oblicza opłaty dla tego konta.
#
# * Każdy zwrócony czek kosztuje 20 PLN
4337ebf6db5c7cc89e4173803ef3875a
4
DRY — przekleństwo powielania

61
# * Jeśli na rachunku występuje debet dłużej niż 3 dni,
# nalicza opłatę 10 PLN za każdy dzień
# * Jeśli średnie saldo rachunku jest większe niż 2000 PLN,
# zmniejsza opłaty o 50%
def fees(a)
f = 0
if a.returned_check_count > 0
f += 20 * a.returned_check_count
end
if a.overdraft_days > 3
f += 10*a.overdraft_days
end
if a.average_balance > 2_000
f /= 2
end
f
end
Istota działania tej funkcji została podana dwa razy: raz w komentarzu i ponownie w kodzie. Jeśli klient zmieni opłatę, aktualizację trzeba wprowadzić w dwóch
miejscach. Z biegiem czasu z dużą dozą pewności można założyć, że komentarz przestanie być zgodny z kodem.
Zadaj sobie pytanie, jaką wartość dodaje komentarz do kodu. Z naszego punktu
widzenia komentarz jedynie rekompensuje niezbyt dobre nazewnictwo i układ.
A gdyby tak funkcja miała następującą treść:
def calculate_account_fees(account)
fees = 20 * account.returned_check_count
fees += 10 * account.overdraft_days if account.overdraft_days > 3
fees /= 2 if account.average_balance > 2_000
fees
end
Nazwa mówi, co robi funkcja, a jeśli ktoś potrzebuje szczegółów, są one wyjaśnione w kodzie źródłowym. Ten kod spełnia zasadę DRY!
Naruszenia zasady DRY w danych
Struktury danych reprezentują wiedzę. Jeśli są nieprawidłowo skonstruowane,
mogą doprowadzić do konfliktu z zasadą DRY. Przyjrzyjmy się poniższej klasie
reprezentującej odcinek:
class Line {
public:
Point start;
Point end;
double length;
};
Na pierwszy rzut oka klasa w tej formie wydaje się rozsądna. Odcinek, oczywiście, ma swój początek i koniec oraz zawsze ma jakąś długość (nawet jeśli ta
długość wynosi zero). Okazuje się jednak, że mamy tutaj do czynienia z powieleniem informacji. Długość jest uzależniona od położenia punktu początkowego
4337ebf6db5c7cc89e4173803ef3875a
4
62 
Rozdział 2.
Postawa pragmatyczna
i punktu końcowego — przesunięcie któregoś z tych punktów może powodować
zmianę długości. W tej sytuacji lepszym rozwiązaniem będzie przekształcenie
długości w pole wyliczane:
class Line {
public:
Point start;
Point end;
double length() { return start.distanceTo(end); }
};
Na późniejszych etapach procesu wytwarzania oprogramowania możemy świadomie podjąć decyzję o naruszeniu zasady DRY z myślą o poprawie wydajności.
Taka sytuacja często ma miejsce wtedy, gdy chcemy przechowywać w pamięci
podręcznej jakieś dane, aby uniknąć konieczności powtarzania kosztownych
obliczeń. Cała sztuka polega wówczas na właściwej ocenie skutków tej decyzji.
Naruszenie zasady DRY nie powinno być widoczne z zewnątrz — za zachowanie spójności powinny odpowiadać wyłącznie metody wchodzące w skład danej
klasy.
class Line {
private double length;
private Point start;
private Point end;
public Line(Point start, Point end) {
this.start = start;
this.end = end;
calculateLength();
}
// metody publiczne
void setStart(Point p) { this.start = p; calculateLength(); }
void setEnd(Point p) { this.end = p; calculateLength(); }
Point getStart() { return start; }
Point getEnd() { return end; }
double getLength() { return length; }
private void calculateLength() {
this.length = start.distanceTo(end);
}
};
Przykład ten ilustruje również ważną kwestię: zawsze gdy moduł ujawnia strukturę danych, sprzęgamy cały kod, który używa tej struktury z implementacją
modułu. Wszędzie, gdzie to możliwe, do odczytu i zapisu atrybutów obiektów
należy używać funkcji akcesorów. Dzięki temu dodawanie funkcjonalności będzie
w przyszłości łatwiejsze.
Korzystanie z funkcji akcesorów wiąże się z zasadą jednorodnego dostępu
Meyera, opisaną w książce Object-Oriented Software Construction [Mey97]. Zasada
ta stanowi, że wszystkie usługi oferowane przez moduł powinny być dostępne
za pośrednictwem jednorodnej notacji, która nie zdradza, czy są one realizowane
poprzez składowanie, czy przez obliczenia.
4337ebf6db5c7cc89e4173803ef3875a
4
DRY — przekleństwo powielania

63
Powielanie na poziomie reprezentacji
Twój kod komunikuje się ze światem zewnętrznym: innymi bibliotekami za
pośrednictwem API, innymi usługami za pośrednictwem zdalnych wywołań,
danymi z zewnętrznych źródeł i tak dalej. I prawie za każdym razem, gdy to
robisz, wprowadzasz jakieś naruszenie zasady DRY: Twój kod musi mieć świadomość, że jest obecny w kodzie zewnętrznym. Musi on znać API, schemat, znaczenie kodów błędów lub inne szczegóły. Powielanie w tym przypadku ujawnia
się w tym, że dwa elementy (kod i komponent zewnętrzny) muszą posiadać
wiedzę dotyczącą reprezentacji swoich interfejsów. Jeśli zmienimy go w jednym
komponencie, drugi może przestać działać.
To powielenie jest nieuniknione, ale można złagodzić skutki jego występowania.
Oto kilka strategii.
Powielanie za pośrednictwem wewnętrznych interfejsów API
W przypadku wewnętrznych interfejsów API, należy szukać narzędzi, które
pozwalają określić interfejs API w jakiejś neutralnej formie. Narzędzia te zazwyczaj generują dokumentację, makiety API, testy funkcjonalne i klienty API —
te ostatnie w wielu różnych językach. W idealnym przypadku takie narzędzie
powinno przechowywać wszystkie API w centralnym repozytorium, co umożliwia ich współdzielenie między zespołami.
Powielanie za pośrednictwem zewnętrznych interfejsów API
Publiczne interfejsy API coraz częściej są dokumentowane formalnie przy użyciu
mechanizmów podobnych do OpenAPI3. Pozwala to na importowanie specyfikacji
API do lokalnych narzędzi i ich bardziej niezawodne integrowanie z usługą.
Jeśli nie ma takiej specyfikacji, należy rozważyć jej utworzenie i opublikowanie.
Taka specyfikacja nie tylko przyda się użytkownikom, ale także poprawi możliwości utrzymania interfejsu.
Powielanie za pośrednictwem źródeł danych
Wiele źródeł danych pozwala na introspekcję ich schematu danych. Mechanizmy
introspekcji można wykorzystać do usunięcia sporej części powielania pomiędzy
źródłami danych a kodem. Zamiast ręcznego tworzenia kodu zawierającego składowane dane, można wygenerować kontenery bezpośrednio na podstawie schematu. Te uciążliwe operacje może za nas wykonać wiele frameworków utrwalania danych.
Istnieje także inna opcja, którą preferujemy. Zamiast pisania kodu, który reprezentuje zewnętrzne dane w stałej strukturze (na przykład egzemplarzu struktury lub klasy), można utrzymywać je w formacie klucz-wartość (w języku
3
https://github.com/OAI/OpenAPI-Specification
4337ebf6db5c7cc89e4173803ef3875a
4
64 
Rozdział 2.
Postawa pragmatyczna
programowania taka konstrukcja może nazywać się mapą, tablicą asocjacyjną,
słownikiem lub nawet obiektem).
Takie postępowanie, bez stosowania innych mechanizmów, może być ryzykowne:
pogarsza bezpieczeństwo, ponieważ nie wiemy, z jakimi danymi pracujemy.
Z tego powodu zalecamy dodanie drugiej warstwy do tego rozwiązania: prostego
zestawu reguł walidacyjnych konfigurowanych przez tablicę, który sprawdza,
czy utworzona mapa zawiera potrzebne dane w odpowiednim formacie. Taką
tablicę mogą generować niektóre narzędzia do tworzenia dokumentacji API.
Powielanie wśród wielu programistów
Z zupełnie inną sytuacją mamy do czynienia w przypadku zjawiska powielania
zachodzącego wśród wielu różnych programistów zaangażowanych w projekt —
tu wykrycie i wyeliminowanie problemu jest szczególnie trudne. Całe zbiory funkcji mogą być przypadkowo powielane i te powielenia mogą pozostawać niewykryte całymi latami, prowadząc do poważnych problemów związanych z konserwacją. Słyszeliśmy kiedyś od dobrze poinformowanej osoby historię o jednym
z amerykańskich stanów, którego rządowy komputer był poddany badaniom pod
kątem odporności na problem 2000 roku. Audyt wykazał istnienie ponad 10 tys.
programów, z których każdy stosował własną wersję weryfikacji numerów ubezpieczenia społecznego (SSN).
Na wysokim poziomie należy radzić sobie z tym problemem poprzez stworzenie zespołu, który dobrze ze sobą współpracuje.
Na poziomie modułów wspomniany problem okazuje się jednak bardziej podstępny. Często potrzebne funkcje czy dane, które trudno jednoznacznie skojarzyć z konkretnym obszarem odpowiedzialności, mogą być implementowane
wiele razy w ramach tego samego projektu.
Wydaje się, że najlepszym sposobem radzenia sobie z tym problemem jest
zachęcanie członków zespołu do aktywnej i możliwie częstej komunikacji.
Warto przeprowadzać codzienne spotkanie stand-up. Można stworzyć fora (np.
kanały Slack) umożliwiające swobodną dyskusję o typowych problemach. Dzięki
temu możliwa jest nieinwazyjna komunikacja, także z udziałem programistów
zatrudnionych w różnych miejscach, z możliwością trwałego zapisywania formułowanych wniosków).
Warto wyznaczyć jednego członka zespołu do roli swoistego bibliotekarza projektu, którego zadaniem będzie wspieranie procesu wymiany wiedzy. Dobrym
pomysłem jest też wyznaczenie centralnego miejsca w drzewie kodu źródłowego,
w którym będzie można zapisywać procedury i skrypty użytkowe. Należy też
wypracować procedury czytania cudzego kodu źródłowego i dokumentacji (albo
w sposób nieformalny, albo w ramach przeglądów kodu). Nie chodzi o wtykanie
nosa w nie swoje sprawy — naszym celem jest nauka. Musimy przy tym pamiętać
4337ebf6db5c7cc89e4173803ef3875a
4
Ortogonalność
 65
o zasadzie wzajemności — nie możemy zazdrośnie strzec własnego kodu przed
wzrokiem współpracowników.
WSKAZÓWKA NR 16
Należy dbać o możliwość wielokrotnego stosowania kodu.
Naszym celem jest stworzenie środowiska, w którym znajdowanie i ponowne
wykorzystywanie istniejących rozwiązań będzie łatwiejsze niż samodzielne pisanie analogicznego kodu. Jeśli to nie będzie dostatecznie łatwe, nikt nie będzie
tego robił. A jeśli istniejące rozwiązania nie są ponownie używane, ryzykujemy
powielanie wiedzy.
Pokrewne podrozdziały
10
38

Temat 8., „Istota dobrego projektu”.

Temat 28., „Eliminowanie sprzężeń”.

Temat 32., „Konfiguracja”.

Temat 38, „Programowanie przez koincydencję”.

Temat 40., „Refaktoryzacja”.
Ortogonalność
Ortogonalność jest podstawową koncepcją dla każdego programisty zainteresowanego tworzeniem systemów łatwych do zaprojektowania, skonstruowania,
przetestowania i rozwijania. Okazuje się jednak, że idea ortogonalności rzadko
jest bezpośrednio proponowana studentom. Ortogonalność nierzadko ma postać
niejawnej, ukrytej cechy rozmaitych metod i technik, które poznajemy w trakcie
studiów. To błąd. Kiedy programista opanuje sztukę bezpośredniego stosowania
zasady ortogonalności, od razu zauważy poprawę jakości tworzonych przez siebie systemów.
Czym jest ortogonalność?
Ortogonalność to pojęcie zaczerpnięte z geometrii. Dwie proste są ortogonalne,
jeśli przecinają się pod kątem prostym (tak jest na przykład w przypadku osi
układu współrzędnych). W przypadku wektorów mówi się o liniowej niezależności. Kiedy wartość 1 na wykresie przesunie się w kierunku północnym, nie
ma to wpływu na to, jak daleko jest ona w kierunkach wschodnim lub zachodnim. Wartość 2 przemieszcza się w kierunku wschodnim, ale nie w północnym
lub południowym.
4337ebf6db5c7cc89e4173803ef3875a
4
66 
Rozdział 2.
Postawa pragmatyczna
W świecie komputerów interesujący nas termin oznacza rodzaj niezależności lub
izolacji. Mówimy, że co najmniej dwa elementy są ortogonalne, jeśli zmiana
jednego z nich nie wpływa na pozostałe. W dobrze zaprojektowanym systemie
kod bazy danych jest ortogonalny względem interfejsu użytkownika — oznacza
to, że możemy zmienić ten interfejs bez wpływu na bazę danych oraz wymienić system bazy danych na inny bez wpływu na interfejs.
Zanim przeanalizujemy korzyści wynikające ze stosowania systemu ortogonalnego, przeanalizujemy system, który nie jest ortogonalny.
System nieortogonalny
Odbywamy turystyczny lot helikopterem w Wielkim Kanionie, gdy nagle pilot,
który nierozważnie przejadł się rybą podczas lunchu, blednie i traci przytomność. Na szczęście, w ostatnim odruchu udało mu się ustabilizować maszynę
kilkadziesiąt metrów nad ziemią.
Na szczęście poprzedniego wieczoru czytałeś stronę Wikipedii poświęconą helikopterom. Wiesz, że do sterowania helikopterem służą cztery podstawowe przyrządy. W prawej dłoni pilot trzyma drążek sterowy (do tzw. sterowania okresowego). Zmiana położenia tego drążka powoduje ruch helikoptera w odpowiednim
kierunku. W lewej ręce pilot trzyma dźwignię skoku. Pociągnięcie tej dźwigni do
góry powoduje zwiększenie kąta natarcia wszystkich łopat wirnika i — tym
samym — wygenerowanie większej siły nośnej. Na końcu dźwigni skoku znajduje się przyrząd do sterowania obrotami silnika. I wreszcie, pilot dysponuje
dwoma pedałami sterującymi kątem natarcia wirnika ogonowego i ułatwiającymi
obrót śmigłowca wokół osi pionowej.
„Łatwe” — myślisz sobie — delikatne obniżenie położenia dźwigni skoku spowoduje powolne zbliżanie się do ziemi. Czujesz się bohaterem. Kiedy jednak próbujesz to zrobić, szybko odkrywasz, że życie jest bardziej skomplikowane. Nos
helikoptera pochylił się, a cała maszyna zaczęła coraz szybciej obracać się
w lewo. Nagle odkryłeś, że w systemie sterowania śmigłowcem każdy ruch przyrządem prowadzi do jakichś skutków ubocznych. Obniżenie dźwigni trzymanej
lewą ręką wymaga korekty położenia drążka trzymanego w prawej dłoni i lekkiego dociśnięcia prawego pedału. Co więcej, każda z tych zmian ponownie
4337ebf6db5c7cc89e4173803ef3875a
4
Ortogonalność
 67
wpływa na wszystkie pozostałe przyrządy. Żonglujemy więc niewiarygodnie złożonym systemem, w którym każda, nawet najdrobniejsza modyfikacja wpływa
na wszystkie pozostałe elementy. Obciążenie, jakiemu jesteśmy poddawani, jest
wprost niewiarygodne — nasze dłonie i stopy stale zmieniają położenie przyrządów, próbując reagować na te wszystkie wzajemnie oddziałujące siły.
Sterowanie helikopterem z pewnością nie jest systemem ortogonalnym.
Zalety ortogonalności
Jak pokazuje przykład helikoptera, systemy nieortogonalne są z natury rzeczy
bardziej wymagające zarówno w kontekście zmian, jak i zwykłego sterowania.
Kiedy komponenty systemu są powiązane silnymi, wzajemnymi zależnościami,
nie może być mowy o takich rozwiązaniach jak lokalne poprawki.
WSKAZÓWKA NR 17
Należy eliminować wzajemny wpływ niepowiązanych elementów.
Chcemy projektować autonomiczne komponenty, czyli niezależne byty tworzone
z myślą o jednym, precyzyjnie zdefiniowanym celu (Yourdon i Constantine określają tę cechę mianem spójności [YC79]). Jeśli poszczególne komponenty są od
siebie odizolowane, możemy być pewni, że zmiana jednego z nich nie będzie
wymagała troski o pozostałe. Dopóki nie zmieniamy interfejsów zewnętrznych
naszych komponentów, możemy być pewni, że wprowadzane modyfikacje nie
spowodują problemów ujawniających się w różnych częściach systemu.
Pisanie ortogonalnych systemów ma dwie podstawowe zalety: poprawia produktywność i ogranicza ryzyko.
Wyższa produktywność

Zmiany są ściśle związane z konkretnymi miejscami, zatem czas wytwarzania i testowania można znacznie skrócić. Pisanie stosunkowo niewielkich,
autonomicznych komponentów jest nieporównanie prostsze od tworzenia
jednego wielkiego bloku kodu. Proste komponenty po zaprojektowaniu,
zakodowaniu i poddaniu testom jednostkowym można po prostu zapomnieć — nie ma potrzeby ustawicznego modyfikowania istniejącego kodu
przy okazji dodawania nowych elementów.

Model ortogonalny dodatkowo zwiększa możliwości wielokrotnego stosowania tych samych rozwiązań. Jeśli komponenty mają przypisane konkretne, precyzyjnie zdefiniowane obszary odpowiedzialności, można je
z powodzeniem łączyć z nowymi komponentami, stosując techniki, o których twórcy oryginału nawet nie pomyśleli. Im luźniejsze będą związki
w naszych systemach, tym prostsze będzie ich ponowne konfigurowanie
i rekonstruowanie.
4337ebf6db5c7cc89e4173803ef3875a
4
68 

Rozdział 2.
Postawa pragmatyczna
Produktywność nieznacznie wzrasta także w przypadku łączenia ortogonalnych komponentów. Przypuśćmy, że jeden komponent wykonuje M zadań,
a inny komponent wykonuje N odrębnych zadań. Jeśli oba komponenty są
ortogonalne i jeśli je łączymy, otrzymujemy M×N zadań. Jeśli jednak oba
komponenty nie są ortogonalne, część ich funkcji będzie się pokrywała,
zatem w wyniku tego połączenia otrzymamy mniejszą liczbę rozwiązań.
Okazuje się więc, że łączenie ortogonalnych elementów pozwala uzyskać
większą liczbę funkcji w przypadku tych samych nakładów.
Mniejsze ryzyko
Model ortogonalny pozwala ograniczyć ryzyko związane z wytwarzaniem każdego kodu.

Problematyczne sekcje kodu są izolowane. Jeśli jakiś moduł jest źle zaprojektowany lub zaimplementowany, prawdopodobieństwo występowania
symptomów tych niedociągnięć w całym systemie będzie mniejsze. Właściwie odizolowany kod można też nieporównanie łatwiej dzielić i przenosić do nowych, prawidłowo zaprojektowanych modułów.

Utworzony w ten sposób system jest mniej wrażliwy na zmiany. Drobne
zmiany i poprawki są wprowadzane w konkretnym obszarze, a ewentualne
problemy wynikające z tych zmian dotyczą tylko tego obszaru.

Ortogonalny system prawdopodobnie zostanie też lepiej przetestowany,
ponieważ projektowanie i wykonywanie testów na komponentach takich
systemów jest dużo prostsze.

Ortogonalność eliminuje ścisłe zależności z tym czy innym producentem,
produktem lub platformą, ponieważ interfejsy łączące nasz system z komponentami zewnętrznymi są odizolowane i mają postać drobnych elementów składowych ogólnego procesu wytwarzania.
Przeanalizujmy teraz inne sposoby stosowania zasady ortogonalności w codziennej pracy.
Projekt
Większość programistów doskonale rozumie potrzebę projektowania ortogonalnych systemów, mimo że używają nieco innych określeń do opisywania tego
procesu (mówią o systemach modułowych, komponentowych czy wielowarstwowych). Systemy powinny się składać ze zbioru współpracujących modułów, z których każdy implementuje funkcje niezależne od mechanizmów oferowanych przez pozostałe moduły. W pewnych przypadkach komponenty organizuje
się w ramach warstw, z których każda udostępnia jeden poziom abstrakcji. Model
wielowarstwowy jest jednym z najlepszych sposobów projektowania systemów
ortogonalnych. Ponieważ każda warstwa używa tylko abstrakcji udostępnianych przez warstwy znajdujące się pod nią, możemy pozwolić sobie na daleko
idącą elastyczność podczas modyfikowania implementacji bez wpływu na kod
4337ebf6db5c7cc89e4173803ef3875a
4
Ortogonalność
 69
w pozostałych warstwach. Podział na warstwy ogranicza ryzyko powstawania
przypadkowych zależności pomiędzy modułami. Podział na warstwy często
wyraża się w formie diagramów podobnych do tego z rysunku poniżej.
Istnieje pewien prosty test ortogonalności projektu. Po rozplanowaniu komponentów warto zadać sobie następujące pytanie: na ile modułów wpłynęłaby
zasadnicza zmiana wymagań dotyczących jednej funkcji? W ortogonalnym systemie odpowiedź zawsze powinna brzmieć „jeden”4. Przeniesienie przycisku na
panelu graficznego interfejsu użytkownika nie powinno wymagać zmiany schematu bazy danych. Dodanie pomocy kontekstowej nie powinno wymagać modyfikacji podsystemu generowania faktur.
Przeanalizujmy przykład złożonego systemu do monitorowania pracy ciepłowni
i sterowania jej funkcjonowaniem. Przyjmijmy, że oryginalne wymagania obejmowały opracowanie graficznego interfejsu użytkownika i że z czasem w miejsce
tego interfejsu zażądano stworzenia systemu odpowiedzi głosowej i mechanizmu sterowania za pośrednictwem klawiatury telefonu. W systemie zaprojektowanym zgodnie z zasadą ortogonalności zmiana tego wymagania wymagałaby
modyfikacji tylko modułów ściśle powiązanych z interfejsem użytkownika —
znajdująca się poniżej logika sterowania pracą ciepłowni powinna pozostać
niezmieniona. W praktyce odpowiednio zaprojektowana struktura systemu
powinna umożliwić nawet obsługę obu interfejsów bez najmniejszych zmian
wewnętrznej bazy kodu.
Warto też sprawdzić, na ile nasz projekt jest odporny na zmiany zachodzące
w rzeczywistym świecie. Czy używamy na przykład numeru telefonu w roli identyfikatora klienta? Co będzie, jeśli operator telekomunikacyjny zmieni sposób
przypisywania numerów kierunkowych? Nie powinniśmy uzależniać naszego
systemu od właściwości, na które nie mamy wpływu.
4
W rzeczywistości takie założenie okazuje się jednak dość naiwne. Jeśli nie mamy wyjątkowego szczęścia, większość typowych zmian wymagań będzie wpływała na wiele funkcji
naszego systemu. Jeśli jednak analizujemy skutki zmian na poziomie funkcji, powinniśmy pamiętać, że w idealnych warunkach modyfikacja powinna wpływać tylko na jeden
moduł.
4337ebf6db5c7cc89e4173803ef3875a
4
70 
Rozdział 2.
Postawa pragmatyczna
Zestawy narzędzi i biblioteki
O zachowanie ortogonalności systemu należy dbać także w trakcie dodawania
zestawów narzędzi i bibliotek innych producentów. Powinniśmy rozważnie dobierać stosowane technologie.
Kiedy rozważamy użycie jakiegoś zestawu narzędzi (a nawet biblioteki autorstwa innego członka naszego zespołu), powinniśmy zadać sobie pytanie, czy nowe
rozwiązania będą wymagały wprowadzenia nieuzasadnionych zmian w dotychczasowym kodzie. Jeśli schemat utrwalania obiektów jest transparentny z perspektywy pozostałego kodu, mamy do czynienia z mechanizmem ortogonalnym.
Jeśli jednak ten schemat wymaga utworzenia specjalnych obiektów dostępu,
z pewnością nie jest ortogonalny. Izolowanie tego rodzaju szczegółów od własnego kodu ma też tę zaletę, że ułatwia zmianę dostawcy dodatkowych rozwiązań w przyszłości.
Ciekawym przykładem ortogonalności jest system Enterprise Java Beans (EJB).
W większości systemów transakcyjnych kod aplikacji musi wprost wskazywać
początek i koniec każdej transakcji. W technologii EJB informacje tego typu
wyraża się deklaratywnie w formie metadanych, a więc poza właściwym kodem.
Ten sam kod aplikacji można wykonywać w różnych środowiskach transakcyjnych technologii EJB bez konieczności wprowadzania jakichkolwiek zmian.
Opisany model najprawdopodobniej będzie powielany w wielu przyszłych środowiskach.
W pewnym sensie EJB jest przykładem wzorca Dekorator: polega na dodawaniu funkcjonalności do kodu bez jego zmieniania. Ten styl programowania może
być stosowany w niemal każdym języku programowania i niekoniecznie wymaga
frameworka lub biblioteki. Jedyne, co jest potrzebne, to po prostu nieco dyscypliny podczas programowania.
Kodowanie
Za każdym razem, gdy piszemy jakiś kod, musimy liczyć się z ryzykiem ograniczenia ortogonalności naszej aplikacji. Jeśli stale nie monitorujemy zarówno
naszych bieżących poczynań, jak i szerszego kontekstu budowy naszej aplikacji, możemy przypadkowo powielić funkcje zaimplementowane już w jakimś
innym module lub dwukrotnie wyrazić istniejącą wiedzę.
Istnieje wiele technik, których można z powodzeniem używać do zachowywania
ortogonalności:

Należy dbać o izolację kodu. Powinniśmy pisać skromny kod, czyli moduły, które bez potrzeby nie udostępniają wszystkich swoich funkcji pozostałym modułom, ale też przesadnie nie korzystają z implementacji innych
modułów. Warto zapoznać się z prawem Demeter [LH89], które zostanie
omówione w temacie 28., „Eliminowanie sprzężeń”. Jeśli musimy zmienić
stan jakiegoś obiektu, powinniśmy doprowadzić do sytuacji, w której inte-
4337ebf6db5c7cc89e4173803ef3875a
4
Ortogonalność
 71
resujący nas stan zostanie zmieniony przez sam obiekt. W ten sposób
możemy zachować izolację naszego kodu względem implementacji zawartych w cudzym kodzie i — tym samym — zwiększyć szanse zachowania
dotychczasowej ortogonalności systemu.

Należy unikać danych globalnych. Za każdym razem, gdy nasz kod
odwołuje się do danych globalnych, jest wiązany z pozostałymi komponentami używającymi tych danych. Nawet dane globalne, które w założeniu
mają być tylko odczytywane, mogą powodować poważne problemy (jeśli na
przykład nagle będziemy musieli przystosować nasz kod do pracy wielowątkowej). Ogólnie nasz kod jest bardziej zrozumiały i łatwiejszy w konserwacji, jeśli wprost przekazujemy cały niezbędny kontekst do swoich
modułów. W aplikacjach obiektowych kontekst często jest przekazywany
w formie parametrów konstruktorów obiektów. W kodzie pozostałych aplikacji możemy tworzyć struktury reprezentujące kontekst i przekazywać referencje do tych struktur.
Wzorzec projektowy Singleton opisany w książce Wzorce projektowe
[GHJV95] gwarantuje nam, że będzie istniał tylko jeden egzemplarz obiektu
określonej klasy. Wielu programistów używa tych singletonowych obiektów
w roli swoistych zmiennych globalnych (szczególnie w takich językach jak
Java, gdzie obsługa danych globalnych w inny sposób jest niemożliwa).
Obiekty singletonowe należy stosować rozważnie — także one mogą rodzić
zbędne powiązania.

Należy unikać podobnych funkcji. W swojej pracy często spotykamy
zbiory funkcji, które wyglądają bardzo podobnie — zdarza się, że kod na
ich początku i końcu jest identyczny, a różnice sprowadzają się do stosowania odmiennych algorytmów centralnych. Powielony kod zawsze należy
traktować jako symptom problemów strukturalnych. Warto więc zapoznać
się ze wzorcem Strategia opisanym w książce Wzorce projektowe i podjąć
próbę opracowania lepszej implementacji.
Należy opanować sztukę krytycznej oceny własnego kodu. Warto poszukiwać
wszelkich okazji do reorganizacji kodu z myślą o poprawie jego struktury i ortogonalności i wykorzystywać je. Odpowiedni proces, który określa się mianem
refaktoryzacji, jest na tyle istotny, że poświęcimy mu sporo uwagi w dalszej
części tej książki (patrz temat 40., „Refaktoryzacja”).
Testowanie
Systemy projektowane i implementowane zgodnie z zasadą ortogonalności są
dużo łatwiejsze do testowania. Ponieważ interakcje pomiędzy komponentami
systemu są sformalizowane i ograniczone, znaczną część testów można wykonać na poziomie pojedynczych modułów. Takie rozwiązanie jest o tyle korzystne,
że testy na poziomie modułu (jednostki) są nieporównanie prostsze do definiowania i wykonywania niż testy integracyjne. Sugerujemy nawet opracowanie
osobnego testu jednostkowego dla każdego modułu i umieszczenie go w kodzie
4337ebf6db5c7cc89e4173803ef3875a
4
72 
Rozdział 2.
Postawa pragmatyczna
tego modułu. Tak zdefiniowane testy powinny być wykonywane automatycznie
w ramach procesu regularnego kompilowania kodu (patrz temat 41., „Kod łatwy
do testowania”).
Budowa testów jednostkowych sama w sobie jest dość ciekawym testem ortogonalności systemu. Co należy zrobić, aby przygotować test jednostkowy i połączyć go z systemem? Czy kompilacja lub łączenie tego testu wymaga dostępu do
znacznej części pozostałych składników systemu? Jeśli tak, właśnie odkryliśmy
moduł, który nie jest dobrze odizolowany od reszty systemu.
Dobrym momentem oceny ortogonalności systemu jest także usuwanie znalezionych błędów. Po napotkaniu problemu warto przeanalizować zasięg oddziaływania usterki. Czy wystarczy zmienić tylko jeden moduł, czy zmiany dotyczą
całego systemu? Czy jedna zmiana wystarczy do usunięcia problemu, czy raczej
powoduje tajemnicze pojawienie się innych problemów? To wprost doskonała
okazja do zastosowania automatycznych rozwiązań. Jeśli posługujemy się systemem kontroli wersji (po lekturze tematu 19., „Kontrola kodu źródłowego” to
raczej pewne), możemy oznaczać usunięte błędy przy okazji zwracania kodu
do repozytorium po zakończeniu testów. W takim przypadku warto skorzystać
z możliwości generowania comiesięcznych raportów ilustrujących trendy w zakresie liczby plików źródłowych objętych procedurami usuwania usterek.
Dokumentacja
Co ciekawe, zasada ortogonalności znajduje zastosowanie także w dokumentacji. W tym przypadku funkcje osi pełni treść i prezentacja. Naprawdę ortogonalna dokumentacja powinna umożliwiać zasadniczą zmianę wyglądu bez
konieczności modyfikowania treści. Współczesne edytory tekstu oferują obsługę
arkuszy stylów i makr, które znacznie ułatwiają budowę ortogonalnych dokumentów. Osobiście preferujemy stosowanie systemu znaczników, takich jak
Markdown: podczas pisania skupiamy się tylko na treści, a prezentację pozostawiamy narzędziu wykorzystywanemu do renderowania5.
Życie z ortogonalnością
Ortogonalność jest ściśle związana z zasadą DRY, wprowadzoną we wcześniejszej części tego rozdziału. Zasada DRY ma na celu zminimalizowanie zjawiska
powielania w systemie, natomiast ortogonalność pozwala ograniczyć wzajemne
zależności łączące komponenty tego systemu. Być może trudno w to uwierzyć,
ale konsekwentne łączenie zasady ortogonalności z zasadą DRY pozwala tworzyć systemy nieporównanie bardziej elastyczne, zrozumiałe oraz łatwiejsze do
diagnozowania, testowania i konserwowania.
5
Ta książka została napisana z wykorzystaniem systemu Markdown i złożona bezpośrednio ze źródła Markdown.
4337ebf6db5c7cc89e4173803ef3875a
4
Ortogonalność
 73
Jeśli przystępujemy do prac nad projektem, w którym wszelkie zmiany budzą
niechęć i opór pozostałych członków zespołu, gdzie każda modyfikacja może
uniemożliwić funkcjonowanie pięciu innych elementów, warto przypomnieć sobie
koszmar sterowania helikopterem. Projekt prawdopodobnie nie został zaprojektowany i nie jest kodowany zgodnie z zasadą ortogonalności. Najwyższy czas
przystąpić do refaktoryzacji.
A jeśli pilotujemy śmigłowiec, powinniśmy unikać jedzenia ryb…
Pokrewne podrozdziały

Temat 3., „Entropia oprogramowania”.

Temat 8., „Istota dobrego projektu”.

Temat 11., „Odwracalność”.

Temat 28., „Eliminowanie sprzężeń”.

Temat 31., „Podatek od dziedziczenia”.

Temat 33., „Eliminowanie związków czasowych”.

Temat 34., „Współdzielony stan jest zły”.

Temat 36., „Czarne tablice”.
Wyzwania

Warto przestudiować różnice dzielące wielkie narzędzia z graficznym interfejsem użytkownika (tworzone zwykle dla systemu Windows) i niewielkie,
ale złożone narzędzia uruchamiane z poziomu wiersza poleceń. Które programy są bardziej ortogonalne? Dlaczego? Który rodzaj programów jest
łatwiejszy w użyciu z perspektywy osoby zainteresowanej oryginalnym
przeznaczeniem tych produktów? Który zbiór programów można łatwiej
łączyć z innymi narzędziami z myślą o nowych wyzwaniach?

Język C++ obsługuje wielokrotne dziedziczenie, natomiast Java oferuje
możliwość implementowania wielu interfejsów przez jedną klasę. Jaki jest
wpływ tych rozwiązań na ortogonalność kodu, w którym są stosowane?
Czy wpływ stosowania wielokrotnego dziedziczenia jest inny niż wpływ
implementowania wielu interfejsów? Czy w tym kontekście stosowanie
delegacji różni się od stosowania dziedziczenia?
Ćwiczenia
1. Napisz program, który czyta informacje z pliku wiersz po wierszu. Każdy wiersz należy podzielić na pola. Która z poniższych klas napisanych
w pseudokodzie w większym stopniu spełnia warunek ortogonalności?
Ta:
class Split1 {
constructor(fileName) # otwiera plik do odczytu
def readNextLine()
# przejście do następnego wiersza
4337ebf6db5c7cc89e4173803ef3875a
4
74 
Rozdział 2.
Postawa pragmatyczna
def getField(n)
# zwraca n-te pole w bieżącym wierszu
}
czy ta:
class Split2 {
constructor(line)
def getField(n)
}
# dzieli wiersze
# zwraca n-te pole w bieżącym wierszu
2. Jakie są różnice w ortogonalności pomiędzy kodem napisanym w języku
obiektowym a analogicznym kodem napisanym w języku funkcyjnym?
Czy te różnice wynikają z charakterystyki samych języków, czy też ze sposobu, w jaki są wykorzystywane?
11
39
Odwracalność
Nie ma niczego bardziej niebezpiecznego niż idea, jeśli to wszystko,
co mamy.
Emil-Auguste Chartier, Propos sur la religion, 1938
Inżynierowie lubują się w prostych, pojedynczych rozwiązaniach problemów.
Testy matematyczne, które umożliwiają stwierdzenie z całą pewnością, że x = 2,
są dużo wygodniejsze i bardziej atrakcyjne od nieścisłych, mętnych rozpraw na
temat niezliczonych przyczyn rewolucji francuskiej. Menedżerowie w tej kwestii
wykazują zadziwiającą zgodność z inżynierami — pojedyncze, proste odpowiedzi
dużo lepiej prezentują się w arkuszach kalkulacyjnych i planach projektów.
Gdyby tylko rzeczywistość zechciała z nami współpracować! W praktyce nierzadko okazuje się, że x, które dzisiaj ma wartość 2, jutro może mieć wartość 5,
by za tydzień mieć na przykład wartość 3. Nic nie jest wieczne — jeśli zbyt mocno
uzależniamy działanie naszego systemu od jakiegoś warunku, możemy być niemal pewni, że warunek ten ulegnie zmianie.
Zawsze istnieje więcej niż jeden sposób implementacji zaplanowanych mechanizmów. Co więcej, przeważnie istnieje wielu producentów oferujących gotowe rozwiązania. Jeśli przystępujemy do prac nad projektem, który przez krótkowzroczność lidera może być zrealizowany tylko w jeden sposób, może nas spotkać
niemiła niespodzianka. Członkom wielu zespołów projektowych oczy otwiera
dopiero niekorzystny rozwój wypadków:
„Mówiłeś przecież, że użyjemy bazy danych XYZ! Mamy już gotowe 85% kodu
projektu, więc nie możemy teraz zmienić bazy danych!” — zaprotestował
programista. „Przykro mi, ale nasza firma zdecydowała, że we wszystkich
projektach ma być stosowana standardowa baza danych PDQ. Decyzja nie
należy do mnie. Musimy po prostu dostosować się do tej decyzji. Wszyscy
będziecie pracowali w weekendy do odwołania”.
4337ebf6db5c7cc89e4173803ef3875a
4
Odwracalność
 75
Zmiany, oczywiście, nie muszą być tak dramatyczne ani nie muszą wymagać
natychmiastowej interwencji. Z czasem, wraz z postępem projektu, możemy
znaleźć się w sytuacji, w której dalsze podążanie w dotychczasowym kierunku
będzie niemożliwe. Każda krytyczna decyzja oznacza, że zespół projektowy wyznacza mniejszy cel — węższą wersję rzeczywistości, która z natury rzeczy oferuje
mniej opcji.
Z czasem liczba podjętych krytycznych decyzji jest na tyle duża, a cel na tyle
mały, że każdy jego ruch (zmiana kierunku wiatru, trzepotanie skrzydłami przez
motyla w Tokio lub cokolwiek innego) uniemożliwia nam trafienie tego celu6. Być
może nawet popełniliśmy jakieś zasadnicze błędy.
Problem w tym, że krytyczne decyzje nie mogą być łatwo odwracane.
W momencie, w którym decydujemy się użyć bazy danych określonego producenta, pewnego wzorca architekturalnego lub konkretnego modelu wdrażania,
w praktyce obieramy kierunek działania, którego nie można następnie zmienić, chyba że bardzo dużym kosztem.
Odwracalność
Wiele zagadnień omówionych w tej książce ma na celu ułatwienie tworzenia
elastycznego oprogramowania. Postępowanie zgodnie z proponowanymi tutaj
zaleceniami — w szczególności z zasadą DRY (patrz wcześniejsza część tego rozdziału), z zapewnieniem właściwej izolacji (więcej w rozdziale 5.) oraz przy użyciu
zewnętrznej konfiguracji (tamże) — pozwala ograniczyć liczbę niezbędnych krytycznych, nieodwracalnych decyzji. Mniejsza liczba takich decyzji jest o tyle
pożądana, że nie zawsze podejmujemy najlepsze decyzje za pierwszym razem.
Nierzadko decydujemy się na określoną technologię tylko dlatego, że nie możemy
sobie pozwolić na zatrudnienie odpowiednio wielu pracowników dysponujących
niezbędnymi umiejętnościami. W ten sposób ryzykujemy uzależnienie od konkretnego producenta na przykład bezpośrednio przed jego przejęciem przez konkurenta. Wymagania, użytkownicy i sprzęt zmieniają się szybciej, niż jesteśmy
w stanie tworzyć nasze oprogramowanie.
Przypuśćmy, że na wczesnym etapie prac nad projektem decydujemy się użyć
relacyjnej bazy danych producenta A. Po dłuższym czasie, już w trakcie testów
wydajności, odkrywamy, że wybrana wcześniej baza danych jest po prostu zbyt
wolna. Istnieje za to obiektowa baza danych producenta B, która jest nieporównanie szybsza. W przypadku większości typowych projektów byłaby to niemal katastrofa. W większości przypadków odwołania do oprogramowania
zewnętrznych producentów wymagają zapisania w wielu miejscach kodu. Gdyby
6
Warto spróbować wprowadzić drobną zmianę w jednym z parametrów wejściowych nieliniowego, chaotycznego systemu. Wyniki uzyskiwane wskutek tej zmiany często są nieprzewidywalne. Przysłowiowy motyl trzepoczący skrzydłami w Tokio może zapoczątkować
łańcuch zdarzeń, który skończy się powstaniem tornada w Teksasie. Czy właśnie tak
wyglądały wszystkie projekty, w których braliśmy udział?
4337ebf6db5c7cc89e4173803ef3875a
4
76 
Rozdział 2.
Postawa pragmatyczna
jednak udało nam się wyabstrahować bazę danych (przynajmniej do poziomu,
w którym ta baza miałaby status usługi odpowiedzialnej za utrwalanie danych),
moglibyśmy dużo swobodniej zmieniać ten produkt na dowolnym etapie prac.
Podobnie, załóżmy, że projekt początkowo jest realizowany jako aplikacja webowa
oparta na przeglądarce, ale później, na zaawansowanym etapie projektu, pracownicy działu marketingu doszli do wniosku, że lepsza byłaby aplikacja mobilna. Na ile trudna będzie realizacja tego wymagania? W idealnej sytuacji ta
zmiana nie powinna wywierać na nas zbyt dużego wpływu, przynajmniej po
stronie serwerowej. Powinno wystarczyć usunięcie mechanizmów renderowania
HTML i zastąpienie ich przez API.
Błąd popełniamy już na początku, kiedy przyjmujemy, że każda decyzja powinna
być trwale wyryta w kamieniu — początkowo nie jesteśmy gotowi na wszystkie
ewentualności, które mogą wymagać korekty pierwotnych założeń. Zamiast
utrwalać decyzje w kamieniu, powinniśmy raczej zapisywać je patykiem na piasku na najbliższej plaży. Wystarczy większa fala, aby całkowicie zmyć nasze
wcześniejsze założenia.
WSKAZÓWKA NR 18
Nie istnieją ostateczne decyzje.
Elastyczna architektura
Wielu programistów podejmuje co prawda próby pisania elastycznego kodu, jednak dbałość o elastyczność nie może ograniczać się tylko do tego obszaru —
musi dotyczyć także architektury, wdrażania i integracji z innymi produktami.
Piszemy tę książkę w roku 2019. Od przełomu XX i XXI wieku obserwowaliśmy
następujące „najlepsze praktyki” architektur serwerowych:

Serwery fizyczne o dużej mocy obliczeniowej.

Federacje mocnych serwerów fizycznych.

Klastry serwerów z mechanizmami równoważenia obciążenia.

Maszyny wirtualne w chmurze obliczeniowej serwujące aplikacje.

Maszyny wirtualne w chmurze obliczeniowej serwujące usługi.

Kontenerowe wersje usług i aplikacji.

Bazujące na chmurze obliczeniowej aplikacje działające w trybie bezserwerowym.

Oraz, co nieuniknione, powrót do serwerów fizycznych w przypadku niektórych zadań.
Śmiało dodaj do tej listy najnowsze i najlepsze trendy mody, a następnie traktuj
je z podziwem: to cud, że cokolwiek kiedykolwiek działało.
4337ebf6db5c7cc89e4173803ef3875a
4
Odwracalność
 77
W jaki sposób możesz zaplanować tego rodzaju zmienność architektoniczną?
Otóż nie możesz.
Możesz jedynie zadbać o to, aby projekt można było łatwo zmienić. Ukryj
zewnętrzne interfejsy API za własnymi warstwami abstrakcji. Podziel swój kod
na komponenty: nawet jeśli ostatecznie wdrożysz kod na pojedynczym, monolitycznym serwerze, takie podejście jest dużo łatwiejsze od stworzenia monolitycznej aplikacji i prób jej późniejszego dzielenia (wiemy, o czym mówimy, znamy
to z doświadczenia).
I choć nie jest to problem specyficznie dotyczący odwracalności, zapamiętaj
jedną, końcową radę.
WSKAZÓWKA NR 19
Przewiduj przyszłe trendy.
Nikt nie wie, co przyniesie nam przyszłość, a już na pewno nie wiedzą tego programiści! Warto więc przygotowywać kod na wszystkie ewentualności — na
automatyczne uzupełnianie o nowe wymagania oraz na równie sprawne eliminowanie wymagań, które okazują się nietrafione.
Pokrewne podrozdziały

Temat 8., „Istota dobrego projektu”.

Temat 10., „Ortogonalność”.

Temat 19., „Kontrola kodu źródłowego”.

Temat 28., „Eliminowanie sprzężeń”.

Temat 45., „Kopalnia wymagań”.

Temat 51., „Zestaw startowy pragmatyka”.
Wyzwania

Warto poświęcić chwilę mechanice kwantowej i eksperymentowi myślowemu z kotem Schrödingera. Przypuśćmy, że zamykamy kota w pojemniku
z cząstką radioaktywną. Prawdopodobieństwo rozszczepienia tej cząstki
wynosi dokładnie 50%. Jeśli to nastąpi, kot zginie. W przeciwnym razie kot
zachowa życie. Czy ten hipotetyczny kot jest żywy, czy martwy? Według
Schrödingera, do momentu otwarcia pojemnika obie odpowiedzi są prawidłowe. Za każdym razem, gdy ma miejsce wspomniana reakcja subjądrowa, która może prowadzić do jednego z dwóch wyników, wszechświat
jest klonowany. W jednej kopii interesujące nas zdarzenie miało miejsce,
w drugiej kopii to zdarzenie nie wystąpiło. Kot jest więc żywy w jednym
wszechświecie i martwy w drugim. Dopiero otwarcie pojemnika pozwala
stwierdzić, w którym wszechświecie się znajdujemy.
4337ebf6db5c7cc89e4173803ef3875a
4
78 
Rozdział 2.
Postawa pragmatyczna
Trudno się dziwić, że kodowanie z myślą o przyszłych wymaganiach jest
takie trudne.
Gdybyśmy jednak postrzegali ewolucję kodu jako pudło pełne kotów Schrödingera, każda nasza decyzja musiałaby skutkować inną wersją przyszłości. Ile możliwych scenariuszy może obsługiwać nasz kod? Który z tych
scenariuszy jest bardziej prawdopodobny? Na ile trudna będzie obsługa tych
scenariuszy w przyszłości, kiedy wreszcie nastąpią?
Czy odważymy się otworzyć to pudełko?
12
40
Pociski smugowe
Przygotuj broń, cel, pal…
— Anon
Kiedy tworzymy oprogramowanie, często mówimy o trafianiu w cele. W istocie
nie celujemy do niczego na strzelnicy, ale nadal jest to bardzo użyteczna i obrazowa metafora. W szczególności warto zastanowić się, jak trafić w cel w złożonym i zmieniającym się świecie.
Odpowiedź oczywiście zależy od rodzaju urządzenia, do którego celujesz. W przypadku wielu masz tylko jedną szansę, aby wycelować, a następnie możesz się
przekonać, czy trafiłeś w dziesiątkę, czy nie. Istnieje jednak lepszy sposób.
Znacie te wszystkie filmy, programy telewizyjne i gry wideo, w których występujące postacie strzelają z karabinów maszynowych? W tych scenach często można
zobaczyć drogę, jaką pokonują kule w powietrzu — co jakiś czas widać jasne
smugi. Te ślady pochodzą od pocisków smugowych.
Pociski smugowe ładuje się wśród standardowej amunicji na taśmie nabojowej
w stałych odstępach. Fosfor spalany w wystrzelonym pocisku pozostawia smugę
widoczną od karabinu strzelca do punktu, w który ten pocisk trafił. Jeśli pociski
smugowe trafiają w cel, można przyjąć, że robią to także zwykłe kule.
Ta sama zasada dotyczy projektów, szczególnie jeśli tworzymy rozwiązania, których nikt wcześniej nie stworzył. Używamy terminu wytwarzanie oprogramowania w stylu pocisków smugowych, aby wizualnie zilustrować potrzebę
uzyskania natychmiastowej informacji zwrotnej w rzeczywistych warunkach
z ruchomym celem.
Podobnie jak strzelcy, próbujemy trafić cel w ciemnościach. Ponieważ nasi użytkownicy nigdy wcześniej nie widzieli podobnego systemu, ich oczekiwania
i wymagania mogą być niejasne. Ponieważ możemy używać algorytmów, technik, języków i bibliotek, których nie opanowaliśmy w dostatecznym stopniu,
musimy się liczyć z dużą liczbą niewiadomych. Co więcej, ponieważ realizacja
4337ebf6db5c7cc89e4173803ef3875a
4
Pociski smugowe
 79
projektu wymaga czasu, możemy być niemal pewni, że środowisko, w którym
pracujemy, zmieni się przed zakończeniem prac.
Klasyczną reakcją na taką sytuację jest próba opracowania wyczerpującej specyfikacji. Próbujemy tworzyć sterty papierów rozkładających na części pierwsze
każde wymaganie, rozprawiających o każdej niewiadomej i szczegółowo opisujących docelowe środowisko. Jesteśmy jak strzelcy, którzy przed oddaniem pierwszego strzału prowadzą żmudne obliczenia. Najpierw długie obliczenia — potem
strzał w nadziei na trafność kalkulacji.
Pragmatyczni programiści wolą jednak używać pocisków smugowych.
Kod rozświetlający mrok
Pociski smugowe sprawdzają się w praktyce, ponieważ są używane w tych
samych warunkach i z uwzględnieniem tych samych ograniczeń co normalne
pociski. Ponieważ docierają do celu bardzo szybko, strzelec natychmiast uzyskuje
wskazówki dotyczące skuteczności ostrzału. Co więcej, z praktycznego punktu
widzenia pociski smugowe są stosunkowo tanim rozwiązaniem.
Aby uzyskać ten sam efekt w kodzie implementowanego systemu, potrzebujemy
czegoś, co możliwie szybko, jednoznacznie i powtarzalnie będzie prowadziło od
wymagania do odpowiedniego aspektu ostatecznego systemu.
Szukaj ważnych wymagań — takich, które definiują system. Szukaj obszarów,
co do których masz wątpliwości oraz takich, dla których dostrzegasz największe
zagrożenia. Następnie ustal priorytety w programowaniu tak, aby tymi obszarami zająć się najpierw.
WSKAZÓWKA NR 20
Należy znajdować cel za pomocą pocisków smugowych.
Biorąc pod uwagę złożoność konfiguracji dzisiejszych projektów, ze względu na
mnóstwo zależności i zewnętrznych narzędzi, pociski smugowe stają się jeszcze
ważniejsze. Dla nas, pierwszy pocisk smugowy to utworzenie projektu „Witaj,
świecie!” i zadbanie o to, aby się skompilował i uruchomił. Następnie powinniśmy
poszukać obszarów niepewności w całej aplikacji i dodać szkielet niezbędny do
tego, by wszystko działało.
Przyjrzyjmy się poniższemu rysunkowi. Ten system składa się z pięciu warstw
architektonicznych. Mamy pewne obawy co do sposobu ich integracji, więc szukamy prostych funkcji, które pozwolą nam je wspólnie testować. Linia przecinająca wykres pokazuje drogę, jaką obejmuje funkcjonalność w kodzie. Aby
wszystko działało, musimy zaimplementować tylko zacienione obszary w każdej
warstwie: elementy oznaczone falowanymi liniami będą wykonane później.
4337ebf6db5c7cc89e4173803ef3875a
4
80 
Rozdział 2.
Postawa pragmatyczna
Przejęliśmy kiedyś projekt polegający na opracowaniu złożonego systemu marketingowego z bazą danych w architekturze klient-serwer. Jedno z wymagań
mówiło o możliwości wpisywania i wykonywania tymczasowych zapytań. Serwery
wchodzące w skład systemu miały obejmować zarówno standardowe, relacyjne
bazy danych, jak i specjalistyczne bazy danych. Graficzny interfejs użytkownika
warstwy klienta, który napisano w losowym języku A, korzystał z bibliotek napisanych w innym języku, implementujących interfejs dostępu do tych serwerów.
Zapytania użytkowników były przechowywane po stronie serwera w notacji przypominającej kod Lispa. Dopiero z tego formatu były konwertowane na zoptymalizowany kod języka SQL i ostatecznie wykonywane. Istniało wiele niewiadomych
i wiele różnych środowisk. Nikt nie mógł z całą pewnością stwierdzić, jak ten
graficzny interfejs użytkownika powinien działać.
Była to wprost doskonała okazja do użycia kodu smugowego. Opracowaliśmy
framework dla interfejsu użytkownika, biblioteki na potrzeby reprezentowania
zapytań oraz strukturę niezbędną do konwersji przechowywanych zapytań na
zapytania właściwe określonej bazie danych. Połączyliśmy następnie wszystkie
te składniki i sprawdziliśmy, czy działają. W tej wstępnej wersji funkcje systemu
ograniczały się do możliwości wysłania zapytania zwracającego wszystkie wiersze
pewnej tabeli. To wystarczyło jednak do wykazania, że nasz interfejs potrafi
komunikować się z bibliotekami, że same biblioteki prawidłowo zapisują i odczytują zapytania oraz że serwer potrafi wygenerować prawidłowy kod języka SQL
na podstawie tych zapytań. W kolejnych miesiącach stopniowo rozbudowywaliśmy tę podstawową strukturę, dodając nowe funkcje i równolegle rozszerzając
poszczególne komponenty kodu smugowego. Wskutek dodawania nowych typów
zapytań przez interfejs użytkownika biblioteka zaczęła się rozrastać, a mechanizm generowania kodu języka SQL stawał się coraz bardziej skomplikowany.
4337ebf6db5c7cc89e4173803ef3875a
4
Pociski smugowe
 81
Kod smugowy nie ma jednorazowego charakteru — raz napisany powinien być
zachowany na przyszłość. Kod smugowy zawiera wszystkie niezbędne mechanizmy sprawdzania błędów, odpowiednią strukturę, dokumentację i rozwiązania
samosprawdzające charakterystyczne dla kodu produkcyjnego. Jedyną wadą
tego kodu jest niepełna funkcjonalność. Okazuje się jednak, że po osiągnięciu
docelowych związków łączących komponenty systemu możemy sprawdzić, na
ile nasze dzieło jest zgodne z założeniami, i — w razie konieczności — wprowadzić niezbędne poprawki. Kiedy już mamy cel na muszce, dodawanie nowych
funkcji jest bardzo proste.
Koncepcja tworzenia kodu smugowego jest w pełni zgodna z przekonaniem o tym,
że projekt nigdy się nie kończy — zawsze będą istniały jakieś wymagane zmiany
i funkcje do dodania. Tworzenie oprogramowania jest procesem przyrostowym.
Konwencjonalną alternatywą jest model znany z wielkich przedsięwzięć inżynieryjnych, gdzie kod jest dzielony na moduły implementowane niezależnie od pozostałych składników projektu. Moduły łączy się w większe podzespoły, które z kolei
są dalej łączone, aż pewnego dnia dysponujemy kompletną, bardzo złożoną aplikacją. Dopiero wówczas aplikację można w całości zaprezentować użytkownikowi
i poddać testom.
Kod smugowy ma wiele ważnych zalet:

Użytkownicy mogą sprawdzić działający system już na wczesnych etapach projektu. Jeśli potrafimy skutecznie prezentować nasze poczynania
(patrz temat 52., „Wpraw w zachwyt użytkowników”), nasi użytkownicy będą
mieli świadomość, że mają do czynienia z niedojrzałym produktem, którego
ostateczny kształt będzie nieco inny. W takim przypadku użytkownicy nie
będą zawiedzeni brakiem pewnych funkcji — będą raczej zadowoleni z możliwości obserwowania postępu prac nad systemem. Udział w pracach nad
projektem będzie stopniowo zwiększał zaangażowanie użytkowników.
Ci sami użytkownicy najprawdopodobniej będą potrafili stwierdzić, na ile
blisko celu znajdują się poszczególne iteracje.

Sami programiści przygotowują strukturę, w której będą pracowali.
Najbardziej zniechęcającym dokumentem jest pusta kartka papieru. Jeśli
sami opracowywaliśmy i rozwijaliśmy wszystkie interakcje naszej aplikacji
oraz jeśli zaimplementowaliśmy te interakcje w kodzie, członkowie naszego
zespołu nie będą musieli tracić czasu na zgłębianie cudzych rozwiązań.
Oznacza to, że wszyscy będą bardziej produktywni, a cały system będzie
cechował się większą spójnością.

Dysponujemy platformą integracji. Skoro struktura systemu jest kompletna, dysponujemy środowiskiem, które możemy uzupełniać o nowe fragmenty kodu bezpośrednio po poddaniu ich testom jednostkowym. Zamiast
próbować integrować wielki system po opracowaniu wszystkich komponentów, integrujemy nasz produkt codziennie (a często wiele razy w ciągu
dnia). Skutki wprowadzenia każdej zmiany są łatwiejsze do przewidzenia,
a zakres interakcji jest ograniczony, zatem diagnozowanie i testowanie całego
systemu przebiega szybciej i prowadzi do bardziej precyzyjnych wyników.
4337ebf6db5c7cc89e4173803ef3875a
4
82 
Rozdział 2.
Postawa pragmatyczna

Dysponujemy czymś, co możemy zademonstrować. Podmioty finansujące projekt i członkowie kierownictwa organizacji zwykle oczekują demonstracji tworzonych systemów w najmniej odpowiednich momentach. Kod
smugowy powoduje, że zawsze dysponujemy czymś gotowym do pokazania.

Sami dysponujemy pełniejszą wiedzą o postępach prac. W modelu
wytwarzania z uwzględnieniem kodu smugowego programiści analizują
i rozstrzygają kolejne przypadki użycia. Po gruntownym przestudiowaniu
jednego przypadku użycia programiści przystępują do analizy kolejnego.
W takim przypadku mierzenie wydajności i demonstrowanie postępu użytkownikowi jest nieporównanie prostsze. Ponieważ każde zadanie programistyczne jest mniejsze, możemy uniknąć tworzenia monolitycznych
bloków kodu, które całymi tygodniami są opisywane jako gotowe w 95
procentach.
Pociski smugowe nie zawsze trafiają w cel
Pociski smugowe pokazują, gdzie trafiamy. Nie we wszystkich przypadkach trafiany punkt jest naszym celem. Jeśli nie, musimy tak długo korygować sposób celowania, aż osiągniemy zamierzony efekt. Właśnie po to używa się tych
pocisków.
Dokładnie to samo dotyczy kodu smugowego. Opisywana technika jest przydatna w sytuacjach, w których nie możemy być pewni na 100 procent, że
obrana droga jest słuszna. Nie powinniśmy być zaskoczeni, jeśli w pierwszych
kilku próbach chybimy celu — jeśli użytkownik stwierdzi „nie do końca o to mi
chodziło”, jeśli niezbędne dane nie będą dostępne od razu lub jeśli wystąpią
jakieś problemy z wydajnością. Powinniśmy raczej wskazać sposób zmiany
dotychczasowych rozwiązań, tak aby zbliżyć się do celu, i jednocześnie docenić
możliwość stosowania tak wygodnej metodyki. Małe fragmenty kodu cechują
się niewielką bezwładnością i jako takie mogą być łatwo i szybko zmieniane.
Mamy możliwość uzyskiwania cennej wiedzy o naszej aplikacji i jednocześnie
tworzenia nowej, lepszej (celniejszej) wersji szybciej i mniejszym kosztem niż
w przypadku jakiejkolwiek innej metody. Co więcej, ponieważ każdy ważny
komponent aplikacji jest reprezentowany w naszym kodzie smugowym, użytkownicy mogą być pewni, że to, co widzą, zaimplementowano na bazie rzeczywistych rozwiązań, nie papierowej specyfikacji.
Kod smugowy kontra prototypy
Na pierwszy rzut oka wydaje się, że idea kodu smugowego nie jest niczym więcej niż formą tworzenia prototypów, tyle że pod bardziej atrakcyjną nazwą.
Istnieje pewna różnica. Prototyp służy do zbadania tylko wybranych, konkretnych aspektów docelowego systemu. Prawdziwy prototyp rozwiewa wszelkie
wątpliwości dotyczące analizowanej koncepcji czy założeń — po jego analizie możemy przystąpić do ponownego kodowania odpowiedniego składnika
z uwzględnieniem ewentualnych poprawek.
4337ebf6db5c7cc89e4173803ef3875a
4
Pociski smugowe
 83
Przypuśćmy na przykład, że pracujemy nad aplikacją, która ma ułatwić firmie
spedycyjnej określanie sposobu rozmieszczania przesyłek o nietypowych rozmiarach w kontenerach. Do najważniejszych problemów należy zaprojektowanie odpowiednio intuicyjnego interfejsu użytkownika oraz zaimplementowanie
bardzo skomplikowanych algorytmów określania optymalnego rozmieszczenia
przesyłek.
Prototyp interfejsu użytkownika można opracować (z myślą o użytkownikach
końcowych) za pomocą narzędzi do projektowania graficznych interfejsów
użytkownika. Kod takiego prototypu ogranicza się do rozwiązań zapewniających właściwe reagowanie interfejsu na czynności użytkownika. Po uzyskaniu
pozytywnej oceny układu elementów należy ten prototyp wyrzucić i od nowa
zaimplementować odpowiednie rozwiązanie, tym razem z uwzględnieniem odpowiedniej logiki biznesowej i w docelowym języku programowania. Podobnie,
możemy zdecydować się na opracowanie prototypów wielu algorytmów odpowiedzialnych za właściwe rozmieszczanie paczek w kontenerze. Testy funkcjonalne można zakodować na wysokim poziomie, korzystając z takich języków
jak Perl, natomiast niskopoziomowe testy wydajnościowe warto zaimplementować w języku bliższym instrukcji maszynowych. W każdym przypadku po podjęciu dotyczącej prototypu decyzji należy przystąpić do ponownego kodowania
odpowiednich atrybutów w ich środowisku docelowym, gdzie będą narażone
na oddziaływanie nieco innych czynników. Właśnie na tym polega tworzenie
prototypów.
Model z kodem smugowym ma na celu rozwiązywanie nieco innego problemu.
Tworząc kod smugowy, chcemy dowiedzieć się, jak nasza aplikacja będzie funkcjonowała jako całość. Chcemy pokazać naszym użytkownikom, jak opisywane
interakcje będą wyglądały w praktyce. Chcemy też udostępnić programistom
szkielet architektury, w ramach którego będą implementowali swój kod. W tym
przypadku moglibyśmy skonstruować kod smugowy składający się na przykład z uproszczonego algorytmu rozmieszczenia przesyłek (być może w kolejności dodawania do kontenera) oraz prostym, ale działającym interfejsem użytkoonwnika. Po połączeniu wszystkich komponentów aplikacji w jedną całość
dyspujemy frameworkiem, który możemy prezentować zarówno użytkownikom,
jak i programistom zaangażowanym w projekt. Z czasem uzupełniamy ten framework o nowe funkcje oraz wypełniamy stosowane wcześniej namiastki procedur. Warto jednak pamiętać, że sam framework pozostaje niezmieniony, zatem
możemy być pewni, że system będzie zachowywał się tak jak w momencie ukończenia prac nad pierwszym kodem smugowym.
Wspomniane rozróżnienie jest na tyle ważne, że warto je powtórzyć. W wyniku
tworzenia prototypu powstaje tymczasowy, jednorazowy kod. Kod smugowy jest
uproszczony, ale kompletny, zatem stanowi fragment szkieletu docelowego systemu. Tworzenie prototypów jest jak rozpoznanie, próba zebrania cennych
danych wywiadowczych przed wystrzeleniem pierwszego pocisku smugowego.
4337ebf6db5c7cc89e4173803ef3875a
4
84 
Rozdział 2.
Postawa pragmatyczna
Pokrewne podrozdziały
13
41

Temat 13., „Prototypy i karteczki samoprzylepne”.

Temat 27., „Nie prześcigaj swoich świateł”.

Temat 40., „Refaktoryzacja”.

Temat 49., „Pragmatyczne zespoły”.

Temat 50., „Nie próbuj przecinać kokosów”.

Temat 51., „Zestaw startowy pragmatyka”.

Temat 52., „Wpraw w zachwyt użytkowników”.
Prototypy i karteczki samoprzylepne
Wiele różnych branż używa prototypów do sprawdzania przyszłych rozwiązań
w najróżniejszych obszarach; tworzenie prototypów jest nieporównanie tańsze
od produkcji w pełnej skali. Na przykład producenci samochodów nierzadko
budują wiele odmiennych prototypów przed wyborem ostatecznego projektu
nowego modelu. Każdy prototyp jest projektowany z myślą o przetestowaniu
konkretnego aspektu samochodu — parametrów aerodynamicznych, stylistyki,
właściwości konstrukcyjnych itd. Model z gliny być może jest budowany z myślą
o testach w tunelu aerodynamicznym, natomiast model z drewna balsy i mocnej taśmy samoprzylepnej ma trafić do działu stylistyki itd. Niektórzy producenci
samochodów idą jeszcze krok dalej i ograniczają się do modelowania swoich
aut w komputerze, dodatkowo ograniczając koszty. W ten sposób ryzykowne lub
niesprawdzone elementy można łatwo wypróbować bez konieczności konstruowania właściwego produktu.
Prototypy oprogramowania buduje się w ten sam sposób i z tych samych powodów — z myślą o analizie i ujawnieniu czynników ryzyka oraz o stworzeniu
dodatkowej szansy wprowadzenia korekt znacznie mniejszym kosztem. Tak jak
producenci samochodów możemy stworzyć prototyp z myślą o przetestowaniu
jednego lub wielu konkretnych aspektów realizowanego projektu.
Technikę tworzenia prototypów zwykle utożsamiamy z pisaniem jakiegoś kodu,
jednak prototypy nie zawsze muszą mieć postać programów. Tak jak producenci samochodów możemy konstruować swoje prototypy z różnych materiałów.
Na przykład karteczki samoprzylepne doskonale sprawdzają się w roli budulca
prototypów tak dynamicznych aspektów jak przepływ pracy czy logika aplikacji.
Prototyp interfejsu użytkownika można narysować na tablicy, opracować
w programie graficznym jako pozbawiony funkcji szkic lub zbudować przy użyciu programu do konstruowania interfejsów.
Prototypy projektuje się z myślą o uzyskiwaniu odpowiedzi na zaledwie kilka
pytań, zatem z natury rzeczy są tańsze i szybsze w tworzeniu niż aplikacje trafiające do środowiska produkcyjnego. Kod może ignorować nieistotne szczegóły
4337ebf6db5c7cc89e4173803ef3875a
4
Prototypy i karteczki samoprzylepne

85
(nieważne w danej chwili, ale być może bardzo ważne dla użytkownika właściwego produktu w przyszłości). Jeśli na przykład tworzymy prototyp graficznego interfejsu użytkownika, możemy całkowicie pominąć problem nieprawidłowych wyników czy danych. Jeśli jednak przedmiotem analizy są aspekty
obliczeniowe i wydajnościowe, możemy zrezygnować z atrakcyjnego graficznego
interfejsu użytkownika (a być może nawet z jakiegokolwiek interfejsu tego typu).
Jeśli znajdujemy się w środowisku, w którym nie możemy ani na moment pominąć żadnego szczegółu, powinniśmy odpowiedzieć sobie na pytanie, czy konstruowanie prototypu w ogóle ma sens. W takim przypadku być może lepszym
rozwiązaniem będzie przyjęcie modelu programowania analogicznego do pocisków smugowych (patrz temat 12., „Pociski smugowe”).
Co może być przedmiotem prototypu
Jakiego rodzaju aspekty warto wybierać do zbadania przy użyciu prototypu?
Wszystko, z czym wiąże się jakieś ryzyko. Wszystko, czego wcześniej nie próbowano lub co ma krytyczne znaczenie dla ostatecznego systemu. Wszystko,
co jest niedowiedzione, eksperymentalne lub wątpliwe. Wszystko, co budzi
nasz dyskomfort. Przedmiotem prototypu mogą być:

architektura;

nowe funkcje w istniejącym systemie;

struktura lub treść danych zewnętrznych;

narzędzia lub komponenty zewnętrznych producentów;

problemy związane z wydajnością;

projekt interfejsu użytkownika.
Tworzenie i badanie prototypów jest typowym procesem uczenia się. Wartość
prototypu nie leży w jego kodzie, tylko we wnioskach, które sformułowaliśmy na
jego podstawie. Właśnie te wnioski są istotą idei posługiwania się prototypami.
WSKAZÓWKA NR 21
Prototypy należy tworzyć z myślą o nauce.
Jak używać prototypów
Które szczegóły można zignorować podczas konstruowania prototypu?

Poprawność. W pewnych sytuacjach można z powodzeniem posługiwać
się nieprawidłowymi, wręcz głupimi danymi.

Kompletność. Prototyp może obejmować swoim działaniem bardzo ograniczony aspekt funkcjonowania docelowego produktu, być może tylko
wybrany wcześniej zestaw danych wejściowych i jeden element menu.
4337ebf6db5c7cc89e4173803ef3875a
4
86 
Rozdział 2.
Postawa pragmatyczna

Niezawodność. Mechanizm sprawdzania błędów może być niekompletny
lub w ogóle może nie istnieć. W razie zboczenia z ustalonej wcześniej ścieżki
prototyp może bez żadnych konsekwencji ulec awarii i spłonąć, dając przy
okazji wspaniały pokaz fajerwerków. To zupełnie naturalne.

Styl. Kod prototypu w większości przypadków nie wymaga komentarzy ani
dokumentacji (choć w wyniku doświadczeń przeprowadzonych z użyciem
prototypu można opracować całe sterty dokumentów).
Ponieważ prototyp powinien ukrywać szczegóły i koncentrować się na konkretnych aspektach przyszłego systemu, do implementowania prototypów warto
używać języków bardzo wysokiego poziomu (wyższych niż w przypadku reszty
projektu, na przykład takich języków jak Python lub Ruby). Możesz kontynuować tworzenie oprogramowania w tym samym języku, w którym stworzyłeś
prototyp, lub możesz go zmienić — ostatecznie prototyp, gdy spełni swoją funkcję, przestaje być potrzebny.
Do stworzenia prototypów interfejsów użytkownika skorzystaj z narzędzi,
które pozwolą Ci skupić się na wyglądzie i (lub) na interakcjach, ale bez koncentrowania się na kodzie lub zestawie znaczników.
Języki skryptowe doskonale sprawdzają się w roli spoiwa łączącego niskopoziomowe fragmenty kodu w ramach nowych kombinacji. Stosując prototypy można
błyskawicznie wiązać istniejące komponenty w nowe konfiguracje i sprawdzać,
jak działają poszczególne kombinacje.
Tworzenie prototypów architektury
Wiele prototypów konstruuje się z myślą o modelowaniu całych systemów.
W przeciwieństwie do pocisków smugowych żaden z modułów wchodzących
w skład prototypowego systemu nie musi być szczególnie funkcjonalny. W praktyce opracowanie prototypu architektury nie wymaga nawet kodowania — taki
prototyp można z powodzeniem stworzyć na tablicy, na karteczkach samoprzylepnych lub na fiszkach. Interesuje nas sposób funkcjonowania systemu jako
całości, zatem szczegółowe rozstrzygnięcia możemy odłożyć na później. Poniżej
wymieniono kilka konkretnych obszarów, w których warto rozważyć opracowanie prototypu architektury:

Czy zakres odpowiedzialności najważniejszych komponentów jest dobrze
zdefiniowany i przemyślany?

Czy odpowiednio precyzyjnie zdefiniowano zasady współpracy głównych
komponentów?

Czy udało się zminimalizować zjawisko powiązań?

Czy potrafimy zidentyfikować potencjalne źródła powielania?

Czy definicje i ograniczenia interfejsu są możliwe do zaakceptowania?
4337ebf6db5c7cc89e4173803ef3875a
4
Prototypy i karteczki samoprzylepne


87
Czy każdy moduł dysponuje ścieżką dostępu do danych niezbędnych w czasie wykonywania? Czy ma dostęp do tych danych w czasie, gdy ich potrzebuje?
Ostatni aspekt bywa źródłem wyjątkowo wielu niespodzianek i jednocześnie
najcenniejszych wniosków z eksperymentów przeprowadzanych przy użyciu
prototypów.
Jak nie używać prototypów
Zanim zdecydujemy się zaprezentować jakikolwiek prototyp w formie oprogramowania, koniecznie musimy poinformować wszystkich, że demonstrowany kod
ma jednorazowy charakter i nie będzie wykorzystywany w docelowym produkcie. Prototypy mogą wydać się atrakcyjne osobom, które nie wiedzą, że mają
do czynienia właśnie z prototypami. Musimy możliwie jednoznacznie zaznaczyć, że prezentowany kod jest jednorazowy, niekompletny i niemożliwy do
dokończenia w tej formie.
Pozorna kompletność demonstrowanego prototypu bardzo łatwo może stać się
źródłem nieporozumień, które w skrajnych przypadkach mogą prowadzić do
nacisków ze strony sponsorów projektu i kierownictwa firmy, aby wdrożyć właśnie prototyp (lub jego potomka). Musimy pamiętać, że chociaż istnieje możliwość konstruowania doskonałego prototypu nowego samochodu z drzewa balsy
i mocnej taśmy samoprzylepnej, nikt o zdrowych zmysłach nie będzie próbował przebić się tym „autem” przez miasto w godzinach szczytu.
Jeśli wydaje nam się, że w określonym środowisku lub kulturze ryzyko błędnej
interpretacji przeznaczenia kodu prototypu jest szczególnie duże, być może
powinniśmy całkowicie zrezygnować z tej drogi na rzecz pocisków smugowych.
W ten sposób opracujemy solidny framework, na którym będziemy mogli budować właściwy system.
Właściwie użyty prototyp może nam oszczędzić mnóstwo czasu, pieniędzy, kłopotów i trudności związanych z identyfikacją i eliminowaniem potencjalnych
problemów już na wczesnym etapie cyklu wytwarzania, kiedy usuwanie usterek
jest jednocześnie tanie i łatwe.
Pokrewne podrozdziały

Temat 12., „Pociski smugowe”.

Temat 14., „Języki dziedzinowe”.

Temat 17., „Powłoki”.

Temat 27., „Nie prześcigaj swoich świateł”.

Temat 37., „Słuchaj swojego jaszczurczego mózgu”.

Temat 45., „Kopalnia wymagań”.

Temat 52., „Wpraw w zachwyt użytkowników”.
4337ebf6db5c7cc89e4173803ef3875a
4
88 
Rozdział 2.
Postawa pragmatyczna
Ćwiczenia
3. Pracownicy działu marketingu chcą się z nami spotkać i przeprowadzić
burzę mózgów poświęconą kilku projektom strony internetowej. Zastanawiają się między innymi nad możliwością użycia map obrazów z możliwością klikania, które będą przenosiły użytkowników na inne strony.
Nie potrafią jednak zdecydować, który model obrazu będzie najlepszy —
czy powinien przedstawiać samochód, telefon, czy dom. Dysponujemy listą stron docelowych i ich treścią; warto więc opracować i przedstawić
kilka prototypów. A, bylibyśmy zapomnieli — mamy na to 15 minut. Jakich
narzędzi należałoby użyć?
14
42
Języki dziedzinowe
Ograniczenia naszego języka są ograniczeniami naszego świata.
Ludwig Wittgenstein
Języki komputerowe wpływają zarówno na sposób postrzegania przez nas problemów, jak i na sposób, w jaki myślimy o komunikacji. Każdy język oferuje
pewną listę funkcji — zwykle są to takie slogany jak statyczna lub dynamiczna
kontrola typów, wczesne lub późne wiązanie, modele dziedziczenia (pojedynczego,
wielokrotnego lub żadnego). Każdy z tych elementów może sugerować pewne
rozwiązania lub zniechęcać do tych rozwiązań. Projektowanie rozwiązania
z myślą o C++ doprowadzi do innych wyników niż projektowanie rozwiązania
z myślą o języku Haskell (i odwrotnie). Można na tę kwestię spojrzeć także z drugiej strony (naszym zdaniem jeszcze ważniejszej) — język dziedziny problemu
może być źródłem sugestii dotyczących rozwiązań programistycznych.
Zawsze staramy się pisać kod, stosując słownictwo z dziedziny danej aplikacji
(patrz podrozdział „Utrzymywanie glosariusza”). W pewnych przypadkach możemy
nawet wejść na wyższy poziom i programować przy użyciu słownictwa, składni
i semantyki (tylko w praktyce języka) właściwych danej dziedzinie.
WSKAZÓWKA NR 22
Programuj z zachowaniem ścisłego związku z dziedziną problemu.
Przykłady języków dziedzinowych
Spójrzmy na kilka przykładów języków dziedzinowych.
4337ebf6db5c7cc89e4173803ef3875a
4
Języki dziedzinowe

89
RSpec
RSpec7 to biblioteka obsługi testów dla języka Ruby. Biblioteka RSpec zainspirowała wersje dla większości innych współczesnych języków programowania.
Test w RSpec ma odzwierciedlać oczekiwane zachowanie kodu.
describe BowlingScore do
it "totals 12 if you score 3 four times" do
score = BowlingScore.new
4.times { score.add_pins(3) }
expect(score.total).to eq(12)
end
end
Cucumber
Cucumber8 to niezależna od języka programowania notacja specyfikowania testów. Testy są uruchamiane przy użyciu wersji języka Cucumber odpowiedniej
dla używanego języka. W celu zapewnienia wsparcia dla składni przypominającej język naturalny, trzeba także zastosować określone mechanizmy dopasowujące, które pozwolą na rozpoznawanie fraz i wyodrębnianie parametrów
dla testów.
Feature: Scoring
Background:
Given an empty scorecard
Scenario: bowling a lot of 3s
Given I throw a 3
And I throw a 3
And I throw a 3
And I throw a 3
Then the score should be 12
Testy pisane w Cucumber miały być przeznaczone do czytania przez użytkowników oprogramowania (chociaż w praktyce rzadko są wykorzystywane w tej
roli, co wyjaśniono w poniższej ramce).
Trasy Phoenix
Wiele frameworków webowych obsługuje mechanizm routingu — mapowania
przychodzących żądań HTTP na funkcje ich obsługi w kodzie. Oto przykład użycia frameworka Phoenix9.
scope "/", HelloPhoenix do
pipe_through :browser # Użyj stosu domyślnej przeglądarki
get "/", PageController, :index
resources "/users", UserController
end
7
https://rspec.info
8
https://cucumber.io/
9
https://phoenixframework.org/
4337ebf6db5c7cc89e4173803ef3875a
4
90 
Rozdział 2.
Postawa pragmatyczna
Dlaczego niezbyt wielu użytkowników biznesowych czyta specyfikacje
w notacji Cucumber?
Jednym z powodów, dla których nie sprawdza się klasyczny model: zbieranie wymagań, projektowanie, kodowanie, wdrażanie jest to, że model ten bazuje na założeniu,
że wiemy, jakie są wymagania. Jednak w praktyce rzadko tak jest. Użytkownicy biznesowi mają mgliste pojęcie o tym, co chcą osiągnąć, ale nie znają szczegółów ani
się nimi nie przejmują. Na tym polega część wartości pragmatycznych programistów: intuicyjnie wyczuwamy zamiar użytkownika i przekształcamy go na kod.
W związku z tym, kiedy staramy się skłonić użytkownika biznesowego, aby zatwierdził
dokument określający wymagania lub zgodził się na zbiór wymagań zapisanych
w notacji Cucumber, robimy coś, co można porównać do nakłonienia użytkownika
do sprawdzenia pisowni w eseju napisanym w języku sumeryjskim. W konsekwencji
użytkownik biznesowy wprowadza kilka losowych zmian, aby zachować twarz, i podpisuje wymagania, aby pozbyć się nas ze swojego biura.
Jeśli jednak damy mu kod, który działa, będzie mógł go wypróbować. Na tej podstawie
będzie mógł określić swoje rzeczywiste potrzeby.
Powyższa specyfikacja trasy mówi, że żądania zaczynające się od znaku / będą
przechodziły przez zbiór filtrów odpowiednich dla przeglądarek. Żądanie adresu
/ zostanie obsłużone przez funkcję index w module PageController. Moduł Users
Controller implementuje funkcje potrzebne do zarządzania zasobami dostępnymi za pomocą adresu url /users.
Ansible
Ansible10 jest narzędziem, które konfiguruje oprogramowanie, zazwyczaj na kilku
zdalnych serwerach. Do tego celu wykorzystuje dostarczoną specyfikację.
Następnie, zgodnie z nią wykonuje odpowiednie działania na serwerach. Specyfikacja może być zapisana w notacji YAML11 — języku, który buduje struktury
danych na podstawie tekstowych opisów:
--- name: install nginx
apt: name=nginx state=latest
- name: ensure nginx is running (and enable it at boot)
service: name=nginx state=started enabled=yes
- name: write the nginx config file
template: src=templates/nginx.conf.j2 dest=/etc/nginx/nginx.conf
notify:
- restart nginx
Powyższa specyfikacja określa, że na serwerach zostanie zainstalowana najnowsza wersja nginx, która zostanie domyślnie uruchomiona i wykorzysta dostarczony plik konfiguracyjny.
10
https://www.ansible.com/
11
https://yaml.org/
4337ebf6db5c7cc89e4173803ef3875a
4
Języki dziedzinowe

91
Cechy języków dziedzinowych
Przyjrzyjmy się tym przykładom nieco bliżej.
RSpec i router Phoenix są napisane za pomocą języków-gospodarzy (Ruby i Elixir). Korzystają one z dość sprytnego kodu, włącznie z technikami metaprogramowania i makrami, ale ostatecznie są kompilowane i uruchamiane tak jak
zwykły kod.
Testy Cucumber i konfiguracje Ansible są napisane we własnych językach. Testy
Cucumber są przekształcane na kod do uruchomienia albo na strukturę danych,
natomiast specyfikacje Ansible są zawsze przekształcane na strukturę danych,
która jest uruchamiana przez sam system Ansible.
W rezultacie kod RSpec i kod routera są osadzone w kodzie, który uruchamiamy:
są one rzeczywistymi rozszerzeniami słownictwa naszego kodu. Specyfikacje
Cucumber i Ansible są odczytywane za pomocą kodu i przekształcane w jakąś
formę możliwą do wykorzystania przez kod.
Kody RSpec i routera Phoenix są wywoływane za pomocą wewnętrznych języków dziedzinowych, natomiast Cucumber i Ansible korzystają z języków zewnętrznych.
Kompromisy pomiędzy językami wewnętrznymi
a zewnętrznymi
Ogólnie rzecz biorąc, wewnętrzny język dziedzinowy może skorzystać z funkcjonalności swojego języka-gospodarza: tworzony język dziedzinowy ma większe możliwości, które uzyskujemy „za darmo”. Na przykład możemy użyć kodu Ruby, aby automatycznie stworzyć kilka testów RSpec. W tym przypadku
możemy przetestować wyniki, w sytuacji, gdy nie ma trafień spare lub strike:
describe BowlingScore do
(0..4).each do |pins|
(1..20).each do |throws|
target = pins * throws
it "totals #{target} if you score #{pins} #{throws} times" do
score = BowlingScore.new
throws.times { score.add_pins(pins) }
expect(score.total).to eq(target)
end
end
end
end
Właśnie napisałeś 100 testów. Przez pozostałą część dnia możesz mieć wolne.
Wadą wewnętrznych języków dziedzinowych jest ich powiązanie ze składnią
i semantyką języka. Chociaż niektóre języki są niezwykle elastyczne w tym
zakresie, to nadal jesteśmy zmuszeni do kompromisu między językiem pożądanym a tym, który możemy zaimplementować.
4337ebf6db5c7cc89e4173803ef3875a
4
92 
Rozdział 2.
Postawa pragmatyczna
Trzeba pamiętać, że stworzony kod musi być poprawny pod względem składni
języka docelowego. Języki z obsługą makr (takie jak Elixir, Clojure i Crystal)
dają nieco większą elastyczność, ale ostatecznie składnia jest składnią.
Języki zewnętrzne nie wprowadzają takich ograniczeń. Wystarczy tylko napisać
odpowiedni parser dla języka. Czasami można użyć parsera napisanego przez
kogoś innego (tak, jak w przypadku Ansible i YAML), ale w takiej sytuacji jesteśmy zmuszeni do podejmowania kompromisów.
Napisanie parsera zwykle oznacza dodawanie do aplikacji nowych bibliotek
i ewentualnie narzędzi. Napisanie dobrego parsera nie jest zadaniem trywialnym. Nie trzeba się jednak tego obawiać. Można skorzystać z generatorów parserów, takich jak bison lub ANTLR, oraz z frameworków parsowania, takich jak
wiele parserów PEG.
Nasza sugestia jest dość prosta: w opracowanie parsera nie należy wkładać
zbyt wiele wysiłku. Powinien on być proporcjanalny do osiąganych dzięki temu
zysków. Pisanie języka dziedzinowego wiąże się z pewnymi dodatkowymi kosztami projektu. Trzeba mieć pewność, że poniesienie tych kosztów przyniesie
(w dłuższej perspektywie) jakieś oszczędności.
Ogólnie rzecz biorąc, należy używać, o ile to możliwe, gotowych języków zewnętrznych (takich jak YAML, JSON lub CSV). Jeśli nie ma takiej możliwości, należy
korzystać z języków wewnętrznych. Zalecamy używanie języków zewnętrznych
jedynie w tych przypadkach, gdy Twój język będzie napisany przez użytkowników aplikacji.
Tani wewnętrzny język dziedzinowy
Tworzenie wewnętrznych języków dziedzinowych jest niepotrzebne, jeśli nie
przeszkadza nam składnia języka-gospodarza. Nie warto intensywnie korzystać z metaprogramowania. Zamiast tego wystarczy napisać funkcje, które wykonają potrzebne zadania. W ten sposób działa specyfikacja RSpec:
describe BowlingScore do
it "totals 12 if you score 3 four times" do
score = BowlingScore.new
4.times { score.add_pins(3) }
expect(score.total).to eq(12)
end
end
W tym kodzie describe, it, expect, to i eq to po prostu metody języka Ruby. „Za
kulisami” potrzeba trochę dodatkowych mechanizmów związanych z przekazywaniem obiektów, ale wszystko to jest po prostu kodem. Omówimy to trochę
dokładniej w ćwiczeniach.
4337ebf6db5c7cc89e4173803ef3875a
4
Języki dziedzinowe

93
Pokrewne podrozdziały

Temat 8., „Istota dobrego projektu”.

Temat 13., „Prototypy i karteczki samoprzylepne”.

Temat 32., „Konfiguracja”.
Wyzwania

Czy część wymagań aktualnie realizowanego projektu można by wyrazić
w języku właściwym danej dziedzinie? Czy byłoby możliwe napisanie kompilatora lub translatora, który generowałby większość wymaganego kodu?

Jeśli decydujemy się na stosowanie minijęzyków jako sposobu programowania bliżej dziedziny problemu, godzimy się na dodatkowe koszty związane
z implementacją tych języków. Czy framework opracowany z myślą o jednym projekcie będzie można ponownie wykorzystać w innych projektach?
Ćwiczenia
4. Chcemy zaimplementować minijęzyk sterujący działaniem prostego pakietu do rysowania (na przykład systemem tzw. grafiki żółwia — ang. turtle graphics). Język składa się z jednoliterowych poleceń. Po niektórych poleceniach należy podać pojedynczą liczbę. Na przykład poniższe dane
wejściowe spowodowałyby narysowanie prostokąta.
P
D
W
N
E
S
U
2
2
1
2
1
#
#
#
#
#
#
#
wybierz drugie pióro
opuść pióro
narysuj odcinek o długości 2 cm na zachód
teraz 1 cm na północ
2 cm na wschód
i jeszcze 1 cm na południe
unieś pióro
5. W poprzednim ćwiczeniu zaimplementowaliśmy parser dla języka rysowania — był to zewnętrzny język dziedzinowy. Teraz zaimplementuj go
ponownie jako język wewnętrzny. Nie rób niczego nadzwyczajnego: po
prostu napisz funkcję dla każdego polecenia. Być może trzeba będzie zmienić nazwy poleceń na pisane małymi literami oraz być może opakować
polecania, aby dostarczyć pewien kontekst.
6. Zaprojektuj gramatykę BNF z myślą o analizie składniowej specyfikacji
godziny. Gramatyka powinna akceptować wszystkie poniższe przykłady.
4pm, 7:38pm, 23:42, 3:16, 3:16am
7. Zaimplementuj analizator składniowy dla gramatyki BNF z poprzedniego
ćwiczenia za pomocą wybranego generatora parserów PEG w wybranym
języku. Wynik powinien być liczbą całkowitą oznaczającą liczbę minut, które
upłynęły od północy.
8. Zaimplementuj parser godziny w języku skryptowym z wykorzystaniem wyrażeń regularnych.
4337ebf6db5c7cc89e4173803ef3875a
4
94 
15
43
Rozdział 2.
Postawa pragmatyczna
Szacowanie
Biblioteka Kongresu w Waszyngtonie obecnie obejmuje około 75 terabajtów
informacji cyfrowych w internecie. Szybko! Ile czasu zajmie przesłanie tych
wszystkich informacji w sieci 1 Gbps? Ile przestrzeni dyskowej potrzeba do przechowywania miliona nazwisk i adresów? Ile czasu zajmie skompresowanie 100
megabajtów tekstu? Ile miesięcy zajmie realizacja danego projektu?
Na pewnym poziomie wszystkie te pytania są o tyle nieistotne, że dotyczą brakujących, nieznanych informacji. Okazuje się jednak, że można na te pytania
odpowiedzieć, pod warunkiem dysponowania odpowiednim modelem szacowania. Co więcej, przy okazji szacowania czasu trwania projektu możemy lepiej
zrozumieć otoczenie, w którym będą funkcjonowały nasze programy.
Kiedy opanujemy sztukę szacowania, kiedy rozwiniemy umiejętność oceny czasu
trwania projektu w stopniu niezbędnym do prawidłowego określania rzędu wielkości, będziemy potrafili w magiczny sposób określać wykonalność zleceń. Kiedy
ktoś powie, że będzie wysyłał kopie zapasowe przez sieć do usługi S3, będziemy
potrafili intuicyjnie stwierdzić, czy taka koncepcja jest praktyczna. Podczas
kodowania będziemy od razu wiedzieli, które podsystemy wymagają optymalizacji, a które można pozostawić w dotychczasowej formie.
WSKAZÓWKA NR 23
Szacowanie pozwala unikać przykrych niespodzianek.
Na końcu tego podrozdziału zaproponujemy jedną prawidłową odpowiedź, której można udzielać każdemu, kto oczekuje od nas jakichś szacunków.
Kiedy dokładne jest wystarczająco dokładne?
W pewnym sensie każda odpowiedź jest szacunkiem. Część tych szacunków jest
po prostu bardziej dokładna, trafna niż pozostałe. W tej sytuacji pierwsze pytanie, które powinniśmy sobie zadać przed rozważeniem żądania oszacowania
czegokolwiek, powinno dotyczyć kontekstu użycia ewentualnej odpowiedzi. Czy
odbiorca oczekuje dużej dokładności, czy raczej zadowoli się wskazaniem rzędu
wielkości?
Jednym z interesującym aspektów szacowania jest to, że stosowane jednostki
istotnie wpływają na interpretację wyników. Kiedy przewidujemy, że realizacja
jakiegoś zadania zajmie około 130 dni roboczych, większość rozmówców uzna,
że termin ukończenia prac jest dość bliski. Jeśli jednak powiemy, że ten sam
projekt wymaga około 6 miesięcy, rozmówca uzna, że termin jest dość odległy
i nadejdzie dopiero za pięć do siedmiu miesięcy. Co ciekawe, obie wartości reprezentują ten sam okres, ale 130 dni sugeruje większy stopień precyzji, czyli
4337ebf6db5c7cc89e4173803ef3875a
4
Szacowanie
 95
trafniejszą prognozę. Zalecamy stosowanie następujących reguł dla jednostek
wyrażania oszacowań:
Czas
Jednostka oszacowania
1 – 15 dni
dni
3 – 8 tygodni
tygodnie
8 – 20 tygodni miesiące
20 + tygodni
warto się dobrze zastanowić, czy szacowanie w ogóle ma sens
Jeśli po przeprowadzeniu wszystkich niezbędnych analiz stwierdzamy, że jakiś
projekt zajmie 125 dni roboczych (25 tygodni), być może warto oszacować ten
czas jako „około sześć miesięcy”.
To samo dotyczy szacunków każdej innej wielkości — zawsze powinniśmy
wybierać jednostki dobrze odzwierciedlające precyzję, którą chcemy zakomunikować odbiorcy.
Skąd się biorą oszacowania?
Wszystkie oszacowania formułuje się na podstawie modeli problemu. Zanim
jednak zajmiemy się technikami budowania modeli, warto zwrócić uwagę na
prosty zabieg, który zawsze prowadzi do prawidłowych odpowiedzi — wystarczy
spytać kogoś, kto już to robił. Zanim zbyt mocno zaangażujemy się w konstruowanie modelu, powinniśmy rozejrzeć się dookoła w poszukiwaniu kogoś, kto
w przeszłości znalazł się w podobnej sytuacji. Warto sprawdzić, jak oni rozwiązali ten problem. Znalezienie identycznego scenariusza jest co prawda mało
prawdopodobne, ale możliwości korzystania z cudzych doświadczeń w wielu
przypadkach są zaskakująco duże.
Należy zrozumieć przedmiot pytania
Pierwszym etapem każdego procesu szacowania powinno być dobre rozumienie
przedmiotu pytania. Oprócz omówionych przed chwilą kwestii dokładności
musimy jeszcze dysponować pewną wiedzą o zakresie danej dziedziny. Informacje na ten temat zwykle są pośrednio zawarte w pytaniu, ale powinniśmy
odruchowo analizować zasięg przed przystąpieniem do właściwego szacowania.
Wybrany przez nas zasięg nierzadko decyduje o istotnym fragmencie naszej
odpowiedzi: „Jeśli przyjmiemy, że nie będzie korków spowodowanych wypadkiem
drogowym i że samochód jest zatankowany, powinniśmy dotrzeć na miejsce
w 20 minut”.
Należy zbudować model systemu
To jeden z przyjemniejszych aspektów szacowania. Na podstawie tego, jak rozumiemy zadane pytanie, możemy zbudować przemyślany model pamięciowy. Jeśli
przedmiotem szacowania jest czas odpowiedzi, model może obejmować serwer
4337ebf6db5c7cc89e4173803ef3875a
4
96 
Rozdział 2.
Postawa pragmatyczna
i jakieś odwzorowanie ruchu przychodzącego. W przypadku projektu model może
obejmować kroki używane przez naszą organizację w czasie wytwarzania oraz
bardzo ogólny obraz potencjalnej implementacji systemu.
Budowa modelu może być nie tylko twórczym doświadczeniem, ale też czynnością o wymiernych korzyściach praktycznych. Procedura konstruowania modelu
często prowadzi do odkryć wzorców i procesów, które w pierwszej chwili nie
były widoczne. W pewnych przypadkach budowa modelu może doprowadzić nas
do wniosku o konieczności przebudowy oryginalnego pytania: „Prosiłeś o oszacowanie czasu trwania projektu X. Wydaje się jednak, że projekt Y, czyli pewien
wariant projektu X, można zrealizować blisko dwukrotnie szybciej, rezygnując
z zaledwie jednej funkcji”.
Budowanie modelu wprowadza do procesu szacowania pewne nieścisłości. Brak
dokładności na tym etapie jest nie tylko nieunikniony, ale też korzystny. Poświęcamy precyzję oszacowania na rzecz prostoty modelu. Co ciekawe, dwukrotne
wydłużenie prac nad projektem może doprowadzić do zaledwie niewielkiego
wzrostu trafności oszacowania. Z czasem nabierzemy doświadczenia, które
pozwoli nam stwierdzić, w którym momencie należy zakończyć doskonalenie
modelu.
Należy podzielić model na komponenty
Kiedy już będziemy dysponowali modelem, możemy podzielić go na komponenty.
Będziemy musieli odkryć reguły matematyczne opisujące wzajemne interakcje
tych komponentów. W pewnych przypadkach komponent wpływa na powstawanie pojedynczej wartości dodawanej do zbiorczego wyniku. Niektóre komponenty mogą dostarczać współczynniki używane w operacjach mnożenia, inne
mogą być bardziej złożone (mogą na przykład odpowiadać za symulowanie ruchu
przychodzącego do jakiegoś węzła).
Szybko odkryjemy, że każdy komponent ma parametry wpływające na jego
udział w funkcjonowaniu całego modelu. Na tym etapie wystarczy tylko zidentyfikować te parametry.
Każdemu parametrowi należy nadać wartość
Po wyodrębnieniu wszystkich parametrów możemy przystąpić do przypisywania każdemu z nich konkretnej wartości. W tym kroku musimy liczyć się z możliwością popełnienia błędów. Cała sztuka polega na wskazaniu parametrów,
które będą miały największy wpływ na wynik, i skoncentrowaniu wysiłków na
prawidłowym zdefiniowaniu ich wartości. Parametry, których wartości są dodawane do końcowego wyniku, zwykle są mniej ważne od tych, przez które wynik
jest mnożony lub dzielony. Dwukrotne zwiększenie przepustowości łącza może
spowodować dwukrotny wzrost ilości danych otrzymywanych w ciągu godziny,
ale na przykład dodanie opóźnienia przesyłania na poziomie 5 ms może nie
mieć istotnego wpływu na funkcjonowanie systemu.
4337ebf6db5c7cc89e4173803ef3875a
4
Szacowanie
 97
Powinniśmy wypracować przemyślany schemat wyznaczania tych krytycznych
parametrów. W przypadku mechanizmu kolejkowania być może powinniśmy mierzyć rzeczywistą liczbę transakcji przychodzących do istniejącego systemu lub
znaleźć podobny system, dla którego można by zgromadzić odpowiednie statystyki. Podobnie, możemy albo mierzyć aktualny czas potrzebny do obsługi pojedynczego żądania, albo użyć technik opisanych w tym podrozdziale szacowania
tego czasu. W praktyce często musimy opierać jedno oszacowanie na innych
podoszacowaniach. Właśnie takie łączenie oszacowań zwykle prowadzi do największych błędów.
Należy obliczyć odpowiedzi
Tylko w najprostszych przypadkach wynikiem szacowania jest pojedyncza odpowiedź. Oczywiście, chcielibyśmy powiedzieć: „Spacer przez pięć przecznic zajmie
mi 15 minut”. Ponieważ jednak systemy są coraz bardziej złożone, nasze odpowiedzi muszą uwzględniać przyjęte założenia. Powinniśmy przeprowadzić wiele
obliczeń, zmieniając wartości najważniejszych parametrów, aż wypracujemy
kombinacje najbardziej zbliżające nasz model do rzeczywistości. Sporym ułatwieniem mogą być arkusze kalkulacyjne. Warto następnie sformułować odpowiedź na podstawie tych parametrów. „Czas odpowiedzi wyniesie w przybliżeniu
trzy czwarte sekundy, jeśli system będzie korzystał z magistrali SCSI i 64 MB
pamięci, lub jedną sekundę, jeśli system będzie używał 48 MB pamięci”.
(Należy pamiętać, że „trzy czwarte sekundy” robi na odbiorcy zupełnie inne
wrażenie precyzji niż wartość 750 milisekund).
Na etapie obliczeń możemy uzyskiwać odpowiedzi, które wydadzą nam się
dziwne. Nie powinniśmy jednak lekkomyślnie rezygnować z tych wartości. Jeśli
przyjęte obliczenia arytmetyczne są prawidłowe, być może problem tkwi w błędnym rozumieniu analizowanej kwestii lub niewłaściwym modelu. Taka informacja jest wyjątkowo cenna.
Należy śledzić własne umiejętności szacowania
Uważamy, że rejestrowanie własnych szacunków z myślą o ich porównywaniu
z rzeczywistymi wartościami jest wprost doskonałym pomysłem. Jeśli łączne
oszacowanie wymagało obliczenia podoszacowań, warto śledzić także te wartości składowe. W wielu przypadkach będziemy z satysfakcją stwierdzali, że nasze
oszacowania były całkiem trafne — w rzeczywistości po jakimś czasie dość precyzyjne oszacowania będą dla nas czymś oczywistym.
Jeśli nasze oszacowanie okaże się błędne, w żadnym razie nie powinniśmy wzruszyć ramionami i przejść nad tym do porządku dziennego. Powinniśmy raczej
sprawdzić, skąd wzięła się różnica dzieląca rzeczywistość od naszych przewidywań. Być może wybraliśmy parametry, które nie odzwierciedlały rzeczywistego
charakteru problemu. A może przyjęliśmy błędny model. Niezależnie od powodu
warto poświęcić trochę czasu na analizę przyczyn tego stanu rzeczy. Jeśli to
zrobimy, nasze następne oszacowanie będzie lepsze.
4337ebf6db5c7cc89e4173803ef3875a
4
98 
Rozdział 2.
Postawa pragmatyczna
Szacowanie harmonogramów projektów
Zwykle słyszymy prośby o oszacowanie czasu, jaki zajmie nam wykonanie jakiejś
czynności. Jeśli ta czynność jest złożona, podanie dokładnych szacunków może
okazać się bardzo trudne. W tym podrozdziale zajmiemy się dwoma technikami
zmniejszania niepewności szacowania.
Malowanie pocisku
— Jak długo zajmie pomalowanie domu?
— Cóż, jeśli wszystko pójdzie dobrze, a ta farba pokrywa tak, jak podano na
opakowaniu, to być może uda się skończyć w 10 godzin. To jednak jest mało
prawdopodobne. Przypuszczam, że bardziej realistyczna ocena to około 18 godzin.
Oczywiście, jeśli pogoda zmieni się na gorszą, czas może wydłużyć się do 30 lub
więcej godzin.
Tak szacują ludzie w prawdziwym świecie. Nie podają jednej liczby (chyba że
ich do tego zmusimy), ale prezentują różne scenariusze.
Kiedy w US Navy planowano projekt łodzi podwodnej Polaris, przyjęto styl szacowania z wykorzystaniem metodologii określanej jako PERT (ang. Program
Evaluation Review Technique — dosłownie: technika przeglądów szacowania
programu).
Dla każdego zadania PERT wyznaczane są oceny: optymistyczna, najbardziej
prawdopodobna i pesymistyczna. Zadania są organizowane w sieć zależności,
a następnie wykorzystywanych jest kilka prostych obliczeń statystycznych w celu
zidentyfikowania prawdopodobnego, najlepszego i najgorszego czasu realizacji
całego projektu.
Użycie takiego zakresu wartości to świetny sposób uniknięcia jednego z najczęstszych powodów błędu szacowania: zwiększenia szacunku ze względu na
brak pewności. Zamiast tego, obliczenia statystyczne stosowane w metodzie PERT
eliminują niepewność, co przyczynia się do uzyskania lepszych szacunków dla
całego projektu.
Nie jesteśmy jednak wielkimi fanami tej metody. Ludzie mają tendencję do
wytwarzania zajmujących całą ścianę wykresów wszystkich zadań w projekcie
i na tej podstawie wierzą, że skoro skorzystali z obliczeń według wzoru, to mają
dokładne oszacowanie. Istnieje ryzyko, że szacunki są niedokładne, ponieważ
nie robiliśmy tego nigdy wcześniej.
Jedzenie słonia
Często okazuje się, że jedynym sposobem określenia harmonogramu prac nad
projektem jest gromadzenie doświadczenia w realizacji tego projektu. Wspomniane zjawisko wcale nie musi być paradoksem, jeśli tylko stosujemy metodykę
4337ebf6db5c7cc89e4173803ef3875a
4
Szacowanie
 99
przyrostowego wytwarzania oprogramowania i konsekwentnie powtarzamy
następujące kroki:

weryfikacja wymagań;

analiza ryzyka;

projektowanie, implementacja i integracja;

weryfikacja z użytkownikami.
Początkowo możemy mieć mgliste pojęcie o liczbie niezbędnych interakcji bądź
o czasie zajmowanym przez te interakcje. Niektóre metody wymagają od nas pełnego szacowania już na etapie przygotowywania początkowego planu, jednak
poza najprostszymi projektami taki model pracy jest chybiony. Jeśli nie pracujemy nad aplikacją zbliżoną do naszego ostatniego produktu, jeśli nie pracujemy w tym samym zespole i jeśli nie korzystamy z dokładnie tej samej technologii, szacowanie czasu trwania projektu jest zwykłym zgadywaniem.
Warto więc zakończyć kodowanie i testowanie kilku podstawowych funkcji, po
czym oznaczyć tak opracowany fragment systemu jako pierwszą iterację. Na
podstawie doświadczeń zebranych w trakcie tej iteracji możemy poprawić początkowe przypuszczenia dotyczące liczby iteracji i elementów składających się na
poszczególne iteracje. Kolejne korekty powinny być coraz bliższe rzeczywistości,
powodując stopniową poprawę trafności całego harmonogramu.
Jest to zgodne ze starym przysłowiem, które mówi, jak można zjeść słonia: kęs
po kęsie.
WSKAZÓWKA NR 24
Harmonogram i kod powinny powstawać iteracyjnie.
Proponowane rozwiązanie nie jest zbyt popularne wśród menedżerów, którzy
zwykle oczekują pojedynczej, precyzyjnej liczby jeszcze przed przystąpieniem
do realizacji projektu. Powinniśmy pomóc przełożonym w zrozumieniu, że
o harmonogramie prac decyduje zespół, jego produktywność i otoczenie, w którym pracuje. Przyjęcie formalnych reguł i stopniowe doskonalenie harmonogramu w ramach każdej iteracji powinno nam umożliwić dostarczanie możliwie precyzyjnych oszacowań.
Co odpowiedzieć na prośbę o oszacowanie
Wystarczy powiedzieć: „Jeszcze do tego wrócę”.
Niemal zawsze możemy uzyskać lepsze wyniki, jeśli spowolnimy cały proces szacowania i poświęcimy trochę czasu na czynności opisane w tym podrozdziale.
Szacunki wymyślone naprędce przy automacie z kawą mogą się na nas szybko
zemścić (podobnie jak sama kawa).
4337ebf6db5c7cc89e4173803ef3875a
4
100 
Rozdział 2.
Postawa pragmatyczna
Pokrewne podrozdziały

Temat 7., „Komunikuj się!”.

Temat 39., „Szybkość algorytmu”.
Wyzwania

Należy zacząć prowadzić rejestr własnych szacunków. Dla każdego oszacowania należy śledzić trafność w zestawieniu z rzeczywistością. Jeśli błąd
oszacowania przekracza 50%, warto sprawdzić, gdzie popełniono błąd.
Ćwiczenia
9. Zadano nam pytanie: „Kiedy przepustowość jest większa — w przypadku
linii komunikacyjnej o szybkości 1 Gb/s czy w przypadku osoby przenoszącej pomiędzy dwoma komputerami nośnik pamięci o pojemności 1 TB?”.
Jakie ograniczenia należałoby uwzględnić w odpowiedzi, aby mieć pewność,
że jej zasięg jest prawidłowy? (Możemy na przykład zastrzec, że nie uwzględniamy czasu potrzebnego do uzyskania dostępu do danych na taśmie).
10. W którym modelu przepustowość jest większa?
4337ebf6db5c7cc89e4173803ef3875a
4
Rozdział 3.
Podstawowe narzędzia
Każdy rzemieślnik rozpoczyna pracę od skompletowania podstawowego zestawu
narzędzi odpowiednio wysokiej jakości. Stolarz najprawdopodobniej będzie
potrzebował miarek, kilku pił, dobrego hebla, precyzyjnych dłut, wierteł i świdrów, młotków i imadeł. Sam wybór narzędzi sprawi rzemieślnikowi mnóstwo
radości. Każde narzędzie będzie z powodzeniem służyło do konkretnych czynności
(obszar działania nieznacznie będzie pokrywał się z innymi narzędziami). Prawdziwą sprawność wspomniane narzędzia osiągną dopiero w fachowych dłoniach
stolarza.
Właśnie dlatego po wyborze narzędzi następuje proces ich poznawania i dostosowywania do potrzeb. Każde narzędzie ma własną osobowość i dziwactwa,
zatem wymaga specjalnego traktowania. Każde narzędzie musi być ostrzone
(a często także trzymane) w niepowtarzalny sposób. Z czasem po każdym narzędziu widać pewne oznaki zużycia — na każdym uchwycie widać ślady dłoni stolarza. Powierzchnie tnące doskonale dopasowują się do kąta, pod którym
stolarz tnie drewno. Na tym etapie narzędzia zyskują status przedłużenia umysłu rzemieślnika w kontakcie z końcowym produktem — stanowią teraz tylko
rozszerzenia gołych dłoni stolarza. Z czasem stolarz uzupełnia swój zestaw
o nowe narzędzia, jak foremki do wycinania wzorów, ukośnice czy frezarki do
wczepów — wszystkie osiągnięcia współczesnej technologii. Nie ma jednak wątpliwości, że najwięcej radości będzie sprawiało stolarzowi korzystanie z oryginalnych narzędzi, kiedy będzie miał poczucie bezpośredniego obcowania
z drewnem.
Narzędzia pozwalają nam rozwinąć talent. Im lepsze są nasze narzędzia i im
lepiej potrafimy ich używać, tym większa może być nasza produktywność. Warto
zacząć od podstawowego zbioru uniwersalnych narzędzi. Kiedy nabierzemy
doświadczenia i kiedy dojdziemy do specjalnych wymagań, będziemy mogli rozważyć poszerzenie tego podstawowego zestawu. Tak jak rzemieślnicy, musimy
być przygotowani na systematyczne uzupełnianie swojego zestawu narzędzi.
4337ebf6db5c7cc89e4173803ef3875a
4
102 
Rozdział 3.
Podstawowe narzędzia
Musimy stale poszukiwać sposobów doskonalenia swojego warsztatu. Jeśli znajdziemy się w sytuacji, w której stwierdzimy, że nasze narzędzia nie wystarczą do
przecięcia jakiegoś elementu, koniecznie powinniśmy zanotować sobie konieczność poszukania mocniejszego narzędzia, które poradziłoby sobie z tym wyzwaniem. O dodaniu nowych narzędzi powinny decydować faktyczne potrzeby.
Wielu niedoświadczonych programistów popełnia błąd polegający na stosowaniu
od początku pojedynczego, bardzo rozbudowanego narzędzia, na przykład zintegrowanego środowiska wytwarzania (IDE), i bardzo szybko przyzwyczaja się do
jego wygodnego interfejsu. To naprawdę poważny błąd. Musimy opanować sztukę
radzenia sobie z narzędziami pozbawionymi udogodnień oferowanych przez środowiska IDE. Jedynym sposobem osiągnięcia tego celu jest skompletowanie
i utrzymywanie w gotowości odpowiedniego zestawu podstawowych narzędzi.
W tym rozdziale omówimy kwestie związane z inwestowaniem w taki podstawowy zestaw narzędzi. Jak w przypadku każdej dyskusji poświęconej narzędziom, zaczniemy (w podrozdziale „Potęga zwykłego tekstu”) od analizy naszych
podstawowych materiałów, swoistego surowca, oraz produktów, które chcemy
stworzyć. Zaraz potem zajmiemy się kwestią środowiska pracy, w tym przypadku
kwestią komputera. Jak możemy użyć swojego komputera do uzyskania możliwie najwyższej produktywności oferowanej przez stosowane narzędzia? Omówimy ten problem w podrozdziale „Powłoki”. Skoro dysponujemy już materiałem i odpowiednio przygotowanym miejscem pracy, możemy skoncentrować się
na narzędziu, którego prawdopodobnie będziemy używali częściej niż innych.
W podrozdziale „Efektywna edycja” zaproponujemy sposoby poprawy naszej
produktywności.
Aby wyeliminować ryzyko utraty cennych efektów pracy, zawsze powinniśmy
korzystać z systemu kontroli kodu źródłowego (patrz podrozdział „Kontrola
kodu źródłowego”) nawet dla takich zasobów jak osobista książka adresowa!
A ponieważ pan Murphy w rzeczywistości był optymistą, musimy mieć świadomość, że doskonałym programistą nie może zostać ktoś, kto nie opanował do
perfekcji diagnozowania kodu (patrz podrozdział „Debugowanie”).
Będziemy jeszcze potrzebowali jakiegoś spoiwa, które zwiąże wszystkie te magiczne
elementy. Pewne możliwości, w tym języki awk, Perl i Python, omówimy w podrozdziale „Operowanie na tekście”.
Wreszcie, najbardziej blady atrament jest lepszy niż najlepsza pamięć. Należy
prowadzić dziennik swoich przemyśleń oraz zapisywać w nim historię projektu.
Sposób prowadzenia takiego dziennika opisano w podrozdziale „Dzienniki inżynierskie”.
Wystarczy poświęcić trochę czasu na naukę technik korzystania z tych narzędzi,
aby pewnego dnia ze zdziwieniem odkryć, że nasze palce niemal automatycznie
naciskają właściwe klawisze, modyfikując tekst bez konieczności jakichkolwiek
świadomych przemyśleń. Dopiero wówczas narzędzia zyskują status przedłużeń
naszych rąk.
4337ebf6db5c7cc89e4173803ef3875a
4
Potęga zwykłego tekstu
16
36
 103
Potęga zwykłego tekstu
Dla pragmatycznego programisty podstawowym materiałem do obróbki jest nie
drewno czy żelazo, tylko wiedza. Gromadzimy wymagania w formie wiedzy, po
czym wyrażamy tę wiedzę pod postacią projektów, implementacji, testów i dokumentów. Wierzymy też, że najlepszym formatem trwałego przechowywania wiedzy jest zwykły tekst. Właśnie zwykły tekst umożliwia nam przetwarzanie
wiedzy (zarówno ręcznie, jak i programowo) przy użyciu niemal każdego dostępnego narzędzia.
Problem z większością formatów binarnych polega na tym, że kontekst niezbędny do zrozumienia danych jest oddzielony od samych danych. W sztuczny
sposób rozdzielamy dane od ich znaczenia. Równie dobrze dane można zaszyfrować. Bez logiki aplikacji pozwalającej przeanalizować dane są one całkowicie
niezrozumiałe. Natomiast za pomocą zwykłego tekstu można uzyskać samoopisujący się strumień danych, niezależny od aplikacji, w której dane zostały
stworzone.
Czym jest zwykły tekst?
Zwykły tekst składa się ze znaków drukowalnych w formie możliwej do odczytania i zrozumienia przez człowieka. Może to być coś tak prostego, jak lista
zakupów:
* mleko
* sałata
* kawa
lub tak złożonego, jak tekst źródłowy tej książki (tak, pisaliśmy go zwykłym tekstem, pomimo protestów redaktora, który chciał, żebyśmy używali
procesora tekstu).
Aspekt informacyjny jest bardzo ważny. Poniższy fragment nie jest przydatnym zwykłym tekstem:
hlj;uijn bfjxrrctvh jkni'pio6p7gu;vh bjxrdi5rgvhj
Podobnie niezrozumiały jest tekst:
Field19=467abe
Czytelnik nie ma pojęcia, co może oznaczać wartość 467abe. Dużo lepszym rozwiązaniem jest stosowanie zapisów zrozumiałych dla ludzi.
WSKAZÓWKA NR 25
Wiedzę należy zapisywać zwykłym tekstem.
4337ebf6db5c7cc89e4173803ef3875a
4
104 
Rozdział 3.
Podstawowe narzędzia
Potęga tekstu
Skoro użytkownicy rzadko wyrażają w swoich wymaganiach oczekiwanie otrzymania większych i wolniejszych rozwiązań, skąd pomysł, by utrudniać sobie
życie, stosując zwykły tekst? Jakie są korzyści stosowania zwykłego tekstu?

Trwałe bezpieczeństwo zamiast starzenia się.

Szerokie możliwości stosowania.

Łatwiejsze testowanie.
Trwałe bezpieczeństwo zamiast starzenia się
Formy danych czytelne dla ludzi, które nie wymagają dodatkowego opisu, okazują się dużo trwalsze od innych rodzajów danych i aplikacji, które je tworzą.
Kropka. Skoro cykl użycia danych jest dłuższy, będziemy mieli możliwość ich
użycia w przyszłości, być może na długo po rezygnacji z oryginalnej aplikacji,
która te dane wytworzyła.
Do przetwarzania tego rodzaju plików wystarczy szczątkowa znajomość ich formatu; z zupełnie inną sytuacją mamy do czynienia w przypadku plików binarnych, gdzie skuteczna analiza składniowa wymaga znajomości wszystkich szczegółów stosowanego formatu.
Przeanalizujmy przykład danych z pewnego przestarzałego systemu1. Naszym
zadaniem jest operowanie na jego danych. Nasza wiedza o oryginalnej aplikacji
jest dość ograniczona — z naszego punktu widzenia najważniejsza jest zapisana
przez ten system lista numerów ubezpieczenia społecznego (SSN) klientów, którą
musimy odczytać. Wśród zgromadzonych danych odnajdujemy następujące
elementy:
<FIELD10>123-45-6789</FIELD10>
...
<FIELD10>567-89-0123</FIELD10>
...
<FIELD10>901-23-4567</FIELD10>
Po rozpoznaniu charakterystycznego formatu numerów SSN możemy bez trudu
napisać prosty program wyodrębniający te dane, nawet jeśli nie dysponujemy
żadnymi informacjami o pozostałych wartościach zapisanych w tym pliku.
Wyobraźmy sobie teraz, że przetwarzany plik został sformatowany w następujący
sposób:
A.C.27123456789B11P
...
XY43567890123QTYL
...
6T2190123456788AM
1
Każde oprogramowanie staje się przestarzałe niemal natychmiast po napisaniu.
4337ebf6db5c7cc89e4173803ef3875a
4
Potęga zwykłego tekstu
 105
Tym razem rozpoznanie znaczenia tych liczb nie jest już takie proste. Właśnie
na tym polega różnica dzieląca tekst czytelny dla człowieka od tekstu zrozumiałego dla człowieka.
Warto przy tej okazji wspomnieć, że także łańcuch FIELD10 niewiele nam mówi.
Zapis w tej formie:
<SOCIAL-SECURITY-NO>123-45-6789</SOCIAL-SECURITY-NO>
spowodowałby, że zadanie identyfikacji numerów SSN byłoby dziecinnie proste.
Co więcej, mielibyśmy pewność, że dane w tej formie zachowają wartość dużo
dłużej niż jakikolwiek projekt, który je utworzył.
Filozofia systemu Unix
System operacyjny Unix jest znany z tego, że został zaprojektowany zgodnie z koncepcją licznych, niewielkich, szybkich narzędzi, z których każde ma realizować jedno zadanie. Realizacja tej filozofii jest możliwa dzięki stosowaniu wspólnego formatu danych — zwykłych plików tekstowych z podziałem na wiersze. Bazy danych
używane przez administratora systemu (a więc listy użytkowników i haseł, konfiguracja sieci itp.) bez wyjątku są zapisywane w zwykłych plikach tekstowych. (Niektóre
systemy dodatkowo utrzymują pewne bazy danych w formie binarnej, aby poprawić wydajność. W każdym takim przypadku wersja w postaci zwykłego tekstu pełni
funkcję swoistego interfejsu wersji binarnej).
W razie awarii systemu możemy stanąć przed koniecznością użycia minimalnego
środowiska do odtworzenia zapisanych zasobów (możemy na przykład stracić dostęp
do sterowników graficznych). W takich przypadkach zapewne docenimy prostotę
zwykłego tekstu.
Zwykły tekst jest również łatwiejszy do przeszukiwania. Jeśli nie możesz zapamiętać,
który plik konfiguracyjny jest odpowiedzialny za kopie zapasowe systemu, wystarczy,
że wpiszesz polecenie grep -r backup /etc, żeby się tego dowiedzieć.
Szerokie możliwości stosowania
Praktycznie każde narzędzie w świecie przetwarzania komputerowego, od systemów zarządzania kodem źródłowym, przez środowiska kompilatorów, po edytory
i autonomiczne filtry, może operować na zwykłym tekście.
Przypuśćmy na przykład, że dysponujemy wdrożeniem produkcyjnym obejmującym jakąś wielką aplikację ze skomplikowanym plikiem konfiguracyjnym w formacie właściwym tylko tej aplikacji (warto przypomnieć sobie choćby przykład
programu sendmail). Jeśli taki plik zawiera zwykły tekst, możemy umieścić go
w systemie kontroli kodu źródłowego (patrz temat 19., „Kontrola kodu źródłowego”), tak aby pełna historia zmian była utrzymywana automatycznie. Narzędzia
do porównywania plików, jak diff czy fc, umożliwiają nam błyskawiczne
identyfikowanie wprowadzonych zmian, natomiast narzędzie sum umożliwia
nam generowanie sum kontrolnych z myślą o monitorowaniu pliku pod kątem
przypadkowych lub złośliwych modyfikacji.
4337ebf6db5c7cc89e4173803ef3875a
4
106 
Rozdział 3.
Podstawowe narzędzia
Łatwiejsze testowanie
Jeśli zwykły tekst jest używany do tworzenia danych syntetycznych na potrzeby
testów systemu, zmiany scenariuszy testowych będą sprowadzały się do dodawania, aktualizowania lub modyfikowania tych danych testowych (bez konieczności tworzenia jakichkolwiek specjalnych narzędzi). Podobnie, dane wynikowe
w formie zwykłego tekstu generowane przez testy regresyjne można bez trudu
analizować (na przykład za pomocą polecenia diff) bądź przekazywać do dalszego
przetwarzania przez polecenia powłoki lub proste skrypty.
Najmniejszy wspólny mianownik
Nawet w przyszłych inteligentnych agentach operujących na danych w formacie
XML i samodzielnie przemierzających dziki i niebezpieczny świat internetu (negocjując przy tym wymianę danych między sobą) zwykłe pliki tekstowe zachowają swoją ważną pozycję. W praktyce w środowiskach heterogenicznych zalety
zwykłego tekstu nierzadko przewyższają wady tego formatu. Musimy zagwarantować możliwość wzajemnego komunikowania się przez składniki tych środowisk przy użyciu jednego wspólnego standardu. Takim standardem jest właśnie
zwykły tekst.
Pokrewne podrozdziały

Temat 17., „Powłoki”.

Temat 21., „Operowanie na tekście”.

Temat 32., „Konfiguracja”.
Wyzwania

Zaprojektuj bazę danych niewielkiej książki adresowej (obejmującej nazwisko, numer telefonu itp.), stosując prostą reprezentację binarną dostępną
w stosowanym języku programowania. Zrób to przed zapoznaniem się
z dalszą częścią tego zadania.
1. Przetłumacz ten format na zwykły tekst, stosując format XML lub
JSON.
2. Dla obu wersji dodaj nowe pole zmiennej długości nazwane „kierunek”,
w którym będą umieszczane wskazówki, jak dojechać do domu danej
osoby.
Jakie problemy można napotkać w kontekście zarządzania wersjami
i rozszerzalności? Który format był łatwiejszy do zmodyfikowania? Jak
przebiegała konwersja istniejących danych?
4337ebf6db5c7cc89e4173803ef3875a
4
Powłoki
17
37
 107
Powłoki
Każdy stolarz potrzebuje dobrego, solidnego blatu, na którym będzie mógł
trzymać tworzone przedmioty na wygodnej dla siebie wysokości. Stół do pracy
staje się centralnym elementem warsztatu stolarskiego. Rzemieślnik wielokrotnie wraca w to miejsce podczas nadawania właściwego kształtu swojemu dziełu.
Z perspektywy programisty operującego na plikach tekstowych funkcję stołu
pełni powłoka poleceń. Właśnie z poziomu wiersza poleceń możemy wywoływać
narzędzia z naszego zestawu, stosując przy tym potoki umożliwiające łączenie
narzędzi w sekwencje, które nawet nie śniły się twórcom tych narzędzi. Za
pośrednictwem powłoki możemy uruchamiać aplikacje, debugery, przeglądarki,
edytory i inne narzędzia użytkowe. Możemy szukać plików, wykonywać zapytania dotyczące statusu systemu i filtrować uzyskiwane dane wynikowe. Co
więcej, programując powłokę, możemy konstruować makra złożone z wielu
poleceń, aby ułatwić wielokrotne wykonywanie tych sekwencji.
Programistom, którzy dorastali w otoczeniu graficznych interfejsów użytkownika
(GUI) i zintegrowanych środowisk programisty (IDE), proponowane rozwiązania
mogą wydać się ekstremalne. Czy nie można by tych wszystkich czynności wykonać równie łatwo, wybierając i klikając odpowiednie opcje za pomocą myszy?
Odpowiedź jest prosta: nie. Graficzne interfejsy użytkownika są wspaniałe
i rzeczywiście bywają szybsze i bardziej wygodne podczas wykonywania pewnych prostych czynności. Przenoszenie plików, odczytywanie wiadomości poczty
elektronicznej z kodowaniem MIME czy pisanie listów to typowe operacje, które
każdy użytkownik woli wykonywać w środowisku graficznym. Jeśli jednak
wykonujemy całą pracę za pomocą graficznych interfejsów użytkownika, tracimy z oczu pełen potencjał środowiska, z którego korzystamy. Nie będziemy
mogli automatyzować typowych czynności ani korzystać ze wszystkich funkcji
dostępnych narzędzi. Co więcej, nie będziemy mogli łączyć tych narzędzi, tworząc własne, niestandardowe makra. Niewątpliwą zaletą graficznych interfejsów
użytkownika jest model WYSIWYG (od ang. what you see is what you get) —
to, co widzisz, to to, co otrzymasz. Wadą tych interfejsów jest zasada WYSIAYG
(od ang. what you see is all you get) — to, co widzisz, to wszystko, co otrzymasz.
Możliwości środowisk z graficznym interfejsem użytkownika zwykle są ograniczone do tego, co zaplanowali ich twórcy. Jeśli z jakiegoś powodu musimy wyjść
poza model opracowany przez projektanta takiego środowiska, najczęściej
musimy poszukać innych rozwiązań (w takich przypadkach rezygnacja z rozszerzenia ograniczonego modelu rzadko jest możliwa). Pragmatyczni programiści nie ograniczają się do budowy kodu, projektowania modeli obiektowych,
pisania dokumentacji czy automatyzacji procesu kompilacji — musimy wykonywać wszystkie te czynności. Potencjał każdego narzędzia zwykle jest ograniczony do zadań, które to narzędzie miało wykonywać w założeniu swoich
twórców. Przypuśćmy na przykład, że musimy zintegrować jakiś preprocesor
kodu (z myślą o implementacji paradygmatu projektowania kontraktowego,
4337ebf6db5c7cc89e4173803ef3875a
4
108 
Rozdział 3.
Podstawowe narzędzia
przetwarzania wieloprocesorowego itp.) z naszym środowiskiem IDE. Jeśli projektant środowiska IDE wprost nie udostępnił punktów dołączania tego rodzaju
mechanizmów, nasze możliwości są bardzo niewielkie.
WSKAZÓWKA NR 26
Należy korzystać z potencjału poleceń powłoki.
Wystarczy dobrze opanować umiejętność korzystania z powłoki, a błyskawicznie zauważymy wzrost produktywności. Musimy stworzyć listę unikatowych
nazw pakietów bezpośrednio importowanych przez nasz kod Javy? Poniższe
polecenie zapisuje odpowiednią listę w pliku nazwanym list:
sh/packages.sh
grep '^import ' *.java |
sed -e's/.*import *//' -e's/;.*$//' |
sort -u >list
Jeśli nie poświęciliśmy dość dużo czasu na analizę możliwości powłoki poleceń
systemu, w którym pracujemy, jej obsługa może wydawać nam się zniechęcająca. Wystarczy zainwestować trochę energii w lepsze poznanie tej powłoki,
a szybko odkryjemy, że wszystko działa wyjątkowo sprawnie. Warto poeksperymentować z powłoką poleceń — po pewnym czasie czytelnik będzie zaskoczony osiągniętym wzrostem produktywności.
Twoja własna powłoka
Tak samo, jak stolarz dostosowuje swoje miejsce pracy do własnych potrzeb,
tak programista powinien dostosować swoją powłokę. Zazwyczaj obejmuje to
również zmianę konfiguracji używanego programu terminala.
Oto często wykonywane zmiany:

Ustawianie motywów kolorów. Wypróbowanie każdego motywu, który jest
dostępny online dla konkretnej powłoki, może zająć bardzo wiele godzin.

Konfigurowanie „symbolu zachęty”. Symbol zachęty (ang. prompt), który
informuje, że jesteś gotowy do wpisania komendy, można skonfigurować
w taki sposób, aby wyświetlał dowolne informacje, które są Ci potrzebne
(i różne rzeczy, które nie są potrzebne). Wszystko zależy od osobistych preferencji: my preferujemy proste symbole zachęty ze skróconą nazwą bieżącego katalogu oraz stanem systemu kontroli wersji i godziną.

Aliasy i funkcje powłoki. Warto uprościć swój przepływ pracy przez przekształcenie często używanych komend na proste aliasy. Być może regularnie aktualizujesz Linuksa, ale nie pamiętasz, czy najpierw korzystasz
z polecenia update, a potem upgrade, czy odwrotnie. Stwórz alias:
alias apt-up='sudo apt-get update && sudo apt-get upgrade'
4337ebf6db5c7cc89e4173803ef3875a
4
Efektywna edycja
 109
Być może zdarzyło Ci się przypadkowo usunąć pliki za pomocą polecenia
rm o jeden raz za dużo. Napisz alias, aby w przyszłości próba uruchomienia
polecenia rm zawsze powodowała wyświetlenie pytania o potwierdzenie:
alias rm ='rm -iv'

Uzupełnianie poleceń. Większość powłok automatycznie uzupełnia nazwy
komend i plików: wpisz kilka pierwszych znaków, wciśnij Tab, a powłoka
uzupełni nazwę. Ale ten mechanizm można wykorzystać znacznie bardziej.
Możesz skonfigurować powłokę tak, aby rozpoznawała wpisywane polecenia i uzupełniała nazwy w zależności od kontekstu. Niektórzy nawet
dostosowują uzupełnianie nazw w zależności od bieżącego katalogu.
Z powłoką będziesz pracować przez znaczną część czasu. Bądź jak krab i uczyń
ją swoim domem.
Pokrewne podrozdziały

Temat 13., „Prototypy i karteczki samoprzylepne”.

Temat 16., „Potęga zwykłego tekstu”.

Temat 21., „Operowanie na tekście”.

Temat 30., „Programowanie transformacyjne”.

Temat 51., „Zestaw startowy pragmatyka”.
Wyzwania
18
38

Czy aktualnie wykonujesz jakieś czynności ręcznie za pośrednictwem graficznego interfejsu użytkownika? Czy kiedykolwiek opisywałeś znajomym
procedury obejmujące wiele kroków typu „kliknij ten przycisk”, „zaznacz
tę pozycję” itp.? Czy można było zautomatyzować te procedury?

Za każdym razem, kiedy zaczynasz pracę w nowym środowisku, koniecznie sprawdź, jakie powłoki masz do dyspozycji. Warto wiedzieć, czy można
przenieść do tego środowiska także dotychczas stosowaną powłokę.

Sprawdź alternatywne rozwiązania dla aktualnie używanej powłoki. W razie
znalezienia problemu, którego nie można rozwiązać przy użyciu bieżącej
powłoki, sprawdź, czy alternatywna powłoka nie poradziłaby sobie z tym
problemem lepiej.
Efektywna edycja
Wspomnieliśmy wcześniej o narzędziach jako o przedłużeniu naszych rąk. Takie
postrzeganie narzędzi pasuje do edytorów bardziej niż do jakichkolwiek innych
narzędzi programowych. Musimy mieć możliwość możliwie wygodnej pracy
z tekstem, ponieważ tekst jest dla programisty podstawowym surowcem.
4337ebf6db5c7cc89e4173803ef3875a
4
110 
Rozdział 3.
Podstawowe narzędzia
W pierwszym wydaniu tej książki zalecaliśmy używanie jednego edytora do
wszystkiego: kodu, dokumentacji, notatek, administrowania systemem i tak
dalej. W tym wydaniu złagodziliśmy trochę to stanowisko. Można korzystać
z dowolnej liczby edytorów. Zalecamy jednak, by dążyć do uzyskania płynności
każdego z nich.
WSKAZÓWKA NR 27
Postaraj się o uzyskanie płynności edytora.
Dlaczego to jest takie ważne? Czy mówiliśmy już, że pozwala to zaoszczędzić
dużo czasu? Właściwie tak: jeśli zwiększysz wydajność swojego edytora tylko
o 4%, a korzystasz z niego przez 20 godzin tygodniowo, to w ciągu roku możesz
zyskać dodatkowy tydzień.
Ale to nie na tym polega prawdziwa korzyść. Największy zysk z zapewnienia
płynności edytora polega na tym, że gdy uda Ci się ją osiągnąć, nie będziesz
zmuszony myśleć o mechanice edycji. Dystans pomiędzy chwilą, gdy o czymś
pomyślisz, a momentem, kiedy to znajdzie się w buforze edytora, znacznie się
skróci. Twoje myśli będą płynąć do edytora, co wpłynie korzystnie na skuteczność
Twojego programowania (jeśli kiedykolwiek uczyłeś kogoś prowadzenia pojazdów, z pewnością rozumiesz różnicę pomiędzy osobą, która musi myśleć o każdej wykonywanej czynności, a bardziej doświadczonym kierowcą, który kontroluje samochód instynktownie).
Co to znaczy „płynny”?
Co wpływa na „płynność” edytora? Oto lista cech:

Możliwość zaznaczania w tekście pojedynczych znaków, słów, wierszy
i akapitów.

Poruszanie się po kodzie z wykorzystaniem różnych jednostek składniowych
(pasujących separatorów, funkcji, modułów).

Modyfikowanie wcięć w kodzie po wprowadzeniu zmian.

Komentowanie bloków kodu i usuwanie znaczników komentarza za pomocą
jednego polecenia.

Cofanie zmian i powtarzanie wykonanych operacji.

Podział okna edytora na kilka paneli i poruszanie się pomiędzy nimi.

Przechodzenie do wiersza o określonym numerze.

Sortowanie zaznaczonych wierszy.

Wyszukiwanie zarówno ciągów znaków, jak i wyrażeń regularnych, oraz
powtarzanie poprzednich operacji wyszukiwania.

Tymczasowe utworzenie wielu kursorów na podstawie zaznaczenia lub
dopasowania wzorców i równoległe edytowanie zawartego w nich tekstu.
4337ebf6db5c7cc89e4173803ef3875a
4
Efektywna edycja

Wyświetlanie błędów kompilacji w bieżącym projekcie.

Uruchamianie testów w bieżącym projekcie.
 111
Czy możesz wykonać wyżej wymienione działania bez użycia myszki (trackpada)?
Być może Twój obecny edytor nie pozwala na wykonanie niektórych z tych
operacji. A może nadszedł czas, by go zmienić?
Zmierzanie w kierunku płynności
Wątpimy, by istniało wiele osób, które znają wszystkie polecenia w rozbudowanym edytorze. Od Ciebie też tego nie oczekujemy. Zamiast tego sugerujemy
bardziej pragmatyczne podejście: naucz się poleceń, które ułatwią Ci życie.
Przepis na to jest dość prosty.
Po pierwsze obserwuj, co robisz podczas edycji. Za każdym razem, gdy powtarzasz jakieś działania, spróbuj pomyśleć: „Musi istnieć lepszy sposób”. Następnie postaraj się ten sposób znaleźć.
Kiedy odkryjesz nową, przydatną funkcję, musisz zainstalować ją w swojej
„pamięci mięśniowej” tak, abyś mógł z niej skorzystać bez zastanowienia. Jedynym sposobem, aby to osiągnąć, jest powtarzanie. Świadomie szukaj możliwości
korzystania z nowego udogodnienia — najlepiej wiele razy dziennie. Mniej
więcej po tygodniu będziesz korzystać z nowej funkcji bez zastanowienia.
Rozbudowa edytora
Większość zaawansowanych edytorów kodu jest zbudowanych wokół podstawowego rdzenia, który jest następnie rozbudowywany przez rozszerzenia. Wiele
z nich jest dostarczanych razem z edytorem, natomiast inne można dodawać
później.
Kiedy napotkasz jakieś widoczne ograniczenie edytora, którego używasz, postaraj się znaleźć odpowiednie rozszerzenie. Są szanse, że nie tylko Ty potrzebujesz nowej funkcji. Jeśli masz szczęście, być może ktoś inny opublikował
odpowiednie rozwiązanie.
Nie poprzestawaj na tym. Przestudiuj język rozszerzeń edytora. Dowiedz się, jak
z niego korzystać w celu zautomatyzowania niektórych wykonywanych przez
Ciebie powtarzających się zadań. Często wystarczy jeden lub dwa wiersze kodu.
Czasami można pójść jeszcze dalej i napisać rozbudowane rozszerzenie. Jeśli to
zrobisz, opublikuj je. Jeśli było potrzebne Tobie, inni także z niego skorzystają.
Pokrewne podrozdziały

Temat 7., „Komunikuj się!”.
4337ebf6db5c7cc89e4173803ef3875a
4
112 
Rozdział 3.
Podstawowe narzędzia
Wyzwania

Nigdy więcej autopowtarzania.
Każdy to robi: musisz usunąć ostatnie wpisane słowo, więc naciskasz klawisz Backspace i czekasz, aż uaktywni się mechanizm autopowtarzania.
Możemy się założyć, że Twój mózg wykonywał tę czynność tak wiele razy,
że dokładnie wiesz, kiedy możesz zwolnić klawisz.
Wyłącz autopowtarzanie. Zamiast z niego korzystać, naucz się kombinacji
klawiszy do przemieszczania się po tekście, zaznaczania i usuwania tekstu
według znaków, słów, wierszy i bloków.

Ta czynność może być uciążliwa.
Przestań korzystać z myszy (trackpada). Przez cały tydzień korzystaj
z edytora wyłącznie za pomocą klawiatury. Odkryjesz wiele operacji, które
możesz wykonać bez wskazywania i klikania, więc nadszedł czas, aby
się ich nauczyć. Rób notatki (zalecamy starą szkołę z użyciem ołówka
i papieru) — zapisuj kombinacje klawiszy, których się uczysz.
W ciągu kilku dni zwiększysz produktywność. Kiedy nauczysz się wykonywać działania bez przemieszczania rąk z pozycji wyjściowej, przekonasz
się, że edytujesz szybciej i płynniej niż kiedykolwiek wcześniej.
19
39

Szukaj integracji. Podczas pisania tego rozdziału Dave zastanawiał się,
czy można podejrzeć ostateczny układ dokumentu (plik PDF) w buforze
edytora. Wystarczyło pobrać odpowiednie rozszerzenie, aby móc oglądać
w edytorze oryginalny tekst razem z docelowym układem. Sporządź listę
udogodnień, które chciałbyś wprowadzić w swoim edytorze, a następnie
ich poszukaj.

Coś bardziej ambitnego? Jeśli nie potrafisz znaleźć wtyczki lub rozszerzenia, które wykonują potrzebną operację, napisz je samodzielnie. Andy
jest autorem wtyczki do Wiki dla swoich ulubionych edytorów. Jeśli nie
możesz czegoś znaleźć, zbuduj to.
Kontrola kodu źródłowego
Postęp jest uzależniony od tego, co trwałe, nie od zmian.
Ten, kto nie pamięta przeszłości, jest skazany na jej wielokrotne
powtarzanie.
George Santayana, Life of Reason
Jedną z najważniejszych funkcji, której powinniśmy szukać w interfejsie użytkownika, jest przycisk Cofnij — pojedynczy przycisk służący do wybaczania
naszych błędów. Jeszcze cenniejsza jest obsługa operacji wycofywania i powtarzania zmian na wielu poziomach, tak aby można było cofać i przywracać operacje, które miały miejsce na przykład przed kilkoma minutami.
4337ebf6db5c7cc89e4173803ef3875a
4
Kontrola kodu źródłowego
 113
Co będzie, jeśli błąd miał miejsce tydzień temu i jeśli od tego czasu wyłączyliśmy
i włączyliśmy komputer dziesięć razy? To jedna z wielu zalet stosowania
systemu kontroli wersji kodu źródłowego (ang. Version Control System — VCS) —
wielki klawisz Cofnij, czyli swoisty wehikuł czasu dla całego projektu, który
może nas cofnąć do pradawnych dni z zeszłego tygodnia, kiedy skompilowaliśmy
i uruchomiliśmy nasz kod.
Dla wielu osób na tym kończą się zastosowania systemów VCS. Tacy ludzie
nie korzystają z większego środowiska współpracy, potoków wdrażania, śledzenia problemów i — ogólnie rzecz biorąc — interakcji w zespołach.
Spróbujmy zatem przyjrzeć się systemom VCS. Najpierw w roli repozytorium
zmian, a następnie jako centralnemu miejscu spotkań dla członków Twojego
zespołu i tworzonego przez nich kodu.
Współdzielone katalogi to NIE jest system kontroli kodu źródłowego
Nadal spotykamy zespoły, które udostępniają pliki źródłowe swojego projektu
w sieci: wewnętrznie lub za pomocą jakiegoś magazynu w chmurze obliczeniowej.
To nie jest dobry sposób.
Zespoły, które to robią, ciągle napotykają na problemy związane z „wchodzeniem
sobie w drogę” — powoduje to mieszanie się zmian wprowadzanych przez różnych
członków zespołu, utratę zmian, wprowadzanie błędów i walki na pięści na parkingu.
To tak, jak pisanie współbieżnego kodu ze współdzielonymi danymi bez mechanizmów
synchronizacji. Zamiast tego należy użyć systemu kontroli wersji.
Ale to nie wszystko! Niektórzy ludzie używają systemu kontroli wersji, ale przechowują główne repozytorium na dysku sieciowym lub w chmurze. Uważają, że to
najlepsze połączenie obu światów: ich pliki są dostępne w dowolnym miejscu
i (w przypadku przechowywania w chmurze) są archiwizowane na zewnątrz.
Okazuje się, że to jest jeszcze gorszy sposób od współdzielenia katalogów, bo stwarza
ryzyko utraty wszystkiego. Oprogramowanie systemów VCS korzysta ze zbioru oddziałujących ze sobą plików i katalogów. Wprowadzenie zmian w dwóch egzemplarzach
jednocześnie może doprowadzić do uszkodzenia ogólnego stanu systemu i nie ma
sposobu na stwierdzenie, jak wielkie będą szkody. A nikt przecież nie lubi widoku
płaczących programistów.
Wszystko zaczyna się od kodu źródłowego
Systemy kontroli kodu źródłowego pozwalają śledzić każdą zmianę wprowadzaną
w kodzie źródłowym i dokumentacji. Jeśli system kontroli wersji jest poprawnie skonfigurowany, zawsze można wrócić do poprzedniej wersji oprogramowania.
Możliwości systemu kontroli kodu źródłowego nie ograniczają się jednak tylko
do wycofywania pomyłek. Dobry system VCS umożliwia śledzenie zmian i odpowiada na następujące pytania: Kto wprowadził zmiany w tym wierszu kodu?
4337ebf6db5c7cc89e4173803ef3875a
4
114 
Rozdział 3.
Podstawowe narzędzia
Jaka jest różnica pomiędzy bieżącą wersją a wersją sprzed tygodnia? Ile wierszy kodu zostało zmienionych w bieżącym wydaniu? Które pliki są zmieniane
najczęściej? Tego rodzaju informacje są bezcenne w kontekście śledzenia błędów,
prowadzenia audytów, badania wydajności i sprawdzania jakości.
System VCS umożliwia też identyfikację wydań naszego oprogramowania. Po
zidentyfikowaniu wydania zawsze będziemy mogli do niego wracać i ponownie
je generować (niezależnie od zmian, które wprowadzono do tworzonego oprogramowania po tym wydaniu).
Systemy kontroli kodu źródłowego mogą śledzić pliki utrzymywane w centralnym repozytorium — takie repozytorium jest wprost doskonałym kandydatem do archiwizacji.
I wreszcie, systemy VCS oferują możliwość równoczesnej pracy wielu użytkowników na tym samym zbiorze plików, a nawet wprowadzanie równoległych
zmian w tych samych plikach. Systemy te zarządzają następnie scalaniem tych
zmian w momencie odsyłania plików do repozytorium. Chociaż opisany mechanizm sprawia wrażenie dość ryzykownego, wspomniane systemy zadziwiająco
dobrze sprawdzają się w praktyce niezależnie od rozmiaru projektu.
WSKAZÓWKA NR 28
Zawsze należy stosować system kontroli kodu źródłowego.
Zawsze. Nawet jeśli stanowimy jednoosobowy zespół i jeśli cały projekt zajmuje
zaledwie tydzień. Nawet jeśli pracujemy nad prototypem przeznaczonym do
wyrzucenia. Nawet jeśli nie pracujemy nad kodem źródłowym. Kontroli kodu
źródłowego powinno podlegać dosłownie wszystko — dokumentacja, listy numerów telefonów, notatki dla producentów, pliki Makefile, procedury kompilacji
i wydań, a nawet prosty skrypt odpowiedzialny za porządkowanie logów — słowem, wszystko. Sami odruchowo korzystamy z systemu kontroli kodu źródłowego, kiedy tylko planujemy coś napisać (w tym tekst tej książki). Nawet kiedy
nie pracujemy nad konkretnym projektem, efekty naszej codziennej pracy są
zabezpieczane w repozytorium.
Gałęzie kodu
Działanie systemów kontroli wersji nie ogranicza się do utrzymywania pojedynczej historii Twojego projektu. Jedną z ich najpotężniejszych i najbardziej
przydatnych funkcji jest możliwość wyizolowania „wysp wytwarzania oprogramowania” do postaci konstrukcji nazywanych gałęziami (ang. branches).
Możesz utworzyć gałąź w dowolnym momencie w historii projektu, a wszelkie
prace wykonane w tej gałęzi będą odizolowane od wszystkich innych. W pewnym
momencie w przyszłości możesz scalić gałąź, nad którą pracujesz, z inną gałęzią,
4337ebf6db5c7cc89e4173803ef3875a
4
Kontrola kodu źródłowego
 115
dzięki czemu gałąź docelowa będzie zawierać zmiany wprowadzone w Twojej.
Nad jedną gałęzią może nawet pracować wiele osób: w pewnym sensie gałęzie
są jak małe klony projektów.
Jedną z korzyści stosowania gałęzi jest izolacja kodu. Jeśli pracujesz nad
funkcją A w jednej gałęzi, a kolega z zespołu pracuje nad funkcją B w innej, nie
będziecie sobie wzajemnie przeszkadzać.
Drugą zaletą, która może być zaskakująca, jest to, że gałęzie często stanowią
sedno przepływu pracy w zespole.
Tutaj sprawy nieco się komplikują. Stosowanie gałęzi kodu źródłowego ma wiele
wspólnego z organizacją testów. W obu przypadkach mamy do czynienia
z wieloma osobami, które mówią nam, w jaki sposób należy to robić. Te rady
w dużej mierze są bez znaczenia, ponieważ w istocie osoby te mówią „U mnie
się to sprawdziło”.
Zatem korzystaj z systemu kontroli wersji w projekcie, a kiedy natkniesz się
na problemy związane z przepływem pracy, poszukaj możliwych rozwiązań.
I pamiętaj, żeby po zdobyciu doświadczeń dokonać przeglądu i dostosować
sposób swojego postępowania.
Eksperyment dający do myślenia
Rozlej filiżankę herbaty (zwykłą angielską, z odrobiną mleka) na klawiaturze swojego
laptopa. Zanieś maszynę do serwisu i posłuchaj, co jego pracownik o Tobie myśli.
Kup nowy komputer. Zabierz go do domu.
Ile czasu zajmie Ci doprowadzenie maszyny do stanu sprzed zalania (ze wszystkimi
kluczami SSH, konfiguracją edytora, konfiguracją powłoki, zainstalowanymi aplikacjami i tak dalej)? Taki problem niedawno dotknął jednego z nas.
Prawie wszystko, co definiowało konfigurację i korzystanie z maszyny, było przechowywane w systemie kontroli wersji. Między innymi były w nim:





Wszystkie ustawienia użytkownika i pliki konfiguracyjne.
Konfiguracja edytora.
Lista oprogramowania zainstalowanego za pomocą menedżera Homebrew.
Skrypt Ansible używany do skonfigurowania aplikacji.
Wszystkie bieżące projekty.
Maszynę udało się przywrócić do poprzedniego stanu jeszcze tego samego popołudnia.
4337ebf6db5c7cc89e4173803ef3875a
4
116 
Rozdział 3.
Podstawowe narzędzia
System kontroli wersji jako centralna część
projektu
Chociaż systemy VCS są niezwykle przydatne w projektach jednoosobowych,
ich prawdziwa potęga ujawnia się podczas pracy zespołowej. Duża część tej wartości wynika ze sposobu przechowywania repozytorium.
Obecnie wiele systemów kontroli wersji nie wymaga żadnego hostingu. Są one
całkowicie zdecentralizowane, a wszyscy programiści współpracują ze sobą
na zasadach partnerskich. Ale nawet w przypadku systemów niewymagających
hostingu warto zastanowić się nad skorzystaniem z centralnego repozytorium.
Pozwala to na korzystanie z wielu integracji oraz usprawnia przepływy pracy
w projekcie.
Wiele systemów repozytorium to systemy open source, zatem nie ma przeszkód,
aby je zainstalować i uruchomić we własnej firmie. Utrzymywanie systemu
kontroli wersji to jednak nie jest Twój biznes, zatem zalecamy, aby korzystać
z oferty firm zewnętrznych. Dzięki temu możemy uzyskać następujące korzyści:

Dobre zabezpieczenia i kontrola dostępu.

Intuicyjny interfejs użytkownika.

Możliwość wykonywania operacji z poziomu wiersza poleceń (co ułatwia
automatyzację).

Automatyczne kompilacje i testy.

Ułatwienia w scalaniu gałęzi (tzw. operacje żądania ściągnięcia — ang. pull
requests).

Zarządzanie błędami (często zintegrowane z commitami i scaleniami) — co
pozwala utrzymywać metryki.

Dobre mechanizmy raportowania (widok w postaci tablicy Kanban nierozstrzygniętych problemów i zadań może być bardzo przydatny).

Dobra komunikacja w zespole: e-maile lub inne powiadomienia o zmianach,
wiki i tak dalej.
Wiele zespołów konfiguruje używane systemy VCS w taki sposób, że przesłanie
kodu do konkretnej gałęzi inicjuje automatyczne budowanie systemu, uruchomienie testów oraz — w przypadku ich powodzenia — wdrożenie nowego kodu
do produkcji.
Brzmi groźnie? Nie wtedy, gdy korzystasz z systemu kontroli wersji. Zawsze
możesz się cofnąć.
Pokrewne podrozdziały

Temat 11., „Odwracalność”.

Temat 49., „Pragmatyczne zespoły”.

Temat 51., „Zestaw startowy pragmatyka”.
4337ebf6db5c7cc89e4173803ef3875a
4
Debugowanie
 117
Wyzwania

Świadomość, że za pomocą systemu VCS można przywrócić kod do dowolnego poprzedniego stanu to jedno, ale czy rzeczywiście potrafisz to zrobić? Czy znasz polecenia pozwalające na prawidłowe wykonanie tej czynności? Naucz się ich teraz, a nie wtedy, gdy nastąpi awaria i będziesz
pod presją.

Poświęć trochę czasu na przemyślenie sposobu na odzyskanie środowiska
swojego laptopa w przypadku awarii. Co trzeba będzie odtworzyć? Wiele
elementów, których potrzebujesz, to zwykłe pliki tekstowe. Jeśli nie są
one przechowywane w VCS (są zapisane tylko na laptopie), znajdź sposób,
aby dodać je do VCS. Następnie pomyśl o innych elementach: zainstalowanych aplikacjach, konfiguracji systemu i tak dalej. W jaki sposób
można zapisać reprezentacje wszystkich tych komponentów w plikach
tekstowych, tak aby można było je przechowywać w systemie VCS?
Ciekawym eksperymentem, kiedy już zrobisz pewien postęp, jest znalezienie starego komputera, którego już nie używasz, i sprawdzenie, czy
możesz wykorzystać nowy system do jego skonfigurowania.

Świadomie przetestuj funkcje systemu VCS, z którego aktualnie korzystasz,
oraz systemu oferowanego przez dostawcę, z którego oferty nie korzystasz.
Jeśli Twój zespół nie korzysta z gałęzi, zrób eksperyment z ich wprowadzeniem. Zrób to samo dla funkcji żądań ściągnięcia (scalenia); ciągłej integracji; potoków budowania. Spróbuj zastosować system ciągłego wdrażania. Przyjrzyj się również narzędziom komunikacji w zespole: tablicom
Kanban, wiki itp.
Nie trzeba korzystać z nich wszystkich. Ale warto wiedzieć, do czego one
służą, by móc świadomie podjąć decyzję o ich wykorzystaniu.

40
20
Korzystaj z systemu VCS także w odniesieniu do elementów, które nie są
częścią projektu.
Debugowanie
To wielki ból,
Widzieć twoją straszną dolę
I wiedzieć, że jest ona wyłącznie twoim dziełem.
Sofokles, Ajas
Angielskie słowo bug jest używane w znaczeniu „czegoś przerażającego” już od
czternastego wieku. Kontradmirał dr. Grace Hopper, twórczyni języka COBOL,
przypisuje się odkrycie pierwszego błędu (robaka) komputerowego, a konkretnie ćmy w pierwszym systemie komputerowym. Technik zapytany o przyczyny
działania komputera niezgodnie z oczekiwaniami odpowiedział, że w systemie
pojawił się robak, po czym karnie wkleił jego szczątki do prowadzonego dziennika.
4337ebf6db5c7cc89e4173803ef3875a
4
118 
Rozdział 3.
Podstawowe narzędzia
Okazuje się, że na tym przypadku nie zakończyło się występowanie „robaków”
w systemach komputerowych, tyle że teraz nie są to stworzenia latające.
Okazuje się jednak, że czternastowieczne znaczenie tego słowa (odnoszące się
do strachu) jest dzisiaj dużo bardziej uzasadnione niż wówczas. Usterki w oprogramowaniu ujawniają się na rozmaite sposoby, od błędów powodowanych
niewłaściwym rozumieniem wymagań po błędy popełnione na etapie kodowania. Okazuje się, że współczesne systemy komputerowe wciąż oferują dość
ograniczone możliwości realizacji tego, co im zlecamy, i nie zawsze robią to
w sposób, którego sobie życzymy.
Nikt nie pisze doskonałego oprogramowania, zatem musimy pogodzić się z tym,
że błędy będą miały spory udział w naszym czasie pracy. Przeanalizujmy teraz
wybrane problemy związane z diagnozowaniem i usuwaniem błędów oraz kilka
strategii odnajdywania trudnych do uchwycenia błędów.
Psychologia debugowania oprogramowania
Debugowanie oprogramowania dla wielu programistów jest tematem dość
drażliwym i budzącym niemałe emocje. Zamiast traktować tę czynność jak układanki, łamigłówki, wielu programistów wypiera się pomyłek, zrzuca winę na
współpracowników, posługuje się żałosnymi wymówkami lub po prostu popada
w apatię.
Warto więc przyzwyczaić się do myśli, że diagnozowanie to tylko rozwiązywanie
problemów i jako takie wymaga stosownego nastawienia.
Po znalezieniu cudzego błędu możemy, oczywiście, tracić czas i energię na
potępianie winowajcy. W niektórych obszarach taki model jest częścią kultury
i bywa oczyszczający. W świecie nowoczesnych technologii wolelibyśmy jednak
koncentrować się na eliminowaniu problemów, nie na szukaniu winnych.
WSKAZÓWKA NR 29
Należy rozwiązywać problemy, nie szukać winowajcy.
Nie ma większego znaczenia to, czy błąd wynika z naszego przeoczenia, czy
jest spowodowany cudzym niedopatrzeniem. To wciąż nasz problem.
Właściwa postawa
Najłatwiej okłamywać samego siebie
Edward Bulwer-Lytton, The Disowned
Zanim przystąpimy do debugowania, warto zadbać o odpowiednie nastawienie.
Musimy wyzbyć się wielu spośród reakcji obronnych, które w codziennej pracy
pozwalają nam chronić ego. Powinniśmy lepiej radzić sobie z presją i stworzyć
4337ebf6db5c7cc89e4173803ef3875a
4
Debugowanie
 119
sobie warunki zapewniające odpowiedni komfort psychiczny. Najważniejsze jest
pamiętanie o pierwszej zasadzie debugowania.
WSKAZÓWKA NR 30
Nie panikuj.
Nietrudno o panikę, szczególnie jeśli stoimy w obliczu trudnych do dotrzymania terminów bądź jeśli w czasie poszukiwania przyczyny błędu stoi nad nami
zdenerwowany szef lub klient. Warto mimo wszystko zatrzymać się na moment
i dobrze przemyśleć, co może powodować symptomy, które naszym zdaniem
prowadzą do błędu.
Jeśli naszą pierwszą reakcją na spostrzeżenie błędu lub zapoznanie się z raportem o błędzie jest okrzyk „to niemożliwe!”, z pewnością musimy zmienić swoje
podejście. Nie ma sensu marnować aktywności choćby jednego neuronu na
rozważania rozpoczynające się od wniosku „to nie mogło się zdarzyć”, ponieważ błąd nie tylko mógł wystąpić, ale po prostu wystąpił.
Podczas debugowania należy wystrzegać się krótkowzroczności. Powinniśmy
radzić sobie z pokusą eliminowania samych symptomów. Bardzo często faktyczny
problem tkwi wiele kroków od miejsca, które obserwujemy, i może dotyczyć
wielu różnych, pokrewnych aspektów. Zawsze należy podejmować próby odkrywania pierwotnego źródła problemu, zamiast koncentrować się na miejscach,
w których ten problem się ujawnił.
Od czego zacząć
Zanim przystąpimy do analizy błędu, musimy upewnić się, że pracujemy nad
kodem, który został pomyślnie skompilowany, czyli bez żadnych ostrzeżeń.
Sami zawsze ustawiamy możliwie najwyższe poziomy ostrzegania przez kompilator. Poszukiwanie problemu nie ma najmniejszego sensu, jeśli może to za nas
zrobić kompilator! Powinniśmy raczej koncentrować uwagę na rozwiązywaniu
trudniejszych problemów.
Każdą próbę rozwiązania problemu należy zacząć od zebrania wszystkich
istotnych danych. Okazuje się, niestety, że raportowanie o błędach nie jest
nauką ścisłą. Nietrudno o pomyłkę wskutek przypadkowej zbieżności zdarzeń,
a przecież nie możemy sobie pozwolić na poświęcanie czasu na badanie takich
zbieżności. Musimy w pierwszej kolejności zadbać o dokładność obserwacji.
Zachowanie dokładności w raportach o błędach jest utrudnione także wskutek
zaangażowania osób trzecich — w niektórych przypadkach uzyskanie niezbędnej wiedzy wymagałoby obserwowania działań użytkownika, który zgłosił błąd.
Andy pracował kiedyś nad wielką aplikacją graficzną. Niedługo przed datą
wydania testerzy zgłosili, że aplikacja ulega awarii za każdym razem, gdy rysują
coś określonym pędzlem. Programista odpowiedzialny za tę część aplikacji
4337ebf6db5c7cc89e4173803ef3875a
4
120 
Rozdział 3.
Podstawowe narzędzia
stwierdził, że nie dostrzega usterki, ponieważ jego próby malowania tym pędzlem
przebiegały zupełnie normalnie. Spór ciągnął się jeszcze wiele dni, powodując
w zespole coraz większe emocje.
Ostatecznie zdecydowaliśmy się zaprosić obie strony na spotkanie w jednym
pomieszczeniu. Tester wybrał narzędzie pędzla i namalował linię ciągnącą się
od lewego górnego do prawego dolnego narożnika. Aplikacja momentalnie
wyświetliła komunikat o błędzie. „Cóż” — powiedział programista cichym głosem, by wreszcie, nie kryjąc zakłopotania, przyznać, że testował swoje narzędzie,
wykonując tylko ruchy pędzla od lewego dolnego do prawego górnego narożnika (wówczas błąd rzeczywiście nie występował).
Z przytoczonej historii można wysnuć dwa wnioski.

Sytuacja może zmusić nas do przedyskutowania błędu z użytkownikiem,
który go zgłosił, aby uzyskać więcej informacji, niż początkowo sam nam
przekazał.

Sztuczne testy (na przykład kilka pociągnięć pędzlem z dołu do góry wykonanych przez programistę) nie weryfikują dostatecznie wielu aspektów
działania aplikacji. Musimy brutalnie testować zarówno warunki graniczne,
jak i realistyczne wzorce działania użytkownika końcowego. Musimy robić
to możliwie systematycznie (patrz podrozdział „Bezlitosne testy” w rozdziale 8.).
Strategie debugowania
Kiedy już sami stwierdzimy, że wiemy, co się dzieje, warto podjąć próbę określenia, co o sytuacji „myśli” nasz program.
Reprodukcja błędów
Nie, nasze błędy nie rozmnażają się (chociaż niektóre z nich są wystarczająco
stare, aby mogły to robić w pełni legalnie). Mówimy o zupełnie innym rodzaju
reprodukcji.
Wprost wymarzonym początkiem procedury usuwania błędu jest stworzenie
warunków do jego odtwarzania. Gdybyśmy nie mogli odtworzyć sytuacji, w której występuje, skąd wiedzielibyśmy, czy został naprawiony?
Naszym celem nie jest jednak tylko reprodukcja błędu poprzez wykonywanie
pewnej długiej sekwencji kroków — chcemy raczej stworzyć możliwość jego
odtwarzania za pomocą jednego polecenia. Naprawienie błędu jest dużo
trudniejsze, jeśli dojście do punktu, w którym ten błąd się ujawnia, wymaga
wykonania 15 kroków. W pewnych sytuacjach praca nad samym wyizolowaniem
okoliczności, w których ma miejsce dany błąd, bardzo zbliża programistę do
usunięcia usterki.
4337ebf6db5c7cc89e4173803ef3875a
4
Debugowanie
 121
Oto najważniejsza reguła debugowania:
WSKAZÓWKA NR 31
Przed naprawieniem kodu napisz test negatywny.
Czasami zmuszenie się do wyizolowania okoliczności, które prowadzą do błędu,
może dać nam wskazówkę, w jaki sposób można go naprawić. Na rozwiązanie
naprowadza nas proces pisania testu.
Programista we wrogim świecie
W całym tym gadaniu o izolowaniu błędu jest jakiś sens. Zastanówmy się jednak co robi biedny programista, gdy zetknie się z 50 000 wierszy kodu i tykającym zegarem?
Po pierwsze należy przyjrzeć się problemowi. Czy to na pewno awaria? Zawsze
podczas kursów programowania, które prowadzimy, zaskakuje nas to, jak
wielu programistów widząc wyświetlający się na czerwono komunikat o wyjątku,
natychmiast zaczyna szukać przyczyny w kodzie.
WSKAZÓWKA NR 32
Czytaj komunikaty o błędach.
Złe wyniki
A jeśli to nie była awaria? A jeśli to tylko zły wynik?
Dostań się do odpowiedniego fragmentu kodu za pomocą debugera i wykorzystaj przygotowany wcześniej negatywny test w celu wywołania problemu.
Zanim zrobisz cokolwiek innego, upewnij się, że w debugerze również widzisz
nieprawidłową wartość. Obu nam zdarzało się zmarnować wiele godzin na
próbach wyśledzenia błędu, by ostatecznie odkryć, że badany fragment kodu
działał prawidłowo.
Czasami problem jest oczywisty: zmienna interest_rate ma wartość 4,5,
a powinna mieć 0,045. Częściej, aby dowiedzieć się, dlaczego wartość jest nieprawidłowa, trzeba spojrzeć głębiej. Sprawdź, czy potrafisz poruszać się w górę
i w dół stosu wywołań i analizować środowisko lokalnego stosu.
Z naszych doświadczeń wynika, że warto mieć pod ręką ołówek i kartkę papieru,
aby móc robić notatki. Często mamy jakiś trop i staramy się go zbadać dokładniej, by po jakimś czasie dojść do wniosku, że to jednak nie to. Jeśli nie zanotowaliśmy miejsca, w którym byliśmy na początku, możemy stracić dużo czasu,
by dotrzeć tam ponownie.
4337ebf6db5c7cc89e4173803ef3875a
4
122 
Rozdział 3.
Podstawowe narzędzia
Czasami przeglądamy ślad stosu, który wydaje się nieskończony. W takim
przypadku zwykle istnieje szybszy sposób na znalezienie problemu od badania
wszystkich ramek po kolei: można zastosować wyszukiwanie binarne. Ale zanim
je omówimy, przyjrzyjmy się dwóm innym popularnym scenariuszom błędów.
Wrażliwość na dane wejściowe
Z pewnością zetknąłeś się z taką sytuacją. Twój program doskonale działa ze
wszystkimi danymi testowymi i bezproblemowo przetrwał swój pierwszy tydzień
w produkcji. Następnie, przy zetknięciu z konkretnym zestawem danych, nagle
uległ awarii.
Można spróbować przyjrzeć się miejscu w kodzie, gdzie wystąpiła awaria,
i stamtąd przeprowadzić analizę. Czasami jednak łatwiej rozpocząć od danych.
Stwórz kopię zestawu danych i przetwórz ją za pomocą lokalnej kopii aplikacji.
Sprawdź, czy program nadal ulega awarii. Następnie podziel dane za pomocą
algorytmu wyszukiwania binarnego do czasu wyizolowania tych danych wejściowych, które prowadzą do awarii.
Regresja w wydaniach
Pracujesz w dobrym zespole i właśnie opublikowaliście oprogramowanie do
produkcji. W pewnym momencie pojawia się błąd w kodzie, który tydzień wcześniej pracował poprawnie. Czy nie byłoby miło, gdyby można było zidentyfikować konkretną zmianę, która doprowadziła do powstania tego błędu? Zgadnijcie,
z czego można skorzystać? Czas na wyszukiwanie binarne.
Wyszukiwanie binarne
Każdy student informatyki miał okazję pisać kod implementujący algorytm
podziału binarnego (czasami nazywany wyszukiwaniem binarnym). Koncepcja
jest bardzo prosta. Szukamy konkretnej wartości w posortowanej tablicy.
Moglibyśmy po prostu przeglądać każdą wartość po kolei. Takie postępowanie
doprowadziłoby jednak do konieczności przeglądania średnio połowy elementów
tablicy. Trzeba by było przeglądać je do czasu znalezienia wartości szukanej albo
większej od niej, co by oznaczało, że wartości, której szukamy, nie ma w tablicy.
Szybsze jednak będzie zastosowanie podejścia dziel i rządź. Wybierz wartość
w środku tablicy. Jeśli jest to wartość, której szukasz, zakończ pracę. W przeciwnym razie możesz podzielić tablicę na dwie części. Jeśli wybrana wartość
jest większa od wartości docelowej, to znaczy, że cel musi znajdować się w pierwszej połowie tablicy, w przeciwnym razie jest on w drugiej połowie. Powtórz tę
procedurę w odpowiedniej połówce tablicy, a w krótkim czasie znajdziesz szukaną wartość (jak dowiesz się z opisu notacji Duże O w rozdziale 7., wyszukiwanie liniowe ma złożoność O(n), natomiast wyszukiwanie binarne O(logn)).
4337ebf6db5c7cc89e4173803ef3875a
4
Debugowanie
 123
Zatem dla większości problemów w rozsądnych rozmiarach wyszukiwanie
binarne jest wielokrotnie szybsze od liniowego. Zobaczmy jak koncepcję wyszukiwania binarnego można zastosować do debugowania.
Kiedy masz do przeanalizowania obszerny ślad stosu (ang. stack trace) i starasz się ustalić, która funkcja doprowadziła do błędu, możesz dokonać podziału
poprzez wybranie ramki stosu gdzieś pośrodku i sprawdzenie, czy nie ma tam
błędu. Jeśli go tam nie było, to wiesz, że należy się skupić na ramkach wcześniejszych, w przeciwnym razie problem występuje w ramkach późniejszych.
Dokonaj kolejnego podziału. Nawet jeśli ślad stosu zawiera 64 ramki, to zastosowanie podziału binarnego pozwala znaleźć odpowiedź po co najwyżej sześciu próbach.
Jeśli znajdziesz błędy, które występują tylko dla niektórych zestawów danych,
możesz zrobić to samo. Podziel zbiór danych na dwie części i sprawdź, czy
problem nadal występuje, jeśli przetworzysz w aplikacji jedną lub drugą część
zbioru. Kontynuuj dzielenie danych aż do uzyskania minimalnego zestawu wartości, które w dalszym ciągu powodują problemy.
Jeśli Twój zespół wprowadził błąd w którymś spośród kilku poprzednich wydań,
możesz użyć podobnej techniki. Stwórz test, który powoduje awarię w bieżącym
wydaniu. Następnie wybierz wydanie w połowie drogi pomiędzy wersją bieżącą,
a ostatnią znaną działającą wersją. Uruchom test ponownie i zobacz, czy
udało Ci się zawęzić wyszukiwanie. Możliwość wykonywania takich podziałów
to tylko jedna z wielu korzyści stosowania w projektach dobrego systemu kontroli wersji. Wiele spośród nich usprawnia ten proces poprzez automatyzację
wyboru wydań w zależności od wyniku testu.
Logowanie i śledzenie
Debugery zwykle koncentrują się na bieżącym stanie programu. W pewnych
przypadkach to nie wystarczy — musimy obserwować stan programu lub zmiany
jakiejś struktury danych w dłuższym czasie. Analiza stosu wywołań pozwala
określić tylko to, jak trafiliśmy w miejsce, w którym jesteśmy. Nie można na
tej podstawie stwierdzić, co działo się przed utworzeniem danego łańcucha
wywołań (szczególnie w systemach operujących na zdarzeniach)2.
Wyrażenia śledzące to wszystkie te drobne komunikaty diagnostyczne wyświetlane na ekranie lub zapisywane w plikach, jak „jestem tutaj” czy „wartość
x = 2”. To dość prymitywna technika w porównaniu z mechanizmami diagnozowania kodu znanymi ze środowisk IDE, ale okazuje się wyjątkowo efektywna podczas lokalizowania wielu klas błędów, których nie wykrywają debugery.
Śledzenie błędów jest bezcenne w każdym systemie, w którym czas sam w sobie
jest istotnym czynnikiem — w przypadku procesów współbieżnych, systemów
czasu rzeczywistego czy aplikacji działających w oparciu o zdarzenia.
2
Chociaż istnieje debuger przeznaczony dla języka Elm, pozwalający na debugowanie
wcześniejszych stanów.
4337ebf6db5c7cc89e4173803ef3875a
4
124 
Rozdział 3.
Podstawowe narzędzia
Za pomocą wyrażeń śledzących możemy coraz głębiej analizować nasz kod.
Oznacza to, że możemy dodawać wyrażenia śledzące na przykład w trakcie
schodzenia coraz niżej drzewa wywołań.
Komunikaty ze śledzenia powinny cechować się standardowym, spójnym formatem — być może w przyszłości będziemy chcieli poddawać je automatycznej
analizie składniowej. Jeśli na przykład musimy wyśledzić wyciek zasobów (spowodowany na przykład brakiem niezbędnych wyrażeń close dla wszystkich
wyrażeń open), możemy zapisywać każdą operację otwierającą i zamykającą
w pliku dziennika. Przetwarzając ten plik dziennika za pomocą Perla, możemy
łatwo zidentyfikować problematyczną operację open, dla której nie istnieje odpowiednia operacja close.
Przemawianie do gumowej kaczki
Jedną z najprostszych, a jednocześnie wyjątkowo przydatną techniką odnajdywania przyczyny problemu jest zwykłe wyjaśnianie badanego aspektu komuś
innemu. Druga osoba powinna stale patrzeć na monitor przez ramię programisty i potakiwać głową (tak jak gumowa kaczka kołysze się na wodzie w wannie). Nie musi wypowiadać choćby jednego słowa. Chodzi raczej o wyjaśnianie
jej krok po kroku, co analizowany kod powinien robić — okazuje się, że w ten
sposób zadziwiająco często można odkrywać źródła problemów3.
Idea wydaje się dziecinnie prosta, ale praktyka pokazuje, że wyjaśnianie problemu komuś innemu zmusza nas do omawiania wprost, na głos kolejnych
założeń zapisanych w kodzie, które dotąd wydawały nam się oczywiste. Konieczność takiego przedstawiania swoich przemyśleń nierzadko prowadzi do zupełnie nowych wniosków na temat analizowanego problemu. A jeśli nie masz
z kim porozmawiać, możesz powiedzieć to do gumowej kaczki, pluszowego
misia lub kwiatka w doniczce.
Proces eliminacji
W większości projektów diagnozowany kod może być mieszaniną kodu aplikacji napisanego przez nas samych i przez innych członków zespołu projektowego, produktów zewnętrznych (baz danych, interfejsów, bibliotek graficznych,
wyspecjalizowanych mechanizmów komunikacji lub algorytmów itp.) oraz środowiska właściwego danej platformie (systemu operacyjnego, bibliotek systemowych i kompilatorów).
3
Skąd wzięła się ta analogia do gumowej kaczki? Jeszcze jako student londyńskiego
Imperial College Dave bardzo dużo czasu spędzał w towarzystwie asystenta Grega Pugha,
jednego z najlepszych programistów, jakich kiedykolwiek spotkał. Przez wiele miesięcy
Greg nosił przy sobie niewielką, żółtą gumową kaczkę, którą zawsze kładł na terminalu
na czas kodowania. Minęło sporo czasu, zanim Dave zdecydował się spytać o tę kaczkę…
4337ebf6db5c7cc89e4173803ef3875a
4
Debugowanie
 125
Nie można wykluczyć sytuacji, w której błąd tkwi w systemie operacyjnym, kompilatorze lub produkcie zewnętrznym. Nie powinniśmy jednak zaczynać od tego
rodzaju założeń. Znacznie bardziej prawdopodobne jest występowanie błędu
w kodzie tworzonej aplikacji. Ogólnie, korzystniejszym rozwiązaniem jest przyjęcie założenia, zgodnie z którym to kod aplikacji nieprawidłowo wywołuje jakąś
bibliotekę, zamiast przyjmować, że sama biblioteka działa nieprawidłowo.
Nawet jeśli problem rzeczywiście tkwi w rozwiązaniach zewnętrznych, przed
wysłaniem raportu o błędzie i tak musimy wyeliminować własny kod z kręgu
podejrzeń.
Pracowaliśmy kiedyś nad projektem, w którym starszy inżynier był przekonany
o błędnym działaniu wywołania systemowego select w systemie Unix. Żadne
techniki perswazji ani logicznego tłumaczenia nie mogły zmienić tego przekonania (to, że we wszystkich innych aplikacjach sieciowych wspomniane
wywołanie działało bez zarzutu, nie miało żadnego znaczenia). Całymi tygodniami
pisał kod, który miał obejść „wadliwe” wywołanie, ale z jakiegoś powodu nie
udawało mu się wyeliminować problemu. Kiedy wreszcie został zmuszony do
zapoznania się z dokumentacją wywołania select, szybko odkrył problem
i w parę minut usunął usterkę. Od tej pory używamy zdania „select nie działa”
do delikatnego zwracania uwagi na każdą próbę zrzucania odpowiedzialności
za błąd na system w sytuacji, gdy bardziej prawdopodobna jest pomyłka we
własnym kodzie.
WSKAZÓWKA NR 33
Wywołanie select działa.
Musimy pamiętać, że widząc ślady kopyt, powinniśmy w pierwszej chwili myśleć
o koniach, nie o zebrach. System operacyjny prawdopodobnie działa prawidłowo. Także baza danych najprawdopodobniej działa bez zarzutu.
Jeśli „zmieniono tylko jedną rzecz” i jeśli system przestał działać, właśnie ta
zmiana najprawdopodobniej odpowiada (pośrednio lub bezpośrednio) za problemy niezależnie od tego, jak niewinnie wygląda. W pewnych przypadkach
nie mamy wpływu na wprowadzoną zmianę — mogły zostać wydane nowe
wersje systemu operacyjnego, kompilatora, bazy danych lub innego oprogramowania zewnętrznego, powodując prawdziwe spustoszenie w kodzie, który
wcześniej działał w pełni prawidłowo. Nowe wersje mogą zawierać nowe błędy.
Być może usunięto błędy, które udało nam się obejść — w takich przypadkach
istnieje ryzyko nieprawidłowego działania naszego obejścia. Zmieniają się
interfejsy API i zmieniają się dostępne funkcje. Krótko mówiąc, zasady gry stale
są modyfikowane, a naszym zadaniem jest ponowne przetestowanie systemu
w nowych warunkach. Warto więc mieć na uwadze harmonogram realizacji
projektu przed podjęciem decyzji o aktualizacji do nowej wersji — być może
powinniśmy wstrzymać się na przykład do następnego wydania.
4337ebf6db5c7cc89e4173803ef3875a
4
126 
Rozdział 3.
Podstawowe narzędzia
Element zaskoczenia
Kiedy z zaskoczeniem odkrywamy jakiś błąd (być może nawet mamrocząc po
cichu „to niemożliwe”), musimy ponownie przeanalizować założenia, które przyjmowaliśmy za pewnik. Czy w przypadku procedury przetwarzającej listę jednokierunkową (tę, którą uważaliśmy za w pełni bezpieczną i która w założeniu
nie mogła powodować błędów) sprawdziliśmy wszystkie warunki graniczne?
A może należałoby sprawdzić fragment kodu, którego używamy od lat — przecież nie mógł zawierać błędu, prawda? Czy na pewno?
Oczywiście, że mógł. Poziom zaskoczenia odczuwanego po odkryciu błędu jest
wprost proporcjonalny do poziomu zaufania i wiary w niezawodność problematycznego kodu. Właśnie dlatego po napotkaniu „zaskakującego” błędu musimy
zdać sobie sprawę z konieczności weryfikacji co najmniej jednego założenia. Nie
wolno nam pomijać w testach żadnej procedury czy fragmentu kodu tylko dlatego, że „jesteśmy pewni” jego prawidłowego działania. Powinniśmy raczej dowieść
prawidłowości kodu. Musimy udowodnić jego poprawność w określonym kontekście, z bieżącymi danymi i w określonych warunkach granicznych.
WSKAZÓWKA NR 34
Nie należy niczego zakładać — należy to udowodnić.
Kiedy natrafiamy na zaskakujący błąd, oprócz jego zwykłego usunięcia musimy
podjąć próbę określenia, dlaczego usterka nie została wykryta wcześniej. Należy
zastanowić się, czy nie należałoby poprawić testów jednostkowych lub innych
testów, tak aby w przyszłości wykrywały podobne błędy.
Jeśli błąd wynika z nieprawidłowych danych, które były propagowane przez wiele
poziomów, zanim doprowadziły do ostatecznej eksplozji, warto sprawdzić, czy
lepszy mechanizm weryfikacji parametrów w odpowiednich procedurach nie
pozwoliłby zidentyfikować tego błędu wcześniej (patrz materiał poświęcony wczesnym awariom i asercjom w rozdziale 4.).
Warto przy tej okazji sprawdzić, czy na ten sam błąd nie są narażone inne
miejsca kodu. To najlepszy moment, aby znaleźć i naprawić te miejsca. Cokolwiek się stało, musimy mieć pewność, że to się nie powtórzy.
Jeśli naprawienie danego błędu zajęło nam dużo czasu, warto zadać sobie
pytanie dlaczego. Czy można w jakiś sposób ułatwić naprawianie tego błędu
w razie jego wystąpienia w przyszłości? Być może istnieje możliwość skonstruowania lepszych punktów zaczepienia dla naszych testów lub opracowania
analizatora pliku logu.
I wreszcie, jeśli błąd wynika z błędnych założeń innego programisty, warto
omówić ten problem z całym zespołem; skoro jedna osoba coś źle zrozumiała,
niewykluczone, że błędne założenie podzielają też inni.
4337ebf6db5c7cc89e4173803ef3875a
4
Operowanie na tekście

127
Wystarczy wykonać wszystkie te kroki, aby uniknąć przykrych niespodzianek
w przyszłości.
Lista kontrolna debugowania

Czy zgłoszony problem ma postać bezpośredniego wyniku jakiegoś błędu,
czy tylko symptomu?

Czy błąd rzeczywiście występuje w kompilatorze? Czy błąd występuje
w systemie operacyjnym? A może problem tkwi w naszym kodzie?

Gdybyśmy mieli szczegółowo wyjaśnić ten problem współpracownikowi,
co byśmy powiedzieli?

Jeśli podejrzany kod przechodzi swoje testy jednostkowe, czy te testy są
dostatecznie kompletne? Co dzieje się, kiedy te same testy jednostkowe
są wykonywane dla tych samych danych?

Czy warunki, które spowodowały ten błąd, występują w jakimś innym
miejscu systemu?
Pokrewne podrozdziały

Temat 24., „Martwe programy nie kłamią”.
Wyzwania

21
41
Debugowanie jest dostatecznie trudnym wyzwaniem.
Operowanie na tekście
Pragmatyczni programiści pracują z tekstem tak samo, jak stolarze kształtują
drewno. W dotychczasowych podrozdziałach omawialiśmy konkretne narzędzia
używane przez programistów: powłoki, edytory i debugery. Wspomniane narzędzia mają swoje odpowiedniki w warsztacie stolarskim: dłuta, piły i heble, czyli
narzędzia stworzone z myślą o jednej czy dwóch czynnościach. Jednak co jakiś
czas musimy podejmować działania, których nie da się łatwo realizować za
pomocą podstawowego zestawu narzędzi. Potrzebujemy więc uniwersalnego
narzędzia do pracy z tekstem.
Języki do przetwarzania tekstu są dla programowania tym, czym frezarki są
w pracy stolarza. Są głośne, skomplikowane i stosują brutalną siłę. Wystarczy
popełnić drobny błąd, a cały materiał (tekst lub blok drewna) będzie można
wyrzucić do kosza. Niektórzy zarzekają się, że nie mają już miejsca w swoim
zestawie narzędzi. W dobrych rękach zarówno frezarki, jak i języki do przetwarzania tekstu mogą mieć wprost niewiarygodny potencjał i oferować niespotykaną wszechstronność. Możemy błyskawicznie nadawać drewnu kształt,
4337ebf6db5c7cc89e4173803ef3875a
4
128 
Rozdział 3.
Podstawowe narzędzia
tworzyć zaprojektowane profile i wycinać dowolne figury. Prawidłowo stosowane
narzędzia oferują zaskakującą finezję i subtelność. Dobre opanowanie tych
narzędzi wymaga jednak sporo czasu.
Liczba dobrych języków do przetwarzania tekstu stale rośnie. Programiści
aplikacji dla systemu Unix często korzystają z ogromnego potencjału powłok
poleceń, które dodatkowo można rozszerzać o takie narzędzia jak awk czy sed.
Programiści, którzy preferują narzędzia o lepiej zdefiniowanej strukturze, częściej wybierają takie języki, jak Python i Ruby.
Wymienione języki są ważnymi ogniwami otwierającymi drogę do innych technologii. Za pomocą tych języków możemy błyskawicznie tworzyć niezbędne
narzędzia pomocnicze i zrealizować w praktyce koncepcję prototypów — w konwencjonalnych językach te same zadania zajęłyby pięć lub dziesięć razy więcej
czasu. Właśnie ten mnożnik ma zasadnicze znaczenie dla prowadzonych przez
nas eksperymentów. Poświęcenie 30 minut na sprawdzenie jakiegoś szalonego
pomysłu brzmi dużo lepiej niż poświęcenie na tę samą czynność na przykład
pięciu godzin. Poświęcenie jednego dnia na automatyzację ważnych komponentów projektu można zaakceptować, ale już poświęcenie temu zadaniu tygodnia byłoby trudne do usprawiedliwienia. W książce Inżynieria oprogramowania
[KP99] Kernighan i Pike napisali ten sam program w pięciu różnych językach.
Wersja opracowana w Perlu była najkrótsza (wymagała tylko 17 wierszy, podczas gdy wersja napisana w C zajęła aż 150 wierszy). W skryptach Perla możemy
przetwarzać tekst, implementować interakcję z innymi programami, komunikować się za pośrednictwem sieci, generować strony internetowe, wykonywać
obliczenia arytmetyczne dowolnej precyzji i pisać programy przypominające
popularnego Snoopy’ego.
WSKAZÓWKA NR 35
Warto opanować jeden język przetwarzania tekstu.
Aby zademonstrować szerokie możliwości stosowania języków operujących na
tekście, poniżej opisano przykłady kilku aplikacji napisanych w Ruby i w Pythonie w związku z pisaniem niniejszej książki.
Pisanie książki
System budowania książek z cyklu „Biblioteczka praktyka” jest napisany
w Ruby. Autorzy, redaktorzy, specjaliści od składu oraz personel pomocniczy używali zadań Rake do koordynowania budowania dokumentów PDF
i ebooków.
Przykłady kodu i wyróżnianie składni
Uważamy, że jest bardzo ważne, aby każdy kod zaprezentowany w książce
został wcześniej przetestowany. Większość znajdującego się w niej kodu
została przetestowana. Jednak zgodnie z zasadą DRY (patrz temat 9.,
„DRY — Przekleństwo powielania”) nie chcieliśmy kopiować i wklejać
4337ebf6db5c7cc89e4173803ef3875a
4
Operowanie na tekście

129
wierszy kodu z testowanych programów do tekstu książki. Oznaczałoby
to powielanie kodu, co w praktyce mogłoby doprowadzić do tego, że zapomnielibyśmy zaktualizować przykład po zmodyfikowaniu odpowiedniego
programu. W przypadku niektórych przykładów nie chcieliśmy także zanudzać czytelników kodem frameworka potrzebnym do skompilowania i uruchomienia naszego przykładu. Skorzystaliśmy z języka Ruby. Podczas
formatowania książki wywołujemy stosunkowo prosty skrypt. Wyodrębnia
on identyfikowany przez nazwę segment pliku źródłowego, wykonuje
podświetlanie składni i konwertuje wynik na język, którego używamy do
składu tekstu.
Aktualizacja strony WWW
Mamy prosty skrypt, który wykonuje częściowe budowanie książki, wyodrębnia spis treści, a następnie przesyła go na stronę książki w naszej
witrynie internetowej. Mamy również skrypt, który wydziela fragmenty
książki i przesyła je jako przykłady.
Wzory matematyczne
Dysponujemy prostym skryptem Pythona, który konwertuje znaczniki
wzorów matematycznych w formacie LaTeX na estetycznie sformatowany
tekst.
Generowanie zestawień
Większość zestawień tworzymy w postaci odrębnych dokumentów (co
sprawia, że ich utrzymanie w przypadku zmian w dokumencie jest trudne).
Nasze zestawienia są oznaczone w samym tekście, a skrypt Ruby zbiera
i formatuje wpisy.
I tak dalej. Książki z cyklu „Biblioteczka praktyka” są tworzone za pomocą
narzędzi do przetwarzania tekstu. Jeśli skorzystasz z naszej porady, aby używać do wszystkiego formatu zwykłego tekstu, będziesz mógł skorzystać z języków przetwarzania tekstu, co przynosi cały szereg korzyści.
Pokrewne podrozdziały

Temat 16., „Potęga zwykłego tekstu”.

Temat 17., „Powłoki”.
Ćwiczenia
11. Przepisujesz aplikację, która w roli języka konfiguracji używała formatu
YAML. Twoja firma standardowo korzysta z formatu JSON, więc masz
zbiór plików .yaml, które muszą zostać przekształcone na format .json.
Napisz skrypt, który pobiera katalog i konwertuje wszystkie pliki .yaml
z tego katalogu na odpowiedni plik .json (tak, aby plik database.yaml stał
się plikiem database.json w poprawnym formacie JSON).
4337ebf6db5c7cc89e4173803ef3875a
4
130 
Rozdział 3.
Podstawowe narzędzia
12. Twój zespół początkowo zdecydował się używać nazw zmiennych w formacie
camelCase, ale potem zmieniono zdanie i zdecydowano, że będzie stosowany
format snake_case. Napisz skrypt, który skanuje wszystkie pliki źródłowe,
wyszukuje w nich nazwy w formacie camelCase i je wyświetla.
13. Bazując na poprzednim ćwiczeniu, dodaj możliwość automatycznej zamiany
tych nazw zmiennych w co najmniej jednym pliku. Pamiętaj, aby zachować
kopię zapasową oryginałów w przypadku, gdyby coś poszło źle.
22
42
Dzienniki inżynierskie
Dave pracował kiedyś dla małego producenta komputerów, co oznaczało współpracę z inżynierami elektronikami i czasami inżynierami mechanikami.
Wielu z nich chodziło z papierowym notatnikiem, zazwyczaj z wpiętym długopisem. Co jakiś czas podczas rozmów ci inżynierowie sięgali po notatnik i coś
zapisywali.
W końcu Dave zadał oczywiste pytanie. Okazało się, że przeszkolono ich, aby
prowadzili inżynierskie dzienniki, rodzaj pamiętnika, w którym rejestrowali
to, co zrobili, czego się dowiedzieli; wpisywali w nim szkice pomysłów, odczyty
urządzeń pomiarowych: w zasadzie wszystko, co miało związek z ich pracą.
Po zapełnieniu notatnika zapisywali zakres dat na grzbiecie, a następnie odkładali na półkę obok poprzednich dzienników. Między inżynierami występowała
łagodna rywalizacja dotycząca tego, czyje dzienniki zajmą najwięcej miejsca
na półce.
Używamy dzienników do różnych rzeczy: do sporządzania notatek na spotkaniach, aby zanotować, nad czym pracujemy, w celu zapisania wartości
zmiennych podczas debugowania, aby zidentyfikować miejsca, w których skończyliśmy pracę, aby zarejestrować dzikie pomysły, a czasem po prostu, żeby
sobie pobazgrać4.
Prowadzenie takiego dziennika przynosi trzy główne korzyści:
4

Dziennik jest bardziej niezawodny niż pamięć. Ktoś może zadać pytanie:
„Jaka jest nazwa firmy, z której w zeszłym tygodniu dzwoniono w sprawie
problemów z zasilaczem?”. Wtedy możesz otworzyć notatnik i znaleźć w nim
nazwę i numer telefonu.

Notatnik jest miejscem do przechowywania pomysłów, które nie są bezpośrednio istotne dla zadania, które wykonujesz. W ten sposób możesz
nadal być skoncentrowany na tym, co robisz, wiedząc, że świetne pomysły
nie wylecą Ci z głowy.
Istnieją pewne dowody na to, że bazgranie pomaga się skupić i poprawia zdolności
poznawcze, przykłady można znaleźć w książce What does doodling do? [And10].
4337ebf6db5c7cc89e4173803ef3875a
4
Dzienniki inżynierskie


131
Dziennik spełnia rolę gumowej kaczki (opisanej wcześniej w rozdziale).
Kiedy zatrzymasz się, żeby coś zapisać, Twój mózg może „przełączyć bieg”,
niemal tak, jakbyś z kimś rozmawiał — to olbrzymia szansa na refleksję.
Możesz zacząć sporządzać notatkę, a potem nagle zdać sobie sprawę, że
to, co właśnie zrobiłeś — temat Twojej notatki — było po prostu błędem.
Jest też dodatkowa korzyść. Co jakiś czas możesz spojrzeć wstecz na to, co
robiłeś wiele lat wcześniej, i pomyśleć o ludziach, projektach lub o okropnych
ubraniach i fryzurach.
Warto zatem prowadzić tego rodzaju inżynierski dziennik. Używaj notatnika
papierowego — nie korzystaj z pliku lub wiki: w akcie fizycznego pisania jest
coś szczególnego, nieporównywalnego z „wklepywaniem” do komputera. Poświęć
na prowadzenie notatnika miesiąc i przekonaj się, czy masz z tego jakieś
korzyści.
Jeśli nie uzyskasz niczego więcej, to będziesz mógł wykorzystać umiejętność
notowania do spisywania wspomnień, gdy będziesz bogaty i sławny.
Pokrewne podrozdziały

Temat 6., „Portfolio wiedzy”.

Temat 37., „Słuchaj swojego jaszczurczego mózgu”.
4337ebf6db5c7cc89e4173803ef3875a
4
132 
Rozdział 3.
Podstawowe narzędzia
4337ebf6db5c7cc89e4173803ef3875a
4
Rozdział 4.
Pragmatyczna paranoja
WSKAZÓWKA NR 36
Pisanie doskonałego oprogramowania jest niemożliwe.
Czy to bolało? Nie powinno. Należy się z tym pogodzić jako z jednym z aksjomatów. Należy to wykorzystać. Należy to celebrować. Skoro doskonałe oprogramowanie nie istnieje, nikt w krótkiej historii przetwarzania komputerowego
nie mógł napisać doskonałego fragmentu oprogramowania. To mało prawdopodobne, abyśmy właśnie my jako pierwsi to osiągnęli. Jeśli nie pogodzimy się
z tym faktem, będziemy tracili cenny czas i energię na próby realizacji nierealnego marzenia.
Jak w takim razie pragmatyczny programista może zmienić tę dość przygnębiającą rzeczywistość w zaletę? Właśnie tym zagadnieniem zajmiemy się w tym
rozdziale.
Każdy uważa, że jest jedynym naprawdę dobrym kierowcą na Ziemi. Reszta
świata zostaje daleko w tyle — nie zatrzymuje się na znaku STOP, nie potrafi
trzymać się swojego pasa, nie używa kierunkowskazów, rozmawia przez telefon,
czyta gazety i ogólnie nie trzyma naszych standardów. W tej sytuacji musimy
prowadzić bardzo ostrożnie. Staramy się przewidywać problemy, zanim nastąpią. Spodziewamy się niespodziewanego i konsekwentnie unikamy pułapek,
z których nie moglibyśmy się wyplątać.
Analogia z kodowaniem jest dość oczywista. Stale mamy kontakt z kodem
autorstwa innych programistów — kodem, który nie zawsze odpowiada naszym
wysokim standardom — i musimy radzić sobie z danymi wejściowymi, które
mogą, ale nie muszą być w pełni poprawne. Sytuacja zmusza nas więc do ostrożnego kodowania. W razie jakichkolwiek wątpliwości weryfikujemy wszystkie
otrzymywane informacje. Do wykrywania błędnych danych używamy asercji.
4337ebf6db5c7cc89e4173803ef3875a
4
134 
Rozdział 4.
Pragmatyczna paranoja
Sprawdzamy spójność, definiujemy ograniczenia dla kolumn bazy danych i cieszymy się dobrym samopoczuciem w przekonaniu o spełnionym obowiązku.
Pragmatyczni programiści muszą jednak iść krok dalej. Nie ufają nawet samym
sobie. Skoro nikt nie pisze doskonałego kodu, także oni, pragmatyczni programiści, kodują swoje rozwiązania, zabezpieczając się również przed własnymi
pomyłkami. Pierwsze techniki obronne omówimy w podrozdziale „Projektowanie kontraktowe” — klienci i dostawcy muszą uzgodnić i precyzyjnie opisać swoje
prawa i obowiązki.
W podrozdziale „Martwe programy nie kłamią” zajmiemy się sposobami unikania uszkodzeń przy okazji usuwania błędów. Próbujemy możliwie często sprawdzać poprawiany system i przerywać wykonywanie programu natychmiast po
wykryciu działania niezgodnego z oczekiwaniami.
W podrozdziale „Programowanie asertywne” omówimy prostą metodę sprawdzania poprawności niejako przy okazji właściwego programowania — technikę
pisania kodu, który aktywnie sprawdza nasze założenia.
Kiedy nasze programy zyskują na dynamice, szybko odkrywamy, że coraz trudniej zapanować nad zasobami systemowymi — pamięcią, plikami, urządzeniami itp. W podrozdziale „Jak zrównoważyć zasoby” zasugerujemy sposoby
eliminowania ryzyka zaniedbania któregoś z tych aspektów.
A co najważniejsze, zgodnie z tym, co opisano w podrozdziale „Nie prześcigaj
swoich świateł”, zawsze należy poruszać się małymi krokami, tak aby nie spaść
z krawędzi urwiska.
W świecie niedoskonałych systemów, absurdalnych harmonogramów, śmiesznych narzędzi i niewykonalnych wymagań warto przynajmniej zadbać o bezpieczeństwo. Jak powiedział Woody Allen: Kiedy wszyscy na ciebie dybią, paranoja jest czymś zupełnie naturalnym.
23
36
Projektowanie kontraktowe
Nic nie szokuje ludzi równie mocno jak zdrowy rozsądek i jasne reguły.
Ralph Waldo Emerson, Eseje
Praca z systemami komputerowymi jest trudna. Praca z ludźmi jest jeszcze
trudniejsza. Nasza natura sprawia, że rozumienie zależności i relacji pomiędzy
ludźmi zajmuje nam najwięcej czasu. Pewne wyzwania, z którymi ludzkość mierzy się od tysięcy lat, dotyczą także pisania oprogramowania. Jednym z najlepszych rozwiązań zapewniających jasne reguły jest kontrakt.
Kontrakt definiuje nasze prawa i obowiązki oraz prawa i obowiązki drugiej strony.
Dodatkowo obejmuje uzgodnienia dotyczące skutków ewentualnego niedotrzymania zapisów zawartych w umowie.
4337ebf6db5c7cc89e4173803ef3875a
4
Projektowanie kontraktowe

135
Kontrakt może dotyczyć na przykład zatrudnienia i określać godziny pracy oraz
reguły obowiązujące na danym stanowisku. Za wypełnianie takiego kontraktu
pracodawca wpłaca pracownikowi wynagrodzenie i premie. Obie strony wypełniają swoje obowiązki i wszyscy na tym zyskują.
Idea kontraktów (zarówno formalnych, jak i nieformalnych) jest z powodzeniem
stosowana na całym świecie i ułatwia relacje międzyludzkie. Czy możemy wykorzystać tę koncepcję także do ułatwienia implementowania interakcji pomiędzy
modułami oprogramowania? Odpowiedź jest prosta: tak.
Projektowanie kontraktowe (DBC)
Bertrand Meyer (Object-Oriented Software Construction [Mey97]) stworzył koncepcję projektowania kontraktowego (ang. design by contract — DBC) dla języka
programowania Eiffel1. To stosunkowo prosta, ale też niezwykle skuteczna
technika koncentrująca się na dokumentowaniu (i uzgadnianiu) praw i obowiązków modułów oprogramowania, tak aby gwarantowały prawidłowe działanie
całego programu. Czym właściwie jest prawidłowy program? To taki, który nie
robi ani więcej, ani mniej, niż oczekują jego odbiorcy. Dokumentowanie i weryfikowanie tych oczekiwań jest sercem projektowania kontraktowego (DBC).
Każda funkcja i metoda systemu oprogramowania odpowiada za konkretne
działanie. Zanim przystąpi do tego działania, procedura lub metoda może wyrażać pewne oczekiwania dotyczące stanu świata. Procedura lub metoda może też
przyjmować jakieś założenia odnośnie stanu świata w momencie końca swojej
pracy. Meyer opisał te oczekiwania i wnioski w następujący sposób:
1

Warunki wstępne. Te warunki muszą być spełnione, aby procedura
w ogóle została wywołana. Są to wymagania tej procedury. Procedura
nigdy nie powinna być wywołana w sytuacji, gdy jej warunki wstępne są
naruszone. Za przekazywanie właściwych danych odpowiada kod wywołujący (patrz ramka „Kto jest odpowiedzialny” w dalszej części tego podrozdziału).

Warunki końcowe. Te warunki określają, do czego dana procedura ma
doprowadzić — opisują stan świata po zakończeniu jej działania. Istnienie
warunku końcowego procedury oznacza, że nie są dopuszczalne na przykład nieskończone pętle.

Niezmienniki klasy. Klasa gwarantuje, że ten warunek jest zawsze spełniony z perspektywy kodu wywołującego. W czasie wewnętrznego przetwarzania kodu procedury niezmiennik może nie być zachowany, ale już
w momencie kończenia przetwarzania i zwracania sterowania do kodu
wywołującego inwariant musi być spełniony. (Należy pamiętać, że klasa
nie może udostępniać nieograniczonego dostępu do zapisu do żadnej składowej danych wchodzącej w skład niezmiennika).
Język powstał po części na podstawie wcześniejszych rozwiązań między innymi autorstwa Dijkstry, Floyda, Hoare’a i Wirtha.
4337ebf6db5c7cc89e4173803ef3875a
4
136 
Rozdział 4.
Pragmatyczna paranoja
Kontrakt pomiędzy procedurą a dowolnym potencjalnym kodem wywołującym
można zatem sformułować w następujący sposób:
Jeżeli kod wywołujący spełni wszystkie warunki wstępne, procedura, po
jej zakończeniu powinna zagwarantować spełnienie wszystkich warunków
końcowych i niezmienników.
Jeżeli którakolwiek ze stron nie wywiąże się z warunków umowy, zostanie
wywołany środek zaradczy (uzgodniony wcześniej) — może to być zgłoszenie
wyjątku albo zakończenie programu. Niezależnie od tego, co się stanie, nie
należy mylić niespełnienia kontraktu z błędem. Taki scenariusz nigdy nie
powinien się wydarzyć, dlatego nie należy wykorzystywać warunków wstępnych do realizacji takich zadań, jak sprawdzanie poprawności danych wprowadzanych przez użytkownika.
Niektóre języki mają lepsze wsparcie dla tych pojęć niż inne. Na przykład Clojure
obsługuje warunki wstępne i końcowe, a także bardziej kompleksowe oprzyrządowanie dostarczone przez specyfikacje. Oto przykład funkcji aplikacji bankowej do dokonywania wpłaty z wykorzystaniem prostych warunków wstępnych i końcowych:
(defn accept-deposit [account-id amount]
{ :pre [ (> amount 0.00)
(account-open? account-id) ]
: post [ (contains? (account-transactions account-id) %) ] }
"Przyjęcie depozytu i zwrócenie identyfikatora transakcji"
;; Tutaj znajdą się inne obliczenia...
;; Zwrócenie nowo utworzonej transakcji:
(create-transaction account-id :deposit amount))
Dla funkcji accept-deposit istnieją dwa warunki wstępne. Pierwszy określa, że
kwota jest większa od zera, a drugi to, że rachunek jest otwarty i aktywny,
o czym decyduje wynik funkcji o nazwie account-open?. Jest również warunek
końcowy: funkcja gwarantuje, że wśród transakcji dla tego rachunku jest nowa
transakcja (wartość zwracana z tej funkcji, reprezentowana tutaj przez symbol %).
Jeśli wywołamy funkcję accept-deposit z dodatnią wartością kwoty depozytu
i dla aktywnego rachunku, funkcja przejdzie do utworzenia transakcji odpowiedniego typu i wykonania innych obliczeń. Jednak jeśli wystąpi błąd w programie i w jakiś sposób przekażemy ujemną kwotę depozytu, zostanie zgłoszony
wyjątek fazy wykonywania programu:
Exception in thread "main"...
Caused by: java.lang.AssertionError: Assert failed: (> amount 0.0)
Funkcja ta także wymaga, aby podany rachunek był otwarty i aktywny. Jeśli nie
spełnia tych warunków, zostanie zgłoszony wyjątek:
Exception in thread "main"...
Caused by: java.lang.AssertionError: Assert failed: (account-open? account-id)
4337ebf6db5c7cc89e4173803ef3875a
4
Projektowanie kontraktowe

137
W innych językach istnieją mechanizmy, które pomimo że nie są specyficzne
dla programowania kontraktowego, również są skuteczne. Na przykład w języku
Elixir do rozsyłania wywołań funkcji do kilku dostępnych bloków kodu są wykorzystywane klauzule strażników (ang. guard clauses):
defmodule Deposits do
def accept_deposit(account_id, amount) when (amount > 100000) do
# Zawołaj menedżera!
end
def accept_deposit(account_id, amount) when (amount > 10000) do
# Specjalne wymagania dotyczące raportowania
# Obliczenia...
end
def accept_deposit(account_id, amount) when (amount > 0) do
# Obliczenia...
end
end
Wywołanie funkcji accept_deposit w przypadku odpowiednio wysokiej kwoty
może wymagać dodatkowych kroków i osobnego przetwarzania. Jeśli jednak
spróbujesz wywołać tę funkcję z kwotą mniejszą lub równą zeru, zostanie
zgłoszony wyjątek informujący, że to niedozwolone:
** (FunctionClauseError) no function clause matching in
Deposits.accept_deposit/2
To lepsze rozwiązanie niż sprawdzanie wprowadzonych danych. W tym przypadku, jeśli argumenty są poza zakresem, po prostu nie można wywołać funkcji.
WSKAZÓWKA NR 37
Należy projektować z uwzględnieniem kontraktów.
W podrozdziale „Ortogonalność” w rozdziale 2. zasugerowaliśmy pisanie
skromnego kodu. Tym razem kładziemy nacisk raczej na leniwy kod — należy
precyzyjnie określić, co będzie akceptowane na początku, i obiecać możliwie
niewiele na wyjściu. Musimy pamiętać, że jeśli z kontraktu wynika, że akceptujemy dosłownie wszystko i obiecujemy w zamian wszystkie skarby świata,
będziemy musieli napisać mnóstwo kodu!
W dowolnym języku programowania, niezależnie od tego, czy jest to język funkcyjny, obiektowy, czy proceduralny, stosowanie DBC zmusza do myślenia.
Niezmienniki klas i języki funkcyjne
To kwestia nazewnictwa. Eiffel to obiektowy język programowania, więc Meyer
użył nazwy „niezmiennik klas”. Jednak pojęcie niezmienników jest bardziej
ogólne. W gruncie rzeczy chodzi o niezmienność stanu. W językach obiektowych
stan jest powiązany z egzemplarzami klas. Ale w innych językach pojęcie
stanu również występuje.
4337ebf6db5c7cc89e4173803ef3875a
4
138 
Rozdział 4.
Pragmatyczna paranoja
W języku funkcyjnym zazwyczaj przekazujemy stan do funkcji, a w wyniku
otrzymujemy stan zaktualizowany. W takich okolicznościach pojęcie niezmienników jest równie przydatne.
Projektowanie kontraktowe a technika TDD
Czy technika programowania kontraktowego jest potrzebna w świecie, w którym
programiści stosują testy jednostkowe, mechanizmy wytwarzania oprogramowania
sterowanego testami (TDD), testowanie oparte na właściwościach lub programowanie defensywne?
Krótka odpowiedź brzmi „tak”.
DBC i testowanie to różne podejścia do szerszego tematu poprawności programu.
Oba podejścia mają swoją wartość i oba mają zastosowanie w różnych sytuacjach.
DBC ma kilka zalet w stosunku do określonych metod testowania:





DBC nie wymaga żadnej konfiguracji ani tworzenia obiektów-makiet (ang. mocks).
DBC definiuje parametry dla sukcesu lub niepowodzenia we wszystkich przypadkach, podczas gdy testy każdorazowo mogą badać tylko jeden przypadek.
TDD i inne metody testowania są stosowane tylko „w fazie testowania”, w ramach
cyklu budowania oprogramowania. Z kolei DBC i asercje są stosowane w całym
cyklu życia oprogramowania: podczas projektowania, tworzenia, wdrażania i utrzymania.
Techniki TDD nie koncentrują się na sprawdzaniu wewnętrznych niezmienników
w testowanym kodzie — skupiają się raczej na testach typu „czarna skrzynka”
publicznego interfejsu.
DBC jest techniką bardziej wydajną (i lepiej spełnia zasadę DRY) niż programowanie defensywne, ponieważ wszystkie elementy kodu muszą sprawdzać poprawność danych.
TDD jest świetną techniką, ale tak jak w przypadku wielu technik, może skłaniać do
koncentrowania się na „szczęśliwej ścieżce”, a nie na prawdziwym świecie pełnym
niepoprawnych danych, złych aktorów, niewłaściwych wersji i nieprawidłowych
specyfikacji.
Implementacja koncepcji
projektowania kontraktowego
Proste zapisywanie na etapie projektowania, w jakim przedziale muszą mieścić
się dane wejściowe, jakie są warunki graniczne oraz co dana procedura obiecuje
zwrócić (i — co jeszcze ważniejsze — czego nie obiecuje zwrócić), to ważny krok
w kierunku pisania lepszego oprogramowania. Rezygnacja z wyrażania i zapisywania tych aspektów cofa nas do programowania przez koincydencję (patrz
rozdział 6.), czyli modelu, od którego zaczyna się wiele projektów, na którym te
projekty się kończą i przez które ostatecznie upadają.
4337ebf6db5c7cc89e4173803ef3875a
4
Projektowanie kontraktowe

139
W językach, które nie obsługują projektowania kontraktowego na poziomie kodu,
należy skorzystać przynajmniej z tych możliwości, które mamy — nawet wówczas efekt będzie zadowalający. Projektowanie kontraktowe jest — jak sama
nazwa wskazuje — techniką projektową. Nawet bez automatycznego sprawdzania możemy umieścić kontrakt w kodzie (w formie komentarzy) i nadal uzyskiwać wymierne korzyści.
Asercje
O ile samo dokumentowanie tych założeń jest doskonałym punktem wyjścia,
dużo większe korzyści możemy osiągnąć, zmuszając kompilator do automatycznego sprawdzania zgodności kodu z zapisami kontraktu. W niektórych
językach można to działanie przynajmniej częściowo emulować za pomocą
asercji: sprawdzeń w czasie wykonywania programu lub warunków logicznych (patrz temat 25., „Programowanie asertywne”). Dlaczego tylko częściowo?
Czy za pomocą asercji nie można osiągnąć wszystkich celów projektowania kontraktowego?
Niestety nie. Po pierwsze w językach obiektowych nie jest obsługiwane propagowanie asercji w dół hierarchii dziedziczenia. Oznacza to, że w razie nadpisania metody klasy bazowej, dla której zdefiniowano kontrakt, asercje implementujące ten kontrakt nie będą wywoływane prawidłowo (chyba że zostaną
ręcznie powielone w nowym kodzie). Musimy pamiętać o konieczności ręcznego
wywołania niezmiennika klasy (i wszystkich niezmienników klasy bazowej)
przed opuszczeniem każdej metody. Podstawowym problemem jest więc brak
mechanizmów automatycznego wymuszania stosowania kontraktu.
W innych środowiskach wyjątki generowane przez asercje w stylu DBC mogą
być globalnie wyłączone lub ignorowane w kodzie.
Po drugie, nie istnieje wbudowany mechanizm zarządzania „starymi” wartościami, czyli wartościami istniejącymi w momencie wywołania metody. Jeśli do
wymuszania stosowania zapisów kontraktów używamy asercji, musimy uzupełnić warunek wstępny o kod zapisujący wszelkie informacje niezbędne podczas sprawdzania warunku końcowego — o ile stosowany język na to pozwala.
W języku Eiffel, z którego programowanie kontraktowe się wywodzi, można użyć
konstrukcji old wyrażenie.
I wreszcie po trzecie, system i biblioteki wykonawcze nie są projektowane
z myślą o obsłudze kontraktów, zatem nasze wywołania nie podlegają weryfikacji. Problem jest o tyle poważny, że właśnie na granicy dzielącej nasz kod
od używanych przez nas bibliotek zwykle wykrywa się najwięcej problemów
(szczegółowe omówienie tego zagadnienia można znaleźć w temacie 24., „Martwe programy nie kłamią”).
4337ebf6db5c7cc89e4173803ef3875a
4
140 
Rozdział 4.
Pragmatyczna paranoja
Projektowanie kontraktowe
i wczesne wykrywanie usterek
Projektowanie kontraktowe wprost doskonale wpisuje się w koncepcję wczesnego wykrywania błędów (patrz temat 24., „Martwe programy nie kłamią”).
Dzięki zastosowaniu asercji lub mechanizmu DBC w celu sprawdzenia warunków wstępnych, końcowych i niezmienników, można sprowokować awarię na
wczesnym etapie wykonywania kodu i zgłosić dokładne informacje dotyczące
problemu.
Przypuśćmy, że dysponujemy metodą obliczającą pierwiastki kwadratowe.
Metoda wymaga zastosowania warunku wstępnego ograniczającego dziedzinę
do liczb dodatnich. W językach wspierających DBC, jeśli do funkcji sqrt przekażemy liczbę ujemną, otrzymamy komunikat o błędzie sqrt_arg_must_be_positive
i stos wywołań prezentujący łańcuch kolejno wywoływanych procedur.
To lepsze niż alternatywne rozwiązanie w innych językach, na przykład Java,
C i C++, gdzie przekazanie liczby ujemnej na wejściu funkcji sqrt powoduje zwrócenie wartości specjalnej NaN (od ang. Not a Number). Problem może pozostawać
ukryty przez dłuższy czas, jeśli dopiero w dalszej części programu wykonujemy
jakieś operacje matematyczne na wartości NaN (wówczas otrzymamy niespodziewany wynik).
Dużo prostszym rozwiązaniem jest znalezienie i zdiagnozowanie tego problemu
poprzez wczesne wygenerowanie błędu, kiedy usterkę można łatwo skojarzyć
z konkretnym miejscem w kodzie.
Niezmienniki semantyczne
Za pomocą niezmienników semantycznych możemy wyrażać nienaruszalne
wymagania, rodzaj kontraktu filozoficznego.
Napisaliśmy kiedyś system zarządzający transakcjami kartami debetowymi.
Zgodnie z najważniejszym wymaganiem użytkownik karty debetowej nigdy nie
powinien dwukrotnie wykonywać na swoim koncie tej samej transakcji. Innymi
słowy, niezależnie od awarii i usterek, które mogły mieć miejsce, błąd powinien
prowadzić raczej do rezygnacji z przetwarzania transakcji, nigdy do dwukrotnego
przetworzenia tej samej transakcji.
Ta prosta zasada wynikająca bezpośrednio z wymagań okazała się bardzo
pomocna podczas realizacji scenariuszy odtwarzania systemu po skomplikowanych błędach i wskazywała właściwe kierunki dla szczegółowych rozwiązań projektowych i implementacyjnych w wielu obszarach.
Nigdy nie powinniśmy mylić wymagań, które wyrażają stałe i nienaruszalne
zasady, z wymaganiami reprezentującymi zaledwie bieżącą politykę, które
z natury rzeczy mogą ulec zmianie wraz z nastaniem nowego kierownictwa.
4337ebf6db5c7cc89e4173803ef3875a
4
Projektowanie kontraktowe

141
Kto jest odpowiedzialny?
Co odpowiada za sprawdzanie warunku wstępnego: kod wywołujący czy wywoływana procedura? W przypadku implementacji wchodzących w skład języka programowania żadna odpowiedź nie jest prawidłowa — warunek wstępny jest testowany w tle zaraz po wywołaniu procedury przez kod wywołujący, ale przed wejściem
w samą procedurę. Oznacza to, że jeśli jakiś parametr musi zostać bezpośrednio
sprawdzony, takiego sprawdzenia powinien dokonać kod wywołujący, ponieważ sama
procedura nigdy nie otrzyma parametrów naruszających jej warunki wstępne.
(W przypadku języków pozbawionych wbudowanej obsługi projektowania kontraktowego programista musi otoczyć wywoływaną procedurę wstępem i zakończeniem odpowiedzialnymi za weryfikację asercji).
Przeanalizujmy przykład programu odczytującego liczbę z konsoli, obliczającego jej
pierwiastek kwadratowy (za pomocą wywołania funkcji sqrt) i wyświetlającego wynik.
Funkcja sqrt ma zdefiniowany warunek wstępny — jej argument nie może być liczbą
ujemną. Jeśli użytkownik wpisuje w konsoli liczbę ujemną, to kod wywołujący powinien wykluczyć możliwość przekazania tej wartości na wejściu funkcji sqrt. W takim
przypadku kod wywołujący ma do dyspozycji wiele rozwiązań — może przerwać
działanie programu, może wygenerować ostrzeżenie i odczytać inną liczbę lub może
zamienić tę liczbę na dodatnią i dopisać jednostkę i do wyniku zwróconego przez
funkcję sqrt. Niezależnie od wybranego rozwiązania z pewnością nie jest to problem samej funkcji sqrt.
Wyrażenie dziedziny funkcji pierwiastka kwadratowego w formie warunku wstępnego procedury sqrt powoduje przeniesienie odpowiedzialności za poprawność
przekazywanej liczby na kod wywołujący, skąd pochodzi ta wartość. Możemy następnie przystąpić do projektowania bezpiecznej procedury sqrt, ponieważ wiemy, że
jej dane wejściowe będą mieściły się w akceptowanym przedziale.
Właśnie dlatego posługujemy się terminem niezmienników semantycznych —
taki warunek musi wskazywać znaczenie pozostałych rozwiązań, a więc nie
może być narażony na żadne kaprysy (do tego służą raczej bardziej dynamiczne
reguły biznesowe).
W razie napotkania wymagania, które spełnia te kryteria, warto zapisać je
w widocznym miejscu tworzonej dokumentacji (niezależnie od jej formy). Może
to być wyróżniony trzykrotnie większą czcionką wpis na liście wymagań w dokumencie wielokrotnie prezentowanym wszystkim członkom zespołu lub po prostu
wielka notatka na widocznej dla wszystkich białej tablicy. Należy wyrazić to
wymaganie możliwie jasno i jednoznacznie. Na przykład dla systemu obsługi kart
debetowych można by napisać:
BŁĘDY NA KORZYŚĆ UŻYTKOWNIKA.
To wyjątkowo jasne, zwięzłe i zrozumiałe stwierdzenie jest potem stosowane
w wielu różnych obszarach tworzonego systemu. To swoisty kontrakt ze wszystkimi użytkownikami systemu. Gwarancja zachowania oprogramowania, którą
twórcy dają użytkownikom.
4337ebf6db5c7cc89e4173803ef3875a
4
142 
Rozdział 4.
Pragmatyczna paranoja
Kontrakty dynamiczne i agenty
Do tej pory mówiliśmy o kontraktach jako o stałych, niezmiennych specyfikacjach. Okazuje się jednak, że w świecie autonomicznych agentów taka definicja
nie zawsze ma sens. Przez autonomiczne agenty rozumiemy mechanizmy, które
mogą swobodnie odrzucać żądania, których z tego czy innego powodu nie
chcą obsłużyć. Takie agenty mogą dowolnie renegocjować kontrakt — „mogę
zwrócić to, czego żądasz, ale jeśli dasz mi na wejściu to lub tamto, mogę zwrócić
coś innego”.
Każdy system korzystający z technologii autonomicznych agentów definiuje,
oczywiście, pewne krytyczne zależności na potrzeby przyszłych kontraktów
(nawet jeśli te kontrakty mają być generowane dynamicznie).
Wyobraźmy to sobie — przy odpowiedniej liczbie komponentów i agentów, które
mogłyby negocjować między sobą kontrakty z myślą o osiągnięciu celu, moglibyśmy raz na zawsze rozwiązać kryzys związany z produktywnością oprogramowania, pozwalając zrobić to samym programom.
Jeśli jednak nie potrafimy ręcznie używać kontraktów, siłą rzeczy nie będziemy
potrafili robić tego także automatycznie. Kiedy więc następnym razem będziemy
projektowali jakieś oprogramowanie, koniecznie zaprojektujmy także jego
kontrakt.
Pokrewne podrozdziały

Temat 24., „Martwe programy nie kłamią”.

Temat 25., „Programowanie asertywne”.

Temat 38., „Programowanie przez koincydencję”.

Temat 42., „Testowanie na podstawie właściwości”.

Temat 43., „Pozostań w bezpiecznym miejscu”.

Temat 45., „Kopalnia wymagań”.
Wyzwania

Kwestie do przemyślenia: Skoro projektowanie kontraktowe jest takie
korzystne, dlaczego nie zyskało szerokiej popularności? Czy postępowanie
zgodnie z zapisami kontraktu jest trudne? Czy zmusza programistę do
bieżącego rozwiązywania problemów, zamiast odkładać je na później? Czy
zmusza nas do MYŚLENIA!? Nie ma wątpliwości, że to bardzo niebezpieczne
narzędzie!
Ćwiczenia
14. Zaprojektuj interfejs kuchennego blendera. Ostatecznie będzie to webowa
wersja blendera IoT, ale na razie potrzebujemy tylko interfejsu, by nim sterować. Blender ma ustawienia szybkości pracy (0 oznacza, że jest wyłączony).
4337ebf6db5c7cc89e4173803ef3875a
4
Martwe programy nie kłamią
 143
Blendera nie można obsługiwać, jeśli jest pusty, a prędkość każdorazowo
można zmieniać tylko o jedną jednostkę (czyli z 0 na 1, z 1 na 2, a nie
z 0 na 2).
Poniżej zestawiono metody. Dodaj właściwe warunki wstępne i końcowe
oraz niezmienniki.
int getSpeed()
void setSpeed(int x)
boolean isFull()
void fill()
void empty()
15. Ile liczb jest w ciągu 0, 5, 10, 15, ..., 100?
24
37
Martwe programy nie kłamią
Czy nigdy nie zdarzyła nam się sytuacja, w której inni zwrócili uwagę na nasze
problemy szybciej niż my sami? To samo dotyczy cudzego kodu. Jeśli coś
w naszych programach zaczyna działać niezgodnie z oczekiwaniami, nierzadko
pierwszy sygnał o nietypowej sytuacji pochodzi z procedury należącej do biblioteki zewnętrznej. Być może zbłąkany wskaźnik spowodował nadpisanie uchwytu
pliku jakimiś bezsensownymi danymi. Dopiero kolejne wywołanie operacji
odczytu pozwoli wykryć usterkę. Być może przepełnienie bufora spowodowało
zmianę wartości jakiegoś licznika, który zostanie zaraz użyty do określenia
ilości pamięci do przydzielenia. Być może odpowiedni błąd zwróci dopiero
wywołanie malloc. Błąd logiczny popełniony kilka milionów instrukcji temu może
powodować, że selektor wyrażenia case nie ma już oczekiwanej wartości 1, 2
lub 3. W takim przypadku zostanie przypadek domyślny (to jeden z powodów,
dla których każde wyrażenie case/switch powinno zawierać klauzulę przypadku
domyślnego — chcemy wiedzieć, kiedy zdarzyło się „niemożliwe”).
Nietrudno wpaść w pułapkę postawy „to nie mogło się stać”. Większość z nas
napisała kiedyś kod, który nie sprawdzał, czy plik został prawidłowo zamknięty
lub czy wyrażenie trace zapisało to, czego oczekiwaliśmy. A jeśli wszystko
działało zgodnie z planem, najprawdopodobniej w ogóle nie musieliśmy tego
sprawdzać — odpowiedni kod i tak działał prawidłowo (przynajmniej w normalnych okolicznościach). Staramy się jednak kodować nasze rozwiązania
możliwie ostrożnie. Staramy się wychwytywać błędne wskaźniki, które w innych
częściach programu mogą powodować uszkodzenia stosu. Sprawdzamy poprawność wersji bibliotek współdzielonych, które rzeczywiście są ładowane przez nasze
systemy.
Każdy błąd jest źródłem informacji. Można, oczywiście, wmawiać sobie, że błąd
nie mógł mieć miejsca, i dalej ignorować zagrożenie. Pragmatyczni programiści
postępują jednak inaczej — powtarzają sobie, że skoro wystąpił błąd, musiało
zdarzyć się coś bardzo, bardzo złego. Nie zapomnij przeczytać tego przeklętego
komunikatu o błędzie (patrz „Programista we wrogim świecie” w rozdziale 3.).
4337ebf6db5c7cc89e4173803ef3875a
4
144 
Rozdział 4.
Pragmatyczna paranoja
Złów i wypuść to dobra praktyka
podczas wędkowania
Niektórzy programiści uznają za dobry styl przechwytywanie wszystkich możliwych wyjątków i ponowne ich zgłaszanie po wyświetleniu jakiegoś komunikatu.
Ich kod jest pełen konstrukcji podobnych do poniższej (gdzie pojedyncza instrukcja raise ponownie zgłasza bieżący wyjątek):
try do
add_score_to_board(score);
rescue InvalidScore
Logger.error("Nie można dodać nieprawidłowej punktacji. Kończę pracę");
raise
rescue BoardServerDown
Logger.error("Nie można dodać punktacji: plansza nie odpowiada. Kończę
pracę");
raise
rescue StaleTransaction
Logger.error("Nie można dodać punktacj: transakcja zdezaktualizowana.
Kończę pracę");
raise
end
Oto jak pragmatyczny programista zapisałby taki fragment kodu:
add_score_to_board(score);
Wolimy taki styl z dwóch powodów. Po pierwsze kod aplikacji nie jest zaciemniony kodem obsługi błędów. Po drugie, i chyba ważniejsze, kod jest mniej
sprzężony. W pierwszym przykładzie musimy wymienić wszystkie wyjątki, jakie
może zgłosić metoda add_score_to_board. Jeśli autor tej metody doda kolejny
wyjątek, nasz kod będzie subtelnie nieaktualny. W bardziej pragmatycznej drugiej wersji, nowy wyjątek będzie propagowany automatycznie.
WSKAZÓWKA NR 38
Awarie powinny następować możliwie wcześnie.
Awaria, nie śmiecenie
Jedną z zalet możliwie szybkiego wykrywania problemów jest wcześniejsze
występowanie awarii. Co ciekawe, w wielu przypadkach awaria programu jest
najlepszym rozwiązaniem. Często jedyną alternatywą jest dalsza praca polegająca na zapisywaniu uszkodzonych danych w ważnej bazie danych lub wydaniu pralce po raz dwudziesty z rzędu polecenia wirowania.
Taka filozofia jest stosowana w językach Erlang i Elixir. Często cytowany Joe
Armstrong, twórca języka Erlang i autor książki Programming Erlang:Software
for a Concurrent World [Arm07], powiedział „Programowanie defensywne to strata
4337ebf6db5c7cc89e4173803ef3875a
4
Programowanie asertywne

145
czasu, jeśli wystąpi błąd w programie, nie przeciwdziałajcie jego awarii!”.
W wymienionych językach programy działają z uwzględnieniem możliwości
awarii, ale te awarie są zarządzane przez nadzorców. Nadzorca jest odpowiedzialny za uruchomienie kodu i wie, co zrobić w przypadku, gdy kod ulegnie
awarii — te działania mogą obejmować wykonywanie operacji porządkujących,
zrestartowanie programu i tak dalej. Co się stanie, gdy nastąpi awaria w kodzie
nadzorcy? Tym zdarzeniem zarządza jego nadzorca. Prowadzi to do konstrukcji
złożonej z drzew nadzorców. Technika ta jest bardzo skuteczna i jest jednym
z powodów, dla których języki Erlang i Eliksir są wykorzystywane w odpornych
na awarie systemach o wysokiej dostępności.
W innych środowiskach proste opuszczenie działającego programu nie byłoby,
oczywiście, wystarczającym rozwiązaniem. Program mógł przecież zajmować
zasoby, które w takim scenariuszu nie zostałyby zwolnione. Niewykluczone, że
program powinien zapisać jakieś komunikaty w dzienniku, zamknąć otwarte
transakcje lub obsłużyć komunikację z pozostałymi procesami.
Podstawowa zasada pozostaje jednak ta sama — kiedy nasz kod odkrywa, że
zdarzyło się coś, co wydawało się niemożliwe, dalsze działanie programu w tej
formie jest po prostu niemożliwe. Wszystko, co nasz program zrobi od tego
momentu, stanie się podejrzane. Należy więc przerwać wykonywanie programu
tak szybko, jak to możliwe.
Martwy program zwykle powoduje dużo mniejsze uszkodzenia niż kaleki
program.
Pokrewne podrozdziały
25
38

Temat 20,. „Debugowanie”.

Temat 23., „Projektowanie kontraktowe”.

Temat 25., „Programowanie asertywne”.

Temat 26., „Jak zrównoważyć zasoby”.

Temat 43., „Pozostań w bezpiecznym miejscu”.
Programowanie asertywne
W wyrzutach sumienia jest coś komfortowego. Kiedy winimy o coś sami
siebie, mamy poczucie, że nikt inny nie ma prawa winić nas za to samo.
Oscar Wilde, Portret Doriana Graya
Wiele wskazuje na istnienie pewnej mantry, którą musi sobie wpoić każdy
programista już na początku swojej kariery. To swoisty dogmat w świecie oprogramowania — zasada, którą od początku przygody z komputerami uczymy
się stosować dla wymagań, projektów, kodu, komentarzy i wszystkich innych
tworzonych rozwiązań. Brzmi ona tak:
4337ebf6db5c7cc89e4173803ef3875a
4
146 
Rozdział 4.
Pragmatyczna paranoja
TO NIGDY SIĘ NIE WYDARZY…
„Skoro ta aplikacja nie będzie używana za granicą, po co mielibyśmy ją umiędzynarodawiać?”. „Licznik nie może mieć wartości ujemnej”, „Logowanie nie
może zawieść”.
Pragmatyczny programista nie może sobie pozwolić na podobną postawę, szczególnie podczas kodowania.
WSKAZÓWKA NR 39
Jeśli coś nie może się zdarzyć, należy użyć asercji do zagwarantowania,
że rzeczywiście się nie zdarzy.
Kiedy tylko odkryjemy, że nasza postawa niebezpiecznie zbliża się do filozofii
„to nigdy nie nastąpi”, powinniśmy zakodować rozwiązania weryfikujące ten fakt.
Najprostszym sposobem jest użycie asercji. W implementacji wielu języków
istnieje jakaś forma asercji sprawdzających warunki logiczne2. Takie sprawdzenia bywają wprost bezcenne. Jeśli parametr lub wynik nigdy nie powinien mieć
wartości NULL, należy to sprawdzić:
assert(result != NULL);
W implementacji w Javie można (i należy) dodać opisowy ciąg:
assert result != null && result.size() > 0 : "Pusty wynik z XYZ";
Asercje przydają się także podczas sprawdzania działania algorytmów.
Wyobraźmy sobie, że napisaliśmy pewien sprytny algorytm sortujący w funkcji
o nazwie my_sort. Warto sprawdzić, czy rzeczywiście działa:
books=my_sort(find(*scifi*)
assert(is_sorted(books))
Asercji nie należy używać zamiast właściwych mechanizmów obsługi błędów.
Asercja służą do sprawdzania zdarzeń, które nigdy nie powinny mieć miejsca —
nie chcemy przecież pisać kodu podobnego do poniższego przykładu:
puts("Wpisz 'T' lub 'N': ");
ans = gets[0] # pobranie pierwszego znaku odpowiedzi
assert((ch == 'T') || (ch == 'N'));
# Kiepski pomysł!
To, że wiele dostępnych makr assert kończy program w momencie napotkania
niezgodności z jakąś asercją, nie oznacza jeszcze, że podobnie powinny działać
nasze wersje. Jeśli musimy zwolnić zasoby, nasza asercja powinna wygenerować wyjątek, wykonać skok do punktu wyjścia i wywołać mechanizm obsługi
2
W C i C ++ są one zazwyczaj zaimplementowane jako makra. W Javie asercje są
domyślnie wyłączone. Aby je włączyć, należy wywołać maszynę wirtualną Javy z flagą
-enableassertions.
4337ebf6db5c7cc89e4173803ef3875a
4
Programowanie asertywne

147
błędów. Musimy tylko upewnić się, że kod wykonywany w tych krytycznych
milisekundach nie korzysta z informacji, które przed momentem doprowadziły
do błędu niezgodności z asercją.
Asercje i skutki uboczne
To żenujące, gdy kod dodany w celu wykrywania błędów w rzeczywistości
tworzy nowe błędy. Może się to zdarzyć w przypadku asercji, jeśli sprawdzanie
warunku ma skutki uboczne. Na przykład zastosowanie kodu takiego jak poniżej to zły pomysł:
while (iter.hasMoreElements()) {
assert(iter.nextElement() != null);
Object obj = iter.nextElement();
// ....
}
Wywołanie .nextElement() wewnątrz asercji ma skutek uboczny polegający na
przeniesieniu iteratora poza pobierany element. Z tego powodu pętla przetworzy
jedynie połowę elementów w kolekcji. Lepiej byłoby napisać następujący kod:
while (iter.hasMoreElements()) {
Object obj = iter.nextElement();
assert(obj != null);
// ....
}
Ten problem to rodzaj błędu typu Heisenbug3 — debugowanie, które zmienia
zachowanie debugowanego systemu.
(Uważamy również, że w dzisiejszych czasach, kiedy w większości języków jest
dostępna przyzwoita obsługa iterowania kolekcji z wykorzystaniem funkcji,
stosowanie tego rodzaju jawnej pętli jest niepotrzebne i w złym stylu).
Pozostaw asercje włączone
Istnieje dość powszechna, całkowicie błędna opinia na temat asercji. Brzmi ona
mniej więcej tak:
Asercje stanowią pewne dodatkowe obciążenie dla kodu. Ponieważ sprawdzają zjawiska, które nigdy nie powinny mieć miejsca, są wyzwalane tylko
przez błędy w kodzie. W tej sytuacji po przetestowaniu i dostarczeniu kodu
asercje stają się niepotrzebne i jako takie powinny być wyłączone, aby
przyspieszyć działanie kodu. Asercje to w istocie mechanizmy debugowania kodu.
3
http://www.eps.mcgill.ca/jargon/jargon.html#heisenbug
4337ebf6db5c7cc89e4173803ef3875a
4
148 
Rozdział 4.
Pragmatyczna paranoja
Takie rozumowanie opiera się na dwóch zupełnie nieuprawnionych założeniach.
Po pierwsze, przyjmuje się, że testy pozwalają odnaleźć wszystkie błędy. W rzeczywistości w przypadku jakiegokolwiek złożonego programu jest mało prawdopodobne, by udało się przetestować choćby niewielki odsetek permutacji, które
mogą wystąpić w kodzie. Po drugie, optymiści szerzący tę plotkę zdają się zapominać, że nasz program działa w niebezpiecznym świecie. W trakcie testów
szczury prawdopodobnie nie będą podgryzały kabla sieciowego, nikt nie będzie
grał w grę zajmującą wszystkie zasoby pamięciowe, a pliki dzienników nie
zapełnią twardego dysku. Wszystkie te zdarzenia mogą jednak mieć miejsce po
wdrożeniu programu w środowisku produkcyjnym. Pierwszą linią obrony jest
sprawdzanie kodu pod kątem wszelkich możliwych błędów; drugą linią jest
stosowanie asercji, które mają na celu wykrywanie błędów przeoczonych podczas testów.
Wyłączenie asercji w momencie przekazywania programu do środowiska produkcyjnego jest jak spacer po wysoko zawieszonej linie bez zabezpieczenia (uzasadniony tym, że podczas prób udało się to raz zrobić). Taki krok, oczywiście,
niesie ze sobą sporo dramaturgii, ale rodzinie śmiałka trudno byłoby uzyskać
choćby grosz z ubezpieczenia na życie.
Nawet jeśli rzeczywiście borykamy się z problemem niedostatecznej wydajności,
powinniśmy wyłączyć tylko te asercje, które rzeczywiście powodują opóźnienia.
Przytoczony powyżej przykład algorytmu sortującego istotnie może być krytycznym elementem aplikacji i jako taki może wymagać naprawdę szybkiego działania. Dodanie kodu weryfikującego powoduje dodatkowe przetworzenie danych,
co w pewnych okolicznościach może być nie do zaakceptowania. Warto wówczas
ustawić konkretną asercję jako opcjonalną, pozostawiając pozostałe asercje na
swoich miejscach.
Używaj asercji w kodzie produkcyjnym i wygraj duże pieniądze
Były sąsiad Andy’ego założył mały startup zajmujący się produkcją urządzeń sieciowych. Jednym z sekretów sukcesu przedsięwzięcia była decyzja o pozostawieniu asercji
w kodzie w wersjach produkcyjnych. Te asercje miały na celu zgłaszanie wszystkich
istotnych danych prowadzących do awarii i były prezentowane końcowym użytkownikom za pośrednictwem estetycznego interfejsu użytkownika. Ten poziom
informacji zwrotnej od użytkowników w rzeczywistych warunkach pozwolił programistom załatać luki i usunąć niejasne, trudne do zreprodukowania błędy, czego
skutkiem było powstanie niezwykle stabilnego, odpornego na błędy oprogramowania.
Ta mała, nieznana firma wyprodukowała tak solidny produkt, że wkrótce został on
sprzedany za setki milionów dolarów.
Warto o tym pamiętać.
4337ebf6db5c7cc89e4173803ef3875a
4
Jak zrównoważyć zasoby
 149
Ćwiczenia
16. Krótki test z wiedzy o otaczającym nas świecie. Które z tych „niemożliwych”
zjawisk mogą mieć miejsce?

Miesiąc składający się z mniej niż 28 dni.
Kod błędu z wywołania systemowego (brak dostępu do bieżącego katalogu).
W języku C++: a = 2; b = 3; ale (a + b) nie jest równe 5.
Trójkąt, którego suma kątów wewnętrznych jest różna od 180º.
Minuta, która nie ma 60 sekund.

(a + 1) <= a




Pokrewne podrozdziały
26
39

Temat 23., „Projektowanie kontraktowe”.

Temat 24., „Martwe programy nie kłamią”.

Temat 42., „Testowanie na podstawie właściwości”.

Temat 43., „Pozostań w bezpiecznym miejscu”.
Jak zrównoważyć zasoby
Zapalenie świecy powoduje rzucanie cienia.
Ursula K. Le Guin, A Wizard of Earthsea
Wszyscy przy okazji kodowania musimy zarządzać zasobami: pamięcią, transakcjami, wątkami, plikami, licznikami czasu — słowem, wszystkim, co jest
dostępne w ograniczonych ilościach. Przez większość czasu poziom wykorzystania zasobów jest zgodny z przewidywalnym wzorcem — przydzielamy zasób,
używamy go i zwalniamy.
Wielu programistów nie postępuje jednak według spójnego planu przydzielania
i późniejszego zwalniania zasobów. Warto przy tej okazji zaproponować prostą
wskazówkę:
WSKAZÓWKA NR 40
Należy kończyć to, co się zaczyna.
W większości sytuacji stosowanie tej wskazówki nie stanowi żadnego problemu.
Oznacza to tylko tyle, że procedura lub obiekt, które przydzielają jakiś zasób,
powinny odpowiadać także za jego zwolnienie. Sprawdźmy teraz, jak to działa
w praktyce, analizując przykład kiepskiego kodu — aplikacji otwierającej plik,
odczytującej informacje o kliencie, aktualizującej jedno pole i zapisującej wynik.
Aby uprościć ten przykład, wyeliminowaliśmy mechanizmy obsługi błędów.
4337ebf6db5c7cc89e4173803ef3875a
4
150 
Rozdział 4.
Pragmatyczna paranoja
def read_customer
@customer_file = File.open(@name + ".rec", "r+")
@balance
= BigDecimal(@customer_file.gets)
end
def write_customer
@customer_file.rewind
@customer_file.puts @balance.to_s
@customer_file.close
end
def update_customer(transaction_amount)
read_customer
@balance = @balance.add(transaction_amount,2)
write_customer
end
Na pierwszy rzut oka procedura update_customer sprawia wrażenie w pełni prawidłowej. Wydaje się, że poprawnie implementuje interesującą nas logikę —
odczytuje rekord, aktualizuje saldo i zapisuje rekord z powrotem w pliku.
Okazuje się jednak, że za tym starannym kodem kryje się poważny problem.
Procedury read_customer i write_customer są ze sobą ściśle powiązane4 — obie
korzystają z tej samej zmiennej globalnej customer_file. Procedura read_customer
odczytuje plik i zapisuje wskaźnik do tego pliku w zmiennej customer_file,
natomiast procedura write_customer używa tej referencji do zamknięcia pliku,
kiedy nie jest już potrzebny. Wspomniana zmienna globalna nawet nie pojawia
się w kodzie procedury update_customer.
Dlaczego to rozwiązanie jest złe? Przeanalizujmy scenariusz, w którym pechowy
programista odpowiedzialny za konserwację dowiaduje się o zmianie specyfikacji — od tej pory saldo powinno być aktualizowane tylko wtedy, gdy nowa
wartość nie jest ujemna. Przegląda więc kod źródłowy i zmienia procedurę
update_customer:
def update_customer(transaction_amount)
read_customer
if (transaction_amount >= 0.00)
@balance = @balance.add(transaction_amount,2)
write_customer
end
end
Testy nie wykazały niczego niepokojącego. Kiedy jednak kod trafił do środowiska produkcyjnego, już po kilku godzinach przestał działać, wyświetlając błąd
o zbyt dużej liczbie otwartych plików. Ponieważ procedura write_customer w pewnych sytuacjach nie jest wywoływana, plik nie jest wówczas zamykany.
Zaimplementowanie obsługi tego specjalnego przypadku w procedurze update_
customer byłoby wyjątkowo niepożądanym rozwiązaniem:
4
Szczegółowe omówienie zagrożeń wynikających z tego rodzaju związków w kodzie można
znaleźć w temacie 28., „Eliminowanie sprzężeń”.
4337ebf6db5c7cc89e4173803ef3875a
4
Jak zrównoważyć zasoby
 151
def update_customer(transaction_amount)
read_customer
if (transaction_amount >= 0.00)
@balance += BigDecimal(transaction_amount, 2)
write_customer
else
@customer_file.close # Zły pomysł!
end
end
W ten sposób można co prawda usunąć samą usterkę (plik będzie teraz zamykany niezależnie od nowego salda), jednak poprawka oznacza też, że już trzy
procedury są ze sobą powiązane za pośrednictwem zmiennej globalnej customer_
file. W ten sposób wpadamy w pułapkę — jeśli zachowamy ten kurs, cały kod
będzie narażony na gwałtowny upadek. Ten kod nie jest zrównoważony!
Zasada kończenia tego, co się zaczęło, mówi nam, że (w idealnych warunkach)
procedury, które przydzielają jakiś zasób, powinny ten zasób także zwalniać.
Możemy wprowadzić tę zasadę w życie, dokonując nieznacznej refaktoryzacji
analizowanego kodu:
def read_customer(file)
@balance=BigDecimal(file.gets)
end
def write_customer(file)
file.rewind
file.puts @balance.to_s
end
def update_customer(transaction_amount)
file=File.open(@name + ".rec", "r+")
read_customer(file)
@balance = @balance.add(transaction_amount,2)
write_customer(file)
file.close
end
# >-#
|
#
|
#
|
# <--
Zamiast przechowywania referencji do pliku zmieniliśmy kod w taki sposób,
aby referencja ta była przekazywana jako parametr5. Tym razem cała odpowiedzialność za przetwarzany plik spada na procedurę update_customer. Wspomniana
procedura nie tylko otwiera, ale też (po skończeniu pracy) zamyka ten plik przed
zwróceniem sterowania. Procedura równoważy użycie pliku — operacje otwierania i zamykania pliku znajdują się w tym samym miejscu i wydaje się, że dla
każdej operacji otwierania istnieje odpowiednia operacja zamykania. Refaktoryzacja pozwoliła też usunąć niepożądaną zmienną globalną.
Istnieje także niewielkie, ale ważne usprawnienie, które możemy wprowadzić
do tego kodu. W wielu współczesnych językach można ograniczyć czas życia
zasobu do jakiegoś zamkniętego bloku. W Ruby dostępna jest odmiana operacji otwierania pliku, w której przekazujemy referencję do otwartego pliku do
bloku kodu. Poniżej pokazano ten mechanizm pomiędzy instrukcjami do a end:
5
Zobacz wskazówkę „Język X nie obsługuje potoków” w rozdziale 5.
4337ebf6db5c7cc89e4173803ef3875a
4
152 
Rozdział 4.
Pragmatyczna paranoja
def update_customer(transaction_amount)
File.open(@name + ".rec", "r+") do |file|
read_customer(file)
@balance = @balance.add(transaction_amount,2)
write_customer(file)
end
end
# >-#
|
#
|
#
|
# <--
W tym przypadku na końcu bloku zmienna file wykracza poza zakres,
a zewnętrzny plik jest zamykany. Kropka. Nie trzeba pamiętać o zamykaniu
pliku i zwalnianiu źródła, to dzieje się automatycznie i jest zagwarantowane.
W przypadku wątpliwości zawsze opłaca się ograniczyć zakres.
WSKAZÓWKA NR 41
Korzystaj z lokalnego zasięgu.
Zagnieżdżanie przydzieleń
Podstawowy wzorzec przydzielania zasobów można rozszerzyć na potrzeby procedur, które potrzebują więcej niż jednego zasobu jednocześnie. Wystarczy
postępować według dwóch dodatkowych sugestii:

Należy zwalniać zasoby w kolejności odwrotnej, niż są przydzielane. W ten
sposób unikniemy zjawiska osieroconych zasobów, jeśli jeden zasób zawiera
odwołania do innego.

W przypadku przydzielania tego samego zbioru zasobów w różnych miejscach kodu zawsze należy przydzielać w tej samej kolejności. W ten sposób można ograniczyć ryzyko występowania zakleszczeń. (Jeśli proces
A dysponuje zasobem 1. i zaraz zażąda zasobu 2., natomiast proces
B dysponuje zasobem 2. i próbuje uzyskać zasób 1., oba procesy będą czekały na żądane zasoby w nieskończoność).
To, jakiego rodzaju zasobów używamy (transakcji, pamięci, plików, wątków,
okien itp.), nie ma znaczenia — podstawowa zasada pozostaje niezmienna: ktokolwiek przydziela zasób, powinien odpowiadać także za jego zwolnienie. Okazuje się jednak, że w niektórych językach można ten model jeszcze rozwinąć.
Obiekty i wyjątki
Równowaga pomiędzy przydzieleniami i zwolnieniami przypomina trochę model
konstruktorów i destruktorów klas. Klasa reprezentuje pewien zasób, konstruktor daje nam dostęp do obiektu typu tego zasobu, a destruktor usuwa ten obiekt
z naszego zasięgu.
4337ebf6db5c7cc89e4173803ef3875a
4
Jak zrównoważyć zasoby
 153
Równoważenie w czasie
W tym podrozdziale przyglądamy się głównie efemerycznym zasobom wykorzystywanym przez uruchomiony proces. Warto jednak zastanowić się, jaki jeszcze inny
bałagan możemy pozostawiać.
Na przykład, w jaki sposób są obsługiwane pliki logów? Kiedy tworzysz dane, zużywasz pamięć masową. Czy istnieje mechanizm rotacji logów i ich porządkowania?
A co z nieoficjalnymi plikami debugowania, które pozostawiasz? Jeśli dodajesz rekordy
logowania do bazy danych, to czy istnieje proces zarządzający ich dezaktualizacją?
W przypadku wszystkiego, co tworzysz, a co zajmuje skończony zasób, należy zastanowić się nad sposobem równoważenia tego zasobu.
Czy jest coś jeszcze, o czym zapomniałeś?
Jeśli programujemy w języku obiektowym, możemy rozważyć rozwiązanie polegające na zamykaniu zasobów w klasach. Za każdym razem, gdy będziemy
potrzebowali określonego typu zasobu, utworzymy obiekt odpowiedniej klasy.
Kiedy obiekt wychodzi poza zasięg naszego kodu lub jest zwalniany przez
mechanizm odzyskiwania, destruktor tego obiektu automatycznie zwalnia opakowany zasób.
Proponowane rozwiązanie jest szczególnie korzystne podczas pracy w takich
językach, w których wyjątki mogą wpływać na proces zwalniania zasobów.
Równoważenie i wyjątki
Języki z obsługą wyjątków mogą nieco utrudniać konsekwentne zwalnianie
zasobów. Jak w przypadku wygenerowania wyjątku zagwarantować, że wszystkie zasoby przydzielone przed tym wyjątkiem zostaną prawidłowo zwolnione?
Odpowiedź po części zależy od języka programowania. Zwykle mamy do wyboru
dwie opcje:
1. Użycie zakresu zmiennych (na przykład zmiennych stosu w językach C++
lub Rust).
2. Skorzystanie z klauzuli finally w bloku try...catch.
Zgodnie ze zwykłymi zasadami dotyczącymi zakresów w takich językach jak
C++ lub Rust, pamięć zmiennej zostanie odzyskana, gdy zmienna wyjdzie poza
zakres za pośrednictwem instrukcji return, po wyjściu z bloku lub po zgłoszeniu wyjątku. Można jednak również „posprzątać” po zewnętrznych zasobach
w destruktorze zmiennej. W poniższym przykładzie, przy wyjściu z zakresu
zmienna Rust o nazwie accounts automatycznie zamknie skojarzony z nią plik:
{
let mut accounts = File::open("mydata.txt")?; // >-// korzystaj ze zmiennej 'accounts'
4337ebf6db5c7cc89e4173803ef3875a
//
|
4
154 
Rozdział 4.
Pragmatyczna paranoja
...
//
|
}
// <-// zmienna accounts jest teraz poza zakresem, co powoduje
// automatyczne zamknięcie pliku
Inną opcją, jeśli język ją obsługuje, jest użycie klauzuli finally. Gwarantuje
ona uruchomienie zawartego wewnątrz niej bloku kodu niezależnie od tego, czy
w bloku try...catch zostanie zgłoszony wyjątek, czy nie:
try
// działania, które mogą spowodować wyjątki
catch
// zgłoszono wyjątek
finally
// sprzątanie niezależnie od tego, czy wystąpił wyjątek
Istnieje jednak pewien haczyk.
Antywzorzec obsługi wyjątków
Często spotykamy programistów, którzy piszą kod w następującej postaci:
begin
thing = allocate_resource()
process(thing)
finally
deallocate(thing)
end
Czy widzisz w tym kodzie coś złego?
Co się stanie, jeśli przydzielenie zasobu nie powiedzie się i spowoduje zgłoszenie
wyjątku? Wyjątek zostanie przechwycony w klauzuli finally, gdzie nastąpi próba
zwolnienia zasobu, który nigdy nie został przydzielony.
Prawidłowy wzorzec obsługi zwolnienia zasobu w środowisku z wyjątkami jest
następujący:
thing = allocate_resource()
begin
process(thing)
finally
deallocate(thing)
end
Kiedy nie można zrównoważyć zasobów
W pewnych sytuacjach stosowanie podstawowego wzorca przydzielania i zwalniania zasobów jest po prostu niemożliwe. Problem najczęściej dotyczy programów używających dynamicznych struktur danych. Jedna procedura przydziela
obszar pamięci i wiąże go z jakąś większą strukturą, gdzie może być używany
przez pewien czas.
4337ebf6db5c7cc89e4173803ef3875a
4
Jak zrównoważyć zasoby
 155
Cała sztuka polega wówczas na ustanowieniu jakiegoś semantycznego niezmiennika na potrzeby alokacji pamięci. Musimy zdecydować, kto odpowiada
za dane w skojarzonej strukturze danych. Co powinno się stać w chwili
zwolnienia struktury najwyższego poziomu? Mamy do wyboru trzy główne
rozwiązania:

Struktura najwyższego poziomu dodatkowo odpowiada za zwalnianie wszelkich zawartych w sobie struktur podrzędnych. Same struktury podrzędne
rekurencyjnie usuwają zawierane dane itd.

Struktura najwyższego poziomu jest po prostu zwalniana. Wszystkie struktury, które wcześniej były wskazywane przez tę strukturę (i które nie są
przedmiotem innych odwołań), stają się sierotami.

Struktura najwyższego poziomu odmawia zwolnienia samej siebie, jeśli
zawiera w sobie jakieś struktury podrzędne.
Wybór zależy od okoliczności, w których stosujemy poszczególne struktury
danych. Musimy jednak podjąć taką decyzję dla każdej struktury i konsekwentnie implementować wybrane rozwiązanie w całym kodzie. Implementacja każdej
z tych opcji w języku proceduralnym (np. w języku C) jest o tyle trudna, że
same struktury danych nie są aktywne. W takich przypadkach zalecamy napisanie dla każdej ważnej struktury danych osobnego modułu, który będzie
definiował standardowe mechanizmy przydzielania i zwalniania tej struktury.
(Moduł może dodatkowo udostępniać takie mechanizmy jak wyświetlanie
komunikatów diagnostycznych, serializacja, deserializacja czy przeszukiwanie).
Sprawdzanie równowagi
Ponieważ pragmatyczni programiści nie ufają nikomu, nawet sobie, wydaje
się, że zawsze dobrym rozwiązaniem jest budowa kodu sprawdzającego, czy
zasoby rzeczywiście są prawidłowo zwalniane. W przypadku większości aplikacji
weryfikacja równowagi sprowadza się do tworzenia opakowań dla wszystkich
typów zasobów i stosowania tych opakowań do śledzenia wszystkich zdarzeń
przydzielania i zwalniania. Na pewnych punktach w kodzie logika programu
wymusza określone stany zasobów — wspomniane opakowania mogą nam
posłużyć do sprawdzania tych stanów. Na przykład długo działający program,
który obsługuje żądania, prawdopodobnie będzie zawierał jeden punkt na
początku swojej głównej pętli przetwarzającej, w którym czeka na przyjście
kolejnego żądania. To dobre miejsce dla mechanizmu sprawdzającego, czy poziom
wykorzystania zasobów nie przekroczył akceptowanego progu od poprzedniej
iteracji pętli.
Na niższym, ale nie mniej ważnym poziomie możemy zainwestować w narzędzia,
które będą (między innymi) monitorowały nasze programy pod kątem ewentualnych wycieków pamięci.
4337ebf6db5c7cc89e4173803ef3875a
4
156 
Rozdział 4.
Pragmatyczna paranoja
Pokrewne podrozdziały

Temat 24., „Martwe programy nie kłamią”.

Temat 30., „Programowanie transformacyjne”.

Temat 33., „Eliminowanie związków czasowych”.
Wyzwania

Mimo że nie istnieją w pełni niezawodne sposoby zapewniania zwalniania
wszystkich zasobów, niektóre techniki projektowe (pod warunkiem konsekwentnego stosowania) mogą nam bardzo pomóc. W powyższym tekście
omówiliśmy metodę ustanawiania niezmiennika semantycznego dla najważniejszych struktur danych jako sposób podejmowania właściwych
decyzji o zwalnianiu pamięci. Warto zastanowić się, jak rozwiązania zaproponowane w podrozdziale „Projektowanie kontraktowe” we wcześniejszej
części tego rozdziału mogłyby pomóc w udoskonaleniu tego modelu.
Ćwiczenia
17. Niektórzy programiści języków C i C++ konsekwentnie przypisują wskaźnikom wartość NULL zaraz po zwolnieniu wskazywanych obszarów pamięci.
Dlaczego takie rozwiązanie jest korzystne?
18. Niektórzy programiści Javy konsekwentnie przypisują zmiennym obiektów
wartość NULL zaraz po zakończeniu korzystania z tych obiektów. Dlaczego
takie rozwiązanie jest korzystne?
27
40
Nie prześcigaj swoich świateł
Trudno jest prognozować, zwłaszcza jeśli chodzi o przyszłość.
— Lawrence „Yogi” Berra, na podstawie duńskiego przysłowia
Jest późna, ciemna noc, leje deszcz. Dwumiejscowy sportowy wóz jedzie krętą,
wąską górską drogą, ledwo pokonując zakręty. Dojeżdża do „agrafki”, której
kierowca nie zauważa. Uderza w skromną barierkę i spada w przepaść. Na
miejsce wypadku przybywa policja. Starszy oficer smutno kręci głową. „Chyba
przegonił swoje światła”.
Czy to znaczy, że ten samochód jechał z prędkością większą od prędkości światła? Nie. Tej prędkości nie da się przekroczyć. Funkcjonariusz mówił o zdolności
kierowcy do zatrzymania się lub zapanowania nad pojazdem na dystansie
oświetlanym przez światła.
Reflektory mają pewien ograniczony zakres określany jako odległość rzutu
(ang. throw distance). Poza tym punktem, światło jest zbyt rozproszone, aby
mogło być skuteczne. Ponadto reflektory świecą tylko w linii prostej i nie oświe-
4337ebf6db5c7cc89e4173803ef3875a
4
Nie prześcigaj swoich świateł

157
tlają niczego, co znajduje się poza osią, na przykład zakrętów, wzgórz lub fałd
na drodze. Według National Highway Traffic Safety Administration, średnia
odległość oświetlana przez światła mijania wynosi około 50 metrów. Niestety
droga hamowania przy prędkości 60 km/h wynosi około 60 metrów, a przy
prędkości 100 km/h już około 140 metrów6. Zatem rzeczywiście bardzo łatwo
można prześcignąć swoje światła.
W procesie wytwarzania oprogramowania jest podobnie. Nasze „reflektory” mają
ograniczenia. Nie widzimy zbyt daleko w przyszłość, a im bardziej spoglądamy
poza oś, tym jest ciemniej. W związku z tym pragmatyczni programiści konsekwentnie stosują zasadę:
WSKAZÓWKA NR 42
Zawsze poruszaj się małymi krokami.
Zawsze należy robić małe, przemyślane kroki, a przed podjęciem kolejnych
sprawdzać informacje zwrotne i odpowiednio dostosowywać swoje działania.
Tempo, w jakim otrzymujemy informacje zwrotne, to nasze ograniczenie prędkości. Nigdy nie rób zbyt dużego kroku lub nie wykonuj zadania, które jest
„zbyt duże”.
Co dokładnie mamy na myśli mówiąc o informacjach zwrotnych? Wszystko, co
w niezależny sposób potwierdza słuszność lub brak słuszności Twoich działań.
Na przykład:

Wyniki w REPL dostarczają informację zwrotną dotyczącą rozumienia
interfejsów API i algorytmów.

Testy jednostkowe dostarczają informację zwrotną na temat ostatnich
zmian w kodzie.

Wersje demo dla użytkowników i rozmowy z nimi dostarczają informację
zwrotną dotyczącą funkcji i użyteczności.
Jakie zadanie jest za duże? Każde, które wymaga „wróżenia przyszłości”. Tak
samo, jak reflektory samochodu mają ograniczony rzut, tak my możemy przewidywać przyszłość tylko o jeden bądź dwa kroki — być może tylko kilka godzin
lub dni naprzód. Poza tym punktem łatwo możemy przejść od uzasadnionej wiedzą prognozy do dzikiej spekulacji. Zmierzasz w kierunku wróżenia przyszłości,
kiedy wykonujesz jedną z wymienionych poniżej czynności:
6

Szacujesz daty ukończenia zadania oddalone o wiele miesięcy w przyszłość.

Planujesz projekt pod kątem przyszłej możliwości utrzymania lub rozszerzania.

Zgadujesz przyszłe potrzeby użytkowników.

Zgadujesz przyszłe dostępne technologie.
Według NHTSA, Droga zatrzymania = Odległość reakcji + Droga hamowania, przy założeniu czasu reakcji około 1,5 s oraz hamowania w tempie 5 m/s².
4337ebf6db5c7cc89e4173803ef3875a
4
158 
Rozdział 4.
Pragmatyczna paranoja
Słyszymy Twoje protesty: czy nie powinniśmy projektować oprogramowania pod
kątem możliwości utrzymywania go w przyszłości? Tak, ale tylko do pewnego
momentu: tylko tak daleko, jak jesteśmy w stanie zobaczyć. Im większa konieczność przewidywania przyszłości, tym większe ryzyko popełnienia błędu. Zamiast
tracić energię na projektowanie nieznanej przyszłości, lepiej projektować kod
w taki sposób, aby można go było łatwo zastąpić. Zaprojektuj kod tak, aby
można go było łatwo wyrzucić i zastąpić czymś, co lepiej nadaje się do wykonania zadania. Projektowanie kodu w taki sposób, by był wymienny, pomoże
również zapewnić lepszą spójność, luźniejsze sprzężenia oraz ściślejsze stosowanie zasady DRY, co w konsekwencji doprowadzi do lepszego projektu
w sensie ogólnym.
Nawet jeśli masz poczucie, że przyszłość jest pewna, zawsze istnieje ryzyko, że
za rogiem czeka jakiś czarny łabędź.
Czarne łabędzie
W książce The Black Swan: The Impact of the Highly Improbable [Tal10]7 Nassim
Nicholas Taleb założył, że wszystkie istotne wydarzenia w historii wynikały
z powodu specyficznych, trudnych do przewidzenia i rzadkich zdarzeń, wykraczających poza sferę zwykłych oczekiwań. Te odbiegające od normy zdarzenia,
choć są statystycznie rzadkie, wywierają nieproporcjonalne skutki. Ponadto,
z powodu naszych własnych uprzedzeń poznawczych, jesteśmy ślepi na zmiany,
które powoli wkradają się do naszej pracy (patrz „Zupa z kamieni i gotowane
żaby”).
Mniej więcej w czasie, kiedy zostało opublikowane pierwsze wydanie „Pragmatycznego programisty”, w czasopismach komputerowych i na forach internetowych toczyła się debata dotycząca palącego problemu: „Kto wygra wojnę
o dominację wśród bibliotek GUI dla środowisk komputerów desktop: Motif,
czy OpenLook?”8. To było źle postawione pytanie. Być może nigdy nie słyszeliście o tych technologiach, ponieważ żadna z nich nie „wygrała”, a rynek szybko
zdominowały aplikacje webowe.
WSKAZÓWKA NR 43
Unikaj wróżenia przyszłości.
7
Wydanie polskie: „Czarny łabędź. Jak nieprzewidywalne zdarzenia rządzą naszym życiem”,
Wydawnictwo Zysk i S-ka, 2020.
8
Motif i OpenLook były standardami GUI dla uniksowych stacji roboczych bazujących
na systemie X-Window.
4337ebf6db5c7cc89e4173803ef3875a
4
Nie prześcigaj swoich świateł

159
W większości przypadków jutro bardzo przypomina dzisiaj. Nie należy jednak
na to liczyć.
Pokrewne podrozdziały

Temat 12., „Pociski smugowe”.

Temat 13., „Prototypy i karteczki samoprzylepne”.

Temat 40., „Refaktoryzacja”.

Temat 41., „Kod łatwy do testowania”.

Temat 48., „Istota zwinności”.

Temat 50., „Nie próbuj przecinać kokosów”.
4337ebf6db5c7cc89e4173803ef3875a
4
160 
Rozdział 4.
Pragmatyczna paranoja
4337ebf6db5c7cc89e4173803ef3875a
4
Rozdział 5.
Zegnij lub złam
Życie nie stoi w miejscu. To samo dotyczy pisanego przez nas kodu. Aby dotrzymać tempa galopującym zmianom, musimy robić, co w naszej mocy, aby pisany
kod był możliwie elastyczny i pozbawiony ścisłych związków. W przeciwnym razie
nasz kod szybko może okazać się nieaktualny lub zbyt kruchy do naprawy —
będzie nadawał się raczej do kosza niż do rozwijania z myślą o przyszłości.
W podrozdziale „Odwracalność” w rozdziale 2. omówiliśmy czynniki ryzyka
związane z nieodwracalnymi decyzjami. W tym rozdziale skoncentrujemy się na
sposobach podejmowania odwracalnych decyzji, tak aby nasz kod zachowywał
elastyczność i potencjał dostosowywania do wymogów niepewnego świata.
Musimy najpierw zająć się kwestią sprzężeń (ang. coupling), czyli zależności
łączących moduły kodu. Operacja eliminowania sprzężeń (ang. decoupling)
pokazuje, w jaki sposób można izolować odrębne pojęcia, co zmniejsza liczbę
sprzężeń.
Następnie zajmiemy się różnymi technikami, które można zastosować podczas
„Żonglerki realnym światem”. Przeanalizujemy cztery różne strategie, które
ułatwiają zarządzanie zdarzeniami i reagowanie na nie — kluczowy aspekt nowoczesnych aplikacji.
Tradycyjne typy kodu — proceduralny i obiektowy — mogą być zbyt mocno
sprzężone, aby mogły służyć naszym celom. W podrozdziale „Programowanie
transformacyjne” wykorzystamy bardziej elastyczny i czystszy styl zaoferowany przez potoki funkcji, które można stosować nawet wtedy, gdy nasz język
nie obsługuje ich bezpośrednio.
Stosowanie popularnego stylu obiektowego może zwabić nas w inną pułapkę.
Nie daj się w nią złapać, bo jeśli się tak stanie, będziesz zmuszony płacić wysoki
„Podatek od dziedziczenia”. Przeanalizujemy lepsze alternatywy zapewnienia
elastyczności kodu i łatwiejszego wprowadzania w nim zmian.
4337ebf6db5c7cc89e4173803ef3875a
4
162 
Rozdział 5.
Zegnij lub złam
Oczywistym sposobem zapewnienia większej elastyczności jest pisanie mniejszej ilości kodu. Modyfikowanie kodu otwiera możliwość wprowadzania nowych
błędów. W podrozdziale „Konfiguracja” wyjaśnimy, w jaki sposób całkowicie
przesunąć szczegóły z kodu do miejsca, w którym można je zmieniać w bezpieczniejszy i łatwiejszy sposób.
Wymienione techniki sprawią, że będziemy mogli pisać kod, który będzie się
zginał, ale się nie złamie.
28
36
Eliminowanie sprzężeń
Kiedy staramy się wybrać cokolwiek, zdajemy sobie sprawę, że jest
to połączone ze wszystkim we Wszechświecie.
John Muir, My First Summer in the Sierra
W temacie 8., „Istota dobrego projektu” w rozdziale 2. powiedzieliśmy, że dzięki
stosowaniu zasad dobrego projektu kod, który piszemy, będzie łatwiejszy do
modyfikowania. Sprzężenia są wrogiem zmian, ponieważ wiążą ze sobą elementy,
które muszą być zmieniane równolegle. To sprawia, że wprowadzanie zmian
staje się trudniejsze: albo trzeba poświęcić czas na znalezienie wszystkich
modułów, które wymagają zmian, albo na zastanawianie się, dlaczego niektóre
komponenty przestały działać, skoro zmieniliśmy „tylko jedną rzecz”, a nie
zmienialiśmy innych elementów.
Gdy projektujemy coś, co powinno być sztywne — na przykład most lub wieżę —
staramy się, aby poszczególne elementy były ze sobą sprzężone:
Dzięki istnieniu sprzężeń konstrukcja nabiera sztywności.
Porównajmy tę strukturę z tą, którą pokazano poniżej:
4337ebf6db5c7cc89e4173803ef3875a
4
Eliminowanie sprzężeń

163
Tutaj nie ma strukturalnej sztywności: kiedy zmieniają się poszczególne łącza,
inne się dostosowują.
Kiedy projektujesz most, chcesz, żeby utrzymał swój kształt — jego konstrukcja
powinna być sztywna. Ale kiedy projektujesz oprogramowanie, w którym ma być
zachowana możliwość wprowadzania zmian, chcesz czegoś dokładnie odwrotnego: ta konstrukcja powinna być elastyczna. Żeby to osiągnąć, poszczególne
komponenty powinny być powiązane z jak najmniejszą liczbą innych składników.
Na domiar złego sprzężenia są przechodnie: jeśli komponent A jest sprzężony
z B i C, B jest sprzężony z M i N, a C z X i Y, to w efekcie A jest sprzężony
z B, C, M, N, X i Y.
Oznacza to, że należy postępować zgodnie z prostą zasadą:
WSKAZÓWKA NR 44
Kod bez sprzężeń jest łatwiejszy do modyfikowania.
Zastanówmy się, co oznacza eliminowanie sprzężeń z kodu, wziąwszy pod
uwagę, że zwykle nie kodujemy z wykorzystaniem stalowych prętów i nitów?
W tym podrozdziale omówimy następujące zagadnienia:

Pociągi-wraki — łańcuchy wywołań metod.

Globalizacja — niebezpieczeństwa związane ze statycznym kodem.

Dziedziczenie — dlaczego tworzenie klas potomnych jest niebezpieczne.
Do pewnego stopnia powyższa lista jest sztuczna: sprzężenia mogą wystąpić
niemal zawsze, gdy dwa fragmenty kodu coś współdzielą, dlatego podczas lektury kolejnej części tego rozdziału zwróć uwagę na zaprezentowane wzorce, aby
móc je zastosować podczas kodowania. Należy także zwrócić uwagę na niektóre
popularne symptomy występowania sprzężeń:

Głupie zależności pomiędzy niepowiązanymi ze sobą modułami lub bibliotekami.

„Proste” zmiany w jednym module, które propagują się w systemie lub
przyczyniają się do awarii w innych jego miejscach.
4337ebf6db5c7cc89e4173803ef3875a
4
164 
Rozdział 5.
Zegnij lub złam

Programiści boją się wprowadzania zmian w kodzie, ponieważ nie są pewni,
na jakie elementy systemu może to mieć wpływ.

Spotkania, w których musi uczestniczyć szerokie grono osób, ponieważ nikt
nie jest pewien, na kogo zmiana będzie miała wpływ.
Pociągi-wraki
Wszyscy widzieliśmy (i prawdopodobnie pisaliśmy) kod następującej postaci:
public void applyDiscount(customer, order_id, discount) {
totals = customer
.orders
.find(order_id)
.getTotals();
totals.grandTotal = totals.grandTotal - discount;
totals.discount = discount;
}
Pobieramy referencję do zamówień za pośrednictwem obiektu klienta i wykorzystujemy tę referencję do znalezienia określonego zamówienia. Następnie
obliczamy dla tego zamówienia zbiór podsumowań. Korzystając z nich odejmujemy rabat od sumy zamówienia i jednocześnie aktualizujemy zamówienie stosując obliczoną zniżkę.
Ten fragment kodu przechodzi przez pięć poziomów abstrakcji — od klienta do
kwot zestawień. Ostatecznie kod najwyższego poziomu musi wiedzieć, że obiekt
klienta daje dostęp do zamówień. Z kolei zamówienia udostępniają metodę find
pobierającą identyfikator zamówienia i zwracającą obiekt zamówienia; ponadto
obiekt zamówienia zawiera obiekt podsumowań z getterami i seterami dla
podsumowań i rabatów. To bardzo dużo ukrytej wiedzy. Ale gorsze jest to, że,
aby ten kod mógł dalej działać w przyszłości, nie może się zmienić bardzo wiele
rzeczy. Wszystkie wagony w pociągu są ze sobą połączone, podobnie jak wszystkie metody i atrybuty we wraku pociągu.
Wyobraźmy sobie, że firma zdecydowała, że dla żadnego zamówienia nie można
udzielić rabatu w wysokości przekraczającej 40%. Gdzie należałoby umieścić
kod, który wymusza stosowanie tej zasady?
Można powiedzieć, że ten kod powinien znaleźć się w funkcji applyDiscount, którą
właśnie napisaliśmy. To z pewnością część odpowiedzi. Ale w przypadku kodu
w postaci takiej jak teraz, nie możemy powiedzieć, że to jest cała odpowiedź.
Dowolny fragment kodu, w dowolnym miejscu, może ustawiać pola w obiekcie
totals, a jeśli opiekun tego kodu nie dostanie informacji, to nie zapewni stosowania nowej reguły.
Jednym ze sposobów spojrzenia na ten kod jest pomyślenie o nim z punktu
widzenia odpowiedzialności. Z pewnością obiekt totals powinien być odpowiedzialny za zarządzanie podsumowaniami. A jednak nie jest za to odpowiedzialny:
w istocie jest jedynie zbiorem pól, które każdy może aktualizować i o które
każdy może pytać.
4337ebf6db5c7cc89e4173803ef3875a
4
Eliminowanie sprzężeń

165
Rozwiązaniem pokazanego problemu jest zastosowanie się do poniższej zasady:
WSKAZÓWKA NR 45
Mów. Nie pytaj.
Powyższa zasada mówi, że nie należy podejmować decyzji w oparciu o wewnętrzny
stan obiektu, a następnie aktualizować ten obiekt. Postępowanie w ten sposób
całkowicie niszczy korzyści wynikające z hermetyzacji i rozsiewa po kodzie
wiedzę związaną z implementacją. Zatem pierwszą poprawką do naszego wraku
pociągu jest przekazanie odpowiedzialności za rabaty do obiektu total:
public void applyDiscount(customer, order_id, discount) {
customer
.orders
.find(order_id)
.getTotals()
.applyDiscount(discount);
}
Ten sam rodzaj problemu TDA (od ang. tell, don’t ask — dosłownie: mów, nie
pytaj) mamy z obiektem klienta i jego zamówieniami: nie powinniśmy jednocześnie pobierać listy zleceń i ich przeszukiwać. Zamiast tego powinniśmy pobrać
potrzebne zamówienie bezpośrednio z obiektu klienta:
public void applyDiscount(customer, order_id, discount) {
customer
.findOrder(order_id)
.getTotals()
.applyDiscount(discount);
}
To samo dotyczy obiektu order i zapisanych w nim podsumowań. Dlaczego świat
zewnętrzny ma wiedzieć, że w implementacji obiektu reprezentującego zamówienie wykorzystano oddzielny obiekt do przechowywania podsumowań?
public void applyDiscount(customer, order_id, discount) {
customer
.findOrder(order_id)
.applyDiscount(discount);
}
W tym miejscu prawdopodobnie powinniśmy się zatrzymać.
W tym momencie można by pomyśleć, że zgodnie z zasadą TDA metodę apply
DiscountToOrder(order_id) należałoby dodać do obiektu klienta. Gdybyśmy niewolniczo stosowali tę zasadę, to tak by było.
Ale TDA nie jest prawem natury; jest to po prostu wzorzec, który pomaga nam
rozpoznawać problemy. W tym przypadku zdecydowaliśmy się na odsłonięcie
faktu, że klient ma zamówienia i że możemy znaleźć jedno z nich za pośrednictwem obiektu klienta. To jest pragmatyczna decyzja.
4337ebf6db5c7cc89e4173803ef3875a
4
166 
Rozdział 5.
Zegnij lub złam
W każdej aplikacji istnieją pewne pojęcia najwyższego poziomu, które są uniwersalne. W tej do tych pojęć należą klienci i zamówienia. Całkowite ukrywanie
zamówień wewnątrz obiektów klientów nie ma sensu — zamówienia żyją własnym życiem. Nie ma więc przeciwwskazań, aby stworzyć interfejs API, który
udostępnia obiekty zamówień.
Prawo Demeter
Często, gdy mówimy o sprzężeniach, powołujemy się na tzw. prawo Demeter
(ang. Law of Demeter — LoD). LoD to zbiór wskazówek1 spisanych pod koniec lat
osiemdziesiątych przez Iana Hollanda. Stworzył je, by pomóc programistom
pracującym w projekcie tworzyć funkcje czystsze i z mniejszą liczbą sprzężeń.
Prawo LoD mówi, ze metoda zdefiniowana w klasie C powinna wywoływać
wyłącznie:

inne metody egzemplarza klasy C,

jej parametry,

metody obiektów, które tworzy, zarówno na stosie, jak i na stercie,

zmienne globalne.
W pierwszym wydaniu tej książki poświęciliśmy trochę miejsca opisowi prawa
LoD. W ciągu minionych 20 lat ta róża trochę przekwitła. Teraz nie podoba nam
się klauzula „zmienne globalne” (ze względów, o których napiszemy w następnym podrozdziale). Odkryliśmy również, że trudno jest wykorzystać to prawo
w praktyce: jego stosowanie przypomina konieczność brania go pod uwagę przy
wywołaniu każdej metody.
Sama zasada jest jednak nadal wartościowa. Zalecamy nieco prostszą regułę,
która wyraża niemal to samo:
WSKAZÓWKA NR 46
Nie łącz wywołań metod w łańcuchy.
Gdy chcesz uzyskać dostęp do czegoś, staraj się nie korzystać z więcej niż
jednej kropki. Dostęp do czegoś obejmuje także przypadki, w których korzystamy
ze zmiennych pośrednich, tak jak w poniższym kodzie:
# To jest kiepski styl
amount = customer.orders.last().totals().amount;
# ten styl też nie jest dobry…
orders = customer.orders;
last = orders.last();
totals = last.totals();
amount = totals.amount;
1
Zatem w rzeczywistości nie jest to prawo, ale raczej dobry pomysł Demeter.
4337ebf6db5c7cc89e4173803ef3875a
4
Eliminowanie sprzężeń

167
Jest jeden ważny wyjątek od reguły jednej kropki: zasada nie ma zastosowania,
jeżeli prawdopodobieństwo zmian w komponentach wywoływanych w łańcuchu
jest małe. W praktyce wszystko, co jest w aplikacji, należy rozważać jako możliwe do zmiany. Wszystko, co znajduje się w zewnętrznych bibliotekach, należy
uznawać za ulotne — zwłaszcza wtedy, gdy dostawcy biblioteki często zmieniają
API pomiędzy kolejnymi wersjami. Z kolei biblioteki standardowe dostarczane
wraz z językiem można uznać za dość stabilne, zatem bez przeszkód można
pisać taki oto kod:
people
.sort_by {|person| person.age }
.first(10)
.map {| person | person.name }
Ten kod w Ruby działał kiedy pisaliśmy pierwsze wydanie tej książki 20 lat
temu i prawdopodobnie nadal będzie działać, gdy za jakiś czas znajdziemy się
w domu starców dla programistów (co może się zdarzyć lada dzień…).
Łańcuchy i potoki
W temacie 30., „Programowanie transformacyjne”, mówiliśmy o komponowaniu
funkcji w potoki. Te potoki przekształcają dane, przekazując je z jednej funkcji
do następnej. To nie jest to samo co pociągi-wraki wywołań metod, ponieważ
w przypadku potoków nie polegamy na ukrytych szczegółach implementacji.
Nie znaczy to, że potoki nie wprowadzają pewnych sprzężeń. Format danych
zwracanych przez jedną funkcję w potoku musi być zgodny z formatem akceptowanym przez następną.
Z naszego doświadczenia wynika, że ta forma sprzężenia jest znacznie mniejszą
barierą dla zmian w kodzie niż forma wprowadzana przez pociągi-wraki.
Zło globalizacji
Dane dostępne globalnie to podstępne źródło sprzężeń pomiędzy komponentami aplikacji. Każdy element globalnych danych działa tak, jakby do każdej
metody w aplikacji nagle został wprowadzony dodatkowy parametr — ostatecznie ten globalny element danych jest dostępny wewnątrz każdej metody.
Zmienne globalne sprzęgają kod z wielu powodów. Najbardziej oczywistym jest
fakt, że zmiana w implementacji globalnego elementu danych potencjalnie
wpływa na cały kod w systemie. Oczywiście w praktyce ten wpływ jest dość
ograniczony. Problem w istocie sprowadza się do konieczności zidentyfikowania
wszystkich miejsc, które trzeba zmienić.
Dane globalne tworzą również sprzężenia przeszkadzające w rozdzielaniu kodu.
Powszechnie znane są zalety możliwości wielokrotnego wykorzystywania kodu.
Z naszych doświadczeń wynika, że wielokrotne wykorzystywanie kodu prawdopodobnie nie powinno być głównym celem jego pisania, ale myślenie o możliwościach
4337ebf6db5c7cc89e4173803ef3875a
4
168 
Rozdział 5.
Zegnij lub złam
płynących ze stosowania takiego podejścia powinno być częścią praktyki kodowania. Kiedy staramy się tworzyć kod wielokrotnego użytku, definiujemy w nim
czytelne interfejsy, co pozwala oddzielić określony moduł od pozostałej części
kodu. To pozwala wyodrębnić metodę lub moduł bez konieczności dołączania
do nich innych elementów. Jeśli Twój kod wykorzystuje dane globalne, to
oddzielenie jednego modułu od reszty staje się trudne.
Problem ten jest widoczny podczas pisania testów jednostkowych kodu, który
korzysta z danych globalnych. W takiej sytuacji tylko po to, aby uruchomić test,
trzeba napisać mnóstwo kodu odpowiedzialnego za skonfigurowanie globalnego
środowiska.
WSKAZÓWKA NR 47
Unikaj globalnych danych.
Singletony to też dane globalne
W poprzednim punkcie zachowywaliśmy ostrożność — mówiliśmy o globalnych
danych, a nie o globalnych zmiennych. To dlatego, że często spotykamy ludzi,
którzy mówią nam „Popatrzcie! Nie mamy zmiennych globalnych. Wszystko opakowaliśmy jako dane instancji w obiekcie singletona lub globalnym module”.
Przeczytaj to jeszcze raz. Tym razem powoli. Jeśli masz w aplikacji singleton
zawierający zbiór zmiennych egzemplarza, to nadal są to dane globalne. Mają
jedynie bardziej złożoną nazwę.
Programiści biorą ten singleton i ukrywają wszystkie dane za metodami. Zamiast
wywołań Config.log_level stosują wywołania Config.log_level() lub Config.getLog
Level(). To nieco lepsze podejście, ponieważ dostęp do globalnych danych jest
realizowany za pośrednictwem jakiejś warstwy abstrakcji. Jeśli zdecydujemy się
zmienić reprezentację poziomów logowania, będziemy mogli zachować zgodność
ze starym kodem poprzez mapowanie pomiędzy nową i starą wersją za pośrednictwem interfejsu API konfiguracji. Jednak w dalszym ciągu mamy tylko jeden
zbiór danych konfiguracyjnych.
Zewnętrzne zasoby to także dane globalne
Wszelkie zmienne zasoby zewnętrzne to dane globalne. Jeśli Twoja aplikacja
korzysta z bazy danych, magazynu danych, systemu plików, API usługi, to
w dalszym ciągu jest narażona na wpadnięcie w pułapkę globalizacji. Również
w tym przypadku rozwiązaniem jest opakowanie tych zasobów kodem, który
możemy kontrolować.
WSKAZÓWKA NR 48
Jeśli jakiś zasób jest na tyle ważny, aby był globalny, opakuj go interfejsem API.
4337ebf6db5c7cc89e4173803ef3875a
4
Eliminowanie sprzężeń

169
Dziedziczenie dodaje sprzężenia
Nadużywanie dziedziczenia w taki sposób, że klasa dziedziczy stan i zachowanie z innej klasy, jest na tyle ważne, że zagadnienie to omówimy w osobnym
punkcie — jest nim temat 31., „Podatek od dziedziczenia”, w dalszej części
rozdziału.
Najważniejsza jest możliwość
wprowadzania zmian
W kodzie zawierającym sprzężenia wprowadzanie zmian jest trudne: zmiany
w jednym miejscu mogą powodować skutki uboczne w innych miejscach
w kodzie, często w trudnych do znalezienia. Takie błędy mogą ujawnić się
dopiero po wprowadzeniu kodu do produkcji — często za miesiąc lub po jeszcze dłuższym czasie od opublikowania kodu.
Utrzymywanie skromnego kodu — takiego, który wchodzi w interakcje tylko
z tymi komponentami, które bezpośrednio zna, pomaga wyeliminować z aplikacji sprzężenia, dzięki czemu wprowadzanie zmian w kodzie będzie łatwiejsze.
Pokrewne podrozdziały
2

Temat 8., „Istota dobrego projektu”.

Temat 9., „DRY— Przekleństwo powielania”.

Temat 10., „Ortogonalność”.

Temat 11., „Odwracalność”.

Temat 29., „Żonglerka realnym światem”.

Temat 30., „Programowanie transformacyjne”.

Temat 31., „Podatek od dziedziczenia”.

Temat 32., „Konfiguracja”.

Temat 33., „Eliminowanie związków czasowych”.

Temat 34., „Współdzielony stan jest zły”.

Temat 35., „Aktorzy i procesy”.

Temat 36., „Czarne tablice”.

Zasadę TDA omówiliśmy w artykule The Art of Enbugging, opublikowanym
w cyklu Software Construction w magazynie ☻IEEE Software” w 2003 roku2.
https://media.pragprog.com/articles/jan_03_enbug.pdf
4337ebf6db5c7cc89e4173803ef3875a
4
170 
29
37
Rozdział 5.
Zegnij lub złam
Żonglerka realnym światem
Rzeczy nie dzieją się ot tak. Ktoś lub coś za tym stoi.
John F. Kennedy
W dawnych czasach, kiedy autorzy tej książki mieli jeszcze chłopięcy wygląd,
komputery nie były szczególnie elastyczne. Zwykle organizowaliśmy interakcje
z nimi na podstawie znanych ograniczeń.
Dziś oczekujemy od komputerów więcej: chcemy, by zintegrowały się z realnym
światem, a nie na odwrót, żeby świat zintegrował się z nimi. A w naszym świecie
panuje bałagan: ciągle się coś dzieje, rzeczy zmieniają miejsce, ludzie zmieniają
zdania… A aplikacje, które piszemy, muszą w jakiś sposób ustalić, co trzeba
zrobić.
Ten podrozdział dotyczy sposobów pisania responsywnych aplikacji.
Zaczniemy od omówienia pojęcia zdarzenia.
Zdarzenia
Zdarzenie reprezentuje dostępność informacji. Może ona pochodzić ze świata
zewnętrznego: użytkownik kliknął przycisk lub wprowadzono aktualizację notowania ceny akcji. Zdarzenie może pochodzić z wnętrza aplikacji: wynik obliczeń
jest gotowy, zakończyło się wyszukiwanie. Może ono nawet dotyczyć czegoś tak
trywialnego, jak pobranie następnego elementu z listy.
Aplikacje, które reagują na zdarzenia i na ich podstawie dostosowują swoje
działania, w świecie rzeczywistym działają lepiej od tych, które zdarzeń nie
wykorzystują, niezależnie od źródła informacji. Są one bardziej interaktywne
i w bardziej ekonomiczny sposób korzystają z zasobów.
Ale jak można pisać takie aplikacje? Bez konkretnej strategii, wszystko szybko
by się nam pomyliło, a nasze aplikacje stałyby się bezładnym zlepkiem ściśle
sprzężonego kodu.
Przyjrzyjmy się czterem strategiom, które mogą nam pomóc w pisaniu aplikacji
obsługujących zdarzenia.
1. Maszyna stanów skończonych.
2. Wzorzec Obserwator.
3. Publikowanie i subskrypcje.
4. Programowanie reaktywne i strumienie.
4337ebf6db5c7cc89e4173803ef3875a
4
Żonglerka realnym światem

171
Maszyny stanów skończonych
Dave twierdzi, że kod z wykorzystaniem maszyny stanów skończonych (ang.
Finite State Machine — FSM) pisze niemal każdego tygodnia. Dość często implementacja maszyny FSM ma tylko kilka linijek kodu, ale te kilka linijek pomaga
rozwikłać mnóstwo potencjalnego bałaganu.
Korzystanie z FSM jest trywialnie proste, a jednak wielu programistów niechętnie je stosuje. Panuje przekonanie, że są one trudne, że mają zastosowanie
tylko w przypadku pracy ze sprzętem, albo że wymagają skorzystania z trudnych
do zrozumienia bibliotek. Nic z tych rzeczy.
Anatomia pragmatycznej maszyny stanów skończonych
Maszyna stanów w gruncie rzeczy jest jedynie specyfikacją sposobu obsługi
zdarzeń. Składa się ze zbioru stanów, z których jeden jest stanem bieżącym. Dla
każdego stanu określamy listę zdarzeń, które są dla niego istotne. Dla każdego
z tych zdarzeń definiujemy nowy bieżący stan systemu.
Na przykład możemy otrzymywać wieloczęściowe komunikaty z gniazda sieciowego. Pierwszy komunikat to nagłówek. Za nim następuje dowolna liczba
komunikatów danych, a następnie komunikat końcowy. Taki system możemy
zaprezentować za pomocą następującej maszyny stanów:
Zaczynamy od stanu Initial. Jeśli odbierzemy nagłówek, przechodzimy do stanu
Reading. Jeśli otrzymamy cokolwiek innego podczas gdy jesteśmy w stanie początkowym (linia oznaczona gwiazdką), to przechodzimy do stanu Error i kończymy
działanie.
Gdy jesteśmy w stanie Reading, możemy zaakceptować dowolne komunikaty
danych. W tym przypadku kontynuujemy czytanie w tym samym stanie. Możemy
też przyjąć komunikat końcowy (trailer), co spowoduje przejście do stanu Done.
Dowolne inne dane spowodują przejście do stanu Error.
4337ebf6db5c7cc89e4173803ef3875a
4
172 
Rozdział 5.
Zegnij lub złam
Interesującą cechą maszyn FSM, dzięki której są one bardzo „zgrabne”, jest
możliwość zaprezentowania całej maszyny stanów wyłącznie za pomocą danych.
Oto tabela reprezentująca nasz parser komunikatów:
Stan
Zdarzenia
header
data
trailer
inne
Initial
Reading
Error
Error
Error
Reading
Error
Reading
Done
Error
Wiersze w tabeli reprezentują stany. Aby dowiedzieć się, co zrobić, gdy wystąpi
zdarzenie, poszukaj wiersza reprezentującego bieżący stan, a następnie znajdź
kolumnę reprezentującą zdarzenie. Zawartość tej komórki to nowy stan maszyny.
Kod, który obsługuje maszynę stanów, jest równie prosty:
event/simple_fsm.rb
Line 1
TRANSITIONS = {
initial: {header: :reading},
reading: {data: :reading, trailer: :done},
}
5
state = :initial
while state != :done && state != :error
msg = get_next_message()
10
state = TRANSITIONS[state][msg.msg_type] || :error
end
Kod implementujący przejścia pomiędzy stanami znajduje się w wierszu 10.
Wykorzystano w nim odwołanie do tabeli TRANSITIONS przy użyciu indeksu
o wartości bieżącego stanu, a następnie użyto indeksu przejścia dla tego stanu
o wartości reprezentowanej przez typ komunikatu. W przypadku braku pasującego nowego stanu, maszyna przechodzi do stanu :error.
Dodawanie akcji
Czysta maszyna FSM, taka jak pokazana w powyższym przykładzie, to parser
strumienia zdarzeń. Jego działanie sprowadza się do wyświetlania stanu końcowego. Możemy ją rozbudować poprzez dodanie akcji, które zostaną uruchomione przy niektórych przejściach.
Na przykład możemy wyodrębnić wszystkie ciągi z pliku źródłowego. Ciąg jest
tekstem ujętym w cudzysłów, ale lewy ukośnik w ciągu jest znakiem ucieczki
dla następnego znaku. Zgodnie z tym ciąg "Ignoruj \"cudzysłów"" to jeden ciąg
znaków. Oto maszyna stanów, która realizuje takie działania:
4337ebf6db5c7cc89e4173803ef3875a
4
Żonglerka realnym światem

173
Tym razem każde przejście jest identyfikowane przez dwie etykiety. Górna
oznacza zdarzenie, które wyzwala przejście, a dolna reprezentuje działanie,
jakie należy wykonać przy przejściu pomiędzy stanami.
Tak jak poprzednim razem, zaprezentujemy tę maszynę stanów w tabeli. Jednak w tym przypadku każdy wpis w tabeli to dwuelementowa lista zawierająca
następny stan oraz nazwę operacji:
event/strings_fsm.rb
TRANSITIONS = {
# current
new state
action to take
#--------------------------------------------------------look_for_string: {
'"'
=> [ :in_string,
:start_new_string ],
:default => [ :look_for_string, :ignore ],
},
in_string: {
'"'
=> [ :look_for_string, :finish_current_string ],
'\\'
=> [ :copy_next_char, :add_current_to_string ],
:default => [ :in_string,
:add_current_to_string ],
},
copy_next_char: {
:default => [ :in_string,
:add_current_to_string ],
},
}
Dodaliśmy również możliwość określenia domyślnego przejścia, które jest realizowane w przypadku, gdy zdarzenie nie pasuje do żadnego z pozostałych przejść
dla tego stanu.
Teraz przyjrzyjmy się kodowi:
event/strings_fsm.rb
state = :look_for_string
result = []
while ch = STDIN.getc
state, action = TRANSITIONS[state][ch] || TRANSITIONS[state][:default]
case action
when :ignore
when :start_new_string
result = []
4337ebf6db5c7cc89e4173803ef3875a
4
174 
Rozdział 5.
Zegnij lub złam
when :add_current_to_string
result << ch
when :finish_current_string
puts result.join
end
end
Ten kod przypomina poprzedni przykład w tym sensie, że iterujemy po zdarzeniach (znakach na wejściu) i wyzwalamy przejścia. Ale ten kod robi więcej niż
poprzedni. Wynikiem każdego przejścia jest zarówno nowy stan, jak i nazwa
operacji do wykonania. Używamy nazwy akcji w celu wskazania kodu, który
zostanie uruchomiony, zanim wrócimy do przetwarzania w pętli.
Ten kod jest bardzo prosty, ale wykonuje potrzebne działania. Istnieje wiele
innych wariantów: tabela przejść może używać dla akcji anonimowych funkcji
lub wskaźników na funkcje; można opakować kod implementujący maszynę
stanów w oddzielnej klasie, z własnym stanem i tak dalej.
Nie trzeba dodawać, że nie jest konieczne zaimplementowanie wszystkich przejść
stanów w tym samym czasie. Jeśli wykonujesz procedurę rejestracji użytkownika w aplikacji, prawdopodobnie maszyna stanów będzie zawierała kilka przejść,
wykonywanych, kiedy użytkownik wprowadza swoje dane, potwierdza adres
e-mail, akceptuje ostrzeżenia związane ze 107 różnymi aktami prawnymi i tak
dalej. Utrzymanie stanu w pamięci zewnętrznej i wykorzystywanie jej do sterowania maszyną stanów to świetny sposób, aby obsłużyć wymagania tego rodzaju
przepływów pracy.
Maszyny stanów to dobry początek
Maszyny stanów są niedostatecznie często wykorzystywane przez programistów. Zachęcamy do poszukiwania możliwości ich zastosowania. Trzeba jednak
pamiętać, że zastosowanie maszyn stanów nie rozwiąże wszystkich problemów
związanych ze zdarzeniami. Przejdźmy więc do omówienia innych sposobów
obsługi problemów związanych z żonglerką zdarzeniami.
Wzorzec Obserwator
We wzorcu projektowym Obserwator mamy źródło zdarzeń zwane obiektem
obserwowalnym oraz listę klientów — obserwatorów, zainteresowanych tymi
zdarzeniami.
Obserwator rejestruje swoje zainteresowanie obiektem obserwowalnym, zazwyczaj poprzez przekazanie referencji do funkcji, która ma być wywołana w odpowiedzi na zdarzenie. Gdy wystąpi zdarzenie, obiekt obserwowalny iteruje po
liście swoich obserwatorów i wywołuje funkcje, które każdy z nich do niego
przesłał. Zdarzenie jest przekazywane do wywołania w postaci parametru.
Oto prosty przykład w Ruby. Do zakończenia działania aplikacji wykorzystywany
jest moduł Terminator. Jednak przed zakończeniem aplikacji, moduł ten infor-
4337ebf6db5c7cc89e4173803ef3875a
4
Żonglerka realnym światem

175
muje wszystkich swoich obserwatorów, że aplikacja ma się zakończyć3. Mogą oni
skorzystać z tego powiadomienia, aby posprzątać tymczasowe zasoby, zatwierdzić dane i tak dalej:
event/observer.rb
module Terminator
CALLBACKS = []
def self.register(callback)
CALLBACKS << callback
end
def self.exit(exit_status)
CALLBACKS.each { |callback| callback.(exit_status) }
exit!(exit_status)
end
end
Terminator.register(-> (status) { puts "callback 1 widzi #{status}" })
Terminator.register(-> (status) { puts "callback 2 widzi #{status}" })
Terminator.exit(99)
$ ruby event/observer.rb
callback 1 widzi 99
callback 2 widzi 99
Stworzenie obiektu obserwowalnego nie wymaga zbyt wiele kodu: umieszczamy
referencje do funkcji na liście, a następnie wywołujemy te funkcje, gdy wystąpi
zdarzenie. Jest to dobry przykład sytuacji, kiedy nie należy korzystać z biblioteki.
Wzorzec obserwator – obiekt obserwowalny jest z powodzeniem stosowany od
dziesięcioleci. Szczególnie często jest używany w systemach z interfejsem użytkownika, gdzie funkcje wywołań zwrotnych są wykorzystywane do poinformowania aplikacji o działaniach użytkownika.
Jednak z wzorcem Obserwator związany jest pewien problem: konieczność,
aby każdy obserwator zarejestrował się w obiekcie obserwowalnym, wprowadza
sprzężenia. Ponadto, ze względu na to, że w typowej implementacji wywołania
zwrotne są obsługiwane przez obiekt obserwowalny w trybie inline synchronicznie, mogą powstawać wąskie gardła.
Problemy te rozwiązuje kolejna strategia: publikuj – subskrybuj.
Publikowanie i subskrypcje
Wzorzec publikuj – subskrybuj (pubsub) uogólnia wzorzec Obserwator, a jednocześnie rozwiązuje problemy sprzężeń i wydajności.
W modelu pubsub mamy wydawców i subskrybentów. Są ze sobą powiązani
za pomocą kanałów. Kanały są zaimplementowane w odrębnym kodzie — czasami jest to biblioteka, czasami proces, a czasami infrastruktura rozproszona.
Wszystkie te szczegóły implementacji nie są widoczne z kodu.
3
Tak, wiemy, że Ruby ma już taką możliwość zaimplementowaną za pomocą funkcji at_exit.
4337ebf6db5c7cc89e4173803ef3875a
4
176 
Rozdział 5.
Zegnij lub złam
Każdy kanał ma nazwę. Subskrybenci rejestrują zainteresowanie co najmniej
jednym z tych identyfikowanych przez nazwę kanałów, a wydawcy zapisują
do nich zdarzenia. W odróżnieniu od wzorca Obserwator, komunikacja pomiędzy wydawcą a subskrybentem jest obsługiwana poza kodem i jest potencjalnie asynchroniczna.
Chociaż można samodzielnie zaimplementować bardzo prosty system pubsub,
to jednak zwykle się tego nie robi. Większość dostawców chmury obliczeniowej
ma w swojej ofercie usługę pubsub, do której mogą się łączyć aplikacje na
całym świecie. Każdy popularny język programowania ma co najmniej jedną
bibliotekę pubsub.
Pubsub jest dobrą technologią umożliwiającą rozdzielenie obsługi asynchronicznych zdarzeń. Pozwala na dodawanie i zastępowanie kodu — potencjalnie
nawet wtedy, gdy aplikacja jest uruchomiona — bez zmiany istniejącego kodu
aplikacji. Minusem jest to, że w systemie, który intensywnie korzysta z techniki
pubsub, może być trudne zaobserwowanie co się dzieje: nie da się spojrzeć
w kod wydawcy i natychmiast zobaczyć subskrybentów, którzy są zainteresowani
określonym komunikatem.
W porównaniu z wzorcem Obserwator, technika pubsub jest doskonałym przykładem zmniejszenia liczby sprzężeń poprzez wyabstrahowanie współdzielonego
interfejsu (kanału). Jednak w gruncie rzeczy nadal jest to jedynie system przekazywania komunikatów. Tworzenie systemów, które reagują na kombinacje
zdarzeń, będzie wymagało bardziej złożonego mechanizmu. Przyjrzyjmy się zatem
sposobom wprowadzenia do przetwarzania zdarzeń wymiaru czasowego.
Programowanie reaktywne, strumienie
i zdarzenia
Jeśli kiedykolwiek korzystałeś z arkusza kalkulacyjnego, to prawdopodobnie
wiesz, czym jest programowaniem reaktywne. Jeżeli komórka zawiera formułę,
która odwołuje się do innej komórki, to aktualizacja tej drugiej komórki powoduje również aktualizację pierwszej. Wartości komórek z formułami reagują na
zmiany wartości używanych do ich wyliczania.
Istnieje wiele frameworków, które mogą pomóc w obsłudze tego rodzaju reaktywności na poziomie danych: w świecie przeglądarek faworytami są obecnie
React i Vue.js (ale ponieważ to jest JavaScript, to zanim ta książka ukaże się
w druku, ta informacja może już być nieaktualna).
To jasne, że zdarzenia mogą być również wykorzystywane do wyzwalania reakcji
w kodzie, ale ich zastosowanie nie zawsze jest łatwe. W takich przypadkach
możemy skorzystać ze strumieni.
Strumienie pozwalają traktować zdarzenia tak, jakby były zbiorem danych.
Mechanizm działa tak, jakbyśmy mieli listę zdarzeń, która wydłuża się wraz
4337ebf6db5c7cc89e4173803ef3875a
4
Żonglerka realnym światem

177
z napływem nowych zdarzeń. Elegancja takiego rozwiązania polega na tym, że
możemy traktować strumienie podobnie, jak wszystkie inne kolekcje: możemy
je przetwarzać, scalać, filtrować oraz wykonywać wszystkie inne dobrze znane
operacje typowe dla kolekcji danych. Możemy nawet łączyć strumienie zdarzeń
ze zwykłymi kolekcjami. Do tego strumienie mogą być asynchroniczne, co oznacza, że kod może reagować na zdarzenia w miarę ich napływu.
Bieżący bazowy standard de facto dla reaktywnej obsługi zdarzeń zdefiniowano
na stronie http://reactivex.io. Określono tam niezależny od języka zestaw zasad
i udokumentowano kilka popularnych implementacji. W tym podrozdziale
skorzystamy z biblioteki RxJs dla języka JavaScript.
W pierwszym przykładzie pobierzemy dwa strumienie i połączymy je ze sobą:
w wyniku otrzymamy nowy strumień, w którym każdy składnik zawiera jeden
element z pierwszego strumienia wejściowego i jeden z drugiego. W tym przypadku, pierwszy strumień jest po prostu listą nazw pięciu zwierząt. Drugi strumień jest bardziej interesujący: jest to timer, który generuje zdarzenie co 500 ms.
Ponieważ strumienie są ze sobą scalone, wynik jest generowany tylko wtedy,
gdy dane są dostępne w obu strumieniach. Zatem strumień wynikowy emituje
wartości co pół sekundy:
event/rx0/index.js
import * as Observable from 'rxjs'
import { logValues } from "../rxcommon/logger.js"
let animals = Observable.of("ant", "bee", "cat", "dog", "elk")
let ticker = Observable.interval(500)
let combined = Observable.zip(animals, ticker)
combined.subscribe(next => logValues(JSON.stringify(next)))
Powyższy kod wykorzystuje prostą funkcję logowania4, która dodaje elementy
do listy wyświetlanej w oknie przeglądarki. Z każdym elementem jest powiązany
stempel czasu zawierający czas w milisekundach, jaki upłynął od chwili uruchomienia programu. Oto zawartość okna przeglądarki dla naszego kodu:
4
https://media.pragprog.com/titles/tpp20/code/event/rxcommon/logger.js
4337ebf6db5c7cc89e4173803ef3875a
4
178 
Rozdział 5.
Zegnij lub złam
Zwróćmy uwagę na stemple czasu: otrzymujemy jedno zdarzenie ze strumienia
co 500 ms. Każde zdarzenie zawiera numer seryjny (utworzony przez interwał
obiektu obserwowalnego) i nazwę następnego zwierzęcia z listy. Podczas oglądania działania kodu na żywo w przeglądarce, wiersze logu pojawiają się co pół
sekundy.
Strumienie zdarzeń są zwykle wypełniane w miarę występowania zdarzeń, co
oznacza, że obiekty obserwowalne, które te strumienie wypełniają, mogą
działać równolegle. Oto przykład kodu, który pobiera ze zdalnej witryny informacje o użytkownikach. Na potrzeby tego kodu użyjemy publicznej witryny
https://reqres.in, która dostarcza otwarty interfejs REST. Za pomocą tego API,
możemy pobrać dane na temat danego użytkownika (fałszywego), kierując żądanie GET do punktu końcowego users/<<id>>. Nasz kod pobiera użytkowników
o identyfikatorach 3, 2 i 1:
event/rx1/index.js
import * as Observable from 'rxjs'
import { mergeMap } from 'rxjs/operators'
import { ajax } from 'rxjs/ajax'
import { logValues } from "../rxcommon/logger.js"
let users = Observable.of(3, 2, 1)
let result = users.pipe(
mergeMap((user) => ajax.getJSON(`https://reqres.in/api/users/${user}`))
)
result.subscribe(
resp => logValues(JSON.stringify(resp.data)),
err => console.error(JSON.stringify(err))
)
Wewnętrzne szczegóły kodu nie są zbyt ważne. Ekscytujący jest wynik, pokazany na poniższym zrzucie ekranu:
Spójrzmy na stemple czasu: trzy żądania lub trzy oddzielne strumienie były
przetwarzane równolegle. Obsługa pierwszego, dotycząca użytkownika o identyfikatorze 2, zajęła 82 ms, a dwa kolejne zostały obsłużone 50 i 51 ms później.
Strumienie zdarzeń to kolekcje asynchroniczne
W poprzednim przykładzie lista identyfikatorów użytkowników (w obserwowalnym obiekcie users) była statyczna. Ale nie musi taka być. Załóżmy, że chcemy
zebrać te informacje, kiedy użytkownicy logują się w naszej witrynie. Wszystko,
4337ebf6db5c7cc89e4173803ef3875a
4
Żonglerka realnym światem

179
co musimy zrobić, to wygenerowanie obserwowalnego zdarzenia zawierający
identyfikator użytkownika podczas tworzenia sesji i korzystanie z tego obserwowalnego obiektu zamiast statycznego. Następnie będziemy pobierać dane
o użytkownikach w miarę odbierania identyfikatorów i przypuszczalnie gdzieś
je przechowywać.
Taka abstrakcja daje bardzo duże możliwości: nie musimy już myśleć o czasie
jako o czymś, czym powinniśmy zarządzać. Strumienie zdarzeń unifikują przetwarzanie synchroniczne i asynchroniczne za wspólnym, wygodnym interfejsem API.
Zdarzenia są wszechobecne
Zdarzenia występują wszędzie. Niektóre z nich są oczywiste: kliknięcie przycisku,
upływ interwału czasowego. Inne są mniej trywialne ktoś się loguje, wiersz
w pliku pasuje do wzorca. Ale bez względu na ich źródło, kod, który jest wykonywany w odpowiedzi na zdarzenia, jest bardziej responsywny i lepiej oddzielony
od swojego liniowego odpowiednika.
Pokrewne podrozdziały

Temat 28., „Eliminowanie sprzężeń”.

Temat 36., „Czarne tablice”.
Ćwiczenia
19. W podrozdziale poświęconym maszynom stanów skończonych wspomnieliśmy, że można przenieść generyczną implementację maszyny stanów do
odrębnej klasy. Tę klasę można by zainicjować poprzez przekazanie tabeli
przejść i stanu początkowego.
Spróbuj zaimplementować w ten sposób aplikację do wyodrębniania ciągów
znaków.
20. Zastosowanie której z opisanych technologii (być może kilku) byłoby dobrym
rozwiązaniem w następujących sytuacjach:




Jeśli w ciągu pięciu minut otrzymasz informacje o zdarzeniach niedostępności
trzech interfejsów sieciowych, powiadom personel operacyjny.
Jeśli po zachodzie słońca wykryjesz ruch na dole schodów, po czym wykryjesz
ruch u szczytu schodów, włącz światła na piętrze.
Chcesz powiadomić różne systemy raportowania, że obsługa zamówienia została
zakończona.
W celu ustalenia, czy klient kwalifikuje się do uzyskania kredytu na samochód,
aplikacja musi wysłać żądania do trzech usług typu backend i poczekać na odpowiedzi.
4337ebf6db5c7cc89e4173803ef3875a
4
180 
30
38
Rozdział 5.
Zegnij lub złam
Programowanie transformacyjne
Jeśli nie potrafisz opisać tego, co robisz, w formie procesu,
to nie wiesz, co robisz.
W. Edwards Deming
Wszystkie programy przekształcają dane — dokonują konwersji danych wejściowych na wyniki. A jednak, kiedy myślimy o projekcie, rzadko uwzględniamy
tworzenie transformacji. Zamiast tego martwimy się o klasy i moduły, struktury
danych i algorytmy, języki i frameworki.
Uważamy, że ta koncentracja na kodzie sprawia, że gubimy sedno problemu:
musimy wrócić do myślenia o programach jako mechanizmach przekształcających dane wejściowe na wyniki. Kiedy to zrobimy, wiele problemów, o które
wcześniej się martwiliśmy, po prostu zniknie. Struktura stanie się czytelniejsza,
obsługa błędów bardziej spójna, a sprzężenia spadną na dalszy plan.
Aby rozpocząć naszą analizę, cofnijmy się w czasie do roku 1970. Poprosimy
tam programistę Uniksa, aby napisał dla nas program, który wyświetla pięć
najdłuższych plików w drzewie katalogów, gdzie „najdłuższy” oznacza „mający
największą liczbę wierszy”.
Moglibyście się spodziewać, że otworzy edytor tekstu i zacznie pisać kod w C.
On jednak tego nie zrobi, bo myśli o rozwiązaniu problemu w kategoriach tego,
co mamy (drzewo katalogów), i tego, co chcemy uzyskać (listę plików). Potem
otworzy terminal i wpisze polecenie podobne do następującego:
$ find . -type f | xargs wc -l | sort -n | tail -5
Powyższa komenda zawiera ciąg transformacji:
find . -type f
Wpisz do standardowego wyjścia listę wszystkich plików (-type f) z bieżącego katalogu (.) i jego podkatalogów.
xargs wc -l
Czytaj wiersze ze standardowego wejścia i przygotuj je w taki sposób, aby
wszystkie zostały przekazane w formie argumentów do polecenia wc -l.
Program wc z opcją -l zlicza wiersze w każdym z plików przekazanych jako
argument i zapisuje każdy wynik na standardowym wyjściu jako licznik
dla tego pliku.
sort -n
Komenda sort sortuje standardowe wejście zakładając, że każdy wiersz
zaczyna się od liczby (-n) i zapisuje wynik na standardowym wyjściu.
tail -5
Czyta standardowe wejście i zapisuje na standardowym wyjściu tylko
ostatnich pięć wierszy.
4337ebf6db5c7cc89e4173803ef3875a
4
Programowanie transformacyjne
 181
Po uruchomieniu tego polecenia w katalogu z plikami naszej książki otrzymaliśmy:
470 ./test_to_build.pml
487 ./dbc.pml
719 ./domain_languages.pml
727 ./dry.pml
9561 total
Ostatni wiersz oznacza całkowitą liczbę wierszy we wszystkich plikach (nie
tylko tych, które wyświetlono w wyniku), ponieważ właśnie w ten sposób działa
polecenie wc. Możemy obciąć ten wiersz poprzez zażądanie jednego wiersza
więcej z ogona, a następnie zignorowanie ostatniego wiersza:
$ find . -type f | xargs wc -l | sort -n | tail -6 | head -5
470 ./debug.pml
470 ./test_to_build.pml
487 ./dbc.pml
719 ./domain_languages.pml
727 ./dry.pml
Przyjrzyjmy się temu poleceniu z perspektywy danych, które przepływają pomiędzy poszczególnymi krokami. Nasze pierwotne wymaganie, „pierwszych 5 plików
o największej liczbie wierszy”, zostaje przekształcone na cykl transformacji
(pokazano je również na rysunku 5.1):
Nazwa katalogu
→ lista plików
→ lista z numerami wierszy
→ lista posortowana
→ lista pięciu plików o najwyższej liczbie wierszy plus wiersz z sumą
→ lista pięciu plików o najwyższej liczbie wierszy
Rysunek 5.1. Potok polecenia find jako ciąg transformacji
4337ebf6db5c7cc89e4173803ef3875a
4
182 
Rozdział 5.
Zegnij lub złam
Procedura przypomina przemysłową linię produkcyjną: wprowadzamy surowe
dane na jednym końcu, a z drugiej strony wychodzi gotowy produkt (informacje).
Warto myśleć w ten sposób o każdym kodzie.
WSKAZÓWKA NR 49
Programowanie dotyczy kodu, ale programy dotyczą danych.
Wyszukiwanie transformacji
Czasami najprostszym sposobem na znalezienie transformacji jest wyjście od
wymagania i określenie dla niego wejść i wyjść. Potem należy zdefiniować funkcję
reprezentującą cały program. Następnie trzeba znaleźć kroki, które doprowadzą
Cię od wejścia do wyjścia. Jest to podejście góra-dół (ang. top-down).
Załóżmy na przykład, że chcemy stworzyć stronę internetową dla ludzi grających
w gry słowne, wyszukującą wszystkie słowa, które można stworzyć z zestawu
liter. Nasze wejście to zbiór liter, a wyjściem jest lista słów trzyliterowych, czteroliterowych i tak dalej:
"lvyin" zostaje przekształcone na
3 => ivy, lin, nil, yin
4 => inly, liny, viny
5 => vinyl
(Tak, te wszystkie ciągi są słowami, przynajmniej zgodnie ze słownikiem systemu macOS).
Idea całej aplikacji jest prosta: mamy słownik, który grupuje słowa według
sygnatury dobranej w taki sposób, że wszystkie słowa zawierające te same
litery mają taką samą sygnaturę. Najprostsza funkcja sygnatury to po prostu
posortowana lista liter w słowie. Możemy następnie poszukać ciągu wejściowego
poprzez wygenerowanie dla niego sygnatury, a potem zobaczyć, które słowa
(jeśli takie istnieją) w słowniku mają taką samą sygnaturę.
Zatem program do wyszukiwania anagramów można podzielić na cztery
oddzielne transformacje:
4337ebf6db5c7cc89e4173803ef3875a
4
Programowanie transformacyjne
 183
Krok
Transformacja
Przykładowe dane
Krok 0.
Początkowe dane wejściowe
"ylvin"
Krok 1.
Wszystkie kombinacje trzech lub
więcej liter
vin, viy, vil, vny, vnl, vyl,
iny, inl, iyl, nyl, viny,
vinl, viyl, vnyl, inyl, vinyl
Krok 2.
Sygnatury kombinacji liter
inv, ivy, ilv, nvy, lnv, lvy,
iny, iln, ily, lny, invy,
ilnv, ilvy, lnvy, ilny, ilnvy
Krok 3.
Lista wszystkich słów ze słownika,
które pasują do dowolnej sygnatury
ivy, yin, nil, lin, viny,
liny, inly, vinyl
Krok 4.
Słowa pogrupowane według długości
3 => ivy, lin, nil, yin
4 => inly, liny, viny
5 => vinyl
Kolejne transformacje
Zacznijmy od przeanalizowania kroku 1., w którym pobieramy słowo i tworzymy
listę wszystkich kombinacji złożonych z trzech lub więcej liter. Ten krok także
można zaprezentować jako listę transformacji:
Krok
Transformacja
Przykładowe dane
Krok 1.0 Początkowe dane wejściowe
"vinyl"
Krok 1.1 Konwersja na znaki
v, i, n, y, l
Krok 1.2 Wyznaczenie wszystkich podzbiorów
[], [v], [i], … [v,i],
[v,n], [v,y], … [v,i,n],
[v,i,y], …[v,n,y,l],
[i,n,y,l], [v,i,n,y,l]
Krok 1.3 Wybranie tych podzbiorów, które
zawierają więcej niż trzy znaki
[v,i,n], [v,i,y], …
[i,n,y,l], [v,i,n,y,l]
Krok 1.4 Ponowna konwersja na ciągi znaków
[vin,viy, … inyl,vinyl]
Dotarliśmy do miejsca, w którym możemy łatwo zaimplementować każdą transformację w kodzie (w tym przypadku w języku Elixir):
function-pipelines/anagrams/lib/anagrams.ex
defp all_subsets_longer_than_three_characters(word) do
word
|> String.codepoints()
|> Comb.subsets()
|> Stream.filter(fn subset -> length(subset) >= 3 end)
|> Stream.map(&List.to_string(&1))
end
4337ebf6db5c7cc89e4173803ef3875a
4
184 
Rozdział 5.
Zegnij lub złam
Co z operatorem |>?
Eliksir, a także wiele innych języków funkcyjnych, ma operator potoku, czasami nazywany potokiem „w przód” lub po prostu potokiem5. Jego działanie
sprowadza się do przyjmowania wartości po jego lewej stronie i wstawiania jej
jako pierwszego parametru do funkcji po prawej stronie operatora. Tak więc
zapis:
"vinyl" |> String.codepoints |> Comb.subsets()
jest równoważny zapisowi:
Comb.subsets(String.codepoints("vinyl"))
(W innych językach ta przekazywana przez potok wartość może być wstrzyknięta jako ostatni parametr kolejnej funkcji — w dużej mierze zależy to od stylu
wbudowanych bibliotek).
Można by pomyśleć, że jest to po prostu składniowy cukier. Ale operator potoku
jest okazją do prawdziwie rewolucyjnej zmiany sposobu myślenia. Zastosowanie potoku sprawia, że automatycznie myślimy o programie w kategoriach
przekształcania danych. Za każdym razem, gdy w kodzie widzimy operator |>,
faktycznie widzimy miejsce, w którym dane przepływają pomiędzy jedną transformacją, a następną.
Podobny operator istnieje w wielu językach. W Elm i F# jest operator |>,
w Clojure są operatory -> i ->> (działające nieco inaczej), w R jest operator %>%.
W Haskellu istnieje zarówno operator potoku, jak i mechanizmy pozwalające
na dodawanie nowych takich operatorów. W czasie, gdy powstaje ta książka,
toczy się dyskusja o dodaniu operatora |> do języka JavaScript.
Jeśli Twój bieżący język obsługuje podobny operator, masz szczęście. Jeśli nie
obsługuje, zajrzyj do ramki „Język X nie obsługuje potoków”.
Tak czy inaczej, wróćmy do kodu.
Dalsze transformacje
Przyjrzyjmy się teraz krokowi 2. programu głównego, gdzie przekształcamy
podzbiory znaków na sygnatury. Ponownie mamy do czynienia z prostą transformacją — na podstawie listy podzbiorów znaków tworzymy listę sygnatur:
5
Pierwsze użycie znaków |> jako oznaczenia potoku zanotowano w 1994 roku, w dyskusji
na temat języka Isabelle/ML. Dyskusję tę zarchiwizowano pod adresem https://blogs.
msdn.microsoft.com/dsyme/2011/05/17/archeological-semiotics-the-birth-of-the-pipelinesymbol-1994/.
4337ebf6db5c7cc89e4173803ef3875a
4
Programowanie transformacyjne
Krok
Transformacja
Przykładowe dane
Krok 2.0
Początkowe dane wejściowe
vin, viy, … inyl, vinyl
Krok 2.1
Konwersja na sygnatury
inv, ivy … ilny, inlvy
 185
Kod w języku Elixir na poniższym listingu jest równie prosty:
function-pipelines/anagrams/lib/anagrams.ex
defp as_unique_signatures(subsets) do
subsets
|> Stream.map(&Dictionary.signature_of/1)
end
Teraz przekształcamy tę listę sygnatur: każdą z nich mapujemy na listę znanych wyrazów o tej samej sygnaturze lub na wartość nil, jeśli nie ma takich słów.
Następnie musimy usunąć wartości nil i spłaszczyć zagnieżdżone listy do
jednego poziomu:
function-pipelines/anagrams/lib/anagrams.ex
defp find_in_dictionary(signatures) do
signatures
|> Stream.map(&Dictionary.lookup_by_signature/1)
|> Stream.reject(&is_nil/1)
|> Stream.concat(&(&1))
end
Krok 4., pogrupowanie słów według długości, to kolejna prosta transformacja
powodująca konwersję naszej listy na mapę, w której kluczami są długości
wyrazów, a wartościami wszystkie słowa o tej długości:
function-pipelines/anagrams/lib/anagrams.ex
defp group_by_length(words) do
words
|> Enum.sort()
|> Enum.group_by(&String.length/1)
end
Połączenie transformacji w całość
Napisaliśmy kod dla każdej pojedynczej transformacji. Pozostało je wszystkie
połączyć w głównej funkcji aplikacji:
function-pipelines/anagrams/lib/anagrams.ex
def anagrams_in(word) do
word
|> all_subsets_longer_than_three_characters()
|> as_unique_signatures()
|> find_in_dictionary()
|> group_by_length()
end
Czy to działa? Spróbujmy:
iex(1)> Anagrams.anagrams_in "lyvin"
%{
4337ebf6db5c7cc89e4173803ef3875a
4
186 
Rozdział 5.
Zegnij lub złam
3 => ["ivy", "lin", "nil", "yin"],
4 => ["inly", "liny", "viny"],
5 => ["vinyl"]
}
Język X nie obsługuje potoków
Potoki istnieją od dłuższego czasu, ale tylko w niszowych językach. Znalazły się
w głównym nurcie dopiero niedawno, a wiele popularnych języków nadal ich nie obsługuje.
Dobrą wiadomością jest to, że myślenie w kategoriach transformacji nie wymaga
składni konkretnego języka: jest to raczej filozofia projektowania. Jeśli język nie obsługuje potoków, nadal można konstruować kod w postaci transformacji, ale należy je
zapisać w postaci ciągu operacji przypisania:
const content = File.read(file_name);
const lines = find_matching_lines(content, pattern)
const result = truncate_lines(lines)
To trochę bardziej uciążliwy sposób, ale pozwala on wykonać zadanie.
Dlaczego potoki są takie wspaniałe?
Przyjrzyjmy się jeszcze raz treści głównej funkcji:
word
|> all_subsets_longer_than_three_characters()
|> as_unique_signatures()
|> find_in_dictionary()
|> group_by_length()
Jest to po prostu ciąg transformacji niezbędnych do spełnienia naszego wymagania. Każda transformacja pobiera dane wejściowe z poprzedniej i przekazuje
wynik do następnej. W ten sposób kod staje się bardzo bliski językowi naturalnemu.
Ale jest coś ważniejszego. Jeśli znasz zasady programowania obiektowego,
z pewnością przypominasz sobie wymaganie, aby ukryć dane, które powinny
być hermetycznie zamknięte wewnątrz obiektów. Te obiekty następnie wzajemnie
się ze sobą komunikują, przy okazji zmieniając sobie nawzajem stan. Wprowadza
to sporo sprzężeń i sprawia, że modyfikowanie systemów obiektowych może być
trudne.
WSKAZÓWKA NR 50
Nie należy gromadzić stanów, tylko je przekazywać.
W modelu transformacyjnym powinniśmy pamiętać o powyższej regule. Zamiast
posługiwania się niewielkimi pulami danych rozsianych po całym systemie,
powinniśmy myśleć o danych jak o potężnej rzece, która płynie. Dane stają się
4337ebf6db5c7cc89e4173803ef3875a
4
Programowanie transformacyjne
 187
partnerem funkcjonalności: potok to sekwencja kod → dane → kod → dane…
Dane nie są już przywiązane do konkretnej grupy funkcji, tak jak to ma miejsce
w definicji klasy. Zamiast tego możemy swobodnie reprezentować postęp aplikacji przekształcającej dane wejściowe w wyniki. Dzięki temu możemy znacznie
zmniejszyć liczbę sprzężeń: funkcję można wykorzystać (wielokrotnie) wszędzie
tam, gdzie jej parametry pasują do wyników jakiejś innej funkcji.
Oczywiście w dalszym ciągu występuje pewna liczba sprzężeń, ale z naszych
doświadczeń wynika, że programowanie transformacyjne zapewnia łatwiejsze
zarządzanie w porównaniu ze stylem obiektowym. A jeśli używasz języka z kontrolą typów, podczas próby połączenia dwóch niezgodnych ze sobą elementów,
otrzymasz ostrzeżenia na etapie kompilacji.
Co z obsługą błędów?
Dotychczas nasze transformacje działały w świecie, w którym nic złego się
nie działo. W jaki sposób możemy je wykorzystać w realnym świecie? Jak dodać
całą logikę warunkową potrzebną do sprawdzania błędów w przypadku zbudowania liniowych łańcuchów wywołań funkcji?
Istnieje wiele sposobów, aby to zrobić. Wszystkie bazują na prostej konwencji:
nigdy nie przekazujemy surowych wartości pomiędzy transformacjami. Zamiast
tego opakowujemy je w strukturę danych (lub typ), która jednocześnie informuje
nas, czy opakowana wartość jest prawidłowa. Na przykład w Haskellu to opakowanie ma nazwę Maybe. W językach F# i Scala tę rolę pełni funkcja Option.
Sposób wykorzystania tej koncepcji jest specyficzny dla języka. Na ogół jednak
istnieją dwa podstawowe sposoby pisania kodu: można obsługiwać sprawdzanie błędów wewnątrz transformacji lub poza nimi.
W języku Eliksir, którego używaliśmy do tej pory, nie ma takiej wbudowanej
obsługi błędów. Dla naszych celów jest to dobre, ponieważ zaprezentujemy
implementację od podstaw. Podobne rozwiązanie powinno działać w większości
innych języków.
Najpierw wybierz reprezentację
Potrzebujemy dla naszego opakowania reprezentacji (struktury danych, która
zawiera wartość lub informację o błędzie). Można użyć do tego struktur, ale Elixir
stosuje do tego dość silną konwencję: funkcje zazwyczaj zwracają krotki zawierające albo {:ok, wartość}, albo {:error, powód}. Na przykład wywołanie File.open
zwraca albo :ok i proces wejścia-wyjścia, albo :error i kod przyczyny błędu:
iex(1)> File.open("/etc/passwd")
{:ok, #PID<0.109.0>}
iex(2)> File.open("/etc/wombat")
{:error, :enoent}
Podczas przekazywania danych przez potok użyjemy krotek :ok (:error).
4337ebf6db5c7cc89e4173803ef3875a
4
188 
Rozdział 5.
Zegnij lub złam
Następnie obsłuż ją wewnątrz każdej transformacji
Spróbujmy napisać funkcję, która zwraca wszystkie wiersze w pliku zawierające
określony ciąg znaków, obcięte do pierwszych 20 znaków. Chcemy zapisać ją jako
transformację, więc wejściem będą nazwa pliku i wyszukiwany ciąg, a wyjściem
albo krotka :ok z listą wierszy, albo krotka :error z powodem błędu. Funkcja najwyższego poziomu powinna mieć postać podobną do następującej:
function-pipelines/anagrams/lib/grep.ex
def find_all(file_name, pattern) do
File.read(file_name)
|> find_matching_lines(pattern)
|> truncate_lines()
end
Nie ma tu jawnej obsługi błędów, ale jeśli dowolny krok w potoku zwraca krotkę
błędu, to potok zwróci ten błąd bez wykonywania funkcji, która występuje dalej
w potoku6. Można to uzyskać za pomocą mechanizmu dopasowywania wzorców
języka Elixir:
function-pipelines/anagrams/lib/grep.ex
defp find_matching_lines({:ok, content}, pattern) do
content
|> String.split(~r/\n/)
|> Enum.filter(&String.match?(&1, pattern))
|> ok_unless_empty()
end
defp find_matching_lines(error, _), do: error
# ---------defp truncate_lines({ :ok, lines }) do
lines
|> Enum.map(&String.slice(&1, 0, 20))
|> ok()
end
defp truncate_lines(error), do: error
# ---------defp ok_unless_empty([]), do: error("nothing found")
defp ok_unless_empty(result), do: ok(result)
defp ok(result), do: { :ok, result }
defp error(reason), do: { :error, reason }
Spójrzmy na funkcję find_matching_lines. Jeśli jej pierwszym parametrem jest
krotka :ok, funkcja używa zawartości tej krotki w celu znalezienia wierszy
pasujących do wzorca. Jednak jeżeli pierwszym parametrem nie jest krotka :ok,
uruchamia się druga wersja funkcji, która po prostu zwraca ten parametr.
W ten sposób funkcja po prostu przekazuje błąd w dół potoku. To samo dotyczy
funkcji truncate_lines.
Możemy przetestować to zachowanie w konsoli:
6
Jest to dość swobodna interpretacja. Z technicznego punktu widzenia wywołujemy
następne funkcje, ale nie wykonujemy w nich kodu.
4337ebf6db5c7cc89e4173803ef3875a
4
Programowanie transformacyjne
 189
iex> Grep.find_all "/etc/passwd", ~r/www/
{:ok, ["_www:*:70:70:World W", "_wwwproxy:*:252:252:"]}
iex> Grep.find_all "/etc/passwd", ~r/wombat/
{:error, "nothing found"}
iex> Grep.find_all "/etc/koala", ~r/www/
{:error, :enoent}
Można zauważyć, że błąd w dowolnym miejscu potoku natychmiast staje się
wartością tego potoku.
Albo obsłuż błędy w potoku
Być może patrząc na funkcje find_matching_lines i truncate_lines pomyśleliście,
że ciężar obsługi błędów został przeniesiony do transformacji. To prawda. W językach, które używają dopasowywania wzorców w wywołaniach funkcji, takich jak
Elixir, efekt jest mniej widoczny, ale zachowanie wciąż nie jest zbyt eleganckie.
Byłoby lepiej, gdyby Elixir miał wersję operatora potoku |>, która byłaby
świadoma istnienia krotek ok: (:error) i która, w przypadku błędu, powodowałaby skrócone wykonanie potoku7. Jednak faktem jest, że Elixir nie pozwala nam
dodać podobnego mechanizmu. Dotyczy to również szeregu innych języków.
Problem, z którym mamy do czynienia, polega na tym, że w przypadku wystąpienia błędu nie chcemy uruchamiać kodu w dalszej części potoku i nie chcemy,
aby kod wiedział, że coś podobnego się dzieje. Oznacza to, że musimy odłożyć
uruchamianie funkcji w potoku do czasu uzyskania potwierdzenia, że wszystkie
poprzednie kroki w potoku zakończyły się sukcesem. Aby to zrobić, musimy
zastąpić wywołania funkcji wartościami funkcji, które mogą zostać wywołane
później. Oto jedna z możliwych implementacji:
function-pipelines/anagrams/lib/grep1.ex
defmodule Grep1 do
def and_then({ :ok, value }, func), do: func.(value)
def and_then(anything_else, _func), do: anything_else
def find_all(file_name, pattern) do
File.read(file_name)
|> and_then(&find_matching_lines(&1, pattern))
|> and_then(&truncate_lines(&1))
end
defp find_matching_lines(content, pattern) do
content
|> String.split(~r/\n/)
|> Enum.filter(&String.match?(&1, pattern))
|> ok_unless_empty()
end
7
W rzeczywistości można dodać taki operator do języka Elixir, używając jego mechanizmu
makr. Przykładem ich wykorzystania jest biblioteka Monad. Można również użyć konstrukcji with tego języka, ale wtedy stracilibyśmy większość sensu pisania transformacji
i obsługi ich za pomocą potoku.
4337ebf6db5c7cc89e4173803ef3875a
4
190 
Rozdział 5.
Zegnij lub złam
defp truncate_lines(lines) do
lines
|> Enum.map(&String.slice(&1, 0, 20))
|> ok()
end
defp ok_unless_empty([]), do: error("nothing found")
defp ok_unless_empty(result), do: ok(result)
defp ok(result), do: { :ok, result }
defp error(reason), do: { :error, reason }
end
Funkcja and_then jest przykładem funkcji powiązania (ang. bind): przyjmuje
wartość opakowaną w coś, a następnie stosuje funkcję do tej wartości, w celu
zwrócenia nowej opakowanej wartości. Korzystanie z funkcji and_then w potoku
wymaga trochę więcej znaków interpunkcyjnych, bo trzeba poinformować język
Elixir, żeby dokonał konwersji wywołań funkcji na wartości funkcji. Ten dodatkowy wysiłek jest jednak rekompensowany przez fakt, że funkcje transformacji
stają się proste: każda po prostu pobiera wartość (i wszelkie dodatkowe parametry) i zwraca {: ok, nowa_wartość} lub {:error, powód}.
Transformacje zmieniają programowanie
Myślenie o kodzie jako o ciągu (zagnieżdżonych) transformacji może doprowadzić do liberalizacji podejścia do programowania. Przyzwyczajenie się do tego
sposobu myślenia zajmuje trochę czasu, ale kiedy już nabierzesz takiego nawyku,
Twój kod stanie się czystszy, Twoje funkcje krótsze, a projekt bardziej płaski.
Warto spróbować.
Pokrewne podrozdziały

Temat 8., „Istota dobrego projektu”.

Temat 17., „Powłoki”.

Temat 26., „Jak zrównoważyć zasoby”.

Temat 28., „Eliminowanie sprzężeń”.

Temat 35., „Aktorzy i procesy”.
Ćwiczenia
21. Czy potrafisz wyrazić poniższe wymagania w postaci transformacji najwyższego poziomu? Oznacza to, że dla każdego z nich należy zidentyfikować
wejście i wyjście.
1. Dodanie do zamówienia podatku od wysyłki i podatku VAT.
2. Ładowanie przez aplikację informacji o konfiguracji z pliku o podanej
nazwie.
3. Logowanie użytkownika do aplikacji webowej.
4337ebf6db5c7cc89e4173803ef3875a
4
Podatek od dziedziczenia

191
22. Zidentyfikowałeś potrzebę walidacji pola wejściowego i jego konwersji
z ciągu znaków na liczbę całkowitą o wartości od 18 do 150. Całkowitą
transformację opisuje poniższy kod:
field contents as string
[walidacja i konwersja]
{:ok, value} | {:error, reason}
Napisz pojedyncze transformacje składające się na operację walidacja
i konwersja.
23. W ramce „Język X nie obsługuje potoków” napisaliśmy następujący kod:
const content = File.read(file_name);
const lines = find_matching_lines(content, pattern)
const result = truncate_lines(lines)
Wiele osób pisze kod obiektowy łącząc wywołania metod. Takie osoby
mogłyby ulec pokusie napisania kodu podobnego do poniższego:
const result = content_of(file_name)
. find_matching_lines(pattern)
. truncate_lines()
Jaka jest różnica pomiędzy tymi dwoma fragmentami kodu? Który, Twoim
zdaniem, wolimy?
31
39
Podatek od dziedziczenia
Chciałeś banana, a dostałeś goryla trzymającego banana i całą
dżunglę.
Joe Armstrong
Czy programujesz w języku obiektowym? Czy korzystasz z dziedziczenia?
Jeśli tak, to przestań to robić! Prawdopodobnie to nie jest mechanizm, którego
chciałbyś używać.
Zobaczmy dlaczego.
Wprowadzenie
Dziedziczenie w programowaniu po raz pierwszy pojawiło się w 1969 roku,
w języku Simula 67. Było to eleganckie rozwiązanie problemu kolejkowania
wielu typów zdarzeń na tej samej liście. W Simuli zastosowano podejście polegające na użyciu mechanizmu zwanego klasami przedrostkowymi (ang. prefix
classes). Można było napisać następujący kod:
link CLASS car;
... implementacja klasy car
link CLASS bicycle;
... implementacja klasy bicycle
4337ebf6db5c7cc89e4173803ef3875a
4
192 
Rozdział 5.
Zegnij lub złam
link jest klasą przedrostkową, która dodaje funkcjonalność jednokierunkowej
listy. Dzięki temu możemy dodać zarówno samochody, jak i rowery do listy pojazdów oczekujących na (załóżmy) światła. Zgodnie z aktualną terminologią, klasę
link nazwalibyśmy bazową.
Model mentalny, stosowany przez programistów Simuli, polegał na dołączeniu
danych egzemplarza i implementacji klasy link do implementacji klas car
i bicycle. Część reprezentowana przez klasę link była postrzegana niemal jak
kontener zawierający egzemplarze klas car i bicycle. W ten sposób uzyskano
rodzaj polimorfizmu: obiekty klas car i bicycle implementowały interfejs klasy
link, ponieważ oba zawierały kod tejże klasy.
Po Simuli powstał Smalltalk. Alan Kay, jeden z twórców tego języka, w odpowiedzi udzielonej w witrynie Quora w 2019 roku8 napisał, dlaczego w Smalltalku
było dziedziczenie:
Kiedy zaprojektowałem Smalltalka-72 — był to język bardzo nowoczesny
w porównaniu ze Smalltalkiem-71 — myślałem, że byłoby fajnie wykorzystać konstrukcje dynamiczne, podobne do stosowanych w Lispie, w celu
wykonywania eksperymentów z „programowaniem różnicowym” (czyli
różnymi sposobami osiągnięcia stanu dającego się określić słowami: „to
działanie jest podobne do tego, z pewnym wyjątkiem”).
To tworzenie klas potomnych służy wyłącznie dodawaniu zachowań.
Te dwa rodzaje dziedziczenia (które faktycznie miały ze sobą sporo wspólnego)
rozwijały się w ciągu kolejnych dziesięcioleci. Podejście języka Simula, w którym
dziedziczenie pojawiło się po raz pierwszy, było sposobem łączenia typów, co
było kontynuowane w takich językach, jak C++ i Java. Szkołę Smalltalka, gdzie
dziedziczenie było dynamiczną organizacją zachowań, zastosowano w takich
językach, jak Ruby i JavaScript.
Obecnie mamy do czynienia z pokoleniami programistów obiektowych, którzy
używają dziedziczenia z jednego z dwóch powodów: nie lubią pisania albo lubią
typy.
Ci, którzy nie lubią pisania, ratują swoje palce używając dziedziczenia, aby
dodać wspólną funkcjonalność z klasy bazowej do klas potomnych: zarówno
klasa User, jak i klasa Product są podklasami klasy ActiveRecord::Base.
Ci, którzy lubią typy, używają dziedziczenia do wyrażenia relacji między klasami:
Samochód jest rodzajem (relacja is-a) Pojazdu.
Niestety, z obydwoma rodzajami dziedziczenia wiążą się problemy.
8
https://www.quora.com/What-does-Alan-Kay-think-about-inheritance-in-object-orientedprogramming
4337ebf6db5c7cc89e4173803ef3875a
4
Podatek od dziedziczenia

193
Problemy związane z używaniem dziedziczenia
do współdzielenia kodu
Dziedziczenie wprowadza sprzężenia. Nie tylko klasa potomna jest sprzężona
z rodzicem, rodzicem rodzica i tak dalej, ale także kod, który wykorzystuje klasę
potomną, jest sprzężony z wszystkimi klasami nadrzędnymi. Oto przykład:
class Vehicle
def initialize
@speed = 0
end
def stop
@speed = 0
end
def move_at(speed)
@speed = speed
end
end
class Car < Vehicle
def info
"Jestem samochodem i jadę z prędkością #{@speed}"
end
end
# kod najwyższego poziomu
my_ride = Car.new
my_ride.move_at(30)
Gdy w kodzie najwyższego poziomu zostanie użyte wywołanie my_car.move_at,
faktycznie będzie wywołana metoda w klasie Vehicle, rodzicu klasy Car.
Wyobraźmy sobie teraz, że programista odpowiedzialny za klasę Vehicle zmienił
jej API tak, że metoda move_at stała się metodą set_velocity, a zmienna egzemplarza @speed zmieniła się na @velocity.
Oczekuje się, że zmiana API spowoduje awarię działania klientów klasy Vehicle.
Ale kod na najwyższym poziomie dalej będzie się kompilował, ponieważ będzie
używał klasy Car. To, co robi klasa Car w swojej implementacji, nie jest problemem kodu na najwyższym poziomie, ale podczas próby wykonania tego
kodu powstaje błąd. Podobnie nazwa zmiennej egzemplarza to wyłącznie
wewnętrzny szczegół implementacji, ale kiedy zmieni się klasa Vehicle, to równocześnie (niezauważalnie) dochodzi do awarii klasy Car.
To bardzo silne sprzężenie.
Problemy związane z użyciem dziedziczenia
do budowania typów
Niektórzy postrzegają dziedziczenie jako sposób definiowania nowych typów.
Ich ulubiony diagram projektowy pokazuje hierarchię klas. Postrzegają problemy
w taki sposób, w jaki wiktoriańscy naukowcy postrzegali naturę: jak coś, co
można podzielić na kategorie.
4337ebf6db5c7cc89e4173803ef3875a
4
194 
Rozdział 5.
Zegnij lub złam
Niestety, tego rodzaju schematy szybko rozrastają się do monstrualnych
rozmiarów. Dodawane są coraz to nowe warstwy w celu wyrażenia nawet
najmniejszych niuansów i różnic występujących pomiędzy klasami. Ta dodatkowa złożoność może spowodować, że aplikacja stanie się bardziej krucha,
a zmiany mogą wymagać ingerencji w wielu warstwach w górę i w dół.
Jeszcze poważniejsze problemy występują przy dziedziczeniu wielokrotnym.
Samochód może być rodzajem Pojazdu, ale również może być rodzajem Mienia,
PrzedmiotuUbezpieczenia, ZastawuPożyczki itd. Prawidłowe zamodelowanie takich
relacji wymagałoby wielokrotnego dziedziczenia.
Język C++ w latach dziewięćdziesiątych ubiegłego wieku przyniósł wielokrotnemu dziedziczeniu złą sławę z powodu wątpliwej jakości semantyki mającej
wyeliminować niejednoznaczności. W rezultacie w wielu językach obiektowych
dziedziczenie wielokrotne nie jest dostępne. W związku z tym, nawet jeśli podobają Ci się złożone drzewa typów, i tak nie będziesz w stanie dokładnie zamodelować swojej domeny.
WSKAZÓWKA NR 51
Nie płać podatku od dziedziczenia.
Istnieją lepsze alternatywy
Pozwólcie nam zasugerować trzy techniki, dzięki którym nigdy nie będziecie
potrzebowali używać dziedziczenia:

Interfejsy i protokoły.

Delegacje.

Domieszki (ang. mixins) i cechy (ang. traits).
4337ebf6db5c7cc89e4173803ef3875a
4
Podatek od dziedziczenia

195
Interfejsy i protokoły
Większość języków obiektowych pozwala wskazać, że klasa implementuje co
najmniej jeden zbiór zachowań. Można powiedzieć, na przykład, że klasa Car
implementuje zachowanie Drivable i Locatable. Składnia dla wyrażenia tego
stanu może być różna — w Javie może wyglądać następująco:
public
//
//
//
}
class Car implements Drivable, Locatable {
Kod klasy Car. Ten kod musi zawierać
zarówno funkcjonalność Drivable,
jak i Locatable
Drivable i Locatable to konstrukcje, które w Javie są nazywane interfejsami.
W innych językach nazywa się je protokołami, a w jeszcze innych cechami —
ang. traits (choć nie jest to ten mechanizm, który będziemy nazywali cechami
w dalszej części tego rozdziału).
Interfejsy definiujemy w następujący sposób:
public interface Drivable {
double getSpeed();
void stop();
}
public interface Locatable() {
Coordinate getLocation();
boolean locationIsValid();
}
Powyższe deklaracje nie tworzą kodu: one jedynie informują, że każda klasa,
która implementuje interfejs Drivable, musi implementować dwie metody: getSpeed
i stop, a klasa, która implementuje interfejs Locatable, musi implementować
metody getLocation i locationIsValid. To oznacza, że nasza poprzednia definicja
klasy Car będzie prawidłowa tylko wtedy, gdy będzie zawierała wszystkie te
cztery metody.
Potęga interfejsów i protokołów wynika stąd, że możemy wykorzystać je jako
typy, a każda klasa, która implementuje odpowiedni interfejs, będzie kompatybilna z tym typem. Jeśli zarówno klasa Car, jak i Phone implementują interfejs
Locatable, będziemy mogli zapisać egzemplarze obu tych klas na liście obiektów
Locatable:
List<Locatable> items = new ArrayList<>();
items.add(new Car(...));
items.add(new Phone(...));
items.add(new Car(...));
// ...
Następnie możemy przetwarzać tę listę mając świadomość, że każdy element
ma metody getLocation i locationIsValid:
void printLocation(Locatable item) {
if (item.locationIsValid() {
print(item.getLocation().asString());
4337ebf6db5c7cc89e4173803ef3875a
4
196 
Rozdział 5.
Zegnij lub złam
}
// ...
items.forEach(printLocation);
WSKAZÓWKA NR 52
Wykorzystuj interfejsy do wyrażania polimorfizmu.
Interfejsy i protokoły pozwalają korzystać z polimorfizmu bez dziedziczenia.
Delegacje
Dziedziczenie zachęca programistów do tworzenia klas, których obiekty mają
dużą liczbę metod. Jeśli klasa bazowa zawiera 20 metod, a jej klasa potomna
chce skorzystać tylko z dwóch spośród nich, jej obiekty nadal będą zawierały
18 zupełnie niepotrzebnych metod. Taka klasa traci kontrolę nad swoim interfejsem. Jest to częsty problem — w wielu frameworkach oferujących funkcjonalności utrwalania i obsługi interfejsów użytkownika, komponenty są potomkami pewnej klasy bazowej:
class Account < PersistenceBaseClass
end
Klasa Account przenosi teraz całość interfejsu API utrwalania. Zamiast tego można
skorzystać z alternatywy w postaci delegacji, tak jak w poniższym przykładzie:
class Account
def initialize(. . .)
@repo = Persister.for(self)
end
def save
@repo.save()
end
end
Teraz żadna część interfejsu API frameworka nie jest ujawniona klientom klasy
Account: to sprzężenie zostało wyeliminowane. Ale to nie wszystko. Teraz, kiedy
nie jesteśmy już ograniczeni przez API używanego frameworka, możemy swobodnie utworzyć taki interfejs API, jakiego potrzebujemy. Co prawda mogliśmy to
zrobić wcześniej, ale zawsze narażaliśmy się na ryzyko, że napisany interfejs
można ominąć i zamiast niego użyć API utrwalania. Teraz mamy kontrolę nad
wszystkim.
WSKAZÓWKA NR 53
Stosuj delegacje do usług. Relacja has-a ma przewagę nad relacją is-a.
W gruncie rzeczy możemy pójść o krok dalej. Dlaczego klasa Account powinna
wiedzieć, jak powinna się utrwalać? Czy jej zadaniem nie jest znajomość i egzekwowanie reguł biznesowych dotyczących konta?
4337ebf6db5c7cc89e4173803ef3875a
4
Podatek od dziedziczenia
class
#
end
class
#
#
end

197
Account
wyłącznie kod dotyczący konta
AccountRecord
opakowuje konto możliwościami
jego pobierania i zapisywania
Teraz naprawdę wyeliminowaliśmy sprzężenia, ale stało się to pewnym kosztem.
Trzeba napisać więcej kodu, a pewna jego część będzie musiała być napisana
w wielu miejscach: na przykład jest wysoce prawdopodobne, że wszystkie nasze
klasy rekordów będą potrzebowały metody find.
Na szczęście ten problem można rozwiązać za pomocą domieszek i cech.
Domieszki, cechy, kategorie, rozszerzenia protokołów…
W naszej branży kochamy nadawać nazwy. Dość często nadajemy wiele nazw
tej samej rzeczy. Im więcej, tym lepiej, prawda?
Właśnie z takim zjawiskiem mamy do czynienia w przypadku domieszek. Podstawowa idea jest prosta: chcemy mieć możliwość rozszerzania klas i obiektów
o nowe funkcje bez użycia dziedziczenia. W związku z tym tworzymy zbiór tych
funkcji, nadajemy temu zbiorowi nazwę, a następnie w jakiś sposób rozszerzamy
za ich pomocą klasę lub obiekt. W tym momencie utworzyliśmy nową klasę
lub obiekt, które łączą w sobie możliwości oryginału i wszystkich jego domieszek.
W większości przypadków będziemy w stanie tworzyć takie rozszerzenia, nawet
jeśli nie mamy dostępu do kodu źródłowego rozszerzanej klasy.
Implementacja i nazwa tej własności są różne w różnych językach. W tej
książce będziemy nazywać ją domieszką (ang. mixin), ale chcielibyśmy, aby
Czytelnicy myśleli o niej jak o własności niezależnej od języka. Ważną rzeczą
są możliwości, jakie dają wszystkie te implementacje: łączenie funkcji pomiędzy
kodem istniejącym i nowym.
W ramach przykładu powróćmy do naszej klasy AccountRecord. Klasa Account
Record w postaci, w jakiej ją pozostawiliśmy, musiała mieć świadomość istnienia
zarówno kont, jak i frameworka utrwalania. Musiała również oddelegować
wszystkie metody w warstwie utrwalania, które chciała udostępnić do świata
zewnętrznego.
Domieszki oferują nam rozwiązanie alternatywne. Po pierwsze, możemy napisać domieszkę, która implementuje (na przykład) dwie lub trzy standardowe
metody wyszukiwania. Następnie możemy ją dodać do klasy AccountRecord.
Możemy również dodać domieszkę do nowych klas, w celu dodania do nich funkcjonalności utrwalania:
mixin CommonFinders {
def find(id) { ... }
def findAll() { ... }
end
4337ebf6db5c7cc89e4173803ef3875a
4
198 
Rozdział 5.
Zegnij lub złam
class AccountRecord extends BasicRecord with CommonFinders
class OrderRecord extends BasicRecord with CommonFinders
Możemy pójść znacznie dalej. Na przykład wszyscy wiemy, że obiekty biznesowe
wymagają kodu walidacji, który jest potrzebny do zapobieżenia wykorzystywaniu w obliczeniach nieprawidłowych danych. Co jednak dokładnie mamy na
myśli przez walidację?
Na przykład w przypadku konta istnieje wiele różnych warstw walidacji, które
można zastosować:

Sprawdzenie, czy skrót hasła pasuje do hasła wprowadzonego przez użytkownika.

Walidacja danych formularza wprowadzanych przez użytkownika podczas
tworzenia konta.

Walidacja danych formularza wprowadzanych przez administratora aktualizującego dane użytkownika.

Walidacja danych dodanych do konta przez inne komponenty systemu.

Walidacja danych pod kątem ich spójności przed utrwaleniem.
Powszechnym (i naszym zdaniem dalekim od ideału) podejściem jest umieszczenie wszystkich walidacji w jednej klasie (obiekcie biznesowym lub obiekcie
utrwalania), a następnie dodanie flagi decydującej o tym, jaki kod walidacji
zostanie uruchomiony w jakich okolicznościach.
Uważamy, że lepszym sposobem jest zastosowanie domieszek w celu stworzenia
wyspecjalizowanych klas właściwych do zastosowania w odpowiednich sytuacjach:
class AccountForCustomer extends Account
with AccountValidations, AccountCustomerValidations
class AccountForAdmin extends Account
with AccountValidations, AccountAdminValidations
Tutaj obie klasy potomne zawierają kod walidacji wspólny dla wszystkich obiektów reprezentujących konta. Wariant dla klienta zawiera również kod walidacji
właściwy dla API wykorzystywanego na styku z klientem, natomiast wariant
dla administratora zawiera (przypuszczalnie mniej restrykcyjny) kod walidacji
funkcji administratora.
Teraz, dzięki przekazywaniu egzemplarzy klas AccountForCustomer lub AccountFor
Admin, nasz kod automatycznie zapewni zastosowanie odpowiednich mechanizmów walidacji.
WSKAZÓWKA NR 54
Korzystaj z domieszek w celu współdzielenia funkcji.
4337ebf6db5c7cc89e4173803ef3875a
4
Konfiguracja
 199
Dziedziczenie rzadko jest dobrym rozwiązaniem
Zaprezentowaliśmy trzy alternatywy dla tradycyjnego dziedziczenia klas:

Interfejsy i protokoły.

Delegacje.

Domieszki i cechy.
Każdy z tych sposobów może się lepiej sprawdzać w różnych okolicznościach,
w zależności od tego, czy naszym celem jest udostępnianie informacji o typach,
dodawanie funkcjonalności, czy też udostępnianie metod. Tak jak zawsze
w programowaniu, należy dążyć do stosowania techniki, która najlepiej wyraża
nasze zamiary.
Warto również zadbać o to, by nie ciągnąć za bananem całej dżungli.
Pokrewne podrozdziały

Temat 8., „Istota dobrego projektu”.

Temat 10., „Ortogonalność”.

Temat 28., „Eliminowanie sprzężeń”.
Wyzwania

32
40
Gdy następnym razem zauważysz, że masz zamiar stosować dziedziczenie,
poświęć chwilę, aby zbadać inne opcje. Czy to, czego chcesz, można osiągnąć za pomocą interfejsów, delegacji i (lub) domieszek? Czy dzięki temu
możesz zmniejszyć poziom sprzężeń?
Konfiguracja
Niech wszystkie rzeczy mają swoje miejsce; niech każda część Twojej
działalności ma swój czas.
Benjamin Franklin, Trzynaście cnót, autobiografia
Gdy Twój kod korzysta z wartości, które mogą ulec zmianie po przekazaniu
aplikacji do produkcji, przechowuj te wartości na zewnątrz aplikacji. Gdy Twoja
aplikacja będzie działać w różnych środowiskach i potencjalnie dla różnych
klientów, przechowuj wartości specyficzne dla środowiska i dla klientów poza
aplikacją. W ten sposób możesz sparametryzować swoją aplikację — kod dostosowuje się do miejsc, w których działa.
WSKAZÓWKA NR 55
Parametryzuj aplikacje korzystając z zewnętrznej konfiguracji
4337ebf6db5c7cc89e4173803ef3875a
4
200 
Rozdział 5.
Zegnij lub złam
Do danych, które zazwyczaj umieszczamy w konfiguracji, należą:

Poświadczenia do zewnętrznych usług (bazy danych, zewnętrznych API, itp.).

Poziomy logowania i miejsca docelowe.

Port, adres IP, nazwa komputera oraz nazwy klastrów wykorzystywane przez
aplikację.

Parametry walidacji specyficzne dla środowiska.

Zbiory zewnętrznych parametrów, na przykład stawki podatkowe.

Szczegóły formatowania specyficzne dla lokalizacji.

Klucze licencyjne.
Ogólnie rzecz biorąc, poszukaj wszystkiego, o czym wiesz, że będzie musiało
się zmieniać, a co można wyrazić na zewnątrz głównej treści kodu, i umieść to
w zestawie konfiguracyjnym.
Konfiguracja statyczna
Wiele frameworków i sporo niestandardowych aplikacji przechowuje konfigurację w płaskich plikach lub tabelach bazy danych. Jeśli informacje są zapisane
w płaskich plikach, istnieje trend do używania gotowych formatów tekstowych.
Obecnie dużą popularnością w tego typu zastosowaniach cieszą się formaty
YAML i JSON. Czasami aplikacje napisane w językach skryptowych używają
specjalnych plików źródłowych, dedykowanych do przechowywania wyłącznie
konfiguracji. Jeśli informacje mają strukturę i istnieje prawdopodobieństwo,
że będą zmienione przez klienta (na przykład stawki podatku VAT), to lepszym
rozwiązaniem może być zapisanie ich w tabeli bazy danych. Oczywiście można
używać obu sposobów, dzieląc informacje o konfiguracji zgodnie z zastosowaniem.
Niezależnie od zastosowanego formatu, konfiguracja jest wczytywana do aplikacji jako struktura danych, zwykle podczas uruchamiania aplikacji. Powszechnie ta struktura danych jest globalna. Uzasadnieniem jest to, że dzięki temu
sięgnięcie do zapisanych w niej wartości z dowolnej części kodu będzie łatwiejsze.
Nie zalecamy takiego postępowania. Zamiast tego lepiej opakować informacje
konfiguracyjne jakąś cienką warstwą API. Taki zabieg pozwala oddzielić kod od
szczegółów reprezentacji konfiguracji.
Konfiguracja jako usługa
Choć powszechnie stosowana jest konfiguracja statyczna, obecnie preferujemy
inne podejście. Nadal chcemy przechowywać dane konfiguracyjne na zewnątrz
aplikacji, ale zamiast trzymać je w płaskim pliku lub bazie danych, wolimy
udostępniać je za pośrednictwem API usługi. Takie podejście daje szereg korzyści:
4337ebf6db5c7cc89e4173803ef3875a
4
Konfiguracja
 201

Informacje o konfiguracji może współdzielić wiele aplikacji, a dzięki mechanizmom uwierzytelniania i kontroli dostępu można ograniczyć zakres
widocznych danych.

Zmiany w konfiguracji mogą być wprowadzane globalnie.

Dane konfiguracji mogą być zarządzane za pośrednictwem wyspecjalizowanego interfejsu użytkownika.

Dane konfiguracyjne stają się dynamiczne.
Ostatni punkt, mówiący o tym, że dane konfiguracyjne powinny być dynamiczne,
jest kluczowy w przypadku aplikacji o wysokiej dostępności. Pomysł, że powinniśmy zatrzymać i ponownie uruchomić aplikację, aby zmienić jeden parametr,
jest beznadziejnie oderwany od współczesnych realiów. Dzięki wykorzystaniu
usługi konfiguracji komponenty aplikacji mogą zarejestrować zainteresowanie
powiadomieniami o aktualizacjach parametrów, których używają, a usługa może
wysyłać do nich informacje zawierające nowe wartości w przypadku, gdy zostaną
zmienione.
Niezależnie od przyjętej formy, dane konfiguracyjne zarządzają zachowaniem
aplikacji w czasie jej działania. Gdy zmieni się wartość w konfiguracji, nie ma
potrzeby ponownego kompilowania kodu.
Nie należy pisać kodu-dodo
Bez zewnętrznej konfiguracji nasz kod nie jest odpowiednio elastyczny i nie
może dostosowywać się do zmiennych warunków. Czy to źle? Cóż, w naszym
świecie gatunki, które nie potrafią dostosowywać się do otoczenia, po prostu giną.
Ptaki dodo nie potrafiły przystosować się do obecności ludzi i zwierząt domowych na wyspie Mauritius, co szybko doprowadziło do wymarcia całego gatunku9.
Był to pierwszy udokumentowany przypadek zagłady całego gatunku rękami
człowieka.
Nie możemy pozwolić, aby nasz projekt (lub wręcz nasza kariera) podzielił los
ptaków dodo.
Pokrewne podrozdziały
9

Temat 9., „DRY— Przekleństwo powielania”.

Temat 14., „Języki dziedzinowe”.

Temat 16., „Potęga zwykłego tekstu”.

Temat 28., „Eliminowanie sprzężeń”.
Oczywiście, niemały wpływ na fatalny los tych łagodnych (choć głupich) ptaków miało
ich zabijanie dla zabawy przez osadników.
4337ebf6db5c7cc89e4173803ef3875a
4
202 
Rozdział 5.
Zegnij lub złam
Nie należy przesadzać
W pierwszym wydaniu tej książki, podobnie jak tu, proponowaliśmy używanie
zewnętrznej konfiguracji zamiast wpisywania jej w kod, ale najwyraźniej powinniśmy przekazywać instrukcje trochę bardziej szczegółowo. Każda porada może być
rozumiana skrajnie lub wykorzystywana w niewłaściwy sposób. Oto kilka przestróg:
Nie należy przesadzać. Jeden z naszych klientów podjął kiedyś decyzję, że każde pole
w jego aplikacji powinno być konfigurowalne. W rezultacie wprowadzenie nawet
najprostszej zmiany zajmowało tydzień, ponieważ trzeba było zaimplementować
zarówno pole danych, jak i cały kod administracyjny potrzebny do jego zapisywania
i edytowania. W konfiguracji było ponad 40 000 zmiennych, a kodowanie aplikacji stało
się koszmarem.
Nie należy umieszczać w konfiguracji danych wpływających na decyzje podejmowane
w aplikacji tylko z powodu lenistwa. Jeśli jest prowadzona debata o tym, czy funkcjonalność powinna działać w ten lub inny sposób, decyzja powinna należeć do
użytkowników. Trzeba wypróbować jeden sposób i uzyskać opinię na temat tego,
czy podjęta decyzja była dobra.
4337ebf6db5c7cc89e4173803ef3875a
4
Rozdział 6.
Współbieżność
Abyśmy wszyscy „byli na tej samej stronie”, zacznijmy od kilku definicji:
Współbieżność (ang. concurrency) jest wtedy, gdy dwa fragmenty kodu (lub większa ich liczba) działają tak, jakby były uruchomione w tym samym czasie. Przetwarzanie równoległe (ang. parallelism) zachodzi wtedy, gdy one rzeczywiście
działają w tym samym czasie.
Aby korzystać ze współbieżności, trzeba uruchomić kod w środowisku, w którym możemy przełączać wykonywanie pomiędzy różnymi częściami kodu podczas jego działania. Często implementujemy ten mechanizm za pomocą takich
technik, jak włókna (ang. fibers), wątki i procesy.
Do przetwarzania równoległego potrzebny jest sprzęt zdolny do realizacji dwóch
operacji w tym samym czasie. Może to być wiele rdzeni w procesorze, wiele
procesorów w komputerze lub wiele komputerów połączonych ze sobą.
36
Wszystko jest współbieżne
Napisanie kodu systemu przyzwoitej wielkości, który nie miałby elementów
współbieżnych, jest prawie niemożliwe. Może to być współbieżność wyrażona
jawnie albo zaszyta wewnątrz biblioteki. Jeśli chcesz, aby Twoja aplikacja obsługiwała problemy realnego świata, współbieżność jest wymagana. Zdarzenia
dzieją się asynchronicznie: użytkownicy komunikują się z aplikacją, pobierane
są dane, wywoływane usługi zewnętrzne — wszystko wydarza się w tym samym
czasie. Jeśli wymusisz, aby proces realizowany był szeregowo — najpierw dzieje
się jedna rzecz, potem następna i tak dalej — Twój system będzie ospały i prawdopodobnie nie będzie w pełni wykorzystywał możliwości sprzętu, na którym
działa.
W tym rozdziale zajmiemy się współbieżnością i przetwarzaniem równoległym.
4337ebf6db5c7cc89e4173803ef3875a
4
204 
Rozdział 6.
Współbieżność
Programiści często mówią o sprzężeniach występujących pomiędzy fragmentami
kodu. Wspominają o zależnościach oraz o tym, dlaczego z powodu tych zależności wprowadzanie zmian jest trudne. Istnieje jednak inna forma sprzężeń.
Związki czasowe występują w przypadku, gdy kod wymusza sekwencyjne
wykonywanie działań, które nie jest wymagane, aby rozwiązać problem. Czy
w Twoim kodzie operacja „tik” musi występować przed operacją „tak”? Nie może
tak być, jeśli ma być elastyczna. Czy Twój kod sięga do wielu usług backendowych sekwencyjnie — jedna po drugiej? Nie może tego robić, jeśli chcesz
utrzymać swoich klientów. Sposobom identyfikowania tego rodzaju sprzężeń
przyjrzymy się w podrozdziale „Eliminowanie związków czasowych”.
Dlaczego pisanie współbieżnego i równoległego kodu jest takie trudne? Jednym
z powodów jest to, że programowania uczyliśmy się korzystając z systemów
sekwencyjnych, a języki, którymi się posługujemy, mają cechy, które są stosunkowo bezpieczne, gdy są wykorzystywane sekwencyjnie, ale stają się trudne,
kiedy dwie rzeczy mogą się zdarzyć w tym samym czasie. Jednym z największych
winowajców jest „współdzielony stan”. Nie oznacza to tylko zmiennych globalnych: ze współdzielonym stanem mamy do czynienia za każdym razem, gdy co
najmniej dwa fragmenty kodu odwołują się do tego samego fragmentu zmiennych danych. W podrozdziale „Współdzielony stan jest zły” opisaliśmy kilka
obejść tego problemu, ale w gruncie rzeczy wszystkie one stwarzają ryzyko
popełniania błędów.
Jeśli to wprawia Cię w smutek, nil desperandum1! Są lepsze sposoby konstruowania współbieżnych aplikacji. Jednym z nich jest wykorzystanie modelu
aktorów, w którym niezależne procesy, które nie współdzielą żadnych danych,
komunikują się za pomocą kanałów, korzystając ze zdefiniowanej, prostej
semantyki. Teorię i praktykę tego podejścia omówimy w podrozdziale „Aktorzy
i procesy”.
Na koniec przyjrzymy się czarnym tablicom. Są to systemy, które działają jak
połączenie magazynu obiektów i inteligentnego brokera publikuj – subskrybuj.
W swojej pierwotnej formie systemy te tak naprawdę nigdy się nie przyjęły.
Obecnie jednak spotykamy coraz więcej implementacji warstw middleware
o semantyce zbliżonej do czarnych tablic. Jeśli te typy systemów są prawidłowo
stosowane, pozwalają w dużym stopniu wyeliminować sprzężenia.
Zarówno kod współbieżny, jak i przetwarzanie równoległe były niegdyś egzotyczne. Dziś tego rodzaju kod jest koniecznością.
33
37
Eliminowanie związków czasowych
Moglibyśmy zapytać: „czego dotyczą związki czasowe?”. Dotyczą wyłącznie czasu.
1
Z łacińskiego „nie rozpaczaj” — przyp. tłum.
4337ebf6db5c7cc89e4173803ef3875a
4
Eliminowanie związków czasowych
 205
Czas jest często ignorowanym aspektem architektur oprogramowania. Kwe-stią
czasu zajmujemy się na etapie przygotowywania harmonogramu prac oraz
podczas monitorowania liczby dni pozostałych do terminu wydania — nie o to
nam jednak chodzi w tym podrozdziale. Skoncentrujemy się raczej na roli czasu
jako elementu projektowania samego oprogramowania. Z naszego punktu widzenia czas ma dwa ważne aspekty: współbieżność (czyli operacje wykonywane
jednocześnie) i kolejność (czyli względne położenie operacji na osi czasu).
Okazuje się, że zwykle nie postrzegamy programowania przez pryzmat któregokolwiek z tych aspektów. Kiedy przystępujemy do projektowania architek-tury
lub pisania właściwego programu, wszystko wydaje nam się liniowe. Właśnie
w ten sposób myśli większość ludzi — najpierw trzeba zrobić to, po-tem zrobi się
tamto. Taki sposób postrzegania sekwencji działań prowadzi jednak do powstawania związków czasowych — wiązania działań w czasie. Metoda A zawsze musi
być wywoływana przed metodą B; jednocześnie można generować tylko jeden
raport; informacje o zdarzeniu kliknięcia przycisku otrzymamy dopiero po
odświeżeniu ekranu. „Tik” musi wystąpić przed „ta-kiem”.
Taki model jest nie tylko nieelastyczny, ale też niezbyt realistyczny.
Musimy mieć na uwadze współbieżność i myśleć o eliminowaniu wszelkich
zależności czasowych i kolejnościowych. W ten sposób możemy jednocześnie
zyskać elastyczność i ograniczyć liczbę zależności czasowych w wielu obszarach
analizy przepływu pracy, budowy architektury, projektowania i wdrażania.
W rezultacie powstaną systemy, które są łatwiejsze do analizy, które potencjalnie
szybciej reagują i działają bardziej niezawodnie.
Poszukiwanie współbieżności
W wielu projektach musimy modelować i analizować przepływ pracy użytkowników w ramach analizy wymagań. Chcemy określić, co może się dziać w tym
samym czasie i co musi mieć miejsce w określonej kolejności. Jednym ze sposobów osiągnięcia tego celu jest opisanie przepływu pracy w odpowiedniej notacji,
na przykład w formie diagramu czynności UML2.
WSKAZÓWKA NR 56
Warto analizować przepływ pracy, aby na tej podstawie poprawiać
współbieżność.
2
Chociaż UML stopniowo wychodzi z użycia, wiele schematów wchodzących w skład tej
notacji nadal stosuje się w takiej czy innej postaci. Jednym z przykładów jest bardzo
użyteczny diagram czynności. Aby uzyskać więcej informacji na temat wszystkich typów
diagramów UML, zobacz UML Distilled: A Brief Guide to the Standard Object Modeling
Language [Fow04].
4337ebf6db5c7cc89e4173803ef3875a
4
206 
Rozdział 6.
Współbieżność
Diagram czynności składa się ze zbioru czynności reprezentowanych przez prostokąty z zaokrąglonymi narożnikami. Strzałka wychodząca z pola czynności
prowadzi albo do drugiej czynności (która może rozpocząć się dopiero po zakończeniu pierwszej czynności), albo do grubej linii nazywanej paskiem synchronizacji. Po zakończeniu wszystkich czynności prowadzących do jednego paska
synchronizacji można przystąpić do dalszych czynności wskazywanych przez
strzałki wychodzące z tego paska. Czynność, do której nie prowadzą żadne
strzałki, można rozpocząć w dowolnej chwili.
Diagramów czynności można używać do zapewniania jak największej równoległości projektowanego systemu poprzez identyfikację czynności, które nie są,
a mogą być wykonywane równolegle.
Na przykład na potrzeby projektu blendera użytkownicy mogą opisać swój
dotychczasowy przepływ pracy w następujący sposób:
1. Otwórz blender.
2. Otwórz butelkę piña colada.
3. Przelej koktajl do blendera.
4. Odmierz połowę miarki białego rumu.
5. Wlej rum.
6. Dodaj dwie miarki lodu.
7. Zamknij blender.
8. Miksuj przez 2 minuty.
9. Otwórz blender.
10. Podaj szklanki.
11. Włóż do szklanek różowe parasolki.
12. Podaj napój gościom.
Jednak barman straciłby pracę, gdyby wykonywał te czynności krok po kroku —
jedną po zakończeniu drugiej. Mimo że użytkownicy opisują te czynności
w formie sekwencji działań i mimo że rzeczywiście można je wykonywać w ten
sposób, łatwo zauważyć, że wiele spośród tych punktów można równie dobrze
wykonywać równolegle. Do odnajdywania miejsc, gdzie potencjalnie można
zastosować współbieżność, wykorzystamy następujący diagram czynności.
4337ebf6db5c7cc89e4173803ef3875a
4
Eliminowanie związków czasowych
 207
Możliwość obserwacji miejsc, w których zależności rzeczywiście występują, jest
bardzo cenna. W tym przypadku wszystkie zadania najwyższego poziomu (1.,
2., 4., 10. i 11.) można wykonywać równolegle od samego początku. Czynności
3., 5. i 6. mogą być wykonywane równolegle po wymienionych zadaniach.
Gdybyśmy brali udział w zawodach polegających na przyrządzaniu drinków
piña colada na czas, taka optymalizacja mogłaby sporo zmienić.
Okazje do współbieżności
Diagramy czynności pokazują potencjalne obszary zastosowania współbieżności, ale nie dają żadnych informacji na temat tego, czy te obszary są warte
zbadania. W przykładzie z piña coladą barman musiałby mieć pięć rąk, aby móc
jednocześnie wykonać wszystkie potencjalne zadania początkowe.
Takie ograniczenia trzeba zidentyfikować na etapie projektowania. Kiedy spojrzymy na diagram aktywności, zauważymy, że czynność nr 8, miksowanie, zajmie
minutę. W tym czasie nasz barman może wziąć szklankę i parasolki (czynności
10 i 11) i prawdopodobnie będzie miał jeszcze czas, aby obsłużyć innego klienta.
4337ebf6db5c7cc89e4173803ef3875a
4
208 
Rozdział 6.
Współbieżność
Szybsze formatowanie
Niniejsza książka jest napisana zwykłym tekstem. Aby zbudować wersję do druku,
ebook lub cokolwiek innego, ten tekst jest przetwarzany przez potok narzędzi przetwarzających tekst. Niektóre wyszukują konkretnych konstrukcji (cytowań bibliografii, pozycji w skorowidzu, specjalnych znaczników wskazówek i tak dalej). Inne
działają na dokumencie jako całości.
Wiele narzędzi w potoku musi mieć dostęp do informacji zewnętrznych (odczyt
z plików, zapisywanie do plików, potoki za pośrednictwem programów zewnętrznych). Wszystkie te stosunkowo wolno wykonywane zadania dają nam możliwość
wykorzystania współbieżności: każdy krok w potoku jest w istocie wykonywany
współbieżnie — czyta informacje z poprzedniego etapu i zapisuje do następnego.
Ponadto niektóre etapy procesu wymagają stosunkowo intensywnego wykorzystania procesora. Jednym z nich jest przekształcanie wzorów matematycznych.
Z różnych względów historycznych konwersja każdego równania może trwać do
500 ms. Aby przyspieszyć przetwarzanie, możemy skorzystać ze współbieżności.
Ponieważ każdy wzór jest niezależny od innych, możemy dokonać konwersji każdego
z nich w osobnym, współbieżnym procesie i zebrać wyniki, kiedy będą dostępne,
a następnie wstawić je do książki.
W rezultacie proces budowania książki przebiega znacznie szybciej na maszynach
wielordzeniowych.
(Trzeba też przyznać, że podczas pracy odkryliśmy w naszym potoku szereg błędów
związanych ze współbieżnością…).
Właśnie tego szukamy podczas projektowania z myślą o współbieżności. Mamy
nadzieję znaleźć działania, które zajmują czas, ale nie jest to czas w naszym
kodzie. Odpytywanie bazy danych, dostęp do zewnętrznej usługi, czekanie na
wprowadzenie danych wejściowych przez użytkownika: wszystkie te działania
powodują przestoje w naszym programie do czasu, kiedy się zakończą. To są
właśnie możliwości wykonania działań w sposób bardziej produktywny niż
pozwolenie procesorowi, aby był bezczynny.
Okazje do przetwarzania równoległego
Przypomnijmy różnicę: współbieżność jest mechanizmem programowym, natomiast przetwarzanie równoległe dotyczy sprzętu. Jeśli mamy wiele procesorów,
lokalnie lub zdalnie, to jeśli uda nam się podzielić pracę pomiędzy te procesory,
będziemy mogli skrócić ogólny czas wykonywania zadania.
Idealnymi kandydatami do takiego podziału są te fragmenty pracy, które są
stosunkowo niezależne — takie, w których jeden proces może kontynuować
działanie nie oczekując niczego od innych. Powszechny wzorzec postępowania
w takiej sytuacji to zidentyfikowanie dużego zadania, podział go na niezależne
fragmenty, równoległe przetwarzanie każdego z nich, a następnie scalenie
wyników.
4337ebf6db5c7cc89e4173803ef3875a
4
Współdzielony stan jest zły

209
Ciekawym przykładem takiego mechanizmu zastosowanego w praktyce jest
sposób działania kompilatora języka Elixir. Kiedy rozpoczyna pracę, dzieli
budowany projekt na moduły i po kolei kompiluje każdy z nich. Czasami jeden
moduł zależy od innego — w takim przypadku jego kompilacja zatrzymuje się
do czasu, aż staną się dostępne wyniki pracy innego modułu. Zakończenie kompilacji modułu najwyższego poziomu oznacza, że zostały skompilowane wszystkie
zależności. Rezultatem jest szybki proces kompilacji, który korzysta ze wszystkich dostępnych rdzeni.
Identyfikowanie możliwości to łatwa część
Wróćmy do naszej aplikacji. Zidentyfikowaliśmy miejsca, w których będziemy
korzystać ze współbieżności oraz przetwarzania równoległego. Teraz trudniejsze
zadanie: trzeba znaleźć bezpieczny sposób implementacji. To będzie temat pozostałej części tego rozdziału.
Pokrewne podrozdziały

Temat 10., „Ortogonalność”.

Temat 26., „Jak zrównoważyć zasoby”.

Temat 28., „Eliminowanie sprzężeń”.

Temat 36., „Czarne tablice”.
Wyzwania

34
38
Ile czynności wykonujesz równolegle, gdy przygotowujesz się rano do pójścia
do pracy? Czy potrafisz to zaprezentować za pomocą diagramu czynności
UML? Czy potrafisz znaleźć sposób przyspieszenia swoich działań poprzez
zwiększenie współbieżności?
Współdzielony stan jest zły
Jesteś w swojej ulubionej restauracji. Skończyłeś jeść danie główne i pytasz
kelnera, czy jest jeszcze szarlotka. Patrzy przez ramię, widzi jeden kawałek
w gablocie i odpowiada, że tak. Zamawiasz ją i wzdychasz z zadowoleniem.
Tymczasem po drugiej stronie restauracji, inny klient zadaje innemu kelnerowi
to samo pytanie. Tamten kelner także potwierdza, że jest jeden kawałek, a klient
go zamawia.
Jeden z klientów będzie niezadowolony.
Zamień gablotę na wspólny rachunek bankowy, a następnie zastąp kelnerów
punktami sprzedaży urządzeń. Ty i Twoja partnerka w tym samym czasie
4337ebf6db5c7cc89e4173803ef3875a
4
210 
Rozdział 6.
Współbieżność
podjęliście decyzję o zakupie nowego telefonu, ale na rachunku pozostały środki
wystarczające do zakupu tylko jednego urządzenia. Ktoś — bank, sklep lub
Ty — będzie bardzo niezadowolony.
WSKAZÓWKA NR 57
Współdzielony stan jest zły.
Problemem jest współdzielony stan. Każdy z kelnerów w restauracji zaglądał
do gabloty nie zwracając uwagi na innych. W każdym punkcie sprzedaży sprawdzano środki na rachunku niezależnie od innych.
Nieatomowe aktualizacje
Spójrzmy na nasz przykład z kelnerami w taki sposób, jak byśmy pisali kod:
Dwaj kelnerzy pracują współbieżnie (a w rzeczywistości równolegle). Przyjrzyjmy
się kodowi:
if gablota.liczba_ciastek > 0
obiecaj_ciastko_klientowi()
gablota.weź_ciastko()
daj_ciastko_klientowi()
end
Kelner numer 1 pobiera aktualną liczbę ciastek i stwierdza, że jest jedno. Obiecuje ciastko klientowi. Ale w tym momencie pracuje kelner numer 2. On także
widzi, że liczba ciastek wynosi jeden i składa taką samą obietnicę swojemu
klientowi. Następnie jeden z dwóch kelnerów bierze ostatni kawałek ciastka,
a drugi kelner wchodzi w jakiś stan błędu (co prawdopodobnie wiąże się z kajaniem się przed klientem).
4337ebf6db5c7cc89e4173803ef3875a
4
Współdzielony stan jest zły

211
Problemem w tym przypadku nie jest to, że dwa procesy mogą pisać do tej
samej pamięci. Kłopot polega na tym, że żaden z procesów nie może zagwarantować, że jego widok pamięci jest spójny. W gruncie rzeczy, gdy kelner wykonuje metodę gablota.liczba_ciastek(), to kopiuje wartość z gabloty do własnej
pamięci. Jeśli wartość w pamięci gabloty się zmieni, to pamięć kelnerów (której
używają do podejmowania decyzji) jest już nieaktualna.
To wszystko dlatego, że pobieranie i aktualizowanie informacji o liczbie ciastek
nie jest operacją atomową: informacja ta może się zmienić w trakcie realizacji tych
dwóch operacji.
Co zatem zrobić, aby stała się atomowa?
Semafory i inne formy wzajemnego wykluczania
Semafor jest po prostu rzeczą, która w danym momencie może być w posiadaniu tylko jednej osoby. Możesz utworzyć semafor, a następnie użyć go do
kontroli dostępu do jakiegoś zasobu. W naszym przykładzie, możemy stworzyć
semafor do kontroli dostępu do gabloty z ciastkami i przyjąć konwencję, zgodnie
z którą każdy, kto chce zaktualizować zawartość gabloty, może to zrobić tylko
wtedy, kiedy ma semafor.
Powiedzmy, że zarząd restauracji postanawia rozwiązać problem z wykorzystaniem fizycznego semafora. Kładą plastikowego krasnala na gablocie z ciastkami. Aby jakikolwiek kelner mógł sprzedać ciastko, musi trzymać figurkę
w ręku. Po zakończeniu realizacji zamówienia (co oznacza dostarczenie ciastka
na stół) powinien odłożyć figurkę krasnala na miejsce, tak aby krasnal mógł
strzec skarbu w postaci ciastek podczas mediacji na temat następnego zamówienia.
Przyjrzyjmy się realizacji tego scenariusza w kodzie. Historycznie operacja
pobierania semafora nosiła nazwę P, a operacja jego zwalniania nazywała się V3.
Dziś używamy takich określeń jak lock (unlock), claim (release) i tak dalej.
case_semaphore.lock()
if gablota.liczba_ciastek > 0
obiecaj_ciastko_klientowi()
gablota.weź_ciastko()
daj_ciastko_klientowi()
end
case_semaphore.unlock()
W tym kodzie założono, że semafor został już stworzony i zapisany w zmiennej
case_semaphore.
3
Nazwy P i V pochodzą od pierwszych liter słów holenderskich. Istnieją jednak różne
opinie co do tego, o które dokładnie słowa chodzi. Twórca techniki, Edsger Dijkstra,
zasugerował zarówno słowa passering, jak i prolaag dla operacji P oraz vrijgave i ewentualnie verhogen dla operacji V.
4337ebf6db5c7cc89e4173803ef3875a
4
212 
Rozdział 6.
Współbieżność
Załóżmy, że obaj kelnerzy uruchamiają ten kod w tym samym czasie. Obaj
starają się zablokować semafor, ale tylko jednemu z nich się to uda. Ten, który
dostanie semafor, dalej działa w zwykły sposób. Ten, który nie dostaje semafora, jest zawieszony do czasu, aż semafor będzie dostępny (kelner czeka…).
Kiedy pierwszy kelner zrealizuje zamówienie, odblokowuje semafor, a drugi
kelner kontynuuje pracę. Widzi teraz, że nie ma ciastek w gablocie, przeprasza
więc klienta.
Z tym podejściem wiąże się kilka problemów. Prawdopodobnie najbardziej
istotne jest to, że działa ono tylko wtedy, kiedy każdy, kto ma dostęp do gabloty,
zgodzi się na konwencję posługiwania się semaforem. Jeśli ktoś o niej zapomni
(czyli jakiś programista napisze kod, który nie przestrzega konwencji), ponownie
powstanie chaos.
Tworzenie zasobów transakcyjnych
Nasz dotychczasowy projekt jest słaby, ponieważ deleguje odpowiedzialność za
ochronę dostępu do gabloty z ciastkami do osób, które z niej korzystają. Zmieńmy
go w taki sposób, by scentralizować tę kontrolę. Aby to zrobić, musimy zmienić
API tak, aby kelnerzy mogli sprawdzić liczbę ciastek, a jednocześnie wziąć
kawałek ciastka w tym samym wywołaniu:
kawałek_ciasta = gablota.weź_ciastko_jeśli_dostępne()
if kawałek_ciasta
daj_ciastko_klientowi()
end
Aby to rozwiązanie mogło działać, musimy napisać metodę, która działa jako
część samej gabloty:
def weź_ciastko_jeśli_dostępne()
####
if @kawałki.size > 0
#
aktualizuj_dane_sprzedaży(:ciastko) #
return @kawałki.shift
#
else
# nieprawidłowy kod!
false
#
end
#
end
####
Ten kod ilustruje powszechnie stosowane błędne podejście do problemu. Co
prawda przenieśliśmy operację dostępu do zasobów do centralnego miejsca, ale
nasza metoda nadal może być wywoływana z wielu współbieżnych wątków,
więc nadal musimy chronić ją semaforem:
def weź_ciastko_jeśli_dostępne()
@case_semaphore.lock()
if @kawałki.size > 0
aktualizuj_dane_sprzedaży(:ciastko)
return @kawałki.shift
else
false
4337ebf6db5c7cc89e4173803ef3875a
4
Współdzielony stan jest zły

213
end
@case_semaphore.unlock()
end
Nawet taki kod może działać niepoprawnie. Jeśli wystąpi wyjątek w metodzie
aktualizuj_dane_sprzedaży, to semafor nigdy nie zostanie odblokowany, a wszystkie
kolejne próby dostępu do gabloty z ciastkami będą zawieszone w nieskończoność. Trzeba obsłużyć tę sytuację:
def weź_ciastko_jeśli_dostępne()
@case_semaphore.lock()
try {
if @kawałki.size > 0
aktualizuj_dane_sprzedaży(:ciastko)
return @kawałki.shift
else
false
end
}
ensure {
@case_semaphore.unlock()
}
end
Ze względu na powszechność tego błędu, w wielu językach istnieją biblioteki,
które obsługują ten problem:
def weź_ciastko_jeśli_dostępne()
@case_semaphore.protect() {
if @kawałki.size > 0
aktualizuj_dane_sprzedaży(:ciastko)
return @kawałki.shift
else
false
end
}
end
Transakcje obejmujące wiele zasobów
W naszej restauracji właśnie zainstalowano zamrażarkę do lodów. Jeżeli klient
zamówi ciastko à la mode, kelner będzie musiał sprawdzić, czy są dostępne
zarówno ciastko, jak i lody. Moglibyśmy zmienić kod działania kelnera w taki
sposób, by przyjął następującą postać:
kawałek_ciasta = gablota.weź_ciastko_jeśli_dostępne()
gałka = zamrażarka.weź_loda_jeśli_dostępny()
if kawałek_ciasta && gałka
zrealizuj_zamówienie_klienta()
end
4337ebf6db5c7cc89e4173803ef3875a
4
214 
Rozdział 6.
Współbieżność
Takie rozwiązanie jednak nie zadziała. Co będzie, jeśli zażądamy kawałka
ciasta, ale gdy spróbujemy wziąć gałkę lodów, dowiemy się, że żadna nie jest
dostępna? Pozostaniemy z kawałkiem ciasta w ręku, z którym nie będziemy
mogli nic zrobić (bo nasz klient chce dostać ciastko z lodami). A fakt, że trzymasz ciastko w ręku świadczy o tym, że nie ma go w gablocie, więc nie jest
dostępne dla jakiegoś innego klienta, który (będąc purystą) nie chce ciastka
razem z lodami.
Możemy rozwiązać ten problem poprzez dodanie do gabloty metody pozwalającej zwrócić do niej ciastko. Musimy także dodać blok obsługi wyjątków, aby
mieć pewność, że nie będziemy blokować zasobów, jeśli coś się nie powiedzie:
kawałek_ciasta = gablota.weź_ciastko_jeśli_dostępne()
if kawałek_ciasta
try {
gałka = zamrażarka.weź_loda_jeśli_dostępny()
if gałka
try {
zrealizuj_zamówienie_klienta()
}
rescue {
zamrażarka.oddaj(gałka)
}
end
}
rescue {
gablota.oddaj(kawałek_ciasta)
}
end
Także to rozwiązanie jest dalekie od idealnego. Kod jest wyjątkowo brzydki:
wywnioskowanie co faktycznie robi, jest trudne: logika biznesowa jest powiązana z kodem porządkowania zasobów.
Wcześniej naprawiliśmy podobny problem poprzez przeniesienie kodu obsługi
zasobu do samego zasobu. W tym przypadku jednak mamy dwa zasoby. Czy
powinniśmy umieścić kod w klasie gabloty, czy zamrażarki?
Uważamy, że odpowiedź brzmi „nie” dla obu opcji. Zgodnie z pragmatycznym
podejściem, „szarlotka à la mode” to oddzielny zasób. Należałoby przenieść ten
kod do nowego modułu. Wtedy klient mógłby po prostu powiedzieć „Poproszę
szarlotkę z lodami” i to zamówienie albo by się powiodło, albo nie.
Oczywiście w realnym świecie istnieje wiele złożonych dań tego rodzaju. Nie
chcielibyśmy zatem pisać nowych modułów dla każdej możliwej kombinacji.
Zamiast tego trzeba by napisać reprezentację pozycji w menu, która zawierałaby referencje do jego komponentów. Następnie należałoby napisać generyczną
metodę get_menu_item, która wykonuje obsługę zasobów.
4337ebf6db5c7cc89e4173803ef3875a
4
Współdzielony stan jest zły

215
Aktualizacje bez transakcji
Wiele osób jako źródło problemów ze współbieżnością wskazuje współdzieloną
pamięć. W rzeczywistości jednak problemy mogą pojawić się wszędzie, gdzie kod
aplikacji współdzieli zmienne zasoby: pliki, bazy danych, zewnętrzne usługi
i tak dalej. Zawsze, kiedy co najmniej dwa egzemplarze kodu mogą uzyskać
dostęp do jakiegoś zasobu w tym samym czasie, mamy do czynienia z potencjalnym problemem.
Czasami zasób nie jest czymś oczywistym. Podczas pisania tego wydania książki
zaktualizowaliśmy zestaw narzędzi, aby wykonywać więcej zadań współbieżnie,
z wykorzystaniem wątków. To spowodowało problemy z budowaniem aplikacji,
choć działo się to w dziwaczny sposób i w przypadkowych miejscach. Częstą
przyczyną przewijającą się przez wszystkie błędy był brak możliwości znalezienia
plików lub katalogów, mimo że w rzeczywistości pliki były we właściwym miejscu.
W wyniku analizy kodu znaleźliśmy w nim kilka miejsc, w których następowała
tymczasowa zmiana bieżącego katalogu. W wersji bez współbieżności do naprawienia błędu wystarczyło, że kod przywrócił katalog. Ale w wersji współbieżnej,
jeden wątek zmieniał katalog, a następnie, już po zmianie, mógł zacząć działać
kolejny wątek. Ten wątek oczekiwał, że jest w oryginalnym katalogu, ale ponieważ bieżący katalog jest współdzielony między wątkami, tak nie było.
Z natury tego problemu wynika kolejna wskazówka:
WSKAZÓWKA NR 58
Losowe awarie często wynikają z problemów ze współbieżnością.
Inne rodzaje uzyskania dostępu na wyłączność
W większości języków są dostępne biblioteki dostarczające jakieś mechanizmy
wyłącznego dostępu do współdzielonych zasobów. Czasami są nazywane muteksami (od ang. mutex — mutual exclusion, czyli dosłownie: wzajemne wykluczanie), monitorami lub semaforami. Mechanizmy te są zaimplementowane jako
biblioteki.
Jednak w niektórych językach wsparcie dla współbieżności jest wbudowane
w samym języku. Na przykład w języku Rust istnieje pojęcie własności danych.
Tylko jedna zmienna (lub parametr) może w danym czasie posiadać referencję
do określonego elementu mutowalnych danych.
Można też argumentować, że w językach funkcyjnych, z powodu ich skłonności
do stosowania wszędzie niezmienności danych, implementacja współbieżności
jest prostsza. Jednak w tych językach wciąż napotykamy na te same problemy,
ponieważ w pewnym momencie jesteśmy zmuszeni do wkroczenia do prawdziwego, zmiennego świata.
4337ebf6db5c7cc89e4173803ef3875a
4
216 
Rozdział 6.
Współbieżność
Panie doktorze, to boli…
Jeśli po lekturze tego podrozdziału nie wyciągnąłeś żadnych innych wniosków,
zapamiętaj jedno: implementacja współbieżności w środowisku ze współdzielonymi zasobami jest trudna, a samodzielne zarządzanie nią jest pełne wyzwań.
Dlatego w roli puenty zacytujemy stary dowcip:
— Panie doktorze, boli mnie, kiedy to robię.
— To proszę tego nie robić.
W kolejnych kilku podrozdziałach zasugerujemy alternatywne i bezproblemowe
sposoby uzyskania korzyści ze współbieżności.
Pokrewne podrozdziały
35
39

Temat 10., „Ortogonalność”.

Temat 28., „Eliminowanie sprzężeń”.

Temat 38., „Programowanie przez koincydencję”.
Aktorzy i procesy
Gdyby nie było pisarzy, nikt nie napisałby opowieści.
Gdyby nie było aktorów, nikt nie powołałby opowieści do życia.
Angie-Marie Delsante
Wykorzystanie aktorów i procesów to ciekawy sposób implementacji współbieżności bez konieczności rozwiązywania problemów związanych z dostępem
do współdzielonej pamięci.
Zanim przejdziemy do szczegółów, zdefiniujemy co przez nie rozumiemy. Będzie
to brzmiało w sposób dość akademicki. Nie obawiaj się, wkrótce wszystko się
wyjaśni.

Aktor to niezależny, wirtualny procesor z własnym lokalnym (i prywatnym)
stanem. Każdy aktor ma swoją skrzynkę pocztową. Gdy w skrzynce pocztowej pojawi się wiadomość, a aktor jest w stanie spoczynku, uaktywnia się
i ją przetwarza. Gdy zakończy przetwarzanie, przetwarza kolejną wiadomość w skrzynce pocztowej lub, jeżeli skrzynka jest pusta, wraca do stanu
uśpienia.
Podczas przetwarzania wiadomości aktor może tworzyć innych aktorów,
wysyłać wiadomości do innych aktorów, których zna, oraz tworzyć nowy
stan, który stanie się stanem aktualnym, gdy będzie przetwarzana następna
wiadomość.
4337ebf6db5c7cc89e4173803ef3875a
4
Aktorzy i procesy

 217
Proces jest zazwyczaj bardziej ogólnym wirtualnym procesorem, często
implementowanym w systemie operacyjnym w celu ułatwienia współbieżności. Procesy mogą być ograniczone (zgodnie z konwencją) tak, by zachowywały się jak aktorzy. Właśnie takiego rodzaju procesami będziemy zajmowali się w tym podrozdziale.
Aktorzy mogą działać wyłącznie współbieżnie
Jest kilka elementów, których nie znajdziemy w definicji aktorów:

Nie ma jednej rzeczy, która posiada sterowanie. Nic nie planuje tego, co
będzie dalej, ani nie koordynuje przekazywania informacji z surowych
danych do ostatecznego wyjścia.

Jedyny stan, jaki istnieje w systemie, jest przechowywany w wiadomościach oraz w lokalnym stanie każdego z aktorów. Wiadomości nie mogą
być analizowane inaczej niż poprzez ich odczytanie przez odbiorcę, a lokalny
stan poza aktorem jest niedostępny.

Wszystkie wiadomości są jednokierunkowe. Nie istnieje pojęcie odpowiedzi. Jeśli chcesz, aby aktor zwrócił odpowiedź, powinieneś dołączyć adres
własnej skrzynki pocztowej w wysyłanej wiadomości, a aktor otrzymujący
wiadomość (ostatecznie) odeśle odpowiedź jako kolejną wiadomość przekazaną do tej skrzynki pocztowej.

Aktor przetwarza każdą wiadomość do końca i każdorazowo przetwarza
tylko jedną wiadomość.
W efekcie aktorzy działają równolegle, asynchronicznie i niczego nie współdzielą. Gdybyśmy mieli wystarczającą liczbę fizycznych procesorów, moglibyśmy
uruchomić na każdym osobnego aktora. Jeśli masz tylko jeden procesor, to
jakiś mechanizm zajmuje się przełączaniem kontekstu między aktorami. W każdym z tych przypadków kod uruchomiony wewnątrz aktorów jest taki sam.
WSKAZÓWKA NR 59
Do obsługi współbieżności używaj aktorów bez współdzielenia stanu.
Prosty aktor
Spróbujmy zaimplementować system obsługi restauracji korzystając z aktorów.
W naszym przypadku mamy trzech aktorów (klienta, kelnera i kawałek ciasta).
Ogólny przepływ wiadomości będzie wyglądać następująco:

My (jako pewnego rodzaju zewnętrzna istota — coś w rodzaju Boga) mówimy
klientowi, że jest głodny.

W odpowiedzi klient prosi kelnera o ciasto.
4337ebf6db5c7cc89e4173803ef3875a
4
218 
Rozdział 6.
Współbieżność

Kelner prosi gablotę z ciastami o wydanie ciasta dla klienta.

Jeśli w gablocie z ciastami jest dostępny kawałek ciasta, gablota prześle
go klientowi oraz powiadomi kelnera, aby dodał ciasto do rachunku.

Jeśli nie ma ciasta, gablota mówi o tym kelnerowi, a kelner przeprasza
klienta.
Zdecydowaliśmy, że zaimplementujemy ten kod w języku JavaScript, z wykorzystaniem biblioteki Nact4. Dodaliśmy do niej trochę kodu-opakowania, pozwalającego napisać aktorów jako proste obiekty, w których klucze oznaczają rodzaje
odbieranych wiadomości, a wartości są funkcjami uruchamianymi po odebraniu
określonej wiadomości (większość systemów korzystających z aktorów ma
podobną strukturę, ale szczegóły zależą od języka gospodarza).
Zacznijmy od klienta. Klient może odbierać trzy wiadomości.

Masz ochotę na ciasto (przesyłana przez kontekst zewnętrzny).

Na stole jest ciasto (przesyłana przez gablotę z ciastami).

Przykro mi, nie ma ciasta (przesyłana przez kelnera).
Oto kod:
concurrency/actors/index.js
const customerActor = {
'ochota na ciasto':(msg, ctx, state) => {
return dispatch(state.waiter,
{ type: "zamówienie", customer: ctx.self, wants: 'ciasto' })
},
'umieszczone na stole': (msg, ctx, _state) =>
console.log(`Klient ${ctx.self.name} widzi "${msg.food}" umieszczone
na stole`),
'nie ma ciasta': (_msg, ctx, _state) =>
console.log(`${ctx.self.name} jest niezadowolony…`)
}
Interesującym przypadkiem jest otrzymanie wiadomości „masz ochotę na ciasto”, którą następnie wysyłamy do kelnera (sposób poinformowania klienta
o istnieniu kelnera pokażemy wkrótce).
Oto kod kelnera:
concurrency/actors/index.js
const waiterActor = {
"zamówienie": (msg, ctx, state) => {
if (msg.wants == "ciasto") {
dispatch(state.pieCase,
{ type: "weź kawałek", customer: msg.customer, waiter: ctx.self })
}
else {
console.dir(`Nie wiem, jak zrealizować zamówienie ${msg.wants}`);
4
https://github.com/ncthbrt/nact
4337ebf6db5c7cc89e4173803ef3875a
4
Aktorzy i procesy
 219
}
},
"dodaj do zamówienia": (msg, ctx) =>
console.log(`Kelner dodaje ${msg.food} do zamówienia klienta
${msg.customer.name}`),
"error": (msg, ctx) => {
dispatch(msg.customer, { type: 'nie ma ciasta', msg: msg.msg });
console.log(`\nKelner przeprasza klienta ${msg.customer.name}: ${msg.msg}`)
}
};
Gdy kelner otrzyma od klienta wiadomość „zamówienie”, sprawdza, czy zamówienie dotyczy ciasta. Jeśli tak, to wysyła żądanie do gabloty z ciastami, przekazując referencje zarówno do samego siebie, jak i do klienta.
Gablota z ciastami ma swój stan: tablicę zawierającą wszystkie kawałki ciasta,
które się w niej znajdują (wkrótce zobaczymy, jak można to skonfigurować).
Gdy gablota otrzyma od kelnera wiadomość „weź kawałek”, sprawdza, czy jakiś
kawałek jeszcze w niej pozostał. Jeśli tak jest, to przekazuje kawałek ciasta
klientowi, prosi kelnera o zaktualizowanie zamówienia i, na koniec, zwraca
zaktualizowany stan zawierający o jeden kawałek mniej. Oto kod:
concurrency/actors/index.js
const pieCaseActor = {
'weź kawałek': (msg, context, state) => {
if (state.slices.length == 0) {
dispatch(msg.waiter,
{ type: 'error', msg: "nie ma ciasta", customer: msg.customer })
return state
}
else {
var slice = "ciasto "+ state.slices.shift();
dispatch(msg.customer,
{ type: 'umieszczone na stole', food: slice });
dispatch(msg.waiter,
{ type: 'dodaj do zamówienia', food: slice, customer:
msg.customer });
return state;
}
}
}
Chociaż często aktorzy są uruchamiani dynamicznie przez innych aktorów,
w naszym przypadku, dla uproszczenia, będziemy uruchamiać aktorów ręcznie.
Będziemy również przekazywać do każdego z nich jakiś stan początkowy:

Gablota z ciastami otrzyma początkową listę ciast, które zawiera.

Kelner otrzyma referencję do gabloty z ciastami.

Klient otrzyma referencję do kelnera.
4337ebf6db5c7cc89e4173803ef3875a
4
220 
Rozdział 6.
Współbieżność
concurrency/actors/index.js
const actorSystem = start();
let pieCase = start_actor(
actorSystem,
'gablota',
pieCaseActor,
{ slices: ["szarlotka", "brzoskwiniowe", "wiśniowe"] });
let waiter = start_actor(
actorSystem,
'kelner',
waiterActor,
{ pieCase: pieCase });
let c1 = start_actor(actorSystem, 'klient1',
customerActor, { waiter: waiter });
let c2 = start_actor(actorSystem, 'klient2',
customerActor, { waiter: waiter });
I na koniec uruchamiamy aktorów. Nasi klienci są łakomi. Klient nr 1 prosi o trzy
kawałki ciasta, a klient nr 2 prosi o dwa kawałki:
concurrency/actors/index.js
dispatch(c1, { type: 'ochota
dispatch(c2, { type: 'ochota
dispatch(c1, { type: 'ochota
dispatch(c2, { type: 'ochota
dispatch(c1, { type: 'ochota
sleep(500)
. then(() => {
stop(actorSystem);
})
na
na
na
na
na
ciasto',
ciasto',
ciasto',
ciasto',
ciasto',
waiter:
waiter:
waiter:
waiter:
waiter:
waiter
waiter
waiter
waiter
waiter
});
});
});
});
});
Gdy uruchomimy program, możemy zaobserwować komunikację pomiędzy
aktorami5. Kolejność wyświetlania się komunikatów może być inna:
$ node
Klient
Klient
Klient
Kelner
Kelner
Kelner
index.js
klient1 widzi
klient2 widzi
klient1 widzi
dodaje ciasto
dodaje ciasto
dodaje ciasto
"ciasto jabłkowe" umieszczone na stole
"ciasto brzoskwiniowe" umieszczone na stole
"ciasto wiśniowe" umieszczone na stole
jabłkowe do zamówienia klienta klient1
brzoskwiniowe do zamówienia klienta klient2
wiśniowe do zamówienia klienta klient1
Kelner przeprasza klienta klient2: nie ma ciasta
klient2 jest niezadowolony…
Kelner przeprasza klienta klient1: nie ma ciasta
klient1 jest niezadowolony…
5
Aby uruchomić ten kod, potrzebne są także funkcje pomocnicze, których tutaj nie
zaprezentowano. Potrzebny kod można pobrać z witryny FTP wydawnictwa Helion.
4337ebf6db5c7cc89e4173803ef3875a
4
Aktorzy i procesy
 221
Brak jawnej współbieżności
W modelu aktorów nie ma potrzeby pisania kodu do obsługi współbieżności,
ponieważ nie ma współdzielonego stanu. Nie ma też potrzeby jawnego kodowania
logiki od końca do końca typu „zrób to, zrób tamto”, ponieważ aktorzy ustalają
ją samodzielnie na podstawie wiadomości, które otrzymują.
Nie ma też różnicy w działaniu w zależności od stosowanej architektury. Ten
zestaw komponentów działa równie dobrze na jednym procesorze, na wielu
rdzeniach, czy też na wielu komputerach połączonych w sieci.
Erlang ustawia scenę
Świetny przykład implementacji aktorów można znaleźć w języku Erlang i jego
środowisku wykonawczym (choć twórcy Erlanga nawet nie czytali oryginalnego
artykułu opisującego architekturę aktorów). W Erlangu aktorów nazywa się
procesami, ale nie są to standardowe procesy w rozumieniu systemu operacyjnego. Podobnie jak aktorzy, których omówiliśmy, procesy Erlanga są lekkie
(można uruchomić ich miliony na jednym komputerze), a komunikują się
pomiędzy sobą poprzez wysłanie wiadomości. Każdy działa w izolacji od innych,
więc nie ma współdzielonego stanu.
Ponadto w środowisku wykonawczym Erlanga zaimplementowano system
nadzorcy, który zarządza życiem procesów — potencjalnie restartuje proces lub
zestaw procesów w przypadku awarii. Erlang oferuje również ładowanie kodu
„na gorąco”: pozwala zastąpić kod w uruchomionym systemie bez jego zatrzymywania. Systemy w Erlangu należą do najbardziej niezawodnych na świecie,
często ich dostępność osiąga poziom dziewięciu dziewiątek.
Ale Erlang (i wywodzący się z niego Elixir) nie jest unikatowy. Implementacje
aktorów istnieją dla większości języków. Warto zastanowić się nad ich użyciem
do implementacji współbieżności.
Pokrewne podrozdziały

Temat 28., „Eliminowanie sprzężeń”.

Temat 30., „Programowanie transformacyjne”.

Temat 36., „Czarne tablice”.
Wyzwania

Czy obecnie wykorzystujesz kod, który stosuje wzajemne wykluczanie
w celu ochrony współdzielonych danych? Zastanów się nad stworzeniem
prototypu tego samego kodu z wykorzystaniem aktorów.

Kod obsługi restauracji w architekturze aktorów ma wsparcie tylko dla
zamawiania kawałków ciasta. Rozszerz go o obsługę zamówień ciast à la
4337ebf6db5c7cc89e4173803ef3875a
4
222 
Rozdział 6.
Współbieżność
mode z oddzielnymi agentami zarządzającymi kawałkami ciasta i gałkami
lodów. Zorganizuj kod w taki sposób, aby można było obsłużyć sytuacje,
gdy zabraknie jednego lub drugiego składnika.
36
40
Czarne tablice
Wszystko jest napisane na ścianie...
Księga Daniela
Zastanówmy się nad przypadkiem, w którym detektywi wykorzystują czarną
tablicę do koordynowania i rozwiązywania śledztwa w sprawie morderstwa.
Główny inspektor zaczyna od ustawienia dużej, czarnej tablicy w sali konferencyjnej. Na tej tablicy zapisuje pojedyncze pytanie:
H. Domański (mężczyzna, żonaty): Wypadek? Morderstwo?
Czy Domański naprawdę spadł, czy został popchnięty? Każdy detektyw może
wnieść swój wkład do opowieści o tym potencjalnym morderstwie. Może wprowadzić fakty, zeznania świadków, dowolne uzyskane dowody kryminalistyczne
i tak dalej. W miarę gromadzenia danych, detektyw może zauważyć związki
i zapisać te obserwacje lub spekulacje na tablicy. Ten proces trwa i ewoluuje.
Do czasu zamknięcia sprawy bierze w nim udział wiele różnych osób i agentów.
Przykład czarnej tablicy pokazano na rysunku 2.
Oto kilka kluczowych cech podejścia typu „czarna tablica”:

Żaden z detektywów nie musi wiedzieć o istnieniu jakiegokolwiek innego
detektywa — wszyscy oglądają tablicę pod kątem nowych informacji i dodają
swoje odkrycia.

Detektywi mogą być wyszkoleni w różnych dziedzinach, mogą mieć różne
poziomy edukacji i wiedzy, a nawet działać w różnych strefach czasowych.
Każdy z nich dąży do rozwiązania zagadki, ale to wszystko, co ich łączy.

W trakcie procesu mogą przychodzić i odchodzić różni detektywi, mogą też
pracować na różnych zmianach.

Nie ma żadnych ograniczeń dotyczących tego, co może być umieszczone na
tablicy. Mogą to być zdjęcia, zeznania, fizyczne dowody i tak dalej.
Jest to forma współbieżności określana jako leseferyzm (od fr. laissez faire).
Detektywi są niezależnymi procesami, agentami, aktorami i tak dalej. Niektórzy
zapisują fakty na tablicy. Inni odczytują je z tablicy, starają się je łączyć lub
przetwarzać, a także dodają do tablicy nowe informacje. Stopniowo zawartość
tablicy pozwala im wyciągać wnioski.
Komputerowe systemy czarnych tablic pierwotnie były wykorzystywane w systemach sztucznej inteligencji, gdzie problemy do rozwiązania są obszerne i złożone — rozpoznawanie mowy, systemy wnioskowania oparte na wiedzy i tak dalej.
4337ebf6db5c7cc89e4173803ef3875a
4
Czarne tablice

223
Rysunek 2. Ktoś znalazł związek pomiędzy długami hazardowymi Domańskiego a billingami
telefonów. Być może Domański odbierał telefony z pogróżkami
Jednym z pierwszych systemów typu „czarna tablica” była Linda Davida Gelerntera. Fakty w tym systemie były przechowywane w postaci typowanych krotek.
Aplikacje mogły zapisywać nowe krotki do systemu Linda i odpytywać o istniejące krotki za pomocą swego rodzaju mechanizmu dopasowywania wzorców.
Później pojawiły się rozproszone systemy typu „czarna tablica”, takie jak JavaSpaces i T Spaces. W tych systemach można było przechowywać na tablicy
aktywne obiekty Javy — nie tylko dane — i pobierać je za pomocą częściowego
dopasowywania pól (za pośrednictwem szablonów i symboli wieloznacznych)
lub podtypów. Dla przykładu załóżmy, że mamy typ Author, który jest podtypem
typu Person. Można przeszukiwać tablicę zawierającą obiekty Person przy użyciu
szablonu Author dla pola lastName o wartości „Szekspir”. Dzięki temu w odpowiedzi znalazłby się William Szekspir, który był autorem, ale nie znalazłby się
Fred Szekspir, który był ogrodnikiem.
Systemy te nigdy się nie przyjęły. Naszym zdaniem po części dlatego, że jeszcze
nie było zapotrzebowania na tego rodzaju współbieżne przetwarzanie.
Czarna tablica w praktyce
Załóżmy, że piszemy program, którego zadaniem jest przyjmowanie i przetwarzanie wniosków o kredyty hipoteczne lub pożyczki gotówkowe. Przepisy regulujące ten obszar są niezwykle złożone, swoje zdania wyrażają w nich instytucje federalne, stanowe i samorządowe. Kredytodawcy muszą udowodnić, że nie
ujawnią pewnych informacji, muszą prosić o pewne informacje, ale nie mogą
zadawać niektórych pytań i tak dalej, i tak dalej.
Poza „wyziewami” z obowiązującego prawa, trzeba także rozwiązać następujące
problemy:
4337ebf6db5c7cc89e4173803ef3875a
4
224 
Rozdział 6.
Współbieżność

Odpowiedzi mogą nadchodzić w dowolnej kolejności. Na przykład zapytania do biura informacji kredytowej lub badanie stanu prawnego może
zająć dużo czasu, natomiast takie dane, jak nazwisko lub adres, mogą być
dostępne natychmiast.

Zbieranie danych może być wykonywane przez różnych ludzi, rozproszonych w różnych biurach, a nawet w różnych strefach czasowych.

Niektóre operacje gromadzenia danych mogą być wykonywane automatycznie przez inne systemy. Takie dane mogą również nadchodzić asynchronicznie.

Niemniej jednak niektóre dane mogą nadal zależeć od innych. Na przykład
możesz nie być w stanie rozpocząć badania stanu prawnego samochodu
do czasu otrzymania dowodu jego własności lub ubezpieczenia.

Nadejście nowych danych może inicjować nowe zapytania i strategie.
Załóżmy, że z Biura Informacji Kredytowej nadchodzi raport daleki od
doskonałego. Teraz trzeba dostarczyć pięć dodatkowych formularzy i być
może jeszcze próbkę krwi.
Można spróbować obsłużyć każdą możliwą kombinację i okoliczność za pomocą
systemu bazującego na przepływach pracy. Istnieje wiele takich systemów, ale
mogą być złożone i wymagają napisania obszernego kodu. Gdy zmienią się
przepisy, trzeba zreorganizować przepływ pracy. Czasami także zmieniają się
procedury, co powoduje konieczność przepisywania kodu.
Eleganckim rozwiązaniem trudności, jakie tu występują, jest zastosowanie
systemu typu „czarna tablica” łącznie z silnikiem przetwarzania reguł. Kolejność
napływu danych jest bez znaczenia: opublikowanie faktu może inicjować odpowiednie reguły. Można również w łatwy sposób obsługiwać sprzężenia zwrotne:
wyjście z dowolnego zestawu reguł może być publikowane na tablicy i powodować zadziałanie kolejnych reguł.
WSKAZÓWKA NR 60
Korzystaj z systemów typu „czarne tablice” do koordynowania przepływów pracy.
Systemy wymiany komunikatów mogą działać
jak czarne tablice
W czasie, kiedy piszemy drugie wydanie tej książki, wiele aplikacji konstruuje
się w postaci niewielkich, odizolowanych od siebie usług, które komunikują się
między sobą za pomocą jakiejś formy systemu przekazywania komunikatów.
Te systemy wymiany komunikatów (takie jak Kafka i NATS) robią znacznie
więcej niż wysyłanie danych z punktu A do B. W szczególności oferują one
mechanizmy utrwalania (w postaci dziennika zdarzeń) oraz możliwość pobierania
4337ebf6db5c7cc89e4173803ef3875a
4
Czarne tablice

225
wiadomości za pomocą jakiejś formy mechanizmu dopasowywania wzorców.
Dzięki temu można je wykorzystać zarówno jako system czarnych tablic, jak
i platformę, na której można uruchomić kilka egzemplarzy aktorów.
Ale to nie jest takie proste…
Aktorzy i (lub) czarna tablica i (lub) mikroserwisy jako podejścia do architektury
usuwają z aplikacji całą klasę potencjalnych problemów współbieżności. Ale
uzyskanie tych korzyści wiąże się z kosztami. Wymienione podejścia są trudniejsze do analizy, ponieważ wiele działań jest wykonywanych pośrednio. Stosowanie tych podejść pomaga w utrzymaniu centralnego repozytorium formatów
wiadomości i (lub) interfejsów API, szczególnie jeśli repozytorium może wygenerować kod i dokumentację. Potrzebne jest również dobre oprzyrządowanie
pozwalające śledzić wiadomości i fakty podczas ich przekazywania w systemie
(przydatną techniką jest dodanie unikatowego identyfikatora śledzenia, gdy
zostanie zainicjowana określona funkcja biznesowa, i propagowanie tego identyfikatora do wszystkich aktorów, których to interesuje. Następnie można zrekonstruować to, co się dzieje, na podstawie logów).
Te rodzaje systemów mogą być też bardziej kłopotliwe do wdrażania i zarządzania, gdyż istnieje w nich więcej „ruchomych części”. Do pewnego stopnia jest
to równoważone przez fakt, że system jest bardziej ziarnisty i może być aktualizowany poprzez zastępowanie poszczególnych aktorów, a nie całego systemu.
Pokrewne podrozdziały

Temat 28., „Eliminowanie sprzężeń”.

Temat 29., „Żonglerka realnym światem”.

Temat 33., „Eliminowanie związków czasowych”.

Temat 35., „Aktorzy i procesy”.
Ćwiczenia
24. Czy system w stylu „czarna tablica” jest odpowiedni dla poniższych zastosowań? Dlaczego tak lub dlaczego nie?
Przetwarzanie obrazów. Chcesz utworzyć szereg równoległych procesów do
przechwytywania fragmentów obrazu, przetwarzania ich i zwracania przetworzonego fragmentu.
Kalendarze grupowe. Zarządzasz pracownikami rozproszonymi po całym
świecie, w różnych strefach czasowych i mówiących różnymi językami. Próbujesz zaplanować spotkanie.
Narzędzie do monitorowania sieci. System zbiera statystyki wydajności
i rejestruje raporty o błędach wykorzystywane przez agentów do wyszukiwania problemów w systemie.
4337ebf6db5c7cc89e4173803ef3875a
4
226 
Rozdział 6.
Współbieżność
Wyzwania

Czy używasz systemów typu „czarna tablica” w realnym świecie — tablicy
wiadomości na lodówce lub dużej, białej tablicy w pracy? Co sprawia, że
takie systemy są skuteczne? Czy wiadomości są publikowane w spójnym
formacie? Czy ma to znaczenie?
4337ebf6db5c7cc89e4173803ef3875a
4
Rozdział 7.
Kiedy kodujemy…
Zgodnie z konwencjonalną wiedzą, od momentu wejścia projektu w fazę kodowania praca staje się w dużej mierze mechaniczna i sprowadza się do przepisywania projektu w formie wykonywalnych wyrażeń. Wydaje nam się, że właśnie to
nieuzasadnione przekonanie jest jedną z głównych przyczyn powstawania brzydkich, nieefektywnych programów z fatalną strukturą i trudnych w konserwacji.
Kodowanie nie jest czynnością mechaniczną. Gdyby było inaczej, wszystkie te
narzędzia CASE, z którymi wiązano ogromne nadzieje na początku lat osiemdziesiątych ubiegłego wieku, dawno zastąpiłyby programistów. Pewne decyzje
należy podejmować nawet co minutę — każda taka decyzja wymaga należytej
ostrożności, rozwagi i oceny, czy tworzony program będzie cieszył się długim,
szczęśliwym i produktywnym życiem.
Nie wszystkie decyzje są podejmowane świadomie. Lepiej słuchać własnych
instynktów i nieświadomych myśli. Opisaliśmy ten proces w podrozdziale „Słuchaj swojego jaszczurczego mózgu”. Opowiemy w nim, jak słuchać bardziej
uważnie i szukać sposobów aktywnego reagowanie na te, czasami wyglądające
na nieistotne, myśli.
Ale słuchanie instynktów nie oznacza, że można po prostu lecieć na autopilocie.
Programiści, którzy aktywnie nie myślą o swoim kodzie, w praktyce programują
przez koincydencję — ich kod może działać, jednak trudno wskazać powód,
dla którego ten pozytywny scenariusz miałby się urzeczywistnić. W podrozdziale
„Programowanie przez koincydencję” będziemy przekonywali do większego
zaangażowania w proces kodowania.
O ile większość pisanego przez nas kodu jest wykonywana bardzo szybko, od
czasu do czasu musimy pisać algorytmy, które mogą zajmować sporo czasu
nawet na najszybszych procesorach. W podrozdziale „Szybkość algorytmu”
omówimy sposoby szacowania szybkości wykonywania kodu i zaproponujemy
kilka wskazówek, jak eliminować potencjalne problemy, zanim jeszcze wystąpią.
4337ebf6db5c7cc89e4173803ef3875a
4
228 
Rozdział 7.
Kiedy kodujemy…
Pragmatyczni programiści są krytyczni dla każdego kodu, także własnego.
Stale odkrywamy nowe możliwości doskonalenia naszych programów i projektów. W podrozdziale „Refaktoryzacja” omówimy techniki ułatwiające poprawianie
istniejącego kodu (także w samym środku projektu).
W testowaniu nie chodzi głównie o znajdowanie błędów, lecz o uzyskanie informacji zwrotnych o kodzie: aspektach projektu, API, sprzężeniach i tak dalej.
Oznacza to, że główne korzyści z testów osiągamy wtedy, gdy wymyślamy testy
i je piszemy, a nie tylko podczas ich uruchamiania. Tę koncepcję przeanalizujemy w podrozdziale „Kod łatwy do testowania”.
Oczywiście podczas testowania własnego kodu można kierować się swoimi
uprzedzeniami. W podrozdziale „Testowanie na podstawie własności” pokażemy,
jak skłonić komputer do realizacji obszernych testów oraz jak radzić sobie
z nieuniknionymi błędami, które się pojawią.
Kluczowe znaczenie ma pisanie kodu, który jest czytelny i łatwy do analizy.
Świat kodu jest brutalny, pełen złych aktorów, którzy aktywnie próbują włamać się do systemu i spowodować w nim szkody. W podrozdziale „Pozostań
w bezpiecznym miejscu” omówimy kilka podstawowych technik i podejść, które
pomogą Ci zachować bezpieczeństwo.
Jedną z najtrudniejszych rzeczy w tworzeniu oprogramowania jest wymyślanie
nazw. Musimy nazwać wiele rzeczy, a pod wieloma względami nazwy, które
wybieramy, definiują tworzoną przez nas rzeczywistość. Podczas kodowania
musimy mieć świadomość potencjalnego semantycznego dryfu.
Większość z nas prowadzi samochód w sposób, który nie odbiega zbytnio od
zachowania potencjalnego autopilota — nie wydajemy wprost naszej stopie
polecenia, aby nacisnęła na przykład pedał hamulca, ani naszym ramionom,
aby kręciły kierownicą — ograniczamy się raczej do myślenia: „zwolnij i skręć
w prawo”. Dobrzy kierowcy, którzy prowadzą naprawdę bezpiecznie, stale oceniają sytuację na drodze, analizując potencjalne problemy i dbając o to, aby
w razie nieoczekiwanych sytuacji znajdować się w możliwie dobrym miejscu. To
samo dotyczy kodowania — mimo że wiele naszych czynności jest rutynowych,
ciągłe rozważanie różnych wariantów może uchronić nas przed katastrofą.
37
36
Słuchaj swojego jaszczurczego mózgu
Tylko ludzie potrafią bezpośrednio na coś spojrzeć, uzyskać wszystkie
informacje potrzebne do dokładnego przewidywania, a czasami
dokonać dokładnej prognozy, by następnie jej zaprzeczyć.
Gavin de Becker, Dar strachu
4337ebf6db5c7cc89e4173803ef3875a
4
Słuchaj swojego jaszczurczego mózgu
 229
Dziełem życia Gavina de Becker jest pomaganie ludziom, aby się zabezpieczali.
Swój przekaz zamieścił w książce Dar strachu. Jak wykorzystywać sygnały
o zagrożeniu, które ostrzegają nas przed przemocą i zapewniają przeżycie.
Jednym z kluczowych poruszonych w niej tematów jest myśl, że ludzie, jako
rozwinięte istoty, nauczyli się ignorować naszą bardziej zwierzęcą stronę: nasze
instynkty oraz nasz jaszczurczy mózg. Gavin de Becker twierdzi, że większość
ludzi, którzy są atakowani na ulicy, jeszcze przed atakiem odczuwa dyskomfort
lub zdenerwowanie. Ci ludzie mówią sobie wtedy, że ich obawy są po prostu
głupie. Za chwilę z ciemności wyłania się postać…
Instynkty są odpowiedzią na wzorce załadowane do tej części naszego mózgu,
którą nie sterujemy świadomie. Niektóre są wrodzone, inne wyuczone przez
powtarzanie. W miarę zdobywania doświadczenia programisty, Twój mózg układa
warstwy ukrytej wiedzy: technik, które działają, i takich, które nie działają,
prawdopodobnych przyczyn błędów różnego typu — wszystkiego, co dostrzegasz
podczas swojej pracy. To właśnie ta część Twojego mózgu każe nacisnąć przycisk Zapisz plik, kiedy zaczynasz z kimś rozmawiać, nawet jeśli nie zdajesz
sobie sprawy z tego, że to robisz.
Bez względu na źródło, instynkty mają jedną wspólną cechę: nie wymagają słów.
Instynkty sprawiają, że czujesz, a nie myślisz. Kiedy zadziała instynkt, nie
zobaczysz migającej żarówki z bannerem owiniętym wokół niej. Zamiast tego
staniesz się nerwowy, poczujesz mdłości lub będziesz miał wrażenie, że po prostu masz zbyt dużo pracy.
Sztuką jest najpierw zauważyć co się dzieje, a następnie wywnioskować dlaczego. Na początek przyjrzyjmy się kilku typowym sytuacjom, w których ukryty
w Tobie jaszczur próbuje Ci coś powiedzieć. Następnie opowiemy, jak pozwolić
wyjść temu instynktownie działającemu mózgowi z jego ochronnej skorupy.
Obawa przed czystą kartką
Wszyscy boimy się pustego ekranu, samotnego migającego kursora otoczonego
całą masą niczego. Rozpoczynanie nowego projektu (lub nawet nowego modułu
w istniejącym projekcie) może być niepokojącym doświadczeniem. Wielu z nas
wolałoby odłożyć na później prace początkowe.
Uważamy, że są dwa problemy, które powodują to zjawisko. Oba mają to samo
rozwiązanie.
Jednym z problemów jest to, że Twój jaszczurczy mózg próbuje Ci coś powiedzieć; istnieje jakaś wątpliwość, która czai się tuż pod powierzchnią percepcji.
To bardzo ważne.
Jako programista wypróbowałeś wiele rzeczy i wiesz, które się sprawdziły,
a które nie. Gromadzisz swoje doświadczenia i wiedzę. Kiedy odczuwasz dokuczliwe wątpliwości lub doświadczasz pewnych oporów w obliczu zadania, może
się okazać, że Twoje doświadczenie stara się do Ciebie mówić. Uważaj na to.
4337ebf6db5c7cc89e4173803ef3875a
4
230 
Rozdział 7.
Kiedy kodujemy…
Może nie jesteś w stanie jednoznacznie stwierdzić, co dokładnie jest źle, ale daj
sobie czas, a Twoje wątpliwości prawdopodobnie skrystalizują się w coś bardziej
trwałego, w coś, czemu będziesz w stanie zaradzić. Niech Twoje instynkty mają
wpływ na Twoją wydajność.
Inny problem jest nieco bardziej prozaiczny: może po prostu boisz się, że popełnisz pomyłkę.
To rozsądna obawa. My, programiści, wkładamy w nasz kod dużą część naszej
osobowości; błędy popełnione podczas kodowania możemy odbierać jako zwierciadło naszych kompetencji. Być może istnieje w nas również element syndromu
oszusta; możemy myśleć, że ten projekt wykracza poza nasze możliwości. Nie
potrafimy dostrzec drogi do celu; dotarliśmy bardzo daleko, a następnie zostaliśmy zmuszeni do przyznania się, że się zgubiliśmy.
Walka z samym sobą
Czasami kod po prostu wypływa z naszego umysłu do edytora: pomysły stają się
bitami pozornie bez wysiłku.
Innym razem kodowanie odczuwamy jak spacer pod górę w błocie. Wykonanie
każdego kolejnego kroku wymaga ogromnego wysiłku, a po każdych trzech
krokach w górę, zsuwamy się o dwa w dół.
Ale będąc profesjonalistą, zachowujesz się jak żołnierz — kroczysz po błotnistym
szlaku, masz zadanie do wykonania. Niestety, jest to prawdopodobnie dokładne
przeciwieństwo tego, co powinieneś robić.
Twój kod próbuje Ci coś powiedzieć. Mówi, że to zadanie jest trudniejsze niż
powinno. Być może struktura lub projekt są złe, może rozwiązujesz niewłaściwy
problem, a może po prostu tworzysz mrowisko błędów. Niezależnie od powodu,
Twój jaszczurczy mózg wykrywa wnioski płynące z kodu i rozpaczliwie próbuje
skłonić Cię do słuchania.
Jak rozmawiać z jaszczurem
Dużo się mówi o słuchaniu instynktów, będącego poza naszą świadomością
jaszczurczego mózgu. Techniki zawsze są takie same.
WSKAZÓWKA NR 61
Słuchaj jaszczura, który jest wewnątrz Ciebie.
Po pierwsze przestań robić to, co robisz. Daj sobie trochę czasu i miejsca, aby
pozwolić Twojemu mózgowi się zorganizować. Przestań myśleć o kodzie i przez
jakiś czas zacznij robić coś, co nie wymaga zbyt wiele myślenia, z dala od klawiatury. Pójdź na spacer, wyjdź na obiad, porozmawiaj z kimś. Być może się
4337ebf6db5c7cc89e4173803ef3875a
4
Słuchaj swojego jaszczurczego mózgu
 231
prześpij. Pozwól, by pomysły same przeniknęły przez warstwy Twojego mózgu:
nie możesz tego wymusić. W końcu, jak bańka z powietrzem, zdołają przedostać
się do Twojej świadomości, a Ty przeżyjesz jeden ze słynnych momentów aha!
Jeśli to nie zadziała, spróbuj uzewnętrznić problem. Zacznij bazgrać na kartce
o kodzie, który piszesz, albo opowiedz o nim współpracownikowi (najlepiej
takiemu, który nie jest programistą), albo po prostu gadaj do swojej gumowej
kaczki. Przedstaw problem, który starasz się rozwiązać, różnym częściom Twojego mózgu i sprawdź, czy któraś z nich ma lepszy sposób na jego rozwiązanie.
Niezliczoną liczbę razy zdarzało się nam, że jeden z nas wyjaśniając problem
drugiemu, nagle wykrzyknął „No tak! Przecież to oczywiste!”. Po czym rozwiązał
problem, który go dręczył.
Ale być może próbowałeś wszystkich tych rzeczy i nadal tkwisz w martwym
punkcie. To czas do działania. Musisz powiedzieć swojemu mózgowi, że to, co
masz zamiar zrobić, nie ma znaczenia. Robimy to poprzez prototypowanie.
Przedstawienie czas zacząć!
Obaj (Andy i Dave) spędziliśmy wiele godzin gapiąc się na puste bufory edytora.
Pisaliśmy jakiś kod, potem gapiliśmy się w sufit, potem braliśmy kolejnego
drinka, po czym pisaliśmy trochę więcej kodu. Następnie zaczynaliśmy czytać
zabawną historię o kocie z dwoma ogonami, po czym pisaliśmy jeszcze trochę
więcej kodu, by w końcu wykonać operacje zaznacz wszystko i usuń, a potem
zaczynaliśmy od nowa. I tak w kółko.
Na przestrzeni lat udało nam się znaleźć sposób działania, który wydaje się
sprawdzać. Powiedz sobie, że musisz wykonać prototyp. Jeśli widzisz przed sobą
pusty ekran, poszukaj jakiegoś aspektu projektu, który chcesz przeanalizować.
Być może używasz nowego frameworka i chcesz się dowiedzieć, w jaki sposób
jest w nim realizowane wiązanie danych. A może jest to nowy algorytm, a Ty
chcesz się dowiedzieć, jak on działa w przypadkach brzegowych. A może chcesz
wypróbować kilka różnych stylów interakcji z użytkownikiem.
Jeśli pracujesz na istniejącym kodzie, który Cię ogranicza, odłóż go na bok
i stwórz prototyp czegoś podobnego.
Wykonaj następujące działania.
1. Napisz na karteczce „tworzę prototyp” i przyklej ją z boku Twojego ekranu.
2. Przypomnij sobie, że prototypy nie muszą w pełni działać. Przypomnij
sobie także, że prototypy się wyrzuca, nawet wtedy, gdy działają. Tworzenie prototypów to same korzyści.
3. W pustym buforze edytora stwórz komentarz opisujący jednym zdaniem,
czego chcesz się dowiedzieć lub co zrobić.
4. Zacznij kodować.
4337ebf6db5c7cc89e4173803ef3875a
4
232 
Rozdział 7.
Kiedy kodujemy…
Jeśli zaczynasz mieć wątpliwości, spójrz na karteczkę.
Jeśli podczas kodowania te dręczące Cię wątpliwości nagle skrystalizują się
w wyraźny problem, rozwiąż go.
Jeśli dojdziesz do końca eksperymentu i nadal będziesz czuć się nieswojo,
zacznij ponownie od spaceru, rozmowy i odpoczynku.
Ale z naszych doświadczeń wynika, że w pewnym momencie podczas tworzenia
pierwszego prototypu, zaskoczy Cię fakt, że zaczynasz nucić do muzyki, którą
słyszysz, i zaczniesz odczuwać przyjemność z tworzenia kodu. Nerwowość
wyparuje, a zastąpi ją poczucie pilnej potrzeby wykonania zadania!
Na tym etapie wiesz już, co robić. Usuń cały kod prototypu, wyrzuć karteczkę
i wypełnij pusty bufor edytora jasnym, błyszczącym, nowym kodem.
Nie liczy się tylko Twój kod
Duża część naszej pracy związana jest z korzystaniem z istniejącego kodu, często
pisanego przez inne osoby. Ci ludzie mają inne instynkty niż Ty, a zatem podejmują także inne decyzje. Niekoniecznie są one gorsze — po prostu inne.
Możesz czytać ich kod mechanicznie, przegryzać się przez niego i robić notatki
dotyczące rzeczy, które wydają się ważne. To uciążliwa praca, ale przynosi efekty.
Możesz także spróbować eksperymentu. Jeśli zauważysz, że coś zostało wykonane w sposób, który wydaje Ci się dziwny, zrób notatki. Rób tak przez jakiś czas
i szukaj wzorców. Jeśli odkryjesz, co było powodem takiego pisania kodu, może
się okazać, że zadanie zrozumienia go staje się dużo łatwiejsze. Będziesz mógł
świadomie stosować wzorce, które inni programiści stosowali instynktownie.
A przy okazji możesz nauczyć się czegoś nowego.
Nie liczy się sam kod
Ważną umiejętnością podczas kodowania jest zdolność słuchania własnego
wnętrza. Ale odnosi się to również do szerszej perspektywy. Czasami nie podoba
Ci się projekt albo jakieś wymaganie sprawia, że czujesz się nieswojo. Poświęć
chwilę na przeanalizowanie tych odczuć. Jeśli jesteś w otoczeniu współpracowników, wyraź je na głos. Zbadaj je. Być może coś czai się w ciemnej bramie.
Słuchaj swoich instynktów i unikaj problemów, zanim na Ciebie spadną.
Pokrewne podrozdziały

Temat 13., „Prototypy i karteczki samoprzylepne”.

Temat 22., „Dzienniki inżynierskie”.

Temat 46., „Rozwiązywanie niemożliwych do rozwiązania łamigłówek”.
4337ebf6db5c7cc89e4173803ef3875a
4
Programowanie przez koincydencję

233
Wyzwania

38
37
Czy jest coś, o czym wiesz, że powinieneś zrobić, ale to odkładasz, ponieważ Cię to przeraża lub uważasz, że jest zbyt trudne? Zastosuj techniki
opisane w tym podrozdziale. Ustaw timer na godzinę, może dwie, i obiecaj
sobie, że gdy zadzwoni dzwonek, usuniesz to, co przez ten czas zrobiłeś.
Czego się nauczyłeś?
Programowanie przez koincydencję
Czy kiedykolwiek oglądałeś stare czarno-białe filmy wojenne? Zmęczeni żołnierze ostrożnie przedzierają się przez zarośla. Przed nimi jest polana: czy nie
ma tam min i czy można przez nią przejść? Ewentualne pole minowe nie będzie
w żaden sposób oznaczone — nie będzie ostrzeżeń, drutu kolczastego ani kraterów po wybuchach. Żołnierz ostrożnie wbija w ziemię bagnet i natychmiast
upada, aby zminimalizować skutki eksplozji. Wybuch nie następuje. Żołnierz
posuwa się więc naprzód, pokonuje parę metrów i ponownie sprawdza teren,
nakłuwając ziemię bagnetem. Ostatecznie, przekonany o braku min oddział
wstaje i dumnie podąża naprzód, aby za chwilę wejść prosto na ukryte kawałek dalej miny.
Wstępne badanie terenu nie wykazało żadnych zagrożeń, ale przyszłość pokazała, że był to raczej przypadek. Początkowe szczęście doprowadziło do fałszywych wniosków, których efekt był katastrofalny.
Jako programiści także pracujemy na polach minowych. Codziennie czyhają na
nas setki najróżniejszych pułapek. Mając na uwadze przytoczoną historię żołnierzy, powinniśmy konsekwentnie unikać fałszywych wniosków. Powinniśmy
unikać programowania przez koincydencję (opierania się na samym szczęściu
i przypadkowych sukcesach) na rzecz umyślnego, przemyślanego programowania.
Jak programować przez koincydencję
Przypuśćmy, że Fred otrzymał zlecenie programistyczne. Napisał więc jakiś kod,
po czym sprawdził program w działaniu — wydawało się, że wszystko działa
prawidłowo. Fred napisał więcej kodu i sprawdził nowe rozwiązania — jego
program wciąż działał. Po kilku tygodniach kodowania w ten sposób program
nagle przestał działać. Co więcej, po wielogodzinnych próbach naprawiania
kodu Fred wciąż nie wiedział, co jest źródłem problemu. Fred równie dobrze
mógłby poświęcić wiele dni pracy na analizę i poprawianie swojego kodu,
a mimo to nigdy nie byłby w stanie zlokalizować i usunąć usterki. Cokolwiek
zrobi, program po prostu nie zadziała prawidłowo.
4337ebf6db5c7cc89e4173803ef3875a
4
234 
Rozdział 7.
Kiedy kodujemy…
Fred nie wie, dlaczego jego kod nie działa, ponieważ nigdy nie wiedział, dlaczego ten kod wcześniej działał. Wydawało się, że program działa (przynajmniej podczas ograniczonych testów wykonanych przez Freda), ale praktyka
pokazała, że był to raczej zbieg okoliczności, koincydencja. Na podstawie mylnego przeświadczenia o prawidłowości dotychczasowych rozwiązań Fred zmierzał wprost ku katastrofie. Większość inteligentnych ludzi słyszała podobne
historie, a mimo to powiela ten błąd. Nie możemy uzależniać funkcjonowania
naszych programów od zbiegu okoliczności, prawda?
Okazuje się, że każdemu to się zdarza. W pewnych przypadkach nietrudno
pomylić przypadek, łut szczęścia z przemyślanym planem. Przeanalizujmy kilka
przykładów.
Przypadkowa implementacja
Przypadkowa implementacja to wszystkie rozwiązania wynikające wprost
z niewłaściwego sposobu pisania własnego kodu. Taki tryb pracy zwykle
kończy się powstaniem nieudokumentowanego błędu lub warunków granicznych.
Przypuśćmy, że wywołujemy jakąś procedurę z błędnymi danymi. Procedura
odpowiada wówczas w określony sposób, a nasz dalszy kod opiera się właśnie
na tej odpowiedzi. Mimo to autor nigdy nie planował takiego działania wspomnianej procedury — co więcej, takie działanie nigdy nie było nawet rozważane. Po „naprawieniu” procedury może okazać się, że reszta kodu przestała
działać. W skrajnych przypadkach wywołana przez nas procedura może nawet
nie być projektowana z myślą o tych zastosowaniach, a mimo to będzie sprawiała wrażenie prawidłowej. Z podobnym problemem mamy do czynienia
podczas wywoływania funkcji w niewłaściwej kolejności lub w błędnym kontekście.
Tutaj wygląda na to, że Fred desperacko próbuje coś wyświetlić na ekranie za
pomocą jakiegoś frameworka renderowania GUI:
paint();
invalidate();
validate();
revalidate();
repaint();
paintImmediately(r);
Okazuje się jednak, że te procedury nigdy nie były projektowane z myślą
o wywoływaniu w ten sposób; chociaż wszystko zdaje się działać prawidłowo,
w rzeczywistości mamy do czynienia z koincydencją.
Jakby tego było mało, komponent ostatecznie został wyświetlony, powodując,
że Fred nawet nie spróbuje wrócić do tego zagadnienia, aby wyeliminować te
podejrzane zapytania. „Skoro wszystko już działa, lepiej tego nie ruszać…”.
4337ebf6db5c7cc89e4173803ef3875a
4
Programowanie przez koincydencję

235
Taka postawa może łatwo doprowadzić do błędnych założeń. Dlaczego mielibyśmy podejmować ryzyko zepsucia czegoś, co już działa? Cóż, przychodzi
nam do głowy kilka powodów:

Być może to wcale nie działa, a jedynie sprawia takie wrażenie.

Warunek graniczny, na którym opieramy nasze rozwiązanie, może być po
prostu dziełem przypadku. W różnych okolicznościach (na przykład w różnych rozdzielczościach ekranu) nasze rozwiązanie może działać zupełnie
inaczej.

Nieudokumentowane zachowania mogą się zmienić wraz z następnym
wydaniem tej biblioteki.

Dodatkowe i zbędne wywołania mogą spowolnić nasz kod.

Dodatkowe wywołania zwiększają też ryzyko wprowadzenia nowych błędów.
W przypadku kodu pisanego z myślą o wywołaniach przez innych programistów
sporym ułatwieniem będzie konsekwentne stosowanie zasad podziału na moduły
i ukrywania implementacji za niewielkimi, dobrze udokumentowanymi interfejsami. Precyzyjnie zdefiniowany kontrakt (patrz temat 23., „Projektowanie
kontraktowe” w rozdziale 4.) może pomóc wyeliminować nieporozumienia.
W przypadku procedur, które sami wywołujemy, powinniśmy korzystać wyłącznie z udokumentowanych zachowań. Jeśli z jakiegoś powodu to niemożliwe,
powinniśmy przynajmniej dbać o jasne dokumentowanie przyjmowanych założeń.
Blisko to nie znaczy dokładnie
Kiedyś pracowaliśmy nad dużym projektem, którego celem było tworzenie raportów na podstawie danych pobieranych z bardzo dużej liczby rozproszonych urządzeń rozmieszczonych w terenie. Sprzęty te były rozmieszczone w różnych
państwach i strefach czasowych, a z różnych względów logistycznych i historycznych w każdym z urządzeń był ustawiony czas lokalny1. Wskutek błędnych
interpretacji stref czasowych i niespójności w strategii zmian czasu z letniego
na zimowy i na odwrót, wyniki były prawie zawsze błędne, ale tylko o jedną
godzinę. Programiści pracujący w projekcie nabrali nawyku dodawania lub odejmowania jedynki po to, aby uzyskać właściwą odpowiedź. Tłumaczyli, że wynik
różni się o jeden tylko w tej jednej sytuacji. A potem kolejna funkcja sprawdzała,
że wynik różni się o jeden, i zmieniała go ponownie.
Ale fakt, że różnica od dokładnego wyniku wynosiła „tylko” jeden, raz na jakiś
czas dowodził istnienia zbiegu okoliczności, który maskował o wiele głębszą
i bardziej istotną wadę. Ze względu na brak prawidłowego modelu konwersji
czasu, cała rozbudowana baza kodu zmieniła się w końcu w niedopuszczalną
masę instrukcji +1 i –1. Ostatecznie żaden wynik nie był poprawny, w związku
z czym projekt zamknięto.
1
Uwaga z doświadczenia: czas UTC istnieje nie bez powodu. Używaj go.
4337ebf6db5c7cc89e4173803ef3875a
4
236 
Rozdział 7.
Kiedy kodujemy…
Wzorce-widma
Ludzie mają tendencję do zauważania wzorców i przyczyn nawet wtedy, gdy jest
to tylko zbieg okoliczności. Na przykład rosyjscy przywódcy zawsze są naprzemiennie łysi i owłosieni: łysy (lub w oczywisty sposób łysiejący) przywódca Rosji
zwyciężał tego z włosami i odwrotnie, przez prawie 200 lat2.
Ale o ile nikt raczej nie pokusi się o napisanie kodu, który zależy od tego, czy
kolejny rosyjski lider będzie łysy czy owłosiony, o tyle w niektórych dziedzinach
myślimy w ten sposób przez cały czas. Hazardziści wyobrażają sobie wzorce
w loteriach liczbowych, grach w kości lub ruletkę, podczas gdy w rzeczywistości
losowane wartości są zdarzeniami statystycznie niezależnymi. Na rynkach
finansowych, w obrocie akcjami i obligacjami jest podobnie. Roi się tam od
stosowania przypadkowych zamiast rzeczywistych i namacalnych wzorców.
Plik logu, w którym widać okresowy błąd raz na 1000 żądań, może wskazywać
na trudny do zdiagnozowania wyścig lub na zwykły stary błąd. Testy, które
przechodzą na komputerze lokalnym, ale nie przechodzą na serwerze, mogą
oznaczać różnicę pomiędzy tymi dwoma środowiskami, ale może to być jedynie
zbieg okoliczności.
Nie przyjmuj założeń, posługuj się dowodami.
Przypadkowy kontekst
Możemy też stanąć przed problemem przypadkowego kontekstu. Przypuśćmy,
że piszemy jakiś kod pomocniczy. Czy to, że aktualnie pracujemy nad kodem
dla środowiska z graficznym interfejsem użytkownika, uzasadnia uzależnianie
tego modułu od obecności środowiska GUI? Czy tworzymy oprogramowanie tylko
dla anglojęzycznych użytkowników? A może kierujemy swój produkt tylko do
wykształconych ludzi? Jakie jeszcze założenia przyjęliśmy, mimo że nie mamy
stosownych gwarancji?
Czy polegasz na założeniu, że w bieżącym katalogu istnieje prawo zapisu? Czy
zakładasz, że istnieją pewne zmienne środowiskowe lub pliki konfiguracyjne?
Albo że czas na serwerze jest dokładny — jaką przyjmujesz tolerancję? Czy
polegasz na dostępności sieci lub szybkości jej działania?
Jeśli skopiowałeś kod z pierwszej odpowiedzi znalezionej w internecie, to czy
jesteś pewien, że Twój kontekst jest taki sam, jak u autora tego kodu? A może
budujesz kod w stylu „kultu cargo”, jedynie naśladując formę, ale bez treści3?
Znalezienie odpowiedzi, która wygląda na właściwą, to nie to samo, co znalezienie odpowiedzi właściwej.
2
https://en.wikipedia.org/wiki/Correlation_does_not_imply_causation
3
Zobacz temat 50., „Nie próbuj przecinać kokosów”; znajdziesz go w rozdziale 9.
4337ebf6db5c7cc89e4173803ef3875a
4
Programowanie przez koincydencję

237
WSKAZÓWKA NR 62
Nie należy programować przez koincydencję.
Ukryte założenia
Przypadkowe zbiegi okoliczności mogą nas prowadzić w niewłaściwym kierunku
na wszystkich poziomach — od generowania wymagań po testy. Fałszywe
związki przyczynowo-skutkowe i przypadkowe wyniki szczególnie często występują na etapie testowania. Czasem bardzo łatwo ulec pokusie założenia, że to
X powoduje Y, jednak, jak zasugerowaliśmy w rozdziale 3., w temacie 20.,
„Debugowanie”, nie należy niczego zakładać — należy to udowodnić.
Na wszystkich poziomach ludzie operują na rozmaitych założeniach przyjmowanych we własnych umysłach — takie założenia są jednak rzadko dokumentowane i często są sprzeczne z założeniami przyjmowanymi przez innych
ludzi. Założenia, które nie są oparte na sprawdzonej wiedzy, mogą doprowadzić
do niepowodzenia każdego projektu.
Jak programować celowo
Chcemy poświęcać jak najmniej czasu na pracę z gotowym kodem — chcemy
wykrywać i eliminować błędy na możliwie wczesnym etapie cyklu wytwarzania
(i oczywiście tworzyć jak najmniej błędów). Sporym ułatwieniem będzie programowanie celowe (umyślne):

Zawsze należy wiedzieć, co się robi. Fred dopuścił do sytuacji, w której
stracił kontrolę nad swoim kodem — przez nieuwagę podzielił żaby z tematu
„Zupa z kamieni i gotowane żaby” w rozdziale 1.

Czy potrafisz szczegółowo wyjaśnić kod mniej doświadczonemu programiście z Twojego zespołu? Jeśli nie, to prawdopodobni bazujesz na zbiegach
okoliczności.

Nie należy kodować po omacku. Próba budowy aplikacji, której do końca
nie rozumiemy, lub użycia technologii, której dobrze nie znamy, jest jak
zaproszenie do nieporozumień wynikających z przypadkowych zbiegów
okoliczności.

Należy postępować według planu niezależnie od tego, czy jest to plan
w naszej głowie, zapisany na serwetce lub na białej tablicy.

Należy opierać się na tym, co niezawodne. Nie powinniśmy uzależniać
naszych rozwiązań od przypadków ani założeń. Jeśli nie potrafimy rozstrzygnąć jakichś kwestii, powinniśmy zakładać najgorszy scenariusz.

Należy dokumentować założenia. Propozycje zawarte w rozdziale 4., w temacie 23., „Projektowanie kontraktowe”, mogą nam ułatwić zarówno precyzyjne opisywanie założeń rodzących się w naszych umysłach, jak i komunikowanie tych założeń innym.
4337ebf6db5c7cc89e4173803ef3875a
4
238 
Rozdział 7.
Kiedy kodujemy…

Samo testowanie kodu nie wystarczy — należy jeszcze testować przyjmowane założenia. Nie powinniśmy zgadywać, tylko sprawdzać. Należy
zapisywać asercje niezbędne do testowania naszych założeń (patrz temat
25., „Programowanie asertywne”, mieszczący się w rozdziale 4.). Jeśli stosowane asercje będą prawidłowe, będziemy dodatkowo dysponowali lepszą
dokumentacją swojego kodu. Jeśli w ten sposób odkryjemy, że jakieś założenie było błędne, możemy mówić o dużym szczęściu.

Należy nadawać priorytety swoim wysiłkom. Warto poświęcić swój czas na
najważniejsze aspekty, które najczęściej stanowią najtrudniejsze elementy
tworzonego systemu. Jeśli fundamenty lub infrastruktura naszego projektu
nie są prawidłowe, nawet najlepsze dodatki i dekoracje okażą się zupełnie
nieistotne.

Nie możemy być niewolnikami historii. Nie możemy pozwolić, aby istniejący kod dyktował nam, jak tworzyć kod w przyszłości. Cały dotychczasowy kod można zastąpić, jeśli przestał spełniać nasze oczekiwania. Nawet
w ramach jednego programu nie powinniśmy dopuszczać do sytuacji,
w której dotychczasowe rozwiązania wymuszają kierunki dalszych działań —
musimy być gotowi na refaktoryzację (patrz temat 40., „Refaktoryzacja”).
Ta decyzja może mieć istotny wpływ na harmonogram prac nad projektem.
Zakładamy jednak, że jej negatywny wpływ na czas realizacji projektu
będzie mniejszy niż koszt zaniechania zmian4.
Kiedy więc następnym razem coś będzie sprawiało wrażenie prawidłowego, ale
nie będziemy wiedzieli dlaczego, koniecznie powinniśmy upewnić się, że nie
mamy do czynienia z koincydencją.
Pokrewne podrozdziały

Temat 4., „Zupa z kamieni i gotowane żaby”.

Temat 9., „DRY — Przekleństwo powielania”.

Temat 23., „Projektowanie kontraktowe”.

Temat 34., „Współdzielony stan jest zły”.

Temat 43., „Pozostań w bezpiecznym miejscu”.
Ćwiczenia
25. Źródło danych od dostawcy zwraca tablicę krotek reprezentujących pary
klucz-wartość. Kluczowi DepositAccount odpowiada ciąg znaków reprezentujący numer rachunku:
[
...
{:DepositAccount, "564-904-143-00"}
4
W tej sprawie można też zabrnąć za daleko. Znaliśmy kiedyś programistę, który zdecydował się przepisać cały przekazany mu kod źródłowy, ponieważ był niezgodny z jego
konwencjami nazewniczymi.
4337ebf6db5c7cc89e4173803ef3875a
4
Szybkość algorytmu
 239
...
]
Kod działał idealnie, gdy był testowany na 4-rdzeniowym laptopie programisty oraz na 12-rdzeniowym serwerze budowania, ale na serwerach produkcyjnych działających w kontenerach, otrzymywane numery rachunków
są nieprawidłowe. Co się dzieje?
26. Piszesz kod automatycznego dialera do powiadomień głosowych i zarządzasz bazą danych z informacjami kontaktowymi. Specyfikacja ITU określa,
że numery telefonów nie powinny być dłuższe niż 15 cyfr, więc przechowujesz kontaktowy numer telefonu w polu numerycznym, które gwarantuje
przechowywanie przynajmniej 15 cyfr. Dokładnie przetestowałeś kod dla
całej Ameryki Północnej i wszystko wydaje się działać w porządku, ale nagle
otrzymujesz wiele raportów o błędach z innych części świata. Dlaczego?
27. Napisałeś aplikację, która skaluje przepisy kulinarne dla jadłodajni na
statku wycieczkowym, który może pomieścić 5000 pasażerów. Otrzymujesz jednak skargi, że konwersje nie są precyzyjne. Sprawdziłeś, że w kodzie
wykorzystano formułę konwersji 16 szklanek na galon. Wydaje się, że to
prawidłowa formuła. A może jednak nie?
39
38
Szybkość algorytmu
W temacie 15., „Szacowanie”, mieszczącym się w rozdziale 2., omówiliśmy problem szacowania czasu rozmaitych czynności, w tym czasu potrzebnego do
pieszego pokonania kilku ulic oraz do zakończenia projektu. Okazuje się jednak,
że istnieje jeszcze inny rodzaj szacowania, który pragmatyczni programiści stosują niemal codziennie — szacowanie zasobów (czasu, procesora, pamięci itp.)
używanych przez algorytmy.
Ten rodzaj szacowania często jest bardzo ważny dla powodzenia realizowanych
przedsięwzięć. Jeśli będziemy mieli wybór pomiędzy dwoma sposobami implementacji jakiegoś rozwiązania, który z nich wybierzemy? Skoro wiemy, ile czasu
zajmie naszemu programowi przetworzenie tysiąca rekordów, czy potrafimy
przeskalować te szacunki dla miliona rekordów? Które elementy kodu wymagają
optymalizacji?
Okazuje się, że na wiele podobnych pytań można odpowiedzieć, kierując się
zdrowym rozsądkiem, wykonując pewne analizy oraz zapisując szacunki w tzw.
notacji wielkiego O.
Co właściwie rozumiemy
przez szacowanie algorytmów?
Większość nietrywialnych algorytmów obsługuje jakiś rodzaj zmiennych danych
wejściowych — sortuje n łańcuchów, odwraca macierz m×n lub odszyfrowuje
4337ebf6db5c7cc89e4173803ef3875a
4
240 
Rozdział 7.
Kiedy kodujemy…
wiadomość przy użyciu n-bitowego klucza. Rozmiar tych danych wejściowych
zwykle wpływa na algorytm — im większe są te dane, tym dłużej trwa ich przetworzenie i tym więcej pamięci trzeba użyć.
Gdyby ta relacja była zawsze liniowa (gdyby czas działania algorytmu był
wprost proporcjonalny do wartości n), niniejszy podrozdział byłby w ogóle
niepotrzebny. Okazuje się jednak, że najważniejsze algorytmy są nieliniowe.
Niewątpliwym pocieszeniem jest to, że złożoność wielu spośród tych algorytmów jest mniejsza niż liniowa. Na przykład algorytm przeszukiwania binarnego
nie musi analizować każdego kandydata podczas odnajdywania dopasowania.
Mniej optymistyczna jest wieść o tym, że pozostałe algorytmy są istotnie wolniejsze od algorytmów liniowych — w ich przypadku czas wykonywania i wymagania pamięciowe rosną dużo szybciej niż n. Algorytm, któremu przetworzenie
dziesięciu elementów zabiera minutę, może potrzebować dziesięcioleci do przetworzenia 100 elementów.
Za każdym razem, gdy sami piszemy jakikolwiek kod zawierający pętle lub
wywołania rekurencyjne, podświadomie sprawdzamy czas wykonywania i wymagania pamięciowe nowego kodu. Wspomniany proces rzadko ma formalny charakter — to raczej szybka weryfikacja wykonalności stosowanych rozwiązań
w konkretnych okolicznościach. Zdarza się jednak, że wykonujemy bardziej
szczegółowe analizy. Właśnie wtedy notacja O() jest nieocenionym ułatwieniem.
Notacja O()
Notacja O() to matematyczny sposób wyrażania i opisywania przybliżeń. Kiedy
zapisujemy, że określona funkcja sortuje n rekordów w czasie O(n2), w rzeczywistości szacujemy, że w najgorszym przypadku czas sortowania będzie kwadratem liczby n. Wystarczy więc podwoić liczbę rekordów, aby czas sortowania
wzrósł (w przybliżeniu) czterokrotnie. Zapis O należy traktować jako rząd
wielkości.
Notacja O() wyznacza tylko górną granicę dla mierzonej wartości (czasu, pamięci
itp.). Kiedy mówimy, że jakaś funkcja wymaga O(n2) czasu, w rzeczywistości
określamy, że górna granica czasu jej wykonywania nie będzie rosła szybciej niż
n2. W niektórych przypadkach funkcje O() są dość złożone, ale ponieważ wyraz
najwyższego rzędu dominuje wzrost zasobów używanych przez tę funkcję wraz
ze wzrostem wartości n, zgodnie z konwencją wyrazy niższego rzędu należy
pominąć i nie zawracać sobie głowy zapisywaniem wszystkich stałych współ2
2
czynników. Zapis O( n2  3n) jest więc równoważny zapisowi O( n2 ) , który z kolei
odpowiada prostszemu zapisowi O(n2).
To jedna ze słabości notacji O() — jeden algorytm o złożoności O(n2) może być
na przykład tysiąc razy wolniejszy od innego algorytmu o złożoności O(n2), czego
w żaden sposób nie można stwierdzić na podstawie tej notacji.
4337ebf6db5c7cc89e4173803ef3875a
4
Szybkość algorytmu
 241
Na rysunku 3. pokazano kilka najbardziej typowych zapisów w notacji O()
wraz z wykresem ilustrującym czasy wykonywania algorytmów w poszczególnych
kategoriach. Jak łatwo zauważyć, sprawy szybko wymykają się spod kontroli,
kiedy złożoność przekracza poziom O(n2).
Rysunek 3. Czasy wykonywania różnych algorytmów
Przypuśćmy na przykład, że dysponujemy funkcją, która przetwarza 100 rekordów w ciągu sekundy. Ile czasu zajmie jej przetworzenie 1000 rekordów? Jeśli
złożoność naszego kodu wynosi O(1), przetworzenie 1000 rekordów nadal będzie
zajmowało sekundę. W przypadku złożoności O(lg(n)) prawdopodobnie będziemy
musieli poczekać 3 sekundy. Złożoność O(n) oznacza liniowy wzrost do 10
sekund, a wykonywanie algorytmu o złożoności O(n lg(n)) zajęłoby około 33
sekundy. Jeśli nie mamy tyle szczęścia i nasza funkcja cechuje się złożonością
O(n2), musimy przygotować się na oczekiwanie przez około 100 sekund. A jeśli
posługujemy się algorytmem o złożoności wykładniczej O(2n), możemy spokojnie przystąpić do parzenia kawy — nasza funkcja powinna zakończyć działanie
po 10263 latach. Wydaje się, że ludzkość nie ma tyle czasu.
4337ebf6db5c7cc89e4173803ef3875a
4
242 
Rozdział 7.
Kiedy kodujemy…
Notacja O() nie musi być stosowana tylko dla czasu — równie dobrze można
jej używać do reprezentowania dowolnych innych zasobów używanych przez
algorytm. Notacji O() często używa się do modelowania poziomu wykorzystania
pamięci (patrz ćwiczenia na końcu tego podrozdziału).
Szacowanie zdroworozsądkowe
Przybliżoną złożoność wielu prostych algorytmów możemy szacować, posługując się wyłącznie intuicją i zdrowym rozsądkiem.

Proste pętle. Jeśli prosta pętla wykonuje od 1 do n-tej iteracji, złożoność
całego algorytmu najprawdopodobniej wynosi O(n) — czas jego wykonywania rośnie liniowo wraz z wartością n. Do typowych przykładów należy
wyczerpujące wyszukiwanie, odnajdywanie wartości maksymalnej w tablicy
oraz generowanie sum kontrolnych.

Pętle zagnieżdżone. Jeśli zagnieżdżamy jedną pętlę w innej pętli, otrzymujemy algorytm o złożoności O(m×n), gdzie m oraz n to liczby iteracji obu
pętli. Taka sytuacja często ma miejsce w prostych algorytmach sortujących
(na przykład w algorytmie sortowania bąbelkowego), gdzie pętla zewnętrzna
przeszukuje wszystkie elementy tablicy, a pętla wewnętrzna określa, gdzie
należy umieszczać każdy z tych elementów w posortowanej tablicy wynikowej. Takie algorytmy sortujące zwykle mają złożoność O(n2).

Przeszukiwanie dwudzielne. Jeśli nasz algorytm dzieli na pół zbiór elementów w każdym przebiegu pętli, złożoność tego kodu najprawdopodobniej jest logarytmiczna i wynosi O(lg(n)). Taką złożonością cechuje się
wyszukiwanie binarne na posortowanej liście, chodzenie po drzewie binarnym oraz odnajdywanie pierwszego ustawionego bitu w słowie maszynowym.

Dziel i zwyciężaj. Algorytmy, które dzielą swoje dane wejściowe i pracują
niezależnie na dwóch połowach, po czym łączą wyniki, osiągają złożoność
O(n lg(n)). Klasycznym przykładem jest algorytm sortowania szybkiego, który
dzieli dane na dwie połowy i rekurencyjnie sortuje każdą z nich. Mimo że
formalnie wciąż mamy do czynienia z algorytmem O(n2), w praktyce algorytm działa szybciej, kiedy otrzymuje posortowane dane wejściowe, zatem
średnia złożoność sortowania szybkiego wynosi O(n lg(n)).

Algorytmy kombinatoryczne. Każdy algorytm poszukujący permutacji
oznacza ryzyko utraty kontroli nad czasami wykonywania. Problem
w tym, że liczba permutacji jest równa silni liczby elementów (istnieje 5! =
5×4×3×2×1 = 120 permutacji cyfr od 1 do 5). Oznacza to, że czas potrzebny
do przetworzenia przez algorytm kombinatoryczny pięciu elementów wydłuży
się 6-krotnie w przypadku sześciu elementów oraz 42-krotnie w przypadku siedmiu elementów. Tego rodzaju algorytmy stosuje się do rozwiązywania trudnych obliczeniowo problemów, jak problem komiwojażera,
problem optymalnego rozmieszczania zawartości kontenera, dzielenie zbioru
4337ebf6db5c7cc89e4173803ef3875a
4
Szybkość algorytmu
 243
liczb tak, aby suma elementów w każdym podzbiorze była identyczna itp.
Często stosuje się algorytmy heurystyczne, które pozwalają skrócić czas
wykonywania tych algorytmów w konkretnych dziedzinach problemu.
Szybkość algorytmu w praktyce
Trudno oczekiwać, aby pisanie funkcji sortujących miało zająć istotną część
naszej kariery. Rozwiązania zaimplementowane już w dostępnych bibliotekach
prawdopodobnie oferują wyższą wydajność niż jakikolwiek kod, który moglibyśmy napisać w rozsądnym czasie. Okazuje się jednak, że podstawowe rodzaje
algorytmów, które opisaliśmy wcześniej, w różnych formach przewijają się
w pracy każdego programisty. Za każdym razem, gdy piszemy prostą pętlę,
możemy być pewni, że złożoność tego algorytmu wyniesie O(n). Jeśli ta pętla
zawiera pętlę wewnętrzną, mamy do czynienia z algorytmem o złożoności
O(m×n). Sami powinniśmy zadawać sobie pytania, na jak duże wartości możemy
sobie pozwolić. Jeśli dopuszczalne liczby wejściowe są ograniczone z góry, możemy
od razu stwierdzić, ile czasu zajmie wykonywanie naszego kodu. Jeśli te wartości zależą od czynników zewnętrznych (na przykład liczby rekordów zwróconych przez plik wsadowy uruchamiany na noc czy liczby nazwisk na liście
klientów), być może powinniśmy skoncentrować się raczej na maksymalnym
akceptowanym czasie działania lub na maksymalnym dopuszczalnym poziomie
wykorzystania pamięci.
WSKAZÓWKA NR 63
Należy szacować rzędy wielkości algorytmów.
Istnieją pewne techniki, które można z powodzeniem wykorzystywać do rozwiązywania potencjalnych problemów. Jeśli złożoność naszego algorytmu wynosi
O(n2), warto podjąć próbę zastosowania techniki „dziel i zwyciężaj”, aby obniżyć
tę złożoność do O(n lg(n)).
Jeśli nie jesteśmy pewni, ile czasu potrzeba na wykonanie naszego kodu lub ile
pamięci wykorzysta nasz kod, wystarczy to sprawdzić, przeprowadzając eksperymenty przy różnej liczbie rekordów wejściowych (lub różnych wartościach
dowolnego innego parametru, który prawdopodobnie będzie miał wpływ na czas
działania algorytmu). Warto następnie nanieść wyniki na wykres. Kształt tak
tworzonego wykresu dość szybko powinien nam zasugerować faktyczną złożoność. Czy mamy do czynienia z krzywą rosnącą, prostą, czy może krzywą dążącą
do jakiejś wartości stałej (przy rosnącej ilości danych wejściowych)? Trzy lub
cztery pomiary powinny wystarczyć.
Warto też testować rozwiązania stosowane w samym kodzie. Dla odpowiednio
małych wartości n prosta pętla o złożoności O(n2) może działać dużo szybciej od
skomplikowanej pętli o złożoności O(n lg(n)), szczególnie jeśli algorytm O(n lg(n))
zawiera kosztowną pętlę wewnętrzną.
4337ebf6db5c7cc89e4173803ef3875a
4
244 
Rozdział 7.
Kiedy kodujemy…
Opisana teoria nie powinna przesłaniać nam praktycznych aspektów szybkości
algorytmów. Wzrost czasu wykonywania może sprawiać wrażenie liniowego dla
stosunkowo niedużych zbiorów danych wejściowych. Wystarczy jednak użyć
tego samego kodu do przetworzenia milionów rekordów, aby czas działania
wydłużył się do tego stopnia, że wydajność całego systemu będzie nie do zaakceptowania. Jeśli testujemy procedurę sortującą, która operuje na losowych
kluczach wejściowych, możemy być pozytywnie zaskoczeni czasem działania
w razie napotkania już uporządkowanych danych. Pragmatyczni programiści
starają się pamiętać zarówno o podstawach teoretycznych, jak i ich wymiarze
praktycznym. Po przeprowadzeniu wszystkich tych szacunków jedynym naprawdę
istotnym wnioskiem jest szybkość naszego kodu wykonywanego w środowisku
produkcyjnym na prawdziwych danych. W ten sposób dochodzimy do kolejnej
wskazówki.
WSKAZÓWKA NR 64
Należy testować swoje szacunki.
Jeśli uzyskanie precyzyjnych szacunków jest zbyt trudne, warto użyć mechanizmów profilowania kodu do określenia liczby wykonań poszczególnych kroków algorytmu i nanieść te wartości na wykres uwzględniający ilość danych
wejściowych.
Najlepsze nie zawsze jest najlepsze
Pragmatyzm musimy wykazywać także na etapie doboru właściwych algorytmów — najszybszy algorytm nie we wszystkich przypadkach jest najlepszy.
Dla niewielkiego zbioru wejściowego proste sortowanie przez wstawianie będzie
równie efektywne jak sortowanie szybkie, a jednocześnie będzie łatwiejsze do
zaimplementowania i przetestowania. Warto też zachować daleko idącą ostrożność, jeśli interesujący algorytm wiąże się z dużymi kosztami na etapie inicjalizacji. W przypadku niedużych zbiorów wejściowych czas samej inicjalizacji może
przekroczyć czas właściwego działania algorytmu (w takim przypadku należy
szukać innych rozwiązań).
Ważne jest także unikanie pochopnych decyzji o optymalizacji. Zawsze warto
upewnić się, że interesujący nas algorytm rzeczywiście jest wąskim gardłem,
zanim zdecydujemy się poświęcić swój cenny czas na doskonalenie tego algorytmu.
Pokrewne podrozdziały

Temat 15., „Szacowanie”.
Wyzwania

Każdy programista powinien wiedzieć, jak należy projektować i analizować
algorytmy. Robert Sedgewick napisał serię książek wprowadzających te
4337ebf6db5c7cc89e4173803ef3875a
4
Refaktoryzacja
 245
zagadnienia w wyjątkowo przejrzysty sposób (Algorithms [SW11], An Introduction to the Analysis of Algorithms [SF13] i inne). Zachęcamy do włączenia którejś z tych pozycji do własnej biblioteki oraz jej uważną lekturę.


Czytelnicy, którzy szukają bardziej wyczerpujących materiałów na ten
temat, powinni sięgnąć po książki Sztuka programowania Donalda Knutha,
w których szczegółowo przeanalizowano najróżniejsze algorytmy

Sztuka programowania, Tom I: Algorytmy podstawowe [Knu98a].

Sztuka programowania, Tom II: Algorytmy seminumeryczne [Knu98b].

Sztuka programowania, Tom III. Sortowanie i wyszukiwanie [Knu98c].

The Art of Computer Programming, Volume 4A: Combinatorial Algorithms, Part 1 [Knu11].
W ćwiczeniu 28. przyjrzymy się problemowi sortowania tablic długich liczb
całkowitych. Jaki wpływ na szybkość działania algorytmu mają bardziej
złożone klucze? Jaki wpływ na łączną wydajność algorytmu mają kosztowne
operacje porównywania kluczy? Czy struktura klucza wpływa na efektywność algorytmów sortujących, czy też jeden algorytm zawsze jest najszybszy?
Ćwiczenia
28. Opracowaliśmy kilka prostych funkcji sortujących w języku Rust5. Zachęcamy do uruchomienia tego kodu na różnych komputerach. Czy Twoje
wykresy pasują kształtem do oczekiwanych krzywych? Do jakich wniosków
można dojść na podstawie względnej szybkości testowych komputerów? Jaki
wpływ na wyniki mają rozmaite ustawienia optymalizacji kompilatorów?
29. W punkcie „Szacowanie zdroworozsądkowe” stwierdziliśmy, że złożoność
przeszukiwania dwudzielnego wynosi O(lg(n)). Czy potrafisz to udowodnić?
30. Na rysunku 3. można zauważyć, że złożoność O(lg n) jest taka sama, jak
O(log10n) (lub złożoność logarytmiczna o dowolnej podstawie). Czy potrafisz
wyjaśnić dlaczego?
40
39
Refaktoryzacja
Wokół mnie tylko zmiany i zepsucie…
H.F. Lyte, Abide With Me
Ewolucja programu wymusza na nas ponowne przemyślenie wcześniejszych
decyzji i przebudowę fragmentów istniejącego kodu. Proces poprawiania kodu
jest czymś zupełnie naturalnym. Kod nigdy nie ma charakteru statycznego
i jako taki musi ewoluować.
5
https://media-origin.pragprog.com/titles/tpp20/code/algorithm_speed/sort/src/main.rs
4337ebf6db5c7cc89e4173803ef3875a
4
246 
Rozdział 7.
Kiedy kodujemy…
Okazuje się jednak, że wytwarzanie oprogramowania jest najczęściej porównywane do konstrukcji inżynieryjnych (Bertrand Meyer w słynnej książce
Object-Oriented Software Construction [Mey97] używa nawet pojęcia „konstrukcja oprogramowania”). My, skromni autorzy niniejszej książki, redagowaliśmy
kolumnę „Software construction” w magazynie „IEEE Software” na początku lat
dwutysięcznych6.
Konstrukcja inżynieryjna jako powszechnie stosowana metafora sugeruje, że
cały proces wytwarzania składa się z następujących kroków:
1. Architekt rysuje projekt na papierze.
2. Wykonawca kopie fundamenty, buduje podstawową strukturę, kładzie
przewody i rury oraz wykańcza budynek.
3. Od tego momentu lokatorzy mogą się wprowadzać i czerpać pełnymi garściami z dostępnych udogodnień, wzywając od czasu do czasu fachowców
do ewentualnych usterek.
Cóż, świat oprogramowania funkcjonuje nieco inaczej. Oprogramowanie przypomina bardziej ogród niż tradycyjny plac budowy — więcej tam elementów
organicznych niż betonu. Sadzimy w naszym ogrodzie wiele roślin zgodnie
z początkowym planem i panującymi warunkami. Część roślin kwitnie, inne
muszą trafić na kompost. Część roślin szczególnie dobrze rozwija się w towarzystwie innych gatunków, które nie zasłaniają światła lub — wprost przeciwnie — zapewniają cień oraz osłonę przed wiatrem i deszczem. Przerośnięte
rośliny są przycinane, a rośliny, których kolory odbiegają od projektowanego
zabarwienia całości, trafiają w inne, bardziej odosobnione miejsca. Wyrywamy
chwasty i nawozimy rośliny, które wymagają dodatkowej opieki. Stale monitorujemy zdrowie całego ogrodu, wprowadzając na bieżąco niezbędne poprawki
(w glebie, w samych roślinach i w całym projekcie).
Do ludzi biznesu dużo bardziej przemawia porównanie do konstruowania
budowli, ponieważ wydaje się bardziej ścisłe od ogrodnictwa i bardziej powtarzalne. Co więcej, w budownictwie mamy do czynienia z surowymi zasadami
składania przełożonym raportów o stanie prac. Naszym zadaniem nie jest jednak
budowa drapaczy chmur — nie ograniczają nas tak sztywne prawa fizyki.
Porównanie do ogrodnictwa jest więc dużo bliższe realiom wytwarzania oprogramowania. Być może jakaś funkcja zbyt mocno się rozrosła lub próbuje robić
zbyt wiele — warto ją podzielić (rozsadzić) na dwie funkcje. Rozwiązania, które
nie zdały egzaminu, należy wykopać lub przyciąć.
Takie czynności jak przepisywanie, przebudowywanie czy modyfikowanie architektury kodu określa się wspólnym mianem restrukturyzacji. Istnieje jednak
podzbiór tych działań, który określa się mianem refaktoryzacja.
6
Tak. Wyraziliśmy nasze zaniepokojenie tytułem.
4337ebf6db5c7cc89e4173803ef3875a
4
Refaktoryzacja
 247
Martin Fowler w książce Refaktoryzacja [Fow19] definuje refaktoryzację jako
zdyscyplinowaną technikę restrukturyzacji istniejącej treści kodu polegającą na
zmianie jego wewnętrznej struktury bez zmiany zewnętrznego zachowania.
Oto kluczowe elementy tej definicji:
1. Działania są zdyscyplinowane, a nie swobodne, dostępne dla wszystkich.
2. Zachowanie zewnętrzne nie zmienia się. Refaktoryzacja nie jest okazją do
dodawania nowych funkcji.
Refaktoryzacja nie powinna być specjalną ceremonią wykonywaną raz na jakiś
czas, podobną do przekopania całego ogródka w celu posadzenia nowych roślin.
Zamiast tego refaktoryzacja powinna być działaniem codziennym, w którym
wykonujemy niewielkie kroki o niskim ryzyku, bardziej przypominające pielenie i grabienie. Refaktoryzacja nie jest masowym przepisywaniem całej bazy
kodu. Jest to raczej ukierunkowane, precyzyjne podejście zmierzające do ułatwienia wprowadzania zmian w kodzie.
Aby zagwarantować, że zewnętrzne zachowanie nie zmieni się, potrzebne są
dobre, zautomatyzowane testy jednostkowe, które sprawdzają zachowanie kodu.
Kiedy należy refaktoryzować?
Refaktoryzujemy, kiedy się czegoś dowiemy; kiedy rozumiemy coś lepiej niż
w zeszłym roku, poprzedniego dnia lub nawet tylko dziesięć minut temu.
Kiedy napotykamy w swoim kodzie blok, który nie pasuje już do reszty programu, kiedy odkrywamy rozwiązania, które wymagają scalenia, lub kiedy
trafiamy na cokolwiek innego, co wydaje nam się po prostu złe, nie powinniśmy
unikać niezbędnych zmian. Właśnie teraz jest najlepszy moment na zmiany.
O konieczności refaktoryzacji może decydować wiele różnych czynników i zjawisk:

Powielanie. Odkryliśmy naruszenie zasady DRY.

Nieortogonalny projekt. Odkryliśmy, że fragment naszego kodu lub projektu mógłby być bardziej ortogonalny.

Zdezaktualizowana wiedza. Sytuacja się zmienia, wymagania ewoluują,
a Twoja wiedza na temat problemu jest coraz szersza. Trzeba to uwzględnić w kodzie.

Korzystanie z systemu. W czasie, gdy system jest wykorzystywany przez
prawdziwych ludzi w rzeczywistych warunkach, zdajesz sobie sprawę, że
niektóre własności są ważniejsze niż wcześniej sądzono, a funkcjonalności
„niezbędne” być może okazały się nie być niezbędne.

Wydajność. Być może musimy przenieść pewne funkcje z jednego obszaru
systemu w inny, aby podnieść wydajność naszego kodu.
4337ebf6db5c7cc89e4173803ef3875a
4
248 

Rozdział 7.
Kiedy kodujemy…
Testy przechodzą. Poważnie. Powiedzieliśmy, że refaktoryzacja powinna
być działaniem na niewielką skalę, wspieraną przez dobre testy. Kiedy zatem
dodaliśmy niewielką ilość kodu, a dodatkowe testy, które ten kod pokrywają, pomyślnie przechodzą, powstaje doskonała okazja, by zagłębić się
w kod i uporządkować to, co właśnie napisaliśmy.
Refaktoryzacja kodu, czyli przenoszenie pewnych elementów funkcjonalności
i korygowanie wcześniejszych decyzji, jest w istocie sztuką radzenia sobie
z bólem. Modyfikowanie kodu źródłowego bywa wyjątkowo kłopotliwe — wszystko
niemal działało i nagle trzeba to całkowicie przebudować. Wielu programistów
bardzo niechętnie ingeruje w swój kod, jeśli jedynym powodem takiej ingerencji
miałyby być nieistotne błędy.
Komplikacje występujące w praktyce
Wyobraźmy sobie, że idziemy do swojego szefa lub klienta i mówimy: „Ten kod
działa, ale potrzebuję jeszcze tygodnia na jego refaktoryzację”.
Odpowiedź nie nadaje się do druku.
Presja czasu często jest wykorzystywana jako wymówka usprawiedliwiająca
rezygnację z refaktoryzacji. Trudno jednak przyjąć takie uzasadnienie — unikanie refaktoryzacji teraz oznacza konieczność dużo większych inwestycji w usunięcie problemu w przyszłości (kiedy będziemy musieli analizować i uwzględniać
dodatkowe zależności). Czy wtedy będziemy mieli więcej czasu? Nasze doświadczenie sugeruje coś wprost przeciwnego.
Podczas tłumaczenia tej zasady przełożonym warto sięgnąć po medyczną analogię: kod wymagający refaktoryzacji jest jak narośl. Jej usunięcie wymaga
inwazyjnego zabiegu chirurgicznego. Narośl można wyciąć od razu, kiedy jest
jeszcze stosunkowo mała. Alternatywnym rozwiązaniem jest odłożenie zabiegu —
kiedy jednak narośl będzie większa, jej usunięcie będzie nie tylko bardziej kosztowne, ale też bardziej niebezpieczne. Jeszcze dłuższe oczekiwanie może oznaczać nawet śmierć pacjenta.
WSKAZÓWKA NR 65
Refaktoryzację należy przeprowadzać możliwie wcześnie i jak najczęściej.
Skumulowane uszkodzenia w kodzie z biegiem czasu mogą mieć tak samo
zabójcze skutki (patrz temat 3., „Entropia oprogramowania”, mieszczący się
w rozdziale 1.). Refaktoryzację, jak większość zmian, łatwiej jest przeprowadzić,
gdy problemy są małe. Powinna być ona aktywnością wykonywaną na bieżąco
podczas kodowania. Nie powinieneś potrzebować „tygodnia na refaktoryzację”
fragmentu kodu — tyle powinno zająć przepisanie go od podstaw. Jeśli zmiana
wymaga tak wielkiej skali czasowej, to może się zdarzyć, że nie będziemy w stanie
wprowadzić jej od razu. W takim przypadku należy umieścić modyfikację
4337ebf6db5c7cc89e4173803ef3875a
4
Refaktoryzacja
 249
w harmonogramie przyszłych prac. Użytkownicy tak oznaczonego kodu powinni
wiedzieć, że w przyszłości będzie miała miejsce refaktoryzacja oraz jak ta refaktoryzacja wpłynie na ich kod.
Jak refaktoryzować?
Koncepcja refaktoryzacji zrodziła się w społeczności programistów języka Smalltalk i zaczęła zyskiwać szerszą akceptację w czasie, gdy pisaliśmy pierwsze
wydanie tej książki. Stało się tak prawdopodobnie dzięki wydaniu pierwszej
ważnej publikacji poświęconej temu tematowi: Refaktoryzacja. Ulepszanie
struktury istniejącego kodu [Fow19]; obecnie doczekała się ona drugiego wydania.
Istotą refaktoryzacji jest zmiana projektu. Wszystko, co sami zaprojektowaliśmy
lub co zaprojektowali pozostali członkowie naszego zespołu, można przeprojektować z uwzględnieniem nowych faktów, głębszej wiedzy, zmienionych wymagań
itd. Gdybyśmy jednak bezmyślnie przebudowywali lub wręcz wyrzucali do kosza
istotne fragmenty naszego kodu, niewykluczone, że znaleźlibyśmy się w sytuacji
gorszej niż sprzed tego procesu.
Refaktoryzacja to bez wątpienia jedna z tych czynności, które należy poprzedzić
uważną analizą i które wymagają namysłu i ostrożności. Martin Fowler proponuje następujące wskazówki dotyczące refaktoryzacji, które pozwolą osiągnąć pożądane rezultaty przy minimalnym ryzyku7:
1. Nie należy jednocześnie próbować refaktoryzacji i dodawania nowych
funkcji.
2. Przed przystąpieniem do refaktoryzacji koniecznie należy przygotować dobre
testy. Testy należy przeprowadzać tak często, jak to możliwe. Dzięki temu
będziemy szybko wiedzieli, czy nasze zmiany niczego nie zepsuły.
3. Należy wykonywać krótkie, przemyślane kroki, jak przeniesienie jednego
pola z jednej klasy do innej, podzielenie metody, czy też zmiana nazwy
zmiennej. Refaktoryzacja często polega na wprowadzaniu wielu zmian o charakterze lokalnym, które składają się na jedną zmianę w większej skali.
Jeśli stosujemy technikę drobnych kroków i sumiennie testujemy kod po
każdym kroku, możemy uniknąć potrzeby czasochłonnego debugowania
kodu8.
7
Po raz pierwszy sformułowane w książce UML Distilled: A Brief Guide to the Standard
Object Modeling Language [Fow00].
8
Jest to doskonała porada ogólna (patrz temat 27., „Nie prześcigaj swoich świateł”, mieszczący się w rozdziale 4.).
4337ebf6db5c7cc89e4173803ef3875a
4
250 
Rozdział 7.
Kiedy kodujemy…
Refaktoryzacja automatyczna
W pierwszym wydaniu tej książki stwierdziliśmy, że „wspomniana technologia jeszcze
nie zyskała popularności poza językiem Smalltalk, ale sytuacja najprawdopodobniej
zmieni się...”. I rzeczywiście tak się stało. Mechanizmy automatycznej refaktoryzacji są
dostępne w wielu środowiskach IDE i dla większości głównych języków programowania.
Te środowiska IDE pozwalają modyfikować nazwy zmiennych i metod, umożliwiają
podzielenie długiej procedury na mniejsze, automatyczne propagowanie wymaganych zmian, technikę przeciągnij i upuść ułatwiającą poruszanie się po kodzie
i tak dalej.
Problemowi testowania na tym poziomie poświęcimy więcej uwagi w temacie
41., „Kod łatwy do testowania”. Do kwestii testowania w większej skali wrócimy
w temacie „Bezlitosne testy” w rozdziale 9. Fowler sugeruje, że utrzymywanie
dobrych testów regresji jest kluczem do bezpiecznej refaktoryzacji.
W takim przypadku kompilacja starego kodu klienckiego powinna być niemożliwa. W ten sposób można szybko znaleźć wszystkie te moduły i wprowadzić
zmiany dostosowujące ich kod do nowych rozwiązań. Kiedy więc następnym
razem spotkamy fragment kodu, który nie będzie do końca odpowiadał naszym
oczekiwaniom, powinniśmy zmienić zarówno ten fragment, jak i wszystkie
moduły zależne. Należy rozsądnie walczyć z bólem — jeśli coś sprawia nam ból
już teraz, a może być jeszcze bardziej bolesne w przyszłości, nie warto czekać
na najgorsze. Pamiętajmy o lekcji z tematu 3., „Entropia oprogramowania”
w rozdziale 1.: nigdy nie należy pozostawiać wybitych szyb.
Pokrewne podrozdziały
41
40

Temat 3., „Entropia oprogramowania”.

Temat 9., „DRY — Przekleństwo powielania”.

Temat 12., „Pociski smugowe”.

Temat 27., „Nie prześcigaj swoich świateł”.

Temat 48., „Istota zwinności”.
Kod łatwy do testowania
Pierwsze wydanie tej książki pisaliśmy w bardziej prymitywnych czasach. Wtedy
większość programistów nie pisała żadnych testów — po co się martwić, skoro
świat i tak skończy się w 2000 roku?
W tamtej książce był podrozdział poświęcony sposobom tworzenia kodu, który
jest łatwy do testowania. To był podstępny sposób, by przekonać programistów
do pisania testów.
4337ebf6db5c7cc89e4173803ef3875a
4
Kod łatwy do testowania
 251
Dziś mamy bardziej oświecone czasy. Jeśli są jacyś programiści, którzy nadal nie
piszą testów, to przynajmniej wiedzą, że powinni je pisać.
Nadal jednak jest pewien problem. Gdy pytamy programistów, po co piszą
testy, spoglądają na nas tak, jakbyśmy pytali ich, czy nadal kodują za pomocą
kart perforowanych, i w końcu odpowiadają: „Aby upewnić się, że kod działa”,
z niewypowiedzianym „głupku” na końcu. Uważamy, że to zły powód.
A zatem, co uważamy w testowaniu za ważne? I w jaki sposób, naszym zdaniem,
powinniśmy się do tego zabrać?
Zacznijmy od śmiałego stwierdzenia:
WSKAZÓWKA NR 66
W testowaniu nie chodzi o znajdowanie błędów.
Uważamy, że główne korzyści z testów osiągamy podczas ich wymyślania
i pisania, a nie podczas uruchamiania.
Myślenie o testach
Jest poniedziałek rano. Właśnie usiadłeś w fotelu, aby rozpocząć pracę nad
jakimś nowym kodem. Musisz napisać kod, który odpytuje bazę danych w celu
uzyskania listy osób oglądających ponad 10 filmów tygodniowo za pośrednictwem swojej ulubionej witryny wideo.
Uruchomiłeś edytor i zaczynasz pisać funkcję, która wykonuje zapytanie:
def return_avid_viewers do
# ... hmmm ...
end
Stop! Skąd wiesz, że to, co masz zamiar zrobić, jest dobre?
Odpowiedź jest taka, że nie możesz tego wiedzieć. Nikt nie może. Ale dzięki
myśleniu o testach możemy sprawić, że prawdopodobieństwo wykonania prawidłowego kodu będzie większe. Oto wyjaśnienie, w jaki sposób to działa.
Zacznijmy sobie wyobrażać, że właśnie skończyliśmy pisać funkcję i teraz
musimy ją przetestować. W jaki sposób byśmy to robili? Trzeba by użyć jakichś
danych testowych, co prawdopodobnie oznacza, że chcielibyśmy pracować
z bazą danych, która byłaby pod naszą kontrolą. Obecnie istnieją frameworki,
które potrafią to obsłużyć — uruchomić testy z wykorzystaniem testowej bazy
danych. W naszym przypadku oznacza to jednak konieczność przekazania do
funkcji egzemplarza testowej bazy danych zamiast używania globalnej. Dzięki
temu bowiem możemy zmienić bazę danych podczas testowania:
def return_avid_users(db) do
4337ebf6db5c7cc89e4173803ef3875a
4
252 
Rozdział 7.
Kiedy kodujemy…
Następnie musimy pomyśleć o tym, w jaki sposób można by wypełnić takie
testowe dane. Zgodnie z wymaganiem, chcemy stworzyć „listę osób, które oglądają ponad 10 filmów w tygodniu”. W związku z tym zaglądamy do schematu
bazy danych w celu znalezienia pól, które mogą nam pomóc. Znaleźliśmy dwa
pola w tabeli, które prawdopodobnie mogą nam odpowiedzieć na pytanie kto
i co oglądał: opened_video i completed_video. Aby napisać nasze testowe dane,
musimy wiedzieć, z jakiego pola skorzystać. Nie wiemy jednak, co oznacza
nasze wymaganie, a nie mamy dostępu do kontaktu biznesowego. Spróbujmy
wyszukać i przekazać nazwę pola (co pozwoli nam przetestować to, co mamy,
i potencjalnie zmienić to później):
def return_avid_users(db, qualifying_field_name) do
Zaczęliśmy myśleć o testach i bez napisania nawet jednej linijki kodu dokonaliśmy dwóch odkryć, które wykorzystaliśmy do zmiany API naszej metody.
Testy sterują kodowaniem
W poprzednim przykładzie, dzięki myśleniu o testach udało nam się ograniczyć
sprzężenia w naszym kodzie (dzięki przekazaniu połączenia do bazy danych
zamiast używania bazy globalnej) oraz poprawiliśmy elastyczność (dzięki wykorzystaniu testowanego pola jako parametru). Myślenie o pisaniu testu dla
naszej metody dało nam jeszcze jedną korzyść: udało nam się spojrzeć na kod
z zewnątrz — tak, jak byśmy byli klientami kodu, a nie jego autorami.
WSKAZÓWKA NR 67
Test jest pierwszym użytkownikiem naszego kodu.
Naszym zdaniem to jest prawdopodobnie największa korzyść z testowania:
dostarczenie sprzężenia zwrotnego, które jest przewodnikiem dla dalszego
kodowania.
Funkcja (lub metoda), która jest ściśle sprzężona z innym kodem, jest trudna
do testowania, bo wymaga skonfigurowania wszystkich tych środowisk przed
jej uruchomieniem. Zatem doprowadzenie kodu do stanu testowalności jednocześnie zmniejsza sprzężenia.
Aby przetestować kod, trzeba go najpierw zrozumieć. Choć brzmi to niezbyt
mądrze, to z pewnością wszystkim nam zdarzało się rozpoczynać kodowanie
w oparciu o mgliste zrozumienie tego, co mieliśmy do zrobienia. Zazwyczaj
utwierdzamy się w przekonaniu, że wszystko stanie się jasne w trakcie pracy.
Aha, a kod, który ma obsługiwać warunki brzegowe, także dodamy później.
O, i jeszcze dodamy obsługę błędów. W efekcie kod jest pięć razy dłuższy niż
powinien być, ponieważ jest pełen logiki warunkowej oraz przypadków szczególnych. Wystarczy jednak rzucić światło na testy tego kodu i wszystko staje
się jaśniejsze. Jeśli myślisz o testowaniu warunków brzegowych oraz w jaki
4337ebf6db5c7cc89e4173803ef3875a
4
Kod łatwy do testowania
 253
sposób będzie działać kod przed rozpoczęciem kodowania, być może znajdziesz
również wzorce w logice, dzięki którym będzie możliwe uproszczenie funkcji.
Jeśli pomyślisz o warunkach błędów, które należy przetestować, to także nadasz
swojej funkcji odpowiednią strukturę.
Programowanie sterowane testami
Istnieje szkoła programowania, która mówi, że ze względu na korzyści płynące
z myślenia o testach przed kodowaniem, można pójść o krok dalej i napisać
je przed napisaniem kodu. Praktykę tę określa się terminem programowania
sterowanego testami (ang. Test-Driven Development — TDD. Znany jest również termin „najpierw test”9.
Podstawowy cykl techniki TDD jest następujący:
1. Znajdź niewielki fragment funkcjonalności, który chcesz dodać.
2. Napisz test, który przejdzie, kiedy funkcjonalność zostanie zaimplementowana.
3. Uruchom wszystkie testy. Upewnij się, że jedyne testy, które nie przechodzą,
to te, które właśnie napisałeś.
4. Napisz jak najmniejszą ilość kodu potrzebną do tego, aby test zaczął
przechodzić i sprawdź, czy teraz wszystkie testy przechodzą bez problemu.
5. Przeprowadź refaktoryzację kodu: sprawdź, czy istnieje sposób poprawienia
kodu, który właśnie napisałeś (testu lub funkcjonalności). Zadbaj o to,
aby testy po refaktoryzacji nadal przechodziły.
Chodzi o to, aby ten cykl był jak najkrótszy: powinien trwać najwyżej kilka
minut, tak abyśmy ciągle pisali testy, a następnie doprowadzali system do stanu,
w którym te testy przechodzą.
Widzimy istotną korzyść ze stosowania TDD dla osób, które dopiero zaczynają
uczyć się testowania. Jeśli będziesz przestrzegać przepływu pracy TDD, zyskasz
gwarancję, że zawsze będziesz mieć testy dla kodu. A to oznacza, że zawsze
będziesz myśleć o swoich testach.
Spotykaliśmy jednak również osoby, które zachowywały się tak, jakby były niewolnikami TDD. Taka postawa objawia się na wiele sposobów:

9
Poświęcanie bardzo wiele czasu na zapewnienie stuprocentowego pokrycia
testami.
Niektórzy twierdzą, że podejście „najpierw test” i technika TDD to dwie różne rzeczy.
Uzasadniają, że intencje tych dwóch podejść są różne. Jednak z historycznego punktu
widzenia podejście „najpierw test” (które wywodzi się z nurtu programowania ekstremalnego — ang. eXtreme Programming) jest identyczne z techniką, którą dziś określamy jako TDD.
4337ebf6db5c7cc89e4173803ef3875a
4
254 
Rozdział 7.
Kiedy kodujemy…

Wiele nadmiarowych testów. Na przykład przed napisaniem klasy po raz
pierwszy, wielu zwolenników TDD najpierw pisze nieprzechodzący test,
który po prostu odwołuje się do nazwy klasy. Test nie przechodzi, więc
piszą pustą definicję klasy i test przechodzi. Ale teraz mamy test, który nie
robi zupełnie niczego; kolejny napisany test również będzie odwoływać się
do klasy, co sprawia, że ten pierwszy test jest zbyteczny. Jeśli nazwa klasy
później ulegnie zmianie, będzie więcej rzeczy do zmiany. To bardzo trywialny przykład.

Projekty są tworzone w stylu dół-góra (zobacz: „Dół-góra czy góra-dół. Jak
powinien wyglądać projekt?”).
Z całą pewnością należy stosować TDD. Ale jeśli to robisz, nie zapomnij, aby
zatrzymać się co jakiś czas i przyjrzeć się większemu obrazowi. Łatwo może
nas zwieść komunikat „testy przechodzą na zielono” — często oznacza on pisanie mnóstwa kodu, który w gruncie rzeczy nie przybliża Cię do rozwiązania.
TDD: musisz wiedzieć dokąd zmierzasz
W jednym ze starych dowcipów pojawia się pytanie: „Jak zjeść słonia?”. Odpowiedź brzmi „Kęs po kęsie”. Ten pomysł jest często reklamowany jako korzyść
ze stosowania TDD. Kiedy nie jesteś w stanie zrozumieć problemu w całości,
podejmuj małe kroki — po jednym teście na raz. Jednak takie podejście może
wprowadzać Cię w błąd — zachęcać do skupiania się na nieustannym polerowaniu łatwych problemów i ignorowaniu prawdziwego powodu, dla którego
kodujemy. Ciekawy przykład takiej postawy miał miejsce w 2006 roku, kiedy
Ron Jeffries, czołowa postać w ruchu Agile, rozpoczął serię postów w blogu,
które dokumentowały stosowanie podejścia TDD podczas kodowania aplikacji
do rozwiązywania Sudoku10. Po pięciu postach Ron zmodyfikował reprezentację
planszy i kilkakrotnie zrefaktoryzował kod, aż w końcu był zadowolony ze swojego modelu obiektowego. Ale potem porzucił projekt. Warto przeczytać posty
w blogu po kolei i zaobserwować, jak mądrego człowieka mogą zwieść drobne
detale, a blask przechodzących testów może go utwierdzać w przekonaniu
o słuszności swojej postawy.
Peter Norvig opisuje inne, alternatywne podejście11, które ma zupełnie odmienny
charakter: zamiast sterowania kodu testami zaczyna od podstawowego zrozumienia, jak te rodzaje problemów są rozwiązywane tradycyjnie (przy użyciu
propagacji ograniczeń), a następnie skupia się na udoskonalaniu algorytmu.
Rozwiązuje on reprezentację planszy za pomocą kilkunastu linijek kodu, które
wynikają bezpośrednio z jego opisu notacji.
10
https://ronjeffries.com/categories/sudoku. Wielkie „dziękuję” dla Rona za udostępnienie
nam tej historii.
11
http://norvig.com/sudoku.html
4337ebf6db5c7cc89e4173803ef3875a
4
Kod łatwy do testowania
 255
Dół-góra czy góra-dół. Jak powinien wyglądać projekt?
W czasach, gdy informatyka była dziedziną młodą i beztroską, istniały dwie szkoły
tworzenia projektów: góra-dół i dół-góra. Zwolennicy podejścia góra-dół twierdzili,
że należy zacząć od ogólnego problemu, który próbujesz rozwiązać, i podzielić go
na niewielką liczbę części. Następnie trzeba podzielić każdą z tych części na mniejsze kawałki i tak dalej, aż liczba kawałków będzie dostatecznie mała, aby wyrazić je
w kodzie.
Zwolennicy podejścia dół-góra budowali kod tak, jak buduje się dom. Zaczynali od
dołu, tworząc warstwę kodu, który dostarczał pewną warstwę abstrakcji zbliżającą ich
do problemu, który starali się rozwiązać. Potem dodawali kolejną warstwę, odpowiadającą wyższemu poziomowi abstrakcji. Postępowali w ten sposób tak długo, aż w końcu
dodali końcową warstwę abstrakcji, która rozwiązywała problem. „Zrób to tak…”.
Żadna z tych szkół faktycznie nie działa, ponieważ obie ignorują jeden z najważniejszych aspektów rozwoju oprogramowania: gdy zaczynamy, nie wiemy, co robimy.
Zwolennicy podejścia góra-dół zakładają, że mogą wyrazić wszystkie wymagania
z góry: otóż nie mogą. Zwolennicy podejścia dół-góra zakładają, że potrafią zbudować listę abstrakcji, które w końcu doprowadzą ich do jednego rozwiązania na najwyższym poziomie. W jaki sposób jednak mogą zdecydować o funkcjonalnościach
w poszczególnych warstwach, gdy nie wiedzą, dokąd zmierzają?
WSKAZÓWKA NR 68
Buduj oprogramowanie „od końca do końca”, a nie w stylu góra-dół czy
dół-góra.
Jesteśmy przekonani, że jedynym prawidłowym sposobem budowania oprogramowania jest tworzenie go przyrostowo. Buduj niewielkie kawałki funkcjonalności
od końca do końca, ucząc się problemu w miarę postępów prac. Stosuj uzyskaną
wiedzę podczas nadawania kształtu kodowi, na każdym kroku konsultuj się z klientem
i pozwól mu sterować procesem.
Testy z całą pewnością mogą pomóc w sterowaniu wytwarzaniem oprogramowania. Ale, tak jak z każdym napędem, jeśli nie mamy w głowie docelowego
kierunku, możemy kręcić się w kółko.
Wracamy do kodowania
Tworzenie oprogramowania bazujące na komponentach jest od dawna szczytnym celem rozwoju oprogramowania12. Chodzi o stworzenie generycznych kom12
Próbowaliśmy tej techniki co najmniej od 1986 roku, kiedy Cox i Novobilski w swojej
książce Object-Oriented Programming: An Evolutionary Approach [CN91] ukuli termin
„oprogramowanie IC”.
4337ebf6db5c7cc89e4173803ef3875a
4
256 
Rozdział 7.
Kiedy kodujemy…
ponentów oprogramowania, które będą łączone równie łatwo, jak są łączone
układy scalone (IC). Takie podejście sprawdza się jednak tylko wtedy, gdy wykorzystywane elementy są niezawodne, stosują standardowe napięcia, sposoby
połączeń, obsługę czasu i tak dalej.
Czipy projektuje się z myślą o dalszych testach — nie tylko w fabryce czy
miejscu ich instalacji, ale także w warunkach, w których zostaną wdrożone
i będą używane. Bardziej złożone czipy i systemy nierzadko zawierają wbudowane funkcje samotestujące BIST (od ang. Built-In Self Test), które wewnętrznie
wykonują podstawowe czynności diagnostyczne, lub mechanizm testowania
dostępu (ang. Test Access Mechanism — TAM) umożliwiający badanie i gromadzenie statystyk odpowiedzi układu na żądanie z zewnątrz.
To samo możemy osiągnąć z naszym oprogramowaniem. Tak jak nasi koledzy
z branży układów scalonych, musimy od samego początku uwzględniać przyszłe
testy w pisanym kodzie oraz dokładnie testować każdy fragment oprogramowania jeszcze przed jego połączeniem z resztą systemu.
Testy jednostkowe
Odpowiednikiem testów na poziomie sprzętowego czipu są testy jednostkowe
oprogramowania, czyli testy poszczególnych modułów wykonywane bez udziału
innych modułów i weryfikujące ich zachowanie. Testy w kontrolowanych (choćby
wymyślonych) warunkach pozwalają nam lepiej zrozumieć reakcje modułu
w rozmaitych sytuacjach.
Test jednostkowy oprogramowania ma postać kodu sprawdzającego konkretny
moduł. Test jednostkowy zwykle tworzy pewnego rodzaju sztuczne środowisko,
po czym wywołuje z poziomu tego środowiska funkcje testowanego modułu.
Test sprawdza otrzymane wyniki, porównując je albo ze znanymi wartościami,
albo z wynikami uzyskanymi podczas wcześniejszych wykonań tego samego
testu (w takim przypadku mówi się o tzw. testach regresji).
Kiedy w przyszłości łączymy nasze programowe „układy scalone” w kompletny
system, możemy być pewni, że poszczególne składniki tego systemu działają
zgodnie z naszymi oczekiwaniami. Co więcej, możemy wykorzystać te same
testy jednostkowe do sprawdzania systemu jako całości. Problemem weryfikacji systemu w większej skali zajmiemy się w podrozdziale „Bezlitosne testy”
w rozdziale 9.
Zanim jednak skupimy swoją uwagę na całym systemie, musimy zdecydować,
co należy testować na poziomie jednostki. Programiści zwykle umieszczają
w kodzie kilka losowo dobranych wartości, aby na ich przykładzie testować
tworzony moduł. Okazuje się, że możemy uzyskać dużo lepsze rezultaty.
4337ebf6db5c7cc89e4173803ef3875a
4
Kod łatwy do testowania
 257
Testowanie według kontraktu
Lubimy myśleć o testach jednostkowych jako o sposobie testowania zgodności z kontraktem (patrz temat 23., „Projektowanie kontraktowe”, mieszczący
się w rozdziale 4.). Chcemy pisać przypadki testowe, które dadzą nam pewność,
że dana jednostka wypełnia swój kontrakt. W ten sposób uzyskujemy dwie
informacje — to, czy nasz kod jest zgodny z kontraktem, oraz to, czy kontrakt rzeczywiście oznacza to, co o nim myślimy. Chcemy sprawdzić, czy nasz
moduł dostarcza obiecane funkcje w szerokim zakresie przypadków testowych
i warunków granicznych.
Co to oznacza w praktyce? Zacznijmy od prostego przykładu funkcji obliczającej
pierwiastek kwadratowy. Kontrakt dla tej funkcji jest dość prosty:
warunki-wstępne:
argument >= 0;
warunki-końcowe:
((result * result) - argument).abs <= epsilon * argument;
Na tej podstawie możemy łatwo stwierdzić, co należy przetestować:

Należy przekazać ujemny argument i sprawdzić, czy zostanie odrzucony.

Należy przekazać argument równy 0 i sprawdzić, czy zostanie zaakceptowany (to nasza wartość graniczna).

Należy przekazać wartość z przedziału od 0 do maksymalnego argumentu
możliwego do wyrażenia w ten sposób i sprawdzić, czy różnica dzieląca
kwadrat wyniku od oryginalnego argumentu jest mniejsza niż pewien
niewielki ułamek tego argumentu.
Skoro dysponujemy kontraktem w tej formie i przyjmujemy, że nasza funkcja
zawiera mechanizmy sprawdzania warunków początkowych i końcowych,
możemy napisać prosty skrypt testowy, który sprawdzi funkcję pierwiastka
kwadratowego.
Możemy następnie wywołać tę funkcję, aby przetestować właściwą funkcję obliczającą pierwiastek kwadratowy:
assertWithinEpsilon(my_sqrt(0), 0)
assertWithinEpsilon(my_sqrt(2.0), 1.4142135624)
assertWithinEpsilon(my_sqrt(64.0), 8.0)
assertWithinEpsilon(my_sqrt(1.0e7), 3162.2776602)
assertRaisesException fn => my_sqrt(-4.0) end
Zaproponowany test jest dość prosty; w rzeczywistych rozwiązaniach każdy
niebanalny moduł jest zależny od wielu innych modułów. Jak w takich przypadkach należałoby testować kombinacje tych modułów?
Przypuśćmy, że moduł A używa modułów DataFeed i LinearRegression. Powinniśmy
przetestować:
4337ebf6db5c7cc89e4173803ef3875a
4
258 
Rozdział 7.
Kiedy kodujemy…
1. Kontrakt dla modułu DataFeed (w całości).
2. Kontrakt dla modułu LinearRegression (w całości).
3. Kontrakt modułu A, którego działanie zależy od innych kontraktów, mimo
że te kontrakty nie są wskazane wprost.
Opisany styl testowania wymaga w pierwszej kolejności testowania komponentów w ramach samego modułu. Po ich sprawdzeniu można przystąpić do testów
samego modułu.
Jeśli testy modułów DataFeed i LinearRegression przebiegły prawidłowo, a mimo
to test modułu A zakończył się niepowodzeniem, możemy być niemal pewni, że
problem leży właśnie w tym module A lub sposobie korzystania przez ten
moduł z niezbędnych komponentów. Proponowana technika jest wprost doskonałym sposobem ograniczania kosztów przyszłego debugowania kodu — możemy
od razu skoncentrować się na prawdopodobnym źródle problemu w ramach
modułu A, zamiast tracić czas na analizę jego komponentów.
Dlaczego w ogóle poświęcamy czas temu zagadnieniu? Chcemy przede wszystkim uniknąć sytuacji, w której nasz kod będzie zawierał swoistą bombę z opóźnionym zapłonem — błędu, który pozostanie niezauważony w kodzie i ujawni
się w najmniej oczekiwanym momencie na późniejszym etapie prac nad projektem. Kładąc nacisk na testy pod kątem zgodności z kontraktem, możemy wyjątkowo skutecznie unikać podobnych katastrof.
WSKAZÓWKA NR 69
Należy projektować z myślą o testach.
Testy ad hoc
Testy ad hoc są wykonywane podczas wstępnego przeglądania kodu. Takie testy
mogą ograniczać się do prostych wyrażeń console.log() bądź fragmentów kodu
jednorazowo wpisywanych w debugerze, środowisku IDE lub REPL.
Na końcu sesji debugowania powinniśmy nadać testom ad hoc formalny charakter. Jeśli nasz kod raz uległ awarii, najprawdopodobniej sytuacja kiedyś się
powtórzy. Nie należy więc rezygnować z utworzonego testu — powinniśmy raczej
dodać nowy kod sprawdzający do istniejącego testu jednostkowego.
Budowa okna testowego
Nawet najlepsze zestawy testów zwykle nie znajdują wszystkich błędów.
W środowisku produkcyjnym często mamy do czynienia z warunkami, które
nigdy nie występują w warsztacie programisty.
Oznacza to, że często musimy testować nasze oprogramowanie już po wdrożeniu, kiedy w żyłach naszego systemu płyną rzeczywiste dane. W przeci-
4337ebf6db5c7cc89e4173803ef3875a
4
Kod łatwy do testowania
 259
wieństwie do płytki drukowanej czy czipu nasze oprogramowanie nie oferuje
pinów testowych. Istnieje jednak wiele innych sposobów zapewniania dostępu
do wewnętrznego stanu modułu bez konieczności stosowania debugera (którego
używanie w przypadku aplikacji produkcyjnych może być niewygodne lub wręcz
niemożliwe).
Jednym z takich mechanizmów są pliki logów zawierające stosowne komunikaty. Zapisy w pliku dziennika powinny cechować się standardowym, spójnym
formatem — być może w przyszłości będziemy chcieli automatycznie je parsować, aby na tej podstawie określać czas przetwarzania lub wybierane przez program ścieżki logiczne. Wartość komunikatów diagnostycznych w nieprzemyślanym lub niespójnym formacie jest bardzo niewielka — są nie tylko trudne
do odczytania, ale też niepraktyczne podczas parsowania.
Alternatywnym mechanizmem zaglądania do działającego kodu jest specjalna
sekwencja klawiszy. Po naciśnięciu odpowiedniej kombinacji klawiszy na ekranie jest wyświetlane okno diagnostyczne prezentujące na przykład komunikaty
o stanie systemu. Tego rodzaju dane w normalnych warunkach nie powinny być
udostępniane użytkownikom końcowym, ale mogą być bezcenne dla pracowników działu wsparcia technicznego.
Bardziej ogólnym rozwiązaniem może być użycie przełącznika, który włącza
dodatkowe mechanizmy diagnostyczne dla konkretnego użytkownika lub grupy
użytkowników.
Kultura testowania
Każde pisane przez nas oprogramowanie zostanie przetestowane — jeśli nie przez
nas ani nasz zespół, to przez użytkowników końcowych. Warto więc uważnie
zaplanować niezbędne testy. Wystarczy odrobina przezorności, aby znacznie
ograniczyć koszty konserwacji i liczbę telefonów do działu wsparcia.
W istocie mamy do wyboru tylko kilka opcji:

Testy najpierw.

Testy w trakcie.

Testy nigdy.
Podejście „testy najpierw”, włącznie z techniką TDD, jest w większości przypadków najlepsze, ponieważ daje pewność wykonania testów. Czasami jednak
nie jest ono zbyt wygodne lub przydatne. W związku z tym dobrą alternatywą
może być podejście „testy w trakcie”, polegające na napisaniu fragmentu kodu,
sprawdzeniu go, napisaniu dla niego zbioru testów, a następnie przejściu do
następnego fragmentu. Najgorsze z podejść często jest określane jako „testy
później”. To jednak jest jakiś żart. Podejście „testy później” w praktyce oznacza
„testy nigdy”.
4337ebf6db5c7cc89e4173803ef3875a
4
260 
Rozdział 7.
Kiedy kodujemy…
Kultura testowania wymaga, aby zawsze przechodziły wszystkie testy. Zignorowanie kilku testów, które „nigdy nie przechodzą” sprawia, że zbliżamy się do
ignorowania wszystkich testów i zaczyna się spirala zepsucia (patrz temat 3.,
„Entropia oprogramowania”, mieszczący się w rozdziale 1.).
Spowiedź
Ja (Dave) jestem znany z mówienia innym, że przestałem pisać testy. Częściowo robię
to po to, aby zachwiać wiarą tych, którzy z testowania zrobili religię. A częściowo
dlatego, że jest to (w pewnym sensie) prawda.
Koduję od 45 lat, a automatyczne testy pisałem od ponad 30. Myślenie o testowaniu
jest wbudowane w sposób, w jaki podchodzę do kodowania. Czuję się z tym komfortowo. Moja intuicja podpowiada mi, że gdy w czymś zaczynam czuć się komfortowo, to powinienem przejść do czegoś innego.
W tym przypadku postanowiłem przestać pisać testy na kilka miesięcy i zobaczyć,
jaki to będzie miało wpływ na mój kod. Ku mojemu zaskoczeniu, odpowiedź
brzmiała „niezbyt wielki”. W związku z tym poświęciłem trochę czasu, aby odpowiedzieć na pytanie dlaczego tak się stało.
Uważam, że chodzi o to, że (przynajmniej dla mnie) większość korzyści z testowania
pochodzi z myślenia o testach oraz ich wpływie na kod. Po pisaniu testów przez tak
długi czas potrafię myśleć o testowaniu bez faktycznego pisania testów. Mój kod
nadal był testowalny, a jedynie nie był testowany.
Ale zignorowałem fakt, że testy są również sposobem komunikowania się z innymi
programistami. Z tego powodu teraz znów piszę testy dla kodu, który współdzielę
z innymi, lub takiego, który bazuje na osobliwościach zewnętrznych zależności.
Andy mówi, że nie powinienem zamieszczać tej ramki. Obawia się, że będzie kusić
niedoświadczonych programistów, by nie pisać testów. Oto mój kompromis:
Czy trzeba pisać testy? Tak. Ale kiedy robisz to od 30 lat, możesz trochę poeksperymentować, aby zobaczyć, skąd pochodzą korzyści, jakie uzyskujesz.
Traktuj kod testów z taką samą starannością, jak kod produkcyjny. Staraj się
eliminować z niego sprzężenia, dąż do tego, by był czysty i odporny na awarie.
Nie polegaj na informacjach, które są niewiarygodne (patrz temat 38., „Programowanie przez koincydencję”), takich jak bezwzględna pozycja widżetów
w interfejsie GUI, dokładne znaczniki czasu w logu serwera lub dokładna treść
komunikatów o błędach. Testowanie z wykorzystaniem tego rodzaju elementów
doprowadzi do powstania kruchych testów.
WSKAZÓWKA NR 70
Należy testować swoje oprogramowania; w przeciwnym razie zrobią to
nasi użytkownicy.
4337ebf6db5c7cc89e4173803ef3875a
4
Testowanie na podstawie właściwości
 261
Nie popełnij błędu, testowanie jest częścią programowania. Nie jest czymś, czym
powinien zająć się inny dział lub wyznaczony personel.
Testowanie, projektowanie, kodowanie — to wszystko jest programowanie.
Pokrewne podrozdziały
42
41

Temat 27., „Nie prześcigaj swoich świateł”.

Temat 51., „Zestaw startowy pragmatyka”.
Testowanie na podstawie właściwości
Доверяй, но проверяй (Ufaj, ale sprawdzaj)
Przysłowie rosyjskie
Zalecamy pisanie testów jednostkowych dla funkcji. W tym celu próbujemy
znaleźć typowe problemy, na jakie może napotkać funkcja, na podstawie naszej
znajomości przedmiotu testu.
W tym podejściu tkwi jednak niewielki, ale potencjalnie istotny problem. Jeśli
napiszesz kod, a następnie napiszesz dla niego testy, to czy istnieje możliwość,
że w obu zostaną wyrażone nieprawidłowe założenia? Kod przechodzi testy,
ponieważ robi to, co powinien w oparciu o nasze zrozumienie problemu.
Jednym ze sposobów na poradzenie sobie z tym problemem jest pisanie testów
i testowanego kodu przez różne osoby. To jednak nam się nie podoba. Jak
powiedzieliśmy w temacie 41., „Kod łatwy do testowania”, jedną z największych
zalet myślenia o testach jest wnioskowanie na temat pisania kodu na podstawie
testów. Tracimy tę korzyść w przypadku, gdy praca nad testami jest oddzielona
od pracy nad kodem.
Zamiast tego zalecamy alternatywę polegającą na tym, że to komputer, który
nie współdzieli Twoich uprzedzeń, wykonuje testowanie za Ciebie.
Kontrakty, niezbędniki i właściwości
W rozdziale 4., w temacie 23., „Projektowanie kontraktowe”, omawialiśmy koncepcję, zgodnie z którą kod spełnia określone kontrakty: spełnia warunki, gdy
dostarczysz do niego dane wejściowe i sprawisz, że będą zapewnione określone
gwarancje dotyczące generowanych wyników.
Istnieją również niezmienniki kodu — założenia co do fragmentu jakiegoś
stanu, które są prawdziwe, gdy przekażemy ten stan za pomocą funkcji. Na
przykład, jeśli sortujesz listę, wynik będzie miał taką samą liczbę elementów,
co oryginał — długość listy jest niezmiennikiem.
4337ebf6db5c7cc89e4173803ef3875a
4
262 
Rozdział 7.
Kiedy kodujemy…
Gdy określimy kontrakty i niezmienniki (które będziemy wspólnie określać
właściwościami), możemy je wykorzystać do zautomatyzowania testów. Taką
technikę będziemy nazywali testowaniem w oparciu o właściwości.
WSKAZÓWKA NR 71
Do weryfikacji założeń stosuj technikę testowania w oparciu o właściwości.
W ramach wymyślonego przykładu spróbujmy stworzyć kilka testów dla naszej
posortowanej listy. Ustaliliśmy już jedną właściwość: posortowaną listę, która
ma taki sam rozmiar co lista wejściowa. Możemy również stwierdzić, że żaden
element w wyniku nie może być większy niż ten, który następuje po nim.
Teraz możemy wyrazić to w kodzie. W większości języków jest dostępny jakiś
framework testowania w oparciu o właściwości. Prezentowany przykład napisany jest w Pythonie i korzysta z narzędzi hyphothesis i pytest, ale zasady w nim
zastosowane są dość uniwersalne.
Oto kompletny kod źródłowy testów:
proptest/sort.py
from hypothesis import given
import hypothesis.strategies as some
@given(some.lists(some.integers()))
def test_list_size_is_invariant_across_sorting(a_list):
original_length = len(a_list)
a_list.sort()
assert len(a_list) == original_length
@given(some.lists(some.text()))
def test_sorted_result_is_ordered(a_list):
a_list.sort()
for i in range(len(a_list) - 1):
assert a_list[i] <= a_list[i + 1]
Oto wyniki uruchomionych testów:
$ pytest sort.py
============================= test session starts =============================
...
plugins: hypothesis-5.16.1
collected 2 items
sort.py ..
[100%]
============================== 2 passed in 2.53s ==============================
W tym kodzie nie dzieje się nic specjalnego. Ale za kulisami, narzędzie Hypothesis uruchomiło oba nasze testy sto razy, za każdym razem przekazując do
nich inną listę. Listy mają różne długości i różne zawartości. To tak, jakbyśmy
uruchomili 200 pojedynczych testów z 200 losowymi listami.
4337ebf6db5c7cc89e4173803ef3875a
4
Testowanie na podstawie właściwości
 263
Generowanie danych testowych
Podobnie jak większość bibliotek testowania na podstawie właściwości, biblioteka Hypothesis oferuje minijęzyk do opisywania danych, które framework
powinien generować. Język ten opiera się na wywołaniach funkcji w module
hypothesis.strategies, dla którego zastosowaliśmy alias some, po prostu dlatego,
że lepiej czyta się go w kodzie.
Jeśli napiszemy:
@given(some.integers())
nasza funkcja testowa uruchomiłaby się wiele razy. Za każdym razem w roli
parametru zostałaby przekazana inna liczba całkowita. Jeśli zamiast tego
napiszemy:
@given(some.integers(min_value=5, max_value=10).map(lambda x: x * 2))
to do funkcji będą przekazywane liczby parzyste z przedziału od 10 do 20.
Możemy także zastosować styl mieszany:
@given(some.lists(some.integers(min_value=1), max_size=100))
Dzięki temu uzyskamy listę liczb naturalnych o długości co najwyżej 100 elementów.
Niniejsza książka nie ma być podręcznikiem żadnego konkretnego frameworka,
dlatego pominiemy szczegóły i zamiast nich zajmiemy się rzeczywistym przykładem.
Znajdowanie złych założeń
Napiszemy prosty system przetwarzania zamówień i zarządzania zapasami
(ponieważ zawsze jest miejsce, by taki system napisać). System modeluje poziomy
magazynowe za pomocą obiektu Warehouse. Możemy odpytać obiekt Warehouse,
aby sprawdzić, czy towar jest w magazynie, żeby pobrać towary z magazynu
i uzyskać aktualne stany magazynowe.
Oto kod:
proptest/stock.py
class Warehouse:
def __init__(self, stock):
self.stock = stock
def in_stock(self, item_name):
return (item_name in self.stock) and (self.stock[item_name] > 0)
def take_from_stock(self, item_name, quantity):
if quantity <= self.stock[item_name]:
self.stock[item_name] -= quantity
4337ebf6db5c7cc89e4173803ef3875a
4
264 
Rozdział 7.
Kiedy kodujemy…
else:
raise Exception("Wyprzedano {}".format(item_name))
def stock_count(self, item_name):
return self.stock[item_name]
Napisaliśmy podstawowy test jednostkowy, który przechodzi:
proptest/stock.py
def test_warehouse():
wh = Warehouse({"buty": 10, "kapelusze": 2, "parasole": 0})
assert wh.in_stock("buty")
assert wh.in_stock("kapelusze")
assert not wh.in_stock("parasole")
wh.take_from_stock("buty", 2)
assert wh.in_stock("buty")
wh.take_from_stock("kapelusze", 2)
assert not wh.in_stock("kapelusze")
Następnie napisaliśmy funkcję, która przetwarza żądanie zamówienia towarów
z magazynu. Funkcja zwraca krotkę, w której pierwszym elementem jest ciąg
"ok" lub "niedostępny", a kolejnymi zamawiany towar i żądana ilość. Napisaliśmy
też kilka testów, które przechodzą:
proptest/stock.py
def order(warehouse, item, quantity):
if warehouse.in_stock(item):
warehouse.take_from_stock(item, quantity)
return ( "ok", item, quantity )
else:
return ("niedostępny", item, quantity )
proptest/stock.py
def test_order_in_stock():
wh = Warehouse({"buty": 10, "kapelusze": 2, "parasole": 0})
status, item, quantity = order(wh, "kapelusze", 1)
assert status
== "ok"
assert item
== "kapelusze"
assert quantity == 1
assert wh.stock_count("kapelusze") == 1
def test_order_not_in_stock():
wh = Warehouse({"buty": 10, "kapelusze": 2, "parasole": 0})
status, item, quantity = order(wh, "parasole", 1)
assert status
== "niedostępny"
assert item
== "parasole"
assert quantity == 1
assert wh.stock_count("parasole") == 0
def test_order_unknown_item():
wh = Warehouse({"buty": 10, "kapelusze": 2, "parasole": 0})
status, item, quantity = order(wh, "bajgiel", 1)
assert status
== "niedostępny"
assert item
== "bajgiel"
assert quantity == 1
4337ebf6db5c7cc89e4173803ef3875a
4
Testowanie na podstawie właściwości
 265
Na pierwszy rzut oka wszystko wygląda dobrze. Ale zanim wdrożymy kod do
produkcji, dodajmy kilka testów właściwości.
Wiemy — między innymi — że podczas całej transakcji towar nie może pojawiać
się i znikać. Oznacza to, że jeśli weźmiemy kilka towarów z magazynu, to
liczba, którą wzięliśmy plus liczba towarów aktualnie znajdujących się w magazynie powinna być taka sama, jak liczba towarów, które pierwotnie były
w magazynie. W poniższym teście parametr item został wybrany losowo ze zbioru
"kapelusze" lub "buty", a wybrana ilość wynosi od 1 do 4:
proptest/stock.py
@given(item
= some.sampled_from(["kapelusze", "buty"]),
quantity = some.integers(min_value=1, max_value=4))
def test_stock_level_plus_quantity_equals_original_stock_level(item, quantity):
wh = Warehouse({"buty": 10, "kapelusze": 2, "parasole": 0})
initial_stock_level = wh.stock_count(item)
(status, item, quantity) = order(wh, item, quantity)
if status == "ok":
assert wh.stock_count(item) + quantity == initial_stock_level
Spróbujmy uruchomić testy:
$ pytest stock.py
...
stock.py:64:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stock.py:69: in test_stock_level_plus_quantity_equals_original_stock_level
(status, item, quantity) = order(wh, item, quantity)
stock.py:35: in order
warehouse.take_from_stock(item, quantity)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stock.Warehouse object at 0x00000269702D43A0>, item_name = 'kapelusze',
quantity = 3
>
E
def take_from_stock(self, item_name, quantity):
if quantity <= self.stock[item_name]:
self.stock[item_name] -= quantity
else:
raise Exception("Wyprzedano {}".format(item_name))
Exception: Wyprzedano kapelusze
stock.py:15: Exception
--------------------------------- Hypothesis ---------------------------------Falsifying example: test_stock_level_plus_quantity_equals_original_stock_level(
item='kapelusze', quantity=3,
)
Test zgłasza wyjątek w funkcji warehouse.take_from_stock: staraliśmy się usunąć
trzy kapelusze z magazynu, choć były w nim tylko dwie sztuki.
4337ebf6db5c7cc89e4173803ef3875a
4
266 
Rozdział 7.
Kiedy kodujemy…
Nasze testy właściwości wykryły błędne założenie: funkcja in_stock sprawdza
jedynie, czy istnieje co najmniej jedna pozycja w magazynie. Zamiast tego
musimy sprawdzić, czy mamy wystarczającą ilość, aby spełnić zamówienie:
proptest/stock1.py
def in_stock(self, item_name, quantity):
return (item_name in self.stock) and (self.stock[item_name] >=
quantity)
Zmodyfikujemy także funkcję order:
proptest/stock1.py
def order(warehouse, item, quantity):
if warehouse.in_stock(item, quantity):
warehouse.take_from_stock(item, quantity)
return ( "ok", item, quantity )
else:
return ("niedostępny", item, quantity )
Teraz nasze testy właściwości przechodzą.
Testy właściwości często zaskakują
W poprzednim przykładzie użyliśmy testu właściwości w celu sprawdzenia, czy
stany magazynowe były prawidłowo aktualizowane. Test znalazł błąd, ale nie
miał on nic wspólnego z aktualizacją stanu magazynowego. Zamiast tego, test
znalazł błąd w funkcji in_stock.
Testy bazujące na właściwościach mogą przynosić zarówno korzyści, jak i frustracje. Testy dają duże możliwości, ponieważ wystarczy skonfigurować pewne
zasady generowania danych wejściowych oraz przyjąć kilka założeń co do weryfikacji wyników, a reszta dzieje się automatycznie. Nigdy nie wiadomo, co się
wydarzy. Test może przejść. Asercja może się nie powieść. Kod może także ulec
awarii z powodu braku możliwości obsługi danych wejściowych.
Frustracja wynika z trudności w ustaleniu tego, co faktycznie zawiodło.
Nasza propozycja jest taka, że gdy zwiodą testy bazujące na właściwościach,
należy przyjrzeć się parametrom przekazywanym do funkcji testowej, a następnie wykorzystać te wartości w celu stworzenia osobnego, standardowego testu
jednostkowego. Taki test jednostkowy daje nam dwie korzyści. Po pierwsze
pozwala skupić się na problemie bez wykonywania w kodzie dodatkowych wywołań przez framework testów bazujących na właściwościach. Po drugie taki
test jednostkowy spełnia rolę testu regresji. Ze względu na to, że testy bazujące na właściwościach generują losowe wartości, które są przekazywane do
testu, nie ma gwarancji, że przy uruchomieniu testów następnym razem zostaną
wykorzystane te same wartości. Stworzenie testu jednostkowego, który wymusza przekazywane wartości, daje pewność uniknięcia błędu wynikającego
z losowości parametrów.
4337ebf6db5c7cc89e4173803ef3875a
4
Pozostań w bezpiecznym miejscu
 267
Testy właściwości wspomagają projekt
Kiedy mówiliśmy o testach jednostkowych, powiedzieliśmy, że jedną z głównych
ich zalet jest skłonienie programistów do myślenia o kodzie: test jednostkowy
jest pierwszym klientem Twojego API.
Tak samo jest z testami właściwości, choć objawia się to w nieco inny sposób.
Sprawiają one, że myślimy o kodzie w kontekście niezmienników i kontraktów;
myślimy o tym, co nie może się zmienić, a co musi być prawdą. Ten dodatkowy
wgląd ma magiczny wpływ na kod, powoduje usunięcie przypadków brzegowych
i wyróżnienie funkcji, które pozostawiają dane w niespójnym stanie.
Uważamy, że testy właściwości powinny być uzupełnieniem testów jednostkowych: każdy z typów testów rozwiązuje inne problemy i każdy przynosi inne
korzyści. Jeśli jeszcze nie używasz testów właściwości, spróbuj zacząć to robić.
Pokrewne podrozdziały

Temat 23., „Projektowanie kontraktowe”.

Temat 25., „Programowanie asertywne”.

Temat 45., „Kopalnia wymagań”.
Ćwiczenia
31. Przyjrzyj się ponownie przykładowi ze stanami magazynowymi. Czy znajdujesz jakieś inne właściwości, które można przetestować?
32. Twoja firma dostarcza maszyny. Każda z nich jest dostarczana w skrzyni,
a każda skrzynia jest prostokątna. Skrzynie różnią się rozmiarami. Twoim
zadaniem jest napisanie kodu, który pozwoli zapakować jak najwięcej skrzyń
w jednej warstwie, która zmieści się w samochodzie dostawczym. Wynik
Twojego kodu to lista wszystkich skrzyń. Dla każdej skrzyni na liście dodawane są informacje: lokalizacja w ciężarówce oraz szerokość skrzyni i jej
wysokość. Jakie właściwości wyniku można przetestować?
Wyzwania

43
42
Pomyśl o kodzie, nad którym aktualnie pracujesz. Jakie ma właściwości:
kontrakty i niezmienniki? Czy możesz skorzystać z frameworka testowania właściwości, aby zbadać je automatycznie?
Pozostań w bezpiecznym miejscu
Dobre ogrodzenia sprawiają, że masz dobrych sąsiadów.
Robert Frost, „Naprawianie muru”
4337ebf6db5c7cc89e4173803ef3875a
4
268 
Rozdział 7.
Kiedy kodujemy…
Podczas omawiania sprzężeń w kodzie, w pierwszym wydaniu zamieściliśmy
odważne i naiwne oświadczenie: „Nie chcemy popadać w paranoję właściwą
szpiegom czy dysydentom”. Nie mieliśmy racji. W rzeczywistości codziennie
powinniśmy być paranoikami.
W czasie, kiedy piszemy te słowa, aktualności wypełnione są opowieściami
o niszczących naruszeniach bezpieczeństwa danych, włamaniach do systemów
i oszustwach cybernetycznych. Cyberprzestępcy każdorazowo kradną setki
milionów rekordów, koszty odszkodowań i naprawy systemów sięgają wielu
miliardów dolarów, a liczby te z roku na rok szybko rosną. W zdecydowanej
większości przypadków to nie dlatego, że napastnicy byli wybitnie mądrzy albo
kompetentni.
To dlatego, że programiści byli nieostrożni.
Pozostałe 90%
Podczas kodowania zwykle przechodzimy przez kilka cykli „To działa!”, „Dlaczego to nie działa” oraz — od czasu do czasu — „To nie mogło się zdarzyć”13. Po
kilku wzlotach i upadkach podczas tego marszu pod górę, łatwo jest sobie
powiedzieć: „Uff, wszystko działa!” i ogłosić, że kod jest gotowy. Oczywiście nie
jest jeszcze gotowy. Jest w 90% gotowy, ale pozostaje jeszcze drugie 90% do
rozważenia.
Następną rzeczą, którą powinieneś zrobić, jest przeanalizowanie tego, co może
się nie udać, i uwzględnienie tych przypadków w zestawie testów. Trzeba wziąć
pod uwagę takie rzeczy jak przekazywanie nieprawidłowych parametrów, przecieki zasobów lub ich niedostępność.
W dawnych, dobrych czasach, taka ocena wewnętrznych błędów mogła być
wystarczająca. Ale dzisiaj to dopiero początek, ponieważ oprócz błędów wynikających z przyczyn wewnętrznych trzeba rozważyć, w jaki sposób zewnętrzny
napastnik może celowo zepsuć nasz system. Być może zaprotestujesz: „Przecież
nikt nie dba o ten kod, to nie jest ważne, nikt nawet nie wie o tym serwerze…”.
Świat jest wielki i w większości przypadków składa się z elementów, które są
ze sobą połączone. Dzieciak, który nudzi się po drugiej stronie planety, terroryzm sponsorowany przez państwo, gangi, osoby zajmujące się korporacyjnym
szpiegostwem, a nawet mściwy ex-partner — oni są w tym świecie i na Ciebie
czyhają. Czas przeżycia systemów bez aktualizacji, przestarzałych lub działających w otwartej sieci jest mierzony w minutach lub nawet mniejszych jednostkach.
Zabezpieczenie polegające na nieujawnianiu po prostu nie działa.
13
Patrz temat „Debugowanie”.
4337ebf6db5c7cc89e4173803ef3875a
4
Pozostań w bezpiecznym miejscu
 269
Podstawowe zasady bezpieczeństwa
Pragmatyczni programiści są zdrowymi paranoikami. Wiemy, że mamy wady
i ograniczenia, że napastnicy zewnętrzni wykorzystają każdą lukę po to, by
włamać się do naszego systemu. W każdym środowisku programistycznym
i wdrożeniowym istnieją inne potrzeby w zakresie bezpieczeństwa. Istnieje jednak kilka podstawowych zasad, o których powinniśmy pamiętać:
1. Minimalizowanie przestrzeni ataku.
2. Zasada najmniejszego poziomu uprawnień.
3. Bezpieczne wartości domyślne.
4. Szyfrowanie wrażliwych danych.
5. Zarządzanie aktualizacjami zabezpieczeń.
Rzućmy okiem na każdą z nich.
Minimalizowanie przestrzeni ataku
Przestrzeń ataku systemu jest sumą wszystkich punktów dostępowych,
w których napastnik może wprowadzać dane, wydobywać je lub wywoływać
usługi. Oto kilka przykładów:

Złożoność kodu prowadzi do wektorów ataku. Złożoność kodu poszerza
wektor ataku, stwarza więcej możliwości powstania nieoczekiwanych skutków ubocznych. Pomyśl o złożonym kodzie jako o kodzie, który ma bardziej
porowatą powierzchnię i jest w większym stopniu otwarty na infekcję. Trzeba
zapamiętać, że kod prosty i mniej rozbudowany jest lepszy. Mniej kodu oznacza mniej błędów, mniejsze ryzyko wystąpienia wyniszczającej luki w zabezpieczeniach. Prostszy, bardziej zwięzły i mniej skomplikowany kod jest
łatwiejszy do analizy i łatwiej jest w nim znaleźć potencjalne słabości.

Dane wejściowe są wektorem ataku. Nigdy nie ufaj danym wprowadzanym
przez podmioty zewnętrzne. Zawsze „dezynfekuj je” przed przekazaniem
do bazy danych, wyrenderowaniem widoku lub innym przetwarzaniem14.
Niektóre języki mają mechanizmy, które mogą w tym pomóc. Na przykład
w Ruby zmienne posiadające wejście zewnętrzne są uznawane za skażone,
co ogranicza zakres operacji, jakie można na nich wykonywać. Na przykład
poniższy kod korzysta z narzędzia wc w celu zwrócenia liczby znaków
w pliku przekazanym do programu w czasie jego wykonywania:
safety/taint.rb
puts "Podaj nazwę pliku do zliczania znaków: "
name = gets
system("wc -c #{name}")
14
Pamiętaj także o naszych dobrych przyjaciołach, niepozornych tabelach Bobby’ego
(https://xkcd.com/327). Podczas przypominania ich sobie zajrzyj na stronę https://bobbytables.com, gdzie znajdziesz sposoby odkażania danych przekazywanych do zapytań do
bazy danych.
4337ebf6db5c7cc89e4173803ef3875a
4
270 
Rozdział 7.
Kiedy kodujemy…
Złośliwy użytkownik może spowodować uszkodzenia w systemie za pośrednictwem tego kodu w następujący sposób:
Podaj nazwę pliku do zliczania znaków:
test.dat; rm -rf /
Jednak ustawienie poziomu SAFE na1 spowoduje, że dane z zewnątrz będą
uznane za skażone, co oznacza, że nie mogą być używane w niebezpiecznych kontekstach:
$SAFE = 1
puts "Podaj nazwę pliku do zliczania znaków: "
name = gets
system("wc -c #{name}")
Teraz, kiedy spróbujemy uruchomić ten kod, zostaniemy „złapani na gorącym uczynku”:
$ ruby taint.rb
Podaj nazwę pliku do zliczania znaków:
test.dat; rm -rf /
code/safety/taint.rb:5:in `system': Insecure operation - system (SecurityError)
from code/safety/taint.rb:5:in `main

Nieuwierzytelnione usługi są wektorem ataku. Nieuwierzytelnione
usługi, ze względu na ich naturę, może uruchomić każdy użytkownik
w dowolnym miejscu na świecie. W związku z tym, nawet nie biorąc
po uwagę innego sposobu wykorzystania luki, pozostawiając dostęp do
takich usług, natychmiast stwarzamy okazję co najmniej do ataku
denial-of-service. Ostatnio zanotowano sporo naruszeń użycia publicznie
dostępnych danych. Powodem tych naruszeń było nieumyślne pozostawienie przez programistów danych w nieuwierzytelnionych, publicznie dostępnych magazynach w chmurze.

Uwierzytelnione usługi są wektorem ataku. Liczba użytkowników
uprawnionych do korzystania z usług powinna być utrzymywana na
poziomie absolutnego minimum. Należy pamiętać o wycofywaniu
uprawnień nieużywanych, starych lub nieaktualnych usług, a także
nieaktywnych użytkowników. Wiele urządzeń sieciowych ma ustawione
proste hasła domyślne lub nieużywane i niezabezpieczone konta
administracyjne. W przypadku uzyskania dostępu do konta z uprawnieniami wdrażania, zagrożony jest cały produkt.

Dane wynikowe są wektorem ataku. Istnieje (prawdopodobnie apokryficzna) opowieść o systemie, który sumiennie zgłaszał komunikat
o błędzie „Hasło jest używane przez innego użytkownika”. Nie należy
ujawniać informacji o systemie. Zadbaj o to, aby dane przekazywane
w komunikatach były odpowiednie do uprawnień użytkownika, który
może je zobaczyć. Wyeliminuj z komunikatów potencjalnie ryzykowne
informacje, takie jak numery ubezpieczenia społecznego lub inne
numery identyfikacyjne.
4337ebf6db5c7cc89e4173803ef3875a
4
Pozostań w bezpiecznym miejscu

 271
Informacje diagnostyczne są wektorem ataku. Nie ma nic tak cieszącego oko napastnika jak kompletny ślad stosu z danymi w lokalnym
bankomacie, automacie na lotnisku lub witrynie internetowej, która
uległa awarii. Informacje, które mają ułatwić debugowanie, mogą
również ułatwiać włamanie do systemu. Zadbaj o to, aby wszystkie
„okna testowe” (omówione wcześniej w rozdziale) i raporty o wyjątkach
w czasie wykonywania programu były niedostępne dla wścibskich
oczu15.
WSKAZÓWKA NR 72
Zadbaj o prostotę kodu i zminimalizuj obszary ataku.
Zasada najmniejszego poziomu uprawnień
Inną kluczową zasadą jest udzielanie jak najmniejszych uprawnień na jak
najkrótszy czas. Innymi słowy, nie udzielaj automatycznie najwyższego poziomu
uprawnień właściwych dla użytkownika root lub Administrator. Jeśli taki wysoki
poziom uprawnień jest potrzebny, wykonaj z ich wykorzystaniem minimalną
ilość pracy, a następnie szybko obniż swoje uprawnienia w celu zmniejszenia
ryzyka. Poniższa zasada sięga początku lat siedemdziesiątych ubiegłego wieku:
Każdy program i każdy uprzywilejowany użytkownik systemu powinien
działać przy użyciu jak najmniejszej ilości uprawnień niezbędnych do
wykonania zadania — Jerome Saltzer, „Communications of the ACM”,
1974.
Weźmy za przykład program login w systemach uniksowych. Początkowo jest
on wykonywany z uprawnieniami użytkownika root. Jednak niezwłocznie po
zakończeniu uwierzytelniania właściwego użytkownika, program ten obniża
swoje uprawnienia do poziomu użytkownika, który się uwierzytelnił.
Nie dotyczy to tylko poziomów uprawnień systemu operacyjnego. Czy w Twojej
aplikacji zaimplementowano różne poziomy dostępu? Czy jest to tępe narzędzie, w którym obowiązują uprawnienia „administratora” albo „użytkownika”?
Jeśli tak, zastanów się nad bardziej drobnoziarnistym podziałem, tak aby
wrażliwe zasoby były podzielone na różne kategorie, a poszczególni użytkownicy
mieli uprawnienia tylko do niektórych z tych kategorii.
Dla tej techniki wykorzystywana jest taka sama koncepcja, jak podczas minimalizowania zakresu wektorów ataku, zarówno co do czasu, jak i poziomu
uprawnień. W tym przypadku mniej w rzeczywistości oznacza więcej.
15
Technika ta okazała się skuteczna na poziomie chipów CPU, gdzie za pomocą znanych
eksploitów atakowano mechanizmy diagnostyczne i administracyjne. Po ich złamaniu
napastnik uzyskuje dostęp do całego systemu.
4337ebf6db5c7cc89e4173803ef3875a
4
272 
Rozdział 7.
Kiedy kodujemy…
Bezpieczne wartości domyślne
Domyślne ustawienia w Twojej aplikacji lub dla użytkowników w Twojej witrynie powinny być najbardziej bezpiecznymi wartościami. Nie zawsze powinny to
być wartości najbardziej przyjazne dla użytkownika lub najbardziej wygodne,
ale lepiej niech każdy indywidualnie decyduje o kompromisach pomiędzy
bezpieczeństwem a wygodą.
Na przykład domyślnym ustawieniem dla wprowadzania hasła może być ukrycie
wprowadzanych znaków poprzez zastępowanie każdego z nich gwiazdką. Jeśli
wprowadzasz hasło w zatłoczonym miejscu publicznym lub kiedy obserwuje
Cię wiele osób, jest to sensowne ustawienie domyślne. Jednak niektórzy
użytkownicy chcą zobaczyć napisane hasło. Jeśli istnieje niewielkie ryzyko, że
ktoś patrzy przez ramię, udostępnienie takiej opcji dla takich użytkowników jest
rozsądne.
Szyfrowanie wrażliwych danych
Nie należy pozostawiać danych osobowych, finansowych, haseł lub innych
poświadczeń w postaci zwykłego tekstu, niezależnie od tego, czy przechowujemy
je w bazie danych, czy też w jakimś innym zewnętrznym pliku. Jeśli trzeba udostępnić takie dane, dodatkowy poziom bezpieczeństwa zapewnia szyfrowanie.
W temacie „Kontrola kodu źródłowego” w rozdziale 3. zaleciliśmy umieszczanie
wszystkiego, co jest potrzebne do realizacji projektu, w systemie kontroli wersji.
Cóż. Prawie wszystkiego. Oto jeden z głównych wyjątków od tej reguły:
W systemie kontroli wersji nie należy umieszczać razem z kodem źródłowym
tajemnic, kluczy API, kluczy SSH haseł szyfrowania lub innych poświadczeń.
Klucze i tajemnice powinny być przechowywane oddzielnie, na ogół w plikach
konfiguracyjnych lub za pośrednictwem zmiennych środowiskowych dostarczanych podczas budowania i wdrażania.
Zarządzanie aktualizacjami zabezpieczeń
Aktualizacja systemów komputerowych może być bardzo kłopotliwa. Poprawka
zabezpieczeń jest potrzebna, ale efektem ubocznym jej zainstalowania mogą być
problemy z działaniem części aplikacji. Niektórzy w takiej sytuacji decydują, by
odłożyć aktualizację na później. To fatalny pomysł, bo do czasu zainstalowania
poprawki system jest wrażliwy na działanie znanego eksploita.
WSKAZÓWKA NR 73
Instaluj aktualizacje bezpieczeństwa jak najwcześniej.
Ta porada dotyczy wszystkich urządzeń podłączonych do sieci, w tym telefonów,
samochodów, urządzeń, osobistych laptopów, maszyn deweloperskich, maszyn
używanych do budowania, serwerów produkcyjnych i obrazów w chmurze.
4337ebf6db5c7cc89e4173803ef3875a
4
Pozostań w bezpiecznym miejscu
 273
Wszystkiego. A jeśli uważasz, że to naprawdę nie ma znaczenia, zapamiętaj,
że największe naruszenia bezpieczeństwa danych w historii (do tej pory) były
spowodowane przez systemy, które nie były aktualizowane.
Nie pozwól, aby zdarzyło się to Tobie.
Antywzorce haseł
Jednym z podstawowych problemów związanych z bezpieczeństwem jest to, że
często dobre zabezpieczenia pozostają w sprzeczności ze zdrowym rozsądkiem lub
powszechnymi praktykami. Na przykład można by pomyśleć, że ścisłe wymagania
dotyczące haseł zwiększą bezpieczeństwo Twojej aplikacji lub witryny. To nieprawda.
W rzeczywistości ścisłe zasady dotyczące haseł faktycznie przyczyniają się do obniżenia
bezpieczeństwa. Oto krótka lista bardzo złych pomysłów, wraz z kilkoma zaleceniami NIST:a
 Nie ograniczaj długości hasła do mniej niż 64 znaków. NIST zaleca 256 jako dobrą
długość maksymalną.
 Nie obcinaj haseł wybranych przez użytkowników.
 Nie wprowadzaj ograniczeń dla znaków specjalnych, takich jak []();&%$# lub /.
Zobacz uwagę na temat tabel Bobby’ego wcześniej w tym podrozdziale. Jeżeli
z powodu znaków specjalnych w haśle dojdzie do złamania zabezpieczeń systemu, będziesz mieć większe problemy. NIST zaleca akceptowanie wszystkich
drukowalnych znaków ASCII, znaku spacji oraz znaków Unicode.
 Nie udostępniaj wskazówek dotyczących haseł nieuwierzytelnionym użytkownikom i nie pytaj o określone rodzaje informacji (na przykład „Jak wabił się Twój
pierwszy zwierzak?”).
 Nie blokuj funkcjonalności wklejania w przeglądarce. Blokowanie funkcjonalności przeglądarek i menedżerów haseł nie sprawi, że system stanie się bardziej
bezpieczny. W rzeczywistości skłania ono użytkowników do tworzenia prostszych, krótszych haseł, które są o wiele łatwiejsze do złamania. Zarówno NIST
w USA, jak i National Cyber Security Centre w Wielkiej Brytanii wymagają, by
weryfikatory haseł pozwalały na stosowanie funkcjonalności wklejania właśnie
z tego powodu.
 Nie nakładaj obowiązku stosowania innych zasad kompozycji haseł. Na przykład nie wymagaj żadnej konkretnej kombinacji dużych i małych liter, znaków
numerycznych lub specjalnych, nie zakazuj powtarzania znaków i tak dalej.
 Nie wymagaj arbitralnie od użytkowników zmieniania ich haseł po pewnym
czasie. Rób to tylko z ważnego powodu (np. jeśli doszło do naruszenia zabezpieczeń).
Należy zachęcać do stosowania długich, losowych haseł, o wysokim stopniu entropii.
Wprowadzanie sztucznych ograniczeń zmniejsza entropię i zachęca do złych nawyków związanych z hasłami, co sprawia, że konta użytkowników stają się podatne na
przejęcia.
a
Publikacja specjalna NIST 800-63B: Digital Identity Guidelines: Authentication and Lifecycle
Management, dostępna za darmo online pod adresem https://doi.org/10.6028/NIST.SP.800-63b.
4337ebf6db5c7cc89e4173803ef3875a
4
274 
Rozdział 7.
Kiedy kodujemy…
Zdrowy rozsądek a kryptografia
Warto pamiętać, że jeśli chodzi o sprawy kryptografii, zdrowy rozsądek może Cię
zawieść. Oto pierwsza i najważniejsza zasada, jeśli chodzi o kryptografię: nigdy nie
wymyślaj jej samodzielnie16. Nawet dla czegoś tak prostego jak hasła, powszechnie stosowane zasady mogą Cię zmylić (patrz ramka „Antywzorce haseł”). W świecie krypto nawet najmniejszy, najbardziej nieistotny z pozoru błąd, może sprowadzić na Twój system ogromne zagrożenie: złamanie Twojego mądrego, nowego
algorytmu szyfrowania domowej roboty prawdopodobnie zajmie ekspertowi zaledwie kilka minut. Algorytmów szyfrowania nie należy wymyślać samodzielnie.
Jak już wcześniej wspomniano, polegaj tylko na wiarygodnych komponentach:
dobrze przetestowanych, właściwie utrzymywanych, często aktualizowanych
bibliotekach i frameworkach (najlepiej open source).
Oprócz prostych zadań szyfrowania, zwróć szczególną uwagę na inne funkcjonalności związane z bezpieczeństwem Twojej witryny lub aplikacji. Weźmy
na przykład uwierzytelnianie.
W celu implementacji własnego mechanizmu logowania z wykorzystaniem
haseł lub uwierzytelniania biometrycznego, trzeba zrozumieć pojęcia skrótów
(ang. hash) i ziaren (ang. salt), znać sposób wykorzystywania przez krakerów
takich technik, jak tablice tęczowe, wiedzieć dlaczego nie należy używać algorytmów MD5 lub SHA1 oraz zdawać sobie sprawę z wielu innych problemów.
A nawet jeśli się to wszystko uda, ostatecznie i tak będziesz odpowiedzialny za
przechowywanie danych i zapewnienie ich bezpieczeństwa, z uwzględnieniem
wszystkich nowych regulacji prawnych i innych przepisów.
Alternatywnie możesz zastosować pragmatyczne podejście i skorzystać z usług
zewnętrznego dostawcy uwierzytelniania, dzięki czemu ktoś inny będzie zmuszony martwić się wszystkimi wymienionymi wcześniej problemami. Może to
być usługa zewnętrzna uruchomiona na komputerach lokalnych lub usługa
w chmurze obliczeniowej. Usługi uwierzytelniania są często dostępne za pośrednictwem poczty elektronicznej, dostawców usług telefonicznych lub mediów
społecznościowych. Należy je odpowiednio dopasować do konkretnej aplikacji.
Trzeba pamiętać, że usługodawcy zewnętrzni przez cały czas dbają o bezpieczeństwo swoich systemów i są w tym lepsi od Ciebie.
Pozostań w bezpiecznym miejscu.
Pokrewne podrozdziały
16

Temat 23., „Projektowanie kontraktowe”.

Temat 24., „Martwe programy nie kłamią”.
Chyba że masz doktorat z kryptografii, a nawet wtedy powinieneś się zastanowić nad
zapewnieniem przeglądu przez współpracowników, szeroko zakrojonych badań z wykorzystaniem programów „bug bounty” oraz budżetu na długoterminowe utrzymywanie.
4337ebf6db5c7cc89e4173803ef3875a
4
Nazewnictwo
44
43

Temat 25., „Programowanie asertywne”.

Temat 38., „Programowanie przez koincydencję”.

Temat 45., „Kopalnia wymagań”.
 275
Nazewnictwo
Początkiem mądrości jest nazywanie rzeczy po imieniu.
Konfucjusz
Czym jest nazwa? Jeśli chodzi o programowanie, odpowiedź brzmi „Wszystkim!”.
Tworzymy nazwy dla aplikacji, podsystemów, modułów, funkcji i zmiennych —
stale tworzymy nowe rzeczy i nadajemy im nazwy. Te nazwy są bardzo, bardzo
ważne, gdyż ujawniają wiele na temat Twoich intencji i przekonań.
Uważamy, że elementy kodu powinny być nazywane w zależności od roli, jaką
w nim odgrywają. Oznacza to, że zawsze, kiedy coś tworzysz, powinieneś się
zatrzymać i pomyśleć „Jaka jest moja motywacja, żeby to stworzyć?”.
To bardzo ważne pytanie, ponieważ odwodzi Cię od myślenia o aktualnie rozwiązywanym problemie i sprawia, że patrzysz na szerszy obraz. Kiedy bierzesz
pod uwagę rolę zmiennej lub funkcji, myślisz o tym, co jest w nich szczególnego
w zakresie tego, co mogą one robić, i z jakimi elementami współdziałają. Często
zdajemy sobie sprawę, że to, co mieliśmy zamiar zrobić, jest bez sensu — wszystko
dlatego, że nie potrafiliśmy wymyślić odpowiedniej nazwy.
Istnieje naukowe uzasadnienie koncepcji nadawania nazw mających głęboki
sens. Okazuje się, że mózg potrafi czytać i rozumieć słowa bardzo szybko:
znacznie szybciej niż potrafi wykonać wiele innych działań. Oznacza to, że
kiedy staramy się coś zrozumieć, słowa mają pewien priorytet. Można to zademonstrować za pomocą efektu Stroopa17.
Spójrzmy na poniższy panel. Zamieszczono na nim listę nazw kolorów lub
odcieni, a każdy z nich jest pokazany w odpowiednim kolorze lub odcieniu. Ale
nazwy i kolory nie zawsze do siebie pasują. Oto jedna część wyzwania — przeczytaj na głos nazwę każdego koloru w panelu18:
17
Studies of Interference in Serial Verbal Reactions [Str35].
18
Mamy dwie wersje tego panelu. Jeden wykorzystuje różne kolory, a drugi odcienie szarości. Jeśli widzisz ten w czerni i bieli i chcesz wersję w kolorze, lub jeśli masz problemy
z rozróżnieniem kolorów i chcesz spróbować wersji w skali szarości, odwiedź stronę
https://pragprog.com/the-pragmatic-programmer/stroop-efekt.
4337ebf6db5c7cc89e4173803ef3875a
4
276 
Rozdział 7.
Kiedy kodujemy…
Teraz powtórz ćwiczenie, ale zamiast odczytywania nazwy powiedz na głos
kolor użyty do narysowania słowa. Prawda, że trudniej? Płynne czytanie jest
łatwe, ale próba rozpoznawania kolorów jest znacznie trudniejsza.
Twój mózg traktuje napisane słowa jako coś, czego należy przestrzegać. Musimy
zadbać o to, aby używane przez nas nazwy były odpowiednie.
Przyjrzyjmy się kilku przykładom:

Uwierzytelniamy osoby, które wchodzą na naszą stronę, gdzie sprzedajemy
biżuterię wykonaną ze starych kart graficznych:
let użytkownik = uwierzytelnij(poświadczenia)
Zmienna ma nazwę użytkownik ponieważ zawsze stosujemy nazwę użytkownik.
Zastanówmy się jednak dlaczego? To nic nie znaczy. A może tak klient lub
nabywca? W ten sposób podczas kodowania otrzymujemy stałe przypomnienia
dotyczące roli osoby oraz co ona dla nas oznacza.

Mamy metodę egzemplarza klasy, stosującą rabat do zamówienia:
public void odejmijProcent(double kwota)
// ...
W tym kodzie na uwagę zasługują dwie rzeczy. Po pierwsze nazwa odejmij
Procent informuje co metoda robi, ale nie dlaczego to robi. Z kolei nazwa
parametru kwota jest w najlepszym przypadku myląca: czy to jest kwota
bezwzględna, czy może procent?
Być może taki nagłówek metody byłby lepszy:
public void zastosujRabat(Procent rabat)
// ...
Teraz nazwa metody czytelnie identyfikuje jej przeznaczenie. Zmieniliśmy
także typ parametru z double na Procent — typ, który zdefiniowaliśmy. Nie
wiem jak Wy, ale gdy chodzi o procenty my nigdy nie wiemy, czy wartość
ma mieścić się w zakresie od 0 do 100, czy od 0,0 do 1,0. Skorzystanie
z własnego typu pozwala udokumentować, czego oczekuje funkcja.

Mamy moduł, który robi ciekawe rzeczy z liczbami należącymi do ciągu
Fibonacciego. Jedną z tych rzeczy jest obliczenie n-tego wyrazu w ciągu.
Zatrzymaj się i pomyśl, jak nazwałbyś tę funkcję.
4337ebf6db5c7cc89e4173803ef3875a
4
Nazewnictwo
 277
Większość osób, które pytaliśmy, nazwałaby ją fib. Wydaje się to rozsądne,
ale należy pamiętać, że funkcja ta zazwyczaj będzie wywoływana w kontekście swojego modułu, więc wywołanie będzie wyglądało Fib.fib(n).
A gdyby tak nazwać ją po prostu dla lub wyraz:
Fib.dla(0) # => 0
Fib.wyraz(20) # => 4181
Gdy nadajemy nazwy elementom kodu, powinniśmy stale poszukiwać sposobów
wyjaśniania, co one oznaczają. Ten akt wyjaśniania doprowadzi nas do lepszego
zrozumienia kodu podczas jego pisania.
Należy jednak pamiętać, że nie wszystkie nazwy muszą być kandydatami do
nagrody literackiej.
Wyjątek, który potwierdza regułę
O ile należy dbać o przejrzystość w kodzie, o tyle nie wolno zapominać o budowaniu
świadomości marki.
Istnieje ugruntowana tradycja, zgodnie z którą projekty i zespoły projektowe powinny
mieć niejasne, „sprytne” nazwy. Nazwy Pokémonów, superbohaterów Marvel, słodkich
ssaków, postaci z „Władcy Pierścieni”.
Należy je stosować dosłownie.
Szanuj kulturę
W informatyce są tylko dwie trudne rzeczy: unieważnianie zbuforowanych
informacji i nadawanie rzeczom nazw.
W większości podręczników do informatyki dla początkujących można znaleźć
zalecenia, by nigdy nie używać pojedynczych liter jako nazw zmiennych, takich
jak i, j lub k19.
Nie zgadzamy się z tym. Oczywiście w pewnym sensie.
W rzeczywistości wszystko zależy od kultury danego języka programowania
lub środowiska. W języku programowania C i, j i k są tradycyjnie używane jako
zmienne inkrementacji pętli, s oznacza łańcuch znaków i tak dalej. Jeśli programujesz w tym środowisku, zapewne przyzwyczaiłeś się do tego, a naruszenie
tej normy byłoby wstrząsem (a więc czymś złym). Z drugiej strony, stosowanie
19
Czy wiesz, dlaczego litera i jest powszechnie stosowana jako zmienna pętli? Tradycja ma
ponad 60 lat, pochodzi z czasów, gdy zmienne od I do N oznaczały w FORTRANIE liczby
całkowite. Z kolei FORTRAN został opracowany na podstawie notacji obowiązujących
w algebrze.
4337ebf6db5c7cc89e4173803ef3875a
4
278 
Rozdział 7.
Kiedy kodujemy…
tej konwencji w innym środowisku, w którym jest ona nieoczekiwana, również
jest złe. Nigdy nie należy zrobić czegoś tak haniebnego, jak w poniższym przykładzie kodu w Clojure, w którym przypisano ciąg do zmiennej i:
(let [i "Witaj świecie "]
(println i))
Społeczności niektórych języków preferują konwencję camelCase, z wielkimi
literami oznaczającymi kolejne słowa, podczas gdy inni wolą konwencję snake_case, w której poszczególne słowa są oddzielone znakami podkreślenia.
Z punktu widzenia samych języków stosowana metoda zapisu nie ma znaczenia, ale dowolne jej stosowanie nie jest dobre. Należy szanować lokalną kulturę.
Niektóre języki pozwalają na stosowanie w nazwach podzbioru znaków Unicode. Warto zorientować się, czego oczekuje społeczność, zanim zastosujemy tak
słodkie nazwy, jak ɹǝsn lub εξέρχεται.
Spójność
Emerson słynie z powiedzenia „Głupia spójność jest hobgoblinem małych
umysłów…”, ale Emerson nie był w zespole programistów.
Każdy projekt ma swoje słownictwo: żargonowe słowa, które mają szczególne
znaczenie dla zespołu. „Porządek” oznacza co innego dla zespołu tworzącego sklep
online, a coś zupełnie innego dla zespołu pracującego nad aplikacją tworzącą
wykresy rodów dla grup religijnych. Ważne, aby wszystkie osoby w zespole
wiedziały, co znaczą te słowa, i aby ich konsekwentnie używały.
Jednym ze sposobów osiągnięcia tego celu jest zachęcenia do komunikacji.
Jeśli w zespole stosowane jest programowanie w parach, a pary się często zmieniają, to żargon rozprzestrzeni się osmotycznie.
Innym sposobem jest stworzenie słowniczka projektu, w którym wymieniono
hasła mające dla zespołu szczególne znaczenie. Jest to nieformalny dokument,
który może być utrzymywany w postaci wiki, ewentualnie w formie kart indeksowych rozwieszonych gdzieś na ścianie.
Po pewnym czasie żargon projektu zacznie żyć własnym życiem. Gdy wszyscy
przyzwyczają się do słownictwa, będzie można używać żargonu jako skrótu,
pozwalającego wyrazić wiele znaczeń w sposób zwięzły i dokładny (dokładnie
w taki sposób działają języki wzorców).
4337ebf6db5c7cc89e4173803ef3875a
4
Nazewnictwo
 279
Zmiany nazw są jeszcze trudniejsze
Istnieją dwa trudne problemy w informatyce: unieważnianie zbuforowanych
elementów, nazywanie rzeczy oraz jeszcze pomyłki „o jeden”20.
Bez względu na to, jak wiele wysiłku poświęcimy na przygotowanie projektu,
rzeczywistość się zmienia. Kod podlega refaktoryzacji, zmieniają się użytkownicy, znaczenia ulegają subtelnym zmianom. Jeśli nie będziemy czujni co do
aktualizacji nazw na bieżąco, szybko możemy osiągnąć koszmar znacznie gorszy od bezsensownych nazw: nazwy mylące. Czy kiedykolwiek słyszałeś, jak
ktoś wyjaśniał niespójności w kodzie mówiąc: „Procedura nazywa się getData,
ale tak naprawdę zapisuje dane do pliku archiwum”?
Zgodnie z tym, co powiedzieliśmy w punkcie „Entropia oprogramowania”, gdy
zauważysz problem, napraw go — tu i teraz. Kiedy zobaczysz nazwę, która
już nie wyraża przeznaczenia nazwanej rzeczy, jest błędna lub niezrozumiała,
napraw to. Z pewnością masz kompletne testy regresji, więc zdołasz dostrzec
wszelkie nieścisłości.
WSKAZÓWKA NR 74
Nadawaj właściwe nazwy i pamiętaj o ich zmienianiu, gdy zajdzie taka
potrzeba.
Jeśli z jakiegoś powodu nie możesz zmienić złej nazwy od razu, masz większy
problem: naruszenie zasad ETC (patrz temat 8., „Istota dobrego projektu”,
mieszczący się w rozdziale 2.). Napraw to naruszenie, a następnie zmień złą
nazwę. Zadbaj o to, aby wprowadzanie zmian było łatwe, a potem często zmieniaj nazwy.
W przeciwnym razie będziesz zmuszony wyjaśniać nowym osobom w zespole,
że funkcja getData naprawdę zapisuje dane do pliku, i będziesz musiał to robić
z kamienną twarzą.
Pokrewne podrozdziały
20

Temat 3., „Entropia oprogramowania”.

Temat 40., „Refaktoryzacja”.

Temat 45., „Kopalnia wymagań”.
https://pl.wikipedia.org/wiki/Off_by_one — przyp. tłum.
4337ebf6db5c7cc89e4173803ef3875a
4
280 
Rozdział 7.
Kiedy kodujemy…
Wyzwania

Gdy znajdziesz funkcję lub metodę, która ma nadmiernie generyczną
nazwę, spróbuj zmienić tę nazwę w taki sposób, aby wyrażała wszystko,
co naprawdę robi funkcja (metoda). Teraz jest ona łatwiejszym celem dla
refaktoryzacji.

W naszych przykładach proponowaliśmy używanie bardziej konkretnych
nazw, takich jak nabywca zamiast bardziej tradycyjnej i generycznej nazwy
użytkownik. Wskaż inne nazwy, którymi posługujesz się zwyczajowo, a które
mogłyby być lepsze.

Czy nazwy w Twoim systemie uwzględniają hasła stosowane przez użytkowników domeny? Jeśli nie, to dlaczego? Czy powoduje to dla zespołu
dysonans poznawczy w stylu efektu Stroopa?

Czy nazwy w Twoim systemie są trudne do zmiany? Co możesz zrobić, aby
naprawić to konkretne wybite okno?
4337ebf6db5c7cc89e4173803ef3875a
4
Rozdział 8.
Przed projektem
Na samym początku projektu musimy określić wymagania. Samo wsłuchiwanie
się w głos użytkowników nie wystarczy — więcej informacji na ten temat można
znaleźć w temacie 45., „Kopalnia wymagań”. Zawarto tam także opis sposobów
unikania powszechnych pułapek i zagrożeń.
Konwencjonalną wiedzą i sposobami zarządzania ograniczeniami zajmiemy
się w temacie 46., „Rozwiązywanie niemożliwych do rozwiązania łamigłówek”.
W zależności od tego, czy pracujemy nad wymaganiami, analizą, kodowaniem, czy testami, możemy spodziewać się różnych problemów. W większości
przypadków wspomniane problemy nie są takie trudne, na jakie początkowo
wyglądają.
A kiedy pojawi się projekt niemożliwy do zrealizowania, możemy sięgnąć do
naszej tajnej broni: jest nią „Praca zespołowa”. Przez „pracę zespołową” nie
rozumiemy jednak współdzielenia ogromnego dokumentu z wymaganiami,
wysyłania e-maili z rozbudowaną listą CC lub ciągłych i niekończących się
spotkań. Rozumiemy przez nią wspólne rozwiązywanie problemów podczas
kodowania. Pokażemy, kogo będziesz potrzebować i od czego należy zacząć.
Mimo że Manifest Agile zaczyna się od zdania „Ludzie i interakcje ponad procesy
i narzędzia”, praktycznie wszystkie projekty „agile” rozpoczynają się od ironicznych dyskusji na temat stosowanego procesu i wykorzystywanych narzędzi.
Ale bez względu na to, jak dobrze projekt jest przemyślany i niezależnie od
zaproponowanych „najlepszych praktyk”, żaden sposób nie zastąpi myślenia.
Nie potrzeba żadnego szczególnego procesu lub narzędzia. Potrzeba raczej „Istoty
zwinności”.
Jeśli uda się wyeliminować te krytyczne problemy przed przystąpieniem do właściwego projektu, będziemy mogli dużo skuteczniej unikać paraliżu analitycznego i sprawnie rozpocząć prace nad projektem.
4337ebf6db5c7cc89e4173803ef3875a
4
282 
45
36
Rozdział 8.
Przed projektem
Kopalnia wymagań
Doskonałość osiąga się nie wtedy, kiedy nie można już nic dodać,
lecz gdy już niczego nie można ująć…
Antoine de St. Exupéry, Ziemia, planeta ludzi, 1939
W wielu książkach i podręcznikach zbieranie wymagań jest prezentowane jako
wczesna faza projektu. Samo słowo „zbieranie” sugeruje istnienie jakiejś grupy
beztroskich analityków, którzy żywią się leżącymi wokół orzeszkami wiedzy przy
pobrzmiewających cicho dźwiękach symfonii Pastoralnej. „Zbieranie” wskazuje
na to, że wymagania już istnieją, a nasza rola sprowadza się tylko do ich odnalezienia, umieszczenia w koszyku i radosnego podążania naprzód.
Rzeczywistość jest nieco inna. Wymagania rzadko są dostępne od ręki. Zwykle
są raczej dobrze ukryte pod warstwami założeń, nieporozumień i decyzji politycznych. Co gorsza, często one w ogóle nie istnieją.
WSKAZÓWKA NR 75
Nikt dokładnie nie wie, czego chce.
Mit wymagań
W pierwszych latach programowania komputery były droższe (biorąc pod uwagę
zamortyzowany koszt godziny pracy) niż ludzie, którzy z nimi pracowali. Oszczędności uzyskiwano dzięki dążeniu do tworzenia programów „od razu dobrze”.
Częścią tego procesu była próba dokładnego wyspecyfikowania, co maszyna ma
dla nas zrobić. Zaczynaliśmy od uzyskania specyfikacji wymagań, przekształcaliśmy ją w dokument projektowy, następnie na schematy blokowe i pseudokod, a na końcu na kod. Jednak przed wprowadzeniem go do komputera
poświęcaliśmy mu czas przy biurku, w celu dokładnego sprawdzenia.
To kosztowało mnóstwo pieniędzy. Ze względu na te koszty próbowaliśmy automatyzować coś dopiero wtedy, gdy dokładnie wiedzieliśmy, czego chcemy. Ponieważ pierwsze komputery były dość ograniczone, zakres problemów, które pozwalały rozwiązać, również był ograniczony: całość problemu można było zrozumieć
jeszcze przed przystąpieniem do realizacji projektu.
Ale to nie jest prawdziwy świat. W prawdziwym świecie panuje bałagan, są
konflikty i wiele niewiadomych. W tym świecie dokładne specyfikacje czegokolwiek są rzadkie, a czasami wręcz w ogóle niemożliwe.
My, programiści, mamy tu do odegrania swoją rolę. Naszym zadaniem jest pomóc
użytkownikom w zrozumieniu tego, czego chcą. Rzeczywiście, to jest prawdopodobnie nasz najcenniejszy atrybut. Warto powtórzyć:
4337ebf6db5c7cc89e4173803ef3875a
4
Kopalnia wymagań
 283
WSKAZÓWKA NR 76
Programiści pomagają użytkownikom zrozumieć, czego użytkownicy
chcą.
Programowanie jako terapia
Nazwijmy ludzi, którzy proszą nas o napisanie oprogramowania, naszymi
klientami.
Typowy klient przychodzi do nas z konkretnym zapotrzebowaniem. Może ono
być strategiczne, ale jest równie prawdopodobne, że będzie to problem taktyczny:
rozwiązanie bieżącego zadania. Potrzeba może dotyczyć zmiany w istniejącym
systemie lub utworzenia czegoś nowego. Potrzeba czasami jest wyrażana w kategoriach biznesowych, a innym razem w kategoriach technicznych.
Błędem, który często popełniają nowicjusze, jest opracowanie rozwiązania na
podstawie sformułowania potrzeby przez klienta.
Z naszego doświadczenia wynika, że to wstępne sformułowanie wymagań nie
jest kompletne. Klient może nie zdawać sobie z tego sprawy, ale to sformułowanie
problemu jest w gruncie rzeczy zaproszeniem do odkrywania wymagań.
Weźmy prosty przykład.
Pracujesz dla wydawcy książek w formie papierowej i elektronicznej. Otrzymałeś
nowe wymaganie:
Dla wszystkich zamówień na kwotę 200 PLN lub więcej dostawa powinna
być darmowa.
Zatrzymaj się na chwilę i wyobraź sobie siebie w sytuacji klienta. Co jest
pierwszą rzeczą, która przychodzi Ci do głowy?
Prawdopodobnie zadajesz sobie następujące pytania:

Czy te 200 PLN zawiera podatek?

Czy te 200 PLN obejmuje bieżące opłaty związane z wysyłką?

Czy te 200 PLN dotyczy książek papierowych, czy też zamówienie może
obejmować również e-booki?

Jaki rodzaj wysyłki ma być oferowany? Priorytetowy? Zwykły?

A co z zamówieniami zagranicznymi?

Jak często limit 200 PLN będzie się w przyszłości zmieniać?
Właśnie to robimy. Kiedy klient podaje wymaganie, które wydaje się proste,
drażnimy go szukając przypadków brzegowych i zadajemy na ich temat pytania.
4337ebf6db5c7cc89e4173803ef3875a
4
284 
Rozdział 8.
Przed projektem
Być może klient już pomyślał o części z tych problemów i założył, że zostaną
uwzględnione w implementacji. Zadanie mu pytań pozwala wydobyć z niego
dodatkowe informacje.
Ale mogą też pojawić się inne pytania, takie, o których klient wcześniej nie
pomyślał. Tutaj robi się ciekawie, a dobry programista powinien nauczyć się
dyplomacji.
Ty: Zastanawialiśmy się nad kwotą 200 PLN. Czy ta kwota obejmuje
opłatę, którą normalnie naliczamy za wysyłkę?
Klient: Oczywiście. To całkowita kwota, którą nasz klient nam płaci.
Ty: To miłe i proste do zrozumienia dla klientów. Myślę, że będzie to dla nich
atrakcyjne rozwiązanie. Ale widzę też ryzyko, że niektórzy pozbawieni skrupułów klienci będą próbowali przechytrzyć system.
Klient: W jaki sposób?
Ty: Załóżmy, że klient chce kupić książkę za 150 PLN, a następnie wybiera
najdroższą opcję wysyłki w ciągu 24 godzin. Załóżmy, że ta wysyłka
kosztuje około 60 PLN, dzięki czemu całe zamówienie wynosi 210 PLN.
W związku z tym, że dostawa dla zamówień powyżej 200 PLN ma być za
darmo, to w gruncie rzeczy klient płaci za książkę wartą 150 PLN, przesłaną
w ciągu 24 godzin, jedynie 150 PLN.
(W tym momencie doświadczony programista powinien się zatrzymać;
przedstawiaj fakty i pozwól klientowi podejmować decyzje).
Klient: Och. To z pewnością nie było to, co miałem na myśli. Takie zamówienia wygenerują nam straty. Jakie są inne opcje?
Taka rozmowa rozpoczyna badanie tematu. Twoja rola polega na interpretowaniu tego, co klient mówi, i uświadamianiu mu implikacji. Jest to proces
zarówno intelektualny, jak i twórczy: myślisz nad problemem klienta i wspólnie tworzycie rozwiązanie, które prawdopodobnie będzie lepsze niż to, które
Ty albo klient znaleźlibyście oddzielnie.
Wymagania to proces
W poprzednim przykładzie programista przeanalizował wymaganie i wyjaśnił
klientowi, jakie są jego konsekwencje. To zainicjowało proces rozpoznawania
tematu. Podczas tego procesu prawdopodobnie zgłosisz więcej opinii na temat
rozwiązań sugerowanych przez klienta. Oto realia procesu zbierania wymagań:
WSKAZÓWKA NR 77
Wymagania poznajemy w pętli sprzężenia zwrotnego.
Twoim zadaniem jest pomóc klientowi zrozumieć konsekwencje formułowanych
przez niego wymagań. Robisz to poprzez generowanie informacji zwrotnych
i pozwalasz wykorzystać te informacje w celu uściślenia wymagań.
4337ebf6db5c7cc89e4173803ef3875a
4
Kopalnia wymagań
 285
W poprzednim przykładzie informacje zwrotne można było łatwo wyrazić słowami. Czasami jednak tak nie jest. A czasem po prostu masz niewystarczającą
wiedzę na temat dziedziny problemu, aby móc wypowiadać się na jej temat.
W takich przypadkach pragmatyczni programiści polegają na szkole pozyskiwania informacji zwrotnych poprzez zadawanie pytania „Czy właśnie to
masz na myśli?”. Tworzymy makiety i prototypy, po czym dajemy je klientowi
do wypróbowania. W idealnej sytuacji programy, które tworzymy, są na tyle
elastyczne, że możemy je zmieniać w trakcie naszych rozmów z klientem, co
pozwala nam reagować na opinie „Nie to miałem na myśli” za pomocą odpowiedzi „A może lepiej tak?”.
Czasami te makiety wyrzuca się zaledwie po kilku godzinach. Jedynym ich
przeznaczeniem jest zgrubne wyrażenie koncepcji.
Realia są jednak takie, że wszystko, co robimy, jest w rzeczywistości jakąś
formą makiety. Nawet po zakończeniu projektu nadal interpretujemy to, czego
chce nasz klient. W rzeczywistości w tym momencie zwykle mamy więcej klientów: mogą być nimi użytkownicy zadający pytania i udzielający odpowiedzi,
pracownicy działu marketingu, a czasami nawet grupy testowych klientów.
W związku z tym pragmatyczny programista postrzega cały projekt jako zadanie gromadzenia wymagań. Dlatego preferujemy krótkie iteracje — takie, które
kończą się bezpośrednim sprzężeniem zwrotnym od klienta. To utrzymuje nas
„na ścieżce” i daje pewność, że jeśli podążymy w złym kierunku, ilość straconego
czasu będzie minimalna.
Spróbuj „wejść w buty” swojego klienta
Istnieje prosta, choć rzadko stosowana, technika pozwalająca na zajrzenie do
głów Twoich klientów: zostań swoim klientem. Piszesz system dla helpdesku?
Poświęć kilka dni na monitorowanie telefonów z doświadczonym pracownikiem
wsparcia technicznego. Automatyzujesz ręczny system kontroli stanów magazynowych? Popracuj przez tydzień w magazynie1.
Główną korzyścią będzie uzyskanie wglądu w sposób, w jaki będzie wykorzystywany gotowy system. Zdziwisz się jednak także, jak bardzo prośba w stylu
„Czy mogę posiedzieć z Tobą przez tydzień, i popatrzyć, jak pracujesz?” pomaga
w budowaniu zaufania i ustanawia podstawy komunikacji z Twoimi klientami.
Pamiętaj tylko, żeby nie przeszkadzać!
1
Tydzień to długo? Nie, zważywszy na to, że chodzi o poznanie procesów, które dla
kierownictwa znaczą coś zupełnie innego niż dla pracowników. Przedstawiciele kierownictwa prezentują pewien obraz tego, jak działają niektóre mechanizmy, ale gdy
przyjrzymy się im w praktyce, okazuje się, że rzeczywistość jest całkiem inna i trzeba
się z nią oswoić.
4337ebf6db5c7cc89e4173803ef3875a
4
286 
Rozdział 8.
Przed projektem
WSKAZÓWKA NR 78
Pracuj z użytkownikiem, aby myśleć jak użytkownik.
Zbieranie informacji zwrotnych jest również czasem, w którym warto zacząć
budować relacje z bazą Twoich klientów — uczyć się ich oczekiwań co do systemu, który budujesz. Więcej informacji na ten temat znajdziesz w temacie 52.,
„Wpraw w zachwyt użytkowników”.
Wymagania a strategia
Wyobraź sobie, że podczas omawiania systemu kadrowego klient mówi: „Dane
pracownika mogą oglądać tylko pracownicy działu personalnego i ich przełożeni”. Czy to zdanie jest naprawdę wymaganiem? Być może dzisiaj tak, ale
czyni strategię biznesową czymś kategorycznym.
Strategia biznesowa? Wymaganie? Jest pomiędzy nimi stosunkowo niewielka
różnica, ale jest ona bardzo istotna, ponieważ ma poważne konsekwencje dla
programistów. Jeżeli wymaganie zostało sformułowane jako „Tylko pracownicy
działu personalnego i ich przełożeni mogą przeglądać dane pracownika”, programista może zakodować jawny test za każdym razem, gdy aplikacja uzyska
dostęp do tych danych. Jeśli jednak wymaganie brzmi „Tylko uprawnieni użytkownicy mogą uzyskać dostęp do danych pracownika”, programista prawdopodobnie zaprojektuje i zaimplementuje jakąś formę systemu kontroli dostępu.
Gdy zmieni się strategia (co stanie się z pewnością), trzeba będzie zaktualizować
tylko metadane dla tego systemu. W istocie zbieranie wymagań w ten sposób
naturalnie prowadzi do systemu, który jest dobrze zaprojektowany do wspierania
metadanych wsparcia.
Oto ogólna zasada:
WSKAZÓWKA NR 79
Strategia to metadane.
Należy zaimplementować przypadek ogólny z informacjami dotyczącymi strategii, jako przykład typu informacji, które system powinien obsługiwać.
Wymagania kontra rzeczywistość
W artykule umieszczonym w magazynie „Wired”, w numerze ze stycznia
19992 roku, producent i muzyk Brian Eno opisał niesamowitą technologię —
kompletną konsoletę. Wykonywała z dźwiękiem wszystko, co można z nim zro2
https://www.wired.com/1999/01/eno/
4337ebf6db5c7cc89e4173803ef3875a
4
Kopalnia wymagań
 287
bić. A jednak, zamiast umożliwić muzykom tworzenie lepszej muzyki lub realizowanie nagrań szybciej lub taniej, wchodziła im w drogę, zakłócając proces
twórczy.
Aby zrozumieć dlaczego, trzeba przyjrzeć się sposobom, w jaki pracują inżynierowie dźwięku. Tworzą harmonię dźwięków intuicyjnie. Przez lata doświadczeń
rozwijają wrodzoną pętlę sprzężenia zwrotnego pomiędzy swoim słuchem,
a palcami — poprzez przesuwanie suwaków, obracanie gałek itd. Jednak interfejs do nowego miksera nie uwzględniał tych zdolności. Zamiast tego zmuszał
użytkowników do stukania na klawiaturze lub klikania myszką. Funkcje udostępniane przez system były obszerne, ale zostały zaimplementowane w nieznany
i egzotyczny sposób. Funkcje potrzebne inżynierom były czasami ukryte za niejasnymi nazwami lub można je było osiągnąć za pośrednictwem nieintuicyjnych
kombinacji podstawowych zadań.
Przykład ten ilustruje również nasze przekonanie, że dobre narzędzia powinny
dostosowywać się do rąk, które ich używają. Skuteczne zbieranie wymagań
powinno to uwzględniać. Właśnie dlatego wcześnie uzyskane opinie, z wykorzystaniem prototypów lub „pocisków smugowych”, pozwalają klientom powiedzieć „Tak, ten system robi to, co chcę, ale nie tak, jak chcę”.
Dokumentowanie wymagań
Uważamy, że najlepszą, a czasami jedyną dokumentacją wymagań, jest działający kod.
Nie znaczy to jednak, że można całkowicie obyć się bez dokumentowania Twojego zrozumienia tego, co chce klient. Oznacza jedynie to, że tworzone dokumenty
nie są produktem: nie służą do tego, żebyś wręczył je klientowi do podpisania.
Są one po prostu „drogowskazami”, które mają ułatwić proces implementacji.
Dokumenty wymagań nie są przeznaczone dla klientów
W przeszłości zarówno Andy, jak i Dave pracowali w projektach, w których
tworzono bardzo szczegółowe wymagania. Te dość obszerne dokumenty były
budowane na podstawie wstępnych dwuminutowych wyjaśnień klientów na
temat tego, czego chcą. W efekcie powstawały grube opracowania pełne wykresów i tabel. Funkcjonalności były opisywane tak szczegółowo, że niemal nie
było miejsca na niejasności w implementacji. Gdyby skorzystać z odpowiednich narzędzi, dokument wymagań mógłby w istocie być końcowym programem.
Tworzenie tego rodzaju dokumentów było błędem z dwóch powodów. Po pierwsze, jak już powiedzieliśmy, klienci zazwyczaj nie wiedzą z góry, czego chcą.
Kiedy więc na podstawie tego, co mówią, rozszerzymy opis do rozmiarów czegoś,
co jest niemal dokumentem prawnym, zbudujemy niezwykle skomplikowany
zamek na piasku.
4337ebf6db5c7cc89e4173803ef3875a
4
288 
Rozdział 8.
Przed projektem
Można by powiedzieć: „Ale przecież prezentujemy dokument klientowi i uzgadniamy go z nim; zbieramy jego opinie”. To jednak prowadzi nas do drugiego
problemu związanego z dokumentami specyfikacji: klient nigdy ich nie czyta.
Klient zatrudnia programistów, ponieważ jest zmotywowany do rozwiązywania
nieco mgliście określonego problemu na wysokim poziomie, ale to programiści
są zainteresowani wszystkimi szczegółami i niuansami. Dokumenty wymagań
są napisane dla programistów; zawierają informacje i subtelności, które są czasem niezrozumiałe, a często nudne dla klienta.
Jeśli przedstawisz 200-stronicowy dokument wymagań klientowi, on prawdopodobnie oceni go pod kątem objętości — rzuci okiem, czy jest wystarczająco
obszerny, aby mógł mieć znaczenie. Być może zapozna się z kilkoma pierwszymi
akapitami (dlatego pierwsze dwa akapity są zawsze zatytułowane „Streszczenie
dla kierownictwa”), a resztę jedynie przejrzy, czasem zatrzymując się, gdy natrafi
na interesujący wykres.
Tu nie chodzi o to, aby zbyć klienta. Ale przekazanie mu obszernego dokumentu
technicznego jest jak przekazanie przeciętnemu programiście kopii „Iliady”
Homera napisanej greką z prośbą o zakodowanie gry wideo na tej podstawie.
Dokumenty wymagań służą do planowania
Nie jesteśmy zatem zwolennikami monolitycznych, tak ciężkich dokumentów
wymagań, że można by nimi ogłuszyć woła. Wiemy jednak, że wymagania
trzeba spisać — po prostu dlatego, żeby programiści w zespole wiedzieli, co mają
robić.
Jaką formę powinien przyjąć taki dokument? Opowiadamy się za dokumentem,
który można zmieścić na rzeczywistej (lub wirtualnej) karcie katalogowej. Te
krótkie opisy są często nazywane historyjkami użytkowników (ang. user stories). Opisują, co powinien robić niewielki fragment aplikacji z punktu widzenia
użytkownika funkcjonalności.
Wymagania napisane w ten sposób mogą być umieszczone na tablicy i przekazywane w celu pokazania zarówno statusu, jak i priorytetów.
Można by pomyśleć, że na pojedynczej karcie katalogowej nie da się zmieścić
informacji niezbędnych do zaimplementowania komponentu aplikacji. To prawda.
Ale to tylko fragment obrazu. Dzięki temu, że sformułowanie wymagań jest
krótkie, zachęcamy programistów do zadawania pytań o wyjaśnienia. W ten
sposób usprawniamy proces sprzężenia zwrotnego pomiędzy klientami a programistami przed tworzeniem każdego fragmentu kodu i podczas jego tworzenia.
Zbyt duża liczba szczegółów
Jednym z największych zagrożeń podczas sporządzania dokumentu z wymaganiami jest dążenie do zapisania zbyt wielu szczegółów. Dobre dokumenty
o wymaganiach zachowują swoją abstrakcyjność. W przypadku wymagań naj-
4337ebf6db5c7cc89e4173803ef3875a
4
Kopalnia wymagań
 289
prostsze stwierdzenia, które możliwie precyzyjnie wyrażają potrzeby biznesowe,
sprawdzają się zdecydowanie najlepiej. Nie chodzi jednak o przesadną ogólnikowość — w naszych wymaganiach musimy uwzględnić niezmienniki semantyczne, a konkretne lub bieżące praktyki należy udokumentować raczej w formie
polityki.
Wymagania to nie architektura. Wymagania to nie projekt ani interfejs użytkownika. Wymagania to opis potrzeby.
Jeszcze tylko jedna malutka funkcja…
Wiele projektów kończy się niepowodzeniem wskutek niekontrolowanego rozszerzania zakresu prac, czyli zjawiska określanego mianem przerostu funkcji.
Mamy tutaj do czynienia z pewnym aspektem syndromu gotowanej żaby
z tematu 4., „Zupa z kamieni i gotowane żaby”. Co możemy zrobić, aby zapobiec wpadnięciu w pułapkę zbyt wielu wymagań?
Odpowiedź (znowu) brzmi: sprzężenie zwrotne. Jeśli pracujemy z klientem iteracyjnie, stale zbierając jego opinie, to klient natychmiast doświadczy wpływu
zjawiska „tylko jednej dodatkowej funkcji”. Zauważy dodatkową historyjkę na
tablicy i pomoże wybrać inną kartę, aby przejść do następnej iteracji. Sprzężenie zwrotne działa w obie strony.
Utrzymywanie glosariusza
W momencie przystąpienia do rozmowy o wymaganiach użytkownicy i eksperci
z danej dziedziny zaczynają używać pewnych terminów, które mają dla nich
specyficzne znaczenie. Mogą na przykład odróżniać klienta od kupującego.
W takim przypadku zamienne stosowanie obu słów w systemie byłoby niewłaściwe.
Warto więc utworzyć i utrzymywać glosariusz na potrzeby projektu, czyli jedno
miejsce, w którym będą definiowane wszystkie terminy i słownictwo używane
w ramach projektu. Wszyscy uczestnicy projektu, od użytkowników końcowych
po pracowników działu wsparcia, powinni posługiwać się tym glosariuszem,
aby zachowywać spójność terminologii. Oznacza to, że glosariusz powinien być
powszechnie dostępny — to jeden z argumentów przemawiających za dokumentacją udostępnianą online.
WSKAZÓWKA NR 80
Należy stosować glosariusz projektu.
Bardzo trudno pomyślnie zakończyć projekt, w którym użytkownicy i programiści stosują odmienne nazwy dla tych samych elementów czy zdarzeń lub —
co gorsza — odwołują się do różnych aspektów, posługując się tą samą nazwą.
4337ebf6db5c7cc89e4173803ef3875a
4
290 
Rozdział 8.
Przed projektem
Pokrewne podrozdziały

Temat 5., „Odpowiednio dobre oprogramowanie”.

Temat 7., „Komunikuj się!”.

Temat 11., „Odwracalność”.

Temat 13., „Prototypy i karteczki samoprzylepne”.

Temat 23., „Projektowanie kontraktowe”.

Temat 43., „Pozostań w bezpiecznym miejscu”.

Temat 44., „Nazewnictwo”.

Temat 46., „Rozwiązywanie niemożliwych do rozwiązania łamigłówek”.

Temat 52., „Wpraw w zachwyt użytkowników”.
Ćwiczenia
33. Które z poniższych zdań zasługują na miano pełnowartościowych wymagań? Spróbuj (jeśli to możliwe) inaczej wyrazić zdania, które nie spełniają
warunków dobrych wymagań.
1. Czas odpowiedzi musi być krótszy niż 500 ms.
2. Okna dialogowe będą miały szary kolor tła.
3. Aplikacja zostanie zorganizowana jako pewna liczba procesów frontowych oraz jeden serwer wewnętrzny.
4. Jeśli użytkownik poda znaki nienumeryczne w polu numerycznym, system odtworzy dźwięk ostrzegawczy i odrzuci wprowadzoną wartość.
5. Kod i dane aplikacje nie mogą zajmować więcej niż 32 MB.
Wyzwania
46
37

Czy używasz oprogramowania, które piszesz? Czy można dobrze zgromadzić
i zrozumieć wymagania bez samodzielnego sprawdzenia oprogramowania?

Wybierz jakiś problem niezwiązany z komputerami, który właśnie musisz
rozwiązać. Opracuj wymagania dla rozwiązania tego problemu (bez użycia
komputera).
Rozwiązywanie niemożliwych
do rozwiązania łamigłówek
Gordios, król Frygii, zawiązał kiedyś węzeł, którego nikt nie potrafił
rozsupłać. Mówiono, że ten, kto rozwiąże zagadkę węzła gordyjskiego,
zdobędzie władzę nad Azją. Zagadkę rozwiązał dopiero Aleksander
Wielki, który przeciął węzeł mieczem. Okazało się, że wystarczyła tylko
inna interpretacja wymagań — to wszystko… i rzeczywiście Aleksander
podbił znaczną część Azji.
4337ebf6db5c7cc89e4173803ef3875a
4
Rozwiązywanie niemożliwych do rozwiązania łamigłówek
 291
Od czasu do czasu odkrywamy gdzieś w środku projektu, że nie potrafimy zrobić
choćby kroku naprzód. Trafiamy na przeszkodę niemożliwą do rozwiązania, jak
nieumiejętność radzenia sobie z jakąś technologią czy fragment kodu, który
okazuje się dużo trudniejszy do napisania, niż początkowo zakładaliśmy. Być
może problem rzeczywiście wydaje się niemożliwy do rozwiązania. Czy jednak
rzeczywiście jest taki trudny, na jaki wygląda?
Przeanalizujmy tradycyjne układanki — wszystkie te kłopotliwe kształty
z drewna, stali lub plastiku, które tak często znajdujemy pod choinką lub na
wyprzedażach niepotrzebnych rzeczy. Zwykle wystarczy przenieść okrągły kształt
w inne miejsce, umieścić klocek w kształcie T w określonym miejscu itp.
Przenosimy więc okrągły kształt lub próbujemy umieścić klocek w kształcie
litery T w określonym miejscu, aby szybko odkryć, że to oczywiste rozwiązanie
nie zdaje egzaminu. Układanek nie można rozwiązywać w ten sposób. To, że
rozwiązanie nie jest oczywiste, nie powstrzymuje ludzi przed próbami wielokrotnego powtarzania tych samych czynności w przekonaniu, że łamigłówka
musi mieć jakieś rozwiązanie.
To oczywiste, że w ten sposób nie można dojść do rozwiązania. Rozwiązanie leży
gdzie indziej. Sekretem układanki jest identyfikacja rzeczywistych (nie wyobrażonych) ograniczeń i znalezienie rozwiązania w ich ramach. Niektóre ograniczenia mają bezwzględny charakter; inne mają raczej postać nieuzasadnionych
uprzedzeń. Ograniczenia bezwzględne muszą być przestrzegane niezależnie od
tego, czy sprawiają wrażenie nielogicznych lub wręcz głupich.
Istnieją też, co udowodnił Aleksander Wielki, pozorne ograniczenia, które nie
mają nic wspólnego z rzeczywistością. Wiele problemów dotyczących oprogramowania ma równie przebiegły charakter.
Stopnie swobody
Popularne wyrażenie „wykraczać myślami poza schematy” (ang. thinking outside
the box) zachęca nas do identyfikacji ograniczeń, które w naszym przypadku nie
znajdują zastosowania, i do ich ignorowania. Przytoczona koncepcja nie jest
jednak w pełni słuszna. Jeśli tym „schematem” jest warunek graniczny, problem
polega raczej na znalezieniu schematu, który co najwyżej może być istotnie
szerszy, niż początkowo sądzimy.
Kluczem do rozwiązania układanki jest zarówno rozpoznanie krępujących nas
ograniczeń, jak i stopni swobody, którymi dysponujemy — dopiero na tej
podstawie możemy znaleźć wyjście z sytuacji. Właśnie dlatego układanki są takie
kłopotliwe; często zbyt pochopnie rezygnujemy z potencjalnych rozwiązań.
Czy potrafimy na przykład połączyć wszystkie punkty na poniższym rysunku
i wrócić do punktu wyjścia, rysując zaledwie trzy proste odcinki (bez odrywania
długopisu od papieru ani dwukrotnego rysowania odcinka łączącego te same
punkty) Math Puzzles & Games [Hol92]?
4337ebf6db5c7cc89e4173803ef3875a
4
292 
Rozdział 8.
Przed projektem
Musimy zmierzyć się ze wszystkimi przyjętymi z góry wyobrażeniami i ocenić,
czy rzeczywiście reprezentują fizyczne ograniczenia.
Problemem nie jest więc to, czy myślimy schematycznie, czy potrafimy wyjść poza
schematy. Kluczem do rozwiązania jest raczej znalezienie schematu — identyfikacja faktycznych ograniczeń.
WSKAZÓWKA NR 81
Nie należy wykraczać myślami poza schemat — należy raczej znaleźć
ten schemat.
W razie napotkania szczególnie kłopotliwego problemu warto zapisać sobie
wszystkie możliwe ścieżki rozwiązania, które na tym etapie potrafimy dostrzec.
Nie należy niczego pomijać, choćby wydawało się zupełnie niepraktyczne lub
wręcz głupie. Dopiero po sporządzeniu tej listy warto ją uważnie przejrzeć i wyjaśnić, dlaczego ta czy inna ścieżka nie doprowadzi do szczęśliwego końca. Czy
na pewno? Potrafimy to udowodnić?
Przypomnijmy sobie historię konia trojańskiego — nowatorskiego rozwiązania
problemu, który wydawał się niemożliwy do rozwiązania. Jak niepostrzeżenie
przerzucić wojsko do dobrze ufortyfikowanego miasta? Jesteśmy pewni, że koncepcja „przez główną bramę” początkowo była odrzucana jako samobójcza.
Warto przypisywać poszczególne ograniczenia do kategorii i nadawać im priorytety. Kiedy stolarze przystępują do projektu, zaczynają od cięcia najdłuższych
fragmentów drewna, by następnie odpowiednio pociąć pozostałe fragmenty.
W ten sam sposób chcemy najpierw identyfikować najbardziej krępujące ograniczenia i umieszczać pozostałe ograniczenia w ich ramach.
Rozwiązanie zagadki czterech punktów łączonych trzema odcinkami można
znaleźć na końcu książki.
Musi istnieć prostszy sposób!
Zdarza się, że pracujemy nad rozwiązaniem problemu, który sprawia wrażenie
dużo trudniejszego, niż jest w rzeczywistości. Często sądzimy, że obraliśmy
niewłaściwą drogę — musi przecież istnieć prostszy sposób osiągnięcia celu!
Być może już teraz nie jesteśmy w stanie dotrzymać harmonogramu lub wręcz
popadamy w rozpacz, tracąc wiarę w możliwość prawidłowego funkcjonowania
systemu, ponieważ jakiś problem wydaje się „niemożliwy do rozwiązania”.
4337ebf6db5c7cc89e4173803ef3875a
4
Rozwiązywanie niemożliwych do rozwiązania łamigłówek
 293
W takich przypadkach powinniśmy zatrzymać się na chwilę i przez jakiś czas
zająć się czymś innym. Idź na spacer z psem, prześpij się z problemem.
Twój świadomy umysł zdaje sobie sprawę z problemu, ale jest w istocie bardzo
głupi (bez obrazy). Nadchodzi więc czas, aby Twojemu prawdziwemu mózgowi,
tej niezwykłej asocjacyjnej sieci neuronowej, która działa poniżej Twojej świadomości, dać trochę przestrzeni. Będziesz zaskoczony, jak często odpowiedź
pojawi się w Twojej głowie, gdy celowo odwrócisz swoją uwagę od problemu.
Jeśli to brzmi dla Ciebie zbyt mistycznie, nie przejmuj się. Nie ma w tym niczego
mistycznego. W artykule zamieszczonym w witrynie Psychology Today3 można
znaleźć stwierdzenie: „Mówiąc krótko — osoby, które potrafiły odwrócić swoją
uwagę od problemu, lepiej radziły sobie z rozwiązaniem niż osoby, które usiłowały
rozwiązywać problem świadomie”.
Jeśli nadal nie jesteś skłonny, aby odłożyć problem na jakiś czas, następną
w kolejności najlepszą rzeczą jest znalezienie kogoś, kto potrafi Ci wytłumaczyć, na czym problem polega. Często odwrócenie uwagi od rozwiązania polegające na porozmawianiu o problemie doprowadzi Cię do oświecenia.
Podczas rozmowy postaraj się znaleźć odpowiedzi na następujące pytania:

Dlaczego rozwiązujesz problem?

Jakie korzyści wynikają z jego rozwiązania?

Czy rozwiązywany problem ma związek z przypadkami brzegowymi? Czy
potrafisz je wyeliminować?

Czy istnieje prostszy, podobny problem, który potrafisz rozwiązać?
To kolejny praktyczny przykład zastosowania techniki „rozmowy z gumową
kaczką”.
Szczęście sprzyja przygotowanym umysłom
Louis Pasteur miał powiedzieć:
W dziedzinie obserwacji szczęście sprzyja umysłom przygotowanym.
To twierdzenie jest prawdziwe również w odniesieniu do rozwiązywania problemów. Aby móc przeżywać chwile „eureka!”, Twoja podświadomość musi mieć
dużo surowca — wcześniejszych doświadczeń, które mogą przyczynić się do
znalezienia odpowiedzi.
Świetnym sposobem dostarczenia pożywki dla Twojego mózgu jest przekazywanie mu podczas wykonywania codziennych zadań opinii na temat tego,
co działa, a co nie działa. Świetnym sposobem, aby to robić, jest prowadzenie
3
https://www.psychologytoday.com/us/blog/your-brain-work/201209/stop-tryingsolve-problems
4337ebf6db5c7cc89e4173803ef3875a
4
294 
Rozdział 8.
Przed projektem
dzienników inżynierskich (zobacz temat 22., „Dzienniki inżynierskie”). Zawsze
także należy pamiętać o poradzie umieszczonej na okładce książki Autostopem
przez galaktykę: NIE PANIKUJ!
Pokrewne podrozdziały

Temat 5., „Odpowiednio dobre oprogramowanie”.

Temat 37., „Słuchaj swojego jaszczurczego mózgu”.

Temat 45., „Kopalnia wymagań”.

Andy napisał całą książkę na temat rozwiązywania problemów: Pragmatic
Thinking and Learning: Refactor Your Wetware [Hun08].
Wyzwania
47
38

Spróbuj z dystansu spojrzeć na dowolny trudny problem, który właśnie
próbujesz rozwiązać. Czy możesz po prostu przeciąć ten węzeł gordyjski?
Zadaj sobie wymienione powyżej pytania, w szczególności: „Czy w ogóle
musisz to robić?”.

Czy zbiór ograniczeń był znany w momencie podpisywania kontraktu na
bieżący projekt? Czy zdefiniowane wówczas ograniczenia wciąż są aktualne
i czy ich interpretacja zachowała swoją wartość?
Praca zespołowa
Nigdy nie spotkałem człowieka, któremu chciałoby się czytać 17 000
stron dokumentacji, a gdybym go znalazł, to zabiłbym go, aby
wyeliminować tego osobnika z puli genów.
Joseph Costello, Prezes firmy Cadence
To był jeden z tych „niemożliwych” projektów — słyszałeś o nich — takich, które
sprawiają wrażenie zarówno radosnych, jak i przerażających. „Starożytny”
system zbliżał się do końca swoich dni — sprzęt był fizycznie wycofywany.
Trzeba było więc wykonać całkiem nowy system, taki, który dokładnie pasowałby
do (często nieudokumentowanych) zachowań. Przez system miało przechodzić
wiele setek milionów dolarów pieniędzy innych ludzi, a termin od początku
projektu do jego wdrożenia miał nie przekroczyć kilku miesięcy.
Wtedy właśnie Andy i Dave spotkali się po raz pierwszy. Niemożliwy projekt
o śmiesznym terminie wykonania. Była tylko jedna rzecz, dzięki której projekt
mógł przeistoczyć się w wielki sukces. Ekspert, który zarządzał tym systemem
od lat, siedział w swoim biurze, po drugiej stronie korytarza naszego pokoju
programistów o wielkości kantorka na miotły. Był stale gotowy do odpowiedzi
na pytania, udzielania wyjaśnień, pomocy w podejmowaniu decyzji i oglądania
prezentacji.
4337ebf6db5c7cc89e4173803ef3875a
4
Praca zespołowa
 295
W tej książce zalecamy ścisłą współpracę z użytkownikami — powinni oni
być częścią zespołu. W tym pierwszym projekcie stosowaliśmy technikę, którą
teraz można by nazwać programowaniem w parach lub programowaniem
mob: jedna osoba wpisuje kod, a co najmniej jeden spośród innych członków
zespołu komentuje problemy, rozmyśla nad nimi i wspólnie z autorem kodu
opracowuje rozwiązania. To doskonały sposób współpracy, który eliminuje
potrzebę niekończących się spotkań, notatek i zatłoczonej dokumentacji technicznej ocenianej nie pod kątem przydatności, ale objętości.
To jest to, co naprawdę uważamy za „wspólną pracę”: nie samo zadawanie pytań,
prowadzenie dyskusji i robienie notatek, ale zadawanie pytań i dyskutowanie
podczas kodowania.
Prawo Conwaya
W 1967 roku Melvin Conway w książce How do Committees Invent? [Con68] przedstawił pomysł, który jest dziś znany jako prawo Conwaya:
Organizacje, które projektują systemy, ograniczają się do tworzenia projektów,
które są kopiami struktur komunikacyjnych tych organizacji.
Oznacza to, że struktury i ścieżki komunikacyjne zespołu i organizacji zostaną odzwierciedlone w aplikacji, witrynie lub tworzonym produkcie. Słuszność tego stwierdzenia
wykazano w licznych badaniach. Byliśmy bezpośrednimi świadkami potwierdzenia
słuszności tej koncepcji niezliczoną ilość razy — na przykład w zespołach, w których
nikt z nikim nie rozmawiał, co skutkowało powstawaniem systemów typu silos (nazywanych również „systemami rur piecowych” — ang. stove pipe). Obserwowaliśmy to
zjawisko także w zespołach, które były dzielone na dwie części — np. klient – serwer
lub frontend – backend.
Badania potwierdzają również słuszność odwrotnej zasady: strukturę zespołu można
świadomie ustawić pod kątem tego, jak ma wyglądać kod. Na przykład zespoły
rozproszone geograficznie wykazują tendencję do tworzenia bardziej modułowego,
rozproszonego oprogramowania.
Ale co najważniejsze, zespoły programistów, które zawierają użytkowników, tworzą
oprogramowanie, które wyraźnie odzwierciedla zaangażowanie tychże, a w produktach zespołów, które rezygnują z udziału użytkowników, ten brak zaangażowania
także jest widoczny.
Programowanie w parach
Programowanie w parach jest jedną z praktyk techniki eXtreme Programming, która stała się popularna poza metodologią XP. Technika ta polega na tym,
że podczas programowania jeden programista operuje klawiaturą, a drugi siedzi
obok. Oboje pracują nad problemem wspólnie i mogą się zmieniać przy klawiaturze, jeśli jest taka potrzeba.
4337ebf6db5c7cc89e4173803ef3875a
4
296 
Rozdział 8.
Przed projektem
Programowanie w parach przynosi wiele korzyści. Różne osoby mają różną
wiedzę i doświadczenia, stosują różne techniki rozwiązywania problemów i różne
podejścia, a także różne poziomy koncentracji uwagi w odniesieniu do danego
problemu. Programista operujący klawiaturą musi koncentrować się na niskopoziomowych szczegółach składni i stylu kodowania, a drugi programista ma
swobodę potrzebną do koncentrowania się na kwestiach wyższego poziomu
i szerszym zakresie. Mimo że ta różnica może wydawać się subtelna, należy
pamiętać, że ludzie mają ograniczone pasmo mózgu. Koncentrowanie się na
wpisywaniu ezoterycznych słów i symboli, które zaakceptuje kompilator, zajmuje znaczącą część mocy obliczeniowej naszego mózgu. Posiadanie w dyspozycji mózgu drugiego programisty podczas wykonywania zadania znacznie zwiększa nasze możliwości.
Nieodłączna „presja” związana z obecnością drugiej osoby pomaga przezwyciężyć chwile słabości i wyeliminować złe nawyki w rodzaju nazywania zmiennych foo, a także uniknąć innych podobnych problemów. Gdy ktoś nas uważnie
obserwuje, jesteśmy mniej skłonni do stosowania potencjalnie kłopotliwych
skrótów, co również prowadzi do wyższej jakości oprogramowania.
Programowanie mob
Skoro dwie głowy są lepsze niż jedna, to może jeszcze lepszy byłby układ, w którym kilka osób wspólnie pracuje nad tym samym problemem, a tylko jedna
z nich operuje klawiaturą.
Programowanie mob, pomimo nazwy, nie polega na korzystaniu z pochodni
i wideł. To rozszerzenie programowania parami, które obejmuje zaangażowanie więcej niż dwóch programistów. Zwolennicy tej techniki podkreślają jej
wielkie zalety podczas rozwiązywania trudnych problemów. W zespole mob
mogą się również znaleźć osoby, których zwykle nie traktuje się jako członków
zespołu projektowego, w tym użytkownicy, sponsorzy projektu i testerzy. Podczas realizacji naszego pierwszego wspólnego „niemożliwego” projektu często
zdarzało się, że jeden z nas operował klawiaturą, podczas gdy pozostali omawiali
problem z naszym ekspertem biznesowym. Była to niewielka grupa mob złożona
z trzech osób.
Programowanie mob można interpretować jako stosowanie ścisłej współpracy
z kodowaniem na żywo.
Co powinienem zrobić?
Jeśli obecnie programujesz wyłącznie solo, być może powinieneś spróbować
programowania parami. Poświęć na to co najmniej dwa tygodnie — na początek w sesjach po kilka godzin — ponieważ początkowo będziesz czuć się dziwnie.
W celu przeprowadzenia burzy mózgów dla nowych pomysłów lub zdiagnozowania kłopotliwych kwestii, spróbuj zastosować sesję programowania mob.
4337ebf6db5c7cc89e4173803ef3875a
4
Istota zwinności
 297
Jeśli już praktykujesz programowanie w parach lub programowanie mob, zastanów się, z kim to robisz. Czy są to tylko programiści, czy też grupę tworzą
członkowie rozszerzonego zespołu — użytkownicy, testerzy, sponsorzy?
Podobnie jak w przypadku każdego rodzaju współpracy, trzeba uwzględnić
zarówno aspekt ludzki, jaki i techniczny. Oto kilka wskazówek na początek:

Buduj kod, a nie swoje ego. Nie chodzi tu o to, kto jest najbystrzejszy;
wszyscy mamy chwile dobre i złe.

Zacznij od pracy w niewielkiej grupie. Grupy mob powinny mieć 4 – 5 osób,
możesz też na początek wyznaczyć tylko kilka par pracujących w krótkich
sesjach.

Krytykuj kod, a nie osoby. Uwaga „Spójrz na ten blok kodu” brzmi znacznie lepiej niż „Mylisz się”.

Słuchaj i staraj się zrozumieć punkt widzenia innych. Różny nie znaczy zły.

Przeprowadzaj częste retrospektywy, aby dążyć do ciągłej poprawy.
Kodowanie we wspólnym biurze lub zdalnie, samotnie, w parach lub w grupach mob, to skuteczne sposoby współpracy w celu rozwiązywania problemów.
Jeśli Ty i Twój zespół stosowaliście tylko jeden z tych sposobów, spróbujcie
poeksperymentować z innym stylem. Nie należy jednak korzystać z naiwnego
podejścia: dla każdego stylu programowania istnieją zasady, sugestie i wytyczne.
Na przykład w przypadku programowania mob operator klawiatury powinien
zmieniać się co 5 – 10 minut.
Czytaj literaturę i studiuj — korzystaj zarówno z podręczników, jak i z raportów ze zdobytych doświadczeń. Staraj się wyszukiwać zalety i eliminować możliwe pułapki. Możesz zacząć od zakodowania prostego zadania zamiast podejmowania od razu próby rozwiązywania w ten sposób najtrudniejszych problemów
związanych z tworzeniem kodu produkcyjnego.
Niezależnie jednak od przyjętego sposobu, warto posłuchać następującej rady:
WSKAZÓWKA NR 82
Nie staraj się tworzyć kodu w samotności.
48
39
Istota zwinności
Ciągle używasz tego słowa. Nie sądzę, że znaczy ono to, co myślisz,
że znaczy.
Inigo Montoya, Narzeczona dla księcia
4337ebf6db5c7cc89e4173803ef3875a
4
298 
Rozdział 8.
Przed projektem
Słowo zwinny (ang. agile) jest przymiotnikiem: określa sposób, w jaki coś robimy.
Możesz być zwinnym programistą. Możesz być członkiem zespołu, który stosuje
zwinne praktyki, zespołu, który reaguje na zmiany i błędy za pomocą zwinnego podejścia. Zwinność to Twój styl, a nie Ty.
WSKAZÓWKA NR 83
Zwinny nie jest rzeczownikiem. Zwinny to sposób, w jaki rozwiązujesz
problemy.
Te słowa piszemy prawie 20 lat po ogłoszeniu Manifestu Agile4. Przez te lata
spotykamy wielu programistów, którzy z powodzeniem stosują opisywane
w nim wartości. Widzieliśmy wiele fantastycznych zespołów, które znalazły
sposób, aby przyjąć te wartości i stosować je jako wytyczne do kierowania tym,
co robią i jak zmieniają to, co robią.
Ale widzimy też inną stronę zwinności. Spotykaliśmy zespoły i firmy skłonne
do stosowania rozwiązań „z półki” Agile-in-a-Box. Spotykaliśmy także wielu
konsultantów i liczne firmy; ci pierwsi pomyślnie sprzedawali drugim to, czego
te firmy chciały. Spotykaliśmy przedsiębiorstwa stosujące więcej warstw zarządzania, bardziej formalne raportowanie, zatrudniające bardziej wyspecjalizowanych programistów i posiadające więcej fantazyjnych nazw stanowisk, które
oznaczały po prostu „osoby z notatnikiem i stoperem”5.
Uważamy, że wiele osób utraciło prawdziwe znaczenie słowa „zwinność” i chcielibyśmy spowodować powrót do podstaw.
Przypomnijmy wartości z manifestu:
Odkrywamy lepsze sposoby tworzenia oprogramowania, wykonując je i pomagając to robić innym. Dzięki tej pracy odkryliśmy następujące wartości:

Ludzie i interakcje ponad procesy i narzędzia.

Działające oprogramowanie ponad kompleksową dokumentację.

Współpraca z klientami ponad negocjacje kontraktów.

Reagowanie na zmiany ponad podążanie za planem.
Choć zdajemy sobie sprawę, że istnieje wartość w elementach wymienionych po prawej stronie, bardziej cenimy sobie pozycje wymienione po stronie lewej.
Każdy, kto sprzedaje Wam coś, co zwiększa ważność rzeczy po prawej stronie
ponad rzeczy po stronie lewej, wyraźnie nie ceni tych samych elementów,
które cenimy my i inni twórcy manifestu.
4
https://agilemanifesto.org
5
Więcej informacji o tym, jak złe może być to podejście, można znaleźć w książce The
Tyranny of Metrics [Mul18].
4337ebf6db5c7cc89e4173803ef3875a
4
Istota zwinności
 299
A każdy, kto sprzedaje Wam „rozwiązanie w pudełku”, wyraźnie nie przeczytał
wstępnego zdania. Wartości są motywowane i inspirowane przez ciągły akt
odkrywania lepszych sposobów wytwarzania oprogramowania. Ten dokument
nie jest statyczny. Są to raczej propozycje dla procesu twórczego.
Nie istnieje coś takiego, jak proces Agile
Gdy ktoś mówi „zrób to, a będziesz Agile”, jest w błędzie. Z definicji.
To dlatego, że zwinność — zarówno w świecie fizycznym, jak i w branży rozwoju
oprogramowania — polega na reagowaniu na zmiany, odpowiadaniu na niewiadome, które napotykasz po dostarczeniu produktu. Biegnąca gazela nie porusza się po linii prostej. Gimnastyk robi setki korekt na sekundę, ponieważ reaguje
na zmiany w swoim otoczeniu i drobne błędy w ułożeniu swoich stóp.
Tak samo jest z zespołami i pojedynczymi programistami. Podczas tworzenia
oprogramowania nie ma jednego planu, którego można by przestrzegać. Mówią
o tym trzy spośród czterech wartości wymienionych powyżej. Wszystkie one
dotyczą gromadzenia opinii i reagowania na nie.
Wartości nie mówią Ci, co masz robić. Wskazują, na co zwracać uwagę, kiedy
sam decydujesz, co zrobisz.
Te decyzje są zawsze podejmowane w kontekście: zależą od tego, kim jesteś,
jaki jest charakter Twojego zespołu, Twojej aplikacji, Twojego oprzyrządowania,
Twojej firmy, klienta, otoczenia zewnętrznego. To niezwykle obszerna lista
czynników, z których niektóre są ważne, a inne trywialne. Żaden ustalony,
statyczny plan nie jest w stanie sprostać tym niepewnościom.
Co powinienem robić?
Nikt nie może Ci powiedzieć, co masz robić. Uważamy jednak, że możemy
powiedzieć coś o duchu, w jakim powinieneś to robić. Wszystko sprowadza się
do sposobu, w jaki radzisz sobie z niepewnością. Manifest sugeruje, że należy
to robić poprzez gromadzenie opinii i reagowanie na nie. Oto nasza recepta na
pracę w zgodzie z duchem Agile:
1. Oceń, w jakim miejscu jesteś.
2. Zrób jak najmniejszy znaczący krok w kierunku, w którym chcesz podążać.
3. Oceń dokąd dotarłeś i napraw wszystko, co po drodze zepsułeś.
Powtarzaj te czynności tak długo, aż wykonasz zadanie. Wykonuj je rekurencyjnie, na każdym poziomie wszystkiego, co robisz.
Czasami nawet najbardziej błaha z pozoru decyzja staje się ważna, gdy zbierzesz informacje zwrotne.
4337ebf6db5c7cc89e4173803ef3875a
4
300 
Rozdział 8.
Przed projektem
Teraz mój kod musi uzyskać dostęp do właściciela konta.
let user = accountOwner(accountID);
Hmmm... user to bezużyteczna nazwa. Zastosuję nazwę owner.
let owner = accountOwner(accountID);
Ale teraz kod wygląda na nieco nadmiarowy. Co ja właściwie próbuję tutaj
zrobić? W historyjce użytkownika jest stwierdzenie, że wysyłam e-mail
do tej osoby, więc muszę znaleźć jej adres e-mail. Może wcale nie potrzebuję wszystkich informacji o właścicielu konta?
let email = emailOfAccountOwner(accountID);
Dzięki zastosowaniu pętli sprzężenia zwrotnego na naprawdę niskim poziomie
(nazewnictwa zmiennej) w istocie poprawiliśmy projekt całego systemu — zredukowaliśmy sprzężenia pomiędzy tym kodem a kodem obsługującym konta.
Pętla sprzężenia zwrotnego ma również zastosowanie na najwyższym poziomie
projektu. Nasze najbardziej udane prace wykonywaliśmy wtedy, kiedy zaczynaliśmy od analizy wymagań klienta — przeanalizowaliśmy jeden krok i zdaliśmy
sobie sprawę z tego, że to, co mamy zamiar zrobić, nie było konieczne, że
najlepsze rozwiązanie nawet nie dotyczyło oprogramowania.
Pętla sprzężenia zwrotnego wykracza poza zakres jednego projektu. Zespoły
powinny ją stosować do przeglądu swoich procesów i ich skuteczności. Zespół,
który nie eksperymentuje ciągle ze swoim procesem, nie jest zespołem zwinnym.
To steruje projektem
W temacie 8., „Istota dobrego projektu” stwierdziliśmy, że miarą jakości projektu jest łatwość wprowadzania w nim zmian: dobry projekt tworzy coś, co jest
łatwiejsze do zmiany niż zły projekt.
Niniejszy opis zwinności wyjaśnia, dlaczego tak jest.
Wprowadzasz zmianę i odkrywasz, że Ci się ona nie podoba. W kroku 3. na
naszej liście powiedzieliśmy, że musimy być w stanie ustalić, co zepsuliśmy.
Aby nasza pętla sprzężenia zwrotnego była wydajna, ta poprawka musi być jak
najbardziej bezbolesna. Jeśli taka nie będzie, to będziemy podatni na pokusę,
aby jej nie wprowadzać i pozostawić błąd bez naprawy. Mówiliśmy o tym efekcie
w temacie 3., „Entropia oprogramowania”. Aby cała zwinność miała sens,
musimy tworzyć dobre projekty, bo w dobrym projekcie można łatwo wprowadzać zmiany. A jeśli projekt jest łatwy do zmiany, bez wahania możemy go
dostosować na każdym jego etapie.
Na tym polega zwinność.
4337ebf6db5c7cc89e4173803ef3875a
4
Istota zwinności
 301
Pokrewne podrozdziały

Temat 27., „Nie prześcigaj swoich świateł”.

Temat 40., „Refaktoryzacja”.

Temat 50., „Nie próbuj przecinać kokosów”.
Wyzwania

Prosta pętla sprzężenia zwrotnego nie dotyczy wyłącznie oprogramowania.
Pomyśl o innych decyzjach, które niedawno podjąłeś. Czy dowolną z nich
mógłbyś poprawić dzięki pomyśleniu o tym, w jaki sposób można by cofnąć,
to co zrobiłeś, gdyby okazało się, że posunąłeś się w niewłaściwym kierunku? Czy potrafisz znaleźć sposoby poprawy tego, co robisz, poprzez
gromadzenie opinii i reagowanie na nie?
4337ebf6db5c7cc89e4173803ef3875a
4
302 
Rozdział 8.
Przed projektem
4337ebf6db5c7cc89e4173803ef3875a
4
Rozdział 9.
Pragmatyczne projekty
W momencie przystąpienia do projektu musimy zapomnieć o problemach związanych z filozofią pracy poszczególnych członków zespołu i sposobem kodowania
na rzecz kwestii dotyczących całego przedsięwzięcia. Nie chcemy wchodzić
w szczegóły zarządzania projektami — ograniczymy się do prezentacji kilku
krytycznych obszarów, które mogą zdecydować o powodzeniu bądź porażce
projektu.
Jeśli tylko w realizację projektu jest zaangażowanych więcej niż jedna osoba,
musimy uzgodnić podstawowe reguły współpracy i właściwie podzielić odpowiedzialność za poszczególne aspekty projektu. W podrozdziale „Pragmatyczne
zespoły” pokażemy, jak osiągnąć te cele, zachowując pragmatyczną filozofię.
Celem metodologii wytwarzania oprogramowania jest wspomaganie pracy zespołowej. Czy Ty i Twój zespół robicie to, co się dla Was dobrze sprawdza, czy też
inwestujecie jedynie w trywialne, płytkie artefakty i nie uzyskujecie rzeczywistych korzyści, na które zasługujecie? Przekonamy się, dlaczego nie warto przecinać kokosów i zdradzimy prawdziwy sekret sukcesu.
Oczywiście nic, co powiedzieliśmy, nie byłoby istotne, gdybyśmy nie potrafili
dostarczyć oprogramowania w sposób konsekwentny i rzetelny. To podstawa
magicznego trio kontroli wersji, testowania i automatyzacji: startera dla pragmatycznego programisty.
Sukces jest rzeczą względną — zależy od oceny sponsora projektu. Liczy się
przede wszystkim odbiór sukcesu; w podrozdziale „Wpraw w zachwyt użytkowników” zaproponujemy sposoby wprawiania w zachwyt sponsorów projektu.
Ostatnia wskazówka zawarta w tej książce będzie wprost wynikała z wcześniejszych zaleceń. W podrozdziale „Duma i uprzedzenie” spróbujemy zachęcić
czytelnika do brania odpowiedzialności za swoją pracę i do odczuwania dumy
ze swoich osiągnięć.
4337ebf6db5c7cc89e4173803ef3875a
4
304 
49
36
Rozdział 9.
Pragmatyczne projekty
Pragmatyczne zespoły
W grupie L Stoffel nadzoruje pracę sześciu doskonałych programistów
— to dla menedżera wyzwanie porównywalne do pilnowania grupy
kotów.
„The Washington Post Magazine”, 9 czerwca 1985
Nawet w 1985 roku żart o pilnowaniu grupy kotów był przestarzały. W czasie,
gdy powstawało pierwsze wydanie tej książki, na przełomie XX i XXI wieku, był
pozytywnie starożytny. Pomimo to przetrwał, ponieważ tkwi w nim ziarno prawdy.
Programiści trochę przypominają koty: inteligentni, mający silną wolę, uparci,
niezależni i często czczeni przez otoczenie.
Do tej pory koncentrowaliśmy się na pragmatycznych technikach, które ułatwiają poszczególnym członkom zespołu stawanie się lepszymi programistami.
Czy proponowane metody sprawdzają się także w przypadku zespołów złożonych
ze zmotywowanych, niezależnych osób? Odpowiedź jest oczywista: tak! Bycie
pragmatycznym programistą ma, oczywiście, swoje zalety, ale wszystkie te
korzyści są wielokrotnie większe, jeśli taki programista dodatkowo pracuje
w pragmatycznym zespole.
Zespół, naszym zdaniem, jest niewielkim, zwykle stabilnym, odrębnym podmiotem. Pięćdziesiąt osób to nie jest zespół, sto osób to jest tłum1. Zespoły, których
członkowie stale realizują różne zadania i się wzajemnie nie znają, nie są zespołami. Są oni jedynie grupą osób, które tymczasowo stoją na tym samym przystanku autobusowym podczas deszczu.
Pragmatyczny zespół jest niewielki. Składa się maksymalnie z 10 – 12 osób.
Jego członkowie rzadko się zmieniają. Wszyscy dobrze się znają, ufają sobie
i wzajemnie od siebie zależą.
WSKAZÓWKA NR 84
Utrzymuj niewielkie, stabilne zespoły.
W tym podrozdziale krótko omówimy, jak te pragmatyczne techniki można stosować dla zespołów jako całości. Proponowane rozwiązania to zaledwie początek.
Kiedy już zbierzemy grupę pragmatycznych programistów i zapewnimy im
środowisko gwarantujące odpowiedni potencjał, szybko sami zaczną rozwijać
i doskonalić dynamikę zespołu w sposób najlepiej pasujący do sytuacji.
Spróbujmy z nieco innej strony (w kontekście zespołów) spojrzeć na zagadnienia
przedstawione we wcześniejszych rozdziałach.
1
W miarę wzrostu liczby członków zespołu liczba ścieżek komunikacji rośnie w tempie
O(n2), gdzie n oznacza liczbę członków zespołu. W przypadku większych zespołów,
komunikacja staje się nieskuteczna.
4337ebf6db5c7cc89e4173803ef3875a
4
Pragmatyczne zespoły
 305
Żadnych wybitych okien
Jakość jest problemem całego zespołu. Nawet najlepszy programista zatrudniony w zespole, który nie wykazuje większego zainteresowania projektem, najprawdopodobniej straci entuzjazm potrzebny do badania i rozwiązywania problemów. Sytuację może dodatkowo pogarszać niechęć zespołu do pomysłów
programisty, aby poświęcać czas eliminowaniu usterek.
Zespoły jako całość nie powinny tolerować wybitych szyb — niewielkich, z pozoru
nieważnych niedoskonałości, których zwykle nikt nie eliminuje. Zespół musi
brać odpowiedzialność za jakość swojego produktu, wspierając programistów,
którzy rozumieją filozofię naprawiania wybitych szyb (opisaną w podrozdziale
„Entropia oprogramowania” w rozdziale 1.), i zachęcając do tego samego tych
swoich członków, którzy jeszcze tego nie odkryli.
Niektóre metodyki pracy zespołowej sugerują wskazanie jednego pracownika,
który zajmuje się tylko jakością — to na jego barki spada cała odpowiedzialność
za jakość końcowego produktu. Pomysł jest kuriozalny — warunkiem koniecznym zapewnienia właściwej jakości jest zaangażowanie wszystkich członków
zespołu. Jakość powinna być wbudowana, a nie przykręcona śrubami.
Ugotowane żaby
W podrozdziale „Zupa z kamieni i gotowane żaby” w rozdziale 1. wspomnieliśmy
o marnym losie żaby wrzuconej do garnka z wodą. Żaba nie zauważa stopniowej zmiany w swoim środowisku, co ostatecznie prowadzi do jej ugotowania.
To samo dotyczy programistów, którzy nie wykazują dostatecznej czujności.
W gorącej atmosferze towarzyszącej projektowi często trudno obserwować całe
środowisko.
Okazuje się, że ryzyko „ugotowania” całego zespołu jest jeszcze większe. Ludzie
odruchowo przyjmują, że kto inny zajmie się problemem lub że lider zespołu
zapewne zatwierdził zmianę, której domaga się jakiś użytkownik. Nawet najlepsze intencje członków zespołu mogą nie wystarczyć do odkrywania poważnych zmian w projektach.
Musimy z tym walczyć. Musimy upewnić się, że każdy aktywnie monitoruje
środowisko pod kątem ewentualnych zmian. Należy zachować czujność i być
wyczulonym na rozszerzanie się zakresu prac, skracanie terminów, dodatkowe
funkcje, nowe środowiska — słowem, wszystko, o czym nie było mowy w początkowych ustaleniach. Należy też mierzyć nowe wymagania2. Zespół nie musi
odruchowo odrzucać wszystkich zmian — musimy jednak mieć świadomość
tego, że te zmiany mają miejsce. W przeciwnym razie szybko odkryjemy, że
jesteśmy w ukropie.
2
Do tego celu lepszy jest wykres burnup niż zwykły wykres burndown. Na podstawie
wykresu burnup można wyraźnie zobaczyć, jak wprowadzanie dodatkowych funkcji
zbliża nas do celu.
4337ebf6db5c7cc89e4173803ef3875a
4
306 
Rozdział 9.
Pragmatyczne projekty
Zaplanuj portfolio swojej wiedzy
W temacie 6., „Portfolio wiedzy”, przyjrzeliśmy się sposobom inwestowania
w swój osobisty zasób wiedzy w odpowiednim czasie. Zespoły, które chcą odnieść
sukces, powinny rozważać swoje inwestycje w wiedzę i umiejętności.
Jeśli Twój zespół podchodzi poważnie do doskonalenia i innowacji, powinien je
zaplanować. Próba realizacji tych inwestycji „w wolnej chwili” oznacza, że one
nigdy nie będą zrealizowane. Niezależnie od rodzaju prowadzonego dziennika
zadań do wykonania, nie rezerwuj w nim czasu wyłącznie na zadania programistyczne. Zespół nie powinien pracować wyłącznie nad nowymi funkcjonalnościami. Oto niektóre z możliwych przykładów innych zadań:

Konserwacja starych systemów. Choć kochamy pracować nad „błyszczącym”, nowym systemem, istnieje prawdopodobieństwo, że trzeba będzie
przeprowadzić pewne prace konserwacyjne w starym systemie. Spotykaliśmy zespoły, które próbowały zamieść te prace do kąta. Jeśli zespół jest
odpowiedzialny za wykonywanie tych zadań, powinien je wykonywać
naprawdę.

Refleksje na temat procesu i jego usprawniania. Ciągła poprawa może
następować tylko wtedy, gdy poświęcimy trochę czasu, aby się rozejrzeć
i ustalić co działa, a co nie, a następnie dokonać zmian (patrz temat 48.,
„Istota zwinności”). Zbyt wiele zespołów jest tak zajętych wylewaniem wody
z łodzi, że nikt nie poświęca czasu, aby naprawić źródło wycieku. Zaplanuj
to. Potem napraw.

Eksperymentowanie z nowymi technologiami. Nie korzystaj z nowych
technologii, frameworków lub bibliotek tylko dlatego, że „wszyscy to robią”
albo na podstawie czegoś, co obejrzałeś podczas konferencji lub przeczytałeś w internecie. Technologie kandydujące sprawdzaj w sposób przemyślany, poprzez tworzenie prototypów. Zaplanuj zadania wypróbowywania
nowych rzeczy w harmonogramie i analizuj wyniki przeprowadzonych prób.

Uczenie się i usprawnianie umiejętności. Osobiste uczenie się i doskonalenie to dobry początek, ale wiele umiejętności zdobywamy skuteczniej,
gdy robimy to wraz z zespołem. Zaplanuj takie przedsięwzięcia — w formie
nieformalnych sesji „brown bag”, czy też bardziej formalnych szkoleń.
WSKAZÓWKA NR 85
Aby coś się wydarzyło, zaplanuj to.
Komunikacja
To dość oczywiste, że programiści pracujący w zespole muszą ze sobą rozmawiać. Pewne sugestie, jak poprawiać komunikację w ramach zespołu, można
znaleźć w temacie 7., „Komunikuj się!”. Bardzo łatwo zapomnieć o tym, że
4337ebf6db5c7cc89e4173803ef3875a
4
Pragmatyczne zespoły
 307
sam zespół funkcjonuje w ramach większej organizacji. Zespół jako jedna
całość musi możliwie efektywnie komunikować się z resztą świata.
Z perspektywy osób z zewnątrz najgorsze zespoły projektowe to takie, które
sprawiają wrażenie nadętych i małomównych. Organizują spotkania pozbawione jakiegokolwiek planu. Co więcej, nikt na tych spotkaniach nie chce
zabierać głosu. Tworzone przez nich dokumenty to prawdziwy koszmar: nigdy
nie wyglądają tak samo, a w każdym stosuje się odmienną terminologię.
Najlepsze zespoły projektowe mają wyróżniającą się osobowość. Ludzie wprost
nie mogą doczekać się spotkań z tymi zespołami, ponieważ wiedzą, że mogą
oczekiwać doskonale przygotowanego pokazu, który wszystkim poprawi nastrój.
Tworzone przez nich dokumenty są zwięzłe, precyzyjne i spójne. Cały zespół mówi
jednym głosem3. Takie zespoły nierzadko mają też świetne poczucie humoru.
Istnieje prosta sztuczka marketingowa, która ułatwia zespołom komunikację —
należy stworzyć markę. Przy okazji początku prac nad projektem warto wymyślić dla niego jakąś nazwę, najlepiej coś zaskakującego. (W przeszłości nazywaliśmy swoje projekty, czerpiąc inspiracje z tego, co powie gadająca papuga,
z iluzji optycznych czy z nazw mitycznych miast). Warto też poświęcić pół godziny
na stworzenie jakiegoś zabawnego logo, które będzie potem powielane na notatkach i raportach. Nazwę zespołu można następnie swobodnie wykorzystywać
podczas rozmów z innymi. Proponowane rozwiązania mogą wydawać się niepoważne, jednak pozwalają zbudować tożsamość zespołu, a reszcie świata zapewniają coś, co można łatwo zapamiętać i w przyszłości kojarzyć z naszą pracą.
Nie powtarzaj się
W temacie 9., „DRY — przekleństwo powielania”, omówiliśmy rozmaite utrudnienia związane z powielaniem pracy przez członków tego samego zespołu.
Powielanie tych samych czynności jest nie tylko stratą czasu, ale też znacznie
utrudnia konserwację. W takich zespołach często dochodzi do powstawania
systemów typu „silos”; kod jest współdzielony w niewielkim stopniu oraz występuje mnóstwo zdublowanych funkcjonalności.
Kluczem do uniknięcia tych problemów jest dobra komunikacja. Przez słowo
„dobra” mamy na myśli komunikację natychmiastową i bezproblemową.
Powinieneś być w stanie zadać pytanie innym członkom zespołu i uzyskać niemal natychmiastową odpowiedź. Jeśli zespół pracuje w jednej lokalizacji, taka
komunikacja może sprowadzać się do podejścia do biurka współpracownika
w tym samym pokoju lub po drugiej stronie korytarza. Zdalne zespoły mogą
korzystać z komunikatorów lub innych środków elektronicznych.
3
Zespół mówi jednym głosem tylko na zewnątrz. Gorąco zachęcamy do prowadzenia
ożywionych sporów wewnątrz zespołu. Dobrzy programiści zwykle wykazują zaangażowanie emocjonalne w swoją pracę.
4337ebf6db5c7cc89e4173803ef3875a
4
308 
Rozdział 9.
Pragmatyczne projekty
Jeśli musisz czekać tydzień do spotkania zespołu, aby na nim zadać swoje
pytanie lub podzielić się swoim statusem, komunikacja jest znacznie utrudniona4. Bezproblemowa komunikacja oznacza, że zadawanie pytań jest łatwe
i nie wymaga „ceremonii”. Możesz bez trudu podzielić się postępami swojej pracy,
przekazać spostrzeżenia i odkrycia oraz zdobyć informacje o tym, co robią
Twoi współpracownicy.
Aby zachować zasadę DRY, bądź świadom, co robią inni.
Pociski smugowe zespołu
Zespół projektowy musi wykonywać wiele różnych zadań w różnych obszarach
projektu, z wykorzystaniem wielu różnych technologii. Trzeba zrozumieć wymagania, zaprojektować architekturę, zająć się kodowaniem warstwy frontend
i serwera, testowaniem — wszystko to musi się odbywać. Istnieje jednak błędne
przekonanie, że te działania i zadania mogą być realizowane osobno, w izolacji
od innych. Otóż nie mogą.
W niektórych metodologiach wyznacza się w zespole różnego rodzaju role
i stanowiska o specjalnych nazwach lub tworzy oddzielne wyspecjalizowane
zespoły. Problem z tym podejściem polega jednak na tym, że wprowadza ono
bariery między członkami zespołu i konieczność przekazywania pracy. Teraz
zamiast sprawnego przepływu pracy od zespołu do wdrożenia, mamy sztuczne
bariery, w których praca się zatrzymuje. Podczas przekazywania pracy pojawiają
się opóźnienia. Oczekiwania na zatwierdzenia. Papierkowa robota. Zwolennicy
metodologii Lean nazywają to marnotrawstwem i starają się aktywnie eliminować tego rodzaju zjawiska.
Wszystkie te różnego rodzaju stanowiska i działania, to w rzeczywistości różne
spojrzenia na ten sam problem. Sztuczne ich rozdzielanie może powodować
wiele kłopotów. Na przykład w przypadku programistów, którzy są oddzieleni
od rzeczywistych użytkowników i używanego przez nich kodu o dwa lub trzy
poziomy, istnieje małe prawdopodobieństwo świadomości kontekstu, w jakim
jest wykorzystywana ich praca. Takie osoby mogą być niezdolne do podejmowania świadomych decyzji.
W temacie „Pociski smugowe” zalecaliśmy rozwijanie pojedynczych funkcjonalności, początkowo niewielkich i ograniczonych, które obejmują zakres „od
końca do końca” przez cały system. Oznacza to, że członek zespołu powinien
posiadać wszystkie umiejętności potrzebne do wykonywania prac: tworzenie
frontendu, interfejsu użytkownika, dbanie o ergonomiczność interfejsu (ang.
user experience — UX), tworzenie warstwy serwerowej, administracja bazą
danych, zapewnienie jakości itd. Każdy członek zespołu powinien umieć swobodnie wykonywać te zadania, we współpracy z innymi. Dzięki stosowaniu
4
Andy spotkał się z zespołem, który przeprowadzał spotkania stand-up — które powinny
odbywać się codziennie — w piątki.
4337ebf6db5c7cc89e4173803ef3875a
4
Pragmatyczne zespoły
 309
podejścia pocisków smugowych, można bardzo szybko wdrożyć bardzo niewielkie fragmenty funkcjonalności i natychmiast uzyskać informacje zwrotne
o tym, jak dobrze członkowie zespołu komunikują się ze sobą i dostarczają
rozwiązania. W ten sposób tworzy się środowisko, w którym można szybko
i łatwo wprowadzać zmiany oraz dostrajać zespół i proces.
WSKAZÓWKA NR 86
Organizuj w pełni funkcjonalne zespoły.
Buduj zespoły w taki sposób, aby mogły budować kod „od końca do końca”,
stopniowo i iteracyjnie.
Automatyzacja
Doskonałym sposobem jednoczesnego zapewniania spójności i dokładności jest
automatyzacja możliwie wielu czynności zespołu. Po co ręcznie dbać o układ
kodu, skoro nasz edytor może to robić automatycznie w trakcie pisania? Po co
wypełniać formularze z testów, skoro nocna kompilacja może obejmować automatyczne wykonanie testów? Po co wdrażać oprogramowanie ręcznie, jeśli dzięki
automatyzacji można zrealizować wdrażanie za każdym razem w taki sam sposób i równie niezawodnie?
Automatyzacja jest ważnym aspektem funkcjonowania każdego zespołu projektowego. Należy zadbać o to, aby zespół posiadał umiejętności tworzenia
i wdrażania narzędzi automatyzujących operacje związane z programowaniem
i wdrażaniem.
Należy wiedzieć, kiedy przestać dodawać
nowe warstwy farby
Musimy pamiętać, że zespoły składają się z indywidualności. Każdy członek
zespołu powinien mieć możliwość prezentacji umiejętności na własny sposób.
Należy zadbać tylko o taką strukturę, która zapewni im niezbędne wsparcie,
oraz o to, aby wnosili do projektu wartość. W pewnym momencie, tak jak malarz
z tematu „Wystarczająco dobre oprogramowanie”, nie możemy ulegać pokusie
dodawania farby.
Pokrewne podrozdziały

Temat 2., „Kot zjadł mój kod źródłowy”.

Temat 7., „Komunikuj się!”.

Temat 12., „Pociski smugowe”.
4337ebf6db5c7cc89e4173803ef3875a
4
310 
Rozdział 9.
Pragmatyczne projekty

Temat 19., „Kontrola kodu źródłowego”.

Temat 50., „Nie próbuj przecinać kokosów”.

Temat 51., „Zestaw startowy pragmatyka”.
Wyzwania
50
37

Przeanalizuj sposób działania zespołów osiągających sukcesy spoza świata
wytwarzania oprogramowania. Co decyduje o ich sukcesach? Czy stosują
któryś z procesów omówionych w tym podrozdziale?

Przy okazji rozpoczynania kolejnego projektu spróbuj przekonać współpracowników do nadania mu marki. Daj swojej organizacji trochę czasu na
przyzwyczajenie się do tej koncepcji, po czym dokonaj krótkiego przeglądu
uzyskanych efektów (zarówno w ramach zespołu, jak i poza nim).

Algebra zespołowa — w szkole często rozwiązywaliśmy podobne problemy:
„Jeśli 4 robotników potrzebuje 6 godzin na wykopanie rowu, ile czasu ta
sama czynność zajmie 8 robotnikom?”. Jakie dodatkowe czynniki wpływają
na wynik w przypadku rzeczywistych projektów? Ile zajmie stworzenie
aplikacji przez 8 programistów, jeśli 4 programiści mogą opracować tę
samą aplikację w 6 miesięcy? W ilu scenariuszach rzeczywiście można skrócić ten czas?

Przeczytaj książkę Fredericka Brooksa The Mythical Man Month [Bro96].
Aby uzyskać dodatkowe korzyści, kup dwa egzemplarze; dzięki temu cały
zespół będzie mógł zapoznać się z nią dwa razy szybciej.
Nie próbuj przecinać kokosów
Rdzenni mieszkańcy wyspy nigdy nie widzieli wcześniej samolotu ani nie spotykali ludzi podobnych do tych, którzy nim przylecieli. W zamian za korzystanie
z ziemi, obcy udostępnili mechaniczne ptaki, które latały na wyspę i do świata
zewnętrznego, przywożąc na wyspę niesamowite bogactwa materialne. Obcy
wspominali coś o wojnie i walkach w ich kraju. Pewnego dnia wojna się skończyła i wszyscy obcy opuścili wyspę, zabierając ze sobą ich dziwne bogactwa.
Wyspiarze usilnie starali się odtworzyć swoje fortuny. Przy użyciu lokalnych
materiałów: winorośli, łupin orzechów kokosowych, liści palmowych itp. zbudowali lotnisko, wieżę kontrolną oraz urządzenia. Ale z jakiegoś powodu, choć
wszystko było na miejscu, samoloty nie przylatywały. Udało im się naśladować
formę, ale nie treść. Antropolodzy nazywają to kultem cargo.
Zbyt często zachowujemy się tak jak wyspiarze.
Łatwo można wpaść w pułapkę kultu cargo: poprzez inwestycję w budowę
dobrze widocznych artefaktów mamy nadzieję przyciągnąć działającą magię.
4337ebf6db5c7cc89e4173803ef3875a
4
Nie próbuj przecinać kokosów
 311
Jednak, podobnie jak w przypadku pierwotnych kultów cargo w Melanezji5,
sztuczne lotnisko wykonane z łupin orzechów kokosowych nie zastąpi prawdziwego.
Na przykład spotykaliśmy zespoły, które twierdzą, że korzystają z metodologii
Scrum. Jednak po bliższym przyjrzeniu się ich pracy okazało się, że codzienne
spotkania stand-up są organizowane raz w tygodniu, iteracje zwykle trwają
cztery tygodnie, a często przeciągają się do sześciu lub ośmiu tygodni. Członkowie zespołu uważali, że to jest w porządku, ponieważ stosowali popularne
„zwinne” narzędzia planowania. W rzeczywistości inwestowali jedynie w powierzchowne artefakty, które często tylko z nazwy przypominały te prawdziwe techniki, tak jakby „stand-up” lub „iteracja” były jakimiś magicznymi zaklęciami
dla przesądnych. Nic dziwnego, że im również nie udało się przywołać prawdziwej magii.
Kontekst ma znaczenie
Czy Ty lub Twój zespół wpadliście w taką pułapkę? Zadaj sobie pytanie, dlaczego
korzystasz z tej konkretnej metodologii programowania. Albo z tego frameworka?
Lub z tej techniki testowania? Czy ona rzeczywiście dobrze nadaje się do wykonywanych prac? Czy dobrze się sprawdza w Twoim przypadku? A może została
zastosowana tylko dlatego, że była używana w projekcie znanym z internetu
i przedstawiona jako najnowsza historia sukcesu?
Obecnie panuje trend do przyjmowania strategii i procedur stosowanych przez
firmy, które odniosły sukces, takie jak Spotify, Netflix, Stripe, GitLab i inne.
Każda z tych firm stosuje własne, unikatowe podejście do rozwoju oprogramowania oraz sposobu zarządzania. Weźmy jednak pod uwagę kontekst: czy
jesteś na tym samym rynku, z tymi samymi ograniczeniami i możliwościami,
doświadczeniem i rozmiarem organizacji? Czy masz podobny styl zarządzania
oraz podobną kulturę? Podobną bazę użytkowników i wymagania?
Nie daj się nabrać. Konkretne artefakty, powierzchowne struktury, zasady, procesy i metody nie wystarczą.
WSKAZÓWKA NR 87
Rób to, co się sprawdza, a nie to, co jest modne.
Skąd wiadomo, że „coś się sprawdza”? Polegaj na najbardziej podstawowej
pragmatycznej technice:
Wypróbuj to.
5
Zobacz https://pl.wikipedia.org/wiki/Kulty_cargo
4337ebf6db5c7cc89e4173803ef3875a
4
312 
Rozdział 9.
Pragmatyczne projekty
Stwórzcie z małym zespołem lub zbiorem zespołów rozwiązanie pilotażowe.
Zachowaj dobre fragmenty, które wydają się dobrze działać, i odrzuć resztę jak
odpady lub niepotrzebny balast. Nikt nie będzie degradować swojej organizacji
tylko dlatego, że działa odmiennie od Spotify czy Netflix. Nawet te firmy w czasie,
kiedy się rozwijały, nie postępowały zgodnie ze swoimi bieżącymi procesami.
A za kilka lat, gdy dojrzeją i nadal się będą rozwijać, także będą robić coś innego.
Właśnie w tym tkwi sekret ich sukcesu.
Rozmiar uniwersalny nikomu dobrze nie pasuje
Celem metodologii wytwarzania oprogramowania jest wspomaganie pracy
zespołowej. Zgodnie z tym, o czym mówiliśmy w podrozdziale „Istota zwinności”,
podczas tworzenia oprogramowania nie ma jednego planu, zgodnie z którym
można by postępować — zwłaszcza jeśli to jest plan opracowany przez kogoś
innego, w jakiejś innej firmie.
Wiele programów certyfikacji działa jeszcze gorzej: są one przyznawane studentom, którzy potrafią zapamiętać reguły i ich przestrzegać. Nie tego jednak
chcemy. Potrzebna nam jest zdolność postrzegania wykraczająca poza obowiązujące reguły i wykorzystywania możliwości uzyskania korzyści. To zupełnie
inny sposób myślenia od „ale zgodnie z metodyką Scrum, Lean, Kanban, XP,
Agile robi się to w ten sposób…” i tak dalej.
Zamiast tego, powinniśmy wybrać najlepsze fragmenty z konkretnej metodologii i dostosować je do praktycznego wykorzystania. Żaden rozmiar nie pasuje
wszystkim, a współczesne metody są dalekie od kompletnych, więc należy
korzystać z większej liczby technik niż tylko te, które oferuje jedna popularna
metodologia.
Na przykład Scrum definiuje pewne praktyki zarządzania projektami, ale sam
nie dostarcza wystarczająco dużo wskazówek na poziomie technicznym dla
zespołów lub na poziomie zarządzania portfolio dla kierownictwa.
Od czego więc należy zacząć?
Być jak Oni!
Często słyszymy, jak liderzy rozwoju oprogramowania mówią do swoich pracowników: „Powinniśmy działać jak Netflix” (lub jedna z innych czołowych firm). Oczywiście
można to robić.
Najpierw jednak trzeba dysponować kilku setkami tysięcy serwerów i mieć dziesiątki milionów użytkowników…
4337ebf6db5c7cc89e4173803ef3875a
4
Nie próbuj przecinać kokosów
 313
Prawdziwy cel
Cel oczywiście nie polega na „postępowaniu zgodnie ze Scrum”, „Agile”, „Lean”,
lub co tam sobie wymyślimy. Celem jest wypracowanie metodologii pozwalającej
na błyskawiczne dostarczanie działającego oprogramowania, które daje użytkownikom kilka nowych możliwości. Nie za wiele tygodni, miesięcy lub lat od
teraz, ale natychmiast. Dla wielu zespołów i organizacji ciągłe dostawy brzmią
jak wzniosły, nieosiągalny cel, zwłaszcza jeśli jesteśmy ograniczeni przez proces,
który wymaga dostaw co kilka miesięcy lub tygodni. Ale tak jak w przypadku
dążenia do każdego celu, kluczem jest podążanie we właściwym kierunku.
Jeśli dostarczacie oprogramowanie w skali lat, spróbujcie skrócić cykl do kilku
miesięcy. Kilka miesięcy spróbujcie zamienić na kilka tygodni. Ze sprintu trwającego cztery tygodnie spróbujcie przejść na dwutygodniowy. Z dwutygodniowego
sprintu przełączcie się na jednotygodniowy. Następnie postarajcie się dostarczać
oprogramowanie codziennie. W końcu dostarczajcie je na żądanie. Należy zauważyć, że zdolność dostarczania oprogramowania na żądanie nie oznacza, że jesteśmy zmuszeni do dostarczania go w każdej minucie każdego dnia. Dostarczacie
je, gdy użytkownicy go potrzebują — wtedy, gdy ma to sens biznesowy.
WSKAZÓWKA NR 88
Dostarczaj oprogramowanie wtedy, gdy użytkownicy go potrzebują.
Aby zastosować taki styl ciągłego rozwoju, potrzeba solidnej infrastruktury.
Omówimy ją w następnym temacie, „Zestaw startowy pragmatyka”. Oprogramowanie należy rozwijać na głównym pniu systemu kontroli wersji. Nie
powinno się robić tego za pomocą gałęzi. Należy także korzystać z takich technik, jak przełączniki funkcji, aby selektywnie dostarczać testowe funkcjonalności użytkownikom.
4337ebf6db5c7cc89e4173803ef3875a
4
314 
Rozdział 9.
Pragmatyczne projekty
Gdy mamy odpowiednią infrastrukturę, musimy zdecydować, jak zorganizować
pracę. Początkujący mogą zacząć od wykorzystania do zarządzania projektami
metodologii Scrum wzbogaconej technicznymi praktykami programowania
ekstremalnego (XP). Bardziej zdyscyplinowane i doświadczone zespoły mogą skorzystać z technik Kanban i Lean, zarówno na poziomie zespołu, jak i na wyższych poziomach zarządzania.
Nie należy jednak wierzyć nam na słowo. Powinieneś spróbować tych podejść
i sprawdzić, jak działają w Twoich realiach. Trzeba jednak zachować ostrożność
i uważać, żeby nie przesadzić. Nadmierne inwestowanie w konkretną metodologię może sprawić, że staniemy się ślepi na dostępne alternatywy. Łatwo jest się
przyzwyczaić do jednej metody. Wkrótce będzie Ci trudno sobie wyobrazić, że
można postępować w jakikolwiek inny sposób. Staniesz się zwapniały i stracisz
zdolność szybkiego przystosowania się do nowych warunków.
Równie dobrze mógłbyś używać łupin z kokosów.
Pokrewne podrozdziały
51
38

Temat 12., „Pociski smugowe”.

Temat 27., „Nie prześcigaj swoich świateł”.

Temat 48., „Istota zwinności”.

Temat 49., „Pragmatyczne zespoły”.

Temat 51., „Zestaw startowy pragmatyka”.
Zestaw startowy pragmatyka
Postęp cywilizacji odbywa się poprzez rozszerzanie zbioru ważnych
czynności, które możemy wykonywać bez myślenia.
Alfred North Whitehead
W czasach, gdy samochody były nowością, instrukcja do uruchomienia Forda
modelu T miała ponad dwie strony. W nowoczesnych samochodach wystarczy
wcisnąć przycisk — procedura startu jest automatyczna i niezawodna. Osoba
wykonująca instrukcje z listy może zalać silnik, ale nie zrobi tego automatyczny
starter.
Chociaż branża wytwarzania oprogramowania jest wciąż na etapie modelu T,
nie możemy sobie pozwolić, aby wielokrotnie wykonywać dwustronicowe instrukcje w celu realizacji jakiegoś popularnego działania. Niezależnie od tego, czy
jest to procedura budowania i publikacji, testowania, tworzenia dokumentacji
projektu lub innego powtarzalnego zadania w projekcie, musi ona być automatyczna i powtarzalna na dowolnym urządzeniu, na którym ma działać oprogramowanie.
4337ebf6db5c7cc89e4173803ef3875a
4
Zestaw startowy pragmatyka
 315
Ponadto chcemy zapewnić w projekcie spójność i powtarzalność. Procedury
manualne pozostawiają spójność przypadkowi; powtarzalność nie jest gwarantowana, szczególnie jeśli aspekty procedury są przedmiotem interpretacji
przez różne osoby.
Po napisaniu pierwszego wydania Pragmatycznego programisty, chcieliśmy
stworzyć więcej książek, które mogłyby pomóc zespołom w tworzeniu oprogramowania. Doszliśmy do wniosku, że powinniśmy zacząć od początku: od
opisywania najbardziej podstawowych, najważniejszych elementów, których
potrzebuje każdy zespół, niezależnie od metodologii, języka lub stosu technologicznego. Tak zrodził się pomysł na Zestaw startowy pragmatyka, obejmujący
następujące trzy kluczowe i powiązane ze sobą tematy:

Kontrola wersji.

Testy regresji.

Pełna automatyzacja.
Są to trzy podstawy, na których stoi każdy projekt. Oto ich opis.
Sterowanie projektem za pomocą systemu
kontroli wersji
Zgodnie z tym, co napisaliśmy w temacie „Kontrola kodu źródłowego”, w systemie kontroli wersji należy przechowywać wszystko, co jest potrzebne do zbudowania projektu. Ta koncepcja staje się jeszcze bardziej istotna w kontekście
samego projektu.
Po pierwsze, pozwala korzystać z ulotnych maszyn budowania. Zamiast jednej
„świętej”, stojącej w kącie biura skrzypiącej maszyny, której każdy boi
się dotknąć6, maszyny budowania i (lub) klastry są tworzone na żądanie jako
egzemplarze w chmurze. Konfiguracja wdrażania również jest zawarta w systemie kontroli wersji, dzięki czemu publikowanie systemu w celu wdrażania do
produkcji może być wykonywane automatycznie.
Oto ważne spostrzeżenie: kontrola wersji napędza proces budowania i publikowania na poziomie projektu.
WSKAZÓWKA NR 89
Korzystaj z systemu kontroli wersji do zarządzania budowaniem systemu,
jego testowaniem i publikowaniem.
Oznacza to, że budowanie, testy i wdrażanie są wyzwalane poprzez commity
lub przekazania kodu (ang. push) do systemu kontroli wersji, a budowanie
6
Widzieliśmy to na własne oczy więcej razy, niż mogłoby się wydawać.
4337ebf6db5c7cc89e4173803ef3875a
4
316 
Rozdział 9.
Pragmatyczne projekty
odbywa się w kontenerze, w chmurze obliczeniowej. Publikowanie do środowiska
próbnego (ang. staging) lub produkcyjnego jest określane w systemie kontroli
wersji za pomocą tagowania. Publikowanie staje się zatem znacznie mniej
celebrowaną, codzienną czynnością — osiąga się w ten sposób prawdziwy system ciągłego dostarczania, niepowiązany z żadną maszyną pojedynczą maszyną
budowania lub maszyną programistyczną.
Bezlitosne testy
Większość programistów stara się testować oprogramowanie możliwie ostrożnie.
Podświadomie wiedzą oni, gdzie kod może zawierać widoczne błędy, i unikają
jego słabych punktów. Pragmatyczni programiści postępują inaczej. Naszym
celem jest możliwie efektywne znalezienie błędów i nie możemy pozwolić na to,
aby inni znajdowali nasze błędy w przyszłości.
Znajdowanie błędów przypomina trochę łowienie ryb w sieć. Używamy delikatnych sieci z drobnymi oczkami (testów jednostkowych) do łapania minogów
oraz wielkich, mocnych sieci (testów integracyjnych) do łapania rekinów ludojadów. Zdarza się, że jakiejś rybie uda się uciec, zatem stale naprawiamy
wszystkie znalezione dziury w sieci w nadziei na złapanie coraz większej ilości
obślizgłych usterek, które wciąż pływają w basenie naszego projektu.
WSKAZÓWKA NR 90
Należy testować wcześnie. Należy testować często. Należy testować automatycznie.
Chcemy przystąpić do testów tak szybko, jak tylko będziemy dysponowali jakimś
kodem. Wszystkie te drobne minogi mają brzydki zwyczaj błyskawicznego wzrostu do rozmiarów gigantycznych rekinów ludojadów, a łapanie rekinów z natury
rzeczy jest dużo trudniejsze. Dlatego piszemy testy jednostkowe, mnóstwo testów
jednostkowych.
W rzeczywistości dobry projekt może obejmować więcej kodu testowego niż
kodu produkcyjnego. Czas potrzebny do utworzenia tego kodu testowego jest
wart niezbędnych nakładów pracy. W dłuższej perspektywie taki kod jest dużo
tańszy, ponieważ daje szansę stworzenia produktu z liczbą usterek bliską zeru.
Co więcej, świadomość, że nasz kod przeszedł wszystkie testy, to także pewność,
że dany fragment kodu jest już gotowy.
WSKAZÓWKA NR 91
Kodowanie nie jest skończone, dopóki nie zostaną wykonane wszystkie
testy.
4337ebf6db5c7cc89e4173803ef3875a
4
Zestaw startowy pragmatyka
 317
Automatyczny proces budowania uruchamia wszystkie dostępne testy. Ważne
jest, aby dążyć do „testów na serio” — innymi słowy, środowisko testowe powinno
być bardzo zbliżone do produkcyjnego. Ewentualne luki to miejsca, gdzie przenikają błędy.
Proces budowania może obejmować kilka głównych rodzajów testów oprogramowania: testy jednostkowe, integracyjne, walidację i weryfikację oraz testy
wydajności.
Powyższa lista w żadnym razie nie jest kompletna — specjalistyczne projekty
na pewno będą wymagały rozmaitych innych typów testów. Przytoczona lista
jest jednak dobrym punktem wyjścia.
Testy jednostkowe
Test jednostkowy oprogramowania ma postać kodu sprawdzającego pewien
moduł. Zagadnienia związane z tymi testami omówiliśmy już w temacie 41.,
„Kod łatwy do testowania”. Testy jednostkowe stanowią podstawę dla wszystkich pozostałych form testowania, którymi zajmiemy się w dalszej części tego
podrozdziału. Jeśli elementy składowe nie działają prawidłowo, najprawdopodobniej nie zadziałają także w ramach większej całości. Wszystkie używane
przez nas moduły muszą przejść własne testy jednostkowe, zanim będziemy
mogli kontynuować pracę.
Dopiero kiedy wszystkie moduły przejdą swoje indywidualne testy, jesteśmy
gotowi do następnego kroku. Musimy przetestować, jak wszystkie te moduły
współpracują ze sobą w ramach systemu.
Testy integracyjne
Testy integracyjne wykazują, czy główne podsystemy składające się na projekt
prawidłowo ze sobą współpracują. Odpowiednie, dobrze przetestowane kontrakty umożliwiają łatwe i szybkie wykrywanie problemów związanych z integracją. W przeciwnym razie integracja jest jak hodowanie błędów na urodzajnej
glebie. W praktyce właśnie integracja jest często źródłem największej liczby
błędów w systemie.
Testy integracyjne są w istocie rozszerzeniem opisanych przed chwilą testów
jednostkowych, z tą różnicą, że tym razem testujemy zgodność całych podsystemów z ich kontraktami.
Walidacja i weryfikacja
Kiedy tylko będziemy dysponowali wykonywalnym interfejsem użytkownika
lub prototypem, będziemy musieli odpowiedzieć sobie na bardzo ważne pytanie: użytkownicy powiedzieli nam, czego oczekują, ale czy rzeczywiście tego
potrzebują?
4337ebf6db5c7cc89e4173803ef3875a
4
318 
Rozdział 9.
Pragmatyczne projekty
Czy wyrażone przez nich oczekiwania odpowiadają wymaganiom funkcjonalnym systemu? To także trzeba przetestować. Nawet bezbłędny system, który
odpowiada na niewłaściwe pytanie, nie ma większej wartości. Musimy mieć
świadomość wzorców dostępu użytkowników końcowych i różnic dzielących
te wzorce od danych testowych stosowanych przez programistów (odpowiedni
przykład można znaleźć w historii o ruchach pędzlem w podrozdziale „Debugowanie” w rozdziale 3.).
Testy wydajnościowe
Ważnym aspektem projektu mogą być także testy wydajnościowe lub testy
obciążeniowe.
Warto zadać sobie pytanie, czy nasze oprogramowanie spełnia wymagania
w zakresie wydajności w rzeczywistych warunkach — podczas realizacji żądań
oczekiwanej liczby użytkowników, przy oczekiwanej liczbie połączeń lub podczas wykonywania planowanej liczby transakcji na sekundę. Czy system jest
skalowalny?
Niektóre aplikacje wymagają zastosowania wyspecjalizowanego sprzętu lub
oprogramowania testowego zdolnego do realistycznej symulacji obciążenia.
Testowanie testów
Skoro nie możemy pisać doskonałego oprogramowania, z natury rzeczy nie
możemy też opracować doskonałego oprogramowania testowego. Musimy więc
testować same testy.
Zbiór pakietów testowych można postrzegać jako rozbudowany system zabezpieczeń zaprojektowany z myślą o uruchamianiu syreny alarmowej w momencie
wykrycia błędu. Czy można lepiej przetestować system zabezpieczeń, niż wykonując próby jego złamania?
Po napisaniu testu wykrywającego konkretny błąd warto celowo doprowadzić
do występowania tego błędu i upewnić się, że test rzeczywiście wykaże usterkę.
Dzięki temu będziemy wiedzieli, że test wykryje interesujący nas błąd w razie
jego wystąpienia w rzeczywistości.
WSKAZÓWKA NR 92
Do testowania testów należy stosować techniki sabotażu.
Jeśli traktujemy naprawdę poważnie problem testowania tworzonego systemu,
możemy wyznaczyć członka zespołu do roli sabotażysty. Zadaniem sabotażysty
będzie utworzenie odrębnej kopii drzewa kodu źródłowego, celowe wprowadzanie błędów w tej kopii i sprawdzanie, czy istniejące testy prawidłowo sygnalizują usterki. Na wysokim poziomie możemy użyć mechanizmu podobnego do
4337ebf6db5c7cc89e4173803ef3875a
4
Zestaw startowy pragmatyka
 319
stosowanej w firmie Netflix techniki Chaos Monkey7, polegającej na zakłócaniu
(niszczeniu) usług i testowaniu odporności aplikacji na takie sytuacje.
Kiedy tworzymy testy, musimy mieć pewność, że syrena alarmowa zabrzmi
wtedy, kiedy to konieczne.
Gruntowne testowanie
Skoro jesteśmy już pewni, że nasze testy są prawidłowe i że wykrywają popełniane przez nas błędy, jak sprawdzić, czy przetestowaliśmy naszą bazę kodu
źródłowego wystarczająco dokładnie?
Odpowiedź jest prosta: to nie jest i nigdy nie będzie możliwe. Okazuje się
jednak, że istnieją na rynku produkty, które mogą nam w tym pomóc. Narzędzia do analizy pokrycia testami obserwują nasz kod w trakcie testów i śledzą,
które wiersze kodu są, a które nie są wykonywane. Narzędzia tego typu mogą
co prawda wskazywać, na ile wyczerpujące są nasze testy, ale nigdy nie osiągniemy stuprocentowego pokrycia8.
Nawet jeśli w trakcie testów zostanie wykonany każdy wiersz kodu, nie możemy
być pewni wyczerpania wszystkich możliwości. Ważna jest raczej liczba stanów,
w których może się znaleźć nasz program. Stany nie są równoważne wierszom
kodu. Przypuśćmy na przykład, że dysponujemy funkcją otrzymującą na wejściu dwie liczby całkowite, z których każda może mieć wartość od 0 do 999:
int test(int a, int b) {
return a / (a + b);
}
W teorii ta funkcja złożona z zaledwie trzech wierszy kodu ma 1 000 000 logicznych stanów, z których 999 999 działa prawidłowo, a jeden powoduje błąd
(kiedy wyrażenie a + b ma wartość 0). Sama świadomość wykonania tego wiersza kodu niewiele nam mówi — powinniśmy raczej zidentyfikować wszystkie
możliwe stany programu. Okazuje się, niestety, że analiza tak rozumianego
pokrycia stanowi naprawdę poważny problem. Prędzej Słońce zmieni się
w zimną, twardą bryłę, niż ktokolwiek rozwiąże ten problem.
WSKAZÓWKA NR 93
Należy testować pokrycie stanów, nie pokrycie kodu.
Testowanie na podstawie właściwości
Świetną metodą pozwalającą na zbadanie sposobu, w jaki kod obsługuje nieoczekiwane stany, jest zlecenie komputerowi wygenerowania tych stanów.
7
https://netflix.github.io/chaosmonkey
8
Interesujące wnioski na temat korelacji pomiędzy pokryciem testami a liczbą defektów
można znaleźć w książce Mythical Unit Test Coverage [ADSS18].
4337ebf6db5c7cc89e4173803ef3875a
4
320 
Rozdział 9.
Pragmatyczne projekty
Skorzystaj z technik testowania na podstawie właściwości, aby wygenerować
dane testowe zgodnie z kontraktami i niezmiennikami testowanego kodu.
Ten temat szczegółowo omówiliśmy w podrozdziale „Testowanie na podstawie
właściwości”.
Zacieśnianie pętli
Na koniec chcielibyśmy zwrócić uwagę na najważniejszą cechę dobrych testów.
To tak oczywiste, że niemal w każdej książce można znaleźć odpowiednie zalecenia. Okazuje się jednak, że z jakiegoś powodu większość zespołów wciąż tego
nie robi.
Jeśli jakiś błąd uwolni się z sieci istniejących testów, musimy dodać nowy
test, tak aby od tej pory wykrywać ten błąd.
WSKAZÓWKA NR 94
Każdy błąd należy znajdować tylko raz.
Kiedy żywy tester znajduje jakiś błąd, powinien to być ostatni przypadek,
kiedy wykrycie tego błędu wymaga zaangażowania człowieka. Zautomatyzowane
testy należy tak zmodyfikować, aby od tej pory za każdym razem, bez wyjątku,
sprawdzały kod pod kątem zawierania tego błędu, nawet jeśli usterka wydaje
się trywialna i mimo żarliwych zapewnień programisty, jakoby podobny błąd
nigdy nie miał się powtórzyć.
Każdy błąd prędzej czy później wystąpi ponownie. Nie mamy czasu na samodzielne ściganie błędów, które równie dobrze mogłyby być wykrywane przez
zautomatyzowane testy. Powinniśmy raczej poświęcać nasz cenny czas na pisanie
nowego kodu (z nowymi błędami).
Pełna automatyzacja
Zgodnie z tym, co powiedzieliśmy na początku tego podrozdziału, nowoczesne
podejście do wytwarzania oprogramowania bazuje na automatycznych procedurach zaimplementowanych za pomocą skryptów. Niezależnie od tego, czy
korzystasz z czegoś tak prostego, jak skrypty powłoki rsync i ssh, czy też
z w pełni funkcjonalnych rozwiązań, takich jak Ansible, Puppet, Chef lub Salt,
po prostu nie powinieneś bazować na jakichkolwiek interwencjach ręcznych.
Mieliśmy kiedyś okazję obserwować dział IT naszego klienta, gdzie wszyscy
programiści używali tego samego środowiska IDE. Administrator systemu
dawał każdemu programiście zbiór instrukcji, jak zainstalować dodatkowe
pakiety i moduły w tym środowisku. Instrukcje składały się z wielu stron pełnych zdań poleceń „kliknij tutaj”, „przewiń tam”, „przeciągnij to”, „dwukrotnie
kliknij tamto” itp.
4337ebf6db5c7cc89e4173803ef3875a
4
Zestaw startowy pragmatyka
 321
Trudno się więc dziwić, że komputer każdego programisty działał nieco inaczej.
Drobne różnice w zachowaniu tej aplikacji ujawniały się w sytuacjach, gdy
różni programiści uruchamiali ten sam kod. Błędy występowały na jednym
komputerze, ale nie występowały na pozostałych. Analiza różnic w wersjach
poszczególnych komponentów zwykle prowadziła do zaskakujących wyników.
WSKAZÓWKA NR 95
Nie należy stosować ręcznych procedur.
Ludzie po prostu nie są tak powtarzalni jak komputery. Trudno nawet tego
oczekiwać. Skrypt powłoki czy plik wsadowy za każdym razem wykona te same
operacje w tej samej kolejności. A jeśli dodatkowo umieścimy ten skrypt w systemie kontroli wersji, będziemy mogli śledzić zmiany w samej procedurze (unikając problemu „to przecież działało…”).
Wszystko zależy od automatyzacji. Nie da się zbudować projektu na anonimowym
serwerze w chmurze, jeśli jego proces budowania nie będzie w pełni automatyczny. Nie da się przeprowadzić automatycznego wdrażania, jeśli w procesie
tego wdrażania występują jakieś etapy wykonywane ręcznie. A kiedy wprowadzimy ręczne czynności („tylko dla tej jednej części…”), wybijemy bardzo
dużą szybę9.
Dzięki opisanym trzem podstawom: kontroli wersji, bezwzględnemu testowaniu
i pełnej automatyzacji, projekt będzie miał solidne fundamenty, niezbędne do
tego, by można było skoncentrować się na tym, co trudne — wprawieniu użytkowników w zachwyt.
Pokrewne podrozdziały

Temat 11., „Odwracalność”.

Temat 12., „Pociski smugowe”.

Temat 17., „Powłoki”.

Temat 19., „Kontrola kodu źródłowego”.

Temat 41., „Kod łatwy do testowania”.

Temat 49., „Pragmatyczne zespoły”.

Temat 50., „Nie próbuj przecinać kokosów”.
Wyzwania

9
Czy Twoje nocne kompilacje lub kompilacje wykonywane w ramach systemu ciągłej integracji są automatyczne, ale wdrażanie do produkcji nie
jest takie? Dlaczego? Czym specjalnym charakteryzuje się ten serwer?
Zawsze należy pamiętać o tym, o czym pisaliśmy w temacie „Entropia oprogramowania”.
4337ebf6db5c7cc89e4173803ef3875a
4
322 
52
39
Rozdział 9.
Pragmatyczne projekty

Czy możesz automatycznie testować swój projekt? Wiele zespołów musi
odpowiedzieć, że nie. Dlaczego? Czy zdefiniowanie możliwych do zaakceptowania wyników rzeczywiście jest takie trudne? Czyż takie testy nie
ułatwiłyby nam wykazania przed sponsorami projektu, że jego realizacja
jest zakończona?

Czy testowanie logiki aplikacji w oderwaniu od jej graficznego interfejsu
użytkownika jest zbyt trudne? O czym to świadczy (w kontekście interfejsu)?
Może o zbyt ścisłych sprzężeniach?
Wpraw w zachwyt użytkowników
Kiedy chcesz zaczarować ludzi, Twoim celem nie powinno być
zarabianie na nich pieniędzy lub skłonienie do tego, by robili to,
czego chcesz, ale wprawienie ich w zachwyt.
Guy Kawasaki
Celem programistów powinno być wprawienie użytkowników w zachwyt. Na
tym polega sens naszej pracy. Nie chodzi o wydobywanie od nich danych lub
liczenie ich oczu, czy też opróżnianie portfeli. Pomijając nikczemne cele, nawet
ograniczenie się do dostarczenia działającego oprogramowania na czas nie
wystarczy. Samo to nie wprawi użytkowników w zachwyt.
Użytkownicy Twojego oprogramowania nie są szczególnie motywowani kodem.
Zamiast tego mają problem biznesowy, który wymaga rozwiązania w kontekście
ich celów i budżetu. Wierzą, że dzięki pracy z Twoim zespołem będą mogli go
osiągnąć.
Oczekiwania użytkowników nie są związane z oprogramowaniem. Nie są nawet
ukryte w żadnej specyfikacji, którą Ci przekażą (ponieważ ta specyfikacja będzie
niekompletna, dopóki Twój zespół nie przetworzy jej w kilku iteracjach).
Zatem w jaki sposób można odkryć oczekiwania użytkowników? Należy zadać
sobie proste pytanie:
Skąd będziesz wiedzieć, że wszyscy odnieśli sukces za miesiąc (rok lub po
upływie innego czasu) po zrealizowaniu projektu?
Odpowiedź może Cię zaskoczyć. Projekt poprawy rekomendacji produktu może
być właściwie oceniony pod kątem retencji klienta; projekt polegający na konsolidacji dwóch baz danych można ocenić pod kątem jakości danych albo
oszczędności finansowych. Ale są to oczekiwania względem wartości biznesowych. To one naprawdę się liczą, a nie projekt oprogramowania sam w sobie.
Oprogramowanie jest tylko środkiem do realizacji tych celów.
4337ebf6db5c7cc89e4173803ef3875a
4
Wpraw w zachwyt użytkowników
 323
Kiedy już uda Ci się odkryć niektóre z oczekiwań dotyczących wartości związanych z projektem, możesz zacząć myśleć o tym, jak możesz dostarczyć oprogramowanie, aby spełnić te oczekiwania:

Zadbaj o to, aby wszyscy członkowie zespołu dokładnie rozumieli te oczekiwania.

Przy podejmowaniu decyzji pomyśl o tym, która ścieżka zbliża Cię do spełnienia tych oczekiwań.

Poddaj krytycznej analizie wymagania użytkowników w świetle ich oczekiwań. W wielu projektach obserwowaliśmy sformułowane „wymagania”,
które w rzeczywistości były jedynie domysłami na temat tego, co można
zrobić za pomocą technologii: był to jedynie amatorski plan implementacji
przebrany za dokument wymagań. Nie obawiaj się sugerowania zmiany
wymagań, jeżeli potrafisz wykazać, że dzięki temu projekt zbliży się do celu.

Nie przestawaj myśleć o oczekiwaniach użytkowników podczas realizacji
projektu.
Niejednokrotnie przekonaliśmy się, że w miarę poszerzania się naszej wiedza
o dziedzinie, byliśmy w stanie udzielać bardziej trafnych sugestii na temat innych
rzeczy, które można zrobić, aby rozwiązać problemy biznesowe. Jesteśmy przekonani, że programiści, którzy poznają wiele różnych aspektów organizacji, często
potrafią zauważyć sposoby interakcji pomiędzy różnymi obszarami działalności
przedsiębiorstwa, które nie zawsze są oczywiste dla pojedynczych działów.
WSKAZÓWKA NR 96
Nie ograniczaj się do dostarczania kodu. Staraj się wprawić użytkowników
w zachwyt.
Jeśli chcesz zachwycić swoich klientów, ukształtuj relacje z nimi w taki sposób,
abyś mógł aktywnie pomagać w rozwiązywaniu ich problemów. Pomimo, że
chociaż Twoje stanowisko nosi nazwę „programista” lub „inżynier oprogramowania”, to w rzeczywistości powinno ono nazywać się „specjalista ds. rozwiązywania problemów”. Wszyscy rozwiązujemy problemy — na tym polega
istota pragmatycznego programisty.
Rozwiązywanie problemów to podstawa naszej działalności.
Pokrewne podrozdziały

Temat 12., „Pociski smugowe”.

Temat 13., „Prototypy i karteczki samoprzylepne”.

Temat 45., „Kopalnia wymagań”.
4337ebf6db5c7cc89e4173803ef3875a
4
324 
53
40
Rozdział 9.
Pragmatyczne projekty
Duma i uprzedzenie
Zachwycałeś nas dostatecznie długo.
Jane Austen, Duma i uprzedzenie
Pragmatyczni programiści nie unikają odpowiedzialności. Przyjmowanie nowych
wyzwań i szerzenie wiedzy o naszych dokonaniach jest dla nas raczej źródłem
satysfakcji. Jeśli odpowiadamy za jakiś projekt lub fragment kodu, wykonujemy
pracę, z której możemy być naprawdę dumni.
WSKAZÓWKA NR 97
Podpisuj efekty swojej pracy.
Rzemieślnicy zawsze byli dumni ze swojej pracy. Także my mamy powody do
dumy.
Zespoły projektowe składają się jednak z ludzi, co znacznie utrudnia stosowanie tych reguł. W przypadku niektórych projektów koncepcja własności kodu
może utrudniać współpracę. Ludzie mogą traktować pewne obszary systemu
jako własne lub niechętnie angażować się w prace nad wspólnymi, podstawowymi rozwiązaniami. Produkt opracowany w ramach takiego projektu może
przypominać zbiór odrębnych, małych parceli. Z czasem zaczynamy traktować
swój kod z dużo większą wyrozumiałością niż dzieło naszych współpracowników.
Nie tego chcemy. Nie powinniśmy zazdrośnie strzec swojego kodu przed intruzami; podobnie, powinniśmy traktować cudzy kod z należytym respektem.
Kluczem do realizacji tych założeń jest zasada „nie czyń drugiemu, co tobie
niemiłe” oraz zwykły szacunek do innych programistów.
Anonimowość, szczególnie w przypadku wielkich projektów, może stanowić
wyjątkowo podatny grunt dla niechlujstwa, błędów, lenistwa i złego kodu. Zbyt
łatwo akceptujemy rolę drobnego trybiku większej maszyny, wymyślając żałosne
wymówki i stale bez końca pracując nad bezwartościowymi raportami, zamiast
tworzyć dobry kod.
Kod musi co prawda mieć jakiegoś właściciela, ale nie musi to być jedna osoba.
Na przykład wyjątkowo udana metodyka programowania ekstremalnego (XP)
autorstwa Kenta Becka10 zaleca dzielenie odpowiedzialności za kod pomiędzy
wielu autorów (taki model wymaga jednak stosowania dodatkowych praktyk,
jak programowanie w parach, które dodatkowo zabezpieczają projekt przed
niekorzystnymi skutkami anonimowości).
Chcemy, aby własność była źródłem dumy. „Napisałem ten kod i stoję murem
za efektami swojej pracy”. Nasz podpis powinien być rozpoznawany jako znak
10
http://www.extremeprogramming.org
4337ebf6db5c7cc89e4173803ef3875a
4
Duma i uprzedzenie

325
jakości. Ludzie powinni widzieć nasze nazwisko pod fragmentem kodu i traktować je jako sygnał, że mają do czynienia z solidną, dobrze napisaną, przetestowaną i udokumentowaną pracą. Naprawdę profesjonalną robotą. Dziełem
prawdziwego profesjonalisty.
Pragmatycznego programisty.
Dziękujemy.
4337ebf6db5c7cc89e4173803ef3875a
4
W dłuższej perspektywie kształtujemy nasze życie i kształtujemy
siebie. Proces nigdy się nie kończy — trwa dopóki nie umrzemy.
Wybory, których dokonujemy, to nasza własna odpowiedzialność.
Eleanor Roosevelt
Posłowie
W ciągu dwudziestu lat, które doprowadziły do powstania pierwszego wydania
tej książki, byliśmy świadkami komputerowej ewolucji; w części jej inspiracją
była ciekawość, a w części konieczność spełnienia potrzeb biznesowych. W ciągu
dwudziestu lat od tamtego czasu oprogramowanie rozwinęło się — wykroczyło
poza zakres zwykłych maszyn biurowych i rzeczywiście zawładnęło światem.
Ale co to tak naprawdę oznacza dla nas?
W książce Mythical Man-Month: Essays on Software Engineering [Bro96] Fred
Brooks napisał: „Programista jest jak poeta, tworzy tylko wtedy, gdy jest nieznacznie odsunięty od racjonalnego świata. Buduje swoje zamki w powietrzu
i z powietrza — tworzy wysiłkiem własnej wyobraźni”. Zaczynamy od pustej
kartki — możemy stworzyć prawie wszystko, co można sobie wyobrazić. To,
co stworzymy, może zmienić świat.
Począwszy od Twittera, który pomaga ludziom planować rewolucje, poprzez
pracujące w samochodach procesory, które pozwalają zapobiec poślizgom, do
smartfonów, dzięki którym nie musimy już pamiętać szczegółów planu dnia —
nasze programy są wszędzie. Nasza wyobraźnia jest wszędzie.
Programiści to bardzo uprzywilejowana grupa. Naprawdę budujemy przyszłość.
To niezwykła moc. Ale wiąże się ona z wielką odpowiedzialnością.
Jak często zatrzymujemy się, żeby o tym pomyśleć? Jak często rozmawiamy —
zarówno między sobą, jak i w szerszym gronie — co to oznacza?
4337ebf6db5c7cc89e4173803ef3875a
4
Posłowie
 327
W urządzeniach wbudowanych stosuje się komputery w liczbie o rząd wielkości
większej w porównaniu z tymi stosowanymi w laptopach, komputerach desktop
i centrach danych. Te wbudowane komputery często zarządzają systemami
kluczowymi dla naszego życia — od elektrowni, poprzez samochody, aż do
sprzętu medycznego. Nawet prosty system sterowania centralnym ogrzewaniem
lub urządzenie domowe może kogoś zabić, jeśli będzie źle zaprojektowane lub
zainstalowane. Gdy opracowujesz oprogramowanie dla tych urządzeń, bierzesz
na siebie wielką odpowiedzialność.
Wiele niewbudowanych systemów może również zarówno czynić wielkie dobro,
jak i wyrządzać wielkie szkody. Media społecznościowe mogą promować pokojową
rewolucję lub propagować nienawiść. Techniki Big Data ułatwiają robienie
zakupów, ale przyczyniają się także do zniszczenia każdego śladu prywatności,
który — jak nam się wydaje — jeszcze nam pozostał. Systemy bankowe podejmują decyzje kredytowe, które zmieniają ludziom życie. I niemal każdy system
może być wykorzystany do szpiegowania użytkowników.
Możemy sobie wyobrazić utopijną przyszłość i znaleźć przykłady niezamierzonych konsekwencji prowadzących do koszmaru. Różnica pomiędzy tymi dwoma
wynikami może być bardziej subtelna niż myślisz. Wszystko w Twoich rękach.
Kompas moralny
Ceną za tę niespodziewaną władzę jest konieczność zachowania czujności.
Nasze działania bezpośrednio wpływają na ludzi. Nie ma już tworzonych w garażu
hobbystycznych programów na 8-bitowe procesory, wyizolowanych wsadowych
procesów biznesowych na komputerach mainframe w centrach obliczeniowych
lub wykonywanych w izolacji na komputerze stacjonarnym. Całe nasze oprogramowanie tka tę samą tkaninę codziennego współczesnego życia.
Mamy obowiązek zadać sobie dwa pytania o każdy fragment kodu, który dostarczamy:
1. Czy zabezpieczyliśmy użytkowników?
2. Czy sam będę korzystać ze swojego programu?
Po pierwsze należy zadać pytanie: „Czy zrobiłem wszystko, aby zabezpieczyć
użytkowników tego kodu przed szkodami?”. Czy zapewniłem mechanizmy instalacji bieżących poprawek zabezpieczeń dla tego prostego programu monitorującego dzieci? Czy dałem użytkownikowi możliwość ręcznego sterowania automatycznym termostatem centralnego ogrzewania? Czy przechowuję tylko te dane,
które są mi potrzebne, i szyfruję wszystkie dane osobowe?
Nikt nie jest doskonały; każdy czasem o czymś zapomina. Ale jeśli nie możesz
zgodnie z prawdą powiedzieć, że starałeś się znaleźć wszystkie konsekwencje
i zadbać o ochronę przed nimi użytkowników, to poniesiesz odpowiedzialność,
gdy coś pójdzie źle.
4337ebf6db5c7cc89e4173803ef3875a
4
328 
Posłowie
WSKAZÓWKA NR 98
Po pierwsze — nie szkodzić.
Po drugie należy przeprowadzić ocenę spełnienia złotej reguły: czy byłbym zadowolony, gdybym był użytkownikiem tego oprogramowania? Czy chcę udostępniać moje dane? Czy życzę sobie, żeby to, co robię, zostało przekazane do sklepów
online? Czy byłbym zadowolonym pasażerem tego autonomicznego pojazdu? Czy
czułbym się z w nim komfortowo?
Niektóre odkrywcze idee zaczynają wykraczać poza granice postępowania etycznego. Jeśli bierzesz udział w takim projekcie, jesteś za to tak samo odpowiedzialny, jak sponsorzy.
Bez względu na liczbę stopni separacji, które potrafisz uzasadnić, jedna zasada
pozostaje prawdą:
WSKAZÓWKA NR 99
Nie pozwalaj na łajdactwo.
Wyobraź sobie wymarzoną przyszłość
Wszystko zależy od Ciebie. To Twoja wyobraźnia, Twoje nadzieje, Twoje obawy
przyczynią się do powstania przemyślanych programów, które zbudują następne
dwadzieścia lat i dalszą przyszłość.
Budujesz przyszłość dla siebie i dla swoich potomków. Twoim zadaniem jest
zadbanie o to, aby była to taka przyszłość, jakiej wszyscy moglibyśmy sobie
życzyć. Rozpoznaj sytuacje, gdy robisz coś przeciwko temu ideałowi i miej
odwagę powiedzieć „nie!”. Przewiduj przyszłość, jaką możemy mieć, i miej odwagę,
aby ją tworzyć. Codziennie buduj zamki w powietrzu.
Wszyscy mamy wspaniałe życie.
WSKAZÓWKA NR 100
To jest Twoje życie.
Dziel się nim. Świętuj je. Buduj.
I MIEJ Z TEGO PRZYJEMNOŚĆ!
4337ebf6db5c7cc89e4173803ef3875a
4
Bibliografia
[ADSS18] Antinyan Vard, Derehag Jesper, Sandberg Anna, Staron Miroslaw,
Mythical Unit Test Coverage, „IEEE Software” 2018, no. 35, s. 73–79.
[And10] Andrade Jackie, What does doodling do?, „Applied Cognitive Psychology”
2010, vol. 24(1), s. 100–106.
[Arm07] Armstrong Joe, Programming Erlang: Software for a Concurrent World,
The Pragmatic Bookshelf, Raleigh 2007.
[Bec98] Becker Gavin de, The Gift of Fear: And Other Survival Signals That
Protect Us from Violence, Dell Publishing, New York 1998.
[BR89] Bernstein Albert J., Rozen Sydney Craft, Dinosaur Brains: Dealing with
All Those Impossible People at Work, John Wiley & Sons, New York 1989.
[Bro96] Brooks Jr. Frederick P., The Mythical Man-Month: Essays on Software
Engineering, Addison-Wesley, Reading 1996 [wydanie polskie: Legendarny
osobomiesiąc. Opowieści o inżynierii oprogramowania, Helion, Gliwice 2019].
[CN91] Cox Brad J., Novobilski Andrew J., Object-Oriented Programming: An
Evolutionary Approach, Addison-Wesley, Reading 1991.
[Con68] Conway Melvin E., How do Committees Invent?, „Datamation” 1968,
vol. 14(5), s. 28–31.
[DL13] DeMacro Tom, Lister Tim, Peopleware: Productive Projects and Teams,
Addison-Wesley, Boston 2013.
[Fow00] Fowler Martin, UML Distilled: A Brief Guide to the Standard Object
Modeling Language, Addison-Wesley, Boston 2000.
[Fow04] Fowler Martin, UML Distilled: A Brief Guide to the Standard Object
Modeling Language, Addison-Wesley, Boston 2004.
4337ebf6db5c7cc89e4173803ef3875a
4
330 
Bibliografia
[Fow19] Martin Fowler, Refactoring: Improving the Design of Existing Code.
Addison-Wesley, Boston 2019 [wydanie polskie: Refaktoryzacja. Ulepszanie
struktury istniejącego kodu, Helion, Gliwice 2011].
[GHJV95] Gamma Erich, Helm Richard, Johnson Ralph, Vlissides John, Design
Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley,
Reading 1995 [wydanie polskie: Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku, Helion, Gliwice 2010].
[Hol92] Holt Michael, Math Puzzles & Games, Dorset House, New York 1992.
[Hun08] Hunt Andy, Pragmatic Thinking and Learning: Refactor Your Wetware,
The Pragmatic Bookshelf, Raleigh 2008.
[Joi94] Joiner T.E., Contagious depression: Existence, specificity to depressed
symptoms, and the role of reassurance seeking, „Journal of Personality and
Social Psychology” 1994, vol. 67(2), s. 287–296.
[Knu11] Knuth Donald E., The Art of Computer Programming. Volume 4A: Combinatorial Algorithms, Part 1, Addison-Wesley, Boston 2011.
[Knu98a] Knuth Donald E., The Art of Computer Programming. Volume 1: Fundamental Algorithms, Addison-Wesley, Reading 1998 [wydanie polskie: Sztuka
programowania. Tom I: Algorytmy podstawowe, Wydawnictwa NaukowoTechniczne, Warszawa 2002].
[Knu98b] Knuth Donald E., The Art of Computer Programming. Volume 2:
Seminumerical Algorithms, Addison-Wesley, Reading 1998 [wydanie polskie:
Sztuka programowania. Tom II: Algorytmy seminumeryczne, Wydawnictwa
Nukaowo-Techniczne, Warszawa 2002].
[Knu98c] Knuth Donald E., The Art of Computer Programming. Volume 3: Sorting
and Searching, Addison-Wesley, Reading 1998 [wydanie polskie: Sztuka
programowania. Tom III: Sortowanie i wyszukiwanie, Wydawnictwa
Naukowo-Techniczne, Warszawa 2002].
[KP99] Kernighan Brian W., Pike Rob, The Practice of Programming, Addison-Wesley, Reading 1999 [wydanie polskie: Lekcja programowania. Najlepsze
praktyki, Helion, Gliwice 2011].
[LH89] Lieberherr Karl J., Holland Ian, Assuring good style for object-oriented
programs, „IEEE Software” 1989, vol. 6, s. 38–48.
[Mey97] Meyer Bertrand, Object-Oriented Software Construction, Prentice Hall,
Upper Saddle River 1997 [wydanie polskie: Programowanie zorientowane
obiektowo, Helion, Gliwice 2005].
[Mul18] Muller Jerry Z., The Tyranny of Metrics, Princeton University Press,
Princeton 2018.
4337ebf6db5c7cc89e4173803ef3875a
4
Możliwe odpowiedzi do ćwiczeń

331
[SF13] Sedgewick Robert, Flajolet Phillipe, An Introduction to the Analysis of
Algorithms, Addison-Wesley, Boston 2013.
[Str35] Stroop James Ridley, Studies of Interference in Serial Verbal Reactions,
„Journal of Experimental Psychology” 1935, no. 18, s. 643–662.
[SW11] Sedgewick Robert, Wayne Kevin, Algorithms, Addison-Wesley, Boston
2011 [wydanie polskie: Algorytmy, Helion, Gliwice 2011].
[Tal10] Taleb Nassim Nicholas, The Black Swan: Second Edition: The Impact
of the Highly Improbable. Random House, New York 2010 [wydanie polskie:
Czarny łabędź. Jak nieprzewidywalne zdarzenia rządzą naszym życiem,
Zysk i S-ka, Poznań 2020].
[WH82] Wilson James Q., Helling George, The police and neighborhood safety,
„The Atlantic Monthly” 1982, vol. 249(3), s. 29–38.
[YC79] Yourdon Edward, Constantine Larry L., Structured Design: Fundamentals of a Discipline of Computer Program and Systems Design, Prentice
Hall, Englewood Cliffs 1979.
[You95] Yourdon Edward, When good-enough software is best, „IEEE Software”
1995, vol. 12(3), s. 79–81.
Wolałbym mieć pytania, na które nie można odpowiedzieć
niż odpowiedzi, dla których nie da się postawić pytań.
Richard Feynman
36
Możliwe odpowiedzi do ćwiczeń
Odpowiedź do ćwiczenia 1.
W naszej ocenie klasa Split2 jest bardziej ortogonalna. Klasa Split2 koncentruje się na konkretnym zadaniu (podziału wierszy) i ignoruje takie szczegóły
jak źródło, z którego pochodzą te wiersze. Takie rozwiązanie nie tylko ułatwia
opracowanie kodu, ale; też czyni program bardziej elastycznym. Klasa Split2
może dzielić wiersze odczytywane z pliku, generowane przez inną procedurę lub
przekazywane za pośrednictwem środowiska.
4337ebf6db5c7cc89e4173803ef3875a
4
332 
Bibliografia
Odpowiedź do ćwiczenia 2.
Zacznijmy od twierdzenia: można napisać dobry, ortogonalny kod w niemal
każdym języku. Jednocześnie w każdym języku istnieją pokusy: własności, które
mogą prowadzić do zwiększenia sprzężeń i zmniejszenia ortogonalności.
W językach obiektowych takie konstrukcje jak dziedziczenie wielokrotne, wyjątki,
przeciążanie operatorów czy przykrywanie metod klas bazowych (przez podklasy) stwarzają wystarczająco dużo okazji do tworzenia zbędnych, nie zawsze
oczywistych sprzężeń. Istnieje również rodzaj sprzężenia wynikający z powiązania przez klasę kodu z danymi. Jest to zazwyczaj dobre (gdy sprzężenie jest
dobre, nazywamy je spójnością — ang. cohesion). Ale jeśli klasy nie będą odpowiednio skoncentrowane, powstałe za ich pomocą interfejsy mogą być bardzo
brzydkie.
Języki funkcyjne zachęcają do pisania wielu niewielkich, niezależnych od siebie
funkcji i łączenia ich na różne sposoby w celu rozwiązania konkretnego problemu. W teorii to brzmi dobrze. W praktyce często także jest dobre. Ale istnieje
rodzaj sprzężenia, które może wystąpić również w językach funkcyjnych.
Funkcje zazwyczaj przekształcają dane, co oznacza, że wyjście jednej funkcji
może stać się wejściem drugiej. Jeśli nie zachowamy odpowiedniej ostrożności,
to wprowadzenie zmian w formacie danych generowanych przez jedną z funkcji
może doprowadzić do awarii w jakimś miejscu strumienia transformacji.
W złagodzeniu tego problemu może pomóc zastosowanie języków z dobrymi systemami typowania.
Odpowiedź do ćwiczenia 3.
Ratunkiem jest niska technologia! Warto narysować pisakiem kilka kształtów
na białej tablicy — samochód, telefon i dom. Nasze rysunki nie muszą być
dziełem sztuki — proste szkice w zupełności wystarczą. Należy teraz umieścić
na tablicy (w miejscach reprezentujących obszary klikania) karteczki samoprzylepne opisujące zawartość docelowych stron. W trakcie spotkania możemy
stopniowo doskonalić rysunki i rozmieszczenie karteczek.
Odpowiedź do ćwiczenia 4.
Ponieważ chcemy, aby nasz język był rozszerzalny, stworzymy analizator
składniowy sterowany przez tabelę. Każdy wpis w tej tabeli zawiera literę polecenia, flagę określającą, czy jest wymagany jakiś argument, oraz nazwę procedury wywoływanej w celu obsłużenia danego polecenia.
lang/turtle.c
typedef struct {
char cmd;
/* litera polecenia */
int hasArg;
/* czy wymaga jakiegoś argumentu */
void (*func)(int, int); /* wywoływana funkcja */
} Command;
static Command cmds[] = {
4337ebf6db5c7cc89e4173803ef3875a
4
Możliwe odpowiedzi do ćwiczeń
{
{
{
{
{
{
{
'P',
'U',
'D',
'N',
'E',
'S',
'W',
ARG,
NO_ARG,
NO_ARG,
ARG,
ARG,
ARG,
ARG,

333
doSelectPen },
doPenUp },
doPenDown },
doPenDir },
doPenDir },
doPenDir },
doPenDir }
};
Główny program jest dość prosty: czytanie wiersza, poszukiwanie komendy,
pobranie argumentu, jeśli jest potrzebny, a następnie wywołanie procedury
obsługi.
lang/turtle.c
while (fgets(buff, sizeof(buff), stdin)) {
Command *cmd = findCommand(*buff);
if (cmd) {
int arg = 0;
if (cmd->hasArg && !getArg(buff+1, &arg)) {
fprintf(stderr, "'%c' wymaga argumentu\n", *buff);
continue;
}
cmd->func(*buff, arg);
}
}
Funkcja szukająca polecenia liniowo przeszukuje naszą tabelę i zwraca albo
pasujący wpis, albo wartość NULL.
lang/turtle.c
Command *findCommand(int cmd) {
int i;
for (i = 0; i < ARRAY_SIZE(cmds); i++) {
if (cmds[i].cmd == cmd)
return cmds + i;
}
fprintf(stderr, "Nieznane polecenie '%c'\n", cmd);
return 0;
}
I wreszcie, odczytanie liczbowego argumentu wymaga prostego wywołania
funkcji scanf.
lang/turtle.c
int getArg(const char *buff, int *result) {
return sscanf(buff, "%d", result) == 1;
}
Odpowiedź do ćwiczenia 5.
W zasadzie już rozwiązaliśmy ten problem w poprzednim ćwiczeniu, w którym
napisaliśmy interpreter języka zewnętrznego, który będzie zawierać wewnętrzny
interpreter. W przypadku naszego przykładowego kodu są to funkcje doXxx.
4337ebf6db5c7cc89e4173803ef3875a
4
334 
Bibliografia
Odpowiedź do ćwiczenia 6.
Odpowiednia specyfikacja w notacji Backusa-Naura (BNF) mogłaby mieć następującą postać:
<time>
::= <hour> <ampm> |
<hour> : <minute> <ampm> |
<hour> : <minute>
<ampm>
::= am | pm
<hour > ::= <digit> |
<digit> <digit>
<minute> ::= <digit> <digit>
<digit> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
Lepsze definicje godziny i minuty powinny uwzględniać fakt, że godzina może
mieć wartość z przedziału od 00 do 23, a minuta od 00 do 59:
hour ::= h-tens digit | digit
minute ::= m-tens digit
h-tens ::= 0 | 1
m-tens ::= 0 | 1 | 2 | 3 | 4 | 5
digit ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
Odpowiedź do ćwiczenia 7.
Oto parser napisany przy użyciu biblioteki Pegjs dla języka JavaScript:
lang/peg_parser/time_parser.pegjs
time
= h:hour offset:ampm { return h + offset }
/ h:hour ":" m:minute offset:ampm { return h + m + offset }
/ h:hour ":" m:minute { return h + m }
ampm
= "am" { return 0 }
/ "pm" { return 12*60 }
hour
= h:two_hour_digits { return h*60 }
/ h:digit { return h*60 }
minute
= d1:[0-5] d2:[0-9] { return parseInt(d1+d2, 10); }
digit
= digit:[0-9] { return parseInt(digit, 10); }
two_hour_digits
= d1:[01] d2:[0-9 ] { return parseInt(d1+d2, 10); }
/ d1:[2] d2:[0-3] { return parseInt(d1+d2, 10); }
Oto przykładowe użycie tego parsera w testach:
lang/peg_parser/test_time_parser.js
let test = require('tape');
let time_parser = require('./time_parser.js');
// time ::= hour ampm |
// hour : minute ampm |
// hour : minute
//
// ampm ::= am | pm
4337ebf6db5c7cc89e4173803ef3875a
4
Możliwe odpowiedzi do ćwiczeń
//
// hour ::= digit | digit digit
//
// minute ::= digit digit
//
// digit ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
const
const
const
const
h = (val) => val*60;
m = (val) => val;
am = (val) => val;
pm = (val) => val + h(12);
let tests = {
"1am": h(1),
"1pm": pm(h(1)),
"2:30": h(2) + m(30),
"14:30": pm(h(2)) + m(30),
"2:30pm": pm(h(2)) + m(30),
}
test('time parsing', function (t) {
for (const string in tests) {
let result = time_parser.parse(string)
t.equal(result, tests[string], string);
}
t.end()
});
Odpowiedź do ćwiczenia 8.
Oto możliwe rozwiązanie w Ruby:
lang/re_parser/time_parser.rb
TIME_RE = %r{
(?<digit>[0-9]){0}
(?<h_ten>[0-1]){0}
(?<m_ten>[0-6]){0}
(?<ampm> am | pm){0}
(?<hour> (\g<h_ten> \g<digit>) | \g<digit>){0}
(?<minute> \g<m_ten> \g<digit>){0}
\A(
( \g<hour> \g<ampm> )
| ( \g<hour> : \g<minute> \g<ampm> )
| ( \g<hour> : \g<minute> )
)\Z
}x
def parse_time(string)
result = TIME_RE.match(string)
if result
result[:hour].to_i * 60 +
(result[:minute] || "0").to_i +
(result[:ampm] == "pm" ? 12*60 : 0)
end
end
4337ebf6db5c7cc89e4173803ef3875a

335
4
336 
Bibliografia
(W tym kodzie wykorzystano sztuczkę polegającą na zdefiniowaniu nazwanych
wzorców na początku wyrażenia regularnego, a następnie odwołaniu się do nich
jako podwzorców w faktycznym dopasowaniu).
Odpowiedź do ćwiczenia 9.
Naszą odpowiedź należy obwarować wieloma założeniami:

Urządzenie pamięci masowej zawiera informacje, które mają zostać przeniesione.

Znamy prędkość, z jaką porusza się osoba przenosząca urządzenie.

Znamy odległość dzielącą oba komputery.

Nie uwzględniamy czasu potrzebnego do skopiowania informacji na urządzenie pamięci masowej i z niego.

Koszty kopiowania danych na taśmę są zbliżone do kosztów wysyłania tych
danych za pośrednictwem linii komunikacyjnej.
Odpowiedź do ćwiczenia 10.
Należy tutaj zastosować te same zastrzeżenia co w odpowiedzi 9. Taśma
o pojemności 1 TB zawiera 8×240 bitów, zatem linia o przepustowości 1 Gb/s
pozwoliłaby przesłać podobną ilość danych w czasie 9000 sekund, czyli w przybliżeniu w ciągu 2,5 godziny. Jeśli człowiek pokonuje średnio 5 km w ciągu
godziny, oba komputery musiałyby być oddalone o co najmniej 12,5 kilometra,
aby linia komunikacyjna zapewniała wyższą wydajność niż kurier. W przeciwnym razie człowiek niosący taśmę wygrywa.
Odpowiedź do ćwiczenia 14.
Sygnatury funkcji zaprezentujemy w Javie. Warunki wstępne i końcowe umieścimy w komentarzach.
Po pierwsze tworzymy niezmiennik dla klasy:
/**
* @invariant getSpeed() > 0
* implies isFull()
*
* @invariant getSpeed() >= 0 &&
* getSpeed() < 10
*/
// Nie uruchamiaj, jeśli pusty
// Sprawdzenie zakresu
Następnie określamy warunki wstępne i końcowe:
/**
* @pre Math.abs(getSpeed() - x) <= 1 // Zmiana tylko o jeden
* @pre x >= 0 && x < 10
// Sprawdzenie zakresu
* @post getSpeed() == x
// Uwzględnienie żądanej szybkości
*/
public void setSpeed(final int x)
4337ebf6db5c7cc89e4173803ef3875a
4
Możliwe odpowiedzi do ćwiczeń

337
/**
* @pre !isFull()
// Nie wypełniaj dwa razy
* @post isFull()
// Sprawdź, czy operację wykonano
*/
void fill()
/**
* @pre isFull() // Nie opróżniaj dwukrotnie
* @post !isFull() // Sprawdź, czy operację wykonano
*/
void empty()
Odpowiedź do ćwiczenia 15.
Ciąg składa się z 21 wyrazów. Gdybyśmy udzielili odpowiedzi 20, wpadlibyśmy
w pułapkę błędu słupków ogrodzeniowych (nie wiedzielibyśmy, czy liczymy
słupki, czy też przestrzenie pomiędzy słupkami).
Odpowiedź do ćwiczenia 16.

Wrzesień w 1752 roku miał tylko 19 dni. Taki zabieg miał na celu synchronizację kalendarzy w ramach reform gregoriańskich.

Katalog mógł zostać usunięty przez inny proces, możemy nie dysponować
uprawnieniami do jego odczytania, zmienna &sb może być nieprawidłowa —
trudno przewidzieć wszystkie scenariusze.

Celowo nie określiliśmy typów zmiennych a i b. Przeciążanie operatorów
umożliwia zdefiniowanie operatora +, = lub != w sposób powodujący nieoczekiwane zachowania. Co więcej, symbole a i b mogą być aliasami tej
samej zmiennej, zatem drugie przypisanie nadpisze wartość z pierwszego
przypisania.

W geometrii nieeuklidesowej suma miar kątów trójkąta nie jest równa 180º.
Wystarczy wyobrazić sobie trójkąt rzutowany na powierzchnię sfery.

Minuty przestępne mogą mieć 61 lub 62 sekundy.

Przepełnienie może spowodować, że wynik wyrażenia a + 1 będzie ujemny
(w zależności od języka programowania).
Odpowiedź do ćwiczenia 17.
W większości implementacji języków C i C++ nie istnieją mechanizmy do
sprawdzania, czy wskaźnik rzeczywiście wskazuje na prawidłowy obszar pamięci.
Typowym błędem jest zwolnienie bloku pamięci i odwoływanie się do tego
bloku w dalszej części programu. Wskazywana pamięć mogła zostać w międzyczasie przydzielona na potrzeby innych zadań. Programiści mają nadzieję,
że przypisanie takiemu wskaźnikowi wartości NULL zapobiegnie niebezpiecznym
odwołaniom w przyszłości — w większości przypadków próba odwołania do
wskaźnika pustego powoduje błąd czasu wykonywania.
4337ebf6db5c7cc89e4173803ef3875a
4
338 
Bibliografia
Odpowiedź do ćwiczenia 18.
Przypisanie wartości NULL pozwala ograniczyć o jeden liczbę wskaźników do
danego obiektu. Kiedy licznik tych wskaźników osiągnie zero, obiekt może zostać
usunięty z pamięci przez mechanizm odzyskiwania. Przypisywanie wartości
NULL może być szczególnie ważne w przypadku długo działających programów,
gdzie programiści muszą wyeliminować ryzyko zwiększania potrzeb pamięciowych w czasie.
Odpowiedź do ćwiczenia 19.
Oto przykład prostej implementacji:
event/strings_ex_1.rb
class FSM
def initialize(transitions, initial_state)
@transitions = transitions
@state = initial_state
end
def accept(event)
@state, action = TRANSITIONS[@state][event] ||
TRANSITIONS[@state][:default]
end
end
(Pobierz ten plik, aby uzyskać zaktualizowany kod, który wykorzystuje nową
klasę FSM).
Odpowiedź do ćwiczenia 20.

…awaria trzech interfejsów sieciowych w ciągu pięciu minut
Mechanizm ten można zaimplementować za pomocą maszyny stanów,
ale byłoby to trudniejsze, niż może się z pozoru wydawać: jeśli zdarzenia
wystąpią w minutach 1, 4, 7 i 8, to dla czwartego zdarzenia należałoby
zainicjować ostrzeżenie. To oznacza, że maszyna stanów musi zapewnić
możliwość resetowania samej siebie.
Z tego powodu lepszą techniką wydają się strumienie zdarzeń. Należałoby
zaimplementować reaktywną funkcję o nazwie buffer z parametrami size
i offset. Funkcja ta pozwoliłaby zwrócić każdą grupę z trzech nadchodzących zdarzeń. Następnie, aby określić, czy należy wszcząć alarm, można by
przyjrzeć się znacznikom czasu pierwszego i ostatniego zdarzenia w grupie.

…jeśli po zachodzie słońca wykryjesz ruch na dole schodów, po czym
wykryjesz ruch u szczytu schodów…
Ten mechanizm można by prawdopodobnie zaimplementować za pomocą
kombinacji wzorca pubsub i maszyny stanów. Wzorca pubsub można użyć
do rozpowszechniania zdarzeń do dowolnej liczby maszyn stanów, a następnie maszyny stanów mogłyby zdecydować, co należy zrobić.

…powiadom różne systemy raportowania, że obsługa zamówienia została
zakończona.
4337ebf6db5c7cc89e4173803ef3875a
4
Możliwe odpowiedzi do ćwiczeń

339
Ten mechanizm prawdopodobnie najlepiej obsłużyć za pomocą wzorca
pubsub. Można by użyć strumieni, ale w takim przypadku powiadamiane
systemy także musiałyby bazować na strumieniach.

…wysłać żądania do trzech usług backend i poczekać na odpowiedzi.
To zadanie jest podobne do przykładu, w którym używaliśmy strumieni
do pobierania danych użytkownika.
Odpowiedź do ćwiczenia 21.
1. Dodanie do zamówienia podatku od wysyłki i podatku VAT.
podstawowe zamówienie
sfinalizowane zamówienie
W konwencjonalnym kodzie najprawdopodobniej zdefiniowalibyśmy jedną
funkcję, obliczającą koszty wysyłki, i drugą, obliczającą podatek. Tutaj
jednak myślimy o transformacjach, więc przekształcimy zamówienie zawierające listę towarów na zamówienie gotowe do wysłania.
2. Aplikacja ładuje informacje o konfiguracji z pliku o podanej nazwie.
nazwa pliku
struktura konfiguracji
3. Logowanie użytkownika do aplikacji webowej.
poświadczenia użytkownika
sesja
Odpowiedź do ćwiczenia 22.
Transformację wysokiego poziomu:
field contents as string
[walidacja i konwersja]
{:ok, value} | {:error, reason}
można podzielić na:
field contents as string
[konwersja ciągu znaków na liczbę integer]
[sprawdzenie value >= 18]
[sprawdzenie value <= 150]
{:ok, value} | {:error, reason}
W rozwiązaniu zakładamy istnienie potoku obsługi błędów.
Odpowiedź do ćwiczenia 23.
Najpierw odpowiemy na drugą część pytania: wolimy pierwszy fragment kodu.
W drugim fragmencie w każdym kroku zwracany jest obiekt, który implementuje następną wywoływaną funkcję: obiekt zwrócony przez content musi implementować find_matching_lines i tak dalej.
Oznacza to, że obiekt zwrócony przez funkcję content_of jest sprzężony z naszym
kodem. Wyobraźmy sobie, że wymaganie się zmieniło i musimy zignorować wiersze zaczynające się od znaku #. W przypadku zastosowania stylu transformacyjnego, spełnienie tego wymagania byłoby proste:
4337ebf6db5c7cc89e4173803ef3875a
4
340 
const
const
const
const
Bibliografia
content = File.read(file_name);
no_comments = remove_comments(content)
lines = find_matching_lines(no_comments, pattern)
result = truncate_lines(lines)
Moglibyśmy nawet zamienić kolejność wywołania funkcji remove_comments i find_
matching_lines, a rozwiązanie nadal by działało.
Natomiast w przypadku zastosowania stylu „łańcuchowego” spełnienie nowego
wymagania będzie trudniejsze. Gdzie powinna znaleźć się metoda remove_comments:
w obiekcie zwróconym przez funkcję content_of, czy w obiekcie zwróconym przez
funkcję find_matching_lines? I jaki inny kod przestanie działać, jeśli zmienimy
ten obiekt? Z powodu tego sprzężenia styl łączenia wywołań metod jest czasami nazywany pociągiem-wrakiem.
Odpowiedź do ćwiczenia 24.

Przetwarzanie obrazów. W celu stworzenia prostego harmonogramu obciążenia pomiędzy równoległymi procesami, bardziej niż wystarczająca może
być wspólna kolejka zadań. Być może, w przypadku gdy istnieje sprzężenie
zwrotne pomiędzy fragmentami kodu — to znaczy, jeżeli wyniki jednego
przetworzonego fragmentu wpływają na inne fragmenty (tak, kawałki, jak
w aplikacjach przetwarzania wizji lub realizujących złożone przekształcenia
osnowy obrazu 3D) — warto rozważyć system typu czarna tablica.

Kalendarze grupowe. System typu czarna tablica może być w tym przypadku dobrym rozwiązaniem. Na tablicy można publikować zaplanowane
spotkania i dostępność. Występują tu podmioty funkcjonujące autonomicznie — informacje zwrotne dotycząc decyzji są ważne, a uczestnicy
mogą przychodzić i odchodzić.
Warto rozważyć podział tego rodzaju systemu typu czarna tablica w zależności od tego, kto realizuje wyszukiwanie: niższy personel może być zainteresowany tylko lokalną siedzibą, dział kadr może być zainteresowany
tylko anglojęzycznymi przedstawicielstwami na całym świecie, a prezes
całością.
Istnieje również pewna elastyczność dotycząca formatów danych: możemy
swobodnie ignorować formaty lub języki, których nie rozumiemy. Musimy
rozumieć różne formaty tylko dla tych placówek, które prowadzą ze sobą
spotkania i nie musimy ujawniać wszystkim użytkownikom pełnego,
tranzytywnego domknięcia wszystkich możliwych formatów. To ogranicza
sprzężenia tylko do tych miejsc, w których są one konieczne, i nie wprowadza sztucznych ograniczeń.

Narzędzie do monitorowania sieci. To zadanie jest bardzo podobne do
programu do obsługi kredytu hipotecznego z podrozdziału „Czarna tablica
w praktyce”. Zgłaszane przez użytkowników raporty dotyczące usterek
sieci oraz automatycznie generowane statystyki są umieszczane na tablicy.
W celu zdiagnozowania awarii sieci tablicę może analizować człowiek lub
agent programowy: dwa błędy z rzędu mogą być spowodowane czymś
4337ebf6db5c7cc89e4173803ef3875a
4
Możliwe odpowiedzi do ćwiczeń

341
równie ulotnym, jak promieniowanie kosmiczne, ale wystąpienie 20 000
błędów świadczy o problemie sprzętowym. Podobnie jak podczas pracy wielu
detektywów, którzy rozwiązują tajemnicę morderstwa, możemy mieć wiele
podmiotów, które dokonują analizy i przedstawiają pomysły dotyczące
tego, jak rozwiązać problemy z siecią.
Odpowiedź do ćwiczenia 25.
W przypadku list par klucz-wartość ogólnie rzecz biorąc zakłada się, że klucz
jest unikatowy. Wymuszają to zazwyczaj biblioteki obsługi tablic asocjacyjnych — albo przez zachowanie samej tablicy asocjacyjnej, albo poprzez jawne
generowanie komunikatów o błędach w przypadku zdublowanych kluczy. Jednak
zwykła tablica zazwyczaj nie ma takich ograniczeń i bezproblemowo zaakceptuje przechowywanie zdublowanych kluczy, chyba że specjalnie obsłużymy
tę sytuację w kodzie. Zatem w tym przypadku znaleziony zostanie pierwszy klucz,
który pasuje do klucza DepositAccount, a wszystkie pozostałe pasujące elementy
będą zignorowane. Kolejność elementów nie jest gwarantowana, więc czasami
rozwiązanie będzie działać, a czasami nie.
A z czego wynika różnica w działaniu kodu na maszynie programistycznej
i produkcyjnej? To zwykły przypadek.
Odpowiedź do ćwiczenia 26.
Fakt, że czysto numeryczne pole działa w Stanach Zjednoczonych, Kanadzie
i na Karaibach, to przypadek. Zgodnie ze specyfikacją ITU numery międzynarodowe rozpoczynają się od znaku +. W niektórych lokalizacjach jest również
stosowany znak *, a bardziej powszechną częścią numeru mogą być wiodące
zera. Nigdy nie należy przechowywać numerów telefonów w polu numerycznym.
Odpowiedź do ćwiczenia 27.
Wszystko zależy od tego, gdzie jesteś. W Stanach Zjednoczonych powszechną
miarą objętości jest galon, czyli objętość cylindra o wysokości 6 cali i 7 calach
średnicy, zaokrąglona do całkowitej wartości cali sześciennych.
W Kanadzie „jedna szklanka” w przepisie może oznaczać dowolną z poniższych
wartości:

1/5 imperialnej kwarty, czyli 227 ml,

1/4 amerykańskiej kwarty, czyli 236 ml,

16 metrycznych łyżek stołowych, czyli 240 ml,

1/4 litra, czyli 250 ml
Wyjątkiem są przepisy dotyczące gotowania ryżu; w tym przypadku „jedna
szklanka” oznacza 180 ml. Wynika to z tzw. koku — oznaczającego szacowaną
objętość suchego ryżu potrzebną do nakarmienia jednej osoby przez okres
4337ebf6db5c7cc89e4173803ef3875a
4
342 
Bibliografia
jednego roku: wynosi to około 180 litrów. Szklanka ryżu to 1 gō, czyli 1/1000
z koku. Jest to mniej więcej taka ilość ryżu, jaką jedna osoba zjada w jednym
posiłku1.
Odpowiedź do ćwiczenia 28.
Nie istnieje, oczywiście, jedno rozwiązanie tego ćwiczenia. Możemy jednak
sformułować kilka wskazówek.
Jeśli odkryjemy, że nasze wyniki nie tworzą na wykresie gładkiej krzywej, być
może powinniśmy sprawdzić, czy jakieś inne działania nie zajmują mocy obliczeniowej procesora. Prawdopodobnie nie będziemy w stanie zgromadzić wiarygodnych statystyk w systemie wielu użytkowników ani nawet w systemie
używanym tylko przez nas, jeśli jakieś procesy działające w tle będą cyklicznie
zajmowały czas procesora. Warto też sprawdzić poziom wykorzystania pamięci —
jeśli monitorowana aplikacja zacznie używać przestrzeni wymiany, jej wydajność
drastycznie spadnie.
Oto wykres z wynikami uruchomienia kodu na jednej z naszych maszyn:
1
Za tę ciekawostkę dziękujemy Aviemu Bryantowi (@avibryant).
4337ebf6db5c7cc89e4173803ef3875a
4
Możliwe odpowiedzi do ćwiczeń

343
Odpowiedź do ćwiczenia 29.
Istnieje kilka sposobów na przeprowadzenie tego dowodu. Jednym z nich jest
próba rozwiązania tego problemu w pamięci. Jeśli tablica ma tylko jeden
element, nie jest potrzebne iterowanie w pętli. Każda dodatkowa iteracja podwaja
rozmiar tablicy, którą możemy przeszukiwać. Dlatego ogólny wzór na rozmiar
tablicy ma postać n = 2m, gdzie m oznacza liczbę iteracji. Jeśli obliczymy logarytmy o podstawie 2 dla każdej strony, otrzymamy lg n = lg2m, co z definicji
logarytmu jest równoważne z lg n = m.
Odpowiedź do ćwiczenia 30.
Trzeba przypomnieć sobie matematykę ze szkoły średniej. Wzór konwersji logarytmu o podstawie a na logarytm o podstawie b ma postać:
log b x 
log a x
log a b
Ponieważ logab jest stałą, to możemy zignorować ten wyraz wewnątrz wyniku
Wielkie O.
Odpowiedź do ćwiczenia 31.
Jedną z właściwości, którą możemy przetestować, jest poprawność zamówienia
w sytuacji, gdy w magazynie jest wystarczająca ilość towaru. Możemy wygenerować zamówienia dla przypadkowych ilości elementów i sprawdzić, czy
system zwraca krotkę "OK", jeśli w magazynie jest odpowiednia ilość towaru.
Odpowiedź do ćwiczenia 32.
Jest to dobre zastosowanie dla testów właściwości. Testy jednostkowe mogą
skupić się na indywidualnych przypadkach, w których wyniki obliczyliśmy
za pomocą innych mechanizmów, natomiast testy właściwości mogą skupić się
na następujących elementach:

Czy lokalizacja dowolnych dwóch skrzyń pokrywa się?

Czy jakakolwiek część dowolnej ze skrzyń przekracza szerokość lub długość
ciężarówki?

Czy gęstość upakowania (powierzchnia zajmowana przez skrzynie podzielona przez powierzchnię platformy ciężarówki) jest mniejsza lub równa 1?

Jeśli gęstość upakowania należy do wymagań, to można również sprawdzić,
czy przekracza ona minimalną dopuszczalną gęstość.
Odpowiedź do ćwiczenia 33.
1. To zdanie brzmi jak prawdziwe wymaganie: mogą przecież istnieć ograniczenia nakładane na aplikację przez jej środowisko działania.
4337ebf6db5c7cc89e4173803ef3875a
4
344 
Bibliografia
2. Samo w sobie to zdanie nie jest wymaganiem. Aby dowiedzieć się, co jest
wymagane, należy sobie zadać magiczne pytanie: „Dlaczego?”.
Może to być standard korporacyjny — wtedy wymaganie mogłoby brzmieć:
„Wszystkie elementy interfejsu użytkownika muszą być zgodne ze standardem korporacyjnym V12.76”.
Być może ten kolor szczególnie podoba się członkom zespołu projektowego.
W takim przypadku trzeba uwzględnić fakt, że członkowie zespołu lubią
zmieniać zdanie. Wtedy należałoby sformułować wymaganie jako: „Kolor tła
wszystkich okien modalnych musi być konfigurowalny. Domyślnie będzie
wyświetlany kolor szary”. Jeszcze lepsze byłoby bardziej ogólne sformułowanie: „Użytkownik końcowy musi mieć możliwość konfiguracji wszystkich
elementów wizualnych aplikacji (kolorów, czcionek i języków)”.
Wymaganie może też po prostu wskazywać, że użytkownik musi mieć
możliwość odróżniania okien modalnych od niemodalnych. W takim przypadku trzeba podać więcej informacji.
3. To zdanie nie jest wymaganiem, to opis architektury. Każde takie zdanie
wymaga głębszej analizy — dopiero na tej podstawie można stwierdzić, co
użytkownik rzeczywiście ma na myśli. Czy chodzi o skalowanie? Wydajność? Koszty? Bezpieczeństwo? Decyzja będzie zależeć od odpowiedzi na te
pytania.
4. Za przytoczonym sformułowaniem prawdopodobnie kryje się następujące
wymaganie: „System będzie zapobiegał wprowadzaniu błędnych wartości
w poszczególnych polach i będzie ostrzegał użytkownika o próbach dodawania takich wpisów”.
5. Stwierdzenie w tej formie można traktować jako jasne wymaganie, bazujące na ograniczeniach sprzętowych.
Oto rozwiązanie zagadki czterech punktów łączonych trzema odcinkami.
4337ebf6db5c7cc89e4173803ef3875a
4
4337ebf6db5c7cc89e4173803ef3875a
4
4337ebf6db5c7cc89e4173803ef3875a
4
Download