Temporary Field: স্কুল ব্যাগে ক্রিকেট কিট
Temporary Field কোড স্মেল শেখো একটা স্কুল ব্যাগের গল্পের মাধ্যমে — TypeScript আর C#-এ null-ভর্তি field দেখো এবং Extract Class দিয়ে ধাপে ধাপে ঠিক করো।
স্কুল ব্যাগে ক্রিকেট কিটের গল্প
ধরো রাহিম, ক্লাস সেভেনের একজন ছাত্র। প্রতিদিন সকালে তার আম্মু ব্যাগ গুছিয়ে দেন: পাঠ্যবই, খাতা, টিফিন বক্স, পানির বোতল। আর তারপর — প্যাড, গ্লাভস, হেলমেট, আর উপর থেকে উঁকি দেওয়া একটা ভারী ক্রিকেট ব্যাট।
"রাহিম, প্রতিদিন পুরো ক্রিকেট কিট নিয়ে যাচ্ছ কেন?"
"কারণ Sports Day আসছে আম্মু। পরের মাসের ১৫ তারিখে।"
তাই কয়েক সপ্তাহ ধরে রাহিম স্কুলে যায় আর আসে আট কেজি বাড়তি ওজন নিয়ে। ব্যাগের চেন লাগে না ঠিকমতো। জ্যামিতি বক্সটা গ্লাভসের নিচে কোথাও চাপা পড়ে আছে। বন্ধু করিম পেন্সিল খুঁজতে ব্যাগে হাত দিলে আগে একটা হেলমেট বেরিয়ে আসে। সে ভাবে, "আজকে কি Sports Day? এটা এখানে কেন?" ব্যাগ তোলা প্রতিটা বন্ধু একই প্রশ্ন করে। আর বেশিরভাগ দিন ক্রিকেট কিট সম্পর্কে সৎ উত্তর হলো: "আজকে এটা ব্যবহার হবে না। এটা ignore করো।"
ব্যাপারটা আরও খারাপ হয়। একটা মঙ্গলবার ক্লাস টিচার সুমাইয়া ম্যাডাম হঠাৎ বললেন সবাই জ্যামিতি বক্স বের করতে — surprise পরীক্ষা। রাহিম খোঁজে। একটা গ্লাভস বের করে। একটা প্যাড বের করে। ক্লাসে হাসাহাসি। বক্স খুঁজে পেতে পেতে পাঁচ মিনিট চলে যায়। কিট চুপচাপ বসে থাকে না — এটা ব্যাগের অন্য সব কাজ ধীর করে দেয়।
আর এখানে একটা বিষয় ছিল যেটা কেউ খেয়াল করেনি, যতক্ষণ না গুরুত্বপূর্ণ হয়ে উঠল। গত বছরের Sports Day-এ রাহিম একটা পুরনো ছেঁড়া গ্লাভস একটা সাইড পকেটে গুঁজে রেখেছিল আর ভুলে গিয়েছিল। এ বছর আসল Sports Day-এ সে তাড়াহুড়ো করে ব্যাগে হাত দিয়ে নতুনটার বদলে পুরনো ছেঁড়া গ্লাভস তুলে নিল। আর দুটো catch ফেলল। পুরনো বাসি জিনিস, ভুলে তুলে নেওয়া, কারণ পুরনো আর নতুন জিনিস একই ব্যাগে।
স্বাভাবিক সমাধান রাহিম ছাড়া সবার কাছে পরিষ্কার। ক্রিকেট কিটের একটা আলাদা কিট ব্যাগ থাকা উচিত, শুধু Sports Day-এ গোছানো, শুধু মাঠে নেওয়া, ম্যাচ শেষে বাড়ি আনা। স্কুল ব্যাগে থাকবে স্কুলের জিনিস — সবসময়, প্রতিদিন, কোনো ব্যতিক্রম নেই।
এবার তোমার class-এর দিকে তাকাও। কখনো এমন কোনো class দেখেছ যেখানে একটা field প্রায় সবসময় null? একটা field যেটা শুধু একটা বিশেষ method চলার সময় মান পায়, আর তার আগে বা পরে কোনো মানে রাখে না? সেটাই রাহিমের ক্রিকেট কিট। class-টা প্রতিদিন বোঝা বহন করছে যেটা একটা "Sports Day"-এ — একটাই method-এ — ব্যবহার হয়। আর class-এর প্রতিটা পাঠক তা এড়িয়ে গিয়ে জিজ্ঞেস করে, "এটা এখানে কেন?"
এটাই Temporary Field কোড স্মেল। রাহিম, আম্মু, করিম, আর সেই ছেঁড়া গ্লাভস মনে রাখো — পুরো লেখাটা এই একটা ব্যাগকে কেন্দ্র করে এগোবে।
এই smell আসলে কী?
Temporary Field হলো Object-Orientation Abuser smell-গুলোর একটা। এখানে abuse-টা সূক্ষ্ম। Field হলো OOP-এর সবচেয়ে গুরুত্বপূর্ণ প্রতিশ্রুতিগুলোর একটা। যখন তুমি একটা class পড়ো, তার field-গুলো বলে এই class-এর একটা object আসলে কী। একজন Student-এর name, rollNumber, className আছে। তুমি বিশ্বাস করো যে প্রতিটা field object-এর পুরো জীবনকালে মানে রাখে।
Temporary field সেই প্রতিশ্রুতি ভাঙে। এটা এমন একটা instance variable যেটা শুধু বিশেষ পরিস্থিতিতে — সাধারণত শুধু একটা নির্দিষ্ট method চলার সময় — একটা real মান পায়। সেই সংকীর্ণ সময়ের বাইরে এটা null, undefined, zero, একটা খালি array, বা আরও খারাপ: stale — আগের রানের ডেটা রেখে গেছে। সাইড পকেটের ছেঁড়া গ্লাভস।
এটা কীভাবে হয়? প্রায় সবসময়ই একটা নির্দোষ shortcut থেকে। একজন প্রোগ্রামার একটা বড় algorithm লেখেন। সেটা বড় হতে থাকে, তাই helper method-এ ভাগ করেন। এখন সব helper-এর একই পাঁচটা intermediate মান দরকার। পাঁচটা parameter প্রতিটা helper-এ পাঠানো দেখতে বাজে লাগছে — তাই প্রোগ্রামার সেই মানগুলো class-এর field-এ তুলে নেন। parameter list ছোট আর সুন্দর হয়ে যায়। কিন্তু class চুপচাপ একটা ক্রিকেট কিট গিলে নিয়েছে: পাঁচটা field যেগুলো শুধু সেই একটা algorithm চলার সময় মানে রাখে।
এক লাইনে সারাংশ: temporary field হলো এক method-এর scratch paper যেটা object-এর permanent আকৃতিতে আটকে গেছে — object এখন মিথ্যা বলছে সে কী ধারণ করে।
একটু ভাবো এখানে কী আদান-প্রদান হলো। প্রোগ্রামার Long Parameter List smell এড়াতে গিয়ে Temporary Field smell তৈরি করলেন। Smell-গুলো প্রায়ই এভাবে কাজ করে — একটা চাপলে আরেকটা বেরিয়ে আসে — যদি না তুমি আসল কারণটা ঠিক করো। আর আসল কারণ হলো algorithm আর তার working data নিজেরাই একটা আলাদা object হতে চায়।
কলেজ কর্নার: এখানে যে formal ধারণাটা ভাঙা হচ্ছে সেটা হলো class invariant — এমন একটা শর্ত যেটা class-এর প্রতিটা object-এর জন্য সত্য, constructor শেষ হওয়ার মুহূর্ত থেকে object মারা যাওয়া পর্যন্ত। "প্রতিটা field একটা অর্থপূর্ণ মান ধারণ করে" হলো সবচেয়ে মৌলিক invariant। Temporary field সেই invariant-কে একটা দুর্বল, সময়-নির্ভর একটায় নামিয়ে দেয়: "field-গুলো শুধুমাত্র method X call stack-এ থাকলে অর্থপূর্ণ।" সময়-নির্ভর invariant যাচাই করা reviewer, type checker, আর future maintainer-দের কাছে সবচেয়ে কঠিন। নিচে যে cure দেখবে — field-গুলোকে এমন একটা class-এ নিয়ে যাওয়া যার constructor সম্পূর্ণভাবে invariant প্রতিষ্ঠা করে — সেটা strong form ফিরিয়ে আনে।
পুরো smell-টা একটা মানচিত্রে আঁটে:
কীভাবে চিনবে
কোনো class পড়ার সময় এই checklist-এ চোখ বোলাও:
- কিছু field constructor-এ
null/undefinedদিয়ে initialize হয় আর শুধু একটা method-এর ভেতরে fill হয়। - একটা field method-এর শুরুতে set হয় আর শেষে clear হয় (বা শুধু ফেলে রাখা হয়)।
-
if (this.workingTotal !== null)এর মতো defensive check দেখা যাচ্ছে field fill হওয়ার জায়গা থেকে অনেক দূরে। - কিছু field-এর cluster মাত্র একটা method আর তার private helper-রাই ব্যবহার করে — আর কিছু না।
- Field-এর নামই তার স্বভাব স্বীকার করছে:
_temp,_current,_working,_buffer,_inProgress। - কোনো comment লেখা "only valid during processing" — smell-এর লিখিত স্বীকৃতি।
- "ভুল order-এ" method call করলে object ভেঙে পড়ে (
calculate()আগে চালাতে হবে, নইলে আবর্জনা পাবে)। - একই method-এর দুটো call একে অপরের সাথে conflict করে, কারণ দ্বিতীয় রান প্রথম রানের leftover পড়ে।
এখানে symptom table:
| যা দেখছো | আসলে যা বলছে |
|---|---|
| Field একটা method-এ ছাড়া null | Field হলো method-এর scratch space, object-এর property না |
if (field != null) guard fill code থেকে অনেক দূরে | পাঠকরা বুঝছে না field কখন valid — অনুমান করে guard দিচ্ছে |
| Field + একটা method মিলে একটা private দ্বীপ বানিয়েছে | পুরো একটা hidden class এই class-এর ভেতরে আটকে আছে |
"prepare() আগে call করো, তারপর run()" নিয়ম | Object-এর গোপন temporal নিয়ম শুধু মানুষের মাথায় বাস করছে |
| Method শেষে stale মান রেখে যাচ্ছে | পরবর্তী caller চুপচাপ আগের রানের ডেটা দিয়ে হিসাব করতে পারে |
| দুজন caller একসাথে object ব্যবহার করতে পারে না | Temporary field object-টাকে accidentally non-reentrant করে ফেলেছে |
সবচেয়ে শক্তিশালী একক পরীক্ষা: একটা সন্দেহজনক field নিয়ে নিজেকে জিজ্ঞেস করো, "random একটা মুহূর্তে এই object print করলে কি এই field-এর মান কোনো মানে রাখত?" student.name এর মতো real property-র ক্ষেত্রে হ্যাঁ, সবসময়। Temporary field-এর ক্ষেত্রে উত্তর হবে "ওয়েল, নির্ভর করছে computeReport() এখন চলছে কিনা তার উপর" — আর সেই "নির্ভর করছে" মানেই smell।
ধরো রাহিমের স্কুল বছরের কথা। ব্যাগটা বছরে প্রায় ২২০ দিন বহন করা হয়। সেই দিনগুলোর মধ্যে কতদিন প্রতিটা জিনিস আসলে তার জায়গার মূল্য পরিশোধ করছে?
দুইশোর মধ্যে একদিন ব্যবহার হওয়া field কোনো property না। এটা একজন যাত্রী।
কেন এটা সমস্যা
সমস্যা ১: class প্রতিটা পাঠককে মিথ্যা বলে। Field হলো documentation। যখন ReportBuilder তার field হিসেবে currentRow, runningTotal, আর pageBuffer ঘোষণা করে, নতুন টিমমেট স্বাভাবিকভাবে ধরে নেয় একটা ReportBuilder সত্যিই এই জিনিসগুলো ধারণ করে। সত্যিটা — "ওগুলো garbage, শুধু build() চলার ৪০ মিলিসেকেন্ড ছাড়া" — কোথাও লেখা নেই। মানুষ সেটা কষ্ট করে শেখে, ঠিক যেমন করিম পেন্সিল খুঁজতে গিয়ে হেলমেটে হাত দেয়।
সমস্যা ২: null check খরগোশের মতো বাড়ে। যেহেতু field সাধারণত invalid, অনিশ্চিত প্রতিটা পাঠক একটা guard যোগ করে। Guard-গুলো আসল logic লুকিয়ে ফেলে, আর যে একটা জায়গায় কেউ guard ভুলে যায় সেটা null reference crash হয়। Defensive code প্রতিটা লাইনে চিরতরে একটা ট্যাক্স হয়ে থাকে।
সমস্যা ৩: লুকানো call-order নিয়ম। Temporary field অদৃশ্য sequencing তৈরি করে: আগে ভরো, পরে ব্যবহারো, মাঝখানে কখনো দেখো না। Compiler এই নিয়মগুলো জানে না। Test পাস করে যখন method-গুলো ভাগ্যক্রমে সঠিক order-এ call হয়, আর production ভুল order খুঁজে পায়।
সমস্যা ৪: Object share করা unsafe হয়ে যায়। দুটো thread বা দুটো async call একই instance ব্যবহার করলে তারা একে অপরের temporary field-এ মেশামেশি করে। দেখতে নিরীহ service আসলে গোপনে single-use।
সমস্যা ৫: Testing ভারী হয়। Algorithm test করতে হলে পুরো host object তৈরি করে সঠিক "filled" state-এ আনতে হয়। Algorithm আর তার ডেটা একটা বড় প্রাণীর ভেতরে আটকে আছে।
দেখো একই root থেকে তিনটা আলাদা পচনের শাখা বের হচ্ছে। সেজন্যই এই smell মনোযোগ দাবি করে, যদিও code review-এ এটা কখনো dramatic দেখায় না।
সবচেয়ে বিপজ্জনক শাখাটা হলো stale-read, কারণ এটা crash না করে ভুল উত্তর দেয়। এখানে torn-glove দুর্ঘটনাটা message sequence হিসেবে দেখানো হলো — দুটো module একটা object share করছে, দ্বিতীয়টা চুপচাপ প্রথমটার leftover পড়ছে:
কোনো exception নেই। কোনো log entry নেই। শুধু ভুল score-এর একটা সার্টিফিকেট, যেটা বার্ষিক অনুষ্ঠানে একজন অভিভাবক আবিষ্কার করেন। আর সেই bug ঘণ্টার পর ঘণ্টা ধরে খোঁজা কেমন লাগে:
Mood score-গুলো গল্পটা বলছে। Stale-field bug ধরা যন্ত্রণাদায়ক ঠিক কারণ ভুলটা ঘটে method call-এর মাঝখানে, যেখানে কোনো debugger তাকিয়ে নেই।
বাস্তব কোড উদাহরণ
Sports Day-এর code লিখি এবার। স্কুলে একটা Student class আছে, আর একদিন কেউ তাতে Sports Day scoring যোগ করল — temporary field ব্যবহার করে:
// BAD CODE: the cricket kit lives inside the school bag
class Student {
// Real, always-valid properties of a student
name: string;
rollNumber: number;
className: string;
// ---- Sports Day "kit": only meaningful during computeSportsScore ----
private eventTimings: number[] | null = null; // null 364 days a year
private penaltyPoints: number | null = null; // null 364 days a year
private bestTiming: number | null = null; // null 364 days a year
constructor(name: string, rollNumber: number, className: string) {
this.name = name;
this.rollNumber = rollNumber;
this.className = className;
}
computeSportsScore(timings: number[], falseStarts: number): number {
// Pack the kit...
this.eventTimings = timings;
this.penaltyPoints = falseStarts * 2;
this.bestTiming = null;
this.findBestTiming(); // fills this.bestTiming
const score = this.applyPenalties(); // reads bestTiming + penaltyPoints
// ...and "unpack" it. Or forget to. Who checks?
this.eventTimings = null;
return score;
}
private findBestTiming(): void {
if (this.eventTimings === null) {
throw new Error("Call computeSportsScore first!"); // hidden rule!
}
this.bestTiming = Math.min(...this.eventTimings);
}
private applyPenalties(): number {
// Guards everywhere, because nothing here is trustworthy
if (this.bestTiming === null || this.penaltyPoints === null) {
throw new Error("Scoring not in progress!");
}
return Math.max(0, 100 - this.bestTiming - this.penaltyPoints);
}
}সমস্যাগুলো গুনি, যেমন সুমাইয়া ম্যাডাম হোমওয়ার্ক দেখেন:
- তিনটা field বছরে ৩৬৪ দিন
null—Student-এর প্রতিটা পাঠককে মানসিকভাবে সেগুলো skip করতে হবে। - দুটো private method throw-if-null guard দিয়ে শুরু হয়, যেটা class-এর বলা "আমি নিজেকে বিশ্বাস করি না।"
- লক্ষ্য করো bug ইতিমধ্যে লুকিয়ে আছে:
computeSportsScoreশেষেeventTimingsreset করে কিন্তুbestTimingআরpenaltyPointsclear করতে ভুলে যায়। পরের যে কেউ সেই field পড়লে আগের রানের leftover পাবে — সাইড পকেটের ছেঁড়া গ্লাভস। এটা কৃত্রিম উদাহরণ না, ঠিক এভাবেই চিত্র ৪-এর wrong-certificate bug জন্ম নেয়। - যদি report module আর certificates module একই instance-এ একসাথে
computeSportsScorecall করে (async code, কেউ?), field-গুলো একে অপরকে overwrite করে।
এই field-গুলোর একটার জীবন state machine হিসেবে দেখলে বোঝা যায়। একটা সুস্থ field-এর দুটো state: constructed এবং valid, তারপর gone। Temporary field-এর পুরো গোপন জীবন আছে:
ডানদিকের path-এর প্রতিটা transition — Stale, WrongRead, SilentBug — শুধু এই কারণে আছে যে field computation-এর চেয়ে বেশিক্ষণ বেঁচে থাকে। সেই mismatch দূর করো, diagram-এর পুরো ডান অংশ মিলিয়ে যায়।
ধাপে ধাপে পরিষ্কার করা
ছোট ছোট নিরাপদ ধাপে পরিষ্কার করব — মাঝখানে কোনো ধাপে code ভাঙবে না। এটা আম্মুর kit-bag plan, code-এ প্রয়োগ করা।
ধাপ ১: Temporal নিয়মগুলো দৃশ্যমান করো। কিছু সরানোর আগে, kit field-গুলো একসাথে group করো আর সত্যিটা document করো। এই ধাপ কোনো behaviour পরিবর্তন করে না — শুধু মিথ্যা বলা বন্ধ করে:
// Step 1: at least admit it (an honest intermediate stage, not the goal)
class Student {
name: string;
rollNumber: number;
className: string;
// SCRATCH STATE: valid ONLY inside computeSportsScore and its helpers.
// TODO: extract into its own class.
private scratch: {
eventTimings: number[];
penaltyPoints: number;
bestTiming: number | null;
} | null = null;
// ...
}ইতিমধ্যে ভালো: তিনটার বদলে এখন একটাই nullable জিনিস, আর তার নামই বলছে সে কী। কিন্তু কিট এখনো স্কুল ব্যাগেই আছে।
ধাপ ২: Extract Class — কিটকে তার নিজের ব্যাগ দাও। Scratch field এবং সেগুলো ব্যবহার করা method-গুলো একটা নতুন class-এ নিয়ে যাও। এই class-এর একটা instance প্রতি computation-এ তৈরি হয়, তাই এর ভেতরে প্রতিটা field জন্ম থেকেই valid:
// Step 2: the kit bag — packed on Sports Day, thrown away after
class SportsScoreCalculation {
private readonly eventTimings: number[];
private readonly penaltyPoints: number;
private bestTiming = 0;
constructor(timings: number[], falseStarts: number) {
this.eventTimings = timings; // valid from the first moment
this.penaltyPoints = falseStarts * 2;
}
run(): number {
this.findBestTiming();
return this.applyPenalties();
}
private findBestTiming(): void {
// No guard needed — eventTimings CANNOT be null here. Ever.
this.bestTiming = Math.min(...this.eventTimings);
}
private applyPenalties(): number {
// No guard needed here either.
return Math.max(0, 100 - this.bestTiming - this.penaltyPoints);
}
}ধাপ ৩: Host class-কে সততায় ফিরিয়ে আনো। Student এখন শুধু সেই জিনিসগুলো ধারণ করে যেগুলো একজন student সম্পর্কে বছরের প্রতিটা দিন সত্য:
// Step 3: the school bag carries school things — always valid, all of them
class Student {
constructor(
public readonly name: string,
public readonly rollNumber: number,
public readonly className: string,
) {}
computeSportsScore(timings: number[], falseStarts: number): number {
return new SportsScoreCalculation(timings, falseStarts).run();
}
}এখানে চূড়ান্ত design — দুটো class, প্রতিটার নিজস্ব সৎ lifetime:
লাইনে লাইনে যা পেলাম:
- প্রতিটা null check উধাও। সরানো না — উধাও।
SportsScoreCalculation-এ constructor যেকোনো method চলার আগেই প্রতিটা field ভরে দেয়, তাই guard করার মতো কোনো invalid মুহূর্ত নেই। - Stale-data bug এখন অসম্ভব। Kit object
run()এর পরে ফেলে দেওয়া হয়। Stale হওয়ার কিছু নেই। ছেঁড়া গ্লাভস সেই সন্ধ্যায় kit bag-এর সাথে বাড়ি চলে যায়। Studentscoring-এর জন্য stateless হয়ে গেল — দুটো module, দশটা thread, একসাথে score করতে পারবে, কারণ প্রতিটা call তার নিজের private kit তৈরি করে।- Algorithm এককভাবে test করা যায়।
new SportsScoreCalculation([12.5, 11.9], 1).run()— কোনোStudentদরকার নেই।
এই exact move — একটা method-এর scratch field-কে একটা dedicated object-এ পরিণত করা — এর formal নাম হলো Replace Method with Method Object। এটা "এক বড় algorithm" পরিস্থিতির জন্য Extract Class।
কলেজ কর্নার: এখানে যে গভীর নীতিটা কাজ করছে সেটা হলো lifetime alignment। প্রতিটা state-এর একটা স্বাভাবিক lifetime আছে: per-application, per-object, per-request, per-call। Bug জমে ঠিক সেখানে যেখানে state তার স্বাভাবিক lifetime-এর চেয়ে দীর্ঘ lifetime-এ রাখা হয় — per-call data একটা per-object field-এ, per-request data একটা singleton-এ। Extract Class cure আসলে একটা lifetime correction: নতুন object-এর lifetime computation-এর lifetime-এর সমান, তাই validity construction দিয়েই নিশ্চিত হয়, discipline দিয়ে না।
আর সংখ্যায় ফলাফল — আগে ও পরে defensive line গুনি:
শূন্য আর শূন্য। আমরা সাবধান ছিলাম বলে না — বরং design-টাই unsafe state অপ্রতিনিধিত্বযোগ্য করে ফেলেছে। এটাই সবসময় সেরা ধরনের নিরাপত্তা।
একটা সাধারণ অর্ধেক-সমাধান হলো field রেখে দিয়ে আরও বেশি null check যোগ করা "নিরাপদ থাকতে।" এটা symptom-এর চিকিৎসা করে আর রোগ পুষে রাখে। প্রতিটা নতুন guard code দীর্ঘ করে আর temporal নিয়মগুলো আরও কঠিন করে দেখতে। Cure হলো field-গুলোকে সেখানে নিয়ে যাওয়া যেখানে সেগুলো সবসময় valid — যেখানে valid না সেখানে harder guard না।
C#-এ একই smell
C#-এ একই pattern, সংক্ষিপ্ত — একটা billing class যেটা instance-এ invoice-calculation scratch রাখে:
// BAD: scratch fields on a long-lived service
class InvoiceService
{
private List<decimal>? _lineAmounts; // null between invoices
private decimal _discount; // stale between invoices!
public decimal Total(Order order)
{
_lineAmounts = order.Lines.Select(l => l.Price * l.Qty).ToList();
_discount = order.IsFestivalSeason ? 0.10m : 0m;
return ApplyDiscount(Sum());
}
private decimal Sum() => _lineAmounts!.Sum(); // trust me, it's filled
private decimal ApplyDiscount(decimal s) => s * (1 - _discount);
}সেই _lineAmounts! null-forgiving operator হলো C#-এর "team শুধু জানে" বলার উপায়। আর যদি এই service dependency injection-এ singleton হিসেবে register থাকে — যেমন service সাধারণত থাকে — তাহলে দুটো একসাথে request আসলে একে অপরের _discount overwrite করবে। Fix হলো একই kit-bag move:
// GOOD: one short-lived calculation object per invoice
class InvoiceService
{
public decimal Total(Order order) => new InvoiceCalculation(order).Run();
}
class InvoiceCalculation
{
private readonly List<decimal> _lineAmounts;
private readonly decimal _discount;
public InvoiceCalculation(Order order)
{
_lineAmounts = order.Lines.Select(l => l.Price * l.Qty).ToList();
_discount = order.IsFestivalSeason ? 0.10m : 0m;
}
public decimal Run() => _lineAmounts.Sum() * (1 - _discount);
}readonly field, কোনো null নেই, কোনো ! নেই, construction দিয়েই thread-safe। C# compiler এখন সেটা prove করছে যেটা আগে comment-এ promise করতে হতো।
বাস্তব প্রজেক্টে কোথায় লুকিয়ে থাকে
একবার ক্রিকেট কিট চিনলে অবাক করা জায়গায় দেখতে পাবে:
- Dependency injection container-এ singleton service। একটা service পুরো app-এর জন্য একবার register হয়ে চুপচাপ per-request scratch field-এ রাখে। Testing-এ ঠিকঠাক চলে (একটা request এক সময়), real load-এ data corrupt করে। এটা web backend-এ সবচেয়ে সাধারণ production-only bug source-গুলোর একটা।
- Report আর export generator।
currentPage,runningTotal,rowBufferএকটা চিরকাল-বেঁচে-থাকাReportGenerator-এর field হিসেবে — classic "algorithm helper-দের shared state দরকার।" - Parser আর importer।
_currentLine,_tokenBuffer,_errorsSoFarএকটা long-lived parser object-এ। প্রতিটা parse-এর নিজের short-lived object থাকা উচিত। - Wizard বা multi-step form handler। Step 3-এর field-গুলো Step 2 না চললে garbage। কখনো এটা সত্যিই একটা state machine — তাহলে সেভাবে model করো। প্রায়ই শুধু temporary field extraction চাইছে।
- Game loop।
collidingPairsThisFrameআর এরকম per-frame scratch list একটা manager object-এ permanent world state-এর সাথে মিশে। - "Result" field আর "compute" method পাশাপাশি।
calculate()this.resultভরে, caller-দের মনে রাখতে হয় সঠিক order-এ call করতে। Result field হতে না চেয়ে return value হতে চায়।
সাধারণ সুতো: একটা long-lived object একটা short-lived workspace হিসেবে ব্যবহার হচ্ছে। Lifetime মেলে না, আর সেই mismatch দেখা যায় null আর stale field হিসেবে — স্কুল ব্যাগ কিট ব্যাগের কাজ করছে।
কখন ignore করা যায়
সৎ table — সব field যেটা মাঝে মাঝে খালি থাকে সেটা অপরাধ না:
| পরিস্থিতি | রায় | কেন |
|---|---|---|
| Lazy cache: field একটা ব্যয়বহুল computed মান রাখে, স্পষ্ট fill/clear নিয়ম সহ | ঠিক আছে | এটা object-এর real property প্রতিনিধিত্ব করে, ইচ্ছাকৃতভাবে দেরিতে computed |
| Pure function-এর result-এর memoization | ঠিক আছে | প্রতিবার একই মান computed হতো — রাখাটা optimization, scratch না |
Optional domain data যেমন middleName absent হতে পারে | ঠিক আছে — আলাদা বিষয় | "বাস্তব দুনিয়ায় কখনো কখনো absent" মানে "একটা method চলাকালীনই valid" না |
| Measured hot path-এ reused buffer field, জোরে documented | সহনীয় | Calculated performance exception — isolate করো আর warning comment লেখো |
| ছোট algorithm — field পাঁচ লাইনে এক screen-এ বেঁচে আছে | সাধারণত রেখে দাও | Class extract করার ceremony clarity-র চেয়ে বেশি খরচ লাগে |
| Field মাত্র একটা method-এ valid, null guard ছড়িয়ে পড়ছে | Refactor করো | এটাই আসল smell — Extract Class সাথে সাথে পরিশোধ করে |
| Shared বা singleton service-এ scratch field | জরুরি ভিত্তিতে Refactor করো | শুধু অস্পষ্ট না — traffic আসলে এটা একটা concurrency bug |
দুটো প্রশ্ন যেকোনো সন্দেহজনক field-কে মানচিত্রে রাখে: object-এর জীবনের কতটুকু সময় field valid, আর object কতটা widely shared?
Singleton-with-scratch একটা কারণে top-right কোণায়: এটা মিথ্যা আকৃতিকে real concurrent ক্ষতির সাথে মেলায়। এটাই এই sprint-এ fix করার টা — someday-তে নয়।
দ্রুত বিচারের প্রশ্ন: field কি object সম্পর্কে একটা তথ্য (cache, optional data) নাকি একটা computation সম্পর্কে একটা তথ্য (intermediate total, working buffer)? Computation সম্পর্কে তথ্য এমন একটা object-এ থাকা উচিত যার lifetime সেই computation-এর সমান।
কোন refactoring এটা সারায়
| Refactoring | কখন ব্যবহার করবে |
|---|---|
| Extract Class | মূল cure — temporary field আর তাদের method একটা নতুন class-এ নিয়ে যাও যেখানে সেগুলো সবসময় valid |
| Replace Method with Method Object | একটা বড় algorithm field তৈরি করেছে — পুরো algorithm-টাকে একটা short-lived object-এ পরিণত করো |
| Introduce Parameter / pass values through | "Shared state" ছোট — helper-দের মধ্যে দুই বা তিনটা parameter সৎভাবে পাঠাও |
| Introduce Null Object | Field সত্যিই মাঝে মাঝে absent — null-কে এমন একটা object দিয়ে replace করো যেটা "empty" হিসেবে behave করে এবং guard মুছে ফেলো |
| Helper-গুলো inline করো | Helper-এ ভাগ করাটাই ভুল ছিল — একটা ১৫-লাইনের method হয়তো কোনো shared field দরকারই না |
দ্রুত revision বক্স
+----------------------------------------------------------------+
| TEMPORARY FIELD — CHEAT SHEET |
+----------------------------------------------------------------+
| Story : Ravi carries the cricket kit in his school bag |
| every day; it is used only on Sports Day. |
| Smell : Field with a real value only during one method; |
| null / stale the rest of the object's life. |
| Spot it : null-guards far from the fill site, names like |
| _temp/_working, "call X before Y" rules, |
| fields touched by only one method. |
| Danger : Lying object shape, null crashes, stale data, |
| unsafe sharing, untestable algorithm. |
| Cure : Extract Class / Replace Method with Method Object |
| -> a kit bag packed per computation, then thrown. |
| Keep : Honest caches & optional domain data are NOT this. |
| Mantra : Match the field's lifetime to its owner's lifetime.|
+----------------------------------------------------------------+অনুশীলন
ধরো একটা library management system-এ এই class আছে। ক্রিকেট কিট খোঁজো আর extract করো:
class Library {
books: Book[] = [];
members: Member[] = [];
// Used ONLY while computeFine runs:
private daysLate: number | null = null;
private finePerDay: number | null = null;
private maxFine: number | null = null;
computeFine(member: Member, book: Book, returnDate: Date): number {
this.daysLate = this.calcDaysLate(book.dueDate, returnDate);
this.finePerDay = member.isStudent ? 1 : 5;
this.maxFine = member.isStudent ? 50 : 200;
return this.applyCap();
}
private calcDaysLate(due: Date, ret: Date): number {
return Math.max(0, Math.ceil((ret.getTime() - due.getTime()) / 86400000));
}
private applyCap(): number {
if (this.daysLate === null || this.finePerDay === null || this.maxFine === null) {
throw new Error("Fine calculation not in progress");
}
return Math.min(this.daysLate * this.finePerDay, this.maxFine);
}
}তোমার কাজ:
- চিহ্নিত করো: প্রতিটা temporary field আর প্রতিটা hidden temporal নিয়ম তালিকা করো (কোন method আগে run করতে হবে?)। Stale-data risk-ও খোঁজো —
computeFinereturn করার পরে কোন field পুরনো মান রেখে যায়? - Extract করো: একটা
FineCalculationclass তৈরি করো যার constructormember,book, আরreturnDateনেয়, সব field তাৎক্ষণিকভাবে ভরে, আর একটাইrun(): numbermethod expose করে। কোনো field কখনো null হবে না। - যাচাই করো:
Library.computeFineকে এক-লাইনে rewrite করো। তারপর উত্তর দাও: app-এর দুটো অংশ কি এখন একইLibraryinstance-এ একসাথে fine compute করতে পারে? আগে এটা কেন বিপজ্জনক ছিল? - গল্প check: পুরনো
daysLatefield-এর জন্য (কাগজে) চিত্র ৬-এর state diagram আঁকো, আর কোন state-এ torn-glove bug বাস করে সেটা mark করো। তারপর নতুনFineCalculationversion-এর জন্য একই diagram আঁকো — কতটা state বাকি রইল? - Bonus:
calcDaysLateকোনো field ব্যবহার করে না। এটা তোমাকে কী বলছে যেখানে এটা belong করে? (Hint: একটা pure function standalone helper বা static method হতে পারে — এটা কখনো ব্যাগে থাকার দরকারই ছিল না।)
সচরাচর জিজ্ঞাসা
- Temporary field আসলে কী জিনিস?
- এটা হলো এমন একটা instance field যেটা শুধু একটা নির্দিষ্ট method চলার সময় কোনো কাজে লাগে। বাকি সময় সে null, zero, বা বাসি ডেটা নিয়ে বসে থাকে। object-এর আকৃতি মিথ্যা বলছে — field-টা দেখতে permanent property-র মতো, কিন্তু আসলে একটা algorithm-এর scratch paper মাত্র।
- প্রোগ্রামাররা কেন temporary field বানায়?
- সাধারণত long parameter list এড়াতে। একটা বড় algorithm-কে যখন helper method-এ ভাগ করা হয়, আর সব helper-এরই একই কিছু মান দরকার হয়, তখন সেই মানগুলো field-এ তুলে নেওয়া সহজ মনে হয় — সব জায়গায় parameter পাঠানোর চেয়ে। কিন্তু এর মাশুল পরে দিতে হয়।
- Temporary field-এর মূল সমাধান কী?
- Extract Class, অথবা কাছের সমতুল্য Replace Method with Method Object। Temporary field আর সেগুলো ব্যবহার করা method-গুলো একটা নতুন ছোট class-এ নিয়ে যাও, যার object মাত্র একটি computation-এর জন্য তৈরি হয়। সেই class-এর ভেতরে প্রতিটি field সবসময় valid থাকে।
- Cache আর memoized মান কি temporary field?
- সাধারণত না। একটা cache যেটা ব্যয়বহুল computed result রাখে — আর কখন ভরা হবে, কখন মুছা হবে তার স্পষ্ট নিয়ম আছে — সেটা object-এর real property সম্পর্কে ইচ্ছাকৃত সিদ্ধান্ত। Temporary field হলো এক method-এর scratch space যেটা object-এর আকৃতিতে ঢুকে গেছে।
- Temporary field কীভাবে null reference bug তৈরি করে?
- কারণ field-টা শুধু একটা method চলার সময় valid, তাই অন্য সব জায়গায় এটা touch করতে গেলে null check দরকার হয়। যে মুহূর্তে কেউ ভুল সময়ে এটা পড়ে বা একটা guard ভুলে যায়, program crash করে বা চুপচাপ পুরনো ডেটা ব্যবহার করে।
আরো দেখো
সম্পর্কিত পাঠ
Long Method: যখন একটা function সব কিছু করতে চায়
Long Method code smell শিখো সহজ গল্পের মাধ্যমে — TypeScript আর C# example সহ, Extract Method দিয়ে step-by-step refactoring। একদম beginner-friendly গাইড।
Long Parameter List: দশটা নির্দেশনার চায়ের অর্ডার
Long Parameter List কোড স্মেল সহজ ভাষায় — কেন বেশি argument-এর method বাগ তৈরি করে, আর কীভাবে parameter object দিয়ে call ছোট, পরিষ্কার আর নিরাপদ করা যায়।
Data Clumps: যে বন্ধুরা সবসময় একসাথে ঘোরে
শিক্ষার্থীদের জন্য Data Clumps code smell — শেখো কীভাবে সবসময় একসাথে চলা value-এর গ্রুপ চেনা যায় আর সেগুলোকে একটা class-এ bundle করা যায়, ঠিক যেমন একটা student ID card।
Extract Class: অতিরিক্ত কাজে ডুবে যাওয়া class-কে একটু সাহায্য করো
Extract Class refactoring শেখো একটা মজার school office-এর গল্পের মাধ্যমে। একটা overloaded class-কে দুটো focused class-এ ভাগ করো — প্রতিটার একটাই কাজ।