přístup v modelu k ostatním modelům

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

Nějak nemůžu přijít na to, jak nejčistším způsobem umožnit modelu přistupovat k jiným modelům – a jestli to, že to potřebuji, náhodou samo o sobě nesmrdí :-)

Řekněme, že aplikace řeší statistiky hráčů golfu. Hraje se na turnajích, zaznamenává se skóre a složitým algoritmem se počítají body, které hráči podle dosaženého skóre (a jiných údajů) dostanou.
Mám model PointGenerator, který se potřebuje dotázat ScoreModelu na skóre, EventModelu na to, kdo vyhrál celý turnaj a kdo vyhrál v jednotlivých skupinách (neurčuje to jen skóre), a dalších modelů na další věci.

Z presenteru přistupuji k modelům přes model loader, ale jak se dostat ke ScoreModelu z PointGeneratoru?

Vidím následující možnosti:

  1. PointGeneratoru předám celý klavír (model loader)
  2. konstruktoru PointGeneratoru vyjmenuji všechny modely, které může kdy potřebovat
  3. PointGeneratoru nepředám modely, ale hodnoty, které bych prostřednictvím těchto modelů potřeboval získat. Tj. modely budu volat mimo PointGenerator v presenteru a předám výsledky

Ad 1) Předávání klavíru je de facto prohřešek proti DI, což smrdí. Kdybych pak chtěl PointGenerator testovat, budu muset asi mocknout model loader a vracet mockované modely.

Ad 2) Nakonec je jich docela dost a jak tak refaktoruju, musím je furt přidávat a vyhazovat z config.neon a deklarace proměnných v PointGeneratoru. Otravná manuální práce.

Ad 3) Tím si ale zabordelím presenter a navíc opět, hromada otravného psaní při změnách.

Napadá vás k tomu něco? Předem díky.

arron
Člen | 464
+
0
-

Hutný dotaz :-)

Osobně bych asi nakonec skončil u toho, že PointGeneratoru bych předával přímo ty body. Nicméně by se hodila další třída, která tohle všechno zastřešuje, dostává svoje závislosti (EventModel, ScoreModel) a zajišťuje logiku celé té operace.

Snažil jsem se být stručný, je tomu alespoň trochu rozumnět?

Editoval arron (24. 4. 2012 22:48)

Filip Procházka
Moderator | 4668
+
0
-

Nějak nemůžu přijít na to, jak nejčistším způsobem umožnit modelu přistupovat k jiným modelům

To nepotřebuješ. Máš sestavit graf tříd v DI Containeru – propojit je tím, že je navzájem předáš.

konstruktoru PointGeneratoru vyjmenuji všechny modely, které může kdy potřebovat

Tohle je z tvých vyjmenovaných ta nejlepší. Ovšem je hloupé předávat „všechny modely, které může kdy potřebovat“. Daleko jednodušší je předat ty, které zrovna teď potřebuješ. Pokud jich potřebuješ moc, tak musíš třídu rozdělit na víc menších a ty složit do sebe.

Další věc je, že žádnou ruční práci dělat nemusíš. Když budeš důsledně psát typehinty, tak za tebe všechno udělá autowiring.

arron
Člen | 464
+
0
-

HosipLan wrote:

konstruktoru PointGeneratoru vyjmenuji všechny modely, které může kdy potřebovat

Tohle je z tvých vyjmenovaných ta nejlepší…

Ono totiž záleží na tom, co přesně ta třída PointGenerator dělá. Protože možná je to přesně ta, o které já mluvím jako o té „zastřešující“ :-) V tom případě jsme se s @HospiLanem shodli :-)

Editoval arron (24. 4. 2012 22:50)

Honza Marek
Člen | 1664
+
0
-

Model loader je v případě, že máme krásný DI kontejner, naprostá zbytečnost. Řeší to samé jako DI kontejner, jen méně obecně.

petr.pavel
Člen | 535
+
0
-

Musím přiznat pánové, že z těch odpovědí nejsem tak úplně moudrý. Zareaguji na vaše připomínky odděleně.

arron napsal(a):
… PointGeneratoru bych předával přímo ty body. … další třída, která tohle všechno zastřešuje, dostává svoje závislosti (EventModel, ScoreModel)…

Jenom se ujistím: myslel's, „předával hodnoty“, ne „body“, že jo? Body tenhle objekt vytváří z hodnot jako skóre, vítěz, pořadí odpalování atd.

Rozdělení PointGeneratoru na zastřešující a jádro:
Tím ale jen odsunu problém jinam a řeším, jak předat modely tomu zastřešujícímu objektu. Nebo jsem tě nepochopil.

Editoval petr.pavel (25. 4. 2012 9:41)

Filip Procházka
Moderator | 4668
+
0
-

To že vůbec řešíš, jak někam něco předávat je postavené na hlavu. Tohle je práce pro DI Container. Model loader je anti-pattern.

petr.pavel
Člen | 535
+
0
-

HosipLan napsal(a):

…umožnit modelu přistupovat k jiným modelům

To nepotřebuješ. Máš sestavit graf tříd v DI Containeru – propojit je tím, že je navzájem předáš.

Promiň, ale tomuhle nerozumím. Co to je „graf tříd“? (Google jsem zkoušel.)
DI Container používám, parametry předáváné PointGeneratoru definuji v config.neon (což je zmíněná varianta 2) a samotný PG získávám přes $this->context->createPointGenerator(...). Jestli to není poznat z mého původního postu, řekni prosím, co tam chybí, doplním.

A nerozumím, proč konstatuješ, že nepotřebuji umožnit modelu přistupovat k jiným modelům, a přitom dál vysvětluješ, jak to udělat. Jestli to fakt nepotřebuju, tak jak má PG získávat vstupní hodnoty pro generování bodů?

…je hloupé předávat „všechny modely, které může kdy potřebovat“. Daleko jednodušší je předat ty, které zrovna teď potřebuješ.

To je nedorozumění: Nemyslel jsem klavír, myslel jsem opravdu jen ty, které potřebuje. Ono to není tak, že by konkrétně PG potřeboval hodně modelů. Potřebuje aktuálně myslím tři. Ale stejná situace je i u některých jiných modelů a jak refaktoruju, tak do toho seznamu použitých modelů musím furt hrabat. To je ta ruční práce, které se chci vyhnout.

Pokud jich potřebuješ moc, tak musíš třídu rozdělit na víc menších a ty složit do sebe.

Tohle už je relativní. Nemyslím, že by a priori bylo lepší nahradit dlouhou metodou složitou hierarchií objektů. V mém případě si myslím, že už to dál drobit není dobré a že PG dělá opravdu jen jednu práci a dělá ji dobře :-) Ze stejného důvodu nevidím, jak bych ho mohl ještě dál rozdělit na zastřešující a jádro (@arron).

Další věc je, že žádnou ruční práci dělat nemusíš. Když budeš důsledně psát typehinty, tak za tebe všechno udělá autowiring.

Tohle je moje definice PG v Neonu:

factories:
		pointGenerator:
			class: PointGenerator(..., %constants%, %customization%)
			parameters: [event, eventModel, scoreModel]
			setup:
				- $event(%event%)
				- $eventModel(%eventModel%)
				- $scoreModel(%scoreModel%)
class PointGenerator extends BaseModel {
  // nemá konstruktor
}
abstract class BaseModel extends NObject {
public function __construct(NotORM $database, $constants, $customization) {
	// $constants a $customization jsou parameters/constants a parameters/customization z Neonu
}
}

Nevím, jak tohle vylepšit typehintováním a kam ho narvat. Nakonec možná ani nebude dědit BaseModel, nechci z PG přistupovat do db přímo, ale jen prostřednictvím modelů.

Typehintování přece taky bude v nějakém seznamu (nejspíš v seznamu parametrů konstruktoru PG), takže si nepomůžu. Zase je to seznam, který musím ručně upravovat.

EDIT: Oprava definice PG v Neonu.

Editoval petr.pavel (25. 4. 2012 10:22)

Vojtěch Dobeš
Gold Partner | 1316
+
0
-

Proč ten PointGenerator nefunguje jako služba? Pokud dobře chápu jeho roli, ty mu něco předáváš pouze za účelem výpočtu, nejsou to „jeho“ data. Pokud pak interně potřebuje nějakou další službu, fajn, ať ji dostane v konstruktoru. Pokud potřebuje nějakou službu tehdy a v té samé metodě nebo své jiné public metodě zase jen jinou službu, tak to je ideální adept na rozdělení do více tříd, více specializovaných, které vždy mohou s čistým svědomím požadovat pouze ty služby, které určitě využijí.

No, a PointGenerator má asi nějakou public metodu (jak se jmenuje?). Té bych předával pak konkrétní turnaje atd. (myslím entity).

petr.pavel
Člen | 535
+
0
-

vojtech.dobes napsal(a):
Proč ten PointGenerator nefunguje jako služba?

Já myslel, že služba se od továrny liší jen tím, že služba si hlídá, aby měla jen jednu instanci, a tím, že továrně lze předávat při volání parametry. Jedinou instanci nepotřebuji, zatímco parametr předat potřebuju – id turnaje, pro který body počítám.

No, a PointGenerator má asi nějakou public metodu (jak se jmenuje?). Té bych předával pak konkrétní turnaje atd. (myslím entity).

Má veřejné metody getEvent(), generate() a save(). V podstatě rozdělení procesu přiřazení bodů na fáze, které volám z presenteru a po kterých případně přesměruji uživatele s chybovou hláškou. Souhlasím, že pokud nějaký model/službu potřebuju volat jen v jedné metodě, měl bych ho předávat rovnou té metodě. Jenže ty modely, které předávám PG přes továrnu, potřebuje víc metod (i některé private). Takže bych řekl, že to předávání továrně je v tomhle případě košer.

Se zbytkem souhlasím, pěkně napsáno. Předávám data jen za účelem výpočtu. Resp. předávám modely proto, aby mohly být zavolány pro získání dat potřebných pro výpočet :-)

Původně jsem chtěl debatovat o problému obecně, ale vidím, že to asi nejde a že vždycky záleží na konkrétní situaci.

arron
Člen | 464
+
0
-

petr.pavel wrote:

Jenom se ujistím: myslel's, „předával hodnoty“, ne „body“, že jo? Body tenhle objekt vytváří z hodnot jako skóre, vítěz, pořadí odpalování atd.

Jo takhle přesně jsem to myslel, ale blbě jsem to napsal.

Rozdělení PointGeneratoru na zastřešující a jádro:
Tím ale jen odsunu problém jinam a řeším, jak předat modely tomu zastřešujícímu objektu. Nebo jsem tě nepochopil.

Přesně, přesuneš problém na místo kam patří a ne tam, kde nemá co dělat.

Jestli to správně chápu, tak ani tak nejde o to, jak technicky se to pak vyřeší, ale spíš jak to správně a čistě navrhnout. To, že to pak bude někde v nějakém neonu a že celou tu práci s předáváním závislosti udělá container, považuji tedy za samozřejmé.

Takže opravdu zcela obecně. Mám z toho pocit, že ta třída PointGenerator porušuje single responsibility principle, protože dělá víc věcí než by dělat měla. Má přepočítávat body z hodnot. Čili jejím vstupem jsou hodnoty a výstupem body. Kdo ty hodnoty a kde získá, to by neměla být její starost ne? Problém se tím posouvá o level výš, ale na místo kam patří.

Píšeš, že tam jsou 3 public metody (getEvent(), generate() a save()). Takže já vidím 4 třídy. Jednu, která umí získat eventy, jednu, která umí generovat body (PointGenerator), jednu, která je dokáže uložit a jednu, která ví, jak celý tenhle proces projít. A není to takový overkill jak to vypadá, protože: EventModel načte eventy, PointGenerator vygeneruje body, ScoreModel je uloží a chybí tam ta třída, která to umí celé poskládat dohromady. Když to nebudeš chtít řešit, tak to bude nějaký presenter nebo na to uděláš samostatnou třídu, které se predájí ty dva modely a instance PointGenerátoru, který má využít. Výsledek: celkem logická, testovatelná architektura, kde každý dělá to, co umí nejlépe :-)

A teď otázka, je tohle to, co jsi chtěl slyšet? ;-)

Filip Procházka
Moderator | 4668
+
0
-

petr.pavel napsal(a):

Promiň, ale tomuhle nerozumím. Co to je „graf tříd“? (Google jsem zkoušel.)

Graf tříd je, že je skládáš do sebe.

$a = new A(new B(new C), new D(new E, new F(...)));

Připomíná to totiž graf ;)

A nerozumím, proč konstatuješ, že nepotřebuji umožnit modelu přistupovat k jiným modelům, a přitom dál vysvětluješ, jak to udělat.

Nepotřebuješ modelu umožnit kamkoliv přistupovat, když mu to, kam potřebuje přistupovat, předáš. Hollywood: „Nevolejte nám, my zavoláme vám.“

Nemyslím, že by a priori bylo lepší nahradit dlouhou metodou složitou hierarchií objektů.

No, a já si to myslím :)

Tohle je moje definice PG v Neonu:

Samozřejmě, parametry si musíš předat ručně. To je přeci jasné, jak bys to chtěl napsat jednoduššeji? To prostě nejde. Někde to napsat musíš, ať v configu, nebo třeba někde v kódu. Tomu se nevyhneš. A v configu je tomu lépe.

Proč je to factory? Udělal bych raději třídu, která by přijímala parametry metodou, které jí teď předáváš konstruktorem, a vracela ti nějaký jiný objekt, nakonfigurovaný :)

class PointGenerator extends Nette\Object
{
	// ...
	public function generate($event) // eventModel, scoreModel ??? předat konstruktorem!
	{
		return new MatchPoints($this->eventModel, $this->scoreModel, $event);
	}
	// ...
}

$matchPoints = $generator->generate(10);
$matchPoints->add(10);
$matchPoints->save();

Třeba, nebo tak něco. Z tvého kódu mám pocit, že máš dopletené co je to entita.

Prostě to máš špatně navržené, kdyby ne, tak tyhle problémy neřešíš :)

petr.pavel
Člen | 535
+
0
-

arron napsal(a):
…ani tak nejde o to, jak technicky se to pak vyřeší, ale spíš jak to správně a čistě navrhnout.

Je pravda, že takhle jsem položil otázku, ale jsem praktik a akademicky čisté řešení, jehož součástí bude 20 souborů/tříd každá o pěti řádkách nedokážu považovat za nejlepší :-) Takže mi jde o čistotu a eleganci obojího (návrhu i implementace).

Takže já vidím 4 třídy. … A není to takový overkill jak to vypadá…

Myslím, že ti rozumím, a souhlasím, že výsledek je snadno testovatelný (o což mi taky jde).

Jen si nemyslím, že vytváření jednoúčelových tříd napomáhá přehlednosti. Taky mám dojem, že PG porušuje single responsibility třeba tím, že si samo získává seznam turnajů. (Trochu mlžím tím getEvent() protože ve snaze nezahltit vás informacemi, kloubím dohromady společné nemoci PointGenerator, FlightAssignmentGenerator, TeeTimeGenerator… Takže kdybych si někdy odporoval, bude to tím.)

Jenže PG potřebuje jen určité turnaje (z určité série a jen ty, co mají skóre). Takový seznam nepotřebuji nikde jinde v celé aplikaci. Takže akademicky čisté řešení by sice bylo přesunout getEvents do EventModelu, jenže když tohle udělám vždycky, bude EventModel mít stovku metod, z nichž většina se použije jen jednou. A zakládat místo toho PGEventModel a TTEventModel a FAEventModel a … mi nepřijde jako krok k větší přehlednosti.

A teď otázka, je tohle to, co jsi chtěl slyšet? ;-)

Ani nevím, co jsem chtěl slyšet :-) Všechny vaše komentáře pro mě byly zatím cenné a díky za ně. Polemizujeme a o to mi šlo.

petr.pavel
Člen | 535
+
0
-

@HosipLan: Díky za vysvětlení a ukázkový kód. Myslím, že chápu, jak's to myslel. Vypadá to jako akademicky čisté řešení, na pohled se mi líbí :-)

Nechce se mi ale štěpit jeden 300 řádkový soubor na 10 tříd celkem o 400 řádcích. Takhle mám veškerou logiku generování/počítání bodů na jednom místě, nemusím lítat od čerta k ďáblu, když hledám, jak se vlastně body počítají.

Z tvého kódu mám pocit, že máš dopletené co je to entita.

To je možné, těžko říct. Entity jako takové nepoužívám vůbec (resp. ne v této části) a nechybí mi.

Prostě to máš špatně navržené, kdyby ne, tak tyhle problémy neřešíš :)

Určitě to mám špatně navržené. Teď už jen přijít na to, jak je to správně ;-)

petr.pavel
Člen | 535
+
0
-

Honza Marek napsal(a):

Model loader je v případě, že máme krásný DI kontejner, naprostá zbytečnost. Řeší to samé jako DI kontejner, jen méně obecně.

Člověče nevím… nepovažuji to za zbytečnost. Moje modely dědí BaseModel, jehož konstruktoru předávám něco, co nejde automaticky nadrátovat přes typehinting. Kdybych měl nahradit model loader DI kontejnerem (v Nette se bavíme o contextu a configu, že?), tak bych v configu musel vyjmenovat všechny modely, které v aplikaci mám, a ke každému zkopírovat ten samý seznam parametrů konstruktoru. Takhle mi stačí seznam jeden pro model loader. Nemám pravdu?

Honza Marek
Člen | 1664
+
0
-

Pravdu máš v případě, že veškeré modelové služby bez výjimek mají přesně ty samé parametry konstruktoru. To mi přijde ale dost zvláštní, u mě se tohle nestává. Pokud bude jedna modelová service chtít využít jiné service, jak to uděláš?

Nelíbí se mi taky to, že presentery a další třídy přistupují k „modelům“ přes nějakého prostředníka, jehož úkolem je řešit jen ty stejné parametry konstruktorů modelových tříd. Ale co je presenteru po tom, jak se která služba vyrábí? On je chce dostat všechny stejně.

arron
Člen | 464
+
0
-

petr.pavel wrote:
…ale jsem praktik a akademicky čisté řešení, jehož součástí bude 20 souborů/tříd každá o pěti řádkách nedokážu považovat za nejlepší :-) Takže mi jde o čistotu a eleganci obojího (návrhu i implementace)…
…Jen si nemyslím, že vytváření jednoúčelových tříd napomáhá přehlednosti…

Jako praktik bys měl ocenit, že když pak potřebuješ někde něco změnit, tak v případě čistého návrhu to změníš na jednom místě a je hotovo :-) Vytváření jednoúčelových tříd napomáhá čistotě návrhu zatímco přehledné samo o sobě nebývá. K přehlednosti Ti dopomohou zásady psaní čistého kódu, kdy budeš správně a přehledně pojmenovávat proměnné, správně strukturovat kód apod.

petr.pavel
Člen | 535
+
0
-

@Honza Marek: Nad zápachem stejných parametrů předávaných konstruktoru modelu se zamyslím, díky.
Když potřebuji předat jiné služby (jako např. v případě PointGeneratoru), tak nepoužívám model loader ale žádám rovnou context.

Ale co je presenteru po tom, jak se která služba vyrábí? On je chce dostat všechny stejně.

Tohle jsem asi nepochopil. Když nebudu používat model loader a vždy budu žádat context, tak sice budu v presenteru všechny modely získávat stejně, ale moc nevím, v čem/proč je to lepší. Teď „běžné“ modely získávám přes model loader a „speciální“ přes context.

Honza Marek
Člen | 1664
+
0
-

Teď „běžné“ modely získávám přes model loader a „speciální“ přes context.

To mi přijde jako úplně zbytečná schizofrenie.

David Grudl
Nette Core | 8145
+
0
-

Možná by pomohlo z úvah úplně vypustit slovo model a dívat se na to jako na obyčejné třídy. Neexistuje nic jako „modely“.

Ascaria
Člen | 187
+
0
-

Napadlo někoho něco jako:

class PointGenerator extends Nette\Object
{
        // ...
        public function generate($event, EventModel $e, ScoreModel $s)
        {
                return new MatchPoints($e, $s, $event);
        }
        // ...
}

$matchPoints = $generator->generate(10, $eventModel, $scoreModel);
$matchPoints->add(10);
$matchPoints->save();

Tenhle způsob se asi víc hodí, když z nějakého důvodu není možné použít konstruktor… a navíc dělá to tak třeba i funkce mysql_query kerá jako druhý argument přijímá objekt spojení.

Editoval Ascaria (17. 5. 2012 10:43)

petr.pavel
Člen | 535
+
0
-

@David Grudl: Obecně máš pravdu a pak by vlastně tvůj seriál o DI řešil stejnou otázku (což je do jisté míry pravda).

Moje otázka je víc v kontextu Nette, kde mám:

  • model loader, který mi inicializuje objekty a předává jim výchozí parametry (všem modelům stejné)
  • config.neon, jeho definice továren s možnosti nastavit hodnoty objektu předanými hodnotami (i jinými z configu)
  • a DI kontejner, který mi zprostředkovává přístup k továrnám z config.neon

Dosavadní odpovědi se hodně věnovaly obecné architektuře, což nebylo vyloženě na škodu, pořád ještě nad nimi přemýšlím.

Původně jsem ale čekal spíš odpověď typu:
Nejčistší je předávat všechno v parametrech konstruktoru nebo metodě, což lze za okolností xyz usnadnit přes config.neon, ale naopak za okolností abc si tím spíš přiděláš práci. Za okolností def je pak lepší situaci chápat jako ghf, při níž je zcela v pořádku předávat celý container a nechat model ať si z něj vytahuje, co potřebuje.

V souvislosti s unit testováním zjišťuji, že je nejlepší funkcionalitu modelu rozdrobit tak, jak moc podrobně ji chci testovat, a předávat všechny závislosti v parametrech. Přiznám se ale, že podvádím. Místo toho, abych údaje o turnaji zjišťoval přes TurnajModel, hrabu rovnou do databáze. Právě proto, abych si nemusel předávat tucet modelů, z nichž z každého si zavolám jednu metodu.

petr.pavel
Člen | 535
+
0
-

@Ascaria: Díky, ale nechal ses unést @HosipLanovým příkladem. Ve vašich příkladech PG předává závislosti nějaké třídě MatchPoint, která (asi) částečně body sama počítá a částečně se jí zvenku body posílají. Ve skutečnosti PG je právě tím jediným místem, které počítá body, a zvenku se už nic nepřidává. PG potřebuje detaily turnaje, odehraného skóre, detaily hráčů, kteří se účastnili, detaily hráčů, kteří byli diskvalifikování atd. Vaše kódy tedy oba reprezentují mou možnost 2). Nic proti tomu, jestli tohle jste chtěli sdělit.

Další štěpení PG na jiné třídy/objekty (děti) ale neřeší mou otázku. Stejně úplně nahoře bude vrstva, která bude vědět o všech dětech, a o tom, co potřebují. Takže si mou otázku přeformulujte jako „jakým způsobem inicializovat tuto nejvyšší vrstvu“. Jestli totiž za tuto vrstvu považujete presenter, tak to asi předpokládáte, že řešením je má možnost 1).