Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyの隠れた宝石:DelegatorとForwardableによる委譲パターン徹底解説

今回は、Ruby標準ライブラリに隠された宝石の中から「委譲(delegation)」について掘り下げていきます。

残念ながら、この用語は他の多くの技術用語と同じように、長年の間に意味が曖昧になり、人によって異なる解釈をされるようになりました。Wikipediaによると、委譲とは次のように定義されています。

委譲とは、あるオブジェクト(受信者)のメンバー(プロパティやメソッド)を、別の元オブジェクト(送信者)のコンテキストで評価することを指す。明示的な方法として、送信側オブジェクトを受信側オブジェクトに渡す方法があり、これはあらゆるオブジェクト指向言語で実現できる。また、言語のメンバールックアップ規則によって暗黙的に行う方法もあり、こちらは言語自体がこの機能をサポートしている必要がある。

しかし実際には多くの場合、「自分自身を引数として渡さずに、あるオブジェクトが別のオブジェクトの対応するメソッドを呼び出す」ことまで含めて「委譲」と表現されます。より正確に区別するなら、後者は「転送(forwarding)」と呼ぶべきものです。

この点を踏まえた上で、本記事では以降、これら両方のパターンをまとめて「委譲」という言葉で説明していきます。

Delegatorクラス

Rubyにおける委譲の探索は、標準ライブラリが提供するDelegatorクラスから始めましょう。このクラスは、いくつかの委譲パターンを提供しています。

SimpleDelegator

最もシンプルで、実際のコードベースでもよく見かけるのがSimpleDelegatorです。これはイニシャライザで渡されたオブジェクトをラップし、未定義のメソッド呼び出しをすべてそのオブジェクトに委譲します。実際に動作を見てみましょう。

require 'delegate'
 
User = Struct.new(:first_name, :last_name)
 
class UserDecorator < SimpleDelegator
  def full_name
    "#{first_name} #{last_name}"
  end
end

まず、SimpleDelegatorを使用可能にするためにrequire 'delegate'が必要です。次に、Structを使ってfirst_namelast_nameのアクセサを持つシンプルなUserクラスを作成しました。そして、個々の名前要素を1つの文字列に結合するfull_nameメソッドを定義したUserDecoratorを追加します。ここでSimpleDelegatorの出番です。現在のクラスにfirst_namelast_nameも定義されていないため、これらのメソッドはラップされたオブジェクトに対して呼び出されます。

decorated_user = UserDecorator.new(User.new("John", "Doe"))
decorated_user.full_name
#=> "John Doe"

SimpleDelegatorでは、superを使ってラップ先オブジェクトの対応するメソッドを呼び出しながら、委譲されるメソッドをオーバーライドすることもできます。これを利用して、フルネームではなくイニシャルだけを表示するようにしてみましょう。

class UserDecorator < SimpleDelegator
  def first_name
    "#{super[0]}."
  end
end
decorated_user.first_name
#=> "J."
decorated_user.full_name
#=> "J. Doe"

Delegator基底クラス

上記の例を読んでいて、「UserDecoratorはどのようにして委譲先のオブジェクトを知るのだろう?」と疑問に思った方もいるでしょう。答えはSimpleDelegatorの親クラスであるDelegatorにあります。これはカスタムの委譲スキームを定義するための抽象基底クラスであり、委譲先を取得・設定するための__getobj____setobj__の実装を提供する必要があります。この知識を活かせば、デモンストレーション用に独自版のSimpleDelegatorを簡単に作れます。

class MyDelegator < Delegator
  attr_accessor :wrapped
  alias_method :__getobj__, :wrapped
 
  def initialize(obj)
    @wrapped = obj
  end
end
 
class UserDecorator < MyDelegator
  def full_name
    "#{first_name} #{last_name}"
  end
end

これは、initializeメソッド内で__setobj__を呼び出すというSimpleDelegatorの実際の実装とは少し異なります。今回のカスタム委譲クラスでは不要なため、このメソッドは完全に省略しました。

動作は以前の例とまったく同じになるはずです。実際に確認してみましょう。

UserDecorator.superclass
#=> MyDelegator < Delegator
decorated_user = UserDecorator.new(User.new("John", "Doe"))
decorated_user.full_name
#=> "John Doe"

DelegateClass

Delegateライブラリが提供する最後の委譲パターンが、やや奇妙な名前のObject.DelegateClassメソッドです。このメソッドは特定のクラスに対する委譲クラスを生成して返し、それを継承して使うことができます。

  class MyClass < DelegateClass(ClassToDelegateTo)
    def initialize
      super(obj_of_ClassToDelegateTo)
    end
  end

一見すると分かりにくいかもしれません——特に継承の右辺に任意のRubyコードを書けるという点は驚きかもしれませんが——実はこれは以前に見たパターンと同じ流れであり、SimpleDelegatorを継承するのに似ています。

Rubyの標準ライブラリ自身も、この機能を使ってTempfileクラスを定義しています。Tempfileは処理の大部分をFileクラスに委譲しつつ、保存場所やファイル削除に関する特別なルールを実装しています。同じ仕組みを使えば、独自のLogfileクラスを次のように作成できます。

class Logfile < DelegateClass(File)
  MODE = File::WRONLY|File::CREAT|File::APPEND
 
  def initialize(basename, logdir = '/var/log')
    # logdirで指定された場所にログファイルを作成
    path = File.join(logdir, basename)
    logfile = File.open(path, MODE, 0644)
 
    # ここでDelegatorのinitializeメソッドが呼ばれるため、
    # 以降はLogfileインスタンスからFileの任意のメソッドを呼び出せる。
    super(logfile)
  end
end

Forwardableモジュール

興味深いことに、Rubyの標準ライブラリにはもう一つ委譲のためのライブラリが用意されています。それがForwardableモジュールと、そのdef_delegatordef_delegatorsメソッドです。

元のUserDecoratorの例をForwardableを使って書き直してみましょう。

require 'forwardable'
 
User = Struct.new(:first_name, :last_name)
 
class UserDecorator
  extend Forwardable
  def_delegators :@user, :first_name, :last_name
 
  def initialize(user)
    @user = user
  end
 
  def full_name
    "#{first_name} #{last_name}"
  end
end
 
decorated_user = UserDecorator.new(User.new("John", "Doe"))
decorated_user.full_name
#=> "John Doe"

最も顕著な違いは、委譲がmethod_missing経由で自動的に提供されるのではなく、転送したいメソッドごとに明示的に宣言する必要がある点です。これにより、ラップしたオブジェクトのメソッドのうち外部に公開したくないものを「隠す」ことができ、公開インターフェースを細かく制御できます。私が普段SimpleDelegatorよりもForwardableを好む主な理由はここにあります。

Forwardableのもう一つの便利な機能は、def_delegatorを使ったメソッド名のリネームです。省略可能な第3引数で別名を指定できます。

class UserDecorator
  extend Forwardable
  def_delegator :@user, :first_name, :personal_name
  def_delegator :@user, :last_name, :family_name
 
  def initialize(user)
    @user = user
  end
 
  def full_name
    "#{personal_name} #{family_name}"
  end
end

上記のUserDecoratorは、エイリアス化されたpersonal_namefamily_nameのみを公開し、内部的にはラップされたUserオブジェクトのfirst_namelast_nameへ転送します。

decorated_user = UserDecorator.new(User.new("John", "Doe"))
decorated_user.first_name
#=> NoMethodError: undefined method `first_name' for #<UserDecorator:0x000000010f995cb8>
decorated_user.personal_name
#=> "John"

この機能は意外と重宝します。過去には、似たインターフェースを持ちながらメソッド名の期待値が異なるライブラリ間での移行作業などに成功裏に使ったことがあります。

標準ライブラリの外へ

標準ライブラリに既存の委譲ソリューションがあるにもかかわらず、Rubyコミュニティでは長年にわたりいくつかの代替手段が開発されてきました。続いて、そのうちの2つを見ていきます。

Railsのdelegateメソッド

Railsの人気を考えると、RailsのdelegateメソッドはRuby開発者が最も頻繁に使う委譲の形態と言えるでしょう。お馴染みのUserDecoratorをこれで書き直してみます。

# 実際のRailsアプリケーションではApplicationRecordのサブクラスであることが多い
User = Struct.new(:first_name, :last_name)
 
class UserDecorator
  attr_reader :user
  delegate :first_name, :last_name, to: :user
 
  def initialize(user)
    @user = user
  end
 
  def full_name
    "#{first_name} #{last_name}"
  end
end
 
decorated_user = UserDecorator.new(User.new("John", "Doe"))
decorated_user.full_name
#=> "John Doe"

Forwardableとかなり似ていますが、delegateModuleに直接定義されているため、すべてのクラスやモジュールの本体で利用可能であり(良し悪しはありますが)、extendが不要になっています。さらにdelegateにはいくつかの便利な機能があります。

まず、:prefixオプションがあります。これを指定すると、委譲されるメソッド名に委譲先オブジェクトの名前が接頭辞として付加されます。

delegate :first_name, :last_name, to: :user, prefix: true

これにより、user_first_nameuser_last_nameというメソッドが生成されます。あるいは、カスタムの接頭辞を指定することも可能です。

delegate :first_name, :last_name, to: :user, prefix: :account

これで、ユーザー名の各部分にaccount_first_nameaccount_last_nameとしてアクセスできるようになります。

もう一つの興味深いオプションが:allow_nilです。委譲先のオブジェクトがnilの場合——例えば未設定のActiveRecord関連がある場合など——通常はNoMethodErrorが発生します。

decorated_user = UserDecorator.new(nil)
decorated_user.first_name
#=> Module::DelegationError: UserDecorator#first_name delegated to @user.first_name, but @user is nil

しかし、:allow_nilオプションを付けると、この呼び出しは成功し、代わりにnilを返します。

class UserDecorator
  delegate :first_name, :last_name, to: :user, allow_nil: true
 
  ...
end
 
decorated_user = UserDecorator.new(nil)
decorated_user.first_name
#=> nil

Casting gem

最後に紹介する委譲の選択肢は、Jim Gay氏によるCasting gemです。「selfを保持したままRubyでメソッドを委譲する」ことを可能にするツールです。これは厳密な意味での委譲の定義に最も近いもので、Rubyの動的な性質を利用して、メソッド呼び出しのレシーバを一時的に再バインドします。イメージとしては次のような感じです。

UserDecorator.instance_method(:full_name).bind(user).call
#=> "John Doe"

このアプローチの最も興味深い点は、スーパークラス階層を変更することなく、オブジェクトに振る舞いを追加できることです。

require 'casting'
 
User = Struct.new(:first_name, :last_name)
 
module UserDecorator
  def full_name
    "#{first_name} #{last_name}"
  end
end
 
user = User.new("John", "Doe")
user.extend(Casting::Client)
user.delegate(:full_name, UserDecorator)

ここではuserCasting::Clientで拡張し、delegateメソッドを使えるようにしました。あるいは、Userクラス内でinclude Casting::Clientを行えば、全インスタンスにこの能力を与えることもできます。

さらにCastingには、ブロックの存続期間中だけ、または手動で削除するまでの間、一時的に振る舞いを追加するオプションもあります。これを機能させるには、まず存在しないメソッドの委譲を有効にする必要があります。

user.delegate_missing_methods

単一のブロックの期間だけ振る舞いを追加したい場合は、Castingdelegatingクラスメソッドを使用します。

Casting.delegating(user => UserDecorator) do
  user.full_name #=> "John Doe"
end
 
user.full_name
#NoMethodError: undefined method `full_name' for #<struct User first_name="John", last_name="Doe">

あるいは、明示的にuncastを呼び出すまで振る舞いを追加し続けることも可能です。

user.cast_as(UserDecorator)
user.full_name
#=> "John Doe"
user.uncast
NoMethodError: undefined method `full_name' for #<struct User first_name="John", last_name="Doe">

他のソリューションよりやや複雑ですが、Castingは非常に高い制御性を提供しており、Jim氏は著書『Clean Ruby』でそのさまざまな活用法などを詳しく紹介しています。

まとめ

委譲とメソッド転送は、関連するオブジェクト間で責務を分担するための有用なパターンです。素のRubyプロジェクトではDelegatorForwardableの両方が使えますし、Railsコードでは自然とdelegateメソッドに向かいます。何を委譲するかを最大限に制御したい場合には、Casting gemが優れた選択肢となりますが、他のソリューションよりやや複雑です。

ゲスト執筆者のMichael Kohl氏は2003年頃からRubyとの付き合いを始めました。同氏はRubyについての執筆や講演も楽しんでおり、Bangkok.rbとRubyConf Thailandの共同主催者でもあります。

  1. Rubyでカスタム例外を作成する方法【初心者向けガイド】

    Rubyでは、独自の例外クラスを簡単に作成できます。この記事では、カスタム例外を作るための基本的な手順を、コード例とともにわかりやすく解説します。 1. 新しいクラスを作成する Rubyにおいて、例外も他のオブジェクトと同じくクラスとして定義されています。新しい種類の例外を作りたい場合は、StandardError、あるいはそのサブクラスを継承したクラスを定義するだけでOKです。 class MyError < StandardError end raise MyError 慣例として、新しく作る例外クラスの名前は「Error」で終わるように命名します。また、カスタム例外をモジュ

  2. Windows 11の隠れた便利機能15選!今すぐ使えるヒントと設定を徹底解説

    Windows 11は対応デバイス向けに無料アップグレードとして提供されており、数多くの新機能と改善が含まれています。再デザインされたスタートメニューとタスクバー、Androidアプリに対応した強化されたMicrosoft Store、統合されたMicrosoft Teams、スナップレイアウト、ウィジェットなど、目立つ新機能は多いものの、Redmond(マイクロソフト本社)が公式に発表した大きなニュース以外にも、一見すると気づきにくい小さな変更点がたくさん隠れています。この記事では、知っておくべきWindows 11の隠れた機能を厳選してご紹介します。 スタートメニューを左側に戻す方法 W