Predpone in pripone ne spadajo v imena vmesnikov

pred 3 leti od David Grudl  

Uporaba predpone I ali pripone Interface pri vmesnikih, prav tako Abstract pri abstraktnih razredih, je antipattern. V čisti kodi nima kaj iskati. Razlikovanje imen vmesnikov v resnici zamegljuje principe OOP, vnaša v kodo šum in povzroča zaplete pri razvoju. Razlogi so naslednji.

Tip = razred + vmesnik + potomci

V svetu OOP se razredi in vmesniki štejejo za tipe. Če uporabim tip pri deklaraciji lastnosti ali parametra, z vidika razvijalca ni razlike med tem, ali je tip, na katerega se zanaša, razred ali vmesnik. To je odlična stvar, zahvaljujoč kateri so vmesniki pravzaprav tako uporabni. To daje njihovemu obstoju smisel. (Resno: za kaj bi bili vmesniki, če ta princip ne bi veljal? Poskusite razmisliti o tem.)

Poglejte si to kodo:

class FormRenderer
{
	public function __construct(
		private Form $form,
		private Translator $translator,
	) {
	}
}

Konstruktor pravi: “Potrebujem obrazec in prevajalnik.” In mu je popolnoma vseeno, ali dobi objekt GettextTranslator ali DatabaseTranslator. In hkrati kot uporabniku mi je popolnoma vseeno, ali je Translator vmesnik, abstraktni razred ali konkretni razred.

Mi je popolnoma vseeno? Pravzaprav ne, priznam se, da sem precej radoveden, zato ko raziskujem tujo knjižnico, pokukam, kaj se skriva za tipom, in nanj postavim miško:

Aha, tako zdaj vem. In s tem se konča. Znanje, ali gre za razred ali vmesnik, bi bilo pomembno, če bi želel ustvariti njegovo instanco, ampak to ni ta primer, zdaj le govorim o tipu spremenljivke. In tukaj želim biti od teh podrobnosti zaščiten. In sploh jih nočem vnašati v svojo kodo! Kaj se za tipom skriva je del njegove definicije, ne tipa samega.

In zdaj se poglejte naslednjo kodo:

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

Ta definicija konstruktorja dobesedno pravi: “Potrebujem abstraktni obrazec in vmesnik prevajalnika.” Ampak to je neumnost. Potrebuje konkreten obrazec, ki ga mora izrisati. Ne abstraktnega obrazca. In potrebuje objekt, ki opravlja nalogo prevajalnika. Ne potrebuje vmesnika.

Vi veste, da se besedi Interface in Abstract morata ignorirati. Da konstruktor želi enako kot v prejšnjem primeru. Ampak… resno? Res se vam zdi dobra ideja si v imenske konvencije uvesti uporabo besed, ki se morajo spregledati?

Saj ustvarja napačno predstavo o principih OOP. Začetnik mora biti zmeden: „Če se s tipom Translator razume bodisi 1) objekt razreda Translator 2) objekt implementirajoč vmesnik Translator ali 3) objekt od njih dedujoč, kaj se potem razume s tem TranslatorInterface?“ Na to ni mogoče razumno odgovoriti.

Ko pišemo TranslatorInterface, čeprav tudi Translator lahko je interface, se lotevamo tavtologije. Enako ko deklariramo interface TranslatorInterface. In tako dalje. Dokler ne nastane programatorski vic:

interface TranslatorInterface
{
}

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

Izjemna implementacija

Ko vidim nekaj kot TranslatorInterface, je verjetno, da bo obstajala tudi implementacija z imenom Translator implements TranslatorInterface. Sili me k razmišljanju: s čim je Translator tako izjemen, da ima edinstveno pravico imenovati se Translator? Vsaka druga implementacija potrebuje opisno ime, na primer GettextTranslator ali DatabaseTranslator, ampak ta je nekako “privzeta”, kot nakazuje njen prednostni položaj, ko se imenuje Translator brez pridevnika.

Celo ljudi zmede in ne vejo, ali naj pišejo typehint za Translator ali TranslatorInterface. V klientski kodi se potem meša oboje, zagotovo ste na to že mnogokrat naleteli (v Nette na primer v povezavi z Nette\Http\Request vs IRequest).

Ali ne bi bilo bolje se izjemne implementacije znebiti in pustiti splošen naziv Translator za vmesnik? Torej imeti konkretne implementacije s konkretnim imenom + splošen vmesnik s splošnim imenom. To ima vendar smisel.

Breme opisnega imena potem leži čisto na implementacijah. Če preimenujemo TranslatorInterface v Translator, naša bivša klasa Translator potrebuje novo ime. Ljudje imajo tendenco ta problem reševati tako, da jo imenujejo DefaultTranslator, tudi jaz sem kriv. Ampak spet, s čim je tako izjemna, da se imenuje Default? Ne bodite leni in se temeljito zamislite nad tem, kaj dela in zakaj se razlikuje od ostalih možnih implementacij.

In kaj če si ne morem predstavljati več implementacij? Kaj če mi pride na misel le en veljaven način? Potem preprosto vmesnika ne ustvarjajte. Vsaj zaenkrat.

Ehle, pojavila se je še ena implementacija

In je tu! Potrebujemo drugo implementacijo. To se dogaja pogosto. Nikoli ni nastala potreba po shranjevanju prevodov drugače kot z enim preizkušenim načinom, npr. v podatkovno bazo, ampak zdaj se je pojavila nova zahteva in je potrebno imeti v aplikaciji prevajalcev več.

To je tudi trenutek, ko si jasno ugotovite, kakšna je bila specifičnost prvotnega edinega prevajalca. Bil je podatkovni prevajalec, noben privzeti.

Kaj s tem?

  1. Iz imena Translator naredimo vmesnik
  2. Prvotno klaso preimenujete v DatabaseTranslator in bo implementirala Translator
  3. In ustvarite nove klase GettextTranslator in na primer NeonTranslator

Vse te spremembe se delajo zelo udobno in enostavno, posebej če je aplikacija zgrajena v skladu s principi dependency injection. V kodi ni treba ničesar spreminjati, le v konfiguraciji DI vsebnika spremenimo Translator v DatabaseTranslator. To je super!

Diametralno drugačna situacija bi pa nastala, če bi vztrajali na predponah/sufiksih. Morali bi v kodi po vsej aplikaciji preimenovati tipe iz Translator v TranslatorInterface. Takšno preimenovanje bi bilo čisto namensko zaradi upoštevanja konvencije, ampak bi šlo proti smislu OOP, kot smo si pokazali pred trenutkom. Vmesnik se ni spremenil, uporabniška koda se ni spremenila, ampak konvencija zahteva preimenovati? Potem gre za napačno konvencijo.

Če bi se poleg tega sčasoma izkazalo, da bi bila boljša kot vmesnik abstraktna klasa, bi preimenovali ponovno. Takšen poseg sploh ne bi bil trivialen, na primer če je koda razdeljena na več paketov ali jo uporabljajo tretje strani.

Ampak saj to delajo vsi

Vsi ne. Res je, da je v svetu PHP populariziral razlikovanje imen vmesnikov in abstraktnih razredov Zend Framework in za njim Symfony, torej veliki igralci. Ta pristop je prevzela tudi PSR, ki paradoksalno objavlja le vmesnike, in kljub temu pri vsakem navaja v imenu besedo vmesnik.

Na drugo stran drug pomemben framework Laravel vmesnike in abstraktne razrede nikakor ne razlikuje. Ne dela tega na primer niti popularna podatkovna plast Doctrine. In ne dela tega niti standardna knjižnica v PHP (imamo tako vmesnika Throwable ali Iterator, abstraktno klaso FilterIterator, ipd.).

Če bi se pogledali na svet izven PHP, tako na primer C# uporablja predpono I za vmesnike, nasprotno v Javi ali TypeScriptu se imena ne razlikujejo.

Ne delajo tega torej vsi, vendar tudi če bi delali, ne pomeni, da je to tako dobro. Prevzemati brez razmišljanja kaj delajo ostali ni razumno, ker lahko prevzamete tudi napake. Napake, katerih bi se drugi verjetno zelo radi sami znebili, le da je to preveč zahtevno.

Ne poznam v kodi, kaj je vmesnik

Številni programerji bodo ugovarjali, da so jim predpone/sufiksi koristni, saj zahvaljujoč njim takoj v kodi prepoznajo, kaj so vmesniki. Imajo občutek, da bi jim takšno razlikovanje manjkalo. Pa poglejmo, ali prepoznate v teh primerih, kaj je klasa in kaj vmesnik?

$o = new X;

class X extends X implements Y
{}

interface Y
{}

X::fn();

X::$v;

X je vedno klasa, Y je vmesnik, je enoznačno tudi brez predpon/sufiksov. Seveda ve to tudi IDE in v danem kontekstu vam bo vedno pravilno predlagal.

Ampak kaj tukaj:

function foo(A $param): A
{}

public A $property;

$o instanceof A

A::CONST

try {
} catch (A $x) {
}

V teh primerih tega ne prepoznate. Kot smo si rekli čisto na začetku, tukaj nima biti z vidika razvijalca razlike med tem, kaj je klasa in kaj vmesnik. Kar prav daje vmesnikom in abstraktnim razredom smisel.

Če bi tukaj bili sposobni razlikovati klaso od vmesnika, bi to zanikalo osnovni princip OOP. In vmesniki bi izgubili smisel.

Sem na to navajen

Spreminjati navade preprosto boli 🙂 Kolikokrat že samo ta predstava. Ampak da ne krivdimo, številne ljudi spremembe nasprotno privlačijo in se jih veselijo, vendar za večino velja, da je navada železna srajca.

Ampak dovolj je pogledati v preteklost, kako nekatere navade je odnesel čas. Verjetno najbolj znana je t.i. madžarska notacija uporabljana od osemdesetih let in popularizirana s strani Microsofta. Notacija je temeljila na tem, da se je ime vsake spremenljivke začelo s kratico simbolizirajočo njen podatkovni tip. V PHP bi to izgledalo tako echo $this->strName ali $this->intCount++. Od madžarske notacije se je začelo odstopati v devetdesetih letih in danes Microsoft v svojih navodilih razvijalce od nje neposredno odvrača.

Nekoč je bila to nepogrešljivost in danes nikomur ne manjka.

Ampak zakaj hoditi v tako davno preteklost? Morda se spomnite, da je bilo v PHP navada razlikovati nejavne člane razredov s podčrtajem (ukázka ze Zend Framework). Bilo je v času, ko je že davno obstajalo PHP 5, ki je imelo modifikatorje vidnosti public/protected/private. Ampak programerji so to delali iz navade. Bili so prepričani, da brez podčrtajev bi se prenehali orientirati v kodi. „Kako bi v kodi razlikoval javne od zasebnih spremenljivk, aha?“

Danes podčrtajev ne uporablja nihče. In nikomur ne manjkajo. Čas je odlično preveril, da so bili strahovi neutemeljeni.

Pri tem je to popolnoma enako kot ugovor: „Kako bi v kodi razlikoval vmesnik od klase, aha?“

Jaz sem prenehal uporabljati predpone/sufikse pred desetimi leti. Nikoli se ne bi vrnil, bilo je odlična odločitev. Ne poznam niti nobenega drugega programerja, ki bi se želel vrniti. Kot je rekel en prijatelj: „Poskusi to in čez mesec dni ne boš razumel, da si kdaj delal drugače.“

Želim ohranjati doslednost

Lahko si predstavljam, da si programer reče: „Uporabljati predpone in sufikse je res nesmisel, razumem to, le da imam že tako postavljen kód in sprememba je zelo težka. In če bi začel nov kód pisati pravilno brez njih, bi mi nastala nedoslednost, ki je morda še hujša, kot slaba konvencija.“

V resnici že zdaj je vaš kód nedosleden, ker uporabljate sistemsko knjižnico PHP, ki nima nobenih predpon in sufiksov:

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

In roko na srce, ali to moti? Ste kdaj pomislili, da bi bilo bolj dosledno tole?

class Collection implements ArrayAccessInterface, CountableInterface, IteratorAggregateInterface
{
	public function add(string|StringableInterface $item): void
	{
	}
}

Ali tole?

try {
	$command = $this->find($name);
} catch (ThrowableInterface $e) {
	return $e->getMessage();
}

Mislim, da ne. Doslednost ne igra tako pomembne vloge, kot bi se lahko zdelo. Nasprotno oko preferira manj vizualnega šuma, možgani čistoto načrta. Torej prilagoditi konvencijo in začeti pisati nove vmesnike pravilno brez predpon in sufiksov ima smisel.

Lahko jih ciljno odstranimo tudi iz velikih projektov. Primer je Nette Framework, ki je zgodovinsko uporabljal predpone I v imenih vmesnikov, česar se je pred nekaj leti začel postopoma in s polnim ohranjanjem povratne združljivosti znebiti.