Programarea nu este doar scrierea codului

În principiu, programarea este procesul prin care, pornind de la enunțul unei probleme se ajunge la un program executabil care rezolvă problema respectivă. Scrierea codului pare a fi pasul fundamental al procesului (sau cel puțin cel mai important). De multe ori, celelalte etape sunt mai mult sau mai puțin ignorate. Acest articol încearcă să ilustreze importanța celorlalți pași.

O problemă simplă

Pentru a putea scrie cod este destul de clar că trebuie să știm cam ce am dori să facă programul care va rezulta. Pe parcursul acestui articol vom folosi o problemă foarte simplă: calculul modulului unui număr. Deci…

În principiu funcționează și, dacă avem noroc, chiar asta trebuia să facem. Am scris repede codul și am scăpat. Cu cât problema este mai simplă, cu atât avem șanse mai mari. Dar, de obicei, problemele sunt complicate și riscul să nu fi rezolvat corect o problemă sau să fi rezolvat de fapt o altă problemă (poate mai simplă, poate mult mai complicată decât cea reală) este mare.

Codul pare OK, dar putem să găsim probleme destul de ușor. De exemplu, ce s-ar întâmpla dacă am introduce numărul 1.23456789. Am avea cam așa:

Nu mai pare totul chiar în regulă, nu? Putem rezolva problema în diverse moduri dar, pentru moment, tragem concluzia că nu este chiar bine să sărim direct la implementare. Trebuie să ne gândim puțin…

În cazul de față, deși nu a fost evident, am avut și un pas intermediar între citirea enunțului și scrierea codului: am ales limbajul de programare (C++ în acest caz). De fapt, am știut că trebuie să calculăm modulul, ne-am gândit că putem scrie repede un progrămel în C++ și apoi am făcut-o. Am evidențiat trei pași care ar trebui parcurși de fiecare dată când încercăm să rezolvăm o problemă:

  • trebuie să știm care este problema
  • trebuie să ne gândim cum putem rezolva problema
  • trebuie să implementăm rezolvarea

Partea de scriere a codului este inclusă în ultimul pas. În cazul de față, probabil am scris codul în câteva minute, ne-a luat câteva secunde să citim enunțul și probabil nu ne-am gândit aproape deloc la modul în care o vom rezolva.

În continuare vom încerca să scoatem în evidență importanța celor doi pași pe care i-am tratat superficial și vom vedea că ei trebuie parcurși de mai multe ori pentru a obține o soluție cât mai corectă a problemei.

Clarificarea enunțului

Am văzut deja că soluția noastră nu este chiar corectă pentru numere care au mai multe zecimale. Când am scris codul am vrut să fim cât mai generali așa că am folosit tipul double. Oare chiar era nevoie? De fapt, n-am prea știut exact ce trebuie să facem. Am ales tipul care ni se părea cel mai puțin riscant și gata.

Dar, primul pas al procesului de programare este a ști exact ce problemă trebuie să rezolvăm. Să presupunem că “clientul” nu are nevoie decât de modulul unor numere întregi. Cum avem deja puțină experiență, să zicem că întrebăm repede cât de mari pot fi numerele și ni se spune că vom lucra doar cu numere întregi cu semn “standard” reprezentabile pe patru octeți.

Analiza

Probabil acum nu vom mai sări direct la scrierea codului. Nu trebuie să ne gândim prea mult să ne dăm seama că ar trebui să folosim tipul int. Oare chiar trebuia să scriem codul în C++? Poate avem dreptul să alegem, dar poate “clientul” are niște “planuri” care necesită o soluție în Java. Trebuie să știm și nu putem continua. Va trebui să ne întoarcem la enunț. (Situația este evident exagerată; ideea este că, de multe ori, în timpul analizei ne dăm seama că nu avem la dispoziție informații suficiente pentru a implementa o soluție corectă).

Detalii suplimentare

“Clientul” ne spune acum că, de fapt, dorea o librărie Java pe care să o poată folosi oricând ca să calculeze modulele unor numere întregi care pot fi reprezentate pe patru octeți.

O nouă analiză (scurtă)

Deci, vom scrie o clasă în Java care va o metodă care va calcula modul unui int.

Implementarea (în sfârșit)

Am scris codul și suntem destul de siguri că funcționează corect. Livrăm librăria…

Din nefericire, clientului nu-i place. Din diverse motive, nu-i place să instanțieze obiecte când folosește librăria.

Înapoi la analiză

Trebuie să ne gândim acum la o soluție care nu necesită instanțierea de obiecte noi. E destul de simplu, vom folosi o metodă statică.

Noua implementare

Toate bune și frumoase. O vreme nu mai auzim nimic de la “clientul” nostru. Dar, la un moment dat (total nepotrivit, aveam alte planuri), primim un mesaj:

Echipa noastră de testare a identificat o eroare: codul de mai jos ilustrează problema:

Rulăm și noi programul și observăm că, într-adevăr, soluția noastră indică faptul că modului numărului -2147483648 este -2147483648. Ghinion, cine s-ar fi gândit la asta? Dacă am fi avut mai multă experiență sau am fi testat mai în detaliu codul poate am fi descoperit singuri problema. Dar acum…

Din nou analiza

Ce putem face? Așa cum sunt reprezentate numerele întregi în memorie, putem reprezenta cu unul mai multe numere strict negative decât strict pozitive. Am ales ca funcția noastră să returneze un int. Părea rezonabil. Am putea, teoretic, să modificăm în long, dar clientul ar trebui acum să își modifice apelurile. A trecut deja ceva vreme și nu pare acceptabil. Oare să aruncăm o excepție în acest caz? Nu prea știm ce trebuie făcut…

Înapoi la enunț

Trebuie să știm ce trebuie făcut în acest caz. Enunțul trebuie să îl trateze. Pentru a evita ca acest articol să se prelungească prea mult, să presupunem că avem un “client” simpatic care acceptă să renunțe la posibilitatea de a calcula modulul numărului -2147483648. Vom considera că această valoare nu este permisă. Am putea spune că enunțul devine ceva de genul: calculul modulului unui număr întreg care poate fi reprezentat pe patru octeți și al cărui modul poate de asemenea fi reprezentat pe patru octeți.

Analiza finală

Putem să nu mai modificăm nimic și să ne bazăm că funcția noastră nu va mai fi apelată cu valoarea -2147483648. Totuși, poate decidem să nu riscăm. Vom arunca o excepție în cazul în care primim acest număr.

Implementarea finală

Să zicem că am scăpat acum. Un client ar mai putea găsi probleme, dar acestea vor fi rezolvate fie revenind la analiză, fie la enunț.

Concluzie

Până să ajungem să scriem codul, este util să acordăm suficient timp clarificarea enunțului și analizei. În momentul în care scriem cod, trebuie să știm exact ce facem și cum facem. Dacă nu e totul clar, vor apărea des situații în care ne vom întoarce la enunț sau la analiză. Unii susțin că după analiză ar trebui să avem un model matematic al soluției care să facă trivială scrierea codului, dar… să nu exagerăm. Totuși, problemele identificate în primele două faze evită scrierea de cod greșit. Este preferabil să ne întoarcem la enunț în timpul analizei decât în momentul scrierii codului.

Un programator prolific scrie și rescrie mult cod. Un programator bun scrie puțin cod și nu îl modifică foarte des.

Rezumând, cele trei faze ale programării descrise în cadrul acestui articol sunt:

  • KNOW (trebuie să știm ce)
  • THINK (trebuie să ne gândim cum)
  • DO (trebuie să facem)

Nu sunt singurele faze ale programării. În principiu, ar fi trebuit să descoperim noi problemele nu “clientul”. Dar, despre asta, într-un viitor articol...