মূল বিষয়বস্তুতে যান
Clean Code Mastery

Separate Query from Modifier: জিজ্ঞেস করলেই বিল বাড়ে না তো?

Separate Query from Modifier সহজ ভাষায় — যে method একসাথে question করে আর state বদলায়, সেটাকে ভেঙে দুটো করো: একটা pure query আর একটা আলাদা command। Bertrand Meyer-এর Command-Query Separation (CQS) principle অনুযায়ী: question করলে answer বদলানো উচিত না।

22 মিনিট আপডেট: June 11, 2026intermediate
refactoringseparate query from modifiercommand query separationCQSside effectspure functionssimplifying method calls

☕ "ভাই, কত হইছে?" — আর বিল বাইড়া গেল

ধরো বাস স্ট্যান্ডের পাশে রাহিম ভাইয়ের একটা চায়ের দোকান। নিয়মিত কাস্টমাররা মাসে মাসে খাতায় বাকিতে চা খায়, বেতন পাইলে হিসাব মেটায়। ব্যাপারটা পুরোপুরি বিশ্বাসে চলে।

এখন কল্পনা করো — মাসের মাঝামাঝি জামাল ভাই জিজ্ঞেস করলেন, "রাহিম ভাই, এ পর্যন্ত কত হইছে?" রাহিম ভাই খাতা উলটায়, হাসিমুখে বলে "দুইশো দশ টাকা" — আর সাথে সাথে চুপচাপ আজকের একটা চা খাতায় লিখে দেয়, যদিও জামাল ভাই আজকে কিছু অর্ডারই করেননি।

জামাল ভাই একটা question করলেন। রাহিম ভাই একটা action নিল। জামাল ভাই জানলেনই না। পরের সপ্তাহে আবার জিজ্ঞেস করলেন শুধু হিসাব রাখতে — "দুইশো ত্রিশ এখন।" ত্রিশ টাকা বাড়লো, অথচ মাঝে মাত্র একবার এসেছিলেন! প্রতিবার হিসাব জিজ্ঞেস করলে বিল বাড়ে।

আস্তে আস্তে নিয়মিতরা বুঝে গেল। সুমাইয়া আপা পাশের দোকান থেকে সতর্ক করলেন — "এই দোকানে জিজ্ঞেস করাটাও পয়সা লাগে!" সবাই জিজ্ঞেস করা বন্ধ করে দিল। খাতায় ছোট ভুল জমতে লাগলো, বেতনের দিনে বড় ঝামেলা হলো। আর সবচেয়ে দুঃখের কথা — একটা সৎ দোকান চলে মানুষ অবাধে জিজ্ঞেস করতে পারে বলেই। প্রশ্ন করাটা যখন বিপজ্জনক হয়, তখন সবাই চুপ করে থাকে, আর ভুলগুলো লুকিয়ে থাকে।

সৎ দোকানের নিয়ম একটাই: দাম জিজ্ঞেস করলে দাম বাড়ে না। প্রশ্ন করা নিরাপদ, বারবার করা যায়। আর action — চা অর্ডার করা, বিল দেওয়া — সেটা ইচ্ছাকৃতভাবে, জেনেশুনে করো।

Code-এও একই সততা দরকার। যে method question-এর উত্তর দেয়, সে যেন caller-এর অজান্তে কিছু বদলে না দেয়। যখন একটা method দুটো কাজ করে — balance return করে আর fee charge করে, password check করে আর account lock করে — তখন "শুধু জানতে চাওয়া" প্রতিটা caller চুপচাপ কিছু একটা trigger করে। Separate Query from Modifier হলো সেই refactoring যেটা এরকম method-কে দুটো সৎ অংশে ভেঙে দেয়: একটা pure question আর একটা deliberate action।

চিত্র ১: খাতার যাত্রা — বিশ্বস্ত নোটবুক থেকে চুপচাপ এন্ট্রি, তারপর query আর charge আলাদা করে সততা ফিরে আসা।

Separate Query from Modifier কী জিনিস?

Separate Query from Modifier হলো Martin Fowler-এর Refactoring বই থেকে একটা refactoring:

যে method একসাথে value return করে আর state বদলায়, সেটাকে দুটো method-এ ভাগ করো — একটা pure query যেটা কোনো side effect ছাড়া value return করে, আর একটা command (modifier) যেটা শুধু change করে।

এই refactoring-এর পেছনে আছে object-oriented design-এর অন্যতম পুরনো আর কাজের principle: Command-Query Separation (CQS)। এটা বানিয়েছিলেন Bertrand Meyer — Eiffel programming language-এর জনক — ১৯৮৮ সালের Object-Oriented Software Construction বইয়ে। Meyer-এর নিয়ম সব method-কে ঠিক দুই ভাগে ভাগ করে:

  • Query একটা question-এর উত্তর দেয়। Data return করে, কিছুই বদলায় না observable sense-এ। একবার call করো, পঞ্চাশবার call করো, যেকোনো order-এ করো — দুনিয়া একই থাকে।
  • Command একটা action করে। State বদলায় — আর কিছুই return করে না (যাতে কেউ এটাকে question হিসেবে ব্যবহার করতে না পারে)।

মনে রাখার সহজ লাইন: question করলে answer বদলানো উচিত না।

কেন এটা নিয়ে আলাদা principle দরকার? কারণ side-effect-free query হলো একটা superpower। তুমি যদি জানো একটা method কিছু বদলায় না, তাহলে তুমি পারো:

  • যেকোনো জায়গায় call করতে — if condition-এ, log-এ, assertion-এ, debugger-এ — কোনো ভয় ছাড়াই;
  • যতবার খুশি call করতে — দুইবার, loop-এ, দুই জায়গা থেকে — আর consistent answer পাবে;
  • cache বা reorder করতে, কারণ কখন বা কতবার চলছে সেটা কোনো ব্যাপার না;
  • test করতে সহজ input-output check দিয়ে, state verification gymnastics ছাড়াই।

একটা query যখন side effect লুকিয়ে রাখে, এই সব guarantee চুপচাপ উড়ে যায়। আর যে bug আসে সেটা সবচেয়ে কুটিল ধরনের — কারণ এটা ঘটে যেখানে কেউ "শুধু একটা value পড়েছে।" তারিক একটা innocent log line যোগ করলো, logger.info(account.getBalance()), আর customer-রা double fee charge পেতে শুরু করলো। কেউ কয়েকদিন ধরে log line-কে সন্দেহই করে না।

💡

কোথায় পড়বে: এই refactoring Fowler-এর Refactoring বইয়ের দুটো edition-এই Separate Query from Modifier নামে আছে। এটা Rename Method বা Add Parameter-এর মতো Change Function Declaration-এ merge হয়নি — ২য় edition-এও নিজের নামে আছে। এর পেছনের principle CQS-এর নিজস্ব page আছে Martin Fowler-এর bliki-তে, যেখানে তিনি Bertrand Meyer-কে credit দেন আর stack.pop()-এর মতো classic exception-ও সৎভাবে উল্লেখ করেন। Refactoring আর principle একটা pair: CQS হলো নিয়ম, Separate Query from Modifier হলো সেই নিয়ম ভাঙা code ঠিক করার উপায়।

একটা naming bonus আছে। Method ভেঙে ফেললে দুটো অংশ অবশেষে সৎ নাম পায় — query-টা noun-ish question হয় (pendingBalance()), command-টা verb হয় (applyLateFee()). যে method-এর সৎ নামে "And" লাগে — getBalanceAndApplyFee — সেখানে CQS ইতোমধ্যে ভাঙা। এই refactoring (আর Rename Method) হলো সমাধান।

পুরো principle এক ছবিতে:

চিত্র ২: এক ছবিতে CQS — query উত্তর দেয়, command কাজ করে, কারণ question-গুলো সবসময় নিরাপদ রাখতে হবে।

একটু ভাবো: পুরো web এই idea-র উপর দাঁড়িয়ে আছে। HTTP-তে GET হলো safe method — server state বদলাতে পারবে না — আর idempotent: একবার বা পঞ্চাশবার পাঠালে একই effect। POST, PUT, আর DELETE হলো command। ইন্টারনেটের প্রতিটা cache, proxy, browser, monitoring bot GET-কে pure query মনে করে চলে। কোনো backend যদি GET handler-এ state mutate করে — ধরো GET /api/balance fee charge করে — তাহলে পুরো contract ভেঙে যায়। একটা crawler visit, একটা prefetch, একটা network retry — আর কেউ চাইনি এমন state বদলে যায়। Method level-এ CQS আর protocol level-এ "GET must be safe" — একই নিয়ম দুটো আলাদা scale-এ।

কখন দরকার হয়?

এই refactoring ব্যবহার করো যখন এই চিহ্নগুলো দেখবে — প্রতিটাই রাহিম ভাইয়ের মতো উত্তর দিতে দিতে চুপচাপ খাতায় লেখা:

  1. "get" যেটা আসলে "set"-ও করে। getNextInvoiceNumber() number return করে আর counter increment করে। Screen-এ দুইবার দেখালে একটা invoice number চিরতরে হারিয়ে যায়।
  2. Check করে কিন্তু action-ও নেয়। isPasswordValid(pw) false return করে আর failed-attempts counter বাড়ায়, আর হয়তো account lock করে। এখন unit test তিনবার call করলে test user lock হয়ে যায়।
  3. Query যেটা বারবার call করা নিরাপদ না। Method-এর উপরে comment — // do not call twice! — মানে যেটা read মনে হচ্ছে আসলে তা না। Comment-টা নিজেই confession।
  4. অদ্ভুত caller ritual। Caller result variable-এ save করে ঘুরিয়ে বেড়ায় আবার call করার ভয়ে; অথবা method call করে result discard করে (pure command হিসেবে ব্যবহার করছে) — প্রমাণ যে দুটো আলাদা চাহিদা একটা method-এ আটকে আছে।
  5. Assertion আর log bug বানায়। assert বা debug log যোগ করলে program-এর behavior বদলে যায়। Meyer ঠিক এই কারণেই CQS আর Design by Contract একসাথে ডিজাইন করেছিলেন।
  6. "And" নাম। উপরে বললাম: সৎ নামে "And" লাগলে method-এর দুটো কাজ আছে। সম্পর্কিত smell: দুটো কাজ করা method প্রায়ই Long Method-এর ভেতরে লুকিয়ে থাকে।

চিহ্ন নম্বর ৪ একটা chart দিয়ে বোঝা যায়। একটা combined method-এর caller audit করলে দেখা যায় বেশিরভাগ caller আসলে দুটো অংশের একটাই চেয়েছিল:

চিত্র ৩: Combined method-এর caller audit — বেশিরভাগ শুধু উত্তর চেয়েছিল, কিন্তু সবাই action trigger করলো।

ষাট শতাংশ caller হলো জামাল ভাই: question করেছে, বিল বেড়েছে।

আর সৎ exception, যেখানে query আর command একসাথে রাখতে হয়:

  • Concurrent atomic operations: stack.pop(), queue.dequeue(), iterator.next(), compare-and-swap, "fetch next ID"। এগুলো ভাগ করলে দুটো call-এর মাঝখানে অন্য thread ঢুকে race condition করতে পারে। Fowler এই deviation সরাসরি accept করেছেন।
  • Expensive shared computation: command যদি value compute করতে অনেক cost লাগে, আলাদা query-তে আবার compute করা wasteful হতে পারে — ভাগ করার আগে measure করো।
  • Transaction boundary: দুটো আলাদা call হয়তো আলাদা state দেখবে যেটা একটা atomic call দেখতো না — যেখানে correctness নির্ভর করে, সেখানে atomicity রাখো।

দুটো question সিদ্ধান্ত নেয় — state কতটা contested আর computation কতটা entangled:

চিত্র ৪: Single-threaded state আর সস্তা question হলে এখনই ভাগ করো; অনেক writer race করলে atomicity সম্মান করো।

Before আর After এক নজরে

চায়ের দোকানের খাতা, TypeScript-এ:

// BEFORE — asking the bill ADDS to the bill
class KhataAccount {
  private entries: number[] = [];
 
  // Query and command glued together — the dishonest shopkeeper
  getBalanceAndChargeServiceFee(): number {
    const balance = this.entries.reduce((sum, e) => sum + e, 0);
    if (balance > 200) {
      this.entries.push(10); // sneaky Rs.10 "service fee" — a SIDE EFFECT
    }
    return balance;
  }
}
 
// An innocent caller, just displaying the khata:
const due = account.getBalanceAndChargeServiceFee(); // charged!
showOnScreen(due);
console.log(account.getBalanceAndChargeServiceFee()); // charged AGAIN, and shows a different number!

দুটো read, দুটো চাপা fee, একই question-এ দুটো আলাদা answer। এখন সৎ split:

// AFTER — a pure question and a deliberate action
class KhataAccount {
  private entries: number[] = [];
 
  // QUERY — safe to call any number of times, anywhere
  getBalance(): number {
    return this.entries.reduce((sum, e) => sum + e, 0);
  }
 
  // COMMAND — changes state, returns nothing, called on purpose
  chargeServiceFeeIfDue(): void {
    if (this.getBalance() > 200) {
      this.entries.push(10);
    }
  }
}
 
// Callers now choose explicitly:
showOnScreen(account.getBalance());        // ask freely — nothing changes
console.log(account.getBalance());         // same answer, guaranteed
 
account.chargeServiceFeeIfDue();           // the action happens HERE, visibly, on purpose

একই behavior পাওয়া যাচ্ছে — কিন্তু এখন পড়া free, আর charge করাটা visible, deliberate এক লাইন যেটা reviewer দেখতে পায় আর প্রশ্ন করতে পারে।

দুই-মুখো এই exchange, frame by frame — দেখো কীভাবে caller মনে করছে শুধু পড়ছে অথচ লুকিয়ে write হয়ে যাচ্ছে:

চিত্র ৫: Split-এর আগে, প্রতিটা innocent question চুপচাপ একটা action করে — caller কখনো write হতে দেখে না।

Split-এর পরে class তার সততা নিজের shape-এ ঘোষণা করে — return type দিয়ে বোঝা যায়: query value return করে, command void।

চিত্র ৬: সৎ CQS class — signature দেখেই প্রতিটা method query না command বোঝা যায়।

নিরাপদে Step-by-Step

আমরা getBalanceAndChargeServiceFee() সাবধানে ভাগ করবো, প্রতিটা ধাপে program চলমান রাখবো।

চিত্র ৭: Split pipeline — combined method glue হিসেবে বেঁচে থাকে যতক্ষণ প্রতিটা caller তার আসল intent declare না করে।

ধাপ ১ — Pure query তৈরি করো। শুধু value-producing logic নতুন method-এ copy করো। ভালো করে check করো কিছু touch হচ্ছে না: কোনো field-এ assignment নেই, কোনো save নেই, কোনো send নেই।

// Step 1 — the new pure query (original method still exists, untouched)
getBalance(): number {
  return this.entries.reduce((sum, e) => sum + e, 0);
}

ধাপ ২ — Original method-কে query ব্যবহার করাও। Behavior একই থাকে; শুধু ভেতরে duplication কমছে।

// Step 2 — original now delegates the "question" part
getBalanceAndChargeServiceFee(): number {
  const balance = this.getBalance();
  if (balance > 200) {
    this.entries.push(10);
  }
  return balance;
}

Compile করো, test চালাও। সবকিছু pass করতে হবে — কোনো observable পরিবর্তন নেই।

ধাপ ৩ — Command বের করো। Side-effecting অংশটা verb-named, void-returning method-এ নিয়ে যাও, আর original দুটো অংশই call করুক:

// Step 3 — the command exists; original is now just a glue method
chargeServiceFeeIfDue(): void {
  if (this.getBalance() > 200) {
    this.entries.push(10);
  }
}
 
getBalanceAndChargeServiceFee(): number {
  const balance = this.getBalance();   // question
  this.chargeServiceFeeIfDue();        // action
  return balance;
}

আবার test। সূক্ষ্ম একটা বিষয়: fee-র আগের balance return করছি, original-এর behavior ঠিক মেলাতে। ভাগ করার সময় order গুরুত্বপূর্ণ — original observable behavior হুবহু রাখো।

ধাপ ৪ — Caller-দের intent অনুযায়ী একে একে migrate করো। প্রতিটা call site-এ যাও আর ভাবো: এই caller উত্তর চেয়েছিল, নাকি action চেয়েছিল, নাকি সত্যিই দুটোই?

// Caller that only displayed the balance (most of them!):
const due = account.getBalance();
 
// Caller that ran the monthly fee job:
account.chargeServiceFeeIfDue();
 
// Caller that truly needed both, in the original order:
const due = account.getBalance();
account.chargeServiceFeeIfDue();

প্রতিটা migration-এর পর test চালাও।

ধাপ ৫ — Glue method মুছে দাও। যখন আর কোনো caller getBalanceAndChargeServiceFee() ব্যবহার করছে না, সেটা remove করো। "And" নামটা codebase থেকে উধাও হয়ে গেল — সবসময় একটা ভালো দিন।

ধাপ ৬ — Test দিয়ে দরজা বন্ধ করো। একটা regression test যোগ করো যেটা assert করে query সত্যিই pure:

test("getBalance does not change state", () => {
  const account = makeAccountWithBalance(250);
  const first = account.getBalance();
  const second = account.getBalance();
  expect(second).toBe(first); // asking twice gives the same answer
});

উপর থেকে দেখলে, refactoring চারটা safe state-এর মধ্য দিয়ে যায় — আর gradual rename-এর মতোই, যেকোনো মাঝের state-এ থামলেও কিছু ভাঙবে না:

চিত্র ৮: Split-এর চারটা state — প্রতিটা transition test-guarded, আর combined method বেঁচে থাকে শেষ caller নিজের পক্ষ না বেছে নেওয়া পর্যন্ত।
⚠️

দুটো trap জোরে বলা দরকার। Trap এক — লুকানো behavior change: কিছু পুরনো caller হয়তো side effect-এর উপর নির্ভর করছিল কেউ না জেনেই ("balance screen দেখানোটাই fee charge করে" — ভয়ানক, কিন্তু real system-এ এরকম হয়)। সেই caller-কে pure query-তে migrate করলে fee চুপচাপ বন্ধ হয়ে যায়। তাই একটা একটা করে migrate করো আর প্রতিটার পরে test করো, আর ধাপ ২ আর ৩-এ combined method জীবিত রাখো যতক্ষণ প্রতিটা caller-এর আসল intent confirm না হয়। Trap দুই — concurrency: দুটো thread যদি পুরনো method-এর read-and-update একটা atomic step হিসেবে ব্যবহার করতো, তোমার split এখন query আর command-এর মাঝে একটা race window খুলে দিয়েছে। Shared mutable state under concurrency-তে atomic operation রাখো (এটাই pop() exception) অথবা proper synchronization যোগ করো।

একটু গভীরে যাই: তোমার নতুন query যে property পেয়েছে তার একটা precise নাম আছে — idempotence (আর তার চেয়ে গভীরে, referential transparency)। Idempotent read stack-এর যেকোনো layer বিনা ক্ষতিতে retry করতে পারে — এই কারণেই distributed system-এ এটা mandatory: network fail করে mid-call-এ, client timeout করে আর retry করে, load balancer দ্বিতীয় server-এ request replay করে। "balance পড়া" যদি fee charge করে, প্রতিটা retry মানে নতুন fee — এটা real production billing incident-এর একটা পুরো category। এর architectural cousin হলো CQRS (Command Query Responsibility Segregation), যেটা Meyer-এর method-level নিয়ম পুরো service-এ নিয়ে যায়: write model command handle করে, আলাদা read model (প্রায়ই আলাদা database) question handle করে। Method level-এ CQS দ্বিতীয় প্রকৃতি না হওয়া পর্যন্ত CQRS নিয়ে ভাবার দরকার নেই — এই refactoring হলো সেই প্রকৃতি তৈরির জায়গা।

একটা বড় Real-Life Example

Login system — এই bug সবচেয়ে কড়া কামড় দেয় যেখানে:

// BEFORE — "checking" a password punishes the account
class LoginService {
  // Returns whether login succeeded... and counts attempts... and locks accounts.
  validate(username: string, password: string): boolean {
    const user = this.users.find(username);
    if (!user) return false;
 
    const ok = hash(password) === user.passwordHash;
 
    if (!ok) {
      user.failedAttempts += 1;                  // side effect 1
      if (user.failedAttempts >= 3) {
        user.locked = true;                      // side effect 2
        this.mailer.sendLockoutWarning(user);    // side effect 3!
      }
      this.users.save(user);
    }
    return ok;
  }
}

Reasonable মনে হচ্ছে — যতক্ষণ না দেখো কে call করছে:

// The login controller — fine, this is the intended user:
if (loginService.validate(name, pw)) { startSession(); }
 
// The admin "test credentials" tool — oops, admins are locking customers:
const works = loginService.validate(customer.name, typedPassword);
 
// A unit test — runs validate 3 times, locks the fixture user, later tests fail mysteriously:
expect(loginService.validate("ravi", "wrong")).toBe(false);
expect(loginService.validate("ravi", "wrong")).toBe(false);
expect(loginService.validate("ravi", "alsowrong")).toBe(false); // ravi is now locked!

"password কি সঠিক?" question আর "ব্যর্থতায় শাস্তি দাও" action একসাথে জোড়া লাগানো — তাই প্রতিটা জিজ্ঞেসকারী হয়ে যায় শাস্তিদাতা। ভাগ করো:

// AFTER — question and action, separated and named honestly
class LoginService {
  // QUERY — pure; safe for admin tools, tests, anyone
  isPasswordCorrect(username: string, password: string): boolean {
    const user = this.users.find(username);
    if (!user) return false;
    return hash(password) === user.passwordHash;
  }
 
  // COMMAND — the security policy, applied deliberately
  recordFailedAttempt(username: string): void {
    const user = this.users.find(username);
    if (!user) return;
    user.failedAttempts += 1;
    if (user.failedAttempts >= 3) {
      user.locked = true;
      this.mailer.sendLockoutWarning(user);
    }
    this.users.save(user);
  }
}
 
// The real login flow composes both, explicitly and readably:
const ok = loginService.isPasswordCorrect(name, pw);
if (ok) {
  startSession();
} else {
  loginService.recordFailedAttempt(name);  // the punishment is now a visible decision
}
 
// The admin tool asks freely — no customer gets locked:
const works = loginService.isPasswordCorrect(customer.name, typedPassword);
 
// Tests ask as many times as they like:
expect(loginService.isPasswordCorrect("ravi", "wrong")).toBe(false); // ravi unharmed

দেখো split কী দিলো। Security policy দুর্বল হয়নি — login flow এখনো count করে আর lock করে। কিন্তু শাস্তি দেওয়ার decision একটা "check"-এর ভেতরের অন্ধকার কোণ থেকে বেরিয়ে flow-তে plain sight-এ এলো, যেখানে reviewer দেখতে পায়, product discuss করতে পারে, আর কোনো innocent caller accident-এ trigger করতে পারে না। Admin tool আর test একেবারে safe হয়ে গেল। আর দুটো method-এরই এখন একটা কাজের নাম — কোনো "And" নেই।

Support team পার্থক্যটা সাথে সাথে টের পেল। Split-এর আগে "রহস্যজনকভাবে locked account" ছিল weekly ticket category — প্রতিটা admin credential check চুপচাপ শাস্তি দিচ্ছিল। তারপর:

চিত্র ৯: Surprise side effect support ticket হিসেবে দেখা যায় — question আর শাস্তি আলাদা করলে এই category প্রায় উধাও হয়ে যায়।

C#-এ একই Refactoring

Classic bank-balance example, overdraft fee সহ:

// BEFORE — reading the balance can charge the customer
public class AccountService
{
    public decimal GetBalanceAndApplyOverdraftFee(Account account)
    {
        if (account.Balance < 0)
        {
            account.Balance -= 35m;   // side effect: Rs.35 overdraft fee
            _repo.Save(account);
        }
        return account.Balance;       // query: current balance
    }
}

Mobile app home screen-এ balance দেখায়। Overdrawn customer প্রতিবার screen refresh করলে আরেকটা ৳৩৫ charge হয়। হতাশায় পাঁচবার pull-to-refresh — ৳১৭৫ fee। এই shape-এর bug real financial system-এ হয়েছে।

Split, প্রতিটা অংশ সৎভাবে typed — query value return করে, command void return করে:

// AFTER — CQS restored
public class AccountService
{
    // QUERY — no side effects; call it from screens, logs, anywhere
    public decimal GetBalance(Account account)
    {
        return account.Balance;
    }
 
    // COMMAND — state change, deliberate, returns nothing
    public void ApplyOverdraftFeeIfOverdrawn(Account account)
    {
        if (account.Balance < 0)
        {
            account.Balance -= 35m;
            _repo.Save(account);
        }
    }
}
 
// The nightly fee job — the ONE place that charges:
foreach (var account in overdrawnAccounts)
{
    accountService.ApplyOverdraftFeeIfOverdrawn(account);
}
 
// The app's balance screen — asks all day, harms no one:
var balance = accountService.GetBalance(account);

C#-এর কিছু specific বিষয়:

  • Command-এ void return type নিজেই documentation: কেউ ApplyOverdraftFeeIfOverdrawn-কে question হিসেবে accident-এ ব্যবহার করতে পারবে না।
  • C# 8+ এ [Pure] attribute (System.Diagnostics.Contracts) দিয়ে tool-কে query purity বলতে পারো, আর struct-এ readonly member compile time-এ mutation enforce করে।
  • Property নিয়ে সাবধান: C#-এ property getter দেখতে pure data access মনে হয়, তাই getter-এ side effect হলে সেটা method-এর চেয়েও খারাপ CQS violation — প্রতিটা style guide এটা forbid করে। get { _count++; return _count; } দেখলে এই refactoring urgently দরকার।

Python-এ একটু দেখা যাক

একই split Python-এ, আর পরে door lock করার purity test সহ:

class KhataAccount:
    def __init__(self) -> None:
        self._entries: list[int] = []
 
    # QUERY — pure; safe in f-strings, logs, asserts, loops
    def balance(self) -> int:
        return sum(self._entries)
 
    # COMMAND — changes state, returns None on purpose
    def charge_service_fee_if_due(self) -> None:
        if self.balance() > 200:
            self._entries.append(10)
 
 
def test_balance_is_pure():
    account = KhataAccount()
    account._entries.extend([100, 150])  # balance 250, fee territory
    first = account.balance()
    second = account.balance()
    assert first == second == 250  # asking twice changes nothing

Python-এ mutating method থেকে None return করার convention হলো CQS folk-wisdom যেটা standard library-তে baked in: list.sort() in-place sort করে আর None return করে, sorted(my_list) নতুন list return করে আর কিছু touch করে না। Language তোমাকে চুপচাপ Meyer-এর rule শিখিয়ে আসছিল।

IDE Support

Rename (F2 / Shift+F6) বা Change Signature (Ctrl+F6)-এর মতো এক-key "Separate Query from Modifier" button কোনো IDE-এ নেই — কারণ split-এ human judgment লাগে কোন lines question আর কোনগুলো action। কিন্তু building block-গুলো fully automated:

ToolFeatureএখানে কীভাবে কাজে লাগে
JetBrains IDEsCtrl+Alt+M — Extract MethodSide-effecting lines select করে এক ধাপে নতুন command method-এ extract করো।
VS CodeCtrl+Shift+R — Refactor menu — Extract Method (language extensions)Command অংশের জন্য একই extraction।
সব IDERename (F2 / Shift+F6)দুটো অংশকে সৎ নাম দাও — query question হিসেবে, command verb হিসেবে।
Analyzer / linter.NET [Pure] + Code Contracts, ESLint plugin যেটা getter-এ side effect flag করেImpure "getter" detect করতে আর split-এর পরে query pure রাখতে সাহায্য করে।
Find Usages (Shift+F12 VS Code / Alt+F7 JetBrains)প্রতিটা caller list করে যাতে intent অনুযায়ী একে একে migrate করতে পারো।

তাহলে practical recipe হলো: Extract Method command-এর জন্য, Rename দুটো অংশের জন্য, Find Usages migration-এর জন্য — তিনটা automated tool, একজন thinking human-এর orchestration-এ।

সুবিধা আর ঝুঁকি

সুবিধাঝুঁকি / খরচ
Query সব জায়গায় safe হয়ে যায় — condition, log, assert, debugger watch, dashboard — state বদলের কোনো ভয় নেই।Atomicity হারানো: concurrent code-এ query-then-command একটা race window খোলে যেটা পুরনো combined method-এ ছিল না। pop()-style operation combined রাখো বা synchronize করো।
Side effect হয়ে যায় visible, deliberate lines of code যেটা reviewer দেখতে পায় আর question করতে পারে।Caller হয়তো চুপচাপ side effect-এর উপর নির্ভর করছিল; তাকে pure query-তে migrate করলে চুপচাপ effect বন্ধ হয়ে যায়। একে একে migrate করো, প্রতিটার পর test।
Query cache, memoize, reorder, বারবার call করা যায় — impure method-এ এই performance option ছিল না।Command expensively value compute করলে আলাদা query-তে কাজ দুইবার হতে পারে — hot path-এ ভাগ করার আগে measure করো।
Testing দুটো সহজ অংশে ভাগ হয়: query-র জন্য input/output check, command-এর জন্য state check।দুটো call একটার চেয়ে একটু বেশি code সেখানে যেখানে সত্যিই দুটোই দরকার।
প্রতিটা অংশ সৎ একটা কাজের নাম পায় — আর কোনো "getXAndDoY" নেই। Rename Method-এর সাথে দারুণ মিলে।Transaction boundary-তে দুটো state read আলাদা state দেখতে পারে যেটা একটা atomic call দেখতো না — transactional correctness সম্মান করো।

কোন Smell-গুলো সারায়?

SmellSeparate Query from Modifier কীভাবে সাহায্য করে
Mysterious Name ("And" নাম)getBalanceAndApplyFee হয়ে যায় দুটো সত্যিকারের নামের method; "And" উধাও।
Long Methodদুটো কাজ (উত্তর + action) করা method ভেঙে দুটো ছোট, single-purpose method হয়।
Side-effecting getter / চমকানো state changeমূল cure: read আর mutate আলাদা হয়।
Comments// careful: also charges a fee! এই warning-গুলো অপ্রয়োজনীয় হয়ে যায় — structure নিজেই বলে দেয়।
Caller-দের মধ্যে temporal coupling"একবারই call করো!" ritual উধাও; query-গুলো by construction repeat-safe।

Quick Revision Box

+------------------------------------------------------------------+
|          SEPARATE QUERY FROM MODIFIER — CHEAT SHEET               |
+------------------------------------------------------------------+
| Story    : Asking "how much do I owe?" must NOT add chai to bill  |
| Principle: CQS — Bertrand Meyer (Eiffel, OOSC 1988)               |
|            "Asking a question should not change the answer."      |
| QUERY    : returns data, changes NOTHING observable               |
| COMMAND  : changes state, returns NOTHING (void)                  |
|                                                                   |
| SAFE STEPS:                                                       |
|   1. Create pure query (value logic only)                         |
|   2. Original uses query internally — test                        |
|   3. Extract command (side effects, void) — test                  |
|   4. Migrate callers BY INTENT: query / command / both            |
|   5. Delete the combined "And" method                             |
|   6. Add a test: calling the query twice changes nothing          |
|                                                                   |
| EXCEPTIONS: stack.pop(), queue.dequeue(), iterator.next(),        |
|             atomic fetch-next-id — keep atomic under concurrency  |
| TELLS     : "And" in the name | "don't call twice!" comments |    |
|             a get/is/has method that writes anything              |
+------------------------------------------------------------------+

Practice Exercise

একটা metro card system-এ এই method আছে, আর operations team জানাচ্ছে "staff card check করলেই balance রহস্যজনকভাবে কমে যায়":

class MetroCard {
  private balance: number;
  private trips: Trip[] = [];
 
  // Returns remaining balance... and does WHAT else? Read carefully.
  checkBalance(gateId: string): number {
    if (this.balance < 20) {
      this.sms.send(this.ownerPhone, "Low balance! Please recharge."); // effect 1
    }
    this.trips.push(new Trip(gateId, new Date()));                     // effect 2 (!!)
    this.balance -= 1; // Rs.1 "balance inquiry charge"                // effect 3 (!!!)
    return this.balance;
  }
}

কাজগুলো:

  1. এই "check"-এর ভেতরে লুকিয়ে থাকা প্রতিটা side effect list করো। প্রতিটার জন্য বলো — একজন station staff পাঁচবার card check করলে কে ক্ষতিগ্রস্ত হয়। কোন effect সবচেয়ে খারাপ CQS violation, আর কেন?
  2. পুরো split করো, চিত্র ৮-এর প্রতিটা intermediate state দেখাও: (a) pure getBalance() query, (b) command(s) — decide করো SMS warning, trip logging, আর inquiry charge এক command-এ নাকি আলাদা আলাদা (hint: এগুলো কি সবসময় একই কারণে একসাথে হয়?), (c) temporary glue method, (d) caller-রা migrate হওয়ার পর final state।
  3. Codebase-এ তিনটা caller আছে: entry gate (যেটা সত্যিই trip শুরু করে), staff handheld checker (যেটা শুধু পড়া উচিত), আর customer app (যেটা শুধু পড়া উচিত)। Refactoring-এর পর প্রতিটা caller-এর code লেখো — query, command, বা দুটোই বেছে নিয়ে। তারপর প্রতিটা caller চিত্র ৩-এর pie-তে কোন slice-এ পড়ে বলো।
  4. তোমার নতুন getBalance()-এর জন্য recipe-র ধাপ ৬-এর purity regression test লেখো।
  5. চিন্তার প্রশ্ন: entry gate-কে balance check করতে আর fare কাটতে একটা step-এ করতে হবে — দুজন একই সাথে একই card দুটো gate-এ tap করলে তোমার split কি race condition বানাতে পারে? এটা কোন CQS exception-এর মতো, চিত্র ৪-এর quadrant-এ এই scenario plot করো, আর ওই operation safe রাখার একটা উপায় suggest করো।
  6. শেষে, rename check: তোমার শেষ method-গুলোতে কোথাও "And" লাগছে? যদি লাগে, কিছু এখনো একসাথে আটকে আছে — আবার ভাগ করো।

যখন তোমার staff checker একটা card একশোবার check করতে পারবে আর balance একটা টাকাও কমবে না — তখন চায়ের দোকানের খাতা আবার সৎ হয়ে গেছে, জামাল ভাই প্রতিদিন হিসাব জিজ্ঞেস করতে পারবেন, আর Bertrand Meyer তোমাকে এক কাপ চা খাওয়াতেন। আলাদা, পরিষ্কার itemised bill-এ, অবশ্যই।

সচরাচর জিজ্ঞাসা

Command-Query Separation (CQS) আসলে কী?
এটা Bertrand Meyer-এর একটা design principle — তিনি Eiffel language বানিয়েছিলেন, আর ১৯৮৮ সালে Object-Oriented Software Construction বইয়ে এই idea দিয়েছিলেন। নিয়মটা সহজ: প্রতিটা method হয় query হবে (data return করবে, কিছু বদলাবে না) অথবা command হবে (state বদলাবে, কিছু return করবে না) — কখনো দুটো একসাথে না। এক লাইনে মনে রাখার উপায়: question করলে answer বদলানো উচিত না।
CQS ভাঙা method কীভাবে চিনবো?
তিনটা সহজ চিহ্ন। এক — নামের মধ্যে 'and' লাগে সৎ হতে গেলে, যেমন getBalanceAndApplyFee। দুই — method একসাথে value return করছে আর field-এ assign করছে, database-এ লিখছে, বা message পাঠাচ্ছে। তিন — caller-রা অদ্ভুত আচরণ করছে — কেউ শুধু পড়তে call করছে কিন্তু কিছু একটা বদলে যাচ্ছে, বা কেউ দুইবার call করতে ভয় পাচ্ছে। এগুলোর যেকোনো একটা দেখলে বুঝবে query আর command একসাথে আটকে আছে।
CQS-এর কি কোনো সৎ exception আছে?
হ্যাঁ, কয়েকটা বিখ্যাত exception আছে। stack.pop(), queue.dequeue(), আর iterator.next() ইচ্ছা করেই value return করে আর state বদলায় — কারণ concurrent code-এ এগুলো ভাগ করলে race condition হয়, দুটো thread-এর মাঝখানে অন্য thread ঢুকে যেতে পারে। Fowler নিজেই বলেছেন এই ক্ষেত্রে CQS ভাঙা ঠিক আছে। এটা একটা strong default rule, iron law না — exception হবে rare আর পরিচিত।
একটার বদলে দুটো method call করলে কি code slow হবে না?
সাধারণত বোঝাই যায় না। একটা pure query সাধারণত খুব সস্তা, আর side effect না থাকায় এখন safely cache করা যায়। আসল exception হলো যখন command-কে value compute করতেই বড় cost লাগে, অথবা query+command একসাথে atomic হতেই হবে concurrency-র জন্য — সেক্ষেত্রে আগে measure করো, তারপর সিদ্ধান্ত নাও।
CQS আর CQRS কি একই জিনিস?
সম্পর্কিত কিন্তু আলাদা scale। CQS হলো Meyer-এর method-level rule: একটা method হয় query নয় command। CQRS (Command Query Responsibility Segregation) এই একই idea পুরো architecture-এ নিয়ে যায়: read আর write-এর জন্য আলাদা model — কখনো আলাদা service আর database পর্যন্ত। আগে CQS শেখো, তারপর CQRS বুঝবে।

আরো দেখো

সম্পর্কিত পাঠ

Rename Method: দোকানের সাইনবোর্ড যেন সত্যি কথা বলে

Rename Method সহজভাবে বোঝানো — কেন method-এর নাম সত্যিটা বলতে হবে, কীভাবে নিরাপদে নাম বদলাতে হয় delegate পদ্ধতিতে, আর কীভাবে VS Code (F2) ও JetBrains (Shift+F6) দিয়ে এটা এক কীতেই সারা যায়।

আরও পড়ুন

Remove Setting Method: কিছু জিনিস কলমে লেখা, পেন্সিলে না

Remove Setting Method সহজ ভাষায় — কেন এমন একটা field যেটা তৈরির পরে কখনো বদলানো উচিত না তার setter রাখা ঠিক না, আর কীভাবে read-only field, init-only property, আর record দিয়ে 'এটা বদলিও না' কথাটাকে compiler-এর গ্যারান্টিতে বদলানো যায়।

আরও পড়ুন

Extract Method: একটা বিশাল ফাংশনকে ছোট ছোট নামওয়ালা helper-এ ভাগ করো

Extract Method ধাপে ধাপে শিখে নাও। একটা লম্বা ফাংশন থেকে এলোমেলো block বের করে তাকে একটা পরিষ্কার নাম দাও, আর তোমার কোডকে একটা সহজ to-do লিস্টের মতো পড়ার যোগ্য করে তোলো।

আরও পড়ুন

Long Method: যখন একটা function সব কিছু করতে চায়

Long Method code smell শিখো সহজ গল্পের মাধ্যমে — TypeScript আর C# example সহ, Extract Method দিয়ে step-by-step refactoring। একদম beginner-friendly গাইড।

আরও পড়ুন