酒と泪とRubyとRailsと

Ruby on Rails と Objective-C は酒の肴です!

プロキシ Ruby 2.0.0 デザインパターン速攻習得[Proxy][Design Pattern]

GoFのデザインパターン(Design Pattern)のプロキシ(Proxy)をRubyのサンプルコードで紹介します。

プロクシパターンは1つのオブジェクトに複数の関心事がある場合にそれを分離するために用います。例えば、オブジェクトの本質的な目的とは異なる「セキュリティ要件やトランザクション管理など」を切り離して実装することができます。

サッカーが専門の「サッカー選手」と、チームとの交渉や契約が専門の「代理人」のような関係です。


Porxyの構成要素

Proxyの構成要素は次の2つです。

対象オブジェクト(subject):本物のオブジェクト
代理サブジェクト(proxy):特定の「関心事」を担当、それ以外を対象サブジェクトに渡す

Proxyオブジェクトは対象オブジェクトと同じインターフェースを持ちます。利用する際は、Proxyオブジェクトを通して対象となるオブジェクトを操作します。

Proxyの3つのタイプ

Proxyには次の3つの種類があります。

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

今回は、防御Proxyと仮想Proxyについてサンプルソースで説明していきます。

サンプルソース1:防御Proxy

このサンプルでは銀行の窓口業務(入金/出金)を担当するBankAccountクラスと、ユーザー認証を担当するBankAccountProxyクラスにより「関心事を分離」するProxyデザインパターンのモデルを作ります。

まず銀行の入出金の窓口業務を行うBankAccountクラスを作成します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 銀行の入出金業務を行う(対象オブジェクト/subject)
class BankAccount
  attr_reader :balance

  def initialize(balance)
    @balance = balance
  end

  # 出金
  def deposit(amount)
    @balance += amount
  end

  # 入金
  def withdraw(amount)
    @balance -= amount
  end
end

次に銀行の入出金業務とは関心事の異なるユーザー認証を担当する防御ProxyとしてBankAccountProxyクラスを作ります。このクラスはBankAccountクラスと同じインターフェイスを持っており、利用する側はProxyを介して入出金を行います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# etcはRubyの標準ライブラリで、/etc に存在するデータベースから情報を得る
# この場合は、ログインユーザー名を取得するために使う
require "etc"

# ユーザーログインを担当する防御Proxy
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

コーディングは以上で、こちらを実際に動かしてみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# ログインユーザーの場合
account = BankAccount.new(100)
# login_userの部分はこの処理を行うMac/Linuxのログイン中のユーザー名に書き換えて下さい
proxy = BankAccountProxy.new(account, "login_user")
puts proxy.deposit(50)
#=> 150
puts proxy.withdraw(10)
#=> 140

# ログインユーザーではない場合
account = BankAccount.new(100)
proxy = BankAccountProxy.new(account, "no_login_user")
puts proxy.deposit(50)
#`check_access': Illegal access: no_login_user cannot access account. (RuntimeError)

このようにユーザー認証という「特定の関心事」を代理オブジェクトに分離させることができました。

仮想Proxy

次に仮想Proxyのサンプルです。今回は、先ほどの入出金を行うBankAccountクラスと、BankAccountのインスタンス生成を遅らせるためのVirtualAccountProxyクラスを作成します。インスタンスの生成を遅らせる理由はここではシステム全体の性能向上と仮定します。

まず先程と同じ入出金業務のBankAccountクラスです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 銀行の入出金業務を行う(対象オブジェクト/subject)
class BankAccount
  attr_reader :balance

  def initialize(balance)
    puts "BankAccountを生成しました"
    @balance = balance
  end

  def deposit(amount)
    @balance += amount
  end

  def withdraw(amount)
    @balance -= amount
  end
end

次にシステム全体の性能向上を目的としてBankAccountクラスの生成を遅らせる仮想Proxyとして、VirtualAccountProxyクラスを作成します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# BankAccountの生成を遅らせる仮想Proxy
class VirtualAccountProxy
  def initialize(starting_balance)
    puts "VirtualAccountPoxyを生成しました。BankAccountはまだ生成していません。"
    @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 announce
    "Virtual Account Proxyが担当するアナウンスです"
  end

  def subject
    @subject || (@subject = BankAccount.new(@starting_balance))
  end
end

コーディング以上となります。ではこのサンプルを動かしてみます。

1
2
3
4
5
6
7
8
9
10
11
12
proxy = VirtualAccountProxy.new(100)
#=> VirtualAccountPoxyを生成しました。BankAccountはまだ生成していません。

puts proxy.announce
#=> Virtual Account Proxyが担当するアナウンスです

puts proxy.deposit(50)
#=> BankAccountを生成しました
#=> 150

puts proxy.withdraw(10)
#=> 140

結果のようにdepositメソッドを実行するまで、BankAccountクラスが生成されないようになりました。この例では、VirtualAccountProxyが「BankAccountインスタンスの生成タイミング」という関心事を分離しています。

Tips:method_missingによる委譲

ここでRubyの標準機能の一つであるmethod_missingを使ったProxyを紹介します。

Rubyでは未定義のメソッドが呼び出された場合にmethod_missingが呼び出されます。これを利用することで先程の防御プロクシのBankAccountProxyクラスを以下のように短くすることができます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# ユーザーログインを担当する防御Proxy
class BankAccountProxy
  def initialize(real_object, owner_name)
    @real_object = real_object
    @owner_name = owner_name
  end

  # Rubyでは未定義のメソッド呼び出しが発生 => method_missingが呼び出される
  # method_missingを用いることでdeposit, withdrawを省略
  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

BankAccountProxyクラスに存在しない#depositメソッド#withdrawが呼び出された場合、#method_missingが呼ばれます。そして、#method_missing内で@real_objectに格納したオブジェクトの同名のメソッドが呼び出されます。

この方法はForwardableと同じ「委譲」のやり方の一つです。

ただし、method_missingの利用には次のようなデメリットもあります。このデメリットをしっかり理解した上で、利用することが大切です。

ソースコードが追いづらくなる
マシンパワーを消費する

このサンプルソースはGitHubにも置いています。

サンプルソース(GitHub)

Special Thanks

Proxy

Proxy パターン - プロキシパターン

21.Proxyパターン

第9回 Compositeパターン/Proxyパターン

Amazon.co.jp: Rubyによるデザインパターン: Russ Olsen, ラス・オルセン, 小林 健一, 菅野 裕, 吉野 雅人, 山岸 夢人, 小島 努: 本

変更来歴

12/10 09:00 GitHubへのサンプルソースの設置。導入文の修正
12/11 00:00 書籍へのリンクをAmazon アフィリエイトに変更
12/12 22:50 ソースコードに説明を追加
06/21 11:40 Ruby2.0.0対応、読みづらい部分を修正

おすすめの書籍