CA-Visual Objects

László István

Subsistemul OOP

Acest material este o continuare a articolului despre mecanismele automate din CA-Visual Objects. Se presupune, că cititorul a avut contact cu un limbaj orientat obiect, măcar la nivelul obiectelor limbajului CA-Clipper.

În mare parte, subsistemul OOP al limbajului CA-Visual Objects se conformează standardului în cea ce privește, în general, limbajele orientate obiect. Totuși - parțial datorat prezenței unui depozit de date active - există anumite diferențe de implementare. Pe lângă aceste diferențe, mai puțin importante, CA-Visual Objects oferă o serie de noutăți excitante și soluții elegante, care nu pot fi regăsite la alte limbaje. Cei doi factori, prezența depozitului activ de date, respectiv noutățile prezente fac din CA-Visual Objects un limbaj promițător.

Merită menționat, de la început, că entitătile din CA-Visual Objects nu sunt delimitate de clauze de tipul BEGIN - END sau FUNCTION - RETURN. Entitătile sunt păstrate independent în depozitul de date al mediului (repository) -aceasta are implicația interesantă, pe de o parte că secventialitatea entităților nu are nici o importanță, pe de altă parte o entitate pur și simplu se termină acolo, unde începe o nouă entitate...

Noțiuni de bază

În CA-Visual Objects sunt prezente diverse clase predefinite care pot fi utilizate direct, dar și programatorului i se permite definirea claselor proprii. În acest sens, sunt disponibile toate mecanismele obișnuite și chiar și facilități noi în privința mecanismelor orientate obiect.

Declararea unei clase
O clasă este o colecție a obiectelor similare. Compilatorul trebuie să dețină informații asupra obiectelor cu care programele vor opera. (Evident, se poate închipui un sistem, care să manevreze cu definiții de clase în runtime...) Ca urmare, clasele într-un mod sau altul trebuie declarate. Comanda pentru a declara o clasă utilizator este

class MyClass

Clasa astfel declarată este cea mai simplă clasă, deoarece un obiect al acestei clase nu are nici atribute (numite variabile de instantă), nici rutine de servicii sau acțiuni asociate (numite metode).

O clasă sau o declarație singuratică de clasă nu are nici o utilitate, deoarece așa cum este nu poate să facă nimic; este doar o definiție a proprietătilor pe care le-ar avea un obiect al clasei, și comportamentul lui - dacă acest obiect ar exista...

Diferența între o clasă și un obiect este critică: pe când obiectul există în spatiu și în timp, clasa este o definiție abstractă, o rețetă sau un plan de a construi obiectul respectiv. Puțin simplificat , o clasă există în timpul compilării, și obiectele doar în timpul execuției programului.

Instanțierea obiectelor

Comanda executabilă de creare a unui obiect, sau a unei instanțe a unei clase, se dă prin numele clasei, urmat de acolade ({}). În contextul exemplului precedent:

MyClass{}

Ca urmare a acestei comenzi executabile, sistemul va crea un obiect, un exemplar al clasei MyClass. Se poate testa tipul rezultatului returnat, folosind funcția ValType().

? ValType( MyClass{} ) // "O" de la Obiect

Obiecte ale claselor predefinite pot fi instanțiate exact la fel, doar că nu se declară clasele respective - declarația lor se găsește în diverse biblioteci.

În anumite situații, obiectele create conform exemplului precedent sunt utile prin efectele laterale ale procesului de creare. Dacă în viitor se dorește accesarea obiectului creat, este nevoie de un mecanism prin care se asigură regăsirea obiectului respectiv.

Implementare referențială

Ca și rezultat al instanțierii unei clase, sistemul returnează o referință la obiectul creat; această valoare (referință) poate fi păstrată într-o variabilă. Operatiile pe obiecte se pot face numai prin variabile, care conțin o referință la obiectul respectiv.

oObiect := MyClass{}

Valoarea referinței este o valoare totalmente obișnuită, este de tipul „O“ (de la Object), ea poate fi atribuită unei variabile, transmisă unei funcții ca și parametru, sau returnată de aceasta.

Fără să intrăm în detalii, propun studierea următoarei secvențe de cod dat, pentru exemplificarea caracterului referențial:

local oOne, oTwo

oOne := MyClass{}
 	  oTwo := MyClass{}
 	  ? oOne == oTwo // afiseaza FALSE
 	  oTwo := oOne
 	  ? oOne == oTwo // afiseaza TRUE
 	  oTwo:MyExportVar := 10
 	  ? oOne:MyExportVar // afiseaza 10

Tipizarea variabilelor

Din motive de productivitate și eficiență, CA-Visual Objects suportă atât variabile netipizate, cât și variabile strict tipizate. (Parantezele drepte încadrează clauze opționale.)

local oObiect [as usual] 	 

Linia de mai sus declară o variabilă locală de tip nedefinit sau polimorfă. Compilatorul nu verifică și nu restricționează sub nici o formă tipul valorilor care pot fi păstrate în variabile polimorfe.

CA-Visual Objects oferă posibilitatea tipizării statice a variabilelor. În acest context, cuvântul static nu se referă la delimitarea vizibilitătii, ci la restricționarea definitivă doar la un singur tip a valorilor care pot fi păstrate în variabila respectivă.
Conform sintaxei de definire a funcțiilor, argumentele acestuia urmează aceleași reguli de tipizare ca și variabilele în general.

Variabilele utilizate, indiferent de valabilitatea lor, pot fi deci strict tipizate.

local oObiect as  	 

Pe lângă tipurile de bază ale limbajului (SYMBOL; INT, FLOAT, SHORT, LONG, BYTE, WORD, DWORD, REAL4, REAL8; DATE; LOGIC; VOID; PTR, PSZ, AS , AS PTR) ca și tip se acceptă și numele unei structuri sau - cea ce este important în acest context - și numele unei clase. Bibliografia citată (Edward Yourdon, Object-Oriented Systems Design - An Integrated Approach, [Prentice Hall, 1992] pag. 5) sintetizând ceea ce este comun la autorii diverselor cărți, definește noțiunea de clasă sau tipul obiectelor ca și sinonime pentru setul de obiecte cu caracteristici identice (aceleași atribute descriptive și același comportament). În acest mod, practic „se extinde“ limbajul, creând noi tipuri.

Tipul OBJECT este un tip de bază predefinit.

local oObiect as object 	 

Prin această comandă, se declară o variabilă, care poate păstra valori de tip OBJECT. Orice altă tentativă de atribuire se va solda cu eroare.

class MyClass1
 	  class MyClass2 	 
function Start
 	  local oObiect1, oObiect2 as 
 	  object
 	  oObiect1 := MyClass1{}
 	  oObiect2 := MyClass2{}
 	  oObiect1 := 4 // eroare de com
 	  pilare
 	  Process(oObiect2) // acceptat 	 
function Process(oObj as object)
 	  ... 	 

Problema este că, în acest fel, compilatorul nu va fi capabil să verifice cu exactitate clasa. Nivelul următor de severitate în declararea tipului se poate regăsi în exemplul de mai jos:

class MyClass1
 	  class MyClass2 	 
function Start
 	  local oObiect1 as MyClass1
 	  local oObiect2 as MyClass2 	 
 oObiect1 := MyClass1{}
 	  oObiect2 := MyClass2{}
 	  oObiect1 := oObiect2 // eroare 
 	  de compilare
 	  Process(oObiect2) // eroare 
 	  de compilare 	 
function Process1(oObj as 
 	  MyClass1)
 	  ...

Folosind o tipizare strictă, indiferent de nivelul de severitate, aceasta oferă nu numai un cod mai rapid, dar și posibilitatea eliminării unor erori latente, deja din timpul compilării.

function Process(oObj as object) 	 

Bineînțeles, și varianta mai slab tipizată are avantajele ei: în acest mod se permite definirea unui cod polimorf, care de exemplu poate prelucra obiectele diverselor clase.

 Process( MyClass1{} )
 	  Process( MyClass2{} ) 	 

În concluzie, se poate lucra cu următoarele feluri de variabile:
• variabile netipizate sau polimorfe (AS USUAL)
• variabile strict tipizate (AS )

Tipizarea variabilelor, care vor conține referințe la obiecte, poate fi făcută cu diverse grade de libertate:
• netipizat, sau declarat polimorf (AS USUAL)
• tipizat static ca și obiect în general (AS OBJECT)
• tipizat static corespunzător clasei (AS )

Subclasarea sau specializarea

În ideea refolosirii componentelor, una din proprietătile de importanță deosebită a limbajului este posibilitatea definirii unei clase, bazată pe definiția unei alte clase. În acest caz, clasele copil moștenesc toate atributele și comportamentul de la clasa părinte. Prin această tehnică, este posibilă extinderea unei clase, fără perturbarea funcționalității.

class MyClass
 	  class MyClass1 inherit MyClass
 	  class MyClass2 inherit MyClass

Variabilele de instanță

Proprietățile, atributele unui obiect se materializează în structuri de date de o complexitate oarecare. Fiecare exemplar (instanță) al clasei la crearea ei „primește“ o copie a întregului set de variabile. Declararea acestor variabile, adică a variabilelor de instanță se face la nivelul clasei, dar în acel moment (la compilare) reprezintă doar „șablonul“ după care se vor crea obiectele clasei respective.

class MyClass
 	  instance nVarNumerica [as ] 	 

Variabilele de instanță se conformează acelorași reguli de tipizare ca și toate variabilele: pot fi polimorfe, sau - folosind clauza opțională AS - pot fi și tipizate static (tip clasic sau tip extins, prin numele unei structuri de date - structure, union, sau prin numele unei clase).

Valoare inițială a variabilelor de instanță
Ca și la variabilele obișnuite, se permite inițializarea și a variabilelor de instanță deja la definirea lor, cu observația că, în acest scop, se pot folosi doar valori simple (deci de exemplu nu se poate inițializa cu valori de tipul OBJECT).

class MyClass
 	  instance nVarNumerica := 0 as short 	 

Variabilele tipizate, la crearea lor, automat se inițializează la valoarea nulă a tipului respectiv (NULL_ARRAY, NULL_CODEBLOCK, NULL_DATE, FALSE, NULL_OBJECT, NULL_STRING, NULL_SYMBOL, 0, NULL_PSZ, NULL_PTR); dacă variabila este polimorfă (AS USUAL), valoarea de inițializare este NIL (NIL este o valoare fără tip, în contrast cu VOID, care este un tip fără valori)

Valabilitatea și vizibilitatea variabilelor de instanță
Luând în considerare principiile încapsulării, capitolul privind vizibilitatea variabilelor de instanță primește o importanță deosebită.

Valabilitatea variabilelor de instanță este strict legată de valabilitatea obiectului din care fac parte, dar vizibilitatea lor este determinată de modificatorul aplicat la declararea lor.

[protect|hidden|export] 
[instance] [as ]

Vizibilitatea variabilelor de instanță din CA-Visual Objects se conformează tabelului următor:

Exterior
 	  în Clasă
 	  în Subclase
 	  EXPORT 
 	  (
 	  (
 	  (
 	  PROTECT
 	  (
 	  (
 	  HIDDEN
 	  (
 	  INSTANCE
 	  )
 	  ))))) 	 

Deoarece diverse obiecte ale aceleiași clase (sau al unei alte clase) au inevitabil variabile de instanță cu același nume, nu este suficientă specificarea numelui variabilei de instanță. Pentru a înlătura nesiguranța la referirea unei variabile de instanță, este nevoie și de specificarea obiectului de care aparține variabila. Pentru a referi o variabilă de instanță se folosește operatorul send (:).

class MyClass
 	  export MyExportVar as int 	 
function Start
 	  local oOne, oTwo as MyClass 	 
oOne := MyClass{}
 	  oTwo := MyClass{} 	 
oOne:MyExportVar := 10
 	  oTwo:MyExportVar := 49 	 
? oOne:MyExportVar // afiseaza 10 	 

Variabilele de instanță exportate fac parte din interfața publică a obiectelor, reflectă starea reală a acestora. Variabilele EXPORT care sunt deci vizibile codului exterior clasei violează principiul încapsulării, deoarece permit acces necontrolat la atributele obiectelor. Ca urmare, vom reveni cu o soluție la această problemă după o scurtă introducere a folosirii metodelor...

Metode

Comportamentul, serviciile specifice unei clase se materializează într-un grup de funcții sau așa-zise metode, care sunt strict legate de clasa respectivă.

O metodă conform definiției din CA-Visual Objects este codul unei singure acțiuni în comportamentul obiectului; în general, metodele sunt ca și funcțiile (au parametri, conțin declarații și instrucțiuni, comenzi și valori returnate), cu anumite diferențe:
• sunt definite în contextul unei clase
• în timpul execuției metodele „cunosc“, adică posedă informatii despre clasa de care aparțin, și pot accesa metodele și variabilele de instanță proprii și moștenite ale clasei.
• implicit o metodă returnează o referintă la obiectul însuși (vezi cuvântul cheie special SELF)

Definirea metodelor
Metodele unei clase se definesc similar cu funcțiile. Diferența evidentă la prima vedere este prezența unei clauze suplimentare în prima linie a definiției, clauză care se referă la clasa de care aparține metoda.

method MyMethod() 
 	  class MyClass
 	   	 

Corpul metodei poate conține și una sau mai multe comenzi RETURN [], a cărei prezență nu este necesară sau obligatorie.

Invocarea metodelor
Sistemul folosit pentru invocarea metodelor este sistemul mesajelor. Pentru a invoca o metodă a unui obiect (de fapt o metodă care este atașată clasei, pentru o instanță a clasei), se trimite un mesaj obiectului, folosind uzual operatorul send (:), precum și numele metodei, urmat de paranteze rotunde - încuibând o eventuală listă a parametrilor actuali.

// :([]) 	 
class MyClass 	 
method Music() class MyClass
 	  Tone(440,5)
 	  function Start
 	  local oOne := MyClass{}
 	  oOne:Music() 	 

La recepționarea unui mesaj, se execută metoda cu același nume - dacă aceasta există. Invocarea unei metode prin sistemul de mesaje este similară apelării unei funcții sau a unei subrutine.

Referințe la obiectul curent
În cadrul unei metode, se poate referi obiectul curent pentru care s-a invocat metoda, prin cuvântul cheie SELF.

class MyClass 	 
method MyMethod1 class MyClass
 	  ? "Sunt in prima metoda"
 	  MyMethod2() // 1
 	  self:MyMethod2() // 2 	 
method MyMethod2 class MyClass
 	  Qout("Sunt in a doua metoda") // 3 	 

Linia marcată cu 1 este eronată, deoarece - vezi liniile 1 și 3 - apelul unei metode se poate confunda cu apel de funcție. Soluția este folosirea unei referințe la obiectul curent SELF și a operatorului send (:) pentru a înlătura nesiguranța - vezi linia 2.

Merită făcută observația, că limbajul este consecvent în modul de invocare al funcțiilor și metodelor: funcțiile se apelează totdeauna prin simplul nume, iar metodele folosind o valoare de tip referință (variabilă sau SELF), operatorul send și numele metodei.

Valoarea returnată
Dacă nu se definește printr-o comandă explicită RETURN [], atunci prin definiție metodele returnează implicit valoarea SELF. În acest fel, este posibilă o înlănțuire, în aceeași comandă, a mai multor apeluri de metode:

oWindow:Line(oP1, oP2):Draw()

Vizibilitatea metodelor
Metodele unei clase sunt vizibile atât din afara clasei, cât și din celelalte metode ale clasei. Vizibilitatea, în general, determină din ce context cum pot fi invocate metodele. Vizibilitatea normală implică că toate metodele pot fi apelate din clasă și toate subclasele acesteia, precum și din modulele exterioare clasei.

Există două cuvinte cheie care pot fi folosite pentru a modifica vizibilitatea metodelor: PROTECT și HIDDEN. Metodele protejate sunt vizibile din clasa curentă și subclasele ei, iar cele ascunse doar din clasa pentru care s-a definit metoda.

Metode tipizate

Începând cu versiunea 2.0 CA-Visual Objects permite definirea și a metodelor strict tipizate. Metodele tipizate - cu totul, că flexibilitatea lor este mult mai mică, sunt utile deoarece prin folosirea lor se obține un cod mult mai stabil și mai rapid (legarea metodelor poate fi făcută timpurie, deja în timpul compilării).

Pentru a declara metodele tipizate, se folosește clauza opțională DECLARE a definiției de clasă:

declare method  

Definiția metodei se extinde cu specificarea tipului returnat, și a convenției de apel, ca și la funcții. (Datorită caracterului hibrid al limbajului, sunt prezente mai multe posibilităti de a declara modul de apel al funcțiilor și procedurilor. CALLBACK respectă convenția Windows, PASCAL asigură eficiență prin rigiditatea ei, respectiv CLIPPER oferă flexibilitatea obișnuită la limbajele Xbase.)

class MyClass
 	  declare method TypedMusic 	 
method TypedTone(wFreq as word, wDur as word) 
 	      as MyClass pascal
 	      Tone(wFreq, wDur)
 	  return self 	 
method UntypedTone(wFreq, wDur)
 	      Tone(wFreq, wDur)
 	  return self 	 

Avantajul variantei tipizate este viteza mai mare, respectiv siguranța, corectitudinea mărită a codului, față de flexibilitatea superioară a metodelor netipizate.

Metoda Init()
În limbajul CA-Visual Objects nu există o metodă specială predefinită sau definită de utilizator, de tip constructor. Alocarea memoriei necesare se face automat de sistem, programatorului i se ofer㠄doar“ posibilitatea definirii unei metode speciale Init(), care - dacă este definită - se invocă automat, imediat după crearea obiectului. Automat se transmit, ca și parametrii de apel ai metodei Init(), argumentele operatorului de instanțiere.

class MyClass 	 
method Init(arg1, arg2) class 
 	      MyClass
 	  ? arg1, arg2 	 
function Start()
 	  local oMyClass as MyClass
 	  oMyClass := MyClass{440, 20}
 	  // se tipareste 440, 20 	 

De obicei, metoda Init() este folosită la executarea unor inițializări (de exemplu, la setarea unor valori implicite pentru variabilele de instanță sau alocarea unor resurse - ca și deschiderea unui fișier - necesare funcționării obiectului).

Metoda Axit()
Controlul asupra procesului de creare a obiectelor este preluat de CA-Visual Objects, dar cum am mai precizat, definind o metodă cu numele Init(), utilizatorul poate executa funcții de inițializare sau alocare de resurse, imediat după crearea obiectului. Similar, procesul de distrugere a obiectelor în mod normal este controlat de sistem, dar utilizatorul primește dreptul de a executa anumite operațiuni, înainte de distrugerea obiectului în cauză.

De remarcat este, conform filozofiei CA-Visual Objects, că timpul de viață al unui obiect este strâns legat de posibilitatea de referențiere a ei. Adică, până când există o variabilă prin care obiectul poate fi referit, el există, dar odată cu dispariția tuturor referințelor asupra ei, obiectul este osândit distrugerii. Acest proces de distrugere este executat de un mecanism automat de colectare a reziduurilor.

Dacă utilizatorul definește metoda Axit(), aceasta este invocată automat, înainte de distrugerea obiectului.

class DBServer
 	  PROTECT cAlias as STRING 	 
method Init(cFileName) class 
 	  DBServer
 	  cAlias := cFileName
 	  use (cAlias) new 	 
method Axit() class DBServer
 	  (cAlias)->(dbCloseArea())

Extinderea funcționalității unei clase

Cum am mai discutat, obiectele au o parte statică - care se reflectă prin structură, prin relații și atribute; și o parte dinamică - oglindită prin comportament, sau mesaje și servicii (sinonimă: metode).

„Fiecare obiect are o interfață bine definită, care determină care sunt stimulii care sunt acceptați de obiect, adică ce operații pot fi efectuate asupra obiectelor. (...) Fiecare stimul cauzează executarea unei operații, sau accesează direct o variabilă de instanță.“

Folosind mecanismul subclasării, se asigură moștenirea structurii și comportamentului clasei părinte de către clasele copil.

Funcționalitatea noii clase se poate extinde prin
• definirea de noi variabile de instanță (reale sau virtuale)
• definirea unor metode noi
• extinderea funcționalității metodelor existente.

Variabilele virtuale

Din exteriorul unei clase doar variabilele de instanță EXPORT sunt vizibile, celelalte variabile interne sau neexportate sunt neaccesibile din funcții sau metode externe clasei. Pentru a accesa variabilele interne se pot folosi metode. Aceasta corespunde principiului încapsulării, dar este o soluție incomodă.

class Employee
 	  export Name
 	  protect Salary 	 
method SetSalary(x) class Employee
 	  if self:ValidSalary(x) // Vali
 	        darea valorii
 	      return (Salary := x)
 	  else
 	      self:Error(...)
 	      return nil
 	  endif 	 
method GetSalary() class Employee
 	  return Salary
 	  ...
 	  oEmp := Employee{}
 	  oEmp:Name := "Jones"
 	  oEmp:SetSalary(10000) // Sin
 	  taxa diferita!
 	  ? oEmp:Name
 	  ? oEmp:GetSalary() // Sin
 	  taxa diferita! 	 

O soluție mult mai elegantă - și extrem de flexibilă - este oferită de metodele speciale ACCESS și ASSIGN. Aceste metode se folosesc la implementarea variabilelor de instanță virtuale, care de fapt simulează prin intermediul metodelor ACCESS și ASSIGN o variabilă de instanț㠄obișnuită“.

Sintaxa pentru accesarea, respectiv pentru asignarea acestor variabile virtuale este aceeași ca și pentru o variabilă reală - singura diferență este că accesul prin acest mod devine controlat.

class Employee
 	  export Name
 	  protect Salary 	 
assign Salary(x) class Employee
 	  if self:ValidSalary(x) // Vali
 	        darea asignarii
 	      return (Salary := x)
 	  else
 	      self:Error(...)
 	      return nil
 	  endif 	 
access Salary() class Employee
 	  return Salary
 	  ...
 	  oEmp := Employee{}
 	  oEmp:Name := "Jones"
 	  oEmp:Salary := 10000 // Nu exista nici o
 	  ? oEmp:Name // diferenta la asignare
 	  ? oEmp:Salary // sau referentiere 	 

Cum se observă, se pot defini fără nici o restricție sau obligativitate metode ACCESS și ASSIGN cu același nume ca și variabila de instanță la care au rolul de a controla accesul.

Variabilele calculate

Dat fiind faptul că nu se impune ca să existe o variabilă concretă care să stea „în spatele“ metodelor de acces și asignare, se pot defini metode speciale care implementează variabile virtuale read-only, write-only, sau read-write; aceste variabile sunt vizibile doar, sau dacă doriți, există prin intermediul acestor metode speciale.

class Meeting
 	  protect cPerson as string
 	  protect dDate as date
 	  // Variabile virtuale
 	  //Data - R/W
 	  //Zi, Luna, An - R/O  	 
method Init(nume_persoana) class Meeting
 	  self:cPerson := nume_persoana 	 
assign Data(data_intilnire) class Meeting
 	  if (data_intilnire := Today())
 	      self:dDate := data_intilnire
 	  else // eroare
 	  endif 	 
access Data class Meeting
 	  return self:dDate 	 
access Zi class Meeting
 	  return Day(self:dDate) 	 
access Luna class Meeting
 	  return Month(self:dDate) 	 
access An class Meeting
 	  return Year(self:dDate)
 	  ...
 	  oMeeting := Meeting{}
 	  oMeeting:Data := 1997.10.14
 	  ? oMeeting:Zi // 14
 	  ? oMeeting:Year // 1997

Referința la superclasă

Cuvântul cheie SUPER servește la referirea predecesorului direct al unei clase. Prin acest cuvânt cheie, este posibilă referirea metodelor și a variabilelor de instanță vizibile ale superclasei (sau a părintelui).

Extinderea funcționalității metodelor

Dacă pentru o subclasă se definește o metodă cu un nume, care se suprapune cu denumirea unei metode a părintelui, pentru obiectele subclasei la un apel se va identifica și executa noua metodă. Practic noua metodă înlocuiește vechea metodă a superclasei. Prin aceasta, funcționalitatea clasei se modifică.

class MyParentClass
 	  method DoSomething() class MyParentClass
 	  ? "Face ceva cu MyParentClass." 	 
class MyClass inherit MyParentClassmethod 
     DoSomething class MyClass
 	  ? "Face ceva cu MyClass." 	 
function Start
 	  local oMyClass as MyClass
 	  oMyClass := oMyClass{}
 	  oMyClass:DoSomething()
 	  // Face ceva cu MyClass. 	 

Sunt situații, când se dorește - fără perturbarea funcționalității - o extindere a metodei redefinite. Pentru aceasta, nu este necesară cunoașterea structurii clasei-părinte: folosind cuvântul cheie SUPER, se poate executa și metoda superclasei.

class MyParentClass
 	  method DoSomething() class MyParentClass
 	  ? "Face ceva cu MyParentClass." 	 
class MyClass inherit MyParentClass
 	  method DoSomething class MyClass
 	  super:DoSomething()
 	  ? "Face ceva cu MyClass." 	 
function Start
 	  local oMyClass as MyClass
 	  oMyClass := oMyClass{}
 	  oMyClass:DoSomething()
 	  // Face ceva cu MyParentClass.
 	  // Face ceva cu MyClass. 	 

Folosirea acestei tehnici este singura posibilitate de a extinde funcționalitatea unei metode la o subclasă.

Exemplu clasic este metoda specială Init(), care trebuie să execute anumite inițializări la nivelul clasei curente - fără ca să se cunoască structura sau „nevoile“ superclasei.

class MyCild inherit MyParent
 	  // Structura lui MyParent nu este cunoscuta
 	  method Init(arg_list) class 
 	  MyChild
 	  super:Init(arg_list) // Init() al clasei MyParent
 	   // cod specific clasei MyChild 	 

Metodele NoIVarGet(), NoIVarPut() și NoMethod()

Pentru a detecta mesajele la care în mod direct nu poate răspunde clasa, sunt dedicate trei metode. În funcție de sintaxa prin care se trimite mesajul obiectului, se invocă metoda corespunzătoare.

NoIVarGet() se apelează la referirea pentru citire (operatia get) a unei variabile de instanță inexistentă conform sintaxei :

NoIVarPut() se apelează la referirea pentru atribuire (operatia put) a unei variabile de instanță inexistentă conform sintaxei : :=

NoMethod() se apelează la receptionarea de către un obiect a unei mesaj pentru care nu există metoda corespunzătoare, conform sintaxei :([])

Ideea de bază este ca, în cazul în care obiectul primește un mesaj pe care mecanismul automat nu îl poate controla direct, să nu se „anunțe“ direct sistemul de erori, ci să fie apelat un mecanism care poate decide comportamentul obiectului în cauză.

Obiecte cu interfață dinamică

O utilizare deosebit de interesantă a acestei tehnici, specifice limbajului CA-Visual Objects, este posibilitatea creării obiectelor care sunt capabile să-și „modifice“ structura sau comportamentul în runtime.

Ca și exemplu de valoare clasică, se poate prezenta problema reflectării structurii unei baze de date (.DBF) în obiectul de interfață, să o numim DBServer - în cazul în care structura fișierului de date nu este în prealabil cunoscută.

class DBServer
 	  protect cAlias as string 	 
method Init(cFileName, AliasName) 
 	  class DBFServer
 	  use (cFileName) alias (Alias
 	  Name) new
 	  self:cAlias := AliasName 	 
method NoIVarGet(SymName) class 
 	  DBFServer
 	  local symAlias as symbol
 	  symAlias := String2Symbol(cAlias)
 	  return FieldGetAlias(symAlias, 
 	  SymName) 	 
method NoIVarPut(SymName,uValue) 
 	  class DBFServer
 	  local symAlias as symbol
 	  symAlias := 
 	  String2Symbol(cAlias)
 	  return FieldPutAlias(symAlias,Sym
 	  Name,uValue) 	 
function Main()
 	  local oDBF as DBFServer
 	  oDBF := DBFServer{"\DEMO\ 
 	  TEST.DBF", "TEST"}
 	  ? oDBF:CIMP1, oDBF:CIMP2
 	  // citire
 	  oDBF:CIMP1 := "NetWorks Ltd." 
 	  // scriere

Operatori ca și metode

O funcționalitate extrem de interesantă, oferită de CA-Visual Objects, este posibilitatea extinderii elegante a sintaxei operatorilor folositi la tipuri de date simple și pe clase (tipuri) definite de utilizator.

Operatorii care pot fi redefiniți, respectiv corespondența dintre operatori și metodele care joacă rolul operatoarelor este determinată intern.

Lista metodelor dublate prin operatori, care pot fi definite pentru orice clasă, este: Add(), Mul(), Sub(), Div(), Pow(), Gtr(), Less(), GtrEqu(), LessEqu(), Neg(), Not()

În fond, pentru operatorii redefiniți se vor apela pur și simplu metodele corespunzătoare. Avantajul major al acestei tehnici este că acest apel se va face conform sintaxei operatorilor.

class Complex
 	  export re as float
 	  export im as float 	 
method Init(fReal, fImaginar) 
 	  class Complex
 	  self:re := fReal
 	  self:im := fImaginar 	 
method Add(b) class Complex 
 	  // redefineste +
 	  local z as COMPLEX
 	  z := Complex{SELF:re+b:re, 
 	  SELF:im+b:im}
 	  return z 	 
function Start()
 	  local z1, z2, z3 as COMPLEX
 	  z1 := Complex{1, -2}
 	  z2 := Complex{1, 3.5}
 	  z3 := z1 + z2
 	  ? z3:im // afiseaza 1.5 	 

Avantajul major al acestei tehnici este că se poate lucra mult mai natural cu obiecte, folosind sintaxa deja obișnuită a operatorilor pentru operații considerate clasice.

Informații de clase runtime

Interfața publică a unei clase poate fi interogată prin intermediul unor funcții speciale. În cele ce urmează, prezentăm câteva categorii - însă fără dorinta să fim compleți
• transmiterea mesajelor: Send(), SendClass(), asend(), ASendClass()
• informații despre clase: CheckInstanceOf(), ClassCount(), ClassList(), ClassName(), ClassTree(), ClassTreeClass(); IsAccess(), IsAssign(), IsClass(), IsInstanceOf(), IsInstanceOfUsual(), IsMethod(), IsMethodClass(), IsMethodUsual(), OOPTree(), OOPTreeClass()
• manipulare obiecte: CreateInstance(), Object2Array(), GetAppObject()
• prelucrarea variabilelor de instanță: IVarGet(), IVarGetInfo(), IVarGetSelf(), IVarList(), IVarListClass(), IVarPut(), IVarPutInfo(), IVarPutSelf(), NoIVarGet(), NoIVarPut()
• prelucrări metode: MethodList(), MethodListClass(), NoMethod()

Domeniul fiind prea vast, în acest moment nu intenționăm să intrăm în detalii.
Array-uri de obiecte

În cazul în care un vector conține obiecte similare, același mesaj poate fi trimis la toate obiectele vectorului printr-o sintaxă foarte simplă:

local aChildWindows as ARRAY 	 

Expresia

aChildWindows:Destroy() 	 

este perfect echivalentă cu următoarea secvență de comenzi

 for i := 1 to Len(aChildWindows)
 	      aChildWindows[i]:Destroy() 
 	  next i

Concluzii

Subsistemul obiectual al limbajului CA-Visual Objects este robust și comprehensiv. El se bazează pe sâmburele obiectual al limbajului CA-Clipper, și ca urmare este o extensie naturală a unui limbaj Xbase, dedicat prelucrării bazelor de date. Eficient și flexibil, este la fel de competitiv ca și celelalte limbaje obiectuale populare. Dar CA-Visual Objects este caracterizat de simplitate și de o eleganță deosebită.

Subsistemul obiectual al CA-Visual Objects, împreună cu mecanismele automate și serviciile oferite, îi conferă limbajului puterea necesară ca aceasta să poate aspira spre a deveni standard industrial.


BYTE România - decembrie 1997


(C) Copyright Computer Press Agora