Introduce Null Object: 'কিছু নেই' কে একটা ভদ্র প্রতিনিধি দাও
Introduce Null Object refactoring শেখো একটা school guardian card-এর গল্পের মাধ্যমে — Tony Hoare-এর billion-dollar mistake, ছড়িয়ে-ছিটিয়ে থাকা null check গুলো, আর কীভাবে একটা ভদ্র default object সব সামলে নেয়। আর কখন null object আসলে bug লুকিয়ে ফেলতে পারে সেটাও জানবে।
Guardian card যেটা কখনো খালি থাকে না
ধরো, ঢাকার একটা school-এ প্রতিটা student-এর জন্য একটা guardian card আছে — নাম, phone number, আর emergency-তে কে call পাবে। Office এই card সব জায়গায় ব্যবহার করে। Fee reminder guardian-এর কাছে যায়। Picnic consent form-এ guardian-এর নাম print হয়। Sports teacher রাহিম স্যার কেউ আঘাত পেলে guardian-কে call করেন।
এখন সমস্যার কথা। কিছু student আছে — hostel-এ থাকে, বা paperwork এখনো আসেনি — তাদের guardian listed নেই। পুরনো system-এ তাদের guardian slot টা ছিল blank। আর সেই blank-টা সব জায়গায় সমস্যা তৈরি করত। Fee reminder printer empty phone field দেখে জ্যাম হয়ে যেত। Consent form print হত "Dear ______" লিখে। আর একদিন রাহিম স্যার corridor-এ আঘাত পাওয়া রুবেলকে নিয়ে দাঁড়িয়ে — card-এ কিছু নেই, কাকে call করবেন বুঝতেই পারছেন না। প্রতিটা clerk, প্রতিটা teacher, প্রতিটা form-কে মনে রাখতে হত একই extra rule — আগে check করো guardian আছে কিনা, না থাকলে কিছু একটা করো। আর সবাই ভিন্ন ভিন্ন "কিছু একটা" বেছে নিত। Office clerk নাসরিন আপা লিখত "N/A"। Form printer জায়গা ফাঁকা রেখে দিত। রাহিম স্যার ঠায় দাঁড়িয়ে থাকতেন।
নতুন headmistress, ফাতেমা ম্যাডাম, একটা সহজ কিন্তু brilliant সমাধান বের করলেন। কোনো student-এর guardian slot আর blank থাকতে পারবে না। যদি guardian listed না থাকে, office একটা standard "School Office Contact" card ঢুকিয়ে দেবে — নাম হবে "School Office", phone হবে front desk-এর number, আর instruction থাকবে "Contact the admin office"। এখন printer সবসময় একটা number পায়। Consent form সবসময় একটা নাম পায়। রাহিম স্যার সবসময় জানেন কাকে call করতে হবে। কেউ আর blank check করে না, কারণ blank বলে কিছু আর নেই — 'না থাকা' টারই একটা ভদ্র card হয়ে গেছে।
এই card-টাই আজকের refactoring। Code-এ blank slot মানে null, chaos মানে ছড়িয়ে-ছিটিয়ে থাকা if (x === null) check, আর office contact card মানে Null Object — একটা real object যেটা "কিছু নেই" কে represent করে আর প্রতিটা প্রশ্নের উত্তর safe agreed default দিয়ে দেয়।
Introduce Null Object আসলে কী?
যখন কোনো field বা return value null হতে পারে, তখন প্রতিটা caller যে সেটা touch করে তার একটা hidden দায়িত্ব থাকে — আগে check করো, নইলে crash। একই defensive if codebase জুড়ে copy-paste হতে থাকে, আর — আরও খারাপ ব্যাপার — প্রতিটা copy missing case-এর জন্য নিজের মতো default বানিয়ে নেয়। তিনজন caller, "guardian নেই" মানে তিনটা আলাদা ধারণা। আর যে check তুমি ভুলে গেলে, সেটাই production-এ crash করে।
এই ব্যথার পেছনে একটা বিখ্যাত ইতিহাস আছে। Sir Tony Hoare ১৯৬৫ সালে ALGOL W-এর type system design করার সময় null reference বানিয়েছিলেন — তার নিজের কথায়, "শুধু এটা implement করাটা অনেক সহজ ছিল বলে।" ২০০৯ সালের একটা conference-এ তিনি publicly ক্ষমা চান, এটাকে তার "billion-dollar mistake" বলেন — দশকের পর দশক ধরে crash, vulnerability আর নষ্ট productivity মিলিয়ে industry-এর কোটি কোটি ডলার ক্ষতি হয়েছে। মূল সমস্যা হলো null কোনো interface মানে না। একটা real Guardian object name() call করলে নাম দেয়। null প্রতিটা message-এর উত্তর দেয় explosion দিয়ে।
Null Object pattern ভদ্রতা ফিরিয়ে আনে। Recipe হলো:
- Absence-এর জন্য একটা class বানাও —
NullGuardian(বাUnknownGuardian) — real class-এর মতোই same interface implement করে। - Agreed defaults দাও:
name()return করে "School Office",phone()return করে front desk number,notify()চুপচাপ office-কে জানায়। - Source পরিবর্তন করো — getter, repository বা factory যেটা আগে
nullreturn করত — এখন null object return করবে। - Null check গুলো delete করো — caller by caller। Present আর absent guardian এখন একই uniform code দিয়ে handle হয়।
Refactoring-এর দ্বিতীয় সংস্করণে Fowler এটাকে আরও broad একটা refactoring-এ ভাঁজ করেছেন — নাম Introduce Special Case — কারণ এই same trick যেকোনো recurring special value-র জন্য কাজ করে ("unknown customer", "guest user"), শুধু nothing-at-all-এর জন্য না। Null Object হলো এই পরিবারের সবচেয়ে সহজ, সবচেয়ে pure সদস্য।
এক লাইনে বললে: Introduce Null Object ছড়িয়ে থাকা if (x == null) check গুলোকে একটা real object দিয়ে replace করে যেটা absence represent করে আর প্রতিটা method-এ safe default দেয় — "missing" মানে কী সেটা একবার লেখা হয়, একটা tested class-এ, প্রতিটা call site-এ আলাদা করে বানানো হয় না।
বড় ভাইদের জন্য একটু extra: Null Object pattern formally describe করেছিলেন Bobby Woolf ১৯৯৬ সালের Pattern Languages of Program Design 3 collection-এ। এটাকে সবচেয়ে ভালো বোঝা যায় Replace Conditional with Polymorphism-এর একটা ছোট application হিসেবে — "null কি না-null?" branch আসলে দুই-case type switch, আর null object হলো absent case-এর জন্য subclass। Functional language একই billion-dollar mistake-কে type-এর দিক থেকে attack করে — Haskell-এর Maybe, Rust-এর Option<T>, Scala-র Option absence-কে একটা visible type বানায় যেটা compiler force করে unwrap করাতে, ফলে check ভুলে যাওয়া compile error হয়ে যায়, রাত ২টার crash না। C#-এর nullable reference types আর TypeScript-এর strictNullChecks পুরনো type system-এ সেই same idea যোগ করে। Null Object আর Option type একটাই প্রশ্নের দুটো উত্তর — "absence কে আর invisible থাকতে দেব না।" একটা default behaviour centralise করে, অন্যটা explicit handling force করে।
পুরো pattern-টা একটা ছোট picture-এ আঁটে:
কখন এটা দরকার?
এই signs গুলো দেখলে বুঝবে:
- Same null check সব জায়গায় shadow করছে। যেখানেই
student.guardiantouch হচ্ছে, আগে=== nullcheck হচ্ছে। দশজন caller, দশটা check — এটাই Duplicated Code safety vest পরে ঘুরে বেড়াচ্ছে। - আলাদা caller আলাদা default বেছে নিচ্ছে। নাসরিন আপা "N/A" লিখছে, আরেক screen empty string দেখাচ্ছে, তৃতীয়টা crash করছে। Absence-এর মানে drift করছে কারণ তার কোনো single home নেই।
- 'না থাকা'টা normal আর expected একটা অবস্থা। কিছু student-এর genuinely guardian listed নেই। কিছু site-এ genuinely এখনো কোনো customer নেই। "Missing" domain-এর অংশ, error না।
- একটাই sensible default behaviour আছে। School agree করেছে — guardian না থাকলে office-কে contact করো। সবাই যদি agree করতে পারে absence কী করবে, তাহলে null object সেটা encode করতে পারে।
- Null-dereference bug আগেই production-এ পালিয়ে গেছে। এটাই সবচেয়ে জোরালো sign। প্রতিটা ভুলে-যাওয়া check একটা
NullReferenceExceptionবাTypeError: cannot read properties of null— customer-এর জন্য অপেক্ষা করছে।
শেষের point-টা ছোট না। Teams যখন crash log audit করে, null dereference সবসময় সবচেয়ে বড় slice-গুলোর একটা:
আর counter-sign গুলো — এগুলোও সমান মনোযোগ দিয়ে পড়ো:
- যদি absence মানে loud error হওয়া উচিত, চুপ করিয়ে দিও না। Account-ছাড়া payment একটা bug, কোনো state না। Null object সেটাকে slide through করতে দেবে, কিছু না করে, সমস্যাটা লুকিয়ে রেখে। বরং exception throw করো বা assertion রাখো।
- যদি আলাদা caller-দের genuinely আলাদা default দরকার হয়, একটা null object সবাইকে serve করতে পারবে না।
Optional/Maybe-style type, বা explicit handling, বেশি honest। - যদি মাত্র এক-দুইটা null check থাকে, পুরো নতুন class বানানো ceremony। Modern optional chaining (
?.) ছোট case সুন্দরভাবে handle করে — নিচে আরও বলছি।
ফাতেমা ম্যাডামের প্রশ্ন — "Guardian না থাকাটা কি আমরা handle করি এমন normal situation, নাকি এমন mistake যেটা catch করতে হবে?" — এটাই এই decision map-এর x-axis:
Before আর After এক নজরে
School-এর fee-reminder code, check-এ ডুবে আছে:
// BEFORE: every property access wears a null-check helmet
function feeReminder(student: Student): string {
const guardian = student.guardian; // may be null
const name = guardian === null ? "Parent/Guardian" : guardian.name;
const phone = guardian === null ? SCHOOL_OFFICE_PHONE : guardian.phone;
const channel = guardian === null ? "office-noticeboard" : guardian.preferredChannel;
return `To ${name} (${phone}) via ${channel}: fee for ${student.name} is due.`;
}তিনটা ternary, তিনটা locally-invented default — আর এটা guardian touch করা অনেকগুলো function-এর মাত্র একটা। এখন office contact card:
// AFTER: absence is an object; callers stop asking
interface Guardian {
readonly name: string;
readonly phone: string;
readonly preferredChannel: string;
}
class NullGuardian implements Guardian {
readonly name = "School Office";
readonly phone = SCHOOL_OFFICE_PHONE;
readonly preferredChannel = "office-noticeboard";
}
class Student {
private _guardian: Guardian | null = null;
get guardian(): Guardian {
return this._guardian ?? new NullGuardian(); // never null again
}
}
function feeReminder(student: Student): string {
const g = student.guardian; // always a real object
return `To ${g.name} (${g.phone}) via ${g.preferredChannel}: fee for ${student.name} is due.`;
}Conditional গুলো চলে গেছে — move হয়নি, গেছে। "School Office / front desk / noticeboard" policy মাত্র একটা class-এ আছে। ভবিষ্যতের প্রতিটা caller automatically সঠিক behaviour পাবে, কোনো check মনে রাখতে হবে না, তাই ভুলে যাওয়ারও সুযোগ নেই।
Runtime-এ কী হয় দেখো — caller এত calm থাকে কারণ সে বুঝতেই পারে না কোন ধরনের card পেয়েছে:
ধাপে ধাপে, নিরাপদ পথে
Pattern-টা সহজ, কিন্তু live codebase convert করতে ছোট ছোট ধাপের discipline লাগে।
ধাপ ১: Interface আলাদা করো। নিশ্চিত করো caller-রা একটা Guardian interface-এর উপর depend করছে (বা class-এর public surface-এর উপর), concrete detail-এ না। Caller-রা আসলে কোন member ব্যবহার করে সেগুলোর list করো — এটাই সেই contract যেটা তোমার null object-কে honor করতে হবে।
ধাপ ২: Null class বানাও agreed defaults দিয়ে। এই ধাপটা শুধু typing না, এটা একটা team conversation। Missing guardian-এর phone কী হবে? Notification কে পাবে? ফাতেমা ম্যাডাম printer-কে decide করতে দেননি — staff meeting ডেকেছিলেন। Domain answer নিয়ে আসো, তারপর encode করো:
class NullGuardian implements Guardian {
readonly name = "School Office";
readonly phone = SCHOOL_OFFICE_PHONE;
readonly preferredChannel = "office-noticeboard";
get isPresent(): boolean { return false; } // escape hatch, used sparingly
}isPresent flag (Fowler isUnknown/isNull ব্যবহার করেন) rare caller-দের জন্য যাদের সত্যিই আলাদা behaviour দরকার — কিন্তু প্রতিটা ব্যবহার scattered check-এর দিকে একধাপ ফেরত যাওয়া, তাই count করে রাখো।
ধাপ ৩: Source পরিবর্তন করো যাতে null কখনো return না হয়। Getter, repository বা factory — এটাই একমাত্র জায়গা যেখানে blankness office card-এ রূপান্তরিত হয়। Tests run করো। Existing null check গুলো এখনো pass করবে (NullGuardian হলো null না, তাই === null branch গুলো শুধু fire করা বন্ধ করবে)। Verify করো defaults পুরনো branches-এর মতো match করছে কিনা।
ধাপ ৪: একটা একটা করে caller-এর check delete করো। Fee reminder নাও। Ternary গুলো remove করো। Test করো। Consent form নাও। Remove, test। প্রতিটা deletion ছোট আর reversible।
ধাপ ৫: Type tight করো। যখন কোনো caller আর null check করছে না, property-টা non-nullable declare করো (guardian: Guardian, | null ছাড়া)। TypeScript-এ strictNullChecks বা C#-এ nullable reference types enable থাকলে compiler এখন এই পুরো bug category-র guarantee দেবে যে সেটা ফিরে আসতে পারবে না।
ধাপ ৬: দুজনকে sync রাখো। যখনই real Guardian interface-এ নতুন method আসবে, NullGuardian-কেও তার জন্য একটা default পেতে হবে। Subclassing-এর বদলে interface implement করলে compiler নিজেই এটা enforce করবে।
Data-এর দিক থেকে দেখলে, refactoring-টা পরিবর্তন করে guardian slot কোন কোন state-এ থাকতে পারে:
Rollout-এর সময় payoff curve দেখতে satisfying লাগে। প্রতিটা caller convert হওয়ার সাথে সাথে check কমতে থাকে, যতক্ষণ না ধাপ ৫ count-টাকে চিরতরে শূন্যে lock করে দেয়:
Null object-টাকে directly test করো। এটা একটা real policy encode করে — "guardian না থাকলে office-কে contact করো" — আর policies-এর test দরকার calculation-এর মতোই। Null object-কে immutable আর stateless রাখো। Mutable null object একটা trap: code সেখানে data "save" করে, data চুপচাপ উধাও হয়ে যায় (কোন student-এর জন্য সেটা থাকত?), আর তুমি weekend কাটাও writes খুঁজে যেগুলো কোথাও গেল না। Freeze করো, চাইলে একটা instance share করো, শুধু প্রশ্নের উত্তর দিতে দাও।
একটু বড় real-life example
Absence নেস্টেড হতে পারে। Guardian missing হতে পারে, আবার listed guardian-এরও notification subscription নাও থাকতে পারে। দেখো injury-alert flow-এ check কীভাবে বাড়তে থাকে — আর তারপর কীভাবে সব collapse হয়:
// BEFORE: nested absence, nested checks
function injuryAlert(student: Student): string {
const guardian = student.guardian; // Guardian | null
if (guardian === null) {
return `Inform office about ${student.name}`;
}
const sub = guardian.subscription; // Subscription | null
if (sub === null) {
return `Call ${guardian.phone} manually about ${student.name}`;
}
if (sub.smsEnabled) {
return `SMS ${guardian.phone}: ${student.name} injured`;
}
return `Email ${sub.email}: ${student.name} injured`;
}// AFTER: each level of absence gets its own polite stand-in
class NullSubscription implements Subscription {
readonly smsEnabled = false;
readonly email = SCHOOL_OFFICE_EMAIL; // alerts fall back to the office
}
class NullGuardian implements Guardian {
readonly name = "School Office";
readonly phone = SCHOOL_OFFICE_PHONE;
readonly preferredChannel = "office-noticeboard";
get subscription(): Subscription { return new NullSubscription(); }
}
// real Guardian also guarantees a subscription:
// get subscription() { return this._sub ?? new NullSubscription(); }
function injuryAlert(student: Student): string {
const g = student.guardian; // never null
const sub = g.subscription; // never null either
return sub.smsEnabled
? `SMS ${g.phone}: ${student.name} injured`
: `Email ${sub.email}: ${student.name} injured`;
}দেখো null object গুলো কীভাবে compose হচ্ছে — NullGuardian.subscription return করে NullSubscription, তাই doubly-absent case (guardian নেই, তাই subscription-ও নেই) একই দুই-লাইনের happy path দিয়ে যাচ্ছে। Unlisted guardian-এর alert office email-এ যাচ্ছে — ফাতেমা ম্যাডামের চাওয়া policy — আর সেটা একবার লেখা, প্রতিটা alert function-এ আলাদা করে derive করা না।
Class structure-টা twin-track design পরিষ্কার করে দেখায় — প্রতিটা real class-এর একটা ভদ্র stand-in আছে same contract implement করে:
Optional chaining: lightweight cousin
Modern language এই idea-র একটা mini version syntax-এ বেঁধে দিয়েছে। TypeScript (3.7+), JavaScript, আর C# সবাই optional chaining ?. আর coalescing operator ?? offer করে:
// the one-line cousin: stop the crash, supply a default inline
const phone = student.guardian?.phone ?? SCHOOL_OFFICE_PHONE;
const email = student.guardian?.subscription?.email ?? SCHOOL_OFFICE_EMAIL;এটা crash থামায় আর পড়তে ভালো লাগে। তাহলে কখন full pattern দরকার? সৎভাবে compare করো:
Optional chaining ?. + ?? | Null Object class | |
|---|---|---|
| Default কোথায় থাকে | প্রতিটা call site-এ, repeated | একটা class-এ, একবার লেখা |
| Defaults drift হওয়ার সম্ভাবনা | Caller বাড়লে হওয়া সম্ভব | অসম্ভব — single source |
| Default behaviour (methods, actions) | Possible না — শুধু value | Natural — notify() real কাজ করতে পারে |
| Setup cost | শূন্য | প্রতিটা type-এর জন্য একটা class |
| সবচেয়ে ভালো | ১–৩ জায়গায় nullable touch হলে | অনেক caller, agreed domain default |
Rule of thumb: ?. হলো নিজের ছাতা; null object হলো school-এর covered walkway বানানো। এক-দুইবার হাঁটতে হলে ছাতা নাও। যখন সারা school প্রতিদিন সেই পথ হাঁটে, walkway বানাও।
C#-এ একই refactoring
C# দুটো সুন্দর জিনিস যোগ করে: nullable reference types দিয়ে compiler boundary police করে, আর shared singleton instance কারণ null object immutable:
public interface IGuardian
{
string Name { get; }
string Phone { get; }
string PreferredChannel { get; }
}
public sealed class NullGuardian : IGuardian
{
// one immutable instance for the whole application
public static readonly NullGuardian Instance = new();
private NullGuardian() { }
public string Name => "School Office";
public string Phone => SchoolConfig.OfficePhone;
public string PreferredChannel => "office-noticeboard";
}
public class Student
{
private IGuardian? _guardian; // nullable INSIDE, only here
public IGuardian Guardian =>
_guardian ?? NullGuardian.Instance; // non-nullable OUTSIDE
public string Name { get; init; } = "";
}
// callers are check-free:
public string FeeReminder(Student s) =>
$"To {s.Guardian.Name} ({s.Guardian.Phone}) via {s.Guardian.PreferredChannel}: " +
$"fee for {s.Name} is due.";C#-specific notes:
- Nullable reference types enable করো (
<Nullable>enable</Nullable>)। Property-র typeIGuardian(কোনো?নেই) compiler-checked promise হয়ে যায় — caller-রা null check লিখতেই পারবে না warning ছাড়া। Billion-dollar mistake compile time-এ fence হয়ে গেল। - Private constructor আর
Instancefield null object-কে singleton বানায় — safe কারণ এটা কোনো state রাখে না।s.Guardian == NullGuardian.Instancecomparison-ও সম্ভব হয় (sparingly ব্যবহার করো,isPresent-এর মতো)। - C#-এও
?.আর??আছে — TypeScript-এর মতো same lightweight-cousin trade-off। - Frameworks সব জায়গায় এই pattern ব্যবহার করে: Microsoft.Extensions.Logging-এর
NullLogger.Instanceএকটা textbook null object — logger যেটা politely কিছু করে না, তাই library code-কে logging configured কিনা check করতে হয় না। তুমি হয়তো এই refactoring-এর output ব্যবহার করেছ, খেয়ালই করোনি।
Python-টাও একটু দেখা যাক, কারণ এর duck typing pattern-টাকে প্রায় weightless বানিয়ে দেয় — কোনো interface declaration দরকার নেই, শুধু matching method name থাকলেই হয়:
# Python: duck typing means the stand-in just needs the same methods
class NullGuardian:
name = "School Office"
phone = SCHOOL_OFFICE_PHONE
def notify(self, message: str) -> None:
office_inbox.append(message) # politely falls back to the office
class Student:
def __init__(self) -> None:
self._guardian = None
@property
def guardian(self):
return self._guardian or NullGuardian()IDE support
"Introduce Null Object" বলে single button নেই, কিন্তু ধাপগুলো ভালো support পায়:
- JetBrains Rider / IntelliJ IDEA / ReSharper: Extract Interface এক action-এ real class থেকে contract বের করে। Implement missing members তারপর null class-এর method stub generate করে। Code inspection
nulldereference flag করে (Possible 'System.NullReferenceException') — ফ্রিতে একটা map পেলে কোন check delete করা যায়। - Visual Studio:
Ctrl+.Extract interface আর Implement interface offer করে। Nullable reference types enable করলে compiler নিজেই auditor হয়ে যায়, warning (CS8602 এবং অন্যরা) দিয়ে দেখায় পুরনো null কোথায় leak করতে পারত। - TypeScript
strictNullChecksদিয়ে same role করে — ধাপ ৫-এ types tight করার পরে, বাকি=== nullcomparison always-false হিসেবে flag হয় — compiler deletion list ধরিয়ে দেয়।
IDE skeleton বানায়। Defaults — absence মানে কী — এটা domain decision, কোনো tool বানাতে পারবে না। ফাতেমা ম্যাডাম যে staff meeting ডেকে front desk number ঠিক করেছিলেন, সেটা automate হওয়ার না।
সুবিধা আর ঝুঁকি
| সুবিধা | ঝুঁকি / cost |
|---|---|
Codebase জুড়ে if (x == null) check delete হয় | Real bug লুকাতে পারে: absence loud fail করা উচিত হলে do-nothing object চুপচাপ error গিলে ফেলে |
| "Missing" মানে একবার লেখা হয়, একবার test হয় | এক default সেই caller-দের serve করতে পারে না যাদের genuinely আলাদা absence behaviour দরকার |
| Caller code থেকে পুরো crash category (null dereference) চলে যায় | Collaborating type-এর জন্য নতুন class — মাত্র কয়েকটা check থাকলে overhead |
| Absence first-class, documented domain concept হয় | Null object কে real interface-এর সাথে চিরকাল lockstep রাখতে হবে |
| Chain-এ compose হয় (NullGuardian থেকে NullSubscription) | Mutable null object লেখা data silently হারায় — immutable থাকতে হবে |
| Type system boundary-তে "never null" enforce করতে পারে | Overuse করলে "zombie object" system-এর গভীরে চলে যায় কেউ notice করার আগেই |
প্রথম ঝুঁকিটা একটু মনোযোগ দিয়ে পড়ো। Null Object pattern loud early failure কে quiet default behaviour-এর সাথে trade করে। এই trade দারুণ যখন absence normal আর default genuinely সঠিক — আর dangerous যখন absence মানে upstream-এ কিছু ভুল হয়েছে। যে program crash করে সে bug-এর দিকে আঙুল তোলে। যে program চুপচাপ কিছু না করে সে bug লুকায়। Null object introduce করার আগে সবসময় ফাতেমা ম্যাডামের প্রশ্নটা করো — "Guardian না থাকাটা কি আমরা handle করি এমন normal situation, নাকি catch করা দরকার এমন mistake?" শুধু প্রথমটাই ভদ্র card deserves করে। দ্বিতীয়টার জন্য দেখো Introduce Assertion।
কোন smell গুলো এটা সারায়?
| Smell | এই refactoring কীভাবে সাহায্য করে |
|---|---|
| Duplicated Code | প্রতিটা call site-এ copy-paste হওয়া null check একটা class দিয়ে replace হয় |
| Switch Statements | Null Object হলো "absent kind"-এর জন্য polymorphism — null/not-null branch যেকোনো type-switch-এর মতোই গলে যায় |
| Temporary Field | "কখনো null, কখনো meaningful" fields-গুলো একটা defined always-valid value পায় |
| Long Method | Defensive branching-এ ফুলে ওঠা method গুলো happy path-এ সংকুচিত হয় |
| Comments | "// মনে রেখো: guardian এখানে null হতে পারে!" warning গুলো অর্থহীন হয়ে যায় — type forbid করে |
Quick revision box
+----------------------------------------------------------------+
| INTRODUCE NULL OBJECT - REVISION CARD |
+----------------------------------------------------------------+
| Problem : nullable value -> every caller checks for null, |
| each invents its own default, forgotten check = crash|
| (Hoare 1965: the "billion-dollar mistake") |
| Solution : a real class for absence (NullGuardian) that |
| implements the SAME interface with safe defaults; |
| the SOURCE returns it instead of null |
| Result : callers treat present & absent uniformly; |
| "missing" is defined ONCE, tested ONCE |
| |
| MECHANICS: interface -> null class -> fix the source -> |
| delete checks caller-by-caller -> tighten types |
| LIGHTWEIGHT COUSIN: ?. and ?? — fine for 1-3 call sites |
| KEEP IT : immutable, stateless, in sync with the interface |
| DANGER : if absence = ERROR, do NOT silence it — fail loud |
| FOWLER 2e: generalised as "Introduce Special Case" |
+----------------------------------------------------------------+Practice exercise
ধরো একটা food-delivery app-এ restaurant page আছে। কিছু restaurant-এর কোনো active discount offer নেই, আর সেই null সব জায়গায় leak করছে:
interface Offer {
readonly bannerText: string;
readonly percentOff: number;
apply(total: number): number;
}
class Restaurant {
offer: Offer | null = null;
}
// caller 1 — the banner
function bannerLine(r: Restaurant): string {
return r.offer === null ? "" : r.offer.bannerText;
}
// caller 2 — the bill
function finalBill(r: Restaurant, total: number): number {
if (r.offer !== null) return r.offer.apply(total);
return total;
}
// caller 3 — the sort key (a teammate forgot the check!)
function sortKey(r: Restaurant): number {
return r.offer.percentOff; // crashes for offer-less restaurants
}ধাপে ধাপে refactor করো:
- আগে ফাতেমা ম্যাডামের প্রশ্ন: "offer নেই" কি normal state নাকি error? এক বাক্যে উত্তর লেখো। (এটা normal — বেশিরভাগ restaurant বেশিরভাগ দিন কোনো offer চালায় না।)
NoOffer implements Offerবানাও:bannerTextহবে"",percentOffহবে0, আরapply(total)return করবেtotalunchanged। Frozen singleton বানাও।Restaurantপরিবর্তন করো যাতেoffergetter কখনো null return না করে (?? NoOffer.instance), nullable field private রেখে।bannerLineআরfinalBill-এ check গুলো একটা একটা করে delete করো, মাঝে মাঝে test করো। দেখো caller 3-এর crash কীভাবে caller 3 edit না করেই চলে যায় — এটাই pattern-এর quiet power।- Type tight করো: public
offerহবেOffer, কখনোOffer | nullনা। Verify করো compiler এখন leftover null check-কে pointless বলে flag করছে। - Bonus: UI team জিজ্ঞেস করল, "কিন্তু offer না থাকলে banner hidden হওয়া উচিত, empty না।" এখানে
offer === NoOffer.instanceacceptable, নাকি interface-এisActiveproperty বেশি clean? Identity check-এর চেয়ে explicit query কেন ভালো, এক বাক্যে বলো। - দ্বিতীয় bonus: কোনটার null object করা উচিত না — (a) user-এর profile photo, নাকি (b) placed order-এর delivery address? Bug-hiding risk ব্যবহার করে explain করো।
- College bonus: Rust-এ
Option<Offer>আরmatchদিয়ে caller 2 কেমন দেখাবে? Option version কী force করে যেটা null object version invisible করে ফেলে — এক বাক্যে, আর কেন দুটোই Hoare-এর mistake-এর valid উত্তর।
যদি ৭ নম্বরে তোমার উত্তর হয় "(b) — placed order-এ address না থাকা মানে upstream bug যেটা loud fail করা দরকার, কিন্তু missing photo normal একটা state যার obvious default avatar আছে," তাহলে তুমি pattern আর তার boundary দুটোই বুঝেছ। এই balance-টাই পুরো lesson — ফাতেমা ম্যাডাম তোমার উত্তর staff noticeboard-এ লাগিয়ে দিতেন।
সচরাচর জিজ্ঞাসা
- Null object আসলে কী জিনিস?
- Null object হলো একটা real object যেটা 'এখানে কিছু নেই' কথাটা ভদ্রভাবে প্রকাশ করে। এটা real object-এর মতোই same interface implement করে, কিন্তু প্রতিটা method safe default দিয়ে উত্তর দেয় — যেমন খালি নাম, শূন্য পরিমাণ, কিছু-না-করা action। Caller এটাকে real object-এর মতোই ব্যবহার করে, তাই null check করতেই হয় না।
- Tony Hoare কেন null-কে তার billion-dollar mistake বলেছিলেন?
- Hoare ১৯৬৫ সালে ALGOL W-এর type system design করার সময় null reference বানিয়েছিলেন — শুধু কারণ implement করাটা সহজ ছিল। ২০০৯ সালের একটা conference-এ তিনি ক্ষমা চান, বলেন null-related crash, vulnerability আর নষ্ট হওয়া productivity মিলিয়ে শিল্পের কোটি কোটি ডলার ক্ষতি হয়েছে। Null কোনো interface মানে না — প্রতিটা method call-এ সে crash দিয়ে উত্তর দেয়।
- Null object কি real bug লুকিয়ে ফেলতে পারে?
- হ্যাঁ, আর এটাই এই pattern-এর সবচেয়ে বড় বিপদ। যদি 'missing' মানে আসলে একটা loud error হওয়া উচিত — যেমন account ছাড়া payment, বা address ছাড়া order — তাহলে null object সেটা চুপচাপ গিলে ফেলে আর program কিছু না করে চলতে থাকে। Null object শুধু সেখানে ব্যবহার করো যেখানে 'না থাকা'টা normal আর expected একটা অবস্থা।
- Optional chaining (?.) কি Null Object pattern-এর মতোই?
- এটা হলো lightweight modern cousin। customer?.name ?? 'occupant' এক জায়গায় crash থামায়, কিন্তু প্রতিটা caller নিজেই default ঠিক করে — ফলে defaults আলাদা হয়ে যেতে পারে। Null object একটা tested class-এ default centralise করে। এক-দুইটা জায়গায় ?. ব্যবহার করো। যখন অনেক caller-এর একই agreed behaviour দরকার তখন null object বানাও।
- Introduce Special Case আর Introduce Null Object-এর পার্থক্য কী?
- Refactoring-এর দ্বিতীয় সংস্করণে Fowler idea-টা generalize করে নাম দিয়েছেন Introduce Special Case। Null object শুধু 'এখানে কিছু নেই' handle করে। Special case object যেকোনো recurring special value handle করতে পারে — যেমন 'unknown customer' বা 'guest user' — সম্ভবত do-nothing default-এর চেয়ে বেশি behaviour দিয়ে। Null Object হলো এই পরিবারের সবচেয়ে সহজ সদস্য।
আরো দেখো
সম্পর্কিত পাঠ
Replace Conditional with Polymorphism: প্রতিটি ধরনকে তার নিজের ডেস্ক দাও
Replace Conditional with Polymorphism রিফ্যাক্টরিং শেখো স্কুল রিসেপশনের গল্প দিয়ে — বারবার আসা type switch কীভাবে subclass-এ পরিণত হয়, TypeScript ও C#-এ factory কীভাবে কাজ করে, আর কখন সাধারণ switch রেখে দেওয়াই ভালো সেটাও বুঝবে।
Strategy Pattern: সাইকেল, বাস, নাকি অটো — তুমিই ঠিক করো
Strategy design pattern শেখো একটা সহজ স্কুলে যাওয়ার গল্পের মাধ্যমে — TypeScript আর C# কোড, runtime swapping, বাস্তব উদাহরণ, আর প্র্যাকটিস exercise সহ।
Temporary Field: স্কুল ব্যাগে ক্রিকেট কিট
Temporary Field কোড স্মেল শেখো একটা স্কুল ব্যাগের গল্পের মাধ্যমে — TypeScript আর C#-এ null-ভর্তি field দেখো এবং Extract Class দিয়ে ধাপে ধাপে ঠিক করো।
Switch Statements: সেই রিসেপশনিস্ট আর তার বিশাল নিয়মের খাতা
Switch Statements code smell শেখো একটা school-এর গেটকিপারের গল্পের মাধ্যমে — TypeScript আর C#-এ duplicate switch-এর উদাহরণ সহ, আর কীভাবে polymorphism দিয়ে এটা ঠিক করবে।