ModelFactory v modulární aplikaci

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
pehape
Člen | 9
+
0
-

Č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)

CZechBoY
Člen | 3608
+
0
-

To je stejny jako pouzivat $di->getByType(). Lepsi injectovat primo konkretni modely.
Pokud modely pouzivas napric moduly tak je zkus vice rozvrstvit aby delaly jen jednu vec a ne ostatni – tzn jen to co modul potrebuje aby dany model delal.

pehape
Člen | 9
+
0
-

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

@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í…

pehape
Člen | 9
+
0
-

Zkušenost je nejlepší učitel. Děkuju @Oli.

Můžeme tedy pokládat agregace závislostí za chybný a nedoporučovaný způsob?

Editoval pehape (28. 2. 2016 16:55)

Šaman
Člen | 2668
+
0
-

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

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

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

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

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

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

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

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

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é.)
Šaman
Člen | 2668
+
0
-

Jo a co se toho prefixování modulem týče – model jako takový o modulech nic neví. Jestli tím odděluješ nějaké logicky související části, pak by to spíš ukazovalo na rozdělení do více modelů. A taky předpokládám, že tohle má sloužit jen pro modely, nikoliv pro jiné třídy.

pehape
Člen | 9
+
0
-

Š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.