Rubyデザインパターン学習のために、自分なりに読書の結果をまとめていくことに決めました。第7日目はProxyです。(http://www.amazon.co.jp/gp/product/4894712857/ref=as_li_qf_sp_asin_tl?ie=UTF8&camp=247&creative=1211&creativeASIN=4894712857&linkCode=as2&tag=morizyun00-22)
7日目 Proxy
7日目はProxyパターンです。
直訳で代理という意味です。
ものすごくざっくり言うと、プログラム本体のクラスの前にもう一つクラスを咬ませて、番人かつ代理人のような役割を持たせるパターンです。
Proxy サンプルコード
銀行の管理プログラムを例にとってみましょう
class BankAccount
attr_reader :balance
def initialize(starting_balance = 0)
@balance = starting_balance
end
def deposit(amount)
@balance += amount
end
def withdraw(amount)
@balance -= amount
end
end
ユーザーがこのオブジェクトにモバイルからアクセスしたいと考えましょう。
ここで、Proxyとしてもう一つクラスを作ってみましょう。
class BankAccountProxy
def initialize(real_object)
@real_object = real_object
end
def balance
@real_object.balance
end
def deposit(amount)
@real_object.deposit(amount)
end
def withdraw(amount)
@real_object.withdraw(amount)
end
end
account = BankAccount.new
proxy = BankAccountProxy.new(account)
proxy.deposit(50)
proxy.withdraw(10)
Proxyを一つ作ってみましたが、これだけではただ参照をするだけの役立たずクラスです。
防御Proxy
先ほどの章で作ったProxyを、役に立つものに変えていきましょう。
とはいっても実装は簡単です。
require 'etc'
class AccountProtectionProxy
def initialize(real_account, owner_name)
@subject = real_account
@owner_name = owner_name
end
def deposit(amount)
check_access
return @subject.deposit(amount)
end
def withdraw(amount)
check_access
return @subject.withdraw(amount)
end
def balance
check_access
return @subject.balance
end
def check_access
if Etc.getlogin != @owner_name
raise "Illegal access: #{Etc.getlogin} cannot access account."
end
end
end
引数はreal_account
とowner_name
の二つを受け取るようにします。
check_access
メソッドで、一度監査が入り、オーナーであれば本体のBankAccountオブジェクトへのアクセス権を得ることができ、オーナーではないと判断されればエラーが起きるような実装になっています。
こういった形で、データを管理するBankAccountの前に一つProxyを咬ませるような実装にすることで、本体オブジェクト(以下、サブジェクトと呼びます)へのアクセスを制御できます。
サブジェクトの前に咬ませるような構造なので、そのとっかえも、仕様変更も簡単です。セキュリティ仕様を変えたければAccountProtectionProxyだけを編集すれば良いし、希望があれば、除去することもできます。
こういった、責務の分離は非常に重要な概念です。
セキュリティの仕様変更をしたいときは、セキュリティに関する部分だけを見れば良いので、素早く問題解決に当たることができます。
さて、これだけでも立派なセキュリティ実装パターンが実現できているのですが、現状での問題点がもう一つあります。
それは、負荷のかかるオブジェクト作成を初めてに行ってしまっている点です
account = BankAccount.new(100)
アプリケーションにおいて、負荷のかかるオブジェクト作成を可能な限り遅延させるという方法は、アプリケーション全体のパフォーマンスに良い影響を与えることがよくあります。
オブジェクト作成を遅延させる
class VirtualAccountProxy
def initialize(starting_balance = 0)
@starting_balance = starting_balance
end
def deposit(amount)
s = subject
return s.deposit(amount)
end
def withdraw(amount)
s = subject
return s.withdraw(amount)
end
def balance
s = subject
return s.balance
end
def subject
@subject || (@subject = BankAccount.new(@starting_balance))
end
end
subject
メソッドを実装することにより、可能な限りオブジェクトの生成を遅らせています。
オブジェクトが作られるのは、subject
が実行され、かつ@subject
にまだ中身がないときだけです。
@subject
がtrue
であれば右辺は評価されません。
def subject
@subject || (@subject = BankAccount.new(@starting_balance))
end
更に責務を分ける
さて、だいぶ洗練されてきましたが、現在はまだオブジェクト作成機能をProxyが持っている状態です。
Proxyはなるべく番人、代理人出会って欲しいので、言ってしまえばサブジェクトへの橋渡しが最も大事な業務です。余分な業務を持つ必要もないでしょう。
ここでまたRubyのコードブロックを使ってみましょう。
class VirtualAccountproxy
def initialize(&creation_block)
@creation_block = creation_block
end
def subject
@subject || (@subject = @creation_block.call)
end
end
こうすることで Proxyがかなり洗練されました。持っている機能は、セキュリティ機能とサブジェクトへの橋渡しです。
実行は、ブロックを渡すだけです
account = VirtualAccountProxy.new { BankAccount.new(100) }
requested_account = account.subject # => BankAccountオブジェクト
requested_account.deposit(1000)
requested_account.withdraw(500)
Rubyらしくしてみましょう!
お待たせしました。Ruby感溢れるコードにしていきましょう。
ここではObjectクラスで定義されているmethod_missing
メソッドを使います。
まずはお試しコードから...
class TestMethodMissing
def hello
puts ("hello from original method")
end
def method_missing(name, *args)
puts ("Warning! unknown method error: #{name}")
puts ("Arguments: #{args.join(' ')}")
end
end
tmm = TestMethodMissing.new
tmm.hello # => "hello from original method"
tmm.good_by(10000) # => warning, unknown method error: good_by Arguments: 10000
method_missing
はObjectクラスで定義されているものですが、こういった形でオーバーライドできます。
Rubyのクラス継承ツリーから行くと、TestMethodMissingも親であるObjectの子クラスなので、結果としてmethod_missing
をオーバーライドしていることになります。
逆に、メソッドを呼び出して、その名前のメソッドが見つからなければ常にObjectクラスのmethod_missing
が呼び出されることになります。
class AccountProtectionProxy
def initialize(real_account, owner_name)
@subject = real_account
@owner_name = owner_name
end
def method_missing(name, *args)
check_access
@subject.send(name, *args)
end
def check_access
if Etc.getlogin != @owner_name
raise "Illegal access: #{Etc.getlogin} cannot access account"
end
end
end
これで、当初求めていた防御Proxyの完全な実装になります。
method_missing
使うことで、セキュリティ認証機能付きの、どんなメソッドでも受け付けるProtectionProxyが完成しました!
実行時に何が起きているか?
AccountProtectionProxy
にないメソッドが実行されれば、
check_access
subject.send(name, *args)
の順にメソッドが実行され、2でメソッドが見つからなければ本来のObjectクラスのmethod_missing
が実行されます。
なので、このProxyは事実上どんなメソッド受け入れることができます。
まとめ
Proxyパターンで解決できる3つのこと
- オブジェクトへのアクセス制御
- 場所に依存しないオブジェクトの取得法の提供
- オブジェクト生成の遅延
うまく使えば、非常に柔軟な設計ができそうですね!