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
endThis 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
endThe 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
endIn 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
endThanks 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
endThe 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
endThe modular approach (mixins) allows us to build classes like Lego blocks. An object can be Approvable, Archivable, 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
endThat'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.2297You can also add logic to them without losing conciseness:
Coordinates = Data.define(:lat, :lng) do
def to_s
"#{lat}°N, #{lng}°E"
end
endSummary
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!