Business logika v entitách

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
Jan Mikeš
Člen | 771
+
+1
-

Ahoj, řeším momentálně dilema, jakým způsobem budu přistupovat k entitám a kam budu schovávat business logiku projektu – entity nemusí být pouze hloupé přepravky na data (DTO), ale mohou být docela i chytré. Zajímaly by mě klady a zápory obou přístupů a případně co používáte vy, pravděpodobně bude nejlepší najít nějaký balance mezi obojím.

Pokusím se ukázat na příkladu (za účelem ukázky velmi zjednodušeném).

1. Chytré entity s logikou uvnitř

class User
{
	/** @OneToMany */ // cascade persist ...
	private $bans;

	public function ban($reason = NULL)
	{
		$ban = new Ban($this, $reason);
		$this->bans->add($ban);

		return $ban;
	}
}

class Ban
{
	private $user;
	private $reason;

	public function __construct(User $user, $reason = NULL)
	{
		$this->user = $user;
		$this->reason = $reason;
	}
}

Use case:

	// Např v presenteru
	public function handleBan($userId)
	{
		$user = $this->userRepository->find($userId);

		$user->ban();

		$this->em->persist($user);
		$this->em->flush();
	}

2. Mít logiku pouze ve službách a omezit entity pouze na „hloupé“ datové objekty

class User
{
	/** @OneToMany */
	private $bans;

	public function addBan(Ban $ban)
	{
		$this->bans->add($ban);
	}
}


class Ban
{
	private $user;
	private $reason;

	public function __construct(User $user, $reason = NULL)
	{
		$this->user = $user;
		$this->reason = $reason;

		$user->addBan($this);
	}
}


class BanService
{
	private $em;

	public function __construct(EntityManager $em)
	{
		$this->em = $em;
	}

	public function banUser(User $user, $reason = NULL)
	{
		$ban = new Ban($user, $reason);

		$this->em->persist($ban);
		$this->em->flush();
	}
}

Use case:

	// Např v presenteru
	public function handleBan($userId)
	{
		$user = $this->userRepository->find($userId);

		$this->banService->banUser($user);
	}

Osobně se mi zamlouvá více 1. přístup, ale obávám se toho, že u větších aplikací entity (právě např. User) mohou narůstat do nekontrolovatelných rozměrů.

newPOPE
Člen | 648
+
0
-

Sam pises, ze pristup 1. sa ti pozdava viac. Ano on aj dokaze byt lepsim/pouzitelnejsim/chytrejsim. S tymi nekontrolovatelnymi rozmermi mas pravdu moze sa to tam dostat ale len za podmienky, ze to tam pustis :).

Skus si precitat nieco o bounded contextoch tam najdes odpoved kde postavit hranice tak aby tie rozmery boli rozumne.

Pavel Kravčík
Člen | 1180
+
0
-

My jsme tu hranici nastavili na hloupý ref, related a případnou úpravou dat. Nikdy tam není ukládání, to by neměla být zodpovědnost entity.

$klientEntity = $smlouvaEntity->fetchKlientEntity(); //$activeRow->ref('klient', 'klient_id') (to samé na kolekce)

$klientEntity->getFullName() //return $klientEntity->jmeno . ' ' . $klientEntity->prijmeni;

A používáme to jen pro snadnější výpis v latte.

newPOPE
Člen | 648
+
0
-

Ukladanie si myslim ze je v poriadku. Aj ked s Nette\DB sa to trochu horsie riesi (oproti Doctrine). Da sa tam ist cestou Aplikačnej služby.

	$klientEntity->getFullName()

Tato metoda podla mna v Entite moc co hladat nema nakolko je to uz mimo Model. Jedna sa skor o ViewModel pripadne na to pouzit nejaky Formatter.

Editoval newPOPE (24. 8. 2016 13:53)

hitzoR
Člen | 51
+
+1
-

newPOPE napsal(a):

Ukladanie si myslim ze je v poriadku. Aj ked s Nette\DB sa to trochu horsie riesi (oproti Doctrine). Da sa tam ist cestou Aplikačnej služby.

	$klientEntity->getFullName()

Tato metoda podla mna v Entite moc co hladat nema nakolko je to uz mimo Model. Jedna sa skor o ViewModel pripadne na to pouzit nejaky Formatter.

Já bych zas naopak řekl, že tohle by právě mělo být v entitě. Aspoň z mojeho pohledu to dává největší smysl. Je to jako kdyby ses jmenoval Petr Novák. Když se tě někdo zeptá, jak se jmenuješ, tak mu odpovíš jak? „Moje jméno je Petr a moje příjmení je Novák“ nebo „jmenuju se Petr Novák?“ Navíc je to property, která se při vypisování bude vypisovat vždycky úplně stejně a žádný jiný tvar tam řešit nebudeš.

To, co popisuješ ty, by mělo platit spíš třeba u výpisu data, kde se ti na různých místech aplikace může vypisovat různě. Jednou třeba jen datum bez roku, jinde datum, rok i čas apod.

Pavel Kravčík
Člen | 1180
+
0
-

Máte pravdu oba, ale přijde mi zbytečné dělat filtr v Latte pro každou entitu. Mnohem pohodlnější je, když si tyhle „nesmysly“ hlídá entita. V entitě by se mi například už nelíbilo tohle: „CONCAT(jmeno, přijmeni)“, ale každý tu hranici může mít nastavenou jinak.

Za definici entity považuju jen lehce chytřejší ActiveRow. Neměl by se starat o žádnou bussiness logiku, jen nabídnout nějaké sugar věci a jednoduchou kontrolu vstupních dat.

jiri.pudil
Nette Blogger | 1028
+
+4
-

@Lexi na prvním přístupu – budu mluvit k tvému konkrétnímu příkladu, ale platí to obecně – je super, že User se sám stará o svoje Bany a vnitřní konzistenci a navenek jen vystaví veřejné API (třeba ban(), unban() a isBanned()), které je jednoduché, striktní v tom, co ti dovolí změnit, a do značné míry samopopisné. Z celé entity Ban se stává implementační detail, o kterém zvenku vůbec nepotřebuješ vědět, a vlastně jsi tím stvořil takový malinkatý agregát :)

Svaťa Šimara
Člen | 98
+
0
-

@Lexi Prosím nejdřív si ujasni, jaký jazyk a z jakého důvodu použiješ. Stejně tak se zamysli, jaké paradigma a z jakého důvodu použiješ.

Vypadá to, že sis vybral PHP a OOP. Jednou ze základních vlastností OOP je zapouzdření dat chováním.

V ukázce číslo 1 to tak je.

V ukázce číslo 2 přímo zapouzdření porušuješ, a i deklaruješ, že takto je ukázka koncipovaná. Pro tento styl programování bych rozhodně použil jiné paradigma, než OOP. Navíc jde vidět, že OOP v tomto případě jenom háže klacky pod nohy. Přitom by stačily struktury a funkce, a bylo by.

jiri.pudil
Nette Blogger | 1028
+
0
-

V ukázce číslo 2 přímo zapouzdření porušuješ

@Fafin Čím přesně? Když se bavíme abstraktně, bavme se abstraktně: zapouzdřením se v OOP rozumí toliko to, že objekt schová před vnějším světem svůj vnitřní stav (atributy) a případně ho umožní modifikovat pomocí veřejného rozhraní (metod). V ukázce číslo 2 nevidím nic, co by tohle porušovalo. Pravda, to rozhraní je ve srování s první ukázkou dosti triviální (a anemické), ale z pohledu OOP zcela validní

Svaťa Šimara
Člen | 98
+
0
-

@jiri.pudil Gettery a settery jsou zajímavou ukázkou porušení zapouzdření. Objekty zapouzdřují data chováním. Get a Set metody nejsou žádné chování, chování je „vygeneruj“, „pošli“, „spočítej“, nikoliv „nastav/přečti jeden atribut“. Navíc jejich používání poukazuje na nezralost návrhu.

jiri.pudil
Nette Blogger | 1028
+
0
-

Jasně, už chápu, kam tím míříš. Můj argument byl ryze akademický, na té praktičtější úrovni souhlasím, že gettery a především settery nejsou úplně nejšťastnější volba a vždycky se dá kód navrhnout tak, aby je nepoužíval. Ovšem i v praxi mají svůj use case – extrémním jsou value objecty, kde gettery a „settery“ (uvozovky protože immutability) imo zapouzdření neporušují, protože to je typ objektu, od kterého ani chování neočekáváš.

Svaťa Šimara
Člen | 98
+
0
-

… settery …vždycky se dá kód navrhnout tak, aby je nepoužíval…

@jiri.pudil Stačí, aby návrh modelu (a kódu) vycházel z případů užití. Případ užití je „stornovat objednávku“, a nikoliv „změnit stav objednávky“. Jak prosté :-)

Value object chápu jako součást doménové vrstvy, kde i on může mít svou logiku ač nemění svůj stav, proto se dál budu raději bavit o DTO. Bohužel narážím na to, že v systému mám spoustu přepravek, které mají jenom get metody, a to mi taky nepřijde objektové…

Projistotu příklad value objectu s chováním – nalezená cesta mezi 2 uzly v binárním stromě – Route. Route je definovaná těmito dvěmi uzly, nikoliv svou identitou, proto je value object. Ale Route může mít metodu „spočítej vzdálenost“.

fizzy
Backer | 49
+
0
-

Fafin napsal(a):

@jiri.pudil Stačí, aby návrh modelu (a kódu) vycházel z případů užití. Případ užití je „stornovat objednávku“, a nikoliv „změnit stav objednávky“. Jak prosté :-)".

a co znovupouzitelnost kodu? ked na jednom z X projektov bude logika stornovania objednavky rozdielna? :)

Svaťa Šimara
Člen | 98
+
0
-

@fizzy Jakože použít kód, který je stavěný na určité případy užití pro projekt, který těmto případům užití neodpovídá? To je nesmysl.

Editoval Fafin (25. 8. 2016 16:09)

fizzy
Backer | 49
+
0
-

Fafin napsal(a):

@fizzy Jakože použít kód, který je stavěný na určité případy užití pro projekt, který těmto případům užití neodpovídá? To je nesmysl.

mame 15 custom eshopov, vsetky zdielaju nejaky spolocny zaklad a lisia sa v par veciach, logiku v entitach mam minimalnu, ked je nejaka specialna poziadavka od klientov tak sa nahradi v DI jedna sluzba a je vyriesene, pri spominanom sposobe by som musel extendnut celu entitu a nahradit nejaku metodu + upravit mapovanie. Je nerealne udrziavat kazdy projekt samostatne, preto tvrdim, ze zlozitejsiu logiku mat riesenu v sluzbach, ktore sa daju jednoducho nahradit.

newPOPE
Člen | 648
+
0
-

@fizzy lenze zlozitejsia logika v tych „sluzbach“ (aplikacne sluzby, fasady, …) aj je. Tu skor ide o logiku typu $entity->disable() namiesto $entity->set....

To ze mas x custom veci je fajn, lenze cim viac sa dany produkt takto customizuje tak tym viac musis ist do vacsej miere abstrakcie. Ono na jednu stranu je to dobre na druhu nie. Fakt zalezi od typu projektu/produktu.

Ono aj @Fafin vybral nestastny priklad lebo akonahle mam tych statusov povedzme 10 mam napisat 10 metod do danej entity? To velmi nedava zmysel. Tam sa viac hodi nieco ->switchStatus() ked uz tam nema byt ->set...

Svaťa Šimara
Člen | 98
+
+2
-

newPOPE napsal(a):

Ono aj @Fafin vybral nestastny priklad lebo akonahle mam tych statusov povedzme 10 mam napisat 10 metod do danej entity? To velmi nedava zmysel. Tam sa viac hodi nieco ->switchStatus() ked uz tam nema byt ->set...

@newPOPE Děkuji za komentář, a jsem rád, že považuješ daný příklad za nešťastný.

Situace – zákazník si na e-shopu objedná produkty, tím vzniká objednávka (pro Vás objednávka ve stavu „new“). Hodně zákazníků si ale objednává po e-mailu/telefonu, kde se dospecifikovává, co vlastně všechno potřebují – tím vzniká v systému poptávka (pro Vás objednávka ve stavu „pre-new“). V rámci poptávky je možné měnit úplně všechno – položky, občas dobří zákazníci dohadujou lepší ceny. Jakmile se dohodnou, poptávka zaniká a vzniká objednávka.
Objednávka se ihnez zobrazuje lidem ve skladu, kde ji musí zkompletovat. Ke kompletaci používají čtečky čárových kódů a v momentě, kdy mají všechny položky načteny, předávají krabici s kompletním zbožím do expedice. Tímto se z objednávky stává zkompletované zboží (pro Vás stav „goods-ready“). V expedici zabalí krabici, vytisknou si štítek pro daného přepravce a zásilku připraví na svoz. V momentě tisknutí štítku se ze zkompletovaného zboží stává zásilka (pro Vás stav „to-delivery“).

Toto je jenom proces vyskladnění a připravení k odeslání, neřeším tady storna a reklamace (které vracejí peníze a věrnostní body).

Pokud tento systém namodeluji objektově (hodně, hodně zjednodušeně):

$request = new Request(...);
$request->addItem(...);
...
$order = $request->order();
$readyGoods = $order->complete();
$delivery = $readyGoods->sendVia($delieryCompany);

V rámci tohoto modelování lze na každé úrovni k objektům přistupovat různě – poptávku lze libovolně měnit, objednávka může blokovat zboží, nová hotová zásilka může způsobit doobjednání dalšího svozu přepravní společnosti.

Jak zajistíte tyto procesy pomocí ->switchStatus()? Co se bude dít, když změním stav z „to-delivery“ na „new“?

Jak bude vypadat Vaše entita objednávky? Bude v ní napatlané vše od úvodních komunikací v rámci poptávky, informace o všech položkách přes číslo objednávky, identifikace skladníka, čárových kódů jednotlivých položek ze skladu, informaci o blokacích, až po identifikaci přepravce a čísla zásilky?

newPOPE
Člen | 648
+
0
-

@Fafin ono tieto debaty vacsinou nemaju konca nakolko nic nie je silver bullet. Ako som spominal zalezi od poziadaviek na system.

K tvojmu prikladu len ze popisujes bounded contexty kde si kazdy „objednavku“ riesi po svojom cize by ti z toho vysla nie entita objednavka ale povedzme 3 entity napr. PreOrder, Order, Delivery…

Netvrdim ze entita ma byt povolna kludne si svoj stav moze strazit len nema zasahovat mimo kontext svojho posobenia. Takisto suhlasim s tym nepouzivat settery.

Svaťa Šimara
Člen | 98
+
0
-

@newPOPE Neformální požadavky jsem snad stanovil (a taky mi to dalo práci :-) ), ano nejsou rozebrané do use-casů.

Nechtěl jsem v tomto případě ukázat stříbrnou kulku, chtěl jsem ukázat, že změna stavu není nešťastný příklad – pro dané požadavky. Taky jsem chtěl okatě naznačit, že jde o oddělené koncepty, konec konců by mohlo být zainteresováno více oddělených systémů.

Ale přijde mi, že stříbrná kulka existuje: požadavky → případy užití → modelování → programování ;-)

fizzy
Backer | 49
+
0
-

@Fafin vdaka za use case :)

Este k tym setterom, ako potom riesite nejake zmeny, napriklad ako by ste riesili zmenu poctu produktu v objednavke, ked settery su zlo? odobrali produkt z objednavky a vlozili novy s pozadovanym poctom?

Vedeli by ste mi odporucit nejake materialy k tejto problematike? Rad by som sa nieco nove dozvedel :)

newPOPE
Člen | 648
+
0
-

@Fafin rozumiem. Lenze dost casto sa stava hlavne u novycj projektov ze sa poziadavky menia velmi rychlo a na veci z vodopadoveho modelu kazdy kasle…

@fizzy za mna toto (je to tak nq 1 den citania) https://leanpub.com/ddd-in-php. Su aj pokrocilejsie literatury ale skor vseobecn. Hladaj DDD.

Svaťa Šimara
Člen | 98
+
0
-

@newPOPE Ano, požadavky se velmi mění, proč? Zjišťují se od zákazníka jeho opravdové potřeby? Zjišťuje, jakým způsobem se softwarem potřebuje pracovat?

Dělal jsem na jednom mini projektu, kde se toto dělo – analýza potřeb, požadavků, use-casů trvala opravdu dlouho, implementace byla za chvilku hotová. Proč? Protože jsme věděli, co zákazník potřebuje, čeho chce dosáhnout. Kupodivu se potom už požadavky neměnily. A pokud se někdy změní, bude to prostě další iterace související s remodelováním – přirozený proces.

Ok, u mini projektu to jde, co u velkého projektu? Tam jsem zatím vždycky narazil. Ale pokud by se podařilo ze zákazníka vytáhnout jeho opravdovou potřebu, bavilo by se s ním jeho jazykem, dodávaly by se pravidelně malé kousky systému, věřím, že remodelování bude vždycky, ale nemuselo by být přímo na úrovni koncepce, ale na úrovni komponent. Jednou to chci zažít :-D

@fizzy Jak píše tady newPOPE, jenom doplním o odkazy:
Domain-driven design od Evanse. Nedávno vydal jeho kolega Vernon užší knížku, kterou jsem ale ještě nečetl: DDDD . Mimo jiné existuje od Vernona Implementing DDD , a ta pěkně doplňuje DDD o implementační detaily, které nejsou vůbec, ale vůbec zřejmé.