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

SOLID ১ — single responsibility আর open-closed principle

SOLID, Part 1 — The Single Responsibility Principle and the Open-Closed Principle

Taught in Bangla, this lesson introduces SOLID and its origins, then covers the Single Responsibility Principle as responsibility to one actor rather than doing one thing, finding actors and splitting a class at function, class and module level without over-splitting, Meyer's and Martin's Open-Closed Principle, refactoring a type switch into a registry, plugins and higher-order functions, and spotting premature abstraction.

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

  • SOLID-এর পাঁচটি অক্ষর কী, কোথা থেকে এলো, আর এগুলো আইন নয়, নকশার বিচারবুদ্ধি কেন
  • SRP-এর দুই সংজ্ঞা — “পরিবর্তনের একটি কারণ” আর “একটি actor” — আর “এক কাজ করে” ভুল পড়া কেন
  • একটি class-এর method থেকে actor খুঁজে ভাগ করা, function-class-module তিন স্তরে, আর কখন থামবেন
  • Meyer-এর আর Martin-এর OCP, type-switch থেকে registry, plugin, configuration আর higher-order function
  • কোন দিকে বদল আসবে তার অনুমান, অকালপক্ব abstraction চেনা, আর দুই নীতির test-এর লাভ

১ · SOLID — পাঁচটি অক্ষর, একটি লক্ষ্য

পর্ব ২৫-এ (Modularity, coupling আর cohesion) আপনি ভালো নকশার দুটি মাপকাঠি দেখেছেন: module-গুলো একে অপরের ওপর যত কম নির্ভর করে (কম coupling), আর একটি module-এর ভেতরের অংশগুলো যত বেশি একই উদ্দেশ্যে কাজ করে (বেশি cohesion), নকশা তত সহজে বদলানো যায়। পর্ব ২৬-এ (নকশার নীতি) DRY, KISS, YAGNI আর separation of concerns দেখেছেন। সেগুলো ছিল সাধারণ উপদেশ। এবার object-oriented নকশার জন্য সবচেয়ে পরিচিত পাঁচটি নীতি — একসাথে যাদের নাম SOLID.

অক্ষরনীতিএক লাইনেকোথায়
SSingle Responsibility Principle (SRP)একটি module-এর বদলের কারণ আসবে একটিমাত্র actor থেকেএই পর্ব
OOpen-Closed Principle (OCP)নতুন আচরণ যোগ হবে নতুন কোড লিখে, পুরোনো কোড বদলে নয়এই পর্ব
LLiskov Substitution PrincipleSubtype-কে base type-এর জায়গায় বসালে প্রোগ্রাম ঠিক থাকবেপর্ব ২৮
IInterface Segregation Principleকাউকে এমন method-এর ওপর নির্ভর করতে বাধ্য করবেন না, যা সে ব্যবহার করে নাপর্ব ২৮
DDependency Inversion Principleউঁচু স্তরের নীতি নিচু স্তরের খুঁটিনাটির ওপর নয়, abstraction-এর ওপর নির্ভর করবেপর্ব ২৮

নীতিগুলো একদিনে আসেনি। Open-closed principle-এর নাম Bertrand Meyer-এর বই Object-Oriented Software Construction (১৯৮৮) থেকে। Robert C. Martin (অনেকে “Uncle Bob” বলেন) ১৯৯০-এর দশকে C++ Report পত্রিকায় এই নীতিগুলো নিয়ে একাধিক প্রবন্ধ লেখেন, আর প্রায় ২০০০ সালে “Design Principles and Design Patterns” নামের একটি লেখায় সেগুলো একসাথে উপস্থাপন করেন। “SOLID” — প্রথম অক্ষরগুলো সাজিয়ে বানানো শব্দটি — Martin নিজে বানাননি; প্রচলিত বর্ণনা অনুযায়ী Michael Feathers ২০০৪ সালের কাছাকাছি এই ক্রম প্রস্তাব করেন। সঠিক বছর নিয়ে উৎসগুলোয় সামান্য ভিন্নতা আছে, তাই “প্রায়” শব্দটি মনে রাখুন।

পাঁচটির পেছনে একটিই লক্ষ্য: বদল যেন সস্তা আর নিরাপদ হয়। বড় খরচ প্রথমবার লেখায় নয়, পরের বছরগুলোর বদলে (পর্ব ৩). একটি অনুরোধ এলে কতগুলো file খুলতে হয়, কতগুলো test আবার চালাতে হয়, আর কতজনের কোড ভাঙার ভয় থাকে — SOLID এই প্রশ্নগুলোর উত্তর ছোট রাখার কৌশল।

একটি বড় বাড়ির বৈদ্যুতিক লাইনের কথা ভাবুন। সব ঘরের বাতি, পাখা, ফ্রিজ আর পানির পাম্প যদি একটিমাত্র তারে, একটিমাত্র সুইচে থাকে, তবে রান্নাঘরে নতুন একটি সকেট লাগাতে গেলে পুরো বাড়ির বিদ্যুৎ বন্ধ রাখতে হয়। ভালো মিস্ত্রি আলাদা আলাদা সার্কিট বানান (SRP), আর বোর্ডে কয়েকটি ফাঁকা জায়গা রাখেন, যাতে নতুন সার্কিট যোগ করতে পুরোনো তার কাটতে না হয় (OCP). কিন্তু তিনি প্রতিটি দেয়ালে দশটি ফাঁকা সকেটও বসান না — যেখানে প্রয়োজন আসবে বলে বোঝা যায়, সেখানেই রাখেন।

মূল ধারণা

SOLID আইন নয়, প্রশ্ন করার একটি ছাঁচ। প্রতিটি নীতি একটি নির্দিষ্ট ধরনের ব্যথা কমায়; ব্যথা না থাকলে নীতির জন্য কোড জটিল করা নিজেই একটি ভুল। এই পর্বে দুটি নীতির সাথে আমরা তাদের সীমাও শিখব।

২ · SRP-এর আসল কথা — “এক কাজ” নয়, “এক actor”

সবচেয়ে বেশি শোনা ব্যাখ্যা: “একটি class একটিই কাজ করবে।” শুনতে সহজ, কিন্তু Martin নিজেই বলেছেন এটি ভুল পড়া। “একটি function একটি কাজ করবে” — এটি function ছোট রাখার আলাদা একটি পরামর্শ (পর্ব ৪১-এ clean code). Single responsibility principle-এর কথা অন্য জায়গায়: কে এই কোডের বদল চায়।

সংজ্ঞাটি সময়ের সাথে পরিষ্কার হয়েছে:

  • পুরোনো রূপ (Martin-এর Agile Software Development: Principles, Patterns, and Practices, ২০০২-এর কাছাকাছি): “A class should have only one reason to change.” — একটি class-এর বদলানোর কারণ থাকবে একটিই।
  • পরের রূপ (Clean Architecture, ২০১৭): “A module should be responsible to one, and only one, actor.” — একটি module-এর জবাবদিহি থাকবে একটি, এবং কেবল একটি, actor-এর কাছে।

“কারণ” শব্দটি ঝাপসা ছিল: bug ঠিক করা কি একটি কারণ? নতুন library? Martin তাই কারণটিকে মানুষের সাথে বাঁধলেন। এখানে actor মানে একজন ব্যক্তি নয়, একদল মানুষ (বা একটি ভূমিকা), যাঁরা একই ধরনের বদল চান — হিসাব বিভাগ, মানবসম্পদ, অপারেশন দল, পরীক্ষা বোর্ড। পর্ব ২২-২৩-এর use case-এর actor-এর সাথে মিল আছে (দুটিই ভূমিকা), কিন্তু প্রশ্ন আলাদা: use case-এ “কে system ব্যবহার করেন”, SRP-তে “কে এই অংশের বদল চাইবেন”।

একটি স্কুলের অফিসের একটিই খাতা ভাবুন, যাতে একসাথে লেখা: বেতনের হিসাব (হিসাবরক্ষক দেখেন), অভিভাবকদের ফোন নম্বর (অফিস সহকারী দেখেন), আর বোর্ড পরীক্ষার রেজিস্ট্রেশন নম্বর (পরীক্ষা শাখা দেখে)। হিসাবরক্ষক বেতনের ঘরের মাপ বড় করতে পাতার দাগ নতুন করে টানলেন — পরের দিন পরীক্ষা শাখা দেখল, রেজিস্ট্রেশন নম্বরের ঘর ছোট হয়ে গেছে। খাতাটি “একটি কাজ” করছিল — স্কুলের তথ্য রাখা — কিন্তু তিনটি দলের কাছে দায়বদ্ধ ছিল।

একটি পরিচিত উদাহরণের ভাবনা

Clean Architecture-এ Martin একটি বেতন-ব্যবস্থার উদাহরণ দেন, যার ভাবনাটি আমরা সাধারণভাবে বলি: একটি Employee class-এ তিনটি method — calculatePay() (হিসাব বিভাগ ঠিক করে বেতন কীভাবে হবে), reportHours() (মানবসম্পদ বিভাগ ঠিক করে কাজের ঘণ্টার report কেমন হবে), আর save() (database দেখেন যাঁরা, তাঁরা ঠিক করেন কীভাবে সংরক্ষণ হবে)। তিনটি method, তিনটি actor. ধরুন দুটি method একটি সাধারণ helper ব্যবহার করে — “নিয়মিত ঘণ্টা” হিসাবের একই function. হিসাব বিভাগের অনুরোধে developer helper-টি বদলালেন; মানবসম্পদের report-এর সংখ্যাও চুপচাপ বদলে গেল, আর কেউ টের পেলেন না, কারণ মানবসম্পদ দলের কাছে কেউ জিজ্ঞেসই করেনি।

Martin এই ধরনের ভুলের নাম দিয়েছেন accidental duplication: দুটি জিনিস আজ একই রকম দেখায় বলে আমরা এক করে ফেলি, অথচ তাদের বদলের কারণ আলাদা। পর্ব ২৬-এর DRY বলে জ্ঞানের পুনরাবৃত্তি করবেন না — কিন্তু দুটি দলের দুটি আলাদা নিয়ম আকস্মিকভাবে একই সূত্রে লেখা হলে সেটা একই “জ্ঞান” নয়।

চালিয়ে দেখুন — একটি অনুরোধ, দুটি নকশা

একটি কাল্পনিক courier কোম্পানির মাসিক report. ReportService তিনটি কাজ করে: মোট টাকা যোগ (finance দল), report-এর চেহারা (accounts দল), আর archive file-এর নাম (ops দল)। তারিখের format একটি setting-এ রাখা, যা দুই জায়গায় লাগে। Accounts দল চাইল তারিখ 05-01-2026-এর বদলে 05/01/2026 দেখাতে। নিচের কোড একই অনুরোধ দুটি নকশায় চালায়, আর দেখায় কী কী বদলাল।

from datetime import date
ROWS = [("P-101", 450, date(2026, 1, 5)), ("P-102", 1200, date(2026, 1, 5))]

class ReportService:                 # BEFORE: one class, three actors
    date_fmt = "%d-%m-%Y"            # one setting shared by two actors
    def total(self, rows):           # finance team: how money is added
        return sum(fee for _, fee, _ in rows)
    def format(self, rows):          # accounts team: what the sheet looks like
        r = rows[0]
        return f"{r[2].strftime(self.date_fmt)} {r[0]} {r[1]} ... TOTAL {self.total(rows)}"
    def save(self, rows):            # ops team: archive file name
        return f"report_{rows[0][2].strftime(self.date_fmt)}.txt"

class TotalCalculator:               # AFTER: one class per actor
    def total(self, rows): return sum(fee for _, fee, _ in rows)
class ReportFormatter:
    def __init__(self, fmt="%d-%m-%Y"): self.fmt = fmt
    def format(self, rows):
        r = rows[0]
        return f"{r[2].strftime(self.fmt)} {r[0]} {r[1]} ... TOTAL {TotalCalculator().total(rows)}"
class ReportArchive:
    def save(self, rows): return f"report_{rows[0][2].strftime('%d-%m-%Y')}.txt"

def compare(title, old, new):
    print(title)
    for part, a, b in zip(("total", "sheet", "archive"), old, new):
        print(f"  {part:8}: {a}  ->  {b}  " + ("same" if a == b else "CHANGED"))

svc = ReportService(); old = (svc.total(ROWS), svc.format(ROWS), svc.save(ROWS))
svc.date_fmt = "%d/%m/%Y"            # accounts team asks for slashes
compare("BEFORE: one ReportService", old, (svc.total(ROWS), svc.format(ROWS), svc.save(ROWS)))
calc, fmt, arc = TotalCalculator(), ReportFormatter(), ReportArchive()
old = (calc.total(ROWS), fmt.format(ROWS), arc.save(ROWS))
fmt = ReportFormatter("%d/%m/%Y")    # the same request, now in one class
compare("AFTER: split by actor", old, (calc.total(ROWS), fmt.format(ROWS), arc.save(ROWS)))

আউটপুট:

BEFORE: one ReportService
  total   : 1650  ->  1650  same
  sheet   : 05-01-2026 P-101 450 ... TOTAL 1650  ->  05/01/2026 P-101 450 ... TOTAL 1650  CHANGED
  archive : report_05-01-2026.txt  ->  report_05/01/2026.txt  CHANGED
AFTER: split by actor
  total   : 1650  ->  1650  same
  sheet   : 05-01-2026 P-101 450 ... TOTAL 1650  ->  05/01/2026 P-101 450 ... TOTAL 1650  CHANGED
  archive : report_05-01-2026.txt  ->  report_05-01-2026.txt  same

প্রথম নকশায় accounts দলের ছোট্ট অনুরোধে ops দলের archive file-এর নামও বদলে গেল — report_05/01/2026.txt. বেশিরভাগ operating system-এ / folder আলাদা করার চিহ্ন, তাই এই নামে file লেখা হয় ভুল জায়গায়, বা লেখাই যায় না। Ops দল কিছু চায়নি, অথচ তাদের কাজ ভাঙল; আর code review-তে diff দেখে মনে হতো “শুধু একটি format string বদলেছে”। দ্বিতীয় নকশায় প্রতিটি actor-এর নিজের class, নিজের format; একই অনুরোধে শুধু sheet বদলায়। খেয়াল করুন, দ্বিতীয় নকশায় তারিখের format দুই জায়গায় লেখা — এটা ইচ্ছাকৃত, কারণ দুটি আলাদা দলের আলাদা সিদ্ধান্ত।

SRP: আগে আর পরে (UML-ধাঁচের class sketch)

One class answering to three actors vs one class per actor; dashed open arrow = dependency

SRP: আগে একটি class তিন actor, পরে প্রতি actor-এ একটি class আগে — এক class, তিন actor ReportService - date_fmt: str + total(rows) + format(rows) + save(rows) accounts + ops ⚠ finance accounts ops পরে — প্রতি actor-এ একটি class finance accounts ops TotalCalculator + total(rows) ReportFormatter - fmt: str + format(rows) ReportArchive + save(rows) ড্যাশ রেখা + খোলা তীর = dependency: Formatter মোট টাকার জন্য TotalCalculator ব্যবহার করে। Archive কারও ওপর নির্ভর করে না।
ছবি ২৭.১ — আগের নকশায় একটি attribute দুই actor-এর মধ্যে ভাগ করা (লাল) — ঠিক সেখানেই কোডের bug-টি হয়েছিল। পরের নকশায় প্রতিটি বাক্সের ওপরে একটিই actor. Class diagram-এর পূর্ণ নোটেশন পর্ব ২৯-এ (UML class diagram).

৩ · Actor আর পরিবর্তনের কারণ খোঁজা

SRP মানার কাজ আসলে একটি অনুসন্ধান: প্রতিটি অংশের বদল কে চাইবেন? কয়েকটি ব্যবহারিক প্রশ্ন সাহায্য করে:

  • “এই method-এর আচরণ বদলাতে হলে কার অনুমতি লাগবে?” যাঁর অনুমতি, তিনিই actor. বেতনের সূত্রের জন্য হিসাব বিভাগ, receipt-এর নকশার জন্য সামনের ডেস্ক।
  • “কোন meeting-এ এই বদলের সিদ্ধান্ত হবে?” দুটি method-এর সিদ্ধান্ত যদি দুটি আলাদা meeting-এ, দুটি আলাদা বিভাগে হয়, তারা আলাদা actor-এর।
  • Git-এর ইতিহাস দেখুন। কোন file কারা, কোন অনুরোধে বদলেছেন — অংশ ৯-এ বাস্তব একটি git log দেখবেন।
  • নাম পরীক্ষা। Class-এর নাম দিতে “and” বা “Manager”, “Helper”, “Util” লাগলে সন্দেহ করুন: FeeAndReceiptAndSmsManager নিজেই নিজের সমস্যা বলে দিচ্ছে।

এখন একটি ছোট বিশ্লেষক। একটি কাল্পনিক স্কুলের বেতন-ব্যবস্থার প্রতিটি method-এর পাশে লেখা আছে কোন actor তার বদল চান। প্রোগ্রামটি class ধরে actor গোনে, একাধিক actor হলে SRP লঙ্ঘন বলে কীভাবে ভাঙা যায় দেখায়, আর উল্টো দিকের একটি সতর্কসংকেতও খোঁজে — একই actor-এর জন্য খুব ছোট ছোট আলাদা class.

// "Reasons to change" analyser for a fictional school-fee system.
// Each method is tagged with the actor (the group of people) who asks
// for its changes. A class that serves more than one actor is flagged.
const classes = {
  FeeManager: { calculateFee: "accounts", applyScholarship: "accounts",
                printReceipt: "front-desk", sendSmsReminder: "guardian-office",
                exportBoardCsv: "exam-board" },
  StudentRepo: { findById: "it", save: "it" },
  ReceiptLayout: { header: "front-desk" },
  ReceiptFooter: { footer: "front-desk" },
};
const actorsOf = {};                      // actor -> [class.method]
for (const [cls, methods] of Object.entries(classes)) {
  const byActor = {};
  for (const [m, actor] of Object.entries(methods)) {
    (byActor[actor] ??= []).push(m);
    (actorsOf[actor] ??= new Set()).add(cls);
  }
  const n = Object.keys(byActor).length;
  console.log(`${cls}: ${n} actor(s) ${n > 1 ? "-> SRP violation" : "ok"}`);
  if (n > 1) for (const [a, ms] of Object.entries(byActor))
    console.log(`   split -> ${a}: ${ms.join(", ")}`);
}
for (const [actor, set] of Object.entries(actorsOf)) {
  const solo = [...set].filter(c => Object.keys(classes[c]).length === 1 &&
    Object.values(classes[c]).every(a => a === actor));
  if (solo.length > 1)
    console.log(`hint: ${solo.join(" + ")} change together for "${actor}"` +
                " -> maybe over-split, consider merging");
}

আউটপুট:

FeeManager: 4 actor(s) -> SRP violation
   split -> accounts: calculateFee, applyScholarship
   split -> front-desk: printReceipt
   split -> guardian-office: sendSmsReminder
   split -> exam-board: exportBoardCsv
StudentRepo: 1 actor(s) ok
ReceiptLayout: 1 actor(s) ok
ReceiptFooter: 1 actor(s) ok
hint: ReceiptLayout + ReceiptFooter change together for "front-desk" -> maybe over-split, consider merging

FeeManager চারটি দলের কাছে দায়বদ্ধ — হিসাব (দুটি method একসাথে), সামনের ডেস্ক, অভিভাবক-যোগাযোগ অফিস আর পরীক্ষা বোর্ড। বিশ্লেষক চারটি নতুন class-এর প্রস্তাব দিল। খেয়াল করুন calculateFee আর applyScholarship একসাথে থাকল — দুটোই হিসাব বিভাগের নিয়ম, তাই তাদের আলাদা করা দরকার নেই। শেষ লাইনটি উল্টো দিকের সংকেত: receipt-এর ওপরের অংশ আর নিচের অংশ দুটি আলাদা class, অথচ দুটিই সামনের ডেস্কের জন্য আর প্রায় নিশ্চিত একসাথে বদলাবে। FeeManager-এর printReceipt-ও একই দলের — ভাঙার পরে তিনটি এক class-এ যাবে। এটি খেলনা নিয়ম, বিচার নয়; ট্যাগ ভুল হলে উত্তরও ভুল।

এবার ReportService-কে হাতে ধরে ভাঙি — শুধু কোড নয়, প্রতিটি সিদ্ধান্তের কারণসহ। “পরে” চাপুন।

ReportService ভাঙার হিসাব
কাজ
অংশগুলো
Actor সংখ্যা
ঝুঁকি
  1. সব public method আর ভাগ করা data-র তালিকা। total, format, save আর setting date_fmt. Private helper-গুলোও তালিকায় রাখুন — লুকানো সংযোগ প্রায়ই সেখানেই থাকে।
  2. প্রতিটির পাশে actor. প্রশ্ন: “এর বদল কে চাইবেন?” মোট টাকার নিয়ম finance-এর, report-এর চেহারা accounts-এর, archive-এর নাম ops-এর। তিনটি actor — অর্থাৎ বদলের তিনটি আলাদা উৎস।
  3. ভাগ করা জিনিস খুঁজুন। date_fmt দুই দলই ব্যবহার করে। এটাই বিপদের জায়গা। সিদ্ধান্ত: দুই দলের তারিখ আজ এক রকম, কিন্তু সিদ্ধান্ত আলাদা — তাই প্রত্যেকে নিজের format রাখবে। এটা DRY-এর লঙ্ঘন নয়, দুটি আলাদা জ্ঞান।
  4. Actor ধরে class. প্রতিটি actor-এর জন্য একটি class, নাম তার দায়িত্ব দিয়ে: TotalCalculator, ReportFormatter, ReportArchive. প্রতিটির ভেতরে একাধিক method থাকতেই পারে, যদি সবগুলো একই actor-এর হয়।
  5. নির্ভরতার দিক ঠিক করুন। Report-এ মোট টাকা দেখাতে হয়, তাই ReportFormatter TotalCalculator-কে ব্যবহার করে — ছবি ২৭.১-এর ড্যাশ তীর। উল্টো দিকে কোনো নির্ভরতা নেই, তাই চক্রও নেই। মোট টাকার নিয়ম বদলালে report-এর সংখ্যা বদলাবে — এটা তো চাওয়াই হয়।
  6. পুরোনো caller-দের ভাঙবেন না। যাঁরা আগে ReportService ডাকতেন, তাঁদের জন্য একটি পাতলা ReportService রাখা যায় যা শুধু তিনটি class-কে ডেকে দেয় — একে facade বলে (পর্ব ৩৪-এ structural pattern). এর ভেতরে কোনো ব্যবসার নিয়ম নেই, তাই এর নিজের কোনো actor-ও নেই।
  7. Test-ও ভাগ হলো। এখন প্রতিটি দলের নিয়মের test আলাদা file-এ। Accounts দলের তারিখের অনুরোধে শুধু formatter-এর test বদলায়; archive-এর test একই থাকে এবং পাস করে — যা প্রমাণ করে ops-এর কিছু ভাঙেনি।

৪ · SRP-এর তিন স্তর — আর বেশি ভাঙার বিপদ

“Module” শব্দটি Martin ইচ্ছা করেই ব্যবহার করেছেন: এক file, এক class, এক package বা এক service — যেকোনো স্তরে একই প্রশ্ন চলে। তবে প্রতিটি স্তরে প্রশ্নের চেহারা একটু আলাদা।

Function-এর স্তরে

এখানে “এক কাজ” পরামর্শটি সত্যিই কাজে লাগে: একটি function-এ হিসাব, ছাপানো আর file লেখা মিশিয়ে ফেললে ভাগ করুন। কিন্তু এটা clean code-এর নিয়ম (পর্ব ৪১); SRP-র actor-প্রশ্ন সাধারণত এক ধাপ ওপরে ওঠে।

function

Class-এর স্তরে

SRP-এর সবচেয়ে পরিচিত জায়গা। একটি class-এর সব method একই actor-এর হলে, সংখ্যায় যতই হোক, ঠিক আছে। God class — যে class সবার সব কাজ করে — এর উল্টো ছবি।

class

Module বা service-এর স্তরে

Package, library বা service ভাগ করার সময়ও একই কথা: এক দলের সিদ্ধান্ত এক জায়গায়। এখানে Conway's law (পর্ব ১-এ উল্লেখ) মনে রাখুন — দলের কাঠামো আর কোডের কাঠামো একে অপরকে টানে।

module / service

SRP-এর সাথে পুরোনো একটি ধারণার গভীর মিল আছে: David Parnas-এর ১৯৭২ সালের বিখ্যাত প্রবন্ধ “On the Criteria to Be Used in Decomposing Systems into Modules”. Parnas বলেছিলেন, module ভাগ করুন যে সিদ্ধান্তগুলো বদলাতে পারে সেগুলো ধরে — প্রতিটি module একটি সিদ্ধান্ত লুকিয়ে রাখবে, যাতে সিদ্ধান্ত বদলালে শুধু সেই module বদলায়। এর নাম information hiding. Martin-এর actor সেই সিদ্ধান্তের মালিককে নাম দিল।

বেশি ভাঙা — উল্টো দিকের রোগ

SRP শেখার পরে অনেকে এমন কোড লেখেন: InvoiceTotalAdder, InvoiceTaxAdder, InvoiceLineCounter, InvoiceRounder — প্রতিটিতে একটি করে method. সমস্যা কী?

  • একটি বদলে অনেক file. VAT-এর নিয়ম বদলালে tax, total আর rounding তিনটি file খুলতে হয়, কারণ তারা আসলে একই নিয়মের অংশ। Martin Fowler এই অবস্থার নাম দিয়েছেন shotgun surgery — একটি বদলের জন্য অনেক জায়গায় ছোট ছোট কাটাছেঁড়া (পর্ব ৪২-এ code smell).
  • Cohesion কমে যায়। যে জিনিসগুলো একসাথে বদলায়, সেগুলো ছড়িয়ে পড়লে module-এর ভেতরের মিল কমে, বাইরের নির্ভরতা বাড়ে — পর্ব ২৫-এর ঠিক উল্টো লক্ষ্য।
দুই দিকের নিয়ম

Martin নিজে SRP-কে এভাবেও বলেছেন: “Gather together the things that change for the same reasons. Separate those things that change for different reasons.” — যা একই কারণে বদলায় তা একসাথে রাখুন; যা ভিন্ন কারণে বদলায় তা আলাদা করুন। প্রথম অর্ধেক ভুললে বেশি ভাঙা, দ্বিতীয় অর্ধেক ভুললে god class.

৫ · OCP — Meyer থেকে Martin

Open-closed principle-এর ধারণা Meyer-এর; তাঁর ১৯৮৮-এর বইয়ের ভাষা ছিল “Modules should be both open and closed.” আজকের প্রচলিত বাক্যটি Martin-এর ১৯৯৬-এর লেখায় Meyer-এর নামে উদ্ধৃত: “Software entities (classes, modules, functions…) should be open for extension, but closed for modification.” — বাড়ানোর জন্য খোলা, বদলানোর জন্য বন্ধ। প্রথম শুনলে স্ববিরোধী মনে হয়: বদল না করে নতুন আচরণ আসবে কীভাবে?

একটি মাল্টিপ্লাগ ভাবুন। নতুন একটি charger লাগাতে আপনি দেয়ালের তার খুলে নতুন করে জোড়া দেন না; প্লাগটি একটি নির্দিষ্ট মাপের ছিদ্রে ঢুকিয়ে দেন। ছিদ্রের মাপটি একটি চুক্তি। যে যন্ত্র চুক্তি মানে, সে যোগ হতে পারে (open), অথচ দেয়ালের তারে হাত পড়ে না (closed). OCP-তে সেই চুক্তির নাম abstraction — যেমন একটি interface.

Meyer-এর OCP (১৯৮৮) — inheritance দিয়ে

Meyer-এর যুগে একটি বড় সমস্যা ছিল: একটি module compile হয়ে library হয়ে গেলে অনেক দল সেটি ব্যবহার করছে। তাকে বদলালে সবাইকে আবার compile, test আর release করতে হয়। Meyer-এর সমাধান: module-টি “closed” — স্থির, documentation-সহ, অন্যরা ব্যবহার করছে। নতুন আচরণ চাইলে তার একটি subclass বানান (inheritance), পুরোনোটি অক্ষত রেখে। অর্থাৎ Meyer-এর OCP-তে বাড়ানোর পথ ছিল implementation inheritance — parent-এর কোড উত্তরাধিকারে নিয়ে কিছু অংশ বদলে দেওয়া।

Martin-এর polymorphic OCP — abstraction আর বদলযোগ্য implementation

১৯৯০-এর দশকে Martin আর অন্যরা দেখলেন, implementation inheritance-এর নিজস্ব বিপদ আছে — parent বদলালে child-রা অপ্রত্যাশিতভাবে ভাঙে (“fragile base class” সমস্যা), আর গভীর উত্তরাধিকারের গাছ বোঝা কঠিন। Gang of Four-এর বইও (১৯৯৪) পরামর্শ দেয় “inheritance-এর চেয়ে composition” পছন্দ করতে। তাই Martin-এর রূপে OCP দাঁড়াল এভাবে:

  • যে কোড স্থির থাকবে (ব্যবসার মূল ধারা), সে একটি abstraction-এর ওপর নির্ভর করবে — abstract class বা interface, যা শুধু বলে “কী করা যায়”, কীভাবে নয়।
  • প্রতিটি রূপ (variant) সেই abstraction-এর একটি আলাদা implementation — নতুন class, নতুন file.
  • Polymorphism — একই ডাকে ভিন্ন object-এর ভিন্ন আচরণ — দিয়ে মূল কোড না জেনেই সঠিক implementation চালায়।
দিকMeyer (১৯৮৮)Martin (১৯৯০-এর দশক থেকে)
বাড়ানোর পথSubclass বানিয়ে, parent-এর কোড উত্তরাধিকারেএকটি interface-এর নতুন implementation
“Closed” মানেপ্রকাশিত, স্থির module — অন্যরা ব্যবহার করছেযে কোড abstraction ব্যবহার করে, তাকে নতুন রূপের জন্য বদলাতে হয় না
নির্ভরতাChild নির্ভর করে parent-এর implementation-এর ওপরসবাই নির্ভর করে একটি পাতলা চুক্তির ওপর
ঝুঁকিFragile base class, গভীর গাছবেশি interface, অকারণ জটিলতা (অংশ ৮)

Python-এ abstraction লেখা যায় abc.ABC বা typing.Protocol দিয়ে, Java-তে interface দিয়ে, JavaScript-এ প্রায়ই শুধু একটি চুক্তি — “এই object-এ cost() method থাকবে”। বিস্তারিত দেখুন Python কোর্সের ABC, duck typing আর Protocol আর Java কোর্সের polymorphism আর dynamic dispatch পর্বে। Interface-কে একটি চুক্তি হিসেবে কীভাবে লিখবেন, তা পর্ব ৩১-এ (Interface আর contract).

৬ · Type-switch থেকে registry — OCP হাতেকলমে

OCP লঙ্ঘনের সবচেয়ে চেনা চেহারা হলো type-এর ওপর if/switch: “যদি standard হয় তবে এই, express হলে ওই, same-day হলে আরেকটি।” একটি জায়গায় একবার থাকলে তেমন ক্ষতি নেই। সমস্যা শুরু হয় যখন একই switch তিন-চার জায়গায় দেখা দেয় — দাম, পৌঁছানোর সময়, label, ফেরতের নিয়ম। নতুন একটি courier type এলে প্রতিটি switch খুঁজে বের করে একটি শাখা যোগ করতে হয়; একটি ভুলে গেলে চুপচাপ ভুল ফল। Fowler-এর Refactoring-এ একে repeated switch code smell বলা হয়, আর সমাধান হিসেবে “conditional-কে polymorphism দিয়ে বদলানো” refactoring দেওয়া আছে।

নিচের কোডে একটি কাল্পনিক courier-এর shipping খরচ দুইভাবে: আগে একটি type-switch, পরে strategy object-এর একটি registry — নাম থেকে object-এর একটি dictionary, যেখানে নতুন রূপ নিজেকে নিবন্ধন করে। তারপর “cold chain” (ঠান্ডা রেখে পাঠানো) নামে নতুন একটি type যোগ হয় — কোনো পুরোনো লাইন না বদলে।

from typing import Protocol

def cost_switch(kind, kg):           # BEFORE: every new courier edits this
    if kind == "standard":   return 60 + 15 * kg
    elif kind == "express":  return 120 + 25 * kg
    elif kind == "same_day": return 250 + 30 * kg
    raise ValueError(f"unknown courier: {kind}")

class Rate(Protocol):                # AFTER: a stable abstraction
    def cost(self, kg: int) -> int: ...
RATES: dict[str, Rate] = {}
def register(name):                  # plug-in point: add, never edit
    def wrap(cls):
        RATES[name] = cls()
        return cls
    return wrap

@register("standard")
class Standard:
    def cost(self, kg): return 60 + 15 * kg
@register("express")
class Express:
    def cost(self, kg): return 120 + 25 * kg
@register("same_day")
class SameDay:
    def cost(self, kg): return 250 + 30 * kg
def cost(kind, kg): return RATES[kind].cost(kg)   # closed: never edited
for k in RATES: print(f"{k:9} 2 kg: switch={cost_switch(k, 2)} registry={cost(k, 2)}")

@register("cold_chain")              # NEW courier type, added later in a
class ColdChain:                     # new file; nothing above is edited
    def cost(self, kg): return 300 + 40 * kg + (100 if kg > 5 else 0)
print("cold_chain 2 kg:", cost("cold_chain", 2), "| 6 kg:", cost("cold_chain", 6))
try: cost_switch("cold_chain", 2)
except ValueError as e: print("switch version:", e)

আউটপুট:

standard  2 kg: switch=90 registry=90
express   2 kg: switch=170 registry=170
same_day  2 kg: switch=310 registry=310
cold_chain 2 kg: 380 | 6 kg: 640
switch version: unknown courier: cold_chain

প্রথম তিন লাইন প্রমাণ করে দুই নকশা একই ফল দেয় — refactoring-এর আগে আর পরে আচরণ এক থাকতে হয়। হিসাব: standard ৬০ + ১৫ × ২ = ৯০, express ১২০ + ২৫ × ২ = ১৭০, same-day ২৫০ + ৩০ × ২ = ৩১০। এরপর ColdChain class যোগ হলো, আর cost() function বা অন্য কোনো class-এর একটি অক্ষরও বদলাল না — ২ কেজিতে ৩০০ + ৮০ = ৩৮০, ৬ কেজিতে ৩০০ + ২৪০ + ১০০ (৫ কেজির বেশির বাড়তি) = ৬৪০। Switch-এর সংস্করণ নতুন type চেনে না; তাকে খুলে elif যোগ করতেই হবে। @register একটি decorator — class তৈরি হওয়ার সাথে সাথে তার একটি object registry-তে রেখে দেয়। এটাই strategy pattern-এর একটি সরল রূপ; pattern-টির পূর্ণ আলোচনা পর্ব ৩৫-এ।

OCP: switch-এর বদলে abstraction (UML-ধাঁচের sketch)

Dashed line + hollow triangle = realization (implements); dashed line + open arrow = dependency

OCP: type-switch বনাম Rate interface আর তার implementation আগে — প্রতিটি নতুন type-এ পুরোনো কোড খোলা cost_switch(kind, kg): if kind == "standard": ... elif kind == "express": ... elif kind == "same_day": ... + elif kind == "cold_chain": ... ✎ edit পরে — নতুন type মানে নতুন class cost() closed «interface» Rate + cost(kg): int uses Standard Express SameDay ColdChain নতুন file ড্যাশ + ফাঁপা ত্রিভুজ = realization (interface মেনে চলে)
ছবি ২৭.২ — আগে নতুন type মানে পুরোনো function-এ লাল রেখা (বদল)। পরে cost() শুধু Rate চুক্তি জানে; নতুন ColdChain চুক্তি মেনে পাশে যোগ হয়। তিনটি পুরোনো class-এর তীর একটি ভাগ করা রেখায় মিলেছে — UML-এ এভাবে এক ত্রিভুজে একাধিক realization আঁকা চলে।

এবার একটি পূর্ণ গল্প: একটি কাল্পনিক courier-এর checkout-এ নতুন পেমেন্ট পদ্ধতি যোগ করা। লক্ষ করুন, OCP-তে পৌঁছাতে একবার পুরোনো কোড বদলাতেই হয় — সেটা refactoring; OCP-র সুফল আসে তার পরের বদলগুলোতে।

Checkout-এর অবস্থা
কোড
পুরোনো কোড বদল
Test
প্রশ্ন
  1. শুরুর অবস্থা। Checkout-এর তিনটি কাজ — টাকা নেওয়া, receipt-এর label, টাকা ফেরত — প্রতিটিতে একই if method == "cod" / "card" / "wallet". তিন পদ্ধতি × তিন কাজ = ৯টি test আছে।
  2. বদলের অক্ষ চিনুন। Issue tracker আর Git ইতিহাস বলছে, কাল্পনিক এই দলে গত বছরে দুবার নতুন পেমেন্ট পদ্ধতি এসেছে, আর প্রতিবার তিনটি জায়গা খুঁজতে হয়েছে। এটাই সম্ভাব্য বদলের দিক — এখানে খোলা রাখার যুক্তি আছে।
  3. চুক্তি বানান। তিনটি কাজ থেকেই চুক্তি: charge(amount), label(), refund(amount). চুক্তিতে শুধু সেটুকু, যা checkout-এর লাগে — বেশি নয় (এর কারণ পর্ব ২৮-এর interface segregation).
  4. প্রতিটি শাখা নিজের class-এ। Cod, Card, Wallet — প্রতিটি চুক্তির তিনটি method লেখে। নাম থেকে object-এর METHODS registry. এটা একবারের refactoring — পুরোনো কোড বদলায়, তাই আগের ৯টি test চালিয়ে নিশ্চিত হন আচরণ একই আছে।
  5. Switch সরান। Checkout এখন শুধু METHODS[name].charge(total) ডাকে। কোনো if বাকি নেই; checkout জানেই না কতগুলো পদ্ধতি আছে।
  6. নতুন অনুরোধ — voucher. নতুন file-এ Voucher class, আর অ্যাপ যেখানে সব অংশ জোড়া লাগায় (একে composition root বলে; পর্ব ৩৭-এ dependency injection) সেখানে এক লাইন: METHODS["voucher"] = Voucher(). সততার সাথে বলি — কোথাও না কোথাও কাউকে নতুন class-এর কথা জানতেই হয়। OCP-র “closed” মানে ব্যবসার মূল কোড বন্ধ, পুরো প্রোগ্রামের প্রতিটি লাইন নয়।
  7. Test-এর লাভ। Voucher-এর জন্য ৩টি নতুন test; পুরোনো ৯টির কোনো বদল নেই এবং সব পাস। Diff-এ শুধু একটি নতুন file আর এক লাইনের নিবন্ধন।

৭ · Extension point-এর নানা রূপ

নতুন আচরণ ঢোকানোর জায়গাকে প্রায়ই extension point বলা হয় (পর্ব ২৩-এর use case-এর extension point-এর সাথে নামের মিল কাকতালীয় নয় — চিন্তাটা একই)। কোডে এটি কয়েকভাবে বানানো যায়:

উপায়কীভাবেকখন মানায়
Polymorphism / strategyInterface আর তার implementation; caller শুধু interface জানেরূপগুলোর আচরণ উল্লেখযোগ্যভাবে আলাদা, একাধিক method লাগে
Pluginমূল প্রোগ্রাম একটি নিবন্ধনের ব্যবস্থা দেয়; বাইরের কোড নিজেকে যোগ করে, অনেক সময় আলাদা package হিসেবেঅন্য দল বা অন্য প্রতিষ্ঠান বাড়াবে; মূল কোড তাদের হাতে নেই
Configurationআচরণ data-তে: দামের টেবিল, নিয়মের ক্রম, চালু-বন্ধ সুইচ (feature flag)রূপগুলো একই ছাঁচের, শুধু সংখ্যা বা ক্রম আলাদা
Higher-order functionFunction-কে argument হিসেবে দেওয়া বা map-এ রাখাপ্রতিটি রূপ একটি ছোট নিয়ম; class বানানো ভারী মনে হয়
Event / hookমূল কোড একটি ঘটনা ঘোষণা করে; যে চায় সে শোনেঅনেকে একই মুহূর্তে কিছু করতে চায় (observer — পর্ব ৩৫)

শেষ কোড একটি কাল্পনিক স্কুলের বেতনে ছাড়ের নিয়ম — plugin-ধাঁচে, function-এর একটি Map দিয়ে। মূল function জানে শুধু একটি চুক্তি: “নিয়ম মানে একটি function, যে ছাত্রের তথ্য নিয়ে ছাড়ের পরিমাণ ফেরত দেয়।” কোন নিয়ম কোন ক্রমে চলবে, তা আসে configuration থেকে। JavaScript-এ function-কে মান হিসেবে ব্যবহারের ভিত্তি দেখুন functional programming পর্বে।

// Plugin-style OCP for a fictional school-fee system.
// The core knows only "a rule is a function (student) -> discount".
const rules = new Map();                         // name -> function
const addRule = (name, fn) => rules.set(name, fn);

function monthlyFee(student, enabled) {          // CORE: closed
  let fee = student.base, log = [];
  for (const name of enabled) {                  // order comes from config
    const d = rules.get(name)?.(student, fee) ?? 0;
    if (d > 0) { fee -= d; log.push(`${name} -${d}`); }
  }
  return `${student.name}: ${student.base} -> ${fee}  [${log.join(", ") || "none"}]`;
}

// built-in plugins
addRule("merit",  s => (s.gpa >= 4.5 ? 500 : 0));
addRule("staff",  (s, fee) => (s.staffChild ? Math.round(fee * 0.5) : 0));

const kids = [
  { name: "Mitu",  base: 3000, gpa: 4.8, staffChild: false, siblings: 1 },
  { name: "Rafi",  base: 3000, gpa: 3.9, staffChild: true,  siblings: 2 },
  { name: "Tanha", base: 2500, gpa: 4.6, staffChild: true,  siblings: 0 },
];
const config1 = ["merit", "staff"];
kids.forEach(k => console.log(monthlyFee(k, config1)));

// a new plugin, added later from another file -- monthlyFee is untouched
addRule("sibling", s => s.siblings * 200);
const config2 = ["sibling", "merit", "staff", "unknown-rule"];
console.log("-- after adding the sibling plugin --");
kids.forEach(k => console.log(monthlyFee(k, config2)));

আউটপুট:

Mitu: 3000 -> 2500  [merit -500]
Rafi: 3000 -> 1500  [staff -1500]
Tanha: 2500 -> 1000  [merit -500, staff -1000]
-- after adding the sibling plugin --
Mitu: 3000 -> 2300  [sibling -200, merit -500]
Rafi: 3000 -> 1300  [sibling -400, staff -1300]
Tanha: 2500 -> 1000  [merit -500, staff -1000]

প্রথম configuration-এ Tanha আগে merit-এ ৫০০ ছাড় পায় (২৫০০ → ২০০০), তারপর staff নিয়ম বাকি বেতনের অর্ধেক কাটে — ১০০০, শেষে ১০০০। তারপর নতুন “sibling” নিয়ম যোগ হলো — monthlyFee-এর একটি লাইনও না বদলে। Rafi-র দুই ভাইবোন, তাই আগে ৪০০ কাটে (৩০০০ → ২৬০০), তারপর staff-এর অর্ধেক ১৩০০ — আগে ছিল ১৫০০! অর্থাৎ নিয়মের ক্রম ফল বদলায়; ক্রমটি configuration-এ থাকায় হিসাব বিভাগ ঠিক করতে পারে, কোন ক্রম তাদের নীতি। আরেকটি জিনিস লক্ষ করুন: "unknown-rule" নামে কোনো নিয়ম নেই, তবু কোনো error হলো না — ?. আর ?? সেটা চুপচাপ ০ ধরে নিল। Configuration-এ বানান ভুল হলে ছাড় হারিয়ে যাবে, কেউ জানবে না। বাস্তব plugin ব্যবস্থায় অজানা নাম পেলে শুরুতেই জোরে ব্যর্থ হওয়া (fail fast) ভালো — অনুশীলন ৩-এ এটি আবার আসবে।

৮ · OCP পরম নয় — বদল কোন দিকে আসবে, তা অনুমান

একটি প্রোগ্রাম সব ধরনের বদলের বিরুদ্ধে “closed” হতে পারে না। Courier-এর কোড নতুন courier type-এর জন্য খোলা রাখলেন — কিন্তু হঠাৎ অনুরোধ এলো, খরচ ওজনে নয়, দূরত্বে হবে। এবার Rate চুক্তিটাই (cost(kg)) ভুল; সব implementation বদলাতে হবে। Martin নিজেও লিখেছেন, কোনো উল্লেখযোগ্য প্রোগ্রাম ১০০% closed হয় না; closure হতে হবে কৌশলগত — যেদিকে বদল আসার সম্ভাবনা বেশি, সেদিকে।

সেই দিক কীভাবে অনুমান করবেন?

  • ইতিহাস: গত এক-দুই বছরে কোন ধরনের বদল বারবার এসেছে? Git log আর issue tracker সবচেয়ে সৎ সাক্ষী।
  • Requirement আর roadmap: পর্ব ২৪-এর backlog-এ কি দ্বিতীয় রূপের কোনো story আছে? “আগামী quarter-এ আরও দুটি courier partner” — এটা শক্ত প্রমাণ।

Martin-এর একটি পরামর্শ প্রায়ই “প্রথম গুলিটা খান” (take the first bullet) নামে উল্লেখ হয়: প্রথমে সরলভাবে লিখুন, যেন কোনো বদল আসবে না। প্রথমবার যে ধরনের বদল আসে, তখন সেই ধরনের বিরুদ্ধে abstraction বানান। এর সাথে মেলে Don Roberts-এর rule of three (Fowler-এর Refactoring-এ উল্লেখ): তৃতীয়বার একই জিনিস করতে গেলে refactor করুন। আর পর্ব ২৬-এর YAGNI — “You aren't gonna need it” — মনে করিয়ে দেয়, আজ লাগছে না এমন নমনীয়তার দাম আজই দিতে হয়: বেশি file, বেশি indirection, পড়তে বেশি সময়।

TaxPolicy প্রস্তাবের বিচার (কাল্পনিক স্কুল-বেতন ব্যবস্থা)
প্রশ্ন
উত্তর
সংকেত
  1. প্রস্তাব। একজন সহকর্মী বললেন, “কর তো বদলাতে পারে — TaxPolicy interface, একটি factory আর একটি config file বানিয়ে রাখি।” কথাটা শুনতে দূরদর্শী। প্রশ্ন করার পালা।
  2. আজ কয়টি রূপ? একটি — সব বেতনে একই VAT নিয়ম। একটিমাত্র implementation-এর interface প্রায়ই সংকেত যে abstraction-টি কল্পনা থেকে এসেছে, বাস্তব থেকে নয়। (ব্যতিক্রম: test-এ নকল বসানোর জন্য, বা অন্য দলের সাথে চুক্তি হিসেবে — পর্ব ২৮ ও ৪৯।)
  3. ইতিহাস। git log -- tax.py দেখাচ্ছে দুই বছরে একটিমাত্র commit — প্রথম লেখা। এদিকে বদল আসেনি।
  4. Requirement-এ প্রমাণ। Backlog-এ দ্বিতীয় কোনো কর-নিয়মের story নেই, কোনো stakeholder চাননি। অনুমানটি কারও প্রয়োজনের ওপর দাঁড়িয়ে নেই।
  5. দামের হিসাব। তিনটি বাড়তি file, প্রতিটি ডাকে এক স্তর বেশি, নতুন সদস্যের বুঝতে বেশি সময়। লাভ এখনো শূন্য, খরচ আজই নিশ্চিত।
  6. সিদ্ধান্ত: এখনো নয়। একটি সরল, ভালো নামের function আর তার test. সরল কোড পরে বদলানোও সহজ — ভালো test থাকলে interface বের করা কয়েক মিনিটের refactoring.
  7. আট মাস পরে — প্রথম গুলি। দ্বিতীয় campus-এর জন্য ভিন্ন কর-নিয়মের অনুরোধ এলো। এখন বদলের অক্ষ প্রমাণিত; এখনই interface বের করুন, দুটি implementation লিখুন। তৃতীয় নিয়ম এলে সে আসবে নতুন class হয়ে — OCP এখন বাস্তব প্রয়োজনের ওপর দাঁড়িয়ে।

আরও গভীরে: Craig Larman ২০০১ সালে IEEE Software-এ “Protected Variation: The Importance of Being Closed” প্রবন্ধে OCP আর Parnas-এর information hiding-কে একটি নামে বাঁধেন — protected variations: অনুমিত বদলের বিন্দু চিনুন আর তার চারপাশে একটি স্থির interface বানান। Meyer-এর Object-Oriented Software Construction-এর দ্বিতীয় সংস্করণ (১৯৯৭) আর Martin-এর Clean Architecture (২০১৭)-এর SRP ও OCP অধ্যায় মূল উৎস হিসেবে পড়ার মতো। বড় আকারে একই চিন্তা plugin architecture আর পর্ব ৪০-এর hexagonal architecture-এ ফিরে আসে।

৯ · দুই নীতি একসাথে — test, review আর Git-এর ইতিহাস

SRP আর OCP একে অপরকে সাহায্য করে। SRP বদলকে এক জায়গায় আনে; OCP সেই জায়গায় বদলকে যোগ বানায়, কাটাছেঁড়া নয়। দুটোর সবচেয়ে বাস্তব লাভ দেখা যায় test-এ:

  • ছোট, স্বাধীন unit test. TotalCalculator-এর test-এ file system বা তারিখের format লাগে না। প্রতিটি class একটি actor-এর নিয়ম, তাই test-এর নামও ব্যবসার ভাষায়: “VAT সহ মোট টাকা ঠিক”।
  • Regression-এর ঝুঁকি কম। নতুন courier type নতুন class হলে পুরোনো class-এর কোড ছোঁয়া হয়নি; তাদের test-এর পাস করা মানে পুরোনো আচরণ অক্ষত।
  • Review সহজ। Pull request-এর diff যদি শুধু নতুন file হয় (পর্ব ১৬), reviewer-এর প্রশ্ন ছোট হয়: “নতুন class কি চুক্তি মানে?”

Git দিয়ে actor খোঁজা

কোন file কয়টি actor-এর কাছে দায়বদ্ধ, তা Git ইতিহাস থেকেও আন্দাজ করা যায় — যদি commit-এ অনুরোধকারী লেখা থাকে। নিচের কাল্পনিক repository-তে প্রতিটি commit message-এর শেষে একটি trailer আছে: Requested-by: …। (Trailer-এর নিয়ম পর্ব ১৬-১৮-এ দেখেছেন; “Requested-by” এখানে আমাদের বানানো নাম, কোনো মানদণ্ড নয়।) একটি file-এর ইতিহাস থেকে trailer-গুলো তুলে গোনা হলো। উদাহরণটি Git ২.৫৩-এ বানানো:

$ git log --format="%h %s | %(trailers:key=Requested-by,valueonly,separator=%x2C)" -- report_service.py
fb2a63a feat: add VAT line to total | finance
99c877e fix: archive file name without slashes | ops
7d699b1 feat: show date as dd/mm/yyyy | accounts
fab61c5 fix: round COD fee to whole taka | finance
f0b71ae feat: add monthly parcel report | accounts
$ git log --format="%(trailers:key=Requested-by,valueonly,separator=%x2C)" -- report_service.py | sort | uniq -c
      2 accounts
      2 finance
      1 ops

-- report_service.py ইতিহাসকে শুধু ওই file ছোঁয়া commit-এ সীমিত করে; %(trailers:key=…,valueonly) শুধু trailer-এর মান ছাপে (separator=%x2C দেওয়ায় শেষে বাড়তি ফাঁকা লাইন আসে না)। ফল: একটি file-এ তিনটি আলাদা দলের পাঁচটি অনুরোধ — SRP-এর প্রশ্ন তোলার যথেষ্ট কারণ। এমনকি বাগের ইতিহাসও গল্প বলে: 7d699b1-এর তারিখ বদলের ঠিক পরেই 99c877e “archive file name without slashes” — অংশ ২-এর কোডের ঠিক সেই দুর্ঘটনা। এই repository একটি ফাঁকা scratch folder-এ বানানো (লেখক Rahim, নির্দিষ্ট তারিখ); একই file-এর লেখা, লেখক আর তারিখ দিয়ে বানালে hash-গুলোও হুবহু এক আসবে। বাস্তবে trailer না থাকলে issue-এর নম্বর বা commit-এর লেখক দেখে একই প্রশ্ন করা যায়; কোন file-গুলো সবসময় একসাথে বদলায় তা বের করার কৌশলকে অনেকে change coupling বলেন।

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

  • SRP কী? “একটি class একটি কাজ করবে” কি ঠিক? — একটি module-এর বদলের কারণ আসবে একটি actor থেকে; “এক কাজ” function-এর পরামর্শ, SRP নয়।
  • OCP একটি উদাহরণসহ বোঝান। — নতুন courier type নতুন class হয়ে একটি interface মেনে যোগ হয়; খরচের মূল function বদলায় না।
  • Meyer আর Martin-এর OCP-র পার্থক্য? — Meyer (১৯৮৮) subclass দিয়ে বাড়াতেন; Martin abstraction আর তার বিভিন্ন implementation দিয়ে।
  • Type-এর ওপর switch সবসময় খারাপ? — না; একবার থাকলে আর রূপ কম বদলালে ঠিক আছে। একই switch বহু জায়গায় আর type প্রায়ই যোগ হলে polymorphism ভাবুন।
  • SOLID-এর অতিরিক্ত প্রয়োগের উদাহরণ? — এক method-এর অগণিত class, একটিমাত্র implementation-এর interface আর factory — বদলের প্রমাণ ছাড়াই।

১০ · AI-এর যুগে — AI-এর “SOLID” কোড যাচাই

AI-কে “এই কোড SOLID বানাও” বললে সাধারণত দ্রুত, পরিপাটি, অনেক interface-ওয়ালা কোড আসে। পরিপাটি মানেই ভালো নকশা নয়। এই বিষয়ে AI-এর চেনা ভুলগুলো:

AI-এর ভুলকীভাবে ধরবেন
Method ধরে ভাঙে, actor ধরে নয় — এক method-এর অনেক classপ্রতিটি নতুন class-এর actor জিজ্ঞেস করুন; একই actor-এর হলে একসাথে রাখুন
প্রতিটি class-এর জন্য interface, factory, config — একটিমাত্র implementation নিয়েবদলের প্রমাণ চান: ইতিহাস, backlog; না থাকলে সরল রাখুন
Refactor-এর পরে আচরণ চুপচাপ বদলায় (rounding, ক্রম, ডিফল্ট মান)আগের test রেখে চালান; আগে-পরে একই input-এ একই output মিলিয়ে দেখুন
Implementation-এর আয়না test — mock-এ কী ডাকা হলো শুধু তা দেখে, ফল নয়Test-এ ব্যবসার ফল যাচাই হয় কি না দেখুন; ইচ্ছা করে bug ঢুকিয়ে test ব্যর্থ হয় কি না পরখ করুন (mutation testing, পর্ব ৫০)
বানানো উদ্ধৃতি আর তারিখ — “Martin বলেছেন class একটি কাজ করবে”মূল বই বা প্রবন্ধ মিলিয়ে দেখুন; সূত্র ছাড়া উদ্ধৃতি গ্রহণ করবেন না
অন্য project থেকে কোড, license-এর কথা না বলে; prompt-এ গোপন তথ্যবড় অংশের উৎস জিজ্ঞেস করুন, license যাচাই করুন; prompt থেকে key, গ্রাহকের তথ্য সরান

নিচের prompt-এ AI-কে আগে বিশ্লেষণ, পরে কোড দিতে বলা হয়েছে, আর প্রতিটি নতুন abstraction-এর পক্ষে প্রমাণ চাওয়া হয়েছে। কোড পাঠানোর আগে key, password আর গ্রাহকের তথ্য সরিয়ে নিন।

নিচে একটি কাল্পনিক courier অ্যাপের একটি class দিলাম (গোপন তথ্য বাদ দিয়ে)। [এখানে কোড] ১. কোড বদলানোর আগে প্রতিটি public method-এর পাশে লেখো কোন actor (কোন দল বা ভূমিকা) এর বদল চাইবে বলে তুমি মনে করো, আর কেন। অনিশ্চিত হলে "অনিশ্চিত" লেখো — বানিয়ে বলবে না। ২. শুধু যেখানে একাধিক actor আছে, সেখানে ভাঙার প্রস্তাব দাও। একই actor-এর method আলাদা করবে না। ৩. কোথাও type-এর ওপর if/switch থাকলে বলো সেটা কয় জায়গায় আছে। শুধু একাধিক জায়গায় থাকলে বা আমি বললে polymorphism বা registry প্রস্তাব করো; প্রতিটি নতুন interface-এর জন্য লেখো কোন বদল এটা সহজ করবে। ৪. refactor-এর আগে-পরে আচরণ যে একই, তা দেখাতে কয়েকটি unit test দাও, যেগুলো ফল যাচাই করে, শুধু কোন method ডাকা হলো তা নয়। ৫. Robert C. Martin বা অন্য কারও উদ্ধৃতি দিলে বই ও বছর লেখো; নিশ্চিত না হলে উদ্ধৃতি দেবে না।

AI-এর দেওয়া refactor হাতে পেলে — চেকলিস্ট

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

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

প্র ০১SRP-এর সংজ্ঞা কেন “পরিবর্তনের একটি কারণ” থেকে “একটি actor”-এ সরে এলো? এর পেছনের চিন্তার ইতিহাস কী?

SRP-এর শিকড় Martin-এর চেয়ে পুরোনো। ১৯৭০-এর দশকে structured design-এর যুগে Larry Constantine, Ed Yourdon, Tom DeMarco আর Meilir Page-Jones cohesion নিয়ে লেখেন — একটি module-এর অংশগুলো কতটা একসাথে থাকার যোগ্য। ১৯৭২ সালে David Parnas বলেন, module ভাগের মানদণ্ড হওয়া উচিত “কোন সিদ্ধান্ত বদলাতে পারে” — প্রতিটি module একটি বদলযোগ্য সিদ্ধান্ত লুকাবে। Martin নিজেও SRP-এর উৎস হিসেবে cohesion-এর এই কাজগুলোর কথা বলেছেন।

Martin-এর ২০০০-এর দশকের শুরুর সংজ্ঞা — “একটি class-এর বদলানোর কারণ একটিই থাকবে” — Parnas-এর চিন্তার একটি সংক্ষিপ্ত রূপ। কিন্তু “কারণ” শব্দটি দুই রকমে ভুল পড়া হতো। কেউ ভাবতেন প্রতিটি প্রযুক্তিগত কাজই একটি কারণ — তাই প্রতিটি method আলাদা class. কেউ আবার বলতেন “আমার class-এর একটিই কারণ: ব্যবসার নিয়ম বদলালে বদলাবে” — এতে সবই এক কারণ হয়ে যায়। দুই দিকেই নীতিটি অর্থহীন।

Martin পরে (একটি ২০১৪ সালের blog লেখায় এবং Clean Architecture, ২০১৭-এ) কারণটিকে মানুষের সাথে বাঁধেন: বদল চায় মানুষ, আর software একটি প্রতিষ্ঠানের ভিন্ন ভিন্ন দলের সেবা করে। তাই প্রশ্ন হলো “এই কোডের বদল কারা চাইবেন?” এতে নীতিটি সংগঠনের কাঠামোর সাথে যুক্ত হলো — ঠিক যেমন Melvin Conway ১৯৬৮ সালে লক্ষ করেছিলেন, প্রতিষ্ঠানের যোগাযোগের কাঠামো আর system-এর কাঠামো একে অপরকে প্রতিফলিত করে।

এর একটি সূক্ষ্ম ফল আছে। দুটি দলের নিয়ম আজ হুবহু একই হলেও, সিদ্ধান্তের মালিক আলাদা হলে তাদের আলাদা রাখাই নিরাপদ — বেতনের উদাহরণের “নিয়মিত ঘণ্টা” function যেমন দেখাল। আবার একই দলের দশটি method এক class-এ থাকতে পারে। অর্থাৎ SRP আকারের নিয়ম নয়, মালিকানার নিয়ম।

অবশ্য actor-এর সংজ্ঞারও সীমা আছে। ছোট startup-এ একজনই হয়তো হিসাব, অপারেশন আর product — সব দেখেন। তখন “ভূমিকা” ধরে ভাবতে হয়: একই মানুষ ভিন্ন টুপি পরে ভিন্ন কারণে বদল চান। প্রতিষ্ঠান বড় হলে এই টুপিগুলোই আলাদা দল হয়ে যায় — আর আগে থেকে ভূমিকা ধরে ভাগ করা কোড তখন সহজে ভাগ হয়।

মূল উপলব্ধি: SRP-এর বিবর্তন cohesion আর information hiding থেকে মানুষের দিকে: কোডকে ভাগ করুন বদলের সিদ্ধান্তের মালিক ধরে — আকার বা method গুনে নয়।

প্র ০২Meyer-এর inheritance-ভিত্তিক OCP কেন Martin-এর abstraction-ভিত্তিক OCP-তে বদলে গেল? Inheritance কি তবে খারাপ?

Meyer যখন ১৯৮৮ সালে Object-Oriented Software Construction লেখেন, তখন তাঁর চিন্তার কেন্দ্রে ছিল reusable library: একটি module লেখা, পরীক্ষা করা, প্রকাশিত হয়ে গেছে, অনেকে ব্যবহার করছেন। তাকে বদলানো ব্যয়বহুল আর ঝুঁকির। Object-oriented ভাষার নতুন শক্তি inheritance তাঁকে একটি পথ দিল: পুরোনো class অক্ষত রেখে একটি subclass বানান, যা কিছু method নতুন করে লেখে বা নতুন যোগ করে। পুরোনো ব্যবহারকারীরা পুরোনোটা পান, নতুনরা নতুনটা।

১৯৯০-এর দশকে বড় C++ আর পরে Java codebase-এ এই পথের দাম স্পষ্ট হলো। Subclass parent-এর ভেতরের খুঁটিনাটির ওপর নির্ভর করে — কোন method কোন method-কে ডাকে, কোন field কখন বদলায়। Parent-এর লেখক নিরীহ একটি ভেতরের বদল করলেন, আর অন্য দলের subclass ভেঙে গেল। এর নাম হলো fragile base class problem. তাছাড়া গভীর উত্তরাধিকারের গাছে আচরণ খুঁজতে পাঁচ স্তর ওপরে-নিচে যেতে হয়। ১৯৯৪ সালে Gang of Four তাঁদের বইয়ের শুরুতেই দুটি নীতি দেন: “implementation-এর নয়, interface-এর উদ্দেশে program লিখুন” আর “class inheritance-এর চেয়ে object composition পছন্দ করুন”।

Martin-এর ১৯৯৬ সালের C++ Report প্রবন্ধ আর পরের লেখায় OCP সেই দিকেই গেল: মূল কোড একটি abstract চুক্তির ওপর নির্ভর করে, আর প্রতিটি রূপ সেই চুক্তির একটি implementation. এখানে inheritance থাকতে পারে, কিন্তু শুধু interface-এর (কী করা যায়), implementation-এর (কীভাবে) নয়। ১৯৯৫ সালে Java-র interface এই চিন্তাকে ভাষার স্তরে সহজ করে দেয়। পরে Python-এর duck typing আর Protocol, Go-র implicit interface, বা JavaScript-এ function-কে মান হিসেবে দেওয়া — সবই একই ধারণার নানা রূপ।

তাই বলে inheritance খারাপ নয়। যেখানে সত্যিকারের “is-a” সম্পর্ক আছে, parent-এর আচরণ স্থির, আর গাছ অগভীর — যেমন একটি framework-এর ছোট base class — সেখানে inheritance স্বাভাবিক। শর্ত একটি: subclass যেন parent-এর জায়গায় বসলে কিছু না ভাঙে। সেই শর্তের নাম Liskov substitution principle, যা পর্ব ২৮-এ (SOLID ২) বিস্তারিত। Composition আর inheritance-এর তুলনা দেখতে পারেন Python কোর্সের inheritance বনাম composition পর্বে।

মূল উপলব্ধি: OCP-র লক্ষ্য একই রইল — পুরোনো, পরীক্ষিত কোড অক্ষত রেখে বাড়ানো; বদলেছে কৌশল: ভেতরের কোড উত্তরাধিকারের বদলে একটি পাতলা, স্থির চুক্তির ওপর নির্ভরতা।

প্র ০৩SOLID কি সমালোচনার ঊর্ধ্বে? কেউ কেউ কেন বলেন SOLID অতি-জটিল “enterprise” কোডের জন্ম দিয়েছে?

না, এবং সমালোচনাগুলো শেখার মতো। ২০০০ আর ২০১০-এর দশকের অনেক বড় Java ও C# codebase-এ এমন কোড দেখা যেত: প্রতিটি class-এর একটি interface, প্রতিটি interface-এর একটি implementation, তাদের বানাতে factory, factory বানাতে আরেকটি factory, আর সব জোড়া লাগাতে বিশাল XML configuration. একটি সাধারণ বদল করতে সাত-আটটি file খুলতে হতো।

সমালোচকদের যুক্তি কয়েক রকম। প্রথমত, নীতিগুলোর ভাষা অস্পষ্ট — “responsibility” বা “closed” এর মাপ কেউ নির্দিষ্ট করে দেয় না, তাই একেকজন একেক রকম বোঝেন, আর code review-তে নীতি একটি অস্ত্র হয়ে যায়: “এটা SRP মানছে না।” দ্বিতীয়ত, নীতিগুলো মূলত class-ভিত্তিক object-oriented ভাষার অভিজ্ঞতা থেকে এসেছে; functional programming-এ বা ছোট script-এ এগুলোর অনেকটা আপনা থেকেই মেটে, বা অন্য রূপে আসে। তৃতীয়ত, কিছু লেখক বিকল্প প্রস্তাব করেছেন — যেমন Dan North ২০২১-২২ সালের দিকে “CUPID” নামে কয়েকটি গুণের কথা বলেন (composable, Unix philosophy, predictable, idiomatic, domain-based), নিয়মের বদলে বৈশিষ্ট্যের ভাষায়। (নামগুলো তাঁর প্রকাশিত লেখা থেকে নিজে মিলিয়ে নিন।)

অন্য দিকে, সমর্থকদের যুক্তি: সমস্যা নীতিতে নয়, প্রয়োগে। Martin নিজেও OCP-কে কৌশলগত বলেছেন, আর YAGNI ও Beck-এর “simple design” একই যুগের XP আন্দোলন থেকে এসেছে। অতিরিক্ত interface আসলে নীতির ভুল পড়া, যেমন আমরা অংশ ৮-এ দেখলাম।

একটি সুষম দৃষ্টিভঙ্গি: SOLID একটি রোগনির্ণয়ের তালিকা, চিকিৎসার প্রেসক্রিপশন নয়। ব্যথা থাকলে — একটি বদলে বারবার অন্য দলের কোড ভাঙে, নতুন type-এ পাঁচ জায়গায় switch খুঁজতে হয় — তখন নীতিগুলো কারণ চিনিয়ে দেয় আর সমাধানের দিক দেখায়। ব্যথা না থাকলে নীতির নামে কোড জটিল করা নিজেই technical debt (পর্ব ৪৩)।

মূল উপলব্ধি: SOLID-এর মূল্য বদলের খরচ কমানোয়; কোনো প্রয়োগ যদি বদলের খরচ বাড়ায়, তবে সেটা নীতির পালন নয়, ভুল প্রয়োগ।

প্র ০৪Plugin architecture-কে বলা হয় বড় আকারের OCP. এতে লাভ কী, আর দামটা কে দেয়?

অনেক জনপ্রিয় software-এর মূল শক্তি তার plugin বা extension ব্যবস্থা। Code editor, browser, content management system, build tool — এগুলোর বেশিরভাগ ক্ষমতা মূল দল বানায়নি, বানিয়েছে হাজারো বাইরের developer. মূল দল একটি প্রকাশিত চুক্তি দেয়: কোন ঘটনায় কোন hook ডাকা হবে, কোন function-এ কোন data আসবে। বাইরের কোড সেই চুক্তি মেনে নিজেকে নিবন্ধন করে। মূল কোড বদলায় না, অথচ system-এর ক্ষমতা বাড়তেই থাকে — OCP-র সবচেয়ে বড় রূপ। Operating system-এ device driver-ও একই চিন্তা: নতুন printer-এর জন্য kernel নতুন করে লেখা হয় না।

লাভ স্পষ্ট: মূল দলের ওপর চাপ কমে, ব্যবহারকারী নিজের প্রয়োজনমতো system সাজাতে পারেন, আর একটি plugin-এর bug সাধারণত অন্য plugin-কে ছোঁয় না। কিন্তু দামটা আসে চুক্তির স্থিরতায়। একবার হাজারো plugin একটি API-র ওপর নির্ভর করলে সেটি বদলানো অত্যন্ত কঠিন। প্রতিটি ভাঙা বদল (breaking change) সবার কাজ নষ্ট করে, তাই মূল দলকে পুরোনো আচরণ বছরের পর বছর বাঁচিয়ে রাখতে হয়, deprecation-এর সময় দিতে হয়, SemVer-এর প্রতিশ্রুতি রাখতে হয় (পর্ব ১৮)। Google-এর একজন engineer-এর নামে প্রচলিত “Hyrum's law” বলে, যথেষ্ট ব্যবহারকারী থাকলে আপনার system-এর প্রতিটি দৃশ্যমান আচরণের ওপর কেউ না কেউ নির্ভর করবেই — documentation-এ লেখা থাকুক বা না থাকুক।

দ্বিতীয় দাম নিরাপত্তা আর মান। বাইরের কোড আপনার প্রক্রিয়ার ভেতরে চলে; একটি ক্ষতিকর বা ধীর plugin পুরো অভিজ্ঞতা নষ্ট করতে পারে। তাই বড় plugin ব্যবস্থায় অনুমতির সীমা (least privilege), আলাদা প্রক্রিয়ায় চালানো, review বা স্বাক্ষর যাচাই থাকে — platform-ভেদে ব্যবস্থা আলাদা।

মূল উপলব্ধি: Plugin ব্যবস্থা মূল কোডকে বন্ধ আর system-কে খোলা রাখে, কিন্তু চুক্তিটিকে প্রায় চিরস্থায়ী করে তোলে; তাই চুক্তি যত ছোট আর ভেবেচিন্তে, ভবিষ্যৎ তত সহজ।

প্র ০৫SRP-কে service-এর স্তরে টেনে নিলে কী হয়? Microservice ভাগের ভুলের সাথে এর সম্পর্ক কী?

Martin-এর সংজ্ঞায় “module” যেকোনো স্তরের হতে পারে, তাই অনেক দল microservice ভাগ করতেও SRP-র কথা বলে: “প্রতিটি service একটি দায়িত্ব।” এটা ঠিক পড়লে খুব কাজের — প্রতিটি service একটি ব্যবসার ক্ষমতা আর তার মালিক দলের সাথে মেলে (billing, delivery, notification), আর একটি দলের বদল অন্য দলের deploy আটকায় না। System Design কোর্সের Monolith বনাম microservices পর্বে service boundary নিয়ে বিস্তারিত আছে।

কিন্তু “এক কাজ” ভুল পড়াটি এই স্তরে এসে অনেক দামি হয়। কিছু দল প্রায় প্রতিটি function-কে একটি আলাদা service বানায় — “price-service”, “tax-service”, “rounding-service”. তখন অংশ ৪-এর বেশি ভাঙার সমস্যা নেটওয়ার্কের ওপর দিয়ে ঘটে: একটি নিয়ম বদলাতে তিনটি service-এ বদল, তিনটি আলাদা deploy, আর প্রতিটি ডাকে নেটওয়ার্কের দেরি ও ব্যর্থতার ঝুঁকি। একে অনেকে “distributed monolith” বলেন — আলাদা আলাদা চলে, কিন্তু একসাথে ছাড়া বদলানো যায় না। এক file-এর ভেতরের shotgun surgery-র চেয়ে এটা বহুগুণ ব্যয়বহুল, কারণ এখন বদলের সমন্বয় করতে হয় একাধিক দল, pipeline আর release-এর মধ্যে।

সঠিক প্রশ্ন একই থাকে: এই বদলগুলো কি একই actor থেকে, একই সময়ে আসে? যা একসাথে বদলায়, তা একসাথে deploy হওয়া উচিত। Git ইতিহাস এখানেও সাহায্য করে — যে service-গুলো প্রায় সবসময় একই সপ্তাহে একসাথে বদলায়, সেগুলোর ভাগ সম্ভবত ভুল জায়গায়। Adam Tornhill তাঁর Your Code as a Crime Scene (২০১৫) বইয়ে version control-এর ইতিহাস থেকে এ ধরনের change coupling খোঁজার কৌশল জনপ্রিয় করেন।

আরেকটি শিক্ষা: সীমা ভুল হলে class-এর স্তরে ঠিক করা সস্তা (একটি refactoring, একটি pull request), service-এর স্তরে দামি (data migration, API version, দলের দায়িত্ব বদল)। তাই অনেক অভিজ্ঞ engineer পরামর্শ দেন, আগে একটি ভালোভাবে ভাগ করা monolith-এর ভেতরে module-এর সীমা পরখ করুন; সীমাটি সময়ের সাথে টিকে গেলে তবেই আলাদা service.

মূল উপলব্ধি: স্তর যত বড়, ভুল ভাগের দাম তত বেশি; service-এর সীমা টানুন actor আর একসাথে-বদলানোর প্রমাণ ধরে, “এক কাজ” গুনে নয়।

অনুশীলন

একটি refactoring, একটি আউটপুট অনুমান, একটি নকশার সমালোচনা আর একটি হিসাব — প্রতিটির পূর্ণ উত্তর দেওয়া আছে।

  1. এই কোড refactor করুন (কাল্পনিক স্কুল-বেতন ব্যবস্থা): নিচের FeeManager-এ কয়টি actor আছে? SRP আর OCP মেনে নতুন নকশা লিখুন, যাতে (ক) receipt-এর লেখা বদলালে বেতনের হিসাব ছোঁয়া না লাগে, আর (খ) “full_free” (পূর্ণ বিনা বেতন, ০ টাকা) নামে নতুন ছাত্র-শ্রেণি যোগ করতে পুরোনো কোনো method না বদলাতে হয়। আচরণ একই আছে কি না, কীভাবে দেখাবেন?
    class FeeManager:
        def fee(self, s):                       # accounts
            if s["cat"] == "regular":    return 3000
            elif s["cat"] == "staff":    return 1500
            elif s["cat"] == "merit":    return 3000 - 500
        def receipt(self, s):                   # front desk
            return f"Receipt: {s['name']} paid {self.fee(s)} taka"
        def sms(self, s):                       # guardian office
            return f"Dear guardian, {self.fee(s)} taka is due for {s['name']}"
    
    fm = FeeManager()
    s = {"name": "Mitu", "cat": "merit"}
    print(fm.receipt(s)); print(fm.sms(s))

    Actor তিনটি: fee — হিসাব বিভাগ; receipt — সামনের ডেস্ক; sms — অভিভাবক-যোগাযোগ অফিস। তাছাড়া fee-এর if/elif হলো type-switch; নতুন শ্রেণি এলে সেটা খুলতে হয়, আর অজানা শ্রেণিতে চুপচাপ None ফেরত দেয় (receipt-এ ছাপা হতো “paid None taka”). একটি সম্ভাব্য নকশা:

    FEES = {"regular": lambda: 3000, "staff": lambda: 1500,
            "merit": lambda: 3000 - 500}
    
    class FeeCalculator:                        # actor: accounts
        def __init__(self, fees=FEES): self.fees = fees
        def fee(self, s): return self.fees[s["cat"]]()
    
    class ReceiptPrinter:                       # actor: front desk
        def __init__(self, calc): self.calc = calc
        def receipt(self, s):
            return f"Receipt: {s['name']} paid {self.calc.fee(s)} taka"
    
    class GuardianNotifier:                     # actor: guardian office
        def __init__(self, calc): self.calc = calc
        def sms(self, s):
            return f"Dear guardian, {self.calc.fee(s)} taka is due for {s['name']}"
    
    calc = FeeCalculator()
    s = {"name": "Mitu", "cat": "merit"}
    print(ReceiptPrinter(calc).receipt(s)); print(GuardianNotifier(calc).sms(s))
    FEES["full_free"] = lambda: 0                  # new category: one line, no edits
    print(ReceiptPrinter(calc).receipt({"name": "Rafi", "cat": "full_free"}))

    চালালে ছাপে:

    Receipt: Mitu paid 2500 taka
    Dear guardian, 2500 taka is due for Mitu
    Receipt: Rafi paid 0 taka

    প্রথম দুই লাইন পুরোনো কোডের আউটপুটের সাথে হুবহু এক — refactoring-এর আগে-পরে একই input-এ একই output, এটাই আচরণ রক্ষার প্রমাণ (বাস্তবে এগুলো unit test হিসেবে লিখুন)। ReceiptPrinter আর GuardianNotifier দুজনেই FeeCalculator-এর ওপর নির্ভর করে, কারণ দুজনেরই টাকার অঙ্ক লাগে — কিন্তু receipt-এর বাক্য বদলালে শুধু ReceiptPrinter বদলায়। নতুন শ্রেণি full_free এক লাইনে যোগ হলো, কোনো method না খুলে। অজানা শ্রেণিতে এখন KeyError — চুপচাপ ভুলের চেয়ে জোরে ব্যর্থ হওয়া ভালো। সহজ করতে এখানে global FEES বদলানো হয়েছে; বাস্তবে constructor-এ টেবিল দেওয়া (FeeCalculator(fees=...)) test-এ বেশি নিরাপদ।

  2. আউটপুট অনুমান করুন: নিচের JavaScript কোড চালালে ঠিক কী ছাপবে? বিশেষভাবে ভাবুন: একই নামে দ্বিতীয়বার set করলে Map-এ কী হয়, আর ক্রম কোথায় থাকে।
    const rules = new Map();
    const add = (n, f) => rules.set(n, f);
    add("flat", x => x - 100);
    add("half", x => x / 2);
    add("flat", x => x - 300);
    let fee = 2000;
    for (const [n, f] of rules) { fee = f(fee); console.log(n, fee); }
    console.log(rules.size, rules.get("vat")?.(fee) ?? "no vat rule");
    flat 1700
    half 850
    2 no vat rule

    Map-এ একই key-তে আবার set করলে মান বদলায়, কিন্তু key-র প্রথম ঢোকানোর অবস্থান থেকে যায়। তাই ক্রম flat, তারপর half — আর flat-এর function এখন x - 300. হিসাব: ২০০০ − ৩০০ = ১৭০০, তারপর ১৭০০ / ২ = ৮৫০। Map-এ দুটিই key, তাই size ২। rules.get("vat") undefined, ?.() ডাকটাই এড়িয়ে undefined দেয়, আর ?? তখন ডান দিকের লেখাটি নেয়। শিক্ষা: plugin-এর registry-তে একই নামে দ্বিতীয় নিবন্ধন চুপচাপ পুরোনোটিকে মুছে দেয় — অনেক বাস্তব ব্যবস্থা তাই একই নাম দুবার পেলে error দেয়।

  3. নকশার সমালোচনা: একটি কাল্পনিক courier অ্যাপে একজন সহকর্মী (বা একটি AI) এই নকশা দিয়েছেন: (১) Invoice-কে ভেঙে InvoiceTotalAdder, InvoiceVatAdder, InvoiceRounder — প্রতিটিতে একটি method, তিনটিই হিসাব বিভাগের নিয়ম; (২) IDistanceCalculator interface, একটিমাত্র implementation, একটি DistanceCalculatorFactory আর একটি YAML config — দূরত্ব মাপার নিয়ম কখনো বদলায়নি; (৩) courier type-এর ওপর একই switch তিনটি function-এ — cost, eta, label — আর গত ছয় মাসে দুটি নতুন type এসেছে; (৪) plugin-এর নাম config থেকে পড়া হয়, অজানা নাম পেলে চুপচাপ বাদ। প্রতিটির মূল্যায়ন করুন — কোনটা রাখবেন, কোনটা বদলাবেন, কেন?
    • (১) বেশি ভাঙা। তিনটিই একই actor-এর নিয়ম, আর VAT-এর নিয়ম বদলালে সম্ভবত total আর rounding-ও বদলাবে — shotgun surgery. একটি InvoiceCalculator-এ একসাথে রাখুন; SRP-এর প্রথম অর্ধেক: যা একই কারণে বদলায়, তা একসাথে।
    • (২) অকালপক্ব abstraction. একটি implementation, বদলের ইতিহাস নেই, backlog-এ প্রমাণ নেই — YAGNI. একটি সরল function আর test রাখুন; interface, factory আর config সরান। দ্বিতীয় রূপ এলে তখন বের করুন। (ব্যতিক্রম: দূরত্ব যদি বাইরের map service থেকে আসে, তবে test-এ নকল বসানোর জন্য একটি পাতলা চুক্তি যুক্তিসংগত হতে পারে — পর্ব ২৮-এর dependency inversion আর পর্ব ৪৯-এর test double.)
    • (৩) এখানে OCP দরকার। একই switch তিন জায়গায়, আর বদলের অক্ষ প্রমাণিত (ছয় মাসে দুটি নতুন type). প্রতিটি courier type একটি class বা strategy, যার cost(), eta(), label() আছে; registry দিয়ে খোঁজা। নতুন type মানে একটি নতুন file আর এক লাইনের নিবন্ধন।
    • (৪) চুপচাপ ব্যর্থতা। Config-এ বানান ভুল হলে একটি ছাড় বা নিয়ম হারিয়ে যাবে, কেউ জানবে না। প্রোগ্রাম শুরুর সময়েই সব নাম যাচাই করে অজানা নাম পেলে স্পষ্ট error দিন (fail fast), আর একই নামে দুবার নিবন্ধনও আটকান।

    সারকথা: একই নকশায় নীতির অপব্যবহার (১, ২) আর নীতির প্রয়োজন (৩) পাশাপাশি থাকতে পারে। বিচারের মাপকাঠি বদলের প্রমাণ — নীতির নাম নয়।

  4. হিসাব — কোন file কয়টি actor-এর? একটি কাল্পনিক স্কুল-বেতন ব্যবস্থার আটটি change request (CR): CR১ accounts — fee_manager.py; CR২ front-desk — fee_manager.py, receipt.py; CR৩ guardian-office — sms.py, fee_manager.py; CR৪ accounts — fee_manager.py; CR৫ exam-board — board_export.py, fee_manager.py; CR৬ front-desk — receipt.py; CR৭ accounts — fee_manager.py, sms.py; CR৮ guardian-office — sms.py. (ক) প্রতিটি file কয়টি CR-এ আর কয়টি আলাদা actor-এর অনুরোধে বদলেছে? (খ) কোন file-গুলোতে SRP-এর প্রশ্ন ওঠে? (গ) ধরুন fee_manager.py-এর যেকোনো বদলে বাকি সব actor-এর অংশের test আবার চালাতে আর সেই দলকে জানাতে হয়। আটটি CR-এ মোট কতবার “অন্য দলের অংশ” অকারণে পুনঃপরীক্ষা হলো?
    FileCRসংখ্যাActor
    fee_manager.py১, ২, ৩, ৪, ৫, ৭৬৪টি — accounts (১, ৪, ৭), front-desk (২), guardian-office (৩), exam-board (৫)
    receipt.py২, ৬২১টি — front-desk
    sms.py৩, ৭, ৮৩২টি — guardian-office (৩, ৮), accounts (৭)
    board_export.py৫১১টি — exam-board

    (খ) fee_manager.py স্পষ্ট লঙ্ঘন — চারটি দল। sms.py-তে একটি সূক্ষ্ম সংকেত: accounts কেন SMS বদলাল? সম্ভবত SMS-এর ভেতরে টাকার অঙ্ক বা হিসাবের নিয়ম লেখা আছে। সমাধান — অঙ্কটি হিসাবের class থেকে নেওয়া, SMS শুধু বাক্য সাজাবে; তাহলে accounts-এর বদল আর SMS file ছোঁবে না। তাছাড়া প্রায় প্রতিটি CR fee_manager.py-কেও ছুঁয়েছে — সেটা ইঙ্গিত দেয় অন্য দলের কাজও এই file-এর ভেতরে জড়ানো।

    (গ) fee_manager.py-তে চারটি actor-এর অংশ। প্রতিটি বদলে অনুরোধকারী বাদে বাকি ৩ দলের অংশ পুনঃপরীক্ষা: ৬টি CR × ৩ = ১৮ বার। File-টি actor ধরে চারটি file-এ ভাঙলে প্রতিটি CR শুধু নিজের দলের file ছোঁবে — অকারণ পুনঃপরীক্ষা ০। (পুরো regression suite তো CI-তে চলবেই; লাভটা হলো মানুষের review, সমন্বয় আর ঝুঁকিতে।)

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

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

ক্রমবহু জায়গায় ছড়ানো একটি type-switch-কে OCP মেনে refactor করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ইতিহাস দেখে নিশ্চিত হওয়া যে নতুন type সত্যিই বারবার আসে
  2. বর্তমান আচরণ test দিয়ে বেঁধে ফেলা
  3. সব switch থেকে প্রয়োজনীয় কাজগুলো নিয়ে একটি interface সংজ্ঞায়িত করা
  4. প্রতিটি শাখা নিজের class-এ সরিয়ে registry-তে নিবন্ধন করা
  5. Switch-গুলোর জায়গায় registry থেকে object নিয়ে ডাকা, আর পুরোনো test চালানো
  6. নতুন type-কে নতুন class হিসেবে যোগ করা

Robert C. Martin-এর Clean Architecture (২০১৭)-এ single responsibility principle-এর ভাষা কোনটির সবচেয়ে কাছাকাছি?

  • একটি class একটিমাত্র কাজ করবে
  • একটি module-এর দায় থাকবে একটি, এবং কেবল একটি, actor-এর কাছে
  • একটি class-এ একটির বেশি public method থাকবে না
  • একটি module বাড়ানোর জন্য খোলা, বদলানোর জন্য বন্ধ থাকবে

একটি কাল্পনিক Employee class-এ আছে calculatePay() (নিয়ম ঠিক করে হিসাব বিভাগ), reportHours() (নিয়ম ঠিক করে মানবসম্পদ) আর save() (নিয়ম ঠিক করেন database দেখেন যাঁরা)। প্রথম দুটি একই helper function ব্যবহার করে। SRP-এর দৃষ্টিতে মূল ঝুঁকি কী?

  • Class-এ তিনটি method আছে, যা বেশি
  • ভাগ করা helper-এর কারণে প্রোগ্রাম ধীর চলবে
  • এক দলের অনুরোধে helper বদলালে অন্য দলের ফলও অজান্তে বদলে যেতে পারে
  • Class-টি database ব্যবহার করে, তাই এটি নিজেই SRP লঙ্ঘন

একটি courier-এর খরচ হিসাবের কোড Rate নামের একটি interface-এর ওপর নির্ভর করে, আর প্রতিটি courier type তার একটি implementation, একটি registry-তে নিবন্ধিত। নতুন “cold chain” type যোগ করার কোন উপায় open-closed principle-এর সবচেয়ে কাছাকাছি?

  • খরচের মূল function-এ একটি নতুন elif kind == "cold_chain" যোগ করা
  • Express class খুলে তার ভেতরে cold chain-এর শর্ত যোগ করা
  • Rate interface-এ একটি নতুন parameter যোগ করা
  • Rate মেনে চলা নতুন ColdChain class লিখে registry-তে নিবন্ধন করা

Bertrand Meyer ১৯৮৮ সালে open-closed principle প্রস্তাব করার সময় একটি “closed” module-কে বাড়ানোর প্রধান উপায় হিসেবে কী দেখিয়েছিলেন?

  • Inheritance — পুরোনো class-এর একটি subclass বানানো
  • একটি interface-এর নতুন implementation লেখা
  • একটি configuration file-এ নতুন নিয়ম লেখা
  • Module-টি কপি করে নতুন নামে বদলে নেওয়া

একটি কোডবেসে courier type-এর ওপর একই if/elif শৃঙ্খল তিনটি আলাদা function-এ আছে, আর প্রায় প্রতি মাসে নতুন type আসে। এর সবচেয়ে বড় ঝুঁকি কী?

  • if/elif-এর কারণে প্রোগ্রাম খুব ধীর চলবে
  • নতুন type যোগে তিন জায়গাই বদলাতে হয়, একটি ভুলে গেলে চুপচাপ ভুল ফল
  • এই কোড compile বা interpret-ই হবে না
  • একটি switch-এ নির্দিষ্ট সংখ্যার বেশি শাখা রাখা যায় না

একটি দল একটি কর-নিয়মের জন্য TaxPolicy interface, একটি factory আর config বানাতে চায়। আজ নিয়ম একটিই, দুই বছরে কর-নিয়মের file-এ একটিমাত্র commit, আর backlog-এ দ্বিতীয় নিয়মের কোনো অনুরোধ নেই। সবচেয়ে যুক্তিসংগত সিদ্ধান্ত কোনটি?

  • এখনই interface, factory আর config বানানো, কারণ OCP সবসময় মানতে হয়
  • কর-নিয়ম প্রতিটি হিসাবের জায়গায় সরাসরি লিখে দেওয়া
  • একটি সরল, test-সহ function রাখা, আর দ্বিতীয় নিয়ম এলে তখন interface বের করা
  • কর-নিয়মকে একটি বাইরের plugin হিসেবে বানানো

একটি কাল্পনিক invoice ব্যবস্থায় InvoiceTotalAdder, InvoiceVatAdder আর InvoiceRounder তিনটি আলাদা class, প্রতিটিতে একটি method, তিনটিই হিসাব বিভাগের নিয়ম, আর VAT-এর নিয়ম বদলালে প্রায়ই তিনটিই বদলায়। এই নকশা সম্পর্কে কোনটি সঠিক?

  • প্রতিটি class-এ একটি method, তাই এটি SRP-এর আদর্শ প্রয়োগ
  • এটি OCP-র উদাহরণ, কারণ প্রতিটি class আলাদাভাবে বাড়ানো যায়
  • তিনটি আলাদা actor আছে, তাই আলাদা রাখাই ঠিক
  • এটি বেশি ভাঙা — একই actor-এর, একসাথে বদলানো জিনিস একটি class-এ রাখা উচিত

শব্দকোষ

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

মনে রাখুন

  1. SOLID পাঁচটি নীতি, লক্ষ্য একটাই — বদল সস্তা আর নিরাপদ; নীতিগুলো প্রশ্নের ছাঁচ, আইন নয়।
  2. SRP: একটি module-এর দায় একটি actor-এর কাছে — যে একদল মানুষ এর বদল চান। “এক কাজ” function-এর পরামর্শ, SRP নয়।
  3. দুই দিকের নিয়ম: যা একই কারণে বদলায়, একসাথে; যা ভিন্ন কারণে, আলাদা। বেশি ভাঙা মানে shotgun surgery.
  4. OCP: Meyer (১৯৮৮) inheritance দিয়ে, Martin abstraction আর বদলযোগ্য implementation দিয়ে। বারবারের type-switch-এর ওষুধ registry, polymorphism, plugin, configuration বা function-এর map.
  5. OCP কৌশলগত: প্রমাণিত বদলের দিকে খোলা রাখুন; প্রথম বদলের আগে abstraction প্রায়ই YAGNI. আর SRP-OCP-র বড় লাভ ছোট, স্বাধীন test আর ছোট diff-এ।

পরের পর্বে: এই পর্বে নতুন class-গুলো চুক্তি “মেনে” যোগ হলো — কিন্তু চুক্তি মানা আসলে কী? পর্ব ২৮-এ SOLID-এর বাকি তিন অক্ষর: Liskov substitution (subtype কখন সত্যিই বদলযোগ্য), interface segregation (মোটা interface ভাঙা) আর dependency inversion (নীতি কেন খুঁটিনাটির ওপর নির্ভর করবে না)।

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

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