Az előtagok és utótagok nem tartoznak az interfésznevekbe
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?
- Legyen a neve
Translator
a felület - Nevezd át az eredeti osztályt
DatabaseTranslator
-ra, és az implementálja a következő nevetTranslator
- És új osztályokat hozol létre
GettextTranslator
és esetlegNeonTranslator
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.
A hozzászólás elküldéséhez kérjük, jelentkezzen be