পাঠ ২৪ · ৫৮-এর মধ্যে · মডিউল ৫
Home / Courses / Software Engineering Principles & Git / বিহেভিয়ারাল প্যাটার্ন

বিহেভিয়ারাল প্যাটার্ন — Command ও Iterator

Behavioral patterns — Command & Iterator
৮ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • Command প্যাটার্ন কীভাবে একটি অ্যাকশনকে "পরে চালানো/আনডু করার যোগ্য" অবজেক্টে পরিণত করে
  • একটি স্ট্যাক ব্যবহার করে multi-step undo বাস্তব কোডে ইমপ্লিমেন্ট করা
  • Python-এর __iter__/__next__ প্রোটোকল নিজে ইমপ্লিমেন্ট করা
  • একটি বাইনারি ট্রি-এর ইন-অর্ডার ট্রাভার্সাল একটি কাস্টম ইটারেটর দিয়ে বাস্তবায়ন করা

১ · Command প্যাটার্ন — অ্যাকশনকে অবজেক্ট বানানো

Command প্যাটার্নCommand Patternএকটি রিকোয়েস্ট/অ্যাকশনকে একটি অবজেক্টে encapsulate করে, সেই অ্যাকশন চালানোর জন্য প্রয়োজনীয় সব তথ্য ধরে রেখে — পরে চালানো, সারিবদ্ধ করা, লগ করা, বা আনডু করার জন্য। একটি রিকোয়েস্ট/অ্যাকশনকে একটি অবজেক্টে encapsulate করে — সরাসরি একটি মেথড কল করার বদলে, client কোড একটি Command অবজেক্ট তৈরি করে সংরক্ষণ করে রাখে যা "এই নির্দিষ্ট কাজটি করো" বোঝায়। genuine, concrete practical সুবিধা: এটি UNDO/REDO সম্ভব করে (প্রতিটি executed command নিজেকে REVERSE করার জন্য যথেষ্ট state ধরে রাখতে পারে — অনেক বাস্তব অ্যাপ্লিকেশন, যেমন টেক্সট এডিটর, ঠিক এভাবেই undo বাস্তবায়ন করে) এবং অ্যাকশন QUEUE/LOG করাও সম্ভব করে।

Add "প্রথম লাইন" Add "দ্বিতীয় লাইন" Add "তৃতীয় লাইন" ← টপ undo_last() → pop ২ বার undo_last() পর "তৃতীয় লাইন" reverse হলো "দ্বিতীয় লাইন" reverse হলো শুধু "প্রথম লাইন" টিকে আছে
স্ট্যাক (LIFO — Last In, First Out) নিশ্চিত করে সবচেয়ে সাম্প্রতিক কমান্ডটিই সবার আগে undo হয় — ঠিক ../dsa/-এর Stack ADT-এর মতোই।
Python
# Command pattern -- প্রতিটি Command একটি অ্যাকশনকে encapsulate করে -- execute()
# করলে অ্যাকশন ঘটে, undo() করলে সেটি reverse হয়। CommandHistory একটি LIFO
# স্ট্যাক রাখে (../dsa/-এর Stack ADT-এর বাস্তব প্রয়োগ) যাতে সবচেয়ে সাম্প্রতিক
# কমান্ডটিই সবার আগে undo হয়।

class Command:
    def execute(self):
        raise NotImplementedError

    def undo(self):
        raise NotImplementedError

class Document:
    def __init__(self):
        self.text = ""

class AddTextCommand(Command):
    def __init__(self, document, text):
        self.document = document
        self.text = text

    def execute(self):
        self.document.text += self.text

    def undo(self):
        self.document.text = self.document.text[: -len(self.text)]

class CommandHistory:
    def __init__(self):
        self._stack = []  # LIFO স্ট্যাক -- সর্বশেষ executed command সবার উপরে

    def execute(self, command):
        command.execute()
        self._stack.append(command)

    def undo_last(self):
        if not self._stack:
            return
        command = self._stack.pop()
        command.undo()

doc = Document()
history = CommandHistory()

history.execute(AddTextCommand(doc, "প্রথম লাইন। "))
history.execute(AddTextCommand(doc, "দ্বিতীয় লাইন। "))
history.execute(AddTextCommand(doc, "তৃতীয় লাইন। "))
print("৩টি কমান্ডের পর  :", repr(doc.text))

history.undo_last()
print("১ বার undo-এর পর :", repr(doc.text))

history.undo_last()
print("২ বার undo-এর পর :", repr(doc.text))

    
স্ট্যাক LIFO (Last In, First Out) বলেই দ্বিতীয় undo_last() কল সবচেয়ে সাম্প্রতিক কমান্ড ("তৃতীয় লাইন") নয়, বরং তার আগেরটি ("দ্বিতীয় লাইন") আনডু করে — কারণ প্রথম undo_last() কলেই "তৃতীয় লাইন" কমান্ডটি স্ট্যাক থেকে pop হয়ে গেছে। শেষ পর্যন্ত শুধু "প্রথম লাইন। " টিকে থাকে।

২ · Iterator প্যাটার্ন — কালেকশনের ইউনিফর্ম অ্যাক্সেস

Iterator প্যাটার্নIterator Patternএকটি কালেকশনের এলিমেন্ট সিকোয়েন্সিয়ালি অ্যাক্সেস করার একটি ইউনিফর্ম উপায় দেয়, কালেকশনের ইন্টারনাল রিপ্রেজেন্টেশন উন্মুক্ত না করেই। একটি কালেকশনের এলিমেন্ট একের-পর-এক অ্যাক্সেস করার একটি ইউনিফর্ম উপায় দেয়, কালেকশনটি ভেতরে ঠিক কীভাবে সাজানো (একটি লিংকড লিস্ট, একটি অ্যারে, একটি ট্রি) তা client কোড থেকে সম্পূর্ণ লুকিয়ে রেখে। Python-এর নিজস্ব __iter__/__next__ প্রোটোকল (যা প্রতিটি for লুপ ব্যবহার করে) এই ঠিক এই প্যাটার্নেরই একটি real, everyday প্রয়োগ — বিমূর্ত প্যাটার্নটিকে এমন কিছুর সাথে যুক্ত করা যা একজন শিক্ষার্থী ইতিমধ্যে সবসময় ব্যবহার করছেন।

Python
# Iterator pattern -- Python-এর নিজস্ব for-loop __iter__/__next__ প্রোটোকল
# ব্যবহার করে -- BinaryTreeCollection ভেতরে কীভাবে ডেটা সাজানো (../dsa/-এর
# বাইনারি ট্রি) তা client কোড থেকে সম্পূর্ণ লুকিয়ে রাখে, শুধু একটি uniform
# sequential access দেয় -- ইন-অর্ডার ট্রাভার্সালের জন্য ভেতরে আবার একটি
# স্ট্যাকই (LIFO) ব্যবহার হচ্ছে।

class TreeNode:
    def __init__(self, value, left=None, right=None):
        self.value = value
        self.left = left
        self.right = right

class BinaryTreeCollection:
    def __init__(self, root):
        self.root = root

    def __iter__(self):
        return _InOrderIterator(self.root)

class _InOrderIterator:
    def __init__(self, root):
        self._stack = []
        self._push_left(root)

    def _push_left(self, node):
        while node is not None:
            self._stack.append(node)
            node = node.left

    def __iter__(self):
        return self

    def __next__(self):
        if not self._stack:
            raise StopIteration
        node = self._stack.pop()
        self._push_left(node.right)
        return node.value

#           5
#          / \
#         3   8
#        / \   \
#       1   4   9
tree = TreeNode(5,
    TreeNode(3, TreeNode(1), TreeNode(4)),
    TreeNode(8, None, TreeNode(9)))

collection = BinaryTreeCollection(tree)

result = []
for value in collection:  # আসল Python for-loop -- __iter__/__next__ ব্যবহার করছে
    result.append(value)

print("In-order sequence:", result)

    
উপরের ট্রি-তে ইন-অর্ডার ট্রাভার্সাল (বাম → নিজে → ডান) মানে প্রত্যাশিত ক্রম [1, 3, 4, 5, 8, 9] — হাতে ট্রেস করে দেখুন: প্রথমে সবচেয়ে বাম দিকে নামা হয় (৫→৩→১), তারপর ১ প্রিন্ট হয়ে ৩-এ ফিরে আসা হয়, তারপর ৩-এর ডান সাবট্রি (৪), তারপর মূল ৫, তারপর ৫-এর ডান সাবট্রি (৮→৯)। যেহেতু এই ট্রিটি একটি বাইনারি সার্চ ট্রি, ইন-অর্ডার আউটপুট স্বয়ংক্রিয়ভাবে সাজানো ক্রমে আসে — একটি ভালো correctness চেক।
মূল কথা · Key takeaway

Command অ্যাকশনকে ডেটা বানায়, Iterator ট্রাভার্সালকে ইউনিফর্ম বানায় — দুটোই এমন দায়িত্ব যা অন্যথায় ছড়িয়ে থাকতো (client কোড নিজে undo-লজিক ম্যানেজ করতো, বা কালেকশনের ইন্টারনাল স্ট্রাকচার সরাসরি জানতে বাধ্য হতো) একটি dedicated অবজেক্টে encapsulate করে দেয় — M5-এর সব বিহেভিয়ারাল প্যাটার্নের সাধারণ থিম।

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

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

প্র ০১ CommandHistory-তে LIFO স্ট্যাকের বদলে যদি একটি সাধারণ লিস্ট (কোনো নির্দিষ্ট অর্ডার ছাড়া) ব্যবহার করা হতো, undo-এর আচরণ কেন ভুল হয়ে যেতে পারতো?

undo সবসময় সবচেয়ে সাম্প্রতিক অ্যাকশনটি reverse করা উচিত — এই "সবচেয়ে সাম্প্রতিক আগে" নিয়মটি ঠিক LIFO (Last In, First Out) স্ট্যাকের সংজ্ঞা। যদি কোনো অর্ডার নিশ্চিত না থাকতো (যেমন একটি সাধারণ সেট বা এলোমেলোভাবে অ্যাক্সেস করা লিস্ট), ভুল ক্রমে কমান্ড undo হতে পারতো — যেমন "প্রথম লাইন" আগে undo হয়ে গেলে document-এর টেক্সট এমন একটি স্টেটে পৌঁছাতো যা কখনো বাস্তবে ঘটেইনি।

প্র ০২ উপরের কোড সেলে for value in collection: লেখাটি Python-এর জন্য ঠিক কীভাবে কাজ করে — ভেতরে কী ঘটছে?

Python collection-এর __iter__() মেথড কল করে একটি iterator অবজেক্ট (এখানে _InOrderIterator) পায়, তারপর প্রতিটি লুপ ধাপে সেই iterator-এর __next__() কল করে পরবর্তী মান নেয় — যতক্ষণ না StopIteration ব্যতিক্রম ওঠে, তখন লুপ থেমে যায়। এটাই ঠিক সেই মেকানিজম যা প্রতিটি Python for লুপ, লিস্ট, ডিকশনারি, ফাইল সহ, ব্যবহার করে — BinaryTreeCollection শুধু নিজস্ব ডেটা স্ট্রাকচারের জন্য এই একই প্রোটোকল ইমপ্লিমেন্ট করেছে।

প্র ০৩ Iterator প্যাটার্ন ব্যবহার না করে যদি client কোড সরাসরি ট্রি-এর .left/.right পয়েন্টার নিজে ট্রাভার্স করতো, তাহলে কী সমস্যা হতো?

client কোডকে ট্রি-এর ইন্টারনাল স্ট্রাকচার সম্পর্কে বিস্তারিত জানতে হতো (এটি বাইনারি ট্রি, নাকি লিংকড লিস্ট, নাকি অ্যারে) — এবং ট্রাভার্সাল লজিক (ইন-অর্ডার recursion/stack) প্রতিটি জায়গায় যেখানে iterate করা দরকার সেখানে বারবার ডুপ্লিকেট হতো (সরাসরি L15-এর DRY লঙ্ঘন)। এছাড়া ভবিষ্যতে যদি BinaryTreeCollection-এর ভেতরের গঠন বদলানো হয় (যেমন একটি ভিন্ন ট্রি ইমপ্লিমেন্টেশনে), সব client কোড ভেঙে যেতো — Iterator এই কাপলিং সম্পূর্ণ এড়িয়ে যায়।

অনুশীলন

  1. চিন্তা করুন: একটি টেক্সট এডিটরে "ডিলিট" ও "রিপ্লেস" অ্যাকশনের জন্য Command সাবক্লাস ডিজাইন করলে প্রতিটির undo() মেথডে কী কী তথ্য (state) সংরক্ষণ করে রাখতে হবে তা লিখুন।

    DeleteTextCommand-কে অবশ্যই মুছে ফেলা টেক্সট ও তার position সংরক্ষণ করে রাখতে হবে, যাতে undo করলে ঠিক সেই টেক্সট ঠিক সেই জায়গায় পুনরায় বসানো যায়। ReplaceTextCommand-কে এমনকি আরও বেশি — replace করার আগের ORIGINAL টেক্সট, নতুন টেক্সট, ও position — তিনটিই সংরক্ষণ করতে হবে, কারণ undo করলে নতুন টেক্সট সরিয়ে আসল টেক্সট ফিরিয়ে আনতে হবে। মূল নীতি: undo করার জন্য যথেষ্ট তথ্য execute()-এর সময়ই সংরক্ষণ করে রাখতে হবে।

  2. পরীক্ষা করুন: Iterator কোড সেলে ট্রি-তে একটি নতুন নোড যোগ করুন (যেমন ৩-এর বাম সাবট্রিতে একটি ০ মান) এবং Run করে নতুন in-order আউটপুট কী হয় যাচাই করুন।

    TreeNode(3, TreeNode(1, TreeNode(0)), TreeNode(4)) — অর্থাৎ ১-এর বাম দিকে ০ যোগ করলে, নতুন ইন-অর্ডার আউটপুট হবে [0, 1, 3, 4, 5, 8, 9] — ০ সবার প্রথমে আসবে কারণ এটি সবচেয়ে বাম দিকের নোড, আর _InOrderIterator-এর কোনো কোড না বদলিয়েই এটি স্বয়ংক্রিয়ভাবে সঠিক জায়গায় বসেছে।

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

  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ পরবর্তী পাঠ — সফটওয়্যার আর্কিটেকচার মডিউল, আর্কিটেকচারাল স্টাইল লেয়ার্ড ও ক্লায়েন্ট-সার্ভার — শীঘ্রই যুক্ত হবে।
  • Data Structures & Algorithms কোর্স সহোদর কোর্স উপরের CommandHistory-এর স্ট্যাক ও BinaryTreeCollection-এর ট্রি — দুটোরই গভীর, ঘনঘন ব্যবহৃত ডেটা স্ট্রাকচার এই কোর্সে বিস্তারিত কভার করা হয়েছে।
  • সব 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 — সব এক জায়গায়।
আগের পাঠ
বিহেভিয়ারাল প্যাটার্ন — Observer ও Strategy