ডিজাইন প্যাটার্ন ওভারভিউ ও Singleton/Factory
এই পাঠে যা শিখবেন
- ডিজাইন প্যাটার্নের সুনির্দিষ্ট সংজ্ঞা এবং এটি "কোড কপি করা" থেকে ঠিক কীভাবে আলাদা
- তিনটি ক্লাসিক Gang-of-Four ক্যাটাগরি — বাকি M5 এই ক্যাটাগরি অনুযায়ী সংগঠিত
- Singleton প্যাটার্ন বাস্তবায়ন করা এবং
isঅপারেটর দিয়ে একক-ইনস্ট্যান্স নিশ্চিতকরণ যাচাই করা - Factory প্যাটার্ন বাস্তবায়ন করা এবং OCP-কমপ্লায়েন্ট অবজেক্ট তৈরি দেখা
১ · ডিজাইন প্যাটার্ন কী
ডিজাইন প্যাটার্নDesign Patternএকটি নাম-করা, পুনর্ব্যবহারযোগ্য সমাধান টেমপ্লেট একটি বারবার-ঘটা ডিজাইন সমস্যার জন্য — কপি-পেস্ট করার নির্দিষ্ট কোড নয়, বরং নির্দিষ্ট পরিস্থিতিতে অভিযোজিত করার মতো একটি সাধারণ পদ্ধতি। হলো একটি নাম-করা, পুনর্ব্যবহারযোগ্য সমাধান টেমপ্লেট একটি বারবার-ঘটা সফটওয়্যার ডিজাইন সমস্যার জন্য। এটি গুরুত্বপূর্ণ একটি পার্থক্য স্পষ্ট করা দরকার — একটি প্যাটার্ন কোনো নির্দিষ্ট কোড-স্নিপেট নয় যা সরাসরি কপি-পেস্ট করা যায়, বরং একটি সাধারণ পদ্ধতি যা নির্দিষ্ট পরিস্থিতিতে অভিযোজিত করতে হয়। M4-এর SOLID প্রিন্সিপল ও কাপলিং/কোহেশন লক্ষ্যগুলোর প্র্যাকটিক্যাল, নাম-করা, পুনর্ব্যবহারযোগ্য বাস্তবায়ন হিসেবে ডিজাইন প্যাটার্ন এই মডিউলে আসছে।
২ · তিনটি ক্লাসিক ক্যাটাগরি
Gang-of-Four (এই এলাকার ক্লাসিক, বহুল-উদ্ধৃত রেফারেন্স বইয়ের লেখকদের ডাকনাম) প্যাটার্নগুলোকে তিনটি ক্যাটাগরিতে ভাগ করেছে — বাকি M5 এই তিন ক্যাটাগরি অনুসরণ করে সংগঠিত।
অবজেক্ট তৈরি করার নিয়ন্ত্রিত পদ্ধতি নিয়ে — এই পাঠের ও L21-এর ফোকাস (Singleton, Factory, Builder, Prototype)।
অবজেক্ট/ক্লাস একসাথে কম্পোজ করে বড় স্ট্রাকচার বানানো নিয়ে — L22-এর ফোকাস (Adapter, Decorator, Facade)।
অবজেক্টের মধ্যে যোগাযোগ ও দায়িত্ব বণ্টন নিয়ে — L23-L24-এর ফোকাস (Observer, Strategy, Command, Iterator)।
৩ · Singleton প্যাটার্ন
Singleton নিশ্চিত করে একটি ক্লাসের ঠিক একটি ইনস্ট্যান্স থাকে, যা গ্লোবালি
অ্যাক্সেসযোগ্য — একটি ক্লাস-লেভেল get_instance() মেথড দিয়ে বাস্তবায়িত, যা প্রথমবার কল হলে
ইনস্ট্যান্স তৈরি করে, তারপর প্রতিবার সেই একই ইনস্ট্যান্স ফেরত দেয়।
# Singleton প্যাটার্ন -- ক্লাসের ঠিক একটিই ইনস্ট্যান্স, সবার জন্য একই
class AppConfig:
_instance = None
def __init__(self):
if AppConfig._instance is not None:
raise RuntimeError("AppConfig() সরাসরি ডাকবেন না -- get_instance() ব্যবহার করুন")
self.settings = {}
@classmethod
def get_instance(cls):
if cls._instance is None:
cls._instance = cls.__new__(cls)
cls._instance.settings = {}
print(" (নতুন AppConfig ইনস্ট্যান্স তৈরি হলো -- এটাই প্রথম ও শেষবার)")
return cls._instance
print("-- দুটো আলাদা get_instance() কল --")
config1 = AppConfig.get_instance()
config1.settings["theme"] = "dark"
config2 = AppConfig.get_instance()
print(f"config1 is config2 (identical object)? {config1 is config2}")
print(f"config2.settings (config1-এ সেট করা 'theme' এখানেও দেখা যাচ্ছে): {config2.settings}")
print(f"id(config1) = {id(config1)}, id(config2) = {id(config2)} -- সম্পূর্ণ একই মেমরি অবজেক্ট")
config1 is config2 — == নয়, is — পাইথনের আইডেন্টিটি অপারেটর, যা
নিশ্চিত করে দুটো ভ্যারিয়েবল আসলে একই মেমরি অবজেক্টকে নির্দেশ করছে, শুধু "সমান দেখতে" দুটো
আলাদা অবজেক্ট নয়। config1-এ সেট করা settings["theme"] config2-তেও
দেখা যাওয়াই এর প্রমাণ — এটি সেই একই অবজেক্ট।
৪ · Factory প্যাটার্ন
Factory অবজেক্ট তৈরির লজিক একটি নির্দিষ্ট মেথড/ক্লাসে এনক্যাপসুলেট করে — ক্লায়েন্ট কোড ফ্যাক্টরিকে একটি অবজেক্ট চায়, কোন কংক্রিট ক্লাস সেটা আসলে ইনস্ট্যান্সিয়েট করা হচ্ছে তা না জেনেই। এটি L16-এর OCP থিমের সরাসরি পুনর্ব্যবহার — একটি নতুন তৈরিযোগ্য টাইপ যোগ করতে সাধারণত শুধু ফ্যাক্টরি সম্প্রসারণ লাগে, ক্লায়েন্ট কোড নয়।
import math
# --- Shape হায়ারার্কি (L16-এর OCP-কমপ্লায়েন্ট পলিমরফিক ভার্সনের পুনর্ব্যবহার) ---
class Shape:
def area(self):
raise NotImplementedError
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return round(math.pi * self.radius ** 2, 2)
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Triangle(Shape):
def __init__(self, base, height):
self.base = base
self.height = height
def area(self):
return 0.5 * self.base * self.height
# --- Factory: ক্লায়েন্ট কোড কখনো কংক্রিট ক্লাসের নাম সরাসরি লেখে না ---
class ShapeFactory:
_registry = {
"circle": Circle,
"rectangle": Rectangle,
"triangle": Triangle,
}
@classmethod
def create_shape(cls, shape_type, *args):
shape_class = cls._registry.get(shape_type)
if shape_class is None:
raise ValueError(f"অজানা shape_type: {shape_type}")
return shape_class(*args)
print("-- ক্লায়েন্ট কোড শুধু ShapeFactory-কে জিজ্ঞেস করে, কখনো Circle/Rectangle/Triangle সরাসরি লেখে না --\n")
requests = [
("circle", (4,)),
("rectangle", (5, 6)),
("triangle", (8, 3)),
]
for shape_type, args in requests:
shape = ShapeFactory.create_shape(shape_type, *args)
print(f"create_shape('{shape_type}', {args}) -> {type(shape).__name__} object, area = {shape.area()}")
ডিজাইন প্যাটার্ন M4-এর নীতিগুলোকে নাম-করা, পুনর্ব্যবহারযোগ্য টেমপ্লেটে রূপান্তরিত করে — কিন্তু কোনো প্যাটার্নই সবসময়, নিঃশর্ত সমাধান নয় (Singleton-এর গ্লোবাল-স্টেট ট্রেড-অফ যেমন দেখাল)। প্রতিটি প্যাটার্ন কোন নির্দিষ্ট সমস্যা সমাধান করে, এবং কী মূল্যে, তা বোঝাটাই আসল দক্ষতা — শুধু নাম মুখস্থ করা নয়। পরের পাঠে (L21) আমরা ক্রিয়েশনাল ক্যাটাগরির বাকি দুটি প্যাটার্ন — Builder ও Prototype — দেখব।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ একজন নতুন ডেভেলপার বলছে "ডিজাইন প্যাটার্ন মানে তো ইন্টারনেট থেকে কোড কপি করে বসিয়ে দেওয়া" — এই ধারণাটি ঠিক কোথায় ভুল?
ভুল, কারণ একটি প্যাটার্ন কোনো নির্দিষ্ট, রেডিমেড কোড-স্নিপেট নয় যা যেকোনো সমস্যায় হুবহু বসিয়ে দেওয়া যায়
— এটি একটি সাধারণ পদ্ধতি/টেমপ্লেট যা প্রতিটি নির্দিষ্ট পরিস্থিতির জন্য অভিযোজিত করতে হয়।
উদাহরণস্বরূপ, এই পাঠের ShapeFactory একটি নির্দিষ্ট ব্যবহারের জন্য লেখা — অন্য একটি প্রজেক্টে
Factory প্যাটার্ন প্রয়োগ করতে হলে সম্পূর্ণ ভিন্ন ক্লাস/টাইপ নিয়ে, ভিন্ন রেজিস্ট্রি স্ট্রাকচার নিয়ে লিখতে
হবে — শুধু "ধারণাটা" (একটি কেন্দ্রীভূত মেথড অবজেক্ট তৈরি করে) পুনর্ব্যবহৃত হয়, কোড নয়।
প্র ০২
উপরের কোড সেলে config1.settings["theme"] = "dark" সেট করার পর config2.settings-এও কেন সেই মান দেখা গেল?
কারণ config1 ও config2 আসলে দুটো আলাদা ভ্যারিয়েবল নাম হলেও, উভয়েই একই একক
AppConfig অবজেক্টকে নির্দেশ করছে (get_instance() প্রথমবার একটি অবজেক্ট তৈরি
করেছে, পরের কলে সেই একই অবজেক্ট ফেরত দিয়েছে, নতুন কিছু তৈরি করেনি)। যেহেতু settings dict-টি
সেই একক অবজেক্টেরই অংশ, একটি ভ্যারিয়েবল দিয়ে তাতে পরিবর্তন করলে সেটি অন্য ভ্যারিয়েবলের মাধ্যমেও দেখা
যাবে — কারণ দুটোই একই মেমরি অবস্থানে "নির্দেশ" করছে, আলাদা কপি নয়।
প্র ০৩
Factory কোড সেলে যদি একটি নতুন Pentagon শেপ যোগ করতে হয়, ঠিক কোন কোন জায়গায় পরিবর্তন লাগবে — আর কোথায় লাগবে না?
একটি নতুন class Pentagon(Shape) ক্লাস লিখতে হবে (নিজস্ব area() সহ), এবং
ShapeFactory._registry dict-এ "pentagon": Pentagon এন্ট্রি যোগ করতে হবে —
এই দুটোই "সম্প্রসারণ" (extension)। কিন্তু ShapeFactory.create_shape() মেথডের লজিক বা
existing Circle/Rectangle/Triangle ক্লাস, কিংবা ক্লায়েন্ট কোড
যেখানে ShapeFactory.create_shape(...) কল হয় — এসব কোথাও কোনো পরিবর্তন লাগবে না। এটাই
L16-এর OCP-র সরাসরি বাস্তবায়ন — extension-এর জন্য open, modification-এর জন্য closed।
অনুশীলন
-
চিন্তা করুন: একটি অ্যাপ্লিকেশনের লগিং সিস্টেমের জন্য Singleton ব্যবহার করা কি ভালো সিদ্ধান্ত? এর পক্ষে ও বিপক্ষে যুক্তি লিখুন, L18-এর DIP-এর সাথে সংযোগ ধরে।
পক্ষে: একটি অ্যাপ্লিকেশনে সাধারণত একটিই লগ ফাইল/স্ট্রিমে সব লেখা উচিত, তাই একক গ্লোবাল অ্যাক্সেস পয়েন্ট স্বাভাবিক মনে হয়। বিপক্ষে (L18-এর DIP সংযোগ): Singleton ব্যবহারকারী ক্লাসগুলো লগারের একটি নির্দিষ্ট গ্লোবাল ইনস্ট্যান্সের সাথে হার্ড-বাঁধা হয়ে যায় — dependency injection (L18) দিয়ে বরং একটি
Loggerঅ্যাবস্ট্রাকশন ইনজেক্ট করাই ভালো, যাতে টেস্টে একটি ফেক/নীরব লগার সহজে বসানো যায়। বাস্তবে অনেক আধুনিক কোডবেস "Singleton-এর মতো আচরণ" (একটি শেয়ার্ড instance) রাখে dependency injection দিয়েই, বিশুদ্ধ Singleton প্যাটার্ন এড়িয়ে। -
পরীক্ষা করুন: উপরের Factory কোড সেলে
ShapeFactory._registry-তে একটি নতুন এন্ট্রি যোগ করুন —"square": lambda side: Rectangle(side, side)(একটি Rectangle-ই পুনর্ব্যবহার করে) — এবংrequestsলিস্টে("square", (7,))যোগ করে Run চেপে দেখুন।যেহেতু
_registry-এর মান যেকোনো callable হতে পারে (শুধু ক্লাস নয়, একটি lambda-ও), নতুন"square"এন্ট্রি একটিRectangle(7, 7)অবজেক্ট ফেরত দেবে, যারarea()হবে ৪৯। এটি দেখায় Factory প্যাটার্ন কতটা নমনীয় — এমনকি একটি "নতুন টাইপ" আসলে existing ক্লাসেরই একটি বিশেষ কনফিগারেশন হলেও ফ্যাক্টরির মাধ্যমে সহজে সাপোর্ট করা যায়, ক্লায়েন্ট কোডে কোনো `if shape == "square"` জাতীয় শাখা যোগ না করেই।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ M5-এর বাকি পাঠ — ক্রিয়েশনাল, স্ট্রাকচারাল ও বিহেভিয়ারাল প্যাটার্ন — সহ পুরো কোর্স ম্যাপ।
- আগের পাঠ L19 UML বেসিকস — ক্লাস ও সিকোয়েন্স ডায়াগ্রাম, M4-এর সমাপ্তি।
- পরের পাঠ L21 ক্রিয়েশনাল প্যাটার্ন — Builder ও Prototype।
- SOLID প্রিন্সিপল — SRP ও OCP L16 Factory প্যাটার্ন যে OCP থিম পুনর্ব্যবহার করে, তার মূল ব্যাখ্যা এই পাঠে।
- সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity, Cloud Computing & DevOps, Computer Networks, Operating Systems, Computer Architecture, Programming Languages & Compiler Design ও Software Engineering & Git — সব এক জায়গায়।