পাঠ ২০ · ৫৮-এর মধ্যে · মডিউল ৫
Home / Courses / Software Engineering Principles & Git / প্যাটার্ন ওভারভিউ ও Singleton/Factory

ডিজাইন প্যাটার্ন ওভারভিউ ও Singleton/Factory

Design patterns overview & Singleton/Factory
৮ মিনিট পড়া মাঝারি · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • ডিজাইন প্যাটার্নের সুনির্দিষ্ট সংজ্ঞা এবং এটি "কোড কপি করা" থেকে ঠিক কীভাবে আলাদা
  • তিনটি ক্লাসিক 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 গ্লোবাল স্টেট নিয়ে আসে, যা প্র্যাকটিসে প্রায়ই সমালোচিত একটি প্যাটার্ন — গ্লোবাল স্টেট টেস্টিং কঠিন করে তোলে (L18-এর DIP/dependency-injection পছন্দের সাথে সরাসরি টেনশন — একটি Singleton-নির্ভর ক্লাস সহজে আলাদা করে টেস্ট করা যায় না)। Singleton-কে নিঃশর্ত ভালো সমাধান হিসেবে উপস্থাপন করা ভুল হবে — প্রতিটি ব্যবহারের ক্ষেত্রে এই ট্রেড-অফ সচেতনভাবে যাচাই করা দরকার।
Python
# 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 থিমের সরাসরি পুনর্ব্যবহার — একটি নতুন তৈরিযোগ্য টাইপ যোগ করতে সাধারণত শুধু ফ্যাক্টরি সম্প্রসারণ লাগে, ক্লায়েন্ট কোড নয়।

Python
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()}")

    
মূল কথা · Key takeaway

ডিজাইন প্যাটার্ন 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।

অনুশীলন

  1. চিন্তা করুন: একটি অ্যাপ্লিকেশনের লগিং সিস্টেমের জন্য Singleton ব্যবহার করা কি ভালো সিদ্ধান্ত? এর পক্ষে ও বিপক্ষে যুক্তি লিখুন, L18-এর DIP-এর সাথে সংযোগ ধরে।

    পক্ষে: একটি অ্যাপ্লিকেশনে সাধারণত একটিই লগ ফাইল/স্ট্রিমে সব লেখা উচিত, তাই একক গ্লোবাল অ্যাক্সেস পয়েন্ট স্বাভাবিক মনে হয়। বিপক্ষে (L18-এর DIP সংযোগ): Singleton ব্যবহারকারী ক্লাসগুলো লগারের একটি নির্দিষ্ট গ্লোবাল ইনস্ট্যান্সের সাথে হার্ড-বাঁধা হয়ে যায় — dependency injection (L18) দিয়ে বরং একটি Logger অ্যাবস্ট্রাকশন ইনজেক্ট করাই ভালো, যাতে টেস্টে একটি ফেক/নীরব লগার সহজে বসানো যায়। বাস্তবে অনেক আধুনিক কোডবেস "Singleton-এর মতো আচরণ" (একটি শেয়ার্ড instance) রাখে dependency injection দিয়েই, বিশুদ্ধ Singleton প্যাটার্ন এড়িয়ে।

  2. পরীক্ষা করুন: উপরের 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-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
UML বেসিকস — ক্লাস ও সিকোয়েন্স ডায়াগ্রাম