Prefixele și sufixele nu aparțin numelor de interfețe
Utilizarea prefixului I
sau a sufixului Interface
pentru interfețe, precum și Abstract
pentru clasele abstracte,
este un antipattern. Nu are ce căuta în codul curat. Diferențierea numelor de
interfețe, de fapt, estompează principiile OOP, introduce zgomot în cod și
cauzează complicații în dezvoltare. Motivele sunt următoarele.

Tip = clasă + interfață + descendenți
În lumea OOP, atât clasele, cât și interfețele sunt considerate tipuri. Dacă folosesc un tip la declararea unei proprietăți sau a unui parametru, din punctul de vedere al dezvoltatorului nu există nicio diferență dacă tipul pe care se bazează este o clasă sau o interfață. Acesta este un lucru extraordinar, datorită căruia interfețele sunt de fapt atât de utile. Acest lucru dă sens existenței lor. (Serios: la ce ar folosi interfețele dacă acest principiu nu s-ar aplica? Încercați să vă gândiți la asta.)
Priviți acest cod:
class FormRenderer
{
public function __construct(
private Form $form,
private Translator $translator,
) {
}
}
Constructorul spune: “Am nevoie de un formular și un
traducător.” Și nu-i pasă absolut deloc dacă primește un obiect
GettextTranslator
sau DatabaseTranslator
. Și, în
același timp, ca utilizator, nu-mi pasă absolut deloc dacă
Translator
este o interfață, o clasă abstractă sau o clasă
concretă.
Nu-mi pasă deloc? De fapt, nu, recunosc că sunt destul de curios, așa că atunci când examinez o bibliotecă străină, arunc o privire la ce se ascunde în spatele tipului și trec mouse-ul peste el:

Aha, deci acum știu. Și aici se termină. Cunoașterea dacă este o clasă sau o interfață ar fi importantă dacă aș dori să creez o instanță a acesteia, dar nu este cazul, acum vorbesc doar despre tipul variabilei. Și aici vreau să fiu izolat de aceste detalii. Și cu siguranță nu vreau să le introduc în codul meu! Ce se ascunde în spatele tipului face parte din definiția sa, nu din tipul însuși.
Și acum priviți următorul cod:
class FormRenderer
{
public function __construct(
private AbstractForm $form,
private TranslatorInterface $translator,
) {
}
}
Această definiție a constructorului spune literalmente: “Am nevoie de un formular abstract și o interfață de traducător.” Dar asta este o prostie. Are nevoie de un formular concret pe care să-l randeze. Nu de un formular abstract. Și are nevoie de un obiect care îndeplinește rolul de traducător. Nu are nevoie de o interfață.
Știți că cuvintele Interface
și Abstract
trebuie ignorate. Că constructorul dorește același lucru ca în exemplul
anterior. Dar… serios? Chiar vi se pare o idee bună să introduceți în
convențiile de denumire utilizarea cuvintelor care trebuie trecute cu
vederea?
De fapt, creează o imagine falsă despre principiile OOP. Un începător
trebuie să fie confuz: „Dacă tipul Translator
înseamnă fie 1)
un obiect al clasei Translator
2) un obiect care implementează
interfața Translator
sau 3) un obiect care moștenește de la ele,
ce se înțelege atunci prin TranslatorInterface
?” La asta nu se
poate răspunde rezonabil.
Când scriem TranslatorInterface
, deși și
Translator
poate fi o interfață, comitem o tautologie. Același
lucru când declarăm interface TranslatorInterface
. Și așa mai
departe. Până când apare o glumă de programator:
interface TranslatorInterface
{
}
class FormRendererClass
{
/**
* Constructor
*/
public function __construct(
private AbstractForm $privatePropertyForm,
private TranslatorInterface $privatePropertyTranslator,
) {
// 🤷♂️
}
}
Implementare excepțională
Când văd ceva ca TranslatorInterface
, este probabil să existe
și o implementare cu numele
Translator implements TranslatorInterface
. Mă face să mă
gândesc: ce face Translator
atât de special încât are dreptul
unic de a se numi Translator? Orice altă implementare necesită un nume
descriptiv, de exemplu GettextTranslator
sau
DatabaseTranslator
, dar aceasta este cumva “implicită”, așa
cum sugerează poziția sa prioritară, când se numește
Translator
fără adjectiv.
Chiar îi face pe oameni nesiguri și nu știu dacă ar trebui să scrie
typehint pentru Translator
sau TranslatorInterface
.
În codul clientului, se amestecă apoi ambele, cu siguranță ați întâlnit
asta de multe ori (în Nette, de exemplu, în legătură cu
Nette\Http\Request
vs IRequest
).
Nu ar fi mai bine să scăpăm de implementarea excepțională și să
păstrăm numele general Translator
pentru interfață? Adică să
avem implementări concrete cu nume concrete + interfață generală cu nume
general. Asta are sens, nu-i așa?
Povara numelui descriptiv revine atunci exclusiv implementărilor. Dacă
redenumim TranslatorInterface
în Translator
, fosta
noastră clasă Translator
are nevoie de un nume nou. Oamenii au
tendința de a rezolva această problemă numind-o
DefaultTranslator
, și eu sunt vinovat. Dar din nou, ce o face
atât de specială încât se numește Default? Nu fiți leneși și
gândiți-vă serios la ce face și de ce diferă de alte implementări
posibile.
Și ce se întâmplă dacă nu-mi pot imagina mai multe implementări? Ce se întâmplă dacă îmi vine în minte doar o singură modalitate validă? Atunci pur și simplu nu creați interfața. Cel puțin deocamdată.
Iată, a apărut o altă implementare
Și iată! Avem nevoie de a doua implementare. Se întâmplă frecvent. Nu a existat niciodată nevoia de a stoca traducerile altfel decât într-un singur mod dovedit, de exemplu, într-o bază de date, dar acum a apărut o nouă cerință și este necesar să avem mai mulți traducători în aplicație.
Acesta este și momentul în care vă dați seama clar care a fost specificitatea traducătorului unic original. A fost un traducător de bază de date, niciunul implicit.
Ce facem cu asta?
- Din numele
Translator
facem o interfață - Redenumiți clasa originală în
DatabaseTranslator
și va implementaTranslator
- Și creați noi clase
GettextTranslator
și poateNeonTranslator
Toate aceste modificări se fac foarte comod și ușor, mai ales dacă
aplicația este construită în conformitate cu principiile injecției de
dependențe. Nu este nevoie să schimbați nimic în cod, doar în
configurația containerului DI schimbăm Translator
în
DatabaseTranslator
. Asta e minunat!
O situație diametral opusă ar apărea însă dacă am insista pe
prefixare/sufixare. Ar trebui să redenumim tipurile în codul din întreaga
aplicație de la Translator
la TranslatorInterface
.
O astfel de redenumire ar fi pur și simplu pentru a respecta convenția, dar
ar merge împotriva sensului OOP, așa cum am arătat mai devreme. Interfața nu
s-a schimbat, codul utilizatorului nu s-a schimbat, dar convenția necesită
redenumire? Atunci este o convenție greșită.
Dacă, în plus, s-ar dovedi în timp că o clasă abstractă ar fi mai bună decât o interfață, am redenumi din nou. O astfel de intervenție nu trebuie să fie deloc trivială, de exemplu, dacă codul este distribuit în mai multe pachete sau este utilizat de terți.
Dar așa fac toți
Nu toți. Este adevărat că în lumea PHP, diferențierea numelor de interfețe și clase abstracte a fost popularizată de Zend Framework și apoi de Symfony, adică jucători mari. Această abordare a fost preluată și de PSR, care, paradoxal, publică doar interfețe și totuși menționează cuvântul interfață în numele fiecăreia.
Pe de altă parte, un alt framework important, Laravel, nu diferențiază în
niciun fel interfețele și clasele abstracte. Nu o face, de exemplu, nici
popularul strat de bază de date Doctrine. Și nu o face ani biblioteca
standard din PHP (avem astfel interfețele Throwable
sau
Iterator
, clasa abstractă FilterIterator
, etc.).
Dacă ne-am uita în afara lumii PHP, de exemplu, C# utilizează prefixul
I
pentru interfețe, în schimb în Java sau TypeScript numele nu
se diferențiază.
Deci nu o fac toți, totuși, chiar dacă ar face-o, nu înseamnă că este bine. Preluarea fără discernământ a ceea ce fac alții nu este rezonabilă, deoarece puteți prelua și greșeli. Greșeli de care celălalt, foarte probabil, s-ar debarasa bucuros el însuși, doar că este prea dificil.
Nu recunosc în cod ce este o interfață
Mulți programatori vor obiecta că prefixele/sufixele le sunt utile, deoarece datorită lor recunosc imediat în cod ce sunt interfețele. Au senzația că le-ar lipsi o astfel de distincție. Să vedem, recunoașteți în aceste exemple ce este o clasă și ce este o interfață?
$o = new X;
class X extends X implements Y
{}
interface Y
{}
X::fn();
X::$v;
X
este întotdeauna o clasă, Y
este
o interfață, este neechivoc chiar și fără prefixe/postfixe. Desigur, știe
și IDE-ul și în contextul dat vă va sugera întotdeauna corect.
Dar ce ziceți aici:
function foo(A $param): A
{}
public A $property;
$o instanceof A
A::CONST
try {
} catch (A $x) {
}
În aceste cazuri nu recunoașteți. Așa cum am spus chiar la început, aici nu ar trebui să existe nicio diferență din punctul de vedere al dezvoltatorului între ce este o clasă și ce este o interfață. Ceea ce tocmai dă sens interfețelor și claselor abstracte.
Dacă ați fi capabili să distingeți aici clasa de interfață, ar nega principiul fundamental al OOP. Și interfețele și-ar pierde sensul.
Sunt obișnuit cu asta
Schimbarea obiceiurilor pur și simplu doare 🙂 De câte ori chiar și doar gândul. Dar să nu fim nedrepți, mulți oameni sunt, dimpotrivă, atrași de schimbări și se bucură de ele, totuși pentru majoritatea este valabil că obiceiul este o cămașă de fier.
Dar este suficient să ne uităm în trecut, cum unele obiceiuri au fost
luate de vânt. Probabil cea mai faimoasă este așa-numita notație maghiară
utilizată din anii optzeci și popularizată de Microsoft. Notația consta în
faptul că numele fiecărei variabile începea cu o abreviere care simboliza
tipul său de date. În PHP ar arăta astfel echo $this->strName
sau $this->intCount++
. De la notația maghiară s-a început să
se renunțe în anii nouăzeci și astăzi Microsoft în instrucțiunile sale
îi descurajează direct pe dezvoltatori de la aceasta.
Odată era indispensabilă și astăzi nu-i lipsește nimănui.
Dar de ce să mergem într-un trecut atât de îndepărtat? Poate vă amintiți că în PHP era obiceiul să se diferențieze membrii nepublici ai claselor cu un underscore (“exemplu din Zend Framework”|https://github.com/zendframework/zf1/blob/master/library/Zend/Acl.php#L86-L108). Era pe vremea când exista de mult PHP 5, care avea modificatorii de vizibilitate public/protected/private. Dar programatorii o făceau din obișnuință. Erau convinși că fără underscore-uri nu s-ar mai orienta în cod. „Cum aș distinge în cod variabilele publice de cele private, aha?”
Astăzi nimeni nu mai folosește underscore-uri. Și nimănui nu-i lipsesc. Timpul a verificat excelent că temerile erau nefondate.
Și totuși este exact același lucru ca obiecția: „Cum aș distinge în cod interfața de clasă, aha?”
Eu am încetat să folosesc prefixe/postfixe acum zece ani. Nu m-aș mai întoarce niciodată, a fost o decizie excelentă. Nu cunosc niciun alt programator care ar dori să se întoarcă. Cum a spus un prieten: „Încearcă și într-o lună nu vei înțelege că ai făcut vreodată altfel.”
Vreau să mențin consistența
Îmi pot imagina că un programator își spune: „Utilizarea prefixelor și sufixelor este într-adevăr un nonsens, înțeleg asta, doar că am deja codul construit astfel și schimbarea este foarte dificilă. Și dacă aș începe să scriu codul nou corect fără ele, mi-ar apărea o inconsistență, care este poate chiar mai rea decât o convenție proastă.”
De fapt, deja acum codul dvs. este inconsistent, deoarece utilizați biblioteca de sistem PHP, care nu are niciun prefix și postfix:
class Collection implements ArrayAccess, Countable, IteratorAggregate
{
public function add(string|Stringable $item): void
{
}
}
Și, sincer, deranjează asta? V-ați gândit vreodată că ar fi mai consistent așa?
class Collection implements ArrayAccessInterface, CountableInterface, IteratorAggregateInterface
{
public function add(string|StringableInterface $item): void
{
}
}
Sau așa?
try {
$command = $this->find($name);
} catch (ThrowableInterface $e) {
return $e->getMessage();
}
Cred că nu. Consistența nu joacă un rol atât de important pe cât s-ar putea părea. Dimpotrivă, ochiul preferă mai puțin zgomot vizual, creierul curățenia designului. Deci, ajustarea convenției și începerea scrierii noilor interfețe corect, fără prefixe și sufixe, are sens.
Ele pot fi eliminate intenționat și din proiecte mari. Un exemplu este
Nette Framework, care istoric a utilizat prefixe I
în numele
interfețelor, de care a început să
se debaraseze treptat acum câțiva ani, cu păstrarea completă a
compatibilității inverse.
Pentru a trimite un comentariu, vă rugăm să vă conectați