Domain-driven design: konsalting i implementacija

Domain-driven design konsalting je način da složen proizvod prestane biti gomila ekrana i postane model poslovanja: riječi koje ljudi zaista koriste, pravila koja nikada ne smiju pući i granice između timova koji samo izgledaju kao da dijele isti pojam. Kad „narudžba”, „kupac” ili „nalog” znače tri različite stvari u tri dijela firme, kod naslijedi tu zbrku, i svaka nova funkcija postaje skuplja.

U CodeWizu vodimo modeliranje, a zatim ga i implementiramo. Radionice radimo sa ljudima koji poslovanje znaju, crtamo bounded contexte, dizajniramo aggregate koji čuvaju invarijante i taj model upisujemo u Laravel, Node.js ili u postojeći kod. Dijagrami su sredstvo, ne cilj. Isporuka je kod koji vaš tim može mijenjati bez straha, plus zajednički jezik koji i proizvod i inženjering prepoznaju. Pojmovnik, context mapa i testovi koji pravila izgovaraju jezikom poslovanja ostaju u repozitoriju, tako da sljedeći programer ne rekonstruiše radionicu iz sjećanja. Kad se pravilo promijeni, mijenja se model, a ne još jedan uslov sakriven u kontroleru.

Usluga je za proizvode čija pravila više ne stanu u kontrolere i kolone baze: marketplace, naplatu, logistiku, rezervacije, multi-tenant SaaS i svaki sistem u kojem pogrešan prelaz stanja košta pravog novca. Ako je aplikacija običan CRUD alat sa malo politike iza sebe, reći ćemo to i zadržati jednostavan dizajn.

Domain: Bijele table sa žutim samoljepljivim papirićima i markerima u radionici

Modeliranje domena: srž domain-driven design konsaltinga

Modeliranje domena je vođen razgovor koji način rada firme pretvara u model dovoljno precizan da se isprogramira. Za široku sliku koristimo event storming, a example mapping kad jedno pravilo treba zakucati. Samoljepljivi papirići su alat; poenta su rasprave koje izađu na vidjelo prije nego što postanu bagovi u produkciji.

Šta nastane na radionici modeliranja

  • Vremenska linija domain eventova: činjenice koje su važne, napisane u prošlom vremenu („OrderPlaced”, „InvoiceOverdue”, „SeatReleased”), a ne imena ekrana.
  • Komande koje te događaje izazivaju, akteri koji ih pokreću i politike koje reaguju („kad plaćanje ne uspije, oslobodi sjedište”).
  • Vruće tačke: pravila koja niko ne umije čisto da izgovori, riječi koje podijele sobu i vanjski sistemi od kojih proces zavisi.
  • Konkretni primjeri, uključujući one loše: djelimične povrate, dupli submit, rezervacija koja prelazi ponoć, tenant koji se zatvori usred fakture.

Ubiquitous language, zapisan

Ubiquitous language je mali rječnik domena, isti u razgovoru, u korisničkom interfejsu i u imenima klasa. Kratak pojmovnik držimo u repozitoriju i tretiramo ga kao kod: izmjena pojma je izmjena koju tim vidi na reviewu, a ne fusnota u prezentaciji. Ako poslovanje kaže „rezervacija”, a kod kaže „temp_order_row”, jedno od to dvoje mora da se promijeni. Preimenovanje je dio posla, ne kozmetika na kraju, jer pogrešno ime vuče pogrešne provjere za sobom. Sajt Alberta Brandolinija o event stormingu je referenca koju timovima pošaljemo prije radionice, da format bude poznat, a vrijeme potrošeno na domen, ne na metodu.

Bounded contexti: jedna riječ, jedno značenje

Bounded context je granica unutar koje je model dosljedan. „Kupac” u prodaji (lead sa fazom u pipelineu) nije „kupac” u naplati (pravno lice sa poreskim brojem) i nije „kupac” u podršci (osoba koja piše tiket). Jedna tabela i jedna klasa za sve tri uloge daju nullable kolone, posebne slučajeve i bagove koji se pojave samo na rubovima.

Contexte crtamo iz jezika i iz timova, ne iz željenog dijagrama mikroservisa. Context može biti modul u jednoj Laravel ili Node.js aplikaciji, šema u PostgreSQL-u ili zaseban servis. Bitna je granica. Dijeljenje na zasebne deployable jedinice dolazi kasnije, tek kad tim ili potreba za skaliranjem to stvarno traži. Fowlerovo objašnjenje domain-driven designa je kratak uvod za ljude iz poslovanja koji će biti u sobi.

Kako znamo da je granica stvarna

  • Ista imenica ima različita pravila, različit životni ciklus ili različitog vlasnika sa svake strane.
  • Izmjena na jednoj strani ne bi smjela da forsira release na drugoj.
  • Svaki context se novom programeru može objasniti u jednom sjedenju, bez obilaska cijele firme.
  • Integracija između contexta je eksplicitna: API, domain event ili preveden ID, nikada zajednička tabela koju obje strane mijenjaju.

Aggregate koji čuva poslovne invarijante

Aggregate je grupa objekata koja se mijenja kao jedna cjelina. Ima korijen (Order, Subscription, Booking) i to je jedina vrata kroz koja se ti objekti mijenjaju. Razlog da se time bavimo je invarijanta: pravilo koje mora važiti svaki put kad se aggregate sačuva. Narudžba se ne može potvrditi bez stavki. Sjedište se ne može prodati dvaput. Povrat ne može biti veći od naplaćenog iznosa. Ta pravila žive u aggregateu, ne u tri kontrolera koja svaki zaborave po jedan slučaj. Kad je pravilo na jednom mjestu, test koji ga opisuje štiti i API, i pozadinski posao, i ručnu ispravku iz admin panela.

Pravila po kojima dizajniramo

  • Aggregate držimo malim. „Customer” koji sadrži svaku narudžbu, fakturu i tiket zaključava previše i učitava previše.
  • Druge aggregate referenciramo identitetom, ne pokazivačem na objekat. Narudžba čuva customerId, a domain service pita kad joj treba činjenica izvana.
  • Jedna transakcija po aggregateu. Procesi preko više aggregatea (potvrdi narudžbu, pa naplati, pa obavijesti) postaju politike i domain eventi, ne jedna džinovska transakcija baze.
  • Value objecti nose nezgodna pravila: Money, DateRange, Email, Quantity. Iznos novca sa valutom ne smije biti goli float.

Skraćeni PHP aggregate, oblik koji koristimo u Laravel aplikacijama, pokazuje ideju. Konstruktor je privatan, tako da se narudžba dobija samo preko imenovane fabrike, a place odbija praznu narudžbu:

final class Order { private function __construct( private OrderId $id, private OrderStatus $status, private array $lines, ) {} public function addLine(Sku $sku, Quantity $qty, Money $price): void { if ($this->status !== OrderStatus::Draft) { throw new OrderClosed(); } $this->lines[] = new OrderLine($sku, $qty, $price); } public function place(): OrderPlaced { if ($this->lines === []) { throw new EmptyOrder(); } $this->status = OrderStatus::Placed; return new OrderPlaced($this->id); } }

Isti oblik se direktno preslikava u TypeScript ili u jezgro bez frameworka koje zove Laravel ili Express adapter. Perzistencija ostaje vani: repository učitava i snima aggregate, a testovi vježbaju place bez baze. Ista disciplina čuva online prodavnicu od gubitka narudžbe ili uplate u procjepu između dva koraka, o čemu smo pisali u tekstu o čuvanju svake radnje kupca i svake narudžbe.

Arhitektonski nacrti i pribor za crtanje rašireni po radnom stolu

Strateški dizajn i context mape

Strateški dizajn je mapa odnosa između contexta i toga koji tim koji model drži. Context mapa sprečava dva tima da „samo dijele tabelu korisnika” i čini cijenu svakog odnosa vidljivom. Odnos biramo namjerno, jer svaki od njih drugačije raspoređuje ko čeka koga kad se model promijeni. Većina bola koji naslijedimo dolazi od slučajne zajedničke baze koju niko nije pristao da održava, a koju svaki tim ipak mora da razumije prije nego što dirne svoju funkciju.

OdnosŠta značiKoristite ga kad
PartnershipDva tima zajedno razvijaju dijeljeni dio i zajedno uspijevaju ili padajuPreklapanje je stvarno i obje strane mogu da planiraju zajedno
Shared kernelMali komad modela koji oba tima mijenjaju, čuvan zajedničkim testovimaDijeljeni dio je sitan i dovoljno stabilan da sprega vrijedi
Customer–supplierUpstream uzima potrebe downstreama kao ulaz u svoje planiranjeJedan context zaista služi proizvodu drugog tima
ConformistDownstream preuzima upstream model takav kakav jesteDrugu stranu ne kontrolišete: payment provider, legacy sistem
Anti-corruption layerSloj za prevođenje drži tuđi model van vašegIntegracija legacy softvera ili tuđeg API-ja čiji jezik nije vaš
Open host serviceUpstream objavljuje jedan stabilan protokol za mnogo potrošačaPlatformski API od kojeg već zavisi nekoliko contexta
Separate waysNema integracije. Dupliranje je jeftinije od zajedničkog modelaRiječi se preklapaju, ali problemi su zapravo različiti

Prvo moduli, servisi kad ih zarade

Bounded context ne mora biti mrežni servis. U modularnom monolitu svaki context je namespace sa svojim application servisom, svojom perzistencijom i javnim interfejsom koji drugi moduli smiju da zovu. Servis izdvajamo kad tim treba da deploya nezavisno, kad je profil opterećenja drugačiji ili kad je context spreman da se zamijeni. Ranije od toga nastaju distribuirane transakcije i operativni posao bez dobitka. Kad context preraste jednu bazu, šav već postoji – ista vrsta granice o kojoj pišemo u vodiču za sharding i particioniranje.

Taktički građevni blokovi koje implementiramo

Strateški dizajn odlučuje gdje su linije. Taktički dizajn je kod unutar jednog contexta. Klasične blokove koristimo samo tamo gdje zarade svoje mjesto:

  • Entity ima identitet i životni ciklus. Dvije narudžbe istog sadržaja i dalje su različite narudžbe.
  • Value object je definisan svojim vrijednostima, nepromjenjiv je i drži pravila koja bi inače iscurila u svaki upit.
  • Domain event bilježi da se nešto desilo, imenovan je ubiquitous languageom i ulaz je u druge contexte i u projekcije.
  • Domain service drži operaciju koja prirodno ne stoji na jednom entityju, poput politike cijene kojoj trebaju katalog i ugovor.
  • Repository učitava i snima aggregate. Nije generička deponija svakog upita koji korisničkom interfejsu zatreba.
  • Factory pravi validan aggregate. Napola složeni objekti nikada ne sjede u bazi čekajući da ih neki kasniji zahtjev dovrši.

Namjerno preskačemo ceremoniju koja datom kodu ne pomaže: nema repository interfejsa po tabeli „jer knjiga kaže”, i nema domain event busa za pravilo koje jedna metoda može da sprovede. Test obrasca je da li novi programer poslovno pravilo može naći na jednom mjestu.

Kada se domain-driven design isplati

DDD se isplati kad je složenost u poslovanju, ne u frameworku. Znakovi da ulaganje ima smisla:

  • Pravila različiti ljudi objašnjavaju različito, a bagovi u produkciji se gomilaju na prelazima stanja.
  • Nova funkcija traži čitanje pet servisa da bi se znalo da li je izmjena sigurna.
  • Šema baze je jedina dokumentacija, i protivrječi načinu na koji prodaja opisuje proizvod.
  • Više timova commituje u iste module i međusobno se blokira.

Marketing sajt, jednostavan interni CRUD alat ili tanak omot oko jednog vanjskog API-ja ne trebaju aggregate i context mape. Polazimo od poslovnog problema, onako kako angažman na proizvodu i treba, a taj stav smo opisali u tekstu o tome kako softversko partnerstvo prati poslovanje. Ako je dovoljna čistija struktura modula, to je i preporuka.

Česte greške u modeliranju koje ispravljamo

  • Jedan džinovski aggregate („Account”) koji svaka funkcija zaključava i učitava.
  • Anemični model: klase sa podacima bez ponašanja, i pravila rasuta po kontrolerima koji se razilaze.
  • Bounded contexti nacrtani kao jedan servis po tabeli baze.
  • Ubiquitous language koji postoji na slajdu, a nigdje u kodu.
  • Dijeljenje tabela između contexta, pa preimenovanje kolone postane incident za više timova.
  • Modeliranje trenutne baze umjesto poslovanja, što današnje slučajnosti zamrzne u novi dizajn.

Šta određuje cijenu domain-driven design konsaltinga

Procjenu dajemo nakon kratkog pogleda na proizvod i kod, ne iz generičkog paketa. Napor prati nekoliko predvidivih faktora:

  • Koliko različitih jezika organizacija zaista govori i mogu li ljudi koji ih znaju doći na radionice.
  • Broj contexta u obimu. Modeliranje jednog neurednog toka narudžbe nije isti projekat kao mapa cijele firme.
  • Stanje koda: modularna Laravel ili Node.js aplikacija upija model brže nego jedan kontroler od 8,000 linija.
  • Koliko ponašanja treba premjestiti i treba li podatke seliti da prate nove granice.
  • Stajemo li na context mapi i pilot aggregateu, ili model implementiramo kroz cijeli proizvod.

Kako radimo

  1. Upoznavanje: čitamo proizvod, kod i dosadašnje bagove, i razgovaramo sa ljudima koji pravila sprovode svaki dan.
  2. Modeliranje: event storming i example mapping, pojmovnik i prvi rez contexta i aggregatea.
  3. Dogovor: context mapa, invarijante koje svaki aggregate drži i obim onoga što se u kodu mijenja sada.
  4. Implementacija: model ulazi u kod uz testove koji pravila izgovaraju jezikom domena, i adaptere za HTTP, redove i bazu.
  5. Prolazak: sa poslovanjem ponovo puštamo stvarne scenarije, uključujući otkaze, i jezik dotjerujemo tamo gdje model i dalje zvuči strano.
  6. Podrška: kako se pojavljuju nova pravila, širimo pojmovnik i aggregate umjesto da u kontroler dodajemo još jedan poseban slučaj.

Zašto CodeWiz

  • Implementiramo, a ne samo vodimo radionice: radionice se završavaju izmjenama u Laravelu, Node.js-u i bazi, ne folderom dijagrama koji niko više ne otvori.
  • Tehnologija služi poslovanju: nećemo uvoditi aggregate, evente ili dodatne servise tamo gdje je jednostavan model iskren izbor.
  • Open-source na prvom mjestu: model živi u vašem repozitoriju, u običnom PHP-u, TypeScriptu i PostgreSQL-u, bez vlasničkog alata za modeliranje.
  • Full-stack kontekst: API-ji, podaci, redovi i korisnički interfejs nastaju oko istog jezika, jer sve to i gradimo.
  • Fer cijene: vrhunski razvoj bez vrhunskih cijena, od tima iz Banje Luke koji radi na daljinu sa klijentima iz regiona i svijeta. Više o CodeWizu.

Često postavljana pitanja

Koliko košta domain-driven design konsalting?

Fokusirana radionica i zapisani model jednog dijela proizvoda kratak su angažman: pojmovnik, context mapa i jedan pilot aggregate koji tim može da čita. Implementacija aggregatea, izmještanje ponašanja iz kontrolera i selidba podataka preko nekoliko contexta veći su posao, jer tu ulaze testovi, migracija i pažljivo puštanje pored postojećeg saobraćaja. Ponudu dajemo nakon upoznavanja, kad znamo koliko jezika i tokova koda ulazi u obim.

Da li nam domain-driven design treba za CRUD aplikaciju?

Obično ne. Ako se ekrani čisto mapiraju na tabele i pravila stanu u šačicu validacija, bolja investicija je uredno složen modularan kod. DDD zaslužuje mjesto kad prelazi stanja i politike drže bagove i kašnjenja.

Da li domain-driven design znači mikroservise?

Ne. Bounded contexti su logičke granice. Lijepo se slažu na module u jednoj aplikaciji, i tu obično počinjemo. Zasebni servisi su kasnija odluka o deploymentu, vlasništvu tima i opterećenju, ne zahtjev metode.

Koliko traje radionica modeliranja?

Jedan važan tok često je jasan za jednu ili dvije sesije, pod uslovom da u sobi sjede ljudi koji pravilo stvarno sprovode, a ne samo oni koji ga opisuju sa distance. Mapiranje cijelog proizvoda, razrješavanje sukobljenog rječnika i provjera modela prema postojećoj šemi traju duže, a implementacija koja slijedi odnosi najveći dio kalendara.

Možete li ovo primijeniti na postojeći Laravel ili Node.js kod?

Možemo. Većina posla koji radimo je na sistemima koji su već u produkciji. Model uvodimo context po context, iza postojećeg API-ja, i pravila prebacujemo kad testovi pokažu da se ponašanje poklapa. Korisnici ne dobijaju prepisivanje; dobijaju manje pokvarenih rubnih slučajeva.

Spremni da poslovna pravila budu na jednom mjestu?

Recite nam gdje se jezik lomi i kojim pravilima vaš tim više ne vjeruje da će ih kod sprovesti. Predložićemo obim modeliranja koji odgovara proizvodu koji imate. Kontaktirajte nas da prođemo kroz to, ili pogledajte naše usluge domain-driven designa.