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

نرم‌افزاری که فقط کار می‌کند، هنوز لزوماً خوب نیست

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

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

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

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

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

فصل‌های نخست کتاب معماری تمیز (Clean Architecture) از همین شکاف آغاز می‌کنند: نرم‌افزار فقط برای اجرای رفتار امروز ساخته نمی‌شود؛ نرم‌افزار باید بتواند در برابر تغییرهای فردا هم دوام بیاورد. این حرف ساده به نظر می‌رسد، اما در عمل یکی از دشوارترین بحث‌های میان مهندسان، مدیران محصول و ذی‌نفعان کسب‌وکار است.

حرف اصلی

نرم‌افزار دو ارزش دارد: ارزشی که امروز از راه رفتار درست به کاربر می‌دهد، و ارزشی که در آینده از راه تغییرپذیری به تیم و کسب‌وکار برمی‌گرداند. معماری، بیش از هر چیز، نگهبان ارزش دوم است.

دو ارزش نرم‌افزار

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

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

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

چرا ارزش نخست بهتر دیده می‌شود؟

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

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

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

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

این‌ها دیگر حرف‌های مبهم درباره‌ی «کد تمیز» نیستند. این‌ها توضیح می‌دهند چرا یک تصمیم امروز، مسیر تغییر فردا را تنگ می‌کند.

سوءبرداشت رایج

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

وقتی فقط رفتار را می‌بینیم، چه چیزی پنهان می‌ماند؟

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

async function placeOrder(input) {
const discount = input.totalPrice > 1_000_000 ? 0.1 : 0;
const finalPrice = input.totalPrice * (1 - discount);

const order = await ordersRepository.create({
userId: input.userId,
items: input.items,
finalPrice,
});

await notificationClient.send(input.userId, 'سفارش شما ثبت شد.');

return order;
}

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

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

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

src/
order/
order-service.ts
order-repository.ts
pricing/
discount-policy.ts
notification/
notification-gateway.ts

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

مهندس باید از چه چیزی دفاع کند؟

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

این دفاع وقتی جدی می‌شود که به چند پرسش پاسخ بدهد:

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

وقتی مهندس چنین پرسش‌هایی را مطرح می‌کند، بحث از «من این مدل را دوست دارم» به «این تصمیم هزینه‌ی تغییر را بالا یا پایین می‌برد» منتقل می‌شود.

پرسش‌های عملی هنگام طراحی

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

معماری خوب همیشه از روز اول بزرگ نیست

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

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

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

type NotificationGateway = {
sendOrderCreatedMessage(userId: string, orderId: string): Promise<void>;
};

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

«بعداً درستش می‌کنیم» چرا خطرناک است؟

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

یک راه بهتر این است که تصمیم‌های موقت را آشکار کنیم:

تصمیم موقت:
در نسخه‌ی نخست، قانون تخفیف داخل سرویس سفارش پیاده‌سازی شد.

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

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

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

نکته‌ی مهم

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

پیشنهاد عملی

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

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

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

pnpm test
pnpm typecheck
pnpm build

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

جمع‌بندی

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

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

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