Skip to Content
Course content

20: define_method in Practice

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

Imagine you're running a craft shop and you need to create 50 different stamps for 50 different employees' names. You could spend all afternoon carving each single stamp by hand, which is tedious and prone to error. Or, you could buy one "adjustable" stamp kit. With the kit, you have a single frame, and you just slide in a small plate with the specific name you want. The mechanism of stamping is the same every time; only the label changes.

In Ruby, define_method is that adjustable stamp kit. Normally, when you use def, you're carving a stamp by hand. You're telling Ruby exactly what the method is called and exactly what it does at the moment you write the code. But define_method lets you create methods programmatically while the program is actually running. You define the "mechanism" once, and then you can spin up as many methods as you need based on a list of names.

Stopping the Boilerplate Bleed

I've seen a lot of junior devs write code that looks like this when they're handling user roles:

def admin?
  role == 'admin'
end

def editor?
  role == 'editor'
end

def viewer?
  role == 'viewer'
end

It's not "wrong," but it's boring. And the moment your boss tells you that you now need super_admin, billing_manager, and support_agent roles, you're stuck writing more of the same. It's a maintenance nightmare. Instead, we can use define_method to handle this dynamically.

class User
  attr_accessor :role

  # We define the list of roles we care about here
  ROLES = [:admin, :editor, :viewer, :moderator]

  ROLES.each do |role_name|
    # This is where the magic happens
    define_method("#{role_name}?") do
      self.role == role_name.to_s
    end
  end

  def initialize(role)
    @role = role
  end
end

user = User.new('admin')
puts user.admin? # => true
puts user.editor? # => false

The Mechanics of the Block

Here is the crucial part you need to understand: define_method takes a symbol (or a string) as the name of the method and a block of code as the body. Because it's a block, it captures the surrounding scope. In the example above, the role_name variable from the each loop is "remembered" inside the method body.

You might be wondering: "Why can't I just use def inside the loop?" Try it. You'll realize that def expects a hard-coded method name. You can't do def "#{role_name}?" because the Ruby parser doesn't work that way. define_method is the only way to turn a variable into a method name.

Don't Overdo the Magic

I'll give you a piece of advice I wish someone gave me early on: just because you can metaprogram everything doesn't mean you should. When you use define_method, the methods don't exist in your source code. If a new developer joins your team and searches the codebase for def admin?, they won't find it. They'll be staring at the screen wondering where the hell that method is coming from.

Use this when you have a clear, repetitive pattern based on a data set (like roles, API endpoints, or database columns). If the logic for each method is wildly different, just stick to regular def. Clarity always beats cleverness in a production environment.




📋 Practical Task

Building a Dynamic Attribute Validator

You are building a Profile class. Instead of writing individual validation methods for every field (like username_present?, email_present?, etc.), you need to implement a system using define_method.

Requirements:

  • Create a Profile class with an attr_accessor :details (which will be a Hash).
  • Create a constant array called REQUIRED_FIELDS containing :username, :email, and :bio.
  • Use define_method to dynamically create a method for each field in REQUIRED_FIELDS.
  • Each generated method should follow the naming convention field_present? (e.g., username_present?).
  • The method should return true if the key exists in the details hash and the value is not nil or empty; otherwise, return false.

Test your code with:

profile = Profile.new
profile.details = { username: "ruby_pro", email: "" }

puts profile.username_present? # Should be true
puts profile.email_present?    # Should be false
puts profile.bio_present?      # Should be false
Rating
0 0

There are no comments for now.

to be the first to leave a comment.