DDD reprezintă o metodă practică de a transforma o problemă complexă într-un model ce poate fi implementat ulterior de echipa de dezvoltare software. Există diferite abordări, astfel încât soluția finală să fie SOA, funcțională, bazată pe microservicii sau chiar reactive programming. Este prioritar ca la aceeași masă să se așeze experți din domeniul ce urmează a fi analizat dar și personalul tehnice. Ambele părți vor învăța să folosească un limbaj / tehnici comune, astfel încât transferul de cunoștințe să se poată realiza.
Această abordare este în general utilă pentru aplicațiile software practice care încearcă să rezolve o problemă tehnică de business / enterprise. Implicarea programatorilor în găsirea unei soluții este esențială. Sugestivă în acest sens afirmația lui Scott Wlaschin în lucrarea sa, Domain Modeling Made Functional:
As a developer, you may think that your job is to write code. I disagree.
A developer's job is to solve a problem through software, and coding is just one aspect of software development. Good design and communication are just as important, if not more so.
Abordarea DDD presupune o dezvoltare directă a soluției de către echipa de dezvoltare, fără a mai avea nivele / filtre introduse de către business analiști sau chiar de arhitecți software. Avantajul acestei abordări constă în înțelegerea exactă a problemei, direct de la sursă și dezvoltarea unei soluții pornind de la aceasta. Dezavantajul constă bineînțeles în timpul de dezvoltare mai mare în care echipa R&D trebuie să îl petreacă în dezvoltarea soluției.
Dat fiind timpul scurt impus pentru finalizarea proiectelor software, există riscul unei abordări superficiale în înțelegerea esenței problemei, ajungându-se ca echipa de dezvoltare să primească doar o listă de taskuri în backlog care trebuie rezolvate. Scopul final este ca toate să se mute în secțiunea done , cât se poate de repede. Pentru soluții simple, această abordare poate fi de succes dar pentru un produs complex, cu o estimare a duratei de viață mare vă propun o dezvoltare DDD.
Primul lucru cu care trebuie să începem este să definim problemele pe care dorim să le rezolvăm, vom încerca să le eliminăm de la început pe acelea care nu fac parte din context. Totodată vom defini un limbaj comun între developeri și experții în domeniu.
Începem prin a pune pe o tablă note cu acțiunile posibile, le grupăm pe domenii și adăugăm evenimentele de legătură între ele. În cazul de mai sus, order place și ack. sent to customer reprezintă acțiunile de legătură între un sistem de primire a comenzilor și sistemul de livrare.
Există diferite abordări pentru definirea unui limbaj comun între programatori și experții în domeniu. O abordare clasică este folosirea unor cuvinte cheie precum: and, or, when, than. Un pseudocod mai simplu, care să poate fi înțeles de un expert al domeniului dar care nu este programator. Odată definite diferitele scenarii, se pot implementa și acceptant tests după cum este cel din exemplul următor:
Scenario: The product owner commits a backlog item to a sprint
Given a backlog item that is scheduled for release
And the product owner of the backlog item
And a sprint for commitment
And a quorurn of team approval for commitment
When the product owner commits the backlog item to the sprint Then the backlog item is committed to the sprint
And the backlog item committed event is created
[Test]
public void ShouldCommitBacklogitemToSprint()
{
// Given
var backlogitem=BacklogitemScheduledForRelease();
var productOwnner=ProductOwnerOf(backlogitem);
var sprint=SprintForCommitment();
var quorum=QuorumOfTeamApproval(backlogitem
, sprint);
// When
backlogitem.CommitTo(sprint, productOwner, quorum);
// Then
Assert. IsTrue(backlogitem. IsCommitted{));
var backlogitemCommitted =
backlogitem.Events.OfType()
.SingleOrDefault();
Assert.IsNotNull(backlogitemCommitted);
}
Exemplu de scenariu / test de acceptanță folosit în dezvoltarea unui tool de management Agile Scrum. Sursa
Primul avantaj al acestei abordări este că majoritatea experților în domeniu, fie ea formată și din non-tehnici, va putea înțelege testul de mai sus. Al doilea avantaj va fi folosirea acestui cod în mod real, rularea fără eroare reprezentând implementarea cu succes a acestei acțiuni.
Acțiunile concrete vor fi denumite comenzi: "Realizează taskul X". Rezultatul fiecărei comenzi va fi un eveniment "Taskul X a fost realizat", dacă acesta a fost terminat cu succes. În continuare, vă prezentăm o imagine de ansamblu a acestei abordări:
Se poate observa cum un eveniment va lansa o comandă și care la rândul său poate lansa mai multe evenimente pentru a fi finalizată.
Un amănunt important în această abordare este necesitatea de a se ignora nivelul de persistență. Dorim să definim modele corecte, fără a fi într-un fel sau altul constrânși de limitările bazelor de date. Un alt lucru periculos este să ne gândim în diagrame de clase legăturile dintre diferiți actori.
Definirea structurilor de date se poate realiza într-un limbaj natural:
bounded context: Order-Taking
data Order =
CustomerInfo
AND ShippingAddress
AND BillingAddress
AND list of OrderLines
AND AmountToBill
Iar avantajul acestei abordări este că nu îi va speria pe cei care nu sunt programatori. Continuăm să detaliem și vom ajunge la un pseudocod, așa cum se poate vedea în continuare:
substep "ValidateOrder" =
input: UnvalidatedOrder
output: ValidatedOrder OR ValidationError
dependencies: CheckProductCodeExists
, CheckAddressExists
validate the customer name
check that the shipping and billing address exist
for each line:
check product code syntax
check that product code exists in ProductCatalog
În contextul limitelor domeniului, DTO-urile vor fi validate la intrarea și la ieșirea din limitele acestuia:
În literatura de specialitate, se mai numește context mapping și este o procedură menită să asigure o bună comunicare între ambele domenii. Există mai multe tipuri de abordări similare cu cele din mediul de business sau cu acelea din cadrul unei organizații:
Shared Kernel - ambele domenii folosesc împreună un model comun. Echipele trebuie să fie de acord cu acesta, iar orice schimbare trebuie realizată de comun acord. Această abordare este în general dificil de realizat și de menținut.
Customer/Supplier - este o modalitate de comunicare Vânzător / Client unde clientul își formulează nevoia. Cele două domenii evoluează independent atât timp cât Vânzătorul implementează protocolul clientului.
Conformist - este opusul modelului Client / Vânzător. Vânzătorul definește limbajul, iar clientul trebuie să își implementeze nivelul de translatare. Orice API pus la dispoziția publicului de către un provider de servicii poate fi considerat un exemplu.
Exemplu de folosire în comunicarea diferitelor domenii. Referință imagine.
În cazul programării funcționale se folosește o abordare orientată spre comenzi, care nu alterează datele de intrare într-o funcție. Există întotdeauna o separare clară între date și comportament. Datele de intrare într-o funcție sunt immutable, iar acestea returnează întotdeauna valori noi în loc să le modifice pe cele existente. Rezultatul este un număr redus de side effects. Din perspectiva conceptuală, diferențele dintre o abordare OOP și una funcțională se poate vedea în următoarele diagrame de modelare ale aceluiași domeniu:
Așa cum se poate observa, în cea de-a doua abordare, listenerul OrderPlaced pentru eveniment este adăugat în exteriorul contextului.
Merită menționată și arhitectura de tip Usturoi (Onion Architecture) folosită în programarea funcțională. Dacă în abordarea OOP, modelele și accesul la baza de date se realizează în interiorul domeniului, într-o abordare funcțională, acestea sunt poziționate la extremități astfel încât toate dependințele sunt pe intrarea / ieșirea din componentă. O funcție care scrie și citește dintr-o bază de date va fi considerată impură, iar folosirea acestora va fi evitată în partea de core a domeniului :
Sursa imagine: https://www.safaribooksonline.com/library/view/domain-modeling-made/9781680505481/f_0034.xhtml
O abordare monolit este una în care baza de date este comună. Pe de o parte, se procesează datele clienților online, pe de altă parte, la finalul zilei, pe aceeași bază de date se procesează rapoartele și se optimizează datele acumulate în timpul zilei. Un exemplu în acest sens este clasicul ETL (extract/transform/load).
Un prim pas al evoluției acestei arhitecturi este să includem o bază de date în fiecare componentă, iar comunicarea dintre ele să fie prin intermediul unui API. Deși componentele vor fi mai clare, tot vom avea o complexitate destul de mare și va fi nevoie de o bună comunicare pentru integrarea celor două componente.
Următorul pas este să aplicăm DDD, să definim limitele domeniilor și să modelăm arhitectura în felul următor:
La final vom ajunge la concluzia că o arhitectură bazată pe micro-servicii, se va potrivi cel mai bine acestui exemplu. Vom avea o definire clară a domeniilor, a responsabilităților fiecărei componente și a modului în care acestea comunică.
Design-Driven Development reprezintă o modalitate de comunicare și reprezentare, folosind un limbaj comun al unui sistem complex. Este un proces ce aduce împreună experții în domeniu și echipa de dezvoltare. Rezultatul este unul de înțelegere profundă a problemei care trebuie rezolvată. Vom avea la final o definire a domeniilor, a modului de comunicare între acestea dar și descoperirea unui nucleu de bază (core-ul). Majoritatea conceptelor introduse în acest articol s-ar putea să vă fie familiare, dar ceea ce contează este abordarea DDD. Aceasta este diferită de clasicul PRD/FRD urmat de un backlog complex, unde clientul este abstract și de multe ori aparent inaccesibil echipei de dezvoltare.