PHP Jet - Dependency Injection, továrny a tak dále

10. 2. 2023 9:38 (aktualizováno) Mirek Marek

Dnešní článek, u kterého Vás srdečně vítám, bude opět o SW architektuře a filozofii PHP Jet. Tentokrát se podíváme na zajímavé téma Dependency Injection, továrem a další zajímavé věci. Tak jdeme na to.

Dependency Injection – tak takto ne!

Nejprve je bohužel potřeba vyvrátit mýty, které šíří například vážený kolega David Grudl v Nette, ale nejen on a nejen v Nette.

Zapomeňte na 1. pravidlo – „používej parametry“. To pravidlo sice platí, ale ne univerzálně. Jeho špatné pochopení a používání vede k tvorbě dlouhodobě těžko udržitelných velkých projektů. A hned přejdu k argumentů.

Kolega uvádí jako příklad článek, který nemá vědět jak a kam se ukládá. Ale pravý opak je pravdou. Nechme pro teď být článek článkem a pojďme na mé oblíbené e-shopy.

Mám entitu, která představuje zboží. Instance této entity se používá (bez nadsázky) na stovkách míst v rámci projektů (ne jednoho projektu, ale projektů). Nahrává se a ukládá, aktualizuje, dotazuje .. A to vše za účelem všemožných operací – ať je to práce („klikání“) v administrací, zobrazení stránky produkty zákazníkovi, či třeba export na nějaké tržiště, sync s ERP, export do XML a tak dále.

Na stovkách míst projektu prostě vím, že potřebuji určité zboží a je mi úplně jedno jak se instance zboží nahraje a odkud, či naopak jak se uloží a kam. To jiná komponenta aplikace nemá / nesmí vůbec řešit – je to čistě vnitřní věc entity. To je totiž jeden ze základních principů a smyslu existence OOP

Jako uživatel třídy neřeším její vnitřní implementaci. Nezajímá mě. Vím jaké má třída vnější rozhraní a to používám. A to rozhraní je ideálně unifikované (případně pouze rozšiřované – ať žije dědičnost) v rámci daného projektu, případně v rámci celého systému na kterém může být postaveno X projektů. Tedy jeden e-shop, nebo CMS použiji na X projektech, ale na všech budu mít určitou garantovanou množinu tříd a jejich metod, které se budou chovat předpokládaným způsobem. A je úplně jedno že vnitřní implementace se může (a v praxi bude) projekt od projektu do jisté míry lišit. Mimochodem – zde se dá použít nad dědičností vrstvení, ale o tom třeba jindy.

Vím prostě, že 

$product = Product::get( $id ); 

mi vrátí instanci produktu (nebo null, pokud neexistuje) a jak to třída Product udělá už je její věc.

Stejně tak vím, že když chci konečnou prodejní cenu s DPH, tak zavolám 

$price = $product->getFinalPriceWithVAT(); 

A mimochodem ani mě nenapadne operovat s (public) vlastnostmi třídy. To je mimochodem další velký a škodlivý zlozvyk! S velkými a dlouhodobými projekty naprosto neslučitelný.

Proč? Pro legraci králíkům to opravdu není … Ukažme si opět konkrétní situaci:

Příklad: Změna typu úložiště a celého workflow ukládání

Na jednom z projektů potřebujete do vrstvy nahrání a ukládání instance entity přidat keš. Třeba Redis. A potřebujete to aplikovat na celý projekt.

Měnit třeba parametry konstruktoru v celém projektu, nebo na X velkých projektech? Naprosto nemyslitélné.

OK, podle oné chybné filozofie z článku na který odkazuji bych například použil tovární metody a kontejner (což však není kontejner v abstraktním smyslu OOP – o tom později) a měl s tím různé „oplejtačky“ – např. tahal další subsystém, který ale prakticky vůbec nepotřebuji.

Ne, nic takového nechci. Prostě upravím vnitřní implementaci třídy a hotovo. Potřebné závislosti (zde v tomto příkladu třeba klienta pro Redis) si vytáhnu ad-hoc uvnitř třídy v momentě, kdy jej opravdu potřebuji a pouze pokud jej opravdu potřebuji. Ne a priory, ale ad hoc. To je mimochodem jeden z důležitých klíčů k tomu, aby online aplikace byla výkonná – což je jaksi dosti zásadní věc.

Příklad: Změna cenotvorby

Teď trošku odbočím protože to zdánlivě není tak úplné o DI, ale i zde může existovat závislost – jak si záhy ukážeme.

Přímém používání public vlastností tříd, kde to fakt není vhodné (což je 99% situací) je opravdu strašný zlozvyk a ještě horší je používání „kouzelných“ (neexistujících) vlastností přes magické metody.

V PHP Jet jsou na všechny vlastnosti gettery a settery. Teda ne že by někde nebylo možné použít přímo vlastnost, ale je dobré mít jeden styl a ne „každý pes, jiná ves“ – proto jsou gettery a settery všude.

A to hlavní! Člověk prostě stejně neví co bude za pár let. Tedy: teď v tuto chvíli může být OK operovat přímo s public vlastností, ale za rok to může znamenat opravdu bolehlav.

Ukažme si to názorně. Onen příklad s prodejní cenou produktu s DPH – to je krásná modelová ukázka. Všude potřebuji konečnou cenu s DPH – na desítkách a stovkách míst projektu a projektů.

Ale vlastně na každém projektu mám jiný postup jak tu cenu určit. Už rozdílné sazby DPH jsou základní problém, ale je to rutinní věc.

V praxi máme takové například „maličkosti“, jako že zákazník má věrnostní slevu, nebo individuální slevu, …A už jsme opět u nějaké závislosti jedné entity na druhé! Zboží bude potřebovat znát přihlášeného zákazníka (pokud je nějaký přihlášen) a hlavně další informace o něm, protože bez toho neručí cenu. Je to závislost – daná třída potřebuje jinou entitu a její informace, ale řešit ji jinak než implementací uvnitř je zcela mimo hru.

Nebo můžeme mít B2B e-shop a ceny jsou v různých měnách podle segmentace zákazníků, jejich skupinami … A opět tu máme kooperaci s jinou entitou (která je mimochodem také nějak a někam ukládána) o které uživatelská třída (nebo jiná komponenta aplikace) třídy Product ani nemusí vědět. Je nejlepší takovou závislost řešit ad-hoc a nikde jinde se se závislostmi „netrápit“.

Řekněte mi jednu věc: Jak by šel udělat udržitelný projekt, kde si klient (můj zákazník) klidně může cenorvorbu za dva roky navymýšlet úplně jinak, kdyby mi úplně „cizí kód“(myšleno jiné entity) na stovkách míst „sahal“ na public vlastnost $product->price? To je čisté peklo na zemi …

Takhle mám prostě jasně dané místo, kde se bude tvořit cena. A pokud pro daný projekt stačí pouze vrátit hodnotu vlastnosti, tak super. Ale kdykoliv tam mohu doplnit libovolný algoritmus, který se automaticky promítne do celé aplikace – celého projektu. A ten algoritmus může být závislí na čem chce, protože vnitřní závislosti jsou v této situaci čistě záležitostí dané entity.

A úplně stejné je to se závislostmi. Jaké má třída závislosti a proč je má a jak s nimi pracuje je v takové situaci vnitřní implementace třídy. I kdybych to vytáhl do kontejnerů (a ještě ke všemu v nestandardním datovém formátu), tak získám jen a pouze to, že to co se týká třídy a její logiky mám prostě „hozené bokem“ a v aplikaci mám subsystém, který ve skutečnosti nepotřebuji – ba naopak.

A tak by se dalo ještě dlouho pokračovat. Kolega uvádí jako příklad články (Article), já argumentuji s e-shopem a produktem. Ale ani zdánlivé triviální články bych takto (jak demonstruje kolega) nedělal. Protože i ty se mohou stát např. „mezinárodními“ (s více jazykovými mutacemi) a čert ví jaká bude vnitřní logika za rok, dva, za tři … Že je něco teď v tuto chvíli zdánlivě jednoduché neznamená, že to tak bude za pár let.

Pokud netvoříme marketingovou landing page s životností pár týdnů, tak je třeba stále myslet na budoucnost. Myslet pouze „na teď“ je jeden z velkých programátorských hříchů.

Ale zpět k tématu DI. Problém se závislostí a její změnou kolega v jeho článku také dále uvádí a posléze přechází k řešení pomocí celého subsystému obecných továren a DI kontejnerů … Zbytečně. Je pouze generován problém tak, kde vlastně vůbec není a ani správně nesmí být. Generování složitosti a „jalového kódu“, který nezefektivní ani práci, ani běh aplikace, ale udělá zcela objektivně (měřitelně) přesný opak.

Když už si nějaká třída / entita řeší své závislosti, tak je rozhodně lepší, když si je řeší v momentě, kdy je opravdu potřebuje a to jednoduše, transparentně, přímočaře a intuitivně.

Ještě jeden příklad: Dejme tomu, že se produkt v e-shopu musí nějak aktualizovat vůči ERP. Pomyslné ERP je dobré ERP a má webservices rozhraní (kéž by … ) – dejme tomu třeba API založené na SOAP. Produkt tedy bude potřebovat instanci SOAP klienta. Ale proč by ta instance měla být v nějakém obecném „kontejneru“? To není třeba a je to kontraproduktivní. Pro tu instanci si produkt může hezky „sáhnout“ (teď je jedno kam a jak) pro SOAP klienta v momentě, kdy aktualizaci dat opravdu chce / musí provést a SOAP klienta opravdu použít. No a když už jsme u toho, tak ještě jednou: Mají uživatelské třídy používající entity produkt (tedy zbytek projektu) vůbec vědět o nějakém SOAP klientu? Samozřejmě že ne – to není a nesmí být jejich problém. Uživatelská třída prostě nechá entitu produkt uložit metodou save() a vnitřní implementace třídy již ví co a jak má provést a proč. Například administrační modul pro správu produktů synchronizace s ERP opravdu nezajímá a opravdu zajímat nemá. Už z toho důvodu, že na jednom projektu se aktualizace s ERP nebude provádět vůbec, na druhém projektu pomocí SOAP a na třetím úplně jinak (třeba na úrovni MS SQL databáze), ale rozhraní třídy entity produkt je stále stejné a může zůstat stejný i administrační modul pro správu produktů (například). Neprovádí se žádný zbytečný kód tehdy když se vůbec provádět nemusí a nemá. A vývojář se nemusí starat o nějaký obecný systém továren a DIC – nic takového prostě zde na tomto místě není třeba. Vývojář má naopak možnost kouknout na implementaci určité logiky, která je na jasně určeném místě – pokud to opravdu potřebuje. To není nevýhoda, ale naopak velká výhoda.

Tedy: Ve světě ve kterém se pohybuji já nesmí pravidlo „používejte argumenty“ vůbec existovat. To je zdroj problémů, vnáší to obrovskou složitost tam kde je zcela zbytečná a kontraproduktivní a jde to přímo proti smyslu OOP. V mém světě je třeba dodržovat základní pravidlo OOP: Třída má jasně dané rozhraní a jak je uvnitř implementována uživatelé třídy nemají co řešit. Tak OOP funguje a je to jeden ze základních rysů a smyslu samotné existence OOP. Tak lze tvořit udržitelné aplikace toho typu a rozsahu pro které je OOP určeno.

Ale tím zdaleka nejsme u konce

Dependency Injection, kontejnery a fasády, … v PHP Jet

DI není špatný koncept, vůbec ne! Jen jako vše je nutné jej správně použít na správném místě. Tak je to na světě se vším. A také nebrat vše doslova – OOP je prostě abstraktní a kontejner je také čistě abstraktní „věc – pojem“.

PHP Jet koncept DI používá a hned si to ukážeme prakticky. Kolega v článku zmiňuje logování, tak to si rovnou vezměme na paškál.

Pár myšlenek, než se dostaneme k jádru věci:

  • Kolega řeší adresář kam se bude logovat, mluví o konstantě, parametru konstruktoru … No jo, ale co když vůbec nechci logovat do souborů? Co když chci použít databázi? Nebo nějakou speciální aplikaci a provádět nějaké zvláštní operace? A hlavně: Co když změna toho jak se loguje nenastane během vývoje, ale během dalšího života a provozu aplikace?
  • Z praxe vím, že logování událostí je vlastně vždy homogenní pro danou část projektu. Prostě administrace loguje nějakým stylem, e-shop (či web, intranet, …) nějakým stylem, API zase jinak. Ale vždy je toto chování globální a homogenní pro danou část projektu a je úplně jedno zda se záznam uloží do souborů, databáze, nebo úplně jinak … Nemá smysl řešit, že jedna entita chce logovat jinak než druhá. Např. správa produktů nebude logovat jinak než správa katategorií katalogu – proč by taky. Proč bych něco takového měl řešit? Však by to bylo i kontraproduktivní. Správa produktů má prostě logovat operace uživatelů v ní provedené a nemá co „kecat“ do toho jak se loguje – to není „její“ věc a kompetence .. Tedy aplikace potřebuje jednotné rozhraní pro logování, které loguje, ale zcela odstíní aplikaci od toho jak se fakticky loguje – přesně k tomu je princip DI určen.
  • A co když chci něco fakt extra – třeba na určitou událost rovnou někoho upozorňovat? Co když chci implementovat řekněme poslání SMS při opakovaném chybném přihlášení do administrace? I to lze pomocí konceptu který používá PHP Jet řešit.

V PHP Jet aplikacích se události logují například takto:

Logger::warning(
    event: static::EVENT_LOGIN_FAILED,
    event_message: 'Login failed. Username: \'' . $username . '\'',
    context_object_id: $username,
);

Tedy Logger má nějakou úroveň / druh / závažnost události, strojově čitelný kód události, člověkem čitelný stručný popis události a kontext (id kontextu a/nebo data kontextu).

Nic zvláštního – celkem běžné. Ovšem nevytváří se žádná instance loggeru, ale používá se statická fasáda, která zároveň slouží jako kontejner.

Zde se trochu zastavím. Již jsem narazil na názory „puristů“, že statické metody a fasády jsou fuj-fuj. Vůbec ne. Vše má svůj význam, jen to správně pochopit a použít. Proč mít instance něčeho, co je ve svém principu použití statickém, nemá to žádný vnitřní stav – proto statické metody máme a používáme. Nehledě na to, že to co zde popisuji lze takto snadno implementovat a zároveň použití. Tedy integrace logování v aplikaci je maximálně jednoduchá – a to je to hlavní. Je to jasné a přímočaré. To mě zajímá. Na debaty o „nesmrtelnosti chrousta“ nemám s prominutím čas.

Tak a teď zpět k popisu logování a samotné logice uplatnění DI v PHP Jet. Máme tedy obecnou třídu Logger, kterou zbytek projektu používá pro logování. Cokoliv co tuto třídu použije (zavolá) je z hlediska koncepce DI konzument. Třída Logger však není poskytovatelem služby logování. Třída Logger je pouze fasáda a kontejner zároveň. Poskytovatelem služby logování (tedy tím co zalogování opravdu fakticky provede a záznam nějak a někam uloží) je libovolná instance třídy, která musí splňovat tu podmínku, že implementuje rozhraní Logger_Interface

Do třídy (kontejneru a fasády) Logger je tedy nutné vložit poskytovatele služby. To se dělá takto:

Logger::setLogger( new Logger_Web() );

Nebo takto:

Logger::setLogger( new Logger_Admin() );

Či takto:

Logger::setLogger( new Logger_REST() );

Nebo jakkoliv jinak :-)

Prostě a jednoduše stačí vytvořit třídu, která implementuje rozhraní (interface) Logger_Interface a tu vložit. Jestli ta třída bude logovat do databáze (jak to dělá ukázková aplikace PHP Jet), nebo posílat zprávy Marťanům, to už je aplikaci celkem jedno. Pro to aby aplikace začala logovat, tak stačí doplnit úplně jednoduchá volání bez jakékoliv složitosti (která je – opakuji – naprosto zbytečná a kontraproduktivní).

A kde toto vložení provést? Pokud používáte Jet MVC, tak ideální místo je inicializátor báze:

namespace JetApplication;

...

class Application_Admin
{
    ...

    public static function init( MVC_Router $router ): void
    {
        Logger::setLogger( new Logger_Admin() );
        Auth::setController( new Auth_Controller_Admin() );
        ...
    }

    ...
}

A mimochodem … Všimli jste si toho? Závislosti jsou nastaveny za běhu. To je smyslem existence konceptu DI! Tady je to jádro věci. V celé aplikaci mám logování, ale až při běhu aplikace na základě okolností (zde rozhodnutí zda jsem v administraci, či kdekoliv jinde) nastavím správně poskytovatele služeb. To je rozhodně užitečnější a smysluplnější než mít závislosti v jakémkoliv typu konfigurace.

Tedy shrňme si příklad s logováním: Například administrace vašeho projektu bude logovat jinak než třeba REST API (minimálně do jiné tabulky, ale může klidně použít úplně jinou technologii), ale například administrační moduly prostě chtějí logovat a kam to „spadne“ je nezajímá.

Rovněž se může stát, že tatáž logika – tentýž kód – poběží pokaždé v jiné části projektu. Pokaždé bude logovat, ale pokaždé jinak a jinam. To je kouzlo a smysl DI a jeho správné použití na správném místě.

Ne vše v PHP Jet je DI!

Obecně se dá říct, že velkou chybou vývojářů je vyhledávání a prosazování dogmat s někdy až náboženskou posedlostí. Někdy je to snaha určitý koncept „narvat“ všude a vše tomu přizpůsobit – až ohnout. Ale to je špatná cesta. DI je super věc, ale má své místo tam, kde je to užitečné a smysluplné.

Každý koncept má své určení, svůj smysl a své místo. Správné pochopení určitého konceptu nastává až tehdy, když je člověk schopen správně posoudit kdy je daný koncept vhodné použít a proč, nebo naopak kdy ne a proč.

Je to jak v životě. Na pár krtinců na zahradě mi stačí lopata a nemusím si půjčovat bagr, naopak díru na bazén budu dělat bagrem raději než lopatou. (Fakt! – před lety jsou to zkoušel :-D ) Proto tento koncept není v PHP Jet použit úplně všude a také ne úplně stejně (a tedy ani neexistuje nic jako obecný systém DI kontejner;), ale koncept je použit tam kde to má smysl a tak jak to má smysl, to jest zde:

Logger

To je triviální věc, kterou jsme zde fakticky popsal.

Autentizace a autorizace

To už je zajímavější ;-) Co potřebuje aplikace / projekt?

  • Vědět zda je uživatel přihlášen a zda je platný (např. není zablokovaný).
  • Potřebuje instanci přihlášeného uživatele (pro zjištění jména a dalších údajů).
  • Potřebuje ověřit, zda má uživatel určité oprávnění.
  • Samozřejmě je nutné uživatele přihlašovat a odhlašovat.

A opět … Kde jsou uživatelé fakticky uloženi? V databázi, nebo někde jinde? Jak probíhá autentizace? Jaké jsou workflow autentizace? Je nějaký OAuth systém? Informace o přihlášení se drží kde? V session, nebo se pokaždé ověřuje nějaký autorizační token (REST API)? A tak dále … Opět je „tisíc“ věcí, co je nutno řešit.

Ale opět to zbytek aplikace nezajímá. Aplikace prostě chce vědět, zda uživ je přihlášen a zda může dělat to a to (zda má uživatel práva – tedy zda je autentizován a i autorizován).

Proto v rámci autentizace a autorizace existuje fasáda a kontejner Auth, která však počítá s vložením auth kontroleru – což je instance třídy implementující rozhraní Auth_Controller_Interface, které už je poněkud zajímavější než logger. A tento kontroler se již stará o to, aby použil správné třídy pro entitu uživatel, pro entitu role, aby správně ohlídal např. platnosti hesel a postará se i o to, aby určený aplikační modul v případě potřeby zobrazil a obsloužil přihlašovací formulář a také o to, aby informace držel v session, nebo používal HTTP autentizaci. Viz např. třídy JetApplication\Auth_Controller_Admin a JetApplication\Auth_Controller_REST

Ve své podstatě je to celé velice malé a jednoduché, pro použití aplikací primitivní, ale opravdu flexibilní.

Práce s aplikačními moduly

Malý subsystém pro získávání instancí aplikačních modulů a obecnou práci s nimi třída Application_Modules má ve skutečnosti také backend (zde lépe řečeno handler), který operace fakticky provádí a třída Application_Modules je opět pouze fasáda a kontejner používaný aplikací. 

To jak PHP Jet operuje s aplikačními moduly (např. kam si ukládá data o tom který modul je již nainstalovaný) lze tedy také snadno změnit – třeba kompletně „překopat“ bez toho, aby aplikace přestala fungovat. Opět je zachováno objektové rozhraní. 

Ovšem zde se nepředpokládá, že by to někdo dělal v běžné aplikaci. Proto má tento kontejner implementováno instancování výchozího handleru Application_Modules_Handler_Default pokud do prvního použití aplikačních modulů nebyla vložena instance handleru jiného. Tedy toto berte jako zajímavost a hlubší znalosti PHP Jet. V běžné praxi není třeba se o to starat ( ale můžete :-) ).

Autoloader

I autoloader počítá s tím, že bude mít nějaké moduly (zde tzv. loadey), které se budou starat o zjišťování cest k třídám. Ovšem autoloader již řeší kešování mapy (mimochodem nikdy nesestavuje žádnou kompletní mapu, ale ad hoc postupně sestavuje mapu toho, co aplikace opravdu potřebuje – čím méně dat, tím lépe) a tak dále. A především samozřejmě nepočítá s jedním loaderem, ale s libovolným počtem loaderů.

Inilizace se provádí již v inicializačním skriptu ~/application/Init/Autoloader.php

Více viz dokumentace.

Keš (Autoloader a MVC)

I pro systémové keše autoloaderu a MVC platí zde uvedené principy, kdy se samotný backend keše vkládá do příslušných kotejnerů.

Také v tomto případě se tak děje již během inicializace ve skriptech ~/application/Init/Cache/Autoloader.php a ~/application/Init/Cache/MVC.php

Db

A je tu samozřejmě přímá práce s databází (bez ORM, nebo pro potřeby ORM DataModel). To se používá například takto: 

$db = Db::get(); //Default connection
$db->execute('INSERT INTO some_table SET
    name=:name,
    age=:age',
[
    'name' => 'Some Name',
    'age' => 42
]);


$erp_db = Db::get('erp_connection');
$bills = $erp_db->fetchAssoc(
    query: 'SELECT * FROM bills WHERE invoice_date>=:date AND paid=0',
    query_data: [
        'invoice_date' => '2021-12-01'
    ],
    key_column: 'invoice_number'
);

Tedy třída Db vrací instance jednotlivých spojení a až s těmi se pracuje. Počítá se s tím, že projekt používá často více jak jedno DB spojení (běžný projekt má dvě DB spojení, ale mám tu i projekt se čtyřmi různými spojeními).

U práce s databázi již ovšem není tak úplně důležitý koncept DI, jako spíše továrny – tedy dávám to zde hlavně jako oslí můstek.

Továrny

Znáte, ne? Pro pořádek: Továrny jsou koncept, kdy instance tříd nejsou vytvářeny přímo, ale přes tovární metody a tovární třídy.

Důvodů proč nevytvářet instance tříd přímo může být víc. Například proto, aby bylo možné vyřešit závislosti – jak píše kolega v článku na který odkazuji v úvodu. 

PHP Jet však používá továrny z jiného důvodu: Aby bylo možné třídy vyměnit za jiné

Tedy nevyhovuje vám například implementace stránek v rámci Jet MVC? Tak si klidně udělejte svojí implementaci. A klidně použijte dědičnost a upravte si jen to co potřebujete. Co vám v tom brání? Vůbec nic. Pouze je nutné dodržet minimální rozhraní entity (zde Jet\MVC_Page_Interface), ale klidně si můžete doplnit i své metody nad rámec tohoto rozhraní – tomu také samozřejmě vůbec nic nebrání.

A protože to na čem PHP Jet bazíruje je základ OOP – tedy jasně daná rozhraní tříd (bez kouzel a čar), tak aplikace prostě bude fungovat i když vaše implementace MVC stránek bude operovat třeba s databázi místo definic v souborech (což bych teda nedoporučoval, ale možné to je).

Celé to funguje tak, že určitá škála řekněme systémových tříd se v rámci PHP Jet nikdy nepoužívá přímo (ani neinstancuje, ani volají statické metody) a vše je řešeno přes subsystém továren

Smyslem továren samozřejmě není jen vytvořit instance, ale i umožnit aplikaci nastavit si jaké třídy pro danou entitu budou v systému použity.  Pro tento účel je předchystán inicializační skript ~/application/Init/Factory.php. Ten je však ve výchozím stavu de facto prázdný, protože samozřejmě existuje výchozí stav (výchozí použité třídy), který v běžné situaci není nutné měnit.

Seznam továren a tedy i toho co takto můžete ovlivnit najdete v dokumentaci.

Záveřem

Tak teď už víte, jak PHP Jet chápe a používá koncepty DI a továrny. Také víte proč Jet nemá a nebude mít žádný obecný DI kontejner systém, ani žádný obecný univerzální systém továren.

PHP Jet používá tyto koncepty tam kde to má jasně danou logiku a smysl a používá je velice jednoduše.

Zároveň jsem chtěl zdůraznit toto: Pro Jet jsou „svaté krávy“ rozhraní tříd objektů. Nic jako „použijte parametry“. Ne! Naopak. Použijte jasně dané rozhraní, použijte chytře, přímočaře a zejména vhodně další koncepty OOP jako jsou zapouzdření, dědičnost, i ono DI a továrny, singletony, fasády, a modularitu – tvořte aplikaci jako něco co je tvořeno kooperujícími komponentami, které se budou v budoucnu samostatně rozvíjet, ale které budou vždy kooperovat pomocí jasně daných rozhraní a tvořit tak celek. Získáte tak dlouhodobě udržitelnou, refaktorizovatelnou, sice rozsáhlou, ale přes to jasnou a čitelnou aplikaci / projekt, která může sloužit mnoho let a kterou nepřekvapí žádné požadavky klienta (vašeho zákazníka), které přijdou třeba za tři roky a které „dnes“ možná ani nečekáte. 

Není čas ztrácet čas s něčím, co přináší neodůvodnitelnou složitost, naopak je nutné promýšlet vše tak, aby to obstálo prověrkou časů budoucích – aby bylo možné implementovat i to, co vás právě teď nenapadne a třeba ani nemůže napadnout a zároveň aby vše zůstalo co nejvíce jednoduché, protože do všeho složitého se nakonec stejně tak akorát zamotáte.

Hlavní pravidlo je: Každá komponenta dělá to co má a dělá to nejlépe jak to jde. Každá komponenta aplikace je zapouzdřená – její logika a ani nastavení není „roztahané“ po celé aplikaci. Vše potřebné pro danou činnost je prostě na jednom místě a je tedy jasné kde to najít a jak to dále rozvíjet a organizačně vzato i kdo za to zodpovídá (pokud je jasně dáno, že za modul X odpovídá ten a ten člověk či tým). A komponenty spolu „mluví“ pomocí jasně definovaných rozhraní.  Mít aplikaci postavenou tak že se do komponent zasahuje z vnějšku jde přímo proti této myšlence.

Komponenty používají jiné komponenty tam, kde je to třeba a v momentě kdy je to třeba a podle logiky která je v nich implementována – vše je v komponentách zapouzdřeno – i závislosti.

A ano, když budu chtít a potřebovat vědět jak se co dělá, tak se kouknu „dovnitř“. To je totiž super. Vím kde „to“ mám hledat. Když to tedy hledat potřebuji – většinou mi stačí, že vím co „to“ (daná třída) dělá. A jak „to“ dělá budu řešit v momentě až nastane zásadní změna projektu a bude třeba onu logiku upravit. Ale pak stále vím kde co upravit a co se pak stane. Mám vše zapouzdřené. Vevnitř vidím jak se co dělá a navenek vidím, jak se to „baví“ s „okolním světem“. Mám jasně danou věc, kterou musím dodržet – rozhraní. Ale dovnitř mi nic „nekecá“ a pomyslná „střeva“ komponenty jsou tam kde je očekávám.

Je to vlastně jako auto. Tam mě také primárně zajímá jak se ovládá, jaké má jízdní vlastnosti a co od něj mohu čekat a jak jej použít. A pod kapotu se podívám pouze když potřebuji dolít náplň do ostřikovačů, ale jinak to nechávám na autorizovaném servisu. Nebo se pod kapotu podívám když opravdu vím proč to dělám a vím co a jak chci udělat. Ale v praxi mě nezajímá jaké má auto vstřikovače a turbo. A až pojedu do garáže, tak ve výtahu rozhodně nepotáhnu vstřikovače a turbo, abych je vložil pod kapotu a mohl jet. Ne, mám auto, to potřebuji prostě nastartovat a použít ke svému účelu.

Koncept DI má smysl tam, kde lze jasně určit, co daná komponenta má dělat (prostě logger loguje), ale je nutné „za běhu“ určit jak přesně a pomocí čeho to má dělat: logovat do souborů? do databáze? jinak?

A především zlaté pravidlo: KISS! Jo, fajn kapela, syn je má rád, já taky. Ale zde míněno: Keep It Small and Simple. A nevytvářet problémy tam kde nejsou tím, že se z něčeho v podstatě jednoduchého udělená něco co je složité.

Doslov

Především chci zdůraznit, že si kolegy Davida Grudla velice vážím. I když s ním ve velkém množství čistě technických věcí nesouhlasím a Nette jsem sice zkoumal, ale z mnoha důvodů bych jej pro žádný projekt nepoužil. Ať je to dáno možná odlišnou profesní oblastí působení, či čímkoliv jiným, tak to nic nemění na faktu, že kolega Grudl dokázal udělat zajímavý a úspěšný framework, na kterém běží určitě mnoho zajímavých projektů. Veškerá úcta a respekt.

Ovšem já to na základě mých zkušeností a znalostí a poznatků z mých projektů prostě vidím jinak. Avšak je možné, že mé názory jsou dané právě oblastí kde jsem se vždy profesně pohyboval a pochopitelně neexistuje nic jako svatá pravda. I tak jsem si jistý, že určité typy projektů je možné pomocí mnou prosazovaných postupů vyvinout rychleji a efektivněji a také je dlouhodobě udržovat a rozvíjet – a udělat tak větší radost klientovi, tedy uspět.

Díky za Váš čas a příště zase na shledanou!

Sdílet