読者です 読者をやめる 読者になる 読者になる

Murayama blog.

プログラミングと、その次の話

Proxy

Rubyによるデザインパターン生活。続けます。
本日はProxyパターンを取り上げます。


Rubyによるデザインパターン

Rubyによるデザインパターン


Proxyパターンでは、呼び出し対象となるオブジェクトに対して、
同じインタフェースを持つ代理オブジェクトを用意します。
対象となるオブジェクトのメソッドをそのまま呼び出すのではなく、
代理オブジェクトを通じて、対象となるオブジェクトのメソッドを操作します。


Proxyパターンを用いることで、いわゆる「関心事の分離」を実現することができます。
関心事の分離とは、クラス(オブジェクト)の持つ本来の責務とは質の異なる要件(例えばセキュリティ要件やトランザクション管理など)を本来のクラスから切り離して実装することです。
#なんだか、ちょっとゴリ押しでまとめた感があるけど。。


この本では、Proxyパターンの活用方法を以下の3つに分類しています。

  • 防御Proxy
  • リモートProxy
  • 仮想Proxy


まずは防御Proxyから見ていきます。


この章のサンプルでは銀行処理クラス(BankAccount)クラスが登場します。

class BankAccount
  attr_reader :balance
  def initialize(balance)
    @balance = balance
  end
  
  def deposit(amount)
    @balance += amount
  end
  
  def withdraw(amount)
    @balance -= amount
  end
end

BankAccountクラスは残高照会処理(balance)と、入金処理(deposit)、引落処理(withdrow)を持っています。
言い換えると、BankAccountクラスは、銀行処理の基盤となる振る舞い(メソッド)は実装していますが、
利用権限を確認するためのユーザの認証処理といったセキュリティに関する要件は実装していません。


ここで、BankAccountクラスにユーザ認証処理を付加してみようと思います。
BankAccountクラスをそのまま修正してもよいのですが、その分、BankAccountクラスは複雑になってしまいます。
これはBankAccountクラスの本来の責務である銀行処理に加えて、ユーザ認証といった関心事の異なる要件を実装してしまうために発生する問題です。


そこでProxyパターンを用いると既存のBankAccountクラスに変更を加えずにユーザ認証機能を実現することができます。
BankAccountの代理となるBankAccountProxyクラスを作成します。

require "etc"
class BankAccountProxy
  def initialize(real_object, owner_name)
    @real_object = real_object
    @owner_name = owner_name
  end
  
  def balance
    check_access
    @real_object.balance
  end
  
  def deposit(amount)
    check_access
    @real_object.deposit(amount)
  end
  
  def withdraw(amount)
    check_access
    @real_object.withdraw(amount)
  end
  
  def check_access
    if(Etc.getlogin != @owner_name)
      raise "Illegal access: #{@owner_name} cannot access account."
    end
  end
end

BankAccounrProxyクラスは、インスタンス変数にBankAccountオブジェクトを保持します。
また、BankAccountProxyクラスには、depositやwithdrowといったメソッドを定義しています。
depositメソッドやwithdrowメソッドは、
内部でcheck_accessメソッドを呼び出しユーザ認証を行っています。*1
ユーザ認証をパスした場合のみ、BankAccountオブジェクトに処理を委譲することになります。


プログラムの呼び出しもとは次のようになります。

account = BankAccount.new(100)
proxy = BankAccountProxy.new(account, "murayama")
puts proxy.deposit(50)
puts proxy.withdraw(10)

ユーザ認証をパスした場合の実行結果は以下のとおりです。

150
140

一方で、ユーザ認証に失敗した場合はRuntimeErrorが発生します。


以上が、防御Proxyのお話になります。
ユーザ認証を担当するBankAccountProxyクラスを作成することで、
セキュリティ要件を代理クラスに任せることができました。


つづいて、仮想Proxyのお話です。
仕組みはさきほどの防御Proxyとよく似ていますが、その利用目的が異なります。
仮想Proxyでは、複雑なオブジェクトの生成を遅延させることを目的とします。


VirtualAccountProxyは入金処理(deposit)、引落処理(withdrow)、残高照会処理(balance)が呼び出されるまで、
処理の実体となるBankAccountオブジェクトの生成を遅延します。

class VirtualAccountProxy
  def initialize(starting_balance)
    @starting_balance = starting_balance
  end
  
  def balance
    subject.balance
  end
  
  def deposit(amount)
    subject.deposit(amount)
  end
  
  def withdraw(amount)
    subject.withdraw(amount)
  end
  
  def subject
    @subject || (@subject = BankAccount.new(@starting_balance))
  end
end

このVirtualAccountProxyは、subjectメソッドを呼び出すことでBankAccountオブジェクトを生成します。
subjectメソッドはdepositメソッドやwithdrowメソッドが呼び出されたときに実行します。
言い換えると、depositメソッドや、withdrowメソッドが呼び出されるまで、
処理の本体となるBankAccountオブジェクトは生成されないことになります。


以上がProxyパターンのお話です。
#リモートProxyの話はおいときます。
せっかくなのでクラス図も載せておきます。


と、ここまでが一般的なProxyパターンのお話です。


ここからは、さきほどの防御Proxy(BankAccountProxy)を、
Rubyらしいコードに変換してみます。


少し話が変わりますが、Rubyには未定義のメソッド呼び出しが発生した場合に、
method_missingという名前のメソッドが呼び出されます。*2
このmethod_missingメソッドを利用することで、
さきほどのBankAccountProxyクラスを次のように実装することができます。

require "etc"
class BankAccountProxy
  def initialize(real_object, owner_name)
    @real_object = real_object
    @owner_name = owner_name
  end
  
  def method_missing(name, *args)
    check_access
    @real_object.send(name, *args)
  end
  
  def check_access
    if(Etc.getlogin != @owner_name)
      raise "Illegal access: #{@owner_name} cannot access account."
    end
  end
end

depositメソッドや、withdrowメソッドを定義する必要がなくなったのでシンプルになりました。
method_missingメソッドの内部でsendメソッドを使用することで、本体となるオブジェクトのメソッドを呼び出しています。
また、method_missingメソッドの第2匹数*argsは不定個の引数を配列に格納します。


以上、Proxyパターンの勉強でした。おしまい。

*1:RubyのEtcモジュールが便利だということを知りました。

*2:これも知らなかった。