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

Visitor Pattern: যে ডাক্তার প্রতিটি class-এ যায়

স্কুলের health check-up গল্পের মাধ্যমে Visitor design pattern শেখো — double dispatch সহজ ভাষায়, TypeScript আর C# কোড, বাস্তব উদাহরণ, আর practice।

22 মিনিট আপডেট: June 11, 2026beginner
visitordesign patternsbehavioraltypescriptdouble dispatchast

বিদ্যা মন্দির স্কুলে 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 শুধু একটাই কাজ শিখেছে: দরজা খোলো, বলো তুমি কে।

চিত্র ১: দুই সোমবার, একটা স্কুল — নতুন operation মানে নতুন visitor, 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 করো:

  1. একটা Visitor interface বানাও — প্রতিটা element type-এর জন্য একটা method: visitJunior(...), visitMiddle(...), visitSenior(...)
  2. প্রতিটা element-এ একটা ছোট্ট method দাও — accept(visitor) — যার একমাত্র কাজ হলো নিজের type অনুযায়ী visit method call করা: JuniorClass-এর ভেতরে accept call করবে visitor.visitJunior(this)
  3. প্রতিটা নতুন 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 interfaceaccept(visitor) declare করে (class room-এর দরজা)।
  • Concrete elementsJuniorClass, MiddleClass, SeniorClass; প্রতিটার accept মাত্র এক লাইন।
  • Visitor interface — প্রতিটা element type-এর জন্য একটা visitX method।
  • Concrete visitorsDoctorVisitor, 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 রাখে:

চিত্র ২: Operation বাড়লে element class-এর ভেতরে method — Visitor ছাড়া গুণ হয়, Visitor দিয়ে flat থাকে

আরেকটা 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 হবে।

চিত্র ৩: Visitor ছাড়া প্রতিটা নতুন operation প্রতিটা element class কেটে যায়

আমরা চাই — class-এর set stable রেখে operation যোগ করতে। আর প্রতিটা operation-এর logic একটা জায়গায় রাখতে। এটাই Visitor-এর offer।

কীভাবে কাজ করে, ধাপে ধাপে

  1. Hierarchy stable কিনা নিশ্চিত করো। Visitor তখনই লাভজনক যখন element type কমই বদলায়। Junior, Middle, Senior — বছরের পর বছর fixed। ভালো।
  2. Visitor interface declare করো — প্রতিটা concrete element-এর জন্য একটা method: visitJunior(j), visitMiddle(m), visitSenior(s)। আলাদা নাম রাখলে সব clear থাকে।
  3. Element interface-এ accept(visitor) যোগ করো। প্রতিটা class room promise করে visitor নেবে।
  4. প্রতিটা element-এ accept implement করো — এক লাইন করে। JuniorClass.accept(v) call করে v.visitJunior(this)। Pattern element-এ এটুকুই যোগ করে।
  5. প্রতিটা operation-এর জন্য একটা concrete visitor লেখো। DoctorVisitor তিনটা visit method implement করে; PhotographerVisitorও। যেকোনো running total বা buffer visitor-এর field হিসেবে থাকে।
  6. Client structure-এর মধ্য দিয়ে হাঁটে। class room গুলোতে loop করো, room.accept(visitor) call করো, তারপর visitor থেকে result পড়ো।
চিত্র ৪: Structure — বামদিকে stable elements, ডানদিকে বাড়তে থাকা operations

গোপন 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 জুড়ে দেয়:

  1. room.accept(visitor)room-এর real type দিয়ে select হয়। আমরা যাই, ধরো, JuniorClass.accept-এর ভেতরে — যেখানে this নিশ্চিতভাবে JuniorClass
  2. visitor.visitJunior(this)visitor-এর real type দিয়ে select হয়। ডাক্তার হোক বা ফটোগ্রাফার, সঠিক জন সাড়া দেয়।

স্কুলের দুই-প্রশ্নের protocol মনে করো। নাসরিন ম্যাডাম দরজায় টোকা দেন; class room বলে "আমরা Class 2" (প্রথম dispatch — room type দিয়ে); তারপর visitor তার নিজের checklist-এর সঠিক page খোলেন (দ্বিতীয় dispatch — visitor type দিয়ে)।

চিত্র ৫: Double dispatch traced — দুইটা সাধারণ call জুড়ে দিয়ে দুই type দিয়ে behaviour select

দুইটা 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 করো:

  1. একই তিনটা classroom object দুইটা সম্পূর্ণ আলাদা operation serve করল — একটাও edit ছাড়া। পরের মাসে LibrarianVisitor মানে একটাই নতুন class, আর কিছু না।
  2. কোনো instanceof, কোনো cast, কোনো type ladder নেই। Loop room গুলো plain SchoolClass হিসেবে ধরে। Double dispatch handshake নিজেই visitJunior বনাম visitSenior বেছে নিল।
  3. প্রতিটা visitor তার নিজের state জমা করল — ডাক্তার student count করলেন, ফটোগ্রাফার photo count করল। Visitor object হলো traversal-এ collect করা running total, buffer, আর error list-এর natural ঘর।

একটা classroom-এর দৃষ্টিকোণ থেকে visit দিন হলো একটা ছোট, শান্ত state machine। Room কখনো visitor-এর ব্যবসা শেখে না — শুধু host করে আর পড়ায় ফিরে যায়:

চিত্র ৬: যেকোনো visit দিনে একটা classroom — 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()))   # 20

Recursion-টা লক্ষ্য করো: 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 আর CSharpSyntaxRewriter base 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 কোথায় সবচেয়ে বেশি কাজে আসে — compiler দুনিয়ায় রাজত্ব

কখন ব্যবহার করবে, কখন করবে না

পরিস্থিতি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-এ দেখো:

চিত্র ৮: তোমার project কোন axis-এ বাড়ে? সেই pattern বেছে নাও যেটা তোমার growth সহজ করে

ছাত্রছাত্রীরা যে ভুলগুলো করে

⚠️

সবচেয়ে বড় ভুল: element family এখনো বাড়ছে অথচ Visitor ব্যবহার করা। যদি মনে হয় পরের মাসে NurseryClass আর তার পরের মাসে SportsClass আসবে — প্রতিটা addition প্রতিটা visitor ভাঙবে। Compile error storm কখনো কখনো useful (compiler force করে প্রতিটা operation নতুন case handle করতে), কিন্তু প্রতি sprint-এ এটা হলে Visitor ভুল pick ছিল — plain polymorphic method type বাড়ানো সহজ করে।

আরো কিছু trap:

  1. accept বাদ দিয়ে type-check করা। Client-এ if (room instanceof JuniorClass) visitor.visitJunior(room) লিখলে pattern-এর type safety নষ্ট হয়। accept-এর পুরো point হলো কেউ manually type inspect করবে না।
  2. accept method-এ real কাজ করা। accept এক লাইন হওয়া উচিত: v.visitX(this)। অনেকে এখানে business logic ঢুকিয়ে দেয়, operation আবার element-এ ফিরে যায়।
  3. কে traverse করবে সেটা ভুলে যাওয়া। Flat list-এর জন্য client loop করে। Tree-এর জন্য (Composite, যেমন আমাদের Python AST) visitor বা element নিজেই children-এ recurse করে। একবার decide করো, document করো, দুই জায়গায় duplicate করো না।
  4. প্রতিটা element-এর জন্য নতুন visitor বানানো। Visitor একবার বানাও, সব element-এর মধ্যে দিয়ে পাঠাও, তারপর accumulated result পড়ো। Loop-এর ভেতরে নতুন AreaVisitor বানালে প্রতিটা একটা shape-এর অনেক total পাবে।
  5. Element খুলে ফেলা। Visitor-এর জন্য যদি প্রতিটা element-এ বারোটা getter যোগ করতে হয়, design তোমার বিরুদ্ধে লড়ছে। হয় প্রয়োজনীয় data visit method-এ pass করো, নয়তো ভাবো operation আসলে element-এর ভেতরেই থাকা উচিত কিনা।
💡

accept-এর জন্য memory trick: element শুধু একটা প্রশ্নের উত্তর দিচ্ছে — "তুমি কে?" — আর সে উত্তর দেয় নিজের নামে visit method call করে। বাকি সব visitor-এর।

কাছের pattern গুলোর সাথে তুলনা

Patternকী extract করা হয়?Behaviour কীভাবে বেছে নেওয়া হয়?সাধারণ ব্যবহার
VisitorType-specific behaviour-এর পুরো family (প্রতিটা element-এর জন্য একটা)accept দিয়ে double dispatchStable hierarchy-তে নতুন operation
Plain methodsকিছু না — behaviour প্রতিটা class-এ থাকেSingle dispatch (সাধারণ virtual call)Stable operation, বাড়তে থাকা type
Strategyএকটা algorithm একটা interface-এর পেছনেClient একটা object install করেSwappable single behaviour
IteratorTraversal নিজেইCaller একটা একটা করে element নেয়Walking, logic caller-এ থাকে
CompositeElement-এর tree structureVisitor যে 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 করে।

চিত্র ৯: Plain method আর Visitor-এর মধ্যে choose করা নির্ভর করে কোন axis বাড়ছে তার উপর

পুরো pattern এক পাতায়

এই mind map মুখস্থ করে আঁকতে পারলে — আর double dispatch branch জোরে বলে explain করতে পারলে — pattern তোমার হয়ে গেছে:

চিত্র ১০: Visitor pattern mapped — চরিত্র, engine, trade-off, আর আসল দুনিয়ায় কোথায় থাকে

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 হও:

  1. তৃতীয় visitor, শূন্য edit। এই post-এর school code নাও আর LibrarianVisitor যোগ করো — junior-দের picture book card, middle-দের fiction card, senior-দের reference access, মোট card count। Game-এর নিয়ম: JuniorClass, MiddleClass, বা SeniorClass ছোঁয়া যাবে না। ছুঁতে মন চাইলে — pattern আবার পড়ো!
  2. Electricity meter reader। House, Shop, আর Factory building model করো, প্রতিটায় unitsConsumed field আর accept আছে। BillingVisitor লেখো (house-এ ৫ টাকা/unit, shop-এ ৮, factory-তে ১০, মোট bill জমা করে) আর InspectionVisitor লেখো (প্রতিটা building type-এর জন্য আলাদা safety checklist print করে)। একই building list-এ দুইটা run করো।
  3. Trade-off feel করো (thinking task)। এবার exercise 2-এ চতুর্থ building type School যোগ করো। কতটা file বদলাতে হলো লিখে রাখো। তারপর দুই বাক্যে answer করো: প্রতি মাসে নতুন building type আসলে Visitor রাখবে নাকি plain method-এ ফিরবে? তোমার project expression problem-এর কোন axis-এ বাড়ছে?
  4. College stretch। Python baby compiler-এ Sub node আর 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 শেখো। একটা আইটেম আর পুরো গ্রুপকে একই interface দিয়ে ট্রিট করো, আর সহজ recursion দিয়ে মোট হিসাব বের করো।

আরও পড়ুন