এই পর্বে যা শিখবেন
- পাঁচটি GoF creational pattern-এর উদ্দেশ্য, সমস্যা, UML কাঠামো, কোড, ফল আর ফাঁদ
- Singleton কেন প্রায়ই anti-pattern, আর Python ও JavaScript-এ তার সহজ বিকল্প
- Factory Method, simple factory আর static factory method-এর পার্থক্য — নামের গোলমাল দূর করা
- Builder দিয়ে বৈধ object নিশ্চিত করা, আর Prototype-এ shallow বনাম deep copy-র বাগ ধরা
- তুলনার টেবিল থেকে পরিস্থিতি দেখে ঠিক pattern — অথবা কোনো pattern নয় — বেছে নেওয়া
১ · Object বানানো কেন আলাদা নকশার প্রশ্ন
একটি কাল্পনিক বাস-টিকিট অ্যাপ ধরুন। টিকিট কাটা হলে যাত্রীকে বার্তা যায়। প্রথম দিন কোডে লেখা হলো new SmsChannel(). কয়েক মাস পরে কিছু যাত্রী email চান, পরে app-এর push বার্তাও এলো। এখন যেখানে যেখানে new SmsChannel() লেখা, সব জায়গায় if বসাতে হবে। আর test-এর সময় আসল SMS gateway-তে বার্তা পাঠানো চলবে না — তখন নকল channel বসাতে হবে, কিন্তু কোডের গভীরে new বাঁধা থাকায় বসানোর জায়গা নেই।
সমস্যাটি এখানে: new SmsChannel() লেখা মানে কোডটি একটি নির্দিষ্ট class-এর নাম জানে। পর্ব ২৮-এর dependency inversion বলেছিল, উঁচু স্তরের কোড চুক্তির ওপর নির্ভর করবে, নির্দিষ্ট বাস্তবায়নের ওপর নয়। কিন্তু কাউকে না কাউকে তো আসল object বানাতেই হবে। Creational pattern সেই “কাউকে না কাউকে”-র নকশা — বানানোর দায় কোথায় থাকবে, কতবার বানানো হবে, আর কতটা বদলযোগ্য হবে।
একটি রেস্টুরেন্টের কথা ভাবুন। খদ্দের বলেন “এক প্লেট কাচ্চি”। তিনি জানেন না কোন বাবুর্চি রাঁধবেন, চাল কোন দোকানের, হাঁড়ি কোনটি। রান্নাঘর এই সিদ্ধান্তগুলো নিজের কাছে রাখে। কাল বাবুর্চি বদলালেও খদ্দেরের অর্ডারের ভাষা একই থাকে। Creational pattern কোডে ঠিক এই রান্নাঘরটি বানায়: ব্যবহারকারী কোড “একটি channel দিন” বলে, আর কোন class থেকে, কীভাবে — সেই জ্ঞান এক জায়গায় থাকে।
Design pattern কী, pattern-এর বর্ণনার কাঠামো (নাম, উদ্দেশ্য, সমস্যা, কাঠামো, ফল) আর কখন pattern ব্যবহার করবেন না — এসব পর্ব ৩২-এ এসেছে। এখানে সরাসরি পাঁচটি pattern-এ যাব। ১৯৯৪ সালের Design Patterns: Elements of Reusable Object-Oriented Software বইয়ের চার লেখক — Erich Gamma, Richard Helm, Ralph Johnson আর John Vlissides, যাঁদের ডাকনাম Gang of Four (GoF) — ২৩টি pattern-কে তিন ভাগে ভাগ করেছিলেন। Creational ভাগে পাঁচটি:
| Pattern | যে প্রশ্নের উত্তর দেয় | এক লাইনে |
|---|---|---|
| Singleton | কতগুলো? | ঠিক একটি instance, সবাই সেটিই পায় |
| Factory Method | কোন class? (একটি product) | Subclass override করে ঠিক করে |
| Abstract Factory | কোন পরিবার? (কয়েকটি product) | একটি factory মিলে যাওয়া পুরো সেট বানায় |
| Builder | কীভাবে, কোন ক্রমে? | ধাপে ধাপে জোড়া, শেষে যাচাই করে সম্পূর্ণ object |
| Prototype | কোন নমুনা থেকে? | তৈরি object কপি করে নতুন object |
প্রতিটি pattern-এর আলোচনায় একই ক্রম মানব: উদ্দেশ্য (intent), সমস্যা, কাঠামো (UML), কোড, ফল (consequences — কী পাবেন, কী দাম দেবেন), ফাঁদ, আর বাস্তব library-তে কোথায় দেখা যায়। Library-র উদাহরণগুলো পরিচিত নকশার দিক থেকে বলা; ভেতরের বাস্তবায়ন version-ভেদে বদলাতে পারে, তাই ব্যবহারের আগে সেই library-র নিজের documentation দেখুন।
২ · Singleton — একটিমাত্র instance, আর তার দাম
উদ্দেশ্য (GoF): একটি class-এর ঠিক একটি instance নিশ্চিত করা, আর সেটিতে পৌঁছানোর একটি বৈশ্বিক পথ দেওয়া। খেয়াল করুন, এখানে দুটি প্রতিশ্রুতি — “একটিই” আর “সবখান থেকে পাওয়া যায়”। Singleton-এর প্রায় সব সমস্যা দ্বিতীয়টি থেকে আসে।
সমস্যা: কিছু জিনিস সত্যিই একটি থাকা উচিত — একটি প্রক্রিয়ার configuration, একটি log লেখার ব্যবস্থা, একটি printer spooler. দুটি config object থাকলে একটিতে ভাষা “bn”, আরেকটিতে “en” — অদ্ভুত বাগ। কাঠামো: class-টি নিজের একটি static field-এ নিজের একমাত্র instance রাখে, constructor বাইরে থেকে ব্যবহার করতে দেয় না (Java-তে private), আর একটি static method (getInstance()) প্রথমবার instance বানায়, পরে প্রতিবার সেটিই ফেরত দেয়। UML-এ একটি class বাক্স: - instance: Config (নিচে দাগ, অর্থাৎ static), - Config(), + getInstance(): Config (নিচে দাগ) — পর্ব ২৯-এর static member-এর চিহ্ন।
স্কুলের একটিমাত্র ঘণ্টা। সবাই সেই ঘণ্টার শব্দেই ক্লাস বদলায়, দুটি ঘণ্টা থাকলে বিশৃঙ্খলা। এ পর্যন্ত ঠিক আছে। কিন্তু ভাবুন, যে কেউ যেকোনো ক্লাসরুম থেকে ঘণ্টার দড়ি টানতে পারে, আর কে টেনেছে কেউ জানে না। “একটিই ঘণ্টা” সমস্যা নয়; “সবাই দড়ি টানতে পারে” সমস্যা।
নিচের Python কোডে তিনটি জিনিস: (১) __new__ দিয়ে ক্লাসিক Singleton — Python-এ Config() ডাকলে আগে __new__ চলে object বানাতে, তাই সেখানে একই object ফেরত দেওয়া যায়; (২) module-এর ভেতরে একটি instance — Python-এর স্বাভাবিক বিকল্প; (৩) একটি সতর্কবার্তা — দুটি test, যারা ক্রম বদলালেই ফল বদলায়।
import sys, threading, types
class Config: # way 1: __new__ hands back one object
_instance = None
_lock = threading.Lock()
def __new__(cls):
with cls._lock: # makes check-then-create atomic
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.settings = {"lang": "bn"}
return cls._instance
a, b = Config(), Config()
a.settings["theme"] = "dark"
print("1) same object:", a is b, "| b sees:", b.settings)
# way 2: a module-level instance; this string plays the file appconfig.py
src = "class _Settings:\n def __init__(self): self.values = {}\nsettings = _Settings()\n"
mod = types.ModuleType("appconfig"); exec(src, mod.__dict__)
sys.modules["appconfig"] = mod # what the first real import does
import appconfig as m1
import appconfig as m2
print("2) cached module, same instance:", m1.settings is m2.settings)
# way 3 is a warning: hidden global state leaks between tests
def test_switch_to_english():
Config().settings["lang"] = "en"; assert Config().settings["lang"] == "en"
def test_default_is_bangla():
lang = Config().settings["lang"]; assert lang == "bn", f"got {lang!r}"
def run(*tests):
for t in tests:
try: t(); print(" PASS", t.__name__)
except AssertionError as e: print(" FAIL", t.__name__, "-", e)
print("3) order A:"); run(test_default_is_bangla, test_switch_to_english)
Config._instance = None # reset the hidden state
print(" order B:"); run(test_switch_to_english, test_default_is_bangla)আউটপুট:
1) same object: True | b sees: {'lang': 'bn', 'theme': 'dark'}
2) cached module, same instance: True
3) order A:
PASS test_default_is_bangla
PASS test_switch_to_english
order B:
PASS test_switch_to_english
FAIL test_default_is_bangla - got 'en'
১) a আর b একই object, তাই a-তে বসানো theme b-তেও দেখা যায়। ২) প্রথম import-এর পর Python module-টিকে sys.modules-এ জমা রাখে (module cache); পরের সব import একই module object পায়, তাই module-এর ভেতরের settings নিজে থেকেই একটিমাত্র। আসল project-এ এখানে শুধু একটি appconfig.py ফাইল থাকত — exec আর sys.modules-এর লাইন দুটি শুধু এক ফাইলের ভেতরে সেটি দেখানোর জন্য। ৩) দুটি test, কোনোটিতে ভুল নেই। ক্রম A-তে দুটিই পাস। ক্রম B-তে প্রথম test ভাষা “en” করে রেখে গেল, আর দ্বিতীয়টি সেই অবস্থাই পেল — ব্যর্থ। Test runner ক্রম বদলালে বা test সমান্তরালে চালালে এমন test মাঝে মাঝে ব্যর্থ হয়; এর কারণ খুঁজতে দলের ঘণ্টার পর ঘণ্টা যায়।
Thread-safety আর lazy initialization — সংক্ষেপে
উপরের Config প্রথম দরকারের মুহূর্তে বানানো হয় — একে বলে lazy initialization. দুটি thread একই সময়ে প্রথমবার Config() ডাকলে দুজনই “এখনো কেউ নেই” দেখে দুটি instance বানাতে পারে — একটি race condition. তাই _lock: যাচাই আর বানানো একসাথে, একজনের পর একজন। প্রতিবার lock নেওয়ার খরচ এড়াতে কেউ কেউ lock-এর বাইরে একবার আর ভেতরে আরেকবার যাচাই করেন — double-checked locking. Java-তে এই কৌশল Java 5-এর আগে নির্ভরযোগ্য ছিল না; এখন field-টি volatile হলে কাজ করে (ভাবনার প্রশ্ন ৩-এ বিস্তারিত)। Thread-safety-র সবচেয়ে সহজ পথ প্রায়ই lazy না হওয়া: প্রোগ্রাম শুরুর সময় একবার বানিয়ে ফেলা। Python-এ module import নিজেই একটি import lock-এর ভেতরে হয়, তাই module-স্তরের instance সাধারণত একবারই তৈরি হয়।
কেন Singleton প্রায়ই anti-pattern
- লুকানো dependency:
OrderService-এর constructor দেখে বোঝা যায় না যে ভেতরেConfig.getInstance()ডাকা হয়। Function-এর signature মিথ্যা বলে; নির্ভরতা লুকিয়ে থাকে। এটি আসলে একটি সুন্দর নাম দেওয়া global variable — পর্ব ২৫-এর common coupling. - Test-এর কষ্ট: উপরে দেখলেন — global state এক test থেকে আরেকটিতে চুইয়ে পড়ে, test isolation ভাঙে। আর নকল config বা test double বসাতে হলে singleton-এর ভেতরে হাত দিতে হয়।
- “একটিই” অনুমান ভাঙে: আজ একটি database; কাল একটি read replica এলো, বা একই প্রক্রিয়ায় দুটি tenant-এর আলাদা config লাগল। Class নিজেই “আমি একটি” ঘোষণা করে রাখলে বদল কঠিন।
- দুই দায় এক class-এ: নিজের কাজ আর “কতগুলো আমি থাকব” — পর্ব ২৭-এর SRP-র চোখে দুটি আলাদা পরিবর্তনের কারণ।
“একটি instance” প্রায়ই ঠিক চাওয়া; “সবখান থেকে getInstance()” প্রায়ই ভুল উপায়। ভালো বিকল্প: প্রোগ্রামের শুরুর কাছে একটি জায়গায় (composition root) একটিমাত্র object বানান, তারপর যার দরকার তাকে constructor-এ দিন — dependency injection. এতে একটিই থাকে, কিন্তু নির্ভরতা চোখে দেখা যায়, আর test-এ নকল দেওয়া যায়। বিস্তারিত পর্ব ৩৭-এ।
Python আর JavaScript-এর idiom: Python-এ config.py ফাইলে settings = Settings() লিখলেই যথেষ্ট; ES module-এও তাই — export const settings = new Settings(); লেখা module প্রথম import-এ একবার চলে, আর module map সবাইকে একই object দেয়। এগুলো GoF-এর কাঠামো ছাড়াই “একটিমাত্র” দেয়, তবে global access-এর সমস্যা পুরো যায় না — যেখানে সম্ভব, তবু parameter দিয়ে দিন। বিস্তারিত দেখুন Python-এর module আর JavaScript-এর ES module পর্বে।
বাস্তবে: Python-এর None, True, False ভাষার স্তরে singleton — x is None লেখা যায় এজন্যই। logging.getLogger("app") একই নামে সবসময় একই logger ফেরত দেয় — নাম ধরে একটি করে instance, যাকে কেউ কেউ “multiton” বা registry বলেন। Java-র Runtime.getRuntime() প্রায়ই ক্লাসিক GoF singleton-এর উদাহরণ হিসেবে বলা হয়।
৩ · Factory Method — কোন product, ঠিক করে subclass
উদ্দেশ্য (GoF): object বানানোর জন্য একটি interface ঘোষণা করা, কিন্তু কোন class-এর object হবে তা subclass-কে ঠিক করতে দেওয়া। GoF এর আরেক নাম দিয়েছিল “virtual constructor”. সমস্যা: একটি base class-এর একটি কাজ আছে, যার ভেতরে একটি product object লাগে — যেমন Notifier.notify()-এর ভেতরে একটি channel. কাজের ধাপগুলো সব subclass-এ এক, শুধু product আলাদা। Base class-এ new SmsChannel() লিখলে email-এর subclass কিছু করতে পারে না; প্রতিটি subclass-এ পুরো notify() কপি করলে duplication.
সমাধান: base class-এ একটি আলাদা method — createChannel() — যাকে বলে factory method. notify() সেটিকে ডাকে, কিন্তু নিজে কোনো concrete class-এর নাম জানে না। প্রতিটি subclass শুধু createChannel() override করে নিজের product ফেরত দেয়। এটি polymorphism দিয়ে বানানোর সিদ্ধান্ত পিছিয়ে দেওয়া; আর পর্ব ৩৫-এর template method pattern-এর খুব কাছের আত্মীয় — notify() একটি ছাঁচ, যার একটি ধাপ subclass পূরণ করে।
Factory Method-এর কাঠামো (UML class diagram)
Creator → ConcreteCreator; Product → ConcreteProduct. Italic = abstract
Notifier শুধু Channel চুক্তি চেনে, SmsChannel চেনে শুধু SmsNotifier.ছবিতে চারটি ভূমিকা, GoF-এর নামে: Creator (Notifier — abstract class), ConcreteCreator (SmsNotifier — inheritance দিয়ে), Product (Channel — interface) আর ConcreteProduct (SmsChannel — realization). তীরগুলো পর্ব ২৯-এর নিয়মে আঁকা: generalization-এ ত্রিভুজ সাধারণটির দিকে, আর dependency-তে খোলা তীর যাকে ব্যবহার করা হয় তার দিকে। Factory Method আর Abstract Factory দুটির কোডই চালানো যায় এমনভাবে পরের অংশে একসাথে আছে।
নামের গোলমাল: simple factory আর static factory method
সবচেয়ে প্রচলিত “factory” আসলে Factory Method নয়। একটি function, যা parameter দেখে ঠিক object বানায় — makeChannel("sms") — তাকে বলে simple factory. এটি খুব কাজের, কিন্তু GoF-এর ২৩টি pattern-এর একটি নয়; এতে কোনো subclass বা override নেই, আছে একটি সিদ্ধান্তের জায়গা (প্রায়ই একটি switch বা একটি dictionary-র registry). Python-এ datetime.fromtimestamp(...) বা Java-র List.of(...)-এর মতো নাম দেওয়া বিকল্প constructor-কে বলে static factory method — Joshua Bloch-এর Effective Java-র প্রথম পরামর্শই এটি, আর Bloch নিজেই লিখেছেন যে এটি GoF-এর Factory Method-এর সমান নয়। পরীক্ষা বা ইন্টারভিউতে “factory pattern” বললে প্রশ্নকর্তা কোনটি বোঝাচ্ছেন, জিজ্ঞেস করে নেওয়া বুদ্ধিমানের কাজ।
ফল: (+) ব্যবহারকারী কোড concrete class থেকে মুক্ত, নতুন product মানে নতুন subclass — পর্ব ২৭-এর open-closed. (+) Subclass-কে একটি “hook” দেয়, যেখানে বানানোর খুঁটিনাটি বদলানো যায়। (−) শুধু একটি object বানানোর জন্য নতুন একটি subclass লাগে; প্রতিটি product-এর জন্য creator-এর একটি জোড়া — class-এর সংখ্যা বাড়ে। ফাঁদ: constructor-এর ভেতর থেকে factory method ডাকবেন না — Java বা C#-এ তখন subclass-এর field তৈরি হওয়ার আগেই override-করা method চলে। আর যেখানে শুধু parameter দেখে বাছাই দরকার, সেখানে subclass-এর পুরো শ্রেণিবিন্যাস বানানো over-engineering; একটি simple factory বা function-এর parameter হিসেবে class পাঠানোই যথেষ্ট।
বাস্তবে: Java-র Collection.iterator() প্রায়ই ক্লাসিক উদাহরণ — প্রতিটি collection class নিজের রকমের iterator বানায়, আর ব্যবহারকারী শুধু Iterator চুক্তি চেনে। Python-এর __iter__-ও একই ভাবনার: for লুপ জানে না কোন iterator আসবে, object নিজে ঠিক করে।
৪ · Abstract Factory — মিলে যাওয়া পুরো পরিবার
উদ্দেশ্য (GoF): সম্পর্কিত বা পরস্পরনির্ভর কয়েকটি object-এর পরিবার বানানোর একটি interface দেওয়া, তাদের concrete class উল্লেখ না করে। সমস্যা: বাস-টিকিট অ্যাপের বার্তায় এখন তিনটি অংশ — বার্তার লেখা (message), ঠিকানা (address) আর পাঠানোর যন্ত্র (sender). SMS-এর লেখা ছোট আর সাদা, ঠিকানা ফোন নম্বর, পাঠায় SMS gateway; email-এর লেখা HTML, ঠিকানা email, পাঠায় mail server. এগুলো একটি product family. কেউ ভুল করে SMS sender-এ email ঠিকানা দিলে — gateway “rahim@example.com” নম্বরে SMS পাঠানোর চেষ্টা করবে। মেশানো অবস্থাটিই বাগ।
সমাধান: একটি interface NotificationKit, যার তিনটি method: createMessage, createAddress, createSender. প্রতিটি পরিবারের জন্য একটি concrete kit — SmsKit, EmailKit — যা নিজের পরিবারের তিনটিই বানায়। ব্যবহারকারী কোড একটি kit হাতে পায় আর তা থেকেই সব নেয়, তাই পরিবার মেশানো অসম্ভব হয়ে যায়। কোন kit, সেটি প্রোগ্রামের শুরুতে একবার ঠিক হয়। খেয়াল করুন, Abstract Factory-র প্রতিটি createX method নিজেই প্রায়ই একটি factory method — GoF-ও তা বলেছে।
Abstract Factory-র কাঠামো (UML class diagram)
AbstractFactory → ConcreteFactory; AbstractProduct → ConcreteProduct; একই রঙ = একই পরিবার
NotificationKit, Message, Sender চুক্তি চেনে (ছবি পরিষ্কার রাখতে Client-এর শেষ দুটি dependency আঁকা হয়নি)। প্রতিটি kit শুধু নিজের রঙের product বানায় — তাই পরিবার মেশে না।এবার stepper-এ দেখুন, একটি kit কীভাবে একটি সামঞ্জস্যপূর্ণ পরিবার বানায়, আর configuration-এর একটি শব্দ বদলালে পুরো পরিবার একসাথে কীভাবে বদলে যায়।
- Config
channelশুরুতে একবার - Kit
NotificationKitএকটি পরিবার - Message
createMessageলেখা - Address
createAddressঠিকানা - Sender
createSenderপাঠানো
| config channel | |
| kit | |
| message | |
| address | |
| sender | |
| ফল |
- Kit বাছাই — একবারই। প্রোগ্রামের শুরুতে configuration-এ
channel = email, তাই composition rootEmailKitবেছে নিয়েsendTicket-কে দিল।sendTicketজানে শুধু kit-এর তিনটি method-এর নাম। kit.createMessage(text)—EmailKitএকটিEmailMessageবানাল, যার লেখা HTML অনুচ্ছেদে মোড়া।sendTicket“Email” শব্দটি কোথাও লেখেনি।kit.createAddress(user)— একই kit, তাই ঠিকানাও email পরিবারের: user-এর email ঠিকানাrahim@example.com. ফোন নম্বর এই পরিবারে আসার কোনো পথ নেই।kit.createSender()—EmailSender. এখন তিনটি product একই পরিবারের, আর এই সামঞ্জস্যের দায় নিয়েছে kit, ব্যবহারকারী কোড নয়।s.send(a, m)— mail server ঠিক ঠিকানায় ঠিক ধরনের লেখা পাঠাল। হাতে হাতে বানালে কেউnew SmsSender()-এর সাথে email ঠিকানা মেশাতে পারতেন; এখানে সেই সুযোগই নেই।- একটি শব্দ বদল। Configuration-এ
channel = sms— composition root এবারSmsKitদিল।sendTicket-এর একটি লাইনও বদলায়নি, কিন্তু তিনটি product একসাথে SMS পরিবারে চলে গেল: ১৮ অক্ষরের লেখা, ফোন নম্বর, gateway.
নিচের JavaScript কোডে আগে Factory Method (অংশ ৩-এর ছবির মতো), তারপর একই অ্যাপের Abstract Factory. প্রতিটি তৈরি product-এর class-এর নাম ছাপা হয়েছে, যাতে দেখা যায় কোন object আসলে তৈরি হলো। (সব নাম, নম্বর আর ঠিকানা কাল্পনিক।)
// Fictional bus-ticket app: a notification kit (all names invented)
// --- Factory Method: a subclass decides which channel object to create
class Notifier {
createChannel() { throw new Error("subclass must override"); }
notify(to, text) { // uses the product, never names its class
const ch = this.createChannel();
return `${ch.constructor.name} -> ${ch.deliver(to, text)}`;
}
}
class SmsChannel { deliver(to, t) { return `sms ${to}: ${t.slice(0, 18)}...`; } }
class EmailChannel { deliver(to, t) { return `mail ${to}: ${t}`; } }
class SmsNotifier extends Notifier { createChannel() { return new SmsChannel(); } }
class EmailNotifier extends Notifier { createChannel() { return new EmailChannel(); } }
for (const n of [new SmsNotifier(), new EmailNotifier()])
console.log(n.notify("rahim", "Seat A3 confirmed, bus leaves 07:30"));
// --- Abstract Factory: one kit creates a whole matching family
class SmsMessage { constructor(t) { this.body = t.slice(0, 18); } }
class SmsAddress { constructor(u) { this.value = u.phone; } }
class SmsSender { send(a, m) { return `[gateway] ${a.value} <- ${m.body}`; } }
class EmailMessage { constructor(t) { this.body = `<p>${t}</p>`; } }
class EmailAddress { constructor(u) { this.value = u.email; } }
class EmailSender { send(a, m) { return `[smtp] ${a.value} <- ${m.body}`; } }
const SmsKit = { createMessage: t => new SmsMessage(t),
createAddress: u => new SmsAddress(u), createSender: () => new SmsSender() };
const EmailKit = { createMessage: t => new EmailMessage(t),
createAddress: u => new EmailAddress(u), createSender: () => new EmailSender() };
function sendTicket(kit, user, text) { // client knows only the kit's methods
const m = kit.createMessage(text), a = kit.createAddress(user), s = kit.createSender();
console.log([m, a, s].map(p => p.constructor.name).join(" + "));
console.log(" " + s.send(a, m));
}
const user = { phone: "01700-000000", email: "rahim@example.com" };
for (const kit of [SmsKit, EmailKit]) sendTicket(kit, user, "Seat A3 confirmed, bus leaves 07:30");আউটপুট:
SmsChannel -> sms rahim: Seat A3 confirmed,...
EmailChannel -> mail rahim: Seat A3 confirmed, bus leaves 07:30
SmsMessage + SmsAddress + SmsSender
[gateway] 01700-000000 <- Seat A3 confirmed,
EmailMessage + EmailAddress + EmailSender
[smtp] rahim@example.com <- <p>Seat A3 confirmed, bus leaves 07:30</p>
প্রথম দুই লাইন Factory Method: notify() একটিই, কিন্তু SmsNotifier থেকে এলো SmsChannel আর EmailNotifier থেকে EmailChannel — সিদ্ধান্ত নিল subclass-এর createChannel(). পরের চার লাইন Abstract Factory: প্রতিটি kit থেকে তিনটি product, আর তিনটিই একই পরিবারের — কোথাও “SmsSender + EmailAddress” আসেনি। JavaScript-এ kit দুটি সাধারণ object, কোনো class বা extends নেই; ভাষাটি duck typing-এর, তাই “interface” এখানে শুধু একটি চুক্তি — তিনটি একই নামের method থাকলেই চলে। TypeScript-এ interface NotificationKit লিখে compiler দিয়ে চুক্তিটি যাচাই করানো যেত।
ফল, ফাঁদ আর বাস্তব উদাহরণ
(+) পরিবারের সামঞ্জস্য নিশ্চিত, concrete class ব্যবহারকারী কোড থেকে লুকানো, আর পুরো পরিবার বদলানো এক জায়গার কাজ। (+) নতুন পরিবার যোগ করা সহজ: PushKit আর তার তিনটি product — নতুন ফাইল, পুরোনো কোড অপরিবর্তিত। (−) নতুন ধরনের product যোগ করা কঠিন: ধরুন প্রতিটি বার্তার সাথে একটি “template” object লাগবে। তখন NotificationKit interface-এ নতুন method, আর প্রতিটি concrete kit বদলাতে হবে — তিন পরিবার থাকলে তিনটিই। এটি একটি পুরোনো টানাপোড়েনের রূপ, যাকে বলে expression problem: একটি নকশা হয় নতুন ধরন সহজ করে, নয়তো নতুন operation; দুটি একসাথে সহজ হয় না (ভাবনার প্রশ্ন ৫)।
ফাঁদ: পরিবার আসলে না থাকলে — অর্থাৎ product-গুলো একে অপরের সাথে বাঁধা না হলে — Abstract Factory অকারণ স্তর। দুটি UI theme-এর মধ্যে শুধু রঙ আলাদা হলে একটি theme object-এ কয়েকটি মান রাখাই যথেষ্ট; button-এর আচরণ আর menu-র আচরণ যখন theme-ভেদে একসাথে বদলায়, তখনই kit-এর দাম ওঠে। বাস্তবে: UI toolkit-এর “look and feel” (একই প্রোগ্রাম Windows-ধাঁচ বা অন্য ধাঁচের button, scrollbar, menu একসাথে পায়) আর কোনো payment provider-এর SDK-র kit (একই provider-এর payment, refund, webhook যাচাইয়ের object) এর প্রচলিত উদাহরণ। Java-র DocumentBuilderFactory-কেও অনেক বইয়ে Abstract Factory-র উদাহরণ বলা হয়, যদিও এর নকশা খুঁটিনাটিতে pure GoF রূপ নয়।
৫ · Builder — ধাপে ধাপে বানানো, শেষে যাচাই
উদ্দেশ্য (GoF): একটি জটিল object-এর বানানোর প্রক্রিয়াকে তার রূপ থেকে আলাদা করা, যাতে একই প্রক্রিয়া থেকে ভিন্ন রূপ বানানো যায়। আজকের ব্যবহারে Builder-এর সবচেয়ে প্রচলিত রূপটি একটু ভিন্ন, যাকে Bloch-এর Effective Java জনপ্রিয় করেছে: অনেক parameter-এর object ধাপে ধাপে জোড়া, আর শেষে একটি build() সব নিয়ম যাচাই করে সম্পূর্ণ, প্রায়ই immutable object দেয়।
সমস্যা — telescoping constructor: একটি টিকিটে route, তারিখ, আসন, যাত্রী, ভাড়া আবশ্যক; খাবার, বিমা ঐচ্ছিক। Java-র মতো ভাষায় এর জন্য একটির পর একটি constructor: ৫টি parameter-এর, ৬টির, ৭টির — দূরবীনের নল যেমন একটির ভেতর থেকে আরেকটি বেরোয়, তাই নাম telescoping constructor. ডাকার জায়গায় দেখা যায় new Ticket("DHK-CTG", "2026-12-14", "A3", "Rahim", 950, false, true) — শেষের দুটি boolean-এর কোনটি খাবার আর কোনটি বিমা, পড়ে বোঝার উপায় নেই। উল্টে গেলেও compiler কিছু বলে না, কারণ দুটির type একই।
দর্জির দোকানে পাঞ্জাবির মাপ নেওয়া হয় ধাপে ধাপে — বুক, লম্বা, হাতা, কলারের ধরন, পকেট আছে কি না। খাতায় প্রতিটি ঘরের পাশে নাম লেখা থাকে, তাই কেউ হাতার মাপ বুকের ঘরে লেখেন না। কাপড় কাটার আগে ওস্তাদ একবার পুরো খাতা দেখে নেন: “কলারের মাপ নেই — আগে নিন।” Builder ঠিক এই খাতা: নাম ধরে ধাপে ধাপে লেখা, আর কাটার আগে build()-এ একবার পুরো যাচাই।
কাঠামো: একটি TicketBuilder class, যার প্রতিটি method একটি অংশ বসিয়ে নিজেকেই ফেরত দেয় (return self), তাই কলগুলো শিকলের মতো লেখা যায় — fluent interface. শেষে build(). GoF-এর মূল রূপে আরও একটি ঐচ্ছিক অংশ আছে — Director, যে একটি ঠিক করা ক্রমে builder-এর ধাপগুলো ডাকে (যেমন “ঈদের বিশেষ বাস” টিকিটের একটি তৈরি রেসিপি)। আর GoF-এ একই director ভিন্ন builder পেলে ভিন্ন রূপ বানায় — একই নথি থেকে একটি builder HTML, আরেকটি সাদা লেখা। আধুনিক fluent builder-এ director প্রায়ই থাকে না; client নিজেই director.
1b = TicketBuilder()2b.route("DHK-CTG").on("2026-12-14")3b.passenger("Rahim")4b.build() # too early?5b.seat("A3").fare(950)6b.with_meal()7t = b.build()
| route | |
| date | |
| passenger | |
| seat | |
| fare | |
| meal |
- খালি খাতা।
TicketBuilder()একটি খালি dictionary নিয়ে শুরু করে। এখনো কোনোTicketনেই — আর অসম্পূর্ণ টিকিট কখনো তৈরিই হবে না, এটিই Builder-এর মূল প্রতিশ্রুতি। - দুটি ধাপ এক লাইনে।
route()নিজেকে ফেরত দেয়, তাই তার ওপরেই.on()ডাকা গেল — fluent interface. প্রতিটি মানের পাশে method-এর নাম, তাই কোনটি কী, পড়লেই বোঝা যায়। - যাত্রী বসল। ধাপগুলো যেকোনো ক্রমে আসতে পারে; route-এর আগে যাত্রী দিলেও চলত।
- আগেভাগে
build(). আবশ্যক পাঁচটির মধ্যে seat আর fare নেই, তাইbuild()একটি স্পষ্ট বার্তাসহValueErrorদিল: “missing seat, fare”. অর্ধেক-তৈরি object প্রোগ্রামে ঢুকতে পারল না। - বাকি দুটি অংশ। আসন A3, ভাড়া ৯৫০ টাকা। এখন আবশ্যক সবকিছু আছে।
- ঐচ্ছিক অংশ, নাম ধরে।
with_meal()— positional তালিকার “কোন boolean কোনটি” প্রশ্নই আর ওঠে না। বিমা দেওয়া হয়নি, তাইTicket-এর default (False) থাকবে। - বৈধ, অপরিবর্তনীয় টিকিট।
build()সব নিয়ম (আবশ্যক অংশ, ভাড়া ধনাত্মক) এক জায়গায় যাচাই করে একটিfrozenTicketদিল। এরপর টিকিট বদলানো যাবে না — পর্ব ৩১-এর immutability.
এবার পুরো কোডটি চালিয়ে দেখুন। প্রথমে positional ডাকের সেই নিঃশব্দ ভুল, তারপর builder, তারপর ভাড়া শূন্য দিলে build()-এর আপত্তি।
from dataclasses import dataclass
@dataclass(frozen=True)
class Ticket:
route: str; date: str; seat: str; passenger: str
fare: int; meal: bool = False; insurance: bool = False
# Telescoping style: a long positional list. Which True is which?
t1 = Ticket("DHK-CTG", "2026-12-14", "A3", "Rahim", 950, False, True)
print("positional:", t1.meal, t1.insurance, "(meant meal=True!)")
class TicketBuilder:
REQUIRED = ("route", "date", "seat", "passenger", "fare")
def __init__(self): self._d = {}
def route(self, r): self._d["route"] = r; return self
def on(self, d): self._d["date"] = d; return self
def seat(self, s): self._d["seat"] = s; return self
def passenger(self, p): self._d["passenger"] = p; return self
def fare(self, taka): self._d["fare"] = taka; return self
def with_meal(self): self._d["meal"] = True; return self
def build(self): # every rule checked in one place
missing = [k for k in self.REQUIRED if k not in self._d]
if missing: raise ValueError("missing " + ", ".join(missing))
if self._d["fare"] <= 0: raise ValueError("fare must be positive")
return Ticket(**self._d)
b = TicketBuilder().route("DHK-CTG").on("2026-12-14").passenger("Rahim")
try: b.build()
except ValueError as e: print("build() refused:", e)
t2 = b.seat("A3").fare(950).with_meal().build()
print("builder:", t2)
try: TicketBuilder().route("DHK-CTG").on("2026-12-14").seat("B1") \
.passenger("Karim").fare(0).build()
except ValueError as e: print("build() refused:", e)আউটপুট:
positional: False True (meant meal=True!)
build() refused: missing seat, fare
builder: Ticket(route='DHK-CTG', date='2026-12-14', seat='A3', passenger='Rahim', fare=950, meal=True, insurance=False)
build() refused: fare must be positive
প্রথম লাইনে লেখক খাবার চেয়েছিলেন, কিন্তু দুটি boolean উল্টো ক্রমে দেওয়ায় যাত্রী পেলেন বিমা, খাবার নয় — কোনো error ছাড়া। দ্বিতীয় লাইনে অসম্পূর্ণ builder প্রত্যাখ্যাত, ঠিক stepper-এর ধাপ ৪-এর মতো। তৃতীয় লাইনে সম্পূর্ণ, frozen টিকিট। চতুর্থ লাইনে ভাড়া ০ — নিয়মটি শুধু build()-এ লেখা, তাই যত জায়গা থেকেই টিকিট বানানো হোক, যাচাই একটিই। dataclass এখানে নিজেই __init__ আর __repr__ বানিয়েছে, তাই সুন্দর ছাপা হলো।
Python-এ Builder কম লাগে। Python-এ নাম ধরে মান দেওয়া যায় — keyword argument: Ticket(route="DHK-CTG", ..., meal=True). আর @dataclass(frozen=True, kw_only=True) লিখলে (Python 3.10 থেকে) positional ডাক নিষিদ্ধ হয়; যাচাই বসানো যায় __post_init__-এ। ফলে telescoping সমস্যার বেশির ভাগই ভাষা নিজে সমাধান করে। JavaScript-এ একটি options object (new Ticket({ route, seat, meal: true })) একই কাজ করে। Builder তখনই দাম পায়, যখন বানানো সত্যিই ধাপে ধাপে হয় — যেমন একটি SQL query বা HTTP request ধীরে ধীরে জোড়া, মাঝপথে অন্য function-কে দেওয়া, বা একই ধাপ থেকে ভিন্ন রূপ। বিস্তারিত Python-এর dataclass পর্বে।
ফল: (+) পড়তে সহজ ডাক, ঐচ্ছিক অংশের যেকোনো মিশ্রণ, এক জায়গায় যাচাই, আর immutable ফল। (−) প্রতিটি field দুবার লেখা — একবার object-এ, একবার builder-এ; field যোগ করলে দুটি জায়গা বদলাতে হয়। ফাঁদ: build()-এ যাচাই না রাখলে builder শুধু লম্বা লেখা constructor; একই builder থেকে দুবার build() করলে দুটি object যেন ভেতরের mutable list ভাগ না করে, সেটি দেখুন। বাস্তবে: Java 11-এর HttpRequest.newBuilder() একটি পরিষ্কার fluent builder; Java-র Lombok library-র @Builder annotation নিজেই builder class লিখে দেয়; অনেক ভাষার SQL query builder library-ও এই ধাঁচের। StringBuilder-কেও প্রায়ই builder বলা হয়, যদিও সেটি মূলত একটি পরিবর্তনযোগ্য string-এর buffer — নামের মিল, pattern-এর পুরো রূপ নয়।
৬ · Prototype — কপি করে বানানো, আর shallow copy-র ফাঁদ
উদ্দেশ্য (GoF): একটি নমুনা instance দিয়ে কোন ধরনের object বানাতে হবে তা বোঝানো, আর সেই নমুনা কপি করে নতুন object বানানো। সমস্যা: কিছু object বানাতে খরচ বেশি বা ধাপ অনেক — যেমন একটি বাস রুটের template: route, ভাড়া, থামার জায়গার তালিকা, আসনের নকশা, ডেটাবেস থেকে পড়া নিয়ম। প্রতিদিনের প্রতিটি যাত্রার জন্য সব নতুন করে বানানোর চেয়ে একটি তৈরি template কপি করে শুধু তারিখ আর সময় বদলানো সহজ। আরেক সুবিধা: কপি করার কোড জানতেই হয় না object-টি কোন class-এর — prototype.clone() ডাকলেই হলো।
কাঠামো: একটি interface-এ clone() method; প্রতিটি concrete class নিজের কপি বানাতে জানে। প্রায়ই একটি prototype registry থাকে: নাম থেকে নমুনা — "DHK-CTG" চাইলে সেই template-এর কপি। Prototype-এর আসল কঠিন প্রশ্ন একটিই: কপি কতটা গভীর হবে?
পরীক্ষার আগে বন্ধুর নোটখাতা ফটোকপি করলেন। ফটোকপি আপনার নিজের — তাতে দাগ দিলে বন্ধুর খাতায় দাগ পড়ে না। কিন্তু খাতার এক পাতায় লেখা ছিল “বাকি অংশ শেয়ার করা Google Doc-এ দেখুন”। ফটোকপিতে লিংকটি এলো, Doc নয়। আপনি সেই Doc-এ লিখলে বন্ধুও দেখবেন, কারণ Doc একটিই। Shallow copy ঠিক এমন: বাইরের খাতা নতুন, ভেতরের reference পুরোনো। Deep copy মানে Doc-টিও আলাদা কপি করা।
- template
{ route, fare, stops }নমুনা - shallow
{ ...template }বাইরেটা নতুন - array #1
stopsভাগ করা - array #2
deep.stopsনিজস্ব
| template.fare | |
| shallow.fare | |
| template.stops | |
| shallow.stops | |
| deep.stops | |
| stops ক’টি (template) |
- নমুনা।
template-এ দুটি সাধারণ মান (route, fare) আর একটি arraystops. Object-টি array-কে নিজের ভেতরে রাখে না, রাখে তার ঠিকানা (reference) — ছবিতে array #1. { ...template }— shallow copy. নতুন একটি বাইরের object তৈরি হলো, আর প্রতিটি field-এর মান কপি হলো। Fare একটি সংখ্যা, তাই নিজস্ব 950. কিন্তু stops-এর মান একটি ঠিকানা — কপি হলো সেই ঠিকানাই। দুই object এখন একই array #1 দেখাচ্ছে।shallow.fare = 1100. শুধু shallow-এর নিজের ঘর বদলাল; template-এর fare 950-ই রইল। এই ধাপ দেখে মনে হয় “কপি তো ঠিকঠাক কাজ করছে” — এখানেই ফাঁদ।shallow.stops.push("Feni"). Push বদলায় array #1-কে, আর সেটি দুজনেরই। Template-এ এখন ৪টি থামার জায়গা, যদিও কেউ template-এ হাত দেননি।- বাগ ছড়িয়ে পড়ে। কাল template থেকে আরেকটি যাত্রা কপি করা হলে সেটিও Feni পাবে। ভুল কোথায় ঢুকল, তা খুঁজতে হবে অন্য একটি যাত্রার কোডে — বাগ যেখানে দেখা যায়, কারণ সেখানে নয়।
- Deep copy. Template ঠিক করে (Feni সরিয়ে)
structuredClone(template)নেওয়া হলো — ভেতরের array-ও নতুন, #2. এবারdeep.stops-এ Feni যোগ করলেও template-এ ৩টিই থাকে।
নিচের কোডটি stepper-এর গল্পই চালায়, আর শেষে একটি দ্বিতীয় ফাঁদ দেখায়: structuredClone class চেনে না।
// Prototype: new trips are cloned from a ready-made template (fictional)
const template = { route: "DHK-CTG", fare: 950,
stops: ["Dhaka", "Cumilla", "Chattogram"] };
const shallow = { ...template }; // copies the top level only
shallow.fare = 1100; // a number: copied by value
shallow.stops.push("Feni"); // an array: still the SAME array
console.log("template.fare :", template.fare);
console.log("template.stops:", template.stops.join(" > "));
console.log("same array? :", shallow.stops === template.stops);
template.stops.pop(); // undo the damage
const deep = structuredClone(template); // copies nested data too
deep.stops.push("Feni");
console.log("after deep :", template.stops.length, "vs", deep.stops.length);
class Trip { // a pitfall of structuredClone
constructor(route, stops) { this.route = route; this.stops = stops; }
label() { return `${this.route} (${this.stops.length} stops)`; }
clone() { return new Trip(this.route, [...this.stops]); } // Prototype's clone()
}
const t = new Trip("DHK-SYL", ["Dhaka", "Bhairab", "Sylhet"]);
const plain = structuredClone(t);
console.log("structuredClone keeps class?", plain instanceof Trip);
const c = t.clone(); c.stops.push("Moulvibazar");
console.log("clone():", c.label(), "| original:", t.label());আউটপুট:
template.fare : 950
template.stops: Dhaka > Cumilla > Chattogram > Feni
same array? : true
after deep : 3 vs 4
structuredClone keeps class? false
clone(): DHK-SYL (4 stops) | original: DHK-SYL (3 stops)
প্রথম তিন লাইন stepper-এর ধাপ ৩–৪: fare নিরাপদ, কিন্তু template-এ Feni ঢুকে গেছে, কারণ দুটি stops একই array (=== সত্য)। চতুর্থ লাইনে structuredClone ভেতরের array-ও কপি করেছে, তাই ৩ বনাম ৪. পঞ্চম লাইন দ্বিতীয় ফাঁদ: structuredClone একটি Trip object-কে সাধারণ object বানিয়ে ফেলে — label() আর clone() method হারায়, কারণ এটি data কপি করে, class-এর prototype নয়; আর function-ওয়ালা মান পেলে error দেয়। শেষ লাইনে Prototype pattern-এর আসল রূপ: class নিজেই জানে কীভাবে নিজেকে কপি করতে হয়, আর ভেতরের array-র নতুন কপি ([...this.stops]) নিজে বানায়। কোন অংশ ভাগ করা চলে আর কোনটি নিজস্ব হতেই হবে, সেই সিদ্ধান্ত class-এর লেখক নেন — বাইরের কোড নয়।
Python-এ copy আর deepcopy
Python-এর copy module-এ দুটি function: copy.copy(x) shallow, copy.deepcopy(x) deep. Class চাইলে __copy__ আর __deepcopy__ লিখে নিজের নিয়ম দিতে পারে — Python-এ Prototype-এর clone() এটিই। deepcopy একটি memo dictionary রাখে, তাই নিজের দিকে ফিরে আসা (cyclic) গঠনেও আটকে যায় না। কিন্তু lock, খোলা file বা network connection-এর মতো object অর্থপূর্ণভাবে কপি করা যায় না — deepcopy হয় error দেবে, নয়তো এমন কিছু কপি করবে যা আপনি চাননি। List-এর list(x), x[:], dict.copy() — সবই shallow. বিস্তারিত দেখুন Python-এর mutability ও copying পর্বে; JavaScript-এর spread আর prototype-এর জন্য destructuring ও spread আর prototype পর্ব।
ফল: (+) concrete class না জেনে নতুন object, দামি প্রস্তুতি একবারই, আর চলার সময় নতুন “ধরন” যোগ (একটি নতুন নমুনা registry-তে রাখা)। (−) প্রতিটি class-এ ঠিক clone() লেখা কঠিন, বিশেষ করে ভেতরে ভাগ করা আর নিজস্ব অংশ মেশানো থাকলে। ফাঁদ: shallow copy-র ভাগ হওয়া অংশ; deep copy-র খরচ আর অ-কপিযোগ্য অংশ; আর Java-র Object.clone()/Cloneable — Bloch এটিকে নকশায় ত্রুটিপূর্ণ বলেছেন আর copy constructor বা copy factory-র পরামর্শ দিয়েছেন। সবচেয়ে পরিষ্কার বিকল্প প্রায়ই immutable data: যা বদলায় না, তা ভাগ করা নিরাপদ, কপির প্রশ্নই ওঠে না (পর্ব ৩১-এর value object আর defensive copy). নামের মিল: JavaScript-এর “prototype-ভিত্তিক inheritance” (Object.create(proto)) একটি ভাষার বৈশিষ্ট্য — নতুন object একটি পুরোনো object-কে ভিত্তি করে, কপি না করে। ধারণা কাছাকাছি, কিন্তু GoF-এর Prototype pattern-এর সমান নয়।
৭ · Object pool, তুলনা আর কোনটি কখন
Object pool — সংক্ষেপে (GoF-এর বাইরে)
কিছু object বানাতে সময় আর সম্পদ — দুটোই খরচ হয়: database connection (network handshake, login), thread, বড় buffer. Object pool এগুলোর কয়েকটি আগে থেকে বানিয়ে রাখে; কাজের সময় একটি ধার নেওয়া হয়, কাজ শেষে ফেরত — নতুন বানানো বা ধ্বংস নয়। বাস টার্মিনালের কাউন্টারে ভাড়ার ছাতার মতো: সংখ্যা সীমিত, নিয়ে ফেরত দিতে হয়। এটি GoF-এর ২৩টির মধ্যে নেই, তবে বাস্তবে খুব প্রচলিত — Java-র HikariCP-র মতো connection pool, SQLAlchemy-র ভেতরের pool, Python-এর concurrent.futures.ThreadPoolExecutor. ফাঁদ: ফেরত দিতে ভুললে pool খালি হয়ে সবাই অপেক্ষায় আটকায় (তাই Python-এ with); ফেরত দেওয়ার আগে অবস্থা পরিষ্কার না করলে পরের ব্যবহারকারী আগের জনের data পায়; আর সস্তা object pool করলে লাভের চেয়ে জটিলতা বেশি — আধুনিক garbage collector-এ ছোট object বানানো খুব সস্তা।
তুলনার টেবিল
| Pattern | কখন ব্যবহার | প্রধান দাম | Python/JS-এ প্রায়ই যথেষ্ট |
|---|---|---|---|
| Singleton | সত্যিই একটিমাত্র shared সম্পদ, আর একাধিক হলে ভুল | লুকানো global state, test-এর কষ্ট | Module-এর instance + composition root থেকে inject |
| Factory Method | Base class-এর কাজ এক, কিন্তু subclass-ভেদে product আলাদা | প্রতি product-এ নতুন subclass | Class বা function-কে parameter হিসেবে পাঠানো |
| Simple factory (GoF নয়) | Input দেখে কোন class, এক জায়গায় ঠিক করা | বড় হলে switch-এর ভিড় | Dictionary/Map-এর registry |
| Abstract Factory | কয়েকটি product একসাথে বদলায় আর মেশানো চলে না | নতুন product-এর ধরন যোগ করা কঠিন | Function-এর একটি object/module (kit) |
| Builder | অনেক ঐচ্ছিক অংশ, ধাপে ধাপে জোড়া, শেষে যাচাই | প্রতিটি field দুবার লেখা | Keyword argument, dataclass, options object |
| Prototype | বানাতে দামি, বা class না জেনে কপি দরকার | ঠিক clone() লেখা, copy-র গভীরতা | copy.deepcopy, structuredClone, immutable data |
| Object pool (GoF নয়) | বানাতে খুব দামি, সংখ্যা সীমিত | ফেরত না দেওয়া, পুরোনো অবস্থা | Library-র নিজস্ব pool ব্যবহার |
বাছাইয়ের সহজ নির্দেশিকা
- আগে জিজ্ঞেস করুন: সাধারণ constructor কি যথেষ্ট? একটি জায়গায়
Ticket(...)ডাকলে কোনো pattern লাগে না। Pattern আনুন ব্যথা দেখা দিলে — একইif kind == ...তিন জায়গায়, বা test-এ নকল বসানোর জায়গা নেই (পর্ব ২৬-এর YAGNI আর rule of three). - “কোন class” প্রশ্ন একটি product নিয়ে? Input দেখে বাছাই → simple factory বা registry. Subclass-এর কাজের ভেতরে product বদলায় → Factory Method.
- কয়েকটি product একসাথে বদলায়? Abstract Factory — কিন্তু আগে দেখুন, নতুন product-এর ধরন ঘনঘন আসবে কি না।
- সমস্যা “কীভাবে জোড়া” — অনেক অংশ, নিয়ম, ক্রম? Builder; Python-এ আগে keyword argument আর dataclass চেষ্টা করুন।
- তৈরি নমুনা থেকে কপি? Prototype — আর প্রথমেই ঠিক করুন কোন অংশ shallow চলবে, কোনটি deep হতেই হবে।
- “একটিই থাকবে” মনে হচ্ছে? একটি বানান, inject করুন। Class-কে নিজে নিজের সংখ্যা পাহারা দিতে দেবেন না, যদি না সত্যিই উপায় না থাকে।
আরও গভীরে: GoF বইয়ের creational অধ্যায় পাঁচটি pattern-ই একটি গোলকধাঁধা (maze) খেলার একই উদাহরণে দেখায় — কীভাবে একই createMaze কোড factory, builder বা prototype বদলে ভিন্ন রকম গোলকধাঁধা বানায়; পাঁচটির তুলনা বোঝার জন্য অংশটি পড়ার মতো। Joshua Bloch-এর Effective Java-র শুরুর তিনটি পরামর্শ (static factory method, builder আর enum দিয়ে singleton) Java-র দিক থেকে একই প্রশ্নের আধুনিক উত্তর। Mark Seemann-এর dependency injection নিয়ে লেখায় Singleton-কে “lifetime” হিসেবে দেখার ধারণা আছে — একটি object একটি কিনা, সেটি container বা composition root-এর সিদ্ধান্ত, class-এর নয়।
পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এই ধরনের প্রশ্ন আসতে পারে:
- Factory Method আর Abstract Factory-র পার্থক্য? — প্রথমটি একটি product, subclass-এর override দিয়ে; দ্বিতীয়টি সম্পর্কিত product-এর পুরো পরিবার, একটি factory object দিয়ে।
- Singleton-কে anti-pattern বলা হয় কেন? — লুকানো global state আর dependency, test-এর মধ্যে অবস্থা চুইয়ে পড়া, আর “একটিই” অনুমান পরে ভাঙে; বিকল্প dependency injection.
- Builder কোন সমস্যা সমাধান করে? — Telescoping constructor: অনেক ঐচ্ছিক parameter, পড়তে কঠিন ডাক, আর অসম্পূর্ণ object;
build()-এ এক জায়গায় যাচাই। - Shallow আর deep copy-র পার্থক্য, একটি উদাহরণসহ? — Shallow-এ ভেতরের list ভাগ হয়, তাই কপির list-এ append করলে আসলটিও বদলায়; deep-এ ভেতরের অংশও নতুন।
- Thread-safe singleton কীভাবে? — শুরুতেই বানানো (eager), lock দিয়ে যাচাই-আর-বানানো একসাথে, Java-তে
volatile-সহ double-checked locking, holder idiom বা enum.
৮ · AI-এর যুগে creational pattern
“একটি notification system design করে দিন” বললে AI প্রায়ই একসাথে একটি NotificationFactory, একটি NotificationManager singleton, একটি NotificationBuilder আর তিন স্তরের interface নিয়ে আসে। কোডটি “পেশাদার” দেখায়, কিন্তু একটি ছোট অ্যাপে এর অর্ধেকই অকারণ। AI pattern-এর নাম আর আকার ভালো জানে; আপনার project-এর আকার, পরিবর্তনের ইতিহাস আর test-এর চাহিদা জানে না। তাই AI-এর প্রস্তাবে প্রথম প্রশ্ন সবসময়: “এই স্তর না থাকলে কী ক্ষতি হতো?”
| AI-এর সাধারণ ভুল | কীভাবে ধরবেন |
|---|---|
| প্রতিটি “manager” বা “service”-কে singleton বানানো, প্রায়ই metaclass দিয়ে | Test দুটি ক্রমে চালান; constructor-এ নির্ভরতা দেখা যায় কি না দেখুন |
| Simple factory-কে “Factory Method pattern” বলা | Subclass আর override আছে কি না দেখুন; GoF-এর intent-এর সাথে মেলান |
| একটি product-এর জন্যই Abstract Factory, বা ভবিষ্যতের অনুমানে স্তর | পরিবার আসলে আছে কি না, আর গত কয়েক মাসের বদলের ইতিহাস দেখুন |
Builder আছে কিন্তু build()-এ কোনো যাচাই নেই | আবশ্যক অংশ বাদ দিয়ে build() ডাকার একটি test লিখুন |
“Clone” বলে shallow copy ({...obj}, copy.copy) দেওয়া, বা class instance-এ structuredClone | কপির ভেতরের list বদলে আসলটি দেখুন; instanceof যাচাই করুন |
| UML-এ ভুল তীর: generalization-এ খোলা তীর, বা realization-এ কঠিন রেখা | পর্ব ২৯-এর চিহ্নের সাথে মিলিয়ে দেখুন |
| বানানো library-API বা ভুল version-এর বৈশিষ্ট্য (যেমন কোনো method যা নেই) | Library-র অফিসিয়াল documentation, আর একটি ছোট script চালিয়ে দেখুন |
AI-কে pattern বাছাইয়ে সাহায্যকারী হিসেবে ব্যবহার করুন, সিদ্ধান্তদাতা হিসেবে নয়। নিচের prompt-এ খেয়াল করুন — বিকল্প হিসেবে “কোনো pattern নয়” রাখতে বলা হয়েছে, আর প্রতিটি প্রস্তাবের সাথে test চাওয়া হয়েছে:
AI-এর প্রস্তাব হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১GoF-এর একজন লেখক নিজেই নাকি Singleton বাদ দিতে চেয়েছিলেন। তাহলে এটি বইয়ে এলো কেন, আর আজও logger আর config-এ কেন এত দেখা যায়?
বই প্রকাশের প্রায় ১৫ বছর পরে, ২০০৯ সালের একটি সাক্ষাৎকারে (InformIT-এ প্রকাশিত, “Design Patterns 15 Years Later” নামে প্রায়ই উদ্ধৃত) Erich Gamma-কে প্রশ্ন করা হয়েছিল নতুন সংস্করণে কোন pattern বাদ পড়বে। উদ্ধৃতিটি সাধারণত এভাবে বলা হয়: তিনি Singleton বাদ দেওয়ার পক্ষে, কারণ এর ব্যবহার প্রায় সবসময়ই একটি design smell. শব্দগুলো হুবহু মনে রাখার দরকার নেই; মূল কথা হলো, pattern-টির লেখকদের একজনই এর ব্যবহার নিয়ে সন্দিহান হয়েছিলেন।
১৯৯৪ সালে প্রেক্ষাপট ভিন্ন ছিল। C++ আর Smalltalk-এর জগতে global variable-এর সমস্যা জানা ছিল — কে কখন initialize করবে, ক্রম কী হবে (C++-এ ভিন্ন ফাইলের global object-এর initialization-এর ক্রম নির্দিষ্ট নয়)। Singleton অন্তত “প্রথম ব্যবহারের সময় বানানো” আর “একটিই” নিশ্চিত করত — global variable-এর চেয়ে নিয়ন্ত্রিত। সেই সময়ে automated unit test আজকের মতো দৈনন্দিন অভ্যাস ছিল না; Kent Beck-এর SUnit আর পরে JUnit (১৯৯০-এর দশকের শেষে) সেই অভ্যাস ছড়ায়। Test-এর চাপ যখন এলো, তখনই Singleton-এর আসল দাম দেখা গেল: প্রতিটি test-এর আগে অবস্থা পরিষ্কার করতে হয়, নকল বসানো কঠিন, আর test-এর ক্রম বদলালে ফল বদলায়।
তবু logger আর config-এ এটি টিকে আছে, কারণ এখানে দাম কম আর সুবিধা বেশি। Logger-এর কাজ প্রায় শুধু লিখে যাওয়া — test-এর ফলাফল সাধারণত এর ওপর নির্ভর করে না, আর প্রতিটি function-এ logger parameter হিসেবে পাঠানো অসহ্য হতো। Python-এর logging.getLogger(name) তাই একটি registry দিয়ে নাম ধরে একটি করে logger দেয়। Config-এর বেলায় ভালো অভ্যাস হলো প্রোগ্রাম শুরুর সময় একবার পড়ে immutable object বানানো — অবস্থা বদলায় না, তাই ভাগ করাও নিরাপদ; তারপর যেখানে সম্ভব, constructor দিয়ে দেওয়া। দুটি জিনিস আলাদা করে দেখুন: “একটি instance” একটি lifetime-এর সিদ্ধান্ত, যা composition root নিতে পারে; “সবখান থেকে getInstance()” একটি access-এর সিদ্ধান্ত, যেটি সমস্যার মূল।
মূল উপলব্ধি: একটি pattern তার সময়ের সমস্যার উত্তর। প্রেক্ষাপট বদলালে — এখানে automated testing-এর প্রসার — একই উত্তরের দাম বদলায়। Pattern শেখার সাথে তার দাম কখন বাড়ে, সেটাও শিখুন।
প্র ০২Python আর JavaScript-এ এই পাঁচটি pattern-এর অনেকটাই যেন “অদৃশ্য”। তাহলে কি design pattern শুধু Java আর C++-এর জন্য?
১৯৯৬ সালে Peter Norvig “Design Patterns in Dynamic Languages” নামে একটি উপস্থাপনায় দাবি করেন, GoF-এর ২৩টির মধ্যে ১৬টি Lisp বা Dylan-এর মতো ভাষায় হয় অদৃশ্য, নয়তো অনেক সরল — কারণ ভাষার বৈশিষ্ট্যই কাজটি করে দেয়। Creational pattern-এ এটি স্পষ্ট দেখা যায়। Python আর JavaScript-এ class নিজেই একটি মান — একটি function-কে parameter হিসেবে SmsChannel class পাঠানো যায়, আর ভেতরে cls() ডেকে object বানানো যায়। তাহলে Factory Method-এর জন্য subclass-এর পুরো শ্রেণিবিন্যাস লাগে না। Abstract Factory একটি dictionary বা module — function-এর একটি সংগ্রহ। Builder-এর বেশির ভাগ কাজ keyword argument আর default মান করে দেয়। Prototype-এর জন্য ভাষায় copy module বা structuredClone আছে। Singleton-এর জন্য module নিজেই একটিমাত্র।
কিন্তু এর মানে এই নয় যে pattern অপ্রাসঙ্গিক। Pattern-এর দুটি স্তর: একটি সমস্যা আর ভাবনা (বানানোর সিদ্ধান্ত ব্যবহারকারী থেকে আলাদা করা, পরিবার মেশানো আটকানো, অসম্পূর্ণ object কখনো তৈরি হতে না দেওয়া), আরেকটি বাস্তবায়নের কাঠামো (কোন class, কোন তীর)। Dynamic ভাষায় দ্বিতীয় স্তরটি ছোট হয়ে যায়, প্রথমটি থাকে পুরোপুরি। একটি Python dictionary-তে {"sms": SmsKit, "email": EmailKit} লেখা আসলে Abstract Factory-র ভাবনাই — শুধু অনুষ্ঠান কম।
আরও একটি লাভ হলো শব্দভান্ডার. Code review-এ “এটি একটি kit — একই পরিবারের সব object এখান থেকে আসবে” বললে দলের সবাই এক বাক্যে নকশাটি বোঝেন। আর বড় Python বা TypeScript project-এ, যেখানে type checker আর interface ফিরে আসে, GoF-এর কাঠামোও আবার কাজে লাগে। উল্টো দিকে, Java-তেও আজকের ভাষায় lambda, record আর List.of-এর মতো static factory কিছু pattern-কে ছোট করেছে।
মূল উপলব্ধি: Pattern-এর নাম আর ভাবনা ভাষা-নিরপেক্ষ; তার কাঠামো ভাষার ক্ষমতার ওপর নির্ভর করে। নতুন ভাষায় একটি pattern ঢোকানোর আগে জিজ্ঞেস করুন — ভাষা কি এটি আগেই সহজে দিয়ে রেখেছে?
প্র ০৩Java-তে double-checked locking দিয়ে বানানো singleton বহু বছর “ভাঙা” ছিল বলে শোনা যায়। দুবার যাচাই করেও ভুল হয় কীভাবে?
কৌশলটি দেখতে নিরীহ: প্রথমে lock ছাড়াই দেখুন instance == null কি না; null হলে lock নিন, ভেতরে আবার দেখুন, তারপর বানান। উদ্দেশ্য — প্রতিটি getInstance()-এ lock-এর খরচ এড়ানো, কারণ প্রথমবারের পরে instance তো থাকছেই। সমস্যা লুকিয়ে আছে “বানান” শব্দটিতে। instance = new Singleton() আসলে কয়েকটি কাজ: memory নেওয়া, constructor চালিয়ে field পূরণ করা, আর ঠিকানাটি instance-এ লেখা। Compiler বা CPU — একটি thread-এর দিক থেকে ফল একই থাকলে — এগুলোর ক্রম বদলাতে পারে: ঠিকানা আগে লেখা, constructor পরে।
এখন দ্বিতীয় একটি thread lock ছাড়াই প্রথম যাচাইটি করছে। সে দেখল instance আর null নয় — তাই lock না নিয়েই object ব্যবহার শুরু করল, অথচ constructor তখনো শেষ হয়নি। অর্ধেক-তৈরি object! এই বাগ ধরা অসম্ভবের কাছাকাছি: খুব কম ঘটে, যন্ত্র আর JVM-ভেদে আলাদা, debugger-এ পুনরায় ঘটানো যায় না। ২০০০ সালের দিকে Bill Pugh-সহ কয়েকজন গবেষক আর প্রকৌশলী “The ‘Double-Checked Locking is Broken’ Declaration” নামে একটি লেখা প্রকাশ করেন, যা দেখায় তখনকার Java memory model-এ এর কোনো নিরাপদ রূপ নেই।
সমাধান এলো ভাষার নিয়মে। JSR-133 (Java 5, ২০০৪) memory model নতুন করে লেখে, আর volatile-এর অর্থ শক্ত করে: একটি volatile field-এ লেখার আগের সব লেখা, সেটি পড়া অন্য thread দেখতে পাবে। তাই এখন private static volatile Singleton instance দিলে double-checked locking কাজ করে। তবে সহজ বিকল্পগুলো প্রায়ই ভালো: Pugh-এর “holder” idiom (একটি ভেতরের static class, যা JVM-এর class loading-এর নিয়মে একবারই, নিরাপদে initialize হয়), Bloch-এর enum singleton, অথবা lazy না হওয়া। Python-এ GIL অনেক ক্রম-বদলের সমস্যা আড়াল করে রাখে, কিন্তু যাচাই আর বানানোর মাঝে thread বদল হতেই পারে, আর Python ৩.১৩ থেকে GIL ছাড়া চালানোর একটি ঐচ্ছিক build-ও আছে — তাই lock বা module-স্তরের instance-ই নিরাপদ পথ।
মূল উপলব্ধি: একাধিক thread-এর কোডে “আমি তো দুবার যাচাই করেছি” যুক্তি যথেষ্ট নয়; ভাষার memory model যা নিশ্চিত করে, শুধু তার ওপরই ভরসা করা যায়। আর সবচেয়ে নিরাপদ concurrency-কোড হলো যা concurrency-র চালাকি এড়িয়ে চলে।
প্র ০৪Shallow copy বিপজ্জনক হলে সবসময় deep copy করলেই তো হয়। সেটি কি নিরাপদ সমাধান?
না — deep copy-র নিজের তিনটি সমস্যা আছে। প্রথমত খরচ। একটি বাস রুটের template-এ যদি হাজার আসনের নকশা আর কয়েক হাজার ভাড়ার নিয়ম থাকে, প্রতিটি যাত্রার জন্য সব deep copy করলে memory আর সময় দুটোই বাড়ে, অথচ এর বেশির ভাগ কেউ কখনো বদলাবে না। দ্বিতীয়ত সব কিছু কপি করা যায় না, বা করা উচিত নয়। একটি database connection, খোলা file, lock বা network socket “কপি” করার অর্থ কী? দুটি object একই connection ব্যবহার করুক, এটাই হয়তো ঠিক। Python-এর deepcopy এমন object-এ হয় error দেয়, নয়তো এমন কিছু কপি করে যা আপনি চাননি; JavaScript-এর structuredClone function পেলে error দেয় আর class instance-কে সাধারণ object বানিয়ে ফেলে। তৃতীয়ত পরিচয় (identity). কিছু অংশ ইচ্ছা করেই ভাগ করা — যেমন সব যাত্রা একই Route object দেখায়, যাতে রুটের নাম একবার বদলালে সবখানে বদলায়। Deep copy সেই সংযোগ কেটে দেয়।
তাই Prototype-এর clone()-এ সিদ্ধান্তটি field ধরে ধরে নিতে হয়: কোন অংশ নিজস্ব (যাত্রীর তালিকা, বুক করা আসন — নতুন কপি), কোন অংশ ভাগ করা (রুট, ভাড়ার নিয়ম — একই reference). Java-র Object.clone()-এর সমালোচনা এখান থেকেই: এটি নিজে থেকে shallow copy করে, আর Cloneable interface-এ কোনো method-ই নেই — Joshua Bloch Effective Java-য় একে নকশার দিক থেকে ত্রুটিপূর্ণ বলে copy constructor বা copy factory-র পরামর্শ দিয়েছেন।
সবচেয়ে শক্ত সমাধান প্রায়ই প্রশ্নটিই সরিয়ে দেয়: immutable data. যে list কেউ বদলাতে পারে না (Python-এর tuple, frozen dataclass, JavaScript-এ Object.freeze — যদিও freeze নিজেও shallow), তা ভাগ করা পুরোপুরি নিরাপদ। বদল চাইলে নতুন মান বানানো হয়, আর ভালো immutable library “structural sharing” দিয়ে না-বদলানো অংশ পুরোনোর সাথে ভাগ করে খরচও কমায় — Clojure-এর data structure আর JavaScript-এর Immer-এর মতো library এই ভাবনার পরিচিত উদাহরণ। Functional programming-এর জগৎ এজন্যই copy-র বাগ নিয়ে কম ভোগে।
মূল উপলব্ধি: “Shallow না deep” প্রশ্নের আসল রূপ হলো “কোন অংশের মালিক কে?” মালিকানা স্পষ্ট হলে কপির গভীরতা নিজেই ঠিক হয়ে যায়; আর যা বদলায় না, তার মালিকানা নিয়ে ভাবতেই হয় না।
প্র ০৫Abstract Factory-তে নতুন পরিবার যোগ করা সহজ, কিন্তু নতুন ধরনের product যোগ করা কঠিন। এই অসমতা কি নকশার দোষ, নাকি এর পেছনে গভীর কোনো কারণ আছে?
এর পেছনে software নকশার একটি পুরোনো ও মৌলিক টানাপোড়েন আছে। ১৯৯৮ সালে Philip Wadler একটি email-এ এর নাম দেন expression problem, যদিও সমস্যাটি আগেই আলোচিত ছিল। ধারণাটি একটি ছক দিয়ে বোঝা যায়: সারিতে পরিবার (SMS, email, push), কলামে product-এর ধরন (message, address, sender). প্রতিটি ঘরে একটি concrete class. Object-oriented নকশা সারি ধরে কোড সাজায় — প্রতিটি পরিবার একটি kit-এ নিজের সব ঘর রাখে। তাই নতুন সারি (নতুন পরিবার) মানে নতুন একটি kit — পুরোনো কোড অছোঁয়া। কিন্তু নতুন কলাম (নতুন product, যেমন template) মানে প্রতিটি সারিতে একটি করে ঘর যোগ — interface আর সব kit বদলাতে হবে।
Functional নকশা উল্টো দিকে সাজায় — প্রতিটি operation একটি function, যার ভেতরে সব ধরনের জন্য case. সেখানে নতুন operation সহজ, নতুন ধরন কঠিন। পর্ব ৩৬-এর visitor pattern এই উল্টো দিকের OOP রূপ: নতুন operation যোগ সহজ করে, নতুন element-এর ধরন কঠিন করে। কোনো সাধারণ নকশাই দুটি দিক একসাথে পুরোপুরি সহজ করে না; ভাষাভিত্তিক কিছু উন্নত কৌশল (type class, multimethod, extension method ইত্যাদি) এর কাছাকাছি যায়, কিন্তু দাম দিয়ে।
বাস্তব শিক্ষা হলো, কোন অক্ষে বদল বেশি আসে, সেটি অনুমান করে নকশা বাছাই। একটি payment SDK-তে নতুন provider (পরিবার) প্রায়ই আসে, কিন্তু provider-প্রতি কাজের ধরন (charge, refund, webhook যাচাই) কয়েক বছরে একবার বদলায় — Abstract Factory মানানসই। উল্টো দিকে, যদি দেখেন প্রতি মাসে kit-এ নতুন method যোগ করতে হচ্ছে আর তিনটি kit একসাথে বদলাতে হচ্ছে, তাহলে নকশাটি ভুল অক্ষে খোলা — পর্ব ২৭-এর ভাষায়, OCP ভুল দিকে প্রয়োগ হয়েছে। Default implementation দেওয়া (base kit-এ একটি সাধারণ createTemplate) যন্ত্রণা কমায়, কিন্তু সব পরিবারের জন্য ঠিক default সবসময় থাকে না।
মূল উপলব্ধি: প্রতিটি নকশা কোনো একটি দিকে বদলকে সস্তা করে, আরেক দিকে দামি। ভালো নকশাকার আগে বদলের ইতিহাস দেখেন — Git-এর log-ই প্রায়ই বলে দেয় কোন অক্ষ ঘনঘন নড়ে।
অনুশীলন
আউটপুট অনুমান, pattern বাছাই, refactoring আর একটি হিসাব — চারটিই পরীক্ষা ও ইন্টারভিউর ধরনের।
-
আউটপুট অনুমান করুন (copy-র বাগ): চালানোর আগে লিখে ফেলুন দুটি লাইনে কী ছাপা হবে।
import copy base = {"route": "DHK-RAJ", "seats": ["A1"]} a = copy.copy(base); b = copy.deepcopy(base) a["route"] = "DHK-KHL"; a["seats"].append("A2"); b["seats"].append("B1") print(base["route"], base["seats"], len(b["seats"])) print(a["seats"] is base["seats"], b["seats"] is base["seats"])DHK-RAJ ['A1', 'A2'] 2 True Falsecopy.copyএকটি নতুন dictionary বানায়, তাইa["route"]-এ নতুন string বসালেbase-এর route বদলায় না — “DHK-RAJ”. কিন্তুa["seats"]আরbase["seats"]একই list (isসত্য), তাই A2 আসলটিতেও ঢুকেছে।deepcopy-তে list নিজেও নতুন, তাই B1 শুধুb-তে: দৈর্ঘ্য ২, আরisমিথ্যা। মূল পার্থক্য: কোনো key-তে নতুন মান বসানো (assignment) কপিকে আলাদা করে, কিন্তু ভাগ করা list-এর ভেতরে বদল (append) দুজনকেই ছোঁয়। -
Pattern বেছে নিন: প্রতিটি পরিস্থিতির জন্য সবচেয়ে মানানসই pattern — অথবা “কোনো pattern নয়” — বলুন, এক লাইনে কারণসহ। (ক) একটি কাল্পনিক অনলাইন বাজার অ্যাপে light আর dark theme; প্রতিটি theme-এ button, menu আর dialog-এর আঁকা আর আচরণ একসাথে বদলায়। (খ) একটি report-এ ১৫টি ঐচ্ছিক setting (কাগজের মাপ, ভাষা, logo, footer…), আর কয়েকটি setting একসাথে অবৈধ। (গ) একটি রুটের template থেকে প্রতিদিন ৪০টি যাত্রা বানাতে হয়; template বানাতে database থেকে অনেক নিয়ম পড়তে হয়। (ঘ) একটি ছোট script-এ একবার একটি
Ticketবানানো হয়, তিনটি parameter দিয়ে। (ঙ) একটি base classReportExporter-এরexport()সব subclass-এ এক, শুধু কোন writer (PDF, CSV) বানাবে তা আলাদা। (চ) পুরো অ্যাপে একটি database connection-এর ব্যবস্থা লাগবে, আর test-এ in-memory database চাই।- (ক) Abstract Factory — button, menu, dialog একটি পরিবার, একসাথে বদলায় আর মেশানো চলে না; একটি
ThemeKitতিনটিই বানাবে। (শুধু রঙ বদলালে একটি theme-এর মানের object-ই যথেষ্ট হতো।) - (খ) Builder — অনেক ঐচ্ছিক অংশ আর মিলিত নিয়ম, যা
build()-এ এক জায়গায় যাচাই হবে। Python-এ keyword-only dataclass আর__post_init__-এ যাচাইও সমান ভালো বিকল্প। - (গ) Prototype — দামি template একবার বানিয়ে কপি; কোন অংশ ভাগ (নিয়ম) আর কোনটি নিজস্ব (যাত্রী, আসন), তা
clone()-এ ঠিক করতে হবে। - (ঘ) কোনো pattern নয় — সাধারণ constructor, keyword argument দিয়ে। এখানে pattern আনা over-engineering.
- (ঙ) Factory Method — কাজের ছাঁচ এক, product subclass ঠিক করে:
create_writer()override. Python-এ writer-এর class parameter হিসেবে দেওয়াও চলে। - (চ) Singleton নয় — একটি instance, dependency injection দিয়ে। Composition root-এ একটি connection pool বানিয়ে যাদের দরকার তাদের দিন; test-এ in-memory বিকল্প দিন। সংখ্যা “একটি” রাখা composition root-এর দায়, class-এর নয় (পর্ব ৩৭)।
- (ক) Abstract Factory — button, menu, dialog একটি পরিবার, একসাথে বদলায় আর মেশানো চলে না; একটি
-
Refactoring — design ও সমালোচনা: নিচের কোডে কোন কোন সমস্যা দেখছেন? ছোট নিরাপদ ধাপে এমনভাবে বদলান, যাতে (১) নতুন channel যোগ করতে
BookingServiceখুলতে না হয়, আর (২) test-এ আসল gateway ছাড়াই চালানো যায়।class Gateway: # singleton _inst = None @classmethod def instance(cls): if cls._inst is None: cls._inst = cls() return cls._inst def send(self, to, text): ... # talks to the real SMS provider class BookingService: def confirm(self, booking, kind): if kind == "sms": ch = SmsChannel(Gateway.instance()) elif kind == "email": ch = EmailChannel() ch.deliver(booking.contact, f"Seat {booking.seat} confirmed")সমস্যা: (ক)
Gateway.instance()একটি লুকানো dependency —BookingService-এর constructor দেখে বোঝা যায় না যে এটি আসল SMS provider-এ যায়, আর test-এ নকল দেওয়ার জায়গা নেই। (খ)if kind == ...— নতুন channel মানে এই method খোলা (OCP লঙ্ঘন); একই if অন্য জায়গাতেও থাকলে shotgun surgery. (গ) অজানাkindএলেchতৈরিই হয় না, আর কোনো স্পষ্ট বার্তা ছাড়াUnboundLocalErrorআসে। (ঘ)Gateway-এর lazy তৈরিতে lock নেই।ধাপ: ১)
BookingService.__init__(self, channels)যোগ করুন, যেখানেchannelsএকটি dictionary — নাম থেকে channel object. ২)confirm-এch = self.channels[kind](না পেলে স্পষ্টValueError). ৩) Composition root-এ একবার:gw = Gateway();service = BookingService({"sms": SmsChannel(gw), "email": EmailChannel()}). ৪) Test-এ:BookingService({"sms": FakeChannel()}), যেখানেFakeChannelপাঠানো বার্তা একটি list-এ জমা রাখে, আর test সেই list যাচাই করে। ৫) সবাই নতুন পথে এলেGateway.instance()মুছে দিন। প্রতিটি ধাপের পরে test চালান।class BookingService: def __init__(self, channels): self.channels = channels def confirm(self, booking, kind): ch = self.channels.get(kind) if ch is None: raise ValueError(f"unknown channel {kind!r}") ch.deliver(booking.contact, f"Seat {booking.seat} confirmed")এখানে Abstract Factory লাগেনি — channel একটি একক product, পরিবার নয়; dictionary-ই simple factory/registry-র কাজ করছে। Gateway এখনো একটিই, কিন্তু সেটি এখন composition root-এর সিদ্ধান্ত।
-
হিসাব — Abstract Factory-র দাম: একটি notification kit-এ ৩টি পরিবার (SMS, email, push) আর প্রতি পরিবারে ৩টি product-এর ধরন (message, address, sender). (ক) মোট কতগুলো concrete product class আর কতগুলো concrete factory class? (খ) চতুর্থ পরিবার “chat” যোগ করতে কতগুলো নতুন class লাগবে, আর কতগুলো পুরোনো class বা interface বদলাতে হবে? (গ) এর বদলে চতুর্থ product-এর ধরন “template” যোগ করলে? (ঘ) সাধারণভাবে F পরিবার আর P ধরন থাকলে দুটি ক্ষেত্রের সূত্র কী?
(ক) Concrete product = পরিবার × ধরন = ৩ × ৩ = ৯টি; concrete factory = পরিবার = ৩টি (সাথে ১টি abstract factory interface আর ৩টি product interface).
(খ) নতুন পরিবার: নতুন class = ১টি
ChatKit+ ৩টি product (ChatMessage,ChatAddress,ChatSender) = ৪টি; পুরোনো class বা interface বদল = ০ (শুধু composition root-এ kit বাছাইয়ের এক লাইন)।(গ) নতুন ধরন: নতুন = ১টি
Templateinterface + ৩টি concrete (SmsTemplate,EmailTemplate,PushTemplate) = ৪টি; পুরোনো বদল = ১টিNotificationKitinterface + ৩টি concrete kit = ৪টি — আর সেই kit-গুলোর যত ব্যবহারকারী আর test আছে, তাদেরও আবার যাচাই করতে হবে।(ঘ) সূত্র: নতুন পরিবার — নতুন class P + ১, বদল ০. নতুন ধরন — নতুন class F + ১, বদল F + ১. সংখ্যা দুটি এখানে কাছাকাছি (৪ আর ৪), কিন্তু পার্থক্য গুণগত: প্রথম ক্ষেত্রে পুরোনো, পরীক্ষিত কোড ছোঁয়া হয় না; দ্বিতীয় ক্ষেত্রে সব kit খুলতে হয়, আর F বাড়লে বদলও সমানুপাতে বাড়ে। এটিই expression problem-এর সংখ্যার রূপ।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমসব জায়গা থেকে Config.instance() ডাকা একটি singleton সরিয়ে dependency injection-এ নেওয়ার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 🔍 কোথায় কোথায়
Config.instance()ডাকা হয়, সব খুঁজে তালিকা করা - 🧩 প্রতিটি ব্যবহারকারী class-এর constructor-এ একটি
configparameter যোগ করা - 🏗️ Composition root-এ একটিমাত্র
Configবানিয়ে সবাইকে দেওয়া - 🧪 Test-এ প্রতিবার নতুন বা নকল config দেওয়া, আর test এলোমেলো ক্রমে চালিয়ে দেখা
- 🗑️ কেউ আর ব্যবহার না করলে
instance()method মুছে ফেলা
GoF-এর Factory Method pattern-এর মূল ভাবনা কোনটি?
- একটি function, যা string parameter দেখে ঠিক class-এর object ফেরত দেয়
- Base class একটি বানানোর method ঘোষণা করে, আর কোন product হবে তা subclass override করে ঠিক করে
- একটি object মিলে যাওয়া কয়েকটি product-এর পুরো পরিবার বানায়
- একটি তৈরি object কপি করে নতুন object বানানো হয়
একটি Abstract Factory-তে ৩টি concrete factory (SMS, email, push) আর ৩টি product-এর ধরন আছে। কোন বদলটি সবচেয়ে বেশি পুরোনো কোড খুলতে বাধ্য করে?
- চতুর্থ পরিবার “chat” যোগ করা
- Production-এ SMS-এর বদলে email factory ব্যবহার শুরু করা
- EmailSender-এর ভেতরে mail server-এর ঠিকানা বদলানো
- প্রতিটি পরিবারে চতুর্থ product-এর ধরন “template” যোগ করা
নিচের JavaScript কোড কী ছাপবে? const a = { tags: ["x"] }; const b = { ...a }; b.tags.push("y"); b.n = 1; console.log(a.tags.length, a.n);
1 undefined2 12 undefined- TypeError, কারণ spread-করা object-এ push চলে না
একটি team-এর দুটি test আলাদাভাবে চালালে পাস করে, কিন্তু একসাথে নির্দিষ্ট ক্রমে চালালে একটি ব্যর্থ হয়। দুটিই একটি singleton Settings.instance() পড়ে-লেখে। সবচেয়ে সম্ভাব্য কারণ আর ভালো সমাধান কোনটি?
- Singleton-এর অবস্থা এক test থেকে আরেকটিতে থেকে যায়; settings object constructor দিয়ে দেওয়া (dependency injection)
- Test runner খুব দ্রুত চলে; প্রতিটি test-এর আগে একটু অপেক্ষা করা
- Test-গুলো সবসময় একই ক্রমে চালানোর নিয়ম করা
- Singleton-এ lock যোগ করা, যাতে thread-safe হয়
একটি Python class-এ ৪টি আবশ্যক আর ৬টি ঐচ্ছিক field আছে, আর ঐচ্ছিক field-এর কয়েকটি একসাথে অবৈধ। Builder pattern আনার আগে কোনটি সবচেয়ে সরল বিকল্প, যা প্রায় একই সুবিধা দেয়?
- Class-টিকে singleton বানানো
@dataclass(kw_only=True)আর__post_init__-এ যাচাই- প্রতিটি ঐচ্ছিক field-এর সংমিশ্রণের জন্য একটি subclass
- সব field positional parameter হিসেবে নেওয়া, ঠিক ক্রমে
Simple factory (input দেখে class বাছাই করা একটি function) সম্পর্কে কোন বক্তব্যটি সঠিক?
- এটি GoF-এর ২৩টি pattern-এর একটি, Factory Method-এর আরেক নাম
- এটি একটি anti-pattern, সবসময় Abstract Factory দিয়ে বদলানো উচিত
- এটি প্রচলিত ও কাজের কৌশল, কিন্তু GoF pattern নয়; সিদ্ধান্ত এক জায়গায় রাখে
- এটি নিশ্চিত করে যে প্রতিটি class-এর একটিমাত্র object থাকবে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Creational pattern বানানোর সিদ্ধান্ত ব্যবহারকারী কোড থেকে আলাদা করে: কতগুলো (Singleton), কোন class (Factory Method), কোন পরিবার (Abstract Factory), কীভাবে জোড়া (Builder), কোন নমুনা থেকে (Prototype).
- Singleton-এর “একটিই” প্রায়ই ঠিক, “সবখান থেকে পাওয়া” প্রায়ই ভুল; module-এর instance বা composition root থেকে inject করুন।
- Simple factory GoF pattern নয়; Factory Method-এ subclass override করে product ঠিক করে; Abstract Factory-তে নতুন পরিবার সহজ, নতুন product-এর ধরন কঠিন।
- Builder-এর প্রাণ হলো
build()-এর যাচাই; Python-এ keyword argument আর dataclass প্রায়ই যথেষ্ট। - Prototype-এ প্রথম প্রশ্ন “কোন অংশের মালিক কে”; shallow copy ভেতরের list ভাগ করে,
structuredCloneclass হারায়, আর immutable data কপির প্রশ্নই সরিয়ে দেয়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৩৪ · Structural pattern — adapter, decorator, facade, proxy আর compositeপরবর্তী পর্বতৈরি object-গুলো জোড়া আর মোড়ানোর pattern.
- পর্ব ৩৭ · Dependency injection আর compositionSingleton-এর বিকল্পComposition root, inversion of control আর inheritance-এর বদলে composition.
- পর্ব ৩২ · Design pattern কী — Gang of Four, pattern-এর ভাষা আর কখন নয়আগের পর্বPattern-এর ইতিহাস, বর্ণনার কাঠামো আর অতিব্যবহারের বিপদ।
- সফটওয়্যার ইঞ্জিনিয়ারিং ও Git · কোর্স হোম৭০টি পর্বপুরো কোর্সের রোডম্যাপ।
- Python · Mutability আর copyingপাশের ট্র্যাক
copy,deepcopyআর reference-এর খুঁটিনাটি। - JavaScript · Prototypeপাশের ট্র্যাকJavaScript-এর prototype-ভিত্তিক inheritance — Prototype pattern থেকে কোথায় আলাদা।