概要
「Rubyによるデザインパターン」を読んでデザインパターンを勉強中。
Javaをやっていた人間としての目線で情報を整理してみます。
今までに整理したもの
Template Method Pattern
Strategy Pattern
Observer Pattern
Composite Pattern
Command Pattern
Proxy Pattern
- 実クラス(RealSubjet)を内部に持ち、実クラスと同じインターフェースを持ったクラス(Proxy)を作成する
- 基本的に処理は内部に持っている実クラスのオブジェクトに移譲するが、Proxy 側で付加的な処理を行うことができる
- 本質的な処理は RealSubject 側に、付加的な処理は Proxy 側に、というように責務の分離を行う
- Client 側は Subject インターフェースに対するプログラムにすることで、Proxy を通しているかどうか意識する必要がなくなる
基本形
Java の場合
public interface Subject {
public void doSomething();
}
public class RealSubject implements Subject {
public void doSomething() {
System.out.println("RealSubject#doSomething is called.");
}
}
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と同様の書き方。
class Subject
def do_something
puts "Subject.do_something is called."
end
end
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 に定義。
権限を持たないユーザの操作を制限する、というような場合を考える。
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 に定義。
ネットワーク越しに別マシンで処理を行うような場合。
class RemoteProxy
def initialize(subject)
@subject = subject
end
def do_something
# ここで接続処理などを実施し、
@subject.do_something # ネットワーク越しにメソッド実行
end
end
Virtual Proxy
インスタンス生成のオーバーヘッドが大きい(画像ファイルの読み込みとか?)場合、すぐにインスタンスを生成せず、必要になった時点で初期化を行うようにする。
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 クラスにできる。
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 ではあるオブジェクトのメソッドを呼び出したとき(より正確にはオブジェクトに対してメッセージを送ったとき)、概ね次のような順序で実行すべきメソッドが探索されます。
- そのクラスにメソッドが定義されていればそのメソッドを実行
- なければクラスの継承関係を辿っていき、メソッドが定義されていればそれを実行
- 基底クラス(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
が現れない形でクラスが定義できます。
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 を作成して同じようなことができます。
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