এই পর্বে যা শিখবেন
- UI-কে logic থেকে আলাদা করার তিনটি কারণ, আর না করলে কী হয়
- MVC, MVP, MVVM আর একমুখী data flow-এ কে কাকে চেনে, তীর কোন দিকে — ছবি আঁকতে পারা
- একটি click বা একটি টাইপ কোন পথে model পর্যন্ত যায় আর screen-এ ফেরে
- Business logic কোথায় রাখবেন, আর প্রতিটি স্তর কীভাবে আলাদাভাবে test করবেন
- React/Vue/Svelte-এর component-কে এই নকশাগুলোর ভাষায় পড়তে পারা
১ · UI আর logic কেন আলাদা — একটি জট পাকানো screen
একটি কাল্পনিক বিশ্ববিদ্যালয়ের ফলাফল পোর্টালের কথা ভাবুন। প্রথম সংস্করণে একজন developer একটি button-এর click handler-এর ভেতরেই সব লিখলেন: text box থেকে roll নম্বর পড়া, সেটি ঠিক আছে কি না দেখা, server থেকে নম্বর আনা, GPA হিসাব করা, ২.০০-এর নিচে হলে “fail” লেখা, আর শেষে একটি HTML টেবিল বানিয়ে screen-এ বসানো। কাজ চলল। ছয় মাস পরে তিনটি অনুরোধ এলো: একটি mobile অ্যাপ, যেখানে একই ফলাফল দেখাবে; GPA-র নিয়মে একটি বদল; আর নিয়মটির automated test. তিনটি কাজই আটকে গেল একই কারণে — নিয়মটি button-এর handler-এর ভেতরে, HTML বানানোর লাইনের ফাঁকে ফাঁকে বসে আছে।
এই পর্বের সব নকশার মূল চিন্তা একটাই: separation of concerns (পর্ব ২৬) — UI-তে প্রয়োগ করা। User interface (UI) মানে ব্যবহারকারী যা দেখেন আর যা চাপেন। এর কোডে দুই ধরনের প্রশ্ন মিশে থাকে: “কী দেখাব আর কোথায়” (রং, টেবিল, button) আর “নিয়মটা কী” (GPA কীভাবে হিসাব হয়, কোন roll বৈধ)। প্রথমটি ঘন ঘন বদলায় — designer-এর ইচ্ছায়, নতুন device-এ। দ্বিতীয়টি বদলায় ব্যবসা বা নিয়মের সিদ্ধান্তে। দুটির বদলের কারণ আলাদা, তাই পর্ব ২৭-এর single responsibility principle-এর ভাষায় এদের আলাদা ঘরে থাকা উচিত।
রেস্তোরাঁর কথা ভাবুন। রান্নাঘরে রেসিপি খাতা আর বাবুর্চি (নিয়ম আর data), সামনে খাবার পরিবেশনের থালা আর সাজানো (view), আর মাঝখানে ওয়েটার, যিনি অর্ডার নিয়ে রান্নাঘরে পৌঁছে দেন (controller). একই রান্না প্লেটে, টিফিন বাক্সে বা home delivery-র প্যাকেটে যেতে পারে — রেসিপি বদলায় না। বাবুর্চি যদি টেবিলে টেবিলে ঘুরে অর্ডারও নেন আর থালাও সাজান, রেসিপির মান পরীক্ষা করতে পুরো রেস্তোরাঁ খুলতে হয়।
আলাদা করার তিনটি লাভ
🧪 Test করা যায়
নিয়ম যদি screen ছাড়া একটি সাধারণ object-এ থাকে, তার unit test মিলিসেকেন্ডে চলে — browser খুলতে হয় না, button চাপতে হয় না।
testability🖥️ অনেক view, এক data
একই ফলাফল web পাতায়, mobile অ্যাপে, PDF-এ আর একটি ছোট badge-এ। নিয়ম একবার লেখা — single source of truth.
multiple views👥 সমান্তরাল কাজ
Designer screen সাজান, developer নিয়ম লেখেন — একই ফাইলে হাত না দিয়ে। Git-এ merge conflict-ও কমে (পর্ব ১৩)।
parallel workএই পর্বে চারটি নকশা দেখবেন — MVC, MVP, MVVM আর একমুখী data flow. চারটিই একই প্রশ্নের ভিন্ন উত্তর: screen আর নিয়মের মাঝে কে বসবে, কে কাকে চিনবে, আর বদলের খবর কোন দিকে যাবে। পুরো system-এর স্তর ভাগ (layered, client-server) পর্ব ৩৮-এর (Software architecture) বিষয়; এখানে আমরা শুধু UI-র অংশটুকুর ভেতরে ঢুকছি। এগুলোকে কখনো “architectural pattern” বলা হয়, কখনো “UI pattern” — নামের এই দ্বিধা নিজেই একটি শিক্ষা, যা অংশ ৩-এ দেখবেন।
২ · Classic MVC — Reenskaug, Smalltalk আর observer
নরওয়ের প্রকৌশলী Trygve Reenskaug ১৯৭৮–৭৯ সালে Xerox PARC-এ অতিথি গবেষক থাকার সময় Smalltalk নিয়ে কাজ করতে গিয়ে ধারণাটি দাঁড় করান। তাঁর নিজের প্রকাশিত নোট অনুযায়ী প্রথম নাম ছিল “Thing-Model-View-Editor”, পরে ১৯৭৯ সালের শেষে “Model-View-Controller”. লক্ষ্য ছিল ব্যবহারকারীর মনের ছবি (mental model) আর কম্পিউটারের data-র মধ্যে সেতু বানানো। পরে Smalltalk-80-এর class library-তে MVC একটি কাঠামো হিসেবে ঢোকে; ১৯৮৮ সালে Glenn Krasner আর Stephen Pope-এর একটি বহুল উদ্ধৃত প্রবন্ধে Smalltalk-80-এর MVC-র বিবরণ ছাপা হয়। খুঁটিনাটি সূত্রভেদে কিছুটা আলাদা — মূল কাঠামোটাই এখানে গুরুত্বপূর্ণ।
MVC-এর তিনটি ভূমিকা:
- Model — data আর তার নিয়ম। বাজারের তালিকায় কোন জিনিস কত পরিমাণ, কোনটি কেনা হয়ে গেছে, খালি নাম গ্রহণযোগ্য কি না। Model কোনো view-কে চেনে না।
- View — model-এর একটি চেহারা। একই model-এর অনেক view হতে পারে: পুরো তালিকা, আর কোণে একটি ছোট badge “২টি বাকি”। View model থেকে পড়ে, কিন্তু model-এ লেখে না।
- Controller — ব্যবহারকারীর input (keyboard, mouse) নিয়ে তাকে model-এর ভাষায় অনুবাদ করে: “row 0-তে click” মানে
model.toggle(0).
মূল কৌশলটি হলো খবর দেওয়ার দিক। Model বদলালে সে view-দের সরাসরি ডাকে না, বরং observer pattern-এর মতো নিবন্ধিত শ্রোতাদের জানায় “আমি বদলেছি”; প্রতিটি view তখন নিজে model থেকে পড়ে আবার আঁকে। Observer-এর পুরো কাঠামো, unsubscribe আর memory leak-এর ফাঁদ পর্ব ৩৫-এ (Behavioral pattern ১) দেখেছেন; এখানে শুধু তার ব্যবহার। Smalltalk-এ এই সম্পর্ককে বলা হতো “dependents” — model-এর ওপর নির্ভরশীলদের তালিকা। ফলে model-এর কোডে কোনো view-এর নাম নেই, আর নতুন view যোগ করতে model খুলতে হয় না।
নিচের stepper-এ বাজারের তালিকায় দুটি জিনিস আছে — চাল (২) আর পেঁয়াজ (১), কোনোটিই কেনা হয়নি। ব্যবহারকারী চালের row-তে click করলেন। দেখুন click কোন পথে যায়।
- ব্যবহারকারী
click row 0input - Controller
onRowClick(0)অনুবাদ করে - Model
toggle(0)বদলায়, খবর দেয় - দুটি view
render()নিজে পড়ে আঁকে
| এখন কাজ করছে | |
| items[0].done | |
| তালিকা দেখাচ্ছে | |
| badge দেখাচ্ছে |
- Click. ব্যবহারকারী চালের row-তে click করলেন। Screen-এ চাল এখনো কেনা হয়নি (
[ ]), badge-এ “2 left”. Classic MVC-তে input-এর ঘটনা যায় controller-এর কাছে, view-এর কাছে নয়। - Controller অনুবাদ করে। “row 0-তে click” কথাটি controller model-এর ভাষায় বদলে দেয়:
model.toggle(0). Controller screen-এ কিছু আঁকে না, আর তালিকার ভেতরের array-তেও হাত দেয় না — শুধু model-এর একটি method ডাকে। - Model বদলায় আর খবর দেয়।
items[0].doneএখনtrue. Model তার শ্রোতাদের বলে “toggle 0”. খেয়াল করুন, এই মুহূর্তে দুটি view-ই পুরোনো ছবি দেখাচ্ছে — খবর এখনো পৌঁছায়নি। - প্রথম view আঁকে। ListView খবর পেয়ে model থেকে নতুন তালিকা পড়ে আঁকে:
0[x]rice. Model তাকে নতুন data ঠেলে দেয়নি; view নিজে পড়েছে — পর্ব ৩৫-এর pull model. - দ্বিতীয় view আঁকে। BadgeView-ও একই খবরে নিজের মতো পড়ে: বাকি ১টি। একটি বদল, দুটি view, কোনো view অন্যটিকে চেনে না — আর model দুজনের কাউকেই নামে চেনে না।
এবার একই নকশা চালানো যায় এমন কোডে। DOM বা browser লাগবে না — প্রতিটি “view” শুধু একটি লাইন ছাপে, যাতে প্রবাহটি পরিষ্কার দেখা যায়। Controller একটি text box থেকে "নাম,পরিমাণ" পায়।
// Classic MVC for a fictional bazaar list. No DOM: each "view" prints text.
class ListModel { // data + rules; knows no view
#items = []; #subs = []; subscribe(fn) { this.#subs.push(fn); }
#notify(change) { for (const fn of this.#subs) fn(change); } // Observer
add(name, qty) {
name = name.trim();
if (!name) throw new Error("empty name"); // business rules live here
if (!(qty >= 1)) throw new Error("qty must be >= 1");
this.#items.push({ name, qty, done: false });
this.#notify(`add ${name}`);
}
toggle(i) { this.#items[i].done = !this.#items[i].done; this.#notify(`toggle ${i}`); }
get items() { return this.#items.map(x => ({ ...x })); } // copies only
get left() { return this.#items.filter(x => !x.done).length; }
}
class ListView { // reads the model, never writes
constructor(m) { m.subscribe(c => console.log(`list <- ${c}: ` +
m.items.map((x, i) => `${i}[${x.done ? "x" : " "}]${x.name} x${x.qty}`).join(", "))); }
}
class BadgeView { // a second view of the same model
constructor(m) { m.subscribe(() => console.log(`badge <- ${m.left} left`)); }
}
class ListController { // user input -> model calls
constructor(model) { this.m = model; }
onSubmit(text) { // "name,qty" from a text box
const [name, qty = "1"] = text.split(",");
try { this.m.add(name, Number(qty)); }
catch (e) { console.log(`ctrl : rejected "${text}" (${e.message})`); }
}
onRowClick(i) { this.m.toggle(i); }
}
const model = new ListModel(); new ListView(model); new BadgeView(model); // two views
const ctrl = new ListController(model);
ctrl.onSubmit("rice,2"); ctrl.onSubmit(" ,3"); ctrl.onSubmit("dal,0"); ctrl.onSubmit("onion");
ctrl.onRowClick(0);আউটপুট:
list <- add rice: 0[ ]rice x2
badge <- 1 left
ctrl : rejected " ,3" (empty name)
ctrl : rejected "dal,0" (qty must be >= 1)
list <- add onion: 0[ ]rice x2, 1[ ]onion x1
badge <- 2 left
list <- toggle 0: 0[x]rice x2, 1[ ]onion x1
badge <- 1 left
প্রতিটি সফল বদলে দুটি লাইন — একটি তালিকার, একটি badge-এর — কারণ দুটি view একই model-এ নিবন্ধিত। দুটি input প্রত্যাখ্যাত: খালি নাম আর শূন্য পরিমাণ। খেয়াল করুন নিয়ম দুটি কোথায় — model-এর add()-এ, controller-এ নয়। কাল একটি mobile controller বা একটি voice command এলে নিয়ম নতুন করে লিখতে হবে না, আর model-কে কোনো view ছাড়াই test করা যায়। Controller শুধু text ভেঙে সংখ্যা বানায় আর error-কে বার্তায় বদলায় — input-এর অনুবাদ, নিয়ম নয়। items getter কপি ফেরত দেয়, যাতে কোনো view ভুল করেও model-এর ভেতরের array বদলাতে না পারে।
Classic MVC-তে view আর controller প্রায় জোড়ায় জোড়ায় থাকত — প্রতিটি view-এর নিজের controller. আধুনিক GUI toolkit-এ (button, text box-এর মতো widget) input ধরার কাজ widget নিজেই করে, তাই আলাদা controller-এর প্রয়োজন কমে গেছে। এখান থেকেই পরের নকশাগুলোর জন্ম।
৩ · MVC-র অনেক অর্থ — web framework-এর MVC
“MVC” সম্ভবত software-এ সবচেয়ে বেশি অর্থে ব্যবহৃত শব্দগুলোর একটি। Martin Fowler তাঁর “GUI Architectures” প্রবন্ধে (২০০৬) প্রায় এরকম মন্তব্য করেছেন যে MVC-কে সবচেয়ে বেশি উদ্ধৃত করা হয়, আর সবচেয়ে বেশি ভুলভাবে। দুটি বড় অর্থ পাশাপাশি রাখুন।
১৯৯০-এর দশকের শেষ থেকে web application-এর জগতে “MVC” নামটি একটি ভিন্ন কাঠামোর জন্য ব্যবহৃত হতে থাকে — Java-র JSP “Model 2” নকশা, পরে Struts, আর ২০০৪ সালে প্রকাশিত Ruby on Rails এটিকে ব্যাপক জনপ্রিয় করে। এই web MVC-তে:
- Controller একটি HTTP request গ্রহণ করে — যেমন
GET /results/240101. Router ঠিক করে কোন controller-এর কোন method (action) চলবে। - Model সাধারণত database-এর একটি টেবিলের সাথে যুক্ত class (Rails-এ ActiveRecord), সাথে validation আর নিয়ম।
- View একটি template — HTML-এর ফাঁকে ফাঁকে data বসানোর জায়গা। Controller data জোগাড় করে template-কে দেয়, template একটি পূর্ণ HTML পাতা বানায়, পাতা browser-এ যায়।
বড় পার্থক্যটি হলো: এখানে model view-কে খবর দেয় না। HTTP-র প্রতিটি request আলাদা — পাতা একবার বানিয়ে পাঠানো হয়ে গেলে server আর সেই পাতার সাথে যুক্ত থাকে না। Observer-এর সম্পর্ক নেই; প্রবাহটি request → controller → model → view → response, প্রতিবার নতুন করে। Python-এর Django framework (২০০৫) নিজের নকশাকে বলে MTV — Model, Template, View. Django-র ভাষায় “view” হলো যে function request সামলায় (অর্থাৎ অন্যদের controller), আর “template” হলো যা অন্যরা view বলে; Django-র নিজের FAQ-এ বলা আছে, framework নিজেই অনেকটা controller-এর কাজ করে। নাম আলাদা, ভূমিকা প্রায় এক।
| প্রশ্ন | Classic MVC (Smalltalk, desktop) | Web MVC (Rails, Django, Laravel, ASP.NET MVC ধাঁচ) |
|---|---|---|
| Controller কী পায় | keyboard আর mouse-এর ঘটনা | একটি HTTP request (URL, form data) |
| View কী | চলমান একটি screen-এর অংশ, বারবার নিজেকে আঁকে | একটি template, প্রতি request-এ একবার HTML বানায় |
| Model বদলালে view জানে কীভাবে | Observer: model খবর দেয় | জানে না; পরের request-এ নতুন পাতা |
| কতক্ষণ বাঁচে | অ্যাপ খোলা থাকা পর্যন্ত | একটি request-এর সময়টুকু |
| Test-এর সহজ জায়গা | model সরাসরি | model সরাসরি; controller-কে নকল request দিয়ে |
তাহলে কোনটি “আসল” MVC? প্রশ্নটি ভাবনার প্রশ্ন ১-এ বিস্তারিত আছে। কাজের কথা হলো, কেউ “আমাদের অ্যাপ MVC” বললে জিজ্ঞেস করুন — কোন MVC? Server-এ পাতা বানানো, না browser-এ চলা interactive UI? দুটির test, bug আর সীমাবদ্ধতা আলাদা। Web-এ request কীভাবে route হয় আর controller কেমন দেখায়, তা হাতেকলমে দেখতে পারেন JavaScript কোর্সের Express দিয়ে REST API পর্বে; URL আর resource-এর নকশা System Design কোর্সের REST API design পর্বে।
Web MVC-র পরিচিত বিপদ হলো মোটা controller: action-এর ভেতরেই database query, ছাড়ের হিসাব, email পাঠানো, আর template-এর জন্য data সাজানো। কয়েক মাস পরে একই নিয়ম আরেকটি controller-এ (বা একটি mobile API-তে) কপি হয়। Rails-সমাজে ২০০০-এর দশকের মাঝামাঝি থেকে “skinny controller, fat model” কথাটি এর প্রতিক্রিয়া হিসেবে জনপ্রিয় হয়; পরে দেখা গেল model-ও অতিরিক্ত মোটা হতে পারে। এই টানাপোড়েন অংশ ৮-এ।
৪ · MVP — presenter, passive view আর UI ছাড়া test
Desktop-এর widget যখন নিজেই input ধরতে শুরু করল, তখন প্রশ্ন দাঁড়াল: “ব্যবহারকারী এই button চাপলে কী হবে” — এই সিদ্ধান্তের কোড কোথায় থাকবে? MVP (Model-View-Presenter)-এর উত্তর: একটি presenter-এ। সূত্রমতে নকশাটি ১৯৯০-এর দশকের মাঝামাঝি Taligent-এ (Apple, IBM আর HP-র একটি যৌথ প্রতিষ্ঠান) বর্ণিত হয় — Mike Potel-এর ১৯৯৬ সালের একটি প্রবন্ধে — আর প্রায় একই সময়ে Dolphin Smalltalk-এর নির্মাতারা এটিকে গ্রহণ করেন। সাল আর খুঁটিনাটি সূত্রভেদে কিছুটা আলাদা।
MVP-তে view ঘটনাটি ধরে সঙ্গে সঙ্গে presenter-কে জানায়: “search চাপা হয়েছে, roll এই”. Presenter model বা service-কে ডাকে, ফল সাজায়, তারপর view-কে বলে কী দেখাতে হবে — কিন্তু view-এর আসল class নয়, একটি interface-এর মাধ্যমে: showResult(text), showError(msg), setBusy(flag). এই interface-ই MVP-র শক্তি। Presenter জানে না view একটি web পাতা, Android screen না একটি নকল object — তাই test-এ একটি নকল view বসানো যায়।
দুই রূপ: passive view আর supervising controller
Martin Fowler ২০০৬ সালে MVP-কে দুটি আলাদা নকশায় ভাগ করে বর্ণনা করেন:
Passive view
Passive view-এ view একেবারে বোকা: নিজে কোনো সিদ্ধান্ত নেয় না, model চেনে না। প্রতিটি লেখা, প্রতিটি রং presenter বসায়। Test-এ সবচেয়ে বেশি অংশ ঢাকা পড়ে, কিন্তু presenter-এ লেখার কাজ বেশি।
সর্বোচ্চ testabilitySupervising controller
Supervising controller-এ সহজ জিনিস (একটি নাম একটি লেখার ঘরে দেখানো) view নিজেই data binding দিয়ে model থেকে নেয়; presenter শুধু জটিল যুক্তি সামলায়। কোড কম, কিন্তু binding-এর অংশ UI ছাড়া test হয় না।
কম কোডনিচের কোডে ফলাফল পোর্টালের search screen-এর presenter. View হিসেবে বসানো হয়েছে একটি spy — একধরনের test double, যে প্রতিটি ডাক লিখে রাখে — আর server-এর বদলে একটি fake service, যার কাছে দুজন কাল্পনিক ছাত্রের ফল আছে। Test runner-টিও কোডের ভেতরেই লেখা, তাই কোনো library লাগে না। Test double-এর নামকরণ (stub, spy, mock, fake) নিয়ে লেখকদের মধ্যে কিছু মতভেদ আছে; বিস্তারিত পর্ব ৪৯-এ (Test double).
// MVP: test a presenter with a fake view and a fake service. No UI at all.
class ResultPresenter { // fictional result portal
constructor(view, service) { this.view = view; this.service = service; }
onSearch(roll) {
if (!/^\d{6}$/.test(roll)) return this.view.showError("Roll must be 6 digits");
this.view.setBusy(true);
const r = this.service.find(roll);
this.view.setBusy(false);
if (!r) return this.view.showError(`No result for ${roll}`);
this.view.showResult(`${r.name}: GPA ${r.gpa.toFixed(2)}${r.gpa >= 2 ? "" : " (fail)"}`);
}
}
class FakeView { calls = []; // records every call (a spy)
showError(m) { this.calls.push(`error:${m}`); }
showResult(t) { this.calls.push(`result:${t}`); }
setBusy(b) { this.calls.push(`busy:${b}`); }
}
const fakeService = { asked: [], find(roll) { this.asked.push(roll);
return { "240101": { name: "Mitu", gpa: 3.5 }, "240102": { name: "Rafi", gpa: 1.75 } }[roll]; } };
let passed = 0, failed = 0;
function test(name, fn) {
const view = new FakeView(); fakeService.asked = [];
fn(new ResultPresenter(view, fakeService), view);
console.log(`${name.padEnd(24)} -> ${view.calls.join(" | ")}`);
}
function expect(cond, what) { cond ? passed++ : (failed++, console.log(` FAIL: ${what}`)); }
test("bad roll", (p, v) => { p.onSearch("24A1");
expect(v.calls.length === 1 && fakeService.asked.length === 0, "no service call"); });
test("found, formats GPA", (p, v) => { p.onSearch("240101");
expect(v.calls.at(-1) === "result:Mitu: GPA 3.50", "two decimals"); });
test("below 2.00 marked fail", (p, v) => { p.onSearch("240102");
expect(v.calls.at(-1).endsWith("(fail)"), "fail mark"); });
test("not found", (p, v) => { p.onSearch("999999");
expect(v.calls.join() === "busy:true,busy:false,error:No result for 999999", "busy off first"); });
console.log(`${passed} passed, ${failed} failed`);আউটপুট:
bad roll -> error:Roll must be 6 digits
found, formats GPA -> busy:true | busy:false | result:Mitu: GPA 3.50
below 2.00 marked fail -> busy:true | busy:false | result:Rafi: GPA 1.75 (fail)
not found -> busy:true | busy:false | error:No result for 999999
4 passed, 0 failed
চারটি test-ই পাস, আর প্রতিটি লাইন দেখাচ্ছে presenter view-কে ঠিক কী কী বলেছে, কোন ক্রমে। প্রথম test-এ ভুল roll-এ শুধু একটি error, আর fakeService.asked খালি — অর্থাৎ ভুল input-এ server-কে বিরক্ত করা হয়নি। দ্বিতীয় আর তৃতীয়টি GPA-র দুই দশমিক আর “fail” চিহ্ন যাচাই করে। চতুর্থটি একটি সূক্ষ্ম UI bug ধরে: ফল না পেলেও loading চিহ্ন (busy) error দেখানোর আগে বন্ধ হতে হবে, নইলে screen-এ চিরকাল ঘুরতে থাকা চাকা। এই সব যাচাই হলো কোনো browser, কোনো click, কোনো network ছাড়া — কয়েক মিলিসেকেন্ডে। Presenter আর service constructor-এ বাইরে থেকে দেওয়া হয়েছে — পর্ব ৩৭-এর dependency injection; ঠিক এজন্যই নকল বসানো এত সহজ।
এখানে service-টি synchronous রাখা হয়েছে, যাতে কোড ছোট থাকে। বাস্তবে server-এর ডাক asynchronous (await), আর তখন নতুন একটি প্রশ্ন আসে: ব্যবহারকারী দ্রুত দুবার search চাপলে পুরোনো উত্তর পরে এসে নতুনটিকে ঢেকে দিতে পারে কি না। Presenter-এ এমন race-এর নিয়ম রাখলে সেটিও নকল view দিয়ে test করা যায়। Promise আর async/await দেখুন JavaScript কোর্সের async/await পর্বে।
৫ · MVVM — view model, data binding আর two-way binding-এর ফাঁদ
MVP-তে presenter-কে প্রতিটি বদলের পরে view-কে আলাদা করে বলতে হয়: “এখানে এই লেখা, ওই button বন্ধ”. Screen বড় হলে এই “বলার কোড” অনেক হয়ে যায়। MVVM (Model-View-ViewModel) এই কাজটি একটি যন্ত্রের হাতে দেয় — data binding. Microsoft-এর John Gossman ২০০৫ সালে একটি blog লেখায় WPF (Windows-এর তখনকার নতুন UI কাঠামো)-এর জন্য নকশাটি বর্ণনা করেন, পরে Silverlight-এও এটি প্রচলিত হয়; এর আগে Fowler ২০০৪ সালে প্রায় একই চিন্তাকে Presentation Model নামে লিখেছিলেন। পরে JavaScript-এ Knockout.js-এর মতো library এটিকে browser-এ আনে।
View model হলো “screen-এর model” — screen-এ যা দেখাবে তার অবস্থা (state) আর ব্যবহারকারী যা করতে পারেন তার command. বাজার অ্যাপের ক্ষেত্রে: draft (text box-এ যা টাইপ হচ্ছে), items (তালিকা), canAdd (Add button চালু থাকবে কি না) আর add() command — পর্ব ৩৫-এর command pattern-এর একটি হালকা রূপ, সাথে “এখন চালানো যায় কি না” প্রশ্ন। View model কোনো button বা text box চেনে না; সে শুধু observable property রাখে। View declarative ভাবে বলে, “এই text box draft-এর সাথে বাঁধা, এই লেখা items-এর সংখ্যার সাথে”. Binder নামের একটি ছোট যন্ত্র দুই দিক মিলিয়ে রাখে।
- One-way binding: view model বদলালে view বদলায় — যেমন “items(1): potato” লেখাটি।
- Two-way binding: দুই দিকেই — ব্যবহারকারী টাইপ করলে view model বদলায়, আর view model-এ কোড
draftখালি করলে text box-ও খালি হয়।
Two-way binding-এর একটি জন্মগত ফাঁদ আছে। View model বদলালে binder text box-এ লেখে; অনেক UI-তে text box-এ লেখা হলে সে আবার একটি “input” ঘটনা ছোড়ে; binder সেটি দেখে view model-এ লেখে; view model আবার খবর দেয়… দুই আয়না মুখোমুখি রাখলে যেমন অসীম প্রতিবিম্ব। প্রতিরোধের সবচেয়ে সাধারণ উপায় হলো equality guard: নতুন মান আগের মানের সমান হলে কিছুই না করা, খবরও না দেওয়া।
নিচের stepper-এ guard চালু আছে। ব্যবহারকারী text box-এ “potato” লিখে Add চাপেন। writes হলো text box-এ binder কতবার লিখেছে।
| ঘটনা | |
| box.value | |
| vm.draft | |
| canAdd | |
| vm.items | |
| label.text | |
| writes |
- Bind. Two-way binding তৈরির সময় binder view model-এর বর্তমান মান (
"") text box-এ একবার লেখে — writes ১। Label-ও one-way binding-এ “items(0): ” দেখায়। - টাইপ। ব্যবহারকারী লিখলেন “potato”. Text box-এর মান বদলাল, আর সে একটি input ঘটনা ছুড়ল। View model এখনো জানে না —
vm.draftখালি। - View → view model. Binder ঘটনাটি ধরে
vm.draft.set("potato")ডাকে। মান বদলাল, তাই view model তার শ্রোতাদের খবর দেয় — আর শ্রোতাদের মধ্যে আছে binder নিজেই, যে text box-এ “potato” লেখে (writes ২)।canAddএখনtrue, তাই Add button চালু হবে। - প্রতিধ্বনি থামল। Text box-এ লেখা হওয়ায় সে আবার input ছোড়ে, binder আবার
set("potato")ডাকে। কিন্তু মান আগেরটির সমান — guard ফিরে যায়, কোনো খবর নয়, আর কোনো লেখা নয়। চক্র এখানেই শেষ। - Add command.
add()প্রথমেcanAddদেখে, তারপরitems-এ “potato” যোগ করে। Label-এর one-way binding নিজে থেকে “items(1): potato” দেখায় — কেউ label-কে আলাদা করে কিছু বলেনি। - Draft খালি। Command শেষে
draft.set(""); binder text box খালি করে (writes ৩), প্রতিধ্বনি আবার guard-এ থামে।canAddআবারfalse. চূড়ান্ত অবস্থা: label “items(1): potato”, box খালি, writes ৩ — নিচের কোডের প্রথম দুই লাইনের সাথে হুবহু।
// MVVM: an observable view model + a tiny binder + a fake view (no DOM).
function observable(value, guard = true) {
const subs = new Set();
return { get: () => value,
set(v) { if (guard && Object.is(v, value)) return; // the loop guard
value = v; subs.forEach(f => f(v)); },
subscribe(f) { subs.add(f); f(value); } };
}
class FakeBox { // like an input box: it fires "input" on every write
constructor(name) { this.name = name; this.value = ""; this.writes = 0; this.oninput = null; }
write(v) {
if (++this.writes > 5) throw new Error(`${this.name}: binding loop`);
this.value = v; this.oninput?.(v);
}
}
const bindTwoWay = (box, obs) => { obs.subscribe(v => box.write(v)); box.oninput = v => obs.set(v); };
const bindText = (label, obs, fmt) => obs.subscribe(v => label.text = fmt(v)); // one-way
class BazaarVM { // state + commands; never touches a box or label
constructor(guard) { this.draft = observable("", guard); this.items = observable([], guard); }
get canAdd() { return this.draft.get().trim() !== ""; }
add() { if (!this.canAdd) return; // a command with a canExecute check
this.items.set([...this.items.get(), this.draft.get().trim()]); this.draft.set(""); }
}
for (const guard of [true, false]) {
const vm = new BazaarVM(guard), box = new FakeBox("draftBox"), label = {};
try {
bindTwoWay(box, vm.draft);
bindText(label, vm.items, xs => `items(${xs.length}): ${xs.join(", ")}`);
box.value = "potato"; box.oninput("potato"); // the user types
console.log(`guard=${guard}: vm.draft="${vm.draft.get()}" canAdd=${vm.canAdd}`);
vm.add();
console.log(` label="${label.text}" box="${box.value}" writes=${box.writes}`);
} catch (e) { console.log(`guard=${guard}: stopped - ${e.message} (writes=${box.writes})`); }
}আউটপুট:
guard=true: vm.draft="potato" canAdd=true
label="items(1): potato" box="" writes=3
guard=false: stopped - draftBox: binding loop (writes=6)
প্রথম দুই লাইন stepper-এর শেষ অবস্থা। তৃতীয় লাইনটি একই কোডের, শুধু guard বন্ধ রাখা অবস্থায় — guard=false. এবার bind ঠিকঠাক হয় (প্রথম লেখার সময় box-এর oninput তখনো বসানো হয়নি), কিন্তু ব্যবহারকারী টাইপ করতেই চক্র শুরু হয়: binder box-এ "potato" লেখে → box input ছোড়ে → view model একই "potato" পেয়েও খবর দেয় → binder আবার লেখে… নকল box-টি ছয় নম্বর লেখায় নিজেই থামিয়ে দেয়; আসল UI-তে browser আটকে যেত বা stack overflow হতো। Guard-টি মাত্র একটি লাইন — Object.is(v, value) — কিন্তু তার অনুপস্থিতি পুরো screen অচল করে। লক্ষ করুন, BazaarVM-এ কোথাও box বা label-এর নাম নেই; তাই view model-কে binder ছাড়াই সরাসরি test করা যায়: draft.set("x"), তারপর canAdd দেখা।
Two-way binding কখন কষ্ট দেয়
- রূপান্তর দুই দিকে না মিললে: view model-এ
1000, box-এ “1,000”. Box থেকে ফেরার পথে “1,000” যদি1000-এ না ফিরে অন্য কিছুতে (ধরুন"1,000"string) যায়, equality guard আর মেলে না — চক্র বা ভুল মান। নিয়ম: format আর parse পরস্পরের উল্টো হতে হবে। - এক property আরেকটিকে বদলালে:
priceবদলালেtotal,totalবদলালেdiscount,discountআবারprice… guard থাকলেও কয়েক ধাপ ঘুরে থামে কি না অনুমান করা কঠিন। এমন নির্ভরতা একটি জায়গায় হিসাব (computed property) হিসেবে রাখুন, শৃঙ্খলে নয়। - “কে বদলাল?” প্রশ্নের উত্তর মেলে না: একটি মান চারটি জায়গা থেকে বদলাতে পারলে bug-এর উৎস খুঁজতে হয় সবখানে। এই অভিযোগই পরের অংশের নকশার জন্ম দেয়।
৬ · একমুখী data flow — action, reducer আর store
২০১৪ সালে Facebook তাদের web UI-র জন্য Flux নামের একটি নকশা প্রকাশ্যে উপস্থাপন করে। তাদের বক্তব্য অনুযায়ী, দুই দিকে খবর আদান-প্রদানকারী অনেক model আর view-এর জালে একটি পরিচিত bug বারবার ফিরে আসছিল — না-পড়া বার্তার সংখ্যা ভুল দেখানো — কারণ কোন বদল কোথা থেকে এসেছে তা খুঁজে পাওয়া কঠিন ছিল। সমাধান: বদলের একটিমাত্র পথ। ২০১৫ সালে Dan Abramov আর Andrew Clark-এর Redux এই চিন্তাকে আরও সরল করে, Elm ভাষার “Elm architecture” থেকে অনুপ্রেরণা নিয়ে। এই unidirectional data flow-এর চারটি অংশ:
- Action — “কী ঘটেছে” তার একটি সাধারণ data object:
{ type: "toggled", id: 1 }. কোনো কোড নয়, শুধু বিবরণ। - Reducer — একটি pure function:
(পুরোনো state, action) → নতুন state. কোনো network ডাক নেই, কোনো side effect নেই, পুরোনো state বদলায় না, নতুন একটি বানায় — এই গুণের নাম immutability. - Store — বর্তমান state-এর একমাত্র রক্ষক।
dispatch(action)পেলে reducer চালায়, নতুন state রাখে, শ্রোতাদের জানায়। - View — state থেকে screen বানানোর একটি function. ব্যবহারকারী কিছু করলে view নিজে state বদলায় না, শুধু একটি action পাঠায়।
মুদি দোকানের হিসাবের খাতা ভাবুন। দোকানি কখনো আগের পাতার অঙ্ক ঘষে মুছে নতুন লেখেন না; প্রতিটি লেনদেন একটি নতুন লাইন, যেমন “রহিম, ২ কেজি চাল, বাকি” — এটাই action. মাসের শেষের বাকির হিসাব (state) ওই লাইনগুলো প্রথম থেকে যোগ করেই পাওয়া যায় — এটাই reducer-এর কাজ। হিসাবে গরমিল হলে খাতা খুলে লাইন ধরে ধরে মেলানো যায়, আর “১৫ তারিখে বাকি কত ছিল” জানতে প্রথম থেকে ১৫ তারিখ পর্যন্ত যোগ করলেই হয়।
নিচের stepper-এ তালিকায় দুটি জিনিস — rice (id 1) আর egg (id 2), কোনোটিই কেনা হয়নি। ব্যবহারকারী rice-এর row-তে click করলেন।
- View
dispatch(a)action পাঠায় - Store
log.push(a)লিখে রাখে - Reducer
reducer(s, a)নতুন state বানায় - Store → view
subscribeনতুন state, নতুন screen
| action | |
| log.length | |
| বর্তমান state.items | |
| পুরোনো s1 | |
| egg object |
- View একটি action পাঠায়। Click-এর handler state-এ হাত দেয় না; সে শুধু বর্ণনা করে কী ঘটেছে:
{type: "toggled", id: 1}, তারপরstore.dispatch(). Log-এ আগের দুটি “added” আছে। - Store লিখে রাখে। Action log-এ যোগ হলো, দৈর্ঘ্য ৩। এই log-ই পরে replay আর “time travel”-এর কাঁচামাল। Action একটি সাধারণ data, তাই log-টি ফাইলে লেখা বা network-এ পাঠানোও যায়।
- Reducer নতুন state বানায়।
reducer(s1, a)একটি নতুন object ফেরত দেয়:items-এর নতুন array, যেখানে rice-এর একটি নতুন কপিdone: true. Egg বদলায়নি, তাই তার object হুবহু একই, কপি করা হয়নি — একে বলে structural sharing. পুরোনোs1অক্ষত। - Store state বদলায়। Store-এর বর্তমান state এখন
s2. এটিই একমাত্র জায়গা যেখানে “বর্তমান” বদলায় — একটি লাইনে, একটি পথে। কোনো view, কোনো controller এখানে সরাসরি লেখে না। - View নতুন করে আঁকে। Store শ্রোতাদের জানায় — ভেতরে আবার সেই পুরোনো observer. View
s2থেকে পুরো screen বানায়:1[x]rice 2[ ]egg. View-কে “কী বদলাল” খুঁজতে হয় না; সে শুধু “এখন কেমন” থেকে আঁকে।
পুরো প্রোগ্রামটি নিচে। শেষ অংশে log replay করে state আবার বানানো হয়, আর প্রথম তিনটি action পর্যন্ত replay করে “অতীতের” screen দেখা হয়।
// Unidirectional flow: action -> pure reducer -> store -> view. Replay the log.
const initial = { items: [], filter: "all", nextId: 1 };
function reducer(s, a) { // pure: no I/O, never mutates s
switch (a.type) {
case "added": return { ...s, nextId: s.nextId + 1,
items: [...s.items, { id: s.nextId, name: a.name, done: false }] };
case "toggled": return { ...s, items: s.items.map(x => x.id === a.id ? { ...x, done: !x.done } : x) };
case "removed": return { ...s, items: s.items.filter(x => x.id !== a.id) };
case "filterSet": return { ...s, filter: a.filter };
default: return s; // unknown action: same object back
}
}
function createStore(reduce, state) {
const log = [], subs = [];
return { log, getState: () => state, subscribe: f => subs.push(f),
dispatch(a) { log.push(a); state = reduce(state, a); subs.forEach(f => f(state)); } };
}
const view = s => s.items.filter(x => s.filter === "all" || (s.filter === "left") === !x.done)
.map(x => `${x.id}${x.done ? "[x]" : "[ ]"}${x.name}`).join(" ") || "(empty)";
const store = createStore(reducer, initial);
store.subscribe(s => console.log(`${store.log.at(-1).type.padEnd(11)} -> ${view(s)} (filter=${s.filter})`));
const before = store.getState();
store.dispatch({ type: "added", name: "rice" });
store.dispatch({ type: "added", name: "egg" });
store.dispatch({ type: "toggled", id: 1 });
store.dispatch({ type: "filterSet", filter: "left" });
store.dispatch({ type: "added", name: "oil" });
store.dispatch({ type: "removed", id: 2 });
store.dispatch({ type: "typo-action" });
console.log("old state untouched:", before.items.length === 0 && before !== store.getState());
const rebuilt = store.log.reduce(reducer, initial); // replay every action
console.log("replay equals live:", JSON.stringify(rebuilt) === JSON.stringify(store.getState()));
const at3 = store.log.slice(0, 3).reduce(reducer, initial); // "time travel"
console.log("after 3 actions:", view(at3));আউটপুট:
added -> 1[ ]rice (filter=all)
added -> 1[ ]rice 2[ ]egg (filter=all)
toggled -> 1[x]rice 2[ ]egg (filter=all)
filterSet -> 2[ ]egg (filter=left)
added -> 2[ ]egg 3[ ]oil (filter=left)
removed -> 3[ ]oil (filter=left)
typo-action -> 3[ ]oil (filter=left)
old state untouched: true
replay equals live: true
after 3 actions: 1[x]rice 2[ ]egg
প্রথম তিন লাইনের দ্বিতীয় আর তৃতীয়টি stepper-এর সাথে মেলে। filterSet-এর পরে view শুধু বাকি জিনিস দেখায়; rice কেনা হয়ে গেছে, তাই লুকানো — কিন্তু state থেকে মুছে যায়নি। একটি ভুল নামের action (typo-action) কোনো error ছাড়াই উপেক্ষিত হলো: reducer-এর default একই state ফেরত দেয়। এটি সুবিধা (অজানা action-এ ভাঙে না), আবার ঝুঁকিও (বানান ভুল চুপচাপ হারায়) — TypeScript-এর মতো type ব্যবস্থা বা action-এর নাম একটি জায়গায় constant হিসেবে রাখা এর প্রতিকার। শেষ তিন লাইন একমুখী প্রবাহের পুরস্কার: শুরুর state অক্ষত; log.reduce(reducer, initial) ঠিক বর্তমান state-টিই ফেরত দেয়; আর প্রথম তিনটি action replay করলে তিন ধাপ আগের screen. Redux-এর developer tool-এ এই time-travel debugging একটি পরিচিত সুবিধা। খেয়াল করুন nextId-ও state-এর ভেতরে — id যদি reducer-এর বাইরে Date.now() বা random থেকে আসত, replay করলে অন্য id পাওয়া যেত, আর reducer pure থাকত না।
Command আর observer-এর সাথে সম্পর্ক
একমুখী data flow নতুন কোনো জাদু নয়; পুরোনো দুটি pattern-এর একটি শৃঙ্খলাবদ্ধ জোড়া। Action হলো পর্ব ৩৫-এর command-এর আত্মীয় — একটি অনুরোধকে data বানানো, যাতে তা লিখে রাখা, queue-তে রাখা আর replay করা যায়। পার্থক্য: command-এর ভেতরে নিজের execute() থাকে, action শুধু বর্ণনা — কাজটি করে reducer. আর store থেকে view-এ খবর যাওয়া হলো observer. নতুন যা, তা হলো নিয়ম: state বদলের একমাত্র দরজা dispatch, আর বদলের যুক্তি একটি pure function. Server-এর দিকে একই চিন্তা — ঘটনার log থেকে অবস্থা বানানো — পাবেন System Design কোর্সের event sourcing আর CQRS পর্বে; JavaScript-এ pure function আর reduce-এর ভিত্তি functional programming পর্বে।
দাম আছে। ছোট একটি form-এর জন্য action, reducer, store আর তাদের নামের constant — চারটি ফাইল ছুঁতে হয়। Redux-এর নিজের লেখকেরাও প্রকাশ্যে বলেছেন, সব অ্যাপে এর দরকার নেই। পর্ব ২৬-এর YAGNI মনে রাখুন: যে state শুধু একটি component-এর (একটি dropdown খোলা কি না), সেটি global store-এ তোলার দরকার নেই।
৭ · চারটি নকশা পাশাপাশি, আর component-ভিত্তিক UI
নিচের ছবিতে চারটি নকশার তীর পাশাপাশি। প্রতিটি তীর মানে “এ ওকে ডাকে বা ওকে চেনে”; ড্যাশ তীর মানে “নিবন্ধিত শ্রোতাকে খবর দেয়” — যে খবর দেয়, সে শ্রোতার আসল class চেনে না।
MVC, MVP, MVVM আর একমুখী flow — কে কাকে চেনে
Solid arrow = calls / knows; dashed arrow = notifies a registered listener
| প্রশ্ন | MVC (classic) | MVP | MVVM | একমুখী |
|---|---|---|---|---|
| ব্যবহারকারীর input প্রথমে যায় | controller | view → presenter | view → binding → view model | view → action |
| Screen হালনাগাদ করে কে | view নিজে, model পড়ে | presenter, interface দিয়ে | binder, নিজে থেকে | view, পুরো state থেকে |
| মাঝের অংশ view চেনে? | controller প্রায়ই চেনে | হ্যাঁ, interface-টুকু | না | না (reducer screen জানে না) |
| UI ছাড়া test হয় | model | presenter, নকল view দিয়ে | view model, সরাসরি | reducer, সবচেয়ে সহজে |
| পরিচিত দুর্বলতা | মোটা controller | presenter-এ অনেক “বলার কোড” | binding-এর চক্র, লুকানো প্রবাহ | ছোট কাজে অনেক আনুষ্ঠানিকতা |
Component-ভিত্তিক UI — প্রতিটি টুকরো নিজেই view আর view model
React (২০১৩ সালে open source), Vue আর Svelte-এর মতো আজকের জনপ্রিয় UI library-গুলো screen-কে ভাবে component-এর গাছ হিসেবে: একটি ResultCard, তার ভেতরে GpaBadge, পাশে SearchBox. প্রতিটি component একটি ছোট ইউনিট, যার ভেতরে থাকে নিজের state (“এই card খোলা কি না”), সেই state বদলানোর handler, আর state থেকে চেহারা বানানোর template বা JSX. MVVM-এর ভাষায় পড়লে, প্রতিটি component নিজেই একটি ছোট “view + view model” — শুধু একই ফাইলে। Component-গুলো একে অপরের ভেতরে বসে, যেমন পর্ব ৩৪-এর composite pattern-এ অংশ আর সমগ্র।
- React: data নিচে নামে props হিসেবে, ঘটনা ওপরে ওঠে callback হিসেবে — প্রতিটি component-এর ভেতরেই ছোট একমুখী flow. State বদলালে component আবার “আঁকা” হয়, আর library পার্থক্যটুকু DOM-এ বসায়। প্রথম দিকে React নিজেকে “MVC-র V” হিসেবে পরিচয় দিত।
- Vue আর Svelte: reactive state — একটি variable বদলালে যেসব জায়গা তাকে পড়ে, শুধু সেগুলো হালনাগাদ হয়; form-এর জন্য two-way binding-এর সুবিধাজনক রূপও আছে, যেমন
v-modelআরbind:value. MVVM-এর binding-এর আধুনিক বংশধর। - Global state: অনেক component-এর ভাগের state (লগইন করা ব্যবহারকারী, বাজারের ঝুড়ি) প্রায়ই একটি store-এ যায় — Redux, বা প্রতিটি framework-এর নিজস্ব store ব্যবস্থা। দুই ভাইবোন component কথা বলতে না পারলে একটি সাধারণ অভিভাবক বা store মাঝখানে বসে — পর্ব ৩৬-এর mediator pattern-এর চিন্তা।
Framework-গুলোর বিস্তারিত তুলনা দেখুন JavaScript কোর্সের framework-এর পরিচয় পর্বে, আর browser-এর DOM, যার ওপর সবাই দাঁড়িয়ে, DOM-এর মৌলিক পর্বে। Framework ছাড়া browser-এর নিজস্ব component ব্যবস্থা — custom element আর shadow DOM — HTML & CSS কোর্সের web component পর্বে।
Component-এর যুগেও এই পর্বের প্রশ্নগুলো হারায়নি, শুধু ছোট আকারে ফিরে এসেছে। একটি component-এর ভেতরে যদি fetch, GPA-র নিয়ম, টাকার হিসাব আর ৩০০ লাইনের JSX একসাথে থাকে, সেটি একটি নতুন পোশাকে পুরোনো “massive view controller”. সমাধানও পুরোনো: নিয়ম আলাদা function বা module-এ (model), screen-এর state আর command একটি hook বা store-এ (view model), আর component-এ শুধু চেহারা।
৮ · Business logic কোথায়, প্রতিটি স্তরের test আর তিনটি anti-pattern
Business logic (বা domain logic) মানে সেই নিয়ম, যা UI বদলালেও বদলায় না: GPA ২.০০-এর নিচে হলে fail, ৫০০ টাকার বেশি কেনাকাটায় delivery ফ্রি, একই roll-এ দুবার ভর্তি নয়। এই নিয়মের জায়গা view-এ নয়, controller বা presenter-এও নয়। কারণ সোজা: view আর controller সংখ্যায় অনেক (web, mobile, admin panel, API) আর ঘন ঘন বদলায়; নিয়ম একটি, আর তার ভুলের দাম বেশি।
Fat model, নাকি service layer?
- Fat model: নিয়ম যে data-র, সেই model class-এর method-এ —
result.isFail(),cart.deliveryFee(). ছোট আর মাঝারি অ্যাপে সরল আর কার্যকর। বিপদ: একটি model একসময় শত শত method-এর একটি god class হয়ে যায়। - Service layer: যে কাজ একাধিক model ছোঁয়, বা বাইরের ব্যবস্থাকে ডাকে (ভর্তি নিশ্চিত করা = আসন কমানো + ফি নেওয়া + SMS) — তা একটি service-এ, যেমন
EnrollmentService.enroll(). Controller শুধু service ডাকে। Fowler-এর Patterns of Enterprise Application Architecture (২০০২) এই নামটি ছড়িয়ে দেয়। - সাবধান — anemic domain model: সব নিয়ম service-এ ঠেলে model-কে শুধু getter-setter-এর থলে বানালে Fowler একে বলেন anemic (রক্তশূন্য) — object আছে, কিন্তু object-oriented নকশার সুবিধা নেই।
বাস্তবের ভারসাম্য: যে নিয়ম একটি object-এর নিজের data নিয়ে, তা model-এ; যে কাজ অনেক object আর বাইরের জগৎ মিলিয়ে, তা service-এ; আর UI-র দিকের কোডে শুধু অনুবাদ আর চেহারা। নিয়মকে database, framework আর UI থেকে পুরোপুরি স্বাধীন রাখার আরও কঠোর নকশা — port, adapter আর domain-কে কেন্দ্রে রাখা — দেখবেন পর্ব ৪০-এ: Clean আর hexagonal architecture.
প্রতিটি স্তর কীভাবে test করবেন
| স্তর | কী দিয়ে test | কী যাচাই | গতি |
|---|---|---|---|
| Model / domain, reducer | সরাসরি function ডাক | নিয়ম আর হিসাব: ২.০০-এর সীমা, ফ্রি delivery-র সীমা, অজানা action | খুব দ্রুত |
| Service | Fake repository, stub SMS | কাজের ক্রম, ব্যর্থতায় কী হয় | দ্রুত |
| Presenter / view model | Spy বা নকল view; property আর command সরাসরি | কী দেখাবে, কখন button বন্ধ, loading চিহ্ন | দ্রুত |
| View / template | Component test, snapshot (সাবধানে) | ঠিক জায়গায় ঠিক লেখা, accessibility | মাঝারি |
| পুরো ব্যবস্থা | End-to-end, আসল browser | কয়েকটি প্রধান যাত্রা — লগইন, search, ফল | ধীর, মাঝে মাঝে flaky |
নিয়ম যত নিচের স্তরে, test তত সস্তা। এই ভাগই পর্ব ৫১-এর (Test pyramid আর integration test) test pyramid-এর মূল যুক্তি; unit test-এর গঠন (Arrange-Act-Assert) পর্ব ৪৬-এ। JavaScript-এ test লেখার হাতেকলমে পরিচয় পাবেন JavaScript testing পর্বে।
তিনটি পরিচিত anti-pattern
Massive view controller
iOS-এর UIKit-এ প্রতিটি screen-এর একটি view controller থাকে, আর সব কোড সেখানেই জমা হওয়ার প্রবণতা এত পরিচিত যে developer-সমাজে MVC-র ঠাট্টার নাম হয়ে গেছে “Massive View Controller”. Network, নিয়ম, format, navigation — হাজার লাইনের একটি class. প্রতিকার: নিয়ম model/service-এ, screen-এর state view model বা presenter-এ।
Template-এ logic
{% if gpa < 2.0 and not retake and year > 2 %} — template-এর ভেতরে নিয়ম। Test করা কঠিন, আরেকটি template-এ কপি হয়, আর designer HTML বদলাতে গিয়ে নিয়ম ভাঙেন। প্রতিকার: model-এ result.status হিসাব, template-এ শুধু {{ result.status }} দেখানো।
Binding-এর চক্র
Guard ছাড়া two-way binding, বা একে অপরকে বদলানো property-র শৃঙ্খল — অংশ ৫-এর মতো অসীম loop, অথবা চুপচাপ ধীর screen. AngularJS (২০১০)-এর digest cycle এই সমস্যায় পড়লে কয়েকবার ঘুরে হাল ছেড়ে দিয়ে একটি পরিচিত error দিত। প্রতিকার: guard, computed property, অথবা একমুখী flow.
এই anti-pattern-গুলো চেনা আর ছোট নিরাপদ ধাপে সারানো পর্ব ৪২-এর (Code smell আর refactoring) বিষয়; পুরোনো massive controller-কে test ছাড়াই ভাঙতে যাওয়ার ঝুঁকি নিয়ে আলোচনা পর্ব ৪৩-এ: Technical debt আর legacy code. Code review-তে (পর্ব ৪৪, Code review) একটি সহজ প্রশ্ন অনেক কিছু ধরে: “এই if-টি কি screen-এর সিদ্ধান্ত, নাকি ব্যবসার নিয়ম?”
আরও গভীরে: Reenskaug নিজে পরে লিখেছেন, MVC-র আসল লক্ষ্য ছিল ব্যবহারকারীর মনের ছবিকে কোডে প্রতিফলিত করা — শুধু ফাইল ভাগ নয়; পরে তিনি James Coplien-এর সাথে DCI (Data, Context, Interaction) নামে একটি নকশা প্রস্তাব করেন। Fowler-এর “GUI Architectures” (২০০৬) প্রবন্ধটি MVC, MVP আর Presentation Model-এর ইতিহাস ও পার্থক্যের সবচেয়ে পরিচিত বিবরণগুলোর একটি। Redux-এর নিজস্ব documentation-এ তিনটি নীতি লেখা আছে: single source of truth, state শুধু পড়া যায়, আর বদল হয় pure function দিয়ে।
পরীক্ষা ও ইন্টারভিউতে:
- MVC আর MVVM-এর মূল পার্থক্য? — MVVM-এ view model view-কে চেনে না, data binding screen মেলায়; MVC-তে controller input নিয়ে model ডাকে আর view নিজে model পড়ে।
- MVP-তে presenter কেন সহজে test হয়? — View-কে সে একটি interface দিয়ে চেনে, তাই test-এ নকল view বসানো যায়।
- Reducer কেন pure হতে হবে? — একই action-log replay করলে একই state পেতে, আর পুরোনো আর নতুন state-এর তুলনা দিয়ে বদল ধরতে।
- Business logic controller-এ রাখলে সমস্যা? — অনেক controller-এ কপি হয়, UI ছাড়া test হয় না, আর massive controller তৈরি হয়।
- Two-way binding-এর ঝুঁকি? — Update-এর চক্র আর “কে বদলাল” খুঁজে না পাওয়া; প্রতিকার equality guard বা একমুখী flow.
৯ · AI-এর যুগে — AI-এর লেখা UI architecture যাচাই
AI-কে “এই screen-টা MVVM-এ লিখে দাও” বললে প্রায় সবসময় পরিচ্ছন্ন দেখতে কয়েকটি ফাইল আসবে — ResultViewModel, ResultView, ResultService. নাম দেখে নকশা ঠিক ধরে নেওয়া সবচেয়ে বড় ভুল। নামগুলো সঠিক রেখেও ভেতরে view model button-এর রং বদলাতে পারে, বা GPA-র নিয়ম view-এর template-এ বসে থাকতে পারে।
| AI-এর ভুল | কীভাবে ধরবেন |
|---|---|
View model-এর ভেতরে UI-র class (DOM element, Android TextView, document.querySelector) | View model ফাইলের import আর global ব্যবহার খুঁজুন; UI library-র কিছু থাকলেই সন্দেহ |
| নিয়ম template বা component-এ; একই if তিনটি screen-এ কপি | নিয়মের সংখ্যা (২.০০, ৫০০ টাকা) দিয়ে পুরো project-এ search করুন; একটির বেশি জায়গায় পেলে সরান |
Reducer-এর ভেতরে state.items.push(), Date.now() বা fetch | Test-এ পুরোনো state Object.freeze করে reducer চালান; একই log দুবার replay করে ফল মেলান |
| Guard ছাড়া two-way binding, বা একে অপরকে বদলানো property | প্রতিটি set-এ গণনা রাখুন (অংশ ৫-এর writes-এর মতো); একটি টাইপে কতবার লেখা হয় দেখুন |
| Test যা কিছুই যাচাই করে না — শুধু “presenter তৈরি হলো”, বা আসল কোডের হুবহু প্রতিচ্ছবি | Presenter-এ ইচ্ছা করে bug ঢুকিয়ে (যেমন toFixed(2) বাদ) দেখুন test ব্যর্থ হয় কি না — mutation testing, পর্ব ৫০ |
| ছোট form-এর জন্য Redux, saga, ছয়টি ফাইল — pattern-এর অতিপ্রয়োগ | State কতগুলো component ভাগ করে গুনুন; একটি হলে local state যথেষ্ট |
| বানানো ইতিহাস বা নিয়ম — ভুল সাল, ভুল লেখক, “Django-র official নাম MVC” | মূল documentation বা প্রবন্ধ মিলিয়ে দেখুন; সূত্র ছাড়া দাবি নয় |
| অন্য project থেকে হুবহু কোড, license না জানিয়ে; prompt-এ API key বা ছাত্রদের আসল তথ্য | বড় অংশের উৎস আর license যাচাই; prompt-এ কাল্পনিক data আর নকল key |
নিচের prompt-এ AI-কে আগে দায়িত্বের ভাগ বলতে, তারপর কোড আর শেষে এমন test দিতে বলা হয়েছে, যা UI ছাড়া চলে। কোড পাঠানোর আগে key, password আর আসল ব্যক্তিগত তথ্য সরিয়ে নিন।
AI-এর দেওয়া UI নকশা হাতে পেলে — চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Rails বা Django-র “MVC” কি Reenskaug-এর MVC? একটি শব্দের অর্থ এভাবে বদলে গেলে ক্ষতি কী?
সরাসরি উত্তর: নামের মিল আছে, কাঠামোর মিল আংশিক। Reenskaug-এর MVC জন্মেছিল ১৯৭০-এর দশকের শেষে Smalltalk-এর graphical পরিবেশে, যেখানে একটি screen অনেকক্ষণ খোলা থাকে, model বদলালে view নিজে থেকে নতুন করে আঁকে, আর controller mouse ও keyboard-এর ঘটনা সামলায়। এর প্রাণ হলো model থেকে view-এ observer-এর সম্পর্ক — model কোনো view চেনে না, তবু সবাই হালনাগাদ থাকে।
Web-এ HTTP-র কাঠামোই আলাদা। একটি request আসে, server একটি পূর্ণ পাতা বানিয়ে পাঠায়, সম্পর্ক শেষ। Server-এর model বদলালে browser-এ খোলা পাতাকে জানানোর কোনো স্বাভাবিক পথ নেই। ১৯৯০-এর দশকের শেষে Java-র JSP “Model 2” নির্দেশিকা “request সামলানো servlet, data-র bean, আর পাতা বানানো JSP” — এই তিন ভাগকে MVC বলে ব্যাখ্যা করে, আর পরে Struts ও ২০০৪ সালের Rails নামটিকে web-এ স্থায়ী করে। কিছু লেখক একে আলাদা নামে ডাকার প্রস্তাবও দিয়েছেন। Django জেনেশুনে নিজের নকশাকে MTV বলে, আর তার FAQ-এ বলা আছে, নাম নিয়ে বিতর্ক অর্থহীন — দায়িত্বের ভাগটাই আসল।
তাহলে ক্ষতি কোথায়? প্রথমত, যোগাযোগে। একজন iOS developer “controller” বলতে বোঝেন screen-এর মালিক এক বিশাল class; একজন Rails developer বোঝেন request-এর handler; একজন Smalltalk-প্রেমী বোঝেন input-এর অনুবাদক। একই design meeting-এ তিনজন তিন জিনিস ভাবছেন। দ্বিতীয়ত, শিক্ষায়। “MVC মানে model view-কে notify করে” — এই কথা web MVC-তে খুঁজতে গিয়ে ছাত্ররা বিভ্রান্ত হন। তৃতীয়ত, নকশার সিদ্ধান্তে। “আমরা তো MVC মানি” বলে দল ভাবে কাজ শেষ, অথচ business logic কোথায় থাকবে সেই প্রশ্নের উত্তর MVC নাম থেকে আসে না।
লাভও আছে। একটি পরিচিত নাম নতুন ক্ষেত্রে মূল চিন্তাটিকে — screen, নিয়ম আর input আলাদা — দ্রুত ছড়িয়ে দিয়েছে। Rails-এর “convention over configuration” হাজার হাজার developer-কে শিখিয়েছে কোন ফাইল কোথায় থাকে।
মূল উপলব্ধি: নকশার নামের চেয়ে দায়িত্বের ভাগ গুরুত্বপূর্ণ। “MVC” শুনলে জিজ্ঞেস করুন: কে input পায়, কে নিয়ম জানে, আর model বদলালে screen কীভাবে জানে — উত্তর তিনটিই আসল নকশা।
প্র ০২MVC থাকতে MVP আর MVVM-এর দরকার পড়ল কেন? প্রতিটি নকশা কোন প্রযুক্তির চাপ থেকে জন্মেছে?
প্রতিটি নকশা তার সময়ের UI প্রযুক্তির একটি উত্তর। Smalltalk-80-এর যুগে screen-এর প্রতিটি অংশের জন্য developer নিজে input সামলাতেন — mouse কোথায়, কোন key চাপা হলো। তাই একটি আলাদা controller যুক্তিসংগত ছিল। ১৯৯০-এর দশকে Windows, Mac আর পরে Java-র widget toolkit এলো, যেখানে button নিজেই জানে কখন সে চাপা হয়েছে, text box নিজেই keyboard সামলায়। Controller-এর পুরোনো কাজ widget-এর ভেতরে চলে গেল। কিন্তু একটি প্রশ্ন রয়ে গেল: “Save চাপলে কী হবে, কোন field লাল হবে, কখন button বন্ধ থাকবে” — এই screen-এর যুক্তি কোথায় থাকবে? Taligent-এর প্রবন্ধে (Mike Potel, ১৯৯৬ — সূত্রমতে) আর Dolphin Smalltalk-এর নকশায় উত্তর এলো presenter: view ঘটনা ধরে presenter-কে দেয়, presenter সিদ্ধান্ত নেয়।
২০০০-এর দশকে আরেকটি চাপ এলো — automated test আর agile-এর প্রসার। GUI-কে test করা ছিল কঠিন আর ভঙ্গুর: আসল window খোলা, button খুঁজে চাপা, pixel মেলানো। MVP-র passive view রূপটি তাই জনপ্রিয় হলো: view এত বোকা যে তাকে test না করলেও চলে, আর সব যুক্তি presenter-এ, যা নকল view দিয়ে test হয়। Fowler ২০০৬ সালে এই দুই ঝোঁককে passive view আর supervising controller নামে আলাদা করে বর্ণনা করেন।
MVVM এলো declarative UI আর শক্তিশালী data binding-এর সাথে। Microsoft-এর WPF-এ screen লেখা হতো XAML নামের একটি markup-এ, যেখানে লেখা যায় “এই text box ওই property-র সাথে বাঁধা”. তখন presenter-কে আর হাতে হাতে view-কে বলতে হয় না; binding engine নিজেই মেলায়। John Gossman ২০০৫ সালে এই রূপকে MVVM নামে বর্ণনা করেন; চিন্তাটি Fowler-এর Presentation Model-এর খুব কাছের। দাম হলো binding-এর জাদু — প্রবাহ কোডে দেখা যায় না, ভুল binding প্রায়ই নিঃশব্দে ব্যর্থ হয়।
২০১০-এর দশকে browser-এ বড় single-page অ্যাপ এলো, আর অনেক two-way binding-এর জালে “কে বদলাল” প্রশ্ন অসহনীয় হলো। উত্তর: Flux আর Redux-এর একমুখী flow, আর component-ভিত্তিক UI.
মূল উপলব্ধি: নকশা জন্মায় সীমাবদ্ধতা থেকে — input কে সামলায়, binding আছে কি না, test কতটা কঠিন। একটি নকশা বেছে নেওয়ার আগে জিজ্ঞেস করুন, আপনার প্ল্যাটফর্মে সেই সীমাবদ্ধতাগুলো আছে কি না।
প্র ০৩Two-way binding এত সুবিধাজনক হলেও অনেক দল কেন একমুখী data flow-এ সরে গেছে? আবার কেন অনেকে ফিরছেন?
Two-way binding-এর আকর্ষণ সহজবোধ্য: একটি form-এ দশটি field, প্রতিটি একটি property-র সাথে বাঁধা, আর মান পড়া ও বসানোর কোনো হাতে লেখা কোড নেই। ২০১০ সালে প্রকাশিত AngularJS এই সুবিধা browser-এ জনপ্রিয় করে। কিন্তু বড় অ্যাপে তিনটি সমস্যা দেখা দেয়। প্রথমত, পরিবর্তন ধরার খরচ: AngularJS-এর digest cycle প্রতিটি ঘটনার পরে সব watch করা মান আগের সাথে মেলাত, আর একটি watcher অন্য মান বদলালে আবার ঘুরত; অনেকবার ঘুরেও স্থির না হলে একটি পরিচিত error দিয়ে থেমে যেত (“10 $digest() iterations reached” ধরনের বার্তা)। দ্বিতীয়ত, প্রবাহ অদৃশ্য: একটি মান চার জায়গা থেকে বদলাতে পারে, আর bug খুঁজতে সব জায়গা খুঁজতে হয়। তৃতীয়ত, ক্রমের অনিশ্চয়তা: কোন binding আগে চলবে, তার ওপর ফল নির্ভর করলে bug মাঝে মাঝে দেখা দেয়।
Facebook ২০১৪ সালে Flux উপস্থাপনের সময় এরকম একটি উদাহরণ দেয় — না-পড়া বার্তার সংখ্যা ভুল দেখানোর একটি bug, যা বারবার সারানো হলেও ফিরে আসত (এটি তাদের প্রকাশ্য বক্তব্য; ভেতরের খুঁটিনাটি আমরা জানি না)। একমুখী flow-এর লাভ: প্রতিটি বদল একটি action, একটি log, একটি pure function. Debug মানে log পড়া। Angular-এর পরের সংস্করণগুলোও ডিফল্টভাবে একমুখী binding-এ যায়, two-way রাখে একটি স্পষ্ট বাক্য-রীতি হিসেবে।
তবে পেন্ডুলাম আবার দুলছে। Redux-এর আনুষ্ঠানিকতা (action, reducer, constant) ছোট কাজে ভারী লাগে; তাই Redux Toolkit-এর মতো সরল রূপ এসেছে। আর “signal” নামে fine-grained reactive state (SolidJS, Svelte-এর নতুন সংস্করণ, Angular-এর signal) জনপ্রিয় হচ্ছে — যা অনেকটা MVVM-এর observable property-র আধুনিক রূপ, কিন্তু নির্ভরতা নিজে থেকে হিসাব করে আর চক্র এড়ানোর নিয়ম ভেতরে রাখে। Form-এর জন্য two-way binding-এর সুবিধাও এসব framework রাখে।
আসল শিক্ষাটি তাই “two-way খারাপ, one-way ভালো” নয়। প্রশ্ন হলো: state-এর মালিক কে, আর বদল কতগুলো পথে আসতে পারে। একটি form-এর ভেতরে স্থানীয় two-way binding প্রায় নিরীহ; পুরো অ্যাপের ভাগের state-এ বহু দিক থেকে লেখা বিপজ্জনক।
মূল উপলব্ধি: যত বেশি জায়গা থেকে একটি state বদলানো যায়, bug খোঁজা তত কঠিন। ভাগের state-এ বদলের পথ কমান; স্থানীয় state-এ সুবিধা নিন।
প্র ০৪“Skinny controller, fat model” নাকি service layer — business logic-এর আদর্শ ঠিকানা আসলে কোথায়?
এই বিতর্কের তিনটি ধাপ চেনা যায়। প্রথম ধাপে web MVC-র নতুন developer-রা সব কোড controller-এ লিখতেন — request এলো, query চালানো, হিসাব, template-এ পাঠানো — সব এক জায়গায়। ফল: একই ছাড়ের নিয়ম পাঁচটি controller-এ, আর কোনো test নেই। Rails-সমাজে ২০০৬ সালের আশেপাশে “skinny controller, fat model” পরামর্শটি জনপ্রিয় হয় (Jamis Buck-এর একটি blog লেখা প্রায়ই এর উৎস হিসেবে উদ্ধৃত হয়): নিয়ম model-এ সরানো হোক, controller শুধু সমন্বয় করুক।
দ্বিতীয় ধাপে দেখা গেল model নিজেই বিশাল। একটি User class-এ লগইন, বিল, email, report, permission — হাজার লাইন, পর্ব ২৫-এর ভাষায় দুর্বল cohesion. আর ActiveRecord-ধাঁচের model database-এর সাথে বাঁধা, তাই তার test-এ database লাগে। তখন service object, form object, query object-এর মতো আলাদা ছোট class জনপ্রিয় হলো।
তৃতীয় ধাপে বিপরীত ভুল: সব নিয়ম service-এ, model শুধু data-র থলে। Fowler ২০০৩ সালে একে anemic domain model বলে সতর্ক করেন — procedural কোডকে object-এর পোশাক পরানো। Domain-driven design (Eric Evans, ২০০৩) উল্টো দিকে বলে: নিয়ম থাকবে domain-এর ভাষায়, domain object-এর ভেতরে; service শুধু সেই কাজের জন্য, যা স্বাভাবিকভাবে কোনো একটি object-এর নয়।
একটি ব্যবহারিক নিয়ম: (১) যে নিয়ম একটি object-এর নিজের data নিয়ে — result.isFail() — তা সেই object-এ। (২) যে কাজ একাধিক object আর বাইরের ব্যবস্থা মিলিয়ে — ভর্তি নিশ্চিত করা, ফি নেওয়া, SMS — তা একটি application service-এ। (৩) Controller, presenter, view model শুধু অনুবাদ করে: input থেকে service-এর ডাক, ফল থেকে screen-এর state. (৪) Template-এ কোনো নিয়ম নয়। আর নিয়মের কোড যেন framework, database বা UI-র কোনো class import না করে — তাহলে framework বদলালেও নিয়ম অক্ষত থাকে; এই চিন্তার পূর্ণ রূপ পর্ব ৪০-এর hexagonal architecture.
মূল উপলব্ধি: “Fat” বা “skinny” নয়, প্রশ্ন হলো cohesion: প্রতিটি নিয়ম সেই জায়গায়, যার data আর যার বদলের কারণ তার সাথে মেলে — আর UI-র দিকে কখনো নয়।
প্র ০৫React-এর মতো component-ভিত্তিক UI এসে কি MVC, MVP আর MVVM-কে অপ্রাসঙ্গিক করে দিয়েছে?
প্রথম দেখায় তাই মনে হয়। React ২০১৩ সালে open source হওয়ার সময় একটি বিতর্কিত সিদ্ধান্ত নেয়: HTML-এর মতো markup (JSX) আর JavaScript-এর logic একই ফাইলে, একই component-এ। অনেকে বললেন, এটা separation of concerns-এর লঙ্ঘন — template আর কোড তো আলাদা থাকার কথা! React দলের পাল্টা যুক্তি ছিল: technology (HTML বনাম JS) অনুযায়ী ভাগ করা আর concern (দায়িত্ব) অনুযায়ী ভাগ করা এক নয়। একটি “ResultCard”-এর চেহারা আর তার “খোলা-বন্ধ” আচরণ একই concern; সেগুলো একসাথে থাকাই স্বাভাবিক।
কিন্তু খেয়াল করুন, component পুরোনো নকশাগুলোকে মুছে দেয়নি, ছোট আকারে ভেতরে নিয়ে নিয়েছে। একটি component-এর state আর handler হলো তার view model; JSX হলো view; state বদলালে আবার আঁকা হলো binding-এর এক রূপ। Props নিচে, callback ওপরে — প্রতিটি component-এর ভেতরে একমুখী flow. আর বহু component-এর ভাগের state-এর জন্য store — Flux-এর উত্তরাধিকার।
যা component নিজে সমাধান করে না, তা হলো business logic-এর ঠিকানা। একটি component-এর ভেতরে fetch, GPA-র নিয়ম, টাকার হিসাব আর চেহারা — এটি নতুন পোশাকে পুরোনো massive view controller. React-এর hooks (২০১৯) এর একটি উত্তর দিয়েছে: custom hook-এ screen-এর state আর আচরণ আলাদা করা যায় (প্রায় একটি view model), আর বিশুদ্ধ নিয়ম সাধারণ JavaScript function-এ (model), যা React ছাড়াই test হয়। Mobile-এর জগতেও একই প্রবণতা — Android-এর Jetpack Compose আর Apple-এর SwiftUI declarative component আনার পরেও ViewModel ধারণাটি Android-এর official নির্দেশিকায় রয়ে গেছে (লেখার সময়ে)।
অর্থাৎ নাম আর কাঠামো বদলায়, প্রশ্নগুলো থাকে: কে state-এর মালিক, বদল কোন পথে আসে, নিয়ম কোথায়, আর কোন অংশ UI ছাড়া test হয়। যে developer MVC থেকে Redux পর্যন্ত এই প্রশ্নগুলো বোঝেন, তিনি পরের framework-টিও দ্রুত পড়তে পারবেন।
মূল উপলব্ধি: Component হলো ভাগের একক বদল — technology অনুযায়ী নয়, screen-এর টুকরো অনুযায়ী। কিন্তু টুকরোর ভেতরে view, screen-এর state আর business rule আলাদা রাখার দায় এখনো আপনার।
অনুশীলন
একটি আউটপুট অনুমান, একটি “কোন কোড কোথায়” সেট, একটি গণনা আর একটি নকশার কাজ — প্রতিটির পূর্ণ উত্তর দেওয়া আছে।
-
আউটপুট অনুমান করুন (reducer-এর state): অংশ ৬-এর
reducerআরviewfunction হুবহু একই। শুরুর state{ items: [], filter: "all", nextId: 1 }. নিচের নয়টি action একে একে reducer-এ দেওয়া হলো। প্রতিটির পরেview(state)কী দেখাবে? শেষ state-এরitemsআরnextIdকী? আর শেষ action-টির পরে reducer কি আগের state object-টিই ফেরত দেয়?const actions = [ { type: "added", name: "tea" }, { type: "added", name: "sugar" }, { type: "added", name: "milk" }, { type: "toggled", id: 2 }, { type: "removed", id: 1 }, { type: "filterSet", filter: "done" }, { type: "toggled", id: 3 }, { type: "added", name: "salt" }, { type: "toggled", id: 9 }, ]; let s = { items: [], filter: "all", nextId: 1 }; for (const a of actions) { const next = reducer(s, a); const arg = a.name ?? a.id ?? a.filter; console.log(`${a.type.padEnd(9)} ${String(arg).padEnd(5)} -> ${view(next)} same=${next === s}`); s = next; } console.log(JSON.stringify(s));added tea -> 1[ ]tea same=false added sugar -> 1[ ]tea 2[ ]sugar same=false added milk -> 1[ ]tea 2[ ]sugar 3[ ]milk same=false toggled 2 -> 1[ ]tea 2[x]sugar 3[ ]milk same=false removed 1 -> 2[x]sugar 3[ ]milk same=false filterSet done -> 2[x]sugar same=false toggled 3 -> 2[x]sugar 3[x]milk same=false added salt -> 2[x]sugar 3[x]milk same=false toggled 9 -> 2[x]sugar 3[x]milk same=false {"items":[{"id":2,"name":"sugar","done":true},{"id":3,"name":"milk","done":true},{"id":4,"name":"salt","done":false}],"filter":"done","nextId":5}শেষ state:
items= sugar (id 2, done), milk (id 3, done), salt (id 4, not done);filter="done";nextId=5. তিনটি ফাঁদ: (ক)removed 1-এর পরেnextIdকমে না — id কখনো পুনর্ব্যবহার হয় না, তাই salt পায় 4. (খ)filterSet done-এর পরে view শুধু কেনা জিনিস দেখায়; salt যোগ হলেও screen-এ আসে না, কিন্তু state-এ আছে। (গ)toggled 9-এ কোনো জিনিস বদলায় না, তবু reducer নতুন object ফেরত দেয় — কারণ{...s, items: s.items.map(...)}সবসময় নতুন object আর নতুন array বানায়। ফলে “state বদলেছে কি না” যারা reference (===) দিয়ে দেখে, তারা অকারণে screen আবার আঁকবে। প্রতিকার: id না পেলেreturn s. (এই আউটপুট আসল কোড চালিয়ে মেলানো।) -
কোন কোড কোথায়: একটি কাল্পনিক অনলাইন বাজার অ্যাপের আটটি টুকরো। প্রতিটি কোন স্তরে বসবে — model/domain, service, controller বা presenter, view model, view/template, না reducer? (ক) “৫০০ টাকার বেশি হলে delivery ফ্রি” (খ) Add button চালু থাকবে কি না, তা draft খালি কি না দেখে (গ) দাম “৳১,২৫০.০০” আকারে দেখানো (ঘ)
POST /ordersrequest থেকে form data পড়ে service ডাকা (ঙ) অর্ডার নিশ্চিত: stock কমানো + টাকা কাটা + SMS (চ){type: "cartCleared"}পেলে খালি ঝুড়ির নতুন state (ছ) ঝুড়ির টেবিলের HTML আর রং (জ) “একজন গ্রাহক দিনে ৩টির বেশি অর্ডার বাতিল করতে পারবেন না”- (ক) Model/domain — ঝুড়ির নিজের data-র নিয়ম:
cart.deliveryFee(). UI বা channel বদলালেও একই। - (খ) View model (বা presenter) — screen-এর অবস্থা, ব্যবসার নিয়ম নয়:
canAdd. MVP-তে presenterview.setAddEnabled()ডাকবে। - (গ) View model বা view-এর formatter — দেখানোর ধরন; model-এ টাকা সংখ্যা হিসেবেই থাকবে। Template-এ জটিল হিসাব না করে একটি ছোট format function ব্যবহার করুন, যার আলাদা test হয়।
- (ঘ) Controller (web MVC) — request থেকে input অনুবাদ, তারপর service-এর ডাক; নিজে কোনো নিয়ম নয়।
- (ঙ) Service layer — অনেক object আর বাইরের ব্যবস্থা মিলিয়ে একটি কাজ:
OrderService.confirm(). - (চ) Reducer — action থেকে নতুন state, pure.
- (ছ) View/template — শুধু চেহারা; কোনো if-এ ব্যবসার নিয়ম নয়।
- (জ) Model/domain (প্রয়োজনে service-এর সাহায্যে) — এটি ব্যবসার নিয়ম। গ্রাহকের দিনের বাতিল-সংখ্যা জানতে data আনতে হলে service সেটি আনবে, কিন্তু নিয়মের সিদ্ধান্ত (“৩-এর বেশি নয়”) domain-এর একটি function-এ, যাতে web, mobile আর API — তিন জায়গাতেই একই থাকে।
পরীক্ষার একটি সহজ প্রশ্ন: “UI পুরোটা বদলে দিলে কি এই কোড বদলাতে হবে?” না হলে এটি UI-র দিকে থাকার কথা নয়।
- (ক) Model/domain — ঝুড়ির নিজের data-র নিয়ম:
-
গণনা — কতগুলো render আর কতগুলো লেখা: (ক) অংশ ২-এর MVC কোডে model-এ দুটি view নিবন্ধিত, আর controller পাঁচটি input পায়, যার দুটি প্রত্যাখ্যাত। আউটপুটে কয়টি লাইন হবে? যদি তৃতীয় একটি view যোগ করা হয় আর একই input-গুলো দেওয়া হয়? সাধারণ সূত্র লিখুন (k টি view, n টি সফল বদল, r টি প্রত্যাখ্যান)। (খ) অংশ ৫-এর MVVM কোডে guard চালু রেখে ব্যবহারকারী “potato” টাইপ করে Add চাপলে, তারপর “egg” টাইপ করে আবার Add চাপলে text box-এ মোট কতবার লেখা (
writes) হবে?(ক) সফল বদল n = ৩ (rice যোগ, onion যোগ, row 0 toggle), প্রত্যাখ্যান r = ২, view k = ২। প্রতিটি সফল বদলে প্রতিটি view একটি লাইন, প্রতিটি প্রত্যাখ্যানে controller একটি লাইন: লাইন = k·n + r = ২·৩ + ২ = ৮ — অংশ ২-এর আসল আউটপুটের আট লাইনের সাথে মেলে। তৃতীয় view যোগ করলে ৩·৩ + ২ = ১১. লক্ষ করুন, view যোগ করতে model-এর এক লাইনও বদলাতে হয়নি, কিন্তু প্রতিটি বদলের খরচ view-এর সংখ্যার সমানুপাতে বাড়ে — অনেক view থাকলে অকারণ render এড়াতে “কী বদলাল” খবরে পাঠানো হয়।
(খ) Bind করার সময় ১টি লেখা। প্রতিটি “টাইপ + Add” চক্রে দুটি: টাইপের পরে view model থেকে প্রতিধ্বনি (মান বদলেছে, তাই একবার লেখা; পরের প্রতিধ্বনি guard-এ থামে), আর Add-এর পরে
draft.set(""). মোট = ১ + ২ + ২ = ৫. প্রথম চক্রের শেষে writes ৩ — অংশ ৫-এর আউটপুটের সাথে মেলে। Guard না থাকলে প্রশ্নটির উত্তর “অসীম”, আর নকল box ছয় নম্বর লেখায় থামিয়ে দেয়। -
নকশা ও সমালোচনা: একটি কাল্পনিক বিশ্ববিদ্যালয়ের ফলাফল পোর্টালের বর্তমান screen-এ একটি ৬০০ লাইনের
ResultPagecomponent: সেখানে fetch, roll যাচাই, GPA থেকে letter grade, “fail” নির্ধারণ, PDF বানানো, আর JSX. দলকে এখন একটি mobile অ্যাপ বানাতে হবে, যেখানে একই ফল দেখাবে। (ক) বর্তমান নকশার তিনটি সমস্যা লিখুন। (খ) MVVM (বা MVP) ধাঁচে একটি ভাগ প্রস্তাব করুন: কোন কোন module, প্রতিটির দায়িত্ব আর কী expose করে। (গ) প্রতিটি অংশ কীভাবে test হবে? (ঘ) কোন অংশ web আর mobile ভাগ করে নিতে পারে?(ক) সমস্যা: (১) GPA আর fail-এর নিয়ম UI-র ভেতরে — mobile-এ নতুন করে লিখতে হবে, আর দুটি কপি একসময় আলাদা হয়ে যাবে। (২) নিয়মের test-এ পুরো component render করতে হয়, network-এর নকল লাগে — ধীর আর ভঙ্গুর। (৩) একটি ফাইলে চারটি বদলের কারণ (নিয়ম, network, PDF, চেহারা) — দুজন developer একসাথে হাত দিলে merge conflict, আর একটি বদলে আরেকটি ভাঙার ঝুঁকি।
(খ) প্রস্তাবিত ভাগ:
grading(model/domain, বিশুদ্ধ function):letterGrade(gpa),isFail(gpa),validRoll(roll). কোনো import নেই।ResultService(data আনা):find(roll), interface-এর পেছনে আসল HTTP; সাথেPdfExporterআলাদা।ResultViewModel: state —roll,busy,error,result(নাম, GPA-র লেখা, grade, fail কি না); command —search()(canSearch= বৈধ roll আর busy নয়),downloadPdf(). Service constructor-এ inject হয়। কোনো UI import নয়।ResultPage(web) আরResultScreen(mobile): শুধু চেহারা, view model-এর state-এ binding আর command-এ button.
(গ) Test:
grading— boundary value: ২.০০, ১.৯৯, ৪.০০, ভুল roll (পর্ব ৪৭-এর কৌশল)। View model — fake service দিয়ে: ভুল roll-এ service ডাকা হয় না; ফল না পেলেbusyবন্ধ হয়ে error; দ্রুত দুবার search-এ পুরোনো উত্তর নতুনটিকে ঢাকে না। View — কয়েকটি component test (ফল দেখায়, error দেখায়); পুরো যাত্রার একটি-দুটি end-to-end.(ঘ) ভাগ:
gradingসরাসরি ভাগ হয়, আর একই ভাষা হলে view model আর service-এর interface-ও। Mobile ভিন্ন ভাষায় হলে অন্তত নিয়মের test case-গুলো দুই দিকে একই রাখুন, অথবা নিয়মটি server-এ রেখে দুই client-কে শুধু ফল দিন — তখন নিয়মের একমাত্র কপি server-এ, আর UI দুটিই বোকা থাকে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমRedux-ধাঁচের একমুখী data flow-এ একটি click থেকে নতুন screen পর্যন্ত ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 👆 ব্যবহারকারী view-এ একটি button চাপেন
- 📝 View একটি action object বানায়: “কী ঘটেছে”
- 📤 Action
dispatchদিয়ে store-এ যায় - 🧮 Store reducer চালায়: পুরোনো state আর action থেকে নতুন state
- 📣 Store নতুন state রাখে আর শ্রোতাদের জানায়
- 🖥️ View নতুন state থেকে screen আবার আঁকে
Classic (Smalltalk-ধাঁচের) MVC-তে model বদলালে view সাধারণত কীভাবে জানতে পারে?
- Controller প্রতিটি view-কে আলাদা করে ডেকে জানায়
- Model নিবন্ধিত view-দের খবর দেয় (observer), view নিজে model পড়ে
- View প্রতি সেকেন্ডে model-কে জিজ্ঞেস করে
- জানতে পারে না; পরের request-এ নতুন পাতা আসে
MVP-র passive view রূপে presenter-কে UI ছাড়া test করা সহজ কেন?
- কারণ presenter-এ খুব কম কোড থাকে
- কারণ presenter model-কে কখনো ডাকে না
- কারণ presenter view-কে শুধু একটি interface দিয়ে চেনে, তাই নকল view বসানো যায়
- কারণ data binding নিজে থেকে test করে
একটি MVVM অ্যাপে ResultViewModel ফাইলে নিচের কোনটি থাকা সবচেয়ে বড় নকশাগত ভুল?
document.querySelector("#gpa").textContent = gpa- একটি observable
busyproperty - একটি
search()command আরcanSearch - Constructor-এ একটি
ResultServiceনেওয়া
Redux-ধাঁচের reducer হিসেবে নিচের কোনটি সঠিক?
(s, a) => { s.items.push(a.item); return s; }(s, a) => { fetch("/save"); return { ...s, saved: true }; }(s, a) => ({ ...s, updatedAt: Date.now() })(s, a) => ({ ...s, items: [...s.items, a.item] })
একটি two-way binding-এ view model-এর set(v) নতুন মান আগের সমান হলেও শ্রোতাদের খবর দেয়, আর text box-এ লেখা হলে সেটি আবার input ঘটনা ছোড়ে। কী ঘটবে, আর সবচেয়ে সরল প্রতিকার কী?
- একবার লেখা হয়ে থেমে যাবে; কোনো প্রতিকার লাগবে না
- শুধু ক্রম ভুল হবে; binding-এর ক্রম বদলালেই ঠিক
- অসীম চক্র হবে; মান সমান হলে set কিছু না করলে (equality guard) থামবে
- চক্র হবে; set-কে asynchronous বানালেই সমাধান
Django framework নিজের নকশাকে “MTV” বলে। সেখানে Django-র “view” মোটামুটি কোন ভূমিকা পালন করে?
- যে function HTTP request সামলায় — অন্য framework-এর controller-এর কাছাকাছি
- HTML template, যা পাতার চেহারা বানায়
- Database-এর টেবিলের সাথে যুক্ত class
- Browser-এ চলা JavaScript component
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- কেন আলাদা: UI ঘন ঘন বদলায়, নিয়ম অন্য কারণে বদলায়। আলাদা রাখলে UI ছাড়া test, এক data-র অনেক view, আর দলের সমান্তরাল কাজ।
- Classic MVC: controller input নেয়, model observer দিয়ে খবর দেয়, view নিজে পড়ে। Web MVC-তে controller request সামলায়, view একটি template, আর model view-কে খবর দেয় না; Django বলে MTV.
- MVP: presenter view-কে interface দিয়ে চেনে — নকল view দিয়ে test করা যায়। MVVM: view model state আর command রাখে, view চেনে না; binding screen মেলায়; two-way binding-এ equality guard.
- একমুখী flow: action → pure reducer → store → view. Log replay করলে একই state; reducer-এ mutation,
Date.now(), fetch নয়। - নিয়মের ঠিকানা: model বা service layer — view, template, controller-এ নয়। Massive view controller, template-এ logic আর binding-এর চক্র চিনে রাখুন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৪০ · Clean আর hexagonal architecture — port, adapter আর domain-driven design-এর ধারণাপরবর্তী পর্বBusiness logic-কে framework আর UI থেকে পুরোপুরি স্বাধীন রাখা।
- পর্ব ৩৮ · Software architecture — layered, client-server, pipe-filter, event-driven আর ADRআগের পর্বপুরো system-এর স্তর ভাগ, যার ভেতরে UI-র নকশা বসে।
- পর্ব ৩৫ · Behavioral pattern ১ — strategy, observer, command আর template methodভিত্তিMVC-র observer আর Redux-এর action-এর পূর্বপুরুষ command.
- সফটওয়্যার ইঞ্জিনিয়ারিং ও Git · কোর্স হোম৭০টি পর্বপুরো কোর্সের রোডম্যাপ।
- JavaScript · framework-এর পরিচয়পাশের ট্র্যাকReact, Vue, Svelte আর Angular — component আর state হাতেকলমে।
- System Design · Event sourcing আর CQRSগভীরে যানAction-log replay-এর চিন্তা যখন server আর database-এ যায়।