Form Template Method: একটা রেসিপি কার্ড, অনেক রকম বিরিয়ানি
দুই চাচির বিরিয়ানির গল্প দিয়ে Form Template Method রিফ্যাক্টরিং শেখো — TypeScript আর Python-এ shared algorithm skeleton বের করার ধাপে ধাপে পদ্ধতি, আর কীভাবে এই রিফ্যাক্টরিং Template Method design pattern তৈরি করে।
🍚 দুই চাচি, এক বিরিয়ানি
ধরো ঢাকার একটা অ্যাপার্টমেন্ট বিল্ডিং-এ দুজন বিখ্যাত রাঁধুনি থাকেন। দ্বিতীয় তলার রাহেলা চাচির চিকেন বিরিয়ানি মানুষ সপ্তাহ ধরে মনে রাখে। চতুর্থ তলার সুমাইয়া চাচি কঠোর নিরামিষভোজী, আর তার ভেজ বিরিয়ানির নিজস্ব ভক্তকুল আছে। দুই চাচি বন্ধুত্বপূর্ণ প্রতিদ্বন্দ্বী — প্রতিটা বিল্ডিং ফাংশনে দুই কিচেনের থালা ঘোরে।
এক রবিবার, বিল্ডিং একটা কমিউনিটি রেসিপি বই ছাপানোর সিদ্ধান্ত নিল। ষষ্ঠ তলার engineering ছাত্রী ফাতিমা রেসিপি টাইপ করার দায়িত্ব নিল। দুই চাচি একে একে রেসিপি বললেন, আর দ্বিতীয়টা মাঝপথে ফাতিমা হাসতে শুরু করল। রাহেলা চাচির রেসিপি:
- বাসমতি চাল আধ ঘণ্টা ভিজিয়ে রাখো।
- পেঁয়াজ আর গোটা মসলা ঘিয়ে ভাজো।
- ম্যারিনেট করা চিকেন দিয়ে নরম না হওয়া পর্যন্ত রান্না করো।
- আধসেদ্ধ চালের স্তর মসলার উপর দাও।
- পাতিল বন্ধ করে ২০ মিনিট দমে রাখো।
সুমাইয়া চাচির রেসিপি:
- বাসমতি চাল আধ ঘণ্টা ভিজিয়ে রাখো।
- পেঁয়াজ আর গোটা মসলা ঘিয়ে ভাজো।
- পনির আর সবজি দিয়ে নরম না হওয়া পর্যন্ত রান্না করো।
- আধসেদ্ধ চালের স্তর মসলার উপর দাও।
- পাতিল বন্ধ করে ২০ মিনিট দমে রাখো।
পাঁচটা step। চারটা — হুবহু একই শব্দে — অভিন্ন। শুধু step ৩ আলাদা। ফাতিমা বুদ্ধিমানের কাজ করল। দুটো পূর্ণ রেসিপি ছাপানোর বদলে সে একটাই রেসিপি কার্ড ছাপাল "বিরিয়ানি" শিরোনামে। Step ১, ২, ৪, ৫ একবার লিখল, আর step ৩ রাখল fill-in-the-blank হিসেবে: "মূল উপকরণ দিয়ে রান্না করো — চিকেন (রাহেলা স্টাইল) অথবা পনির আর সবজি (সুমাইয়া স্টাইল)।"
চাচিরা ঠিক এক মিনিট প্রতিবাদ করলেন — "আমার রেসিপি স্পেশাল!" — যতক্ষণ না ফাতিমা কার্ড দেখিয়ে জিজ্ঞেস করল, "চাচি, এই চারটা step-এর মধ্যে কোনটা তোমার কিচেনে আলাদা?" নিরব। স্পেশালনেস পুরোটা step ৩-এ, আর কার্ড সেটা সৎভাবে বলল।
এখন একটু ভাবো সেই একটা কার্ড পুরো বিল্ডিং-এর জন্য কী করে। দমের সময় বিশ মিনিট থেকে পঁচিশ মিনিট হলে — রাহেলা চাচি পরীক্ষা-নিরীক্ষা করছেন — এক জায়গায় ঠিক করলেই বিল্ডিং-এর সব বিরিয়ানি একসাথে উন্নত হয়। তৃতীয় তলার নাসরিন চাচি ডিমের বিরিয়ানি বানাতে চাইলে, পুরো নতুন রেসিপি লিখতে হবে না — শুধু blank পূরণ করতে হবে। আর কারো রেসিপির order চুপচাপ বদলে যেতে পারবে না, কারণ order টা shared কার্ডে থাকে। গত বছর, কার্ডের আগে, সুমাইয়া চাচির ভাগ্নি হাতে রেসিপি কপি করতে গিয়ে step ২ আর ৪ উল্টে ফেলেছিল — সেই বিরিয়ানির কথা বিল্ডিং এখনো বলে। এক কার্ড থাকলে সেই ভুলটা structurally অসম্ভব।
সেই রেসিপি কার্ডটাই আজকের রিফ্যাক্টরিং। যখন দুটো subclass একই sequence of steps perform করে আর শুধু কোনো step-এর details-এ আলাদা, তখন shared sequence টা একটা superclass method-এ তুলে নেওয়া হয় আর আলাদা step গুলো blank হিসেবে রাখা হয় যা subclass গুলো পূরণ করে। এটাই Form Template Method।
Form Template Method কী? 📋
Form Template Method সবচেয়ে চতুর ধরনের duplication-এর বিরুদ্ধে লড়াই করে: shape-এর duplication। দেখো, দুটো subclass method একই procedure follow করে — একই step, একই order-এ — কিন্তু লাইনগুলো detail-এ আলাদা। Line-by-line diff দুটো আলাদা method দেখে। কিন্তু তোমার চোখ দুটো পড়ে বুঝতে পারে — একই algorithm দুবার লেখা।
এই duplication বিপজ্জনক কারণ এটা tool-এর কাছে অদৃশ্য। আর সব duplicated knowledge-এর মতো, এটা drift করে। একটা subclass একটা validation step যোগ করল, অন্যটা পেল না। কেউ এক copy-তে দুটো step reorder করল "কারণ পড়তে ভালো লাগছে।" ছয় মাস পরে, দুটো procedure যেগুলো "একই" ছিল সেগুলো তিনটা subtle জায়গায় disagree করছে, আর কেউ বলতে পারছে না কোন পার্থক্যগুলো intentional।
"আলাদা" method-এর একটা typical জুটির কতটুকু আসলে shared? ফাতিমা তার দুটো রেসিপি measure করল, উত্তর সবসময়ের মতোই:
সমাধানের দুটো movement:
- Skeleton গুলো identical করো। প্রতিটা subclass-এর ভেতরে, আলাদা অংশগুলো ছোট method-এ extract করো। গুরুত্বপূর্ণ বিষয় হলো উভয় subclass-এ একই name আর signature দিতে হবে। যথেষ্ট extraction-এর পরে, দুটো top-level method textually identical হয়ে যাবে — প্রতিটা লাইন হয় shared logic নাহলে একটা same-named step-এর call।
- Skeleton তোলো। এখন identical top-level method টা superclass-এ pull up করো। আলাদা step গুলো subclass-এ রয়ে যায়, parent-এ abstract (বা overridable) operation হিসেবে। তোলা method টাকে template method বলে: এটা order-এর মালিক; subclass গুলো details-এর মালিক।
ব্যাপারটা হলো, ফলাফল একটা বিখ্যাত নামের structure। Form Template Method হলো সেই রিফ্যাক্টরিং যেটা Template Method pattern তৈরি করে — Gang of Four pattern যার definition আমাদের রেসিপি কার্ডের মতোই: একটা operation-এ algorithm-এর skeleton define করো, কিছু step subclass-এ defer করে। Pattern হলো গন্তব্য; এই রিফ্যাক্টরিং হলো সেই রাস্তা যেটা existing duplicated code থেকে শুরু হয়।
এক লাইনে সারমর্ম: যখন দুটো subclass একই step একই order-এ আলাদা details সহ run করে, আলাদা step গুলো same-named method-এ extract করো, shared sequence টা superclass template method-এ pull up করো, আর প্রতিটা subclass শুধু তার নিজের blank পূরণ করুক।
আরেকটা vocabulary word শিখে নাও — hook। প্রতিটা step abstract হতে হবে না। এমন একটা step যেটা শুধু কিছু subclass বদলাতে চায়, সেটা parent-এ একটা default implementation দেওয়া যায় — প্রায়ই empty body। Subclass override করতে পারে কিন্তু বাধ্য নয়। রেসিপি কার্ডে: "Step ৪½ (ঐচ্ছিক): ভালো লাগলে ভাজা কাজু বাদাম দাও।" রাহেলা চাচি এড়িয়ে যান, সুমাইয়া চাচি দেন। Hook গুলো template-কে flexible রাখে, প্রতিটা রাঁধুনিকে প্রতিটা প্রশ্নের উত্তর দিতে বাধ্য না করে।
কখন এটা দরকার? 🔍
যেসব লক্ষণ খেয়াল রাখতে হবে:
- Sibling subclass-এ parallel method। দুটো (বা বেশি) subclass প্রতিটায়
generate(),process(), বাexport()-এর মতো method implement করেছে। পাশাপাশি পড়লে মনে হয় কয়েকটা শব্দ swap করা একই paragraph পড়ছো। এটা হলো structure-এর level-এ Duplicate Code — সবচেয়ে খারাপ ধরনের, কারণ diff tool দেখতে পায় না। - Bug দুবার fix। History দেখায় একই fix মার্চে
HtmlReport.render-এ আর মে মাসেMarkdownReport.render-এ apply হয়েছে, যখন কেউ দ্বিতীয় copy-টা অবশেষে খেয়াল করেছে। দুবার লেখা knowledge মানেই drift করবে। - নতুন variant মানে copy-paste।
PdfReportযোগ করা শুরু হয় "copyHtmlReport, তিনটা লাইন বদলাও" দিয়ে। প্রতিটা copy গর্ত আরো গভীর করে। - পার্থক্যগুলো similarity লুকিয়ে রাখে। Code reviewer দ্রুত বলতে পারে না "এই দুটো subclass-এ আসলে কী আলাদা?" কারণ genuine পার্থক্যগুলো দুইশত shared লাইনের ভেতরে লুকানো।
আর যেসব পরিস্থিতিতে এটা প্রযোজ্য নয়:
- Order সত্যিই আলাদা। একটা subclass যদি ইচ্ছা করে validate-then-save করে আর অন্যটা save-then-validate করে, তাহলে তোলার মতো কোনো shared skeleton নেই। একটায় জোর করলে দুটো procedure-ই নষ্ট হয়। Step গুলো সত্যিই order share করতে হবে।
- মিলটা superficial। যে দুটো method "loop করে আর string তৈরি করে" সেগুলো একই algorithm নয়। একটু ভাবো: তুমি কি একটা numbered recipe লিখতে পারবে যেটা সততার সাথে দুটোকে describe করে? না পারলে, আলাদা রাখো।
- Variation runtime-এ বদলাতে হবে। Template method variation-কে compile time-এ class hierarchy-তে আটকে দেয়। কোনো live object-কে step swap করতে হলে — বা step স্বাধীনভাবে combine করতে হলে — তোমার Strategy object দরকার, Replace Inheritance with Delegation-এর মাধ্যমে।
- Class গুলো এখনো sibling নয়। Duplicated method গুলো unrelated class-এ থাকলে, আগে Extract Superclass দিয়ে common parent দাও; তারপরই template method থাকতে পারে। আর কোনো subclass parent-এর বেশিরভাগ জিনিস refuse করলে, তুমি Refused Bequest-এর দিকে যাচ্ছ — inheritance এখানে ঠিক আছে কিনা পুনর্বিবেচনা করো।
Apply করব কি না সেটা এক নজরে:
| প্রশ্ন | হ্যাঁ → | না → |
|---|---|---|
| প্রতিটা variant-এ একই order-এ একই step? | এগিয়ে যাও | থামো — shared skeleton নেই |
| একটা সৎ numbered recipe কি সব variant describe করতে পারে? | এগিয়ে যাও | থামো — মিলটা superficial |
| Variation কি class-প্রতি fixed (call-প্রতি নয়)? | এগিয়ে যাও | Delegation দিয়ে Strategy ব্যবহার করো |
| Class গুলো কি sibling (বা সততার সাথে sibling হতে পারে)? | Form Template Method apply করো | আগে Extract Superclass, তারপর পুনরায় দেখো |
আর template method আর তার নিকটতম প্রতিযোগী Strategy pattern-এর মধ্যে পছন্দ — step order কতটা stable, আর variation runtime-এ বদলাতে হবে কিনা:
Before আর after এক নজরে
দুই চাচিকে code-এ রাখা যাক। Before — দুটো cook method, চার-পঞ্চমাংশ identical:
// BEFORE: the same procedure written twice; only one step truly differs
class ChickenBiryaniCook {
cook(): string[] {
const log: string[] = [];
log.push("Soak basmati rice 30 min"); // shared
log.push("Fry onions and whole masala in ghee"); // shared
log.push("Add marinated chicken, cook till tender"); // DIFFERENT
log.push("Layer part-cooked rice over masala"); // shared
log.push("Seal pot, dum for 20 min"); // shared
return log;
}
}
class VegBiryaniCook {
cook(): string[] {
const log: string[] = [];
log.push("Soak basmati rice 30 min"); // shared (copy #2)
log.push("Fry onions and whole masala in ghee"); // shared (copy #2)
log.push("Add paneer and vegetables, cook till soft"); // DIFFERENT
log.push("Layer part-cooked rice over masala"); // shared (copy #2)
log.push("Seal pot, dum for 20 min"); // shared (copy #2)
return log;
}
}After — parent-এ একটা রেসিপি কার্ড, প্রতিটা চাচির একটা করে filled-in blank:
// AFTER: the skeleton lives once; subclasses supply only their difference
abstract class BiryaniCook {
cook(): string[] { // the TEMPLATE METHOD
const log: string[] = [];
log.push("Soak basmati rice 30 min");
log.push("Fry onions and whole masala in ghee");
log.push(this.addMainIngredient()); // the blank to fill in
log.push("Layer part-cooked rice over masala");
log.push(this.garnish()); // a HOOK - optional
log.push("Seal pot, dum for 20 min");
return log;
}
protected abstract addMainIngredient(): string; // every cook must answer
protected garnish(): string { return "No garnish"; } // default: skip
}
class ChickenBiryaniCook extends BiryaniCook {
protected addMainIngredient(): string {
return "Add marinated chicken, cook till tender";
}
}
class VegBiryaniCook extends BiryaniCook {
protected addMainIngredient(): string {
return "Add paneer and vegetables, cook till soft";
}
protected garnish(): string { // only Meena Aunty overrides the hook
return "Add fried cashews";
}
}এখন প্রতিটা subclass কী ধারণ করে গণনা করো: শুধু তার genuine পার্থক্য। নাসরিন চাচির ডিমের বিরিয়ানি যোগ করা মাত্র তিন লাইনের class। Dum-এর সময় বদলানো একটা লাইন edit, একবার, সবার জন্য। আর step-এর order variant গুলোর মধ্যে physically drift করতে পারবে না, কারণ order-এর একটাই copy আছে।
একটা বিরিয়ানি রান্না হওয়ার সময় template আর subclass-এর কথোপকথন দেখো। Parent show চালায়; child শুধু জিজ্ঞেস করলে উত্তর দেয়:
কলেজ কর্নার: এই উল্টো control flow-এর একটা বিখ্যাত ডাকনাম আছে — Hollywood principle: "আমাদের call করো না, আমরা তোমাকে call করব।" সাধারণ code-এ specific জিনিস general জিনিসকে call করে (তোমার function একটা library call করে)। Template method-এ, general জিনিস specific জিনিসকে call করে: abstract parent drive করে, আর concrete subclass গুলো blank-এ তার দ্বারা call হয়। এই inversion of control একই idea যেটা সর্বত্র framework চালায় — তুমি React বা ASP.NET call করো না; তারা well-defined point-এ তোমার component আর handler call করে। Template method form করলে, তুমি named extension point সহ একটা ছোট framework বানাচ্ছ।
ধাপে ধাপে, নিরাপদ পথে 🪜
রিফ্যাক্টরিং হলো একের পর এক ছোট, individually-safe move-এর sequence। সোনালী নিয়ম: কিছু pull up করার আগে দুটো method অবশ্যই textually identical হতে হবে।
ধাপ ১: দুটো method পাশাপাশি রাখো। প্রতিটা লাইন হয় shared (দুটোতেই identical) নাহলে varying হিসেবে চিহ্নিত করো। এমন চিহ্নিতকরণ তৈরি করতে না পারলে — order disagree করলে — এখানেই থামো; এই রিফ্যাক্টরিং প্রযোজ্য নয়।
ধাপ ২: প্রতিটা varying অংশ method-এ extract করো — উভয় subclass-এ একই নাম দিয়ে। এটাই technique-এর হৃদয়। প্রতিটা subclass-এর ভেতরে Extract Method apply করো যাতে আলাদা লাইনগুলো ছোট method-এ চলে যায়, যাদের name আর signature siblings জুড়ে ঠিক match করে:
// Intermediate state: both cook() methods now read IDENTICALLY,
// but each still lives in its own subclass. Nothing pulled up yet.
class ChickenBiryaniCook {
cook(): string[] {
const log: string[] = [];
log.push("Soak basmati rice 30 min");
log.push("Fry onions and whole masala in ghee");
log.push(this.addMainIngredient()); // extracted, same name in both
log.push("Layer part-cooked rice over masala");
log.push("Seal pot, dum for 20 min");
return log;
}
private addMainIngredient(): string {
return "Add marinated chicken, cook till tender";
}
}
// VegBiryaniCook.cook() is now line-for-line THE SAME — only
// its addMainIngredient() body differs.প্রতিটা extraction-এর পরে compile আর test করো, সব একসাথে নয়।
ধাপ ৩: Skeleton গুলো match করছে কিনা verify করো। দুটো cook() body diff করো। Character-for-character identical হতে হবে। কোনো stray পার্থক্য থাকলে — ভিন্ন variable name, extra log line — হয় shared করো নাহলে সেটাও extract করো।
ধাপ ৪: Skeleton pull up করো। Pull Up Method apply করো (প্রথমে Extract Superclass দিয়ে superclass তৈরি করো যদি না থাকে)। উঠানো cook() এখন template method। উভয় subclass থেকে identical copy মুছে ফেলো।
ধাপ ৫: Parent-এ step গুলো declare করো। প্রতিটা extracted step method superclass-এ protected abstract হয়; subclass version গুলো তার implementation হয়। Compiler এখন enforce করবে যে প্রতিটা নতুন variant প্রতিটা required প্রশ্নের উত্তর দেয়।
ধাপ ৬: Optional step গুলো hook-এ রূপান্তর করো। যেখানে শুধু কিছু subclass কোনো step চায়, সেখানে abstract-এর বদলে parent-কে default (প্রায়ই empty) implementation দাও। Final template-এ কোনো remaining inline if (this instanceof ...)-style পার্থক্য review করো আর সেগুলোকেও hook-এ রূপান্তর করো।
দুটো trap সম্পর্কে সতর্ক থাকো। প্রথমত, skeleton identical হওয়ার আগে pull up করো না — "mostly same" method pull up করলে parent-এ type check দিয়ে পার্থক্য patch করতে হবে, যেটা শুরুর duplication-এর চেয়েও খারাপ। দ্বিতীয়ত, মনে রাখো subclass গুলো এখন parent-এর promised order-এ step call করার উপর নির্ভর করছে — এটা একটা real contract। পরে কোনো teammate template-এর ভেতরে দুটো লাইন reorder করলে চুপচাপ প্রতিটা subclass ভেঙে যেতে পারে। Step গুলো clearly নাম দাও, sequence document করো, আর একটা test রাখো যেটা order lock down করে।
কলেজ কর্নার: সেই দ্বিতীয় trap হলো fragile base class problem template-এর এপ্রোন পরা। Template method ইচ্ছাকৃতভাবে subclass গুলোকে parent-এর internal calling sequence-এর সাথে couple করে — এটাই পুরো বিষয়। মানে হলো sequence টা "implementation detail" থেকে "published contract"-এ upgrade হয়েছে। Template-এর body-কে public API-এর মতো treat করো: পরিবর্তনে একই যত্ন, একই review, আর ideally একটা characterization test যেটা কোনো step drop বা reorder হলে fail করে। Pattern-টা powerful কারণ এটা এক ধরনের coupling-কে official করে আর তার নাম দেয়।
একটা বড় real-life উদাহরণ 🧾
ধরো একটা school management system দুটো format-এ fee receipt generate করে: parent portal-এর জন্য HTML receipt আর SMS-এর জন্য plain-text receipt। দুটো generator আলাদাভাবে বেড়েছে, এখন প্রতিটায় চল্লিশ লাইন half-shared logic। তাদের সত্যিকারের রূপটা এখানে:
// BEFORE: one algorithm, two bodies, already drifting
class HtmlReceipt {
generate(s: Student, items: FeeItem[]): string {
let out = "<div class='receipt'>";
out += `<h2>Sunrise Public School</h2>`;
out += `<p>${s.name} — Class ${s.className} — Roll ${s.roll}</p>`;
let total = 0;
for (const item of items) {
out += `<row>${item.label}: Rs.${item.amount}</row>`;
total += item.amount;
}
out += `<b>Total: Rs.${total}</b>`;
out += "</div>";
return out;
}
}
class SmsReceipt {
generate(s: Student, items: FeeItem[]): string {
let out = "SUNRISE PUBLIC SCHOOL\n";
out += `${s.name} | Cl ${s.className} | Roll ${s.roll}\n`;
let total = 0;
for (const item of items) {
out += `${item.label}: Rs.${item.amount}\n`;
total += item.amount;
}
out += `TOTAL: Rs.${total}`;
// bug fixed here last month... but not in HtmlReceipt:
return out.trim();
}
}Procedure টা identical: school header, student line, total accumulate করতে fee items-এর উপর loop, total line, শেষ। শুধু প্রতিটা piece-এর formatting আলাদা। দুটো movement apply করো — same-named step extract করো, তারপর skeleton তোলো:
// AFTER: the algorithm lives once; formats fill in the blanks
abstract class FeeReceipt {
generate(s: Student, items: FeeItem[]): string { // template method
let out = this.header();
out += this.studentLine(s);
let total = 0;
for (const item of items) {
out += this.feeLine(item);
total += item.amount;
}
out += this.totalLine(total);
return this.finish(out);
}
protected abstract header(): string;
protected abstract studentLine(s: Student): string;
protected abstract feeLine(item: FeeItem): string;
protected abstract totalLine(total: number): string;
protected finish(out: string): string { return out; } // hook
}
class HtmlReceipt extends FeeReceipt {
protected header() { return "<div class='receipt'><h2>Sunrise Public School</h2>"; }
protected studentLine(s: Student) {
return `<p>${s.name} — Class ${s.className} — Roll ${s.roll}</p>`;
}
protected feeLine(i: FeeItem) { return `<row>${i.label}: Rs.${i.amount}</row>`; }
protected totalLine(t: number) { return `<b>Total: Rs.${t}</b></div>`; }
}
class SmsReceipt extends FeeReceipt {
protected header() { return "SUNRISE PUBLIC SCHOOL\n"; }
protected studentLine(s: Student) {
return `${s.name} | Cl ${s.className} | Roll ${s.roll}\n`;
}
protected feeLine(i: FeeItem) { return `${i.label}: Rs.${i.amount}\n`; }
protected totalLine(t: number) { return `TOTAL: Rs.${t}`; }
protected finish(out: string) { return out.trim(); } // SMS-only need
}তিনটা চুপচাপ জয় লক্ষ্য করো। Total-accumulation logic — actual business logic — এখন একবার আছে, তাই "wrong total" শ্রেণির bug শুধু এক জায়গায় হতে পারে। trim() fix যেটা SMS পেয়েছিল আর HTML মিস করেছিল, এখন সেটা honest, visible, named পার্থক্য (finish) — কোনো accident নয়। আর school পরের term-এ WhatsApp receipt যোগ করলে, নতুন class হবে পাঁচটা ছোট formatting method — algorithm free পাওয়া যাবে।
শেষ দাবিটা measure করো। প্রতিটা নতুন receipt format-এর জন্য কতটা লিখতে হবে, আগে আর পরে:
পুরো school project-এর অভিজ্ঞতা, যেভাবে team এটা বেঁচেছে:
Python-এ একই রিফ্যাক্টরিং 🐍
Python abc দিয়ে result টা বিশেষভাবে cleanly প্রকাশ করা যায়। এখানে একটা data importer যেটা file পড়ে database-এ নেয় — CSV আর JSON version পুরো pipeline duplicate করেছিল; শুধু parsing step-ই আসলে আলাদা ছিল:
from abc import ABC, abstractmethod
class FileImporter(ABC):
def run(self, path: str) -> int: # the template method
raw = self._read(path) # shared
records = self._parse(raw) # the blank: varies per format
valid = [r for r in records if self._is_valid(r)] # shared
self._before_save(valid) # hook: default does nothing
return self._save(valid) # shared
def _read(self, path: str) -> str:
with open(path, encoding="utf-8") as f:
return f.read()
@abstractmethod
def _parse(self, raw: str) -> list[dict]: ...
def _is_valid(self, record: dict) -> bool:
return bool(record.get("id"))
def _before_save(self, records: list[dict]) -> None:
pass # hook — override if needed
def _save(self, records: list[dict]) -> int:
# one shared, well-tested save path for every format
return database.bulk_insert(records)
class CsvImporter(FileImporter):
def _parse(self, raw: str) -> list[dict]:
header, *rows = raw.splitlines()
keys = header.split(",")
return [dict(zip(keys, row.split(","))) for row in rows]
class JsonImporter(FileImporter):
def _parse(self, raw: str) -> list[dict]:
import json
return json.loads(raw)
def _before_save(self, records: list[dict]) -> None:
records.sort(key=lambda r: r["id"]) # JSON feeds arrive unsortedPython-specific কিছু নোট:
@abstractmethodআরABCথেকে inherit করা TypeScript-এরabstract-এর মতো compiler-style safety দেয়।_parseভুলে যাওয়া subclass instantiate করলে সাথে সাথে raise হয়, কোনো পরবর্তী call-এ নয়।- Leading underscore signal করে "step method — template দ্বারা call হয়, client দ্বারা নয়।" Template (
run) কে একমাত্র public entry point রাখো যাতে কেউ অর্ধেক algorithm invoke না করে। - Hook গুলো এখানে idiomatic:
_before_savedefault-এpass, আর যে subclass care করে শুধু সেটাই override করে। - Python-এর standard library নিজেই এই exact shape-এ ভরা — যেমন,
unittest.TestCase.run()হলো একটা template method যেটা fixed order-এsetUp, test, আরtearDowncall করে। তুমি এই pattern আগে থেকেই ব্যবহার করে আসছ।
IDE সাপোর্ট 🛠️
Mainstream IDE-তে কোনো একক "Form Template Method" button নেই — এটা একটা composite refactoring। কিন্তু প্রতিটা ingredient automated, যেটা প্রতিটা step safe রাখে:
- IntelliJ IDEA / PyCharm / Rider: Extract Method (Ctrl+Alt+M) varying step গুলো isolate করে; Pull Members Up unified skeleton তোলে আর একই dialog-এ remaining step গুলো
abstractচিহ্নিত করতে পারে। Extract Superclass parent তৈরি করে যখন duplicated class গুলোর কোনোটার parent নেই। - ReSharper (Visual Studio, C#): Extract Method, Pull Members Up (member-প্রতি "Make abstract" checkbox সহ), আর Extract Superclass পুরো sequence cover করে।
- VS Code (TypeScript): built-in Extract to method refactoring Step 2 handle করে; pull-up manual, কিন্তু TypeScript-এর
abstractkeyword আর compiler verify করে কিছু miss হয়নি — কোনো step missing subclass compile করতে refuse করবে।
আসলে research literature এটাকে automate করার জন্য সবচেয়ে কঠিন refactoring গুলোর একটা বলে, কারণ কোনটা একই step সেই সিদ্ধান্তটা human judgment দরকার। IDE code move করে; তুমি meaning সরবরাহ করো।
সুবিধা আর ঝুঁকি ⚖️
| সুবিধা | ঝুঁকি / cost |
|---|---|
| Structural duplication সরিয়ে দেয় যা line-by-line diff tool দেখতেও পায় না | Step order সত্যিই identical না হলে, একটা template force করলে দুটো procedure-ই বিকৃত হয় |
| Algorithm-এর order একটা জায়গায় থাকে — একবার fix করো, প্রতিটা variant উপকৃত হয় | Subclass গুলো parent-এর calling order-এর উপর নির্ভর করে: template edit করলে চুপচাপ override ভেঙে যেতে পারে (fragile base class) |
| নতুন variant হলো ছোট class — শুধু blank, কখনো skeleton নয় | ডজন খানেক one-line hook-এ over-fragment করলে duplication-এর চেয়েও readability বেশি ক্ষতিগ্রস্ত হয় |
| Compiler enforce করে যে প্রতিটা variant প্রতিটা required step-এর উত্তর দেয় | Variation compile time-এ hierarchy-তে welded — step গুলো runtime-এ swap করা যায় না |
| বিখ্যাত Template Method pattern তৈরি করে, named extension point সহ | প্রতিটা class method name-প্রতি শুধু একটা template support করে; competing variation Strategy demand করতে পারে |
কোন smells এটা সারায়? 👃
| Smell | Form Template Method কীভাবে সাহায্য করে |
|---|---|
| Duplicate Code | Duplicated skeleton — diff-এর কাছে invisible — একটা template method-এ collapse হয় |
| Shotgun Surgery | Procedure-এ পরিবর্তন প্রতিটা subclass copy-র বদলে একটা template edit করে |
| Divergent copies | Variant গুলো physically order-এ drift করতে পারে না, কারণ order-এর একটাই copy আছে |
| Oversized subclass | প্রতিটা subclass তার genuine পার্থক্যে সঙ্কুচিত হয় — কয়েকটা ছোট step method |
| Lazy Class (প্রতিরোধ) | নতুন variant হলো ছোট কিন্তু meaningful class: তাদের প্রতিটা লাইন real পার্থক্য |
দ্রুত revision box 📦
+------------------------------------------------------------------+
| FORM TEMPLATE METHOD - REVISION CARD |
+------------------------------------------------------------------+
| Problem : sibling subclasses run the SAME STEPS in the SAME |
| ORDER, differing only in step details. |
| (Two biryani recipes, four identical steps.) |
| |
| Solution : 1. extract each differing part into a method with |
| the SAME NAME in every subclass |
| 2. when skeletons are textually identical, |
| PULL UP the method -> the TEMPLATE METHOD |
| 3. declare varying steps abstract on the parent |
| 4. optional steps become HOOKS (default body) |
| |
| Produces : the Template Method design pattern |
| (Hollywood principle: don't call us, we call you) |
| Rule : skeletons must MATCH EXACTLY before pulling up |
| Not for : differing step orders, or steps that must swap |
| at runtime (use Strategy / delegation instead) |
+------------------------------------------------------------------+অনুশীলন ✏️
তোমার পালা। ধরো একটা e-commerce backend দুটো courier-এর মাধ্যমে order ship করে, আর নিচের দুটো method ইতিমধ্যে একবার drift করেছে (খুঁজে বের করো):
class BlueDartShipper {
ship(order: Order): Label {
if (!order.address.pincode) throw new Error("Pincode required");
const weight = order.items.reduce((w, i) => w + i.weightKg, 0);
const cost = 50 + weight * 12; // BlueDart pricing
const tracking = `BD-${order.id}-${Date.now()}`; // BlueDart format
audit.log(`Shipped ${order.id} via BlueDart`);
return new Label(order.address, cost, tracking);
}
}
class DelhiveryShipper {
ship(order: Order): Label {
if (!order.address.pincode) throw new Error("Pincode required");
const weight = order.items.reduce((w, i) => w + i.weightKg, 0);
const cost = weight <= 1 ? 40 : 40 + (weight - 1) * 15; // Delhivery pricing
const tracking = `DLV${order.id}X${order.address.pincode}`; // Delhivery format
return new Label(order.address, cost, tracking);
// note: no audit.log here — drift or design?
}
}এর মধ্য দিয়ে কাজ করো:
- প্রতিটা লাইন shared অথবা varying চিহ্নিত করো। তোমার ঠিক দুটো genuinely varying step (pricing আর tracking format) আর একটা সন্দেহজনক পার্থক্য (missing audit log) পাওয়া উচিত। সিদ্ধান্ত নাও: missing log কি accidental drift যেটা repair করতে হবে, নাকি real পার্থক্য যেটা hook হিসেবে preserve করতে হবে?
- উভয় class-এ identical signature সহ
calculateCost(weight)আরtrackingCode(order)extract করো। প্রতিটা extraction-এর পরে compile আর test করো। - দুটো
shipbody diff করো। Character for character match করলে, একটাShippersuperclass তৈরি করো আরshipকে template method হিসেবে pull up করো। - দুটো varying step
protected abstractdeclare করো। Audit log real পার্থক্য হলে, empty default সহ একটাafterShip(order)hook যোগ করো আরBlueDartShipper-এ override করো। - Payoff prove করো: flat Rs. 60 pricing আর tracking format
EK/<id>সহ একটাEkartShipperযোগ করো। তুমি কতটা লিখলে গণনা করো — দশের নিচে হওয়া উচিত, যেমন চিত্র ৮ প্রতিশ্রুতি দিয়েছিল — আর confirm করো wiring-up ছাড়া কোনো existing file edit করোনি। - তোমার
Shipper-এর চিত্র ৬-এর মতো sequence diagram sketch করো: কোন call গুলো template থেকে নিচে যায়, আর কোন results উপরে আসে? কোনো arrow subclass থেকে template-এর algorithm-এর দিকে গেলে, Hollywood principle ভেঙেছ — fix করো। - Bonus চিন্তা: business এখন চায় checkout সময় প্রতিটা order-এ courier বেছে নেওয়া, আর একটা "rate comparison" screen যেটা এক order-এ সব pricing rule চালায়। Template method এখনো fit করে, নাকি variation সবে swappable object হওয়ার দাবি করছে? এক বাক্যে বলো — আর সেই বাক্যটাই পরের lesson-এর দরজা, Replace Inheritance with Delegation।
তোমার ধাপ ১-এর উত্তর যদি "missing audit log হলো ঠিক কীভাবে invisible structural duplication drift ঘটায় — fix করো নাহলে নাম দাও, কখনো accidental রেখো না," তাহলে তুমি এই পুরো lesson-এর গভীরতম বিষয়টা বুঝেছ। ফাতিমা একটা correction ছাড়াই তোমার রেসিপি কার্ড print করত। শাবাশ।
সচরাচর জিজ্ঞাসা
- Form Template Method আর শুধু Pull Up Method ব্যবহারের মধ্যে পার্থক্য কী?
- Pull Up Method তখন কাজ করে যখন দুটো subclass method হুবহু একই — পুরো body-টা parent-এ তুলে দেওয়া যায়। Form Template Method আরও কঠিন কেসটা handle করে, যেখানে method দুটো শুধু structure-এর দিক থেকে একই: step-এর order একই, কিন্তু step-এর details আলাদা। আগে method দুটোকে reshape করতে হয় যেন skeleton মিলে যায়, তারপর skeleton pull up করো, আলাদা step গুলো subclass-এ রেখে দাও overridable operation হিসেবে।
- Form Template Method আর Template Method design pattern কি একই জিনিস?
- এই দুটো একই বিল্ডিং-এ যাওয়ার দুটো আলাদা রাস্তা। Template Method pattern হলো গন্তব্য — একটা superclass method যেটা algorithm-এর skeleton define করে, আর subclass গুলো কিছু step পূরণ করে। Form Template Method হলো সেই রিফ্যাক্টরিং রাস্তা যেটা existing duplicated code থেকে শুরু করে সেই গন্তব্যে পৌঁছায়, up-front design না করে।
- Hook method কী? Abstract step-এর বদলে কখন সেটা ব্যবহার করব?
- Abstract step মানে প্রতিটা subclass-কে বাধ্য করা implementation দিতে। Hook হলো এমন একটা step যার একটা sensible default আছে — অনেক সময় empty body — subclass চাইলে override করতে পারে, কিন্তু বাধ্য নয়। যেসব parts-এ প্রতিটা variant সত্যিই আলাদা সেখানে abstract step ব্যবহার করো। আর optional extension point-এ hook ব্যবহার করো যেখানে শুধু কিছু variant-এর দরকার।
- আমার দুটো method-এ step গুলো মিলে, কিন্তু order আলাদা — তখন কী করব?
- তাহলে থামো — এই রিফ্যাক্টরিং এখানে লাগবে না। Template method-এর পুরো মূল্যটাই হলো step-এর order সব subclass-এ genuinely invariant থাকে। ভিন্ন order-এর দুটো procedure-কে এক skeleton-এ জোর করে ঢোকালে দুটোই নষ্ট হয়ে যায়। হয় আলাদাভাবে refactor করো, নাহলে এমন ছোট shared অংশ খোঁজো যার order সত্যিই fixed।
- Template method-এর বদলে Strategy pattern কখন ব্যবহার করব?
- যখন varying step গুলো runtime-এ বদলাতে হবে, স্বাধীনভাবে combine করতে হবে, বা আলাদাভাবে test করতে হবে। Template method compile time-এ variation-কে class hierarchy-তে আটকে দেয়। Live object-এ step swap করতে হলে সেই step-কে আলাদা object হিসেবে রাখো — এটাই Strategy, যেটায় Replace Inheritance with Delegation দিয়ে পৌঁছানো যায়।
আরো দেখো
সম্পর্কিত পাঠ
Template Method Pattern: চা আর কফি, একই ধাপ
চায়ের দোকানের গল্প দিয়ে Template Method design pattern শেখো — সহজ TypeScript আর C# কোড, hooks, diagram, বাস্তব উদাহরণ আর practice task সহ।
Duplicate Code: ৫০টা বিয়ের কার্ডে হাতে লেখা একই ঠিকানা
বিয়ের কার্ডের গল্প দিয়ে Duplicate Code smell বোঝো। DRY, Rule of Three, আর Extract Method দিয়ে copy-paste কোডের বিপদ থেকে বাঁচো।
Pull Up Method: পুরো স্কুলের জন্য একটাই নির্দেশিকা
Pull Up Method refactoring শেখো স্কুলের ছুটির আবেদনের গল্পের মাধ্যমে — subclass-এ duplicate হয়ে যাওয়া method-গুলো superclass-এ তুলে আনো, TypeScript আর C#-এ safe steps সহ, IDE dialog আর কখন Form Template Method বেছে নেবে সেটাসহ।
Extract Method: একটা বিশাল ফাংশনকে ছোট ছোট নামওয়ালা helper-এ ভাগ করো
Extract Method ধাপে ধাপে শিখে নাও। একটা লম্বা ফাংশন থেকে এলোমেলো block বের করে তাকে একটা পরিষ্কার নাম দাও, আর তোমার কোডকে একটা সহজ to-do লিস্টের মতো পড়ার যোগ্য করে তোলো।