সরাসরি পর্বে যান
পর্ব ৩৫ · ৭০-এর মধ্যে · মডিউল ৬

Behavioral pattern ১ — strategy, observer, command আর template method

Behavioral Design Patterns, Part 1 — Strategy, Observer, Command and Template Method

Taught in Bangla, this lesson covers four behavioral patterns: Strategy, often just a function in JavaScript or Python; Observer with push versus pull, ordering, exceptions, unsubscribing and lapsed-listener memory leaks; Command for undo/redo stacks, queues, macros, logging and replay; and Template Method with hooks and the Hollywood principle, ending with when to choose each pattern or none.

এই পর্বে যা শিখবেন

  • চারটি pattern-এর প্রতিটির উদ্দেশ্য, সমস্যা, কাঠামো, কোড, ফলাফল আর চেনা ফাঁদ
  • Function দিয়ে strategy, আর strategy বনাম state-এর প্রথম পরিচয়
  • Observer-এ push বনাম pull, ক্রম, exception, unsubscribe আর lapsed listener-এর memory leak
  • Command দিয়ে undo/redo stack, queue, macro, log আর replay — ধাপে ধাপে
  • Template method-এর hook আর Hollywood principle, আর template method বনাম strategy বেছে নেওয়ার যুক্তি

১ · Behavioral pattern — কে সিদ্ধান্ত নেয়, কে কাকে ডাকে

পর্ব ৩২-এ (Design pattern কী) দেখেছেন, একটি design pattern হলো বারবার আসা একটি নকশা-সমস্যার নাম দেওয়া, পরীক্ষিত সমাধানের ছাঁচ। পর্ব ৩৩-এর creational pattern-এর প্রশ্ন ছিল “object কে বানাবে”, পর্ব ৩৪-এর structural pattern-এর প্রশ্ন “object-গুলো কীভাবে জোড়া লাগবে”। এবার তৃতীয় প্রশ্ন: object-গুলো কীভাবে কাজ ভাগ করবে আর একে অপরের সাথে কথা বলবে। এই দলের নাম behavioral pattern.

১৯৯৪ সালে Erich Gamma, Richard Helm, Ralph Johnson আর John Vlissides প্রকাশ করেন Design Patterns: Elements of Reusable Object-Oriented Software. লেখক চারজন বলে বইটি আর লেখকদল Gang of Four (GoF) নামে পরিচিত। বইয়ের ২৩টি pattern-এর ৫টি creational, ৭টি structural আর ১১টি behavioral. বইটি behavioral দলকে আবার দুই ভাগ করে: যেগুলো class inheritance দিয়ে কাজ ভাগ করে (template method আর interpreter), আর যেগুলো object composition দিয়ে (বাকিগুলো)। এই পর্বের চারটির মধ্যে template method প্রথম দলের, বাকি তিনটি দ্বিতীয় দলের — পার্থক্যটি অংশ ৭-এ কাজে লাগবে।

চারটি pattern-কে চারটি পরিচিত দৃশ্যে ভাবুন:

🧭 Strategy

মিরপুর থেকে মতিঝিল যাবেন — মেট্রো, বাস না রিকশা? গন্তব্য একই, পদ্ধতি বদলায়, আর পদ্ধতিটি আপনি যাত্রার আগে বেছে নেন।

বদলযোগ্য algorithm

📣 Observer

পাড়ার দোকানে বলে এলেন, “চালের দাম কমলে ফোন দেবেন।” দোকানদার জানেন না আপনি কে, শুধু নম্বরের তালিকা রাখেন; দাম বদলালে সবাইকে জানান।

এক থেকে অনেক খবর

🧾 Command

রেস্তোরাঁর ওয়েটার আপনার অর্ডার একটি স্লিপে লেখেন। স্লিপটি সারিতে থাকে, বাতিল করা যায়, দিনের শেষে হিসাবও মেলানো যায় — অনুরোধটি এখন একটি জিনিস।

অনুরোধ = object

📋 Template method

বিয়েবাড়ির আয়োজনের তালিকা: গায়েহলুদ, বিয়ে, বৌভাত — ক্রম নির্দিষ্ট। কিন্তু প্রতিটি পরিবার নিজের মেনু আর সাজ ঠিক করে।

স্থির কাঠামো, বদলযোগ্য ধাপ

প্রতিটি pattern-কে আমরা একই ছকে দেখব: উদ্দেশ্য (GoF-এর ভাষায় intent), সমস্যা (pattern না থাকলে কোড কেমন হয়), কাঠামো (কোন ভূমিকায় কোন object), কোড, ফলাফল (কী পাবেন, কী দাম দেবেন — GoF-এর consequences) আর ফাঁদ। সব উদাহরণ একটি কাল্পনিক অনলাইন বাজার অ্যাপের — চাল, ডাল, ডেলিভারি আর দামের খবর।

মূল ধারণা

Pattern কোনো library নয় যা install করবেন; এটি ভূমিকার একটি নকশা। একই pattern Java-তে interface আর class দিয়ে, JavaScript-এ একটি function দিয়ে, Python-এ একটি dataclass দিয়ে লেখা হতে পারে। নামটি জানলে দলের সাথে কথা ছোট হয়: “এখানে observer বসাই” বললে সবাই বোঝেন কাঠামো কী, আর কোন ফাঁদে নজর রাখতে হবে।

২ · Strategy — একই কাজ, বদলযোগ্য পদ্ধতি

উদ্দেশ্য। GoF-এর ভাষায়: একদল algorithm সংজ্ঞায়িত করুন, প্রতিটিকে আলাদা করে মুড়ে রাখুন, আর তাদের বদলযোগ্য করুন; তাহলে algorithm তার ব্যবহারকারী থেকে স্বাধীনভাবে বদলাতে পারে। বইয়ে এর আরেক নাম Policy. Strategy pattern-এর নাম আপনি পর্ব ২৭-এ (SOLID ১) প্রথম শুনেছেন — সেখানে courier-এর খরচের class-গুলো একটি registry-তে নিবন্ধিত হয়ে open-closed principle মেনেছিল। এই অংশে সেই উদাহরণ আবার নয়; এখানে দেখব strategy কে বাছাই করে, কখন বদলায়, আর function দিয়ে কত হালকাভাবে লেখা যায়।

সমস্যা। একটি কাল্পনিক মুদি-ডেলিভারি অ্যাপের checkout-এ ডেলিভারি ফি হিসাব হয়। নিয়ম অনেক: সমান ফি, দূরত্ব অনুযায়ী, সদস্যদের জন্য ৫০০ টাকার বেশি অর্ডারে ফ্রি, বৃষ্টির দিনে দেড় গুণ। সব নিয়ম checkout()-এর ভেতরে if-else দিয়ে লিখলে function লম্বা হয়, প্রতিটি নিয়ম আলাদা করে test করা যায় না, আর নিয়ম যোগ করতে মূল checkout খুলতে হয়।

কাঠামো। তিনটি ভূমিকা: Strategy — একটি চুক্তি (এখানে “order দিলে ফি ফেরত দাও”); ConcreteStrategy — প্রতিটি নিয়ম; আর Context (context) — যে object একটি strategy ধরে রাখে আর কাজের সময় তাকে ডাকে। Context জানে না কোন নিয়ম চলছে; সে শুধু চুক্তি জানে — এটাই polymorphism.

Function-ই strategy

Java-র পুরোনো সংস্করণে strategy মানে প্রায় সবসময় একটি interface আর কয়েকটি class. কিন্তু JavaScript আর Python-এ function নিজেই একটি মান — variable-এ রাখা যায়, argument-এ পাঠানো যায় (first-class function). একটিমাত্র method-এর strategy-কে তাই একটি function হিসেবে লেখাই স্বাভাবিক। আর যে function অন্য function নেয় বা ফেরত দেয় (higher-order function), তা দিয়ে strategy-কে মুড়ে নতুন strategy বানানো যায়। নিচের কোডে freeOver আর rainSurge ঠিক সেটাই করে।

// Strategy as plain functions: delivery-fee rules of a fictional grocery app.
// Every rule has the same shape: (order) => fee in taka.
const flat = () => 60;
const byDistance = o => 30 + 10 * Math.ceil(o.km);
const freeOver = (limit, base) => o => (o.total >= limit ? 0 : base(o)); // wraps a rule
const rainSurge = base => o => Math.round(base(o) * 1.5);                // wraps a rule

class Checkout {                         // the context: knows only "a rule"
  constructor(rule) { this.rule = rule; }
  setRule(rule) { this.rule = rule; }    // swap the strategy at runtime
  fee(order) { return this.rule(order); }
}

function pickRule({ member, raining }) { // the choice lives in ONE place
  let rule = member ? freeOver(500, byDistance) : byDistance;
  return raining ? rainSurge(rule) : rule;
}

const orders = [{ id: "A", km: 2.5, total: 350 }, { id: "B", km: 6, total: 800 }];
const co = new Checkout(flat);
const show = label =>
  console.log(label.padEnd(18), orders.map(o => `${o.id}=${co.fee(o)}`).join("  "));

show("flat");
const contexts = [
  { member: false, raining: false },
  { member: true, raining: false },
  { member: true, raining: true },
  { member: false, raining: true },
];
for (const ctx of contexts) {
  co.setRule(pickRule(ctx));
  show(`member=${ctx.member ? "Y" : "N"} rain=${ctx.raining ? "Y" : "N"}`);
}

আউটপুট:

flat               A=60  B=60
member=N rain=N    A=60  B=90
member=Y rain=N    A=60  B=0
member=Y rain=Y    A=90  B=0
member=N rain=Y    A=90  B=135

হিসাব: দূরত্বের নিয়মে A-র ২.৫ কিমি ওপরের দিকে গোল হয়ে ৩, তাই ৩০ + ১০ × ৩ = ৬০; B-র ৬ কিমিতে ৩০ + ৬০ = ৯০। সদস্যের নিয়মে B-র অর্ডার ৮০০ টাকা (৫০০-র বেশি), তাই ০; A-র ৩৫০, তাই দূরত্বের নিয়মেই ৬০। বৃষ্টিতে প্রতিটি ফল দেড় গুণ: A ৯০, সদস্য B ০ × ১.৫ = ০, অ-সদস্য B ৯০ × ১.৫ = ১৩৫। তিনটি জিনিস লক্ষ করুন। (১) Checkout-এর ভেতরে একটিও if নেই; নিয়ম চলার সময় setRule দিয়ে বদলানো যায়। (২) কোন নিয়ম চলবে, সেই সিদ্ধান্ত এক জায়গায় — pickRule. Strategy if-else মুছে দেয় না, বাছাইকে এক জায়গায় সরিয়ে আনে। (৩) rainSurge(freeOver(500, byDistance)) — ছোট নিয়ম জোড়া দিয়ে বড় নিয়ম, নতুন class ছাড়াই। প্রতিটি নিয়ম একটি ছোট pure function, তাই আলাদা unit test লেখা সহজ।

ফলাফল আর ফাঁদ

  • লাভ: algorithm-এর পরিবার এক জায়গায়, শর্তের শাখা কম, নতুন নিয়ম মানে নতুন function বা class (পর্ব ২৭-এর open-closed principle), আর চলার সময় বদলানো যায়।
  • দাম: caller-কে জানতে হয় কোন strategy কখন দিতে হবে — বাছাইয়ের দায় কোথাও যেতেই হবে। Object-এর সংখ্যা বাড়ে।
  • ফাঁদ ১ — মোটা চুক্তি: কোনো strategy-র বাড়তি data লাগলে সবার চুক্তিতে parameter বাড়ানোর লোভ হয়। GoF-ও এই দামের কথা বলে: সব strategy একই interface ভাগ করে, তাই সরল strategy-কেও এমন তথ্য নিতে হয় যা তার লাগে না। একটি order object পাঠানো (যেমন এখানে) সাধারণত সবচেয়ে সহজ সমাধান।
  • ফাঁদ ২ — একটিমাত্র রূপের strategy: নিয়ম একটি, বদলের প্রমাণ নেই — তবু interface, factory আর config. এটি পর্ব ২৬-এর YAGNI-র লঙ্ঘন; একটি সাধারণ function রাখুন।
  • ফাঁদ ৩ — strategy-র ভেতরে লুকানো অবস্থা: একটি strategy যদি নিজের ভেতরে গণনা জমায়, তবে দুটি checkout একই object ভাগ করলে ফল মিশে যায়। Strategy যত stateless, তত নিরাপদ।

Strategy বনাম state — প্রথম পরিচয়: পর্ব ৩৬-এর (Behavioral pattern ২) state pattern-এর class diagram প্রায় হুবহু strategy-র মতো: একটি context একটি object-কে কাজ দিয়ে দেয়। পার্থক্য উদ্দেশ্যে। Strategy-তে বাইরের কেউ (client বা pickRule) পদ্ধতি বাছাই করে, আর strategy-রা একে অপরকে চেনে না। State-এ object-এর নিজের অবস্থা বদলায় — অর্ডার “নতুন” থেকে “পাঠানো হয়েছে” — আর প্রায়ই একটি state নিজেই পরের state বসিয়ে দেয়। প্রশ্নটি মনে রাখুন: “বদলটা কে ঘটায়?”

৩ · Observer — একটি ঘটনা, অনেক শ্রোতা

উদ্দেশ্য। GoF: object-দের মধ্যে এক-থেকে-অনেক নির্ভরতা সংজ্ঞায়িত করুন, যাতে একটি object-এর অবস্থা বদলালে তার ওপর নির্ভরশীল সবাই আপনা থেকে খবর পায় আর হালনাগাদ হয়। বইয়ে এর আরও দুটি নাম: Dependents আর Publish-Subscribe. Observer pattern সম্ভবত সবচেয়ে বেশি ব্যবহৃত pattern — browser-এর প্রতিটি click handler এর একটি রূপ।

সমস্যা। বাজার অ্যাপে চালের দাম বদলালে তিনটি কাজ হতে হয়: দাম ৪০০ টাকার নিচে নামলে গ্রাহককে SMS, admin dashboard-এ নতুন সংখ্যা, আর একটি log. সরল সমাধান: setPrice()-এর ভেতরে সরাসরি sms.send(), dashboard.update(), logger.write() ডাকা। এতে দামের class তিনটি অংশকে চেনে — পর্ব ২৫-এর ভাষায় বেশি coupling. চতুর্থ কোনো শ্রোতা (ধরুন একটি push notification) যোগ করতে দামের class খুলতে হয়, আর dashboard ছাড়া দামের class test-ও করা যায় না।

কাঠামো। দুই ভূমিকা। Subject — যার অবস্থার খবর দরকার; সে শ্রোতাদের একটি তালিকা রাখে, আর subscribe (GoF-এ attach), unsubscribe (detach) আর notify দেয়। Observer — একটি ছোট চুক্তি, সাধারণত একটি update() method; JavaScript-এ প্রায়ই শুধু একটি callback function. Subject শুধু চুক্তি জানে, শ্রোতাদের আসল class নয়।

Observer-এর কাঠামো (UML class diagram)

Solid line + open arrow = navigable association; dashed line + hollow triangle = realization

Observer pattern: PriceFeed subject, Observer interface, তিনটি concrete observer «subject» PriceFeed - observers: Set - price: int + subscribe(o) + unsubscribe(o) + setPrice(p) «interface» Observer + update(item, price) 0..* SmsAlert Dashboard Logger error ছোড়ে setPrice(p): list = copy(observers) for o in list: try o.update() catch: record, continue
ছবি ৩৫.১ — Subject-এর কাছে শুধু Observer চুক্তির একটি তালিকা (০ বা তার বেশি — 0..*)। তিনটি concrete observer চুক্তি মেনে চলে; JavaScript কোডে এগুলো class নয়, তিনটি function — ভূমিকা একই। নিচের note-এ notify-এর নিরাপদ নিয়ম, যা অংশ ৪-এর কোডে চলবে। নোটেশনের পূর্ণ আলোচনা পর্ব ২৯-এ (UML class diagram).

Push বনাম pull

খবর দেওয়ার সময় data কীভাবে যাবে, তার দুটি ধরন GoF-এ আছে। Push model-এ subject নিজেই বদলের তথ্য পাঠায়: update("rice-5kg", 395). Pull model-এ subject শুধু বলে “আমি বদলেছি” — update(feed) — আর observer নিজের দরকারমতো feed.price পড়ে নেয়। Push-এ observer-কে subject-এর ভেতরটা জানতে হয় না, কিন্তু subject-কে আন্দাজ করতে হয় কার কী লাগবে। Pull-এ subject সরল থাকে, কিন্তু প্রতিটি observer subject-এর interface-এর ওপর নির্ভর করে, আর “কী বদলাল” খুঁজতে তাকে বাড়তি কাজ করতে হয়। বাস্তবে মিশ্র রূপ বেশি দেখা যায়: একটি ছোট event object, যার ভেতরে ঘটনার নাম আর মূল তথ্য — browser-এর click event যেমন।

ক্রম আর exception — একজনের ভুলে সবাই যেন না থামে

Observer-এর দুটি সূক্ষ্ম প্রশ্ন প্রায়ই ভুলে যাওয়া হয়। প্রথম: ক্রম। বেশিরভাগ বাস্তবায়ন নিবন্ধনের ক্রমে ডাকে, কিন্তু GoF-এর observer ক্রমের কোনো প্রতিশ্রুতি দেয় না। “Dashboard আগে, তারপর SMS” — এমন নির্ভরতা থাকলে সেটা লুকানো coupling; দরকার হলে সেটা স্পষ্ট করুন (দুটি আলাদা ঘটনা, বা একজন অন্যজনকে ডাকে)। দ্বিতীয়: exception. একটি সরল loop-এ তৃতীয় observer error ছুড়লে চতুর্থজন কখনো খবর পায় না, আর error-টি setPrice()-এর caller-এর কাছে গিয়ে পৌঁছায় — যে হয়তো শুধু দাম বসাতে চেয়েছিল। নিরাপদ notify প্রতিটি ডাক try-এর ভেতরে রাখে, ব্যর্থতা জমা রাখে, সবাইকে ডাকা শেষে জানায় বা log করে। তবে error চুপচাপ গিলে ফেলা (swallowed exception) আরেক বিপদ — জমা রাখা error কোথাও দেখাতেই হবে।

নিচের stepper-এ অংশ ৪-এর কোডের প্রথম অর্ধেক ধাপে ধাপে চলছে। তিনজন শ্রোতা নিবন্ধনের ক্রমে আছে: SMS, dashboard, আর একটি logger যার disk ভরা।

rice-5kg PriceFeed-এর notify
ঘটনা
তালিকার কপি
এখন ডাকা
failed
  1. দাম বসল, তালিকার কপি নেওয়া হলো। setPrice(395) প্রথমে নিজের অবস্থা বদলায়, তারপর শ্রোতাদের তালিকার একটি কপি নেয়। কপি কেন? কোনো শ্রোতা খবর পেয়ে নিজেই unsubscribe করলে (বা নতুন কাউকে যোগ করলে) চলমান loop-এর তালিকা যেন মাঝপথে না বদলায়।
  2. SMS শ্রোতা। ৩৯৫ টাকা ৪০০-র কম, তাই সে একটি SMS লাইন ছাপে। খেয়াল করুন, “কখন SMS যাবে” — এই নিয়ম শ্রোতার ভেতরে; subject জানেই না SMS-এর কোনো শর্ত আছে।
  3. Dashboard শ্রোতা। সে কোনো শর্ত ছাড়াই নতুন দাম দেখায়। Push model: দুজনেই (item, price) হাতে পেয়েছে, subject-কে আবার কিছু জিজ্ঞেস করতে হয়নি।
  4. Logger error ছুড়ল। “logger: disk full”. Error-টি catch-এ ধরা পড়ে failed তালিকায় জমা হলো, আর loop থামেনি। Try না থাকলে এখানেই setPrice ভেঙে পড়ত, আর logger-এর পরে কেউ থাকলে খবর পেত না।
  5. সবাইকে ডাকা শেষ — হিসাব। তিনজনকে ডাকা হয়েছে, একজন ব্যর্থ। ব্যর্থতা চুপচাপ হারায়নি, একটি সারাংশ লাইনে ছাপা হলো; বাস্তবে এটি log বা monitoring-এ যাবে।
  6. Unsubscribe. subscribe একটি function ফেরত দিয়েছিল — stopLogger. সেটি ডাকতেই logger মূল তালিকা থেকে সরে গেল; এখন শ্রোতা দুজন।
  7. পরের দাম ৪১০। SMS-এর শর্ত মেলে না (৪১০ ৪০০-র কম নয়), তাই সে চুপ; শুধু dashboard ছাপে। called 2, failed 0 — ব্যর্থ শ্রোতা সরে যাওয়ায় আর কোনো error নেই।

৪ · Observer-এর ফাঁদ — unsubscribe, memory leak আর event emitter

Observer-এর সবচেয়ে চেনা bug-টি কোনো error দেখায় না। একটি screen খোলার সময় subject-এ নিবন্ধন করল, বন্ধ হওয়ার সময় unsubscribe করতে ভুলে গেল। Subject-এর তালিকায় তার callback থেকে গেল, আর callback-এর ভেতর দিয়ে পুরো screen-এর object-ও। Garbage collector সেটি মুছতে পারে না, কারণ subject এখনো তাকে ধরে আছে — এটাই memory leak. এমন “মৃত অথচ তালিকায় জীবিত” শ্রোতাকে বলে lapsed listener. শুধু memory নয় — প্রতিটি নতুন খবরে বন্ধ screen-গুলোও কাজ করে: অকারণ redraw, দুবার SMS, বা বন্ধ হয়ে যাওয়া জিনিসে লিখতে গিয়ে অদ্ভুত error.

ভাবুন, আপনি মহল্লার তিনটি দোকানে নম্বর দিয়ে এসেছিলেন, “দাম কমলে জানাবেন।” পরে বাসা বদলে অন্য শহরে গেলেন, কিন্তু কাউকে বলেননি। দোকানিরা প্রতি সপ্তাহে ফোন করেই চলেন, তালিকাও লম্বা হতে থাকে। দোষটা দোকানির নয় — তালিকা থেকে নাম কাটানোর দায় যিনি নাম লিখিয়েছিলেন, তাঁর।

নিচের কোডের প্রথম অর্ধেক অংশ ৩-এর stepper-এ দেখেছেন। দ্বিতীয় অর্ধেকে একটি product page খোলার সময় subscribe করে; “অসতর্ক” page বন্ধ হওয়ার সময় কিছু করে না, “সতর্ক” page unsubscribe করে। তিনবার খোলা-বন্ধের পরে শ্রোতা গোনা হয়।

// Observer: a price feed in a fictional online bazaar app.
class PriceFeed {                                  // the subject
  #observers = new Set();
  constructor(item, price) { this.item = item; this.price = price; }
  subscribe(fn) { this.#observers.add(fn); return () => this.#observers.delete(fn); }
  get count() { return this.#observers.size; }
  setPrice(p) {
    this.price = p;
    const list = [...this.#observers], failed = []; // snapshot of the list
    for (const fn of list) {
      try { fn(this.item, p); }                    // push model: data goes with the call
      catch (e) { failed.push(e.message); }        // one bad observer must not stop the rest
    }
    console.log(`  -> called ${list.length}, failed ${failed.length}` +
                (failed.length ? ` (${failed.join("; ")})` : ""));
  }
}
const rice = new PriceFeed("rice-5kg", 420);
rice.subscribe((item, p) => { if (p < 400) console.log(`  SMS: ${item} now ${p} taka`); });
rice.subscribe((item, p) => console.log(`  dashboard: ${item} = ${p}`));
const stopLogger = rice.subscribe(() => { throw new Error("logger: disk full"); });
console.log("price 395:"); rice.setPrice(395);
stopLogger();
console.log("price 410:"); rice.setPrice(410);
// A product page subscribes when it opens. Forgetting to unsubscribe = a leak.
const openPage = (feed, id, careful) => {
  const stop = feed.subscribe((item, p) => console.log(`  page#${id} redraws ${p}`));
  return careful ? stop : () => {};                // close(): the careless page does nothing
};
for (const careful of [false, true]) {
  const feed = new PriceFeed("dal-1kg", 140);
  for (let id = 1; id <= 3; id++) openPage(feed, id, careful)();  // open, then close
  console.log(`${careful ? "careful" : "careless"}: 3 pages closed, listeners = ${feed.count}`);
  feed.setPrice(150);
}

আউটপুট:

price 395:
  SMS: rice-5kg now 395 taka
  dashboard: rice-5kg = 395
  -> called 3, failed 1 (logger: disk full)
price 410:
  dashboard: rice-5kg = 410
  -> called 2, failed 0
careless: 3 pages closed, listeners = 3
  page#1 redraws 150
  page#2 redraws 150
  page#3 redraws 150
  -> called 3, failed 0
careful: 3 pages closed, listeners = 0
  -> called 0, failed 0

প্রথম সাত লাইন stepper-এর সাথে হুবহু মেলে — logger-এর error সত্ত্বেও বাকি দুজন খবর পেয়েছে। শেষ অংশটি leak-এর ছবি: অসতর্ক অবস্থায় তিনটি page বন্ধ, তবু শ্রোতা ৩, আর দাম বদলাতেই তিনটি বন্ধ page redraw করছে। Page যত বেশিবার খোলা হবে, সংখ্যা তত বাড়বে — সারাদিন চলা একটি অ্যাপে কয়েকশো। সতর্ক অবস্থায় সংখ্যা ০। subscribe নিজেই unsubscribe function ফেরত দেওয়ায় “কাকে সরাতে হবে” মনে রাখার ঝামেলা নেই — এই নকশা অনেক library-তে দেখা যায়। Set ব্যবহারের আরেকটি লাভ: একই function দুবার যোগ হয় না।

Leak ঠেকানোর অভ্যাস

  • জোড়ায় লিখুন: যেখানে subscribe, তার পাশেই unsubscribe-এর জায়গা — screen-এর “বন্ধ” বা component-এর “মুছে যাওয়া” ধাপে। Code review-তে (পর্ব ৪৪) প্রতিটি subscribe-এর জোড়া খুঁজুন।
  • শ্রোতার আয়ু subject-এর চেয়ে ছোট হলে বিশেষ সতর্কতা: দীর্ঘজীবী subject (পুরো অ্যাপের দামের feed) আর স্বল্পজীবী শ্রোতা (একটি page) — leak-এর ক্লাসিক জুটি।
  • Browser-এ: removeEventListener কাজ করে শুধু একই function reference দিলে; তাই inline arrow function দিয়ে যোগ করলে পরে সরানো যায় না। { once: true } একবার চলেই সরে যায়, আর নতুন browser-এ { signal } option দিয়ে একটি AbortController-এর এক ডাকে অনেক listener একসাথে সরানো যায়। বিস্তারিত JavaScript কোর্সের event আর memory leak পর্বে।
  • Weak reference: কিছু ব্যবস্থা শ্রোতাকে weak reference-এ রাখে (Python-এর weakref.WeakSet/WeakMethod, JavaScript-এর WeakRef), যাতে শ্রোতা অন্য কোথাও ব্যবহৃত না হলে মুছে যেতে পারে। এটি সুরক্ষা-জাল, নকশার বিকল্প নয়: garbage collector কখন চলবে তা অনিশ্চিত, ততক্ষণ মৃত শ্রোতা খবর পেতেই পারে।

Event emitter আর DOM event — observer প্রতিদিন

বাস্তবে আপনি observer বেশিরভাগ সময় নিজে লেখেন না, একটি তৈরি event emitter ব্যবহার করেন: Node.js-এর EventEmitter (on, off, emit) বা browser-এর addEventListener. প্রতিটি ঘটনার নামে (যেমন "price", "click") আলাদা শ্রোতার তালিকা — একটি subject-এ অনেক ধরনের খবর। প্রতিটি শ্রোতা একটি event listener. তবে দুটির exception-এর আচরণ আলাদা, আর এই পার্থক্য interview-তেও আসে:

প্রশ্নNode.js EventEmitterBrowser DOM event
শ্রোতাদের কখন ডাকেসাথে সাথে (synchronous), নিবন্ধনের ক্রমেdispatchEvent-এ synchronous; ব্যবহারকারীর click-এ browser-এর event loop থেকে
একজন শ্রোতা error ছুড়লেError emit-এর caller-এর কাছে যায়; পরের শ্রোতারা ডাক পায় নাError report হয় (console-এ দেখা যায়), কিন্তু পরের শ্রোতারা ডাক পায়
বিশেষ নিয়ম"error" ঘটনার কোনো শ্রোতা না থাকলে emit নিজেই error ছোড়েBubbling: ঘটনা ভেতরের element থেকে বাইরের দিকে যায় — chain of responsibility-র কাছাকাছি (পর্ব ৩৬)
Leak-এর সতর্কসংকেতএকটি ঘটনায় ডিফল্টভাবে ১০টির বেশি শ্রোতা হলে “possible memory leak” warning (setMaxListeners দিয়ে বদলানো যায়)DevTools-এ element-এর listener তালিকা আর memory snapshot

MVC, pub/sub আর observer-এর সীমা

Observer-এর জন্ম আসলে user interface-এর প্রয়োজন থেকে। ১৯৭০-এর দশকের শেষে Xerox PARC-এ Smalltalk-এর জন্য Trygve Reenskaug যে Model-View-Controller ধারণা দেন, তাতে model (data) জানে না কোন view তাকে দেখাচ্ছে; model বদলালে নির্ভরশীল view-দের খবর দেয় — ঠিক observer. এই সম্পর্ক, আর MVP ও MVVM-এ তার রূপ, পর্ব ৩৯-এ (MVC, MVP আর MVVM).

আরেকটি পার্থক্য মনে রাখুন। Observer-এ subject নিজের শ্রোতাদের সরাসরি ধরে রাখে, সাধারণত একই প্রোগ্রামের ভেতরে, প্রায়ই synchronous. Publish-subscribe ব্যবস্থায় মাঝখানে একটি broker বা topic থাকে: প্রকাশক জানে না কে শুনছে, শ্রোতাও জানে না কে পাঠিয়েছে, আর বার্তা প্রায়ই asynchronous, network পেরিয়ে, আলাদা machine-এ যায়। Service-এর মধ্যে এমন বার্তার নকশা — fan-out, delivery-র নিশ্চয়তা, choreography — System Design কোর্সের Pub/sub আর event-driven architecture পর্বে আছে; এখানে আমরা প্রোগ্রামের ভেতরের নকশাতেই থাকছি। একই প্রোগ্রামের ভেতরে অনেক subject আর অনেক শ্রোতার মাঝে একটি কেন্দ্রীয় “event bus” রাখলে সেটি mediator-এর দিকে যায় (পর্ব ৩৬)।

Observer-এর লুকানো দাম হলো প্রবাহ দেখা যায় না। setPrice(395) পড়ে বোঝার উপায় নেই এর পরে কী কী ঘটবে — SMS, log, আরেকটি subject-এর দাম বদল, যা আবার তার শ্রোতাদের ডাকে… এক শ্রোতা যদি একই subject-এর দাম আবার বদলায়, notify-এর ভেতরে notify শুরু হয়, আর অসাবধান হলে অসীম চক্র। Debug করতে তখন একটি sequence diagram (পর্ব ৩০) আঁকা, বা প্রতিটি notify log করা সবচেয়ে কাজের।

৫ · Command — অনুরোধকে একটি object বানানো

উদ্দেশ্য। GoF: একটি অনুরোধকে object হিসেবে মুড়ে রাখুন; তাহলে client-কে ভিন্ন ভিন্ন অনুরোধ দিয়ে সাজানো যায়, অনুরোধ সারিতে রাখা বা log করা যায়, আর undo করা যায় এমন কাজ সমর্থন করা যায়। বইয়ে এর অন্য নাম Action আর Transaction. Command pattern-এর মূল কৌশল একটি বাক্যে: “এখনই করো” ডাককে “করার জিনিস”-এ বদলে দেওয়া।

সমস্যা। একটি কাল্পনিক বাজার অ্যাপের admin panel-এ পণ্যের বিবরণ লেখার ছোট একটি editor. Toolbar-এর বোতাম, keyboard shortcut আর menu — তিনটিই একই কাজ করে (লেখা ঢোকানো, মোছা)। প্রতিটি বোতামের handler সরাসরি document বদলালে: কাজটি তিন জায়গায় লেখা হয়, undo করতে হলে কী বদলেছিল তা কেউ জানে না, আর কোন admin কোন বদল করেছেন তার log রাখার কোনো এক জায়গা নেই।

কাঠামো। চার ভূমিকা। Command — চুক্তি: execute(), আর undo চাইলে undo(). ConcreteCommand — একটি নির্দিষ্ট অনুরোধ, সাথে তার সব তথ্য (কোথায়, কী লেখা)। Receiver — যে আসলে কাজটি জানে (এখানে Document). Invoker — যে command চালায় আর ইতিহাস রাখে (এখানে Editor); বোতাম বা shortcut শুধু একটি command বানিয়ে invoker-কে দেয়। এই বানানোর কাজ যে করে, GoF তাকে Client বলে।

Command-এর কাঠামো (UML class diagram)

Invoker knows only the Command interface; concrete commands hold a reference to the receiver

Command pattern: Editor invoker, Command interface, Insert ও Delete, Document receiver, Client «invoker» Editor - undos: Command[*] - redos: Command[*] + run(c) + undo() + redo() «interface» Command + execute() + undo() 0..* Insert - pos; s + execute() + undo() Delete - pos; n; gone + execute() + undo() Document «receiver» - text: str + insert(pos, s) + delete(pos, n) doc 1 1 Client বোতাম, shortcut «create»
ছবি ৩৫.২ — Editor শুধু Command চুক্তি চেনে; কোন command লেখা ঢোকায় আর কোনটি মোছে, সে জানে না। প্রতিটি concrete command নিজের Document (receiver)-এর reference আর কাজের তথ্য রাখে। Client (বোতাম বা shortcut) command বানায় — ড্যাশ তীর মানে dependency, «create» লেবেল সেই নির্ভরতার ধরন; ছবি পরিষ্কার রাখতে শুধু Delete-এর দিকে আঁকা, Insert-এর ক্ষেত্রেও একই।

Undo আর redo — দুটি stack

Command undo-র সবচেয়ে পরিচিত বাস্তবায়ন দুটি stack দিয়ে, যাকে বলা যায় undo/redo stack: নতুন command চালালে সেটি undo stack-এর ওপরে যায়। Undo মানে ওপরেরটি তুলে তার undo() ডাকা আর redo stack-এ রাখা। Redo মানে redo stack থেকে তুলে আবার execute(). আর একটি নিয়ম, যা অনেকে ভুলে যান: নতুন command এলে redo stack খালি হয় — পুরোনো “ভবিষ্যৎ” আর বৈধ নয়, কারণ লেখা এখন অন্য পথে চলে গেছে।

Undo করতে হলে command-কে জানতে হবে কী উল্টাতে হবে। Insert-এর জন্য সহজ — যতগুলো অক্ষর ঢুকিয়েছি, ততগুলো মুছব। কিন্তু Delete-এর জন্য মোছার আগে মুছে ফেলা লেখাটি মনে রাখতে হবে, নইলে ফেরত আনা অসম্ভব। নিচের stepper-এ অংশের কোডটির প্রথম আটটি ধাপ।

Editor-এর অবস্থা (stack-এর ডান দিকেরটি ওপরে)
কাজ
text
undo stack
redo stack
Delete.gone
  1. প্রথম command. Insert(0,'Dhaka') object তৈরি হলো, Editor.run তার execute() ডাকল, তারপর undo stack-এ রাখল। Text এখন 'Dhaka'.
  2. দ্বিতীয় command. ৫ নম্বর অবস্থানে ' city'. Stack-এ দুটি command — ডানেরটি সবচেয়ে নতুন।
  3. Delete — আর তার স্মৃতি। Delete(0,2) প্রথম দুই অক্ষর মোছে: 'aka city'. মোছার সময় সে মুছে ফেলা লেখা 'Dh' নিজের gone-এ রেখে দিল — undo-র জন্য এটাই একমাত্র সূত্র।
  4. প্রথম undo. ওপর থেকে Delete তোলা হলো; তার undo() ০ নম্বর অবস্থানে 'Dh' ফেরত বসাল — 'Dhaka city'. Command-টি হারায়নি, redo stack-এ গেল।
  5. দ্বিতীয় undo. এবার Insert(5,' city')-এর undo(): ৫ নম্বর থেকে ৫টি অক্ষর মোছা। Text 'Dhaka'; redo stack-এ এখন দুটি, ওপরে Ins city.
  6. Redo. Redo stack-এর ওপরেরটি — Ins city — আবার execute() হয়ে undo stack-এ ফিরল। Del(0,2) এখনো redo-র অপেক্ষায়।
  7. নতুন command — redo stack খালি। Insert(10,'!'): 'Dhaka city!'. এখন Del(0,2) redo করলে সে ধরে নেবে লেখা আগের মতো আছে — কিন্তু লেখা বদলে গেছে। তাই নিয়ম অনুযায়ী redo stack মুছে ফেলা হলো।
  8. খালি redo. Redo চাপলে কিছুই হয় না — text আর দুই stack অপরিবর্তিত। বাস্তব অ্যাপে এ সময় redo বোতাম ধূসর দেখায়।
from dataclasses import dataclass, replace
class Document:                                  # receiver: really edits the text
    def __init__(self): self.text = ""
    def insert(self, pos, s): self.text = self.text[:pos] + s + self.text[pos:]
    def delete(self, pos, n):
        gone, self.text = self.text[pos:pos + n], self.text[:pos] + self.text[pos + n:]
        return gone
@dataclass
class Insert:                                    # a request stored as an object
    doc: Document; pos: int; s: str
    def execute(self): self.doc.insert(self.pos, self.s)
    def undo(self): self.doc.delete(self.pos, len(self.s))
    def __str__(self): return f"Insert({self.pos},{self.s!r})"
@dataclass
class Delete:
    doc: Document; pos: int; n: int; gone: str = ""
    def execute(self): self.gone = self.doc.delete(self.pos, self.n)  # keep what was cut
    def undo(self): self.doc.insert(self.pos, self.gone)
    def __str__(self): return f"Delete({self.pos},{self.n})"
class Editor:                                    # invoker: runs commands, keeps history
    def __init__(self, doc): self.doc, self.undos, self.redos = doc, [], []
    def run(self, c): c.execute(); self.undos.append(c); self.redos.clear(); self.show(f"run {c}")
    def undo(self):
        if self.undos: c = self.undos.pop(); c.undo(); self.redos.append(c)
        self.show("undo")
    def redo(self):
        if self.redos: c = self.redos.pop(); c.execute(); self.undos.append(c)
        self.show("redo")
    def show(self, what): print(f"{what:20} text={self.doc.text!r:14} undo={len(self.undos)} redo={len(self.redos)}")
d = Document(); ed = Editor(d)
ed.run(Insert(d, 0, "Dhaka")); ed.run(Insert(d, 5, " city")); ed.run(Delete(d, 0, 2))
ed.undo(); ed.undo(); ed.redo(); ed.run(Insert(d, 10, "!")); ed.redo()
d2 = Document()                                  # replay the command log on a new document
for c in ed.undos: replace(c, doc=d2).execute()
print("replayed log:", [str(c) for c in ed.undos], "->", repr(d2.text))

আউটপুট:

run Insert(0,'Dhaka') text='Dhaka'        undo=1 redo=0
run Insert(5,' city') text='Dhaka city'   undo=2 redo=0
run Delete(0,2)      text='aka city'     undo=3 redo=0
undo                 text='Dhaka city'   undo=2 redo=1
undo                 text='Dhaka'        undo=1 redo=2
redo                 text='Dhaka city'   undo=2 redo=1
run Insert(10,'!')   text='Dhaka city!'  undo=3 redo=0
redo                 text='Dhaka city!'  undo=3 redo=0
replayed log: ["Insert(0,'Dhaka')", "Insert(5,' city')", "Insert(10,'!')"] -> 'Dhaka city!'

প্রথম আট লাইন stepper-এর আট ধাপ — প্রতিটি লাইনে text আর দুই stack-এর আকার মিলিয়ে দেখুন। শেষ লাইনটি Command-এর আরেকটি শক্তি: undo stack আসলে কার্যকর command-গুলোর একটি log. সেই log নতুন, ফাঁকা একটি document-এ আবার চালালে (replay) একই ফল 'Dhaka city!' পাওয়া গেল। dataclasses.replace প্রতিটি command-এর একটি কপি বানায়, শুধু receiver বদলে। এভাবেই command log দিয়ে crash-এর পরে কাজ ফেরানো, অন্য machine-এ একই বদল চালানো, বা “কে কখন কী বদলেছিলেন” — audit — রাখা যায়। Editor-এ একটিও if isinstance নেই; নতুন command (ধরুন Bold) যোগ করতে Editor ছুঁতে হয় না।

Queue, macro, log আর CQRS

  • Queue: command যেহেতু একটি object, তাকে সারিতে রেখে পরে চালানো যায় — একটি job queue-র প্রতিটি job, বা “রাত ২টায় দাম হালনাগাদ” ধরনের কাজ। চালানোর সময় আর বানানোর সময় আলাদা হয়ে যায়।
  • Macro: কয়েকটি command মিলিয়ে একটি command — macro command. এর execute() সবগুলোকে ক্রমে চালায়, undo() উল্টো ক্রমে উল্টায়। এক undo-তে পুরো “টেমপ্লেট বসাও” কাজ ফেরানো যায়। এটি পর্ব ৩৪-এর composite pattern-এর সাথে command-এর মিলন (Structural pattern).
  • Log আর replay: অংশের কোডে দেখলেন। Database-এর write-ahead log বা একটি game-এর replay-ও একই চিন্তার বড় রূপ।
  • CQRS-এর command: System Design-এর CQRS-এ “command” মানে অবস্থা বদলানোর অনুরোধ-বার্তা, যা একটি handler গ্রহণ করে — অনুরোধকে data বানানোর একই চিন্তা, কিন্তু এটি পুরো architecture-এর read আর write অংশ আলাদা করার নকশা (Event sourcing আর CQRS); আর এর নামের মূলে Meyer-এর command–query separation.

Command-এর ফাঁদ। (১) Undo যা আসলে উল্টায় না: Delete মুছে ফেলা লেখা না রাখলে undo ভুল জিনিস ফেরায় — প্রতিটি command-এর জন্য “execute তারপর undo করলে অবস্থা হুবহু আগের মতো” — এই test লিখুন। (২) উল্টানো যায় না এমন কাজ: একটি SMS পাঠানো বা টাকা কাটা undo করা যায় না; তার জায়গায় নতুন একটি “ক্ষতিপূরণ” কাজ লাগে (ফেরতের SMS, refund) — ঠিক যেমন পর্ব ১১-এর git revert পুরোনো commit মুছে না, উল্টো একটি নতুন commit বানায়। (৩) সীমাহীন ইতিহাস: প্রতিটি command memory ধরে রাখে; বাস্তব editor-এ undo-র সংখ্যার একটি সীমা থাকে। (৪) প্রতিটি method-এর জন্য command class: undo, queue বা log-এর প্রয়োজন না থাকলে একটি সাধারণ function ডাকাই যথেষ্ট।

৬ · Template method — কাঠামো base class-এ, ধাপ subclass-এ

উদ্দেশ্য। GoF: একটি operation-এ algorithm-এর কঙ্কাল সংজ্ঞায়িত করুন, কিছু ধাপ subclass-এর ওপর ছেড়ে দিন; তাহলে subclass algorithm-এর কাঠামো না বদলে নির্দিষ্ট ধাপগুলো নতুন করে লিখতে পারে। Template method pattern-এ যে method কঙ্কালটি ধরে রাখে, সেটিই “template method”.

সমস্যা। বাজার অ্যাপের admin দিনের দামের report নামান — হিসাব বিভাগের জন্য CSV, ওয়েবসাইটে দেখানোর জন্য HTML. দুটির ধাপ একই: শিরোনাম, প্রতিটি পণ্যের সারি, কখনো মোট, শেষে পাদটীকা। দুটি আলাদা function লিখলে ক্রম আর loop দুই জায়গায় কপি হয়; কেউ একটিতে “খালি সারি বাদ দাও” যোগ করলে অন্যটিতে ভুলে যান।

কাঠামো। একটি abstract class-এ template method (export) ধাপগুলোর ক্রম ঠিক করে, আর ধাপগুলো তিন ধরনের: abstract ধাপ — subclass-কে লিখতেই হবে (header, row); ডিফল্টসহ ধাপ — চাইলে override করবে (footer); আর hook — একটি ফাঁকা বা ডিফল্ট উত্তরের method, যা দিয়ে subclass কাঠামোর একটি বিকল্প অংশ চালু-বন্ধ করে (wants_total). GoF নিজেই hook operation-এর কথা বলে, আর পরামর্শ দেয় কোনগুলো hook (override করা ঐচ্ছিক) আর কোনগুলো abstract (বাধ্যতামূলক) তা স্পষ্ট রাখতে।

Hollywood principle

“Don't call us, we'll call you” — আমাদের ডাকবেন না, আমরাই আপনাকে ডাকব। অডিশনের পরে প্রযোজকেরা প্রার্থীদের এ কথা বলেন বলে নাম। GoF template method-এর আলোচনায় এই Hollywood principle-এর উল্লেখ করে: subclass কখনো parent-এর template method ডাকে না, বরং parent ঠিক সময়ে subclass-এর ধাপ ডাকে। নিয়ন্ত্রণ উল্টে যায় — পর্ব ২৮-এ দেখা inversion of control. প্রতিটি framework এই নীতিতে চলে: test framework আপনার setUp আর test method ডাকে, web framework আপনার handler ডাকে।

নিচের stepper-এ HtmlExporter().export(ROWS) চলার সময় কে কাকে ডাকে, তা দেখুন। বাঁ দিকের কোড base class-এর template method-এর সরল রূপ; সবুজ দাগের লাইন এই ধাপে চলছে।

1class Exporter(ABC):2  def export(self, rows):3    out = [self.header()]4    out += [self.row(n, p) ...]5    if self.wants_total():6      out.append(self.row("TOTAL", ...))7    out.append(self.footer())8    return "\n".join(...)
এই মুহূর্তে
ডাক
কার কোড
out-এ লাইন
  1. Client ডাকে template method. HtmlExporter-এ export নেই, তাই base class Exporter-এর export চলে — কিন্তু self একটি HtmlExporter object. পরের প্রতিটি self.… ডাক তাই subclass-এর method খুঁজবে।
  2. প্রথম ধাপ — header. Base class ডাকল self.header(); চলল HtmlExporter.header, ফেরত দিল <table>. ক্রম ঠিক করল base, বিষয়বস্তু দিল subclass.
  3. সারি ১। Loop-এর ভেতরে self.row('rice-5kg', 420) — HTML-এর একটি <tr> সারি।
  4. সারি ২। একই লাইন, দ্বিতীয় পণ্য। Loop লেখা হয়েছে একবারই, base class-এ — CSV আর HTML দুজনেই এটা ভাগ করে।
  5. Hook-এর প্রশ্ন। Base class জিজ্ঞেস করল, “মোট চাই?” ডিফল্ট উত্তর False, কিন্তু HtmlExporter hook override করে True বলে। Subclass কাঠামো বদলায়নি, শুধু একটি ঐচ্ছিক শাখা চালু করল।
  6. মোটের সারি। ৪২০ + ১৪০ = ৫৬০; সারিটি বানাতে আবার subclass-এর row. Hook False হলে (CSV-তে) এই ধাপ বাদ যেত।
  7. Footer. HtmlExporter </table> দিল। CSV-র footer ডিফল্ট "", যা শেষে বাদ পড়ে।
  8. Base class শেষ করে। লাইনগুলো জোড়া দিয়ে ফেরত। পুরো সময় subclass একবারও base class-কে ডাকেনি; base class-ই ছয়বার (header, তিনটি row, wants_total, footer) subclass-কে ডেকেছে — Hollywood principle হাতেনাতে।

একই কাজ, দুই পথে — template method আর strategy

নিচের কোডের ওপরের অর্ধেক stepper-এর template method; নিচের অর্ধেকে একই কাঠামো একটি সাধারণ function, আর বদলযোগ্য ধাপগুলো একটি Format object-এ — অর্থাৎ strategy. দুই পথের আউটপুট মেলানো হয়েছে।

from abc import ABC, abstractmethod
from dataclasses import dataclass, replace
ROWS = [("rice-5kg", 420), ("dal-1kg", 140)]
class Exporter(ABC):                     # TEMPLATE METHOD: skeleton in the base class
    def export(self, rows):              # the fixed order of steps (not overridden)
        out = [self.header()] + [self.row(n, p) for n, p in rows]
        if self.wants_total():           # hook: a default answer subclasses may change
            out.append(self.row("TOTAL", sum(p for _, p in rows)))
        return "\n".join(x for x in out + [self.footer()] if x)
    @abstractmethod
    def header(self): ...
    @abstractmethod
    def row(self, name, price): ...
    def footer(self): return ""          # optional step with a default
    def wants_total(self): return False  # the hook
class CsvExporter(Exporter):
    def header(self): return "item,price"
    def row(self, n, p): return f"{n},{p}"
class HtmlExporter(Exporter):
    def header(self): return "<table>"
    def row(self, n, p): return f"<tr><td>{n}</td><td>{p}</td></tr>"
    def footer(self): return "</table>"
    def wants_total(self): return True
@dataclass(frozen=True)                  # STRATEGY: the varying steps as one object
class Format:
    header: str; row: "Callable[[str, int], str]"; footer: str = ""; total: bool = False
def export(rows, f: Format):             # the same skeleton, written once, as a function
    out = [f.header] + [f.row(n, p) for n, p in rows]
    if f.total: out.append(f.row("TOTAL", sum(p for _, p in rows)))
    return "\n".join(x for x in out + [f.footer] if x)
CSV = Format("item,price", lambda n, p: f"{n},{p}")
HTML = Format("<table>", lambda n, p: f"<tr><td>{n}</td><td>{p}</td></tr>", "</table>", True)
print(CsvExporter().export(ROWS)); print(HtmlExporter().export(ROWS))
print("same:", export(ROWS, CSV) == CsvExporter().export(ROWS), export(ROWS, HTML) == HtmlExporter().export(ROWS))
print(export(ROWS, replace(CSV, total=True)))   # a new combination at runtime, no subclass

আউটপুট:

item,price
rice-5kg,420
dal-1kg,140
<table>
<tr><td>rice-5kg</td><td>420</td></tr>
<tr><td>dal-1kg</td><td>140</td></tr>
<tr><td>TOTAL</td><td>560</td></tr>
</table>
same: True True
item,price
rice-5kg,420
dal-1kg,140
TOTAL,560

CSV-তে মোট নেই (hook-এর ডিফল্ট False), HTML-এ আছে। same: True True প্রমাণ করে দুই নকশা একই লেখা তৈরি করে। পার্থক্য শেষ লাইনে: “মোটসহ CSV” চাইলে template method-এ নতুন একটি subclass (CsvWithTotalExporter) লিখতে হতো; strategy-তে replace(CSV, total=True) — চলার সময় একটি নতুন সমন্বয়, কোনো নতুন class ছাড়া। আর abc-এর লাভটিও দেখুন: কোনো subclass row লিখতে ভুলে গেলে object বানানোর সময়ই TypeError — ভুল দেরিতে নয়, শুরুতেই ধরা পড়ে। Python-এ abstract class আর Protocol-এর পার্থক্য দেখুন ABC, duck typing আর Protocol পর্বে।

ফলাফল আর ফাঁদ

  • লাভ: ক্রম আর সাধারণ কোড এক জায়গায় (DRY); subclass ছোট থাকে; framework বানানোর সবচেয়ে সরল কৌশল।
  • ফাঁদ ১ — fragile base class: subclass parent-এর ভেতরের ক্রমের ওপর নির্ভর করে। Parent-এর লেখক ধাপের ক্রম বা কোনো ধাপের ডাক বদলালে সব subclass চুপচাপ ভাঙতে পারে — একে বলে fragile base class সমস্যা।
  • ফাঁদ ২ — template method override: subclass যদি পুরো export-ই নতুন করে লেখে, pattern-এর উদ্দেশ্য শেষ। Java-তে template method-কে final করা যায়; Python-এ নাম আর documentation দিয়ে জানাতে হয়।
  • ফাঁদ ৩ — hook-এর অরণ্য: প্রতিটি নতুন অনুরোধে একটি hook যোগ করলে base class দশটি “চাইলে বদলান” method-এর জঙ্গল হয়, আর কোন hook কখন ডাকা হয় তা বুঝতে base class-এর পুরোটা পড়তে হয়।
  • ফাঁদ ৪ — দুই অক্ষে বদল: format (CSV/HTML) আর উৎস (দাম/মজুদ) দুটোই বদলালে inheritance-এ ২ × ২ = ৪টি subclass, তিনটি অক্ষে আরও বেশি। Composition-এ প্রতিটি অক্ষ আলাদা strategy.

৭ · চারটি pattern পাশাপাশি — কখন কোনটি, কখন একটিও নয়

Template method আর strategy একই সমস্যার দুই উত্তর: algorithm-এর একটি অংশ বদলাতে হবে। Template method inheritance দিয়ে — বদলের অংশ subclass-এ, সিদ্ধান্ত class লেখার সময়। Strategy object composition দিয়ে — বদলের অংশ একটি আলাদা object-এ, সিদ্ধান্ত চলার সময়। GoF-এর শুরুর পরামর্শ — class inheritance-এর চেয়ে object composition পছন্দ করুন (composition over inheritance) — এই তুলনার পটভূমি; বিস্তারিত পর্ব ৩৭-এ (Dependency injection আর composition). তবু template method টিকে আছে, কারণ ধাপগুলো একে অপরের সাথে ঘনিষ্ঠ হলে (একই state ভাগ করে, একসাথে বদলায়) একটি subclass-এ রাখা অনেক সময় বেশি পরিষ্কার।

Patternযা বদলযোগ্য করেকৌশলকে কাকে ডাকেপ্রতিদিনের উদাহরণপ্রধান ফাঁদ
Strategyপুরো একটি algorithmCompositionContext ডাকে strategy-কে; client বাছাই করেsorted(key=…), ফি বা ছাড়ের নিয়মএকটিমাত্র রূপের জন্য interface; মোটা চুক্তি
Observerকে কে খবর পাবেComposition (শ্রোতার তালিকা)Subject ডাকে সব শ্রোতাকেClick handler, EventEmitter, model-viewUnsubscribe না করা; error-এ loop থামা; লুকানো ক্রম
Commandকোন অনুরোধ, কখন চলবেComposition (অনুরোধ = object)Invoker ডাকে command, command ডাকে receiverUndo/redo, job queue, menu আর shortcutUndo যা উল্টায় না; উল্টানো যায় না এমন কাজ
Template methodএকটি algorithm-এর কিছু ধাপInheritanceBase class ডাকে subclass-এর ধাপTest-এর setUp/tearDown, framework-এর hookFragile base class; hook-এর জঙ্গল

চারটি প্রায়ই একসাথে আসে। একটি বোতাম click হলে (observer) একটি command বানানো হয় (command), যা চালানোর সময় একটি বদলযোগ্য নিয়ম ব্যবহার করে (strategy), আর পুরো কাঠামো একটি framework-এর base class-এর template method মেনে চলে। Factory method (পর্ব ৩৩, Creational pattern) প্রায়ই একটি template method-এর একটি ধাপ — “কোন object বানাব” সিদ্ধান্তটি subclass-এ। আর GoF-এর একটি চমৎকার তুলনা: decorator (পর্ব ৩৪) object-এর “চামড়া” বদলায়, strategy বদলায় “ভেতরটা”।

কখন একটিও নয়

  • নিয়ম একটি, বদলের ইতিহাস নেই — strategy নয়, একটি function (পর্ব ২৬-এর YAGNI; অপ্রয়োজনীয় নমনীয়তা হলো speculative generality).
  • শ্রোতা একজন, আর সে কখনো বদলায় না — observer নয়, সরাসরি ডাক; প্রবাহ পড়া যায়।
  • Undo, queue, log — কোনোটাই লাগে না — command class নয়, একটি method.
  • দুটি “ধাপ”-ই মাত্র আলাদা — template method-এর বদলে একটি parameter বা ছোট function পাঠানো।

আরও গভীরে: চারটি pattern-এর মূল উৎস GoF বইয়ের (১৯৯৪) সংশ্লিষ্ট অধ্যায় — প্রতিটির “Consequences” আর “Implementation” অংশে এই পর্বের অনেক ফাঁদ (notify-এর ভেতরে notify, undo-র জন্য বাড়তি অবস্থা, hook বনাম abstract ধাপ) আলোচিত আছে। Undo-র আরেকটি পথ GoF-এর memento pattern: command কাজ উল্টানোর বদলে আগের অবস্থার একটি snapshot রাখে; দুটি প্রায়ই একসাথে ব্যবহার হয়। Observer-এর আধুনিক উত্তরসূরি reactive programming-এর stream (যেমন ReactiveX), যেখানে ঘটনাগুলো একটি সময়ের ধারা, আর map-filter দিয়ে সাজানো যায়।

পরীক্ষা ও ইন্টারভিউতে:

  • Strategy আর template method-এর পার্থক্য? — দুটিই algorithm-এর অংশ বদলায়; strategy composition দিয়ে, চলার সময়; template method inheritance দিয়ে, class লেখার সময়।
  • Observer-এ memory leak কীভাবে হয়? — দীর্ঘজীবী subject স্বল্পজীবী শ্রোতাকে তালিকায় ধরে রাখে, কারণ unsubscribe হয়নি; শ্রোতা আর তার সব data মুছতে পারে না (lapsed listener).
  • Command দিয়ে undo/redo কীভাবে? — দুটি stack; run → undo stack, undo → তুলে undo() করে redo stack, redo → উল্টো; নতুন command এলে redo stack খালি।
  • Hollywood principle কী? — “আমাদের ডাকবেন না, আমরাই ডাকব”: framework বা base class আপনার কোড ডাকে; inversion of control-এর একটি রূপ।
  • Observer আর pub/sub কি এক? — কাছাকাছি; observer-এ subject শ্রোতাদের সরাসরি চেনে, pub/sub-এ মাঝখানে broker, দুই পক্ষ পরস্পরকে চেনে না, প্রায়ই asynchronous.

৮ · AI-এর যুগে — AI-এর লেখা pattern যাচাই

AI-কে “এখানে design pattern ব্যবহার করো” বললে প্রায় সবসময় কিছু একটা pattern আসবে — দরকার থাক বা না থাক। Pattern-এর নাম আর কাঠামো AI ভালোই জানে; দুর্বলতা আসে ফাঁদগুলোতে, যেগুলো কোড চালিয়ে না দেখলে ধরা পড়ে না।

AI-এর ভুলকীভাবে ধরবেন
একটিমাত্র নিয়মের জন্য strategy interface, factory আর registry — pattern-এর অতিপ্রয়োগপ্রতিটি নতুন abstraction-এর আজকের দ্বিতীয় ব্যবহারকারী দেখাতে বলুন; না থাকলে function
Observer-এ subscribe আছে, unsubscribe নেই; notify-এর loop-এ try নেইScreen বারবার খুলে-বন্ধ করে শ্রোতা গুনুন (অংশ ৪-এর মতো); একটি শ্রোতাকে ইচ্ছা করে error ছুড়তে দিন
Undo যা আগের অবস্থায় ফেরে না — মুছে ফেলা লেখা রাখে না; নতুন command-এ redo stack খালি করে নাপ্রতিটি command-এ “execute তারপর undo = আগের অবস্থা” test; undo-redo-নতুন command-redo ক্রম চালিয়ে দেখুন
দেখতে ঠিক কিন্তু ভুল UML — realization-এ কঠিন রেখা, composition-এর ভরাট diamond যেখানে associationপর্ব ২৯-এর নোটেশনের সাথে প্রতিটি তীর আর multiplicity মিলিয়ে দেখুন
বানানো উদ্ধৃতি — “GoF বলেছে observer সবসময় asynchronous”মূল বইয়ের intent আর consequences অংশ মিলিয়ে দেখুন; সূত্র ছাড়া উদ্ধৃতি গ্রহণ করবেন না
Test যা কিছুই যাচাই করে না — শুধু “update ডাকা হয়েছে” দেখে, ফল নয়ফল যাচাই করুন; ইচ্ছা করে bug ঢুকিয়ে test ব্যর্থ হয় কি না দেখুন (mutation testing, পর্ব ৫০)
অন্য project থেকে হুবহু কোড, license না জানিয়ে; prompt-এ গোপন key বা গ্রাহকের তথ্যবড় অংশের উৎস জিজ্ঞেস করুন, license দেখুন; prompt থেকে গোপন তথ্য সরান

নিচের prompt-এ AI-কে আগে প্রয়োজন যাচাই, পরে কোড, আর শেষে ফাঁদের test দিতে বলা হয়েছে। কোড পাঠানোর আগে key, password আর গ্রাহকের তথ্য সরিয়ে নিন।

নিচে একটি কাল্পনিক বাজার অ্যাপের কোড দিলাম (গোপন তথ্য বাদ দিয়ে)। [এখানে কোড] ১. কোড বদলানোর আগে বলো: strategy, observer, command বা template method-এর কোনোটি কি এখানে সত্যিই দরকার? প্রতিটির পক্ষে কোডের কোন অংশ প্রমাণ, তা লাইন উল্লেখ করে দেখাও। দরকার না হলে "দরকার নেই" লেখো। ২. Observer প্রস্তাব করলে unsubscribe কোথায় হবে দেখাও, আর notify-এর loop-এ একজন শ্রোতার error যেন বাকিদের না থামায়। ৩. Command দিয়ে undo করলে প্রতিটি command-এর জন্য "execute তারপর undo করলে অবস্থা আগের মতো" — এই test দাও, আর নতুন command এলে redo stack খালি হওয়ার test. ৪. কোনো UML দিলে প্রতিটি তীরের ধরন (association, realization, dependency) আর multiplicity লিখে ব্যাখ্যা করো। ৫. GoF বা অন্য কোনো বইয়ের উদ্ধৃতি দিলে বই, বছর আর অধ্যায় লেখো; নিশ্চিত না হলে উদ্ধৃতি দেবে না।

AI-এর দেওয়া pattern হাতে পেলে — চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১Function-কে মান হিসেবে পাঠানো যায় এমন ভাষায় strategy আর command কি অপ্রয়োজনীয় হয়ে গেছে?

প্রশ্নটি পুরোনো। ১৯৯৬ সালে Peter Norvig “Design Patterns in Dynamic Languages” নামের একটি উপস্থাপনায় দেখান, GoF-এর ২৩টি pattern-এর অনেকগুলো — তাঁর হিসাবে ১৬টি — Lisp বা Dylan-এর মতো ভাষায় “অদৃশ্য বা অনেক সরল” হয়ে যায়। কারণ সেসব ভাষায় function প্রথম শ্রেণির মান: একটি function variable-এ রাখা যায়, argument-এ পাঠানো যায়, অন্য function ফেরত দিতে পারে। তখন strategy মানে শুধু একটি function পাঠানো — interface আর class-এর আনুষ্ঠানিকতা লাগে না। Command-ও প্রায়ই একটি closure: যে function তার প্রয়োজনীয় তথ্য নিজের ভেতরে ধরে রাখে।

মূলধারার ভাষাও সেই পথে গেছে। Python-এর sorted(items, key=...)-এর key একটি strategy; JavaScript-এর array.sort((a, b) => ...)-ও তাই। Java ২০১৪ সালে Java 8-এ lambda যোগ করার পরে Comparator-এর জন্য আলাদা class লেখার প্রয়োজন প্রায় উঠে গেছে।

তবু pattern-টি মরেনি, বদলেছে তার পোশাক। “Strategy” নামটি এখনো একটি নকশার সিদ্ধান্ত বোঝায়: একটি কাজের পদ্ধতি বাইরে থেকে দেওয়া হবে, আর caller শুধু চুক্তি জানবে। ভূমিকাটি থাকে, শুধু কাঠামো হালকা হয়। আর কিছু ক্ষেত্রে class এখনো ভালো: যখন strategy-র কয়েকটি method একসাথে লাগে (দাম আর বিবরণ আর বাতিলের নিয়ম), যখন তাকে নাম দিয়ে log বা config-এ রাখতে হয়, বা যখন command-কে undo করতে হয় — একটি closure-কে “উল্টো করো” বলা যায় না, কিন্তু execute আর undo দুটি method-এর object-কে বলা যায়। Command-কে queue-তে রেখে আরেকটি machine-এ পাঠাতে হলে সেটিকে data হিসেবে serialize করতে হয় — function-কে সাধারণত পাঠানো যায় না, কিন্তু {"type": "insert", "pos": 0, "s": "Dhaka"} পাঠানো যায়।

অর্থাৎ ভাষা যত শক্তিশালী, pattern-এর কাঠামো তত পাতলা; কিন্তু pattern-এর চিন্তা — কী বদলায়, কে বাছাই করে, কী মনে রাখতে হয় — ভাষা থেকে স্বাধীন।

মূল উপলব্ধি: Pattern হলো ভূমিকার নকশা, class-এর নয়; first-class function সেই ভূমিকার কাঠামো ছোট করে, কিন্তু undo, নাম, serialize বা একাধিক method লাগলে object আবার ফিরে আসে।

প্র ০২এত জনপ্রিয় হয়েও observer pattern-কে কেন কেউ কেউ “অবসর দেওয়ার” কথা বলেছেন? এর বিকল্প কী?

Observer-এর জন্ম user interface থেকে। Smalltalk-80-এ model-এর “dependents” তালিকা আর MVC-র view-হালনাগাদ এর পূর্বসূরি, আর GoF-এ (১৯৯৪) এটি Dependents নামেও আছে। Java-র প্রথম দিক থেকেই ছিল java.util.Observable class আর Observer interface. কিন্তু ২০১৭ সালে Java 9-এ সেই দুটি deprecated ঘোষিত হয়। কারণ হিসেবে বলা হয়েছিল এদের ঘটনার মডেল খুব সীমিত — কোনো ধরন বা ক্রমের প্রতিশ্রুতি নেই, thread-এর নিরাপত্তা নেই — আর এর বদলে java.beans-এর property-change ব্যবস্থা বা concurrency-র নতুন যন্ত্র ব্যবহারের পরামর্শ দেওয়া হয়। আরেকটি নকশাগত দুর্বলতা: Observable ছিল একটি class, interface নয়; তাই একটি class একসাথে অন্য কিছুর subclass আর observable হতে পারত না।

২০১০ সালে EPFL-এর Ingo Maier, Tiark Rompf আর Martin Odersky “Deprecating the Observer Pattern” শিরোনামে একটি প্রতিবেদন লেখেন (পরে Scala.React নামে বিস্তৃত)। তাঁদের অভিযোগগুলো এই পর্বেরই ফাঁদ: callback-এর প্রবাহ কোড পড়ে বোঝা যায় না; অনেক observer মিলে একটি ঘটনার ক্রম সামলাতে হলে মানুষের হাতে লেখা state machine জড়িয়ে যায়; side effect ছড়িয়ে পড়ে; শ্রোতা সরানো (resource management) ভুলে যাওয়া সহজ; আর একাধিক subject বদলালে কিছু শ্রোতা মাঝামাঝি, অসংগত অবস্থা দেখতে পায় — যাকে “glitch” বলা হয়।

বিকল্প হিসেবে এসেছে reactive programming. Microsoft-এ Erik Meijer-এর দলের Reactive Extensions (Rx) ঘটনাকে একটি সময়ের ধারা (stream) হিসেবে দেখে: “দামের ধারা থেকে ৪০০-র নিচেরগুলো ছেঁকে নাও, প্রতি সেকেন্ডে একবারের বেশি নয়” — map, filter, debounce দিয়ে ঘোষণামূলকভাবে লেখা যায়, আর subscription একটি object যা একবারে বাতিল করা যায়। আধুনিক UI framework-গুলোতেও “signal” বা স্বয়ংক্রিয় নির্ভরতা-ট্র্যাকিং দেখা যায়, যেখানে কোন view কোন data পড়েছে তা framework নিজেই মনে রাখে আর নিজেই শ্রোতা যোগ-বিয়োগ করে।

লক্ষ করুন, এগুলোর কোনোটিই observer-এর মূল চিন্তা — “subject শ্রোতাদের চেনে না, শুধু চুক্তি চেনে” — বাদ দেয়নি। বাদ দিয়েছে হাতে-হাতে তালিকা সামলানোর ঝুঁকি। ছোট প্রোগ্রামে একটি সাধারণ event emitter এখনো সবচেয়ে সরল আর ভালো সমাধান।

মূল উপলব্ধি: Observer-এর দুর্বলতা তার ধারণায় নয়, হাতে লেখা ব্যবস্থাপনায় — unsubscribe, ক্রম, glitch আর অদৃশ্য প্রবাহ; reactive stream আর signal সেই ব্যবস্থাপনা কাঠামোর হাতে তুলে দেয়।

প্র ০৩Undo বাস্তবে কতটা কঠিন? Command ছাড়া আর কী কী পথ আছে, আর কোথায় undo অসম্ভব?

Undo-র তিনটি প্রধান কৌশল আছে। প্রথম, অংশ ৫-এর মতো উল্টো কাজ: প্রতিটি command জানে নিজেকে কীভাবে উল্টাতে হয়। এটি memory-তে সাশ্রয়ী, কিন্তু প্রতিটি command-এর undo নির্ভুল হতে হয়, আর একটি ভুল undo পরের সব undo-কে ভুল অবস্থা থেকে শুরু করায়। দ্বিতীয়, snapshot: প্রতিটি কাজের আগে পুরো অবস্থার একটি কপি রাখা — GoF-এর memento pattern-এর ধারণা। সরল আর নির্ভরযোগ্য, কিন্তু বড় document বা ছবির ক্ষেত্রে memory খরচ বেশি; তাই অনেক সময় শুধু বদলানো অংশের কপি রাখা হয়। তৃতীয়, log থেকে পুনর্গঠন: শুরুর অবস্থা থেকে শেষেরটি বাদে সব command আবার চালানো। এটি event sourcing-এর চিন্তার কাছাকাছি, তবে দীর্ঘ log-এ ধীর, তাই মাঝে মাঝে snapshot নিয়ে মেশানো হয়।

দলীয় সম্পাদনায় (একই document-এ কয়েকজন একসাথে লিখছেন) সমস্যা আরও জটিল। আমার “undo” কি শুধু আমার শেষ কাজ উল্টাবে, নাকি সবার? আমার মোছার পরে অন্য কেউ সেই জায়গায় লিখে থাকলে আমার পুরোনো Delete(0,2)-এর অবস্থান আর সঠিক নয়। অনেক collaborative editor এজন্য operational transformation বা CRDT-র মতো কৌশল ব্যবহার করে, যেখানে একটি command-এর অবস্থান অন্যদের command অনুযায়ী বদলে নেওয়া হয়। অংশ ৫-এ redo stack খালি করার নিয়মটি এই সমস্যারই এক-ব্যবহারকারী রূপ: লেখা বদলে গেলে পুরোনো command-এর অনুমান আর খাটে না।

আর কিছু কাজ উল্টানোই যায় না। পাঠানো SMS ফেরত আসে না, কাটা টাকা আপনা থেকে ফেরত যায় না, ছাপা receipt মুছে যায় না। সেখানে undo-র জায়গায় নতুন একটি “ক্ষতিপূরণমূলক” কাজ — দুঃখপ্রকাশের বার্তা, refund. Git-এর নকশায়ও একই দর্শন: পর্ব ১১-এ দেখেছেন, ভাগ করা history-তে git revert পুরোনো commit মুছে না, তার উল্টো বদলের একটি নতুন commit বানায়, কারণ অন্যরা পুরোনোটি ইতিমধ্যে নিয়ে গেছেন।

মূল উপলব্ধি: Undo একটি প্রতিশ্রুতি — “অবস্থা হুবহু আগের মতো হবে”; উল্টো কাজ, snapshot আর log সেই প্রতিশ্রুতির তিন রকম দাম, আর বাইরের জগতে যাওয়া কাজের ক্ষেত্রে প্রতিশ্রুতিটি শুধু ক্ষতিপূরণ দিয়েই রাখা যায়।

প্র ০৪একজন observer error ছুড়লে কী হওয়া উচিত — বাকিদের ডেকে যাওয়া, না সবকিছু থামানো? Node.js আর browser দুই রকম কেন?

এখানে দুটি ভালো নীতি একে অপরের বিরুদ্ধে দাঁড়ায়। একদিকে বিচ্ছিন্নতা: observer-এর পুরো উদ্দেশ্য হলো subject আর শ্রোতারা একে অপরকে না চেনে। তাহলে একজন অচেনা শ্রোতার disk ভরে যাওয়ায় দাম হালনাগাদ বা অন্য শ্রোতাদের SMS আটকে যাবে কেন? অন্যদিকে দ্রুত ব্যর্থতা (fail fast): ভুল চুপচাপ এগিয়ে যেতে দিলে অসংগত অবস্থা তৈরি হয় — হয়তো logger ব্যর্থ হওয়ায় audit-এর রেকর্ডই নেই, অথচ দাম বদলে গেছে।

দুটি প্রচলিত ব্যবস্থা দুই দিকে ঝুঁকেছে। Browser-এর DOM-এ একটি event listener error ছুড়লে browser সেটি report করে (console-এ, আর page-এর global error handler-এ), কিন্তু একই ঘটনার পরের listener-দের ডাকতেই থাকে। কারণ একটি web page-এ নানা উৎসের script থাকে — একটি বিজ্ঞাপন বা analytics script-এর ভুলে page-এর বোতাম অচল হলে চলবে না। Node.js-এর EventEmitter অন্যদিকে সরল আর synchronous: emit একটি সাধারণ loop-এর মতো, তাই একজন শ্রোতার error সরাসরি emit-এর caller-এর কাছে উঠে যায়, আর পরের শ্রোতারা ডাক পায় না। Server-এর প্রোগ্রামে ধরে নেওয়া হয় শ্রোতারা একই দলের লেখা, আর একটি না-ধরা error-কে লুকিয়ে রাখার চেয়ে জোরে জানানো ভালো। "error" নামের ঘটনার বিশেষ নিয়মও (শ্রোতা না থাকলে emit নিজেই error ছোড়ে) এই দর্শনের অংশ।

আপনার নিজের subject-এ কোনটি বাছবেন? প্রশ্ন করুন: শ্রোতারা কি একই বিশ্বাসের সীমানায়? একটি শ্রোতার ব্যর্থতা কি পুরো কাজটিকেই অর্থহীন করে? Audit log বাধ্যতামূলক হলে সেটিকে আদৌ observer হিসেবে রাখা উচিত কি না, তাও ভাবুন — হয়তো সেটি মূল কাজেরই অংশ, যা একই transaction-এ হতে হবে। ঐচ্ছিক কাজ (SMS, dashboard) observer-এ, বাধ্যতামূলক কাজ সরাসরি ডাকে — এই ভাগটিই প্রায়ই সবচেয়ে পরিষ্কার। আর ক্রমের নির্ভরতা থাকলে (“আগে মজুদ কমাও, তারপর SMS”) সেটিও observer-এর অলিখিত ক্রমের ওপর ছেড়ে দেবেন না।

অংশ ৪-এর কোডের নকশা একটি মাঝামাঝি পথ: সবাইকে ডাকা, error জমা রাখা, শেষে জানানো। এটি বিচ্ছিন্নতা রাখে, আবার ব্যর্থতাকে হারিয়েও যেতে দেয় না।

মূল উপলব্ধি: Observer-এ error-নীতি একটি নকশার সিদ্ধান্ত, স্বয়ংক্রিয় কিছু নয়; শ্রোতারা অচেনা আর ঐচ্ছিক হলে বিচ্ছিন্নতা, অপরিহার্য হলে তারা আসলে observer-ই নয়।

প্র ০৫Framework-গুলো template method আর Hollywood principle-এর ওপর দাঁড়িয়ে। তবু অনেক framework কেন inheritance থেকে composition-এর দিকে সরেছে?

“Don't call us, we'll call you” — software-এ এই কথাটির প্রচলিত প্রথম উৎস হিসেবে Richard E. Sweet-এর ১৯৮৫ সালের Mesa programming environment নিয়ে একটি প্রবন্ধের কথা বলা হয়, যেখানে একে “Hollywood's Law” বলা হয়েছিল (উৎস নিয়ে নিশ্চিত হতে মূল প্রবন্ধ মিলিয়ে নিন)। GoF ১৯৯৪ সালে template method-এর আলোচনায় এই নামটি জনপ্রিয় করে। চিন্তাটি framework-এর সংজ্ঞাই: library-কে আপনি ডাকেন, framework আপনাকে ডাকে।

প্রথম দিকের অনেক framework template method দিয়ে বানানো। Kent Beck আর Erich Gamma-র JUnit-এর প্রথম সংস্করণগুলোতে প্রতিটি test একটি TestCase-এর subclass; framework একটি স্থির ক্রমে চালায় — setUp, test method, tearDown. Python-এর unittest.TestCase আজও ঠিক এভাবে চলে। Java-র AbstractList-এ আপনি শুধু get আর size লেখেন, বাকি অনেক method সেগুলো ডেকে কাজ করে। GUI toolkit-এ একটি widget-এর subclass বানিয়ে paint override করা — একই ছাঁচ।

সময়ের সাথে এর দামও স্পষ্ট হলো। Subclass parent-এর ভেতরের ক্রম আর state-এর ওপর নির্ভর করে, তাই framework-এর নতুন সংস্করণে ভেতরের সামান্য বদলে ব্যবহারকারীর কোড ভাঙে — fragile base class. একটি class একটিই parent পায় (অনেক ভাষায়), তাই দুটি framework-এর base class একসাথে নেওয়া যায় না। আর পুনর্ব্যবহারযোগ্য আচরণ (ধরুন “data আনো আর লোড হওয়া পর্যন্ত spinner দেখাও”) এক subclass থেকে আরেকটিতে নিতে গভীর উত্তরাধিকারের গাছ লাগে। JUnit 4 (২০০৬) subclass-এর বদলে annotation-এ চলে যায়। React ২০১৯ সালে (সংস্করণ 16.8) hooks আনে, যার একটি বড় যুক্তি ছিল class component-এর lifecycle method-এ ছড়িয়ে থাকা সম্পর্কিত কোডকে ছোট, জোড়া দেওয়া যায় এমন function-এ আনা — template method থেকে composition-এর দিকে যাত্রা।

তবু Hollywood principle বাদ যায়নি — React এখনো আপনার component ডাকে, JUnit এখনো আপনার test ডাকে। বদলেছে “কীভাবে নিজেকে framework-এ জুড়বেন”: subclass-এর বদলে একটি function, একটি annotation, বা একটি object পাঠিয়ে — অর্থাৎ strategy আর observer-এর ধাঁচে। ছোট, অগভীর, স্থির ক্রমের ক্ষেত্রে template method এখনো সবচেয়ে সরল সমাধান।

মূল উপলব্ধি: নিয়ন্ত্রণ উল্টানো framework-এর প্রাণ, কিন্তু সংযোগের পথ হিসেবে inheritance দামি; তাই আধুনিক framework আপনাকে ডাকে ঠিকই, তবে subclass-এর বদলে আপনার পাঠানো function বা object-কে।

অনুশীলন

একটি আউটপুট অনুমান, একটি pattern চেনার সেট, একটি নকশার কাজ আর একটি হিসাব — প্রতিটির পূর্ণ উত্তর দেওয়া আছে।

  1. আউটপুট অনুমান করুন (undo/redo): অংশ ৫-এর Document, Insert, Delete আর Editor class হুবহু একই রেখে নিচের লাইনগুলো চালানো হলো। প্রতিটি ডাক একটি লাইন ছাপে (text=… undo=… redo=…)। দশটি লাইনের প্রতিটিতে text আর দুই stack-এর আকার কী হবে?
    d = Document(); ed = Editor(d)
    ed.run(Insert(d, 0, "tea")); ed.run(Insert(d, 3, " cake")); ed.run(Delete(d, 0, 4))
    ed.undo(); ed.undo()
    ed.run(Insert(d, 3, "!")); ed.redo(); ed.undo(); ed.undo(); ed.redo()
    run Insert(0,'tea')  text='tea'          undo=1 redo=0
    run Insert(3,' cake') text='tea cake'     undo=2 redo=0
    run Delete(0,4)      text='cake'         undo=3 redo=0
    undo                 text='tea cake'     undo=2 redo=1
    undo                 text='tea'          undo=1 redo=2
    run Insert(3,'!')    text='tea!'         undo=2 redo=0
    redo                 text='tea!'         undo=2 redo=0
    undo                 text='tea'          undo=1 redo=1
    undo                 text=''             undo=0 redo=2
    redo                 text='tea'          undo=1 redo=1

    ব্যাখ্যা: Delete(0,4) প্রথম চারটি অক্ষর 'tea ' মুছে gone-এ রাখে, তাই প্রথম undo সেটি ফেরত দেয়। দ্বিতীয় undo ' cake' সরায়। এরপর নতুন Insert(3,'!') — redo stack-এর দুটি command (Del আর Ins cake) মুছে যায়, তাই পরের redo-তে কিছু হয় না (redo=0). তারপর দুটি undo: প্রথমটি '!' সরায়, দ্বিতীয়টি 'tea' — text খালি, redo stack-এ এখন [Ins !, Ins tea], ওপরে Ins tea. শেষ redo তাই 'tea' ফেরায়, '!' নয়। মূল ফাঁদ দুটি: নতুন command-এ redo stack খালি হওয়া, আর redo-র ক্রম LIFO — শেষে undo করা জিনিস আগে ফেরে। (এই আউটপুট আসল কোড চালিয়ে মেলানো।)

  2. Pattern চিনুন: প্রতিটি দৃশ্যে এই পর্বের কোন pattern (strategy, observer, command, template method) সবচেয়ে মানানসই, আর কেন? (ক) একটি কাল্পনিক বাজার অ্যাপে “লেনদেনের ইতিহাস” পাতা একটি ক্রমে চলে — data আনা, সাজানো, দেখানো; শুধু data-র উৎস (bKash-ধাঁচের wallet, card, cash) অনুযায়ী “আনা” ধাপ আলাদা, আর ধাপের ক্রম কখনো বদলায় না। (খ) মজুদ শেষ হলে গুদামের app, বিক্রেতার dashboard আর গ্রাহকের “আবার পাওয়া গেলে জানাবেন” — তিনজনকেই জানাতে হবে, আর আগামী মাসে আরেকজন যোগ হবে। (গ) Admin “সব পণ্যের দাম ৫% বাড়াও” চালালেন, পরে ভুল বুঝে ফেরত নিতে চান; প্রতিটি বদল কে করেছেন তার রেকর্ডও চাই। (ঘ) Checkout-এ গ্রাহক কোন ছাড়ের নিয়ম পাবেন — উৎসবের, সদস্যের, প্রথম অর্ডারের — তা চলার সময় ঠিক হয়। (ঙ) রাতে যত দামের হালনাগাদ জমা হয়, সেগুলো একটি সারিতে রেখে ভোর ৪টায় একে একে চালানো হয়। (চ) Test framework প্রতিটি test-এর আগে setUp, পরে tearDown ডাকে; আপনি শুধু ওই দুটি আর test method লেখেন।
    • (ক) Template method. ক্রম স্থির আর একটি ধাপই বদলায়; base class-এ show_history() ক্রম রাখবে, subclass শুধু fetch() লিখবে। (উৎস চলার সময় বদলালে বা আরও অক্ষ যোগ হলে strategy-ও ভালো — “fetch”-কে একটি object হিসেবে দেওয়া।)
    • (খ) Observer. এক ঘটনা, অনেক শ্রোতা, শ্রোতার সংখ্যা বাড়বে; মজুদের class শ্রোতাদের চিনবে না। আলাদা service-এ গেলে pub/sub.
    • (গ) Command. “৫% বাড়াও” একটি command object, যা পুরোনো দামগুলো মনে রাখে (undo-র জন্য), আর command log-এ admin-এর নাম আর সময় — audit. এখানে undo নিরাপদ, কারণ দাম বদল অভ্যন্তরীণ; গ্রাহককে ইতিমধ্যে SMS গিয়ে থাকলে সেটা ক্ষতিপূরণের প্রশ্ন।
    • (ঘ) Strategy. একই কাজের (ছাড় হিসাব) বদলযোগ্য পদ্ধতি, বাছাই চলার সময়, অংশ ২-এর pickRule-এর মতো এক জায়গায়।
    • (ঙ) Command (queue রূপে)। অনুরোধ বানানো আর চালানোর সময় আলাদা — command object ছাড়া এটা সম্ভব নয়।
    • (চ) Template method, আর তার পেছনে Hollywood principle: framework আপনার method ডাকে, আপনি framework-কে নয়।
  3. নকশার কাজ — একটি বিশ্ববিদ্যালয়ের ফলাফল পোর্টাল (কাল্পনিক): পরীক্ষা নিয়ন্ত্রক ফল প্রকাশ করলে শিক্ষার্থীকে SMS, অভিভাবককে email আর বিভাগের notice board-এ খবর যাবে; পরে আরও মাধ্যম যোগ হতে পারে। নিয়ন্ত্রক কোনো নম্বর সংশোধন করতে পারেন, আর প্রকাশের আগে সংশোধন ফেরত নিতে পারেন; প্রতিটি সংশোধনের রেকর্ড চাই। ফলের তালিকা PDF আর CSV দুই রূপে নামানো যায়, আর GPA হিসাবের নিয়ম বিভাগভেদে আলাদা। কোন অংশে কোন pattern ব্যবহার করবেন, মূল class/function কী হবে, আর কোন তিনটি ফাঁদে নজর রাখবেন?
    • প্রকাশের খবর — observer. ResultPublisher (subject) publish(exam) করলে শ্রোতাদের ডাকে: SmsNotifier, EmailNotifier, NoticeBoard. নতুন মাধ্যম মানে নতুন শ্রোতা, publisher বদলায় না। Notify নিরাপদ হবে — email server বন্ধ থাকলেও SMS যাবে, ব্যর্থতা log আর পরে retry. ঐচ্ছিক নয় এমন কাজ (যেমন ফল database-এ “প্রকাশিত” চিহ্নিত করা) শ্রোতা নয়, publish-এর মূল অংশ।
    • নম্বর সংশোধন — command. CorrectMark(student, course, old, new, by); execute() নতুন নম্বর বসায়, undo() পুরোনোটি। একটি CorrectionLog (invoker) undo stack আর স্থায়ী audit রাখে — কে, কখন, কোন নম্বর থেকে কোনটিতে। প্রকাশের পরে undo বন্ধ; তখন নতুন একটি সংশোধন-command (ক্ষতিপূরণ) আর নতুন বিজ্ঞপ্তি।
    • রপ্তানি — template method বা strategy. ধাপ স্থির হলে ResultExporter.export() base class-এ, PdfExporter/CsvExporter subclass. বিভাগ বা মাধ্যম অনুযায়ী অনেক সমন্বয় হলে export(rows, fmt) আর format strategy.
    • GPA-র নিয়ম — strategy. gpa_rule(department) একটি function ফেরত দেয় (যেমন বিভিন্ন grade scale); ফলাফলের class শুধু “নিয়ম” চেনে।

    তিনটি ফাঁদ: (১) প্রকাশের পরে command undo করলে শিক্ষার্থী আগেই SMS পেয়েছেন — undo-র সীমা স্পষ্ট রাখুন। (২) শ্রোতা যোগ হয় কিন্তু সরানো হয় না (যেমন প্রতিটি পরীক্ষার জন্য নতুন dashboard শ্রোতা) — leak আর দুবার বার্তা। (৩) GPA নিয়ম একটিই হলে strategy-র কাঠামো অকালপক্ব; বিভাগভেদের প্রমাণ (requirement, পর্ব ২১) থাকলেই বানান। আর বাস্তব ব্যবস্থায় বার্তা পাঠানো আলাদা service-এ গেলে observer-এর জায়গায় queue বা pub/sub.

  4. হিসাব — lapsed listener-এর খরচ: একটি কাল্পনিক বাজার অ্যাপে product page খোলার সময় দামের feed-এ দুটি শ্রোতা যোগ করে (দাম আর মজুদ), কিন্তু বন্ধ হওয়ার সময় সরায় না। Cart-এর badge-এর একটি স্থায়ী শ্রোতাও আছে। একজন ব্যবহারকারী page-টি ১২ বার খোলেন; প্রথম ১১ বার বন্ধ করেন, ১২তম বার খোলা থাকে। এরপর দাম ৫ বার বদলায়। (ক) Feed-এ এখন কয়টি শ্রোতা? (খ) কয়টি কাজের, কয়টি lapsed? (গ) ৫টি বদলে মোট কতবার শ্রোতা ডাকা হলো, তার কতগুলো অকারণ? (ঘ) প্রতিটি অকারণ redraw ৪ মিলিসেকেন্ড নিলে প্রতিটি দাম বদলে কত সময় নষ্ট?

    (ক) ১২ বার × ২ = ২৪টি page-শ্রোতা, সাথে ১টি badge = ২৫।

    (খ) কাজের: খোলা page-এর ২টি + badge ১টি = ৩। Lapsed: প্রথম ১১টি বন্ধ page × ২ = ২২।

    (গ) প্রতিটি বদলে ২৫টি ডাক, ৫ বদলে ২৫ × ৫ = ১২৫; অকারণ ২২ × ৫ = ১১০ — অর্থাৎ ৮৮% ডাকই নষ্ট।

    (ঘ) ২২ × ৪ = ৮৮ মিলিসেকেন্ড প্রতিটি বদলে। একটি screen-এর এক frame আঁকার সময় সাধারণত প্রায় ১৬ মিলিসেকেন্ড (৬০ frame প্রতি সেকেন্ডে), তাই এটি কয়েকটি frame বাদ পড়ার সমান — ব্যবহারকারী অ্যাপকে “আটকে যাওয়া” টের পাবেন। আর সংখ্যাটি প্রতিবার page খোলার সাথে বাড়বে; সমাধান সংখ্যা কমানো নয়, প্রতিটি বন্ধে unsubscribe. অংশ ৪-এর কোডে যেমন দেখলেন, সতর্ক page-এ শ্রোতা থাকত শুধু ৩টি।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমObserver pattern-এ একটি subject-এর নিরাপদ notify-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Subject নিজের অবস্থা বদলায় (যেমন নতুন দাম বসায়)
  2. শ্রোতাদের তালিকার একটি কপি নেয়
  3. কপির প্রতিটি শ্রোতাকে ক্রমে, try-এর ভেতরে ডাকে
  4. কোনো শ্রোতা error ছুড়লে সেটি জমা রেখে পরের শ্রোতায় যায়
  5. সবাইকে ডাকা শেষে জমা error-গুলো log বা report করে

Gang of Four-এর বর্ণনা অনুযায়ী command pattern-এর মূল উদ্দেশ্য কোনটি?

  • একদল algorithm-কে বদলযোগ্য করা
  • একটি object বদলালে নির্ভরশীল সবাইকে খবর দেওয়া
  • একটি অনুরোধকে object হিসেবে মুড়ে রাখা, যাতে তা সারিতে রাখা, log করা আর undo করা যায়
  • Algorithm-এর কঙ্কাল base class-এ রেখে কিছু ধাপ subclass-এ ছেড়ে দেওয়া

একটি অ্যাপে product page খোলার সময় একটি দীর্ঘজীবী দামের feed-এ callback যোগ করে, কিন্তু বন্ধ হওয়ার সময় সরায় না। বারবার খোলা-বন্ধের পরে সবচেয়ে সম্ভাব্য ফল কী?

  • Garbage collector বন্ধ page-গুলো নিজে থেকেই মুছে দেবে, কোনো সমস্যা নেই
  • শ্রোতার সংখ্যা বাড়তেই থাকে; memory leak আর বন্ধ page-গুলোও প্রতিটি বদলে কাজ করে
  • দ্বিতীয়বার খোলার সময়ই অ্যাপ syntax error দেখাবে
  • নতুন খোলা page আর কোনো খবর পাবে না

“Don't call us, we'll call you” — Hollywood principle — কোন pattern-এর আলোচনায় Gang of Four উল্লেখ করেছে, আর এর অর্থ কী?

  • Command — invoker কখনো receiver-কে ডাকে না
  • Observer — শ্রোতারা কখনো subject-কে ডাকতে পারে না
  • Strategy — client কখনো strategy বাছাই করে না
  • Template method — base class ঠিক সময়ে subclass-এর ধাপ ডাকে, subclass কঙ্কালটি ডাকে না

একটি report-এর format (CSV, HTML) আর উৎস (দাম, মজুদ) — দুটিই বদলাতে পারে, আর কোন সমন্বয় লাগবে তা চলার সময় ঠিক হয়। কোন নকশা সবচেয়ে মানানসই?

  • Format আর উৎস — দুটিকেই আলাদা strategy হিসেবে পাঠানো (composition)
  • প্রতিটি সমন্বয়ের জন্য একটি template method subclass লেখা
  • Report-কে subject আর format-কে observer বানানো
  • সব সমন্বয় একটি function-এর ভেতরে if-else দিয়ে লেখা

একটি editor-এ undo stack আর redo stack আছে; নতুন command এলে redo stack খালি হয়। Text খালি অবস্থা থেকে ক্রমে: run Insert(0,"ab"), run Insert(2,"c"), undo, run Insert(2,"d"), redo. শেষে text কী?

  • ab
  • abdc
  • abd
  • abcd

Observer-এর pull model-এর বর্ণনা কোনটি?

  • Subject ডাকের সাথে বদলের সব তথ্য পাঠায়, যেমন update(item, price)
  • Subject শুধু জানায় “আমি বদলেছি”, আর শ্রোতা নিজে subject থেকে দরকারি তথ্য পড়ে নেয়
  • শ্রোতা প্রতি সেকেন্ডে subject-কে জিজ্ঞেস করে বদলেছে কি না
  • একটি broker সব বার্তা জমা রাখে, শ্রোতা topic ধরে নেয়

Strategy আর state pattern-এর class diagram প্রায় একই রকম। তাদের মূল পার্থক্য কোনটি?

  • Strategy inheritance ব্যবহার করে, state composition
  • Strategy-তে একটি method থাকে, state-এ অনেক
  • State শুধু distributed system-এ ব্যবহৃত হয়
  • Strategy বাইরে থেকে বাছাই হয়; state-এ object-এর অবস্থা বদলালে আচরণ বদলায়, প্রায়ই state নিজেই পরের state বসায়

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Strategy: বদলযোগ্য algorithm, context শুধু চুক্তি জানে; বাছাই এক জায়গায়। JavaScript-Python-এ প্রায়ই একটি function-ই যথেষ্ট।
  2. Observer: এক ঘটনা, অনেক শ্রোতা। তালিকার কপি নিয়ে notify, একজনের error-এ সবাই যেন না থামে, আর প্রতিটি subscribe-এর জোড়া unsubscribe — নইলে lapsed listener আর memory leak.
  3. Command: অনুরোধ = object; undo/redo-র দুই stack, নতুন command-এ redo stack খালি। Queue, macro, log আর replay একই চিন্তা থেকে।
  4. Template method: কঙ্কাল base class-এ, ধাপ আর hook subclass-এ; Hollywood principle — “আমরাই ডাকব”। দাম fragile base class.
  5. Inheritance বনাম composition: template method class লেখার সময় বাঁধে, strategy চলার সময় জোড়ে; প্রমাণ ছাড়া কোনো pattern নয়।

পরের পর্বে: Behavioral pattern-এর বাকি পাঁচটি — state (strategy-র যমজ, কিন্তু নিজের অবস্থা নিজে বদলায়), iterator, chain of responsibility (DOM-এর bubbling মনে আছে?), mediator আর visitor — পর্ব ৩৬-এ।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান