مواجهه با منطق پیچیدهٔ کسب‌وکار | ترجمه Learning Domain-Driven Design

مواجهه با منطق پیچیدهٔ کسب‌وکار

مواجهه با منطق پیچیدهٔ کسب‌وکار

عنوان اصلی: Tackling Complex Business Logic
کتاب: Learning Domain-Driven Design
نویسنده: Vlad Khononov
زبان اصلی: انگلیسی
NewsID: 2353
بازهٔ PDF: 101–124
ترجمه: ترجمه با کمک هوش مصنوعی
تاریخ ترجمه: ۱۴۰۵/۰۵/۱۹ — 2026-08-10

فصل ۶ — مواجهه با منطق پیچیدهٔ کسب‌وکار

فصل قبل دو الگو برای منطق نسبتاً سادهٔ کسب‌وکار را بررسی کرد: Transaction Script و Active Record. این فصل موضوع پیاده‌سازی منطق کسب‌وکار را ادامه می‌دهد و الگویی را معرفی می‌کند که برای منطق پیچیده طراحی شده است: مدل دامنه (Domain Model).

تاریخچه

مانند Transaction Script و Active Record، الگوی Domain Model نخست در کتاب Martin Fowler با عنوان Patterns of Enterprise Application Architecture معرفی شد. Fowler بحث خود را با این جمله به پایان رساند که «Eric Evans در حال نوشتن کتابی دربارهٔ ساخت Domain Modelهاست.» منظور همان اثر بنیادین Evans، Domain-Driven Design: Tackling Complexity in the Heart of Software، بود.

Evans در کتاب خود مجموعه‌ای از الگوها ارائه می‌کند که هدفشان پیوند نزدیک کد با مدل زیربنایی دامنهٔ کسب‌وکار است: Aggregate، Value Object، Repository و موارد دیگر. این الگوها ادامهٔ طبیعی جایی هستند که Fowler بحث را متوقف کرد و مجموعه ابزار مؤثری برای پیاده‌سازی Domain Model فراهم می‌کنند.

الگوهایی که Evans معرفی کرد معمولاً «طراحی تاکتیکی DDD» نامیده می‌شوند. برای پرهیز از این برداشت اشتباه که اجرای DDD حتماً به استفاده از همهٔ این الگوها برای منطق کسب‌وکار نیاز دارد، نویسنده ترجیح می‌دهد از اصطلاح اصلی Fowler استفاده کند: خود الگو Domain Model است و Aggregateها و Value Objectها بلوک‌های سازندهٔ آن هستند.

مدل دامنه

الگوی Domain Model برای موقعیت‌هایی با منطق کسب‌وکار پیچیده ساخته شده است. در اینجا به‌جای رابط‌های CRUD، با گذارهای پیچیدهٔ وضعیت، قواعد کسب‌وکار و ناورداها (Invariants) سروکار داریم؛ قواعدی که باید در تمام زمان‌ها حفظ شوند.

فرض کنید یک سامانهٔ Help Desk پیاده‌سازی می‌کنیم. بخشی از نیازمندی‌های مدیریت چرخهٔ عمر تیکت‌های پشتیبانی چنین است:

  • مشتریان تیکت‌های پشتیبانی را برای شرح مشکلات خود باز می‌کنند.
  • هم مشتری و هم کارشناس پشتیبانی پیام اضافه می‌کنند و تمام مکاتبات توسط تیکت ردیابی می‌شود.
  • هر تیکت اولویت Low، Medium، High یا Urgent دارد.
  • کارشناس باید در مهلت مشخص SLA که بر اساس اولویت تیکت تعیین می‌شود راه‌حل ارائه کند.
  • اگر کارشناس در SLA پاسخ ندهد، مشتری می‌تواند تیکت را به مدیر کارشناس Escalate کند.
  • Escalation مهلت پاسخ کارشناس را ۳۳٪ کاهش می‌دهد.
  • اگر کارشناس یک تیکت Escalate‌شده را تا زمانی که ۵۰٪ از مهلت پاسخ باقی مانده باز نکرده باشد، تیکت خودکار به کارشناس دیگری تخصیص داده می‌شود.
  • اگر مشتری تا هفت روز به پرسش‌های کارشناس پاسخ ندهد، تیکت به‌طور خودکار بسته می‌شود.
  • تیکت‌های Escalate‌شده را نه سامانه و نه خود کارشناس نمی‌توانند خودکار ببندند؛ فقط مشتری یا مدیر کارشناس می‌تواند آنها را ببندد.
  • مشتری فقط زمانی می‌تواند یک تیکت بسته‌شده را دوباره باز کند که از بسته‌شدن آن بیش از هفت روز نگذشته باشد.

این نیازمندی‌ها شبکه‌ای درهم‌تنیده از وابستگی‌ها میان قواعد مختلف می‌سازند که همگی بر منطق چرخهٔ عمر تیکت اثر می‌گذارند. این دیگر یک صفحهٔ CRUD ساده نیست. اگر این منطق را با Active Record پیاده‌سازی کنیم، تکرار منطق و نقض برخی قواعد کسب‌وکار بسیار آسان خواهد شد و در نتیجه State سامانه می‌تواند خراب شود.

پیاده‌سازی

Domain Model یک مدل شیئی از دامنه است که هم رفتار و هم داده را در خود دارد. الگوهای تاکتیکی DDD ــ Aggregateها، Value Objectها، Domain Eventها و Domain Serviceها ــ بلوک‌های سازندهٔ چنین مدل شیئی‌ای هستند.

همهٔ این الگوها یک مضمون مشترک دارند: منطق کسب‌وکار را در اولویت قرار می‌دهند.

پیچیدگی

منطق کسب‌وکار دامنه ذاتاً پیچیده است؛ بنابراین اشیایی که آن را مدل می‌کنند نباید پیچیدگی تصادفی دیگری وارد کنند. مدل باید از دغدغه‌های زیرساختی یا فناورانه، مثل فراخوانی پایگاه داده یا اجزای خارجی سیستم، خالی باشد. در نتیجه اشیای مدل باید Plain Old Objects باشند: منطق کسب‌وکار را بدون وابستگی مستقیم به Framework یا زیرساخت پیاده‌سازی کنند.

زبان فراگیر

تمرکز بر منطق کسب‌وکار به‌جای دغدغه‌های فنی باعث می‌شود اشیای Domain Model آسان‌تر از اصطلاحات زبان فراگیر زمینهٔ محدود پیروی کنند. به بیان دیگر، کد می‌تواند «زبان فراگیر را صحبت کند» و با مدل ذهنی خبرگان دامنه هم‌راستا شود.

بلوک‌های سازنده

اکنون بلوک‌های اصلی Domain Model یا الگوهای تاکتیکی DDD را بررسی می‌کنیم: Value Objectها، Aggregateها و Domain Serviceها.

شیء مقدار (Value Object)

Value Object شیئی است که با ترکیب مقادیرش شناسایی می‌شود. برای مثال شیء رنگ زیر را در نظر بگیرید:

class Color
{
 int _red;
 int _green;
 int _blue;
}

ترکیب سه مقدار red، green و blue یک رنگ را تعریف می‌کند. تغییر هرکدام، رنگ جدیدی می‌سازد. دو رنگ با مقادیر یکسان در واقع همان رنگ‌اند؛ بنابراین برای شناسایی رنگ نیازی به فیلد شناسهٔ صریح نیست.

فیلد ColorId در شکل ۶-۱ نه‌تنها زائد است بلکه می‌تواند منبع Bug باشد: ممکن است دو ردیف با RGB یکسان اما ColorId متفاوت داشته باشیم و مقایسهٔ شناسه‌ها نتواند نشان دهد که هر دو یک رنگ را بازنمایی می‌کنند.

شکل ۶-۱ — فیلد زائد ColorId که امکان وجود دو ردیف با مقادیر یکسان را فراهم می‌کند.

زبان فراگیر

تکیهٔ صرف بر انواع ابتدایی کتابخانهٔ زبان، مانند String، Integer یا Dictionary، برای نمایش مفاهیم دامنه «Primitive Obsession» نامیده می‌شود. کلاس زیر را در نظر بگیرید:

class Person
{
 private int _id;
 private string _firstName;
 private string _lastName;
 private string _landlinePhone;
 private string _mobilePhone;
 private string _email;
 private int _heightMetric;
 private string _countryCode;
 public Person(...) {...}
}
static void Main(string[] args)
{
 var dave = new Person(
  id: 30217,
  firstName: "Dave",
  lastName: "Ancelovici",
  landlinePhone: "023745001",
  mobilePhone: "0873712503",
  email: "dave@learning-ddd.com",
  heightMetric: 180,
  countryCode: "BG");
}

در این پیاده‌سازی بیشتر مقادیر از نوع String هستند و معنای آنها بر اساس قرارداد ذهنی تعیین می‌شود. مثلاً landlinePhone باید شمارهٔ تلفن ثابت معتبر باشد و countryCode باید کد دوحرفی بزرگ‌شدهٔ کشور باشد. سامانه نمی‌تواند همیشه به ورودی کاربر اعتماد کند، بنابراین کلاس مجبور است تمام ورودی‌ها را اعتبارسنجی کند.

این رویکرد چند ریسک طراحی دارد: منطق Validation تکرار می‌شود؛ تضمین اینکه پیش از استفاده حتماً Validation فراخوانی شده دشوار است؛ و با بزرگ‌شدن Codebase و ورود مهندسان دیگر، کنترل آن دشوارتر می‌شود.

حالا طراحی همان شیء با Value Objectها را مقایسه کنید:

class Person {
 private PersonId _id;
 private Name _name;
 private PhoneNumber _landline;
 private PhoneNumber _mobile;
 private EmailAddress _email;
 private Height _height;
 private CountryCode _country;
 public Person(...) { ... }
}
static void Main(string[] args)
{
 var dave = new Person(
  id: new PersonId(30217),
  name: new Name("Dave", "Ancelovici"),
  landline: PhoneNumber.Parse("023745001"),
  mobile: PhoneNumber.Parse("0873712503"),
  email: Email.Parse("dave@learning-ddd.com"),
  height: Height.FromMetric(180),
  country: CountryCode.Parse("BG"));
}

نخست، وضوح افزایش یافته است. مثلاً برای متغیر country لازم نیست نام طولانی countryCode استفاده شود تا معلوم شود کد کشور است نه نام کامل کشور؛ خود نوع Value Object قصد را روشن می‌کند.

دوم، پیش از Assignment نیازی به Validation جداگانه نیست، چون منطق اعتبارسنجی در خود Value Object قرار دارد. اما رفتار Value Object فقط Validation نیست. ارزش واقعی آن زمانی آشکار می‌شود که منطق کسب‌وکاری مربوط به دستکاری مقدار را متمرکز کند. منطق منسجم در یک مکان قرار می‌گیرد و به‌سادگی Test می‌شود. مهم‌تر از همه، Value Object مفاهیم دامنه را بیان می‌کند و کد زبان فراگیر را به کار می‌گیرد.

مثلاً Value Object مربوط به قد، علاوه بر روشن‌کردن قصد، اندازه‌گیری را از واحد خاص جدا می‌کند و می‌تواند با واحد متریک یا امپریال مقداردهی شود، تبدیل انجام دهد، نمایش متنی بسازد و مقادیر را مقایسه کند:

var heightMetric = Height.Metric(180);
var heightImperial = Height.Imperial(5, 3);
var string1 = heightMetric.ToString(); // "180cm"
var string2 = heightImperial.ToString(); // "5 feet 3 inches"
var string3 = heightMetric.ToImperial().ToString(); // "5 feet 11 inches"
var firstIsHigher = heightMetric > heightImperial; // true

Value Object مربوط به PhoneNumber می‌تواند Parse و Validation و استخراج کشور و نوع شماره را کپسوله کند:

var phone = PhoneNumber.Parse("+359877123503");
var country = phone.Country; // "BG"
var phoneType = phone.PhoneType; // "MOBILE"
var isValid = PhoneNumber.IsValid("+972120266680"); // false

نمونهٔ زیر قدرت Value Object را وقتی همهٔ منطق دستکاری داده و تولید نمونه‌های جدید را در خود نگه می‌دارد نشان می‌دهد:

var red = Color.FromRGB(255, 0, 0);
var green = Color.Green;
var yellow = red.MixWith(green);
var yellowString = yellow.ToString(); // "#FFFF00"

Value Objectها نیاز به قراردادهای ذهنی مانند «این String ایمیل است و آن یکی شماره تلفن» را حذف و Object Model را شهودی‌تر و کم‌خطاتر می‌کنند.

پیاده‌سازی Value Object

از آنجا که تغییر هر فیلد یک Value Object از نظر مفهومی مقدار جدیدی ایجاد می‌کند، Value Objectها به‌صورت Immutable پیاده‌سازی می‌شوند. عملیاتی مثل MixWith شیء اصلی را تغییر نمی‌دهد، بلکه نمونهٔ جدیدی بازمی‌گرداند:

public class Color
{
 public readonly byte Red;
 public readonly byte Green;
 public readonly byte Blue;
 public Color(byte r, byte g, byte b)
 {
  this.Red = r;
  this.Green = g;
  this.Blue = b;
 }
 public Color MixWith(Color other)
 {
  return new Color(
   r: (byte) Math.Min(this.Red + other.Red, 255),
   g: (byte) Math.Min(this.Green + other.Green, 255),
   b: (byte) Math.Min(this.Blue + other.Blue, 255)
  );
 }
 ...
}

چون برابری Value Object بر اساس مقادیر است نه ID یا Reference، باید Equality به‌درستی پیاده‌سازی شود. برای مثال در C#:

public class Color
{
 ...
 public override bool Equals(object obj)
 {
  var other = obj as Color;
  return other != null &&
   this.Red == other.Red &&
   this.Green == other.Green &&
   this.Blue == other.Blue;
 }
 public static bool operator == (Color lhs, Color rhs)
 {
  if (Object.ReferenceEquals(lhs, null)) {
   return Object.ReferenceEquals(rhs, null);
  }
  return lhs.Equals(rhs);
 }
 public static bool operator != (Color lhs, Color rhs)
 {
  return !(lhs == rhs);
 }
 public override int GetHashCode()
 {
  return ToString().GetHashCode();
 }
 ...
}

جالب است که با وجود اینکه استفاده از String عمومی برای ارزش‌های دامنه‌ای با ایدهٔ Value Object تضاد دارد، String در .NET و Java و زبان‌های دیگر دقیقاً مانند Value Object پیاده‌سازی شده است: Immutable است و عملیات آن نمونهٔ جدید می‌سازد؛ همچنین رفتار غنی برای Trim، Concatenate، Replace، Substring و غیره دارد.

چه زمانی از Value Object استفاده کنیم؟

پاسخ ساده: هر زمان که می‌توانید. Value Objectها علاوه بر بیانگرتر کردن کد و متمرکزکردن منطق کسب‌وکاری پراکنده، کد را امن‌تر می‌کنند. به‌دلیل Immutable بودن، رفتارشان Side Effect ندارد و Thread-safe است.

از دید دامنه، قاعدهٔ خوبی این است که برای عناصر دامنه که ویژگی اشیای دیگر را توصیف می‌کنند Value Object بسازید. در مثال Person، شناسه، نام، شماره‌ها، ایمیل و قد همگی Value Object هستند. وضعیت‌ها، Passwordها و مفاهیم دامنه‌ای دیگری که با مقدارشان شناسایی می‌شوند نیز کاندیدا هستند. یکی از مهم‌ترین موارد، پول و مقادیر مالی است. استفاده از Primitiveها برای پول هم کپسوله‌سازی منطق مالی را محدود می‌کند و هم می‌تواند خطاهای خطرناکی مانند Rounding و مشکلات Precision ایجاد کند.

موجودیت‌ها (Entities)

Entity نقطهٔ مقابل Value Object است: برای تمایز نمونه‌های مختلف به یک فیلد شناسهٔ صریح نیاز دارد. Person نمونهٔ ساده‌ای است. کلاس زیر فقط نام دارد:

class Person
{
 public Name Name { get; set; }
 public Person(Name name)
 {
  this.Name = name;
 }
}

این طراحی ناقص است، چون افراد مختلف می‌توانند دقیقاً هم‌نام باشند ولی همان شخص نیستند. بنابراین باید شناسه اضافه شود:

class Person
{
 public readonly PersonId Id;
 public Name Name { get; set; }
 public Person(PersonId id, Name name)
 {
  this.Id = id;
  this.Name = name;
 }
}

فیلد Id از نوع PersonId خود یک Value Object است و می‌تواند بر اساس نیاز دامنه، GUID، عدد، String یا حتی مقدار دامنه‌ای مثل Social Security Number باشد.

الزام اصلی شناسه، یکتا بودن آن برای هر نمونهٔ Entity است؛ شکل ۶-۲. به‌جز استثناهای نادر، مقدار شناسه باید در طول چرخهٔ عمر Entity تغییرناپذیر بماند. این موضوع ما را به تفاوت مفهومی دیگر می‌رساند: برخلاف Value Objectها، Entityها Immutable نیستند و انتظار می‌رود در طول زمان تغییر کنند. همچنین Value Objectها ویژگی‌های Entity را توصیف می‌کنند.

شکل ۶-۲ — شناسهٔ صریح اجازه می‌دهد نمونه‌ها حتی با مقادیر یکسان سایر فیلدها از هم متمایز شوند.

Entityها بلوک بنیادی هر دامنهٔ کسب‌وکارند، اما نویسنده آنها را مستقل در فهرست بلوک‌های Domain Model نیاورد، چون Entityها مستقل پیاده‌سازی نمی‌شوند؛ فقط در زمینهٔ الگوی Aggregate استفاده می‌شوند.

تجمیع‌ها (Aggregates)

Aggregate یک Entity است: شناسهٔ صریح دارد و State آن در چرخهٔ عمر تغییر می‌کند. اما بسیار فراتر از Entity ساده است. هدف الگو، حفاظت از سازگاری داده است. چون دادهٔ Aggregate Mutable است، الگو باید چالش‌های ناشی از تغییر را حل کند تا State همیشه معتبر باقی بماند.

اجرای سازگاری

قابل‌تغییر بودن State راه‌های زیادی برای فساد داده ایجاد می‌کند. Aggregate مرز روشنی میان خود و محیط بیرونی می‌کشد و به یک مرز تضمین سازگاری تبدیل می‌شود. منطق Aggregate باید تمام تغییرات ورودی را اعتبارسنجی کند و مطمئن شود هیچ تغییری با قواعد کسب‌وکار تناقض ندارد.

از دید پیاده‌سازی، فقط منطق کسب‌وکار خود Aggregate اجازهٔ تغییر State را دارد. اشیا و فرایندهای بیرونی فقط می‌توانند State را بخوانند؛ تغییر State باید از طریق متدهای رابط عمومی Aggregate انجام شود.

متدهای تغییردهندهٔ State که در رابط عمومی Aggregate قرار می‌گیرند معمولاً Command نامیده می‌شوند. Command می‌تواند یک متد عمومی عادی باشد:

public class Ticket
{
 ...
 public void AddMessage(UserId from, string body)
 {
  var message = new Message(from, body);
  _messages.Append(message);
 }
 ...
}

یا به شکل Parameter Object که تمام ورودی لازم را کپسوله می‌کند:

public class Ticket
{
 ...
 public void Execute(AddMessage cmd)
 {
  var message = new Message(cmd.from, cmd.body);
  _messages.Append(message);
 }
 ...
}

نحوهٔ بیان Command سلیقه‌ای است؛ نویسنده روش صریح‌تر با ساختارهای Command و ارسال Polymorphic آنها به Execute را ترجیح می‌دهد.

رابط عمومی Aggregate مسئول Validation ورودی و اجرای تمام قواعد و Invariantهای مرتبط است. این مرز سخت تضمین می‌کند که تمام منطق مربوط به Aggregate در یک مکان، یعنی خود Aggregate، باقی بماند.

بنابراین Application Layer که عملیات Aggregate را هماهنگ می‌کند ساده است: State فعلی را Load می‌کند، Action را اجرا می‌کند، State تغییرکرده را ذخیره و نتیجه را بازمی‌گرداند:

01 public ExecutionResult Escalate(TicketId id, EscalationReason reason)
02 {
03 try
04 {
05 var ticket = _ticketRepository.Load(id);
06 var cmd = new Escalate(reason);
07 ticket.Execute(cmd);
08 _ticketRepository.Save(ticket);
09 return ExecutionResult.Success();
10 }
11 catch (ConcurrencyException ex)
12 {
13 return ExecutionResult.Error(ex);
14 }
15 }

کنترل Concurrency در خط ۱۱ حیاتی است. اگر چند Process هم‌زمان همان Aggregate را Update کنند، نباید تراکنش دوم کورکورانه تغییر Commit‌شدهٔ اول را بازنویسی کند. Process دوم باید بفهمد State مبنای تصمیمش قدیمی شده و عملیات را Retry کند.

پس ذخیره‌ساز Aggregate باید مدیریت هم‌زمانی را پشتیبانی کند. در ساده‌ترین حالت Aggregate فیلد Version دارد که پس از هر Update افزایش می‌یابد:

class Ticket
{
 TicketId _id;
 int _version;
 ...
}

هنگام Commit باید مطمئن شویم Version بازنویسی‌شونده همان مقداری است که ابتدا خوانده‌ایم. نمونهٔ SQL:

01 UPDATE tickets
02 SET ticket_status = @new_status,
03     agg_version = agg_version + 1
04 WHERE ticket_id=@id and agg_version=@expected_version;

این دستور State را تغییر می‌دهد و Version را افزایش می‌دهد، اما فقط وقتی Version فعلی همان مقدار مورد انتظار باشد. مدیریت Concurrency می‌تواند خارج از پایگاه دادهٔ رابطه‌ای نیز انجام شود و Document Databaseها اغلب برای Aggregateها مناسب‌اند؛ نکتهٔ مهم آن است که ذخیره‌ساز از Concurrency Management پشتیبانی کند.

مرز تراکنش

چون State Aggregate فقط توسط منطق خودش تغییر می‌کند، Aggregate همچنین یک مرز تراکنش است. همهٔ تغییرات State باید در یک عملیات اتمیک Commit شوند: یا همه یا هیچ.

هیچ عملیات سیستم نباید به تراکنشی متکی باشد که چند Aggregate را هم‌زمان Commit کند. تغییر State هر Aggregate باید جداگانه، یک Aggregate در هر تراکنش پایگاه داده، Commit شود.

قانون «یک نمونهٔ Aggregate در هر تراکنش» ما را مجبور می‌کند مرز Aggregate را دقیق طراحی کنیم تا Invariantها و قواعد دامنه رعایت شوند. نیاز مستمر به Commit هم‌زمان چند Aggregate علامت مرز تراکنشی و در نتیجه مرز Aggregate نادرست است.

سلسله‌مراتب Entityها

سناریوهایی وجود دارد که چند شیء باید یک مرز تراکنشی مشترک داشته باشند؛ مثلاً وقتی هم‌زمان تغییر می‌کنند یا قاعدهٔ یکی به State دیگری وابسته است. DDD می‌گوید طراحی باید توسط دامنه هدایت شود. Aggregate برای چنین حالتی یک سلسله‌مراتب از Entityها می‌سازد که همه سازگاری تراکنشی مشترک دارند؛ شکل ۶-۳.

شکل ۶-۳ — Aggregate به‌عنوان سلسله‌مراتب Entityها.

این سلسله‌مراتب هم Entity و هم Value Object دارد و همه زمانی در یک Aggregate قرار می‌گیرند که منطق کسب‌وکار دامنه آنها را به یک مرز تراکنش واحد پیوند دهد. نام Aggregate نیز از همین «تجمیع» موجودیت‌ها و Value Objectهای یک مرز تراکنش می‌آید.

کد زیر قاعده‌ای را نشان می‌دهد که چند Entity در Aggregate را درگیر می‌کند: «اگر Agent تا زمانی که ۵۰٪ مهلت پاسخ باقی مانده یک تیکت Escalate‌شده را باز نکرده باشد، تیکت خودکار به Agent دیگری تخصیص داده شود»:

01 public class Ticket
02 {
03 ...
04 List<Message> _messages;
05 ...
06
07 public void Execute(EvaluateAutomaticActions cmd)
08 {
09 if (this.IsEscalated && this.RemainingTimePercentage < 0.5 &&
10     GetUnreadMessagesCount(for: AssignedAgent) > 0)
11 {
12     _agent = AssignNewAgent();
13 }
14 }
15
16 public int GetUnreadMessagesCount(UserId id)
17 {
18 return _messages.Where(x => x.To == id && !x.WasRead).Count();
19 }
20
21 ...
22 }

متد بررسی می‌کند تیکت Escalate شده باشد، زمان باقی‌مانده کمتر از ۵۰٪ باشد و پیام خوانده‌نشده برای Agent فعلی وجود داشته باشد. اگر همهٔ شرایط برقرار باشند، تیکت به Agent دیگری واگذار می‌شود. Aggregate تضمین می‌کند این شروط روی دادهٔ strongly consistent بررسی شوند و پس از بررسی، داده در میانهٔ عملیات تغییر نکند؛ چون همهٔ تغییرات Aggregate در یک تراکنش اتمیک انجام می‌شوند.

ارجاع به Aggregateهای دیگر

چون تمام اشیای درون Aggregate یک مرز تراکنشی مشترک دارند، بزرگ‌شدن بیش از حد Aggregate مشکلات Performance و Scalability ایجاد می‌کند.

سازگاری داده معیار خوبی برای تعیین مرز Aggregate است. فقط اطلاعاتی که منطق کسب‌وکار برای آنها Strong Consistency لازم دارد باید داخل Aggregate باشند. اطلاعاتی که می‌توانند Eventually Consistent باشند باید بیرون از مرز Aggregate، مثلاً در Aggregate دیگری، قرار گیرند؛ شکل ۶-۴.

شکل ۶-۴ — Aggregate به‌عنوان مرز سازگاری.

قاعدهٔ عملی: Aggregateها را تا حد ممکن کوچک نگه دارید و فقط اشیایی را داخل آنها قرار دهید که منطق کسب‌وکار واقعاً به State کاملاً سازگارشان نیاز دارد:

public class Ticket
{
 private UserId _customer;
 private List<ProductId> _products;
 private UserId _assignedAgent;
 private List<Message> _messages;
 ...
}

در این مثال، پیام‌ها داخل مرز Ticket Aggregate هستند. در مقابل، Customer، Products و Assigned Agent بیرون‌اند و فقط با ID ارجاع داده می‌شوند. ارجاع به Aggregate خارجی از طریق ID صریح می‌کند که آن شیء عضو مرز Aggregate فعلی نیست و هر Aggregate مرز تراکنش خودش را دارد.

برای تصمیم‌گیری دربارهٔ عضویت یک Entity در Aggregate بررسی کنید آیا منطق Aggregate در صورت کار روی دادهٔ Eventually Consistent ممکن است State نامعتبر بسازد. در مثال واگذاری تیکت، اگر وضعیت «خوانده/نخوانده» پیام‌ها با تأخیر به‌روز شود، ممکن است تعداد زیادی تیکت بی‌دلیل واگذار شوند. بنابراین دادهٔ پیام باید در مرز Aggregate باشد.

ریشهٔ Aggregate

State Aggregate فقط با Commandهای خودش تغییر می‌کند. چون Aggregate سلسله‌مراتبی از Entityهاست، فقط یکی از آنها باید به‌عنوان رابط عمومی تعیین شود: Aggregate Root؛ شکل ۶-۵.

شکل ۶-۵ — ریشهٔ Aggregate.

نمونهٔ زیر نشان می‌دهد حتی وقتی Entity داخلی Message تغییر می‌کند، عملیات فقط از طریق ریشهٔ Ticket قابل دسترسی است:

public class Ticket
{
 ...
 List<Message> _messages;
 ...
 public void Execute(AcknowledgeMessage cmd)
 {
  var message = _messages.Where(x => x.Id == cmd.id).First();
  message.WasRead = true;
 }
 ...
}

رویدادهای دامنه (Domain Events)

علاوه بر رابط عمومی Aggregate Root، سازوکار دیگری برای ارتباط جهان بیرون با Aggregate وجود دارد: Domain Event.

Domain Event پیامی است که یک رویداد مهمِ رخ‌داده در دامنهٔ کسب‌وکار را توصیف می‌کند، مانند:

  • Ticket assigned
  • Ticket escalated
  • Message received

چون Domain Event چیزی را توصیف می‌کند که قبلاً رخ داده، نام آن باید در زمان گذشته باشد.

هدف رویداد دامنه این است که آنچه در کسب‌وکار رخ داده و داده‌های لازم مرتبط با آن را منتقل کند. نمونه:

{
 "ticket-id": "c9d286ff-3bca-4f57-94d4-4d4e490867d1",
 "event-id": 146,
 "event-type": "ticket-escalated",
 "escalation-reason": "missed-sla",
 "escalation-time": 1628970815
}

نام‌گذاری مهم است؛ نام Domain Event باید کوتاه و دقیقاً بازتاب‌دهندهٔ اتفاق کسب‌وکاری باشد.

Domain Event بخشی از رابط عمومی Aggregate است. Aggregate رویدادهای خود را منتشر می‌کند و Processها، Aggregateهای دیگر یا حتی سیستم‌های خارجی می‌توانند Subscribe کنند و در پاسخ منطق خود را اجرا کنند؛ شکل ۶-۶.

شکل ۶-۶ — جریان انتشار Domain Event.

در نمونهٔ زیر رویداد جدید ساخته و به مجموعهٔ رویدادهای Ticket اضافه می‌شود:

01 public class Ticket
02 {
03 ...
04 private List<DomainEvent> _domainEvents;
05 ...
06
07 public void Execute(RequestEscalation cmd)
08 {
09 if (!this.IsEscalated && this.RemainingTimePercentage <= 0)
10 {
11 this.IsEscalated = true;
12 var escalatedEvent = new TicketEscalated(_id, cmd.Reason);
13 _domainEvents.Append(escalatedEvent);
14 }
15 }
16
17 ...
18 }

در فصل ۹ روش انتشار قابل اتکای Domain Eventها به مشترکان بررسی می‌شود.

زبان فراگیر و Aggregate

Aggregateها نیز باید زبان فراگیر را بازتاب دهند. نام Aggregate، اعضای داده‌ای، Actionها و Domain Eventها همگی باید با زبان فراگیر همان زمینهٔ محدود بیان شوند. به تعبیر Eric Evans، کد باید بر همان زبانی بنا شود که توسعه‌دهندگان هنگام صحبت با یکدیگر و خبرگان دامنه استفاده می‌کنند. این موضوع به‌ویژه برای منطق پیچیده اهمیت دارد.

سرویس‌های دامنه (Domain Services)

دیر یا زود با منطقی روبه‌رو می‌شوید که نه به Aggregate خاصی تعلق دارد و نه به Value Object، یا ظاهراً به چند Aggregate مرتبط است. DDD پیشنهاد می‌کند چنین منطقی در Domain Service قرار گیرد.

Domain Service شیئی بدون State است که منطق کسب‌وکار را پیاده‌سازی می‌کند. در اغلب موارد، این منطق برای انجام محاسبه یا تحلیل، فراخوانی چند جزء سیستم را هماهنگ می‌کند.

به Ticket Aggregate برگردیم. Agent مهلت محدودی برای پاسخ دارد. این مهلت فقط به دادهٔ Ticket مثل Priority و Escalation وابسته نیست، بلکه به Policy واحد سازمانی برای SLA هر اولویت و Shiftهای کاری Agent نیز بستگی دارد؛ نمی‌توان از Agent انتظار پاسخ خارج از ساعات کاری داشت.

محاسبهٔ Deadline نیازمند داده از Ticket، Department و Work Schedule است؛ بنابراین Domain Service مناسب است:

public class ResponseTimeFrameCalculationService
{
 ...
 public ResponseTimeframe CalculateAgentResponseDeadline(UserId agentId,
  Priority priority, bool escalated, DateTime startTime)
 {
  var policy = _departmentRepository.GetDepartmentPolicy(agentId);
  var maxProcTime = policy.GetMaxResponseTimeFor(priority);
  if (escalated) {
   maxProcTime = maxProcTime * policy.EscalationFactor;
  }
  var shifts = _departmentRepository.GetUpcomingShifts(agentId,
   startTime, startTime.Add(policy.MaxAgentResponseTime));
  return CalculateTargetTime(maxProcTime, shifts);
 }
 ...
}

Domain Serviceها هماهنگی کار چند Aggregate را آسان می‌کنند، اما راه فراری از قانون «فقط یک Aggregate در هر تراکنش» نیستند. این قانون همچنان برقرار است. Domain Service بیشتر برای منطق محاسباتی‌ای مناسب است که باید دادهٔ چند Aggregate را بخواند.

همچنین Domain Service هیچ ارتباطی با Microservice، SOA یا دیگر کاربردهای واژهٔ Service ندارد؛ فقط یک شیء Stateless برای میزبانی منطق کسب‌وکار است.

مدیریت پیچیدگی

همان‌طور که در مقدمهٔ فصل اشاره شد، Aggregate و Value Object برای مقابله با پیچیدگی پیاده‌سازی منطق کسب‌وکار مطرح شده‌اند. دلیل آن را بررسی کنیم.

Eliyahu M. Goldratt در کتاب The Choice تعریف کوتاه اما قدرتمندی از پیچیدگی سیستم ارائه می‌کند. برای سنجش پیچیدگی یک سیستم، دشواری کنترل و پیش‌بینی رفتار آن مهم است. این دو جنبه در درجات آزادی (Degrees of Freedom) سیستم منعکس می‌شوند.

درجات آزادی، نقاط داده‌ای لازم برای توصیف State سیستم هستند. دو کلاس زیر را مقایسه کنید:

public class ClassA
{
 public int A { get; set; }
 public int B { get; set; }
 public int C { get; set; }
 public int D { get; set; }
 public int E { get; set; }
}
public class ClassB
{
 private int _a, _d;
 public int A
 {
  get => _a;
  set {
   _a = value;
   B = value / 2;
   C = value / 3;
  }
 }
 public int B { get; private set; }
 public int C { get; private set; }
 public int D
 {
  get => _d;
  set {
   _d = value;
   E = value * 2
  }
 }
 public int E { get; private set; }
}

در نگاه اول ClassB پیچیده‌تر به نظر می‌رسد: همان تعداد متغیر را دارد و محاسبات اضافه هم انجام می‌دهد. اما از دید Degrees of Freedom نتیجه برعکس است.

برای توصیف State کلاس A به پنج مقدار نیاز داریم؛ پس پنج درجهٔ آزادی دارد. در ClassB، مقادیر B و C تابع A هستند و E تابع D. اگر A و D را بدانیم، بقیه قابل استنتاج‌اند؛ پس فقط دو درجهٔ آزادی دارد.

کدام کلاس از نظر کنترل و پیش‌بینی دشوارتر است؟ کلاسی که Degrees of Freedom بیشتری دارد، یعنی ClassA. Invariantهای ClassB پیچیدگی را کاهش داده‌اند. Aggregate و Value Object نیز همین کار را می‌کنند: Invariantها را کپسوله و در نتیجه درجات آزادی را کاهش می‌دهند.

تمام منطق مربوط به State یک Value Object در مرز خودش قرار دارد و همین دربارهٔ Aggregate نیز صادق است. Aggregate فقط با متدهای خودش تغییر می‌کند و منطق آن Invariantها را محافظت می‌کند.

چون Domain Model فقط برای زیردامنه‌هایی با منطق پیچیده به کار می‌رود، معمولاً این زیردامنه‌ها همان زیردامنه‌های هسته‌ای، قلب نرم‌افزار، هستند.

جمع‌بندی

Domain Model برای منطق پیچیدهٔ کسب‌وکار طراحی شده و سه بلوک اصلی دارد:

  • Value Object: مفهوم دامنه‌ای که فقط با مقادیرش شناسایی می‌شود و ID صریح نمی‌خواهد. چون تغییر فیلد از نظر معنایی مقدار جدیدی می‌سازد، Immutable است. Value Object فقط داده نیست و رفتار دستکاری مقادیر و تولید نمونه‌های جدید را نیز دربر می‌گیرد.
  • Aggregate: سلسله‌مراتبی از Entityها که یک مرز تراکنشی مشترک دارند. همهٔ داده‌های داخل Aggregate باید برای منطق آن Strongly Consistent باشند. State فقط از طریق رابط عمومی و Commandها تغییر می‌کند؛ فیلدها برای بیرون Read-only هستند تا منطق مرتبط پراکنده نشود. تمام State Aggregate در یک تراکنش اتمیک Commit می‌شود. Aggregate با Domain Eventها اتفاقات مهم چرخهٔ عمر خود را به بیرون اعلام می‌کند.
  • Domain Service: شیء Stateless برای منطق کسب‌وکاری که طبیعی نیست در Aggregate یا Value Object خاصی قرار گیرد.

این بلوک‌ها با کپسوله‌سازی منطق در مرز Value Object و Aggregate پیچیدگی را مهار می‌کنند. جلوگیری از تغییر خارجی State تضمین می‌کند منطق در Application Layer تکرار نشود. فصل بعد شیوهٔ پیشرفته‌تری از Domain Model را با وارد کردن بُعد زمان به مدل معرفی می‌کند.

تمرین‌ها

  1. کدام عبارت درست است؟
    1. Value Object فقط داده دارد.
    2. Value Object فقط رفتار دارد.
    3. Value Objectها Immutable هستند.
    4. State Value Object می‌تواند تغییر کند.
  2. اصل راهنمای عمومی برای طراحی مرز Aggregate چیست؟
    1. Aggregate فقط می‌تواند یک Entity داشته باشد چون فقط یک نمونهٔ Aggregate در تراکنش مجاز است.
    2. Aggregate باید تا حد ممکن کوچک باشد، تا زمانی که الزامات سازگاری دادهٔ دامنه حفظ شوند.
    3. چون Aggregate سلسله‌مراتب Entity است، برای بیشترین سازگاری باید تا حد ممکن بزرگ باشد.
    4. بستگی دارد؛ در برخی دامنه‌ها Aggregate کوچک و در برخی Aggregate بزرگ کارآمدتر است.
  3. چرا فقط یک نمونهٔ Aggregate در یک تراکنش Commit می‌شود؟
    1. برای عملکرد بهتر زیر بار زیاد.
    2. برای تضمین مرزهای تراکنشی صحیح.
    3. چنین الزامی وجود ندارد.
    4. برای کار با Databaseهایی که تراکنش چندرکوردی ندارند.
  4. کدام عبارت رابطهٔ بلوک‌های Domain Model را بهتر توصیف می‌کند؟
    1. Value Objectها ویژگی Entityها را توصیف می‌کنند.
    2. Value Objectها می‌توانند Domain Event منتشر کنند.
    3. Aggregate شامل یک یا چند Entity است.
    4. A و C.
  5. کدام عبارت دربارهٔ تفاوت Active Record و Aggregate درست است؟
    1. Active Record فقط داده دارد اما Aggregate رفتار هم دارد.
    2. Aggregate تمام منطق کسب‌وکار خود را کپسوله می‌کند، اما منطق دستکاری‌کنندهٔ Active Record می‌تواند بیرون مرز آن باشد.
    3. Aggregate فقط داده دارد و Active Record داده و رفتار.
    4. Aggregate مجموعه‌ای از Active Recordهاست.

شکل‌ها و جدول‌های منبع

برای حفظ نمایش بصری دقیق عناصر گرافیکی، بازنمایی صفحه‌های دارای شکل یا جدول منبع در ادامه آمده است. متن آموزشی فصل در بخش‌های بالا به فارسی ترجمه شده است.

بازنمایی صفحهٔ 104 منبع برای حفظ دقیق شکل/جدول اصلی. Figure 6-1. Redundant ColorId field, making it possible to have two rows with the same
بازنمایی صفحهٔ 110 منبع برای حفظ دقیق شکل/جدول اصلی. Figure 6-2. Introducing an explicit identification field, allowing differentiating instances
بازنمایی صفحهٔ 114 منبع برای حفظ دقیق شکل/جدول اصلی. Figure 6-3. Aggregate as a hierarchy of entities
بازنمایی صفحهٔ 115 منبع برای حفظ دقیق شکل/جدول اصلی. Figure 6-4. Aggregate as consistency boundary
بازنمایی صفحهٔ 116 منبع برای حفظ دقیق شکل/جدول اصلی. Figure 6-5. Aggregate root
بازنمایی صفحهٔ 118 منبع برای حفظ دقیق شکل/جدول اصلی. Figure 6-6. Domain events publishing flow

امتیاز کاربران به این مقاله

☆☆☆☆☆

0 نفر امتیاز داده اند. میانگین: 0.0 از 5

 

0 نظر

نظر محترم شما در مورد مقاله های وب سایت برنامه نویسی و پایگاه داده

نظرات محترم شما در خدمات رسانی بهتر ما را یاری می نمایند. لطفا اگر مایل بودید یک نظر ما را مهمان فرمائید. آدرس ایمیل و وب سایت شما نمایش داده نخواهد شد.

0 / 500