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

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

مقدمه​

در معماری مونولیت مدولار، چالش اصلی حفظ استقلال ماژول‌ها در عین فراهم کردن ارتباط بین ماژولی ضروری است. این مستند مفاهیم، دغدغه‌ها و مزایای استراتژی جداسازی پیاده‌سازی شده در پروژه 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. مقیاس‌پذیری

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

قانون طلایی

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