সরাসরি পর্বে যান
পর্ব ৪৩ · ৭০-এর মধ্যে · মডিউল ৭

Technical debt, legacy code আর maintenance — Lehman-এর সূত্র আর পুরোনো কোড সামলানো

Technical Debt, Legacy Code and Maintenance — Lehman's Laws and Working with Old Code

Taught in Bangla, this lesson explains technical debt as principal and interest, Fowler's quadrant and kinds of debt, finding hotspots from Git churn and complexity, keeping a debt register and a break-even calculation, repayment strategies from the Boy Scout rule to the strangler fig, the four maintenance types and Lehman's laws, and changing legacy code safely with seams, characterization tests, sprout and wrap methods and dependency upgrades.

এই পর্বে যা শিখবেন

  • Technical debt-এর আসল অর্থ, principal আর interest, আর Fowler-এর চার ঘরে debt চেনা
  • Git history-র churn আর complexity মিলিয়ে hotspot বের করা, debt register রাখা, আর শোধের break-even হিসাব
  • Boy scout rule, refactoring budget, strangler fig — আর “পুরোটা নতুন করে লিখি” প্রস্তাব কীভাবে যাচাই করবেন
  • Maintenance-এর চার রকম আর Lehman-এর software বিবর্তনের সূত্র
  • Legacy code-এ seam, characterization test, sprout method আর wrap method দিয়ে নিরাপদে বদল, আর dependency upgrade-এর কৌশল

১ · ধার করা সময় — technical debt রূপকটি আসলে কী বলে

পাড়ার মুদি দোকানের বাকির খাতার কথা ভাবুন। মাসের শেষে টাকা কম, তাই চাল-ডাল বাকিতে নিলেন — আজকের সমস্যা মিটল। বাকি ছোট আর তাড়াতাড়ি শোধ হলে সমস্যা নেই, বরং কাজে লাগে। বিপদ হয় যখন বাকির ওপর বাকি জমে, আর নতুন কিছু নিতে গেলেই আগের হিসাব মেলাতে দিন চলে যায়।

Software-এও ঠিক এমন হয়। ধরুন একটি কাল্পনিক কুরিয়ার কোম্পানির billing system (এই পর্বে নাম ধরে নিই “পার্সেলবাড়ি”)। ঈদের আগের সপ্তাহে নতুন COD চার্জ চালু করতে হবে। দলের হাতে দুই দিন। তাঁরা চার্জের হিসাব একটি if-এর ভেতরে hard-code করে দিলেন, test লিখলেন না, ভাবলেন পরে গুছাবেন। ঈদের ভিড় সামলানো গেল। এই শর্টকাটটিই technical debt.

মূল ধারণা

Principal (আসল) = debt শোধ করতে যে কাজ লাগবে — কোড গুছানো, test লেখা, নকশা ঠিক করা। Interest (সুদ) = debt রেখে দিলে প্রতিটি পরের বদলে যে বাড়তি সময়, ভুল আর ঝুঁকি আসে। সুদ দিতে হয় শুধু তখন, যখন সেই কোড ছুঁতে হয়। তাই যে কোড কেউ আর বদলায় না, তার debt প্রায় সুদহীন — এই কথাটি পরে hotspot আর শোধের হিসাবে খুব কাজে লাগবে।

রূপকটি এসেছে Ward Cunningham-এর কাছ থেকে — wiki-র উদ্ভাবক হিসেবে যিনি বেশি পরিচিত। ১৯৯২ সালের OOPSLA সম্মেলনে WyCash নামের একটি portfolio management system নিয়ে তাঁর experience report-এ তিনি লেখেন, প্রথমবারের কোড ship করা অনেকটা ধার নেওয়ার মতো; একটু ধার কাজ দ্রুত করে, যদি তা তাড়াতাড়ি rewrite করে শোধ করা হয়। বিপদ আসে যখন ধার শোধ হয় না — তখন “not-quite-right” কোডে কাটানো প্রতিটি মিনিট সেই ধারের সুদ। (মূল লেখার শব্দ এখানে ভাবানুবাদ, হুবহু উদ্ধৃতি নয়।)

খেয়াল করার বিষয়, Cunningham যা বোঝাতে চেয়েছিলেন, আর আজ অনেকে যা বোঝান, তার মধ্যে ফারাক আছে। পরে (প্রায় ২০০৯ সালে একটি ভিডিও আলোচনায়) তিনি ব্যাখ্যা করেন বলে সাধারণত উল্লেখ করা হয়: তিনি জেনেশুনে খারাপ কোড লেখার পক্ষে বলেননি। তাঁর কথা ছিল, শুরুর কোড লেখা হয় সমস্যার তখনকার বোঝাপড়া দিয়ে; ব্যবহার করতে করতে দল সমস্যাটি আরও ভালো বোঝে, আর তখন কোডকে সেই নতুন বোঝাপড়ার সাথে মিলিয়ে নিতে হয় — সেটাই শোধ। শর্ত একটাই: কোড এমন পরিষ্কার রাখতে হবে, যাতে পরে refactor করা যায়।

তাহলে debt কি খারাপ? না, ব্যবসার ঋণের মতো এটি একটি হাতিয়ার — ঈদের বাজার ধরা হয়তো দুই সপ্তাহের বাড়তি কাজের চেয়ে বেশি মূল্যবান। সমস্যা হয় যখন debt অদৃশ্য, তার সুদ অজানা, আর শোধের পরিকল্পনা নেই। পর্ব ১-এ (Software engineering কী) দেখেছেন, software-এর জীবনকালের খরচের বড় অংশ যায় maintenance-এ; জমে থাকা debt সেই খরচের একটি বড় চালক।

“Technical debt” শব্দটি অনেক সময় যেকোনো অপছন্দের কোডের নাম হয়ে যায়। কিন্তু বাগ (ভুল আচরণ) debt নয় — সেটি defect. আর অসম্পূর্ণ feature-ও debt নয় — সেটি backlog-এর কাজ। Debt হলো এমন কিছু, যা আজ ঠিকঠাক চলে, কিন্তু ভবিষ্যতের বদলকে ধীর বা ঝুঁকিপূর্ণ করে।

২ · Fowler-এর চতুর্ভুজ আর debt-এর রকমফের

Martin Fowler ২০০৯ সালে তাঁর website-এর একটি লেখায় debt-কে দুটি প্রশ্নে ভাগ করেন, যা technical debt quadrant নামে পরিচিত। প্রথম প্রশ্ন: debt কি জেনেশুনে (deliberate) নেওয়া, নাকি অজান্তে (inadvertent)? দ্বিতীয় প্রশ্ন: সিদ্ধান্তটি কি বিবেচক (prudent) — খরচ আর লাভ মেপে — নাকি বেপরোয়া (reckless)? নিচের ছবির উদাহরণ-বাক্যগুলো Fowler-এর লেখার ভাবানুবাদ।

Technical debt quadrant

Reckless / prudent × deliberate / inadvertent (Fowler, ২০০৯)

Fowler-এর technical debt quadrant বেপরোয়া · Reckless বিবেচক · Prudent জেনেশুনে · Deliberate অজান্তে · Inadvertent “নকশার সময় নেই” We don't have time for design “এখনই ছাড়তে হবে, ফল পরে সামলাব” We must ship now and deal with consequences “Layering আবার কী?” What's layering? “এখন বুঝলাম কীভাবে করা উচিত ছিল” Now we know how we should have done it
ছবি ৪৩.১ — চার ঘরের প্রতিটির করণীয় আলাদা। ওপরের ডান ঘরটিই Cunningham-এর মূল অর্থের সবচেয়ে কাছে; নিচের ডান ঘরটি অনিবার্য — ভালো দলও কাজ করতে করতেই শেখে।

এবার পার্সেলবাড়ির চারটি বাস্তব-ধাঁচের debt নিয়ে ঘর চিনে দেখি। প্রতিটি ধাপে দুটি প্রশ্নের উত্তর দেখুন, তারপর ঘর আর করণীয়।

বেপরোয়া · জেনেশুনে“নকশার সময় নেই”
বেপরোয়া · অজান্তে“Layering আবার কী?”
বিবেচক · জেনেশুনে“এখনই ছাড়তে হবে”
বিবেচক · অজান্তে“এখন বুঝলাম”
কাল্পনিক “পার্সেলবাড়ি” billing system
Debt
জেনেশুনে?
বিবেচনা ছিল?
ঘর
করণীয়
  1. বিবেচক · জেনেশুনে. দল জানে শর্টকাট নিচ্ছে, জানে কেন (ঈদের বাজার), আর লিখে রেখেছে কবে শোধ হবে। এটি স্বাস্থ্যকর ঋণ — শর্ত, সময়মতো শোধ।
  2. বেপরোয়া · জেনেশুনে. “Test লেখার সময় নেই” — ছয় মাস ধরে। সিদ্ধান্তটি সচেতন, কিন্তু সুদের হিসাব কেউ করেননি। এখানে সমস্যা কোডে নয়, সিদ্ধান্ত নেওয়ার ধারায়; শোধের পাশাপাশি নিয়মও বদলাতে হবে (যেমন Definition of Done-এ test রাখা, পর্ব ৫)।
  3. বেপরোয়া · অজান্তে. নতুন developer জানতেনই না যে UI আর database-এর মাঝে স্তর রাখা হয়। একে দোষারোপ করে লাভ নেই — সমাধান শেখানো আর review. Layered architecture নিয়ে বিস্তারিত পর্ব ৩৮-এ (Software architecture).
  4. বিবেচক · অজান্তে. শুরুর দল ভালো নকশাই করেছিলেন, কিন্তু দুই বছর ব্যবসা চালিয়ে বোঝা গেল zone (কোথায়) আর tariff (কত টাকা) দুটি আলাদা ধারণা। Fowler-এর মতে এটি অনিবার্য — ভালো দলও কাজ করেই শেখে। করণীয়: নতুন বোঝাপড়া অনুযায়ী refactor, আর সিদ্ধান্তের কারণ একটি ADR-এ লেখা।
  5. সারসংক্ষেপ. চারটি debt চার ঘরে পড়ল। ঘর চেনার উদ্দেশ্য কাউকে দোষ দেওয়া নয়; ঘর বলে দেয় কোন ধরনের প্রতিকার লাগবে — পরিকল্পনা, প্রক্রিয়া বদল, শেখানো, নাকি refactoring.

Debt কোথায় কোথায় জমে

Debt শুধু অগোছালো কোডে নয়, software-এর প্রায় প্রতিটি স্তরে জমে। নিচের ভাগ প্রচলিত একটি রূপ; বইভেদে নাম ও ভাগ কিছুটা আলাদা।

ধরনপার্সেলবাড়িতে উদাহরণসুদ যেভাবে দিতে হয়
Code debt৪০০ লাইনের function, ডুপ্লিকেট হিসাব, অস্পষ্ট নামপ্রতিটি বদলে বুঝতে বাড়তি সময়; smell-এর তালিকা পর্ব ৪২-এ (Code smell আর refactoring)
Design / architecture debtZone আর tariff এক class-এ; সব module সরাসরি database ছোঁয়একটি বদল অনেক জায়গায় ছড়ায় (ripple effect); ঠিক করা সবচেয়ে দামি
Test debtBilling-এর কোনো automated test নেইপ্রতিটি release-এর আগে হাতে যাচাই; ভয়ে কেউ কোড ছোঁয় না
Documentation debtকেন পাহাড়ি অঞ্চলে ৫ টাকা কম, কেউ জানেন নানতুন সদস্যের শিখতে মাস লাগে; সিদ্ধান্ত পুনরায় খুঁড়তে হয়
Infrastructure / build debtহাতে করে FTP-তে deploy; build শুধু একজনের ল্যাপটপে চলেRelease ধীর আর ঝুঁকিপূর্ণ; সেই একজন ছুটিতে গেলে সব বন্ধ
Dependency debtপাঁচ বছর পুরোনো PDF library, যার নিরাপত্তা-patch আর আসে নানিরাপত্তা ঝুঁকি; পরে upgrade করতে হলে একসাথে অনেক major version লাফ

৩ · Debt মাপা আর দেখা — hotspot, debt register আর remediation-এর অনুমান

সব debt একসাথে শোধ করা যায় না, করার দরকারও নেই। প্রশ্ন হলো, কোনটি আগে? সুদের সংজ্ঞাটি মনে করুন: সুদ দিতে হয় শুধু কোড ছুঁলে। তাই অগ্রাধিকারের দুটি প্রশ্ন: কোডটি কতটা জটিল, আর কতবার ছুঁতে হয়? যে file জটিল এবং ঘনঘন বদলায়, সেটি hotspot. এই ধারণা জনপ্রিয় করেছেন Adam Tornhill তাঁর Your Code as a Crime Scene (২০১৫; পরে নতুন সংস্করণ) বইয়ে — অপরাধ-বিশ্লেষণের মানচিত্রের মতো, কোড কোথায় “বারবার ঘটনা ঘটছে” তা খোঁজা।

বদলের হার (churn) জানার সবচেয়ে সৎ উৎস Git history নিজেই। নিচের transcript একটি ছোট scratch repository থেকে — পার্সেলবাড়ির ৮টি কাল্পনিক commit:

$ git log --oneline
a1f8340 fix: invoice total off by one taka
aba24d9 chore: zone names in Bangla
5e1be2f fix: express surcharge on Fridays
6e242b7 feat: monthly report
16f89f3 fix: COD fee for Dhaka
d5a618b feat: add new upazila zones
0bd7dd9 fix: hill zone weight rounding
9cdf2f2 feat: first version of courier billing

$ git log --format= --name-only | sort | uniq -c | sort -rn
      5 billing/tariff.py
      3 zones/zone_map.py
      3 billing/invoice.py
      1 reports/monthly.py
      1 notify/sms.py

$ git log --format= --name-only --since=2026-01-15T10:04 | sort | uniq -c | sort -rn
      2 billing/tariff.py
      1 zones/zone_map.py
      1 reports/monthly.py
      1 billing/invoice.py

$ git log --oneline -- billing/tariff.py
a1f8340 fix: invoice total off by one taka
5e1be2f fix: express surcharge on Fridays
16f89f3 fix: COD fee for Dhaka
0bd7dd9 fix: hill zone weight rounding
9cdf2f2 feat: first version of courier billing

দ্বিতীয় command-টি pipeline: --format= commit-এর বার্তা বাদ দেয়, --name-only প্রতিটি commit-এ বদলানো file-এর নাম ছাপে; sort | uniq -c একই নাম গুনে দেয়, sort -rn বড় থেকে ছোট সাজায়। ফল: ৮টি commit-এর ৫টিতে tariff.py বদলেছে। তৃতীয় command --since দিয়ে সময়ের জানালা ছোট করে — বাস্তবে লিখবেন --since="6 months ago", কারণ দশ বছর আগের churn আজকের অগ্রাধিকার বলে না। শেষ command দেখায় tariff.py-এর পাঁচটি বদলের চারটিই fix: — বারবার ভুল সারাতে হচ্ছে, যা debt-এর একটি জোরালো সংকেত (Conventional Commits-এর prefix নিয়ে পর্ব ১৮)। sort আর uniq Unix-এর tool; Windows-এ Git Bash-এ পাবেন। --pretty=format: লিখলে commit-এর মাঝে ফাঁকা লাইন আসে, তাই অনেক রেসিপিতে মাঝে grep -v '^$' থাকে।

শুধু churn যথেষ্ট নয় — একটি ছোট config file দিনে দশবার বদলালেও তাতে debt কম। তাই churn-কে জটিলতার সাথে মেলাতে হয়। জটিলতার মাপ হতে পারে লাইনের সংখ্যা, cyclomatic complexity-র যোগফল (বিস্তারিত পর্ব ৫০-এ), বা indentation-এর গভীরতা। নিচের খেলনা-বিশ্লেষণে ২০০টি কাল্পনিক commit একটি seeded PRNG দিয়ে বানানো, যাতে প্রতিবার একই ফল আসে:

// Hotspot = how often a file changes (churn) x how complex it is.
function mulberry32(a) {                       // small seeded PRNG
  return function () {
    a = (a + 0x6D2B79F5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}
const rand = mulberry32(43);
const files = {   // file: [complexity (sum of cyclomatic complexity), change tendency]
  "billing/tariff.py": [96, 9], "billing/invoice.py": [41, 6], "zones/zone_map.py": [12, 7],
  "notify/sms.py": [18, 2], "reports/monthly.py": [88, 1], "core/utils.py": [25, 3],
  "auth/login.py": [30, 1] };
const names = Object.keys(files);
const totalW = names.reduce((s, f) => s + files[f][1], 0);
function pickFile() {                          // weighted random choice
  let r = rand() * totalW;
  for (const f of names) { r -= files[f][1]; if (r < 0) return f; }
  return names[names.length - 1];
}
const churn = Object.fromEntries(names.map(f => [f, 0]));
for (let c = 0; c < 200; c++) {                // 200 fake commits
  const touched = new Set();
  const k = 1 + Math.floor(rand() * 3);        // each touches 1 to 3 files
  for (let i = 0; i < k; i++) touched.add(pickFile());
  for (const f of touched) churn[f]++;
}
const maxC = Math.max(...names.map(f => churn[f])), maxX = Math.max(...names.map(f => files[f][0]));
const rows = names.map(f => ({ f, ch: churn[f], cx: files[f][0],
  score: (churn[f] / maxC) * (files[f][0] / maxX) }))   // both scaled to 0..1
  .sort((a, b) => b.score - a.score);
console.log("rank file                churn  cx score");
rows.forEach((r, i) => console.log(String(i + 1).padStart(4) + " " + r.f.padEnd(19) +
  String(r.ch).padStart(6) + String(r.cx).padStart(4) + "  " + r.score.toFixed(2)));

আউটপুট:

rank file                churn  cx score
   1 billing/tariff.py      95  96  1.00
   2 billing/invoice.py     63  41  0.28
   3 reports/monthly.py     16  88  0.15
   4 core/utils.py          46  25  0.13
   5 zones/zone_map.py      82  12  0.11
   6 auth/login.py          23  30  0.08
   7 notify/sms.py          26  18  0.05

দুটি মাপকে ০ থেকে ১-এ এনে গুণ করা হয়েছে। tariff.py দুই মাপেই শীর্ষে (৯৫ বার বদল, জটিলতা ৯৬) — স্পষ্ট hotspot. monthly.py প্রায় সমান জটিল (৮৮) কিন্তু মাত্র ১৬ বার বদলেছে: debt আছে, সুদ কম। zone_map.py ৮২ বার বদলেছে কিন্তু সহজ (১২) — নতুন উপজেলা যোগ হওয়া স্বাভাবিক বদল। গুণের বদলে অন্য ওজন নিলে ক্রম কিছুটা বদলাতে পারে; score আলোচনা শুরুর হাতিয়ার, চূড়ান্ত রায় নয়।

Hotspot মানচিত্র — churn বনাম complexity

ওপরের কোডের ৭টি file (সংখ্যা আউটপুট থেকে)

Churn বনাম complexity: tariff.py একাই hotspot অঞ্চলে hotspot অঞ্চল 050100 050100 churn — কতবার বদলেছে complexity — জটিলতা জটিল, কিন্তু কম বদলায় ঘনঘন বদলায়, কিন্তু সহজ tariff.py invoice.py monthly.py utils.py zone_map.py login.py sms.py
ছবি ৪৩.২ — ওপরের ডান ঘরে একটিই file. বাস্তব project-এও প্রায়ই দেখা যায়, অল্প কয়েকটি file-এ বদলের বড় অংশ জমে; তাই পুরো codebase নয়, সেই কয়েকটি জায়গাই আগে দেখার।

Debt register — অদৃশ্য ঋণকে দৃশ্যমান করা

জেনেশুনে নেওয়া বা review-তে চোখে পড়া debt-এর হিসাব রাখতে হয় বাকির খাতার মতো একটি debt register — সাধারণত issue tracker-এ একটি label, বা repository-তে একটি file. পর্ব ৬২-এর risk register-এর সাথে এর মিল আছে: দুটোই অনিশ্চিত ভবিষ্যৎ খরচের তালিকা।

IDকোথায় · কীসুদ (অনুমান)Principal (অনুমান)সিদ্ধান্ত
DEBT-7tariff.py — পাহাড়ি অঞ্চলে ৫ টাকার রহস্যময় ছাড়, কোনো test নেইপ্রতি বদলে ~৪ ঘণ্টা হাতে যাচাই~৬০ ঘণ্টাএই quarter-এ, characterization test দিয়ে শুরু
DEBT-12COD চার্জ hard-code (ঈদের তাড়া)চার্জ বদলালে deploy লাগে~৬ ঘণ্টাপরের sprint, owner ঠিক করা
DEBT-15monthly.py — বিশাল functionবছরে ২–৩ বার বদলায়~৩০ ঘণ্টাআপাতত রাখা; বদলাতে হলে তখন গুছানো

Tool-এর হিসাব — SQALE-ধাঁচের remediation অনুমান

Static analysis tool (পর্ব ৪৪-এ code review আর linter) প্রায়ই debt-কে সময়ে প্রকাশ করে। চিন্তাটি আসে SQALE পদ্ধতি থেকে (Jean-Louis Letouzey, ২০১০-এর দিকে): প্রতিটি নিয়ম-ভঙ্গের একটি remediation cost ধরে (যেমন “এই function ভাঙতে ২০ মিনিট”) সব যোগ করা। কিছু tool debt ratio-ও দেখায় = remediation cost ÷ কোডটি নতুন করে লেখার আনুমানিক খরচ।

একটি হিসাব দেখি (সব সংখ্যা ধরা যাক)। tariff.py-তে ১,২০০ লাইন। Tool বলছে remediation ৩ কর্মদিবস = ২৪ ঘণ্টা = ১,৪৪০ মিনিট। লেখার খরচ ধরা হলো প্রতি লাইনে ৩০ মিনিট (কিছু জনপ্রিয় tool-এ লেখার সময়ে এটিই ডিফল্ট মান বলে উল্লেখ আছে; tool-ভেদে আলাদা)। তাহলে লেখার খরচ = ১,২০০ × ৩০ = ৩৬,০০০ মিনিট, আর debt ratio = ১,৪৪০ ÷ ৩৬,০০০ = ০.০৪, অর্থাৎ ৪%. অনেক tool এমন ratio-কে ভালো rating দেয় — অথচ ওপরের hotspot বিশ্লেষণে এটিই সবচেয়ে বিপজ্জনক file!

Tool গোনে নিয়ম-ভঙ্গ (লম্বা function, ডুপ্লিকেট লাইন), কিন্তু zone আর tariff-এর জট বা ৫ টাকার রহস্যের মতো design আর knowledge debt দেখে না, আর সুদ (বদলের হার) তার হিসাবেই নেই। তাই tool-এর সংখ্যা একটি সংকেত মাত্র; churn আর debt register মিলিয়ে সিদ্ধান্ত নিন।

৪ · সুদের হিসাব — কখন শোধ করা লাভজনক

Product owner জিজ্ঞেস করবেন, “৬০ ঘণ্টা refactoring-এ দিলে নতুন feature কবে আসবে?” প্রশ্নটি ন্যায্য। উত্তর দিতে হয় তাঁর ভাষায় — সময় আর টাকায়। সবচেয়ে সহজ মডেল: শোধ না করলে প্রতি বদলে বাড়তি h ঘণ্টা (সুদ); শোধ করলে একবার P ঘণ্টা (principal), তারপর প্রতি বদলে বাড়তি থাকে h′ ঘণ্টা (কিছু ঘর্ষণ থেকেই যায়)। মাসে r বার বদল হলে প্রতি মাসের সাশ্রয় r × (h − h′), আর

Break-even (মাস) ≈ P ÷ ( r × (h − h′) )

এই সূত্রে সুদ বাড়ে না ধরা হয়েছে। কিন্তু Lehman-এর দ্বিতীয় সূত্র (অংশ ৬) মনে করিয়ে দেয়, যত্ন না নিলে জটিলতা বাড়ে — তাই নিচের মডেলে সুদ প্রতি মাসে ৩% বাড়ে ধরা হয়েছে। প্রতিটি সংখ্যা ধরে নেওয়া; নিজের দলের সংখ্যা বসিয়ে চালিয়ে দেখুন।

// Technical-debt interest model. EVERY number here is an assumption.
const A = {
  changesPerMonth: 6,      // how often the team touches this module
  extraHoursPerChange: 4,  // interest: extra hours per change because of the debt
  hoursAfterPayback: 1,    // some friction remains even after the clean-up
  principal: 60,           // hours to refactor and add tests (paid in month 0)
  growth: 0.03 };          // interest grows 3% a month if the debt is left alone
function simulate(a, months) {
  let keep = 0, pay = a.principal, extra = a.extraHoursPerChange, even = null;
  const rows = [];
  for (let m = 1; m <= months; m++) {
    keep += a.changesPerMonth * extra;               // option 1: live with it
    pay += a.changesPerMonth * a.hoursAfterPayback;  // option 2: pay it down now
    extra *= 1 + a.growth;
    if (even === null && pay <= keep) even = m;
    rows.push([m, keep, pay]);
  }
  return { rows, even };
}
const r = simulate(A, 12);
for (const [m, k, p] of r.rows) if (m % 3 === 0 || m === r.even)
    console.log("month " + String(m).padStart(2) + ": keep debt " +
      k.toFixed(0).padStart(3) + " h | pay now " + p.toFixed(0).padStart(3) + " h");
console.log("break-even month:", r.even);
const saving = A.extraHoursPerChange - A.hoursAfterPayback;
console.log("no-growth formula:", (A.principal / (A.changesPerMonth * saving)).toFixed(2), "months");
for (const rate of [2, 0.5]) {
  const e = simulate({ ...A, changesPerMonth: rate }, 36).even;
  console.log("changes/month " + rate + " -> " + (e ? "break-even month " + e : "no break-even in 36 months"));
}

আউটপুট:

month  3: keep debt  74 h | pay now  78 h
month  4: keep debt 100 h | pay now  84 h
month  6: keep debt 155 h | pay now  96 h
month  9: keep debt 244 h | pay now 114 h
month 12: keep debt 341 h | pay now 132 h
break-even month: 4
no-growth formula: 3.33 months
changes/month 2 -> break-even month 9
changes/month 0.5 -> break-even month 25

তৃতীয় মাসেও “এখনই শোধ” পিছিয়ে (৭৮ বনাম ৭৪ ঘণ্টা), কারণ ৬০ ঘণ্টা আগেই খরচ হয়েছে। চতুর্থ মাসে পাল্লা ঘোরে, আর ১২ মাসে পার্থক্য ৩৪১ − ১৩২ = ২০৯ ঘণ্টা। সুদ স্থির ধরলে সূত্র দেয় ৬০ ÷ (৬ × ৩) = ৩.৩৩ মাস — এখানেও চতুর্থ মাস। সবচেয়ে বড় শিক্ষা শেষ দুই লাইনে: একই debt, শুধু বদলের হার কমলে break-even সরে যায় ৯ মাসে, আর মাসে আধাবার হলে ২৫ মাসে। তাই বদলের হার, অর্থাৎ hotspot, ঠিক করে কোন debt আগে শোধ করবেন। মডেলে production-এর ঝুঁকি বা নতুন সদস্যের শেখার সময় নেই; বাস্তব সিদ্ধান্তে এগুলো যোগ করুন (অনুমানের কৌশল পর্ব ৬০-এ)।

৫ · Debt শোধের কৌশল — boy scout rule থেকে strangler fig

শোধের পদ্ধতি বাছাই হয় debt-এর আকার দেখে — ছোট debt প্রতিদিনের কাজে, বড় architecture debt মাসব্যাপী পরিকল্পনায়।

🏕️ Boy scout rule

“যেখানে ক্যাম্প করেছিলেন, যাওয়ার সময় তার চেয়ে পরিষ্কার রেখে যান।” Robert C. Martin তাঁর Clean Code (২০০৮) বইয়ে কথাটি জনপ্রিয় করেন। যে file-এ কাজ করছেন, সেখানে একটি নাম ঠিক করা, একটি ছোট function আলাদা করা — hotspot-এ নিজে থেকেই বেশি যত্ন পড়ে।

ছোট debt · প্রতিদিন

⏱️ Refactoring budget

প্রতি sprint-এর ক্ষমতার একটি নির্দিষ্ট ভাগ debt-এর জন্য রাখা — অনেক দল ১০–২০%-এর মতো একটি অংশ রাখে বলে শোনা যায়। এটি কোনো মান বা নিয়ম নয়, একটি চর্চা; সঠিক ভাগ দলের অবস্থা দেখে ঠিক হয়।

মাঝারি debt · নিয়মিত

🎯 নির্দিষ্ট slice

একটি বড় debt-কে কয়েকটি ছোট, আলাদাভাবে release করা যায় এমন কাজে ভেঙে backlog-এ তোলা, প্রতিটির নিজস্ব লাভসহ (“DEBT-7a: tariff-এর characterization test”).

বড় debt · পরিকল্পিত

🌳 Strangler fig

পুরোনো system-এর সামনে একটি facade বসিয়ে একটি একটি অংশ নতুন কোডে সরানো, যতক্ষণ না পুরোনোটি খালি হয়। Martin Fowler ২০০৪ সালে বট-জাতীয় strangler fig গাছের নামে নামটি দেন। বিস্তারিত system design-এর দিক থেকে System Design কোর্সের microservices পর্বে।

Architecture debt · মাসব্যাপী

Strangler fig গাছ অন্য গাছের গায়ে জন্মায়, শিকড় নামিয়ে ধীরে ধীরে নিজেই দাঁড়িয়ে যায়, আর ভেতরের পুরোনো গাছটি একসময় মিলিয়ে যায় — পুরো সময় জুড়ে বনে একটি গাছ দাঁড়িয়েই থাকে। Software-এও তাই: মাঝপথের প্রতিটি দিনে system চালু থাকে। নিচের stepper-এ পার্সেলবাড়ির পুরোনো billing থেকে নতুন billing service-এ সরে যাওয়া দেখুন। Strangler fig-এর পাশাপাশি দুটি সহযোগী কৌশল কাজে লাগে: কোডের ভেতরে interface বসিয়ে বাস্তবায়ন বদলানো (branch by abstraction, পর্ব ১৭), আর পুরোনো মডেল যাতে নতুন কোডে চুইয়ে না ঢোকে সেজন্য anti-corruption layer (পর্ব ৪০, Clean আর hexagonal architecture).

  1. Clientapp, websiteকিছুই টের পায় না
  2. Facaderouterকোন route কোথায়
  3. পুরোনো billingmonolithধীরে খালি হয়
  4. নতুন billingserviceধীরে বাড়ে
কোন route কে সামলায়
/zones
/tariff
/invoice
/reports
পুরোনো কোডে route
  1. শুরু. সব request সরাসরি পুরোনো monolith-এ যায়। চারটি route-ই পুরোনো কোডে। বড় rewrite-এর বদলে দল ঠিক করল, ধাপে ধাপে সরবে; প্রথম শর্ত — প্রতিটি ধাপের পরেও system চালু থাকবে।
  2. Facade বসানো. Client আর পুরোনো কোডের মাঝে একটি router বসল। এখনো সব request পুরোনো কোডেই যায় — আচরণ একটুও বদলায়নি। কিন্তু এখন থেকে কোন route কোথায় যাবে, সেটি এক জায়গা থেকে বদলানো যায়। এটিই পুরো কৌশলের seam.
  3. প্রথম route: /zones, ছায়া-চালনায়. সবচেয়ে সহজ আর কম ঝুঁকির অংশ দিয়ে শুরু। কিছু দিন facade দুটি কোডেই request পাঠায়, ব্যবহারকারী পায় পুরোনোর ফল, আর দুই ফল মিলিয়ে log রাখা হয়। অমিল শূন্য হলে নতুনটি আসল দায়িত্ব নেয়।
  4. /tariff, feature flag দিয়ে ১০%. সবচেয়ে বড় hotspot-টি সাবধানে — মাত্র ১০% request নতুন কোডে, বাকি ৯০% পুরোনোয়। সমস্যা দেখলে flag বন্ধ করলেই সবাই পুরোনোয় ফেরে, নতুন deploy লাগে না (feature flag). পুরোনো tariff কোড এখন শুধু পড়া যায়, নতুন বদল সেখানে নয়।
  5. /tariff আর /invoice পুরো নতুনে. পুরোনো কোডে বাকি শুধু /reports. মাঝপথেও দুটি system মিলে একটি সম্পূর্ণ সেবা দিচ্ছে।
  6. পুরোনো billing বন্ধ. শেষ route সরানোর পরে পুরোনো কোড মুছে ফেলা হলো — এই ধাপটি ভুলে গেলে দুটি system চালানোর খরচ চিরকাল থেকে যায়, যা নিজেই নতুন debt. Facade চাইলে API gateway হিসেবে থাকতে পারে।

পুরোটা নতুন করে লিখব? — rewrite বনাম refactor

পুরোনো কোড দেখে প্রায় সব developer-এর মনে হয়, “ফেলে দিয়ে শুরু থেকে লিখলে অনেক ভালো হবে।” Joel Spolsky ২০০০ সালে তাঁর বহুল পঠিত লেখা “Things You Should Never Do, Part I”-এ এই প্রলোভনকে software কোম্পানির সবচেয়ে বড় কৌশলগত ভুলগুলোর একটি বলেন; তাঁর উদাহরণ Netscape-এর browser নতুন করে লেখা, যার সময়ে বছর কয়েক নতুন বড় release আসেনি (ঘটনার ব্যাখ্যা নিয়ে ভিন্নমতও আছে)। মূল যুক্তি: পুরোনো কোডের অগোছালো লাইনগুলোর অনেকগুলো আসলে বছরের পর বছরের bug fix — নতুন করে লিখলে সেই জ্ঞান হারায়।

আরেকটি ফাঁদ পর্ব ২-এর (Software crisis আর software engineering-এর ইতিহাস) Brooks-এর second-system effect: rewrite প্রায়ই দ্বিতীয় system, তাই আগেরবার বাদ রাখা সব ইচ্ছা ঢুকে পড়ে। এদিকে পুরোনো system-এ চাহিদা আসতেই থাকে, নতুনটি চলমান লক্ষ্যের পেছনে দৌড়ায়।

তবু rewrite কখনো যুক্তিসংগত: platform মৃত (নিরাপত্তা-সমর্থন নেই), system ছোট আর আচরণ পুরো জানা, বা ব্যবসার মডেলই এত বদলেছে যে পুরোনো ধারণা আর খাটে না। তখনও নিরাপদ পথ সাধারণত অংশে অংশে প্রতিস্থাপন — পুরোটা একবারে (big-bang rewrite) নয়।

৬ · Maintenance-এর চার রকম আর Lehman-এর সূত্র

Technical debt যে প্রক্রিয়ায় জমে আর শোধ হয়, তার নাম maintenance. পর্ব ১-এ এর চারটি ভাগের নাম দেখেছেন; আন্তর্জাতিক মান ISO/IEC 14764 (প্রথম ১৯৯৯; পরে ২০০৬ আর ২০২২ সালে ISO/IEC/IEEE 14764 হিসেবে হালনাগাদ) এগুলোকে আনুষ্ঠানিক রূপ দেয়। ২০০৬-এর সংস্করণে চারটিকে দুটি দলে রাখা হয়: correction (corrective আর preventive) আর enhancement (adaptive আর perfective).

ধরনঅর্থপার্সেলবাড়িতে উদাহরণকখন
Correctiveধরা পড়া ভুল সারানোInvoice-এ এক টাকা কম দেখাচ্ছেঘটনার পরে (reactive)
Adaptiveবদলে যাওয়া পরিবেশে মানিয়ে নেওয়ানতুন VAT আইন; নতুন Android version; SMS gateway-এর API বদলবাইরের চাপে
Perfectiveনতুন চাহিদা, দ্রুততা বা ব্যবহারযোগ্যতা বাড়ানোExpress delivery-র নতুন চার্জ; invoice দ্রুত তৈরিব্যবহারকারীর চাহিদায়
Preventiveভবিষ্যতের ভুল ঠেকাতে আগেই কোড উন্নত করাtariff.py-তে test যোগ আর refactorসমস্যার আগে (proactive)

খেয়াল করুন, technical debt শোধ মূলত preventive maintenance. আর বহু জরিপে maintenance-এর বড় অংশ adaptive আর perfective — অর্থাৎ software-কে বাড়ানো, শুধু bug fix নয়। Adaptive-এর বিখ্যাত উদাহরণ Y2K (দুই অঙ্কের বছর), আর ১ জানুয়ারি ২০২০-এ Python 2-এর সমর্থন শেষ হওয়ায় অসংখ্য project-এর Python 3-এ সরে যাওয়া।

Lehman-এর software বিবর্তনের সূত্র

Meir M. (Manny) Lehman ১৯৬০-এর দশকের শেষ থেকে IBM-এর OS/360-এর release-গুলোর তথ্য বিশ্লেষণ করে লক্ষ করেন, বড় software একটি নিয়মিত ধাঁচে বদলায়। এই কাজে তাঁর প্রধান সহযোগী ছিলেন Laszlo Belady. এই সূত্রগুলো Lehman’s laws নামে পরিচিত। ১৯৭৪ সালে প্রথম তিনটি প্রকাশিত হয়, পরে আরও যোগ হয়; ১৯৯৬ নাগাদ সংখ্যাটি দাঁড়ায় আটে। সূত্রগুলো প্রযোজ্য E-type software-এ — যা বাস্তব জগতে ব্যবহৃত হয় আর বাস্তব জগতের অংশ হয়ে যায় (একটি billing system), গাণিতিকভাবে পুরো সংজ্ঞায়িত S-type প্রোগ্রামে (যেমন একটি sort function) নয়। নম্বর আর নামের ক্রম বিভিন্ন উৎসে সামান্য আলাদা; সাধারণত উদ্ধৃত রূপে প্রথম কয়েকটি:

  1. Continuing change (ক্রমাগত পরিবর্তন): একটি E-type system-কে ক্রমাগত মানিয়ে নিতে হয়, নইলে সেটি ধীরে ধীরে কম সন্তোষজনক হয়ে পড়ে। পার্সেলবাড়ির কোড না বদলালেও জগৎ বদলায় — নতুন উপজেলা, নতুন আইন, নতুন ফোন।
  2. Increasing complexity (ক্রমবর্ধমান জটিলতা): বিবর্তনের সাথে system-এর জটিলতা বাড়ে, যদি না তা ধরে রাখা বা কমানোর জন্য আলাদা কাজ করা হয়। এটিই technical debt-এর “প্রাকৃতিক নিয়ম” — refactoring budget সেই আলাদা কাজ।
  3. Self-regulation (স্ব-নিয়ন্ত্রণ): বড় system-এর বিবর্তন প্রক্রিয়া নিজেই নিজেকে নিয়ন্ত্রণ করে; release-এর আকারের মতো মাপগুলো একটি গড়ের আশপাশে থাকে।
  4. পরের সূত্রগুলো ১৯৮০ আর ১৯৯০-এর দশকে যোগ হয়, যেমন continuing growth (সন্তুষ্টি ধরে রাখতে কার্যক্ষমতা বাড়াতে হয়) আর declining quality (কঠোর যত্ন ছাড়া মান কমছে বলে মনে হবে)।

আরও গভীরে: Lehman-এর সবচেয়ে উদ্ধৃত লেখা ১৯৮০ সালের Proceedings of the IEEE-তে প্রকাশিত “Programs, Life Cycles, and Laws of Software Evolution”, যেখানে S-, P- আর E-type প্রোগ্রামের ভাগও আছে। পরের গবেষণায়, বিশেষ করে open-source project নিয়ে (যেমন ২০০০ সালের দিকে Linux kernel-এর বৃদ্ধি নিয়ে Godfrey আর Tu-র কাজ), কিছু সূত্র সব ক্ষেত্রে খাটে কি না তা নিয়ে প্রশ্ন উঠেছে। Legacy code নিয়ে সবচেয়ে ব্যবহারিক বই Michael Feathers-এর Working Effectively with Legacy Code (২০০৪); আর maintenance প্রক্রিয়ার আনুষ্ঠানিক বর্ণনা ISO/IEC/IEEE 14764-এ।

৭ · Legacy code — Feathers-এর সংজ্ঞা, seam আর characterization test

“Legacy” শুনলে মনে হয় বহু পুরোনো কোড, COBOL বা mainframe. কিন্তু Michael Feathers তাঁর ২০০৪ সালের বই Working Effectively with Legacy Code-এ একটি ধারালো সংজ্ঞা দেন: legacy code হলো test ছাড়া কোড। তাঁর যুক্তি: test না থাকলে আপনি জানতে পারবেন না বদলের পরে কোড আরও ভালো হলো না খারাপ; কোড যত সুন্দরই হোক, বদলানো ঝুঁকিপূর্ণ। এই সংজ্ঞায় গত মাসে লেখা test-হীন কোডও legacy code, আর দশ বছর পুরোনো কিন্তু ভালো test-সহ কোড নয়।

পুরান ঢাকার একটি পুরোনো বাড়ির wiring বদলাবেন এক ইলেকট্রিশিয়ান; নকশা নেই। অভিজ্ঞ মিস্ত্রি প্রথমেই দেয়াল ভাঙেন না — প্রতিটি সুইচ টিপে খাতায় লেখেন কোনটিতে কোন বাতি জ্বলে (“৩ নম্বর: বারান্দা আর সিঁড়ি, দুটোই”), অদ্ভুত হলেও। তার বদলের পরে আবার সব সুইচ টিপে খাতার সাথে মেলান। Legacy code-এ characterization test ঠিক সেই খাতা।

Feathers-এর দুটি মূল ধারণা। প্রথমটি seam: এমন জায়গা, যেখানে সেই জায়গার কোড না বদলেই আচরণ বদলানো যায় (যেমন test-এ আসল database-এর বদলে fake). Object-oriented কোডে সবচেয়ে সাধারণ seam হলো constructor বা parameter দিয়ে dependency পাঠানো — পর্ব ৩৭-এর (Dependency injection) কৌশল। যে function সরাসরি ঘড়ি (datetime.now()), database বা global পড়ে, তাকে test করতে আগে সেই নির্ভরতাকে parameter বানাতে হয়।

দ্বিতীয়টি characterization test: এমন test, যা কোডের উচিত আচরণ নয়, বর্তমান আচরণ লিখে রাখে। উদ্দেশ্য ভুল ধরা নয়, বদলের সময় আচরণ যাতে অজান্তে না বদলায় তা নিশ্চিত করা। নিচের stepper-এ পার্সেলবাড়ির পুরোনো চার্জ-হিসাবের function-এ একটি characterization test কীভাবে লেখা হয় দেখুন।

tariff.pyবদলের জায়গা চিহ্নিত
def charge_legacy(weight_g, zone, cod):
    c = 60
    if zone == "dhaka":
        ...   # ১৫ লাইনের জট
    if zone == "hill" and weight_g >= 5000:
        c = c - 5     # nobody remembers why
    return c
seam খোঁজাfunction কি একা ডাকা যায়?
# database? না · ঘড়ি? না · global? না
charge_legacy(5000, "hill", True)   # সরাসরি ডাকা যায়

# যদি ভেতরে datetime.now() থাকত:
def charge(weight_g, zone, cod, today=None): ...
test_tariff.pyজেনেশুনে ভুল প্রত্যাশা
def test_hill_heavy_cod(self):
    self.assertEqual(charge_legacy(5000, "hill", True), 0)
Test চালানোব্যর্থতাই উত্তর বলে দেয়
FAIL: test_hill_heavy_cod
AssertionError: 295 != 0
test_tariff.pyবর্তমান আচরণ পিন করা
def test_hill_heavy_cod(self):
    # characterizes current behaviour; the -5 hill
    # rule has unknown origin, see DEBT-7
    self.assertEqual(charge_legacy(5000, "hill", True), 295)
অনেক input · golden masterতারপর refactor
recorded 150 cases
charge_v1 -> 10 differences   # hill নিয়ম বাদ পড়েছে
charge_v2 -> 0 differences    # আচরণ অক্ষত
  1. বদলের জায়গা চিহ্নিত. লক্ষ্য: charge_legacy-কে পরিষ্কার করা (DEBT-7). Function-টিতে একটি নিয়ম আছে যার কারণ কেউ জানেন না — পাহাড়ি অঞ্চলে ৫ কেজি বা তার বেশি হলে ৫ টাকা কম। এখন এটি ঠিক না ভুল, সে প্রশ্ন নয়; আগে আচরণ ধরে রাখতে হবে।
  2. Seam খোঁজা. Function-টি database, ঘড়ি বা global কিছু পড়ে না — input দিলে output দেয়। তাই function-এর ডাকটিই test-এর জায়গা। যদি ভেতরে আজকের তারিখ পড়ত, আগে today নামে একটি parameter যোগ করে seam খুলতে হতো, ডিফল্ট মান দিয়ে, যাতে পুরোনো ডাকগুলো না ভাঙে।
  3. জেনেশুনে ভুল প্রত্যাশা লেখা. উত্তর কী হবে আমরা জানি না, তাই প্রত্যাশা হিসেবে ইচ্ছা করে 0 লিখলাম।
  4. চালানো — ব্যর্থতাই উত্তর. Test ব্যর্থ হলো: AssertionError: 295 != 0. প্রথম সংখ্যাটি কোডের আসল ফল — ৫ কেজি, পাহাড়, COD-সহ চার্জ এখন ২৯৫ টাকা।
  5. বর্তমান আচরণ পিন করা. প্রত্যাশা বদলে ২৯৫ করা হলো, সাথে মন্তব্য: এটি বর্তমান আচরণ, কারণ অজানা, DEBT-7 দেখুন। এখন এই test পাস করে — আর refactor-এর পরে এই সংখ্যা বদলালেই সাথে সাথে জানা যাবে।
  6. অনেক input, তারপর refactor. একটি-দুটি test যথেষ্ট নয়; ওজন, অঞ্চল আর COD-এর ১৫০টি সমাবেশের ফল একবারে রেকর্ড করা হলো (golden master). প্রথম refactor (charge_v1) ১০টি ক্ষেত্রে আলাদা — পাহাড়ি নিয়মটি বাদ পড়েছিল। দ্বিতীয়টি (charge_v2) সব মেলে। নিচের কোডে পুরোটা চালিয়ে দেখুন।

অনেক input-এর ফল একসাথে রেকর্ড করে রাখার কৌশলকে বলা হয় golden master (কাছাকাছি নাম: snapshot বা approval testing; নামের ব্যবহার সম্প্রদায়ভেদে আলাদা)। নিচের কোডে পুরো প্রক্রিয়াটি:

def charge_legacy(weight_g, zone, cod):        # 12 years old, no tests
    c = 60
    if zone == "dhaka":
        if weight_g > 1000:
            c = c + ((weight_g - 1) // 1000) * 15
    else:
        c = 110
        if weight_g > 500:
            c += ((weight_g - 501) // 500 + 1) * 20
    if cod:
        c = c + int(c * 0.01 + 0.5) if zone == "dhaka" else c + 10
    if zone == "hill" and weight_g >= 5000:
        c = c - 5                               # nobody remembers why
    return c
# 1) Characterize: record what the code DOES today (a golden master)
cases = [(w, z, cod) for w in range(0, 6001, 250)
         for z in ("dhaka", "outside", "hill") for cod in (False, True)]
golden = {case: charge_legacy(*case) for case in cases}
print("recorded", len(golden), "cases, e.g.", golden[(1250, "dhaka", True)],
      golden[(750, "outside", False)], golden[(5000, "hill", True)])

# 2) Refactor, then compare every case with the golden master
def charge_v1(weight_g, zone, cod):
    if zone == "dhaka":
        base = 60 + max(0, (weight_g - 1) // 1000) * 15
        return base + (int(base * 0.01 + 0.5) if cod else 0)
    base = 110 + max(0, -(-(weight_g - 500) // 500)) * 20   # ceiling
    return base + (10 if cod else 0)

def charge_v2(weight_g, zone, cod):
    hill_rebate = 5 if zone == "hill" and weight_g >= 5000 else 0
    return charge_v1(weight_g, zone, cod) - hill_rebate
for fn in (charge_v1, charge_v2):
    diffs = [(c, golden[c], fn(*c)) for c in cases if fn(*c) != golden[c]]
    print(fn.__name__, "->", len(diffs), "differences", diffs[:2])

আউটপুট:

recorded 150 cases, e.g. 76 130 295
charge_v1 -> 10 differences [((5000, 'hill', False), 285, 290), ((5000, 'hill', True), 295, 300)]
charge_v2 -> 0 differences []

২৫টি ওজন (০ থেকে ৬,০০০ গ্রাম, ২৫০ করে) × ৩টি অঞ্চল × COD আছে-নেই = ১৫০টি ক্ষেত্র। নমুনা হিসাব মিলিয়ে নিন: ঢাকায় ১,২৫০ গ্রাম, COD-সহ — ৬০ + ১ × ১৫ = ৭৫, তার ১% = ০.৭৫, তার সাথে ০.৫ যোগ করে int নিলে ১, মোট ৭৬। charge_v1 দেখতে পরিষ্কার, পড়তেও সহজ, কিন্তু ১০টি ক্ষেত্রে ৫ টাকা বেশি নেয়: পাহাড়ি অঞ্চলে ৫,০০০ থেকে ৬,০০০ গ্রামের ৫টি ওজন × COD-এর দুই অবস্থা। এই ভুল কোনো code review-তে চোখ এড়িয়ে যেতে পারত; golden master এক সেকেন্ডে ধরল। charge_v2 নিয়মটিকে একটি নাম দিয়ে (hill_rebate) ফিরিয়ে আনে, আর সব ১৫০টি মেলে। নিয়মটি আসলে ব্যবসার সিদ্ধান্ত না পুরোনো ভুল — সেটি এখন আলাদা প্রশ্ন, product owner-এর সাথে আলোচনা করে, জেনেশুনে বদলানোর বিষয়। Refactoring-এর নিরাপদ ছোট ধাপগুলো পর্ব ৪২-এ (Code smell আর refactoring), আর unit test লেখার নিয়ম পর্ব ৪৬-এ।

Golden master শুধু রেকর্ড করা input-গুলোকেই রক্ষা করে। ওপরে ৬ কেজির বেশি ওজন নেই — ২৬ কেজির parcel-এ refactor আলাদা ফল দিলেও test টের পাবে না (অনুশীলন ২)। তাই input বাছুন বাস্তব data দেখে, সীমানার মান (৫০০, ৫০১, ১০০০, ১০০১ গ্রাম) রেখে।

৮ · পুরোনো কোডে নতুন কাজ — sprout, wrap আর অচেনা কোড পড়া

ধরুন DEBT-7 শোধের সময় এখনো আসেনি, কিন্তু কালই একটি নতুন চাহিদা: “২০ থেকে ৩১ মার্চ ঈদ-অভিযানে কাপড়ের পণ্যে ১০% ছাড়, সর্বোচ্চ ২০০ টাকা।” Invoice তৈরির পুরোনো function জটিল, test নেই, আর পুরোটার characterization করতে কয়েক দিন লাগবে। Feathers-এর পরামর্শ: পুরোনো জটের ভেতরে নতুন লজিক মেশাবেন না। নতুন কাজটি আলাদা, ছোট, test-সহ একটি function-এ লিখুন, আর পুরোনো function থেকে শুধু এক লাইনে ডাকুন। একে বলে sprout method — পুরোনো গাছ থেকে নতুন অঙ্কুর।

import unittest

def build_invoice(order):                       # legacy: tangled, no tests
    total = 0
    for name, cat, qty, price in order["lines"]:
        if cat == "food" and qty > 10:
            price = price - 2                   # old bulk rule
        total += qty * price
    if order.get("member"):
        total = total - total // 20             # 5% member discount
    total -= campaign_discount(order["lines"], order["day"])  # the ONLY new line
    total += 0 if total >= 1000 else 60         # delivery charge
    return total

def campaign_discount(lines, day):              # sprouted: new, small, tested
    if not ("2026-03-20" <= day <= "2026-03-31"):    # ISO dates compare as text
        return 0
    clothing = sum(q * p for _, cat, q, p in lines if cat == "clothing")
    return min(clothing // 10, 200)             # 10%, at most 200 taka

class CampaignDiscountTest(unittest.TestCase):
    L = [("panjabi", "clothing", 1, 1500), ("dates", "food", 2, 300)]
    def test_ten_percent_of_clothing(self):
        self.assertEqual(campaign_discount(self.L, "2026-03-25"), 150)
    def test_capped_at_200(self):
        self.assertEqual(campaign_discount([("saree", "clothing", 1, 5000)], "2026-03-25"), 200)
    def test_outside_campaign(self):
        self.assertEqual(campaign_discount(self.L, "2026-04-02"), 0)

result = unittest.TestResult()
unittest.defaultTestLoader.loadTestsFromTestCase(CampaignDiscountTest).run(result)
print("tests run:", result.testsRun, "| failures:", len(result.failures), "| errors:", len(result.errors))
order = {"lines": CampaignDiscountTest.L, "member": True, "day": "2026-03-25"}
print("invoice in campaign:", build_invoice(order))
print("invoice after campaign:", build_invoice({**order, "day": "2026-04-02"}))

আউটপুট:

tests run: 3 | failures: 0 | errors: 0
invoice in campaign: 1845
invoice after campaign: 1995

নতুন লজিকের তিনটি test-ই পাস: ১,৫০০ টাকার পাঞ্জাবিতে ১০% = ১৫০; ৫,০০০ টাকার শাড়িতে সীমা ২০০; অভিযানের বাইরে ০। Invoice: পণ্যের মোট ১,৫০০ + ২ × ৩০০ = ২,১০০; সদস্য-ছাড় ১০৫ বাদে ১,৯৯৫; অভিযানে আরও ১৫০ কমে ১,৮৪৫; ১,০০০-এর বেশি, তাই delivery চার্জ নেই। অভিযান শেষে ফল আগের মতো ১,৯৯৫ — পুরোনো আচরণ অক্ষত। নতুন লাইনটি কোথায় বসবে (সদস্য-ছাড় বা delivery-র সীমার আগে না পরে), সেটি ব্যবসায়িক সিদ্ধান্ত — sprout এমন প্রশ্ন স্পষ্ট করে তোলে।

একই পরিবারের আরও তিনটি কৌশল:

  • Sprout class: নতুন কাজের জন্য পুরো একটি নতুন class, যখন পুরোনো class-টি test-এর মধ্যে আনাই কঠিন (যেমন constructor database-এ যুক্ত হয়)। নতুন class-কে পুরোনোটি শুধু তৈরি করে ডাকে।
  • Wrap method: নতুন আচরণ যখন পুরোনো কাজের আগে বা পরে ঘটবে (যেমন প্রতিটি invoice তৈরির পরে SMS). পুরোনো method-এর নাম বদলে (build_invoice_core), একই নামে নতুন method বানানো হয়, যা পুরোনোটিকে ডেকে তারপর নতুন কাজ করে। ডাকা কোড কিছুই টের পায় না।
  • Wrap class: একই চিন্তা class-এর স্তরে — বাইরে থেকে একটি wrapper, যা আসলে decorator pattern (পর্ব ৩৪)।

Feathers-এর বড় প্রক্রিয়াটি মোটামুটি এমন: বদলের জায়গা চিহ্নিত → test-এর জায়গা খোঁজা → dependency ভেঙে seam খোলা → test লেখা → বদল আর refactor. Sprout আর wrap সেই অবস্থার জন্য, যখন পুরো test-জাল বোনার সময় নেই — অন্তত নতুন কোডটুকু শুরু থেকেই test-সহ থাকুক।

অচেনা কোড পড়ার একটি পদ্ধতি

নতুন চাকরিতে প্রথম সপ্তাহে প্রায়ই একটি দশ বছরের পুরোনো repository হাতে আসে। পুরোটা পড়ার চেষ্টা বৃথা। বরং ভাবুন নতুন শহরে রিকশাচালকের মতো: আগে বড় রাস্তা আর মোড় চিনুন, গলি পরে।

  1. Build আর চালানো: README, build script আর CI configuration — না চললে সবাই সাথে সাথে টের পান, তাই এগুলোই সবচেয়ে হালনাগাদ “documentation”.
  2. Entry point: main, web-এর route তালিকা, cron job, message-এর consumer — বাইরের জগৎ কোথা দিয়ে কোডে ঢোকে, সেই দরজাগুলোর তালিকা করুন।
  3. Test: থাকলে test-ই সবচেয়ে ভালো উদাহরণ — কোন input-এ কী output আশা করা হয়। না থাকলে সেটিই প্রথম তথ্য (legacy!)।
  4. Git history: git log --oneline -- <file>, ওপরের churn command, git shortlog -sn (কে বেশি কাজ করেছেন — কাকে জিজ্ঞেস করবেন)। একটি লাইন কেন এমন, তা খুঁজতে git blame আর git log -S — বিস্তারিত পর্ব ৫৫-এ।
  5. Scratch refactoring: বোঝার জন্য একটি আলাদা branch-এ নির্ভয়ে নাম বদলান, function ভাঙুন — তারপর পুরোটা ফেলে দিন। Feathers এই কৌশলের কথা বলেন: উদ্দেশ্য কোড বদলানো নয়, বোঝা।

৯ · Dependency upgrade আর deprecation — বাইরের কোডের debt

আধুনিক project-এর কোডের বড় অংশ আপনার লেখা নয় — package manager দিয়ে আনা library. এক version পিছিয়ে থাকা সহজে সামলানো যায়; পাঁচ major version পিছিয়ে থাকলে upgrade নিজেই একটি project. এটিই dependency debt, আর এর সুদ শুধু সময়ে নয় — নিরাপত্তায়ও।

দুটি বহুল আলোচিত ঘটনা (বিস্তারিত ভাবনার প্রশ্ন ৪-এ): ২০১৭ সালে Equifax-এর data ফাঁসে সরকারি তদন্তে Apache Struts-এর এমন একটি ত্রুটির কথা বলা হয়, যার patch কয়েক মাস আগেই প্রকাশিত হয়েছিল; আর ২০২১ সালের ডিসেম্বরে Log4j-এর “Log4Shell” প্রকাশের পর বহু প্রতিষ্ঠানের প্রথম প্রশ্নই ছিল — “আমাদের কোথায়, কোন version-এ Log4j আছে?” যাঁদের dependency-র তালিকা (SBOM — software bill of materials) ছিল, তাঁরা দ্রুত উত্তর পেয়েছিলেন।

নিরাপদ upgrade-এর অভ্যাস

  • ছোট আর ঘনঘন: প্রতি মাসে অল্প upgrade, বছরে একবার বিশাল লাফ নয়। Dependabot বা Renovate-এর মতো tool (লেখার সময়ে; platform-ভেদে আলাদা) নিজে upgrade-এর pull request খোলে, CI পাস করলে review করে merge.
  • এক major করে: ২.x থেকে সরাসরি ৫.x নয়; ৩, ৪, ৫ — প্রতিটির changelog আর migration guide পড়ে। SemVer-এ MAJOR মানে breaking change (পর্ব ১৮), তবে সব library SemVer কঠোরভাবে মানে না।
  • Deprecation warning-কে debt হিসেবে ধরা: এখনই সারালে সস্তা। Python-এ -W error::DeprecationWarning দিয়ে test চালালে এমন warning error হয়ে ধরা পড়ে।
  • Lockfile commit করা: যাতে সবার যন্ত্রে আর CI-তে ঠিক একই version থাকে, আর upgrade হয় সচেতন একটি commit-এ — যা দরকার হলে revert করা যায়।
  • Support-এর মেয়াদ জানা: ভাষা, framework আর OS-এর end-of-life তারিখ debt register-এ রাখুন। শেষ মাসের জন্য রেখে দিলে সেটিই বেপরোয়া debt.

আপনি নিজে যখন library বা API-র মালিক, তখন দায়িত্ব উল্টো দিকের: পুরোনো রূপ হঠাৎ সরিয়ে দেবেন না। নতুন রূপ পাশাপাশি দিন, পুরোনোটিকে deprecated ঘোষণা করুন, তারপর পরের major version-এ সরান — পর্ব ৩১-এর (Interface আর contract) parallel change-এর ধারা।

পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এই ধরনের প্রশ্ন আসতে পারে:

  • Technical debt কী? Principal আর interest কী? — ভবিষ্যতের বদলকে ধীর করে এমন শর্টকাট বা অপূর্ণ নকশা; principal = শোধের কাজ, interest = debt থাকার কারণে প্রতি বদলে বাড়তি খরচ।
  • Maintenance-এর চার রকম? — corrective (ভুল সারানো), adaptive (পরিবেশে মানানো), perfective (উন্নতি/নতুন চাহিদা), preventive (ভবিষ্যতের সমস্যা ঠেকানো); ISO/IEC 14764.
  • Lehman-এর প্রথম দুটি সূত্র? — continuing change (না বদলালে কম কাজের হয়), increasing complexity (যত্ন না নিলে জটিলতা বাড়ে); E-type system-এ প্রযোজ্য।
  • Legacy code-এ নিরাপদে বদল কীভাবে? — আগে characterization test দিয়ে বর্তমান আচরণ ধরা, seam খুলে dependency ভাঙা, নতুন কাজ sprout/wrap করে, ছোট ধাপে refactor.
  • Rewrite না refactor? — সাধারণত ধাপে ধাপে refactor বা strangler fig; big-bang rewrite-এ জমা জ্ঞান হারায়, second-system effect আর চলমান লক্ষ্যের ঝুঁকি।

১০ · AI-এর যুগে — পুরোনো কোডে AI-কে সাথে নেওয়া

Legacy code-এ AI কাজের সঙ্গী হতে পারে: অচেনা function ব্যাখ্যা, characterization test-এর input-এর তালিকা, পুরোনো framework থেকে অনুবাদের খসড়া। কিন্তু পুরোনো কোডের আসল সম্পদ তার লুকানো আচরণ — আর AI সেটাই সবচেয়ে সহজে মুছে দেয়, কারণ তার চোখে “অদ্ভুত” লাইন মানেই ভুল।

AI প্রায়ই যা করেবিপদযাচাইয়ের উপায়
“Refactor” করে অদ্ভুত নিয়ম চুপচাপ বাদ দেয় (যেমন পাহাড়ি ৫ টাকা)আচরণ বদলায়, কেউ টের পান নাআগে golden master; refactor-এর পরে সব ক্ষেত্র মেলানো
Characterization test-এ কোডের উচিত ফল লেখে, আসল ফল নয়; বা implementation-ই নকল করে test বানায়Test কিছুই যাচাই করে নাTest আসল কোডে চালানো; ইচ্ছা করে একটি ভুল ঢুকিয়ে দেখা test ধরে কি না (mutation, পর্ব ৫০)
“পুরোটা নতুন framework-এ লিখে ফেলুন” বলেBig-bang rewrite-এর সব ঝুঁকিHotspot আর break-even হিসাব; strangler fig পরিকল্পনা চাওয়া
সব dependency একসাথে সর্বশেষ major-এ তোলার command দেয় (যেমন npm-এর audit fix --force, যা major version-ও বদলাতে পারে)একসাথে অনেক breaking changeএক major করে, changelog পড়ে, throwaway clone-এ আগে চালিয়ে
Lehman-এর সূত্রের সংখ্যা, SQALE-এর নিয়ম বা ISO-র সংস্করণ বানিয়ে বলেভুল তথ্য লেখায় বা পরীক্ষায়মূল উৎস, মানের প্রকাশিত সারাংশ মিলিয়ে দেখা
History পরিষ্কারের জন্য push --force বা reset --hard দেয়, সতর্কতা ছাড়াদলের কাজ হারানোScratch clone-এ আগে; git help পড়া; --force-with-lease (পর্ব ১৪)
Prompt-এ পুরো proprietary কোড বা config-সহ secret পাঠানো হয়গোপন তথ্য বা লাইসেন্স-ঝুঁকিশুধু দরকারি function; secret সরিয়ে; প্রতিষ্ঠানের নীতি মেনে

নিচের prompt AI-কে আচরণ বদলাতে নিষেধ করে, আর প্রতিটি দাবির প্রমাণ চায়। শেষ শর্তটি সবচেয়ে গুরুত্বপূর্ণ — অদ্ভুত নিয়মগুলো আলাদা তালিকায়, যাতে সিদ্ধান্ত মানুষ নেন:

নিচে একটি পুরোনো Python function আছে (test নেই)। আমি এটি refactor করতে চাই, কিন্তু আচরণ একটুও বদলানো যাবে না। ১. আগে একটি characterization test লেখো: ৩০–৫০টি input বেছে নাও, সীমানার মানসহ (যেমন ঠিক 500, 501, 1000, 1001), আর প্রত্যাশিত ফল হিসেবে কিছু অনুমান করো না — কোডটি চালিয়ে যা আসে সেটিই রেকর্ড করার কোড দাও। ২. তারপর ছোট ছোট refactoring ধাপ দাও; প্রতিটি ধাপের পরে test চালানোর কথা বলো। ৩. যেসব লাইন অদ্ভুত বা সম্ভাব্য ভুল মনে হয়, সেগুলো বাদ দিও না — আলাদা তালিকায় লেখো, কেন অদ্ভুত লাগছে তাও বলো। ৪. কোনো বই, মান বা নিয়মের কথা বললে নাম আর বছর দাও; নিশ্চিত না হলে বলো "নিশ্চিত নই"। [এখানে function-টি পেস্ট করুন — password, API key বা গ্রাহকের data সরিয়ে]

AI-এর সাহায্যে legacy code বদলানোর আগে — চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১“Technical debt” কি এখন খারাপ কোডের ভদ্র অজুহাত হয়ে গেছে? Cunningham-এর মূল অর্থ থেকে আমরা কতটা সরে এসেছি?

অনেকটাই সরে এসেছি, আর এই সরে আসার দুটি দিক আছে। ১৯৯২ সালের WyCash experience report-এ Cunningham-এর মূল কথা ছিল: শুরুর কোড লেখা হয় সমস্যার অপূর্ণ বোঝাপড়া দিয়ে; ব্যবহারের সাথে সাথে বোঝাপড়া বাড়ে; তখন কোডকে সেই বোঝাপড়ার সাথে মিলিয়ে নেওয়াই শোধ। পরে তিনি স্পষ্ট করেন বলে উল্লেখ করা হয় যে তিনি অগোছালো কোড লেখার অনুমতি দিচ্ছিলেন না — কোড এমন পরিষ্কার হতে হবে, যাতে বোঝাপড়া বদলালে refactor করা যায়। অর্থাৎ “debt” মূলত শেখার ব্যবধান।

আজকের ব্যবহারে শব্দটি প্রায়ই সব খারাপ জিনিসের ঝুড়ি: test নেই, নাম খারাপ, ভুল architecture, পুরোনো library — সবই “debt”. এর একটি ভালো দিক আছে। Product owner বা ব্যবস্থাপক “cohesion কম” বোঝেন না, কিন্তু “সুদ দিচ্ছি” বোঝেন; রূপকটি প্রকৌশলের সমস্যাকে ব্যবসার ভাষায় অনুবাদ করে। Fowler-এর ২০০৯ সালের quadrant এই বিস্তৃত ব্যবহারকে মেনে নিয়েই শ্রেণিবিন্যাস দেয়, আর তাতে বেপরোয়া debt-কেও জায়গা দেয়।

খারাপ দিকটি হলো, “debt” শব্দটি দায়িত্ব এড়ানোর সুযোগ দেয়। “আমরা debt নিলাম” বলার পরে যদি register-এ কিছু না থাকে, শোধের তারিখ না থাকে, আর সুদ কেউ না মাপেন, তাহলে সেটি ঋণ নয়, সেটি শুধু অযত্ন — Fowler-এর ভাষায় বেপরোয়া। Robert C. Martin-ও একবার লিখেছিলেন, অগোছালো কোড debt নয়, শুধুই অগোছালো।

ব্যবহারিক মাপকাঠি তাই এমন: একটি শর্টকাটকে “debt” বলার অধিকার আছে, যদি (১) সিদ্ধান্তটি সচেতন বা অন্তত এখন সচেতনভাবে চিহ্নিত, (২) লিখে রাখা, (৩) আনুমানিক সুদ আর principal জানা, আর (৪) শোধের পরিকল্পনা বা না-শোধের যুক্তিসংগত কারণ আছে। বাকিটা আগে register-এ তুলে তবেই debt.

মূল উপলব্ধি: রূপকটি যোগাযোগের হাতিয়ার হিসেবে অসাধারণ, কিন্তু রূপক মানে চুক্তি। হিসাব না রাখলে “debt” শব্দটি শুধু অযত্নকে সম্মানজনক নাম দেয়।

প্র ০২Spolsky বলেছিলেন কোড নতুন করে লেখা কখনো উচিত নয়। তাহলে প্রতিষ্ঠানগুলো এখনো বড় rewrite করে কেন, আর কখন সেটি যুক্তিসংগত?

Spolsky-র ২০০০ সালের লেখার শক্তি একটি পর্যবেক্ষণে: পুরোনো কোড কুৎসিত দেখায় কারণ তাতে বছরের পর বছরের শেখা জমা — একটি অদ্ভুত if হয়তো কোনো প্রিন্টারের ত্রুটি সামলায়। নতুন কোড লেখার সময় developer সেই প্রেক্ষাপট জানেন না, তাই একই ভুলগুলো আবার করতে হয়। তবে একটি ঘটনা দিয়ে নিয়ম প্রমাণ হয় না।

Brooks নিজেও এই প্রশ্নে মত বদলেছিলেন। ১৯৭৫ সালের The Mythical Man-Month-এ তিনি লেখেন, “একটি ফেলে দেওয়ার পরিকল্পনা করুন” (plan to throw one away) — প্রথম system-টি শেখার জন্য, কারণ সেটি ফেলতেই হবে। ১৯৯৫ সালের বিংশ বার্ষিকী সংস্করণে তিনি স্বীকার করেন বলে উল্লেখ করা হয় যে এই পরামর্শ অতি সরল ছিল; incremental development-এর যুগে ধাপে ধাপে বাড়ানোই ভালো পথ।

তবু rewrite হয়, আর কখনো কখনো তা সঠিক সিদ্ধান্ত। যুক্তিসংগত পরিস্থিতিগুলো সাধারণত এমন: platform মৃত (ভাষা, framework বা OS-এর নিরাপত্তা-সমর্থন শেষ, দক্ষ মানুষ পাওয়া যায় না); system-টি ছোট, আর তার আচরণ ভালো test বা স্পষ্ট specification-এ ধরা; অথবা ব্যবসার মডেল এতটাই বদলেছে যে পুরোনো কোডের মূল ধারণাগুলোই (যেমন “একটি order-এ একটি parcel”) আর খাটে না। কিন্তু এসব ক্ষেত্রেও সফল দলগুলো সাধারণত rewrite করে অংশে অংশে — strangler fig, branch by abstraction, ছায়া-চালনায় দুই system-এর ফল মিলিয়ে।

তাহলে প্রশ্নটি “rewrite না refactor” নয়, প্রশ্ন “একবারে না ধাপে ধাপে”। একবারে rewrite-এ সব ঝুঁকি শেষে জমা হয়, মাঝপথে কোনো মূল্য দেয় না, আর থামানো যায় না। ধাপে ধাপে প্রতিস্থাপনে প্রতিটি ধাপ নিজেই মূল্য দেয়, আর মাঝপথে থেমে গেলেও system আগের চেয়ে ভালো অবস্থায় থাকে।

মূল উপলব্ধি: পুরোনো কোড শুধু কোড নয়, জমা জ্ঞান। নতুন করে লিখতে হলে সেই জ্ঞান আগে test-এ তুলে নিন, তারপর অংশে অংশে বদলান — যাতে প্রতিটি দিন system চালু থাকে।

প্র ০৩Lehman-এর সূত্রকে “law” বলা কি ঠিক? পদার্থবিদ্যার সূত্রের সাথে এগুলোর পার্থক্য কোথায়?

পদার্থবিদ্যার সূত্র প্রকৃতির নিয়ম — মানুষ চাইলেও বদলাতে পারে না। Lehman-এর সূত্রগুলো তেমন নয়; এগুলো মানুষ, সংগঠন আর software-এর মিলিত ব্যবস্থার পর্যবেক্ষিত প্রবণতা। Lehman নিজেও এগুলোকে সামাজিক-প্রযুক্তিক প্রক্রিয়ার নিয়মিততা হিসেবে দেখতেন, আর ১৯৬০-এর দশকের শেষ থেকে IBM OS/360-এর release-এর তথ্য দিয়ে শুরু করে পরে অন্য বড় system-এর তথ্য দিয়ে যাচাই করেছিলেন। তাঁর গুরুত্বপূর্ণ অবদান ছিল প্রোগ্রামের ধরন আলাদা করা: S-type (specification দিয়ে পুরো সংজ্ঞায়িত, যেমন একটি গাণিতিক function), P-type, আর E-type — বাস্তব জগতে ব্যবহৃত, জগতের অংশ হয়ে যাওয়া system. সূত্রগুলো মূলত E-type-এর জন্য।

সমালোচনাও আছে। ২০০০ সালের দিকে Michael Godfrey আর Qiang Tu Linux kernel-এর বৃদ্ধি বিশ্লেষণ করে দেখান, kernel-এর আকার কিছু সময় ধরে প্রত্যাশার চেয়ে দ্রুত (super-linear) বাড়ছিল, যা Lehman-এর কিছু ভবিষ্যদ্বাণীর (যেমন বৃদ্ধির হার ধীর হওয়া) সাথে মেলে না বলে আলোচনা হয়। একটি ব্যাখ্যা: হাজার স্বেচ্ছাসেবী আর driver-এর মতো প্রায়-আলাদা অংশের open-source কাঠামো বড় প্রতিষ্ঠানের এক-দলের প্রক্রিয়া থেকে আলাদা।

তবে প্রথম দুটি সূত্র — continuing change আর increasing complexity — প্রায় সব প্রেক্ষাপটে টিকে আছে, কারণ এগুলোর যুক্তি সরল: জগৎ বদলায়, তাই জগতের অংশ software-কেও বদলাতে হয়; আর প্রতিটি বদল কিছু না কিছু জটিলতা যোগ করে, যা আলাদা করে সরাতে কাজ লাগে। দ্বিতীয় সূত্রের শর্তযুক্ত অংশটিই আশার কথা: “যদি না তা কমানোর জন্য কাজ করা হয়।” অর্থাৎ জটিলতা বাড়া অনিবার্য নয়; তবে বিনা খরচে কমেও না।

“Law” শব্দটি তাই অর্থনীতির “চাহিদার সূত্র”-র কাছাকাছি — একটি জোরালো প্রবণতা, যা পরিকল্পনায় হিসাবে রাখতে হয়, কিন্তু যার ব্যতিক্রম আর শর্ত আছে।

মূল উপলব্ধি: Lehman-এর সূত্রগুলো ভবিষ্যদ্বাণী নয়, সতর্কবাণী। এগুলো বলে refactoring budget বিলাসিতা নয় — জটিলতার স্বাভাবিক বৃদ্ধির বিরুদ্ধে একমাত্র নিয়মিত প্রতিরোধ।

প্র ০৪Dependency upgrade “পরে করব” বলে ফেলে রাখা কেন নিরাপত্তার সমস্যা হয়ে দাঁড়ায়? Equifax আর Log4Shell থেকে কী শেখার আছে?

নিরাপত্তা-ত্রুটি প্রকাশিত হলে একটি দৌড় শুরু হয়: রক্ষাকারীরা patch বসাতে চান, আক্রমণকারীরা তার আগেই ত্রুটিটি কাজে লাগাতে চান। Patch প্রকাশ মানে ত্রুটির খবরও প্রকাশ — তাই patch না বসানো system প্রকাশের দিন থেকেই বেশি ঝুঁকিতে। ২০১৭ সালের Equifax ঘটনায় মার্কিন সরকারি তদন্ত-প্রতিবেদনগুলোতে বলা হয়, Apache Struts-এর যে ত্রুটি দিয়ে অনুপ্রবেশ ঘটে, তার সংশোধন মার্চে প্রকাশিত হয়েছিল, অথচ আক্রান্ত system-এ তা প্রয়োগ হয়নি; পরের কয়েক মাসে কোটি মানুষের ব্যক্তিগত তথ্য ফাঁস হয়। (বিস্তারিত ঘটনাক্রম প্রতিবেদনভেদে কিছুটা আলাদা।)

এখানে dependency debt দুইভাবে সুদ নেয়। প্রথমত, upgrade যত পিছিয়ে, নিরাপত্তা-patch বসানো তত কঠিন — যদি আপনি তিনটি major version পিছিয়ে থাকেন, আর patch শুধু সর্বশেষ version-এ আসে, তাহলে “একটি patch” আসলে একটি বড় migration project হয়ে যায়, আর সেই সপ্তাহগুলোতে system অরক্ষিত থাকে। দ্বিতীয়ত, কোন dependency কোথায় আছে সেটিই না জানা — ২০২১ সালের ডিসেম্বরে Log4Shell প্রকাশের পর বহু প্রতিষ্ঠান দিনের পর দিন শুধু খুঁজতে ব্যয় করেছে কোন system-এ, কোন version-এ Log4j আছে, কারণ এটি প্রায়ই সরাসরি নয়, অন্য library-র ভেতর দিয়ে (transitive dependency) এসেছিল।

প্রতিরক্ষামূলক অভ্যাসগুলো তাই সরল কিন্তু নিয়মিত হতে হয়: lockfile আর dependency-র তালিকা (SBOM) রাখা, যাতে প্রশ্ন উঠলে এক ঘণ্টায় উত্তর মেলে; ছোট ছোট upgrade নিয়মিত করা, যাতে জরুরি patch একটি ছোট লাফ হয়; CI-তে dependency-র পরিচিত ত্রুটি যাচাই চালানো; আর end-of-life তারিখ debt register-এ রাখা।

মূল উপলব্ধি: Dependency debt-এর সুদ হঠাৎ, একসাথে আর সবচেয়ে খারাপ সময়ে আদায় হয়। নিয়মিত ছোট upgrade হলো সেই কিস্তি, যা বড় বিপদের দিনে আপনাকে প্রস্তুত রাখে।

প্র ০৫Characterization test একটি সম্ভাব্য bug-কেও (যেমন পাহাড়ি অঞ্চলের ৫ টাকা ছাড়) “ঠিক” হিসেবে পিন করে। এটি কি ভুলকে স্থায়ী করা নয়?

প্রশ্নটি স্বাভাবিক, আর এর উত্তরে লুকিয়ে আছে legacy কাজের মূল দর্শন। Characterization test কখনো দাবি করে না যে আচরণটি সঠিক; সে শুধু বলে “আজ এমনটাই ঘটে।” এর উদ্দেশ্য দুটি প্রশ্নকে আলাদা করা: (১) refactoring-এর সময় আচরণ অজান্তে বদলাচ্ছে কি না, আর (২) কোন আচরণ জেনেশুনে বদলানো উচিত। দুটি একসাথে করলে বোঝা যায় না কোন পার্থক্যটি ইচ্ছাকৃত আর কোনটি দুর্ঘটনা।

দ্বিতীয় কারণটি আরও গভীর: যে আচরণ দীর্ঘদিন ধরে চলছে, তার ওপর কেউ না কেউ নির্ভর করেন। পাহাড়ি অঞ্চলের ৫ টাকা হয়তো কোনো পুরোনো চুক্তির অংশ। Google-এর প্রকৌশলী Hyrum Wright-এর নামে প্রচলিত “Hyrum's law” কথাটি এমন: API-র ব্যবহারকারী যথেষ্ট হলে, আপনি চুক্তিতে কী প্রতিশ্রুতি দিয়েছেন তাতে কিছু আসে যায় না — system-এর প্রতিটি দেখা যায় এমন আচরণের ওপর কেউ না কেউ নির্ভর করবেন। তাই চুপচাপ “ঠিক” করা নিজেই একটি breaking change হতে পারে।

সঠিক প্রক্রিয়া তাই এমন: test-এ আচরণ পিন করুন, মন্তব্যে লিখুন যে এটি সন্দেহজনক, debt register-এ একটি ticket খুলুন (DEBT-7), refactor শেষ করুন। তারপর আলাদাভাবে product owner আর ব্যবসার মানুষদের সাথে আলোচনা — নিয়মটির ইতিহাস খোঁজা (git log -S, পুরোনো ticket), কারা প্রভাবিত হবেন, আগে জানাতে হবে কি না। সিদ্ধান্ত হলে সেই বদল একটি আলাদা, স্পষ্ট commit-এ — test-এর প্রত্যাশা বদলে, কারণসহ।

এভাবে ভুলটি স্থায়ী হয় না; বরং প্রথমবারের মতো দৃশ্যমান হয় — একটি নাম-দেওয়া test আর একটি ticket, সিদ্ধান্তের অপেক্ষায়।

মূল উপলব্ধি: আগে আচরণ ধরে রাখুন, পরে আচরণ নিয়ে সিদ্ধান্ত নিন। Refactoring আর আচরণ-বদল আলাদা commit-এ থাকলে দুটোই নিরাপদ, আর ভুল হলে কোনটি দায়ী তা এক নজরে বোঝা যায়।

অনুশীলন

হিসাব, পূর্বাভাস, সমালোচনা আর command — চার রকম কাজ। আগে নিজে চেষ্টা করুন, তারপর উত্তর মেলান।

  1. Break-even হিসাব: দুটি debt. (ক) invoice.py মাসে ৮ বার বদলায়; debt-এর কারণে প্রতি বদলে বাড়তি ২.৫ ঘণ্টা লাগে; শোধ করতে লাগবে ৫০ ঘণ্টা, তারপরও প্রতি বদলে ০.৫ ঘণ্টা বাড়তি থাকবে। (খ) monthly.py মাসে গড়ে ০.৫ বার বদলায়; বাড়তি ৬ ঘণ্টা; শোধ ৮০ ঘণ্টা; পরে বাড়তি ১ ঘণ্টা। সুদ স্থির ধরে প্রতিটির break-even আর ১২ মাসের নিট লাভ-ক্ষতি বের করুন। কোনটি আগে শোধ করবেন?

    (ক) মাসিক সাশ্রয় = ৮ × (২.৫ − ০.৫) = ১৬ ঘণ্টা। Break-even = ৫০ ÷ ১৬ = ৩.১২৫ মাস, অর্থাৎ চতুর্থ মাসে। ১২ মাসে সাশ্রয় ১২ × ১৬ = ১৯২ ঘণ্টা, খরচ ৫০, নিট লাভ ১৪২ ঘণ্টা।

    (খ) মাসিক সাশ্রয় = ০.৫ × (৬ − ১) = ২.৫ ঘণ্টা। Break-even = ৮০ ÷ ২.৫ = ৩২ মাস। ১২ মাসে সাশ্রয় ৩০ ঘণ্টা, খরচ ৮০, নিট ৫০ ঘণ্টা ক্ষতি।

    সিদ্ধান্ত: (ক) আগে, এখনই। (খ)-এর প্রতি বদলের সুদ বেশি (৬ ঘণ্টা), কিন্তু বদল এত কম যে শোধ লাভজনক নয় — register-এ রেখে দিন, আর যখন বদলাতে হবে তখন boy scout rule-এ সেই অংশটুকু গুছান। ব্যতিক্রম: যদি monthly.py-র ভুলে বড় আর্থিক ঝুঁকি থাকে (যেমন সরকারি রিপোর্ট), তাহলে সময়ের হিসাবের বাইরে ঝুঁকির হিসাবও যোগ করতে হবে।

  2. আউটপুট অনুমান করুন: নিচের function charge_legacy-এর ঢাকা-অংশ c = 60 + ((w - 1) // 1000) * 15 (w > 1000 হলে, নইলে 60), আর COD হলে c + int(c * 0.01 + 0.5). একজন developer refactor করে int(c * 0.01 + 0.5)-এর জায়গায় Python-এর round(c * 0.01) লিখলেন। ০ থেকে ৬,০০০ গ্রামের মধ্যে ২৫০ গ্রাম পরপর ২৫টি ওজনের golden master-এর সাথে তুলনা করলে ঢাকা-COD ক্ষেত্রে কতটি অমিল আসবে? তাহলে কি refactor-টি নিরাপদ?

    অমিল: ০টি। এই পরিসরে ঢাকার base চার্জ হয় ৬০, ৭৫, ৯০, ১০৫, ১২০ বা ১৩৫; তার ১% = ০.৬ থেকে ১.৩৫ — দুই পদ্ধতিতেই ১। চালিয়ে যাচাই করা ফল: v3 diffs on golden: 0.

    কিন্তু নিরাপদ নয়। Python-এর round() ঠিক মাঝের মানে জোড় সংখ্যার দিকে যায় (banker's rounding): round(4.5) = 4, অথচ int(4.5 + 0.5) = 5. Base ৪৫০ হলে (২৬,০০১ গ্রাম থেকে) পার্থক্য দেখা দেয় — চালিয়ে পাওয়া প্রথম অমিল: ২৬,০০১ গ্রামে legacy 455, নতুন 454. Golden master শুধু রেকর্ড করা input রক্ষা করে; ভারী parcel-এর ক্ষেত্র না রাখায় পরীক্ষাটি এই পার্থক্য ধরতে পারেনি। শিক্ষা: সীমানা আর বাস্তব data-র পরিসর থেকে input বাছুন, আর rounding-এর মতো “একই তো” বদলকে সন্দেহ করুন।

  3. সমালোচনা — rewrite না refactor? একটি কাল্পনিক বিশ্ববিদ্যালয়ের ফলাফল portal: ১০ বছর পুরোনো, test নেই, চারজনের দল। নতুন ব্যবস্থাপক প্রস্তাব দিলেন: “ছয় মাস নতুন feature বন্ধ, পুরোটা একটি নতুন framework-এ নতুন করে লিখব।” Portal-এর ভাষার version-এর নিরাপত্তা-সমর্থন আগামী বছর শেষ হবে। প্রস্তাবটির সমালোচনা করুন আর একটি বিকল্প পরিকল্পনা দিন।

    প্রস্তাবের যৌক্তিক দিক: platform-এর মেয়াদ শেষ হচ্ছে, তাই কিছু একটা করতেই হবে — “কিছু না করা” এখানে বেপরোয়া debt.

    ঝুঁকি: (১) ছয় মাসের অনুমান প্রায় নিশ্চিতভাবেই আশাবাদী — পুরোনো কোডের লুকানো নিয়ম (grace mark, পুনঃপরীক্ষার নিয়ম, পুরোনো ব্যাচের আলাদা গ্রেডিং) test-এ নেই, নতুন দল সেগুলো ধাপে ধাপে আবিষ্কার করবে। (২) ফলাফল প্রকাশের মৌসুমে feature বন্ধ রাখা অবাস্তব; পুরোনো system-এ বদল চলতেই থাকবে, নতুনটি চলমান লক্ষ্যের পেছনে দৌড়াবে। (৩) Second-system effect — নতুন framework-এ সব ইচ্ছা ঢোকানোর প্রলোভন। (৪) শেষে একদিনে সুইচ — সব ঝুঁকি এক দিনে।

    বিকল্প: (১) প্রথম দুই সপ্তাহ: hotspot বিশ্লেষণ আর গুরুত্বপূর্ণ হিসাবগুলোর (GPA, grade) golden master — গত কয়েক বছরের আসল ফলাফল দিয়ে, অনুমতি আর গোপনীয়তা মেনে। (২) সামনে একটি facade; strangler fig-এ route ধরে নতুন framework-এ সরানো, সবচেয়ে কম ঝুঁকির অংশ (notice, profile) আগে, GPA হিসাব শেষে, ছায়া-চালনায় ফল মিলিয়ে। (৩) Refactoring budget-এর মতো ক্ষমতার একটি ভাগ migration-এ, বাকিটা জরুরি feature-এ। (৪) Platform-এর মেয়াদ শেষের আগে অন্তত বাইরের দিকের অংশগুলো সরে যাক; বাকি অংশ ঝুঁকির তালিকায়, দরকার হলে বাড়তি নিরাপত্তা-ব্যবস্থাসহ। ব্যবস্থাপককে দেখান: শেষ লক্ষ্য একই, পথের ঝুঁকি আলাদা।

  4. Command লিখুন: নিজের একটি repository-তে গত ছয় মাসে সবচেয়ে বেশি বদলানো ৫টি file বের করার command লিখুন, যাতে package-lock.json গোনা না হয়। তারপর সেই file-গুলোর লাইন-সংখ্যা দেখার একটি উপায় বলুন, আর এই পদ্ধতির দুটি সীমাবদ্ধতা লিখুন।

    একটি সম্ভাব্য উত্তর (Git Bash বা Linux/macOS terminal-এ):

    git log --since="6 months ago" --format= --name-only -- . ':(exclude)package-lock.json' | sort | uniq -c | sort -rn | head -5
    wc -l billing/tariff.py billing/invoice.py

    -- . ':(exclude)…' হলো Git-এর pathspec: “পুরো repository, কিন্তু এই file বাদ”। পার্সেলবাড়ির scratch repository-তে ':(exclude)zones/' দিয়ে চালিয়ে দেখা গেছে, zone-এর file তালিকা থেকে বাদ পড়ে। লাইন-সংখ্যার জন্য wc -l; আরও ভালো মাপের জন্য language-এর complexity tool (পর্ব ৫০)।

    সীমাবদ্ধতা: (১) File-এর নাম বদলালে (rename) পুরোনো নামের churn আলাদা গোনা হয়, কারণ --name-only শুধু নাম দেখে। (২) যন্ত্রে তৈরি file (lockfile, build-এর ফল, migration) churn বাড়ায় কিন্তু debt নয় — বাদ দিতে হয়। (৩) Commit-এর আকার আলাদা: একটি বিশাল formatting commit আর একটি এক-লাইনের fix সমান গোনা হয়। এগুলো মাথায় রেখে সংখ্যাকে আলোচনা শুরুর তথ্য হিসেবে নিন।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমMichael Feathers-এর legacy code বদলানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🎯 বদলের জায়গা (change point) চিহ্নিত করা
  2. 🔍 Test কোথায় বসানো যায় (test point) খোঁজা
  3. 🔌 Dependency ভেঙে seam খোলা
  4. 🧪 Characterization test লেখা
  5. 🛠️ বদল করা আর refactor

Ward Cunningham ১৯৯২ সালে “debt” রূপক দিয়ে মূলত কী বোঝাতে চেয়েছিলেন?

  • তাড়া থাকলে অগোছালো কোড লেখা ঠিক আছে, পরে কেউ গুছিয়ে নেবে
  • প্রথমবারের কোড ship করা একরকম ধার — বোঝাপড়া বাড়লে দ্রুত rewrite বা refactor করে শোধ করতে হয়
  • Software project-এর জন্য ব্যাংক থেকে ঋণ নেওয়ার ঝুঁকি
  • শুধু পুরোনো library ব্যবহারের ঝুঁকি

একটি দল বলল: “Demo-র তারিখ বদলানো যাবে না; আমরা জানি payment module-এর নকশা দুর্বল থাকছে, ticket খুলে পরের দুই sprint-এ ঠিক করব।” Fowler-এর quadrant-এ এটি কোন ঘরে?

  • বেপরোয়া · জেনেশুনে
  • বেপরোয়া · অজান্তে
  • বিবেচক · জেনেশুনে
  • বিবেচক · অজান্তে

চারটি file-এর তথ্য (ছয় মাসের commit-সংখ্যা, জটিলতা): A (৯০, ৯৫), B (১০, ৯০), C (৯৫, ১০), D (২০, ২০)। Hotspot-এর যুক্তিতে কোনটির debt আগে দেখা উচিত?

  • A
  • B
  • C
  • D

সরকার নতুন VAT হার ঘোষণা করল, তাই একটি billing software-এ হিসাব বদলাতে হলো। ISO/IEC 14764 অনুযায়ী এটি কোন ধরনের maintenance?

  • Corrective
  • Adaptive
  • Perfective
  • Preventive

Lehman-এর “increasing complexity” সূত্রটি কী বলে?

  • যা-ই করা হোক, software-এর জটিলতা বাড়বেই
  • ব্যবহারকারী বাড়লে জটিলতা বাড়ে
  • প্রতিটি নতুন feature ঠিক দ্বিগুণ জটিলতা আনে
  • বিবর্তনের সাথে জটিলতা বাড়ে, যদি না তা কমানোর জন্য আলাদা কাজ করা হয়

Test ছাড়া একটি জটিল build_invoice() function-এ নতুন ছাড়ের নিয়ম যোগ করতে হবে। “Sprout method” কৌশলে কী করা হয়?

  • পুরোনো function-এর ভেতরে প্রয়োজনীয় জায়গায় নতুন লজিক লিখে দেওয়া
  • পুরোনো function-এর নাম বদলে একই নামে নতুন একটি function বানানো, যা পুরোনোটিকে ডাকে
  • নতুন লজিক একটি আলাদা, test-সহ function-এ লিখে পুরোনো function থেকে এক লাইনে ডাকা
  • পুরো function নতুন করে লিখে ফেলা

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Debt = principal + interest: শোধের কাজ আর না-শোধ করার চলমান খরচ; সুদ দিতে হয় শুধু কোড ছুঁলে, তাই বদলের হার অগ্রাধিকার ঠিক করে।
  2. Fowler-এর চার ঘর — জেনেশুনে/অজান্তে × বিবেচক/বেপরোয়া; ঘর চিনলে প্রতিকার বাছাই সহজ। Debt থাকে code, design, test, documentation, infrastructure আর dependency-তে।
  3. Hotspot = churn × complexity, Git history থেকে; debt register-এ লিখে রাখুন; break-even ≈ P ÷ (r × (h − h′)).
  4. শোধের পথ: boy scout rule, refactoring budget, ছোট slice, strangler fig; big-bang rewrite প্রায় সবসময় শেষ বিকল্প।
  5. Maintenance-এর চার রকম (ISO/IEC 14764), Lehman: ব্যবহৃত software বদলাতেই হয়, যত্ন না নিলে জটিলতা বাড়ে। Legacy code = test ছাড়া কোড — আগে characterization test, তারপর seam, sprout, wrap আর ছোট ধাপের refactor.

পরের পর্বে: Debt সবচেয়ে সস্তায় ধরা যায় কোড merge হওয়ার আগেই। পর্ব ৪৪-এ code review — কী দেখবেন, কীভাবে মন্তব্য করবেন, আর linter ও static analysis কোথায় মানুষের চোখকে সাহায্য করে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান