Jiné závislosti pro děděný objekt než pro rodiče

Lukáš Francálek
Člen | 11
+
0
-

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 | 762
+
+1
-

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 | 8136
+
+1
-

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
+
0
-

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 | 472
+
+3
-

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 | 291
+
+4
-

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:

  1. 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.
  2. 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.
  3. 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 | 762
+
0
-

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
+
0
-

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 | 291
+
+1
-

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 | 291
+
+1
-

$service = ServiceFactory->create(25, 'ColorServiceFactory'). Tohle je to nejhorší co bys mohl udělat. Vznikne ti kód s neprosto neprhlednou strukturu závislostí, kde jakékoli změna může rozbít cokoliv kdekoliv. To vážně nechceš.

mystik
Člen | 291
+
+2
-

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 | 291
+
+2
-

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
+
0
-

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 | 291
+
+2
-

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 | 291
+
+4
-
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)

mystik
Člen | 291
+
+2
-

Jedno z pravidel je, že společnou abstrakci (předka/rozhraní) bys měl použít pokud potřebuješ polymorfismus. Tedy někde používáš některého z potomků aniž bys věděl o kterého jde. Pokud takové použití nemáš tak dědičnost nejspíš není správná cesta.

nightfish
Člen | 472
+
+2
-

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 | 762
+
0
-

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
+
0
-

@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 | 291
+
0
-

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
+
0
-

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 | 291
+
0
-

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 | 291
+
0
-

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
+
0
-

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 | 291
+
+1
-

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
+
0
-

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 | 291
+
0
-

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
+
0
-

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é?

mystik
Člen | 291
+
0
-

Zjus sem hodit kod jak tu db a cache v base modelu pouzivas

mystik
Člen | 291
+
0
-

Vztak Model:Mapper nemusi byt vudy 1:1 i kdyz casto byva. Ale jsou pripady, kdy jeden model pouziva ruzne mappery. A pripady, kdy k jednomu mapperu mas ruzne modely.

Lukáš Francálek
Člen | 11
+
0
-

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 | 291
+
0
-

Takhle to je imho ok jen bych rad videl kde pouzivas tu cache.

Pomud by toho kodu ale bylo vic a s dalsi logikou nebo jsi ho prave potreboval mit i v jinych tridach mimo model z nejakeho rozumneho duvodu tak bych vytvoril Mapper.

mystik
Člen | 291
+
0
-

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 | 291
+
0
-

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 | 291
+
0
-
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
+
0
-

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 | 291
+
0
-

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 | 8136
+
0
-

Nette se v tomhle směru zdržuje doporučování :-) Protože jak píšeš, je to velké dilema.