Az előtagok és utótagok nem tartoznak az interfésznevekbe

2 éve A címről David Grudl  

A I prefix or Interface utótag használata az interfészeknél, és a Abstract az absztrakt osztályoknál, antipattern. Ez nem tartozik a tiszta kódba. Az interfésznevek megkülönböztetése valójában elhomályosítja az OOP elveket, zajt okoz a kódban, és bonyodalmakat okoz a fejlesztés során. Itt vannak az okok.

Típus = osztály + interfész + leszármazottak

Az OOP világában mind az osztályok, mind az interfészek típusoknak számítanak. Ha egy tulajdonság vagy paraméter deklarálásakor típust használok, a fejlesztő szempontjából nincs különbség aközött, hogy a típus, amelyre támaszkodik, egy osztály vagy egy interfész. Tulajdonképpen ez az a klassz dolog, ami az interfészeket olyan hasznossá teszi. Ez az, ami értelmet ad a létezésüknek. (Komolyan: mire lennének az interfészek, ha ez az elv nem érvényesülne? Próbáljon meg belegondolni.)

Vess egy pillantást erre a kódra:

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 nem érdekli, hogy egy GettextTranslator objektumot vagy egy DatabaseTranslator objektumot kap. És ugyanakkor engem mint felhasználót nem érdekel, hogy a Translator egy interfész, egy absztrakt osztály vagy egy konkrét osztály.

Tényleg nem érdekel? Valójában nem, bevallom, elég kíváncsi vagyok, ezért amikor valaki más könyvtárát fedezem fel, megnézem, mi van a típus mögött, és lebegek felette:

Ó, értem. És ezzel vége a történetnek. Az, hogy tudjuk, hogy osztály vagy interfész, akkor lenne fontos, ha egy példányt akarnék létrehozni belőle, de nem erről van szó, most csak a változó típusáról beszélek. És ez az a pont, ahol ki akarok maradni ezekből az implementációs részletekből. És semmiképpen sem akarom, hogy a kódomba ágyazva legyenek! Ami egy típus mögött van, az a definíció része, nem maga a típus.

Most nézzük meg a másik kódot:

class FormRenderer
{
	public function __construct(
		private AbstractForm $form,
		private TranslatorInterface $translator,
	) {
	}
}

Ez a konstruktor definíciója szó szerint azt mondja: "Szükségem van egy absztrakt formára és egy interface fordítóra. " De ez butaság. Szüksége van egy konkrét formára a rendereléshez. Nem egy absztrakt formára. És szüksége van egy objektumra, amely fordítóként működik. Nincs szüksége interfészre.

Tudja, hogy a Interface és a Abstract szavakat figyelmen kívül kell hagyni. Hogy a konstruktor ugyanazt akarja, mint az előző példában. De… tényleg? Tényleg jó ötletnek tűnik a névadási konvenciókba bevezetni a figyelmen kívül hagyandó szavak használatát?

Végül is ez hamis elképzelést teremt az OOP alapelveiről. Egy kezdő biztosan összezavarodik: “Ha a Translator típus vagy 1) a Translator osztály objektumát jelenti 2) a Translator interfészt megvalósító objektumot, vagy 3) az ezekből öröklődő objektumot, akkor mit jelent a TranslatorInterface?” Erre nincs ésszerű válasz.

Amikor a TranslatorInterface-t használjuk, még ha a Translator lehet is interfész, akkor is tautológiát követünk el. Ugyanez történik, amikor a interface TranslatorInterface deklaráljuk. Fokozatosan egy programozási vicc fog történni:

interface TranslatorInterface
{
}

class FormRendererClass
{
	/**
	 * Constructor
	 */
	public function __construct(
		private AbstractForm $privatePropertyForm,
		private TranslatorInterface $privatePropertyTranslator,
	) {
		// 🤷‍♂️
	}
}

Kiemelkedő megvalósítás

Amikor valami olyasmit látok, mint TranslatorInterface, akkor valószínűleg van egy Translator implements TranslatorInterface nevű megvalósítás. Ez elgondolkodtat: mitől olyan különleges a Translator, hogy az a kivételes kiváltsága van, hogy fordítónak nevezik? Minden más implementációnak szüksége van egy leíró névre, mint például GettextTranslator vagy DatabaseTranslator, de ez az implementáció egyfajta “alapértelmezett”, ahogy azt a preferált státusza is sugallja, hogy Translator a címke nélkül.

Még az emberek is összezavarodnak, és nem tudják, hogy a Translator vagy a TranslatorInterface nevet kell-e begépelni. Aztán mindkettő összekeveredik a klienskódban, biztos vagyok benne, hogy sokszor belefutottál már ebbe (a Nette-ben például a Nette\Http\Request vs IRequest) kapcsolatban.

Nem lenne jobb, ha megszabadulnánk a speciális megvalósítástól, és megtartanánk a Translator általános nevet az interfésznek? Vagyis, hogy speciális implementáció egy speciális névvel + egy általános interfész egy általános névvel. Ennek van értelme.

A leíró név terhe akkor kizárólag az implementációkra hárul. Ha átnevezzük a TranslatorInterface -t Translator-re, 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 hívják, még én is bűnös vagyok ebben. De még egyszer, mitől olyan különleges, hogy Defaultnak hívják? Ne légy lusta, és gondolkodj el alaposan azon, hogy 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 több lehetséges implementációt elképzelni? Mi van, ha csak egyetlen érvényes módot tudok elképzelni? Akkor egyszerűen ne hozza létre az interfészt. Legalábbis egyelőre.

Eh, van egy másik megvalósítás

És itt van! Szükségünk van egy második implementációra. Mindig ez történik. Eddig soha nem volt szükség arra, hogy a fordításokat több bevált módon tároljuk, például egy adatbázisban, de most új követelmény merült fel, és több fordítóra van szükségünk az alkalmazásban.

Ilyenkor is világosan rájövünk, hogy mik voltak az eredeti egyetlen fordító sajátosságai. Ez egy adatbázis fordító volt, nem volt alapértelmezett.

Mi van vele?

  1. Legyen a neve Translator a felület
  2. Nevezd át az eredeti osztályt DatabaseTranslator -ra, és az implementálja a következő nevet Translator
  3. És új osztályokat hozol létre GettextTranslator és esetleg NeonTranslator

Mindezek a változtatások nagyon kényelmesek és könnyen elvégezhetők, különösen, ha az alkalmazás a dependency injection elvei szerint épül. Nem kell semmit sem változtatni a kódban, csak a DI konténer konfigurációjában a Translator -t DatabaseTranslator -re kell módosítani. Ez nagyszerű!

Azonban szögesen más helyzet állna elő, ha ragaszkodnánk a prefixáláshoz/szuffixáláshoz. Az egész alkalmazásban át kellene neveznünk a típusokat Translator -ról TranslatorInterface -re a kódban. Az ilyen átnevezés pusztán a konvenció kedvéért történne, de ellentétes lenne az OOP szellemével, ahogyan azt az imént bemutattuk. A felület nem változott, a felhasználói kód nem változott, de a konvenció miatt át kell nevezni? Akkor ez egy hibás konvenció.

Ráadásul, ha idővel kiderülne, hogy egy absztrakt osztály jobb lenne, mint az interfész, akkor újra átneveznénk. Egy ilyen művelet egyáltalán nem biztos, hogy triviális, például ha a kód több csomagban van szétterítve, vagy harmadik fél használja.

De mindenki ezt csinálja

Nem mindenki csinálja. Igaz, hogy a PHP világában a nagyok közül a Zend Framework, majd a Symfony népszerűsítette a megkülönböztető felületet és az absztrakt osztályneveket. Ezt a megközelítést átvette a PSR, amely ironikus módon csak interfészeket tesz közzé, mégis mindegyik nevében szerepel az interfész szó.

Másrészt egy másik nagy keretrendszer, a Laravel nem tesz különbséget interfészek és absztrakt osztályok között. Még a népszerű Doctrine adatbázis réteg sem teszi ezt például. És a PHP szabványos könyvtára sem (így van a Throwable vagy a Iterator interfész, a FilterIterator absztrakt osztály stb.).

Ha a PHP-n kívüli világot nézzük, a C# a I előtagot használja az interfészekre, míg a Java-ban vagy a TypeScriptben a nevek nem különböznek.

Tehát nem mindenki csinálja ezt, de ha mégis, az még nem jelenti azt, hogy ez jó dolog. Nem bölcs dolog esztelenül átvenni azt, amit mások csinálnak, mert lehet, hogy ezzel hibákat is átveszel. Olyan hibákat, amiktől a másik inkább megszabadulna, ez manapság már túl nagy falat.

Nem tudom a kódban, hogy mi az interfész

Sok programozó azzal érvel, hogy a prefixek/szuffixek azért hasznosak számukra, mert lehetővé teszik számukra, hogy a kódban azonnal tudják, hogy mi az interfész. Úgy érzik, hogy hiányolnák az ilyen megkülönböztetést. Lássuk tehát, meg tudod-e mondani, hogy mi az osztály és mi az interfész ezekben a példákban?

$o = new X;

class X extends X implements Y
{}

interface Y
{}

X::fn();

X::$v;

X is always a class, Y egy interfész, ez egyértelmű, még előtagok/utótagok nélkül is. Természetesen az IDE ezt is tudja, és mindig a megfelelő tippet adja az adott kontextusban.

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 fogsz tudni róla. Ahogy a legelején mondtuk, egy fejlesztő szemszögéből nézve nem szabadna különbséget tenni aközött, hogy mi egy osztály és mi egy interfész. Ez adja az interfészek és az absztrakt osztályok értelmét.

Ha itt különbséget lehetne tenni osztály és interfész között, az az OOP alapelvét tagadná meg. És az interfészek értelmetlenné válnának.

Hozzászoktam

A szokások megváltoztatása csak fáj 🙂 Gyakran már a gondolat is fáj. Ne hibáztassuk azokat az embereket, akik vonzódnak a változásokhoz és várják azokat, a legtöbbjükre ugyanis ahogy a cseh közmondás tartja, hogy a szokás vasalt ing.

De nézzük csak meg a múltat, hogy egyes szokásokat hogyan söpört el az idő. A leghíresebb talán a nyolcvanas évek óta használt, a Microsoft által népszerűsített úgynevezett magyar jelölés. A jelölés abból állt, hogy minden változó nevét az adattípusát szimbolizáló rövidítéssel kezdték. PHP-ben ez így nézne ki: echo $this->strName vagy $this->intCount++. A magyar jelölést a kilencvenes években kezdték elhagyni, és ma már a Microsoft irányelvei egyenesen lebeszélik a fejlesztőket a használatáról.

Régebben nélkülözhetetlen funkció volt, és ma már senki sem hiányolja.

De miért is menjünk vissza ilyen régen? Talán emlékszel, hogy a PHP-ben szokás volt az osztályok nem nyilvános tagjait aláhúzással megkülönböztetni (sample from Zend Framework). Ez még akkor volt, amikor még létezett a PHP 5, amiben voltak public/protected/private láthatósági módosítók. De a programozók megszokásból tették ezt. Meg voltak győződve arról, hogy aláhúzás nélkül nem értenék a kódot. “Hogyan különböztetném meg a kódban a publikus és a privát tulajdonságokat, hm?”.

Ma már senki sem használ aláhúzásokat. És senkinek sem hiányoznak. Az idő tökéletesen bebizonyította, hogy a félelmek hamisak voltak.

Mégis, ez pontosan ugyanaz, mint az az ellenvetés, hogy “Hogyan különböztetnék meg egy interfészt egy osztálytól a kódban, hm?”.

Tíz évvel ezelőtt felhagytam a prefixek/postfixek használatával. 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 megérteni, hogy valaha is másképp csináltad”.

Fenntartani akarom a következetességet

El tudom képzelni, hogy egy programozó azt mondja: “Az előtagok és utótagok használata tényleg nonszensz, értem én, csak már így épült fel a kódom, és nagyon nehéz változtatni rajta. És ha elkezdek új kódot írni helyesen ezek nélkül, akkor a végén következetlenséget fogok kapni, ami talán még rosszabb, mint a rossz konvenció.”

Valójában a kódod már most is inkonzisztens, mert a PHP rendszer könyvtárát használod:

class Collection implements ArrayAccess, Countable, IteratorAggregate
{
	public function add(string|Stringable $item): void
	{
	}
}

És kéz a kézben, számít ez? Gondoltad volna, hogy ez így konzisztensebb lesz?

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();
}

Nem hinném. A következetesség nem játszik olyan nagy szerepet, mint amilyennek látszik. Épp ellenkezőleg, a szem a kevesebb vizuális zajt, az agy pedig a letisztult dizájnt részesíti előnyben. Tehát a konvenció kiigazításának és az új felületek helyes, előtagok és utótagok nélküli írásának van értelme.

Ezeket szándékosan el lehet távolítani még a nagy projektekből is. Erre példa a Nette Framework, amely történelmileg I előtagokat használt az interfésznevekben, amelyeket néhány évvel ezelőtt kezdett fokozatosan megszüntetni, miközben fenntartotta a teljes visszafelé kompatibilitást.