Extract Class: অতিরিক্ত কাজে ডুবে যাওয়া class-কে একটু সাহায্য করো
Extract Class refactoring শেখো একটা মজার school office-এর গল্পের মাধ্যমে। একটা overloaded class-কে দুটো focused class-এ ভাগ করো — প্রতিটার একটাই কাজ।
স্কুলের একটাই অফিস, কিন্তু কাজ সব
ধরো Sunrise Public School-এ একটাই অফিস রুম। সেখানে বসে আছেন রাহেলা ম্যাডাম — পুরো building-এর সবচেয়ে বেশি কাজে ডোবা মানুষ।
সকালে admission-এর জন্য অভিভাবকরা লাইন দিচ্ছেন — ফর্ম, birth certificate, interview date। তাদের পেছনে fee challan হাতে আরেক দল। ফোন বাজছে, ব্যাংক থেকে cheque নিয়ে জিজ্ঞেস করছে। একজন শিক্ষক এলেন salary slip নিতে। পিয়ন জামাল বিদ্যুৎ বিল এনে রাখল। সব কিছু একটাই desk-এ, একজন মানুষের সামনে, একটাই রুমে।
ফলাফল? সব কিছু ধীর, সব কিছু এলোমেলো। admission ফর্মের সাথে fee receipt স্ট্যাপল হয়ে গেছে। দুই মিনিটের admission প্রশ্ন নিয়ে আসা অভিভাবক আধাঘণ্টার fee ঝামেলার পেছনে দাঁড়িয়ে। রাহেলা ম্যাডাম admission register আর fee ledger-এর মধ্যে দিনে চল্লিশবার switch করছেন, আর প্রতিটা switch-এ এক মিনিট নষ্ট — "আচ্ছা কোথায় ছিলাম?" যখন auditor আসেন accounts দেখতে, পুরো অফিস বন্ধ — admission-সহ — কারণ টাকার কাগজ আর admission কাগজ একই আলমারিতে। একটা রুম, দুটো সম্পূর্ণ আলাদা কাজ, চরম confusion।
স্কুল management অবশেষে stopwatch দিয়ে তাঁর একটা দিন মাপল:
শেষের সেই ৮% টুকু চুপে চুপে সময় নষ্ট করছে। কাজ করতে গিয়ে না, বরং এক ধরনের কাজ থেকে আরেক ধরনে যেতে গিয়ে — ঠিক যেন একটা class-এর method-গুলো একে অপরের field-এ হোঁচট খাচ্ছে।
স্কুল management অবশেষে সঠিক কাজটা করল। করিডোরে ১২ নম্বর রুমে একটা নতুন Accounts Office খুলল, আর একজন dedicated accounts clerk, জামাল সাহেবকে নিয়োগ দিল। সব টাকার বিষয় সেখানে চলে গেল — fee register, challan book, cash box। রাহেলা ম্যাডামের অফিসে রইল admission, records, আর general enquiries। নতুন signboard লাগল: "Fee-র জন্য Accounts Office, Room 12-এ যান।"
এক সপ্তাহের মধ্যে দুটো অফিসই সুন্দর চলছে। Fee-র অভিভাবক সরাসরি ১২ নম্বরে যান। Admission শেষ হয় মিনিটে। Auditor জামাল সাহেবের books দেখেন, admission একটুও বন্ধ হয় না। প্রতিটা অফিসের এখন একটাই কাজ — আর প্রতিটা নিজের কাজ নিজে ভালো করতে পারছে, অন্যটাকে ছাড়াই।
Code-এ আমরা বারবার এই পুরনো school office-এর মতো class দেখি — একটা class চুপে চুপে দুটো কাজ করছে। সমাধান হলো management-এর সমাধান — নতুন অফিস খোলো, একটা কাজ সম্পূর্ণভাবে সেখানে সরিয়ে দাও। এই refactoring-এর নাম Extract Class।
Extract Class আসলে কী?
Extract Class হলো একটা refactoring যেখানে দুটো (বা বেশি) responsibility বহন করা একটা class-কে ভেঙে দুটো class বানাই — প্রতিটা একটাই responsibility বহন করে। Martin Fowler-এর Refactoring catalog-এ এটা আছে, আর এটাই standard উত্তর যখন একটা class বড় হয়ে "অনেক কিছু করছে"।
এক লাইনে recipe:
- cluster খোঁজো — বড় class-এর ভেতরে এমন field আর method-এর দল যারা মিলে একটা আলাদা concept বোঝাচ্ছে।
- সেই concept-এর নামে একটা নতুন, খালি class বানাও।
- দলটাকে এক এক করে সরিয়ে দাও, Move Field আর Move Method দিয়ে।
- পুরনো class নতুন class-এর একটা reference রাখুক।
প্রতিটা refactoring-এর মতো এখানেও behaviour change হয় না। Program একই উত্তর দেবে; শুধু code-এর ভেতরের structure পাল্টাবে। স্কুল একই student admit করছে, একই fee নিচ্ছে — শুধু একই desk-এ আর দুটো কাজ হচ্ছে না।
একটা class একটাই কাজ করবে — এই effort কেন worth it? এটাই বিখ্যাত Single Responsibility Principle: একটা class-এর change হওয়ার কারণ একটাই হওয়া উচিত। পুরনো school office-এ change হওয়ার দুটো কারণ ছিল — admission rule আর money rule — তাই যেকোনো একটায় change হলে দুটোই ঝাঁকুনি খেত। ভাগ হওয়ার পর, fee rule পাল্টালে শুধু Accounts Office পাল্টায়। Code-এর ভাষায়: ছোট class পড়তে সহজ, আলাদা test করা সহজ, আর নিরাপদে modify করা যায় — কারণ যেকোনো change-এর প্রভাব একটা focused class-এ সীমাবদ্ধ।
একটা চুপচাপ সুন্দর side effect-ও আছে। Extract Class প্রায়ই এমন একটা concept আবিষ্কার করে যেটা সামনেই লুকিয়ে ছিল। phoneAreaCode, phoneNumber, phoneExtension — এই তিনটা field আসলে কখনো তিনটা আলাদা field ছিল না। এগুলো একটা PhoneNumber object ছিল, জন্মের অপেক্ষায়। Extraction সেই লুকানো ধারণাটাকে একটা নাম দেয়, আর নাম পাওয়া ধারণাকে সব জায়গায় reuse করা যায়।
Cluster খোঁজার সবচেয়ে সহজ উপায়: "কে কার সাথে কথা বলে" খেলো। বড় class-এর প্রতিটা method-এর জন্য লিখো সে কোন field ছোঁয়। দেখবে সাধারণত দুটো দল তৈরি হয় — কিছু method ছোঁয় field a, b, c আর কিছু method ছোঁয় field x, y, z — মাঝে খুব কম crossing। এই ভাগের রেখাটাই তোমার extraction seam। Shared name prefix (phone-, address-, fee-) হলো সেই একই clue-এর word version।
College-এর জন্য একটু বেশি: "কে কার সাথে কথা বলে" খেলার একটা formal নাম আছে — এটাই LCOM family of metrics গণনা করে। LCOM4, সবচেয়ে practical variant, একটা graph বানায় যার nodes হলো class-এর method আর field, আর edge থাকে যেখানে কোনো method কোনো field ব্যবহার করে বা sibling method call করে, তারপর connected components গণনা করে। একটা component মানে একটা cohesive class। দুটো component মানে দুটো class এক কোট পরে আছে — আর component-গুলোই বলে দেয় কোন member কোন নতুন class-এ যাবে। NDepend, SonarQube, আর JDeodorant এটা তোমার জন্য গণনা করে; JDeodorant এমনকি dependency graph cluster করে Extract Class refactoring propose করে। তোমার professor যখন বলেন "class-এ high cohesion থাকা উচিত", LCOM4 equal to one মানেই সেই কথাটার numeric রূপ।
কখন Extract Class দরকার?
এই signals দেখলে সতর্ক হও:
১. Large Class — সবচেয়ে পরিচিত কেস। ডজন ডজন field আর method থাকা class প্রায় কখনোই একটা ধারণা না — কয়েকটা ধারণা একসাথে জট পাকিয়ে আছে। Large Class হলো সমস্যা, Extract Class হলো অপারেশন। School office-এর দরকার ছিল না একজন দ্রুততর clerk — দরকার ছিল দুটো office।
২. Divergent Change। এই সপ্তাহে fee rule-এর জন্য class edit করলে, পরের সপ্তাহে admission rule-এর জন্য, তার পরের সপ্তাহে report formatting-এর জন্য। একটা class, অনেক আলাদা কারণে change — মানে কয়েকটা responsibility একটা শরীরে। ভাগ করলে প্রতিটা কারণ নিজের ঘর পাবে।
৩. Data Clumps। একই তিনটা field বারবার একসাথে দেখা দিচ্ছে — এই class-এ, ওই method-এর parameters-এ, আরেক class-এ। যে data সবসময় একসাথে ঘোরে, সে নিজেই একটা class হতে চাইছে। একবার extract করো, তাহলে যেখানে clump ব্যবহার হতো সেখানে একটা neat object রাখা যাবে।
৪. Class-এর অর্ধেক অন্যদিকে হিংসুটে। কখনো কখনো Feature Envy সারাতে Move Method চালাতে গিয়ে দেখো পাঁচটা method একই জায়গায় যেতে চাইছে — কিন্তু সেই জায়গাটার অস্তিত্বই নেই। যখন অনেক member এমন একটা destination চায় যার নাম নেই, সেটাকে class বানাও। Extract Class সেটা তৈরি করে।
৫. একটা অংশ test না করে আরেকটা test করা যাচ্ছে না। Fee calculation test করতে গেলে admission data-ও বানাতে হচ্ছে? তাহলে দুটো কাজ জট পাকানো। Extract-এর পরে প্রতিটা class নিজের ছোট, দ্রুত test পাবে।
নিচে "কে কার সাথে কথা বলে" খেলাটা school office class-এ table আকারে দেখো:
| Method | admissions | feesPaid | feePerTerm | lateFinePerDay |
|---|---|---|---|---|
admitStudent | ✅ | — | — | — |
isAdmitted | ✅ | — | — | — |
collectFee | — | ✅ | — | — |
feeDue | — | ✅ | ✅ | — |
lateFine | — | — | — | ✅ |
দুটো দল, zero crossing। উপরের দল হলো admissions office; নিচের দল হলো Room 12 যেটা খোলার অপেক্ষায়।
কখন extract করবে না?
- আসলে দ্বিতীয় responsibility নেই। নতুন class-এর জন্য একটা স্পষ্ট noun খুঁজে না পেলে, হয়তো তুমি একটা ধারণাকে দুই ভাগ করছো। জোর করে extract করলে Lazy Class তৈরি হবে — আর সেটার সমাধান হলো উল্টো refactoring, Inline Class।
- দুই অর্ধেককে সারাক্ষণ state share করতে হচ্ছে। নতুন class আর পুরনো class যদি প্রতিটা operation-এ data আদান-প্রদান করে, তাহলে তুমি ভুল জায়গায় কাটছো।
- Cluster-টা ছোট। দুটো field আর একটা method এখনো নতুন file আর নতুন নামের যোগ্য নাও হতে পারে। Concept নিজেকে প্রমাণ করুক, তারপর দেখো।
Extract করবো কি করবো না — এই সিদ্ধান্ত ছবিতে:
আগে আর পরে — এক নজরে
প্রথমে একটা ছোট TypeScript example। Teacher class চুপে চুপে একটা পুরো telephone-number concern ভেতরে ঢুকিয়ে নিয়েছে:
// BEFORE — one class, two jobs (teaching info + phone formatting)
class Teacher {
constructor(
public name: string,
public subject: string,
public phoneStdCode: string,
public phoneNumber: string,
) {}
formattedPhone(): string {
return `(${this.phoneStdCode}) ${this.phoneNumber}`;
}
isMumbaiNumber(): boolean {
return this.phoneStdCode === "022";
}
}phoneStdCode, phoneNumber, formattedPhone, isMumbaiNumber — এই cluster আসলে teacher-এর বিষয়ই না। এটা phone number-এর বিষয়। এদের নিজের class দাও:
// AFTER — each class has one clear job
class PhoneNumber {
constructor(
public stdCode: string,
public number: string,
) {}
formatted(): string {
return `(${this.stdCode}) ${this.number}`;
}
isMumbai(): boolean {
return this.stdCode === "022";
}
}
class Teacher {
constructor(
public name: string,
public subject: string,
public phone: PhoneNumber,
) {}
}এখন PhoneNumber একটা real citizen। Parent class একটা রাখতে পারে। Vendor class রাখতে পারে। তিনটা ছোট test দিয়ে unit test করা যায়, সব জায়গায় reuse করা যায় — যেটা সম্ভব ছিল না যখন এটা Teacher-এর ভেতরে লুকিয়ে ছিল।
ধাপে ধাপে, নিরাপদ পথে
Extract Class একটা composite refactoring — ভেতরে ভেতরে এটা কয়েকটা Move Field আর Move Method step। কখনো এটা একটা বড় cut-and-paste হিসেবে করো না। স্কুল এক সপ্তাহ বন্ধ রেখে সব গুছিয়ে reopen করেনি — Room 12 খালি খুলল, ledger এক এক করে সরল, আর দুটো অফিসই চালু ছিল পুরোটা সময়। এই নিরাপদ sequence follow করো।
ধাপ ১ — Concept-এর নাম ঠিক করো। Cluster-টা কী সেটা ঠিক করো আর একটা crisp noun দাও: PhoneNumber, AccountsOffice, DiscountPolicy। Noun খুঁজে না পেলে থামো — হয়তো আসলে দ্বিতীয় class নেই।
ধাপ ২ — একটা খালি নতুন class বানাও আর link করো। ভেতরে কিছু নেই এমন নতুন class যোগ করো, আর পুরনো class-এ একটা field রাখো যেটা new instance ধরে রাখবে:
// INTERMEDIATE STATE 1 — empty new class, linked but unused
class PhoneNumber {}
class Teacher {
private _phone = new PhoneNumber(); // link in place, nothing moved yet
// ...all old fields and methods still here...
}Compile করো আর test করো। Behaviour-এ কিছু পাল্টায়নি — শুধু ভিত্তিপ্রস্তর রাখা হলো। Room 12 আছে; এখনো খালি।
ধাপ ৩ — Field এক এক করে সরাও। Cluster-এর প্রতিটা field-এর জন্য Move Field procedure follow করো। phoneStdCode সরানোর পরে intermediate state দেখতে এরকম:
// INTERMEDIATE STATE 2 — one field moved, old accessor delegates
class PhoneNumber {
stdCode = "";
}
class Teacher {
private _phone = new PhoneNumber();
get phoneStdCode(): string {
return this._phone.stdCode; // temporary delegation
}
set phoneStdCode(v: string) {
this._phone.stdCode = v;
}
// phoneNumber, formattedPhone(), isMumbaiNumber() still here
}প্রতিটা field-এর পরে compile করো আর test করো। তারপর পরেরটা সরাও।
ধাপ ৪ — Method সরাও। এখন cluster-এর প্রতিটা method-এর জন্য Move Method use করো, সবচেয়ে কম dependency আছে যেগুলোতে সেগুলো দিয়ে শুরু করো। formattedPhone হয়ে যাবে PhoneNumber.formatted(), আর পুরনো নাম সাময়িকভাবে delegator হিসেবে টিকে থাকবে।
ধাপ ৫ — দুটো interface পরিষ্কার করো। প্রতিটা class এখন কী expose করছে সেটা review করো। Callers update করতে পারলে Teacher-এর temporary delegating accessor-গুলো delete করো, অথবা অনেক caller পুরনো নামের উপর নির্ভর করলে পাতলা façade রাখো। নতুন class-এর internals যতটা সম্ভব private করো।
ধাপ ৬ — Exposure আর ownership decide করো। Callers কি নতুন object দেখবে (teacher.phone.formatted()) নাকি শুধু পুরনো class (teacher.formattedPhone() যেটা ভেতরে forward করে)? আর: PhoneNumber কি একজন teacher-এর (value, অবাধে copy করা যায়) নাকি object-এর মধ্যে shared (reference, সতর্ক থাকো)? Value semantics দুর্ঘটনা এড়ায় — একজন teacher-এর phone পাল্টালে আরেকজনেরটা চুপে পাল্টে যাওয়ার ঘটনা হবে না।
পুরো বিভাজন, big class যে state-গুলো পার করে:
প্রতিটা move-এর পরে full test suite চালাও — প্রতিটা field, প্রতিটা method। Extract Class দুটো class-এ অনেক লাইন ছোঁয়, আর সবচেয়ে common ভুল হলো half-moved cluster: field সরেছে কিন্তু একটা method এখনো পুরনো পথ দিয়ে stale copy পড়ছে। ছোট ছোট step-এ সরালে test তুরন্ত ধরে ফেলে, আর সব একবারে সরালে প্রায় কখনোই ধরে না।
একটা বড় real-life example
এবার পুরো school office গল্পটা TypeScript-এ। এই overworked office admission আর accounts দুটোই একটা class-এ করছে:
// BEFORE — one office, every queue
class SchoolOffice {
private admissions: string[] = [];
private feesPaid = new Map<string, number>();
private feePerTerm = 5000;
private lateFinePerDay = 20;
admitStudent(name: string): string {
this.admissions.push(name);
return `${name} admitted. Welcome to Sunrise Public School!`;
}
isAdmitted(name: string): boolean {
return this.admissions.includes(name);
}
collectFee(name: string, amount: number): void {
const paid = this.feesPaid.get(name) ?? 0;
this.feesPaid.set(name, paid + amount);
}
feeDue(name: string): number {
return this.feePerTerm - (this.feesPaid.get(name) ?? 0);
}
lateFine(daysLate: number): number {
return daysLate * this.lateFinePerDay;
}
}"কে কার সাথে কথা বলে" খেলো (বা আগের section-এর camp table-এ ফিরে তাকাও)। admitStudent আর isAdmitted শুধু admissions ছোঁয়। collectFee, feeDue, আর lateFine শুধু money field-গুলো ছোঁয়। দুটো দল, zero crossing — নিখুঁত seam। LCOM4-এর ভাষায়, এই class আসলে দুটো connected component যারা একটার ভান করছে। নতুন অফিস খোলো:
// AFTER — admissions stay; all money matters move to Room 12
class AccountsOffice {
private feesPaid = new Map<string, number>();
private feePerTerm = 5000;
private lateFinePerDay = 20;
collectFee(name: string, amount: number): void {
const paid = this.feesPaid.get(name) ?? 0;
this.feesPaid.set(name, paid + amount);
}
feeDue(name: string): number {
return this.feePerTerm - (this.feesPaid.get(name) ?? 0);
}
lateFine(daysLate: number): number {
return daysLate * this.lateFinePerDay;
}
}
class SchoolOffice {
private admissions: string[] = [];
readonly accounts = new AccountsOffice(); // the signboard to Room 12
admitStudent(name: string): string {
this.admissions.push(name);
return `${name} admitted. Welcome to Sunrise Public School!`;
}
isAdmitted(name: string): boolean {
return this.admissions.includes(name);
}
}
// Callers now go to the right window:
// office.admitStudent("Meera");
// office.accounts.collectFee("Meera", 3000);
// office.accounts.feeDue("Meera"); // 2000স্কুল কী পেল?
- Auditor সমস্যা সমাধান। এখন
AccountsOfficeসম্পূর্ণ আলাদাভাবে test করা যাচ্ছে — admission data লাগছে না। পাঁচটা ছোট money test, দ্রুত আর focused। - প্রতিটা অফিস একটাই কারণে পাল্টায়। নতুন late-fine rule? শুধু
AccountsOfficeপাল্টায়। নতুন admission form? শুধুSchoolOfficeপাল্টায়। Divergent Change শেষ। - Concept reusable। পরের বছর স্কুলের summer camp নিজের
AccountsOfficeinstance ব্যবহার করতে পারবে, admission টেনে না নিয়ে।
বিভাজনের পরেও fee দিতে আসা অভিভাবক এক smooth experience পাচ্ছেন — শুধু সঠিক দরজায় যাচ্ছেন, আর reference field হলো সেই signboard যেটা পথ দেখাচ্ছে:
আর management যেটা দেখতে চেয়েছিল — প্রতিটা class-এ কতটা আলাদা কারণে change হওয়ার সুযোগ:
College-এর জন্য একটু বেশি: Extract Class সবচেয়ে সরাসরি Single Responsibility Principle-এর সাথে যুক্ত refactoring, কিন্তু গভীর measurement হলো cohesion আর coupling-এর জুটি। ভালো extraction-এ intra-class cohesion বাড়ে (প্রতিটা নতুন class-এর LCOM4 এক হয়) আর inter-class coupling কম থাকে (দুটো class একটা সরু reference দিয়ে যোগাযোগ করে, ঠিক Room 12-এর signboard-এর মতো)। খারাপ extraction — ভুল seam-এ কাটলে — সংখ্যায় সাথে সাথে ধরা পড়ে: cohesion প্রায় বাড়ে না, coupling বিস্ফোরিত হয় কারণ দুই অর্ধেক getter দিয়ে সারাক্ষণ কথা বলছে। Researchers এই ভালো ফলাফলকে বলে "high cohesion, low coupling", আর Parnas-এর ১৯৭২ সালের decomposition paper থেকে modular design-এর এটাই north star: একসাথে পাল্টায় যেগুলো, তাদের একসাথে রাখো — কোনো flowchart-এর step হিসেবে ভাগ করো না।
C# এবং Python-এ একই refactoring
C#-এ একই move, Fowler-এর বিখ্যাত employee-and-phone shape ব্যবহার করে:
// BEFORE
class Employee
{
public string Name { get; set; }
public decimal Salary { get; set; }
public string PhoneStdCode { get; set; }
public string PhoneNumber { get; set; }
public string FormattedPhone() => $"({PhoneStdCode}) {PhoneNumber}";
public bool IsTollFree() => PhoneStdCode is "1800" or "1860";
}// AFTER
class PhoneNumber
{
public string StdCode { get; set; }
public string Number { get; set; }
public string Formatted() => $"({StdCode}) {Number}";
public bool IsTollFree() => StdCode is "1800" or "1860";
}
class Employee
{
public string Name { get; set; }
public decimal Salary { get; set; }
public PhoneNumber Phone { get; set; } = new();
}Employee আবার শুধু employment-এর বিষয় হলো। PhoneNumber এখন Customer, Supplier, যে কেউ ধরতে পারবে — আর একটাই জায়গায় proper validation grow করতে পারবে। C#-এ এটাকে record বানালে value semantics free পাওয়া যায়, যেটা ownership প্রশ্নের উত্তর দেয় সহজেই।
Python-এ same split, miniature-এ — দেখো extracted class সাথে সাথে নিজেই testable হয়ে গেছে:
# AFTER the extraction, in Python
class PhoneNumber:
def __init__(self, std_code, number):
self.std_code = std_code
self.number = number
def formatted(self):
return f"({self.std_code}) {self.number}"
def is_mumbai(self):
return self.std_code == "022"
class Teacher:
def __init__(self, name, subject, phone):
self.name = name
self.subject = subject
self.phone = phone # holds one PhoneNumber
# a three-line unit test, impossible while the cluster hid inside Teacher:
assert PhoneNumber("022", "23456789").is_mumbai()IDE support
Extract Class-এর জন্য, বিশেষ করে JetBrains পরিবারে, ভালো tooling আছে:
| Tool | Support | কীভাবে |
|---|---|---|
| JetBrains Rider (C#) | Full | Refactor এ Extract Class: field আর method tick করো, Rider class আর reference field বানিয়ে দেবে |
| IntelliJ IDEA (Java/Kotlin) | Full | Extract Delegate selected member নতুন class-এ সরিয়ে delegation wire করে |
| ReSharper in Visual Studio | Full | Visual Studio-র ভেতরে same Extract Class dialog |
| Visual Studio (plain) | Partial | Extract Interface / Extract Base Class আছে; member extraction manual safe sequence-এ করতে হবে |
| VS Code (TypeScript) | Manual | Automated Extract Class নেই; "Find All References" দিয়ে Move Field / Move Method guide করো |
IDE এক click-এ করলেও, produce করা output review করো: tools সঠিকভাবে member সরায়, কিন্তু ভালো নাম, visibility, আর নতুন object expose করবে কিনা — এটা শুধু তুমিই decide করতে পারবে।
সুবিধা আর ঝুঁকি
| সুবিধা ✅ | ঝুঁকি / খরচ ⚠️ | |
|---|---|---|
| Responsibility | প্রতিটা class একটাই কাজ পায়, একটাই কারণে পাল্টায় (SRP) | ভুল seam-এ কাটলে দুটো class সারাক্ষণ কথা বলতে থাকবে |
| Testing | নতুন class একাই unit test করা যায়, ছোট দ্রুত test দিয়ে | দুটো class মানে কিছু test-এ একটু বেশি setup wiring |
| Reuse | লুকানো concept নাম পায়, অন্য class-ও ধরতে পারে | ভুল নামের class যেখানে reuse হবে সব জায়গায় confusion ছড়াবে |
| Readability | ছোট file, পরিষ্কার গল্প | পড়তে গিয়ে একটা extra indirection hop; আরেকটা file খুলতে হবে |
| Encapsulation | নতুন class internals লুকায় focused interface-এর পেছনে | Ownership প্রশ্ন আসে: নতুন object shared নাকি owned? সচেতনভাবে decide করতে হবে |
দাঁড়িপাল্লা: Extract Class ↔ Inline Class। এই দুটো refactoring exact inverse — একটা দাঁড়িপাল্লার দুই প্রান্ত, একদিকে "অনেক বেশি responsibility" আরেকদিকে "অনেক কম"। একটা class দুটো কাজ করছে? Extract Class দিয়ে পাল্লা নামাও। Extract করা class কখনো বড় হলো না — শুধু একটা field আর একটা forwarding method? এটা Lazy Class হয়ে গেছে; Inline Class দিয়ে পাল্লা উল্টো দিকে নামাও। এটা failure না — এটাই normal design breathing। Requirements পাল্টায়, class বাড়ে আর কমে, আর একটা healthy codebase বছরের পর বছর ধরে দুই দিকেই পাল্লা নাড়ে। দক্ষতা হলো বোঝা যে আজকে পাল্লা কোন দিকে হেলে আছে। স্কুলও এটা জানে: যদি fee collection পুরোপুরি online হয়ে যায় আর Room 12 একটা আলমারিতে সীমাবদ্ধ হয়, জামাল সাহেবের desk চুপে মিশে যাবে front office-এ — আর সেটাই তখনকার সঠিক সিদ্ধান্ত হবে, ঠিক যেমন বিভাজনটা এখনকার সঠিক সিদ্ধান্ত।
কোন কোন smell ঠিক করে?
| Smell | Extract Class কীভাবে সাহায্য করে |
|---|---|
| Large Class | প্রধান সমাধান — বড় class একটা পুরো responsibility নতুন class-কে দিয়ে দেয় |
| Divergent Change | Change হওয়ার প্রতিটা কারণ এখন নিজের class-এ থাকবে |
| Data Clumps | একসাথে ঘোরা field-এর দল একটা named object হয়ে যায় |
| Feature Envy | অনেক method যে destination চায় কিন্তু সেটা নেই, extraction সেটা তৈরি করে |
| Primitive Obsession | ছাড়া primitive-গুলো (stdCode + number) একটা real type-এ পরিণত হয় যেমন PhoneNumber |
Quick revision box
+--------------------------------------------------------------+
| EXTRACT CLASS — CHEAT CARD |
+--------------------------------------------------------------+
| Story : one overworked school office -> open a new |
| Accounts Office for all money matters |
| Problem : one class doing TWO jobs (two reasons to change) |
| Detect : field/method clusters; shared name prefixes; |
| "who talks to whom" shows two camps (LCOM4 > 1) |
| Fix : name the concept -> empty class + reference -> |
| Move Field x N -> Move Method x N -> clean up |
| Rule : one class, one job, one reason to change (SRP) |
| Test : full suite after EVERY field/method move |
| Inverse : Inline Class (the other end of the seesaw) |
| Don't : extract with no crisp noun; create a Lazy Class |
+--------------------------------------------------------------+Practice exercise
এই class-টা চুপে চুপে দুটো কাজ করছে। তোমার mission: এদের আলাদা করো।
class LibraryDesk {
private issuedBooks = new Map<string, string>(); // member -> book
private memberPhoneStd = new Map<string, string>();
private memberPhoneNumber = new Map<string, string>();
issueBook(member: string, book: string): void {
this.issuedBooks.set(member, book);
}
bookWith(member: string): string | undefined {
return this.issuedBooks.get(member);
}
setPhone(member: string, std: string, num: string): void {
this.memberPhoneStd.set(member, std);
this.memberPhoneNumber.set(member, num);
}
formattedPhone(member: string): string {
return `(${this.memberPhoneStd.get(member)}) ${this.memberPhoneNumber.get(member)}`;
}
}তোমার কাজ:
১. "কে কার সাথে কথা বলে" খেলাটা কাগজে খেলো। কোন method কোন field ছোঁয়? দুটো দল একটা table-এ আঁকো, এই article-এর মতো — অথবা college হলে LCOM4 graph বানাও connected components দেখিয়ে।
২. লুকানো concept-টার নাম দাও। (Hint: shared prefix-সহ দুটো map আসলে একটাই map যেটা একটা object ধরে।)
৩. নিরাপদ ধাপে extraction করো: নতুন class খালি বানাও, phone data সরাও, setPhone আর formattedPhone সরাও, প্রতিটা move-এর পরে test করো। আগে একটা ছোট test লেখো: phone set করো ("011", "23456789") আর expect করো "(011) 23456789"।
৪. Decide করো: callers কি সরাসরি নতুন class দেখবে, নাকি LibraryDesk delegating method রাখবে? একটা বাক্যে তোমার choice defend করো।
৫. তোমার extraction চিত্র ৪-এর quadrant-এ plot করো: cluster কতটা separable ছিল, আর কতটা heavy? "Extract now" quadrant-এ পড়েছে?
৬. Bonus seesaw প্রশ্ন: ধরো এক বছর পরে, extracted class-এ এখনো শুধু একটা constructor আর একটা getter — কিছু grow করেনি। কোন refactoring তোমার কাজ undone করবে, আর কেন undoing করাটাই সঠিক move, defeat নয়?
যদি উত্তর হয় "Inline Class — কারণ যে class কোনো real responsibility বহন করে না সে দেওয়ার চেয়ে নেওয়া বেশি করে" — তাহলে তুমি দাঁড়িপাল্লার দুই প্রান্ত বুঝে ফেলেছো। রাহেলা ম্যাডাম আর জামাল সাহেব দুজনেই মাথা নাড়বেন: queue থাকলে অফিস খোলো, queue না থাকলে বন্ধ করো — শুধু sentiment-এর জন্য একটা room আলাদা রেখো না।
সচরাচর জিজ্ঞাসা
- Extract Class refactoring মানে আসলে কী?
- Extract Class মানে হলো একটা class যে দুটো কাজ করছে, সেটাকে দুটো আলাদা class-এ ভাগ করা — প্রতিটা class একটাই কাজ করবে। বড় class-এর ভেতরে field আর method-এর একটা cluster খুঁজে বের করো যারা আসলে আলাদা একটা concept বোঝাচ্ছে। সেই concept-এর জন্য নতুন class বানাও, তারপর Move Field আর Move Method দিয়ে cluster-টাকে সেখানে নিয়ে যাও। পুরনো class নতুন class-এর একটা reference রাখে।
- কীভাবে বুঝবো যে একটা class-এ Extract Class দরকার?
- একটা cluster খোঁজো — এমন field-এর দল যেগুলো সবসময় একসাথে ব্যবহার হয়, আর method যেগুলো শুধু সেই field-গুলোই ছোঁয়। আরেকটা sign হলো shared name prefix, যেমন phoneAreaCode, phoneNumber, phoneExtension — এই prefix আসলে একটা লুকানো class নিজের নাম চিৎকার করে বলছে। নিজেকে জিজ্ঞেস করো: এই class কি দুটো আলাদা কারণে change হয়? হ্যাঁ হলে, এটা আসলে দুটো class একটা file-এ বাস করছে।
- Extract Class কোন কোন code smell ঠিক করে?
- এটা Large Class-এর প্রধান ওষুধ — যে class অনেক বেশি responsibility জমিয়ে ফেলেছে। এটা Divergent Change-ও ঠিক করে, মানে একটা class অনেক আলাদা কারণে change হচ্ছে। আর Data Clumps-ও ঠিক করে, মানে একই দল field বারবার একসাথে দেখা দিচ্ছে।
- Extract Class আর Inline Class-এর মধ্যে সম্পর্ক কী?
- এই দুটো একদম উল্টো — একটা দাঁড়িপাল্লার দুই দিক। Extract Class সেই class-কে ভাগ করে যে অনেক বেশি কাজ করছে; Inline Class সেই class-কে মিলিয়ে দেয় যে অনেক কম কাজ করছে। তুমি যদি বেশি উৎসাহে extract করো আর নতুন class কখনো বড় না হয়, তাহলে Inline Class দিয়ে পাল্লা উল্টো দিকে নামাও। ভালো design মানে প্রতিটা class যথেষ্ট পরিমাণ responsibility বহন করছে।
- নতুন extracted class কি বাইরের callers-দের কাছে দেখানো উচিত?
- তুমি decide করবে। expose করতে পারো — callers লিখবে employee.phone.formatted() — অথবা পুরনো class-এ delegating method রেখে লুকিয়ে রাখতে পারো, callers তখনও employee.formattedPhone() লিখবে যেটা ভেতরে forward করে। লুকিয়ে রাখলে পুরনো public interface stable থাকে, বড় codebase-এর জন্য সেটা ভালো। expose করলে সরাসরি। কতজন caller আছে সেটা দেখে decide করো।
আরো দেখো
সম্পর্কিত পাঠ
Move Method: কাজটা সেই class-এ নিয়ে যাও যেখানে সে আসলে থাকে
একটা স্কুলের গল্পের মাধ্যমে Move Method রিফ্যাক্টরিং শেখো। যে class-এর data method-টা সবচেয়ে বেশি ব্যবহার করে, সেখানেই সরিয়ে নাও — যাতে behaviour আর data একসাথে থাকে।
Move Field: ডেটা রাখো যেখানে সে কাজে লাগে
Move Field শেখো একটা মজাদার স্কুলের গল্প দিয়ে। ডেটাকে সেই class-এ সরাও যেটা আসলে ওই ডেটা ব্যবহার করে, যাতে state আর behaviour একসাথে বাস করতে পারে।
Inline Class: যে Class কিছুই করে না, তাকে মিলিয়ে দাও
Inline Class refactoring শেখো একটা school committee-র গল্পের মাধ্যমে। যে class কিছুই করে না তাকে তার user-এর সাথে মিলিয়ে দাও আর অকারণ layer মুছে ফেলো।
Large Class: যে স্কুলের ব্যাগে সব কিছু থাকে
Large Class code smell কী সেটা বুঝো — কেন god class বড় হয়, low cohesion কীভাবে চেনা যায়, আর Extract Class দিয়ে কীভাবে ছোট ছোট focused class-এ ভাগ করা যায়।