عنوان:

‫Domain-Driven Design (DDD)؛ بخش چهارم: Bounded Contexts


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۵/۳۰ ۰۸:۱۰
آدرس: www.dntips.ir
Bounded Contexts: مرزهایی برای مبارزه با پیچیدگی 🚧

تا به حال در پروژه‌ای بوده‌اید که یک کلمه، معانی متفاوتی در بخش‌های مختلف داشته باشد؟ مثلاً در یک سیستم فروشگاهی، "محصول" (Product) در بخش "انبارداری" (Inventory) یک مفهوم است و در بخش "فروش" (Sales) یک مفهوم دیگر.
  • در بخش انبارداری، "محصول" دارای ویژگی‌هایی مثل وزن، ابعاد و کد انبار است.
  • در بخش فروش، "محصول" دارای قیمت، تخفیف، و یک توصیف جذاب برای مشتری است.
اگر این دو مفهوم متفاوت را در یک مدل واحد ترکیب کنیم، به زودی با یک کلاس بزرگ و گیج‌کننده روبرو خواهیم شد که مملو از منطق نامرتبط است. اینجاست که Sub-Domains و Bounded Contexts به کمک ما می‌آیند.

Sub-Domains: تقسیم دامنه به بخش‌های منطقی

Sub-Domain به زیرمجموعه‌ای از یک دامنه بزرگ اشاره دارد که یک بخش خاص از کسب‌وکار را نمایندگی می‌کند. به عنوان مثال، در یک سیستم فروشگاهی، می‌توانیم دامنه‌های زیر را داشته باشیم:
  • زیردامنه هسته (Core Sub-domain): "پردازش سفارش" که مستقیماً به مزیت رقابتی کسب‌وکار مربوط است.
  • زیردامنه عمومی (Generic Sub-domain): "احراز هویت" که یک مشکل عمومی است و می‌توان از یک راه حل آماده استفاده کرد.
  • زیردامنه پشتیبانی (Supporting Sub-domain): "ارسال مرسوله" که گرچه ضروری است، اما به اندازه زیردامنه هسته حیاتی نیست.
شناسایی این زیردامنه‌ها به ما کمک می‌کند تا تمرکز و منابع خود را به درستی تقسیم کنیم.

Bounded Contexts: مرزهای نرم‌افزاری

Bounded Context یک مفهوم کلیدی در DDD است که یک مرز منطقی و نرم‌افزاری را مشخص می‌کند. این مرز، تضمین می‌کند که یک مدل خاص، یک زبان مشترک (Ubiquitous Language) و یک مفهوم خاص، تنها در داخل آن مرز معتبر است.
به بیان ساده، یک Bounded Context یک ظرف (Container) برای یک مدل دامنه است. این ظرف، مدل را از هرگونه نفوذ و تداخل از بخش‌های دیگر سیستم محافظت می‌کند.
در مثال فروشگاه، ما می‌توانیم دو Bounded Context جداگانه داشته باشیم:
  • Bounded Context "مدیریت موجودی": این بخش، مدل "محصول" (Product) را با ویژگی‌های انبارداری خود (مانند StockQuantity) نگهداری می‌کند.
  • Bounded Context "فروش": این بخش، مدل "محصول" (Product) را با ویژگی‌های فروشگاهی خود (مانند Price) نگهداری می‌کند.
در این دو Bounded Context، حتی اگر نام کلاس‌ها یکسان باشد، مفاهیم و زبان آن‌ها کاملاً مستقل و متفاوت است.
این تقسیم‌بندی به ما امکان می‌دهد که:
  • هر تیم به صورت مستقل روی یک Bounded Context کار کند.
  • پیچیدگی‌ها را به صورت موضعی مدیریت کنیم و از سرایت آن‌ها به بخش‌های دیگر جلوگیری نماییم.
  • سیستم‌هایی بسازیم که نگهداری و مقیاس‌پذیری آن‌ها بسیار آسان‌تر باشد.
به این فکر کنید: آیا در پروژه فعلی شما، مدل‌ها و کلاس‌ها در بخش‌های مختلف به صورت "بزرگ و همه کاره" هستند؟ چقدر تلاش برای مدیریت وابستگی‌ها و منطق در این کلاس‌ها صرف می‌کنید؟
DDD به ما یادآوری می‌کند که راه‌حل مشکلات بزرگ، اغلب در تقسیم آن‌ها به مسائل کوچک‌تر و مدیریت‌پذیرتر نهفته است. Bounded Contexts، دقیقاً همان ابزاری هستند که این کار را برای ما امکان‌پذیر می‌کنند.


Context Mapping: نقشه‌راهی برای درک تعاملات سیستم 🗺️

تا به حال در سیستمی کار کرده‌اید که بخش‌های مختلف آن به صورت نامنظم و بدون یک الگوی مشخص با هم ارتباط برقرار می‌کنند؟ این ارتباطات آشفته، منجر به وابستگی‌های پیچیده و غیرقابل کنترل می‌شود.
در دنیای DDD، پس از اینکه دامنه‌مان را به Bounded Context های کوچک و مدیریت‌پذیر تقسیم کردیم، قدم بعدی این است که بفهمیم این بخش‌ها چگونه با هم تعامل دارند. اینجاست که Context Mapping وارد میدان می‌شود، ابزاری حیاتی برای ترسیم روابط بین Bounded Context ها.

Context Mapping چیست؟

Context Mapping یک نقشه بصری از روابط و وابستگی‌ها بین Bounded Context های مختلف است. این نقشه، به ما کمک می‌کند تا الگوهای ارتباطی (Integration Patterns) را شناسایی کنیم و به صورت آگاهانه، بهترین راهکار را برای یکپارچه‌سازی بخش‌های سیستم انتخاب کنیم.
به جای اینکه هر تیم به صورت مستقل و بدون هماهنگی با تیم‌های دیگر ارتباط برقرار کند، Context Map یک دید کلی به ما می‌دهد و اجازه می‌دهد که روابط را به صورت واضح تعریف کنیم.

الگوهای ارتباطی کلیدی:
  • Customer-Supplier (مشتری-تامین‌کننده): در این الگو، یک Bounded Context (مشتری) به یک Bounded Context دیگر (تامین‌کننده) وابسته است. تیم مشتری برای تغییرات مورد نیاز خود با تیم تامین‌کننده مذاکره می‌کند. این یک رابطه سالم و مدیریت‌شده است که به هر دو طرف اجازه می‌دهد تا به صورت هماهنگ پیش بروند.
  • Conformist (پیرو): در این الگو، یک Bounded Context (پیرو) به طور کامل از مدل و زبان Bounded Context دیگر (لیدر) پیروی می‌کند. این الگو، ارتباط را ساده می‌کند اما تیم پیرو را در برابر تغییرات تیم لیدر آسیب‌پذیر می‌سازد.
  • Partnership (مشارکت): در این الگو، دو تیم به صورت برابر و با توافق متقابل، مدل‌های خود را توسعه می‌دهند و با یکدیگر همکاری نزدیکی دارند. این رابطه نیازمند ارتباط و هماهنگی بسیار بالاست.
  • Published Language (زبان منتشر شده): یک Bounded Context، یک API یا مدل عمومی را منتشر می‌کند که Bounded Context های دیگر می‌توانند از آن استفاده کنند. این الگو به وابستگی‌های ضعیف‌تر منجر می‌شود، چرا که مصرف‌کننده‌ها به یک زبان مشترک از پیش تعریف‌شده متکی هستند.

چرا Context Mapping مهم است؟
  • کاهش ریسک: با درک وابستگی‌ها، می‌توانیم ریسک‌های مرتبط با تغییرات را پیش‌بینی و مدیریت کنیم.
  • ارتباط شفاف: Context Map به عنوان یک سند زنده، ارتباط بین تیم‌ها را شفاف‌تر می‌کند و از سوتفاهم‌ها جلوگیری می‌نماید.
  • انتخاب استراتژی درست: با شناخت روابط، می‌توانیم تصمیم بگیریم که کدام الگو برای هر ارتباط مناسب‌تر است.

به این فکر کنید: روابط بین بخش‌های مختلف نرم‌افزار شما چگونه است؟ آیا این روابط مدیریت‌شده هستند یا به صورت خودجوش شکل گرفته‌اند؟
Context Mapping یک ابزار استراتژیک برای طراحی معماری یک سیستم پیچیده است. این نقشه به ما کمک می‌کند تا به جای اینکه در یک شبکه از وابستگی‌ها گم شویم، یک دید واضح از سیستم خود داشته باشیم و آن را به صورت آگاهانه مدیریت کنیم.

نظرات

  • وحید نصیری در ۱۴۰۴/۰۵/۳۰ ۱۰:۲۲
    بررسی یک مثال قدم به قدم طراحی یک سیستم امانت کتابخانه‌ای با رویکرد Domain-Driven Design (DDD)

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

    کشف Sub-Domains
    وقتی به یک کتابخانه فکر می‌کنی، شاید در ذهن‌ات چندین فعالیت و نقش مختلف شکل بگیرد:
    • بخشی که با مدیریت کتاب‌ها سر و کار دارد: ثبت، ویرایش، دسته‌بندی.
    • بخشی که به امانت و بازگشت کتاب‌ها اختصاص دارد: چه کسی چه کتابی را گرفت؟ تا کی باید برگرداند؟ آیا دیرکرد دارد؟
    • بخشی که اعضا و کاربران را مدیریت می‌کند: ثبت‌نام، تمدید عضویت، وضعیت فعال یا غیرفعال بودن.
    • شاید هم بخش‌های جنبی مثل پرداخت جریمه‌ها یا حتی گزارش‌گیری و آمارگیری.

    هر کدام از این‌ها یک Sub-Domain است؛ یک قلمرو کوچک‌تر در دل سیستم بزرگ‌تر ما. اگر بخواهیم نامی روی آن‌ها بگذاریم، می‌توانیم این‌گونه دسته‌بندی کنیم:
    • Catalog Sub-Domain: برای مدیریت کتاب‌ها و منابع.
    • Circulation Sub-Domain: برای امانت و بازگشت.
    • Membership Sub-Domain: برای مدیریت اعضا.
    • Billing/Payment Sub-Domain: برای جریمه‌ها یا تراکنش‌های مالی.

    سفر به Bounded Contexts
    اینجاست که به موضوع جذاب Bounded Context می‌رسیم. DDD به ما می‌گوید که هر Sub-Domain الزاماً باید در یک مدل یکپارچه حل نشود. چرا؟ چون زبان، قوانین و حتی داده‌های هر کدام با دیگری متفاوت است.
    • مثلاً در Catalog Context، مهم‌ترین موجودیت‌ها (Entities) چیزهایی مثل Book، Author، Category هستند. قوانین اینجا حول اطلاعات کتاب می‌چرخد.
    • اما در Circulation Context، دیگر کتاب فقط یک «منبع قابل امانت» است. موجودیت اصلی می‌شود Loan یا BorrowingRecord. قوانین اینجا با تاریخ امانت، تمدید و دیرکرد سروکار دارند.
    • در Membership Context، ما با Member، MembershipType یا Subscription سر و کار داریم، و زبان این فضا با Catalog کاملاً فرق دارد.
    • اگر هم Billing Context داشته باشیم، قوانینش روی Fine و PaymentTransaction متمرکز می‌شوند.

    به این ترتیب هر Sub-Domain در قالب یک Bounded Context تعریف می‌شود و مدل خودش را دارد.

    نقشه‌برداری از روابط (Context Mapping)
    حالا وقت آن است که این مرزها را به هم وصل کنیم. Context Mapping مثل ترسیم نقشه‌ی ارتباطات یک شهر است: مرز هر محله مشخص است، اما خیابان‌هایی هم هستند که آن‌ها را به هم وصل می‌کند.
    • بین Catalog و Circulation یک ارتباط واضح داریم:
    • Circulation باید بداند کدام کتاب در کاتالوگ ثبت شده است. پس Catalog به نوعی Upstream Context می‌شود و Circulation نقش Downstream.
    • بین Membership و Circulation هم رابطه‌ای مشابه داریم:
    • Circulation نمی‌تواند به هر کسی کتاب امانت دهد؛ باید وضعیت عضویت را از Membership بپرسد. پس باز Membership یک Upstream برای Circulation است.
    • Billing هم به Circulation وابسته است: چون جریمه‌ها بر اساس دیرکرد امانت‌ها تعریف می‌شوند. اینجا Circulation اطلاعات لازم را به Billing می‌دهد.

    اگر بخواهیم تصویری ساده بکشیم، چنین چیزی می‌بینیم:
    Catalog ----> Circulation <---- Membership
                        |
                        v
                     Billing

    یک قدم جلوتر
    حالا که نقشه را داریم، می‌توانیم فکر کنیم که در EF Core، هر Bounded Context می‌تواند یک DbContext جداگانه داشته باشد. این DbContext دقیقاً همان محدوده‌ای است که باید مستقل از بقیه مدل شود.
    مثلاً:
    • CatalogDbContext فقط موجودیت‌های کتاب‌ها و دسته‌بندی‌ها را نگه می‌دارد.
    • CirculationDbContext روی رکوردهای امانت متمرکز است.
    • MembershipDbContext فقط با اعضا و وضعیت آن‌ها سروکار دارد.
    • BillingDbContext هم کار خودش را می‌کند.

    در این طراحی، ما عملاً یک سیستم ماژولار می‌سازیم که هر Context می‌تواند مستقل توسعه یابد، ولی همچنان از طریق قراردادهای مشخص (APIها، رویدادها، یا Integration Events) با هم تعامل دارند.
    می‌بینید؟ ما بدون اینکه به سراغ کد برویم، یک معماری ذهنی ترسیم کردیم که هم ساده است، هم انعطاف‌پذیر، و هم با اصول DDD سازگار.
  • وحید نصیری در ۱۴۰۴/۰۵/۳۰ ۱۰:۲۵
    در گام بعدی، وارد جزئیات بیشتری می‌شویم و موجودیت‌ها (Entities)، Value Objectها و Aggregate Rootهای هر Bounded Context را هم مشخص می‌کنیم (البته این موارد را به تفصیل در قسمت‌های بعدی، با جزئیات بیشتری بررسی خواهیم کرد).

    حالا که نقشهٔ Sub-Domainها و Bounded Contextها را شناختیم، وقت آن است که یک لایه عمیق‌تر شویم و ببینیم در دل هر Context چه موجودیت‌ها (Entities)، اشیای ارزشی (Value Objects) و ریشه‌های تجمیع (Aggregate Roots) زندگی می‌کنند. بیا با هم، هر قلمرو را بررسی کنیم:

    1. Catalog Context
    اینجا همه‌چیز دربارهٔ کتاب‌ها و منابع است.
    Entities:
    • Book: کتاب با ویژگی‌هایی مثل عنوان، سال انتشار، ISBN.
    • Author: نویسنده با نام و مشخصات پایه.
    • Category: دسته‌بندی موضوعی کتاب‌ها.
    Value Objects:
    • ISBN: یک شناسهٔ منحصربه‌فرد که خودش قواعد اعتبارسنجی دارد (مثلاً طول و الگوی معتبر).
    • PublicationInfo: شامل ناشر و سال انتشار.
    Aggregate Root:
    • Book به‌عنوان ریشهٔ اصلی؛ چرا که Author یا Category می‌توانند مستقل مدیریت شوند، اما در تعامل با امانت‌ها، معمولاً کتاب واحد پایه است.

    2. Circulation Context
    این قلمرو، قلب سیستم امانت است.
    Entities:
    • Loan (یا BorrowingRecord): رکوردی که می‌گوید کدام عضو چه کتابی را کی گرفته.
    • Reservation: درخواست رزرو یک کتاب.
    Value Objects:
    • LoanPeriod: شامل تاریخ شروع و پایان.
    • DueDate: تاریخ بازگشت مورد انتظار.
    Aggregate Root:
    • Loan چون تمام قوانین (مثل تمدید یا دیرکرد) حول آن می‌چرخد.
    • همچنین می‌توان Reservation را یک Aggregate جدا در نظر گرفت، چون چرخهٔ زندگی خودش را دارد.

    3. Membership Context
    اینجا با هویت کاربران و وضعیت عضویتشان سر و کار داریم.
    Entities:
    • Member: شخصی که عضو کتابخانه است.
    • MembershipType: نوع عضویت (مثلاً سالانه، دانشجویی، ویژه).
    Value Objects:
    • ContactInfo: شامل ایمیل، شماره تماس.
    • Address: آدرس پستی یا محل سکونت.
    Aggregate Root:
    • Member چون همه‌چیز (نوع عضویت، وضعیت فعال یا غیرفعال بودن) به او متصل است.

    4. Billing Context
    اینجا جایی است که اعداد و جریمه‌ها حرف اول را می‌زنند.
    Entities:
    • Fine: جریمهٔ ایجاد شده برای دیرکرد یا خسارت.
    • PaymentTransaction: ثبت پرداخت‌ها.
    Value Objects:
    • Money: شامل مقدار و واحد پول.
    • PaymentMethod: نوع پرداخت (کارت، نقدی، آنلاین).
    Aggregate Root:
    • Fine (به‌عنوان منبع بدهی).
    • PaymentTransaction می‌تواند یک Aggregate مستقل باشد، چون چرخهٔ زندگی‌اش جدا از جریمه تعریف می‌شود.

    5. رابطهٔ Aggregateها با EF Core
    در EF Core، هر Aggregate Root می‌تواند به‌عنوان DbSet اصلی در DbContext مربوطه تعریف شود.
    مثلاً در Circulation:
    public class CirculationDbContext : DbContext
    {
        public DbSet<Loan> Loans { get; set; }
        public DbSet<Reservation> Reservations { get; set; }
    }
    ایده این است که DbContext ما تنها Aggregate Rootها را به‌صورت DbSet نمایان کند؛ موجودیت‌های داخلی (Entities داخلی یا Value Objectها) از طریق Navigation Properties مدیریت می‌شوند، نه اینکه به‌طور مستقیم Query شوند.
    حالا می‌توانی تصور کنی که هر Bounded Context شبیه یک «جزیره» است که Aggregateهای خاص خودش را دارد. ارتباط این جزیره‌ها از طریق رویدادهای دامنه‌ای (Domain Events) یا APIها برقرار می‌شود، نه از طریق اشتراک مستقیم دیتابیس.

    // نقشهٔ سادهٔ روابط (Context Map)
    
         +--------------------+
         |   Membership BC    |
         |--------------------|
         | Member             |
         | MembershipType     |
         +--------------------+
                  ^
                  |  Validate member
                  |
       +----------+-----------+
       |                      |
       |                      |
       v                      v
    +------------------+   +---------------------+
    | Circulation BC   |   | Reservation BC      |
    |------------------|   |---------------------|
    | Loan             |   | ReservationQueue    |
    | LoanPolicy       |   | PickupWindow        |
    +------------------+   +---------------------+
          |   ^                         ^
          |   |                         |
          |   |LoanReturned             | consumes LoanReturned
          |   +-------------------------+
          |   checks ActiveReservation
          v
    +------------------+
    |   Billing BC     |
    |------------------|
    | Fine             |
    | Payment          |
    +------------------+

  • وحید نصیری در ۱۴۰۴/۰۵/۳۰ ۱۰:۴۹
    بررسی یک مثال قدم به قدم طراحی یک سیستم پرداخت شارژ آپارتمان با رویکرد Domain-Driven Design (DDD)

    تصور کنید در میانهٔ یک مجتمع مسکونی ایستاده‌ایم؛ جایی که قبض‌های شارژ، پرداخت‌ها و حتی جریمه‌های تأخیر مثل رشته‌هایی نامرئی، زندگی ساکنان را به هم وصل می‌کنند. اگر بخواهیم این رشته‌ها را به زبان Domain-Driven Design توصیف کنیم، باید اول محدوده‌های معنا (Sub-Domain) و سپس مرزهای آنها (Bounded Context) را پیدا کنیم.

    Sub-Domains
    وقتی به سیستم نگاه می‌کنیم، چهار فعالیت کلیدی جلوه می‌کند:
    • صدور قبض شارژ (IssueChargeBill) – جایی که بدهی واحدها محاسبه و به قبض تبدیل می‌شود.
    • مشاهده قبض (ViewChargeBill) – امکان مشاهده و دسترسی به اطلاعات قبض.
    • پرداخت قبض (PayChargeBill) – مرحله‌ای که واحدها مبلغ را تسویه می‌کنند.
    • محاسبه جریمه تأخیر (CalculateLatePenalty) – بخشی که در صورت دیرکرد، مبلغ اضافه‌ای را لحاظ می‌کند.

    اینجا می‌توان گفت:
    • Billing Sub-Domain → مسئول صدور و مدیریت قبض‌ها.
    • Payment Sub-Domain → مسئول پرداخت‌ها و تسویه‌ها.
    • Penalty Sub-Domain → مسئول منطق جریمهٔ تأخیر.
    • Query/Read Sub-Domain → بیشتر به نمایش اطلاعات قبض‌ها می‌پردازد (در اصل یک Supporting Sub-Domain است).

    Bounded Contexts
    هر Sub-Domain خودش را در یک محدودهٔ مستقل پیاده می‌کند.
    Billing Context
    • مسئول: صدور قبض شارژ، نگهداری وضعیت قبض‌ها.
    • خروجی: قبض‌هایی با وضعیت اولیه (Unpaid).
    Payment Context
    • مسئول: ثبت پرداخت‌ها، تغییر وضعیت قبض به Paid.
    • تعامل: قبض را از Billing می‌گیرد و پس از پرداخت، وضعیتش را به‌روز می‌کند.
    Penalty Context
    • مسئول: محاسبه جریمهٔ دیرکرد بر اساس تاریخ سررسید.
    • تعامل: به داده‌های Billing نیاز دارد (تاریخ صدور و سررسید قبض‌ها).
    ChargeQuery Context
    • مسئول: نمایش و جست‌وجوی قبض‌ها برای کاربران.
    • تعامل: از Billing داده می‌خواند، اما تغییر مستقیم نمی‌دهد.

    Context Mapping
    حالا وقت آن است که روابط این مرزها را تصور کنیم.
    • Billing → Payment : Billing قبض را صادر می‌کند، Payment آن را تسویه می‌کند. (Relationship: Customer-Supplier، Payment مشتریِ Billing است.)
    • Billing → Penalty : Penalty برای محاسبه جریمه به اطلاعات Billing وابسته است. (Relationship: Conformist، Penalty باید مدل Billing را بپذیرد.)
    • Billing → ChargeQuery : ChargeQuery داده‌ها را از Billing می‌خواند. (Relationship: Open Host Service یا Published Language، چون Billing داده‌ها را در قالبی قابل مصرف منتشر می‌کند.)

    نقشهٔ سادهٔ روابط (Context Map)
    // Context map
    
       +-------------------+             +-------------------+
       |   Billing Context |<----------->| Payment Context   |
       |  (Issue Charge)   |   Supplier  |  (Pay Charge)     |
       +-------------------+             +-------------------+
                 |
                 | Conformist
                 v
       +-------------------+
       | Penalty Context   |
       | (Late Penalty)    |
       +-------------------+
                 ^
                 | OHS / PL
                 |
       +-------------------+
       | ChargeQuery Ctx   |
       | (View Charges)    |
       +-------------------+
    اینجا ما یک چشم‌انداز کلی ساخته‌ایم: مرزها مشخص، روابط تعریف‌شده و جایگاه هر بخش روشن.

    گام بعدی این است که به درون هر Bounded Context سفر کنیم و ببینیم چه موجودیت‌ها (Entities)، اشیاء ارزشی (Value Objects) و ریشه‌های تجمعی (Aggregate Roots) در دل هرکدام زنده هستند. پس بیاییم مثل یک کاوشگر وارد هر Bounded Context شویم. انگار داریم لایه‌های زمین را کنار می‌زنیم تا ببینیم چه موجودیت‌ها و اشیای پنهانی آن زیر زندگی می‌کنند.

    ۱. Billing Context (صدور قبض)
    اینجا همه‌چیز از یک قبض شروع می‌شود.
    Aggregate Root:
    • ChargeBill (قبض شارژ)
    • موجودیت مرکزی که همه چیز حول آن می‌چرخد.
    Entities:
    • Unit (واحد آپارتمانی) → برای هر قبض باید مشخص باشد که مربوط به کدام واحد است.
    • ChargeItem (آیتم‌های شارژ) → مثلاً هزینه نظافت، نگهبانی، آسانسور.
    Value Objects:
    • Money (مبلغ با واحد پولی)
    • DueDate (تاریخ سررسید)

    ۲. Payment Context (پرداخت قبض)
    اینجا تمرکز روی ثبت تراکنش است.
    Aggregate Root:
    • Payment (پرداخت)
    Entities:
    • Transaction (جزئیات هر تراکنش) → شماره پیگیری، زمان پرداخت.
    • Payer (پرداخت‌کننده، که همان ساکن یا مالک است).
    Value Objects:
    • PaymentMethod (روش پرداخت: کارت، درگاه، نقدی)
    • Money (مبلغ پرداختی، دوباره از همان Value Object استفاده می‌کنیم تا سازگار باشد)

    ۳. Penalty Context (محاسبه جریمه تأخیر)
    اینجا ریاضی و زمان نقش اصلی دارند.
    Aggregate Root:
    • PenaltyPolicy (سیاست محاسبه جریمه)
    Entities:
    • LatePenalty (جریمهٔ محاسبه‌شده برای یک قبض خاص)
    Value Objects:
    • Rate (نرخ جریمه: مثلاً ۲٪ ماهانه)
    • Duration (مدت تأخیر بر اساس تاریخ سررسید)

    ۴. ChargeQuery Context (مشاهده قبض‌ها)
    این بخش بیشتر حالت Read Model دارد. ممکن است حتی Aggregate نداشته باشد و فقط Projection از داده‌های Billing باشد.
    Entities / Read Models:
    • ChargeBillView (نمای خلاصه قبض)
    • PaymentStatusView (وضعیت پرداخت قبض)
    Value Objects:
    • Period (بازه زمانی جستجو، مثل فروردین ۱۴۰۴)

    ارتباط Aggregateها
    اگر بخواهیم به صورت تصویری در ذهن بسازیم:
    • ChargeBill در Billing خلق می‌شود.
    • Payment در Payment وضعیت آن را تغییر می‌دهد.
    • PenaltyPolicy از Billing سررسیدها را می‌گیرد و LatePenalty تولید می‌کند.
    • ChargeBillView در ChargeQuery تنها مصرف‌کنندهٔ داده است و هیچ منطق تجاری اضافه نمی‌کند.

  • وحید نصیری در ۱۴۰۴/۰۵/۳۰ ۱۱:۰۵
    بررسی یک مثال قدم به قدم طراحی یک سیستم رزرو وقت ویزیت در مطب پزشک با رویکرد Domain-Driven Design (DDD)

    فرض کنید وارد دنیای یک مطب پزشک شده‌ایم، جایی که بیماران به دنبال وقت ویزیت هستند، پزشکان زمان‌های خالی دارند، و در نهایت پرداخت‌ها باید ساماندهی شوند. حالا ببینیم چطور با DDD و کمی کمک از EF Core می‌توانیم این پیچیدگی را به چند بخش قابل فهم تقسیم کنیم.

    ۱. کشف Sub-Domainها
    وقتی به یک سیستم رزرو وقت فکر می‌کنیم، درواقع با چند زیردامنه (Sub-Domain) روبه‌رو هستیم:
    • Scheduling (زمان‌بندی و رزرو): قلب ماجرا همین‌جاست؛ جایی که وقت‌های خالی پزشکان مدیریت می‌شود و بیماران رزرو می‌کنند.
    • Billing & Payment (صورتحساب و پرداخت): بعد از رزرو، بحث پول وسط می‌آید. این زیردامنه فقط به جریان مالی سروکار دارد.
    • User Management (مدیریت کاربران): اطلاعات پایهٔ بیماران و پزشکان، بدون اینکه خیلی وارد جزئیات بالینی شویم.

    ۲. Bounded Contextها
    هر زیردامنه نیاز دارد برای خودش مرزی داشته باشد؛ چیزی شبیه اتاق‌های جداگانه در یک کلینیک.
    Scheduling BC
    • مسئول مدیریت نوبت‌ها (Appointment)
    • قوانین: جلوگیری از تداخل زمان‌ها، امکان لغو یا تغییر وضعیت وقت.
    Billing & Payment BC
    • مدیریت پرداخت‌ها (Payment)
    • قوانین: موفق بودن یا شکست خوردن تراکنش، صدور رسید.
    User Management BC
    • مدیریت اطلاعات پایهٔ پزشک و بیمار
    • قوانین: ثبت مشخصات فردی، نگهداری داده‌های هویتی، بدون ورود به فرآیندهای پزشکی.

    ۳. ارتباطات بین Contextها (Context Mapping)
    اگر بخواهیم این دنیا را روی نقشه بکشیم:
    • Scheduling → User Management
    • برای رزرو یک وقت، باید هویت پزشک و بیمار را بشناسیم. پس Scheduling وابسته به اطلاعات پایه‌ای User Management است. (Dependency)
    • Scheduling → Billing & Payment
    • وقتی وقت رزرو شد، جریان کار به Payment سپرده می‌شود. اما این وابستگی یک‌طرفه است: Payment فقط به سرنوشت خودش اهمیت می‌دهد و از جزئیات نوبت خبر ندارد. (Customer-Supplier)

    ۴. طراحی Aggregateها و موجودیت‌ها
    در Scheduling BC
    Aggregate Root: Appointment
    • Properties: تاریخ/ساعت، وضعیت (Reserved, Cancelled, Completed)
    • References: بیمار، پزشک (به صورت Value Object یا Reference ID، نه Entity کامل)
    Entities:
    • (اینجا Appointment کافی است؛ پزشک و بیمار در این Context فقط نقش reference دارند.)
    Value Objects:
    • TimeSlot (نمایش بازه زمانی)
    • AppointmentStatus

    در Billing & Payment BC
    Aggregate Root: Payment
    • Properties: مبلغ، روش پرداخت (کارت، نقدی)، وضعیت پرداخت (Pending, Success, Failed)
    • References: شناسهٔ وقت (AppointmentId)
    Value Objects:
    • Money (شامل Amount و Currency)
    • PaymentMethod

    در User Management BC
    Aggregate Rootها:
    • Patient
    • Doctor
    Entities:
    • Patient: شامل اطلاعات هویتی، شماره تماس
    • Doctor: شامل تخصص، اطلاعات تماس
    Value Objects:
    • Name, ContactInfo, Specialty

    ۵. نقشهٔ سادهٔ روابط (Context Map)
    //Context map
    
       [User Management BC]
             ↑
             │  (اطلاعات پایه بیمار/پزشک)
             │
       [Scheduling BC]  ───────►  [Billing & Payment BC]
             │                   (پرداخت بابت وقت رزرو شده)
             │
             (قلب سیستم: وقت ویزیت)