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

تعریف دقیق و مرزبندی با «بازنویسی»

Refactoring یعنی اصلاح طراحی داخلی همان کد؛ خروجی، قراردادهای عمومی (Public API) و رفتار بیرونی نباید تغییر کنند. افزودن فیچر جدید یا بازنویسی کامل (Rewrite) Refactoring نیست؛ حتی اگر کد بعدی تمیزتر به نظر برسد. هر تغییر باید به گام‌های بسیار کوچک، با امکان بازگشت سریع تقسیم شود و در تاریخچه گیت با پیام‌هایی مثل «refactor: rename…» ثبت شود.

چه زمانی Refactor کنیم و چه زمانی نه

زمان‌های مناسب:

  • هنگام افزودن یا اصلاح فیچر؛ «Boy Scout Rule»: هر جا را لمس کردید کمی تمیزتر رها کنید.
  • وجود Code Smell‌ها: تابع‌های طولانی، شرط‌های تودرتو، نام‌های مبهم، اعداد جادویی، تکرار کد، وابستگی حلقه‌ای، پارامترهای فراوان، کامنت‌هایی که فقط منظور کد نامفهوم را توضیح می‌دهند.
  • زمانی که تغییر کوچک آینده را بسیار پرریسک می‌کند.

زمان‌های نامناسب:

  • نزدیک ددلاین حیاتی بدون تست کافی.
  • وقتی رفتار فعلی ناشناخته است و ابتدا باید با تست «شخصیت‌نگار» (Characterization) آن را ثبت کنید.
  • در یک «Big-Bang Refactor» که یک‌جا همه‌چیز را عوض می‌کند و برگشت‌پذیر نیست.

پیش‌نیازها و چک‌لیست ایمنی قبل از Refactor

  • کنترل نسخه: شاخه جداگانه بسازید و کمیت‌های کوچک با پیام شفاف ثبت کنید.
  • تست پوششی: واحد، یکپارچه یا حداقل تست‌های شخصیت‌نگار که ورودی/خروجی سناریوهای رایج را قفل می‌کنند.
  • CI سبز: اجرای خودکار تست‌ها در هر تغییر.
  • ابزارهای IDE: Rename Symbol، Extract Method، Move File و… تا ریسک خطای انسانی کم شود.
  • اصول کار امن: تغییرات کوچک، یک تکنیک در هر مرحله، اجرای تست‌ها بعد از هر قدم، امکان Revert فوری.

یک مثال عملی: بازآرایی تابع محاسبه فاکتور

هدف: یک تابع قدیمی را خواناتر و ماژولار کنیم، بدون تغییر نتیجه خروجی.

نسخه اولیه (بوی کد: تابع طولانی، اعداد جادویی، نام‌های مبهم)

function calcTotal(items, user, coupon) {
  var t = 0;
  for (var i = 0; i < items.length; i++) {
    var it = items[i];
    var p = it.p;
    if (it.d) p = p * (1 - it.d / 100);
    t += p * it.q;
  }
  if (coupon && coupon.type === 'percent') {
    t = t - (t * coupon.value / 100);
  }
  if (user && user.vip) {
    t = t * 0.95; // VIP
  }
  if (t > 1000000) {
    t = t * 0.98; // سفارش بزرگ
  }
  var tax = t * 0.09;
  return Math.round(t + tax);
}

تست‌های شخصیت‌نگار برای قفل کردن رفتار

قبل از تغییر، چند ورودی واقعی انتخاب کنید و خروجی را ثبت کنید تا هر Refactor باید همان خروجی را بدهد.

// اجرای این تست‌ها باید قبل و بعد از Refactor سبز بماند
function assertEq(actual, expected, msg) {
  if (actual !== expected) throw new Error(msg + " | got: " + actual);
}

var items1 = [
  { p: 300000, q: 1, d: 0 },
  { p: 200000, q: 2, d: 10 }
];
assertEq(
  calcTotal(items1, { vip: true }, { type: 'percent', value: 10 }),
  615087,
  "invoice with VIP and 10% coupon"
);

var items2 = [{ p: 600000, q: 2, d: 0 }];
assertEq(
  calcTotal(items2, null, null),
  1281840, // سفارش بزرگ، تخفیف 2%، سپس مالیات 9%
  "big order without coupon"
);

گام‌های Refactor که انجام می‌دهیم

  1. نام‌گذاری معنادار: p→price، q→qty، d→discount.
  2. استخراج توابع: محاسبه مبلغ هر سطر و اعمال تخفیف درصدی.
  3. حذف اعداد جادویی: ثابت‌های TAX_RATE، VIP_DISCOUNT، BIG_ORDER_THRESHOLD، BIG_ORDER_DISCOUNT.
  4. تقسیم شرط‌ها و استفاده از Guard Clause تا مسیر اجرای کد واضح شود.
  5. جداسازی منطق کوپن در تابع مستقل.

نسخه بازآرایی‌شده (همان خروجی، ساختمان داخلی بهتر)

var TAX_RATE = 0.09;
var VIP_DISCOUNT = 0.05;
var BIG_ORDER_THRESHOLD = 1000000;
var BIG_ORDER_DISCOUNT = 0.02;

function calculateInvoiceTotal(items, user, coupon) {
  var subtotal = sumLines(items);
  var afterCoupon = applyCoupon(subtotal, coupon);
  var afterVip = (user && user.vip) ? applyPercentDiscount(afterCoupon, VIP_DISCOUNT) : afterCoupon;
  var afterBigOrder = (afterVip >= BIG_ORDER_THRESHOLD) ? applyPercentDiscount(afterVip, BIG_ORDER_DISCOUNT) : afterVip;
  var tax = afterBigOrder * TAX_RATE;
  return Math.round(afterBigOrder + tax);
}

function sumLines(items) {
  var total = 0;
  for (var i = 0; i < items.length; i++) {
    total += lineTotal(items[i]);
  }
  return total;
}

function lineTotal(item) {
  var price = item.p !== undefined ? item.p : item.price;
  var qty = item.q !== undefined ? item.q : item.qty;
  var discount = item.d !== undefined ? item.d : item.discount;
  var priceAfterDiscount = discount ? applyPercentDiscount(price, discount / 100) : price;
  return priceAfterDiscount * qty;
}

function applyCoupon(amount, coupon) {
  if (!coupon) return amount;
  if (coupon.type === 'percent') return applyPercentDiscount(amount, coupon.value / 100);
  return amount; // انواع دیگر در آینده
}

function applyPercentDiscount(amount, rate) {
  return amount * (1 - rate);
}

اکنون همان تست‌های قبل را روی calculateInvoiceTotal اجرا کنید. اگر مقادیر یکی است، یعنی رفتار تغییر نکرده و Refactor موفق بوده است.

چگونه مطمئن شویم رفتار تغییر نکرده است؟

  • تست‌های شخصیت‌نگار: مجموعه‌ای از ورودی‌های متنوع (لبه‌ها، داده‌های بزرگ، مقادیر خالی) را ثبت کنید و قبل/بعد را مقایسه کنید.
  • Golden Master: خروجی نسخه قدیمی را برای ورودی‌های زیاد ضبط کنید و خروجی نسخه جدید را با آن Diff کنید. هر اختلاف غیرمنتظره زنگ خطر است.
  • Property-based: به‌جای مقدار دقیق، خاصیت را بررسی کنید (مثلا مبلغ نهایی منفی نشود، افزودن آیتم صفر تاثیری نداشته باشد).
  • قراردادهای بیرونی: اگر این تابع از API عمومی استفاده می‌شود، تست قرارداد (Contract Test) بنویسید.

تکنیک‌های کلیدی Refactoring که واقعا به کار می‌آیند

  • Extract Method / Inline Method: تکه‌های معنادار را به توابع کوچک تبدیل کنید؛ یا بالعکس توابع بی‌مصرف را درجا کنید.
  • Rename Symbol: نام‌ها را به واژه‌های دامنه مسئله نزدیک کنید تا نیاز به کامنت کمتر شود.
  • Introduce Constant: جایگزینی اعداد جادویی با ثابت‌های نام‌دار.
  • Introduce Parameter Object: وقتی لیست پارامترها طولانی است، آن‌ها را در یک آبجکت معنایی جمع کنید.
  • Replace Temp with Query: متغیرهای موقتی غیرضروری را حذف کنید تا جریان داده واضح‌تر شود.
  • Decompose Conditional / Guard Clauses: شرط‌های تو در تو را به قطعات کوچک و مسیرهای واضح بشکنید.
  • Dependency Injection: وابستگی‌های سخت‌کد را تزریق کنید تا تست‌پذیری و جداسازی بهتر شود.
  • Remove Dead Code: کدهای بلااستفاده و شاخه‌های غیرقابل دسترس را حذف کنید.

خطاهای رایج و راه پیشگیری

  • آمیختن Refactor با تغییر فیچر: اول رفتار فعلی را قفل کنید، Refactor کنید، سپس فیچر را اضافه کنید.
  • چند تکنیک در یک Commit: هر بار یک حرکت تا علت شکست‌ها مشخص بماند.
  • بی‌توجهی به API عمومی: امضا و قرارداد بیرونی را دست نزنید؛ اگر ناگزیر هستید، این دیگر Refactor نیست و باید جداگانه نسخه‌بندی و اعلام شود.
  • Big-Bang Refactor: به جای تغییرات عظیم، بهینه‌سازی تدریجی و پیوسته انجام دهید.
  • نادیده گرفتن عملکرد: Refactor هدفش بهبود طراحی است؛ اما پس از تغییر، پروفایلینگ سناریوهای حساس را بررسی کنید تا پسرفت شدید رخ ندهد.

چگونه پیشرفت را بسنجیم؟

  • پیچیدگی حلقوی (Cyclomatic Complexity) توابع بحرانی را کاهش دهید.
  • تکرار کد (Duplication) را با ابزارهای تحلیل استاتیک پایش کنید.
  • طول توابع و فایل‌های داغ (پر تغییر) را کوتاه‌تر کنید.
  • زمان مرور کد و تعداد نکات قابل فهم‌سازی در Code Review را ملاک کیفی قرار دهید.

هشدار: معیارها فقط نشانه‌اند. هدف نهایی، خوانایی و قابلیت تغییر امن است؛ نه صرفا رسیدن به عدد دلخواه.

از کجا شروع کنیم؟

  1. یک ناحیه کوچک و پرتکرار را انتخاب کنید.
  2. تست‌های شخصیت‌نگار بنویسید و آن‌ها را در CI سبز نگه دارید.
  3. یک شاخه جدا بسازید؛ هر گام کوچک: Rename، Extract، Introduce Constant.
  4. بعد از هر گام تست‌ها را اجرا و در صورت لزوم Revert کنید.
  5. وقتی خروجی ثابت ماند و کد خواناتر شد، Pull Request بدهید و از بازخوردها برای مرحله بعدی استفاده کنید.

اگر تازه با این رویکرد آشنا می‌شوید، مطالعه «Refactoring» اثر Martin Fowler و تمرین مداوم روی کد واقعی بهترین شروع است.