Jak implementujete servisní třídy vy? Předávání dat, business logika
- frosty22
- Člen | 373
Zdravím Vás,
zajímalo by mě, jakým způsobem implementujete servisní třídy vy, například:
1) Problematika – Označování objednávek za zaplacené
- Buď vytvořím třídu PayOrderProcess, která bude mít metodu execute(Order $order), která přijímá entitu objednávky a označí jí za zaplacenou + udělá dodatečnou logiku
- Napadá mě ještě možnost, kdy bych měl nějakou třídu, která by se starala o ukládání objednávek, a zde by se kontrolovali provedené změny na entitě, a když by byla zaplacená, tak by se to předalo objektu PayOrderProcess samo .. alternativně by mohl být připojený listener na entitě, který by kontroloval změnu stavu ..
2) Problematika – Vytváření produktu
Mám formulář, který uživatel vyplní a z dat se vytváří produkt, teď
ale jak s těmi daty naložit –
- Vytvořit servisní třídu CreateProductProcess, a ten bude přijímat data a z nich vytvoří a uloží produkt .. ale otázka – přijímat data jako parametry metod? Jenže když jich je hodně, je to problématické,
- takže přijímat je přes settery/gettery – blbost to už by reflektovalo entitu …
- takže přijímat jako pole? ArrayHash? .. nevýhoda, musím uvnitř kontrolovat parametry..
- Nebo vytvářet entitu v presenteru, nasetovat jí ty parametry, a potom předat nějakému objektu, který ty data dodatečně zvaliduje a případně uloží ..
… možností je celkově spousty, ale chtěl bych mít hezký objektový kód, takže by mě zajímalo, jak to děláte vy? Abych si našel nějaké tipy.
Díky
- Tomáš Jablonický
- Člen | 115
Tak vytvořil bych si service, která má metoda třeba pro vytvoření objednávky, změna stavu objednávky podle vstupu (vstupy bych definoval do konstant), smazání objednávky (například nastavení flag DELETED).
No a pak bych si udělal Facade, tam bych si sestavil vytvoření objednávky pro registrovaného uživatele, neregistrovaného uživatele, označení objednávky jako přijatá, nepřijatá, exportovaná (vše zajišťuje metoda v services) a taky smazání objednávky s ověřením opravánění atd.
No a nakonec bych Facade využil v presenteru (klidně i ve více presenterech).
- frosty22
- Člen | 373
Děkuji za odpověď, alespoň někdo se ozval :) Jenom tedy to přímo neřeší mou problematiku, tj. jakým způsobem předávat data, zároveň tedy určitě bych to nedával do jedné třídy – nemám rád nějaké god objekty, které dělají tolik věcí, čili bych určitě zůstal u rozdělení funkčnosti.
Mě hlavně šlo právě o tu obecnou logiku, příklady se zaplacením objednávky, vytvářením produktu byly jen příklady, čili ty změny stavu objednávky přes flagy, to už jsou konkrétní implementace.
- arron
- Člen | 464
Jsou tu dvě nezávislé věci:
- označování objednávek jako zaplacené
Jak píše @jablon, z presenteru bych komunikoval výhradně s fasádou, která by měla mít metodu něco jako markOrderAsPaid($order) a musí vědět, co všechno je potřeba udělat (nejenom s objednávkou, ale třeba i poslat mail atd.). K tomu, aby toho tahle třída/fasáda docílila, tak využívá svoje dependencies (OrderModel, MailSender, co já vím). Jinak facade je normální návrhový vzor, který slouží k tomu, zjednodušil přístup k jinak složitým rozhraním a jejich funkce sdružil do logických celků. V zásadě ale sama o sobě nic moc nedělá, jenom deleguje na ostatní.
- předávání parametrů
Zase stejná struktura. Fasáda, která má metodu createProductFromData a která ví, jak se taková věc dělá. Pokud je dat rozumné množství, tak předávat jako jednotlivé parametry, pokud to nejde, tak bych klidně zvolil pole. Tak jako tak bude potřeba celý objekt Product nějak validovat. Buď bude mít dlouhý konstruktor, který zabrání vytvoření nevalidního objektu, nebo by měl mít nějakou metodu validate, která zajistí, že je objekt v pořádku. Takže se té kontrole parametrů nevyhneš a klidně bych se tou cestou vydal.
- frosty22
- Člen | 373
Díky za odpověď, v podstatě tímto principem to nyní dělám, jen tedy akce, které přímo nesouvisí s procesem tj. právě například to odesílání emailů při označování objednávky za zaplacenou navěšuji přes listenery (Kdyby\Events).
Spíše tedy jsem přemýšlel, jestli neexistuje nějaké know-how, které mi chybí – nejsem moc spokojen právě s tím principem například vytváření produktu.
Tím, že využívám doctrine, tak z pohledu OOP se na to hodí továrnička, která slouží k vytváření instancí, takže bych si mohl udělat nějakou továrnu, ale jakmile mám další zdroj, třeba vytváření produktu přes API, tak už mám jiný formát dat, tudíž bych potřeboval další továrničku, OK. A najednou přibude vytváření produktu z importu, které navíc ještě vytváří například kategorie, či značku apod. ...... a tady už by se duplikovala logika, jelikož to vytváření právě nebývá jen na setování properties na objekt, ale i validace.
Takže právě jen hledám jestli mi nechybí nějaký kousek v těch mých skládačkách – entity, získávání dat přes doménové objekty, atd. to vše mám snad relativně hezky pořešené, ale tohle je taková jedna část designu aplikace, ve které si myslím, že mi něco chybí.
- arron
- Člen | 464
frosty22 wrote:
…navěšuji přes listenery (Kdyby\Events).
Jasný, asi i čistší a variabilnější řešení :-)
A proč by ta továrna nemohla mít několik různých metod vytváření produktu? A zároveň s tím samozřejmě jedna metoda může využívat služeb těch ostatních, aby si se nikde nemusel opakovat. Fasáda pak může mít různé metody pro vytváření produktů při různých usecase. Takže třeba createProductFromImport($importData) a podobné.Protože fasáda využívá služeb okolo a umí je jenom správně zkombinovat do ucelených funkcí, tak se imho nikde nic moc opakovat nemusí. Stačí mít okolo fasády dobře rozvržené jednotlivé služby, které dělají jednu malou věc, ale dělají ji správně.
Editoval arron (18. 11. 2013 15:16)
- frosty22
- Člen | 373
Jo když nadtím přemýšlím, tohle by asi šlo :) Já právě hledal jestli někdo nemá nějaké „super magic“ řešení, u které bych si řekl – jasně tohle je bomba, to mi ulehčí práci, ale ve finále takové asi neexistuje, přeci jen je to dost obecný problém.
Původně jsem si hrál s myšlenkou, kdy mám různé readery (FormReader, CsvReader, RpcReader, …), který předám nějaký mapper podle vstupu a ty mi namapujou data na nějakou Collection (v podstatě ArrayHash) a to by se vždy předávalo nějakému procesu – vytvoření objednávky, zaplacení objednávky.. zároveň by každý proces věděl, jaká data přesně potřebuje.
Potom ale co jsem si to celé napsal (i s unit testama :D) (… https://github.com/…ocessManager) tak jsem zjistil, že mi to tahle myšlenka spíše komplikuje než-li usnadňuje …
- frosty22
- Člen | 373
dcepelik no zeptat se samozřejmě můžeš, ale jak jsem již psal v úvodu, tak jsem to plácnul jako příklady, jinak mi vůbec nejde o žádné označování objednávek či vytváření produktů atd. Jde mi o obecnou logiku servisních tříd a rozvržení těchto procesů.
Ale tedy, pokud to měl být sarkasmus, jelikož si myslíš že označit objednávku za zaplacenou nic moc složitého není, tak naopak zrovna tohle může obnášet mnoho – co jsem dělal asi před půl rokem, tak proces zaplacené objednávky dělal následující:
- označení sloupce za zaplaceno (= to co asi čekáš)
- odeslání emailu, že bylo zaplaceno
- u našeho zboží: kontrola a rezervace položek na skladě
- u zboží ext. dodavatelů: objednání položek u dodavatelů
- odeslání platby do účetního systému
- vytvoření výdejky pro sklad
:)
- arron
- Člen | 464
frosty22 wrote:
Původně jsem si hrál s myšlenkou, kdy mám různé readery (FormReader, CsvReader, RpcReader, …)…
Možná by se to trochu zjednodušilo, když si uvědomíš, že se to všechno točí okolo entity. Ona je tím výsledným produktem a z ní se nechají vytáhnout všechna data, která potřebuješ k vytvoření „mapperu“. Takže čistě teoreticky by mělo být možné vytvořit univerzální mapper dat na entitu. A pak samozřejmě různé readery dávají smysl ne? Hlavní otázka pak zní, jestli se to vyplatí na daném projektu. Ale jakmile bude tenhle mapper vymyšlený, tak už se dá použít kdekoliv. Jen jestli je možné udělat něco takového fakt univerzálně. Nevím jak v komerčním prostředí, ale na dlouhé zimní večery je to jistě zajímavé programátorské cvičení :-)
- Filip Procházka
- Moderator | 4668
Můžeš mít různé vstupy
- CreateOrderForm + CreateOrderFormMapper
- CreateOrderApiCall + CreateOrderApiCallMapper
ze kterých ti vyleze entita objednávky a tu pak předáš kam potřebuješ. Podle toho jak by ten mapper byl velký je můžeš schovat do facade, popřípadě na ně udělat samostatnou třídu.
Nebo ti klidně ten mapper může zastupovat celou tu facade a rovnou delegovat na služby. Pokud v něm je pouze ta logika, která se týká konkrétního formuláře/api-callu a všechno ostatní se deleguje tak na tom nevidím nic špatného :)
- Jiří Nápravník
- Člen | 710
frosty22 napsal(a):
- označení sloupce za zaplaceno (= to co asi čekáš)
- odeslání emailu, že bylo zaplaceno
- u našeho zboží: kontrola a rezervace položek na skladě
- u zboží ext. dodavatelů: objednání položek u dodavatelů
- odeslání platby do účetního systému
- vytvoření výdejky pro sklad
Tohle je celkem pěkný příklad (resp. rozpis kroků). Mohl bys mi načrtnout, jak by se to správně mělo řešit. Já mám totiž nějak problém rozpoznat co má být Fasáda a co Service (v kontextu toho pětivrstvého modelu Honzy Tichého, případně článku o Architektuře nad Doctrine na Zdrojáku).
Používám Kdyby/Doctrine, což jsem pochopil že ty taky. No a mě by nejjednodušší přišlo mít nějakou OrderFacade a tam by byla metoda, která by si upravila jednotlivé entity a zavolala flush(). Nicméně to by byla celkem dlouhá metoda, a popravdě není to příliš fasáda, fasáda by měla být co nejtenčí. Neměly by přidávat nějakou funkcionalitu, ale jen přidávat „zkratku“
Tak pak volat nějaké ostatní závislé fasády napr. fasadu pro sklad), které by řešili pak to za co jsou zodpovědné. To už je lepší, ale pořád nevím, jestli je to správně a nemají tam nějakou takovouhle funkci zastávat ty Service třídy a já je pak z Fasád jenom volat. Mám v tom kapku guláš, budu rád za upřesnění…
No a pak je tu ještě jedna věc a to jsou pak nějaké DQL dotazy, zatím je dávám do fasád, ale taky mi to nepřijde úplně správné, protože to přidává novou funkcionalitu… A v Kdyby/Doctrine je zakázáné (a já s tím celkem souhlasím) rozšiřovat repozitáře. Pak tam jsou ty QueryAble třídy (či jak se jmenují) ale popravdě kvůli každému dotazu dělat vlastní třídu mi přijde celkem overkill
- frosty22
- Člen | 373
Jiří Nápravník no já jsem to řešil takto:
- Služba PayOrderProcess by měla metodu markOrderAsPay(Order $order), která by pouze označila objednávku za zaplacenou a flushla do databáze, zároveň by měla událost onPay(Order $order)
- Listener PaidOrderNotification navěšený na událost onPay, posílá email
- Služba CreateWarehouseReservation s metodou reservation(Product $product, $amount), která rezervuje položku zboží ve skladu
- Listener WarehouseReservation navěšený na událost onPay, projde objednávku a pro každý produkt volá metodu reservation v té službě
...... ostatní procesy co jsem uvedl totožně jako s tím skladem .. listener + services
Fasády jsem v podstatě doposud nepoužíval, do presenteru případně importu atd. si vždy předávám ty procesy, ale jinak by šla udělat fasáda nějaký OrderManagement, která by měla tu metodu markOrderAsPay() a zároveň třeba ještě cancelOrder() atd. což pokud je více akcí, tak aby se nemuseli předávat všechny procesy do presenteru zvlášt, případně pokud bych chtěl nahradit PayOrderProcess jiným objektem, tak by to bylo jednodušší..
- Tomáš Jablonický
- Člen | 115
@frosty22 co se týče validace, tak bych to v klidu nechal na entitě nebo na datovém typu (v Doctrine2 se dá přeci vlastní datový prvek vytvořit). No a vstupy by mohla validovat facada nebo formulář. V service bych už nic nevalidoval maximálně občas vyhodil nějakou tu výjimku v důsledky jiné výjimky a nechal to probublat až k facadě nebo presenteru.
- Jiří Nápravník
- Člen | 710
frosty22: díky za upřesnění, popřemýšlím, ale listenery mi moc nesedí, i když tady se asi zrovna dobře uplatní. Nicméně moc to neřeší pořád ten můj problém, tenká fasáda a co tam patří, případně co je vůbec/co má být v té Service v pětivrstvém modelu:)
- frosty22
- Člen | 373
Děkuji vše za návrhy, zatím jsem z toho bohužel stále smutný :) Zkouším teď různé přístupy, ale ta architektura se mi stále nezdá tak cool. Ale myslím, si že je to stále pole nezoranné, a co jsem viděl mnoho projektů od různých programátorů, tak každý to řeší úplně jinak a někdy i třeba vůbec – vše v presenterech, takže třeba to i někomu dalšímu pomůže.
Zkusím pro příklad, jak to mám nyní, na triviální CRUD operaci:
Zadání: Kontakt má neomezeno kontaktních údajů (jednoduché 2 entity)
Entity:
- Contact – v podstatě jen settery, gettery
- Item – položka kontaktu, má typ a hodnotu (třeba telefon, email, atd.)
Formuláře (továrničky):
- ContactFormFactory – vytváří form pro kontakt + s možností přiřazení položek kontaktu
- ItemFormFactory – vytváří form pro přidání jedné položky kontaktu
Továrnička na entity:
- ContactFactory – metoda create přijímá parametry, které validuje a vytváří a vrací instanci entity Contact
- ItemFactory – stejné jako contact + hledá duplicitu, a vrací entitu Item
Fasáda:
- ContactFacade – má v tuto chvíli metody:
--- addItem(…) která volá ItemFormFactory, vytváří Item tu předává kontaktu u volá na DAO objektu pro Contact metodu save()
--- createContactFromForm(…) jelikož formulář z ContactFormFactory, již je složitější – vytváží spolu s Contact i několik Item, tak jsem toto delegoval do nové třídy CreateContactProcess, která tedy prochází ten formulář, následně volá jak ItemFormFactory, pro vytváření jednotlivých Item, tak i Contact
=========
Presenter tedy posílá zprávy na ContactFasade, která komunikuje
s továrničkama entit, a s DAO objektem pro ukládání.
Ale už nyní je tu jeden problém, co když chci updatovat kontakt, na to použít ItemFormFactory nemůžu protože to mi vrátí novou instanci, takže to nasetování na entitu, a validaci entity musím provádět znovu na jiném místě. Nebo by se factory, nechovalo jako továrnička, ale nějaká služba, která by v případě, že entita existuje jí nastavila nové parametry a vrátila ji, ale v tomhle případě bych musel při vytváření kontrolovat duplicitu jinde a nevyhazovat vyjímku.
No koukám jsem se sakra rozepsal, a pochybuji, že se to někomu bude chtít
číst =)
Ale je to zatím prostě magie :) optimální cesta nenalezena.
Editoval frosty22 (19. 11. 2013 12:52)
- Tomáš Jablonický
- Člen | 115
@frosty22 nejsou továrničky pro Entity tak nějak zbytečné?
co když chci updatovat kontakt, na to použít ItemFormFactory nemůžu protože to mi vrátí novou instanci, takže to nasetování na entitu, a validaci entity musím provádět znovu na jiném místě
Co vložit do té metody Entitu a nestarat se o to kde se vzala? Pak ji prostě uložit a Doctrine2 se už postará o to jestli ji mí insertnout nebo updatnout. Entitu bych asi vyvolal na úrovni presenteru a předal do Facade.
Status Entity se dá zjistit pomocí \Doctrine\ORM\UnitOfWork::getEntityState($entity) a vrací:
STATE_MANAGED 1
STATE_NEW 2
STATE_DETACHED 3
STATE_REMOVED
A pak si podle statusu můžeš i sám řídit co s tou entitou uděláš …
- arron
- Člen | 464
frosty22 wrote:
Ale už nyní je tu jeden problém, co když chci updatovat kontakt, na to použít ItemFormFactory nemůžu protože to mi vrátí novou instanci, takže to nasetování na entitu, a validaci entity musím provádět znovu na jiném místě. Nebo by se factory, nechovalo jako továrnička, ale nějaká služba, která by v případě, že entita existuje jí nastavila nové parametry a vrátila ji, ale v tomhle případě bych musel při vytváření kontrolovat duplicitu jinde a nevyhazovat vyjímku.
Nesnaž se udělat perfektní architekturu hned na poprvý. Když od sebe budeš mít jednotlivé části dobře oddělené, tak není problém to v případě nutnosti změnit.
Nevím, jestli je to optimální řešení (skoro bych řekl, že není), ale mohlo by to být jednoduché…udělej do té factory novou metodu, která si převezme id záznamu a vytáhne příslušný záznam z db a vrátí ho. A nebo tu factory uplně obejdi a udělej přesně tohle v té fasádě pomocí dao objektu. Hlavně bych to moc nekomplikoval, zkusil to nějak udělat, a když to nebude vyhovovat, tak se to upraví :-)
- frosty22
- Člen | 373
arron tak já se nesnažím na poprvé – v podstatě různé způbosby architektury se snažím už pře rok resp. snažím se u navrhovaných projektů nalézt ten svatý grál, kde si řeknu tohle je ono, přesně tohle mi chybělo. Ale u každé architektury narazím pak na něco, co se mi implementačně nelíbí a již to většinou nejde dobře řešit, tak mě napadlo zkusit se zeptat, jak to řeší ostatní, zda mi nějaká část nechybí, něco mě nenapadlo.
Ale así máš pravdu, že prostě ideální stav neexistuje a chce to stále refaktorovat dle nových požadavků.
jablon nemyslím si že továrničky jsou zbytečné, ale spíše tedy ASI by to nemusela být továrna, ale spíše rozšířit asi funkčnost na mapper, aby dokázala i přijmout instaci na settovat. Ikdyž zase na druhou stranu leč se editace a vytváření může zdát dosti podobné, tak ve finále je to podobné jen v nasettování proměnných a validaci, ale nasettování bych neviděl problém v duplikaci, to není v podstatě funkční kód, a validace zdelegovat jinam. Ale nevím se stavem entity by se taky asi dalo něco vymyslet, jak píšeš.
- Filip Procházka
- Moderator | 4668
Vždycky když jsem hrotil architekturu tak jsem na tom nechal třeba půl roku a bez výsledku.
Jednoduchý a ověřený postup: napiš 10 aplikaci a ono tě to trkne samo ;) Raději napiš víc tříd než míň a neboj se v průběhu je rozdělovat a spojovat. Dokud budeš dodržovat DI tak to půjde krásně.
A hlavně nevymýšlet univerzální řešení! Potřebuješ něco udělat? Napiš třídu, pojmenuj ji podle toho co dělá a neřeš nějaké facade a service.
- webdata
- Člen | 153
No nevím jestli nebudu za lamu, ale ja to dělám takto:
Mám vytvořený databázový model (tedy čistě tabulky v databázi a jejich vazby mezi sebou). Pak vytvořím třídy modelů pro každou tabulku. I s tím že třeba model produktu šahá do modelu kategorii (například).
Modely jsou jako service. Ty injektuju do presenteru ve kterém je potřebuji. Pokud model chce vyuzit funkce jineho modelu, tak je dostane pres konstruktor injektem. O tohle vše se mě stará DI.
Presenter je pak už jen čistě věc která vezme požadavek od uživatele, zavola patřičnou funkci v patricnem modelu a vygeneruje zpet vystup pres sablonu.
Takze kdyz chci treba v budoucnu dodelat do eshopu API pro export XML kategorii a produktu do jiného eshopu (XML feed pro odberatele – napr. dropshipping). Tak vytvorim presenter napriklad ApiPresenter kde dam actionCategoryXml a actionProductXml, injektnu potrebne modely, v tomto pripade produktu a kategorii. A pak to vypisu pres patricne sabloby.
Takze v tomto pripade pridam jen ApiPresenter + 2 latte.
Modely obsahuji vse co se da delat s daty v databazi.
Jinak vice parametru predavam pres pole. To je ten puvodni dotaz.
Nějaký model modulu jsem už zavrhnul, protoze vedsinou delam aplikace na zakazku, takze nejake to kopirovani tam je ale neni toho tolik. A věci jako je eshop (spíš doplnuji o nove funkce než abych z něj použil nějaké věci například na fotogalerii – prostě je to postavený jako eshop).
Snad muj prispevek neni mimo misu.
Editoval webdata (3. 1. 2014 0:59)
- akadlec
- Člen | 1326
Trochu se v mísím. Řeším neco podobného a zatím bohužel duplikací
kódu.
Mám entitu kde uchovává emailové adresy. Každý user jich může mít
vícero. Když někdo přidá novou adresu tak se ji založí entita a zároveň
se pošle validační email pro ověření adresy. Tento validační email se
pošle i na vyžádání usera.
Adresy přidávám jak v komponentě, kde je form pro přidání nové adresy tak i v presenteru editace účtu kde user zvolí primární mail a pokud je to nová adresa tak se pošle ověření a taky v procesu založení účtu. Takže aktuálně mám poslání validačního emailu na 4ech místech.
Jaký by tedy byl správný postup? Využít k tomu nějak Kdyby\Events? Udělat si nějakou service co bude řešit ono posílání emailu? Měla by řešit jen ono poslání nebo i třeba samotné uložení? (asi jen to poslání co?;))
- David Matějka
- Moderator | 6445
@akadlec: urcite si udelej sluzbu, ktera bude posilat overovaci emaily – tu budes volat rucne, kdyz uzivatel pozada o znovuzaslani. v pripade ukladani novyho mailu mas asi dve moznosti a je na tobe, jak to vyresis:
- fasada, ktera ulozi entitu a zavola metodu na te sluzbe
- entity listener, ktery pri ulozeni entity automaticky odesle email
- frosty22
- Člen | 373
@webdata: tak mimo mísu tolik nejsi, ale to co jsi popsal, to je mi víceméně jasné – tady řeším problematiku té části, kterou ty uvádíš jako „model“. V podstatě, to co jsi popsal vypadá, že máš nějakou GOD třídu, která zařizuje všechno v rámci dané „entity“ případně jejich vazeb, a ta ti s novými fukcemi roste, což určitě nechci dělat. (možná se mýlím, ale podle toho co uvádíš, jsem to tak odvodil).