From 33c8838d569262b96e56012fd24087015951445a Mon Sep 17 00:00:00 2001 From: jbranchaud Date: Thu, 9 Jul 2026 11:07:04 -0500 Subject: [PATCH] Add Avoid Dynamically Dispatching Private Methods as a Ruby TIL --- README.md | 3 +- ...dynamically-dispatching-private-methods.md | 84 +++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 ruby/avoid-dynamically-dispatching-private-methods.md diff --git a/README.md b/README.md index 6a448f1..c42fc09 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ working across different projects via [VisualMode](https://www.visualmode.dev/). For a steady stream of TILs, [sign up for my newsletter](https://visualmode.kit.com/newsletter). -_1814 TILs and counting..._ +_1815 TILs and counting..._ See some of the other learning resources I work on: @@ -1427,6 +1427,7 @@ If you've learned something here, support my efforts writing daily TILs by - [Assoc For Hashes](ruby/assoc-for-hashes.md) - [Audit Your Ruby Project For Any CVEs](ruby/audit-your-ruby-project-for-any-cves.md) - [Avoid Double Negation With Minitest Refute](ruby/avoid-double-negation-with-minitest-refute.md) +- [Avoid Dynamically Dispatching Private Methods](ruby/avoid-dynamically-dispatching-private-methods.md) - [Block Comments](ruby/block-comments.md) - [Block Syntaxes Have Different Precedence](ruby/block-syntaxes-have-different-precedence.md) - [Build HTTP And HTTPS URLs](ruby/build-http-and-https-urls.md) diff --git a/ruby/avoid-dynamically-dispatching-private-methods.md b/ruby/avoid-dynamically-dispatching-private-methods.md new file mode 100644 index 0000000..2e706cc --- /dev/null +++ b/ruby/avoid-dynamically-dispatching-private-methods.md @@ -0,0 +1,84 @@ +# Avoid Dynamically Dispatching Private Methods + +Ruby makes it easy to call any method on an object with the +[`Object#send`](https://docs.ruby-lang.org/en/4.0/Object.html#method-i-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. + +```ruby +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`](https://docs.ruby-lang.org/en/4.0/Object.html#method-i-public_send) +method. + +Here is a basic `Invoice` class to demonstrate: + +```ruby +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: + +```bash +❯ 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 '
' +``` + +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. + +```ruby +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 +```