The Hidden Power of Method Objects

Most Rubyists are comfortable calling methods on objects. We do it thousands of times a day. However, there is a distinct difference between calling a method and capturing a method. In Ruby, you can turn any method into a first-class object using the method method. This allows you to pass behavior around your application as a variable, effectively treating a method like a Proc or a Lambda without the overhead of redefining it.

I first stumbled upon this while building a complex data validation engine. I had a dozen different validation rules, each living in a separate service class. Initially, I used a giant case statement to decide which validation method to run based on a configuration string. It was brittle, hard to test, and a nightmare to extend. By switching to method objects, I turned that rigid logic into a flexible pipeline.

The Real-World Scenario: Dynamic Strategy Execution

Imagine you're building a payment processing system. Depending on the payment provider (Stripe, PayPal, Adyen), you need to execute different normalization steps on the incoming payload. Instead of writing a complex adapter pattern with multiple classes that do almost the same thing, you can map provider keys directly to the methods that handle them.

class PaymentNormalizer
  def normalize_stripe(data)
    # Stripe uses 'amount' in cents
    { amount: data[:amount] / 100.0, currency: data[:currency].upcase }
  end

  def normalize_paypal(data)
    # PayPal uses a nested 'transaction' object
    { amount: data.dig(:transaction, :amount), currency: 'USD' }
  end

  def normalize_adyen(data)
    # Adyen requires specific field mapping
    { amount: data[:value], currency: data[:currency_code] }
  end

  def process(provider, payload)
    # Instead of a case statement, we dynamically fetch the method object
    method_name = "normalize_#{provider}"
    
    if respond_to?(method_name)
      # This is the magic: capturing the method as an object
      handler = self.method(method_name)
      handler.call(payload)
    else
      raise ArgumentError, "Unsupported provider: #{provider}"
    end
  end
end

# Usage
normalizer = PaymentNormalizer.new
puts normalizer.process(:stripe, { amount: 5000, currency: 'usd' }) # {:amount=>50.0, :currency=>"USD"}
puts normalizer.process(:paypal, { transaction: { amount: 25.0 } }) # {:amount=>25.0, :currency=>"USD"}

Why This Beats the Alternatives

You might be thinking, "Why not just use send("normalize_#{provider}", payload)?" While send works, it's a blunt instrument. Capturing the method via method() provides several strategic advantages:

  • Decoupling Execution from Definition: You can pass the method object to another service or a background job. The receiver doesn't need to know the name of the method or the class it belongs to; it only needs to know it has a #call method.
  • Better Introspection: A Method object carries metadata. You can check its owner, its parameters, and its source_location. This is invaluable for debugging complex meta-programming layers.
  • Consistency: By treating methods as objects, your API becomes consistent with Ruby's functional leanings. Whether you're passing a Proc, a Lambda, or a Method object, the interface is always .call.
Pro Tip: When using method(), always pair it with respond_to?. Attempting to capture a method that doesn't exist will raise a NameError, which can crash your request cycle if not handled.

Integrating with Higher-Order Functions

The true elegance of this technique appears when you combine it with Ruby's enumerable methods. Suppose you have a list of transformation methods that must be applied to a record in a specific order. Instead of manually calling each one, you can create an array of method objects.

class UserProfileCleaner
  def strip_whitespace(val); val&.strip; end
  def downcase_email(val); val&.downcase; end
  def remove_special_chars(val); val&.gsub(/[^a-zA-Z0-9@ ]/, ''); end

  def clean(attribute, value)
    # Define the pipeline as a list of method objects
    pipeline = [
      method(:strip_whitespace),
      method(:remove_special_chars),
      method(:downcase_email)
    ]

    # Reduce the value by calling each method object in sequence
    pipeline.reduce(value) { |acc, step| step.call(acc) }
  end
end

cleaner = UserProfileCleaner.new
puts cleaner.clean(:email, "  User@Example.Com!  ") # "user@example.com"

Final Thoughts

Using Method#call transforms how you think about logic flow. It moves you away from rigid conditional structures and toward a more compositional style of programming. It's a lightweight way to implement the Strategy pattern without the boilerplate of creating five different classes for five different behaviors. Next time you find yourself writing a long case statement to delegate work, try capturing the methods instead.