Un concept cu care am avut ceva de furcă la început a fost folosirea interfețelor. Peste tot găseam o explicație vag sumară, eventual și ceva legat de un contract și rămâneam cel puțin la fel de confuz ca înainte.
Din punctul meu de vedere, dacă nu plănuiești să ai un cod extensibil (e.g. prin plugin-uri), interfețele sunt overkill. Nu strică, dar nici nu aduc valoare.
Conceptele SOLID sunt another can of worms, despre care probabil voi scrie cu altă ocazie.
Pentru a înțelege scopul interfețelor, este util să înțelegi conceptele SOLID, în special open-closed principle, care zice că un obiect ar trebui să permită extinderea, nu modificarea.
Un exemplu clasic și practic este reprezentat de un calculator de suprafețe a diferitelor obiecte geometrice. De exemplu, să zicem că avem o clasă simplă ce calculează suprafața unui pătrat:
class Suprafata
{
public function patrat($latura)
{
return $latura * $latura;
}
}
Simplu, eficient, dar ce se întâmplă dacă avem nevoie să calculăm și un dreptunghi? Păi… modificăm clasa:
class Suprafata
{
public function patrat($latura)
{
return $latura * $latura;
}
public function dreptunghi($lungime, $latime)
{
return $lungime * $latime;
}
}
Ce ne zice zice principiul Open-Closed? Că un obiect – o clasă în acest caz – trebuie extins, nu modificat. Deci noi tocmai am încălcat acest principiu!
Ce se întâmplă dacă vrem să calculăm și suprafața unui cerc? Sau a unui cilindru? Sau a unei piramide? Va trebui să modificăm clasa de fiecare dată!
În plus, SRP – primul principiu din acronimul SOLID – este și el încalcat. Principiul ăsta zice că o clasă trebuie să aibă un singur motiv pentru care este modificată. Cum putem repara problema?
În primul rând, extragem logica necesară calcului într-o clasă separată:
class Patrat
{
public function __construct($latura)
{
$this->latura = $latura;
}
public function calculeazaSuprafata()
{
return $this->latura * $this->latura;
}
}
(și facem același lucru și pentru dreptunghi, cerc și tot ce mai avem noi nevoie)
Apoi modificăm clasa inițială să accepte o instanță ce calculează ce avem noi nevoie:
class Suprafata
{
public function calculeaza($obiectGeometric)
{
if (method_exists($obiectGeometric, 'calculeazaSuprafata')) {
return $obiectGeometric->calculeazaSuprafata();
}
}
}
Chestia asta funcționează și am împușcat doi iepuri dintr-un foc: bifăm OCP și SRP. Dar e o problemă: trebuie să verificăm dacă putem apela metoda aia (method_exists($obiectGeometric, 'calculeazaSuprafata')
). Oare există o modalitate mai elegantă? Desigur!
Interludiu: Contracte
Un contract în viața reală presupune o înțelegere între două părți. De ce n-ar însemna același lucru si în programare? Putem face un contract între clasa Suprafata
și clasa Patrat
, astfel încât clasa Patrat
promite că va avea întotdeauna o metodă numită calculeazaSuprafata
.
Acest contract este numit Interfață și conține doar semnăturile metodelor:
interface ObiectGeometric
{
public function calculeazaSuprafata();
}
Semnătura metodei presupune că va fi implementată întocmai. Dacă metoda are argumente în interfață, trebuiesc adăugate aceleași argumente și în implementare.
Modificăm clasa Patrat
, astfel încât să implementeze metodele interfeței (i.e. se angajează să respecte contractul):
class Patrat implements ObiectGeometric
{
//....
Și modificăm și clasa Suprafata
astfel încât acceptă doar implementări ale acestei interfețe
class Suprafata
{
public function calculeaza(ObiectGeometric $obiectGeometric)
{
return $obiectGeometric->calculeazaSuprafata();
}
}
Treaba asta, cu un cuvânt înainte de parametru( function calculeaza(ObiectGeometric $obiectGeometric)
) se numește Type Hinting și va obliga metoda să accepte doar instanțe ale acelei clase – sau implementări, în cazul nostru.
Ca type hinting poți folosi numele unei clase, al unei interfețe sau al unei primitive a limbajului (e.g. Array
, String
). Eu am ajuns la concluzia că nu are sens să folosești numele unei clase, ci doar numele unei interfețe.
Una dintre cele mai mari greșeli
Când am descoperit prima dată interfețele, am zis că e musai să definesc toate metodele dintr-o clasă, indiferent că-s publice sau private. Practic aveam o oglindire 1:1 a clasei în interfață! Ceea ce, evident, este greșit.
De fapt, o interfață definește doar metode publice ce sunt apelate dintr-o altă clasă. De exemplu, în WordPress există conceptul de hooks, ce ne obligă să avem metode publice:
add_action( 'foo', [$instanta, 'numeMetoda'] );
Aceste metode nu trebuiesc definite în interfață, pentru că sunt specifice acelei implementări.
Data viitoare voi scrie despre clasele abstracte.
Lasă un răspuns