Prefixele și sufixele nu se regăsesc în numele interfețelor
Utilizarea sufixului I
prefix or Interface
pentru
interfețe și a sufixului Abstract
pentru clasele abstracte este
un antipattern. Nu are ce căuta în codul pur. Distincția dintre numele
interfețelor estompează de fapt principiile OOP, adaugă zgomot în cod și
provoacă complicații în timpul dezvoltării. Iată care sunt motivele.
Tip = Clasă + Interfață + Descendenți
În lumea OOP, atât clasele, cât și interfețele sunt considerate tipuri. Dacă folosesc un tip atunci când declar o proprietate sau un parametru, din punctul de vedere al dezvoltatorului, nu există nicio diferență între faptul că tipul pe care se bazează este o clasă sau o interfață. De fapt, acesta este lucrul grozav care face ca interfețele să fie atât de utile. Este ceea ce dă sens existenței lor. (Serios: la ce ar servi interfețele dacă acest principiu nu s-ar aplica? Încercați să vă gândiți la asta).
Aruncați o privire la acest cod:
class FormRenderer
{
public function __construct(
private Form $form,
private Translator $translator,
) {
}
}
Constructorul spune: "Am nevoie de un formular și de un traducător.
" Și nu-i pasă dacă primește un obiect GettextTranslator
sau un obiect DatabaseTranslator
. Și, în același timp, mie, ca
utilizator, nu-mi pasă dacă Translator
este o interfață,
o clasă abstractă sau o clasă concretă.
Chiar nu-mi pasă? De fapt, nu, recunosc că sunt destul de curios, așa că, atunci când explorez biblioteca altcuiva, arunc o privire la ce se află în spatele tipului și trec pe deasupra lui:
Oh, înțeleg. Și ăsta e sfârșitul poveștii. Ar fi important să știu dacă este o clasă sau o interfață dacă aș vrea să creez o instanță a acesteia, dar nu este cazul, acum vorbesc doar despre tipul variabilei. Și aici vreau să nu mă amestec în aceste detalii de implementare. Și, cu siguranță, nu vreau ca ele să fie încorporate în codul meu! Ceea ce se află în spatele unui tip face parte din definiția sa, nu tipul în sine.
Acum uitați-vă la celălalt cod:
class FormRenderer
{
public function __construct(
private AbstractForm $form,
private TranslatorInterface $translator,
) {
}
}
Această definiție a constructorului spune literalmente: "Am nevoie de o formă abstractă și de o interfață de traducător. " Dar asta e o prostie. Are nevoie de o formă concretă de redare. Nu de o formă abstractă. Și are nevoie de un obiect care să acționeze ca un traducător. Nu are nevoie de o interfață.
Știți că cuvintele Interface
și Abstract
trebuie ignorate. Că constructorul vrea același lucru ca în exemplul
anterior. Dar… chiar așa? Chiar vi se pare o idee bună să introduceți în
convențiile de denumire utilizarea unor cuvinte care trebuie ignorate?
La urma urmei, se creează o idee falsă despre principiile OOP. Un
începător trebuie să fie confuz: “Dacă tipul Translator
înseamnă fie 1) un obiect din clasa Translator
2) un obiect care
implementează interfața Translator
, fie 3) un obiect care
moștenește din acestea, atunci ce înseamnă
TranslatorInterface
?” Nu există un răspuns rezonabil la
această întrebare.
Atunci când folosim TranslatorInterface
, chiar dacă
Translator
poate fi o interfață, comitem o tautologie. Același
lucru se întâmplă atunci când declarăm
interface TranslatorInterface
. Treptat, se va întâmpla o glumă
de programare:
interface TranslatorInterface
{
}
class FormRendererClass
{
/**
* Constructor
*/
public function __construct(
private AbstractForm $privatePropertyForm,
private TranslatorInterface $privatePropertyTranslator,
) {
// 🤷♂️
}
}
Implementare remarcabilă
Când văd ceva de genul TranslatorInterface
, este posibil să
existe o implementare numită
Translator implements TranslatorInterface
. Asta mă face să mă
întreb: ce face ca Translator
să fie atât de special încât să
aibă privilegiul unic de a fi numit Translator? Orice altă implementare are
nevoie de un nume descriptiv, cum ar fi GettextTranslator
sau
DatabaseTranslator
, dar aceasta este oarecum “implicită”,
după cum sugerează statutul său preferat de a fi numită
Translator
fără etichetă.
Chiar și oamenii se încurcă și nu știu dacă trebuie să tastezehint
pentru Translator
sau TranslatorInterface
. Apoi,
ambele se confundă în codul clientului, sunt sigur că v-ați confruntat cu
acest lucru 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 specială și să păstrăm
numele generic Translator
pentru interfață? Adică, având
implementări specifice cu un nume specific + o interfață generică cu un
nume generic. Acest lucru are sens.
În acest caz, sarcina unui nume descriptiv revine 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
, chiar și eu sunt vinovat de acest lucru. Dar,
din nou, ce o face atât de specială încât să fie numită Default? Nu fiți
leneși și gândiți-vă bine la ceea ce face și de ce este diferită de alte
posibile implementări.
Și ce se întâmplă dacă nu-mi pot imagina mai multe implementări posibile? Dacă nu mă pot gândi decât la o singură modalitate validă? Atunci nu creați interfața. Cel puțin deocamdată.
Eh, există o altă implementare
Și este aici! Avem nevoie de o a doua implementare. Se întâmplă tot timpul. Nu a existat niciodată nevoia de a stoca traducerile în mai mult de un mod dovedit, de exemplu într-o bază de date, dar acum există o nouă cerință și trebuie să avem mai mulți traducători în aplicație.
Acesta este, de asemenea, momentul în care vă dați seama în mod clar care au fost particularitățile traducătorului unic inițial. Era un traducător de bază de date, nu default.
Ce este cu el?
- Faceți ca numele
Translator
interfața - Redenumiți clasa originală în
DatabaseTranslator
și aceasta va implementaTranslator
- Și creați clase noi
GettextTranslator
și poateNeonTranslator
Toate aceste modificări sunt foarte convenabile și ușor de făcut, mai
ales dacă aplicația este construită conform principiilor injecției de
dependență. Nu este nevoie să schimbați nimic în cod, doar
schimbați Translator
în DatabaseTranslator
în
configurația containerului DI. Este minunat!
Cu toate acestea, ar apărea o situație diametral diferită dacă am
insista asupra prefixării/sufixării. Ar trebui să redenumim tipurile din
Translator
în TranslatorInterface
în codul din
întreaga aplicație. O astfel de redenumire ar fi pur de dragul convenției,
dar ar fi contrară spiritului OOP, așa cum tocmai am arătat. Interfața nu
s-a schimbat, codul utilizatorului nu s-a schimbat, dar convenția necesită
o redenumire? Atunci este o convenție defectuoasă.
În plus, dacă s-ar dovedi în timp că o clasă abstractă ar fi mai bună decât interfața, am redenumi din nou. O astfel de acțiune ar putea să nu fie deloc trivială, de exemplu atunci când codul este răspândit în mai multe pachete sau este utilizat de terți.
Dar toată lumea o face
Nu toată lumea o face. Este adevărat că, în lumea PHP, Zend Framework, urmat de Symfony, marii jucători, au popularizat distincția interfață și numele de clase abstracte. Această abordare a fost adoptată de PSR, care, în mod ironic, publică numai interfețe, dar include cuvântul interfață în numele fiecăreia.
Pe de altă parte, un alt framework important, Laravel, nu face distincția
între interfețe și clase abstracte. Nici măcar popularul strat de baze de
date Doctrine nu face acest lucru, de exemplu. Și nici biblioteca standard din
PHP nu face acest lucru (deci avem interfețele Throwable
sau
Iterator
, clasa abstractă FilterIterator
, etc.).
Dacă ne uităm la lumea din afara PHP, C# folosește prefixul I
pentru interfețe, în timp ce în Java sau TypeScript denumirile nu sunt
diferite.
Așadar, nu toată lumea o face, dar chiar dacă ar face-o, nu înseamnă că este un lucru bun. Nu este înțelept să adoptați fără noimă ceea ce fac alții, pentru că s-ar putea să adoptați și dumneavoastră greșeli. Greșeli de care ceilalți ar prefera să scape, e o mușcătură prea mare în zilele noastre.
Nu știu în cod ce este interfața
Mulți programatori vor susține că prefixoanele/sufixele le sunt utile pentru că le permit să știe imediat în cod ce interfețe sunt. Ei consideră că le-ar lipsi o astfel de distincție. Deci, să vedem, puteți spune ce este o clasă și ce este o interfață în aceste exemple?
$o = new X;
class X extends X implements Y
{}
interface Y
{}
X::fn();
X::$v;
X
is always a class, Y
este o interfață, este
lipsit de ambiguitate chiar și fără prefixe/postfixe. Bineînțeles, IDE-ul
știe și el acest lucru și vă va oferi întotdeauna indiciul corect într-un
anumit context.
Dar ce se întâmplă aici:
function foo(A $param): A
{}
public A $property;
$o instanceof A
A::CONST
try { ... } catch (A $x) { ... }
În aceste cazuri, nu veți ști. Așa cum am spus la început, din punctul de vedere al unui dezvoltator nu ar trebui să existe nicio diferență între ceea ce este o clasă și ceea ce este o interfață. Ceea ce conferă interfețelor și claselor abstracte sensul lor.
Dacă ați putea face aici distincția între o clasă și o interfață, ați nega principiul de bază al OOP. Iar interfețele ar deveni lipsite de sens.
M-am obișnuit cu asta
Schimbarea obiceiurilor doar doare 🙂 Adesea, ideea de a o face doare. Să nu dăm vina pe oamenii care sunt atrași de schimbări și le așteaptă cu nerăbdare, pentru majoritatea așa cum se aplică proverbul ceh că obiceiul este o cămașă de fier.
Dar uitați-vă doar la trecut, cum unele obiceiuri au fost măturate de
timp. Poate cel mai cunoscut este așa-numita notație maghiară, folosită
încă din anii '80 și popularizată de Microsoft. Notația consta în
începerea numelui fiecărei variabile cu o abreviere care simboliza tipul de
date al acesteia. În PHP, ar arăta ca echo $this->strName
sau
$this->intCount++
. Notația maghiară a început să fie
abandonată în anii '90, iar în prezent orientările Microsoft descurajează
direct dezvoltatorii să o folosească.
Înainte era o caracteristică esențială și astăzi nimeni nu-i simte lipsa.
Dar de ce să ne întoarcem la un trecut atât de îndepărtat? Poate vă amintiți că în PHP, era obișnuit să se distingă membrii nepublici ai claselor cu o subliniere (sample from Zend Framework). Acest lucru se întâmpla pe vremea când exista PHP 5, care avea modificatori de vizibilitate public/protejat/privat. Dar programatorii o făceau din obișnuință. Erau convinși că, fără sublinieri, nu vor mai înțelege codul. “Cum aș putea distinge proprietățile publice de cele private în cod, nu?”.
Nimeni nu mai folosește astăzi sublinierile. Și nimeni nu le simte lipsa. Timpul a dovedit perfect că temerile erau false.
Și totuși, este exact la fel ca obiecția: “Cum aș putea distinge o interfață de o clasă în cod, nu?”.
Am încetat să mai folosesc prefixe/postfixe acum zece ani. Nu m-aș mai întoarce niciodată, a fost o decizie excelentă. Nu cunosc nici un alt programator care să vrea să se întoarcă înapoi. Așa cum spunea un prieten: “Încearcă și peste o lună nu vei înțelege că ai făcut vreodată altfel”.
Vreau să păstrez consecvența
Îmi pot imagina un programator spunând: “Folosirea prefixelor și sufixelor este un adevărat nonsens, înțeleg, doar că eu am deja codul construit în acest fel și schimbarea lui este foarte dificilă. Iar dacă încep să scriu cod nou corect fără ele, voi ajunge la inconsecvență, ceea ce este poate chiar mai rău decât o convenție proastă.”
De fapt, codul dvs. este deja inconsecvent deoarece utilizați biblioteca de sistem PHP:
class Collection implements ArrayAccess, Countable, IteratorAggregate
{
public function add(string|Stringable $item): void
{
}
}
Și, cu mâna pe inimă, contează asta? V-ați gândit vreodată că acest lucru ar fi mai consistent?
class Collection implements ArrayAccessInterface, CountableInterface, IteratorAggregateInterface
{
public function add(string|StringableInterface $item): void
{
}
}
Sau asta?
try {
$command = $this->find($name);
} catch (ThrowableInterface $e) {
return $e->getMessage();
}
Nu prea cred. Consecvența nu joacă un rol atât de important pe cât ar părea. Dimpotrivă, ochiul preferă mai puțin zgomot vizual, iar creierul preferă claritatea designului. Așa că ajustarea convenției și începerea scrierii corecte a noilor interfețe fără prefixe și sufixe are sens.
Acestea pot fi eliminate intenționat chiar și din proiectele mari. Un exemplu este Nette Framework, care a folosit în mod istoric prefixe I în numele interfețelor, pe care a început să le elimine treptat în urmă cu câțiva ani, menținând în același timp compatibilitatea totală cu trecutul.
Pentru a trimite un comentariu, vă rugăm să vă conectați