ارتباط بین ماژولی: مفاهیم و مزایای جداسازی
مقدمه
در معماری مونولیت مدولار، چالش اصلی حفظ استقلال ماژولها در عین فراهم کردن ارتباط بین ماژولی ضروری است. این مستند مفاهیم، دغدغهها و مزایای استراتژی جداسازی پیادهسازی شده در پروژه Planet را بررسی میکند.
جداسازی چیست؟
جداسازی یک شیوه معماری برای کاهش وابستگیهای بین ماژولها است که به آنها اجازه میدهد به صورت مستقل و بدون تأثیرگذاری بر یکدیگر تکامل یابند. در زمینه ارتباط بین ماژولی، این به معنای:
- ماژولها به صورت مستقیم به پیادهسازیهای داخلی یکدیگر وابسته نیستند
- تغییرات در یک ماژول نیازی به تغییر در ماژولهای وابسته ندارد
- ماژولها میتوانند به صورت مستقل تست، استقرار و نگهداری شوند
- سیستم انعطافپذیر و سازگار با نیازمندیهای آینده باقی میماند
مشکل: وابستگی شدید
مشکلات رویکرد سنتی
در یک برنامه مدولار معمولی بدون جداسازی مناسب، ماژولها اغلب به صورت مستقیم با یکدیگر ارتباط برقرار میکنند:
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 وابسته است
- تغییرات در چندین ماژول موج میزنند
- وابستگیهای دایرهای رایج میشوند
- نمیتوان ماژول A را بدون پایگاه داده ماژول B تست کرد
- Mock کردن پیچیده و شکننده میشود
- تستها به دلیل وابستگی به پایگاه داده کند هستند
- تستهای یکپارچه تنها گزینه میشوند
- نمیتوان ماژول B را بدون تأثیر بر ماژول A جایگزین کرد
- نمیتوان ماژولها را به میکروسرویسها تبدیل کرد
- پیادهسازی feature flag دشوار است
- پشتیبانی از چندین پیادهسازی سخت است
نقض مرزهای معماری
- ماژولها اطلاعات زیادی درباره یکدیگر دارند
- جزئیات پیادهسازی داخلی از مرزها نشت میکند
- منطق دامنه در سراسر ماژولها پخش میشود
- اصل مسئولیت واحد نقض میشود
کابوس نگهداری
- تغییرات در یک ماژول دیگران را میشکند
- توسعهدهندگان باید چندین ماژول را بفهمند
- بازسازی کد خطرناک و پرهزینه میشود
- بدهی فنی به سرعت انباشته میشود
راهحل: استراتژی جداسازی
پروژه 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ها ایمنی نوع را تضمین میکنند
public function getById(UserId $id): UserDTO;
// نمیتوان نوع اشتباه را پاس داد
$user = $userQuery->getById(new UserId(123)); // ✅
$user = $userQuery->getById(123); // ❌ خطای نوع
5. قراردادهای صریح
رابطهای واضح و مستندشده تعریف میکنند چه چیزی در دسترس است:
- کد خودمستندساز
- پشتیبانی از تکمیل خودکار IDE
- اعتبارسنجی زمان کامپایل
- مرزهای واضح API
6. تغییرناپذیری
مزیت: 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 کردن آسان
- بدون وابستگی به پایگاه داده
- پوشش تست بالا امکانپذیر است
مقیاسپذیری
- آماده برای مهاجرت به میکروسرویسها
- پشتیبانی از سیستمهای توزیعشده
- مقیاسبندی افقی امکانپذیر است
- ایزولهسازی عملکرد
قابلیت نگهداری
- مرزهای واضح
- بازسازی آسانتر
- کاهش خطر رگرسیون
- سازماندهی بهتر کد
افزایش پیچیدگی
- فایلهای بیشتری برای نگهداری
- لایههای بیشتری برای درک
- منحنی یادگیری تندتر
- انتزاعهای اضافی
کد تکراری
- رابط + پیادهسازی + DTO برای هر پرسوجو
- نگاشت از Entity به DTO
- کد بیشتری برای نوشتن در ابتدا
- الگوهای تکراری
سربار عملکرد
- ایجاد و نگاشت DTO
- تخصیص شیء اضافی
- سربار حافظه اندک
- هزینه غیرمستقیم (حداقلی)
زمان توسعه
- توسعه اولیه کندتر
- برنامهریزی بیشتر مورد نیاز
- طراحی دقیق رابطها ضروری است
- فایلهای بیشتری برای ایجاد
پیچیدگی اشکالزدایی
- ردیابی جریان رویداد سختتر است
- عملیات ناهمزمان اشکالزدایی را پیچیده میکنند
- غیرمستقیم بودن بیشتر برای دنبال کردن
- نیاز به لاگگذاری بهتر
زمانی که مزایا بر معایب غلبه میکنند
- ✅ سیستم چندین ماژول با مرزهای واضح دارد
- ✅ قابلیت نگهداری بلندمدت حیاتی است
- ✅ چندین تیم روی ماژولهای مختلف کار میکنند
- ✅ سیستم نیاز به مقیاسبندی یا تکامل به میکروسرویسها دارد
- ✅ پوشش تست بالا مورد نیاز است
- ✅ یکپارچگی دامنه مهم است
- ❌ برنامه CRUD ساده
- ❌ توسعهدهنده واحد یا تیم کوچک
- ❌ پروژه کوتاهمدت
- ❌ ماژولها به طور ذاتی به شدت مرتبط هستند
- ❌ عملکرد حیاتی است و سربار غیرقابل قبول است
چه زمانی از این معماری استفاده کنیم
از زیرساخت پرسوجو استفاده کنید وقتی:
class InvoiceService {
public function __construct(
private UserQuery $userQuery,
private OrderQuery $orderQuery
) {}
}
- خواندن داده از ماژول دیگر
- چندین ماژول به داده یکسان نیاز دارند - قابل استفاده مجدد در سراسر ماژولها
- تست نیاز به ایزولهسازی دارد - آسان برای Mock کردن
از زیرساخت دستور/رویداد استفاده کنید وقتی:
// فعالسازی عملیات در ماژولهای دیگر
event(new OrderCompletedEvent($order));
- فعالسازی عملیات در ماژولهای دیگر
- چندین ماژول به یک رویداد واکنش نشان میدهند - شنوندگان متعدد (فاکتور، اعلان، تحلیل)
- پردازش ناهمزمان مورد نیاز است - پردازش مبتنی بر صف
استفاده نکنید وقتی:
class UserService {
public function __construct(
private UserRepository $repository // وابستگی مستقیم مشکلی ندارد
) {}
}
- درون یک ماژول
- عملیات CRUD ساده
- پاسخ همزمان با داده پیچیده مورد نیاز است
مقایسه با رویکردهای جایگزین
- دسترسی مستقیم به ریپوزیتوری
- پایگاه داده مشترک
- REST API بین ماژولها
- لایه سرویس مشترک
- رویکرد ما: مبتنی بر رابط
رویکرد:
class AccountingService {
public function __construct(
private UserRepository $userRepository
) {}
}
مزایا:
- ساده و مستقیم
- کد کمتری برای نوشتن
- توسعه اولیه سریعتر
معایب:
- وابستگی شدید بین ماژولها
- تست به صورت جداگانه سخت است
- مرزهای معماری را نقض میکند
- بازسازی دشوار است
چه زمانی استفاده کنیم: فقط درون یک ماژول
رویکرد:
// ماژول A مستقیماً جداول ماژول B را پرسوجو میکند
$user = DB::table('users')->where('id', $userId)->first();
مزایا:
- بسیار ساده
- بدون لایه اضافی
- دسترسی مستقیم به داده
معایب:
- وابستگی بسیار شدید
- تغییرات طرح پایگاه داده چندین ماژول را میشکند
- بدون کپسولهسازی
- نمیتوان به میکروسرویسها مهاجرت کرد
- همه اصول معماری را نقض میکند
چه زمانی استفاده کنیم: هرگز در معماری مدولار
رویکرد:
class AccountingService {
public function __construct(
private HttpClient $httpClient
) {}
public function getUser(int $id): array
{
return $this->httpClient->get("/api/users/{$id}");
}
}
مزایا:
- جداسازی کامل
- آماده برای میکروسرویسها
- مستقل از زبان
معایب:
- سربار شبکه در مونولیت
- پیچیدگی بیش از حد
- عملکرد کندتر
- نیاز به نسخهبندی API
چه زمانی استفاده کنیم: وقتی ماژولها در واقع سرویسهای جداگانه هستند
رویکرد:
class SharedUserService {
// توسط همه ماژولها استفاده میشود
}
مزایا:
- منطق متمرکز
- پیدا کردن کد آسان
معایب:
- سرویس خدایی ایجاد میکند
- وابستگی شدید
- نگهداری سخت
- SRP را نقض میکند
چه زمانی استفاده کنیم: به ندرت، فقط برای دغدغههای واقعاً مشترک
رویکرد:
// رابط در هسته مشترک
interface UserQuery extends QueryInterface {
public function getById(UserId $id): UserDTO;
}
// پیادهسازی در ماژول کاربر
class UserQueryImplementation implements UserQuery { }
// استفاده در ماژولهای دیگر
class AccountingService {
public function __construct(
private UserQuery $userQuery // وابسته به رابط
) {}
}
مزایا:
- جداسازی قوی
- ایمنی نوع
- قابلیت تست
- قابلیت نگهداری
- مقیاسپذیری
- قراردادهای واضح
معایب:
- کد اولیه بیشتر
- منحنی یادگیری
- سربار اندک
چه زمانی استفاده کنیم: مونولیتهای مدولار با مرزهای واضح
نتیجهگیری
استراتژی جداسازی پیادهسازی شده در پروژه Planet به چالش اساسی حفظ استقلال ماژول در عین فراهم کردن ارتباط ضروری میپردازد. با جداسازی عملیات خواندن (پرسوجوها) از عملیات نوشتن (دستورها/رویدادها)، و با استفاده از رابطها و DTOها برای تعریف قراردادهای واضح، معماری به موارد زیر دست مییابد:
- استقلال قوی ماژول
- قابلیت تست بالا
- ایمنی نوع
- قابلیت نگهداری
- مقیاسپذیری
در حالی که این رویکرد پیچیدگی و کد تکراری اضافی معرفی میکند، مزایای بلندمدت از نظر قابلیت نگهداری، قابلیت تست و انعطاف معماری آن را به انتخاب درست برای یک مونولیت مدولار که نیاز به مقیاسبندی و تکامل در طول زمان دارد، تبدیل میکند.
کلید درک چه زمانی این معماری را اعمال کنیم است: از آن برای ارتباط بین ماژولی که جداسازی حیاتی است استفاده کنید، اما از آن برای عملیات درون ماژولی که وابستگیهای مستقیم سادهتر و مناسبتر هستند اجتناب کنید.