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

UML behaviour diagram — sequence, activity আর state machine diagram

UML Behaviour Diagrams — Sequence Diagrams, Combined Fragments, Hierarchical State Machines and Activity Token Flow

Taught in Bangla, this lesson covers UML behaviour diagrams: sequence diagrams with lifelines, activations, message kinds and combined fragments such as alt, opt, loop, par, break and ref, mapping diagrams to code and traces back to diagrams, state machines with entry, exit and do behaviour, composite states, history and orthogonal regions, activity diagrams with object flow, pins and fork/join token rules, and choosing the right diagram.

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

  • UML ২.৫.১-এর নোটেশনে sequence diagram পড়া ও আঁকা — lifeline, activation, সাত রকম message আর combined fragment
  • Sequence diagram থেকে কোড, আর চালানো কোডের trace থেকে sequence diagram তৈরি করা
  • উন্নত state machine — entry/exit/do, internal বনাম external transition, composite state, history, orthogonal region, choice আর junction
  • Design-স্তরের activity diagram — object flow, pin, fork/join-এর token-নিয়ম আর interruptible region
  • কোন প্রশ্নের জন্য কোন diagram, আর PlantUML/Mermaid দিয়ে diagram-কে Git-এ রাখার মতো লেখায় পরিণত করা

১ · কাঠামোর ছবি থেকে আচরণের ছবিতে

একটি কাল্পনিক অনলাইন চা-পাতার দোকান, নাম “চা-বাগান”। পর্ব ২৯-এ (UML class diagram) দল এর class diagram এঁকেছে: Cart, Order, Payment, Stock — কোন class কার সাথে যুক্ত, কার কাছে কী data. কিন্তু নতুন developer নাদিয়া প্রশ্ন করলেন, “গ্রাহক ‘অর্ডার দিন’ চাপলে প্রথমে কে কাকে ডাকে? টাকা কাটার আগে stock আটকানো হয়, না পরে? পেমেন্ট ব্যর্থ হলে আটকানো stock কি ছেড়ে দেওয়া হয়?” Class diagram এর একটিরও উত্তর দেয় না। সে দেখায় কারা আছে, কে কাকে চেনে; কিন্তু সময়ের সাথে কী ঘটে, তা দেখায় না।

বিয়েবাড়ির কথা ভাবুন। অতিথির তালিকা আর কে কার আত্মীয় — এটা কাঠামো (class diagram). কিন্তু অনুষ্ঠানের দিন বাবুর্চি কখন বাজার চাইবেন, ডেকোরেটর কখন মঞ্চ বুঝিয়ে দেবেন, বর এলে কে গেট ধরবেন — এটা আচরণ। আয়োজকের খাতায় তিন রকম আচরণের লেখা থাকে: “কে কাকে কখন কী বলবে” (sequence), “কাজগুলো কোন ক্রমে, কোনটা একসাথে” (activity), আর “বরযাত্রী এখন কোন অবস্থায় — রওনা, পথে, পৌঁছেছে” (state machine).

UML ২.৫.১-এ (OMG-র প্রকাশ, ২০১৭ সালের ডিসেম্বর) মোট ১৪ রকম diagram: ৭টি structure diagram আর ৭টি behaviour diagram. Behaviour দলে আছে use case, activity আর state machine diagram, আর তাদের একটি উপদল interaction diagram — sequence, communication, timing আর interaction overview diagram. পর্ব ২৩-এ (Requirement modeling) আপনি activity আর state diagram দেখেছেন requirement-এর স্তরে: ব্যবসার প্রক্রিয়া আর একটি অর্ডারের জীবনচক্র। এই পর্বে একই নোটেশন যাবে design-এর স্তরে — যেখানে বাক্সগুলো আসল object, তীরগুলো আসল method call, আর প্রতিটি নিয়ম কোডে কোথাও বসবে।

Diagramযে প্রশ্নের উত্তর দেয়কেন্দ্রে কী
Sequence diagramএই একটি scenario-তে কে কাকে, কোন ক্রমে, কোন message পাঠায়?কয়েকটি object, একটি সময়-রেখা
State machineএই object-টি কোন অবস্থায় কোন event-এ কী করে?একটি object-এর সারা জীবন
Activity diagramকাজটি কোন ধাপে, কোন শর্তে, কোনটা সমান্তরালে এগোয়, কোন data কোথায় যায়?একটি algorithm বা workflow

একটি সহজ পরীক্ষা মনে রাখুন: diagram-এর উল্লম্ব দিকে সময় থাকলে সেটা sequence; বাক্সগুলো যদি “অবস্থা” হয় (Paid, Shipped), সেটা state machine; বাক্সগুলো যদি “কাজ” হয় (পেমেন্ট নিন, প্যাক করুন), সেটা activity. শেষ অংশে (অংশ ৮) এই বাছাই আরও বিস্তারিত।

২ · Sequence diagram — lifeline, activation আর message

Sequence diagram পড়া হয় ওপর থেকে নিচে: ওপরে সময় শুরু, নিচে শেষ। পাশাপাশি দাঁড়ানো প্রতিটি অংশগ্রহণকারীর নিচে একটি খাড়া ড্যাশ রেখা — lifeline. মাথার বাক্সে লেখা থাকে নাম:Type, যেমন c:Cart, অথবা নাম না জানলে শুধু :Cart (কোলন রেখে)। UML ২-এ মাথার নামের নিচে দাগ (underline) দেওয়া বাধ্যতামূলক নয়; UML ১-এর object-নোটেশন থেকে এই অভ্যাস অনেক বইতে রয়ে গেছে। Lifeline-এর ওপর সরু আয়তক্ষেত্র — activation bar (UML-এর আনুষ্ঠানিক নাম execution specification) — দেখায় object-টি কখন কাজ করছে, অর্থাৎ তার একটি method কখন চলছে।

এর পরে message, আর এখানেই সবচেয়ে বেশি ভুল হয়। UML ২.৫.১ অনুযায়ী তীরের রেখা আর মাথা মিলিয়ে অর্থ:

Messageআঁকার নিয়মঅর্থ ও কোডে রূপ
Synchronousসরল রেখা, ভরাট ত্রিভুজ মাথাপ্রেরক উত্তর না পাওয়া পর্যন্ত অপেক্ষা করে — সাধারণ method call, await করা call
Asynchronousসরল রেখা, খোলা মাথা (দুটি দাগ)পাঠিয়েই প্রেরক নিজের কাজে চলে যায় — queue-তে event রাখা, await ছাড়া promise ছুড়ে দেওয়া
Replyড্যাশ রেখা, খোলা মাথা (spec ভরাট মাথাও অনুমোদন করে)Synchronous call-এর ফেরত মান — return
Createড্যাশ রেখা, খোলা মাথা, সরাসরি নতুন lifeline-এর মাথার বাক্সে গিয়ে লাগেnew Order(…) — তাই নতুন object-এর বাক্স অন্যদের চেয়ে নিচে আঁকা হয়
DestroyLifeline-এর শেষে বড় ×Object-টির জীবন শেষ — এর পরে তাকে আর কোনো message পাঠানো যাবে না
Self-messageএকই lifeline থেকে বেরিয়ে আবার সেখানে ফেরা তীর, ওপরে আরেকটি activation barthis.validate() — নিজের আরেকটি method
Found / lostFound: শুরুতে ছোট ভরাট বৃত্ত; lost: শেষে ছোট ভরাট বৃত্তFound — প্রেরক অজানা বা diagram-এর বাইরে (একটি timer, hardware interrupt); lost — message পাঠানো হলো, কিন্তু গন্তব্যে পৌঁছায়নি বা গন্তব্য diagram-এর বাইরে

Reply-র মাথা নিয়ে একটু দ্বিমত আছে। UML ২.৫.১-এর লেখা অনুযায়ী reply-র ড্যাশ রেখায় খোলা বা ভরাট, দুই রকম মাথাই চলে; বেশিরভাগ বই আর tool খোলা মাথা আঁকে। তবে ভরাট মাথার সরল রেখা সবসময়ই synchronous call — এটা কখনো reply নয়। পরীক্ষায় বা review-তে এই পার্থক্যটাই দেখা হয়।

এবার “চা-বাগান”-এর একটি সহজ অর্ডার ধাপে ধাপে আঁকি। নিচে diagram-এর পাশাপাশি সেই কোডটিও আছে, যা এই message-গুলো তৈরি করে — প্রতিটি ধাপে কোন লাইন কোন তীর বানাল, তা মিলিয়ে দেখুন।

অর্ডার দেওয়ার sequence diagram, ধাপে ধাপে গ্রাহক:OrderService:Inventory placeOrder(cart) validate(cart) reserve(items) true :Order «create» ORD-31
1class OrderService {2  placeOrder(cart) {3    this.validate(cart);4    if (!this.inventory.reserve(cart.items))5      throw new Error("out of stock");6    const order = new Order(cart);7    return order.id;          // "ORD-31"8  }9}
  1. অংশগ্রহণকারী। বাঁয়ে actor গ্রাহক (কাঠির মানুষ), তারপর দুটি object: :OrderService আর :Inventory. নাম নেই, শুধু type — তাই কোলন দিয়ে শুরু। প্রতিটির নিচে ড্যাশ lifeline. এখনো কোনো message নেই; কোডে শুধু class-টি আছে।
  2. placeOrder(cart) — synchronous call. সরল রেখা, ভরাট মাথা: গ্রাহকের দিক (UI) উত্তরের অপেক্ষায় থাকবে। OrderService-এর lifeline-এ সবুজ activation bar শুরু হলো — method-টি চলছে, আর call ফেরা পর্যন্ত চলবে (কোডের লাইন ২)।
  3. validate(cart) — self-message. তীর বেরিয়ে একই lifeline-এ ফিরেছে, আর পুরোনো bar-এর ওপরে একটি ছোট কমলা bar বসেছে: একই object-এর ভেতরে দ্বিতীয় একটি method চলছে (লাইন ৩)। Reply তীর আঁকা হয়নি — ফেরত মান না থাকলে বা স্পষ্ট হলে অনেক দল reply বাদ দেয়, তাতে UML ভাঙে না।
  4. reserve(items) আর reply true. Inventory-র activation শুধু call-টুকু জুড়ে। ফেরার তীর ড্যাশ, খোলা মাথা, লেবেলে ফেরত মান। কোডের লাইন ৪–৫-এর throw এই diagram-এ নেই: এটা শুধু সফল scenario. ব্যর্থ পথ দেখাতে লাগবে alt বা break (অংশ ৩)।
  5. «create» — নতুন object জন্ম নিল। ড্যাশ তীর সোজা :Order-এর মাথার বাক্সে লেগেছে, তাই বাক্সটি অন্যদের চেয়ে নিচে, ঠিক সৃষ্টির মুহূর্তে। এর ওপরে Order-এর কোনো lifeline নেই, কারণ তখন সে ছিলই না (লাইন ৬)।
  6. Reply ORD-31. ড্যাশ তীর গ্রাহকের কাছে ফিরল, OrderService-এর activation শেষ। পুরো diagram পড়লে কোডটি প্রায় হুবহু লেখা যায় — আবার কোড থেকেও diagram. এই দুই দিকের মিল পরের অংশগুলোতে আরও জোরালো হবে। খেয়াল করুন: :Order ধ্বংস হয়নি, তাই lifeline-এর শেষে × নেই।

এক সাধারণ প্রশ্ন: কোন object-গুলো diagram-এ রাখবেন? নিয়ম — শুধু সেগুলো, যারা এই scenario-র প্রশ্নের উত্তরে লাগে। Logging, getter-setter বা প্রতিটি utility call আঁকলে diagram একটি অপঠনীয় ঘন জাল হয়ে যায়। একটি diagram-এ পাঁচ-ছয়টির বেশি lifeline আর পনেরো-বিশটির বেশি message দেখলে ভাবুন, ভাগ করা যায় কি না (ref, অংশ ৩)।

মূল ধারণা

একটি sequence diagram একটি বা কয়েকটি নির্দিষ্ট scenario-র ছবি — সব সম্ভাব্য আচরণের নয়। একটি object-এর সব অবস্থায় সব event-এ কী হয়, তা বলার কাজ state machine-এর (অংশ ৫)।

৩ · Combined fragment — alt, opt, loop, par, break আর ref

বাস্তব কোডে if আছে, loop আছে, ব্যর্থতা আছে। Sequence diagram-এ এগুলো দেখানো হয় combined fragment দিয়ে: কয়েকটি message ঘিরে একটি আয়তক্ষেত্র, বাঁ-ওপরের কোণে একটি পাঁচকোনা লেবেলে interaction operator-এর নাম। আয়তক্ষেত্রটি ড্যাশ রেখায় এক বা একাধিক operand-এ ভাগ হতে পারে, আর প্রতিটি operand-এর শুরুতে বন্ধনীতে একটি guard: [paid].

Operatorঅর্থকোডে সাধারণ রূপ
altকয়েকটি বিকল্প operand, যার guard সত্য শুধু সেটি চলে; [else] লেখা যায়if … else if … else, switch
optএকটিমাত্র operand, guard সত্য হলে চলে, না হলে কিছুই নয়else-বিহীন if
loopOperand বারবার চলে; loop(min,max) সীমা দেয়, loop একা মানে ০ থেকে অসীম, loop(3) মানে ঠিক ৩ বার; guard-ও দেওয়া যায়for, while
parOperand-গুলো একসাথে চলে; এক operand-এর ভেতরের ক্রম ঠিক থাকে, কিন্তু দুই operand-এর message যেকোনো ক্রমে মিশতে পারেPromise.all, thread, একসাথে কয়েকটি request
breakGuard সত্য হলে এই operand চলে, তারপর ঘিরে থাকা fragment-এর বাকি অংশ বাদআগেভাগে return, throw, loop-এর break
refএটি আসলে operator নয়, interaction use: অন্য একটি নাম দেওয়া sequence diagram এখানে বসানো হলোআরেকটি function ডাকা, যার ভেতর আলাদা করে আঁকা

UML ২.৫.১-এ মোট ১২টি interaction operator: ওপরের পাঁচটির সাথে seq, strict, neg (এই ক্রম ঘটা নিষেধ), critical (মাঝখানে অন্য কিছু ঢুকতে পারবে না), ignore, consider আর assert. বাস্তবে প্রায় সব diagram-এ প্রথম পাঁচটি আর ref-ই যথেষ্ট। নিচের ছবিতে “চা-বাগান”-এর checkout: প্রতিটি item-এর জন্য stock আটকানো (loop), তারপর টাকা কাটা, আর ফল অনুযায়ী দুই পথ (alt).

Checkout — loop আর alt fragment-সহ sequence diagram

Synchronous calls (filled), an asynchronous message (open, solid), replies (dashed), activation bars and guards

Checkout-এর sequence diagram, loop আর alt-সহ গ্রাহক:Cart:Stock:Payment loop [প্রতিটি item] alt [paid] [declined] checkout() reserve(item) ok charge(total) ok commit() receipt declined error
ছবি ৩০.১ — Loop-এর ভেতরের দুটি message প্রতিটি item-এর জন্য একবার করে ঘটে। Alt-এর দুই operand-এর মধ্যে ঠিক একটি চলে। commit()-এর মাথা খোলা আর রেখা সরল: Cart অপেক্ষা না করেই গ্রাহককে রসিদ দেয়। আর খেয়াল করুন একটি ঘাটতি — declined হলে আটকানো stock ছাড়ার কোনো message নেই। Diagram আঁকতেই design-এর ফাঁকটি চোখে পড়ল।

Guard নিয়ে দুটি নিয়ম মনে রাখুন। প্রথম, guard লেখা হয় সেই lifeline-এর ওপরে, যার data দিয়ে শর্তটি যাচাই হয় — এখানে Cart জানে পেমেন্টের ফল। দ্বিতীয়, alt-এর guard-গুলো পরস্পর-বর্জিত আর সম্পূর্ণ হওয়া উচিত; [amount > 500] আর [amount < 1000] একসাথে সত্য হতে পারে, তাতে কোনটি চলবে তা অস্পষ্ট। নিশ্চিত হতে শেষ operand-এ [else] রাখুন।

par আর ref-এর একটি ছোট উদাহরণ লেখায়: অর্ডার নিশ্চিত হলে SMS আর email একসাথে যায় — par fragment, দুই operand. আর লগইনের পুরো আদান-প্রদান যদি আলাদা diagram “Authenticate”-এ আঁকা থাকে, checkout diagram-এর শুরুতে শুধু একটি ref বাক্স: ref Authenticate. এতে একটি diagram ছোট থাকে, আর লগইনের নিয়ম বদলালে এক জায়গায় বদলাতে হয় — কোডে function বের করে আনার মতোই।

Fragment থেকে test scenario

প্রতিটি combined fragment একটি করে সিদ্ধান্ত। সবগুলো মিলিয়ে diagram-এর শুরু থেকে শেষ পর্যন্ত যতগুলো আলাদা পথ, প্রতিটি একটি test scenario-র প্রার্থী। Loop অসীম পথ বানাতে পারে, তাই একটি সীমা ধরে নিতে হয় — এখানে item ১ বা ২টি। নিচের কোডটি fragment-এর একটি ছোট গাছ থেকে সব পথ গুনে বের করে:

// From combined fragments (loop, opt, alt) to test scenarios: enumerate every path.
const M = s => ({ k: "msg", s });
const checkout = [
  M("Customer->Cart: checkout()"),
  { k: "loop", min: 1, max: 2, guard: "items", body: [M("Cart->Stock: reserve(item)")] },
  { k: "opt", guard: "coupon", body: [M("Cart->Promo: apply(code)")] },
  M("Cart->Pay: charge(total)"),
  { k: "alt", ops: [
    { guard: "paid",     body: [M("Pay-->Cart: ok"), M("Cart-->Customer: receipt")] },
    { guard: "declined", body: [M("Pay-->Cart: declined"), M("Cart-->Customer: error")] } ] }
];
const join = (a, b) => ({ g: [...a.g, ...b.g], m: [...a.m, ...b.m] });
function paths(frags) {                       // all paths through a sequence of fragments
  let acc = [{ g: [], m: [] }];
  for (const f of frags) { const alts = expand(f); acc = acc.flatMap(a => alts.map(o => join(a, o))); }
  return acc;
}
function expand(f) {                          // the alternatives one fragment offers
  const tag = (g, ps) => ps.map(p => join({ g: [g], m: [] }, p));
  if (f.k === "msg") return [{ g: [], m: [f.s] }];
  if (f.k === "opt") return [{ g: ["no " + f.guard], m: [] }, ...tag(f.guard, paths(f.body))];
  if (f.k === "alt") return f.ops.flatMap(op => tag(op.guard, paths(op.body)));
  const out = [];                             // loop(min, max): unroll n = min..max times
  for (let n = f.min; n <= f.max; n++)
    out.push(...tag(f.guard + "=" + n, paths(Array(n).fill(f.body).flat())));
  return out;
}
const all = paths(checkout);
all.forEach((p, i) => console.log("S" + (i + 1) + " [" + p.g.join(", ") + "] " + p.m.length + " msgs"));
console.log("total " + all.length + " = loop 2 x opt 2 x alt 2");
console.log("S8 messages:\n  " + all[7].m.join("\n  "));

আউটপুট:

S1 [items=1, no coupon, paid] 5 msgs
S2 [items=1, no coupon, declined] 5 msgs
S3 [items=1, coupon, paid] 6 msgs
S4 [items=1, coupon, declined] 6 msgs
S5 [items=2, no coupon, paid] 6 msgs
S6 [items=2, no coupon, declined] 6 msgs
S7 [items=2, coupon, paid] 7 msgs
S8 [items=2, coupon, declined] 7 msgs
total 8 = loop 2 x opt 2 x alt 2
S8 messages:
  Customer->Cart: checkout()
  Cart->Stock: reserve(item)
  Cart->Stock: reserve(item)
  Cart->Promo: apply(code)
  Cart->Pay: charge(total)
  Pay-->Cart: declined
  Cart-->Customer: error

পরপর বসানো fragment-এর পথ গুণ হয়: loop-এর ২টি বিকল্প (item ১ বা ২), opt-এর ২টি (coupon আছে বা নেই), alt-এর ২টি — মোট ২ × ২ × ২ = ৮. প্রতিটি সারির guard-তালিকাই একটি test-এর নাম হতে পারে, যেমন “২টি item, coupon-সহ, পেমেন্ট ব্যর্থ”। S8-এর message-তালিকা সেই test-এ কোন call-গুলো ঘটার কথা, তা বলে দেয়। Loop-এর সীমা বাড়ালে সংখ্যা দ্রুত বাড়ে; তখন সব পথ না চালিয়ে boundary value আর equivalence class বেছে নেওয়ার কৌশল লাগে, যা আসবে পর্ব ৪৭-এ (test case নকশা)।

৪ · Diagram থেকে কোড, কোড থেকে diagram

Design-স্তরের sequence diagram-এর আসল শক্তি হলো, এর প্রায় প্রতিটি চিহ্নের একটি নির্দিষ্ট কোড-রূপ আছে। তাই diagram দেখে কোড review করা যায়, আর কোড দেখে diagram যাচাই করা যায়।

Diagram-একোডে (JavaScript-এর উদাহরণ)
A থেকে B-তে synchronous m(x)A-র কোনো method-এর ভেতরে const r = b.m(x); — A-র কাছে B-র reference থাকতে হবে
Reply rB-র m থেকে return r;
Asynchronous publish(e)queue.publish(e); — ফলের অপেক্ষা নেই (await ছাড়া)
«create»new Order(cart)
Self-messagethis.validate(cart)
alt [paid] / [declined]if (res === "ok") { … } else { … }
loop [প্রতিটি item]for (const item of cart.items) { … }
parawait Promise.all([sms.send(id), mail.send(id)])
break [out of stock]if (!ok) return "out-of-stock"; বা throw

এই মিল থেকে design-এর গুণও পড়া যায়। একটি lifeline থেকে যদি প্রায় সব lifeline-এ তীর যায়, ওই class সম্ভবত বেশি কিছু জানে আর বেশি কিছুর ওপর নির্ভর করে — পর্ব ২৫-এর (Modularity, coupling আর cohesion) ভাষায় উঁচু coupling. আর তীরের লম্বা শিকল (A ডাকে B-কে, B-র কাছ থেকে পাওয়া C-কে, C-র কাছ থেকে পাওয়া D-কে) হলো সেই কাঠামো, যার বিরুদ্ধে Law of Demeter সতর্ক করে — পর্ব ২৬ (নকশার নীতি)। প্রতিটি synchronous message আসলে একটি interface-এর method; তার আগে-পরের শর্ত (precondition, postcondition) কীভাবে লিখবেন, তা পর্ব ৩১-এ (Interface আর contract).

চালানো কোড থেকে sequence diagram

উল্টো দিকও সম্ভব, আর অচেনা কোডবেস বুঝতে খুব কাজের: কোডটি চালান, প্রতিটি call রেকর্ড করুন, তারপর রেকর্ড থেকে diagram বানান। নিচের প্রোগ্রাম তিনটি class-এর প্রতিটি method-কে একটি “মোড়ক” (wrapper) দিয়ে ঢেকে দেয়। মোড়কটি একটি stack রাখে — কে এখন ডাকছে — আর প্রতিটি call ও return-কে PlantUML-এর মতো এক লাইনে লেখে।

// Record a real call trace and print it as PlantUML-like sequence text.
let trace = [], stack = ["Customer"];             // stack top = who is calling now
function instrument(name, obj) {
  const proto = Object.getPrototypeOf(obj);
  for (const m of Object.getOwnPropertyNames(proto)) {
    if (m === "constructor") continue;
    obj[m] = function (...args) {                 // wrapper: log call, run, log return
      const caller = stack[stack.length - 1];
      trace.push(caller + " -> " + name + " : " + m + "(" + args.join(", ") + ")");
      stack.push(name);
      const r = proto[m].apply(this, args);
      stack.pop();
      trace.push(name + " --> " + caller + " : " + r);
      return r;
    };
  }
  return obj;
}
class Stock   { reserve(sku, qty) { return qty <= 5; } }
class Payment { charge(amount) { return amount <= 2000 ? "ok" : "declined"; } }
class Checkout {
  constructor(stock, pay) { this.stock = stock; this.pay = pay; }
  placeOrder(sku, qty, price) {
    if (!this.stock.reserve(sku, qty)) return "out-of-stock";
    const total = this.total(qty, price);         // self-call
    return this.pay.charge(total) === "ok" ? "ORD-7" : "payment-failed";
  }
  total(qty, price) { return qty * price; }
}
const co = instrument("Checkout",
  new Checkout(instrument("Stock", new Stock()), instrument("Payment", new Payment())));
for (const [qty, price] of [[2, 350], [3, 900]]) {
  trace = []; co.placeOrder("tea", qty, price);
  console.log("@startuml\n" + trace.join("\n") + "\n@enduml");
}

আউটপুট:

@startuml
Customer -> Checkout : placeOrder(tea, 2, 350)
Checkout -> Stock : reserve(tea, 2)
Stock --> Checkout : true
Checkout -> Checkout : total(2, 350)
Checkout --> Checkout : 700
Checkout -> Payment : charge(700)
Payment --> Checkout : ok
Checkout --> Customer : ORD-7
@enduml
@startuml
Customer -> Checkout : placeOrder(tea, 3, 900)
Checkout -> Stock : reserve(tea, 3)
Stock --> Checkout : true
Checkout -> Checkout : total(3, 900)
Checkout --> Checkout : 2700
Checkout -> Payment : charge(2700)
Payment --> Checkout : declined
Checkout --> Customer : payment-failed
@enduml

দুটি চালানো, দুটি diagram-লেখা। -> হলো synchronous call আর --> ড্যাশ reply — PlantUML-এ ঠিক এই চিহ্নেই আঁকা হয়। Checkout -> Checkout : total(2, 350) সেই self-message, যা stack-এর ওপরে Checkout থাকায় ধরা পড়েছে। দুই আউটপুট পাশাপাশি রাখলে তফাত শুধু শেষ তিন লাইনে, আর সেটাই ছবি ৩০.১-এর alt [paid] / [declined]. অর্থাৎ একটি trace = একটি scenario; কয়েকটি trace মিলিয়ে তবেই fragment-সহ একটি diagram হয়। আর একটি পথ কখনো চলেনি — stock না থাকার "out-of-stock" — তাই trace থেকে বানানো diagram-এ সেটা থাকবেই না। Trace দেখায় যা ঘটেছে, যা ঘটতে পারে তা নয়।

পেশাদার জগতে একই চিন্তা বড় আকারে: কিছু IDE আর static analysis tool কোড পড়ে sequence diagram আঁকতে পারে, আর অনেকগুলো service-এর system-এ distributed tracing (যেমন OpenTelemetry মানদণ্ডে) প্রতিটি request-এর span রেকর্ড করে, যা দেখতে প্রায় sequence বা timing diagram-এর মতো। বিস্তারিত System Design কোর্সের Observability পর্বে।

Communication আর timing diagram — সংক্ষেপে

Communication diagram (UML ১-এ নাম ছিল collaboration diagram) একই আদান-প্রদান দেখায়, কিন্তু সময়-রেখা ছাড়া: object-গুলো মুক্তভাবে ছড়ানো, যোগাযোগের রেখার পাশে নম্বর দেওয়া message — 1: checkout(), 1.1: reserve(item), 1.2: charge(total). দশমিক নম্বর nesting বোঝায়: ১.১ হলো ১-এর ভেতর থেকে ডাকা। কে কার সাথে যুক্ত, তা এক নজরে বোঝা যায়; কিন্তু ক্রম পড়তে নম্বর খুঁজতে হয়, আর fragment আঁকা যায় না। Sequence আর communication diagram একই তথ্যের দুই রূপ, তাই অনেক tool একটি থেকে অন্যটি বানাতে পারে। Timing diagram এক লাইনে: আড়াআড়ি অক্ষে সময়, আর প্রতিটি lifeline-এর state কখন বদলায় তা ধাপ-আকৃতির রেখায় — embedded আর real-time system-এ, যেখানে “৫০ ms-এর মধ্যে” ধরনের শর্ত জরুরি।

৫ · State machine — entry, exit, do আর তিন রকম transition

পর্ব ২৩-এ state, transition আর event [guard] / effect লেবেল দেখেছেন। Design-স্তরে একটি state শুধু নাম নয়; তার ভেতরে কাজও থাকে। State-এর বাক্সে নামের নিচে আরেকটি ঘর (compartment) রাখা যায়:

  • entry / কাজ — state-এ ঢোকার সময় প্রতিবার একবার চলে, যে তীর দিয়েই ঢুকুন। উদাহরণ: Buffering-এ ঢুকলেই spinner দেখানো।
  • exit / কাজ — বেরোনোর সময় প্রতিবার একবার, যে তীর দিয়েই বেরোন। উদাহরণ: spinner লুকানো।
  • do / কাজ — entry শেষ হলে শুরু হয়, state-এ থাকা অবস্থায় চলতে থাকে; শেষ হওয়ার আগে state ছাড়তে হলে মাঝপথে থামিয়ে দেওয়া হয়। উদাহরণ: do / downloadChunk. Do শেষ হলে একটি completion event তৈরি হয়, আর লেবেলবিহীন তীর সেটার জন্যই অপেক্ষা করে।

এখানেই entry আর effect-এর তফাত: effect একটি নির্দিষ্ট তীরের কাজ, entry একটি state-এর. কোনো state-এ পাঁচটি তীর ঢুকলে আর পাঁচটির effect-এ একই কাজ লেখা থাকলে, সেটা entry-তে সরিয়ে দিন — কোডে duplicate কমানোর মতোই।

TransitionআঁকাExit/entry চলে?
InternalState-এর ভেতরের ঘরে লেখা: volumeUp / vol++না — state-এর বাইরে যাওয়াই হয় না
External (self-transition-সহ)বাক্স থেকে বেরোনো তীর; নিজের দিকে ফিরলে একটি ফাঁস (loop)হ্যাঁ — নিজের দিকে ফিরলেও আগে exit, তারপর entry
LocalComposite state-এর সীমা থেকে তার নিজের ভেতরের substate-এComposite-এর exit/entry চলে না, শুধু ভেতরের substate-গুলোর

একটি তীর চললে কাজের ক্রম UML-এ নির্দিষ্ট: উৎস state-এর exit → transition-এর effect → লক্ষ্য state-এর entry. তারপর লক্ষ্যে do শুরু। আরেকটি মৌলিক নিয়ম run-to-completion: state machine একবারে একটি event নেয়, আর তার সব কাজ (exit, effect, entry) শেষ হলে তবেই পরের event. মাঝখানে আসা event queue-তে অপেক্ষা করে। এজন্যই entry বা effect-এ দীর্ঘ কাজ (নেটওয়ার্ক call) রাখা বিপজ্জনক — পুরো machine আটকে যায়; সেসব do-তে বা আলাদা asynchronous কাজে রাখুন।

Choice আর junction — দুটি সিদ্ধান্ত-বিন্দু

একটি তীর মাঝপথে ভাগ হতে পারে, শর্ত অনুযায়ী। UML-এ এর দুটি pseudostate (আসল state নয়, পথের একটি বিন্দু):

◇ Choice

হীরা আকার। Guard যাচাই হয় সেখানে পৌঁছানোর পরে, অর্থাৎ আগের অংশের effect চলে যাওয়ার পরে। তাই effect-এ হিসাব করা নতুন মান guard দেখতে পায়। অন্তত একটি guard সত্য হতেই হবে — নিরাপদ থাকতে [else] রাখুন।

dynamic conditional branch

● Junction

ছোট ভরাট বৃত্ত। পুরো পথটি চলার আগেই সব guard একসাথে যাচাই হয়; কোনো পথ না মিললে transition-ই হয় না। কয়েকটি তীর এক করতে বা ভাগ করতে ব্যবহার হয়।

static conditional branch

তফাতটি একটি উদাহরণে: Cart-এর submit / total := computeTotal() তীরের পরে একটি সিদ্ধান্ত — [total > 5000] হলে NeedsApproval, [else] হলে AwaitingPayment. Choice হলে guard নতুন হিসাব করা total দেখবে। Junction হলে guard যাচাই হবে effect চলার আগে, অর্থাৎ পুরোনো total দিয়ে — এক সূক্ষ্ম bug, যা diagram-এ একটি বৃত্ত বনাম হীরার তফাত মাত্র।

State table — একই machine, সারণিতে

State machine-কে সারণিতেও লেখা যায়: সারিতে state, কলামে event, ঘরে “পরের state / effect”. ছবিতে যা চোখ এড়ায়, সারণিতে তা ধরা পড়ে — প্রতিটি ঘর আপনাকে জিজ্ঞেস করে, “এই অবস্থায় এই event এলে কী হবে?” ফাঁকা ঘর মানে হয় ইচ্ছাকৃত উপেক্ষা, নয়তো ভুলে যাওয়া requirement. নিচে পরের অংশের মিডিয়া প্লেয়ারের state table (একটি কাল্পনিক অনলাইন অডিও-বই অ্যাপ):

State \ Eventplaystallreadypauseresumestop
IdlePlaying / openStream—————
Playing.Normal—Buffering—Paused / saveTime*—Idle / close*
Playing.Buffering——NormalPaused / saveTime*—Idle / close*
Paused————Playing(H) / seekIdle / close

“—” মানে event উপেক্ষিত। তারকা (*) চিহ্নিত ঘরগুলো আসলে একটিই তীর — composite Playing-এর সীমা থেকে বেরোনো — যা দুই substate-ই উত্তরাধিকারে পায়। সারণিতে তা দুবার লিখতে হলো; ছবিতে একবার আঁকলেই চলে। এটাই composite state-এর সুবিধা, যা এখন দেখব। State table-এর প্রতিটি ঘর একটি test case-এর প্রার্থী; test case নকশার কৌশলগুলো আসবে পর্ব ৪৭-এ।

৬ · Composite state, history আর orthogonal region

১৯৮৭ সালে David Harel তাঁর statecharts প্রবন্ধে সাধারণ state diagram-এর একটি বড় সমস্যা দেখান: জটিল system-এ state-এর সংখ্যা বিস্ফোরিত হয়, আর একই তীর বহু জায়গায় আঁকতে হয়। তাঁর সমাধান, যা পরে UML-এ এসেছে, তিনটি: state-এর ভেতরে state (nesting), history, আর একসাথে চলা অংশ (orthogonality).

  • Composite state: একটি state, যার ভেতরে নিজস্ব ছোট state machine — substate-গুলো। ভেতরে থাকা মানে বাইরেরটিতেও থাকা: Buffering-এ থাকলে প্লেয়ার একই সাথে Playing-এও। Composite-এর সীমা থেকে বেরোনো একটি তীর (pause) ভেতরের সব substate-এর জন্য প্রযোজ্য।
  • Shallow history (বৃত্তে H): Composite থেকে বেরিয়ে আবার H-এর দিকে ফিরলে, ঠিক যে substate-এ ছিলেন সেখানে ফেরেন — তবে শুধু এক স্তর; সেই substate নিজে composite হলে তার ভেতরে যায় তার initial state-এ। আগে কখনো ঢোকা না হলে H-এর নিজের default তীর, তা না থাকলে composite-এর initial state.
  • Deep history (H*): সব স্তর মনে রাখে — একদম ভেতরের state পর্যন্ত আগের মতো ফেরায়।
  • Orthogonal region: একটি composite state ড্যাশ রেখায় কয়েকটি region-এ ভাগ, প্রতিটি region-এ একটি করে substate একসাথে সক্রিয়। যেমন Playing-এর ভেতরে এক region-এ Normal/Buffering, আরেক region-এ Muted/Audible.

মিডিয়া প্লেয়ার — composite state আর shallow history

Composite state with entry/exit, an initial pseudostate inside, a history pseudostate (H) and a transition from the composite boundary

মিডিয়া প্লেয়ারের composite state machine, history-সহ Idle Playing entry / startClock exit / stopClock Normal Buffering entry / spin exit / unspin H Paused play stop stall ready pause resume
ছবি ৩০.২ — pause তীরটি composite-এর সীমা থেকে বেরিয়েছে, তাই Normal বা Buffering, যেখানেই থাকুন, কাজ করে। resume আসে H-এ: আগে যে substate-এ ছিলেন, প্লেয়ার সেখানে ফেরে। play আসে composite-এর সীমায়, তাই সবসময় ভেতরের initial state Normal-এ। একই H-এ বাঁধা, দুই ভিন্ন তীর — ফল ভিন্ন।

এবার এই machine-কে কয়েকটি event দিয়ে চালাই, আর প্রতিটি event-এ কোন exit, কোন effect, কোন entry কোন ক্রমে চলে, তা দেখি। সবচেয়ে বেশি নজর দিন শেষ সারিতে — history কখন বদলায়।

মিডিয়া প্লেয়ার (ছবি ৩০.২)
Event
কোন তীর
কাজের ক্রম
সক্রিয় state
history[Playing]
  1. শুরু। Initial বিন্দু থেকে প্লেয়ার Idle-এ। Playing-এ কখনো ঢোকা হয়নি, তাই history খালি।
  2. play. Idle থেকে বেরোনো (exit Idle), তীরের effect /openStream, তারপর বাইরে থেকে ভেতরে ঢোকা: আগে composite Playing-এর entry (startClock), তারপর ভেতরের initial state Normal-এর entry. তীরটি সীমায় এসেছে, H-এ নয়, তাই initial-ই।
  3. stall. Normal থেকে Buffering — দুটিই Playing-এর ভেতরে। তাই শুধু exit Normal আর entry Buffering (spinner দেখা যায়)। Playing-এর exit বা entry চলেনি, ঘড়িও থামেনি। History এখনো খালি — কারণ Playing থেকে বেরোনোই হয়নি।
  4. pause. তীরটি Playing-এর সীমা থেকে, তাই আগে ভেতরের exit Buffering (unspin), তারপর বাইরের exit Playing (stopClock), তারপর effect /saveTime, তারপর entry Paused. বেরোনোর মুহূর্তে history-তে লেখা হলো: Playing-এর শেষ সক্রিয় substate ছিল Buffering.
  5. ready. নেটওয়ার্ক ঠিক হলো, কিন্তু প্লেয়ার এখন Paused-এ, আর Paused-এর কোনো ready তীর নেই। Event উপেক্ষিত — কোনো exit, effect বা entry নেই। History বদলায়নি।
  6. resume. তীর যায় H-এ। exit Paused, effect /seek, entry Playing, তারপর history মনে রেখেছে Buffering — তাই entry Buffering, Normal নয়। ব্যবহারকারীর চোখে: থামানোর সময় যেমন buffering চলছিল, ফিরেও তেমনই। (খেয়াল করুন: ধাপ ৫-এর ready হারিয়ে গেছে, তাই প্লেয়ার এখন পরের ready-র অপেক্ষায় buffering-এ। হারাতে না চাইলে UML-এ Paused-এর ভেতরে ready / defer লিখে event-টি পরের state-এর জন্য রেখে দেওয়া যায় — একে বলে deferred event.)
  7. ready. এবার Buffering-এ ready-র তীর আছে: exit Buffering, entry Normal. History-তে কিন্তু এখনো Buffering লেখা! History বদলায় কেবল Playing থেকে বেরোনোর সময়, ভেতরে ঘোরাঘুরিতে নয়। পরের pause-এ সেটা Normal হবে।

একই নিয়ম একটি ছোট runner-এ। প্রতিটি state-এর parent জানা আছে; একটি event এলে কোড সবচেয়ে ভেতরের যে state ওই event সামলায় সেটা খোঁজে, উৎস আর লক্ষ্যের সাধারণ পূর্বপুরুষ (common ancestor) পর্যন্ত exit করে, effect চালায়, তারপর লক্ষ্য পর্যন্ত entry করে।

// Hierarchical state machine: entry/exit order, effects and a shallow history (H).
const PARENT = { Idle: null, Playing: null, Paused: null, Normal: "Playing", Buffering: "Playing" };
const INITIAL = { Playing: "Normal" };              // default substate of the composite
const T = {                                         // "source:event" -> [target, effect]
  "Idle:play": ["Playing", "openStream"],   "Normal:stall": ["Buffering", ""],
  "Buffering:ready": ["Normal", ""],        "Playing:pause": ["Paused", "saveTime"],
  "Paused:resume": ["Playing.H", "seek"],   "Playing:stop": ["Idle", "close"],
  "Paused:stop": ["Idle", "close"]
};
const history = {};                                 // composite -> last active child
const path = s => (s ? [...path(PARENT[s]), s] : []); // [outermost, ..., s]
let leaf = "Idle";
function fire(ev) {
  let src = leaf;
  while (src && !T[src + ":" + ev]) src = PARENT[src]; // innermost state that handles ev
  if (!src) return console.log(ev.padEnd(7) + "ignored in " + leaf);
  const [spec, effect] = T[src + ":" + ev], [tgt, viaH] = spec.split(".");
  const from = path(leaf), to = path(tgt), out = [];
  let k = 0;                                        // length of the common, untouched prefix
  while (k < from.length && k < to.length && from[k] === to[k] && from[k] !== src) k++;
  for (let i = from.length - 1; i >= k; i--) {      // exit: innermost first
    if (i + 1 < from.length) history[from[i]] = from[i + 1];
    out.push("exit " + from[i]);
  }
  if (effect) out.push("/" + effect);               // effect runs between exits and entries
  for (let i = k; i < to.length; i++) out.push("entry " + to[i]); // entry: outermost first
  let s = tgt, child = (viaH && history[tgt]) || INITIAL[tgt];
  while (child) { out.push("entry " + child); s = child; child = INITIAL[child]; }
  leaf = s;
  console.log(ev.padEnd(7) + out.join(", ") + "  => " + path(leaf).join("."));
}
["play", "stall", "pause", "ready", "resume", "ready", "pause", "resume", "stop"].forEach(fire);

আউটপুট:

play   exit Idle, /openStream, entry Playing, entry Normal  => Playing.Normal
stall  exit Normal, entry Buffering  => Playing.Buffering
pause  exit Buffering, exit Playing, /saveTime, entry Paused  => Paused
ready  ignored in Paused
resume exit Paused, /seek, entry Playing, entry Buffering  => Playing.Buffering
ready  exit Buffering, entry Normal  => Playing.Normal
pause  exit Normal, exit Playing, /saveTime, entry Paused  => Paused
resume exit Paused, /seek, entry Playing, entry Normal  => Playing.Normal
stop   exit Normal, exit Playing, /close, entry Idle  => Idle

প্রথম ছয় লাইন stepper-এর ছয়টি event-এর সাথে হুবহু মেলে। entry Playing মানে Playing-এর entry কাজ চলল (ছবিতে startClock). সপ্তম আর অষ্টম লাইনে history-র দ্বিতীয় রূপ: এবার Playing থেকে বেরোনোর সময় সক্রিয় ছিল Normal, তাই resume ফেরায় Normal-এ। শেষ লাইনে stop-এর তীর Normal-এ নেই, আছে তার parent Playing-এ — কোড while loop দিয়ে ওপরে উঠে সেটা খুঁজে পেয়েছে। ঠিক এভাবেই composite-এর একটি তীর সব substate-এর কাজ করে। কোডে state machine-এর এই নিয়মগুলো object দিয়ে সাজানোর design pattern (State pattern) আসবে পর্ব ৩৬-এ।

Orthogonal region আর state-বিস্ফোরণ

ধরুন প্লেয়ারের তিনটি স্বাধীন দিক: playback (Normal/Buffering — ২টি), গুণমান (Low/Medium/High — ৩টি), আর শব্দ (Muted/Audible — ২টি)। Region ছাড়া একটি সমতল machine-এ প্রতিটি সম্ভাব্য মিশ্রণ আলাদা state: ২ × ৩ × ২ = ১২টি state, আর “mute” তীর আঁকতে হবে ৬ জোড়া state-এর মধ্যে। তিনটি orthogonal region দিলে state লাগে ২ + ৩ + ২ = ৭টি, আর প্রতিটি region নিজের তীর নিজে রাখে। সাধারণভাবে k-টি করে state-এর n-টি স্বাধীন দিক থাকলে সমতলে kⁿ, region-এ k·n. স্বাধীন দিক যত বাড়ে, পার্থক্য তত সূচকীয় হারে বাড়ে — Harel ঠিক এই state explosion-এর সমাধান খুঁজছিলেন।

লেখার আকারে, PlantUML-এর নথি অনুযায়ী ছবি ৩০.২ মোটামুটি এভাবে লেখা যায় (Playing[H] মানে Playing-এর shallow history; deep হলে [H*]):

উদাহরণ · player.puml

@startuml
[*] --> Idle
Idle --> Playing : play / openStream
state Playing {
  [*] --> Normal
  Normal --> Buffering : stall
  Buffering --> Normal : ready
}
Playing : entry / startClock
Playing : exit / stopClock
Buffering : entry / spin
Buffering : exit / unspin
Playing --> Paused : pause / saveTime
Paused --> Playing[H] : resume / seek
Playing --> Idle : stop / close
@enduml

PlantUML-এ composite-এর ভেতরে -- লিখে region ভাগ করা যায়। Mermaid-এর stateDiagram-v2-তে composite আর region (--) আছে, কিন্তু লেখার সময়ে history pseudostate-এর আলাদা চিহ্ন নেই বলেই আমাদের জানা — তাই history-নির্ভর নকশা Mermaid-এ আঁকলে একটি note দিয়ে বুঝিয়ে দিন। দুটি tool-এর নিয়মই version-ভেদে বদলায়; নিজের version-এ render করে মিলিয়ে নিন।

৭ · Design-স্তরের activity diagram — object flow, pin আর token

পর্ব ২৩-এর activity diagram ছিল ব্যবসার প্রক্রিয়া: কোন ধাপ কার দায়িত্ব। Design-স্তরে একই ছবি একটি algorithm বা workflow-এর ভেতর দেখায়, আর তখন দুটি জিনিস জরুরি হয়ে ওঠে — data কোথা থেকে কোথায় যায়, আর সমান্তরাল অংশগুলো ঠিক কীভাবে মেলে। UML ২-এ activity-র অর্থ token দিয়ে সংজ্ঞায়িত: একটি action চলে তখনই, যখন তার প্রতিটি আগত তীরে token অপেক্ষা করছে; শেষ হলে প্রতিটি বহির্গামী তীরে token দেয়।

  • Control flow বনাম object flow: সাধারণ তীর শুধু “এরপর এটা” বোঝায় (control token). Object flow-এর তীর একটি data বহন করে (object token) — যেমন একটি Order object.
  • Object node: আয়তক্ষেত্র, ভেতরে type: Order, প্রয়োজনে অবস্থাসহ Order [paid]. দুটি action-এর মাঝে data কোথায় অপেক্ষা করে, তা দেখায়।
  • Pin: Action-এর গায়ে লাগানো ছোট বর্গ, পাশে নাম ও type — বাঁয়ে input pin (cart: Cart), ডানে output pin (order: Order). Pin হলো action-এর parameter আর return value; কোডে function signature-এর সরাসরি ছবি।
  • Decision বনাম merge: দুটিই হীরা। Decision-এ এক তীর ঢোকে, কয়েকটি guard-যুক্ত তীর বেরোয়, token যায় একটিতে। Merge-এ কয়েকটি তীর ঢোকে, একটি বেরোয়, আর যে token আসে তা অপেক্ষা না করে চলে যায়।
  • Fork আর join: Fork একটি token নিয়ে প্রতিটি বহির্গামী তীরে একটি করে কপি দেয়। Join প্রতিটি আগত তীরে token-এর অপেক্ষা করে, সব এলে সেগুলো নিয়ে একটি token ছাড়ে।
  • Activity final বনাম flow final: Activity final (বৃত্তের ভেতরে ভরাট বৃত্ত)-এ একটি token পৌঁছালেই পুরো activity শেষ, অন্য সব token মুছে যায়। Flow final (বৃত্তে ×) শুধু সেই একটি token-কে শেষ করে।
  • Interruptible region: ড্যাশ রেখার গোল কোণের আয়তক্ষেত্র। ভেতরের একটি accept-event action (যেমন “গ্রাহক বাতিল করলেন”) থেকে বিদ্যুৎ-চমক আকৃতির (zigzag) তীর বাইরে গেলে, সেই মুহূর্তে region-এর ভেতরের সব token মুছে যায় — প্যাকিং আর পেমেন্ট দুটোই থেমে যায়, ধারা যায় “refund”-এ।

নিচে “চা-বাগান”-এর অর্ডার প্রক্রিয়ার একটি অংশ: fork-এর পরে টাকা কাটা (২ tick সময় লাগে) আর প্যাক করা (৩ tick) একসাথে, join-এর পরে পাঠানো (১ tick). কমলা বিন্দুগুলো token.

Fork আর join-এ token-এর প্রবাহ charge(২ tick) pack(৩ tick) ship (১ tick) forkjoin cp
Token-এর হিসাব
সময়
চলছে
Join-এ অপেক্ষায়
মোট token
  1. t=0 — একটি token. শুরুর বিন্দুতে একটি control token. এখনো কিছু চলছে না।
  2. t=1 — fork কপি করল। একটি token ঢুকল, দুটি বেরোল — একটি charge-এ, একটি pack-এ। দুটি action একসাথে শুরু। Fork কোনো শর্ত দেখে না; সব শাখাই সবসময় পায়।
  3. t=3 — join অপেক্ষা করছে। charge ২ tick পরে শেষ, তার token এখন c-তীরে join-এর মুখে। কিন্তু p-তীর খালি — pack এখনো চলছে। Join কিছুই ছাড়ে না। টাকা কাটা হয়েছে, কিন্তু প্যাক শেষ না হলে পাঠানো যাবে না।
  4. t=4 — join চলল। pack শেষ (৩ tick), p-তীরেও token. এখন দুই তীরেই token আছে, join দুটোকে নিয়ে একটি token ছাড়ল; ship শুরু। মোট token আবার ১.
  5. t=5 — শেষ। ship শেষ, token activity final-এ, পুরো activity শেষ। Join-এর জায়গায় merge থাকলে কী হতো? t=3-এ charge-এর token একাই পার হয়ে ship চালু করত, t=4-এ pack-এর token আবার — ship দুবার. নিচের কোড ঠিক এটাই দেখায়।
# Token game: a join waits for a token on EVERY incoming edge; a merge does not.
from collections import Counter

DUR = {"charge": 2, "pack": 3, "ship": 1}        # ticks each action takes
IN = {"charge": "f1", "pack": "f2", "ship": "g"}  # incoming edge of each action
OUT = {"charge": "c", "pack": "p", "ship": "end"}

def run(gate):
    print(f"== gate before ship is a {gate}")
    tok, busy = Counter(start=1), {}
    for t in range(1, 7):
        ev = []
        for a in list(busy):                       # running actions tick down
            busy[a] -= 1
            if busy[a] == 0:
                del busy[a]; tok[OUT[a]] += 1; ev.append(a + " done")
        if tok["start"]:                           # fork: 1 token in, 1 copy per out-edge
            tok["start"] -= 1; tok["f1"] += 1; tok["f2"] += 1; ev.append("fork 1->2")
        if gate == "join":
            while tok["c"] and tok["p"]:           # needs both, consumes both, emits one
                tok["c"] -= 1; tok["p"] -= 1; tok["g"] += 1; ev.append("join fires")
        else:
            for e in ("c", "p"):                   # merge passes every token on its own
                while tok[e]:
                    tok[e] -= 1; tok["g"] += 1; ev.append("merge passes " + e)
        for a in DUR:                              # an action starts when its token waits
            if tok[IN[a]] and a not in busy:
                tok[IN[a]] -= 1; busy[a] = DUR[a]; ev.append("start " + a)
        wait = {e: tok[e] for e in ("c", "p") if tok[e]}
        print(f"t={t}: {', '.join(ev) or '-'} | waiting at gate {wait}")
    print(f"ship ran {tok['end']} time(s)\n")

run("join")
run("merge")

আউটপুট:

== gate before ship is a join
t=1: fork 1->2, start charge, start pack | waiting at gate {}
t=2: - | waiting at gate {}
t=3: charge done | waiting at gate {'c': 1}
t=4: pack done, join fires, start ship | waiting at gate {}
t=5: ship done | waiting at gate {}
t=6: - | waiting at gate {}
ship ran 1 time(s)

== gate before ship is a merge
t=1: fork 1->2, start charge, start pack | waiting at gate {}
t=2: - | waiting at gate {}
t=3: charge done, merge passes c, start ship | waiting at gate {}
t=4: pack done, ship done, merge passes p, start ship | waiting at gate {}
t=5: ship done | waiting at gate {}
t=6: - | waiting at gate {}
ship ran 2 time(s)

Join-এর ক্ষেত্রে t=3-এ {'c': 1} — charge-এর token দরজায় দাঁড়িয়ে, কারণ pack-এর token আসেনি। t=4-এ দুটো একসাথে নিয়ে join একবার চলে, ship একবার। Merge-এর ক্ষেত্রে কেউ অপেক্ষা করে না: t=3-এ charge-এর token সোজা ship চালু করে (প্যাক শেষ হওয়ার আগেই!), আর t=4-এ pack-এর token দ্বিতীয়বার — ship ran 2 time(s). বাস্তবে এর মানে একই অর্ডারের দুটি ডেলিভারি। উল্টো ভুলটিও পরিচিত: decision-এর পরে join বসালে, decision কেবল একটি শাখায় token দেয়, অথচ join সব শাখার অপেক্ষা করে — activity চিরকাল আটকে থাকে (deadlock). নিয়মটি মুখস্থ রাখুন: fork-এর জোড়া join, decision-এর জোড়া merge. আসল UML-এ token-এর নিয়মে আরও খুঁটিনাটি আছে (যেমন join-এর joinSpec, object token-এর queue); এই simulation শুধু মূল ধারণাটুকু দেখায়।

সমান্তরাল কাজের এই সমস্যাগুলো কোডেও একই রূপে আসে: Promise.all হলো join, আর দুটি callback-এর যেকোনো একটিতে “পরের ধাপ” ডাকা হলো merge-এর ভুল। Thread আর synchronization-এর দিকটি বিস্তারিত দেখতে Operating Systems কোর্সের Thread পর্ব।

৮ · কোন প্রশ্নে কোন diagram — আর diagram-কে লেখায় রাখা

সব diagram আঁকার দরকার নেই, দরকার সঠিকটি। শুরু করুন প্রশ্ন দিয়ে, diagram দিয়ে নয়:

আপনার প্রশ্নDiagramসতর্কতা
একটি request-এ কোন object/service কাকে কোন ক্রমে ডাকে?Sequenceএকটি scenario-তেই আটকে থাকুন; সব শাখা আঁকতে গেলে state machine বা activity ভাবুন
কে কার সাথে কথা বলে — যোগাযোগের জাল?Communicationক্রম পড়া কঠিন
একটি object কোন অবস্থায় কোন event গ্রহণ করে, কোনটা প্রত্যাখ্যান?State machine (+ state table)State মানে “অপেক্ষার অবস্থা”, কাজের ধাপ নয়
Algorithm বা workflow-এর ধাপ, শাখা, সমান্তরাল অংশ আর data-প্রবাহ?ActivityFork-join আর decision-merge জোড়া মিলিয়ে নিন
সময়ের সীমা — “৫০ ms-এর মধ্যে সাড়া”?Timingসাধারণ business app-এ কম লাগে
কয়েকটি scenario কোন ক্রমে মিলে একটি বড় ধারা?Interaction overviewভেতরের নোড হলো ref করা sequence diagram

একটি সাধারণ ভুল হলো state machine-এ কাজের ধাপকে state বানানো: “ValidateCart”, “ChargeCard”, “SendEmail” — এগুলো activity-র action. State-এর নাম সাধারণত বিশেষণ বা অবস্থা: Pending, Paid, Buffering; সেখানে object কিছুক্ষণ থাকে আর event-এর অপেক্ষা করে।

Diagram as code — Git-এ রাখার মতো

ছবির ফাইল (PNG) Git-এ রাখলে diff পড়া যায় না, review-ও কঠিন। তাই অনেক দল diagram লেখে লেখার আকারে — diagram as code — আর tool সেটা থেকে ছবি বানায়। সবচেয়ে প্রচলিত দুটি open-source tool PlantUML আর Mermaid. লেখার সময়ে GitHub আর GitLab-সহ কয়েকটি platform Markdown-এর ভেতরের Mermaid block সরাসরি ছবি করে দেখায়; platform-ভেদে সমর্থন আলাদা। তাতে design বদলালে pull request-এ লাইন-ধরে পরিবর্তন দেখা যায় (পর্ব ১৬, Pull request আর fork). ছবি ৩০.১-এর diagram Mermaid-এ (Mermaid ১১.৪.১-এ render করে যাচাই করা):

উদাহরণ · checkout.mmd

sequenceDiagram
    actor U as Customer
    participant C as Cart
    participant S as Stock
    participant P as Payment
    U->>C: checkout()
    activate C
    loop for each item
        C->>S: reserve(item)
        S-->>C: ok
    end
    C->>P: charge(total)
    activate P
    alt paid
        P-->>C: ok
        C-)S: commit()
        C-->>U: receipt
    else declined
        P-->>C: declined
        C-->>U: error
    end
    deactivate P
    deactivate C

সাবধান, দুই tool-এর চিহ্ন এক নয়। Mermaid-এ ->> হলো ভরাট মাথার সরল রেখা (synchronous), -->> ড্যাশ reply, আর -) খোলা মাথার asynchronous. PlantUML-এ -> synchronous, --> ড্যাশ, আর ->> সরু খোলা মাথা — অর্থাৎ PlantUML-এর ->> আর Mermaid-এর ->> দুই ভিন্ন অর্থের তীর। এক tool-এর অভ্যাসে অন্যটিতে লিখলে diagram চুপচাপ ভুল কথা বলবে। প্রতিটি tool-এর বর্তমান নথি দেখে নিন; আর এখানে দেওয়া PlantUML লেখাগুলো তাদের নথির নিয়ম মেনে লেখা, কিন্তু এই পাতার জন্য render করে যাচাই করা হয়নি — নিজের version-এ একবার চালিয়ে দেখুন।

আরও গভীরে। UML-এর আনুষ্ঠানিক সংজ্ঞা OMG-র Unified Modeling Language ২.৫.১ নথিতে (Interactions, StateMachines আর Activities অধ্যায়); activity-র চালানোর অর্থ আরও কঠোরভাবে দেয় OMG-র আলাদা মানদণ্ড fUML (Foundational UML). State machine-এর মূল চিন্তা David Harel-এর ১৯৮৭ সালের প্রবন্ধ “Statecharts: A Visual Formalism for Complex Systems”, আর XML-এ statechart লেখার একটি W3C মানদণ্ড আছে — SCXML (২০১৫ সালে Recommendation হয় বলে আমাদের জানা)। Sequence diagram-এর পূর্বসূরি telecom জগতের Message Sequence Chart (ITU-T Z.120). হাতে-কলমে বই হিসেবে Martin Fowler-এর UML Distilled (তৃতীয় সংস্করণ ২০০৩) এখনো সংক্ষিপ্ত আর বাস্তবমুখী।

পরীক্ষা ও ইন্টারভিউতে।

  • Synchronous আর asynchronous message কীভাবে আঁকবেন? — দুটিই সরল রেখা; synchronous-এ ভরাট মাথা, asynchronous-এ খোলা মাথা; reply ড্যাশ রেখা।
  • alt আর opt-এর তফাত? — alt-এ কয়েকটি বিকল্প operand, ঠিক একটি চলে; opt-এ একটিই operand, চলে বা চলে না।
  • Shallow আর deep history? — H শুধু সরাসরি substate মনে রাখে; H* সব স্তরের পুরো অবস্থা ফেরায়।
  • Internal transition আর self-transition? — Internal-এ exit/entry চলে না; self-transition-এ চলে।
  • Join আর merge? — Join সব আগত token-এর অপেক্ষা করে; merge যেকোনোটি এলেই ছেড়ে দেয়।

৯ · AI-এর যুগে — AI-এর আঁকা behaviour diagram যাচাই

AI এক মুহূর্তে PlantUML বা Mermaid লিখে দেয়, আর render করলে ছবিটি দেখতে নিখুঁত লাগে। বিপদটা সেখানেই: বিশ্বাসযোগ্য দেখায়, কিন্তু ভুল. Behaviour diagram-এ AI-এর পরিচিত ভুলগুলো:

AI-এর ভুলকীভাবে ধরবেন
কোডে যা নেই এমন call বা object বানিয়ে আঁকে, বা কোডের একটি শাখা বাদ দেয়কোড চালিয়ে trace নিন (অংশ ৪-এর মতো), diagram-এর সাথে লাইন ধরে মেলান
Reply-কে ভরাট মাথার সরল রেখা আঁকে, বা যে call-এর ফল লাগে তাকে asynchronous দেখায়প্রতিটি তীরের মাথা আর রেখা আলাদা করে দেখুন; কোডে await/return আছে কি না
Decision-এর পরে join, বা fork-এর পরে merge বসায়Fork-join আর decision-merge জোড়া গুনুন; token হাতে চালিয়ে দেখুন
Mermaid-এ PlantUML-এর চিহ্ন, বা উল্টো; অস্তিত্বহীন keywordTool-এর official নথি পড়ুন, আর নিজে render করুন — error বা অদ্ভুত ছবি এলেই সন্দেহ
“UML ৩.০ অনুযায়ী” ধরনের বানানো মানদণ্ড বা versionOMG-র website-এ বর্তমান version নিজে দেখুন
প্রতিটি কাজের ধাপকে state বানায়প্রতিটি state-এর জন্য জিজ্ঞেস করুন: এখানে object কোন event-এর অপেক্ষা করে?

AI-কে diagram বানাতে দিলে কোড, প্রশ্ন আর যাচাইয়ের দাবি — তিনটিই দিন, যাতে উত্তর মিলিয়ে দেখা যায়:

নিচের JavaScript কোড থেকে PlantUML sequence diagram বানাও। [কোড এখানে] নিয়ম: ১. শুধু কোডে যে call আছে সেগুলো আঁকবে; কোনো নতুন class বা method বানাবে না। ২. synchronous call -> দিয়ে, reply --> দিয়ে, await ছাড়া পাঠানো call ->> দিয়ে। ৩. প্রতিটি if/else-এর জন্য alt, else-বিহীন if-এর জন্য opt, loop-এর জন্য loop fragment দেবে, guard-সহ। ৪. শেষে একটি তালিকা দাও: diagram-এর প্রতিটি message কোডের কোন লাইন থেকে এসেছে। ৫. যে অংশ কোড থেকে নিশ্চিত বোঝা যায় না, সেটা আঁকবে না — আলাদা করে প্রশ্ন হিসেবে লিখবে।

AI-এর diagram হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১Sequence diagram এল কোথা থেকে, আর agile দলে কি এর এখনো জায়গা আছে?

Sequence diagram-এর শিকড় telecom জগতে। Telephone switch-এর protocol — কোন যন্ত্র কাকে কোন সংকেত কোন ক্রমে পাঠাবে — বোঝাতে প্রকৌশলীরা বহু আগে থেকেই খাড়া রেখা আর আড়াআড়ি তীরের ছবি আঁকতেন। ১৯৯০-এর দশকের শুরুতে আন্তর্জাতিক টেলিযোগাযোগ সংস্থা ITU (তখনকার CCITT) এই ছবিকে Message Sequence Chart নামে মানদণ্ড করে (Recommendation Z.120). Ivar Jacobson Ericsson-এ telecom-এর কাজের অভিজ্ঞতা থেকে তাঁর OOSE পদ্ধতিতে object-দের “interaction diagram” আনেন, যা UML ১-এ sequence diagram হয়। UML ২-এ যে combined fragment (alt, loop, par) আসে, তার ওপর MSC-র পরের সংস্করণগুলোর প্রভাব ছিল বলে সাধারণত বলা হয়।

Agile আন্দোলনের পরে অনেকে ভাবলেন, diagram মানেই ভারী documentation. কিন্তু Martin Fowler তাঁর UML Distilled-এ UML ব্যবহারের তিনটি রূপ আলাদা করেন: sketch (ভাবনা ভাগ করার খসড়া), blueprint (কোড লেখার আগে পূর্ণ নকশা) আর programming language (diagram থেকেই কোড তৈরি)। Agile দলে প্রথমটিই সবচেয়ে কাজের। হোয়াইটবোর্ডে পাঁচ মিনিটে আঁকা একটি sequence diagram প্রায়ই এক ঘণ্টার তর্ক বাঁচায় — “আমার ধারণা Payment আগে” — “না, Stock আগে” ধরনের ভুল বোঝাবুঝি ছবিতে সঙ্গে সঙ্গে ধরা পড়ে।

কোথায় এখনো এটি প্রায় অপরিহার্য? অনেকগুলো service-এর মধ্যে request-এর পথ (microservice), বাইরের API-র সাথে authentication-এর আদান-প্রদান, retry আর timeout-এর নিয়ম, আর নতুন সদস্যকে কোডবেস চেনানো। আর diagram as code (PlantUML, Mermaid) আসার পরে diagram কোডের পাশেই Git-এ থাকে, pull request-এ review হয় — “পুরোনো হয়ে যাওয়া ছবি”-র সমস্যা অনেকটা কমেছে।

মূল উপলব্ধি: প্রশ্নটা “diagram আঁকব কি না” নয়, “কোন প্রশ্নের উত্তরে, কতটুকু”। যে diagram একটি সিদ্ধান্তকে স্পষ্ট করে, সেটা agile; যে diagram কেউ পড়ে না, সেটা অপচয় — যত সুন্দরই হোক।

প্র ০২Harel কেন statechart বানালেন? সাধারণ state diagram-এ সমস্যা কোথায় ছিল?

Finite state machine-এর ধারণা পুরোনো — ১৯৫০-এর দশকের automata তত্ত্ব থেকে। কিন্তু বাস্তব system-এর আচরণ আঁকতে গেলে সমতল state diagram দুটি দেয়ালে ধাক্কা খায়। প্রথম, state-বিস্ফোরণ: একটি যন্ত্রের যদি তিনটি স্বাধীন দিক থাকে, প্রতিটির ২, ৩ আর ২টি অবস্থা, তাহলে সমতল machine-এ ২ × ৩ × ২ = ১২টি state লাগে, অথচ আসল তথ্য মাত্র ২ + ৩ + ২ = ৭টি অবস্থায়। দিক যত বাড়ে, গুণফল সূচকীয় হারে বাড়ে। দ্বিতীয়, একই তীরের পুনরাবৃত্তি: “যেকোনো অবস্থায় power বোতাম চাপলে বন্ধ” — সমতল ছবিতে প্রতিটি state থেকে একটি করে তীর।

David Harel ১৯৮০-র দশকের শুরুতে Israel Aircraft Industries-এর একটি যুদ্ধবিমানের avionics system-এর আচরণ বর্ণনার কাজে যুক্ত ছিলেন বলে তাঁর নিজের পরের স্মৃতিচারণে (“Statecharts in the Making”, ২০০৭) লিখেছেন। প্রকৌশলীদের শত শত পাতার লেখা নিয়ম সমতল state diagram-এ ধরা যাচ্ছিল না। ১৯৮৭ সালে Science of Computer Programming পত্রিকায় প্রকাশিত তাঁর প্রবন্ধের বিখ্যাত সূত্রটি মোটামুটি এমন: statecharts = state diagram + depth (nesting) + orthogonality (একসাথে চলা region) + broadcast communication. Depth দিয়ে “যেকোনো অবস্থায় power” একটি তীর — composite-এর সীমা থেকে। Orthogonality দিয়ে গুণফলের বদলে যোগফল। আর history দিয়ে “যেখানে ছিলাম সেখানে ফেরা” — যা সমতল machine-এ প্রতিটি সম্ভাব্য পূর্বাবস্থার জন্য আলাদা state ছাড়া সম্ভব নয়।

UML state machine সরাসরি statechart থেকে এসেছে (কিছু অর্থগত পার্থক্যসহ)। আজ embedded firmware, গাড়ির নিয়ন্ত্রক, UI framework আর game-এর AI-তে একই চিন্তা চলে; XML-এ statechart লেখার W3C মানদণ্ড SCXML-ও এই ধারারই।

মূল উপলব্ধি: ভালো নোটেশন শুধু ছবি নয় — এটা জটিলতা ভাগ করার একটি কৌশল। Composite state হলো abstraction, orthogonal region হলো separation of concerns — পর্ব ২৬-এর নকশার নীতিই, শুধু আচরণের ভাষায়।

প্র ০৩Entry action-এ network call রাখলে কী সমস্যা? Run-to-completion নিয়মের বাস্তব অর্থ কী?

UML state machine একবারে একটি event নেয় এবং তার সব প্রতিক্রিয়া — exit, effect, entry — শেষ না হওয়া পর্যন্ত পরের event ধরে না। এটাই run-to-completion (RTC). এর সুবিধা বিশাল: একটি event সামলানোর মাঝপথে আরেকটি event এসে অর্ধেক-বদলানো অবস্থা দেখবে না, তাই lock বা জটিল synchronization ছাড়াই আচরণ নির্ধারিত (deterministic) থাকে। Event-গুলো একটি queue-তে জমা হয়, আর machine এক এক করে তুলে নেয় — অনেকটা JavaScript-এর event loop-এর মতো, যেখানে একটি callback শেষ না হলে পরেরটা চলে না (JavaScript-এর event loop).

এখন ধরুন Buffering-এর entry-তে লেখা হলো “server থেকে পরের অংশ নামানো”, আর server সাড়া দিতে ৮ সেকেন্ড নিল। RTC নিয়মে এই ৮ সেকেন্ড machine অন্য কোনো event-এ সাড়া দিতে পারবে না — ব্যবহারকারী pause চাপলেও না। Embedded system-এ এটা আরও ভয়ংকর: একটি দীর্ঘ entry-র কারণে নিরাপত্তা-সংকেত queue-তে আটকে থাকতে পারে।

সমাধান তিনটি। (১) দীর্ঘ কাজ রাখুন do activity-তে, যা state-এ থাকা অবস্থায় চলে আর state ছাড়লে মাঝপথে থামানো হয়। (২) Entry-তে শুধু কাজটি শুরু করুন (asynchronous request পাঠান), আর ফল আসুক একটি নতুন event হয়ে (chunkLoaded, timeout). (৩) প্রতিটি অপেক্ষার সাথে একটি timeout transition রাখুন, যাতে উত্তর না এলে machine আটকে না থাকে। Embedded জগতে Miro Samek-এর Practical UML Statecharts in C/C++ (দ্বিতীয় সংস্করণ ২০০৮) এই active object ধারার একটি পরিচিত বই।

মূল উপলব্ধি: State machine-এর প্রতিটি “তাৎক্ষণিক” কাজ সত্যিই তাৎক্ষণিক হওয়া চাই। অপেক্ষা মানেই একটি state, আর অপেক্ষার শেষ মানেই একটি event — এই চিন্তায় নকশা করলে আটকে যাওয়া UI আর হারানো সংকেত দুটোই কমে।

প্র ০৪একটি sequence diagram কি একটি specification? Distributed system-এ এর সীমা কোথায়?

আনুষ্ঠানিক অর্থে UML ২-এ একটি interaction কিছু trace-এর (message-ঘটনার ক্রম) একটি সেট সংজ্ঞায়িত করে: কোন ক্রমগুলো বৈধ, আর neg fragment দিয়ে কোনগুলো নিষিদ্ধ। তবে একটি diagram সাধারণত আংশিক specification: “এই scenario-তে এভাবে ঘটে” — বাকি সব scenario-তে কী হবে, সে নীরব। তাই sequence diagram দিয়ে সব আচরণ নির্দিষ্ট করার চেষ্টা প্রায়ই শত শত diagram-এ গিয়ে ঠেকে; সেখানে state machine বা লেখা contract (পর্ব ৩১) বেশি কার্যকর।

Distributed system-এ আরেকটি সীমা আসে সময় নিয়ে। Leslie Lamport-এর ১৯৭৮ সালের বিখ্যাত প্রবন্ধ “Time, Clocks, and the Ordering of Events in a Distributed System”-এর space-time diagram দেখতে প্রায় sequence diagram-এর মতো — খাড়া রেখায় process, তির্যক তীরে message. তির্যক কারণ message পৌঁছাতে সময় লাগে। Lamport দেখান, আলাদা যন্ত্রের ঘটনাগুলোর মধ্যে প্রায়ই কোনো সর্বজনীন “আগে-পরে” নেই, আছে শুধু “happened-before” সম্পর্ক। সাধারণ sequence diagram-এর আড়াআড়ি তীর এই সত্য লুকিয়ে ফেলে, যেন সব তাৎক্ষণিক আর পুরোপুরি ক্রমানুসারী। UML তির্যক message-রেখা আর duration constraint অনুমোদন করে, কিন্তু খুব কম দল তা আঁকে। ফলে diagram-এ যে message দুটি পরপর, বাস্তবে সেগুলো উল্টো ক্রমে পৌঁছাতে, দুবার পৌঁছাতে বা হারিয়ে যেতে পারে (lost message!)। বিস্তারিত: Clock আর ঘটনার ক্রম.

Test-এর দিকেও একটি সাবধানতা: mock দিয়ে “ঠিক এই ক্রমে এই call-গুলো হয়েছে” যাচাই করা test আসলে একটি sequence diagram-কে test-এ বেঁধে ফেলে। Implementation সামান্য বদলালেই এমন test ভাঙে, যদিও আচরণ ঠিক থাকে — এই ভঙ্গুরতা নিয়ে আলোচনা পর্ব ৪৯-এ।

মূল উপলব্ধি: Sequence diagram একটি উদাহরণ, পুরো আইন নয়। এটা দিয়ে ভাবুন, আলোচনা করুন, test-এর scenario বের করুন — কিন্তু “এটাই একমাত্র সম্ভাব্য ক্রম” ধরে নেবেন না, বিশেষ করে নেটওয়ার্কের ওপারে।

প্র ০৫Activity diagram-এর token-নিয়ম এল কোথা থেকে? Business process-এর জন্য BPMN থাকতে UML activity কেন?

Token-এর ধারণা এসেছে Petri net থেকে — জার্মান গণিতবিদ Carl Adam Petri তাঁর ১৯৬২ সালের ডক্টরাল গবেষণায় সমান্তরাল ও বিতরণকৃত প্রক্রিয়া বর্ণনার যে গাণিতিক কাঠামো দেন, তার উত্তরসূরি। Petri net-এ “place”-এ token থাকে, আর একটি “transition” তখনই ঘটে যখন তার প্রতিটি input place-এ token আছে — ঠিক UML-এর join-এর মতো। UML ১-এ activity diagram-কে state machine-এর একটি বিশেষ রূপ ধরা হতো; UML ২-এ এর অর্থ নতুন করে লেখা হয় Petri net-এর মতো token-প্রবাহ দিয়ে, যাতে সমান্তরালতা আর data-প্রবাহ নিখুঁতভাবে বলা যায়।

এই গাণিতিক ভিত্তির একটি বাস্তব লাভ: কিছু নকশার ভুল যান্ত্রিকভাবে ধরা যায়। Decision-এর পরে join বসালে deadlock (যে শাখা বাছা হয়নি, তার token কখনো আসবে না); fork-এর পরে merge বসালে পরের কাজ বারবার চলে। Petri net বিশ্লেষণের tool এমন “liveness” আর “boundedness” সমস্যা খুঁজে বের করতে পারে।

BPMN (Business Process Model and Notation) OMG-রই আরেকটি মানদণ্ড; BPMN ২.০ আসে ২০১১ সালে। এর লক্ষ্য ব্যবসায়ী আর বিশ্লেষক — চিহ্নগুলো ব্যবসার ভাষায় (event, gateway, pool), আর অনেক workflow engine BPMN সরাসরি চালাতে পারে। BPMN-এর exclusive gateway (×) মোটামুটি UML-এর decision/merge, আর parallel gateway (+) fork/join-এর সমতুল্য। সাধারণ চর্চা: প্রতিষ্ঠানের প্রক্রিয়া আঁকতে BPMN, আর software-এর ভেতরের algorithm বা object-এর data-প্রবাহ দেখাতে UML activity — যেখানে pin আর object flow সরাসরি method-এর parameter-এ মেলে।

মূল উপলব্ধি: ছবির নিচে গণিত থাকলে ছবি দিয়ে প্রমাণও করা যায়। Fork-join আর decision-merge-এর জোড়া মেলানোর নিয়মটি নিছক শৈলী নয় — এটা deadlock আর দ্বিগুণ কাজ ঠেকানোর নিয়ম।

অনুশীলন

কাগজ-কলম নিন। আগে নিজে উত্তর লিখুন, তারপর মিলিয়ে দেখুন।

  1. আউটপুট অনুমান করুন (entry/exit-এর ক্রম): ছবি ৩০.২-এর মিডিয়া প্লেয়ার, শুরু Idle-এ, history খালি। Event-ক্রম: play, stall, stop, play, pause, resume, ready. প্রতিটি event-এ কোন exit, effect আর entry কোন ক্রমে চলবে, আর শেষে প্লেয়ার কোন state-এ? (stop-এর তীর Playing-এর সীমা থেকে Idle-এ, effect /close.)

    hsm_history.js-এ এই ক্রম দিয়ে চালানো আসল আউটপুট:

    play   exit Idle, /openStream, entry Playing, entry Normal  => Playing.Normal
    stall  exit Normal, entry Buffering  => Playing.Buffering
    stop   exit Buffering, exit Playing, /close, entry Idle  => Idle
    play   exit Idle, /openStream, entry Playing, entry Normal  => Playing.Normal
    pause  exit Normal, exit Playing, /saveTime, entry Paused  => Paused
    resume exit Paused, /seek, entry Playing, entry Normal  => Playing.Normal
    ready  ignored in Normal

    ফাঁদটি চতুর্থ লাইনে। stop-এর সময় history-তে Buffering লেখা হয়েছিল, কিন্তু দ্বিতীয় play-এর তীর যায় Playing-এর সীমায়, H-এ নয় — তাই history ব্যবহার হয় না, প্রবেশ initial state Normal-এ। তারপর pause-এর সময় history নতুন করে Normal হয়, তাই resume ফেরায় Normal-এ। শেষে Normal-এ ready-র তীর নেই, event উপেক্ষিত। চূড়ান্ত state: Playing.Normal.

  2. কোড থেকে sequence diagram আঁকুন: একটি কাল্পনিক বিক্রয়-রিপোর্ট service:
    class ReportService {
      monthly(month) {
        const rows = this.repo.salesOf(month);
        for (const r of rows) {
          if (r.amount > 10000) this.audit.flag(r.id);
        }
        const pdf = this.pdf.render(rows);
        this.mailer.sendLater(pdf);   // only queues the mail, no waiting
        return pdf.name;
      }
    }
    Lifeline: :Client, :ReportService, :Repo, :Audit, :Pdf, :Mailer. PlantUML-এ লিখুন বা কাগজে আঁকুন।
    @startuml
    participant ":Client" as C
    participant ":ReportService" as RS
    participant ":Repo" as R
    participant ":Audit" as A
    participant ":Pdf" as P
    participant ":Mailer" as M
    C -> RS : monthly(month)
    activate RS
    RS -> R : salesOf(month)
    R --> RS : rows
    loop for each r in rows
      opt r.amount > 10000
        RS -> A : flag(r.id)
      end
    end
    RS -> P : render(rows)
    P --> RS : pdf
    RS ->> M : sendLater(pdf)
    RS --> C : pdf.name
    deactivate RS
    @enduml

    যাচাইয়ের বিন্দু: (১) if-এর else নেই, তাই opt, alt নয়; আর সেটি loop-এর ভেতরে, কারণ কোডে if-টি for-এর ভেতরে। (২) flag-এর ফেরত মান ব্যবহার হয়নি, তাই reply না আঁকলেও চলে। (৩) sendLater শুধু queue-তে রাখে, অপেক্ষা নেই — তাই খোলা মাথার asynchronous (->> PlantUML-এ)। (৪) শেষ reply pdf.name Client-এর কাছে, ড্যাশ রেখায়। অনেকে loop-এর guard ভুলে যান; guard ছাড়া loop মানে “০ থেকে অসীম বার”, যা এখানে অর্থের দিক থেকে ঠিক, কিন্তু পাঠককে কম বলে।

  3. একটি ভুল diagram সমালোচনা করুন: একজন সহকর্মী “চা-বাগান”-এর refund-এর একটি sequence diagram এঁকেছেন। বর্ণনা: (ক) :Support থেকে :RefundService-এ refund(orderId) — ড্যাশ রেখা, খোলা মাথা। (খ) RefundService থেকে :Payment-এ reverse(txId) — সরল রেখা, খোলা মাথা, আর পরের লাইনেই সেই ফল দিয়ে alt. (গ) alt-এর guard দুটি: [amount > 500] আর [amount < 1000]. (ঘ) একটি loop, কোনো guard বা সীমা নেই, ভেতরে retry(). (ঙ) :Order-এর lifeline-এ মাঝপথে বড় ×, তার নিচে Order-এ markRefunded() message. (চ) :RefundService-এর activation bar শেষই হয়নি, diagram-এর নিচ পর্যন্ত। ভুলগুলো আর সংশোধন বলুন।
    • (ক) ড্যাশ রেখা reply বা create-এর চিহ্ন। একটি call-এর জন্য সরল রেখা লাগবে — Support উত্তরের অপেক্ষা করলে ভরাট মাথা।
    • (খ) খোলা মাথার সরল রেখা মানে asynchronous — প্রেরক অপেক্ষা করে না। অথচ ঠিক পরেই সেই ফলের ওপর alt; তাহলে call-টি আসলে synchronous (ভরাট মাথা) আর একটি reply তীর লাগবে। নয়তো asynchronous রেখে ফলটি পরে আলাদা message হয়ে আসা দেখাতে হবে।
    • (গ) Guard দুটি একসাথে সত্য হতে পারে (যেমন amount ৭০০), তাই কোন operand চলবে অস্পষ্ট। পরস্পর-বর্জিত করুন: [amount > 500] আর [else].
    • (ঘ) Guard বা সীমাহীন loop মানে ০ থেকে অসীম বার retry — সম্ভবত উদ্দেশ্য নয়। লিখুন loop(1,3) [reverse failed], আর সীমা পেরোলে কী হবে তা break দিয়ে দেখান।
    • (ঙ) × মানে object ধ্বংস; তার পরে সেই lifeline-এ কোনো message যেতে পারে না। হয় × নিচে সরান, নয়তো message বাদ দিন — আর ভাবুন, refund-এর সময় Order ধ্বংস করা কি আদৌ ঠিক? সাধারণত অর্ডারের ইতিহাস রাখতে হয়।
    • (চ) Activation bar শেষ না হওয়া মানে method কখনো ফেরেনি। শেষ reply-র জায়গায় bar শেষ করুন। ছোট ভুল, কিন্তু পাঠক ভাববেন কোনো asynchronous কাজ চলছে।
  4. হিসাব করুন — কতগুলো test scenario: একটি sequence diagram-এ পরপর দুটি fragment: প্রথমে loop(0,3), যার প্রতিটি পুনরাবৃত্তির ভেতরে একটি দুই-operand alt; তারপর একটি opt. সব পথ চালাতে কতগুলো scenario লাগবে? Loop loop(1,3) হলে?

    Loop n বার ঘুরলে, প্রতিবার alt-এ ২টি বিকল্প, আর পুনরাবৃত্তিগুলো স্বাধীন — তাই n বারে ২ⁿটি পথ। n = ০, ১, ২, ৩ যোগ করলে: ১ + ২ + ৪ + ৮ = ১৫. তারপর opt-এর ২টি বিকল্প পরপর বসানো, তাই গুণ: ১৫ × ২ = ৩০টি scenario. loop(1,3) হলে n = ০ বাদ: ২ + ৪ + ৮ = ১৪, × ২ = ২৮টি. সাধারণ সূত্র: পরপর fragment-এর পথ গুণ হয়, একটি fragment-এর বিকল্পগুলো যোগ হয়; n বার পুনরাবৃত্ত k-বিকল্পের অংশে kⁿ. (অংশ ৩-এর fragments_to_tests.js-এ loop-এর ভেতরে alt বসিয়ে চালালে একই সংখ্যা আসে।) বাস্তবে ৩০টির সবগুলো না চালিয়ে loop-এর জন্য ০, ১ আর সর্বোচ্চ — এই boundary-গুলো বেছে নেওয়া হয় (পর্ব ৪৭)।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি UML state machine-এ object আছে composite state A-র substate A1-এ। Event e-তে একটি তীর A-র সীমা থেকে আরেক composite state B-তে যায়, effect /f, আর B-র initial substate B1. কাজগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. A1-এর exit কাজ
  2. A-র exit কাজ
  3. Transition-এর effect /f
  4. B-র entry কাজ
  5. B1-এর entry কাজ

UML ২.৫.১-এর sequence diagram-এ asynchronous message কীভাবে আঁকা হয়?

  • সরল রেখা, ভরাট ত্রিভুজ মাথা
  • সরল রেখা, খোলা মাথা
  • ড্যাশ রেখা, খোলা মাথা
  • Lifeline-এর শেষে বড় ×

একটি sequence diagram-এ loop-এর ভেতরে একটি break [stock = 0] fragment. Loop চলাকালে stock 0 হলে কী ঘটে?

  • break-এর ভেতরের message বাদ যায়, loop চলতে থাকে
  • পুরো diagram-এর সব কাজ শেষ, কিছুই আর ঘটে না
  • break-এর ভেতরের message চলে, তারপর loop-এর বাকি অংশ বাদ দিয়ে loop-এর পরে যায়
  • break সবসময় উপেক্ষিত হয়, কারণ loop-এর guard আগে

State S-এ entry / a আর exit / b. S-এর ভেতরে internal transition e / c লেখা। S-এ থাকা অবস্থায় event e এলে কোন কাজগুলো চলে?

  • শুধু c
  • b, c, a
  • a, c, b
  • কিছুই না

Composite state Playing-এর ভেতরে Normal আর Buffering; Buffering নিজেও composite, ভেতরে Fetching (initial) আর Decoding. Object যখন Playing.Buffering.Decoding-এ, তখন Playing থেকে বেরিয়ে যায়। পরে একটি তীর Playing-এর shallow history (H)-এ এলে object কোথায় যাবে?

  • Playing.Normal
  • Playing.Buffering.Decoding
  • কোথাও নয়; transition বাতিল
  • Playing.Buffering.Fetching

একটি activity-তে fork-এর পরে দুটি সমান্তরাল action, তারপর একটি merge (join নয়), তারপর action X. Fork-এ একটি token ঢুকলে X কতবার চলবে?

  • একবার
  • দুবার
  • একবারও না, deadlock
  • অসীমবার

State machine-এ submit / total := computeTotal() তীরের পরে একটি সিদ্ধান্ত-বিন্দু, যার guard [total > 5000]. Guard-কে effect-এর পরে হিসাব করা নতুন total দেখতে হলে কোন pseudostate লাগবে?

  • Junction (ছোট ভরাট বৃত্ত)
  • Shallow history (H)
  • Choice (হীরা)
  • Fork (মোটা দণ্ড)

শব্দকোষ

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

মনে রাখুন

  1. Sequence = একটি scenario-র সময়ক্রম; state machine = একটি object-এর সারা জীবন; activity = একটি কাজের ধারা আর data-প্রবাহ।
  2. তীর: ভরাট মাথা synchronous, খোলা মাথা asynchronous, ড্যাশ reply; create সরাসরি নতুন মাথায়, × মানে ধ্বংস।
  3. Fragment: alt if/else, opt if, loop, par, break, আর ref অন্য diagram; পরপর fragment-এর পথ গুণ হয়।
  4. Transition-এ ক্রম: exit (ভেতর → বাইরে) → effect → entry (বাইরে → ভেতর); internal transition-এ exit/entry নেই; H এক স্তর, H* সব স্তর মনে রাখে।
  5. Fork-এর জোড়া join, decision-এর জোড়া merge; ভুল জোড়ায় deadlock বা দ্বিগুণ কাজ। AI-এর diagram render করে, কোড আর trace-এর সাথে মিলিয়ে তবেই বিশ্বাস।

পরের পর্বে: Sequence diagram-এর প্রতিটি message একটি interface-এর call. সেই call-এর আগে কী সত্য হতে হবে, পরে কী নিশ্চিত — design by contract, error handling আর immutability দিয়ে interface-কে নির্ভরযোগ্য করা।

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

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