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

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

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

23 মিনিট আপডেট: June 11, 2026intermediate
refactoringassertionspreconditionsinvariantsdefensive programmingtypescriptcsharp

রাঁধুনি যে পরিবেশনের আগে চেখে দেখেন

ধরো ঢাকার মিরপুরে একটা বড় মেস চলে। ফাতেমা আপা সেই রান্নাঘর চালান। প্রতিদিন বিকেলে আশিজন ছাত্র তার ডাল খায়। ত্রিশ বছরে একবারও তিনি খারাপ batch পরিবেশন করেননি। তার রহস্য কোনো জাদু না — একটা ছোট্ট স্টিলের চামচ।

কোনো পাতিল রান্নাঘর ছাড়ার আগে তিনি চেখে দেখেন। এক চামচ ডাল জিভে নেন, আধসেকেন্ড থামেন, তারপর মাথা নাড়েন। তিনি আসলে নিজের assumption গুলো চেক করছেন: লবণ একবার দেওয়া হয়েছে (দুবার নয়, শূন্যবারও নয়), তড়কা পোড়েনি, ডাল সেদ্ধ হয়েছে। অনেক দিন সবকিছু ঠিকঠাক থাকে আর স্বাদ সেটা confirm করে — চেকটা পাঁচ সেকেন্ড সময় নেয়, কিছু বদলায় না। কিন্তু গত মাসে নতুন সাহায্যকারী করিম দুবার লবণ দিয়ে ফেলেছিল। চামচটা সেটা রান্নাঘরেই ধরে ফেলল — যেখানে ভুলটা হয়েছিল, যেখানে ঠিক করা সম্ভব ছিল — ডাইনিং টেবিলে নয়, যেখানে আশিজন ছাত্র তিতা এক গ্রাস খেয়ে আবিষ্কার করত। করিম একটা ভালো বকা আর শিক্ষা পেল; ছাত্রদের তাজা পাতিল মিলল আর তারা কিছুই জানল না।

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

চিত্র ১: ফাতেমা আপার বিকেল — পাঁচ সেকেন্ডের স্বাদ পরীক্ষা রান্না আর পরিবেশনের মাঝে বসে, আর একটা খারাপ দিনে এটা আশিটা থালা বাঁচায়

Code-এ এরকম অনেক internal প্রতিশ্রুতি আছে — আর বেশিরভাগই অদৃশ্য। একটা function একটা rate দিয়ে ভাগ করে, চুপচাপ ধরে নেয় এটা কখনো শূন্য হবে না। একটা method একটা field পড়ে, চুপচাপ ধরে নেয় setup আগেই চলে গেছে। লেখক এই বিষয়গুলো জানতেন; code কখনো সেটা বলে না। Introduce Assertion হলো program-এর জন্য ফাতেমা আপার চামচ: assumption-কে executable check হিসেবে লেখো যা সেটা নিশ্চিত করে, জোরে জোরে আর ঠিক যেখানে প্রতিশ্রুতি থাকে, ফলাফল downstream পাঠানোর আগেই।

Introduce Assertion কী?

একটা assertion হলো এমন একটা statement যে program-এর একটা নির্দিষ্ট বিন্দুতে একটা condition সত্য হতে হবে। সত্য হলে কিছু ঘটে না — code চলতে থাকে, যেমন ডাল স্বাদ পরীক্ষা পাস করে। মিথ্যা হলে program ঠিক সেই line-এ একটা স্পষ্ট message দিয়ে সাথে সাথে থেমে যায়।

Refactoring হলো একটা লুকানো assumption খুঁজে বের করে explicit করার কাজ:

  1. এমন একটা assumption খোঁজো যার উপর code নির্ভর করে কিন্তু কখনো বলে না। সংকেত: comment যেখানে লেখা "assumes X is set", চেক ছাড়া ব্যবহৃত value, এমন calculation যা "হতে পারে না" এমন input-এ বাজে ফলাফল দেয়।
  2. নির্ভরতার বিন্দুতে এটাকে assertion হিসেবে লেখো, expectation নামকরণ করা message সহ: assert(rate > 0, "discount rate must be positive here")
  3. Test চালাও। সবগুলো pass করতে হবে — কারণ assertion শুধু বলছে যা আগে থেকেই সত্য ছিল। আচরণ অপরিবর্তিত; সত্য দৃশ্যমান হলো।

Refactoring বইতে Fowler-এর কথাটা চমৎকার: একটা assertion হলো একটা executable comment// rate is always positive here লেখা সাধারণ comment ক্ষয় পায় — কেউ চেক করে না, আর একদিন এটা মিথ্যা হয়ে যায়। একটা assertion মিথ্যা হতে পারে না। এটা সত্য না থাকলে, program সেই line-এ থামে, ভাঙা প্রতিশ্রুতির নাম বলে। Documentation যা নিজেকে enforce করে।

আর এখানে গুরুত্বপূর্ণ সীমারেখা — assertion সেই condition-এর জন্য যেগুলো অসম্ভব হওয়া উচিত: programmer error, ভাঙা internal প্রতিশ্রুতি। এগুলো user input, network data, বা file content validate করার জন্য নয়, কারণ সেগুলো runtime-এ সত্যিই ভুল হতে পারে আর real, সবসময়-চালু error handling দাবি করে। চামচ রান্নাঘরের জন্য; কাউন্টার customer-দের জন্য।

💡

এক লাইনে সারকথা: Introduce Assertion একটা অদৃশ্য assumption-কে executable check-এ বদলায় যা expectation document করে আর ঠিক যেখানে প্রতিশ্রুতি ভেঙেছে সেখানে জোরে fail করে — "তিন layer downstream-এ রহস্যময় ভুল উত্তর" থেকে "কারণের জায়গায় স্পষ্ট alarm"-এ রূপান্তর।

কলেজ কর্নার: assertion হলো একটা গভীর formal ধারণার practical, debuggable রূপ। ১৯৬৯ সালে Tony Hoare (null গল্পের সেই Hoare) Hoare logic publish করেছিলেন, যেখানে program সঠিকতা triple হিসেবে বলা হয়: code-এর একটা অংশের আগে যে precondition সত্য হতে হবে, code নিজে, আর পরে guaranteed postcondition। একটা invariant হলো কোনো scope জুড়ে সত্য থাকা condition — loop-এর প্রতিটি iteration (loop invariant) বা object-এর প্রতিটি public-method boundary (class invariant)। Bertrand Meyer পুরো একটা language, Eiffel, বানিয়েছিলেন এগুলোকে executable করে — Design by Contract, require (precondition), ensure (postcondition), আর invariant clause runtime-এ check করা হয়। তোমার সাধারণ assert হলো এর lightweight দৈনন্দিন রূপ: method-এর শুরুতে precondition assert, return-এর আগে postcondition assert, জটিল loop-এর ভেতরে invariant assert। পরীক্ষায় যদি জিজ্ঞেস করে "assertion আর invariant-এর সম্পর্ক কী?", উত্তর হলো: assertion হলো mechanism; precondition, postcondition, বা invariant হলো সেই mechanism যে ধরনের প্রতিশ্রুতি enforce করে।

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

এই সংকেতগুলো খোঁজো:

  • একটা comment যা precondition বর্ণনা করে। // caller must initialise the cache first, // rate is always positive by this point। প্রতিটি পায়জামা পরা assertion — জাগিয়ে তোলো আর executable করো।
  • কারণ থেকে অনেক দূরে যে failure দেখা দিল। Invoice total-এ NaN এসেছিল, আর আসল ভুল ছিল চল্লিশটা call আগে শূন্য rate সেট করা। এরকম প্রতিটি debugging marathon একটা জায়গা চিহ্নিত করে যেখানে assertion blame radius একটা line-এ সংকুচিত করত
  • "অসম্ভব" case-এর জন্য defensive if ধরো code চুপচাপ এমন condition handle করছে যা লেখক বিশ্বাস করতেন কখনো ঘটবে না — এটা একটা গুরুত্বপূর্ণ প্রশ্ন ঝাপসা করে: এই case কি valid (handling রাখো) নাকি impossible (assert করো)? Refactoring উত্তরটা সামনে আনতে বাধ্য করে।
  • এক জনের মাথায় যে জ্ঞান আছে। "ওহ, এই list এখানে পৌঁছানোর সময় সবসময় sorted থাকে — সবাই জানে।" সবাই জানে, যতক্ষণ না যে জানত সে চলে যায়। Assertion সেই জ্ঞান code-এ লিখে দেয়।
  • Internal invariant সহ algorithm। মাঝ-computation সত্য — "running total কখনো negative হয় না", "এই দুটো array একই length থাকে" — perfect assertion material, বিশেষত জটিল loop-এর ভেতরে।

আর বিপরীত সংকেত:

  • condition টা runtime-এ সত্যিই ঘটতে পারে (খারাপ user input, missing file, network বন্ধ)? সেটা validation-এর এলাকা — if + বিনয়ী error + সবসময়-চালু। কখনো assertion নয়।
  • check টা পরের line পুনরাবৃত্তি করে (x = 5 এর পরেই assert(x === 5))? Noise। ওই ইচ্ছাটা মুছে ফেলো।
  • "missing" case কি একটা normal domain state? তাহলে Introduce Null Object ভাবো — অনুপস্থিতিকে violation-এর বদলে ভালো-আচরণের object দাও।

"কারণ থেকে দূরে fail করে" কেন এত গুরুত্বপূর্ণ? দেখো, bug ঘটে আর দেখা দেয় — এই দুটোর মধ্যে দূরত্বই debugging-এর কষ্টের মূল কারণ। টিমগুলো যখন মাপে তাদের debugging ঘণ্টা কোথায় যায়, বেশিরভাগ সময় পেছন দিকে হাঁটতেই যায়:

চিত্র ২: Debugging ঘণ্টা আসলে কোথায় যায় — বেশিরভাগ সময় অনেক দূরে ঘটা কারণে failure trace করতে লাগে
চিত্র ৩: কারণ আর crash-এর দূরত্বের সাথে hunt time নির্মমভাবে বাড়ে — একটা assertion সেই দূরত্ব শূন্যে নামিয়ে আনে

Assertion হলো সেই tool যা প্রতিটি failure-কে সবচেয়ে বাম bar-এ নিয়ে যায়: program ঠিক সেই line-এ থামে যেখানে প্রতিশ্রুতি ভেঙেছিল, একটা message সহ এটার নাম বলে। করিমের দ্বিগুণ লবণ চুলায়ই ধরা পড়েছিল, চৌদ্দ নম্বর টেবিলে নয়।

assert করা আর validate করার মধ্যে সিদ্ধান্ত নিতে, condition-টা এই map-এ রাখো:

চিত্র ৪: সোনালি সীমারেখা একটা map হিসেবে — বাইরের দুনিয়ার condition-এর সবসময়-চালু validation দরকার; internal impossible state assertion চায়

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

মেস রান্নাঘরের billing code। ছাত্ররা ভর্তুকি মূল্য পায়, আর upstream কোথাও billing চলার আগে ভর্তুকি সবসময় set হয় — হওয়ার কথা:

// BEFORE: the assumption is invisible — and quietly mishandled
class MessBill {
  private subsidyRate: number | null = null; // set during monthly setup
 
  amountFor(meals: number): number {
    // "subsidyRate is always set by now" — says who? where?
    if (this.subsidyRate !== null && this.subsidyRate > 0) {
      return meals * FULL_RATE * (1 - this.subsidyRate);
    }
    return meals * FULL_RATE; // silently bills FULL price!
  }
}

ওই if টা একজন maintainer-এর মতো পড়ো। "কোনো subsidy নেই" কি কিছু ছাত্রের জন্য legitimately সত্যিকারের case? নাকি null rate হলো একটা setup bug যা চুপচাপ একজন ছাত্রকে বেশি charge করল? Code তোমাকে বলতে পারছে না। এখন প্রতিশ্রুতি explicit করো:

// AFTER: the assumption is stated, enforced, and impossible to misread
function assertTruth(condition: boolean, message: string): asserts condition {
  if (!condition) throw new Error(`Assertion failed: ${message}`);
}
 
class MessBill {
  private subsidyRate: number | null = null;
 
  amountFor(meals: number): number {
    assertTruth(this.subsidyRate !== null && this.subsidyRate > 0,
      "subsidyRate must be set and positive before billing runs");
 
    return meals * FULL_RATE * (1 - this.subsidyRate);
  }
}

Method body তার real কাজে সংকুচিত হলো, আর contract এখন উপরে লেখা: billing-এর জন্য প্রতিষ্ঠিত positive subsidy দরকার। Monthly setup যদি কোনো ছাত্রকে বাদ দেয়, program এখানেই থামবে, ভাঙা প্রতিশ্রুতির নাম বলবে — সেই ছাত্রকে চুপচাপ বেশি বিল পাঠানোর বদলে যা support তিন সপ্তাহ পরে আবিষ্কার করত।

(TypeScript reader-দের জন্য বোনাস: asserts condition return type compiler-কেও সত্যটা শেখায় — assertion line-এর পরে, this.subsidyRate number-এ narrow হয়, কোনো ! operator দরকার নেই।)

চিত্র ৫: Assertion ছাড়া, ভাঙা assumption downstream ভেসে যায় আর কারণ থেকে অনেক দূরে বিস্ফোরিত হয়; assertion সহ, failure ঠিক যেখানে প্রতিশ্রুতি ভেঙেছিল সেখানে ধরা পড়ে

Module-গুলোর মধ্যে কথোপকথন timing স্পষ্ট করে — assertion handover-এ fire করে, ক্ষতির পরে নয়:

চিত্র ৬: Assertion setup আর billing-এর মধ্যে handover guard করে — কোনো ভুল bill তৈরির আগেই ভাঙা প্রতিশ্রুতির নাম বলা হয়

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

ধাপ ১: একটা assumption খোঁজো। Comment দিয়ে শুরু করো (// assumes..., // by now X is...), তারপর যেকোনো value দেখো যা চেক ছাড়াই ব্যবহৃত হচ্ছে আর ভুল হলে বাজে ফলাফল হবে। একটাই assumption বেছে নাও — এক চামচ, এক পাতিল।

ধাপ ২: প্রথম নির্ভরতার বিন্দুতে assertion হিসেবে লেখো। File-এর শীর্ষে নয়, অস্পষ্টভাবে আগেও নয় — যে line থেকে code এটার উপর নির্ভর করা শুরু করে সেখানে। Message-এ কারণ-নামকরণের একটা বাক্য দাও যার জন্য রাত ৩টার debugger কৃতজ্ঞ হবে:

// INTERMEDIATE: assumption stated; old defensive branch still present
amountFor(meals: number): number {
  assertTruth(this.subsidyRate !== null && this.subsidyRate > 0,
    "subsidyRate must be set and positive before billing runs");
 
  if (this.subsidyRate !== null && this.subsidyRate > 0) {  // old guard, still here
    return meals * FULL_RATE * (1 - this.subsidyRate);
  }
  return meals * FULL_RATE;
}

ধাপ ৩: নিশ্চিত করো assertion side-effect-free। এটা release build-এ compile out হতে পারে, তাই এটা শুধু read করতে পারবে। assert(items.pop() !== undefined) হলো একটা বিখ্যাত bug template — pop টা assertion-এর সাথে অদৃশ্য হয়, আর program Debug আর Release-এর মধ্যে আচরণ পরিবর্তন করে।

ধাপ ৪: পুরো test suite চালাও। দুটো ফলাফল, উভয়ই ভালো। সব green: assumption সত্যিই সত্য, আর তুমি বিনামূল্যে documentation পেলে। কিছু fail করলে: assumption মিথ্যা ছিল — তুমি এইমাত্র এর উৎসে একটা real bug আবিষ্কার করলে। producer (setup code) ঠিক করো, assertion নয়।

ধাপ ৫: পুরনো defensive code-এর ভবিষ্যৎ ঠিক করো। এখন মূল প্রশ্নটা করো: সঠিক production use-এ খারাপ state ঘটতে পারে?

  • না, এটা সম্পূর্ণরূপে programmer error → defensive branch মুছে ফেলো; assertion হলো সৎ রূপ।
  • হ্যাঁ, এটা runtime-এ সত্যিই ঘটতে পারে → তাহলে এটা কখনো assertion material ছিল না; সঠিক handling রাখো (thrown exception, guard clause) আর assert বাদ দাও, বা module boundary-তে উভয় layer রাখো।

ধাপ ৬: বিরল হয়ে পুনরাবৃত্তি করো। যেখানে assumption ঝুঁকি বহন করে সেখানে assertion যোগ করো; প্রতিটি line সাজানোর লোভ এড়িয়ে চলো। তিনটা ধারালো assertion সহ একটা page communicate করে; ত্রিশটা সামান্য assertion সহ একটা page হলো কুয়াশা। ফাতেমা আপা প্রতিটি পাতিল একবার চেখে দেখেন — প্রতিটি চামচ নয়।

প্রতিশ্রুতির দৃষ্টিকোণ থেকে, পুরো mechanism টা একটা ছোট state machine:

চিত্র ৭: একটা internal প্রতিশ্রুতির জীবন — assertion হলো checkpoint যা শান্ত flow আর কারণের জায়গায় জোরে থামার মধ্যে সিদ্ধান্ত নেয়
⚠️

প্রতিটি assertion যোগ করার পরে সাথে সাথে পুরো test suite চালাও — অন্য কোনো code স্পর্শ করার আগে। কোনো test fail করলে, suite সবুজ করতে assertion শিথিল করে "fix" করো না; এটা পুরো উদ্দেশ্যকে নষ্ট করে। Failing test তোমাকে বলছে তুমি যে assumption-এ বিশ্বাস করতে সেটা ইতিমধ্যে কোথাও ভাঙছে। প্রতিশ্রুতি ভাঙা producer খুঁজে বের করো, সেখানে ঠিক করো, তারপর এগিয়ে যাও। Buggy আচরণ মেনে নিতে বাঁকানো assertion হলো codebase-এ বসে থাকা একটা স্বাক্ষরিত মিথ্যা।

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

ধরো মেস রান্নাঘর scale up হলো। একটা meal-planning module দিনের উপস্থিতি sheet থেকে কতটুকু ডাল রান্না করতে হবে হিসাব করে। Pipeline হলো: উপস্থিতি সংগ্রহ করো → validate করো → পরিমাণ হিসাব করো। compute step চুপচাপ ধরে নেয় validate step আগেই চলে গেছে। Assumption গুলো দেখো, তারপর দেখো সেগুলো কীভাবে check-এ পরিণত হয়:

// BEFORE: three invisible assumptions in one function
class MealPlanner {
  // assumes: attendance validated (no negatives, no duplicates)
  // assumes: ratePerStudent loaded from config (grams, 150-250 range)
  computeDalGrams(attendance: DayAttendance, ratePerStudent: number): number {
    const total = attendance.entries.reduce((sum, e) => sum + e.count, 0);
    return total * ratePerStudent;
  }
}

দুটো comment — পায়জামা পরা দুটো assertion। Validation কখনো skip হলে, negative count চুপচাপ ডালের পরিমাণ কমিয়ে দেবে, আর আশিজন ছাত্র আধপেট যাবে যখন bug লুকিয়ে থাকে। এখানে explicit version আছে, আর boundary layer যা properly validation:

// AFTER: validation at the boundary, assertions on internal promises
class MealPlanner {
  // BOUNDARY: raw sheets come from humans — this CAN be wrong. Validate, don't assert.
  static validate(raw: RawSheet): DayAttendance {
    if (raw.entries.some(e => e.count < 0))
      throw new InvalidSheetError("Attendance counts cannot be negative");
    if (hasDuplicateStudents(raw.entries))
      throw new InvalidSheetError("Duplicate student entries found");
    return new DayAttendance(raw.entries);
  }
 
  // INTERNAL: by the time we are here, the promises must already hold. Assert.
  computeDalGrams(attendance: DayAttendance, ratePerStudent: number): number {
    assertTruth(attendance.entries.every(e => e.count >= 0),
      "computeDalGrams received unvalidated attendance — call validate() first");
    assertTruth(ratePerStudent >= 150 && ratePerStudent <= 250,
      `ratePerStudent ${ratePerStudent}g outside sane config range 150-250`);
 
    const total = attendance.entries.reduce((sum, e) => sum + e.count, 0);
 
    const grams = total * ratePerStudent;
    assertTruth(grams >= 0, "computed dal quantity went negative — logic error");
    return grams;
  }
}

কাজের বিভাজন ভালো করে পড়ো, কারণ এটাই এই পাঠের মূল:

  • validate() অগোছালো দুনিয়ার মুখোমুখি। মানুষ উপস্থিতি sheet পূরণ করে; sheet কখনো কখনো ভুল হবেই। তাই এটা real, সবসময়-চালু, user-facing error throw করে। এটা হলো ভদ্রলোক কাউন্টারে, customer যাই নিয়ে আসুক না কেন ভদ্রভাবে সামলাচ্ছেন।
  • computeDalGrams() শুধু অন্য code-এর মুখোমুখি। Contract অনুযায়ী, এর input আগেই পরিষ্কার করা হয়েছে। এর assertion হলো রাঁধুনির চামচ: এগুলো contract document করে ("validated attendance only"), সেই teammate-কে ধরে যে একদিন raw sheet সরাসরি wire করবে, আর বের হওয়ার পথে একটা internal invariant confirm করে (পরিমাণ কখনো negative নয়)।

একই condition — count >= 0উভয় layer-এ আছে, আর এটা duplication নয়। Boundary-তে এটা সম্ভাব্য ঘটনা handle করা; ভেতরে এটা অসম্ভব ঘটনা assert করা। একই expression, বিপরীত অর্থ।

চিত্র ৮: দুটো layer — validation দুনিয়ার মুখোমুখি আর সবসময় চলে; assertion অন্য code-এর মুখোমুখি আর internal contract document করে

Hoare-logic-এর পোশাকে, computeDalGrams-এর structure হলো একটা textbook triple: প্রথম দুটো assert হলো preconditions, শেষেরটা postcondition, আর DayAttendance শুধুমাত্র validated entries বহন করে — এটা হলো class invariant যা validate() প্রতিষ্ঠা করে। জড়িত type গুলো কম আর নিজেরাই গল্প বলে:

চিত্র ৯: Pipeline-এর type গুলো — RawSheet হলো অবিশ্বস্ত দুনিয়া, DayAttendance হলো invariant-বহনকারী পরিষ্কার রূপ, MealPlanner তাদের মধ্যে contract assert করে

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

.NET System.Diagnostics-এ assertion-কে first-class home দেয়। Debug.Assert শুধু Debug build-এ চলে — Debug class-এর call গুলো Release compilation থেকে সম্পূর্ণ সরানো হয়, তাই shipped code-এ শূন্য size আর শূন্য speed cost:

using System.Diagnostics;
 
public class MealPlanner
{
    // boundary validation: always-on, throws real exceptions
    public static DayAttendance Validate(RawSheet raw)
    {
        if (raw.Entries.Any(e => e.Count < 0))
            throw new InvalidSheetException("Attendance counts cannot be negative");
        return new DayAttendance(raw.Entries);
    }
 
    // internal contract: Debug-build spoon-tasting
    public int ComputeDalGrams(DayAttendance attendance, int ratePerStudent)
    {
        Debug.Assert(attendance.Entries.All(e => e.Count >= 0),
            "ComputeDalGrams received unvalidated attendance — call Validate() first");
        Debug.Assert(ratePerStudent is >= 150 and <= 250,
            $"ratePerStudent {ratePerStudent}g outside sane range 150-250");
 
        int grams = attendance.Entries.Sum(e => e.Count) * ratePerStudent;
 
        Debug.Assert(grams >= 0, "computed dal quantity went negative — logic error");
        return grams;
    }
}

মনে রাখার মতো C#-specific বিষয়:

  • Debug.Assert বনাম Trace.Assert Debug.Assert Release-এ অদৃশ্য হয়; Trace.Assert Release build-এ টিকে থাকে। Trace.Assert (বা custom সবসময়-চালু check) বেছে নাও invariant-এর জন্য যেগুলো production-এও guard করতে চাও — এটা একটা ইচ্ছাকৃত নীতির সিদ্ধান্ত, default নয়।
  • Visual Studio-তে fire করলে, Debug.Assert একটা dialog দেখায় message আর পুরো call stack সহ, সরাসরি failing line-এ debugger-এ break করার button সহ — built-in "কারণের জায়গায় alarm" অভিজ্ঞতা।
  • Public API parameter-এ throwing helper পাও, assertion নয়। তোমার module-এর বাইরে থেকে আসা argument-এর জন্য, modern .NET ArgumentNullException.ThrowIfNull(x), ArgumentOutOfRangeException.ThrowIfNegative(n) আর অন্যান্য দেয় — এক line-এ সবসময়-চালু validation। Internal প্রতিশ্রুতি assert করো; boundary input-এ throw করো।
  • কখনো ভেতরে side effect রাখো না। Debug.Assert(queue.TryDequeue(out var item)) Debug-এ dequeue করে আর Release-এ কিছুই করে না — classic build-dependent bug। আগে compute করো, ফলাফল assert করো।

Python ছাত্ররা built-in assert statement দিয়ে একই ধারণার সাথে পরিচিত হয় — আর একই compile-out আচরণ, কারণ Python-কে -O flag দিয়ে চালালে প্রতিটি assert বাদ পড়ে:

# Python: assert is built in, and python -O removes it — same rules apply
def compute_dal_grams(attendance, rate_per_student):
    assert all(e.count >= 0 for e in attendance.entries), \
        "received unvalidated attendance - call validate() first"
    assert 150 <= rate_per_student <= 250, \
        f"rate {rate_per_student}g outside sane range 150-250"
 
    grams = sum(e.count for e in attendance.entries) * rate_per_student
 
    assert grams >= 0, "computed dal quantity went negative - logic error"
    return grams

এই কারণেই প্রতিটি Python style guide আমাদের সোনালি নিয়ম পুনরাবৃত্তি করে: user input validate করতে assert ব্যবহার করো না, কারণ একটা -O flag চুপচাপ তোমার "validation" মুছে দেয়।

IDE সহায়তা

Assertion হলো সাধারণ statement, তাই IDE-এর গল্প হলো দ্রুত লেখা আর failure ভালোভাবে অনুভব করা:

  • Visual Studio Debug.Assert গভীরভাবে integrate করে: debug session-এ failing assertion একটা dialog pop করে (message + stack) সহ Retry button যা সরাসরি exact line-এ debugger-এ break করে। Live Unit Testing আর test runner assertion failure গুলো exception-এর মতোই surface করে।
  • JetBrains Rider / ReSharper code annotation আর contract inspection offer করে: helper গুলোকে [AssertionMethod]/ContractAnnotation দিয়ে mark করলে analyzer-কে শেখায় assert line-এর পরের code condition-এর উপর নির্ভর করতে পারে — false "possible null" warning সরিয়ে, TypeScript-এর asserts-এর analyzer-level cousin।
  • TypeScript tooling: compiler-এর control-flow analysis natively asserts condition আর asserts x is T signature বোঝে (TS 3.7+), তাই VS Code তোমার assertion call-এর পরে type narrow করে — তোমার editor visually প্রমাণ করে assertion তার documentation কাজ করেছে।
  • IntelliJ IDEA (Java) মনে করিয়ে দেয় যে plain assert-এ run configuration-এ -ea JVM flag দরকার, আর এর inspection গুলো side effect সহ assert flag করে।

একটা অভ্যাস যা কোনো IDE replace করতে পারবে না: সিদ্ধান্ত নেওয়া যে check টা কোন layer-এ পড়ে। Tools চামচ ধরা সহজ করে; শুধু ফাতেমা আপাই জানেন রান্নাঘর কী প্রতিশ্রুতি করেছিল।

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

সুবিধাঝুঁকি / খরচ
লুকানো assumption গুলো explicit, machine-checked documentation হয়User input-এ ভুলভাবে ব্যবহার করলে, assert Release-এ অদৃশ্য হয় আর validation চুপচাপ মিলিয়ে যায়
Failure কারণে fire করে, তিন layer downstream-এ নয় — debugging সময় কমেAssertion-এর ভেতরে side effect Debug-vs-Release আচরণ পার্থক্য তৈরি করে
Comment-এর মতো পুরনো হতে পারে না — মিথ্যা assertion program থামিয়ে দেয়Over-asserting গুরুত্বপূর্ণ check গুলো noise-এ ডুবিয়ে দেয়
Production-এ সাধারণত বিনামূল্যে (Release build থেকে compile out)Production-এ disabled মানে assert একা live system রক্ষা করতে পারে না — boundary-তে এখনো real validation দরকার
টিমকে প্রতিটি defensive if-এর জন্য "সম্ভব নাকি অসম্ভব?" উত্তর দিতে বাধ্য করেObvious জিনিস restating assertion তথ্য ছাড়াই clutter যোগ করে
কোনো teammate component ভুলভাবে wire করার মুহূর্তে contract violation ধরেFailing test চুপ করাতে assertion শিথিল করা ঠিক সেই bug লুকায় যা এটা খুঁজে পেয়েছিল

আর এক table যার জন্য এই post আছে — ডেস্কের উপরে pin করো:

প্রশ্নGuard / validation ব্যবহার করোAssertion ব্যবহার করো
সঠিক program-এ এটা ঘটতে পারে?হ্যাঁ — user, file, network ভুল আচরণ করেনা — শুধু code bug এটা সত্য করে
কে এটা ঘটাল?বাইরের দুনিয়াএকজন programmer
Production-এ চলে?সবসময়প্রায়ই compile out (নীতির সিদ্ধান্ত)
Failure-এভদ্র, recoverable errorজোরে থামা, ভাঙা প্রতিশ্রুতির নাম বলে
রান্নাঘরের analogyCustomer-এর অনুরোধ সামলানো কাউন্টারের ভাইফাতেমা আপা নিজের ডাল চেখে দেখছেন

কোন smell এটা সারায়?

Smellএই রিফ্যাক্টরিং কীভাবে সাহায্য করে
Comments"// assumes X is set" comment গুলো enforced, undecaying executable check হয়
রহস্যময় downstream failureBlame radius একটা line-এ সংকুচিত হয় — ভাঙা প্রতিশ্রুতিতে assertion
অস্পষ্ট defensive if"সম্ভব বনাম অসম্ভব" প্রশ্ন force করে; impossible case logic-এর ছদ্মবেশ ছাড়ে
Dead CodeInvariant assert করলে প্রায়ই এমন branch প্রকাশ পায় যা কখনো execute হতে পারে না — নিরাপদে delete করা যায়
Implicit, undocumented contractPrecondition আর invariant প্রতিটি method-এর শুরুতে visible, fixed home পায়

পুরো পাঠ, একটা revision ছবিতে ভাঁজ করা:

চিত্র ১০: Introduce Assertion একনজরে — কী assert করতে হয়, কোথায়, আর সোনালি সীমারেখা যা এটাকে নিরাপদ রাখে

দ্রুত revision box

+----------------------------------------------------------------+
|            INTRODUCE ASSERTION - REVISION CARD                 |
+----------------------------------------------------------------+
| Problem  : code silently RELIES on conditions it never states; |
|            broken assumptions explode far from their cause     |
| Solution : write the assumption as an ASSERTION at the point   |
|            of dependence -> an "executable comment" that       |
|            documents the promise AND fails loudly if broken    |
| Result   : contracts visible; failures land AT the cause       |
|                                                                |
| MECHANICS: find assumption -> assert it (no side effects!)     |
|            -> run tests (must stay green) -> settle the old    |
|            defensive if: impossible? delete. possible? guard.  |
|                                                                |
| THE GOLDEN LINE:                                               |
|   user input / files / network  -> VALIDATE (always on)        |
|   internal "can never happen"   -> ASSERT  (may compile out)   |
|                                                                |
| C# : Debug.Assert (Debug only) / Trace.Assert (stays on)       |
| TS : asserts condition  -> compiler narrows types too          |
| Never: side effects inside, or asserting the obvious           |
+----------------------------------------------------------------+

অনুশীলনী

ধরো একটা movie-ticket system senior-citizen ছাড় প্রয়োগ করে। Pipeline: booking form বয়স নেয় → registerCustomer validate করে → concessionPrice discount হিসাব করে। এখানে বর্তমান code, comment সহ:

class TicketCounter {
  // assumes: age was validated during registration (5 to 120)
  // assumes: basePrice is loaded from the show config (always > 0)
  concessionPrice(age: number, basePrice: number): number {
    if (basePrice <= 0) {
      return 100; // "should never happen" fallback someone added in a hurry
    }
    if (age >= 60) {
      return basePrice * 0.5;
    }
    return basePrice;
  }
 
  registerCustomer(formAge: unknown): number {
    // TODO: validation
    return Number(formAge);
  }
}

ধাপে ধাপে refactor করো:

  1. concessionPrice-এ লুকানো assumption গুলো তালিকা করো — দুটো comment, আর তাড়াহুড়ার fallback। প্রতিটির জন্য, এক বাক্যে সোনালি প্রশ্নের উত্তর দাও: runtime-এ সম্ভব, নাকি সঠিক program-এ অসম্ভব?
  2. registerCustomer অগোছালো দুনিয়ার মুখোমুখি (একটা web form!)। সেখানে real validation লেখো: বয়স ৫ থেকে ১২০-এর মধ্যে number না হলে স্পষ্ট error throw করো। এটা boundary কাজ — এখানে assertion নয়।
  3. asserts condition signature সহ assertTruth(condition, message) helper লেখো, তারপর concessionPrice-এ দুটো assertion যোগ করো: age validated range-এর মধ্যে, আর basePrice > 0 — প্রতিটিতে কে প্রতিশ্রুতি ভেঙেছে তা নামকরণ করা message সহ ("call registerCustomer first", "show config must provide a positive price")।
  4. Test চালাও। কিছু fail করলে, producer ঠিক করো — কখনো assertion দুর্বল করো না।
  5. এখন তাড়াহুড়ার fallback settle করো: non-positive price-এর জন্য return 100 একটা বানানো সংখ্যার পেছনে config bug লুকাচ্ছিল। এটা মুছে দাও — assertion হলো সৎ রূপ। দেখো function তার real কাজে সংকুচিত হয়।
  6. Bonus: result invariant যোগ করো — returned price অবশ্যই positive আর basePrice-এর বেশি নয়। এই check কোন দুটো layer-এর মধ্যে, আর কেন?
  7. কলেজ bonus: তুমি যে প্রতিটি check লিখেছ সেটা Design-by-Contract নাম দিয়ে label করো — precondition, postcondition, বা invariant — আর concessionPrice-এর Hoare triple এক line-এ লেখো।
  8. চূড়ান্ত চিন্তার প্রশ্ন, এক বাক্য: একজন teammate registerCustomer-এর ভেতরে assertTruth(formAge !== null) করার পরামর্শ দেয়। এটা ঠিক কোন ভুল ব্যবহার সম্পর্কে এই post সতর্ক করেছে?

প্রশ্ন ৮-এর উত্তর যদি হয় "কারণ form input বাইরের দুনিয়া থেকে আসে আর null হতেই পারে — এটার সবসময়-চালু validation দরকার, assertion নয় যা compile away হতে পারে," তাহলে তুমি সেই একটা পার্থক্য আয়ত্ত করেছ যা এই ছোট্ট refactoring-কে নিরাপদ আর শক্তিশালী করে। রান্নাঘরে ডাল চেখে দেখো; কাউন্টারে অনুরোধ সামলাও। ফাতেমা আপা নিজেই তোমাকে স্টিলের চামচটা দিতেন। শাবাশ।

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

assertion আর input validation-এর পার্থক্য কী?
Input validation সেই জিনিসগুলো সামলায় যেগুলো ঘটতেই পারে — কেউ ভুল PIN দিল, ফাইল নষ্ট হয়ে এলো, network reply মিস হলো। এটা production-এ সবসময় চলবে আর ভদ্রভাবে সাড়া দেবে। Assertion হলো এমন কিছু যা program ঠিকঠাক থাকলে IMPOSSIBLE হওয়া উচিত — validation-এর পরে negative total, setup-এর পরে uninitialised field। Assertion programmer-এর ভুল ধরে; validation দুনিয়ার ভুল ধরে।
assertion কি production-এ চলে?
বেশিরভাগ ক্ষেত্রে না, ইচ্ছা করেই না। Java -ea flag ছাড়া assert statement উপেক্ষা করে; C#-এর Debug.Assert Release build থেকে পুরোপুরি বাদ পড়ে যায়। তাই assertion-এ কখনো এমন logic রাখা যাবে না যা program-এর দরকার, আর production-এ সত্যিই ঘটে এমন কিছু guard করতে assertion ব্যবহার করা যাবে না। কিছু টিম ইচ্ছা করে সস্তা assertion production-এ চালু রাখে fast fail করার জন্য — এটা একটা নীতির সিদ্ধান্ত, দুর্ঘটনা নয়।
assertion করার বদলে exception throw করলেই হয় না?
Module boundary আর recoverable পরিস্থিতিতে throw-ই করো — সেখানে exception-ই সঠিক tool। Assertion ভিন্ন কথা বলে: 'এটা fire করলে বুঝবে code নিজেই ভুল, input নয়।' এটা documentation আর alarm একসাথে, developer-দের জন্য, release-এ সরানো যায়। অনেক টিম public API-তে ArgumentException-এর মতো throwing helper ব্যবহার করে আর internal invariant-এর জন্য assertion — দুটো একসাথে কাজ করে।
assertion যোগ করলে কি আমার program-এর আচরণ বদলাতে পারে?
বদলানো উচিত না — এটাই সঠিকভাবে করার test। Assertion এমন একটা condition বলে যা এমনিতেই সবসময় সত্য; তুমি শুধু অদৃশ্য সত্যকে দৃশ্যমান করছো। একটা যোগ করার পরে যদি test suite fail করতে শুরু করে, অভিনন্দন: তুমি যে assumption-এ বিশ্বাস করতে সেটা মিথ্যা ছিল, আর তুমি এইমাত্র তিন layer downstream-এর বদলে এর উৎসেই একটা real bug আবিষ্কার করলে।
কতগুলো assertion বেশি হয়ে যায়?
যেসব assumption-এ real ঝুঁকি আছে সেগুলো assert করো — যেগুলো একজন maintainer plausibly ভাঙতে পারে। প্রতিটি line assert করলে গুরুত্বপূর্ণ check গুলো গোলমালে ডুবে যায়। Fowler-এর পরামর্শ: assertion দিয়ে communicate করো, সাজাও না। সঠিক জায়গায় একটা ধারালো assertion দশটা obvious assertion-এর চেয়ে ভালো।

আরো দেখো

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

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

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

আরও পড়ুন

Introduce Null Object: 'কিছু নেই' কে একটা ভদ্র প্রতিনিধি দাও

Introduce Null Object refactoring শেখো একটা school guardian card-এর গল্পের মাধ্যমে — Tony Hoare-এর billion-dollar mistake, ছড়িয়ে-ছিটিয়ে থাকা null check গুলো, আর কীভাবে একটা ভদ্র default object সব সামলে নেয়। আর কখন null object আসলে bug লুকিয়ে ফেলতে পারে সেটাও জানবে।

আরও পড়ুন

Comments Smell: যখন স্টিকি নোট একটা এলোমেলো আলমারি লুকিয়ে রাখে

জানো কেন বেশি comment একটা code smell হতে পারে। ভালো WHY comment আর খারাপ WHAT comment-এর পার্থক্য বোঝো — স্টিকি নোটের আলমারির গল্প দিয়ে, সহজ উদাহরণ দিয়ে।

আরও পড়ুন

Replace Error Code with Exception: ব্যর্থতাকে চুপিচুপি নয়, সরাসরি জানাও

Replace Error Code with Exception রিফ্যাক্টরিং শেখো একটা সরকারি অফিসের গল্পের মাধ্যমে — before/after TypeScript আর C# উদাহরণ, নিরাপদ migration ধাপ, আর Result type-এর সাথে সৎ তুলনাসহ।

আরও পড়ুন