1
0
mirror of https://github.com/jbranchaud/til synced 2026-07-09 16:36:10 +00:00
Files
til/ruby/avoid-dynamically-dispatching-private-methods.md
T

2.0 KiB
Raw Blame History

Avoid Dynamically Dispatching Private Methods

Ruby makes it easy to call any method on an object with the Object#send method. It's powerful because I can dynamically call methods (e.g. based on a variable that is being passed in) and I can even dispatch private methods which is normally disallowed.

obj.send(:initialize, new_args)

If I want the benefits of dynamic dispatch without allow private methods to be called, then I can instead reach for the Object#public_send method.

Here is a basic Invoice class to demonstrate:

class Invoice
  TAX_RATE = 0.08

  def initialize(subtotal:)
    @subtotal = subtotal
  end

  def total
    (@subtotal + tax).round(2)
  end

  private

  def tax
    @subtotal * TAX_RATE
  end
end

invoice1 = Invoice.new(subtotal: 250)

puts "Total: #{invoice1.send(:total)}"
puts "Tax: #{invoice1.send(:tax)}"

# this will error
invoice1.public_send(:tax)

When I run this file, the two send statements both work while the public_send on a private method raises:

 ruby object_send.rb
Total: 270.0
Tax: 20.0
object_send.rb:25:in 'Kernel#public_send': private method 'tax' called for an instance of Invoice (NoMethodError)

invoice1.public_send(:tax)
        ^^^^^^^^^^^^
Did you mean?  tap
	from object_send.rb:25:in '<main>'

Where this behavior might be most useful is in a Rails concern file where I am defining some shared behavior that needs to work dynamically across classes that are including it.

module ValidatesAttachment
  extend ActiveSupport::Concern

  IMAGE_TYPES = %w[image/gif image/jpeg image/jpg image/png].freeze
  MAX_SIZE = 5.megabytes

  class_methods do
    def validates_attachment(name, content_types: IMAGE_TYPES, max_size: MAX_SIZE, types_message: nil)
      validate do
        attachment = public_send(name)

        # ... validation logic omitted
      end
    end
  end
end