De multe ori începerea unui proiect nou are sens sub formă de monolit, în mod deosebit în proiecte lean în care cerințele și produsul în sine nu sunt foarte bine închegate de la început. În astfel de proiecte, modelele datelor domeniului aplicației se transformă și se modifică mult, o dată ce aplicația pivotează iar cerințele evoluează. Pe măsură ce proiectul și produsul se maturizează, modelul și domeniul datelor se sedimentează și devine din ce în ce mai stabil. Este momentul în care unele domenii din aplicație vor deveni mai active decât altele.
Aceasta este etapa în care microserviciile ar putea să aducă avantaje, permițând echipelor de dezvoltare să se concentreze pe arii mai restrânse care devin mai ușor de gestionat și de dezvoltat. Microserviciile implică însă și un efort suplimentar dat de nevoia de a le integra, configura și automatiza. Din această cauză, la începutul unui proiect, când aproape toate ariile unui produs evoluează constant, iar criteriile de partajare a ariilor în servicii se pot modifica frecvent, (unele servicii dispărând nu de puține ori complet sau migrând în altele) nu prea există suficiente beneficii în pornirea lor sub formă de microservicii.
Dar odată ce aplicația prinde contur, cum facem tranziția de la un monolit la o aplicație bazată pe microservicii? Cum o partiționăm? Cum ne asigurăm că toate acele partiții sunt bine delimitate și nu creează dependențe care să facă dezvoltarea un coșmar? De unde începem să migrăm funcționalitate și cum ne asigurăm că în fiecare moment continuăm să avem o aplicație stabilă și pe care o putem exploata în producție?
Unele din conceptele și tiparele introduse de Domain Driven Design (DDD) ne pot fi folositoare în a răspunde acestor întrebări.
Eric Evans a introdus termenul de Domain Driven Design (DDD) pentru prima data în cartea sa (cu același nume) unde prezintă tipare și principii care abordează complexitatea din dezvoltarea aplicațiilor informatice. Deși acestea s-au întâmplat înaintea apariției conceptelor de microservicii, DDD a apărut în perioada de plin avânt al arhitecturii orientate pe servicii (Service Oriented Architectures) unde partiționarea bine delimitată a serviciilor este la fel de importantă. Din această perspectivă, microserviciile sunt o nouă implementare a conceptelor fundamentale prezente și în SOA. Adevărat că este o implementare care analizează cu succes o serie de neajunsuri din zona serviciilor suprareglementate centrate pe Enterprise Service Bus.
O potențială problemă este dată de faptul că microserviciile se bazează pe un model curat cu puține sau chiar fără dependențe de alte servicii în condițiile în care un monolit existent are deja modele gata stabilite și implementate. Există bune practici de modelare care să minimizeze cuplarea dintre module și pachete și care să asigure premisele unei tranziții ușoare. Cu toate acestea însă, aceste constrângeri într-un monolit, sunt mai puțin drastice în comparație cu cerințele microserviciilor.
De exemplu, o problemă comună apare când este modelată entitatea sub forma unui singur concept care este folosit în diferite contexte de domeniu. Exemple clasice sunt modele ale pacienților sau ale produselor. Dacă considerăm conceptul de produs, de multe ori un super model este construit astfel încât să încerce să acopere toate aspectele unui produs real: de la stoc, un concept ce aparține de gestiune, la preț care este parte din contextul de vânzări și până la ordine de achiziții care țin de departamentul de achiziții.
Figura 1- Produs este un model gigant utilizat in contexte multiple
Rezultatul unui astfel de supermodel utilizat în contexte multiple este că oricare context pe care dorim să-l transformăm (complet sau parțial) într-un microserviciu va fi constrâns de restul de contexte care folosesc același model.
Cea mai bună abordare de decuplare ar putea fi un singur model pentru fiecare context conținând o reprezentare unică a unui produs care satisface doar cerințele contextului din care face parte. Pentru cineva care lucrează în cadrul departamentului de vânzări, proprietățile produsului care prezintă valoare pot fi: imaginea, specificațiile sau categoria. În aceeași companie însă personalul din cadrul departamentului de achiziții ar putea fi interesat mai degrabă de timpul de livrare, numărul de produse din lotul comandat sau de prețul de achiziție și nu ar fi de loc interesat de detalii de genul imaginii sau categoriei de produs.
Fiecare astfel de model din cadrul unui context al domeniului aplicației ar avea propriile proprietăți distincte chiar dacă ele aparțin aceluiași produs fizic.
Figura 2- Fiecare context de domeniu are modelul sau specific de produs
Înseși modelele rezultate vor fi decuplate, mai simple, ceea ce le face mai ușor de întreținut.
Dar acesta este exact tiparul de context delimitat (bounded context) din Domain Driven Design.
Un context delimitat are, precum sugerează și numele, o demarcare în care fiecare model are o semnificație fără echivoc. Într-un context delimitat vom avea modele care au sens doar în contextul respectiv, precum conceptul de "lead" (client potențial) într-un context de marketing.
Putem, de asemenea, avea modele similare dar cu semnificații foarte diferite în contexte diferite. Un exemplu ar fi conceptul de preț care în contextul de vânzări are cu totul altă semnificație decât în contextul de achiziții.
În definiția Domain Driven Design un context delimitat deține responsabilitatea pentru întreaga partiție verticală de funcționalitate de la nivelul de prezentare prin cel de logică a aplicației și până la stocarea datelor.
Figura 3- Fiecare context delimitat devine un micro serviciu
Astfel putem observa similaritatea cu arhitectura bazată pe microservicii împreună cu toate beneficiile aduse mentenabilității, o modificare a unui model în interiorul unui context delimitat are șanse mari de a nu avea impact asupra altor contexte.
Ba mai mult, în acest fel nu există constrângeri în a adopta un tip de arhitectură diferit în interiorul unui context delimitat. În figura de mai sus, am folosit o arhitectură de tip layered în interiorul fiecărui context, dar nimic nu ne împiedică să alegem un tip de arhitectură care se potrivește cel mai bine specificului operațiunilor din contextul respectiv: layered, CQRS, microkernel, ...
Chiar dacă în imagine nu figurează vreo relație între contexte, vor fi situații în care aceasta va exista. Nu poți avea un proces de vânzare a unui produs fără o verificare prealabilă a stocului. Deci există dependențe între contexte delimitate precum există și între microservicii, iar acestea sunt foarte importante. Din fericire, Domain Driven Design ne poate ajuta cu două noțiuni adiționale: strat de anti-corupție (anti-coruption layer) și harta contextelor (contexts map).
Stratul anti-corupție are responsabilități de translatare și acționează precum un grănicer în cadrul contextului delimitat. El asigură, în situația în care este nevoie de integrări cu servicii externe, că modele contextului nu sunt influențate (corupte) de către modele externe. Acest aspect devine extrem de important în situația în care se pornește de la un monolit întrucât modele din cadrul monolitului sunt deja stabilite și este posibil să nu se potrivească cu cele dezvoltate în cadrul contextului delimitat.
Pot exista mai multe tipuri de relații între contexte și din acest punct de vedere DDD definește câteva tipare în abordarea acestor relații:
Shared kernel - Prin care un model comun este dezvoltat pentru a fi utilizat în contexte multiple. Precum ar fi o librărie în care modelele au fost stabilite de echipe multiple lucrând la sisteme sau contexte diferite și care este folosită pentru schimbul de date între aceste contexte sau sisteme.
Open host service - În situația în care un sistem extern este folosit de contexte multiple, implementarea unui strat de translatare anti-corupție pentru fiecare context ar putea fi redundantă și ineficientă. În această situație, definirea unui contract clar pentru serviciul extern și expunerea lui sub forma unui serviciu ce poate fi consumat de către orice context pe baza contractului ar fi o soluție mult mai facilă.
Upstream/downstream - În situația în care este o dependență clară și direcțională între contexte în care unul furnizează serviciul (upstream) iar celălalt îl consumă (downstream).
Pe măsură ce mai multe microservicii / contexte delimitate interoperează devine crucial să documentăm relațiile dintre ele. O hartă a contextelor din DDD este responsabilă cu captarea relațiilor tehnice și organizaționale între contexte.
Figura 4- Harta contextelor unei aplicații
Este important să știm cum ne pot ajuta practicile și tiparele DDD , dar și de unde pornim cu partajarea unui monolit și în ce mod?
Considerând diferențele potențiale dintre așteptările din partea modelelor dintr-un context delimitat și ale celor oferite de un monolit, o rescriere va aduce riscuri majore și va destabiliza proiectul din cauza introducerii unui număr mare de defecte. În contextul migrării aplicațiilor învechite, care poate fi foarte asemănător cu situația transformării din monolit în microservicii, Eric Evans, inițiatorul DDD, nu recomandă o rescriere substanțială ci una graduală în pași mici. O abordare incrementală în care formăm contexte balon pe care le extindem etapizat.
Strategia contextelor balon, descrisă de Evans, propune să creeze un context delimitat de mici dimensiuni stabilit prin intermediul unui strat anti-corupție. Pentru început este recomandat să alegem o funcționalitate nouă într-un domeniu din aplicație care este important, dar restrânsă ca dimensiune. Ar fi bine ca echipa să fie mică dar experimentată în practicile DDD și să cunoască codul aplicației monolit și domeniul ales.
Funcționalitatea nouă poate fi modelată fără constrângeri iar integrarea în aplicația existentă se va face prin intermediul bazei de date și a serviciilor existente în monolit. În această idee, nu vom crea o bază de date nouă pentru contextul nou stabilit ci vom interoga baza de date folosită de monolit, dar vom rearanja datele corespunzătoare noilor modele. DDD folosește tiparul repository pentru a abstractiza operațiunile de acces la date. În acest fel se justifică implementarea stratului anti-corupție în cadrul acestui repository.
De fiecare dată când o instanță a unui model este necesară în interiorul contextului balon stabilit, aceasta este cerută prin intermediul repository. Repository-ul folosește servicii din monolit sau face interogări în baza de date a monolitului, returnează datele unor translatori care rearanjează informațiile conform modelelor stabilite în contextul balon. În final, stratul anti-corupție returnează aceste instanțe prin interfața repository-ului. Funcționalitatea tiparului repository este îndeplinită fără ca acesta să posede date proprii (în această etapă).
Figura 5- Stabilirea contextului balon
Deși contextul balon rezultat va fi curat, acesta nu are încă toate avantajele prezentate de un microserviciu din cauza dependenței de datele din cadrul monolitului. Partea de independență și scalabilitate a contextului rezultat nu se situează încă la nivelul așteptărilor.
Pentru a-l duce la nivelul următor trebuie să transformăm contextul balon într-un context independent cu propria soluție de stocare a datelor. Abordarea depinde foarte mult de specificul modelelor implementate de aplicația monolit. Fie reușim să smulgem funcționalitatea completă a noului context din monolit, în cazul în care modelele din monolit nu sunt partajate cu alte potențiale contexte sau refactorizăm modelele din monolit în așa fel încât referințele vechi să fie redirecționate către noul context.
Figura 6-Contextul balon autonom
În această refactorizare metoda Mikado ne poate da o mână de ajutor.
Metoda Mikado (care își trage numele din jocul european cu bețișoare) prezintă o metodă structurată de a face modificări substanțiale în codul sursă al unei aplicații într-o manieră sigură. De câte ori ne-am trezit porniți într-o refactorizare doar să ne regăsim după multe ore cu atât de multe modificări încât ne-am pierdut încrederea că sistemul va continua să funcționeze corect ?
Metoda are la bază patru pași:
Stabilirea unui obiectiv. Ne gândim la ce anume dorim să obținem, un punct de pornire și un punct final sau criteriu de succes. În exemplul nostru bazat pe monolit ar putea fi înlocuirea unor câmpuri folosite din supermodelul produsului cu modelul din contextul balon.
Experimentarea. Începem să executăm modificările pe care le credem necesare pentru a ajunge la obiectivul fixat. Țelul este obținerea de informații despre impactul modificărilor necesare. De îndată ce o modificare rezultă într-o eroare ne oprim și ne alegem corectarea acelei erori ca obiectiv următor.
Vizualizarea. Fiecare etapă o notăm, începând cu obiectivul inițial urmat de fiecare pas pe care l-am executat cu succes dar și de obiectivele ulterioare rezultate din erori.
La un anumit moment dat, o modificare (obiectiv) va putea fi implementată fără eroare, acela fiind momentul în care putem aplica în ordine inversă (de la cea mai recentă) modificările notate (vizualizate).
Figura 7-Vizualizarea unui lanț de modificări prin metoda Mikado
Prin folosirea acestei metode refactorizările devin clare, mai ușor de gestionat și mai puțin riscante.
Multe din tiparele și practicile introduse de Domain Driven Design pot avea un impact pozitiv mare asupra calității aplicației rezultate dar și asupra modului în care comunicăm în echipă sau cu clienții. Importanța acestor tipare și practici devine tot mai mare pe măsură ce aplicațiile pe care le dezvoltăm sunt nevoite să se adapteze tot mai des și la un set de cerințe tot mai divers. Prin combinarea DDD cu arhitecturi recent rezultate din proiecte la scala web precum și prin practicile introduse de mișcarea agile de genul refactorizărilor ne asigură setul de unelte și practici pentru a dezvolta proiecte care să satisfacă nevoilor.
Pentru mai multe detalii despre DDD și metoda Mikado vă recomand următoarele cărți:
"Patterns, Principles, and Practices of Domain-Driven Design", Scott Millett si Nick Tune