Overview
Refactoring transforms existing code to improve its internal structure while preserving external functionality. The practice emerged from the observation that software systems accumulate structural debt over time as requirements change and features accumulate. Martin Fowler's seminal work cataloged and formalized these transformations, establishing refactoring as a disciplined engineering practice rather than ad-hoc code cleanup.
The core premise distinguishes between two modes of software development: adding functionality and restructuring code. During refactoring, tests remain green throughout the process. Each transformation represents a small, verifiable step that maintains system behavior. This incremental approach reduces risk and enables continuous improvement of codebases.
Refactoring addresses code smells—symptoms indicating deeper structural problems. A long method suggests missing abstractions. Duplicated code signals the need for extraction. Feature envy indicates misplaced responsibilities. These smells guide developers toward specific refactoring techniques that resolve underlying design issues.
# Before: Code smell - long method with multiple responsibilities
def process_order(order_data)
total = order_data[:items].sum { |item| item[:price] * item[:quantity] }
tax = total * 0.08
shipping = total > 100 ? 0 : 15
grand_total = total + tax + shipping
if order_data[:payment_method] == 'credit_card'
# 20 lines of credit card processing
elsif order_data[:payment_method] == 'paypal'
# 20 lines of PayPal processing
end
# 15 lines of inventory updates
# 10 lines of email notifications
end
# After: Refactored into single-responsibility methods
def process_order(order_data)
total = calculate_totals(order_data)
process_payment(order_data[:payment_method], total)
update_inventory(order_data[:items])
send_notifications(order_data[:customer], total)
end
The practice integrates with test-driven development and continuous integration workflows. Automated test suites verify that refactoring preserves behavior. Version control systems provide safety nets for experimental changes. Static analysis tools detect code smells automatically, suggesting refactoring opportunities.
Key Principles
Refactoring operates under the constraint that external behavior remains unchanged. This principle distinguishes refactoring from rewriting or feature development. The system's API, its responses to inputs, and its observable effects must remain identical after transformation. This constraint enables confident, incremental improvement without breaking client code.
The principle of small steps guides the refactoring process. Each transformation represents a single, atomic change—extracting one method, renaming one variable, or moving one field. Small steps maintain a working system at all times. Developers commit changes frequently, creating a trail of verified transformations. If a step introduces errors, the small scope simplifies diagnosis and rollback.
Code ownership affects refactoring strategy. Modifying published APIs requires different techniques than refactoring internal implementation details. Public interfaces need deprecation strategies and migration paths. Internal refactoring proceeds freely since no external code depends on implementation details. This distinction shapes refactoring scope and risk assessment.
Refactoring priorities emerge from pain points in development. Frequently modified code receives attention first. Complex areas that slow feature development become candidates. Code that causes recurring bugs needs structural improvement. This opportunistic approach maximizes return on refactoring investment by targeting areas where improvement yields immediate benefits.
The two-hat metaphor clarifies when to refactor. Under the feature-development hat, developers add functionality without restructuring. Under the refactoring hat, they improve structure without adding features. Switching hats consciously prevents mixing concerns. Attempting both simultaneously increases risk and complexity.
Refactoring preserves behavior through comprehensive test coverage. Unit tests verify that individual components maintain their contracts. Integration tests ensure that component interactions remain stable. The test suite acts as a specification of current behavior, catching unintended changes during refactoring. Without tests, refactoring becomes risky restructuring.
Preparatory refactoring makes subsequent feature additions simpler. Before implementing a feature, developers refactor the codebase to accommodate the new functionality naturally. This approach surfaces design issues early and creates clearer integration points. The pattern follows the adage: "Make the change easy, then make the easy change."
Ruby Implementation
Ruby's dynamic nature provides unique refactoring capabilities and challenges. Method definitions can change at runtime. Modules and classes remain open for modification. These features enable powerful metaprogramming but complicate static analysis and IDE support compared to statically-typed languages.
Extract Method represents one of the most common Ruby refactorings. Ruby's block syntax and flexible parameter handling make method extraction particularly clean. The extracted method captures local variables automatically through closure semantics when using lambdas or procs.
# Before: Complex calculation embedded in method
def generate_report(users)
report = []
users.each do |user|
revenue = user.orders.map { |o| o.items.sum(&:price) }.sum
commission = revenue * (user.premium? ? 0.15 : 0.10)
report << "#{user.name}: $#{revenue} (commission: $#{commission})"
end
report.join("\n")
end
# After: Extracted methods with clear responsibilities
def generate_report(users)
users.map { |user| user_report_line(user) }.join("\n")
end
def user_report_line(user)
revenue = calculate_user_revenue(user)
commission = calculate_commission(revenue, user)
"#{user.name}: $#{revenue} (commission: $#{commission})"
end
def calculate_user_revenue(user)
user.orders.sum { |order| order.items.sum(&:price) }
end
def calculate_commission(revenue, user)
rate = user.premium? ? 0.15 : 0.10
revenue * rate
end
Replace Conditional with Polymorphism converts type-based conditionals into polymorphic method dispatch. Ruby's duck typing and open classes facilitate this refactoring. The pattern replaces case statements or if-else chains with method calls resolved through Ruby's method lookup mechanism.
# Before: Type checking with conditionals
class DocumentExporter
def export(document, format)
case format
when :pdf
generate_pdf_header + document.content + generate_pdf_footer
when :html
"<html><body>#{document.content}</body></html>"
when :markdown
"# #{document.title}\n\n#{document.content}"
end
end
end
# After: Polymorphic exporters
class PdfExporter
def export(document)
generate_header + document.content + generate_footer
end
private
def generate_header
# PDF header logic
end
def generate_footer
# PDF footer logic
end
end
class HtmlExporter
def export(document)
"<html><body>#{document.content}</body></html>"
end
end
class MarkdownExporter
def export(document)
"# #{document.title}\n\n#{document.content}"
end
end
# Usage with strategy pattern
exporter = MarkdownExporter.new
exporter.export(document)
Introduce Parameter Object groups related parameters into a cohesive object. Ruby's keyword arguments and struct creation make this refactoring straightforward. The pattern reduces method signatures and creates opportunities for extracting behavior into the parameter object.
# Before: Long parameter list
def create_user(first_name, last_name, email, street, city, state, zip, phone)
user = User.new(first_name: first_name, last_name: last_name, email: email)
user.address = Address.new(street: street, city: city, state: state, zip: zip)
user.phone = phone
user.save
end
# After: Parameter object with keyword arguments
class UserCreationParams
attr_reader :first_name, :last_name, :email, :address, :phone
def initialize(first_name:, last_name:, email:, street:, city:, state:, zip:, phone:)
@first_name = first_name
@last_name = last_name
@email = email
@address = Address.new(street: street, city: city, state: state, zip: zip)
@phone = phone
end
end
def create_user(params)
user = User.new(first_name: params.first_name,
last_name: params.last_name,
email: params.email)
user.address = params.address
user.phone = params.phone
user.save
end
Replace Magic Numbers with Named Constants improves code clarity. Ruby's constant scoping and convention of SCREAMING_SNAKE_CASE for constants support this refactoring. Constants can live in modules, classes, or the top-level namespace depending on scope requirements.
# Before: Magic numbers obscure meaning
def calculate_late_fee(days_late)
return 0 if days_late <= 7
base = 5.00
base + (days_late - 7) * 2.50
end
# After: Named constants clarify intent
GRACE_PERIOD_DAYS = 7
BASE_LATE_FEE = 5.00
DAILY_LATE_FEE = 2.50
def calculate_late_fee(days_late)
return 0 if days_late <= GRACE_PERIOD_DAYS
BASE_LATE_FEE + (days_late - GRACE_PERIOD_DAYS) * DAILY_LATE_FEE
end
Practical Examples
Extract Superclass consolidates common behavior from sibling classes into a parent class. This refactoring eliminates duplication while establishing inheritance relationships that reflect domain concepts.
# Before: Duplication across similar classes
class Invoice
attr_reader :number, :date, :items
def initialize(number, date)
@number = number
@date = date
@items = []
end
def add_item(item)
@items << item
end
def total
items.sum(&:amount)
end
def overdue?
date < Date.today - 30
end
end
class PurchaseOrder
attr_reader :number, :date, :items
def initialize(number, date)
@number = number
@date = date
@items = []
end
def add_item(item)
@items << item
end
def total
items.sum(&:amount)
end
def overdue?
date < Date.today - 60
end
end
# After: Common behavior extracted to superclass
class FinancialDocument
attr_reader :number, :date, :items
def initialize(number, date)
@number = number
@date = date
@items = []
end
def add_item(item)
@items << item
end
def total
items.sum(&:amount)
end
def overdue?
date < Date.today - overdue_threshold_days
end
private
def overdue_threshold_days
raise NotImplementedError, "Subclass must define overdue threshold"
end
end
class Invoice < FinancialDocument
private
def overdue_threshold_days
30
end
end
class PurchaseOrder < FinancialDocument
private
def overdue_threshold_days
60
end
end
Replace Nested Conditional with Guard Clauses flattens complex conditional logic. Guard clauses handle special cases early, allowing the main logic to proceed without deep nesting.
# Before: Deep nesting obscures logic
def calculate_shipping_cost(order)
if order.items.any?
if order.customer.premium?
if order.total > 100
0
else
5.00
end
else
if order.total > 50
10.00
else
15.00
end
end
else
nil
end
end
# After: Guard clauses flatten structure
def calculate_shipping_cost(order)
return nil if order.items.empty?
return 0 if order.customer.premium? && order.total > 100
return 5.00 if order.customer.premium?
return 10.00 if order.total > 50
15.00
end
Decompose Conditional extracts complex conditions into well-named methods. The refactoring clarifies the meaning of conditions and enables reuse.
# Before: Complex conditional logic
def ticket_price(customer, event)
if customer.age < 18 || customer.age >= 65 ||
(customer.member? && customer.visits > 10) ||
(event.date.weekday? && event.time.hour < 17)
event.base_price * 0.80
else
event.base_price
end
end
# After: Extracted condition methods
def ticket_price(customer, event)
eligible_for_discount?(customer, event) ? event.base_price * 0.80 : event.base_price
end
def eligible_for_discount?(customer, event)
age_discount?(customer) || loyalty_discount?(customer) || matinee_discount?(event)
end
def age_discount?(customer)
customer.age < 18 || customer.age >= 65
end
def loyalty_discount?(customer)
customer.member? && customer.visits > 10
end
def matinee_discount?(event)
event.date.weekday? && event.time.hour < 17
end
Replace Loop with Collection Method converts imperative iteration into declarative collection operations. Ruby's Enumerable methods express transformations concisely and clearly.
# Before: Imperative loop accumulation
def total_revenue_by_category(orders)
revenue = {}
orders.each do |order|
order.items.each do |item|
category = item.category
revenue[category] ||= 0
revenue[category] += item.price * item.quantity
end
end
revenue
end
# After: Declarative collection methods
def total_revenue_by_category(orders)
orders
.flat_map(&:items)
.group_by(&:category)
.transform_values { |items| items.sum { |item| item.price * item.quantity } }
end
Common Patterns
The Composed Method pattern structures methods as sequences of steps at a consistent abstraction level. Each method performs one task and calls other methods at the same conceptual level. This creates readable, maintainable code where each method tells a clear story.
# Composed method pattern
class OrderProcessor
def process(order)
validate_order(order)
calculate_totals(order)
charge_customer(order)
fulfill_order(order)
send_confirmation(order)
end
private
def validate_order(order)
validate_items(order.items)
validate_shipping_address(order.address)
validate_payment_method(order.payment)
end
def calculate_totals(order)
order.subtotal = calculate_subtotal(order.items)
order.tax = calculate_tax(order.subtotal, order.address)
order.shipping = calculate_shipping(order.items, order.address)
order.total = order.subtotal + order.tax + order.shipping
end
# Additional implementation methods...
end
The Strategy pattern combined with dependency injection enables flexible algorithm selection. This pattern replaces conditional logic that selects algorithms with polymorphic method dispatch.
# Strategy pattern for flexible algorithm selection
class PriceCalculator
def initialize(pricing_strategy)
@pricing_strategy = pricing_strategy
end
def calculate(order)
@pricing_strategy.calculate(order)
end
end
class StandardPricing
def calculate(order)
order.items.sum { |item| item.price * item.quantity }
end
end
class VolumeDiscountPricing
def calculate(order)
subtotal = order.items.sum { |item| item.price * item.quantity }
discount_rate = case order.items.sum(&:quantity)
when 0..9 then 0
when 10..49 then 0.05
when 50..99 then 0.10
else 0.15
end
subtotal * (1 - discount_rate)
end
end
class MemberPricing
def calculate(order)
order.items.sum { |item| item.member_price * item.quantity }
end
end
# Usage
calculator = PriceCalculator.new(VolumeDiscountPricing.new)
total = calculator.calculate(order)
The Null Object pattern eliminates nil checks by providing an object that implements the expected interface but performs no operations. This refactoring removes conditional logic and clarifies code flow.
# Before: Nil checks throughout code
class User
attr_reader :name, :subscription
end
def send_newsletter(user)
return unless user
return unless user.subscription
return unless user.subscription.active?
deliver_email(user.subscription.email)
end
# After: Null object pattern
class User
attr_reader :name, :subscription
def initialize(name, subscription = NullSubscription.new)
@name = name
@subscription = subscription
end
end
class Subscription
attr_reader :email, :plan
def active?
@active
end
end
class NullSubscription
def active?
false
end
def email
nil
end
end
def send_newsletter(user)
return unless user.subscription.active?
deliver_email(user.subscription.email)
end
The Replace Inheritance with Delegation pattern favors composition over inheritance when inheritance relationships feel forced. This refactoring increases flexibility and reduces coupling.
# Before: Inheritance feels unnatural
class Stack < Array
def push(item)
super
end
def pop
super
end
def peek
last
end
end
# After: Delegation clarifies relationship
class Stack
def initialize
@elements = []
end
def push(item)
@elements.push(item)
end
def pop
@elements.pop
end
def peek
@elements.last
end
def size
@elements.size
end
def empty?
@elements.empty?
end
end
Tools & Ecosystem
RuboCop serves as the primary static analysis tool for Ruby code quality. The tool enforces style guidelines and detects code smells. Configuration files customize rules for project-specific needs. RuboCop integrates with editors and CI pipelines, providing immediate feedback on code quality issues.
# .rubocop.yml configuration
Metrics/MethodLength:
Max: 15
Metrics/CyclomaticComplexity:
Max: 8
Style/Documentation:
Enabled: false
Layout/LineLength:
Max: 100
Reek identifies code smells specifically in Ruby code. The tool detects feature envy, data clumps, long parameter lists, and other structural problems. Reek outputs reports suggesting specific refactorings to address detected smells.
# Example Reek report
# FeatureEnvy: Order#calculate_tax refers to address more than self
# LongParameterList: UserService#create_user has 7 parameters
# DuplicateMethodCall: ReportGenerator#generate calls calculate_total 3 times
SimpleCov measures test coverage and identifies untested code paths. Coverage reports guide refactoring decisions by highlighting risky areas with insufficient tests. The tool integrates with test frameworks like RSpec and Minitest.
The ruby-lint gem performs static analysis without executing code. It detects undefined variables, unreachable code, unused parameters, and type mismatches. The tool complements RuboCop by checking for errors rather than style violations.
Flay detects structural duplication in Ruby code. Unlike simple text matching, Flay analyzes code structure to find duplicated logic expressed differently. The tool generates a similarity score and highlights candidates for Extract Method or Extract Class refactorings.
# Flay usage example
$ flay lib/**/*.rb
Total score (lower is better): 842
1) Similar code found in :defn (mass = 156)
lib/order_processor.rb:15
lib/invoice_generator.rb:23
lib/report_builder.rb:45
Fasterer suggests performance improvements for common Ruby idioms. The tool identifies patterns that have faster alternatives, guiding micro-optimizations during refactoring sessions.
Editor support varies significantly across tools. RubyMine provides automated refactorings including rename, extract method, and inline variable. Visual Studio Code with the Ruby extension offers basic refactoring support. Vim and Emacs rely on external tools and language servers for refactoring assistance.
Common Pitfalls
Refactoring without comprehensive test coverage introduces significant risk. Tests serve as the safety net that catches behavior changes. Developers who refactor untested code often introduce subtle bugs that appear later in production. The solution requires writing tests before refactoring or accepting higher risk.
Over-refactoring creates unnecessary abstraction and complexity. Extracting every expression into a separate method produces fragmented code that's harder to understand than the original. Premature abstraction anticipates flexibility that never materializes. The guideline suggests refactoring when patterns emerge from concrete examples, not based on speculation.
# Over-refactored example
def process_order(order)
validate(order)
calculate(order)
save(order)
end
def validate(order)
check_items(order)
check_customer(order)
end
def check_items(order)
verify_items_present(order)
verify_items_valid(order)
end
def verify_items_present(order)
raise "No items" if order.items.empty?
end
def verify_items_valid(order)
order.items.each { |item| validate_item(item) }
end
# Better: Appropriate abstraction level
def process_order(order)
validate_order(order)
order.calculate_totals
order.save
end
def validate_order(order)
raise "No items" if order.items.empty?
order.items.each { |item| validate_item(item) }
validate_customer(order.customer)
end
Mixing refactoring with feature development increases risk and complexity. Combining structural changes with behavioral changes makes it difficult to identify the source of bugs. Code reviews become harder when commits contain both refactorings and features. The practice of separate commits for refactoring and features improves traceability.
Refactoring published APIs without deprecation strategies breaks client code. Public interfaces require stability. Changes to public methods need version transitions, deprecation warnings, and migration guides. Internal refactoring proceeds freely, but API changes demand careful planning.
Incomplete refactoring leaves code in an inconsistent state. Starting a refactoring pattern and abandoning it midway creates confusion. Mixed coding styles, partially extracted abstractions, and orphaned methods indicate incomplete refactoring. Completing refactoring patterns or reverting changes maintains codebase consistency.
Ignoring performance implications during refactoring occasionally creates bottlenecks. Extracting methods adds call overhead. Creating objects increases memory allocation. Most refactorings have negligible performance impact, but hot paths in performance-critical code require measurement before and after refactoring.
Refactoring code the developer doesn't understand risks introducing bugs. Understanding the existing behavior, including edge cases and assumptions, precedes safe refactoring. When working with unfamiliar code, writing tests before refactoring clarifies current behavior and provides safety.
Reference
Core Refactoring Techniques
| Technique | Purpose | When to Apply |
|---|---|---|
| Extract Method | Break down long methods | Method exceeds 10 lines or has multiple responsibilities |
| Inline Method | Remove unnecessary indirection | Method body is clearer than method name |
| Extract Variable | Clarify complex expressions | Expression is used multiple times or hard to understand |
| Inline Variable | Remove redundant variables | Variable adds no clarity and is used once |
| Rename Variable | Improve clarity | Variable name doesn't reveal intent |
| Extract Class | Split bloated classes | Class has multiple responsibilities |
| Inline Class | Remove unnecessary classes | Class does too little to justify existence |
| Move Method | Improve cohesion | Method uses features of another class more than its own |
| Replace Conditional with Polymorphism | Eliminate type checking | Conditional switches on type or class |
| Replace Magic Number with Constant | Clarify intent | Numeric literal appears without explanation |
Data Structure Refactorings
| Technique | Purpose | When to Apply |
|---|---|---|
| Encapsulate Field | Protect data integrity | Field is public or directly accessed |
| Replace Array with Object | Add structure | Array elements have different types or meanings |
| Replace Data Value with Object | Add behavior to data | Simple data needs validation or calculation |
| Change Value to Reference | Share objects | Multiple copies of same entity exist |
| Change Reference to Value | Simplify equality | Object comparisons are complex |
| Introduce Parameter Object | Reduce parameter lists | Group of parameters appear together frequently |
| Preserve Whole Object | Reduce coupling | Multiple fields extracted from object passed as parameters |
Conditional Refactorings
| Technique | Purpose | When to Apply |
|---|---|---|
| Decompose Conditional | Clarify logic | Complex conditional hard to understand |
| Consolidate Conditional Expression | Remove duplication | Multiple conditionals produce same result |
| Replace Nested Conditional with Guard Clauses | Flatten structure | Special cases obscure normal flow |
| Replace Conditional with Strategy | Enable flexibility | Algorithm selection happens at runtime |
| Introduce Null Object | Eliminate nil checks | Nil checks appear throughout code |
| Introduce Assertion | Document assumptions | Assumption affects code behavior |
Method Call Refactorings
| Technique | Purpose | When to Apply |
|---|---|---|
| Replace Parameter with Method Call | Reduce parameters | Parameter can be computed by receiving method |
| Introduce Parameter | Add flexibility | Method needs more information |
| Remove Parameter | Simplify interface | Parameter is unused |
| Separate Query from Modifier | Clarify side effects | Method returns value and changes state |
| Parameterize Method | Reduce duplication | Similar methods differ only in values |
| Replace Constructor with Factory Method | Control object creation | Creation logic is complex or varies |
Organization Refactorings
| Technique | Purpose | When to Apply |
|---|---|---|
| Extract Module | Share behavior | Methods are duplicated across unrelated classes |
| Move Method | Improve cohesion | Method uses another class more than its own |
| Move Field | Group related data | Field is used primarily by another class |
| Extract Superclass | Eliminate duplication | Similar classes share behavior |
| Form Template Method | Standardize algorithm structure | Subclasses implement same algorithm with variations |
| Replace Inheritance with Delegation | Increase flexibility | Subclass uses only part of superclass interface |
| Replace Delegation with Inheritance | Simplify forwarding | Delegating all methods of simple interface |
Code Smell Indicators
| Smell | Symptom | Suggested Refactoring |
|---|---|---|
| Long Method | Method exceeds 15 lines | Extract Method, Decompose Conditional |
| Large Class | Class has many instance variables or methods | Extract Class, Extract Module |
| Long Parameter List | More than 3 parameters | Introduce Parameter Object, Preserve Whole Object |
| Divergent Change | Class changes for multiple reasons | Extract Class |
| Shotgun Surgery | Change requires edits across many classes | Move Method, Move Field, Inline Class |
| Feature Envy | Method uses another class extensively | Move Method |
| Data Clumps | Same group of data appears together | Extract Class, Introduce Parameter Object |
| Primitive Obsession | Using primitives instead of objects | Replace Data Value with Object |
| Switch Statements | Type-based conditionals | Replace Conditional with Polymorphism |
| Temporary Field | Field used only in certain circumstances | Extract Class, Introduce Null Object |
| Refused Bequest | Subclass ignores inherited behavior | Replace Inheritance with Delegation |
| Comments | Explaining complex code | Extract Method, Rename Method |
Refactoring Safety Checklist
| Step | Action | Verification |
|---|---|---|
| 1 | Ensure comprehensive test coverage | All tests pass |
| 2 | Make small, incremental changes | Each change compiles and tests pass |
| 3 | Commit after each successful refactoring | Version control shows progression |
| 4 | Use IDE automated refactorings when available | Tool performs change correctly |
| 5 | Review changes before committing | Code reads clearly |
| 6 | Run full test suite frequently | No regressions introduced |
| 7 | Deploy to staging before production | Integration testing passes |
Ruby-Specific Patterns
| Pattern | Ruby Idiom | Example |
|---|---|---|
| Collection Pipeline | Use Enumerable methods | flat_map, group_by, transform_values |
| Block Refactoring | Extract logic to blocks | map, select, reduce |
| Symbol to Proc | Shorten block syntax | map(&:name) instead of map with block |
| Keyword Arguments | Replace parameter objects | def create(name:, age:, email:) |
| Splat Operators | Variable argument handling | def process(*args, **kwargs) |
| Safe Navigation | Replace nil checks | user&.profile&.email |
| Tap Method | Chainable initialization | User.new.tap with block |
| Module Composition | Share behavior | include Authenticatable, Loggable |