vineri, 26 octombrie 2012

Emularea polimorfismului în PHP


        Supraîncarcarea (overloading) si redefinirea/suprascrierea (overridding) reprezinta cele doua forme de manifestare ale polimorfismului, unul din cele trei concepte-cheie ale paradigmei obiectuale în programare.
        Supraîncarcarea reprezinta capacitatea unei functii/metode sau a unui operator de a se adapta dinamic unui anumit context. Pentru a întelege mai bine acest concept, voi folosi o metafora din lumea înconjuratoare. Sa presupunem ca un „obiect medical” este înzestrat cu „metoda” „acorda primul ajutor”. În functie de contextul în care se aplica, aceasta metoda poate însemna cu totul si cu totul altceva, de exemplu masaj cardiac, respiratie artificiala, oprirea hemoragiei, sau multe alte lucruri. În viata de toate zilele spunem ca expresia sau cuvântul care defineste „metoda” noastra este polisemantic – are mai multe întelesuri, care depind, evident, de context. Tot astfel, în programarea orientata pe obiecte, o functie – voi folosi, în continuare, acest termen, deoarece el este uzitat în PHP – este supraîncarcata daca „stie” sa faca mai multe lucruri, în functie de context. Aceasta presupune fie faptul ca poate primi parametri de diferite tipuri, fie ca poate primi un numar variabil de parametri, fie amândoua. Sa presupunem, de exemplu, ca avem o functie scurteaza() care, daca primeste un parametru numeric real, extrage partea sa întreaga (deci scurteaza(3.14) == 3), iar daca primeste un string, elimina spatiile goale (deci scurteaza( ”un text oarecare”) == ”untextoarecare”) – acesta este un exemplu de functie care „stie” sa faca lucruri diferite în functie de tipul parametrului primit. Un exemplu de functie care accepta un numar variabil de parametri este usor de imaginat, de exemplu o functie aduna() ar putea fi conceputa astfel încât sa accepte oricâti termeni.
        Suprascrierea reprezinta capacitatea unei functii mostenite a unui obiect de a face altceva decât functia similara din clasa parinte. De exemplu, considerând clasa ”lumea vie”, vom constata ca trebuie sa îi implementam o functie de „hranire”, fiindca orice instanta a ei – adica orice organism viu – are aceasta functie. Presupunând ca am alcatuit o ierarhie de clase în care clasa lumea_vie a fost extinsa succesiv, astfel încât a rezultat o ierarhie de clase precum lumea_vie => lumea_animala => vertebrate => mamifere => hominide => om, vom constata ca functia „hranire” a clasei „om”, chiar daca „mosteneste” anumite caracteristici (=comportament) functia similara a clasei parinte, trebuie, totusi, sa îl modifice, pentru a implementa „abilitatea” omului de a se hrani cu alimente procesate, ca si pentru a renunta la unele deprinderi precum a ceea de a scuipa cojile semintelor pe jos. În acest sens, în functie de modul în care este conceputa, functia „hranire” a omului va prelua o parte din „codul” functiei „hranire” a hominidelor, va renunta la alta parte, si va modifica o alta parte. Putem sa ne imaginam, de asemenea, ca din clasa „lumea_vie” deriva si clasa „lumea_vegetala”, care, bineînteles, trebuie sa aiba definita si ea functia „hranire”. Numai ca diferentele între ceea ce se întelege între a „hrani” o planta (de ex. a o stropi, de a o orienta spre soare etc.) si a „hrani” un animal sunt atât de mari, încât, poate ca nu exista niciun element comun, care sa poata fi mostenit atât de clasa „lumea_animala”, cât si de clasa „lumea_vegetala”. În acest caz, solutia este aceea a definirii unei functii abstracte (si implicit, declararea ca abstracta a clasei „lumea_vie”), fara niciun fel de cod, lasând claselor derivate responsabilitatea de a defini, în mod concret, functia „hranire”. În alte cuvinte, functia „hranire” nu spune si nu face nimic despre ea însasi în clasa „lumea_vie”, dar obliga orice „obiect” de acest fel sa aiba o astfel de functie, care sa fie (re)definita în clasa de care apartine: toate organismele vii (= instante ale unor clase concrete derivate din clasa „lumea_vie”) sunt „obligate” sa se „hraneasca”, dar, ce înseamna, la modul concret, pentru fiecare organism viu în parte, este definit la nivelul clasei de care apartine.
        Dupa cum se stie, PHP nu a fost conceput, de la bun început, ca un limbaj obiectual, ci ca unul structural, adica unul in care codul este scris respectând paradigma programarii structurate Böhm-Jacopini. De aceea, creatorii limbajului PHP au ales sa îl faca loosely-typed, pentru un spor de versatilitate si eficienta. Pe de alta parte, aceasta optiune a facut, practic, imposibila, supraîncarcarea functiilor, cel putin pe baza tipului parametrilor.
        Sa consideram urmatorul exemplu:
<?php
class test {
    public function afiseazaMesaj1($un_parametru) {
        echo "Apelul functiei pentru un parametru: $un_parametru! <br/>";
    }

    public function afiseazaMesaj2($par1, $par2) {
        echo "Apelul functiei pentru doi parametri: $par1 $par2";
    }
}

$obj1 = new test;
$obj1->afiseazaMesaj1('Salut');
$obj1->afiseazaMesaj2('Salut,','lume!');
?>
        Dupa cum, probabil va asteptati, acest exemplu functioneaza perfect în PHP, producând iesirea:
Apelul functiei pentru un parametru: Salut! 
Apelul functiei pentru doi parametri: Salut, lume!
        Desigur, în acest exemplu nu este vorba de niciun polimorfism, ci, pur si simplu, de doua functii diferite, afiseazaMesaj1() si afiseazaMesaj2(), prima primind un parametru, cealalta doi parametri.
        Modificând putin acest exemplu, în sensul redenumirii functiilor afiseazaMesaj1() si afiseazaMesaj2() cu acelasi nume, afiseazaMesaj(), ca în exemplul de mai jos:
<?php
class Test {
    public function afiseazaMesaj($un_parametru) {
        echo "Apelul functiei pentru un parametru: $un_parametru! <br/>";
    }

    public function afiseazaMesaj($par1, $par2) {
        echo "Apelul functiei pentru doi parametri: $par1 $par2";
    }
}

$obj = new test;
$obj->afiseazaMesaj('Salut');
$obj->afiseazaMesaj('Salut,','lume!');
?>
primim eroarea fatala:
Fatal error: Cannot redeclare test::afiseazaMesaj() in ....
asta în timp ce codul similar din din Java:
public class transpusPHP {
 public static void main(String[] args) {
  transpusPHP obiect = new transpusPHP();
  obiect.afiseazaMesaj("Salut!");
  obiect.afiseazaMesaj("Salut, ","lume!");
 }
 public void afiseazaMesaj(String par){
  System.out.println(par);
 }
 public void afiseazaMesaj(String par1, String par2){
  System.out.print(par1);
  System.out.println(par2);
 }
}
functioneaza fara cusur si returneaza:
Salut!
Salut, lume!
        Cum putem rezolva aceasta problema?
        O prima idee este aceea de a folosi o functie cu parametri impliciti. PHP permite utilizarea functiilor cu parametri impliciti, al caror antet arata astfel:
function ($par_1, ..., $par_i, $par_j = val_j, ... , par_n = var_n){
//cod PHP
}
        Apelul unei astfel de functii se face pentru cel putin i parametri ($par_i este ultimul parametru care nu este definit implicit) , si pentru cel mult n. Daca parametrii care urmeaza dupa par_i (par_j si celialti) nu sunt definiti în mod explicit, atunci se folosesc valorile implicite corespunzatoare, iar daca, la un moment dat, un parametru este definit implicit, toti parametrii care urmeaza (pâna la n) sunt definiti implicit
        Cu ajutorul parametrilor impliciti, am putea implementa supraîncarcarea astfel:
<?php
class Test { 
    public function afiseazaMesaj($par1='', $par2='', $par3='') {
  if ($par3){
   echo "Apelul functiei pentru trei paramentri: 
/**/ $par1 $par2 $par3 <br/>";
  } elseif ($par2) {
   echo "Apelul functiei pentru doi parametri: 
/**/ $par1 $par2<br/>";
  } elseif ($par1) {
   echo "Apelul functiei pentru un parametru: 
/**/ $par1 <br/>";
  } else {
   echo "Nu a fost transmis niciun parametru <br/>";
  }
 }
}

$obj = new Test;
$obj->afiseazaMesaj('Salut');
$obj->afiseazaMesaj('Salut,','lume!');
$obj->afiseazaMesaj('Primul parametru, ', 'Al doilea, ', 'Al treilea');
$obj->afiseazaMesaj();
?>
        Efectul executiei programului este urmatorul:
Apelul functiei pentru un parametru: Salut 
Apelul functiei pentru doi parametri: Salut, lume!
Apelul functiei pentru trei paramentri: Primul parametru,  Al doilea, Al treilea 
Nu a fost transmis niciun parametru
        Principalele probleme pe care le ridica o astfel de abordare sunt urmatoarele:
  • Daca o un parametru nu este definit, sau are o valoare egala cu cea implicita, atunci toti parametrii care îi urmeaza primesc valorile implicite.
  • Nu putem, în acest fel, sa implementam functii care accepta oricât de multi parametri.
        O alta abordarea poate viza utilizarea functiei magice __call(), ca în exemplul urmator:
<?php
class Test
{
    public function __call($nume, $parametri)
    {
  $nr_parametri = sizeof($parametri);
  if ($nr_parametri){
   if ($nr_parametri == 1) {
    echo "Apelul functiei $nume pentru un 
/**/singur parametru: ";
   } elseif ($nr_parametri == 2) {
    echo "Apelul functiei $nume pentru 
/**/doi parametri: ";
   } else {
    echo "Apelul functiei $nume pentru 
/**/$nr_parametri parametri: ";
   } 
  } else {
   echo "Nu ati introdus niciun parametru.";
  }
        
  foreach ($parametri as $cheie => $valoare){
    echo $valoare;
  }
  echo "<br/>";
    }   
}
$obj = new Test;
$obj->afiseazaMesaj('Salut, ','lume!');
$obj->afiseazaMesaj('Salut!');
$obj->afiseazaMesaj('Salut, lume!');
$obj ->afiseazaMesaj('Primul parametru ', 'Al doilea parametru ', 
/**/'Al treilea parametru');
$obj->afiseazaMesaj();
?>
        Programul va afisa:
Apelul functiei afiseazaMesaj pentru doi parametri: Salut, lume!
Apelul functiei afiseazaMesaj pentru un singur parametru: Salut!
Apelul functiei afiseazaMesaj pentru un singur parametru: Salut, lume!
Apelul functiei afiseazaMesaj pentru 3 parametri: Primul parametru Al doilea parametru Al treilea parametru
Nu ati introdus niciun parametru.
        Functia __call() nu este o functie sistem, si prin urmare nu poate fi apelata precum functiile sistem. Dimpotriva, __call este un identificator care permite programatorului sa construiasca o functie proprie, în care sa indice ceea ce trebuie sa se întâmple în cazul în care programul încearca sa acceseze, în mod nepermis, o functie-membra a unei clase, respectiv o functie care nu exista (nu este definita).
        În exemplul de mai sus, functia afiseazaMesaj() nu exista (nu este definita) pentru un obiect de tip Test. Încercarea de a o apela transmite functiei __call() cei doi parametri solicitati: primul parametru (case-sensitive) fiind chiar numele functiei apelante, iar al doilea, un sir indexat numeric, ale carui componente sunt chiar parametrii cu care a fost apelata functia apelanta.
        De exemplu, apelul:
$obj->afiseazaMesaj('Salut, ','lume!');
determina transmiterea catre functia __call() a doi parametri si anume stringul $nume = ’afiseazaMesaj’ si array-ul $parametri([0]=>’Salut, ’, [1]=’lume!’). Dupa cum se vede, functia __call() nu utilizeaza, în acest caz, parametrul $nume, dar, în cazul în care sunt mai multe functii al caror comportament trebuie astfel descris, functia __call() va utiliza si acest parametru.
        În ceea ce priveste suprascrierea functiilor, aceasta functioneaza normal, faraniciun fel de probleme, ca în exemplul de mai jos:
<?php
class Bunic{
    public function afiseaza() {
        echo "Afisare din clasa Bunic<br/>";
    }
}


class Parinte extends Bunic{
    public function afiseaza() {
        echo "Afisare din clasa Parinte<br/>";
    }
}

class Copil extends Parinte { 

    public function afiseaza() {
  echo"<br/> Afisare din toate clasele<br/>";
  Bunic::afiseaza();
  Parinte::afiseaza();
        echo "Afisare din clasa Copil<br/>";
    }
}


$obj0 = new Bunic;
$obj1 = new Parinte;
$obj2 = new Copil;

$obj0->afiseaza();
$obj1->afiseaza();
$obj2->afiseaza();
?>
        Iesirea este urmatoarea:
Afisare din clasa Bunic
Afisare din clasa Parinte

Afisare din toate clasele
Afisare din clasa Bunic
Afisare din clasa Parinte
Afisare din clasa Copil
        Avem, asadar, o clasa Bunic, care este extinsa succesiv de clasa Parinte si clasa Copil. În clasa Bunic avem definita o functie afiseaza(), care afiseaza un mesaj corespunzator clasei respective. În clasa Parinte, redefinim aceasta functie, astfel incât sa afiseze un mesaj diferit. De remarcat ca functia afiseaza() din clasa Parinte este o functie complet noua, care nu refoloseste nimic din codul corespunzator din clasa Bunic. Pe de alta parte, functia afiseaza() din Copil reutilizeaza atât codul corespunzator din Bunic, cât si cel din Parinte.

Niciun comentariu:

Trimiteți un comentariu