সরাসরি পর্বে যান
পর্ব ৩৯ · ৭০-এর মধ্যে · মডিউল ৭

MVC, MVP আর MVVM — UI-র কোড সাজানো

MVC, MVP and MVVM — Organising User-Interface Code

Taught in Bangla, this lesson explains why UI and logic should be separated, then covers classic Smalltalk MVC with observers, the different meaning of MVC in web frameworks, MVP with a presenter and passive view tested without a UI, MVVM with data binding and two-way binding loops, unidirectional data flow with actions, reducers and a store, component-based UIs, where business logic belongs, and testing each layer.

এই পর্বে যা শিখবেন

  • 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 কোন পথে যায়।

  1. ব্যবহারকারীclick row 0input
  2. ControlleronRowClick(0)অনুবাদ করে
  3. Modeltoggle(0)বদলায়, খবর দেয়
  4. দুটি viewrender()নিজে পড়ে আঁকে
প্রতিটি ধাপে অবস্থা
এখন কাজ করছে
items[0].done
তালিকা দেখাচ্ছে
badge দেখাচ্ছে
  1. Click. ব্যবহারকারী চালের row-তে click করলেন। Screen-এ চাল এখনো কেনা হয়নি ([ ]), badge-এ “2 left”. Classic MVC-তে input-এর ঘটনা যায় controller-এর কাছে, view-এর কাছে নয়।
  2. Controller অনুবাদ করে। “row 0-তে click” কথাটি controller model-এর ভাষায় বদলে দেয়: model.toggle(0). Controller screen-এ কিছু আঁকে না, আর তালিকার ভেতরের array-তেও হাত দেয় না — শুধু model-এর একটি method ডাকে।
  3. Model বদলায় আর খবর দেয়। items[0].done এখন true. Model তার শ্রোতাদের বলে “toggle 0”. খেয়াল করুন, এই মুহূর্তে দুটি view-ই পুরোনো ছবি দেখাচ্ছে — খবর এখনো পৌঁছায়নি।
  4. প্রথম view আঁকে। ListView খবর পেয়ে model থেকে নতুন তালিকা পড়ে আঁকে: 0[x]rice. Model তাকে নতুন data ঠেলে দেয়নি; view নিজে পড়েছে — পর্ব ৩৫-এর pull model.
  5. দ্বিতীয় 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-এ লেখার কাজ বেশি।

সর্বোচ্চ testability

Supervising 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 কতবার লিখেছে।

View (text box, label) আর view model
ঘটনা
box.value
vm.draft
canAdd
vm.items
label.text
writes
  1. Bind. Two-way binding তৈরির সময় binder view model-এর বর্তমান মান ("") text box-এ একবার লেখে — writes ১। Label-ও one-way binding-এ “items(0): ” দেখায়।
  2. টাইপ। ব্যবহারকারী লিখলেন “potato”. Text box-এর মান বদলাল, আর সে একটি input ঘটনা ছুড়ল। View model এখনো জানে না — vm.draft খালি।
  3. View → view model. Binder ঘটনাটি ধরে vm.draft.set("potato") ডাকে। মান বদলাল, তাই view model তার শ্রোতাদের খবর দেয় — আর শ্রোতাদের মধ্যে আছে binder নিজেই, যে text box-এ “potato” লেখে (writes ২)। canAdd এখন true, তাই Add button চালু হবে।
  4. প্রতিধ্বনি থামল। Text box-এ লেখা হওয়ায় সে আবার input ছোড়ে, binder আবার set("potato") ডাকে। কিন্তু মান আগেরটির সমান — guard ফিরে যায়, কোনো খবর নয়, আর কোনো লেখা নয়। চক্র এখানেই শেষ।
  5. Add command. add() প্রথমে canAdd দেখে, তারপর items-এ “potato” যোগ করে। Label-এর one-way binding নিজে থেকে “items(1): potato” দেখায় — কেউ label-কে আলাদা করে কিছু বলেনি।
  6. 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-এর চারটি অংশ:

  1. Action — “কী ঘটেছে” তার একটি সাধারণ data object: { type: "toggled", id: 1 }. কোনো কোড নয়, শুধু বিবরণ।
  2. Reducer — একটি pure function: (পুরোনো state, action) → নতুন state. কোনো network ডাক নেই, কোনো side effect নেই, পুরোনো state বদলায় না, নতুন একটি বানায় — এই গুণের নাম immutability.
  3. Store — বর্তমান state-এর একমাত্র রক্ষক। dispatch(action) পেলে reducer চালায়, নতুন state রাখে, শ্রোতাদের জানায়।
  4. View — state থেকে screen বানানোর একটি function. ব্যবহারকারী কিছু করলে view নিজে state বদলায় না, শুধু একটি action পাঠায়।

মুদি দোকানের হিসাবের খাতা ভাবুন। দোকানি কখনো আগের পাতার অঙ্ক ঘষে মুছে নতুন লেখেন না; প্রতিটি লেনদেন একটি নতুন লাইন, যেমন “রহিম, ২ কেজি চাল, বাকি” — এটাই action. মাসের শেষের বাকির হিসাব (state) ওই লাইনগুলো প্রথম থেকে যোগ করেই পাওয়া যায় — এটাই reducer-এর কাজ। হিসাবে গরমিল হলে খাতা খুলে লাইন ধরে ধরে মেলানো যায়, আর “১৫ তারিখে বাকি কত ছিল” জানতে প্রথম থেকে ১৫ তারিখ পর্যন্ত যোগ করলেই হয়।

নিচের stepper-এ তালিকায় দুটি জিনিস — rice (id 1) আর egg (id 2), কোনোটিই কেনা হয়নি। ব্যবহারকারী rice-এর row-তে click করলেন।

  1. Viewdispatch(a)action পাঠায়
  2. Storelog.push(a)লিখে রাখে
  3. Reducerreducer(s, a)নতুন state বানায়
  4. Store → viewsubscribeনতুন state, নতুন screen
Store-এর ভেতরে
action
log.length
বর্তমান state.items
পুরোনো s1
egg object
  1. View একটি action পাঠায়। Click-এর handler state-এ হাত দেয় না; সে শুধু বর্ণনা করে কী ঘটেছে: {type: "toggled", id: 1}, তারপর store.dispatch(). Log-এ আগের দুটি “added” আছে।
  2. Store লিখে রাখে। Action log-এ যোগ হলো, দৈর্ঘ্য ৩। এই log-ই পরে replay আর “time travel”-এর কাঁচামাল। Action একটি সাধারণ data, তাই log-টি ফাইলে লেখা বা network-এ পাঠানোও যায়।
  3. Reducer নতুন state বানায়। reducer(s1, a) একটি নতুন object ফেরত দেয়: items-এর নতুন array, যেখানে rice-এর একটি নতুন কপি done: true. Egg বদলায়নি, তাই তার object হুবহু একই, কপি করা হয়নি — একে বলে structural sharing. পুরোনো s1 অক্ষত।
  4. Store state বদলায়। Store-এর বর্তমান state এখন s2. এটিই একমাত্র জায়গা যেখানে “বর্তমান” বদলায় — একটি লাইনে, একটি পথে। কোনো view, কোনো controller এখানে সরাসরি লেখে না।
  5. 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

Classic MVC ১. Classic MVC ব্যবহারকারী View Controller Model input দেখায় update read notify MVP ২. MVP (passive view) View Presenter Model event set (interface) call ফল / notify View ✕ Model: পরস্পরকে চেনে না MVVM ৩. MVVM View ViewModel Model binding, command changed call notify ViewModel জানে না কোন View তাকে দেখায় একমুখী data flow ৪. একমুখী (Flux/Redux) View Action Reducer Store dispatch (state, action) নতুন state subscribe সব বদল এক পথে, এক দিকে
ছবি ৩৯.১ — (১) Classic MVC: input যায় controller-এ, model view-কে খবর দেয়, view নিজে model পড়ে। (২) MVP: view আর model-এর মাঝে presenter; presenter view-কে চেনে শুধু একটি interface দিয়ে। (৩) MVVM: view view model-এ বাঁধা; view model শুধু “বদলেছি” জানায়, কোন view তা চেনে না — এই কারণেই MVP-র presenter→view তীরটি এখানে ড্যাশ। (৪) একমুখী: তীর একটি চক্রে, কোনো উল্টো পথ নেই।
প্রশ্নMVC (classic)MVPMVVMএকমুখী
ব্যবহারকারীর input প্রথমে যায়controllerview → presenterview → binding → view modelview → action
Screen হালনাগাদ করে কেview নিজে, model পড়েpresenter, interface দিয়েbinder, নিজে থেকেview, পুরো state থেকে
মাঝের অংশ view চেনে?controller প্রায়ই চেনেহ্যাঁ, interface-টুকুনানা (reducer screen জানে না)
UI ছাড়া test হয়modelpresenter, নকল view দিয়েview model, সরাসরিreducer, সবচেয়ে সহজে
পরিচিত দুর্বলতামোটা controllerpresenter-এ অনেক “বলার কোড”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খুব দ্রুত
ServiceFake repository, stub SMSকাজের ক্রম, ব্যর্থতায় কী হয়দ্রুত
Presenter / view modelSpy বা নকল view; property আর command সরাসরিকী দেখাবে, কখন button বন্ধ, loading চিহ্নদ্রুত
View / templateComponent 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() বা fetchTest-এ পুরোনো 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 আর আসল ব্যক্তিগত তথ্য সরিয়ে নিন।

নিচে একটি কাল্পনিক ফলাফল পোর্টালের search screen-এর বর্তমান কোড দিলাম (গোপন তথ্য বাদ দিয়ে)। [এখানে কোড] ১. কোড বদলানোর আগে একটি টেবিলে দেখাও: প্রতিটি লাইন বা function কোন দায়িত্বের — screen দেখানো, input অনুবাদ, business rule, না data আনা। কোন লাইন ভুল জায়গায় আছে, লাইন নম্বরসহ বলো। ২. তারপর MVVM-এ ভাগ করো। ViewModel-এ কোনো DOM বা UI library-র import থাকবে না; GPA-র নিয়ম থাকবে আলাদা একটি model function-এ। ৩. Two-way binding থাকলে কোথায় equality guard আছে দেখাও। ৪. ViewModel আর model-এর unit test দাও, যা browser ছাড়া চলে; অন্তত একটি test যেন ভুল roll, একটি "result নেই" আর একটি loading চিহ্ন বন্ধ হওয়া যাচাই করে। ৫. কোনো framework-এর নিয়ম বা ইতিহাস উল্লেখ করলে সূত্র দাও; নিশ্চিত না হলে "নিশ্চিত নই" লেখো।

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 আলাদা রাখার দায় এখনো আপনার।

অনুশীলন

একটি আউটপুট অনুমান, একটি “কোন কোড কোথায়” সেট, একটি গণনা আর একটি নকশার কাজ — প্রতিটির পূর্ণ উত্তর দেওয়া আছে।

  1. আউটপুট অনুমান করুন (reducer-এর state): অংশ ৬-এর reducer আর view function হুবহু একই। শুরুর 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. (এই আউটপুট আসল কোড চালিয়ে মেলানো।)

  2. কোন কোড কোথায়: একটি কাল্পনিক অনলাইন বাজার অ্যাপের আটটি টুকরো। প্রতিটি কোন স্তরে বসবে — model/domain, service, controller বা presenter, view model, view/template, না reducer? (ক) “৫০০ টাকার বেশি হলে delivery ফ্রি” (খ) Add button চালু থাকবে কি না, তা draft খালি কি না দেখে (গ) দাম “৳১,২৫০.০০” আকারে দেখানো (ঘ) POST /orders request থেকে form data পড়ে service ডাকা (ঙ) অর্ডার নিশ্চিত: stock কমানো + টাকা কাটা + SMS (চ) {type: "cartCleared"} পেলে খালি ঝুড়ির নতুন state (ছ) ঝুড়ির টেবিলের HTML আর রং (জ) “একজন গ্রাহক দিনে ৩টির বেশি অর্ডার বাতিল করতে পারবেন না”
    • (ক) Model/domain — ঝুড়ির নিজের data-র নিয়ম: cart.deliveryFee(). UI বা channel বদলালেও একই।
    • (খ) View model (বা presenter) — screen-এর অবস্থা, ব্যবসার নিয়ম নয়: canAdd. MVP-তে presenter view.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-র দিকে থাকার কথা নয়।

  3. গণনা — কতগুলো 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 ছয় নম্বর লেখায় থামিয়ে দেয়।

  4. নকশা ও সমালোচনা: একটি কাল্পনিক বিশ্ববিদ্যালয়ের ফলাফল পোর্টালের বর্তমান screen-এ একটি ৬০০ লাইনের ResultPage component: সেখানে 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 পর্যন্ত ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 👆 ব্যবহারকারী view-এ একটি button চাপেন
  2. 📝 View একটি action object বানায়: “কী ঘটেছে”
  3. 📤 Action dispatch দিয়ে store-এ যায়
  4. 🧮 Store reducer চালায়: পুরোনো state আর action থেকে নতুন state
  5. 📣 Store নতুন state রাখে আর শ্রোতাদের জানায়
  6. 🖥️ 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 busy property
  • একটি 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

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. কেন আলাদা: UI ঘন ঘন বদলায়, নিয়ম অন্য কারণে বদলায়। আলাদা রাখলে UI ছাড়া test, এক data-র অনেক view, আর দলের সমান্তরাল কাজ।
  2. Classic MVC: controller input নেয়, model observer দিয়ে খবর দেয়, view নিজে পড়ে। Web MVC-তে controller request সামলায়, view একটি template, আর model view-কে খবর দেয় না; Django বলে MTV.
  3. MVP: presenter view-কে interface দিয়ে চেনে — নকল view দিয়ে test করা যায়। MVVM: view model state আর command রাখে, view চেনে না; binding screen মেলায়; two-way binding-এ equality guard.
  4. একমুখী flow: action → pure reducer → store → view. Log replay করলে একই state; reducer-এ mutation, Date.now(), fetch নয়।
  5. নিয়মের ঠিকানা: model বা service layer — view, template, controller-এ নয়। Massive view controller, template-এ logic আর binding-এর চক্র চিনে রাখুন।

পরের পর্বে: আজ UI-কে নিয়ম থেকে আলাদা করলেন। পর্ব ৪০-এ একই চিন্তা পুরো application-এ — নিয়মকে কেন্দ্রে রেখে database, framework আর UI-কে বাইরের “adapter” বানানো: clean আর hexagonal architecture, port, adapter আর domain-driven design-এর ধারণা।

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

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান