Predpone in pripone ne sodijo v imena vmesnikov

pred 2 leti od David Grudl  

Uporaba pripone I prefix or Interface za vmesnike in Abstract za abstraktne razrede je protiustavna. Ne sodi v čisto kodo. Razlikovanje imen vmesnikov dejansko zamegljuje načela OOP, v kodo vnaša šum in povzroča zaplete pri razvoju. Tukaj so razlogi.

Tip = razred + vmesnik + potomci

V svetu OOP se tako razredi kot vmesniki štejejo za tipe. Če pri razglasitvi lastnosti ali parametra uporabim tip, z vidika razvijalca ni razlike med tem, ali je tip, na katerega se zanašajo, razred ali vmesnik. To je pravzaprav tista kul stvar, zaradi katere so vmesniki tako uporabni. To je tisto, kar daje njihovemu obstoju smisel. (Resno: za kaj bi bili vmesniki, če to načelo ne bi veljalo? Poskusite razmisliti o tem.)

Oglejte si to kodo:

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

Konstruktor pravi: "Potrebujem obrazec in prevajalnik. " In mu je vseeno, ali dobi objekt GettextTranslator ali objekt DatabaseTranslator. In hkrati je meni kot uporabniku vseeno, ali je Translator vmesnik, abstraktni razred ali konkreten razred.

Ali mi je res vseeno? Pravzaprav ne, priznam, da sem precej radoveden, zato, ko raziskujem knjižnico nekoga drugega, pokukam, kaj se skriva za tipom, in se pomaknem nadenj:

Aha, vidim. In to je konec zgodbe. Vedeti, ali gre za razred ali vmesnik, bi bilo pomembno, če bi želel ustvariti njegov primerek, vendar ni tako, zdaj govorim samo o vrsti spremenljivke. In tu se želim izogniti tem podrobnostim izvajanja. Vsekakor pa jih ne želim vgraditi v svojo kodo! To, kar je v ozadju tipa, je del njegove definicije in ne tip sam.

Zdaj si oglejte drugo kodo:

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

Ta definicija konstruktorja dobesedno pravi: "Potrebujem abstraktno obliko in vmesnik prevajalnika. " Toda to je neumno. Potrebuje konkretno obliko za preslikavo. Ne abstraktno obliko. In potrebuje objekt, ki deluje kot prevajalnik. Ne potrebuje vmesnika.

Veste, da besed Interface in Abstract ne smete upoštevati. Da konstruktor želi isto stvar kot v prejšnjem primeru. Ampak… res? Ali se res zdi dobra zamisel, da bi v poimenovalne konvencije uvedli uporabo besed, ki jih je treba ignorirati?

Navsezadnje to ustvarja napačno predstavo o načelih OOP. Začetnik mora biti zmeden: “Če tip Translator pomeni 1) objekt razreda Translator 2) objekt, ki implementira vmesnik Translator, ali 3) objekt, ki deduje od njih, kaj potem pomeni TranslatorInterface?” Na to vprašanje ni razumnega odgovora.

Ko uporabimo TranslatorInterface, čeprav je Translator lahko vmesnik, zagrešimo tavtologijo. Enako se zgodi, ko deklariramo interface TranslatorInterface. Postopoma se bo zgodila programerska šala:

interface TranslatorInterface
{
}

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

Izjemno izvajanje

Ko vidim nekaj takega, kot je TranslatorInterface, verjetno obstaja izvedba z imenom Translator implements TranslatorInterface. Zato se sprašujem: kaj je Translator tako posebnega, da ima edinstven privilegij, da se imenuje Prevajalnik? Vsaka druga implementacija potrebuje opisno ime, na primer GettextTranslator ali DatabaseTranslator, ta pa je nekako “privzeta”, kar nakazuje njen prednostni status, da se imenuje Translator brez oznake.

Celo ljudje se zmedejo in ne vedo, ali naj tipkajo za Translator ali TranslatorInterface. Potem se v odjemalčevi kodi obe pomešata, prepričan sem, da ste na to že večkrat naleteli (v Nette na primer v povezavi s Nette\Http\Request proti IRequest).

Ali ne bi bilo bolje, da se znebimo posebne implementacije in za vmesnik ohranimo generično ime Translator? To pomeni, da bi imeli posebne implementacije s posebnim imenom + splošni vmesnik s splošnim imenom. To je smiselno.

Breme opisnega imena je potem izključno na izvajalcih. Če preimenujemo TranslatorInterface v Translator, potrebuje naš nekdanji razred Translator novo ime. Ljudje ta problem običajno rešujejo tako, da ga poimenujejo DefaultTranslator, celo sam sem kriv tega. Toda še enkrat, kaj ga dela tako posebnega, da se imenuje Default? Ne bodite leni in dobro premislite, kaj počne in zakaj se razlikuje od drugih možnih izvedb.

In kaj, če si ne morem predstavljati več možnih izvedb? Kaj pa, če si lahko zamislim samo en veljaven način? Torej preprosto ne ustvarite vmesnika. Vsaj za zdaj.

Eh, obstaja še ena izvedba

In tukaj je! Potrebujemo drugo izvajanje. To se dogaja ves čas. Nikoli ni bilo potrebe po shranjevanju prevodov na več kot en preverjen način, npr. v podatkovni zbirki, zdaj pa se je pojavila nova zahteva in v aplikaciji moramo imeti več prevajalnikov.

Takrat se tudi jasno zavemo, kakšne so bile posebnosti prvotnega enega prevajalnika. To je bil prevajalnik za podatkovno zbirko, brez privzetega.

Kaj je bilo s tem?

  1. Naredite ime Translator vmesnika
  2. Prvotni razred preimenujte v DatabaseTranslator in bo implementiral Translator
  3. In ustvarite nova razreda GettextTranslator in morda NeonTranslator

Vse te spremembe so zelo priročne in enostavne za izvedbo, zlasti če je aplikacija zgrajena po načelih vbrizgavanja odvisnosti. Ničesar ni treba spreminjati v kodi, samo v konfiguraciji vsebnika DI spremenite Translator v DatabaseTranslator. To je odlično!

Vendar pa bi nastala diametralno drugačna situacija, če bi vztrajali pri prefiksiranju/sufiksiranju. V kodi celotne aplikacije bi morali vrste preimenovati iz Translator v TranslatorInterface. Takšno preimenovanje bi bilo zgolj zaradi konvencionalnosti, vendar bi bilo v nasprotju z duhom OOP, kot smo pravkar pokazali. Vmesnik se ni spremenil, uporabniška koda se ni spremenila, vendar konvencija zahteva preimenovanje? Potem je to pomanjkljiva konvencija.

Poleg tega, če bi se sčasoma izkazalo, da je abstraktni razred boljši od vmesnika, bi spet preimenovali. Takšno dejanje morda sploh ne bo trivialno, na primer kadar je koda razpršena po več paketih ali jo uporabljajo tretje osebe.

Vendar to počnejo vsi

Vsi tega ne počnejo. Res je, da je v svetu PHP Zend Framework, ki mu je sledil Symfony, veliki igralci, populariziral razlikovalni vmesnik in abstraktna imena razredov. Ta pristop je prevzel PSR, ki ironično objavlja samo vmesnike, vendar v imenu vsakega od njih vključuje besedo vmesnik.

Po drugi strani pa drugo veliko ogrodje, Laravel, ne razlikuje med vmesniki in abstraktnimi razredi. Tega na primer ne počne niti priljubljeni sloj za podatkovne zbirke Doctrine. Prav tako tega ne počne standardna knjižnica v PHP (tako imamo vmesnike Throwable ali Iterator, abstraktni razred FilterIterator itd.).

Če pogledamo svet zunaj PHP, C# za vmesnike uporablja predpono I, medtem ko se v Javi ali TypeScriptu imena ne razlikujejo.

Zato tega ne počnejo vsi, a tudi če bi to počeli, to še ne pomeni, da je to dobro. Ni pametno brezglavo prevzemati tega, kar počnejo drugi, saj lahko s tem prevzamete tudi napake. Napake, ki bi se jih drugi raje znebili, saj je to dandanes prevelik zalogaj.

V kodi ne vem, kaj je vmesnik

Številni programerji bodo trdili, da so predponske oz. sufiksne oznake zanje koristne, ker jim omogočajo, da v kodi takoj vedo, kaj so vmesniki. Menijo, da bi takšno razlikovanje pogrešali. Poglejmo, ali lahko v teh primerih ugotovite, kaj je razred in kaj vmesnik?

$o = new X;

class X extends X implements Y
{}

interface Y
{}

X::fn();

X::$v;

X is always a class, Y je vmesnik, to je nedvoumno tudi brez predpon/postfiksov. Seveda to ve tudi IDE in vam bo v danem kontekstu vedno dal pravilen namig.

Kaj pa v tem primeru:

function foo(A $param): A
{}

public A $property;

$o instanceof A

A::CONST

try { ... } catch (A $x) { ... }

V teh primerih tega ne boste vedeli. Kot smo povedali že na začetku, z vidika razvijalca ne bi smelo biti razlike med tem, kaj je razred in kaj vmesnik. Prav to daje vmesnikom in abstraktnim razredom smisel.

Če bi tu lahko razlikovali med razredom in vmesnikom, bi zanikali osnovno načelo OOP. In vmesniki bi postali nesmiselni.

Navadil sem se na to

Spreminjanje navad pač boli 🙂 Pogosto boli že sama zamisel o tem. Ne krivimo ljudi, ki jih spremembe privlačijo in se jih veselijo, saj za večino velja, kot pravi češki pregovor, da je navada železna srajca.

Toda samo poglejte v preteklost, kako je nekatere navade odnesel čas. Morda je najbolj znana tako imenovana madžarska notacija, ki se uporablja od osemdesetih let prejšnjega stoletja in jo je populariziral Microsoft. Zapis je bil sestavljen tako, da se je ime vsake spremenljivke začelo s kratico, ki je simbolizirala njeno podatkovno vrsto. V jeziku PHP bi bil videti kot echo $this->strName ali $this->intCount++. Madžarski zapis se je začel opuščati v devetdesetih letih, danes pa Microsoftove smernice razvijalce neposredno odvračajo od njegove uporabe.

Včasih je bila to bistvena funkcija in danes je nihče ne pogreša.

Toda zakaj bi se vračali v tako davno preteklost? Morda se spomnite, da je bilo v PHP-ju običajno, da smo nejavne člane razredov razlikovali s podčrtalnikom (vzorec iz Zend Framework). To je bilo še v času, ko je obstajal PHP 5, ki je imel modifikatorje vidnosti public/protected/private. Toda programerji so to počeli iz navade. Prepričani so bili, da bodo brez podčrtajev nehali razumeti kodo. “Kako bi v kodi razlikoval med javnimi in zasebnimi lastnostmi, kajne?”

Danes podčrtajev ne uporablja nihče več. In nihče jih ne pogreša. Čas je odlično dokazal, da so bili strahovi napačni.

Vendar je to popolnoma enako kot ugovor: “Kako bi v kodi ločil vmesnik od razreda, kajne?”

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

Želim ohraniti doslednost

Lahko si predstavljam programerja, ki pravi: “Uporaba predpon in končnic je res nesmisel, razumem, samo da imam svojo kodo že zgrajeno na ta način in jo je zelo težko spremeniti. In če začnem pisati novo kodo pravilno brez njih, bom na koncu imel nedoslednost, kar je morda še slabše od slabe konvencije.”

Pravzaprav je vaša koda že zdaj nekonsistentna, ker uporabljate sistemsko knjižnico PHP:

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

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

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

Ali to?

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

Mislim, da ne. Doslednost nima tako velike vloge, kot se morda zdi. Nasprotno, oko ima raje manj vizualnega šuma, možgani pa imajo raje jasnost zasnove. Zato je prilagoditev konvencije in začetek pravilnega pisanja novih vmesnikov brez predpon in pripon smiselna.

Lahko jih namenoma odstranite tudi iz velikih projektov. Primer je ogrodje Nette, ki je v preteklosti v imenih vmesnikov uporabljalo predpone I, ki jih je pred nekaj leti začelo postopoma odpravljati, pri čemer je ohranilo popolno združljivost za nazaj.