Shotgun Surgery: এক জায়গায় পরিবর্তন, দশ জায়গায় দৌড়াদৌড়ি
Shotgun Surgery code smell শিখবে রুবেলের বাসা বদলের গল্পের মাধ্যমে — সহজ সংজ্ঞা, TypeScript আর C# এর example, Divergent Change এর সাথে পার্থক্য, আর practice সহ।
রুবেলের বাসা বদলের সেই মহাবিপদ
ধরো রুবেলদের পরিবার মিরপুরে থাকত। গত মাসে বাবার transfer হলো, তারা উঠে গেল মোহাম্মদপুরে। নতুন বাসা, নতুন পাড়া, নতুন প্রতিবেশী — সবাই খুশি। কিন্তু তারপর বাবা একটা খাতা বের করলেন আর list করা শুরু করলেন।
"বাবা, কী লিখছ?" রুবেল জিজ্ঞেস করল।
"বাবা, এটা সেই সব জায়গার list যেখানে যেখানে আমাদের পুরানো ঠিকানা লেখা আছে," বাবা দীর্ঘশ্বাস ফেলে বললেন। আর আঙুলে গুনতে লাগলেন:
- NID card — উপজেলা অফিসে যাও, token নাও, দুই ঘণ্টা বসে থাকো।
- Bank — KYC form ভরো, ঠিকানার proof দাও, তিন জায়গায় sign করো।
- রুবেলের school — প্রধান শিক্ষকের কাছে আবেদন লিখতে হবে।
- Gas connection — agency তে যাও, নতুন ভাড়ার agreement দেখাও।
- Ration card — আরেকটা অফিস, আরেকটা লাইন, আরেকটা photocopy।
- বিদ্যুৎ বিল, পোস্ট অফিস, রিকশার কাগজ... list শেষই হয় না।
পরের দুইদিন পুরো ঢাকা শহর চষে বেড়ানো। Token machine, photocopy, "দুপুরের পরে আসেন", "server down, কাল আসেন"। বাবার মেজাজ যেন সারাদিন কোন অবস্থায় ছিল:
বাস্তব জীবনে একটাই পরিবর্তন — পরিবারটা নতুন বাসায় উঠেছে — কিন্তু দশটা আলাদা অফিস আলাদাভাবে হাতে হাতে update করতে হচ্ছে। আর ভয়ের ব্যাপারটা হলো রাতে খাওয়ার সময় বাবা বললেন: "একটা অফিস ভুলে গেলেই পরে বিপদ। গত বার তোমার চাচা bank ভুলে গিয়েছিল। পরে debit card block হয়ে গেল কারণ ঠিকানা মিলছিল না।"
একটু ভাবো এই যন্ত্রণার shape টা। পরিবর্তন একটাই। Update করার জায়গা অনেক। কোনো অফিস অন্য অফিসকে জানায় না। বাবাকে নিজের মাথায় মনে রাখতে হয় সব। আর দেখো journey-র শেষ row — রিকশার কাগজ এক সপ্তাহ পরে মনে পড়ল।
রুবেল সেদিন সেই golden question টা করল: "বাবা, একটাই অফিস থাকলে ভালো হতো না? সেখানে ঠিকানা বদলালে বাকি সব এমনিই update হয়ে যেত!"
বাবা হাসলেন। "বাবা, সেটা যেদিন বানাবে, সেদিন একটা বিখ্যাত সমস্যা solve করবে।"
Software-এও ঠিক একই রোগ আছে। কখনো কখনো একটা ছোট পরিবর্তন — নতুন tax rate, নতুন date format, একটা status rename — একজন programmer কে দশটা file খুলে হাতে হাতে edit করতে বাধ্য করে। একটা file ভুলে গেলে production-এ চুপচাপ bug বসে থাকে, ঠিক চাচার blocked debit card-এর মতো। এই smell-এর নাম Shotgun Surgery।
এই smell আসলে কী?
Shotgun Surgery হয় যখন একটা পরিবর্তনের জন্য অনেক আলাদা class-এ অনেক ছোট ছোট edit করতে হয়।
নামটা ইচ্ছে করেই এমন রাখা হয়েছে। Rifle একটা পরিষ্কার ছিদ্র করে। Shotgun চারদিকে অনেক ছোট ছোট pellet ছড়িয়ে দেয়। এই smell থাকলে একটা conceptual change একটা পরিষ্কার জায়গায় না পড়ে পুরো codebase-এ ছড়িয়ে যায়। আর প্রতিটা edit একটা ছোট "surgery" — সাবধানে করতে হয়, ভুল হওয়া সহজ।
Martin Fowler Refactoring বইয়ে বলেছেন: যখনই কোনো একটা ধরনের পরিবর্তন করতে যাও, দেখবে অনেকগুলো class-এ অনেক ছোট ছোট edit করতে হচ্ছে। পরিবর্তনগুলো সব জায়গায় ছড়িয়ে থাকলে সেগুলো খুঁজে পাওয়া কঠিন, আর একটা গুরুত্বপূর্ণ জায়গা মিস হয়ে যাওয়া সহজ।
মূল কারণ প্রায় সবসময় একটাই: একটা concept-এর কোনো নির্দিষ্ট বাড়ি নেই। ধরো "address কীভাবে format করব" বা "late fine কীভাবে হিসাব হবে" — এই idea যদি একটা class-এ থাকে, বদলাতে হলে একটা জায়গাতেই বদলাতে হবে। কিন্তু যদি এটা copy-paste করে, বা বারবার নিজের মতো করে type করে, প্রতিটা জায়গায় লেখা থাকে — তাহলে এই idea-র কোনো owner নেই। পুরো system-এ মাখনের মতো মাখানো। এখন idea বদলাতে গেলে প্রতিটা "মাখানো জায়গা" খুঁজে বের করতে হবে।
Shotgun Surgery হলো Change Preventers পরিবারের দ্বিতীয় সদস্য। এর ভাই Divergent Change-এর মতোই এটা program crash করায় না। এটা তোমার পরিবর্তন করার গতি কমিয়ে দেয়। যে team এই smell-এ ভুগছে তারা বলতে থাকে "আবার address format?" — একটা সহজ কাজের আগেই ভয় পাওয়া এই smell-এর আবেগিক signature।
মনে রাখার সহজ trick: Shotgun Surgery = একটা বাসা বদল, দশটা অফিস। একটা কারণে পরিবর্তন, অনেক class-এ edit করতে হয়। সমাধান হলো "একটাই অফিস বানাও যে বাকিদের জানিয়ে দেবে" — ছড়ানো logic একটা class-এ জড়ো করো।
Fowler আরেকটা sharp কথা বলেছেন: Divergent Change-এ অন্তত সব একজায়গায় থাকে — সমস্যা হলো বেশি একজায়গায়। কিন্তু Shotgun Surgery-তে সমস্যা আরো খারাপ একভাবে: টুকরোগুলো খুঁজেই পাওয়া যায় না। যা খুঁজে পাবে না, তা fix করবে কীভাবে? আর compiler সাধারণত string format আর if-condition খুঁজে দিতে সাহায্য করে না।
পুরো smell টা একটা map-এ:
কীভাবে চিনবে এই smell
Shotgun Surgery দেখার আগেই অনুভব হয়। নিচের checklist দেখো। তিনটা বা তার বেশি মিলে গেলে এই smell আছে।
- একটা "ছোট" requirement-এর জন্য pull request করতে গিয়ে দেখলে ৮+ টা file ছুঁয়েছ, প্রতিটায় মাত্র দুই-তিন লাইন বদলেছে।
- তোমার team মাথায় (বা কাগজে!) একটা checklist রাখে: "যখন X বদলাবে, মনে রেখো A, B, C, আর ওই legacy folder-এর job-টাও বদলাতে হবে।"
- এরকম bug ship করেছ যেখানে "সব জায়গায় update করেছিলাম, একটা জায়গা ছাড়া।"
- একই constant, format string, বা validation rule — একটু ভিন্নভাবে হলেও — অনেক file-এ ছড়িয়ে আছে।
- নতুন developer জিজ্ঞেস করে "X কোথায় বদলাব?" আর সৎ উত্তর হলো "সব জায়গায়, একটু একটু করে।"
- ছোট পরিবর্তনের code review-তে অনেক সময় লাগে, কারণ reviewer কে verify করতে হয় কোনো জায়গা miss হয়নি — যা verify করা অনেক কঠিন।
দ্রুত revision-এর জন্য table:
| লক্ষণ | কী দেখছ | কী মানে |
|---|---|---|
| Confetti PR | এক ticket, দশটা file, দুই লাইন প্রতিটায় | যে concept বদলাচ্ছ তার কোনো একক বাড়ি নেই |
| Tribal checklist | "এটাও update করতে ভুলো না..." knowledge | শুদ্ধতা মানুষের স্মৃতির উপর নির্ভর করছে |
| Except-one bug | একটা মিস জায়গায় production bug | ছড়ানো edit স্বভাবতই অসম্পূর্ণ |
| Cloned rules | অনেক file-এ একই validation/format logic | Copy-paste concept টাকে সব জায়গায় ছড়িয়েছে |
| Slow review | Reviewer কে missing edit খুঁজতে হয় | অনুপস্থিতি review করা উপস্থিতির চেয়ে কঠিন |
| Grep-driven কাজ | পরিবর্তন করতে codebase-এ keyword খুঁজতে হয় | Type system তোমাকে সব জায়গা দেখাতে পারছে না |
একটা practical detection trick: version control history দেখো যেসব commit সবসময় একসাথে যায়। A, D, আর K file যদি commit-এর পর commit-এ একসাথে বদলায়, তাহলে একটা invisible thread তাদের বেঁধে রেখেছে। সেই thread হলো একটা homeless concept — এটাকেই বের করে একটা বাড়ি দিতে হবে।
এই ছড়ানো ভাবটা একটা chart-এ দেখা যাক। ধরো "ঠিকানা কীভাবে লিখব" — এই code এখন কোথায় কোথায় আছে:
সুস্থ concept-এর pie chart বোরিং হয় — একটাই slice, একটাই owner। পাঁচটা প্রায় সমান slice মানে পাঁচটা অফিস, প্রতিটায় তোমার ঠিকানার আলাদা photocopy, প্রতিটা আলাদাভাবে পুরানো হচ্ছে।
College corner: গবেষকরা version-control history দেখে এটাকে বলেন change coupling (বা "logical coupling"): যেসব file compile-time-এ একে অপরের উপর নির্ভর করে না, কিন্তু একই commit-এ বারবার বদলায়। CodeScene-এর মতো tool file pair গুলোকে co-change frequency দিয়ে rank করে ঠিক এই Shotgun Surgery ধরার জন্য যা static analyzer দেখতে পায় না — কারণ coupling টা import-এ না, concept-এ। Software engineering mini-project করলে real repo-র git log থেকে co-change pair mine করা publishable exercise হতে পারে।
কেন এটা সমস্যা
১. ভুলে যাওয়ার error। এটাই সবচেয়ে ভয়ের। বিপদ হলো যে ৯টা edit করলে সেগুলোতে না — বিপদ হলো ১০তম যেটা ভুলে গেলে। ছড়ানো change-site থাকলে ভুলে যাওয়াটাই default, আর সম্পূর্ণভাবে করতে মাথার জোর লাগে। রুবেলের বাবা bank ভুলেছিলেন; তোমার team LegacyExportJob ভুলবে।
২. ছোট পরিবর্তন ব্যয়বহুল হয়ে যায়। এক লাইনের conceptual change হয়ে যায় দশটা file খুঁজে, edit করে, re-test করার এক বিকেলের কাজ। যখন ছোট উন্নতিতে এত খরচ হয়, মানুষ ছোট উন্নতি করা বন্ধ করে দেয়, আর codebase ধীরে ধীরে ভালো হওয়া থেমে যায়।
৩. Review দুর্বল হয়ে যায়। Reviewer check করতে পারে তোমার দশটা edit ঠিক আছে কিনা। কিন্তু একাদশতম জায়গা মিস করেছ কিনা সে কীভাবে check করবে? অনুপস্থিতি review করা প্রায় অসম্ভব, তাই review হয়ে যায় আশার উপর ভরসা।
৪. Copies দূরে সরে যায়। প্রতিটা ছড়ানো জায়গা বছরের পর বছর আলাদাভাবে edit হয়। একটা জায়গায় fine উপরে গোল করা হয়, আরেকটায় নিচে। একটায় ঠিকানায় comma, আরেকটায় নেই। ছোট ছোট পার্থক্য জমতে জমতে হয় "invoice-এ ঠিকানা আলাদা কেন, receipt-এ আলাদা কেন?" রহস্য।
৫. Knowledge মানুষের মাথায় আটকে যায়। সব change-site-এর পুরো list প্রায়ই শুধু কোনো senior developer-এর মাথায় থাকে। সেই মানুষ ছুটিতে গেলে team আটকে যায় — একটা code smell-এর ভেতরে লুকানো project risk।
Monday সকালে যখন ticket বলছে "ছোট পরিবর্তন", তখন ব্যাপারটা আসলে কেমন লাগে:
আর এই smell-এ বাঁচা team-এর ভেতরে কথোপকথন কেমন হয়। দেখো সবচেয়ে গুরুত্বপূর্ণ প্রশ্নের — "এটাই কি সব?" — কোনো confident উত্তর নেই:
College corner: এই smell হলো change amplification-এর textbook case — John Ousterhout-এর A Philosophy of Software Design বই থেকে — যেখানে একটা সহজ পরিবর্তনে অনেক জায়গায় code বদলাতে হয়। এটা লক্ষ্য করো এর ভাই smell-এর সাথে কীভাবে মেলে: Divergent Change একটা edit করতে যে বোঝাপড়া দরকার সেটা বাড়িয়ে দেয়; Shotgun Surgery edit site-এর সংখ্যা বাড়িয়ে দেয়। দুটোই একটা পরিবর্তনের আসল খরচ তার idea-র আকারের চেয়ে অনেক বেশি বাড়িয়ে দেয়। ভালো architecture সেটাই যেখানে একটা পরিবর্তনের খরচ সেই idea-র আকারের সমানুপাতিক, codebase-এর বয়সের না।
Code-এ real উদাহরণ
চলো রুবেলের ঠিকানার সমস্যাটা code-এ দেখি। একটা ছোট দোকানের software-এ customer-এর ঠিকানা রাখা হয়। দেখো "ঠিকানা কীভাবে লিখব" — এই idea-র কোনো বাড়ি দেওয়া হয়নি — যে class-এর ঠিকানা দরকার, সে নিজেই string বানিয়ে নিয়েছে:
// The concept "format an address" is smeared across the system.
class CustomerView {
renderProfile(c: Customer): string {
return `${c.houseNo}, ${c.street}, ${c.city} - ${c.pincode}`;
}
}
class InvoicePdf {
billingBlock(c: Customer): string {
// same idea, typed again, slightly different
return `${c.houseNo} ${c.street}, ${c.city}-${c.pincode}`;
}
}
class ShippingLabel {
print(c: Customer): string {
// and again, with its own little variation
return `${c.houseNo}, ${c.street}\n${c.city} ${c.pincode}`;
}
}
class SmsSender {
confirmDelivery(c: Customer): void {
smsGateway.send(c.phone, `Delivered to ${c.houseNo}, ${c.street}, ${c.city}`);
}
}
// ...and somewhere in a dusty corner:
class LegacyExportJob {
exportRow(c: Customer): string {
return [c.houseNo, c.street, c.city, c.pincode].join("|");
}
}এখন requirement আসল: "প্রতিটা ঠিকানায় জেলার নাম যোগ করো, আর pincode-টা 'PIN: 1206' style-এ লেখো।" এক বাক্যের পরিবর্তন। কিন্তু দেখো এর দাম কতটুকু: CustomerView, InvoicePdf, ShippingLabel, SmsSender, আর LegacyExportJob — পাঁচটা file, পাঁচটা সাবধানী ছোট surgery। আর LegacyExportJob যে আছে সেটা মনে ছিল? Code কোথাও বলেনি ওখানেও যেতে হবে। pincode খুঁজে চারটা পাবে, কিন্তু export job-টা join করে fields, কাছাকাছি সেই word নেই। পঞ্চম pellet-টা ইতিমধ্যে হারিয়ে গেছে।
আরো দেখো: পাঁচটার মধ্যে তিনটা format-এ comma আর dash নিয়ে ইতিমধ্যে মতানৈক্য। ইচ্ছে করে আলাদা করা হয়নি — শুধু বছরের পর বছর প্রতিটা আলাদাভাবে edit হতে হতে সরে গেছে। এই সরে যাওয়াটাই smell-এর বুড়িয়ে যাওয়া।
ধাপে ধাপে ঠিক করা
সমাধান Divergent Change-এর উল্টো: ভাঙা না — জড়ো করো। Homeless concept-কে একটা বাড়ি দাও, আর পুরানো প্রতিটা জায়গা সেখানে delegate করুক। Tools হলো Move Method, Move Field, আর কখনো কখনো Inline Class।
ধাপ ১ — Homeless concept-এর নাম দাও। জিজ্ঞেস করো: "কোন ideaটা বারবার এই multi-file sweep করাচ্ছে?" এখানে পরিষ্কার "ঠিকানা কীভাবে দেখাব"। নামটা জোরে বলো; সাধারণত একটা class-এর নামের মতো শোনায়: Address।
ধাপ ২ — Owner তৈরি করো বা বেছে নাও। কখনো কখনো কোনো class আছে কিন্তু সেই behavior-টা নেই। এখানে কোনো class-ই নেই — তাই নতুন বানাই আর Move Field দিয়ে address data-গুলো ওখানে নিয়ে যাই:
// One home for the concept.
class Address {
constructor(
private houseNo: string,
private street: string,
private city: string,
private state: string,
private pincode: string
) {}
full(): string {
return `${this.houseNo}, ${this.street}, ${this.city}, ${this.state} - PIN: ${this.pincode}`;
}
short(): string {
return `${this.houseNo}, ${this.street}, ${this.city}`;
}
exportRow(): string {
return [this.houseNo, this.street, this.city, this.state, this.pincode].join("|");
}
}ধাপ ৩ — একটা একটা করে পুরানো জায়গা redirect করো। Move Method-এর মাথায় রেখে কাজ করো: formatting behavior চলে যাক Address-এ, প্রতিটা ছড়ানো জায়গা হয়ে যাক এক লাইনের delegation। একটা site convert করো, test চালাও, commit করো। তারপর পরেরটা।
// After: every site delegates to the one owner.
class CustomerView { renderProfile(c: Customer) { return c.address.full(); } }
class InvoicePdf { billingBlock(c: Customer) { return c.address.full(); } }
class ShippingLabel { print(c: Customer) { return c.address.full(); } }
class SmsSender {
confirmDelivery(c: Customer) {
smsGateway.send(c.phone, `Delivered to ${c.address.short()}`);
}
}
class LegacyExportJob { exportRow(c: Customer) { return c.address.exportRow(); } }জড়ো করার পরের structure টা দেখার মতো। মাঝখানে একটা owner; প্রতিটা পুরানো জায়গা এখন ভদ্রভাবে এক লাইনে delegate করছে:
ধাপ ৪ — Compiler কে তোমার হয়ে খুঁজতে দাও। এখানে একটা দারুণ bonus আছে। Customer-এ চারটা আলাদা string-এর বদলে যখন একটা Address object থাকে, তখন যে জায়গায় এখনো c.pincode করা আছে সেটা compile-ই হবে না। Compiler হয়ে যায় তোমার checklist। "মিস করলাম না তো?" এই ভয় — এই smell-এর সবচেয়ে কষ্টের অংশ — সরাসরি চলে যায়, কারণ মিস করা জায়গা লাল underline দিয়ে নিজেই জানান দেয়।
ধাপ ৫ — Over-fragmentation check করো। কখনো কখনো Shotgun Surgery হয় কারণ একটা concept-কে অনেক বেশি ছোট ছোট class-এ ভেঙে ফেলা হয়েছে যেগুলো সবসময় একসাথে বদলায়। সেক্ষেত্রে সমাধান Inline Class: টুকরোগুলো আবার একটা class-এ মিলিয়ে দাও।
একটা concept-এর পুরো যাত্রা — homeless থেকে দুলতে দুলতে, শেষে নিরাপদ বাড়িতে — একটা state machine-এ:
এখন আবার মূল requirement টা চালিয়ে দেখো: "জেলার নাম যোগ করো, PIN style ব্যবহার করো।" একটা edit, Address-এর full method-এ। একটা file, একটা test run, একটা সহজ review। রুবেলের বাবার স্বপ্ন: বাসা বদলাও, একটাই অফিসে জানাও, সেই অফিস বাকি সবাইকে জানিয়ে দেবে।
আগে আর পরের পার্থক্য একটা chart-এ:
College corner: এইমাত্র আমরা যা বানালাম সেটা হলো single source of truth — DRY principle-এর practical রূপ ("Don't Repeat Yourself", The Pragmatic Programmer থেকে)। কিন্তু DRY-এর exact কথাটা মনে রেখো: প্রতিটা knowledge-এর একটাই authoritative representation থাকা উচিত। এটা text নিয়ে না, knowledge নিয়ে। পাঁচটা visually আলাদা লাইন যদি একই business rule encode করে, সেটা DRY violate করে — যদিও কোনো দুটো লাইন হুবহু একই না। তাই "duplicate code খোঁজার" tool Shotgun Surgery-র সহজ case গুলোই ধরে। উল্টোদিকে, দুটো হুবহু একই দেখতে লাইন যদি আলাদা decision encode করে, সেগুলো merge করা উচিত না। Meaning দিয়ে জড়ো করো, দেখতে একই বলে না।
বেশি জড়ো করো না! Unrelated জিনিস একটা "common" class-এ টেনে আনলে একটা god class বানিয়ে ফেলবে — আর unrelated কারণ সেটাকে ঘিরে ধরবে। এটাই Divergent Change, mirror smell। শুধু সেটাই জড়ো করো যা সত্যিই একই কারণে একসাথে বদলায়।
C# আর Python-এ একই smell
C#-এ একই রোগ, payroll setting-এ। "Professional tax কীভাবে হিসাব হয়" এই rule-এর কোনো বাড়ি দেওয়া হয়নি:
// Before: the tax rule lives in three places.
public class PayslipPrinter
{
public string Footer(Employee e) =>
$"Prof. Tax: {(e.GrossPay > 15000 ? 200 : 0)}";
}
public class SalaryCalculator
{
public decimal NetPay(Employee e) =>
e.GrossPay - (e.GrossPay > 15000 ? 200 : 0) - e.Pf;
}
public class ComplianceReport
{
public decimal TotalProfTax(IEnumerable<Employee> all) =>
all.Sum(e => e.GrossPay > 15000 ? 200m : 0m);
}সরকার slab বদলালে — ধরো ২১,০০০-এর উপরে ২০৮ টাকা — তিনটা file একইভাবে বদলাতে হবে। Rule-কে একটা বাড়ি দাও:
// After: one owner; everyone else delegates.
public static class ProfessionalTax
{
public static decimal For(Employee e) => e.GrossPay > 15000 ? 200m : 0m;
}
public class PayslipPrinter { public string Footer(Employee e) => $"Prof. Tax: {ProfessionalTax.For(e)}"; }
public class SalaryCalculator { public decimal NetPay(Employee e) => e.GrossPay - ProfessionalTax.For(e) - e.Pf; }
public class ComplianceReport { public decimal TotalProfTax(IEnumerable<Employee> a) => a.Sum(ProfessionalTax.For); }পরের slab change হবে ProfessionalTax-এ একটাই edit। Payslip, salary, আর report কখনো আর আলাদা হতে পারবে না, কারণ সবাই একই rule পড়ছে।
Python-এও একই রোগ সহজেই লাগে — সাধারণত f-string প্রতিটা print site-এ আবার type করলে:
# Before: the late-fine rule retyped in three modules
# library_desk.py
def fine_for(days_late):
return days_late * 5
# fee_counter.py
def receipt_line(days_late):
return f"Late fine: Rs.{days_late * 5}"
# parent_sms.py
def remind(days_late, phone):
sms.send(phone, f"Pending fine: Rs.{days_late * 5}. Please pay.")
# After: one home — fine_policy.py
FINE_PER_DAY = 5
def fine_for(days_late: int) -> int:
return days_late * FINE_PER_DAY
# Every other module imports fine_for. The next fine change is ONE line.Language কোনো ব্যাপার না। প্রশ্ন সবসময় একটাই: এই rule বদলালে কতগুলো file খুলতে হবে? উত্তর যদি একের বেশি হয়, rule-এর বাড়ি নেই।
Divergent Change বনাম Shotgun Surgery
এই comparison টা code smell-এর প্রতিটা ছাত্রকে আয়ত্ত করতে হবে। এই দুটো smell perfect mirror image, আর ভুল diagnosis মানে ভুল চিকিৎসা।
| প্রশ্ন | Shotgun Surgery | Divergent Change |
|---|---|---|
| যন্ত্রণার shape | একটা কারণে অনেক class বদলাতে হয় | একটা class-এ অনেক কারণে বদলাতে হয় |
| গল্পের version | একটা ঠিকানা বদলাতে দশটা অফিস | এক clerk-কে সব department ডিস্টার্ব করে |
| কী সমস্যা | Concept ছড়িয়ে আছে, কোনো বাড়ি নেই | Class অনেক unrelated কাজ করছে |
| কখন বুঝবে | "ছোট" পরিবর্তনে দশটা file খুঁজতে হচ্ছে | প্রতিটা ধরনের PR-এ একই file আসছে |
| সবচেয়ে বড় বিপদ | ছড়ানো জায়গার একটা মিস হয়ে যাওয়া | একটা কাজ edit করতে গিয়ে আরেকটা ভাঙা |
| সমাধানের দিক | জড়ো করো — টুকরো একটা বাড়িতে আনো | ভাগ করো — class টাকে ভেঙে দাও |
| Main refactoring | Move Method, Move Field, Inline Class | Extract Class, Move Method |
| বেশি করার বিপদ | বেশি জড়ো করলে Divergent Change হয় | বেশি ভাগ করলে Shotgun Surgery হয় |
দুটো diagnosis question পাশাপাশি:
- একটা ধরনের পরিবর্তনে কতগুলো class edit করি? অনেক → Shotgun Surgery → জড়ো করো।
- একটা class-এ কত ধরনের পরিবর্তন আসে? অনেক → Divergent Change → ভাগ করো।
এই map-এ যেকোনো confusing situation plot করলে diagnosis নিজেই বেরিয়ে আসে:
Address case নিচে আর ডানদিকে: একটা কারণ (address দেখানো), অনেক class ছোঁয় — pure Shotgun Surgery। সমাধানের পরে Address class চলে যায় শান্ত কোণে: একটা কারণ, একটা class। আর ভয়ের উপর-ডান কোণ? সেটা এমন codebase যেখানে god class আর ছড়ানো rule একসাথে আছে — দুটো smell একসাথে। এটা হয়, আর চিকিৎসার ক্রম গুরুত্বপূর্ণ: আগে ছড়ানো concept জড়ো করো, তারপর god class ভাগ করো — যাতে সবসময় জানো জিনিস কোথায় আছে।
আর see-saw warning মনে রেখো: সমাধানগুলো উল্টো দিকে টানে। বেশি জড়ো করলে god class হয় যেটা সবাই সব কারণে edit করে (Divergent Change)। বেশি ভাগ করলে একটা concept confetti class-এ ছড়িয়ে যায় (Shotgun Surgery)। সুস্থ code এই দুটোর মাঝামাঝি ব্যালেন্সে থাকে: প্রতিটা class-এ এক responsibility, প্রতিটা responsibility-র জন্য এক class। Fowler-এর দুটো smell হলো এই see-saw থেকে দুইদিকে পড়ার দুটো উপায়।
College corner: Coupling/cohesion-এর ভাষায়, Shotgun Surgery হলো system level-এ low cohesion দেখতে কেমন লাগে: একটা concept-এর অংশগুলো (যেগুলো high cohesion চাইলে একসাথে থাকার কথা) আলাদা module-এ ছড়িয়ে আছে, shared variable-এর বদলে shared knowledge দিয়ে common coupling তৈরি করছে। Compiler পাঁচটা independent class দেখে; business একটা rule দেখে। যখনই business-এর mental model আর code-এর module structure-এ মিল নেই, প্রতিটা business-driven change-এ একটা "translation tax" দিতে হয়। Domain-Driven Design-এর লোকেরা এই fix-কে বলেন "model কে domain-এর সাথে align করা" — রুবেল বলে "একটাই অফিস যে বাকিদের জানিয়ে দেবে।"
Real project-এ এই smell কোথায় লুকায়
Practitioners (Fowler-এর Refactoring, refactoring.guru, NDepend blog, আর অগণিত team retrospective) বারবার একই জায়গার কথা বলেন:
- Magic value হাতে ছড়ানো। Tax rate, fee amount, limit, URL, format string প্রতিটা use site-এ সরাসরি type করা। সেই value বদলানোর দিন hunt শুরু।
- Cross-cutting concern-এর কোনো wrapper নেই। Logging format, permission check, audit entry, error-response shape প্রতিটা endpoint-এ হাতে লেখা। Policy বদলাতে প্রতিটা endpoint ঘুরতে হয়।
- Primitive Obsession-এর ফলাফল। "ফোন নম্বর", "টাকা", বা "ঠিকানা" raw string আর number হিসেবে ঘুরলে, প্রতিটা consumer নিজেই formatting আর validation implement করে। Missing value-object মানেই missing home।
- Switch statement একই enum-এ, সব জায়গায়। পাঁচটা আলাদা file-এ
OrderStatus-এ switch করা। নতুন status যোগ করলে পাঁচটা switch-এই নতুন case দরকার — compiler কোনোটাতেই warn নাও করতে পারে। - Rigid ceremony সহ layered architecture। একটা field যোগ করতে entity, DTO, mapper, validator, API contract, আর UI form — সব ছুঁতে হয়। কিছুটা এড়ানো যায় না; কিন্তু যখন প্রতিটা layer নিজে কোনো সিদ্ধান্ত নেয় না, তখন এই ceremony-টাই Shotgun Surgery।
- Microservice-গুলো একটা hidden concept share করছে। কয়েকটা service যখন একই business rule নিজেদের মধ্যে hard-code করে, একটা rule change হলে multi-repo, multi-deploy sweep লাগে — এই smell-এর distributed, ব্যয়বহুল edition।
কখন ignore করা যাবে
সৎ কথা বলা দরকার। সব multi-file change রোগ না, আর সব ছড়ানো জিনিস আজকেই consolidate করার দরকার নেই।
| Situation | Ignore করবে? | কেন |
|---|---|---|
| পরিবর্তনটা সত্যিই cross-cutting (নতুন parameter সত্যিকারের layer-এর মধ্য দিয়ে যেতে হবে) | হ্যাঁ | কিছু sweep essential structure, smell না; জড়ো করলেও এগুলো যাবে না |
| ছড়ানো rule পাঁচ বছরে একবার বদলায় | সাধারণত | Consolidation-এর খরচ বিরল sweep-এর সঞ্চয়ের চেয়ে বেশি |
| Concept এখনো ভালো বোঝা যায়নি | অপেক্ষা করো | তাড়াতাড়ি জড়ো করলে ভুল abstraction হয়, যেটা নিজেই সমস্যা টানে |
| "ছড়ানো" মানে দুটো জায়গা, দুটোই obvious | সম্ভবত | দুটো জানা জায়গা সস্তা; নতুন abstraction ভাড়া নাও উশুল করতে পারে |
| একই sweep প্রতি মাসে হচ্ছে আর গত বছর একটা জায়গা মিস হয়েছিল | না — ঠিক করো | Smell সক্রিয়ভাবে bug আর ভয় তৈরি করছে |
| প্রতিটা ছড়ানো জায়গা একে অপরের সাথে একটু আলাদা | না — ঠিক করো | Drift শুরু হয়েছে; সময়ের সাথে আরো খারাপ হবে |
Rule of thumb: ছড়ানো fix করো যখন পরিবর্তন frequent বা miss costly। বিরল, কম-ঝুঁকির sweep অপেক্ষা করতে পারে। মাসিক sweep যেটায় একবার production bug হয়েছে, সেটা এই sprint-এই consolidate করো — payoff তাৎক্ষণিক আর স্থায়ী।
কোন refactoring দিয়ে ঠিক হয়
| Refactoring | এখানে কী করে | কখন ব্যবহার করবে |
|---|---|---|
| Move Method | ছড়ানো logic একটা class-এ নিয়ে যায় | Primary gathering tool |
| Move Field | Related data owner class-এ নিয়ে আসে, তার behavior-এর পাশে | যখন moved method বারবার পুরানো জায়গার data টানে |
| Inline Class | বেশি ভাগ করা tiny class গুলো একটায় মিলিয়ে দেয় | যখন অতিরিক্ত split-এর কারণে ছড়িয়েছে |
| Extract Class | Missing home তৈরি করে যখন কোনো উপযুক্ত owner নেই | যখন concept-এর (যেমন Address) কোনো class-ই নেই |
| Replace Primitive with Object | Raw string/number কে concept-owning type-এ পরিণত করে | যখন Primitive Obsession ছড়িয়েছে |
কাজের rhythm: concept-এর নাম দাও, owner তৈরি করো বা বেছে নাও, behavior আর data সেখানে নিয়ে যাও, একটা একটা করে call site convert করো test pass রেখে, শেষ করো শুধু যখন সেই ধরনের পরিবর্তনে ঠিক একটাই file ছুঁতে হবে।
দ্রুত revision
+--------------------------------------------------------------+
| SHOTGUN SURGERY — QUICK REVISION |
+--------------------------------------------------------------+
| Story : House shifted once -> Aadhaar, bank, school, |
| gas, ration card... ten offices to update |
| Smell : ONE change forces edits in MANY classes |
| Family : Change Preventers |
| Root : A concept was never given a single home |
| Spot it : Confetti PRs; tribal checklists; "missed one |
| place" bugs; cloned rules drifting apart |
| Costs : Errors of omission, costly small changes, |
| weak reviews, inconsistency, locked knowledge |
| Cure : GATHER -> Move Method, Move Field, Inline Class |
| (one home; every old site delegates to it) |
| Opposite : Divergent Change (one class, many reasons) |
| Memory : Shotgun = one shot, many pellets -> GATHER |
| Ignore : Rare changes; genuine cross-cutting layers |
+--------------------------------------------------------------+Practice করো
"একটাই অফিস বানাও যে সবাইকে জানিয়ে দেবে" — সেই officer হও!
Exercise ১ — Pellet গুলো খোঁজো। একটা school app-এ late-fine rule কয়েক জায়গায় আছে। Fine যদি ৫ থেকে ১০ টাকা প্রতিদিন হয়, কোন কোন file edit করতে হবে সেগুলো list করো, আর homeless concept টা চিহ্নিত করো।
class LibraryDesk {
fineFor(daysLate: number): number { return daysLate * 5; }
}
class FeeCounter {
receiptLine(daysLate: number): string { return `Late fine: Rs.${daysLate * 5}`; }
}
class ParentSms {
remind(daysLate: number, phone: string): void {
smsGateway.send(phone, `Pending fine: Rs.${daysLate * 5}. Please pay.`);
}
}
class AnnualReport {
totalFines(records: { daysLate: number }[]): number {
return records.reduce((s, r) => s + r.daysLate * 5, 0);
}
}Exercise ২ — বাড়ি বানাও। একটা LateFinePolicy class বানাও একটাই method দিয়ে fineFor(daysLate)। তারপর চারটা class convert করো যাতে সেটায় delegate করে। Refactor-এর পরে rule change মানে এক লাইনের edit।
Exercise ৩ — Drift ধরো। ধরো ParentSms চুপচাপ daysLate * 4 ব্যবহার করছিল পুরানো একটা typo-র কারণে। ছড়ানো version-এ এই bug কতদিন লুকিয়ে থাকতে পারে? জড়ো করা version-এ কি এটা আদৌ থাকতে পারে? দুটো বাক্যে ব্যাখ্যা করো কেন জড়ো করা এই পুরো bug পরিবারকে মেরে ফেলে।
Exercise ৪ — নিজের ছড়ানো chart করো। তোমার যেকোনো project থেকে একটা rule বেছে নাও (discount, date format, validation regex)। সেটা grep করো আর চিত্র ৩-এর মতো একটা pie chart আঁকো কোথায় কোথায় আছে দেখিয়ে। একের বেশি slice পেলে concept টার নাম লিখে রাখো — সেই নামটাই তোমার missing class।
Exercise ৫ — Diagnosis drill। Shotgun Surgery, Divergent Change, নাকি সুস্থ?
- নতুন report type যোগ করতে শুধু
ReportFactoryedit করতে হয়। - GST rate বদলাতে ৩টা folder-এর ৭টা file edit করতে হয়।
AppManagerlogin change-এ, billing change-এ, আর UI text change-এ — সব কারণে edit হয়।- Database column যোগ করতে entity, migration, আর form বদলাতে হয় — প্রতিটায় নিজস্ব real logic আছে।
(ভেবে দেখো: ১ সুস্থ — একটা কারণ, একটা জায়গা। ২ Shotgun Surgery — rate টা একটা বাড়িতে জড়ো করো। ৩ Divergent Change — class ভাগ করো। ৪ বেশিরভাগ essential layering — প্রতিটা layer নিজের সিদ্ধান্ত নিচ্ছে, তাই sweep-টা কমানো যাবে না।)
যখন confidently বলতে পারবে "এটায় জড়ো করতে হবে, ওটায় ভাগ করতে হবে" — তখন দুটো mirror smell-ই আয়ত্তে এসেছে। এরপর দেখো এই smell-এর structural special case: Parallel Inheritance Hierarchies।
সচরাচর জিজ্ঞাসা
- Shotgun Surgery মানে কী, এক কথায়?
- Shotgun Surgery হলো যখন একটাই ছোট পরিবর্তন — যেমন ঠিকানা format বদলানো — পুরো codebase-এর অনেকগুলো আলাদা class আর file-এ ছোট ছোট edit করতে বাধ্য করে। একটা কারণ, কিন্তু অনেক জায়গায় হাত দিতে হয়।
- এটাকে Shotgun Surgery বলে কেন?
- Shotgun এক জায়গায় একটা পরিষ্কার ছিদ্র করে না — চারদিকে ছোট ছোট অনেকগুলো pellet ছড়িয়ে দেয়। এই smell-ও তাই: একটা পরিবর্তন পুরো codebase-এ ছোট ছোট edit ছড়িয়ে দেয়। আর 'surgery' কারণ প্রতিটা edit সাবধানে করতে হয়, একটু ভুল হলেই bug।
- Shotgun Surgery আর Divergent Change এর পার্থক্য কী?
- দুটো একদম উল্টো। Shotgun Surgery হলো একটা কারণে অনেক class বদলাতে হয় — fix হয় ছড়ানো code একজায়গায় জড়ো করে। Divergent Change হলো একটা class-কে অনেক কারণে বদলাতে হয় — fix হয় class টাকে ভেঙে ফেলে।
- Shotgun Surgery ঠিক করতে কোন refactoring দরকার?
- Move Method আর Move Field দিয়ে ছড়ানো behavior আর data একটা class-এ জড়ো করো। আর যদি অনেক বেশি ছোট ছোট class বানিয়ে ফেলার কারণে এই সমস্যা হয়, তাহলে Inline Class দিয়ে সেগুলো একসাথে মিলিয়ে দাও।
- Shotgun Surgery আর code duplication কি একই জিনিস?
- দুটো কাছের জিনিস, কিন্তু একই না। Duplicated Code মানে একই logic-এর copy। Shotgun Surgery মানে হলো একটা concept-এর কোনো নির্দিষ্ট 'বাড়ি' নেই, তাই বদলাতে গেলে সব জায়গায় দৌড়াতে হয়। Duplication সবচেয়ে সাধারণ কারণ, কিন্তু non-duplicated related edit-ও Shotgun Surgery হতে পারে।
আরো দেখো
সম্পর্কিত পাঠ
Divergent Change: এক বেচারা কেরানি, অনেক বস
Divergent Change code smell শেখো একটা school-এর কেরানির গল্পের মাধ্যমে — সহজ সংজ্ঞা, TypeScript ও C# example, Shotgun Surgery-র সাথে তুলনা, আর practice exercise।
Parallel Inheritance Hierarchies: প্রতিটা জিনিসের একটা ছায়া থাকে
Parallel Inheritance Hierarchies code smell শেখো একটা মিষ্টির দোকানের গল্পের মাধ্যমে — mirrored class tree কেন সমস্যা, TypeScript আর C# example দিয়ে কীভাবে fix করবে, আর কখন এটা রেখে দেওয়া ঠিক আছে।
Move Method: কাজটা সেই class-এ নিয়ে যাও যেখানে সে আসলে থাকে
একটা স্কুলের গল্পের মাধ্যমে Move Method রিফ্যাক্টরিং শেখো। যে class-এর data method-টা সবচেয়ে বেশি ব্যবহার করে, সেখানেই সরিয়ে নাও — যাতে behaviour আর data একসাথে থাকে।
Move Field: ডেটা রাখো যেখানে সে কাজে লাগে
Move Field শেখো একটা মজাদার স্কুলের গল্প দিয়ে। ডেটাকে সেই class-এ সরাও যেটা আসলে ওই ডেটা ব্যবহার করে, যাতে state আর behaviour একসাথে বাস করতে পারে।