Predpone in pripone ne spadajo v imena vmesnikov
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?
- Iz imena
Translator
naredimo vmesnik - Prvotno klaso preimenujete v
DatabaseTranslator
in bo implementiralaTranslator
- In ustvarite nove klase
GettextTranslator
in na primerNeonTranslator
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.
Če želite oddati komentar, se prijavite