LoginSignup
9
9

More than 5 years have passed since last update.

JavaエンジニアがRubyでデザインパターンを学ぶ - Proxy Pattern

Posted at

概要

「Rubyによるデザインパターン」を読んでデザインパターンを勉強中。
Javaをやっていた人間としての目線で情報を整理してみます。

今までに整理したもの

Template Method Pattern
Strategy Pattern
Observer Pattern
Composite Pattern
Command Pattern

Proxy Pattern

png.png

  • 実クラス(RealSubjet)を内部に持ち、実クラスと同じインターフェースを持ったクラス(Proxy)を作成する
  • 基本的に処理は内部に持っている実クラスのオブジェクトに移譲するが、Proxy 側で付加的な処理を行うことができる
  • 本質的な処理は RealSubject 側に、付加的な処理は Proxy 側に、というように責務の分離を行う
  • Client 側は Subject インターフェースに対するプログラムにすることで、Proxy を通しているかどうか意識する必要がなくなる

基本形

Java の場合

Subject.java
public interface Subject {
  public void doSomething();
}
RealSubject.java
public class RealSubject implements Subject {
  public void doSomething() {
    System.out.println("RealSubject#doSomething is called.");
  }
}
Proxy.java
public class Proxy implements Subject {
  private Subject subject;

  public Proxy(Subject subject) {
    this.subject = subject;
  }

  public void doSomething() {
    subject.doSomething();
  }
}
実行
public class Main {
  public static void main(String[] args) {
    Subject subject = new Proxy(new RealSubject()); // Proxy も Subject インターフェースを実装しているので
    subject.doSomething();                              // メソッド呼び出し時に Proxy を通しているのかどうか意識しなくて済む
  }
}

Ruby の場合

interface は存在しないので Subject をクラスとして定義する以外はJavaと同様の書き方。

Subject
class Subject
  def do_something
    puts "Subject.do_something is called."
  end
end
Proxy
class Proxy
  def initialize(subject)
    @subject = subject
  end

  def do_something
    @subject.do_something
  end
end
実行
subject = Proxy.new(Subject.new)
subject.do_something

適用例

Protection Proxy

権限チェック処理を Subject クラスから独立させて Proxy に定義。
権限を持たないユーザの操作を制限する、というような場合を考える。

ProtectionProxy
class ProtectionProxy
  def initialize(subject, user)
    @subject = subject
    @user = user
  end

  def do_something
    check_auth
    @subject.do_something
  end

  # 単純に文字列比較してますが、DBから権限情報を取得して認証する、みたいなことを想定
  def check_auth
    raise "NOT AUTHORIZED!!!" if @user != 'admin' 
  end
end

Remote Proxy

接続・通信に関する処理を Subject クラスから独立させて Proxy に定義。
ネットワーク越しに別マシンで処理を行うような場合。

RemoteProxy
class RemoteProxy
  def initialize(subject)
    @subject = subject
  end

  def do_something
    # ここで接続処理などを実施し、
    @subject.do_something # ネットワーク越しにメソッド実行
  end
end

Virtual Proxy

インスタンス生成のオーバーヘッドが大きい(画像ファイルの読み込みとか?)場合、すぐにインスタンスを生成せず、必要になった時点で初期化を行うようにする。

VirtualProxy
class VirtualProxy
  def do_something
    s = subject
    s.do_something
  end

  def subject
    # 初期化されていない(@subject = nil)ならばインスタンス生成
    @subject || Subject.new
  end
end

Subject.new を Proxy クラス内に書くのではなく、ブロックとして渡すようにすれば Subject クラスに依存しない Proxy クラスにできる。

VirtualProxy(改良版)
class VirtualProxy
  def initialize(&create_block)
    @create_block = create_block
  end

  def do_something
    s = subject
    s.do_something
  end

  def subject
    @create_block.call
  end
end


# 実行時は次のように
subject = VirtualProxy.new { Subject.new }
subject.do_something

# この場合は VirtualProxy クラスが Subject クラスに依存しないので
# 初期化処理をブロックで渡せばどんなクラスでも遅延初期化が可能になる
subject = VirtualProxy.new { Hoge.new }

より Ruby らしく(method_missing を使う)

基本的なつくりは次のようになりますが、これを Ruby らしいやり方でシンプルに記述できます。

class Proxy
  def initialize(subject)
    @subject = subject
  end

  def method001
    @subject.method001
  end

  def method002
    @subject.method002
  end

  def method003
    @subject.method003
  end
end

Ruby ではあるオブジェクトのメソッドを呼び出したとき(より正確にはオブジェクトに対してメッセージを送ったとき)、概ね次のような順序で実行すべきメソッドが探索されます。

  1. そのクラスにメソッドが定義されていればそのメソッドを実行
  2. なければクラスの継承関係を辿っていき、メソッドが定義されていればそれを実行
  3. 基底クラス(BasicObject ?)まで辿っても見つからなければ再び子クラスから method_missing メソッドを探索

…ということは、以下のようにすれば先ほどのコードと同じことが実現できます。

class Proxy
  def initialize(subject)
    @subject = subject
  end

  def method_missing(name, *args) # 引数は(メソッド名シンボル, 引数リスト)
    @subject.send(name, *args)
  end
end

これを先ほどの ProtectionProxy に適用すると
次のように、do_something が現れない形でクラスが定義できます。

ProtectionProxy(改良版)
class ProtectionProxy
  def initialize(subject, user)
    @subject = subject
    @user = user
  end

  def method_missing(name, *args)
    check_auth
    @subject.send(name, *args)
  end

  def check_auth
    raise "NOT AUTHORIZED!!!" if @user != 'admin' 
  end
end

こうしておけば Subject クラスにどれだけメソッドが増えようが Proxy 側に影響がありません。さらに、この ProtectionProxy クラスは Subject クラスとは完全に独立しているので、どのようなオブジェクトに対しても利用可能です。
例えば次のように String に権限チェックを行うことも可能になります。

str = ProtectionProxy.new('This string is protected!', 'John Doe')
str.upcase # 例外発生(NOT AUTHORIZED!!! (RuntimeError))

method_missing を使う場合の注意

  • 暗黙的に継承されているメソッド(to_s など)は @subject にメッセージが送られずに親クラスのメソッド(to_s ならば Object#to_s )が実行されてしまう実行されてしまう
  • メソッドを探索する分パフォーマンスが落ちる
  • コードの可読性が落ちる

ついでに Java の場合

java.lang.reflect.Proxy を使えば動的に Proxy を作成して同じようなことができます。

Main.java
import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

class LoggingHandler implements InvocationHandler {
  private Object subject;

  public LoggingHandler(Object subject) {
    this.subject = subject;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    System.out.println("LOG: " + method.getName() + " is called.");
    return method.invoke(subject, args); // ここで RealSubject のメソッドを実行
  }
}

public class Main {
  public static void main(String[] args) {
    Subject subject = (Subject) Proxy.newProxyInstance(Subject.class.getClassLoader(),
                                                       new Class<?>[] { Subject.class },
                                                       new LoggingHandler(new RealSubject()));
    subject.doSomething();
  }
}

参考

Olsen, R. 2007. Design Patterns in Ruby

9
9
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
9
9