Skip to Content
Course content

242: Class Macros Like has_many Explained

Click on the "Edit" button in the top corner of the screen to edit your slide content.

If you've spent any time with Ruby on Rails, you've seen lines like has_many :comments or validates :email, presence: true. They look like special keywords or configuration settings, but they aren't. They are just Ruby methods. We call them "class macros" because they change the structure of the class while it's being defined.

I remember a junior dev on my team trying to build a lightweight custom validation system for a non-Rails project. They wrote this, and they were genuinely baffled when it crashed immediately on startup:

class User
  def validates(field)
    puts "Validating #{field}..."
  end

  validates :username
end

# Result: NoMethodError (undefined method `validates' for User:Class)

The NoMethodError Mystery

At first glance, the code looks perfect. The method validates is defined right there in the class, and then it's called. But here is the thing about Ruby: the code inside a class definition (but outside of any method) is executed immediately as the class is being loaded.

When Ruby hits the line validates :username, it is executing that call in the context of the User class object itself, not an instance of User. However, the validates method was defined as an instance method. You can't call an instance method on the class object. It's like trying to drive a car by sitting on the roof; you're in the right general area, but you're not in the driver's seat.

Elevating the Method to the Class Level

To fix this, we need to make validates a class method. By adding self. to the method definition, we tell Ruby that this method belongs to the class object, making it available to be called during the class definition phase.

class User
  def self.validates(field)
    puts "Validating #{field}..."
  end

  validates :username
end

# This now works! 
# Output: Validating username...

Now the code runs, but it's still not very useful. All it does is print a string. A real class macro doesn't just "do something" once; it usually "sets something up" for the instances of that class to use later.

How the Magic Actually Works

The "magic" of has_many or validates is that these methods use metaprogramming to define other methods on the fly. I like to think of a class macro as a method that writes more methods.

Let's turn our validates macro into something that actually works. We'll have it create a validation method for every field we specify:

class User
  def self.validates(field)
    # We are defining a new instance method on the fly
    define_method("validate_#{field}?") do
      puts "Checking if #{field} is present..."
      !self.send(field).nil?
    end
  end

  attr_accessor :username, :email
  
  validates :username
  validates :email
end

user = User.new
user.username = "ruby_dev"
puts user.validate_username? # => Checking if username is present... true
puts user.validate_email?    # => Checking if email is present... false

Here is what's happening under the hood:

  • We call the class method validates.
  • Inside that method, we use define_method. This is a powerful Ruby tool that creates a new instance method based on the string or symbol we provide.
  • Because define_method is called within the context of the User class, the new method (e.g., validate_username?) becomes available to every instance of User.

It’s a bit of a brain-bender the first time you see it. You're essentially using a method to manufacture other methods. This is exactly how Rails manages to keep its models so concise—it hides the repetitive boilerplate inside these macros.




📋 Practical Task

Build a Custom Relationship Macro

Your task is to implement a simple has_attribute class macro. Instead of manually writing getter and setter methods for a set of configuration options, you will create a macro that handles it for you.

Requirements:

  1. Create a class called Setting.
  2. Implement a class method self.has_attribute(name).
  3. The has_attribute method should use define_method to create a getter method (e.g., if you call has_attribute :theme, it should create a method theme that returns the value from an internal hash).
  4. The has_attribute method should also create a setter method (e.g., theme=`) that updates the value in that internal hash.
  5. Initialize an empty hash called @attributes in the initialize method of the Setting class to store these values.

Example Usage:

class Setting
  # Your macro implementation here
end

Setting.has_attribute :font_size
Setting.has_attribute :dark_mode

s = Setting.new
s.font_size = 12
puts s.font_size # Should print 12
Rating
0 0

There are no comments for now.

to be the first to leave a comment.