بدهی فنی (Technical Debt)
هر میانبر در کد مثل یک وام است. امروز سریعتر میرویم، ولی بعداً با هر تغییر سود آن را میپردازیم. بدهی را ثبت کن، جایی را اول درست کن که هم پیچیده است و هم زیاد تغییر میکند، و با زبان وقت و پول دربارهاش حرف بزن.
نویسنده: bezzad
مشکل: چرا همه چیز کند شده است؟
فروشگاه اینترنتی ما دو سال پیش راه افتاد. آن روزها هر ویژگی جدید یک هفته طول میکشید. حالا مدیر محصول میپرسد: «چرا یک تغییر ساده در تخفیف، سه هفته طول کشید؟»
تیم جواب را میداند، ولی گفتنش سخت است:
- قانون تخفیف در سه جا کپی شده است. در سبد خرید، در صفحه پرداخت و در گزارش فروش. هر تغییر باید سه بار انجام شود.
- تست خودکار برای پرداخت نداریم. هر تغییر را باید دستی امتحان کرد.
- کلاس CheckoutService سه هزار خط است. هیچ کس جرأت نمیکند به آن دست بزند.
هر کدام از اینها روزی یک تصمیم سریع بود. به اینها بدهی فنی میگوییم.
ایده: وام و سود
این تشبیه را وارد کانینگهام (Ward Cunningham) مطرح کرد. وقتی یک میانبر میزنیم، مثل این است که وام گرفتهایم:
- اصل وام. کاری که باید بعداً انجام دهیم تا کد را درست کنیم. مثلاً یکی کردن سه نسخه قانون تخفیف.
- سود وام. وقت اضافهای که هر بار برای کار با کد بد میپردازیم. مثلاً سه بار تغییر به جای یک بار، و باگهایی که چون یکی از سه جا فراموش شد، پیش آمد.
- نکته مهم. تا وقتی وام را پس ندادهایم، سود را میدهیم. و سود فقط وقتی پرداخت میشود که به آن کد دست بزنیم.
وام بد نیست. شرکتها برای رشد وام میگیرند. مشکل، وامی است که کسی یادش نیست، و سودی که هر ماه بیشتر میشود.
مثال زنده: سود بدهی در طول زمان
در این مدل ساده، هر اسپرینت ده واحد وقت داریم. هر اسپرینت یک میانبر تازه اضافه میشود. هر واحد بدهی، یک واحد از وقت هر اسپرینت را میخورد. دو روش را امتحان کن:
این عددها ساختگی هستند. فقط شکل کلی را نشان میدهند.
به اسپرینتهای اول دقت کن. روش «فقط ویژگی» اول جلوتر است. برای همین این روش وسوسهانگیز است. ولی چند اسپرینت بعد، سود بدهی بیشتر وقت تیم را میخورد.
چهار نوع بدهی
همه بدهیها یکسان نیستند. مارتین فاولر (Martin Fowler) بدهی را با دو سؤال تقسیم میکند: آگاهانه بود؟ و با احتیاط بود؟
- آگاهانه و محتاطانه. «حراج جمعه است. با یک راه ساده میفرستیم و هفته بعد درستش میکنیم.» این یک تصمیم کسبوکاری درست است، اگر واقعاً ثبت شود و هفته بعد انجام شود.
- آگاهانه و بیاحتیاط. «وقت طراحی نداریم.» تیم میداند که بد است، ولی هیچ برنامهای برای برگشت ندارد.
- ناآگاهانه و بیاحتیاط. تیم نمیداند که کد بد است. راه حلش آموزش و بازبینی کد است.
- ناآگاهانه و محتاطانه. «حالا که ساختیم، فهمیدیم چطور باید میساختیم.» این همیشه پیش میآید. نشانه یادگیری است، نه اشتباه.
کدام بدهی را اول بدهیم؟
همه کد بد ارزش درست کردن ندارد. یادت هست؟ سود فقط وقتی پرداخت میشود که به کد دست بزنیم. پس کد زشتی که هیچ کس تغییرش نمیدهد، تقریباً سودی ندارد.
یک روش ساده برای پیدا کردن این نقطههای داغ (Hotspot):
- تعداد تغییر را بشمار. تاریخچه گیت نشان میدهد کدام فایلها در چند ماه اخیر بیشتر تغییر کردهاند.
- پیچیدگی را ببین. فایلهای خیلی بزرگ، متدهای خیلی طولانی، شرطهای تودرتو.
- این دو را کنار هم بگذار. فایلی که در هر دو فهرست بالا است، اولویت اول است.
- از تیم بپرس. «کدام بخش را دوست نداری باز کنی؟» این جواب معمولاً با عددها جور است.
# Files changed most often in the last 6 months
git log --since="6 months ago" --name-only --pretty=format: -- "*.cs" \
| grep -v '^$' | sort | uniq -c | sort -rn | head -10
نمونه: یکی کردن قانون تخفیف
قبل از تغییر، همین منطق در سه کلاس جدا بود:
// CartService.cs, CheckoutService.cs and SalesReport.cs (copied 3 times)
var discount = order.Total > 5_000_000 && customer.IsVip ? 0.1m : 0m;
var payable = order.Total - order.Total * discount;
بعد از تغییر، قانون یک جا و به زبان کسبوکار است:
public static class DiscountPolicy
{
private const decimal VipThreshold = 5_000_000;
private const decimal VipRate = 0.10m;
public static decimal PayableAmount(decimal total, bool isVip) =>
isVip && total > VipThreshold ? total * (1 - VipRate) : total;
}
حالا یک تست کافی است و تغییر بعدی فقط یک جا انجام میشود. این بازآرایی (Refactoring) کوچک بود. لازم نبود کل سیستم را بازنویسی کنیم.
چطور بدهی را کم کنیم؟
- ثبتش کن. هر بدهی آگاهانه یک کار در فهرست کارهای تیم (Backlog) میشود. بنویس کجا است، سودش چیست و درست کردنش چقدر وقت میبرد. بدهی ثبتنشده فراموش میشود.
- قانون پیشاهنگی. هر بار که به یک فایل دست میزنی، آن را کمی تمیزتر از قبل ترک کن. یک اسم بهتر، یک متد کوتاهتر، یک تست.
- سهم ثابت در هر اسپرینت. بخشی از وقت هر اسپرینت را برای بدهی کنار بگذار. پرداخت کم و همیشگی بهتر از یک پاکسازی بزرگ است که هیچ وقت تأیید نمیشود.
- بدهی را به کار کسبوکاری بچسبان. «قبل از تخفیف جدید، قانون تخفیف را یکی میکنیم» راحتتر تأیید میشود تا «یک اسپرینت برای Refactoring».
- بخش بزرگ را کمکم عوض کن. برای یک بخش خیلی بد، بازنویسی کامل خطرناک است. کد جدید را کنار کد قدیمی بساز و کمکم مسیرها را به آن ببر. به این روش Strangler Fig میگوییم.
با مدیر چطور حرف بزنیم؟
مدیر محصول کلمه «کد کثیف» را نمیفهمد. ولی وقت، پول و ریسک را خوب میفهمد.
بد
- «کد خیلی بد است. باید بازنویسی کنیم.»
- «Refactoring لازم داریم.»
- «این معماری قدیمی است.»
خوب
- «هر تغییر در تخفیف الان سه برابر وقت میبرد. با سه روز کار، این را به یک برابر برمیگردانیم.»
- «ماه گذشته دو باگ پرداخت از همین بخش آمد. با این کار و تستهایش، این ریسک کم میشود.»
- «قبل از کمپین عید، این بخش باید درست شود. وگرنه هر تغییر کمپین کند است.»
اشتباههای رایج
| اشتباه | نتیجه | راه درست |
|---|---|---|
| بدهی آگاهانه بدون ثبت | «هفته بعد درستش میکنیم» هیچ وقت نمیآید. | یک کار در Backlog با سود و هزینه. |
| درست کردن کد زشتی که کسی تغییرش نمیدهد | وقت میرود و هیچ سودی برنمیگردد. | اول نقطههای داغ: پیچیده و پرتغییر. |
| بازنویسی کامل از صفر | ماهها کار، باگهای جدید، و سیستم قدیمی هم هنوز باید نگه داشته شود. | تغییر کمکم، با تست، یا روش Strangler Fig. |
| پاکسازی بزرگ یک بار در سال | هیچ وقت تأیید نمیشود یا وسط کار رها میشود. | سهم کوچک و ثابت در هر اسپرینت. |
| حرف زدن با زبان فنی با مدیر | مدیر دلیل را نمیفهمد و اولویت نمیدهد. | وقت، پول و ریسک. |
| هر چیزی را بدهی نامیدن | کلمه معنایش را از دست میدهد. سلیقه شخصی بدهی نیست. | بدهی یعنی کدی که سود واقعی دارد: کندی یا باگ. |
خلاصه در شش خط
- هر میانبر یک وام است. سودش را با هر تغییر در همان کد میپردازیم.
- بدهی آگاهانه و محتاطانه یک ابزار است، اگر ثبت شود و برگردد.
- سود فقط جایی است که کد تغییر میکند. نقطههای داغ را اول درست کن.
- پرداخت کم و همیشگی بهتر از بازنویسی بزرگ است.
- بدهی را ثبت کن و به کارهای کسبوکاری بچسبان.
- با مدیر به زبان وقت، پول و ریسک حرف بزن.