Jiné závislosti pro děděný objekt než pro rodiče
- Lukáš Francálek
- Člen | 11
Jde o klasický problém s děděním a zároveň předáváním závislostí. O způsobech, jak předávat závislosti vím. O tom, co je správné a co horší taky. Ale nenašel jsem nikde, jak teda tento klasický problém co nejelegantněji vyřešit.
Mám třídu která dědí po jiné základní třídě. Základní třída má nějaké závislosti, ale potomek potřebuje jiné, ale také povinné. Co s tím? Vím jaké jsou možnosti, ale ani jedna se mi nelíbí :D
Potřeboval bych asi něco jako Parametrizovanou továrnu, ale s tím, že ji potřebuji volat ručně a to s parametrem známým až v průběhu requestu (tzn. nemožnost předání přes config).
Např. mám třídu BaseService, která je základní a konstruktorem
předávám připojení k DB, službu pro práci s cache atp. Pak mám třídu
např. Translator extends BaseService
. Té chci ale předat
konstruktorem např. kód jazyka. Nechci ale přidávat ID jazyka a všechny
závislosti pro základní třídu, protože jich je moc + případné další
rozšíření by bylo komplikované atp.
To by asi uměla vyřešit výše zmíněná Parametrizovaná továrna, ale
já potřebuji volat Translator ručně pomocí new Translator
. Jde
nějak pomocí Nette volat $translator = new Translator('cs');
ale
tak, aby měla třída přístup k DB a Cache atp.?
Díky :)
Editoval Lukáš Francálek (10. 1. 2023 20:39)
- Kamil Valenta
- Člen | 822
To je důvod, proč je BaseCokoliv zlo :)
Začal bych od konce. Proč chceš ručně volat new Translator(‚cs‘)
?
Proč nechceš Translator dostat a ručně na něm jen volat
->setLang(‚cs‘)?
- David Grudl
- Nette Core | 8239
Jak píše @KamilValenta. Už samotné
Translator extends BaseService
rovná se špatný návrh kódu, bez
šance to jakkoliv vysvětlit.
- Lukáš Francálek
- Člen | 11
Kamil Valenta napsal(a):
To je důvod, proč je BaseCokoliv zlo :)
Začal bych od konce. Proč chceš ručně volat new Translator(‚cs‘) ?
Proč nechceš Translator dostat a ručně na něm jen volat ->setLang(‚cs‘)?
Protože jazyk je povinný, takže nechci řešit problém s tím, že není nastaven, ale instance třídy existuje.
Odpověď že jde o špatný návrh beru a předpokládal jsem to. Už to, že se dostanu do této situace je ten důvod. Teď už jen zjistit, jak by měl vypadat správný návrh :)
Mě ty Base třídy vyhovují. Vždycky dělávám BasePresenter, BaseModel a těm definuji právě základní funkce, které chci pro všechny Presentery a Modely. To je ještě OK nebo už to je špatně? Nebo je špatně až když Base* má nějaké závislosti? U Presenteru a Modelu to zatím funguje, protože Presenter ani Model nevolám ručně.
Ještě jeden příklad – snad vhodnější:
Mám seznam deseti (nebo klidně stovky) nějakých obecných services
(nemyslím v terminologii Nette, ale prostě třída, která něco umí). Pro
ty mám Interface „Service“, protože k nim potřebuji přistupovat
stejně. A protože všechny služby budou potřebovat připojení k DB a
Cache (a možná jednou další závislosti), tak mám BaseService
s předáním závislostí přes konstruktor.
Jenže v kódu potřebuji na základě uživatelské volby vytvořit instanci
služby – třeba ColorService (nebo SoundService nebo TimeService, cokoli).
Název třídy zjistím až z požadavku uživatele, takže proto
$service = new ColorService();
ale té službě potřebuji předat
informaci (třeba ID té služby, protože ona své ID nezná). Proto
$service = new ColorService(25);
.
Takže teď mě napadají otázky:
- Ve kterém místě se stává návrh špatným?
- Pokud mi u BasePresenter a ModelPresenter fungují – je to OK nebo už to je blbě? Přece ale nebudu předávat všem modelům stejné závislosti, ne?
- Jak celý tento problém vyřešit tak, aby instance ColorService neběžela dokud nemá všechny své závislosti – obecné i specifické)?
Pokud vás napadá kód, kde je to vyřešeno nebo popsáno, tak mě přesměrujte – nechci vás zdržovat něčím, co už se 100× řešilo. Ale jak říkám, když já pokládám dotaz na fóru, tak to už musí být hodně zle. Mám pročtené všechno možné a znám jednotlivé podproblémy a jak je řešit, ale jak vyřešit takovouto konkrétní situaci elegantně jsem prostě nenašel.
- nightfish
- Člen | 519
Lukáš Francálek napsal(a):
- Ve kterém místě se stává návrh špatným?
- Pokud mi u BasePresenter a ModelPresenter fungují – je to OK nebo už to je blbě? Přece ale nebudu předávat všem modelům stejné závislosti, ne?
Zhruba v okamžiku, kdy vytvoříš první třídu, která bude dědit
Base*
předka, ale nevyužije všechny jeho
závislosti.
Na dlouhé zimní večery bych doporučil k pročtení toto vlákno – https://forum.nette.org/…bo-dedicnost
- Jak celý tento problém vyřešit tak, aby instance ColorService neběžela dokud nemá všechny své závislosti – obecné i specifické)?
Závislost vyžadovat v konstruktoru. Pokud nejsem schopen závislost dodat
už při registraci služby v DI kontejneru, tak bych si napsal továrnu,
která mi instanci vyrobí. Tovární metodě bych pak předal všechny
parametry. A pokud bych potřeboval, aby všechny instance pro konkrétní
hodnotu parametru byly identické, tak nevolat pokaždé
new ColorService()
, nýbrž si vytvořený objekt uložit
do pole.
V kódu zhruba takto:
class ColorServiceFactory {
private array $instances = [];
public function create(int $serviceId): {
if (isset($this->instances[$serviceId]) === false) {
$this->instances[$serviceId] = new ColorService($serviceId);
}
return $this->instances[$serviceId];
}
}
Továrnu zaregistruji v DI kontejneru, na místě, kde ji chci použít, si
ji vyžádám přes konstruktor, a pak použiji
$this->colorServiceFactory->create(25)->...
.
- mystik
- Člen | 313
Ideálně by to chtělo příklady kódu jak to teď máš napsané. Ale ve stručnosti co vidím za problémy podle toho jak jsi to popsal.
Mám seznam deseti (nebo klidně stovky) nějakých obecných services (nemyslím v terminologii Nette, ale prostě třída, která něco umí). Pro ty mám Interface „Service“, protože k nim potřebuji přistupovat stejně.
Proč k nim potřebuješ přistupovat stejně? Nic takového by nemělo být za normálních okolností potřeba.
A protože všechny služby budou potřebovat připojení k DB a Cache (a možná jednou další závislosti), tak mám BaseService s předáním závislostí přes konstruktor.
To nemusí být v zásadě špatně, ale opravdu bych se zamyslel jestli opravdu všechny třídy potřebují databázi a cache. Obvykle to tak není.
Název třídy zjistím až z požadavku uživatele
Tady bych rád viděl kód, který máš protože ač výběr polymorfní třídy pole požadavku je validní use-case není zase tak obvyklý. Otázka tedy je zda si tam něco zbytečně nekomplikuješ.
Jak celý tento problém vyřešit tak, aby instance ColorService neběžela dokud nemá všechny své závislosti – obecné i specifické)?
Jednoduše tu třídu musíš vytvářet až ty závislosti máš a předáš. Otázka spíš je, jestli ty instance vytváříš na správném místě.
Co bych ti poradil promyslet je:
- Nepoužíváš dědičnost tam, kde by měla být použitá kompozice? Dědičnost se často chybně zneužívá proto, abys dostal nějakou společnou funkčnost do tříd. Ve většině případů je ale lepší použít kompozici. Teda než mít nějakou Base třídu ve které ta funkčnost je lepší mít nějaký další objekt, který tu funkčnost umí a ten předávat všem třídám jako závislost. Vždycky se snaž upřednostnit kompozici před použitím dědičnosti.
- Nemícháš do jedné třídy služby (což by měly být globální objekty, které dělají nějakou činnost) a entity (objekty představující něco z domény problému). Například ta tvoje ImageService na mě působí že spojuje service, které načítá/ukládá obrázky a entitu, které představuje obrázek. Obvykle je lepší toto oddělovat. Hodně se tím zjednoduší. Service máš jen jednu, ta má závislost na databázi apod. A entity si vytváříš libovolně a nepotřebují žádné závislosti. A když chceš uložit obrázek tak zavoláš metodu ze service a předáš jí entitu.
- Factory služby. Pokud potřebuješ na různých místech vytvářet
instance a jejich vytvoření je složité (třeba proto, že mají různé
závislosti) tak obvyklé řešení je udělat si factory službu, které se
stará o vytváření instancí. Ve zbytku aplikace pak jen místo
new Image($id, $db, $cache)
voláš$imageFactory->createImage($id)
a to vytváření instance je schované ve factory. Pokud pak zjistíš, že potřebuješ další závislost stačí upravit factory a zbytek kódu se nezmění.
Editoval mystik (11. 1. 2023 9:20)
- Kamil Valenta
- Člen | 822
Lukáš Francálek napsal(a):
Protože jazyk je povinný, takže nechci řešit problém s tím, že není nastaven, ale instance třídy existuje.
V tomto konkrétním případě bych se nebál mít jeden jazyk jako výchozí a public metodou to případně jen přepnout na jiný.
Mě ty Base třídy vyhovují. Vždycky dělávám BasePresenter, BaseModel a těm definuji právě základní funkce, které chci pro všechny Presentery a Modely. To je ještě OK nebo už to je špatně?
No, toto vlákno vzniklo proto, že Ti to vlastně až tak moc nevyhovuje,
protože v předkovi (Base*) máš závislosti, které dál nechceš.
Druhý neduh je, že programátor tu závislosti nevidí, dokud si předka
neproklikne.
Třetí neduh je, pokud se nemýlím, že se takové závislosti nedají při
testech mockovat (nebo alespoň já nevím jak).
- Pokud mi u BasePresenter a ModelPresenter fungují – je to OK nebo už to je blbě? Přece ale nebudu předávat všem modelům stejné závislosti, ne?
Proč ne?
- Jak celý tento problém vyřešit tak, aby instance ColorService neběžela dokud nemá všechny své závislosti – obecné i specifické)?
Já bych se nejprve snažil dostat z hlavy termín „obecná závislost“. Ony jsou všechny specifické :) Některé jsou méně časté, jiné více. Ale specifické jsou všechny.
- Lukáš Francálek
- Člen | 11
Děkuji všem. Přikládám pár komentářů
@nightfish Pokud mám třeba 30 služeb se stejným interfacem a většina připojení k DB použije, tak považuješ za lepší mít 30 továren a zvlášť závislosti? Mě přijde lepší jeden předek s přístupem do DB a když třeba 7 z těch 30 služeb DB nepotřebuje, tak .. mě to moc vadit nebude, ne?
Rady nezpochybňuji, vážně si chci dostat do hlavy vaše myšlení a
zkušenosti. Jinak za ukázku díky. Nelíbí se mi mít tolik továren kolik
mám služeb, ale umím si představit
$service = ServiceFactory->create(25, 'ColorServiceFactory')
.
–
@mystik Přistupovat k nim stejně chci protože všechny služby
mají jeden vstup a jeden výstup (s různým obsahem). Např.
ColorService
předám barvu a ona mi vrátí informace o barvě.
DateService
předám datum a ona mi vrátí info o datu,
GpsService
předám polohu a ona mi vrátí info o místě.
Všechny nebo skoro všechny chtějí mít přístup k DB, kde mají data.
–
@KamilValenta
No, toto vlákno vzniklo proto, že Ti to vlastně až tak moc nevyhovuje, protože v předkovi (Base*) máš závislosti, které dál nechceš.
No u Modelů a Presenterů je to zatím OK, tam využívám všude všechno. Problém nastává u ostatních tříd, kde potřebuji připojení k DB atd.
Ale obecně – nejde jen o připojení k DB, ale když si udělám
BaseModel a ten má připojení k DB a nějaké obecné metody pro práci
s DB, třeba findAll()
a findById()
atp. a
ošetřené názvy DB tabulek (nerad používám hardkódnuté názvy tabulek
pro případ změny názvu nebo potřeby zavést prefix atp.).
A z něj dědí třeba ArticleModel
, kde neřeším NIC – jen
používám $this->db
a $this->tableName
a
$this->findAll()
a až když potřebuji, tak přidám metodu
findByAuthor()
. Toto je fakt špatný přístup?
No a pak chci třeba třídu, která dělá něco nesouvisejícího se samotným chodem webu – jen obsluhuje uživatelův požadavek – třeba pro výpočty vzdáleností mezi dvěma body v mapě (data má v DB). Takže potřebuje přístup do DB, ale zároveň chci mít vyřešené názvy tabulek a třeba použít ty obecné metody, které mám v BaseModelu :D Tak jí dám rodiče BaseModel abych to celé nedělal vícekrát.
- mystik
- Člen | 313
Můžeš mi ukázat kód, kde ty služby voláš tak, že potřebuješ aby měly jednotné rozhraní? Mě nenapadá případ, kdy bych potřeboval jednotně volat metodu ze nějaké service a bylo mi jedno jestli mi vrátí Color nebo Gps.
Třída co počítá vzdálenosti mezi body by vůbec neměla sahat do databáze. TA by měly dostat dva body vytažené z databáze a spočítat vzdálenost.
Z toho co popisuješ mi fakt čím dál víc přijde, že ty používáš dědičnost místo kompozice, protože ti to přijde jednodušší než si předávat závislosti. Což ti pak přináší spoustu komplikací.
- mystik
- Člen | 313
Ty bys měl mít třeba objekt Gps, který představuje souřadnice a nemá ideálně žádné závislosti a nějakou službu GpsRepository, která zajišťuje načtení/uložení Gps do databáze. Pak můžeš mít třídu GpsCalculator, která umí s předanými Gps objekty dělat výpočty.
Když pak chceš spočítat vzdálenost 2 bodů tak si jako závislost dáš GpsRepository, na něm zavoláš metodu find abys získal první a druhý bod, Pak si vytvoříš GpsCalculator, předáš mu ty dva body a necháš si spočítat výsledek.
Tak máš jasně rozdělené zodpovědnosti a oddělené závislosti. Takže pak případné změny a rozšiřování jsou mnohem jednodušší.
- mystik
- Člen | 313
Pokud by sis o tomhle chtěl přečíst víc a kouknout i na argumenty proč to dělat takhle i když je to o trošku složitější, tak sem kdysi dávno napsal sérii článků o tech principech, kterými se OOP návrh řídí https://zdrojak.cz/…artin-jonas/
- Lukáš Francálek
- Člen | 11
mystik napsal(a):
Můžeš mi ukázat kód, kde ty služby voláš tak, že potřebuješ aby měly jednotné rozhraní? Mě nenapadá případ, kdy bych potřeboval jednotně volat metodu ze nějaké service a bylo mi jedno jestli mi vrátí Color nebo Gps.
Kód není, ale je to tak, jak říkám :D
Lepší příklad – převody jednotek…
Kdybych dělal web na převody jednotek. V DB bych měl 100 kombinací
převodů a podle requestu uživatele bych zvolil správnou službu
(Vzdálenost/Objem/Rychlost) a předal jí data uživatele (from:‚km‘,
to:‚m‘, value:5) a služba by si třeba sáhla do DB (a nebo ne – to je
její věc) a vrátila „5000“. To bych poslal uživateli.
V Presenteru je mi jedno jestli jde o vzdálenost nebo objem – vezmu data
od uživatele, dám službě a návratová data ze třídy vrátím uživateli.
Jen musím zavolat správnou službu třeba DistanceConverter
implementující IConverter
a dědící z BaseConverter, která
má třeba metodu array getData(void)
, která
jen: return $this->result;
Tu kompozici tady nevidím jako přínos. Která třída by vlastnila kterou?
Kdybych měl třídu Converter
, která má přístup k DB a ta by
vlastnila třídu Distance
, tak jí stejně musím předávat ty
přístupy do DB, protože nevím, jaká data chce (a jestli vůbec).
Seriál přečtu, dík.
- mystik
- Člen | 313
To co popisujes by mela byt jedna trida s metodou convert.
Ty se do sebe snazis nejak smichat sluzbu a jeji vysledek. Pritom by ses tohle mel naopak snazit co nejvic oddelit.
Co myslis tim ze by ji vlastnila? Na co by Distance potrebovala pristup do databaze? Distance je jen entita obsahujici nejaka data. V tomhle pripade treba value a unit. Nic vic. Rozhodne by nemela mit moznost se nejak sama nacitat z databaze.
- mystik
- Člen | 313
interface Converter
{
public convert(float $value, string $fromUnit, string $toUnit): float;
}
$converter = new DatabaseConverter($db);
echo $converter->convert(10, 'km', 'm');
Pokud bys chtěl data reprezentovat objektově pak třeba:
interface ValueWithUnit {
public function getValue(): float;
public function getUnit(): string;
public function __toString(): string;
}
class Distance implements ValueWithUnit {
}
interface Converter
{
public convert(ValueWithUnit $from, string $toUnit): ValueWithUnit;
}
$converter = new DatabaseConverter($db);
echo $converter->convert(new Distance(10, 'km'), 'm');
Pak si můžeš udělat jakoukoli implementaci converteru. Přes databázi, přes soubor, přes volání API, … a zbytek kódu bude úplně stejný. Pokud začneš databázi míchat někam do vstupů, výstupů tak si zaděláváš na problém.
Editoval mystik (11. 1. 2023 20:49)
- nightfish
- Člen | 519
Lukáš Francálek napsal(a):
@nightfish Pokud mám třeba 30 služeb se stejným interfacem a většina připojení k DB použije, tak považuješ za lepší mít 30 továren a zvlášť závislosti? Mě přijde lepší jeden předek s přístupem do DB a když třeba 7 z těch 30 služeb DB nepotřebuje, tak .. mě to moc vadit nebude, ne?
Mám 30 služeb, z toho tak 25 má závislosti, které jsou
„pevné“ – službu zaregistruji do DI kontejneru, továrnu nepotřebuji.
Některé z těch služeb nemají žádné závislosti, některé potřebují
databázi, některé potřebují cache, některé potřebují jiné služby –
do každé služby dám jenom ty závislosti, které potřebuji. (Když tam mám
nějakou navíc, tak mě tuším PHPStan upozorní, že mám ve třídě
property, do které se jen zapisuje – to je závislost, kterou v dané
třídě vůbec nepoužívám a mohu ji tedy odstranit.)
Pro zbývajících 5, kterým potřebuji závislost přidat runtimově, buď
udělám Nette generovanou továrnu (interface + registrace do DI) – pokud
potřebuji vytvářet více instancí této služby, a nebo napíšu tovární
třídu (class + registrace do DI), která vytvoří instanci jenom jednou a
při dalších voláních mi tuto instanci vrací opakovaně.
Pokud bych měl 30 služeb a k nim potřeboval 30 továren, tak by mi to přišlo zvláštní a snažil bych se vymyslet, jestli by to nešlo udělat jednodušeji.
>
Rady nezpochybňuji, vážně si chci dostat do hlavy vaše myšlení a zkušenosti. Jinak za ukázku díky. Nelíbí se mi mít tolik továren kolik mám služeb, ale umím si představit
$service = ServiceFactory->create(25, 'ColorServiceFactory')
.
Volat továrnu, které jako argument předáš továrnu, kterou to má použít k vytvoření něčeho, sice možné je, na pár projektech to používáme, ale z vývojářského hlediska je to velmi nepříjemné, protože to vytváří skryté závislosti, které z kódu ani konfigurace DI kontejneru napřímo nevyčteš.
- Kamil Valenta
- Člen | 822
nightfish napsal(a):
Volat továrnu, které jako argument předáš továrnu, kterou to má použít k vytvoření něčeho, sice možné je, na pár projektech to používáme, ale z vývojářského hlediska je to velmi nepříjemné, protože to vytváří skryté závislosti, které z kódu ani konfigurace DI kontejneru napřímo nevyčteš.
+ to nepůjde otypovat a IDE nebude napovídat, přes to bych se nepřenesl…
Editoval Kamil Valenta (12. 1. 2023 8:40)
- Lukáš Francálek
- Člen | 11
@mystik K tomu příkladu s převodem…
A teď hypoteticky … řekněme, že mám délkovou míru loket a značí se
„l“ a mám litr, který je taky „l“. A řekněme, že délkové míry
se převádí jiným způsobem než objemové. Té metodě convert
bych musel předat i info o jakou jde veličinu aby si mohla zjistit, jestli
požaduji vyřešitelnou kombinaci a jak ji vyřešit. A jednotlivá řešení
by byla kde? Určitě ne v metodě convert
, ale v jednotlivých
třídách reprezentující veličiny, ne?
Tzn. naznačuji potřebu různých výpočtů, ale se stejným rozhraním
(vstupem a výstupem).
A ještě poprosím o reakci na:
Lukáš Francálek napsal(a):
Ale obecně – nejde jen o připojení k DB, ale když si udělám BaseModel a ten má připojení k DB a nějaké obecné metody pro práci s DB, třeba findAll() a findById() atp. a ošetřené názvy DB tabulek (nerad používám hardkódnuté názvy tabulek pro případ změny názvu nebo potřeby zavést prefix atp.).
A z něj dědí třeba ArticleModel, kde neřeším NIC obecného – jen používám $this->db a $this->tableName a $this->findAll() a až když potřebuji, tak přidám metodu findByAuthor(). Toto je fakt špatný přístup?
- mystik
- Člen | 313
V tomhle případě bych použil tu objektovou verzi. Informace o tom o jaký typ jednotky se jedná pak vyplývá z předaného parametru. Pokud předám objekt Distance tak hledám v databázi záznamy pro převod délek. Pokud předám objekt typu Volume hledám záznamy pro převod objemu.
Různé instance converteru podle typu veličiny by ale mohly dávat smysl pokud bys měl ty převodní tabulky v kódu. Pak by dávalo smysl mít ConverterCalculator a jeho implementace VolumeConverterCalculator, DistanceConverterCalculator, … Někde pak ale musíš mít logiku, která určí, jaký typ veličiny převádíš.
Osobně bych pak měl service Converter s rozhraním uvedeným výše, kterou bys používal v presenteru. A ta by interně pak vybrala správnou implementaci ConverterCalculator a tu použila pro provedení výpočtu.
Ale obecně – nejde jen o připojení k DB, ale když si udělám BaseModel a ten má připojení k DB a nějaké obecné metody pro práci s DB, třeba findAll() a findById() atp. a ošetřené názvy DB tabulek (nerad používám hardkódnuté názvy tabulek pro případ změny názvu nebo potřeby zavést prefix atp.).
A z něj dědí třeba ArticleModel, kde neřeším NIC obecného – jen používám $this->db a $this->tableName a $this->findAll() a až když potřebuji, tak přidám metodu findByAuthor(). Toto je fakt špatný přístup?
V tomhle případě je to asi ok.
Osobně v tomhle případě volím jednu úroveň navíc. Mám třídy *Mapper pro jednotlivé datové typy, které dědí od nějakého BaseMapper. Třída Model/Repository/DAO pak dostává předaný Mapper jako závislost a jako rozhraní vystavuje jen metody, které se týkají práce s konkrétním datovým typem pro zbytek apliakce (findAll, findById) a to včetně toho, že definuje typ návratové hodnoty atd. Model pak neřeší závislosti na databáízi/cache/… které může interně používat Mapper.
Každý model si mimo příslušného Mapperu může vyžadovat i nějaké další závislosti (často třeba používá interně jiný Model, nějakou service co řeší výpočty, …)
Model i Mapper jsou service zaregistrované v DI a vždycky mají jen jednu instanci. Předávání závislostí tak za mě vyřeší DI a já se nemusím o nic starat.
V Presenteru si pak jen řeknu o Model a s ním pracuju a vůbec neřeším co je na pozadí za závislosti.
(Ve skutečnosti to mám ještě trochu složitěji, kvůli možnosti mít data i jinde než v DB, lazy loadingu atd. ale tím to teď nebudu komplikovat)
Editoval mystik (13. 1. 2023 9:24)
- Lukáš Francálek
- Člen | 11
No a pokud by toto ještě bylo OK, tak co když do BaseModelu
předávám krom DB i Cache?
Protože Modely pracují s daty z DB a mohou chtít načtená a zpracovaná
data cachovat. Mohl bych předávat Cache, jen tam, kde ji fakt potřebuji (ale
už bych bojoval s tím, že BaseModel
chce DB a
ArticleModel extends BaseModel
chce i Cache a už by se to
komplikovalo v konstruktorech.
Proto dám Cache už BaseModelu a mají ji všichni nehledě na to, jestli
některé modely Cache nepoužijí. A opět – udělám si třeba pomocné
vlastnosti nebo metody pro cache přímo v BaseModelu – např.
$this->getCache(?string $key = null)
, kdy pokud nepředám
$key
, tak se vytvoří cache s klíčem podle názvu děděné
třídy, takže ArticleModel->getCache()
dostane cache
s klíčem ArticleModel
.
Je mi jasné, že tady už je něco špatně, ale nevím co :D
Resp. trošku vím, ale spíš nevím, jak z toho ven…
Editoval Lukáš Francálek (13. 1. 2023 9:48)
- mystik
- Člen | 313
Správně by sis měl cache předávat jen tam, kde ji potřebuješ.
Ty pomocné metody by pravděpodobně měly být součástí Cache ne BaseModelu, ale to byhc musel vidět jak přesně vypadají. V určitých případech by dávaly smysl jako protected metody v BaseModel.
Co přesně myslíš tím, že by se konstruktory komplikovaly?
Vždycky si můžeš vytvořit BaseCachedModel extends BaseModel a cachované modely dědit od ní.
- mystik
- Člen | 313
Třeba ten příklad s cache bych asi řešil takhle
abstract class BaseCachedModel extends BaseModel {
public function __construct(Database $db, Storage $cacheStorage)
{
parent::__construct($db);
$this->cache = new ModelCache($cacheStorage, static::class /* default key */);
}
}
Editoval mystik (13. 1. 2023 9:57)
- Lukáš Francálek
- Člen | 11
Tou komplikací myslím to, že pokud BaseModel vyžaduje DB a ArticleModel
vyžaduje Cache, tak musím ArticleModelu předat i závislosti pro BaseModel.
A v ArticleModelu volat parent::__construct(DB)
A to je vlastně
důvod, proč jsem založil toto téma.
Pokud udělám BaseCachedModel, tak mi to zavání budoucí potřebou
multidědičnosti. Co když bude chtít jiný Model ještě něco jiného a to
něco, co budou chtít i některé další modely? To už se blížíme
k traitám, ale ty zase neumožňují společné vlastnosti.
Proto v úvahách vždycky skončím na BaseModelu, který toho má a umí
hodně a dědí z něj třídy, které dělají jen svá specifika.
- mystik
- Člen | 313
Volat konstruktor rodiče přece není žádný problém ne? Tomu by ses neměl snažit vyhýbat. Naopak tohle bys měl upřednostnit před čímkoli jiným.
Předpokládám, že se bojíš situace, kdy rodiči přidáš závislost a budeš muset upravit všechny potomky. Pokud ti tohle hrozí je to opět známka, že bys možná měl změnit strukturu. A závislosti rodiče nějak sdružit. Viz v mém příkladu Modelu předávám jen Mapper. Pokud přidám cache do Mapperu Model ani jeho potomci se nezmění.
V okamžiku, kdy máš pocit, že bys potřeboval multidědičnost jsi právě v bodě, kde začínáš vidět, proč je lepší preferovat kompozici před dědičností. Při kompozici si můžeš libovolně nakombinovat jaké objekty používáš.
To že máš třídu, která toho umí hodně a od ní dědíš věci, které dělají jen specifika je chyba. Ta třída totiž bude nevyhnutelně bobtnat a jakákoli změna v ní bude ovlivňovat spoustu jiných tříd. Dlouhodobě je to pak noční můra na udržování. Opravdu radši vol kompozici. Několik malých tříd, které dělají to co máš teď v tom BaseModel. A jednotlivé modely si jen řeknou o ty z nich, které používají.
- Lukáš Francálek
- Člen | 11
U volání konstruktoru rodiče může být problém to co popisuješ, potom to, že se dobře zapomíná volat v odvozených třídách a taky to, že může být potřeba tahat závislosti přes odvozené třídy (tj. vypořádání se s tím – co když je závislostí moc atp.).
Ten Mapper je něco ve smyslu ServiceLocator? To jeho použití jsem možná nepochopil.
100% souhlasím, že kompozice je v dritivé většině lepší než dědičnost, ale asi ji neumím použít u zásadnějších závislostí. Tj. mít v nějaké třídě jako vlastnost připojení k DB a další vlastnost třídu pro práci s Cache.
K poslednímu odstavci – s tímto nemám zkušenost jako s chybou. Základní třída sice bobtná, ale pouze o funkce použitelné ve všech odvozených. Jinak musejí bobtnat ty ostatní. Ale je fakt, že mohou vznikat metody, které využijí dvě třídy a tam už je to takové nahnuté. Nechci ty metody dávat do dvou tříd (protože DRY), ale v základní už je to dost diskutabilní.
Editoval Lukáš Francálek (13. 1. 2023 11:56)
- mystik
- Člen | 313
Zapomenute volani konstruktoru rodice je bug ktery odhalis hned.
Pokud jenzavislosti moc je to signal ze je neco spatne a mel bys mozna nejakou funkcnost presunout do oddelene tridy nebo tridu rozdelit.
ServiceLocator nepouzivat nikdy.Mapper je proste trida co zapouzdruje presistenci dat. Umi predany objekt ulozit/nacist z databaze, hledat,…
Na co narazis u kompozice za problem? Muzse hodit priklad kde nevis jak to pouzit? Zkusim ti ho upravit.
Ty predpokladas ze funkcnost je bud v rodici nebo rozkopirovana v potomcich. Ale lepsi reseni je ji nemit ani v rodici ani v potomcich ale v samostatne tride, kterou pak pouzivaji potomci co tu funkcnost potrebuji. Dedeni funkcnosti od rodice bys mel nahradit kompozici.
- Lukáš Francálek
- Člen | 11
Ad Mapper – aha, už to vidím. To nevypadá zle, ale ke každému modelu je potřeba zvlášť Mapper. Ale OK.
ServiceLocator – souhlas – mě by sice vyřešil všechno, ale to bych mohl rovnou udělat všechno staticky :D
Ad Kompozice – No kdybych chtěl použít tu kompozici pro DB a Cache v těch Modelech. Tam Ti to taky přijde vhodné?
- Lukáš Francálek
- Člen | 11
BaseModel
declare(strict_types=1);
namespace App\Model;
use Nette\Caching\Cache;
use Nette\Caching\Storage;
use Nette\Database\Explorer;
use Nette\Database\Connection;
use Nette\Database\Table\Selection;
abstract class BaseModel
{
protected string $tablePrefix = '';
protected string $defaultTableName = '';
protected array $tables = [
'article' => 'article',
'author' => 'author',
];
public function __construct(
protected Connection $dbConnection,
protected Explorer $dbExplorer,
private Storage $cacheStorage,
) {
}
public function findAll(): Selection
{
return $this->dbExplorer->table($this->getTableName($this->defaultTableName));
}
protected function getTableName(?string $tableName = null): string
{
$tableName = $tableName ?? $this->defaultTableName;
return $this->tablePrefix . isset($this->tables[$tableName]) ? $this->tables[$tableName] : $tableName;
}
protected function getCache(?string $nameSpace = null): Cache
{
$nameSpace = $nameSpace ?? $this->getClassName();
return new Cache($this->cacheStorage, $nameSpace);
}
protected function getClassName(): string
{
$classParts = explode('\\', get_class($this));
return end($classParts);
}
}
Odvozený model
declare(strict_types=1);
namespace App\Model;
use Nette\Database\Table\ActiveRow;
final class ArticleModel extends BaseModel
{
protected string $defaultTableName = 'article';
public function findBySlug(string $slug): ?ActiveRow
{
$article = $this->dbExplorer
->table($this->getTableName())
->where('slug = ?', $slug)
->fetch();
return $article;
}
}
Což u těch modelů nemám problém, pokud mi nevadí, že mohu mít v nějakém modelu závislost, kterou pak nepoužiji. Problém je, když ten BaseModel mám tendence dát jako předka jiné třídě, která už má jinou funkčnost, ale taky potřebuje DB a Cache a ty bázové metody – ale navíc vlastní konstruktor. To už cítím, že scházím z pěkné cesty…
Editoval Lukáš Francálek (14. 1. 2023 10:01)
- mystik
- Člen | 313
Zhruba něco v tomhle smyslu:
abstract class BaseMapper
{
public function __construct(
string $tablePrefix,
protected string $defaultTableName,
protected Connection $dbConnection,
protected Explorer $dbExplorer
) {
}
public function findAll(): Selection
{
return $this->dbExplorer->table($this->getTableName($this->defaultTableName));
}
protected function getTableName(?string $tableName = null): string
{
$tableName = $tableName ?? $this->defaultTableName;
return $this->tablePrefix . isset($this->tables[$tableName]) ? $this->tables[$tableName] : $tableName;
}
}
abstract class BaseModel
{
public function __construct(
protected Mapper $mapper,
private Storage $cacheStorage,
) {
}
public function findAll(): Selection
{
return $this->mapper->findAll();
}
}
abstract class AnotherService
{
public function __construct(
protected Mapper $mapper,
) {
}
public function findAll(): Selection
{
return $this->mapper->findAll();
}
}
`
Editoval mystik (14. 1. 2023 9:55)
- mystik
- Člen | 313
Je tam pak otázka kde a jak budeš vytvářet ty Mappery s parametry a jak to vzájemně provázat.
A můžýeš si vytvořit odvozená ArticleMapper, který v konstruktoru zavolá rpodiče a nastaví mu ty parametry. A v ArticleModel a Another Service pak budeš mít už jako závislost ArticleModel. Pak ti pojede autowiring a předejde se chybám, že někam předáš chybný mapper.
- mystik
- Člen | 313
abstract class ArticleMapper extends BaseMapper
{
public function __construct(
protected Connection $dbConnection,
protected Explorer $dbExplorer
) {
parent:.__construct('article', '', dbConnection, dbExplorer);
}
}
abstract class BaseModel
{
public function __construct(
protected ArticleMapper $articleMapper,
private Storage $cacheStorage,
) {
}
public function findAll(): Selection
{
return $this->articleMapper->findAll();
}
}
Editoval mystik (14. 1. 2023 9:57)
- Lukáš Francálek
- Člen | 11
Tu cache může potřebovat třeba ArticleModel na uložení „nejčtenějších“ – to jsem tam nedal.
Ale začínám se myslím chytat…
Ještě do toho budu chvilku koukat a zkusím přenést na svůj playground.
Ještě se zeptám – jsou nějaké konvence (obecné nebo Nette), kam ukládat různé pomocné třídy? Grupovat spíš podle funkce (Interface, Factory, Mapper) nebo spíš podle témat (Article, Author, Comment)? Vím, že fo funguje všude a je to moje věc, ale pokud jsou nějaké zvyky nebo konvence…
Každopádně moc díky za trpělivost. Čtu i ten seriál, ale jako vždy – tam mě všechno dává smysl … a pak to neumím aplikovat :D Ty ukázky mě víc nutí přemýšlet jinak.
- mystik
- Člen | 313
To je velke dilemma jak udelat tuhle strukturu. Nette doporucuje spis podle modulu (skupina temat) a pak podle funkce. Me spis vyhovuje podle funkce a az v dalsi urovni podle modulu (u jednoduchy apek tuhle vrstvu nekdy vynecham). Ale je to spsi otazka co se ti vic libi nez ze by na to byla spravna odpoved.
Pokud bys chtel na tohle tema dalsi zdroje tak muzu doporucit nejake knihy/clanky.
- David Grudl
- Nette Core | 8239
Nette se v tomhle směru zdržuje doporučování :-) Protože jak píšeš, je to velké dilema.