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

وقتی هر قابلیت تازه یعنی دست زدن به کدهای قدیمی

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

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

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

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

function calculateFee(transaction: Transaction) {
if (transaction.type === 'deposit') {
return 0
}

if (transaction.type === 'withdraw') {
return transaction.amount * 0.01
}

if (transaction.type === 'transfer') {
return 500
}

if (transaction.type === 'refund') {
return 0
}
}

در یک فایل دیگر هم همین الگو تکرار شده است:

function buildReportRow(transaction: Transaction) {
if (transaction.type === 'deposit') {
return {title: 'واریز', sign: '+'}
}

if (transaction.type === 'withdraw') {
return {title: 'برداشت', sign: '-'}
}

if (transaction.type === 'transfer') {
return {title: 'انتقال', sign: '-'}
}

if (transaction.type === 'refund') {
return {title: 'برگشت وجه', sign: '+'}
}
}

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

مسئولیت یکتا یعنی یک دلیل برای تغییر، نه فقط یک تابع کوچک‌تر

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

یک کلاس در مرکز که از چند سمت با خواسته‌های مالی، نمایشی، ذخیره‌سازی و گزارشی کشیده می‌شود.

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

class PaymentService {
async process(payment: Payment) {
const fee = payment.amount * 0.01
const finalAmount = payment.amount - fee

const savedPayment = await db.payments.insert({
userId: payment.userId,
amount: payment.amount,
fee,
finalAmount,
status: 'done',
})

return {
id: savedPayment.id,
amount: savedPayment.amount,
fee: savedPayment.fee,
finalAmount: savedPayment.finalAmount,
message: 'پرداخت با موفقیت انجام شد',
}
}
}

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

بیشتر باگ‌ها از جایی آغاز می‌شوند که وضعیت عوض می‌شود

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

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

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

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

شی‌گرایی یعنی کنترل وابستگی، نه فقط ساختن کلاس

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

پروژه از بیرون کاملاً شی‌گرا به نظر می‌رسید. تقریباً برای هر مفهوم، یک کلاس وجود داشت: UserService، OrderManager، PaymentHandler، ReportGenerator و چندین کلاس دیگر. کدها دیگر مثل یک اسکریپت بلند و تخت نبودند. هر چیز، جایی داشت و هر فایل، اسمی آشنا. اما وقتی تیم خواست پایگاه داده را برای آزمون‌ها جایگزین کند، یا بخشی از منطق پرداخت را بدون فریم‌ورک وب اجرا کند، واقعیت خودش را نشان داد: کلاس‌ها زیاد بودند، اما وابستگی‌ها همچنان به دیتابیس، فریم‌ورک و جزئیات بیرونی قفل شده بودند.

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

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

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

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

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

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

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

معماری تمیز را با نقاشی دایره‌ها اشتباه نگیریم

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

تیم تصمیم گرفته بود «معماری تمیز» را جدی‌تر وارد پروژه کند. چند پوشه‌ی تازه ساخته شد: entity، usecase و adapter. نمودار دایره‌ای معروف هم در جلسه‌ی فنی روی تخته کشیده شد. از بیرون، همه‌چیز شبیه یک حرکت درست به نظر می‌رسید: نام‌ها آشنا بودند، ساختار پروژه مرتب‌تر شده بود و کدها دیگر همگی در یک پوشه‌ی بزرگ کنار هم نبودند.

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

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