Niezawodność Oprogramowania, R08, 1


0x08 graphic
8.

Reszta jest kwestią nawyków

Poprzednie rozdziały niniejszej książki poświęcone były rozmaitym technikom ułatwiającym wykrywanie błędów i zwiększającym szansę ich uniknięcia. Same środki techniczne nie są jednak magiczną różdżką, zdolną załatwić wszystko za programistę; nie zdadzą się one na wiele, jeśli nie będą wsparte pożądanymi nawykami i właściwą praktyką ze strony programistów.

Jest skądinąd oczywiste, iż skompletowanie drużyny piłkarskiej złożonej z najlepszych nawet graczy nie daje gwarancji zwycięstwa. Potrzebny jest jeszcze codzienny trening — i odpowiednia podbudowa, finansowa i nie tylko; to ostatnie ma się co prawda nijak do samej techniki kopania piłki, lecz odpowiednio warunkuje ludzką motywację, niezbędną do odnoszenia wszelkich sukcesów.

W niniejszym rozdziale omówię kilka typowych barier sprawiających, iż możliwości wynikające z metodologii niezawodnego oprogramowania nie przynoszą spodziewanych efektów. Pierwszym, bodaj najważniejszym krokiem do przezwyciężenia tychże barier jest po prostu uświadomienie sobie ich istnienia.

Hokus-pokus, nie ma błędu

Jakże często programiści, pytani o kwestię usunięcia jakiegoś wykrytego wcześniej błędu, odpowiadają, iż błąd ten... zwyczajnie zniknął. Sam przecież prezentowałem kiedyś ten sposób myślenia, dopóki mój kierownik nie uświadomił mi oczywistej prawdy, iż błędy nie znikają same z siebie; jeżeli błąd nie daje znać o sobie i brak go także w raporcie sporządzanym przez testerów, to może to być spowodowane jedną z trzech następujących przyczyn:

Obowiązkiem programisty odpowiedzialnego za usunięcie błędu jest rozpoznanie, która z wymienionych okoliczności jest w danym przypadku prawdziwa.

Jeżeli błędu nie usunięto, lecz przestał się on manifestować w wyraźny sposób, może to być skutek dodania lub zmodyfikowania kodu programu; zdarza się często, iż zespół testujący i programista używają dwóch różnych wersji programu. Obowiązkiem programisty jest wówczas powrót do wcześniejszych „źródeł” i wyjaśnienie, dlaczego błąd przestał się pojawiać — być może wprowadzone zmiany stworzyły po prostu mniej sprzyjające warunki do jego występowania. Otwiera to prostą drogę do poprawienia błędu w nowej wersji.

0x01 graphic

Błędy nigdy nie znikają samoczynnie.

0x01 graphic

Zbędny wysiłek?

Programiści często narzekają na konieczność powrotu do starszych wersji kodu źródłowego w poszukiwaniu określonego błędu, traktując to jak zwykłą stratę czasu. Tymczasem właśnie porównanie różnych wersji kodu źródłowego pozwala na stwierdzenie, iż błąd faktycznie został poprawiony. Poza tym — które z rozwiązań jest bardziej dobroczynne dla projektu: stwierdzenie, iż błąd został definitywnie wyeliminowany, czy zaklasyfikowanie go jako „nieujawniający się” i skierowanie programu z powrotem do testowania? Gdy przyjąć tę drugą ewentualność — co powinni uczynić testerzy: próbować doprowadzić do ujawnienia się błędu, czy też pozostawić go jako „niewykrywalny” w nadziei, że zostanie poprawiony w przyszłości? Obydwa te warianty są zdecydowanie gorszym wyborem od porównywania kodu źródłowego.

Zrób dziś, co masz zrobić jutro

Gdy rozpocząłem swą pracę w grupie Excela, powszechną praktyką było odkładanie wykrywania i usuwania błędów do czasu, aż projekt zostanie ukończony. Nietrudno dociec przyczyny takiego sposobu myślenia — presji na terminowe ukończenie projektu nie towarzyszyła równie silna presja na poprawianie błędów. Jeżeli więc błędy nie powodowały zawieszenia systemu albo nie zmuszały grupy programistów do nieprzerwanej pracy przez dwa dni i noce, stanowiły one zadanie drugorzędne wobec tworzenia kodu.

Podejście takie stwarza wiele niewiadomych, spośród których najważniejszą jest termin faktycznego ukończenia projektu. Jakże bowiem można określić termin usunięcia wszystkich błędów z ukończonego właśnie kodu, jeżeli liczba tych błędów wynosi niemal dwa tysiące?! A przecież, gdy poprawia się stare błędy, wprowadza się zazwyczaj (lub tylko powoduje ujawnienie) wiele nowych — wszystko to stwarza warunki, którym grupa testująca może po prostu nie podołać w krótkim czasie.

Inną typową konsekwencją odkładania na później testowania projektu jest sytuacja, kiedy to kierownicy działu rozwoju (ang. Development) długo — niekiedy przez kilka miesięcy — nie mogą doczekać się przekazania „niemal całkowicie ukończonego projektu”. Wszak projekt „wydaje się” pracować poprawnie, dlaczego więc trzeba czekać pół roku na usunięcie „jakichś drobnych usterek”? Twierdzący tak menedżerowie nie wiedzą nic o błędach wykraczania poza przydzieloną pamięć, posługiwania się „wiszącymi” wskaźnikami, ponadto „sprawdzając produkt” uruchamiają w istocie niewielką część jego funkcji. A przecież na ogół oni sami znajdują się pod presją innych klientów...

Powyższe rozważania skłaniają więc do następujących konkluzji:

0x01 graphic

Nie odkładaj diagnostyki na później.

0x01 graphic

Doktora!!!

Anthony Robbins w jednej ze swych książek („OBUDŹ W SOBIE OLBRZYMA”) opowiada historię pięknej lekarki, która stojąc na brzegu rwącej rzeki usłyszała wołanie tonącego człowieka o pomoc. Nie namyślając się długo, wskoczyła do rzeki, wyłowiła topielca, ułożyła go na brzegu i za pomocą sztucznego oddychania usta-usta doprowadziła go do przytomności. Wtem znowu usłyszała wołanie o pomoc — tym razem topiło się dwóch mężczyzn. Również ich wyłowiła, przywróciła do przytomności — i znów wołanie o pomoc, tym razem topiło się czterech chłopaków. Ich również wyłowiła, a zaraz potem usłyszała, jak ośmiu tonących...

W efekcie lekarka, zajęta wyławianiem coraz to nowych ofiar, nie miała czasu na udzielenie pomocy już wyłowionym.

Przypomina to jako żywo sytuację programisty bombardowanego błędami w takim tempie, iż nie jest on w stanie nadążyć z rozpoznawaniem przyczyn powodujących te błędy. Namiastkę takiej lawiny błędów mieliśmy okazję obserwować w rozdziale 7., przy okazji omawiania funkcji strFromUns. Po usunięciu pierwszego błędu, spowodowanego umieszczeniem bufora w pamięci statycznej, pojawiły się błędy związane nie z samą funkcją strFromUns, lecz jej wywoływaniem. Określenie przyczyny tych błędów nie jest już takie łatwe — czy była nią funkcja strFromUns, czy może funkcje ją wywołujące?

Doświadczyłem takiej sytuacji osobiście, podczas przenoszenia kilku mechanizmów „windowsowego” Excela do Excela dla komputerów Macintosh (wówczas były to dwa oddzielne produkty, posiadające całkowicie odrębny kod źródłowy). Gdy po zaimplementowaniu pierwszej funkcji przystąpiłem do testowania, jedna z funkcji zasygnalizowała błąd otrzymania pustego wskaźnika (NULL), którego otrzymać nie powinna.

Gdy udałem się z tym problemem do autora kodu, stwierdził on po prostu, iż przedmiotowa funkcja nie jest przygotowana na pusty wskaźnik; jeżeli go otrzyma (mimo, iż nie powinna), należy natychmiast zakończyć jej działanie za pomocą „szybkiego wyskoku”.

if (pb == NULL)

return (FALSE);

Błąd nie tkwił więc w samej funkcji, lecz na zewnątrz niej i wydawałoby się, że został poprawiony. Mnie jednak opisane postępowanie wydało się raczej usuwaniem objawów niż przyczyny; wróciłem do swego biura i zacząłem szukać przyczyny powodującej, iż argument wywołania funkcji jest pustym wskaźnikiem. Zanim tę przyczynę rozpoznałem, znalazłem dwa inne błędy tego samego typu.

Innym razem, znalazłszy błąd w kodzie źródłowym, stwierdziłem, iż kilka oglądanych przeze mnie funkcji powinno się załamać, tymczasem funkcje te wykonywały się bezbłędnie. Przyczyną było połowiczne (lokalne) poprawienie bardziej ogólnego, globalnego błędu.

0x01 graphic

Usuwaj przyczyny, nie objawy.

0x01 graphic

Jeśli działa, nie poprawiaj

„Nawet jeżeli to działa, i tak należy to poprawić” zdarza się niekiedy słyszeć zatroskany głos programistów. Niezależnie od tego, jak pewnie działa określony fragment kodu, czują się oni wręcz zmuszeni wtrącić do niego przysłowiowe trzy grosze. Jeżeli przyszło Ci kiedyś pracować z programistami uporczywie reformatującymi całe pliki źródłowe stosownie do swoich upodobań, doskonale wiesz, co mam na myśli.

Podstawowy problem związany z „adiustacją” kodu polega na tym, iż ponownie sformatowany kod jest w istocie kodem nowym, zmienionym, a więc być może zawierającym nowe błędy. Aby zrozumieć wagę tego stwier­dzenia, przyjrzyjmy się ponownie znajomemu fragmentowi:

char *strcpy(char *pchTo, char *pchFrom)

{

char *pchStart = pchTo;

while ((*pchTo++ = *pchFrom++) != 0)

{}

return (pchStart);

}

Niektórzy czują w tym momencie nieodpartą chęć zmiany 0 na '\0' w instrukcji while. Porównywanie znaku z liczbą całkowitą może się bowiem wydawać wynikiem pomyłki — wszak łatwo zgubić znak „\”, nie to jest jednak najważniejsze. Któż jednak po wprowadzeniu tak prostej zmiany podejmie się ponownego przetestowania zmienionego przecież kodu?

Można by przypuszczać, iż tak „kosmetyczne” zmiany nie wnoszą do kodu żadnych błędów — przynajmniej wówczas, gdy zmieniony kod nadal kompiluje się bezbłędnie. Zresztą, czy tak delikatne zabiegi, jak zmiana nazwy zmiennej lokalnej w ogóle mogą powodować problemy?

Okazuje się, że mogą. Śledzenie programu w poszukiwaniu błędu zaprowadziło mnie kiedyś do wnętrza funkcji posiadającej lokalną zmienną o nazwie hPrint kolidującą z identycznie nazwaną zmienną globalną. Ponieważ funkcja jeszcze niedawno działała poprawnie, spojrzałem do wcześniejszej wersji kodu i stwierdziłem, że wspomniana zmienna lokalna nazywała się wówczas hPrint1. Ponieważ brak było zmiennych lokalnych o nazwach hPrint2, hPrint3 itd. usprawiedliwiających końcową jedynkę, ktoś widocznie uznał tę jedynkę za wynik pomyłki i postanowił „poprawić” błąd.

W świetle powyższego, całkowicie zasadne okazuje się następujące ostrzeżenie: jeżeli napotkasz jakiś fragment kodu wyglądający Twoim zdaniem ewidentnie źle lub wyglądający na niepotrzebny, zachowaj jak najdalej idącą ostrożność. Niejednokrotnie zdarzało mi się widzieć kod wyglądający śmiesznie lub wręcz karykaturalnie — jedynym powodem jego istnienia było... zniwelowanie błędów tkwiących w kompilatorze! Oczywiście fragmenty takie powinny być odpowiednio skomentowane, jednak brak komentarzy do niczego jeszcze nie upoważnia.

Jeżeli w poniższym fragmencie:

char chGetNext(void)

{

int ch;

ch = getchar();

return (chRemapChar(ch));

}

dokonamy niewinnego usunięcia „zbędnej” zmiennej ch:

char chGetNext(void)

{

ch = getchar();

return (chRemapChar(getchar()));

}

wprowadzimy trudny do wykrycia błąd, gdy chRemapChar będzie makrem wartościującym wielokrotnie swój argument.

0x01 graphic

Nie usuwaj istniejących fragmentów kodu,
jeżeli nie jest to niezbędne dla poprawności projektu.

0x01 graphic

Funkcja z wozu, koniom lżej

Ostrzeżenie przed nieuzasadnionym usuwaniem fragmentów kodu jest szczególnym przypadkiem zasady zabraniającej dokonywania jakichkolwiek modyfikacji (lub dopisywania) kodu bez wyraźnej przyczyny. Jeżeli nawet wydaje Ci się to nieco dziwne, przypomnij sobie, ile razy, spoglądając na jakiś fragment kodu, zadawałeś sobie pytanie „na ile istotny dla całego programu jest ten właśnie fragment?”

Zdarza się, iż wiele funkcji tkwiących w kodzie programu jest z punktu widzenia projektu po prostu zbędnych; pojawiły się tam one niejako „dla kompletności” lub na wyraźne życzenie użytkownika albo po prostu dlatego, iż ich odpowiedniki znajdują się w produktach konkurencyjnych. Niewątpliwie tkwi w tym jakiś przejaw dążenia do programistycznej doskonałości, czy chociażby tylko tworzenia lepszych produktów; rodzi się jednak pytanie o znaczenie określenia „lepszy”: lepszy dla produktu, czy tylko technicznie odkrywczy? Bywa, iż te dwa przypadki istotnie idą ze sobą w parze, bywa, że jest inaczej.

Nie chciałbym być źle zrozumiany: nie występuję bynajmniej przeciwko inwencji programistów, jestem jednak zdecydowanym przeciwnikiem zbędnego kodu zwiększającego liczbę potencjalnych błędów.

0x01 graphic

Nie implementuj niepotrzebnych funkcji.

0x01 graphic

Elastyczność rodzi błędy

Jednym ze sposobów na zmniejszenie ryzyka popełniania błędów jest rezygnacja z niepotrzebnej elastyczności projektu. Idea ta, w mniej lub bardziej wyraźnej postaci, towarzyszy nam od początku niniejszej książki — i tak w rozdziale 1. nasz hipotetyczny kompilator ostrzegał nas przed stosowaniem ryzykownych i redundantnych idiomów języka C; w rozdziale 2. wprowadziłem instrukcję ASSERT, gdyż nie stwarza ona ryzyka błędnego użycia w wyrażeniach; w rozdziale 3. zabroniłem przekazywania pustego wskaźnika do funkcji FreeMemory i postawiłem odpowiednią asercję na straży tej reguły — mimo iż użycie pustego wskaźnika byłoby tu całkiem legalne.

Podobnemu celowi służyło wyeliminowanie w rozdziale 5. niewątpliwie elastycznej funkcji realloc, na rzecz odrębnych funkcji dokonujących (odpowiednio) alokacji, zmniejszenia, zwiększenia i zwolnienia bloku.

Równie ryzykowne, co nadmiernie elastyczne funkcje, są nadmiernie elastyczne ogólne cechy tworzonego kodu. Elastyczność ta prowadzi bowiem do wielu osobliwych sytuacji, całkowicie legalnych z punktu widzenia programu, lecz łatwych do przeoczenia przez programistę testującego kod.

Gdy implementowałem obsługę kolorów w Excelu dla Macintosha II, przeniosłem z „windowsowego” Excela kod umożliwiający użytkownikowi określenie koloru tekstu wyświetlanego w danej komórce. W tym celu istniejący kod w rodzaju:

$#.##0.00 /* wyświetl 1234.5678 jako $1.234.57 */

należało poprzedzić znacznikiem koloru — poniższa specyfikacja:

[blue]$#.##0.00

powoduje wyświetlenie zawartości komórki w kolorze niebieskim.

Składnia wydała się tu jednoznacznie określona — znacznik koloru powinien poprzedzać specyfikację formatu — tymczasem gdy przystąpiłem do testowania, stwierdziłem, iż specyfikacje w rodzaju:

$#.##0.00[blue]

$#.##[blue]0.00

$[blue]#.##0.00

również funkcjonują bezbłędnie — znacznik koloru mógł być umieszczony gdziekolwiek w specyfikacji. Indagowany na tę okoliczność autor wersji oryginalnej (windowsowej) stwierdził, iż „sytuacja taka jest wynikiem określonej konstrukcji pętli analizującej składnię”, i że nie widzi on nic złego w (jakkolwiek by było) dodatkowej elastyczności.

Wydawało się, iż dotarcie do wspomnianej pętli skanującej i wymuszenie położenia znacznika koloru na początku specyfikacji nie będzie trudne — faktycznie, okazało się, iż wymaga to jednej dodatkowej instrukcji if. Analizując wraz z kolegą dokładniej cały problem stwierdziliśmy jednak, iż obecna konstrukcja pętli skanującej wymuszona jest innymi okolicznościami, mającymi swe źródło na wyższym poziomie kodu; „poprawiając” pętlę skanującą usuwalibyśmy więc nie błąd, ale jego objawy. I tak, po dziś dzień, Microsoft Excel umożliwia wpisywanie znacznika koloru w dowolnym miejscu specyfikacji.

0x01 graphic

Nie implementuj nadmiernej elastyczności.

0x01 graphic

Przenoszony kod to nowy kod

Jednym z wniosków wysnutych przeze mnie z niepowodzeń związanych z przenoszeniem Excela z Windows na Macintosha jest ten, iż nie należy zaniedbywać testowania kodu zaadaptowanego do nowego środowiska. Ufny w to, iż windowsowy kod Excela został już gruntownie przetestowany, skopiowałem go do wersji dla Macintosha, poczyniłem kilka zmian niezbędnych do połączenia go z resztą kodu i sprawdziłem pobieżnie jego funkcjonowanie. I to był podstawowy błąd, bowiem wersja dla Windows sama była jeszcze w fazie rozwoju, a był to ten okres w historii Microsoftu, kiedy testowanie tworzonego kodu konsekwentnie odkładane było na później.

Niezależnie więc od tego, jaką metodą implementowane są funkcje danego projektu — czy jest to kod tworzony „od zera”, czy też przenoszony z innych środowisk — zawsze należy go gruntownie przetestować. To, że błędy zaobserwowane na Macintoshu istniały już w wersji Excela dla Windows, nie stanowi żadnego usprawiedliwienia. Lenistwo zdecydowanie się tu nie opłaciło.

Spróbuj

Jakże często na rozmaitych grupach dyskusyjnych, na pytania w rodzaju „Nie wiem jak zrealizować...”, spotyka się charakterystyczne odpowiedzi w stylu „Czy próbowałeś...”. Jeden z dyskutantów pyta na przykład „Jak sprawić, by kursor zniknął z ekranu”, na co inny odpowiada mu „Spróbuj ustalić współrzędne kursora poza ekranem”, inny sugeruje „Spróbuj ustawić wysokość kursora na zero”, jeszcze inny podpowiada „Kursor jest po prostu bitmapą — nadaj jej zerową wysokość i zerową szerokość”, itd.

Spróbuj, spróbuj, spróbuj... Propozycja taka może być co najwyżej traktowana jako pouczenie, nie zaś jako udokumentowane rozwiązanie. Cóż jednak złego w samym „próbowaniu”? Oczywiście nic, jednakże jego wyniki adekwatne są jedynie dla środowiska, w którym próba jest przeprowadzana i nie ma żadnej gwarancji, iż będą one takie same na innym komputerze, w innej wersji kompilatora, czy nawet — o innej porze dnia. Na tej właśnie zasadzie niektórzy programiści odczytują zawartość zwolnionych bloków pamięci, ponieważ wielokrotne próby utwierdziły ich w przekonaniu, iż zawartość tychże bloków zachowywana jest bezpośrednio po ich zwolnieniu; wynikom swych obserwacji nadają oni tym samym rangę obowiązującej reguły.

Nie można jednak budować na tym fundamencie kariery zawodowej — rozwiązania oparte wyłącznie na obserwacjach bazują często na niezdefiniowanych lub źle zdefiniowanych efektach ubocznych. À propos kursora — udokumentowanym sposobem jego ukrycia jest wywołanie:

SetCursorState(INVISIBLE);

Nie „próbuj”; po prostu przeczytaj.

Uczestnicy grup dyskusyjnych często pytają o kwestie, które jasno opisane są w rozmaitych podręcznikach. Spośród wielu mniej lub bardziej „uczonych” odpowiedzi najwłaściwsze okazują się te w rodzaju „Zobacz Inside Macintosh, tom IV, strona 32”. Przed przystąpieniem do rozwiązywania problemu należy więc zapoznać się z dokumentacją. Fakt, iż jest to zajęcie trudniejsze i mniej fascynujące niż eksperymentowanie „na żywo”, czy zadawanie pytań innym użytkownikom; można się jednak wówczas dużo dowiedzieć o danym systemie operacyjnym lub języku programowania.

0x01 graphic

Nie polegaj na doświadczeniach nabytych wyłącznie drogą obserwacji.
Poświęć swój czas na znalezienie właściwego rozwiązania.

0x01 graphic

Święty Harmonogram

Pisałem już wcześniej o przewadze testowania tworzonego kodu na bieżąco nad odkładaniem testowania do ukończenia projektu. Niektórzy programiści istotnie wykazują tendencję do wpisywania kolejnych partii kodu bez troszczenia się o jego poprawność, inni z kolei implementują tuziny różnych funkcji bez weryfikowania na bieżąco ich poprawności. Nie byłoby w tym nic złego pod warunkiem, iż cały stworzony kod byłby w końcu gruntownie przetestowany — ale tu właśnie pojawia się pewien problem.

Wyobraźmy sobie programistę, który ma pięć dni czasu na zaimplementowanie pięciu funkcji. Ma on do wyboru dwie możliwości: poświęcić każdy dzień na stworzenie i przetestowanie jednej funkcji lub stworzyć wszystkie pięć funkcji od razu i poświęcić ewentualną „końcówkę” piątego dnia na przetestowanie ich wszystkich. Jak myślisz, która z wymienionych opcji zostanie z większym prawdopodobieństwem wybrana przez programistę? I która z nich prawdopodobnie daje lepsze wyniki?

Załóżmy, iż programista faktycznie stworzył — bez przetestowania — pięć funkcji w pięciodniowym terminie określonym przez harmonogram. W pewnym momencie uświadamia on sobie, iż na testowanie zabrakło mu już czasu; w porozumieniu z kierownikiem projektu otrzymuje więc dodatkowe dwa dni. Jak myślisz, czy te dwa dni poświęcone zostaną na gruntowne sprawdzenie kodu źródłowego, czy też może tylko na „uwiarygodnienie” jego działania, czyli wyrywkowe przetestowanie metodą „czarnej skrzynki”? Wszystko zależy oczywiście od konkretnego projektu, środowiska programistycznego i oczywiście kwalifikacji i odpowiedzialności programisty, w każdym razie mamy tu do czynienia z wydłużeniem realizacji projektu i naruszeniem harmonogramu; ów święty, nietykalny harmonogram może w takich przypadkach stać się powodem tego, iż testowanie przeprowadzane jest w niepełnym zakresie.

Prawdziwą przyczyną jest jednak nie sam harmonogram, lecz odkładanie testowania na koniec. Odkładanie takie powoduje, iż po napisaniu całego kodu niezwykle trudno jest określić rozsądny termin potrzebny na jego przetestowanie; przy testowaniu na bieżąco ewentualne opóźnienie w stosunku do harmonogramu daje się określić znacznie łatwiej i znacznie łatwiej jest nad nim zapanować. Jeżeli w trakcie wspomnianych pięciu dni programiście uda się zaimplementować i przetestować jedynie trzy funkcje (spośród pięciu założonych) i otrzyma on dwa dodatkowe dni na stworzenie i przetestowanie pozostałych dwóch funkcji, rokowania są znacznie bardziej pomyślne niż w przypadku perspektywy testowania wszystkich pięciu funkcji — testowanie jest bowiem z reguły znacznie trudniejsze i bardziej czasochłonne niż samo tworzenie kodu.

0x01 graphic

Twórz i testuj swój kod w małych fragmentach.

Zawsze testuj swój kod na bieżąco,
nawet jeżeli powodować to będzie opóźnienia względem harmonogramu.

0x01 graphic

„Tester”
— nazwa w sam raz dla testera

Jak już pisałem w rozdziale 5., jedną z przyczyn, dla których programiści traktują w kategoriach znaku wynik zwracany przez funkcję getchar, jest jej nazwa — jak wiadomo myląca, bowiem funkcja zwracać może również wartości ujemne nie stanowiące kodu znaku, lecz informację diagnostyczną. Podobnie mylące jest określanie programistów weryfikujących zachowanie się gotowego produktu mianem „grupy testującej” (lub po prostu mianem „testerów”), rodzi to bowiem u niektórych programistów nieuzasadnione oczekiwania, iż grupa ta będzie w stanie znaleźć i poprawić wszystkie znajdujące się w kodzie błędy.

Aby wykazać bezzasadność takich oczekiwań, zapomnijmy na chwilę o komputerach i przyjrzyjmy się procedurze odbioru zbudowanego właśnie domu. Panuje tu podobny podział pracy, jak przy tworzeniu oprogramowania: robotnicy wykonują swą pracę, zaś inspektorzy weryfikują poprawność jej rezultatów. Inspektorzy nie są jednak w stanie zweryfikować samej pracy robotników, gdyż nie obserwowali jej na bieżąco; nie mogą więc oni wykryć np. faktu, iż niedbały elektryk nie sprawdził poprawności połączeń, nie przeprowadził pomiarów zerowania gniazdek itp.

Niepotrzebny wysiłek?

Gdy spogląda się pobieżnie na proces tworzenia oprogramowania, można odnieść wrażenie, iż programiści i testerzy dublują się nawzajem wykonując tę samą pracę — usuwanie błędów tkwiących w aplikacjach. Faktycznie, ostateczny cel pracy obydwu tych grup jest identyczny, inne są jednak drogi dochodzenia do tego celu.

Programiści rozpoczynają testowanie swego kodu od sprawdzenia przepływu sterowania i przepływu danych, weryfikując, instrukcja po instrukcji, zachowanie się każdej funkcji. Upewniwszy się co do poprawności każdej z nich z osobna, posuwają się o krok wyżej, weryfikując poprawność współdziałania poszczególnych funkcji w ramach konkretnego podsystemu. Kolejnym krokiem jest testowanie poprawnego działania podsystemów w ramach aplikacji. Programiści testujący aplikacje wykonują więc swą pracę niejako „od środka na zewnątrz”.

Elektryk nie może więc zaniedbywać takich testów i zakładać, że „jeżeli pojawią się problemy, inspektorzy na pewno mnie o nich poinformują” (w przeciwnym razie powinien poszukać sobie innego zajęcia).

Na tej samej zasadzie testerzy nie są w stanie zweryfikować stworzonego kodu lepiej, niż może to zrobić jego autor. W szczególności, testerzy nie potrafią dodawać do kodu asercji sprawdzających poprawność przepływu danych; nie są w stanie przeprowadzić testowania podsystemów — takiego, jak w przypadku menedżera pamięci w rozdziale 3.; nie są w stanie wykonać programu w sposób krokowy z gwarancją, iż przetestowana zostanie każda z możliwych ścieżek przepływu sterowania.

Zadaniem testerów jest raczej wykrywanie usterek w funkcjonowaniu produktu, weryfikacja jego tzw. kompatybilności wstecznej (czyli zgodności z wcześniejszymi wersjami) i ogólne spojrzenie na produkt z perspektywy użytkownika, któremu pewne funkcje mogą wydać się sztuczne, zbyt mało elastyczne itp. Tak więc testerzy nie mają prawa stwierdzić, iż „program jest bezbłędny”; mogą oni co najwyżej zapewnić, iż „program wydaje się działać bezbłędnie” — a to już jest różnica.

A może się zdarzyć i tak, iż w danej chwili grupy testującej... po prostu nie będzie, bowiem tworzący ją programiści skierowani zostali do wykonania jakiegoś nie cierpiącego zwłoki projektu. Zdarzyło się tak kiedyś w Microsofcie, może się także zdarzyć gdziekolwiek.

Praca grupy testującej rozpoczyna się natomiast na poziomie najwyższym — testowana aplikacja traktowana jest jako „czarna skrzynka” przetwarzająca określone dane na określone wyniki, a weryfikacji podlega poprawność tego „przetwarzania”.

xxxxxxxxxx

Przypomina to raczej okrążanie wroga z dwóch stron niż dublowanie pracy. Zastosowanie dwóch metod postępowania w celu wyeliminowania błędów znacznie zwiększa szansę powodzenia w osiągnięciu tego celu.

Testerzy sprawdzają także, które zasygnalizowane błędy zostały poprawione, które z „poprawionych” uprzednio błędów wystąpiły ponownie, które błędy przestały się objawiać samoczynnie itp. Na następnym etapie swej pracy schodzą oni o jeden poziom niżej, sprawdzając za pomocą tzw. narzędzi pokrywających (ang. code coverage tools), które fragmenty kodu nie zostały jeszcze przetestowane i budując na tej podstawie nowe testy. Postępowanie takie ma wszelkie cechy postępowania zstępującego — z zewnątrz do środka.

0x01 graphic

Nie oczekuj, iż testerzy poprawią wszystkie Twoje błędy.

0x01 graphic

Programista zawinił, testera powiesili

Najczęstszą reakcją programistów na znalezienie przez testerów błędu w ich kodzie jest uczucie ulgi — „jak to dobrze, iż wykryli oni ten błąd przed wypuszczeniem produktu na rynek”. Zdarza się równie często, iż programiści reagują nerwowo na uwagi testerów odnośnie coraz co nowych błędów znajdowanych w kodzie („dlaczego oni nie dadzą mi spokoju?”). W dodatku, jako że testerzy stanowią ostatnie ogniwo w „łańcuchu wytwórczym” aplikacji, to właśnie oni obarczani są najczęściej odpowiedzialnością za opóźnienia w terminie przekazania produktu.

Błędy popełniane są jednak przez programistów, nie przez testerów, ci ostatni nie mogą więc być traktowani jak winowajcy; są oni raczej posłańcami niosącymi niepokojące wieści. Zamiast złości, bardziej odpowiednią reakcją programisty na wiadomość tego typu powinno być raczej... niedowierzanie („przetestowałem kod tak dokładnie, iż znalezienie w nim błędu wydawało się wręcz niemożliwe”), a następnie wdzięczność za usunięcie błędu przed skierowaniem aplikacji do rozpowszechniania.

0x01 graphic

Nie obwiniaj testerów za swe własne błędy.

0x01 graphic

Nie ma głupich błędów

Zdarza się także słyszeć opinię, iż błędy sygnalizowane przez testerów są po prostu śmieszne, i że w ogóle zawracają oni głowę programistom jakimiś głupimi błędami. Tymczasem nie jest sprawą testerów ocena, jak poważny (czy niepoważny) jest znaleziony błąd — są oni zobowiązani do raportowania wszystkich błędów. Pozornie „głupi” błąd może bowiem powodować całkiem poważne konsekwencje.

Nie to jest jednak najważniejsze, czy dany błąd jest poważny, czy nie; istotne jest to, iż uszedł on uwadze programisty. Wykrycie każdego błędu, niezależnie od jego powagi, może ustrzec programistę przed popełnianiem go w przyszłości.

Tak więc błąd może być mało poważny; bardzo poważny jest natomiast fakt jego wystąpienia.

Zdefiniuj swe priorytety

Przyglądając się uważnie streszczeniom rozdziałów w punktach Podsumowa­nie, mogłeś odnieść wrażenie, iż niekiedy przeczą one sobie nawzajem. Cóż, w naszym niedoskonałym świecie tak już bywa, iż osiągnięcie założonego celu jest zazwyczaj sztuką kompromisu, czyli wyboru pomiędzy sprzecznymi możliwościami. Wiedzą o tym doskonale programiści skazani na przykład na wybór pomiędzy algorytmem szybkim, lecz pamięciożernym, a wolnym, lecz zadowalającym się małym rozmiarem pamięci.

Mniej oczywiste jest istnienie innego kompromisu programistycznego — mianowicie wyboru pomiędzy algorytmem zgrabnym, lecz ryzykownym, a algorytmem bardziej obszernym, lecz łatwym do weryfikacji. Ponieważ konsekwencje takiego wyboru nie ujawniają się natychmiast — a dopiero po wystąpieniu błędów lub w przypadku rozbudowy projektu — wybór ten nie zawsze jest należycie przemyślany.

Aby umiejętnie pogodzić sprzeczne wymagania, należy wpierw określić ich priorytety. Programowanie bez założonych priorytetów jest jak wycieczka bez założonego celu — turysta, znalazłszy się na kolejnym przystanku, zastanawia się „dokąd teraz pójść?”. Gdy decydujemy się na zdefiniowanie priorytetów, musimy znać ich uwarunkowania, a te się zazwyczaj zmieniają.

I tak w latach 70., gdy mikrokomputery były niesamowicie wolne, a każdy bajt ich pamięci był skarbem na wagę złota, wymogi czytelności, prostoty itp. w naturalny sposób przegrywać musiały z żywotnymi wymogami jak największej szybkości i efektywnego wykorzystania pamięci.

Minęło sporo czasu, mamy dwudziesty pierwszy wiek, niemal tysiąckrotnie (!) szybsze procesory i tysiąckrotnie pojemniejsze pamięci (od tych sprzed trzydziestu lat) i filozofia składania czytelności programu na ołtarzu efektywności zdaje się tracić rację bytu. Szybkie procesory z pojemnymi pamięciami mogą realizować nieefektywne nawet algorytmy w sposób akceptowalny dla użytkownika, postęp technologiczny ma się jednak nijak do kodu niepewnego, najeżonego błędami — a trzeba pamiętać, iż wymagania dzisiejszych użytkowników mikrokomputerów są znacznie wyższe od tych sprzed trzydziestu lat.

Oczywiście nie istnieje jedyna, absolutnie dobra, lista priorytetów — w przeciwnym razie bezprzedmiotowa stałaby się cała niniejsza dyskusja — w charakterze przykładu rozpatrzmy więc dwójkę hipotetycznych programistów — Jacka i Jill — przyglądając się osobistemu wyborowi ich priorytetów.

Jack

Jill

poprawność

poprawność

globalna efektywność

łatwość testowania

rozmiar

globalna efektywność

lokalna efektywność

łatwość utrzymania/czytelność

wygoda osobista

spójność kodowania

łatwość utrzymania/czytelność

rozmiar

indywidualizacja

lokalna efektywność

łatwość testowania

indywidualizacja

spójność kodowania

wygoda osobista

Zastanówmy się, jak powyższy wybór priorytetów odzwierciedla się w tworzonym przez nich kodzie. Obydwoje stawiają na pierwszym miejscu poprawność kodu, tu jednak kończy się zgodność między nimi. Dla Jacka najważniejsze są względy „techniczne” — efektywność i rozmiar — podczas gdy względy niezawodności są przez niego traktowane raczej marginalnie. Dla Jill najważniejsze jest tworzenie poprawnego kodu — teraz i w przyszłości, stąd względy czytelności i łatwości utrzymania; efektywność i rozmiar mają dla niej znaczenie o tyle, o ile rzeczywiście są krytyczne dla danego projektu. Umieszczenie łatwości testowania na wysokim (drugim) miejscu jest wyrazem jej przekonania, iż nie sposób w łatwy sposób weryfikować programu, którego nie da się łatwo testować.

Wziąwszy pod uwagę przedstawione priorytety — która z wymienionych osób chętniej stosować się będzie do przedstawionych w niniejszej książce zasad niezawodnego programowania, w szczególności:

Kto z nich z większym zainteresowaniem czytywać będzie literaturę traktującą o stylu programowania, wyszukiwaniu błędów, weryfikacji oprogramowania itp.? Kto z większym upodobaniem cyzelować będzie poszczególne linie kodu, by były one jak najbardziej zwięzłe? Kto przejawiać będzie skłonność do myślenia raczej w kategoriach całego produktu niż poszczególnych instrukcji kodu źródłowego?

Jeżeli jesteś programistą, warto, byś sam zadał sobie postawione pytania i sporządził własną listę priorytetów.

0x01 graphic

Zdefiniuj listę swych priorytetów i trzymaj się jej.

0x01 graphic

Sam pisałeś

Gdy pytam programistów, dlaczego ten fragment kodu napisany został w taki, a nie inny sposób — często dziwaczny — zdarza mi się niekiedy słyszeć „och, nie wiem — może czułem się wtedy gorzej?”.

Może tak, może nie; może po prostu człowiek nie miał czasu zjeść śniadania? Nie to jest jednak główną przyczyną, a raczej brak jasno zdefiniowanych priorytetów — bo to właśnie one warunkują wybór takiej, a nie innej konwencji kodowania.

Jeżeli więc przyłapiesz się na tym, iż nie za bardzo potrafisz uzasadnić konkretną formę stworzonego przez siebie kodu, warto, byś zweryfikował listę swych priorytetów programistycznych.

Podsumowanie

PROJEKT: Jeżeli jesteś kierownikiem zespołu programistycznego, przekonaj swych programistów do stworzenia wspólnej dla wszystkich listy priorytetów programistycznych. Jeżeli Twoja firma zatrudnia programistów o zróżnicowanych kwalifikacjach, być może musisz rozważyć stworzenie kilku list, adekwatnych do poszczególnych poziomów kwalifikacji.

162 Niezawodność oprogramowania

Reszta jest kwestią nawyków 161

162 D:\Roboczy\Niezawodność oprogramowania\9 po skladzie 1\r08.doc

D:\Roboczy\Niezawodność oprogramowania\9 po skladzie 1\r08.doc 161



Wyszukiwarka

Podobne podstrony:
Niezawodność Oprogramowania, R07, 1
Niezawodność Oprogramowania, rdodA, 1
Niezawodność Oprogramowania, R00-2, 1
Niezawodność Oprogramowania, rdodC, 1
Niezawodność Oprogramowania, rdodB, 1
Niezawodnosc oprogramowania nieopr
Metody testowania kodu oprogramowania Badanie niezawodności oprogramowania(1)
Niezawodność Oprogramowania, uwagi po, w r2 trzeba wykonac 4 rysunki
Niezawodność Oprogramowania, R01, 1
Niezawodność Oprogramowania, R09, 1
Niezawodność Oprogramowania, R00-1, 1
Niezawodnosc oprogramowania nieopr
Niezawodnosc oprogramowania 2

więcej podobnych podstron