Ruby: Proxyパターンの解説(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

パターン名は英語で表記します。

Ruby: Proxyパターンの解説(翻訳)

本記事では、Proxyパターンとその種類について学びます。パターンの種類ごとにRubyでの実装を行います。

Proxyパターンの目的

最初にProxyパターンの目的を押さえておきましょう。「Design Patterns: Elements of Reusable Object-Oriented Software」という書籍ではProxyを次のように説明しています。

別のオブジェクトの代理(サロゲート: surrogate)やプレースホルダを提供することで、オブジェクトへのアクセスを制御する

ProxyパターンはSurrogateとも呼ばれています。

同書で使われている例は私のお気に入りです。

あるドキュメントエディタに、グラフィックオブジェクトをドキュメントに埋め込む機能があるとします。画像によってはオブジェクトの作成にコストがかかるものがあります(巨大なラスタ画像など)が、ドキュメントは素早く開けられなければなりません。そのため、ドキュメントを開くときにはコストのかかるオブジェクトをすべて作成するのは避ける必要があります。(中略)解決方法は、実際の画像の代役として振る舞う画像プロキシのような別のオブジェクトを作成することです。このプロキシは画像のように振る舞い、必要に応じてインスタンス化の面倒を見ます。

依存関係は次のような感じになります。

Ruby - image proxy

TextDocumentは最初にImageProxyを用いて何らかのプレースホルダを表示し、ImageProxyは必要に応じて実際のImageを読み込みます。

適用範囲

プロキシは、オブジェクトへの参照をもっとスマートにする必要がある場合に常に有用です。次の応用方法があります。

  1. 仮想プロキシ(virtual proxy)
  2. 保護プロキシ(protection proxy)
  3. リモートプロキシ(remote proxy)
  4. スマート参照(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

メソッドはloadrenderの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を使えばドキュメントの読み込みが一時停止しなくなります。LazyLoadImagerender呼び出しが行われるまで画像を読み込まないからです。

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

この例では、プロキシと元のクラスのインターフェイスが異なっていることは私も認めざるを得ません。Foldercreatedeleteのパラメータは1つしかありませんが、FolderProxyuserもパラメータに取っています。これが保護プロキシの実装として最善かどうか私もわかりません。もっとよい例がありましたらぜひコメントでお知らせください ;)

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の記事をお読みいただくことを強くおすすめいたします。

お読みいただきありがとうございました。

関連記事

Ruby: Chain of Responsibilityパターンの解説(翻訳)

Railsで重要なパターンpart 2: Query Object(翻訳)

Railsで重要なパターンpart 1: Service Object(翻訳)

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の監修および半分程度を翻訳、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れて更新翻訳中。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好きで、Goで書かれたRubyライクなGoby言語のメンテナーでもある。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

夏のTechRachoフェア2019

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ