Observerパターンの事例

2009年5月8日(金)
中川 三千雄

デザインパターンでランチャーを作成する

 前回は、Template Methodパターンの事例を紹介しました。抽象クラスと具象クラスの継承関係をうまく活用したパターンでしたね。日ごろからよく使う基本的なテクニックなので、マスターしておきたいパターンの1つです。

 今回は前回と同様に、JUnit3.8.1(http://www.junit.org/)を事例に、Observerパターンを取り上げます。ObserverパターンはGoFのデザインパターンの1つで、振る舞いに分類されるパターンです。Observerとは「観察者」という意味です。観察対象になる被験者(Subject)の知りたい状態の変化を把握するのに有効なパターンです。ただし、観察といっても観察者が被験者を常に観察しているわけではありません。その実態は、被験者の状態が変化したときに観察者はその変化の通知を受けることで把握しています。

 JUnitでは、ユニットテストを実施するランチャーが用意されています。そのランチャーでは、実施状況を把握するのにObserverパターンが利用されています。観察者はBaseTestRunnerクラスから派生したTestRunnerクラス、被験者はTestResultクラスになります。JUnitでは、この2つのクラスの協調関係によって、ユニットテストの実施状況を把握しています。

 今回はこの2つのクラスに焦点を当てて、どのようにObserverパターンが適用されているのか、どのような効果があるのかを分析して理解を深めます。

必要なランチャーを考える

 前回はテスティングフレームワークが存在しなかったころのテスト手法の説明をしました。そのテスト手法では、テスト実施に関して次のような問題を抱えていました。

 ・実施したテストケースの数がわからない
 ・成功/失敗のときのステータスの表現が統一されていない
 ・実施結果から問題個所の特定が難しい

 今回はこれらの問題を解決するランチャーを作成します。


■実施結果のステータスは3つある
 テストケースには、テスト対象が仕様どおりに実装されていることを確認するためのコードを実装します。仕様どおりの場合は成功、そうでない場合は失敗と判断します。また、仕様の確認とは関係のない処理(テスト前、テスト後の処理)のバグでテストが中断するような場合はエラーとします。

 よって、テストケースの実装が正しい場合、実施結果のステータスは次の3つになります。
 ・成 功……仕様どおりである
 ・失 敗……仕様どおりでない(テスト対象のバグ)
 ・エラー……想定外のエラーが発生した(テストケースのバグ)


■テストの実施状況を把握する必要がある
 テストの実施状況を把握するためには、テストの進み具合や失敗/エラーの原因を知る必要があります。テストケースごとにテストの開始と終了のタイミングを知ることができればテストの進み具合を把握できます。また、失敗やエラーが発生したタイミングを知ることができれば、それらの原因を残すことができます。

 次のタイミングで必要な情報の収集や表示情報の更新ができるような仕組みを用意します。

 <タイミング>     <処理内容>
 テスト開始時   →  テストケースの件数の更新
 テスト終了時   →  今回は何もしない
 失敗時      →  失敗の原因の保存と件数の更新
 エラー発生時   →  エラーの原因の保存と件数の更新


■こんなランチャーを作成する
 まとめると、次のような責務を持ったクラスが必要になります。今回は、この2つのクラスを用いてランチャーを作成します。

●TestRunnerクラス
 ・ユニットテストの開始指示を行う
 ・ユニットテストの実施状況を把握し、表示情報を更新する

●TestResultクラス
 ・ユニットテストの実施結果を収集する
 ・テストケースごとに次のタイミングで表示情報の更新ができるような仕組みを用意する
  テスト開始時
  テスト終了時
  失敗時
  エラー発生時

クラス間の結合度は弱いにこしたことはない

 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.length; i++) {          ……(2)
      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.length; 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クラスのクラス間の結合度を弱くできました。その結果、それぞれの再利用がしやすくなりました。

JUnitのTestRunnerクラスとTestResultクラスに関する補足

 今回の事例の中心は、TestRunnerクラスとTestResultクラスでした。Observerパターンに関係する部分に焦点を当てて説明しました。そのため、関係のない部分の説明は割愛しています。実際は図3-1を見てわかるように、もう少し複雑な構成で実装されています。

 TestRunnerクラスは、標準でCUI版とGUI版が用意されています。それぞれBaseTestRunnerクラスのサブクラスであり、BaseTestRunnerクラスはTestListenerインターフェースを実装しています。TestListenerインターフェースはObserverパターンのObserver役になり、BaseTestRunnerクラスのサブクラスはConcreteObserver役になります。一方、TestResultクラスは、ObserverパターンのSubject役であり、ConcreteSubject役でもあります。

 Observerパターンを適用したことで、JUnitの実装に影響を及ぼすことなく、利用環境に応じてCUI版やGUI版のテストランナーやEclipseのJUnitのビューへ、切り替えが簡単にできるのです。また、TesRunnerクラスは、TestResultクラスからユニットテストの実施状況をリアルタイムに通知を受けることができます。それによってTestRunnerクラスは、プログレスバーでテストの実施状況を表示できるのです。

データ(ロジック)とビューの分離

 今回紹介したObserverパターンは、Subject役が複数のObserver役に対してリアルタイムに通知を行うものでした。Observeの本来の意味は「観察」ですが、実際はSubject役より状態変化の通知を受けることで把握しています。Subject役の通知先が固定に決められないときやクラス間の結合度を弱くしたいときに効果を発揮します。Observerパターンは、クラス間のやりとりが一見複雑に見えますが、メソッドの意味や流れを1つ1つ確認していくことで理解できるはずです。

 このほかにObserverパターンは、MVCパターン(Model/View/Controller)を用いたプレゼンテーション・フレームワークでもよく利用されています。MVCパターンは、データやロジックなど表示形式にとらわれないModelと、Modelの持つ情報をどのような形式で表示するか決定するViewにわけて設計/実装を進めていきます。ModelはViewに依存するような実装をしてはいけません。またModelから見てViewは1つ以上存在します。ObserverパターンのSubject役とObserver役に関係が似ていますね。ちなみにMVCパターンは、Observerパターンのような小さなデザインパターンをいくつか組み合わせて実現しているパターンのため、粒度的にもデザインパターンというよりはソフトウエア・アーキテクチャに近いものです。

 次回は、Factory MethodパターンとAdapterパターンの組み合わせの事例を紹介します。それでは、次回をお楽しみに!

【参考文献】
Erich Gamma, Rechard Helm, Ralph Jonson, John Vlissides『オブジェクト指向における再利用のためのデザインパターン』ソフトバンククリエイティブ(発行年:1999)
結城 浩『Java言語で学ぶデザインパターン入門』ソフトバンクパブリッシング(発行年:2001)
Vincent Massol,Ted Husted『JUnitインアクション』ソフトバンクパブリッシング(発行年:2004)
「JUnit.org」(http://www.junit.org/)(アクセス:2009/04)

株式会社オージス総研
株式会社オージス総研アドバンストモデリングソリューション部アーキテクトチーム兼エンタープライズオープンソースセンター所属。これまでに2社の独立系SI企業で経験を積み、現在に至る。オージス総研では、フレームワークの設計/開発や開発プロセス支援など、アーキテクトとしてプロジェクトに参画する。また、オージス総研のコミュニティー「オブジェクトの広場」にも参加している。
オブジェクトの広場 http://www.ogis-ri.co.jp/otc/hiroba/

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています