0
0

Rubyのsendメソッドとメタプログラミングを少しだけ理解した

Posted at

はじめに

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です。

モデルファイルで次のように使います。

models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable
end

deviseメソッドでUserモデルに様々な認証機能の追加が可能です。

このdeviseメソッドでは内部的にsendメソッドが利用されています。

というわけでDeviseの実装を見てみましょう。

lib/devise/models.rb
# 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メソッドで以下のような処理が行われます。

  1. :"#{config}=":"timeout_in="
  2. 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のコードリーディングなどを通じてさらに理解を深めていければと思います。

またこの記事に関して誤りがあるようでしたら、コメントいただけますと幸いです。

参考資料

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0