Zasady poprawnego i przejrzystego programowania w Zasady poprawnego i przejrzystego programowania w Pascalu



Na podstawie książki Davida M. Chessa Programmming in IBM PC DOS Pascal (Prentice-Hall, New Jersey, 1985) opracował J.  Kobus

Pisanie programów komputerowych to szczególnego rodzaju umiejętność, dzięki której można sprawić, aby komputer wykonywał  za nas i dla nas pewne ściśle określone zadania. Jeśli te zadania są niewielkie, to i program komputerowy będzie mały i jego napisanie nie przysporzy większych kłopotów. Jeśli natomiast program jest duży, złożony lub bardzo ważny, to wyłącznie jego napisanie nie kończy sprawy. Taki bowiem program musi być niezawodny, łatwy dla innych do zrozumienia i ewentualnego poprawienia lub rozszerzenia. Te ostatnie wymagania oznaczają, że program musi oznaczać się przejrzystą konstrukcją i musi być tak napisany, aby dla innych programistów (lub jego autora po upływie pewnego okresu czasu) było zrozumiałe, co dany program i każda z jego części robią.

Sposób programowania lub inaczej technika programowania, która ułatwia pisanie programów spełniających powyższe wymagania nazywa się programowaniem strukturalnym.

Trzeba wyrażnie powiedzieć, że nie istnieją naukowe zasady pisania dobrych programów. Pisanie programów jest swego rodzaju umiejętnością i pewne ogólne reguły dobrego programowania wynikają z doświadczenia ludzi, którzy się tym rzemiosłem parają, a nie z jakichś abstrakcyjnych rozważań lub zasad. Są to tak zwane rules of thumb, czyli zasady praktyczne. Trzeba w tym miejscu zaznaczyć, że część reguł, o których będzie dalej mowa, nie dotyczy wyłącznie programowania w języku Pascal, ale także w inych językach wysokiego poziomu, takich jak FORTRAN lub C.

1  Elementy stylu programowania

1.1  Układ programu

Pascal jest językiem programowania nie narzucającym programiście żadnych wymagań co do formy zapisu instrukcji i deklaracji. Z punktu widzenia kompilatora programista może umieścić w jednej linii tyle instrukcji (deklaracji), ile tylko może pomieścić i może zacząć pisanie instrukcji od dowolnej kolumny (za wyjątkiem ciągu znaków stanowiących tekst). Dla zilustowania rozważań posłużymy się krótkim i prostym programem p.t. Sygnal. Zgodnie z tym co zostało napisane powyżej program Sygnal może wyglądać następująco:

 program n_sy; uses  Crt; const cz=3000;  
 dl = 100;  st = #27; 
 var l_sy: Integer; procedure syg (l_sy, 
 cz, dl: Integer); var n: 
 Integer; begin n:= l_sy; while  n > 0  do 
 begin Sound (cz); Delay (dl); NoSound;  
 Delay(n*50); Dec(n); end end; begin  repeat  ClrScr;  
 writeln;  write  ('Ile sygnalow ma byc wyemitowanych? '); 
 read (l_sy); syg (l_sy, cz,  
 dl);  until ReadKey=stop  end. 

Nie trzeba nikogo przekonywać, że nie jest to program napisany przejrzyście i łatwy do czytania i zrozumienia, chociaż w istocie jest on bardzo prosty. Przy odrobinie wysiłku można go jednak napisać nieco inaczej:

    program n_sygnal; 
    uses 
       Crt; 
    const 
       cz =3000;  
       dl = 100; 
       st = #27; 
    var  
       l_sy: Integer; 
    procedure syg (l_sy, cz, dl: Integer); 
       var 
          n: Integer;  
       begin 
          n:=l_sy; 
          while  n > 0  do  
             begin  
                Sound(cz); 
                Delay(dl);  
                NoSound;     
                Delay(n*50); 
                Dec(n);      
             end 
       end; 

    begin  
       repeat 
          ClrScr;  
          writeln; 
          write  ('Ile sygnalow ma byc wyemitowanych? '); 
          read(l_sy); 
          sygnal(l_sy,cz,dl); 
       until ReadKey=st  
    end.   

Powyższa wersja programu została napisana w oparciu o następujące reguły:

  1. W każdej linii występuje pojedyncza instrukcja.
    Dzięki temu program nie staje się zbyt gęsty i można stosować wcinanie dla poszczególnych instrukcji
  2. Każde słowo end jest umieszczone w oddzielnej linii.
    Ponieważ słówko end określa koniec instrukcji złożonej, więc jego wyodrębnienie pozwala uwidocznić strukturę powstającą z kolejnych zagłębień instrukcji.
  3. Poziomy wcinania.
    Jeśli jakaś instrukcja występuje w ramach instrukcji złożonej takiej jak begin ... end, case ... end, repeat ... until, etc., to pisana jest ona z dodatkowym wcięciem (z poprzedzjącymi ją dodatkowymi spacjami) w stosunku do instrukcji macierzystej. Np. można napisać

                   for i:=1 to n do write(i); 
             
    ale lepiej zużyć dodatkową linię programu i napisać

                   for i:=1 to n do 
                      write(i); 
             
    Różnica jest niewielka, a ułatwia pobieżne czytanie programu.

  4. Porządkowanie zdań typu else.
    eśli instrukcja if zawiera oprócz then także zdanie else, to zdanie to powinno pojawić się w osobnym wierszu pod słowem then, żeby zaznaczyć, że odgrywają one tę samą rolę w instrukcji if. Zatem należy pisać

                 if i<10 then i:=i+1 
                         else i:=i+2*i;
                
    lub

                 if i<10 
                    then i:=i+1 
                    else i:=i+2*i; 
               
    Stosowany jest jednak także zapis:

                   if i<10  then i:=i+1 
                   else i:=i+2*i;
               
    Należy jednak unikać zapisu jednowierszowego:

                if i<10  then i:=i+1 else i:=i+2*i;
             

Za wymienionymi właśnie czterema regułami dotyczącymi rozmieszczania instrukcji kryje się następująca zasada:

R-1 Program powinien być tak napisany, aby tylko na podstawie rozmieszczenia instrukcji można było powiedzieć, jak każda instrukcja programu ma się do pozostałych.

Ta zasada określa pewien ideał, który nie zawsze daje się osiągnąć. Jeśli tylko pojawia się grożba powstania dwuznaczności, co do tego jaką instrukcję złożoną kończy konkretne end lub jakie instrukcje należą do danego bloku repeat ... until, to należy posłużyć się komentarzem, aby tę niejasność usunąć.

Nie należy oczywiście powyższych reguł stosować po doktrynersku. Jeśli przejrzystość programu może zyskać przez złamanie którejś z nich, to bez wahania trzeba poświęcić zasadę dla uzyskania lepszego zapisu deklaracji bądż instrukcji. Poza tym można stosować własne reguły lub konwencje. Rzecz w tym jednak, aby trzymać się ich konsekwentnie.

1.2  Wybór identyfikatorów

Wybierając identyfikatory, czyli nazwy dla stałych, zmiennych, procedur, itp. powinniśmy kierować się oczywistą zasadą:

R-2 Identyfikator powinien opisywać obiekt, który wskazuje (identyfikuje).

Zasada wydaje się oczywista i prosta, ale w praktyce jej stosowanie może nastręczać pewne problemy. W Pascalu identyfikator może być złożony z 64 znaków (alfanumerycznych plus znak podkreślenia). Tworzenie zatem nazw adekwatnych do obiektów nie powinno rodzić zbyt dużych kłopotów. Już jednak niewielka praktyka w programowaniu podpowiada, że identyfikator nie powinien być ani zbyt długi, by nie był uciążliwy w posługiwaniu się, ani zbyt krótki, bo przestaje opisywać obiekt do którego się odnosi. Pascal ma tutaj zdecydowaną przewagę nad tymi językami, które dopuszczają identyfikatory 6 lub 8 znakowe, a w szczególności BASICiem, który ogranicza nazwy tylko do dwóch znaków. Po zastosowaniu reguły R-2 do naszego przykładu otrzymujemy

 program n_sygnal; 
 uses 
    Crt; 
 const 
    czestoc=3000;  
    dlugosc = 100; 
    stop    = #27; 
 var  
    l_sygnalow: Integer; 
 procedure sygnal (l_sygnalow, czestosc, dlugosc: Integer); 
    var 
       n: Integer;  
    begin 
       n:=l_sygnalow; 
       while  n > 0  do  
          begin  
             Sound(czestosc); 
             Delay(dlugosc);  
             NoSound;     
             Delay(n*50); 
             Dec(n);      
          end 
    end; 
 
 begin  
    repeat 
       ClrScr;  
       writeln; 
       write  ('Ile sygnalow ma byc wyemitowanych? '); 
       read(l_sygnalow); 
       sygnal(l_sygnalow,czestosc,dlugosc); 
    until ReadKey=stop  
 end.

1.3  Typy i stałe

Odnośnie deklarowania typów i używania stałych mamy dwie następujące zasady:

R-3 Jeśli jakaś liczba występuje co najmniej dwukrotnie dla tych samych powodów, to należy nadać jej nazwę i posługiwać się tylko nazwą.



R-4 Jeśli ten sam opis typu występuje w więcej niż jednej deklaracji zmiennych i spełnia on tę samą funkcję, to należy nadać mu tę samą nazwę i posługiwać się tą nazwą.

Przestrzeganie powyższych zasad ułatwia ewentualne późniejsze zmiany w programie. Jeśli wartość stałej ulega zmianie, to wystarczy zmiany dokonać tylko w jednym miejscu w programie i zmiana ta automatycznie dotyczy wszystkich miejsc, gdzie dana stała jest używana.

1.4  Komentarze

Rozsądne stosowanie komentarzy przyczynia się wydatnie do uczynienia programu (kodu programu) bardziej zrozumiałym. Wszystkie szczególnie złożone fragmenty programu powinny być opatrzone stosownym objaśnieniem. Także wszystkie funkcje i procedury powinny być poprzedzone pełnym opisem ich działania i parametrów, z których korzystają. Nie należy jednak z komentarzami przesadzać. Dobrze jest kierować się następującą zasadą:

R-5 Komentarz należy umieść wszędzie tam, gdzie ewentualny czytelnik mógłby mieć pytania dotyczące kodu programu.

Jeśli masz wątpliwości dodaj objaśnienie. Duża liczba objaśnień jest błędem, ale jeszcze większym błędem jest ich zbyt skąpa liczba (Dobrym zwyczajem jest by po zakończeniu pisania programu prześledzić go jeszcze raz przy założeniu, że się go nie zna i umieścić objaśnienia tam, gdzie samemu ma się wątpliwości.) Po uwzględnieniu reguły R-5 program Sygnal mógłby wyglądać następująco:

 { Program  wysyla n krotkich sygnalow dzwiekowych o czestosci 
   3000 Hz i czasie trwania 100 milisekund (n jest wczytywanym 
   parametrem). Po zakonczeniu emisji nacisniecie dowolnego  
   klawisza oznacza powrot do poczatku programu (program  
   oczekuje nowej wartosci n). Nacisniecie natomiast klawisza 
   Esc konczy program.} 
 
 program n_sygnal;  
 uses 
   Crt; }Dolaczenie pakietu procedur Crt. W szczegolnosci pakiet 
          ten zawiera nastepujace procedury: Sound, NoSound,  
          Delay, ReadKey} 
 const 
   czestosc=3000; {czestosc emitowanych sygnalow} 
   dlugosc = 100; {czas trwania poszczegolnego sygnalu w milisek.} 
   stop    = #27; {numer 27 odpowiada w ASCII klawiszowi Esc } 
 var 
   l_sygnalow: Integer; {liczba generowanych sygnalow} 

{ ----------------  Procedura SYGNAL  ----------------} 
{ Procedura ta wysyla okreslona liczbe sygnalow dzwiekowych  
    o podanej czestosci i czasie trwania poszczegolnego sygnalu.
    Odstep pomiedzy sygnalami zmienia sie i jest rowny n*50 
    milisekund, gdzie n jest liczba sygnalow, ktore pozostaly  
    do wyemitowania. 

   Parametry procedury: 
     l_sygnalow - liczba sygnalow dzwiekowych  
     czestosc   - ich czestosc w Hz  
     dlugosc    - czas ich trwania w milisekundach 
  
   Procedura nie zmienia wartosci tych parametrow.       } 
   
 procedure sygnal (l_sygnalow, czestosc, dlugosc: Integer); 
   var 
     n: Integer; {zmienna robocza potrzebna, by parametr  
                   l_sygnalow nie  ulegl zmianie} 
   begin 
     n:=l_sygnalow; 
     while  n > 0  do 
       begin 
         Sound(czestosc); {emisja pojedynczego sygnalu}  
         Delay(dlugosc);  {czas trwania emisji}   
         NoSound;     {przerwa w emisji}   
         Delay(n*50); {na przeciag n*50 milisekund} 
         Dec(n);      {n:=n-1}  
      end 
   end; {------------------------------------------- sygnal} 
 
 begin 
   repeat 
     ClrScr; {procedura z pakietu Crt czyszczaca ekran i   
               ustawiajaca kursor  w gornym lewym rogu ekranu} 
     writeln; 
     write  ('Ile sygnalow ma byc wyemitowanych? '); 
     read(l_sygnalow); { wczytaj liczbe sygnalow, ktore maja  
                          byc wyemitowane } 
     sygnal(l_sygnalow,czestosc,dlugosc); 
   until ReadKey=stop  {petle 'repeat ... until' mozna przerwac 
                         naciskajac klawisz Esc konczac tym  
                         samym program } 
 end. 

2  Modułowość

Przejdżmy teraz od opisu jak powinny wyglądać poszczególne fragmenty programu (układ instrukcji, deklaracji, użycie identyfikatorów i objaśnień) do opisu zasad projektowania takich struktur programu jakimi są funkcje i procedury.

Trzeba przede wszystkim dążyć do tego, aby każdy element programu - czy będzie to funkcja, procedura czy też nawet pojedyncza instrukcja - miały jedyny powód do pojawienia się i aby ten powód był dobrze i jasno określony (nie może być tutaj żadnej dwuznaczności). Należy także dążyć do tego, aby czytelnik programu był w stanie zrozumieć jego fragment bez konieczności stałego odwoływania się do innych jego części. Jedną z najważniejszych własności dobrze napisanego fragmentu programu jest oczywisty flow of control, czyli przekaz sterowania. Dla każdej instrukcji powinno być jasne jakie inne instrukcje mogły być wykonane przed nią, a jakie mogą występować po niej.

Stosowanie instrukcji skoku bezwarunkowego go to zaburza w sposób drastyczny przekaz sterowania. I dlatego

R-6 Należy unikać instrukcji go to.

(Istnieje matematyczny dowód twierdzenia, że każdy (poprawny) algorytm daje się w Pascalu zapisać bez użycia instrukcji go to.)

2.1  Opisy funkcjonowania podprogramów

Właściwy dobór w programie funkcji i procedur jest istotną częścią dobrze zaprojektowanego programu. Podprogramy w postaci funkcji bądż procedur powinny być dostępne w postaci czarnych skrzynek, tzn. opis ich funkcjonowania oraz opis parametrów wejściowych i wyjściowych powinien wystarczyć do poprawnego ich wykorzystania. Nie powinna zatem zachodzić konieczność zaglądania do wnętrza podprogramu i analizowania jego instrukcji w celu określenia co dany podprogram robi i czego wymaga dla swojego prawidłowego działania. Stąd mamy kolejną zasadę dobrego programowania:

R-7 Każda procedura (funkcja) powinna mieć do wykonania ściśle określone zadanie i to zadanie powinno być w zupełności opisane w procedurze (funkcji).

W celu ułatwienia korzystania z podprogramów przez innych użytkowników lub w innych programach należy unikać używania odwołań do zewnętrznych zmiennych, tj. takich zmiennych, które nie są ani lokalne w procedurze (funkcji), ani nie zostały przekazane do jej wnętrza poprzez jej parametry. Jeśli już trzeba to zrobić, to fakt ten powinien być wyrażnie zaznaczony w opisie podprogramu. Czasami może się zdarzyć, że poprawne funkcjonowanie podprogramu zależy od wartości pewnych zmiennych; mówimy wówczas, że ten podprogram zależy od tych zmiennych. W zasadzie należy dążyć do usuwania zależności tego typu np. poprzez dodanie do podprogramu odpowiednich instrukcji sprawdzających czy są spełnione warunki poprawnego jego funkcjonowania. W przypadku ich niespełnienia program nie powinien kończyć się błędem, lecz stosownym komunikatem objaśniającym powód niepoprawnego zakończenia się programu.

R-8 W podprogramach należy unikać zewnętrznych odwołań i zależności

3  Projektowanie zstępujące

Dochodzimy w naszych rozważaniach do najwyższego poziomu programowania, tj. do poziomu na którym określa się wzajemne współdziałanie i współzależności pomiędzy podprogramami. W prostym programie związek między jego poszczególnymi częściami jest prosty i kolejność w jakiej są one projektowane nie odgrywa decydującej roli. Napisanie jednak dużego programu wymaga bardzo starannego projektowania. Sprawdzony sposób poprawnego konstruowania tego typu programów polega na podzieleniu zadania, które ma wykonać program na mniejsze części idąc od góry do dołu (od ogólu do szczególu) zawsze mając na uwadze zasadę mówiącą, że

R-9 Każde poszczególne cząstkowe zadanie powinno być małe oraz precyzyjnie i jasno określone.

Znaczy to, że w każdym momencie pracy nad projektowaniem i konstruowaniem programu zadanie, które programista musi rozwiązać powinno być na tyle małe, by było jednoznaczne i łatwe do uchwycenia.

Maksymy programistyczne Dannie van Tassela

Żródło: Praktyka Programowania, Wydawnictwo Naukowo-Techniczne, Warszawa 1982


File translated from TEX by TTH, version 2.01.
On 26 Sep 2002, 19:24.