মূল বিষয়বস্তুতে যান
Clean Code Mastery

Substitute Algorithm: স্কুলে যাওয়ার নতুন সোজা রাস্তা

Substitute Algorithm রিফ্যাক্টরিং শেখো সাইকেলের রুটের গল্প দিয়ে — TypeScript আর Python উদাহরণ সহ, আর টেস্ট-ফার্স্ট নিরাপত্তার নিয়ম যেটা সব শিক্ষার্থীর জানা দরকার।

25 মিনিট আপডেট: June 11, 2026beginner
refactoringsubstitute algorithmcomposing methodsclean codealgorithmstests

নতুন সোজা রাস্তার গল্প

ধরো রুবেলের কথা। সে সপ্তম শ্রেণির ছাত্র, প্রতিদিন সকালে সাইকেল চালিয়ে স্কুলে যায়। তার রুট পুরো এলাকায় বিখ্যাত — হাস্যকর হওয়ার জন্য বিখ্যাত।

এটা কখনো পরিকল্পনা করা হয়নি। এটা বেড়ে উঠেছে। বছর আগে সরাসরি রাস্তাটা পাইপের জন্য খোঁড়া হয়েছিল, তাই সবাই দুধের দোকানের কাছে বাঁ দিকে মোড় নিত। তারপর বাজারের পেছনের শর্টকাট বিয়ের হলের জন্য বন্ধ হয়ে গেল, তাই পুকুরের চারপাশ ঘুরতে হতো। তারপর একটা কুকুরের কোণ এলো, তাই মসজিদের পাশ দিয়ে ঘুর দিতে হলো। প্রতিটা মোড় সেই সময় যুক্তিসংগত ছিল। আজ খোদাই শেষ, বিয়ের হলে পেছনের গেট আছে, কুকুরগুলো চলে গেছে — কিন্তু রুবেল এখনো পুরো আঁকাবাঁকা পথে যায়। দুধের দোকানে বাঁ দিকে, পুকুর ঘুরে, মসজিদের পাশ দিয়ে, অন্য স্কুলের সাইকেল স্ট্যান্ড পেরিয়ে, তারপর নিজের স্কুলের গেট দিয়ে ঢোকে। পঁচিশ মিনিট, এগারোটা মোড়, তিনটা জায়গা যেখানে নতুন ছাত্ররা সবসময় হারিয়ে যায়।

গত টার্মে তার ছোট কাজিন সুমাইয়া স্কুলে ভর্তি হয়ে রুটটা আঁকতে বলল। রুবেলের একটা পুরো পৃষ্ঠা লাগল, আর কাগজের প্রান্তের বাইরে দেখানো দুটো তীর। "মসজিদের পাশ দিয়ে কেন যাই?" সুমাইয়া জিজ্ঞেস করল। রুবেল মুখ খুলল, তারপর বন্ধ করল। সে আসলে আর জানে না। কেউ জানে না। রুটটা প্যাডেল সহ বিশুদ্ধ ইতিহাসে পরিণত হয়েছে।

তারপর এক সোমবার, পৌরসভা একটা একদম নতুন সোজা রাস্তা খুলে দিল। রুবেলের কলোনির গেট থেকে সরাসরি স্কুলে। একই শুরু, একই গন্তব্য — সাত মিনিট, দুটো মোড়।

এখন গুরুত্বপূর্ণ প্রশ্ন: রুবেলের কি পুরনো রুট উন্নত করা উচিত? হয়তো পুকুরের কাছে একটু সময় বাঁচানো? না। একটা আঁকাবাঁকাকে প্যাচ করলেও আঁকাবাঁকাই থাকে। সৎ পদক্ষেপ হলো পুরনো রুট সম্পূর্ণ ছেড়ে দিয়ে নতুন রাস্তা নেওয়া। লক্ষ্য একই — বাড়ি ছেড়ে সময়মতো স্কুলে পৌঁছানো। শুধু পথ বদলায়।

কিন্তু দেখো রুবেল কতটা সতর্ক। স্কুলের দিনে switch করার আগে, সে একটা রবিবার নতুন রাস্তা পরীক্ষা করে। গেটে সুমাইয়া সময় মাপতে থাকতে একবার চালায়। এটা কি আসলেই স্কুলের গেটে পৌঁছায়? সকাল ৭টায় কি খোলা থাকে? বৃষ্টির পরে কি ডুবে যায়? এমনকি ভারী ব্যাগ নিয়েও একবার চালায়। প্রতিটা check পাস করার পরেই সে switch করে — আর তারপর আর পেছনে তাকায় না। পুরনো রুট শুধু একটা জায়গায় বেঁচে আছে: সুমাইয়ার হাতে আঁকা মানচিত্রে।

চিত্র ১: রুবেলের সকাল, নতুন রাস্তার আগে আর পরে

কোডেও আঁকাবাঁকা রুট তৈরি হয়। একটা method সহজ শুরু হয়, তারপর একটা bug fix একটা flag যোগ করে, একটা edge case একটা nested loop যোগ করে, একটা বিশেষ customer একটা অদ্ভুত branch যোগ করে। বছর পরে method-টা কাজ করে, কিন্তু কীভাবে কাজ করে সেটা ঐতিহাসিক দুর্ঘটনার একটা সফর। যখন তুমি একটা সোজা রাস্তা খুঁজে পাও — একটা সহজ algorithm, একটা পরিষ্কার structure, একটা ভালোভাবে পরীক্ষিত library call — সঠিক পদক্ষেপ হলো জট প্যাচ করা নয়। পুরনো body ফেলে দিয়ে সেখানে সোজা রাস্তা রাখা। এই রিফ্যাক্টরিংকে বলে Substitute Algorithm। আর রুবেলের রবিবারের টেস্ট রাইড? সেটাই তোমার test suite, আর সেটা অবশ্যই আগে আসতে হবে।

Substitute Algorithm কী?

Substitute Algorithm হলো সেই রিফ্যাক্টরিং যেখানে তুমি একটা method-এর পুরো body একটা ভিন্ন, পরিষ্কার algorithm দিয়ে replace করো যেটা একই result দেয়। method-এর contract — তার নাম, তার parameter, সে কী return করে, caller-রা কী observe করতে পারে — হুবহু একই থাকে। শুধু ভেতরটা বদলায়।

এটাকে Composing Methods পরিবারের একটা অস্বাভাবিক সদস্য বলা যায়। তার ভাই-বোনেরা বেশিরভাগ কোড সরায়: Extract Method লাইনগুলো একটা নতুন function-এ নিয়ে যায়, Replace Method with Method Object একটা পুরো computation একটা class-এ নিয়ে যায়, Remove Assignments to Parameters কাজটাকে একটা local-এ নতুন নাম দেয়। সেগুলোতে মূল logic বেঁচে থাকে, শুধু নতুন জায়গায়। Substitute Algorithm আলাদা: মূল logic মুছে যায়। গন্তব্য একই; রাস্তা সম্পূর্ণ নতুন।

কেন এত কঠোর কিছু করবে? কারণ কিছু implementation লাইন-বাই-লাইন উন্নত করার যোগ্য নয়। flag, nested loop, manual break, আর প্যাচ-করা special case দিয়ে তৈরি একটা method এত জটিল হতে পারে যে প্রতিটা ছোট edit সেটা ভেঙে দেওয়ার ঝুঁকি রাখে। মৌলিকভাবে সহজ একটা পদ্ধতি জানলে, একটা পরিষ্কার stroke-এ body পুনরায় লেখা দশটা ঝুঁকিপূর্ণ micro-edit-এর চেয়ে সহজ আর নিরাপদ — যদি নীচের নিরাপত্তার নিয়ম মানা হয়।

💡

মনে রাখার একটা লাইন: একই গন্তব্য, নতুন রাস্তা — method-এর contract রাখো, body replace করো, আর test প্রমাণ করুক গন্তব্য সরেনি। প্রমাণ করতে না পারলে তুমি রিফ্যাক্টর করছো না — জুয়া খেলছো।

Fowler-এর বই থেকে একটা নামের নোট। এই রিফ্যাক্টরিং তার নাম ধরে রেখেছে: Martin Fowler-এর Refactoring-এর প্রথম (১৯৯৯) আর দ্বিতীয় (২০১৮) উভয় edition-এই এটা Substitute Algorithm, আর refactoring.com-এর catalog-এও। Fowler-এর নিজের উদাহরণ বিখ্যাতভাবে ছোট — একটা function যেটা hard-coded মানুষের list-এর বিপরীতে নাম check করে, পুনরাবৃত্তিমূলক condition check-এর chain থেকে একটা সহজ collection lookup-এ পুনর্লিখিত। সেই ছোট উদাহরণের শিক্ষাটাই এই পুরো কৌশলের বড় শিক্ষা: substitute করার আগে method-টাকে যতটা সম্ভব ছোট করো, যাতে পাঁচটা নয় একটাই algorithm swap করছো।

চিত্র ২: একটা mind map-এ পুরো কৌশল

কখন এটা দরকার হয়?

Substitute Algorithm ব্যবহার করো যখন নিচের এক বা একাধিকটা সত্য:

১. mechanics intent ডুবিয়ে দেয়। তুমি method পড়ো আর loop, flag, counter, break দেখো — কিন্তু এটা আসলে কী করছে সেটা reverse-engineer করতে হয়। যখন "কী" সহজ কিন্তু "কীভাবে" একটা ধাঁধা, তখন সরাসরি "কী" বলা একটা substitution বিশাল লাভ। ২. তুমি সত্যিকারের ভালো রাস্তা পেয়েছো। হয়তো language-এ একটা feature এসেছে (Set, find, pattern matching), হয়তো একটা ভালোভাবে পরীক্ষিত library function পুরো কাজটা করে, হয়তো তুমি কেবল class-এ একটা ভালো algorithm শিখেছো। হাতে-লেখা কোড যেটা standard operation পুনরায় তৈরি করে সেটা substitute হওয়ার অপেক্ষায় আছে। ৩. পুরনো body বার বার bug তৈরি করে। যদি প্রতিটি দ্বিতীয় bug report একই method-এর flag-and-loop bookkeeping-এর দিকে ফিরে যায়, algorithm নিজেই bug factory। এটা replace করা ভবিষ্যতের ত্রুটির পুরো একটা শ্রেণী সরিয়ে দেয়, শুধু আজকেরটা নয়। ৪. requirement algorithm-এর নিচে সরে গেছে। পুরনো পদ্ধতি পুরনো সমস্যার জন্য সঠিক ছিল; সমস্যা পরিবর্তিত হয়েছে, method প্যাচ করা হয়েছে। বর্তমান requirement-এর জন্য ডিজাইন করা নতুন algorithm প্রায়ই প্যাচগুলোর চেয়ে ছোট। ৫. তুমি মাত্র একটা Long Method decompose করলে। Substitution হলো Extract Method বা method object দিয়ে monolith ভাঙার পর স্বাভাবিক সমাপ্তি। প্রতিটি টুকরো এখন এত ছোট যে তার algorithm swap করা খোলা-হার্ট surgery-র পরিবর্তে একটা সংযত, কম-ঝুঁকির পরিবর্তন।

আর সেই পরিস্থিতিগুলো জানো যেখানে উত্তর এখনো নয় বা না:

  • test দুর্বল আর শক্তিশালী করতে পারছো না। তাহলে substitution অযাচাইযোগ্য। আগে test ঠিক করো; এটা এই রিফ্যাক্টরিংয়ের অংশ, optional homework নয়।
  • method অনেক কাজ করে। আগে decompose করো। ২০০-লাইনের multi-purpose method পুরোটা substitute করা মানে কিছু fail হলে একসাথে পাঁচটা swap debug করা।
  • runtime-এ একাধিক algorithm একসাথে থাকতে হবে — যেমন বিভিন্ন customer tier-এর জন্য আলাদা discount নিয়ম। সেটা Strategy pattern-এর কাজ, substitution নয়।
  • "ভালো" algorithm শুধু কাগজে ভালো। দ্রুত কিন্তু বোঝা কঠিন replacement net loss হতে পারে। স্পষ্টতাই লক্ষ্য; গতি হলো বোনাস।
চিত্র ৩: পুরনো আঁকাবাঁকা body কেন পড়তে এত কঠিন

সিদ্ধান্ত আসলে দুটো প্রশ্নের উপর নির্ভর করে — তোমার safety net কতটা শক্তিশালী, আর স্পষ্টতার লাভ কতটা বড়? কিছু স্পর্শ করার আগে তোমার method এই chart-এ রাখো:

চিত্র ৪: swap করবে? test শক্তি বনাম স্পষ্টতার লাভ

দেখো বড় routeMinutes দানব কোথায় বসে — বিশাল স্পষ্টতার লাভ কিন্তু দুর্বল test। মানে হলো প্রথম কাজ characterization test লিখে সেটাকে ডানদিকে সরানো, পুনরায় লেখা নয়।

আগে আর পরে এক নজরে

TypeScript-এ একটা সাধারণ আঁকাবাঁকা দেখো। ধরো স্কুল ভ্যানের app-কে উত্তর দিতে হবে: রুটে রুবেলের stop কোনটা, আর তার pickup time কী? পুরনো body সাধারণভাবে বড় হয়েছে — nested loop, একটা found-flag, আর দুটো break:

interface Stop {
  name: string;
  pickupTime: string;
}
 
// BEFORE: flags, nested loops, breaks — the mechanics drown the intent
function pickupTimeFor(stops: Stop[], studentStops: string[]): string {
  let found: Stop | null = null;
  for (let i = 0; i < stops.length; i++) {
    for (let j = 0; j < studentStops.length; j++) {
      if (stops[i].name === studentStops[j]) {
        found = stops[i];
        break;
      }
    }
    if (found !== null) {
      break;
    }
  }
  if (found === null) {
    return "NOT-ON-ROUTE";
  }
  return found.pickupTime;
}

এই method-টা কীসের জন্য? bookkeeping সরিয়ে দিলে উত্তর একটা বাক্যে: ছাত্রের stop list-এ নাম আছে এমন প্রথম stop-এর pickup time return করো, না থাকলে একটা marker। নতুন রাস্তা ঠিক সেটাই বলে:

// AFTER: the body states the intent directly — and is faster too
function pickupTimeFor(stops: Stop[], studentStops: string[]): string {
  const wanted = new Set(studentStops);
  const stop = stops.find((s) => wanted.has(s.name));
  return stop ? stop.pickupTime : "NOT-ON-ROUTE";
}

তিনটা লাইন। কোনো flag নেই, কোনো index নেই, কোনো break নেই। একজন reader একবার পড়েই intent বুঝতে পারে। বোনাস হিসেবে, Set-এর মাধ্যমে membership check করা ভেতরের loop-এর চেয়ে দ্রুত যখন list বড় হয়। গুরুত্বপূর্ণ ব্যাপার হলো contract অপরিবর্তিত: একই parameter, একই return value, "প্রথম মিলে যাওয়া stop জেতে" নিয়ম আর no-match-এর জন্য "NOT-ON-ROUTE" marker সহ। caller-রা বলতেই পারবে না কিছু পরিবর্তন হয়েছে।

চিত্র ৫: contract থাকে; শুধু body replace হয়

কলেজ কোণ: এখানে গতির বোনাসটার একটা নির্দিষ্ট নাম আছে — algorithmic complexity। পুরনো body প্রতিটি route stop-এর জন্য প্রতিটি student-এর stop check করে: n route stop আর m student stop নিয়ে, সেটা n×m পর্যন্ত তুলনা, লেখা হয় O(n·m)। নতুন body প্রথমে O(m)-তে একটা hash set তৈরি করে, তারপর প্রতিটি route stop গড় O(1) সময়ের lookup দিয়ে check করে, সর্বমোট O(n + m) দেয়। ১০টা stop-ওয়ালা স্কুল ভ্যানে কেউ পার্থক্য অনুভব করবে না। কিন্তু একই আকৃতির কোড কলেজ project-এ দেখা যায় প্রতিটি পাশে দশ হাজার record নিয়ে: O(n·m) হলো এক কোটি তুলনা, O(n + m) হলো বিশ হাজার operation — একটা জমে যাওয়া page আর তাৎক্ষণিক উত্তরের পার্থক্য। দুটো সৎ footnote মনে রাখো: hash lookup গড়ে O(1) (worst case আছে), আর set-এ O(m) অতিরিক্ত memory লাগে — এটা classic time-versus-space trade। Substitute Algorithm প্রায়ই ঠিক এই পদক্ষেপটা: brute-force আকৃতি একটা ভালো data structure-এর জন্য swap করা, test দিয়ে প্রমাণ করে উত্তর কখনো পরিবর্তিত হয়নি।

দিকপুরনো body (nested loop)নতুন body (Set + find)
Time complexityO(n·m) — প্রতিটি জুটি তুলনাO(n + m) — set তৈরি, তারপর একবার scan
অতিরিক্ত memoryO(1)set-এর জন্য O(m)
কোডের লাইন১৮
Mutable bookkeepingflag, দুটো index, দুটো breakকিছুই না
reader-এর প্রচেষ্টামানসিকভাবে loop simulate করাএকটা বাক্য পড়া
n = m = ১০ হলেযেকোনোটাই তুচ্ছযেকোনোটাই তুচ্ছ
n = m = ১০,০০০ হলেপ্রায় ১০,০০,০০,০০০ তুলনাপ্রায় ২০,০০০ operation
চিত্র ৬: একই গন্তব্য, খুব আলাদা ভ্রমণের সময়

নিরাপদ উপায়ে ধাপে ধাপে

Substitute Algorithm হলো beginner-এর toolkit-এ সবচেয়ে বেশি "silent slip" ঝুঁকির রিফ্যাক্টরিং, কারণ পুরনো body-র কিছুই বেঁচে থাকে না। তাই ধাপগুলো প্রায় সব প্রচেষ্টা swap-এর আগে রাখে।

🚨

test আগে আসে — নতুন algorithm-এর একটা লাইন লেখার আগেও। এটা Substitute Algorithm-এর একমাত্র non-negotiable নিয়ম। move-only রিফ্যাক্টরিংয়ে compiler আর IDE বেশিরভাগ slip ধরে ফেলে। এখানে একটা সূক্ষ্ম আচরণগত পার্থক্য — কোন match tie-তে জেতে, খালি input-এ কী হয়, marker string ঠিক "NOT-ON-ROUTE" কিনা — নিখুঁতভাবে compile হয় আর production-এ চুপচাপ fail করে। method-এর বর্তমান test যদি দুর্বল হয়, তোমার প্রথম কাজ হলো characterization test লেখা যেটা তার প্রকৃত আচরণ, edge case সহ, pin করে রাখে। শুধু সেই net শক্তিশালী হলেই তুমি swap করার অধিকার অর্জন করো। test নেই, swap নেই। কখনো না।

চলো van-stop উদাহরণটাকে পুরো discipline-এর মধ্য দিয়ে নিয়ে যাই।

ধাপ ১ — আগে method ছোট করো। Substitution ছোট, একক-উদ্দেশ্যের method-এ সবচেয়ে নিরাপদ। method যদি message-ও format করে বা data-ও save করে, Extract Method apply করো যতক্ষণ না তুমি যে algorithm replace করতে চাও সেটা একা দাঁড়িয়ে থাকে। আমাদের pickupTimeFor ইতিমধ্যে একটাই কাজ করে, তাই এগিয়ে যাই।

ধাপ ২ — safety net তৈরি করো। contract capture করার test লেখো, বিশেষত edge case। আমাদের method-এর জন্য সৎ তালিকাটা এরকম:

// Step 2: characterization tests — written BEFORE any new code
describe("pickupTimeFor", () => {
  const stops = [
    { name: "Milk Booth", pickupTime: "07:10" },
    { name: "Pond Road", pickupTime: "07:18" },
    { name: "Temple Gate", pickupTime: "07:25" },
  ];
 
  it("returns the time of a matching stop", () => {
    expect(pickupTimeFor(stops, ["Pond Road"])).toBe("07:18");
  });
 
  it("returns the FIRST route stop when several match", () => {
    // order rule: route order wins, not the order of studentStops
    expect(pickupTimeFor(stops, ["Temple Gate", "Milk Booth"])).toBe("07:10");
  });
 
  it("returns the marker when nothing matches", () => {
    expect(pickupTimeFor(stops, ["Airport"])).toBe("NOT-ON-ROUTE");
  });
 
  it("handles an empty route", () => {
    expect(pickupTimeFor([], ["Milk Booth"])).toBe("NOT-ON-ROUTE");
  });
 
  it("handles an empty student list", () => {
    expect(pickupTimeFor(stops, [])).toBe("NOT-ON-ROUTE");
  });
});

দ্বিতীয় test-এ একটু থামো। এটা একটা quirk নথিভুক্ত করে: যখন "Temple Gate" আর "Milk Booth" উভয়ই match করে, পুরনো loop সেটা return করে যেটা route-এ প্রথমে আসে, student-এর list-এ নয়। এটা কি ইচ্ছাকৃত? হয়তো, হয়তো না — কিন্তু caller-রা এর উপর নির্ভর করতে পারে, তাই test সেটা pin করে রাখে। এটা পুরো রিফ্যাক্টরিংয়ের সবচেয়ে মূল্যবান মুহূর্ত: swap-এর আগে tie-breaking নিয়মটা আবিষ্কার করা, production bug report-এ নয়।

পুরনো body-র বিপরীতে suite চালাও। সব সবুজ? net প্রস্তুত।

কলেজ কোণ: এই ধরনের testing-এর একটা আনুষ্ঠানিক নাম আছে — characterization testing (Michael Feathers-এর Working Effectively with Legacy Code থেকে), পুরো output একসাথে record করলে golden master testing-ও বলে। দর্শনটা অস্বাভাবিক: test-গুলো assert করে না কোড কী করা উচিত; এগুলো assert করে কোড আসলে কী করে, quirk সহ। এই ধরনের swap-এর জন্য differential testing দিয়ে এক ধাপ শক্তিশালী করতে পারো: পুরনো আর নতুন algorithm শত শত random input-এ পাশাপাশি চালাও আর সবসময় একমত হচ্ছে কিনা assert করো। Property-based testing library (TypeScript-এর জন্য fast-check, Python-এর জন্য Hypothesis, .NET-এর জন্য FsCheck) ঠিক এটা automate করে: তুমি "old(x) equals new(x) for all x" property বলো, আর library একটা counterexample খোঁজে। দশ মিনিটের property-based তুলনা tie-breaking আর empty-input পার্থক্য ধরে যা হাতে-লেখা case মিস করে।

ধাপ ৩ — পুরনোটার পাশে নতুন algorithm লেখো। এখনো কিছু delete করো না। replacement একটা আলাদা function হিসেবে যোগ করো যাতে উভয় রাস্তা একসাথে বিদ্যমান থাকে:

// Step 3: INTERMEDIATE — both algorithms exist side by side
function pickupTimeForNew(stops: Stop[], studentStops: string[]): string {
  const wanted = new Set(studentStops);
  const stop = stops.find((s) => wanted.has(s.name));
  return stop ? stop.pickupTime : "NOT-ON-ROUTE";
}

ঝুঁকিপূর্ণ swap-এ অতিরিক্ত নিশ্চয়তার জন্য, পুরো test suite pickupTimeForNew-এর দিকে নির্দেশ করো আর চালাও — অথবা একটা ছোট comparison loop লেখো যেটা অনেক random input উভয় function-এ feed করে আর উত্তর match করে কিনা assert করে। এই পাশাপাশি মুহূর্তটাই রুবেলের রবিবারের test ride: পুরনো রুট এখনো আছে, তাই কিছু বাজিতে নেই।

চিত্র ৭: কোডে রবিবারের test ride — একই suite দ্বারা উভয় রাস্তা চেক করা

ধাপ ৪ — এক পরিষ্কার stroke-এ swap করো। পুরনো body নতুন দিয়ে replace করো, অস্থায়ী function delete করো, আর method-এর signature অপরিবর্তিত রাখো:

// Step 4: the swap — contract identical, body replaced
function pickupTimeFor(stops: Stop[], studentStops: string[]): string {
  const wanted = new Set(studentStops);
  const stop = stops.find((s) => wanted.has(s.name));
  return stop ? stop.pickupTime : "NOT-ON-ROUTE";
}

ধাপ ৫ — পুরো suite চালাও। এখানে প্রতিটি failure হলো রাস্তাগুলোর মধ্যে একটা আচরণগত পার্থক্য। প্রতিটির জন্য মূল প্রশ্ন জিজ্ঞেস করো: পুরনো আচরণ কি contract ছিল নাকি দুর্ঘটনা? caller যদি নির্ভর করে, নতুন algorithm ঠিক করো। দুর্ঘটনা হলে নতুন আচরণ রাখো — কিন্তু test সচেতনভাবে আর দৃশ্যমানভাবে পরিবর্তন করো, আদর্শভাবে নিজের commit-এ।

ধাপ ৬ — পুরনো রাস্তা চিরতরে delete করো। একবার সবুজ হলে, পুরনো কোডের যেকোনো অবশিষ্টাংশ, comment-out করা block, বা "just in case" copy সরিয়ে দাও। version control পুরনো algorithm চিরতরে মনে রাখে; তোমার source file-এর উচিত নয়। রুবেল "backup" হিসেবে পুকুরের loop চালাতে থাকে না।

চিত্র ৮: প্রায় সব কাজ swap-এর আগে হয়

method এই যাত্রায় পাঁচটা নামযুক্ত অবস্থার মধ্য দিয়ে যায়। মাঝখানে বাধাগ্রস্ত হলে, বর্তমান অবস্থা জানলে ঠিক কী করা নিরাপদ তা বলে দেয়:

চিত্র ৯: substitution চলাকালীন method-এর পাঁচটা অবস্থা

বড় একটা বাস্তব উদাহরণ

ধরো রুবেলের পুরো রুটটা কোডে রাখি। কলোনির cycling app রাস্তাগুলো segment হিসেবে রাখে, আর পুরনো function বাড়ি থেকে স্কুল পর্যন্ত route গণনা করে যেভাবে route নিজেই বড় হয়েছিল — প্যাচের পর প্যাচ, যুগের পর যুগ:

interface Segment {
  from: string;
  to: string;
  minutes: number;
  openFrom: number; // hour of day the road opens
}
 
// BEFORE: the algorithm IS the zig-zag — every era of patches still visible
function routeMinutes(segments: Segment[], leaveHour: number): number {
  let total = 0;
  let reachedMilkBooth = false;
  let pondLoopDone = false;
  let templeDetourNeeded = false;
 
  for (let i = 0; i < segments.length; i++) {
    const seg = segments[i];
    if (seg.from === "Home" && seg.to === "MilkBooth") {
      if (seg.openFrom <= leaveHour) {
        total += seg.minutes;
        reachedMilkBooth = true;
      }
    }
    if (reachedMilkBooth && !pondLoopDone) {
      if (seg.from === "MilkBooth" && seg.to === "PondRoad") {
        total += seg.minutes;
        pondLoopDone = true;
        if (seg.minutes > 8) {
          templeDetourNeeded = true; // historical: long pond loop => dog corner => temple
        }
      }
    }
    if (pondLoopDone && templeDetourNeeded) {
      if (seg.from === "PondRoad" && seg.to === "TempleGate") {
        total += seg.minutes;
        templeDetourNeeded = false;
      }
    }
    if (pondLoopDone && !templeDetourNeeded && seg.from === "PondRoad" && seg.to === "School") {
      total += seg.minutes;
      break;
    }
    if (seg.from === "TempleGate" && seg.to === "School") {
      total += seg.minutes;
      break;
    }
  }
  return total;
}

চোখ ঘোলা লাগলে দোষ দিও না — সেটাই পয়েন্ট। তিনটা boolean flag ইতিহাস encode করে, logic নয়: "পুকুরের loop বিয়ের হলের কারণে," "মসজিদের detour কুকুরের কারণে।" তিন বছরে প্রতিটি bug fix এই loop-এ আরেকটা if ঢুকিয়েছে। লাইন-বাই-লাইন উন্নত করা আশাহীন, কারণ এর আকৃতি-ই হলো আঁকাবাঁকা।

এখন আমাদের codebase-এর পৌরসভা সোজা রাস্তা খুলে দিল। আজকের সত্যিকারের requirement সহজ: road segment-এর list দেওয়া হলে, Home থেকে School পর্যন্ত chain follow করো, রুবেল বের হওয়ার সময় খোলা প্রতিটি segment-এর minute যোগ করো। সরাসরি বললে:

// AFTER: the algorithm matches today's requirement, not history
function routeMinutes(segments: Segment[], leaveHour: number): number {
  const bySource = new Map(segments.map((s) => [s.from, s]));
  let total = 0;
  let here = "Home";
 
  while (here !== "School") {
    const seg = bySource.get(here);
    if (!seg || seg.openFrom > leaveHour) {
      return 0; // no usable road onward — contract: unreachable means 0
    }
    total += seg.minutes;
    here = seg.to;
  }
  return total;
}

একটা map, chain বরাবর একটা হাঁটা, বন্ধ রাস্তার জন্য একটা পরিষ্কার নিয়ম। flag চলে গেছে কারণ ইতিহাস চলে গেছে — function এখন route-কে data (segments) আর একটা সহজ traversal হিসেবে প্রকাশ করে। আগামীকাল একটা নতুন stop যোগ করা মানে data-তে একটা segment যোগ করা, কোডে চতুর্থ boolean নয়।

আর discipline এখনো প্রযোজ্য, আরও শক্তিশালীভাবে, কারণ এই swap-টা প্রথমটার চেয়ে বড়। সোয়াপ করার আগে, characterization test-গুলোকে অবশ্যই পুরনো function-এর quirk pin করতে হবে: রুবেল milk-booth রাস্তা খোলার আগে বের হলে কী return করে? segment-গুলো অদ্ভুত order-এ এলে কী হয়? একটু মনোযোগ দিয়ে দেখো: পুরনো version চুপচাপ segment order-এর উপর নির্ভর করত; নতুনটা করে না — এটা একটা আচরণগত পার্থক্য যা test-কে সামনে আনতে হবে।

Python-এ একই রিফ্যাক্টরিং

একটা ছোট grading helper-এ Python-এ একই swap। পুরনো body topper নাম দীর্ঘ পথে check করে:

# BEFORE: repetitive condition-chain — a tiny zig-zag
def is_topper(name, toppers_csv):
    found = False
    parts = toppers_csv.split(",")
    for i in range(len(parts)):
        cleaned = parts[i].strip()
        if cleaned != "":
            if cleaned.lower() == name.strip().lower():
                found = True
                break
    if found:
        return True
    return False

contract হলো: comma-separated topper list-এর বিপরীতে নামের case-insensitive match, অতিরিক্ত space আর খালি entry উপেক্ষা করে। আগে test — খালি string, trailing comma, mixed case, space সহ নাম — তারপর সোজা রাস্তা:

# AFTER: the body states the contract directly
def is_topper(name, toppers_csv):
    toppers = {p.strip().lower() for p in toppers_csv.split(",") if p.strip()}
    return name.strip().lower() in toppers

দুটো লাইন, আর প্রতিটি clause contract-এর একটা বাক্যের সাথে মেলে: পরিষ্কার set তৈরি করো, membership check করো। Python-এর comprehension আর in operator এটাকে এই রিফ্যাক্টরিংয়ের জন্য চমৎকার language করে তোলে — প্রায়ই "নতুন algorithm" হলো শুধু standard library যা পুরনো loop হাতে-তৈরি করেছিল। C#-এর LINQ (Any, FirstOrDefault, Where) আর JavaScript-এর array method-এর ক্ষেত্রেও একই কথা: বাস্তব substitution-এর বড় অংশ manual loop-কে একটা ভালোভাবে পরীক্ষিত built-in দিয়ে replace করে।

সম্পূর্ণতার জন্য, C#-এ একই idiom — পুরনো foreach-with-flag body একটা LINQ বাক্যে সংকুচিত হয়:

// BEFORE: flag-and-loop
public bool IsTopper(string name, string toppersCsv)
{
    bool found = false;
    foreach (var part in toppersCsv.Split(','))
    {
        var cleaned = part.Trim();
        if (cleaned.Length > 0 && cleaned.Equals(name.Trim(), StringComparison.OrdinalIgnoreCase))
        {
            found = true;
            break;
        }
    }
    return found;
}
 
// AFTER: the library is the straight road
public bool IsTopper(string name, string toppersCsv)
    => toppersCsv.Split(',')
        .Select(p => p.Trim())
        .Where(p => p.Length > 0)
        .Any(p => p.Equals(name.Trim(), StringComparison.OrdinalIgnoreCase));

IDE সাপোর্ট

কোনো IDE-তে "Substitute Algorithm" button নেই — কোনো tool তোমার জন্য ভালো algorithm আবিষ্কার করতে পারে না। কিন্তু swap-এর আশেপাশের tooling ঠিক যেখানে IDE দক্ষতা দেখায়, আর তুমি সব কিছুর উপর নির্ভর করা উচিত:

  • One-key re-run সহ test runner (Visual Studio Test Explorer, IntelliJ/Rider-এর runner, VS Code-এর testing panel, pytest/jest watch modes): পুরো discipline প্রতিটি পদক্ষেপের পর suite চালানোর উপর নির্ভর করে। watch mode "test চালাও" কে একটা কাজ থেকে একটা heartbeat-এ পরিণত করে।
  • Coverage tools (dotnet-coverage আর Visual Studio-এর coverage view, IntelliJ-এর built-in coverage, coverage.py, Istanbul/nyc): swap-এর আগে পুরনো body-তে তোমার characterization test-এ coverage চালাও। unhit branch-গুলো ঠিক সেই quirk যা net এখনো pin করেনি।
  • Extract Method সাপোর্ট (সব major IDE): official ধাপ ১। IDE-এর extraction ব্যবহার করে method ছোট করো যতক্ষণ না algorithm একা দাঁড়িয়ে থাকে।
  • Version control integration: swap-টাকে নিজের ছোট commit করো, test-গুলো আগে commit করা সহ। পরে কিছু সামনে এলে, git diff পুরনো আর নতুন algorithm পাশাপাশি দেখায়, আর reverting একটা command। IDE-এর diff viewer-ও সহকর্মীর জন্য swap review করার সেরা জায়গা।
  • Inline hints আর analyzer: ReSharper, Roslyn analyzer, SonarLint, আর ESLint প্রায়ই substitution suggest করে — "convert loop to LINQ expression," "use Array.prototype.find," "simplify conditional।" এই hint-গুলো ছোট, machine-verified Substitute Algorithm পদক্ষেপ।

সুবিধা আর ঝুঁকি

সুবিধাঝুঁকি আর সীমাবদ্ধতা
জটিল bookkeeping — flag, index, manual break — সরাসরি intent বলে এমন কোড দিয়ে replace হয়মৌলিক রিফ্যাক্টরিংগুলোর মধ্যে সর্বোচ্চ silent-failure ঝুঁকি: পুরনো body-র কিছুই বেঁচে থাকে না, তাই আগে লেখা শক্তিশালী test ছাড়া সূক্ষ্ম পার্থক্য অদৃশ্যে ship হয়
প্রায়ই দ্রুত: ভালো data structure (set, map) আর library call হাতে-তৈরি loop-কে হারায়দুটো algorithm-কে অবশ্যই পুরো contract-এ একমত হতে হবে — ordering, tie-breaking, empty input, error marker। caller যে hidden quirk-এর উপর নির্ভর করে সেটা ভাঙতে পারে
পুরনো algorithm-এর bookkeeping-এর সাথে bug-এর পুরো একটা শ্রেণী মারা যায়"চালাক" replacement যা কেউ বোঝে না একটা readability সমস্যাকে আরও খারাপ একটার সাথে বদলায় — স্পষ্টতা লক্ষ্য, গতি বোনাস
হাতে-তৈরি কোড একটা ভালোভাবে পরীক্ষিত standard-library বা framework function-এর জায়গায় delete করতে দেয়বড় multi-purpose method পুরোটা swap করা open-heart surgery — আগে decompose করো, তারপর আলাদাভাবে ছোট টুকরো swap করো
ভবিষ্যত পরিবর্তন সহজ হয়, কারণ নতুন body গতকালের প্যাচের পরিবর্তে আজকের requirement-এর সাথে মেলেruntime-এ একাধিক algorithm থাকতে হলে substitution ভুল tool — Strategy pattern ব্যবহার করো

প্রথম ঝুঁকির সারি দুবার পড়ো। এই সিরিজের অন্য প্রতিটি রিফ্যাক্টরিং তোমাকে আংশিকভাবে compiler-এর উপর নির্ভর করতে দেয়; এটা প্রায় সম্পূর্ণরূপে test-এর উপর নির্ভর করে। swap-এর আগে test রাখো — সবসময়। এই একটা অভ্যাস Substitute Algorithm-কে সবচেয়ে ঝুঁকিপূর্ণ মৌলিক রিফ্যাক্টরিং থেকে একটা শান্ত, নিয়মিত রিফ্যাক্টরিংয়ে রূপান্তরিত করে।

শেষ সারিটা — runtime algorithm choice — একটা ছবি প্রাপ্য, কারণ নীচের অনুশীলন exercise তোমাকে এটা নিয়ে জিজ্ঞেস করবে। যখন দুটো রাস্তা উভয়ই খোলা থাকতে হবে (শিক্ষকদের জন্য strict matching, ছাত্রদের জন্য fuzzy matching), তুমি একটার জন্য অন্যটা substitute করো না; তুমি প্রতিটিকে একটা common interface-এর পেছনে রাখো আর caller-কে বেছে নিতে দাও। এটা Strategy pattern, আর আগের পাঠের method-object রিফ্যাক্টরিং প্রায়ই এর দিকে প্রথম পদক্ষেপ:

চিত্র ১০: যখন উভয় রাস্তা খোলা থাকতে হবে, Strategy — substitution নয়

কোন smell-গুলো এটা সারায়?

Smellএই রিফ্যাক্টরিং কীভাবে সাহায্য করে
Long Methoddecomposition monster ছোট করার পর, substitution কাজ শেষ করে: প্রতিটি ছোট টুকরোর জটিল body একটা সরাসরি body-র সাথে swap হয়, প্রায়ই দশটা লাইন দুটোতে সংকুচিত করে
Duplicate Codeteam প্রায়ই পাঁচটা সামান্য আলাদা উপায়ে একই operation হাতে-তৈরি করে। একটা পরিষ্কার, নামযুক্ত algorithm — প্রায়ই একটা library call — সব near-duplicate replace করে
Commentsএকটা body যেটা loop আর flag কীভাবে কাজ করে ব্যাখ্যা করার জন্য একটা অনুচ্ছেদ দরকার সেটার দরকার থাকে না যখন নতুন body সরাসরি বলে সে কী করে
Dead Codeপুরনো algorithm branch জমা করে এমন পরিস্থিতির জন্য যা আর ঘটে না। replacement, আজকের contract-এর জন্য লেখা, সেগুলো ঝরিয়ে দেয় — আর ধাপ ৬ পুরনো কোড সম্পূর্ণ delete করে

দ্রুত revision বক্স

+--------------------------------------------------------------------+
|            SUBSTITUTE ALGORITHM — REVISION CARD                    |
+--------------------------------------------------------------------+
| Story    : Ravi's zig-zag cycle route vs the new straight road —   |
|            same destination, simpler path. Test-ride on Sunday     |
|            BEFORE switching on a school day.                       |
| What     : keep the method's contract; replace its WHOLE body      |
|            with a clearer/faster algorithm. Move-nothing,          |
|            rewrite-everything.                                     |
| Rule #1  : TESTS FIRST. Characterize the old behavior — edges,     |
|            ordering, tie-breaks, empty inputs — before writing     |
|            one line of the new algorithm.                          |
| Steps    : shrink method -> pin behavior with tests -> write new   |
|            algorithm beside old -> swap in one stroke -> full      |
|            suite -> investigate every diff -> delete old road.     |
| Failures : each red test = old behavior. Decide: contract          |
|            (reproduce it) or accident (change it openly).          |
| Avoid    : weak tests, multi-job methods (decompose first),        |
|            clever-but-unreadable code, runtime algorithm choice    |
|            (that is Strategy).                                     |
+--------------------------------------------------------------------+

অনুশীলন exercise

এবার তোমার পালা একটা সোজা রাস্তা খোলার। একটা স্কুল library app থেকে এই TypeScript function অনুরোধকৃত বইয়ের প্রথম পাওয়া copy খোঁজে — লেখা হয়েছে, অবশ্যই, একটা আঁকাবাঁকা হিসেবে:

interface BookCopy {
  title: string;
  shelf: string;
  isIssued: boolean;
}
 
function findShelf(copies: BookCopy[], requestedTitles: string[]): string {
  let answer = "";
  let done = false;
  for (let i = 0; i < copies.length; i++) {
    if (!done) {
      for (let j = 0; j < requestedTitles.length; j++) {
        if (copies[i].title === requestedTitles[j]) {
          if (copies[i].isIssued === false) {
            answer = copies[i].shelf;
            done = true;
            break;
          }
        }
      }
    }
  }
  if (done === false) {
    return "NOT-AVAILABLE";
  }
  return answer;
}

নিরাপদ ক্রমে তোমার কাজগুলো:

১. contract একটা বাক্যে বলো। কিছু স্পর্শ করার আগে function-এর উপরে comment হিসেবে লেখো। (ইঙ্গিত: একাধিক পাওয়া গেলে কোন copy জেতে? title match করলে কিন্তু প্রতিটি copy issued হলে কী হয়?) ২. আগে net তৈরি করো। পুরনো body-র বিপরীতে কমপক্ষে ছয়টা characterization test লেখো: একটা সহজ hit, একটা no-match case, একটা all-copies-issued case, একটা খালি copies array, একটা খালি requestedTitles array, আর — সবচেয়ে গুরুত্বপূর্ণ — একাধিক পাওয়া copy সহ একটা case যেটা pin করে কোনটা জেতে। সবুজ চালাও। ৩. পুরনোটার পাশে সোজা রাস্তা লেখো findShelfNew হিসেবে, lesson উদাহরণের মতো Set আর find ব্যবহার করে। একই test সেটা দিয়ে চালাও। ৪. এক stroke-এ swap করো, অস্থায়ী function delete করো, আর পুরো suite চালাও। কোনো test fail করলে তোমার নোটবুকে একটা বাক্য লেখো: পুরনো আচরণ কি contract ছিল নাকি দুর্ঘটনা, আর তুমি কী সিদ্ধান্ত নিলে? ৫. পুরনো কোড সম্পূর্ণ delete করো — comment-out করা backup নেই। তারপর diff check করো: পুরো পরিবর্তন একটা method body আর তোমার test হওয়া উচিত। ৬. কলেজ কোণ challenge: পুরনো body আর নতুনটার time complexity Big-O পদে বলো, copies-এর জন্য n আর requested title-এর জন্য m ব্যবহার করে। তারপর property-based library (বা random input-এর উপর একটা plain loop) দিয়ে একটা দশ-লাইনের differential test লেখো যা assert করে পুরনো আর নতুন সবসময় একমত — আর শুধু তখনই পুরনো body delete করো যখন এটা এক হাজার case পাস করে। ৭. বোনাস thinking question: ধরো librarian পরে দুটো search নিয়ম চান — শিক্ষকদের জন্য strict title match, ছাত্রদের জন্য fuzzy match — runtime-এ নির্বাচনযোগ্য। Substitute Algorithm কেন সেই request-এর জন্য ভুল tool, আর কোন design pattern সঠিক? (চিত্র ১০ তোমার ইঙ্গিত।)

একই গন্তব্য, সহজ পথ — আর প্রতিটি switch-এর আগে রবিবারের test ride। নিরাপদে চালাও।

সচরাচর জিজ্ঞাসা

Substitute Algorithm অন্য রিফ্যাক্টরিং থেকে কীভাবে আলাদা?
বেশিরভাগ রিফ্যাক্টরিং কোড সরায় — method বের করো, variable-এর নাম বদলাও, local-কে field-এ নাও। Substitute Algorithm একদম ভেতরের জিনিস বদলে দেয়: method-এর পুরো body ফেলে দাও আর একটা পরিষ্কার বা দ্রুত body লেখো যেটা একই result দেয়। contract থাকে, ভেতরটা সম্পূর্ণ বদলে যায়।
এই রিফ্যাক্টরিংয়ে টেস্ট কেন আগে লিখতে হবে?
কারণ তুমি পুরনো body পুরোটাই মুছে ফেলছো, তাই test-ই একমাত্র জিনিস যেটা behavior আটকে রাখে। ছোট move-only রিফ্যাক্টরিংয়ে compiler অনেক ভুল ধরে ফেলে; এখানে একটা সূক্ষ্ম পার্থক্য — ordering, খালি input, tie-breaking — চুপচাপ বেরিয়ে যেতে পারে। swap করার আগে test শক্তিশালী করো, পরে নয়।
পুরনো algorithm-এর অদ্ভুত behavior থাকলে কী করবো যেটার উপর caller নির্ভর করে?
অদ্ভুত behavior-কে contract-এর অংশ ধরো যতক্ষণ না প্রমাণ হয়। একটা test লেখো যেটা সেটা ধরে রাখে, তারপর দেখো এটা ইচ্ছাকৃত নাকি দুর্ঘটনা। caller যদি নির্ভর করে, নতুন algorithm-কে সেটা reproduce করতে হবে। কেউ নির্ভর না করলে সচেতনভাবে আলাদাভাবে বদলাও — swap-এর ভেতরে চুপচাপ নয়।
নতুন algorithm কি যতটা সম্ভব দ্রুত বানানো উচিত?
না। সাধারণত লক্ষ্য হলো পরিষ্কার কোড; গতি একটা বোনাস। চালাক কিন্তু বোঝা কঠিন এমন replacement এক সমস্যার জায়গায় আরেকটা আনে। এমন version বেছে নাও যেটা একজন teammate এক পড়াতেই বুঝবে, আর পারফরম্যান্স অপটিমাইজ করো তখনই যখন measurement দেখায় সত্যিকারের দরকার আছে।
কখন algorithm substitute করা উচিত নয়?
যখন method বড় আর অনেক কাজ করে — আগে Extract Method দিয়ে ভেঙে নাও যাতে প্রতিটি ছোট অংশ আলাদাভাবে swap করা যায়। এটা এড়াও যখন test দুর্বল আর শক্তিশালী করতে পারছো না, আর যখন runtime-এ একাধিক algorithm একসাথে থাকতে হবে — সেটার জন্য Strategy pattern দরকার, swap নয়।

আরো দেখো

সম্পর্কিত পাঠ

Extract Method: একটা বিশাল ফাংশনকে ছোট ছোট নামওয়ালা helper-এ ভাগ করো

Extract Method ধাপে ধাপে শিখে নাও। একটা লম্বা ফাংশন থেকে এলোমেলো block বের করে তাকে একটা পরিষ্কার নাম দাও, আর তোমার কোডকে একটা সহজ to-do লিস্টের মতো পড়ার যোগ্য করে তোলো।

আরও পড়ুন

Replace Method with Method Object: বড় রান্নার জন্য আলাদা স্টেশন বানাও

Replace Method with Method Object শেখো বিয়ের রান্নাঘরের গল্প দিয়ে — TypeScript ও C# উদাহরণ আর নিরাপদ ধাপ-ধাপ পদ্ধতি দিয়ে, একদম শুরু থেকে।

আরও পড়ুন

Remove Assignments to Parameters: ধার করা খাতায় কখনো লিখবে না

Remove Assignments to Parameters refactoring শিখো একটা ধার করা খাতার গল্পের মাধ্যমে — TypeScript আর C# উদাহরণ সহ, সহজ ধাপে ধাপে।

আরও পড়ুন

Replace Temp with Query: তাজা জিজ্ঞেস করো, বাসি চিরকুটে ভরসা করো না

ক্যান্টিনের সিঙ্গারার গল্প দিয়ে Replace Temp with Query বোঝো — TypeScript আর C# উদাহরণ, নিরাপদ ধাপ, আর একটাই সত্যের উৎস।

আরও পড়ুন