14109677 analiza comparata a complexitatii entitatilor text generate prin tehnici de programare

159

Upload: costi-dina

Post on 30-Jul-2015

112 views

Category:

Documents


8 download

TRANSCRIPT

Cuprins

Introducere ......................................................................................................... 4

1. Programarea calculatoarelor ........................................................................ 7

1.1. Limbaje de programare ........................................................................................................ 7

1.2. Implementarea algoritmilor ............................................................................................... 10

1.3. Lucrul cu fişiere .................................................................................................................... 15

1.4. Subprogramele ....................................................................................................................... 17

1.5. Instrucţiunea GoTo .............................................................................................................. 20

2. Ciclul de dezvoltare software ..................................................................... 23

2.1. Activitatea de programare ................................................................................................. 23

2.2. Graful asociat unui program .............................................................................................. 26

2.3. Complexitatea programelor ............................................................................................... 31

2.4. Etapele ciclului de dezvoltare ........................................................................................... 36

3. Abordarea clasică .......................................................................................... 41

3.1. Specificaţiile de programare ............................................................................................. 41

3.2. Preluarea algoritmilor ......................................................................................................... 45

3.3. Elaborarea schemelor logice .............................................................................................. 47

3.4. Scrierea programelor .......................................................................................................... 58

3.5. Testarea şi implementarea programelor ........................................................................ 61

3.6. Între artă, meşteşug şi industrie ..................................................................................... 65

4. Programarea standard .................................................................................. 67

4.1. Reguli de construire ............................................................................................................. 67

4.2. Subprogramul ........................................................................................................................ 69

4.3. Generalitatea subprogramelor .......................................................................................... 79

4.4. Listele cu parametri ............................................................................................................ 83

4.5. Programul principal, programul apelator ......................................................................... 85

5. Abordarea structurată ................................................................................. 89

5.1. Reguli de bază ........................................................................................................................ 89

5.2. Programarea fără GoTo ...................................................................................................... 94

5.3. Conversii de programe ......................................................................................................... 97

5.4. Limbaje de programare structurate ................................................................................ 102

6. Programarea modulară .................................................................................. 106

6.1. Abordarea bottom-up .......................................................................................................... 106

6.2. Abordarea top-down ............................................................................................................ 110

6.3. Raportul subprograme – modul .......................................................................................... 113

6.4. Parametrizarea ..................................................................................................................... 118

7. Programarea orientată obiect .................................................................... 122

7.1. Proiectarea orientată obiect .............................................................................................. 122

7.2. Clasele ..................................................................................................................................... 125

7.3. Constructuri şi destructori ............................................................................................... 128

7.4. Proprietăţile claselor ........................................................................................................... 129

7.4.1. Încapsularea ............................................................................................................ 129

7.4.2. Moştenirea ............................................................................................................... 132

7.4.3. Polimorfism .............................................................................................................. 134

8. Utilizarea de componente ............................................................................ 140

8.1. Premise .................................................................................................................................... 140

8.2. Software orientat pe componente ................................................................................... 141

8.3. Biblioteci de componente ................................................................................................... 149

8.4. Asigurarea interoperabilităţii prin utilizarea de componente ................................... 150

9. Concluzii ........................................................................................................... 153

Anexa 1 – Lista de variabile ....................................................................................................... 156

Bibliografie .......................................................................................................... 158

Introducere

Obiectivul lucrării este prezentarea tehnicilor de programare cu avantajele şi dezavantajele lor, precum şi realizarea unei analize comparate a complexităţii software pentru entităţile text generate cu ajutorul acestor tehnici, folosind aceeaşi problemă spre exemplificare. Codul sursă al programelor este considerat entitate text pentru că are în alcătuire cuvintele unui vocabular şi respectă regulile de construire a elementelor complexe de limbaj asociate acestuia.

Necesitatea lucrării este dată de evidenţierea progresului înregistrat de trecerea de la o tehnică veche de programare la una nouă, în raport cu criteriul creşterii performanţei produselor software, dar şi cu cel al creşterii complexităţii software.

Mijloacele întrebuinţate sunt diferite întrucât fiecare dintre tehnicile de programare au promovat un anumit limbaj care a evidenţiat în primul rând avantajele generate. Limbaje precum FORTRAN sau ALGOL sunt deja istorie, iar limbajul COBOL mai este prezent doar prin intermediul numeroaselor sisteme informatice dezvoltate cu el. Limbajul C şi evoluţia sa C++ sunt folosite în continuare foarte intens, în special ca limbaje de dezvoltare pentru sisteme de operare, însă limbaje precum Java sau C# reprezintă la acest moment principalele instrumente de dezvoltare a produselor software.

Limbajele de programare oferă resurse diferite, dacă prin resurse se înţelege: tipuri de date, cuvinte cheie asociate instrucţiunilor, implementarea recursivităţii, blocurile, expresiile compuse şi bibliotecile de funcţii.

Capitolele prezintă caracteristicile tehnicilor de programare, ipotezele de lucru şi efectele acestora asupra structurii produsului finit – programul – pe care îl construiesc. Se fac referiri asupra capacităţii tehnicilor de programare de a dezvolta lucrul în echipă, de a asigura un nivel calitativ ridicat produsului software şi de a permite transmiterea experienţei de la un proces la altul prin reutilizarea de componente.

Dezvoltarea tehnicilor de programare este dinamică. Pentru a identifica trendul evoluţiei tehnicilor de programare se studiază influenţa acestora asupra complexităţii software. La finalul fiecărui capitol dedicat

4

Tehnici de programare

unei tehnici de programare, se analizează complexitatea software a codului sursă generat cu ajutorul tehnicii respective pentru rezolvarea problemei alese.

În capitolul în care se prezintă Ciclul de dezvoltare software sunt descrise etapele, sarcinile, intrările şi ieşirile acestora, aşa cum decurg din standardele ingineriei software actuale. De asemenea, se prezintă conceptul de complexitate software şi se definesc instrumentele de măsurare ce sunt folosite în cadrul acestei lucrări.

Tehnica de programare clasică este prezentată într-un capitol distinct, în care se accentuează caracterul artizanal, individual al muncii de programare. Arta programării joacă un rol important, întrucât fiecare program este văzut ca un produs unicat, produs ce include foarte multă muncă intelectuală, inspiraţie şi, desigur, inteligenţă.

Programarea standard este o tehnică de programare care pentru prima dată ia în considerare necesitatea constituirii de biblioteci de subprograme, după nişte reguli foarte precise. Orice aplicaţie apare, în viziunea programării standard, sub forma exclusivă a apelului de subprograme. Multe dintre ideile de finalizare specifice programării standard se regăsesc în programarea orientată obiect.

În capitolul Abordarea structurată se evită teoretizarea legată de o bază formală care impune şi dezvoltă această tehnică. Teorema de structură, ipotezele privind transformarea programelor sunt numai abordări de care practica programării nu a ţinut seama în nici un fel. În acest capitol se prezintă structurile fundamentale, cerinţele pe care un program trebuie să le îndeplinească pentru a fi considerat program structurat. Sunt prezentate, de asemenea, avantajele privind omogenitatea textelor sursă, capacitatea de a dezvolta aplicaţii structurate şi extinderea pe care au luat-o limbajele de programare în care instrucţiunea GOTO nu mai trebuie folosită.

Revoluţia în programare prin introducerea de clase, obiecte, proprietăţi şi dezvoltarea tehnicilor de analiză şi proiectare bazate pe aceste concepte este abordată în capitolul Programarea orientată obiect. Se descriu structurile claselor, proprietăţile de încapsulare, moştenire şi polimorfism precum şi modul în care acestea influenţează filosofia întregii activităţi de programare. Programarea orientată obiect reprezintă altceva. Continuarea păstrării vechilor mentalităţi din programare devine imposibilă. Saltul realizat de programatori este vizibil, iar rezultatele sunt dintre cele mai spectaculoase.

5

Introducere

Utilizarea de componente reprezintă o extindere firească a programării orientate obiect. Se pune problema reutilizării funcţionalităţii şi nu neapărat a codului care o implementează. Sunt prezentate tehnici de definire a componentelor, utilizarea acestora în cadrul aplicaţiilor eterogene din punct de vedere al limbajelor de programare şi/sau tehnologiilor folosite la dezvoltarea lor, importanţa componentelor în implementarea interoperabilităţii aplicaţiilor.

Lucrarea se încheie cu concluzii în care se evidenţiază necesitatea sintetizării experienţelor pozitive în vederea trecerii la noi tehnici de programare.

Exemplificările se fac numai în limbajul C++. Se defineşte problema PROB care este prezentată în capitolul Ciclul de dezvoltare software. În celelalte capitole sunt date soluţii corespunzătoare fiecărei tehnici de programare analizate pentru problema PROB. S-a adoptat această modalitate de lucru pentru a putea fi comparate efectele utilizării fiecărei tehnici de programare în cazul implementării aceluiaşi algoritm. S-a ales o problemă cunoscută pentru a facilita urmărirea paşilor algoritmului, simultan cu structurile de program pe care fiecare tehnică le generează.

Analiza comparată se realizează prin soluţionarea aceleaşi probleme, pentru a surprinde mai bine, diferenţele de abordare pentru fiecare dintre tehnicile de programare. Pentru fiecare soluţie se fac comentarii privind lungimea codului sursă şi complexitatea programelor.

Autorii aduc mulţumirile lor membrilor Catedrei de Informatică Economică din Academia de Studii Economice şi, în special, colectivelor de programarea calculatoarelor ale căror lucrări au stat la baza cercetărilor întreprinse şi la obţinerea rezultatelor incluse în capitolele destinate celor mai noi tehnici de programare aflate acum în atenţia dezvoltării de aplicaţii informatice moderne.

Lucrarea este elaborată în cadrul contractului de cercetare INFOSOC „Sistem de evaluare a entităţilor bazate pe text”, nr.148/29.09.2004.

Autorii mulţumesc tuturor celor care, prin contribuţii şi sugestii, vor îmbunătăţi ediţia viitoare a lucrării de faţă, lucrare care se adresează tuturor celor care dezvoltă programe, pentru a găsi noi resurse şi pentru a se perfecţiona continuu.

6

1.1 Limbaje de programare

Limbajele de programare sunt elaborate pentru a implementa algoritmi. Orice algoritm se descrie ca succesiune de paşi în care intervin operanzi şi operatori pentru efectuarea de prelucrări.

Orice limbaj de programare conţine:

• o listă de operatori care arată care sunt operaţiile aritmetice, logice şi de compunere pe care le acceptă limbajul, devenind resurse la dispoziţia programatorilor atunci când construiesc expresii mai simple sau mai complexe; lista operatorilor conţine şi priorităţile asociate precum şi regula de evaluare;

• o listă a cuvintelor cheie asociate unor comenzi executabile; este preferabil să existe o continuitate de la un limbaj la altul, pentru a nu se crea confuzii; aşa se explică faptul că pentru implementarea structurii alternative se păstrează construcţia if() then else, pentru implementarea structurii repetitive se păstrează instrucţiunile for(), while(), do until(), cu mici variaţii;

• o listă a cuvintelor cheie pentru definirea tipurilor de date fundamentale;

• o listă de cuvinte cheie care joacă rolul de atribute sau de specificatori;

• o listă de delimitatori de blocuri şi de instrucţiuni;

• mecanisme de construire a constantelor şi identificatorilor;

• mecanisme de construire a expresiilor de definire şi a expresiilor de referire;

• mecanisme de construire a subprogramelor;

• mecanisme de implementare a structurilor de control.

7

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Orice limbaj are la bază un alfabet şi un vocabular format din cuvinte cheie şi cuvinte construite de utilizatori. Limbajele de programare au înregistrat evoluţii spectaculoase, trecând prin stadii care au impus modificări sau chiar dispariţia. Aşa s-a întâmplat cu limbajul FORTRAN, care a devenit în evoluţia sa FORTRAN-4, FORTRAN-77, FORTRAN-90, iar variantele care au urmat au avut o asimilare limitată, limbajul fiind cu o răspândire extrem de redusă. În schimb limbajul C++ prin capacitatea sa de a asimila construcţii cu grad de complexitate foarte variat, s-a impus şi s-a dezvoltat.

Orientarea spre programarea vizuală are o largă răspândire prin sugestivitatea abordării în raport cu problema de rezolvat şi operaţiile mecanice pe care le presupune implementarea unui algoritm. Limbajele de nivel scăzut presupun instrucţiuni pentru operaţiile elementare. Programatorul trebuie să gestioneze descompunerea expresiilor pentru evaluarea din aproape în aproape.

Limbajele de nivel ridicat includ construcţii complexe, compuneri de expresii şi prin atribuirea de calificative se obţin efecte speciale în ceea ce priveşte referirile de operanzi şi operatori. Sunt implementate structuri dintre cele mai variate, iar mecanismele de definire de operanzi şi operatori au corespondent mecanisme de referire care generează efecte ce asigură generalitate şi eficienţă tuturor construcţiilor; numai aşa se explică creşterea complexităţii şi diversităţii aplicaţiilor informatice de azi.

Exemplificarea limbajelor de programare este realizată folosind numai limbajul C++. Elementele care definesc un limbaj sunt: alfabetul limbajului, vocabularul, operatorii şi precedenţele lor, identificatorii, declaraţiile de funcţii şi variabile. Vocabularul limbajului C++ include cuvintele cheie:

asm auto bool break case catch char class const const_cast continue default delete do double dynamic_cast else enum explicit extern false float for friend goto if inline int long mutable namespace new operator private protected public register reinterpret_cast return short signed sizeof static static_cast struct switch template this throw true try typedef typeid typename union unsigned using virtual void volatile wchar_t while

8

Programarea calculatoarelor

Lista de operatori şi priorităţile sunt date în tabelul 1.1:

Operatorii identificaţi în limbajul C++ Tabelul 1.1

Precedenţă Operatori 1 () [] -> . :: 2 !

- (operator unar) & (adresa)

~ * (dereferire) Sizeof

++ --

3 ->* .* 4 * (înmulţire) / % 5 + - 6 << >> 7 < <= > >= 8 == != 9 & ( AND pe biţi) 10 ^ 11 | 12 && 13 || 14 ?: 15 = += -= etc. 16 ,

Alfabetul limbajului C++ este alcătuit din următoarele caractere: a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 0 1 2 3 5 6 7 8 9 ! “ # % & ‘ ( ) * + , - . / : ; < = > ? [ \ ] ^ _ { | } ~ spaţiu, TAB, CR/LF

Identificatorul este numele dat de programator variabilelor, funcţiilor, etichetelor şi a altor obiecte definite de el. Mecanismul de construire al acestuia este acelaşi în toate limbajele de programare importante: primul caracter este o literă sau un caracter de subliniere, iar caracterele care urmează sunt litere, cifre sau caractere de subliniere. Cuvintele cheie asociate limbajului nu sunt acceptate ca identificatori.

9

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Exemple de identificatori valizi sunt: _matrice, mat1234, arb43bin, for123.

Exemple de identificatori invalizi sunt: 12matrice, for, etc.

Prototipul de funcţie în C++ este următorul: tip_rezutat nume_funcţie ( listă_parametrii ) { corp_funcţie }

Declararea operanzilor se face astfel: tip_operand listă_de_variabile;

Există numeroase lucrări, [IVANI98], [SMEU01], [SMEU02], [STROU87], care detaliază limbajul C++ evidenţiind caracteristicile sale cele mai puternice, studiul acestui limbaj nefiind obiectivul acestei lucrări.

1.2 Implementarea algoritmilor

Algoritmii presupun operanzi şi operatori. Pentru operanzi există:

• iniţializări de date;

• afişări de rezultate;

• prelucrări.

Operatorii arată modul în care sunt transformaţi operanzii, pentru a deveni din date iniţiale, date prelucrate, adică rezultate care să determine acţiuni sau să permită luarea unor decizii.

Orice problemă P are asociat un algoritm căruia îi corespunde structura arborescentă cu două niveluri dată în figura 1.1.

Figura 1.1 Structură de algoritm pe două niveluri

10

Programarea calculatoarelor

Iniţializarea se realizează prin:

• citire date din fişiere sau baze de date;

• introducere date de la tastatură;

• evaluarea expresiilor de atribuire.

Secvenţa de rezultate conţine proceduri pentru:

• afişare rezultate pe ecranul monitorului;

• scrierea la imprimantă;

• creare sau actualizare de fişiere, respectiv actualizare de baze de date.

Implementarea unui algoritm include instrucţiuni pentru una sau mai multe combinaţii de iniţializare, respectiv gestionare a rezultatelor.

Există numeroşi algoritmi daţi în literatura de specialitate şi în cărţile de bază ale programării. Sunt algoritmi verificaţi, aduşi la o formă rafinată, programatorul trebuind numai să-i preia. Sunt şi situaţii în care, programatorul trebuie să-şi construiască algoritmi proprii. În toate cazurile, trebuie avute în vedere:

• generalitatea pe care trebuie să o asigure, nefiind interesant acel algoritm care soluţionează o problemă concretă, ci acela care soluţionează problemele aparţinând unei clase;

• corectitudinea, în sensul că pentru orice problemă aparţinând clasei de probleme pentru care a fost construit algoritmul, trebuie să furnizeze rezultate corecte;

• reproductibilitatea, în ideea că repetând prelucrările pentru aceleaşi date, se vor obţine aceleaşi rezultate, prelucrările având caracter determinist;

• finitudine, în sensul că volumul de prelucrări trebuie limitat la o dimensiune acceptabilă în raport cu obiectivul urmărit.

La implementarea unui algoritm se procedează mai întâi la elaborarea unei scheme logice. Cu acest prilej, se analizează gradul de înţelegere a problemei. Testul de birou folosind date de control are menirea de a da un plus de siguranţă soluţionării problemei prin metoda folosită.

Pentru implementarea algoritmului sunt alese niveluri de detaliere. Este important ca indiferent de procedeul definit şi utilizat, programul sa-şi

11

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

atingă obiectivul pentru care a fost elaborat. De exemplu, se doreşte implementarea algoritmului de sortare prin interschimbare cu contorizarea interschimbărilor.

Se consideră şirul:

şi se doreşte ordonarea descrescătoare. Se interschimbă perechile, începând cu primul element:

şirul devine:

Numărul de interschimbări a fost 4. Se interschimbă perechile

începând cu al doilea element:

şirul devine:

Numărul de interschimbări a fost 3. Se reiau interschimbările cu

primul element:

12

Programarea calculatoarelor şirul devine:

Numărul de interschimbări a fost 4. Se interschimbă începând cu al

doilea element:

Şirul devine:

Numărul interschimbărilor a fost 3. Se reiau paşii:

13

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Algoritmul este finit pentru că în momentul în care nu mai există interschimbări, înseamnă ca şirul este sortat. Pentru implementare trebuie răspuns la întrebările:

• cât de lungi sunt şirurile de termeni care se sortează;

• dacă sortarea este însoţită şi de interschimbarea altor câmpuri.

Funcţie de răspunsurile date se procedează la definirea unui algoritm de sortare în fişiere sau la construirea unui algoritm de sortare exclusiv în memorie. Implementarea algoritmilor depinde de:

• experienţa programatorilor;

• resursele hardware şi software disponibile;

• resursele financiare care stau la baza demersului;

• timpul în care trebuie rezolvată problema.

În mod normal, trebuie să se pornească de la problema de rezolvat. Aceasta impune cum se stochează datele iniţiale, cum se gestionează rezultatele intermediare şi cum sunt memorate rezultatele finale.

Implementarea algoritmilor necesită efectuarea de calcule privind necesarul de memorie pentru a stoca rezultatele intermediare şi finale, precum şi datele iniţiale în timpul prelucrării. Sunt preferaţi algoritmii care nu necesită zone importante de memorie pentru rezultate intermediare.

Pentru calculul mediei aritmetice care utilizează cât mai puţină memorie, programul ce implementează algoritmul este următorul:

#include <stdio.h> int main() { int i, n, s, x; float xm; s = 0; printf("Introduce numarul de elemente:"); scanf("%d", &n); for ( i = 0; i < n; i++ ) { printf( "Introduce x[%d] = ", i ); scanf("%d", &x); s += x; } xm = (float)s/n; printf( "Media aritmetica este %f", xm ); }

14

Programarea calculatoarelor

Se constată că pentru a reutiliza datele pentru alte calcule, acest deziderat este imposibil de realizat. Pentru a reutiliza datele, este necesar ca ele să se salveze în memorie, în structuri de date corespunzătoare, şi apoi să intre în prelucrarea efectivă. Algoritmul este un aspect, programul este o concretizare, una din concretizări, totul depinzând de programator.

1.3 Lucru cu fişiere

Fişierele, bazele de date, sunt fundamentale pentru rezolvarea problemelor în care se manipulează volume mari de date. Fişierul este o colecţie de date reprezentând informaţii privind elementele unei colectivităţi. Dacă datele privind un element Ai al colectivităţii {A1, A2, …., An} are lungime constantă L baiţi, atunci fişierul F are o lungime lg(F) = n * L baiţi.

Fiecare articol descrie un element al colectivităţii. Poziţia pi a articolului care se referă la elementul Ai din colectivitate împreună cu o valoare unică, numită cheie, notată ki, formează o pereche ( ki, pi). Unui fişier F i se asociază un şir de perechi (k1, p1), (k2, p2), ..., (kn, pn). Dacă se efectuează operaţii precum adăugarea, inserarea, ştergerea, interschimbarea de articole, este preferabil ca ele să se efectueze asupra şirului de perechi, fără a provoca schimbări la nivel fizic, întrucât operarea pe zonele de memorie asociate fişierului au durate care reduc volumul tranzacţiilor pe unitatea de timp.

Dacă între cheile k1, k2, .........., kn şi poziţiile p1, p2, ...., pn se identifică o dependenţă, atunci se construieşte o expresie analitică folosind relaţia:

pi = f (ki)

Se obţine deplasarea articolului Ai prin relaţia:

Di = pi * LAi

Problemele se tratează similar şi în cazul în care lungimile articolelor sunt diferite. Dacă articolul Ai are o lungime proprie Li, deplasarea sa Di se obţine din relaţia:

0

1

1DLAD

i

jji += ∑

=

unde D0 este deplasarea primului articol.

15

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Pentru a creşte viteza de identificare a unui articol, fişierul F este împărţit în r părţi, figura 1.2.

Figura 1.2 Divizarea fişierului F

Dacă se efectuează traversarea fişierului F pentru a găsi articolul cu cheia kx, sunt necesare suficient de multe operaţii de citire. Dacă se construieşte şirul de r perechi (k1`, p1`), (k2`, p2`)…..(kr`, pr`) se traversează mai întâi şirul de perechi, se obţine selectarea zonei din fişier unde se află articolul cu cheia kx după care se procedează la traversarea secvenţială a zonei, articol cu articol, până la identificarea celui căutat.

Este important ca de la început articolele să fie dispuse în zonele fişierului F cu condiţia:

k1 < k2 < ……….. < kn

Subşirului de perechi i se asociază un alt subşir, căutarea secvenţială efectuându-se pe două niveluri. Raţionamentul se dezvoltă, fiind necesar un echilibru între numărul nivelurilor de indexare şi efortul de traversare secvenţială în cadrul fiecărui nivel.

Dacă se adaugă atribute, se construiesc tripletele care se asociază articolelor fişierului (k1,p1,a1), (k2,p2,a2), ...., (kn,pn,an).

Operaţiile de intersecţie, reuniune de şiruri de triplete conduc la extragerea din fişier a articolelor ale căror câmpuri au valori ce corespund unor combinaţii de atribute.

Viteza de derulare a tranzacţiilor depinde strict de întreaga filosofie de a organiza procesele de referire ale articolelor din zona de date a fişierului. Lucrul cu indexuri, cu deplasări şi mai ales structurile arborescente asociate perechilor de căutare influenţează duratele tranzacţiilor.

16

Programarea calculatoarelor

1.4 Subprogramele

Acum se concepe dezvoltarea unei aplicaţii numai folosind subprograme. Un subprogram este o entitate de sine stătătoare, care concentrează o prelucrare cu caracter repetitiv. Nu se scriu subprograme pentru prelucrări care nu vor mai fi realizate şi cu alte prilejuri.

Subprogramul are un nume sugestiv, uşor de manevrat, fără a fi definite mai multe subprograme diferite ca prelucrare cu nume având coeficient de similitudine peste un prag considerat limită superioară. Spre exemplificare, se consideră următoarea listă de subprograme:

• read_file() – subprogram care citeşte un articol dintr-un fişier;

• update_file() – subprogram pentru actualizarea unui fişier;

• write_file() – subprogram pentru scrierea unui articol într-un fişier;

• addmat() – subprogram care adună două matrice;

• prodmat() – subprogram care înmulţeşte două matrice;

• copymat() – subprogram pentru copierea unei matrice într-o altă matrice;

• initmat() – subprogram care iniţializează o matrice cu o constantă;

• sort_file() – subprogram pentru sortarea fişierelor;

• sort_vect() – subprogram pentru sortarea elementelor unui vector;

• concat_vect() – subprogram care concatenează doi vectori;

• concat_lists() – subprogram care concatenează două liste simple;

• concat_listd() – subprogram pentru concatenarea două liste duble;

• concat_nlists() – subprogram pentru concatenarea a n liste simple;

• concat_nlistd() – subprogram care concatenează n liste duble;

• concat_nvect() – subprogram care concatenează n vectori.

17

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Subprogramele conţin în construcţiile lor:

• liste de parametri formali în care se regăsesc operanzii care fac obiectul prelucrării, operanzii ce conţin rezultatele şi variabile ce conţin informaţii privind modul în care s-au efectuat prelucrările; listele fixe de parametri au aceeaşi structură şi lungime, indiferent de numărul apelurilor; listele variabile au atât tipuri diferite de parametrii, cât şi număr variabil;

• instrucţiuni care asigură revenirea în programul apelator, return; este preferabil să se construiască subprograme care conţin o singură instrucţiune return, folosind fie compuneri de expresii, fie o variabilă de control utilizată ulterior;

• instrucţiuni repetitive pentru a asigura traversarea structurilor de date omogene (masiv, listă, arbore, fişier), dacă pentru evaluarea de expresii acest lucru este necesar;

• instrucţiuni de iniţializare pentru variabile care îşi modifică conţinutul în vederea returnării de rezultate care vor fi utilizate în programul apelator; programul apelator este şi el tot un subprogram; int main() este, de asemenea, un subprogram, aşa cum într-un arbore binar, rădăcina este tot un nod;

• un tip de rezultat care se returnează; în cazul în care lista rezultatelor returnate este vidă, tipul subprogramului este void.

Forma generală a unui subprogram este: tip_rezultat nume_subprogram( listă_parametrii_formali ) {

definiri variabile locale subprogramului instrucţiuni de prelucrare return ( expresie )

}

Figura 1.3 Forma generală a unui subprogram

Este de dorit ca subprogramele să fie înzestrate cu un grad înalt de generalitate pentru a permite refolosirea lor ori de câte ori e nevoie. Testarea sistematică a subprogramelor le dă un nivel de corectitudine care generează încrederea utilizatorilor în a le încorpora în programele proprii.

Există mai mulţi termeni care desemnează acelaşi lucru: proceduri, subprograme, funcţii; sunt concepte prin care, de regulă, se înţelege o construcţie independentă în raport cu un context, care se referă şi care permite efectuarea de prelucrări ale căror efecte sunt utilizate ulterior.

18

Programarea calculatoarelor

Pentru evaluare expresiei:

∑=

=n

iixS

1

se scriu mai multe forme de subprograme, care diferă după cum sunt transmişi parametrii.

În textul sursă: int suma ( int x[], int n ) { int s, i; for ( s = 0, i = 0; i < n ; i++ ) { s += x[i]; } return s; }

subprogramul returnează rezultatul. În textul: void suma ( int x[], int n, int* ps ) { int i; *ps = 0; for ( i = 0; i < n; i++ ) { *ps += x[i]; } }

rezultatul se regăseşte în zona de memorie a cărei adresă se află în variabila pointer ps.

Faptul că lista de parametri include o mare diversitate de tipuri de date, inclusiv pointeri spre funcţii, subprogramele devin cu grad de generalitate foarte mare. În loc să se scrie, de exemplu, două subprograme pentru a găsi elementul minim, respectiv elementul maxim dintr-un şir, se va scrie un singur subprogram ce are în lista de parametri o funcţie ce returnează evaluări diferite de expresii condiţionale, care vor determina acţiuni diferite în cadrul subprogramului.

Subprogramele trebuie să fie generale atât prin dimensiunea problemelor de rezolvat, cât mai ales prin cuprinderea a cât mai multor cazuri care definesc clasa de probleme rezolvată. În programarea orientată obiect problematica tipului de rezultate returnate şi a tipurilor de variabile din lista de parametrii se soluţionează prin construcţii de tip template.

Limbajele de programare dispun de numeroase modalităţi de transmitere a parametrilor, dintre care transmiterea prin valoare,

19

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

transmiterea prin adrese şi transmiterea prin referinţă, sunt cele mai frecvent întâlnite. Este rezonabilă cunoaşterea în detaliu a acestor mecanisme pentru a înţelege efectele.

Dacă în cazul transmiterii prin valoare operarea pe variabilele asociate parametrilor subprogramului este locală în raport cu domeniul definit de subprogram, în celelalte cazuri aceste operaţii conduc la modificarea conţinutului zonelor de memorie asociate parametrilor formali, la nivelul subprogramului apelator.

Subprogramele nu conţin operaţii de intrare/ieşire, pentru a li se asigura un înalt grad de generalitate şi mentenabilitate. Ele primesc datele de intrare prin intermediul listei de parametrii şi oferă rezultate fie prin parametrii din listă, fie prin valoarea returnată de subprogram, fie prin amândouă modalităţile.

1.5 Instrucţiunea GOTO

Această instrucţiune a fost mărul discordiei de-a lungul timpului pentru programatori. Ea înseamnă salt necondiţionat spre o instrucţiune a cărei etichetă este specificată. Salturile înapoi revin la situaţiile în care eticheta e1 spre care se efectuează saltul necondiţionat precede instrucţiunea GOTO, figura 1.4.

e1

e2GO TO e1

Figura 1.4 Instrucţiune salt înapoi

Salturile înainte revin situaţiilor în care etichetele e2 şi e3 ale instrucţiunilor spre care se efectuează saltul urmează instrucţiunilor GOTO, figura 1.5.

20

Programarea calculatoarelor

Figura 1.5 Instrucţiune salt înainte

Dacă se asociază un graf unui program şi se include într-o matrice instrucţiunile precedente şi instrucţiunile următoare, în cazul unei structuri liniare, figura 1.6, matricea include asteriscuri numai deasupra diagonalei principale:

Figura 1.6 Structură liniară

Figura 1.7 Matricea de precedenţă a instrucţiunilor

În cazul includerii în program a instrucţiunilor de salt necondiţionat, matricea precedenţelor devine mult mai complexă. Pentru programul al cărui graf este dat în figura 1.8, rezultă matricea de precedenţă, din figura 1.9.

21

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Figura 1.8 Graf asociat unui program ce include instrucţiuni de salt

I1

I1

I2

I2 I3 I4 I5 I6 I7

I3

I4

I5

I6

I7 Figura 1.9 Matricea de precedenţă asociată programului cu salturi mixte

Programarea modulară exclude apariţia instrucţiunii GOTO, deşi la nivelurile de programare în limbaj de asamblare este evident că nici o structură de control nu se implementează fără a folosi această instrucţiune. Sunt situaţii în care optimizarea programelor impune o altă abordare care să elimine salturile necondiţionate între componente aflate în segmente diferite.

Programarea calculatoarelor se defineşte ca meserie ce foloseşte resurse, iar eficienţa sa este cu atât mai mare cu cât procesul de alocare şi nivelare a acestor resurse este mai aproape de nivelul optim. Limbajele, subprogramele, instrucţiunile, tehnicile de programare sunt resurse, nivelarea şi alocarea lor o efectuează programatorul.

22

2.1 Activitatea de programare

Activitatea de programare, aşa cum este definită în [MSPCD97], reprezintă arta şi ştiinţa de a crea programe pentru calculator. Programarea începe cu cunoaşterea unuia sau a mai multor limbaje de programare, cum sunt Basic, C, C++, Java, Pascal sau limbajul de asamblare. Cunoaşterea unui limbaj nu este suficient pentru realizarea unui program bun. Alte aspecte necesare se referă la teoria algoritmilor, proiectarea interfeţelor cu utilizatorul şi caracteristici ale dispozitivelor hardware.

Activitatea de programare este colectivă, echipele de programatori dezvoltă procese de alegere şi nivelare a resurselor - echipamente hardware, limbajele de programare, algoritmii - pentru a obţine un software de cea mai bună calitate. Programatorii sunt persoane cu înaltă calificare care scriu şi depanează programe. În funcţie de dimensiunea proiectului şi de mediul de lucru, un programator lucrează singur sau într-o echipă, implicat parţial sau total în procesul de dezvoltare, care începe cu faza de analiză şi se încheie cu faza de testare şi implementare.

Activitatea de programare este deosebit de complexă, de dinamică şi implică eforturi deosebit de mari pentru actualizarea cunoştinţelor, pentru înţelegerea corectă a instrumentelor de dezvoltare a aplicaţiilor. Programatorii trebuie să fie la curent cu ultimele noutăţi în domeniu, să îşi extindă permanent aria de cunoaştere spre noi limbaje şi tehnologii.

Programarea presupune existenţa sistemelor de calcul şi a software necesar pentru dezvoltarea de noi produse, în vederea satisfacerii unor cerinţe precis definite. Acest software reprezintă sistemul de programare şi include următoarele categorii de instrumente: programele editoare de text, programele translatoare, programele editoare de legături, programele depanatoare şi mediile de programare.

Pentru a dezvolta software de bază, au fost create specializări, unii dintre programatori stăpânind foarte bine limbajele de asamblare. Alţii, pentru a dezvolta software cu caracter aplicativ, utilizează limbajele de nivel

23

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

înalt. În cazul aplicaţiilor care operează cu seturi foarte mari de date, programatorii acumulează experienţă în a lucra cu sisteme complexe de gestiune a bazelor de date.

În contextul creşterii complexităţii aplicaţiilor informatice s-au cristalizat următoarele direcţii:

• activitatea de programare este o activitate care se derulează în cadrul unei echipe, în care fiecare membru are definite inputurile, respectiv outputurile, foarte clar;

• s-au adâncit specializările, încât rolul analistului de sistem, rolul designerului, rolul programatorului şi rolul celor care se ocupă de testare, implementare şi mentenanţă a crescut foarte mult; deja există profesiuni foarte bine conturate, iar managerul de proiecte IT este deja o profesie necesară şi, mai ales, recunoscută în societatea informaţională;

• necesitatea utilizării de instrumente tehnice văzută ca necesitate a înzestrării posturilor de muncă, fie că e vorba de analişti, de designeri, de programatori, de testeri de software şi de personal care administrează aplicaţia.

Activitatea de programare este privită, uneori, ca activitate pură de creaţie, aspect exagerat, cum tot exagerat este a-l privi pe programator ca pe un simplu codificator. Programatorul trebuie să se instruiască continuu, forma concretă – programul – a întregii activităţi dintr-o companie de software este rezultatul muncii sale, chiar dacă în spate se află eforturile concertate şi ale analiştilor şi designerilor.

Apar numeroase întrebări referitoare la:

• numărul limbajelor de programare pe care trebuie să le cunoască un programator; dacă programatorul stăpâneşte foarte bine un limbaj de programare, are experienţă în a folosi facilităţile respectivului limbaj, posedă cunoştinţe de algoritmi şi tehnici de programare şi dacă stăpâneşte elementele de bază ale unui sistem de operare, va dezvolta produse de bună calitate; atunci când un alt limbaj de programare devine dominant, se impune ca programatorul să treacă la elaborarea de software folosind resursele noului limbaj dominant; înseamnă că activitatea de programare presupune un continuu efort de instruire, dar, mai ales, autoinstruire;

24

Ciclul de dezvoltare software

• traiectoria pe care o urmează un programator; dacă acesta e o persoană perseverentă adânceşte cunoştinţele dintr-un domeniu sau extinde aria de cuprindere spre alte limbaje sau tehnici de programare; o traiectorie normală este aceea în care programatorul se îndreaptă spre activitatea de proiectare, iar după ce capătă o mai mare experienţă se orientează spre analiza de sistem şi spre a deveni manager de proiect;

• caracterul industrial al activităţii de programare; conceptul de fabrică de software este privit ca o translatare forţată a unor laturi cu caracter repetat, reproductibil şi responsabilizat din producţia industrială spre o zonă unde imaterialitatea produsului imprimă particularităţi mai ales legate de absenţa uzurii fizice şi de costul nul al operaţiei de copiere;

• necesitatea de a compara produsele software prin definirea de metrici ale calităţii, prin utilizarea unor coeficienţi de transformare;

• apariţia pieţii de software ca principală pârghie de reglare a tendinţelor centrifuge din domeniul producţiei de aplicaţii informatice cu grad de generalitate deosebit de ridicat.

Activitatea de programare a calculatoarelor presupune calităţi remarcabile din partea celor care doresc să profeseze meseria de programator. Piaţa de joburi în evoluţia sa a impus meserii deosebit de diverse, cum sunt: programator, dezvoltator, inginer software, inginer de sistem etc.

Pentru analizarea avantajelor şi dezavantajelor tehnicilor de programare prezentate în această carte, a fost aleasă o problemă comună, care este dezvoltată folosind tehnicile prezentate. Se presupune o matrice cu m linii şi n coloane, m şi n numere impare. Pentru aceasta se determină valorile maxime şi minime, se testează simetria matricei, iar dacă matricea este simetrică, atunci se numără elementele pozitive, negative şi nule ale acesteia. Dimensiunile şi elementele matricei sunt informaţii ce vor fi citite de la tastatură, în programele de test.

25

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

2.2 Graful asociat unui program

Orice program este alcătuit din instrucţiuni neexecutabile, prin care se definesc operanzii şi operatorii, cât şi prin instrucţiunile executabile I1, I2, ….., In. Lungimea L a unui program este dacă ca număr de instrucţiuni. Însă nu există o corespondenţă biunivocă între o instrucţiune şi o linie din codul sursă. O linie din codul sursă poate conţine 0, 1 sau mai multe instrucţiuni.

Programul: int main () { int a, b, c, d, e, f; a = b = c = 7; d = 89; e = -1; if ( e + d * a ) f = e + a; else f = e - a; }

are un număr de zece linii sursă.

În cazul în care un alt programator rescrie acelaşi program sub forma: int main () { int a, b, c, d, e, f; a = b = c = 7; d = 89; e = -1; if ( e + d * a ) f = e + a; else f = e - a;}

se obţine un program cu doar şase linii sursă, însă ambele au acelaşi număr de instrucţiuni. Se impune stabilirea unor reguli foarte precise pentru dispunerea instrucţiunilor şi separatorilor de blocuri pentru a se obţine construcţii comparabile ca efort de realizare şi claritate a codului.

În graful asociat unui program, fiecărei instrucţiuni i se asociază un nod, legarea acesteia de instrucţiunea ce urmează a fi executată realizându-se cu ajutorul unui arc orientat.

26

Ciclul de dezvoltare software

Textul sursă al programului pentru alegerea minimului dintre trei elemente este următorul:

int main () { int a, b, c, min; I1 printf( "Introduceti a = " ); I2 scanf("%d", &a); I3 printf( "Introduceti b = " ); I4 scanf("%d", &b); I5 printf( "Introduceti c = " ); I6 scanf("%d", &c); I7

if ( a > b ) { I8 if ( b > c ) { I9

min = c; I10 } else { I11

min = b; I12 }

} else { I13 if ( a > c ) { I14

min = c; I15 } else { I16

min = a; I17 }

}

printf( "Minimul este %d", min); I18 }

şi are asociat structura arborescentă dată în figura 2.1.

27

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Figura 2.1 Graful asociat programului pentru alegerea minimului

dintre a, b, c

Secvenţa de program destinată numărării elementelor pozitive, negative şi nule ale unei matrice este următoarea: int splus = 0; I1 int snegativ = 0; I2 int snul = 0; I3

for ( i = 0;i < m; i++ ) { I4

for ( j = 0; j < n; j++ ) { I5 if ( a[i][j] > 0 ) splus++; I6 if ( a[i][j] < 0 ) snegativ++; I7 if ( a[i][j] == 0 ) snul++; I8

} }

printf("\nNumarul de elemente pozitive este %d", splus ); I9 printf("\nNumarul de elemente negative este %d", snegativ );

I10

printf("\nNumarul de elemente nule este %d\n", snul ); I11

28

Ciclul de dezvoltare software iar graful asociat este dat în figura 2.2.

Figura 2.2 Graf asociat secvenţei de numărare a elementelor

Graful asociat unui program permite evidenţierea modului în care este construit programul. Pentru programul: int splus = 0; I1 int snegativ = 0; I2 int snul = 0; I3

for ( i = 0;i < m; i++ ) { I4 for ( j = 0; j < n; j++ ) { I5 if ( a[i][j] > 0 ) { I6

splus++; I7 } else { I8

if ( a[i][j] < 0 ) { I9 snegativ++; I10

} else { I11 snul++; I12

} }

} }

29

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

printf("\nNumarul de elemente pozitive este %d", splus ); I13 printf("\nNumarul de elemente negative este %d", snegativ );

I14

printf("\nNumarul de elemente nule este %d\n", snul ); I15

este dezvoltat graful din figura 2.3.

Figura 2.3 Graful asociat programului cu structuri alternative imbricate

Abordarea structurii graf permite dezvoltarea de operaţii de transformare care să conducă la construcţii echivalente, însă optimizate. Numărul de arce şi orientarea acestora deosebeşte tehnicile de programare.

30

Ciclul de dezvoltare software

2.3 Complexitatea programelor

Complexitatea software este definită în standardul IEEE 729 – 1983 ca fiind gradul de complicare a unui sistem sau a unei componente dintr-un sistem, determinată de factori cum sunt numărul şi complexitatea interfeţelor, numărul şi complexitatea ramurilor condiţionale, gradul de imbricare, tipurile structurilor de date şi alte caracteristici ale sistemului. O definiţie mult mai completă a fost cea dată de Zuse în 1991, şi anume, complexitatea software este gradul de dificultate în analiza, întreţinerea, testarea, proiectare şi modificarea software-ului. Rezultă că, este de aşteptat ca diferite tipuri de complexitate să apară în diferite etape ale ciclului de dezvoltare.

Intuitiv, fiecare programator îşi dă seama ca un program este mai simplu sau mai complex. Se spune că un program este mai simplu dacă are mai puţine instrucţiuni şi dacă legăturile dintre instrucţiuni sunt mai reduse ca număr şi ca importanţă.

Se consideră un program PR căruia i se asociază o structură de tip graf în care se notează:

na - numărul arcelor

nn - numărul nodurilor din graf

Complexitatea în sens McCabe CM a unui program este dată de relaţia:

CM = na – nn + 2

Complexitatea în sens McCabe sau complexitatea ciclomatică, este cea mai utilizată metrică software a complexităţii. Ea măsoară numărul de căi de execuţie liniar independente, printr-un modul de program. Această măsură oferă ca rezultat un singur număr care serveşte ca instrument de comparaţie cu complexitatea altor programe.

O aplicaţie frecventă a complexităţii ciclomatice este compararea rezultatului obţinut cu un set de valori-prag recunoscute. Un astfel de set este dat în tabelul 2.1.

31

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Complexitatea ciclomatică Tabelul 2.1

Complexitate ciclomatică Evaluarea riscului 1 – 10 un modul/program simplu, fără risc

mare 11 – 20 un modul/program mai complex, cu

risc mediu 21 – 50 un modul/program foarte complex, cu

risc ridicat mai mare de 50 un modul/program netestabil, cu un

risc extrem de ridicat

Dacă un program este format din instrucţiunile I1, I2, …, IK, cărora li se asociază graful dat în figura 2.4.

Figura 2.4 Structură secvenţială de program

Complexitatea CM = 1 pentru că:

na = K – 1

nn = K

Pentru programul cu structura de graf dată în figura 2.5, complexitatea CM = 14 – 10 + 3 = 6, pentru că numărul de arce este 14, iar numărul de noduri este 10.

32

Ciclul de dezvoltare software

Figura 2.5 Structură de program cu salturi înapoi

Pentru programul de alegere a elementului minim şi a elementului maxim dintr-un şir dat prin textul sursă:

int x[N], i, n, minim, maxim;

n = citeste(); //functie de pentru numarul de elemente citeste(n, x); // functie de citire pentru elemente maxim = minim = x[0]; for ( i = 0; i < n; i++ ) { if ( minim > x[i] ) { minim = x[i]; } if ( maxim < x[i] ) { maxim = x[i]; } } printf( "Maximul este %d\n", maxim ); printf( "Minimul este %d\n", minim );

33

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

cu graful asociat dat în figura 2.6, complexitatea este

CM = 13 – 11 + 2 = 4

i++

if(min > x[i])

min = x[i]

max = x[i]

if (max < x[i])

1

2

3

4

5

6

7

8

9

10

11

12

13

Figura 2.6 Graf asociat programului de alegere a minimului şi maximului

Se observă că de la o tehnică de programare la alta diferă nivelul complexităţii în sens McCabe.

O altă metrică a complexităţii software o reprezintă complexitatea Halstead. Ea a fost dezvoltată în special pentru a măsura complexitatea unui modul de program pe baza codului sursă, cu accent pe complexitatea de calcul. Metricile Halstead se bazează pe patru valori numerice, rezultate din codul sursă:

n1 - numărul de operatori distincţi

n2 - numărul de operanzi distincţi

N1 - numărul total de operatori

N2 - numărul total de operanzi

34

Ciclul de dezvoltare software

Din aceste numere, rezultă cinci metrici:

Metrică Simbol Formula Lungimea programului N N = N1 + N2 Vocabularul programului n n = n1 + n2 Volumul V V = N * log2n Dificultatea D D = (n1/2)*(N2/n2) Efort E E=D * V

Volumul modulului, V, reprezintă conţinutul informaţional al modulului. El descrie dimensiunea implementării unui algoritm. Calculul lui V este bazat pe numărul de operaţii realizate şi de numărul de operanzi cu care lucrează algoritmul. Astfel determinat, volumul unei funcţii trebuie să rezulte între valorile 20 şi 1000. Un volum mai mare de 1000 reprezintă o indicaţie că funcţia respectivă conţine mult prea multe prelucrări. Volumul unui fişier cu cod sursă trebuie să se găsească între 100 şi 8000.

Complexitatea în sens Halstead CH este dată de relaţia:

CH = N1 log2N1 + N2 log2 N2

Astfel, pentru secvenţa:

a = 7; b = 8; c = a + b; d = a * c + b * ( a – 1 )

prin numărare operanzi şi operatori N1 = 10, N2 = 13.

CH = 10 log2 10 + 13 log2 13

deoarece cele două paranteze sunt un singur operand.

În secvenţa: a += b++ + c++; a*=--b - --c;

numărul operanzilor N2 = 6, iar numărul operatorilor N1 = 10, ceea ce conduce la complexitatea

CH = 10 log2 10 + 6 log2 6

35

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Pentru programe de mari dimensiuni este preferabil să se utilizeze instrumente care numără operanzii, respectiv operatorii, permiţând automatizarea procesului de măsurare a complexităţii în sens Halstead.

2.4 Etapele ciclului de dezvoltare

Ciclul de viaţă al unui produs software este, conform [MIHAL98], intervalul de timp de la momentul deciziei de realizare şi până la retragerea sau înlocuirea totală a acestuia cu un produs software, reprezentând orizontul de timp în care operează şi evoluează produsul program. Ciclul de dezvoltare este parte a ciclului de viaţă al unui produs software şi este definit ca totalitatea etapelor pe care le parcurge un produs software de la idee, până la utilizarea lui de către beneficiar. Acest interval, împărţit în general în faze, presupune definirea şi analiza problemei, proiectarea produsului software, implementarea de cod, testarea şi experimentarea acestuia, deci reprezintă o parte a ciclului de viaţă şi anume cea care are ca scop dezvoltarea produsului. Ciclul de viaţă adaugă ciclului de dezvoltare etapa de utilizare, etapa de mentenanţă, precum şi momentul scoaterii din uz, datorită uzurii morale a produsului software.

Etapa de definire şi analiză a problemei constă în stabilirea informaţiilor de intrare, a rezultatelor, a modelelor. Există probleme bine definite, când pentru toate rezultatele există modele şi date iniţiale suficiente. Problemele subdefinite fie nu au toate modelele definite, fie lipsesc datele iniţiale. Problemele supradefinite conţin fie modele complementare, fie mai multe date iniţiale decât cer rezultatele. Definirea problemei conduce la elaborarea de specificaţii ale cerinţelor produsului, ce trebuie să fie complete, consistente şi riguroase. Aceste cerinţe trebuie documentate şi agreate cu clientul înainte de a trece la următoarea fază.

Un document riguros de analiză cuprinde, pentru fiecare cerinţă funcţională în parte, următoarele informaţii:

• Intrările;

• Ieşirile; • modelele • datele de test; • dimensiunile volumelor de date; • precizia rezultatelor; • condiţii tehnice.

36

Ciclul de dezvoltare software

Etapa de proiectare constă în elaborarea soluţiei, corelând cerinţele care rezultă din definirea problemei, cu resursele pe care le oferă limbajele de programare, tehnologiile software existente, precum şi sistemele de gestiune a bazelor de date. Această fază asigură programatorului suficiente informaţii pentru ca acesta să scrie rutine software care realizează acţiuni bine definite în cadrul aplicaţiei.

Soluţia elaborată este documentată şi prezentată clientului spre aprobare. Ea se adresează următoarelor arii de interes în cadrul dezvoltării produsului software:

• proiectarea interfeţei cu utilizatorul, ce reprezintă imaginea sistemului pentru utilizator;

• proiectarea bazei de date, prin care se stabileşte cum vor fi stocate şi structurate datele utilizate de produs;

• stabilirea acţiunilor realizate de aplicaţie sub aspectul datelor ce sunt prelucrate şi a algoritmilor folosiţi pentru aceasta;

• identificarea şi definirea interfeţelor externe, prin care sistemul se conectează la alte sisteme existente (automat sau manual); stabilirea formatelor de fişiere folosite pentru exportul şi importul de date;

• conversiile de date, ce se referă la posibilitatea ca, în cazul în care datele existente sunt încărcate într-o aplicaţie nouă, anumite conversii şi reformatări să fie necesare.

Pe lângă cele menţionate mai sus, designul unui produs software precizează arhitectura generală a aplicaţiei, modul în care modulele componente sunt combinate pentru a oferi funcţionalitatea dorită, nivelul planificat pentru caracteristicile de calitate.

Dacă etapa de analiză a problemei spune ce trebuie să se facă, etapa de proiectare stabileşte cum trebuie să se facă. Documentul de proiectare este folosit de programatori şi de testeri, în etapele următoare, pentru a dezvolta produsul şi pentru a testa dacă funcţionalitatea rezultată în urma codificării corespunde funcţionalităţii proiectate.

Etapa de implementare a codului constă în scrierea efectivă a produsului software. Software trebuie scris ca să respecte cerinţele specificate în faza de analiză şi testate în timpul dezvoltării. De regulă există standarde de programare şi recomandări de soluţii pentru problemele comune care sunt aplicate în timpul dezvoltării produsului pentru a spori

37

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

calitatea acestuia. Principalele rezultate ale acestei faze sunt aplicaţia în sine, documentaţia şi rezultatele testelor. Dintre activităţile de asigurare şi control al calităţii, cele mai prezente în timpul acestei faze sunt cele de revizuire a codului sursă şi inspecţiile de cod şi de documente.

Etapa de testare are ca obiectiv să stabilească dacă sistemul este robust din punct de vedere tehnic, fiabil şi corespunde cerinţelor pentru care a fost dezvoltat. Caracteristicile principale ale testării sunt:

• posibilitatea de a identifica ulterior testele realizate;

• posibilitatea de a repeta execuţia unui test;

• documentarea paşilor urmaţi în testarea unei funcţionalităţi şi a rezultatelor obţinute.

Există câteva tipuri de testare, printre care:

• testarea fiecărei rutine software în parte;

• testare de integrare a modulelor asamblate în aplicaţia finală sau într-un subsistem;

• testare de sistem a întregii aplicaţii pe aceeaşi platformă ca cea pe care clientul o va folosi;

• testele de instalare prin care se verifică că procedurile şi instrucţiunile de instalare sunt corecte;

• testele de regresie, realizate după upgrade-uri de software (adăugări de funcţionalitate nouă, corectări de defecte);

• teste de acceptanţă, care sunt parte din faza de instalare, şi prin care clientul verifică dacă ceea ce i se livrează corespunde cu ceea ce a cerut.

Testarea reprezintă o arie extrem de complexă căreia, de cele mai multe ori, i se acordă insuficientă atenţie. Trebuie înţeles că oricât de mult s-ar testa un produs, nu se poate garanta că aplicaţia este complet lipsită de defecte. Costul testării trebuie analizat prin prisma costului potenţial al funcţionării proaste, prin prisma defectelor software. Pentru aplicaţiile necritice, este acceptabilă de exemplu, lansarea unei versiuni beta pentru ca utilizatorii să o testeze în producţie. O astfel de abordare nu este acceptată în cazul sistemelor software critice.

38

Ciclul de dezvoltare software

Etapa de implementare reprezintă predarea oficială a produsului împreună cu documentele asociate clientului. Procesul efectiv de instalare presupune:

• instalarea software pe sistemele de calcul ale clientului;

• executarea testelor de acceptanţă împreună cu clientul;

• semnarea unor documente prin care se acceptă produsul de către client;

• pregătirea viitorilor utilizatori ai sistemului pentru folosirea acestuia.

Etapa de mentenanţă, deşi nu face parte din ciclul de dezvoltare software, este o etapă extrem de importantă a ciclului de viaţă, pe care nici o companie producătoare de software nu trebuie să o neglijeze. Instalarea produsului şi predarea lui către client nu înseamnă că din acel moment, producătorul software nu mai are nici o legătură cu produsul. Mentenanţa include următoarele activităţi:

• rezolvarea problemelor care apar în timpul rulării aplicaţiei în mediul de producţie – detectarea, analiza şi corectarea defectelor software;

• modificarea interfeţelor – determinată de adăugări sau schimbări la platforma hardware pe care rulează software;

• extinderea funcţionalităţii sau îmbunătăţirea performanţelor – clientul solicită modificări după ce livrarea iniţială şi instalarea au fost realizate.

Perioada de mentenanţă care urmează imediat instalării este inclusă ca parte a unui contract de garanţie furnizat de producătorul software. Mentenanţa pe termen lung are un alt statut în cadrul relaţiei client – producător software.

Pentru ca un proiect de dezvoltare să fie un succes, o abordare sistematică este necesară. Proiectul trebuie împărţit în faze, fiecare cu un obiectiv clar. Munca efectuată în cadrul fiecărei faze furnizează dezvoltatorului informaţii necesare pentru a estima efortul din faza următoare. Costul total al proiectului este, astfel, mai scăzut decât estimarea realizată la începutul proiectului.

Nu toate proiectele urmează acest ciclu de dezvoltare. Dimensiunea proiectului, în termeni de cost şi efort necesar, şi natura proiectului

39

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

determină ce faze sunt relevante şi ce efort suplimentar trebuie alocat pentru fiecare fază.

Efectele pe care le generează un produs sunt multiplicate cu numărul de utilizatori simultani ai produsului. Este necesar ca activitatea de dezvoltare software să aibă la bază procese de optimizare dinamică, adică de alegere din aproape în aproape a acelor resurse ce în final asigură realizarea de produse de foarte bună calitate.

40

3.1 Specificaţiile de programare

Elaborarea de programe a depins şi va depinde în continuare de resursele hardware şi software existente la un moment dat. Elaborarea specificaţiilor este un moment extrem de important în ciclul de elaborare a programelor. Specificaţiile sunt într-un context mai larg, descrieri sistematice, riguroase a rezultatelor finale care trebuie obţinute, a datelor iniţiale şi, respective, a formulelor de calcul. Specificaţiile definesc cadrul în care se dezvoltă un program, prin facilităţi, restricţii. Mult mai târziu au apărut problemele legate de grupurile ţintă, de obţinerea unor produse de înaltă calitate.

Elaborarea specificaţiilor presupune un nivel de experienţă ridicat, o bună capacitate de analiză a proceselor şi calităţi de cuprindere a elementelor generale, în succesiunea lor logică. După ani lungi de experienţă în programare s-a pus problema elaborării unor sisteme formalizate, neambigui pentru elaborarea de specificaţii.

Există mari deosebiri între specificaţiile necesare pentru o problemă de calcule ştiinţifice şi specificaţiile pentru dezvoltarea de software pentru contabilitate, de exemplu. În primul caz, o formulă sau un set de formule stabilesc riguros care sunt rezultatele şi care sunt datele de intrare. În cazul problemelor economice, de exemplu, există legi, regulamente, există o experienţă acumulată, există specialişti. Construirea modelelor de calcul, stabilirea rapoartelor finale şi a datelor iniţiale reprezintă o artă, specificaţiile sunt producţiile unor artişti.

Dacă de exemplu, trebuie soluţionată o problemă de statistică, în sensul calculului mediei aritmetice ponderate se definesc:

• datele de intrare:

n – numărul de grupe; fi – frecvenţa de apariţie a datelor din grupa i; xi – nivelul grupei i pentru variabila x.

41

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

• datele de ieşire:

xm – nivelul mediei aritmetice.

• domeniile de variaţie:

]10000;10000[]100;0[

]1000;1000[}1000,....,6,5{

−∈∩∈

−∈∈

m

i

i

xNf

xn

• formule de calcul:

=

== n

ii

n

iii

m

f

xfx

1

1*

Pentru a analiza calitatea specificaţiilor se construiesc tabelele 3.1 şi 3.2 în care sunt stabilite relaţiile între elementele definite.

Dependenţa între variabila endogenă şi variabilele exogene Tabelul 3.1

Variabila endogenăVariabilele exogene

xm

n * fi * xi *

Utilizarea în formule a variabilelor

Tabelul 3.2

Variabile Formula

xm n fi xi

=

== n

ii

n

iii

m

f

xfx

1

1

*

* * * *

42

Abordarea clasică

Având în vedere că în tabelele 3.1 şi 3.2 nu există linii sau coloane fără asteriscuri, rezultă că definirile legăturilor dintre date de intrare şi rezultate sunt complete. Asteriscurile marchează prezenţa unei variabile în formulă sau legătura dintre variabilele exogene şi variabila endogenă, aşa cum rezultă din formulă.

Specificaţiile tratează şi cazurile de excepţie.

Pentru f1 = f2 = ..... = fn = 0, xm = 0.

În specificaţii sunt prezentate date de test, pentru seturile din tabelele 3.3, 3.4 şi 3.5 şi nivelurile variabilei xm.

Set de date identice Tabelul 3.3

Nr. crt fi xi 1 10 10 2 10 10 ..... 15 10 10 xm = 10

Serii de date compensate simetric Tabelul 3.4

Nr. crt fi xi 1 10 -15 2 8 -13 3 6 -11 4 4 -9 5 2 -7 6 2 +7 7 4 +9 8 6 +11 9 8 +13 10 10 +15 xm = 0

43

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Serii de date recursiv dependente Tabelul 3.5

Nr. crt fi = fi-1 + 2 xi = 4*fi - 1 1 1 3 2 3 11 3 5 19 4 7 27

Datele din tabele se construiesc aşa fel încât să existe o formulă de calcul pentru ele ca verificarea corectitudinii prelucrărilor realizate de program să fie mult mai uşoară, ca în exemplul datelor din tabelul 3.6

Serii de date generate Tabelul 3.6

Nr. crt fi = 2*i + 1 xi = i2 + 1 1 3 2 2 5 5 3 7 10 ………… N 2*n + 1 n2 + 1

Aplicând relaţia:

=

== n

ii

n

iii

m

f

xfx

1

1

*

pentru datele din tabelul 3.6 rezultă:

=

=

+

++= n

i

n

im

i

iix

1

1

)12(

)1)(12(

după efectuarea calculelor rezultă:

)2(3862

+++

=n

nnxm

44

Abordarea clasică

Se construieşte o procedură care generează seriile fi, xi şi calculează xm. Prin apelul programului de calcul pentru media aritmetică cu aceste date generate se obţine un rezultat x`m. Dacă x`m = xm, rezultă că programul scris pentru calculul mediei aritmetice este corect în raport cu setul de date pentru care a fost testat.

Specificaţiile de programare sunt construcţii care sistematizează informaţii extrem de variate. Cei care elaborează specificaţiile au rol hotărâtor în definirea contextului prin restricţiile asupra variabilelor, prin formulele construite şi prin informaţiile care însoţesc algoritmii de prelucrare.

Absenţa unor restricţii, definirea incorectă a domeniilor, construirea de formule incomplete generează probleme serioase procesului de elaborare a programelor. În contextul actual sunt soluţionate multe dintre problemele legate de elaborarea de specificaţii. În contextul primelor abordări, în ceea ce priveşte scrierea de programe, programatorul era singurul care îşi definea datele de intrare, definea rezultatele şi crea sau prelua algoritmi. Fără o gândire sistematică, fără instrumente adecvate, specificaţiile includeau o serie de inexactităţi sau eliminau o serie de aspecte, ceea ce reducea aria de cuprindere a programelor. Arta elaborării de programe îşi avea izvorul în arta de a elabora specificaţii.

3.2 Preluarea algoritmilor

Rezolvarea oricărei probleme are la bază un algoritm, dat fie printr-o formulă, fie prin paşi care descriu exact care sunt prelucrările. Pentru o aceeaşi problemă există mai mulţi algoritmi. Trebuie ales acela care îndeplineşte unul dintre criteriile:

• volumul de prelucrări este cât mai redus;

• volumul de prelucrări depinde de particularităţile pe care le prezintă datele de intrare; de exemplu, datele sortate crescător deja trebuie să genereze un volum minim de prelucrări în cazul algoritmului de sortare crescătoare;

• numărul rezultatelor intermediare care trebuie stocate în memorie să fie cât mai redus pentru a asigura dimensiuni cât mai mari problemelor de rezolvat;

• volumul prelucrărilor să fie cât mai redus, cu accent pe scăderea numărului de operaţii de înmulţire sau de împărţire.

45

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Programatorii preiau algoritmii deja analizaţi din punct de vedere a calităţii soluţiei sau construiesc algoritmi proprii. Pentru verificarea unui şir x1, x2, ...., xn care are componentele ordonate crescător se construiesc de către programator doi algoritmi:

Algoritmul A1:

• pasul 1 : se iniţializează variabila iv cu 0;

• pasul 2 : se traversează şirul;

• pasul 3 : se compară xi cu xi+1;

• pasul 4 : dacă xi > xi+1, se incrementează variabila iv;

• pasul 5 : după încheierea procesului de comparare se testează variabila iv;

• pasul 6 : dacă iv = 0, se afişează mesajul „şirul este ordonat”, în caz contrar se afişează mesajul „şirul nu este ordonat”.

Algoritmul A2:

• pasul 1 : se traversează şirul;

• pasul 2 : se compară xi cu xi+1;

• pasul 3 : dacă xi > xi+1 se întrerupe traversarea şi se afişează mesajul „şirul nu este ordonat”;

• pasul 4 : dacă traversarea se încheie normal, se afişează mesajul „şirul este ordonat”.

Este important să se construiască algoritmi compleţi şi corecţi pentru problemele de rezolvat şi abia după aceea să se urmărească îmbunătăţirea lor, fără a accepta conceptul de algoritm optim. Preluarea algoritmilor este o necesitate şi modul concret prin care se realizează constă în:

• asimilarea unei mulţimi de algoritmi de bază, pentru operaţii de prelucrare bine definite;

• alegerea algoritmilor care se potrivesc clasei de probleme în raport cu structurile de date utilizate; într-un fel sunt concepuţi algoritmii care lucrează cu fişiere, altfel sunt construiţi algoritmii care operează pe masive;

• combinarea de algoritmi pentru a obţine fluxurile de prelucrare specifice soluţionării unei probleme.

46

Abordarea clasică

Există mai multe moduri de descriere a algoritmilor, dintre care sunt prezentate în continuare următoarele:

• forma text, în care operaţiile şi operanzii sunt daţi sub formă de fraze precum: „se iniţializează de la tastatura...”; „rând pe rând se adună fiecare element”; „se compară element cu element....”; „primul element se interschimbă cu ultimul, al doilea se interschimbă cu penultimul, procedeul continuă în acelaşi fel până când...”.

• limbaj de descriere a operaţiilor în succesiunea efectuării – pseudo-cod :

x1 = 2 xi = xi-1, i = 2, 3, ...., n

sau x1 = 0 x2 = 1 xi = xi – 1 + xi-2, i = 2, 3, ....., n

sau dacă xi > xi+1 interschimb xi cu xi+1 • schemele logice, care reprezintă o transcriere grafică a etapelor

(paşilor) unui algoritm. Limbajul utilizat în descrierea algoritmului trebuie să conducă la

elaborarea de programe, fără confuzii, care prin testare să evidenţieze că pentru date de intrare complete şi corecte se obţine rezultatul corect indicat.

3.3 Elaborarea schemelor logice

Schema logică reprezintă o modalitate uzuală de reprezentare a algoritmilor folosind simboluri precum cele date în figura 3.1:

Figura 3.1 Simboluri pentru scheme logice

47

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

De asemenea, schemele logice includ conectori:

şi arce orientate Nivelul de detaliere depinde de modul de înţelegere a algoritmului, de

rolul pe care îl are cel care elaborează schema logică şi persoana care elaborează textul sursă. Între schema logică şi program trebuie să existe o corespondenţă clară în ceea ce priveşte numele variabilelor, etichetelor întrebuinţate şi formulelor de calcul.

Pentru aflarea elementului minim dintr-un şir şi a poziţiei acestuia se utilizează variabilele:

n - numărul de componente al vectorului x[];

x[i] - componenta i a vectorului;

xmin - valoarea elementului minim;

pozmin - poziţia elementului minim;

i - variabilă de control.

48

Abordarea clasică

Schema logică este dată în figura 3.2:

Figura 3.2 Schema logică pentru aflarea minimului şi poziţiei

49

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Pentru a verifica dacă o matrice este simetrică se construieşte schema logică din figura 3.3.

Figura 3.3 Schema logică pentru verificarea simetriei unei matrice

50

Abordarea clasică

Pentru numărarea componentelor nule, negative şi pozitive ale unui masiv a[m][n] bidimensional se elaborează schema logică din figura 3.4.

START

Citeste m,n

Citeste a[i][j], i=1, …, mj=1,…..,n

nrnul = 0nrpoz = 0nrneg = 0

i = 0

e1

j = 0

e2

a[i][j] > 0 e3 nrpoz = nrpoz + 1 e5Da

e4 nrneg = nrneg + 1 e5Da

1

a[i][j] < 0

nrnul = nrnul + 1

51

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Figura 3.4 Schema logică pentru numărarea elementelor negative, pozitive şi nule

Schemele logice clasice corespund unui stadiu incipient al dezvoltării limbajelor de programare. De exemplu, limbajul FORTRAN este înzestrat cu instrucţiunea IF cu sintaxa:

IF ( expresie ) e1, e2, e3 unde:

e1 - eticheta instrucţiunii care se execută dacă expresia este negativă;

e2 - eticheta instrucţiunii care se execută dacă expresia este nulă;

e3 - eticheta instrucţiunii care se execută dacă expresia este pozitivă.

52

Abordarea clasică

O astfel de construcţie impune utilizarea de instrucţiuni cu etichete şi continuarea prelucrării prin efectuarea unui salt necondiţionat la secvenţa cu eticheta e4, figura 3.5.

Figura 3.5 Secvenţa cu salturi condiţionate complete

53

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Pentru numărarea elementelor nule, pozitive şi negative ale unui masiv bidimensional se construieşte secvenţa de schemă logică dată în figura 3.6.

Figura 3.6 Schemă logică pentru scrierea de programe în limbajul FORTRAN

54

Abordarea clasică

Pe măsură ce se dezvoltă limbajele de programare, în schemele logice apar modificări. Programul scris în C++ după schema logică din figura 3.4 este:

#include <stdio.h> #define M 10 #define N 10 int main () { int i, j, m, n, a[M][N]; int nrplus, nrnegativ, nrnul; nrplus = nrnegativ = nrnul = 0; printf( "Introduce numarul de linii (<%d) = ", M ); scanf( "%d", &m ); printf( "Introduce numarul de coloane (%d) = ", N ); scanf( "%d", &n ); i = 0; et11: if ( i < m ) { j = 0; et12: if ( j < n ) { printf("a[%d][%d] = ", i, j ); scanf("%d", &a[i][j] ); j++; goto et12; } i++; goto et11; } i = 0; et1: if ( i < m ) { j = 0; et2: if ( j < n ) { if ( a[i][j] > 0 ) { nrplus++; } else { if ( a[i][j] < 0 ) { nrnegativ++; } else { nrnul++; } } j++; goto et2;

55

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

} i++; goto et1; } printf( "Numarul de elemente pozitive este %d\n", nrplus ); printf( "Numarul de elemente negative este %d\n", nrnegativ ); printf( "Numarul de elemente nule este %d\n", nrnul ); }

Datorită caracterului său artizanal, tehnica nu se baza pe reutilizarea codului. Programatorul rescria funcţionalităţi ce altfel se regăseau în biblioteci scrise deja. Prin aceasta dimensiunea codului sursă creştea foarte mult şi odată cu ea şi complexitatea software. De asemenea, frecvente erau şi programele monolit, care conţineau toată funcţionalitatea în programul principal, neexistând apeluri către subrutine.

Pentru aplicarea formulei complexităţii ciclomatice, se identifică următoarele valori pentru parametrii acesteia:

na = 36

nn = 31

Rezultă că valoarea complexităţii ciclomatice este:

CM = 36 – 31 + 2 = 7 Valoarea obţinută este relative mare, având în vedere că complexitatea

problemei este mică, însă ea se datorează în special faptului că pe de o parte au fost folosite instrucţiuni de salt necondiţionat, care au mărit numărul de arce din cadrul grafului asociat, dar şi deoarece nici varianta de algoritm implementată nu este optimă. Conform setului de valori-prag, acest program se încadrează în categoria de risc minim.

Pentru aplicarea formulelor Halstead, se identifică următoarele valori pentru parametrii acestora:

n1 = 5

n2 = 8

N1 = 22

N2 = 36

56

Abordarea clasică

Pentru aceste valori, aplicând formulele de calcul pentru metricile Halstead, se obţine:

Metrică Valoare Lungimea programului = 64Vocabularul programului = 14Volumul = 384Dificultatea = 14,25Efort = 5472

Dacă se raportează valorile obţinute la valorile-prag, se constată că ele se încadrează în intervalele acceptate. Trebuie menţionat că, deşi limbajul C++, folosit pentru a implementa rezolvările la problema aleasă, oferă implementări pentru structuri repetitive (for, while), s-a ales varianta cu instrucţiuni de salt necondiţionat specifică pentru perioada în care această tehnică de programare era folosită. Există limbaje, cum este şi cel de asamblare, care nu au implementări de structuri repetitive, şi prin urmare ele trebuie scrise de către programator.

Programarea clasică are o serie de imperfecţiuni pe care programatorii de azi nu le acceptă sau pentru care au găsit soluţii mai ales prin elaborarea de expresii condiţionale compuse. Acum, pentru a verifica dacă o matrice este sau nu simetrică se scrie secvenţa:

…………. k = 1; for ( i = 0; i < m && k == 1; i++ ) { for ( j = 0; j < n && k == 1; j++ ) { if ( a[i][j] != a[j][i] ) k = 0; } } if ( k == 1 ) printf (“matrice simetrica”); else printf (“matrice nesimetrica”); ………………….

Instrucţiunea if se înlocuieşte cu expresia de atribuire: k = a[i][j] == a[j][i].

secvenţa devenind: ……………………… k = 1; for ( i = 0; i < m && k == 1; i++ ) { for ( j = 0; j < n && k == 1; j++ ) { k = a[i][j] == a[j][i]; } } printf( k == 1 ? “matrice simetrica” : “matrice nesimetrica”); ………………….

57

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Programarea clasică are un caracter direct, având de evaluat expresii simple şi efectuând prelucrări cu paşi mărunţi, din aproape în aproape.

3.4 Scrierea programelor

Şi acum 40 de ani, ca şi acum, programatorul scria programele în limbajele pentru care avea compilatoare, dimensiona probleme pentru resurse de memorie internă, proiecta fişiere încât să respecte restricţiile de stocare pe suport intern sau extern. Şi atunci, ca şi acum, au existat restricţii. Datele şi programele erau stocate pe benzi perforate, pe cartele, iar programatorii care au lucrat între 1985 – 1990 ştiu că un loc la un terminal pentru a lucra interactiv era un lux, în timp ce în ziua de azi nu poate fi conceput în nici un chip decât lucru în reţea, la un post de lucru. Cartelele perforate banda perforată, sunt azi piese de muzeu.

Şi acum 30 – 40 de ani, programatorii, care erau selectaţi dintre cele mai inteligente persoane datorită restricţiilor legate de resursa calculator, au avut de traversat aceleaşi etape ale ciclului de dezvoltare software. Şi atunci, ca şi acum, existau mari probleme, existau preocupări de a le soluţiona şi chiar au existat produse software de foarte bună calitate pentru calcule economice, pentru optimizări, iar cine consideră că problemele s-au dezvoltat odată cu posibilitatea pe care o dau monitoarele VGA, SVGA sau altele de ultimă generaţie, limitează rezultatele prin eliminarea unor soluţii extreme de avansate obţinute încă din anii ’50.

Întotdeauna au existat preocupări pentru programe foarte bune. Este cunoscută colecţia de algoritmi şi programe din biblioteca ACM, publicate şi comentate în numerele mai vechi ale revistei Communications of ACM. Şi azi programatorii care urmăresc adevărata performanţă, consultă această colecţie pentru a preleva rezultate dintre cele mai bune, ştiut fiind că limbajul ALGOL a fost bază pentru tot ce este deosebit de avansat în programarea de azi.

Deşi, limbajele de programare limitau tipurile de date pentru tipurile fundamentale întreg, real simplă precizie şi real dublă precizie, orice problemă avea o rezolvare prin :

• atribuirea de coduri numerice pentru şiruri de caractere;

• utilizarea exclusivă a masivelor unidimensionale sau bidimensionale.

58

Abordarea clasică

Problema ordonării întreprinderilor după cifra de afaceri, îi corespunde azi textul sursă:

#define N 10 #define MAX_STRING 255 int main() { int n, i, j, min; char* sc[N]; float cf[N]; memset(sc, 0, N); printf( "Introduce numarul de societati comerciale (<=%d)", N ); scanf( "%d", &n); for ( i = 0; i < n; i++ ) { sc[i] = (char*)malloc(MAX_STRING); printf("Numele firmei = "); scanf("%s", sc[i]); printf("Cifra de afaceri = "); scanf("%f", &cf[i]); } for ( i = 0; i < n - 1; i++ ) { min = i; for ( j = i+1; j < n; j++ ) { if ( cf[j] < cf[i] ) { min = j; } } //muta elementul minim pe pozitia i char* temp = sc[i]; sc[i] = sc[min]; sc[min] = temp; float f = cf[i]; cf[i] = cf[min]; cf[min] = f; } for ( i = 0; i < n; i++ ) { printf(" Societatea %s are CF %f\n", sc[i], cf[i]); } }

Programarea clasică soluţiona problema prin codificarea întreprinderilor şi operarea cu doi vectori, unul de tip întreg pentru codurile întreprinderilor şi unul de tip real pentru valorile cifrei de afaceri, obţinând

59

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

programul care, la acel moment, din cauza imposibilităţii de a construi blocuri şi datorită absenţei instrucţiunii while, era scris folosind instrucţiunile if şi goto care aveau menirea de a selecta situaţiile diferite, cu efecte negative asupra fluenţei întregului demers. Se obţine textul sursă:

i = 0; et1: if ( i < n - 1 ) { min = i; j = i; et2: if ( j < n ) { if ( cf[j] < cf[i] ) { min = j; } j++; goto et2; } //interschimba pozitia i cu pozitia min int temp1 = sc[i]; sc[i] = sc[min]; sc[min] = temp1; float temp2 = cf[i]; cf[i] = cf[min]; cf[min] = temp2; i++; goto et1; } i = 0; et3: if ( i < n ) { printf(" Societatea %d are CF %f\n", sc[i], cf[i]); i++; goto et3; }

Comparând textele sursă, devine foarte clară necesitatea acceptării faptului că evoluţia conţinutului unui program a depins strict de resursele care au fost la dispoziţia programatorilor. Mulţi dintre programatorii cu părul alb sunt cei care au scris programe foarte bune în limbajul FORTRAN 4 acum 35 de ani, tot ei fiind cei care lucrează în C# sau Java cu aceeaşi uşurinţă.

60

Abordarea clasică

3.5 Testarea şi implementarea programelor

Trebuie spus că în acele vremuri, utilizatorii calculatoarelor nu erau nici pe departe ceea ce sunt azi. Azi, cetăţeanul dispune de calculator, de internet. Acum 35 – 40 de ani, numai companiile importante achiziţionau calculatoare. Existau analişti, programatori, iar operatorii din sălile cu climatizare unde erau amplasate computerele de mărimea unor şifoniere, erau deja persoane foarte importante. Utilizatorii de software ştiau să perforeze cartele de date, ştiau să şi depaneze programe. Înseamnă că programatorii rămâneau în continuare alături de produsele software pe care le realizau.

Ceva mai târziu, programele aplicative au dezvoltat o relaţie nouă. Utilizatorul acestor programe trebuia să ştie exact cum se dispun datele pe cartele pentru a stabili intrări complete şi corecte.

Bibliotecile de program includeau programe de clasă A, testate sub toate aspectele, care nu mai produceau întreruperi în execuţie fără mesaje specifice. Programele de clasă B erau testate suficient de bine, însă, nu erau excluse unele întreruperi cu mesaje ale sistemului de operare. Programele de clasă C erau deja programe despre care nu se dădeau aprecieri asupra comportamentului şi asupra riscurilor de apariţie a incidentelor de prelucrare.

Se observă acum scăderea interesului spre dezvoltarea de tehnici de testare probabil datorită existenţei instrumentelor de asistare a procesului de elaborare a produselor software. Pentru testarea unui program se parcurg mai mulţi paşi:

• se construiesc seturi de date de test pentru care se stabilesc rezultatele pe care programul le oferă;

• se preiau din literatură exemple rezolvate folosind atât datele de intrare ale acestora, cât şi rezultatele intermediare;

• se introduc în program instrucţiuni de afişare a rezultatelor intermediare;

• se introduc în program instrucţiuni care permit afişarea etichetelor instrucţiunilor care se execută.

De exemplu, programul pentru însumarea elementelor unui şir se procedează astfel:

• se iniţializează şirul cu valorile 1,2, 3, ….., n;

61

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

• se afişează valoarea variabilei de control a şirului, valorile şirului şi sumele intermediare;

• se afişează mesaje care arată că au fost parcurse etapele programului.

Pentru testare, programul are textul sursă: #define N 100 #define n 10 void main() { int i, x[N], suma; printf(“Generare date de test”); for (i = 0; i < n; i++) { x[i] = i; } printf(“\n Intrare in procedura”); suma = aduna(x, n); printf(“\n Iesire din procedura”); printf(“\n suma = %d”, suma); } int aduna(int x[], int n) { int suma = 0; for ( i = 0; i < n; i++ ) { suma += x[i]; printf(“Suma dupa adunarea %d element cu valoarea %d este %d”, i, x[i], suma); } printf(“\n Gata”); return suma; }

Pe monitorul computerului pentru n = 10, se afişează: Generare date de test Intrare in procedura 0 0 0 1 1 1 2 2 3 3 3 6 4 4 10 5 5 15 6 6 21 7 7 28 8 8 36 9 9 45 10 10 55 Gata Iesire din procedura Suma = 55

62

Abordarea clasică

Din analiza acestor informaţii se verifică faptul că programul efectuează corect prelucrările. În cazul în care se testează procedura:

int expresie( int a, int i, int k) { int result = 0; if ( i % 2 ) { if ( !(i % k) ) { printf("Numar divizibil cu %d\n", k); result = a * a; } else { printf("Numar nedivizibil cu %d\n", k); result = a; } } else { printf("Numar par\n"); result = a * a * a; } return result; }

pentru evaluarea expresiei:

⎪⎩

⎪⎨⎧

=restîn,a

kprindiviyibilimparnumăuesteidacă,aimparesteidacăa,

e3

2

se introduce în programul principal şi în procedură instrucţiuni care afişează informaţii despre fluxul de execuţie al programului şi din care să rezulte programul execută ceea ce trebuie şi nu evaluarea unei alte expresii.

int expresie( int a, int i, int k) { int result = 0; if ( i % 2 ) { if ( !(i % k) ) { printf("Numar divizibil cu %d\n", k); result = a * a; } else { printf("Numar nedivizibil cu %d\n", k); result = a; } } else { printf("Numar par\n"); result = a * a * a; } return result; }

63

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

int main() { int a, i, k, n, j; printf( "Introduce numarul de seturi de date i = " ); scanf( "%d", &n ); printf( "Introduce numarul de variabile de control k = " ); scanf( "%d", &k ); printf("i\ti*i\ti*i*i\tk\ta\texpresie\n"); for ( j = 1; j <= k; j++ ) { for ( i = 0; i < n; i++ ) { printf("%d\t%d\t%d\t%d\t%d\t%d\n", i, i*i, i*i*i, j, i, expresie(i, i, j)); } } }

Pentru k = 7 şi n = 10, secvenţa care se afişează este: i i*i i*i*i k a expresie Numar par 0 0 0 7 0 0 Numar nedivizibil cu 7 1 1 1 7 1 1 Numar par 2 4 8 7 2 8 Numar nedivizibil cu 7 3 9 27 7 3 3 Numar par 4 16 64 7 4 64 Numar nedivizibil cu 7 5 25 125 7 5 5 Numar par 6 36 216 7 6 216 Numar divizibil cu 7 7 49 343 7 7 49 Numar par 8 64 512 7 8 512 Numar nedivizibil cu 7 9 81 729 7 9 9

Analizând rezultatele intermediare, rezultă că procedura efectuează prelucrări corecte. Pentru programele cu grad de complexitate ridicată, procedurile de testare sunt mai elaborate. Cei care elaborează specificaţiile aveau obligaţia să furnizeze situaţiile care trebuie testate, cu luarea în considerare a situaţiilor care în practică ofereau cazuri greu de controlat,

64

Abordarea clasică aşteptându-se ca prin implementarea produsului software, tocmai acele erori de prelucrare manuală să fie înlăturate.

3.6 Între artă, meşteşug şi industrie

Activitatea de programare, prin faptul că era dezvoltată de persoane deosebit de inteligente şi avea o pondere de peste 90% din costul produsului, a fost înconjurată de misterul creaţiei artistice, nefăcându-se deosebiri radicale de modul în care se scriau programele şi modul în care se realizau operele artistice. În plus, programul nu are o formă materială asemeni unui strung sau a unei statui. Nu întâmplător, David E. Knuth şi-a intitulat monumentala lucrare care stă la baza activităţii oricărui programator, ART OF PROGRAMMING.

Programarea s-a învăţat prin exemple, ca de la maestru la discipol. Numai după apariţia cărţilor de programare care au prezentat limbajele de programare folosind multe exemple s-au conturat unele direcţii de bază. Programarea calculatoarelor a rămas o artă atât timp cât bibliotecile de subprograme nu au fost realităţi cotidiene. De fiecare dată programatorul scria subprograme pe care deja le mai scrisese cândva de mai multe ori. În plus, nu a existat încrederea în programele scrise de alţii. Se justifică această reţinere prin:

• calitatea slabă a programelor incluse în paginile cărţilor;

• contribuţiile nefaste ale redactorilor de carte, cărora ghilimelele li se păreau că prin înlocuire cu apostrofuri dau frumuseţe textului, iar prin eliminarea slash-urilor(//) sau prin includerea pe toate liniile sursă, se obţine un aspect estetic acceptabil pentru carte; iar înlocuirea caracterului _ cu minus deja are efecte dintre cele mai compromiţătoare.

Trecerea spre meşteşug s-a impus când au trebuit scrise multe programe pentru a dezvolta sisteme informatice complexe. Deja există un responsabil de proiect, deja trebuia să existe un mod de comunicare prin specificarea structurilor de articole, dimensiunilor de matrice. Trebuia pusă ordine în modul în care sunt prelucrate datele pentru ca un program să nu distrugă informaţii pe care un alt program trebuie să le preia. Discuţiile dintre analişti, discuţiile dintre programatori au cristalizat tehnici de analiză şi stiluri de programare.

65

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

A început perioada definirii unor reguli care să conducă la creşterea calităţii programelor. De la caracterul meşteşugăresc spre caracterul industrial era doar un pas. Exagerările legate de transferul mecanic a unor elemente de structură şi de management din industria prelucrătoare spre producţia de software a avut un singur efect: compromiterea ideii de industrie de software. Lucrurile apar acum cu totul sub altă lumină în contextul organizaţiei virtuale de producţie software pentru că:

• posturile de lucru sunt legate prin reţea;

• accesul la resurse este dat de facilităţile internetului;

• instrumentele de dezvoltare a aplicaţiilor distribuite sunt o realitate;

• concepţia dezvoltării de aplicaţii complete care să aducă acoperire avantajoasă pentru cetăţean în planul fluxurilor materiale şi a fluxurilor băneşti este deja o realitate.

Aşa cum societatea umană a trecut prin comuna primitivă, epoca bronzului, epoca fierului, prin sclavagism, feudalism, capitalism, comunism şi retur, programarea calculatoarelor a reflectat salturile spectaculoase din domeniul hardware. În software nu există penurie de idei. Există numai restricţii în plan tehnic. De la o etapă la alta, pe măsură ce barierele sunt doborâte, se obţin şi salturi calitative în producţia de software; iar ceea ce se întâmplă an de an este o evoluţie normală şi necesară care ajută dezvoltarea societăţii informaţionale.

66

4.1 Reguli de construire

Acumularea experienţei de către programatori a permis gruparea de subprograme în biblioteci. În primul rând, programele de bibliotecă sunt toate scrise într-un acelaşi limbaj de programare. În al doilea rând, toate programele sunt regrupate astfel încât să conducă la rezolvarea de clase de probleme bine conturate. Bibliotecile matematice au programe de calcul polinomiale, de calcule matriceale, de rezolvare de sisteme de ecuaţii şi de implementare a algoritmilor analizei numerice pentru calcul diferenţial. Bibliotecile statistice conţin programe de analiză dispersională, de covarianţă, de regresie şi de calcul a indicatorilor statistici. Bibliotecile de programe sunt astfel proiectate încât să permită lansarea în execuţie a programelor, cât mai uşor posibil.

În al treilea rând, programele de bibliotecă au interfeţe prietenoase care asigură definirea problemelor de către utilizatori, introducerea de date şi alegerea de opţiuni în vederea obţinerii rezultatelor dorite.

În al patrulea rând, bibliotecile de programe conţin programe testate din toate punctele de vedere, încât să existe garanţia că acestea rezolvă corect problemele. Pentru a dezvolta o bibliotecă de programe se elaborează un plan riguros care include:

• obiectivul bibliotecii; • limbajul de programare utilizat; • algoritmii de prelucrare pentru care se elaborează programe; • nivelul de testare al programelor; • gradul de generalitate atins; • precizia rezultatelor; • dimensiunile problemelor de rezolvat; • modul de tratare a erorilor.

67

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Regulile de construire a bibliotecilor de programe sunt stricte pentru a se obţine programe de foarte bună calitate. Prima regulă constă în acceptarea unui stil de programare caracterizat prin:

• modul de alegere a numelor de variabile;

• modul în care sunt iniţializate variabilele;

• construirea etichetelor şi dispunerea lor, de exemplu, în ordine crescătoare e10, e20, ... , e300;

• utilizarea instrucţiunilor pentru a asigura un mod unitar de interpretare; astfel, dacă se adoptă un mod de parcurgere a unui masiv unidimensional cu ajutorul instrucţiunii for, atunci aceasta este aplicată în toate cazurile din cadrul programului, evitându-se introducerea unor parcurgeri cu while sau chiar instrucţiuni de salt necondiţionat, goto;

• separarea instrucţiunilor de citire şi scriere de celelalte subprograme pentru a nu influenţa procesul de mentenanţă atunci când se doreşte efectuarea de modificări în program, dar şi pentru a obţine creşterea flexibilităţii în ceea ce priveşte iniţializarea variabilelor;

• stabilirea structurilor de date utilizate pentru a obţine maximizarea gradului de utilizare a memoriei alocate; de la un program la altul, pentru aceleaşi prelucrări se folosesc aceleaşi structuri de date, denumirile lor nefiind diferite, mai ales, în ceea ce priveşte pointerii cu care se referă componentele;

• asigurarea continuităţii atât în ceea ce priveşte interfeţele, cât şi în ceea ce priveşte modul de elaborare a programelor. Biblioteca de programe trebuie să apară ca un întreg, format din componente omogene – programele.

Pe măsură ce s-au impus bibliotecile de programe, dezvoltatorii de software au dezvoltat şi biblioteci de subprograme, cele mai multe subprograme dezvoltând funcţii de prelucrare fundamentale în raport cu asigurarea calităţii şi mai ales cu creşterea generalităţii programelor. Şi subprogramele de bibliotecă sunt elaborate pe baza unor reguli, din care mai importante sunt:

• construirea numelui de subprogram în aşa fel încât să se reţină cu uşurinţă şi să se intuiască semnificaţia prelucrării; de exemplu, invmat() este subprogramul pentru inversarea de

68

Programarea standard

matrice, sortstring() este subprogramul pentru sortarea unui şir, sortfile() este subprogramul pentru sortarea unui fişier, prodpol() este subprogramul pentru produsul a două polinoame;

• pentru tipul rezultatului returnat se indică de fiecare dată semnificaţia;

• nivelul de autodocumentare este foarte ridicat, fiind incluse comentarii legate de semnificaţia parametrilor, a secvenţelor conţinând elemente de identificare a programatorului şi a algoritmului utilizat;

• listele de parametrii au elemente comune care să fie uşor interpretate şi mai ales să fie uşor de construit şi de iniţializat pentru a efectua un apel corect pentru fiecare subprogram;

• se stabileşte regula privind modul de revenire în programul principal; într-un fel este privit programul ce conţine o singură instrucţiune return, faţă de programul care are mai multe instrucţiuni return.

Şeful de proiect al unei biblioteci de programe sau de subprograme, când constituie echipa de analişti şi programatori prezintă reguli, care sunt adoptate, chiar cu unele modificări. După ce regulile sunt adoptate, devin obligatorii şi toate programele sau subprogramele sunt elaborate cu utilizarea acestor reguli. Prin respectarea regulilor, biblioteca de programe este o construcţie unitară, operaţională şi, mai ales, care este utilizată de toţi cei care dezvoltă software, datorită economiei de efort pe care o generează.

4.2 Subprogramul

Programarea standard are la bază lucru cu subprograme performante, stocate în biblioteca de subprograme. Programatorul are menirea de a selecta cele mai potrivite subprograme pentru a soluţiona fiecare problemă în modul cel mai eficient. Pentru problema definită în capitolul Ciclul de dezvoltare software se consideră o bibliotecă de subprograme care operează cu masive unidimensionale.

Regulile după care se construiesc subprogramele sunt următoarele:

• numărul de componente ale şirului este n;

• elementele şirului sunt x[0], x[1], …, x[n-1];

69

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

• denumirea subprogramului arată operaţia şi operandul de bază;

• variabila i permite referirea elementelor unui şir sau referă liniile unui masiv bidimensional;

• variabila j referă elementele unei coloane din masivul bidimensional;

• masivul bidimensional este referit prin numele a[M][N]. Biblioteca de subprograme BVECTMAT se organizează pe două

niveluri:

• nivelul de bază conţine proceduri de lucru cu masive unidimensionale (vectori);

• nivelul derivat conţine proceduri de lucru cu masive bidimensionale văzute ca vectori de vectori, folosind subprogramele de lucru cu vectori.

Pentru copierea elementelor unei linii k dintr-un masiv bidimensional a[M][N] într-un masiv unidimensional x[N], se foloseşte subprogramul:

void copylinvect( int a[][N], int x[], int n, int k ) { for ( int j = 0; j < n; j++ ) { x[j] = a[k][j]; } }

Pentru copierea unei coloane k a matricei a[M][N] în vectorul x[M] se foloseşte subprogramul:

void copycolvect( int a[][N], int x[], int m, int k ) { for ( int i = 0; i < m; i++ ) { x[i] = a[i][k]; } }

Pentru însumarea elementelor unui vector se foloseşte subprogramul: int sumvect( int x[], int n ) { int sum = 0; for ( int i = 0; i < n; i++ ) { sum += x[i]; } return sum; }

70

Programarea standard

Pentru produsul scalar a vectorilor x[N], y[N] se defineşte subprogramul:

int prodscalar ( int x[], int y[], int n ) { int prod = 0; for ( int i = 0; i < n; i++ ) { prod += x[i] * y[i]; } return prod; }

Pentru copierea unui vector x[N] pe linia k a unei matrice a[M][N] se utilizează subprogramul:

void copyvectlin( int a[][N], int x[], int n, int k ) { for ( int j = 0; j < n; j++ ) { a[k][j] = x[j]; } }

Pentru copierea vectorului x[M] pe coloana k a matricei a[M][N] se foloseşte subprogramul:

void copyvectcol( int a[][N], int x[], int m, int k ) { for ( int i = 0; i < m; i++ ) { a[i][k] = x[i]; } }

Pentru numărarea elementelor unui şir x[] mai mari decât valoarea k, se foloseşte subprogramul:

int contorgreater ( int x[], int n, int k ) { int contor = 0; for ( int i = 0; i < n; i++ ) { if ( x[i] > k ) { contor++; } } return contor; }

71

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Pentru numărarea elementelor şirului x[] egale cu o valoare k se utilizează subprogramul:

int contorequal ( int x[], int n, int k ) { int contor = 0; for ( int i = 0; i < n; i++ ) { if ( x[i] == k ) { contor++; } } return contor; }

Pentru a contoriza elementele şirului mai mici decât o valoare k, se scrie subprogramul:

int contorless ( int x[], int n, int k ) { int contor = 0; for ( int i = 0; i < n; i++ ) { if ( x[i] < k ) { contor++; } } return contor; }

Iniţializarea prin atribuire a elementelor vectorului x[] cu o valoare k se realizează prin procedura:

void initvect ( int x[], int n, int k ) { for ( int i = 0; i < n; i ++ ) { x[i] = k; } }

Pentru iniţializarea componentei de pe poziţia poz a şirului x[] cu o valoare k se utilizează procedura:

void initpoz ( int x[], int poz, int k ) { x[poz] = k; }

Pentru realizarea procedurilor derivate se utilizează procedeul de lucru cu masive unidimensionale. Astfel, generarea matricei unitate linie de linie presupune conturarea unei structuri repetitive de tip for dată în figura 4.1.

72

Programarea standard

void genunit ( int a[][N], int m, int n ) { int i, j; for ( i = 0; i < m; i++ ) {

} }

Figura 4.1 Funcţia de generare a matricei unitate folosind funcţii de lucru cu vectori

Procedura completă are textul sursă: void genunit (int a[][N], int m, int n ) { int x[N]; for ( int i = 0; i < m; i++ ) { initvect(x, n, 0); initpoz(x, i, 1); copyvectlin(a, x, m , n, i); } }

Subprogramul pentru scăderea a două matrice este: void scadmat(int a[][N], int b[][N], int m, int n) { int i, x[N], y[N], z[N]; for ( i = 0; i < m; i++ ) { copylinvect(a, x, m, n, i); copylinvect(b, y, m, n, i); scadvect(x, y, z, n); copyvectlin(a, z, m, n, i); } }

73

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Subprogramul scadvect() care evaluează expresiile z[i] = x[i] – y[i], are textul sursă:

void scadvect(int x[], int y[], int z[], int n) { int i; for ( i = 0; i < n; i++ ) { z[i] = x[i] - y[i]; } }

Pentru operaţia de adunare a două matrice a şi b cu rezultat în matricea c, se construieşte subprogramul:

void adunmat( int a[][N], int b[][N], int m, int n) { int i, x[N], y[N], z[N]; for ( i = 0; i < m; i++ ) { copylinvect(a, x, m, n, i); copylinvect(b, y, m, n, i); adunvect(x, y, z, n); copyvectlin(a, z, m, n, i); } }

Subprogramul adunvect() care evaluează expresiile z[i] = x[i] + y[i], are textul sursă:

void adunvect(int x[], int y[], int z[], int n) { for ( int i = 0; i < n; i++ ) { z[i] = x[i] + y[i]; } }

Dacă trebuie scris subprogramul pentru transpunerea unei matrice a[][N], obţinându-se matricea b[][M], procedura corespunzătoare este:

void transpune ( int a[][N], int b[][N], int m, int n) { int j; int x[N]; for ( j = 0; j < n; j++ ) { copycolvect(a, x, m, j); copyvectlin(b, x, m, j); } }

74

Programarea standard

Pentru produsul a două matrice se scrie procedura: void prodmat( int a[][N], int b[][P], int c[][P], int m, int n, int p) { int i, j; int x[N], y[N]; for ( i = 0; i < m; i++ ) { copylinvect(a, x, n, i); for ( j = 0; j < n; j++ ) { copycolvect(b, y, n, j); c[i][j] = prodscalar(x, y, n); } } }

Iniţializării de la tastatură a elementelor vectorului x[N] îi corespunde procedura:

void citvect (int x[], int n) { for ( int i = 0; i < n; i++ ) { printf("x[%d] = ", i); scanf("%d", &x[i]); } }

Pentru afişarea elementelor unui şir se scrie procedura: void scrievect (int x[], int n) { for ( int i = 0; i < n; i++ ) { printf("x[%d] = %d", i, x[i]); } }

Iniţializarea unei matrice de la tastatură linie de linie se efectuează prin procedura:

void citmat(int a[][N], int m, int n) { int i, x[N]; for ( i = 0; i < m; i++ ) { citvect(x, n); copyvectlin(a, x, n, i); } }

75

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Procedura pentru numărarea elementelor pozitive, nule respectiv negative ale unei matrice se realizează cu ajutorul procedurilor:

int contorplus (int a[][N], int m, int n) { int i, x[N]; int contor = 0; for ( i = 0; i < m; i++ ) { copylinvect(a, x, n, i); contor += contorgreater(x, n, 0); } return contor; }

int contorminus (int a[][N], int m, int n) { int i, x[N]; int contor = 0; for ( i = 0; i < m; i++ ) { copylinvect(a, x, n, i); contor += contorless(x, n, 0); } return contor; } int contorzero (int a[][N], int m, int n) { int i, x[N]; int contor = 0; for ( i = 0; i < m; i++ ) { copylinvect(a, x, n, i); contor += contorequal(x, n, 0); } return contor; }

Pentru rezolvarea problemei PROB formulate în capitolul Ciclul de dezvoltare software, programul apelator va conţine:

• apelul subprogramului citmat() pentru iniţializarea de la tastatură;

• apelul subprogramului contorplus() pentru contorizare elemente mai mari decât k = 0;

• apelul subprogramului contorzero() pentru numărare elemente egale cu 0;

76

Programarea standard

• apelul subprogramului contorminus() pentru numărarea elementelor mai mici decât k = 0;

• apelul unui subprogram pentru afişarea valorilor nrplus, nrminus şi nrzero cum este următoarea:

void printint (char w[], int x) { printf(" %s %d\n", w, x); }

Se observă că programul apelator conţine exclusiv definiri de operanzi şi apeluri de funcţii.

int main() { int a[M][N]; int m, n; int nrplus, nrminus, nrnul; printf("Introduce numarul de linii = "); scanf("%d", &m); printf("Introduce numarul de coloane = "); scanf("%d", &n); citmat(a, m, n); nrplus = contorplus(a, m , n); nrminus = contorminus(a, m, n); nrnul = contorzero(a, m, n); printint("Numarul de elemente pozitive este ", nrplus); printint("Numarul de elemente negative este ", nrminus); printint("Numarul de elemente zero este ", nrnul); return 1; }

Pentru aplicarea formulei complexităţii ciclomatice pentru programul apelator (funcţia main) se identifică următoarele valori pentru parametri:

na = 11

nn = 12

Rezultă că valoarea complexităţii ciclomatice este:

CM = 11 – 12 + 2 = 1

77

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

O astfel de valoare are mai multe semnificaţii:

datorită atomicizării acţiunilor, şi grupării lor în biblioteci de programe şi subprograme, construirea unui produs software este mult simplificată, ajungându-se la situaţia ideală de a avea de scris doar o succesiune de apeluri de rutine pentru implementarea unei funcţionalităţi; din acest punct de vedere complexitatea produsului este redusă, mai ales în condiţiile unei bune documentări a rutinelor folosite;

dacă bibliotecile sunt concepute în cadrul aceluiaşi proiect ca şi produsul software, atunci valoarea complexităţii este înşelătoare, pentru că ea nu exprimă şi complexitatea ascunsă a rutinelor folosite.

În cazul în care se calculează şi complexităţile ciclomatice ale rutinelor apelate din cadrul programului apelator, se obţin următoarele rezultate pentru parametrii formulei şi respectiv pentru complexitate:

Rutină na nn Complexitate main 11 12 1citmat 5 4 3contorplus 7 6 3contorminus 7 6 3contorzero 7 6 3printint 0 1 1

Complexitatea totală este 14, cu următoarele ipoteze:

instrucţiunea for este considerată o singură instrucţiune şi nu trei;

nu s-a mers mai departe de primul nivel de derivare, adică nu a fost dimensionată şi complexitatea rutinelor de pe nivelul de bază al bibliotecii de subprograme.

Se observă, doar în condiţiile în care se iau în considerare şi complexităţile rutinelor apelate, valoarea complexităţii ciclomatice totale se apropie de valoare obţinută pentru abordarea clasică. Însă, în mod firesc, are sens calculul complexităţii doar pe baza codului din programul apelator, fie şi din simplul motiv că biblioteca de subprograme este deja compilată şi prin urmare nu există un acces la codul-sursă.

78

Programarea standard

În condiţiile în care se consideră semnificativ, analiza doar asupra programului apelator, pentru aplicarea formulelor Halstead se identifică următoarele valori pentru parametrii:

n1 = 2 n2 = 6 N1 = 5 N2 = 20

Numărul total de operanzi este semnificativ mai mare decât celelalte valori datorită utilizării unora dintre aceştia în apeluri repetate către subrutine pentru a implementa funcţionalităţi distincte.

Valorile pentru metricile Halstead, în aceste condiţii sunt:

Metrică Valoare Lungimea programului = 25Vocabularul programului = 8Volumul = 25*log28Dificultatea = 3,3Efort = 75,75*log28

Comparativ cu valorile obţinute pentru abordarea clasică, acestea sunt mult inferioare, ceea ce dovedeşte o mai bună implementare a rezolvării problemei, cu o complexitate şi un efort mult diminuat prin reutilizarea subrutinelor din biblioteca de subprograme. Programarea standard se caracterizează, în principal, printr-o productivitate mult crescută.

4.3 Generalitatea subprogramelor

Programarea standard impune dezvoltarea de subprograme cu un grad de generalitate foarte ridicat. În primul rând, subprogramele trebuie să acopere o arie mai ridicată a problemelor. De exemplu, pentru calculul mediei, se impune a abordare gradată. Mai întâi se construieşte procedura pentru calculul mediei aritmetice simple, cu textul sursă:

float mediaaritm( float x[], int n) { int i; float xmed = 0; for ( i = 0; i < n; i++ ) { xmed += x[i]; } xmed /= n; return xmed; }

79

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

După aceea se construieşte procedura pentru calculul mediei aritmetice ponderate, cu textul sursă:

float mediapond ( float x[], int f[], int n) { int i, sumf = 0; float sumxf = 0, xmed; for ( i = 0; i < n; i++ ) { sumf += f[i]; sumxf += x[i] * f[i]; } xmed = sumxf / sumf; return xmed; }

Dacă se doreşte creşterea generalităţii unui subprogram în sensul că acesta să permită fie calculul mediei aritmetice simple, fie calculul mediei aritmetice ponderate, se procedează la elaborarea textului sursă:

float media ( float x[], int f[], int n, int k) { int i, sumf = 0; float sumxf = 0, xmed; if ( k == 0 ) { for ( i = 0; i < n; i++ ) { f[i] = 1; } } xmed = mediapond(x, f, n); return xmed; }

Utilizatorul iniţializează variabila k pe zero dacă doreşte să calculeze

o medie aritmetică simplă. Pentru a calcula media aritmetică ponderată variabila k se iniţializează cu o valoare diferită de zero. Mai mult, dacă se doreşte calculul altor tipuri de medii (media geometrică, media armonică) în acelaşi subprogram, se impune efectuarea de modificări adecvate.

În al doilea rând, generalitatea trebuie privită prin prisma acceptării cazurilor particulare. Trebuie tratate distinct cazurile particulare. De exemplu, subprogramul care tratează situaţia în care suma frecvenţelor din relaţia este nulă.

=

== n

ii

n

iii

f

fxx

1

1

80

Programarea standard Textul sursă al subprogramului mediapond() este:

float mediapond ( float x[], int f[], int n ) { ..... if ( sumf == 0 ) {

xmed = 0; }else {

xmed = sumxf / sumf;

}

............... }

Proiectantul bibliotecii trebuie să stabilească outputurile subprogramelor pentru situaţiile particulare; de cele mai multe ori subprogramul returnează coduri asociate poziţiilor unei liste de mesaje care trebuie afişate. Codurile trebuie testate pentru a determina fluxuri de prelucrare diferenţiate funcţie de outputurile subprogramelor.

În al treilea rând, subprogramele conţin teste suficient de puternice pentru a permite efectuarea de calcule.

Expresia:

=

== n

ii

n

iii

f

fxx

1

1

pentru a fi evaluată corect trebuie ca fi >= 0, i = 1, 2, ..., n şi n > 0. Aceste condiţii trebuie avute în vedere de designerii de subprograme. Se ia în considerare atitudinea acestora fie de a lăsa programatorul care dezvoltă aplicaţii folosind subprograme de bibliotecă pentru a face validările, fie de a le încorpora în subprograme şi de a furniza mesaje referitoare la modul în care s-a derulat prelucrarea.

Astfel, se dă subprogramul: int mediapondt ( float x[], int f[], int n, float &xmed ) { int rezultat = 0; if ( n < 1 ) { rezultat = -1; } if ( contorless (f, n, 0) ) { rezultat = -2; }

81

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

if ( rezultat == 0 ) { xmed = mediapond( x, f, n); } return rezultat; }

Subprogramul returnează 0 dacă prelucrarea s-a efectuat complet şi corect. Subprogramul returnează -2 dacă există frecvenţe negative şi returnează -1 dacă numărul de componente ale şirului nu conţine cel puţin un element.

În al patrulea rând, generalitatea problemei creşte atunci când subprogramele sunt construite pentru a accepta diferite tipuri de operanzi. Pentru produsul scalar a doi vectori de tip întreg, se construieşte subprogramul:

long int prodscalar( int x[], int y[], int n) { long int rezultat = 0; for ( int i = 0; i < n ; i++ ) { rezultat += x[i] * y[i]; } return rezultat; }

Pentru produsul scalar a doi vectori de tip float se construieşte: float prodscalar( float x[], float y[], int n) { float rezultat = 0; for ( int i = 0; i < n ; i++ ) { rezultat += x[i] * y[i]; } return rezultat; }

În programarea orientată obiect această problemă de multiplicare a codului funcţie de tipul operanzilor este soluţionată folosind descriptorul template.

Biblioteca de subprograme este construită sub formă de componente interdependente. Dacă se doreşte soluţionarea unei probleme, mai întâi se analizează dacă există deja subprogram în bibliotecă. În caz contrar, se identifică subprogramele apte să rezolve problemele fiecărui pas al algoritmului. Dacă acest aspect impune dezvoltarea de subprograme de

82

Programarea standard bază, evident, acestea se elaborează. Din aproape în aproape se dezvoltă o bibliotecă de subprograme care, în al cincilea rând, acoperă prin diversitatea de prelucrări de bază, toate cerinţele pentru a soluţiona orice problemă, generalitatea bibliotecii fiind apreciată în raport cu diversitatea de probleme.

4.4 Listele de parametrii

Când se elaborează o bibliotecă de subprograme, trebuie respectate o serie de reguli, care încep cu lista de parametrii, continuă cu stilul de programare şi se încheie cu modul în care este testată fiecare componentă a bibliotecii.

Lista de parametrii, aşa cum arată experienţa multor ani de programare, este alcătuită din trei liste şi anume:

• lista parametrilor ce corespund datelor de intrare; aceşti parametrii apar în membrul drept al unei expresii aritmetice; este indicat să nu se opereze modificări asupra acestor variabile pentru a nu schimba premisele altor subprograme care au nevoie de acele variabile cu valorile lor iniţiale;

• lista rezultatelor, conţine numele parametrilor care apar în membrul stâng al unei expresii de atribuire;

• lista variabilelor de stare care conţin coduri ce privesc cazurile de excepţie, codurile mesajelor de eroare care vor fi afişate; se construieşte o listă de coduri care se asociază unei variabile de stare, în toate subprogramele fiind atribuite numai valori în lista de coduri; de exemplu, variabila ik este o variabilă de stare; valorile pe care le ia aceasta sunt date în tabelul 4.1.

Valori asociate stărilor unui subprogram Tabelul 4.1

Valoare Semnificaţie 0 Prelucrare completă care a condus la rezultate corecte 1 Întreruperea prelucrărilor înainte de a efectua o împărţire la zero -1 Un masiv are număr de componente negativ -2 O variabilă are valori în afara unui interval impus -3 Argumentul unui logaritm sau al unui radical este negativ 2 Parametrul are o valoare în afara elementelor enumerate 3 Numele fişierului este necunoscut 4 Elementul cu cheia specificată nu există

83

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Dacă toate subprogramele sunt înzestrate cu variabila de stare ik în lista de parametrii, în programul apelator sau în subprograme se introduce o secvenţă de program obligatorie de testare a ei. Continuarea prelucrării se realizează dacă şi numai dacă, variabila ik are la ieşirea din procedură valoarea zero, aşa cum se arată în figura 4.2.

Figura 4.2 Flux cu testarea variabilei de stare

84

Programarea standard

Listele de parametrii pentru subprogramele dintr-o bibliotecă sunt extrase dintr-o listă comună de nume, utilizatorii bibliotecii trebuind obligatoriu să cunoască această listă comună de nume. Mai mult, pentru subprograme care au prelucrări ce se referă la aceleaşi variabile, se impune ca listele de parametrii să fie identice. De exemplu, în tabelul 4.2 sunt date proceduri care au liste de parametrii identice ca structură şi ca poziţie a elementelor şi ca nume pentru parametri.

Proceduri cu liste identice de parametri Tabelul 4.2

Prototipul procedurii Semnificaţia addmat (a, b, m, n, c) adună matricele a, b, rezultatul este matricea c

scadmat (a, b, m, n, c) evaluează expresia c = a – b, unde a, b, c sunt matrice cu m linii şi n coloane

prodmat (a, b, m, n, c) calculează elementele unei matrice c după relaţia cij = aij * bij

compartmat (a, b, m, n, c) compară elementele a două matrice

cij = 0 dacă aij = bij

cij = 1 dacă aij > bij

cij = -1 dacă aij < bij

Programarea standard îşi dovedeşte eficienţa dacă subprogramele sunt bine construite, având un grad de omogenitate maxim, încât programatorii să se recunoască pe ei înşişi prin nivelul ridicat al performanţei încorporate în secvenţele de texte sursă. În cazul în care un programator identifică secvenţe care prin înlocuirea cu altele mult mai performante schimbă calitatea unei proceduri, încrederea în bibliotecă scade, programarea standard fiind periclitată.

4.5 Programul principal, programul apelator

Dorinţa oricărui programator este maximizarea gradului de reutilizare de componente. Programarea standard are ca obiectiv elaborarea de biblioteci de subprograme care să asigure maximizarea gradului de

85

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

reutilizare. Este important să fie cunoscute atât bibliotecile, cât şi componentele lor, adică subprogramele incluse în ele.

Dacă există o problemă P de rezolvat, în faza de analiză se stabilesc datele de intrare DI1, DI2, ...., DIn şi rezultatele care trebuie obţinute RI1, RI2, ...., RIm. În funcţie de volum, de tip, de modul de regrupare, datele de intrare şi rezultatele sunt puse în corespondenţă cu unele dintre structurile de date cunoscute. Rezultă automat tipologia de probleme în raport cu operanzii de prelucrat.

Dacă sunt luate în considerare următoarele structuri de date:

VE - variabile elementare

M1 - masiv unidimensional

M2 - masiv bidimensional

FS - fişier secvenţial

LS - listă simplă

LD - listă dublă

ST - stivă

AB - arbore binar

GR - graf

BB - arbore B

MR - matrice rară

modulele sau subprogramele, în faza de proiectare sunt cu un grad de omogenitate ridicat, în primul rând, dacă includ operanzi de acelaşi tip, în proporţie covârşitoare. De exemplu, în figura 4.3 e dată o structură de program apelator care include apel de subprograme cu grad maxim de omogenitate a structurilor de date utilizate.

86

Programarea standard

FS

Conversie()

LS

Calcul1()

LS

Calcul2()

LS

Calcul3()

LS

Calcul4()

LS

Conversie()

FS Figura 4.3 Program apelator omogen din punct de vedere

al structurilor de date

Stabilirea structurilor de date din lista de parametrii ai subprogramului determină care este biblioteca utilizabilă, care sunt subprogramele care se reutilizează de către programator pentru a-şi soluţiona problema. Problema P se descompune în subproblemele P1, P2, ....., Pr. Problemei P îi va corespunde programul apelator PA, figura 4.4. Fiecărei subprobleme îi corespunde apelarea unui subprogram SP1(), SP2(), ...SPk(). Descompunerea subproblemei Pi în alte subprobleme Pi1, Pi2, ....., Piki impune includerea în subprogramul SPi() a apelurilor de subprograme SPi1(), SPi2(), ....SPiki().

Programarea standard impune ca structura arborescentă să includă un număr de niveluri rezonabil, iar frunzele arborescenţei sunt componente ale unei biblioteci de subprograme. Rezultă că programatorul care stăpâneşte programarea standard, trebuie să dezvolte o structură arborescentă, trebuie să ştie care sunt componentele bibliotecii, tehnologia bottom-up fiind cea mai nimerită de a construi soluţia ca program a problemei. Astfel, pentru o problemă P dată în figura 4.4 se construieşte programul dat în figura 4.5.

87

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Figura 4.4 Structura problemei P

void main() { p_1(); p_2(); p_3(); } p_1() { p_1_1(); p_1_2(); p_1_3(); } p_2() { p_2_1(); p_2_2(); p_2_3(); p_2_4(); } p_3() { p_3_1(); p_3_2(); p_3_3(); }

Figura 4.5 Program asociat structurii arborescente P

Toate componentele de pe nivelul al doilea sunt subprograme de bibliotecă. Programarea standard este programarea reutilizării de subprograme. Activitatea de programare este reorientată spre a dezvolta subprograme reutilizabile, iar comunicarea între programatori este esenţială. În momentul în care activitatea unui programator este analizată calculând raportul dintre secvenţele originale de instrucţiuni şi lungimea programului, toată problematica efortului de programare, se rezolvă de la sine. Secvenţele originale se obţin dintr-un program, după ce se elimină toate secvenţele pentru care există subprograme de bibliotecă. La un moment dat, programarea standard s-a constituit în factor progres, impunând o componentă de bază în ingineria software, partea dedicată reutilizării de subprograme.

88

5.1 Reguli de bază

Programarea structurată, conform [MIHAL83], constă dintr-o mulţime de restricţii şi reguli de programare pe care programatorul trebuie să le respecte, în acest fel, eliminându-se mulţi dintre factorii care conduc la erori şi care complică problemele de testare şi de întreţinere.

Se identifică trei structuri de control şi anume:

• structura liniară: se consideră instrucţiunile I1, I2, ..., Iin care se execută una după cealaltă aşa cum arată arcele orientate din figura 5.1:

Figura 5.1 Structură liniară

într-un program, structura liniară apare sub forma unor instrucţiuni dispuse una după cealaltă; de exemplu secvenţa:

s = 0; i++; s = x[i] + x[i] * x[i];

formează o structură liniară;

• structura alternativă, care presupune o expresie condiţională C1 şi două secvenţe S1 şi S2; dacă, după evaluarea expresiei condiţionale C1, se obţine valoarea logică adevărat, se execută secvenţa S1, în caz contrar se execută secvenţa S2, figura 5.2:

89

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Figura 5.2 Structură alternativă

De exemplu, pentru evaluarea expresiei

⎪⎩

⎪⎨⎧

≤+

>+=

b a daca ,ab a daca ,

32

22

bba

e

se realizează schema logică din figura 5.3:

Figura 5.3 Structură alternativă pentru evaluarea expresiei E

90

Abordarea structurată

• structura repetitivă sau de ciclare, care presupune o expresie condiţională C şi o secvenţă S de program care se execută de un număr finit de ori, până când condiţia îşi schimbă valoarea. O astfel de structură are una din formele:

repetitivă cu test iniţial, în care secvenţa S se repetă atâta timp cât condiţia C este adevărată, apoi se execută secvenţa S`;

Figura 5.4 Structură repetitivă cu test iniţial

repetitivă cu test final, în care se execută secvenţa S, până când condiţia C devine adevărată, apoi se execută secvenţa S`;

Figura 5.5 Structură repetitivă cu test final

repetitivă cu contor, care presupune existenţa unei variabile de control i, a unei valori iniţiale a acesteia, vinit, a unei valori finale vfin si a unei raţii r, precum şi o secvenţă S care se execută, iar variabilele incluse în ea depind de variabila de control i; schema logică corespunzătoare este prezentată în figura 5.6;

91

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Figura 5.6 Structură de control având repetări contorizate

Pentru problema de determinare a numărului de elemente pozitive, negative şi respectiv nule dintr-o matrice, prezentată în capitolul al doilea, schema logică corespunzătoare utilizării în exclusivitate a celor trei tipuri de structuri este dată în figura 5.7.

92

Abordarea structurată

Figura 5.7 Schema logică pentru numărarea elementelor negative, pozitive şi

nule

Programarea structurată determină o ordonare a dezvoltării succesiunii operaţiilor, obligându-i pe programatori să realizeze construcţii cât mai clare.

93

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

5.2 Programarea fără GOTO

Introducerea în limbajele de programare a instrucţiunii GOTO are menirea de a dezvolta programe cu arce orientate care se întretaie, figura 5.8.

Figura 5.8 Graf asociat unui program cu salturi înapoi (A), cu salturi înainte

(B) şi cu salturi înapoi şi înainte (C) întretăiate

O utilizare frecventă a salturilor necondiţionate este pentru implementarea structurilor repetitive, în lipsa unor construcţii oferite de limbajul de programare. De asemenea, o altă utilizare apare în situaţia în care se doreşte creşterea vitezei de execuţie. Secvenţele de cod cu salturi necondiţionate sunt mai rapide, însă mai greu controlabile sub aspectul fluxului de execuţie. Limbajele de nivel înalt doar includ printre cuvintele lor rezervate această instrucţiune, însă nu furnizează nici o implementare. Salturile necondiţionate rămân apanajul limbajelor de asamblare şi ale limbajelor de nivel mediu (C/C++), însă şi la acestea din urmă, utilizarea lor nu este recomandată, preferându-se abordările structurate.

Programarea structurată este o tehnică în care programatorul nu trebuie să folosească instrucţiunea de salt GOTO. Cele trei tipuri fundamentale de structuri permit implementarea acestei cerinţe. Dacă se consideră limbajul de asamblare şi se analizează procesul de compilare, se observă că instrucţiunea de salt necondiţionat – jmp este inevitabilă.

94

Abordarea structurată

Structurii alternative din figura 5.2 i se asociază secvenţa din figura 5.9.

Figura 5.9 Construcţia secvenţială asociată structurii alternative

pentru limbajul de asamblare

Implementarea unei structuri alternative presupune operaţii anterioare testării indicatorilor de condiţie, iar în cazul unui rezultat negativ al evaluării se execută un salt necondiţionat la secvenţa ce trebuie executată în acest caz (corespondentul într-un limbaj de programare evoluat este varianta else a structurii if).

Şi în cazul structurii repetitive, instrucţiunea jmp este inevitabilă, figura 5.10.

95

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Figura 5.10 Construcţie secvenţială asociată structurii repetitive

pentru limbajul de asamblare

Limbajele de programare evoluate includ apeluri de funcţii de bibliotecă. De exemplu, în programul din figura 5.11, programul principal PROG1 apelează subprogramele calcul1, calcul2 şi calcul3.

96

Abordarea structurată

Figura 5.11 Instrucţiuni de salt necondiţionat la apeluri

şi reveniri la lucru cu subprograme

Instrucţiunile call şi ret conţin şi salturi necondiţionate care definesc modificări ale registrului IP cu valori mai mari de 6 baiţi. Rezultă că programarea fără GOTO este un deziderat şi limbajele evoluate reuşesc să mascheze apariţiile de bază ale salturilor necondiţionate.

5.3 Conversii de programe

Din punct de vedere al teoremei de structură, a lui Böhm-Jacopini, un program structurat este unul care are numai structuri de tip secvenţial, if-then-else şi repetitive condiţionate anterior. Variantele de structuri de tip if-then sau repetitive condiţionate posterior trebuie transformate în structurile menţionate anterior, numite şi structuri fundamentale. Principala metodă de transformare o constituie duplicarea de cod. De exemplu, se consideră structura if-then următoare:

97

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Figura 5.12 Structură alternativă de tip if-then

Pentru a o transforma într-o structură în care să se regăsească doar structuri fundamentale, se duplică codul, obţinându-se următoarea schemă:

Figura 5.13 Structură if-then rescrisă folosind doar structuri fundamentale

În practică însă nu se optează pentru scrierea structurii de tip if-then folosind doar structuri fundamentale, pentru că, aşa cum se observă în figură, cantitatea de cod duplicată este semnificativă, iar introducerea unei

98

Abordarea structurată noi testări a condiţiei C1 pe lângă faptul că afectează performanţa codului (este ştiut faptul că operaţiile de comparare sunt costisitoare, din punct de vedere ale procesorului), afectează şi lizibilitatea algoritmului.

În cazul structurii repetitive condiţionate posterior, se realizează tot o duplicare de cod, însă impactul asupra performanţei şi lizibilităţii codului este mult mai mic. Considerându-se schema din figura 5.5, ea se rescrie folosind doar structuri fundamentale în figura 5.14:

Figura 5.14 Structura repetitivă rescrisă folosind doar structuri fundamentale

Se observă că se duplică doar blocul funcţional S, ceea ce nu afectează deloc performanţa codului şi într-o măsură foarte mică lizibilitatea lui. Probleme sunt atunci când blocul S trebuie modificat, fiindcă modificarea trebuie realizată în două locuri.

Atunci când un programator este format să lucreze conform cerinţelor programării structurate, construirea algoritmilor, construirea secvenţelor de blocuri în schema logică se realizează din start pentru cele trei tipuri de structuri fundamentale, nefiind necesară conversia. În cazul în care programatorul are la dispoziţie software mai vechi în care apare frecvent instrucţiunea GOTO trebuie să realizeze conversia de programe, adică să dezvolte astfel de construcţii încât să fie eliminată instrucţiunea GOTO din program.

Structurile de program ce conţin instrucţiuni GOTO sunt greu de analizat, modificat, restructurat şi testat. De aici şi nevoia de a transforma aceste structuri de program în unele din care instrucţiunile GOTO să lipsească.

99

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Pentru transformarea programelor ce conţin instrucţiuni GOTO în programe structurate, cea mai utilizată tehnică este cea a introducerii unei variabile de control. Se consideră următoare structură posibilă în cadrul unei aplicaţii:

C1

S1

Da

S2

C2Da

e

C3

S3

Da

Figura 5.15 Exemplu de structură de program

în care se folosesc instrucţiuni GOTO

Pentru structurarea secvenţei de program din figura 5.15 se introduce variabila de control vb de tip boolean, care va fi iniţializată cu valoarea true. Schema rezultată este prezentată în figura 5.16:

100

Abordarea structurată

Figura 5.16 Schema structurată folosind variabila de control vb

Nu toate programele nestructurate au corespondent sub formă de program structurat.

Se observă că există situaţii, cum este cazul structurilor de tip if-then sau repetitive condiţionate posterior, în care rescrierea lor folosind doar structuri fundamentale nu îşi are rostul. Este şi motivul pentru care toate limbajele de programare acceptă construcţii de acest fel, existând chiar cuvinte cheie speciale, de exemplu, do-while. Nu acelaşi lucru se spune despre secvenţele ce folosesc instrucţiuni GOTO, care trebuiesc eliminate din cod.

Pentru programele deja scrise folosind GOTO, transformarea lor manuală reprezintă un efort semnificativ, motiv pentru care au fost

101

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

dezvoltate o serie de instrumente software cu ajutorul cărora această transformare este automatizată.

5.4 Limbaje de programare structurate

Limbajele PASCAL, C, C++ şi toate care urmează acestora sunt proiectate în vederea implementării fără nici o rezervă a structurilor de control fundamentale. Având în vedere că limbajul C++ este utilizat pentru toate exemplificările, pentru implementarea structurii alternative din figura 5.2 se realizează secvenţa:

if ( C1 ) { S1; } else { S2; }

Pentru structura repetitivă condiţionată anterior în limbajul C++ există construcţia:

while ( condiţie ) { S; }

Structura repetitivă condiţionată posterior este implementată în C++ prin:

do { S; } while ( condiţie );

Pentru structura repetitivă cu contor se realizează secvenţa: initializari; for ( i = vinit; conditie; i += r ) { S; }

Structura liniară se implementează printr-o structură de forma: I1; I2; ...... Iin;

102

Abordarea structurată

Rezultă că pentru schema logică dată în figura 5.7 textul sursă C++ este dat în figura 5.17. #include <stdio.h> #define M 10 #define N 10 int m, n, i, j, a[M][N]; int main() { printf("\nIntroducetie numarul de linii: "); scanf("%d", &m); printf("\nIntroduceti numarul de coloane: "); scanf("%d", &n); for ( i = 0; i < m; i++ ) { for ( j = 0; j < n; j++ ) { printf("\na[%d][%d] = ", i, j); scanf("%d", &a[i][j]); } } int nrplus = 0; int nminus = 0; int nrzero = 0; for ( i = 0;i < m; i++ ) { for ( j = 0; j < n; j++ ) { if ( a[i][j] == 0 ) nrzero++; if ( a[i][j] > 0 ) nrplus++; if ( a[i][j] < 0 ) nrminus++; } } printf("\nNumarul de elemente pozitive este %d",nrplus); printf("\nNumarul de elemente negative este%d",nrminus); printf("\nNumarul de elemente nule este %d\n", nrzero); return 1; }

Figura 5.17 Codul sursă în C++ pe baza schemei logice din figura 5.7

Aplicând metricile de complexitate definite în capitolul Ciclul de dezvoltare software, se obţine următoarele valori pentru complexitatea ciclomatică:

CM =35 – 25 + 2 = 12

103

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

şi pentru metricile Halstead: Metrică Valoare

Lungimea programului = 69Vocabularul programului = 14Volumul = 421,49Dificultatea = 15,38Efort = 6480,38

Analizând variaţia complexităţii faţă de abordarea clasică, unde implementarea a fost tot monolitică, se observă o creştere a complexităţii pe fondul diminuării numărului de noduri din graf. Aceasta deoarece au fost folosite structuri repetitive ale limbajului şi elaborate de programator. Prin urmare, mai multe instrucţiuni din programarea clasică au fost grupate într-una singură. Acest lucru însă nu a diminuat fluxul de execuţie care rămâne acelaşi, dovadă numărul aproape identic de arce.

Metricile Halstead nu diferă foarte mult de valorile înregistrate pentru abordarea clasică, lucru normal, având în vedere se efectuează aceleaşi operaţii logice, pe aceiaşi parametrii.

Comparând cu abordarea standard, lucrurile stau total diferit. Datorită organizării mult mai riguroase a codului, în librării de subprograme, complexitatea programului scade foarte mult, el devenind o succesiune de apeluri de funcţii din librărie. În situaţia în care se iau în considerare şi complexităţile funcţiilor se observă o apropiere a valorilor, în condiţiile în care nu s-a mers decât pe primul nivel de derivare, în cadrul bibliotecii de subprograme. Dacă însă se realizează o implementare şi în variantă structurată, cu ajutorul mai multor subprograme, se constată o creştere a complexităţii. Se construieşte un subprogram care realizează citirea elementelor matricei de la tastatură şi un subprogram care numără elementele din matrice pe baza rezultatului returnat de o funcţie transmisă ca parametru subprogramului de numărare. Această funcţie presupune trei implementări pentru comparaţiile de tip „mai mare”, „egal” şi „mai mic”. Rezultatele obţinute pentru metricile de complexitate devin în acest caz:

CM =15

respectiv: Metrică Valoare

Lungimea programului = 83Vocabularul programului = 40Volumul = 356,5Dificultatea = 20,08Efort = 2116,73

104

Abordarea structurată

Trebuie menţionat că evaluarea a fost făcută prin cumularea valorilor obţinute şi la nivelul subprogramelor, la fel ca în cazul programării standard.

Rezultă că, în general, introducerea de subprograme creşte complexitatea programului. Avantajul principal este legat de reutilizare. În plus, dacă subprogramele nu sunt dezvoltate în cadrul aceluiaşi proiect, ele nu participă la calculul complexităţii, şi atunci valoarea acesteia este foarte mică.

Programarea structurată reprezintă o tehnică de programare care ajută la dezvoltarea de software cu un design şi un flux de execuţie clare şi prin care se asigură un grad ridicat de modularitate a produsului. Eliminarea instrucţiunii GOTO din programe folosind diverse metode, unele prezentate şi în cadrul acestui capitol, ajută la eliminarea unor categorii de erori de programare foarte grave şi la îmbunătăţirea calităţii codului. Trebuie menţionat că această tehnică de programare este fundamentată ştiinţific, prin teoremele de structură, spre deosebire de alte tehnici anterioare ei, care nu posedă un astfel de fundament.

105

6.1 Abordarea bottom-up

Tehnica bottom-up presupune construirea la început a unor elemente primitive, de bază, apoi combinarea lor în elemente mai complexe, dar şi adăugarea de noi elemente, până când se obţine produsul final. În programarea aceasta înseamnă construirea iniţial a unor funcţii primitive, ce implementează elemente atomice de funcţionalitate, apoi utilizarea lor în construirea de funcţii mai complexe. Rezultatul este o structură arborescentă care are ca rădăcină produsul software final.

Se consideră o problemă P definită prin date de intrare – DI, date de ieşire – DE, şi algoritmi de calcul pe baza cărora se obţin datele de ieşire. Pornind de la rezultate, se definesc din aproape în aproape prelucrări şi se ajunge în final la problema P în integralitatea ei. Datele iniţiale sunt transformate în date finale după un număr finit de prelucrări care generează şi date intermediare. Pornind de la datele de ieşire DEi

j, care pot fi cele finale sau unele intermediare, se asociază o prelucrare sau un set de prelucrări cu un modul Mi

j al produsului software, care permite obţinerea lor pe baza unor date de intrare intermediare, DIi

j figura 6.1.

Figura 6.1 Fluxul corespunzător unui modul

106

Programarea modulară

În notaţia din figura 6.1, indicele inferior arată numărul modulului, iar indicele superior arată nivelul pe care se află modulul în structura arborescentă asociată produsului software. Datele de ieşire ale unui modul de pe nivelul j, reprezintă datele de intrare pentru unul sau mai multe module de pe nivelul j-1.

Proiectantul sistemului software abordează întreaga problematică de la elementele concrete spre elementul de sinteză, care se află la nivelul superior. Se construieşte soluţia sub formă de structură arborescentă pornind de la ultimul nivel de descompunere – frunzele arborelui, către rădăcină. Asamblarea se efectuează de la nivelul j+1 spre nivelul j, continuându-se de la nivelul j spre nivelul j–1 până la nivelul 0 ce corespunde rădăcinii. O astfel de abordare permite construcţia produsului pornind de la funcţiile primitive, specifice domeniului problemei, spre funcţiile complexe ce implementează logica aplicaţiei; tehnica este potrivită pentru situaţii în care specificaţiile nu sunt clare, şi de asemenea, asigură o mai bună organizare a codului sursă.

Modulele de pe nivelurile inferioare sunt asamblate în unul sau mai multe module de pe nivelurile superioare. De exemplu, un set de funcţii pentru accesul la fişiere este folosit atât într-un modul de salvare a datelor manipulate de aplicaţia software, cât şi într-un modul de prelucrare care necesită lucrul cu fişiere temporare.

M1p M2

p M3p Mn

p

M1p-1 M2

p-1 Mmp

M11 M2

1 M31

P=M10

………………………………………………………..

…………...

…………...

Figura 6.2 Asamblare bottom-up în sensul de la frunze spre rădăcină

107

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

De exemplu, pentru problema de calcul matriceal aleasă pentru ilustrarea particularităţilor tehnicilor de programare, soluţia prin tehnologia bottom-up presupune:

• cunoaşterea exactă a rezultatelor obţinute: numărul componentelor negative, numărul componentelor pozitive şi numărul componentelor nule;

• cunoaşterea exactă a condiţiilor pe care matricea trebuie să le îndeplinească: matricea este simetrică.

Printr-o abordare bottom-up, programatorul identifică, în primul rând, funcţiile primitive: citirea informaţiilor de la tastatură, afişarea informaţiilor pe ecran, compararea a două numere pentru care rezultatul are valoarea adevărat sau fals. Aceste funcţii se vor găsi pe nivelul de bază a structurii. Pornind de la aceste primitive, se construiesc pe nivelul următor, modulul de citire a dimensiunilor şi elementelor matricei, modulul de afişare a rezultatelor şi a unor mesaje de informare sau de eroare dintr-o listă asociată. Pe nivelul al treilea de la bază, apar modulele de stabilire a simetriei matricei, de determinare a minimului şi a maximului, precum şi de numărare a elementelor pozitive, negative şi nule din matrice. Aceste module se bazează pe funcţiile de comparare a două numere, de afişare de rezultate şi afişare de mesaje. Pe nivelul rădăcină, se apelează aceste module conform fluxului cerut de problemă. Înseamnă că structura arborescentă are 4 niveluri, (figura 6.3).

Figura 6.3 Dezvoltarea bottom-up pentru problema PROB

108

Programarea modulară

Se impun următoarele observaţii referitoare la figura 6.3: un modul de pe un nivel inferior, cum este cazul modului de

„Afişare mesaje din lista de mesaje” să fie implicat în construirea mai multor module de pe nivelurile superioare;

un modul de pe un nivel inferior, cum este cazul modulului „Compararea a două numere” participă la construcţia unui alt modul care nu se află neapărat pe următorul nivel în ierarhie.

Rezultă că, structura nu este arborescentă în proporţie de 100%, ea permiţând ca un nod copil să aibă mai mult de un nod părinte. Însă caracterul ierarhic se menţine, prin faptul că modulele sunt dezvoltate începând cu cele primitive, de bază, şi terminând cu cele complexe, respectiv cu produsul final ca ultim modul, iar acestea din urmă au în construcţia lor apeluri către modulele de bază.

Se construieşte tabelul 6.1 care evidenţiază faptul că datele de intrare ale unui modul de pe nivelul k sunt ieşiri ale modulului de pe nivelul k – 1.

Corelaţia nivel – mod de utilizare variabile Tabelul 6.1

Nivel Modul c1 c2 c3 c4 c5 c6 c7 3 citire informaţii 3 afişare informaţii 3 comparare numere E I 2 citire matrice E E 2 afişare rezultate I I I I 2 afişare mesaje I 1 contorizare E E E I I 1 maxim/minim I I E 1 simetrie E I I 0 main() I/E I/E I/E I/E I/E I/E I/E

Semnificaţia coloanelor c1 – c7 este prezentată mai jos.

c1 - nrplus c2 - nrnegativ c3 - nrnull c4 - text c5 - matricea c6 - dimensiuni matrice c7 - max/min

109

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Avantajul acestei abordări este dat de faptul că programatorul rezolvă părţi ale problemei, oferă rezultate intermediare şi pe măsură ce poate asambla module, obţine în final produsul finit. Dezvoltarea bottom-up corespunde proceselor industriale care realizează repere, prin asamblare se obţin subansambluri, iar la cel mai înalt nivel se obţine produsul finit.

6.2 Abordarea top-down

Abordarea top-down presupune divizarea problemei mari în subprobleme mai mici care sunt tratate separat până la nivelul unor rutine sau module primitive. Proiectarea unui program modular, prin această tehnică se face astfel: modulul de nivel superior specifică ce niveluri descendente ale sale sunt necesare şi precizează ce date se transmit şi ce rezultate se aşteaptă de la ele. Modulele terminale se vor identifica şi implementa, deci, ultimele. Testarea programului se face în aceeaşi abordare: mai precis, proiectarea, programarea şi testarea modulului descendent se vor realiza împreună. Dacă modulul părinte a fost pus la punct atunci, pe parcursul implementării modulului descendent, eventualele erori vor apărea numai în acesta din urmă. Programatorul se va putea, deci, concentra numai pe realizarea acestui modul, munca lui devenind mult mai eficientă.

Problema P a afişării numărului de elemente pozitive, negative, nule, a maximului şi minimului unei matrice simetrice, este privită, ca orice aplicaţie informatică, ca având o structură arborescentă pe două niveluri, figura 6.4.

Figura 6.4 Structura arborescentă top-down pe două niveluri

• SP1 reprezintă subproblema corespunzătoare iniţializării datelor;

• SP2 reprezintă subproblema de prelucrare a datelor;

• SP3 reprezintă subproblema de afişare a rezultatelor.

110

Programarea modulară

Dezvoltarea top-down presupune efectuarea unor detalieri pentru fiecare subproblemă, după cum urmează:

• S-SP11 corespunde iniţializării dimensiunilor matricei, care este subproblemă a lui SP1;

• S-SP12 corespunde iniţializării matricei, care este subproblemă a lui SP1;

• S-SP13 corespunde alegerii minimului dintre numărul de linii şi numărul de coloane ale matricei pentru a stabili dimensiunea matricei pentru care se analizează simetria.

Structura arborescentă asociată subproblemei SP1 este dată în figura 6.5.

Figura 6.5 Descompunerea pe subprobleme a funcţiei de iniţializare

Pentru efectuarea prelucrărilor, subproblema SP2 este descompusă în subprobleme după cum urmează:

• S-SP21 corespunde testării caracterului simetric după diagonala principală;

• S-SP22 corespunde testării caracterului simetric după diagonala secundară;

• S-SP23 corespunde testării simetriei după coloana din mijloc;

• S-SP24 corespunde testării simetriei după linia din mijloc;

• S-SP25 corespunde aflării elementului minim din matricea simetrică;

• S-SP26 corespunde aflării elementului maxim din matricea simetrică

• S-SP27 corespunde contorizării.

111

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Structura arborescentă este dată în figura 6.6.

Figura 6.6 Descompunerea pe subprobleme a funcţiei de calcul

Afişarea rezultatelor impune descompunerea subproblemei SP3 astfel:

• S-SP31 corespunde afişării elementului minim;

• S-SP32 corespunde afişării elementului maxim;

• S-SP33 corespunde afişării numărului de elemente nule, pozitive, respectiv negative;

• S-SP34 corespunde afişării de mesaje.

Descompunerea acestei funcţii este dată în figura 6.7.

SP3

S-SP31 S-SP32 S-SP33 S-SP34

Figura 6.7 Descompunerea pe subprobleme a funcţiei de afişare

Această descompunere corespunde situaţiei în care problema este foarte bine înţeleasă, experienţa analiştilor şi designerilor permit asocierea unei structuri arborescente clare, iar duplicarea de cod este controlată.

Pentru această abordare se procedează astfel:

• se scrie programul apelator SP0, definind variabilele de bază şi apelurile la procedurile corespunzătoare lui SP1, SP2, SP3; în faza de analiză sunt clarificate numărul de niveluri, inputurile corespunzătoare fiecărui nivel;

112

Programarea modulară

• se scriu procedurile SP1, SP2, SP3, în fiecare dintre ele, incluzându-se apelurile de proceduri de tipul S-SPij, i = 1,2,3 j = 1,2, ...., ni;

• se scriu procedurile corespunzătoare frunzelor structurii arborescente.

Produsul se dezvoltă de la întreg spre parte, ceea ce conferă designerului resurse suficiente pentru a obţine un produs complet, prin adăugări de proceduri la fiecare nivel, dacă este cazul.

6.3 Raportul subprograme – modul

Conceptul de modul este foarte cuprinzător, fiind asociat unei prelucrări cu un grad de complexitate acceptabil, care să-i ofere un nivel de autonomie ridicat. De exemplu, există modulul de validare date, modul de sortare date, modul de calcul matriceal, modul de actualizare fişier, modul de preluare inputuri, modul de afişare, modul de conversie etc. Fiecare modul este format din unul sau mai multe subprograme. Într-un caz particular, un modul este format dintr-un singur subprogram. În realitate, un modul are în structura sa mai multe subprograme. De exemplu, modulul de validare a datelor include:

• validarea câmpului alfabetic; • validarea unui câmp dacă este număr întreg; • validarea apartenenţei numărului întreg la un interval; • validarea apartenenţei unui şir la o mulţime de subşiruri

descrisă explicit; • validarea numerelor reale; • validarea apartenenţei unui număr real la un interval; • validarea apartenenţei unui număr la o mulţime dată prin

elemente. Modulul pentru iniţializarea unui masiv unidimensional include

subprograme pentru:

• iniţializarea de la tastatură; • iniţializarea dintr-un fişier; • iniţializarea prin atribuire; • iniţializarea prin rezultatul evaluării unei expresii.

113

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Complexitatea unui modul este rezultatul analizei contextului şi filosofiei adoptate de către echipa de proiectanţi software. Dacă se urmăreşte un nivel de complexitate ridicat pentru module, structura arborescentă se organizează pe un număr scăzut de niveluri. Modulele complexe se află pe acelaşi nivel, funcţie de raportul cu părinţii, respectiv descendenţii săi.

Dacă se adoptă ipoteza ca modulele să aibă un nivel de complexitate redus, structura arborescentă are un număr de niveluri ridicat. În acest context, pentru problema de calcul matriceal, în ipoteza un modul = un subprogram, lista subprogramelor este prezentată în continuare:

• citire_dim() - citeşte dimensiunile matricei de la tastatură; • citire_mat() - citeşte elementele matricei de la tastatură; • simetrie1() – stabileşte simetria matricei faţă de diagonala

principală; • simetrie2() – stabileşte simetria matricei faţă de diagonala

secundară; • contorplus() – numără elementele pozitive dintr-o matrice; • contorminus() – numără elementele negative dintr-o matrice; • contornul() – numără elementele nule ale unei matrice; • minim() – identifică minimul elementelor unei matrice; • maxim() – identifică maximul elementelor unei matrice; • afiseaza() – afişează rezultatele prelucrărilor; • mesaj() – afişează mesaje specifice ale aplicaţiei.

Dacă se procedează la regruparea într-un modul a mai multor subprograme, structura pentru aceeaşi problemă este dată în figura 6.9.

Figura 6.9 Structura aplicaţiei, după gruparea mai multor subprograme într-un singur modul

114

Programarea modulară

Se urmăreşte realizarea unui echilibru între numărul de module şi dimensiunea aplicaţiei. O aplicaţie simplă, dar cu un număr mare de module nu face decât să crească complexitate. Pe de altă parte, o aplicaţie complexă nu foarte bine delimitată pe module, afectează la rândul ei complexitatea, prin faptul că influenţează alte caracteristici de calitate cum sunt mentenabilitatea sau reutilizabilitatea.

Modulele din figura 6.9, sunt transpuse în codul sursă, în limbajul C++, sub forma unor perechi de fişiere cu extensiile .h respectiv .cpp. Astfel, există citire.h şi citire.cpp, validare.h şi validare.cpp, prelucrare.h şi prelucrare.cpp, respectiv afisare.h şi afişare.cpp. Codul sursă pentru operaţiile de citire este dat în figura 6.10 (citire.cpp).

void citire_dimensiuni(int *m, int *n) { printf("Introduceti numarul de linii ale matricei = "); scanf("%d", m); printf("\nIntroduceti numarul de coloane ale matricei = "); scanf("%d", n); } void citire_mat(int a[][N], int m, int n) { for (int i = 0; i < m; i++ ) { for (int j = 0; j < n; j++) { printf("a[%d][%d] = ", i, j); scanf("%d", &a[i][j]); } } }

Figura 6.10 Codul sursă al modulului de citire

Modulul de validare are codul sursă din figura 6.11 (validare.cpp). int simetrie1( int a[][N], int m, int n) { int result = 1; for (int i = 0; result && i < m; i++ ) { for (int j = 0; result && j < n; j++ ) { if ( a[i][j] != a[j][i] ) { result = 0; } } } return result; } int simetrie2( int a[][N], int m, int n) { int result = 1; for (int i = 0; result && i < m; i++ ) {

115

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

for (int j = 0; result && j < n; j++ ) { if ( a[i][j] != a[m-i-1][n-j-1] ) { result = 0; } } } return result; }

Figura 6.11 Codul sursă al modulului de testare a simetriei

Pentru modulul de prelucrări, implementare în C++ este dată în figura 6.12 (prelucrare.cpp).

int contor(int a[][N], int m, int n, int prag,

int(*func)(int, int)) { int contor = 0; for ( int i = 0; i < m; i++ ) { for ( int j = 0; j < n; j++ ) { if ( func(a[i][j],prag) > 0) { contor++; } } } return contor; } int minim (int a[][N], int m, int n) { int min = a[0][0]; for ( int i = 0; i < m; i++ ) { for ( int j = 0; j < n; j++ ) { if ( min > a[i][j] ) min = a[i][j]; } } return min; } int maxim (int a[][N], int m, int n) { int max = a[0][0]; for ( int i = 0; i < m; i++ ) { for ( int j = 0; j < n; j++ ) { if ( max < a[i][j] ) max = a[i][j]; } } return max; }

Figura 6.12 Codul sursă pentru modulul de prelucrări

116

Programarea modulară

În final, codul sursă pentru modulul de afişare mesaje şi rezultate este dat în figura 6.13 (afisare.cpp)

void afiseaza(char* mesaj, int result) { printf("%s %d\n", mesaj, result); } void mesaj(int cod) { switch(cod) { case 1: printf("Matricea este simetrica!\n"); break; case 2: printf("Matricea nu est simetrica!\n"); break; } }

Figura 6.13 Codul sursă pentru modulul de afişare

În aceste condiţii, programul care determină numărul de elemente pozitive, negative şi nule din cadrul unei matrice citită de la tastatură, şi care este scris folosind modulele descrise anterior, are următorul cod sursă:

int maimare(int a, int b) { return a > b; } int maimic(int a, int b) { return a < b; }

int egal( int a, int b) { return a == b; }

int main() { int a[M][N], m, n;

citire_dimensiuni(&m, &n); citire_mat(a, m, n);

//numara elementele pozitive, negative si nule afiseaza("Numarul de elemente pozitive este",

contor(a, m, n, 0, maimare)); afiseaza("Numarul de elemente negative este",

contor(a, m, n, 0, maimic)); afiseaza("Numarul de elemente nule este",

contor(a, m, n, 0, egal));

return 1; }

117

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Cele trei funcţii maimare, maimic, egal sunt transmise ca parametrii funcţiei de contorizare, în aşa fel încât aceasta din urmă capătă un grad ridicat de generalitate, putând număra orice tip de element din matrice care trebuie să respecte o condiţie, condiţie stabilită prin funcţia transmisă ca parametru.

Programarea modulară se bazează tot pe filozofia programării structurate. Aceasta din urmă permite divizarea programelor în module ce sunt implementate şi testate pe calculator în mod independent. Abordarea modulară, fie ca este bottom-up sau top-down, nu aduce un plus de complexitate, ci doar reorganizează acelaşi cod sursă rezultat din programarea structurată. Prin urmare, pentru exemplul considerat, valorile complexităţii obţinute la abordarea structurată, pentru cazul folosirii de funcţii în implementarea soluţiei problemei şi nu în abordarea tip monolit, rămân valabile şi pentru programarea modulară. Există o componentă a complexităţii care se modifică, şi anume, cea cognitivă, pentru că, prin descompunerea programului în module, cu funcţionalităţi clare, bine delimitate, creşte inteligibilitatea codului şi prin urmare el devine mai uşor de înţeles de către programator, chiar dacă, în esenţă este acelaşi cod. Această componentă însă nu se reflectă în nici o măsură cantitativă a complexităţii.

6.4 Parametrizarea

La dezvoltarea programelor trebuie să se ia în considerare modificări ce vor apare în timp, la nivelul:

• datelor de intrare, în sensul introducerii de câmpuri, modificării de domeniu, creşterii dimensiunii şi volumului de date, schimbării criteriilor de căutare;

• formulelor de calcul, prin adăugarea, eliminarea şi înlocuirea de operatori şi de operanzi, prin adăugarea şi eliminarea de formule;

• rezultatelor, prin adăugarea de noi rezultate care trebuie obţinute, prin schimbarea conţinutului unor rezultate existente şi prin eliminarea unora dintre acestea.

Dacă structura produsului software este gândită rigid, orice modificare la nivel conceptual se reflectă prin succesiuni de modificări în textul sursă, modificări caracterizate prin efecte de antrenare multiplă, imprevizibile. De

118

Programarea modulară aceea, trebuie ca programele să fie concepute astfel încât să preia toate modificările prin introducerea unor parametri de către utilizatorul produsului software. De exemplu, în matricea pentru care se efectuează prelucrări, trebuie şterse unele linii, respectiv, unele coloane. Dacă programul este construit rigid, trebuie definite două proceduri, una de ştergere linie din matrice şi alta de ştergere coloană, iar programatorul trebuie să le activeze în program, după ce s-a făcut citirea matricei iniţiale din fişier. O altă variantă constă în definirea încă de la începutul elaborării produsului software a doi vectori, unul pentru gestionarea liniilor, iar celălalt pentru gestionarea coloanelor, care vor avea valorile 1, dacă linia, respective coloana este activă şi 0 în caz contrar.

Dacă programul este conceput rigid, iar la un moment dat se impune ca operaţiile pe matrice să nu ia în considerare una din simetrii, trebuie ca subprogramele de testare a simetriei să fie scoase, necesitând operaţii în cascadă asupra produsului. Proiectarea flexibilă va impune un parametru pe care îl iniţializează utilizatorul cu 1 dacă se lucrează pe matrice simetrică şi 0 dacă se lucrează pe toată matricea.

În cazul unor evaluări de expresii, se introduc coeficienţi, care prin iniţializare cu -1 sau cu 1, vor determina evaluări de expresii diferite, iar alţi coeficienţi iniţializaţi cu 1 sau 0 vor extinde sau vor restrânge formulele. De exemplu, pentru evaluarea expresiilor:

E1 = a + b + c + d + h + g + p E2 = a + b - c + d - h + g - p E3 = a - b - c - d E4 = a + b + d + h + p E5 = a - b + c - d + h + g – p

se procedează astfel:

• variabilele a, b, c, d, h, g, p sunt memorate în componentele x[0], x[1], x[2], x[3], x[4], x[5], respectiv, x[6];

• se construieşte vectorul k[7], care se iniţializează cu +1, dacă termenii se adună sau cu -1, dacă termenii se scad;

• se construieşte vectorul s[7] care se iniţializează cu 1 dacă variabila participă la evaluarea expresiei, respectiv cu 0, în caz contrar;

119

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

• se construieşte vectorul e[5], în care se evaluează una din cele 6 expresii.

Utilizatorul produsului software nu are altceva de făcut decât să iniţializeze vectorii k[] şi s[]. Tabelul 6.2 conţine valorile celor doi vectori pentru cele cinci expresii.

Iniţializările variabilelor de structură ale expresiei Tabelul 6.2

a b c d h g p a b c d h g P k[0] k[1] k[2] k[3] k[4] k[5] k[6] s[0] s[1] s[2] s[3] s[4] s[5] s[6]

E1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1

E2 +1 +1 -1 +1 -1 +1 -1 +1 +1 +1 +1 +1 +1 +1

E3 +1 -1 -1 -1 +1 +1 +1 +1 +1 +1 +1 0 0 0

E4 +1 +1 +1 +1 +1 +1 +1 +1 +1 0 +1 +1 0 +1

E5 +1 -1 +1 -1 +1 +1 -1 +1 +1 +1 +1 +1 +1 +1

În cazul în care apar probleme de stabilire a apartenenţei la intervale şi prin program se definesc în instrucţiuni if limitele intervalelor, ori de câte ori se modifică limitele, se modifică numărul de intervale, trebuie operat în program. De exemplu, pentru evaluarea expresiei

⎪⎪

⎪⎪

−∈

<

=

restîn ,a7] (1, xdacă ,a1] 1,[ xdacă ,a

-1 xdacă ,

4

3

2

a

e

secvenţa de program este ……… if ( x < -1 ) { e = a; } else if ( x <= 1 ) { e = a * a; } else if ( x <= 7 ) { e = a * a * a; } else { e = a * a * a * a; } ............

120

Programarea modulară

Se acceptă ideea introducerii de la tastatură a numărului K de intervale, iniţializarea a doi vectori cu limitele inferioare şi limitele superioare ale K intervale. Se stabileşte o modalitate de preluare a expresiilor ce trebuie calculate. Ori de câte ori se modifică numărul de intervale K şi limitele intervalelor, utilizatorul modifică parametrii. Nu se intervine asupra textului sursă.

Programarea modulară presupune existenţa unor subprograme cu caracteristici de calitate excelente, destinate execuţiei unor operaţii de bază. Subprogramele acestea se încorporează în module, permiţând creşterea gradului de complexitate a prelucrărilor, menţinându-se nivelul ridicat al performanţei.

121

7.1 Proiectarea orientată obiect

Paradigma orientării pe obiecte are marele avantaj al unor instrumente mai bune de reprezentare a problemei, prin combinarea caracteristicilor şi a comportamentului unei entităţi într-o singură construcţie. De exemplu, pentru problema definită în capitolul Ciclul de dezvoltare software, dezvoltarea aplicaţiei software în manieră structurată, presupune:

• definirea unor variabile în cadrul programului, pentru a memora informaţiile cu privire dimensiunile matricei şi la elementele componente ale acesteia;

• definirea de funcţii cărora li se transmit aceste informaţii şi care realizează prelucrările specifice.

Dezavantajul acestei abordări rezidă în faptul că datele sunt gestionate în cadrul aplicaţiei separat de funcţionalitate. Aceasta înseamnă că o secţiune de cod în aplicaţie este legată strâns prin control şi transfer de informaţii, de multe alte secţiuni ale aplicaţiei. Aceste dependenţe apar când sunt folosite variabile globale, de exemplu. În abordarea orientată obiect, separarea nu mai există. Informaţiile sunt memorate în interiorul obiectului, fiind astfel protejate la accesul din exterior, iar funcţiile au acces direct la ele, fără a mai fi nevoie să fie transmise ca parametrii.

Datorită acestor avantaje au fost dezvoltate metodologii de analiză şi proiectare orientate obiect a aplicaţiilor software.

Proiectarea orientată obiect are în vedere identificarea şi separarea responsabilităţilor. Posibilitatea reutilizării codului rezidă tocmai în faptul că acel cod nu are elemente specifice pentru un anumit domeniu sau aplicaţie; el trebuie să delege toată responsabilitatea legată de aspecte specifice ale domeniului către secţiuni specifice ale aplicaţiei. Designul orientat obiect este unul „responsability-driven”. De aceea, procesul de design începe cu analiza comportamentului, pentru că, spre deosebire de structurile de date şi specificaţiile formale ale apelurilor de funcţii, care ajung să fie cunoscute şi stabilite mult mai târziu, în urma analizei mult mai

122

Programarea orientată obiect profunde a problemei, comportamentul, adică ceea ce aşteaptă utilizatorul să facă aplicaţia pentru el, este descris încă de la început, în termeni cu semnificaţie atât pentru programatori cât şi pentru client.

Prima acţiune a echipei de proiectare software este de a rafina şi clarifica specificaţiile iniţiale ale clientului, care, de cele mai multe ori sunt neclare şi incomplete. De asemenea, trebuie avut în vedere posibilele modificări de specificaţii ce apar ulterior şi care afectează produsul în procesul de dezvoltare. De aceea, structura produsului software trebuie gândită în aşa fel încât impactul modificărilor de specificaţii să fie minim.

Ingineria software este simplificată prin identificarea şi dezvoltarea de componente software. O componentă este o entitate abstractă care realizează anumite acţiuni, adică îndeplineşte anumite responsabilităţi. În fazele iniţiale ale procesului de dezvoltare nu este important să se cunoască reprezentarea exactă a unei componente sau cum realizează o anumită acţiune. În final, o componentă se concretizează într-o funcţie, structură sau clasă, sau o colecţie de alte componente. Sunt importante două caracteristici:

• o componentă trebuie să aibă un set redus bine definit de responsabilităţi;

• interacţiunea dintre componente trebuie să fie minimă. Identificarea componentelor se realizează în momentul în care se

imaginează procesul de execuţie a sistemului; asta înseamnă stabilirea acţiunilor efectuate şi identificarea entităţilor care le efectuează.

Oricât de bine este condus procesul de specificare a cerinţelor şi de proiectare a produsului software, întotdeauna este nevoie să se intervină ulterior în formularea cerinţelor şi în structura produsului, datorită unor noi cerinţe ale utilizatorului sau rafinării unora deja existente. Programatorii şi designerii software trebuie să anticipeze aceste situaţii şi să minimizeze impactul lor. Printre acţiunile pe care trebuie să le întreprindă pentru aceasta, se numără:

• obiectivul principal este minimizarea numărului de componente afectate de o schimbare de cerinţe. Chiar şi în cazul schimbărilor majore, acesta nu trebuie să afecteze prea multe secţiuni de cod;

• identificarea încă de la început a codului sursă cel mai predispus în viitor la schimbări şi izolarea efectelor acestor schimbări; cele mai frecvente modificări ţin de interfeţe, de formatele de comunicare, de formatele rezultatelor;

123

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

• reducerea cuplării dintre componente, ceea ce reduce dependenţa dintre ele şi creşte posibilitatea modificării uneia cu implicaţii minime asupra celeilalte.

Pe lângă comportament, componentele conţin şi informaţii. O componentă reprezintă o pereche constituită din comportament şi stare.

• comportamentul unei componente este setul de acţiuni pe care le realizează; descrierea completă a comportamentului unei componente se mai numeşte şi protocol.

• starea unei componente reprezintă toate informaţiile memorate în interiorul ei.

Nu este necesar ca toate componentele să menţină informaţii de stare, însă majoritatea componentelor vor fi o combinaţie de comportament şi stare.

Două concepte importante folosite în proiectarea componentelor software sunt cuplarea şi coeziunea. Coeziunea reprezintă măsura în care responsabilităţile unei singure componente formează un tot semnificativ. O coeziune ridicată este obţinută prin asocierea acţiunilor care sunt relaţionate dintr-un anumit punct de vedere, într-o singură componentă. Cuplarea descrie relaţia dintre componentele software. În general, este de dorit minimizarea dependenţelor dintre componente, din moment ce acestea afectează reutilizarea codului, uşurinţa în modificare şi dezvoltare. În particular, cuplarea este crescută atunci când o componentă software trebuie să acceseze informaţiile de stare ale altei componente. Aceste situaţii trebuie evitate, o modalitate este ca acţiunea respectivă să fie transferată ca responsabilitate componentei care memorează informaţia de stare dorită.

Atunci când o componentă dezvoltată de un programator este utilizată de un altul, este obligatoriu ca acesta din urmă să ştie cum să o folosească şi mai puţin să ştie cum a fost implementată. Folosirea unei componente presupune cunoaşterea responsabilităţilor pe care le are, adică a acţiunilor disponibile şi nu cum aceste acţiuni sunt implementate. Aceasta reprezintă separarea dintre interfaţă şi implementare. Separarea este necesară din mai multe motive:

• ascunderea detaliilor de implementare ţine de o caracteristică fundamentală a programării orientate obiect, şi anume încapsularea, deoarece nu este relevant şi nici nu ar trebui să intereseze pe un programator care foloseşte o componentă cum sunt implementate funcţionalităţile sale;

124

Programarea orientată obiect

• implementarea comportamentului unei componente se schimbă; atâta timp cât interfaţa expusă pentru celelalte componente nu se schimbă, impactul modificării implementării este minim. De exemplu, există o componentă folosită pentru salvarea unor informaţii pe disc; o implementare iniţială este salvarea într-un fişier binar; ulterior însă, se doreşte salvarea într-o bază de date sau un fişier xml. Modificarea modului în care se salvează datele nu trebuie să afecteze implementarea celorlalte componente care utilizează această funcţionalitate.

Implementarea componentelor software se face prin intermediul construcţiilor de tip clasă, puse la dispoziţie de limbajele de programare. Nu toate limbajele de programare sunt 100% orientate obiect. De exemplu, limbajul C++ permite în continuare construcţii din limbajul C, care nu sunt orientate obiect: funcţii definite în afara claselor, variabile globale etc.

7.2 Clasele

Programarea orientată obiect aduce în plus faţă de celelalte construcţii ale altor tehnici de programare, un conglomerat numit clasă. În [MSPCD97], clasa este definită ca o categorie generalizată ce descrie un grup de elemente particulare, numite obiecte, care fac parte din ea. O clasă reprezintă un instrument de descriere utilizat în programare pentru a defini o entitate sub cele două aspecte: al caracteristicilor sale, definite prin atributele clasei şi al comportamentului său, definit prin metodele clasei. Prin comportamentul unei entităţi se înţelege modul în care aceasta reacţionează la interacţiunea cu alte entităţi.

Definirea unei clase se realizează prin: • precizarea operanzilor sau a atributelor; • funcţiile membre; • proprietăţile asociate componentelor pentru a stabili modalităţi

de referire. O clasă este un tip de dată definit de utilizator printr-o construcţie de

forma: class nume { element 1; element 2; ………….

element n; }

125

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Un element este fie variabilă de tip fundamental sau de tip derivat, în cazul atributelor, fie respectiv o funcţie.

Rezultă că o clasă se descrie sub forma: class nume { tip1 variabila1; tip2 variabila2; …………….

tipn variabilan; tip1 functie1 ( lista parametrii 1 ); tip2 functie2 ( lista parametrii 2 ); …………………. tipm functiem ( lista parametrii m );

}

Clasele diferă de structurile de tip articol prin faptul că elementelor din componenţă li se asociază restricţii de referire cu ajutorul specificatorilor de control. Aşa cum variabilele în programe sunt de tip global şi local sau după modul de alocare sunt statice şi dinamice, tot astfel, pentru a defini o anumită disciplină în manipularea claselor se asociază grupurilor de elemente din clasă un specificator de control al cărui câmp de acţiune este anulat de un altul.

Specificatorii de control sunt:

• public – ceea ce înseamnă că elementul respectiv este accesibil oricărei clase externe;

• private – înseamnă că numai elementele componente ale aceleaşi clase au dreptul de a accesa acest membru;

• protected – prin care disponibilitatea elementului include disponibilitatea specificatorului private, şi, în plus, asigură disponibilitatea elementului pentru elementele ce urmează a fi definite în clasele derivate din clasa curentă.

Funcţiile membru ale clasei referă oricare dintre variabilele definite în clasă. La proiectarea unei clase se au în vedere următoarele aspecte:

• funcţiile membru să acopere întreaga gamă de prelucrări;

• să fie definite cât mai multe forme de iniţializare a operanzilor;

• între variabilele şi funcţiile membre să fie o concordanţă perfectă pentru a nu apare erori în execuţia programelor ce folosesc clase.

126

Programarea orientată obiect

Programarea orientată obiect este tehnica de programare fundamentată pe conceptul de reutilizare software. Pentru a atinge acest obiectiv trebuie ca:

• procedurile să efectueze prelucrări complete şi corecte; • referirea procedurilor să se efectueze rapid şi uşor, fără a fi

nevoie de informaţii inutile din moment ce funcţiile membre utilizează variabile definite în aceeaşi clasă;

• manipularea claselor să conducă la obţinerea de noi clase, iar gradul de generalitate să fie cât mai ridicat.

De exemplu, dacă se doreşte implementarea calculului cu numere complexe, se defineşte clasa Complex care trebuie să aibă o structură de tip articol pentru a grupa partea reală şi partea imaginară a numărului complex şi să conţină funcţii membre pentru operaţiile de adunare, scădere, înmulţire, împărţire, ridicare la putere şi extragere de radical.

Această clasă se defineşte în secvenţa de sursă: class Complex { private: struct compl { float real; float img; } c; public: …………………… void aduna ( Complex& cpl ) { c.real = c.real + cpl.c.real; c.img = c.img + cpl.c.img; } void scade ( Complex& cpl ) { c.real = c.real - cpl.c.real; c.img = c.img - cpl.c.img; } void produs ( Complex& cpl ) { c.real = c.real * cpl.c.real - c.img * cpl.c.img; c.img = c.real * cpl.c.real + c.img * cpl.c.img; } void divide ( Complex& cpl ) { float temp; temp = cpl.c.real * cpl.c.real - cpl.c.img * cpl.c.img;

127

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

c.real = ( c.real * cpl.c.real - c.img * cpl.c.img ) / temp; c.img = ( c.real.*cpl.c.img - c.img * cpl.c.real ) / temp; } ……………………….. }

Aşa cum sunt definite funcţiile membru ale clasei Complex, au asociat specificatorul de control public. Clasele sunt bine construite dacă la elaborarea unui program principal apar numai referiri de funcţii membre din clase.

7.3 Constructori şi destructori

Constructorii sunt metode cu acelaşi nume ca al clasei, utilizaţi pentru construirea instanţelor clasei. Ei se execută de câte ori se alocă memorie pentru o nouă instanţă a clasei, după ce s-a produs alocarea de memorie. Este recomandat ca o clasă să conţină mai multe declarări de constructori, care vor diferi între ele prin numărul şi tipul parametrilor transmişi. În felul acesta, se asigură un grad mai mare de utilizabilitate al clasei, dat fiind posibilităţile diferite de instanţiere.

Limbajul C++ pune la dispoziţia utilizatorilor trei tipuri de alocări de memorie:

• statică, atunci când o variabilă este declarată în afara oricărei funcţii sau când pentru un atribut se foloseşte modificatorul static. Un atribut static este comun tuturor instanţelor derivate clasei din care provine;

• automatică, care se face pentru variabile definite în cadrul corpului unei funcţii; memoria alocată este dealocată atunci când se părăseşte corpul funcţiei;

• dinamică, care se face pe heap, la cererea explicită a programatorului.

Destructorii sunt metode ale clasei referiţi înainte de a se produce dealocarea zonei de memorie asociate elementelor clasei. O clasă nu are decât un singur destructor. Destructorul este o procedură care se defineşte prin numele clasei, fără a avea listă de parametrii, fiind precedat de operatorul ~; pentru clasa Complex destructorul are forma ~Complex().

128

Programarea orientată obiect

Atât constructorii, cât şi destructorii nu au tipuri asociate.

Forma constructorilor variază de la o clasă la alta, în funcţie de specificul fiecăreia, dar şi de experienţa şi obiectivele proiectantului clasei. Există o formă de constructor, numită constructor de copiere, foarte utilă în special atunci când se transmit obiecte ca parametrii prin valoare. Constructorul de copiere primeşte ca unic parametru o referinţă la un alt obiect al aceleaşi clase. Implementarea cea mai frecventă presupune copierea efectivă a valorilor atributelor obiectului transmis ca parametru în atributele obiectului nou.

De exemplu, pentru clasa Complex se identifică următorii constructori pentru iniţializarea:

• cu variabile corespunzătoare membrilor structurii care stochează atributele numărului complex;

• cu un alt obiect din aceeaşi clasă.

Corespunzător, textele sursă de definire a constructorilor sunt: Complex ( float a, float b ) { c.real = a; c.img = b; } Complex ( Complex& cpl ) { c.real = cpl.c.real; c.img = cpl.c.img; }

7.4 Proprietăţile claselor

Programarea orientată obiect dezvoltă noi abordări prin faptul că obiectele şi clasele se bucură de o serie de proprietăţi.

7.4.1 Încapsularea

Este rezultatul regrupării operanzilor şi funcţiilor membre într-un conglomerat numit clasă. Operanzii şi operatorii clasei interacţionează, formând un tot unitar.

De exemplu, se consideră clasa Matrice. Încapsularea revine la a defini operanzii care se referă la numărul de linii, numărul de coloane şi elementele matricei în clasă, precum şi funcţiile care efectuează operaţiile de calcul, formând un tot unitar. Funcţiile folosesc direct operanzii întrucât

129

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

prin încapsulare este dreptul lor de a referi ceva definit pe un domeniu comun. În limbajul FORTRAN instrucţiunea COMMON avea menirea de a implementa germeni ai încapsulării asociind zone de memorie grupate unor operanzi, fără a mai fi nevoie de a-i înscrie în lista de parametrii exact ca în cazul încapsulării propriu-zise.

Pentru efectuarea operaţiilor de numărare elemente pozitive, negative şi nule, precum şi pentru testarea simetriei matricei lista parametrilor este nulă, deoarece toate informaţiile necesare sunt deja definite în cadrul clasei şi folosite ca atare, nemaifiind nevoie să fie transmise ca parametrii, codul sursă al clasei Matrice este:

class Matrice { protected: int nrCol; int nrLinii; int mat[M][N]; public: Matrice( int m, int n ) { nrCol = m; nrLinii = n; for ( int i = 0; i < m; i++ ) { for ( int j = 0; j < n; j++ ) { mat[i][j] = 0; } } } Matrice( int a[][N], int m, int n ) { nrCol = m; nrLinii = n; for ( int i = 0; i < nrLinii; i++ ) { for ( int j = 0; j < nrCol; j++ ) { mat[i][j] = a[i][j]; } } } Matrice( Matrice& m ) { nrCol = m.nrCol; nrLinii = m.nrLinii; for ( int i = 0; i < nrLinii; i++ ) { for ( int j = 0; j < nrCol; j++ ) { mat[i][j] = m.mat[i][j]; } } } bool simetrie1() { bool result = true; if ( nrCol != nrLinii ) {

130

Programarea orientată obiect

result = false; } else { for ( int i = 0; result && i < nrLinii; i++ ) { for ( int j = 0; result && j < nrCol; j++ ) { result = mat[i][j] == mat[j][i]; } } } return result; } int contorplus() { int contor = 0; for ( int i = 0; i < nrLinii; i++ ) { for ( int j = 0; j < nrCol; j++ ) { if ( mat[i][j] > 0 ) { contor++; } } } return contor; } int contorminus() { int contor = 0; for ( int i = 0; i < nrLinii; i++ ) { for ( int j = 0; j < nrCol; j++ ) { if ( mat[i][j] < 0 ) { contor++; } } } return contor; } int contornul() { int contor = 0; for ( int i = 0; i < nrLinii; i++ ) { for ( int j = 0; j < nrCol; j++ ) { if ( mat[i][j] == 0 ) { contor++; } } } return contor; }

131

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

7.4.2 Moştenirea

Este o proprietate deosebit de importantă, pe baza ei sunt construite clasele din aproape în aproape, organizându-se pe niveluri de agregare.

Clasele agregate de pe nivelul k preiau operanzii şi funcţiile membre ale claselor de pe nivelul k-1, care intră prin derivare în componenţa lor, precum şi proprietăţile acestora.

De exemplu, dacă se doreşte îmbogăţirea clasei Matrice cu funcţii pentru determinarea minimului şi maximului elementelor din cadrul matricei, se construieşte o nouă clasă Matrice2, unde, pe lângă funcţiile deja existente ale clasei se adaugă noile metode pentru aflarea minimului, respectiv, maximului, clasa obţinută având codul sursă:

class Matrice2 : public Matrice { public: int minim () { int min = mat[0][0]; for ( int i = 0; i < nrLinii; i++ ) { for ( int j = 0; j < nrCol; j++ ) { if ( min > mat[i][j] ) { min = mat[i][j]; } } } return min; } int maxim () { int max = mat[0][0]; for ( int i = 0; i < nrLinii; i++ ) { for ( int j = 0; j < nrCol; j++ ) { if ( max < mat[i][j] ) { max = mat[i][j]; } } } return max; } };

Avantajele folosirii moştenirii sunt: • reutilizabilitatea – când o funcţionalitate este moştenită din altă

clasă, codul respectiv nu trebuie rescris, el trebuie doar apelat în noul context; o altă implicaţie este legată de fiabilitatea codului,

132

Programarea orientată obiect

deoarece prin moştenire, o anumită funcţionalitate este scrisă doar la nivelul unei clase şi apoi moştenită şi utilizată în toate clasele derivate;

• consistenţa interfeţei – când două sau mai multe clase sunt derivate din aceeaşi clasă părinte, se asigură faptul că comportamentul moştenit este acelaşi pentru toate clasele;

• componentele software – moştenirea dă posibilitatea programatorilor să construiască componente software reutilizabile şi gruparea lor în biblioteci; în acest fel, efortul de dezvoltare al unui produs nou este diminuat prin utilizarea de librării cu funcţionalitate deja implementată;

• dezvoltarea rapidă de prototipuri – atunci când sistemul software este construit folosindu-se componente reutilizabile, timpul de dezvoltare este concentrat pe înţelegerea elementelor specifice ale sistemului; astfel se construiesc versiuni de sistem, numite prototipuri, care pun accent pe aspectele critice ale sistemului. Un prototip este dezvoltat, utilizatorii îl folosesc, iar a doua versiune a sistemului este realizată pe baza experienţei acumulată cu prima şi a feedbackului de la utilizatori.

Deşi moştenirea prezintă foarte multe avantaje, există şi o serie de costuri de care trebuie să se ţină seama în momentul proiectării ierarhiilor de clase:

• viteza de execuţie – este influenţată prin prisma faptului că metodele moştenite, care au un caracter mai general, sunt de regulă mai lente decât codul specializat; însă afectarea vitezei de execuţie este compensată cu creşterea vitezei de dezvoltare;

• dimensiunea programelor – este influenţată în sensul că devine mai mare în cazul programelor care folosesc librării de componente, decât programele care folosesc cod specializat, deoarece nu toate componentele dintr-o librărie sunt folosite în proiect, dar librăria trebuie adăugată în totalitatea sa;

• complexitatea programelor – o ierarhie de clase introduce un anumit grad de complexitate în sistem; cu cât ierarhia este mai mare, nivelurile de abstractizare mai multe, cu atât sistemul care foloseşte această ierarhie este mai complex.

133

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

7.4.3 Polimorfism

În limbajele de programare, un obiect polimorfic este orice entitate, cum ar fi o variabilă sau argument de funcţie, căreia îi este permis să stocheze valori de tipuri diferite pe durata execuţiei programului. Funcţiile polimorfice sunt acelea care au argumente polimorfice. În programarea orientată-obiect, polimorfismul reflectă principiul substituibilităţii, şi anume, o variabilă polimorfică poate stoca o valoare a tipului său declarat sau a oricărui subtip al tipului declarat. Polimorfismul funcţiilor reprezintă posibilitatea de a asocia acelaşi nume la diferite funcţii. Evitarea ambiguităţii în procesul de referire se obţine prin:

• atribuirea de liste de parametrii de lungimi diferite;

• atribuirea de liste de parametrii cu aceeaşi lungime, dar parametrii corespunzători ca poziţie au tipuri diferite;

În cazul funcţiilor, polimorfismul poartă şi numele de supraîncărcare.

Pentru clasa Matrice se defineşte operaţia de adunare a două matrice. Implementarea operaţiei se face prin două metode, denumite aduna, diferenţiate prin lista de parametrii. Clasa Matrice devine:

class Matrice { …………………………. void aduna (Matrice& m ) { aduna(m.mat, m.nrLinii, m.nrCol ); } void aduna ( int a[][N], int m, int n ) { for ( int i = 0; i < m; i++ ) { for ( int j = 0; j <n ; j++ ) { mat[i][j] += a[i][j]; } } } };

Cele două metode sunt polimorfice; au acelaşi nume, iar stabilirea apelului corect se face la execuţie, în funcţie de numărul, tipul şi poziţia parametrilor.

O altă situaţie în care se pune problema polimorfismului o reprezintă suprascrierea metodelor. Prin moştenire, clasa derivată preia metodele expuse de clasa părinte, conform cu regulile de derivare. Însă, ea are posibilitatea să furnizeze o altă implementare pentru o metoda moştenită.

134

Programarea orientată obiect Practic, se defineşte o metodă în clasa derivată cu aceeaşi semnătură cu cea din clasa părinte, dar care are o altă implementare. În momentul execuţiei, metoda din clasa derivată este identificată şi executată înaintea metodei din clasa părinte.

Suprascrierea se foloseşte atunci când, pentru o anumită funcţionalitate, există o implementare implicită la nivelul clasei părinte, implementare care poate fi rafinată prin suprascriere, în clasele derivate, dacă se doreşte acest lucru. De exemplu, pentru metoda simetrie1() din clasa Matrice se furnizează o altă implementare în clasa derivată Matrice2, prin care simetria este testată atât pentru diagonala principală, cât şi pentru diagonala secundară. Textul sursă este:

class Matrice { ……………………………… public: virtual bool simetrie1() { ………………………………. } }; class Matrice2 : public Matrice { public: ……………………………… bool simetrie1() { //test pentru diagonala principala bool result = Matrice::simetrie1(); //test pentru diagonala secundata if ( nrCol == nrLinii ) { for ( int i = 0; result && i < nrLinii; i++ ) { for ( int j = 0; result && j < nrCol; j++ ) { if ( mat[i][j] != mat[nrCol-i-1][nrCol-j-1] ) { result = false; } } } } else { result = false; } return result; } };

135

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Se observă că în implementarea din clasa Matrice2 se foloseşte rezultatul implementării din clasa Matrice, în plus testându-se şi simetria faţă de diagonala secundară. Apelarea implementării din clasa părinte nu este o cerinţă obligatorie atunci când se suprascrie o metodă, dar este o practică recomandată, deoarece, teoretic, suprascrierea trebuie să aducă ceva în plus faţă de o prelucrare generală, într-un context particular, al clasei derivate.

Supraîncărcarea operatorilor

Reprezintă un mod elegant de a pune în corespondenţă simbolurile unor operatori cu proceduri complexe de prelucrare, specifice unor tipuri de date derivate.

Limbajul Basic a avut implementată adunarea, scăderea, înmulţirea de numere şi adunarea de matrice. Prin supraîncărcarea de operatori se creează premisele evaluării de calcule matriceal scriind expresiile direct, cu operatori de calcul şi operanzi matrice.

În acest fel, operatorul + este pus în corespondenţă cu procedura de adunare matrice, operatorul – este pus în corespondenţă cu procedura scădere matrice, operatorul * este pus în corespondenţă cu înmulţirea de matrice, iar operatorul = este pus în corespondenţă cu copierea de matrice.

Următorii operatori nu se supraîncarcă:

• ([]) operatorul de selectare a unei componente membre într-o structură de tip articol;

• (*) operatorul de deferire a unei componente din clasă;

• (::) operatorul de selecţie a funcţiei membru dintr-o clasă; operatorul de tratare variabile globale;

• (?:) operatorul condiţional.

Pentru supraîncărcarea operatorului binar numit operator, se utilizează definirea:

tip1 operator simbol_operator ( tip2, tip3 )

unde:

• tip1 – tipul rezultatului returnat;

• tip2 – tipului primului operand;

• tip3 – tipul celui de-al doilea operand.

136

Programarea orientată obiect

Dacă supraîncărcarea operatorului simbol_operator este realizată în clasa numită classa, definirea operatorului în afara clasei se realizează prin:

tip1 classa::calcul simbol_operator (tip2, tip3)

În exemplul de mai jos se prezintă supraîncărcarea operatorului + pentru a însemna adunarea a două matrice şi a operatorului – pentru a însemna scăderea a două matrice pornind de la clasa Matrice.

class Matrice { ………………………… void aduna (Matrice& m ) { aduna(m.mat, m.nrLinii, m.nrCol ); } Matrice& operator + (Matrice& m ) { aduna(m); return *this; } Matrice& operator - (Matrice& m ) { for (int i = 0; i < nrLinii; i++ ) { for ( int j = 0; j < nrCol; j++ ) { mat[i][j] = mat[i][j] - m.mat[i][j]; } } return *this; } };

Datorită concepţiei total diferite cu privire la modul în care o aplicaţie este proiectată şi implementată, evaluarea complexităţii necesită utilizarea unor metrici specifice. Rezultatele furnizate de metricile prezentate în capitolul Ciclul de dezvoltare software, şi anume, complexitatea ciclomatică şi metricile Halstead nu sunt relevante. Pentru aplicaţia care rezolvă problema considerată ca exemplu de control în această carte, se construieşte clasa Matrice, iar programul apelator instanţiază această clasă cu date citite de la tastatură şi va apela diverse metode ale clasei pentru a obţine rezultatele cerute.

Valorile metricilor enunţate anterior suportă, în acest caz două niveluri de agregare; ele sunt determinate la nivelul metodelor clasei, sunt agregate la nivelul clasei, şi în final, la nivelul întregii aplicaţii. Rezultatele sunt:

Metrică Valoare Complexitatea ciclomatică (v) = 29Lungimea programului = 238Vocabularul programului = 92Dificultatea = 35,33

137

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Analizând comparativ cu rezultatele obţinute prin celelalte tehnici de programare, există clar o creştere de complexitate, care are diverse cauze:

existenţa unor construcţii specifice programării orientate-obiect, cum sunt constructorii, metodele de acces la atributele clasei etc.;

analiza întregii clase, nu numai a unora dintre funcţiile implicate în fluxul aplicaţiei, din motivul că, o clasă este privită ca un tot unitar, şi prin urmare trebuie înţeleasă de către programator în totalitatea sa;

deşi informaţiile referitoare la dimensiunile şi elementele matricei sunt înglobate în clasă, şi prin urmare, nu sunt transmise ca parametrii, acest lucru nu este surprins de metricile Halstead de complexitate, care iau în considerare, în fiecare dintre metodele clasei, atributele acesteia ca fiind operanzi distincţi, lucru ce contribuie la creşterea nejustificată a valorilor acestor metrici.

De aceea, pentru analiza complexităţii programelor orientate obiect, sunt utilizate metrici specifice. Există două tipuri de metrici: unele se referă la interdependenţa dintre clase şi se numesc metrici de cuplare, altele se referă la consistenţa clasei şi se numesc metrici de coeziune. Aceste metrici sunt în legătură cu caracteristicile specifice ale programării orientate obiect: încapsulare, moştenire, polimorfism, interacţiunea dintre obiecte.

Metricile de coeziune sunt legate în principal de dimensiunea claselor. Unele cele mai relevante metrici din această clasă de metrici sunt:

numărul de atribute ale clasei (NOA), împărţit în numărul de atribute ale instanţei de clasă (NOI) şi numărul de atribute statice (NOS);

numărul de metode ale clasei (NOM), împărţit în numărul de metode publice (NOP), şi numărul de metode private (NOPV); aceste metrici sunt mărimi ale încapsulării;

gradul de încapsulare (GI), văzut ca raport între numărul de elemente private ale clasei şi numărul total de elemente. Această mărime trebuie să fie cât mai aproape de valoarea 1. Pentru atribute, el trebuie să fie întotdeauna 1.

138

Programarea orientată obiect

Pentru exemplul considerat, în care există clasele Matrice şi Matrice2, se obţin următoarele valori pentru metricile prezentate mai sus:

Metrică Matrice NOA = 3NOI = 3NOS = 0NOM = 7NOP = 7NOPV = 0GI = 0,33

Se observă lipsa metodelor de tip privat, ceea ce înseamnă că funcţionalitatea expusă este foarte mare, practic toate metodele clasei sunt prezente în interfaţa acesteia. O clasă cu o interfaţă bogată în metode are un nivel de complexitate mai mare decât al unei clase care expune mai puţine metode în interfaţa sa, deoarece prima necesită un efort de înţelegere din partea programatorului mai mare, cel puţin teoretic, decât a doua.

Metricile de cuplare sunt legate, în principal, de interacţiunea dintre obiecte sau dintre clase. Interacţiunea dintre obiecte se identifică în parametrii transmişi în apelurile de metode, instanţierea de obiecte locale de tipuri diferite, tipul rezultatului apelului unei metode. Două clase sunt cuplate atunci când metodele declarate în una dintre ele folosesc metode declarate în cealaltă. O cuplare excesivă afectează nu numai reutilizabilitatea claselor, dar şi complexitatea acestora datorită numărului mare de legături dintre clase care determină o înţelegerea mai greoaie a codului de către programator.

Avantajele programării orientate obiect sunt nenumărate, printre cele mai importante regăsindu-se şi reutilizarea codului. Prin intermediul mecanismelor de derivare, se obţine acces la funcţionalitatea de bază a obiectului, cu posibilitatea modificării acesteia prin mecanisme polimorfice, cum sunt suprascrierea metodelor sau supraîncărcarea operatorilor. Prin derivare, se obţin clase noi, ce modelează mai atent anumite aspecte ale entităţilor reale sau abstracte utilizate în modelul aplicaţiei software.

139

8.1 Premise

Programarea orientată obiect oferă o abordare unitară asupra fenomenelor modelate de aplicaţiile software atât din punct de vedere al datelor, cât şi al prelucrărilor. Acest lucru a făcut ca această tehnică de programare să se impună în detrimentul celorlalte. Programarea orientată obiect a dus şi la construirea de metodologii de dezvoltare software orientate obiect.

Un aspect foarte important în activitatea de programare îl constituie reutilizarea codului. Acest lucru se face prin construirea de librării cu elemente de cod, cum sunt funcţiile sau clasele de obiecte, şi utilizarea acestora în noi aplicaţii. Principiile de construire a unor astfel de librării au fost expuse în capitolele Programarea standard şi Programarea orientată obiect. Datorită dezvoltării şi răspândirii de noi limbaje şi tehnologii de programare, a apărut o primă mare problemă: cum se poate folosi o funcţionalitate scrisă într-un limbaj de programare şi înglobată într-o bibliotecă, în cadrul unei aplicaţii scrise în alt limbaj de programare? Aceasta este problema interoperabilităţii dintre limbajele şi tehnologiile diferite de programare.

Situaţia aceasta este foarte frecvent întâlnită, deoarece o companie care utilizează software pentru desfăşurarea activităţilor sale, achiziţionează acest software pe principii de eficienţă şi cost, mai puţin pe principii legate de tehnologii şi limbaje folosite. Inevitabil, la un moment dat, acest software va trebui să comunice, pentru rezolvarea unor probleme mai complexe. Pentru aceasta, au fost fundamentate componentele.

Programarea bazată pe componente reprezintă o extensie a programării orientate obiect în care elementul central este componenta. Componenta este definită ca program sau obiect binar de dimensiuni mici, cu funcţionalitate specifică, bine definită, proiectat într-o manieră care

140

Utilizarea de componente permite combinarea cu alte componente şi/sau aplicaţii. Modelul unei aplicaţii care utilizează componente este prezentat în figura 8.1:

PRODUS SOFTWARE

COMP1I1

COMP2I2

COMPnIn

…………………………….

Figura 8.1 Modelul unei aplicaţii care foloseşte componente

Componentele se împart în trei mari categorii, după cum urmează:

• componente care oferă servicii utilizatorilor de aplicaţii; aici intră în principal componentele vizuale, care sunt afişate pe ecranele cu care lucrează utilizatorii aplicaţiei;

• componente care oferă servicii de business şi care implementează reguli de business, cum sunt de exemplu, reguli de calcul al impozitelor şi taxelor, modalităţi de livrare etc.; rezultatele lor sunt furnizate componentelor din prima categorie, pentru afişarea către utilizator;

• componente care oferă acces la date; acestea sunt componente prin care se permite conectarea la baze de date, la alte aplicaţii mai vechi, şi care furnizează date serviciilor din categoria anterioară, de business, pentru prelucrări.

8.2 Software orientat pe componente

Ingineria software bazată pe componente foloseşte principii legate de modularitate, abstractizare, încapsulare. Primul pas este crearea specificaţiilor pentru interfaţă. Interfaţa unei componente reprezintă setul de metode de prelucrare pe care componenta le expune spre utilizare celorlalte

141

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

componente şi/sau aplicaţii. Principiul este simplu şi are precedent în abordarea orientată obiect: atâta timp cât funcţionalitatea oferită nu se schimbă, modul în care aceasta este implementată nu contează pentru utilizatorul componentei.

Interfaţa trebuie să aibă o serie de caracteristici:

• să fie consistentă, adică funcţionalităţile oferite să se refere la o aceeaşi entitate reală; de exemplu, o interfaţă care expune funcţionalităţi legate de lucrul cu matrice, nu poate conţine funcţionalităţi legate de lucrul cu fişiere;

• să fie clară, ceea ce înseamnă că metodele din interfaţă să exprime clar funcţionalitatea oferită;

• să fie scurtă, deoarece o interfaţă cu foarte multe metode este greu de înţeles şi de folosit de către programatori.

Rezultatul acestei faze este un document de specificaţii privind interfaţa componentei şi, eventual, o descriere a acesteia într-un limbaj specializat.

O componentă poate expune mai multe interfeţe, gândite ca subseturi de funcţionalitate pe care componenta o implementează. Motivul este următorul: diverşi utilizatori ai componentei au nevoie doar de anumite metode ale acesteia, prin urmare, expunerea întregii interfeţe către ei, în primul rând îngreunează utilizarea componentei, dar şi deschide posibilităţi de afectare a stării componentei de către utilizatori care nu ar trebui să o afecteze. Prin urmare, expunerea de interfeţe diferite pentru tipuri diferite de utilizatori înseamnă şi implementarea unui mecanism de securitate la nivelul componentei.

Pentru problema definită în capitolul Ciclul de dezvoltare software, se imaginează următoarea interfaţă ce este expusă unei alte aplicaţii, interfaţă descrisă folosind notaţia UML:

Figura 8.2 Exemplu de interfaţă pentru componente

Se observă că interfaţa are o metodă de iniţializare a componentei cu date referitoare la matrice, o metodă pentru determinarea minimului şi una

142

Utilizarea de componente pentru determinarea maximului dintre elementele matricei, iar ultima metodă numără elementele din matrice, astfel:

• dacă flag = 1, elementele mai mari decât valoarea specificată în parametrul k;

• dacă flag = 2, elementele mai mici decât valoarea specificată în parametrul k;

• dacă flag = 3, elementele egale cu valoarea specificată în parametrul k;

Cuvântul “in” care precede fiecare parametru indică faptul că respectivul este parametru de intrare.

O altă interfaţă a componentei conţine operaţiile de adunare, scădere şi înmulţire. Descrierea, folosind notaţia UML este dată în figura 8.3:

+initializare(in nrLinii : long(idl), in nrCol : long(idl), in elemente : any(idl))+aduna(in matrice : any(idl))+scade(in matrice : any(idl))+inmulteste(in matrice : any(idl))+numarLinii() : long(idl)+numarColoane() : long(idl)

«interface»MatriceINTF2

Figura 8.3 Interfaţa componentei Matrice pentru operaţii cu masive

Se observă că, pe lângă metodele corespunzătoare operaţiilor cu masive, există şi două metode, cu rol de accesorii pentru câmpuri de date ale componentei. Fiecare dintre operaţiile de bază, adunare, scădere, înmulţire primeşte ca parametru o altă componentă de acelaşi tip cu aceasta, iar rezultatul este memorat în componenta curentă.

Pentru că o componentă este scrisă într-un limbaj de programare şi folosită în alt limbaj de programare, mecanismul de construire a instanţelor de componente este diferit faţă de programarea orientată obiect. Din acest motiv este necesară o metodă separată de iniţializare a datelor componentei.

Pasul al doilea înseamnă crearea specificaţiilor pentru componente, ce includ interfeţele şi funcţiile membre. Pornind de la specificaţiile interfeţelor, se elaborează modelul obiectual al componentei care include, pe lângă clasa principală, şi un set de clase ajutătoare. Obiectivul este de a asigura dezvoltatorilor o specificaţie completă atât din punct de vedere al funcţionalităţii implementate, cât şi din punct de vedere al modului în care aceasta va fi implementată. Prin urmare, în această etapă se ţine cont de tehnologia ce este folosită la dezvoltarea componentei.

143

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

Odată ce specificaţiile pentru componentă au fost întocmite, programatorii încep dezvoltarea propriu-zisă. Unitatea de testare în ingineria software bazată pe componente este realizată prin crearea unei alte componente, care testează interfeţele şi funcţiile membre ale acesteia. Fiecare programator va trebui să îşi construiască o a doua componentă prin care să testeze funcţionalitatea componentei iniţiale. Motivaţia acestei abordări este următoarea:

• în primul rând, fiecare funcţionalitate, înainte de a fi folosită trebuie supusă unei faze de testare; programatorul are obligaţia ca funcţionalitatea implementată, pe care el o oferă, să fie deja testată; tehnica se numeşte unit-testing şi vizează stabilirea următoarelor:

conformitatea funcţionalităţii cu specificaţiile: componenta face ceea ce este specificat să facă;

implementarea reacţionează corespunzător şi în situaţiile excepţionale de prelucrare: cazuri extreme, fluxuri alternative de prelucrare etc.

• în al doilea rând, se testează comunicarea între componentă şi mediul extern; practic, se simulează modul în care componenta va fi folosită în cadrul unor aplicaţii concrete.

Faza de testare individuală (unit-testing) a componentei presupune la rândul alte două faze:

• testarea componentei ca şi clasă (pentru că la bază nu este altceva decât o clasă); aceasta presupune testarea directă a metodelor componentei atât cele private, cât mai ales cele publice;

• testarea componentei prin interfeţele expuse; această testare trebuie să aducă în plus, faţă de rezultatele celei anterioare, informaţii despre cum sunt primiţi şi trataţi parametrii de intrare, cum se scriu datele în parametrii de ieşire.

Dezvoltarea unei componente diferă în funcţie de tipul de componentă şi tehnologia folosită. Fiecare tehnologie are propriile mecanisme. Într-un fel sunt scrise componentele folosind tehnologia COM şi limbajul C++ şi în alt mod sunt scrise componentele folosind tehnologia EJB împreună cu limbajul Java. Pentru exemplificare, s-a folosit tehnologia COM folosind

144

Utilizarea de componente ATL şi limbajul C++. Astfel, interfaţa din figura 8.2 se scrie în felul următor, folosind IDL + Interface Definition Language:

[ object, uuid(F8AE2674-8836-433E-B809-D6DB90BFA8E9), dual, helpstring("IMatrice Interface"), pointer_default(unique) ] interface IMatrice : IDispatch { [id(1), helpstring("method initializare")] HRESULT initializare(int nrLinii, int nrCol, int* mat); [id(2), helpstring("method minim")] HRESULT minim(int* min); [id(3), helpstring("method maxim")] HRESULT maxim(int* max); [id(4), helpstring("method contor")] HRESULT contor(int flag,int k, int* numar); }; [ uuid(E64BA31E-7FBC-47CE-A993-474502FD2CB2), version(1.0), helpstring("Matrix 1.0 Type Library") ]

Pentru scrierea interfeţei au fost utilizate mecanismele de tip wizard din cadrul mediului de dezvoltare Microsoft Visual Studio 6.0. Clasa C++ care implementează interfaţa are următorul cod sursă:

class ATL_NO_VTABLE CMatrice : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CMatrice, &CLSID_Matrice>, public IDispatchImpl<IMatrice, &IID_IMatrice, &LIBID_MATRIXLib> { public: CMatrice() { } DECLARE_REGISTRY_RESOURCEID(IDR_MATRICE) DECLARE_PROTECT_FINAL_CONSTRUCT() BEGIN_COM_MAP(CMatrice) COM_INTERFACE_ENTRY(IMatrice) COM_INTERFACE_ENTRY(IDispatch) END_COM_MAP() // IMatrice

145

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

public: STDMETHOD(contor)(int flag,int k, int* numar); STDMETHOD(maxim)(int* max); STDMETHOD(minim)(int* min); STDMETHOD(initializare)(int nrLinii, int nrCol, int* mat); private: int n; int m; int a[M][N]; }; ……………………. STDMETHODIMP CMatrice::initializare(int nrLinii, int nrCol, int* mat) { m = nrLinii; n = nrCol; for ( int i = 0; i < m; i++ ) { for ( int j = 0; j < n; j++ ) { a[i][j] = mat[i*m + j]; } } return S_OK; } STDMETHODIMP CMatrice::minim(int *min) { *min = a[0][0]; for ( int i = 0; i < m; i++ ) { for ( int j = 0; j < n; j++ ) { if ( a[i][j] < *min ) { *min = a[i][j]; } } } return S_OK; } STDMETHODIMP CMatrice::maxim(int *max) { *max = a[0][0]; for ( int i = 0; i < m; i++ ) { for ( int j = 0; j < n; j++ ) { if ( a[i][j] < *max ) { *max = a[i][j]; } } } return S_OK; }

146

Utilizarea de componente

STDMETHODIMP CMatrice::contor(int flag, int k, int *numar) { int i = 0; int j = 0; *numar = 0; switch ( flag ) { case 1: for ( ; i < m; i++ ) { for ( ; j < n; j++ ) { if ( a[i][j] > k ) ( *numar )++; } } break; case 2: for ( ; i < m; i++ ) { for ( ; j < n; j++ ) { if ( a[i][j] < k ) ( *numar )++; } } break; case 3: for ( ; i < m; i++ ) { for ( ; j < n; j++ ) { if ( a[i][j] == k ) ( *numar )++; } } break; } return S_OK; }

Se observă că, pe lângă clasa C++ propriu-zisă, care implementează funcţionalitatea componentei, mai sunt implicate o serie de alte clase, specifice tehnologiei de a căror scriere se ocupă wizard-ul, îmbunătăţindu-se astfel productivitatea. De asemenea, tipurile de date şi mai ales modul în care sunt transmişi parametrii şi rezultatele diferă faţă de situaţiile normale din programarea orientată obiect. De exemplu, rezultatul prelucrării se returnează printr-un parametru de tip ieşire declarat în lista de parametrii; funcţia returnează un cod de succes sau de eroare specific. De asemenea, nu orice tip de dată este permis; numai tipurile fundamentale, şirurile de caractere şi pointerii apar în listele de parametrii ale metodelor componentei expuse prin interfaţă. Pentru metodele din interiorul componentei, nevizibile în afară, se folosesc şi alte tipuri de date.

Pentru evaluarea complexităţii componentelor, metricile din capitolul Ciclul de dezvoltare software nu sunt relevante pentru că filozofia componentelor se referă la utilizarea acestora sub formă binară, în care nu

147

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

are importanţă caracteristicile codului sursă ci capacitatea componentei de a oferi funcţionalitatea aşteptată. Tot din acelaşi motiv, al lipsei accesului la codul sursă, nici metricile specifice abordării orientate obiect, unele enunţate în capitolul Programarea orientată obiect nu sunt aplicabile. De asemenea, deoarece, componentele se bazează pe diferite tehnologii, dezvoltarea lor presupune nu numai doar scrierea funcţionalităţii dar şi a codului sursă ajutător, specific fiecărei tehnologii în parte. Prin mecanismele de tip wizard, mare parte din codul sursă ajutător este generat deja, programatorul trebuind doar să adauge metodele specifice ce implementează funcţionalitatea componentei.

Datorită caracterului binar al componentelor, trebuie luate în considerare metrici care au legătură cu utilizarea componentei în aplicaţii şi practic surprind complexitatea componentei din punct de vedere al utilizării ei şi nu din punct de vedere al construcţiei; astfel de metrici sunt:

numărul de metode dintr-o interfaţă (NMI), numărul de parametrii ai metodelor dintr-o interfaţă (NPM), împărţit în numărul parametrilor de intrare (NPI), numărul parametrilor de ieşire (NPE) şi numărul parametrilor de intrare/ieşire (NPIE); acestea sunt mărimi ale complexităţii interfeţei componentelor;

numărul de interfeţe expuse de componentă (NI), numărul de metode de iniţializare a acesteia (NIM); acestea sunt mărimi ale disponibilităţii componentei.

Pentru exemplul considerat, valorile metricilor enumerate anterior sunt:

Metrică Interfaţa1 Interfata2 NMI = 4 6NPM = 9 8NPI = 5 6NPE = 4 2NPIE = 0 0

Referitor la metricile din categoria a doua, componenta construită expune două interfeţe şi are o singură metodă de iniţializare.

Se observă că o metodă poate face parte din mai multe interfeţe; de aceea, o agregare a metricilor anterioare la nivelul componentei nu are relevanţă, datorită riscului multiplicării valorilor pentru metodele prezente în mai multe interfeţe.

148

Utilizarea de componente

8.3 Biblioteci de componente

Pentru a fi distribuite şi utilizate, componentele sunt grupate în biblioteci de componente. Bibliotecile de componente sunt asemeni bibliotecilor de funcţii sau de clase. Deşi, în ambele cazuri, reutilizarea codului se face sub formă binară, prin specificul construcţiei lor, bibliotecile de componente, permit utilizarea unor funcţionalităţi scrise într-un limbaj de programare sau folosind o anumită tehnologie, în cadrul unor aplicaţii scrise folosind alte limbaje de programare sau tehnologii. În acest scop au fost dezvoltate mecanisme specifice prin care se realizează integrarea de componente în aplicaţii.

O bibliotecă de componente trebuie să îndeplinească o serie de criterii, ca să poate fi utilizabilă în mod corespunzător în aplicaţii:

• să fie generală: componentele din cadrul librăriei să ofere funcţionalităţi comune, generale;

• să fie consistentă: toate componentele din cadrul bibliotecii să se refere la un anumit subiect sau domeniu; de exemplu, o bibliotecă de componente pentru lucrul cu structuri de date conţine o componentă pentru lucrul cu masive, o componentă pentru lucrul cu liste simple şi dublu înlănţuite, o componentă pentru lucrul cu arbori;

• să fie documentată: bibliotecile sunt structuri binare reutilizabile. Dezvoltatorul nu are acces la codul sursă pentru a stabili care este interfaţa componentei pe care doreşte să o folosească şi nici nu poate ştii exact ce anume este implementat pentru o anumită funcţionalitate; de aceea orice bibliotecă trebuie însoţită de o documentaţie detaliată, care să ofere toate aceste informaţii astfel încât utilizarea ei să fie foarte facilă.

Proiectarea şi dezvoltarea bibliotecilor de componente se face într-o manieră asemănătoare cu proiectarea şi dezvoltarea bibliotecilor de funcţii şi clase pentru un limbaj de programare.

Construirea bibliotecilor de componente se realizează în mai multe moduri:

• arbitrar, atunci când componente potenţial reutilizabile sunt grupate în librării, iar reutilizarea lor depinde foarte mult de cunoştinţele despre ele ale programatorilor;

149

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

• bazat pe cerere, prin care dezvoltatorii de produse software sunt încurajaţi să solicite pieţei componente pentru diferite funcţionalităţi; de exemplu, se solicită o componentă pentru lucru cu masive bidimensionale. Dacă există suficient de multe cereri pentru o anumită componentă, atunci acea componentă este dezvoltată, la cerere şi înglobată într-o librărie;

• bazat pe dezvoltarea de către companie a unui set de componente, proprietare, pe care dezvoltatorii să le folosească în cadrul aplicaţiilor. Această abordare este de regulă la îndemâna firmelor puternice, care îşi pot permite să investească timp şi bani în dezvoltarea de librării de componente. Pe de altă parte, pentru produse specifice şi foarte pretenţioase, este necesar chiar ca firma să aibă propriile componente pentru funcţionalităţile critice, chiar dacă acestea ar putea fi achiziţionate din altă parte, tocmai pentru a avea un control mai bun asupra produsului.

8.4 Asigurarea interoperabilităţii prin utilizarea de componente

Obiectivul principal al dezvoltării de componente este de a permite reutilizarea unei anumite funcţionalităţi implementată într-un limbaj de programare, în cadrul unei aplicaţii dezvoltată în alt limbaj de programare. Decizia de a implementa şi folosi componente trebuie să se bazeze pe următoarele argumente:

• costul dezvoltării unei componente, raportat la numărul previzibil de utilizări ale acelei componente; dacă o componentă este dezvoltată spre a fi folosită doar în cadrul unei aplicaţii, atunci efortul necesar acestui proces este prea mare, pentru că tehnologiile folosite pentru construirea de componente, în general sunt complexe, nu sunt facil de manevrat şi pentru aceasta sunt necesari oameni bine pregătiţi. Dacă utilizarea ulterioară este foarte scăzută, se optează pentru o implementare clasică, orientată obiect a funcţionalităţii, cu riscul duplicării implementării pentru fiecare din utilizările ulterioare.

• există situaţia în care o anumită funcţionalitate este optim implementată folosind un anumit limbaj de programare; de

150

Utilizarea de componente

exemplu, lucrul cu fişiere sau cu periferice este mult mai rapid pentru programele dezvoltate în C/C++ decât Java sau C#. De aceea, se optează pentru dezvoltarea de componente în C++ care să lucreze cu periferice, la un nivel mai apropiat de hardware, urmând ca restul funcţionalităţii să fie dezvoltată într-un limbaj ca Java sau C#, din care se vor face apeluri către metodele componentei respective. Bineînţeles, această abordare este viabilă doar în situaţia în care câştigul de performanţă contează foarte mult în economia execuţiei aplicaţiei; dacă nu este atât de important acest câştig de performanţă, din nou, soluţia clasică, orientată obiect este mult mai puţin costisitoare.

Interoperabilitatea aplicaţiilor reprezintă capacitatea acestora de a comunica, de a partaja funcţionalitate şi informaţii, chiar dacă aplicaţiile rulează în medii diferite, au fost concepute şi realizate folosind limbaje de programare sau tehnologii diferite. Soluţiile de interoperabilitate sunt numeroase, dar de cele mai multe ori sunt specifice unui anumit grup de aplicaţii.

Soluţiile de tip componente au la bază un standard de implementare şi utilizare, care asigură o uniformitate în abordarea interoperabilităţii. Componentele sunt dezvoltate pe baza unor schelete bine stabilite, interfeţele sunt descrise prin instrumente standardizate, utilizarea componentelor în aplicaţii se face într-o manieră clară şi bine definită. Standardizarea mecanismelor de comunicare dintre componente are mai multe avantaje:

• din punct de vedere al dezvoltatorului, este suficient să cunoască un singur mecanism pentru a înţelege diversele implementări şi utilizări ale acestuia;

• din punct de vedere al producătorului de software, permite un mai bun management al aplicaţiilor pe care le produce şi le distribuie şi, implicit, costuri mult mai mici;

• din punct de vedere al calităţii aplicaţiilor, pentru că, deşi poate o soluţie particulară, specifică unui anumit context, este mai performantă, costul pierderii de performanţă este mult mai mic decât costul mentenanţei ulterioare a unei soluţii specifice, proprietare.

În ultima vreme, a apărut un nou tip de arhitectură de aplicaţii, care vizează reutilizare maximă de funcţionalitate şi permite interoperabilitate

151

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

facilă, şi anume arhitecturile orientate pe servicii. Serviciul este, de fapt, tot o componentă, însă, cu mecanismul de comunicare complet standardizat şi bazat pe un schelet mult mai uşor de folosit. Problema majoră a componentelor tradiţionale, de tip COM/DCOM, din punct de vedere al dezvoltării, o reprezenta complexitatea foarte mare a tehnologiei, lucru care era descurajant de multe ori pentru producătorii de software, puşi în situaţia de a decide utilizarea lor. Prin intermediul serviciilor, acest impediment este eliminat; dezvoltarea este mult mai uşoară, comunicarea cu un astfel de serviciu de asemenea. Pentru servicii, se foloseşte SOAP ca protocol de invocare a metodelor, folosit peste protocolul HTTP. SOAP se bazează pe XML pentru structurarea informaţiei transferate. În acest fel, nu mai sunt implicate mecanisme de comunicare specifice unui anumit sistem de operare, cum era cazul componentelor tradiţionale.

Din punct de vedere al unui dezvoltator, serviciile WEB sunt o extensie a tehnologiei distribuite. Practic, aplicaţiile sunt rezultatul combinării prelucrărilor interne şi prelucrărilor asigurate se serviciile WEB disponibile. Apare conceptul de „software ca serviciu” (software as service). Asemeni componentelor, serviciile WEB sunt încapsulate complet; dezvoltatorul care le foloseşte nu trebuie să cunoască detalii de implementare; funcţionalitatea expusă de serviciu este descrisă în interfaţa sa, prin intermediului unui limbaj specific, WSDL – Web Services Definition Language.

Conceptul de software ca serviciu contribuie foarte mult la depăşirea graniţelor de integrare, inclusiv în cadrul organizaţiei. Mulţi dezvoltatori şi arhitecţi de sistem văd serviciile WEB ca o opţiune excelentă pentru integrarea aplicaţiilor şi mult mai uşor de implementat decât soluţiile clasice bazate pe componente COM/DCOM de exemplu, sau cele orientate pe mesaje (comunicarea dintre aplicaţii se face prin intermediul unui strat intermediar de mesaje). Acest lucru devine foarte important când se pune problema integrării în extranet şi permite interoperabilitatea dintre diversele locaţii ale companiei sau dintre parteneri, clienţi şi furnizori. Dacă se expune funcţionalitate prin servicii WEB, bazate pe SOAP/XML şi descrise într-un fişier WSDL, oricine o poate accesa fără să fie nevoie de aplicaţii specifice care să se potrivească platformei de tehnologie folosită pentru dezvoltarea funcţionalităţii.

152

Prezentarea tehnicilor de programare are menirea de a evidenţia

evoluţia în timp a unui domeniu deosebit de important pentru tehnologia informaţiei. Trecerea de la o tehnică de programare la alta este rezultatul natural al acumulării de experienţă şi rezultatul evoluţiei structurilor hardware marcată de creşterea vitezei de calcul, creşterea capacităţii de memorare şi creşterea numărului şi tipurilor de echipamente periferice.

Toate acumulările evidenţiază tendinţa spre performanţă, spre creşterea productivităţii muncii şi creşterea calităţii produselor software. Indiferent de tehnica de programare întrebuinţată în dezvoltarea de aplicaţii informatice, un rol deosebit revine persoanelor – analişti şi programatori, care definesc probleme, proiectează soluţii şi dezvoltă cod sursă. În mod natural, dezvoltarea de aplicaţii informatice trebuie să urmeze cerinţele tehnologiei pe obiecte sau cele ale tehnologiei pe componente. În cazul în care apare necesitatea reutilizării de software, integrarea de componente elaborate folosind tehnici de programare mai vechi, devine eficientă dacă şi numai dacă aceste tehnici sunt cunoscute şi se operează pe textele sursă existente.

Cunoaşterea tehnicilor de programare permite identificarea unor părţi stabile care se regăsesc în toate structurile de programe şi stabilirea acelor părţi care diferă radical de la o tehnică la alta. Separarea acestora permite îndreptarea eforturilor spre a executa trecerea a cât mai multor componente dintr-o categorie în alta în vederea obţinerii unui echilibru cât mai stabil.

Există situaţii când sisteme informatice din generaţii mai vechi, aflate în exploatare curentă, trebuie reproiectate pentru a prelua facilităţile oferite de noile tehnologii. Existenţa de programe translatoare care automatizează procesul impune definirea unor parametri. Corectitudinea procesului de translatare depinde de nivelul de cunoaştere a inputurilor, adică de nivelul de cunoaştere a tehnicii sub care a fost dezvoltat sistemul informatic mai vechi.

Este important să se cunoască tehnicile de programare existente pentru a crea premisele evoluţiei spre alte noi tehnici, atunci când se identifică noi

153

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

cerinţe, noi exigenţe şi se stabilesc limitele prin care se caracterizează atât tehnicile de programare vechi, cât şi cele de programare noi.

Analiza complexităţii software realizată pentru implementările soluţiei problemei PROB folosind diferite tehnici de programare, a arătat ca există o influenţă a abordărilor pe care fiecare tehnică o promovează asupra complexităţii softwareului rezultat. Însă, de asemenea, trebuie remarcat faptul că, pentru anumite tehnici de programare cum este cea orientată obiect sau cea bazată pe componente, metricile clasice, cum sunt cele prezentate în această carte, respectiv complexitatea ciclomatică şi metricile Halstead, nu au relevanţă.

În tabelul 9.1 sunt sintetizate rezultatele obţinute pe codurile sursă ale implementărilor pentru diferitele tehnici de programare, ale problemei PROB.

Complexităţile implementărilor problemei PROB

Tabelul 9.1 Indicator

Tehnica de programare

Complexitate

ciclomatică

Dificultate

(Halstead)

Abordarea clasică 7 14,25 Programarea standard 14 3,3 Programarea structurată 12 15 15,38 20,08 Programarea modulară 15 20,08 Programarea orientată obiect 29 35,33 Programarea orientată pe componente * *

În cazul programării structurate, cele două variante corespund situaţiilor în care programul a fost dezvoltat ca un monolit, respectiv folosind funcţii.

Componentele sunt văzute ca şi cutii negre, astfel că importantă este funcţionalitatea expusă şi nu implementarea. Componentele pot fi scrise într-un limbaj de programare şi utilizate în aplicaţii dezvoltate folosind alt limbaj de programare. Prin urmare, analiza pe codul componentelor nu are relevanţă; în plus, în funcţie de diferitele tehnologii orientate pe componente, există elemente suplimentare de cod care apar în dezvoltare. Setul de metrici pentru evaluarea complexităţii componentelor nu conţine

154

Concluzii indicatori ce se aplică asupra codului sursă, mai ales că acesta poate fi rescris, fără a se afecta utilizarea componentei.

Se observă o creştere a complexităţii de la abordarea clasică către cea orientată obiect, creştere datorată specificităţii diverselor tehnici. Diferenţele de complexitate nu sunt mari, cel puţin în cazul primelor tehnici (clasică, standard, structurată, modulară). În cazul tehnologiei orientate obiect, există un salt semnificativ de complexitate, explicabil prin faptul că există multe construcţii de limbaj care aduc un plus de complexitate (de exemplu, existenţa constructorilor), dar şi prin faptul că metricile alese nu au mare relevanţă în cazul acestei tehnici de programare. De aceea, în practica curentă, se pune accentul pe alegerea unui alt set de metrici atunci când se discută despre complexitatea codului sursă orientat obiect sau despre complexitatea aplicaţiilor bazate pe componente, acest lucru fiind subliniat în capitolele 7 şi 8 ale lucrării.

Într-o analiză de acest tip, importantă este reprezentativitatea setului de date pe care se face analiza, respectiv codul sursă. Analiza efectuată în cadrul lucrării oferă indicii primare cu privire la relaţia dintre complexitate şi tehnica de programare folosită la dezvoltarea produsului, însă, pentru o analiză completă, se impune aplicarea acestor metrici pe un lot mai mare de probleme, pentru care să se construiască variante corespunzătoare tuturor tehnicilor de programare prezentate.

155

Anexa 1

P - denumire problemă pentru care se dezvoltă software Ai - element al unei colecţii de date (articol) F - fişier care memorează o colecţie de date (articole) LAi - lungimea în baiţi a unui element Ai lg(F) - lungimea totală a fişierului F, în baiţi pi - poziţia în fişier a unui element Ai ki - cheia elementului Ai Di - deplasarea elementului Ai faţă de începutul fişierului F xi - element al unui şir de numere S - suma elementelor xi ale unui şir Ij - instrucţiunea de pe poziţia j din program PR - program care rezolvă o problemă dată L - lungimea în număr de instrucţiuni a unui program na - numărul de arce din graful asociat programului PR nn - numărul de noduri din graful asociat programului PR CM - complexitatea software în sens McCabe n1 - frecvenţa operanzilor din program n2 - frecvenţa operatorilor din program CH - complexitatea în sens Halstead n - numărul de elemente ale unui şir de numere fi - frecvenţa de apariţie a elementului xi în şir x[i] - elementul de pe poziţia i din vectorul x xmin - valoarea elementului minim din vectorul x pozmin - poziţia din vectorul x pe care o ocupă elementul cu valoarea

minimă ei - eticheta i din program x[N] - masiv unidimensional cu dimensiunea maximă N a[M][N] - masiv bidimensional cu dimensiunile maxime M linii şi N

coloane x - media aritmetică a elementelor unui masiv unidimensional ik - variabilă de stare Pi - subproblemă a problemei P

156

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

SPi - subprogramul corespunzător subproblemei Pi Pij - subproblema j a subproblemei Pi in - numărul de instrucţiuni dintr-o secvenţă C1 - expresie condiţională S1 - secvenţa de instrucţiuni care se execută dacă expresia

condiţională C1 este adevărată S2 - secvenţa de instrucţiuni care se execută daca expresia

condiţională C1 este falsă i - variabila de control vinit - valoarea iniţială a variabilei de control vfin - valoarea finală a variabilei de control r - raţia S - secvenţa de instrucţiuni care se execută în cadrul structurii

repetitive S` - secvenţa de instrucţiuni care se execută după încheierea

execuţiei unei structuri repetitive C - expresia condiţională dintr-o secvenţă repetitivă DI - datele de intrare ale problemei DE - datele de ieşire ale unei probleme NE - numărul datelor de ieşire furnizate de produsul software Mi

j - modulul pentru obţinerea outputurilor DEi aflat pe nivelul j, care utilizează datele de intrare DEi

j SPi - subproblema i a problemei P S-SPij - subproblema j, a subproblemei i, a problemei P

157

[ATHAN95] Irina ATHANASIU, Eugenia KALISZ, Valentin CRISTEA –

Iniţiere în TURBO PASCAL, Bucureşti, Editura Teora, 1995

[BARBU97] Gheorghe BARBU, Ion VĂDUVA, Mircea BOLOŞTEANU – Bazele Informaticii, Bucureşti, Editura Tehnică, 1997

[BUDD97] Timothy BUDD – An Introduction to Object Oriented Programming, Second Edition, Addison-Wesley, 1997

[CATR94] Octavian CATRINA, Iuliana COJOCARU – Turbo C++, Bucureşti, Editura Teora, 1994

[CRIST98] Valeriu CRISTEA, Irina ATHANASIU, Eugenia KALISZ, Valeriu IORGA – Tehnici de programare, Bucureşti, Editura Teora, 1998

[GMICU02] Bodgan GILIC-MICU, Ion Gh. ROŞCA, Constantin APOSTOL, Marian STOICA, Cătălina COCIANU – Algoritmi în Programare, Bucureşti, Editura ASE, 2002

[GMICU03] Ion Gh. ROŞCA, Bogdan GHILIC-MICU, Cătălina COCIANU, Marian STOICA, Cristian USCATU – Programarea calculatorului. Ştiinţa învăţării unui limbaj de programare. Teorie şi aplicaţii, Bucureşti, Editura ASE, 2003

[IVANI98] Ion SMEUREANU, Ion IVAN, Marian DÂRDALĂ – Structuri şi obiecte în C++, Bucureşti, Editura Cison, 1998

[LOREN94] Mark LORENTZ, Jeff KIDD – Object-oriented software metrics: a practical guide, Englewood Cliffs, NJ PTR Prentice Hall, 1994

[MIHAL83] Rodica MIHALCA, Ion IVAN, Ioan ODĂGESCU – Programe applicative, Bucureşti, Editura ASE, 1983

158

Analiza comparată a complexităţii entităţilor text generate prin tehnici de programare

[MIHAL98] Rodica MIHALCA, Csaba FABIAN, Adina UŢĂ, Felix SIMION – Analiză şi proiectare orientate obiect. Instrumente de tip CASE, Bucureşti, Editura Societatea Autonomă de Informatică, 1998

[MSPCD97] Microsoft Press – Computer Dictionary, Third Edition, Microsoft Press, 1997

[ROŞCA94] Ion Gh. ROŞCA, Constantin APOSTOL, Valer ROŞCA, Bogdan GHILIC-MICU – Prelucrarea fişierelor în PASCAL, Bucureşti, Editura Tehnică, 1994

[ROŞCA03] Ion Gh. ROŞCA, Cătălina COCIANU, Cristian USCATU – Programarea calculatoarelor. Aplicaţii, Bucureşti, Editura ASE, 2003

[ROŞCA103] Ion Gh. ROŞCA, Bogdan GHILIC-MICU, Constantin APOSTOL, Valer ROŞCA, Cătălina COCIANU – Programarea calculatoarelor. Tehnica programării în limbajul PASCAL, Bucureşti, Editura ASE, 2003

[ROTAR96] Eugen ROTARU – Limbajul Java, Târgu-Mureş, Computer Press Agora, 1996

[SMEU01] Ion SMEUREANU, Marian DÂRDALĂ – Programarea în limbajul C/C++, Editura Cison, Bucureşti, 2001

[SMEU02] Ion SMEUREANU, Marian DÂRDALĂ – Programarea orientată obiect în limbajul C++, Bucureşti, Editura Cison, 2002

[SMEUR04] Ion SMEUREANU, Marian DÂRDALĂ, Adriana REVEIU – Visual C#. .NET, Bucureşti, Editura Cison, 2004

[SPIR95] Claudia SPIRCU, Ionuţ LOPĂTAN – POO Analiza, proiectarea şi programarea orientate obiect, Bucureşti, Editura Teora, 1995

[STROU87] Bjarne Stroustrup – The C++ Programming Language, Addison Wesley, 1987

159