Prefixele și sufixele nu se regăsesc în numele interfețelor

acum 2 ani De la David Grudl  

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?

  1. Faceți ca numele Translator interfața
  2. Redenumiți clasa originală în DatabaseTranslator și aceasta va implementa Translator
  3. Și creați clase noi GettextTranslator și poate NeonTranslator

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.