এই পর্বে যা শিখবেন
- আসল (জ্ঞানের) duplication আর কাকতালীয় duplication আলাদা করা, আর ধাপে ধাপে একটি নিয়মকে এক উৎসে আনা
- Rule of three, WET আর AHA কখন DRY-এর চেয়ে ভালো পরামর্শ, তা বোঝা
- একটি স্পষ্ট অনুমানের cost model দিয়ে YAGNI-র সিদ্ধান্ত হিসাব করা
- Train-wreck chain চেনা, আর তাকে tell-don't-ask-এ refactor করা; কখন chain ঠিক আছে (builder, stream) তাও জানা
- Least astonishment, fail fast আর command–query separation প্রয়োগ করা, আর নীতির টানাপোড়েনে যুক্তি দিয়ে সিদ্ধান্ত নেওয়া
১ · নকশার নীতি কেন — নিয়মের তালিকা নয়, বদলের খরচ কমানোর অভ্যাস
আগের পর্বে (পর্ব ২৫, modularity, coupling আর cohesion) নকশা মাপার দুটি মাপকাঠি দেখেছেন: coupling — দুটি অংশ একে অপরের ওপর কতটা নির্ভর করে, আর cohesion — একটি অংশের ভেতরের জিনিসগুলো কতটা একই কাজে লাগে। কিন্তু মাপকাঠি জানলেই হাত চলে না। কোড লেখার মুহূর্তে আপনার দরকার ছোট, মনে রাখার মতো কিছু পরামর্শ: “এই নিয়মটা কি আগেও কোথাও লিখেছি?”, “এটা কি এখনই দরকার?”, “এই function কি অন্য object-এর ভেতরে বেশি ঢুকে পড়ছে?” এই পরামর্শগুলোই নকশার নীতি (design principle).
রান্নাঘরের রেসিপি খাতার কথা ভাবুন। মা যদি বিরিয়ানির মশলার মাপ তিনটি আলাদা পাতায় লেখেন — একটি ঈদের পাতায়, একটি বিয়েবাড়ির পাতায়, একটি রোজকার পাতায় — তাহলে একদিন লবণ কমাতে গিয়ে দুটি পাতা ঠিক করবেন, তৃতীয়টি ভুলে যাবেন (DRY). আবার রোজকার ডালের রেসিপিতে “একদিন যদি ৫০ জন খান” তার হিসাব আগাম লিখে রাখা অপচয় (YAGNI). আর মশলা কেনার পাতা, রান্নার পাতা আর অতিথির তালিকা আলাদা রাখা — যাতে একটি বদলালে অন্যটি ঘাঁটতে না হয় — সেটাই separation of concerns.
নিচের মানচিত্রটি পুরো পর্বের সারাংশ। প্রতিটি নীতি আলাদা পথে একই লক্ষ্যে যায় — কম coupling আর বেশি cohesion, যার ফল হলো maintainability (পর্ব ০৩, software quality). লাল ড্যাশ রেখাগুলো টানাপোড়েন: একটি নীতি অতিরিক্ত টানলে অন্য দিক ক্ষতিগ্রস্ত হয়। §৮-এ এগুলো নিয়ে বিস্তারিত।
নীতির মানচিত্র — সব পথ একই লক্ষ্যে
Principles map: how DRY, KISS, YAGNI, SoC and LoD serve low coupling and high cohesion
মনে রাখুন, এই নীতিগুলো উপপাদ্য নয়; কয়েক দশকের অভিজ্ঞতা থেকে আসা heuristic. এগুলো SOLID-এরও ভিত্তি — SOLID আসবে পর্ব ২৭ (single responsibility আর open-closed) ও পর্ব ২৮-এ (Liskov, interface segregation আর dependency inversion); এখানে শুধু নাম ধরে আসবে।
পুরো পর্ব জুড়ে একটি উদাহরণ চলবে: একটি মিষ্টির দোকানের অনলাইন অর্ডার app (কাল্পনিক). গ্রাহক অর্ডার দেন, cart-এ ছাড় হিসাব হয়, invoice তৈরি হয়, SMS-এ রসিদ যায়, আর গ্রাহকের app-এর ভেতরের wallet থেকে টাকা কাটে।
২ · DRY — একটি জ্ঞান, একটি জায়গা
১৯৯৯ সালে Andrew Hunt আর David Thomas তাঁদের বই The Pragmatic Programmer-এ নীতিটির নাম দেন DRY — Don't Repeat Yourself. তাঁদের সংজ্ঞাটি হুবহু মনে রাখার মতো:
লক্ষ্য করুন, বাক্যে “code” শব্দটি নেই — আছে knowledge, জ্ঞান। জ্ঞান মানে একটি ব্যবসায়িক নিয়ম (“৮০০ টাকার বেশি অর্ডারে ১২% ছাড়”), একটি তথ্যের গঠন (অর্ডারে কোন কোন field), একটি algorithm, এমনকি একটি configuration মান। এই জ্ঞানের একটি single source of truth থাকবে; বাকি সব জায়গা সেখান থেকে নেবে। বইটির ২০১৯ সালের ২০তম বার্ষিকী সংস্করণে লেখকেরা নিজেরাই জোর দিয়ে বলেছেন যে অনেকে DRY-কে ভুলভাবে “কোডের লাইন কপি করা নিষেধ” বুঝেছেন; তাঁদের উদ্দেশ্য ছিল জ্ঞান আর উদ্দেশ্যের পুনরাবৃত্তি বন্ধ করা।
Duplication শুধু কোডে থাকে না: একই জ্ঞান থাকতে পারে কোড আর comment-এ (comment বলছে ১০%, কোড করছে ১২%), কোড আর database schema-য়, বা দুটি দলের আলাদা service-এ। যেখানেই দুটি কর্তৃত্বপূর্ণ রূপ, সেখানেই একদিন দুটি আলাদা হয়ে যাবে।
একটি নিয়ম, তিনটি কপি — কী ঘটে
মিষ্টির দোকানে পুরোনো নিয়ম ছিল: ১০০০ টাকা বা তার বেশি অর্ডারে ১০% ছাড়। ঈদের আগে মালিক বললেন, এবার ৮০০ টাকা বা বেশিতে ১২% ছাড়। নিয়মটি কোডে তিন জায়গায় লেখা ছিল — cart, invoice আর SMS রসিদ। Developer দুটি ঠিক করলেন, তৃতীয়টি ভুলে গেলেন।
// DRY: one business rule (the discount) copied into three places.
// Fictional sweet shop. Old rule: 1000 tk or more -> 10% off.
// New Eid rule: 800 tk or more -> 12% off. The developer updated the cart
// and the invoice, but forgot the copy inside the SMS receipt.
function cartTotal(amount) { // updated
return amount >= 800 ? Math.round(amount * 0.88) : amount;
}
function invoiceTotal(amount) { // updated (written differently)
const discount = amount >= 800 ? amount * 0.12 : 0;
return Math.round(amount - discount);
}
function smsReceiptTotal(amount) { // forgotten: still the old rule
if (amount >= 1000) return Math.round(amount * 0.9);
return amount;
}
console.log("--- rule copied in 3 places ---");
for (const a of [700, 900, 1200]) {
console.log(a + " tk: cart " + cartTotal(a) + ", invoice " + invoiceTotal(a) +
", sms " + smsReceiptTotal(a));
}
// Single source of truth: the knowledge lives in ONE place.
const DISCOUNT_RULE = { minAmount: 800, rate: 0.12 }; // change it here only
function discountedTotal(amount, rule = DISCOUNT_RULE) {
return amount >= rule.minAmount ? Math.round(amount * (1 - rule.rate)) : amount;
}
const places = { cart: discountedTotal, invoice: discountedTotal, sms: discountedTotal };
console.log("--- single source ---");
for (const a of [700, 900, 1200]) {
const out = Object.entries(places).map(([name, f]) => name + " " + f(a));
console.log(a + " tk: " + out.join(", "));
}আউটপুট:
--- rule copied in 3 places ---
700 tk: cart 700, invoice 700, sms 700
900 tk: cart 792, invoice 792, sms 900
1200 tk: cart 1056, invoice 1056, sms 1080
--- single source ---
700 tk: cart 700, invoice 700, sms 700
900 tk: cart 792, invoice 792, sms 792
1200 tk: cart 1056, invoice 1056, sms 1056
৭০০ টাকার অর্ডারে কোনো নিয়মেই ছাড় নেই, তাই তিনটি মান এক — এই কারণে দ্রুত হাতে-পরীক্ষায় ভুলটি চোখে পড়ে না। ৯০০ টাকায় নতুন নিয়মে ৯০০ × ০.৮৮ = ৭৯২, কিন্তু SMS পুরোনো নিয়মে চলছে (৯০০ < ১০০০, তাই ছাড় নেই) — গ্রাহক app-এ ৭৯২ দেখলেন, SMS-এ ৯০০. ১২০০ টাকায় ১০৫৬ বনাম ১০৮০. খেয়াল করুন, cart আর invoice-এর কোড দেখতে আলাদা (একটিতে গুণ, অন্যটিতে বিয়োগ), তবু দুটিতে একই জ্ঞান — তাই কোডের লেখা মিলিয়ে খুঁজলে এই duplication ধরা পড়ত না। দ্বিতীয় অংশে নিয়মটি একটি data (DISCOUNT_RULE) আর একটি function-এ; পরের বদল এক লাইনে, আর তিনটি জায়গা কখনো আলাদা হতে পারে না।
আসল duplication বনাম কাকতালীয় duplication
DRY-এর সবচেয়ে বড় ভুল প্রয়োগ হলো দেখতে একই রকম সব কোড জোড়া লাগানো। দুটি প্রশ্ন জিজ্ঞেস করুন:
- দুটি কি একই কারণে, একই মানুষের সিদ্ধান্তে বদলায়? হ্যাঁ হলে একই জ্ঞান — এক উৎসে আনুন।
- একটি বদলালে অন্যটিও কি অবশ্যই বদলাতে হবে? না হলে এটি incidental duplication — আজ মিলে গেছে, কাল আলাদা হবে।
মিষ্টির দোকানে delivery-র নিয়মও ৮০০: “৮০০ টাকার বেশি অর্ডারে delivery ফ্রি।” সংখ্যাটি ছাড়ের সাথে মিলে গেছে, কিন্তু এটি delivery ব্যবস্থাপকের সিদ্ধান্ত, ছাড়টি মার্কেটিংয়ের। কেউ যদি দুটিকে একটি THRESHOLD = 800-এ জুড়ে দেন, তাহলে পরের মাসে মার্কেটিং ছাড়ের সীমা ৫০০ করলে delivery-ও হঠাৎ ৫০০ টাকায় ফ্রি হয়ে যাবে — কেউ চাননি। এটি শুধু কাকতালীয় মিলের ভিত্তিতে জোড়া — পর্ব ২৫-এর coincidental cohesion-এর আত্মীয় — আর ফল অপ্রয়োজনীয় coupling. নিচের stepper-এ পুরো কাজটি ধাপে ধাপে দেখুন।
- খোঁজা
800 · 0.88৪টি জায়গা - বাছাই
৩ + ১একই জ্ঞান? - Test
700·900·1200আচরণ আটকানো - এক উৎস
pricing.jsনিয়ম + function - কপি মোছা
১ লাইনপরের বদল
| নিয়ম কোথায় | |
| ৯০০ টাকার অর্ডার (cart / invoice / sms) | |
| পরের নিয়ম বদলের খরচ |
- খোঁজা। ঈদের নিয়ম বদলের পরে গ্রাহকের অভিযোগ: app-এ ৭৯২, SMS-এ ৯০০. দল কোডে
800,1000আর0.88খুঁজে চারটি জায়গা পেল — cart.js, invoice.js, sms.js, আর delivery.js-এ একটি800. - বাছাই — একই জ্ঞান কি? প্রথম তিনটি একই নিয়ম, একই মানুষ (মার্কেটিং) বদলান, একটি বদলালে বাকিগুলোও বদলাতে হবে — আসল duplication. delivery.js-এর ৮০০ delivery ব্যবস্থাপকের নিয়ম; শুধু সংখ্যা মিলেছে। তাই এটিকে জোড়া লাগানো হবে না।
- আগে test দিয়ে আচরণ আটকানো। ৭০০, ৯০০ আর ১২০০ টাকার জন্য তিনটি পথেই প্রত্যাশিত মান (৭০০, ৭৯২, ১০৫৬) দিয়ে ছোট test লেখা হলো। Cart আর invoice-এর test সবুজ, sms-এর test লাল — ভুলটি এখন লিখিত প্রমাণসহ ধরা পড়ল। ছোট নিরাপদ ধাপে refactoring পর্ব ৪২-এর বিষয়।
- এক উৎস তৈরি। নতুন file
pricing.js-এDISCOUNT_RULE = {minAmount: 800, rate: 0.12}আরdiscountedTotal(). পুরোনো তিনটি কপি এখনো আছে, কোনো caller এখনো বদলায়নি — তাই আচরণ আগের মতোই, sms এখনো ৯০০. - Caller একে একে বদলানো। প্রথমে cart, test চালানো; তারপর invoice, test; তারপর sms — এবার sms-এর লাল test সবুজ হলো: ৯০০ টাকায় তিনটিই ৭৯২. প্রতিটি বদল ছোট আর আলাদা commit, তাই কিছু ভাঙলে ঠিক কোন ধাপে, তা বোঝা যায়।
- পুরোনো কপি মুছে ফেলা। তিনটি পুরোনো function মুছে ফেলা হলো; delivery.js অপরিবর্তিত থাকল। পয়লা বৈশাখে মালিক ১৫% ছাড় চাইলে বদল এখন এক লাইনে —
rate: 0.15— আর তিনটি জায়গা একসাথে বদলাবে।
DRY-এর নামে ভুল জোড়া লাগানোর লক্ষণ: shared function-এ একের পর এক if (caller === "sms") জাতীয় flag আর parameter জমছে, কারণ প্রতিটি caller-এর চাহিদা আলাদা হয়ে যাচ্ছে। এটি বলছে দুটি জ্ঞান জোর করে একসাথে রাখা হয়েছে। তখন shared function ভেঙে আবার আলাদা করাই সঠিক refactoring.
৩ · কখন কপি ভালো — rule of three, WET আর AHA
যদি দুটি কোড একই জ্ঞান কি না নিশ্চিত না হন? অভিজ্ঞ developer-রা তখন একটু অপেক্ষা করার পরামর্শ দেন। এর পেছনে তিনটি প্রচলিত ধারণা আছে।
3️⃣ Rule of three
প্রথমবার লিখুন। দ্বিতীয়বার একই জিনিস লিখতে হলে অস্বস্তি নিয়েও কপি করুন। তৃতীয়বার এলে refactor করে এক জায়গায় আনুন। Martin Fowler-এর Refactoring (১৯৯৯) বইয়ে এই rule of three-কে Don Roberts-এর পরামর্শ হিসেবে উদ্ধৃত করা হয়েছে (“three strikes and you refactor”). তিনটি উদাহরণ দেখলে কোনটি সত্যিই সাধারণ আর কোনটি কাকতালীয়, তা স্পষ্ট হয়।
💧 WET
DRY-এর বিপরীত শব্দ হিসেবে রসিকতা করে বলা হয় WET — “Write Everything Twice” বা “We Enjoy Typing”. কখনো এটি সমালোচনা (অকারণ কপি), কখনো সচেতন নীতি: দুবার পর্যন্ত কপি চলবে, তৃতীয়বারে ভাবুন — rule of three-এর অন্য নাম।
🐢 AHA
AHA — “Avoid Hasty Abstractions”, তাড়াহুড়ো করে abstraction নয়। নামটি JavaScript শিক্ষক Kent C. Dodds-এর লেখার মাধ্যমে ২০১৯-এর দিকে ছড়িয়েছে বলে সাধারণত উল্লেখ করা হয়। মূল বক্তব্য: পরিবর্তনের ধরন বোঝার আগে abstraction বানাবেন না।
এই ধারার সবচেয়ে বেশি উদ্ধৃত বাক্যটি Sandi Metz-এর — ২০১৪ সালের একটি বক্তৃতায় আর ২০১৬ সালের “The Wrong Abstraction” লেখায় তিনি বলেন, duplication ভুল abstraction-এর চেয়ে অনেক সস্তা (“duplication is far cheaper than the wrong abstraction”). কারণটি সহজ: দুটি কপি থাকলে প্রতিটি নিজের মতো বদলাতে পারে, ক্ষতি সীমিত। কিন্তু ভুল abstraction একবার দশটি জায়গায় ছড়িয়ে পড়লে সবাই সেটিকে মানিয়ে নিতে parameter আর flag যোগ করেন, আর সেটি ভাঙতে সাহস লাগে — কারণ “কেউ নিশ্চয়ই কোনো কারণে বানিয়েছিলেন”।
| পরিস্থিতি | সিদ্ধান্ত | কারণ |
|---|---|---|
| ব্যবসায়িক নিয়ম, দাম, কর, সীমা | প্রথম কপিতেই এক উৎসে | ভুল হলে টাকা আর বিশ্বাসের ক্ষতি; জ্ঞানটি স্পষ্টভাবে একটি |
| দুটি screen-এর দেখতে-একই validation | অপেক্ষা (rule of three) | একটি হয়তো শিগগির আলাদা নিয়ম পাবে |
| Test-এর ভেতরের setup কোড | প্রায়ই কপি চলবে | Test পড়ে বোঝা সহজ হওয়া বেশি জরুরি — অনেকে একে বলেন DAMP (Descriptive And Meaningful Phrases) |
| দুটি আলাদা দলের service-এ একই helper | সাবধানে; অনেক সময় কপিই ভালো | Shared library দুই দলের release-কে বেঁধে ফেলে (§৮) |
৪ · KISS আর YAGNI — সরল রাখা, আগাম না বানানো
KISS — সবচেয়ে সরল যা কাজ করে
KISS সাধারণত “Keep It Simple, Stupid” হিসেবে পরিচিত (কেউ কেউ ভদ্রভাবে বলেন “Keep It Short and Simple”). নীতিটি প্রায়ই মার্কিন বিমান প্রকৌশলী Kelly Johnson-এর নামে উদ্ধৃত হয়, যিনি Lockheed-এর Skunk Works নামের নকশা দলের প্রধান ছিলেন। প্রচলিত গল্প অনুযায়ী, তিনি চাইতেন যুদ্ধক্ষেত্রে একজন সাধারণ mechanic সাধারণ যন্ত্রপাতি দিয়েই বিমান সারাতে পারবেন — সেই মাপে নকশা সরল রাখতে হবে। গল্পের খুঁটিনাটি উৎসভেদে আলাদা, কিন্তু শিক্ষাটি স্পষ্ট: সরলতা সৌন্দর্য নয়, চাপের মধ্যে মেরামতযোগ্যতা.
Software-এ সরল মানে কম অংশ, কম সংযোগ, কম বিশেষ নিয়ম, আর এমন কোড যা রাত দুইটায় production ভাঙলে একজন ক্লান্ত developer-ও বুঝতে পারেন। পর্ব ০২-এ Brooks-এর ভাষায় দেখেছেন, সমস্যার নিজের জটিলতা (essential complexity) কমানো যায় না, কিন্তু আমাদের বানানো বাড়তি জটিলতা (accidental complexity) কমানো যায়। KISS দ্বিতীয়টির বিরুদ্ধে। সরল আর সহজ যে এক জিনিস নয়, তা ভাবনার প্রশ্ন ৫-এ দেখবেন।
YAGNI — আপনার এটা লাগবে না
YAGNI — “You Aren't Gonna Need It” — এসেছে ১৯৯০-এর দশকের শেষের Extreme Programming (XP) থেকে (পর্ব ০৫-এ XP-র কথা দেখেছেন)। XP-র সহ-প্রতিষ্ঠাতা Ron Jeffries-এর নামে প্রচলিত পরামর্শটি হলো: কোনো জিনিস তখনই বানান যখন সত্যিই দরকার, শুধু “দরকার হতে পারে” ভেবে কখনো নয়। এটি অলসতার পরামর্শ নয়; এটি একটি অর্থনৈতিক যুক্তি। Martin Fowler ২০১৫ সালের একটি লেখায় আগাম (presumptive) feature-এর খরচ চার ভাগে ভাগ করেছেন:
- বানানোর খরচ: যে সময় দরকারি কাজ থেকে সরে গেল।
- দেরির খরচ: দরকারি কাজটি, আর তার মূল্য, দেরিতে পৌঁছাল।
- বহনের খরচ (cost of carry): feature-টি কোডে বসে থাকে; প্রতিটি নতুন বদলে এটিকে বুঝতে, test করতে, মানিয়ে নিতে হয়।
- মেরামতের খরচ: দরকার হলেও আগাম অনুমান প্রায়ই ভুল — তখন নতুন করে বানাতে হয়।
যা আসলে কখনো লাগেনি, সেই কোডের বহনের খরচ যে কত ভয়ংকর হতে পারে, তা পর্ব ০২-এর দুটি ঘটনায় দেখেছেন: Ariane 5-এ Ariane 4 থেকে আনা একটি হিসাব, যা উড্ডয়নের পরে আর দরকার ছিল না, তবু চলছিল; আর Knight Capital-এ বহু বছর পুরোনো অব্যবহৃত কোড একটি পুনর্ব্যবহৃত flag-এ জেগে উঠেছিল (পর্ব ১৭-এও আলোচিত)। প্রতিদিনের project-এ খরচ এত নাটকীয় নয়, কিন্তু নিয়মিত। নিচের প্রোগ্রামটি একটি সরল cost model — অনুমানগুলো প্রথম লাইনগুলোতেই লেখা, আর সংখ্যাগুলো কাল্পনিক।
// YAGNI cost model (fictional numbers, developer-days, 12-month horizon).
// Assumptions: building the feature now costs 10 days; while it sits in the
// code it costs 0.3 day per month (tests, reviews, slower changes). If it is
// really needed, the month of need is uniform 1..12, and an early guess is
// wrong 40% of the time (6 days of rework). Building later costs LATE days,
// plus carrying cost only from the month of need to month 12.
function mulberry32(a) { return function () {
a |= 0; a = a + 0x6D2B79F5 | 0; let t = Math.imul(a ^ a >>> 15, 1 | a);
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; }; }
const BUILD = 10, CARRY = 0.3, MONTHS = 12, WRONG = 0.4, REWORK = 6, TRIALS = 20000;
function simulate(p, late, rand) {
let now = 0, later = 0;
for (let i = 0; i < TRIALS; i++) {
const needed = rand() < p;
const month = 1 + Math.floor(rand() * MONTHS); // month of need
const wrong = rand() < WRONG; // early guess wrong?
now += BUILD + CARRY * MONTHS + (needed && wrong ? REWORK : 0);
if (needed) later += late + CARRY * (MONTHS - month);
}
return [now / TRIALS, later / TRIALS];
}
for (const late of [12, 25]) {
const rand = mulberry32(2026);
console.log("late build cost = " + late + " days");
for (const p of [0.1, 0.3, 0.5, 0.7, 0.9]) {
const [now, later] = simulate(p, late, rand);
console.log(" p=" + p.toFixed(1) + " build now " + now.toFixed(2) + " wait " +
later.toFixed(2) + " -> " + (now < later ? "build now" : "wait"));
}
const be = (BUILD + CARRY * MONTHS) / (late + CARRY * (MONTHS - 1) / 2 - WRONG * REWORK);
console.log(" break-even p = " + be.toFixed(2) + (be > 1 ? " (> 1: waiting always wins)" : ""));
}আউটপুট:
late build cost = 12 days
p=0.1 build now 13.84 wait 1.38 -> wait
p=0.3 build now 14.32 wait 4.11 -> wait
p=0.5 build now 14.80 wait 6.86 -> wait
p=0.7 build now 15.27 wait 9.56 -> wait
p=0.9 build now 15.74 wait 12.21 -> wait
break-even p = 1.21 (> 1: waiting always wins)
late build cost = 25 days
p=0.1 build now 13.84 wait 2.68 -> wait
p=0.3 build now 14.32 wait 8.02 -> wait
p=0.5 build now 14.80 wait 13.40 -> wait
p=0.7 build now 15.27 wait 18.66 -> build now
p=0.9 build now 15.74 wait 23.84 -> build now
break-even p = 0.56
হাতে হিসাব মিলিয়ে নিন। এখন বানালে প্রত্যাশিত খরচ = ১০ + ০.৩ × ১২ + p × ০.৪ × ৬ = ১৩.৬ + ২.৪p; p = ০.১ হলে ১৩.৮৪ — simulation-এর সাথে হুবহু মেলে, কারণ এখানে এলোমেলো অংশ শুধু rework-এ। অপেক্ষা করলে = p × (LATE + ০.৩ × গড়ে বাকি মাস); মাস ১ থেকে ১২ সমান সম্ভাবনায় এলে বাকি মাসের গড় (১২ − ১) ÷ ২ = ৫.৫, তাই LATE = ১২ হলে p × ১৩.৬৫ — p = ০.১-এ ১.৩৭, simulation-এ ১.৩৮ (এলোমেলো নমুনার সামান্য পার্থক্য)। সমান খরচের বিন্দু: ১৩.৬ + ২.৪p = p × (LATE + ১.৬৫) → p = ১৩.৬ ÷ (LATE + ১.৬৫ − ২.৪). LATE = ১২ হলে p = ১.২১ — সম্ভাবনা ১-এর বেশি হতে পারে না, তাই এই অনুমানে অপেক্ষাই সবসময় সস্তা। কিন্তু পরে বানানো যদি খুব দামি হয় (LATE = ২৫, যেমন পুরোনো data-র গঠন বদলাতে হবে), তখন p ০.৫৬-এর বেশি হলে আগে বানানোই যুক্তিসঙ্গত। শিক্ষা: YAGNI অন্ধ নিয়ম নয়; প্রশ্নটি হলো “পরে বানানো কতটা কঠিন?” — আর সেই খরচ কমানোর সবচেয়ে ভালো উপায় সাধারণত পরিষ্কার, সরল নকশা।
শেষ কথাটিই YAGNI-র সবচেয়ে ভুল-বোঝা অংশ। YAGNI feature-এর বিরুদ্ধে, নকশার মানের বিরুদ্ধে নয়। Refactoring, test আর পরিষ্কার নাম কোনো ভবিষ্যৎ feature নয় — এগুলোই পরে বানানোকে সস্তা রাখে। নিচের stepper-এ একটি আসল সিদ্ধান্ত দেখুন।
- প্রস্তাব
USD · GBP“হয়তো লাগবে” - প্রমাণ
story নেইp ≈ ০.২ - হিসাব
১৪.০৮ বনাম ২.৭৩দিন - পরে কঠিন?
float টাকাঝুঁকি - সস্তা seam
Money১ দিন
| প্রশ্ন | |
| তথ্য | |
| সিদ্ধান্ত |
- প্রস্তাব। একজন developer বললেন, “ভবিষ্যতে বিদেশে মিষ্টি পাঠালে ডলার আর পাউন্ড লাগবে — এখনই multi-currency বানিয়ে রাখি।” আনুমানিক খরচ ১০ developer-দিন।
- প্রমাণ খোঁজা। Backlog-এ এমন কোনো user story নেই, কোনো গ্রাহক বা চুক্তিও নেই। মালিক বললেন, “হয়তো আগামী বছর।” দল দরকারের সম্ভাবনা ধরল প্রায় ০.২ — লিখিত অনুমান।
- হিসাব। উপরের cost model-এর অনুমান নিয়ে: এখন বানালে ১৩.৬ + ২.৪ × ০.২ = ১৪.০৮ দিন; পরে বানালে (LATE = ১২) ০.২ × (১২ + ১.৬৫) = ২.৭৩ দিন। পার্থক্য ১১ দিনের বেশি।
- পরে কি খুব কঠিন হবে? দাম কোডে দশমিক সংখ্যা (float) হিসেবে ২৩ জায়গায় ছড়ানো, তাই পরে বদলানো দামি — LATE হয়তো ২৫. তবু তখনও সমান খরচের বিন্দু p = ০.৫৬, আর আমাদের p = ০.২.
- Feature নয়, সস্তা seam. দল multi-currency বানাল না, কিন্তু এক দিনে একটি ছোট
Moneytype বানাল: টাকা পয়সায় পূর্ণসংখ্যা হিসেবে, পাশে মুদ্রার নাম, আর সব হিসাব এখান দিয়ে। এটি আজকের float-এর গোলমাল সারায়, আর LATE-কে ১২-র কাছে নামায়। - পুনর্বিবেচনার শর্ত লেখা। সিদ্ধান্তটি একটি ছোট নোটে লেখা হলো: “multi-currency এখন নয়; প্রথম রপ্তানি-অর্ডার বা acceptance criteria-সহ story এলে আবার দেখব।” YAGNI মানে “কখনো না” নয় — “প্রমাণ এলে”।
৫ · Separation of concerns আর “যা বদলায়, তাকে আলাদা রাখুন”
Separation of concerns (SoC) কথাটি Edsger W. Dijkstra-র ১৯৭৪ সালের একটি লেখা “On the role of scientific thought”-এ (তাঁর হাতে লেখা নোটের ধারায় EWD 447) দেখা যায় বলে সাধারণত উল্লেখ করা হয়। তাঁর বক্তব্য ছিল চিন্তার কৌশল নিয়ে: একটি সমস্যার একটি দিক নিয়ে মনোযোগ দিয়ে ভাবুন — ধরা যাক প্রোগ্রামটি ঠিক কি না — আর সেই সময় অন্য দিক (দ্রুত কি না, সুন্দর কি না) সচেতনভাবে সরিয়ে রাখুন। পরে software-এ এটি নকশার নীতি হয়ে দাঁড়ায়: আলাদা concern (দায়, বিবেচ্য বিষয়) আলাদা অংশে রাখুন।
মিষ্টির দোকানের app-এ concern-গুলো হলো দাম ও ছাড়ের নিয়ম, data সংরক্ষণ, UI, SMS পাঠানো আর লগ। একটি function যদি একসাথে ছাড় হিসাব করে, database-এ লেখে, HTML বানায় আর SMS পাঠায়, তবে SMS সরবরাহকারী বদলালে ছাড়ের কোডও ঝুঁকিতে পড়ে। পরিচিত উদাহরণ: web page-এ HTML (গঠন), CSS (চেহারা) আর JavaScript (আচরণ) আলাদা; বড় আকারে layered architecture (পর্ব ৩৮) আর MVC (পর্ব ৩৯).
SoC-এর ঘনিষ্ঠ আত্মীয় David Parnas-এর ১৯৭২ সালের information hiding: প্রতিটি module এমন একটি নকশা-সিদ্ধান্ত লুকিয়ে রাখবে, যা বদলাতে পারে (পর্ব ২৫-এ বিস্তারিত)। এখান থেকেই আসে একটি বহুল ব্যবহৃত পরামর্শ — encapsulate what varies: নকশার যে অংশ বদলাবে বলে মনে হয়, সেটিকে একটি স্থির interface-এর পেছনে আলাদা করুন। ১৯৯৪ সালের Gang of Four-এর বই (Design Patterns, Gamma, Helm, Johnson ও Vlissides) প্রায় এই ভাষাতেই বলে — নকশায় কী বদলাতে পারে ভাবুন, আর সেই বদলানো ধারণাটিকে encapsulate করুন। মিষ্টির দোকানে ছাড়ের নিয়ম বারবার বদলায় (ঈদ, পয়লা বৈশাখ, পূজা) — তাই §২-এ সেটিকে pricing.js-এ আলাদা করা শুধু DRY নয়, encapsulate what varies-ও।
একই বইয়ের আরেকটি বিখ্যাত পরামর্শ: “Favor object composition over class inheritance” — composition over inheritance. Inheritance child class-কে parent-এর ভেতরের খুঁটিনাটির সাথে বাঁধে — সবচেয়ে শক্ত coupling-গুলোর একটি। Composition-এ একটি object অন্য object-কে নিজের অংশ হিসেবে রেখে তার interface দিয়ে কাজ করায়, তাই অংশটি বদলানো যায়। যেমন EidOrder extends Order, BoishakhOrder extends Order-এর বদলে Order-এর ভেতরে একটি বদলযোগ্য ছাড়ের নিয়ম। কীভাবে আর কখন inheritance-ই ঠিক — পর্ব ৩৭-এ; UML-এ এই সম্পর্কগুলোর চিহ্ন পর্ব ২৯-এ (UML class diagram).
SoC জিজ্ঞেস করে “এই অংশের দায় কী?”; information hiding আর encapsulate what varies জিজ্ঞেস করে “কোন সিদ্ধান্ত বদলাতে পারে, আর সেটি কে জানে?” দুটির ফল একই — একটি বদল একটি জায়গায় আটকে থাকে। পর্ব ২৭-এর single responsibility principle এই চিন্তারই একটি নির্দিষ্ট রূপ: একটি module-এর বদলানোর কারণ একটি।
৬ · Law of Demeter — শুধু কাছের বন্ধুর সাথে কথা
Law of Demeter এসেছে যুক্তরাষ্ট্রের Northeastern University-র Demeter নামের একটি গবেষণা project থেকে, ১৯৮৭ সালের দিকে। নীতিটি সাধারণত Ian Holland-এর প্রস্তাব বলে উল্লেখ করা হয়, আর Karl Lieberherr ও Holland ১৯৮৮–৮৯ সালের কয়েকটি লেখায় (OOPSLA ১৯৮৮-এ Riel-সহ, আর IEEE Software ১৯৮৯) এটি প্রকাশ করেন। এর আরেক নাম principle of least knowledge. ছোট করে: “শুধু আপনার কাছের বন্ধুদের সাথে কথা বলুন” (talk only to your immediate friends).
Object-oriented ভাষায় নিয়মটি হলো: একটি method m শুধু এদের method ডাকতে পারে —
- নিজের object (
this/self); m-এর parameter হিসেবে আসা object;m-এর ভেতরে তৈরি করা object;- নিজের সরাসরি অংশ (field) হিসেবে রাখা object.
যা নিষেধ: বন্ধুর কাছ থেকে পাওয়া object-এর ভেতরে আবার ঢোকা — “বন্ধুর বন্ধু”। order.customer.wallet.balance -= x লাইনটি checkout কোডকে চারটি জিনিস জানতে বাধ্য করে: order-এ customer আছে, customer-এ wallet আছে, wallet-এ balance নামে একটি সংখ্যা আছে, আর সেটি সরাসরি বদলানো যায়। এমন লম্বা শিকলকে বলা হয় train wreck — রেলের বগির মতো একটির পর একটি বিন্দু, আর একটি বগি লাইনচ্যুত হলে পুরো ট্রেন।
দোকানে বিল দেওয়ার সময় আপনি দোকানিকে বলেন “৩৫০ টাকা দিচ্ছি” — আপনার পকেট থেকে মানিব্যাগ বের করে, তার ভেতরের ভাঁজ খুলে, দোকানি নিজে টাকা গুনে নেন না। যদি নিতেন, তাহলে আপনি মানিব্যাগ বদলে bKash-এ দিতে চাইলে দোকানির পুরো পদ্ধতি ভেঙে যেত। দোকানি শুধু আপনাকে চেনেন; টাকা কোথায় রাখেন, সেটি আপনার ব্যাপার।
নিচের প্রোগ্রামে Wallet-এর দুটি version: প্রথমটি টাকা রাখে balance field-এ, দ্বিতীয়টি (ভালো কারণে — দশমিকের গোলমাল এড়াতে) পয়সায় পূর্ণসংখ্যা রাখে, field-এর নাম paisa. দুই রকম checkout কোড একই কাজ করার চেষ্টা করে।
// Law of Demeter: reaching through objects vs. telling the object.
class Wallet { // version 1: keeps taka in "balance"
constructor(tk) { this.balance = tk; }
debit(tk) { if (tk > this.balance) throw new Error("insufficient balance");
this.balance -= tk; }
get taka() { return this.balance; }
}
class WalletV2 { // version 2: integer paisa, field renamed
constructor(tk) { this.paisa = tk * 100; }
debit(tk) { const p = Math.round(tk * 100);
if (p > this.paisa) throw new Error("insufficient balance");
this.paisa -= p; }
get taka() { return this.paisa / 100; }
}
class Customer {
constructor(name, wallet) { this.name = name; this.wallet = wallet; }
pay(tk) { this.wallet.debit(tk); } // the customer talks to its own wallet
}
// Caller A: a "train wreck" -- it knows Order -> Customer -> Wallet -> balance
function checkoutChain(order) { order.customer.wallet.balance -= order.total; }
// Caller B: tell, don't ask -- it only knows that a customer can pay
function checkoutTell(order) { order.customer.pay(order.total); }
for (const W of [Wallet, WalletV2]) {
const a = { total: 350, customer: new Customer("Rina", new W(1000)) };
const b = { total: 350, customer: new Customer("Rina", new W(1000)) };
checkoutChain(a); checkoutTell(b);
console.log(W.name + ": chain -> " + a.customer.wallet.taka + " tk, tell -> " +
b.customer.wallet.taka + " tk");
}
const big = { total: 1500, customer: new Customer("Rina", new Wallet(1000)) };
checkoutChain(big);
console.log("chain, 1500 tk order: wallet = " + big.customer.wallet.taka + " tk");
try { checkoutTell({ total: 1500, customer: new Customer("Rina", new Wallet(1000)) }); }
catch (e) { console.log("tell, 1500 tk order: " + e.message); }আউটপুট:
Wallet: chain -> 650 tk, tell -> 650 tk
WalletV2: chain -> 1000 tk, tell -> 650 tk
chain, 1500 tk order: wallet = -500 tk
tell, 1500 tk order: insufficient balance
প্রথম version-এ দুটি পথই ঠিক: ১০০০ − ৩৫০ = ৬৫০. Wallet-এর ভেতরটা বদলানোর পরে (WalletV2) tell-পথ তখনো ৬৫০ দেখায়, কারণ সে শুধু pay() জানে। কিন্তু chain-পথে wallet-এ টাকা কাটেইনি — ১০০০ রয়ে গেছে! JavaScript-এ balance field আর নেই, তাই undefined - 350 হয় NaN, আর সেটি একটি নতুন অপ্রয়োজনীয় field-এ বসে যায়; কোনো error নেই, গ্রাহক বিনা পয়সায় মিষ্টি পেলেন। এটাই train wreck-এর আসল বিপদ: ভাঙন নীরব, আর ভাঙে এমন file-এ যা Wallet-এর লেখক কখনো দেখেননি। শেষ দুই লাইন: chain-পথ Wallet-এর নিজের নিয়ম এড়িয়ে −৫০০ বানায়, আর tell-পথে নিয়মটি নিজের জায়গায় থেকে ভুলটি থামায়।
Tell, don't ask
Law of Demeter-এর হাতে-কলমে রূপ হলো tell, don't ask — object-কে প্রশ্ন করে তার data বের করে বাইরে সিদ্ধান্ত নেবেন না; object-কে বলুন কী করতে হবে, সিদ্ধান্ত সে নিক। ধারণাটি Andy Hunt ও Dave Thomas-এর একটি পত্রিকা-কলামের মাধ্যমে (IEEE Software, ২০০৩ সালের দিকে) আর পরে Martin Fowler-এর লেখায় পরিচিত হয়। এতে data আর তার নিয়ম একসাথে থাকে — cohesion বাড়ে, coupling কমে। নিচের stepper-এ একটি train wreck ধাপে ধাপে refactor হচ্ছে।
if (order.customer.wallet.balance >= order.total) {
order.customer.wallet.balance -= order.total;
order.customer.wallet.history.push(order.id);
} else {
showError("টাকা কম");
}
class Wallet {
debit(tk, ref) { // নিয়ম এখন নিজের ঘরে
if (tk > this.balance) throw new Error("insufficient");
this.balance -= tk;
this.history.push(ref);
}
}
try {
order.customer.wallet.debit(order.total, order.id);
} catch (e) {
showError("টাকা কম");
}
class Customer {
pay(tk, ref) { this.wallet.debit(tk, ref); }
}
// checkout.js
order.customer.pay(order.total, order.id);
class Order {
checkout() { this.customer.pay(this.total, this.id); }
}
// checkout.js
order.checkout();
| Checkout যা জানে | |
| Wallet-এর ভেতর বদলালে ভাঙে | |
| “টাকা কম” নিয়ম কোথায় |
- Train wreck. Checkout কোড order থেকে customer, customer থেকে wallet, wallet থেকে balance আর history — পাঁচটি জিনিস জানে, আর “টাকা কম কি না” নিয়মটিও নিজেই প্রয়োগ করে। তিনটি checkout থাকলে নিয়মটিও তিনবার — Law of Demeter আর DRY দুটিই ভাঙছে।
- প্রশ্ন: নিয়মটি কার? Balance যাচাই আর কাটা wallet-এর নিজের দায়। তাই আচরণটি Wallet-এর একটি method
debit()-এ নেওয়া হলো। Checkout এখনো বদলায়নি, শিকল রয়ে গেছে — refactoring এক ধাপে একটি জিনিস। - Caller-এ debit ব্যবহার। Checkout এখন
order.customer.wallet.debit()ডাকে। নিয়ম আর এক জায়গায়, balance-এর নাম বা ধরন বদলালে আর কিছু ভাঙবে না। তবু checkout জানে যে customer-এর একটি wallet আছে — এখনো দুই লাফ। - Customer-কে বলা।
Customer.pay()নিজের wallet-কে কাজ দেয়। কাল মোবাইল ব্যাংকিং বা card এলেও সেই সিদ্ধান্ত Customer-এর ভেতরে; checkout কিছুই জানবে না। - Order-কে বলা — আর কোথায় থামবেন।
order.checkout()— checkout কোড এখন শুধু Order চেনে। সাবধান: প্রতিটি লাফ সরাতে একটি ছোট delegation method লাগে; এমন ডজনখানেক জমলে কোড আবার জটিল হয় — তখন থামুন। এখানে থামা যুক্তিসঙ্গত, কারণ checkout সত্যিই order-এর আচরণ।
কখন শিকল ঠিক আছে — fluent API, builder আর stream
Law of Demeter-কে “এক লাইনে একটির বেশি বিন্দু নয়” হিসেবে গুনলে ভুল হবে। প্রশ্নটি বিন্দু গোনার নয়, আপনি কি অন্য object-এর ভেতরের গঠনের ওপর নির্ভর করছেন? নিচের শিকলগুলো সাধারণত ঠিক আছে:
- Fluent interface আর builder:
new OrderBuilder().item("rasgulla", 2).address("Mirpur").build()— প্রতিটি method একই builder ফেরত দেয়; আপনি একটিই বন্ধুর সাথে কথা বলছেন, কারও ভেতরে ঢুকছেন না। Builder pattern আসবে পর্ব ৩৩-এ। - Stream আর collection pipeline:
orders.filter(o => o.paid).map(o => o.total).reduce(sum)— প্রতিটি ধাপ একটি নতুন মান ফেরত দেয়, ভেতরের গঠন নয়। JavaScript-এর array method আর Java-র stream এই ধরনের। - সরল data structure: JSON config বা DTO-র মতো যে জিনিসের কাজই data ধরে রাখা, তার field পড়া (
config.db.host) সাধারণত সমস্যা নয়; Robert C. Martin-এর Clean Code (২০০৮) এই পার্থক্য করে — Law of Demeter object-এর জন্য, নিছক data structure-এর জন্য নয়।
৭ · Least astonishment, fail fast আর command–query separation
Principle of least astonishment — কেউ যেন অবাক না হন
Principle of least astonishment (POLA, বা least surprise) বলে: একটি function, API বা screen এমনভাবে আচরণ করবে, যা তার ব্যবহারকারী তার নাম, প্রসঙ্গ আর প্রচলিত রীতি দেখে আশা করেন। ধারণাটি অন্তত ১৯৭০-এর দশক থেকে programming language আর interface নকশার আলোচনায় দেখা যায়; প্রথম উৎস নিয়ে নিশ্চিত দাবি করা কঠিন। উদাহরণ: getTotal() নামের function-এর database-এ কিছু লেখা উচিত নয়; sort() যদি মূল list বদলায়, নামেই তা বোঝা উচিত; একটি “বাতিল” button কখনো অর্ডার নিশ্চিত করবে না। যে আচরণে অবাক হতে হয়, সেখানেই bug জন্মায় — কারণ পরের developer যা ভেবেছেন, কোড তা করে না।
Fail fast — ভুল যত আগে, তত সস্তা
Fail fast মানে ভুল অবস্থা দেখামাত্র স্পষ্টভাবে থেমে যাওয়া বা জানানো, চুপচাপ ভুল মান নিয়ে এগিয়ে যাওয়া নয়। Jim Shore-এর ২০০৪ সালের IEEE Software-এর একটি লেখা (“Fail Fast”) এই ধারণাকে জনপ্রিয় করে। উদাহরণ: অর্ডারের পরিমাণ ঋণাত্মক হলে function-এর শুরুতেই exception তোলা; program শুরুর সময়ে config-এ SMS-এর চাবি না থাকলে তখনই থেমে যাওয়া, প্রথম গ্রাহকের অর্ডারে গিয়ে নয়। §৬-এর প্রোগ্রামে NaN চুপচাপ একটি field-এ বসে গেছিল — এটি fail fast-এর ঠিক উল্টো, “fail silently”. মনে রাখুন, fail fast ডেভেলপারের জন্য; ব্যবহারকারীর সামনে আপনি সুন্দর বার্তা দেখাবেন, কিন্তু ভেতরে ভুলটি গোপন করবেন না। Contract আর error handling-এর বিস্তারিত নকশা পর্ব ৩১-এ (interface আর contract).
Command–query separation — প্রশ্ন করলে উত্তর বদলায় না
Bertrand Meyer তাঁর Object-Oriented Software Construction (১৯৮৮) বইয়ে command–query separation (CQS) প্রস্তাব করেন: প্রতিটি method হয় একটি command (অবস্থা বদলায়, কিছু ফেরত দেয় না), নয়তো একটি query (উত্তর দেয়, কোনো side effect নেই) — দুটি একসাথে নয়। প্রচলিত ভাষায়: “প্রশ্ন করলে উত্তর বদলানো উচিত নয়।” কেন জরুরি, তা দেখুন — একটি loyalty card-এর “getter” গোপনে মাসিক fee কাটে।
# Command-query separation (Meyer): a query answers, a command changes state.
# Fictional sweet-shop loyalty card. get_points() looks like a query...
class LoyaltyCardBad:
def __init__(self, points):
self.points = points
def get_points(self): # ...but it also charges a 10-point fee
self.points -= 10
return self.points
card = LoyaltyCardBad(100)
print("log:", card.get_points()) # a harmless-looking log line
if card.get_points() >= 85:
print("free sweet")
else:
print("no free sweet, points =", card.get_points())
# Fixed: the query has no side effect; the change is an explicit command.
class LoyaltyCard:
def __init__(self, points):
self._points = points
def points(self): # query: safe to call any number of times
return self._points
def charge_monthly_fee(self): # command: changes state, returns nothing
self._points -= 10
card = LoyaltyCard(100)
card.charge_monthly_fee() # done once, on purpose
print("log:", card.points())
print("free sweet" if card.points() >= 85 else "no free sweet")
print("points after all checks:", card.points())আউটপুট:
log: 90
no free sweet, points = 70
log: 90
free sweet
points after all checks: 90
গ্রাহকের ১০০ point; এক মাসের fee ১০, তাই আসল অবস্থা ৯০ — ৮৫-এর বেশি, ফ্রি মিষ্টি পাওয়ার কথা। খারাপ version-এ লগের লাইনটি প্রথম fee কাটল (৯০), শর্ত যাচাইয়ে আবার কাটল (৮০ < ৮৫, তাই “no free sweet”), আর error বার্তা ছাপাতে গিয়ে তৃতীয়বার (৭০)। একটি নিরীহ log লাইন যোগ করায় ব্যবসার আচরণ বদলে গেল — debugging-এর সবচেয়ে কঠিন ধরনের bug, কারণ লগ যোগ করলে bug বদলায়। ঠিক version-এ fee কাটা একটি স্পষ্ট command, একবারই; query যতবার খুশি ডাকা যায়, উত্তর সবসময় ৯০.
CQS-এর কিছু স্বীকৃত ব্যতিক্রম আছে: stack-এর pop() (উপাদান সরায় আর ফেরত দেয়), iterator-এর next(), বা একাধিক thread-এর জন্য পড়া আর বাড়ানো একসাথে করা atomic কাজ — এরা ভালো কারণে নিয়ম ভাঙে, আর নামেই তা বোঝা যায়। একই ধারণা system-এর মাপে বড় হলে CQRS (command query responsibility segregation) — লেখা আর পড়ার জন্য আলাদা model; তার আলোচনা System Design কোর্সের event sourcing আর CQRS পর্বে।
৮ · নীতি যখন একে অপরের বিরুদ্ধে — আর over-engineering
প্রতিটি নীতি একা ভালো; একসাথে টানাটানি করে। এটাই নকশার আসল কাজ — নীতি মুখস্থ করা নয়, টানাপোড়েনে যুক্তি দিয়ে সিদ্ধান্ত নেওয়া।
| টানাপোড়েন | কী ঘটে | সিদ্ধান্তের প্রশ্ন |
|---|---|---|
| DRY বনাম decoupling | দুটি service-এর একই helper একটি shared library-তে নিলে দুটি service সেই library-র version-এ বাঁধা পড়ে; একজনের বদল অন্যের release আটকায় | কপি দুটি কি সত্যিই একই জ্ঞান, যা একসাথে বদলাতেই হবে? না হলে কপি-ই কম coupling |
| KISS বনাম নমনীয়তা | সরল if দিয়ে দুটি payment পদ্ধতি; কেউ বলেন “plugin architecture বানাই” | তৃতীয় পদ্ধতির প্রমাণ আছে? পরে বদলানো কি সস্তা থাকবে? (§৪-এর হিসাব) |
| Law of Demeter বনাম KISS | প্রতিটি লাফ সরাতে delegation method, class-গুলো wrapper-এ ফুলে যায় | Caller কি সত্যিই ভেতরের গঠন জানার দায় নিচ্ছে, নাকি এটি নিছক data structure? |
| YAGNI বনাম পরে-কঠিন সিদ্ধান্ত | Data-র গঠন, public API, security-র কাঠামো পরে বদলানো খুব দামি | এখানে LATE বড় — ছোট, সস্তা seam আগেই রাখুন (Money type-এর মতো) |
Over-engineering — নীতির অতিরিক্ত প্রয়োগ
নীতির উল্টো দিকের রোগ হলো over-engineering: সমস্যার চেয়ে বড় সমাধান। কয়েকটি পরিচিত চেহারা চিনে রাখুন:
- Speculative generality: “পরে লাগতে পারে” ভেবে বানানো abstract class, অব্যবহৃত parameter, একটিমাত্র implementation-ওয়ালা interface. Fowler ও Beck-এর code smell-এর তালিকায় এর নাম আছে (পর্ব ৪২).
- Premature abstraction: প্রথম উদাহরণেই generic framework — AHA-র ঠিক উল্টো।
- Pattern-জ্বর: সব জায়গায় design pattern বসানো, কারণ সদ্য শিখেছেন। Pattern সমস্যার সমাধান, সাজসজ্জা নয় — পর্ব ৩২-এ “কখন pattern নয়” আলোচনা আছে।
- Architecture astronaut: Joel Spolsky-র ২০০১ সালের একটি লেখার রসিক নাম — যাঁরা এত উঁচু স্তরের abstraction-এ ওঠেন যে আসল সমস্যা আর দেখা যায় না।
উল্টো রোগও আছে — under-engineering: হাজার লাইনের function আর সর্বত্র কপি-পেস্ট। মাঝের পথ: আজকের চাহিদা সবচেয়ে সরলভাবে মেটান, কিন্তু এমন পরিষ্কারভাবে যাতে কাল বদলানো সস্তা হয়; বদলের আসল প্রমাণ এলে তখন refactor করুন। Kent Beck-এর “simple design”-এর প্রচলিত চারটি নিয়মও এই কথা বলে: test পাস করে, উদ্দেশ্য প্রকাশ করে, duplication নেই, আর যত কম অংশ সম্ভব।
আরও গভীরে: DRY-এর মূল উৎস Hunt ও Thomas-এর The Pragmatic Programmer (১৯৯৯; ২০তম বার্ষিকী সংস্করণ ২০১৯), Law of Demeter-এর জন্য Lieberherr ও Holland-এর IEEE Software (১৯৮৯) লেখা, CQS-এর জন্য Meyer-এর Object-Oriented Software Construction (১৯৮৮; দ্বিতীয় সংস্করণ ১৯৯৭), আর Parnas-এর ১৯৭২ সালের CACM লেখা “On the criteria to be used in decomposing systems into modules”. ISO/IEC 25010-এর maintainability-র উপ-গুণ — modularity, reusability, analysability, modifiability, testability (পর্ব ০৩) — এই নীতিগুলোর ফল মাপার একটি আনুষ্ঠানিক কাঠামো দেয়। John Ousterhout-এর A Philosophy of Software Design (২০১৮) “গভীর module”-এর ধারণা দিয়ে এগুলোর অনেককে নতুন আলোয় দেখায়।
পরীক্ষা ও ইন্টারভিউতে: এই প্রশ্নগুলো প্রায়ই আসে —
- DRY কি কোডের লাইন কপি করা নিষেধ? — না; এটি জ্ঞানের একটি কর্তৃত্বপূর্ণ উৎসের কথা। দেখতে-এক কিন্তু আলাদা কারণে বদলায় এমন কোড আলাদা থাকাই ভালো।
- YAGNI আর KISS-এর পার্থক্য? — YAGNI বলে কী বানাবেন না (আগাম feature); KISS বলে যা বানাবেন তা কীভাবে (সরলতম উপায়ে)।
- Law of Demeter কী, আর
a.getB().getC().doIt()কেন সমস্যা? — শুধু কাছের বন্ধুর method ডাকুন; শিকলটি caller-কে B আর C-এর গঠনের সাথে বাঁধে। - Command–query separation কী? — method হয় অবস্থা বদলায় (command), নয় উত্তর দেয় (query), দুটি একসাথে নয়;
pop()একটি স্বীকৃত ব্যতিক্রম। - Composition over inheritance কেন? — inheritance child-কে parent-এর ভেতরের খুঁটিনাটিতে বাঁধে; composition-এ অংশ বদলানো যায়, coupling কম।
৯ · AI-এর যুগে নকশার নীতি
AI coding সহকারী নকশার নীতিগুলোর নাম খুব ভালো জানে, কিন্তু নাম জানা আর প্রসঙ্গ বুঝে প্রয়োগ করা আলাদা। আপনার ব্যবসার নিয়ম কে বদলান, কোন অংশ আগামী মাসে বদলাবে, কোন দুটি কোড কাকতালীয়ভাবে মিলেছে — AI তা জানে না।
| AI-এর সাধারণ ভুল | কীভাবে ধরবেন |
|---|---|
| দেখতে-এক সব কোড জুড়ে “DRY” refactor — কাকতালীয় duplication-ও | প্রতিটি জোড়ার জন্য জিজ্ঞেস করুন: একই মানুষ, একই কারণে বদলান? নতুন shared function-এ flag parameter এলে সতর্ক হোন |
| অতিরিক্ত pattern আর abstraction: একটিমাত্র class-এর জন্য factory, interface, strategy | প্রতিটি নতুন abstraction-এর আজকের দ্বিতীয় ব্যবহারকারী দেখাতে বলুন; না থাকলে বাদ |
| Train-wreck শিকল আর side effect-ওয়ালা getter | Review-তে a.b.c.d ধরনের শিকল আর get… নামের method-এর ভেতরে লেখার কাজ খুঁজুন; query দুবার ডেকে test করুন |
| বানানো ইতিহাস: “KISS Dijkstra-র”, “DRY Martin Fowler-এর”, ভুল বছর | বই আর লেখকের নাম মূল উৎসে মিলিয়ে নিন; অনিশ্চিত হলে লেখায় সেভাবেই বলুন |
| Refactoring-এর পরে আচরণ বদলে যায়, অথচ AI বলে “শুধু গঠন বদলেছে” | আগে test, পরে refactor; test চালান, diff লাইন ধরে পড়ুন, আর mutation testing (পর্ব ৫০) দিয়ে test-এর জোর দেখুন |
AI দিয়ে নকশা review করাতে প্রসঙ্গ দিন আর অন্ধ প্রয়োগ নিষেধ করুন — আর কোড দেওয়ার আগে গ্রাহকের তথ্য, API key আর গোপন ব্যবসায়িক নিয়ম মুছে নিন:
AI-এর নকশা-প্রস্তাব হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১DRY-এর লেখকেরা নিজেরাই কেন বলেন যে নীতিটি সবচেয়ে বেশি ভুল বোঝা হয়েছে? “জ্ঞান” আর “কোডের লেখা”-র পার্থক্য বাস্তবে কীভাবে চিনবেন?
১৯৯৯ সালে The Pragmatic Programmer বেরোনোর পরে DRY দ্রুত জনপ্রিয় হয় — তিন অক্ষরের সহজ স্লোগান, মনে রাখা সহজ। কিন্তু স্লোগানটি পূর্ণ সংজ্ঞাটিকে ঢেকে দেয়। অনেকে একে পড়লেন “কোনো দুটি লাইন যেন এক না হয়”, আর code review-তে দুটি একই রকম for loop দেখলেই জোড়া লাগানোর দাবি উঠল। ২০১৯ সালের বার্ষিকী সংস্করণে Hunt ও Thomas স্পষ্টভাবে লেখেন যে DRY জ্ঞান আর উদ্দেশ্যের পুনরাবৃত্তি নিয়ে; দেখতে এক রকম দুটি কোড একই জ্ঞান প্রকাশ না-ও করতে পারে, আর দেখতে আলাদা দুটি কোড একই জ্ঞান প্রকাশ করতে পারে।
এই দ্বিতীয় দিকটি সহজে চোখ এড়ায়। §২-এর প্রোগ্রামে cart আর invoice-এর কোড আলাদা ভাবে লেখা — একটিতে amount * 0.88, অন্যটিতে amount - amount * 0.12 — তবু একই নিয়ম। আরেকটি উদাহরণ: database-এ CHECK (quantity > 0) আর JavaScript-এ if (qty <= 0) throw — দুই ভাষা, একই জ্ঞান। আবার comment-এ “১০% ছাড়” আর কোডে ১২% — এটিও duplication, আর এই ধরনের duplication-ই সবার আগে মিথ্যা হয়।
বাস্তবে চেনার কয়েকটি প্রশ্ন:
- কে বদলান? দুটি জায়গা যদি একই মানুষ বা দলের সিদ্ধান্তে বদলায় (মার্কেটিং, আইন, delivery ব্যবস্থাপক), তবে সম্ভবত একই জ্ঞান।
- একসাথে বদলাতেই হবে? একটি বদলে অন্যটি না বদলালে কি bug হবে? হলে একই জ্ঞান।
- ইতিহাস দেখুন:
git logদিয়ে দেখুন অতীতে দুটি জায়গা একসাথে বদলেছে কি না। একসাথে বদলানোর অভ্যাস একই জ্ঞানের শক্ত প্রমাণ।
শেষ পরামর্শটির পেছনে একটি পরিচিত ধারণা আছে — change coupling: version control-এর history-তে যে file-গুলো বারবার একসাথে বদলায়, সেগুলো প্রায়ই লুকানো জ্ঞান ভাগ করে।
মূল উপলব্ধি: DRY-এর একক হলো জ্ঞান, লাইন নয়; সঠিক প্রশ্ন “এগুলো কি দেখতে এক?” নয়, “এগুলো কি একই কারণে, একসাথে বদলাতে বাধ্য?”
প্র ০২Ariane 5 আর Knight Capital-এর মতো ঘটনা কি YAGNI-র পক্ষে প্রমাণ? অব্যবহৃত বা অপ্রয়োজনীয় কোডের “বহনের খরচ” কীভাবে বিপর্যয়ে পরিণত হয়?
দুটি ঘটনাই সরাসরি “আগাম feature” নয়, তবে দুটিই দেখায় যে কোডে থাকা প্রতিটি অংশের একটি ঝুঁকি আছে, এমনকি সেটি কোনো কাজে না লাগলেও।
Ariane 5 (১৯৯৬): তদন্ত কমিটির প্রকাশিত প্রতিবেদন অনুযায়ী, রকেটের inertial reference system-এর software বড় অংশে Ariane 4 থেকে নেওয়া। তার একটি alignment-এর হিসাব উড্ডয়নের আগে দরকারি ছিল, উড্ডয়নের পরে Ariane 5-এ এর কোনো কাজ ছিল না — তবু এটি উড্ডয়নের পরেও কিছুক্ষণ চলত। Ariane 5-এর বেশি আনুভূমিক গতিতে একটি সংখ্যা রূপান্তরে overflow হলো, error সামলানো হয়নি, system বন্ধ হলো, আর রকেট উড্ডয়নের প্রায় ৪০ সেকেন্ড পরে ধ্বংস হলো। প্রতিবেদনের অন্যতম সুপারিশ ছিল, দরকার নেই এমন কোনো কাজ উড্ডয়নের সময় চালানো উচিত নয়।
Knight Capital (২০১২): মার্কিন SEC-এর ২০১৩ সালের আদেশে বর্ণনা অনুযায়ী, বহু বছর ব্যবহার না হওয়া একটি পুরোনো trading কোড একটি server-এ রয়ে গিয়েছিল। নতুন feature-এর জন্য একটি পুরোনো flag পুনর্ব্যবহার করা হয়, আর যে server-এ নতুন কোড পৌঁছায়নি সেখানে flag-টি পুরোনো কোডকে জাগিয়ে তোলে। কয়েক মিনিটে বিপুল অনাকাঙ্ক্ষিত অর্ডার যায়, আর প্রতিষ্ঠানটি বিশাল আর্থিক ক্ষতিতে পড়ে (পর্ব ১৭-এ এর deployment-এর দিক দেখেছেন)।
শিক্ষাগুলো YAGNI-র “বহনের খরচ”-এর চরম রূপ:
- অব্যবহৃত কোড শুধু জায়গা নেয় না; এটি এমন আচরণ, যা কেউ আর পরীক্ষা করেন না, কিন্তু সঠিক পরিস্থিতিতে চলে।
- তাই YAGNI-র সঙ্গী অভ্যাস হলো মুছে ফেলা — অব্যবহৃত কোড আর flag পরিষ্কার রাখা, কারণ Git-এর history-তে সেটি তো থেকেই যাচ্ছে।
তবে সতর্কতা: একটি বা দুটি বিখ্যাত ঘটনা দিয়ে একটি নীতি “প্রমাণ” হয় না; দুটি ঘটনারই অনেক কারণ ছিল (test, deployment, error handling). এগুলো বরং দেখায় যে কোডের প্রতিটি অংশের খরচ শূন্য নয়।
মূল উপলব্ধি: যে কোড কাজে লাগে না, সেটিও ঝুঁকি বহন করে; YAGNI-র অর্থনীতিতে “বানিয়ে রাখি, ক্ষতি তো নেই” কথাটি মিথ্যা — আর অব্যবহৃত কোড মুছে ফেলা YAGNI-রই অর্ধেক।
প্র ০৩Law of Demeter-কে অনেকে “এক লাইনে একটি বিন্দু” নিয়ম বলেন। এটি কেন ভুল, আর jQuery-র মতো fluent API বা stream-এর লম্বা শিকল কেন সাধারণত সমস্যা নয়?
Lieberherr ও Holland-এর মূল সংজ্ঞাটি বিন্দু গোনে না; এটি বলে একটি method কোন object-গুলোর method ডাকতে পারে — নিজে, parameter, ভেতরে তৈরি করা object, আর নিজের সরাসরি অংশ। “একটি বিন্দু” একটি শর্টকাট, যা বেশিরভাগ সময় কাছাকাছি ফল দেয়, কিন্তু দুই দিকেই ভুল করে।
ভুল ইতিবাচক (শিকল আছে, সমস্যা নেই):
- jQuery-র ধাঁচের
$("#menu").addClass("open").fadeIn()— প্রতিটি method একই object ফেরত দেয়। আপনি একজন বন্ধুর সাথেই বারবার কথা বলছেন। Martin Fowler এই ধরনকে fluent interface বলেছেন। - Builder —
OrderBuilder().item(...).address(...).build()— একই ধরন। - Stream/pipeline —
orders.filter(...).map(...).reduce(...): প্রতিটি ধাপ একটি নতুন মান (array বা stream) দেয়; আপনি কারও ভেতরের গঠনে নির্ভর করছেন না, শুধু একটি স্থির, সর্বজনীন interface-এ।
ভুল নেতিবাচক (একটি বিন্দু, তবু সমস্যা): ধরুন const w = order.getWallet(); আর পরের লাইনে w.balance -= 350; — প্রতি লাইনে একটি বিন্দু, কিন্তু আপনি তবু order-এর বন্ধুর ভেতরে ঢুকে তার data বদলাচ্ছেন। শিকলটি শুধু দুই লাইনে ভাগ হয়েছে।
আসল প্রশ্নটি তাই: caller কি অন্য object-এর ভেতরের গঠনের ওপর নির্ভর করছে, যা সেই object-এর লেখক বদলাতে পারেন? §৬-এর প্রোগ্রামে এটাই ঘটেছিল: Wallet-এর লেখক field-এর নাম বদলালেন — তাঁর অধিকার আছে, কারণ এটি তাঁর ভেতরের সিদ্ধান্ত — আর অন্য file-এর কোড নীরবে ভাঙল। Robert C. Martin Clean Code-এ এই কারণেই object আর data structure আলাদা করেন: নিছক data structure (JSON config, DTO) নিজের গঠন প্রকাশ করার জন্যই তৈরি, তার field পড়া স্বাভাবিক — যদিও গঠন বদলালে caller-রা ভাঙবে, আর সেটি জেনেবুঝে মেনে নেওয়া।
মূল উপলব্ধি: Law of Demeter বিন্দু নয়, জ্ঞান গোনে — caller কতটা ভেতরের গঠন জানে; একই object বা নতুন মান ফেরত দেওয়া শিকল ঠিক আছে, অন্যের ভেতরে হাঁটা শিকল নয়।
প্র ০৪দুটি আলাদা দলের দুটি service একই address-যাচাইয়ের কোড ব্যবহার করে। একটি shared library বানানো কি DRY-এর সঠিক প্রয়োগ? Microservice-এর জগতে DRY কোথায় থামা উচিত?
একটি service-এর ভেতরে DRY প্রায় সবসময় ভালো। কিন্তু service-এর সীমানা পেরোলে হিসাব বদলায়, কারণ এখানে duplication-এর বিকল্প হলো দুই দলের মধ্যে coupling — যা সাধারণত আরও দামি।
Shared library বানালে কী হয়:
- Release বাঁধা পড়ে: order দলের একটি বদল (যেমন নতুন এলাকার নাম) library-র নতুন version চায়; delivery দলকে সেই version নিতে, test করতে আর deploy করতে হয় — তাদের নিজেদের কোনো দরকার না থাকলেও। না নিলে দুই service দুই version-এ চলে, তখন DRY-এর উদ্দেশ্যই ব্যর্থ।
- মালিকানা অস্পষ্ট হয়: “common-utils” কার? Conway's law (পর্ব ০১-এ দেখেছেন) মনে করিয়ে দেয়, যে কোড কোনো দলের নয়, সেটি সাধারণত কারও যত্ন পায় না।
Microservice নিয়ে লেখা অনেক বই ও বক্তৃতায় — যেমন Sam Newman-এর Building Microservices — এই পরামর্শ পাওয়া যায় যে service-এর ভেতরে DRY মানুন, কিন্তু service-এর মধ্যে কোড ভাগাভাগির ব্যাপারে খুব সাবধান থাকুন; যৌথ library থাকলে সেটি যেন ছোট, স্থির আর কোনো ব্যবসায়িক জ্ঞানহীন (যেমন logging) হয়।
সিদ্ধান্তের প্রশ্নটি আবার DRY-এর মূল সংজ্ঞায় ফেরে: address যাচাই কি একটি জ্ঞান? যদি আইনগত ঠিকানার নিয়ম (যেমন পোস্ট কোডের ধরন) সবার জন্য এক আর একসাথেই বদলাতে হয় — তবে হয়তো এটি একটি আলাদা “address service”-এর দায়, যাকে দুই service API দিয়ে জিজ্ঞেস করে; এতে জ্ঞান এক জায়গায় থাকে, কিন্তু কোড-library-র coupling থাকে না। আর যদি order দল শুধু “ঠিকানা খালি নয়” দেখে আর delivery দল দেখে “আমাদের এলাকার মধ্যে কি না” — তবে এগুলো আলাদা জ্ঞান, যা দেখতে মিলে গেছে; কপিই ঠিক।
মূল উপলব্ধি: DRY-এর খরচ হলো নির্ভরতা; দলের আর service-এর সীমানা পেরোলে সেই নির্ভরতা প্রায়ই duplication-এর চেয়ে দামি — তাই সেখানে জ্ঞান ভাগ করুন সেবা বা চুক্তি দিয়ে, কোড-library দিয়ে নয়।
প্র ০৫“সরল” আর “সহজ” কি একই? KISS মানতে গিয়ে কোনো দল কি আসলে জটিলতাই বাড়াতে পারে?
Clojure ভাষার স্রষ্টা Rich Hickey ২০১১ সালের একটি বহুল আলোচিত বক্তৃতায় (“Simple Made Easy”) এই দুটি শব্দ আলাদা করেন। তাঁর ব্যাখ্যায় simple (সরল) মানে কম জট — অংশগুলো একে অপরের সাথে কম জড়ানো, একটি জিনিস একটি কাজ করে; এটি জিনিসটির নিজের গুণ। easy (সহজ) মানে হাতের কাছে, পরিচিত — এটি ব্যবহারকারীর অবস্থার ওপর নির্ভর করে। যা আপনার কাছে সহজ (কারণ আপনি জানেন), তা হয়তো ভেতরে খুব জটিল।
এই পার্থক্য না বুঝলে KISS-এর নামে উল্টো কাজ হয়:
- “এক লাইনে হয়ে যায়” framework: একটি ছোট অর্ডার ফর্মের জন্য বিশাল framework আর পঞ্চাশটি dependency যোগ করা আজ সহজ, কিন্তু system আর সরল থাকে না — প্রতিটি dependency-র update, নিরাপত্তা-ত্রুটি আর অদ্ভুত আচরণ এখন আপনার সমস্যা।
- জটিলতা লুকানো, কমানো নয়: একটি function-এর ভেতরে সব ঝামেলা ঢুকিয়ে বাইরে সুন্দর নাম দিলে caller-এর কাছে সহজ হয়, কিন্তু function-টি যদি দশটি অদৃশ্য side effect করে, system জটিলই থাকে — §৭-এর CQS উদাহরণ মনে করুন।
উল্টো দিকে, সরলতা কখনো কখনো শুরুতে কঠিন। §৪-এর Money type বানাতে এক দিন লাগল — float দিয়ে চালিয়ে যাওয়া সহজ ছিল — কিন্তু পরে হিসাব অনেক সরল হলো। Brooks-এর ভাষায় (পর্ব ০২), essential complexity থেকে যাবেই; আমাদের লক্ষ্য accidental complexity কমানো, আর সেটি প্রায়ই চিন্তা আর সময় দাবি করে।
তাই KISS প্রয়োগের ভালো প্রশ্ন “কোনটি লেখা সবচেয়ে দ্রুত?” নয়, বরং “ছয় মাস পরে একজন নতুন সহকর্মী কোনটি সবচেয়ে কম অংশ আর কম জট নিয়ে বুঝবেন ও বদলাতে পারবেন?”
মূল উপলব্ধি: KISS-এর লক্ষ্য সরলতা — কম জট — পরিচিতি বা সুবিধা নয়; সহজ পথ প্রায়ই জটিলতা বাড়ায়, আর আসল সরলতা অনেক সময় শুরুতে বেশি চিন্তা চায়।
অনুশীলন
হিসাব, নীতি চেনা, আউটপুট অনুমান আর সমালোচনা — চারটিই পরীক্ষা ও ইন্টারভিউর ধরনের।
-
YAGNI-র হিসাব: একটি কাল্পনিক ফার্মেসি app-এ একজন developer “পরে লাগতে পারে” ভেবে ওষুধ ফেরতের (return) ব্যবস্থা এখনই বানাতে চান। অনুমান: এখন বানাতে ৮ দিন; কোডে থাকলে প্রতি মাসে ০.৫ দিন বহনের খরচ; সময়সীমা ১২ মাস; দরকার হলে সেটি ১ থেকে ১২-র যেকোনো মাসে সমান সম্ভাবনায় আসবে; আগাম বানানো রূপটি ভুল হওয়ার সম্ভাবনা ২৫%, তখন ৪ দিনের মেরামত; পরে বানালে ১৪ দিন, আর বহনের খরচ শুধু দরকারের মাস থেকে ১২ পর্যন্ত। দরকারের সম্ভাবনা p = ০.৬ হলে দুই পথের প্রত্যাশিত খরচ কত, আর কোনটি বেছে নেবেন? সমান খরচের p কত?
এখন বানালে: ৮ + ০.৫ × ১২ + p × ০.২৫ × ৪ = ৮ + ৬ + ০.৬ × ১ = ১৪.৬ দিন. (দরকার না হলেও বানানো আর বারো মাসের বহন খরচ দিতেই হবে।)
পরে বানালে: দরকারের মাস ১–১২ সমান সম্ভাবনায়, তাই বাকি মাসের গড় = (১২ − ১) ÷ ২ = ৫.৫; খরচ = p × (১৪ + ০.৫ × ৫.৫) = ০.৬ × ১৬.৭৫ = ১০.০৫ দিন. অপেক্ষা প্রায় সাড়ে চার দিন সস্তা — YAGNI.
সমান খরচের বিন্দু: ১৪ + ১ × p = ১৬.৭৫ × p → p = ১৪ ÷ (১৬.৭৫ − ১) = ১৪ ÷ ১৫.৭৫ ≈ ০.৮৯. অর্থাৎ দরকার প্রায় নিশ্চিত (৮৯%-এর বেশি) না হলে এই অনুমানে অপেক্ষাই ভালো। তবে “পরে ১৪ দিন” অনেক বেশি হলে (যেমন লেনদেনের data-র গঠন বদলাতে হবে) সিদ্ধান্ত বদলাতে পারে; তখন অন্তত একটি সস্তা seam আগে রাখুন।
-
কোন নীতি ভাঙা হয়েছে? প্রতিটি পরিস্থিতির জন্য প্রধান ভাঙা নীতির নাম বলুন (DRY, KISS, YAGNI, separation of concerns, Law of Demeter, least astonishment, fail fast, CQS) আর এক লাইনে সংশোধন। (ক)
isLoggedIn()ডাকলে session-এর মেয়াদ আরও ৩০ মিনিট বাড়ে। (খ) VAT-এর হার কোডে ৭টি file-এ লেখা। (গ)report.getShop().getOwner().getPhone().getNumber(). (ঘ) একটি function অর্ডার যাচাই করে, database-এ লেখে, PDF invoice বানায় আর email পাঠায়। (ঙ) একটিমাত্র SMS সরবরাহকারীর জন্যAbstractNotificationProviderFactory, plugin loader আর XML config. (চ) ঋণাত্মক পরিমাণের অর্ডার এলে function চুপচাপ ০ ধরে নিয়ে এগোয়, আর মাসের শেষে হিসাব মেলে না। (ছ) “Delete” নামের button আসলে item-টি archive করে, আর “Archive” button মুছে ফেলে।- (ক) CQS (আর least astonishment): নামে query, আসলে command.
isLoggedIn()শুধু উত্তর দেবে; মেয়াদ বাড়ানো আলাদাtouchSession(). - (খ) DRY: একটি জ্ঞান (সরকারি করের হার), সাত কপি। একটি config বা
tax.js-এ এক উৎস। - (গ) Law of Demeter: report দোকানের মালিকের ফোনের ভেতর পর্যন্ত জানে।
report.shopContactNumber()বা shop-কে জিজ্ঞেস করুন — অথবা report তৈরির সময়ই নম্বরটি parameter হিসেবে দিন। - (ঘ) Separation of concerns (SRP-এর দিকেও, পর্ব ২৭): চারটি দায় এক জায়গায়। যাচাই, সংরক্ষণ, invoice বানানো আর বার্তা পাঠানো আলাদা অংশে, একটি ছোট সমন্বয়কারী function সেগুলো ক্রমে ডাকে।
- (ঙ) YAGNI (আর KISS): দ্বিতীয় সরবরাহকারীর কোনো প্রমাণ নেই। একটি সরল
sendSms()function; দ্বিতীয়টি সত্যিই এলে তখন interface বের করুন। - (চ) Fail fast: ভুল ইনপুট চুপচাপ “ঠিক” করা হচ্ছে, ভুল ধরা পড়ছে অনেক পরে। শুরুতেই স্পষ্ট error তুলুন আর লগ করুন।
- (ছ) Least astonishment: নাম আর আচরণ উল্টো। নামগুলো আচরণের সাথে মেলান, আর মুছে ফেলার মতো অপরিবর্তনীয় কাজে নিশ্চিতকরণ চান।
- (ক) CQS (আর least astonishment): নামে query, আসলে command.
-
আউটপুট অনুমান করুন: একটি কাল্পনিক মিষ্টির দোকানে রসগোল্লার বাক্স বাকি আছে ৩টি। তিনজন গ্রাহক পরপর একটি করে বাক্স চান। চালানোর আগে চারটি লাইনের আউটপুট লিখুন, আর বলুন কোন নীতি ভেঙেছে আর কীভাবে ঠিক করবেন।
let stock = 3; // boxes of roshogolla left function inStock() { stock -= 1; return stock >= 0; } // looks like a query function placeOrder(name) { if (!inStock()) return name + ": sorry, sold out"; stock -= 1; // the real reservation return name + ": ok (stock " + stock + ")"; } for (const n of ["Rina", "Tanim", "Joy"]) console.log(placeOrder(n)); console.log("stock = " + stock);Rina: ok (stock 1) Tanim: ok (stock -1) Joy: sorry, sold out stock = -2কেন: Rina —
inStock()stock ৩ থেকে ২ করে, ২ ≥ ০ সত্য; তারপর আসল সংরক্ষণে ২ থেকে ১. Tanim —inStock()১ থেকে ০, ০ ≥ ০ সত্য; সংরক্ষণে −১ — ঋণাত্মক stock, অথচ অর্ডার “ok”. Joy —inStock()−১ থেকে −২, মিথ্যা, তাই “sold out”. শেষে stock −২. তিনটি বাক্স ছিল, অথচ দুজন পেলেন, তৃতীয়জন ফেরত গেলেন, আর হিসাবের খাতায় −২.ভাঙা নীতি: command–query separation (আর least astonishment) —
inStock()নামে প্রশ্ন, কিন্তু stock বদলায়। প্রতিটি অর্ডারে stock দুবার কমছে। সংশোধন:function inStock() { return stock > 0; }— শুধু প্রশ্ন; কমানো শুধু সংরক্ষণের command-এ। তাহলে আউটপুট হবে Rina: ok (stock 2), Tanim: ok (stock 1), Joy: ok (stock 0), stock = 0. (Fail fast-এর দিক থেকে: stock কখনো ঋণাত্মক হওয়া উচিত নয় — সংরক্ষণের command-এর ভেতরে এই শর্ত যাচাই করে error তোলা ভালো।) -
সমালোচনা করুন — দুটি নীতির টানাপোড়েন: একটি কাল্পনিক অনলাইন বাজার app-এ দুটি দল: order দল আর delivery দল, প্রতিটির আলাদা service আর আলাদা release-এর সময়। দুটি service-এই “ফোন নম্বর ঠিক কি না” যাচাইয়ের প্রায় একই ২০ লাইন কোড আছে। একজন developer প্রস্তাব দিলেন: “DRY মানতে হবে — একটি
common-utilslibrary বানাই, তাতে phone validation, তারিখের format, দামের হিসাব আর delivery-চার্জের নিয়ম রাখি; দুই service সেটি ব্যবহার করবে।” প্রস্তাবটির কোন অংশ যুক্তিসঙ্গত, কোন অংশ বিপজ্জনক? আপনার বিকল্প প্রস্তাব লিখুন।যুক্তিসঙ্গত অংশ: phone validation সম্ভবত সত্যিই একই জ্ঞান — দেশের ফোন নম্বরের নিয়ম দুই দলের জন্য এক, আর বদলালে (নতুন operator code) দুটিতেই বদলাতে হবে। তারিখের format-ও প্রায় ব্যবসায়িক জ্ঞানহীন, স্থির helper.
বিপজ্জনক অংশ:
- DRY বনাম decoupling: দামের হিসাব আর delivery-চার্জ দুটি আলাদা দলের ব্যবসায়িক জ্ঞান। একটি library-তে রাখলে order দলের দামের বদলে delivery দলকেও নতুন version নিতে, test করতে আর release করতে হবে — দুই দলের release-এর সময় বাঁধা পড়ে।
- Separation of concerns:
common-utilsনামটিই সতর্কসংকেত — এতে একটিই কারণ নেই, যেকোনো জিনিস জমতে পারে; cohesion কম, মালিক অস্পষ্ট। - Delivery-চার্জ আসলে delivery দলের নিজের নিয়ম — order service-এর সেটি জানার দরকার থাকলে delivery service-কে API দিয়ে জিজ্ঞেস করা উচিত (tell/ask-এর সীমানা), নিয়মটির কপি বা library নিয়ে নয়।
বিকল্প প্রস্তাব: (১) phone validation-এর জন্য একটি ছোট, স্থির, ভালোভাবে test করা library — শুধু এই এক কাজ, একটি নির্দিষ্ট মালিক দল, SemVer মেনে (পর্ব ১৮); অথবা বিশ লাইন হওয়ায় দুই জায়গায় কপি রেখে একই test-এর তালিকা ভাগ করা, যাতে দুটি আলাদা হলে ধরা পড়ে। (২) দাম order service-এর আর delivery-চার্জ delivery service-এর ভেতরেই থাকবে, প্রত্যেকটি এক উৎসে; অন্য service API দিয়ে জিজ্ঞেস করবে। (৩) সিদ্ধান্ত আর কারণ একটি ছোট নোটে (architecture decision record, পর্ব ৩৮) লিখে রাখা। সারকথা: জ্ঞান এক জায়গায় রাখুন, কিন্তু দলের সীমানা পেরিয়ে কোড ভাগ করার আগে নির্ভরতার দাম হিসাব করুন।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমকোডের কয়েক জায়গায় ছড়ানো একটি ব্যবসায়িক নিয়মকে নিরাপদে এক উৎসে আনার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 🔎 নিয়মটি কোডের কোথায় কোথায় আছে খুঁজে বের করা
- 🧭 প্রতিটি জায়গা একই জ্ঞান কি না যাচাই — কাকতালীয় মিলগুলো আলাদা রাখা
- 🧪 বর্তমান আচরণ test দিয়ে আটকানো
- 📌 নিয়মটির একটি নতুন একক উৎস তৈরি
- 🔁 একে একে caller বদলানো, প্রতিবার test চালিয়ে
- 🧹 পুরোনো কপিগুলো মুছে ফেলা
Hunt ও Thomas-এর The Pragmatic Programmer (১৯৯৯) অনুযায়ী DRY নীতির মূল বিষয় কী?
- কোনো দুটি কোডের লাইন যেন হুবহু এক না হয়
- প্রতিটি জ্ঞানের system-এ একটিই, দ্ব্যর্থহীন, কর্তৃত্বপূর্ণ রূপ থাকা
- প্রতিটি function ২০ লাইনের ছোট রাখা
- কোডে কোনো comment না লেখা
একটি app-এ “৮০০ টাকার বেশি অর্ডারে ১২% ছাড়” (মার্কেটিংয়ের নিয়ম) আর “৮০০ টাকার বেশি অর্ডারে delivery ফ্রি” (delivery ব্যবস্থাপকের নিয়ম) — দুটিতেই 800 সংখ্যাটি আছে। দুটিকে একটি THRESHOLD = 800-এ জোড়া লাগানো সম্পর্কে কোনটি সঠিক?
- ঠিক, কারণ DRY অনুযায়ী একই সংখ্যা দুবার লেখা যাবে না
- তৃতীয়বার 800 এলে জোড়া লাগানো বাধ্যতামূলক
- ভুল, কারণ এটি কাকতালীয় duplication; দুটি আলাদা কারণে বদলায়
- শুধু database-এ রাখলে ঠিক, কোডে রাখলে ভুল
নিচের কোনটি Law of Demeter ভাঙার সবচেয়ে স্পষ্ট উদাহরণ? (সব object ধরে নিন সাধারণ business object, নিছক data structure নয়।)
- order.customer.wallet.balance -= total
- new OrderBuilder().item("doi", 1).address("Uttara").build()
- orders.filter(o => o.paid).map(o => o.total)
- Order-এর ভেতরে this.customer.pay(this.total)
একটি loyalty card-এর get_points() method প্রতিবার ডাকলে ১০ point fee কেটে নতুন মান ফেরত দেয়। একটি program প্রথমে লগে get_points() ছাপে, তারপর get_points() >= 85 যাচাই করে। Card-এ শুরুতে ১০০ point থাকলে যাচাইয়ের সময় কত point দেখা যাবে, আর কোন নীতি ভাঙা হয়েছে?
- ৯০; DRY
- ১০০; KISS
- ৭০; Law of Demeter
- ৮০; command–query separation
একটি সরল cost model-এ আগাম feature এখন বানালে প্রত্যাশিত খরচ ১৩.৬ + ২.৪p দিন, আর অপেক্ষা করে দরকার হলে বানালে ২৬.৬৫p দিন (p = দরকারের সম্ভাবনা)। কোন p-এর বেশি হলে এখন বানানো সস্তা?
- প্রায় ০.৫১
- প্রায় ০.৩৪
- প্রায় ০.৫৬
- প্রায় ০.৮৯
একটি দলে মাত্র একটি SMS সরবরাহকারী আছে, আর অন্য কোনোটির পরিকল্পনা নেই। তবু একজন developer একটি abstract provider interface, একটি factory আর plugin loading বানালেন “যদি পরে লাগে”। এটি প্রধানত কোন নীতির বিরুদ্ধে?
- Law of Demeter
- YAGNI
- Fail fast
- Command–query separation
Gang of Four-এর Design Patterns (১৯৯৪) বইয়ের বিখ্যাত পরামর্শ “favor object composition over class inheritance”-এর মূল যুক্তি কী?
- Inheritance শক্ত coupling আনে; composition-এ অংশ বদলানো যায়
- Composition সবসময় দ্রুত চলে
- Inheritance সব object-oriented ভাষায় নিষিদ্ধ করা উচিত
- Composition-এ কোডের লাইন সবসময় কম লাগে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- সব নীতির লক্ষ্য এক: কম coupling, বেশি cohesion, সস্তা বদল। নীতি heuristic, আইন নয় — টানাপোড়েনে বিচার লাগে।
- DRY = জ্ঞানের একক উৎস (Hunt ও Thomas, ১৯৯৯), কোডের লেখা নয়। কাকতালীয় duplication আলাদা রাখুন; অনিশ্চিত হলে rule of three আর AHA — ভুল abstraction-এর চেয়ে duplication সস্তা।
- KISS = সরলতম যা কাজ করে (সরল ≠ সহজ); YAGNI = প্রমাণ এলে বানান — আগাম feature-এর খরচ বানানো + দেরি + বহন + মেরামত; পরে বানানো দামি হলে শুধু সস্তা seam রাখুন।
- Separation of concerns (Dijkstra, ১৯৭৪), information hiding আর encapsulate what varies; composition over inheritance (GoF, ১৯৯৪). Law of Demeter = শুধু কাছের বন্ধু; train wreck নয়, tell-don't-ask — তবে builder, stream আর data structure-এর শিকল ঠিক আছে।
- Least astonishment (নাম যা বলে, আচরণ তাই), fail fast (ভুল চুপচাপ গিলে ফেলবেন না), CQS (Meyer, ১৯৮৮: প্রশ্ন করলে উত্তর বদলায় না;
pop()-এর মতো স্বীকৃত ব্যতিক্রম)। Over-engineering-ও একটি রোগ।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২৭ · SOLID ১ — single responsibility আর open-closed principleপরবর্তী পর্বSeparation of concerns আর encapsulate what varies-এর object-oriented রূপ।
- পর্ব ২৫ · Modularity, coupling আর cohesion — ভালো নকশার মাপকাঠিআগের পর্বযে মাপকাঠিতে এই পর্বের সব নীতি মাপা হয়।
- পর্ব ৩১ · Interface আর contract — design by contract, error handling আর immutabilityএকই মডিউলFail fast আর CQS-এর আনুষ্ঠানিক রূপ — precondition, postcondition আর invariant.
- সফটওয়্যার ইঞ্জিনিয়ারিং ও Git · কোর্স হোম৭০টি পর্বপুরো কোর্সের রোডম্যাপ — মডিউল ৫ থেকে ৭ নকশা, pattern আর clean code.
- Python · Inheritance আর compositionপাশের ট্র্যাকComposition over inheritance — Python-এ হাতে-কলমে।
- System Design · Event sourcing আর CQRSপাশের ট্র্যাকCommand–query separation যখন পুরো system-এর মাপে।