Hlavní navigace

DI? Rozhodně ano! DI framework? Děkuji, nepotřebuji

5. 5. 2023 7:17 Mirek Marek

Dnes je ještě jednou vrátím k tématu DI a vysvětlím proč Jet není to čemu se říká DI framework i když by bez základního DI princip nefungoval.

Tak pro začátek … Na tomto se určitě shodneme:

Určitě se bez debat shodneme na tom, že psát aplikace třeba takto:

class SomeController extends SomeAbstractController {
    public function someAction() : void
    {
        //...
        $logger = new Logger();
        $logger->log( ‘událost’, $data );
        //...
    }
}

… je zcela špatně. To beru jako naprosto samozřejmou věc.

Logger (v tomto kontextu v roli služby jiné třídy) nemá být instancován a inicializován ono třídou která jej používá – klientem / konzumentem služby.

Pro pořádek proč je to objektivně špatně:

  • Když budu chtít v aplikaci v budoucnu logger vyměnit za jiný, mám problém (ne neřešitelný, ale mám, bude to pracné, náchylné na chyby – vše úměrně k velikosti projektu).
  • Uživatel služby (v tomto příkladu kontroler, ale fakticky jakákoliv jiná třída) se má starat sám o sebe. Nemá tedy co mluvit do toho jak se třeba logger inicializuje. Co když inicializace potřebuje nějaké vstupní parametry? A million dalších důvodů …
  • Jak mohou (třeba i za běhu) aplikace ovlivnit jaký logger bude použit a jak bude nastaven? Prostě nemůžu … A to je velká překážka.
  • Vytvářet stále dokola instance něčeho co by stačilo mít v instanci jedné je i technicky zatěžující (žere to prostředky – zbytečně).
  • No a v neposlední řadě je to šíleně nepohodlné a otravné.

Prostě a jednoduše: Takto (jak je uvedeno v příkladu) se neprogramuje už z principu a programátor takto ani nemůže uvažovat o návrhu aplikace. A v Jetu se tak rozhodně neuvažuje. To beru jako naprostou samozřejmost a něco naprosto přirozeného o čem není třeba diskutovat.

Stejně tak není možné používat statické třídy tohoto charakteru:

class Auth {
    public static getCurrentUser() : ?array
    {
        $current_user_id = static::getCurrentUserID();

        if($current_user_id) {
            $user = Db::read(“SELECT …..”);
            //....
        }

        //....
    }
}

Rozhodně vyloučeno! To je naprosto neflexibilní kód, pro který ani nemusíme mít OOP, ale stačily by pouhé funkce. To je k ničemu … To nemá s OOP vlastně nic moc společného.

I když pozor! Toto se dá později refaktorovat na to co budu popisovat v tomto článku.

Koncept vkládání závislostí nabízí možnost, jak se tomu všemu vyvarovat. Přináší různé abstraktní pojmy (rozhraní, služby, kontejnery, injektory, …) ke kterému já si dnes dovolím přidat další. Pojem, kterému jsem se rozhodl říkat garant služby. Toto označení by mělo lépe vystihovat podstatu věci.

Prostě konečně vysvětlím proč to mám udělané tak a tak, proč to používá DI, ale zároveň nepoužívá předávání služeb formou parametrů konstruktoru. A proč toto řešení považuji za velice dobré řešení pro daný typ aplikací.

Pojďme si to přirovnat k reálnému světu …

Pamatuji si, jak jsem před desítkami let četl knihu vysvětlující (obecné) základy OOP a tam se jako příklad používala zvířata. Kočka, pes … Já si tuto metodu výkladu s dovolením vypůjčím.

Představte si, že ráno vstanete a začíná nový den, během kterého budete dělat spoustu věcí. Dejme tomu, že ráno vzniká naše nová instance naší třídy, která bude bude během dne (životního cyklu – teď se zdržme filozofování a prostě to tak vezmeme jako příklad) potřebovat řadu služeb.

Tak co má člověk v plánu? Dejme tomu, že je to volný den a našinec si to udělá fajn. Ráno zajede do fitka, kde bude potřebovat službu trenér. Pak zajde k kadeřnici, kde jej jeho oblíbená paní ostříhá. Pak určitě bude potřebovat služby číšníka v restauraci, kam si zajde na oběd. A tak dále. Prostě člověk si žije svůj život a používá různé služby.

Tak to chodí, ne?

A teď si představte ten bizár. Ráno mžouráte oči jen co se probouzíte – když se volá váš konstruktor. A u vaší postele už stojí trenér, kadeřnice i číšník. Někdo vám je natlačil až do ložnice s tím, že je budete (možná! to je velice důležité: možná!) potřebovat. Já nevím jak vy, ale já bych hodně zíral …

Když pominu, že toto je samo o sobě dosti bizarní, tak to ani neodpovídá tomu jak se věci na světě dějí.

Taková instance třídy Člověk je dosti komplexní věc řešící mnoho záležitostí (samozřejmě v neustále kooperaci s někým a něčím). A může se krásně stát, že cestou do fitka se instance člověka nečekaně setká s jinou instancí, třeba instancí třídy Lupič a dojde k vyhození výjimky KrádežVTramvaji. Ano, je to extrém, ale stát se může. Pak jsou člověku trenér, kadeřnice i číšník co se s nimi od rána vláčí  už z ložnice přes záchod a koupelnu až na zastávku MHD (pokud by mu je někdo fakt natlačil-injektoval do ložnice) úplně k ničemu. Tyto služby již nebude potřebovat a naopak bude potřebovat služby Policie, ÚřaduMeštskéČásti, Banky a bůh ví co ještě.

Už víte kam směřuji?

Jde o to, že třídy požadující a využívající nějaké služby nejen že nesmí řešit vytváření a inicializaci poskytovatelů těchto služeb, ale dokonce ani není nutné aby držely jejich samostatné instance. A dokonce to v některých (ne všech – k tomu se dostanu) případech není vůbec vhodné.

V reálném světě je to totiž takto:

My se nestaráme o to, jak se kdo stane trenérem, či kadeřnicí. To je nám jedno. A v drtivé většině případů si nesháníme naší osobní kadeřnici, či osobního trenéra ( i když můžeme, ale není to standard ). V reálném životě víme, že jsou věci a instituce, které nám garantují poskytnutí dané služby. A teď nemyslím to, že nám pošlou trenéra a kadeřnici ráno domů. To vůbec nepotřebujeme! My prostě potřebujeme, aby nás někdo chvíli trénoval, aby nás kadeřnice ostříhala, aby náš povoz měl energii na další ježdění, aby nám někdo dovezl jídlo tam kde si jej koupíme a tak dále. V reálném světě potřebujeme aby nám někdo garantoval a zajistil poskytnutí daných služeb a už je v jeho kompetenci jak to udělá a vyřeší si své závislosti a záležitosti – třeba kde a jak sežene a zaplatí majitel kadeřnictví kvalifikovanou kadeřnici.

Mimochodem, malá ale důležitá odbočka … Dost důležitá OOP-filozofická otázka: Je nějaká instance kadeřnice, či trenéra vlastností třídy Člověk? Zdálo by se že je, ale ve skutečnosti ne. Jsou to pouze služby které člověk potřebuje, ne vlastnosti. Vlastnosti jsou výška, váha, datum narození, … A dá se říct, že i instance mého syna je má integrální vlastnost (té se nevzdám a zároveň pevně ovlivňuje můj život). Ale ne instance mé kadeřnice. Protože pokud by kadeřnice byla mou vlastností, tak je vlastně mou vlastností celý svět. I letadlo kterým mohu letět na dovolenou! A to by byl fakt zmatek … Vše by se totálně zamotalo a vše by bylo vlastností všech.  Prosté službu prostě nejsou přirozené vlastnosti tříd – uživatelů / konzumentů služeb, ale pouze a jenom poskytovatelé služeb, kteří nemají s klientem nějakou užší a dočasnou vazbu, lépe řečeno interakci. Nic míň, nic víc.

A pozor! Aby to nebylo pochopeno tak, že žádná služba (její instance) nemá být vlastností třídy. Jako se vším a ve všem záleží na konkrétní situaci.

Pokud by měla mít služba nějaký vnitřní stav relativní ke klientské třídě a mělo by být zaručeno že služba bude i nadále poskytována stejnou službou se stejným vnitřním stavem, tak je rozhodně lepší, aby instance poskytovatele služby byla vlastností klienta / konzumenta služby.

Přirovnejme to opět ke kadeřnici. Pokud by se mělo běžně stávat, že během stříhání šéf té kadeřnice ji odvolá a pošle nám úplně novou, takovou paní která nezná náš požadavek a bude znova nutné ji vysvětlit jak dokončit již započatý střih, tak v takovém případě je rozhodně lepší si danou kadeřnici alespoň po dobu provádění služby držet jako svou instanci – svou vlastnost. Ale hlavní otázka je: Stává se to? Je to reálné? Co je běžná situace a předpokládané chování a co je již nepravděpodobný extrém?

V případě kadeřnice se toto běžně nestává a vlastně stát nesmí. Je v kompetenci garanta služby a je povinností garanta služby nic takového nedopustit, protože by to byla vada poskytované služby.

Ale co třeba instance nějakého spojení někam v rámci třídy? Může jít o spojení na databázi, či již předkonfigurovaný klient nějakého API a tak dále. To je věc, která již má nějaký vnitřní stav (například parametry spojení) a ten je relevantní vůči klientské třídě a navíc by reálně hrozilo že něco zvenčí může tento stav změnit. A ano, v takovém případě je dobré si danou službu držet jako sanostatnou instanci. Otázka je, zda to považovat ještě za službu a jak to nazvat. Je to ještě služba? Ale to už bych moc odbočil a pustil bych se do filozofování. Ale ano, takové situace se dějí a v takových situacích jsou globální stavy nepřípustné a instanci takové věci je nutné držet v kontextu té instance vůči které je to v kontextu. Prostě a jednoduše řečeno: Například instanci spojení na databázi by si měla určitá třída držet u sebe a nemá to být globální. A není to situace, kterou teď řešíme.

Vždy je nutné posoudit, zda to a to je opravdu daná situace a jaké řešení je vhodné použít. Určitá poučka (např. ta o špatnosti globálních stavů) může být pro určitou situaci zcela správná, ale pro určitou situaci naprosto chybná, protože naopak záměrně potřebujeme pravý opak. To si hned ukážeme.

Výhradní garant služby

Služby jako kadeřnici, restaurace, fitka a jakékoliv běžné věci mohu měnit jak libo podle mých preferencí. Ostatně se to tak nějak i z principu očekává. Garanty poskytnutí služeb měníme v životě zcela běžně.

Ale jsou situace, kdy nám danou službu už z principu musí garantovat jen a pouze jedna instituce. Už jsem narazil na to, že náš pomyslný člověk byl bohužel ráno v tramvaji okraden. Bude tedy potřebovat velice choulostivé služby a k tomu jejich garanty. Bude potřebovat Policii ČR (v naději že s tím něco udělá, nebo alespoň dá papír) a státní aparát.

A tu Policii, která reálně šetří kriminalitu máme a musíme mít jen a pouze jednu – musí být globální. Městské a obecní policie jsou něco jiného a moc dobře vědí, kde končí jejich kompetence – je to jiná služba, pro daný moment irelevantní. Státní policie, která má podobné nepříjemnosti v kompetenci je jen a pouze jedna – je to Policie ČR.

A kdybychom měli třeba 2, 3, 4 státní policie snažící se garantovat poskytnutí téže služby, tak by to bylo k ničemu. Byl by to zmatek a celý systém by nefungoval. Garanti služeb by se mezi sebou ve finále hádali a vše by kolidovalo a kolabovalo. Jsou tedy situace, kdy je žádoucí, kdy je naprosto nutné, aby určitou službu garantovala jen a pouze jedna instituce. A pochopitelně i tato instituce má opět své závislosti, své vnější rozhraní (linka 158 a operační oddělení), vnitřní rozhraní, své továrny, své vnitřní subslužby (náboráře, policejní akademii, dodavatele vybavení …)  Ale pro nás běžné smrtelníky je to jeden jediný garant služby a víme že je jeden a chceme aby byl jeden.

Teď se vraťme k začátku dne do ložnice onoho člověka co má tak zběsilý den, že potřebuje Policii ČR. Dokážete si představit, že někdo by byl tak prozíravý a kromě instance konkrétní kadeřnice, konkrétního trenéra a konkrétního číšníka by mu do ložnice k posteli poslal rovnou i třeba policejní hlídku? Bizár, že? Jenže řada lidí tvrdí, že takto se má programovat a takto řešit závislosti všeho na všem. Kdepak … Jde to i jinak.

A teď k našim programům. Není náhodou velká část služeb jako je autentizační a autorizační subsystém, logování, služby mající charakter jádra systému (hlavní router a jeho stav – MVC) a tak dále, nejsou tyto důležité věci už z principu globální? Nejsou to výhradní garanti služeb? Jsou. V těchto určitých situací potřebujeme výhradní garanty služeb. Zde to není špatně, ale naopak – je to žádoucí.

Garant služeb != service locator

To si ujasněme hned na začátku než se hlouběji pustím do pojmu garant služeb. To čemu říkám garant služby není service locator. Možná by se pro to hodil termín fasáda (tak jsem to označoval a dělá to tak i Laravel, k čemuž se ještě dostanu), ale ani to vlastně není úplně přesně a pletlo by se to s tím co dělá právě Laravel – jak jsem si postupně uvědomil a až ex-post mi došlo, že takto to nemusí být zřejmé.

A zároveň se teď vůbec nebavíme o věcech jako je např spojení na jiný systém. Tedy o věcech, které mají existovat v samostatných instancích vázaných na instanci se kterou jsou pro daný účel v kontextu. Někdy je lze chápat spíše jako entity (má to onen vnitřní stav, pro kontext relevantní) a pro jejich vytváření a inicializaci je nejvhodnější použít továrny.

Garant služby je něco o čem vím, že má nějaké jasně dané komunikační rozhraní a princip fungování a vím že mi to poskytne určitou globální službu.

Ale pozor! Garant služby se neřídí jen tím co chce a jak chce on. Dejme stranou kadeřnictví a koukněme se na Policii ČR – to je pro teď mnohem lepší příklad.

O Policii ČR víme, že má poměrně jasně dané role a bude vykonávat poměrně jasně dané služby nám všem. Ale jak přesně to bude dělat? Stanoví si sama pravidla? Nechme stranou, že konkrétní hlídku pošle operační důstojník – takové detaily teď neřešme a koukejme na to z nadhledu.

V normálním fungujícím státě rozhodně způsob práce policie určuje vyšší autorita a policie se řídí zákony, které musí dodržovat. Policie ČR funguje na základě zákonů a pravidel, které jí stanovila zákonodárná moc republiky jakožto vyšší autorita a tato pravidla do Policie vlastně injektovala. Policie (nebo třeba i Armáda ČR – také dobrý příklad) se těmito pravidly řídí a nevymýšlí si vlastní legislativní rámec – vlastní řídící logiku. Ale ostatně i kadeřnický salón musí plnit nějaká pravidla vložená zvenčí, restaurace taktéž. Taktéž i to zda je někdo přijat třeba k té Policii ČR a může se tedy stát poskytovatelem služeb je dáno legislativním rámcem – pravidly vloženými zvenčí vyšší autoritou.  

Garant služeb tedy ve své podstatě neobsahuje tuto základní rozhodovací logiku. Pouze dodržuje to, co je mu dáno vyšší autoritou (a má logiku, která prokazatelně spadá do pouze do jeho kompetence).

Ale service locator funguje takto:

A implementován by byl třeba nějak takto:

class ServiceLocator {
    public static function getLogger() : Logger_Interface
    {
        if( $neco=’A’ ) {
            return new LoggerA();
        }

        if( $neco=’B’ ) {
            return new LoggerA();
        }

        return new LoggerDefault();
    }
}

Tudy cesta nevede. Dělá si to co chce, nepodléhá to žádné kontrole zvenčí … Je to de facto to samé, jako ten příklad, kterým jsem začal na samotném začátku článku, jen jsou tam if() či jiné rozhodování.

Service locator je policie v nějaké totalitní zemi, která se kontroluje zcela sama a nepodléhá rozumné a volené vyšší autoritě a nedá se do ní nic injktovat. A jaké to je? Nic moc … Tu šeď a povinné básničky o Leninovi si já ještě pamatuji …

Lokátor služeb je opravdu špatná cesta. Ale to není příklad garanta služeb.

Garant služeb / výhradní garant služby

V aplikaci je běžné, že pro nějaký účel nutně potřebujeme výhradního garanta služeb. Jako příklad z reálného světa jsem použil Policii ČR. Jako příklad ze světa online aplikací použiji autentizační a autorizační subsystém, který jsem již nakousl.

To prostě musí být výhradní garant daných služeb a musí to mít globální stav. Je to tak trochu taková policie.

Představte si následující situaci v nějaké špatně navržené aplikaci:

Nějaký autentikátor v oné nepovedené aplikaci existuje vždy jako samostatná instance (ne singleton) předávaná vždy jako závislost uživatelům – klientským třídám, například kontrolerům. Ten kdo to navrhoval si někde asi přečetl že singleton či globální věci je fujky-fujky a že vše má předávat jako závislost do konstruktorů a tak nějak si neuvědomil, že to zrovna zde nedává smysl (opakuji: někde poučky platí, někde ne – jde o celkový kontext).

A teď se stane toto:

  1. Vznikne instance třeba nějakého objektu nazvěme jej ObjectA. To má svou instanci autentikátoru kterou nazvěme AuthA (to je pojmenování instance, ne třídy)
  2. ObjectA požádá AuthA o vyhodnocení stavu a ten si od této chvíle myslí (stav si nastaví), že má platného uživatele.
  3. Vznikne ObjectB a má svojí instanci autentikátoru AuthB (stejná třída, jiná instance)
  4. ObjectB na základě nějaké situace zneplatní prostřednictvím AuthB přihlášení uživatele, ale pouze v rámci vnitřního stavu AuthB
  5. Shodou okolností se však ke slovu dostane ještě ObjectA a jeho AuthA a ten si bohužel stále “myslí”, že uživatel je stále platně přihlášený. A třeba pošle do světa informace, které by vůbec poslat neměl. A máme tu bezpečnostní problém.

Co z toho plyne? V některých situacích (opakuji: v některých) je naprosto žádoucí a nutné, aby nějaký stav aplikaci byl globálně sdílený a byl garantován jednou jedinou autoritou. Úplně stejně jako potřebujeme onu jednu Policii ČR.

Tedy pro danou situaci potřebuji jednoho výhradního garanta služeb. Autentizační a autorizační mechanismus je krásný příklad – nikoliv však jediný.

Otázka je: Co ten garant má být? Kde to mít? Kam to “posadit”?

Jako instanci ve formě globální proměnné? Vůbec ne! To nikoho nemůže ani napadnout, protože s tím si každá komponenta aplikace může dělat co chce. Absolutně to není garant služeb. Cokoliv to může rozbít a kdo má vědět kde je jaká zatracená globální proměnná?

Co takhle mít nějakou instanci třídy Application a garanty služeb mít jako její vlastnosti?

class Application {
    protected ?AuthService $auth_service = null;

    public function setAuthService( AuthService_Interface $service ) : void
    {
        $this->auth_service = $service;
    }

    public function getAuthService() : AuthService_Interface
    {
        //Pokud poskytovatel nebude nastaven, jde to prirozene "k zemi"
        return $this->auth_service;
    }
}

To by už asi šlo. No jo, ale ta samotná instance třídy Application bude kde a jako co? A jsme opět na začátku … Opět mám nějakou globální autoritu, která nesmí viset ve vzduchoprázdnu.

Dobře, mohlo by to být statické, to by šlo, ale …

Pak je tu ještě problém. V systému máme neznámý počest služeb / přesněji neznámý počet garantů služeb. Ovšem kdybych měl nějakou třídu Application, tak ta má omezený počet vlastností. Použít dynamické vlastnosti nepřipadá v úvahu (nepřátelské pro IDE, nepřátelské pro automatickou analýzu kódu). Nabízí se mít služby jako nějaké asociované pole a používat to třeba nějak takhle:

$app->service(‘auth’)->getCurrentUser();

Ale to je taky špatně. Opět nepřátelské pro IDE, tedy i pro vývojáře, nepřátelské pro automatickou analýzu kódu a přehlednost …

Já pro to mám jednoduchou frázi: Nejde to v PHP Stormu “prokliknout”? Nenašeptává Storm tak jak má? Tak to fakt není optimální…

Zpět k úvaze. No pak už by mi jako řešení připadalo asi dobré ty služby fakt předávat konstruktorům. A vlastně bych se postupně dostal k tomu podobnému řešení jako u ostatních FW.

Ale nešlo by to jinak?

Malá rekapitulace:

  • Uživatelská třída se nesmí starat o inicializaci svých služeb. To je špatně.
  • Vznik služeb (instance čeho to jsou a jak je to nastavené) by měla být schopna kontrolovat nějaká vyšší entita v aplikačním prostoru. Buď na základě konfigurace (mimochodem to se týká backendů ORM v PHP Jet), nebo okolností a stavu požadavku (inicializátory Jet MVC), nebo jakkoliv je pro daný projekt a situaci třeba. Služby je tedy nutné injektovat injektorem a nad injektorem je potřeba mít určitý druh kontroly (z pohledu vývojáře) v aplikačním prostoru.
  • Uživatelská třída potřebuje službu používat. To je bez debat. Ale potřebuje její instanci / referenci? Však je to přece služba. A uživatelská třída má službu požádat o provedení dané operace (vykonání služby), ale již přece nepotřebuje její referenci (pro jistotu opakuji: v dané situace, jindy je to zcela opačně – to jsem již psal).
  • Samozřejmě věci jako jsou lokátory služeb nepřipadají v úvahu. To už jsme si řekli.
  • Již jsme si vysvětlili, že v daných situacích uživatelská třída nemusí mít (nebo dokonce i nemá mít) k dispozici samotnou instanci poskytovatele služby.
  • Potřebujeme aby to podléhalo vyšší autoritě. Aby to bylo kontrolováno zvenčí.
  • Není vhodné aby vše bylo na jedné hromadě. Každý garant služeb by měl být stavěn přímo na danou garantovanou službu.

Fajn, to by šlo takhle:

Princip je ten, že konzument služby již není závislý na instanci poskytovatele služby, ale ví že v systému je pevně daný bod na který se může spolehnout – garant služby.

Ovšem garant služby již na poskytovateli služby závislý je. A potřebuje aby do něj byl poskytovatel služby vložen – injektován.

Garant služby neobsahuje žádnou vnitřní logiku, která by rozhodovala o tom jaký poskytovatel služby má být použit. Garant služby očekává, že je mu jeho závislost vložena prostřednictvím jeho rozhraní.

Co bude instancí samotného poskytovatele služeb a jak bude poskytovatel služby inicializován už rozhodne nějaký injektor na základě okolností (například inicializátory Jet MVC), nebo prostě a jednoduše při inicializaci aplikace (Jet MVC není mandatorní – lze projekt vyvíjet i bez něj a inicializovat jinak) a to třeba i na základě konfigurace (například backend ORM).

Garant služby tedy může být primitivní třída mající statickou vlastnost (instance poskytovatele služby) a statické metody jako fasádu reflektující rozhraní poskytovatele služby, ale absolutně žádnou další rozhodovací logiku.

Máme cosi globálního a jasně viditelného a všeobecně známého, mohu (nebo i musím) do toho injektovat libovolné poskytovatele služeb, který však musí samozřejmě implementovat dané rozhraní.

Je to vlastně neskutečně primitivní. Ale plně funkční.

Je to opravdu tak? Splňuje to požadavky? 

Ověřme si myšlenku:

  • Má uživatelská třída / klient / konzument služby cokoliv společného s inicializací služby?

    Ne, nemá. Je od toho dokonale izolován.
  • Je někde uvnitř garanta nějaká pevně daná logika, která by rozhodovala o tom jaká instance poskytovatele služby bude použita?

    Ne, není! Garanty služeb nemají žádnou logiku, která by o tom rozhodovala. Čekají na vložení instance služby nadřazenou logikou která je plně v moci vývojáře, ale sami o sobě žádnou takovou logiku neobsahují. Nejsou to tedy lokátory služeb, ale dá se říct, že kontejnery kam se služba vloží, ale zároveň může komunikovat s okolním světem a vykonávat to co mají.

    (Poznámka: V některých případech, jako je například handler aplikačních modulů, garant sice může dostat injektovaného poskytovatele služby, ale pokud se tak nestane, tak inicializuje výchozího poskytovatele služby – v tomto konkrétním případě výchozí handler modulů. Protože je to typické použití aplikace, málo kdo si bude implementovat vlastní handler aplikačních modulů. Ale princip je stále stejný. A týká se to nejen aplikačních modulů, ale např. i hlavního routeru MVC. Prostě vývojář má stále možnost injektovat svého vlastního poskytovatel služby, ale v těchto případech nemusí. To však není rozhodovací logika, ta je stále na injektoru. Je to pouze definice možného výchozího stavu aplikace.  Flexibilita systému zůstává zcela zachována a vývojář není otravován tím, co v 99.9% případů stejně řešit nepotřebuje.)
  • Mám u služeb u kterých potřebuji garantovanou výhradnost a jedinečnost toto garantované?

    Jednoznačně!
  • Je to jednoduché na použití?

    Jednodušší už to asi být nemůže … I juniornější vývojář může psát udržitelný kód a snadno do všeho proniknout. Má to super TCO 🙂 Při troše snahy se to dá pochopit nesrovnatelně rychleji než se naučit nějaký DI framework. Ústně se to dá vysvětlit za pár minut.
  • Odstiňuje to uživatelské třídy od služeb a umožňuje to výměny služeb za jiné?

    Naprosto bez problému.
  • Umožňuje to vznik dalších služeb a jejich garantů?

    Ano, dokonce zcela nezávisle na frameworku, protože je to jen a pouze primitivní myšlenka. Další garanty služeb mohou být v aplikačním prostoru (mimo knihovnu Jet) a klidně mohou začít svou existenci jako prototyp v podobě primitivní statické třídy s možností pozdějšího refactoringu na garanta služby. Nevyžaduje to žádnou technologii z frameworku.
  • Je to testovatelné?

    No jéje! Jednak tomu perfektně rozumí IDE a jeho nástroje na automatické kontroly a analýzy kódu a to bez nutnosti nějakých “kouzel”. Prostě tak jak to je to funguje. No a za druhé je při testech velice snadné vložit mock jakékoliv služby. Tedy klidně nechat logovat do “nikam”, pokud nechceme při testu generovat záznamy logů, nebo vložit pro testy vhodný autorizační a autentizační kontroler a tak dále. Dokonce se dá vložit i vlastní backend pro ORM, či jeho specifické nastavení (vnutit připojení na testovací databázi – například). Nebo je možné některé poskytovatele služeb testovat zcela odděleně aniž by bylo nutné injektovat je do garanta služby. A tak dále.

Celou dobu jsem zde zmiňoval Polici a autentizační a autorizační subsystém, což je takový vzdálený ekvivalent policie v rámci aplikace. Toto je jeho garant služby:

namespace Jet;

class Auth extends BaseObject
{
        protected static Auth_Controller_Interface $controller;

        public static function setController( Auth_Controller_Interface $controller ): void
        {
                static::$controller = $controller;
        }

        public static function getController(): Auth_Controller_Interface
        {
                return static::$controller;
        }

        public static function checkCurrentUser(): bool
        {
                return static::getController()->checkCurrentUser();
        }

        public static function login( string $username, string $password ): bool
        {
                return static::getController()->login( $username, $password );
        }

        public static function logout(): void
        {
                static::getController()->logout();
        }

        public static function getCurrentUser(): Auth_User_Interface|bool
        {
                return static::getController()->getCurrentUser();
        }

        public static function getCurrentUserHasPrivilege( string $privilege, mixed $value=null ): bool
        {
                return static::getController()->getCurrentUserHasPrivilege( $privilege, $value );
        }

        public static function handleLogin(): void
        {
                static::getController()->handleLogin();
        }
}

Naprosto primitivní, ale účinné.

Napadá vás otázka: jak to rozšířit? Pamatuji si, že nad tím jsem se taky zasekl když jsem zkoušel tuto cestu. Ovšem v praxi je to tak, že onu elementární logiku rozšiřovat vlastně nepotřebuji. Potřebuji jednak definovat chování (což mi zajistí onen auth kontroler, jakožto injektovaný poskytovatel služby) a hlavně v praxi potřebuji rozšiřovat hlavně entity. Zde konkrétně uživatele, jehož minimální garantované rozhraní je dáno Auth_User_Interface, ale jinak je to samostatná entita v aplikačním prostoru kterou mohu mít implementovanou jak chci – ve frameworku je jen a pouze ten interface. Od autorizace chci pouze aby mi vrátila přihlášeného uživatele a jakou třídou bude uživatel konkrétně reprezentován už je věcí poskytovatele oné služby – zde toho čemu říkám Auth Controller. Jak bude uživatel implementován? Kde a jak bude uložen? Kolik o něm budu zaznamenávat informací? To je vše věcí aplikace.

No a hlavně … Když má být něco garantované rozhraní, tak to musí být garantované rozhraní. Tedy rozhraní které je pevně dané. Ale není problém vyvořit jiného garanta jiné služby – třeba i podobné a odvozené (dědičnost, přetěžování). Ale garant je prostě garant a aby nebyl zmatek, tak to musí být garant.

Moment, to jsou fasády jak v Laravelu, ne?

Je to hodně podobné, ale není to to samé. I když to má stejné benefity – srozumitelnost a snadnost použití.

Ale Laravel funguje trochu jinak.

Tam jsou to spíš proxy odkazující se na instance služeb v onom hlavním kontejneru, který Laravel má.

V Jetu tento mega kontejner vůbec není. V Jetu je každý garant služby zároveň k tomu uzpůsobeným kontejnerem pro poskytovatele dané služby, přímo pro danou službu stavěný.

Laravel používá pro fasády magickou metodu __callStatic() a funguje takto:

Navíc má Laravel krom toho i pomocné funkce. A rovněž možnost injektovat instance poskytovatelů služeb třeba do kontrolerů (a nejen tam – samozřejmě).

Nepovažuji za úplně šťastné mít X způsobů jak v jednom systému dělat stejnou věc. To může na některých projektech způsobovat nedorozumění a tedy komplikace. I proto má PHP Jet jeden jasně daný princip.

PHP Jet se zcela striktně vyhýbá používání podobných kouzel pro volání metod jako je například __callStatic. V Jetu platí, že co se má volat a používat tak má taky fakticky existovat (vlastnosti i metody musí vždy existovat). Žádná kouzla, ale transparentnost. Jak stále dokola říkám: IDE a nástroje pro analýzu kódu všemu musí ihned rozumět (bez pluginů, extra podpory, nastavení a tak dále). Jen existující kód je fakt přehledný kód.

Jet také nemá jeden mega kontejner na vše, ale každý garant služby je de facto právě pro danou službu zároveň i kontejnerem pro danou službu přímo uzpůsobený. Proto garanty služeb – zdánlivé fasády (sám jsem tak tomu říkal) nejsou zas tak úplně proxy (i když dalo by se o tom dlouze filozofovat). Hlavní je však ten rozdíl, že neexistuje žádný univerzální mega kontejner, ale garant služby je sám o sobě kontejner, který předpokládá, že službu (poskytovatele) dostane od vyšší autority injektovanou.

Laravel je fakt “jen” fasáda, či proxy – jak chcete. Ale opírá se to o služby nacházející se jinde. A používá to “magii”, zatímco v PHP Jet jsou to normální třídy.

Syndrom jedné hromady

V minulém článku o MVC jsem říkal, že není dobré mít třeba definici rout na jedné hromadě.

A úplně to samé platí pro služby a závislosti. Prostě z mnoha důvodů nemám rád házení všeho na jednu hromadu. U MVC to bylo nejmarkantnější, tak jsem o tom napsal samostatný článek.

Ale týká se to všeho. A nebylo by šťastné se těm pomyslným hromadám v jedné části systému vyhýbat a hned vedle je tvořit.

Dovolte mi ještě malou krátkou odbočku do fyzického světa a další přirovnání. Když budete vyvíjet třeba auto, tak jiné principy a jiné požadavky budete mít pro vývoj motoru, jiné na převodovku, jiné na podvozek, jiné na karoserii, jiné na elektroniku. Prostě je to řada věcí, které jsou jiné, často diametrálně odlišné a vyžadující více či méně odlišnou odbornost, ale přesto tvoří jeden celek a vše je spolu propojeno a provázáno. Ale není možné brát poučky a principy z návrhu motorů do oblasti vývoje uživatelského rozhraní používaného řidičem. Mnoho vývojářů si myslí, že určitá dobrá / dobře míněná poučka (“to je dobré”, “to je špatné”) platí globálně. Ne, není tomu tak. Svět je tvořen velice rozmanitými jednotkami. Ale ty pak tvoří jeden celky. Stejně jako třeba to auto. Nebo stejně jako software – včetně našich webových aplikací. Jen (možná by se dalo říct bohužel) ve světě SW to není tak hmatatelné, vše je tak nějak nehmotné, abstraktní. Ale je to tak. 

A ty zmíněne poučky? Nelze říkat „tohle je špatně“, ale je nutné říkat „v té a té dané situaci je to špatně, protože …“, nebo „v té a té situaci je to vhodné, protože …“. Vše má svůj kontext a důvody. Ale teď zpět k tématu.

No a jak jsem již zde zmínil, tak různé garanty služeb se chovají trošku odlišně a je to zcela legitimní. Něco má definovaného výchozího poskytovatele služby, protože se v běžné situaci neočekává nahrazování onoho poskytovatele (ale zároveň jeho nahrazení musí být prostě možné), něco naopak přímo očekává a požaduje injektování poskytovatele služby do garanta. Prostě jablka nejsou jahody i když oboje může být sladké.

Strkat vše do jednoho pytle a házet vše na jednu hromadu není dobré – ani v životě, ani v SW.

Zkusme kouknout na framework jako na malý operační systém 

Už se mi to podařilo vysvětlit? Pokud ne, tak tohle bude možná ještě lepší vysvětlení :-)

Jak jsem již v mnoha článcích vysvětloval, tak PHP Jet je pojatý vlastně jako soustava mikroaplikací, které PHP Jet poskytuje základní rámec (patrné zejména při použití Jet MVC a aplikačních modulů, ale nejen tam).

PHP Jet v plném rozsahu je vlastně tak trochu takový malinkatý operační systém, který má mikro-aplikace.  (Opravdu jen trochu – neberte to doslova! Ale výstižné to je.)

A každý operační systém má nějaké služby, k těm službám se přistupuje přes API či systémová volání. Bez ohledu na to jaký to operační systém je, tak poskytovat služby a zajišťovat transparentně nějaké komunikace (SW ↔ HW, SW ↔ SW, …) je účelem OS.

Každá aplikace ví jak volat potřebné služby daného OS a když je třeba, tak je prostě používá.

OS a jeho API či systémová volání jsou v tomto kontextu vlastně velice často garantem služeb.

A takové ovladače HW a subsystémi pro práci s HW? Co vás napadá? Co to vlastně je? A co třeba subsystém pro takovou běžnou věc jako je zvuk? Co to asi tak jen může být? mrk-mrk! Nebo co síťový subsystém? Však já si na úrovni aplikace také prostě otevřu TCP port někam a nikdo mi nenutí instanci ovladače síťové karty ač teda ve finále to vše co dělám se dostane (po průchodu vrstvami systému) až k tomuto ovladači.

No a teď zpět do webové aplikace.

Co například autentizace a autorizace? To je obecná služba systému a v tomto případě musí existovat v jediné instanci a mít globální stav.

Co třeba logování? To samé. Obecná služba systému, ne služba konkrétního modulu / mikro-aplikace. Modul (nebo jakákoliv komponenta) má prostě logovat (tedy službu používat), kam “to poletí” už je věcí celého systému a předurčeno nadřazenou autoritou, která ví jak se s těmito logovanými informacemi bude zacházet.

A tak dále …

Ovšem v případě PHP Jet má aplikace (ne aplikační modul, ale projekt jako celek) možnost (a v některých případech povinnost) pomocí vkládání závislostí přes rozhraní stanovit a injektovat poskytovatele služeb.

V případě PHP Jet to funguje tak, že se Jet rád přizpůsobí aplikaci (přesněji vývojáři aplikace), vše je možné si uzpůsobit plně ku spokojenosti. Ale Jet stále drží jednotný rámec systému. Stále je garantem služeb. A tak někdo může reimplementovat nějakou systémovou entitu (např. entitu stránek, nebo hlavní router) a ono vše bude fungovat dál. Celek bude držet pohromadě, nerozsype se. (Pokud je ona reimplementace správná – pochopitelně).

PHP Jet v řadě případů přímo vyžaduje, aby si aplikace sama implementovala poskytovatele služeb a ty injektovala do garanta služby. PHP Jet například sám o sobě vůbec nemá implementaci auth kontroleru, ani entity uživatel, role a ani oprávnění, stejně tak žádné loggery. Jet jen a pouze poskytuje rozhraní. Proč? Protože framework opravdu nemůže vědět jaké vlastnosti bude mít třeba uživatel, kde bude vlastně uložen (databázová tabulka opravdu není jediná možnost kde mít uživatele), … Do takových věcí frameworku vlastně nic není. Ten je od toho, aby garantoval základní rámec, existenci určitého subsystému, jeho rozhraní a rozhraní entit do tohoto subsystému náležících. Povšimněte si, že v ukázkové aplikaci jsou věci jako uživatelé a role a auth kontrolery zcela mimo knihovnu, vše je v aplikačním prostoru a plně se očekává, že si to každý upraví jak chce – jsou to pouze ukázky a možný základ od kterého se dá odrazit, ale ne součást frameworku.

A díky vkládání poskytovatelů služeb a systému garantů služeb je extrémně snadné mít například úplně jiný princip autentizace pro REST API, jiný pro web a jiný pro administraci a je možné přidat třeba 100 dalších možností. Vše může být diametrálně odlišné, ale základní rámec garantuje, že základní rozhraní toho všeho bude vždy právě takové a vše se může zeptat zda je vůbec někdo přihlášený. No a pak je snadné dělat takové triky jako přenášet aplikační moduly, opakovaně používat kód na různých místech a tak dále. A mimochodem … Ta podpora REST API je řešená úplně stejně. Opět garant + poskytovatel. O REST API ale bude článek.

Stejně jako aplikace běžící přímo na nějakém OS ví jak zacházet s API a systémovými voláními  OS, tak to samé ví i aplikace (mikro-aplikace / aplikační moduly) v PHP Jetu. Vše v projektu ví že existuje garantované rozhraní. A co je za tím rozhraním? Co a jak službu poskytuje? To už si vývojář může nainjektovat dle libosti. No a stejně jako OS někdy potřebuje dodat třeba ovladače, tak v PHP Jet je někdy nutné dodat poskytovatele služeb.

A samotné použití toho všeho je tak jednoduché, že jednodušší způsob asi neznám. Ale zároveň to zachovává veškerou flexibilitu.

Závěr

Tedy ne, žádné lokátory služeb a podobné věci. Nic takového. Jen naprosto primitivní princip. Nevím zda originální nebo ne  a jestli to má nějakou jinou nálepku, to není podstatné. 

Ale princip funkční a plnící vše co od něj požaduji. Jakkoliv to nebrání flexibilitě vznikajícího projektu. Naopak. Má to jasně daná pravidla, která je možné vysvětlit při setkání “face-2-face” za 15 minut. Není třeba se učit nic složitého. Technicky je to naprosto triviální. Spotřeba prostředků prakticky veškerá žádná.

Zastávám názor, že správné řešení je to nejjednodušší – které samozřejmě zároveň plní požadovaná kritéria. A to systém garantů služeb je.

Dané řešení, ve spolupráci s továrnami a systémovou konfigurací (náhradou za homady konstant) nic neblokuje, ničemu nebrání. Ba naopak. Dá se dělat cokoliv. Naprosto cokoliv. A při tom je to zcela transparentní a směšně jednoduché.

Je to celé jednodušší než vysvětlit to pomocí textu . Tak jsem to dnes snad napravil a věřím, že kdo chce pochopit, tak pochopí.

Tož tak … Framework už mám.

Nu, a teď se musím naučit jak to vysvětlovat 🙂 Snad se mi to dnes trochu povedlo.

Díky za pozornost a mějte se krásně!

Sdílet