Rubyのdelegateについて整理する
Rubyのdelegateという機能にぶつかって、色々調べたんだけどいまいちしっくりくる記事がなかったので、今週はそのあたりを少し掘り下げて、自分用に整理しようと思います。
自分用に整理しているだけなので、実装の詳細に入り込んだりするわけではなく、delegateってこういう機能ですというのをまとめたものになります。
委譲ってなんだっけ
最近はあまり聞かなくなったように思いますが、ちょっと前にはオブジェクト指向の継承をひどく嫌う一派があって、「継承より委譲」の旗印のもと、委譲(delegate)パターンが流行ったことがありました。
委譲についても他のパターンと同様、過度なパターン化によって逆にコードの見通しが悪くなったり、どう動いているのかを理解するのが難しくなったりする弊害があるのは、(実際にそれが濫用されたことで)多くの人が身にしみて理解できていると思います。
Delegateパターンは、平たく言えば「処理を別のオブジェクトに肩代わりしてもらう」パターンで、あるクラスに実装してある処理を、そのクラスのインスタンスを持つ別のクラス側で利用したいような場合に使われるパターンだと言えます。
具体例を交えて
具体的にdelegateパターンをどう使っていくかというのを言葉だけで説明するのは難しいので「俳優」と「マネージャ」の関係を例にとって説明を進めたいと思います。
ここで「俳優」はActorクラスのインスタンスとして、「マネージャ」はMangerクラスのインスタンスとして表現することにします。
この俳優は案外器用で、アクションもコメディーもこなす多才ぶりです。
さらに、はじめのうちは仕事の数も少ないので、スケジュール管理も自分でこなさなければなりません。
Rubyちっくなコードで書くと、こんな具合になると思います。
code:ruby
class Actor
def action
... # ノースタントで過激なアクションもこなす
end
def comedy
... # コメディー的な演出に対応する
end
def schedule
... # スケジュール管理をする
end
end
この俳優、ある映画に助演として出演したことがきっかけで一気にスターダムを駆け上がります。
忙しくなってくると徐々にスケジュール管理が難しくなってきたので、所属事務所に頼み込んで担当マネージャをつけてもらう事にしました。
担当マネージャをRubyちっくなコードで表現すると、こんな具合です。
code: ruby
class Manager
def schedule
... # スケジュール管理を実行
end
end
俳優の方は、スケジュール管理をマネージャに頼む、つまり委譲した訳ですから、これまでのように自分自身にスケジュールの問い合わせがあった場合には、担当マネージャにつないでおかないと正しいスケジュール管理をすることができません。そこで、こんな風に書き換わります。
code: ruby
class Actor
delegate :schedule, to: :@my_manager
def initialize(my_manager)
@my_manager = my_manager
end
def action
... # ノースタントで過激なアクションもこなす
end
def comedy
... # コメディー的な演出に対応する
end
end
以前の実装との違いは、スケジュールメソッドの実装がActorクラスの中から取り除かれたことと、クラスの冒頭のdelegateによって、担当マネージャ(@my_manager)に処理を委譲するようになったことです。
このコードはどういう動きをするのか?
こう書くと、どう動くのか?簡単に図にすると、こんな具合です。
https://gyazo.com/3c6aa04fe6300a9a847c33c210a640ac
先のコードをこんな風に適当に書き換えてやれば、irbなどを使ってコードを動かしてみることもできます。
code:ruby
class Actor
delegate :schedule, to: :@my_manager
def initialize(my_manager)
@my_manager = my_manager
end
end
class Manager
def schedule
"schedule controled by manager!"
end
end
こんな具合に書いたコードをirb上で動かしてみると、次のような挙動を示します。
code:ruby
irb(main):013:0> manager = Manager.new
irb(main):014:0> actor = Actor.new(manager)
irb(main):015:0> actor.schedule # actorクラスのshceduleメソッドを呼び出す
=> "schedule controled by manager!" # 委譲によって、managerの持っているscheduleメソッドが応答する
Actorクラスのインスタンスを作ったときに、Managerクラスのインスタンスがactorオブジェクトに抱え込まれていることがわかります。
コンテキストはどうなっているのか?
先に上げたirb上で動かせるコードは、Actorクラスを通じてscheduleメソッドを呼び出していました。
では、このときのコンテキストはどうなっているのでしょうか。
つまり、actorから呼ばれたscheduleのレシーバになるselfは誰か?という事です。
Managerクラスのscheduleメソッドを書き換えて、selfをについて言及させるようにすれば、この疑問の答えがわかります。
code: ruby
class Manager
def schedule
self.class.name
end
end
code:ruby
irb(main):025:0> Actor.new(Manager.new).schedule
=> "Manager" # Actorに対して呼び出したscheduleなのに、selfとしてManagerを返す
くわえ込んだインスタンスに処理を任せているので、当たり前といえば当たり前かもしれませんが、一見するとActorのメソッドに見えたscheduleは、実は下記の実装と同じ挙動を示していることがわかります。
code:ruby
class Actor
...
def schedule
@manager.schedule
end
end
このあたりの動きを見ると、Rubyのdelegateは一種のシンタックスシュガーと考えることができそうです。
Rubyならではのdelegate
さらにRubyは動的型付けの言語でダックタイピングが可能なので、OpenStructのような一種の構造に対してもdelegateを使うことができます。
code:ruby
irb(main):026:0> manager = OpenStruct.new({schedule: 'by OpenStruct'})
irb(main):027:0> actor = Actor.new(manager)
irb(main):028:0> actor.schedule
=> "by OpenStruct"
メソッドの要領でアクセスすることができれば同様にdelegateを使うことができるので、ActiveRecordのフィールドに対してdelegateする事もできるわけです。
ここで、冒頭の「Rubyのdelegateにぶつかった」話に戻ります。そうです。私がさわったコードでは、ActiveRecordのフィールドに対して、つまりデータベースのカラムに対する委譲が記述されており「なんじゃこりゃ〜!?」となった結果、今回の調査に至ったわけです。
個人的な意見ですが、Rubyの提供するこのdelegateはシンタックスシュガーの一種であって、他の言語で継承の代わりに用いられるような委譲の類とは少し趣を異にする機能ではないかと感じました。
他人の書いたコードを読む上では細かく知っておく必要がありそうですが、書き味を確かめてみると、先のActorの例にように、以前はあるクラスの配下で行っていた処理(schedule)を、外側に対して変更を強いること無く改修したいケース以外では使わ無さそうだなあと思いました。