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

Guard Clause দিয়ে Nested Conditional সরাও: Arrow সমান করো

Guard clause কী, কীভাবে nested if-else-এর arrow shape ভেঙে code সমান করা যায় — মসজিদের গেটের গল্পের মাধ্যমে TypeScript আর C#-এ step-by-step শেখো।

21 মিনিট আপডেট: June 11, 2026beginner
refactoringguard clausesearly returnnested conditionalsconditionalstypescriptcsharp

চার গেটওয়ালা মসজিদের লাইন

ঈদের নামাজের দিন সকাল। ঢাকার একটা বড় মসজিদে হাজার হাজার মানুষ। রাহিম সাহেব এসেছেন তার ছেলে তারিককে নিয়ে। প্রতি বছর আসেন, লাইনের ব্যাপারে তার অনেক মতামত আছে।

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

গেট ১-এ আছে রুবেল। হাসিখুশি তরুণ। "ভাই, token আছে?" token নেই? রুবেল ভদ্রভাবে exit lane দেখিয়ে দেয়। তুমি বেরিয়ে যাও। পেছনে কেউ দেরি করে না।

গেট ২: "আজকের জন্য নামাজের কাতার নিশ্চিত করা আছে?" ভুল দিন? বেরিয়ে যাও, হাসিমুখে।

গেট ৩: "জুতা রাখা হয়েছে?" এখনো জুতা পায়ে? যাও, জুতা রেখে এসো।

গেট ৪: "মোবাইল silent করা?" রিং বাজছে? বেরিয়ে যাও, silent করে আসো।

রাহিম সাহেব আর তারিক দু মিনিটে চারটা গেট পার করে সরাসরি ভেতরে ঢুকে গেল। পথটা পরিষ্কার, সোজা। ভেতরে যে আছে সে নিশ্চিতভাবে সব check পার করেছে — কোনো সন্দেহ নেই। রাহিম সাহেব মাথা নাড়লেন। "এবার system ভালো করেছে।"

কারণ আগেরটা তিনি মনে করতে পারেন। একটাই বিশাল গেট, একদম শেষে। এক ক্লান্ত লোক token দেখে, দিন দেখে, জুতা দেখে, মোবাইল দেখে — সব একসাথে এক লম্বা জিজ্ঞাসাবাদে: "IF token থাকলে, THEN আজকের হলে, THEN জুতা রাখা থাকলে, THEN মোবাইল silent থাকলে, THEN ঢুকতে পারবে, ELSE... else... else... else..." প্রতিটা "else" আলাদা ধরনের মানুষকে ভিড়ের মধ্যে দিয়ে ফেরত পাঠাত। জিজ্ঞাসাবাদটা confusing ছিল, লাইন slow ছিল, আর কেউ বুঝত না কোন নিয়মে fail করেছে।

Code-এও ঠিক এই দুটো design আছে। Deeply nested if/else হলো সেই একটা বিশাল গেট। Guard clause হলো রুবেল আর তার তিন সাথী — একটা নিয়ম check করো, fail করলে সাথেসাথে বেরিয়ে যাও, আর যে pass করেছে সে সোজা এগিয়ে যাক। আজকের refactoring — Replace Nested Conditional with Guard Clauses — প্রথম design-কে দ্বিতীয়তে রূপান্তরিত করে।

চিত্র ১: রাহিম সাহেবের চার গেটের লাইন — প্রতিটা গেট দ্রুত, আর ভেতরটা শান্তিময়

Replace Nested Conditional with Guard Clauses আসলে কী?

Nested conditional হলো যখন function-এর আসল কাজটা if-এর ভেতরে if-এর ভেতরে if-এ আটকে থাকে। প্রতিটা layer একটু একটু করে indentation বাড়ায়। Code দেখতে হয়ে যায় arrow anti-pattern — indentation একটা > চিহ্নের মতো ডানদিকে বাড়তে থাকে, আর আসল কাজের line-টা সেই arrow-এর একদম ডগায় লুকিয়ে থাকে।

এই refactoring প্রতিটা wrapping condition-কে guard clause-এ রূপান্তরিত করে। Guard clause হলো function-এর উপরে একটা ছোট check — সে একটা special case handle করে আর সাথেসাথে return বা throw দিয়ে বেরিয়ে যায়। এক নিঃশ্বাসে recipe:

  1. সবচেয়ে বাইরের if নাও।
  2. তার condition উল্টে দাও ("pass করবে" check টা "বেরিয়ে যাবে" check-এ পরিণত হয়)।
  3. else branch-টাকে immediate early exit দিয়ে replace করো।
  4. ভেতরের সব কিছু un-indent করো, আর পরের layer-এর জন্য একই কাজ করো।

সব layer সরিয়ে ফেললে special case-গুলো উপরে flat list-এ দাঁড়িয়ে থাকে — যেমন চারটা গেট একটার পর একটা — আর main logic নিচে থাকে, কোনো extra indentation ছাড়াই। Arrow সমান হয়ে গেল।

Martin Fowler তার Refactoring বইয়ে এই refactoring নিয়ে একটা তীক্ষ্ণ কথা বলেছেন। Symmetric if/else reader-কে বলে: "দুটো path-ই সমান স্বাভাবিক।" Guard clause বলে: "এই case টা বিরল বা ভুল — deal করো আর বেরিয়ে যাও।" বেশিরভাগ function-এ একটাই স্বাভাবিক path থাকে, আর কয়েকটা উপায়ে সেই path থেকে ছিটকে পড়ার সুযোগ থাকে। Guard clause এই বাস্তবতার সাথে মেলে, nesting সেটা লুকিয়ে রাখে।

একটু ভাবো রুবেলের কথা। তার job description এক লাইনে: token check করো, token নেই এমন মানুষকে ভদ্রভাবে বের করে দাও। সে জুতার কথা ভাবে না। পুরনো single gate-এর লোকটার job description কেউ মুখে বলতে পারত না। প্রতিটা guard clause একটা রুবেল — একটা নিয়ম, একটা exit, পুরো পরিষ্কার।

💡

এক লাইনে সারকথা: Replace Nested Conditional with Guard Clauses প্রতিটা wrapping condition-কে function-এর উপরে early-exit check-এ রূপান্তরিত করে, যাতে special case-গুলো দ্রুত বেরিয়ে যায় আর happy path নিচে flat, unindented, আর স্পষ্টভাবে দাঁড়িয়ে থাকে।

একটু deeper জানতে চাইলে: "এই nesting ভারী লাগছে" — এই অনুভূতির পেছনে formal metric হলো cyclomatic complexity, যেটা Thomas McCabe ১৯৭৬ সালে introduce করেছিলেন। এটা function-এর independent path গুনে — মোটামুটি decision-এর সংখ্যা + ১। একটা honest surprise: guard clause সাধারণত cyclomatic complexity কমায় না, কারণ একই decision গুলো আছে, শুধু সাজানো বদলেছে। যেটা কমে সেটা হলো nesting depth — আর SonarQube-এর cognitive complexity metric সেটাই measure করে। প্রতিটা nesting level-এর জন্য cognitive complexity extra penalty যোগ করে, কারণ মানুষকে সব enclosing condition মাথায় রাখতে হয়। Guard clause সেই memory stack শূন্য করে দেয়। তাই কেউ জিজ্ঞেস করলে — early return কি McCabe-এর number কমায়? সঠিক উত্তর: না, এটা কমায় মানুষের cost, যেটা cognitive complexity capture করে, cyclomatic complexity করে না।

কখন এটা দরকার?

এই signs দেখলে বুঝবে:

  • Arrow shape। Indentation চার-পাঁচ level ডানে চলে গেছে। আসল কাজটা পড়তে horizontally scroll করতে হচ্ছে। নিচে closing brace গুলো দেখতে সিঁড়ির মতো।
  • Happy path খুঁজে পাচ্ছ না। গুরুত্বপূর্ণ line কখন run করে জানতে সব surrounding condition মাথায় রাখতে হচ্ছে: "এটা run করবে যদি active AND not retired AND bank details valid AND..." এই stack পুরো memory load।
  • else branch শুধু error বা absence handle করছে। বেশিরভাগ else block যদি শুধু default set করে, zero return করে, বা complaint log করে — তাহলে এগুলো equal alternative না, এগুলো exit যেগুলো path হওয়ার ভান করছে।
  • একটা result variable অনেক branch-এ assign হয়ে একদম শেষে return হচ্ছে। এই assign-করো-পরে-return-করো dance শুধু single-exit shape সার্ভ করার জন্য আছে। Guard clause সেটা মুছে দেয়।
  • একটা নতুন condition যোগ করতে গেলে আরেকটা indent level যোগ করতে হয়। নতুন নিয়ম যোগ করলে nesting বাড়ে। Guard clause-এ নতুন নিয়ম মানে উপরে একটা নতুন flat line — রুবেলের পাশে শুধু আরেকজনকে দাঁড় করিয়ে দাও।

আর counter-signs, এগুলোও সমান গুরুত্বপূর্ণ:

  • দুটো branch যদি সত্যিই সমান, everyday outcome হয় — online payment বনাম cash payment — তাহলে symmetric if/else রাখো। একটাকে "abnormal" guard বানালে domain সম্পর্কে ছোট্ট মিথ্যা বলা হবে।
  • Guard-এর condition নিজেই লম্বা আর জটিল হলে আগে Decompose Conditional দিয়ে নাম দাও, যাতে প্রতিটা gate একটা বাক্যের মতো পড়া যায়।
  • Function-কে বেরোনোর আগে cleanup করতে হলে (file বন্ধ করা, lock ছাড়া) bare early return সেটা skip করতে পারে। try/finally, using, বা language-এর equivalent ব্যবহার করো যাতে প্রতিটা exit-এ cleanup হয়।

একটা ছোট decision table হাতের কাছে রাখো:

Branch সম্পর্কে প্রশ্নউত্তরকোন shape নেবে
এই case কি বিরল, ভুল, বা অনুপস্থিতি?হ্যাঁGuard clause — exit early
দুটো outcome কি everyday আর সমান?হ্যাঁSymmetric if/else
এই case কি সঠিক code-এ impossible হওয়া উচিত?হ্যাঁAssertion, guard না
Exit-এর আগে cleanup দরকার?হ্যাঁGuard + try/finally বা using
Condition নিজেই জটিল?হ্যাঁআগে Decompose Conditional দিয়ে নাম দাও

Before আর After এক নজরে

ধরো একটা payout calculator — classic arrow shape। দেখো indentation কীভাবে বাড়ছে:

// BEFORE: the arrow anti-pattern — real work buried at the tip
function payout(employee: Employee): number {
  let result: number;
  if (employee.isActive) {
    if (!employee.isRetired) {
      if (employee.hasValidBankDetails) {
        // the ONLY line that matters, four levels deep
        result = computeSalary(employee) + computeBonus(employee);
      } else {
        result = 0;
      }
    } else {
      result = pensionAmount(employee);
    }
  } else {
    result = 0;
  }
  return result;
}

আর এরপর — চারটা flat gate, তারপর main hall:

// AFTER: guard clauses — special cases exit at the door
function payout(employee: Employee): number {
  if (!employee.isActive) return 0;
  if (employee.isRetired) return pensionAmount(employee);
  if (!employee.hasValidBankDetails) return 0;
 
  return computeSalary(employee) + computeBonus(employee);
}

গুনে দেখো কী পেলাম। চোদ্দ line হয়ে গেল পাঁচটা meaningful line। সেই temporary result variable উধাও। প্রতিটা special case নিজের line-এ দাঁড়িয়ে আছে, পাশেই outcome। আর শেষ line — happy path — zero indentation-এ চিৎকার করে বলছে "এটাই আসল কাজ।"

চিত্র ২: Refactoring-এর পর প্রতিটা guard একটা exit lane — survivor-রা সোজা নিচে নামে, ঠিক মসজিদের লাইনের মতো

Diagram-এর shape দেখো: failure গুলো পাশে exit lane হিসেবে বেরিয়ে যাচ্ছে, আর survivor-রা সোজা মাঝখান দিয়ে নিচে নামছে — ঠিক মসজিদের সেই চার-গেটের লাইনের মতো।

সংখ্যা কী বলছে

Team কেন depth নিয়ে এত মাথা ঘামায়? কারণ reading cost nesting-এর সাথে আস্তে আস্তে বাড়ে না — হুট করে খাড়া হয়ে যায়। তারিক (এখন CSE-তে পড়ছে) তার college project audit করতে গিয়ে দেখল conditional-গুলোর depth এরকম:

চিত্র ৩: তারিকের audit — project-এর অর্ধেক conditional তিন বা তার বেশি level গভীরে ছিল

তারপর সে সময় মাপল — বিভিন্ন depth-এর function-এ "সবচেয়ে ভেতরের line কখন run করে?" প্রশ্নের উত্তর দিতে কতক্ষণ লাগে। pattern টা হাড়ে হাড়ে টের পাওয়া গেল:

চিত্র ৪: nesting depth অনুযায়ী পড়ার সময় — গভীরতার সাথে cost দ্রুত বাড়ছে

Depth ১-এ দশ সেকেন্ড; depth ৪-এ প্রায় দুই মিনিট — আর সেটা নিজের লেখা code, নিজে পড়ছে। Teammate পড়লে আরো বেশি লাগবে। Guard clause প্রতিটা function-কে আবার depth ১-এ নামিয়ে আনে।

কিন্তু সব conditional কি guard বানাতে হবে? না। সিদ্ধান্ত নেওয়ার আগে method-টাকে এই map-এ রাখো:

চিত্র ৫: method কোথায় আছে সেটাই বলে কোন shape নেবে — অনেক pre-check আর early failure থাকলে guard; সমান branch থাকলে if-else

Payout method — অনেক pre-check, প্রতিটা failure early exit — সে guard-এর এলাকায় গভীরে। "Cash নাকি card?" payment choice — দুটো সমান everyday branch — সে সৎ if/else-এর দেশের।

Step-by-step, নিরাপদে

পুরো arrow একবারে flatten করার চেষ্টা করো না। একটা একটা layer তুলে নাও, আর প্রতিটা layer তোলার পর test run করো।

Step ১: সবচেয়ে বাইরের condition নাও। আমাদের example-এ, if (employee.isActive)। জিজ্ঞেস করো: এর else branch কি normal alternative, নাকি "deal করো আর চলে যাও" case? Inactive employee-দের জন্য 0 return করা স্পষ্টভাবে exit case।

Step ২: Condition উল্টে দাও আর early exit করো। "pass করতে পারবে" check হয়ে যায় "বেরিয়ে যেতে হবে" check। else body উপরে উঠে আসে আর guard-এর body হয়:

// INTERMEDIATE: one layer peeled, two still nested
function payout(employee: Employee): number {
  if (!employee.isActive) return 0;   // gate 1 installed — Velu is on duty
 
  let result: number;
  if (!employee.isRetired) {
    if (employee.hasValidBankDetails) {
      result = computeSalary(employee) + computeBonus(employee);
    } else {
      result = 0;
    }
  } else {
    result = pensionAmount(employee);
  }
  return result;
}

Step ৩: Test run করো। Behaviour হুবহু একই হতে হবে। হাতে condition উল্টাতে গিয়ে ছোট ভুল সহজেই হয় — একটা ! ভুলে গেলে, বা && যেটা || হওয়ার দরকার ছিল।

Step ৪: পরের layer-এর জন্য একই কাজ করো। if (!employee.isRetired) উল্টে হয় guard if (employee.isRetired) return pensionAmount(employee);। Peel করো, un-indent করো, test করো।

Step ৫: শেষ layer তুলে temp delete করো। শুধু happy path বাকি থাকলে result variable-এর আর কোনো কাজ নেই। Expression সরাসরি return করো।

Step ৬: Gate গুলো গুছিয়ে নাও। দুটো guard যদি related কারণে same value return করে, Consolidate Conditional Expression দিয়ে merge করার কথা ভাবো। আর gate-গুলোর order ঠিক করো: null-check আগে (পরের gate crash না করুক), তারপর domain-এর গল্প যেভাবে স্বাভাবিকভাবে পড়া যায় সেই order-এ। মসজিদের লাইনে token আগে check হয় জুতার আগে — কারণ যে ঢুকতেই পারবে না তার জুতা দেখার কোনো মানে নেই।

একটা value পুরো finish gate-গুলো পার হওয়ার journey দেখো:

চিত্র ৬: একটা request gate পার করছে একটা একটা করে — প্রতিটা gate হয় এগিয়ে দেয়, নয়তো সাথেসাথে বের করে দেয়

আর একটা state machine হিসেবে — function হয় এখনো check করছে, নয়তো সিদ্ধান্ত নিয়ে ফেলেছে:

চিত্র ৭: State machine হিসেবে function — যেকোনো guard fail করলে সরাসরি Rejected; সব guard pass করলেই Approved
⚠️

প্রতিটা inversion-এর পর test run করো, শেষে একবারে না। a && b উল্টালে সঠিক negation হলো !a || !b (De Morgan's law), !a && !b না — এটাই এই refactoring-এ সবচেয়ে common ভুল, আর test run সেটা সেকেন্ডে ধরে ফেলে। Function-এ test না থাকলে আগে দুই-তিনটা characterization test লিখে নাও: একটা happy path-এর জন্য, একটা করে প্রতিটা special case-এর জন্য। পাঁচ মিনিটের test লেখা তোমাকে নির্ভয়ে refactoring করার সুযোগ দেয়।

একটু deeper জানতে চাইলে: "single entry, single exit" rule যেটা কোনো কোনো textbook এখনো quote করে, সেটা এসেছিল ১৯৬০-৭০-এর structured programming era থেকে — Dijkstra-র goto আর spaghetti control flow-এর বিরুদ্ধে লড়াই। C-এর মতো language-এ, যেখানে function ছাড়ার আগে manually memory free করতে হতো, সব কিছু একটা exit point-এ এনে সত্যিই leak কমত। আধুনিক language সেই calculation বদলে দিয়েছে: garbage collection, C++-এ RAII, C#-এ using, সর্বত্র try/finally — cleanup এখন প্রতিটা exit-এ automatically হয়। Fowler Refactoring-এ সরাসরি বলেছেন: single-exit যখন clarity-র বিরুদ্ধে যায় তখন সেটা কোনো useful rule না। ইতিহাসটা জানলে code review-তে slogan না দিয়ে ভদ্রভাবে যুক্তি দিতে পারবে।

একটা বড় real-life উদাহরণ

ধরো production code-এর কাছাকাছি কিছু refactor করি: একটা music concert-এর online ticket booking function, যেটা কেউ nesting পছন্দ করে লিখেছিল।

// BEFORE: booking logic at the tip of a five-level arrow
function bookTicket(user: User, show: Show, seats: number): Booking {
  let booking: Booking;
  if (user.isLoggedIn) {
    if (!user.isBlocked) {
      if (show.isOpenForBooking) {
        if (seats > 0 && seats <= 6) {
          if (show.availableSeats >= seats) {
            booking = createBooking(user, show, seats);
            show.availableSeats -= seats;
          } else {
            throw new Error("Not enough seats left");
          }
        } else {
          throw new Error("You can book 1 to 6 seats");
        }
      } else {
        throw new Error("Booking is closed for this show");
      }
    } else {
      throw new Error("Your account is blocked");
    }
  } else {
    throw new Error("Please log in first");
  }
  return booking;
}

Booking কেন fail করল জানতে চোখকে পুরো arrow-এর ভেতরে ঢুকতে হবে আবার বেরিয়ে আসতে হবে, পাঁচ লাইন বা পঞ্চাশ লাইন উপরে if-এর সাথে else মেলাতে হবে। এবার guard clause version:

// AFTER: five gates, then the main hall
function bookTicket(user: User, show: Show, seats: number): Booking {
  if (!user.isLoggedIn) throw new Error("Please log in first");
  if (user.isBlocked) throw new Error("Your account is blocked");
  if (!show.isOpenForBooking) throw new Error("Booking is closed for this show");
  if (seats < 1 || seats > 6) throw new Error("You can book 1 to 6 seats");
  if (show.availableSeats < seats) throw new Error("Not enough seats left");
 
  const booking = createBooking(user, show, seats);
  show.availableSeats -= seats;
  return booking;
}

একবার উপর থেকে নিচে পড়ো। মসজিদের গেটের পাশে ঝোলানো নিয়মের বোর্ডের মতো পড়া যাচ্ছে: logged in? blocked না? booking open? seat count ঠিক? seat আছে? — ভেতরে আসো। প্রতিটা failure message সেই check-এর same line-এ আছে; দূরে দূরে brace মেলাতে হচ্ছে না। কাল নতুন নিয়ম এলে ("student রা VIP seat নিতে পারবে না") সেটা একটা নতুন flat line, সব কিছু আবার wrap করতে হবে না।

এই example থেকে একটা অভ্যাস copy করার মতো: যেসব guard throw করে সেগুলো invalid request-এর জন্য perfect, আর যেগুলো default return করে সেগুলো absent-but-okay case-এর জন্য। প্রতিটা gate-এ সচেতনভাবে বেছে নাও।

চিত্র ৮: Booking-এর gate গুলো — প্রতিটা guard একটা exit lane; survivor-রা নিচে createBooking-এ পৌঁছায়

এই gate গুলো দিয়ে যাওয়া domain object গুলো simple — guard প্রতিটা থেকে শুধু একটা flag বা একটা number পড়ে:

চিত্র ৯: Booking domain — guard প্রতিটা gate-এ একটা সস্তা property পড়ে, তারপর happy path Booking বানায়

C#-এ একই refactoring

C# team-রা এই arrow প্রতিদিন দেখে, আর language-এ এটা flatten করার জন্য সুন্দর tools আছে। ধরো একটা loan-approval check, before আর after:

// BEFORE
public decimal SanctionLoan(Applicant a, decimal amount)
{
    decimal sanctioned;
    if (a.HasAccount)
    {
        if (a.CreditScore >= 700)
        {
            if (amount <= a.EligibleLimit)
            {
                sanctioned = amount;
            }
            else
            {
                sanctioned = a.EligibleLimit;
            }
        }
        else
        {
            sanctioned = 0m;
        }
    }
    else
    {
        sanctioned = 0m;
    }
    return sanctioned;
}
// AFTER: guards first, happy path last
public decimal SanctionLoan(Applicant a, decimal amount)
{
    if (!a.HasAccount) return 0m;
    if (a.CreditScore < 700) return 0m;
    if (amount > a.EligibleLimit) return a.EligibleLimit;
 
    return amount;
}

C#-এ তিনটা বিশেষ জিনিস মনে রাখো:

  • Argument guard-এর shorthand আছে। Parameter-এ null আর range check-এর জন্য modern .NET-এ one-line throwing guard আছে: ArgumentNullException.ThrowIfNull(applicant); আর ArgumentOutOfRangeException.ThrowIfNegative(amount);। Framework নিজেই এগুলো bless করেছে।
  • Pattern matching কিছু guard সুন্দর করে দেয়। if (applicant is not { HasAccount: true }) return 0m; — এক gate-এ null আর property দুটোই check।
  • IDisposable cleanup মিস করো না। Method-এ connection খোলা থাকলে early return সেটা leak করতে পারে। Resource using declaration-এ wrap করো — তাহলে প্রতিটা return, early হোক বা late, সঠিকভাবে dispose করবে।

আর যেহেতু guard clause language-independent ভদ্রতা, Python-এ একই idea দেখো — flat shape সেখানে একরকম house style:

# Python loves flat guards — "flat is better than nested" (The Zen of Python)
def sanction_loan(applicant, amount):
    if not applicant.has_account:
        return 0
    if applicant.credit_score < 700:
        return 0
    if amount > applicant.eligible_limit:
        return applicant.eligible_limit
 
    return amount

IDE Support

এই refactoring মূলত ছোট ছোট mechanical inversion-এর sequence — IDE ঠিক এই অংশটাই automate করে:

  • Visual Studio-এ Invert if quick action আছে: if-এ cursor রেখে Ctrl+. চাপো, Invert if বেছে নাও। Condition সঠিকভাবে flip করে branch swap করে — De Morgan inversion সহ যেটা মানুষ ভুল করে। প্রতিটা nesting level-এ বাইরে থেকে ভেতরে apply করলে এই refactoring অনেকটাই হয়ে যায়।
  • JetBrains Rider আর IntelliJ IDEA-তে Invert 'if' statement intention action আছে (if-এ Alt+Enter) আর আলাদা Invert Boolean refactoring আছে পুরো boolean member সব usage-এ safely flip করার জন্য।
  • ReSharper arrow-shaped method complexity inspection-এ flag করে আর একই invert-and-return sequence suggest করে।
  • VS Code-এ language extension (আর community refactoring extension যেমন JavaScript/TypeScript-এর জন্য P42) দিয়ে "replace nested if-else with guard clauses" style action পাওয়া যায়।

Automation-এর পরেও প্রতিটা step-এ test চালানোর discipline তোমাকেই রাখতে হবে। IDE সঠিকভাবে invert করে; কিন্তু সে বলতে পারবে না কোনো branch তোমার domain-এ সত্যিই "exit case" কিনা। সেই বিচারটাই refactoring-এর মানবিক অংশ।

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

সুবিধাঝুঁকি / cost
Happy path flat আর unindented — সাথেসাথে চোখে পড়েসত্যিকারের two-way choice কে guard বানালে domain সম্পর্কে reader-কে ভুল বোঝানো হয়
প্রতিটা special case এক self-contained line: check আর outcome একসাথেহাতে condition উল্টাতে ভুল হতে পারে (De Morgan mistake) — প্রতিটা step-এ test চাই
নতুন precondition মানে একটা flat line, আরেকটা indent level নাEarly return cleanup skip করতে পারে — resource-এ using/try-finally ব্যবহার করো
Temporary result variable আর assign-then-return dance মুছে যায়Strict single-exit standard মানা team pushback করতে পারে; আগে আলোচনা করো
Failure তার কারণের কাছে exit করে, debugging সহজ হয়Guard-এর দীর্ঘ দেয়াল দেখা গেলে function হয়তো বেশি কাজ করছে — split করার কথা ভাবো
Cognitive load কমে: memory-তে condition-এর stack ধরতে হয় নাGuard-গুলোর মাঝে side effect থাকলে reorder করা ঝামেলার

কোন smell গুলো এটা ঠিক করে?

Smellএই refactoring কীভাবে সাহায্য করে
Long MethodFlatten করলে brace-staircase আর temp variable সরে, method ছোট হয়
Arrow-shaped / deeply nested codeএটাই এর মূল cure — প্রতিটা nesting level একটা flat gate হয়ে যায়
Comments"// শুধু active আর not retired হলে চলে" ধরনের comment explicit self-documenting gate হয়ে যায়
Control-flag variableresult/done flag যেটা single exit-এ value নিয়ে যায় সেটা চলে যায়; দেখো Remove Control Flag
Duplicated default-handlingবারবার else { result = 0 } branch গুলো পরিষ্কার, orderable guard-এ একত্রিত হয়

পুরো post একটা ছবিতে, revision night-এর জন্য:

চিত্র ১০: পুরো refactoring এক নজরে — সমস্যা, সমাধান, ফলাফল, আর যেসব সীমা মানতে হবে

Quick revision box

+----------------------------------------------------------------+
|  REPLACE NESTED CONDITIONAL WITH GUARD CLAUSES - REVISION CARD |
+----------------------------------------------------------------+
| Problem  : if-inside-if-inside-if -> "arrow" code;             |
|            happy path buried at the tip, equal-looking         |
|            branches hide which path is normal                  |
| Solution : invert each wrapping condition into a GUARD:        |
|            check one special case at the top,                  |
|            return/throw IMMEDIATELY, un-indent the rest        |
| Result   : flat list of gates + unindented happy path at end   |
|                                                                |
| MECHANICS: outermost if -> invert -> early exit -> TEST        |
|            repeat per layer; delete the result temp last       |
| REMEMBER : !(a && b) == !a || !b   (De Morgan!)                |
| KEEP if/else WHEN: both branches are equally normal            |
| guard = "might happen, leave now"                              |
| assertion = "can never happen"  (different tool!)              |
+----------------------------------------------------------------+

Practice করো

তোমার পালা। এই function টা একটা online grocery order-এর delivery charge বের করে, আর এর একটা সুন্দর arrow তৈরি হয়েছে:

function deliveryCharge(order: Order): number {
  let charge: number;
  if (order.pincodeServiceable) {
    if (!order.isCancelled) {
      if (order.totalAmount >= 99) {
        if (order.totalAmount >= 499) {
          charge = 0; // free delivery
        } else {
          charge = order.isPrimeMember ? 0 : 29;
        }
      } else {
        charge = 49;
      }
    } else {
      throw new Error("Order is cancelled");
    }
  } else {
    throw new Error("Sorry, we do not deliver to this pincode");
  }
  return charge;
}

Step-by-step refactor করো:

  1. কোন branch গুলো exit case আর কোনটা (যদি থাকে) সত্যিকারের equal alternative — code ছোঁয়ার আগে এটা এক বাক্যে লিখে রাখো।
  2. সবচেয়ে বাইরের layer তুলে নাও: pincodeServiceable উল্টে throwing guard বানাও। Test চালাও (আগে তিনটা quick test লিখে নাও: serviceable+cancelled, ছোট order, বড় order)।
  3. isCancelled একইভাবে peel করো। আবার test।
  4. totalAmount >= 99-কে guard বানাও যেটা 49 return করে। Test।
  5. ৪৯৯ check আর prime-member check বাকি রইল। সিদ্ধান্ত নাও: এটা exit case নাকি সৎ two-way choice? সেই অনুযায়ী শেষ lines সাজাও, আর charge temp delete করো।
  6. Bonus: নতুন নিয়ম এলো — "বৃষ্টির alert-এর দিনে সব order-এ ১৯ টাকা extra handling, কোনো exception নেই।" এটা কোথায় যাবে, আর দেখো flat shape-এ এই উত্তর কত সহজ, পুরনো arrow-এর তুলনায়।
  7. College bonus: before আর after version-এর cyclomatic complexity হিসাব করো। নিশ্চিত করো দুটো সমান — তারপর এক বাক্যে explain করো after version কেন তবুও পড়তে সহজ, "nesting depth" আর "working memory" শব্দ দুটো ব্যবহার করে।

তোমার final function যদি মসজিদের গেটের পাশের নিয়মের বোর্ডের মতো পড়া যায় — pincode? cancelled না? ৯৯ টাকার উপরে? — আর charge-এর logic নিচে স্বাধীনভাবে দাঁড়িয়ে থাকে, তাহলে তুমি তোমার প্রথম arrow flatten করেছ। রাহিম সাহেবও মাথা নাড়তেন।

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

Guard clause আসলে কী জিনিস?
Guard clause হলো function-এর একদম উপরে একটা ছোট্ট check। সে একটাই special case সামলায়, আর তখনই return বা throw করে বেরিয়ে যায়। মানে দরজায় দাঁড়িয়ে পাহারা দেয়। check fail করলে তুমি সাথেসাথে বেরিয়ে যাও। pass করলে পরের gate-এ যাও। function-এর আসল কাজটা একদম নিচে থাকে — কোনো indentation ছাড়াই।
'একটা function-এ একটাই return থাকবে' — এই নিয়ম কি guard clause-এর বিরুদ্ধে?
Single-exit rule টা এসেছিল পুরনো language-এর জন্য, যেখানে manually cleanup করতে হতো। আধুনিক language-এ garbage collection আর using/try-finally block আছে, তাই multiple early return একদম safe। Fowler নিজেই বলেছেন, এই নিয়ম যখন readability নষ্ট করে তখন এটা কোনো কাজের না। তোমার team-এর standard follow করো, কিন্তু কারণটা জানা রাখো।
কখন guard clause না দিয়ে normal if-else রাখব?
যখন দুটো branch-ই সমান স্বাভাবিক। ধরো, payment online হবে নাকি cash — দুটোই everyday path। তখন symmetric if-else সৎভাবে বলছে 'দুটো সমান choice'। Guard clause বলছে 'এটা unusual, deal করো আর বেরিয়ে যাও'। তোমার domain-এর সত্যিটার সাথে মিলিয়ে shape নাও।
Guard clause কোন order-এ লিখব?
সবচেয়ে সস্তা আর fundamental check আগে। null check আগে করো, তারপর object-এর property check করো — নাহলে crash করবে। এরপর যেভাবে domain-এর গল্পটা স্বাভাবিকভাবে পড়া যায় সেই order-এ লেখো। যেমন: active না, তারপর retired, তারপর bank details নেই।
Guard clause আর assertion কি একই জিনিস?
না। Guard একটা case handle করে যেটা runtime-এ সত্যিই হতে পারে — যেমন subscription নেই এমন customer। Assertion document করে এমন কিছু যেটা program সঠিক থাকলে কখনোই হওয়া উচিত না। Guard মানে 'হতে পারে'; assertion মানে 'কখনো হবে না'। পুরো পার্থক্যটা Introduce Assertion post-এ আছে।

আরো দেখো

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

Decompose Conditional: জটিল if-কে সহজ নামে ভেঙে ফেলো

Decompose Conditional refactoring শেখো স্কুলের নোটিশের গল্প দিয়ে — সহজ TypeScript ও C# উদাহরণ, নিরাপদ ধাপ, আর IDE shortcut সহ।

আরও পড়ুন

Consolidate Conditional Expression: অনেক ছোট চেক, একটাই পরিষ্কার প্রশ্ন

স্কুল গেটের গল্প দিয়ে Consolidate Conditional Expression শেখো — TypeScript আর C# উদাহরণ, নিরাপদ ধাপ, আর side-effect-এর ফাঁদ যেটা না জানলেই নয়।

আরও পড়ুন

Remove Control Flag: পেয়ে গেলেই থেমে যাও

Remove Control Flag refactoring শেখো একজন দারোয়ানের গল্পের মাধ্যমে। TypeScript আর C# এর উদাহরণ দিয়ে বুঝবে break আর return কীভাবে control flag-এর জায়গা নেয়।

আরও পড়ুন

Introduce Assertion: পরিবেশনের আগে ডাল চেখে দেখো

Introduce Assertion রিফ্যাক্টরিং শেখো একটা সতর্ক রাঁধুনির গল্প দিয়ে — লুকানো assumption গুলো executable check-এ বদলানো, C#-এ Debug.Assert আর TypeScript-এ asserts function, আর assertion আর input validation-এর মধ্যে আসল পার্থক্যটা।

আরও পড়ুন