概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Proxy Pattern
- 原文公開日: 2017/11/10
- 著者: Sergii Makagon -- Ruby/Rails開発者であり、RuboCopのcontributorでもあります。
パターン名は英語で表記します。
Ruby: Proxyパターンの解説(翻訳)
本記事では、Proxyパターンとその種類について学びます。パターンの種類ごとにRubyでの実装を行います。
Proxyパターンの目的
最初にProxyパターンの目的を押さえておきましょう。「Design Patterns: Elements of Reusable Object-Oriented Software」という書籍ではProxyを次のように説明しています。
別のオブジェクトの代理(サロゲート: surrogate)やプレースホルダを提供することで、オブジェクトへのアクセスを制御する
ProxyパターンはSurrogateとも呼ばれています。
同書で使われている例は私のお気に入りです。
あるドキュメントエディタに、グラフィックオブジェクトをドキュメントに埋め込む機能があるとします。画像によってはオブジェクトの作成にコストがかかるものがあります(巨大なラスタ画像など)が、ドキュメントは素早く開けられなければなりません。そのため、ドキュメントを開くときにはコストのかかるオブジェクトをすべて作成するのは避ける必要があります。(中略)解決方法は、実際の画像の代役として振る舞う画像プロキシのような別のオブジェクトを作成することです。このプロキシは画像のように振る舞い、必要に応じてインスタンス化の面倒を見ます。
依存関係は次のような感じになります。
TextDocument
は最初にImageProxy
を用いて何らかのプレースホルダを表示し、ImageProxy
は必要に応じて実際のImage
を読み込みます。
適用範囲
プロキシは、オブジェクトへの参照をもっとスマートにする必要がある場合に常に有用です。次の応用方法があります。
- 仮想プロキシ(virtual proxy)
- 保護プロキシ(protection proxy)
- リモートプロキシ(remote proxy)
- スマート参照(Smart reference)
1. 仮想プロキシ
コストの高いオブジェクトを必要に応じて作成します
上述のImageProxy
はまさしくこのタイプのプロキシです。Rubyで実装してみましょう。
2つのメソッドを持つTextDocument
クラスがあるとします。
class TextDocument
attr_accessor :elements
def load
elements.each { |el| el.load }
end
def render
elements.each { |el| el.render }
end
end
メソッドはload
とrender
の2つだけです。最初にドキュメントのすべての要素を読み込み、続いて要素をレンダリングしたいとします。
読み込みに非常に時間のかかるImage
要素があるとします。
class Image
def load
# ... 非常に時間がかかる
end
def render
# 読み込んだ画像をレンダリングする
end
end
画像を含むドキュメントを読み込む場合は次のようになります。
document = TextDocument.new
document.elements.push(Image.new)
document.load # => 画像のせいで読み込みが遅い
画像の読み込みに時間がかかるので、ドキュメントの読み込みにも時間がかかります。
そこで、画像の遅延読み込み(lazy loading)を実装する仮想プロキシを作成します。
class LazyLoadImage
attr_reader :image
def initialize(image)
@image = image
end
def load
end
def render
image.load
image.render
end
end
これで、LazyLoadImage
を使えばドキュメントの読み込みが一時停止しなくなります。LazyLoadImage
はrender
呼び出しが行われるまで画像を読み込まないからです。
document = TextDocument.new
image = Image.new
document.elements.push(LazyLoadImage.new(image))
document.load # => 速い
document.render # => 画像読み込み中なので遅い
SimpleDelegatorを使って、Decoratorパターンで行ったのと同様にLazyLoadImage
からImage
への正しい委譲を実装する方法もあります。
2. 保護プロキシ
保護プロキシは、元のオブジェクトへのアクセスを制御します
これはだいたい見ての通りです。元のオブジェクトを呼び出す前に何らかの保護ルールを適用したい場合、保護プロキシでラップできます。
class Folder
def self.create(name)
# フォルダの作成
end
def self.delete(name)
# フォルダの削除
end
end
class FolderProxy
def self.create(user, folder_name)
raise '管理者以外はフォルダを作成できません' unless user.admin?
Folder.create(folder_name)
end
def self.delete(user, folder_name)
raise '管理者以外はフォルダを削除できません' unless user.admin?
Folder.delete(folder_name)
end
end
この例では、プロキシと元のクラスのインターフェイスが異なっていることは私も認めざるを得ません。Folder
のcreate
とdelete
のパラメータは1つしかありませんが、FolderProxy
はuser
もパラメータに取っています。これが保護プロキシの実装として最善かどうか私もわかりません。もっとよい例がありましたらぜひコメントでお知らせください ;)
3. リモートプロキシ
リモートプロキシは、異なるアドレス空間上にあるオブジェクトをローカル環境で代表します
たとえばRPC(リモートプロシージャコール)を使いたいのであれば、RPC呼び出しを扱うプロキシを簡単に作れます。ここではxml-rpc gemを例に取ります。
次のコードでRPC呼び出しを行えます。
require 'xmlrpc/client'
server = XMLRPC::Client.new2("http://myproject/api/user")
result = server.call("user.Find", id)
私たちの代わりにRPC呼び出しを扱うリモートプロキシを作成します。
class UserProxy
def find(id)
server.call("user.Find", id)
end
private
def server
@server ||= XMLRPC::Client.new2("http://myproject/api/user")
end
end
これで、異なるアドレス空間上にあるオブジェクトへのアクセスに使えるプロキシができました。
4. スマート参照
スマート参照は、オブジェクトへのアクセス時に付加的な操作を実行する生のポインタを置き換えるのに使います
利用法の1つは、最初に参照が行われるときに永続オブジェクトをメモリに読み込むことです。
このタイプのプロキシを使うことで、レスポンスのメモ化(memoization)を作成できます。
たとえば、重たい計算を代行するサードパーティ製ツールを使うとします。そうしたサービスでは専用のgemを提供するのが普通です。gemがどのようなものか考えてみましょう。
class HeavyCalculator
def calculate
# しばらく何か計算する
end
end
サードパーティ製gemなので、ここではメモ化を追加できません。かつ、HeavyCalculator
の呼び出しを長時間待ちたくありません。こんなときは、メモ化の追加を代行するスマート参照プロキシを作成します。
class MemoizedHeavyCalculator
def calculate
@result ||= calculator.calculate
end
private
def calculator
@calculator ||= HeavyCalculator.new
end
end
これで、MemoizedHeavyCalculator
を用いてcalculate
を必要なだけ何度でも呼び出せるようになりました。しかも実際の呼び出しは1回だけにとどまり、以後の呼び出しにはメモ化された値が使われます。
プロキシをスマート参照として使う方法を応用すれば、ログ出力を追加することもできます。何らかの見積もり機能を提供するサードパーティ製サービスがあり(当然サービスのコードは変更できません)、サービスの呼び出しごとにログ出力を追加したいとします。これは次のように実装できます。
class ExpensiveService
def get_quote(params)
# ... リクエストを送信
end
end
class ExpensiveServiceWithLog
def get_quote(params)
puts "次のparamsで見積もりを取得しています: #{params}"
service.get_quote(params)
end
private
def service
@service ||= ExpensiveServiceWithLog.new
end
end
ExpensiveService
はサードパーティのコードなので実装は変更できません。しかしExpensiveServiceWithLog
を用いればどんなログ出力でも必要に応じて追加できます。ここでは簡単のためputs
で出力しています。
その他の関連パターン
Proxyパターンのある種の実装はDecoratorパターンの実装と極めて似ていますが、両パターンの目的は異なります。Decoratorパターンはオブジェクトに責務を追加しますが、Proxyパターンはオブジェクトへのアクセスを制御します。
ProxyパターンはAdapterパターンに似ていることもあります。しかしAdapterパターンは適用先のオブジェクトに別のインターフェイスを提供するためのものです。Proxyパターンは、適用先のオブジェクトと同じインターフェイスを提供します。
追伸: プロキシオブジェクトは、元のオブジェクトが持つすべてのメソッドに応答するべきです。上の例ではプロキシクラスの元のオブジェクトが持つメソッドを単に実装しましたが、元のクラスにもっと多くのメソッドがある場合は、プロキシクラスでも同じコードが多数繰り返すことになるかもしれません。
Proxyパターンの実装とDecoratorパターンの実装はほぼ同じなので、プロキシから元のオブジェクトにメソッドを委譲できるSimpleDelegatorの記事をお読みいただくことを強くおすすめいたします。
お読みいただきありがとうございました。