CrackedRuby CrackedRuby

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