Implementace DTO v CzechIdM

V tomto příspěvku se budeme zabývat návrhem některých datových struktur, kterými implementujeme DTO v našem Identity Manageru CzechIdM. Nebudeme se příliš věnovat samotnému kódu, raději se podíváme na problém s větším odstupem. Ukážeme si, jak jsme v našem případě navrhli jednoduchou hierarchii tříd, která nám umožní spravovat i velmi komplikovaná data. Vše si budeme na závěr ilustrovat na naší třídě UserView, která poskytuje informace o jedné konkrétní identitě v našem CzechIdM.

Co znamená DTO?

DTO je návrhový vzor, tedy jakýsi obecný model používaný v různých konkrétních situacích. Zkratka DTO vznikla z termínu Data Transfer Object a značí strukturu v datové vrstvě, která stojí mezi aplikací samotnou a datovým úložištěm. Aplikační vrstva tedy nevykonává operace přímo nad datovým úložištěm, ale nad DTO objekty, které jí poskytuje datová vrstva. Pro chod celého programu je to vhodnější: jsou jasně vymezeny třídy oprávněné k zásahům do datového podkladu, a zabrání se tak případné nekonzistenci dat.

Spojení s datovým úložištěm

CzechIdM používá pro uchování všech informací klasickou relační databázi. Pro načítání dat a jejich opětovné ukládání proto využíváme Java framework Hibernate, který umožňuje objekty přímo mapovat na jednotlivé řádky. My se v tomto článku nebudeme zabývat technologií Hibernate ani samotným procesem načítání a ukládání. Zaměříme se raději na problém, jak správně a efektivně šířit změny způsobené aplikační vrstvou zpět do databáze.

Třída s informaci o změnách

Náš Identity Manager CzechIdM si uchovává informace o různých subjektech: o identitách, o jejich účtech na jednotlivých koncových systémech, o organizační struktuře a o mnohém dalším. Pokud v CzechIdM dojde ke změně v reálných datech (například ke změně příjmení ženy, která se vdala), musí náš Identity Manager být schopen změnu rozpoznat a správně ji propagovat do všech koncových systémů, které s daty pracují, a do svého interního datového úložiště. Potřebujeme tedy datovou strukturu, která kromě dat uchovává i informace o všech změnách.

Vytvoříme si tedy interface DTOModifiable<T> s jedinou metodou T getModification(). Pokud chce klientská aplikace pracovat se všemi datovými objekty, které si pamatují své změny, může k instancím přistupovat právě přes tento interface. Typ T značí informaci o změnách.

public interface DTOModifiable<T> {
    public T getModification();
}

Naše implementace DTO

Chceme, aby si naše UserView drželo informace ve tvaru „klíč – hodnota“, například „příjmení – Novák“ nebo „email – novak@firma.com“. Vytvoříme si tedy o něco specifičtější interface, nazveme ho přímo DTO. Bude rozšířením interfaců DTOModifiable<T> a Map<String, Object>. Pracujeme-li s tímto rozhraním, můžeme už předpokládat, že jsou data uchovávána jako dvojice a přes klíče můžeme přistupovat k hodnotám. Naše UserView je proto implementací rozhraní DTO.

public interface DTO extends Map<String,Serializable>, DTOModifiable<Map<String,DTOModification>> {
    public Object get(String key);
    public void put(String key, Serializable value);
    ...
}

Pro rozhraní DTO si vytvoříme ještě jednu pomocnou třídu, která bude reprezentovat změnu hodnoty jednoho konkrétního klíče. Budeme ji nazývat DTOModification a můžeme ji chápat jako trojici hodnot: starou hodnotu příslušnou klíči, novou hodnotu příslušnou klíči a typ změny, který může nabývat jen tří hodnot: ADD, REMOVE a CHANGE podle povahy změny. Zavoláme-li tedy metodu getModification() na DTO, dostaneme Map<String, DTOModification>, která zachycuje změny pro jednotlivé klíče.

Často si však situace žádá mnohem složitějších struktur. V našem UserView budeme například potřebovat takzvané „extended“ atributy, které rozšiřují „povinné“ informace o uživateli o další „volitelné“ údaje, které se mohou lišit například při různých nasazeních CzechIdM. Hodilo by se nám, aby byly extended atributy také uchovávány jako dvojice „klíč – hodnota“, ale aby všechny extended atributy byly jako celek jedinou položkou pod jediným klíčem. Zároveň aby se případná změna extended atributu propagovala jako změna celého UserView.

Chceme zkrátka zajistit, abychom mohli jednotlivé instance naší třídy zanořovat jednu do druhé a aby se zanořená instance chovala jako část nadřazené instance a změny v zanořené instanci byly chápány jako změny instance nadřazené. Tato vlastnost nám pak umožňuje organizovat informace do poměrně složitých grafů, v nichž jsou vrcholy tvořeny našimi DTO a hrany mezi vrcholy znamenají, že jedno DTO je vnořeno do druhého. Dojde-li ke změně v jednom DTO, projeví se jako CHANGE modifikace ve všech DTO rodičovských. Zavolá-li klientská třída metodu getModification() na rodičovské třídě, projdou se postupně všechny uložené hodnoty. Je-li některá z hodnot opět instancí DTO, zavolá se na ní rekurzivně metoda getModification() a je-li její výstup neprázdný, zařadí se do výsledku CHANGE modifikace pro příslušný klíč.

Od rozhraní ke konkrétní třídě

Dostáváme se k naší konkrétní třídě DTOGroup, která implementuje DTO pomocí hešovací mapy. Kromě technické implementace nepřináší oproti DTO nic nového, což je z hlediska objektového návrhu správně. Kdybychom se totiž v budoucnu rozhodli, že nechceme používat hešovací mapy, ale třeba mapy provázané spojovými seznamy, můžeme poměrně snadno vytvořit jinou konkrétní implementaci DTO, aniž bychom museli sahat do klientských tříd v aplikační vrstvě.

Možná si kladete otázku, proč jsme na začátku vytvořili rozhraní DTOModifiable<T>, když jsme ho nakonec jen rozšířili na DTO. V našem CzechIdM totiž pracujeme často také s daty, která nemají tvar mapy jako DTO. Museli jsme proto vytvořit například třídu DTOList, která sice uchovává data v jiném tvaru, ale stále potřebuje uchovávat informaci o změnách.

 

Schéma tříd našeho DTOPohled na identitu

Podívejme se na závěr na strukturu našeho UserView. Ono samotné dědí od DTOGroup, k informacím o konkrétní identitě lze přistupovat přes klíče. Zatímco pro klíče „name“ nebo „email“ je hodnotou obyčejný řetězec, pro klíče „accounts“ nebo „attributes“ je situace komplikovanější. Hodnotou jsou totiž další DTOGroup. V případě klíče „accounts“ dostaneme DTOGroup, v níž jsou klíči jména jednotlivých resource, na kterých má uživatel účty. Hodnotami jsou další DTOGroup, tentokrát jsou klíči jména schémat na daném resource. Teprve hodnoty v této tabulce nesou informace o účtech uživatele.

Závěr

V článku jsme se podívali na konkrétní implementaci návrhového vzoru DTO, kterou jsme použili v našem Identity Manageru CzechIdM. Ukázali jsme, jak lze pomocí několika jednoduchých vlastností navrhnout obecný systém tříd, který je schopen pojmout i strukturou velmi komplikovaná data. Kdybyste měli k článku nebo k CzechIdM jakýkoli dotaz, neváhejte se na nás obrátit!