36-წუთიანი სამუშაო სესია რთული ბიზნეს-სისტემების მოთვინიერებაზე — საერთო ენიდან და შემოსაზღვრული კონტექსტებიდან, აგრეგატებისა და დომენის მოვლენების გავლით, სტრატეგიულ გადაწყვეტილებამდე: როდის ღირს DDD და როდის იმარჯვებს ჩუმად ჩვეულებრივი CRUD აპლიკაცია.
სისტემების უმეტესობა დატვირთვისგან არ იშლება; ისინი გაუგებრობისგან იშლება. გუნდი წელიწადი სწრაფად უშვებს ფუნქციონალს, შემდეგ კი ყოველი ცვლილება სამ ისეთ რამეს ამტვრევს, რომლებსაც ერთმანეთთან არავინ აკავშირებდა. ძირეული მიზეზი თითქმის ყოველთვის ერთია: კოდმა შეწყვიტა იმის ასახვა, როგორ მუშაობს ბიზნესი სინამდვილეში. DDD ინსტრუმენტების ნაკრებია, რომელიც ამ ორს სინქრონში ინარჩუნებს.
როცა წესები მიმოიფანტება, ყოველი ცვლილება ძებნაა. DDD ბიზნეს-ლოგიკას ერთ, კარგად დასახელებულ მოდელში კრებს.
დეველოპერები და ბიზნესი ერთსა და იმავე რამეს სხვადასხვა სიტყვით უწოდებენ — კოდში „user“, შეხვედრაზე „დაზღვეული“. თარგმანის შეცდომები ჩუმად გროვდება.
ლოგიკა კონტროლერებზე, შეკითხვებსა და ტრიგერებზე იფანტება. ახალი ადამიანის ჩართვას თვეები სჭირდება; პატარა ცვლილებებიც სახიფათოდ გამოიყურება.
ჩადეთ საერთო მოდელსა და ენაში თავიდანვე. ეს მხოლოდ მაშინ ამართლებს, როცა დომენი მართლაც რთულია — ესაა ის პატიოსანი დათქმა, რომელსაც მე-7 ნაწილში დავუბრუნდებით.
DDD-ის ყველაზე დიდი ეფექტის მქონე იდეა დაწყებისას არაფერი ჯდება: შეთანხმდით სიტყვებზე. როცა დომენის ექსპერტი ამბობს „ფასის შეთავაზებას ვადა ეწურება“, კოდში Quote-ს უნდა ჰქონდეს expire() მეთოდი — და არა status ფლაგი, რომელსაც update-ჰენდლერი ცვლის. ერთი სიტყვა, ერთი ცნება, ყველგან.
ბიზნესსა და კოდს შორის ყოველი ჩუმი თარგმანი ადგილია, სადაც ბაგები და გაუგებრობები მრავლდება. საერთო ტერმინი ამ ნაპრალს აქრობს.
class Order { status: string; lines: Line[] } // just a bag of fields // rules live elsewhere, scattered and unguarded: function ship(o: Order) { if (o.status !== "paid") throw Error("not paid") o.status = "shipped" // anyone, anywhere, can flip this }
class Order { private status = "pending" ship() { // the behavior IS the language if (this.status !== "paid") throw new Error("cannot ship an unpaid order") this.status = "shipped" } } // invalid transitions are impossible from outside
ჰგავს გლოსარს, რომელსაც მთელი გუნდი ხელს აწერს: როცა ყველა — გაყიდვები, მხარდაჭერა, ინჟინერია — „გაგზავნილში“ ზუსტად ერთსა და იმავეს გულისხმობს, კამათი სიტყვებზე წყდება და საქმეზე იწყება.
სცადეთ, ერთმა Customer კლასმა ერთდროულად გაყიდვებს, მხარდაჭერასა და ბილინგს მოემსახუროს და მიიღებთ ორმოცველიან მონსტრს, რომელიც სამივესთვის არასწორია. გამოსავალი უფრო დიდი მოდელი არ არის — რამდენიმე უფრო პატარაა, თითოეული სწორია საკუთარ საზღვრებში. სიტყვას მხოლოდ ერთი მნიშვნელობა აქვს თითო კონტექსტში.
„Customer“ თითოეულ კონტექსტში სხვა მოდელია. მათი ერთ საერთო კლასში ჩატენვა კლასიკური შეცდომაა.
როცა რამდენიმე კონტექსტი გაქვთ, ხატავთ, როგორ უკავშირდებიან ისინი ერთმანეთს. კონტექსტების რუკა თქვენი კონტექსტების ზოგადი დიაგრამაა და თითოეულ საზღვარზე არსებული ინტეგრაციის ურთიერთობა — ვინ ვისზეა დამოკიდებული და ვინ თარგმნის.
აქ ხვდება DDD სისტემურ არქიტექტურას: შემოსაზღვრული კონტექსტი ბუნებრივი ნაკერია, რომლის გასწვრივაც სერვისი იყოფა. ამ დაყოფის მექანიკა აქაა: არქიტექტურული პატერნები & სტილები.
კონტექსტების რუკა: გაყიდვები მხარდაჭერისა და ბილინგის upstream-ია. იდენტობა საერთო ბირთვია, რომელზეც ორივე დამოკიდებულია.
პარტნიორობა — ორი გუნდი მჭიდროდ კოორდინირდება და კონტექსტებს ერთი ნაბიჯით ავითარებს. საერთო ბირთვი — ისინი მოდელის პატარა, ერთობლივად ფლობილ ნაწილს იზიარებენ (მაგ. საერთო Money ან UserId). ძლიერია, მაგრამ ძვირი: ყოველ ცვლილებაზე ორივე გუნდი უნდა შეთანხმდეს, ამიტომ საერთო ნაწილი პატარა შეინახეთ.
upstream კონტექსტი აწვდის იმას, რაც downstream-ს სჭირდება (გაყიდვები კვებავს ბილინგს). ეს ჯანსაღი ურთიერთობაა, როცა downstream გუნდის საჭიროებები მოლაპარაკებით ხვდება upstream გუნდის backlog-ში — მომწოდებელი ემსახურება მყიდველს.
როცა upstream არ იხრება — გადახდების პროცესორი, გიგანტური შიდა პლატფორმა — downstream უბრალოდ ეთანხმება მის მოდელს. იაფი და პრაგმატულია, მაგრამ მემკვიდრეობით იღებთ მათ ენას და მათ უცნაურობებს. კარგია ზოგადი საკითხებისთვის; სახიფათოა თქვენი ძირითადი დომენისთვის.
როცა არეულ ან უცხო მოდელთან ინტეგრაცია გიწევთ, მაგრამ უარს ამბობთ, რომ ის შემოგეპაროთ, აგებთ დამცავ შრეს (anti-corruption layer) — თარგმანის საზღვარს, რომელიც მათ ცნებებს თქვენსად აქცევს. მე-6 ნაწილში მას რეალურ კოდს დავუწერთ; ესაა DDD-ის ყველაზე ღირებული თავდაცვითი პატერნი.
შემოსაზღვრული კონტექსტის შიგნით მოდელი სამი სახის ობიექტისგან შედგება. სწორად გაარჩიეთ ისინი და თქვენი ინვარიანტები — წესები, რომლებიც ყოველთვის უნდა სრულდებოდეს — თავად დაიცავენ თავს. არასწორად გაარჩიეთ და უკან ბრუნდებით მიმოფანტულ ვალიდაციასა და დაზიანებულ მონაცემებთან.
აქვს უნიკალური id, რომელიც ცვლილებების მიღმაც რჩება. ორი შეკვეთა იდენტური შიგთავსით მაინც სხვადასხვა შეკვეთაა. მაგალითად Order, Customer, Shipment.
არავითარი იდენტობა, უცვლელი, ურთიერთშენაცვლებადი. ორი Money(5, "USD") ტოლი და ურთიერთშენაცვლებადია. მაგალითად Money, Address, DateRange.
ენტითებისა და value object-ების კლასტერი, რომელსაც ერთ ერთეულად უყურებენ და რომელშიც ერთადერთი შესასვლელი მისი ფესვია. მის შიგნით წესები ერთად რჩება ძალაში.
// VALUE OBJECT — equal by value, immutable class Money { constructor(readonly amount: number, readonly currency: string) {} add(o: Money): Money { // returns a NEW one return new Money(this.amount + o.amount, this.currency) } } // ENTITY — equal by identity, mutable over time class Order { constructor(readonly id: OrderId) {} }
გარე კოდი მხოლოდ ფესვს ესაუბრება. ხაზები და Money შიგნით არის დაცული; სხვა აგრეგატებზე მითითება id-ით ხდება და არა პირდაპირი შენახვით.
ჰგავს საყიდლების კალათას: ნივთებს ამატებთ და შლით კალათის გავლით და არა უცხო ხაზის პირდაპირი რედაქტირებით. კალათა ფესვია; ის ინახავს ჯამს პატიოსნად.
ბიზნესი მოვლენებით აზროვნებს: შეკვეთა განთავსდა, გადახდა მიღებულ იქნა, გზავნილი გაიგზავნა. ამ ფაქტების პირველხარისხოვნად ქცევა მოდელში — როგორც დომენის მოვლენების — აგრეგატებს პატარას ინარჩუნებს და კონტექსტებს სუსტად დაკავშირებულს, რადგან რეაქციები იმ აგრეგატიდან გადის, რომელმაც ისინი გამოიწვია.
OrderPlaced, PaymentReceived). ცვლილების მფლობელი აგრეგატი მას იწერს; სისტემის სხვა ნაწილები ეწერებიან და რეაგირებენ — ხშირად სხვა შემოსაზღვრულ კონტექსტში.// a fact: past tense, immutable, named in the language class OrderPlaced { constructor( readonly orderId: OrderId, readonly placedAt: Date, ) {} } // the aggregate root records it as part of placing: order.place() // → emits OrderPlaced // Billing, Email, Analytics each react on their own.
აგრეგატი ფაქტს იწერს; გამომწერები დამოუკიდებლად რეაგირებენ. ტრანსპორტი — ბროკერი — ცალკე საკითხია.
დომენის მოვლენები მოდელირების იდეაა; ის, თუ როგორ მოძრაობენ ისინი სერვისებს შორის — ბროკერები, დანაწილებები, მიწოდების გარანტიები — შეტყობინებების რიგებისა & ნაკადების თემაა. ერთი პროცესის შიგნით დომენის მოვლენა უბრალოდ მეხსიერებაში არსებული შეტყობინებაც შეიძლება იყოს.
Event storming სწრაფი, დაბალტექნოლოგიური ვორქშოპია: შეკრიბეთ დომენის ექსპერტები და დეველოპერები კედელთან და დახატეთ ბიზნესი მოვლენების ქრონოლოგიად სტიკერებზე. ნარინჯისფერი სტიკერები მოვლენებია; ლურჯი — ბრძანებები, რომლებიც მათ იწვევს; ყვითელი — აგრეგატები, რომლებზეც ისინი ეშვება; ვარდისფერი — გარე სისტემები. ერთ შუადღეში აღმოაჩენთ რეალურ პროცესს, ენას და — რაც მთავარია — სად გადის შემოსაზღვრული კონტექსტების ნაკერები.
ბრძანება → აგრეგატი → მოვლენა, მარცხნიდან მარჯვნივ ქრონოლოგიაზე. ფერები სტიკერის ტიპებს შეესაბამება; ხარვეზები კონტექსტების საზღვრებს ავლენს.
სტრატეგიული დიზაინი ხაზავს საზღვრებს; ტაქტიკური პატერნები მათ ახორციელებს. სამი მათგანი თითქმის ყოველ DDD პროექტში ამართლებს — ისინი აკავებენ შენახვას, აგრეგატებს შორის ლოგიკასა და უცხო მოდელებს, რომ თქვენს სუფთა დომენში არ შემოიჭრან.
კოლექციის მსგავსი ინტერფეისი, რომელიც აგრეგატს ერთ ერთეულად ტვირთავს და ინახავს. დომენი ლაპარაკობს OrderRepo-ზე — არასდროს SQL-ზე ან ORM-ზე.
როცა წესი რამდენიმე აგრეგატს მოიცავს და არცერთს ეკუთვნის, მოათავსეთ ის მდგომარეობის გარეშე დომენის სერვისში — საერთო ენით დასახელებულში და არა ნაგვის უჯრა Manager-ში.
საზღვარი, რომელიც უცხო ან ძველ მოდელს თქვენსად აქცევს, რომ მათმა ცნებებმა თქვენი დომენი ვერასდროს დააბინძუროს.
interface OrderRepo { // a collection-like illusion findById(id: OrderId): Order save(order: Order): void // the whole aggregate } // domain code depends on OrderRepo — not on SQL. // SqlOrderRepo / InMemoryOrderRepo implement it, // so the model stays testable and persistence-agnostic.
დამცავი შრე ძველი CRM-ის ფორმას სუფთა დომენურ ტერმინებად აქცევს — მისი უცნაურობები საზღვარს ვერასდროს კვეთს.
რეპოზიტორია არის დამოკიდებულებების ინვერსია, გამოყენებული მონაცემთა შრეზე; ACL კი Adapter პატერნია, რომელიც მთელ კონტექსტს იცავს. DDD იმავე კლასის დონის ინსტრუმენტებს იყენებს, რომლებიც უკვე იცით — უბრალოდ დომენისკენ მიმართავს მათ.
DDD არის მოდელირების დისციპლინა და არა ბიბლიოთეკა, რომელსაც აიმპორტებთ. ეს არის ადგილები, სადაც ის თქვენს დანარჩენ ინსტრუმენტებს ეხება — აირჩიეთ იმის მიხედვით, რაც მართლა გჭირდებათ, და გახსოვდეთ: პროექტების უმეტესობას მძიმე ინსტრუმენტები საერთოდ არ სჭირდება.
რეალური საქმე საუბარია. ესენი უბრალოდ ვორქშოპს ზედაპირს აძლევს.
DDD-ის საზღვრები სუფთად ეხმიანება სისტემის დონის ფორმებს — მექანიკა ამ დეკებშია.
შემოსაზღვრული კონტექსტი ბუნებრივი ნაკერია, რომლის გასწვრივაც მიკროსერვისი იყოფა — ენა და მფლობელობა ერთად იცვლება.
დომენის მოვლენები ხდება ინტეგრაციის მოვლენები, რომლებიც კონტექსტებს შეტყობინებების ბროკერით კვეთს.
ისეთი ინსტრუმენტები, როგორიცაა Axon (JVM) ან event-store მონაცემთა ბაზა, ეხმარება event sourcing-სა & CQRS-ს — მაგრამ ისინი იმპლემენტაციის არჩევანია და არასდროს წინაპირობა.
DDD-ის ყველაზე მაღალი დონის უნარი აგრეგატები არ არის — ეს არის ცოდნა იმისა, სისტემის რომელი ნაწილები იმსახურებს სრულ მკურნალობას და რომელი უნდა დარჩეს მოსაწყენ CRUD ფორმად. ეს გადაწყვეტილება სტრატეგიული დიზაინია და ხანდახან პასუხია: „აქ DDD საერთოდ არ გამოიყენოთ“.
ის ნაწილი, რომელიც ბიზნესს განსაკუთრებულს ხდის — ფასწარმოქმნის ძრავა, დამთხვევის ალგორითმი. აქ ჩადეთ საუკეთესო ადამიანები და სრული DDD.
საჭიროა, რომ ბირთვმა იმუშაოს, მაგრამ არ გამოგარჩევთ. დაამოდელირეთ მსუბუქად — საკმარისი სტრუქტურა, ზედმეტი გაპრიალების გარეშე.
ავთენტიფიკაცია, შეტყობინებები, ბილინგის რელსები. იყიდეთ ან აიღეთ მზა გადაწყვეტა — ნუ დაამოდელირებთ ხელით და სიყვარულით იმას, რასაც SaaS უკვე აკეთებს.
ხუთი სწრაფი კითხვა ენაზე, კონტექსტებზე, აგრეგატებზე, მოვლენებსა და სტრატეგიულ გადაწყვეტილებაზე — მყისიერი პასუხი, ავტორიზაციის გარეშე.
ნავიგაცია ← → ღილაკებით ან სქროლით · უკან ბიბლიოთეკაში