ModelFactory v modulární aplikaci
- pehape
- Člen | 9
Často se v modulárních aplikacích setkávám s tím, že jsou modely zaregistrovány jako služby a poté jednotlivě „inject“-ovány do presenterů, formulářů, komponent. Na tom určitě není nic špatného, až na to, že ve výsledku to dopadne následujícím způsobem (zjednodušeno):
<?php
config.neon:
services:
- FirstModel
- SecondModel
...
- TenthModel
?>
<?php
class MyPresenter {
/** @var FirstModel @inject */
public $firstModel;
/** @var SecondModel @inject */
public $secondModel;
...
/** @var TenthModel @inject */
public $tenthModel;
}
?>
Samozřejmě si můžeme napsat tovární třídu, která bude modely vytvářet a „inject“-nout tu. Bohužel stejně musíme napsat metody, které budou dané modelové třídy vytvářet. Napdala mě následující věc a tak ji s Vámi chci zkonzultovat. Uvažujme modulární aplikaci, která sdílí modely napříč moduly.
Tovární třída:
<?php
class ModelFactory
{
/** @var \Nette\Database\Context */
private $db;
/** @var array */
private $scopes;
use ModelFactoryTrait;
public function __construct(\Nette\Database\Context $db, array $scopes = [])
{
$this->db = $db;
$this->scopes = \array_merge(['' => ''], $scopes);
}
public function __get($model)
{
// Validation
...
$className = \ucfirst($classAlias);
$namespace = $this->scopes[$scope];
if (isset($this->$model) === TRUE) {
return $this->$model;
}
$fullClassName = $namespace . '\\' . $className;
$this->$model = new $fullClassName($this->db);
return $this->$model;
}
}
?>
config.neon
<?php
parameters:
modelScopes:
admin: '\App\Modules\AdminModule\Models'
services:
modelFactory:
class: App\Modules\BaseModule\Factories\ModelFactory(..., %modelScopes%)
?>
V presenteru pak pouze
<?php
class MyPresenter {
/** @var App\Modules\BaseModule\Factories\ModelFactory @inject */
public $modelFactory;
public function render() {
// Příklad získání someModel-u z Admin modulu
$this->modelFactory->admin_someModel->...
}
}
?>
Bohužel v tomto okamžiku je IDE co se týče napovídání bezmocné. K tomu by mohl posloužit ModelTrait, který by byl autogenerovaný a obsahoval by následující:
<?php
trait TModelFactory {
/** @var \App\Modules\AdminModule\Models\SomeModel */
public $admin_someModel;
}
?>
Výsledkem by tedy byla dynamická tovární třída na vytváření modelů, která by šla skrze celou aplikaci. Vždy bych „inject“-oval pouze tuto tovární třídu namísto jednotlivých modelů.
TODO: Přepsat daný příklad jako extension.
Otázka na ty zkušenější z nás zní, zda se tímto mám zabývat, nebo to okamžitě zavrhnout jako chybné řešení. Děkuju.
Editoval pehape (27. 2. 2016 16:13)
- pehape
- Člen | 9
Díky za reakci. Doporučuješ tedy držet závislosti na nižší úrovni … Šlo mi o to, když přidám nějaký sdílený model, který se promítne v 5 formulářích napříc 3 moduly, mám ho okamžitě k dispozici, bez nutnosti přepisování „construct-inject“ method.
Editoval pehape (27. 2. 2016 16:17)
- Oli
- Člen | 1215
@pehape ono to na první pohled vypadá, že ti to usnadní práci. A ono taky jo, pokud děláš na jednom projektu a víš, že rozhodně nikdy nebudeš potřebovat ten presenter jinde (v jiným projektu). Za presenter si dosaď cokoli jinýho. Pokud By jsi stejným způsobem pracoval třeba s formulářem…
Taky jsem dělal nějaký takový agregace služeb a vypadalo to dobře. Ale schováváš tím závislost. Ono to vypadá, že ten presenter má jen jednu závislost. Ve skutečnosti jich má třeba 5. A vím ze zkušenosti, že je mnohem jednodušší přenést 5 malých tříd do jiného projektu, než přenést „presesnter“, zjistit že má jednu závislost. Přenést tu a najednou zjišťuješ, že potřebuješ dalších 5 závislostí. Nedej bože, když jedna z nich zase něco schovává. V tuhle chvíli si řekneš, „tak na to se můžu vykašlat“ a radši to přepíšeš, protože nemáš tušení kam až ty závislosti šahají.
Pojinta teda je, neschovávat závislosti za nějakou agregaci závislostí a držet si co nejmenší třídy. Paradoxně je jednoduší přenést třídu s 6 malými závislosti než s 1 obří, rozvětvenou závislostí…
- Šaman
- Člen | 2668
Já si nemyslím, že by agregace závislostí (v určité míře) byla nutně zlo. Ale určitě bych to nedělal taka magicky, jak navrhuješ v prvním příspěvku.
Domnívám se, že celý model mohu chápat jako jedinou závislost. Ano,
každý presenter, nebo komponenta pak bude mít misto jednoho repozitáře
závislost na celém modelu, ale princip zůstane zachovaný. Presenter závisí
na službě Model
, ten má mnoho závislostí na jednotlivých
repozitářích a dalších services. Vlastně se dá říct, že je pak
třída Model
nejvyšší fasádou všech nižších tříd
modelu.
Teď mám všechno řešené přes repozitáře (výjimečně je potřeba nad
nimi ještě fasáda), které injectuji do tříd presenční vrstvy
(presentery, komponenty). Ale přijde mi, že až moc odhaluji vnitřní logiku
modelu.
Třeba továrna na formulář pro vytvoření nové osoby potřebuje nejen
PersonRepository
, ale také repozitáře všech číselníků
(pohlaví, tituly). Podle mě by je měl být schopný dodat přímo
Model
(ve smyslu služba) a je mi jedno, jestli si to načte
z databáze, nebo třeba z definovaných konstant.
Teď mám třeba SexRepository
na několika místech a pokud bych
chtěl tuto tabulku zrušit a nahradit to konstantami ve třídě Person
(Person::SEX_MALE, Person::SEX_FEMALE), tak musím přepsat každý výskyt.
Pokud by měl Model
metodu getAllSex()
, přepsal bych
jen tuto.
Samozřejmě, pokud se to neudělá magicky, tak našeptáváni zůstane
zachované. Ať už by byl model aka service lokátor (tedy
$model->getSexRepository()->getAll()
), kde metoda
$model->getSexRepository()
bude správně anotovaná, stejně
tak případná $model->getAllSex()
, která by vracela kolekci,
nebo pole entit (Sex[]
).
- F.Vesely
- Člen | 369
@Šaman tohle vede do pekel. Kazda trida by se mela ke svym
zavislostem hlasit, aby bylo pekne videt, co ke svemu pusobeni potrebuje. Hezky
se ti pak refaktoruje, jinej programator hned vidi, na cem je zavisla, atd.
Pokud ti jde o zamenu jedne tridy, za jinou, tak k tomu slouzi
Interface
. Vyzadas si v konstruktoru ISexRepository
a
je ti jedno, jestli dostanes SexRepository
, nebo
PersonRepository
, protoze vis, ze musi implementovat metodu
getAllSex()
.
- Šaman
- Člen | 2668
Však se ke svým závislostem všichni hlásí.
Presenter chce Model. Model chce deset různých repository. Není to
skrývání, je to jen obalení celé modelové části.
Prakticky to může být naopak nutné opatření ve chvíli, kdy je model více
komplexní, než jen pár tříd nad databází. Přímé použití nižších
tříd modelu pak může ohrozit konzistenci.
V tu chvíli se může stát, že přímé použití nějakého Repository je
nežádoucí, protože třeba při vytvoření nějaké entity musím ještě
udělat jiné věci. To repozitář nezajímá a zajímat ani nesmí. To, že
při vytvoření článku chci vyhodit událost k aktualizaci RSS kanálu není
věc ArticleRepository. Musím tedy použít nějakou service nad tím. Ta se
vůbec nemusí jmenovat ArticleService. A technicky je problém zaručit, že
se použije správná třída. Když se model uzavře a obalí jasným
rozhraním, tak to zaručitelné je (resp. je to v kompetenci modelu, nikoliv
toho, kdo píše presenter).
Editoval Šaman (28. 2. 2016 13:33)
- F.Vesely
- Člen | 369
Podle me se nehlasi. Pokud by to byla fasada nad par tabulkama, ktere spolu souvisi, tak jo, ale jedna super god fasada nad vsemi tabulkami je podle me spatne.
To co popisujes, se resi pres eventy. Podivej se na Kdyby/Events. ArticleRepository pouze spusti event na vytvoreni clanku, na ktery nasloucha sluzba, ktera se stara o RSS.
- Šaman
- Člen | 2668
Měl jsem rozepsané nějaké delší vysvětlování, ale to by bylo lepší probrat někde osobně u piva. Zkrátím to.
- To, co popisuješ ty, je běžná a na určitém typu projektů osvědčená praxe. V tomto připadě se ovšem pojmem „model“ myslí určitá skupina tříd, většinou nad databází, ke kterým může přistupovat každý, kdo si o ně řekne. Občas je i nejasné, jestli něco patří do modelu, nebo ne (různé pomocné třídy, utility).
- To, co myslím já, vychází spíš z předpokladu, že model je jasně definovaná a uzavřená část aplikace. Model si sám ručí za konzistenci, je uzavřený za rozhraní a na jeho vnitřní třídy nesmí nikdo z venku sahat. Model je z pohledu zvenčí nedělitelný. Může mít i vnitřní stavy, programátor není omezen jeho složitostí. Takových modelů může být i více (třeba v knihovně může být model pro sklad oddělený od modelu pro výpůjčky). Pokud je to vhodné, tak se celý model může přesunout třeba za REST API.
- Ve většině aplikací mám jen dva modely (v tom druhém významu). Skoro všechno patří do aplikačního modelu, navíc mám pak ještě správu uživatelů a rolí. A už několikrát jsem uvažoval o tom, že presenter toho o modelu ví víc, než by měl. Presenter musí znát vnitřní strukturu modelu aby si mohl říct o tu správnou závislost. A nikdo nám nezaručí konzistenci, když presenter klidně může přeskočit vyšší vrstvy a sáhnout si do nižší. Dokud programuje všechno jeden člověk, tak je to v pohodě, ale pokud by model vyvíjel někdo jiný, než presenční část aplikace, tak už to začne skřípat.
Abych to nějak shrnul – neříkám, že tebou popsaný způsob je špatný. Sám ho používám a je to běžný, rozšířený a přehledný způsob práce s modelem. Ale nemyslím si, že je jediný spávný. Pro agregaci závislostí (obalování, nikoliv skrývání!) může být spousta dobrých důvodů.
Editoval Šaman (28. 2. 2016 15:56)
- F.Vesely
- Člen | 369
Šaman napsal(a):
Abych to nějak shrnul – neříkám, že tebou popsaný způsob je špatný. Sám ho používám a je to běžný, rozšířený a přehledný způsob práce s modelem. Ale nemyslím si, že je jediný spávný. Pro agregaci závislostí (obalování, nikoliv skrývání!) může být spousta dobrých důvodů.
Ale jo, jak jsem psal vyse, pokud v nem mas tridy, ktere spolu nejak souvisi, tak ano. Mne slo spise o tvrzeni, ze bys v nem mel uplne vsechno. S tim, ze si nekdo muze v Presenteru sahnout primo na Nette Database nebo EntityManager bohuzel nic neudelas. :)
- Šaman
- Člen | 2668
Moje úvaha jde ale směrem, že většina tříd modelu spolu souvisí. Většinou mám:
- uživatelé a role + authorizátor (jeden model, který by šel znovupoužívat i v dalších aplikacích)
- hlavní aplikační logika (většinou jen jeden model, všechno spolu souvisí)
- výjimečně se pak přidělávala ještě správa novinek, která s aplikační logikou nesouvisela
- ještě by možná šel přes události oddělit model pro logování, já ho mám ale v aplikačním modelu
Jinak to, aby se nesahalo přímo na databázi, to zajistí konvence.
Stejně, jako se podle konvence nesmí sahat na context. Pokud mám ale
ArticleRepository
, ArticleService
,
ArticleCreator
a Publicator
, tak co mám použít,
pokud chci vložit nový článek na net? Bez znalosti modelu to nelze říct a
ani testy moc nepomohou (do databáze se mi zapíše, ale napadne mě, že
chybí informace v RSS generátoru?)
Editoval Šaman (28. 2. 2016 16:30)
- pehape
- Člen | 9
Šaman napsal(a):
Já si nemyslím, že by agregace závislostí (v určité míře) byla nutně zlo. Ale určitě bych to nedělal taka magicky, jak navrhuješ v prvním příspěvku.
Prosímtě, co přesně považuješ za magii? Getter, Trait nebo prefixování modulem? Nebo kombinaci? Jen abych věděl, čemu se vyvarovat …
Editoval pehape (28. 2. 2016 17:00)
- Šaman
- Člen | 2668
pehape napsal(a):
Šaman napsal(a):
Já si nemyslím, že by agregace závislostí (v určité míře) byla nutně zlo. Ale určitě bych to nedělal taka magicky, jak navrhuješ v prvním příspěvku.
Prosímtě, co přesně považuješ za magii? Getter, Trait nebo prefixování modulem? Nebo kombinaci? Jen abych věděl, čemu se vyvarovat …
Getter a to vytváření instancí pomocí operátoru new
.
Z hlediska skrývání závislostí je to stejné řešení, jako použití
statické tovární třídy, nebo přímé instancování pomocí
new
. O tom, že potřebuješ nějakou třídu se u tebe v celém
projektu nepíše, je to dokonale schováno za getter a RobotLoader. Jestli to
dobře chápu, tak tohle ti umožní někam nakopírovat třídu a kdekoliv ji
přes přes ModelFactory používat. Klidně v nějaké zapadlé private
metodě. Taková závislost je velmi dobře ukrytá a dokonce ti bez ní může
projekt fungovat, dokud se nesnažíš použít tuto třídu. To je zdrojem WTF
chyb. Navíc ji vytváříš pro každý požadavek novou instanci, zdá
se mi.
- Rozhodně si každou novou třídu registruj v configu a i kdybys použil tento ModelFactory, tak ji tahej z kontextu přes getByType. V tu chvíli se ti aspoň nahlásí, pokud by ta třída neexistovala, nebo nešla instancovat. A taky budeš pracovat vždy s jedinou instancí stejné služby.
- To ovšem stále skrývá závislosti. Pokud bys tohle chtěl používat čistě(ji), tak si do ModelFactory injectuj každou službu, kterou má znát. Takže při přidání nové služby ji zapíšeš do konfigu a injectuješ do ModelFactory. Tím už ta závislost není skrytá, jasně říkáš, které služby ten ModelFactory zná a pokud budeš chtít nějakou jinou, vyhodí výjimku.
- Presenter (nebo kdokoliv jiný) pak má také jasnou závislost na ModelFactory. Ano, může to být trochu podobné jako s tím piánem a doutníkem, ale i piáno je jasná a neskrývaná závislost. To, že si z něho vezmu jen doutník, to už nikoho zajímat nemusí. (Samozřejmě dokud nezačneš optimalizovat – třeba pokud by měl model obrovské množství tříd a ty bys kvůli každému přístupu vyžádal všechny, pak už je toto řešení moc těžkopádné.)
- pehape
- Člen | 9
Šaman napsal(a):
Jestli to dobře chápu, tak tohle ti umožní někam nakopírovat třídu a kdekoliv ji přes přes ModelFactory používat. Klidně v nějaké zapadlé private metodě. Taková závislost je velmi dobře ukrytá a dokonce ti bez ní může projekt fungovat, dokud se nesnažíš použít tuto třídu. To je zdrojem WTF chyb.
Máš pravdu, toto je dost kritické. Nápad opouštím. Díky za tvé rady.