Vzájemné vztahy objektů Article, ArticleMapper a ArticleRelation (pokus o DDD)

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

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
+
0
-

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 Mapperu. 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 SELECTem 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
+
0
-

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:

  1. Chápu dobře, že ArticleRelation je modelován jako potomek či kompozit třídy Article? 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í)?
  2. 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říde Article? 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
+
0
-

Rovnou k dotazům:

  1. Ano, jedná se o kompozit. Přidané metody pro Relation jsou getNext() a getPrevious(). Pokud bychom se na to měli koukat jako v tvém příkladu s lidmi, byla by tato metoda v DAO. 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 v DAO bychom tuto závislost museli řešit dříve než v šabloně. Pokud by se tedy změnil výpis, museli bychom zasahovat i do Presenteru. Takto stačí udělat o jedno volání metody getPrevious() (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 :-)
  2. 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řídy Article 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ž metoda getUserFullName(), 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
+
0
-

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
+
0
-

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 Mapperu. 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
+
0
-

@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.