Domeniul de design bazat pe domeniu de design-driven

Una dintre componentele unei aplicații de succes este crearea unui domeniu care se potrivește cel mai bine scenariilor de utilizare a sistemului.

Modelul de domeniu anemic







Dacă obiectele dvs. de domeniu sunt containere de date și tot ceea ce conțin sunt proprietățile get / set. atunci folosiți un model de domeniu anemic. Particularitatea sa este că obiectul de domeniu nu are comportament.

Scenarii pentru utilizarea, de exemplu, a unui magazin online:

  1. Utilizatorul care a înregistrat în sistem primește o scrisoare cu un link la confirmarea înregistrării. Făcând clic pe link, confirmă înregistrarea și se poate loga folosind sistemul de conectare și parola.
  2. Utilizatorul poate face comenzi
  3. În același timp, în contul său personal, el vede suma totală pe care a comandat-o. Suma totală a comenzilor curente nu ia în considerare comenzile deja încheiate

Să începem cu un model de date anemice. Vom avea clasa Cont:

Fiecare scenariu este destul de simplu:

Numărul scenariului 1. Activarea utilizatorilor

Scenariul 2. Adăugarea unui ordin

Numărul scenariului 3. Calculul sumei totale

Întrebarea principală: unde va fi localizat acest cod?

Există cea mai simplă și greșită decizie. Vom scrie acest cod direct în manageri pe paginile aspx sau WinForms:

Totul va fi bine, atâta timp cât puteți adăuga produsul numai din acest formular, iar suma totală se calculează numai prin această formulă. Problemele vor începe atunci când o altă formă necesită aceeași funcționalitate. Va trebui să repet codul. Apoi, dacă schimbați logica lucrării, trebuie să o corectați în toate codurile din spate.

Este greu să duplicați codul și apoi să petreceți mult timp pentru a stabili o cerință de afaceri modificată.

Totuși, nu vom repeta. Vom pune codul pentru implementarea scenariilor noastre într-o clasă cu numele de sunet AccountHelper sau AccountManager. Cel mai probabil această clasă va fi fără stat și deci statică.

Problema clasei cu numele * Helper sau * Manager este că își pot permite să facă orice. Numele lor abstract vă permit să "ajutați" clasa Account pentru a face lucruri complet diferite. Aceste clase devin în cele din urmă obiect al lui Dumnezeu.

Astfel de clase au multe deficiențe. De exemplu, este greu să testați codul care utilizează aceste clase deoarece acestea sunt statice. Ele fac codul puternic legat, deoarece încalcă principiul inversării dependențelor. De foarte multe ori sunt chemați alți ajutători ai unui alt ajutor. Ca rezultat, graficul dependențelor seamănă cu o rețea de conexiuni.

În plus, această soluție are toate dezavantajele următoarelor.

Să începem să ne luptăm cu o coerență puternică în cod. O vom face corect și vom crea clasa AccountService cu interfața IAccountService. Toate obiectele care trebuie să activeze sau să adauge o comandă vor utiliza interfața IAccountService în locul unei implementări specifice. Acest lucru ne va ajuta, de asemenea, în testarea codului.

Ne-am ocupat și de conectivitate și testare. Faceți deja un pas înainte. Dar văd încă două probleme.

Vor exista o mulțime de funcții precum AddOrder și CalculateOrdersSum. După o jumătate de an de dezvoltare, interfața IAccountService va crește la 40-50 de funcții. "Contaminarea" interfeței poate fi experimentată, dacă nu pentru a doua problemă.

În codul de oriunde, puteți ocoli serviciul pentru a scrie utilizatorul "dvs. de activare". De exemplu, luați un obiect Cont din baza de date, setați câmpul IsApproved la adevărat și uitați să actualizați câmpul ActivationDate. Același lucru este valabil și pentru scriptul pentru adăugarea unei comenzi. Puteți apela funcția Adăugați proprietatea Comenzi oriunde și uitați să setați câmpul Cont pentru a adăuga ordinea. Acest lucru face sistemul instabil. Aplicația API este vulnerabilă la utilizatorii sistemului. Cu această abordare, rămâne doar speranța că programatorul va găsi funcția de care are nevoie în IAccountService. dar nu va reinventa abordarea sa.

Am pus toate aceste funcții în contul de domeniu propriu-zis. Observați cum s-au schimbat modificatorii de acces pentru câmpurile de obiecte:

Acum, domeniul aplicației noastre oferă utilizatorului un API gata, pe care nici Helper, nici serviciile nu le cer. În plus, protejăm utilizatorul de erori. El nu va mai putea să activeze contul numai cu IsApproved. Acum funcția Activare va completa câmpurile necesare.

Deci, dacă funcția funcționează pe date și obiecte care se află în interiorul domeniului, atunci cel mai probabil este necesar să lăsați această funcție în interiorul domeniului. În plus față de fiabilitatea codului, veți crea, de asemenea, o limbă de domeniu pentru aplicația dvs.

Ei bine, cu privire la decizia numărul 0 - Sunt inconfortabil atunci când îmi amintesc că el el însuși de multe ori folosit. Acest lucru, desigur, este pur și simplu inacceptabil.

soluția numărul 1 - aplicabilă doar cu logica hard-codată și numai acolo unde este disponibilă clasa AccountHelper (salut, căpitanul este evident :)). Într-un proiect mic, este o soluție tolerabilă.

Soluția numărul 2 - pentru mine este destul de potrivit. Dacă aveți logica de afaceri într-un ansamblu separat și script-ul de lucru din logica de afaceri poate fi schimbată (care este, există, sau pot fi mai multe implementări IAccountService), atunci de ce nu. Dar, în general, potrivită ar fi de a crea mai multe interfețe, fiecare dintre acestea ar sprijini un anumit set de operațiuni asupra entităților - de exemplu, IAccountViewer, IAccountActicvator (exemple sunt, desigur, mult exagerată, dar punctul este clar) - toate aceste interfețe pot implementa și o clasă pentru client nu este importantă.







În general, alegerea unui loc pentru stocarea logicii entităților de procesare: entitate sau serviciu este o întrebare filosofică. Aici totul depinde de conexiunea logicii cu domeniul. Dacă doriți ca această logică să fie utilizată de toți utilizatorii domeniului, atunci logica este localizată în entitate. Dacă aceasta este o logică specifică a interacțiunii dintre un client și domeniu - alegeți să eliminați logica din serviciu. Dacă există o opțiune intermediară - aceasta este deja decisă pe baza bunului simț.

Aceasta se numește ISP.

PS: pentru fiecare client al domeniului, se presupune nivelul serviciului său. Dar, dacă clientul folosește majoritatea metodelor unui anumit nivel de serviciu existent, atunci acest nivel de serviciu ar trebui utilizat. Ei bine și pentru a evita duplicarea în servicii, este util să folosiți un model de comandă.

al treilea model neajunsurilor rang, de obicei, au nevoie de vânt stratul de transport, care este, de asemenea, necesară pentru menținerea datelor redundante, și mai mare, care este parțial rezolvată de încărcare leneș.

În măsura în care înțeleg, decizia nr. 2 se referă la un model parțial anemic. Un fel de compromis: are metode de domeniu, cum ar fi activate, care nu depind de clase externe, deși conțin logică. Restul sunt în Layerul de servicii.

@Constantine
Vă mulțumim pentru adăugiri, foarte valoroase.

Va fi interesant să examinați exemplele:

> În plus, am întâlnit problema unui lanț de dependențe

Dacă există o mulțime de cod, aruncați pe poștă.

Și dacă un pic complica exemplul, să introducă o nouă funcții de afaceri Regula: nu se poate adăuga la proiectul de lege ordine, în cazul în care acesta are deja o stare a comenzii IsComplete = true. Unde adăugăm această verificare? Evident, în metoda AddOrder a clasei Account, deoarece el este responsabil pentru adăugarea unei comenzi în cont.
Dar, în acest caz, avem de după cecul a fost pozitiv, dar înainte de a comanda a fost adăugată - un alt utilizator adaugă status personalizat IsComplete = true, apoi ca cec a trecut deja, ordinea primului utilizator va fi adăugat în liniște, iar regula este reguli rupte. Evident, trebuie să adăugați și să verifice o tranzacție efectuată în baza de date, dar tranzacția de baze de date - acest lucru nu este stratul de domeniu.
Cum te descurci de această situație?

Depinde de încărcarea aplicației.

Cea mai ușoară cale de a pune Lock în locul ăsta.

O altă opțiune la deschiderea unei tranzacții este de a specifica Nivelul de izolare, care nu permite o astfel de situație.

Și ați încercat deja să rezolvați problema?

NHibernate Nu folosesc, dar ideea este aproape înțeleasă.

Doar de ce "gestionarea tranzacțiilor este efectuată la nivelul controlerului de către obiectul UnitOfWork"? Iată un exemplu cu o comandă și un proiect de lege, deoarece, de fapt, datoria de a verifica regula că contul nu are comenzi finalizate este responsabilitatea domeniului de domeniu. Și este logic să punem acest cec în metoda AddOrder a contului însuși, înzestrându-l cu o tranzacție prin UoW. Dacă atribuie această responsabilitate controlorului, se pare că trebuie să știe cum funcționează metoda AddOrder. Dacă controlorului i se încredințează verificarea însăși, aceasta va însemna că nivelul logicii de operare se ocupă de logica subiectului.

> Și este logic să punem acest cec în metoda AddOrder a contului însuși, având o împrejmuire cu o tranzacție prin UoW

Verificarea va fi în AddOrder, iar UoW ​​va fi creată în controler. Nu vor sti despre unul pe altul.

Aparent nu ați înțeles întrebarea cum nu ar ști despre UoW dacă UoW va fi numit în interiorul acestei metode. În cazul în care apelul metoda în sine, în controlerul va fi impusă UOW, se presupune că partea apelantă ar trebui să reprezinte ceea ce se întâmplă în interiorul metoda de a avea un motiv de a impune UOW de apel.

UoW este creat în controler, în obiectul de domeniu nu există nici o referință la UoW. De exemplu, metoda client.Lock () modifică starea obiectului. Schimbarea acestor date se face în mod automat. Este capabil să facă, de exemplu, NHibernate.

hmm. și comenty ceva pentru a freca.

Mulțumesc pentru răspunsuri, dar. Îmi dau seama, desigur, că este dificil de a transporta DDD a maselor, dar odată ce ai venit, am cerut să nu se bazeze pe punerea în aplicare specifică în răspunsurile (de modul în care am menționat că eu nu folosesc Nhibernate), acest lucru lasă într-un template-uri și abordări comune. Răspunsul în spiritul "unui NHibernate este capabil" - nu contează =)

UoW este creat la nivelul controlerului, entitățile sunt preluate din UoW și reintroduse în UoW. În plus, UoW urmărește modificările în entități. Da, este deja implementat în multe ORM-uri, dar puteți să vă uitați la teorie, să scrieți propria implementare.

Bună ziua. Există o mică problemă de neînțelegere. Ați obținut design (scrieți) modelul zonei subiect (DDD). și apoi legați cartografiere NHibernate la ea?

Da, așa este. Și cartografierea este necesară numai dacă baza de date este necesară imediat. Pentru a începe dezvoltarea, puteți utiliza o bază de date în memorie sau fișiere text. Maparea este deja următorul pas.

Alo La noi suntem: Există date de asamblare DataAccess cu o configurație ,, etc. (folosim NHibernate); Există un ansamblu de DomainModel. DA are o legătură cu DM. Întrebarea este: ce să faci cu logica care are nevoie de date din baza de date? Nu pot folosi funcțiile ORM în funcțiile obiectului de domeniu. Este necesar să se creeze cea de-a treia Servicii de asamblare, dar nu este vorba despre ceea ce a fost descris mai sus în articol. Cum sa fii?

Faptul că un strat separat este creat pentru manipularea obiectelor este normal, altfel nu va funcționa. Pur și simplu pot fi servicii, în fiecare din cele zece metode, și pot exista comenzi (model de comandă), în care fiecare echipă va fi responsabilă pentru partea sa de logică.

Alexander, este posibil și drept să folosim obiecte de domeniu în prima abordare a codului pentru EF? Și dacă inițial este vorba de obiecte anemice în viitor, să le refaceți în conformitate cu "Soluția 3" (adăugați API și modificați modificatorii de acces pentru setteri)?
Și o altă întrebare:
„Funcții astfel AddOrder și CalculateOrdersSum va fi destul de mult. După o jumătate de an de dezvoltare interfata IAccountService creste pana la 40-50 de funcții.“ Poate astfel de interfețe IAccountService să fie utile în general și unde?

Denis, poți, pentru că nu există restricții. Avem proiecte pe EF, o folosim tot acolo.

"Pot fi astfel de interfețe ale IAccountService utile în general și de unde?"

Articolul, desigur, nu are un an. Dar sper să răspund. Deci, bine, cu contul.AddOrder, account.Activate, etc. totul în general este de înțeles. Comanda, de exemplu, are și o comandă.AdăugațiProdus, comandă.Aprovini, etc. Și avem, de asemenea, produse care au un număr de proprietăți diferite (articol, titlu, shortTitle, url, descriere, caracteristici []). Managerul / operatorul are capacitatea de a le edita pe toate și de a crea astfel de entități nu este clar dacă nu trebuie să aibă setteri publici. productUpdate (date) sau ce ar trebui să fie în domeniu?

Să presupunem în contextul formării unei ordini pentru noi, nu contează. Apoi face cele două entități ProductForOrderContext și ProductForEditingContext? În acest caz, numărul de entități poate crește semnificativ, iar în al doilea caz, "esența" va fi din nou anemică, de fapt chiar și un DTO. În general, nu este clar.

"Al doilea - formează un limbaj de domeniu și nu este o problemă".

Este chiar o problemă, chiar și câteva:

§ Mixat într-o grămadă de logică, de exemplu - pentru un oaspete, un utilizator înregistrat, un utilizator privilegiat, un administrator.

§ Clasa devine obiect al lui Dumnezeu cu timpul.

§ Principiul SOLID este încălcat.
Anume S - pentru fiecare clasă ar trebui
să i se încredințeze o singură datorie.







Trimiteți-le prietenilor: