এই পর্বে যা শিখবেন
- 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.
| অক্ষর | নীতি | এক লাইনে | কোথায় |
|---|---|---|---|
| S | Single Responsibility Principle (SRP) | একটি module-এর বদলের কারণ আসবে একটিমাত্র actor থেকে | এই পর্ব |
| O | Open-Closed Principle (OCP) | নতুন আচরণ যোগ হবে নতুন কোড লিখে, পুরোনো কোড বদলে নয় | এই পর্ব |
| L | Liskov Substitution Principle | Subtype-কে base type-এর জায়গায় বসালে প্রোগ্রাম ঠিক থাকবে | পর্ব ২৮ |
| I | Interface Segregation Principle | কাউকে এমন method-এর ওপর নির্ভর করতে বাধ্য করবেন না, যা সে ব্যবহার করে না | পর্ব ২৮ |
| D | Dependency 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
৩ · 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-কে হাতে ধরে ভাঙি — শুধু কোড নয়, প্রতিটি সিদ্ধান্তের কারণসহ। “পরে” চাপুন।
| কাজ | |
| অংশগুলো | |
| Actor সংখ্যা | |
| ঝুঁকি |
- সব public method আর ভাগ করা data-র তালিকা।
total,format,saveআর settingdate_fmt. Private helper-গুলোও তালিকায় রাখুন — লুকানো সংযোগ প্রায়ই সেখানেই থাকে। - প্রতিটির পাশে actor. প্রশ্ন: “এর বদল কে চাইবেন?” মোট টাকার নিয়ম finance-এর, report-এর চেহারা accounts-এর, archive-এর নাম ops-এর। তিনটি actor — অর্থাৎ বদলের তিনটি আলাদা উৎস।
- ভাগ করা জিনিস খুঁজুন।
date_fmtদুই দলই ব্যবহার করে। এটাই বিপদের জায়গা। সিদ্ধান্ত: দুই দলের তারিখ আজ এক রকম, কিন্তু সিদ্ধান্ত আলাদা — তাই প্রত্যেকে নিজের format রাখবে। এটা DRY-এর লঙ্ঘন নয়, দুটি আলাদা জ্ঞান। - Actor ধরে class. প্রতিটি actor-এর জন্য একটি class, নাম তার দায়িত্ব দিয়ে:
TotalCalculator,ReportFormatter,ReportArchive. প্রতিটির ভেতরে একাধিক method থাকতেই পারে, যদি সবগুলো একই actor-এর হয়। - নির্ভরতার দিক ঠিক করুন। Report-এ মোট টাকা দেখাতে হয়, তাই
ReportFormatterTotalCalculator-কে ব্যবহার করে — ছবি ২৭.১-এর ড্যাশ তীর। উল্টো দিকে কোনো নির্ভরতা নেই, তাই চক্রও নেই। মোট টাকার নিয়ম বদলালে report-এর সংখ্যা বদলাবে — এটা তো চাওয়াই হয়। - পুরোনো caller-দের ভাঙবেন না। যাঁরা আগে
ReportServiceডাকতেন, তাঁদের জন্য একটি পাতলাReportServiceরাখা যায় যা শুধু তিনটি class-কে ডেকে দেয় — একে facade বলে (পর্ব ৩৪-এ structural pattern). এর ভেতরে কোনো ব্যবসার নিয়ম নেই, তাই এর নিজের কোনো actor-ও নেই। - Test-ও ভাগ হলো। এখন প্রতিটি দলের নিয়মের test আলাদা file-এ। Accounts দলের তারিখের অনুরোধে শুধু formatter-এর test বদলায়; archive-এর test একই থাকে এবং পাস করে — যা প্রমাণ করে ops-এর কিছু ভাঙেনি।
৪ · SRP-এর তিন স্তর — আর বেশি ভাঙার বিপদ
“Module” শব্দটি Martin ইচ্ছা করেই ব্যবহার করেছেন: এক file, এক class, এক package বা এক service — যেকোনো স্তরে একই প্রশ্ন চলে। তবে প্রতিটি স্তরে প্রশ্নের চেহারা একটু আলাদা।
Function-এর স্তরে
এখানে “এক কাজ” পরামর্শটি সত্যিই কাজে লাগে: একটি function-এ হিসাব, ছাপানো আর file লেখা মিশিয়ে ফেললে ভাগ করুন। কিন্তু এটা clean code-এর নিয়ম (পর্ব ৪১); SRP-র actor-প্রশ্ন সাধারণত এক ধাপ ওপরে ওঠে।
functionClass-এর স্তরে
SRP-এর সবচেয়ে পরিচিত জায়গা। একটি class-এর সব method একই actor-এর হলে, সংখ্যায় যতই হোক, ঠিক আছে। God class — যে class সবার সব কাজ করে — এর উল্টো ছবি।
classModule বা service-এর স্তরে
Package, library বা service ভাগ করার সময়ও একই কথা: এক দলের সিদ্ধান্ত এক জায়গায়। এখানে Conway's law (পর্ব ১-এ উল্লেখ) মনে রাখুন — দলের কাঠামো আর কোডের কাঠামো একে অপরকে টানে।
module / serviceSRP-এর সাথে পুরোনো একটি ধারণার গভীর মিল আছে: 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
cost() শুধু Rate চুক্তি জানে; নতুন ColdChain চুক্তি মেনে পাশে যোগ হয়। তিনটি পুরোনো class-এর তীর একটি ভাগ করা রেখায় মিলেছে — UML-এ এভাবে এক ত্রিভুজে একাধিক realization আঁকা চলে।এবার একটি পূর্ণ গল্প: একটি কাল্পনিক courier-এর checkout-এ নতুন পেমেন্ট পদ্ধতি যোগ করা। লক্ষ করুন, OCP-তে পৌঁছাতে একবার পুরোনো কোড বদলাতেই হয় — সেটা refactoring; OCP-র সুফল আসে তার পরের বদলগুলোতে।
| কোড | |
| পুরোনো কোড বদল | |
| Test | |
| প্রশ্ন |
- শুরুর অবস্থা। Checkout-এর তিনটি কাজ — টাকা নেওয়া, receipt-এর label, টাকা ফেরত — প্রতিটিতে একই
if method == "cod" / "card" / "wallet". তিন পদ্ধতি × তিন কাজ = ৯টি test আছে। - বদলের অক্ষ চিনুন। Issue tracker আর Git ইতিহাস বলছে, কাল্পনিক এই দলে গত বছরে দুবার নতুন পেমেন্ট পদ্ধতি এসেছে, আর প্রতিবার তিনটি জায়গা খুঁজতে হয়েছে। এটাই সম্ভাব্য বদলের দিক — এখানে খোলা রাখার যুক্তি আছে।
- চুক্তি বানান। তিনটি কাজ থেকেই চুক্তি:
charge(amount),label(),refund(amount). চুক্তিতে শুধু সেটুকু, যা checkout-এর লাগে — বেশি নয় (এর কারণ পর্ব ২৮-এর interface segregation). - প্রতিটি শাখা নিজের class-এ।
Cod,Card,Wallet— প্রতিটি চুক্তির তিনটি method লেখে। নাম থেকে object-এরMETHODSregistry. এটা একবারের refactoring — পুরোনো কোড বদলায়, তাই আগের ৯টি test চালিয়ে নিশ্চিত হন আচরণ একই আছে। - Switch সরান। Checkout এখন শুধু
METHODS[name].charge(total)ডাকে। কোনো if বাকি নেই; checkout জানেই না কতগুলো পদ্ধতি আছে। - নতুন অনুরোধ — voucher. নতুন file-এ
Voucherclass, আর অ্যাপ যেখানে সব অংশ জোড়া লাগায় (একে composition root বলে; পর্ব ৩৭-এ dependency injection) সেখানে এক লাইন:METHODS["voucher"] = Voucher(). সততার সাথে বলি — কোথাও না কোথাও কাউকে নতুন class-এর কথা জানতেই হয়। OCP-র “closed” মানে ব্যবসার মূল কোড বন্ধ, পুরো প্রোগ্রামের প্রতিটি লাইন নয়। - Test-এর লাভ। Voucher-এর জন্য ৩টি নতুন test; পুরোনো ৯টির কোনো বদল নেই এবং সব পাস। Diff-এ শুধু একটি নতুন file আর এক লাইনের নিবন্ধন।
৭ · Extension point-এর নানা রূপ
নতুন আচরণ ঢোকানোর জায়গাকে প্রায়ই extension point বলা হয় (পর্ব ২৩-এর use case-এর extension point-এর সাথে নামের মিল কাকতালীয় নয় — চিন্তাটা একই)। কোডে এটি কয়েকভাবে বানানো যায়:
| উপায় | কীভাবে | কখন মানায় |
|---|---|---|
| Polymorphism / strategy | Interface আর তার implementation; caller শুধু interface জানে | রূপগুলোর আচরণ উল্লেখযোগ্যভাবে আলাদা, একাধিক method লাগে |
| Plugin | মূল প্রোগ্রাম একটি নিবন্ধনের ব্যবস্থা দেয়; বাইরের কোড নিজেকে যোগ করে, অনেক সময় আলাদা package হিসেবে | অন্য দল বা অন্য প্রতিষ্ঠান বাড়াবে; মূল কোড তাদের হাতে নেই |
| Configuration | আচরণ data-তে: দামের টেবিল, নিয়মের ক্রম, চালু-বন্ধ সুইচ (feature flag) | রূপগুলো একই ছাঁচের, শুধু সংখ্যা বা ক্রম আলাদা |
| Higher-order function | Function-কে 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, পড়তে বেশি সময়।
| প্রশ্ন | |
| উত্তর | |
| সংকেত |
- প্রস্তাব। একজন সহকর্মী বললেন, “কর তো বদলাতে পারে —
TaxPolicyinterface, একটি factory আর একটি config file বানিয়ে রাখি।” কথাটা শুনতে দূরদর্শী। প্রশ্ন করার পালা। - আজ কয়টি রূপ? একটি — সব বেতনে একই VAT নিয়ম। একটিমাত্র implementation-এর interface প্রায়ই সংকেত যে abstraction-টি কল্পনা থেকে এসেছে, বাস্তব থেকে নয়। (ব্যতিক্রম: test-এ নকল বসানোর জন্য, বা অন্য দলের সাথে চুক্তি হিসেবে — পর্ব ২৮ ও ৪৯।)
- ইতিহাস।
git log -- tax.pyদেখাচ্ছে দুই বছরে একটিমাত্র commit — প্রথম লেখা। এদিকে বদল আসেনি। - Requirement-এ প্রমাণ। Backlog-এ দ্বিতীয় কোনো কর-নিয়মের story নেই, কোনো stakeholder চাননি। অনুমানটি কারও প্রয়োজনের ওপর দাঁড়িয়ে নেই।
- দামের হিসাব। তিনটি বাড়তি file, প্রতিটি ডাকে এক স্তর বেশি, নতুন সদস্যের বুঝতে বেশি সময়। লাভ এখনো শূন্য, খরচ আজই নিশ্চিত।
- সিদ্ধান্ত: এখনো নয়। একটি সরল, ভালো নামের function আর তার test. সরল কোড পরে বদলানোও সহজ — ভালো test থাকলে interface বের করা কয়েক মিনিটের refactoring.
- আট মাস পরে — প্রথম গুলি। দ্বিতীয় 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 আর গ্রাহকের তথ্য সরিয়ে নিন।
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, একটি আউটপুট অনুমান, একটি নকশার সমালোচনা আর একটি হিসাব — প্রতিটির পূর্ণ উত্তর দেওয়া আছে।
-
এই কোড 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— চুপচাপ ভুলের চেয়ে জোরে ব্যর্থ হওয়া ভালো। সহজ করতে এখানে globalFEESবদলানো হয়েছে; বাস্তবে constructor-এ টেবিল দেওয়া (FeeCalculator(fees=...)) test-এ বেশি নিরাপদ। -
আউটপুট অনুমান করুন: নিচের 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 ruleMap-এ একই key-তে আবারsetকরলে মান বদলায়, কিন্তু key-র প্রথম ঢোকানোর অবস্থান থেকে যায়। তাই ক্রমflat, তারপরhalf— আরflat-এর function এখনx - 300. হিসাব: ২০০০ − ৩০০ = ১৭০০, তারপর ১৭০০ / ২ = ৮৫০। Map-এ দুটিই key, তাইsize২।rules.get("vat")undefined,?.()ডাকটাই এড়িয়েundefinedদেয়, আর??তখন ডান দিকের লেখাটি নেয়। শিক্ষা: plugin-এর registry-তে একই নামে দ্বিতীয় নিবন্ধন চুপচাপ পুরোনোটিকে মুছে দেয় — অনেক বাস্তব ব্যবস্থা তাই একই নাম দুবার পেলে error দেয়। -
নকশার সমালোচনা: একটি কাল্পনিক courier অ্যাপে একজন সহকর্মী (বা একটি AI) এই নকশা দিয়েছেন: (১)
Invoice-কে ভেঙেInvoiceTotalAdder,InvoiceVatAdder,InvoiceRounder— প্রতিটিতে একটি method, তিনটিই হিসাব বিভাগের নিয়ম; (২)IDistanceCalculatorinterface, একটিমাত্র 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), আর একই নামে দুবার নিবন্ধনও আটকান।
সারকথা: একই নকশায় নীতির অপব্যবহার (১, ২) আর নীতির প্রয়োজন (৩) পাশাপাশি থাকতে পারে। বিচারের মাপকাঠি বদলের প্রমাণ — নীতির নাম নয়।
- (১) বেশি ভাঙা। তিনটিই একই actor-এর নিয়ম, আর VAT-এর নিয়ম বদলালে সম্ভবত total আর rounding-ও বদলাবে — shotgun surgery. একটি
-
হিসাব — কোন 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-এ মোট কতবার “অন্য দলের অংশ” অকারণে পুনঃপরীক্ষা হলো?File CR সংখ্যা 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 ছোঁবে না। তাছাড়া প্রায় প্রতিটি CRfee_manager.py-কেও ছুঁয়েছে — সেটা ইঙ্গিত দেয় অন্য দলের কাজও এই file-এর ভেতরে জড়ানো।(গ)
fee_manager.py-তে চারটি actor-এর অংশ। প্রতিটি বদলে অনুরোধকারী বাদে বাকি ৩ দলের অংশ পুনঃপরীক্ষা: ৬টি CR × ৩ = ১৮ বার। File-টি actor ধরে চারটি file-এ ভাঙলে প্রতিটি CR শুধু নিজের দলের file ছোঁবে — অকারণ পুনঃপরীক্ষা ০। (পুরো regression suite তো CI-তে চলবেই; লাভটা হলো মানুষের review, সমন্বয় আর ঝুঁকিতে।)
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমবহু জায়গায় ছড়ানো একটি type-switch-কে OCP মেনে refactor করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- ইতিহাস দেখে নিশ্চিত হওয়া যে নতুন type সত্যিই বারবার আসে
- বর্তমান আচরণ test দিয়ে বেঁধে ফেলা
- সব switch থেকে প্রয়োজনীয় কাজগুলো নিয়ে একটি interface সংজ্ঞায়িত করা
- প্রতিটি শাখা নিজের class-এ সরিয়ে registry-তে নিবন্ধন করা
- Switch-গুলোর জায়গায় registry থেকে object নিয়ে ডাকা, আর পুরোনো test চালানো
- নতুন 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"যোগ করা Expressclass খুলে তার ভেতরে cold chain-এর শর্ত যোগ করাRateinterface-এ একটি নতুন parameter যোগ করাRateমেনে চলা নতুনColdChainclass লিখে 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-এ রাখা উচিত
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- SOLID পাঁচটি নীতি, লক্ষ্য একটাই — বদল সস্তা আর নিরাপদ; নীতিগুলো প্রশ্নের ছাঁচ, আইন নয়।
- SRP: একটি module-এর দায় একটি actor-এর কাছে — যে একদল মানুষ এর বদল চান। “এক কাজ” function-এর পরামর্শ, SRP নয়।
- দুই দিকের নিয়ম: যা একই কারণে বদলায়, একসাথে; যা ভিন্ন কারণে, আলাদা। বেশি ভাঙা মানে shotgun surgery.
- OCP: Meyer (১৯৮৮) inheritance দিয়ে, Martin abstraction আর বদলযোগ্য implementation দিয়ে। বারবারের type-switch-এর ওষুধ registry, polymorphism, plugin, configuration বা function-এর map.
- OCP কৌশলগত: প্রমাণিত বদলের দিকে খোলা রাখুন; প্রথম বদলের আগে abstraction প্রায়ই YAGNI. আর SRP-OCP-র বড় লাভ ছোট, স্বাধীন test আর ছোট diff-এ।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২৮ · SOLID ২ — Liskov substitution, interface segregation আর dependency inversionপরবর্তী পর্বচুক্তি মানা, চুক্তি ছোট রাখা আর নির্ভরতার দিক উল্টে দেওয়া।
- পর্ব ২৬ · নকশার নীতি — DRY, KISS, YAGNI, separation of concerns আর Law of Demeterআগের পর্বYAGNI আর DRY — OCP আর SRP-এর সীমা বোঝার ভিত্তি।
- সফটওয়্যার ইঞ্জিনিয়ারিং ও Git · কোর্স হোম৭০টি পর্বপুরো কোর্সের রোডম্যাপ; মডিউল ৬-এ strategy, decorator-সহ design pattern.
- Python · ABC, duck typing আর Protocolপাশের ট্র্যাকPython-এ abstraction লেখার তিন উপায় — OCP-র চুক্তির ভাষা।
- Java · polymorphism আর dynamic dispatchগভীরে যানএক ডাকে ভিন্ন আচরণ — Martin-এর OCP-র যন্ত্র কীভাবে চলে।