TSM - Domain-Driven Design

Ovidiu Mățan - Fondator @ Today Software Magazine


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.

Contextul problemei

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.

Definirea unui limbaj comun

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.

Definirea proceselor și a modelelor

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.

Sursă imagine

DTOs

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:

Sursă imagine

Legătura între domenii

Î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:

  1. 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.

  2. 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.

  3. 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.

  4. Anti-corruption layer - este un nivel de transformare a modelului de intrare într-unul sigur pentru domeniu. Este un traducător al limbajelor folosite de echipele celor două domenii.

Exemplu de folosire în comunicarea diferitelor domenii. Referință imagine.

Abordarea funcțională

Î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:

Abordarea OOP

Abordarea funcțională

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

Use case - procesarea cardurilor de credit

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ă.

Concluzie

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.

Bibliografie

  1. Domain Modeling Made Functional, Scott Wlaschin. ed. The Pragmatic Programmers - 2018.
  2. Domain-Driven Design Distilled, Vaughn Vernon, ed. Addison-Wesley Professional - 2016
  3. Domain Driven Design for Services Architecture