Vzájemné vztahy objektů Article, ArticleMapper a ArticleRelation (pokus o DDD)
- tomas.lang
- Člen | 53
Dobrý den,
řeším následující problém:
Mám entitu řekněme Article
která má svoje následovníky a
předchůdce – kde by jste logičtěji hledali kód pro přístup
k následovníkům a předchůdcům – přímo v entitě
Article
a nebo v nějakém objektu např.
ArticleRelation
? (ArticleIterator
nepředpokládám,
neboť se chci na konkrétní Article
ptát, nikoliv se k němu
proiterovávat) – nebo to řešit přímo na ArticleMapperu
?
Možná ještě jinak – viděli by jste to jako správný přístup?
<?php
$article = $articleMapper->get($articleId);
$prevArticle = ...
?>
a tady už mám problém, neboť buď bude ArticleRelation
součástí ArticleMapperu
a budu volat
$articleMapper->getPrev($article);
a nebo něco ve
stylu (new ArticleRelation($articleMapper))->getPrev($article).
De-facto se mi ale pořádně nezdá ani jeden způsob – určitě bych v tomto ocenil pomoc někoho zkušenějšího – teď mi z toho jde akorát hlava kolem… :-/
- stekycz
- Člen | 152
Trochu mi chybí informace, co má každá z těch tří tříd za odpovědnosti. Osobně bych to navrhl asi nějak takto na první dobrou.
DAO
objekt by měl řešit veškerou komunikaci, která řeším
objekty daného typu. Tedy asi to, co je v kódu jako Mapper
.
Mapper
by měl mít jen schopnost mapovat databázové záznamy
do objektu a ne je načítat.
Relation
a Article
jsou samy o sobě zodpovědně
za svoje načtení, ale na přesné mapování by se měly vždy zeptat
Mapper
u. Respektive Relation jde ještě dál a deleguje toto
načítání na Article
.
Nicméně otázkou také je, k jakému účelu chceš předchozí (resp.
následující) článek. Pokud potřebuješ jen ID, tak bych si udělal přímo
v DAO
metodu s jednoduchým SELECT
em přímo do
databáze, případně ID předchozího (resp. následujícího) článku
přidal přímo do třídy Article
.
Čistě objektově bez jakéhokoli SQL by to mělo být (IMHO) tak, že
Relation
umí vrátit předchozí (resp. následující) článek.
Pokud je tahle funkcionalita často potřeba, mohlo by to být rovnou jen v
Article
, ale objektová čistota by zase trochu klesla.
Samozřejmě taková metoda by musela vracet instanci stejné třídy. Tak by se
však měla načítat až v případě volání metody
getPrevious()
. Příklad kódu by asi pomohl:
// Čistě
$article = $articleDao->get($articleId); // ArticleRelation
$previousArticle = $article->getPrevious(); // ArticleRelation
// Méně tříd
$article = $articleDao->get($articleId); // Article
$previousArticle = $article->getPrevious(); // Article
Bohužel se nedá udělat dokonalý objektový návrh, který současně bude perfektně výkonný. Záleží na konkrétním projektu a preferencích, zda jít cestou více objektové čistoty nebo lepšího výkonu.
- tomas.lang
- Člen | 53
stekycz: Děkuji za obšírnější odpověd :-)
Každopádně mám ještě pár dotazů jestli by to nevadilo…
Pokud bych se zaměřil na ten čistý návrh, tak:
- Chápu dobře, že
ArticleRelation
je modelován jako potomek či kompozit třídyArticle
? Já osobně jsem se na něj totiž díval více jako na samostatnou třídu (příklad: Mám entitu Člověk a chci zjistit jestli má dítě – předám tedy informace o entitě Člověk entitě Matrika která mi to zodpoví)? - Dále jsem moc nepochopil kam se v daném návrhu poděl
ArticleMapper
, lépeřečeno jak je využit ve třídeArticle
? Lépeřečeno kdo tedy nakonec sahá pro surová data (např. do databáze)?
Protože jestliže to dobře chápu, pak ten DAO objekt je taková service
pro usnadnění práce, pro data si sahá Article
a sám si je
formátuje pomocí ArticleMapperu
– což mi připadá docela
zvrácené? – ale dost dobře možná jsem to špatně pochopil… :-)
- stekycz
- Člen | 152
Rovnou k dotazům:
- Ano, jedná se o kompozit. Přidané metody pro
Relation
jsougetNext()
agetPrevious()
. Pokud bychom se na to měli koukat jako v tvém příkladu s lidmi, byla by tato metoda vDAO
. Je to také možné, ale takto postavený kompozit nám ale umožní projít (sice neefektivně z pohledu SQL) jakékoli množství předchozích (resp. následujících) článků. V případě, že by ta metoda byla vDAO
bychom tuto závislost museli řešit dříve než v šabloně. Pokud by se tedy změnil výpis, museli bychom zasahovat i doPresenteru
. Takto stačí udělat o jedno volání metodygetPrevious()
(resp.getNext()
) více. V příkladu lidí je opět jednodušší se ptát přímo člověka, jaké má děti. A těch případně zase na jejich děti :-) Mapper
může být z mého pohledu použit dvěma způsoby. Může pouze obsahovat mapování názvů atributů třídyArticle
na názvy sloupců v databázi, které jsou do SQL dotazu dále seskládány dle potřeby. Druhou možností je, že obsahuje samotné SQL dotazy, které volá na konkrétní databázové vrstvě (aktualizoval jsem diagram). Druhá možnost se může při více dotazech hůře spravovat. V každém případě třída Article by podle mě měla mít na starost jen a pouze práci s atributy, volání metod na Mapperu a případně by měla implementovat metody, které nemají co do činění s databází. Jako příklad budiž metodagetUserFullName()
, která vrací jméno i příjmení uživatele (autora článku), přičemž třída obsahuje jen atributy křestní jméno a příjmení. Další podobné „výpočetní“ metody si asi každý domyslí :-)
Na závěr musím souhlasit, že DAO
je vlastně služba pro
danou doménu. Z výše napsaného by mělo plynout, že pro data do databáze
sahá Mapper
, který je po načtení dat nastaví hodnoty
atributům třídy Article
podle mapování, které zná jen
Mapper
.
PS: Ještě mě napadá, že Mapper
by klidně mohl jen
nastavovat správné aliasy sloupečkům z databáze, aby si to
Article
po načtení správně rozestrkal po atributech.
Mapper
by se tak stal zdánlivě nepoužitý. Přesto ale
zastávám výše uvedený postup, protože s ním má každá třída jednu
zodpovědnost. Třída Article
by totiž neměla mít
zodpovědnost za načítání data a zároveň za
jejich uchování v paměti, ukládání změn a případně další dopočty
s nimi. Stačí když má zodpovědnost jen za uchování v paměti a
paměťové operace s atributy.
- tomas.lang
- Člen | 53
Ahoj, děkuji za další pěkné vysvětlení :-)
Řeším ještě nějaké otázky, ale jedna zásadně převládá:
Z hlediska logiky věci, má vůbec Domain Object
vědět
o tom že nějaký Data Mapper
existuje? De facto neměl by Mapper
mít možnost přímo Object naplnit?
Jenže pak by asi ještě musel vzknitou třetí objekt, který by takovou transakci umozňoval a zároveň by si Object mohl nechat své proměnné skryté, např.:
<?php
$articleData = $articleMapper->get(1); // ArticleData
$article = new Article;
$article->load($articleData);
/** za techto podminek prepokladam ze kod ArticleDAO by pak mohl vypada nasledovne?
class ArticleDAO
{
protected $articleMapper;
public function get($id)
{
$articleData = $articleMapper->get(1); // ArticleData
$article = new Article;
$article->load($articleData);
return $article;
}
}
$articleDao->get($articleId);
?>
Tedy ArticleMapper
se pouze stará o načítání a ukládání
dat z objektu ArticleData
, a objekt Article
se umí
na základě těchto dat naplnit a umí je i vydat.. – jenže to celé celé
zbytečně složité, a takřka říct kanón na vrabce? – možná se v tom
snažím jen najít něco příliš složitého, nevím…
- stekycz
- Člen | 152
V zásadě je to správně, podle mého. A souhlasím i s tím, že je to kanón na vrabce v případě, že chceš jen a pouze načítat články, případně jejich sousedy. Ovšem jakmile to začneš používat ve větší aplikaci, kde jednotlivé domény mohou jakkoli souviset, začne to být takhle jednodušší pro další správu. Zkrátka a dobře, pokud si budu dělat „blog“ s výpisem článků, které budu zadávat přímo do databáze, vystačím si s dibi nebo \Nette\Database bez dalších objektů kolem.
Dovolím si jen drobně upravit tvoji ukázku kódu:
class ArticleDAO
{
private $articleMapper;
public function __construct($articleMapper)
{
$this->articleMapper = $articleMapper;
}
public function get($id)
{
$article = new Article($this->articleMapper);
$article->setId($id);
$article->load();
return $article;
}
}
class Article
{
private $articleMapper;
// Atributy - ať už jednotlivě nebo v poli
public function __construct($articleMapper)
{
$this->articleMapper = $articleMapper;
}
public function load()
{
// Tady načítám data z Mapperu a vkládám do atributů třídy
// Např.:
$this->articleMapper->load($this); // Díky Setterům naplní data, díky Getterům ví jaký řádek vybrat
}
// Settery a Gettery na atributy
}
$articleDao->get($articleId);
Article
chce vždy nějaká data, i kdyby to mělo být
prázdné pole. Navíc Mapper
chce použít nejen při
načítání, ale i při ukládání. Tudíž by měla mít každá instance
referenci na instanci Mapper
u. Navíc použitím DI pro DAO
zajistíme, že v jednom systému načítáme z databáze (ať už je tam
jakákoli knihovna) nebo klidně i ze souboru, případně webové služby.
Když se na to tak dívám, je to trochu podobné továrničkám :-)
- hAssassin
- Člen | 293
@stekycz > ahoj, zajimava myslenka mit metodu
load()
primo v Article
(entite) a predavat ji mapper.
Mel bych dotaz (spis teoreticky): pokud vezmeme v potaz ten priklad
s predchudcem a naslednikem, tak dejme tomu, ze v tabulce article
v DB jsou sloupecky succ_id
(pro naslednika) a pre_id
(pro predchudce). Dejme tomu ze to jsou cizi klice do stejny tabulky a ze muzou
byt NULL
(pokud je prvni a/nebo posledni v seznamu). Oba tyto
klice vraci mapper do Article v load a ta si je nekam ulozi. Tak a ted bych
chtel videt metody getNext()
a getPrev()
. Predpokladam
ze by logicky mely byt ve tride Article
a idealni by bylo kdyby
vraceli opet instanci Article
nebo NULL
(pokud je
prvni nebo posledni) aby bylo v presenteru mozny volat:
$article = $articleDao->get($articleId);
$next = $article->getNext();
if($next) {
...
}
Jak by to vypadalo? Diky.