পাঠ ২১ · ৫৮-এর মধ্যে · মডিউল ৫
Home / Courses / Software Engineering Principles & Git / Builder ও Prototype

ক্রিয়েশনাল প্যাটার্ন — Builder ও Prototype

Creational patterns — Builder & Prototype
৮ মিনিট পড়া মাঝারি · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • Builder প্যাটার্নের সংজ্ঞা ও telescoping-constructor সমস্যা এটি কীভাবে সমাধান করে
  • চেইনড মেথড কল লেখার কৌশল (প্রতিটি মেথড self রিটার্ন করা)
  • Prototype প্যাটার্নের সংজ্ঞা এবং কখন ক্লোনিং স্ক্র্যাচ-নির্মাণের চেয়ে বেশি সুবিধাজনক
  • shallow copy বনাম deep copy-র বাস্তব পার্থক্য, কোড দিয়ে যাচাই করে

১ · Builder প্যাটার্ন

BuilderBuilder Patternজটিল অবজেক্টের নির্মাণকে তার চূড়ান্ত রূপ থেকে আলাদা করে — চেইনড, নামযুক্ত মেথড দিয়ে ধাপে ধাপে কনফিগার করে, শেষে একটি build() কল দিয়ে চূড়ান্ত অবজেক্ট তৈরি করা হয়। প্যাটার্ন এমন অবজেক্টের জন্য দরকার যাদের অনেকগুলো, প্রায়ই অপশনাল প্যারামিটার/কনফিগারেশন-ধাপ আছে। একটি একক কনস্ট্রাক্টরে সব প্যারামিটার গুঁজে দিলে একটি সুপরিচিত বাস্তব সমস্যা হয় — কখনো কখনো "telescoping constructor" নামে ডাকা হয় — যেখানে কল সাইট বিভ্রান্তিকর হয়ে ওঠে (কোন পজিশনে কোন প্যারামিটার, মনে রাখা কঠিন), অথবা একাধিক ওভারলোডেড কনস্ট্রাক্টর দরকার হয়। Builder এর বদলে চেইনড, নামযুক্ত মেথড দেয় — প্রতিটি মেথড একটি নির্দিষ্ট দিক সেট করে এবং self রিটার্ন করে (চেইনিং সক্ষম করতে), শেষে একটি build() কল চূড়ান্ত অবজেক্ট তৈরি করে।

Python
# Builder প্যাটার্ন -- জটিল অবজেক্ট ধাপে ধাপে, চেইনড মেথডে তৈরি করা

class Pizza:
    def __init__(self, size, crust, toppings):
        self.size = size
        self.crust = crust
        self.toppings = toppings  # tuple -- build()-এর পর অপরিবর্তনীয়

    def __repr__(self):
        return f"Pizza(size={self.size}, crust={self.crust}, toppings={list(self.toppings)})"


class PizzaBuilder:
    def __init__(self):
        self._size = "medium"
        self._crust = "regular"
        self._toppings = []

    def set_size(self, size):
        self._size = size
        return self  # চেইনিং সক্ষম করতে self রিটার্ন

    def set_crust(self, crust):
        self._crust = crust
        return self

    def add_topping(self, name):
        self._toppings.append(name)
        return self

    def build(self):
        return Pizza(self._size, self._crust, tuple(self._toppings))


print("-- চেইনড বিল্ডার কল দিয়ে দুটি আলাদা পিৎজা তৈরি --\n")

pizza1 = (
    PizzaBuilder()
    .set_size("large")
    .set_crust("thin")
    .add_topping("mushroom")
    .add_topping("olive")
    .build()
)

pizza2 = (
    PizzaBuilder()
    .set_size("small")
    .add_topping("cheese")
    .build()
)

print(f"pizza1 = {pizza1}")
print(f"pizza2 = {pizza2}")
print(f"\nদুটো স্বাধীনভাবে তৈরি হয়েছে -- pizza1-এর টপিং পরিবর্তনের প্রভাব pizza2-তে নেই:")
print(f"pizza1.toppings is pizza2.toppings? {pizza1.toppings is pizza2.toppings}")

    
লক্ষ্য করুন — pizza1 তৈরিতে চারটি মেথড কল একসাথে চেইন হয়েছে (.set_size().set_crust() .add_topping().add_topping().build()), যা একটি একক ৪-৫ প্যারামিটারের কনস্ট্রাক্টরের চেয়ে অনেক পরিষ্কার — প্রতিটি মেথডের নাম কী সেট হচ্ছে তা সরাসরি বলে দেয় (writability ও readability দুটোই বাড়ায়, PLC কোর্সের ভাষায়)। pizza2 একটি সম্পূর্ণ আলাদা, স্বাধীন PizzaBuilder() দিয়ে শুরু হয়েছে, তাই দুটো পিৎজা একে অপরকে প্রভাবিত করে না।

২ · Prototype প্যাটার্ন

PrototypePrototype Patternনতুন অবজেক্ট স্ক্র্যাচ থেকে না বানিয়ে, একটি বিদ্যমান, প্রি-কনফিগার্ড "প্রোটোটাইপ" ইনস্ট্যান্স ক্লোন (কপি) করে তৈরি করা। প্যাটার্ন কাজে লাগে যখন একটি অবজেক্ট তৈরি করা ব্যয়বহুল (যেমন জটিল ডেটা লোড করা, ভারী প্রাথমিক সেটআপ) এবং একই রকম-তবে-সামান্য-ভিন্ন অনেকগুলো ইনস্ট্যান্স দরকার। স্ক্র্যাচ থেকে প্রতিবার পুনর্নির্মাণের বদলে, একটি ইতিমধ্যে প্রস্তুত প্রোটোটাইপ ক্লোন করে তারপর কপিতে ছোট পরিবর্তন করা প্রায়ই সস্তা।

Python
import copy

# Prototype প্যাটার্ন -- স্ক্র্যাচ থেকে না বানিয়ে, একটি প্রি-কনফিগার্ড অবজেক্ট ক্লোন করা

class EnemyPrototype:
    def __init__(self, name, stats, inventory):
        self.name = name
        self.stats = stats            # dict -- ব্যয়বহুলভাবে "লোড করা" ধরে নেওয়া হচ্ছে
        self.inventory = inventory    # list

    def clone(self):
        # deepcopy ব্যবহার করে ভেতরের mutable state-ও আলাদা কপি হয়, শুধু রেফারেন্স নয়
        return copy.deepcopy(self)

    def __repr__(self):
        return f"EnemyPrototype(name={self.name!r}, stats={self.stats}, inventory={self.inventory})"


print("-- ব্যয়বহুলভাবে একবার তৈরি করা প্রোটোটাইপ, বারবার ক্লোন করে নতুন ইনস্ট্যান্স --\n")

base_orc = EnemyPrototype(
    name="Orc",
    stats={"hp": 100, "attack": 15},
    inventory=["axe", "shield"],
)
print(f"base_orc     = {base_orc}")

orc_clone = base_orc.clone()
orc_clone.name = "Orc Elite"
orc_clone.stats["attack"] = 25
orc_clone.inventory.append("potion")

print(f"orc_clone    = {orc_clone}  (নতুন, স্বতন্ত্র ইনস্ট্যান্স)")
print(f"base_orc পরে = {base_orc}  (মূল প্রোটোটাইপ অপরিবর্তিত)")

print(f"\nbase_orc is orc_clone? {base_orc is orc_clone}")
print(f"base_orc.stats is orc_clone.stats? {base_orc.stats is orc_clone.stats}")
print(f"base_orc.inventory is orc_clone.inventory? {base_orc.inventory is orc_clone.inventory}")
print("তিনটিই False -- deepcopy সত্যিকারের স্বাধীন কপি তৈরি করেছে, শুধু রেফারেন্স শেয়ার করেনি।")

    
লক্ষ্য করুন কোডে ইচ্ছাকৃতভাবে copy.deepcopy ব্যবহার করা হয়েছে, copy.copy (shallow copy) নয়। shallow copy ব্যবহার করলে orc_clone.stats ও base_orc.stats একই dict অবজেক্টকেই নির্দেশ করত — তখন orc_clone.stats["attack"] = 25 লেখাটি ভুলবশত মূল base_orc.stats-কেও বদলে দিত, যা প্রোটোটাইপের পুরো উদ্দেশ্যই নষ্ট করে দেয়। deepcopy নিশ্চিত করে নেস্টেড mutable অবজেক্টগুলো (dict, list) পর্যন্ত সত্যিকারের আলাদা কপি হয়।
মূল কথা · Key takeaway

Builder ও Prototype দুটোই "একটি অবজেক্ট কীভাবে তৈরি হয়" প্রশ্নের ভিন্ন সমাধান — Builder সমাধান করে জটিলতা (অনেক অপশনাল প্যারামিটার), Prototype সমাধান করে ব্যয় (পুনর্নির্মাণের খরচ)। L20-এর Singleton ও Factory-র সাথে মিলিয়ে, এই চারটিই M5-এর ক্রিয়েশনাল ক্যাটাগরি সম্পূর্ণ করে — পরের পাঠে (L22) আমরা স্ট্রাকচারাল ক্যাটাগরিতে (Adapter, Decorator, Facade) সরে যাব, যেখানে প্রশ্ন হবে "অবজেক্ট কীভাবে একসাথে কম্পোজ হয়," "কীভাবে তৈরি হয়" নয়।

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

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

প্র ০১ "telescoping constructor" সমস্যাটা ঠিক কী, এবং Builder প্যাটার্ন এটি কীভাবে সমাধান করে — নিজের ভাষায় ব্যাখ্যা করুন।

telescoping constructor সমস্যা হয় যখন একটি ক্লাসের অনেকগুলো অপশনাল প্যারামিটার থাকে এবং সব সম্ভাব্য কম্বিনেশন কভার করতে একাধিক ওভারলোডেড কনস্ট্রাক্টর লিখতে হয় (বা একটি বিশাল কনস্ট্রাক্টর যেখানে বেশিরভাগ প্যারামিটারই ডিফল্ট মান নিয়ে চুপচাপ বসে থাকে) — কল সাইটে বোঝা কঠিন হয়ে যায় কোন পজিশনাল আর্গুমেন্ট আসলে কী বোঝাচ্ছে। Builder এই সমস্যা সমাধান করে প্রতিটি কনফিগারেশন-ধাপকে একটি আলাদা, স্পষ্ট-নামের মেথডে ভাগ করে দিয়ে — শুধু প্রয়োজনীয় মেথডগুলোই চেইনে ডাকা হয়, ডিফল্ট মানওয়ালা বাকিগুলো এড়িয়ে যাওয়া যায়, এবং প্রতিটি মেথডের নামই বলে দেয় কী সেট হচ্ছে।

প্র ০২ উপরের Prototype কোড সেলে যদি clone() মেথডে copy.deepcopy(self)-এর বদলে copy.copy(self) (shallow copy) ব্যবহার করা হতো, তাহলে ঠিক কী ভুল ফলাফল দেখা যেত?

shallow copy একটি নতুন EnemyPrototype অবজেক্ট তৈরি করত ঠিকই, কিন্তু তার ভেতরের stats dict ও inventory list-কে নতুন করে কপি করত না — বরং মূল অবজেক্টের সাথে একই dict/list রেফারেন্স শেয়ার করত। ফলে orc_clone.stats["attack"] = 25 লেখাটি base_orc.stats-কেও ২৫ করে দিত (যেহেতু দুটোই একই dict-কে নির্দেশ করছে) — "base_orc পরে" লাইনে মূল প্রোটোটাইপের attack স্ট্যাট ভুলবশত বদলে যেত, যা প্রোটোটাইপ প্যাটার্নের "স্বাধীন কপি" প্রতিশ্রুতি ভেঙে দেয়।

প্র ০৩ Factory (L20) আর Builder — দুটোই তো "অবজেক্ট তৈরি করা" নিয়ে। কখন Factory যথেষ্ট, আর কখন Builder বেশি উপযোগী?

Factory উপযোগী যখন অবজেক্ট তৈরির সিদ্ধান্তটা মূলত "কোন টাইপ" বেছে নেওয়া নিয়ে (যেমন L20-এর ShapeFactory — circle না rectangle) এবং প্রতিটি টাইপের নিজস্ব কনস্ট্রাক্টর তুলনামূলক সরল। Builder উপযোগী যখন একটি একক ধরনের অবজেক্টেরই অনেক অপশনাল কনফিগারেশন-ধাপ থাকে (এই পাঠের Pizza — সাইজ, ক্রাস্ট, টপিং-এর যেকোনো কম্বিনেশন)। ব্যবহারিকভাবে দুটো একসাথেও ব্যবহৃত হতে পারে — একটি ফ্যাক্টরি সঠিক ধরনের বিল্ডার বেছে নিতে পারে, যেটি তারপর জটিল কনফিগারেশন হ্যান্ডেল করে।

অনুশীলন

  1. চিন্তা করুন: একটি HttpRequestBuilder কল্পনা করুন যা .set_url(), .set_method(), .add_header(key, value), .set_body() মেথড দিয়ে একটি HTTP রিকোয়েস্ট তৈরি করে। কেন এই ধরনের ক্লাসের জন্য Builder প্যাটার্ন স্বাভাবিক একটি পছন্দ, একটি একক কনস্ট্রাক্টরের তুলনায়?

    একটি HTTP রিকোয়েস্টের url ও method প্রায় সবসময় লাগে, কিন্তু header (একাধিক হতে পারে, সংখ্যা অনির্দিষ্ট) ও body (GET রিকোয়েস্টে সাধারণত লাগে না) প্রায়ই অপশনাল বা পরিবর্তনশীল-সংখ্যক। একটি একক কনস্ট্রাক্টরে "অনির্দিষ্ট সংখ্যক header" পাস করা পজিশনাল আর্গুমেন্ট দিয়ে বিশ্রী/অসম্ভব — Builder-এর add_header() মেথড বারবার কল করে যেকোনো সংখ্যক header যোগ করা যায়, এবং যে রিকোয়েস্টে body দরকার নেই সেখানে set_body() কলটাই বাদ দেওয়া যায় — ঠিক এই পাঠের PizzaBuilder-এ ঐচ্ছিক টপিং যোগ করার মতোই।

  2. পরীক্ষা করুন: উপরের Prototype কোড সেলে base_orc-এর একটি দ্বিতীয় ক্লোন বানান (orc_clone2 = base_orc.clone()), তার name "Orc Grunt" করুন, এবং Run চেপে দেখুন তিনটি অবজেক্ট (base_orc, orc_clone, orc_clone2) একে অপরকে প্রভাবিত করে কিনা।

    তিনটি অবজেক্টই সম্পূর্ণ স্বাধীন থাকবে — orc_clone2-এর name বদলালে base_orc বা orc_clone-এর কোনো ফিল্ড পরিবর্তন হবে না, কারণ প্রতিটি .clone() কল একটি সম্পূর্ণ নতুন, স্বাধীন deep copy তৈরি করে। এটি নিশ্চিত করে Prototype প্যাটার্ন যতগুলো ইচ্ছা স্বাধীন কপি তৈরি করতে পারে একটি একক প্রোটোটাইপ থেকে, প্রতিটি কপি একে অপরের থেকে সম্পূর্ণ বিচ্ছিন্ন।

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

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