Tech Racho エンジニアの「?」を「!」に。
  • 開発

[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用である」と述べています。

関連記事


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。