Eile õhtul oli arutelu Silver Sepaga, kes on Vita Group OÜ tarkvara arhitekt ning tal oli mure teemal, et kas kasutada DTO -d või mitte oma lahenduses (kus kohas täpsemalt seda loe alt poolt). Kuna olen DTO -de kasutamise temaatikaga varem tegelenud, siis andsin nõu, et vältida temal samade vigade tegemist, mis mina olen teinud.
Antud teema on juba aastaid arutluse all DomainDrivenDesign -i foorumis ja pole siiamaani ühest vastust - eks kõik sõltub kontekstist nagu ikka tarkvara arhitektuuri puhul. Igatahes, pika arutelu käigus jõudsime huvitava lahenduseni, mida sooviksin jagada ja arutelu üles utsitada, et mida kaasarhitektid ja tarkvara disainijad eestis mõtlevad. Aga enne veel kui asuks lahenduse kirjeldamiseni, tuleks lugejale anda tausta ja viia konteksti.
Taust
Selleks, et paremini mõista allolevat konteksti on soovituslik tutvuda mõningate terminite ja materjalidega.
Mis on DDD?
Domain Driven Design -i (DDD) raamat (lühikokkuvõte kuskil 100 lk)
Mis on DTO (Data Transfer Objekt)?
Tegu on Java kommuni poolt välja töötatud mustriga. Antud mustri kirjeldusele sooviks lisada omapoolsed kommentaarid. DTO objekte kasutatakse tegelikult ka laiemalt ja need ei pruugi alati tähendada mitmete objektide üle viimist ühest kihist teise. Pigem võib see tähendada huvi, teisele kihile näidata piiratud vaadet objektist ja võimaldada piiratud tegevusi objektil (kõlab nagu interface?).
Küsimus, kasutada DTO -d või mitte, kerkib tavaliselt kahel juhul.
Veebiteenus ja DTO
Sul on lahendus, mis suures pildis koosneb kahest osast - veebiteenus ja klient. Veebiteenus peab publitseerima teenuse meetodeid, mille sisendid ja tulemused on mõistliku suurusega andmehulgad. Need ei tohi liiga ühendust liiga koormata ning suhtlus teenusega peab olema kiire. Kui sinu domeeni objekt on rikas objekt - väärtuslik ning paljude omaduste ja tegevustega (meetoditega), siis võib juhtuda, et paljuds omadused ei pruugi üldsegi olla vajalikud kliendi poolele. Selleks, et kliendi pool saaks minimaalseima objekti kasutatakse antud juhul DTO -d.
DTO kasutamine atnud juhul, aga toob lisa halduskulusid (on rohkem koodi mida hallata), kuna nüüd peate looma domeeni objektist DTO -ks ja DTO -st tagasi Domeeni objektiks muutmise koodi. Kusjuures antud koodi loomine on suht tülikas ja igav tegevus.
Domeeni teenus ja DTO
Sarnane vajadus on domeeni teenuste (DDD mõistes) ja kliendi kihtide vahelises suhtlemises. Sa soovid kliendi kihile (kihtidele) näidata piiratud võimalustega objekti.
Kontekst
Antud arhitektuuri konteksti viimiseks olen loonud lihtsustatud näidisarhitektuuri, mida mina olen tihti oma lahendustes kasutanud ja mis sarnane natukene ka Silveri poolt kasutatud arhitektuurile. Igatahes kasutab see Model-View-Presenter mustrit, millele on teil ka tark silm peale visata (kusjuures ma olen plaaninud antud mustrist blogida, aga pole kahjuks selleni veel jõudnud, väga väärtuslik muster. Sellest kirjutab ka Martin Fowler UI arhitektuuri mustrite all). Hetke seisuga on aga MVP jagatud kaheks. Lisaks järgitakse Domain Driven Designi disanipõhimõtteid (üleval on link raamatule, mis neid tutvustab).
Arhitektuur ise näeb välja nagu all oleval UML -i mudelil (kusjuures siin on kirjas ainult antud kontekstile tähtsad osad, muidu on veel näiteks DataAccess -i osa jne).
Domeen (Domain) kujutab endast lahenduse äriloogikat (äriobjektid ja nende omadused). Kui domeen on korralikult disainitud ja arenduse käigus hallatud, siis peaks see kiht olema täiesti taaskasutatav. Domeeni teenus (service) on domeeni objekte ja loogikat kontrolliv ning haldav kiht. See võib olla peenike (kõigest suunab edasi domeeni objektidele tegevust) või paksem, kus ta haldab näiteks andmebaasi ühendust jm ressursse ning ka ligipääsu ja domeeni objektide kasutamist. Tavaliselt Service -i kihis on n-ö use case -i põhised teenused ning ühe teenuse sees võib olla mitme domeeni objektiga tegevusi.
Presenterid on kontrollerid, mis täidavad view -d andmetega ja haldavad sündmusi, mis View -s toimuvad. Ka suhtleb Presenter domeeni teenusega, et domeeni vajalikke muudatusi (tegevusi) teha. Kusjuures Presenter näeb View -d läbi interface -i ainult. View on puhtalt visuaalne pool - kasutaja kontrollid (usercontrol) ja lehed. Nad sisaldavad endas kujunduselemnte, paigutust jne. ja suunavad kõik sündmused edasi Presenterile.
Probleem
Täisinformatsioon (omadused ja meetodid) domeeni objektide kohta on vajalik ainult domeeni teenustel, kes haldavad domeeni objekte. Presenter ja View ei vaja muud, kui informatsiooni mida kuvada (mis juhuslikult on ka domeeni objektis hetkel). Nad ei käivita meetodeid domeeni objektidel, sest me soovime et õiged kihid teeksid õigeid tegevusi ja kõik oleks korralikult enkapsuleeritud.
Probleemi sõnastuseks oleks seega: "Kuidas piirata ligipääsu domeeni objektidele nii, et domeeni teenus saaks kõikke teha aga Presenter ja View näeksid limiteeritud versiooni samast objektist (saaksid ainult lugeda omadusi näiteks)? Otsime kõige lihtsamat viisi, mis ei põhjusta suuri lisa koodihaldustööd."
Lahenduse variandid
Minule tulevad lahendustena kohe pähe kolm varianti:
- Interface -i kasutamine (kolm erinevat realiseerimist)
- DTO objektid
- Domeeni objektid (kui ükski ülemistest ei aita)
Alustame seletamisega alt poolt ülesse.
Domeeni objektid
Siin kohal võib mõelda, miks ma selle variandi üldse kirja panin. See ei täida ju tingimusi ja lahenda probleemi. Kahjuks aga on see lahendusvariant, mis tihti peale valitakse, kuna see ei nõua üldse lisa koodi loomist ja haldamist. Antud lahenduse puhul eeldatakse, et Presenter ja View on alati hallatud sama meeskonna poolt, mitte kunagi keegi domeeni objekte valesti ei kasutaks ja üldse jälgitakse aktiivselt arhitektuuri reeglite järgimist ning tagatakse nii, et antud lahendus on sobiv.
DTO objektid
DTO objektid on muidu tore lahendus ja tagab kindlasti, et kuidagi domeeni objektile ligi ei saada. DTO objekti kasutamine tähendab seda, et Domeeni teenus saab sisse ja väljastab alati DTO objekti ning see saadakse domeeni objekti transleerimisel DTO objektiks. DTO objektid on eraldi klassid, mis on 1:1 seoses domeeni objektiga ja, mis sisaldavad neid domeeni omadusi, mida siis view (kliendi) pool vajab kindlasti.
Muidu on lahendus suhteliselt elegantne ja kindlasti korrektne ning üheselt mõistetav. Kahjuks, aga ei vasta lahendus nõudele - me ei soovi hallata veel sama palju klasse, kui on domeeni klassid ning luua transleerimise koodi (ja iga kord uuendada seda, kui uueneb domeeni objekt).
Interface
Liides ehk interafce on variant, mis mulle kõige rohkem meeldib ja kui mõelda, et me soovime piirata, mis funktsionaalsusi domeen välja näitab, siis liides saab selle ülesandega kindlasti hakkama. Originaalselt muidugi on liides mõeldud selleks, et ühendada kõikki sarnaste omadustega objekte ja nõuda objektilt, et ta teatud omadusi/tegevusi võimaldaks. Hetkel vaatame me asja veidi teise nurga alt :)
C#.Net -st on võimalik implementeerida liidest kahte moodi - explicitly (välja toodud, mis interfce -i omadus on) või implicitly (vaikimisi).
Antud lahendus hõlmab mõlemat lähenemist. Pakun välja, et domeeni objektid peaksid implementeerima IDtoObjekt -i, milles on kõik omadused kirjas, mis Presenterile ja View -le on tähtsad.
See nõuab osalist lisatööd, aga mitte nii palju kui DTO -de puhul ja transleerimist ei ole vaja ise realiseerida.
Domeeni objekt implementeeriks tavalised propertid, mis ei ole domeeni objekti või domeeni objekti listi tüüpi implicitly (ehk vaikimisi) ja muud omadused explicitly. Põhjuseks on probleem, et te soovite kasutada domeeni teenuse kihis täisfunktsionaalsust domeeni objektidelt ja samas võimaldada Presenter ja View kihis ainult IDtoObjekti interface -de läbi objektide kasutamist. Kõige lihtsam viis näidata, mida sellega mõtlen on koodi abil. All olev kood illustreeribki lahendust, mida kirjeldasin.
Oletame, et me soovime niisugust DTO objekti näidata Presenterile ja View -le:
public interface IDtoEntity
{
/// <summary>
/// Liides võimaldab ainult Get -da Teksti.
/// </summary>
string Tekst { get; }
/// <summary>
/// See tuleb siis explicitly implementeerida
/// ja võib nõuda lisaarendus natukene :(
/// </summary>
IDtoEntity2[] Entitid { get; }
}
Selle implementatsioon näeks siis välja niisugune:
/// <summary>
/// Entity on tüüpiline domeeni objekt.
/// </summary>
public class Entity : IDtoEntity
{
private Entity2[] _massiiv;
private string _muutuja;
/// <summary>
/// Domeeni objekti tüüpi massiv.
/// Seda ei saa IDtoObjekt liideses avalikustada,
/// kuna võimaldab ligipääsu päris domeeni objektile.
/// </summary>
public Entity2[] Entitid
{
get { return _massiiv; }
set { _massiiv = value; }
}
/// <summary>
/// Tavalist string tüüpi property.
/// Implicitly implementeeritud (piirame liideses!).
/// </summary>
public string Tekst
{
get { return _muutuja; }
set { _muutuja = value; }
}
/// <summary>
/// Explicitly implementeeritud IDtoEntity2 tüüpi massiiv.
/// Antud juhul ta castib/konverteerib teist tüüpi loeteluks/massiiviks.
/// </summary>
IDtoEntity2[] IDtoEntity.Entitid
{
get { return Entitid; }
}
}
Kusjuures toetav klass Entity2 ja liides IDtoEntity2 on täiesti tühjad
/// <summary>
/// Tühi domeeni objekt 2.
/// </summary>
public class Entity2 : IDtoEntity2 { }
/// <summary>
/// Tühi IDtoObjekt interface 2.
/// </summary>
public interface IDtoEntity2 { }
Meil on olemas nüüd funktsionaalsus ja jääb üle vaid proovida seda kasutada ja vaadata, kas kõik toimib nii nagu soovime:
public Runner()
{
//Service -i seest kasutamine
Entity entity = new Entity();
entity.Tekst = "Vaartus";
string m = entity.Tekst;
Entity2[] s6brad = entity.Entitid;
//Presenterist ja View -st kasutamine
IDtoEntity entityDto = entity;
m = entityDto.Tekst;
//entity.GetString = "Vaartus"; // EI saa omistada :( Ainult get on.
//Saame kätte IDtoEntity2 massiivi:
IDtoEntity2[] s6brad2 = entityDto.Entitid;
}
Mulle tundub, et antud lahendus toimis, nii nagu me soovisime ja erikoodi tuli ka luua vähe ning transleerimist ei pidanud eraldi tegema? OK, interface -e tuleb hallata (neil puudub realisatsioon, on ainult kirjeldus).
Muidugi leidub antud lahendusel ka kitsaskohti.
Kõigepealt, kui soovite tagastada domeeni objekti, siis View -le ja Presenterile soovite te tagastada IDtoObjekt -i ning peate selle explicitly implementeerima (samas säilitate samad nimetused propertitel). Lisaks, juhul kui te kasutate generic liste (näiteks IList<Entity>), siis peate explicitly implementeeritud elemendi sees ka manuaalselt konverteerima oma uude tüüpi listiks (ehk käima läbi kõik IList<Entity> elemendid ja lisama nad uude listi IList<IDtoEntity>. Saaks teha generic helperi, mis teeb seda lihtsalt IList -de jaoks. See probleem on generic tüüpidel (tüüp on sisse kirjutatud)).
Üleskutse
Mul oleks ülimalt heameel kuulda teiste arendajate arvamusi! Diskuteeriks veidi kommenteerides antud postitusele.
- Mis kitsaskohti teie näete?
- Mis arvate üldiselt?