Observerパターンの事例
クラス間の結合度は弱いにこしたことはない
TestRunnerクラスとTestResultクラスを下の実装例2-Aのように実装しました。実装した内容を理解するために、処理の流れを追っていきます。
(1)TestRunnerのインスタンスを渡してTestResultを生成する
(2)対象となるすべてのテストケースを実行する
(3)TestResultのインスタンスを渡してテストを実施する
(4)TestRunnerにテスト開始を通知する
(5)テストを開始する
(6)TestRunnerにテストの失敗、またはエラーが発生した場合、通知する
(7)TestRunnerにテスト完了を通知する
※テストケースの件数分、(3)~(7)を繰り返す
【実装例2-A】
public class TestRunner {
// テストケースの配列
private Test[] tests = { ... };
private TestResult result;
public static void main(String[] args) {
TestRunner testRunner = new TestRunner();
testRunner.start();
}
private void start() {
result = new TestResult(this); ……(1)
for (int i = 0; i
tests[i].run(result); ……(3)
}
}
public void startTest(Test test) {
// テストの進捗バーを1つ進め、テストケースの名前を表示する
}
public void endTest(Test test) {
// 今回は何もしない
}
public void addFailure(Test test, AssertionFailedError f) {
// 失敗の原因を表示する
}
public void addError(Test test, Throwable t) {
// エラーの原因を表示する
}
}
public class TestResult {
private TestRunner testRunner;
protected Vector failures = new Vector();
protected Vector errors = new Vector();
protected int runTests = 0;
public int failureCount() {
return failures.size();
}
public Enumeration failures() {
return failures.elements();
}
public int errorCount() {
return errors.size();
}
public Enumeration errors() {
return errors.elements();
}
public int runCount() {
return runTests;
}
public TestResult(TestRunner testRunner) {
this.testRunner = testRunner;
}
public void run(final TestCase test) {
startTest(test); ……(4)
try {
test.runBare(); ……(5)
} catch (AssertionFailedError f) {
addFailure(test, f); ……(6)
} catch (Throwable t) {
addError(test, t); ……(6)
}
endTest(test); ……(7)
}
protected void startTest(Test test) {
runTests++;
testRunner.startTest(test); ……(4)
}
protected void addFailure(Test test, AssertionFailedError f) {
failures.addElement(new TestFailure(test, f));
testRunner.addFailure(test, f); ……(6)
}
protected void addError(Test test, Throwable t) {
errors.addElement(new TestFailure(test, t));
testRunner.addError(test, t); ……(6)
}
protected void endTest(Test test) {
testRunner.endTest(test); ……(7)
}
}
実装例2-Aの処理の流れを見てわかるように、一見この実装に問題はありません。しかし、この実装ではTestRunnerクラスとTestResultクラスがお互いのメソッドを呼び合っているため、図2のBeforeのようにクラス間の依存関係が強くなっています。
このままでは、別のTestRunnerクラスに切り替えようとした場合、TestResultクラスにも修正を加える必要が出てきます。
汎用的な通知の仕組みを実現する
現状では、TestResultクラスから見て、通知する相手はTestRunnerクラス1つです。たいていの場合、通知する相手は1つですが、最近のアプリケーションのように見せたい情報をタブで切り替えてみせるような要求がないとも限りません。今回は、そのような要求にも対応できるようにTestRunnerクラスとTestResultクラスの関係にObserverパターンを適用します。そうすることで、複数の相手へ同時に通知できる汎用的な仕組みを実現することができます。
図2のAfterがObserverパターン適用後のクラス図になります。通知に関するメソッドをインターフェースとして抽出しています。
■デザインパターンの適用
Observerパターンを適用する場合、次のような実装を行うことになります。
・Observer役とSubject役のクラスを抽出する
→今回はTestRunnerクラスがObserver役、TestResultクラスがSubject役。
・Observer役としてSubject役から通知してほしいインターフェースを抽出する
→今回はTestListenerインターフェース。
・Observer役にSubject役から通知を受けた際の処理を実装する
→今回はTestRunnerクラスにTestListenerインターフェースを実装する。
・Subject役はObserver役を自分に追加や削除するためのインターフェースを抽出する
→今回はインターフェースを抽出せず、TestResultクラスにすべて実装する。
・Subject役は保持しているすべてのObserver役に対して通知を行うように実装する
【実装例2-B】
// Observer役
public interface TestListener {
// テスト開始時の通知
public void startTest(Test test);
// テスト完了時の通知
public void endTest(Test test);
// 失時敗の通知
public void addFailure(Test test, AssertionFailedError f);
// エラー発生時の通知
public void addError(Test test, Throwable t);
}
// ConcreteObserver役
public class TestRunner implements TestListener {
// テストケースの配列
private Test[] tests = { ... };
private TestResult result;
public static void main(String[] args) {
TestRunner testRunner = new TestRunner();
testRunner.start();
}
private void start() {
result = new TestResult();
result.addListener(this); ……(1)
for (int i = 0; i
tests[i].run(result);
}
}
public void startTest(Test test) {
// テストの進捗バーを1つ進め、テストケースの名前を表示する
}
public void endTest(Test test) {
// 今回は何もしない
}
public void addFailure(Test test, AssertionFailedError f) {
// 失敗の原因を表示する
}
public void addError(Test test, Throwable t) {
// エラーの原因を表示する
}
}
// Subject役
public class TestResult {
protected Vector failures = new Vector();
protected Vector errors = new Vector();
protected Vector listeners = new Vector();
protected int runTests = 0;
public int failureCount() {
return failures.size();
}
public Enumeration failures() {
return failures.elements();
}
public int errorCount() {
return errors.size();
}
public Enumeration errors() {
return errors.elements();
}
public int runCount() {
return runTests;
}
public void addListener(TestListener listener) { ……(1)
listeners.addElement(listener);
}
public void removeListener(TestListener listener) { ……(1)
listeners.removeElement(listener);
}
public void run(final TestCase test) {
startTest(test); ……(2)
try {
test.runBare();
} catch (AssertionFailedError e) {
addFailure(test, e); ……(2)
} catch (Throwable t) {
addError(test, t); ……(2)
}
endTest(test); ……(2)
}
protected void startTest(Test test) {
runTests++;
for (Enumeration e= listeners.elements(); e.hasMoreElements(); ) {
((TestListener)e.nextElement()).startTest(test); ……(2)
}
}
protected void addFailure(Test test, AssertionFailedError f) {
failures.addElement(new TestFailure(test, f));
for (Enumeration e= listeners.elements(); e.hasMoreElements(); ) {
((TestListener)e.nextElement()).addFailure(test, f); ……(2)
}
}
protected void addError(Test test, Throwable t) {
errors.addElement(new TestFailure(test, t));
for (Enumeration e= listeners.elements(); e.hasMoreElements(); ) {
((TestListener)e.nextElement()).addError(test, t); ……(2)
}
}
protected void endTest(Test test) {
for (Enumeration e= listeners.elements(); e.hasMoreElements(); ) {
((TestListener)e.nextElement()).endTest(test); ……(2)
}
}
}
■デザインパターンの効果
TestRunnerクラスとTestResultクラスの関係にObserverパターンを適用したことで、次の効果を得ることができました。
(1)Observer役のTestRunnerクラスの追加、削除が容易になる
利用例:
TestResult result = new TestResult();
result.addListener( ... ); // Observer役の追加
result.removeListener( ... ); // Observer役の削除
(2)Subject役であるTestResultクラスの状態の変化を複数のObserver役に対してリアルタイムに通知できる
今回は、次の通知を行いました。
・テスト開始時
・テスト完了時
・失敗時
・エラー発生時
また、TestListenerインターフェースを利用したことで、TestRunnerクラス、TestResultクラスのクラス間の結合度を弱くできました。その結果、それぞれの再利用がしやすくなりました。