BLOG
Compact vs Nested class definition in Ruby, what’s the difference?
A industry-like building with a ruby gem on top

Juan Aparicio

Engineering Manager

Jun 1, 2023

4 min read

Ruby

Tools

If you have ever worked with Ruby or Ruby on Rails, then you have probably noticed a certain trend. Some people declare nested classes within modules in a single line while others use multiple lines with the module/class reserved words.


module Api
  module V1
    class UsersController < ApiController
      ...
    end
  end
end

# versus

class Api::V1::UsersController < ApiController
  ...
end

By default, the Rubocop linter suggests using the far lengthier way of declaring a class but doesn’t explain why. Let’s take a closer look.

Nested Style

We’ll refer to the first, more verbose way of declaring nested classes/modules as nested style.

Let’s start exploring how nested style works by writing some code into the Ruby interpreter and running the irb command in the terminal.


class User
  attr_reader :attributes
  
  def initialize(**kwargs)
    puts 'Initializing User class'
    @attributes = kwargs
  end
end

This piece of code defines the User class with an initializer that receives a variable number of keyword arguments. Those keyword arguments are stored as a Hash in the instance variable @attributes. They can be accessed via the instance method attributes.


me = User.new(name: 'Juan') # Initializing User class
me.attributes # => { :name => 'Juan' }
me.attributes[:name] # => 'Juan'

Now that we have the User class, let’s create a class that will handle creating a User and doing other stuff (e.g. showing the message ‘Welcome <name>’).


module Operations
  class UserCreator
    def self.call(**user_attributes)
      user = User.new(**user_attributes)
      puts "Welcome #{user.attributes[:name]}"
      user
    end
  end
end

Operations::UserCreator.call(name: 'Juan')
# => Initializing User class
# => Welcome Juan

This works great, but what if we add another User class inside the Operations module?


module Operations
  class User
    attr_reader :attributes

    def initialize(**kwargs)
      puts 'Initializing Operations::User class'
      @attributes = kwargs
    end
  end
end

Let’s call the Operations::UserCreator.call now and see what happens:


Operations::UserCreator.call(name: 'Juan')
# => Initializing Operations::User class
# => Welcome Juan

What does this have to do with nested vs. compact style?

To answer that question, we’ll look at the compact style and how it behaves differently compared to the nested style.

Compact Style

Let’s close the Ruby interpreter and reopen it by typing exit and executing irb.
After that, we’ll paste the User class and the Operations::User class


class User
  attr_reader :attributes
  
  def initialize(**kwargs)
    @attributes = kwargs
  end
end

module Operations
  class User
    attr_reader :attributes

    def initialize(**kwargs)
      puts 'Initializing Operations::User class'
      @attributes = kwargs
    end
  end
end

We’ll now declare the Operations::UserCreator class by using the compact style and execute the .call method:


class Operations::UserCreator
  def self.call(**user_attributes)
    user = User.new(**user_attributes)
    puts "Welcome #{user.attributes[:name]}"
    user
  end
end

# uninitialized constant Operations (NameError)

Uh-oh! We hit an error. It’s telling us that the constant Operations is not declared.

When using Compact Style, we must declare the parent constants first. So, let’s fix this issue:


module Operations; end

class Operations::UserCreator
  def self.call(**user_attributes)
    user = User.new(**user_attributes)
    puts "Welcome #{user.attributes[:name]}"
    user
  end
end

Operations::UserCreator.call(name: 'Juan')
# => Initializing User class
# => Welcome Juan

The difference is subtle, but notice how this Compact style creates an Object of the User class, and the Nested style creates an Object of the Operations::User class.

It’s all about Lexical Scope and Constants lookup.

Lexical scope is the visibility and accessibility of variables and methods within a particular block of code based on their definition and the nesting structure of the code.
In this case, the nesting of the class is not the same when using compact or nested:


module Operations; end
class Operations::UserCreator
  puts Module.nesting
end
# => Operations::UserCreator

module Operations
  class UserCreator
    puts Module.nesting
  end
end
# => Operations::UserCreator
# => Operations

As you can see, when using Compact style, the lexical scope (or the scope in which the code is executed) is Operations::UserCreator, without any other nesting.
When a constant is looked up, it will look up in the following scopes:

  1. Operations::UserCreator
  2. top-level scope (think of it as the root node of the constants tree)

On the other hand, when using the nested style, the lexical scope is [Operations::UserCreator, Operations].
This time, when a constant is looked up, it will look up in the following scopes instead:

  1. Operations::UserCreator
  2. Operations
  3. top-level scope

Summing up

Using nested or compact styles when declaring classes/modules in Ruby impacts scopes, code execution and constant lookup.

Nested style

Pros

  • Namespacing: The nested style allows for the logical grouping of classes within a module or outer class. This helps in organizing related classes and avoids naming conflicts.
  • Improved readability: Nesting can provide context and improve code readability when classes are closely related.

Cons

  • Increased indentation: Nesting classes result in increased indentation levels, which can make the code less readable if the nesting becomes too deep.

Compact style

Pros

  • Simplicity: The compact style is straightforward and easy to read. It keeps the class declaration concise and separate from other modules or classes.
  • Avoids deep nesting: Using a compact style avoids excessive nesting, which can make code harder to understand and maintain.

Cons

  • Potential name clashes: If you have multiple classes with the same name defined in different places, there could be naming conflicts. However, proper namespacing can mitigate this issue.

Juan Aparicio

Engineering Manager

Jun 1, 2023

4 min read

Ruby

Tools