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_nameとlast_nameのアクセサを持つシンプルなUserクラスを作成しました。そして、個々の名前要素を1つの文字列に結合するfull_nameメソッドを定義したUserDecoratorを追加します。ここでSimpleDelegatorの出番です。現在のクラスにfirst_nameもlast_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
enddecorated_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
endForwardableモジュール
興味深いことに、Rubyの標準ライブラリにはもう一つ委譲のためのライブラリが用意されています。それがForwardableモジュールと、そのdef_delegator・def_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_nameとfamily_nameのみを公開し、内部的にはラップされたUserオブジェクトのfirst_nameとlast_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とかなり似ていますが、delegateはModuleに直接定義されているため、すべてのクラスやモジュールの本体で利用可能であり(良し悪しはありますが)、extendが不要になっています。さらにdelegateにはいくつかの便利な機能があります。
まず、:prefixオプションがあります。これを指定すると、委譲されるメソッド名に委譲先オブジェクトの名前が接頭辞として付加されます。
delegate :first_name, :last_name, to: :user, prefix: trueこれにより、user_first_nameとuser_last_nameというメソッドが生成されます。あるいは、カスタムの接頭辞を指定することも可能です。
delegate :first_name, :last_name, to: :user, prefix: :account
これで、ユーザー名の各部分にaccount_first_nameとaccount_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
#=> nilCasting 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)ここではuserをCasting::Clientで拡張し、delegateメソッドを使えるようにしました。あるいは、Userクラス内でinclude Casting::Clientを行えば、全インスタンスにこの能力を与えることもできます。
さらにCastingには、ブロックの存続期間中だけ、または手動で削除するまでの間、一時的に振る舞いを追加するオプションもあります。これを機能させるには、まず存在しないメソッドの委譲を有効にする必要があります。
user.delegate_missing_methods単一のブロックの期間だけ振る舞いを追加したい場合は、Castingのdelegatingクラスメソッドを使用します。
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プロジェクトではDelegatorとForwardableの両方が使えますし、Railsコードでは自然とdelegateメソッドに向かいます。何を委譲するかを最大限に制御したい場合には、Casting gemが優れた選択肢となりますが、他のソリューションよりやや複雑です。
ゲスト執筆者のMichael Kohl氏は2003年頃からRubyとの付き合いを始めました。同氏はRubyについての執筆や講演も楽しんでおり、Bangkok.rbとRubyConf Thailandの共同主催者でもあります。
-
Rubyでカスタム例外を作成する方法【初心者向けガイド】
Rubyでは、独自の例外クラスを簡単に作成できます。この記事では、カスタム例外を作るための基本的な手順を、コード例とともにわかりやすく解説します。 1. 新しいクラスを作成する Rubyにおいて、例外も他のオブジェクトと同じくクラスとして定義されています。新しい種類の例外を作りたい場合は、StandardError、あるいはそのサブクラスを継承したクラスを定義するだけでOKです。 class MyError < StandardError end raise MyError 慣例として、新しく作る例外クラスの名前は「Error」で終わるように命名します。また、カスタム例外をモジュ
-
Windows 11の隠れた便利機能15選!今すぐ使えるヒントと設定を徹底解説
Windows 11は対応デバイス向けに無料アップグレードとして提供されており、数多くの新機能と改善が含まれています。再デザインされたスタートメニューとタスクバー、Androidアプリに対応した強化されたMicrosoft Store、統合されたMicrosoft Teams、スナップレイアウト、ウィジェットなど、目立つ新機能は多いものの、Redmond(マイクロソフト本社)が公式に発表した大きなニュース以外にも、一見すると気づきにくい小さな変更点がたくさん隠れています。この記事では、知っておくべきWindows 11の隠れた機能を厳選してご紹介します。 スタートメニューを左側に戻す方法 W