Przedmowa do wydania polskiego
Z błędami programistycznymi jest trochę tak, jak z potworem z jeziora Loch Ness — nikt go tak naprawdę nie widział, co wcale nie upoważnia do kategorycznego wnioskowania o jego nieistnieniu; gdyby jednak komuś udało się zaobserwować go choć raz (i oczywiście fakt ten udokumentować), potwór przestałby być potworem, stając się kolejną pozycją w hierarchii Regnum Zoa i nie wzbudzając odtąd żadnych sensacji. Na tej samej zasadzie żaden program nie jest bezbłędny, nawet jeżeli wydaje się być takowym z bardzo dużym prawdopodobieństwem, bezspornie udowodnić można bowiem jedynie istnienie błędów, nigdy zaś ich brak.
W przeciwieństwie do legendarnego potwora, błędy programistyczne nie mają żadnego posmaku sensacji, za to niosą ze sobą całkiem realne — i poważne — konsekwencje, w postaci straconego czasu, nadwerężonej reputacji twórców oprogramowania, zmarnowanych pieniędzy, a nawet nadszarpniętego zdrowia. Skoro więc zdecydowaliśmy się powierzyć komputerom i informatyce najważniejsze z aspektów naszego życia doczesnego, musimy jednocześnie wypracować rozwiązania chroniące nas przed owymi konsekwencjami. Niewystarczające okazują się tu rozwiązania oczywiste, jak wytężona uwaga programistów, szczególna staranność w wykonywaniu pracy itp.; wszystko to rzecz jasna jest jak najbardziej pożądane, lecz musi być dodatkowo wsparte stosowną metodologią tworzenia oprogramowania. Klasyczne ujęcie algorytmiczne, z rzeczywistym problemem na jednym biegunie, a sformalizowanym opisem jego rozwiązania na drugim, musi być uzupełnione trzecim elementem — owym demonem ludzkiej omylności, odciskającym swe piętno na adekwatności wspomnianego opisu.
Pierwszym z założeń wspomnianej metodologii jest wybór takiego stylu programowania, który a priori zmniejsza prawdopodobieństwo popełniania błędów. Często zdarza się na przykład tak, iż wskutek zwykłej pomyłki przy wpisywaniu kodu programu otrzymujemy inną poprawną konstrukcję, nie wzbudzającą żadnej podejrzliwości ze strony kompilatora, lecz całkowicie sprzeczną z naszymi intencjami. Jeżeli świadomi tego faktu zastosujemy taki styl programowania, by błąd typograficzny równoważny był w większości przypadków błędowi syntaktycznemu, znakomita większość takich „czeskich błędów” wykryta zostanie już na etapie kompilacji.
Drugie założenie opiera się na praktycznym spostrzeżeniu, iż najgroźniejszymi błędami są błędy głęboko ukryte, manifestujące się bardzo rzadko i niespodziewanie. Błędy o wyraźnie widocznych i dających się reprodukować na żądanie konsekwencjach łatwiej mogą zostać rozpoznane i usunięte — należy więc dążyć do nadawania tworzonemu programowi jak największej „wrażliwości” w tym względzie.
I wreszcie — nawet najgorszy błąd w oprogramowaniu nie okaże się skrajnym nieszczęściem, jeżeli nie spowoduje utraty cennych danych. Zdając sobie sprawę z mniejszej lub większej zawodności tworzonego programu pamiętajmy o tym, iż bezpieczeństwo przetwarzanych danych jest zadaniem najważniejszym.
Wymienione trzy założenia, akcentujące (kolejno) odporność, wrażliwość i bezpieczeństwo tworzonego oprogramowania składają się na swoistą filozofię zwaną programowaniem defensywnym — nazwa ta wydaje się wystarczająco dobitnie odzwierciedlać ów „obronny” aspekt programowania, jakim jest walka programisty ze skutkami własnej niedoskonałości. Mimo dość ogólnego charakteru przedstawionych zasad, filozofia ta nie jest jednak wyłącznie zbiorem pobożnych życzeń, lecz z powodzeniem stosowana jest w praktyce — czego jednym z dowodów jest właśnie niniejsza książka. Jej autor, doświadczony programista, kierujący niegdyś zespołem programistycznym w firmie Microsoft, w przejrzysty sposób prezentuje rozmaite detale programowania defensywnego na prostych przykładach wyrażonych w języku C. Mimo pewnej tendencji do formułowania prezentowanych stwierdzeń w postaci lakonicznych reguł, treść książki bardzo daleka jest od zbędnego „teoretyzowania”. Jej wyjątkowo praktyczny charakter przejawia się również w tym, iż obok zalecanych wzorców postępowania programistycznego autor nie szczędzi przykładów zachowań niepożądanych, których był świadkiem podczas wieloletniej współpracy z licznym gronem programistów o zróżnicowanych cechach charakterologicznych. W szczególny sposób akcentuje pierwszorzędną rolę programisty w dziele tworzenia niezawodnych programów i wyraźnie piętnuje tendencję do spychania odpowiedzialności za błędy na zespoły testujące.
Skoro mowa o różnorodnych zabiegach mających na celu zwiększenie niezawodności tworzonego oprogramowania, to nie mogę powstrzymać się od pewnej refleksji osobistej. Rozpocznę od przypomnienia pewnego znanego w świecie programistów błędu, którego konsekwencją było unicestwienie projektu kosztującego miliony dolarów. Były wczesne lata sześćdziesiąte i jedynym dostępnym językiem algorytmicznym był osławiony FORTRAN. Poniższy fragment
DO 10 I = 1,10
...
instrukcje
...
10 CONTINUE
powoduje dziesięciokrotne wykonanie ciągu instrukcji dla zmieniającej się od 1 do 10 zmiennej I. Jeżeli jednak pomyłkowo zamienić przecinek na kropkę, pierwsza z cytowanych instrukcji zmienia się w instrukcję
DO 10 I = 1.10
dokonującą — uwaga — przypisania wartości 1.10 do zmiennej o nazwie DO10I. Język FORTRAN zezwala bowiem zarówno na używanie zmiennych bez ich deklarowania, jak i na dowolne wplatanie spacji do nazw zmiennych (spacje takie ignorowane są przez kompilator). Następujący dalej ciąg instrukcji wykonywany jest jednokrotnie, dla przypadkowej (lub wynikającej z poprzednich obliczeń) wartości zmiennej I. Tak błaha i jednocześnie kosztowna pomyłka nie mogłaby się wydarzyć ani w C, ani w Pascalu, gdzie obowiązkowe jest zarówno deklarowanie wszystkich używanych zmiennych, jak i wymóg „spójności” identyfikatorów. Przykład ten udowadnia bardzo istotną tezę — tę mianowicie, iż jednym z elementów programowania defensywnego może być wybór odpowiedniego języka programowania. Autor książki pomija tę kwestię zupełnym milczeniem, ograniczając się wyłącznie do języka C — co jest poniekąd zrozumiałe wobec faktu, iż język ten był bodaj jedynym powszechnie stosowanym w firmie Microsoft w czasach, gdy powstawał oryginał niniejszej książki.
Tak się składa, iż moim ulubionym językiem programowania jest Pascal, z którym związany jestem niemal od samych jego początków (dokładniej — od jego pierwszej dostępnej w Polsce implementacji na CDC CYBER 70). Wielokrotnie słyszałem i czytałem, jak programiści posługujący się językiem C++ odmawiają Pascalowi miana „poważnego” języka, ograniczając jego rolę wyłącznie do celów edukacyjnych. Jakby na przekór tym opiniom niedawno ukazała się szósta już wersja (opartego na rozbudowanym Pascalu) Delphi, nie to jest jednak najważniejsze: celem moich osobistych wywodów jest zwrócenie uwagi na fakt, iż Pascal jako taki charakteryzuje się znacznie większym stopniem defensywności niż C czy C++ — kto nie wierzy, niech przeanalizuje prezentowane w niniejszej książce przykłady rozmaitych błędów i zastanowi się, które z nich nie mogłyby wystąpić w Pascalu (nie zezwalającym np. na używanie instrukcji przypisania w roli wyrażeń, czy samodzielne definiowanie przeciążonych operatorów). Być może ów argument w obronie zasłużonego Pascala okaże się komuś pomocny w wyborze pomiędzy Delphi a C++Builderem jako potencjalnymi narzędziami do realizacji określonego projektu.
I jeszcze jedno: zdecydowaliśmy się na wydanie niniejszej książki, mimo iż jej oryginał powstał przed niemal ośmioma laty... W technologii informatycznej to przecież cała epoka — tymczasem prawie całość prezentowanego materiału wciąż zachowuje swą aktualność, co z jednej strony jest dowodem na istnienie pewnych uniwersalnych idei tkwiących u samej istoty tworzenia oprogramowania, z drugiej natomiast wymaga odrobiny dystansu czytelnika do pewnej szczególnej kwestii. Otóż w sytuacji rozpowszechnienia się systemów operacyjnych wykorzystujących chroniony (protected) tryb pracy procesora (m.in. Windows 9x) wiele błędów programistycznych prowadzi do wykonywania operacji nielegalnych z punktu widzenia samej architektury komputera, wskutek czego błędne programy często kończą się błędem ochrony dostępu, zamiast działać w sposób losowy i (co ważniejsze) destrukcyjny; można zaryzykować stwierdzenie, iż wzrost wspomnianej „defensywności” nie ominął także sprzętu komputerowego. Szczególnie podatnymi na awaryjne zakończenie są programy dokonujące odwoływania się do „nie swoich” obszarów pamięci lub posługujące się wskaźnikami „wiszącymi”, zerowymi albo niezainicjowanymi. Z treści książki można wywnioskować, iż autor obraca się raczej w kręgu trybu adresowania bezpośredniego (real mode — chociaż nie brak wzmianek o błędach ochrony dostępu) , w którym błędy tego rodzaju często przechodzą niemal niezauważone. Nie zmienia to oczywiście w niczym faktu, iż programy przejawiające wspomniane zachowanie są programami ewidentnie błędnymi, niezależnie od platformy systemowej, na której są uruchamiane.
Zagadnienie niezawodności oprogramowania, a tym bardziej — metodologii programowania prowadzącej do owej niezawodności — wciąż jest zagadnieniem znacznie mniej sformalizowanym niż wiele innych zagadnień algorytmicznych. Niniejsza książka nie pretenduje więc do miana jakiegoś oficjalnego podręcznika na ten temat (taki podręcznik trudno byłoby zrealizować w formie książki o tak małej objętości), stanowi raczej rezultat osobistych (i notabene bardzo pouczających) doświadczeń autora. Jest więc rzeczą naturalną, iż wielu programistów-praktyków mogłoby wzbogacić jej treść o własne uwagi, czy nawet zakwestionować zasadność niektórych poglądów autora. Ciekawi jesteśmy opinii naszych Czytelników w tym względzie i oczekujemy na nie w naszej internetowej księdze gości. Zaawansowanym Czytelnikom proponujemy ponadto pouczające doświadczenie, polegające na przejrzeniu kodu źródłowego jakiegoś popularnego oprogramowania — na przykład Delphi, czy rozmaitych odmian Linuksa — i zwrócenie uwagi na te jego fragmenty, których określona postać podyktowana jest względami defensywności oprogramowania. Być może zaowocuje to większą niezawodnością własnego, tworzonego teraz i w przyszłości, oprogramowania.
Mamy nadzieję, iż niniejsza książka przyczyni się do wielu sukcesów w walce z plagą dokuczliwych błędów, krążących wciąż po oprogramowaniu i czyhających na nic nie podejrzewających programistów. Jeżeli przyczyni się ona do uniknięcia chociaż jednej nieprzespanej nocy albo zapobieże stracie wakacji, to będzie oznaczać, że było warto — z nadzieją na co pozostajemy.
W imieniu wydawnictwa
Andrzej Grażyński
w listopadzie 2001
10 Niezawodność oprogramowania
Przedmowa do wydania polskiego 7
10 D:\Roboczy\Niezawodność oprogramowania\9 po skladzie 1\r00-1.doc
D:\Roboczy\Niezawodność oprogramowania\9 po skladzie 1\r00-1.doc 7