Předpony a přípony do názvů rozhraní nepatří
Používání předpony I
nebo přípony Interface
u rozhraní, taktéž Abstract
u abstraktních tříd, je
antipattern. V čistém kódu nemá co dělat. Odlišování názvů rozhraní
ve skutečnosti zamlžuje principy OOP, zanáší do kódu šum, a způsobuje
komplikaci při vývoji. Důvody jsou následující.
Typ = třída + rozhraní + potomci
Ve světě OOP jsou třídy i rozhraní považovány za typy. Pokud použiji typ při deklaraci property nebo parametru, není z pohledu vývojáře rozdíl mezi tím, zda typ, na který se spoléhá, je třída nebo rozhraní. To je parádní věc, díky které jsou rozhraní vlastně tak užitečná. To dává jejich existenci smysl. (Vážně: k čemu by byla rozhraní, kdyby tento princip neplatil? Zkuste se nad tím zamyslet.)
Podívejte se na tento kód:
class FormRenderer
{
public function __construct(
private Form $form,
private Translator $translator,
) {
}
}
Konstruktor říká: „Potřebuji formulář a překladač.“
A je mu úplně jedno, jestli dostane objekt GettextTranslator
nebo DatabaseTranslator
. A zároveň jako uživateli je mi úplně
jedno, jestli Translator
je rozhraní, abstraktní třída nebo
konkrétní třída.
Je mi to úplně jedno? Vlastně ne, přiznávám se, že jsem docela zvědavý, takže když zkoumám cizí knihovnu, nakouknu, co se skrývá za typem, a najedu na něj myší:
Aha, tak už to vím. A tím to končí. Znalost, zda jde o třídu či rozhraní, by byla důležitá, kdybych chtěl vytvářet její instanci, ale to není ten případ, teď jen mluvím o typu proměnné. A tady chci být od těchto detailů odstíněn. A už vůbec je nechci zanášet do svého kódu! Co se za typem skrývá je součástí jeho definice, ne typu samotného.
A teď se podívejte na další kód:
class FormRenderer
{
public function __construct(
private AbstractForm $form,
private TranslatorInterface $translator,
) {
}
}
Tato definice konstruktoru doslova říká: „Potřebuji abstraktní formulář a rozhraní překladače.“ Ale to je hloupost. Potřebuje konkrétní formulář, který má vykreslit. Nikoliv abstraktní formulář. A potřebuje objekt, který plní úlohu překladače. Nepotřebuje rozhraní.
Vy víte, že slova Interface
a Abstract
se mají
ignorovat. Že konstruktor chce totéž, jako v předchozí ukázce. Ale…
jako vážně? Opravdu vám připadá dobrý nápad si do jmenných konvencí
zavést užívání slov, které se mají přehlížet?
Vždyť vytváří falešnou představu o principech OOP. Začátečník
musí být zmatený: „Pokud se typem Translator
rozumí buď 1)
objekt třídy Translator
2) objekt implementující rozhraní
Translator
nebo 3) objekt od nich dědící, co se pak rozumí tím
TranslatorInterface
?“ Na tohle nejde rozumně odpovědět.
Když píšeme TranslatorInterface
, ačkoliv i
Translator
může být interface, dopouštíme se tautologie.
Totéž když deklarujeme interface TranslatorInterface
. A tak
dále. Až vznikne programátorský vtip:
interface TranslatorInterface
{
}
class FormRendererClass
{
/**
* Constructor
*/
public function __construct(
private AbstractForm $privatePropertyForm,
private TranslatorInterface $privatePropertyTranslator,
) {
// 🤷♂️
}
}
Výjimečná implementace
Když vidím něco jako TranslatorInterface
, je pravděpodobné,
že bude existovat i implementace s názvem
Translator implements TranslatorInterface
. Nutí mě to
k zamyšlení: čím je Translator
tak výjimečný, že má
jedinečné právo nazývat se Translator? Každá jiná implementace potřebuje
popisné jméno, například GettextTranslator
nebo
DatabaseTranslator
, ale tahle je jaksi „výchozí“, jak
naznačuje její přednostní postavení, když se jmenuje
Translator
bez přívlastku.
Dokonce to lidi znejistí a neví, zda mají psát typehint pro
Translator
nebo TranslatorInterface
. V klientském
kódu se pak míchá obojí, určitě jste na to už mnohokrát narazili
(v Nette třeba v souvislosti s Nette\Http\Request
vs
IRequest
).
Nebylo by lepší se výjimečné implementace zbavit a ponechat obecný
název Translator
pro rozhraní? Tedy mít konkrétní implementace
s konkrétním názvem + obecné rozhraní s obecným názvem. To dává
přeci smysl.
Břemeno popisného názvu pak leží čistě na implementacích. Pokud
přejmenujeme TranslatorInterface
na Translator
, naše
bývalá třída Translator
potřebuje nové jméno. Lidé mají
tendenci tento problém řešit tak, že ji nazvou
DefaultTranslator
, i já jsem vinen. Ale opět, čím je tak
výjimečná, že se jmenuje Default? Nebuďte líní a pořádně se zamyslete
nad tím, co dělá a proč se to liší od ostatních možných
implementací.
A co když si nedokážu představit více implementací? Co když mě napadá jen jeden platný způsob? Tak prostě rozhraní nevytvářejte. Alespoň prozatím.
Ehle, objevila se další implementace
A je to tady! Potřebujeme druhou implementaci. Stává se to běžně. Nikdy nevznikla potřeba ukládat překlady jiným než jedním osvědčeným způsobem, např. do databáze, ale teď se objevil nový požadavek a je potřeba mít v aplikaci překladačů víc.
To je taky chvíle, kdy si jasně uvědomíte, jaká byla specifičnost původního jediného překladače. Byl to databázový překladač, žádný default.
Co s tím?
- Z názvu
Translator
uděláme rozhraní - Původní třídu přejmenujete na
DatabaseTranslator
a bude implementovatTranslator
- A vytvoříte nové třídy
GettextTranslator
a třebaNeonTranslator
Všechny tyto změny se dělají velmi pohodlně a snadno, obzvlášť pokud
je aplikace postavená v souladu s principy dependency
injection. V kódu není potřeba nic měnit, jen v konfiguraci DI
kontejneru změníme Translator
na DatabaseTranslator
.
To je paráda!
Diametrálně odlišná situace by ale nastala, kdybychom trvali na
prefixování/sufixování. Museli bychom v kódu napříč aplikací
přejmenovat typy z Translator
na TranslatorInterface
.
Takové přejmenování by bylo čistě účelové kvůli dodržení konvence,
ale šlo by proti smyslu OOP, jak jsme si ukázali před chvílí. Rozhraní se
nezměnilo, uživatelský kód se nezměnil, ale konvence vyžaduje
přejmenovávat? Pak jde o chybnou konvenci.
Kdyby se navíc časem ukázalo, že lepší než rozhraní by byla abstraktní třída, přejmenovávali bychom znovu. Takový zásah vůbec nemusí být triviální, třeba když je kód rozložen do více balíčků nebo jej používají třetí strany.
Ale vždyť to tak dělají všichni
Všichni ne. Je pravda, že ve světě PHP zpopularizoval odlišování názvů rozhraní a abstraktních tříd Zend Framework a po něm Symfony, tedy velcí hráči. Tento přístup převzala i PSR, která paradoxně publikuje jen rozhraní, a přesto u každého uvádí v názvu slovo rozhraní.
Na druhou stranu jiný významný framework Laravel rozhraní a abstraktní
třídy nijak neodlišuje. Nedělá to třeba ani populární databázová
vrstva Doctrine. A nedělá to ani standardní knihovna v PHP (máme tak
rozhraní Throwable
nebo Iterator
, abstraktní třídu
FilterIterator
, apod).
Kdybychom se podívali na svět mimo PHP, tak třeba C# používá prefix
I
pro rozhraní, naopak v Javě nebo TypeScriptu se jména
neodlišují.
Nedělají to tedy všichni, nicméně i kdyby dělali, neznamená to, že je to tak dobře. Přebírat bezmyšlenkovitě co dělají ostatní není rozumné, protože můžete přebírat i chyby. Chyby, kterých by se druhý dost možná moc rád sám zbavil, jen je to příliš náročné.
Nepoznám v kódu, co je rozhraní
Řada programátorů bude namítat, že jsou pro ně prefix/sufixy užitečné, neboť díky nim hned v kódu poznají, co jsou rozhraní. Mají pocit, že by jim takové rozlišení chybělo. Tak schválně, poznáte v těchto příkladech, co je třída a co rozhraní?
$o = new X;
class X extends X implements Y
{}
interface Y
{}
X::fn();
X::$v;
X
je vždy třída, Y
je rozhraní, je to
jednoznačné i bez prefixů/postfixů. Samozřejmě ví to i IDE a v daném
kontextu vám bude vždy správně napovídat.
Ale co tady:
function foo(A $param): A
{}
public A $property;
$o instanceof A
A::CONST
try {
} catch (A $x) {
}
V těchto případech to nepoznáte. Jak jsme si řekli úplně na začátku, tady nemá být z pohledu vývojáře rozdíl mezi tím, co je třída a co rozhraní. Což právě dává rozhraním a abstraktním třídám smysl.
Pokud byste zde byli schopni rozlišit třídu od rozhraní, popřelo by to základní princip OOP. A rozhraní by pozbyla smyslu.
Jsem na to zvyklý
Měnit zvyky prostě bolí 🙂 Kolikrát už i ta představa. Ale ať nekřivdíme, řadu lidí změny naopak lákají a těší se na ně, nicméně pro většinu platí, že zvyk je železná košile.
Ale stačí se podívat do minulosti, jak některé zvyklosti odvál čas.
Asi neslavnější je tzv. maďarská notace užívaná od osmdesátých let a
zpopularizovaná Microsoftem. Notace spočívala v tom, že název každé
proměnné začínal zkratkou symbolizující její datový typ. V PHP by to
vypadalo takto echo $this->strName
nebo
$this->intCount++
. Od maďarské notace se začalo ustupovat
v devadesátých letech a dnes Microsoft ve svých pokynech vývojáře od ní
přímo odrazuje.
Kdysi to byla neodmyslitelnost a dnes nikomu nechybí.
Ale proč chodit do tak dávné minulosti? Možná si pamatujete, že v PHP bylo zvykem odlišovat neveřejné členy tříd podtržítkem (ukázka ze Zend Framework). Bylo to v době, kdy už dávno existovalo PHP 5, které mělo modifikátory viditelnosti public/protected/private. Ale programátoři to dělali ze zvyku. Byli přesvědčeni, že bez podtržítek by se přestali orientovat v kódu. „Jak bych v kódu rozeznal veřejné od privátních proměnných, aha?“
Dnes podtržítka nepoužívá nikdo. A nikomu nechybí. Čas skvěle prověřil, že obavy byly liché.
Přitom je to úplně totéž jako námitka: „Jak bych v kódu rozeznal rozhraní od třídy, aha?“
Já přestal používat prefixy/postfixy před deseti lety. Nikdy bych se nevrátil, bylo to skvělé rozhodnutí. Neznám ani žádného jiného programátora, který by se chtěl vrátit. Jak řekl jeden kamarád: „Zkus to a za měsíc nebudeš chápat, že si to někdy dělal jinak.“
Chci udržovat konzistenci
Umím si představit, že si programátor řekne: „Používat předpony a přípony je opravdu nesmysl, chápu to, jenže už mám takto postavený kód a změna je velmi obtížná. A kdybych začal nový kód psát správně bez nich, vznikne mi nekonzistence, která je snad ještě horší, než špatná konvence.“
Ve skutečnosti už teď je váš kód nekonzistentní, protože používáte systémovou knihovnu PHP, která žádné prefixy a postfixy nemá:
class Collection implements ArrayAccess, Countable, IteratorAggregate
{
public function add(string|Stringable $item): void
{
}
}
A ruku na srdce, vadí to? Napadlo vás někdy, že by bylo konzistentnější tohle?
class Collection implements ArrayAccessInterface, CountableInterface, IteratorAggregateInterface
{
public function add(string|StringableInterface $item): void
{
}
}
Nebo tohle?
try {
$command = $this->find($name);
} catch (ThrowableInterface $e) {
return $e->getMessage();
}
Myslím, že ne. Konzistence nehraje tak významnou roli, jak by se mohlo zdát. Naopak oko preferuje méně vizuálního šumu, mozek čistotu návrhu. Tedy upravit konvenci a začít psát nová rozhraní správně bez předpon a přípon dává smysl.
Lze je cíleně odstranit i z velkých projekt. Příkladem je Nette
Framework, který historicky používal prefixy I
v názvech
rozhraní, čehož se před pár lety začal postupně a
s plným zachováním zpětné kompatibility zbavovat.
Komentáře
Imho myslím si že je to takhle správně.
Skvěle shrnuto 👍
Pěkně vysvětleno.
Předpokládám, že totéž platí i pro přípony Exception či Trait, je to tak?
#3 robert.sipek obecně je užitečné mít v názvu třídy krom specifičnosti i obecnost, tedy pojmenovat presenter
ProductPresenter
a ne jenProduct
. TedyException
bych v názvu spíš nechal, ale záleží na osobních preferencích. Třeba u atributů koncovkuAttribute
nepoužívám, protože užití vypadalo blbě.Chybí mi tam spíš čistě praktický důvod a to je, že na začátku projektu nemusím řešit specifičnost. Mám třídu Translator a zatím planuji mít jen jednu. A pokud se náhodou stane, že budou potřeba dvě a více, tak z třídy Translator si udělám TextTranslator + interface Translator. Najednou mám interface a více implementací a nemusel jsem nikde nic refaktorovat a vím, že to bude fungovat.
#5 AntiCZ Vždyť přesně tohle tam je
Aha, tak to pardon. Tohle se mi vytěsnilo.
Chcete-li odeslat komentář, přihlaste se