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

گاهی مرز معماری را می‌کشیم، اما هنوز دیوار نمی‌سازیم

· ۱۰ دقیقه مطالعه
مهدی مالوردی
مهندس نرم‌افزار و نویسندهٔ این سایت

تیم وسط یک تصمیم سخت گیر کرده بود. از یک طرف، چند نفر می‌گفتند باید از همین حالا همه‌چیز را کامل جدا کنیم: ماژول مستقل، پایگاه داده‌ی جدا، قراردادهای رسمی، صف پیام، بسته‌بندی جداگانه و حتی شاید سرویس مستقل. از طرف دیگر، چند نفر می‌گفتند هنوز زود است؛ بهتر است همه‌چیز در همان کد فعلی بماند تا کار سریع‌تر جلو برود. بحث، ظاهراً درباره‌ی معماری بود، اما در عمل به دو گزینه‌ی افراطی تقلیل پیدا کرده بود: یا دیوار کامل بسازیم، یا اصلاً مرزی نکشیم.

مسئله این بود که هر دو طرف بخشی از حقیقت را می‌دیدند. جداکردن کامل، هزینه داشت: کد بیشتر، قرارداد بیشتر، آزمون بیشتر، هماهنگی بیشتر و اصطکاک بیشتر در توسعه‌ی روزمره. اما نساختن هیچ مرزی هم بی‌هزینه نبود. اگر بخش تازه واقعاً قرار بود رشد کند، به چند مسیر محصولی وصل شود و قانون‌های خودش را پیدا کند، چسباندن آن به کد فعلی، تغییرهای آینده را سخت‌تر می‌کرد. تیم نه می‌خواست اسیر طراحی زودهنگام شود، نه می‌خواست بعداً با توده‌ای از وابستگی‌های درهم روبه‌رو شود.

نقشه‌ی ساختمانی با خط‌کشی مرزها، اما فقط بخشی از دیوارها ساخته شده‌اند؛ استعاره‌ای از مرزهای معماری نیمه‌کاره.

فصل بیست‌وچهارم کتاب معماری تمیز (Clean Architecture) دقیقاً به همین ناحیه‌ی خاکستری می‌پردازد: مرزهای نیمه‌کاره. این فصل یادآوری می‌کند که معماری فقط انتخاب میان «جداسازی کامل» و «بی‌مرزی کامل» نیست. گاهی لازم است مرز را ببینیم، جهت وابستگی را کنترل کنیم، قرارداد ذهنی یا کدی آن را مشخص کنیم، اما هنوز هزینه‌ی کامل جداسازی را نپردازیم.

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

مرز نیمه‌کاره چیست؟

مرز نیمه‌کاره یعنی تصمیم معماری را تشخیص داده‌ایم و جهت وابستگی را تا حد لازم کنترل می‌کنیم، اما هنوز همه‌ی هزینه‌های جداسازی کامل، مثل استقرار جدا، پایگاه داده‌ی جدا، بسته‌ی جدا یا ارتباط بین‌فرایندی را نمی‌پردازیم.

مرز یعنی چه، دیوار یعنی چه؟

مرز معماری یعنی دو بخش از سامانه دلیل‌های متفاوتی برای تغییر دارند و نباید بی‌محابا در هم فرو بروند. برای نمونه، بخش محاسبه‌ی قیمت ممکن است با قانون‌های مالی، سیاست‌های تخفیف و تصمیم‌های محصولی تغییر کند. بخش ارسال اعلان ممکن است با ابزار پیام‌رسانی، قالب پیام یا کانال ارتباطی تغییر کند. این دو بخش می‌توانند با هم همکاری کنند، اما لازم نیست جزئیات داخلی همدیگر را بشناسند.

دیوار کامل، شکل پرهزینه‌تر همین مرز است. مثلاً دو سرویس جدا می‌سازیم، هرکدام استقرار جدا دارند، ارتباطشان از راه شبکه است، قرارداد نسخه‌بندی‌شده دارند، و شاید داده‌هایشان هم جدا نگه‌داری شود. این دیوار گاهی لازم است، اما همیشه نه. اگر هنوز نمی‌دانیم مرز واقعاً پایدار است یا نه، ساختن دیوار کامل می‌تواند هزینه‌ای زودهنگام باشد.

مرز نیمه‌کاره بین این دو قرار می‌گیرد. یعنی می‌گوییم «این دو بخش را یکی نمی‌بینیم»، اما هنوز آن‌ها را به دو سرویس یا دو بسته‌ی کاملاً مستقل تبدیل نمی‌کنیم. شاید فقط یک واسط کوچک تعریف کنیم. شاید فقط وابستگی مستقیم را یک‌طرفه نگه داریم. شاید فقط دسترسی به داده را از راه یک درگاه انجام دهیم. شاید فقط نام‌گذاری و ساختار پوشه را طوری بچینیم که بعداً جداکردن آن ممکن باشد.

چرا مرز کامل همیشه تصمیم خوبی نیست؟

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

فرض کنیم تیم می‌خواهد بخش «امتیاز وفاداری کاربران» را اضافه کند. هنوز معلوم نیست این بخش فقط چند قانون ساده خواهد داشت یا در آینده به موتور مستقل پیشنهاد، گزارش، پاداش و کمپین تبدیل می‌شود. اگر از روز نخست آن را به یک سرویس جدا با پایگاه داده‌ی مستقل و قراردادهای رسمی تبدیل کنیم، شاید برای مسئله‌ای کوچک، بار عملیاتی و توسعه‌ای سنگینی ساخته باشیم. هر تغییر ساده نیازمند هماهنگی میان سرویس‌ها، داده‌ها و استقرارها می‌شود.

از طرف دیگر، اگر همین بخش را کاملاً داخل کنترلرها و مدل‌های فعلی پخش کنیم، بعداً بیرون‌کشیدن آن سخت می‌شود. قانون امتیازدهی در چند جای کد تکرار می‌شود، داده‌هایش با داده‌های دیگر قاطی می‌شود، و هر تغییر کوچک، مسیرهای نامرتبط را هم تحت تأثیر قرار می‌دهد.

مرز نیمه‌کاره اینجا کمک می‌کند. می‌توانیم هسته‌ی قانون امتیاز را در یک ماژول مشخص نگه داریم، ورودی و خروجی آن را روشن کنیم، وابستگی آن به جزئیات بیرونی را کنترل کنیم، اما هنوز سرویس جدا، پایگاه داده‌ی جدا یا قرارداد شبکه‌ای نسازیم.

مرز زودهنگام هم می‌تواند بدهی بسازد

همیشه مشکل از کمبود معماری نیست. گاهی مرزهایی که خیلی زود، خیلی سخت و بدون شواهد کافی ساخته می‌شوند، خودشان هزینه‌ی تغییر را بالا می‌برند. جداسازی کامل وقتی ارزش دارد که دلیل تغییر، فشار رشد یا نیاز عملیاتی آن را توجیه کند.

نمونه‌ی ساده از مرز نیمه‌کاره

فرض کنید در یک سامانه‌ی فروش، بخشی برای محاسبه‌ی امتیاز وفاداری اضافه شده است. ساده‌ترین راه این است که محاسبه را همان‌جا بنویسیم که سفارش ثبت می‌شود:

async function createOrder(input) {
const order = await ordersRepository.create(input);

const points = Math.floor(order.totalPrice / 100_000);
await usersRepository.addLoyaltyPoints(order.userId, points);

return {id: order.id};
}

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

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

order/
create-order.ts
loyalty/
loyalty-policy.ts
loyalty-port.ts

و در کد:

type LoyaltyPort = {
grantPoints(userId: string, points: number): Promise<void>;
};

async function createOrder(input) {
const order = await ordersRepository.create(input);
const points = loyaltyPolicy.calculateForOrder(order);

await loyaltyPort.grantPoints(order.userId, points);

return {id: order.id};
}

اینجا هنوز سرویس جدا نداریم. هنوز ارتباط شبکه‌ای نداریم. شاید همه‌چیز در همان برنامه و همان مخزن کد باشد. اما مرز دیده شده است: قانون امتیازدهی در دل ثبت سفارش دفن نشده، و ثبت سفارش لازم نیست جزئیات ذخیره‌سازی امتیاز را بداند. اگر بعداً بخش وفاداری بزرگ‌تر شد، جداکردن آن از نقطه‌ای مشخص آغاز می‌شود، نه از میان کدی پخش‌شده و مبهم.

مرز نیمه‌کاره یعنی تعلیق تصمیم، نه فرار از تصمیم

نکته‌ی ظریف اینجاست: مرز نیمه‌کاره نباید بهانه‌ای برای شلختگی باشد. این‌که بگوییم «فعلاً کامل جدا نمی‌کنیم» با این‌که بگوییم «فعلاً هیچ مرزی نداریم» فرق دارد. در حالت اول، تصمیم معماری را دیده‌ایم، فقط هزینه‌ی کامل آن را عقب انداخته‌ایم. در حالت دوم، مسئله را نادیده گرفته‌ایم.

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

مرز احتمالی:
بخش امتیاز وفاداری از ثبت سفارش جدا نگه داشته شود.

وضعیت فعلی:
فعلاً در همان برنامه اجرا می‌شود، اما قانون امتیازدهی در ماژول جداست و ثبت سفارش فقط از درگاه وفاداری استفاده می‌کند.

نشانه‌ی تکمیل مرز:
اگر امتیاز وفاداری از بیش از دو مسیر محصولی استفاده شد، یا نیاز به زمان‌بندی مستقل پیدا کرد، یا تیم جداگانه‌ای مالک آن شد، جداسازی کامل بررسی شود.

این یادداشت کوتاه، تصمیم را از یک حس مبهم به یک توافق قابل‌پیگیری تبدیل می‌کند. تیم می‌داند چرا امروز دیوار کامل نمی‌سازد، و می‌داند چه زمانی باید دوباره تصمیم را بررسی کند.

مرز نیمه‌کاره و قانون وابستگی

مرز نیمه‌کاره فقط زمانی مفید است که جهت وابستگی را جدی بگیریم. اگر دو بخش هنوز در یک فرایند و یک مخزن کد هستند، دست‌کم باید مراقب باشیم کدام بخش کدام را می‌شناسد. اگر هسته‌ی قانون امتیازدهی به کنترلر سفارش، مدل پایگاه داده یا جزئیات پیام‌رسانی وابسته شود، دیگر مرز نیمه‌کاره نداریم؛ فقط پوشه‌ای جدا داریم که از درون به همه‌چیز وصل است.

در مرز نیمه‌کاره، لازم نیست همه‌چیز را سخت و رسمی کنیم، اما باید چند چیز روشن باشد: ورودی و خروجی بخش چیست، چه کسی تصمیم اصلی را می‌گیرد، جزئیات بیرونی کجا قرار دارند، و اگر روزی خواستیم جداسازی را کامل کنیم، کدام وابستگی‌ها مانع خواهند شد.

به بیان دیگر، مرز نیمه‌کاره دیوار نیست، اما خط روی نقشه هم نباید تزئینی باشد. اگر خطی کشیده‌ایم، باید رفتار امروزمان با آن سازگار باشد.

تصمیم‌گیری عملی

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

کجا مرز نیمه‌کاره کافی نیست؟

مرز نیمه‌کاره برای همه‌چیز مناسب نیست. اگر دو بخش نیاز عملیاتی کاملاً متفاوت دارند، مالکیت تیمی جدا دارند، نرخ تغییر و مقیاس متفاوت دارند، یا خرابی یکی نباید دیگری را زمین بزند، شاید مرز نیمه‌کاره کافی نباشد. در چنین وضعی، دیوار واقعی لازم می‌شود: جداسازی استقرار، داده، قرارداد و مشاهده‌پذیری.

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

پس پرسش درست این نیست که «مرز نیمه‌کاره خوب است یا بد؟» پرسش درست این است که «برای این تصمیم، در این مرحله از محصول، با این هزینه و این ریسک، چه مقدار مرز کافی است؟»

پیشنهاد عملی

برای هر مرزی که در ذهن داری، اول دلیل آن را بنویس. آیا دو بخش دلیل تغییر متفاوت دارند؟ آیا احتمال دارد در آینده مالکیت جدا پیدا کنند؟ آیا قانون یکی نباید زیر جزئیات دیگری پنهان شود؟ اگر نتوانی دلیل مرز را روشن بگویی، شاید هنوز زمان ساختن آن نرسیده باشد.

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

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

برای اعتبارسنجی، یک آزمون ساده انجام بده: ببین آیا می‌توانی بخش موردنظر را بدون اجرای مسیرهای نامرتبط صدا بزنی یا نه. اگر پروژه‌ی تو ابزارهای رایج جاوااسکریپت یا تایپ‌اسکریپت دارد، پس از تغییر ساختار، فرمان‌های مناسب را اجرا کن:

pnpm test
pnpm typecheck
pnpm build

اگر آزمون خودکار نداری، یک سناریوی دستی دقیق بنویس و بعد از جداسازی آن را اجرا کن. هدف از مرز نیمه‌کاره این نیست که خیالمان راحت شود؛ هدف این است که با هزینه‌ای متناسب، خطر تغییرهای آینده را کمتر کنیم.

جمع‌بندی

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

تیم‌های واقعی نباید میان دو افراط زندانی شوند: جداسازی کامل از روز نخست، یا بی‌مرزی کامل تا روز بحران. معماری خوب فقط در طراحی‌های بزرگ و رسمی دیده نمی‌شود؛ گاهی در همین تصمیم‌های کوچک و میانی دیده می‌شود. در این‌که قانون را کجا می‌گذاریم، وابستگی را به کدام سمت نگه می‌داریم، و برای چه زمانی نشانه‌ی بازنگری تعریف می‌کنیم.

پرسش پایانی این نیست که «آیا دیوار ساخته‌ایم؟» پرسش بهتر این است: «آیا می‌دانیم دیوار احتمالی از کجا باید ساخته شود، و آیا امروز کاری نکرده‌ایم که ساختن آن در آینده بی‌دلیل سخت شود؟» همین پرسش، مرز نیمه‌کاره را از بی‌تصمیمی جدا می‌کند.