Az interfésznevekhez nem tartoznak előtagok és utótagok
Az I
előtag vagy az Interface
utótag használata
az interfészeknél, valamint az Abstract
az absztrakt
osztályoknál, antipattern. Tiszta kódban nincs helye. Az interfésznevek
megkülönböztetése valójában elhomályosítja az OOP elveit, zajt visz a
kódba, és bonyodalmakat okoz a fejlesztés során. Az okok a következők.

Típus = osztály + interfész + leszármazottak
Az OOP világában az osztályokat és az interfészeket is típusoknak tekintjük. Ha típust használok egy property vagy paraméter deklarálásakor, a fejlesztő szempontjából nincs különbség abban, hogy a típus, amelyre támaszkodik, osztály vagy interfész. Ez egy nagyszerű dolog, aminek köszönhetően az interfészek valójában olyan hasznosak. Ez ad értelmet a létezésüknek. (Komolyan: mire lennének jók az interfészek, ha ez az elv nem érvényesülne? Próbáljon meg ezen elgondolkodni.)
Nézze meg ezt a kódot:
class FormRenderer
{
public function __construct(
private Form $form,
private Translator $translator,
) {
}
}
A konstruktor azt mondja: “Szükségem van egy űrlapra és egy
fordítóra.” És teljesen mindegy neki, hogy
GettextTranslator
vagy DatabaseTranslator
objektumot
kap. És ugyanakkor felhasználóként nekem teljesen mindegy, hogy a
Translator
interfész, absztrakt osztály vagy konkrét
osztály.
Teljesen mindegy nekem? Valójában nem, bevallom, elég kíváncsi vagyok, így amikor egy idegen könyvtárat vizsgálok, belenézek, mi rejtőzik a típus mögött, és ráviszem az egeret:

Aha, már tudom. És ezzel vége. Annak ismerete, hogy osztályról vagy interfészről van-e szó, akkor lenne fontos, ha példányt akarnék létrehozni belőle, de ez nem az az eset, most csak a változó típusáról beszélek. És itt el akarok vonatkoztatni ezektől a részletektől. És egyáltalán nem akarom őket bevinni a kódomba! Ami a típus mögött van, az a definíciójának része, nem magának a típusnak.
És most nézze meg a következő kódot:
class FormRenderer
{
public function __construct(
private AbstractForm $form,
private TranslatorInterface $translator,
) {
}
}
Ez a konstruktor definíció szó szerint ezt mondja: “Szükségem van egy absztrakt űrlapra és egy fordító interfészre.” De ez butaság. Konkrét űrlapra van szüksége, amelyet ki kell renderelnie. Nem absztrakt űrlapra. És szüksége van egy objektumra, amely a fordító szerepét tölti be. Nincs szüksége interfészre.
Ön tudja, hogy az Interface
és Abstract
szavakat
figyelmen kívül kell hagyni. Hogy a konstruktor ugyanazt akarja, mint az
előző példában. De… komolyan? Tényleg jó ötletnek tartja, hogy a
névkonvenciókba olyan szavak használatát vezesse be, amelyeket figyelmen
kívül kell hagyni?
Hiszen hamis képet alkot az OOP elveiről. Egy kezdőnek össze kell
zavarodnia: „Ha a Translator
típus alatt 1) a
Translator
osztály objektumát 2) a Translator
interfészt implementáló objektumot vagy 3) az ezektől öröklődő
objektumot értjük, akkor mit értünk a TranslatorInterface
alatt?” Erre nem lehet ésszerűen válaszolni.
Amikor TranslatorInterface
-t írunk, bár a
Translator
is lehet interfész, tautológiát követünk el.
Ugyanez, amikor interface TranslatorInterface
-t deklarálunk. És
így tovább. Egészen addig, amíg egy programozói vicc nem születik:
interface TranslatorInterface
{
}
class FormRendererClass
{
/**
* Konstruktor
*/
public function __construct(
private AbstractForm $privatePropertyForm,
private TranslatorInterface $privatePropertyTranslator,
) {
// 🤷♂️
}
}
Kivételes implementáció
Amikor valami olyasmit látok, mint TranslatorInterface
,
valószínű, hogy létezik egy
Translator implements TranslatorInterface
nevű implementáció is.
Ez elgondolkodtat: mitől olyan kivételes a Translator
, hogy
egyedülálló joga van Translatornak nevezni magát? Minden más
implementációnak leíró névre van szüksége, például
GettextTranslator
vagy DatabaseTranslator
, de ez
valahogy “alapértelmezett”, ahogy azt a kiemelt helyzete sugallja, amikor
Translator
-nak hívják jelző nélkül.
Ez még az embereket is elbizonytalanítja, és nem tudják, hogy a
Translator
-ra vagy a TranslatorInterface
-re írjanak-e
typehintet. Az ügyfélkódban aztán keveredik mindkettő, biztosan sokszor
találkozott már ezzel (Nette-ben például a Nette\Http\Request
vs IRequest
kapcsán).
Nem lenne jobb megszabadulni a kivételes implementációtól, és az
általános Translator
nevet meghagyni az interfésznek? Tehát
konkrét implementációk konkrét névvel + általános interfész általános
névvel. Ennek van értelme.
A leíró név terhe ekkor tisztán az implementációkra hárul. Ha
átnevezzük a TranslatorInterface
-t Translator
-ra, a
korábbi Translator
osztályunknak új névre van szüksége. Az
emberek hajlamosak ezt a problémát úgy megoldani, hogy
DefaultTranslator
-nak nevezik, én is bűnös vagyok ebben. De
megint, mitől olyan kivételes, hogy Defaultnak hívják? Ne legyen lusta, és
gondolja át alaposan, mit csinál, és miért különbözik a többi
lehetséges implementációtól.
És mi van, ha nem tudok elképzelni több implementációt? Mi van, ha csak egy érvényes mód jut eszembe? Akkor egyszerűen ne hozzon létre interfészt. Legalábbis egyelőre.
Íme, megjelent egy másik implementáció
És itt van! Szükségünk van egy második implementációra. Ez gyakran előfordul. Soha nem volt szükség a fordítások más módon történő tárolására, mint egy bevált módszerrel, pl. adatbázisban, de most új követelmény jelent meg, és több fordítóra van szükség az alkalmazásban.
Ez az a pillanat is, amikor világossá válik, mi volt az eredeti egyetlen fordító specifikussága. Ez egy adatbázis-fordító volt, nem default.
Mit tegyünk?
- A
Translator
névből interfészt csinálunk - Az eredeti osztályt átnevezzük
DatabaseTranslator
-ra, és implementálja aTranslator
-t - És létrehozunk új
GettextTranslator
és mondjukNeonTranslator
osztályokat
Mindezek a változtatások nagyon kényelmesen és könnyen elvégezhetők,
különösen, ha az alkalmazás a dependency
injection elvei szerint épül fel. A kódban semmit sem kell
változtatni, csak a DI konténer konfigurációjában cseréljük le a
Translator
-t DatabaseTranslator
-ra. Ez nagyszerű!
Gyökeresen eltérő helyzet állna elő azonban, ha ragaszkodnánk az
előtagokhoz/utótagokhoz. A kódban az egész alkalmazáson keresztül át
kellene neveznünk a típusokat Translator
-ról
TranslatorInterface
-re. Egy ilyen átnevezés tisztán célszerű
lenne a konvenció betartása miatt, de ellentmondana az OOP értelmének, ahogy
azt az imént bemutattuk. Az interfész nem változott, a felhasználói kód
nem változott, de a konvenció átnevezést követel? Akkor ez egy hibás
konvenció.
Ha ráadásul idővel kiderülne, hogy az interfésznél jobb lenne egy absztrakt osztály, újra átneveznénk. Egy ilyen beavatkozás egyáltalán nem biztos, hogy triviális, például ha a kód több csomagra van bontva, vagy harmadik felek használják.
De hát mindenki így csinálja
Nem mindenki. Igaz, hogy a PHP világában az interfészek és absztrakt osztályok nevének megkülönböztetését a Zend Framework és utána a Symfony népszerűsítette, tehát nagy játékosok. Ezt a megközelítést átvette a PSR is, amely paradox módon csak interfészeket publikál, és mégis mindegyiknél szerepel a nevében az interfész szó.
Másrészt egy másik jelentős keretrendszer, a Laravel semmilyen módon nem
különbözteti meg az interfészeket és az absztrakt osztályokat. Nem teszi
ezt például a népszerű Doctrine adatbázisréteg sem. És nem teszi ezt a
PHP standard könyvtára sem (így van Throwable
vagy
Iterator
interfészünk, FilterIterator
absztrakt
osztályunk, stb.).
Ha a PHP világán kívül néznénk körül, akkor például a C# az
I
előtagot használja az interfészekhez, ezzel szemben a Java-ban
vagy a TypeScriptben nem különböztetik meg a neveket.
Tehát nem mindenki csinálja így, de még ha így is tennék, az nem jelenti azt, hogy ez így jó. Értelmetlenül átvenni, amit mások csinálnak, nem ésszerű, mert hibákat is átvehet. Hibákat, amelyektől a másik talán nagyon szívesen megszabadulna maga is, csak túl nehézkes.
Nem ismerem fel a kódban, mi az interfész
Sok programozó fog azzal érvelni, hogy számukra hasznosak az elő-/utótagok, mert nekik köszönhetően azonnal felismerik a kódban, mik az interfészek. Úgy érzik, hiányozna nekik egy ilyen megkülönböztetés. Lássuk csak, felismeri ezekben a példákban, mi az osztály és mi az interfész?
$o = new X;
class X extends X implements Y
{}
interface Y
{}
X::fn();
X::$v;
Az X
mindig osztály, az Y
interfész, ez
egyértelmű előtagok/utótagok nélkül is. Természetesen az IDE is tudja
ezt, és az adott kontextusban mindig helyesen fog súgni.
De mi a helyzet itt:
function foo(A $param): A
{}
public A $property;
$o instanceof A
A::CONST
try {
} catch (A $x) {
}
Ezekben az esetekben nem ismeri fel. Ahogy azt a legelején mondtuk, itt a fejlesztő szempontjából nem szabadna különbségnek lennie aközött, hogy mi az osztály és mi az interfész. Ami éppen értelmet ad az interfészeknek és az absztrakt osztályoknak.
Ha itt képes lenne megkülönböztetni az osztályt az interfésztől, az tagadná az alapvető OOP elvet. És az interfészek elveszítenék az értelmüket.
Ehhez vagyok szokva
A szokások megváltoztatása egyszerűen fáj 🙂 Néha már a gondolat is. De hogy ne legyünk igazságtalanok, sok embert éppen ellenkezőleg, vonzanak a változások és örülnek nekik, azonban a többségre igaz, hogy a megszokás nagy úr.
De elég csak a múltba tekinteni, hogyan fújta el az idő néhány
szokást. Talán a leghíresebb az ún. magyar jelölés, amelyet a nyolcvanas
évektől használtak és a Microsoft népszerűsített. A jelölés abból
állt, hogy minden változó neve egy rövidítéssel kezdődött, amely az
adattípusát szimbolizálta. PHP-ban ez így nézett ki:
echo $this->strName
vagy $this->intCount++
.
A magyar jelöléstől a kilencvenes években kezdtek elállni, és ma a
Microsoft az iránymutatásaiban kifejezetten lebeszéli róla a
fejlesztőket.
Valaha elengedhetetlen volt, ma senkinek sem hiányzik.
De miért mennénk ilyen régmúltba? Talán emlékszik rá, hogy PHP-ban szokás volt aláhúzással megkülönböztetni a nem nyilvános osztálytagokat (példa a Zend Frameworkből). Ez abban az időben volt, amikor már régóta létezett a PHP 5, amelynek voltak public/protected/private láthatósági módosítói. De a programozók megszokásból csinálták. Meg voltak győződve arról, hogy aláhúzások nélkül elvesznének a kódban. „Hogyan különböztetném meg a kódban a nyilvánosakat a privát változóktól, aha?”
Ma senki sem használ aláhúzásokat. És senkinek sem hiányoznak. Az idő kiválóan bizonyította, hogy a félelmek alaptalanok voltak.
Pedig ez pontosan ugyanaz, mint az ellenvetés: „Hogyan különböztetném meg a kódban az interfészt az osztálytól, aha?”
Én tíz éve hagytam abba az elő-/utótagok használatát. Soha nem térnék vissza, nagyszerű döntés volt. Nem ismerek más programozót sem, aki vissza akarna térni. Ahogy egy barátom mondta: „Próbáld ki, és egy hónap múlva nem fogod érteni, hogy valaha is másképp csináltad.”
Szeretném fenntartani a következetességet
El tudom képzelni, hogy egy programozó azt mondja: „Az előtagok és utótagok használata valóban értelmetlen, értem, de már így van felépítve a kódom, és a változtatás nagyon nehéz. És ha az új kódot helyesen, nélkülük kezdeném írni, következetlenség jönne létre, ami talán még rosszabb, mint egy rossz konvenció.”
Valójában már most is következetlen a kódja, mert a PHP rendszerkönyvtárát használja, amelynek nincsenek előtagjai és utótagjai:
class Collection implements ArrayAccess, Countable, IteratorAggregate
{
public function add(string|Stringable $item): void
{
}
}
És őszintén, zavarja ez? Eszébe jutott valaha, hogy ez következetesebb lenne?
class Collection implements ArrayAccessInterface, CountableInterface, IteratorAggregateInterface
{
public function add(string|StringableInterface $item): void
{
}
}
Vagy ez?
try {
$command = $this->find($name);
} catch (ThrowableInterface $e) {
return $e->getMessage();
}
Szerintem nem. A következetesség nem játszik olyan jelentős szerepet, mint amilyennek tűnhet. Ezzel szemben a szem a kevesebb vizuális zajt, az agy a tervezés tisztaságát részesíti előnyben. Tehát a konvenció módosítása és az új interfészek helyes, előtagok és utótagok nélküli írásának megkezdése értelmes.
Célzottan el lehet távolítani őket nagy projektekből is. Példa erre a
Nette Framework, amely történelmileg I
előtagokat használt az
interfésznevekben, amelyektől néhány éve fokozatosan
és a teljes visszamenőleges kompatibilitás megőrzésével kezdett
megszabadulni.
A hozzászólás elküldéséhez kérjük, jelentkezzen be