Předpony a přípony do názvů rozhraní nepatří

před 2 lety od David Grudl  

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?

  1. Z názvu Translator uděláme rozhraní
  2. Původní třídu přejmenujete na DatabaseTranslator a bude implementovat Translator
  3. A vytvoříte nové třídy GettextTranslator a třeba NeonTranslator

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

  1. Imho myslím si že je to takhle správně.

    před 2 lety
  2. Skvěle shrnuto 👍

    před 2 lety
  3. Pěkně vysvětleno.
    Předpokládám, že totéž platí i pro přípony Exception či Trait, je to tak?

    před 2 lety · replied [4] David Grudl
  4. #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 jen Product. Tedy Exception bych v názvu spíš nechal, ale záleží na osobních preferencích. Třeba u atributů koncovku Attribute nepoužívám, protože užití vypadalo blbě.

    před 2 lety
  5. 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.

    před 2 lety · replied [6] David Grudl
  6. #5 AntiCZ Vždyť přesně tohle tam je

    před 2 lety
  7. Aha, tak to pardon. Tohle se mi vytěsnilo.

    před 2 lety

Chcete-li odeslat komentář, přihlaste se