پرش به مطلب اصلی

ارتباط بین ماژولی: مفاهیم و مزایای جداسازی

مقدمه

در معماری مونولیت مدولار، چالش اصلی حفظ استقلال ماژول‌ها در عین فراهم کردن ارتباط بین ماژولی ضروری است. این مستند مفاهیم، دغدغه‌ها و مزایای استراتژی جداسازی پیاده‌سازی شده در پروژه Planet را بررسی می‌کند.

جداسازی چیست؟

جداسازی یک شیوه معماری برای کاهش وابستگی‌های بین ماژول‌ها است که به آن‌ها اجازه می‌دهد به صورت مستقل و بدون تأثیرگذاری بر یکدیگر تکامل یابند. در زمینه ارتباط بین ماژولی، این به معنای:

  • ماژول‌ها به صورت مستقیم به پیاده‌سازی‌های داخلی یکدیگر وابسته نیستند
  • تغییرات در یک ماژول نیازی به تغییر در ماژول‌های وابسته ندارد
  • ماژول‌ها می‌توانند به صورت مستقل تست، استقرار و نگهداری شوند
  • سیستم انعطاف‌پذیر و سازگار با نیازمندی‌های آینده باقی می‌ماند

مشکل: وابستگی شدید

مشکلات رویکرد سنتی

در یک برنامه مدولار معمولی بدون جداسازی مناسب، ماژول‌ها اغلب به صورت مستقیم با یکدیگر ارتباط برقرار می‌کنند:

❌ مشکل: ماژول A به صورت مستقیم به داخلی‌های ماژول B وابسته است
class AccountingService
{
public function __construct(
private CartRepository $cartRepository, // وابستگی مستقیم
private PaymentRepository $paymentRepository, // وابستگی مستقیم
private UserRepository $userRepository // وابستگی مستقیم
) {}

public function createDocument(int $cartId): void
{
// دسترسی مستقیم به ریپوزیتوری‌های ماژول‌های دیگر
$cart = $this->cartRepository->find($cartId);
$payment = $this->paymentRepository->findByCart($cartId);
$user = $this->userRepository->find($cart->user_id);

// منطق کسب‌وکار...
}
}

پیامدهای وابستگی شدید

مشکلات حیاتی

مشکلات زیر از وابستگی شدید بین ماژول‌ها ناشی می‌شوند:

  • ماژول A به ریپوزیتوری ماژول B وابسته است
  • ماژول B به موجودیت‌های ماژول C وابسته است
  • تغییرات در چندین ماژول موج می‌زنند
  • وابستگی‌های دایره‌ای رایج می‌شوند
پیامدهای اضافی

نقض مرزهای معماری

  • ماژول‌ها اطلاعات زیادی درباره یکدیگر دارند
  • جزئیات پیاده‌سازی داخلی از مرزها نشت می‌کند
  • منطق دامنه در سراسر ماژول‌ها پخش می‌شود
  • اصل مسئولیت واحد نقض می‌شود

کابوس نگهداری

  • تغییرات در یک ماژول دیگران را می‌شکند
  • توسعه‌دهندگان باید چندین ماژول را بفهمند
  • بازسازی کد خطرناک و پرهزینه می‌شود
  • بدهی فنی به سرعت انباشته می‌شود

راه‌حل: استراتژی جداسازی

پروژه Planet یک استراتژی جداسازی دوشاخه بر اساس اصل تفکیک دستور و پرس‌وجو (CQS) پیاده‌سازی می‌کند:

1. زیرساخت پرس‌وجو (عملیات خواندن)

برای خواندن داده از ماژول‌های دیگر، از اصل وارونگی وابستگی استفاده می‌کنیم:

ویژگی‌های کلیدی
  • مصرف‌کننده به انتزاع (رابط) وابسته است، نه پیاده‌سازی
  • ارائه‌دهنده رابط را در دامنه خود پیاده‌سازی می‌کند
  • داده از طریق DTOهای تغییرناپذیر منتقل می‌شود
  • دسترسی مستقیم به ریپوزیتوری‌ها یا موجودیت‌ها وجود ندارد

2. زیرساخت دستور (عملیات نوشتن)

برای تغییر وضعیت یا فعال‌سازی عملیات در ماژول‌های دیگر، از معماری رویداد-محور استفاده می‌کنیم:

ویژگی‌های کلیدی
  • ناشر نمی‌داند چه کسی به رویدادها گوش می‌دهد
  • مشترکین به صورت مستقل واکنش نشان می‌دهند
  • به صورت پیش‌فرض ناهمزمان (مبتنی بر صف)
  • داده حداقلی برمی‌گرداند (ID، boolean یا void)

دغدغه‌ها و انگیزه‌های اصلی

1. قابلیت نگهداری

دغدغه: با رشد سیستم، تغییرات به طور فزاینده‌ای دشوار و پرخطر می‌شوند.

انگیزه: امکان بازسازی امن و تکامل ماژول‌های جداگانه بدون تأثیر سیستم‌گستر.

چگونه جداسازی کمک می‌کند:

  • تغییرات در ساختار داخلی ماژول B بر ماژول A تأثیر نمی‌گذارد
  • قراردادهای رابط تضمین‌های پایداری فراهم می‌کنند
  • بازسازی می‌تواند به صورت تدریجی و امن انجام شود
  • خطر رگرسیون به حداقل می‌رسد

مثال:

// ماژول B طرح پایگاه داده خود را تغییر می‌دهد
// قبل: ماژول A می‌شکند چون مستقیماً از ریپوزیتوری ماژول B استفاده می‌کرد
// بعد: ماژول A به کار خود ادامه می‌دهد چون فقط به رابط وابسته است

2. قابلیت تست

دغدغه: تست ماژول‌ها به صورت جداگانه زمانی که وابستگی‌های مستقیم دارند دشوار است.

انگیزه: امکان تست‌های واحد سریع و قابل اعتماد بدون پایگاه داده یا وابستگی‌های خارجی.

چگونه جداسازی کمک می‌کند:

  • رابط‌ها به راحتی قابل Mock کردن هستند
  • تست‌ها در میلی‌ثانیه به جای ثانیه اجرا می‌شوند
  • نیازی به پایگاه داده تست یا فیکسچرها نیست
  • تست واحد واقعی امکان‌پذیر می‌شود

مثال:

تست واحد سریع و جداگانه
class AccountingServiceTest extends TestCase
{
public function test_creates_document_successfully()
{
// Mock کردن رابط، نه ریپوزیتوری
$userQuery = Mockery::mock(UserQuery::class);
$userQuery->shouldReceive('getById')
->andReturn(new UserDTO(...));

$service = new AccountingService($userQuery);

// تست سریع و جداگانه
$result = $service->createDocument(1);

$this->assertTrue($result->isSuccess());
}
}

3. مقیاس‌پذیری

مسیر مهاجرت

دغدغه: سیستم باید از رشد و تکامل معماری آینده پشتیبانی کند.

انگیزه: امکان مهاجرت به میکروسرویس‌ها یا سیستم‌های توزیع‌شده در صورت نیاز.

چگونه جداسازی کمک می‌کند:

  • ماژول‌ها از قبل مستقل هستند
  • ارتباط از طریق قراردادهای تعریف‌شده انجام می‌شود
  • می‌توان ماژول‌ها را به سرویس‌های جداگانه استخراج کرد
  • معماری رویداد-محور از سیستم‌های توزیع‌شده پشتیبانی می‌کند

4. استقلال تیم

مزایا برای تیم‌های توسعه
  • تیم‌ها مالک پیاده‌سازی ماژول خود هستند
  • تغییرات رابط نیاز به توافق صریح دارد
  • تیم‌ها می‌توانند به صورت مستقل استقرار کنند
  • تضادهای merge و نیازهای هماهنگی کاهش می‌یابد

5. یکپارچگی دامنه

دغدغه: منطق کسب‌وکار باید در مرزهای دامنه مناسب باقی بماند.

انگیزه: حفظ مدل‌های دامنه تمیز و جلوگیری از نشت منطق.

چگونه جداسازی کمک می‌کند:

  • هر ماژول منطق دامنه خود را کپسوله می‌کند
  • DTOها از نشت موجودیت در مرزها جلوگیری می‌کنند
  • Value Objectها قوانین دامنه را اعمال می‌کنند
  • مدل‌های دامنه خالص و متمرکز باقی می‌مانند

مزایای معماری

1. وارونگی وابستگی (SOLID)

مزیت: ماژول‌های سطح بالا به ماژول‌های سطح پایین وابسته نیستند؛ هر دو به انتزاع وابسته‌اند.

✅ خوب: هر دو به انتزاع وابسته‌اند
interface UserQuery { }

class AccountingService {
public function __construct(
private UserQuery $userQuery // وابسته به انتزاع
) {}
}

class UserQueryImplementation implements UserQuery { }
تأثیر
  • ماژول‌ها می‌توانند به صورت مستقل تکامل یابند
  • پیاده‌سازی‌ها قابل تعویض هستند
  • تست ساده می‌شود
  • مرزهای معماری اعمال می‌شوند

2. مسئولیت واحد (SOLID)

مزیت: هر ماژول یک دلیل برای تغییر دارد.

مدیریت داده‌های کاربر و احراز هویت

3. اصل باز/بسته (SOLID)

مزیت: باز برای توسعه، بسته برای تغییر.

افزودن قابلیت جدید بدون تغییر کد موجود
// افزودن شنونده جدید کد موجود را تغییر نمی‌دهد
class NewFeatureListener
{
public function handle(OrderCompletedEvent $event): void
{
// قابلیت جدید بدون دست زدن به ماژول‌های موجود
}
}

4. ایمنی نوع

مزیت: تضمین‌های زمان کامپایل از طریق نوع‌دهی قوی.

Value Objectها ایمنی نوع را تضمین می‌کنند
// Value Objectها ایمنی نوع را تضمین می‌کنند
public function getById(UserId $id): UserDTO;

// نمی‌توان نوع اشتباه را پاس داد
$user = $userQuery->getById(new UserId(123)); // ✅
$user = $userQuery->getById(123); // ❌ خطای نوع

5. قراردادهای صریح

کد خودمستندساز

رابط‌های واضح و مستندشده تعریف می‌کنند چه چیزی در دسترس است:

  • کد خودمستندساز
  • پشتیبانی از تکمیل خودکار IDE
  • اعتبارسنجی زمان کامپایل
  • مرزهای واضح API

6. تغییرناپذیری

مزیت: DTOها تغییرناپذیر هستند و از تغییرات تصادفی جلوگیری می‌کنند.

DTOهای تغییرناپذیر
class UserDTO
{
public function __construct(
public readonly UserId $id, // قابل تغییر نیست
public readonly string $name, // قابل تغییر نیست
) {}
}
تأثیر
  • به صورت طراحی thread-safe است
  • از باگ‌های ناشی از تغییرات غیرمنتظره جلوگیری می‌کند
  • استدلال درباره جریان داده آسان‌تر است
  • از الگوهای برنامه‌نویسی تابعی پشتیبانی می‌کند

7. قابلیت استفاده مجدد

مزیت: رابط‌های پرس‌وجو می‌توانند توسط چندین ماژول استفاده شوند.

رابط پرس‌وجوی مشترک در سراسر ماژول‌ها
// UserQuery توسط چندین ماژول استفاده می‌شود
class AccountingService {
public function __construct(private UserQuery $userQuery) {}
}

class NotificationService {
public function __construct(private UserQuery $userQuery) {}
}

class ReportingService {
public function __construct(private UserQuery $userQuery) {}
}

8. قابلیت مشاهده

مسیر ممیزی طبیعی

معماری رویداد-محور مسیر ممیزی طبیعی فراهم می‌کند:

  • همه رویدادهای کسب‌وکار مهم صریح هستند
  • افزودن لاگ و مانیتورینگ آسان است
  • در صورت نیاز از event sourcing پشتیبانی می‌کند
  • اشکال‌زدایی آسان‌تر می‌شود

مبادلات و ملاحظات

جداسازی قوی

  • ماژول‌ها واقعاً مستقل هستند
  • می‌توانند به صورت جداگانه تست شوند
  • می‌توانند جداگانه استقرار یابند
  • می‌توانند به صورت مستقل تکامل یابند

ایمنی نوع

  • تشخیص خطا در زمان کامپایل
  • پشتیبانی IDE و تکمیل خودکار
  • اطمینان در بازسازی
  • کد خودمستندساز

قابلیت تست

  • تست‌های واحد سریع
  • Mock کردن آسان
  • بدون وابستگی به پایگاه داده
  • پوشش تست بالا امکان‌پذیر است

مقیاس‌پذیری

  • آماده برای مهاجرت به میکروسرویس‌ها
  • پشتیبانی از سیستم‌های توزیع‌شده
  • مقیاس‌بندی افقی امکان‌پذیر است
  • ایزوله‌سازی عملکرد

قابلیت نگهداری

  • مرزهای واضح
  • بازسازی آسان‌تر
  • کاهش خطر رگرسیون
  • سازماندهی بهتر کد

زمانی که مزایا بر معایب غلبه می‌کنند

از این معماری استفاده کنید وقتی:
  • ✅ سیستم چندین ماژول با مرزهای واضح دارد
  • ✅ قابلیت نگهداری بلندمدت حیاتی است
  • ✅ چندین تیم روی ماژول‌های مختلف کار می‌کنند
  • ✅ سیستم نیاز به مقیاس‌بندی یا تکامل به میکروسرویس‌ها دارد
  • ✅ پوشش تست بالا مورد نیاز است
  • ✅ یکپارچگی دامنه مهم است
ممکن است بیش از حد باشد وقتی:
  • ❌ برنامه CRUD ساده
  • ❌ توسعه‌دهنده واحد یا تیم کوچک
  • ❌ پروژه کوتاه‌مدت
  • ❌ ماژول‌ها به طور ذاتی به شدت مرتبط هستند
  • ❌ عملکرد حیاتی است و سربار غیرقابل قبول است

چه زمانی از این معماری استفاده کنیم

از زیرساخت پرس‌وجو استفاده کنید وقتی:

✅ مورد استفاده خوب: خواندن داده از ماژول دیگر
class InvoiceService {
public function __construct(
private UserQuery $userQuery,
private OrderQuery $orderQuery
) {}
}
سناریوهای ایده‌آل
  1. خواندن داده از ماژول دیگر
  2. چندین ماژول به داده یکسان نیاز دارند - قابل استفاده مجدد در سراسر ماژول‌ها
  3. تست نیاز به ایزوله‌سازی دارد - آسان برای Mock کردن

از زیرساخت دستور/رویداد استفاده کنید وقتی:

✅ مورد استفاده خوب: فعال‌سازی عملیات
// فعال‌سازی عملیات در ماژول‌های دیگر
event(new OrderCompletedEvent($order));
سناریوهای ایده‌آل
  1. فعال‌سازی عملیات در ماژول‌های دیگر
  2. چندین ماژول به یک رویداد واکنش نشان می‌دهند - شنوندگان متعدد (فاکتور، اعلان، تحلیل)
  3. پردازش ناهمزمان مورد نیاز است - پردازش مبتنی بر صف

استفاده نکنید وقتی:

❌ درون یک ماژول - از وابستگی‌های مستقیم استفاده کنید
class UserService {
public function __construct(
private UserRepository $repository // وابستگی مستقیم مشکلی ندارد
) {}
}
از آن اجتناب کنید وقتی:
  • درون یک ماژول
  • عملیات CRUD ساده
  • پاسخ همزمان با داده پیچیده مورد نیاز است

مقایسه با رویکردهای جایگزین

رویکرد:

class AccountingService {
public function __construct(
private UserRepository $userRepository
) {}
}

مزایا:

  • ساده و مستقیم
  • کد کمتری برای نوشتن
  • توسعه اولیه سریع‌تر

معایب:

  • وابستگی شدید بین ماژول‌ها
  • تست به صورت جداگانه سخت است
  • مرزهای معماری را نقض می‌کند
  • بازسازی دشوار است

چه زمانی استفاده کنیم: فقط درون یک ماژول


نتیجه‌گیری

استراتژی جداسازی پیاده‌سازی شده در پروژه Planet به چالش اساسی حفظ استقلال ماژول در عین فراهم کردن ارتباط ضروری می‌پردازد. با جداسازی عملیات خواندن (پرس‌وجوها) از عملیات نوشتن (دستورها/رویدادها)، و با استفاده از رابط‌ها و DTOها برای تعریف قراردادهای واضح، معماری به موارد زیر دست می‌یابد:

دستاوردهای کلیدی
  1. استقلال قوی ماژول
  2. قابلیت تست بالا
  3. ایمنی نوع
  4. قابلیت نگهداری
  5. مقیاس‌پذیری

در حالی که این رویکرد پیچیدگی و کد تکراری اضافی معرفی می‌کند، مزایای بلندمدت از نظر قابلیت نگهداری، قابلیت تست و انعطاف معماری آن را به انتخاب درست برای یک مونولیت مدولار که نیاز به مقیاس‌بندی و تکامل در طول زمان دارد، تبدیل می‌کند.

قانون طلایی

کلید درک چه زمانی این معماری را اعمال کنیم است: از آن برای ارتباط بین ماژولی که جداسازی حیاتی است استفاده کنید، اما از آن برای عملیات درون ماژولی که وابستگی‌های مستقیم ساده‌تر و مناسب‌تر هستند اجتناب کنید.