Encapsulate Collection: লাইভ লিস্ট বাইরে দেওয়া বন্ধ করো
Encapsulate Collection সহজ ভাষায় — কেন live array বা list return করলে যেকেউ তোমার object নষ্ট করে দিতে পারে, আর কীভাবে read-only view আর add/remove method দিয়ে নিয়ন্ত্রণ ফিরিয়ে আনা যায়।
হাজিরা খাতাটা টেবিলে খোলা পড়ে আছে
ধরো তোমাদের স্কুলের ক্লাস সেভেন-বির হাজিরা খাতার কথা। অবহেলার দুনিয়ায়, খাতাটা সারাদিন সামনের টেবিলে খোলা পড়ে থাকে — পাশে কলমও রাখা। যে ছাত্র পাশ দিয়ে যায়, সে কলম তুলতে পারে। রুবেল তার অনুপস্থিত বন্ধুর নামে সুন্দর করে টিক দেয়। কেউ মঙ্গলবারের পাতা ছিঁড়ে কাগজের প্লেন বানায়। একজন দুষ্টু ভর্তির কলামে "ক্রিকেটার ১" লেখে, তিন দিন পর তার নিচে "ক্রিকেটার ২" লেখে — কারণ একজন কিংবদন্তিতে মন ভরে না। মাসের শেষে খাতা বলছে ক্লাসে ৬৭ জন ছাত্র; ক্লাসরুমে ৪২টা চেয়ার। পরিদর্শক এলে কেউই — এমনকি যারা কখনো খাতা ছোঁয়নি তারাও — বলতে পারে না কোন entry-গুলো সত্যি।
নাসরিন ম্যাডাম অন্যভাবে কাজ করেন। খাতাটা তার কাছেই থাকে, তার ড্রয়ারে। কোনো নতুন ছাত্র ভর্তি হলে, অফিস একটা ভর্তির স্লিপ পাঠায়, তিনি নিয়মের বিরুদ্ধে যাচাই করেন — নাম ঠিকমতো লেখা কিনা, আগে থেকে ভর্তি নেই কিনা, ৪২ সিটের সীমা পেরোয়নি কিনা — আর তিনিই নামটা লেখেন। কোনো ছাত্র চলে গেলে, তিনি ট্রান্সফার সার্টিফিকেট যাচাই করে নামে কাটা দেন। প্রতিটা পরিবর্তন একজোড়া সতর্ক হাতের মধ্য দিয়ে যায়, আর প্রতিটা পরিবর্তন নিয়ম মানে।
আর যখন জামাল স্যার, পিটি শিক্ষক, টিম সিলেকশনের জন্য ছাত্রদের তালিকা চান? নাসরিন ম্যাডাম খাতাটা দেন না — তিনি একটা ফটোকপি দেন। তিনি প্রতিটা নাম পড়তে পারেন, গুনতে পারেন, সাজাতে পারেন, পছন্দের নামে লাল কলমে দাগ দিতে পারেন — আর ফটোকপিতে যা-ই করুন না কেন, আসল খাতার একটা অক্ষরও বদলায় না। পড়ার ক্ষমতা: পুরো। লেখার ক্ষমতা: শূন্য। এটাই সেই বিভাজন যেটা জামাল স্যারের দরকার।
এখন খাতা সবসময় সঠিক, কারণ শুধু একজন মানুষ এটা বদলায়, আর সেই মানুষ নিয়ম মানে।
Software object-গুলোরও এরকম খাতা থাকে — orders, courses, players, marks-এর array আর লিস্ট। আর object-oriented code-এ সবচেয়ে সাধারণ ভুল হলো ঠিক এই খোলা খাতা: একটা getter যেটা live collection বাইরে দিয়ে দেয় যেকেউ লিখুক। যে refactoring নাসরিন ম্যাডামকে দায়িত্বে বসায় সেটার নাম Encapsulate Collection।
Encapsulate Collection কী?
Encapsulate Collection হলো Encapsulate Field-এর collection সংস্করণ। এটা বলে: একটা class যখন একটা collection-এর মালিক, তখন বাইরের কাউকে আসল collection স্পর্শ করতে দিও না। বরং:
১. getter একটা read-only view বা copy return করে — খাতা না, ফটোকপি। ২. class স্পষ্ট add আর remove method দেয় — ভর্তির স্লিপ আর ট্রান্সফার সার্টিফিকেট — আর সব সদস্যপদ পরিবর্তন সেগুলোর মধ্য দিয়ে যায়। ৩. পাইকারি setter সরিয়ে দেওয়া হয় — কেউ পুরো খাতা অন্য নোটবুক দিয়ে বদলে দিতে পারে না। (এটা Remove Setting Method টিমে যোগ দেওয়া।)
আমরা আগেই Encapsulate Field শিখেছি, তাহলে আলাদা refactoring কেন দরকার? কারণ collection-এর একটা চালাক বৈশিষ্ট্য আছে: field-কে private করাটা যথেষ্ট না। ধরো courses private আর শুধু একটা getter আছে:
class Student {
private courses: Course[] = [];
getCourses(): Course[] {
return this.courses; // private field... but the LIVE array escapes!
}
}Field টা private, getter দেখতে নিরীহ — তবুও যেকোনো caller student.getCourses().push(anything) বা student.getCourses().length = 0 লিখে student-এর ভেতরটা বদলে দিতে পারে, student জানতেও পারবে না। Fowler-এর ২য় সংস্করণের catalog-এ ঠিক এই ফাঁদটাই বলা হয়েছে: getter যদি collection নিজেই return করে, তাহলে মালিক object-এর পেছনে collection বদলানো যায়, আর encapsulation-এর সুবিধা চুপচাপ উবে যায়। সাধারণ value-র সাথে value return করলে caller একটা তথ্যের কপি পায়; collection-এর সাথে reference return করলে caller তোমার বাড়ির চাবি পায়।
কলেজ কর্নার — defensive copy আর read-only view: নাসরিন ম্যাডামের ফটোকপির academic নাম হলো defensive copy, আর সস্তা বিকল্প হলো read-only view। দুটো একই aliasing সমস্যা ভিন্নভাবে সমাধান করে। Aliasing মানে একটা object-এর দুটো নাম: getter live লিস্ট return করলে, caller-এর variable আর তোমার private field হলো alias — একটা দিয়ে যেকোনো mutation অন্যটা দিয়েও দেখা যায়, কোনো event নেই, কোনো log নেই, কোনো সতর্কবার্তা নেই। defensive copy alias সম্পূর্ণভাবে ভাঙে: caller একটা নতুন লিস্ট পায়, প্রতিটা call-এ O(n) সময় আর memory খরচ হয়, আর caller একটা জমাট snapshot দেখে যেটা পরে team বদলালেও বদলাবে না। read-only view পড়ার জন্য alias রাখে কিন্তু mutating operation বাদ দেয়: তৈরি করতে O(1) সময় লাগে, সবসময় বর্তমান অবস্থা দেখায়, আর লেখা প্রত্যাখ্যান করে — compile time-এ যদি type system সাহায্য করে (TypeScript-এ ReadonlyArray, C#-এ IReadOnlyList), অথবা runtime-এ (Java-তে Collections.unmodifiableList UnsupportedOperationException throw করে)। trade-off টেবিলটা মুখস্থ করো: copy = বিচ্ছিন্ন কিন্তু ব্যয়বহুল আর পুরনো; view = সস্তা আর আপডেট কিন্তু পড়ার জন্য এখনো aliased, তাই owner mutate করার সময় কেউ view iterate করলে মাঝপথে পরিবর্তন দেখতে পারে (Java এমনকি ConcurrentModificationException throw করবে)। আর কোনোটাই element-গুলো রক্ষা করে না — সেটার জন্য immutable element type দরকার।
| কৌশল | প্রতিটা call-এ খরচ | পরের পরিবর্তন দেখায়? | Mutation আটকানো | Element রক্ষা |
|---|---|---|---|---|
| Live reference (bug) | বিনামূল্যে | হ্যাঁ | কিছুই আটকানো নেই | নেই |
| Defensive copy | প্রতিটা call-এ O(n) | না — জমাট snapshot | সম্পূর্ণ — caller আলাদা লিস্ট পায় | নেই — একই element object |
| Read-only view | O(1) | হ্যাঁ — সবসময় আপডেট | Compile time বা runtime-এ | নেই — একই element object |
| Immutable element-এর copy | O(n) | না | সম্পূর্ণ | সম্পূর্ণ — deep safety |
mutable collection-এর reference হলো data না — এটা ক্ষমতা। তোমার getter live লিস্ট return করলে, তুমি তথ্য share করছো না, তুমি খাতা লেখার কলম share করছো। ফটোকপি (copy বা read-only view) দাও; কলম নিজের কাছে রাখো।
কখন এটা দরকার?
এই লক্ষণগুলো দেখো:
১. Getter সরাসরি internal collection return করছে। return this.items; হলো খোলা খাতা। প্রায় প্রতিটা codebase-এ কয়েকটা এরকম আছে।
২. Caller getter-এর মাধ্যমে mutate করছে। .getCourses().add(, .getItems().push(, .getOrders().clear() খোঁজে পাওয়া মানে confirmed emergency, শুধু smell না।
৩. Collection-এর জন্য bulk setter আছে। setCourses(list) দিয়ে caller এমন একটা লিস্ট install করতে পারে যা তোমার class কখনো যাচাই করেনি — আর caller হয়তো নিজের reference ধরে রেখে পরে mutate করবে, তোমার "validation"-এর পরেও।
৪. Collection সম্পর্কে নিয়ম যেগুলো বারবার ভাঙছে। "একজন ছাত্র সর্বোচ্চ ছয়টা course নিতে পারবে।" "Cart-এ কোনো duplicate item নেই।" "একটা order-এ অন্তত একটা line থাকতে হবে।" এসব নিয়ম থাকলেও class-এর বাইরে mutation হলে নিয়মগুলো শুধু সাজসজ্জা।
৫. Derived data পুরনো হয়ে যাচ্ছে। Class totalMarks cache করে কিন্তু marks লিস্ট পেছন থেকে বদলায়, তাই cache মিথ্যা বলে। Class যখন প্রতিটা পরিবর্তন দেখতে পায়, তখন derived data সঠিক রাখতে পারে।
কখন পুরো আনুষ্ঠানিকতার দরকার নেই: একটা ছোট private helper class যেটা এক ফাইলে ব্যবহার হয়, অথবা এমন কোনো value যেটা সত্যিই শুধু দুটো function-এর মধ্যে পাস হওয়া একটা স্বচ্ছ data bundle। Encapsulation হলো নিয়মওয়ালা জিনিসের জন্য guard; খালি করিডোরে guard বসিও না।
একটা team যখন তাদের codebase-এ leaked getPlayers()-style getter-এ প্রতিটা access grep করল, ফলাফল চমকে দেওয়ার মতো ছিল:
সত্তর শতাংশ caller শুধু ফটোকপি চেয়েছিল — তারা refactoring-এ কিছু হারায় না। কিন্তু প্রায় এক-তৃতীয়াংশ লিখছিল, তাদের মধ্যে অর্ধেক না জেনেই (একটা in-place sort() "শুধু দেখানোর জন্য" তবুও একটা write)। এগুলোই ক্লাস সেভেন-বির খাতায় সেই entry যেগুলো কেউ মনে রাখে না।
কোন collection আগে encapsulate করবে তার অগ্রাধিকার — collection-এ কতটা নিয়ম আছে আর কতজন বাইরের লেখক আছে তার ভিত্তিতে:
পড়ার নিয়ম: team-এর players লিস্ট — কঠোর নিয়ম (সর্বোচ্চ ১১, কোনো duplicate নেই) আর অনেক অসতর্ক লেখক — এটা "আজই encapsulate করো" emergency। একটা log buffer-এ অনেক লেখক কিন্তু কোনো real invariant নেই; append-only helper API যথেষ্ট। একটা সাময়িক local array-এর কিছুই দরকার নেই।
একনজরে আগে আর পরে
আগে — code-এ খোলা খাতা:
// BEFORE: the live array escapes
class ClassRegister {
private students: string[] = [];
getStudents(): string[] {
return this.students; // hands out the real register
}
setStudents(students: string[]): void {
this.students = students; // swaps the whole register!
}
}
const register = new ClassRegister();
// Anywhere in the program — none of this asks the teacher:
register.getStudents().push("Virat Kohli"); // prank admission
register.getStudents().length = 0; // entire class erased
register.setStudents(["Ramesh"]); // register swapped wholesaleপরে — নাসরিন ম্যাডাম দায়িত্ব নেন:
// AFTER: read-only photocopies out, controlled changes in
class ClassRegister {
private readonly students: string[] = [];
private static readonly MAX_STRENGTH = 42;
// The photocopy: callers may read, never write
getStudents(): ReadonlyArray<string> {
return [...this.students]; // copy + readonly type
}
addStudent(name: string): void {
if (!name.trim()) throw new Error("Name required");
if (this.students.includes(name)) throw new Error("Already enrolled");
if (this.students.length >= ClassRegister.MAX_STRENGTH) {
throw new Error("Class is full (42 students)");
}
this.students.push(name);
}
removeStudent(name: string): void {
const i = this.students.indexOf(name);
if (i === -1) throw new Error(`${name} is not in this class`);
this.students.splice(i, 1);
}
}
const register = new ClassRegister();
register.addStudent("Asha"); // through the teacher: ok
// register.getStudents().push("Virat"); // compile error: readonly
// register.setStudents([...]) // gone: no bulk swap existsতিনটা পরিবর্তন, প্রতিটা ফাঁকফোকরের জন্য একটা: getter এখন ReadonlyArray typed copy return করে, সদস্যপদ পরিবর্তন addStudent/removeStudent-এর মধ্য দিয়ে যায় যেখানে নিয়মগুলো থাকে, আর bulk setter মুছে দেওয়া হয়েছে।
ক্লাস সেভেন-বির এক মাসের জীবন, দুটো জগতে:
ধাপে ধাপে, নিরাপদ পথে
এখানে ধাপের ক্রমটা অন্য যেকোনো সময়ের চেয়ে বেশি গুরুত্বপূর্ণ। আমরা পুরনোটা বন্ধ করার আগেই নতুন দরজা যোগ করি, যাতে building-এ সবসময় একটা প্রবেশপথ থাকে।
ধাপ ১: add আর remove method যোগ করো, domain-এর সাথে যে validation মিলে সেটা দিয়ে। এখন আর কিছু বদলায় না; পুরনো আর নতুন access একসাথে থাকে।
addStudent(name: string): void { /* rules + this.students.push(name) */ }
removeStudent(name: string): void { /* rules + splice */ }ধাপ ২: getter-এর মাধ্যমে mutate করা প্রতিটা caller খুঁজে বের করো। Codebase-এ .getStudents().push, .getStudents().splice, getStudents()[0] = ... এর মতো pattern খোঁজো। প্রতিটাকে নতুন method-এ redirect করো, প্রতিটা পরিবর্তনের পর compile করো আর test করো।
// before: register.getStudents().push(newName);
// after: register.addStudent(newName);ধাপ ৩: bulk setter সরিয়ে দাও। setStudents(...) এর caller খোঁজো। সাধারণত এগুলো initialization code — সেটা constructor-এ নিয়ে যাও, যেটা নিজের private array-তে data copy করে:
constructor(initialStudents: string[] = []) {
for (const name of initialStudents) this.addStudent(name); // rules apply even at birth
}কৌশলটা লক্ষ্য করো: constructor initial data-কে addStudent-এর মধ্য দিয়ে দেয়, তাই প্রথম দিনের data-ও নিয়ম মানে।
ধাপ ৪: getter পরিবর্তন করো read-only view বা copy return করতে। এটা দরজার তালা। TypeScript-এ, return type-ও ReadonlyArray<string>-এ tighten করো যাতে mutation attempt compile time-এ fail করে, runtime-এ না।
ধাপ ৫: backing field non-reassignable করো (TypeScript-এ private readonly, Java-তে private final, C#-এ private readonly) যাতে class-এর ভেতরের code-ও ভুলে array বদলাতে না পারে।
ধাপ ৬: compile করো, সব test চালাও, আর আরেকবার খোঁজো এমন কেউ বেঁচে আছে কিনা যে এখনো collection কে খোঁচা দিচ্ছে। শূন্য hit মানে খাতা এখন নিরাপদে শিক্ষকের ড্রয়ারে।
Collection টি পথের মধ্যে স্পষ্ট state পার করে — আর নিরাপদ পথ সবসময় পুরনোটা বন্ধ করার আগে দরজা যোগ করে:
আর পরিমাপ করা ফল: একটা team "রহস্যময় corruption" bug ট্র্যাক করেছিল — হারানো item, অসম্ভব count, পুরনো cache — চারটা migration milestone জুড়ে:
Line টা শুধুমাত্র শেষ ধাপে শূন্য ছোঁয় — কারণ যতক্ষণ live reference যেকোনো জায়গায় বের হচ্ছে, কোথাও একটা অসতর্ক sort() সব কিছু উলটে দিতে পারে।
এই ক্রমে ধাপগুলো করো — method আগে, lock শেষে। getter read-only করার আগে যদি caller-রা এখনো এর মাধ্যমে mutate করছে, তুমি একসাথে অনেক জায়গায় program ভাঙবে আর চাপের মধ্যে সব ঠিক করতে হবে। আগে addStudent/removeStudent যোগ করলে প্রতিটা migration ধাপ ছোট, ঐচ্ছিক মনে হওয়া, আর আলাদাভাবে test করার যোগ্য হয়। এই bug-এর নীরব সংস্করণ সম্পর্কেও সতর্ক থাকো: TypeScript-এর readonly type ছাড়া JavaScript-এ, copy return করলে caller-এর copy-তে push করা নীরবে fail করে — program crash করে না, data শুধু কখনো পৌঁছায় না। Migration-এর পরে পুরনো mutation pattern grep করো; crash-এর উপর নির্ভর করো না।
একটা বড় বাস্তব জীবনের উদাহরণ
ধরো একটা ক্রিকেট একাডেমি app টিম সিলেকশন manage করে। এখানে "আগের" অবস্থা, classic ভুল সহ — দেখো কীভাবে একটা সম্পূর্ণ নিরীহ দেখতে helper function টিমকে corrupt করে:
// BEFORE
class Team {
private players: string[] = [];
public maxSize = 11;
getPlayers(): string[] {
return this.players; // THE CLASSIC MISTAKE: live array out
}
}
// Somewhere far away, a helper that "just sorts for display":
function showTeamSorted(team: Team): void {
const list = team.getPlayers();
list.sort(); // OOPS: sort() mutates IN PLACE...
console.log(list.join(", ")); // ...the TEAM's real order is now changed
}
// And a careless selector:
function pickOpeners(team: Team): string[] {
return team.getPlayers().splice(0, 2); // splice REMOVES them from the team!
}
const team = new Team();
team.getPlayers().push("Rohit", "Ishan", "Surya", "Hardik");
pickOpeners(team); // meant to read 2 names...
console.log(team.getPlayers()); // ...["Surya", "Hardik"] — two players VANISHEDএকটু ভাবো — এখানে কেউ দুষ্ট ছিল না। sort() developer একটা sorted display চেয়েছিল। splice developer ভেবেছিল এটা "শুধু প্রথম দুটো নেয়"। কিন্তু getter live array বাইরে দিয়েছিল বলে, প্রতিটা ছোট ভুল বোঝাবুঝি টিমেরই corruption হয়ে গেল। এই কারণেই — evil hacker নয় — Encapsulate Collection আছে: এটা teammates-দের সৎ ভুল থেকে রক্ষা করে। রুবেল কলম নিয়ে অন্তত চুরি করতে চেষ্টা করছিল; sort() developer সত্যিই সাহায্য করতে গিয়ে খাতা corrupt করল।
Refactoring-এর পরে:
// AFTER
class Team {
private readonly players: string[] = [];
private static readonly MAX_SIZE = 11;
constructor(initialPlayers: string[] = []) {
for (const p of initialPlayers) this.addPlayer(p);
}
// Photocopy out: a fresh copy, typed read-only
getPlayers(): ReadonlyArray<string> {
return [...this.players];
}
get size(): number {
return this.players.length;
}
addPlayer(name: string): void {
if (!name.trim()) throw new Error("Player name required");
if (this.players.includes(name)) throw new Error(`${name} already in team`);
if (this.players.length >= Team.MAX_SIZE) {
throw new Error("Team is full: 11 players");
}
this.players.push(name);
}
removePlayer(name: string): void {
const i = this.players.indexOf(name);
if (i === -1) throw new Error(`${name} is not in the team`);
this.players.splice(i, 1);
}
}
// The helpers now CANNOT do damage:
function showTeamSorted(team: Team): void {
const sorted = [...team.getPlayers()].sort(); // sorts its OWN copy
console.log(sorted.join(", "));
}
function pickOpeners(team: Team): ReadonlyArray<string> {
return team.getPlayers().slice(0, 2); // slice reads; splice would not compile
}
const team = new Team(["Rohit", "Ishan", "Surya", "Hardik"]);
pickOpeners(team);
console.log(team.size); // 4 — nobody vanishedReadonlyArray<string> return type বিশেষ প্রশংসার দাবিদার: TypeScript শুধু type থেকে push, splice, sort, আর pop সরিয়ে দেয়। অসতর্ক splice আর runtime disaster নয় — code save হওয়ার আগেই এটা একটা লাল squiggle।
সম্পূর্ণ design, class diagram হিসেবে — লক্ষ্য করো কোনো public member raw mutable লিস্ট expose করে না:
C#-এ একই refactoring
C# এর জন্য first-class tool আছে, আর সেগুলো নাম করে শেখার মতো। "আগের" ভুলটা এরকম দেখতে:
// BEFORE: both holes wide open
public class Team
{
public List<string> Players { get; set; } = new(); // live list AND bulk setter
}
// anywhere:
team.Players.Clear(); // whole team gone
team.Players = someoneElsesList; // backing store swappedEncapsulated সংস্করণ — IReadOnlyList<string> return type-এ মনোযোগ দাও, .NET idiom ফটোকপির জন্য:
// AFTER
public class Team
{
private readonly List<string> _players = new();
private const int MaxSize = 11;
// Read-only view: cheap, always current, no mutation API at all
public IReadOnlyList<string> Players => _players.AsReadOnly();
public int Size => _players.Count;
public void AddPlayer(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Player name required", nameof(name));
if (_players.Contains(name))
throw new InvalidOperationException($"{name} already in team");
if (_players.Count >= MaxSize)
throw new InvalidOperationException("Team is full: 11 players");
_players.Add(name);
}
public void RemovePlayer(string name)
{
if (!_players.Remove(name))
throw new InvalidOperationException($"{name} is not in the team");
}
}
// Callers can read and iterate freely:
foreach (var p in team.Players) Console.WriteLine(p);
var openers = team.Players.Take(2).ToList();
// But mutation does not even compile:
// team.Players.Add("X"); // error: IReadOnlyList has no Add
// team.Players.Clear(); // error: no Clear
// team.Players = newList; // error: no setterতিনটা C# বিষয় জোর দিয়ে বলার মতো:
_players.AsReadOnly()লিস্টটাকেReadOnlyCollection<string>-এ wrap করে — একটা view, copy নয়। এটা সস্তা, team বদলালে আপডেট থাকে, আর কেউ কোনোভাবে mutating member-এ পৌঁছাতে চাইলে throw করে।- কেন property-কে
IEnumerable<string>type করে_playersreturn করি না? কারণ একজন দৃঢ়প্রতিজ্ঞ (বা confused) caller cast করে ফিরিয়ে আনতে পারে:((List<string>)team.Players).Clear()।AsReadOnly()wrapper cast attempt বেঁচে যায়; naked লিস্ট বাঁচে না। - এই pattern হলো domain entity-তে collection model করার standard পদ্ধতি। Jimmy Bogard-এর "Domain-Driven Refactoring" series DDD aggregate-এর জন্য ঠিক এই shape ব্যবহার করে, আর EF Core সরাসরি এটা support করে: ORM private
_playersbacking field populate করতে পারে যখন public surfaceIReadOnlyListথাকে।
Java-তে, একই কাজ করে Collections.unmodifiableList(players) — একটা read-only wrapper যেটা mutation attempt-এ UnsupportedOperationException throw করে; Java 10 থেকে, List.copyOf(players) একটা immutable snapshot দেয়।
Python-এ compiler নেই ড্রয়ার lock করার জন্য, কিন্তু একটা চমৎকার কৌশল আছে: tuple return করো, যার কোনো mutating method-ই নেই:
# Python: photocopy as a tuple — there is no append on a tuple
class Team:
MAX_SIZE = 11
def __init__(self, initial: list[str] | None = None) -> None:
self._players: list[str] = []
for name in (initial or []):
self.add_player(name) # rules apply even at birth
@property
def players(self) -> tuple[str, ...]:
return tuple(self._players) # snapshot with zero mutators
def add_player(self, name: str) -> None:
if not name.strip():
raise ValueError("Player name required")
if name in self._players:
raise ValueError(f"{name} already in team")
if len(self._players) >= Team.MAX_SIZE:
raise ValueError("Team is full: 11 players")
self._players.append(name)
def remove_player(self, name: str) -> None:
try:
self._players.remove(name)
except ValueError:
raise ValueError(f"{name} is not in the team")
team = Team(["Rohit", "Ishan"])
# team.players.append("X") # AttributeError: tuple has no appendtuple return একসাথে copy আর mutation-proof — Python-এর ফটোকপি মেশিন আর locked ড্রয়ার এক চালে।
IDE support
Encapsulate Collection-এর বেশিরভাগ IDE-তে একটা নির্দিষ্ট button নেই — এটা একটা composite refactoring — কিন্তু টুকরোগুলো ভালোভাবে support করে:
- JetBrains Rider / ReSharper: field-এ Encapsulate Field refactoring (
Ctrl+Shift+R) প্রথম ধাপ হিসেবে backing field-এর উপর property তৈরি করে; Find Usages তারপর প্রতিটা mutation site তালিকাভুক্ত করে তোমার নতুনAdd/Removemethod-এ migrate করতে। Property typeIReadOnlyList<T>-এ বদলালে compiler প্রতিটা বাকি illegal caller চিহ্নিত করে। - IntelliJ IDEA (Java): Refactor → Encapsulate Fields field লুকিয়ে রাখে; "Return of collection field" (Encapsulation issues-এর অধীনে) এর মতো inspection সক্রিয়ভাবে internal collection expose করা getter-গুলো detect করে আর
Collections.unmodifiable...-এ wrap করার পরামর্শ দেয় — এই inspection চালু করো আর IDE তোমার খোলা খাতাগুলো খুঁজে দেবে। - Visual Studio:
Ctrl+R, Ctrl+Efield encapsulate করে; এরপর property typeIReadOnlyList<T>-এ বদলাও আর error list দিয়ে caller migration চালাও। Code analyzer (যেমন CA2227, "Collection properties should be read only") out of the box settable collection property সম্পর্কে warn করে। - সব IDE: plain text search তোমার গোপন অস্ত্র।
.getPlayers().বা.Players.Add(খোঁজলে মুহূর্তে প্রতিটা mutation-through-getter দেখায়।
প্রতিটা ভাষায় compiler-driven কৌশল একই: আগে type tighten করো, তারপর যা লাল হয় ঠিক করো। Error list একটা সম্পূর্ণ, বিশ্বাসযোগ্য to-do list হয়ে যায়।
সুবিধা আর ঝুঁকি
| সুবিধা | ঝুঁকি / খরচ |
|---|---|
| নিয়মগুলো (ধারণক্ষমতা, uniqueness, validity) প্রতিটা add আর remove-এ চলে — class অবশেষে তার প্রতিশ্রুতি রাখতে পারে | read-only view সদস্যপদ পরিবর্তন আটকায় কিন্তু element mutation নয়; deep safety-র জন্য immutable element বা deep copy দরকার |
teammate-দের সৎ ভুল (sort() in place, slice-এর বদলে splice) আর internal state corrupt করতে পারে না | প্রতিটা read-এ defensive copy করতে খুব বড় বা hot collection-এর জন্য memory আর সময় খরচ হয় |
Membership পরিবর্তন named, searchable operation হয় (addStudent) বেনামী list edit-এর বদলে | লেখার জন্য আরো method; ছোট internal helper class-এর ক্ষেত্রে এই আনুষ্ঠানিকতা ফল নাও দিতে পারে |
| Internal collection type (array থেকে Set, index যোগ) কোনো caller না ভেঙে বদলানো যায় | Migration নিরাপদ ক্রম মানতে হবে (নতুন method আগে, lock পরে) নয়তো অনেক call site একসাথে ভাঙবে |
| প্রতিটা পরিবর্তনে event, caching, audit logging-এর natural hook তৈরি হয় | Plain JavaScript-এ (TS ছাড়া), returned copy mutate করা নীরবে fail করে — বাকি caller search করে খুঁজতে হবে, crash দিয়ে নয় |
এটা কোন smell ঠিক করে?
| Smell | Encapsulate Collection কীভাবে সাহায্য করে |
|---|---|
| Data Class | Collection-মালিক class প্রকৃত behavior পায় — domain নিয়মসহ validated add/remove — leaky লিস্টের থলে হওয়ার বদলে |
| Inappropriate Intimacy | বাইরের কেউ আর অন্য object-এর internal collection-এ ঢুকে পড়তে আর সাজাতে পারে না |
| Feature Envy | Raw লিস্টে caller-রা যে logic চালাত (সীমা যাচাই, duplicate এড়ানো) সেটা মালিক class-এ ফিরে আসে |
| Shotgun Surgery | একটা নতুন collection নিয়ম ("সর্বোচ্চ ১১ জন খেলোয়াড়") একটা method-এ যোগ করা হয়, codebase জুড়ে প্রতিটা mutation site-এ নয় |
এক মানচিত্রে পুরো guarding কৌশল — এর sibling refactoring Remove Setting Method আর Hide Method সহ:
দ্রুত রিভিশন বক্স
+================================================================+
| ENCAPSULATE COLLECTION — REVISION CARD |
+================================================================+
| SMELL SIGN : getter returns the LIVE list / bulk setter exists |
| PICTURE : attendance register open on the desk |
| GOLDEN RULE: photocopies out, admission slips in |
+----------------------------------------------------------------+
| THE MOVE : 1. Add add()/remove() methods WITH the rules |
| 2. Redirect mutating callers to them (test each) |
| 3. Delete the bulk setter (constructor copies in) |
| 4. Getter -> read-only view or copy |
| 5. Backing field -> private readonly/final |
| 6. Search for survivors: .getX().push / .Add( |
+----------------------------------------------------------------+
| LANGUAGE : TS -> ReadonlyArray<T> + spread copy |
| TOOLBOX C# -> IReadOnlyList<T> + _list.AsReadOnly() |
| Java -> Collections.unmodifiableList / copyOf |
| Py -> return tuple(self._items) |
| REMEMBER : read-only view guards MEMBERSHIP, not elements |
+================================================================+অনুশীলনের কাজ
ধরো একটা স্কুল quiz club app এভাবে তার সদস্য manage করছে:
class QuizClub {
private members: string[] = [];
getMembers(): string[] {
return this.members; // live array escapes
}
setMembers(members: string[]): void {
this.members = members; // bulk swap allowed
}
}
// Found in the codebase:
club.getMembers().push(""); // blank name added
club.getMembers().push("Asha"); // bypasses any limit
club.getMembers().sort(); // "just for display"...
const seniors = club.getMembers().splice(0, 3); // meant to READ three names
club.setMembers(["OnlyMyFriends"]); // coup d'étatতোমার কাজ:
১. নিরাপদ ক্রমে Encapsulate Collection প্রয়োগ করো: আগে addMember(name) আর removeMember(name) নিয়মসহ লেখো — কোনো blank নাম নেই, কোনো duplicate নেই, সর্বোচ্চ ১৫ সদস্য। (চিত্র ৬ দেখো: class কোন state-এ যাচ্ছে?)
২. উপরের পাঁচটা খারাপ call site-কে legal বিকল্পে migrate করো। (Hint: sort caller-এর নিজের copy দরকার; splice caller আসলে slice চেয়েছিল।) চিত্র ২ ব্যবহার করে প্রতিটা call site classify করো: innocent read, accidental mutation, নাকি deliberate mutation?
৩. setMembers delete করো আর একটা constructor যোগ করো যেটা initial সদস্য accept করে আর addMember-এর মধ্য দিয়ে দেয়, যাতে initial data-ও নিয়ম মানে।
৪. getMembers() বদলাও ReadonlyArray<string> return করতে fresh copy-র উপর ভিত্তি করে, আর backing field private readonly করো।
৫. Bonus (C#): একই class লেখো private readonly List<string> _members সহ, একটা property public IReadOnlyList<string> Members => _members.AsReadOnly();, আর AddMember/RemoveMember method দিয়ে। নিশ্চিত করো club.Members.Add("X") compile হয় না। Bonus (Python): class লেখো tuple(self._members) return করে আর mutating caller যে AttributeError পাবে সেটা দেখাও।
৬. কলেজ কর্নার প্রশ্ন: তুমি কাজ ৪-এ defensive copy বেছেছ। কলেজ কর্নার থেকে strategy টেবিল ব্যবহার করে, এই quiz-club app-এ এমন একটা নির্দিষ্ট পরিস্থিতি বলো যেখানে read-only view তোমার copy থেকে ভিন্নভাবে আচরণ করত — আর একটা পরিস্থিতি যেখানে পার্থক্যটা গুরুত্বপূর্ণ হতো।
৭. Reflection প্রশ্ন: তোমার refactoring-এর পরে, একজন caller club.getMembers()[0].toUpperCase() করে একটা নতুন string পায় — কিন্তু ধরো members ছিল Student object আর একজন caller club.getMembers()[0].name = "Hacked" করে। তোমার read-only copy কি সেটা আটকাত? Views বনাম deep immutability সম্পর্কে এটা কী শেখায় — আর এই series-এর কোন refactoring element-গুলো নিজেই ঠিক করে?
সচরাচর জিজ্ঞাসা
- আমার getter লিস্ট return করে যাতে caller পড়তে পারে। এতে সমস্যা কী?
- পড়া ঠিকই আছে — বিপদ হলো live লিস্ট পড়ার চেয়ে অনেক বেশি কিছু করতে দেয়। যে reference দিয়ে caller item loop করতে পারে, সেই একই reference দিয়ে clear(), push(), বা splice() ডেকে চুপচাপ তোমার object-এর ভেতরটা বদলে দিতে পারে। তোমার class তখন নিজের collection সম্পর্কে কোনো নিয়ম মানাতে পারে না। read-only view বা copy return করো: caller পড়ার ক্ষমতা রাখে, আর যে ক্ষমতা তার কখনো থাকা উচিত ছিল না সেটা হারায়।
- copy return করব নাকি read-only view? কোনটা ভালো?
- copy সহজ আর সম্পূর্ণ নিরাপদ কিন্তু প্রতিটা call-এ memory আর সময় খরচ হয়, আর caller একটা জমাট snapshot দেখে। read-only view (Java-তে Collections.unmodifiableList, TypeScript-এ ReadonlyArray টাইপিং বা Object.freeze, C#-এ AsReadOnly) সস্তা আর সবসময় আপডেট, কিন্তু কিছু ভাষায় কেউ mutate করতে চাইলে শুধু runtime-এ error আসে। ছোট collection-এর জন্য দুটোই চলে; একটা style বেছে নাও আর পুরো codebase-এ একইভাবে ব্যবহার করো।
- read-only view কি collection-এর ভেতরের element-গুলোও রক্ষা করে?
- না — আর এটা অনেক developer-কে ফাঁদে ফেলে। read-only view সদস্যপদ পরিবর্তন (add, remove, clear) আটকায় কিন্তু ভেতরের object-গুলো এখনো একই object। যে caller Student-কে লিস্ট থেকে সরাতে পারে না, সে তবুও সেই student-এর নাম বদলাতে পারে। element mutation যদি গুরুত্বপূর্ণ হয়, তাহলে element-গুলোকে নিজেই immutable করো, অথবা deep copy return করো।
- setCourses(list) এর মতো collection setter কেন সরাতে হবে?
- কারণ bulk setter হলো সবচেয়ে বড় ফাঁকফোকর: এটা তোমার পুরো backing store-কে অন্য কেউ বানানো একটা লিস্ট দিয়ে বদলে দেয় — যেখানে duplicate, null, বা বেশি item থাকতে পারে যা তোমার class কখনো যাচাই করেনি, আর caller পরেও সেই reference ধরে রেখে mutate করতে পারে। Fowler ঠিক এই কারণেই Encapsulate Collection-এর সাথে Remove Setting Method জুড়ে দেন। যদি শুরুতে কিছু data দরকার হয়, constructor-এ নিজের private লিস্টে copy করে নাও।
- C#-এ IEnumerable<T> expose করাটাই কি নিরাপদ?
- বেশিরভাগ সময় হ্যাঁ, কিন্তু একটা সমস্যা আছে: property যদি শুধু private List<T>-কে IEnumerable<T> হিসেবে return করে, তাহলে caller সেটাকে List<T>-তে cast করে mutate করতে পারে। নিরাপদ option হলো _items.AsReadOnly() return করা, ReadOnlyCollection wrapper দিয়ে IReadOnlyList<T> expose করা, অথবা copy return করা। EF Core private backing field-এর পেছনে IReadOnlyList property-র সাথে ভালোভাবে কাজ করে, তাই entity-গুলোকেও এভাবে encapsulate করা যায়।
আরো দেখো
- Encapsulate Collection — refactoring.com catalog (Fowler, 2nd ed.) article
- Encapsulate Collection — Refactoring Guru article
- Domain-Driven Refactoring: Encapsulating Collections — Jimmy Bogard article
- Collections.unmodifiableList in Java — GeeksforGeeks article
- Refactoring (2nd Edition) by Martin Fowler book
সম্পর্কিত পাঠ
Encapsulate Field: Object যেন নিজের ডেটা নিজে পাহারা দেয়
Encapsulate Field কী সেটা সহজ ভাষায় — কেন public field যেকোনো কোডকে object-এর ডেটা নষ্ট করতে দেয়, আর কীভাবে private field সাথে getter-setter দিয়ে object নিজেই সব নিয়ন্ত্রণ করে।
Data Class: নিয়মহীন রেজিস্টার — যে কেউ যা খুশি লিখে যায়
Data Class smell শেখো একটা society register-এর গল্পের মাধ্যমে। দেখো কেন behavior ছাড়া data encapsulation ভেঙে পড়ে, আর কখন DTO আর record একদম ঠিকঠাক।
Inappropriate Intimacy: দুটো class যারা একে অপরের রান্নাঘরে ঢুকে পড়ে
দুই প্রতিবেশীর গল্প দিয়ে Inappropriate Intimacy বোঝো — যারা একে অপরের রান্নাঘর সাজিয়ে দেয়। দুটো class যখন একে অপরের private অংশে হাত দেয়, তখন কেউ একা কিছু বদলাতে পারে না। Law of Demeter আর privacy ফিরিয়ে আনার refactoring শেখো।
Primitive Obsession: যখন সব কিছুই শুধু একটা string বা number
Primitive Obsession সহজ ভাষায় — কেন plain string আর number bug লুকিয়ে রাখে, আর কীভাবে Money বা Address-এর মতো value object দিয়ে code-কে safe আর পরিষ্কার করা যায়।