Replace Temp with Query: তাজা জিজ্ঞেস করো, বাসি চিরকুটে ভরসা করো না
ক্যান্টিনের সিঙ্গারার গল্প দিয়ে Replace Temp with Query বোঝো — TypeScript আর C# উদাহরণ, নিরাপদ ধাপ, আর একটাই সত্যের উৎস।
সিঙ্গারার দামের চিরকুটের গল্প
ধরো রহিম নবম শ্রেণিতে পড়ে। স্কুলের ক্যান্টিনে করিম চাচার সিঙ্গারা তার খুব পছন্দ। করিম চাচা প্রতিদিন কাউন্টারের উপরে বোর্ডে দাম লিখে রাখেন। রহিম প্রতিদিন সকালে ক্যান্টিনে গিয়ে বোর্ড দেখে আর একটা চিরকুটে লিখে নেয়: "সিঙ্গারা = ১২ টাকা।" চিরকুটটা পকেটে রাখে। কোনো বন্ধু জিজ্ঞেস করলে চিরকুট বের করে বলে।
বেশ গোছানো মনে হচ্ছে, তাই না? রহিম মনে মনে ভাবে, "প্রতিবার ক্যান্টিনে কেন যাবো? আমি তো লিখেই রেখেছি!" কিন্তু বিপদটা এখানেই।
দুপুরে করিম চাচা সস্তায় আলু পান আর বোর্ড বদলে দেন: "সিঙ্গারা = ১০ টাকা।" রহিমের চিরকুটে এখনো লেখা ১২। তিন বন্ধু রহিমকে মাথাপিছু ১২ টাকা দিল সিঙ্গারার অর্ডারের জন্য, আর রহিম কাউন্টারে বেশি দাম দিয়ে ফেলল। চিরকুটটা বাসি হয়ে গেছে। লেখার সময় সত্যি ছিল, এখন মিথ্যা — আর দেখে বোঝার উপায় নেই। এটাই বাসি কপির সবচেয়ে ভয়ঙ্কর বৈশিষ্ট্য: মেয়াদ শেষের তারিখ লেখা নেই।
আরো মজার হলো, রহিমের বন্ধুরাও নিজেদের চিরকুট বানাতে শুরু করল। ফাতেমা সকাল ১০টায় কপি করল, সুমাইয়া ১১টায় ফাতেমারটা কপি করল। দুপুর নাগাদ পাঁচটা চিরকুট ঘুরছে ক্লাসে — প্রতিটা আলাদা সময়ের দাম নিয়ে। কার চিরকুট সঠিক? কেউ জানে না।
সমাধান কী? চিরকুট ফেলে দাও, প্রতিবার সরাসরি ক্যান্টিনে জিজ্ঞেস করো। করিম চাচার বোর্ড হলো একমাত্র সত্যের উৎস। দুই সেকেন্ড বেশি লাগলেও উত্তর সবসময় তাজা আর সঠিক।
কোডে, একটা temporary variable হলো রহিমের চিরকুট। এটা একটা মুহূর্তে একটা হিসাবের result কপি করে রাখে, একটা method-এর ভেতরে। অন্য method সেটা দেখতে পারে না, তাই তারা নিজেদের formula-র কপি বানায় — ফাতেমা আর সুমাইয়ার মতো। আজকের refactoring চিরকুটের জায়গায় এমন একটা প্রশ্ন রাখে যেটা যেকোনো সময় জিজ্ঞেস করা যায়। এটার নাম Replace Temp with Query।
Replace Temp with Query আসলে কী?
এক কথায়:
তুমি একটা expression-এর result একটা temporary variable-এ রাখো। সেই expression-কে তার নিজস্ব method-এ (একটা query) সরিয়ে দাও, আর যেখানে temp ছিল সেখানে ওই method call করো।
Query মানে হলো এমন method যেটা একটা প্রশ্নের উত্তর দেয় — হিসাব করে, return করে, কিছুই পরিবর্তন করে না। Save নেই, print নেই, update নেই। কারণ এটা কিছু বদলায় না, class-এর যেকোনো জায়গা থেকে যতবার খুশি call করা নিরাপদ।
একটু দেখো। আগে:
class Order {
// ...
price(): number {
const basePrice = this.quantity * this.itemPrice;
if (basePrice > 1000) {
return basePrice * 0.95;
}
return basePrice * 0.98;
}
}পরে:
class Order {
// ...
price(): number {
if (this.basePrice() > 1000) {
return this.basePrice() * 0.95;
}
return this.basePrice() * 0.98;
}
private basePrice(): number {
return this.quantity * this.itemPrice;
}
}Local variable basePrice চলে গেছে। তার জায়গায় একটা ছোট named method basePrice()। পার্থক্যটা ছোট মনে হলেও আসলে বিশাল: একটা local variable শুধু একটাই method দেখতে পারে; একটা query পুরো class-এর জন্য উত্তর দিতে পারে। কাল যখন invoiceLine() বা loyaltyPoints()-এ base price দরকার হবে, শুধু জিজ্ঞেস করলেই হবে — formula-র কোনো কপি নেই, চিরকুট নেই।
ক্যান্টিনের নিয়মটা মনে রাখো: local variable মানে পকেটের চিরকুট; query মানে দেয়ালের দামের বোর্ড। চিরকুট বাসি হয়, ভুলভাবে কপি হয়। বোর্ড হলো একমাত্র সত্যের উৎস — সবাই তাজা পড়ে।
Martin Fowler তার Refactoring (২য় সংস্করণ, পৃষ্ঠা ১৭৮)-এ এই refactoring বর্ণনা করেছেন। Refactoring Guru-ও এটাকে "composing methods"-এর সবচেয়ে গুরুত্বপূর্ণ কৌশলগুলোর একটা বলে — কারণ এটাই long method ভাঙার পথ খুলে দেয়।
কলেজ কর্নার: single source of truth আর staleness। এই ছোট্ট গল্পটা আসলে computer science-এর সবচেয়ে গভীর ধারণাগুলোর একটা। একটা cached কপি শুধু ততক্ষণ সঠিক যতক্ষণ মূলটা পরিবর্তন না হয়। পরিবর্তন হলে কপির প্রতিটা পাঠক চুপচাপ ভুল উত্তর পায়। এই একই সমস্যা দেখবে অনেক জায়গায়: CPU cache যেগুলো core-এর মধ্যে coherent রাখতে হয়, web cache যেটা গতকালের page দেখায়, DNS record যেটা expire হতে ঘণ্টা লাগে, denormalised database column যেটা table-এর সাথে মেলে না, আর React state যেটা কেউ props থেকে কপি করে আপডেট করতে ভুলে গেছে। Phil Karlton-এর বিখ্যাত কথা — "computer science-এ মাত্র দুটো কঠিন জিনিস: cache invalidation আর জিনিসের নামকরণ" — মজার কারণ এটা সত্যি। Replace Temp with Query হলো সেই সমস্যার বিরুদ্ধে সবচেয়ে ছোট জয়: কপির জন্য invalidation protocol বের না করে, কপিটাই মুছে দাও আর চাহিদামতো হিসাব করো। যে কপি নেই সে কখনো বাসি হতে পারে না। Database পড়ুয়ারা এটাকে normalisation-এর পেছনের একই চিন্তা হিসেবে চিনবে: প্রতিটা তথ্য একবারই store করো, বাকি সব derive করো।
কখন এটা দরকার হয়?
এই পরিস্থিতিগুলো দেখলে সতর্ক হও:
১. একই formula দুই বা ততোধিক method-এ আছে। এটা Duplicate Code-এর জন্ম নিচ্ছে। প্রতিটা কপি একটা চিরকুট, আর একদিন না একদিন কপিগুলো মিলবে না। একটা single query কপি করার বিষয়টাই শেষ করে দেয়।
২. তুমি একটা Long Method ছোট করতে চাইছো কিন্তু temp-গুলো বাধা দিচ্ছে। এটাই সবচেয়ে বড় কারণ। Local variable ভরা method Extract Method-কে আটকায়, কারণ তুমি যে টুকরোটা বের করতে চাও সেটা ওই local-গুলো পড়ে আর লেখে — parameter হিসেবে পাঠাতে হবে, যেটা দেখতে বাজে। Temp-গুলো query করো, তাহলে টুকরোগুলো শুধু object-এর উপর নির্ভর করবে। Extraction সহজ হয়।
৩. একটা হিসাব পুরো class-এ দৃশ্যমান নাম পাওয়ার যোগ্য। basePrice, discountFactor, gstAmount — এগুলো object-এর ধারণা, একটা method-এর ব্যক্তিগত নোট নয়।
৪. একটা subclass হয়তো মানটা আলাদাভাবে হিসাব করতে চাইবে। Local variable override করা যায় না। Query method করা যায়। Polymorphism শুধু method-এ কাজ করে।
অবশ্যই একটা method-এর সব temp candidate নয়। এক বছর ধরে বড় হওয়া একটা class-এ temp-গুলো মোটামুটি এভাবে ভাগ হয়:
শুরু করার আগে একটা pre-condition চেক করো: temp-টা অবশ্যই ঠিক একবার assign হতে হবে, আর তার expression side-effect মুক্ত হতে হবে। Variable দুটো আলাদা কাজে দুইবার assign হলে আগে Split Temporary Variable করো। Temp তুচ্ছ আর একবারই ব্যবহার হলে হালকা Inline Temp যথেষ্ট।
আগে আর পরে এক নজরে
পরিবর্তনের আকৃতিটা একটু বড় উদাহরণে দেখি, TypeScript-এ:
আগে:
class Bill {
constructor(private items: { price: number; qty: number }[]) {}
total(): number {
const subtotal = this.items.reduce((s, i) => s + i.price * i.qty, 0);
const gst = subtotal * 0.05;
return subtotal + gst;
}
}পরে:
class Bill {
constructor(private items: { price: number; qty: number }[]) {}
total(): number {
return this.subtotal() + this.gst();
}
private subtotal(): number {
return this.items.reduce((s, i) => s + i.price * i.qty, 0);
}
private gst(): number {
return this.subtotal() * 0.05;
}
}পরের version-টা জোরে পড়ো: "total হলো subtotal যোগ GST।" পাঠ্যবইয়ের বাক্যের মতো। প্রতিটা হিসাবের একটা নাম আছে, প্রতিটা নাম Bill-এর যেকোনো জায়গা থেকে call করা যায়। আর লক্ষ্য করো বোনাস — gst() খুশিমনে subtotal() call করে। Query আরেকটা query-র উপর নির্মাণ করতে পারে, যেমন এক বোর্ড আরেকটার দিকে নির্দেশ করে।
Refactoring-এর পরে class নিজেই তার method list-এ ধারণাগুলো ঘোষণা করে। একজন teammate class-এর outline দেখে billing-এর vocabulary শিখে নিতে পারে, একটাও formula না পড়ে:
নিরাপদ উপায়ে ধাপে ধাপে
আমরা ছোট ছোট ধাপে refactor করি, প্রতিটার পরে test করি। এই code-এ কাজ করি:
class Bill {
total(): number {
const subtotal = this.items.reduce((s, i) => s + i.price * i.qty, 0);
const gst = subtotal * 0.05;
return subtotal + gst;
}
}ধাপ ১: চেক করো temp একবার assign হয়েছে কিনা আর expression pure কিনা। subtotal একবার assign হয়েছে। Expression শুধু this.items পড়ে আর সংখ্যা যোগ করে — কোনো side effect নেই, প্রতিবার call করলে একই উত্তর। দুটো check-ই পাস। Temp যদি দুইবার assign হতো, আমরা থামতাম আর আগে Split Temporary Variable করতাম।
ধাপ ২: Temp-কে read-only করো আর test চালাও। TypeScript-এ const, Java-তে final — এটা single assignment প্রমাণ করে। আমাদেরটা ইতিমধ্যে const। Test সবুজ।
ধাপ ৩: Right-hand side-কে একটা query method-এ extract করো। এখনই temp মুছো না! এটা নিরাপদ intermediate অবস্থা — temp এখন শুধু নতুন query-কে call করে:
class Bill {
total(): number {
const subtotal = this.subtotal(); // temp এখনো আছে, এখন query দিয়ে পূরণ হচ্ছে
const gst = subtotal * 0.05;
return subtotal + gst;
}
private subtotal(): number {
return this.items.reduce((s, i) => s + i.price * i.qty, 0);
}
}ধাপ ৪: Test চালাও। সবুজ? Query সঠিক।
ধাপ ৫: Temp inline করো। এটা ঠিক আগের lesson-এর Inline Temp refactoring — দেখো ছোট tool-গুলো কীভাবে মিলে কাজ করে:
class Bill {
total(): number {
const gst = this.subtotal() * 0.05;
return this.subtotal() + gst;
}
private subtotal(): number {
return this.items.reduce((s, i) => s + i.price * i.qty, 0);
}
}আবার test। সবুজ।
ধাপ ৬: পরের temp-এর জন্য একই কাজ। gst extract করো:
class Bill {
total(): number {
return this.subtotal() + this.gst();
}
private subtotal(): number {
return this.items.reduce((s, i) => s + i.price * i.qty, 0);
}
private gst(): number {
return this.subtotal() * 0.05;
}
}শেষ test চালাও। শেষ। একটু লক্ষ্য করো: এক সময়ে একটা temp, প্রতিটা পদক্ষেপের পরে test, আর পরের query (gst) আগের query (subtotal) call করল — এটা স্বাভাবিক আর ভালো।
দূর থেকে দেখলে, তুমি যে প্রতিটা temp নিয়ে কাজ করো সে একই পথ হাঁটে:
দুটো safety rule অলঙ্ঘনীয়। প্রথমত, প্রতিটা ধাপের পরে test চালাও, শেষে নয় — ধাপ ৩ যদি চুপচাপ কিছু বদলে ফেলে, ধাপ ৫ তার উপরে সাজানোর আগে জানতে চাইবে। দ্বিতীয়ত, side effect আছে এমন expression থেকে query বানাবে না। Expression যদি queue থেকে item বের করে, counter বাড়ায়, বা user input পড়ে — দুইবার call করলে একবার call করার চেয়ে ভিন্ন দুনিয়া তৈরি হয়। Query যেকোনো সংখ্যক বার জিজ্ঞেস করার জন্য নিরাপদ হতে হবে।
কলেজ কর্নার: command-query separation। এখানে "query" শব্দটা Bertrand Meyer-এর command-query separation (CQS) নীতি থেকে নেওয়া। এই নীতি বলে: প্রতিটা method হয় একটা command হবে — state পরিবর্তন করে, কিছু return করে না — অথবা একটা query হবে — মান return করে, কিছু পরিবর্তন করে না। দুটো একসাথে না। CQS-এর query-গুলোর একটা property আছে যেটাকে functional programmer-রা referential transparency বলে: একটা call তার result দিয়ে replace করা যায়, বা যতবার খুশি repeat করা যায়, program-এর মানে না বদলে। সেই property-ই এই refactoring-কে নিরাপদ করে। Side-effect pre-condition অলঙ্ঘনীয় হওয়ার কারণও এটাই: একটা method যেটা একসাথে উত্তর দেয় আর কাজও করে — যেমন parser-এ nextToken() — সেটা referentially transparent নয়। ওই রকম method থেকে temp বানালে আচরণ এমনভাবে বদলাবে যা কোনো test suite উপভোগ করে না। পরে distributed systems course-এ CQRS দেখলে, একই ধারণা পুরো service-এ বড় আকারে চিনবে।
একটা বড় বাস্তব উদাহরণ
রহিমের ক্যান্টিনে ফিরে যাই — কিন্তু এখন স্কুলের একটা ছোট canteen app আছে, আর code-এ ঠিক রহিমের চিরকুটের সমস্যা। দুটো method ভালো করে দেখো:
interface MenuItem {
name: string;
price: number;
qty: number;
}
class CanteenOrder {
constructor(private items: MenuItem[], private isStaff: boolean) {}
totalBill(): number {
const subtotal = this.items.reduce((s, i) => s + i.price * i.qty, 0);
const discount = this.isStaff && subtotal > 100 ? subtotal * 0.1 : 0;
const packingCharge = this.items.length > 3 ? 10 : 5;
return subtotal - discount + packingCharge;
}
receiptText(): string {
// একই formula, কপি করা — রহিমের বন্ধুরা নিজেদের চিরকুট বানাচ্ছে!
const subtotal = this.items.reduce((s, i) => s + i.price * i.qty, 0);
const discount = this.isStaff && subtotal > 100 ? subtotal * 0.1 : 0;
return (
`Canteen Receipt\n` +
`Subtotal: Rs ${subtotal}\n` +
`Discount: Rs ${discount}\n` +
`Pay: Rs ${this.totalBill()}`
);
}
}রোগটা দেখছো? receiptText() totalBill()-এর ভেতরের local-গুলো দেখতে পারত না, তাই কেউ formula-গুলো কপি করেছে। আজকে দুটো কপি মিলছে। কিন্তু ধরো পরের মাসে করিম চাচা staff discount ১৫% করলেন, আর developer totalBill() আপডেট করল কিন্তু receiptText() ভুলে গেল। বিল এক সংখ্যা দেখাচ্ছে, printed receipt অন্য সংখ্যা, আর করিম চাচা সন্ধ্যায় কয়েন গুনতে গুনতে বিড়বিড় করছেন। এটাই production-এ বাসি চিরকুট।
এখন প্রতিটা temp-এ Replace Temp with Query করি, এক সময়ে একটা, প্রতিটা ধাপে test করি। গন্তব্য:
class CanteenOrder {
constructor(private items: MenuItem[], private isStaff: boolean) {}
totalBill(): number {
return this.subtotal() - this.discount() + this.packingCharge();
}
receiptText(): string {
return (
`Canteen Receipt\n` +
`Subtotal: Rs ${this.subtotal()}\n` +
`Discount: Rs ${this.discount()}\n` +
`Pay: Rs ${this.totalBill()}`
);
}
private subtotal(): number {
return this.items.reduce((s, i) => s + i.price * i.qty, 0);
}
private discount(): number {
return this.isStaff && this.subtotal() > 100 ? this.subtotal() * 0.1 : 0;
}
private packingCharge(): number {
return this.items.length > 3 ? 10 : 5;
}
}লাভের হিসাব করি:
- Duplicate formula চলে গেছে। Discount-এর নিয়ম এখন একটাই জায়গায়। একবার বদলাও, বিল আর receipt সবসময় মিলবে — একটা বোর্ড, কোনো চিরকুট নেই।
totalBill()চার লাইনের জটিলতা থেকে একটা পাঠযোগ্য বাক্যে নেমে এল: subtotal বিয়োগ discount যোগ packing charge।- প্রতিটা ধারণা — subtotal, discount, packing charge — এখন একটা নাম পেয়েছে। পুরো class আর teammates আবিষ্কার করে পুনরায় ব্যবহার করতে পারবে।
- আগামীকাল
monthlyReport()-এ discount দরকার হলে শুধু জিজ্ঞেস করলেই হবে। Copying নেই।
এই অভ্যাস গড়া team-গুলো bug tracker-এ পার্থক্য দেখে। Stale-copy bug — "report বিলের সাথে মেলে না", "summary পুরনো total দেখাচ্ছে" — derived value একটাই জায়গায় হিসাব হলে দ্রুত কমে যায়:
C#-এ একই refactoring
C#-এ একই ওষুধ কাজ করে। চিকিৎসার আগে একটা delivery-order class:
public class DeliveryOrder
{
private readonly List<OrderItem> _items;
private readonly double _distanceKm;
public DeliveryOrder(List<OrderItem> items, double distanceKm)
{
_items = items;
_distanceKm = distanceKm;
}
public decimal TotalPayable()
{
decimal itemsTotal = _items.Sum(i => i.Price * i.Quantity);
decimal deliveryFee = itemsTotal > 500m ? 0m : (decimal)_distanceKm * 6m;
return itemsTotal + deliveryFee;
}
}প্রতিটা temp query-তে extract করার পরে:
public class DeliveryOrder
{
private readonly List<OrderItem> _items;
private readonly double _distanceKm;
public DeliveryOrder(List<OrderItem> items, double distanceKm)
{
_items = items;
_distanceKm = distanceKm;
}
public decimal TotalPayable() => ItemsTotal() + DeliveryFee();
private decimal ItemsTotal() => _items.Sum(i => i.Price * i.Quantity);
private decimal DeliveryFee() =>
ItemsTotal() > 500m ? 0m : (decimal)_distanceKm * 6m;
}C#-এর দুটো বিশেষ কথা। প্রথমত, expression-bodied members (=>) ছোট query-গুলোকে সুন্দরভাবে সংক্ষিপ্ত করে — এই refactoring-এর নিখুঁত সঙ্গী। দ্বিতীয়ত, C# একটা মধ্যবর্তী বিকল্প দেয়: read-only property যেমন private decimal ItemsTotal => ...;। Property হলো সস্তা, side-effect-মুক্ত মানের জন্য idiomatic C#। বেশিরভাগ .NET team যে guideline মানে: হিসাব সস্তা আর data-র মতো feel হলে property, উল্লেখযোগ্য কাজ করলে method। যাই হোক, refactoring-এর মূল ধারণা একই — চিরকুট একটা প্রশ্নে পরিণত হয়।
IDE support
একটা সৎ কথা: বেশিরভাগ IDE-তে "Replace Temp with Query" লেবেলের একটা button নেই। কিন্তু দরকারও নেই। Refactoring-টা মাত্র দুটো button-press যেটা তারা ইতিমধ্যেই দেয়: temp-এর expression-এ Extract Method, তারপর temp-এ Inline Variable।
| Tool | কীভাবে করবে |
|---|---|
| JetBrains Rider / IntelliJ IDEA | Expression select করো → Ctrl+Alt+M (Extract Method), তারপর temp-এ cursor → Ctrl+Alt+N (Inline Variable) |
| Visual Studio | Expression select করো → Ctrl+. → Extract method, তারপর temp-এ cursor → Ctrl+. → Inline temporary variable |
| VS Code | Expression select করো → Ctrl+Shift+R → Extract to method/function, তারপর inline support language extension-এর উপর নির্ভর করে |
| Eclipse (Java) | Alt+Shift+M (Extract Method), তারপর Alt+Shift+I (Inline) |
দুটো অংশই automated, তাই modern IDE-তে পুরো refactoring প্রায় risk-free। Tool প্রতিটা reference খুঁজে পায়, call ঠিক করে, আর পদক্ষেপ আচরণ বদলালে এগোতে অস্বীকার করে। "extract, test, inline, test" keyboard sequence muscle memory হওয়া উচিত।
সুবিধা আর ঝুঁকি
প্রতিটা কাজের দুটো দিক আছে। এখানে পূর্ণ balance sheet — performance-এর প্রশ্নসহ যেটা সবাই করে:
| সুবিধা | ঝুঁকি / খরচ |
|---|---|
| হিসাবটা পুরো class-এ দৃশ্যমান নাম পায় — একমাত্র সত্যের উৎস | Query প্রতিটা call-এ পুনরায় হিসাব করে; cached temp একবার হিসাব করেছিল |
| Duplicate formula মতবিরোধের আগেই শেষ করে | Expression সত্যিই ব্যয়বহুল আর hot loop-এ call হলে, পুনরায় হিসাবে সত্যিকার সময় লাগতে পারে |
| Temp সরায়, Extract Method-এর প্রধান বাধা শেষ হয় — long method ভাগ করা যায় | Class-এ আরো ছোট method; পাঠকদের inline formula দেখার বদলে নামকে বিশ্বাস করতে হবে |
| Subclass query override করে হিসাব পরিবর্তন করতে পারে — local-এ সম্ভব নয় | শুধু side-effect-মুক্ত expression-এ কাজ করে; stateful temp query হতে পারে না |
| Method পড়তে named question থেকে বানানো বাক্যের মতো লাগে | দুইবার assign হওয়া temp-এ আগে Split Temporary Variable লাগে — অতিরিক্ত ধাপ |
Performance-এর ব্যাপারে সৎ থাকি। হ্যাঁ, gst() যেটা subtotal() call করে মানে items দুইবার sum হচ্ছে। এটা সত্যিকার তথ্য। কিন্তু context দেখো: হাজার item-এর bill-এ, দ্বিতীয় sum microsecond-এ হয় — user বা সাধারণ profiler লক্ষ্যই করবে না — আর modern compiler আর JIT runtime প্রায়ই এই call aggressively optimize করে। Fowler-এর কথা: আগে স্পষ্টতা নাও, performance পরের বিষয়। স্পষ্টতার জন্য refactor করো, তারপর profile করো, আর profiler নির্দিষ্ট query-তে নির্দেশ করলে তবেই caching যোগ করো। যারা "performance-এর জন্য" এটা এড়িয়ে যায় তারা কখনো না হওয়া খরচ এড়াতে স্থায়ী clarity tax দেয়।
সিদ্ধান্তটা আসলে দুই-অক্ষের বিচার — হিসাব কতটা ব্যয়বহুল, আর underlying data কতটা পরিবর্তনযোগ্য?
কলেজ কর্নার: memoization, শৃঙ্খলাবদ্ধ cache। ধরো profiler সত্যিই একটা query-তে নির্দেশ করল — হাজার হাজার বার call হওয়া ব্যয়বহুল একটা report total। পরিণত সমাধান হলো memoization: query তার public face রাখে, কিন্তু ব্যক্তিগতভাবে প্রথম উত্তর store করে আর পরের call-এ তা return করে, underlying data পরিবর্তন হলে stored মান invalidate করে। লক্ষ্য করো এটা কী: একটা cache, কিন্তু single source of truth-এর ভেতরে, যেখানে একজন মালিক মান আর তার invalidation উভয়ই নিয়ন্ত্রণ করে। Original temp-এর চেয়ে অনেক বেশি নিরাপদ, যেখানে প্রতিটা method নিজের পকেটের চিরকুট রাখত কোনো invalidation ছাড়াই। Python এটা functools.cached_property দিয়ে বিনামূল্যে দেয়:
from functools import cached_property
class CanteenOrder:
def __init__(self, items, is_staff):
self._items = items
self._is_staff = is_staff
@cached_property
def subtotal(self): # একবার হিসাব হয়, তারপর cache থেকে দেওয়া হয়
return sum(i.price * i.qty for i in self._items)
@property
def discount(self):
if self._is_staff and self.subtotal > 100:
return self.subtotal * 0.1
return 0কিন্তু lesson-এর ক্রম গুরুত্বপূর্ণ: আগে query বানাও (সঠিক আর পরিষ্কার), তারপর মাপলে memoize করো (দ্রুত)। কখনো উল্টো না।
কোন smell এটা সারায়?
এই refactoring catalog-এর সবচেয়ে শক্তিশালী smell-killer-গুলোর একটা, কারণ এটা একসাথে দুটো বড় smell আক্রমণ করে:
| Smell | Replace Temp with Query কীভাবে সাহায্য করে |
|---|---|
| Long Method | Local variable সরায় যেগুলো long method ভাগ করা অসম্ভব করে; পরে Extract Method পরিষ্কার টুকরোয় ভাগ করতে পারে |
| Duplicate Code | একটা single named query প্রতিটা কপি করা formula-র জায়গা নেয় — চিরকুট চলে গেছে, শুধু বোর্ড আছে |
| Comments | discount() নামের query নিজেই ব্যাখ্যা করে; "// staff discount হিসাব করো" comment অপ্রয়োজনীয় হয়ে যায় |
আর temp-variable refactoring-গুলোর ছোট পরিবারে এর অবস্থান মনে রাখো: Inline Temp তুচ্ছ এক-ব্যবহারের temp-এর জন্য, Split Temporary Variable যখন এক temp দুটো কাজ করে, Extract Variable যখন expression-এর শুধু local নাম দরকার, আর Replace Temp with Query যখন নামটা class-wide দৃশ্যমানতার যোগ্য। চারটা tool, একটাই প্রশ্ন: এই মানের নামটা কোথায় থাকা উচিত?
পুরো lesson, revision-এর জন্য একটা ছবিতে:
দ্রুত revision box
+--------------------------------------------------------------------+
| REPLACE TEMP WITH QUERY - REVISION CARD |
+--------------------------------------------------------------------+
| WHAT : Move a temp's expression into a method (query); |
| call the method wherever the temp was used |
| STORY : Stop writing price chits; ask the canteen each time |
| WHY : Local variable = visible to ONE method |
| Query method = visible to the WHOLE class |
| UNLOCKS : Extract Method (temps were blocking the split) |
| STEPS : 1. Temp assigned once? expression pure? |
| 2. Make it const/final, test |
| 3. Extract expression -> query method, test |
| 4. Inline the temp, test |
| 5. Repeat for next temp |
| HONESTY : Query recomputes each call. Usually harmless. |
| Profile first; cache later ONLY if proven hot. |
| DO NOT : Make queries from side-effecting expressions |
| FAMILY : Inline Temp (trivial), Split Temp Variable (two jobs), |
| Extract Variable (local name only) |
+--------------------------------------------------------------------+অনুশীলন
এখন তোমার পালা। নিচে একটা hostel mess-bill class আছে সব জায়গায় চিরকুট। Refactor করো।
interface Meal {
type: "breakfast" | "lunch" | "dinner";
price: number;
}
class MessBill {
constructor(
private meals: Meal[],
private daysPresent: number,
private isScholarshipStudent: boolean
) {}
monthlyBill(): number {
const mealTotal = this.meals.reduce((s, m) => s + m.price, 0);
const fixedCharge = this.daysPresent > 20 ? 300 : 150;
const concession = this.isScholarshipStudent ? mealTotal * 0.25 : 0;
return mealTotal + fixedCharge - concession;
}
billSummary(): string {
const mealTotal = this.meals.reduce((s, m) => s + m.price, 0);
const concession = this.isScholarshipStudent ? mealTotal * 0.25 : 0;
return `Meals: Rs ${mealTotal}, Concession: Rs ${concession}, Pay: Rs ${this.monthlyBill()}`;
}
}তোমার কাজ:
১. প্রতিটা temporary variable তালিকা করো আর প্রতিটার দুটো pre-condition চেক করো: একবার assign হয়েছে? Expression-এ side-effect নেই?
২. monthlyBill() আর billSummary()-এর মধ্যে duplicate formula চিহ্নিত করো। এটা কোন smell, আর concession-এর নিয়ম বদলালে কী bug হতে পারে? এটাকে একটা paragraph-এর রহিমের গল্প হিসেবে বলো: কে চিরকুট লিখেছিল, কে কপি করেছে, আর কে কাউন্টারে ভুল দাম দেয়।
৩. mealTotal, fixedCharge, আর concession-এ Replace Temp with Query করো, কঠোরভাবে এক সময়ে এক temp: query extract করো, test করো, temp inline করো, test করো। প্রথম extraction-এর পরে inline করার আগের intermediate version লেখো — সেই snapshot প্রমাণ যে তুমি mechanics follow করেছো।
৪. Refactoring-এর পরে concession() mealTotal() call করবে। একটা লাইনে ব্যাখ্যা করো কেন একটা query আরেকটা query call করা সম্পূর্ণ ঠিক আছে।
৫. Figure 10-এর quadrant chart-এ mealTotal place করো: list-এ ১০,০০০ meal, monthlyBill() প্রতি student প্রতি মাসে একবার call হয়। পুনরায় হিসাবের খরচ নিয়ে চিন্তার দরকার আছে? "profile first" নিয়ম দিয়ে উত্তর justify করো।
৬. College students-দের জন্য Bonus: একটা private cached field দিয়ে mealTotal()-এর একটা memoized version লেখো, তারপর একটা বাক্যে লেখো কোন event তোমাকে সেই cache invalidate করতে বাধ্য করবে। সেই বাক্যের ভার অনুভব করো — সেই ভারই কারণ কেন profiler অভিযোগ না করা পর্যন্ত আমরা plain query পছন্দ করি।
তোমার class-এর প্রতিটা গুরুত্বপূর্ণ হিসাব যখন পকেটের চিরকুটের বদলে একটা নামযুক্ত প্রশ্ন হবে, তখন তুমি এই refactoring আয়ত্ত করেছো। রহিম এখন বোর্ড দেখে; তার বন্ধুরা প্রতিবার সঠিক দাম পায়। পরের lesson: এক বেচারা variable-কে যখন দুটো আলাদা কাজ করতে বাধ্য করা হয় তখন কী করতে হয় — Split Temporary Variable।
সচরাচর জিজ্ঞাসা
- এই refactoring-এর নামে 'query' মানে আসলে কী?
- Query মানে হলো এমন একটা method যেটা শুধু একটা প্রশ্নের উত্তর দেয় — হিসাব করে, return করে, আর কিছুই পরিবর্তন করে না। যেমন ক্যান্টিনে গিয়ে জিজ্ঞেস করা 'সিঙ্গারার দাম কত?' কারণ এটা কিছু বদলায় না, class-এর যেকোনো জায়গা থেকে যতবার খুশি call করা যায় — ভয়ের কিছু নেই।
- বারবার method call করলে কি variable-এর চেয়ে ধীর হয় না?
- হয়তো একটু, কোনো কোনো ক্ষেত্রে। Query সেটা নতুন করে হিসাব করে যেটা temp একবার cache করে রেখেছিল। সাধারণ হিসাবে এই তফাৎ পরিমাপ করাই কঠিন, আর compiler প্রায়ই এটা optimize করে দেয়। Fowler বলেন: আগে পরিষ্কার কোড লেখো। Profiler দিয়ে সত্যিকারের hot spot প্রমাণ হলে তখন caching যোগ করো।
- প্রতিটা temporary variable কি query হতে পারে?
- না। Expression-টা side-effect মুক্ত হতে হবে, আর প্রতিবার call করলে একই উত্তর দিতে হবে। একবারের কাজের result store করা temp — যেমন queue থেকে item বের করা — সেটা দ্বিতীয়বার জিজ্ঞেস করা যাবে না। আর দুটো আলাদা কাজে দুইবার assign হওয়া temp-এ আগে Split Temporary Variable করতে হবে।
- এটা Extract Variable থেকে আলাদা কোথায়?
- Extract Variable একটা নামযুক্ত local বানায় — শুধু সেই একটা method দেখতে পারে। Replace Temp with Query একটা নামযুক্ত method বানায় — পুরো class call করতে পারে। একটা method-এর ভেতরেই মান দরকার হলে variable ব্যবহার করো। অন্য method-এরও একই হিসাব দরকার হলে query বানাও।
- কেন বলা হয় এটা Extract Method-এর তালা খোলার চাবি?
- Long method ভাগ করা কঠিন কারণ এর টুকরোগুলো local variable share করে, যেগুলো parameter হিসেবে পাঠাতে হয়। Temp-গুলো query হয়ে গেলে, টুকরোগুলো শুধু object-এর উপর নির্ভর করে, local-এর উপর না। তখন Extract Method করা সহজ হয়ে যায়, প্রায় নিজে থেকেই হয়।
আরো দেখো
সম্পর্কিত পাঠ
Extract Method: একটা বিশাল ফাংশনকে ছোট ছোট নামওয়ালা helper-এ ভাগ করো
Extract Method ধাপে ধাপে শিখে নাও। একটা লম্বা ফাংশন থেকে এলোমেলো block বের করে তাকে একটা পরিষ্কার নাম দাও, আর তোমার কোডকে একটা সহজ to-do লিস্টের মতো পড়ার যোগ্য করে তোলো।
Inline Temp: একবারই ব্যবহার করা রাফ নোটটা ছুঁড়ে ফেলো
Inline Temp রিফ্যাক্টরিং শেখো একটা মজার রাফ পেপারের গল্প দিয়ে — TypeScript আর C# উদাহরণ, নিরাপদ ধাপ, IDE shortcut, আর কখন variable inline করা উচিত না সেটাসহ।
Split Temporary Variable: একটা বালতি দুই কাজ করতে পারে না
দুই বালতির গল্প দিয়ে Split Temporary Variable শেখো — TypeScript ও C# উদাহরণ আর নিরাপদ ধাপ সহ। প্রতিটা variable-কে একটাই কাজ আর একটাই সৎ নাম দাও।
Extract Variable: ছোট ছোট নামওয়ালা ধাপে বড় হিসাব সমাধান করো
Extract Variable শেখো ধাপে ধাপে। একটা বিশাল, জটিল expression কে ছোট ছোট নামওয়ালা অংশে ভাগ করো — ঠিক যেভাবে গণিতের খাতায় কাজ দেখাও।