MVVM ও MVP প্যাটার্ন
এই পাঠে যা শিখবেন
- MVVM-এর ViewModel কীভাবে বাইন্ডেবল স্টেট এক্সপোজ করে এবং View তা কীভাবে অবজার্ভ করে
- MVP-এর Presenter কীভাবে সরাসরি একটি View ইন্টারফেস ম্যানিপুলেট করে
- MVC, MVVM, MVP — তিনটির মধ্যে মূল পার্থক্য কোথায় (Controller/ViewModel/Presenter-এর ভূমিকা)
- Python দিয়ে একটি সত্যিকারের কার্যকর
Observableপ্রপার্টি ও একটিProtocol-ভিত্তিক View ইন্টারফেস
১ · MVC থেকে MVVM ও MVP কেন আলাদা
L05-এ আমরা দেখেছি MVC-তে Controller রিকোয়েস্ট গ্রহণ করে Model থেকে ডেটা এনে View-কে দিয়ে রেন্ডার করায় — প্রতিটি রিকোয়েস্টে Controller-ই সিদ্ধান্ত নেয় View কী দেখাবে। MVVM ও MVP একই "ডেটা ও প্রেজেন্টেশন আলাদা রাখা" লক্ষ্য অর্জন করে, কিন্তু মাঝের অংশ ও View-এর সাথে তার সম্পর্ক ভিন্নভাবে সংগঠিত করে — বিশেষত যেখানে View-এর অবস্থা বারবার, স্বয়ংক্রিয়ভাবে বদলাতে হয় (যেমন একটি ডেস্কটপ বা মোবাইল UI)।
Controller রিকোয়েস্ট প্রতি একবার Model → View সমন্বয় করে, রেসপন্স ফেরত দেয়। View সাধারণত "প্যাসিভ" — যা দেওয়া হয় তাই রেন্ডার করে।
ViewModel বাইন্ডেবল স্টেট এক্সপোজ করে; View সেই স্টেট অবজার্ভ করে ও স্টেট বদলালেই নিজে থেকে re-render হয় — ViewModel View-এর অস্তিত্ব সম্পর্কে জানে না।
Presenter একটি View ইন্টারফেস সরাসরি ধরে রাখে ও তার মেথড সরাসরি কল করে ("এখন এটা দেখাও") — View সম্পূর্ণ প্যাসিভ, নিজে কিছু সিদ্ধান্ত নেয় না।
২ · MVVM — বাইন্ডেবল স্টেট ও অবজার্ভিং View
নিচের কোড সেলে একটি Observable ক্লাস তৈরি করা হয়েছে যা একটি মান ধারণ করে ও মান বদলালে সব
সাবস্ক্রাইব করা লিসেনারকে জানায়। CounterViewModel একটি বাইন্ডেবল count প্রপার্টি
এক্সপোজ করে, আর CounterView সেই প্রপার্টি "অবজার্ভ" করে — count বদলালেই স্বয়ংক্রিয়ভাবে
নিজেকে re-render করে, কোথাও সরাসরি view.render() কল না করেই।
# ---------- একটি বাইন্ডেবল (observable) প্রপার্টি ----------
class Observable:
def __init__(self, initial_value):
self._value = initial_value
self._listeners = []
def get(self):
return self._value
def set(self, new_value):
old_value = self._value
self._value = new_value
if old_value != new_value:
for listener in self._listeners:
listener(new_value)
def subscribe(self, listener):
self._listeners.append(listener)
# ---------- VIEWMODEL -- বাইন্ডেবল স্টেট এক্সপোজ করে, View সম্পর্কে কিছুই জানে না ----------
class CounterViewModel:
def __init__(self):
self.count = Observable(0)
def increment(self):
self.count.set(self.count.get() + 1)
def reset(self):
self.count.set(0)
# ---------- VIEW -- ViewModel-এর স্টেট অবজার্ভ করে, স্টেট বদলালেই re-render হয় ----------
class CounterView:
def __init__(self, view_model):
self.view_model = view_model
self.render_count = 0
self.view_model.count.subscribe(self._on_count_changed) # অবজার্ভ করা শুরু
self._render(self.view_model.count.get()) # প্রাথমিক রেন্ডার
def _on_count_changed(self, new_count):
self._render(new_count)
def _render(self, count_value):
self.render_count += 1
print(f"[View re-render #{self.render_count}] বর্তমান কাউন্ট: {count_value}")
print("== MVVM ডেমো ==")
vm = CounterViewModel()
view = CounterView(vm) # প্রাথমিক রেন্ডার -- re-render #1
vm.increment() # count: 0 -> 1, re-render #2
vm.increment() # count: 1 -> 2, re-render #3
vm.reset() # count: 2 -> 0, re-render #4
vm.reset() # count: 0 -> 0, মান অপরিবর্তিত -- re-render হয় না
print(f"\nমোট re-render হয়েছে: {view.render_count} বার")
CounterViewModel-এর ভেতরে কোথাও CounterView-এর কোনো উল্লেখ নেই —
ViewModel শুধু Observable স্টেট এক্সপোজ করে, কে সেটা অবজার্ভ করছে তা নিয়ে মাথা ঘামায় না। শেষ
reset() কলে মান 0-ই থেকে যায় (আগে থেকেই 0), তাই
Observable.set()-এর old_value != new_value চেক ব্যর্থ হয় ও কোনো নোটিফিকেশন যায়
না — সেজন্যই মোট re-render সংখ্যা ৫ নয়, ৪।
৩ · MVP — Presenter সরাসরি View ইন্টারফেস ম্যানিপুলেট করে
MVVM-এর বিপরীতে MVP-তে View কোনো স্টেট "অবজার্ভ" করে না — বরং Presenter একটি View
ইন্টারফেসের রেফারেন্স সরাসরি ধরে রাখে এবং তার মেথড সরাসরি কল করে "কী দেখাতে হবে" নির্দেশ দেয়। নিচের কোডে
TodoViewInterface একটি Protocol (একটি চুক্তি — কোন মেথড থাকতে হবে তার সংজ্ঞা),
ConsoleTodoView তার একটি concrete বাস্তবায়ন, ও TodoPresenter সেই ইন্টারফেস ধরে
সরাসরি মেথড কল করে।
from typing import Protocol
# ---------- View ইন্টারফেস -- Presenter এই "চুক্তি"-র বিরুদ্ধে কোড লেখে ----------
class TodoViewInterface(Protocol):
def show_todos(self, todos: list) -> None: ...
def show_error(self, message: str) -> None: ...
# ---------- VIEW -- ইন্টারফেসের একটি concrete বাস্তবায়ন ----------
class ConsoleTodoView:
def __init__(self):
self.last_shown = None
def show_todos(self, todos):
self.last_shown = todos
if not todos:
print("View আউটপুট: (কোনো টুডু নেই)")
else:
for t in todos:
print(f"View আউটপুট: - {t}")
def show_error(self, message):
self.last_shown = None
print(f"View আউটপুট (এরর): {message}")
# ---------- PRESENTER -- View-এর রেফারেন্স সরাসরি ধরে রাখে ও সরাসরি মেথড কল করে ----------
class TodoPresenter:
def __init__(self, view: TodoViewInterface):
self.view = view
self._todos = []
def load_todos(self, raw_titles):
if not raw_titles:
self.view.show_error("কোনো টুডু পাওয়া যায়নি") # Presenter সরাসরি View-কে নির্দেশ দেয়
return
self._todos = list(raw_titles)
self.view.show_todos(self._todos) # Presenter সরাসরি View-কে নির্দেশ দেয়
print("== MVP ডেমো ==")
console_view = ConsoleTodoView()
presenter = TodoPresenter(console_view)
presenter.load_todos(["দুধ কেনা", "রিপোর্ট জমা দেওয়া"])
presenter.load_todos([])
print(f"\nView-এ সর্বশেষ যা দেখানো হয়েছে: {console_view.last_shown}")
TodoPresenter.load_todos([]) কল করলে raw_titles খালি লিস্ট হওয়ায়
if not raw_titles সত্য হয়, তাই Presenter সরাসরি self.view.show_error(...) কল করে —
ফলে console_view.last_shown শেষ পর্যন্ত None-ই থাকে (এরর দেখানোর সময় সেট করা
হয়)। MVVM-এর মতো এখানে কোনো "অবজার্ভ" বা স্বয়ংক্রিয় নোটিফিকেশন নেই — Presenter নিজেই সিদ্ধান্ত নিয়ে
সরাসরি View-এর মেথড কল করে।
MVC, MVVM, MVP তিনটিই ডেটা ও প্রেজেন্টেশন আলাদা রাখে, কিন্তু মাঝের স্তরটি View-এর সাথে ভিন্নভাবে সম্পর্কিত হয় — MVC-তে Controller একবারের জন্য Model → View সমন্বয় করে; MVVM-এ ViewModel বাইন্ডেবল স্টেট এক্সপোজ করে যা View নিজে অবজার্ভ করে; MVP-তে Presenter সরাসরি View ইন্টারফেসের মেথড কল করে নির্দেশ দেয়। কোন প্যাটার্ন কোন ফ্রেমওয়ার্কে বেশি দেখা যায় তা মূলত সেই ফ্রেমওয়ার্কের ডেটা-বাইন্ডিং সক্ষমতার উপর নির্ভর করে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১
MVVM ডেমোতে মোট চারবার set() কল করা হলেও কেন re-render হয়েছে মাত্র ৪ বার নয় (initial
render সহ মোট ৫ বার আশা করলে) — আসলে ঠিক কতবার ও কেন?
মোট re-render হয়েছে ৪ বার: প্রাথমিক রেন্ডার (View তৈরির সময়) + increment()-এর দুটো কল +
প্রথম reset()। শেষ reset() কলে মান আগে থেকেই 0 ছিল, তাই
Observable.set()-এর ভেতরের old_value != new_value শর্ত মিথ্যা হয়, কোনো
লিসেনার কল হয় না, এবং re-render সংখ্যা বাড়ে না — এটাই দেখায় Observable শুধু
প্রকৃত পরিবর্তনে নোটিফাই করে, প্রতিটি set() কলে নয়।
প্র ০২
MVP ডেমোতে TodoViewInterface-কে সরাসরি ইনস্ট্যান্স না করে শুধু Protocol
হিসেবে সংজ্ঞায়িত করার লাভ কী?
Protocol শুধু একটি চুক্তি (contract) সংজ্ঞায়িত করে — কোন মেথড থাকতে হবে তা বলে, কিন্তু
বাস্তবায়ন চাপায় না। ফলে TodoPresenter যেকোনো ক্লাসের সাথে কাজ করতে পারে যতক্ষণ সেটি
show_todos ও show_error মেথড রাখে — টেস্টের জন্য একটি ভুয়া/মক View, বা
বাস্তবে একটি GUI-ভিত্তিক View, দুটোই একই Presenter কোড দিয়ে চালানো যায়, Presenter-এর কোনো পরিবর্তন
ছাড়াই।
প্র ০৩ MVC-এর Controller ও MVP-এর Presenter — দুটোই "মাঝের" অংশ যা Model ও View সমন্বয় করে। তাহলে এদের মধ্যে মূল পার্থক্য কী?
MVC-এর Controller সাধারণত একটি রিকোয়েস্ট-প্রতি একবার চলে, ফলাফল View-কে "দিয়ে দেয়" এবং একটি রেসপন্স ফেরত দেয় — View সাধারণত সেই ডেটা প্যাসিভভাবে রেন্ডার করে একবারই। MVP-এর Presenter সাধারণত একটি দীর্ঘস্থায়ী (long-lived) View অবজেক্টের রেফারেন্স ধরে রাখে ও প্রয়োজনে বারবার সরাসরি তার মেথড কল করে (যেমন ইউজার ইন্টারঅ্যাকশনের প্রতিক্রিয়ায়) — এটি একবারের "রেন্ডার ও শেষ" এর বদলে চলমান, ইন্টারঅ্যাক্টিভ UI-এর জন্য বেশি উপযোগী।
অনুশীলন
-
চিন্তা করুন: যদি
CounterViewViewModel-এরcount.subscribe(...)কখনো কল না করত, তাহলেvm.increment()কল করার পর View-তে কী হতো (বা হতো না), এবং কেন?increment()তখনওcount.set(...)কল করত ও ViewModel-এর ভেতরের মান বদলে যেত, কিন্তুObservable._listenersলিস্ট খালি থাকায় কোনো লিসেনার কল হতো না — অর্থাৎ View স্ক্রিনে কিছুই বদলাত না, যদিও ভেতরের ডেটা ঠিকই বদলেছে। এটাই দেখায় MVVM-এ "স্বয়ংক্রিয় re-render" আসলে সাবস্ক্রিপশনের উপর নির্ভরশীল — জাদু নয়, বরং একটি সত্যিকারের রেজিস্টার করা কলব্যাক। -
পরীক্ষা করুন: MVP কোড সেলে
ConsoleTodoView-এর একটি বিকল্পSilentTestViewক্লাস লিখুন যাprintনা করে শুধুself.last_shownসেট করে, তারপর সেটিTodoPresenter-এ পাস করে দেখুন একইPresenterকোড কোনো পরিবর্তন ছাড়াই কাজ করে কি না।class SilentTestView:ক্লাসেshow_todos(self, todos): self.last_shown = todosওshow_error(self, message): self.last_shown = NoneলিখেTodoPresenter(SilentTestView())দিয়ে চালালে একইload_todos(...)কলগুলো ঠিকঠাক কাজ করে, শুধু কনসোলে কিছু প্রিন্ট হয় না — এটাইProtocol-ভিত্তিক View ইন্টারফেসের মূল সুবিধা, Presenter-এর কোড একদম অপরিবর্তিত থাকে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পরবর্তী পাঠ L07 REST আর্কিটেকচারাল প্রিন্সিপল — স্টেটলেসনেস, ইউনিফর্ম ইন্টারফেস ও ক্যাশেবিলিটি দেখুন।
- আগের পাঠ L05 MVC আর্কিটেকচার প্যাটার্ন — এই পাঠের তুলনার ভিত্তি।
-
Python Programming কোর্স সহোদর কোর্স
উপরের কোডে ব্যবহৃত
typing.Protocolও ক্লাসের ভাষাগত ভিত্তি সেই কোর্সে শেখানো হয়েছে। - সব 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, Theory of Computation, Engineering Economics ও Full-Stack Web Frameworks — সব এক জায়গায়।