La începutul anilor '70, un tânăr pe nume Alan Kay se prezintă la Xerox PARC cu o idee excentrică: un calculator mic, portabil, pe care să-l poată folosi oricine. Proiectul a căpătat numele Dynabook şi a început prin dezvoltarea softului. Inspirându-se din Simula, Lisp şi Logo, Kay a inventat Smaltalk, un limbaj nou, complet obiectual, pe baza căruia a dezvoltat un mediu cu o interfaţă grafică intuitivă, care se folosea de o altă noutate dezvoltată la Xerox PARC, un maus. Prima implementare s-a făcut pe nişte staţii de lucru individuale, numite Alto, care erau legate într-o reţea printr-o tehnologie inventată tot la Xerox PARC: Ethernet.
La ce-ar putea folosi? Xerox a invitat la Palo Alto (în 1980) un grup de manageri pentru a le prezenta tehnologia. Steven Jobs a fost singurul care a înţeles şi s-a inspirat copios din proiectul Dynabook. În 1984 apare Macintosh, primul calculator personal. Revoluţia a început de la Smalltalk.
Limbajul şi mediul
Este imposibil să vorbim despre limbajul Smalltalk fără să vorbim despre mediul de dezvoltare şi execuţie care-l însoţeşte. Integrarea acestora este mai profundă decât în cazul oricărui alt limbaj şi mediu de dezvoltare şi execuţie. Această integrare se datorează mai multor factori, dintre care cel mai important este faptul că mediul este scris în Smalltalk, astfel încât acesta respectă în totalitate "spiritul" limbajului. Aceasta conduce la un ansamblu perfect omogen (se mai spune "uniform"), care poate fi cu uşurinţă stăpânit şi dezvoltat în direcţia dorită.
Mediul Smalltalk este constituit dintr-o imagine virtuală (VI) şi o maşină virtuală (VM). Imaginea virtuală constă dintr-o colecţie (mai precis, o ierarhie) de clase definite în Smalltalk, colecţie care înglobează întreaga funcţionalitate a sistemului. În general, este vorba de un număr de clase de ordinul miilor. În acest context, important este că printre acestea se numără şi cele care implementează compilatorul, decompilatorul şi depanatorul. Compilatorul generează în mod dinamic cod într-un limbaj intermediar, numit byte-code, care este destinat maşinii virtuale.
Maşina virtuală (VM) constă dintr-un interpretor de byte-code, un mecanism de gestionare a memoriei (cu colectare automată a reziduurilor) şi un număr de metode de nivel jos, numite "primitive". Maşina virtuală este singurul element dependent de platformă, astfel încât portarea acesteia este echivalentă cu portarea întregului mediu Smalltalk.
Este cazul să remarcăm faptul că tehnologia Java a preluat în bună măsură această tehnică. Există chiar unele păreri conform cărora Java nu este altceva decât "Smalltalk transpus în sintaxă C".
Doar obiecte
Smalltalk este un limbaj obiectual "pur", în sensul că - din punctul său de vedere - nu există nimic altceva decât obiecte. Orice există este obiect.
Singurul lucru pe care ştiu să-l facă obiectele este să răspundă la anumite mesaje trimise de alte obiecte. Pentru a răspunde la aceste mesaje, obiectele realizează anumite procesări, trimiţând la rândul lor mesaje către diverse obiecte. Aici s-ar cere menţionat faptul că mesajele sunt, de fapt, nişte obiecte.
Un mesaj se compune dintr-un simbol - numit selector - şi, dacă este cazul, din unul sau mai multe argumente. Desigur, simbolurile sunt obiecte, iar argumentele... ce să mai vorbim...
Rolul selectorului este de a aduce la cunoştinţa obiectului receptor ce anume i se cere. Deoarece funcţionalitatea unui obiect este materializată prin metode, putem considera că selectorul este numele metodei care este responsabilă pentru furnizarea răspunsului solicitat de obiectul care a trimis mesajul. Dacă receptorul nu dispune de o metodă corespunzătoare mesajului primit, se produce o eroare.
Obiectele care răspund în acelaşi fel la aceleaşi mesaje sunt grupate în clase. Deoarece ar fi o imensă risipă ca fiecare obiect să poarte cu sine metodele corespunzătoare mesajelor la care răspunde, acestea aparţin de fapt clasei. Fiecare obiect este o instanţă a unei clase. Clasele pot fi considerate un fel de tipare structurale şi comportamentale pe baza cărora sunt create instanţele.
Clasele sunt şi ele obiecte, fiind instanţe ale unor metaclase. Acestea permit definirea comportamentului unei clase ca obiect. Astfel, crearea unei clase constă în instanţierea unei metaclase. Deoarece clasele sunt obiecte, ele dispun de propriile lor metode (numite în continuare "metode de clasă" ). Cele mai comune sunt cele prin care se creează instanţe (echivalente cu "constructorii" din unele limbaje hibride).
Existenţa metaclaselor este un ingredient important al caracterului reflexiv al limbajului şi al mediului. Aşa cum o clasă "ştie" totul despre comportamentul instanţelor sale, o metaclasă dispune de metode prin care poate obţine descrieri exhaustive ale claselor. Acest caracter reflexiv este cel care permite realizarea în Smalltalk a instrumentelor de dezvoltare de genul browser-ului de clase.
Pe de altă parte, reflexivitatea combinată cu caracterul dinamic al mediului face posibilă metaprogramarea. Se pot dezvolta programe care, de exemplu, pot defini sau modifica anumite clase, le pot compila şi instanţia, pot trimite mesaje şi pot controla fluxul acestora, totul într-o singură sesiune.
O ultimă precizare de ordin general. În Smalltalk totul este deschis. Aveţi acces la absolut tot codul, inclusiv la cel care implementează mediul de dezvoltare şi execuţie (mai puţin unele primitive de nivel jos ale VM). Puteţi studia fiecare clasă şi fiecare metodă, puteţi adăuga unele noi sau le puteţi modifica pe cele existente. Dacă vă tentează astfel de experimente, vă recomand să păstraţi la loc sigur o imagine "curată" a mediului.
Instrumente
Limbajul în sine este de o simplitate de-a dreptul neverosimilă. De fapt, întreaga putere este concentrată în ierarhia de clase iar programarea se bazează pe utilizarea acestor clase pentru a crea altele noi, care să corespundă scopului urmărit de proiectant.
Instrumentul de bază pe care mediul îl furnizează programatorului este browser-ul de clase, care permite examinarea claselor şi a metodelor, precum şi definirea altora noi. Toate mediile Smalltalk mai oferă încă (cel puţin) trei instrumente:
"Spaţiu de lucru" (workspace) - o fereastră în care putem scrie cod, pe care îl putem evalua (adică executa) sau inspecta. Se pot instanţia simultan mai multe spaţii de lucru, care servesc în primul rând pentru a testa mici porţiuni de cod. Facilităţile de editare sunt cele obişnuite într-un editor de text. Pentru a executa o porţiune de cod, aceasta trebuie selectată, după care i se pot aplica trei operaţii de bază: "evaluează" (Evaluate It sau Do It), "afişează" (Display It sau Show It sau Print It - ca şi Do It, dar forţează afişarea valorii returnate) şi "inspectează" (Inspect It - deschide un "inspector").
Transcript - este un spaţiu de lucru ceva mai specializat, în care se pot scrie mesaje. Lansarea mediului Smalltalk înseamnă de fapt deschiderea unei ferestre Transcript. Spre deosebire de spaţiile de lucru obişnuite, Transcript este unic.
"Inspector" de obiecte - un browser specializat care permite examinarea dinamică a variabilelor locale a obiectelor. Este util în depanare (dar majoritatea mediilor furnizează şi un debugger).
Desigur, diversele produse disponibile furnizează diverse alte instrumente sau funcţiuni, dar toate mediile dispun de aceste instrumente. Diferă şi ierarhiile de clase precum şi implementările diferitelor clase.
Exemplele din acest articol au fost rulate în mediul Dolphin Smalltalk de la Object Arts.
Indiferent de mediul cu care lucraţi, deschideţi un spaţiu de lucru, scrieţi textul:
Transcript show: 'Hello world'.
Selectaţi acest text (în Dolphin e suficient să plasaţi cursorul în linia respectivă) şi aplicaţi (din meniu) comanda Do It. În fereastra Transcript va apare textul:
Hello world
Ok. Primul pas a fost făcut. Funcţionează!
Elemente de sintaxă
Să analizăm puţin această primă instrucţiune. De fapt, nu am făcut altceva decât să trimitem obiectului Transcript mesajul show: Hello world. Întotdeauna receptorul este primul, urmat de selector şi eventual de argument.
Obiectul Transcript există cu siguranţă, fiind instanţiat automat la lansarea mediului. Mai corect ar fi însă dacă am spune că am trimis mesajul variabilei numite Transcript, care desemnează obiectul receptor. O convenţie importantă este că variabilele al căror nume începe cu o majusculă sunt globale, deci vizibile în tot sistemul (să notăm cu acest prilej că Smalltalk face diferenţa între majuscule şi minuscule). Deoarece în Smalltalk toate obiectele sunt unice, se alocă dinamic şi sunt identificabile, o variabilă nu este decât o referinţă la un obiect (deci nu stochează obiectul).
În privinţa mesajului, selectorul este show: iar argumentul este şirul de caractere Hello world. Ca şi în alte limbaje, anumite tipuri de obiecte pot fi exprimate direct, prin literali.
În Smalltalk se pot exprima prin literali:
numere: 12, 2.045, 8r377 (255 în baza 8), 1.586e3 (1586 în notaţie ştiinţifică). Aparţin claselor numerice corespunzătoare (descendente ale clasei Number).
caractere: $a, $A, '$+', $$ (se prefixează cu semnul $). Aparţin clasei Character.
şiruri de caractere: `Hello world`, `Acesta``i un string` (se delimitează cu apostrof). Aparţin clasei String.
simboluri: #add:, #>= (se prefixează cu semnul #). Aparţin clasei Symbol. Simbolurile trebuie să fie unice în sistem.
tablouri: '#(247, '''Salut!`', #Salut, #($a $b $c)). Se scriu între paranteze rotunde şi se prefixează cu semnul #. Pot cuprinde obiecte din orice clasă şi aparţin clasei Array.
tablouri de octeţi: #[255 0 0 23]. Se scriu între paranteze drepte şi se prefixează cu semnul #. Cuprind doar numere între 0 şi 255. Aparţin clasei ByteArray.
În fine, mai trebuie să remarcă punctul care încheie instrucţiunea. Rolul său este de a separa instrucţiunile. În acest caz, el nu ar fi fost neapărat necesar. Să considerăm însă exemplul următor:
Transcript show: 'Hello world'.
Transcript cr.
Transcript show: '(salutari din Smalltalk)'.
Transcript cr
Selectaţi rândurile şi evaluaţi-le. În Transcript va apărea:
Hello world
(salutari din Smalltalk)
În absenţa punctelor, Smalltalk n-ar fi înţeles ce-i cerem.
Dacă tot suntem la capitolul punctuaţie, e utilă precizarea că dacă vrem să trimitem mai multe mesaje succesiv aceluiaşi obiect (ca în exemplul de mai sus), putem folosi semnul ";" pentru a separa mesajele:
Transcript "acesta-i receptorul"
show: 'Hello world';
cr; "treci la linie noua"
show: '(salutari din Smalltalk)';
cr.
Indentarea nu este obligatorie, dar este recomandabilă (ca regulă de stil). Observaţi comentariile, scrise între ghilimele.
Pentru a încheia capitolul de sintaxă, mai trebuie să menţionez că, în afară de trimiterea unui mesaj către un obiect, în Smalltalk mai există (doar o) instrucţiune: atribuirea. Iată un exemplu:
unString := 'sir'.
În felul acesta, variabila unString va deveni o referinţă la un obiect, în speţă la o instanţă a clasei String care stochează şirul de caractere "sir". În stânga semnului de atribuire vom avea întotdeauna un nume de variabilă (nu trebuie declarat iar tipizarea este dinamică) iar în dreapta o expresie validă, adică un nume de variabilă, un literal sau o instrucţiune (adică trimiterea unui mesaj sau o atribuire). Iată deci un alt exemplu:
var := oClasa := unString class.
Depăşind deja domeniul sintaxei, putem analiza ce se întâmplă. Obiectului referit de variabila unString i se trimite mesajul class. Metoda care-l implementează este definită la nivelul clasei Object - rădăcina ierarhiei de clase din Smalltalk - astfel încât este moştenită de toate clasele. Deci orice obiect va putea răspunde la acest mesaj, furnizând o descriere a clasei căreia îi aparţine receptorul. În cazul nostru, variabilele var şi oClasa vor referi în urma atribuirii o instanţă a clasei Class (subclasă a clasei ClassDescription). Dacă vrem să aflam numele clasei căreia îi aparţine obiectul referit de variabila unString, putem executa (cu Display It) următoarea instrucţiune:
oClasa name
Răspunsul va fi cel aşteptat: #String.
Notă: la comanda Display It răspunsul va fi aşezat în spaţiul de lucru, după ultima instrucţiune evaluată. Pentru a simplifica expunerea, voi evidenţia răspunsurile sistemului prin notaţia '==>>'. Aşadar:
oClasa name ==>> #String
Tipuri de mesaje
Aţi observat deja, probabil, că există mai multe tipuri de mesaje. Să le luăm pe rând.
Mesaje unare
Aceste mesaje nu au argumente, deci se rezumă la un selector. Iată câteva exemple:
5 factorial ==>> 120
În acest exemplu, receptorul este 5 (o instanţă a clasei SmallInteger) iar mesajul este factorial. Răspunsul este, evident, factorialul lui 5. Metoda factorial este definită în clasa Integer, de unde este moştenită de SmallInteger.
Alt exemplu:
Date today ==>> 27 ianuarie 2000
De data aceasta receptorul nu este o instanţă, ci o clasă (Date - exprimă date calendaristice). Aşa cum spuneam, clasele sunt şi ele obiecte, deci pot răspunde la anumite mesaje. În acest caz, metoda today este o metodă "de clasă" (class method - vom discuta mai târziu) care determină crearea unei instanţe ce va reprezenta data curentă. Atenţie, este posibil ca în funcţie de mediul Smalltalk şi de sistemul de operare pe care le folosiţi să primiţi răspunsul în altă formă. De fapt, comanda Display It aplică oricărui obiect metoda displayString pentru a obţine o reprezentare textuală.
Mesaje binare
Sunt cele mai înşelătoare, deoarece par să se abată de la formatul standard "receptor selector argument". Iată câteva exemple (evaluate cu Display It):
7 + 3 ==>> 10
'abc' , 'xyz' ==>> 'abcxyz'
12 >= 11 ==>> true
De fapt singura "abatere" este faptul că selectorii sunt nişte simboluri care corespund unor operaţii binare. În primul caz, receptorul este o instanţă a clasei SmallInteger (numărul 7), selectorul este simbolul #+ (semnul plus) iar argumentul este un alt număr întreg (aici 3). Metoda numită + va returna suma.
În al doilea caz este vorba despre selectorul #, (semnul virgulă). Mesajul este trimis unei instanţe a clasei String şi va avea ca efect concatenarea şirului reprezentat de obiectul receptor cu argumentul.
În fine, al treilea exemplu constă din trimiterea mesajului #>= (mai mare sau egal) cu argumentul 11 către numărul întreg 12. Acesta va răspunde cu o valoare logică (instanţă a uneia dintre cele două subclase ale clasei Boolean).
Mesaje "keyword"
Mesajele de acest tip au întotdeauna cel puţin un argument. Selectorul poate fi format din mai multe părţi (corespunzătoare argumentelor), fiecare parte având ca sufix caracterul ":" (două puncte):
tablou := Array with: $A with: #A with: 'A'
Nu ne interesează în această discuţie decât partea dreaptă a atribuirii. De fapt, se trimite clasei Array un mesaj de tip keyword (cuvânt-chie) cu trei argumente. Ceea ce este interesant este că numele metodei care răspunde acestui mesaj este with:with:with:. Metoda clasei Array va crea o nouă instanţă, adică tabloul '#($A #A A)'. Altceva:
tablou at: 2 put: #B
tablou includes: #B ==>> true
Mesajul at:put: va înlocui elementul din poziţia 2 (indicele pleacă întotdeauna de la 1) cu obiectul furnizat ca al doilea parametru (#B). Metoda 'includes': stabileşte dacă obiectul furnizat ca argument figurează sau nu în tablou.
Secvenţe de mesaje
De foarte multe ori mesajele sunt înlănţuite, astfel încât obiectul returnat de o metodă să primească imediat un mesaj. Eventual, obiectul returnat de acest al doilea mesaj primeşte la rându-i un mesaj şi aşa mai departe. Să considerăm exemplul următor:
x := Set new add: 3.14 + 9 * 2.45 rounded
V-aţi putea aştepta ca variabila x să refere după atribuire un set având ca unic element numărul 25. Surpriza vine când vom constata că x a primit valoarea 24,28. Explicaţia acestui rezultat stă în ordinea în care Smalltalk evaluează expresiile. Iată regulile:
Evaluarea se face de la stânga spre dreapta.
Rezultatul trimiterii unui mesaj înlocuieşte mesajul.
Se evaluează mai întâi parantezele.
Mesajele unare se evaluează înaintea celor binare, iar cele binare înaintea mesajelor keyword.
O consecinţă extrem de importantă este că Smalltalk face abstracţie de ordinea operaţiilor aritmetice, aşa cum le-am învăţat la şcoală. Deci:
3 + 5 * 2 ==>> 16
3 + (5 * 2) ==>> 30
Putem scrie acum instrucţiunea de mai sus astfel încât să obţinem rezultatul aşteptat:
(x := Set new) add: (3.14 + (9 * 2.45)) rounded
Prima paranteză am pus-o pentru că metoda add: nu returnează setul la care s-a adăugat elementul furnizat ca argument, ci chiar argumentul. Paranteza va face ca prima operaţie să fie iniţializarea variabilei x cu un set vid. Acestui set i se trimite apoi mesajul add: 25, argumentul rezultând în urma evaluării expresiei aritmetice urmată de rotunjire.
O variantă care să evite prima paranteză ar putea fi:
x := Set new add: (3.14 + (9 * 2.45)) rounded; yourself
Mesajul yourself întoarce întotdeauna ca răspuns obiectul receptor, ceea ce rezolvă în cazul nostru problema.
Structuri de control
Spre deosebire de majoritatea limbajelor uzuale, structurile de control nu fac parte din limbaj. Nu există instrucţiuni de ramificare sau de iteraţie, toate acestea fiind implementate ca metode ale unor clase.
Ramificaţia fluxului de execuţie pe baza unei condiţii logice este apanajul a două clase specializate, descendente ale clasei Boolean, anume True şi False. Acestea au fiecare câte o singură instanţă.
Mecanismul ramificării se bazează pe blocuri de cod. Acestea sunt secvenţe de instrucţiuni (scrise între paranteze drepte) a căror evaluare este amânată. Desigur, blocurile sunt şi ele obiecte, instanţe ale clasei BlockClosure. Această clasă dispune de metoda value, care determină evaluarea instanţei receptoare.
Un exemplu va clarifica lucrurile:
x size >= 2
ifTrue: [Transcript show: 'Mai mult de un element']
ifFalse: [Transcript show: 'Doar un element'].
Considerând variabila x creată în secţiunea precedentă, evaluarea acestor linii va determina afişarea în fereastra Transcript a mesajului Doar un element.
Conform regulilor care stabilesc ordinea evaluării, mai întâi mesajul size va fi trimis obiectului desemnat de variabila x (un set având ca unic element numărul 25). Rezultatului (numărul întreg 1) îi va fi trimis mesajul >= 2, răspunsul fiind instanţa unică a clasei False. Acesteia i se va trimite mesajul ifTrue:ifFalse: având ca argumente cele două blocuri de cod. Metoda corespunzătoare (poate fi studiată cu browser-ul de clase) nu face altceva decât să trimită celui de-al doilea operand mesajul value, ceea ce determină evaluarea acestuia, cu rezultatul pe care l-am menţionat.
Clasa Boolean defineşte interfaţa (protocolul) pentru metodele care se folosesc pentru controlul fluxului de execuţie prin ramificare: ifTrue:, ifFalse:, ifTrue:ifFalse şi ifFalse:ifTrue:, and: şi or:. Clasa Boolean este însă o clasă abstractă (care nu poate fi instanţiată) şi toate aceste metode sunt abstracte, implementarea lor fiind făcută în subclasele True şi False.
Interesante sunt metodele and: şi or:, care se deosebesc de operatorii logici binari corespunzători (notaţi cu #& şi #|). Aceste metode primesc ca argumente tot blocuri, pe care le evaluează doar dacă este cazul (dacă rezultatul nu poate fi determinat doar pe baza receptorului - evaluare "scurtă"). Astfel, următoarele trei instrucţiuni sunt echivalente:
(x size >= 2) not and: [Transcript show: 'Doar un element'].
x size >= 2 or: [Transcript show: 'Doar un element'].
x size >= 2 ifFalse: [Transcript show: 'Doar un element'].
Dacă ramificarea se face doar pe baza claselor descendente din Boolean, iteraţia se poate face pe baza mai multor clase.
Clasa Integer (şi descendentele) furnizează metoda timesRepeat:, care permite evaluarea repetată a unui bloc:
count := sum := 0.
7 timesRepeat: [count := count + 1. sum := sum + count]
Ca urmare a secvenţei de mai sus, variabila count va indica numărul întreg 7 iar variabila sum va reprezenta suma numerelor întregi de la 1 la 7, adică 28
O metodă ceva mai elaborată de iteraţie este oarecum asemănătoare cu instrucţiunea for din limbajele uzuale:
sum := 0.
1 to: 7 do: [ :i | sum := sum + i]
Cu această ocazie facem cunoştinţă şi cu blocurile cu argumente. Argumentul este de fapt o variabilă temporară, este declarat la începutul blocului, este prefixat (doar în declaraţie) de caracterul ":" şi este separat de restul blocului prin caracterul "|" (bară verticală). În acest caz, blocul va fi evaluat pentru fiecare a valoare argumentului de la 1 la 7. Şi de această dată, variabila sum va fi 28.
Observaţie: Spre deosebire de timesRepeat, metodele to:do: şi to:by:do: (unde by: specifică pasul) aparţin clasei Number. De asemenea, argumentele to: şi by: pot fi la rândul lor orice valori numerice.
Este interesat de remarcat că exact acelaşi rezultat îl vom obţine pentru următoarea secvenţă:
sum := 0.
(1 to: 7) do: [ :i | sum := sum + 1]
Explicaţia este că numerele dispun de metoda to:, care returnează o instanţă a clasei Interval, care este o colecţie şi care dispune în consecinţă de metoda de enumerare do:. De altfel, metodele to:do: şi to:by:do: pentru numere sunt implementate în acest mod.
O a treia variantă de iteraţie este cea care condiţionează evaluarea repetată a unui bloc de satisfacerea unei condiţii. Metodele whileTrue: şi whileFalse: aparţin clasei BlockClosure şi sunt exact ceea ce căutam:
sum := i := 0.
[i < 7] whileTrue: [i := i + 1. sum := sum + i]
În fine, o ultimă variantă de iteraţie se bazează pe enumerarea colecţiilor. Pe lângă metoda do: pe care am menţionat-o deja, există metode diverse de enumerarea. Iată un exemplu:
tablou := #(1 2 3 $A 4 5 6 7).
sum := 0.
(tablou select: [:elem | elem isKindOf: Number]) do:
[:elem | sum := sum + elem]
Metoda select: returnează o nouă colecţie, compusă doar din acele elemente care corespund criteriului furnizat ca argument, prin intermediul unui bloc. În exemplu, condiţia impune ca fiecare element să fie o instanţă a clasei Number sau a descendenţilor acesteia. Colecţiei obţinute i se aplica metoda do: şi, desigur, variabila sum va indica din nou 28.
Asemănătoare sunt şi metodele reject:, detect: şi collect:.
Clasele ca tipare
Până în acest moment am considerat suficientă o definiţie minimală a noţiunii de clasă ("tipar structural şi comportamental pentru instanţele sale"), cu atât mai mult cu cât aceasta corespundea imaginii intuitive pe care cititorii şi-au format-o din experienţa utilizării altor limbaje obiectuale, mai puţin "pure". Am amintit în treacăt şi despre metaclase şi faptul, oarecum paradoxal, că o clasă este de fapt un obiect, o instanţă a unei clase.
Deoarece clasa este noţiunea centrală în Smalltalk şi întreaga muncă de programare constă în crearea unor noi clase, merită să discutam puţin mai în detaliu subiectul. Să începem cu un exemplu:
Object subclass: #Persoana
instanceVariableNames: 'nume adresa data'
classVariableNames: ' '
poolDictionaries: ''.
În felul acesta am creat o nouă clasă, numită Persoana. Această clasă este descendentă a clasei Object (rădăcina ierarhiei de clase). În practică, crearea unei clase se face în mod interactiv, cu ajutorul browser-ului, dar până la urmă sistemul ajunge de fapt să evalueze o expresie Smalltalk de genul celei de mai sus.
Deoarece clasele sunt vizibile în întreg sistemul, numele clasei (un simbol) trebuie să înceapă cu majusculă.
Al doilea argument ne permite să specificăm numele variabilelor instanţelor clasei definite. Fiecare instanţă a clasei va avea aceste variabile, care vor fi locale, deci vizibile doar pentru metodele clasei (ceea ce asigură încapsularea). Din acest motiv este recomandabil ca numele lor să înceapă cu literă mică.
Privind clasa doar ca tipar pentru instanţe, singurul lucru pe care-l mai avem de făcut este să definim metodele care să-i confere funcţionalitatea dorită. Pentru aceasta folosim browser-ul de clase. Se selectează clasa dorită (în acest caz, Persoana), se selectează panoul claselor de instanţă şi se comandă crearea unei noi metode (în Dolphin, Method -> New...). În partea inferioară a browser-ului se introduce codul:
nume: unNume
"Stabileste numele unei persoane"
nume := unNume
Prima linie cuprinde selectorul şi eventualele argumente (parametrii formali). Numele metodei se deduce din numele selectorului declarat (în acest caz nume). De regulă, după primă linie este urmată de un comentariu cu privire la rolul metodei şi un rând liber, urmat de codul propriu-zis. În acest caz codul e banal: variabilei nume a instanţei i se atribuie valoarea furnizată prin argument.
În general metodele efectuează procesări elementare şi arareori ajung la zece linii de cod. Iată un exemplu şi mai simplu:
nume
"Returneaza numele persoanei"
^nume
De data aceasta, metoda răspunde unui mesaj unar. Cu această ocazie facem cunoştinţă cu încă un element sintactic: semnul "^" plasat înaintea unei expresii determină oprirea evaluării şi returnarea valorii furnizate de expresia respectivă. De remarcat că orice metodă Smalltalk returnează o valoare. Dacă nu este explicit indicată prin semnul "^", atunci e valoarea de retur a ultimei instrucţiuni evaluate.
Desigur, se pot crea în mod asemănător diverse metode. Metodele se grupează după funcţionalitate. Un grup de metode care asigură o anumită funcţionalitate formează un protocol. Metode de genul celor exemplificate fac parte din protocolul de acces, deoarece furnizează mecanismele prin care se pot stabili şi consulta variabilele instanţelor. Câteva exemple:
adresa: oAdresa
adresa := oAdresa
nume: unNume adresa: oAdresa
self nume: unNume.
self adresa: oAdresa
data: oData
oData class = #Date
ifTrue: [data := oData]
ifFalse: [data := Date fromString: oData]
virsta
"Furnizeaza vârsta persoanei în ani"
| azi |
azi := Date today.
^azi year - self data year
În cazul metodei adresa: nu e nimic de comentat. În cazul metodei nume:adresa: se remarcă folosirea unei variabile speciale, numită self, care se referă întotdeauna la instanţa care primeşte mesajul. Este evident că puteam să evităm folosirea acesteia, făcând atribuirile direct variabilelor instanţei, ca în cazul metodelor nume: şi adresa:. Regulile de stil ale limbajului ne îndeamnă să implementăm o singură dată fiecare operaţie elementară, după care să folosim în mod sistematic această implementare.
Observaţie: mai există o variabilă specială, numită super, care se referă clasa imediat superioară în ierarhie, pentru a permite folosirea metodelor acesteia.
Metoda data: ne permite să setăm variabila corespunzătoare indiferent dacă data este furnizată ca instanţă a clasei Date sau ca şir de caractere.
Metoda virsta nu va funcţiona (încă), deoarece nu am definit metoda data (e banală). Am fi putut accesa variabila data, dar am fi încălcat regula. Desigur, am fi putut să rafinăm lucrurile, astfel încât să obţinem numărul de ani împliniţi la momentul evaluării. Oricum, să remarcăm modul în care se declară variabilele temporare: la începutul codului metodei, între bare verticale. Variabila azi va exista doar pe durata evaluării masajului virsta. Pot fi declarate mai multe variabile temporare (în cazul acesta nici n-ar fi fost nevoie, am introdus-o doar pentru exemplificare).
Încă ceva legat de variabilele temporare. Nu doar metodele pot dispune de variabile temporare, ci şi blocurile sau "bucăţile" de cod dintr-un spaţiu de lucru. Ca orice variabile, acestea nu sunt tipizate. Sunt iniţializate automat cu valoarea nil, semnificaţia acesteia fiind uşor de dedus.
Clasele ca obiecte
Am văzut cum se defineşte tiparul structural şi comportamental al instanţelor, dar încă nu dispunem de mecanismul prin care să creăm instanţe pe baza acestuia. Lucrurile sunt însă destul de simple, deoarece clasele sunt şi ele obiecte, deci pot avea şi ele metode. Unele dintre aceste metode sunt moştenite. Printre acestea se găseşte întotdeauna metoda new, care creează instanţe. Deci, într-un spaţiu de lucru, următoarea secvenţă funcţionează:
mircea := Persoana new.
mircea nume: 'Mircea' adresa: 'Tg-Mures'
Elegant ar fi însă dacă am crea o metodă care să şi iniţializeze instanţa nou creată. Cu ajutorul browser-ului, selectăm clasa Persoana şi avem grijă ca panoul metodelor să indice metodele de clasă (nu de instanţă):
nume: unNume adresa: oAdresa data: oData
^self new
nume: unNume
adresa: oAdresa;
data: oData
Atenţie la punctuaţie. Self se referă la clasa Persoana, care primeşte mesajul new şi retrurnează o instanţă nouă. Aceasta primeşte mesajul nume:adresa:, care este definit pentru instanţe. Tot această instanţă primeşte apoi (în cascadă) mesajul data:.
Acum putem crea un obiect Persoana într-o manieră mai consistentă:
mircea := Persoana nume: 'Mircea' adresa: 'Tg-Mures' data: '13 Mar 1960'
De remarcat că o clasă moşteneşte automat toate variabilele de instanţă şi metodele superclasei sale. Acestea pot fi suprascrise (overriden) pentru a fi adaptate nevoilor specifice. De pildă, presupunând că am adăugat metodele adresa şi data, putem redefini operatorul de egalitate (este o metodă de instanţă):
= oPers
self == oPers
ifTrue: [ ^true ]
ifFalse: [
^self nume = oPers nume &
self adresa = oPers adresa &
self data = oPers data ]
Fiind obiecte, clasele dispun deci de metode proprii. De ce n-ar dispune atunci şi de variabile proprii? Într-adevăr, există şi aşa ceva. Variabilele de clasă (class variable) sunt disponibile atât pentru metodele clasei cât şi pentru metodele instanţelor. Mai mult, ele sunt disponibile şi pentru toate subclasele şi instanţele acestora. Aceste variabile sunt asemănătoare variabilelor globale (motiv pentru care numele acestora începe cu majusculă), dar vizibilitatea lor este controlată, astfel încât să se evite efectele laterale. Desigur, variabilele de clasă nu fac parte din "tiparul" folosit pentru crearea instanţelor.
Pentru exemplificare să considerăm o clasă standard care exprimă date calendaristice. Clasa Date este o subclasă a clasei abstracte Magnitude, rădăcina comună a tuturor claselor care exprimă mărimi, de la care moşteneşte metode de comparaţie (unele dintre ele abstracte):
Magnitude subclass: #Date
instanceVariableNames: 'days '
classVariableNames: 'DaysInMonth DaysUntilMonth DefaultLongPicture'
poolDictionaries: 'Win32Constants'
În implementarea Dolphin, data se exprimă ca numărul de zile trecute de la o dată de bază (ziua 0), anume 1 ianuarie 1900. Observăm că sunt definite mai multe variabile de clasă, a căror semnificaţie este uşor de dedus.
Variabila DayInMonth este de fapt un tablou de 12 întregi care consemnează numărul zilelor din fiecare lună a unui an care nu este bisect. Această variabilă este mai degrabă o constantă, dar se pot imagina şi variabile propriu-zise de clasă. În general, dacă o clasă dispune de variabile de clasă, dispune şi de o metodă (de clasă) care să le iniţializeze:
Date class >> initialize
self defaultLongPicture: true.
"Days indices are for non-leap years"
DaysInMonth := #(31 28 31 30 31 30 31 31 30 31 30 31).
DaysUntilMonth := #(0 31 59 90 120 151 181 212 243 273 304 334 ).
Observaţie: notaţia ">>" este folosită doar extern, pentru a indica codul sursă a unei metode.
Variabila DayInMonth poate fi folosită atât de metode de clasă cât şi de metode ale instanţelor. Recomandabil este însă ca accesul la acestea să fie izolat în metode de clasă, care să fie apoi apelate de metodele instanţelor. De pildă, metoda de clasă daysInMonthIndex:forYear: aplică corecţia corespunzătoare anilor bisecţi, oferind astfel o interfaţă unică pentru toate metodele instanţelor:
Date class>>daysInMonthIndex: monthIndex forYear: yearInteger
| days |
days := DaysInMonth at: monthIndex.
monthIndex == 2
ifTrue: [ days := days + (self leapYear: yearInteger) ].
^days
Notă: mesajul "==" stabileşte dacă două obiecte sunt identice.
Cu toate avantajele pe care le oferă, există o situaţie în care variabilele de clasă nu sunt potrivite. Este cazul în care dorim ca fiecare subclasă să dispună de propriile variabile, care să nu interfereze cu variabilele de clasă ale superclaselor. Pentru astfel de situaţii, Smalltalk permite declasarea unor variabile speciale, numite class instance variable (greu de tradus... poate "variabile ale claselor ca instanţe").
Să presupunem că am definit o clasă abstractă numită Dictionar, dotată cu metodele necesare pentru verificarea lexicală a textelor. Am vrea ca, folosind diverse seturi de cuvinte, să creăm subclase specializate pentru diverse limbi. Putem folosi o astfel de variabilă class instance, să o numim lexic, în care fiecare subclasă va plasa propria sa colecţie de cuvinte. Astfel, putem defini subclasele DictionarRoman şi DictionarEnglez, instanţele acestora lucrând implicit cu setul corespunzător de cuvinte (prin intermediul unor metode de clasă, deoarece doar acestea pot accesa aceste variabile).
În loc de exemple
În mod obişnuit, articolele în care prezentăm limbaje de programare se încheie cu un exemplu ceva mai amplu, dacă se poate complet, care să ilustreze facilităţile limbajului pe un caz concret. În cazul lui Smalltalk, situaţia este puţin diferită, deoarece o aplicaţie se realizează pe baza unei paradigme numită Model-View-Controller, care este subiectul unui articol viitor. În schimb, orice mediu Smalltalk vă oferă, ca exemplu, întreaga sa construcţie, pe care o puteţi studia fără oprelişti.
Vă sfătuiesc să începeţi cu clasele care implementează mărimi (Magnitude) şi colecţii de obiecte (Collection). Sper că aceste pagini vă vor fi de folos. Succes!