Encapsulate Field: Object যেন নিজের ডেটা নিজে পাহারা দেয়
Encapsulate Field কী সেটা সহজ ভাষায় — কেন public field যেকোনো কোডকে object-এর ডেটা নষ্ট করতে দেয়, আর কীভাবে private field সাথে getter-setter দিয়ে object নিজেই সব নিয়ন্ত্রণ করে।
বাজারের দোকান আর কাউন্টার
ধরো ঢাকার মিরপুরে একটা ছোট মুদির দোকান। জামাল ভাই, দোকানদার, কাউন্টারের পেছনে দাঁড়িয়ে থাকেন। পেছনে তাকে বিস্কুট, সাবান, তেল, আর একটা ক্যাশ ড্রয়ার। তার ভাগনে রুবেল সপ্তাহান্তে সাহায্য করতে আসে আর সব কিছু "আধুনিক" করার স্বপ্ন দেখে।
এক রবিবার রুবেলের একটা আইডিয়া আসে। "মামা, আপনি সারাদিন এখানে দাঁড়িয়ে থাকেন কেন? কাস্টমাররা নিজেরাই নিক! কোনো কাউন্টার না, কোনো অপেক্ষা না। Maximum efficiency!" জামাল ভাই ক্লান্ত, তাই একদিনের জন্য রাজি হয়ে যান।
সকাল দশটার মধ্যেই সব গড়বড়। কাউন্টার নেই, তাই কাস্টমাররা সরাসরি তাকের কাছে হেঁটে যায়। পাশের গলির ছয় বছরের তারিক মিষ্টির বয়াম থেকে দশটা ক্যান্ডি নেয় — একটাকা রেখে যায়, যেটা দিয়ে দুটো কেনা যায়। রাহেলা আপা একশো টাকার ভাংতি চান, তাই নিজেই ক্যাশ ড্রয়ার খোলেন — তিনি সৎ, কিন্তু গণনায় ভুল করেন। কেউ সাবানের প্যাকেট বিস্কুটের তাকে রেখে যায়। এক কাস্টমার তেল নেয়, পরে দেওয়ার ইচ্ছা নিয়ে, আর বাড়ি পৌঁছানোর আগেই ভুলে যায়।
সন্ধ্যায় ড্রয়ারে ভুল পরিমাণ টাকা, স্টক রেজিস্টার তাকের কিছুর সাথে মেলে না। জামাল ভাই বলতে পারছেন না কী ভুল হয়েছে, কখন, বা কে করেছে — কারণ সব কিছু সারাদিন সব জায়গায় হয়েছে। দোকানটা তার, কিন্তু কিছুই নিয়ন্ত্রণে ছিল না।
একটু ভাবো — কেউ চোর ছিল না। তারিক গণনা করতে পারে না। রাহেলা আপা ভুল গণনা করেছেন। তেলের কাস্টমার ভুলে গেছেন। দোকানে ডাকাতি হয়নি। সৎ মানুষদের ছোট ছোট ভুলে দোকান নষ্ট হয়েছে — কারণ দোকানের design তাদের ভুলকে তাক আর ড্রয়ারে সরাসরি পৌঁছানোর সুযোগ দিয়েছিল।
আসল দোকান অন্যভাবে চলে। একটা কাউন্টার আছে, আর সব কিছু সেটার উপর দিয়ে যায়। তুমি জামাল ভাইকে এক কেজি চিনি চাও। তিনি মাপেন, দাম দেখেন, টাকা নেন, ভাংতি দেন, খাতায় নোট করেন। তারিক পঞ্চাশটা ক্যান্ডি বাকিতে চাইলে, জামাল ভাই হেসে না বলতে পারেন। প্রতিটা লেনদেন তার মধ্য দিয়ে যায়, তাই প্রতিটা লেনদেনে নিয়ম কাজ করে।
কোডে, একটা public field হলো কাউন্টার-বিহীন দোকান। প্রোগ্রামের যেকোনো জায়গার যেকোনো লাইন ভেতরে ঢুকে value বদলাতে পারে — কোনো নিয়ম নেই, কোনো রেকর্ড নেই। যে refactoring কাউন্টার বানায় সেটাকে বলে Encapsulate Field: field-কে private করো, আর সব বাইরের access object-এর নিয়ন্ত্রিত method দিয়ে পাঠাও।
Encapsulate Field মানে কী?
Encapsulate Field মানে হলো একটা public field নিয়ে — (১) সেটাকে private করা, তারপর (২) পড়ার জন্য একটা getter method দেওয়া, আর লেখা অনুমোদিত হলে একটা setter method দেওয়া। এরপর থেকে বাইরের কোড raw data ছোঁয়ার সুযোগ পায় না — শুধু object-কে জিজ্ঞেস করতে পারে, ঠিক যেভাবে কাস্টমাররা দোকানদারকে জিজ্ঞেস করে।
ব্যাপারটা হলো — object-oriented programming-এর একটা গভীর ধারণা হলো একটা object তার ডেটা আর সেই ডেটার উপর operation একসাথে রাখবে। public field এই মালিকানা দুই ভাগে ভেঙে দেয়। ডেটা নগ্ন হয়ে বেরিয়ে পড়ে, আর যে নিয়মগুলো সেটা রক্ষা করার কথা সেগুলোর থাকার কোনো জায়গা নেই।
field public থাকলে class কখনোই তার নিজের state সম্পর্কে কোনো প্রতিশ্রুতি দিতে পারে না। সে বলতে পারে না "temperature সবসময় ৫ থেকে ৩৫-এর মধ্যে" বা "balance কখনো ঋণাত্মক হয় না" — কারণ যেকোনো অপরিচিত একটা assignment দিয়ে প্রতিশ্রুতি মিথ্যা করতে পারে।
method দিয়ে access গেলে class একটা একক checkpoint পায় যেখানে সে:
- validate করতে পারে ("থার্মোস্ট্যাটের ৯৫°C target প্রত্যাখ্যান করো")
- react করতে পারে ("target বদলালে, হিটারকে সংকেত দাও")
- log বা audit করতে পারে ("কে দাম বদলালো আর কখন — নোট করো")
- পরে internal storage বদলাতে পারে ("ভেতরে Kelvin-এ temperature রাখো") একটাও caller না ভেঙে
সহজ কথায়: ডেটা হলো ক্যাশ ড্রয়ারের মতো, method হলো দোকানদারের মতো। কাস্টমাররা কখনো ড্রয়ার খোলে না — তারা দোকানদারকে টাকা দেয়, যিনি দোকানের নিয়ম মেনে চলেন। কোড অন্য কোনো object-এর field-এ সরাসরি লিখছে দেখলে, মনে কর জামাল ভাইর ক্যাশ ড্রয়ারে অচেনা হাত ঢুকছে।
এই refactoring-এর একটা কাজিন হলো Self-Encapsulate Field, যেখানে class-এর নিজের method-গুলোও raw field-এর বদলে accessor ব্যবহার করে। Martin Fowler তার bliki-তে লিখেছেন তিনি self-encapsulation শুধু তখনই ব্যবহার করেন যখন concrete প্রয়োজন হয়, যেমন value-র lazy initialisation। আজকের জন্য আমাদের focus বেশি সাধারণ আর বেশি জরুরি বিষয়ে: বাইরের লোকদের field ধরা থেকে থামানো।
কলেজ কর্নার: "balance কখনো ঋণাত্মক হয় না" এই ধরনের প্রতিশ্রুতির formal নাম হলো class invariant — একটা শর্ত যা object-এর প্রতিটা public operation-এর আগে ও পরে ঠিক থাকতে হবে। Encapsulation হলো যা invariant-গুলো কার্যকর করা যায় তা নিশ্চিত করে। প্রতিটা mutation class-নিয়ন্ত্রিত method দিয়ে গেলে, class প্রতিটা বের হওয়ার সময় invariant পুনরায় স্থাপন করতে পারে। public field দিয়ে, state পরিবর্তন করতে পারে এমন operation-এর সেট হলো কার্যত "প্রোগ্রামের প্রতিটা লাইন" — কোনো invariant এর মধ্যে টিকতে পারে না। এজন্যই encapsulated কোডে corrupted state debug করা সম্ভব: সন্দেহভাজনের তালিকা পুরো codebase থেকে মুষ্টিমেয় কয়েকটা method-এ নেমে আসে।
কখন এটা দরকার?
এই warning sign-গুলো খোঁজো:
১. নিয়মওয়ালা class-এ একটা public, mutable field। যদি field-এর valid value নিয়ে কোনো নিয়ম থাকে — ০ থেকে ১০০-এর মধ্যে percentage, ঋণাত্মক-নয় stock সংখ্যা, ছয় সংখ্যার PIN — তাহলে public field মানে সেই নিয়ম enforce করা যাচ্ছে না।
২. ছড়িয়ে-ছিটিয়ে থাকা validation। তুমি একই check (if (price < 0) throw ...) field-এ লেখার আগে পাঁচটা আলাদা caller-এ copy-paste করা দেখছ। নিয়মটা এক জায়গায় থাকতে চাইছে: একটা setter-এ।
৩. রহস্যজনক corrupted state। bug report-এ লেখা "balance ঋণাত্মক হয়ে গেছে, কোথায় হলো জানা নেই।" public field দিয়ে প্রোগ্রামের প্রতিটা লাইন সন্দেহভাজন। setter দিয়ে তুমি ভেতরে একটা breakpoint দাও আর পরের run-এ অপরাধী ধরা পড়ে।
৪. Data Class smell। শুধু public field-এর একটা class — কোনো behaviour নেই, কোনো সুরক্ষা নেই — হলো Data Class। তার field encapsulate করা তাকে মেরুদণ্ড দেওয়ার প্রথম পদক্ষেপ।
৫. তোমার read-only ডেটা দরকার। একটা bare public field বলতে পারে না "সবাই পড়তে পারবে, কেউ লিখতে পারবে না।" setter-ছাড়া getter ঠিক এটাই প্রকাশ করে।
কখন এটা দরকার নেই? ধরো একটা pure immutable data carrier — record, DTO, নিয়মবিহীন configuration struct। get/set আনুষ্ঠানিকতা কোনো সুবিধা ছাড়াই noise যোগ করে। readonly field বা record type এটা ভালোভাবে বলে। দোকানদার যেখানে নিয়ম আর টাকা আছে সেখানে দরকার — বিনামূল্যের খবরের কাগজের স্ট্যান্ডে কেউ পাহারাদার বসায় না।
কোনো নির্দিষ্ট field জরুরি কিনা অনিশ্চিত হলে এই মানচিত্রে রাখো। দুটো প্রশ্ন: এটা কতটা ব্যাপকভাবে পড়া হয়, আর বাইরে থেকে কি পরিবর্তন করা হয়?
উল্লম্ব axis অনুভূমিকের চেয়ে বেশি গুরুত্বপূর্ণ। সব জায়গায় পড়া হয় কিন্তু কোথাও লেখা হয় না এমন field মূলত একটা documentation সমস্যা। অনেক জায়গা থেকে লেখা হয় এমন field হলো সকাল দশটায় রুবেলের দোকান।
আগে আর পরে এক নজরে
আগে — কাউন্টার-বিহীন দোকান:
// BEFORE: anyone can reach over and change anything
class TiffinAccount {
public balance = 0; // wide open
}
const account = new TiffinAccount();
// Scattered across the codebase, far from the class:
account.balance = account.balance + 500; // a deposit... probably?
account.balance = -2000; // nonsense, silently accepted
account.balance = account.balance - 700; // overdraw? nobody checksপরে — প্রতিটা লেনদেন কাউন্টার পেরিয়ে যায়:
// AFTER: the object guards its own data
class TiffinAccount {
private _balance = 0;
get balance(): number { // read: allowed for everyone
return this._balance;
}
deposit(amount: number): void {
if (amount <= 0) throw new Error("Deposit must be positive");
this._balance += amount;
}
withdraw(amount: number): void {
if (amount <= 0) throw new Error("Withdrawal must be positive");
if (amount > this._balance) throw new Error("Insufficient balance");
this._balance -= amount;
}
}
const account = new TiffinAccount();
account.deposit(500); // ok — shopkeeper accepts
account.withdraw(700); // refused — Error: Insufficient balance
// account.balance = -2000; // compile error: no setter, read-only outsideদেখো — আমরা একটা সাধারণ setter লিখিনি। আমরা লিখেছি intention-revealing methods — deposit আর withdraw — কারণ domain আসলে এটাই করে। একটা raw set balance caller-দের object-এর বাইরে নতুন balance গণনা করার সুযোগ দিত। Encapsulate Field-এর সেরা version প্রায়ই generic setter একেবারেই বাদ দেয়।
একটা টেবিলে পুরো দোকানের তুলনা:
| পরিস্থিতি | খোলা দোকান (public field) | কাউন্টারওয়ালা দোকান (encapsulated) |
|---|---|---|
| কে value বদলাতে পারে? | যেকোনো জায়গার যেকোনো লাইন | শুধু object-এর নিজের method |
| নিয়মগুলো কোথায় থাকে? | কার্যকর করা যায় না কোথাও | একটা checkpoint-এ, প্রতিটা write-এ চলে |
| খারাপ value আসলে | চুপচাপ গৃহীত, সপ্তাহ পরে পাওয়া যায় | তৎক্ষণাৎ প্রত্যাখ্যাত, স্পষ্ট error-সহ |
| corruption debug করা | প্রতিটা caller সন্দেহভাজন | setter-এ একটা breakpoint অপরাধী ধরে |
| পরে internal storage বদলানো | প্রতিটা caller ভাঙে | caller-দের কাছে অদৃশ্য |
ধাপে ধাপে, নিরাপদ পথে
কীভাবে করবে? Fowler-এর শেখানো সাবধানী এক-পদক্ষেপ-এক-সময় style-এ। কোনো মুহূর্তে প্রোগ্রাম compile বন্ধ হয় না।
ধাপ ১: public field-এর পাশে getter আর setter যোগ করো। এখনই field-এর visibility ছোঁও না — পুরনো সরাসরি access আর নতুন method access পাশাপাশি চলবে।
class Thermostat {
public targetCelsius = 21; // still public, for now
getTargetCelsius(): number {
return this.targetCelsius;
}
setTargetCelsius(value: number): void {
this.targetCelsius = value; // no rules yet — pure pass-through
}
}ধাপ ২: field-এর প্রতিটা reference খোঁজো। IDE-এর Find Usages ব্যবহার করো। একটা checklist বানাও। প্রত্যাশার চেয়ে বেশি caller থাকতে পারে — public field user টানে যেভাবে খোলা মিষ্টির বাক্স পিঁপড়া টানে।
ধাপ ৩: read-গুলো getter দিয়ে আর write-গুলো setter দিয়ে বদলাও, একটা call site এক সময়ে। প্রতিটা পরিবর্তনের পরে compile করো আর tests চালাও।
// before: display.show(thermostat.targetCelsius);
// after: display.show(thermostat.getTargetCelsius());
// before: thermostat.targetCelsius = 24;
// after: thermostat.setTargetCelsius(24);ধাপ ৪: field-কে private করো। একবার Find Usages শূন্য external reference দেখালে, modifier পরিবর্তন করো:
class Thermostat {
private targetCelsius = 21;
// getter and setter as before
}Compile করো। কিছু ভাঙলে, কোনো caller checklist থেকে বাদ পড়েছে — compiler এখন তোমার জন্য সেটা খুঁজে দিয়েছে। ঠিক করো আর এগিয়ে যাও।
ধাপ ৫: এখন নিয়মগুলো যোগ করো — আসল লাভ।
setTargetCelsius(value: number): void {
if (value < 5 || value > 35) {
throw new Error("Target must be between 5°C and 35°C");
}
this.targetCelsius = value;
}ধাপ ৬: setter-এর নাম পুনর্মূল্যায়ন করো। increaseBy(degrees) বা enableEcoMode() কি caller-রা আসলে কী মানে সেটা ভালো বলবে? intent প্রকাশ করে এমন method-এর দিকে যাও, raw assignment নয়।
Validation যোগ করা (ধাপ ৫) হলো সেই একটা ধাপ যা behaviour পরিবর্তন করতে পারে। ধরো কোনো বিদ্যমান কোড ৫০°C সেট করছিল আর "চলছিল" — ভুলভাবে, কিন্তু চুপচাপ। তোমার নতুন check এখন throw করবে। এটা সাধারণত একটা লুকানো bug উন্মোচিত হওয়া, নতুন bug তৈরি না — কিন্তু সচেতনভাবে করো। আজ কোন মানগুলো flow করছে দেখো, throw করবে না clamp করবে না warning log করবে সিদ্ধান্ত নাও। আর mechanical refactoring (ধাপ ১–৪) behaviour change (ধাপ ৫) থেকে আলাদা commit-এ রাখো। bug দেখা দিলে ঠিক কোন commit দোষী সঙ্গে সঙ্গে জানতে পারবে।
কলেজ কর্নার: ধাপ ১–৪ আর ধাপ ৫ এমনভাবে আলাদা যেটা professionals গুরুত্বের সাথে নেয়। ধাপ ১–৪ হলো behaviour-preserving transformations — প্রোগ্রাম আগে যা করত ঠিক তাই করে, যেটা refactoring-এর কঠোর সংজ্ঞা। ধাপ ৫ হলো একই পোশাকে একটা behaviour change। একই commit-এ মেশানোই হলো কীভাবে team-এ ভয়ঙ্কর review comment আসে "এই refactoring production ভেঙেছে" — যখন আসলে refactoring ঠিকঠাক ছিল আর লুকিয়ে ঢোকানো rule change ভেঙেছিল। আলাদা commit মানে আলাদা blame, আলাদা revert, আর একজন reviewer যে প্রতিটা টুকরো সেকেন্ডে যাচাই করতে পারে।
একটা বড় বাস্তব উদাহরণ
ধরো চট্টগ্রামের একটা library software-এ membership পরিচালনা করছে। মূল class সব public field — কাউন্টার-বিহীন দোকান:
// BEFORE: every field is a self-service shelf
class LibraryMember {
public name = "";
public finePaise = 0; // fine owed, in paise
public booksIssued = 0;
public isBlocked = false;
}
// Scattered through the codebase:
member.booksIssued = member.booksIssued + 1; // issue a book... limit? what limit?
member.finePaise = -500; // negative fine: the library owes HIM?
member.isBlocked = false; // anyone can unblock anyone
member.booksIssued = 99; // the rule "max 4 books" lives nowhereLibrary-র স্পষ্ট নিয়ম আছে — একসাথে সর্বোচ্চ ৪টা বই, ১০০ টাকার বেশি জরিমানা থাকলে member blocked, জরিমানা কখনো ঋণাত্মক না। কিন্তু একটাও enforce করা যাচ্ছে না, কারণ ডেটা সম্পূর্ণ খোলা। Librarian সালাম ভাই প্রতি মাসে একই bug রিপোর্ট দেন: "member blocked দেখাচ্ছে কিন্তু কোনো fine নেই", "member-এর minus পাঁচ টাকা fine", "member কোনোভাবে ৯৯টা বই নিয়েছে।" Developer-রা প্রতিটা ticket হাতে ডেটা ঠিক করে বন্ধ করে, কারণ কোন কোড খারাপ value লিখেছে সেটা কখনো খুঁজে পায় না। পরিচিত লাগছে? এটাই জামাল ভাই সন্ধ্যায়, এমন ড্রয়ারের দিকে তাকিয়ে যেটা খাতার সাথে মেলে না।
Encapsulate Field-এর পরে:
// AFTER: rules live where the data lives
class LibraryMember {
private static readonly MAX_BOOKS = 4;
private static readonly BLOCK_LIMIT_PAISE = 100 * 100; // ₹100
private _finePaise = 0;
private _booksIssued = 0;
constructor(public readonly name: string) {}
get finePaise(): number { return this._finePaise; }
get booksIssued(): number { return this._booksIssued; }
// derived, always correct, cannot be set from outside
get isBlocked(): boolean {
return this._finePaise > LibraryMember.BLOCK_LIMIT_PAISE;
}
issueBook(): void {
if (this.isBlocked) throw new Error(`${this.name} is blocked: pay fine first`);
if (this._booksIssued >= LibraryMember.MAX_BOOKS) {
throw new Error("Book limit reached (4)");
}
this._booksIssued++;
}
returnBook(daysLate: number): void {
if (this._booksIssued === 0) throw new Error("No books to return");
this._booksIssued--;
if (daysLate > 0) this._finePaise += daysLate * 200; // ₹2 per late day
}
payFine(paise: number): void {
if (paise <= 0) throw new Error("Payment must be positive");
this._finePaise = Math.max(0, this._finePaise - paise); // never negative
}
}উন্নতিগুলো দেখো:
isBlockedআর stored field না যেটা যে কেউ পালটাতে পারে — এটা fine থেকে derived। এটা কখনো বাস্তবতার সাথে মিলবে না এমন হতে পারে না, আর "যে কেউ যে কাউকে unblock করতে পারে" এখন অসম্ভব।issueBook()৪-বইয়ের সীমা ঠিক এক জায়গায় enforce করে। আগে সীমার একমাত্র আশা ছিল প্রতিটা caller এটা মনে রাখবে।payFine()শূন্যে clamp করে, তাই ঋণাত্মক fine অপ্রতিনিধিত্বযোগ্য।- পড়া (
member.booksIssued) এখনো clean দেখায়, getter-এর কল্যাণে — caller-রা যা বৈধভাবে দরকার ছিল কিছুই হারায়নি।
সালাম ভাইয়ের team পুরনো bug ticket তদন্ত করতে গিয়ে প্রতিটা corrupted member record-এর জন্য একটা সহজ প্রশ্নের উত্তর দেওয়ার চেষ্টা করল: কোড-এর কোন অংশ খারাপ value লিখেছে? ফলাফল ছিল চমকে দেওয়ার মতো:
এক চতুর্থাংশ corruption কখনোই trace করা যায়নি। এটাই public field-এর স্বাক্ষর: শুধু bug না, দায়বর্ধনহীন bug। Encapsulation-এর পরে team payFine-এর ভেতরে একটা breakpoint আর issueBook-এর ভেতরে একটা breakpoint দিল আর একটা বিকেলেই পরের খারাপ caller ধরল। তাদের bug tracker বাকি গল্পটা বলল:
একটা remaining ticket? একটা নিয়ম যেটা ভুল ছিল, bypass করা হয়েছিল না — জরিমানার হার প্রতিদিন ২ টাকা লেখা হয়েছিল যখন board ১ টাকা অনুমোদন করেছিল। ঠিক ছিল এক লাইন, এক জায়গায়, কারণ নিয়মটা এক জায়গায় থাকে। এটাই এই refactoring-এর শান্ত, দীর্ঘমেয়াদী লাভ: এমনকি তোমার ভবিষ্যতের ভুলগুলোও সস্তা হয়ে যায়।
Python-এ একটু দেখা যাক
Python-এ কোনো private keyword নেই, কিন্তু সাংস্কৃতিকভাবে সমমানের কিছু আছে: একটা naming convention সাথে @property decorator। সুন্দর বিষয়টা হলো caller-রা সাধারণ attribute syntax রাখে আর তোমার check নিচে চলে:
class Thermostat:
def __init__(self) -> None:
self._target_celsius = 21.0 # leading underscore: internal, please knock
@property
def target_celsius(self) -> float:
return self._target_celsius
@target_celsius.setter
def target_celsius(self, value: float) -> None:
if not 5.0 <= value <= 35.0:
raise ValueError("Target must be between 5 and 35 degrees")
self._target_celsius = value
t = Thermostat()
t.target_celsius = 24 # looks like a field write — setter runs the check
t.target_celsius = 95 # ValueError: Target must be between 5 and 35 degreesএটাই সেই কৌশল যা C# properties করে: field-এর syntax সাথে method-এর শক্তি। একটা Python class সাধারণ attribute দিয়ে শুরু করতে পারে আর পরে @property যোগ করতে পারে একটাও caller না বদলে — মানে Python-এ তুমি এই refactoring নিরাপদে দেরি করতে পারো আর প্রথম নিয়ম আসার দিন apply করতে পারো।
C#-এ একই refactoring
C# এই refactoring-এর জন্য সবচেয়ে comfortable ভাষা, কারণ properties ভাষায় built-in। একটা property caller-দের কাছে field-এর মতো দেখায় কিন্তু নিচে code চালায় — getter আর setter logic সহ field-access syntax।
আগে:
// BEFORE
public class Thermostat
{
public double TargetCelsius; // public field: no rules possible
}
// anywhere:
thermostat.TargetCelsius = 95.0; // accepted, sadlyপরে, private backing field-সহ property ব্যবহার করে:
// AFTER
public class Thermostat
{
private double _targetCelsius = 21.0;
public double TargetCelsius
{
get => _targetCelsius;
set
{
if (value < 5.0 || value > 35.0)
throw new ArgumentOutOfRangeException(
nameof(value), "Target must be between 5°C and 35°C");
_targetCelsius = value;
}
}
}
// callers keep the SAME syntax as before:
thermostat.TargetCelsius = 24.0; // ok — setter ran the check
thermostat.TargetCelsius = 95.0; // throws ArgumentOutOfRangeExceptionএটা একটা সুন্দর C# সুবিধা: property syntax field syntax-এর সাথে মেলে বলে, field থেকে property-তে convert করলে call site একদম বদলায় না। একটা সাবধান নোট: এটা এখনো একটা binary breaking change — পুরনো field-এর বিরুদ্ধে compile করা কোড পুনরায় compile করতে হবে। আর তুমি field-এ যেভাবে ref argument পাঠাতে পারতে property-তে সেটা পারবে না।
C# তোমাকে graded স্তরের সুরক্ষাও দেয়, তাই ঠিক যতটুকু দরকার ততটুকুই ব্যবহার করতে পারো:
public class LibraryMember
{
// 1. Read anywhere, write only inside this class:
public int BooksIssued { get; private set; }
// 2. Set once during construction, never again (init-only):
public string Name { get; init; }
// 3. Derived property — computed, not stored:
public bool IsBlocked => FinePaise > 10_000;
// 4. Full control with a backing field when validation is needed:
private int _finePaise;
public int FinePaise
{
get => _finePaise;
private set => _finePaise = Math.Max(0, value);
}
public void IssueBook()
{
if (IsBlocked) throw new InvalidOperationException("Member is blocked");
if (BooksIssued >= 4) throw new InvalidOperationException("Limit reached");
BooksIssued++;
}
}{ get; private set; } একাই বেশিরভাগ সাধারণ ক্ষেত্র সমাধান করে: দুনিয়া পড়তে পারবে, শুধু object লিখতে পারবে। ওই একটা লাইনই Encapsulate Field-কে ছয়টা token-এ পাতন করেছে।
C# member-এর কতটা সুরক্ষা দরকার তা বেছে নিতে এই ladder ব্যবহার করো:
| প্রয়োজন | C# টুল | এক-লাইন উদাহরণ |
|---|---|---|
| সব জায়গায় পড়া, construction-এর পরে কখনো পরিবর্তন না | init-only property | public string Name { get; init; } |
| সব জায়গায় পড়া, শুধু object নিজে পরিবর্তন করে | private setter | public int BooksIssued { get; private set; } |
| অন্য state থেকে computed value | expression-bodied property | public bool IsBlocked => FinePaise > 10_000; |
| প্রতিটা write validate করতে হবে | backing field + full property | set-এ check-সহ property |
| Outsider-রা assignment না, intent প্রকাশ করে | setter-এর বদলে method | member.IssueBook(); |
IDE সমর্থন
এই refactoring এত সাধারণ যে IDE-গুলো এটা সম্পূর্ণভাবে automate করে:
- Visual Studio: field-এ caret রেখে
Ctrl+R, Ctrl+Eচাপো (বাCtrl+.Quick Actions-এর জন্য) আর Encapsulate field বেছে নাও। Microsoft Learn দুটো version document করেছে: encapsulate and update usages (caller-রা property-তে switch করে) বা encapsulate but keep direct usages class-এর ভেতরে। - JetBrains Rider / ReSharper: field-এ caret,
Ctrl+Shift+R(Refactor This) বাAlt+Enter, তারপর Encapsulate Field। Rider auto-property বা backing field-সহ property generate করতে পারে, সব usages update করে, আর সব কিছু একটা operation হিসেবে register করে — একটাCtrl+Zপুরো refactoring undo করে। - IntelliJ IDEA (Java): menu-তে Refactor → Encapsulate Fields একটা dialog খোলে যেখানে তুমি field tick করো, getter/setter name বেছে নাও, আর class-এর নিজের access-ও accessor ব্যবহার করবে কিনা সিদ্ধান্ত নাও (Self-Encapsulate Field একটা checkbox হিসেবে!)।
- VS Code (TypeScript): কোনো single built-in "encapsulate field" action নেই, কিন্তু Rename Symbol (F2) সাথে compiler manual ধাপগুলো দ্রুত করে:
balance-কে_balance-এ rename করো,privatemark করো, আর error list হয়ে যায় migrate করার call site-এর checklist।
Automated encapsulation mechanical অংশ নিখুঁতভাবে করে। চিন্তাশীল অংশ — setter-এ কোন validation আছে, setter আদৌ থাকা উচিত কিনা, deposit() কি সত্যিকারের নাম — এটা তোমার কাজ থেকে যায়, আর এটাই গুরুত্বপূর্ণ অংশ।
সুবিধা আর ঝুঁকি
| সুবিধা | ঝুঁকি / খরচ |
|---|---|
| Validation আর invariants প্রতিটা write-এ চলে — খারাপ value দরজায় প্রত্যাখ্যাত | Immutable, rule-মুক্ত data carrier-এর জন্য accessor pure ceremony |
| Setter-এ একটা breakpoint প্রতিটা write খোঁজে — corrupted state debug করা সহজ | ফাঁকা get/set জোড়া মিথ্যা নিরাপত্তার অনুভূতি দিতে পারে আর behaviour বাইরে থাকতে পারে (ছদ্মবেশী Data Class) |
| Read আর write extension point হয়: lazy loading, change events, audit logs | আগের open field-এ validation যোগ করা বিদ্যমান ভুল caller উন্মোচন করতে পারে — ভালো কিন্তু কষ্টের |
| Internal representation caller না ছুঁয়ে বদলানো যায় | C#-এ field-to-property source-compatible কিন্তু binary-breaking; field-এ ref access হারানো যায় |
| Read-only exposure সম্ভব (setter ছাড়া getter) | Inlining ছাড়া language-এ tiny call overhead — প্রায় কখনোই পরিমাপযোগ্য না |
কোন smell এটা সারায়?
| Smell | Encapsulate Field কীভাবে সাহায্য করে |
|---|---|
| Data Class | Standard cure-এর প্রথম পদক্ষেপ: field লুকাও, তারপর নতুন method দিয়ে class-এ behaviour টানো |
| Inappropriate Intimacy | Outsider-রা আর অন্য class-এর private অংশে ঘোরাফেরা করতে পারে না — interface হয়ে যায় একমাত্র দরজা |
| Duplicate Code | প্রতিটা write-এর আগে copy-paste করা validation একটা setter-এ collapse হয় |
| Shotgun Surgery | নিয়ম পরিবর্তন (নতুন valid range) প্রতিটা caller-এর বদলে একটা setter edit করে |
এই smell cluster Primitive Obsession-এর সাথেও ঘনিষ্ঠভাবে সম্পর্কিত। একটা public primitive field দ্বিগুণ exposed — ভুল আকার আর কোনো guard নেই। প্রায়ই পূর্ণ cure হলো Encapsulate Field তারপর Replace Data Value with Object।
এখানে পুরো lesson একটা মানচিত্রে revision-এর জন্য:
দ্রুত revision কার্ড
+================================================================+
| ENCAPSULATE FIELD — REVISION CARD |
+================================================================+
| SMELL SIGN : public mutable field on a class that has rules |
| PICTURE : shop with no counter -> strangers in cash drawer |
+----------------------------------------------------------------+
| THE MOVE : 1. Add getter + setter (field still public) |
| 2. Find Usages -> checklist of call sites |
| 3. Migrate reads/writes one by one (test each) |
| 4. Make the field PRIVATE |
| 5. Add validation / rules in the setter |
| 6. Rename towards intent: deposit(), issueBook() |
+----------------------------------------------------------------+
| C# GOLD : public int X { get; private set; } |
| derived value -> computed property, not a field |
| REMEMBER : mechanical steps and behaviour change = |
| SEPARATE commits |
+================================================================+অনুশীলন করো
ধরো রাজশাহীর একটা সিনেমা booking system-এ এই class আছে:
class Show {
public movieName = "";
public totalSeats = 100;
public seatsBooked = 0;
public ticketPriceRupees = 150;
public isHouseFull = false;
}
// Found around the codebase:
show.seatsBooked = show.seatsBooked + 4; // booking 4 tickets
show.seatsBooked = 130; // more than total seats!
show.ticketPriceRupees = -50; // negative price
show.isHouseFull = true; // set by hand, often forgottenতোমার কাজ:
১. Encapsulate Field ধাপে ধাপে apply করো: আগে accessor, তারপর caller migrate করো, তারপর প্রতিটা field private করো। প্রতিটা ধাপে কোড compile রাখো।
২. Raw seat-setter-কে একটা intention-revealing method bookSeats(count: number) দিয়ে replace করো যেটা যতটা seat বাকি আছে তার চেয়ে বেশি book করতে রাজি না।
৩. isHouseFull-কে seatsBooked আর totalSeats থেকে computed derived getter-এ রূপান্তরিত করো, যাতে এটা কখনো stale বা ভুলে যাওয়া না হয়।
৪. একটা নিয়ম যোগ করো: ticket price ৫০ থেকে ৫০০ টাকার মধ্যে হতে হবে। এই নিয়মটা এখন ঠিক কোথায় থাকবে?
৫. বোনাস (C#): { get; private set; } ব্যবহার করে SeatsBooked-এর জন্য, expression-bodied IsHouseFull, আর price-এর জন্য validating setter দিয়ে C# properties ব্যবহার করে শেষ class পুনরায় লেখো।
৬. Reflection প্রশ্ন: totalSeats-এর জন্য কি public setter থাকা উচিত? কোন বাস্তব ঘটনা একটা হলের seat সংখ্যা পরিবর্তন করত, আর কোন method নাম সেটা সৎভাবে বর্ণনা করত?
একটা শেষ কথা। কাউন্টার-বিহীন দিনের শেষে রুবেল জামাল ভাইকে জিজ্ঞেস করল দোকান কেন ব্যর্থ হলো যখন প্রতিটা কাস্টমার সৎ ছিল। জামাল ভাই কাউন্টারে থাপ দিয়ে বললেন, "বাবা, এটা এখানে নেই কারণ আমি মানুষকে অবিশ্বাস করি। এটা এখানে আছে যাতে একটা অসাবধানী মুহূর্ত — তাদের বা আমার — একটাই জায়গায় ধরা পড়ে।" এটাই এক বাক্যে encapsulation। তুমি খলনায়কদের বিরুদ্ধে দেওয়াল তৈরি করছ না — তুমি একটা ভালো কাউন্টার তৈরি করছ যেখানে সাধারণ ভুলগুলো, তোমারটাসহ, বিনয়ের সাথে প্রতিটা বার থামানো যাবে।
সচরাচর জিজ্ঞাসা
- আমার getter আর setter যদি শুধু field পড়ে আর লেখে — কোনো চেক নেই — তাহলে লাভটা কী হলো?
- দুটো লাভ আছে। প্রথমত, তুমি একটা বীমা নিলে — কাল যদি validation, logging, বা internal representation বদলাতে হয়, একটাও caller ছোঁয়া লাগবে না, কারণ তারা ইতিমধ্যে method দিয়ে যাচ্ছে। দ্বিতীয়ত, একটা ফাঁকা setter-ও searchable আর breakpoint-able — এক ক্লিকেই সব write খুঁজে পাবে। কিন্তু ভেবে দেখো সেখানে আসল rule রাখা দরকার কিনা; চিরকালের ফাঁকা accessor একটা Data Class smell লুকিয়ে রাখতে পারে।
- getter আর setter কি কেউ কেউ খারাপ মনে করে না?
- যেটার সমালোচনা হয় সেটা হলো প্রতিটা field-এর জন্য আপনা-আপনি get/set জোড়া বানানোর অভ্যাস। আসল লক্ষ্য হলো 'tell, don't ask' — বাইরে থেকে value নিয়ে বদলে আবার set করার বদলে, object-কে একটা method দাও যেমন deposit(500) যেটা কাজটা নিজেই করবে। Encapsulate Field হলো প্রথম পদক্ষেপ; intention-revealing method হলো গন্তব্য।
- TypeScript-এ কি এটা দরকার, যেখানে আমি শুধু readonly বা private লিখতে পারি?
- TypeScript-এর private আর readonly keyword compile-time guard — প্রায়ই যথেষ্ট। Encapsulate Field তখন দরকার যখন value পরিবর্তনযোগ্য থাকতে হবে কিন্তু শুধু নির্দিষ্ট নিয়মের মধ্যে — তখন private field সাথে validation-সহ setter বা method হলো সঠিক টুল। TypeScript-এ get আর set accessor syntax আছে, তাই caller property-style syntax রাখতে পারে আর তোমার check নিচে চলতে থাকে।
- Encapsulate Field আর Self-Encapsulate Field-এর পার্থক্য কী?
- Encapsulate Field class-এর বাইরে থেকে access নিয়ন্ত্রণ করে — field private করো, বাইরের লোককে accessor দিয়ে পাঠাও। Self-Encapsulate Field আরেক ধাপ এগিয়ে যায় — class-এর নিজের method-গুলোও সরাসরি field ছোঁয় না, accessor ব্যবহার করে। Fowler নিজে বলেন তিনি self-encapsulation শুধু তখনই করেন যখন concrete কারণ থাকে, যেমন lazy loading বা subclass-কে access override করতে দেওয়া।
- এত method call কি প্রোগ্রাম ধীর করে দেবে না?
- প্রায় কখনোই না — পরিমাপযোগ্যভাবে তো না-ই। আধুনিক compiler আর runtime যেমন .NET আর JVM-এর JIT, JavaScript-এর V8 — সাধারণ accessor inline করে দেয়, সরাসরি field access-এর মতোই দ্রুত। শুধু lower-level language-এ extreme inner loop-এ হয়তো গুরুত্ব দিতে পারে। আর সেক্ষেত্রেও আগে measure করো, guess করো না।
আরো দেখো
সম্পর্কিত পাঠ
Data Class: নিয়মহীন রেজিস্টার — যে কেউ যা খুশি লিখে যায়
Data Class smell শেখো একটা society register-এর গল্পের মাধ্যমে। দেখো কেন behavior ছাড়া data encapsulation ভেঙে পড়ে, আর কখন DTO আর record একদম ঠিকঠাক।
Primitive Obsession: যখন সব কিছুই শুধু একটা string বা number
Primitive Obsession সহজ ভাষায় — কেন plain string আর number bug লুকিয়ে রাখে, আর কীভাবে Money বা Address-এর মতো value object দিয়ে code-কে safe আর পরিষ্কার করা যায়।
Encapsulate Collection: লাইভ লিস্ট বাইরে দেওয়া বন্ধ করো
Encapsulate Collection সহজ ভাষায় — কেন live array বা list return করলে যেকেউ তোমার object নষ্ট করে দিতে পারে, আর কীভাবে read-only view আর add/remove method দিয়ে নিয়ন্ত্রণ ফিরিয়ে আনা যায়।
Inappropriate Intimacy: দুটো class যারা একে অপরের রান্নাঘরে ঢুকে পড়ে
দুই প্রতিবেশীর গল্প দিয়ে Inappropriate Intimacy বোঝো — যারা একে অপরের রান্নাঘর সাজিয়ে দেয়। দুটো class যখন একে অপরের private অংশে হাত দেয়, তখন কেউ একা কিছু বদলাতে পারে না। Law of Demeter আর privacy ফিরিয়ে আনার refactoring শেখো।