Visitor Pattern: যে ডাক্তার প্রতিটি class-এ যায়
স্কুলের health check-up গল্পের মাধ্যমে Visitor design pattern শেখো — double dispatch সহজ ভাষায়, TypeScript আর C# কোড, বাস্তব উদাহরণ, আর practice।
বিদ্যা মন্দির স্কুলে health check-up দিন
ধরো জুলাই মাসের প্রথম সোমবার। তোমাদের স্কুলে আজ health check-up দিন। ডাক্তার ডা. রাহেলা তার ব্যাগ নিয়ে আসলেন, গেটে sign করলেন, আর class-এর পর class ঘুরতে শুরু করলেন। প্রধান শিক্ষিকা নাসরিন ম্যাডাম এগিয়ে গিয়ে প্রতিটা দরজায় টোকা দিচ্ছেন।
দেখো ডাক্তার প্রতিটা class-এ কী করেন:
- Class 2 (ছোটরা) — উচ্চতা, ওজন মাপেন, আর পোলিও চার্ট দেখেন।
- Class 7 (মাঝের দল) — চোখ পরীক্ষা করেন (এত mobile দেখে!) আর ভারী school bag-এর জন্য posture চেক করেন।
- Class 11 (বড়রা) — blood pressure মাপেন, exam stress আর ঘুম নিয়ে কথা বলেন।
একই ডাক্তার, প্রতিটা class-এ একবার করে যান, কিন্তু প্রতিটা class-এর জন্য আলাদা কাজ। আর মজার ব্যাপার হলো — class room গুলো নিজেরা তার জন্য কিছু বদলায় না। তারা শুধু একটাই কাজ করে: দরজা খোলে আর বলে, "আসুন ডাক্তার, আমরা Class 7।" ডাক্তার নিজেই জানেন কোন class-এ কী করতে হবে।
পরের সোমবার আসে রুবেল ভাই, স্কুলের ফটোগ্রাফার। সেও নাসরিন ম্যাডামের পেছনে একই দরজা দিয়ে ঘোরে। ছোটরা দুই সারিতে বসে, মাঝেরা bench-এ উঠে দাঁড়ায়, বড়রা আলাদা আলাদা portrait দেয় yearbook-এর জন্য। এখানেও — class room গুলো বদলায়নি। একই দরজা, একই কথা — "আমরা Class 7" — আর visitor নিজে নিজের কাজ করে নিল।
এখানেই magic টা। স্কুল একটা সম্পূর্ণ নতুন operation (photography) যোগ করল — একটা class-ও না ছুঁয়ে। আগামী মাসে লাইব্রেরিয়ান আসলে, sports coach আসলে — class room গুলোতে শূন্য পরিবর্তন। class room শুধু একটাই কাজ শিখেছে: দরজা খোলো, বলো তুমি কে।
এটাই Visitor pattern: element class গুলো (class room) stable রাখো, আর প্রতিটা নতুন operation (ডাক্তার, ফটোগ্রাফার, লাইব্রেরিয়ান) আলাদা visitor object-এ রাখো — যে জানে প্রতিটা element type-এর সাথে কী করতে হয়।
Visitor pattern আসলে কী?
Visitor হলো একটা behavioral design pattern। সহজ কথায়: তোমার কাছে একটা fixed class family আছে (Circle/Rectangle, বা JuniorClass/MiddleClass/SeniorClass) আর তুমি সেগুলোর উপর বারবার নতুন operation করতে চাও — প্রতিটা class-এ method যোগ না করেই।
কীভাবে করবে? এই ধাপগুলো follow করো:
- একটা Visitor interface বানাও — প্রতিটা element type-এর জন্য একটা method:
visitJunior(...),visitMiddle(...),visitSenior(...)। - প্রতিটা element-এ একটা ছোট্ট method দাও —
accept(visitor)— যার একমাত্র কাজ হলো নিজের type অনুযায়ী visit method call করা:JuniorClass-এর ভেতরেacceptcall করবেvisitor.visitJunior(this)। - প্রতিটা নতুন operation হবে একটা নতুন concrete visitor class — সে সব visit method implement করবে। পুরো operation একই জায়গায় থাকবে।
Element class একবার লেখা হলে আর ছোঁয়া হয় না। নতুন operation মানে নতুন visitor class। এটাই Open/Closed Principle — operations-এর উপর apply করা।
Gang of Four বই (১৯৯৪)-এ এই pattern আছে। এর আরেকটা নাম আছে যে trick-টা এটাকে power দেয়: Double Dispatch। নিচে সেটা ধীরে ধীরে ব্যাখ্যা করব।
এক লাইনে: Visitor = "কী করতে হবে" সেটা data class থেকে বের করে আলাদা operation object-এ রাখো; data class শুধু বলে "আমি X type, আমার উপর তোমার X কাজটা করো।"
চরিত্রগুলো:
- Element interface —
accept(visitor)declare করে (class room-এর দরজা)। - Concrete elements —
JuniorClass,MiddleClass,SeniorClass; প্রতিটারacceptমাত্র এক লাইন। - Visitor interface — প্রতিটা element type-এর জন্য একটা
visitXmethod। - Concrete visitors —
DoctorVisitor,PhotographerVisitor— প্রতিটা operation-এর জন্য একটা।
যে সমস্যাটা এটা সমাধান করে
ধরো check-up দিনটাকে কষ্টের ভাবে লিখি। প্রতিটা কাজ প্রতিটা class-এর method হিসেবে লিখলে কী হয়:
// BAD CODE: every new school activity bloats every class
class JuniorClass {
healthCheck(): void { console.log("Height, weight, polio chart"); }
takePhoto(): void { console.log("Two neat rows, say cheese"); }
issueLibraryCards(): void { console.log("Picture books section"); }
// next month: fitnessTest()? feeReport()? magazineSurvey()? ...
}
class MiddleClass {
healthCheck(): void { console.log("Eyesight and posture check"); }
takePhoto(): void { console.log("Standing on benches"); }
issueLibraryCards(): void { console.log("Junior fiction section"); }
}
class SeniorClass {
healthCheck(): void { console.log("BP check, sleep advice"); }
takePhoto(): void { console.log("Individual yearbook portraits"); }
issueLibraryCards(): void { console.log("Reference section access"); }
}তিনটা class, তিনটা operation — এখনই নয়টা method, আর school year তো সবে শুরু হলো। সমস্যাগুলো দেখো:
- প্রতিটা নতুন operation প্রতিটা class খুলতে বাধ্য করে। Fitness test এলো? তিনটা class খোলো, তিনটায়
fitnessTest()যোগ করো। দশটা element type থাকলে? প্রতিটা operation-এ দশটা edit, সারাজীবন। - Class গুলো এমন জিনিস শেখে যা তাদের জানার দরকার নেই। একটা class room কেন photography angle আর library section জানবে?
JuniorClass-এর কাজ হওয়া উচিত student আর timetable — photography বা library-র business না। - একটা operation অনেক file-এ ছড়িয়ে থাকে। "Photography plan" তিনটা fragment হয়ে তিনটা class-এ আছে। পুরো photo logic পড়তে গেলে তিনটা file-এ লাফ দিতে হয়।
method count হলো operations গুণ types — multiplication table-এর মতো বাড়ে। দেখো element class-এর ভেতরে method কীভাবে বাড়ে operations বাড়ার সাথে — আর Visitor version-এ element গুলো সবসময় মাত্র একটা accept রাখে:
আরেকটা quick fix — বড় function-এ type check — সেটাও ugly:
// ALSO BAD: type-checking ladder
function healthCheck(room: SchoolClass): void {
if (room instanceof JuniorClass) { /* ... */ }
else if (room instanceof MiddleClass) { /* ... */ }
else if (room instanceof SeniorClass) { /* ... */ }
// add a new class and the compiler will NOT remind you here
}পরের বছর কেউ NurseryClass যোগ করলে এই ladder চুপ করে থাকবে — কোনো compile error নেই, শুধু ৩০ জন চার বছরের বাচ্চার health check-up miss হবে।
আমরা চাই — class-এর set stable রেখে operation যোগ করতে। আর প্রতিটা operation-এর logic একটা জায়গায় রাখতে। এটাই Visitor-এর offer।
কীভাবে কাজ করে, ধাপে ধাপে
- Hierarchy stable কিনা নিশ্চিত করো। Visitor তখনই লাভজনক যখন element type কমই বদলায়। Junior, Middle, Senior — বছরের পর বছর fixed। ভালো।
- Visitor interface declare করো — প্রতিটা concrete element-এর জন্য একটা method:
visitJunior(j),visitMiddle(m),visitSenior(s)। আলাদা নাম রাখলে সব clear থাকে। - Element interface-এ
accept(visitor)যোগ করো। প্রতিটা class room promise করে visitor নেবে। - প্রতিটা element-এ
acceptimplement করো — এক লাইন করে।JuniorClass.accept(v)call করেv.visitJunior(this)। Pattern element-এ এটুকুই যোগ করে। - প্রতিটা operation-এর জন্য একটা concrete visitor লেখো।
DoctorVisitorতিনটা visit method implement করে;PhotographerVisitorও। যেকোনো running total বা buffer visitor-এর field হিসেবে থাকে। - Client structure-এর মধ্য দিয়ে হাঁটে। class room গুলোতে loop করো,
room.accept(visitor)call করো, তারপর visitor থেকে result পড়ো।
গোপন engine: double dispatch
accept-এর এই নাচটা কেন? সরাসরি visitor.visitJunior(room) call করলে কী সমস্যা? কারণ client সাধারণত room গুলো SchoolClass general type হিসেবে রাখে — কোনটা concrete class জানে না। Compiler SchoolClass variable থেকে visitJunior বনাম visitSenior বেছে নিতে পারে না।
সঠিক behaviour নির্ভর করে দুইটা runtime type-এর উপর একসাথে — element-এর type আর visitor-এর type। সাধারণ method call শুধু একটা type দেখে (receiver)। তাই Visitor দুইটা সাধারণ call জুড়ে দেয়:
room.accept(visitor)— room-এর real type দিয়ে select হয়। আমরা যাই, ধরো,JuniorClass.accept-এর ভেতরে — যেখানেthisনিশ্চিতভাবেJuniorClass।visitor.visitJunior(this)— visitor-এর real type দিয়ে select হয়। ডাক্তার হোক বা ফটোগ্রাফার, সঠিক জন সাড়া দেয়।
স্কুলের দুই-প্রশ্নের protocol মনে করো। নাসরিন ম্যাডাম দরজায় টোকা দেন; class room বলে "আমরা Class 2" (প্রথম dispatch — room type দিয়ে); তারপর visitor তার নিজের checklist-এর সঠিক page খোলেন (দ্বিতীয় dispatch — visitor type দিয়ে)।
দুইটা single dispatch জুড়ে দিলে একটা double dispatch হয় — কোনো instanceof check ছাড়াই। Type system সব বেছে নেয়। এজন্যই Visitor type-safe: নতুন NurseryClass যোগ করলে আর তার accept দিলে, সব visitor compile fail করবে যতক্ষণ না নতুন case handle করা হয়। Compiler তোমার checklist হয়ে যায়।
একটু গভীরে যাই — double dispatch ঠিকঠাক: mainstream OO language (Java, C#, C++, TypeScript) হলো single-dispatch: method body বেছে নেওয়া হয় runtime-এ একটামাত্র argument দিয়ে — dot-এর আগে যে receiver। Visitor হলো single-dispatch machinery থেকে multiple dispatch behaviour বের করার standard workaround। কিছু language-এ real multiple dispatch আছে — CLOS (Common Lisp) আর Julia সব argument-এর runtime type দিয়ে method select করে, তাই তাদের Visitor pattern দরকারই নেই। C# dynamic দিয়ে কিছুটা পথ যায়, আর modern pattern matching আরেকটা রাস্তা। Exam-এ crisp sentence: Visitor simulates double dispatch by chaining two single dispatches — accept dispatches on the element, visit dispatches on the visitor।
বাস্তব code উদাহরণ
TypeScript-এ পুরো school দিন — দুইটা visitor, একদম আলাদা কাজ, একই classroom object-এর উপর:
// ---------- Elements (stable — written once) ----------
interface SchoolClass {
accept(v: SchoolVisitor): void;
}
class JuniorClass implements SchoolClass {
constructor(public section: string, public students: number) {}
accept(v: SchoolVisitor): void {
v.visitJunior(this); // "we are juniors — do your junior thing"
}
}
class MiddleClass implements SchoolClass {
constructor(public section: string, public students: number) {}
accept(v: SchoolVisitor): void {
v.visitMiddle(this);
}
}
class SeniorClass implements SchoolClass {
constructor(public section: string, public students: number) {}
accept(v: SchoolVisitor): void {
v.visitSenior(this);
}
}
// ---------- Visitor interface ----------
interface SchoolVisitor {
visitJunior(j: JuniorClass): void;
visitMiddle(m: MiddleClass): void;
visitSenior(s: SeniorClass): void;
}
// ---------- Visitor 1: Dr. Kapoor ----------
class DoctorVisitor implements SchoolVisitor {
private checked = 0; // visitors can accumulate state!
visitJunior(j: JuniorClass): void {
console.log(`Class ${j.section}: height, weight, polio chart (${j.students} kids)`);
this.checked += j.students;
}
visitMiddle(m: MiddleClass): void {
console.log(`Class ${m.section}: eyesight and posture check (${m.students} kids)`);
this.checked += m.students;
}
visitSenior(s: SeniorClass): void {
console.log(`Class ${s.section}: BP check and sleep advice (${s.students} kids)`);
this.checked += s.students;
}
report(): void {
console.log(`Doctor's report: ${this.checked} students checked.\n`);
}
}
// ---------- Visitor 2: Farhan (NEW operation, ZERO element edits) ----------
class PhotographerVisitor implements SchoolVisitor {
private photos = 0;
visitJunior(j: JuniorClass): void {
console.log(`Class ${j.section}: two neat rows, say cheese!`);
this.photos += 1; // one group photo
}
visitMiddle(m: MiddleClass): void {
console.log(`Class ${m.section}: standing on benches, group photo`);
this.photos += 1;
}
visitSenior(s: SeniorClass): void {
console.log(`Class ${s.section}: individual yearbook portraits`);
this.photos += s.students; // one photo per senior
}
report(): void {
console.log(`Photographer's report: ${this.photos} photos taken.\n`);
}
}
// ---------- The school (client) ----------
const classrooms: SchoolClass[] = [
new JuniorClass("2-A", 30),
new MiddleClass("7-B", 35),
new SeniorClass("11-C", 25),
];
console.log("--- Monday: health check-up day ---");
const doctor = new DoctorVisitor();
for (const room of classrooms) room.accept(doctor); // double dispatch!
doctor.report();
console.log("--- Next Monday: photo day ---");
const photographer = new PhotographerVisitor();
for (const room of classrooms) room.accept(photographer); // same rooms, new job
photographer.report();Output:
--- Monday: health check-up day ---
Class 2-A: height, weight, polio chart (30 kids)
Class 7-B: eyesight and posture check (35 kids)
Class 11-C: BP check and sleep advice (25 kids)
Doctor's report: 90 students checked.
--- Next Monday: photo day ---
Class 2-A: two neat rows, say cheese!
Class 7-B: standing on benches, group photo
Class 11-C: individual yearbook portraits
Photographer's report: 27 photos taken.তিনটা জিনিস লাল pen দিয়ে underline করো:
- একই তিনটা classroom object দুইটা সম্পূর্ণ আলাদা operation serve করল — একটাও edit ছাড়া। পরের মাসে
LibrarianVisitorমানে একটাই নতুন class, আর কিছু না। - কোনো
instanceof, কোনো cast, কোনো type ladder নেই। Loop room গুলো plainSchoolClassহিসেবে ধরে। Double dispatch handshake নিজেইvisitJuniorবনামvisitSeniorবেছে নিল। - প্রতিটা visitor তার নিজের state জমা করল — ডাক্তার student count করলেন, ফটোগ্রাফার photo count করল। Visitor object হলো traversal-এ collect করা running total, buffer, আর error list-এর natural ঘর।
একটা classroom-এর দৃষ্টিকোণ থেকে visit দিন হলো একটা ছোট, শান্ত state machine। Room কখনো visitor-এর ব্যবসা শেখে না — শুধু host করে আর পড়ায় ফিরে যায়:
C#-এ একই ব্যাপার
Classic graphics version, খুব ছোট — shape গুলো area report করতে পারে বা নিজেদের export করতে পারে, কিন্তু কোনো skill নিজেদের ভেতরে রাখে না:
interface IShape { void Accept(IShapeVisitor v); }
class Circle : IShape
{
public double Radius;
public void Accept(IShapeVisitor v) => v.VisitCircle(this);
}
class Rect : IShape
{
public double W, H;
public void Accept(IShapeVisitor v) => v.VisitRect(this);
}
interface IShapeVisitor
{
void VisitCircle(Circle c);
void VisitRect(Rect r);
}
class AreaVisitor : IShapeVisitor
{
public double Total;
public void VisitCircle(Circle c) => Total += Math.PI * c.Radius * c.Radius;
public void VisitRect(Rect r) => Total += r.W * r.H;
}
class XmlVisitor : IShapeVisitor
{
public string Xml = "";
public void VisitCircle(Circle c) => Xml += $"<circle r='{c.Radius}'/>";
public void VisitRect(Rect r) => Xml += $"<rect w='{r.W}' h='{r.H}'/>";
}
// Usage
var shapes = new List<IShape> { new Circle { Radius = 2 }, new Rect { W = 3, H = 4 } };
var area = new AreaVisitor();
foreach (var s in shapes) s.Accept(area);
Console.WriteLine($"Total area: {area.Total:F2}"); // 24.57
var xml = new XmlVisitor();
foreach (var s in shapes) s.Accept(xml);
Console.WriteLine(xml.Xml); // <circle r='2'/><rect w='3' h='4'/>Geometry class গুলো pure geometry থাকল। XML formatting আর area math প্রতিটা একটা cohesive class-এ আছে। একটু curious হলে জেনে রাখো: pattern matching যে language-এ আছে (C# switch expression on type, Rust match, Swift enum) সেগুলোতে কখনো কখনো classic Visitor-এর বদলে switch দিয়ে same two-type selection করা যায় — তাই সেই community-গুলো মাঝে মাঝে এই pattern skip করে।
Python-এ আরেকবার: একটা ছোট compiler
Visitor-এর আসল ঘর হলো compiler। এই পাঁচ মিনিটের taste দেখো: একটা ছোট arithmetic expression tree-তে একই node-এর উপর দুইটা visitor — একটা evaluate করে, একটা pretty-print করে। এটাই miniature-এ real compiler আর linter কাজ করার ভঙ্গি।
# ---------- Elements: a tiny AST (stable family) ----------
class Num:
def __init__(self, value): self.value = value
def accept(self, v): return v.visit_num(self)
class Add:
def __init__(self, left, right): self.left, self.right = left, right
def accept(self, v): return v.visit_add(self)
class Mul:
def __init__(self, left, right): self.left, self.right = left, right
def accept(self, v): return v.visit_mul(self)
# ---------- Visitor 1: evaluate the tree ----------
class EvalVisitor:
def visit_num(self, n): return n.value
def visit_add(self, a): return a.left.accept(self) + a.right.accept(self)
def visit_mul(self, m): return m.left.accept(self) * m.right.accept(self)
# ---------- Visitor 2: print the tree (new operation, zero node edits) ----------
class PrintVisitor:
def visit_num(self, n): return str(n.value)
def visit_add(self, a): return f"({a.left.accept(self)} + {a.right.accept(self)})"
def visit_mul(self, m): return f"({m.left.accept(self)} * {m.right.accept(self)})"
# The expression (2 + 3) * 4 as a tree
tree = Mul(Add(Num(2), Num(3)), Num(4))
print(tree.accept(PrintVisitor())) # ((2 + 3) * 4)
print(tree.accept(EvalVisitor())) # 20Recursion-টা লক্ষ্য করো: tree-shaped element-এর জন্য visitor নিজেই children-এর ভেতরে যায় child.accept(self) call করে। কাল TypeCheckVisitor বা OptimizeVisitor যোগ করলে — Num, Add, Mul class কখনো বদলাবে না। এজন্যই প্রতিটা serious compiler AST stable রাখে আর ডজন ডজন visitor ship করে।
Real software-এ কোথায় দেখতে পাবে
Visitor-এর আসল দুনিয়া হলো যেখানে stable node type-এর tree মেলে ever-growing operation list-এর সাথে:
- Compiler আর AST (সবচেয়ে classic উদাহরণ)। Compiler code parse করে Abstract Syntax Tree বানায় — fixed node type family (if-node, loop-node, expression-node)। তারপর একের পর এক pass সেই tree-এ হাঁটে: type checking, optimization, code generation, pretty printing। প্রতিটা pass একটা visitor। .NET compiler platform Roslyn ship করে
CSharpSyntaxVisitorআরCSharpSyntaxRewriterbase class; javac-এ আছে com.sun.source tree visitor; ANTLR তোমার grammar থেকে automatically visitor interface generate করে। - Linter আর code formatter। ESLint rule গুলো AST node type-এ subscribe করে আর traversal enter/leave করার সময় call পায় — তোমার JavaScript-এর উপর একটা visitor-style walk। Babel plugin গুলো literally visitor object হিসেবে লেখা হয়, প্রতিটা node type-এর জন্য একটা handler।
- Document আর scene-graph export। Document model (section, paragraph, image, table) হলো stable Composite tree; PDF export, HTML export, word-count, spell-check সব visitor হিসেবে walk করে। 3D engine-ও scene graph-এ similar pass করে।
- Serializer আর report writer — pricing engine, insurance rule pass, billing audit — প্রতিটা নতুন business report মানে entity class-এ method যোগ করার বদলে নতুন visitor।
- Open-source code পড়তে চাইলে। iluwatar visitor example দিয়ে শুরু করো, তারপর GitHub-এ একটা real ESLint rule দেখো — visit-per-node-type shape সাথে সাথে চিনতে পারবে।
কখন ব্যবহার করবে, কখন করবে না
| পরিস্থিতি | Visitor ব্যবহার করবে? |
|---|---|
| Element type stable, কিন্তু নতুন operation বারবার আসছে | হ্যাঁ — এটাই sweet spot (AST, document model) |
| একটা operation প্রতিটা concrete type-এ আলাদা আচরণ করতে হবে | হ্যাঁ — পুরো operation একটা class-এ থাকে |
| Serialization/reporting/pricing core data class থেকে বাইরে রাখতে হবে | হ্যাঁ — separation of concerns |
| Traversal-এ result জমাতে হবে (total, buffer, error list) | হ্যাঁ — visitor field-এ naturally ধরা যায় |
| নতুন element type বারবার আসছে | না — প্রতিটা নতুন type প্রতিটা visitor-এ edit আনবে |
| মাত্র একটা operation আছে, কমই বদলায় | না — প্রতিটা element-এ plain method সহজ |
| তোমার language-এ closed type-এর exhaustive pattern matching আছে | সম্ভবত না — match expression হয়তো cleaner |
| Visitor-এর element-এর private data দরকার | সাবধান — internals expose করতে হতে পারে |
প্রথম "না" row-টা আলাদাভাবে দেখার দরকার — বিখ্যাত expression problem। Software দুইটা axis-এ বাড়ে, আর প্রতিটা style মাত্র একটা axis সহজ করে:
| নতুন operation যোগ করা | নতুন type যোগ করা | |
|---|---|---|
| প্রতিটা class-এ plain method | কঠিন — প্রতিটা class edit করতে হয় | সহজ — একটা নতুন class লেখো |
| Visitor | সহজ — একটা নতুন visitor লেখো | কঠিন — প্রতিটা visitor edit করতে হয় |
একটু গভীরে — expression problem ঠিকঠাক: এই trade-off-এর একটা নাম আছে আর দীর্ঘ academic ইতিহাস; Philip Wadler ১৯৯৮ সালে term টা popularize করেন। চ্যালেঞ্জ হলো: existing code না ছুঁয়ে আর static type safety রেখে program-কে নতুন type আর নতুন operation দুইটাই দিয়ে extend করা। Plain OO method type সহজ করে operation কঠিন করে; Visitor (closed data type-এর উপর functional pattern matching-এর মতো) operation সহজ করে type কঠিন করে। Plain OO বা Visitor কেউই problem solve করে না — প্রতিটা একটা side বেছে নেয়। Genuine solution-এর জন্য fancier জিনিস লাগে (Haskell-এর type class, trait with default method, object algebra, multimethod)। Exam বা interview-এ gold sentence: Visitor converts the expression problem's hard axis from operations to types — choose it only when your project grows along the operations axis।
তোমার project কোথায় আছে map-এ দেখো:
ছাত্রছাত্রীরা যে ভুলগুলো করে
সবচেয়ে বড় ভুল: element family এখনো বাড়ছে অথচ Visitor ব্যবহার করা। যদি মনে হয় পরের মাসে NurseryClass আর তার পরের মাসে SportsClass আসবে — প্রতিটা addition প্রতিটা visitor ভাঙবে। Compile error storm কখনো কখনো useful (compiler force করে প্রতিটা operation নতুন case handle করতে), কিন্তু প্রতি sprint-এ এটা হলে Visitor ভুল pick ছিল — plain polymorphic method type বাড়ানো সহজ করে।
আরো কিছু trap:
acceptবাদ দিয়ে type-check করা। Client-এif (room instanceof JuniorClass) visitor.visitJunior(room)লিখলে pattern-এর type safety নষ্ট হয়।accept-এর পুরো point হলো কেউ manually type inspect করবে না।acceptmethod-এ real কাজ করা।acceptএক লাইন হওয়া উচিত:v.visitX(this)। অনেকে এখানে business logic ঢুকিয়ে দেয়, operation আবার element-এ ফিরে যায়।- কে traverse করবে সেটা ভুলে যাওয়া। Flat list-এর জন্য client loop করে। Tree-এর জন্য (Composite, যেমন আমাদের Python AST) visitor বা element নিজেই children-এ recurse করে। একবার decide করো, document করো, দুই জায়গায় duplicate করো না।
- প্রতিটা element-এর জন্য নতুন visitor বানানো। Visitor একবার বানাও, সব element-এর মধ্যে দিয়ে পাঠাও, তারপর accumulated result পড়ো। Loop-এর ভেতরে নতুন
AreaVisitorবানালে প্রতিটা একটা shape-এর অনেক total পাবে। - Element খুলে ফেলা। Visitor-এর জন্য যদি প্রতিটা element-এ বারোটা getter যোগ করতে হয়, design তোমার বিরুদ্ধে লড়ছে। হয় প্রয়োজনীয় data
visitmethod-এ pass করো, নয়তো ভাবো operation আসলে element-এর ভেতরেই থাকা উচিত কিনা।
accept-এর জন্য memory trick: element শুধু একটা প্রশ্নের উত্তর দিচ্ছে — "তুমি কে?" — আর সে উত্তর দেয় নিজের নামে visit method call করে। বাকি সব visitor-এর।
কাছের pattern গুলোর সাথে তুলনা
| Pattern | কী extract করা হয়? | Behaviour কীভাবে বেছে নেওয়া হয়? | সাধারণ ব্যবহার |
|---|---|---|---|
| Visitor | Type-specific behaviour-এর পুরো family (প্রতিটা element-এর জন্য একটা) | accept দিয়ে double dispatch | Stable hierarchy-তে নতুন operation |
| Plain methods | কিছু না — behaviour প্রতিটা class-এ থাকে | Single dispatch (সাধারণ virtual call) | Stable operation, বাড়তে থাকা type |
| Strategy | একটা algorithm একটা interface-এর পেছনে | Client একটা object install করে | Swappable single behaviour |
| Iterator | Traversal নিজেই | Caller একটা একটা করে element নেয় | Walking, logic caller-এ থাকে |
| Composite | Element-এর tree structure | — | Visitor যে structure-এ হাঁটে |
ছাত্রছাত্রীরা সবচেয়ে বেশি যেটা গুলিয়ে ফেলে: Visitor বনাম সরাসরি method যোগ করা। দুইটাই per-type behaviour দেয়। পার্থক্য শুধু code কোথায় থাকে আর কী সহজ:
- Element-এ method: প্রতিটা class নিজের
healthCheck()বহন করে। Type যোগ করা সহজ; operation যোগ করা মানে প্রতিটা class edit। Operation logic ছড়ানো। - Visitor: প্রতিটা operation তার per-type case গুলো একসাথে বহন করে। Operation যোগ করা সহজ; type যোগ করা মানে প্রতিটা visitor edit। Operation logic cohesive।
আর দুইটা friendly জুটি: Composite + Visitor হলো textbook pair — visitor composite tree (document, AST) walk করে, recursion accept-এ বা visitor নিজে করে, যেমন আমাদের Python compiler দেখাল। Iterator + Visitor ও একসাথে কাজ করে: iterator element deliver করে, প্রতিটা element visitor accept করে।
পুরো pattern এক পাতায়
এই mind map মুখস্থ করে আঁকতে পারলে — আর double dispatch branch জোরে বলে explain করতে পারলে — pattern তোমার হয়ে গেছে:
Quick revision box
+--------------------------------------------------------------+
| VISITOR PATTERN — REVISION |
+--------------------------------------------------------------+
| WHAT : Add new operations over a stable class family |
| without editing those classes |
| ACTORS : Elements with accept(v) + Visitor interface |
| (one visitX per element) + concrete visitors |
| KEY MOVE : element.accept(v) -> v.visitX(this) |
| = DOUBLE DISPATCH (two chained virtual calls) |
| ACCEPT : Always ONE line; never holds business logic |
| TRADE-OFF: Operations cheap to add, element types costly |
| (the expression problem — Visitor picks a side) |
| AVOID IF : Element hierarchy keeps growing |
| EXAMPLES : Compiler AST passes, ESLint/Babel rules, Roslyn |
| visitors, document exporters |
+--------------------------------------------------------------+Practice করার সময়
এবার তুমিই visitor হও:
- তৃতীয় visitor, শূন্য edit। এই post-এর school code নাও আর
LibrarianVisitorযোগ করো — junior-দের picture book card, middle-দের fiction card, senior-দের reference access, মোট card count। Game-এর নিয়ম:JuniorClass,MiddleClass, বাSeniorClassছোঁয়া যাবে না। ছুঁতে মন চাইলে — pattern আবার পড়ো! - Electricity meter reader।
House,Shop, আরFactorybuilding model করো, প্রতিটায়unitsConsumedfield আরacceptআছে।BillingVisitorলেখো (house-এ ৫ টাকা/unit, shop-এ ৮, factory-তে ১০, মোট bill জমা করে) আরInspectionVisitorলেখো (প্রতিটা building type-এর জন্য আলাদা safety checklist print করে)। একই building list-এ দুইটা run করো। - Trade-off feel করো (thinking task)। এবার exercise 2-এ চতুর্থ building type
Schoolযোগ করো। কতটা file বদলাতে হলো লিখে রাখো। তারপর দুই বাক্যে answer করো: প্রতি মাসে নতুন building type আসলে Visitor রাখবে নাকি plain method-এ ফিরবে? তোমার project expression problem-এর কোন axis-এ বাড়ছে? - College stretch। Python baby compiler-এ
Subnode আরTypeCheckVisitorযোগ করো যে তিন level-এর বেশি গভীর tree reject করে। আগে visitor যোগ করো (সহজ axis), তারপর node (কঠিন axis — কতটা class খুলতে হলো count করো)। উপরের expression problem table-এর সাথে তোমার experience connect করে এক বাক্য লেখো।
সচরাচর জিজ্ঞাসা
- Visitor pattern এক কথায় কী?
- Visitor pattern দিয়ে তুমি একটা class family-তে নতুন operation যোগ করতে পারো — সেই class গুলো না ছুঁয়েই। পুরো operation একটা আলাদা visitor object-এ থাকে, আর প্রতিটা element শুধু visitor-কে 'accept' করে আর নিজের type অনুযায়ী visit method call করে।
- প্রতিটা element-এ accept method কেন দরকার?
- কারণ client সাধারণত element গুলো common interface হিসেবে ধরে রাখে — concrete type জানে না। কিন্তু accept-এর ভেতরে element নিজেই জানে সে কোন type, তাই সে সঠিক visit method call করতে পারে। এই দুই ধাপের কৌশলটাকে double dispatch বলে।
- Double dispatch সহজ কথায় কী?
- যে behaviour চলে সেটা একসাথে দুইটা type-এর উপর নির্ভর করে — element-এর type আর visitor-এর type। সাধারণ method call শুধু একটা type দেখে। Visitor দুইটা সাধারণ call জুড়ে দেয় — element.accept(visitor) তারপর visitor.visitX(element) — তাতে দুই type দিয়ে behaviour select হয়।
- Visitor কখন খারাপ idea?
- যখন element class-এর family বারবার বড় হয়। নতুন element মানেই প্রতিটা visitor-এ একটা করে নতুন visit method যোগ করতে হবে। Visitor তখনই ভালো কাজ করে যখন element type গুলো stable আর operation গুলো বাড়তে থাকে — যেমন compiler AST।
- Visitor মানে কি শুধু object-এর উপর loop করা?
- না। Loop করলেও তোমাকে if-else বা cast দিয়ে জিজ্ঞেস করতে হয় 'এটা কোন type?' Visitor সেই check সরিয়ে দেয় — সঠিক visit method type system নিজেই বেছে নেয়, আর পুরো operation একটা class-এ একসাথে থাকে।
আরো দেখো
সম্পর্কিত পাঠ
State Pattern: একটা object-এর মেজাজ বদলানোর গল্প
State design pattern শেখো সিলিং ফ্যানের রেগুলেটরের গল্প দিয়ে। সহজ TypeScript আর C# code, state diagram, আর real software-এর উদাহরণ সহ।
Strategy Pattern: সাইকেল, বাস, নাকি অটো — তুমিই ঠিক করো
Strategy design pattern শেখো একটা সহজ স্কুলে যাওয়ার গল্পের মাধ্যমে — TypeScript আর C# কোড, runtime swapping, বাস্তব উদাহরণ, আর প্র্যাকটিস exercise সহ।
Template Method Pattern: চা আর কফি, একই ধাপ
চায়ের দোকানের গল্প দিয়ে Template Method design pattern শেখো — সহজ TypeScript আর C# কোড, hooks, diagram, বাস্তব উদাহরণ আর practice task সহ।
Composite Pattern: বাক্সের ভেতরে বাক্স — একটা জিনিস আর অনেক জিনিসকে একই চোখে দেখো
কুরিয়ারের পার্সেলে বাক্সের ভেতরে বাক্সের উদাহরণ দিয়ে Composite pattern শেখো।