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.
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:
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.
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.
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.
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.
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.
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.)
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
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.
Żródło: Praktyka Programowania, Wydawnictwo Naukowo-Techniczne, Warszawa 1982