[Rails 5.1] 新機能: delegate_missing_toメソッド(翻訳)

こんにちは、hachi8833です。今回のBigBinaryシリーズは、週刊Railsウォッチでも紹介していなかったRails 5.1の新機能です。

DHHはこのメソッドをdecoratorで使うことを想定してるとのことです。

概要

新機能: delegate_missing_to(翻訳)

#method_missingを使うときには、#respond_to_missing?も併用するべきです。しかし、#method_missing#respond_to_missing?を両方使うとコードが冗長になるのも確かです。

DHHが自ら上げた#23824で、冗長なコードの典型的な例を見ることができます。

(訳注: このコード例はそのままAPIドキュメントでも使われています)

class Partition
  def initialize(first_event)
    @events = [ first_event ]
  end

  def people
    if @events.first.detail.people.any?
      @events.collect { |e| Array(e.detail.people) }.flatten.uniq
    else
      @events.collect(&:creator).uniq
    end
  end

  private
    def respond_to_missing?(name, include_private = false)
      @events.respond_to?(name, include_private)
    end

    def method_missing(method, *args, &block)
      @events.public_send(method, *args, &block)
    end
end

DHHは、こうしたコードを改善するために新しいModule#delegate_missing_toメソッドの利用を提案しています。利用例は次のとおりです。

class Partition
  delegate_missing_to :@events

  def initialize(first_event)
    @events = [ first_event ]
  end

  def people
    if @events.first.detail.people.any?
      @events.collect { |e| Array(e.detail.people) }.flatten.uniq
    else
      @events.collect(&:creator).uniq
    end
  end
end

SimpleDelegatorクラスでは不足な理由

BigBinary社では従来RubyのSimpleDelegatorクラスを使っていました。このクラスの問題は、このdelegatorは実行時にどんな種類のオブジェクトでも使われる可能性があるため、呼び出しが委譲される先のオブジェクトを静的に確定できないという点です。

DHHはこのパターンについて次のように言っています

この程度のシンプルな機能のために継承ツリーをハイジャックしなくても済む方がいい。

(訳注: 続きはこうなっています)

継承とsuperを使うのもどうかと思う。#delegate_missing_toなら意図が明確になる。

#delegateメソッドでは不足な理由

Module#delegateメソッドを使う手もあります。しかしその場合は全メソッドをホワイトリスト化しなければなりませんし、ホワイトリストが非常に長くなってしまうこともあります。以下は私たちの実際の案件から引用した実例コードです。

delegate :browser_status, :browser_stats_present?,
         :browser_failed_count, :browser_passed_count,
         :sequential_id, :project, :initiation_info,
         :test_run, :success?,
         to: :test_run_browser_stats

すべてを委譲するならdelegate_missing_to

状況によってはあらゆるmissing methodを委譲したくなることがありますが、#delegate_missing_toならそうした作業をすっきり行えます。なお、この委譲は委譲先のオブジェクトのpublicなメソッドに対してのみ有効であることにご注意ください。

詳しくは#23930のPRをご覧ください。

訳注

現在の5.1-stableの#delegate_missing_toは以下のようになっています。

# 5-1-stable/activesupport/lib/active_support/core_ext/module/delegation.rb#L262
  # オブジェクト内の呼び出し可能なものはすべて対象にできる
  # (例: インスタンス変数、メソッド、定数)
  def delegate_missing_to(target)
    target = target.to_s
    target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target)

    module_eval <<-RUBY, __FILE__, __LINE__ + 1
      def respond_to_missing?(name, include_private = false)
        # 見落としのように見えるかもしれないが、privateなものは委譲されないので
        # あえてinclude_privateを渡さないようにしている
        #{target}.respond_to?(name) || super
      end
      def method_missing(method, *args, &block)
        if #{target}.respond_to?(method)
          #{target}.public_send(method, *args, &block)
        else
          super
        end
      end
    RUBY
  end

#23824では#delegate_missing_toが何でも委譲することの是非などについて多少議論が紛糾していました。DHHは最後に「このパターンはdecorator用である」と述べています。

関連記事

Ruby on RailsによるWEBシステム開発、Android/iPhoneアプリ開発、電子書籍配信のことならお任せください この記事を書いた人と働こう! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

BigBinary記事より

ActiveSupport探訪シリーズ