When to use Module over Class in Ruby?

When to use Module over Class in Ruby?

Let's start with a classic case - a Service Object that exists just for the sake of existing. I recently came across some code responsible for resetting passwords that looked something like this.

class PasswordResetInitiator
  def self.call(email)
    new(email).call
  end

  def initialize(email)
    @email = email
  end

  def call
    generate_token
    send_email
  end

  private

  def generate_token
    user = User.find_by!(email: @email)
    user.regenerate_reset_password_token
  end

  def send_email
    # mailing logic
    UserMailer.with(email: @email).reset_password.deliver_later
  end
end

This is a classic example of a ‘shell class’. We create an object, allocate memory, only to call a single sequential procedure. This is not object-oriented, it is procedural programming disguised as an object.

Ruby gives us much better tools for such tasks. We can encapsulate the same logic in a readable class method of the model or in a dedicated module if the logic is more complex.

class User < ApplicationRecord
  class << self
    def initiate_password_reset(email)
      user = find_by!(email: email)
      user.regenerate_reset_password_token
      UserMailer.with(user: user).reset_password.deliver_later
    end
  end
end

The code does the same thing, but the cognitive overhead associated with jumping between files disappears. Business logic ‘belongs’ to the user, so why are we forcing it out?

Module as a toolbox

Another cardinal sin is creating utility classes that we never instantiate. If you have a TextFormatter class where all methods are static (self.method), you are essentially fighting the language.

class TextFormatter
  def self.slugify(text)
    text.downcase.strip.gsub(' ', '-').gsub(/[^\w-]/, '')
  end
end

In Ruby, a class is also an object, but one burdened with the ability to create instances. If you need a bag for functions (namespace), use a module. It is lighter and clearly communicates its intentions: ‘do not create me, just use me’.

module TextFormatter
  extend self

  def slugify(text)
    # clean string for url usage
    text.downcase.strip.gsub(' ', '-').gsub(/[^\w-]/, '')
  end
end

Thanks to extend self, we can call TextFormatter.slugify(‘Hello World’) directly, and the code reader immediately knows that there is no hidden state here.

Mixins instead of artificial inheritance

This is where we most often break SOLID principles in an attempt to be ‘DRY’. Imagine an HR system where we have holiday requests and expense claims. Both require an approval process. This often results in the following hierarchy:

class BaseRequest
  def approve!(manager)
    check_permissions(manager)
    update(status: 'approved', approved_at: Time.current)
    notify_requester
  end

  private

  def check_permissions(manager)
    raise "Not implemented"
  end
end

class VacationRequest < BaseRequest
  # specific implementation
end

The problem arises when VacationRequest suddenly needs to inherit from ApplicationRecord (which is standard in Rails), and we have already used up the only inheritance slot for BaseRequest. This is a dead end.

Instead of building rigid inheritance trees, think about behaviours. “Being acceptable” is a trait, not an identity.

module Approvable
  def approve!(manager)
    check_permissions(manager)
    update(status: 'approved', approved_at: Time.current)
    notify_requester
  end

  private

  def notify_requester
    # notification logic
  end
end

class VacationRequest < ApplicationRecord
  include Approvable

  private

  def check_permissions(manager)
    # logic checking if manager can approve vacation
  end
end

class ExpenseReport < ApplicationRecord
  include Approvable
  # expense specific logic
end

The modular approach (mixins) allows us to build classes like Lego blocks. An object can be ApprovableArchivable, and Searchable at the same time. With inheritance, we would have to create monsters like SearchableArchivableApprovableBase.

Data, or order in parameters

Finally, my favourite: classes that are just containers for data. I often see such creations in code that handles external APIs or simple geometric calculations:

class Coordinates
  attr_reader :lat, :lng

  def initialize(lat:, lng:)
    @lat = lat
    @lng = lng
  end
end

That's a lot of writing for something that stores two numbers. Since Ruby 3.2, we have Data, which perfectly replaces such structures. They are immutable (which is a huge advantage in multithreaded applications) and have built-in value comparison.

Coordinates = Data.define(:lat, :lng)

location = Coordinates.new(lat: 52.2297, lng: 21.0122)
# location.lat #=> 52.2297

You can also add logic to them without losing conciseness:

Coordinates = Data.define(:lat, :lng) do
  def to_s
    "#{lat}°N, #{lng}°E"
  end
end

Summary

Refactoring is not just cleaning up, it is a change in mindset. Every time you write class, ask yourself: does this entity really need an identity and state? Or is it just a function that has lost its way, or a feature that can be shared? Using modules and Data objects makes your code lighter, more modular and, most importantly, easier to maintain. Don't be afraid to delete classes. Less code is better code.

Happy refactoring!