এই পর্বে যা শিখবেন
- পাঁচটি pattern-এর প্রতিটির উদ্দেশ্য, কোন সমস্যায় লাগে, UML গঠন, ফলাফল আর সাধারণ ভুল বলতে পারা
- একটি
switch-ভিত্তিক অবস্থা-যন্ত্রকে State class-এ ভাঙা, আর অবৈধ transition নিরাপদে সামলানো - Python আর JavaScript-এ iterator ও generator লেখা, lazy evaluation মাপা, আর ঘোরানোর সময় collection বদলানোর ফাঁদ চেনা
- Middleware শিকল লেখা — short-circuit, ক্রমের গুরুত্ব আর default handler
- Visitor-এর double dispatch ধাপে ধাপে বোঝা, আর কখন
singledispatchবা pattern matching যথেষ্ট — তা বিচার করা
১ · দ্বিতীয় দল — কে কাকে কাজ দেবে, কে কার সাথে কথা বলবে
পর্ব ৩২-এ (design pattern কী) দেখেছেন, Gang of Four-এর ১৯৯৪ সালের Design Patterns বইয়ে ২৩টি pattern তিন দলে ভাগ করা: creational (object বানানো, পর্ব ৩৩), structural (object জোড়া দেওয়া, পর্ব ৩৪) আর behavioral — object-রা কীভাবে দায়িত্ব ভাগ করে আর একে অপরের সাথে কথা বলে। Behavioral দলে ১১টি pattern; পর্ব ৩৫-এ (Behavioral pattern ১) দেখেছেন strategy, observer, command আর template method. এই পর্বে বাকিগুলোর পাঁচটি বিস্তারিত, আর দুটি (Memento, Interpreter) এক লাইনে — যাতে পুরো তালিকা আপনার হাতে থাকে।
একটি জেলা হাসপাতালের কথা ভাবুন। রোগীর অবস্থা (ভর্তি, অপারেশন-কক্ষে, ছাড়পত্র) বদলালে তাঁর সাথে কর্মীদের আচরণ বদলায় — State. ওয়ার্ডের রোগীর তালিকা নার্স একজন একজন করে দেখেন, তালিকা খাতায় না কম্পিউটারে, তা না জেনেই — Iterator. জরুরি বিভাগের রোগী আগে ইন্টার্ন দেখেন, না পারলে রেজিস্ট্রার, তারপর কনসালট্যান্ট — Chain of Responsibility. ওয়ার্ড, ল্যাব আর ফার্মেসি একে অপরকে সরাসরি না ডেকে নার্সিং স্টেশনের মাধ্যমে চলে — Mediator. আর বছরে একবার হিসাব-পরিদর্শক প্রতিটি বিভাগে গিয়ে নিজের ফর্ম পূরণ করেন, বিভাগগুলো না বদলে — Visitor.
| Pattern | এক বাক্যে উদ্দেশ্য | যে প্রশ্নের উত্তর দেয় |
|---|---|---|
| State | অবস্থা বদলালে object-এর আচরণ বদলায়, যেন class-ই বদলে গেল | “এখন কোন অবস্থা?” — এই if বারবার কোথায় লিখব? |
| Iterator | ভেতরের গঠন না দেখিয়ে উপাদানগুলো একে একে দেওয়া | Collection বদলালে ঘোরানোর কোড কেন ভাঙবে? |
| Chain of Responsibility | অনুরোধ handler-দের শিকলে পাঠানো, যতক্ষণ না কেউ সামলায় | পাঠানো পক্ষকে কেন জানতে হবে কে সামলাবে? |
| Mediator | একদল object-এর পারস্পরিক যোগাযোগ এক জায়গায় রাখা | সবাই সবাইকে চিনলে একটি বদল কত জায়গায় ছড়াবে? |
| Visitor | Element-এর class না বদলে কাঠামোর ওপর নতুন কাজ যোগ | দশটি class-এ নতুন method না ছড়িয়ে নতুন হিসাব কোথায় রাখব? |
| Memento · Interpreter | অবস্থার ছবি রেখে ফেরা · ছোট ভাষার ব্যাকরণকে class-এ লেখা | §৭-এ এক লাইনে |
প্রতিটি pattern একই ছকে দেখব: উদ্দেশ্য (GoF-এর intent), সমস্যা, গঠন, কোড, ফলাফল (consequences) আর ফাঁদ.
২ · State — অবস্থা বদলালে আচরণ বদলায়
উদ্দেশ্য (GoF): “Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.” অর্থাৎ object-এর ভেতরের অবস্থা বদলালে তার আচরণও বদলাবে, আর বাইরে থেকে মনে হবে object-টির class-ই বদলে গেছে। এটিই State pattern. GoF-এর বইয়ে উদাহরণ ছিল একটি TCP connection — Established, Listen আর Closed অবস্থায় একই open() ডাকের অর্থ আলাদা।
সমস্যা: একই switch দশ জায়গায়
একটি কাল্পনিক উপজেলা গণগ্রন্থাগারের app নিন। একটি বইয়ের কপি তিনটি state-এ থাকতে পারে: Available (তাকে আছে), Reserved (কেউ সংরক্ষণ করেছেন) আর OnLoan (কেউ নিয়ে গেছেন)। ঘটনা (event) তিনটি: borrow, give_back আর reserve. ধার দেওয়া কপিতে কেউ সংরক্ষণ করলে কপিটি ফেরত এলে সরাসরি Reserved-এ যায়। পর্ব ২৩-এ (requirement modeling) একটি অর্ডারের জীবনচক্র state machine diagram-এ এঁকেছিলেন, আর পর্ব ৩০-এ (UML behaviour diagram) design-স্তরের state machine — entry, exit আর guard-সহ। এখানে প্রশ্ন কোডের: সেই ছবিটি কোডে কীভাবে লিখবেন?
প্রথম চেষ্টা প্রায় সবসময় একটি status field আর প্রতিটি method-এ if status == .... শুরুতে এটি ঠিকই আছে। কিন্তু অবস্থা আর ঘটনা বাড়লে একই শর্তের শিকল borrow, give_back, reserve, জরিমানার হিসাব, SMS পাঠানো — সবখানে ছড়িয়ে পড়ে। নতুন অবস্থা Lost এলে প্রতিটি জায়গা খুঁজে বদলাতে হয়; একটি ভুলে গেলে bug. পর্ব ২৭-এ (SOLID ১) এই গন্ধের নাম দেখেছেন — repeated switch — আর তার প্রতিকার polymorphism. State pattern ঠিক সেই প্রতিকারটি অবস্থার ক্ষেত্রে প্রয়োগ করে।
গঠন
তিনটি অংশ: (১) context — বাইরের জগৎ যে object-টি দেখে (Book), যার ভেতরে বর্তমান অবস্থার একটি object; (২) একটি State interface — প্রতিটি event-এর জন্য একটি method; (৩) প্রতিটি অবস্থার জন্য একটি concrete class, যা শুধু সেই অবস্থার আচরণ জানে। Context প্রতিটি event পেলে প্রশ্ন না করে বর্তমান state object-কে কাজটি দিয়ে দেয়।
State pattern-এর গঠন — গ্রন্থাগারের বই (কাল্পনিক)
UML class diagram: context, state interface and concrete states
Book (context) একটি BookState object রাখে (association, ভূমিকা state, multiplicity ১)। তিনটি concrete class interface-টি realize করে, আর প্রত্যেকে শুধু নিজের বৈধ event-গুলো override করে; বাকিগুলো base-এর “অবৈধ” আচরণ পায়। UML-এর এই চিহ্নগুলোর বিস্তারিত পর্ব ২৯-এ (UML class diagram).কোড: State class বনাম একটি বড় switch
নিচের প্রোগ্রামে একই নিয়ম দুইভাবে লেখা। প্রথমটি State pattern: base class BookState-এর প্রতিটি event ডিফল্টভাবে InvalidTransition তোলে, আর প্রতিটি concrete state শুধু নিজের বৈধ event-গুলো override করে, পরের state object ফেরত দিয়ে। দ্বিতীয়টি Python-এর match দিয়ে একটি টেবিলের মতো switch. দুটিকে একই ছয়টি event দেওয়া হয়েছে, যার দুটি অবৈধ।
class InvalidTransition(Exception): pass
class BookState: # default: every event is invalid
name = property(lambda self: type(self).__name__)
def _no(self, ev): raise InvalidTransition(f"{ev} not allowed while {self.name}")
def borrow(self, book): self._no("borrow")
def give_back(self, book): self._no("give_back")
def reserve(self, book): self._no("reserve")
class Available(BookState):
def borrow(self, book): return OnLoan()
def reserve(self, book): return Reserved()
class Reserved(BookState):
def borrow(self, book): return OnLoan() # only the reserving member, in real life
class OnLoan(BookState):
def reserve(self, book): book.waiting = True; return self
def give_back(self, book):
nxt = Reserved() if book.waiting else Available(); book.waiting = False; return nxt
class Book: # the context: delegates, never asks "which state?"
def __init__(self): self.state, self.waiting = Available(), False
def handle(self, ev):
old = self.state.name
try: self.state = getattr(self.state, ev)(self); print(f"{ev:9} {old:9} -> {self.state.name}")
except InvalidTransition as e: print(f"{ev:9} {old:9} REJECTED: {e}")
def next_state(state, ev, waiting): # the same rules as one big switch
match (state, ev):
case ("Available" | "Reserved", "borrow"): return "OnLoan", False
case ("Available", "reserve"): return "Reserved", False
case ("OnLoan", "reserve"): return "OnLoan", True
case ("OnLoan", "give_back"): return ("Reserved" if waiting else "Available"), False
case _: return None # None = invalid; every caller must remember to check
book, sw = Book(), ("Available", False)
for ev in ["borrow", "reserve", "borrow", "give_back", "give_back", "borrow"]:
book.handle(ev)
sw = next_state(sw[0], ev, sw[1]) or sw
print("state classes end in:", book.state.name, "| switch ends in:", sw[0])আউটপুট:
borrow Available -> OnLoan
reserve OnLoan -> OnLoan
borrow OnLoan REJECTED: borrow not allowed while OnLoan
give_back OnLoan -> Reserved
give_back Reserved REJECTED: give_back not allowed while Reserved
borrow Reserved -> OnLoan
state classes end in: OnLoan | switch ends in: OnLoan
প্রথম লাইনে Available.borrow চলে নতুন OnLoan() ফেরত দিল। দ্বিতীয় লাইনে ধার-থাকা কপিতে সংরক্ষণ — OnLoan.reserve শুধু waiting চিহ্ন বসায়, অবস্থা একই। তৃতীয় লাইনে ধার-থাকা কপি আবার ধার চাওয়া অবৈধ: OnLoan borrow override করেনি, তাই base-এর _no exception তুলল, আর Book.handle সেটি ধরে “REJECTED” লিখল — অবস্থা অটুট রইল। চতুর্থ লাইনে ফেরত এল, আর waiting সত্য থাকায় পরের অবস্থা Reserved — এটি একটি guard-এর কোড-রূপ। পঞ্চম লাইনে সংরক্ষিত কপি “ফেরত” দেওয়া অবৈধ, আবার প্রত্যাখ্যাত। শেষ লাইনে দুই সংস্করণ একই অবস্থায় পৌঁছেছে। তফাতটা আউটপুটে নয়, বদলের খরচে: Lost যোগ করতে State সংস্করণে একটি নতুন class লাগে আর কয়েকটি পুরোনো class-এ একটি করে method; switch সংস্করণে প্রতিটি case আবার পড়তে হয়, আর None ফেরত এলে প্রতিটি caller-কে মনে রাখতে হয় যাচাই করতে।
নিচের stepper-এ ওপরের ছয়টি event একে একে দেখুন — প্রতিবার কোন class-এর কোন method চলছে।
1Book.handle(ev): state = state.ev(book)2Available.borrow → OnLoan()3Available.reserve → Reserved()4Reserved.borrow → OnLoan()5OnLoan.reserve → waiting=True; self6OnLoan.give_back → Reserved / Available7BookState._no → raise InvalidTransition
| event | |
| আগের state | |
| যে method চলল | |
| waiting | |
| পরের state |
- borrow, Available-এ।
Book.handleজিজ্ঞেস করে না অবস্থা কী; শুধুself.state.borrow(self)ডাকে। বর্তমান objectAvailable, তাই তারborrowচলে আর নতুনOnLoan()ফেরত দেয়। - reserve, OnLoan-এ। ধার-থাকা কপিতে একজন সদস্য সংরক্ষণ করলেন।
OnLoan.reservecontext-এরwaitingসত্য করে আরselfফেরত দেয় — অবস্থা একই থাকে, শুধু একটি তথ্য জমল। - borrow, OnLoan-এ — অবৈধ।
OnLoanclass-এborrowনেই, তাই baseBookState.borrowচলে, যা_noডেকেInvalidTransitionতোলে। Context exception ধরে “REJECTED” লেখে,stateবদলায় না। অবৈধ ঘটনার নিয়ম লেখা আছে এক জায়গায়, base class-এ। - give_back, OnLoan-এ।
OnLoan.give_backদেখেwaitingসত্য, তাই পরের অবস্থাReserved, আরwaitingআবার মিথ্যা। State diagram-এর ভাষায় এটিgive_back [waiting] / notifyধরনের একটি transition. - give_back, Reserved-এ — অবৈধ। তাকে-থাকা সংরক্ষিত কপি “ফেরত” দেওয়ার মানে নেই।
Reservedশুধুborrowজানে, তাই base-এর নিয়মে প্রত্যাখ্যান; অবস্থাReserved-ই। - borrow, Reserved-এ। সংরক্ষণকারী সদস্য কপিটি নিলেন:
Reserved.borrowনতুনOnLoan()দেয়। মোট ছয়টি ঘটনায় একবারও কোনোif state == ...লেখা হয়নি — সিদ্ধান্তটি হয়েছে কোন object-এর method চলল, তা দিয়েই।
Transition কে ঠিক করবে — context, না state?
GoF দুটি পথই আলোচনা করে। State-রা নিজেরা পরের state ঠিক করলে (ওপরের কোড) নতুন state যোগ করা সহজ আর প্রতিটি নিয়ম তার অবস্থার পাশেই থাকে; কিন্তু state class-গুলো একে অপরকে চেনে, তাই তাদের মধ্যে coupling তৈরি হয়। Context বা একটি টেবিল transition ঠিক করলে (যেমন {(Available, borrow): OnLoan}) পুরো জীবনচক্র এক জায়গায় পড়া যায় আর state class-রা শুধু আচরণ রাখে; কিন্তু নতুন state এলে টেবিলও বদলাতে হয়। নিয়ম সহজ আর স্থির হলে টেবিল; প্রতিটি অবস্থায় আচরণ জটিল হলে state class নিজেরাই.
State বনাম Strategy — একই ছবি, ভিন্ন গল্প
UML-এ State আর পর্ব ৩৫-এর Strategy প্রায় হুবহু এক: context-এর ভেতরে একটি interface-এর object, যার কাছে কাজ পাঠানো হয়। তফাতটা উদ্দেশ্যে আর কে বদলায় তাতে।
| প্রশ্ন | Strategy | State |
|---|---|---|
| কে বদলায়? | সাধারণত client বা configuration — বাইরে থেকে একবার বেছে দেয় | State object বা context নিজে — প্রতিটি event-এর জবাবে |
| বিকল্পগুলো কি একে অপরকে চেনে? | না; প্রতিটি স্বাধীন, যেকোনো একটি বসানো যায় | প্রায়ই হ্যাঁ; একটি state পরের state ফেরত দেয় |
| উদাহরণ | ডেলিভারির খরচের হিসাব — ঢাকার ভেতরে না বাইরে | বইয়ের কপি — তাকে, সংরক্ষিত, ধারে |
ফলাফল: প্রতিটি অবস্থার আচরণ এক class-এ (cohesion বাড়ে); নতুন অবস্থা যোগ করা প্রায়ই নতুন class যোগ (পর্ব ২৭-এর open-closed); transition স্পষ্ট হয়, কারণ state পরিবর্তন মানে একটি field-এ নতুন object বসানো। খরচ: class-এর সংখ্যা বাড়ে, আর পুরো জীবনচক্র এক নজরে দেখতে কয়েকটি file খুলতে হয়।
ফাঁদ: (১) দুই-তিনটি অবস্থা আর সহজ নিয়মের জন্য State class — একটি enum আর একটি transition টেবিল বেশি পরিষ্কার। (২) অবৈধ event চুপচাপ উপেক্ষা করা — অন্তত log বা exception দিন, যেমন ওপরের InvalidTransition. (৩) Diagram আর কোডের অমিল — state machine diagram (পর্ব ৩০) থেকে প্রতিটি transition-এর জন্য একটি test লিখুন; অবৈধ transition-এর test-ও।
৩ · Iterator — ভেতর না দেখিয়ে একে একে
উদ্দেশ্য (GoF): “Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.” একটি collection-এর উপাদানগুলো ক্রমানুসারে পাওয়ার উপায় দেওয়া, কিন্তু collection-টি ভেতরে array, linked list, গাছ, database না network-এর পাতা — তা না জানিয়ে। এটিই Iterator pattern.
রেলস্টেশনের টিকিট কাউন্টারের লাইনে কর্মী শুধু বলেন “পরের জন”। লাইনটি প্ল্যাটফর্মে সোজা, না ভিড়ে আঁকাবাঁকা, না টোকেন নম্বরে — তা তাঁর জানার দরকার নেই। “পরের জন” আর “লাইন শেষ” — এই দুটি কথাই চুক্তি। Iterator ঠিক এই দুটি কথা দেয়।
সমস্যা আর গঠন
Iterator না থাকলে ঘোরানোর কোড collection-এর গঠন জানে: array হলে for (i = 0; i < n; i++), linked list হলে while (node) node = node.next, গাছ হলে recursion. Collection বদলালে সব ঘোরানোর কোড বদলাতে হয়। Iterator দুটি দায়িত্ব আলাদা করে: iterable (collection) জানে কীভাবে নতুন iterator বানাতে হয়, আর iterator মনে রাখে কতদূর পড়া হলো। একই collection-এর ওপর তাই একসাথে দুটি স্বাধীন iterator চলতে পারে।
- External iterator: client নিজে
next()ডেকে এগোয় আর ইচ্ছামতো থামে। Python আর JavaScript-এরforলুপ ভেতরে এটিই করে। - Internal iterator: collection নিজে ঘোরে আর প্রতিটি উপাদানে client-এর দেওয়া function চালায় — JavaScript-এর
forEach. লেখা সহজ, কিন্তু মাঝপথে থামা বা দুটি collection একসাথে মিলিয়ে ঘোরানো কঠিন (forEach-এbreakচলে না)।
Python-এর iterator protocol আর generator
Python-এ iterator protocol ভাষারই অংশ (PEP 234, Python ২.১): iterable-এর __iter__ একটি iterator ফেরত দেয়; iterator-এর __next__ পরের মান দেয়, আর শেষ হলে StopIteration তোলে। for, list(), sum(), unpacking — সবাই এই চুক্তি ব্যবহার করে।
class Shelf: # an iterable: can make fresh iterators
def __init__(self, books): self._books = list(books)
def __iter__(self): return ShelfIterator(self._books)
class ShelfIterator: # an iterator: remembers where it is
def __init__(self, books): self._books, self._i = books, 0
def __iter__(self): return self
def __next__(self):
if self._i >= len(self._books): raise StopIteration
self._i += 1
return self._books[self._i - 1]
def shelf_gen(books): # the same iterator, written as a generator
for b in books:
yield b
s = Shelf(["Gitanjali", "Padma Nadir Majhi"])
print(list(s), list(s)) # two passes work: two fresh iterators
it = iter(s); print(next(it), next(it), next(it, "done"))['Gitanjali', 'Padma Nadir Majhi'] ['Gitanjali', 'Padma Nadir Majhi']
Gitanjali Padma Nadir Majhi done
Shelf iterable, ShelfIterator iterator — দুবার list(s) চালালে দুটি নতুন iterator তৈরি হয়, তাই দুবারই পুরো তালিকা আসে। শেষে next(it, "done") শেষ হয়ে গেলে exception-এর বদলে ডিফল্ট মান দেয়। কিন্তু এত লেখা প্রায়ই দরকার হয় না: shelf_gen-এর মতো একটি generator function (PEP 255, Python ২.২) একই কাজ করে। yield-এ এসে function থেমে থাকে, তার local variable-সহ; পরের next()-এ সেখান থেকেই আবার চলে। Iterator-এর “কতদূর পড়া হলো” মনে রাখার দায়িত্ব তখন ভাষা নিজেই নেয়। বিস্তারিত দেখুন Python কোর্সের iterator, generator আর yield পর্বে।
JavaScript-এ Symbol.iterator আর lazy পাতা-আনা
JavaScript-এ (ES2015 থেকে) একই চুক্তি: যার [Symbol.iterator]() method একটি iterator দেয়, সেটি for...of, spread [...x] আর destructuring-এ চলে; iterator-এর next() দেয় { value, done }. function* generator এর সহজ রূপ। নিচের প্রোগ্রামে একটি কাল্পনিক দোকানের API ২০টি পণ্য ৫টি করে পাতায় দেয়। Catalog-এর iterator পাতাগুলো লুকিয়ে রাখে — client শুধু পণ্য দেখে — আর পাতা আনে শুধু দরকার হলে। এই “দরকার হলে তবেই” নীতির নাম lazy evaluation. Data একই থাকার জন্য একটি seeded PRNG (mulberry32, seed 36) ব্যবহার করা হয়েছে।
// Fictional shop API: 20 products, served 5 per page. Seeded PRNG = same data every run.
function mulberry32(a) { return () => { a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
const rand = mulberry32(36);
const DB = Array.from({ length: 20 }, (_, i) => ({ id: i + 1, price: 50 + Math.floor(rand() * 450) }));
let fetched = 0;
function fetchPage(p, size = 5) { // stands in for one network call
fetched++; console.log(` [api] GET /products?page=${p}`);
return { items: DB.slice((p - 1) * size, p * size), next: p * size < DB.length ? p + 1 : null };
}
class Catalog { // client never sees pages, only items
*[Symbol.iterator]() {
try {
for (let p = 1; p !== null; ) { const page = fetchPage(p); yield* page.items; p = page.next; }
} finally { console.log(" [catalog] iterator closed"); }
}
}
function* filter(it, ok) { for (const x of it) if (ok(x)) yield x; }
function* take(it, n) { if (n <= 0) return; for (const x of it) { yield x; if (--n === 0) return; } }
const cheap = take(filter(new Catalog(), x => x.price < 150), 2);
console.log("pipeline built, pages fetched:", fetched);
for (const item of cheap) console.log("got #" + item.id, "price", item.price);
console.log("pages fetched:", fetched, "of", DB.length / 5);
const it = new Catalog()[Symbol.iterator](); // the protocol by hand: next() -> {value, done}
console.log(JSON.stringify(it.next()), "| total fetched now:", fetched);
console.log("eager copy:", [...new Catalog()].length, "items | total fetched now:", fetched);আউটপুট:
pipeline built, pages fetched: 0
[api] GET /products?page=1
got #4 price 123
[api] GET /products?page=2
got #10 price 140
[catalog] iterator closed
pages fetched: 2 of 4
[api] GET /products?page=1
{"value":{"id":1,"price":480},"done":false} | total fetched now: 3
[api] GET /products?page=1
[api] GET /products?page=2
[api] GET /products?page=3
[api] GET /products?page=4
[catalog] iterator closed
eager copy: 20 items | total fetched now: 7
প্রথম লাইনটি মূল কথা: take(filter(...)) পুরো pipeline বানানো হলো, কিন্তু একটি পাতাও আনা হয়নি — generator ডাকলে শুধু একটি থেমে-থাকা iterator তৈরি হয়। for...of প্রথম মান চাইতেই পাতা ১ এল; তার ৪ নম্বর পণ্য (১২৩ টাকা) ১৫০-এর কম, তাই প্রথম ফল। দ্বিতীয় সস্তা পণ্য পাতা ১-এ নেই, তাই পাতা ২ আনা হলো — ১০ নম্বর, ১৪০ টাকা। take দুটি পেয়েই return করল; for...of তখন শিকলের প্রতিটি iterator-এর return() ডাকে, আর Catalog-এর finally চলে “iterator closed” লেখে। পাতা ৩ আর ৪ কখনো আনা হয়নি — ৪টির মধ্যে ২টি। এরপর হাতে next() ডেকে protocol-টি সরাসরি দেখা গেল: {"value":…,"done":false}; এই iterator-টি বন্ধ করা হয়নি, তাই তার finally চলেনি। শেষ লাইনে spread [...] সবকিছু একবারে চায় — চারটি পাতাই আনে (মোট ৩ + ৪ = ৭)। Lazy pipeline-এর শেষে অসাবধানে একটি [...x] বা Python-এ list(x) বসালে সব সুবিধা হারায়। আসল network-এ fetch asynchronous; তখন একই নকশা async function* আর for await...of (ES2018) দিয়ে লেখা হয় — দেখুন JavaScript কোর্সের iterator আর generator পর্ব।
ঘোরানোর সময় collection বদলানো
Iterator-এর সবচেয়ে পরিচিত ফাঁদ হলো concurrent modification — iterator চলার সময়েই collection বদলানো। Iterator মনে রাখে “আমি এখন ২ নম্বর ঘরে”; নিচে ঘর সরে গেলে সে ভুল জায়গায় দাঁড়িয়ে থাকে।
nums = [1, 2, 2, 3]
for x in nums:
if x == 2:
nums.remove(x) # shifts the list under the iterator
print(nums) # [1, 2, 3] -- one 2 survived!
stock = {"rice": 5, "oil": 0}
for item in stock:
stock["salt"] = 9 # RuntimeError: dictionary changed size during iterationList-এর ক্ষেত্রে Python চুপচাপ একটি উপাদান বাদ দেয় — প্রথম 2 মোছার পর দ্বিতীয় 2 এক ঘর বাঁয়ে সরে আসে, আর iterator পরের ঘরে চলে যায়। Dict-এর ক্ষেত্রে Python সতর্ক — আকার বদলালে RuntimeError. Java-র collection-এর iterator একইভাবে ConcurrentModificationException তোলে, যাকে বলে fail-fast iterator (documentation নিজেই বলে, এটি “best-effort” — সবসময় ধরা পড়ার নিশ্চয়তা নেই)। সমাধান: নতুন collection বানান ([x for x in nums if x != 2]), কপির ওপর ঘোরান (for x in list(nums)), নয়তো পরিবর্তনগুলো জমিয়ে লুপ শেষে প্রয়োগ করুন।
ফলাফল: collection-এর গঠন বদলালেও client-এর কোড বদলায় না; একই collection-এ একাধিক স্বাধীন ঘোরানো; lazy হলে অসীম ধারা (যেমন প্রতিটি নতুন log লাইন) বা বিশাল data-ও অল্প memory-তে। খরচ ও ফাঁদ: generator একবারই ঘোরানো যায় — দ্বিতীয়বার ঘোরালে খালি পাবেন; আর lazy কাজের error ঘটে ব্যবহারের মুহূর্তে, তৈরির মুহূর্তে নয়।
৪ · Chain of Responsibility — হাতে হাতে অনুরোধ
উদ্দেশ্য (GoF): “Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.” পাঠানো পক্ষকে গ্রহণকারীর সাথে বেঁধে না রেখে একাধিক object-কে সামলানোর সুযোগ দেওয়া — তাদের একটি শিকলে সাজিয়ে, যতক্ষণ না কেউ সামলায়। এটিই Chain of Responsibility. GoF-এর উদাহরণ ছিল একটি app-এর প্রসঙ্গভিত্তিক help: বোতামে help চাইলে বোতাম না পারলে dialog, dialog না পারলে পুরো application উত্তর দেয়।
সরকারি অফিসে একটি আবেদন প্রথমে যায় অফিস সহকারীর কাছে। ছোট বিষয় হলে তিনিই নিষ্পত্তি করেন; না পারলে ফাইল যায় সেকশন অফিসারের কাছে, তারপর উপ-পরিচালক। আবেদনকারী জানেন না শেষে কে সই করবেন — তিনি শুধু প্রথম টেবিলে জমা দেন।
গঠন
একটি Handler interface বা abstract class, যার একটি handle(req) আর পরের handler-এর একটি reference (next, ০ বা ১টি)। প্রতিটি concrete handler দুটি কাজের একটি করে: নিজে উত্তর দেয় — তখন শিকল সেখানেই থামে, যাকে বলে short-circuit — নয়তো next-কে দেয়। Client শুধু প্রথম handler-টি চেনে।
Chain of Responsibility-র গঠন — class আর চলার সময়ের শিকল
UML class diagram (top) and runtime object chain (bottom)
Handler abstract (নাম বাঁকা অক্ষরে), নিজের দিকে next association (০..১), আর চারটি concrete handler-এর generalization (কঠিন রেখা, ফাঁপা ত্রিভুজ)। নিচে চলার সময়ের শিকল — object-এর নাম UML রীতিতে দাগ টানা। লাল সংখ্যাগুলো দেখায় কোন handler থামলে কোন HTTP উত্তর আসে।আজকের রূপ: web framework-এর middleware
GoF-এর শিকলে একটি handler সামলালে বাকিরা কিছুই করে না। আধুনিক web framework-এর middleware একটু আলাদা ধাঁচের: প্রতিটি ধাপ কিছু কাজ করে (log, login যাচাই, সময় মাপা) আর তারপর next() ডেকে পরের ধাপে পাঠায়, অথবা নিজেই উত্তর দিয়ে শিকল থামায়। Express-এর (req, res, next), Koa-র async next (যাকে “onion” মডেল বলা হয়, কারণ await next()-এর পরের কোড ফেরার পথে চলে), Django-র middleware, Java Servlet-এর Filter আর FilterChain, ASP.NET Core-এর pipeline — সবই এই ধারার। তাত্ত্বিকভাবে এগুলো GoF-এর শুদ্ধ CoR আর Intercepting Filter বা Decorator-এর মাঝামাঝি (ভাবনার প্রশ্ন ৩)।
নিচের প্রোগ্রামে একটি কাল্পনিক মুদি-অর্ডার API-র চারটি ধাপ: auth (login আছে কি?) → rateLimit (একজন বেশি অনুরোধ পাঠাচ্ছেন কি?) → validate (পরিমাণ ১–১০ কেজি কি?) → createOrder. প্রতিটি ধাপ হয় একটি উত্তর ফেরত দেয়, নয় next(). req.trail লিখে রাখে অনুরোধটি কোন কোন ধাপ পেরোল। সংক্ষেপের জন্য rate limit এখানে সময়ের জানালা ছাড়া শুধু গোনে; আসল token bucket বা sliding window দেখুন System Design-এর rate limiting পর্বে।
// Fictional grocery-order API: each handler either answers (short-circuit) or calls next()
function auth(req, next) {
if (req.token !== "tok-rina") return { status: 401, body: "login required" };
req.user = "rina"; return next();
}
const hits = {}; // requests per user (no time window, to keep it short)
function rateLimit(req, next) {
hits[req.user] = (hits[req.user] ?? 0) + 1;
return hits[req.user] > 3 ? { status: 429, body: "too many requests" } : next();
}
function validate(req, next) {
const ok = Number.isInteger(req.qty) && req.qty >= 1 && req.qty <= 10;
return ok ? next() : { status: 400, body: "qty must be 1..10" };
}
function createOrder(req) { return { status: 201, body: `order: ${req.qty} kg rice for ${req.user}` }; }
const notFound = () => ({ status: 404, body: "no handler" }); // the default: chain always ends
function chain(...handlers) {
return req => {
req.trail = [];
const run = i => {
const h = handlers[i] ?? notFound; // ran past the last handler? fall back to default
req.trail.push(h.name); return h(req, () => run(i + 1));
};
return run(0);
};
}
const app = chain(auth, rateLimit, validate, createOrder);
for (const req of [{ qty: 2 }, { token: "tok-rina", qty: 2 }, { token: "tok-rina", qty: 50 },
{ token: "tok-rina", qty: 1 }, { token: "tok-rina", qty: 3 }]) {
const res = app(req);
console.log(res.status, res.body.padEnd(28), "<-", req.trail.join(" > "));
}
const broken = chain(auth, validate); // someone forgot the final handler
console.log(broken({ token: "tok-rina", qty: 1 }).status, "<- default handler caught it");আউটপুট:
401 login required <- auth
201 order: 2 kg rice for rina <- auth > rateLimit > validate > createOrder
400 qty must be 1..10 <- auth > rateLimit > validate
201 order: 1 kg rice for rina <- auth > rateLimit > validate > createOrder
429 too many requests <- auth > rateLimit
404 <- default handler caught it
প্রথম অনুরোধে token নেই — auth নিজেই ৪০১ ফেরত দিল; trail-এ শুধু auth, বাকি তিনটি ধাপ চলেইনি। দ্বিতীয়টি চারটি ধাপই পেরিয়ে ২০১. তৃতীয়টির পরিমাণ ৫০ — validate-এ থেমে ৪০০; createOrder চলেনি। চতুর্থটি আবার সফল। পঞ্চমটি rateLimit-এ থামল (৪২৯) — অথচ Rina-র সফল অর্ডার মাত্র দুটি! কারণ ক্রম: rateLimit বসানো validate-এর আগে, তাই অবৈধ অনুরোধটিও কোটার একটি গুনেছে (২য়, ৩য়, ৪র্থ অনুরোধে গোনা ১, ২, ৩; ৫মটিতে ৪ > ৩)। এটি কখনো ঠিক (ভুল অনুরোধের বন্যাও ঠেকাতে চাইলে), কখনো ভুল — মূল কথা, middleware-এর ক্রম আচরণের অংশ, তাই সিদ্ধান্তটি সচেতনভাবে নিন। একই কারণে auth সবার আগে: login ছাড়া req.user-ই নেই। শেষ লাইনে কেউ createOrder দিতে ভুলে গেছেন; validate next() ডাকল, শিকলে আর কেউ নেই, তাই handlers[i] ?? notFound ডিফল্ট handler চালাল — অনুরোধটি ঝুলে থাকল না, ৪০৪ পেল।
নিচের stepper-এ তৃতীয় অনুরোধটি (qty: 50) শিকল ধরে হাঁটে, তারপর পঞ্চমটির সাথে তুলনা।
- auth
401login? - rateLimit
429বেশি অনুরোধ? - validate
400qty 1..10? - createOrder
201আসল কাজ
| অনুরোধ | |
| এখন কোথায় | |
| সিদ্ধান্ত | |
| trail | |
| উত্তর |
- অনুরোধ এল। Client শুধু শিকলের প্রথম ধাপ চেনে —
app(req). কোন ধাপ শেষে উত্তর দেবে, client জানে না; এটিই পাঠানো আর গ্রহণকারীর মধ্যে বাঁধন না থাকা। - auth. Token ঠিক আছে, তাই
req.userবসিয়েnext(). লক্ষ করুন, একটি ধাপ পরের ধাপের জন্য অনুরোধে তথ্য যোগ করতে পারে — middleware-এর পরিচিত অভ্যাস। - rateLimit. Rina-র গোনা ১ থেকে ২ হলো, সীমা ৩-এর মধ্যে, তাই
next(). গোনা এখনই বাড়ল — অনুরোধটি পরে ব্যর্থ হলেও। - validate — short-circuit. পরিমাণ ৫০, সীমা ১–১০.
validatenext()না ডেকে নিজেই ৪০০ ফেরত দিল।createOrderকখনো চলল না — শিকল এখানেই থামল। - ফেরার পথ।
validate-এর ফেরত মানইrateLimit-এরnext()-এর ফল, সেটিইauth-এর — সবাই একই উত্তর ওপরে পৌঁছে দেয়। - তুলনা: ৫ম অনুরোধ। এবার পরিমাণ বৈধ, কিন্তু গোনা ৩ থেকে ৪ — সীমা পেরোল,
rateLimit-এ ৪২৯. একই শিকলে ভিন্ন অনুরোধ ভিন্ন জায়গায় থামে; কে থামাবে তা আগে থেকে ঠিক নয় — এটিই CoR-এর শক্তি, আর debugging-এর কষ্টও।
ফাঁদ: (১) শিকলের শেষে কেউ নেই — GoF নিজেই সতর্ক করে, অনুরোধ “receipt isn't guaranteed”; একটি default handler বা স্পষ্ট error রাখুন। (২) Express-ধাঁচের middleware-এ উত্তরও না পাঠানো, next()-ও না ডাকা — অনুরোধ ঝুলে থাকে, timeout পর্যন্ত। (৩) উল্টোটা: উত্তর পাঠিয়েও next() ডাকা — দুবার উত্তর পাঠানোর error. (৪) ক্রমের ভুল — auth-এর আগে log করলে পাসওয়ার্ড log-এ চলে যেতে পারে; validate-এর আগে দামি কাজ করলে সম্পদ নষ্ট।
৫ · Mediator — সবার মাঝে একজন সমন্বয়ক
উদ্দেশ্য (GoF): “Define an object that encapsulates how a set of objects interact.” একদল object কীভাবে একে অপরের সাথে কাজ করবে, সেই নিয়মগুলো একটি আলাদা object-এ রাখা; সদস্যরা (GoF বলে colleague) একে অপরকে সরাসরি চেনে না, শুধু সমন্বয়ককে চেনে। এটিই Mediator pattern. GoF-এর উদাহরণ ছিল একটি font বাছাইয়ের dialog box: তালিকায় font বাছলে নমুনা-লেখা বদলায়, কিছু checkbox নিষ্ক্রিয় হয় — widget-রা একে অপরকে না ডেকে একটি “director”-কে জানায়।
বিমানবন্দরের air traffic control (ATC) টাওয়ারের কথা ভাবুন। আকাশে দশটি বিমান থাকলে পাইলটরা একে অপরের সাথে দর-কষাকষি করে ঠিক করেন না কে আগে নামবে; সবাই টাওয়ারের সাথে কথা বলেন, আর টাওয়ার পুরো ছবি দেখে নির্দেশ দেয়। আরেকটি চেনা ছবি — পরিবারের WhatsApp group: প্রত্যেকে আলাদা করে সবাইকে বার্তা পাঠান না, group-এ পাঠান, group সবার কাছে পৌঁছে দেয়।
Mediator ছাড়া ও সহ — ৫টি component-এর যোগাযোগ
Mesh of n(n−1)/2 links vs a star of n links
নিচে একটি কাল্পনিক অফিস-chat-এর ছোট উদাহরণ। Member অন্য কোনো সদস্যকে চেনে না, শুধু room-কে; কাকে বার্তা যাবে, সেই নিয়ম পুরোটা ChatRoom-এ।
class ChatRoom: # the mediator
def __init__(self): self.members = {}
def join(self, m): self.members[m.name] = m; m.room = self
def send(self, sender, text, to=None):
for name, m in self.members.items():
if name != sender.name and to in (None, name):
m.receive(sender.name, text)
class Member: # a colleague: knows only the room
def __init__(self, name): self.name, self.room = name, None
def say(self, text, to=None): self.room.send(self, text, to)
def receive(self, frm, text): print(f"{self.name} <- {frm}: {text}")
room = ChatRoom()
for n in ["Rina", "Karim", "Tuli"]: room.join(Member(n))
room.members["Rina"].say("meeting at 4")
room.members["Karim"].say("ok", to="Rina")Karim <- Rina: meeting at 4
Tuli <- Rina: meeting at 4
Rina <- Karim: ok
কাল যদি নিয়ম আসে “অফিস সময়ের পরে বার্তা জমা থাকবে” বা “Tuli-কে বার্তা গেলে তাঁর সহকারীও পাবেন”, বদলাবে শুধু ChatRoom.send; কোনো Member-এর কোড ছোঁয়া লাগবে না। UI-তে mediator খুব সাধারণ: একটি form-এর controller ঠিক করে “দেশ বাছলে জেলার তালিকা বদলাবে, জেলা বাছলে উপজেলা” — dropdown-রা একে অপরকে চেনে না। পর্ব ৩৯-এর MVC-র controller-এ এই চিন্তাটি আবার দেখবেন।
Mediator, Observer আর Facade — তফাত কোথায়
- Observer (পর্ব ৩৫): একটি subject থেকে অনেক observer-এর দিকে এক-মুখী খবর; subject জানে না কে কী করবে। Mediator-এ যোগাযোগ দুই-মুখী, আর সমন্বয়ের নিয়ম mediator-এর ভেতরে। বাস্তবে mediator প্রায়ই observer দিয়েই বানানো হয় — colleague-রা ঘটনা প্রকাশ করে, mediator শোনে।
- Facade (পর্ব ৩৪, Structural pattern): একটি জটিল subsystem-এর সামনে সহজ দরজা — এক-মুখী, subsystem-এর অংশগুলো facade-কে চেনে না। Mediator-এর colleague-রা mediator-কে চেনে আর তাকে জানায়।
ফাঁদ — god mediator: সব নিয়ম এক জায়গায় আনা সুবিধা, কিন্তু সেই জায়গাটিই ধীরে ধীরে হাজার লাইনের god class হয়ে উঠতে পারে, যা সব colleague-এর খুঁটিনাটি জানে। GoF নিজেই সতর্ক করে, mediator জটিলতা দূর করে না, এক জায়গায় সরিয়ে আনে — mediator নিজেই “a monolith that's hard to maintain” হয়ে উঠতে পারে. প্রতিকার: প্রতিটি সমন্বয়-প্রসঙ্গের জন্য আলাদা ছোট mediator (চেকআউট form-এর একটি, প্রোফাইল form-এর আরেকটি), আর mediator-এর ভেতরে ব্যবসায়িক হিসাব নয়, শুধু “কে কাকে জানাবে”।
৬ · Visitor — কাঠামো স্থির, কাজ নতুন
উদ্দেশ্য (GoF): “Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.” একটি object-কাঠামোর উপাদানগুলোর ওপর একটি কাজকে আলাদা object হিসেবে রাখা, যাতে উপাদানের class না বদলেই নতুন কাজ যোগ করা যায়। এটিই Visitor pattern. GoF-এর উদাহরণ ছিল compiler: abstract syntax tree-র node-গুলো স্থির, কিন্তু তার ওপর কাজ অনেক — type যাচাই, code বানানো, সুন্দর করে ছাপা।
সমস্যা
একটি ছোট হিসাবযন্ত্রের expression tree নিন: Num, Add, Mul. (2 + 3) * 4 হলো একটি Mul, যার বাঁয়ে একটি Add আর ডানে Num(4) — পর্ব ৩৪-এর Composite-এর মতো গাছ। এখন কাজ চাই: মান বের করা, সুন্দর করে ছাপা, গভীরতা মাপা, পরে হয়তো LaTeX-এ রূপান্তর। প্রতিটি কাজের জন্য প্রতিটি node class-এ একটি method যোগ করলে node class-গুলো সব কাজের খুঁটিনাটিতে ভরে যায়, আর নতুন কাজ মানে সব class খোলা। Visitor উল্টো দিকে ভাগ করে: প্রতিটি কাজ একটি class, যার ভেতরে প্রতিটি node type-এর জন্য একটি visit_… method.
Double dispatch — দুই ধাপের বাছাই
বেশিরভাগ OOP ভাষায় x.f(y) লিখলে কোন f চলবে, তা ঠিক হয় শুধু x-এর চলার সময়ের type দেখে — একে বলে single dispatch. কিন্তু Visitor-এ উত্তর নির্ভর করে দুটি type-এর ওপর: কোন node (Add না Num) আর কোন কাজ (Evaluate না Pretty). সমাধান দুটি ডাক: আগে node.accept(visitor) — node-এর type অনুযায়ী বাছাই; তারপর node নিজেই visitor.visit_add(self) ডাকে — visitor-এর type অনুযায়ী বাছাই। এই কৌশলের নাম double dispatch; Smalltalk-প্রসঙ্গে Dan Ingalls-এর ১৯৮৬ সালের OOPSLA প্রবন্ধ এটি পরিচিত করে বলে উদ্ধৃত হয়।
from dataclasses import dataclass
from functools import singledispatch
class Node: # dispatch 1: on the node's class
def accept(self, visitor): return getattr(visitor, "visit_" + type(self).__name__.lower())(self)
@dataclass
class Num(Node): v: int
@dataclass
class Add(Node): l: Node; r: Node
@dataclass
class Mul(Node): l: Node; r: Node
class Evaluate: # dispatch 2: on the visitor object
def visit_num(self, n): return n.v
def visit_add(self, n): return n.l.accept(self) + n.r.accept(self)
def visit_mul(self, n): return n.l.accept(self) * n.r.accept(self)
class Pretty:
def visit_num(self, n): return str(n.v)
def visit_add(self, n): return f"({n.l.accept(self)} + {n.r.accept(self)})"
def visit_mul(self, n): return f"{n.l.accept(self)} * {n.r.accept(self)}"
tree = Mul(Add(Num(2), Num(3)), Num(4))
print(tree.accept(Pretty()), "=", tree.accept(Evaluate()))
@singledispatch # alternative 1: no accept() needed in nodes
def depth(node): raise TypeError(f"depth() has no rule for {type(node).__name__}")
@depth.register
def _(node: Num): return 1
@depth.register(Add)
@depth.register(Mul)
def _(node): return 1 + max(depth(node.l), depth(node.r))
def count_nums(node): # alternative 2: structural pattern matching
match node:
case Num(): return 1
case Add(l, r) | Mul(l, r): return count_nums(l) + count_nums(r)
print("depth:", depth(tree), "| numbers:", count_nums(tree))
try: depth(3.5)
except TypeError as e: print("TypeError:", e)আউটপুট:
(2 + 3) * 4 = 20
depth: 3 | numbers: 3
TypeError: depth() has no rule for float
প্রথম লাইনে একই গাছ দুটি visitor ঘুরল: Pretty লিখল (2 + 3) * 4, Evaluate হিসাব করল ২০. Node class-গুলোতে কোনো হিসাব বা ছাপার কোড নেই — শুধু একটি সাধারণ accept. Java বা C++-এ প্রতিটি node class নিজের accept-এ visitor.visitAdd(this) লেখে; এখানে Python-এর ছোট রূপে Node.accept class-এর নাম থেকে method-এর নাম বানায় — Python-এর নিজের ast.NodeVisitor ঠিক এভাবেই visit_ আর class-এর নাম জুড়ে method খোঁজে। দ্বিতীয় লাইনে দুটি হালকা বিকল্প: functools.singledispatch (Python ৩.৪, PEP 443) প্রথম argument-এর type দেখে function-এর রূপ বাছে — node-এ accept-ই লাগে না; গভীরতা ৩ (Mul → Add → Num). আর structural pattern matching (match, Python ৩.১০) type আর গঠন মিলিয়ে ভেতরের l, r বের করে আনে; সংখ্যা ৩টি (২, ৩, ৪)। শেষ লাইনে depth(3.5)-এর জন্য কোনো নিয়ম নেই, তাই base function-এর TypeError — singledispatch-এ “ভুলে যাওয়া type” ধরা পড়ে চলার সময়ে, compile-এর সময়ে নয়।
নিচের stepper-এ tree.accept(Evaluate())-এর প্রতিটি ডাক দেখুন — প্রতিবার দুটি বাছাই।
1Node.accept(v): getattr(v, "visit_" + cls)(self)2visit_num(n): return n.v3visit_add(n): n.l.accept(self) + n.r.accept(self)4visit_mul(n): n.l.accept(self) * n.r.accept(self)
| ডাক | |
| বাছাই ১ (node) | |
| বাছাই ২ (visitor) | |
| call stack | |
| ফেরত মান |
- প্রথম বাছাই — node-এর type.
tree.accept(Evaluate()).treeএকটিMul, তাইacceptখোঁজেvisit_mulনামের method. কোন কাজ চলছে, node জানে না। - দ্বিতীয় বাছাই — visitor-এর type. Visitor-টি
Evaluate, তাইEvaluate.visit_mulচলে। একই node-এPrettyথাকলে চলতPretty.visit_mul. দুই type মিলে ঠিক একটি method — এটিই double dispatch. - বাঁয়ের শাখা। গুণের আগে বাঁয়ের মান চাই:
n.l.accept(self). বাঁয়ের nodeAdd, তাই আবার দুই বাছাই —Evaluate.visit_add. Visitor নিজেই ঠিক করে কোন ক্রমে গাছ ঘুরবে। - পাতা ২।
Num(2).accept→visit_num→ ফেরত ২. পাতায় পৌঁছে recursion থামে। - পাতা ৩ আর যোগ।
Num(3)ফেরত দেয় ৩;visit_add২ + ৩ = ৫ ফেরত দেয়, আর call stack থেকে সরে যায়। - ডানের শাখা।
visit_mulএবারn.r.accept(self)ডাকে —Num(4)→ ৪. - ফল।
visit_mulফেরত দেয় ৫ × ৪ = ২০. মোট পাঁচটিacceptডাক — গাছের প্রতিটি node-এ একবার — আর প্রতিটির পেছনে দুটি বাছাই। নতুন কাজ (ধরুন LaTeX) যোগ করতে শুধু একটি নতুন visitor class — কোনো node class খুলতে হয় না।
ফলাফল — আর বিখ্যাত দুর্বলতা
সুবিধা: নতুন কাজ যোগ সহজ (একটি নতুন visitor); একটি কাজের সব কোড এক class-এ; visitor ঘোরার পথে অবস্থা জমাতে পারে (যেমন type যাচাইয়ের সময় variable-এর তালিকা)। দুর্বলতা: নতুন element type যোগ করা কঠিন — Sub যোগ করলে প্রতিটি visitor-এ একটি visit_sub লিখতে হয়; ১০টি visitor থাকলে ১০টি class খুলতে হয়। তাই GoF-এর পরামর্শ: কাঠামো স্থির আর কাজ বেশি বদলালে Visitor; কাঠামো ঘন ঘন বদলালে নয়। এটি একটি গভীর প্রশ্নের একটি দিক, যার নাম Philip Wadler ১৯৯৮ সালে দেন expression problem — নতুন data type আর নতুন কাজ, দুটোই পুরোনো কোড না ছুঁয়ে যোগ করা যায় কি না (ভাবনার প্রশ্ন ৪)।
কখন singledispatch বা match যথেষ্ট? Python বা পরিচিত pattern matching-যুক্ত ভাষায় (Rust, Scala, Kotlin-এর when, Java ২১-এর sealed type আর switch pattern) কাজের সংখ্যা কম হলে একটি match-ভিত্তিক function বা singledispatch Visitor-এর চেয়ে অনেক কম কোডে একই কাজ করে, আর node class-এ accept-ই লাগে না। অনেক কাজ আর কাজভিত্তিক অবস্থা থাকলে পূর্ণ Visitor তখনো কাজের। match-এর বিস্তারিত দেখুন Python কোর্সের if/elif/else আর match-case পর্বে; গাছ ঘোরার মৌলিক নিয়ম DSA কোর্সের tree পর্বে।
৭ · Memento আর Interpreter — GoF-এর তালিকা সম্পূর্ণ
- Memento: encapsulation না ভেঙে একটি object-এর ভেতরের অবস্থার ছবি তুলে রাখা, যাতে পরে সেই অবস্থায় ফেরা যায় — text editor-এর undo বা খেলার “save point”-এর ভিত্তি (পরীক্ষার খাতার খসড়া পাতাটি রেখে দেওয়ার মতো)। পর্ব ৩৫-এর Command-এর সাথে প্রায়ই জোড়ায় আসে।
- Interpreter: একটি ছোট ভাষার প্রতিটি ব্যাকরণ-নিয়মকে একটি class বানিয়ে, সেই class-গুলোর গাছ দিয়ে বাক্যের অর্থ বের করা — ওপরের
Num/Add/Mul-এ একটিinterpret()method দিলেই সেটি Interpreter; ব্যাকরণ বড় হলে parser generator ভালো।
এখন GoF-এর ১১টি behavioral pattern-ই আপনার দেখা হলো:
| Pattern | মূল ভাবনা | কোথায় |
|---|---|---|
| Chain of Responsibility | অনুরোধ শিকল ধরে, যে পারে সে সামলায় | এই পর্ব, §৪ |
| Command | অনুরোধকে object বানানো — queue, undo | পর্ব ৩৫ |
| Interpreter | ছোট ভাষার ব্যাকরণ = class-এর গাছ | এই পর্ব, §৭ |
| Iterator | ভেতর না দেখিয়ে একে একে | এই পর্ব, §৩ |
| Mediator | পারস্পরিক যোগাযোগ এক সমন্বয়কে | এই পর্ব, §৫ |
| Memento | অবস্থার ছবি রেখে পরে ফেরা | এই পর্ব, §৭ |
| Observer | বদল হলে নজরদারদের জানানো | পর্ব ৩৫ |
| State | অবস্থা বদলালে আচরণ বদলায় | এই পর্ব, §২ |
| Strategy | বদলযোগ্য algorithm | পর্ব ৩৫ |
| Template Method | কাঠামো base-এ, ধাপ subclass-এ | পর্ব ৩৫ |
| Visitor | স্থির কাঠামোয় নতুন কাজ, double dispatch | এই পর্ব, §৬ |
৮ · কোনটি কখন — গুলিয়ে ফেলা জোড়া আর অতি-ব্যবহার
Pattern-এর নাম মুখস্থ থাকলেও বাস্তবে সমস্যা হয় দুটি জায়গায়: কাছাকাছি দেখতে দুটি pattern গুলিয়ে ফেলা, আর যেখানে দরকার নেই সেখানে pattern বসানো। প্রথমটির জন্য একটি তুলনা:
| জোড়া | মিল | আসল তফাত |
|---|---|---|
| State বনাম Strategy | Context-এ একটি বদলযোগ্য object | State নিজে বদলায় event-এ; Strategy বাছেন client |
| Chain of Responsibility বনাম Decorator | Object-এর শিকল, প্রতিটি পরেরটিকে ডাকে | CoR-এ যেকোনো ধাপ থামাতে পারে; Decorator (পর্ব ৩৪) সাধারণত সবসময় ভেতরেরটিকে ডেকে কিছু যোগ করে |
| Mediator বনাম Observer | Object-রা একে অপরকে সরাসরি চেনে না | Observer এক-মুখী খবর; Mediator-এ সমন্বয়ের নিয়ম কেন্দ্রে, দুই-মুখী |
| Visitor বনাম Iterator | দুটিই একটি কাঠামো ঘোরে | Iterator ঠিক করে কীভাবে একে একে পাবেন; Visitor ঠিক করে প্রতিটি type-এ কী করবেন |
| Visitor বনাম Composite | প্রায়ই একসাথে | Composite গাছটি বানায় (structural); Visitor গাছের ওপর কাজ চালায় (behavioral) |
দ্বিতীয়টির জন্য একটি প্রশ্ন সবসময় করুন: “এই pattern ছাড়া কোডটি কি আজ সত্যিই কষ্টদায়ক?” দুটি অবস্থার জন্য State class, তিন লাইনের validation-এর জন্য পাঁচ handler-এর শিকল, দুটি form field-এর জন্য mediator, বা একটি কাজের জন্য Visitor — সবগুলোই পর্ব ২৬-এর over-engineering. Pattern প্রায়ই সবচেয়ে ভালো আসে refactoring-এর পথে: যখন একই switch তৃতীয়বার লিখতে যাচ্ছেন, বা একটি class পাঁচটি অন্য class-কে সরাসরি ডাকছে — তখন। পর্ব ৪২-এ (code smell আর refactoring) “Replace Conditional with Polymorphism” ধরনের ধাপগুলো দেখবেন; আর পর্ব ৩৭-এ (dependency injection আর composition) দেখবেন, এই পর্বের context, handler আর visitor-গুলো বাইরে থেকে কীভাবে জুড়ে দেওয়া যায়।
আরও গভীরে: মূল উৎস Gamma, Helm, Johnson ও Vlissides-এর Design Patterns: Elements of Reusable Object-Oriented Software (১৯৯৪) — প্রতিটি pattern-এর “Consequences” আর “Implementation” অংশই এই পর্বের ফলাফল ও ফাঁদের উৎস। Iterator-এর ইতিহাস যায় ১৯৭০-এর দশকের মাঝামাঝি Barbara Liskov-এর দলের CLU ভাষায়, যেখানে yield-যুক্ত iterator ছিল; Python-এর PEP 255 নিজেই Icon আর CLU-র কথা বলে। State machine-এর তাত্ত্বিক দিকের জন্য David Harel-এর ১৯৮৭ সালের statecharts প্রবন্ধ, আর Visitor-এর বিকল্প নিয়ে Wadler-এর expression problem-এর লেখা পড়ার মতো।
পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এই ধরনের প্রশ্ন আসতে পারে:
- State আর Strategy-র পার্থক্য কী? — গঠন এক; State নিজে বদলায় ঘটনার জবাবে, Strategy বাইরে থেকে বেছে দেওয়া হয়।
- External আর internal iterator কী? — External-এ client
next()ডেকে এগোয়; internal-এ collection নিজে ঘুরে client-এর function চালায় (forEach). - Express middleware কোন pattern-এর রূপ, আর
next()না ডাকলে কী হয়? — Chain of Responsibility; উত্তরও না দিলে অনুরোধ ঝুলে থাকে। - Visitor-এ নতুন element type যোগ করা কেন কঠিন? — প্রতিটি visitor-এ নতুন
visitmethod লাগে; Visitor কাজ যোগ সহজ করে, type যোগ নয়। - Mediator-এর প্রধান ঝুঁকি? — সব নিয়ম জমে mediator নিজেই god class হয়ে ওঠা।
৯ · AI-এর যুগে behavioral pattern
“এই কোডে State pattern লাগাও” বললে AI কোড-সহকারী কয়েক সেকেন্ডে পাঁচটি class লিখে দেয়। কিন্তু pattern-টি আদৌ দরকার কি না, সে প্রায় কখনো জিজ্ঞেস করে না।
| AI-এর সাধারণ ভুল | কীভাবে ধরবেন |
|---|---|
দুই অবস্থার bool-এর জন্যও State hierarchy, এক কাজের জন্য Visitor — pattern-এর অতি-ব্যবহার | জিজ্ঞেস করুন: pattern ছাড়া সরল রূপটি কেমন? দুটি পাশাপাশি রেখে লাইন আর বোঝার সময় তুলনা করুন |
| State আর Strategy গুলিয়ে ফেলা; UML-এ realization-এর জায়গায় association, বা ভুল দিকের তীর | পর্ব ২৯-এর চিহ্নের নিয়মে প্রতিটি তীর মিলিয়ে দেখুন; “কে বদলায়?” প্রশ্নটি করুন |
Lazy generator pipeline-এর মাঝে list() বা [...x] বসিয়ে laziness নষ্ট; লুপের ভেতরে collection বদলানো | Counter বা log বসিয়ে মাপুন কতবার fetch হলো; লুপের ভেতরে remove/append খুঁজুন |
Middleware যা কোনো পথে উত্তরও দেয় না, next()-ও ডাকে না; বা উত্তর দিয়ে আবার next() | প্রতিটি শাখার জন্য test — প্রতিটি অনুরোধ ঠিক একটি উত্তর পায় কি না; শিকলের শেষে default handler |
নেই এমন API — যেমন standard library-তে functools.multipledispatch (আসলে আছে শুধু singledispatch আর singledispatchmethod; multiple dispatch আলাদা third-party package) | Official documentation দেখুন; import করে চালিয়ে দেখুন |
একটি pattern প্রয়োগের আগে AI-কে বিচারক হিসেবে ব্যবহার করুন, শুধু লেখক হিসেবে নয় — কোডে আসল গ্রাহকের data, API key বা পাসওয়ার্ড থাকলে আগে মুছে নিন:
AI-এর খসড়া হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১State pattern-এর State class, একটি transition টেবিল আর একটি statechart library — বাস্তব project-এ কোনটি কখন বেছে নেবেন?
তিনটিই একই গাণিতিক ধারণার কোড-রূপ: একটি finite state machine — সীমিত সংখ্যক অবস্থা, আর (অবস্থা, ঘটনা) থেকে পরের অবস্থার একটি function. তফাত হলো জ্ঞানটি কোথায় থাকে।
Transition টেবিল (একটি dictionary বা database-এর টেবিল) পুরো জীবনচক্র এক জায়গায় রাখে। পড়া সহজ, test লেখা সহজ (প্রতিটি সারি একটি test), আর অপ্রোগ্রামার — যেমন গ্রন্থাগারের ব্যবস্থাপক — টেবিলটি পড়ে বলতে পারেন নিয়ম ঠিক কি না। কিন্তু প্রতিটি অবস্থায় আচরণ যদি শুধু “পরের অবস্থা” না হয়ে জটিল কাজ হয় (জরিমানা হিসাব, SMS, ভিন্ন validation), টেবিলের পাশে আবার if state == ... ফিরে আসে।
State class ঠিক সেই জায়গায় জেতে, যেখানে প্রতিটি অবস্থার আচরণ সমৃদ্ধ। GoF-এর TCP connection-এর উদাহরণে প্রতিটি অবস্থায় open, close, acknowledge-এর অর্থ সম্পূর্ণ আলাদা — সেখানে একটি class প্রতি অবস্থা স্বাভাবিক। খরচ: জীবনচক্রের পুরো ছবি কয়েকটি file-এ ছড়ানো।
Statechart library আসে যখন অবস্থা নিজেই স্তরে স্তরে সাজানো (composite state), একসাথে একাধিক স্বাধীন দিক চলে (orthogonal region), বা history মনে রাখতে হয় — পর্ব ৩০-এর বিষয়গুলো। David Harel-এর ১৯৮৭ সালের statecharts এই ধারণাগুলো দেয়, আর UML-এর state machine তার ওপর দাঁড়ানো। JavaScript-এ XState বা Java-তে Spring Statemachine-এর মতো library এই নিয়মগুলো (run-to-completion, entry/exit-এর ক্রম) নিজেই রক্ষা করে, আর অনেকে diagram-ও এঁকে দেয়। কিন্তু নতুন dependency আর শেখার খরচ আছে; চার অবস্থার একটি বইয়ের জন্য এটি বাড়াবাড়ি।
একটি ব্যবহারিক নিয়ম: অবস্থা ৫-৬টির কম আর আচরণ সরল হলে enum আর টেবিল; প্রতি অবস্থায় আচরণ আলাদা আর জটিল হলে State class; nested state, parallel region বা অনেক দলের ভাগাভাগি করা জীবনচক্র হলে statechart. আর যেটিই বাছুন, persistence ভুলবেন না — database-এ অবস্থাটি নাম হিসেবে রাখুন ("OnLoan"), object নয়, আর load করার সময় নাম থেকে object বানান।
মূল উপলব্ধি: প্রশ্নটি “pattern ব্যবহার করব কি না” নয়, বরং “state machine-এর জ্ঞান কোথায় থাকলে পড়া, বদলানো আর test করা সবচেয়ে সহজ” — টেবিলে, অবস্থার class-এ, না একটি statechart engine-এ।
প্র ০২CLU থেকে Python আর JavaScript — iterator আর generator কীভাবে একটি “pattern” থেকে ভাষার অংশ হয়ে গেল, আর তাতে কী হারাল?
১৯৭০-এর দশকের মাঝামাঝি MIT-তে Barbara Liskov-এর দল CLU নামে একটি ভাষা বানায়, যার একটি নতুন ধারণা ছিল iterator — একটি বিশেষ procedure যা yield দিয়ে একটি করে মান দেয় আর for লুপ সেই মানগুলো নেয়। Liskov ২০০৮ সালে Turing Award পান, data abstraction-এ অবদানের জন্য; CLU সেই কাজের কেন্দ্রে। ১৯৯৪ সালের GoF বইয়ে Iterator একটি pattern — অর্থাৎ C++ আর Smalltalk-এ হাতে লিখতে হয় এমন কাঠামো — কারণ তখনকার মূলধারার ভাষায় এটি ভাষার অংশ ছিল না।
এরপর ভাষাগুলো pattern-টি গিলে ফেলে। Java ১.২ (১৯৯৮)-এর Collections Framework-এ Iterator interface আসে, আর Java ৫-এ enhanced for. Python ২.১-এ PEP 234 iterator protocol, আর ২.২-এ PEP 255 generator — PEP-টি নিজেই Icon-এর generator আর CLU-র iterator থেকে অনুপ্রাণিত Sather-এর iterator-কে পূর্বসূরি হিসেবে উল্লেখ করে। C# ২.০-তে yield return; JavaScript-এ ES2015-এ Symbol.iterator আর function*, ES2018-এ async iterator. Peter Norvig-এর ১৯৯৬ সালের একটি বহুল উদ্ধৃত উপস্থাপনার যুক্তি ছিল, GoF-এর অনেক pattern dynamic ভাষায় “অদৃশ্য বা সরল” হয়ে যায় — Iterator তার সবচেয়ে স্পষ্ট উদাহরণ।
কী পেলাম: for x in anything — file-এর লাইন, database-এর সারি, অসীম সংখ্যার ধারা — সব একই চুক্তিতে; generator-এ lazy pipeline লেখা প্রায় বিনা খরচে; memory-তে না ধরা data-ও ঘোরানো যায়।
কী হারাল বা লুকাল: (১) generator একবারই ঘোরে — একটি function generator ফেরত দিলে দ্বিতীয় লুপ চুপচাপ খালি পায়, যা নতুনদের প্রায়ই অবাক করে। (২) Laziness-এর কারণে কাজ কখন ঘটে, তা কোড পড়ে বোঝা কঠিন — error ঘটে ব্যবহারের মুহূর্তে, তৈরির নয়; file বা connection বন্ধের সময়ও অনিশ্চিত, তাই finally আর context manager লাগে। (৩) GoF যে “robust iterator” নিয়ে আলোচনা করেছিল — ঘোরার সময় collection বদলালে কী হবে — সেই প্রশ্নটি ভাষার নিয়মে লুকিয়ে গেল; Python dict-এ error দেয়, list-এ চুপচাপ ভুল করে।
মূল উপলব্ধি: একটি pattern সফল হলে ভাষা তাকে নিজের অংশ করে নেয়, তখন pattern-এর নাম হারায় কিন্তু তার নকশা-প্রশ্নগুলো থেকে যায় — কে অবস্থা মনে রাখে, কখন কাজ ঘটে, আর নিচের data বদলালে কী হয়।
প্র ০৩Express বা Django-র middleware কি সত্যিই GoF-এর Chain of Responsibility, নাকি অন্য কিছু?
GoF-এর শুদ্ধ রূপে একটি অনুরোধ শিকল ধরে যায়, আর ঠিক একটি handler সেটি সামলায়; বাকিরা শুধু পাঠিয়ে দেয়। উদ্দেশ্য: পাঠানো পক্ষ জানবে না কে সামলাবে। GoF-এর help-এর উদাহরণ এমনই — বোতাম, dialog, application — যে প্রথম পারে, সে উত্তর দেয়।
Middleware-এ প্রায় প্রতিটি ধাপ কিছু না কিছু করে এবং পাঠায়: logging সব অনুরোধ লেখে, compression সব উত্তর ছোট করে, auth তথ্য যোগ করে পাঠায়। শুধু মাঝে মাঝে কোনো ধাপ থেমে উত্তর দেয় (৪০১, ৪২৯)। এই আচরণ GoF-এর Decorator-এর কাছাকাছি — প্রতিটি স্তর ভেতরেরটিকে মুড়ে কিছু যোগ করে। Koa-র “onion” মডেল এটি আরও স্পষ্ট করে: await next()-এর আগের কোড ঢোকার পথে, পরের কোড ফেরার পথে চলে, ঠিক Decorator-এর মতো। Java EE-র pattern-তালিকায় (Deepak Alur প্রমুখের Core J2EE Patterns, ২০০১) এর আলাদা নামও আছে — Intercepting Filter; Java Servlet-এর Filter আর FilterChain সেই ধারণার রূপ। আবার Unix pipe আর pipe-and-filter architecture (পর্ব ৩৮) একই পরিবারের আরেক আত্মীয়।
তাহলে কোন নামটি ঠিক? Pattern-এর নাম যোগাযোগের হাতিয়ার, শ্রেণিবিভাগের আইন নয়। “Middleware হলো CoR-এর একটি রূপ, যেখানে প্রতিটি handler কাজ করে পাঠাতেও পারে” — এভাবে বললে দলের সবাই বোঝেন: শিকল আছে, ক্রম গুরুত্বপূর্ণ, যেকোনো ধাপ থামাতে পারে। তবে পার্থক্যটি জানা দরকারি, কারণ ভুলের ধরন আলাদা। শুদ্ধ CoR-এ বড় ঝুঁকি “কেউ সামলাল না”; middleware-এ বড় ঝুঁকি “কেউ উত্তর দিল না, আবার next-ও ডাকল না” (অনুরোধ ঝুলে থাকা), বা “দুজন উত্তর দিল”। Express-এ async error-এর জন্য আলাদা চার-parameter-এর error middleware, আর Express ৫-এ promise reject হলে নিজে থেকে error middleware-এ যাওয়া — এগুলো সেই ঝুঁকিগুলোরই প্রতিকার।
মূল উপলব্ধি: Middleware হলো Chain of Responsibility আর Decorator-এর মিশ্রণ; নাম যা-ই দিন, নকশার সিদ্ধান্ত তিনটি — ক্রম, কে থামাতে পারে, আর শিকল কীভাবে নিশ্চিতভাবে শেষ হয়।
প্র ০৪“Expression problem” কী, আর Visitor, OOP-এর সাধারণ method আর functional pattern matching — এর উত্তরে কে কোথায় দাঁড়ায়?
১৯৯৮ সালে Philip Wadler একটি ই-মেইল আলোচনায় পুরোনো একটি টানাপোড়েনের নাম দেন expression problem: একটি data type-এর কয়েকটি ধরন (case) আছে আর কয়েকটি কাজ (operation) আছে — যেমন Num, Add, Mul আর evaluate, pretty. লক্ষ্য: নতুন ধরন এবং নতুন কাজ — দুটোই যোগ করা, পুরোনো কোড recompile বা বদল না করে, আর type-নিরাপত্তা না হারিয়ে।
একটি সারণি কল্পনা করুন: সারিতে ধরন, কলামে কাজ। সাধারণ OOP (প্রতিটি class-এ প্রতিটি কাজের method) সারি ধরে কোড সাজায় — নতুন ধরন মানে একটি নতুন class, সহজ; কিন্তু নতুন কাজ মানে প্রতিটি class খোলা। Functional pattern matching (Haskell, ML, Rust-এর enum আর match, Python-এর match) কলাম ধরে সাজায় — নতুন কাজ মানে একটি নতুন function, সহজ; কিন্তু নতুন ধরন মানে প্রতিটি match খোলা। Visitor হলো OOP ভাষায় functional দিকটি অনুকরণ করা: প্রতিটি visitor একটি কলাম। তাই Visitor-এর বিখ্যাত দুর্বলতা — নতুন element type কঠিন — কোনো দুর্ঘটনা নয়, সারণির উল্টো দিক বেছে নেওয়ার স্বাভাবিক দাম।
বাস্তবে compiler-এ AST-র ধরন তুলনামূলক স্থির (ভাষার ব্যাকরণ বছরে একবার বদলায়), কিন্তু কাজ অনেক — type যাচাই, optimization, code বানানো, lint. তাই compiler আর static analysis tool-এ Visitor এত সাধারণ: Python-এর ast.NodeVisitor, Java compiler API-র tree visitor, অনেক linter. উল্টো দিকে GUI widget-এর জগতে নতুন ধরন ঘন ঘন আসে, তাই সেখানে সাধারণ method-ই স্বাভাবিক।
দুই দিকই একসাথে পাওয়ার চেষ্টাও আছে — Scala-র trait-এর কৌশল, Haskell-এর type class, Clojure-এর protocol আর multimethod, “object algebras”-এর মতো গবেষণার কৌশল। আধুনিক Java-র sealed interface আর switch pattern (Java ২১) একটি আপস দেয়: ধরন সীমাবদ্ধ রাখলে compiler বলে দেয় কোন switch-এ নতুন ধরনটি বাদ পড়েছে — নতুন ধরন যোগ তখনো কাজ, কিন্তু ভুলে যাওয়া অসম্ভব।
মূল উপলব্ধি: প্রতিটি নকশা ঠিক করে কোন দিকের বদল সস্তা হবে; Visitor বেছে নেওয়া মানে বলা “কাঠামো স্থির, কাজ বাড়বে” — সেই ভবিষ্যদ্বাণী ভুল হলে pattern-টি বোঝা হয়ে দাঁড়ায়।
প্র ০৫Air traffic control-কে mediator-এর উপমা বলা হয়। ২০০২ সালের Überlingen দুর্ঘটনা “দুজন সমন্বয়ক” নিয়ে software নকশাকে কী শেখায়?
সাধারণ অবস্থায় আকাশপথ সত্যিই mediator-এর মতো চলে: পাইলটরা একে অপরের সাথে দর-কষাকষি করেন না, controller-এর নির্দেশ মানেন, আর controller পুরো ছবি দেখেন। কিন্তু বিমানে আরেকটি স্বয়ংক্রিয় ব্যবস্থাও থাকে — TCAS (Traffic Collision Avoidance System) — যা কাছাকাছি দুটি বিমানের যন্ত্রের মধ্যে সরাসরি সমন্বয় করে একটিকে ওপরে, অন্যটিকে নিচে যেতে বলে। অর্থাৎ শেষ-রক্ষার জন্য একটি দ্বিতীয়, স্থানীয় সমন্বয়ক।
১ জুলাই ২০০২, জার্মানি-সুইজারল্যান্ড সীমান্তের কাছে Überlingen-এর আকাশে একটি যাত্রীবাহী Tupolev Tu-154 আর একটি DHL মালবাহী Boeing 757 সংঘর্ষে পড়ে; দুই বিমানের সবাই, ৭১ জন, মারা যান। জার্মান তদন্ত সংস্থা BFU-র প্রতিবেদন অনুযায়ী, প্রায় একই মুহূর্তে TCAS Tu-154-কে ওপরে উঠতে বলেছিল, কিন্তু controller — যিনি সেই রাতে একাই দুটি workstation সামলাচ্ছিলেন, আর কিছু যন্ত্র রক্ষণাবেক্ষণের জন্য বন্ধ ছিল — নামতে বলেন। Tu-154-এর ক্রু controller-কে মানেন; 757 TCAS মেনে নামছিল। দুই সমন্বয়ক পরস্পরবিরোধী নির্দেশ দিল, আর কোন নির্দেশ প্রাধান্য পাবে, তা সব দেশের প্রশিক্ষণে একভাবে স্পষ্ট ছিল না। পরে আন্তর্জাতিক নিয়ম আরও স্পষ্ট করা হয় যে TCAS-এর resolution advisory মানতে হবে, controller-এর ভিন্ন নির্দেশ থাকলেও (বিস্তারিত তদন্ত প্রতিবেদনে; এখানে সংক্ষেপ)।
Software-এ শিক্ষাগুলো সরাসরি খাটে। (১) কর্তৃত্ব এক জায়গায়: mediator-এর মূল্যই হলো সমন্বয়ের নিয়ম এক জায়গায় থাকা। পাশে আরেকটি পথে colleague-রা সরাসরি কথা বলতে শুরু করলে (“শুধু এই একটি ক্ষেত্রে”) দুটি সত্যের উৎস তৈরি হয়। (২) Fallback থাকলে অগ্রাধিকারের নিয়ম লিখিত: distributed system-এ cache আর database, বা দুটি leader — দ্বন্দ্বে কে জিতবে, তা নকশায় স্পষ্ট থাকতে হয়। (৩) Mediator-এর ওপর চাপ: god mediator একক ব্যর্থতার বিন্দু — ভাগ করুন।
মূল উপলব্ধি: Mediator জটিলতা কমায় শুধু যদি সে-ই একমাত্র সমন্বয়ক হয়, বা দ্বিতীয় কোনো সমন্বয়কের সাথে অগ্রাধিকারের নিয়ম আগে থেকে লেখা থাকে — দুজন নিয়মহীন সমন্বয়ক একজন না থাকার চেয়েও বিপজ্জনক।
অনুশীলন
Generator-এর আউটপুট অনুমান, pattern চেনা, সংযোগ আর method গোনা, আর একটি ride-এর জীবনচক্রের নকশা — চারটিই পরীক্ষা ও ইন্টারভিউর ধরনের।
-
আউটপুট অনুমান করুন: নিচের JavaScript প্রোগ্রাম চালালে কয়টি লাইন ছাপা হয়, আর কী কী? চালানোর আগে প্রতিটি লাইন লিখুন। বিশেষ করে বলুন:
"end"কখনো ছাপা হয় কি না, আর তৃতীয়console.log-এর শেষ মানটি কী।function* nums() { console.log("start"); for (let i = 1; i <= 3; i++) { console.log("make", i); yield i * 10; } console.log("end"); } const g = nums(); console.log("created"); const [a, b] = g; console.log(a, b, g.next().done); for (const x of nums()) { if (x > 10) break; console.log("use", x); }created start make 1 make 2 10 20 true start make 1 use 10 make 2লাইন ১:
nums()ডাকলে body একটুও চলে না — শুধু একটি থেমে-থাকা generator object তৈরি হয়। তাই “start”-এর আগে “created”. লাইন ২–৪:const [a, b] = gdestructuring iterator protocol ব্যবহার করে ঠিক দুটি মান চায়: প্রথমnext()-এ “start”, “make 1”, তারপরyield 10-এ থামে; দ্বিতীয়টিতে “make 2”,yield 20. তৃতীয় মান চাওয়াই হয় না, তাই “make 3” আসে না। লাইন ৫: ফাঁদটি এখানে — array destructuring দুটি মান নিয়ে iterator-টি বন্ধ করে দেয় (ভেতরেreturn()ডাকে)। তাই পরেরg.next()আর কিছু চালায় না,{ value: undefined, done: true }দেয় —doneহলোtrue. অনেকে ভাবেন এখানে “make 3” আরfalseআসবে। লাইন ৬–৯: নতুন একটি generator; প্রথম মান ১০ — শর্ত মিথ্যা, “use 10”. দ্বিতীয় মানের জন্য “make 2”, মান ২০ > ১০,break.break-ও iterator বন্ধ করে, তাই লুপ শেষ হয়, “make 3” আর “end” কখনো ছাপা হয় না। উত্তর: “end” একবারও আসে না, কারণ দুটি iterator-এর কোনোটিই স্বাভাবিকভাবে শেষ পর্যন্ত ঘোরানো হয়নি; শেষ-পর্যায়ের পরিষ্কারের কাজ রাখতে চাইলেtry...finally-তে রাখুন (যেমন ওপরেরCatalog). মোট ৯টি লাইন — “make 3” বা “end” যোগ করে বেশি লিখলে laziness ভুল বোঝা হয়েছে। -
Pattern চিনুন: প্রতিটি পরিস্থিতিতে কোন behavioral pattern সবচেয়ে মানানসই, আর কেন — এক লাইনে বলুন। (ক) একটি কাল্পনিক মোবাইল ব্যাংকিং app-এ লেনদেন অনুমোদন: ৫,০০০ টাকা পর্যন্ত নিজে থেকেই, ৫০,০০০ পর্যন্ত শাখা ব্যবস্থাপক, তার বেশি হলে প্রধান কার্যালয়। (খ) একটি ভিডিও player-এর play বোতাম Playing অবস্থায় pause করে, Paused-এ resume, Buffering-এ কিছুই করে না। (গ) একটি spreadsheet-এর formula-গাছের ওপর “মান হিসাব”, “কোন cell-এ নির্ভর করে তার তালিকা” আর “formula সুন্দর করে দেখানো” — node-এর ধরন বহু বছর ধরে একই। (ঘ) একটি flight booking form: তারিখ বদলালে ফ্লাইটের তালিকা, ফ্লাইট বদলালে আসনের মানচিত্র আর দাম বদলায়; field-রা একে অপরকে চেনে না। (ঙ) একটি log file ১০ GB — লাইনগুলো একটি একটি করে পড়ে শুধু “ERROR” লাইন গোনা। (চ) একটি ছবি-সম্পাদক app-এ প্রতিটি সম্পাদনার আগে ছবির অবস্থা রেখে দেওয়া, যাতে undo করা যায়। (ছ) ডেলিভারির খরচের হিসাব — ঢাকার ভেতরে, বাইরে, বা express — checkout-এর সময় ক্রেতা বেছে নেন।
- (ক) Chain of Responsibility — অনুমোদনকারীদের শিকল; যিনি সীমার মধ্যে পারেন তিনি সামলান, না পারলে পরের জনকে দেন। শেষে প্রধান কার্যালয় default.
- (খ) State — একই event (
press_play) অবস্থাভেদে ভিন্ন আচরণ, আর অবস্থা নিজেই event-এ বদলায়। - (গ) Visitor (বা Python-এ
singledispatch/match) — কাঠামো স্থির, কাজ বাড়ছে; প্রতিটি কাজ একটি visitor. Formula-গাছটি নিজে Composite. - (ঘ) Mediator — field-দের পারস্পরিক নির্ভরতা একটি form controller-এ; নইলে প্রতিটি field অন্য তিনটিকে চিনত।
- (ঙ) Iterator, lazy — generator বা file object-এর লাইন-ভিত্তিক iterator; পুরো file memory-তে আনা হয় না।
- (চ) Memento — অবস্থার ছবি, ছবির encapsulation না ভেঙে; প্রায়ই Command-এর সাথে।
- (ছ) Strategy — এটি ফাঁদ-প্রশ্ন: বিকল্প পদ্ধতি বেছে নেন client (ক্রেতা), আর এটি ঘটনার জবাবে নিজে বদলায় না — তাই State নয়।
-
গুনে দেখান: (ক) একটি কাল্পনিক বিয়েবাড়ি-আয়োজনের app-এর form-এ ৮টি widget আছে, আর প্রত্যেকে প্রত্যেকের অবস্থা জানতে পারে। সরাসরি কতগুলো যোগাযোগ-জোড়া? Mediator থাকলে কয়টি? নবম widget যোগ করলে দুই ক্ষেত্রে কতগুলো নতুন সংযোগ লাগে? (খ) একটি expression-গাছে ৬টি node type আর ৪টি visitor. মোট কয়টি
visitmethod? নতুন একটি node type যোগ করলে কয়টি পুরোনো class খুলতে হয় আর কয়টি নতুন method লাগে? নতুন একটি visitor যোগ করলে? একই হিসাব করুন যদি Visitor না থাকত, আর প্রতিটি কাজ প্রতিটি node class-এর method হতো।(ক) সরাসরি:
n(n−1)/2 = 8 × 7 / 2 = 28জোড়া। Mediator-সহ: প্রতিটি widget-এর একটি সংযোগ — ৮. নবম widget: সরাসরি পথে তাকে আগের ৮ জনের সাথে জুড়তে হয় — ৮টি নতুন সংযোগ (মোট ৩৬); mediator-এ ১টি (মোট ৯), সাথে mediator-এর ভেতরের নিয়মে কিছু লাইন। সংখ্যাটি দ্বিঘাতে বনাম রৈখিকভাবে বাড়ে — এটিই mediator-এর মূল যুক্তি।(খ) মোট visit method = ৬ × ৪ = ২৪.
- নতুন node type (Visitor-সহ): ১টি নতুন class, আর ৪টি পুরোনো visitor class খুলে প্রত্যেকটিতে একটি করে — ৪টি নতুন method. পুরোনো class খোলা: ৪টি।
- নতুন visitor (Visitor-সহ): ১টি নতুন class, তার ভেতরে ৬টি method; পুরোনো কোনো class খুলতে হয় না — ০.
- Visitor ছাড়া, নতুন node type: ১টি নতুন class, ভেতরে ৪টি কাজের ৪টি method; পুরোনো class খোলা: ০.
- Visitor ছাড়া, নতুন কাজ: ৬টি পুরোনো node class-এর প্রত্যেকটিতে একটি method — খোলা: ৬টি।
হিসাবটি expression problem-এর সারণিটিই সংখ্যায় দেখায়: Visitor কাজ যোগ সস্তা করে (০টি পুরোনো class), node type যোগ দামি করে (৪টি); সাধারণ OOP ঠিক উল্টো (০ বনাম ৬)।
-
নকশা করুন ও সমালোচনা করুন: একটি কাল্পনিক রাইড-শেয়ারিং app-এ একটি ride-এর জীবনচক্র: যাত্রী অনুরোধ করেন → driver নিযুক্ত হন → যাত্রা শুরু → যাত্রা শেষ। যাত্রী বা driver যাত্রা শুরুর আগে বাতিল করতে পারেন; driver নিযুক্ত হওয়ার পরে যাত্রী বাতিল করলে একটি ছোট ফি। এখনকার কোডে
Ride.statusএকটি string, আরassign_driver,start,finish,cancel,fare,notify— ছয়টি method-এর প্রতিটিতে একইif/elif. (ক) অবস্থা, event আর বৈধ transition-এর একটি টেবিল লিখুন। (খ) State pattern-এ কোন class-গুলো, আর transition কে ঠিক করবে — যুক্তিসহ। (গ) অবৈধ event কীভাবে সামলাবেন? (ঘ) ভাড়ার হিসাব (সাধারণ, peak-hour, প্রোমো) State-এ রাখবেন, না অন্য কোনো pattern-এ?(ক) টেবিল:
অবস্থা assign_driver start finish cancel Requested → DriverAssigned অবৈধ অবৈধ → Cancelled (ফি ০) DriverAssigned অবৈধ (বা driver বদল — নীতি ঠিক করুন) → OnTrip অবৈধ → Cancelled (যাত্রী বাতিল করলে ফি) OnTrip অবৈধ অবৈধ → Completed অবৈধ (মাঝপথে থামা আলাদা event) Completed · Cancelled চূড়ান্ত অবস্থা — সব event অবৈধ (খ) গঠন: context
Ride; interfaceRideState(চারটি event-এর method, ডিফল্টেInvalidTransition); concreteRequested,DriverAssigned,OnTrip,Completed,Cancelled. প্রতিটি অবস্থায় আচরণ আলাদা আর সমৃদ্ধ (driver খোঁজা, যাত্রা মাপা, ফি), তাই state class-রা নিজেরাই পরের অবস্থা ফেরত দিক — কোডের জ্ঞান তার অবস্থার পাশে থাকে। তবে ফি-র নিয়ম (“কে বাতিল করল”) একটি guard:DriverAssigned.cancel(ride, by)—by == "rider"হলে ফি. সব data (driver, সময়, দূরত্ব) থাকে context-এ, যাতে অবস্থা বদলালে হারায় না। Database-এ শুধু অবস্থার নাম রাখুন।(গ) অবৈধ event: base class একটি নির্দিষ্ট
InvalidTransition(state, event)তোলে; API স্তর একে HTTP ৪০৯ (Conflict) ধরনের উত্তরে রূপান্তর করে। চুপচাপ উপেক্ষা নয় — দুই ফোন থেকে একসাথেstartআরcancelএলে কোনটি জিতল, তা log-এ দেখা চাই। State pattern একা একসাথে-আসা event সামলায় না; database-স্তরে শর্তসাপেক্ষ update লাগে। প্রতিটি টেবিল-ঘরের জন্য একটি test.(ঘ) ভাড়া: ভাড়ার পদ্ধতি বাছাই অবস্থার ওপর নয়, সময় বা প্রোমোর ওপর, আর ride-এর জীবনভর বদলায় না — এটি Strategy (পর্ব ৩৫)। State-এ রাখলে প্রতিটি অবস্থার class-এ ভাড়ার
ifছড়াবে, যা আমরা সরাতেই চাইছিলাম। প্রোমো আর ছাড় একের পর এক প্রয়োগ হলে সেগুলো একটি শিকল (Chain of Responsibility বা Decorator) হতে পারে। সমালোচনা: বর্তমান কোডের ছয়টি method-এ একই শর্ত মানে নতুন অবস্থা (ধরুনDriverArrived) এলে ছয়টি জায়গা বদলাতে হবে — repeated switch-এর সবচেয়ে পরিষ্কার উদাহরণ।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি বড় switch-ভিত্তিক status কোডকে State pattern-এ refactor করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 🧪 বর্তমান আচরণের test লেখা — প্রতিটি বৈধ ও অবৈধ (অবস্থা, event) জোড়া
- 🗺️ অবস্থা আর event-এর তালিকা ও transition টেবিল বানানো
- 📐 State interface — প্রতিটি event-এর একটি method, ডিফল্টে অবৈধ
- 🧱 প্রতিটি অবস্থার class-এ switch-এর সংশ্লিষ্ট শাখা সরানো
- 🔁 Context-কে বর্তমান state object-এর কাছে কাজ পাঠাতে বদলানো, তারপর পুরোনো switch মুছে test চালানো
State আর Strategy pattern-এর UML গঠন প্রায় এক। দুটির মূল পার্থক্য কোনটি?
- State-এ interface থাকে, Strategy-তে থাকে না
- State structural pattern, Strategy behavioral
- State-এ object-টি ঘটনার জবাবে নিজে বদলায়; Strategy বাইরে থেকে বেছে দেওয়া হয়
- Strategy চলার সময় বদলানো যায় না
JavaScript-এ function* g() { console.log("A"); yield 1; console.log("B"); yield 2; } লেখার পর const it = g(); console.log("C"); it.next(); চালালে কী ছাপা হয়?
- A, তারপর C
- C, তারপর A
- C, A, তারপর B
- শুধু C
Python-এ stock = {"rice": 5} লেখার পর for k in stock: stock["oil"] = 2 চালালে কী ঘটে?
- লুপ চুপচাপ শেষ হয়, stock-এ দুটি key
- নতুন key-ও ঘোরানো হয়, তাই অসীম লুপ
- SyntaxError
- RuntimeError: dictionary changed size during iteration
একটি Express-ধাঁচের web server-এ একটি middleware কোনো শাখায় উত্তরও পাঠায় না, next()-ও ডাকে না। সেই শাখায় পড়া অনুরোধের কী হয়?
- অনুরোধটি ঝুলে থাকে, সাধারণত timeout পর্যন্ত
- Framework নিজে থেকে পরের middleware চালায়
- সাথে সাথে ৪০৪ উত্তর যায়
- পুরো server বন্ধ হয়ে যায়
একটি compiler-এ AST-র ১২টি node type আর ৫টি visitor (type check, optimize, codegen, lint, pretty) আছে। Visitor pattern-এ কোন পরিবর্তনটি সবচেয়ে বেশি পুরোনো class খোলায়?
- একটি নতুন visitor (যেমন documentation বানানো) যোগ করা
- Optimize visitor-এর একটি bug ঠিক করা
- একটি নতুন node type যোগ করা
- Pretty visitor-এর ছাপার ধরন বদলানো
৬টি UI component-এর প্রত্যেকে প্রত্যেকের সাথে সরাসরি কথা বললে কতগুলো যোগাযোগ-জোড়া হয়, আর একটি mediator ব্যবহার করলে কয়টি?
- ৩৬ আর ৬
- ১৫ আর ৬
- ৩০ আর ৬
- ১৫ আর ১
Python-এ functools.singledispatch দিয়ে লেখা area(shape) function সম্পর্কে কোনটি সঠিক?
- সব argument-এর type মিলিয়ে রূপ বাছাই করে
- নিবন্ধিত নয় এমন type দিলে compile-এর সময় error দেয়
- প্রতিটি shape class-এ accept() method লাগে
- প্রথম argument-এর type দেখে নিবন্ধিত রূপ বাছে; না মিললে base function চলে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- State: প্রতিটি অবস্থা একটি class, context শুধু কাজ পাঠায়; অবৈধ event স্পষ্টভাবে প্রত্যাখ্যান। Transition state-রা নিজে বা একটি টেবিল ঠিক করে। Strategy-র সাথে তফাত — কে বদলায়।
- Iterator: iterable নতুন iterator বানায়, iterator জায়গা মনে রাখে। Python
__iter__/__next__/StopIteration, JavaScriptSymbol.iterator/{value, done}; generator lazy আর একবারই ঘোরে; ঘোরার সময় collection বদলাবেন না। - Chain of Responsibility: প্রতিটি handler সামলায় বা পাঠায়; middleware এর চেনা রূপ। ক্রম আচরণের অংশ; শেষে default handler; প্রতিটি পথ ঠিক একটি উত্তর।
- Mediator: n(n−১)/২ সংযোগ নামে n-এ; কিন্তু mediator যেন god class না হয় — প্রসঙ্গভিত্তিক ছোট mediator.
- Visitor: double dispatch; নতুন কাজ সহজ, নতুন element type কঠিন (expression problem). Python-এ
singledispatchআরmatchপ্রায়ই যথেষ্ট। Memento — অবস্থার ছবি; Interpreter — ব্যাকরণ = class-এর গাছ।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৩৭ · Dependency injection আর composition — inversion of control আর inheritance-এর বদলে compositionপরবর্তী পর্বPattern-এর অংশগুলো বাইরে থেকে জোড়া।
- পর্ব ৩৫ · Behavioral pattern ১ — strategy, observer, command আর template methodআগের পর্বStrategy, Observer আর Command — এই পর্বের তুলনার অন্য পক্ষ।
- পর্ব ৩০ · UML behaviour diagram — sequence, activity আর state machine diagramফিরে দেখুনState pattern-এর ছবি — state machine diagram.
- সফটওয়্যার ইঞ্জিনিয়ারিং ও Git · কোর্স হোম৭০টি পর্বপুরো কোর্সের রোডম্যাপ।
- Python · iterator, generator আর yieldপাশের ট্র্যাকIterator protocol আর generator-এর খুঁটিনাটি।
- JavaScript · Express দিয়ে REST APIপাশের ট্র্যাকআসল middleware pipeline আর error middleware.
- System Design · reverse proxy, API gateway আর BFFপাশের ট্র্যাকAuth আর rate limit-এর শিকল server-এর সামনের দরজায়।