はじめに
Rubyでは通常のメソッド呼び出しとは別に、send
メソッドでメソッドを呼び出すことができます。
この記事ではsend
メソッドの使い方とメタプログラミングとの関連、使用する際の注意点についてまとめます。
なおこの記事におけるRubyのバージョンは3.3です。
send
メソッドとは?
send
メソッドはレシーバが持つメソッドを文字列またはシンボルで指定して呼び出せるメソッドです。
実際のコードで確認します。
class Greeting
def hello
puts 'Hello, World!'
end
end
greeting = Greeting.new
# 通常のメソッド呼び出し
greeting.hello
#=> Hello, World!
# sendメソッドを使ったメソッド呼び出し(文字列)
greeting.send('hello')
#=> Hello, World!
# sendメソッドを使ったメソッド呼び出し(シンボル)
greeting.send(:hello)
#=> Hello, World!
第二引数以降には呼び出すメソッドへ渡す引数を指定できます。
class Greeting
def hello(name, i)
i.times do
puts "Hello, #{name}!"
end
end
end
greeting = Greeting.new
# 通常のメソッド呼び出し
greeting.hello('Taro', 3)
# Hello, Taro!
# Hello, Taro!
# Hello, Taro!
# sendメソッドを使ったメソッド呼び出し(文字列)
greeting.send('hello', 'Taro', 3)
# Hello, Taro!
# Hello, Taro!
# Hello, Taro!
# sendメソッドを使ったメソッド呼び出し(シンボル)
greeting.send(:hello, 'Taro', 3)
# Hello, Taro!
# Hello, Taro!
# Hello, Taro!
動的なメソッド呼び出し
send
メソッドの魅力は動的なメソッド呼び出しが可能な点です。
レシーバの持つメソッドを動的に呼び出すことでコードの重複を避けることができます。
たとえば以下のコードがあるとします。
class Animal
def sound(type)
case type
when 'dog'
dog_sound
when 'cat'
cat_sound
when 'cow'
cow_sound
end
end
def dog_sound
puts 'ワン'
end
def cat_sound
puts 'ニャー'
end
def cow_sound
puts 'モー'
end
end
animal = Animal.new
animal.sound('cat')
#=> ニャー
引数animal_name
の中身に応じてcase
文で3パターンの条件分岐を記述しています。
これをsend
メソッドを使って書き換えてみます。
class Animal
attr_accessor :type
def initialize(type)
@type = type
end
def dog_sound
puts 'ワン'
end
def cat_sound
puts 'ニャー'
end
def cow_sound
puts 'モー'
end
end
animal = Animal.new('cat')
animal.send("#{animal.type}_sound")
#=> ニャー
send
メソッドを駆使することでcase
文による条件分岐を丸々書かずに実装できます。
そのほかに、たとえば次のような実装ができます。
①ファイル形式に応じたレポート出力
PDFやCSVなどファイル形式に応じた挙動が簡単に実現できます。
class ReportGenerator
def generate(format)
send("generate_#{format}_report")
end
private
def generate_pdf_report
# 実際のPDFレポート生成ロジックを書く
puts "PDF report generated"
end
def generate_html_report
# 実際のHTMLレポート生成ロジックを書く
puts "HTML report generated"
end
def generate_csv_report
# 実際のcsvレポート生成ロジックを書く
puts "Csv report generated"
end
end
report_generator = ReportGenerator.new
report_generator.generate(:html)
#=> HTML report generated
②支払い方法に応じた決済処理
クレカ・銀行振込など支払い方法に応じた条件分岐も同様に実現できます。
class PaymentProcessor
def process(payment_method)
send("#{payment_method}_payment")
end
private
def credit_card_payment
# 実際のクレジットカード決済処理を書く
puts "Processing credit card payment"
end
def paypal_payment
# 実際のPayPal決済処理を書く
puts "Processing PayPal payment"
end
def bank_transfer_payment
# 実際の銀行振込決済処理を書く
puts "Processing bank transfer payment"
end
end
payment_processor = PaymentProcessor.new
payment_processor.process(:bank_transfer)
#=> Processing bank transfer payment
send
メソッドとメタプログラミング
send
メソッドは、Rubyにおけるメタプログラミングの重要なツールです。
メタプログラミングとは、「コードを書くコードを書くこと」を指し、プログラムの動作を動的に変更したり拡張したりする技術です。
send
メソッドを使用することで、プログラムの実行時に動的にメソッドを呼び出すことができ、これによって柔軟なコードを書くことが可能になります。
OSSのコードで使われ方を見てみます。
Devise
の例
Devise
は認証機能を提供するgemです。
モデルファイルで次のように使います。
class User < ApplicationRecord
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable
end
devise
メソッドでUserモデルに様々な認証機能の追加が可能です。
このdevise
メソッドでは内部的にsend
メソッドが利用されています。
というわけでDevise
の実装を見てみましょう。
# frozen_string_literal: true
module Devise
module Models
# ...省略...
# Include the chosen devise modules in your model:
#
# devise :database_authenticatable, :confirmable, :recoverable
#
# You can also give any of the devise configuration values in form of a hash,
# with specific values for this model. Please check your Devise initializer
# for a complete description on those values.
#
def devise(*modules)
options = modules.extract_options!.dup
selected_modules = modules.map(&:to_sym).uniq.sort_by do |s|
Devise::ALL.index(s) || -1 # follow Devise::ALL order
end
devise_modules_hook! do
include Devise::Orm
include Devise::Models::Authenticatable
selected_modules.each do |m|
mod = Devise::Models.const_get(m.to_s.classify)
if mod.const_defined?("ClassMethods")
class_mod = mod.const_get("ClassMethods")
extend class_mod
if class_mod.respond_to?(:available_configs)
available_configs = class_mod.available_configs
available_configs.each do |config|
next unless options.key?(config)
send(:"#{config}=", options.delete(config))
end
end
end
include mod
end
self.devise_modules |= selected_modules
options.each { |key, value| send(:"#{key}=", value) }
end
end
# ...省略...
end
end
require 'devise/models/authenticatable'
特に注目したいのはこの行です。
send(:"#{config}=", options.delete(config))
たとえばDevise
を次のように設定したとします。
class User < ApplicationRecord
devise :database_authenticatable, :registerable, :timeoutable, timeout_in: 30.minutes
end
この場合、先ほどのsend
メソッドで以下のような処理が行われます。
-
:"#{config}="
は:"timeout_in="
-
options.delete(config)
は30.minutes
つまり実質的に以下のコードと同じになります。
self.timeout_in = 30.minutes
これによりタイムアウト時間が設定されるのです。
また以下の箇所でもsend
メソッドが使われています。
options.each { |key, value| send(:"#{key}=", value) }
これは特定のモジュールに属さない設定オプションを処理します。
たとえば次の実装があったとします。
class User < ApplicationRecord
devise :database_authenticatable, :registerable, skip_session_storage: [:http_auth]
end
この場合、skip_session_storage
は特定のモジュールに属さない一般的なオプションです。
send(:"#{key}=", value)
の実行内容は以下と同様になります。
self.skip_session_storage = [:http_auth]
この場面ではsend
メソッドを使うことで以下の利点が得られます。
- どのような設定オプションが来ても対応するセッターメソッドを動的に呼び出せる
- 多数のオプションに対して個別の条件分岐を書く必要がない
- 新しい設定オプションが追加された場合もこのコードはそのまま機能する
gemはさまざまなユースケースへの対応が求められます。
またそのための拡張性も欠かせません。
このように高い汎用性や拡張性が要求される場面でsend
メソッドのようなメタプログラミング技術が求められます。
sendメソッドを使う際の注意点
send
メソッドは強力な機能ではありますが、注意点もたくさんあります。
以下に主な注意点をまとめます。
セキュリティリスク
send
メソッドはプライベートメソッドを呼び出すことができてしまいます。
class Sample
private
def secret_method
puts "This is a private method!"
end
end
keeper = Sample.new
keeper.send(:secret_method) # => This is a private method!
このように本来アクセスできないはずのメソッドにアクセスできるのです。
なおRubyにはsend
メソッドとは別にpublic_send
メソッドも存在します。
こちらはsend
メソッドとの違いはプライベートメソッドを呼び出せないことです。
class Sample
private
def secret_method
puts "This is a private method!"
end
end
keeper = Sample.new
keeper.public_send(:secret_method)
# => private method `secret_method' called for an instance of Sample (NoMethodError)
安全性を重視したい場合はpublic_send
メソッドを使いましょう。
可読性の低下
send
メソッドのもたらす高い柔軟性はOSSなど多様なユースケースをカバーするだけの汎用性が必要な場面では有効です。
しかし実務では高い汎用性というよりは特定のビジネスニーズ・ロジックに対応することが求められます。
そういった状況下でsend
メソッドを筆頭とするメタプログラミングを過度に活用してしまうと、逆に複雑さを増してしまいかねません。
gemの開発やOSSのコードリーディングのためsend
メソッドやメタプログラミングの知識を持つことは重要ですが、日々の業務では使用を控えめにした方が良さそうです。
※send
メソッドの代替
Ruby 2.7以降では、send
メソッドの代わりに__send__
メソッドを使用することが推奨される場合があります。
send
というメソッド名が他の目的で既に使用されている場合の名前衝突を避けるためです。
class Sample
def send(message)
puts "Sending #{message}"
end
def greet
puts "Hello!"
end
end
sample = Sample.new
sample.send("a message") # => Sending a message
sample.__send__(:greet) # => Hello!
このように、send
メソッドが別の目的で使用されている場合には、__send__
メソッドを予備として使えます。
おわりに
記事を書いたものの、メタプログラミングについてはまだ分かったような分かっていないような感覚が強いです。
OSSのコードリーディングなどを通じてさらに理解を深めていければと思います。
またこの記事に関して誤りがあるようでしたら、コメントいただけますと幸いです。
参考資料