Predpone in pripone ne sodijo v imena vmesnikov
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?
- Naredite ime
Translator
vmesnika - Prvotni razred preimenujte v
DatabaseTranslator
in bo implementiralTranslator
- In ustvarite nova razreda
GettextTranslator
in mordaNeonTranslator
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.
Če želite oddati komentar, se prijavite