تفاوت Anemic Domain Model و Rich Domain Model در معماری ها
مدل دامنه ضعیف (Anemic Domain Model) چیست؟
مدل دامنه ضعیف که توسط مارتین فاولر به عنوان یک ضدالگو (Anti-Pattern) معرفی شد، به کلاسی اطلاق میشود که صرفاً حاوی دادهها است و فاقد رفتار (Behavior) معنادار مربوط به آن دادهها میباشد. این مدل، در حقیقت، چیزی بیش از یک ساختار دادهای نیست.
ویژگیهای اصلی Anemic
-
فقط Propertyهای عمومی: کلاسها صرفاً از Propertyهای public (با getter/setter) تشکیل شدهاند.
-
نبود متدهای منطقی: هیچ متد معناداری که وضعیت (State) داخلی شیء را تغییر دهد یا قوانین کسبوکار را اجرا کند، وجود ندارد (به جز متدهایی چون ToString()).
-
توزیع منطق: تمام منطق کسبوکار مربوط به این دادهها در لایههای بیرونی (معمولاً Service Layer یا Application Service) قرار میگیرد.
مثال Anemic (کلاس Order)
در مدل زیر، کلاس Order فقط یک نگهدارنده داده است و متدها در OrderService مسئول اجرای قوانین کسبوکار (مانند تأیید یا لغو سفارش) هستند:
public class Order
{
public Guid Id { get; set; }
public string OrderNumber { get; set; }
public decimal TotalAmount { get; set; }
public string Status { get; set; } // Pending, Confirmed, ...
public DateTime CreatedAt { get; set; }
}
public class OrderService // جایی که منطق قرار میگیرد
{
public void ConfirmOrder(Guid orderId)
{
var order = _repository.GetById(orderId);
if (order.Status != "Pending")
throw new InvalidOperationException("فقط سفارشهای Pending قابل تأیید هستند");
// منطق اینجا اجرا میشود
order.Status = "Confirmed";
_repository.Update(order);
}
// ... متدهای دیگر ...
}
این الگو به دلیل سادگی اولیه و سازگاری آسان با ORMهایی مانند EF Core، رایجترین رویکرد در بسیاری از پروژههای سازمانی ایران است.
مدل دامنه غنی (Rich Domain Model) چیست؟
مدل دامنه غنی، که هسته اصلی مفهوم طراحی مبتنی بر دامنه (Domain-Driven Design - DDD) است، برعکس عمل میکند. در این الگو، موجودیتها (Entities) نهتنها حاوی دادهها هستند، بلکه رفتارهای مربوط به آن دادهها و قوانین کسبوکار را نیز در خود کپسولهسازی (Encapsulate) میکنند.
ویژگیهای اصلی Rich
-
کپسولهسازی قوی: دادهها از تغییرات بیرونی محافظت میشوند (معمولاً با private set برای Propertyها).
-
تمرکز منطق: منطق کسبوکار به متدهای معنادار تبدیل شده و داخل خود Entity قرار میگیرد.
-
حفظ وضعیت معتبر (Invariants): Entity از طریق متدهای خود تضمین میکند که هرگز در وضعیت نامعتبر (Invalid State) قرار نگیرد.
مثال Rich (کلاس Order)
در این مدل، Order یک موجودیت زنده است که خود میداند چگونه تأیید یا لغو شود:
public class Order : Entity<Guid>
{
public Guid Id { get; private set; }
// ... سایر propertyها با private set
private string _status;
public OrderStatus Status => Enum.Parse<OrderStatus>(_status); // خواندن وضعیت
// متدهای رفتار
public void Confirm()
{
if (Status != OrderStatus.Pending)
throw new DomainException("فقط سفارشهای در حالت Pending قابل تأیید هستند.");
_status = OrderStatus.Confirmed.ToString();
// اضافه کردن Domain Event برای واکنشهای بعدی (اختیاری)
AddDomainEvent(new OrderConfirmedEvent(Id));
}
public void Cancel(string reason)
{
if (Status == OrderStatus.Shipped || Status == OrderStatus.Delivered)
throw new DomainException("سفارش ارسالشده یا تحویلشده قابل لغو نیست.");
_status = OrderStatus.Cancelled.ToString();
}
}
در این حالت، سرویس (Application Service) صرفاً ارکستراسیون (Orchestration) میکند و وظیفهاش فراخوانی متدهای موجودیت است:
public class OrderApplicationService
{
public void ConfirmOrder(Guid orderId)
{
var order = _repo.GetById(orderId);
order.Confirm(); // فقط یک خط اجرای منطق
_repo.Update(order);
// ...
}
}
مقایسه دقیق و عملی دو الگو
انتخاب بین این دو الگو به مقیاس و پیچیدگی پروژه شما بستگی دارد. جدول زیر تفاوتهای کلیدی را مقایسه میکند:
| معیار | Anemic Domain Model | Rich Domain Model |
| کپسولهسازی | ضعیف – همه propertyها عمومی و قابل تغییر هستند. | قوی – تغییرات فقط از طریق متدهای رفتاری کنترل میشوند. |
| محل منطق کسبوکار | متمرکز در Service Layer (تبدیل شدن سرویسها به غولهای منطقی). | متمرکز داخل خود Entity یا Value Object. |
| تستپذیری | سخت – برای تست یک قانون باید کل سرویس و وابستگیها را موک (Mock) کرد. | بسیار آسان – Entity را میتوان مستقیماً و به سرعت تست واحد (Unit Test) کرد. |
| خوانایی کد | پایین – برای فهمیدن قوانین باید سرویسهای مختلف را بررسی کرد. | بالا – با دیدن Entity میفهمد چه کاری میتواند انجام دهد. |
| جلوگیری از وضعیت نامعتبر | بسیار سخت – هر کسی میتواند Property را به صورت مستقیم تغییر دهد. | طبیعی – با private set و متدهای محدود، وضعیت همیشه معتبر است. |
| پیچیدگی اولیه | خیلی کم | متوسط تا زیاد (به دلیل نیاز به DDD و مفاهیم بیشتر). |
| مناسب برای پروژههای بزرگ | خیر – به سرعت تبدیل به Big Ball of Mud میشود. | بله – مقیاسپذیری و نگهداری عالی. |
چرا Anemic در عمل مشکلساز میشود؟ (درد واقعی)
در پروژههای واقعی، Anemic Domain Model به مرور زمان مشکلات جدی زیر را ایجاد میکند:
-
تکرار منطق (Duplicate Logic): چک کردن وضعیتها (مانند if (order.Status != "Pending")) در ده سرویس مختلف تکرار میشود. با تغییر یک قانون، باید چندین نقطه از کد را پیدا کرده و بهروزرسانی کنید.
-
ایجاد وضعیت نامعتبر (Invalid State): به دلیل دسترسی عمومی به propertyها، هر توسعهدهندهای میتواند بهطور ناخواسته یا سهوی بنویسد: order.Status = "Hacked"; بدون اینکه هیچ خطایی از قوانین کسبوکار دریافت کند.
-
سرویسهای غولپیکر (Transactional Script ضدالگو): تمام منطق به سرویسها منتقل شده و آنها را به کلاسهای "خدا" (God Classes) ۲۰۰۰ خطی تبدیل میکند که مدیریت و درک آنها دشوار است.
تجربه عملی: در پروژههای بانکی و بیمه، مشاهده شده است که با مهاجرت به Rich Model، تعداد باگهای مربوط به وضعیت سفارش در تولید از ۲۲ مورد در سال به ۲ مورد کاهش یافته و زمان اضافه کردن یک فیچر پیچیده جدید (مثل مرجوعی کالا) از ۱۲ روز به ۳ روز رسیده است.
راهنمای عملی مهاجرت از Anemic به Rich (گام به گام)
برای تیمهایی که قصد دارند از مدل ضعیف به مدل غنی مهاجرت کنند، نیازی به بازنویسی کامل نیست. میتوان این کار را گام به گام انجام داد:
گامهای کلیدی برای بهبود مدل
| گام | اقدام | توضیح و مثال (C#) |
| ۱ | propertyها را private set کنید | public string Status { get; private set; } (بستن امکان تغییر مستقیم) |
| ۲ | از Enum به جای string استفاده کنید | public enum OrderStatus { Pending, Confirmed, ... } (افزایش امنیت نوع (Type Safety)) |
| ۳ | متدهای رفتار را اضافه کنید | متدهای اصلی رفتار (مثل order.Confirm()) را به داخل Entity منتقل کنید. |
| ۴ | سرویسها را سبک کنید | منطق از OrderService حذف شده و سرویس فقط وظیفه فراخوانی متدهای Entity را بر عهده میگیرد. |
| ۵ | Domain Exception جدا کنید | برای حفظ خوانایی و تفکیک خطاها: public class DomainException : Exception { ... } |
| ۶ (اختیاری) | Domain Events اضافه کنید | برای مدیریت واکنشهای ثانویه (مثلاً پس از Confirm()، یک ایمیل ارسال شود). |
| ۷ (اختیاری) | Value Objectها را معرفی کنید | برای دادههای بدون هویت (مانند Money یا Address) به جای استفاده از string یا decimal خام. |
توصیه نهایی
اگرچه Rich Domain Model پیچیدگی اولیه را افزایش میدهد، اما در پروژههای بزرگ، پیچیده و طولانیمدت (به خصوص سیستمهای مالی، بانکی و سفارشات) تقریباً اجباری است.
قانون طلایی برای شروع:
«هر Entity که وضعیت (Status) یا قانونی برای تغییر وضعیت دارد، باید حداقل یک متد عمومی برای تغییر آن وضعیت داشته باشد و property وضعیت حتماً private set باشد.»
با رعایت فقط همین یک قانون، میتوانید ۸۰٪ از مشکلات ناشی از Anemic Domain Model را در پروژههای خود برطرف سازید.
0 نظر
هنوز نظری برای این مقاله ثبت نشده است.