「Forwardableモジュール」と「委譲」について学びました
ある程度の規模のWebサービスを作ろうとすると、Rubyの組み込みライブラリやRailsのMVC構造だけで設計することに限界を感じてきてしまいます。そんなときに、様々なデザインパターンや便利なモジュールを知っていれば、最適な設計を選択することができるかもしれません。例えば、過去の記事で紹介したObservableモジュールを使えば、見通しが良いコードが書ける場面があるかもしれません。その他にも、便利なモジュールはたくさんあると思いますので、少しずつ調べてブログに残していきたいと考えています。
今回は、わりと忘れがちなRuby標準添付モジュールである「Forwardable」と、それに関わる「委譲」という仕組みについて調べてみました。
Forwardableモジュール
Forwardableモジュールは、クラスに対してメソッドの委譲機能を追加するモジュールです。「委譲」とは何でしょうか。
Forwardableモジュール
http://docs.ruby-lang.org/ja/2.1.0/class/Forwardable.html
委譲(Delegate)
オブジェクト指向プログラミングにおいて、再利用したい機能を自身のオブジェクトに取り込むのではなく、その機能を持つオブジェクトに処理を依頼することを「委譲」と呼びます。処理を依頼するオブジェクトを「委譲元オブジェクト」、依頼を受けて処理を行うオブジェクトを「委譲先オブジェクト」と呼びます。あるオブジェクトに定義されているメソッドやプロパティを他のオブジェクトでも利用したい場合、「委譲」以外に「継承」や「ミックスイン」を使うという方法もあります。「継承」や「ミックスイン」を使うと、継承先(もしくはミックスイン先)のオブジェクトに定義されている全ての機能が利用できようになりますが、「委譲」は一部の機能だけが利用できるようになります。「余計な機能は取り込みたくない」というときに委譲を使うと良いのではないでしょうか。また、委譲元オブジェクトと委譲先オブジェクトに親子関係がうまれないところも委譲のポイントだと思います。
デモンストレーション
デモンストレーションとして、スタック機能を持つクラスを実装してみましょう。まずはForwardableモジュールを使わずに実装してみます。
class Stack < Array; end
はい、出来上がりました。スタックに必要な機能は既にArrayクラスが持っていますので、Arrayクラスを継承したクラスを作るだけで実装は終わりです。スタックとして使えるかどうか試してみましょう。
stack = Stack.new stack.push 1, 2, 3 puts stack.pop # => 3
意図した通りに動いていますね。
しかし、この実装には大きな問題があります。それは「#shiftなどの余計なメソッドが使えてしまうこと」です。Arrayクラスを継承してしまったのですから当然ですね。何とかしないといけません。Rubyには定義済みのメソッドをクラスから取り除くことができるが.undefというメソッドがありますので、これを使って#shiftメソッドを取り除いてみましょう。
class Stack < Array undef :shift end stack = Stack.new stack.push 1, 2, 3 stack.shift # => NoMethodError
#shiftメソッドを取り除くことに成功しました。他にも#unshift、#[]などもスタックには不要ですので、これらも取り除きたいですね。
しかし、いちいち.undefを使ってメソッドを取り除かないといけないのは面倒です。もう少し楽にスタックを実装できないのでしょうか。
そこで登場するのがForwardableモジュールです。Forwardableモジュールを使うと、必要なメソッドだけを持つスタックを簡単に実装できます。
require 'forwardable' class Stack extend Forwardable def initialize @ary = [] end def_delegators :@ary, :push, :pop, :clear, :size end
Forwardableモジュールの使い方は『Ruby Reference Manual』見てください。
Forwardableモジュール
http://docs.ruby-lang.org/ja/2.1.0/class/Forwardable.html
では、先ほど作ったStackクラスを使ってみます。
stack = Stack.new stack.push 1, 2, 3 stack.shift # => NoMethodError
結果を見ると、必要なメソッドだけを持つスタックを実装できていることがわかりますね。
委譲の利用しどころを考えた
デモンストレーションでは「使えるメソッドに制限をかける」という目的で委譲を利用したように見えますが、委譲を利用する本来の目的は「他のオブジェクトに処理を任せること」だそうです。「このクラスではこんな処理を担当するべき」「あのクラスではこんな処理を担当するべき」といった感じに、オブジェクトの責務を考えて設計したいときに委譲を使うと効果的だと思いました。
オブジェクトの責務を考える為に、MVCフレームにおいてControllerクラスからViewクラスにレンダリングの指示を送る処理を委譲を使って書いてみました。
require 'forwardable' class View def render(context) # レンダリング処理 end end class BaseController extend Forwardable def initialize @view = View.new end def_delegator :@view, :render end class AppController < BaseController def index render :index end end
Railsで委譲
Railsで委譲を利用する場合は、ActiveSupportの.delegateメソッドを使う方法もあるようです。
.delegateメソッドはForwardableモジュールの.def_delegatorメソッドと似たような処理を行います。
class Foo < ActiveRecord::Base def method1 'method1' end end class Bar < ActiveRecord::Base belongs_to :foo delegate :method1, to: :foo end bar = Bar.new bar.foo = Foo.new puts bar.method1 # => 'method1'
詳しくはRailsのドキュメントを見てください。
RailsAPIsドキュメントの#delegateメソッド
http://api.rubyonrails.org/classes/Module.html#method-i-delegate
以上、簡単にですが調べたことのまとめでした。