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

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

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

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

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

منطق کسب‌وکار مهم‌ترین بخش نرم‌افزار است؛ اساساً دلیل پیاده‌سازی نرم‌افزار همین است. رابط کاربری یک سامانه می‌تواند بسیار جذاب باشد و پایگاه دادهٔ آن فوق‌العاده سریع و مقیاس‌پذیر، اما اگر نرم‌افزار برای کسب‌وکار مفید نباشد، چیزی جز یک نمایش گران‌قیمت فناوری نیست.

همان‌طور که در فصل ۲ دیدیم، همهٔ زیردامنه‌های کسب‌وکار از نظر اهمیت راهبردی و پیچیدگی یکسان نیستند. این فصل بررسی راه‌های مختلف مدل‌سازی و پیاده‌سازی منطق کسب‌وکار را آغاز می‌کند. از دو الگوی مناسب برای منطق نسبتاً ساده شروع می‌کنیم: اسکریپت تراکنش (Transaction Script) و رکورد فعال (Active Record).

اسکریپت تراکنش

منطق کسب‌وکار را بر اساس رویه‌هایی سازمان‌دهی می‌کند که هر رویه یک درخواست از لایهٔ ارائه را مدیریت می‌کند.

Martin Fowler

رابط عمومی یک سامانه را می‌توان مجموعه‌ای از تراکنش‌های کسب‌وکار دانست که مصرف‌کنندگان قادر به اجرای آنها هستند؛ شکل ۵-۱. این تراکنش‌ها می‌توانند اطلاعات تحت مدیریت سامانه را بازیابی کنند، تغییر دهند یا هر دو کار را انجام دهند. این الگو منطق کسب‌وکار سامانه را بر اساس رویه‌ها سازمان‌دهی می‌کند؛ هر رویه عملیاتی را پیاده می‌کند که مصرف‌کننده از طریق رابط عمومی اجرا می‌کند. در عمل، عملیات عمومی سامانه به مرزهای کپسوله‌سازی تبدیل می‌شوند.

شکل ۵-۱ — رابط Transaction Script.

پیاده‌سازی

هر رویه به‌صورت یک اسکریپت رویه‌ای ساده و مستقیم پیاده‌سازی می‌شود. می‌تواند برای اتصال به مکانیزم‌های ذخیره‌سازی از یک لایهٔ انتزاعی باریک استفاده کند، یا مستقیماً به پایگاه داده دسترسی داشته باشد.

تنها الزام اساسی این رویه‌ها رفتار تراکنشی است. هر عملیات باید یا به‌طور کامل موفق شود یا شکست بخورد و هرگز سامانه را در وضعیت نامعتبر باقی نگذارد. حتی اگر اجرای Transaction Script در نامناسب‌ترین لحظه شکست بخورد، سامانه باید سازگار بماند؛ یا تمام تغییرات انجام‌شده تا زمان شکست را Rollback کند یا اقدامات جبرانی اجرا کند. همین رفتار تراکنشی در نام الگو منعکس شده است.

نمونهٔ زیر یک Transaction Script برای تبدیل دسته‌ای فایل‌های JSON به XML است:

DB.StartTransaction();
var job = DB.LoadNextJob();
var json = LoadFile(job.Source);
var xml = ConvertJsonToXml(json);
WriteFile(job.Destination, xml.ToString();
DB.MarkJobAsCompleted(job);
DB.Commit()

آن‌قدرها هم ساده نیست!

وقتی این الگو را در کلاس‌های DDD معرفی می‌کنم، دانشجویان اغلب با تعجب می‌پرسند: «واقعاً ارزش وقت گذاشتن دارد؟ مگر برای الگوها و تکنیک‌های پیشرفته‌تر نیامده‌ایم؟»

واقعیت این است که Transaction Script پایهٔ الگوهای پیشرفته‌تر پیاده‌سازی منطق کسب‌وکار است که در فصل‌های بعد خواهید آموخت. افزون بر این، با وجود ظاهر ساده، یکی از آسان‌ترین الگوها برای پیاده‌سازی اشتباه است. تعداد قابل توجهی از خطاهای Production که نویسنده در رفع آنها کمک کرده، به نوعی به پیاده‌سازی نادرست رفتار تراکنشی منطق کسب‌وکار برمی‌گردند.

سه نمونهٔ رایج و واقعی از فساد داده در اثر پیاده‌سازی نادرست Transaction Script را بررسی کنیم.

نبود رفتار تراکنشی

نمونه‌ای ساده از شکست در رفتار تراکنشی، اجرای چند Update بدون تراکنش فراگیر است. متد زیر رکوردی در جدول Users را Update می‌کند و سپس رکوردی در VisitsLog درج می‌کند:

01 public class LogVisit
02 {
03 ...
04
05 public void Execute(Guid userId, DataTime visitedOn)
06 {
07 _db.Execute("UPDATE Users SET last_visit=@p1 WHERE user_id=@p2",
08 visitedOn, userId);
09 _db.Execute(@"INSERT INTO VisitsLog(user_id, visit_date)
10 VALUES(@p1, @p2)", userId, visitedOn);
11 }
12 }

اگر پس از Update جدول Users در خط ۷ و پیش از موفق‌شدن درج Log در خط ۹ مشکلی رخ دهد، سامانه در وضعیت ناسازگار قرار می‌گیرد. Users تغییر کرده اما رکورد متناظر در VisitsLog نوشته نشده است. دلیل می‌تواند قطع شبکه، Timeout یا Deadlock پایگاه داده، یا Crash سروری باشد که فرایند را اجرا می‌کند.

راه‌حل، قراردادن هر دو تغییر در یک تراکنش واقعی است:

public class LogVisit
{
 ...
 public void Execute(Guid userId, DataTime visitedOn)
 {
  try
  {
   _db.StartTransaction();
   _db.Execute(@"UPDATE Users SET last_visit=@p1
    WHERE user_id=@p2", visitedOn, userId);
   _db.Execute(@"INSERT INTO VisitsLog(user_id, visit_date)
    VALUES(@p1, @p2)", userId, visitedOn);
   _db.Commit();
  } catch {
   _db.Rollback();
   throw;
  }
 }
}

به‌دلیل پشتیبانی بومی پایگاه‌های دادهٔ رابطه‌ای از تراکنش‌های چندرکوردی، این اصلاح ساده است. اما اگر پایگاه داده از تراکنش چندرکوردی پشتیبانی نکند یا چند مکانیزم ذخیره‌سازی داشته باشیم که نتوان آنها را در یک تراکنش توزیع‌شده متحد کرد، موضوع پیچیده‌تر می‌شود.

تراکنش‌های توزیع‌شده

در سامانه‌های توزیع‌شدهٔ مدرن رایج است که ابتدا داده در پایگاه داده تغییر کند و سپس با انتشار پیام روی Message Bus، سایر اجزای سیستم از آن تغییر مطلع شوند. فرض کنید در مثال قبل به‌جای ثبت بازدید در جدول، باید رویداد را روی Message Bus منتشر کنیم:

01 public class LogVisit
02 {
03 ...
04
05 public void Execute(Guid userId, DataTime visitedOn)
06 {
07 _db.Execute("UPDATE Users SET last_visit=@p1 WHERE user_id=@p2",
08 visitedOn,userId);
09 _messageBus.Publish("VISITS_TOPIC",
10 new { UserId = userId, VisitDate = visitedOn });
11 }
12 }

مانند مثال قبلی، شکست میان خط ۷ و خط ۹ وضعیت را خراب می‌کند: جدول Users تغییر کرده اما به‌دلیل شکست انتشار پیام، سایر اجزا مطلع نمی‌شوند.

این بار اصلاح به آن سادگی نیست. تراکنش‌های توزیع‌شده میان چند مکانیزم ذخیره‌سازی پیچیده، دشوار برای مقیاس‌پذیری و مستعد خطا هستند و معمولاً از آنها اجتناب می‌شود. فصل ۸ نشان می‌دهد چگونه از CQRS برای پرکردن چند ذخیره‌ساز استفاده کنیم و فصل ۹ الگوی Outbox را معرفی می‌کند که انتشار قابل اتکای پیام پس از Commit تغییرات در پایگاه داده را ممکن می‌سازد.

تراکنش‌های توزیع‌شدهٔ ضمنی

متد به‌ظاهر سادهٔ زیر را در نظر بگیرید:

public class LogVisit
{
 ...
 public void Execute(Guid userId)
 {
  _db.Execute("UPDATE Users SET visits=visits+1 WHERE user_id=@p1",
   userId);
 }
}

این متد به‌جای ثبت تاریخ آخرین بازدید، شمارندهٔ بازدید هر کاربر را نگه می‌دارد و با هر فراخوانی آن را یک واحد افزایش می‌دهد. فقط یک مقدار، در یک جدول و یک پایگاه داده تغییر می‌کند؛ بااین‌حال هنوز یک تراکنش توزیع‌شده است و می‌تواند وضعیت ناسازگار بسازد.

دلیل این است که عملیات اطلاعات را هم به پایگاه داده و هم به فرایند خارجی فراخواننده منتقل می‌کند؛ شکل ۵-۲.

شکل ۵-۲ — عملیات LogVisit داده را به‌روزرسانی و موفقیت یا شکست را به فراخواننده اطلاع می‌دهد.

گرچه Execute مقدار بازگشتی ندارد، موفق یا ناموفق بودن عملیات را منتقل می‌کند؛ در صورت شکست، فراخواننده Exception دریافت می‌کند. حال اگر متد موفق شود اما انتقال نتیجه به فراخواننده شکست بخورد چه؟ برای نمونه، اگر LogVisit بخشی از REST Service باشد و شبکه قطع شود، یا اگر هر دو در یک Process باشند اما Process پیش از ثبت موفقیت توسط فراخواننده Crash کند.

در هر دو حالت مصرف‌کننده تصور می‌کند عملیات شکست خورده و دوباره LogVisit را اجرا می‌کند. اجرای دوباره شمارنده را یک بار اضافهٔ دیگر افزایش می‌دهد و در مجموع مقدار ۲ به‌جای ۱ زیاد می‌شود. در نتیجه، Transaction Script باز هم رفتار تراکنشی را درست تضمین نکرده است.

برای این مسئله راه‌حل جهانی و ساده‌ای وجود ندارد و همه‌چیز به دامنهٔ کسب‌وکار بستگی دارد. در این مثال یک روش، Idempotent کردن عملیات است؛ یعنی حتی اگر چندبار تکرار شود نتیجهٔ نهایی یکسان بماند.

مثلاً می‌توان از مصرف‌کننده خواست مقدار شمارنده را ارسال کند. مصرف‌کننده ابتدا مقدار فعلی را می‌خواند، محلی یک واحد اضافه می‌کند و مقدار جدید را پارامتر قرار می‌دهد. تکرار اجرای عملیات نتیجه را عوض نمی‌کند:

public class LogVisit
{
 ...
 public void Execute(Guid userId, long visits)
 {
  _db.Execute("UPDATE Users SET visits = @p1 WHERE user_id=@p2",
   visits, userId);
 }
}

راه دیگر، کنترل هم‌زمانی خوش‌بینانه (Optimistic Concurrency Control) است. فراخواننده پیش از اجرای LogVisit مقدار فعلی را خوانده و به‌عنوان پارامتر ارسال می‌کند. LogVisit فقط وقتی مقدار را افزایش می‌دهد که هنوز برابر مقدار خوانده‌شده باشد:

public class LogVisit
{
 ...
 public void Execute(Guid userId, long expectedVisits)
 {
  _db.Execute(@"UPDATE Users SET visits=visits+1
   WHERE user_id=@p1 and visits = @p2",
   userId, visits);
 }
}

اجرای بعدی با همان پارامترها داده را تغییر نمی‌دهد، چون شرط visits = @p2 دیگر برقرار نیست.

چه زمانی از Transaction Script استفاده کنیم؟

این الگو برای ساده‌ترین دامنه‌های مسئله مناسب است؛ جایی که منطق کسب‌وکار شبیه عملیات رویه‌ای ساده است. نمونهٔ کلاسیک، عملیات ETL است: استخراج داده از منبع، تبدیل آن به فرم دیگر و بارگذاری نتیجه در مقصد؛ شکل ۵-۳.

شکل ۵-۳ — جریان دادهٔ Extract–Transform–Load.

Transaction Script به‌طور طبیعی با زیردامنه‌های پشتیبان که منطق ساده دارند سازگار است. همچنین می‌تواند به‌عنوان Adapter برای یکپارچه‌سازی با سیستم‌های خارجی یا زیردامنه‌های عمومی و نیز بخشی از ACL استفاده شود.

مزیت اصلی الگو سادگی است: انتزاع کم، سربار اجرایی کم و فهم آسان. اما همین سادگی عیب آن نیز هست. هرچه منطق کسب‌وکار پیچیده‌تر شود، احتمال تکرار منطق در تراکنش‌های مختلف بیشتر می‌شود و با ناهمگام شدن کد تکراری، رفتار ناسازگار ایجاد می‌شود. به همین دلیل Transaction Script هرگز گزینهٔ مناسبی برای زیردامنهٔ هسته‌ای با منطق بسیار پیچیده نیست.

همین سادگی به Transaction Script شهرتی دوگانه داده و گاهی آن را Antipattern می‌دانند. اگر منطق پیچیده به‌شکل Transaction Script نوشته شود، دیر یا زود به Big Ball of Mud غیرقابل نگه‌داری تبدیل می‌شود. بااین‌حال باید توجه داشت که این الگو در توسعهٔ نرم‌افزار بسیار فراگیر است و تقریباً همهٔ الگوهای پیاده‌سازی منطق کسب‌وکار که در ادامه بررسی می‌کنیم به نحوی بر آن بنا شده‌اند.

رکورد فعال (Active Record)

شیئی که یک ردیف در جدول یا View پایگاه داده را دربر می‌گیرد، دسترسی به پایگاه داده را کپسوله می‌کند و منطق دامنه را بر آن داده اضافه می‌کند.

Martin Fowler

مانند Transaction Script، Active Record برای منطق سادهٔ کسب‌وکار مناسب است؛ با این تفاوت که منطق می‌تواند روی ساختارهای دادهٔ پیچیده‌تر کار کند. به‌جای رکوردهای تخت، می‌توان درخت‌ها و سلسله‌مراتب شیئی با روابط یک‌به‌چند و چندبه‌چند داشت؛ شکل ۵-۴.

شکل ۵-۴ — مدل دادهٔ پیچیده‌تر با روابط one-to-many و many-to-many.

کار با چنین ساختارهایی از طریق Transaction Script ساده باعث تکرار زیاد کد، به‌ویژه کد نگاشت داده به نمایش درون‌حافظه‌ای، می‌شود.

پیاده‌سازی

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

منطق کسب‌وکار همچنان به شکل Transaction Script سازمان‌دهی می‌شود، اما به‌جای دسترسی مستقیم به پایگاه داده، Transaction Script اشیای Active Record را دستکاری می‌کند. عملیات باید در پایان یا به‌طور کامل Commit شود یا شکست بخورد:

public class CreateUser
{
 ...
 public void Execute(userDetails)
 {
  try
  {
   _db.StartTransaction();
   var user = new User();
   user.Name = userDetails.Name;
   user.Email = userDetails.Email;
   user.Save();
   _db.Commit();
  } catch {
   _db.Rollback();
   throw;
  }
 }
}

هدف الگو کپسوله‌کردن پیچیدگی نگاشت شیء در حافظه به Schema پایگاه داده است. Active Record علاوه بر Persistence می‌تواند منطق کسب‌وکار نیز داشته باشد، مانند اعتبارسنجی مقادیر جدید یا رویه‌هایی که دادهٔ شیء را تغییر می‌دهند. بااین‌حال ویژگی متمایز آن جدابودن ساختار داده و رفتار است: معمولاً فیلدها Getter و Setter عمومی دارند و رویه‌های خارجی می‌توانند State شیء را تغییر دهند.

چه زمانی از Active Record استفاده کنیم؟

چون Active Record اساساً Transaction Script‌ای است که دسترسی به پایگاه داده را بهینه می‌کند، فقط منطق نسبتاً ساده مثل CRUD و حداکثر اعتبارسنجی ورودی کاربر را پشتیبانی می‌کند.

بنابراین مانند Transaction Script برای زیردامنه‌های پشتیبان، یکپارچه‌سازی راه‌حل‌های خارجی در زیردامنه‌های عمومی یا تبدیل مدل مناسب است. تفاوت اصلی آن است که Active Record پیچیدگی نگاشت ساختارهای دادهٔ پیچیده به Schema پایگاه داده را حل می‌کند.

Active Record گاهی با عنوان «مدل دامنهٔ کم‌خون (Anemic Domain Model)» و به‌عنوان Antipattern معرفی می‌شود. نویسنده ترجیح می‌دهد از بار منفی این اصطلاحات پرهیز کند. این یک ابزار است؛ مانند هر ابزار دیگری در زمینهٔ درست مسئله را حل می‌کند و در زمینهٔ نادرست می‌تواند بیشتر از منفعت، آسیب ایجاد کند. وقتی منطق کسب‌وکار ساده است، استفاده از Active Record ایرادی ندارد. در مقابل، به‌کارگیری الگوی پیچیده‌تر برای منطق ساده نیز با ایجاد پیچیدگی تصادفی آسیب‌زا خواهد بود. فصل بعد Domain Model را معرفی و تفاوت آن با Active Record را روشن می‌کند.

عمل‌گرا باشید

هرچند دادهٔ کسب‌وکار مهم است و کدی که طراحی می‌کنیم باید از یکپارچگی آن محافظت کند، گاهی رویکردی عمل‌گرایانه مطلوب‌تر است. به‌ویژه در مقیاس‌های بسیار بزرگ، می‌توان تضمین‌های سازگاری داده را در برخی موارد کاهش داد. بررسی کنید آیا خراب‌شدن وضعیت یک رکورد از یک میلیون رکورد واقعاً برای کسب‌وکار بحرانی است و آیا می‌تواند روی عملکرد یا سودآوری اثر منفی بگذارد. فرض کنید سامانه‌ای می‌سازید که روزانه میلیاردها رویداد از دستگاه‌های IoT دریافت می‌کند. آیا تکراری یا گم‌شدن ۰٫۰۰۱٪ رویدادها واقعاً مسئله‌ای حیاتی است؟

مثل همیشه قانون جهانی وجود ندارد؛ همه‌چیز به دامنهٔ کسب‌وکار بستگی دارد. هر جا ممکن است می‌توان «گوشه‌ها را برید»، اما باید ریسک‌ها و پیامدهای کسب‌وکاری آن را آگاهانه ارزیابی کرد.

جمع‌بندی

در این فصل دو الگوی پیاده‌سازی منطق کسب‌وکار بررسی شد:

  • Transaction Script: عملیات سامانه را به اسکریپت‌های رویه‌ای ساده تبدیل می‌کند. هر رویه باید تراکنشی باشد؛ یا کامل موفق شود یا کامل شکست بخورد. این الگو برای زیردامنه‌های پشتیبان با منطق شبیه ETL مناسب است.
  • Active Record: وقتی منطق ساده است ولی روی ساختار دادهٔ پیچیده کار می‌کند، ساختار داده می‌تواند به Active Record تبدیل شود؛ شیئی که روش‌های سادهٔ CRUD را فراهم می‌کند.

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

تمرین‌ها

  1. کدام‌یک از الگوهای این فصل باید برای پیاده‌سازی منطق یک زیردامنهٔ هسته‌ای استفاده شود؟
    1. Transaction Script
    2. Active Record
    3. هیچ‌کدام برای زیردامنهٔ هسته‌ای مناسب نیستند.
    4. هر دو قابل استفاده‌اند.
  2. کد زیر را در نظر بگیرید:
    public void CreateTicket(TicketData data)
    {
     var agent = FindLeastBusyAgent();
     agent.ActiveTickets = agent.ActiveTickets + 1;
     agent.Save();
     var ticket = new Ticket();
     ticket.Id = Guid.New();
     ticket.Data = data;
     ticket.AssignedAgent = agent;
     ticket.Save();
     _alerts.Send(agent, "You have a new ticket!");
    }

    با فرض نبود سازوکار تراکنش سطح بالا، چه مشکلات سازگاری داده ممکن است رخ دهد؟

    1. با دریافت تیکت جدید، شمارندهٔ تیکت‌های فعال Agent ممکن است بیش از ۱ افزایش یابد.
    2. شمارنده ممکن است ۱ واحد افزایش یابد ولی تیکت جدیدی به Agent تخصیص داده نشود.
    3. Agent ممکن است تیکت جدید بگیرد اما اعلان دریافت نکند.
    4. همهٔ موارد بالا ممکن‌اند.
  3. در کد بالا دست‌کم یک Edge Case دیگر نیز وجود دارد که می‌تواند وضعیت سامانه را خراب کند. آن را پیدا کنید.
  4. به مثال WolfDesk در پیشگفتار برگردید. کدام بخش‌های سامانه می‌توانند با Transaction Script یا Active Record پیاده‌سازی شوند؟

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

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

بازنمایی صفحهٔ 90 منبع برای حفظ دقیق شکل/جدول اصلی. Figure 5-1. Transaction script interface
بازنمایی صفحهٔ 93 منبع برای حفظ دقیق شکل/جدول اصلی. Figure 5-2. The LogVisit operation updating the data and notifying the caller of the
بازنمایی صفحهٔ 95 منبع برای حفظ دقیق شکل/جدول اصلی. Figure 5-3. Extract-transform-load data flow
بازنمایی صفحهٔ 96 منبع برای حفظ دقیق شکل/جدول اصلی. Figure 5-4. A more complicated data model with one-to-many and many-to-many

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

☆☆☆☆☆

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

 

0 نظر

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

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

0 / 500