15
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-07-31

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. オブジェクト生成の遅延

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

15
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?