Prefixele și sufixele nu aparțin numelor de interfețe

acum 3 ani de David Grudl  

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?

  1. Din numele Translator facem o interfață
  2. Redenumiți clasa originală în DatabaseTranslator și va implementa Translator
  3. Și creați noi clase GettextTranslator și poate NeonTranslator

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.