Alternatives to Ruby classes

I've noticed that many Ruby developers tend to suffer from "classitis," as coined by John Ousterhout in his "A Philosophy of Software Design," where there is an explosion of shallow modules (or, in our case, classes) instead of having fewer deep modules.

I understand the general criticism that ActionController instances aren't really objects, but sure this can't be the best alternative:

module Bookshelf
  module Actions
    module Home
      class Show < Bookshelf::Action
        def handle(request, response)
          name = request.params[:name]

          response.body = "Welcome to Bookshelf #{name}!"
        end
      end
    end
  end
end

It gets even worse when you consider the proliferation of the ServiceObject pattern in the Ruby and Rails codebases, which often looks like this:

class BookCreator
  def self.call(*args)
    new(*args).call
  end

  def initialize(title:, description:, author_id:, genre_id:)
    @title = title
    @description = description
    @author_id = author_id
    @genre_id = genre_id
  end

  def call
    create_book
  end

  private
    def create_book
      Book.create!(
        title: @title
        description: @description
        author_id: @author_id
        genre_id: @genre_id
      )
    rescue ActiveRecord::RecordNotUnique => e
      # handle duplicate entry
    end
end

This whole class (which is basically a Factory) could have been a method:

class Book
  class << self
    def create_unique(params)
      Book.create!(params)
    rescue ActiveRecord::RecordNotUnique => e
      # handle duplicate entry
    end
  end

  # rest of the code
end

So I was pleasantly surprised when I saw Dave Thomas's talk at SFRuby titled "Stop Using Classes, which I'd highly recommend watching:

Below are my notes about some the arguments he is making.

When to use module instead of class

In Ruby, there are two main differences between modules and classes:

  • Modules are not instantiable (there isn't a Module.new method)
  • Modules can be mixed into other modules or classes

Or, if we frame it in a different way, you should use a module instead of a class when you need a namespace for related functions or when you need to share code between different object types.

Let's consider this class:

class Utils
  def self.date_to_string(date)
    # code
  end
end

Since it doesn't make sense to instantiate the above class at all, let's replace it with a module:

module Utils
  extend self

  def date_to_string(date)
    # code
  end
end

The extend self part makes it obvious that the Module is used as a namespace, which is a distinct advantage over classes.

A second use for Module is its ability to mix in behavior, thus allowing you to share code between objects.

Here's a common pattern I've seen in several places:

class StrategyBase
  def run
    run_setup
    perform
    run_callbacks
  end

  private
    def run_setup
      raise "Not implemented"
    end
end

class UserStrategy < StrategyBase
  private
    def run_setup
      # some user-specific setup code
    end
end

class AdminStrategy < StrategyBase
  private
    def run_setup
      # some admin-specific setup code
    end
end

The StrategyBase class actually represents a behavior which we want to mix into our UserStrategy and AdminStrategy objects, so why not be explicit about that and convert it to a Module?

module Runnable
  def run
    run_setup
    perform
    run_callbacks
  end
end

class UserStrategy
  include Runnable

  private
    def run_setup
      # some user-specific setup code
    end
end

class AdminStrategy
  include Runnable

  private
    def run_setup
      # some admin-specific setup code
    end
end

The main advantage is that adding behaviors to classes via mixins scales much better than using inheritance:

class AdminStrategy
  include Runnable, Retriable, Loggable
end

class UserStrategy
 include Runnable, Retriable, Loggable
end

class GuestStrategy
  include Runnable, Expirable
end

These different approaches can be seen in ActiveJob and Sidekiq:

class SomeJob < ActiveJob::Base
  def perform(*args)
  end
end

# vs

class SomeJob
  include Sidekiq::Job

  def perform(*args)
  end
end

The inheritance hierarchy is not a big issue in the case of ActiveJob mainly because the convention is that all files in app/jobs are expected to always be a type of job, but this might not be as clear in other parts of the codebase (e.g. app/models).

When to use a Data instead of class

Another common anti-pattern when it comes to using classes in Ruby is when the class is actually a data bag. In that case, there are two better constructs, but I'd like to single out the Data class.

So instead of:

class Person
  attr_reader :first_name, :last_name, :age

  def initialize(first_name:, last_name:, age:)
    @first_name = first_name
    @last_name = last_name
    @age = age
  end
end

it would be better (and simpler!) to use:

Person = Data.define(:first_name, :last_name, :age)

In both cases, the usage is identical:

john = Person.new first_name: "John", last_name: "Doe", age: 42

john.first_name #=> "John

The Data class can also include helper methods, but keep in mind that the object is frozen so you can't manipulate instance variables after it was created:

Person = Data.define(:first_name, :last_name, :age) do
  def full_name
    "#{first_name} #{last_name}"
  end
end

Naming classes after design patterns

Dave Thomas says that if your class is named after a design pattern, then it's a smell. While I agree that in Ruby we don't need to have a dedicated UserFactory class and just use a class method (e.g. User#build_admin), I think his advice is less applicable down when we go beyond the design patterns presented in the GoF book.

For example, I don't think it's a smell that you have a RemindersJob, or a UsersController class even though both contain the name of the pattern ("job" and "controller").

Abstract classes

I wrote my fair share of abstract classes and the logic behind it was that they provided core functionality for the classes that inherited from it (e.g. populating the model with data pulled from a specific URL).

However, looking back, I think I always got to a place where I'd had methods that would just raise some sort of NotImplementedError, or I would have to override the initialize method and I'm fairly sure that using a mix-in would have been much cleaner.

Final thoughts

I think PragDave was a bit too dramatic when he titled his talk "Stop using classes," but otherwise I highly agree with the points that he's making.

Whenever you reach for a class, you are also inviting a lot of complexity, which could be avoided by using other tools, like Module (which is stateless) or Data (which is immutable), that could be easier to reason about since they are generally simpler. Also, refactoring from a Module or Data to a Class is generally extremely trivial.

That being said, I also think there's a lot of essential complexity that you need to manage and, often, objects and classes can be extremely helpful.