Cum se programează
-- un set de reguli pentru scrierea eficientă de programe --

Mihai BUDIU - mihaib@pub.ro

februarie 1996

Subiect:
reguli pentru scrierea programelor.
Cuvinte cheie:
comentariu, indentare, modul, variabilă, procedură.
Cunoștințe necesare:
cel puțin un limbaj de programare.


Contents

Introducere

Voi începe cu un citat:

As soon as we started programming, we found to our surprise that it wasn't as easy to get programs right as we had thought. Debugging had to be discovered. I can remember the exact instant when I realized that a large part of my life from then on was going to be spent in finding mistakes in my own programs.
-- Maurice Wilkes discovers debugging, 1949

În traducere asta ar suna cam așa:

Imediat ce am început să programăm am descoperit spre surprinderea noastră ca nu era așa de ușor să facem programe bune pe cît crezusem. Depanarea (debugging) trebuia să fie inventată. Îmi pot aminti cu precizie momentul exact cînd am realizat că o mare parte din viața mea de atunci încolo urma să fie petrecută în a găsi greșeli în propriile mele programe.

De asemenea -- pentru contrast -- îmi place să citez următorul fragment din manualul care însoțește programul MetaFont, scris de marele informatician Donald Knuth:

On January 4, 1986 the ``final'' bug in Metafont was discovered and removed. If an error still lurks in the code, Donald E. Knuth promises to pay a finder's fee which doubles every year to the first person who finds it. Happy hunting.

Adică:

Pe 4 ianuarie 1986 ``ultimul'' bug (defect) din Metafont a fost descoperit și scos. Dacă o eroare mai zace prin cod, Donald E. Knuth promite să plătească o sumă care se dublează în fiecare an celui care o găsește. Vînătoare plăcută.

Cîți dintre dumneavoastră au reușit să scrie un program mai lung de 2000 de linii sursă (într-un limbaj de nivel înalt) care să exhibe același îngrijorător simptom al perfecțiunii?

Cu riscul de a vă agasa voi mai cita un monstru sacru al informaticii, de data asta însă din memorie. Edsger Dijkstra spunea în cuvîntarea sa făcută cu prilejul decernării premiului ``Turing'' al prestigioasei societăți americane ACM (Association for Computing Machinery), cuvîntare care dealtfel se numea ``The Humble Programmer'' (Programatorul Umil), că un bun programator trebuie să recunoască faptul că sarcina ce și-a asumat-o îl depășește cu mult datorită complexității sale, și ca atare trebuie să încerce cît mai mult să o reducă la bucăți mai mici, cît mai ușor de manevrat.

Deși aparent legăturile dintre cele trei citate de mai sus sunt cam șubrede, sper că textul care urmează va arăta că de fapt acestea sunt trei fețe ale aceluiași poliedru.

În rezolvarea unei probleme cu calculatorul se pot distinge mai multe etape (chiar dacă ele nu sunt distincte temporal): analiza problemei, elaborarea algoritmului, implementarea, documentarea, testarea, etc. Acest articol se ocupă de una singură dintre ele, care dealtfel este foarte puțin dependentă de problema care trebuie rezolvată: presupun că știu algoritmii, tot ce trebuie să fac este să-i implementez într-un limbaj de programare oarecare. Aparent banală, etapa aceasta se dovedește adesea foarte intricată. De fapt acest lucru îl arată și primul citat din introducere.

Cele mai multe considerații care urmează sunt independente de limbajul ales.

Regula de aur

Un program trebuie să fie scris în așa fel încît să poată fi citit de către oricine.

Programele sunt făcute în așa fel încît părțile sunt strîns dependente. Trebuie ca cele scrise într-un loc să fie folosite într-altul (și eventual refolosite). Mintea unui om normal nu poate reține toate amănuntele din textul unui program, de aceea trebuie să-i dăm un ajutor.

Sfatul acesta este clar obligatoriu cînd se dezvoltă soft în echipă, pentru că ceea ce scrie unul trebuie să fie folosit de altul. Paradoxal este că el este extrem de oportun chiar cînd întreg procesul de scriere este făcut de un singur programator. Nu vi s-a întîmplat niciodată să vă uitați la o funcție scrisă în urmă cu o lună și să vă mirați că ați putut scrie așa ceva? Sau să vă întrebați ce face? Acel ``oricine'' din sfat poate fi deci un ``alter ego'' al dumneavoastră.

Aparent scrierea îngrijită a unui program consumă prea mult timp.

Pentru programe scurta acest lucru poate fi adevărat. Pentru programe la care lucrați mai mult de o săptămînă timpul cheltuit cu o scriere îngrijită este recuperat înzecit în fazele ulterioare ale dezvoltării (depanare, extindere, întreținere). Dacă nu credeți, încercați măcar o dată.

Împărțirea programului

Fă bucățile în așa fel încît să înțelegi ușor ce face fiecare fără a trebui să știi cum face.

Domnul Dijkstra (în olandeză se citește aproximativ ``daicstra'') recomanda o tactică veche de pe vremea lui Cezar: ``divide et impera'': ``împarte și stăpînește''. Așa e bine să scrieți și programele: în bucăți cît mai independente unele de altele, care interacționează intre ele în moduri cît se poate de clare și simple, și care nu se influențează ``pe la spate''.

Din fericire toate limbajele moderne de programare aduc un suport extrem de eficace acestei metode de scriere: funcțiile, procedurile și modulele.

Împărțiți deci programul dumneavoastră în astfel de părți cît mai des cu putință. Faceți părțile logic independente; funcțiunea fiecăreia trebuie să fie extrem de clară și cît se poate de limpede enunțată. De exemplu, o funcție care caută ceva într-un șir nu trebuie să facă și ordonarea șirului. Faceți două funcții independente pentru asta, una de căutare și una de sortare.

Dacă limbajul permite module, atunci faceți-le în fișiere separate, ale căror nume să fie suficient de clare. Grupați în fiecare modul numai funcții și variabile înrudite. Dacă programați într-un limbaj orientat pe obiecte, implementați fiecare clasă într-un modul separat, cu toate metodele ei. O să știți apoi unde să le căutați, și apoi o să le puteți reutiliza cu ușurință în alte programe.

Cheia economiei de timp este asta: cînd vrei să folosești o funcție trebuie să știi numai ceea ce face; nu trebuie să o citești în întregime ca să vezi cum face acest lucru. De aceea funcția este mult mai ușor de folosit, și apoi poate fi la nevoie rescrisă în cu totul alt fel, fără a influența cîtuși de puțin restul programului. (De exemplu nu contează dacă sortarea este bubble sort sau quick sort, totul este să fie tot o sortare. Funcțiile care folosesc funcția de sortare nu sunt interesate de modul în care aceasta se face). Un alt uriaș beneficiu al acestei relative independențe între feluritele părți este că în momentul în care un bug este descoperit și scos dintr-una, celelalte vor rămîne neschimbate, pentru că nu se bazau pe felul în care lucra acea parte, ci doar pe ceea ce ea făcea.

Cîte funcții sau linii de program trebuie să fie într-un modul? Asta este destul de mult o chestie de gust, dar editorul de texte pe care îl folosiți poate influența mult alegerea. Dacă aveți 24 de linii de ecran (din care 3 folosite la borduri și meniuri) este greu să scrieți fișiere mari, pentru că vă plimbați cu greu prin ele. Alta este situația dacă aveți 42 sau 80 de linii.

Un editor inteligent reduce handicapul unor fișiere mari, pentru că vă permite să va mutați căutînd automat aparițiile funcțiilor și variabilelor.

Eu personal mă descurc cel mai bine cu fișiere între 300 și 700 de linii, conținînd pînă în 15 funcții.

Minima vizibilitate

Principiul minimei vizibilități cere ca un obiect să nu fie vizibil decît părților din program care au cu adevărat nevoie de el. Să vedem niște aplicări ale lui:

Datele trebuie separate cît se poate de mult de corpul programului.

O variantă a acestei reguli este ``în sursa programului nu trebuie să apară nici un număr cu excepția lui 0, 1, sau -1''. Constantele (în particular cele numerice) se mai numesc și ``magic numbers'' (numere magice). Ele trebuie evitate întotdeauna.

Metoda comună este de a da nume simbolice constantelor undeva la începutul fiecărui modul, sau în fișiere speciale (cu const în Pascal și C, cu #define în C, etc.). Numele dat unei constante are multe avantaje, care sunt expuse în mai toate cursurile de programare. Noi vom reaminti decît pe unul: o constantă face programul mult mai ușor de citit. Aceasta este o aplicare a principiului minimei vizibilități: știi numai numele constantei (care trebuie să arate la ce folosește ea), și nu valoarea ei!

Iată un exemplu:

for i:=1 to 128 do persoana[i].nume := '';

și tradus:

for p:=1 to NumarDePersoane do persoana[p].nume := '';

Nu e mai clar a doua oară?

Cum spuneam părțile din program interacționează te miri cum. Cel mai adesea interacțiunile se fac prin variabile globale, a căror valori pot fi modificate de o funcție și citite de alta.

De aceea este bine să transmitem funcțiilor (procedurilor) toate valorile de care au nevoie prin argumente și să nu le lăsăm să modifice variabile globale.

Dacă vi se pare greu să programați fără variabile globale, aflați că există limbaje de programare în care nu există variabile de loc, și totuși se pot face foarte multe lucruri (un exemplu este Prolog; despre Prolog se crede îndeobște că are variabile, însă o analiză atentă va arăta că ceea ce în Prolog se numește variabilă nu folosește de loc la același lucru ca o variabilă în sensul uzual al limbajelor imperative ca Pascalul).

În realitate este foarte greu adesea să ne descurcăm fără variabile globale, pentru că unele funcții fac modificări atît de masive încît ar trebui să aibă zeci de parametri și să returneze tot atîtea valori (ceea ce multe limbaje nici nu permit, dealtfel!). Tocmai pentru a circumveni aceste dezavantaje au fost inventate limbajele orientate pe obiecte (alde C++), în care în loc să dai unei funcții argumente, grupezi argumentele la un loc într-un obiect (un fel de record din Pascal); funcția însăși devine un cîmp al obiectului (se va numi ``funcție membru''), și în loc să chemi funcția cu cîmpurile obiectului ca argumente, rogi obiectul să-și aplice singur funcția.

Această reducere la maximum a vizibilității din limbajele orientate pe obiecte (căci cîmpurile unui obiect sunt în mod normal accesibile numai funcțiilor care-i sunt membre) se numește ``încapsulare'' și constituie unul din avantajele majore ale programării orientate pe obiecte față de cea ordinară.

Cu disciplină însă se pot aplica beneficiile încapsulării și în programe scrise în limbaje fără astfel de construcții (C, Pascal), cu foarte mult succes.

Spațiile albe; i(n)dentarea

Unii separă părțile din programe prin comentarii baroce ca:

{******************************}
{========intrare/iesire========}
{******************************}

Ei bine, puține spații albe judicios plasate fac mult mai mult bine lizibilității programului. Iată care sunt recomandările mele în ceea ce le privește:

  1. Separați fiecare funcție de vecinele ei printr-o linie albă.

  2. Între titlul funcției și corpul ei eu las mereu un rînd în care pun de obicei comentarii.

  3. Între declarațiile variabilelor locale și corpul funcției las mereu o linie liberă.

  4. Puneți mai multe linii, și eventual un comentariu de o linie de steluțe între părți majore ale programului.

  5. Am observat că o expresie este mult mai ușor de citit dacă separ cu spații unii operatori de argumentele lor. Observați:

    for(i=0;i<ultim;i++) if(!a[i]) total++;
    

    față de:

    for (i = 0; i < ultim; i++) if ( ! a[i] ) total++;
    

  6. Pentru a ușura depanarea este bine să puneți o singură instrucțiune pe linie! Pentru că linia de mai sus la depanare va executa întreg ciclul for la o singură comandă ``step'', iar liniile de mai jos vor permite să vedeți ce se întîmplă la fiecare pas:

    for (i = 0; i < ultim; i++) 
      if ( ! a[i] ) total++;
    

  7. Dacă aveți prea multe argumente la o funcție scrieți așa:

         venit(persoana[pozitie_curenta].nume, 
               departament[persoana[pozitie_curenta].cod_departament],
               indexare_salarii);
    

  8. Dacă trebuie să spargeți o expresie pe două linii lăsați operatorul la începutul celei de-a doua linii.

            venit_max = persoana[pozitie_curenta].venit
              + crestere_salariu;
    

  9. Logica expresiei rupte pe mai multe linii trebuie să fie clară:

            copii := numar_copii( indice[disc_curent, compartiment,
                                         raft_curent],
                                  eticheta);
    

  10. Nu calculați prea multe lucruri într-o singură expresie complicată, pentru că după aceea n-o să înțelegeți nici dumneavoastră ce ați vrut:

    i := 1;
    c := 'y';
    while (i < maxN) and not (a[i] < 0) and not (c = 'n') do begin
      i := i + 1;
      read(f, c)
    end
    

    ci:

    i := 1;
    gata := false;
    c := 'y';
    while not gata do begin
      if i >= maxN then gata := true;
      if a[i] < 0  then gata := true;
      if c = 'n'   then gata := true;
      if not gata  then begin
         i := i + 1;
         read(f, c)
      end
    end
    

  11. În engleză ``to indent'' înseamnă ``a zimțui''. În româna s-a format barbarismul ``a identa'' sau ``a indenta''. care înseamnă a aranja frumos în pagină un program, în așa fel încît structura lui să fie limpede. Mai toate limbajele moderne sunt indiferente la numărul de spații, așa că folosiți-le ca să indicați structura programului. Exemple se găsesc și mai sus.

    Regula de bază este ca o instrucțiune care depinde de altă instrucțiune să fie scrisă puțin mai la dreapta. De pildă if arată așa: if <expresie> then <instructiune1> else <instructiune2>. <instructiune1,2> depind de if. Un if se scrie deci așa:

            if TrebuieSters then
               StergeJucator(BazaDeDate, NumarulDeOrdine)
            else
               calificari(BazaDeDate, NumarulDeOrdine);
    

  12. Dacă instrucțiunea dependentă este o instrucțiune bloc, aveți mai multe posibilități de a o aranja în pagină. Totul e să alegeți una și să o respectați mereu.

            for i := 1 to ultim do begin
               CalculeazaNota(i);
               AlegePostura(i)
            end;
    

    sau

            for i := 1 to ultim do 
               begin
               CalculeazaNota(i);
               AlegePostura(i)
               end;
    

    sau

            for i := 1 to ultim do 
               begin
               CalculeazaNota(i);
               AlegePostura(i)
            end;
    

    sau

            for ( i = 1; i <= ultim; i++) 
              {
                calculeaza_nota(i);
                alege_postura(i);
              }
    

  13. Cît de multe spații trebuie să se folosească pentru a indenta? 1 este prea puțin, și structura programului nu apare suficient de clar, 8 este prea mult, pentru că la o indentare de 4 nivele nu mai ajunge ecranul. Cel mai bine este între 2 și 6.

  14. Etichetele de la goto sau case se indentează puțin mai în stînga:

      repeta:
        switch(raspuns) {
        case 'd': 
    	stiut = DA;
    	break;
        case 'n':
    	stiut = NU;
    	break;
        case '?':
    	interogare();
    	goto repeta;
        default:  /* alt raspuns */
    	return;
        }
    

  15. Construcțiile de genul else if sunt o excepție de la regula de indentare. Ele pot fi scrise în loc de:

            if e1 then cutare1
            else 
               if e2 then cutare2
               else 
                  if e3 then cutare3
                  else cutare3
    

    așa:

            if e1 then cutare1
            else if e2 then cutare2
            else if e3 then cutare3
            else cutare3
    

    pentru că sunt echivalente cu un case din Pascal (switch din C) -- adică un if cu mai multe condiții, care au aceeași importanță.

Comentariile

Comentariile sunt într-adevăr extrem de utile pentru a ușura înțelegerea funcționării unui program. Rolurile lor sunt multiple, și modul în care se pot folosi extrem de variat.

În primul rînd există două feluri de comentarii:

Primele ocupă o linie (sau mai multe) de program în întregime și explică ceva din funcționarea lui. Iată unde sunt extrem de utile:

  1. Orice fișier (modul) trebuie să conțină pe prima sau a doua linie un comentariu arătînd numele fișierului și explicînd pe scurt la ce folosește. Cînd fișierul este scos la imprimantă se acest comentariu indică proveniența sursei.

  2. Orice fișier este bine să conțină la început comentarii descriind autorul, data și versiunea ultimei modificări ale textului. Există programe speciale (``Source Code Control System'' -- control al codului sursă) care ajută păstrarea și folosirea tuturor versiunilor unui program. În cazul în care mai multă lume lucrează la un proiect sunt extrem de utile.

  3. Fiecare funcție trebuie sa aibă o descriere a comportării ei. Vedeți mai jos secțiunea despre funcții. Aceste comentarii se pot întinde pe mai multe linii.

  4. Algoritmii mai complecși trebuie să fie descriși printr-un comentariu care explică sumar comportarea lor. Acest comentariu în mod normal se plasează înaintea zonei de cod care implementează algoritmul.

  5. Comentariile se indentează la fel cu liniile de cod de care țin:

            for i := 1 to sfirsit do
              { verific toate pozitiile }
              verifica( pozitie[i] );
    

Comentariile de sfîrșit de linie în general explică operația făcută pe linia curentă. Ele sunt mult mai greu de întreținut, pentru că schimbarea uneia din linii strică întreaga lor aliniere.

  1. Ele trebuie să însoțească obligatoriu declarațiile variabilelor globale:

    var
      persoane,                     { numar de participanti       }
      locuri,                       { numarul de scaune la masa   }
      LocCurent: integer;           { locul pe care incerc sa pun }
    

  2. Orice linie care face o acțiune mai deșucheată (al cărei sens nu este limpede) trebuie să fie comentată:

      /* elementele nu pot fi 0 */
      for ( i = 0; i < nr_elem; i++ ) {
        if ( a[i] == 0 ) break;   /* eroare! opresc procesarea */
        if ( a[i] < a[i+1] ) swap (&a[i], &a[i+1]);
      }
    

  3. Variabilele locale ale unei funcții trebuie să fie cîteodată comentate.

  4. Mai rar se comentează argumentele unei funcții în declarația ei:

    function arata(x, y, {coordonatele din stinga-sus}
                   lungime, latime: integer): boolean;
    

  5. Blocurile la mare distanță de testele care le controlează trebuie să fie comentate astfel (vedeți la else):

    if ( functionare == INTERACTIV ) {
       int alege;
    
       printf("\a\a\a");           /* un pic de galagie */
       alege = meniu_principal();  /* selectia          */
       switch (alege) {
         case FISIER: 
            fisier();              /* toata intrarea/iesirea */
            break;
         case CALCUL: 
            calcul();
            break;
         default:     
            return 0;               /* nimic valid -> termin */
       }
    }
    else {                 /* aici functionare != INTERACTIV */
            ...
    }
    

Variabilele, constantele, funcțiile, procedurile

Dacă nu poți să-i dai un nume unei variabile înseamnă că acea variabilă nu este bună. Reconsideră arhitectura întregului program.

Într-adevăr, nu trebuie să faci nici un efort ca să-ți aduci aminte ce reprezintă o variabilă; numele ei trebuie să-ți spună tot ce te interesează.

Nu faceți niciodată economie înghesuind în aceeași variabilă mai multe valori, depinzînd de context! Fiecare variabilă trebuie să reprezinte un singur lucru în fiecare clipă. Compilatoarele moderne sunt suficient de inteligente ca să transforme cele două variabile în una singură dacă asta se poate! (De fapt le chiar ușurați munca neînghesuind mai multe lucruri laolaltă).

Numele unei variabile trebuie să arate exact ce reprezintă valoarea ei.

Numele variabilelor, funcțiilor, procedurilor, argumentelor lor, trebuie să fie de asemenea ușor de reamintit. Dacă scrii un program și trebuie mereu să te duci la prima pagină ca să vezi dacă o variabilă se cheamă max_n. sau max_numere sau maximum_numere treaba merge destul de greu. În principiu nu se folosesc prescurtări. Un nume de variabilă ca x sau total este absolut inutil, căci orice poate fi x. Scrieți x_fereastra sau total_venit de pildă.

Nu trebuie să va fie lene să tastați. De altfel un editor foarte bun (ca de exemplu GNU Emacs) vă ajută foarte mult (Emacs la apăsarea tastei M-/ încearcă să completeze cuvîntul curent cu un sufix care se regăsește undeva prin text. De pildă dacă am definit variabila complexitate_totala. atunci probabil ajunge sa tastez com și apoi M-/ ca Emacs să completeze întreg numele).

Nu scrieți funcții care în funcție de un argument fac două lucruri diferite:

int operatie(FILE * f, struct persoana * p, int actiune)
/* daca actiune = 0 -> cauta de cite ori apare persoana p in fisier
 * daca actiune = 1 -> sterge toate aparitiile p din f
 */

Scrieți două funcții, pe care le chemați dintr-un if sau switch (case).

Este util să folosiți notații diferite pentru constantele numite pentru a le distinge de variabile. În C, în care contează literele mari/mici, aceste constante au de obicei numele scrise numai cu majuscule.

Un program cu nume de variabile eficient alese are nevoie de mult mai puține comentarii, și este mult mai ușor de citit și întreținut. Cineva spunea că pot fi citite la gura sobei, ca o carte bună. Am avut ocazia să citesc astfel de programe și trebuie să recunosc că am învățat extrem de mult din ele. Pentru curioși recomand doi monștri sacrii ai programării cristaline, Andrew S. Tanenbaum (autorul sistemelor de operare MINIX și Amoeba) și Richard Stallman (autorul compilatorului GCC și al lui GNU Emacs).

Dacă scrieți module care exportă multe variabile sau funcții puteți prefixa toate obiectele unui modul cu un text care le arată proveniența și semnificația. De exemplu toate procedurile care lucrează cu ferestre se vor numi ceva de genul Win_xxx (win de la ``window''.. Aceasta complică și simplifică deopotrivă unele lucruri. E mai simplu pentru că poți avea o procedură make și pentru ferestre, și pentru stive, numai că una se cheamă Win_make iar alta Stk_make.

Dacă vă decideți pentru prefixe, în general ele sunt preferabil sufixelor. Adică e mai bine Win_make decît make_win. pentru că prefixele se văd mai ușor. În nici un caz nu amestecați într-un program cele două feluri de numire.

Apropos de amestecare, faceți la început niște decizii de numire omogenă a obiectelor cu care operați. De exemplu: ``toate comentariile sunt în engleză'', sau ``toate numele de proceduri sunt cuvinte englezești'', ș.a.m.d. În program coexistă greu o procedură numar_persoane cu una first_person. (Dacă nu știți engleză lucrurile sunt ceva mai simple.)

Fiabilitatea

``Nu avea încredere în nimeni, nici măcar în tine însuți!'' Respectarea acestei reguli aduce niște beneficii greu de imaginat. Ce înseamnă ea pentru un programator?

  1. Funcțiile nu trebuie niciodată să se bazeze pe cel care le cheamă, cum că le furnizează argumente corecte. Verificați toate argumentele înainte de a le prelucra; dacă nu sunt corecte anunțați eroarea, fie scriind ceva pe ecran, fie returnînd o valoare indicînd eroare utilizatorului.

  2. Funcțiile nu trebuie să se bazeze niciodată pe cele pe care le cheamă. De fiecare dată cînd ați chemat o altă funcție verificați dacă nu cumva a semnalat o eroare. Dacă da acționați în consecință, și opriți procesarea.

  3. Un caz particular al lui 2. este apelul unor funcții din biblioteci. Mai ales cele care fac intrare/ieșire pot eșua din zeci de motive. Verificați mereu dacă au funcționat înainte de a vă arunca cu capul înainte. Dacă vă e lene, scrieți funcții ``înveliș'' (``wrappers'') care le cheamă pe cele de bibliotecă și care verifică erorile. Folosiți apoi numai wrapper-ele. De exemplu:

    void * aloca_memorie(size_t marime)
            /* aloca memorie.  eroare fatala daca nu mai este memorie */
    {
            void * p;  /* aici alocam temporar */
    
            p = malloc(marime);
            if (p == NULL) {
                    /* standardul spune ca malloc intoarce NULL numai
                     * daca nu a reusit sa aloce cantitatea indicata */
                    fprintf(stderr, " Alocare nereusita!\n");
                    exit(1);
            }
            return p;
    }
    

    sau (în TURBO-Pascal):

    procedure deschide_fisier(var f:text; nume:string);
            { deschide un fisier; verifica erorile }
    begin
         {$i-}      { inhiba erori de executie pentru intrare-iesire }
         assign(f, nume);
         reset(fis);
         {$i+}      { inversul lui $i- }
         if ioresult <> 0 then begin
            writeln('deschide_fisier: Nu pot deschide fisierul ', nume);
            halt
         end
    end;
    

  4. Din loc în loc introduceți verificări asupra datelor pe care le prelucrați, chiar dacă vi se par inutile. În C există funcția standard assert care este extrem de utilă, dar puteți scrie și în Pascal una asemănătoare. Această funcție oprește definitiv programul dacă este chemată cu un argument 0. Frumusețea este că toate invocările lui assert sunt scoase definitiv din program doar definind macro-ul NDEBUG.

    Metoda universală este următoarea: definiți constanta booleană (sau întreagă) DEPANARE, pe care o faceți true. Apoi în program verificați mereu astfel:

      if DEPANARE then begin
          { verificam daca locasele sunt bine initializate }
          for i := 1 to spatii do 
               if locas[i].urmator = nil then begin
                    writeln('Locas cu "urmator" nil la indicele ', i);
                    halt
               end
      end
    

    Testele pentru locașe se vor efectua numai dacă DEPANARE este true. Dacă faceți DEPANARE false, testele dispar cu desăvîrșire! Puteți astfel să vă depanați programul, iar cînd merge să-l scurtați. Un compilator cu optimizări va observa că liniile de test nu se vor putea efectua niciodată (pentru că DEPANARE este o constantă), și le va scoate cu totul din program. (Pe de altă parte unele teste este înțelept să rămînă pentru totdeauna în program; nu se știe niciodată pentru ce date de intrare programul o ia razna. Este recomandabil să găsești o eroare cît mai repede și să urlii că ai găsit-o decît să o ignori o vreme și ea să facă prăpăd în liniște. Mulți utilizatori vor aprecia.)

  5. Distingeți în programele dumneavoastră (cel puțin) două tipuri de erori: fatale și recuperabile. Scrieți două funcții care tratează aceste tipuri de erori. Toate erorile ar trebui să indice cît mai clar locul unde s-au produs: numele programului, numele modulului, numele funcției, tipul erorii. La depanare nu ajută prea tare dacă vezi ceva de genul: ``eroare de intrare/iesire''. Mai bine e ``statistic: functia `citeste_data': eroare: nu pot deschide fisierul `info.txt'''.

Tocmai din această cauză nu orice eroare trebuie semnalată ca fiind fatală. Nu poți să faci o împărțire la 0? Anunță doar pe funcția care te-a chemat cu argumente greșite că ceva nu e în regulă. Cum ar fi un program de calculator de buzunar care s-ar opri la o astfel de eroare? Penibil! Eroarea trebuie doar să fie anunțată și procesarea continuată.

Poate vom reveni cu un articol special despre tratamentul erorilor, pentru că este un subiect extrem de interesant, și foarte prost tratat în literatură. O eroare se poate adesea propaga în sus pe un lanț de funcții care s-au chemat între ele (mai ales dacă una era recursivă) la distanțe extrem de mari de locul producerii. Cum se face asta cu grijă, cum se semnalează erorile cel mai simplu, asta merită discutat mai pe-ndelete.

Documentația

Donald Knuth (unul din cei cu citatele din introducere) este unul dintre adepții a ceea ce se numește ``litterate programming''. Acesta este un sistem (numit Web) în care programul și documentația sa se scriu dintr-un singur foc, apoi cu două compilatoare diferite se obțin executabilul și cartea de descriere a produsului. Paradoxal, această metodă mărește productivitatea și fiabilitatea, pentru că îți poți introduce explicațiile detaliate chiar în corpul programului în timp ce-l scrii, gîndind mai ușor atît asupra programului cît și documentației.

Dacă nu spuneți cum face ceva un program de-al dumneavoastră, trebuie totuși să spuneți ce face. Chiar dacă nu scrieți programe pentru vînzare este un obicei extrem de util să scrieți scurte documentații pentru utilizatorii lor sau cei care vor să le modifice. Un model împămîntenit este pagina de manual interactivă din UNIX (sau comanda ``help'' din DOS, într-un fel). O descriere a programului dumneavoastră ar trebui să conțină următoarele informații:

  1. numele programului;
  2. modul de lansare (argumente, etc);
  3. o descriere a comportării;
  4. pentru programe interactive indicații sumare de folosire;
  5. informații despre fișierele pe care le folosește și structura lor;
  6. versiunea curentă;
  7. autorul programului cu adresa lui (nu se recomandă dacă programul este un virus);
  8. defecțiuni cunoscute;
  9. documentații înrudite.

Portabilitatea

Dacă trebuie să folosiți trăsături ale limbajului care nu sunt standard (de exemplu în Turbo C gotoxy(), atunci nu le chemați niciodată direct; scrieți pentru ele ``wrappere''. Astfel de wrappere ar trebui să asigure o comportare uniformă a funcțiilor, chiar dacă bibliotecile de care dispuneți se schimbă.

De exemplu dacă faceți un program pe PC care lucrează cu mai multe culori, dacă vreți să meargă bine și pe plăci monocrome și color scrieți o funcție culoare_text() cam așa:

void culoare_text(int culoare)
        /* alege culoarea textului in functie de numarul 
           de culori disponibile (2 sau 16) */
{
        int cul_reala;

        /* MONOCROM este aflat de exemplu intrebind utilizatorul
           la inceputul programului */
        if (MONOCROM) cul_reala = culoare / 8;
          /* daca avem numai 2 culori pastram numai primul bit */
        else cul_reala = culoare;
        textcolor(cul_reala);
}

Dacă vă concepeți programele într-un mod cît mai general vor deveni adesea și ceva mai ușor de scris, și cu siguranță foarte ușor de întreținut și ``portat'' (mutat de la un compilator la altul).

Subiectul este foarte generos, iar tratarea pe care i-am dat-o aici este departe de a fi exhaustivă. Numai experiența vă va convinge de utilitatea unei discipline în actul programării. Deci spor la treabă!