Diskuze: MVC/MVP - závislost jednoho modelu na jiných

PHP PHP MVC/MVP - závislost jednoho modelu na jiných American English version English version

Avatar
Martin Konečný (pavelco1998):

Zdar,

jak v MVC/MVP řešíte, když v metodě jednoho modelu potřebujete instanci jiných modelů?

V podstatě potřebuji vyřešit problém, který jsem nastínil i na fóru Nette. Příklad - mám hráče kupující předmět od jiného hráče. Metoda buyItem() by teoreticky neměla být ve třídě Player, protože by objekt hráče sám o sobě neměl modifikovat předmět.
Navíc je trochu problém s tím, jak tam ten objekt dostat.

Zkusil jsem realizovat malý návrh, kdy budu mít třídy Player, Item a třídu reprezentující vazbu mezi nimi. Nazval jsem to RelationPlayerItem (nenapadá mě vhodný název). Třída RelationPlayerItem se pak bude starat o všechny akce spojené s hráčem a předmětem.
Je u toho menší problém - když např. budu chtít po koupi předmětu poslat zprávu, pak vazba není jen Player - Item, ale Player - Item - Message. V kódu se to dá snadno udělat tak, že prostě předám i objekt třídy Message. Jen pak úplně nesouhlasí název třídy.

Prvně přiložím ukázkový script (snad se v tom dá vyznat):
http://pastebin.com/cL1iRXMA

Mělo by to fungovat tak, že:

  1. metoda Presenter::han­dleBuy() se zavolá, když hráč klikne na odkaz "koupit předmět"
  2. vytvoří se objekt třídy RelationPlayerItem, které se automaticky předá spojení s databází (v Nette pomocí DI Container::get­Service("db"))
  3. objektu $relation se předají oba požadované objekty ($this->player a $item)
  4. v bloku try se zavolá metoda buyItem(), která použije oba zmíněné objekty a v případě chyby vyhodí výjimku

Ty factory třídy jsem oddělil, v Nette se to takto používá (třídy se automaticky vytvoří podle konfiguračního souboru).

Zkusil jsem to udělat podle obrázkového návrhu:
http://www.imagehosting.cz/?…

Další problém je v tom, že v některých případech potřebuji vytvořit objekt až na základě nějakého výsledku. Např. v tomto příkladu potřebuji vytvořit nový objekt třídy Player uvnitř metody buyItem(), protože teprve tam zjistím ID vlastníka předmětu. Ale nemám ho v té třídě RelationPlayerItem jak vytvořit.
Řešením je být zjistit ID vlastníka předmětu už v Presenteru a $relation předat všechny tři objekty (v příkladu jsem to takto vyřešil).

Myslíte, že se dá touto cestou vydat, nebo je to krok špatným směrem?

Doplním, která řešení se mi nezdají OK:

  1. Metodu buyItem() dát přímo třídě Player, protože je třeba upravit i záznam v tabulce item. Myslím, že je pak porušení SRP, když hráč upravuje předmět.
  2. Předávat instance konstruktorem. Pokud se třída rozroste a budu potřebovat pro jiné metody jiné objekty, pak bude v konstruktoru 20 parametrů (construction hell?)
  3. Ani tohle řešení není dle mě v pořádku. Pořád je problém s tím, že instanci některého modelu potřebuji vytvořit až v metodě buyItem() na základě výsledku z jiného objektu.

To se dá řešit tak, že se kód neobalí do jedné metody PlayerItemRela­tion::buyItem(), ale celý postup bude hozen už do Presenteru:

/**
 * @param int
 */
public function handleBuyItem($id)
{
        $item = $this->itemFactory->create($id);
        $ownerId = $item->getData()->ownerId;
        $owner = $this->playerFactory->create($ownerId);

        // je potřeba provést databázovou transakci
        // je ale databázové spojení v Presenteru správně?
        $db = $this->context->getService("db");

        try {
                $db->beginTransaction();
                $this->player->subtractMoney($item->price);  // vyhodí výjimku, pokud nemá hráč dostatek peněz
                $item->changeOwner($this->player);
                $owner->addMoney($item->price);
                $db->commit();
        } catch (Exception $e) {
                $this->flashMessage($e->getMessage());  // zpráva, která se poté objeví na stránce
                $this->redirect("default");
        }
}

U bodu č. 3 je problém nastíněn v ukázce. Práci s databází by měly obstarávat modely, proto by v Presenteru neměla co dělat.
Na druhou stranu se ale v Presenteru nedělá nic jiného, než se zapne a ukončí transakce. Žádná data v žádné tabulce nemodifikuje. Jak to tedy je?

Tak co, napadá vás nějaké smysluplné řešení? Jsem zvědav, jak si s tím místní programátoři poradí :D

Editováno 28.2.2015 17:22
 
Odpovědět 28.2.2015 17:19
Avatar
Martin Konečný (pavelco1998):

Ještě jsem zapomněl přiložit odkaz na fórum Nette:

http://forum.nette.org/…lu-na-jinych

 
Nahoru Odpovědět 28.2.2015 17:23
Avatar
Martin Konečný (pavelco1998):

Nikdo neví, nebo jsou všichni líní to číst a přemýšlet? :D

 
Nahoru Odpovědět 1.3.2015 12:29
Avatar
tomasmanhal
Člen
Avatar
Odpovídá na Martin Konečný (pavelco1998)
tomasmanhal:

Já jsem rád, že si naťukám v PHP kontaktní formulář, tohle je na mě velká liga moc :-D

Nahoru Odpovědět 1.3.2015 12:45
Kdyby nám dodali k životu zdrojový kód, vše by bylo jednodušší...
Avatar
shaman
Člen
Avatar
shaman:

Priznam sa ze som si to precital a otazka nie je jednoznacna. Takze ani odpoved nebude jednoznacna. Nette nepoznam, predpokladam ale ze je to nejaky novsi framework pouzivajuci composer atd.

SRP zachovaj v kazdom pripade Moje modely su ciste, bez hluposti. Kazda classa ma na validaciu vlozenia dat svoju classu a presenter je dalsia classa. Tymto sposobom mozem hocikedy pouzit ten isty model v kazdej poziadavke.

Service classa je helper classa ktora toto vsetko spaja a je v podstate instanciovana v controlleri. Service classa kontroluje co sa bude robit s ktorym modelom a kedy. Vidim ze ty tvoje service classy volas factory, co nie je uplne spravne. Service classa nemusi byt factory. Neviem ci chapem spravne tvoju MVC strukturu. Vies sem pridat nejaky nakres? Ja som sem prilozil taky zjednoduseny nakres, aby si videl co je moja service classa.

Controller posuva viewu priperavene objekty a view si ich zobrazuje ako treba alebo si zavola presenter konkretneho modelu.

Samozrejme som este vynechal kopu veci ako su eventy, queues ... Takze neviem ci som ti odpovedal, ci si hladal radu ohladom MVC, nette frameworku alebo tvojho konkretneho problemu alebo si len chcel podebatit o tvojich nazoroch na dependency hell.

Nahoru Odpovědět 1.3.2015 13:33
try {...} catch (Exception ignored) { echo " ¯\_(ツ)_/¯ "; }
Avatar
Odpovídá na shaman
Martin Konečný (pavelco1998):

Díky za odpověď.
V Nette je Presenter něco jako prostředník mezi view (šablona) a modelem. Zpracovává požadavky od uživatele, volá příslušné metody modelových tříd a předává data šablonám.

Životní cyklus presenteru pak vypadá takto:
http://files.nette.org/…fecycle2.gif

Startup slouží pro inicializaci vlastností v daném presenteru.
Action se většinou stará o případné změny ještě před vykreslením šablony (změna layoutu, nastavení defaultních hodnot formuláře atp.).
Signál je pro požadavky, které mají např. upravit záznam v databázi.
Before render a render se starají o naplnění proměnných pro danou šablonu.

Presenter má také přístup k DI Containeru, který má v sobě uložené servisní třídy (vždy vrací jen jednu instanci), továrny (pokaždé vytvoří novou instanci) a konfigurační nastavení dle .neon (konfiguračního) souboru.
Model je pak čistě na vývojáři, jak si ho vytvoří. Není tam pro něj žádný interface.

Obvykle model stavíš tak, že má více tříd (v tomto případě např. Player, Item, Message). Otázka je, jak udělat, aby se na jedné operaci mohlo podílet více modelových tříd.
Pro příklad - hráč koupí od někoho předmět. Oběma hráčům pak přijde zpráva o provedeném obchodu. Znamená to, že se na této jedné operaci podílí tři modelové třídy a dohromady 5 objektů:

  1. Player (2x - kupujícímu se odečtou peníze, majiteli předmětu se přičtou)
  2. Item (1x - změní vlastníka a místo, kde se nachází (např. z tržiště na inventář))
  3. Message (2x - oběma hráčům se pošle zpráva)

Podle mě je nesmysl, aby to celé mohl udělat objekt třídy Player

$player->buyItem($itemId);

protože objekt hráče by neměl umět posílat zprávy a modifikovat předmět (pak by to bylo porušení SRP), ale v tomto případě jen přičíst/odečíst peníze.
Logická možnost by byla udělat pro danou operaci speciální třídu, třeba ItemSale, které bys předal všechny potřebné objekty (tříd Player, Item a Message).
Metoda by pak použila všechny objekty a provedla celý proces koupi předmětu (přičtení a odečtení peněz, změnu vlastníka předmětu a poslání dvou zpráv).

Zde ale nastává problém - v tomto případě musím už předem vědět, kdo je vlastníkem předmětu, a vytvořit příslušný objekt (který předám objektu třídy ItemSale).
Co když ale budu potřebovat ten objekt vytvořit až uvnitř metody třídy ItemSale? Tam není možnost, jak ten objekt vytvořit, protože nemá přístup k DI Containeru.

 
Nahoru Odpovědět 1.3.2015 14:37
Avatar
shaman
Člen
Avatar
Odpovídá na Martin Konečný (pavelco1998)
shaman:

Asi ti rozumiem:

$Hrac->kupPredmet($predmetId);

Hrac je model v DB a ma sa starat o data z DB iba ohladne Hraca a nie aj o predmetoch, nedajboze posielat emaily predavajucemu/ku­pujucemu. S tymto s tebou suhlasim.

Trieda ItemSale sa mi nepaci. Nerobim classy ktore maju len jedno pouzite. Na jedno pouzitie su metody, z toho mi vyplyva ze itemSale bude metoda v nejakej classe.

Presenter ma pristup k service kontaineru, takze tvoja metoda ItemSale bude tu. Ak chces predavvat predmet, musis uz poznat objekty kupujuceho, predavajuceho a predmet. Metoda sa bude volat napr:

public function ItemSale( Player $buyer, Player $seller, Item $item)
{
...
return $success;
}

Metoda vracia boolean, takze vies ci posielat emaily, alebo posielanie emailov zavolas vo vnutri itemSale.

Tvoj presenter je asi Controller v mojom nakrese. Vsetky tieto metody by mali byt v pomocnej triede, takze presenter by mal byt cisty a lahko citatelny.

Objekty si podavame, ked volame metodu, tak ako som uviedol v itemSale a neinicializujeme ich vo vnutri! Ked budes robit UnitTesty, tak zistis ze metodu vies lepsie testnut ak si objekty 'mock'-nes a tiez nebudu tvoje testy pomale kvoli ustavicnemu spojeniu s DB. Je tam plno vyhod ale o tom inokedy.

Editováno 1.3.2015 15:51
Nahoru Odpovědět  +1 1.3.2015 15:49
try {...} catch (Exception ignored) { echo " ¯\_(ツ)_/¯ "; }
Avatar
shaman
Člen
Avatar
Odpovídá na Martin Konečný (pavelco1998)
shaman:

Teraz ked pozeram lepsie ten tvoj pastebin tak:

public function handleBuyItem($id)
        {
                $item = $this->itemFactory->create($id);
                $ownerId = $item->getData()->ownerId;

                // relace nyní obsahuje objekty kupujícího hráče, vlastníka předmětu a samotný předmět
                $relation = $this->createRelation("playerItem");
                $relation->player = $this->player;
                $relation->item = $item;
                $relation->owner = $this->playerFactory->create($ownerId);

                try {
                        $relation->buyItem();
                } catch (Exception $e) {
                        $this->flashMessage($e->getMessage());  // zpráva, která se poté objeví na stránce
                        $this->redirect("default");
                }
        }

Ja tam nevidim ziadne

$player->buyItem($itemId);

Odkial mas ten kod co si dal sem do otazky? To je tvoj refactoring, kde si chcel celu tu funkciu presunut do presentera? Tak to tak urob, ale bez tych factory a na buyItem si sprav tiez triedu v presenteri. Aha, to bola tvoja original otazka? :)

Nahoru Odpovědět 1.3.2015 16:19
try {...} catch (Exception ignored) { echo " ¯\_(ツ)_/¯ "; }
Avatar
Odpovídá na shaman
Martin Konečný (pavelco1998):

Vážně moc děkuju za příspěvek.
Ta metodu

$player->buyItem($itemId);

jsem si vymyslel, abych ukázal, jaké řešení by se mi nelíbilo (tj. aby objekt $player s metodou buyItem() i poslal zprávu a upravoval předmět), v kódu ji nemám.

Tvá zpráva mi opravdu pomohla, díky!

Editováno 1.3.2015 16:48
 
Nahoru Odpovědět  +1 1.3.2015 16:48
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 9 zpráv z 9.