RFC: Domain query pattern a různé implementace Repository rubrika: Programování: Jiné

9 Martin Mystik Jonáš
položil/-a 9.2.2014

Rád bych vaše názory na jednu architekturu nad kterou teď uvažuju, abych vyřešil klasický problém s příliš mnoha query metodami v Repository.

Uvedu příklad, co se snažím řešit.

Takže mějme nějakou repository, která obsahuje metody pro různé dotazy, které jsou pak volány z Controllerů a Services. Pro něj máme definované rozhraní:

  interface UserRepository {
    public function fetchById($id);
    public function fetchByName($name);
    public function fetchByEmail($email);
    // ...
  }

Volání dotazu vypadá takto:

  $this->userRepository->fetchByName($name);

Toto rozhraní má několik implementací (pro ukládání do databáze, pro načítání přes vzdálenou službu, testovací in-memory implementaci, ...)

  class UserRepositoryDatabase {
    public function fetchById($id) {
      // implementation for database
    }
    public function fetchByName($name) {
      // implementation for database
    }
    public function fetchByEmail($email) {
      // implementation for database
    }
    // ...
  }
  class UserRepositoryRemote {
    public function fetchById($id) {
      // implementation for remote service
    }
    public function fetchByName($name) {
      // implementation for remote service
    }
    public function fetchByEmail($email) {
      // implementation for remote service
    }
    // ...
  }

Tohle řešení funguje poměrně hezky, ale jen do doby než nám počet query metod začne bobtnat. Pak získáváme ohromné rozhraní s desítkami query metod.

Chtěl bych to vyřešit zavedením Query objektů.

Rozhraní Repository by se pak skládalo jen z:

  interface UserRepository {
    public function fetch(Query query);
    // ...
  }

A jednotlivé dotazy by byly představovány objekty implementujícími rozhraní Query

  interface Query {
  }
  interface FindUserByNameQuery {
    private $name;
    public __construct($name) {
      $this->name = $name;
    }
  }

Volání dotazu by pak vypadalo:

  $this->userRepository->query(new FindUserByNameQuery($name));

Tohle je pořád poměrně klasické řešení popsané v řadě zdrojů.

Pak ale přichází komplikace: Protože mám různé typy repository potřebuju, aby se stejný Query object v každé repository provedl jinak. V původním řešení to bylo jednoduché - každá repository měla vlastní implementaci query metody. Tady to ale musím vyřešit nějak jinak.

Prošel jsme nějaké články a zatím se mi jako nejlepší řešení jeví Query Object Handler (volně vycházející z řešení popsaného zde http://stackoverflow.com/a/14518534/1885777)

Tedy že by každá repocitory měla pro každý Query object vytvořenu vlastní třídu QueryHandler, představující implementaci spuštění daného dotazu. Samotný query object by byl jen přepravka na parametry pro tento handler.

Měl bych tedy například třídy:

  class FindUserByNameQueryDatabaseHandler {
    public function execute(FindUserByNameQuery $query) {
      // implementation for database
    }
  }
  class FindUserByNameQueryRemoteHandler {
    public function execute(FindUserByNameQuery $query) {
      // implementation for remote service
    }
  }

Pro každý doménový dotaz bych tak tedy musel vytvořit: 1) query object 2) implementace handlerů ve všech repository (což se příliš neliší od implementace všech query metod ve všech repository).


Vidíte v tomto řešení nějaké problémy? Vylepšili byste nějak tuto architekturu?

Komentáře

  • Taco : Proč se to jmenuje *Handler? 11.2.2014
  • Martin Mystik Jonáš : @Taco: To sem převzal z toho odkazovaného článku. V zásadě mi to ale přijde poměrně vhodné - je to prostě handler pro daný query v konkrétní repository. Jak bys to pojmenoval lépe? 19.2.2014
  • Taco : Mě handler přijde jako něco pověšeného (událostní handler například), nebo něco přidané k něčemu jinému. Zatímco ty se naopak snažíš o konkrétní implementenatci nějakého requestu, filtru, commandu, či jak to nazvat. (Oni se všechny jmenují execute, tak těžko tomu říkat rozhraní.) Nevím, zda bych to pojmenoval lépe. Navíc v angličtině nejsem příliš silný. Jen mi to neodpovídá, proto ta otázka. 19.2.2014
  • Martin Mystik Jonáš : Něco přidané k něčemu jinému je spíš háček "hook". IMHO handler má spíše význam něčeho co s něčím manipuluje nebo obsluhuje. Event handler je pak obsluha/zpracování události. Query handler je obsluha/zpracování query. 20.2.2014
odkaz
9 Augi
odpověděl/-a 17.2.2014

Dělám v .NETu a začal jsem to kdysi dělat přesně tak, jak je popsáno v té odpovědi na StackOverlow. Ve výsledku ale bylo docela pruda každé dotazování, protože člověk musel vytvořit Query objekt a nastavit mu properties, což docela šumělo. Tak mě napadlo, že bych to mohl automaticky konvertovat do interfaců s jedinou metodou, ale pak mi došlo, že vlastně ty interfaces pro queries můžu psát rovnou (tj. query interfaces nahradily query objekty).

Takže teď to vypadá tak, že IUserRepository má jen metody Add, Update, Remove, příp. ještě GetById (obecně dotaz podle primárního klíče). Všechny ostatní dotazy jsou pak schované za query interfaces, např.:

public interface IUsersByFirstNameQuery // no base interface!
{
  public IEnumerable<User> Execute(string firstName);
}

Query interfaces jsou pak často implementovány přímo ve stejné třídě jako IUserRepository.
Dále máme test konvencí, který ověřuje, že všechny queries mají metodu Execute, která vrací non-void (v zásadě ale není potřeba).

Když se na to dívám s odstupem, tak bych možná ještě uvažoval o původním řešení, přičemž bych ty Query třídy udělal immutable (properties by se nastavovaly přes ctor). Ale stejně bych preferoval mnou uvedené řešení, protože nevyžaduje tolik psaní (stačí nadeklarovat a implementovat interface, žádný opičárny okolo).

Komentáře

  • Honza Břešťan : Pak ale stejne musis rozsirovat s kazdou novou query ten repository o implementaci toho interface a hlidat, ze vsechny repository jednoho typu (jeden do DB, jeden k remote service,...) maji stejou mnozinu query interfacu, ne? Nemluve o tom, jestli pak ten konkretni repository vypada tak, ze ma 5 Execute overloadu vedle sebe (to v lepsim pripade, v horsim muze byt potreba explicit interface implementation) 18.2.2014
  • Augi : Nic nehlídám já, všechno se hlídá samo ;-) Nemáme více rovnocenných implementací, máme jednu "persistentní" implementaci a všechno ostatní jsou dekorátory. Všechno nám hlídá pár testů konvencí (které by šly jednoduše napsat i pro případ více rovnocenných implementací). Jak jsem psal, query interfaces jsou často implementovány přímo ve stejné třídě jako repository (btw. ano, vím že je to blbý název), ale není to pravidlem - typicky tam strkám jen queries podle přirozeného klíče. 18.2.2014
  • Martin Mystik Jonáš : V zásadě jde teda jen o to, že jsi oddělil interface jednotlivých query do samostatných interfaces? Tak ale zůstává problém s tím, že pak mám ohromně nabobtnalou implementaci té repository. 18.2.2014
  • Honza Břešťan : Muzou byt implementovane i stranou, ale pak je to zase spousta "mikrorepositaru" a bude potreba ve vsech resit data source atd. 18.2.2014
  • Augi : Tak ještě potřetí - query interfaces NEJSOU VŽDY implementovány přímo ve stejné třídě jako repository. Záleží to vždy na konkrétním případě, žádné exaktní pravidlo asi nemám. Problém s přehršlem drobných tříd nemáme. EDIT: Když nad tím přemýšlím, tak v zásadě se řídím SRP - dávám dohromady věci, které mají stejný důvod ke změně. 18.2.2014
  • Martin Mystik Jonáš : @Augi: Jak potom vypadá volání z kódu, který to používá? 19.2.2014
  • Augi : Myslíš volání nějaké query? Je to interface, takže si nechám nainjectovat "IUsersByFirstNameQuery" a pak zavolám "UsersByFirstNameQuery.Execute("blabla")". Žádnej magic :-) 19.2.2014
  • Martin Mystik Jonáš : @Augi: Ok chápu. Takže v zásadě tedy neinjectuješ repository, ale injectneš si vždycky jen ty query, které daná komponenta potřebuje. Takže pokud se rozhodneš refactorovat repository tak ji jen rozdělíš na části a o zbytek se postará nějakej autowire. 19.2.2014
  • Augi : Ano, přesně tak. Neříkám, že je to nejlepší řešení za všech situací, ale nám to docela funguje. 19.2.2014
  • Martin Mystik Jonáš : Jako řešení je to poměrně hezké. Jediné z čeho bych měl trochu obavy aby pak neměly některé služby potřebu mít injectnutých spoustu závislostí pro různé query. Ale je fakt že takové služby jsou už většinou samy o sobě kandidát na refactoring. 20.2.2014

Pro zobrazení všech 7 odpovědí se prosím přihlaste:

Rychlé přihlášení přes sociální sítě:

Nebo se přihlaste jménem a heslem:

Zadejte prosím svou e-mailovou adresu.
Zadejte své heslo.