সরাসরি পর্বে যান
পর্ব ৪২ · ৭০-এর মধ্যে · মডিউল ৭

Code smell আর refactoring — ছোট নিরাপদ ধাপে কোড উন্নত করা

Code Smells & Refactoring — Improving Code in Small, Safe Steps

Taught in Bangla, this lesson defines refactoring as changing structure without changing observable behaviour, uses tests as a safety net with small steps and a commit after each, groups nineteen common code smells into five families, walks through the mechanics of Extract Function and Replace Conditional with Polymorphism, builds a toy AST smell detector, weighs IDE refactoring and rewrites, and keeps refactor commits separate in Git.

এই পর্বে যা শিখবেন

  • Refactoring আর অন্য বদলের (bug fix, feature, performance tuning) পার্থক্য সংজ্ঞা দিয়ে বলা, আর “observable behaviour” মানে কী তা বোঝা
  • উনিশটি প্রচলিত code smell পাঁচটি দলে চেনা, আর প্রতিটির জন্য উপযুক্ত refactoring বেছে নেওয়া
  • Extract Function আর Replace Conditional with Polymorphism-এর mechanics ধাপে ধাপে প্রয়োগ, প্রতিটি ধাপে test সবুজ রেখে
  • কখন IDE-র automated refactoring নিরাপদ আর কখন নয়, কখন refactor করবেন না, আর কখন rewrite বিবেচনা করবেন তা বিচার করা
  • Git-এ refactoring-এর commit আচরণ-বদলের commit থেকে আলাদা রাখা, আর প্রতিটি commit-এ test চলছে কি না যাচাই করা

১ · Refactoring কী — আচরণ একই, ভেতরটা পরিষ্কার

পর্ব ৪১-এ দেখেছেন পরিষ্কার কোড দেখতে কেমন — ভালো নাম, ছোট function, অর্থপূর্ণ comment. কিন্তু কোড ছয় মাস পরে আর পরিষ্কার থাকে না: তাড়াহুড়োয় একটি if যোগ হয়, কেউ একটি function কপি করে সামান্য বদলান। চলমান কোডকে কিছু না ভেঙে আবার পরিষ্কার করার পদ্ধতিই refactoring.

ঈদের আগে ঘরের আলমারি গোছানোর কথা ভাবুন। জামাকাপড় একটিও বাড়ে না, কমেও না — শুধু শার্ট এক তাকে, শাড়ি আরেক তাকে, শীতের কাপড় ওপরের বাক্সে। গোছানোর পরেও আলমারিতে ঠিক আগের জিনিসগুলোই আছে (আচরণ একই), কিন্তু এখন দরকারি জামা খুঁজতে এক মিনিট লাগে, দশ মিনিট নয় (বদলানো সস্তা)। আর বুদ্ধিমান মানুষ এক তাক গুছিয়ে দেখে নেন সব ঠিক আছে কি না, তারপর পরের তাক — সব কাপড় একসাথে মেঝেতে ঢেলে নয়। Refactoring ঠিক এই গোছানো: একটি একটি তাক, প্রতিবার মিলিয়ে দেখা।

Martin Fowler তাঁর Refactoring: Improving the Design of Existing Code বইয়ে (প্রথম সংস্করণ ১৯৯৯, উদাহরণ Java-তে; দ্বিতীয় সংস্করণ ২০১৮, উদাহরণ JavaScript-এ) শব্দটির দুটি রূপ দেন। বিশেষ্য হিসেবে refactoring হলো “software-এর ভেতরের কাঠামোয় এমন একটি বদল, যা একে বোঝা সহজ আর বদলানো সস্তা করে, কিন্তু এর observable behaviour বদলায় না।” ক্রিয়া হিসেবে refactor করা মানে এমন অনেকগুলো ছোট refactoring পরপর প্রয়োগ করে কাঠামো নতুন করে সাজানো। দুটি শব্দ খেয়াল করার মতো: ভেতরের কাঠামো (বাইরের চুক্তি নয়) আর observable — যা বাইরে থেকে দেখা যায়: ফেরত মান, screen-এর লেখা, file বা database-এ লেখা, ছোড়া error.

শব্দটি ফাউলারের আবিষ্কার নয়। “Refactoring” কথাটি ১৯৮০-র দশকের শেষে Smalltalk সম্প্রদায়ে চালু ছিল বলে ধরা হয়, আর এর প্রথম পূর্ণাঙ্গ গবেষণা হিসেবে সাধারণত উল্লেখ করা হয় William Opdyke-এর ১৯৯২ সালের PhD থিসিস (University of Illinois at Urbana-Champaign, তত্ত্বাবধায়ক Ralph Johnson — Gang of Four-এর চারজনের একজন)। Opdyke দেখান, কোন কোন শর্ত মানলে একটি কাঠামোগত বদল প্রমাণযোগ্যভাবে আচরণ রক্ষা করে। একই বিশ্ববিদ্যালয়ে John Brant আর Don Roberts Smalltalk-এর জন্য Refactoring Browser বানান — automated refactoring-এর প্রথম দিকের প্রভাবশালী tool. Fowler-এর অবদান ছিল এই জ্ঞানকে একটি catalog-এ সাজানো: প্রতিটি refactoring-এর নাম, কখন দরকার, আর ধাপে ধাপে কীভাবে (mechanics). পর্ব ৫-এর Extreme Programming (XP) refactoring-কে প্রতিদিনের অভ্যাস বানায়।

কোনটি refactoring, কোনটি নয়

বদলবাইরের আচরণ বদলায়?Refactoring?
Variable t-এর নাম total করানাহ্যাঁ — Rename
২০ লাইনের loop-এর ভেতরের দাম-হিসাব আলাদা function-এ নেওয়ানাহ্যাঁ — Extract Function
ভুল VAT হার ঠিক করাহ্যাঁ (ইচ্ছাকৃত)না — bug fix
চট্টগ্রামের জন্য নতুন delivery চার্জহ্যাঁ (ইচ্ছাকৃত)না — feature
ধীর loop-এর বদলে cacheআউটপুট না, গতি হ্যাঁসাধারণত আলাদা ধরা হয় — performance optimization
সব file-এ formatter চালানোনাআচরণ রক্ষা করে, কিন্তু অনেকে আলাদা “style” বদল বলেন

“আচরণ” কী, তা প্রসঙ্গ ঠিক করে। গতি সাধারণত observable behaviour ধরা হয় না, কিন্তু real-time system-এ গতিও চুক্তির অংশ। একটি public library-র function-এর নাম নিজেই চুক্তি (বদলালে ব্যবহারকারীর কোড ভাঙে), অথচ private helper-এর নাম কারও চুক্তি নয়।

মূল ধারণা — দুই টুপি

Kent Beck-এর একটি রূপক Fowler-এর বইয়ে আছে: two hats. কাজের সময় আপনি একবারে একটি টুপি পরেন — feature-এর টুপি (নতুন আচরণ যোগ, নতুন test) অথবা refactoring-এর টুপি (আচরণ একই, নতুন test নয়, শুধু কাঠামো)। দিনে দশবার টুপি বদলাতে পারেন, কিন্তু দুটি একসাথে নয়। এই একটি অভ্যাসই পরের সব নিয়মের ভিত্তি: আলাদা ধাপ, আলাদা test-এর প্রত্যাশা, আর Git-এ আলাদা commit.

২ · নিরাপত্তার জাল — আগে test, তারপর ছোট ধাপ, প্রতিটির পর commit

Refactoring-এর সংজ্ঞায় একটি প্রতিশ্রুতি আছে — আচরণ বদলাবে না। সেই প্রতিশ্রুতি রক্ষা হলো কি না, চোখে দেখে বলা যায় না; যাচাই করতে হয়। তাই Fowler-এর নিয়ম সরল: refactoring শুরুর আগে নিশ্চিত হোন যে কোডটির জন্য এমন test আছে, যা নিজে নিজে চলে আর নিজেই বলে পাস না ফেল (self-checking). Test আপনার নিরাপত্তার জাল — সার্কাসের দড়ির নিচের জালের মতো, যা থাকলে শিল্পী সাহস করে নতুন কৌশল চেষ্টা করেন।

পুরোনো কোডে test না থাকলে প্রথম কাজ হলো বর্তমান আচরণ ধরে রাখা — ঠিক হোক বা ভুল। এমন test-কে বলে characterization test; আর পুরো আউটপুট একবার সংরক্ষণ করে প্রতিবার হুবহু মেলালে তাকে অনেকে golden master test বলেন। পুরোনো, test-হীন কোডে কীভাবে প্রথম test বসাবেন, কোথায় “seam” খুঁজবেন — সেই কৌশল পর্ব ৪৩-এ। Test-এর গঠন (Arrange-Act-Assert) পর্ব ৪৬-এ, আর red-green-refactor চক্রে refactoring কোথায় বসে, তা পর্ব ৪৮-এ।

ছোট ধাপের ছন্দ

  1. সবুজ থেকে শুরু: সব test পাস করছে, working tree পরিষ্কার (git status-এ কিছু নেই — পর্ব ৮)।
  2. একটি ছোট refactoring: একটি rename, একটি extract — এমন বদল যা এক মিনিটে বুঝিয়ে বলা যায়।
  3. Test চালানো: সবুজ হলে commit; লাল হলে বদলটি ফেলে দিয়ে শেষ সবুজ commit-এ ফেরা (git restore, পর্ব ১১)। ভুল খোঁজার চেয়ে ফেলে দিয়ে আরও ছোট ধাপে আবার করা প্রায়ই দ্রুত।
  4. আবার: পরের ছোট ধাপ। ছোট commit পরে চাইলে interactive rebase দিয়ে মেলানো যায় (পর্ব ১৫) — ভাঙার চেয়ে মেলানো সহজ।

নিচের stepper-এ একটি “নিরীহ” বদল দেখুন: একটি কাল্পনিক অনলাইন বাজারের invoice-এ দুটি লাইনের ক্রম বদল (Slide Statements — লাইন ওপরে-নিচে সরানো)। পুরোনো test সবুজ থাকে, অথচ আচরণ বদলে গেছে — ধরা পড়ে কেবল সীমানার কাছের একটি test-এ।

1// আগে: আসল ক্রম2let t = sum(lines);3if (coupon && t > 1000) t -= 100;4t += deliveryFee(zone);5// পরে: "শুধু সাজানো"6let t = sum(lines);7t += deliveryFee(zone);8if (coupon && t > 1000) t -= 100;
Test-এর অবস্থা
কোন কোড
পুরোনো ৩টি test
সীমানার test (৯৮০ টাকা)
Git
  1. শুরু সবুজে। আসল ক্রম: আগে পণ্যের যোগফল t, তারপর coupon (১০০০ টাকার বেশি হলে ১০০ টাকা ছাড়), শেষে delivery চার্জ। তিনটি golden test পাস করছে।
  2. “নিরীহ” বদল। যোগফলের হিসাব পাশাপাশি রাখতে delivery-র লাইনটি coupon-এর ওপরে সরানো হলো — দেখে মনে হয় শুধু সাজানো।
  3. পুরোনো test সবুজ — মিথ্যা আশ্বাস। Karim-এর ১৭,১০৫ টাকায় দুই ক্রমেই coupon খাটে; Rina-র ৬৭৫ + ১২০ = ৭৯৫-এ কোনো ক্রমেই নয়; Mitu-র coupon নেই। কোনো test ১০০০-এর সীমানার কাছে নয়।
  4. সীমানার test আগে, পুরোনো কোডে। বদলটি stash করে নতুন test: Selim, ঢাকা, পণ্য ৯৮০ টাকা, coupon আছে। আসল ক্রমে ৯৮০ ১০০০-এর বেশি নয়, তাই ছাড় নেই: ৯৮০ + ৬০ = ১০৪০। পুরোনো কোডে সবুজ — আলাদা commit.
  5. বদলানো কোডে লাল। নতুন ক্রমে ৯৮০ + ৬০ = ১০৪০ আগে হয়, যা ১০০০-এর বেশি — তাই ১০০ টাকা ছাড়, ফল ৯৪০। এটি refactoring নয়, নিঃশব্দ আচরণ-বদল।
  6. শেষ সবুজ অবস্থায় ফেরা। খোঁজাখুঁজি না করে git restore src/invoice.js. শেষ commit মাত্র এক ধাপ আগে, তাই ক্ষতি কয়েক মিনিটের।
  7. সিদ্ধান্ত। এই slide নিরাপদ নয়, কারণ সরানো লাইনটি t বদলায়, যা পরের লাইন পড়ে। ব্যবসার নিয়মই যদি হয় “delivery-সহ মোটে coupon”, সেটি আলাদা fix: commit, মালিকের সম্মতিতে। চারটি test সবুজ; সীমানার test থেকে গেল।

সবুজ test মানে “এই test-গুলো যা দেখে, তা বদলায়নি” — এর বেশি নয়। কোন জায়গায় test লাগবে, তা বাছতে সীমানার মান (boundary value) খুব কাজের — ১০০০-এর শর্ত থাকলে ৯৯৯, ১০০০, ১০০১ আর delivery যোগের পরে সীমা পেরোনো মান। এই কৌশল পর্ব ৪৭-এ, আর test আসলে কতটা ধরতে পারে তা মাপার mutation testing পর্ব ৫০-এ।

৩ · Code smell — কোডের “গন্ধ” আর পাঁচটি দল

কোন কোড refactor করবেন? Fowler-এর বইয়ের তৃতীয় অধ্যায়, Kent Beck আর Fowler-এর যৌথ লেখা “Bad Smells in Code”, এর উত্তর দেয় একটি রূপক দিয়ে: code smell. শব্দটি Kent Beck-এর দেওয়া বলে Fowler নিজে উল্লেখ করেছেন। রান্নাঘর থেকে পোড়া গন্ধ এলে আপনি জানেন না ঠিক কী পুড়ছে — ভাত, না তরকারি — কিন্তু জানেন যে গিয়ে দেখতে হবে। Smell তেমনই: একটি উপরিতলের লক্ষণ, যা প্রায়ই গভীর সমস্যার ইঙ্গিত দেয়। লক্ষণ মানেই ভুল নয়; একটি ২০ লাইনের function একটি জটিল algorithm-এর জন্য একদম ঠিক হতে পারে। Smell শুধু বলে “এখানে একবার তাকান।”

বইয়ে smell-গুলো একটি লম্বা তালিকা (প্রথম সংস্করণে ২২টি, দ্বিতীয় সংস্করণে ২৪টি, আমাদের জানামতে)। এগুলোকে পাঁচটি দলে ভাগ করার জনপ্রিয় রীতিটি Fowler-এর নয় — Mika Mäntylä ও সহকর্মীদের ২০০৩ সালের একটি গবেষণাপত্র থেকে আসে বলে সাধারণত উল্লেখ করা হয়, আর পরে অনলাইন শিক্ষার সাইটগুলো এটি ছড়ায়। দলগুলো মনে রাখার জন্য সুবিধাজনক; পরীক্ষায় কেউ ভিন্ন ভাগ লিখলে সেটিও ভুল নয়। নাম নিয়েও একটি সতর্কতা: দ্বিতীয় সংস্করণে কয়েকটি নাম বদলেছে — Long Method হয়েছে Long Function, Switch Statements হয়েছে Repeated Switches, Lazy Class হয়েছে Lazy Element, আর Inappropriate Intimacy-র জায়গায় এসেছে Insider Trading; নতুন যোগ হয়েছে Mysterious Name, Global Data, Mutable Data আর Loops.

দলSmellলক্ষণ (একটি কাল্পনিক অনলাইন বাজারে)প্রধান refactoring
Bloaters
ফুলে ওঠা
Long functionplaceOrder() ১২০ লাইন: যাচাই, দাম, stock, SMS সব একসাথেExtract Function, Decompose Conditional
Large classOrderManager-এ ৪০টি method — চরম রূপে god classExtract Class
Long parameter listfee(order, zone, coupon, member, festival, weight)Introduce Parameter Object, পুরো object পাঠানো
Primitive obsessionটাকা float-এ, ফোন নম্বর খালি string-এ, যাচাই ২০ জায়গায়Replace Primitive with Object (value object)
Data clumpsdistrict, thana, road সবসময় একসাথে ঘোরেIntroduce Parameter Object, Extract Class
OO abusers
object-orientation-এর অপব্যবহার
Repeated switchshipping type-এর ওপর একই switch তিন function-এReplace Conditional with Polymorphism
Refused bequestGiftCard extends Product, কিন্তু weight() error ছোড়েPush Down Method, Replace Subclass with Delegate
Temporary fieldthis.festivalRate শুধু ঈদের হিসাবে ভরা, বাকি সময় nullExtract Class
Change preventers
বদলের বাধা
Divergent changeএকই Order class বদলায় কখনো VAT-এর নিয়মে, কখনো database বদলেSplit Phase, Extract Class
Shotgun surgeryনতুন জেলার delivery চার্জ যোগ করতে সাতটি fileMove Function, Combine Functions into Class
Dispensables
বাড়তি, বাদ দেওয়া যায়
Duplicated codeএকই VAT হিসাব cart, invoice আর SMS-এ কপিExtract Function, Pull Up Method
Dead code২০২২ সালের বন্ধ অফারের function, কেউ ডাকে নাRemove Dead Code (Git-এ ইতিহাস থেকেই যায়)
Speculative generalityএকটিমাত্র বাস্তবায়নের AbstractPaymentStrategyFactoryCollapse Hierarchy, Inline Function/Class
Comments as deodorant// এখানে কঠিন হিসাব, সাবধানে — তারপর ৩০ লাইনের ধাঁধাExtract Function (comment-টিই নাম), Rename
Lazy classclass TaxHelper-এর একটিই এক লাইনের methodInline Function, Inline Class
Couplers
অতিরিক্ত জড়ানো
Feature envyInvoice.deliveryFee(c) customer-এর তিনটি field পড়ে, নিজের একটিMove Function
Inappropriate intimacyCart সরাসরি Order._items বদলায়, Order পড়ে Cart._cacheMove Function/Field, Hide Delegate
Message chainsorder.customer().address().zone().fee()Hide Delegate
Middle manOrderFacade-এর ১০টির ৯টি method শুধু Order-এ পাঠায়Remove Middle Man, Inline

কয়েকটি জোড়া বিশেষ করে খেয়াল করুন, কারণ পরীক্ষায় এগুলো গুলিয়ে যায়। Divergent change আর shotgun surgery একই মুদ্রার দুই পিঠ: প্রথমটিতে এক module অনেক কারণে বদলায় (কম cohesion, পর্ব ২৭-এর SRP-র লঙ্ঘন); দ্বিতীয়টিতে এক কারণে অনেক module বদলায় (একটি নিয়ম ছড়িয়ে আছে)। Message chains আর middle man উল্টো দিকের বাড়াবাড়ি: শিকল কমাতে Hide Delegate করতে করতে class শুধু “পাঠানোর লোক” হলে সেটি middle man. Message chain পর্ব ২৬-এর Law of Demeter-এর লঙ্ঘনের চেনা রূপ। আর duplicated code সবসময় DRY-র লঙ্ঘন নয় — পর্ব ২৬-এই দেখেছেন, আজ দেখতে এক কিন্তু আলাদা কারণে বদলাবে এমন কোড (incidental duplication) জোর করে মেলালে উল্টো ক্ষতি।

Code smell থেকে refactoring — মানচিত্র

বাঁয়ে উনিশটি smell, দলের রং অনুযায়ী; ডানে প্রতিটির প্রধান প্রতিকার। একটি refactoring অনেক smell সারায়।

Code smell থেকে refactoring-এর মানচিত্র Code smell · লক্ষণ Refactoring · প্রতিকার Long function Large class Long parameter list Primitive obsession Data clumps Repeated switch Refused bequest Temporary field Divergent change Shotgun surgery Duplicated code Dead code Speculative generality Comments (deodorant) Lazy class Feature envy Inappropriate intimacy Message chains Middle man Introduce Parameter Object Replace Primitive with Object Replace Conditional with Polymorphism Extract Class Replace Subclass with Delegate Extract Function Remove Dead Code Move Function / Move Field Inline Function / Class Remove Middle Man Hide Delegate Bloaters · ফুলে ওঠা OO abusers · OO-র অপব্যবহার Change preventers · বদলের বাধা Dispensables · বাড়তি Couplers · অতি-জড়ানো
ছবি ৪২.১ — সবচেয়ে বেশি রেখা গেছে Extract Function, Extract Class, Move Function আর Inline-এ। এই চারটি আর Rename জানলেই বাস্তবের বেশিরভাগ smell-এর প্রথম ধাপ নেওয়া যায়। মানচিত্রটি প্রধান প্রতিকার দেখায়, একমাত্রটি নয় — যেমন long function-এ Decompose Conditional বা Replace Temp with Query-ও লাগতে পারে।

৪ · মূল refactoring আর তাদের mechanics

Fowler-এর catalog-এ প্রতিটি refactoring-এর একটি নাম, উদ্দেশ্য আর mechanics — ধাপের তালিকা, যার প্রতিটি ধাপের পর কোড চলে আর test চালানো যায়। নাম জানলে দলে সংক্ষেপে কথা বলা যায় (পর্ব ৩২-এর pattern-এর শব্দভাণ্ডারের মতো), আর IDE-র menu-তে প্রায় এই নামগুলোই থাকে। নিচে দশটি বহুল ব্যবহৃত refactoring; নাম দ্বিতীয় সংস্করণের, বন্ধনীতে প্রথম সংস্করণের নাম যেখানে আলাদা।

RefactoringকখনMechanics-এর সারকথাসাবধান
Extract Function (Extract Method)কোনো অংশ “কী করে” বুঝতে সময় লাগে; একই অংশ কয়েক জায়গায়উদ্দেশ্যের নামে নতুন function → অংশটি কপি → পড়া variable parameter, বদলানো variable return → পুরোনো জায়গায় ডাক → testবদলানো দুটি variable পরে লাগলে কঠিন
Inline Function (Inline Method)Function-এর দেহ তার নামের মতোই স্পষ্ট; অপ্রয়োজনীয় indirectionPolymorphic নয় নিশ্চিত হোন → ডাকের জায়গায় দেহ বসান → test → function মুছুনSubclass override করলে inline করা যায় না
Rename (variable, function, field)নাম উদ্দেশ্য বলে না: t, data2, process()সব ব্যবহার একসাথে বদলান; public হলে পুরোনো নাম সাময়িক রেখে deprecateString-এ লেখা নাম IDE ধরতে নাও পারে
Extract Variable (Introduce Explaining Variable)লম্বা expression-এর অংশের অর্থ বোঝা যায় নাঅংশটি side effect-মুক্ত কি না দেখুন → নাম দেওয়া const-এ রাখুনবারবার হিসাব হওয়ার কথা থাকলে ফল বদলায়
Introduce Parameter Objectকয়েকটি parameter সবসময় একসাথে (data clumps)নতুন object type → নতুন parameter যোগ → caller-রা object পাঠায় → পুরোনো parameter একে একে সরানObject-টি সাধারণত immutable রাখুন
Replace Conditional with Polymorphismধরনের ওপর একই switch বারবারপ্রতিটি ধরনের class আর একটি factory → caller factory ব্যবহার করে → এক এক শাখা subclass-এর method-এ → switch মুছুনএকটিমাত্র ছোট switch-এ এটি over-engineering
Move Function (Move Method)Function অন্য object-এর তথ্য বেশি ব্যবহার করে (feature envy)লক্ষ্য class-এ কপি → পুরোনোটি নতুনকে delegate করুক → caller সরান → পুরোনো মুছুনদুই class-এর মধ্যে cycle হলো কি না
Replace Temp with Queryএকটি temp variable একটি হিসাব ধরে রাখে, আর সেটি অন্য function-এও লাগেTemp একবারই assign হয় নিশ্চিত হোন → হিসাবটি function-এ → ব্যবহারগুলো ডাক দিয়ে বদলানহিসাবের উপাদান মাঝপথে বদলালে ফল বদলায় (অনুশীলন ২)
Decompose Conditionalজটিল if-এর শর্ত পড়ে “কেন” বোঝা যায় নাশর্ত নাম দেওয়া function-এ (isFestivalSeason(date)), প্রতিটি শাখাওশর্তে side effect থাকলে আগে আলাদা করুন
Replace Magic Literal (Replace Magic Number with Symbolic Constant)1000, 0.05, "dhaka" ব্যাখ্যা ছাড়া, অনেক জায়গায়নাম দেওয়া constant → প্রতিটি ব্যবহার দেখে বদলান → testFind-replace নয়; প্রতিটি জায়গার অর্থ আলাদা হতে পারে

শেষ সারির magic number-এর সতর্কতাটি গুরুত্বপূর্ণ: একটি file-এ 1000 তিনবার থাকলে তিনটি হয়তো তিন জিনিস — coupon-এর সীমা, ১০০০ গ্রাম = ১ কেজি, আর একটি page-এ সর্বোচ্চ সারি। সবগুলো COUPON_MIN করলে কোড “পরিষ্কার” দেখাবে, কিন্তু পরের বছর coupon-এর সীমা বদলালে ওজনের হিসাবও বদলে যাবে।

এবার সবচেয়ে বেশি ব্যবহৃত refactoring — Extract Function — একটি একটি ধাপে। কোডটি একটি কাল্পনিক অনলাইন বাজারের invoice: phone-এ ৫% gadget কর, বইয়ে ১০% ছাড়।

1function inv(it, nm, zone, coupon) {2  let t = 0;3  for (const x of it) {4    let a = x.qty * x.price;5    if (x.kind === "phone") a += a * 0.05;6    else if (x.kind === "book") a -= a * 0.1;7    t += a;8  }9  …coupon, delivery, return…10function lineAmount(x) { …লাইন ৪–৬…; return a; }
Extract-এর হিসাব
এখন করছি
parameter
return
লাগে না
test
  1. অংশ বাছাই আর নাম। লাইন ৪–৬ একটি কাজ করে: একটি লাইনের দাম। নাম দিন কী করে তা দিয়ে, কীভাবে নয় — lineAmount, calcWithTaxAndDiscount নয়। নাম খুঁজে না পেলে অংশটি হয়তো একটি কাজ করে না।
  2. নতুন function, অংশটি কপি। লাইন ১০-এ নতুন function, ভেতরে লাইন ৪–৬ কপি — সরানো নয়। পুরোনো কোড অক্ষত, তাই test এখনো সবুজ।
  3. Variable বিশ্লেষণ। x শুধু পড়া হয় — parameter. a অংশে জন্মায়, বদলায়, আর লাইন ৭-এ লাগে — return. t, it, nm অংশে নেই। দুটি বদলানো variable পরে লাগলে আগে অংশটি ভাঙুন।
  4. পুরোনো জায়গায় ডাক। লাইন ৪–৬ মুছে সেখানে let a = lineAmount(x);. Loop এখন তিন লাইনের, এক নজরে বোঝা যায়।
  5. Test চালানো। তিনটি golden আউটপুট হুবহু আগের মতো।
  6. Commit, তারপর আশেপাশে খোঁজা। refactor: extract lineAmount from inv. শেষ ধাপ: একই কোড আর কোথাও আছে কি? Cart page-এ কপি থাকলে সেখানেও এখন lineAmount — duplicated code-এর প্রতিকার।

এবার পুরো যাত্রা চালিয়ে দেখুন। নিচের কোডে মূল inv চারটি ধাপে বদলায়: (১) Extract Function, (২) Rename আর magic number-এর নাম, (৩) Introduce Parameter Object, (৪) দামের নিয়মে polymorphism-এর একটি হালকা রূপ — পণ্যের ধরন অনুযায়ী function-এর table. প্রতিটি ধাপের পর একটি ছোট test harness মূল function-এর আউটপুটের (golden) সাথে মেলায়।

function inv(it, nm, zone, coupon) {                    // step 0: the original
  let t = 0;
  for (const x of it) {
    let a = x.qty * x.price;
    if (x.kind === "phone") a += a * 0.05; else if (x.kind === "book") a -= a * 0.1;
    t += a;
  }
  if (coupon && t > 1000) t -= 100;
  return nm + ": " + (t + (zone === "dhaka" ? 60 : 120)).toFixed(2);
}
const amt = x => { let a = x.qty * x.price;              // step 1: Extract Function
  if (x.kind === "phone") a += a * 0.05; else if (x.kind === "book") a -= a * 0.1; return a; };
const inv1 = (it, nm, zone, coupon) => { let t = it.reduce((s, x) => s + amt(x), 0);
  if (coupon && t > 1000) t -= 100;
  return nm + ": " + (t + (zone === "dhaka" ? 60 : 120)).toFixed(2); };
const COUPON_MIN = 1000, COUPON_OFF = 100, deliveryFee = z => z === "dhaka" ? 60 : 120; // step 2
function invoiceText(lines, customer, zone, hasCoupon) {
  let total = lines.reduce((sum, line) => sum + amt(line), 0);
  if (hasCoupon && total > COUPON_MIN) total -= COUPON_OFF;
  return customer + ": " + (total + deliveryFee(zone)).toFixed(2);
}
const invoiceText3 = (lines, o) => invoiceText(lines, o.customer, o.zone, o.hasCoupon); // step 3
const rule = { phone: a => a * 1.05, book: a => a * 0.9 };   // step 4: polymorphism
const lineAmount = l => (rule[l.kind] ?? (a => a))(l.qty * l.price);
function invoiceText4(lines, { customer, zone, hasCoupon }) {
  let total = lines.reduce((sum, l) => sum + lineAmount(l), 0);
  if (hasCoupon && total > COUPON_MIN) total -= COUPON_OFF;
  return customer + ": " + (total + deliveryFee(zone)).toFixed(2);
}
const cart = [{ kind: "rice", qty: 10, price: 68 }, { kind: "phone", qty: 1, price: 15000 }, { kind: "book", qty: 3, price: 250 }];
const cases = [[cart, "Karim", "dhaka", true], [cart.slice(2), "Rina", "sylhet", true], [[], "Mitu", "dhaka", false]];
const want = cases.map(c => inv(...c)); console.log("golden:", want.join(" | "));
const asObj = f => (l, c, z, h) => f(l, { customer: c, zone: z, hasCoupon: h });
[["step1", inv1], ["step2", invoiceText], ["step3", asObj(invoiceText3)], ["step4", asObj(invoiceText4)]]
  .forEach(([n, f]) => console.log(n, cases.every((c, i) => f(...c) === want[i]) ? "PASS" : "FAIL"));

আউটপুট:

golden: Karim: 17065.00 | Rina: 795.00 | Mitu: 60.00
step1 PASS
step2 PASS
step3 PASS
step4 PASS

প্রথম লাইন তিনটি অর্ডারে মূল function-এর আউটপুট — এটিই প্রত্যাশা। Karim: চাল ৬৮০ + phone ১৫,০০০-এ ৫% কর মিলে ১৫,৭৫০ + বই ৭৫০-এ ১০% ছাড়ে ৬৭৫ = ১৭,১০৫; coupon-এ ১০০ ছাড়, ঢাকায় delivery ৬০ — ১৭,০৬৫। Rina-র ৬৭৫ টাকা ১০০০-এর নিচে, তাই coupon খাটে না, সিলেটে delivery ১২০ — ৭৯৫। Mitu-র খালি cart-এ শুধু delivery ৬০। চারটি ধাপই তিনটি আউটপুট হুবহু মেলায় (===), তাই PASS. ধাপ ৩-এ signature বদলেছে, তাই harness-এর ছোট adapter (asObj) পুরোনো ধাঁচের ডাককে object-এ বদলায় — বাস্তবে caller-দের একে একে সরাতে হয়। ধাপ ৪-এর rule table-এ rice নেই, তাই ?? দিয়ে “যেমন আছে তেমন”। Class দিয়ে পূর্ণ polymorphism পরের অংশে।

৫ · Replace Conditional with Polymorphism — switch থেকে class-এ

একটি switch নিজে দোষী নয়। সমস্যা হয় যখন একই ধরনের ওপর একই switch বারবার আসে: delivery-র খরচে, পৌঁছানোর সময়ে, ট্র্যাকিং-এর লেখায়। নতুন একটি ধরন (রাতের delivery) এলে প্রতিটি switch খুঁজে বদলাতে হয় — একটি বাদ পড়লে bug. এটিই repeated switch smell, আর প্রতিকার polymorphism: প্রতিটি ধরন নিজের class, আর প্রতিটি class জানে নিজের খরচ আর সময়। Caller শুধু shipping.cost(order) ডাকে। নকশার দিক থেকে এটি পর্ব ২৭-এর open-closed principle-এর বাস্তব রূপ, আর গঠনটি পর্ব ৩৫-এর strategy pattern-এর মতো (ধরন যদি object-এর জীবনে বদলায়, তবে পর্ব ৩৬-এর state pattern).

একটি কুরিয়ার অফিসে একজন কর্মীর সামনে মোটা খাতা: “সাধারণ পার্সেল হলে এই দাম, এক্সপ্রেস হলে ওই দাম, নিজে নিতে এলে বিনা খরচে”; সময়ের খাতা আরেকটি। নতুন সেবা এলে সব খাতা বদলাতে হয়। অন্য অফিসে প্রতিটি সেবার আলাদা ডেস্ক, যে নিজের দাম আর সময় জানে। নতুন সেবা মানে নতুন ডেস্ক; কাউন্টারের কর্মী শুধু গ্রাহককে ঠিক ডেস্কে পাঠান — সেটিই নিচের factory.

  1. switch ×২cost(), eta()শুরু
  2. factoryshippingFor(o)খালি class
  3. শাখা সরানোExpress.costএকে একে
  4. switch মুছুনo => s.cost(o)caller সরল
  5. নতুন ধরনclass Night+১ class
কাল্পনিক অনলাইন বাজারের shipping
ধরনের ওপর switch
class
test
নতুন ধরনে বদল লাগে
  1. শুরু: একই switch দুই জায়গায়। cost(o) আর eta(o) দুটিই o.ship দেখে standard, express, pickup বাছে। চারটি test সবুজ — এগুলোই পুরো যাত্রার নিরাপত্তার জাল।
  2. Class আর factory বানানো। তিনটি খালি class আর shippingFor(o), যা ধরন দেখে ঠিক class-এর object দেয়। কোনো caller এখনো এদের ব্যবহার করে না, তাই আচরণ বদলায়নি; commit.
  3. প্রথম শাখা সরানো। Express-এ cost(o) লেখা হলো, আর switch-এর express শাখা এখন শুধু shippingFor(o).cost(o) ডাকে। Test চালানো, সবুজ, commit — একবারে একটি শাখা।
  4. বাকি শাখা। Standard আর Pickup-এর cost একইভাবে, প্রতিটির পর test. Switch এখনো আছে, কিন্তু প্রতিটি শাখা শুধু class-কে পাঠায়।
  5. eta একই পথে, তারপর switch মুছুন। দুটি function-এর সব শাখা class-এ চলে গেলে switch-গুলো অর্থহীন। Caller এখন shippingFor(o).cost(o) ডাকে। ধরন দেখে বাছাই শুধু একটি জায়গায় — factory-র table-এ।
  6. অজানা ধরন। পুরোনো switch-এর default error ছুড়ত; factory-ও একই বার্তায় error ছোড়ে। Refactoring মানে এই কোণের আচরণও রক্ষা — test-এ এটিও যাচাই হয়।
  7. নতুন ধরন: রাতের delivery. একটি নতুন class Night আর table-এ একটি লাইন। কোনো পুরোনো switch বা class খুলতে হলো না। এটি আর refactoring নয় — নতুন আচরণ, তাই আলাদা feat: commit, নিজের test-সহ।
// BEFORE: the same switch on order.ship appears in two functions
function cost(o) {
  switch (o.ship) {
    case "standard": return o.kg <= 1 ? 60 : 60 + Math.ceil(o.kg - 1) * 20;
    case "express":  return 150 + Math.ceil(o.kg) * 30;
    case "pickup":   return 0;
    default: throw new Error("unknown ship: " + o.ship);
  }
}
function eta(o) {
  switch (o.ship) { case "standard": return 3; case "express": return 1; case "pickup": return 0; }
}
// AFTER: one class per shipping type; one factory keeps the only switch
class Standard { cost(o) { return o.kg <= 1 ? 60 : 60 + Math.ceil(o.kg - 1) * 20; } eta() { return 3; } }
class Express  { cost(o) { return 150 + Math.ceil(o.kg) * 30; } eta() { return 1; } }
class Pickup   { cost() { return 0; } eta() { return 0; } }
const TYPES = { standard: Standard, express: Express, pickup: Pickup };
function shippingFor(o) {
  const C = TYPES[o.ship]; if (!C) throw new Error("unknown ship: " + o.ship); return new C();
}
const old = { cost, eta };                  // the same tests run against both versions
const neu = { cost: o => shippingFor(o).cost(o), eta: o => shippingFor(o).eta(o) };
const tests = [[{ ship: "standard", kg: 0.5 }, 60, 3], [{ ship: "standard", kg: 2.5 }, 100, 3],
               [{ ship: "express", kg: 1.2 }, 210, 1], [{ ship: "pickup", kg: 9 }, 0, 0]];
for (const [name, v] of [["before", old], ["after ", neu]]) {
  const ok = tests.filter(([o, c, d]) => v.cost(o) === c && v.eta(o) === d).length;
  let err; try { v.cost({ ship: "drone", kg: 1 }); } catch (e) { err = e.message; }
  console.log(name, ok + "/" + tests.length, "green |", err);
}
class Night { cost(o) { return 200 + Math.ceil(o.kg) * 25; } eta() { return 0.5; } }
TYPES.night = Night;                        // new type: one class + one line, no switch edited
const n = { ship: "night", kg: 2 }; console.log("night:", neu.cost(n), "taka, eta", neu.eta(n), "day");

আউটপুট:

before 4/4 green | unknown ship: drone
after  4/4 green | unknown ship: drone
night: 250 taka, eta 0.5 day

একই চারটি test দুই সংস্করণে চলেছে — দুটিই ৪/৪। হিসাব মিলিয়ে নিন: standard ২.৫ কেজিতে প্রথম কেজি ৬০, বাকি ১.৫ কেজি উপরের দিকে গোল করে ২ কেজি × ২০ = ৪০, মোট ১০০; express ১.২ কেজি গোল করে ২ × ৩০ + ১৫০ = ২১০। অজানা ধরন drone-এ দুই সংস্করণ একই error দেয় — error-এর আচরণও রক্ষা করতে হয়। শেষ লাইনে Night যোগ হলো কোনো switch না খুলে: ২০০ + ২ × ২৫ = ২৫০ টাকা। বাছাই এখনো আছে, কিন্তু একটিই জায়গায় — factory-র TYPES table (পর্ব ৩৩-এর factory).

কখন এটি করবেন না: switch যদি একটিই থাকে, ছোট হয়, আর ধরনগুলো স্থির (যেমন সপ্তাহের সাত দিন), তবে তিনটি class আর একটি factory শুধু পড়ার বোঝা বাড়ায় — পর্ব ২৬-এর YAGNI. যেসব ভাষায় exhaustive switch আছে (যেমন TypeScript-এর discriminated union, Rust-এর match), সেখানে নতুন ধরন যোগ করলে compiler নিজেই অসম্পূর্ণ switch দেখায়। মূল দর-কষাকষি: class-এ নতুন ধরন যোগ সহজ, নতুন operation কঠিন; switch-এ উল্টো। আপনার system-এ কোনটি বেশি ঘটে, তা দেখে বাছুন।

৬ · Smell মেপে খোঁজা — AST দিয়ে একটি খেলনা detector আর feature envy

কিছু smell মাপা যায়: function-এর লাইন, parameter-এর সংখ্যা, nesting-এর গভীরতা, একই literal কতবার। Linter আর static analysis tool (পর্ব ৪৪-এ, code review-এর সাথে) ঠিক এভাবে কাজ করে — কোডকে চালায় না, পড়ে। পড়ার জন্য তারা source code থেকে বানায় একটি abstract syntax tree (AST) — কোডের গঠনের গাছ, যেখানে প্রতিটি function, if, loop একটি node. Python-এর standard library-র ast module দিয়ে নিজেই একটি ছোট detector লেখা যায়।

import ast
from collections import Counter
SAMPLE = '''
def fee(order, zone, coupon, member, festival, weight):
    if order:
        for item in order:
            if item["kind"] == "phone":
                if festival:
                    if member:
                        return "phone"
    return "dhaka" if zone == "dhaka" else "outside"

def label(zone):
    return "dhaka" if zone == "dhaka" else "outside"

def report(rows):
    print("dhaka", sum(r["amount"] for r in rows))
'''
LIMITS = {"lines": 6, "params": 4, "depth": 3, "repeats": 3}
BLOCKS = (ast.If, ast.For, ast.While, ast.With, ast.Try)
def depth(node, d=0):                     # deepest nesting of blocks
    kids = [depth(c, d + isinstance(c, BLOCKS)) for c in ast.iter_child_nodes(node)]
    return max(kids, default=d)
tree = ast.parse(SAMPLE)
for fn in [n for n in ast.walk(tree) if isinstance(n, ast.FunctionDef)]:
    size, params, deep = fn.end_lineno - fn.lineno + 1, len(fn.args.args), depth(fn)
    found = [f"long function ({size} lines)" * (size > LIMITS["lines"]),
             f"long parameter list ({params})" * (params > LIMITS["params"]),
             f"deep nesting ({deep})" * (deep > LIMITS["depth"])]
    print(f"{fn.name:6} line {fn.lineno:2}:", "; ".join(s for s in found if s) or "ok")
strings = Counter(n.value for n in ast.walk(tree)
                  if isinstance(n, ast.Constant) and isinstance(n.value, str))
for text, k in strings.most_common():
    if k >= LIMITS["repeats"]:
        print(f"duplicate literal {text!r} x{k} -> replace with a named constant")

আউটপুট:

fee    line  2: long function (8 lines); long parameter list (6); deep nesting (5)
label  line 11: ok
report line 14: ok
duplicate literal 'dhaka' x5 -> replace with a named constant

fee তিনটি সীমাই পেরিয়েছে: ৮ লাইন (সীমা ৬), ৬টি parameter (সীমা ৪), আর nesting ৫ স্তর — if → for → if → if → if. depth() recursion দিয়ে গাছ ঘোরে: প্রতিটি block-ধরনের node (If, For, While, With, Try) পেলে গভীরতা এক বাড়ে, আর সবচেয়ে গভীর পথের মান ফেরত দেয়। "..." * (শর্ত) একটি ছোট কৌশল: Python-এ True মানে ১, False মানে ০, তাই শর্ত মিথ্যা হলে খালি string. শেষ লাইনে "dhaka" পাঁচবার — fee-তে দুবার, label-এ দুবার, report-এ একবার; এটি magic literal আর duplicated code দুটিরই সংকেত। fee আর label-এর শেষ লাইন হুবহু এক — বাস্তব clone detector গাছের অংশ মিলিয়ে এটিও ধরে। সীমাগুলো (৬, ৪, ৩) এখানে ইচ্ছেমতো বাছা; বাস্তব tool-এ দল নিজেই ঠিক করে। কোনো সংখ্যাই “সঠিক” নয় — detector শুধু বলে কোথায় তাকাতে হবে।

Feature envy আর Move Function — গুনে দেখা

Feature envy-র লক্ষণ: একটি method নিজের object-এর চেয়ে অন্য একটি object-এর তথ্যে বেশি আগ্রহী। Fowler-এর নিয়মটি সহজ — যে তথ্য একসাথে বদলায়, সেই তথ্য আর তার আচরণ একসাথে রাখুন। নিচে Invoice.delivery_fee customer-এর তিনটি field পড়ে (zone, কত বছরের সদস্য, কতগুলো অর্ডার), আর নিজের মাত্র একটি (base). হিসাবটি customer-এর কাছে সরালে (Move Function) customer-এর নিয়ম বদলালে শুধু Customer খুলতে হয়। কোড দুই সংস্করণ চালিয়ে ফল মেলায়, আর AST দিয়ে প্রতিটি delivery_fee-এ কোন নামের object-এর attribute কতবার পড়া হলো তা গোনে।

import ast
BEFORE = '''class Customer:
    def __init__(self, zone, years, orders):
        self.zone, self.years, self.orders = zone, years, orders
class Invoice:
    def __init__(self, base): self.base = base
    def delivery_fee(self, c):            # envies Customer
        fee = 60 if c.zone == "dhaka" else 120
        if c.years >= 3 or c.orders > 20: fee = fee // 2
        return self.base + fee
'''
AFTER = '''class Customer:
    def __init__(self, zone, years, orders):
        self.zone, self.years, self.orders = zone, years, orders
    def delivery_fee(self):               # moved next to its data
        fee = 60 if self.zone == "dhaka" else 120
        return fee // 2 if self.years >= 3 or self.orders > 20 else fee
class Invoice:
    def __init__(self, base): self.base = base
    def delivery_fee(self, c): return self.base + c.delivery_fee()
'''
def accesses(src, method):                # attribute reads, grouped by owner name
    out = {}
    for cls in ast.parse(src).body:
        for fn in cls.body:
            if fn.name == method:
                names = [n.value.id for n in ast.walk(fn)
                         if isinstance(n, ast.Attribute) and isinstance(n.value, ast.Name)]
                out[cls.name] = {k: names.count(k) for k in sorted(set(names))}
    return out
for label, src in (("before", BEFORE), ("after ", AFTER)):
    env = {}; exec(src, env)
    fees = [env["Invoice"](500).delivery_fee(env["Customer"](*a)) for a in (("dhaka", 4, 2), ("rajshahi", 1, 5))]
    print(label, "fees", fees, "| reads in delivery_fee:", accesses(src, "delivery_fee"))

আউটপুট:

before fees [530, 620] | reads in delivery_fee: {'Invoice': {'c': 3, 'self': 1}}
after  fees [530, 620] | reads in delivery_fee: {'Customer': {'self': 3}, 'Invoice': {'c': 1, 'self': 1}}

দুই সংস্করণেই ফি একই: ঢাকার ৪ বছরের সদস্য ৫০০ + (৬০ ÷ ২) = ৫৩০, রাজশাহীর নতুন ক্রেতা ৫০০ + ১২০ = ৬২০ — আচরণ রক্ষা হয়েছে। আগে Invoice.delivery_fee-এ c-এর attribute ৩বার, self-এর ১বার: অন্যের তথ্যে বেশি আগ্রহ। পরে customer-এর তিনটি পড়াই Customer.delivery_fee-এর self-এ — নিজের তথ্য নিজের কাছে — আর Invoice-এ শুধু একটি ডাক c.delivery_fee() (AST-তে method ডাকও একটি attribute পড়া, তাই c: ১)। Invoice-এর পুরোনো method-টি রেখে দেওয়া হয়েছে, যা নতুনটিকে delegate করে — Move Function-এর mechanics-এর মাঝের ধাপ, যাতে caller-দের একসাথে বদলাতে না হয়। সব “অন্যের তথ্য পড়া” envy নয় — Strategy বা Visitor ইচ্ছা করেই তা করে।

৭ · IDE-র automated refactoring, rewrite আর কখন refactor নয়

IDE যখন refactor করে

আধুনিক IDE আর editor-এ (Java বা .NET-এর IDE, language server-সহ VS Code ইত্যাদি) Rename, Extract, Inline, Move, Change Signature একটি menu বা shortcut-এ পাওয়া যায় — automated refactoring. Tool কোডকে লেখা হিসেবে নয়, AST আর type দিয়ে বোঝে: total নামের দুটি আলাদা variable গুলিয়ে ফেলে না, যা find-and-replace প্রায়ই ফেলে। Extract Function-এ সে নিজেই parameter আর return বের করে।

অবস্থাAutomated refactoring কতটা ভরসার
Static type-এর ভাষা (Java, C#, Kotlin, TypeScript), পুরো project IDE-তে খোলাসাধারণত খুব ভরসার
Dynamic ভাষা (Python, JavaScript), type hint কমমাঝারি — getattr(obj, name) বা obj[key]-এর মতো চালানোর সময়ের নাম tool ধরতে পারে না
নাম string-এ: reflection, template, JSON/YAML config, SQL, URL routeদুর্বল — নিজে খুঁজতে হয়
Public API, যা অন্য repository বা অন্য দল ব্যবহার করেবাইরের caller ভাঙবে; পুরোনো নাম deprecate করে কিছুদিন রাখুন
Code generation, macro, annotation-ভিত্তিক frameworkFramework-ভেদে আলাদা; generated কোড আবার তৈরি করে test চালান

অর্থাৎ IDE-র refactoring হাতে করার চেয়ে অনেক নিরাপদ, কিন্তু test-এর বিকল্প নয়। Automated rename-এর পরেও test চালান, আর git diff দেখে নিন বদল শুধু যেখানে প্রত্যাশিত সেখানেই হয়েছে কি না (পর্ব ৯)।

Refactor না rewrite

পুরোনো জটিল কোড দেখে মনে হয়, “সব ফেলে নতুন করে লিখি।” একবারে শূন্য থেকে লিখে একদিনে বদলে ফেলাকে বলে big-bang rewrite. ঝুঁকি বড়: পুরোনো কোডের অদ্ভুত if-গুলো হয়তো অলিখিত bug fix; নতুনটি সমান হওয়ার আগে মাসের পর মাস কিছু চালু হয় না; আর সেই সময়ে দুই জায়গায় কাজ। Joel Spolsky-র ২০০০ সালের বহুল-উদ্ধৃত প্রবন্ধ “Things You Should Never Do” Netscape browser-এর নতুন করে লেখাকে এই ঝুঁকির উদাহরণ হিসেবে দেখায় (তাঁর বিশ্লেষণ, বিতর্কও আছে)।

Refactoring (ছোট ধাপে)Rewrite
চালু থাকে?প্রতিটি commit-এর পর system চলেনতুনটি শেষ না হওয়া পর্যন্ত কাজে আসে না
ঝুঁকিপ্রতিবার ছোট, test দিয়ে সীমিতশেষে একবারে বড়
কখন যুক্তিসঙ্গতপ্রায় সবসময় প্রথম পছন্দপ্রযুক্তিই অচল (ভাষা বা platform-এর সমর্থন নেই), কোড খুব ছোট, বা মূল প্রয়োজনই বদলে গেছে

মাঝামাঝি একটি পথ হলো ধীরে ধীরে প্রতিস্থাপন: পুরোনো system-এর পাশে নতুন অংশ বানিয়ে একটি একটি কাজ নতুনটিতে সরানো, যতক্ষণ না পুরোনোটি খালি হয়ে যায়। Fowler এর নাম দেন strangler fig (বট-অশ্বত্থের আত্মীয় এক ধরনের ডুমুর-জাতীয় গাছ, যা আশ্রয়-গাছকে ঘিরে বড় হয়ে একসময় তার জায়গা নেয়)। এই কৌশল বড় architecture-এর স্তরে System Design কোর্সের microservices পর্বে আর পুরোনো কোড সামলানোর প্রসঙ্গে পর্ব ৪৩-এ আছে।

কখন refactor করবেন না

  • Test নেই, বসানোর উপায়ও এখনই নেই: শুধু IDE-র সবচেয়ে নিরাপদ automated refactoring, নয়তো আগে characterization test (পর্ব ৪৩)।
  • কোডটি আর কেউ ছোঁবে না: কুৎসিত কিন্তু স্থির কোডের “সুদ” প্রায় শূন্য (technical debt-এর হিসাব পর্ব ৪৩-এ)।
  • Release-এর ঠিক আগে বা আগুন নেভানোর সময়: আগে ছোট fix, পরে শান্ত মাথায় refactor.
  • Feature-এর মাঝপথে: অর্ধেক feature আর অর্ধেক refactoring একসাথে মানে দুটিই অসমাপ্ত। আগে feature-এর অংশ সবুজে আনুন বা stash করুন, তারপর টুপি বদলান।
  • শুধু রুচির জন্য: অন্যের সাম্প্রতিক কোড নিজের ধাঁচে বদলানো review-তে ঘর্ষণ বাড়ায়; দলের style guide আগে।

Preparatory refactoring — আগে বদল সহজ করুন

Refactoring-এর সবচেয়ে লাভজনক সময় সাধারণত একটি নতুন feature-এর ঠিক আগে। Kent Beck-এর নামে প্রচলিত একটি উক্তি (২০১২ সালের একটি tweet বলে উল্লেখ করা হয়): “make the change easy (warning: this may be hard), then make the easy change” — আগে বদলটিকে সহজ করুন (সাবধান, এটি কঠিন হতে পারে), তারপর সহজ বদলটি করুন। একে বলে preparatory refactoring. অংশ ৪-এর invoice-এ চট্টগ্রামের নতুন delivery চার্জ যোগ করতে হলে প্রথমে delivery-র হিসাব deliveryFee(zone) function-এ বের করুন (refactor, test সবুজ, commit), তারপর সেখানে একটি সারি যোগ করুন (feat). Fowler একটি উপমা দেন: সোজা পূর্ব দিকে যেতে হলে কখনো কখনো আগে উত্তরে গিয়ে বড় রাস্তায় ওঠা দ্রুত। এর সাথেই যায় Don Roberts-এর নামে প্রচলিত rule of three: প্রথমবার লিখুন, দ্বিতীয়বার কপি করতে অস্বস্তি হলেও করুন, তৃতীয়বার refactor করুন।

৮ · Git-এ refactoring — “refactor:” আর “feat:” আলাদা commit-এ

দুই টুপির নিয়ম Git-এ এসে দাঁড়ায় একটি সরল অভ্যাসে: একটি commit-এ হয় শুধু আচরণ-রক্ষাকারী কাঠামোর বদল, নয়তো শুধু আচরণের বদল। পর্ব ১৮-এ দেখা Conventional Commits-এর ভাষায় প্রথমটি refactor:, দ্বিতীয়টি feat: বা fix:. (মূল Conventional Commits 1.0.0 নির্দিষ্টভাবে শুধু feat আর fix-এর অর্থ বেঁধে দেয়; refactor, test, docs-এর মতো type এসেছে Angular-এর রীতি আর commitlint-এর প্রচলিত config থেকে — দলভেদে তালিকা আলাদা।) প্রতিটি commit একটি atomic commit: একটি যুক্তিসংগত বদল, যার পরে test সবুজ।

কেন এত যত্ন? চারটি কারণ:

  • Review: refactor: commit-এর reviewer জানেন, এখানে আচরণ বদলানোর কথা নয় — test-এর file বদলায়নি, শুধু কাঠামো। ৩০০ লাইনের মিশ্র diff-এ একটি আচরণ-বদল লুকিয়ে থাকলে চোখে পড়া কঠিন (পর্ব ১৬, আর পর্ব ৪৪-এর code review).
  • Revert: নতুন feature-এ সমস্যা হলে শুধু feat: commit-টি git revert করা যায়, আগের refactoring অক্ষত থাকে।
  • Bisect: প্রতিটি commit ছোট আর সবুজ হলে git bisect ঠিক কোন বদলে bug ঢুকল তা দ্রুত দেখায় (পর্ব ৫৫)।
  • Blame: বড় rename বা format-এর commit git blame-কে “শেষ কে এই লাইন ছুঁয়েছেন” প্রশ্নে বিভ্রান্ত করে। Git ২.২৩ থেকে git blame --ignore-rev আর blame.ignoreRevsFile দিয়ে এমন commit বাদ দেওয়া যায় — কিন্তু শুধু তখনই, যদি সেই commit-এ কেবল কাঠামোর বদল থাকে।

নিচের transcript একটি আসল scratch repository থেকে (Git ২.৫৩-এ, লেখক Rahim, তারিখ নির্দিষ্ট করে দেওয়া, তাই hash পুনরুৎপাদনযোগ্য)। অংশ ৪-এর invoice-এর গল্প: প্রথমে feature, তারপর বর্তমান আচরণ ধরে রাখার test, তিনটি refactoring, শেষে একটি নতুন feature.

$ git log --oneline
1093bab feat: charge 90 taka delivery in chattogram
66f585c refactor: introduce order parameter object
0c13e91 refactor: rename inv to invoiceText and its variables
e55f503 refactor: extract lineAmount from inv
8ca8b97 test: pin current invoice output
958ddeb feat: add invoice text for orders

$ git diff --stat HEAD~4 HEAD~1
 src/invoice.js | 25 ++++++++++++++-----------
 1 file changed, 14 insertions(+), 11 deletions(-)

$ git show --stat --format="%h %s" HEAD
1093bab feat: charge 90 taka delivery in chattogram

 src/invoice.js       | 3 ++-
 test/invoice.test.js | 1 +
 2 files changed, 3 insertions(+), 1 deletion(-)

$ git log --oneline --grep="^refactor"
66f585c refactor: introduce order parameter object
0c13e91 refactor: rename inv to invoiceText and its variables
e55f503 refactor: extract lineAmount from inv

কী দেখা গেল:

  • git log --oneline: ছয়টি commit-এর গল্প নিজেই পড়া যায় — feat, test, তিনটি নামওয়ালা refactoring, feat.
  • git diff --stat HEAD~4 HEAD~1: test-এর commit (8ca8b97) থেকে শেষ refactor (66f585c) পর্যন্ত বদলেছে শুধু src/invoice.js — ১৪টি লাইন যোগ, ১১টি বাদ। test/ ছোঁয়া হয়নি। Refactoring-এর সময় test বদলাতে হলে থামুন — হয় test implementation-এ বাঁধা, নয়তো আপনি আচরণ বদলাচ্ছেন।
  • git show --stat HEAD: feat: commit-এ source আর test দুটিই বদলেছে — নতুন আচরণ, নতুন assertion (চট্টগ্রামে ৬৭৫ + ৯০ = ৭৬৫ টাকা)।
  • --grep="^refactor": বার্তার নিয়ম মানলে শুধু refactoring-এর commit আলাদা করে খোঁজা যায়।

প্রতিটি refactor commit-এ সত্যিই test সবুজ ছিল কি না, পরে যাচাই করতে git rebase -x (বা --exec) কাজের: প্রতিটি commit বসানোর পর command-টি চালায়, আর কোথাও ব্যর্থ হলে সেখানে থামে (পর্ব ১৫)।

$ git rebase -x "node test/invoice.test.js" HEAD~4
Executing: node test/invoice.test.js
ok
Executing: node test/invoice.test.js
ok
Executing: node test/invoice.test.js
ok
Executing: node test/invoice.test.js
ok
Successfully rebased and updated refs/heads/main.

শেষ চারটি commit-এর প্রতিটিতে test চলেছে আর ok দিয়েছে। (টার্মিনালে মাঝে “Rebasing (2/8)”-এর মতো অগ্রগতির লাইন মুছে-মুছে দেখায়; এখানে বাদ দেওয়া হলো। ৮ = চারটি commit বসানো + চারবার test.) কোনো commit বদলায়নি বলে hash একই থাকে। ব্যর্থ হলে rebase সেই commit-এ থেমে যেত। CI সাধারণত শুধু শেষ commit যাচাই করে (পর্ব ৫৮); প্রতিটি commit সবুজ রাখা তাই নিজের দায়িত্ব।

Review-এর ছোট কৌশল: Move Function-এর diff-এ একই লাইন এক জায়গায় লাল, আরেক জায়গায় সবুজ। git diff --color-moved (Git ২.১৫ থেকে) সরানো লাইন আলাদা রঙে দেখায়, আর git diff -w শুধু space-এর বদল লুকায়।

আরও গভীরে: মূল উৎস Fowler-এর Refactoring (২য় সংস্করণ ২০১৮; catalog-এর সংক্ষেপ refactoring.com-এ) আর Opdyke-এর ১৯৯২ সালের থিসিস “Refactoring Object-Oriented Frameworks”. Joshua Kerievsky-র Refactoring to Patterns (২০০৪) দেখায় smell থেকে ধাপে ধাপে মডিউল ৬-এর pattern-এ পৌঁছানোর পথ। Emerson Murphy-Hill ও সহকর্মীদের গবেষণা (২০০৯, ২০১২) অনুযায়ী developer-রা প্রায়ই feature-এর ফাঁকে ছোট refactoring করেন আর IDE-র automated tool কম ব্যবহার করেন — তবে তথ্যগুলো নির্দিষ্ট দল আর সময়ের।

পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এই ধরনের প্রশ্ন আসতে পারে:

  • Refactoring কী, আর bug fix থেকে কীভাবে আলাদা? — বাইরের আচরণ না বদলে ভেতরের কাঠামো উন্নত করা; bug fix ইচ্ছা করে আচরণ বদলায়।
  • Refactoring-এর আগে test কেন? — আচরণ বদলায়নি, তার একমাত্র দ্রুত আর নির্ভরযোগ্য প্রমাণ; test না থাকলে আগে characterization test.
  • পাঁচটি code smell আর তাদের প্রতিকার বলুন। — Long function → Extract Function; long parameter list → Introduce Parameter Object; repeated switch → Replace Conditional with Polymorphism; feature envy → Move Function; duplicated code → Extract Function.
  • Divergent change আর shotgun surgery-র পার্থক্য? — এক module অনেক কারণে বদলায় বনাম এক কারণে অনেক module বদলায়।
  • Refactor না rewrite — কীভাবে সিদ্ধান্ত নেবেন? — ছোট ধাপের refactoring প্রথম পছন্দ; rewrite শুধু প্রযুক্তি অচল বা প্রয়োজন মৌলিকভাবে বদলালে, তাও ধাপে ধাপে প্রতিস্থাপনে।

৯ · AI-এর যুগে refactoring

AI দ্রুত পরিষ্কার দেখতে কোড দেয়, কিন্তু refactoring-এর মূল প্রতিশ্রুতি — আচরণ একই — নিজে যাচাই করে না। সাধারণ ভুল আর ধরার উপায়:

AI-এর সাধারণ ভুলকীভাবে ধরবেন
“Refactor”-এর নামে আচরণ বদল — সীমানার শর্ত (> থেকে >=), গোল করার নিয়ম, error-এর ধরন বা লাইনের ক্রমআগে থেকে থাকা test আর সীমানার test চালান; পুরোনো আর নতুন সংস্করণে একই input-এ আউটপুট মেলান (golden master)
পুরো file নতুন করে লেখা — ৫টি ছোট ধাপের বদলে ৩০০ লাইনের একটি diffধাপে ধাপে চান, প্রতিটি ধাপ আলাদা commit; বড় diff মানে review অসম্ভব
Pattern-জ্বর — দুই শাখার if-এর জন্য তিনটি class, একটি interface আর একটি factoryজিজ্ঞেস করুন: কোন smell সারল? ধরন কি সত্যিই বাড়ছে? (পর্ব ২৬, ৩২)
Test-কেই “refactor” করে বদলানো, যাতে নতুন কোডে পাস করে — বা implementation-এর হুবহু প্রতিচ্ছবি এমন test লেখাRefactoring-এর commit-এ test/ বদলেছে কি না git diff --stat-এ দেখুন; ইচ্ছা করে ভুল ঢুকিয়ে দেখুন test ফেল করে কি না (mutation testing, পর্ব ৫০)
ভুল ইতিহাস বা নাম: “code smell শব্দটি Fowler-এর”, catalog-এ নেই এমন refactoringFowler-এর catalog আর মূল বই মিলিয়ে নিন
Git-এর “পরিষ্কার করার” পরামর্শে git reset --hard বা git push --force, সতর্কতা ছাড়া — বা অস্তিত্বহীন flagThrowaway clone-এ আগে চালান; git help reset পড়ুন; shared branch-এ force push নয় (পর্ব ১১, ১৫)
অজানা licence-এর কোড হুবহু কপিঅস্বাভাবিক লম্বা অংশের উৎস খুঁজুন; দলের licence নীতি মানুন

AI-কে “refactoring-এর সহকারী” হিসেবে ব্যবহার করুন — পরিকল্পনা আর smell খোঁজায়, যাচাইয়ে নিজে থেকে। কোড দেওয়ার আগে গোপন key, password, গ্রাহকের তথ্য আর অভ্যন্তরীণ URL মুছে নিন:

নিচে একটি কাল্পনিক অনলাইন বাজারের JavaScript function দিলাম (গোপন তথ্য মুছে দেওয়া), সাথে এর বর্তমান test. ১. কোডে কোন কোন code smell দেখছ, Fowler-এর নাম দিয়ে তালিকা দাও, লাইন নম্বরসহ। নিশ্চিত না হলে "সম্ভবত" লিখবে। ২. প্রতিটির জন্য একটি নামওয়ালা refactoring প্রস্তাব করো (যেমন Extract Function, Introduce Parameter Object). ৩. পুরো কোড একবারে নতুন করে লিখবে না। ৫–৮টি ছোট ধাপের পরিকল্পনা দাও; প্রতিটি ধাপ এমন হবে যে তার পরে test চলে, আর প্রতিটির জন্য একটি "refactor: ..." commit message দাও। ৪. কোনো ধাপ যদি আচরণ বদলাতে পারে (শর্তের সীমা, লাইনের ক্রম, গোল করা, error), সেটি আলাদা করে চিহ্নিত করো আর কোন test সেটি ধরবে তা লিখে দাও। ৫. Test-এর file বদলানোর প্রস্তাব দেবে না; বর্তমান test-এ যদি কোনো ফাঁক থাকে, নতুন test আলাদা তালিকায় দাও। ৬. কোনো Git command দিলে প্রতিটির ঝুঁকি বলো; reset --hard বা push --force দেবে না।

AI-এর refactoring হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১Programmer-রা কোড পরিষ্কার করে আসছেন কম্পিউটারের শুরু থেকেই। তাহলে ১৯৯০-এর দশকে “refactoring”-কে আলাদা নাম, থিসিস আর বইয়ের catalog দিতে হলো কেন?

কোড পরিষ্কার করা পুরোনো, কিন্তু তা ছিল ব্যক্তিগত দক্ষতা — অভিজ্ঞ programmer “অনুভবে” জানতেন কোথায় কী সরাতে হবে। ১৯৮০-র দশকের শেষে Smalltalk-এর মতো object-oriented পরিবেশে দলগুলো দেখল, framework-গুলো একবার নকশা করে শেষ হয় না; ব্যবহারের অভিজ্ঞতা থেকে বারবার কাঠামো বদলাতে হয়। প্রশ্ন দাঁড়াল: কাঠামো বদলালে চলমান program যে ভাঙবে না, তার নিশ্চয়তা কী?

William Opdyke-এর ১৯৯২ সালের থিসিস (Ralph Johnson-এর তত্ত্বাবধানে) এর একটি আনুষ্ঠানিক উত্তর দেয়: প্রতিটি refactoring-এর জন্য কিছু পূর্বশর্ত (precondition) — যেমন নতুন নামটি ওই scope-এ আগে থেকে নেই — যা মানলে আচরণ রক্ষা হয়। এই পূর্বশর্তগুলো যন্ত্রে যাচাইযোগ্য, তাই automated tool সম্ভব হলো; Brant ও Roberts-এর Smalltalk Refactoring Browser তার প্রথম দিকের সফল উদাহরণ। অর্থাৎ নাম আর সংজ্ঞা দেওয়ার ফলে “পরিষ্কার করা” একটি শিল্প থেকে একটি যাচাইযোগ্য পদ্ধতিতে পরিণত হলো।

Fowler-এর ১৯৯৯ সালের বই এই জ্ঞানকে সাধারণ developer-এর কাছে আনে। এর মূল শক্তি catalog-এর রূপে: প্রতিটি refactoring-এর নাম, প্রেরণা আর mechanics, ঠিক রান্নার বইয়ের রেসিপির মতো — উপকরণ, ধাপ, কোথায় সাবধান। নাম দেওয়ায় দুটি জিনিস ঘটল। প্রথমত, দল একটি শব্দভাণ্ডার পেল; “এখানে Move Function” বললে সবাই একই ধাপ বোঝেন। দ্বিতীয়ত, IDE-নির্মাতারা menu-তে এই নামগুলোই বসালেন, আর ২০০০-এর দশকে Java-র IDE-গুলোতে automated refactoring সাধারণ হয়ে গেল। ২০১৮ সালের দ্বিতীয় সংস্করণ JavaScript-এ লেখা হয় — Fowler-এর ব্যাখ্যায়, বেশিরভাগ পাঠক ভাষাটি পড়তে পারেন, আর তিনি দেখাতে চেয়েছিলেন refactoring শুধু class-ভিত্তিক OOP-এর জিনিস নয়, function-ভিত্তিক কোডেও খাটে; তাই অনেক নাম “Method” থেকে “Function” হলো।

মূল উপলব্ধি: refactoring-এর নতুনত্ব “পরিষ্কার করা”-য় নয়, বরং আচরণ রক্ষার শর্তগুলো স্পষ্ট করা, নাম দেওয়া আর ধাপে ভাঙায় — যা দলগত কথা আর automated tool দুটিই সম্ভব করেছে।

প্র ০২একটি দল বলছে, “এই পুরোনো কোড ঠিক করার চেয়ে নতুন করে লেখা সস্তা।” Netscape-এর ইতিহাস আর strangler fig-এর ধারণা মাথায় রেখে আপনি কী প্রশ্ন করবেন?

Joel Spolsky তাঁর ২০০০ সালের প্রবন্ধ “Things You Should Never Do, Part I”-এ Netscape-এর browser নতুন করে লেখার সিদ্ধান্তকে একটি কোম্পানির করা “সবচেয়ে খারাপ কৌশলগত ভুল”-এর উদাহরণ বলেন। তাঁর যুক্তি: পুরোনো কোড দেখতে কুৎসিত লাগে কারণ তাতে বছরের পর বছরের bug fix জমে আছে — প্রতিটি অদ্ভুত শর্ত হয়তো কোনো বিশেষ অবস্থার সমস্যার সমাধান। নতুন করে লিখলে সেই জ্ঞান ফেলে দেওয়া হয়, আর নতুন কোড একই ভুলগুলো আবার করে আবার শেখে। মাঝের লম্বা সময়ে প্রতিযোগীরা এগিয়ে যায়। এটি Spolsky-র বিশ্লেষণ; Netscape-এর পতনের আরও কারণ ছিল, আর নতুন কোডভিত্তি পরে অন্য প্রকল্পের ভিত্তি হয়েছিল বলেও যুক্তি আছে।

তাই প্রথম প্রশ্ন: “সস্তা” কীসের তুলনায় মাপছেন? লেখার সময় গোনা হয়, কিন্তু পুরোনো system-এর সব আচরণ খুঁজে মেলানো, ডেটা সরানো আর মাঝের সময়ে দুটি system চালানোর খরচ প্রায়ই বাদ পড়ে। আর rewrite-এর ইচ্ছা যদি আসে শুধু পুরোনো কোড বোঝার কষ্ট থেকে, তার প্রতিকার প্রায়ই বোঝার জন্য refactoring — নাম বদলানো, function ভাঙা। দ্বিতীয় প্রশ্ন: পুরোনো আচরণের তালিকা কোথায়? Characterization test না থাকলে “নতুনটি পুরোনোর সমান” প্রমাণের উপায় নেই। তৃতীয়: সমস্যাটি কি কোডের কাঠামোয়, না প্রযুক্তিতে? কাঠামোর সমস্যা হলে refactoring-ই উত্তর; ভাষা বা platform-এর সমর্থন বন্ধ হয়ে গেলে প্রতিস্থাপন যুক্তিসঙ্গত।

প্রতিস্থাপন হলেও “একদিনে বদল” এড়ানো যায়। Fowler-এর strangler fig-এর ধারণায় পুরোনো system-এর সামনে একটি পথ-নির্দেশক স্তর বসিয়ে একটি একটি কাজ — যেমন প্রথমে শুধু invoice, পরে শুধু delivery — নতুন অংশে পাঠানো হয়। প্রতিটি ধাপ চালু হয়, মাপা যায়, দরকারে ফিরিয়ে আনা যায়; পুরোনো অংশ ক্রমে শুকিয়ে যায়, যেমন লতানো ডুমুর-জাতীয় গাছ আশ্রয়-গাছকে ঘিরে ফেলে। এটি আসলে বড় মাপের refactoring-এর দর্শন: ছোট, চালু থাকা, ফেরানো যায় এমন ধাপ।

মূল উপলব্ধি: rewrite-এর আসল খরচ লুকানো জ্ঞান আর দীর্ঘ অচল সময়; বেশিরভাগ ক্ষেত্রে ছোট ধাপের refactoring বা ধাপে ধাপে প্রতিস্থাপন একই লক্ষ্যে কম ঝুঁকিতে পৌঁছায়।

প্র ০৩Code smell কি বস্তুনিষ্ঠ? একই function-কে একজন “লম্বা” বলেন, আরেকজন বলেন “ঠিক আছে”। Tool-এর সংখ্যা দিয়ে কি এই বিতর্ক মেটানো যায়?

Beck আর Fowler নিজেরাই স্পষ্ট বলেছেন, smell-এর জন্য কোনো নির্দিষ্ট সংখ্যা তাঁরা দেবেন না — কত লাইন বেশি, কতগুলো field অনেক — কারণ কোনো মাপকাঠি মানুষের অভিজ্ঞতার বিচারের সমান নয়। Smell একটি প্রশ্ন তোলে, উত্তর দেয় না। ২০ লাইনের একটি function, যা একটি সুপরিচিত algorithm একটানা লেখে আর যার প্রতিটি লাইন একই স্তরের abstraction-এ, ছয়টি ছোট function-এ ভাঙলে হয়তো পড়া কঠিনই হবে। আবার ৮ লাইনের একটি function-এ তিনটি আলাদা দায়িত্ব জট পাকাতে পারে।

গবেষণাও এই অস্পষ্টতা দেখায়। Mäntylä ও সহকর্মীদের কাজসহ কয়েকটি গবেষণার ফল অনুযায়ী, একই কোডে কোন smell আছে, তা নিয়ে developer-দের মধ্যে, এমনকি বিভিন্ন detection tool-এর মধ্যেও, মতের মিল প্রায়ই কম। কারণ একটি smell-এর পেছনে থাকে প্রসঙ্গ: কোডটি কতবার বদলায়, দলের অভিজ্ঞতা কেমন, ভাষার রীতি কী (Python-এ যা স্বাভাবিক, Java-তে তা অস্বাভাবিক হতে পারে)।

তবু সংখ্যা অকেজো নয়। Tool-এর সীমা (যেমন অংশ ৬-এর detector-এ ৬ লাইন, ৪টি parameter) দলের জন্য একটি আলোচনার শুরু — একটি সতর্কসংকেত, যা review-তে প্রশ্ন তোলে। বিপদ আসে যখন সংখ্যা লক্ষ্য হয়ে যায়: “সব function ১০ লাইনের নিচে” নিয়ম চাপালে মানুষ অর্থহীন টুকরো function বানায়, যার নাম part2() — Goodhart-এর সূত্রের চেনা রূপ, যেখানে মাপকাঠি লক্ষ্য হলে সেটি আর ভালো মাপকাঠি থাকে না। আরও কাজের একটি সংকেত হলো সময়ের সাথে বদল: যে file প্রায়ই বদলায় আর জটিল, সেটিই আগে refactor-এর যোগ্য (Git-এর ইতিহাস আর change coupling থেকে এটি বের করা যায়)। কম বদলানো জটিল কোডে smell থাকলেও তার খরচ কম।

অতএব smell-এর বিচার দলগত: tool তালিকা দেয়, review-তে মানুষ জিজ্ঞেস করেন, “এই কোড বদলাতে গিয়ে কি আমরা কষ্ট পাচ্ছি?” উত্তর হ্যাঁ হলে smell সত্যি, আর refactoring-এর প্রাপ্য।

মূল উপলব্ধি: code smell বিচারের ভিত্তিতে চলে, সংখ্যার ভিত্তিতে নয়; tool-এর সীমা আলোচনার সূচনা, আর অগ্রাধিকার ঠিক করে কোডটি কতবার বদলায়।

প্র ০৪Test নেই এমন একটি পুরোনো module-এ জরুরি বদল লাগবে। “Test ছাড়া refactor নয়” নিয়ম মানলে কি কাজই শুরু করা যাবে না? এখানে বাস্তবসম্মত পথ কী?

এটি একটি পরিচিত দুষ্টচক্র: test লিখতে কোড ভাঙতে হবে (কারণ function-টি database, ঘড়ি আর network সব একসাথে ছোঁয়), আর কোড ভাঙতে test লাগবে। Michael Feathers তাঁর Working Effectively with Legacy Code (২০০৪) বইয়ে legacy code-এর একটি বিখ্যাত সংজ্ঞা দেন — test ছাড়া কোড — আর এই চক্র ভাঙার কৌশলই বইয়ের বিষয়; বিস্তারিত পর্ব ৪৩-এ।

বাস্তবসম্মত পথের প্রথম ধাপ: সবচেয়ে নিরাপদ বদলগুলো দিয়ে শুরু, যেখানে ভুলের সম্ভাবনা প্রায় নেই। Static type-এর ভাষায় IDE-র automated rename বা extract — যার পূর্বশর্ত tool নিজে যাচাই করে — অনেক দল test ছাড়াও সাবধানে ব্যবহার করে, কারণ এগুলো Opdyke-এর অর্থে আচরণ-রক্ষাকারী। লক্ষ্য একটাই: function-টির ভেতরে একটি “seam” বানানো — এমন একটি জায়গা, যেখানে database বা ঘড়িকে বাইরে থেকে দেওয়া যায় (পর্ব ৩৭-এর dependency injection).

দ্বিতীয় ধাপ: seam পেলে characterization test — বর্তমান আচরণ যা আছে, তা-ই লিখে রাখা, এমনকি সেটি ভুল মনে হলেও। Test এখন “ঠিক” নয়, “বর্তমান” আচরণ পাহারা দেয়। ভুল মনে হলে একটি নোট রাখুন, কিন্তু refactoring-এর সময় বদলাবেন না; পরে আলাদা fix: commit-এ, প্রয়োজনে ব্যবসার মানুষের সম্মতিতে। অনেক ক্ষেত্রে golden master সবচেয়ে দ্রুত: অনেকগুলো input দিয়ে পুরো আউটপুট একবার সংরক্ষণ, তারপর প্রতিটি বদলের পর তুলনা।

তৃতীয়: বদলটি সত্যিই এক ঘণ্টার মধ্যে লাগলে ন্যূনতম ঝুঁকির ছোট fix, আলাদা commit-এ, আর একটি টিকিট: “এখানে test আর refactoring দরকার।” চাপের মধ্যে পুরো module পরিষ্কারের চেষ্টাই সবচেয়ে বিপজ্জনক।

মূল উপলব্ধি: “আগে test” নিয়মটি অনমনীয় বাধা নয়, একটি ক্রম — প্রথমে যন্ত্র-যাচাইকৃত নিরাপদ refactoring দিয়ে seam, তারপর characterization test, তারপর সাধারণ refactoring.

প্র ০৫একজন ব্যবস্থাপক বলছেন, “Refactoring-এ ব্যবসার কোনো লাভ দেখি না — নতুন কিছু তো আসে না। পুরো এক sprint শুধু refactoring-এর জন্য দেব না।” তিনি কি ভুল বলছেন?

আংশিক ঠিক। একটি আলাদা “refactoring sprint” বা “পরিষ্কারের মাস” প্রায়ই খারাপ ধারণা — কিন্তু ব্যবস্থাপক যে কারণে ভাবছেন, সে কারণে নয়। Fowler নিজে সতর্ক করেন, refactoring-কে আলাদা প্রকল্প হিসেবে দেখলে তা ব্যবসার কাছে বিক্রি করা কঠিন, আর বেশিরভাগ refactoring হওয়া উচিত দৈনন্দিন কাজের ভেতরে। Emerson Murphy-Hill ও Andrew Black (২০০৮) এই দুই ধরনের নাম দেন: floss refactoring — দাঁত মাজার মতো প্রতিদিন অল্প, feature-এর কাজের ফাঁকে — আর root canal refactoring — দাঁতের ব্যথা অসহ্য হলে বড় চিকিৎসা। প্রথমটি স্বাস্থ্যকর অভ্যাস, দ্বিতীয়টি অবহেলার ফল।

ব্যবস্থাপকের ভুল জায়গাটি হলো “লাভ দেখি না”। Refactoring-এর লাভ সরাসরি নতুন feature নয়, পরের feature-এর গতি। Preparatory refactoring-এর যুক্তি এখানে সবচেয়ে জোরালো: ধরা যাক, চট্টগ্রামের নতুন delivery চার্জ যোগ করতে সাতটি file খুলতে হয় (shotgun surgery) — তাহলে কাজটি দুই দিনের আর ভুলের ঝুঁকি বেশি; আগে আধা দিনে হিসাবটি এক জায়গায় আনলে feature-টি এক ঘণ্টার, আর পরের জেলাগুলোও এক ঘণ্টার। তাই দক্ষ দল refactoring-কে আলাদা করে “বিক্রি” করে না — এটি feature-এর estimate-এর অংশ, যেমন রাজমিস্ত্রি দেয়াল তোলার আগে মাপ নেওয়া আর ভারা বাঁধাকে আলাদা কাজ বলে বিল করেন না।

তবে developer-দেরও দায় আছে। দলে “refactoring” মানে যদি হয় “কয়েক দিন কিছু চলবে না, পুরো module নতুন করে লিখছি”, সেটি আসলে অ-ঘোষিত rewrite, আর ব্যবস্থাপকের সন্দেহ যুক্তিসঙ্গত। Git-এর ইতিহাসে ছোট সবুজ refactor: commit আর তারপর ছোট feat: commit — এগুলোই বিশ্বাস তৈরি করে। বড় জমে থাকা কাজে কোন অংশ কত ঘন ঘন বদলায় আর প্রতিবার কত সময় নষ্ট হয়, সেই হিসাবই ব্যবসার ভাষা (technical debt, পর্ব ৪৩)।

মূল উপলব্ধি: আলাদা refactoring sprint-এর বিরোধিতা যুক্তিসঙ্গত হতে পারে, কিন্তু সমাধান refactoring বাদ দেওয়া নয় — feature-এর কাজের ভেতরে ছোট, প্রস্তুতিমূলক, দৃশ্যমান commit-এ refactoring.

অনুশীলন

Smell চেনা, আউটপুট অনুমান, একটি পূর্ণ refactoring আর Git-এর ধাপসহ সমালোচনা — চারটিই পরীক্ষা ও ইন্টারভিউর ধরনের।

  1. Smell আর refactoring-এর নাম বলুন: প্রতিটি বর্ণনায় প্রধান code smell আর একটি উপযুক্ত refactoring লিখুন।
    1. order.getCustomer().getAddress().getZone().getFee() — চেকআউট page-এ আর SMS-এ দুই জায়গাতেই।
    2. কাল্পনিক বিশ্ববিদ্যালয়ের ফলাফল পোর্টালে GPA-র হিসাবে if (credit >= 3.0)-এর পাশে একটি comment: // 3.0 = full course, don't touch, আর একই 3.0 আরও চার file-এ।
    3. sendInvoice(name, phone, district, thana, road, amount), sendReceipt(name, phone, district, thana, road), printLabel(name, district, thana, road).
    4. ReportService বদলাতে হয় যখনই VAT-এর হার বদলায়, যখনই PDF library বদলায়, আর যখনই email-এর ঠিকানা বদলায়।
    5. class Penguin extends Bird, যেখানে fly() ছোড়ে Error("cannot fly").
    6. PaymentProcessorFactoryProvider, যার একটিমাত্র বাস্তবায়ন, আর commit বার্তা “for future gateways”, দুই বছর আগের।
    1. Message chains (Law of Demeter-এর লঙ্ঘন) → Hide Delegate: order.deliveryFee(), যা ভেতরে শিকলটি সামলায়; দুই জায়গার duplication-ও সারে।
    2. Magic number আর comments as deodorant, সাথে পাঁচ file-এ duplicated code → Replace Magic Literal: FULL_COURSE_CREDIT = 3.0 এক জায়গায়, সব ব্যবহার সেটি পড়ে; comment-টি তখন অপ্রয়োজনীয়, কারণ নামটিই ব্যাখ্যা। সব 3.0 একই অর্থের কি না, একে একে দেখুন।
    3. Data clumps (district, thana, road একসাথে) আর long parameter list → Introduce Parameter Object: Address (একটি value object), পরে হয়তো Customer-এ name, phone, address; তিনটি function তখন (customer, amount) বা (customer) নেয়।
    4. Divergent change — একটি class তিনটি আলাদা কারণে বদলায় (SRP-র লঙ্ঘন, পর্ব ২৭) → Extract Class বা Split Phase: TaxPolicy, PdfRenderer, ReportMailer; ReportService তিনটিকে জোড়ে।
    5. Refused bequest (LSP-র লঙ্ঘন, পর্ব ২৮) → Push Down Method (fly() শুধু উড়তে পারে এমন পাখির subclass বা interface-এ) বা Replace Subclass with Delegate, যেখানে উড়ার ক্ষমতা আলাদা object.
    6. Speculative generality → Collapse Hierarchy বা Inline Class: সরাসরি একমাত্র gateway class ব্যবহার। দ্বিতীয় gateway সত্যিই এলে তখন interface বের করা কয়েক মিনিটের refactoring (YAGNI, পর্ব ২৬)।
  2. আউটপুট অনুমান করুন (Replace Temp with Query-র ফাঁদ): একজন developer before-কে “Replace Temp with Query” দিয়ে after-এ বদলেছেন। Node.js-এ চালালে দুটি লাইন কী ছাপবে? বদলটি কি refactoring?
    let calls = 0;
    const cart = [100, 250, 50];
    const subtotal = () => { calls++; return cart.reduce((a, b) => a + b, 0); };
    function before() {
      const s = subtotal();
      cart.push(20);                       // a gift box is added
      return [s, s > 380 ? s - 30 : s];
    }
    function after() {                     // "Replace Temp with Query" applied
      const s = subtotal();
      cart.push(20);
      return [s, subtotal() > 380 ? subtotal() - 30 : subtotal()];
    }
    console.log(before(), calls);
    cart.length = 3; calls = 0;
    console.log(after(), calls);

    আসল আউটপুট (Node.js-এ চালিয়ে মেলানো):

    [ 400, 370 ] 1
    [ 400, 390 ] 3
    • before: s = ১০০ + ২৫০ + ৫০ = ৪০০, subtotal একবার ডাকা (calls ১)। তারপর gift box (২০) যোগ, কিন্তু s আগের ৪০০-ই। ৪০০ > ৩৮০, তাই ৪০০ − ৩০ = ৩৭০।
    • after: প্রথম s একই ৪০০। কিন্তু push-এর পর প্রতিটি subtotal() নতুন করে হিসাব করে: ৪২০। ৪২০ > ৩৮০, তাই ৪২০ − ৩০ = ৩৯০। ডাকা হলো মোট ৩বার (প্রথম s, শর্তে, আর বিয়োগে)।
    • রায়: refactoring নয় — আচরণ বদলেছে (৩৭০ থেকে ৩৯০)। Replace Temp with Query তখনই নিরাপদ, যখন temp-এর হিসাবের উপাদান (এখানে cart) temp তৈরি আর ব্যবহারের মাঝে বদলায় না। এখানে cart.push সেই শর্ত ভাঙে। আরেকটি সূক্ষ্মতা: calls ১ থেকে ৩ — হিসাবটি দামি বা side effect-ওয়ালা (যেমন database ডাকা) হলে এটিও আচরণের বদল। সমাধান: temp রেখে দিন।
  3. এই function refactor করুন: একটি কাল্পনিক কোচিং সেন্টারের ফলাফল আর মাসিক বেতনের কোড। Smell-গুলোর তালিকা দিন, ধাপের পরিকল্পনা (প্রতিটির commit message-সহ) দিন, আর শেষ রূপ লিখুন — আচরণ হুবহু একই রেখে।
    def p(s, m1, m2, m3, att, sch):
        t = m1 + m2 + m3
        if att < 75:
            t = t - 10
        # scholarship
        if sch == 1:
            f = 5000 - 5000 * 0.5
        elif sch == 2:
            f = 5000 - 5000 * 0.25
        else:
            f = 5000
        if t >= 240: g = "A"
        elif t >= 180: g = "B"
        else: g = "F"
        print(s + " " + g + " " + str(f))

    Smell: অস্পষ্ট নাম (p, t, f, g — দ্বিতীয় সংস্করণের Mysterious Name); long parameter list আর data clump (m1, m2, m3); magic number (৭৫, ১০, ৫০০০, ২৪০, ১৮০); type code (sch ১ বা ২); একই function-এ ফল আর বেতন — দুটি আলাদা কারণে বদলাবে (divergent change); comment as deodorant (# scholarship); আর হিসাব ও ছাপা একসাথে, তাই test করা কঠিন।

    ধাপ (প্রতিটির পর golden test — পুরোনো আর নতুন আউটপুট মেলানো):

    1. test: pin output of p for four students — আগে characterization test, print-এর আউটপুট ধরে।
    2. refactor: name magic numbers in p
    3. refactor: extract monthly_fee and grade_for from p
    4. refactor: introduce Marks parameter object
    5. refactor: separate result_line from printing — হিসাব ফেরত দেয়, ছাপে caller.
    MIN_ATTENDANCE, LOW_ATTENDANCE_PENALTY = 75, 10
    MONTHLY_FEE = 5000
    SCHOLARSHIP_SHARE = {1: 0.5, 2: 0.25}      # scholarship code -> share waived
    GRADE_CUTS = [(240, "A"), (180, "B")]
    
    @dataclass(frozen=True)
    class Marks:
        first: int
        second: int
        third: int
        def total(self): return self.first + self.second + self.third
    
    def course_total(marks, attendance):
        penalty = LOW_ATTENDANCE_PENALTY if attendance < MIN_ATTENDANCE else 0
        return marks.total() - penalty
    
    def grade_for(total):
        return next((g for cut, g in GRADE_CUTS if total >= cut), "F")
    
    def monthly_fee(scholarship):
        return MONTHLY_FEE - MONTHLY_FEE * SCHOLARSHIP_SHARE.get(scholarship, 0)
    
    def result_line(student, marks, attendance, scholarship):
        return f"{student} {grade_for(course_total(marks, attendance))} {monthly_fee(scholarship)}"

    চারজনের ওপর দুই সংস্করণ চালিয়ে মেলানো হয়েছে, চারটিতেই হুবহু এক: Rafi A 2500.0, Sumi F 3750.0, Joy F 5000, Nila B 5000 (Sumi-র ১৮৫ থেকে উপস্থিতির জন্য ১০ কাটা — ১৭৫, তাই F). সূক্ষ্ম ফাঁদ: বৃত্তি না থাকলে পুরোনো কোড ছাপে 5000 (int), থাকলে 2500.0 (float). নতুন কোডে .get(scholarship, 0)-এর default 0.0 দিলে ছাপা হতো 5000.0 — আউটপুট বদলে যেত, আর golden test ঠিকই লাল হতো। Observable behaviour-এ আউটপুটের রূপও পড়ে; অদ্ভুততাটি সারাতে চাইলে আলাদা fix: commit.

  4. এই pull request-এর সমালোচনা করুন আর Git-এর ধাপ লিখুন: একটি কাল্পনিক অনলাইন বাজারের দলে একটি PR এসেছে: শিরোনাম “refactor: clean up payment module”; একটিই commit; ১,৮০০ লাইন বদল — ১২টি file-এর নাম বদল, পুরো module-এ formatter, একটি bKash-ধাঁচের মোবাইল-ওয়ালেটের নতুন বিকল্প, আর একটি পুরোনো bug fix (দশমিক গোল করা)। একটি test মুছে ফেলা হয়েছে, বিবরণে লেখা “failing after refactor”. (ক) Review-তে কী বলবেন? (খ) লেখক কীভাবে commit-টি ভেঙে আলাদা করবেন — command-এর ক্রম দিন।

    (ক) Review-এর মূল কথা:

    • শিরোনাম ভুল: নতুন payment বিকল্প আর গোল করার fix আচরণ বদলায় — refactoring নয়। “refactor:” লেবেলে reviewer আচরণ-বদল খুঁজবেন না।
    • মুছে ফেলা test সবচেয়ে বড় লাল পতাকা: refactoring-এর পর test ফেল মানে হয় আচরণ বদলেছে, নয়তো test implementation-এ বাঁধা ছিল। সম্ভবত গোল করার fix-ই কারণ — তাহলে test-টি fix-এর commit-এ নতুন প্রত্যাশায় বদলাবে, মুছবে না।
    • ১,৮০০ লাইন review করা যায় না: formatter আর rename-এর হাজার লাইনের মধ্যে একটি payment-এর শর্ত চোখে পড়বে না। আলাদা করলে format-এর commit-টি blame.ignoreRevsFile-এ রাখা যায়।
    • সুর: “পরিশ্রম চোখে পড়ছে; চারটি আলাদা PR বা অন্তত চারটি commit হলে আমি নিশ্চিন্তে approve করতে পারি। মুছে ফেলা test-টি কেন ফেল করছিল?”

    (খ) Commit ভাঙার একটি ক্রম (PR-এর branch payment-cleanup, এখনো merge হয়নি ধরে):

    git switch -c payment-split main       # fresh branch; payment-cleanup stays as a backup
    npx prettier --write src/payment       # the team's formatter, formatting only
    git commit -am "style: format payment module"
    git restore --source=payment-cleanup -- src test   # bring in the rest, unstaged
    git add -N .                           # let add -p see new (renamed) files
    git add -p                             # pick rename / extract hunks only
    git commit -m "refactor: rename payment files and extract helpers"
    git add -p                             # pick the rounding change and its test
    git commit -m "fix: round payment totals to two decimals"
    git add -A
    git commit -m "feat: add mobile wallet payment option"
    git diff payment-cleanup               # only intended differences may remain
    git rebase -x "npm test" main          # every commit must be green

    ধারণা: আগে main থেকে শুধু formatter চালিয়ে style: commit, তারপর git restore --source দিয়ে পুরোনো branch-এর শেষ অবস্থা working tree-তে আনা (source-এ নেই এমন tracked file, যেমন নাম বদলের আগের file, মুছেও যায় — scratch repository-তে যাচাই করা)। git add -p একটি file-এর ভেতরের আলাদা অংশ (hunk) বেছে stage করতে দেয়, তাই একই file-এর refactor আর fix আলাদা commit-এ যায়। git diff payment-cleanup দেখায় কোনো কোড হারায়নি — পার্থক্য শুধু ইচ্ছাকৃত জায়গায়, যেমন fix-এর commit-এ ফিরিয়ে আনা test. Branch আগেই push হয়ে থাকলে নতুন branch দিয়ে PR হালনাগাদ করুন, অথবা --force-with-lease দিয়ে, দলকে জানিয়ে (পর্ব ১৫, আর undo-র খুঁটিনাটি পর্ব ১১)।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি নতুন feature যোগের আগে preparatory refactoring-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🧪 সব test চালিয়ে সবুজ আর working tree পরিষ্কার নিশ্চিত করা
  2. ✂️ একটি ছোট refactoring করা, যেমন Extract Function
  3. ✅ আবার test চালানো — সবুজ
  4. 📌 “refactor:” বার্তায় commit করা
  5. ✨ নতুন আচরণ আর তার test যোগ করে আলাদা “feat:” commit করা

Martin Fowler-এর সংজ্ঞা অনুযায়ী নিচের কোন বদলটি refactoring?

  • ভুল VAT হার ১৫% থেকে ঠিক করে ৭.৫% করা
  • একটি private function-এর নাম calc2 থেকে deliveryFee করা, সব ডাকের জায়গাসহ, test আগের মতো সবুজ
  • নতুন একটি payment পদ্ধতি যোগ করা
  • ভুল input-এ error ছোড়ার বদলে নিঃশব্দে খালি তালিকা ফেরত দেওয়া

Invoice class-এর delivery_fee(c) method customer object c-এর zone, years আর orders — তিনটি field পড়ে, আর নিজের object-এর শুধু base field. প্রধান code smell আর তার প্রতিকার কোনটি?

  • Shotgun surgery — Inline Class
  • Long parameter list — Introduce Parameter Object
  • Feature envy — Move Function (হিসাবটি Customer-এ)
  • Middle man — Remove Middle Man

একটি দলে নতুন জেলার delivery চার্জ যোগ করতে প্রতিবার cart, invoice, SMS, report, admin panel, API আর test-data — সাতটি file-এ ছোট ছোট বদল লাগে। কোন smell?

  • Shotgun surgery
  • Divergent change
  • Refused bequest
  • Lazy class

Extract Function করার সময় বাছাই করা অংশের ভেতরে variable a তৈরি হয় ও বদলায়, আর অংশের পরের লাইনে a ব্যবহার হয়; variable x অংশে শুধু পড়া হয়। নতুন function-এর সঠিক রূপ কোনটি?

  • a-কে parameter হিসেবে নেওয়া, কিছু ফেরত না দেওয়া
  • x আর a দুটিকেই global variable বানানো
  • কোনো parameter না নিয়ে x ফেরত দেওয়া
  • x-কে parameter হিসেবে নেওয়া আর a ফেরত দেওয়া

একটি branch-এ পরপর তিনটি commit: “refactor: extract deliveryFee”, “refactor: introduce order parameter object”, “feat: add night delivery”. Release-এর পর night delivery-তে সমস্যা ধরা পড়ল। এই commit-বিন্যাসের সবচেয়ে বড় সুবিধা কোনটি?

  • Git পুরোনো version মনে রাখে, যা মিশ্র commit-এ রাখত না
  • শুধু “feat:” commit-টি revert করা যায়, আগের refactoring অক্ষত রেখে
  • Refactor commit-গুলোতে test চালানোর দরকার হয় না
  • Git refactor: commit-গুলো merge-এর সময় নিজে থেকে বাদ দেয়

কোড: const s = subtotal(); cart.push(gift); return s > 1000 ? s - 100 : s; — একজন developer “Replace Temp with Query” করে শেষ লাইন লিখলেন return subtotal() > 1000 ? subtotal() - 100 : subtotal(); — subtotal() প্রতিবার cart-এর বর্তমান যোগফল দেয়। এই বদল সম্পর্কে কোনটি সঠিক?

  • এটি সবসময় নিরাপদ, কারণ subtotal() একই function
  • শুধু গতি কমবে, ফল একই থাকবে
  • ফেরত মান gift-সহ নতুন যোগফলে হিসাব হবে, তাই আচরণ বদলাতে পারে — এটি refactoring নয়
  • এটি syntax error দেবে

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Refactoring মানে observable behaviour না বদলে ভেতরের কাঠামো উন্নত করা (Fowler, ১৯৯৯; ২য় সংস্করণ ২০১৮)। Bug fix, feature বা গতি বাড়ানো আলাদা কাজ — দুই টুপি, একবারে একটি।
  2. নিরাপত্তার জাল: আগে test (না থাকলে characterization test), তারপর ছোট ধাপ, প্রতিটির পর test আর commit; লাল হলে শেষ সবুজ commit-এ ফেরা। সীমানার মানে test রাখুন — পুরোনো test সবুজ থাকলেও আচরণ বদলাতে পারে।
  3. Code smell লক্ষণ, রোগনির্ণয় নয়। পাঁচ দল: bloaters, OO abusers, change preventers, dispensables, couplers (দলভাগটি Mäntylä ও সহকর্মীদের, ২০০৩)। Divergent change বনাম shotgun surgery, message chains বনাম middle man — জোড়ায় মনে রাখুন।
  4. মূল refactoring-এর mechanics: Extract Function-এ পড়া variable parameter, বদলানো variable return; Replace Conditional with Polymorphism-এ একে একে শাখা সরানো, ধরন বাছাই শুধু factory-তে; Replace Temp with Query আর Slide Statements নিরাপদ কেবল উপাদান মাঝপথে না বদলালে।
  5. Git-এ: “refactor:” আর “feat:”/“fix:” আলাদা commit — review, revert, bisect আর blame সহজ। IDE-র automated refactoring ভরসার, তবু test আর git diff. Rewrite শেষ উপায়; preparatory refactoring প্রথম অভ্যাস।

পরের পর্বে (পর্ব ৪৩): যে কোড বহু বছর ধরে চলছে আর যার test নেই — technical debt, legacy code আর maintenance. Lehman-এর সূত্র কেন বলে software বয়সের সাথে জটিল হয়, আর পুরোনো কোডে প্রথম test কীভাবে বসাবেন।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান