R08.DOC

(186 KB) Pobierz
1









              Reszta jest kwestią nawyków              161



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ą, zdolzał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:

¨      poprzedni raport sporządzony przez testerów sam był błędny — stwierdzenie przedmiotowego błędu było wynikiem pomyłki i błąd ten faktycznie nie istnieje;

¨      błąd został usunięty przez innego programistę;

¨      błąd nadal istnieje, lecz nie ujawnia się.

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.

Błędy nigdy nie znikają samoczynnie.

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:

¨      odkładanie wyszukiwania błędów „na później”, w obawie przed stratą czasu, jest „oszczędnością” zdecydowanie źle pojętą; wszak znacznie łatwiej znajduje się błędy w kodzie stworzonym dzień wcześniej, niż w kodzie stworzonym w ubiegłym roku;

¨      poprawianie błędów „na bieżąco” ma tę dodatkową zaletę, iż programista świadom popełnionych (i poprawionych) błędów nie popełnia ich zazwyczaj po raz drugi;

¨      błędy programistyczne stanowią przyczynę swoistego ujemnego sprzężenia zwrotnego: przystąpienie do następnego etapu projektu bez usunięcia błędów tkwiących w etapie właśnie zakończonym stwarza ryzyko, iż wprowadza się do zastosowania funkcje zaimplementowane połowicznie lub wręcz źle zaimplementowane; w momencie, gdy zaczynają się ujawniać tego konsekwencje, staje się oczywiste, iż pierwotny pośpiech skutkuje spowolnieniem całej pracy. I odwrotnie — staranne przetestowanie zakończonego etapu pozwala uniknąć straty czasu w wymiarze znacznie większym niż ten przeznaczony na testowanie;

¨      gdy utrzymuje się liczbę spodziewanych błędów na możliwie niskim poziomie, można łatwiej przewidzieć ostateczne ukończenie projektu. Jeżeli w kodzie, którego pisanie właśnie zakończono, ukrywają się (powiedzmy) 32 błędy, to sytuacja ta jest możliwa do opanowania w rozsądnie dającym się przewidzieć czasie; jeżeli ukrywające się błędy liczyć można w tysiącach, sytuacja staje się dramatyczna. I niekiedy nie ma innego wyjścia, jak skierowanie do rozpowszechniania produktu jedynie w części „odpluskwionego”.

Nie odkładaj diagnostyki na później.

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.

Usuwaj przyczyny, nie objawy.

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.

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

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.

Nie implementuj niepotrzebnych funkcji.

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.

...

Zgłoś jeśli naruszono regulamin