Edited at

Rubyデザインパターン 7日目 : Proxy

More than 3 years have passed since last update.

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)

スクリーンショット 2015-07-27 11.25.28.png


 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_accountowner_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にまだ中身がないときだけです。

@subjecttrueであれば右辺は評価されません。

  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にないメソッドが実行されれば、


  1. check_access

  2. subject.send(name, *args)

の順にメソッドが実行され、2でメソッドが見つからなければ本来のObjectクラスのmethod_missingが実行されます。

なので、このProxyは事実上どんなメソッド受け入れることができます。


 まとめ


 Proxyパターンで解決できる3つのこと


  1. オブジェクトへのアクセス制御

  2. 場所に依存しないオブジェクトの取得法の提供

  3. オブジェクト生成の遅延

うまく使えば、非常に柔軟な設計ができそうですね!