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.
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:
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.
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
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:
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ě.
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:
To je triviální věc, kterou jsme zde fakticky popsal.
To už je zajímavější ;-) Co potřebuje aplikace / projekt?
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í.
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 :-) ).
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.
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
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.
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.
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é.
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!
Ne, není to běžné. Je to náhled do vnímání jednoho člověka, který bojuje dávno vybojované bitvy a řeší problémy, které většina vývojářů nemá. Proto k tomu není žádná velká diskuze, ani flamewar. Prostě exkurze do cizí reality.
Před časem jsem se dostal k dotNet projektům. A mám pocit, že oni PHP programátoři snad dokonce víc dbají na čistý kód, než ti z dotNet.
Muj zasadni problem s navrhnutym resenim je ze ulpne opomnel dalsi dulezity aspekt a to je SRP. Ono verim tomu ze pro jednoduche veci to funguje hezky ale napriklad ono
$product = Product::get( $id );
co to vrati? Vrati to predpokladam Product jenze co kdyz chci jeden produkt nacitat z vice mist najednou priklad, velky globalni eshop kde v globalnim vyhledavani pouzivame Elasticsearch kuli performance ale zaroven mi nevadi ze data jsou klidne den stara. V samotnem prehledu kde ctu data ze slave repliky kde jsou povetsinou stara par vterin/minut ale pro listovani to staci. A nasledne pri zpracovani objednavky uc pracuji s mastrem kde mam zarucena aktualni data.
Tzn efektivne mam 3 repository z toho 2 jsou read-only a master umoznuje zapis. Pak musim metode get($id) rict i odkud ma nacitat jenze to musi nadrazena komponenta vedet treba ten prehled produktu v kosiku bude komponenta "prehled" pracovat s mastrem pri listovani bude pracovat se slavem takze najednou musim komponente predat informaci zda je v "master rezimu" nebo zda je ve "slave" rezimu pripadne musime udelat
Product:setPersistance("slave")
... naka logika
Product:setPersistance("master")
... jina logika
to mi jako elegantni reseni moc neprijde :/
děláte si srandu? Ukazovat neaktální data? To jako fakt? Hmm, alespoň vím, kde se tyhle zhovadilé nápady berou. K čemu je mi eshop, který mi ukazuje "skladem 100ks" ale po vložení do košíku mi to napíše "nelze dodat, není skladem". Takové řešení mi někdo ve firmě napsat, tak má na minutu padáka. Pokud neumíte udělat výkonné řešení a aktuálními daty, raději nepiště e-shopy.
Příklad z praxe :)
Eshop, 220 miliónů položek v mysql, 13 miliónů položek v prodeji. Občas zobrazíme neaktuální data a při navštívení detailu se to na pozadí aktualizuje a co nejdříve promítne do stránky. Když se to nestihne, musíme to stopnout v košíku, jinak to nejde. Nejedná se o žádný automaticky generovaný dropshipping, ale opravdu o seriózní business s kamennou prodejnou.
Můžu to mít všechno aktuální, ale ty náklady bych pak musel promítnout do ceny.
Chápu, že pro minieshop se 100, nebo 1000 položkama je to snadné, ale když bys mi s těmahle hláškama přišel do většího projektu... tak padáka... nemáš, jelikož bys ani nedostal práci.
Mate to nejako naopak
Tento kod je SRP - kazda trieda riesi prave jednu funkcionalitu. Prva riesi servirovanie dat, druha ziskanie dat z mastra, treti ziskanie dat zo slave. Kludne si mozete napisat aj stvrtu pre ziskanie dat z elasticu, bez toho aby ste musel sucastne upravovat aj rodicovsku triedu.
abstract class Product{
public function get($id){
$data=$this->getData($id);
// process and retun data
}
abstract protected function getData($id);
}
class ProductMaster(Product){
protected function getData($id){
// some logic
}
}
class ProductSlave(Product){
protected function getData($id){
// some logic
}
}
To co popisujete vy ako idealne riesenie SRP nie je, trieda riesi hned niekolko funkcnosti.
Tvůj "návrh" ale problém nijak neřeší. Zapomněl jsi totiž, že Product:.get byla statická metoda, tudíž polymorfismus nefunguje a uživatel by stejně musel volat buď ProductMaster::get nebo ProductSlave::get, což je přesně to, co nedává smysl.
Zmysel hlavne nedava pouzivat statiku pre dynamicke data, to by bola degradacia OOP na proceduralne paradigma. Volat sa bude samozrejme $productSource->get, kde $productSource bude instncia ProductMaster alebo ProductSlave. Staticke metody funguju so statickymi polozkami definovanymi v ramci definicie triedy, k instancii objektu sa nedostanu.
Dalsim predpokladom pre schopnost zvladat OOP je nepliest hrusky s jablkami. V kontexe tohoto vlakna sa riesi SRP, nie DI.
Ne, v kontextu tohoto vlákna se řeší, jak udělat Product::get, když to na různým místě má načítat z různé databáze. Autor komentáře to nejspíš nazval SRP, protože chtěl říct, že načítání z DB má být v jiné třídě než Product (což je pravda).
Ten tvůj paskvil, co jsi tam napsal, tophle nejen neřeší, ale ani nedává smysl - proč bych proboha měl mít na načítání z 4 druhů DB 4 subclassy Product? A na načítání User ze 4 druhů DB budu mít další 4 subclassy od User? Možná ovládáš teoretický OOP, kde se hodně maluje a dělají se složitý stromy dědičnosti, ale pomocí toho se skutečnej udržitelnej program napsat nedá (protože jak píšeš jinde, musíš znát všechny požadavky dopředu, a to nikdy nenastane).
Nefabuluj nad tym co autor myslel, drz sa toho co napisal.
Nebudes mat 4 subclasy Product, ale 4 subclasy ProductSource. Kludne aj piatu, ak to pribudne v poziadavkach a tym ze to vlozis do subclasy, tak na povodny kod nemusis ani siahnut. Dokonca tu piatu mozes mat ako zvlast balik, a v priebehu CI si ju natiahnes cez composer len ak to cielove prostredie bude potrebovat.
Co sa tyka mojich znalosti OOP, tak tie som uz realne pouzil a nie len v ramci PHP, ale aj v ramci komplexnejsich jazykov. SOLID je podla teba len take malovanie alebo by sa malo pouzivat aj v PHP?
Prave ze preto ze VZDY ratam s tym ze poziadavky nebudu konecne, tak nespacham aplikaciu tym stylom ze budem mat funkcionalitu natvrdo v kode. Nikdy nefabulujem nad tym ze toto alebo toto by malo byt definitivne. Tak ako si to popisal ty, pretoze pri rozsireni nechcem prepisovat projekt na niekolkych miestach.
Co je ten ProductSource, o kterým pořád mluvíš? V tvojí odpovědi je abstract Product, ProductMaster a ProductSlave. Což je blbost a jen se ti nechce to přiznat, tak jsi začal mlít o nějakým ProductSource (aby bylo jasno, Product je Active Record třída, která představuje řádek v databázi - to se nesnaž zase zpochybňovat, tak to je).
Stebou diskusia nemamoc zmysel, posuvas branky tym ze sa snazis zmenit kontext. Povodny kontext je SRP a moj prispevok sa tykal dedicnosti versus multifunkcionality volenej parametrom.
Si predstav ze Product nemusi byt nutne riadok v DB (strasny demencia snazit sa chapat relacne data ako objektove data). Kludne to bude zaznam v SAPe, pristupny cez SOAP, i ked pravda s tym sa ty asi ani nestretnes.
Ty si fakt nevidíš do huby :-)
RS v původním komentáři psal o tom, že statická Product::get
se blbě konfiguruje, odkud má brát data. Ty jsi přišel s tím, že se udělá ProductMaster
a ProductSlave
. Pokud teda, jak teď tvrdíš, tvůj kód nebyl reakce na něj, ale "sa tykal dedicnosti versus multifunkcionality volenej parametrom", proč jsi ho tam psal? Když mně radíš "Nefabuluj nad tym co autor myslel, drz sa toho co napisal"?
A teď začínáš s tím, že "Product nemusi byt nutne riadok v DB". V diskuzi pod článkem o Jetu, kde Product je řádek v DB. A ostatní obviňuješ z posouvání branek :-D
Ty tvoje výplody mi dost připomínají horečnatý vize chlápka jménem Jegor Bugajenko (Yegor Bugayenko) , pro kterýho OOP je náboženství a vede k naprosto nesmyslnýmu a strašnýmu kódu.
Na statickej Product::get
je blbe hlavne to ze neizoluje jednotlive instancie triedy. Ja som ju previedol na dynamicku formu. Potom tam bol tam navrh na static metodu, ktorej jednotlive spagety sa volia parametrom. Ktore z toho podla teba splna Single responsibility principle?
Nemiesaj sem stale DI, reprezentaciu pruduktu v db, ani definiciu toho co vlastne produkt je. V kontexte vlakna tykajuceho sa SRP je to irelevantne.
(Bohužel nejde udělat edit příspěvku): Přečetl jsem si tvoji úvodní reakci ještě několikrát, abych se pokusil zjistit, jestli jsi to nemyslel celý třeba nějak jinak a nejde jenom o nedorozumění. Ale teď mi přijde, že tam tvrdíš, že když abstract class Product
řeší jednu věc a class ProductMaster extends Product
úplně jinou věc, tak je to SRP. Pokud to tak je, tak je to docela zásadní nepochopení základních principů příčetnýho kódování. Proč by proboha v takovým případě ProductMaster vůbec měl dědit od Product?
A navíc jsi vůbec nepohopil ten komentář, na kterej jsi reagoval. Tam nikdo nepopisuje nic jako "idealne riesenie", tam naopak namítá, že v Jetu to nejde udělat správně bez porušení SRP.
Tady bude asi problem v namingu (aneb jeden z nejvetsich programatorskych problemu :) )
Pokud je opravdu Product jen Storem tak by se tak mel i jmenovat jinak neni jasne co je vlastne zodpovednosti objektu Produkt ? je to reprezentace Produktu? Je to reprezentace Storage ?
Priznam se ze to vase reseni me trosku desi protoze
class ProductMaster(Product) imlikuje ze ProductMaster <b>JE</b> Product ale pokud Product je DTO a ProductMaster je repository tak je ta dedicnost postavena spatne..predpokladam tedy ze zakladni problem v porozumneni je v tom co je vlastne resposibilita classy Product. a co vraci.. pokud Product vraci instanci napriklad ProductDTO/ProductActiveRecord tak pak asi ok (i kdyz active record nemam rad z duvodu zde jich popsanych). V opacnem pripade se jedna o poruseni SRP kdy micham zodpovednosti za nacitani a reprezentaci
$product = Product::get( $id );
a
$product = $product->get( $id );
se liší jen ve filozofii.
V prvém případě mám defakto globální objekt. Veškerá logika rozhodování odkud nebo dokonce co získám je definována vevnitř. Abych tuto logiku mohl nějak ovlivnit musím najít správné místo a šáhnout dovnitř. Nemohu mít dvě instance jen různě nakonfigurované (třeba porovnávat ze dvou různých zdrojů.)
V druhém případě mám instanci. Ta někde vzniká. Což má za důsledek že nejenom že to jde snadno najít, ale taky ten vznik mohu snadno nahradit za své řešení - je to jen proměnná.
Toto:
Product:setPersistance("slave")
... naka logika
Product:setPersistance("master")
... jina logika
je v celku provařená věc. V průběhu času se vám mění chování toho objektu. Velice nepříjemné. Nemám to rád.
Ajaja, tohle se četlo fakt špatně. Předem se omlouvám za tentokrát trochu konfrontačnější tón, ale pokud je článek psaný stylem "všichni ostatní to dělají blbě, správně je to takhle po mém", a přitom obsahuje tolik nesmyslů, polopravd a nelogičností, tak to bohužel jinak nejde.
První část nedává smysl vůbec. Tváří se, že vysvětluje, proč je DI špatně, ale pak místo toho popisuje, proč je dobré používat jasně definovaný interface bez nutnosti vědět, jaká je za ním implementace. Což je samozřejmě pravda, ale taky je to přesně to, s čím DI pomáhá. Námitka, že dependence se použije jenom někdy a tudíž nemá smysl ji předávat v konstruktoru, souvisí s velmi častým antipatternem, který se hojně vyskytuje nejen v Jetu, ale i ve všech ostatních programech používajícíh ORM na bázi Active Record, a tím je God Object (a s tím spojené nedodržování SRP, jak už bylo zmíněno v jiném komentáři). Představa, že pokud něco v e-shopu souvisí s produktem, tak to musí být v třídě Product, je zhoubná, ústí v třídu Product s několika tisíci řádky, která dělá úplně všechno (viz např. https://github.com/redmine/redmine/blob/master/app/models/issue.rb - bohužel je to Ruby). Ano, v takové třídě se pak DI používá špatně, ale není to chyba DI, je to chyba špatně navržené třídy Product.
Zbytek článku už jen popisuje DI "po Laravelovsku", kde se místo zřejmého předání závislostí volají statické settery a gettery, což je věc, kterou na něm kritizuje spousta lidí (a právem). V jednoduchém jazyce jako je PHP, který nemusí řešit věci jako souběžné zpracování více requestů, to jakž takž funguje. Zkuste si ale něco takového napsat třeba v Javě a začnete potřebovat další rovnáky na ohýbáky jako ThreadLocal. Tudy prostě cesta nevede.
Ked uz ten konfrontacny ton, nie je ziadne tajomstvo ze programatorov ktory su schopny dekompozice problemu a jeho optimalneho riesenia v ramci OOP je tak 5 az 10%, ostatok su lopaty. Teda ziadne ja to robim spravne a vy ostatny to robite blbo. A naopak to ze sa pravidelne niektore veci riesia v rozpore OOP, neznamena ze je to spravne, len je to lacne a pricetneho architekta to ani nevidelo. OOP ma totiz aj jednu zaludnost, analyza a navrh musi byt hotovy este pred tym nez sa to zacne realizovat.
Co se týče DI: Řekl jste to jasně: "nechcete". V takovém případě naprosto v pořádku.
Co se nás ostatních týče, tak já se klidně podřídím nějaké zásadě, má-li výhody.
Vaše chyba v úvaze je: "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."
Samozřejmě je v pořádku zadrátovat nějakou závislost na nějakém rutině - pokud neočekávám, že ta rutina bude mít různé varianty. Takže statická třída, nebo statické metody, funkce, proč ne.
Ale třeba instance třídy, to už dost dobře nejde. A aby si ji nějak pokoutně vytahovala - to jsme tu měli (vzor registry), není to dobrý nápad.
Popisujete "Ale kdykoliv tam mohu doplnit libovolný algoritmus, který se automaticky promítne do celé aplikace – celého projektu.". Chápu-li to dobře, vy prostě hrábnete do kódu, a nahradíte nějakou rutinu novou verzí. To může za určitých okolností fungovat. Ale testovat bych to nechtěl. Taky bych viděl nevýhodu v tom, že pak nemůžete používat původní verzi. Místo toho, abyste nakonfiguroval objekt tak aby se choval nějak, a podruhé aby se choval jinak, tak budete... hmm asi nějak složitě.
Píšete "Když už si nějaká třída / entita řeší své závislosti" - entita v mnoha případech neví, jaké konkrétní závislosti má potřebovat. Například zda má načítat z MySQL, nebo SQLite. Toto se neosvědčilo.
Píšete "Pro tu instanci si produkt může hezky „sáhnout“ (teď je jedno kam a jak) pro SOAP klienta" - myslím, že tímto jste to vypíchl. Ano, toto se používalo a není to dobrý způsob.
Zdůrazním jeden argument, proč se DI a IoC (mimo jiné) prosadilo: čitelnost. Pokud třída nějakým způsobem pokoutně vytahuje své závislosti, tak nejsem schopen uhlídat co se kdy zavolá, kolik toho je, co všechno budu muset přepsat v případě refactoringu, ani nevím, jak mohu ovlivnit chování daného objektu.
Píšete "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é." - Mám kolegu, který psal taky prostě jednoduše. Postupně jsem mu začal ukazovat, jak fantasticky dokáže zjednodušit a pročistit kód dodržování pár základních principů (DI + DIC, princip lokality). Ze začátku byl skeptický, ale teď je z toho nadšen.
Myslím si, že není nic špatného používat "svůj" styl. Proč ne. Je to umění. Ale pokud chcete dělat v průmyslu, tak to, jak to máte v php-jet vymyšlené není dobře. Je to tak deset let zpět, kdy se kód navrhoval způsobem jak to děláte vy, a neosvědčilo se to.
Další problém který vnímám je ze autor si zjednodusil praci myslenkovou operaci ze zmeny se aplikuji na cely projekt:
```
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.
```
okud opravdu mame v projektu vzdy jen jeden pristup tak to funguje hezky ale co kdyz v pulce aplikace potrebuji naky pristup a v druhe jiny pristup? co kdyz pul aplikace potrebuje naky logger a druha pulka jiny? tam zacneme brutalne selhavat a jsme v rozporu s tim co autor pise:
```
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ů.
```
Ja to nemyslim konfrontacne je pravda ze autorovo reseni ma sve vyhody a za cenu jistych omezeni je schopen dosahnout vetsi produktivity ale jakmile na tyto omezeni narazi tak bude muset delat spoustu ustupku a kod se stane necitelnym... (coz nemusi byt nutne problem ne kazdy programuje alzu / heureku / amazon) nicmene je treba na tyto problemy upozornit a prezentovat je jako problemy a ne jak vyhodu.
Předváděl mi kamoš svůj "životní" projekt. Bylo to něco z podobného ranku - univerzální apka s administrací a vůbec brutálně vymazlená. Žásnul jsem, kolik funkcionality dokázal v jednom člověku vytvořit.
Celé to měl postavené na dost svérázných principech (ke všemu se choval jako k mysql databázi, i k souborům). Na druhou stranu to prezentoval jako svůj "styl" a navíc to měl všechno velmi dobře promyšlené. Má můj respekt.
php-jet je bohužel prezentován jako něco nového, vyzrálého, něco co netrpí neduhy stávajících řešení. Což ale autor nedokáže dobře obhájit. V případě toho článku se mu to dokonce nedaří vůbec.
Ono to netrpí neduhy stávajících řešení, ale neduhy předchozích řešení. Někdo psal, že takhle se dělaly věci před deseti lety a neosvědčily se - to opravdu ne. První rozsáhle používané DI (EJB) je staré snad 25 let...
Ten přístup z článku defakto má DI, akorát jej řeší statickými metodami a proměnnými, místo, aby rozdělil objekty na data a factory/service/cokoliv. Což do jisté velikosti projektu bude fungovat a dokážu si představit, že může být skutečně i produktivnější (izolaci testů asi bude největší problém). Ale jakmile dojde na větší projekt nebo nedejbože dokonce projekty a myšlenky na reusability, různé proxy, interceptory, různé datasources, tak vítejte v pekle. Ano, v PHP to ještě možná bude trochu zvládnutelné s nějakými omezeními, ale v Java, kde jdou paralelně stovky až miliony requestů nebo v reactive frameworks střídají thready jak ponožky, to už opravdu nejde.
Trochu mi to připomíná, když jsem se před pár lety dohadoval v bývalé práci, jestli má být lokalizační framework postavený na statických metodách nebo na factory (bo DI znamená komplexitu v konstruktoru a field proměnné navíc). Nakonec vyhrál on, ale jenom dočasně - než tým nebyl schopen napsat ani unit testy (to šlo ještě částečně obejít přes PowerMockito) a použít řešení u prvního týmu, který měl potřebu customizace zdrojů dat. Takže jsme strávili další měsíc přepisováním na původní návrh postavený na factory a od té doby nasazení v mnoha specifických situacích bez jakéhokoliv zdržení.
Jako snaha pěkná, letmo jsem ten seriál sledoval. Asi každý jsme si tím vývojem prošli, ale tvrdit, že "já to dělám správně" je v tomto případě založeno na těžké neznalosti moderního software engineering.
Ještě jsem kouknul na ten odkazovaný článek ohledně DI v Nette - musím souhlasit, že to taky není dobré řešení DI, bo přináší další řadu problémů, které souvisí s neoddělením dat od procesu a navíc postrádá abstrakci (což mohlo být jen zjednodušení). Motivaci vyřešit jinak tedy chápu, ale výsledek je diskutabilní. Běžným současným řešením je plná separace dat a repository a repository vkládaná přes DI jako závislost do relevantních (nedatových) komponent.
Řešení DI, nebo implementace DIC?
Buďte prosím konkrétní, bo považuju tuto serii článků v českých krajích za výborný edukativní počin.
DI příklad. Konkrétně, že Article (data) by měl obsahovat odkaz na databázi a publikovat bezparametrickou metodu save(), která by měla uložit sebe do dané databáze.
A co je na tom chybné? V praxi bychom to možná použili, možná ne, záleží na dalším kontextu; ale jako esence DI mi to přijde správně.
Nette článek mě příjemně překvapil. Jako popis problémů, které DI řeší, pro někoho, kdo ho vůbec nezná, je to super. Beru to tak, že diskuze o tom, jestli je Active Record špatný pattern, byla nad rámec toho, co chtěl článek ilustrovat.
Separace dat od věcí, co s nimi pracujou, zatím podle mě v PHP moc rozšířená není, částečně asi i díky jeho jednovláknovosti - v takovém prostředí člověku projdou horší zvěrstva než třeba v té Javě a ani někdy není poznat, že to vlastně není správně. S příchodem readonly tříd v PHP 8.2 by se to snad mohlo trochu zlepšit (bude jednodušší vytvořit ty třídy, co drží data).
Píšete: "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..."
Tento problém se neděje. Například Neťácký DIC dodá závislosti (díky továrničkám) až když jsou potřeba.
Píšete: "... a to jednoduše, transparentně, přímočaře a intuitivně."
Vaše řešení toto IMHO nesplňuje.
Píšete: "Kolega řeší adresář kam se bude logovat, mluví o konstantě, parametru konstruktoru … No jo, ale co když vůbec nechci logovat do souborů? " - To je z toho článku jasně patrné. Logger je vytvořen tak, aby logoval do souborů. Pokud chcete logovat do databáze, vytvoříte DatabaseLogger (jste-li pořádkumilovný původní přejmenujete na FilebasedLogger) a předáte tu závislost, kterou požadujete.
Dokonce si můžete všimnout, že David přísně řeší rozdělení zodpovědnosti. Tedy, že když vytváříte NewsletterDistributor, nepředáváte LogDir, protože ten není jeho starost.
Vy máte vytvořenou třídu Logger, "který je nutné "naplnit" a tedy předat backend". Což je sice formálně v pořádku, ale v přímém rozporu s vaší požadovanou jednoduchostí - protože je to zbytečné.
Na každý způsob logování (ve vaší terminologii backend) stačí prostě jen vytvořit implementaci (hrajete-li na typy, tak použijete implementaci rozhraní) a instanci této implementace předat.
I kdybyste z nějakého důvodu trval na svém řešení v podobě kontejneru tak nastavovat logger způsobem: Logger::setLogger( new Logger_Admin() );
je špatné, protože existuje okamžik, kdy je logger nejen v nepožadovaném, ale dokonce (pravděpodobně) v nevalidním stavu. Proto se u většiny moderních FW setkáte s doporučením nastavením pomocí konstruktoru. Nastavování pomocí setteru (či jeho ekvivalentu, jako je inject* v Nette) je považováno za úlitbu legaci kódu, nebo legaci vývojářům.
Ono cele to PHP a jeho komunita v tom ma gulas.
> "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..."
Tento problém se neděje. Například Neťácký DIC dodá závislosti (díky továrničkám) až když jsou potřeba.
V tomto pripade nutnost pouzitia "tovarnicky" znamena, ze chyba je o uroven vyssie a objekt porusuje SRP. (zavislosti pri DI velmi dobre indikuju porusnie SRP)
Zas, tento clanok hovori o Factories - no v skutocnosti jke to service lokator.
Nette clanok, tiez hovori o factory no vskutocnosti to je repozitory.
Fakt, sa za tie roky nic nezmenilo.
Ano, autor php-jet používá "service locator" (to kdyby si chtěl přečíst kritiku tohoto vzoru).
Ne, článek od Davida nepopisuje ani nepoužívá vzor repository. Návrhový vzor repository řeší jak ukládat, případně načítat entity. Davidův článek popisuje jak poskládat instanci a jak jí dodat závislosti.
Ne, DI neporušuje SRP, naopak. Je důkladnou snahou o jeho dodržení. DI zajistí, že třída má jen svou zodpovědnost, a všechny požadované detaily nesouvisející s jeho prací jsou delegovány do závislostí. SL toto nesplňuje, protože třída ještě navíc musí řešit získání těch závislostí.
S továrnami na řešení on-demand vytváření závislostí v DIC jsem se setkal i v jiných jazycích (C#, Rust, Java). Nette je v tomto zajímavé spíš tím, že si umí ty továrny vymejšlet sám.
Každopádně pokud máte nějaký neotřelý způsob, jak dodat závislosti objektu, rád se nechám poučit.
> Ne, článek od Davida nepopisuje ani nepoužívá vzor repository. Návrhový vzor repository řeší jak ukládat, případně načítat entity. Davidův článek popisuje jak poskládat instanci a jak jí dodat závislosti.
Pouziva "faktory" na vytiahnutie veci z databazy. To je Repozitory. Ja factory povazujem za factory, ked jeho vystup zavisi len na vstupe alebo konstruktore.
>Ne, DI neporušuje SRP, naopak. Je důkladnou snahou o jeho dodržení. DI zajistí, že třída má jen svou zodpovědnost, a všechny požadované detaily nesouvisející s jeho prací jsou delegovány do závislostí. SL toto nesplňuje, protože třída ještě navíc musí řešit získání těch závislostí.
To ste ma nepochopil. Ja som vravel, ze velky pocet zavislosti pri pouziti DI je dobra indikacia, ze objekt porusuje SRP. Samotne DI pomaha dodrzaniu SRP, ved oba su sucastou SOLID.
Používá "factory" na on-demand vytvoření instance. Nenechte se zmást, on tam používá pojmenování NecoNecoFactory protože se to chová jako továrna. Jestli to odpovídá nebo neodpovídá definici vzoru factory pod GoF, je nesouvisející.
Kouknul jsem se znovu, a on tam nikde žádné věci z databáze nevytahuje. Ne, že by to snad nešlo, mohla by to být dobrá divočina, ale už by to šlo nad rámec definice "Továrna je třída, která vyrábí a konfiguruje objekty.". Takže to vaši definici repozitory nesplňuje.
> velky pocet zavislosti pri pouziti DI je dobra indikacia, ze objekt porusuje SRP. Samotne DI pomaha dodrzaniu SRP,
Ah, ano, napsal jste to do závorky. Já jsme reagoval na větu "V tomto pripade nutnost pouzitia "tovarnicky" znamena, ze chyba je o uroven vyssie a objekt porusuje SRP.". Továrničky neřeší problém mnoha závislostí. Takže tato výtka je lichá.
Ale určitě stojí za to zdůraznit, že v protikladu k SL, (který jak to vypadá propaguje php-jet) tak DI právě zčitelňuje, že ta třída má nějak podezřele mnoho závislostí. Programátor to má stále před očima a opakovaně může zhodnotit, zda jsou nutné.
Vypadá to, že vaše obvinění s toho, že PHP comunita v tom má guláš není tak horké, jak se to servírovalo :-)
Díky za zajímavou diskuzi,
Udělám si čas a projdu to.
Ale souhrnně bych rád řekl jednu věc. Jsem technik. Vzděláním i celoživotní praxí. A krom toho hodně dlouho i manažer.
S veškerou úctou ke všem názorům ... Názory mě zajímají, protože mne mohou nějak posunout. Od toho názory jsou.
Ale pro vyhodnocení správnosti názorů znám v mém oboru pouze jedno kritérium:
Čísla!
Tvrdá a ověřitelná čísla. Programování je technický obor. Není to grafika, kde máme subjektivní pojmy líbí / nelíbí.
V technice jsou rozhodující fakta a fakta jsou v technice reprezentována čísly.
Tedy nezajímá mě jestli je něco dobře nebo špatně subjektivně, ale jen a pouze objektivně.
Jaká lze v našem oboru stanovit KPI?
* Kolik času zabere realizace dané úlohy?
* Jak je použitelná odvedená práce a kolik tedy ušetří času?
* Kolik času musí třeba nový člen týmu strávit učením se něčeho? (například šablonovacího systému atd)?
* Jak rychle "běží" reálná aplikace?
* Jak je možné na projektu pracovat v týmu a kde jsou riziková místa kolizí?
* Jak technologie umožňuje samostatnou práci, ale i kooperaci?
= celkově vzato: Jak bude vývoj a práce efektivní?
To se dá změřit. Na to jsou postupy. A použitá technologie v komerční praxi musí v první řadě toto zohledňovat.
To je reálná praxe. Obávám se, že různí aktivní teoretici jsou od praxe tak trochu odtrženi ...
Tedy při vší úctě: Nezajímá mě slovíčkaření. Nezajímá mě teoretizování.
Zajímají mě případové studie vyjádřené ve financích.
Tedy kolik daný postup vývoje a daná technologie ušetřila peněz, jak zlepšila efektivitu.
Ano ... Musím to říct: Zajímají mě peníze. Protože za naše programování nám někdo platí. A ten někdo nás platí, aby sám vydělal peníze.
Tedy: Pokud mi chcete dokázat, že je něco "lepší", tak mi to prosím transparentně přepočtěte na peníze a ukažte na konkrétním případu.
Já mohu z praxe oponovat mnoha případy, kdy konkurence nechápala, jak je možné něco vytvořit lépe a levněji, než to dokáže ona.
Díky za reakci. To, co jste napsal, se ale nijak nevylučuje s kritikou, kterou vám tady napsali jiní (včetně mě).
Naprosto věřím, že při vývoji projektů typu, na kterých pracujete, jste si postupně vyvinul sadu nástrojů, která vám práci zefektivňuje. To se stává běžně, ve vašem případě tato sada nástrojů je Jet, a práci vám usnadňuje, zdá se, až extrémně. Je to velmi dobrý nástroj na zefektivnění vývoje projektů tohoto typu. Ale není to obecný framework, je to spíše "lightweight CMS", a na projekty jiného typu se zdaleka tolik hodit nebude.
Vaše čísla jsou prostě měřená na malém vzorku dat - pouze na projektech, které jste v Jetu vyvíjel a při vývoji kterých se postupně vylepšoval. Tam opravdu může vycházet lépe než cokoliv jiného a nijak to nerozporuju.
Ale například u větších projektů opravdu nezáleží na tom, jestli kostru třídy s entitou Product mi Jet Studio vygeneruje za 2 minuty nebo ji napíšu ručně za 10 minut. To absolutně není to místo, kde se stráví největší množství vývojového času. U úplně malých projektů "napiš a zahoď" zase možná dokonce bude lépe vycházet čistý špagety kód.
Takže obecně mám v článcích problém hlavně s používáním absolutních kvalifikátorů typu "takhle je to správně", "tohle je mýtus", "je lepší mít všechno v jedné třídě", "Jet je intuitivní" (například toto tvrzení vy jakožto jeho autor nejste schopen posoudit vůbec - pro mě například intuitivní není ani trochu).
Každopádně za články díky, vždycky se na ně těším - i když s filozofií Jetu nesouhlasím a nikdy ho používat nebudu, tak je vždycky zajímavé vidět, jak se věci dělají "jinde".
Pane kolego, já dělám pouze velké projekty. Opravdu hodně velké.
Malých projektů jsem za kariéru moc neudělal.
Jsem si naprosto jistý, že i Vy jste někdy někde narazil na to, co já jsem dělal.
A nejsou to projekty charakteru prezentační web, mnohem častěji informační systémy, e-shopy (a hlavně nástorje pro ně) a tak dále.
Stejně tak se potýkám s tím, že jsem si svou práci vždy musel obhájit. A to jako šéf celého oddělení a de facto části firmy, nebo teď když jedu "sám na sebe".
Já neteoretizuji. Za ty desítky let prostě vím co se v praxi na opravdu velkých a dlouhodobých projektech (bavím se i projektech i pro velké korporace) opravdu vyplatí a co funguje.
Proto chci, aby jakýkoliv technický argument byl podložen čísly a to čísly převedenými na finance. To mě zajímá.
Protože právě to jsem mockrát obhajoval. A opakuji že i na úrovni obřích korporací.
Stejně tam mám zkušenosti se spoluprací se slušnou řádkou kolegů, s projekty na kterých pracovala celá řada lidí a tak dále.
Tedy když něco tvdím, tak to vím. Vím to z praxe. Vím to z úspěchů i nezdarů. Vím to z toho, co jsem zažil.
Jet je obecný framework. Pouze je tak efektivní, že už ve svém malém základu má vše, co na reálných projektech potřebujete.
Obávám se, že zde posuzujete a hodnotíte něco, co jste vůbec nepochopil a patrně ani pořádně nenastudoval.
Já mám tu "drzost" psát "tohle je mýtus", protože to prostě mohu dokázat na reálné praxi. Vím to z každodenní reality.
Vy tvrdíte o obecném frameworku, že je to CMS. Při tom je to mnohem obecnějši a flexibilnější framework, než Nette, Laravel a Symfony dohromady.
Prosím více si to prostudujte a vyzkoušejte. Tomu jste se bohužel moc nevěnoval.
A pak si s Vámi klidně rád někde sednu a porovnáme si čísla. Tedy jaká konkrétní sitauice je v čem jak konkrétně řešitelná a jak to umožňuje dodat klientovi v čas a v rozpočtu projekt a o ten se následně roky starat.
To jsou pro mě kritéria a rád se o tom budu s kýmkoliv bavit.
Ale na to si opravdu musíte nastudovat o čem to vlastně je. To jste bohužel evidentně zatím neudělal. Budu rád, když to napravíte a bude možné se bavit konkrétně a vše si demonstrovat na reálných sitiacích z praxe.
> Ale na to si opravdu musíte nastudovat o čem to vlastně je.
Jste v poněkud nezáviděníhodné situaci. Prezentujete nástroj/FW, ve které my od praxe poznáváme ty bolestivé zkušenosti, které jsme si museli zaplatit. To, s čím jsme si za drahé peníze prošli, a zjistili že je to špatné, vy prosazujete jako tu správnou cestu. A vaše argumentace je založena na "musíte mi věřit". Můžete se na nás zlobit, ale to je slabé.
Já připouštím, že je možné, že jste dokázal objevit způsob jak to dělat, který je natolik invenční, že se tím DI stane zastaralé. Bohužel se vám to nedaří popsat a vysvětlit. Popisujete způsoby, které z praxe známe, a víme, že dlouhodobě nefungují.
Vy jste ten, který musí dokázat, že jste objevil něco lepšího. Popisujete ale způsoby které známe. V čem je tedy to kouzlo? Kde je ten detail, který z nepoužitelného udělá použitelné?
Možná nevíte, že Nette bylo původně také navrženo podobným způsobem jako máte navržen vy svůj php-jet. Pak se na základě zkušeností zjistilo, že to byla chyba, a dlouhou dobu se to přepisovalo.
Zajímají vás tvrdá data? Jedny vám mohu poskytnout:
Vždycky se objeví nějaký zajímavý projekt, který je rychle rychle vytvořen, bohužel se špatným základy. Chvíli to roste, a dříve či později už to programátoři nedokážou dál udržovat, protože jim schází skill.
Zkušení programátoři se tomu nechtějí věnovat, protože to není příjemné.
A tak si najmou mě, a já jim pomáhám ten kód vzpamatovat. Obvykle to trvá několik let. A jsem drahej.
Možná máte pocit, že toto není problém php-jet. Možná ne. Ale ukázky kódu a principy které prezentujete mě v tom nepřesvědčují. Vypadá to na super práci pro mě za pár let.
Můžete se hněvat, že nemáme pochopení pro vaši myšlenku. A že vám nedůvěřujeme. Možná vám křivdíme. Ale to se nedá nic dělat. php-jet vypadá jako špatný systém, a tak se podle toho budeme rozhodovat.
Tvoje práce zní zajímavě :-) Jak s něčím takovým člověk začne? Když už máš reference, tak si dokážu představit, že je to pohoda, ale jak jsi našel tu první firmu, která potřebovala pomoct? A jak jsi je přesvědčil, že to dokážeš?
Nic zvláštního. První klient, všechno one-man-show. Pocit že jsem všechno pochopil. Následně velká firma s brutálním trafikem, kde jsem dostal tak strašně přes prsty, že zůstala jen předloktí. Tím jsem získal jakýsi minimální skill. No a pak šlo o mou povahu. Většině vývojářům se v nekvalitním softwaru hrabat nechce. Mě to tolik nevadí, a tak jsem to vysloveně začal nabízet jako službu. "Jo, není problém, dám vám to dohromady. Ne, není nutné business zastavit, jen ho zpomalíme." Což přirozeně klientům vyhovuje. Samozřejmě se musím stále učit, abych zůstal na špici, a měl klientům co nabízet.
Kromě toho, že naprosto souhlasím s tím, co napsali Jan Judas a BoneFlute, tak jenom ve zkratce:
$price = $product->getFinalPriceWithVAT();
Tohle nevypadá na velký projekt, to se zhroutí už když budu mít zákazníka v jiném státě (jiném daňovém pásmu). Doplňte si tam Brazilský daňový systém a jste totálně v háji. Pokud si tedy neuložíte sourceAddress a zákazníkův shippingAddress (a spoustu dalších detailů, které Brazilský daňový systém ovlivňují) někam do Product před zavoláním funkce, což by v Php fungovalo (neznamená, že je dobrý pattern). Můžete argumentovat, že Brazílie je daleko, ale požadavek na EU je rozhodně běžný.
class Application_Admin ... static init()...
A co když ten modul budu chtít použít někde jinde a jenom změnit logger nebo authentication filter? Jsme zpátky u toho - píšete o velkých projektech, ale nemáte ani kousek reusability.
Rozdíl je právě v tom (jak píše BoneFlute), že vaše verze bude fungovat do nějaké míry a možná bude i rychlejší. Ale jakmile zákazník přijde s netypickým požadavkem, tak konkurence změní komponentu v DI, zatímco v Php-Jet se bude přepisovat kód, který bude fork-em původního kódu odjinud (pokud tam bude vůbec nějaké sdílení), což s sebou přinese šílené náklady vůbec na udržitelnost.
Nezlobte se, ale takovýmhle tónem v diskusi pokračovat nechci. Stále vás považuji za rozumného člověka a nerad bych, aby se diskuse zvrhla v osobní útoky jako např. u pánů dw nebo greenlinuxguru.
Jinak souhlasím s tím, co napsal BoneFlute - pokud sám tvrdíte, že technický argument má být podložen čísly, tak byste to měl udělat. Vy tvrdíte, že "DI je mýtus", ale nepodložil jste to vůbec ničím, pouze vašimi dojmy a pocitem - a to je prostě málo.
PHP ani weby mě neživí, takže čísla, která byste si představoval, vám bohužel nejsem schopen poskytnout. Ale pracuju už 10 na stále stejném produktu v Javě - a části, které byly psány vaším způsobem, vykazovaly pokaždé stejný živostní cyklus: rychle napsáno, ale údržba a přidávání dalších funkcí pak šlo čím dál pomaleji, až se to nakonec zadrhlo úplně a muselo se to přepsat do příčetnějšího návrhu. Části, které používaly best practices jako DI a SRP, sice ze začátku trvaly napsat o něco déle, ale čas na přidání dalších funkcí je konstantní a se zvětšováním projektu se nijak nezvyšuje.
Zdravim na jedne strane pisete ze si zakladate na cislech a faktech pritom ale zadna neuvadite vidim tu pouze dojmy. pisete ze je "obecnejsi a flexibilnejsi nez Nette / Symfony" tak mi prosim ukazte jak jednoduse zmenite DB ne pro celou aplikaci ale pro jednu konkretni komponentu.
Rikate nastudujte si: ale tady neni moc co studovat to co vy prezentujete jako DI je naprosto bezna vec ServiceLocator jak uz tu jini psaly. Na tom paternu neni vubec nic spatneho a sve uziti ma jen to principielne neni nic jineho nez Globalni promena a nese si sebou vyhody / nevyhody s tim spojene.
Pisete:
Měnit třeba parametry konstruktoru v celém projektu, nebo na X velkých projektech? Naprosto nemyslitélné.To mi prijde ze naopak vy jste neco nepochopil jak funguje moderni DI v projektech jako symfony / nette / Spring protoze zmenit konstruktor je opravdu jenom o tom ze zmenim konstruktor po vetsinou prave na 2 mistech
Nikde jinde s konstruktorem nepracuji vse za me udela DI kontejner a ja mam naprosto volnou ruku. pouzivat vytvaret si instance rucne uz se az na vyjimky proste nenosi a vyzraly DI kontejner za me udela veskerou spinavou praci ja proste jen reknu ze chci v konstruktoru novou sluzbu "Logger" a taky ji tam dostanu tecka a to i kdyz se ma servisa pouziva na dalsich 100 mistech v projeku.
Mimochodem zrovna u testu se opet sam sebe vyvracite:
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.
Jak napisete test na servisu?
class A{
public function doSomething(int $number){
}
}
pokud se uvnitr vola Product::get() ? vy to ani nemate jak poznat.. ale najednou pisete test a musite se podivat do strev dane classy abyjste videl co vsechno potrebuje ke svemu behu coz je v primem rozporu s tvrzenim "neřeším její vnitřní implementaci. Nezajímá mě. Vím jaké má třída vnější rozhraní a to používám."
Ale v jedné věci s Vámi naprosto souhlasím.
Každý nástroj se hodí na něco jiného. To se naprosto shodneme.
Je chybou tahat postupy např. z vývoje desktopových aplikací na do online aplikací a tak dále.
Ovšem opravdu Jet nechápete. Jet je obecný framework pro jakoukoliv online aplikaci.
Ať už je to web, nebo intranet, e-shop, speciální nástroj s webovým UI (viz Easy Deployer), specializovaný informační systém a tak dále.
S tím vším (a troufám si říct že s mnohem víc) jsem se v praxi setkal a Jet je navržený tak, aby tohle všechno na něm šlo vyvinout rychle a efektivně, aby bylo možné pracovat v týmu a zároveň aby bylo možné aplikaci přizpůsobit.
Dá se na tom postavit dobře webík (teda pokud by pro daný účel nebyl lepší třeba Wordpress - nevím, v této oblasti se moc nepohybji), tak aplikace přes kterou tečou stovky milionu korun a kteoru denně používají lidé jako nástroj obživy - své práce.
A vím, že v této oblasti jsou ostatní mě známé frameworky opravdu velice neefektivní. Už z hlediska organizace práci, struktury celé aplikace a tak dále.
Opakuji: pro mne to není teorie, ale denní realita.
Mirku, máš můj obdiv, že ses pustil do vytváření vlastního frameworku a držím palce (jen ho hlavně nešiř jako open source, haha)
Téma dependency injection je obrovský oříšek pro spoustu programátorů. Ale mám s ním jednu neuvěřitelnou zkušenost: každý (a opravdu nevím o výjimce), kdo začal DI využívat, se nikdy nevrátil k singletonům nebo statickým proměnným. Tedy k něčemu, jako je třeba zmíněné `$product = Product::get($id);`
Navrhuji, pojďme se sejít a probrat to. Nebo tě zvu na své školení, kde DI do hloubky probírám. Věřím, že tě přesvědčím o jeho výhodách. Zároveň bych zkušenost z polemiky využil k připsání další kapitoly do dokumentace nette/di.
Ahoj, ještě jsem do dokumentace Nette připsal článek, který se snaží vysvětlit, proč dependency injection není možné používat jen někde, ale je nutné ho považovat za dogma a náboženskou posledlost. Jinak to přestává být DI.
Nezlob se na mě Davide, ale tvůj příklad s platební bránou je naprosto scestný, a je vidět, že vůbec nerozumíš problematice. Když by si byl profík, tak
a/ Nebudeš posílat 100 dolarů, ale jen 0.01 dolaru. To jistě uznáš, že zcela mění situaci.
b/ Když by si se neutápěl v akademických úvahách, a budeš dělat praxi, tak budeš brát takové prachy, že tě žádných sto babek nemůže vyvézt z míry.
Přečteno 20 756×
Přečteno 18 608×
Přečteno 17 819×
Přečteno 17 569×
Přečteno 16 279×