Remove Setting Method: কিছু জিনিস কলমে লেখা, পেন্সিলে না
Remove Setting Method সহজ ভাষায় — কেন এমন একটা field যেটা তৈরির পরে কখনো বদলানো উচিত না তার setter রাখা ঠিক না, আর কীভাবে read-only field, init-only property, আর record দিয়ে 'এটা বদলিও না' কথাটাকে compiler-এর গ্যারান্টিতে বদলানো যায়।
🖋️ কলম ছাড়া জন্মনিবন্ধন সনদ
ধরো সুমাইয়ার জন্ম হলো। তার বাবা জামাল আর মা নাসরিন ইউনিয়ন পরিষদের অফিসে গেলেন। সালাম সাহেব, নিবন্ধক, সতর্ক মানুষ। তিনি হাসপাতালের কাগজ দুইবার চেক করলেন, সুমাইয়ার নাম আর জন্মতারিখ বড় রেজিস্টারে লিখলেন, সনদ প্রিন্ট করে সিল মেরে দুই হাতে দিলেন।
এবার একটা গুরুত্বপূর্ণ জিনিস লক্ষ্য করো। সনদের সাথে কলম নেই। এমন কোনো বাক্স নেই যেখানে লেখা "জন্মতারিখ বদলাতে হলে এখানে লিখুন।" তারিখটা একবার লেখা হয়েছে, ঘটনার মুহূর্তে, একমাত্র অনুমোদিত মানুষের হাতে। কেন? কারণ জন্মতারিখ কোনো মতামত না যেটা আপডেট হয় — এটা একটা সত্য, একটা নির্দিষ্ট মুহূর্তে স্থির। স্কুলে ভর্তি, পাসপোর্ট, ভোটের বয়স — এই একটা তারিখের উপরে অনেক কিছু নির্মিত হবে।
বছর পরে, সুমাইয়ার স্কুলের স্পোর্টস কোচ চেষ্টা করল। সুমাইয়া আন্ডার-১২ টিমের জন্য তিন মাস বড় — কোচ হাসতে হাসতে বলল, "সনদটা একটু ঠিক করিয়ে আনো না?" জামাল হেসে বললেন উল্টোটা কল্পনা করো: যেখানে যেকোনো কোচ, যেকোনো কেরানি, চাচার বলপয়েন্ট কলম নিয়ে তারিখ বদলে দিতে পারে। এক বছরের মধ্যে কোনো স্কুল, কোনো পাসপোর্ট অফিস — কেউই কোনো সনদ বিশ্বাস করত না। দলিলের সমস্ত মূল্য আসে সেই অনুপস্থিত কলম থেকে।
এটার সাথে তুলনা করো ক্লাসরুমের হাজিরা রেজিস্টার — টেবিলে খোলা, পাশ দিয়ে যাওয়া যেকোনো ছাত্র P-কে A বানিয়ে দিতে পারে। এক সপ্তাহের মধ্যে কেউ সেটাও বিশ্বাস করে না।
Software-এরও নিজস্ব জন্মনিবন্ধন সত্য আছে: account-এর ID, order number, ticket-এর PNR, record তৈরির timestamp। এই মানগুলো একবার সেট হয়, object-এর "জন্মের" সময় — তার constructor-এ। তবুও প্রায়ই অভ্যাসবশত আমরা প্রতিটা field-এ public setter দিই — সনদের সাথে একটা কলম আটকে দিই। যে refactoring কলমটা সরিয়ে দেয় তাকে বলে Remove Setting Method।
🧠 Remove Setting Method কী?
Remove Setting Method মানে হলো এমন field-এর setter মুছে ফেলা যার মান একবার, তৈরির সময়, ঠিক হয়ে যায় আর কখনো বদলায় না। মানটা constructor-এর মাধ্যমে ঢোকে — এরপর field read-only।
আগে — কলম আটকানো সনদ:
class Account {
private _id: string;
constructor(id: string) {
this._id = id;
}
get id(): string { return this._id; }
set id(value: string) { // why does this even exist?
this._id = value;
}
}
// ...much later, in a faraway file:
account.id = "TEMP-FIX-42"; // identity silently corruptedপরে — কলমে লেখা:
class Account {
constructor(readonly id: string) {} // set at birth, fixed forever
}
// account.id = "TEMP-FIX-42"; // compile error — there is no penব্যাপারটা হলো convention আর guarantee-এর পার্থক্য। // please never change id লেখা comment হলো convention — যতক্ষণ কোনো ক্লান্ত developer রাত ১১টায় সেটা বদলে না দেয় ততক্ষণই কাজ করে। Setter সরানো নিয়মটাকে compiler-এর দ্বারা enforce করা guarantee-তে পরিণত করে। কেউ নিয়ম ভাঙতে পারবে না, কারণ ভাষা নিজেই মানা করে দেয়। সালাম সাহেবকে ঢাকার সব কোচকে বিশ্বাস করতে হয় না — সনদে লেখার জায়গাটাই নেই।
Fowler বলেছেন সহজ কথায়: যদি object তৈরির পরে কোনো field বদলাতে না চাও, তাহলে setter method দিও না। Refactoring Guru আরও যোগ করে: মানটা শুধু তৈরির সময় সেট করার কথা, তবুও setter সারা program থেকে "update"-এর আমন্ত্রণ দিয়ে যাচ্ছে।
দেখো code আর object-এর কথোপকথন, আগে আর পরে — guarantee কোথা থেকে আসছে লক্ষ্য করো:
তুমি যে field-ই design করো না কেন, নিজেকে একটা quick sorting প্রশ্ন করো: এই মানটা কি কলমে লেখা নাকি পেন্সিলে? কলম: পরিচয়, তৈরির সময়, object-এর মূল সংজ্ঞামূলক তথ্য — শুধু constructor-এ, কোনো setter নেই। পেন্সিল: যেগুলো জীবনে সত্যিকার অর্থে বদলায় — কিন্তু তখনও raw set status-এর বদলে markDelivered()-এর মতো সৎ method ব্যবহার করো। সিদ্ধান্ত নিতে না পারলে কলম দিয়ে শুরু করো: পরে setter যোগ করা সহজ, কিন্তু সারা codebase setter ব্যবহার শুরু করার পরে সেটা সরানো কঠিন।
🔍 কখন এটা দরকার?
এই লক্ষণগুলো খোঁজো:
- Setter যেটা
new-এর ঠিক পরে মাত্র একবার call হয়। Usage খোঁজো। যদি প্রতিটা callerconst t = new Ticket(); t.pnr = "8642097531";করে, তাহলে মানটা স্পষ্টতই constructor-এ যাওয়া উচিত। Setter হলো শুধু একটা ধীর, ঝুঁকিপূর্ণ constructor। - Setter সহ identity আর জন্মের তথ্য।
id,orderNumber,pnr,createdAt,admissionNo— এমন মান যেগুলো object-কে সংজ্ঞায়িত করে। পরিবর্তনযোগ্য পরিচয় একটা স্ববিরোধিতা, যেমন পরিবর্তনযোগ্য জন্মতারিখ। - "কে এটা বদলাল?" debugging session। Bug report বলছে order-এর customer ID save করা মানের সাথে মেলে না। Public setter থাকলে program-এর প্রতিটা লাইনই সন্দেহভাজন। Setter সরাও, আর প্রশ্নটাই উঠতে পারে না।
- অভ্যাসবশত auto-generated setter। অনেক class-এ প্রতিটা field-এর জন্য get/set pair আছে শুধু কারণ IDE template এগুলো বানিয়েছে। প্রতিটা অপ্রয়োজনীয় setter class-এর mutable surface কোনো সুবিধা ছাড়াই বাড়িয়ে দেয়। আর
new-এর পরে ছয়টা setter call করার construction ritual আসলে ছদ্মবেশে Long Parameter List সমস্যা: constructor-এর কাজ ছয়টা ভঙ্গুর ধাপে ছড়িয়ে পড়েছে। - নতুন তৈরি parameter object। যখন তুমি
AddressবাDateRange-এর মতো parameter object চালু করে Data Clumps সারাও, সেই object method আর caller-এর মধ্যে শেয়ার হয়। Setter থাকলে একটা method এটা mutate করতে পারে যখন অন্যটা এখনো reference ধরে আছে — classic aliasing bug। Parameter object কলম ছাড়াই জন্মানো উচিত।
কখন এটা দরকার নেই? যখন মান সত্যিকার অর্থে object-এর জীবনে বদলায়। ছাত্রের এই টার্মের নম্বর, delivery-র বর্তমান অবস্থান, খেলার score — এগুলো পেন্সিল মান। এগুলোর জন্য নিয়ন্ত্রিত mutation সঠিক; শুধু naked setter-এর বদলে উদ্দেশ্য-প্রকাশক method ব্যবহার করো। Remove Setting Method শুধু কলমের মানের জন্য।
যদি কোনো সাধারণ business class সততার সাথে audit করো, তাহলে বিভাজন সাধারণত অবাক করে — বেশিরভাগ field কখনোই বৈধভাবে বদলায় না:
অর্ধেকেরও বেশি field পেন্সিলের পোশাক পরা কলম ছিল। আর এক-পঞ্চমাংশ আদৌ stored field হওয়া উচিত ছিল না — এগুলো derived মান (totalWithTax) যেগুলো fact সাজছিল। মাত্র এক-চতুর্থাংশের সত্যিকার mutation path দরকার ছিল।
যেকোনো field সম্পর্কে সিদ্ধান্ত নিতে একটা quick chart — এটা কতটা বদলায় বনাম সিস্টেমের কতটা এর উপর নির্ভর করে:
পড়ার উপায়: orderId "Remove setter now"-তে গভীরে — কখনো বদলায় না, সবকিছু এর উপর নির্ভর করে। status প্রায়ই বদলায় আর সবকিছু এর উপর নির্ভর করে — এটায় mutation path রাখে, কিন্তু guarded, named একটা। একটা draft note যেটা মাত্র একটা screen পড়ে? Plain setter কাউকে কষ্ট দেবে না।
| Field | কলম নাকি পেন্সিল? | সঠিক পথ |
|---|---|---|
admissionNo, pnr, orderId | কলম | শুধু Constructor |
dateOfBirth, createdAt, admittedOn | কলম | শুধু Constructor |
status, currentClass, seat | পেন্সিল | নামযুক্ত method নিয়মসহ (promoteToNextClass()) |
score, feePaid | পেন্সিল | নামযুক্ত method (addPoints(), recordFeePayment()) |
totalWithTax, age | কোনোটাই না — derived | getter-এ compute করো; কিছু store করো না |
🪄 এক নজরে আগে ও পরে
আগে — প্রতিটা field চিরকাল সম্পাদনযোগ্য:
// BEFORE: a railway ticket where everything is changeable, always
class TrainTicket {
pnr = "";
passengerName = "";
trainNo = "";
journeyDate = "";
seat = "";
}
const ticket = new TrainTicket();
ticket.pnr = "8642097531"; // four-step ritual just to be born
ticket.passengerName = "Meera";
ticket.trainNo = "12626";
ticket.journeyDate = "2026-07-15";
// ...weeks later, in some helper:
ticket.pnr = "0000000000"; // the ticket's identity, gone
ticket.journeyDate = "2026-07-99"; // nonsense, silently acceptedপরে — কলমের field তৈরিতে fixed; শুধু সত্যিকার পরিবর্তনশীল মান নিয়ন্ত্রিত পথে রাখা:
// AFTER: ink for identity, a guarded pencil only where life requires it
class TrainTicket {
private _seat: string;
constructor(
readonly pnr: string, // ink
readonly passengerName: string, // ink
readonly trainNo: string, // ink
readonly journeyDate: string, // ink
seat: string,
) {
this._seat = seat; // pencil — but guarded below
}
get seat(): string { return this._seat; }
reallocateSeat(newSeat: string): void { // honest name, one entry point
if (!/^\d{1,2}[A-F]$/.test(newSeat)) throw new Error("Invalid seat");
this._seat = newSeat;
}
}
const ticket = new TrainTicket("8642097531", "Meera", "12626", "2026-07-15", "32B");
// ticket.pnr = "0000000000"; // compile error: read-only
ticket.reallocateSeat("41C"); // allowed, validated, searchableএকসাথে দুটো উন্নতি। Construction হলো setter call-এর ritual-এর বদলে একটা সৎ ধাপ — object তার প্রথম নিঃশ্বাস থেকেই সম্পূর্ণ আর বৈধ। আর একমাত্র পরিবর্তনশীল মান, seat, এখন একটা নামযুক্ত validated দরজা দিয়ে বদলায়।
Object-এর পুরো জীবন, state হিসেবে আঁকা — কলম লেখার ঠিক একটাই মুহূর্ত আছে, আর সেটা জন্মের মুহূর্ত:
🪜 ধাপে ধাপে, নিরাপদ পথে
Setter সরানো এক কীস্ট্রোকের মতো শোনায়। কিন্তু জীবন্ত codebase-এ নিরাপদভাবে করতে সতর্কতা দরকার। এখানে ধীর, নিশ্চিত recipe।
ধাপ ১: নিশ্চিত করো field-টা সত্যিই কলম। Class পড়ো, domain নিয়ে ভাবো। এই মান কি সত্যিই তৈরির পরে fixed, নাকি কোনো বৈধ business flow এটা পরিবর্তন করে? যদি বাস্তব কোনো flow পরিবর্তন করে, থামো — এই refactoring সেই field-এর জন্য না।
ধাপ ২: Setter-এর প্রতিটা caller খোঁজো। Find Usages ব্যবহার করো। ফলাফলগুলো দুই ভাগে ভাগ করো: তৈরির সময় (ঠিক new-এর পরে) call হচ্ছে, আর পরে জীবনে call হচ্ছে। প্রথম ভাগ constructor-এ যাবে। দ্বিতীয় ভাগের প্রতিটা item হয় একটা bug যেটা তুমি এইমাত্র আবিষ্কার করলে, অথবা প্রমাণ যে field-টা আসলে পেন্সিল।
ধাপ ৩: নিশ্চিত করো constructor মান নিতে পারে। যদি এখনো এই মান না নেয়, তাহলে এর জন্য একটা parameter যোগ করো। transition-এর সময়, দুটো পথই থাকে:
// Intermediate state: constructor takes the value, setter still alive
class Account {
private _id: string;
constructor(id: string) {
this._id = id;
}
get id(): string { return this._id; }
set id(value: string) { this._id = value; } // still here — for one more step
}ধাপ ৪: তৈরির সময়ের setter call গুলো একে একে constructor argument-এ migrate করো।
// before:
const a = new Account("");
a.id = "ACC-1009";
// after:
const a = new Account("ACC-1009");প্রতিটা migration-এর পরে compile করো আর test করো। বিরক্তিকর, নিরাপদ, দ্রুত।
ধাপ ৫: Setter মুছে ফেলো আর field read-only করো।
class Account {
constructor(readonly id: string) {}
}এখন compile করো। এটাই সুন্দর মুহূর্ত: compiler তোমার detective হয়ে যায়। Codebase-এর যেকোনো জায়গায় বাকি assignment লাল error হিসেবে জ্বলে ওঠে — সনদে এখনো যেখানে scribbling হচ্ছিল তার সম্পূর্ণ তালিকা। প্রতিটাকে construction-এর মাধ্যমে route করো।
ধাপ ৬: সব test চালাও — বিশেষত framework সংক্রান্ত যেকোনো কিছু। Serializer, ORM, আর model binder কখনো কখনো পেছনে setter দিয়ে object পূরণ করে; পরের callout escape route ব্যাখ্যা করে।
এই পরিশোধ real। একটা team পরিমাপ করেছে এক quarter জুড়ে যখন তারা তাদের core entity থেকে setter সরাচ্ছিল — metric হলো প্রতি মাসে "কে এই মান বদলেছে?" debugging hunt-এ কাটানো ঘণ্টা:
শেষ সরু অংশটা কখনো শূন্যে পৌঁছায় না — পেন্সিল field এখনো আছে। কিন্তু প্রতিটা locked field একটা পুরো bug category চিরতরে অবসর নেয়। যে মান বদলাতে পারে না সেটা ভুলভাবে বদলানো হতে পারে না।
Framework হলো একটাই প্রকৃত ফাঁদ। কিছু ORM, JSON deserializer, আর form binder parameterless constructor প্লাস settable property আশা করে। কোনো persisted বা serialized class-এর setter মুছে ফেলার আগে তোমার framework-এর option চেক করো: EF Core আর System.Text.Json উভয়ই constructor binding সাপোর্ট করে, আর C#-এর init accessor deserialization-কে তৈরির সময় মান সেট করতে দেয় পরবর্তী সব কিছু block করে। সঠিক উত্তর প্রায় কখনোই "public setter রাখো" না; এটা সাধারণত "constructor binding বা init accessor ব্যবহার করো।" আর মনে রেখো: published library-তে public setter সরানো external caller-দের জন্য breaking API change — যথাযথ version bump সহ schedule করো।
🏗️ একটা বড় বাস্তব উদাহরণ
ধরো ঢাকার একটা স্কুল management system ছাত্রের record track করে। মূল class-টা সব পেন্সিল — আর bug গুলো সেটা দেখায়:
// BEFORE: everything editable, forever, by anyone
class StudentRecord {
admissionNo = "";
dateOfBirth = "";
admittedOn = "";
currentClass = 6;
feePaidPaise = 0;
}
// Found scattered across the codebase:
record.dateOfBirth = "2014-03-30"; // "small correction" before sports trials
record.admissionNo = "PNE-NEW-77"; // duplicate-fix script rewrote identities
record.admittedOn = "2026-04-01"; // backdated to dodge a late-admission fee
record.currentClass = 8; // skipped class 7 entirely — typo? fraud?এই প্রতিটা লাইন happily compile হয়েছে। প্রথমটা বলপয়েন্ট কলম হাতে সুমাইয়ার স্পোর্টস কোচ — শুধু এই software-এ কলম আসলে কাজ করে। সিস্টেমে মানুষের মাথায় নিয়ম ছিল — "জন্মতারিখ কখনো বদলায় না", "ভর্তি নম্বর স্থায়ী" — কিন্তু code কোনোটাই enforce করেনি। এখন কলম সরাই:
// AFTER: ink facts locked at admission; life changes go through named doors
class StudentRecord {
private _currentClass: number;
private _feePaidPaise = 0;
constructor(
readonly admissionNo: string, // identity — ink
readonly dateOfBirth: string, // birth fact — ink
readonly admittedOn: string, // history fact — ink
startingClass: number,
) {
if (!/^PNE-\d{4}$/.test(admissionNo)) throw new Error("Bad admission number");
this._currentClass = startingClass;
}
get currentClass(): number { return this._currentClass; }
get feePaidPaise(): number { return this._feePaidPaise; }
promoteToNextClass(): void { // the ONLY way class changes
if (this._currentClass >= 12) throw new Error("Already in final class");
this._currentClass += 1; // always exactly +1 — no skipping
}
recordFeePayment(paise: number): void {
if (paise <= 0) throw new Error("Payment must be positive");
this._feePaidPaise += paise;
}
}প্রতিটা removal কী কিনল তা দেখো:
dateOfBirth,admissionNo,admittedOnএখন তথ্য, variable নয়। Sports trial "সংশোধন" আর identity rewriting script আজ compile error। যদি সনদে সত্যিকার ভুল এন্ট্রি হয়, স্কুল একটা নতুন সংশোধিত record ইস্যু করে — একজন অনুমোদিত মানুষের সচেতন দৃশ্যমান কাজ, ঠিক সালাম সাহেবের অফিসের মতো — নীরবে ইতিহাস overwrite করার বদলে।currentClassপেন্সিল, কিন্তু পেন্সিলটা guarded:promoteToNextClass()ঠিক এক class উপরে যায়। "৭ম শ্রেণি বাদ" bug-টা represent করাই অসম্ভব।- টাকা শুধু
recordFeePayment()-এর মাধ্যমে প্রবাহিত হয়, যেটা বাজে কথা refuse করে।
চূড়ান্ত আকৃতি, class diagram হিসেবে — দরজা গুনে দেখো:
আর এখানে immutability bonus যেটা beginners প্রায়ই মিস করে: এই object এখন শেয়ার করার জন্য নিরাপদ। Reports module, ID card printer, আর fees module সবাই একই StudentRecord ধরে রাখতে পারে কোনো ভয় ছাড়াই। কারণ তাদের কেউই কলমের field গুলো অন্যদের জন্য নষ্ট করতে পারবে না। কোনো defensive copying নেই, কোনো "কে এটা পরিবর্তন করল?" hunt নেই।
College corner — immutability আর thread safety: শেয়ারিং argument জীবন-মৃত্যুর প্রশ্ন হয়ে ওঠে যখন thread আসে। একটা mutable object দুটো thread-এর মধ্যে শেয়ার হলে lock, memory barrier, আর প্রার্থনা দরকার: thread A currentClass পড়তে পারে thread B অর্ধেক লিখতে থাকার সময়। আর modern CPU-তে প্রতিটা core-এর cache mutable field-এর পুরানো copy ধরে রাখতে পারে যদি না synchronisation-এর মূল্য দেওয়া হয়। Immutable object-এর এর কোনোটাই দরকার নেই। যেহেতু constructor-এর পরে কোনো field বদলাতে পারে না, race করার মতো কোনো write নেই — প্রতিটা thread চিরকাল একই মান দেখে, কোনো lock ছাড়াই। এই কারণেই functional language default immutability-তে যায়, কেন Java আর C#-এ String immutable। Concurrency guide বারবার এই mantra repeat করে: immutable data অবাধে share করো; mutable data সাবধানে বা একদম না। একটা subtlety: object-কে safely published হতে হবে — reference share হওয়ার আগে সম্পূর্ণ constructed। "Constructor-এর মাধ্যমে সব মান" এটা বিনামূল্যে দেয়। new প্লাস ছয়টা setter call দিয়ে তৈরি object-এর অর্ধ-নির্মিত visibility-র একটা window আছে; constructor-নির্মিত immutable object-এর নেই।
System ব্যবহারকারীদের জন্য দৈনন্দিন জীবন কেমন দেখায়, কলম সরানোর আগে ও পরে:
⚙️ C#-এ একই refactoring
C# এই refactoring-এর জন্য দারুণ সজ্জিত — immutability-র একটা পুরো সিঁড়ি অফার করে। তুমি ঠিক যে রাং দরকার সেটা বেছে নিতে পারো।
রাং ১: get-only auto-property। Classic Remove Setting Method। Constructor-এ assign করা যায়, অন্য সব জায়গায় locked:
public class Account
{
public string Id { get; } // no setter at all
public Account(string id) => Id = id;
}
// account.Id = "oops"; // CS0200: read-onlyরাং ২: readonly field। Plain field-এর জন্য একই guarantee — compiler শুধু declaration বা constructor-এ assignment permit করে:
public class Account
{
private readonly string _id;
public Account(string id) => _id = id;
}রাং ৩: init-only property (C# 9+)। কখনো কখনো caller-দের friendly object-initializer syntax ব্যবহার করতে দিতে চাও, কিন্তু তৈরির পরে মান lock করতে চাও। set কে init দিয়ে replace করো:
public class StudentRecord
{
public string AdmissionNo { get; init; }
public string DateOfBirth { get; init; }
}
var s = new StudentRecord
{
AdmissionNo = "PNE-1042", // allowed: we are still at the birth moment
DateOfBirth = "2013-08-09",
};
s.DateOfBirth = "2014-03-30"; // CS8852: init-only, object already createdinit হলো "কলম শুধু ইস্যুর দিন পৌরসভা অফিসের ভেতরে থাকে"-র precise legal ভাষা। Deserializer আর object initializer তৈরির সময় মান লিখতে পারে; construction শেষ হওয়ার মুহূর্তে কলমের কালি শুকিয়ে যায়।
রাং ৪: record — এক keyword-এ পুরো-object immutability।
public record TrainTicket(string Pnr, string PassengerName,
string TrainNo, string JourneyDate, string Seat);
var ticket = new TrainTicket("8642097531", "Meera", "12626", "2026-07-15", "32B");
// "Changing" the seat = issuing a fresh certificate, old one untouched:
var moved = ticket with { Seat = "41C" };
Console.WriteLine(ticket.Seat); // 32B — original unharmed
Console.WriteLine(moved.Seat); // 41C — new objectPositional record property গুলো automatically init-only, record গুলো value দিয়ে compare করে, আর with expression চিরকালীন প্রশ্নের উত্তর দেয় "কিন্তু তাহলে কীভাবে বদলাবো?" — বদলাও না; একটা corrected copy তৈরি করো। এই non-destructive update style হলো real certificate যেভাবে কাজ করে ঠিক সেটার মতো: সংশোধনে নতুন করে ইস্যু করা document তৈরি হয়, আর পুরানো ধরে রাখা যে কেউ দেখতে পারে এটা পুরানো।
| রাং | Syntax | কখন লেখা যায়? | সবচেয়ে ভালো জন্য |
|---|---|---|---|
readonly field | private readonly string _id; | Declaration বা constructor | Internal state |
| Get-only property | public string Id { get; } | শুধু Constructor | Public কলমের তথ্য |
| Init-only property | public string Id { get; init; } | Constructor + object initializer | Deserialization-বান্ধব কলম |
| Record | record Ticket(string Pnr, ...) | Construction; with দিয়ে copy | পুরো-object immutability |
Python একই সিঁড়ি তার নিজের dialect-এ অফার করে — frozen dataclass প্রতিটা field lock করে, আর read-only property একটাকে guard করে:
# Python: ink via frozen dataclass; guarded pencil via a normal class
from dataclasses import dataclass
@dataclass(frozen=True)
class TrainTicket:
pnr: str
passenger_name: str
train_no: str
journey_date: str
seat: str
def reallocate_seat(self, new_seat: str) -> "TrainTicket":
# no mutation: return a corrected copy, old ticket untouched
from dataclasses import replace
return replace(self, seat=new_seat)
ticket = TrainTicket("8642097531", "Meera", "12626", "2026-07-15", "32B")
moved = ticket.reallocate_seat("41C")
# ticket.pnr = "000" # FrozenInstanceError — there is no pendataclasses.replace হলো Python-এর with expression: নতুন করে ইস্যু করা ticket, পুরানো অক্ষত।
🧰 IDE সাপোর্ট
Tooling অনেকটা watching-এর কাজ করে তোমার জন্য:
- Visual Studio: built-in code-style rule IDE0044 ("Add readonly modifier") private field গুলো flag করে যেগুলো শুধু constructor-এ assign হয় — IDE নিজেই তোমাকে বলে কোন field গুলো গোপনে কলম। Quick Actions (
Ctrl+.) fix apply করে। Compiler error CS0200 (read-only property) আর CS8852 (init-only পরে তৈরি হলে) তারপর নিয়মটা চিরতরে guard করে। - JetBrains Rider / ReSharper: inspection field গুলোকে
readonlyকরতে আর property গুলোকে get-only বা init-only-তে convert করতে suggest করে যখন পরে কোনো write নেই; 2020.3 release থেকে উভয় tool record আরinitaccessor সম্পূর্ণরূপে বোঝে, class-কে record-এ convert করার quick-fix সহ। Setter-এ Find Usages (Shift+F12/Alt+F7) ধাপ ২-এর দুটো pile সেকেন্ডে দেয়। - IntelliJ IDEA (Java): inspection "field may be
final" Java-র জন্য একই ভূমিকা পালন করে, আর Refactor menu constructor parameter introduce করতে পারে যখন তুমি setter সরাও। - VS Code (TypeScript): কোনো একক automated action নেই, কিন্তু method সহজ: field-এ
readonlyযোগ করো, আর TypeScript compiler-এর error list প্রতিটা illegal write-এর সম্পূর্ণ checklist হয়ে যায়। Setter-এ Find All References দেখায় কোন caller গুলো আগে migrate করবে।
সবসময়ের মতো, IDE mechanics handle করে। কোন field গুলো কলম — সেই judgement তোমার domain-এর মানুষের অংশ।
⚖️ সুবিধা ও ঝুঁকি
| সুবিধা | ঝুঁকি / খরচ |
|---|---|
| "কখনো বদলায় না" compile-time guarantee হয়ে যায়, বিনয়ী অনুরোধ না — ভুল code build-ই হতে পারে না | Parameterless constructor + setter দরকার এমন framework ভেঙে যেতে পারে; আগে constructor binding / init support চেক করো |
| Debugging কমে: একবার set করা মান "কেউ কোথাও পরিবর্তন করেছে" হতে পারে না | যদি মান জীবনে বৈধভাবে বদলায়, setter সরানো একদম ভুল — আগে কলম থেকে পেন্সিল আলাদা করো |
| নিরাপদ sharing: অনেক module, thread, বা cache object ধরে রাখতে পারে কোনো ভয় ছাড়াই | Published library থেকে public setter সরানো external caller-দের জন্য breaking change |
Construction একটা সৎ সম্পূর্ণ ধাপ হয়ে যায় fragile new-plus-setters ritual-এর বদলে | Multi-step initialization হয়তো init accessor বা builder pattern দরকার করে |
| Missing setter কোনো comment-এর চেয়ে ভালো intent document করে — reader তাৎক্ষণিক বুঝতে পারে field fixed | Immutable object আপডেট করা মানে copy তৈরি করা; খুব hot loop-এ allocation খরচ হতে পারে (চিন্তার আগে measure করো) |
🩺 কোন smell এটা সারায়?
| Smell | Remove Setting Method কীভাবে সাহায্য করে |
|---|---|
| সর্বত্র Mutable data | Mutable surface কমায়: কম field বদলাতে পারে, তাই object কম state-এ থাকতে পারে আর ভুল হওয়ার কম পথ আছে |
| Large Class | প্রতিটা মুছে ফেলা setter class-এর public surface ছাঁটে; যা থাকে সেটাই real contract |
| Long Parameter List (ছদ্মবেশে) | new-তারপর-ছয়টা-setter construction ritual-কে একটা সম্পূর্ণ constructor দিয়ে replace করে — initialization caller-এ ছড়ানো বন্ধ হয় |
| Data Clumps | নতুন চালু করা parameter object বিশ্বাসযোগ্য থাকে: বৈধভাবে জন্মায়, নিরাপদে শেয়ার হয়, aliasing বিস্ময় থেকে মুক্ত |
| Data Class | Reflexive get/set pair কাটলে প্রশ্নটা জোর করে আসে "এই class আসলে কী করে?" — behavior raw access replace করতে শুরু করে |
পুরো locking-down toolkit, একটা map-এ — Remove Setting Method হলো Hide Method আর Encapsulate Collection-ও অন্তর্ভুক্ত একটা বড় পরিবারের একটি শাখা:
📝 Quick revision box
+=================================================================+
| REMOVE SETTING METHOD — REVISION CARD |
+=================================================================+
| SMELL SIGN : setter on a field that should be fixed at birth |
| (id, orderNo, pnr, createdAt, dateOfBirth) |
| PICTURE : birth certificate — written ONCE, no pen attached |
+-----------------------------------------------------------------+
| THE MOVE : 1. Confirm the field is INK, not pencil |
| 2. Find Usages -> two piles: at-birth vs later |
| 3. Constructor receives the value |
| 4. Migrate at-birth setter calls into constructor |
| 5. DELETE setter; make field readonly |
| -> compiler lists every illegal write for you |
| 6. Test, incl. serializers / ORMs |
+-----------------------------------------------------------------+
| C# LADDER : readonly field -> { get; } -> { get; init; } |
| -> record (+ with-expressions for changed copies) |
| REMEMBER : "change" an immutable object = issue a NEW one; |
| pencil values get named methods, never raw setters |
+=================================================================+🏋️ অনুশীলন exercise
ধরো ঢাকার একটা food delivery app-এ এই class আছে:
class Order {
orderId = "";
customerId = "";
placedAt = "";
restaurantId = "";
status = "PLACED";
deliveryBoyId = "";
totalPaise = 0;
}
// Found around the codebase:
order.orderId = "ORD-" + Math.random(); // set after new Order()
order.placedAt = new Date().toISOString(); // also set after construction
order.status = "DELIVERED"; // set directly from 5 places
order.totalPaise = order.totalPaise - 500; // "discount" applied by mutation
order.customerId = "CUST-ADMIN"; // a support tool "fixing" dataতোমার কাজ:
- সাতটা field-কে কলম ও পেন্সিলে ভাগ করো। প্রতিটা কলমের field-এর জন্য এক লাইনে বলো কেন order তৈরির পরে এটা কখনো বদলানো উচিত না। (প্রতিটা field place করতে চিত্র ৪ quadrant chart ব্যবহার করো।)
- কলমের field গুলোর জন্য Remove Setting Method ধাপে ধাপে apply করো: constructor-এর মাধ্যমে মান route করো, creation-time assignment গুলো migrate করো, তারপর field গুলো
readonlymark করো। প্রতিটা ধাপে code compile হতে রাখো। statusপেন্সিল — কিন্তু পাঁচ জায়গা থেকে raw assignment বিপজ্জনক।accept(),pickUp(deliveryBoyId), আরmarkDelivered()method দিয়ে replace করো, প্রতিটা পরীক্ষা করে যে move-টা legal (যেমন, pickup-এর আগে order deliver হতে পারে না)। এখনdeliveryBoyIdকোথায় লেখা হয়? Allowed move গুলো চিত্র ৫-এর মতো state diagram হিসেবে আঁকো।- "discount by mutation" লাইনটা bug factory।
totalPaiseএমনভাবে redesign করো যাতে total construction-এ fixed থাকে আর discountwithDiscount(paise)method-এর মাধ্যমে একটা নতুনOrderreturn করে। পুরানো object এখনো ধরে থাকা code-এর কী হবে — আর কেন এটা ভালো জিনিস? - Bonus (C#):
Orderকে record হিসেবে লিখো কলমের field positional আরStatusmethod বা non-destructivewithupdate-এর মাধ্যমে handle করা। একটাwithexpression action-এ দেখাও। Bonus (Python):dataclasses.replaceব্যবহার করে frozen dataclass হিসেবে একই লিখো। - College-corner প্রশ্ন: দুটো background thread একই
Orderশেয়ার করছে — একটা invoice print করছে, অন্যটা SMS update পাঠাচ্ছে। তিন বাক্যে ব্যাখ্যা করো কেন তোমার refactored design-এ কলমের field গুলোর জন্য কোনো lock দরকার নেই, আরstatus-এর জন্য এখনো কী যত্ন নিতে হবে। - Reflection প্রশ্ন: তোমার analytics team বলছে তারা timezone migration-এর জন্য পুরানো order-এর
placedAt"ঠিক" করতে চায়। Setter কি আবার যোগ করা উচিত? সৎ বিকল্প কী — আর সালাম সাহেবের অফিস কী করত?
সচরাচর জিজ্ঞাসা
- Object immutable হলে এবং setter না থাকলে value কীভাবে বদলাবো?
- পুরানো object edit করার দরকার নেই — নতুন value সহ একটা নতুন object বানাও। সংশোধিত জন্মনিবন্ধন সনদ মানে পুরানো কাগজে কাটাকাটি না, বরং নতুন করে ইস্যু করা সনদ। TypeScript-এ withSeat(newSeat)-এর মতো একটা method লিখো যেটা নতুন object return করে; C#-এ record থাকলে এটা বিনামূল্যে পাওয়া যায় with-expression দিয়ে: ticket with { Seat = "42A" }। পুরানো object অপরিবর্তিত থাকে, তাই যে কেউ সেটা ধরে আছে সে consistent data দেখবে।
- আমার ORM বা JSON serializer-এর setter দরকার হয় object-এ value ভরতে। সেগুলো কি public রাখতেই হবে?
- বেশিরভাগ ক্ষেত্রে না। আধুনিক framework আরও ভালো option সাপোর্ট করে: Entity Framework Core আর System.Text.Json উভয়ই constructor binding ব্যবহার করতে পারে, আর C#-এর init accessor deserializer-কে object তৈরির সময় property set করতে দেয় কিন্তু পরে আর পরিবর্তনের সুযোগ দেয় না। হার মানার আগে তোমার framework-এর documentation চেক করো — private setter বা init accessor প্রায় সবসময়ই framework-এর চাহিদা পূরণ করে, পুরো দুনিয়ার কাছে field খুলে না দিয়ে।
- সব class-এর সব setter কি সরিয়ে দেওয়া উচিত?
- না। Setter শুধু তখনই সরাও যখন field-টা সত্যিকার অর্থে তৈরির পরে আর বদলানো উচিত না — পরিচয়, তৈরির সময়, মূল সংজ্ঞামূলক মান। এমন field যেটা object-এর জীবনে স্বাভাবিকভাবে বদলায়, যেমন player-এর score বা order-এর status, সেটায় নিয়ন্ত্রিত পরিবর্তনের ব্যবস্থা রাখা উচিত — raw setter-এর বদলে addPoints()-এর মতো উদ্দেশ্য-প্রকাশক method দিয়ে। দক্ষতা হলো কলমে লেখা field আর পেন্সিলে লেখা field আলাদা করতে পারা।
- C#-এ readonly, get-only, init, আর record-এর মধ্যে পার্থক্য কী?
- একই ধারণার চারটা স্তর। readonly field শুধু constructor-এ assign করা যায়। get-only auto-property ({ get; }) হলো সেটার property সংস্করণ। init accessor ({ get; init; }) অতিরিক্তভাবে তৈরির সময় object initializer-এ set করতে দেয়, তারপর lock হয়ে যায়। record সব positional property-তে একসাথে init-only apply করে আর value equality ও with-expression যোগ করে — এক keyword-এ পুরো object immutability।
- Immutable object কেন সহজ আর নিরাপদ?
- তিনটা সহজ কারণ। প্রথমত, কোনো চমক নেই: একবার যা check করা হয়েছে তা check করাই থাকে — কেউ পেছন থেকে বদলায়নি, তাই কে এটা পরিবর্তন করল খোঁজার debugging একদম উধাও হয়ে যায়। দ্বিতীয়ত, নিরাপদ sharing: একই object দশটা module, দশটা thread, বা একটা cache-এ দিতে পারো কোনো ভয় ছাড়াই, কারণ কেউ অন্যদের জন্য এটা নষ্ট করতে পারবে না। তৃতীয়ত, সৎ code: setter না থাকাটা প্রতিটা ভবিষ্যৎ পাঠককে বলে এটা fixed, যেকোনো comment-এর চেয়ে বেশি নির্ভরযোগ্যভাবে।
আরো দেখো
সম্পর্কিত পাঠ
Long Parameter List: দশটা নির্দেশনার চায়ের অর্ডার
Long Parameter List কোড স্মেল সহজ ভাষায় — কেন বেশি argument-এর method বাগ তৈরি করে, আর কীভাবে parameter object দিয়ে call ছোট, পরিষ্কার আর নিরাপদ করা যায়।
Data Clumps: যে বন্ধুরা সবসময় একসাথে ঘোরে
শিক্ষার্থীদের জন্য Data Clumps code smell — শেখো কীভাবে সবসময় একসাথে চলা value-এর গ্রুপ চেনা যায় আর সেগুলোকে একটা class-এ bundle করা যায়, ঠিক যেমন একটা student ID card।
Encapsulate Field: Object যেন নিজের ডেটা নিজে পাহারা দেয়
Encapsulate Field কী সেটা সহজ ভাষায় — কেন public field যেকোনো কোডকে object-এর ডেটা নষ্ট করতে দেয়, আর কীভাবে private field সাথে getter-setter দিয়ে object নিজেই সব নিয়ন্ত্রণ করে।
Hide Method: গোপন মশলা রান্নাঘরেই থাকে
Hide Method সহজ ভাষায় — যে method শুধু class নিজেই ব্যবহার করে সেটা public মেনুতে রাখা উচিত না কেন, আর visibility কমিয়ে private বা internal করলে API ছোট হয়, ভেতরের জিনিস সুরক্ষিত থাকে, আর ভয় ছাড়াই code বদলানো যায়।