CrackedRuby CrackedRuby

Overview

Code smells represent surface-level indicators that hint at deeper problems in code structure, design, or implementation. Martin Fowler popularized the term in his refactoring work, describing smells as symptoms that signal potential maintenance difficulties, design weaknesses, or technical debt accumulation. Unlike bugs or errors that cause immediate failures, code smells indicate areas where code works but exhibits characteristics that increase maintenance costs, reduce readability, or limit extensibility.

The concept originates from the observation that experienced developers develop intuition for problematic code patterns. Rather than requiring formal proof of a problem, a smell triggers investigation into whether refactoring would improve the codebase. Not all smells require immediate action, but recognizing them enables informed decisions about technical debt and code quality.

Code smells exist at multiple levels of abstraction. Method-level smells affect individual functions or procedures. Class-level smells involve design issues within object definitions. Architecture-level smells indicate problems in how components interact or how responsibilities distribute across the system. Each level requires different refactoring approaches and carries different risk-benefit profiles for remediation.

# Example of Long Method smell
def process_order(order)
  # Validate order
  return false unless order.items.any?
  return false unless order.customer.valid?
  return false if order.total < 0
  
  # Calculate totals
  subtotal = order.items.sum(&:price)
  tax = subtotal * 0.08
  shipping = calculate_shipping(order)
  total = subtotal + tax + shipping
  
  # Apply discounts
  if order.customer.premium?
    total *= 0.9
  end
  if order.total > 100
    shipping = 0
  end
  
  # Process payment
  payment = Payment.new(amount: total)
  return false unless payment.process
  
  # Update inventory
  order.items.each do |item|
    item.decrement_stock
  end
  
  # Send notifications
  OrderMailer.confirmation(order).deliver
  NotificationService.notify_warehouse(order)
  
  true
end

The distinction between a smell and a legitimate design choice depends on context. A large class might appropriately represent a complex domain concept, or it might indicate poor separation of concerns. A long parameter list might reflect genuine complexity in an operation, or it might suggest missing abstractions. Experience and domain knowledge inform these judgments.

Key Principles

Code smell identification relies on recognizing patterns that correlate with maintenance difficulties. These patterns manifest as violations of design principles, excessive complexity metrics, or structural characteristics that impede understanding. The presence of a smell does not automatically require refactoring, but triggers evaluation of whether the current structure serves long-term maintainability.

Smell Categories

Bloaters represent code elements that have grown excessively large. Long methods, large classes, primitive obsession, long parameter lists, and data clumps fall into this category. These smells emerge gradually as features accrue and abstractions fail to evolve with complexity. A class that starts with three responsibilities might accumulate seven or eight through incremental changes, each seeming reasonable in isolation.

Object-orientation abusers indicate misuse or absence of object-oriented principles. Switch statements that dispatch on type codes, refused bequests where subclasses ignore inherited behavior, temporary fields that remain unused most of the time, and alternative classes with different interfaces solving the same problem belong here. These smells often result from procedural thinking in object-oriented contexts.

Change preventers create friction during modification. Divergent change occurs when a single class requires modification for multiple different reasons. Shotgun surgery happens when a single change requires touching many classes. Parallel inheritance hierarchies force creation of paired subclasses. These smells directly increase the cost and risk of system evolution.

Dispensables represent elements that serve no clear purpose. Comments explaining what code does rather than why, duplicate code, lazy classes with insufficient responsibility, data classes with only getters and setters, and dead code create maintenance burden without providing value. Removing dispensables simplifies the system without losing functionality.

Couplers create excessive dependencies between elements. Feature envy occurs when methods access another object's data more than their own. Inappropriate intimacy develops when classes depend excessively on each other's internal details. Message chains create fragile dependencies on navigating object graphs. Middle men that only delegate without adding value introduce unnecessary indirection.

Smell Indicators

Quantitative metrics provide objective smell indicators. Method length measured in lines or statements correlates with comprehension difficulty. Cyclomatic complexity counting independent paths through code identifies decision-heavy logic. Class size measured by method count or total lines indicates separation of concerns issues. Depth of inheritance hierarchies and coupling between objects offer structural complexity measures.

Qualitative characteristics complement metrics. Difficulty explaining what a method or class does suggests unclear responsibilities. Trouble naming elements indicates missing or muddled abstractions. Fear of modifying code signals brittle design or insufficient test coverage. Frequent bugs in specific areas point to problematic structure.

Refactoring Connection

Each smell associates with specific refactoring techniques. Long methods decompose through extract method. Large classes split via extract class or extract subclass. Duplicate code consolidates through extract method or pull up method. Feature envy resolves via move method. Understanding smell-refactoring relationships enables targeted improvements.

The refactoring catalog provides mechanical steps for addressing smells. Automated refactoring tools implement these transformations with correctness guarantees. Manual refactoring requires comprehensive test coverage to verify behavior preservation. The safety and effort of refactoring influence whether addressing a smell makes economic sense.

Context Dependence

Smell evaluation requires context. A 200-line method in a bootstrapping script differs from a 200-line method in core business logic. A god class in a small utility differs from a god class coordinating major subsystems. Performance-critical code may accept smells that would be unacceptable elsewhere. Domain complexity sometimes manifests as code complexity without representing a design flaw.

Organizational factors influence smell tolerance. Codebases with comprehensive test suites allow more aggressive refactoring. Teams with continuous integration and deployment can incrementally improve code. Projects nearing end-of-life may not justify refactoring investments. Understanding these trade-offs prevents dogmatic application of smell detection.

Common Patterns

Long Method

Methods exceeding 20-30 lines often indicate multiple responsibilities or excessive detail at one abstraction level. Long methods hinder understanding because readers must track more state, follow more branches, and comprehend more operations simultaneously. The smell manifests through scrolling to see the entire method, difficulty summarizing what the method does, or trouble choosing a descriptive name.

# Long method with multiple concerns
def generate_report(user_id, start_date, end_date)
  user = User.find(user_id)
  orders = Order.where(user_id: user_id)
              .where('created_at >= ?', start_date)
              .where('created_at <= ?', end_date)
  
  total_revenue = 0
  total_items = 0
  categories = Hash.new(0)
  
  orders.each do |order|
    total_revenue += order.total
    order.items.each do |item|
      total_items += item.quantity
      categories[item.category] += item.quantity
    end
  end
  
  report = Report.new
  report.user_name = user.name
  report.date_range = "#{start_date} to #{end_date}"
  report.total_revenue = total_revenue
  report.total_items = total_items
  report.top_category = categories.max_by { |k, v| v }&.first
  
  report.save
  ReportMailer.send_report(user, report).deliver
  
  report
end

Extract method refactoring addresses long methods by identifying cohesive blocks and extracting them into well-named methods. Each extracted method operates at a consistent abstraction level and has a clear, single purpose. The original method becomes a sequence of intention-revealing method calls.

Large Class

Classes with dozens of methods, hundreds of lines, or numerous instance variables indicate multiple responsibilities. Large classes violate the single responsibility principle and become difficult to understand, test, and modify. Changes in one area risk breaking seemingly unrelated functionality. The class acts as a magnet attracting new features because it already does so much.

Extract class refactoring splits large classes by identifying cohesive subsets of data and behavior. Extract subclass creates specialized versions when variation exists. Replace conditional with polymorphism distributes type-specific behavior across classes. These techniques create focused classes with clear purposes.

Primitive Obsession

Overusing primitive types instead of small objects to represent domain concepts creates scattered logic and limits expressiveness. Representing money as floats, phone numbers as strings, or coordinates as separate x and y integers loses type safety and scatters validation and formatting logic.

# Primitive obsession
class Invoice
  attr_accessor :amount_cents, :currency_code
  
  def formatted_amount
    "#{currency_code} #{amount_cents / 100.0}"
  end
  
  def convert_to(target_currency)
    rate = CurrencyConverter.rate(currency_code, target_currency)
    self.amount_cents = (amount_cents * rate).round
    self.currency_code = target_currency
  end
end

# Replace with value object
class Money
  attr_reader :cents, :currency
  
  def initialize(cents, currency)
    @cents = cents
    @currency = currency
  end
  
  def to_s
    "#{currency} #{cents / 100.0}"
  end
  
  def convert_to(target_currency)
    rate = CurrencyConverter.rate(currency, target_currency)
    Money.new((cents * rate).round, target_currency)
  end
end

class Invoice
  attr_accessor :amount
  
  def formatted_amount
    amount.to_s
  end
  
  def convert_to(currency)
    self.amount = amount.convert_to(currency)
  end
end

Replace data value with object creates small classes representing domain concepts. These objects encapsulate validation, formatting, and related operations, centralizing logic that would otherwise scatter across the codebase.

Long Parameter List

Methods requiring many parameters become difficult to call correctly and signal missing abstractions. Parameters exceeding four or five create cognitive burden and increase the chance of passing arguments in wrong order or omitting required values.

# Long parameter list
def create_user(first_name, last_name, email, phone, address_line1, 
                address_line2, city, state, zip, country, newsletter_opt_in)
  # Implementation
end

# Introduce parameter object
class UserRegistration
  attr_accessor :first_name, :last_name, :email, :phone
  attr_accessor :address, :newsletter_opt_in
end

class Address
  attr_accessor :line1, :line2, :city, :state, :zip, :country
end

def create_user(registration)
  # Implementation with registration.first_name, registration.address.city, etc.
end

Introduce parameter object groups related parameters into a cohesive structure. Preserve whole object passes entire objects rather than extracting individual values. Replace parameter with method call eliminates parameters that receivers can determine themselves.

Data Clump

Groups of data items appearing together repeatedly suggest missing abstractions. Three or four values consistently passed as a unit or stored as separate fields indicate a concept that deserves its own class.

Duplicate Code

Identical or nearly identical code in multiple locations multiplies maintenance costs and creates consistency risks. Changes require updating multiple locations, and divergence creates subtle bugs. Duplicate code indicates missing abstractions or failed decomposition.

# Duplicate code in sibling classes
class CreditCardPayment
  def process
    validate_payment_method
    authorize_amount
    capture_funds
    log_transaction
    send_receipt
  end
end

class BankTransferPayment
  def process
    validate_payment_method
    authorize_amount
    capture_funds
    log_transaction
    send_receipt
  end
end

# Extract to template method
class Payment
  def process
    validate_payment_method
    authorize_amount
    capture_funds
    log_transaction
    send_receipt
  end
  
  def validate_payment_method
    raise NotImplementedError
  end
  
  def authorize_amount
    raise NotImplementedError
  end
  
  # Common implementations
  def log_transaction
    # Shared logging logic
  end
  
  def send_receipt
    # Shared receipt logic
  end
end

class CreditCardPayment < Payment
  def validate_payment_method
    # Credit card specific validation
  end
  
  def authorize_amount
    # Credit card authorization
  end
end

Extract method consolidates duplicate code into a single location. Pull up method moves common behavior to superclasses. Form template method creates abstract algorithms with specialized steps.

Switch Statements

Type-based conditionals that dispatch to different logic violate the open-closed principle and require modification when new types emerge. Switch statements on type codes or polymorphic type checking indicate missing polymorphism.

# Switch statement smell
class ShapeCalculator
  def area(shape)
    case shape.type
    when :circle
      Math::PI * shape.radius ** 2
    when :rectangle
      shape.width * shape.height
    when :triangle
      0.5 * shape.base * shape.height
    else
      raise "Unknown shape type"
    end
  end
end

# Replace with polymorphism
class Circle
  attr_reader :radius
  
  def area
    Math::PI * radius ** 2
  end
end

class Rectangle
  attr_reader :width, :height
  
  def area
    width * height
  end
end

class Triangle
  attr_reader :base, :height
  
  def area
    0.5 * base * height
  end
end

Replace conditional with polymorphism distributes type-specific behavior to classes. Replace type code with subclasses creates class hierarchies. Replace type code with state/strategy uses composition instead of inheritance.

Feature Envy

Methods that access another object's data more than their own suggest misplaced responsibility. The envious method belongs with the data it manipulates, improving cohesion and reducing coupling.

Move method transfers functionality to the appropriate class. Extract method can precede moving if the envious code mixes concerns. The refactored design places operations near their data, following the principle that data and behavior belong together.

Inappropriate Intimacy

Classes that excessively access each other's internals create tight coupling and make changes expensive. Private implementation details become public dependencies, preventing independent evolution.

Move method and move field reduce coupling by relocating functionality. Change bidirectional association to unidirectional eliminates unnecessary links. Replace inheritance with delegation loosens coupling when subclasses depend heavily on parent internals. Encapsulate collection protects internal collections from external manipulation.

Message Chains

Long chains of method calls traversing object graphs create fragile code that breaks when intermediate relationships change. Each link in the chain represents a dependency and exposes internal structure.

# Message chain smell
order.customer.address.city.zip_code

# Hide delegate
class Order
  def customer_zip_code
    customer.address.city.zip_code
  end
end

order.customer_zip_code

Hide delegate introduces intermediary methods that encapsulate navigation. Extract method can create meaningful queries that express intent rather than exposing structure.

Middle Man

Classes that only delegate to other classes without adding value create unnecessary indirection. Excessive delegation indicates that the middle man has outlived its purpose or never had one.

Remove middle man eliminates unnecessary delegators. Inline method incorporates trivial delegations. Replace delegation with inheritance makes relationships explicit when appropriate.

Speculative Generality

Code designed for future requirements that never materialize adds complexity without benefit. Abstract classes with one subclass, parameters never used, methods never called, and excessive indirection for current needs all exemplify speculative generality.

Collapse hierarchy removes unnecessary abstractions. Inline class eliminates pointless indirection. Remove parameter cleans up unused flexibility. Rename method makes intent clear when abstract names hide simple purposes.

Ruby Implementation

Ruby-Specific Smells

Ruby's dynamic nature enables certain smells that static languages prevent or manifest differently. Excessive metaprogramming obscures control flow and makes debugging difficult. Overusing method_missing creates implicit interfaces that break tooling and confuse developers. Abusing monkey patching modifies core classes in ways that cause conflicts and surprises.

# Excessive metaprogramming smell
class DynamicModel
  def method_missing(method_name, *args)
    if method_name.to_s.start_with?('find_by_')
      attribute = method_name.to_s.sub('find_by_', '')
      find_by_attribute(attribute, args.first)
    else
      super
    end
  end
  
  def respond_to_missing?(method_name, include_private = false)
    method_name.to_s.start_with?('find_by_') || super
  end
end

# Prefer explicit methods
class Model
  def self.find_by_name(name)
    find_by_attribute('name', name)
  end
  
  def self.find_by_email(email)
    find_by_attribute('email', email)
  end
end

Ruby blocks enable closure-based smells. Excessively long blocks mix abstraction levels. Blocks with side effects create action-at-a-distance problems. Nested blocks become difficult to follow.

# Long block smell
users.select do |user|
  user.active? && 
  user.subscribed? &&
  user.last_login > 30.days.ago &&
  user.posts.any? { |post| post.published? && post.views > 100 }
end.map do |user|
  {
    name: user.full_name,
    email: user.email,
    score: calculate_engagement_score(user),
    rank: determine_user_rank(user)
  }
end.sort_by { |data| -data[:score] }

# Extract methods for clarity
def active_engaged_users
  users.select { |user| active_and_engaged?(user) }
end

def active_and_engaged?(user)
  user.active? && 
  user.subscribed? &&
  recent_activity?(user) &&
  has_popular_posts?(user)
end

def user_summaries(users)
  users.map { |user| user_summary(user) }
       .sort_by { |data| -data[:score] }
end

Ruby Idiom Violations

Not following Ruby conventions creates code that feels foreign to Ruby developers. Using explicit returns everywhere, parentheses for all method calls, or Java-style getters and setters violates Ruby style.

# Non-idiomatic Ruby
class User
  def getName()
    return @name
  end
  
  def setName(name)
    @name = name
    return nil
  end
  
  def isActive()
    if @active == true
      return true
    else
      return false
    end
  end
end

# Idiomatic Ruby
class User
  attr_accessor :name
  
  def active?
    @active
  end
end

Not using Ruby's enumerable methods creates imperative code where declarative code would be clearer. Manual array iteration with index tracking, accumulator initialization, and explicit loop management smell when map, select, or reduce express intent more clearly.

# Imperative style smell
result = []
index = 0
while index < items.length
  item = items[index]
  if item.price > 10
    result << item.name.upcase
  end
  index += 1
end

# Declarative Ruby style
result = items.select { |item| item.price > 10 }
              .map { |item| item.name.upcase }

Rails-Specific Smells

Rails applications exhibit framework-specific smells. Fat controllers containing business logic violate MVC separation. Callbacks proliferating in models create implicit dependencies and make testing difficult. Direct database queries scattered throughout views tightly couple presentation to data access.

# Fat controller smell
class OrdersController < ApplicationController
  def create
    @order = Order.new(order_params)
    @order.user = current_user
    
    # Business logic in controller
    subtotal = @order.items.sum(&:price)
    tax = subtotal * 0.08
    @order.total = subtotal + tax
    
    if @order.total > 100
      @order.discount = @order.total * 0.1
      @order.total -= @order.discount
    end
    
    if @order.save
      @order.items.each { |item| item.decrement_stock }
      OrderMailer.confirmation(@order).deliver_later
      NotificationService.notify_warehouse(@order)
      redirect_to @order
    else
      render :new
    end
  end
end

# Extract to service object
class OrdersController < ApplicationController
  def create
    result = CreateOrder.new(current_user, order_params).call
    
    if result.success?
      redirect_to result.order
    else
      @order = result.order
      render :new
    end
  end
end

class CreateOrder
  def initialize(user, params)
    @user = user
    @params = params
  end
  
  def call
    order = build_order
    
    if order.save
      process_successful_order(order)
      Result.success(order)
    else
      Result.failure(order)
    end
  end
  
  private
  
  def build_order
    Order.new(@params).tap do |order|
      order.user = @user
      order.calculate_total
      order.apply_discounts
    end
  end
  
  def process_successful_order(order)
    order.items.each(&:decrement_stock)
    OrderMailer.confirmation(order).deliver_later
    NotificationService.notify_warehouse(order)
  end
end

Excessive ActiveRecord callbacks create implicit ordering dependencies. Modifications trigger chains of callbacks that become difficult to reason about. Testing requires triggering callbacks or mocking them, both problematic.

Module Misuse

Using modules solely for namespace organization when inheritance makes sense, or using inheritance when composition with modules would be more appropriate, creates awkward designs. Ruby's mixin system allows flexible composition, but overuse creates tangled dependencies.

# Module namespace smell when inheritance appropriate
module AnimalBehaviors
  class Eating
    def eat
      puts "Eating"
    end
  end
  
  class Sleeping
    def sleep
      puts "Sleeping"
    end
  end
end

class Dog
  def initialize
    @eating = AnimalBehaviors::Eating.new
    @sleeping = AnimalBehaviors::Sleeping.new
  end
  
  def eat
    @eating.eat
  end
  
  def sleep
    @sleeping.sleep
  end
end

# Better approach with mixins
module Eating
  def eat
    puts "Eating"
  end
end

module Sleeping
  def sleep
    puts "Sleeping"
  end
end

class Dog
  include Eating
  include Sleeping
end

Practical Examples

Refactoring God Class

Consider a User class that handles authentication, profile management, notifications, billing, and preferences.

# God class smell
class User
  attr_accessor :email, :password_hash, :name, :bio, :avatar_url
  attr_accessor :notification_preferences, :email_verified
  attr_accessor :subscription_plan, :payment_method, :billing_address
  
  def authenticate(password)
    BCrypt::Password.new(password_hash) == password
  end
  
  def update_password(old_password, new_password)
    return false unless authenticate(old_password)
    self.password_hash = BCrypt::Password.create(new_password)
    save
  end
  
  def send_notification(type, message)
    return unless notification_preferences[type]
    
    case type
    when :email
      UserMailer.notification(self, message).deliver
    when :sms
      SmsService.send(phone, message)
    when :push
      PushService.send(device_tokens, message)
    end
  end
  
  def process_subscription_payment
    return unless subscription_plan.present?
    
    charge = Stripe::Charge.create(
      amount: subscription_plan.price_cents,
      currency: 'usd',
      customer: stripe_customer_id,
      description: "Subscription: #{subscription_plan.name}"
    )
    
    create_payment_record(charge)
    extend_subscription
  end
  
  def update_profile(attributes)
    self.name = attributes[:name] if attributes[:name]
    self.bio = attributes[:bio] if attributes[:bio]
    
    if attributes[:avatar]
      self.avatar_url = upload_avatar(attributes[:avatar])
    end
    
    save
  end
  
  # Many more methods...
end

Refactor by extracting cohesive responsibilities into focused classes:

class User
  attr_accessor :email, :name
  
  def authenticator
    @authenticator ||= UserAuthenticator.new(self)
  end
  
  def profile
    @profile ||= UserProfile.new(self)
  end
  
  def notifier
    @notifier ||= UserNotifier.new(self)
  end
  
  def subscription
    @subscription ||= UserSubscription.new(self)
  end
end

class UserAuthenticator
  def initialize(user)
    @user = user
  end
  
  def authenticate(password)
    BCrypt::Password.new(@user.password_hash) == password
  end
  
  def update_password(old_password, new_password)
    return false unless authenticate(old_password)
    @user.password_hash = BCrypt::Password.create(new_password)
    @user.save
  end
end

class UserProfile
  def initialize(user)
    @user = user
  end
  
  def update(attributes)
    @user.name = attributes[:name] if attributes[:name]
    @user.bio = attributes[:bio] if attributes[:bio]
    upload_avatar(attributes[:avatar]) if attributes[:avatar]
    @user.save
  end
  
  private
  
  def upload_avatar(file)
    uploader = AvatarUploader.new(@user)
    uploader.store!(file)
    @user.avatar_url = uploader.url
  end
end

class UserNotifier
  def initialize(user)
    @user = user
    @preferences = user.notification_preferences
  end
  
  def notify(type, message)
    return unless enabled?(type)
    channel_for(type).send(@user, message)
  end
  
  private
  
  def enabled?(type)
    @preferences[type]
  end
  
  def channel_for(type)
    {
      email: EmailNotificationChannel,
      sms: SmsNotificationChannel,
      push: PushNotificationChannel
    }[type]
  end
end

class UserSubscription
  def initialize(user)
    @user = user
  end
  
  def process_payment
    return unless active?
    
    charge = create_stripe_charge
    record_payment(charge)
    extend_period
  end
  
  private
  
  def active?
    @user.subscription_plan.present?
  end
  
  def create_stripe_charge
    Stripe::Charge.create(
      amount: @user.subscription_plan.price_cents,
      currency: 'usd',
      customer: @user.stripe_customer_id,
      description: charge_description
    )
  end
end

Eliminating Shotgun Surgery

When adding a new payment method requires changes across controllers, models, services, and views, shotgun surgery occurs.

# Changes scattered across multiple files

# app/controllers/checkout_controller.rb
def process_payment
  case params[:payment_type]
  when 'credit_card'
    process_credit_card
  when 'paypal'
    process_paypal
  when 'bitcoin'  # New case added
    process_bitcoin
  end
end

# app/models/order.rb
def payment_type_display
  case payment_type
  when 'credit_card'
    'Credit Card'
  when 'paypal'
    'PayPal'
  when 'bitcoin'  # New case added
    'Bitcoin'
  end
end

# app/services/payment_processor.rb
def process(order)
  case order.payment_type
  when 'credit_card'
    CreditCardProcessor.process(order)
  when 'paypal'
    PayPalProcessor.process(order)
  when 'bitcoin'  # New case added
    BitcoinProcessor.process(order)
  end
end

# app/views/orders/show.html.erb
# <%= case @order.payment_type
     when 'credit_card' then 'CC Icon'
     when 'paypal' then 'PP Icon'
     when 'bitcoin' then 'BTC Icon'  # New case added
     end %>

Refactor to polymorphic design that isolates changes:

# Payment method registry pattern
class PaymentMethodRegistry
  def self.register(key, klass)
    registry[key] = klass
  end
  
  def self.find(key)
    registry[key] || raise("Unknown payment method: #{key}")
  end
  
  def self.all
    registry.values
  end
  
  private
  
  def self.registry
    @registry ||= {}
  end
end

class PaymentMethod
  def self.register_as(key)
    PaymentMethodRegistry.register(key, self)
  end
  
  def self.key
    raise NotImplementedError
  end
  
  def self.display_name
    raise NotImplementedError
  end
  
  def self.icon
    raise NotImplementedError
  end
  
  def process(order)
    raise NotImplementedError
  end
end

class CreditCardPayment < PaymentMethod
  register_as :credit_card
  
  def self.display_name
    'Credit Card'
  end
  
  def self.icon
    'credit-card'
  end
  
  def process(order)
    # Credit card processing
  end
end

class PayPalPayment < PaymentMethod
  register_as :paypal
  
  def self.display_name
    'PayPal'
  end
  
  def self.icon
    'paypal'
  end
  
  def process(order)
    # PayPal processing
  end
end

# Adding Bitcoin requires only one new file
class BitcoinPayment < PaymentMethod
  register_as :bitcoin
  
  def self.display_name
    'Bitcoin'
  end
  
  def self.icon
    'bitcoin'
  end
  
  def process(order)
    # Bitcoin processing
  end
end

# Controller simplified
class CheckoutController
  def process_payment
    payment_method = PaymentMethodRegistry.find(params[:payment_type])
    processor = payment_method.new
    processor.process(@order)
  end
end

# Views access metadata
<%= PaymentMethodRegistry.all.map do |method|
  link_to method.display_name, '#', data: { icon: method.icon }
end %>

Removing Data Clumps

Three coordinates (x, y, z) appearing together throughout a physics simulation indicate a missing vector abstraction.

# Data clump smell
class Particle
  attr_accessor :pos_x, :pos_y, :pos_z
  attr_accessor :vel_x, :vel_y, :vel_z
  attr_accessor :acc_x, :acc_y, :acc_z
  
  def update(delta_time)
    vel_x += acc_x * delta_time
    vel_y += acc_y * delta_time
    vel_z += acc_z * delta_time
    
    pos_x += vel_x * delta_time
    pos_y += vel_y * delta_time
    pos_z += vel_z * delta_time
  end
  
  def distance_to(other)
    dx = pos_x - other.pos_x
    dy = pos_y - other.pos_y
    dz = pos_z - other.pos_z
    Math.sqrt(dx*dx + dy*dy + dz*dz)
  end
end

def attract(p1, p2, force)
  dx = p2.pos_x - p1.pos_x
  dy = p2.pos_y - p1.pos_y
  dz = p2.pos_z - p1.pos_z
  distance = Math.sqrt(dx*dx + dy*dy + dz*dz)
  
  p1.acc_x += force * dx / distance
  p1.acc_y += force * dy / distance
  p1.acc_z += force * dz / distance
end

Extract vector class to encapsulate coordinate operations:

class Vector3
  attr_reader :x, :y, :z
  
  def initialize(x, y, z)
    @x, @y, @z = x, y, z
  end
  
  def +(other)
    Vector3.new(x + other.x, y + other.y, z + other.z)
  end
  
  def -(other)
    Vector3.new(x - other.x, y - other.y, z - other.z)
  end
  
  def *(scalar)
    Vector3.new(x * scalar, y * scalar, z * scalar)
  end
  
  def magnitude
    Math.sqrt(x*x + y*y + z*z)
  end
  
  def normalize
    mag = magnitude
    Vector3.new(x / mag, y / mag, z / mag)
  end
end

class Particle
  attr_accessor :position, :velocity, :acceleration
  
  def initialize
    @position = Vector3.new(0, 0, 0)
    @velocity = Vector3.new(0, 0, 0)
    @acceleration = Vector3.new(0, 0, 0)
  end
  
  def update(delta_time)
    @velocity = velocity + (acceleration * delta_time)
    @position = position + (velocity * delta_time)
  end
  
  def distance_to(other)
    (position - other.position).magnitude
  end
end

def attract(p1, p2, force)
  direction = (p2.position - p1.position).normalize
  p1.acceleration = p1.acceleration + (direction * force)
end

Design Considerations

When to Refactor

Not all smells require immediate attention. Refactoring requires time, introduces risk, and may not provide commensurate benefit. Decision factors include change frequency in the affected code, team familiarity with the area, test coverage quality, and remaining project lifetime.

Code changed frequently justifies refactoring investment. Each modification becomes easier, bugs decrease, and new features integrate more smoothly. Code rarely touched may not warrant refactoring regardless of smell intensity. The return on refactoring investment depends on future interaction frequency.

# Low-change-frequency smell
class LegacyImporter
  # 500-line method importing data from deprecated system
  # Used once annually during audit
  # Works correctly despite smell
end

# High-change-frequency smell  
class OrderProcessor
  # 200-line method processing orders
  # Modified every sprint for new features
  # Frequent bugs due to complexity
  # Justifies refactoring investment
end

Team capability influences refactoring decisions. Teams experienced with refactoring patterns and supported by comprehensive test suites can safely improve code incrementally. Teams lacking these capabilities risk introducing bugs or spending excessive time. Building refactoring capability itself becomes a prerequisite investment.

Technical Debt Management

Smells represent technical debt that accrues interest through increased maintenance costs. Unlike financial debt, technical debt lacks precise quantification. Teams estimate debt impact through relative difficulty metrics, bug rates, or velocity changes in affected areas.

Intentionally accepting debt makes sense under deadline pressure or with prototype code. The critical factor is conscious decision-making and tracking. Accidental debt accumulation from lack of attention creates more severe problems than deliberate debt taken for business reasons.

# Documented technical debt
class QuickAndDirtyReport
  # TODO: Technical debt - Extract classes, add tests
  # Reason: Critical deadline for investor meeting
  # Impact: Hard to modify, no test coverage
  # Remediation: Allocated Q2 2025 sprint
  # Tracking: TECH-1234
  
  def generate
    # 300 lines of procedural code
  end
end

Debt repayment strategies include incremental refactoring during feature work, dedicated refactoring sprints, and boy scout rule (leave code better than found). Incremental refactoring spreads cost across feature development. Dedicated sprints tackle systemic issues but require business justification. The boy scout rule creates gradual improvement without explicit allocation.

Performance vs Cleanliness Trade-offs

Performance optimization often conflicts with clean design. Inlining methods eliminates call overhead but increases duplication. Caching breaks encapsulation. Custom memory management adds complexity. These trade-offs require careful evaluation of actual performance requirements versus assumed needs.

Measure before optimizing. Profiling identifies genuine bottlenecks worth performance-smell trade-offs. Premature optimization creates unnecessary complexity. Many smells have negligible performance impact in non-critical code paths. Reserve performance compromises for proven hotspots with meaningful impact.

# Clean but potentially slow
class DataProcessor
  def process(items)
    items.select { |item| valid?(item) }
         .map { |item| transform(item) }
         .each { |item| store(item) }
  end
  
  def valid?(item)
    item.status == 'active' && item.value > 0
  end
  
  def transform(item)
    Item.new(name: item.name.upcase, value: item.value * 2)
  end
end

# Optimized for large datasets but more complex
class DataProcessor
  def process(items)
    result = Array.new(items.size)
    write_index = 0
    
    items.each do |item|
      next unless item.status == 'active' && item.value > 0
      
      result[write_index] = Item.new(
        name: item.name.upcase,
        value: item.value * 2
      )
      write_index += 1
      
      store(result[write_index - 1])
    end
    
    result[0...write_index]
  end
end

Domain complexity sometimes manifests as code complexity without representing a design flaw. Insurance underwriting, tax calculations, or scientific simulations contain inherent complexity that cannot be simplified away. Distinguish essential complexity from accidental complexity introduced by poor design.

Context-Specific Tolerance

Different contexts demand different smell tolerance levels. Core business logic requires the highest standards. Edge cases, throwaway scripts, and exploratory prototypes accept more smells. This differentiation allows efficient resource allocation while maintaining quality where it matters most.

# Core business logic - low smell tolerance
class InvoiceCalculator
  def calculate(line_items, customer)
    subtotal = Subtotal.new(line_items)
    tax = TaxCalculator.new(customer.tax_jurisdiction)
    discount = DiscountPolicy.new(customer).calculate(subtotal)
    
    Invoice.new(
      subtotal: subtotal,
      tax: tax.calculate(subtotal - discount),
      discount: discount
    )
  end
end

# One-off data migration script - higher smell tolerance
# db/migrate/20250315_migrate_legacy_data.rb
class MigrateLegacyData < ActiveRecord::Migration[7.0]
  def up
    # 150-line procedural script
    # Runs once, throws away
    # Smell acceptable given context
  end
end

Legacy integration layers dealing with external constraints may require smells to accommodate external design. Wrapping poorly designed APIs, integrating with legacy systems, or interfacing with third-party code that violates good design sometimes necessitates local smells to isolate external problems.

Tools & Ecosystem

RuboCop

RuboCop enforces Ruby style guide and detects many code smells automatically. Configured through rubocop.yml, it checks method length, cyclomatic complexity, class size, parameter counts, and hundreds of other metrics. RuboCop supports custom cops for project-specific standards.

# .rubocop.yml
Metrics/MethodLength:
  Max: 25
  Exclude:
    - 'db/migrate/**/*'

Metrics/ClassLength:
  Max: 200
  CountComments: false

Metrics/CyclomaticComplexity:
  Max: 10

Metrics/PerceivedComplexity:
  Max: 10

Metrics/AbcSize:
  Max: 20

Layout/LineLength:
  Max: 120

RuboCop auto-correction fixes many issues automatically. Safe corrections apply with --auto-correct flag. Unsafe corrections requiring human review use --auto-correct-all. Integration with editors provides real-time feedback during development.

# Run RuboCop on entire project
rubocop

# Auto-correct safe issues
rubocop --auto-correct

# Check specific files
rubocop app/models/user.rb

# Generate todo file for gradual adoption
rubocop --auto-gen-config

Reek

Reek specifically targets code smells with dedicated detectors. It identifies feature envy, data clumps, long parameter lists, utility functions, and other object-oriented design issues. Reek's strength lies in detecting design problems beyond style issues.

# .reek.yml
detectors:
  LongParameterList:
    max_params: 4
    
  TooManyStatements:
    max_statements: 10
    
  UtilityFunction:
    enabled: true
    
  DataClump:
    max_copies: 2
    min_clump_size: 3
    
  FeatureEnvy:
    enabled: true

Reek integration with CI pipelines prevents new smells from entering the codebase. Configure smell thresholds and fail builds exceeding limits. Gradual improvement strategies use todo files tracking accepted smells with remediation plans.

Flog

Flog calculates complexity scores for methods based on abstract syntax tree analysis. Higher scores indicate more complex code requiring attention. Unlike line-count metrics, Flog accounts for branching, nesting, and method calls.

# Analyze complexity
flog app/models/

# Show worst methods
flog --all app/models/ | sort -rn | head -20

# Generate complexity report
flog --score app/models/ > complexity_report.txt

Complexity trends over time provide objective technical debt metrics. Track Flog scores for critical classes and watch for increases. Set thresholds triggering refactoring discussions when exceeded.

Flay

Flay detects structural code duplication beyond simple text matching. It identifies similar code structures with different variable names or minor variations. This catches duplicated logic that text-based tools miss.

# Find structural duplication
flay app/

# Show duplicated code
flay --diff app/

# Minimum mass threshold
flay --mass 25 app/

Code Climate

Code Climate provides commercial static analysis combining multiple engines. It aggregates RuboCop, Reek, and other tools into unified quality metrics. Code Climate tracks technical debt, complexity trends, and test coverage over time.

Integration with GitHub pull requests provides automated code review feedback. Maintainability ratings quantify code quality changes. Technical debt estimates translate complexity into time costs.

Rubycritic

Rubycritic combines multiple analysis tools into comprehensive quality reports. It generates HTML reports showing smells, complexity metrics, churn analysis, and test coverage together. The unified view helps prioritize refactoring efforts.

# Generate quality report
rubycritic app/

# Compare branches
rubycritic --mode-ci --branch main

# Minimum score threshold
rubycritic --minimum-score 90 app/

Churn analysis combines complexity with change frequency. High-complexity, high-churn files represent the most problematic technical debt. Focus refactoring efforts on these files for maximum impact.

Continuous Integration

CI integration makes smell detection part of the development workflow. Configure thresholds and fail builds exceeding acceptable limits. Balance strictness with practicality to avoid causing friction.

# .github/workflows/quality.yml
name: Code Quality

on: [pull_request]

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      
      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true
      
      - name: Run RuboCop
        run: bundle exec rubocop
      
      - name: Run Reek
        run: bundle exec reek
      
      - name: Run Flog
        run: bundle exec flog --threshold 20 app/

Editor Integration

Real-time feedback during development prevents smells from entering the codebase. Configure editors to run RuboCop and Reek on save. Inline warnings make smell detection part of the coding process rather than a post-hoc review.

VSCode, RubyMine, and Vim support Ruby linting plugins. Language server protocol implementations provide consistent experience across editors. Configure severity levels to distinguish between errors and suggestions.

Reference

Smell Categories

Category Description Examples
Bloaters Excessively large code structures Long Method, Large Class, Long Parameter List, Primitive Obsession, Data Clumps
Object-Orientation Abusers Misuse of OO principles Switch Statements, Refused Bequest, Temporary Field, Alternative Classes
Change Preventers Make modifications difficult Divergent Change, Shotgun Surgery, Parallel Inheritance
Dispensables Serve no clear purpose Comments, Duplicate Code, Lazy Class, Data Class, Dead Code, Speculative Generality
Couplers Excessive coupling Feature Envy, Inappropriate Intimacy, Message Chains, Middle Man

Common Smells Quick Reference

Smell Indicator Primary Refactoring
Long Method Methods exceeding 20-30 lines Extract Method
Large Class Classes with 10+ methods or 200+ lines Extract Class, Extract Subclass
Primitive Obsession Using primitives for domain concepts Replace Data Value with Object
Long Parameter List Methods with 5+ parameters Introduce Parameter Object, Preserve Whole Object
Data Clump Same group of values appearing together Extract Class
Duplicate Code Identical or similar code in multiple places Extract Method, Pull Up Method, Form Template Method
Switch Statements Type-based conditionals Replace Conditional with Polymorphism
Feature Envy Method using another object's data heavily Move Method
Inappropriate Intimacy Classes accessing each other's internals Move Method, Move Field, Change Bidirectional Association to Unidirectional
Message Chains Long method call chains Hide Delegate
Middle Man Class mostly delegating to others Remove Middle Man, Inline Method
Divergent Change One class changing for multiple reasons Extract Class
Shotgun Surgery Single change affecting many classes Move Method, Move Field, Inline Class
Lazy Class Class doing too little Inline Class, Collapse Hierarchy
Speculative Generality Unused abstractions Collapse Hierarchy, Inline Class, Remove Parameter
Temporary Field Field used only sometimes Extract Class, Replace Method with Method Object
Refused Bequest Subclass not using inherited behavior Replace Inheritance with Delegation, Push Down Method
Comments Comments explaining what code does Extract Method, Rename Method, Introduce Assertion
Data Class Class with only getters/setters Move Method, Encapsulate Field, Remove Setting Method

Ruby-Specific Smells

Smell Description Solution
Excessive Metaprogramming Overuse of define_method, method_missing, class_eval Use explicit methods, consider alternative designs
Method Missing Abuse Using method_missing for core functionality Define explicit methods or use method registration
Monkey Patching Modifying core classes unsafely Use refinements, extension modules, or wrapper classes
Long Blocks Blocks exceeding 5-10 lines Extract to methods
Non-Idiomatic Code Java-style code in Ruby Follow Ruby conventions, use attr_accessor, predicate methods
Missing Enumerable Methods Manual iteration instead of map/select/reduce Use appropriate enumerable methods
Fat Active Record Models Models with excessive business logic Extract service objects, use concerns appropriately
Callback Chains Excessive before/after callbacks Use service objects or explicit orchestration

Refactoring Techniques

Technique Purpose When to Use
Extract Method Break apart long methods Method does multiple things
Extract Class Split large classes Class has multiple responsibilities
Move Method Relocate misplaced behavior Method uses another class's data more than its own
Move Field Relocate misplaced data Field used primarily by another class
Inline Method Remove unnecessary indirection Method body as clear as name
Inline Class Remove lazy classes Class no longer pulling its weight
Replace Conditional with Polymorphism Eliminate type switches Conditional behavior varies by type
Introduce Parameter Object Reduce parameter lists Multiple parameters travel together
Preserve Whole Object Simplify method calls Extracting multiple values from same object
Replace Data Value with Object Create meaningful types Primitive used for domain concept
Pull Up Method Consolidate common behavior Identical methods in subclasses
Push Down Method Move specialized behavior Method relevant to only some subclasses
Form Template Method Create algorithmic skeleton Similar methods with varying details
Hide Delegate Reduce coupling Clients navigating object structures
Remove Middle Man Eliminate unnecessary delegation Class mostly delegating

Complexity Metrics

Metric Description Typical Threshold
Lines of Code Method or class size Methods: 20-30, Classes: 150-200
Cyclomatic Complexity Number of independent paths 10 or less
ABC Size Assignments, branches, calls 20 or less
Perceived Complexity Human assessment of difficulty 10 or less
Parameter Count Number of method parameters 4 or less
Depth of Inheritance Levels in inheritance tree 4 or less
Coupling Between Objects Number of class dependencies Minimize
Lack of Cohesion Methods sharing instance variables Maximize sharing

Tool Configuration Priorities

Priority Metric Rationale
High Method Length Direct correlation with comprehension difficulty
High Cyclomatic Complexity Indicates testing and maintenance difficulty
High Duplicate Code Multiplies maintenance cost
Medium Class Length Suggests responsibility issues
Medium Parameter Count Indicates missing abstractions
Medium Feature Envy Points to misplaced responsibility
Low ABC Size Secondary complexity indicator
Low Depth of Inheritance Problems rare in modern Ruby