Skip to main content
โšก Calmops

Class Instance Variables in Ruby: Inheritance, Accessors, and Rails class_attribute

Introduction

A class instance variable is an instance variable (@var) defined on the class object itself โ€” not on instances of the class. Because every class in Ruby is an object (an instance of Class), it can have its own instance variables. This is distinct from class variables (@@var), which are shared across the entire inheritance hierarchy.

Basic Example

class People
  @kind = "human"  # class instance variable

  class << self
    attr_accessor :kind  # getter and setter for @kind on the class
  end
end

class Student < People
end

p People.kind   # => "human"
p Student.kind  # => nil  (Student has its own @kind, unset)

Student inherits the accessor method (kind) from People, but not the value of @kind. Each class object has its own separate @kind instance variable. Student.kind returns nil because Student’s @kind was never set.

Why Values Don’t Inherit

Instance variables belong to specific objects. People and Student are two different objects (both instances of Class), so they each have their own @kind:

People.instance_variables   # => [:@kind]
Student.instance_variables  # => []  (Student has no @kind set)

This is the key difference from class variables (@@kind), which are shared:

class People
  @@kind = "human"
  def self.kind; @@kind; end
end

class Student < People; end

People.kind   # => "human"
Student.kind  # => "human"  (shared!)

Student.class_variable_set(:@@kind, "student")
People.kind   # => "student"  (changed by Student!)

Methods Are Inherited, Values Are Not

This distinction is important: the method (kind) is inherited, but the value (@kind) is not:

class Animal
  @sound = "..."
  class << self
    attr_accessor :sound
  end
end

class Dog < Animal
  @sound = "Woof"
end

class Cat < Animal
  @sound = "Meow"
end

puts Animal.sound  # => "..."
puts Dog.sound     # => "Woof"
puts Cat.sound     # => "Meow"

# Each class has its own @sound โ€” no interference
Dog.sound = "Bark"
puts Cat.sound     # => "Meow"  (unchanged)
puts Animal.sound  # => "..."   (unchanged)

This pattern is used extensively in Rails for per-model configuration.

The Design Principle

Objects are distinguished from each other by their instance variables (data). Methods are shared and reused through inheritance. This is a fundamental principle of object-oriented design:

  • Instance variables โ†’ differentiate objects (each has its own state)
  • Methods โ†’ shared behavior (inherited and reused)

In Go, this separation is enforced by the language โ€” structs hold data, interfaces define behavior, and they’re kept strictly separate. Ruby is more flexible but the same principle applies.

Rails class_attribute

Rails builds on class instance variables with class_attribute, which provides inheritable class-level attributes where subclasses get their own copy that starts with the parent’s value:

class ApplicationRecord < ActiveRecord::Base
  class_attribute :per_page, default: 25
end

class User < ApplicationRecord
  # Inherits per_page = 25
end

class Post < ApplicationRecord
  self.per_page = 10  # Override for Post only
end

puts User.per_page  # => 25  (inherited)
puts Post.per_page  # => 10  (overridden)

# Changing User doesn't affect Post
User.per_page = 50
puts Post.per_page  # => 10  (unchanged)
puts ApplicationRecord.per_page  # => 25  (unchanged)

This is different from a plain class instance variable โ€” class_attribute copies the value to the subclass on first write, so changes to the subclass don’t affect the parent.

Implementing class_attribute Manually

Here’s a simplified version of what class_attribute does:

module ClassAttribute
  def class_attribute(*attrs, default: nil)
    attrs.each do |attr|
      # Set default on this class
      instance_variable_set("@#{attr}", default)

      # Define getter โ€” walks up ancestor chain if not set locally
      define_singleton_method(attr) do
        if instance_variable_defined?("@#{attr}")
          instance_variable_get("@#{attr}")
        else
          superclass.send(attr) if superclass.respond_to?(attr)
        end
      end

      # Define setter โ€” sets on this class only
      define_singleton_method("#{attr}=") do |value|
        instance_variable_set("@#{attr}", value)
      end
    end
  end
end

class Base
  extend ClassAttribute
  class_attribute :timeout, default: 30
end

class Child < Base; end

puts Base.timeout   # => 30
puts Child.timeout  # => 30  (reads from parent)

Child.timeout = 60
puts Child.timeout  # => 60  (own value)
puts Base.timeout   # => 30  (unchanged)

Practical Use Cases

Per-Model Table Configuration (Rails)

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
end

class LegacyUser < ApplicationRecord
  self.table_name = "old_users"  # class instance variable
  self.primary_key = "user_id"
end

class ModernUser < ApplicationRecord
  self.table_name = "users"
end

Plugin/Gem Configuration

module Searchable
  def self.included(base)
    base.extend(ClassMethods)
    base.instance_variable_set(:@searchable_fields, [])
  end

  module ClassMethods
    def searchable(*fields)
      @searchable_fields = fields
    end

    def searchable_fields
      @searchable_fields || []
    end
  end
end

class Article
  include Searchable
  searchable :title, :body, :tags
end

class Product
  include Searchable
  searchable :name, :description, :sku
end

puts Article.searchable_fields.inspect  # => [:title, :body, :tags]
puts Product.searchable_fields.inspect  # => [:name, :description, :sku]

Summary

Feature Class Instance Variable (@var) Class Variable (@@var)
Scope Per-class object Shared across hierarchy
Inherited value No (nil in subclass) Yes (same variable)
Subclass can change parent No Yes (dangerous!)
Rails equivalent class_attribute Avoid
Recommended Yes Rarely

Class instance variables give you clean, per-class state without the surprising shared-mutation behavior of class variables. They’re the foundation of Rails’ per-model configuration system.

Resources

Comments