Powered by SmartDoc

MVCモデルの検証

2001年2月1日
竹本 浩
http://www.st.rim.or.jp/~h_take
SmalltalkのMVCモデルが紹介されてから、20年が経過しているが未だにSmalltalk以上の優れた実装を実感したことがなかった。 今回JDKが1.3にバージョンアップしたことを契機に"Object-Oriented Programming"に紹介されている例をjavaで実装してみる。

目次

Last modified: Fri Mar 16 13:49:45 JST 2001

MVCを導入したい理由

毎日短時間しかプログラミングができない私にとって、少しずつ動作を確かめながらプログラムを作りたいという願いがある。小さなプログラムでも動けば励みになるし、動作のチェックもできる。「ソフトウェア作法」でも、プログラムの骨格から作りはじめ、テストを繰り返しながら完成させていくスタイルが紹介されている。しかし、ユーザインタフェースを伴うプログラムではGUI領域のプログラムと問題解決領域のプログラムを切り分けることが難しい。SmalltalkのMVCでは、これをスマートに解決している。

その例題が"Object-Oriented Programming"のカウンタ問題である。

カウンタのイメージを図[カウンタのポンチ図]に示す。(1)

カウンタのポンチ図

一口にカウンタと言っても、いくつかのバリエーションが考えられる。カウンタ問題では、少し強引な感じはあるが次の3種類を例題として挙げている。

  1. Object-Oriented Programming Fig 1-12から引用。

問題領域での段階的プログラミング

段階的にプログラミングすることとオブジェクトの性質を抽出することは同じではないが、OOPにおいてはオブジェクトの所有する本質的な部分を最初に実装し、その後に修飾部分を実装するのが一般的なプログラミングスタイルである。

Javaを使って、このカウンタ問題を解きながら、現在のJavaの環境と開発スタイルについて整理してみる。

COUNTクラスの属性と機能

属性値としては、

が考えられる。

機能は、図[カウンタのポンチ図]から明白であるが、

がある。

このようなカウンタが持つべき概念的な属性や機能はアブストラクト・クラスとして定義する。

節[整数カウンタ(IntegerCount)]は、アブストラクト・クラスの機能を実装したサブクラスとなる。

カウンタクラスの構成を図[カウンタクラス構成]に示す。

Countクラスはアブトラクト・クラスであるためインスタンスを生成するメソッドを持っていない、increment, decrementは下位クラスで実装が必要なアブストラクトメソッドであるため、イタリックで表示されている。IngegerCountがCountのコンクリートクラスであり、ここでincrement, decrementメソッドを定義している。

カウンタクラス構成

整数カウンタのUML図を図[カウンタクラス構成]に示す。(2)

カウンタクラス(Count)

カウンタクラスをjavaで記述したのが、リスト[Count]である。

Count
/**
 * Count.java
 * カウントのアブストラクトクラス
 *
 * Created: Thu Feb 01 22:28:26 2001
 *
 * @author <a href="mailto:take@pwave.sig.or.jp "竹本 浩</a>
 * @version 1.0
 */

public abstract class Count {
	public Object value_;
	static Object resetValue_;

	public abstract void increment();
	public abstract void decrement();
	public void reset() {
		value_ = resetValue_;
	}
}// Count

resetValueは、クラスで1個決まった値を持つものなので、staticでクラス変数として宣言した。

整数カウンタ(IntegerCount)

次に、整数カウンタクラスをjavaで記述する。

IntegerCount
import java.lang.*;
/**
 * IntegerCount.java
 * 整数カウンタ
 *
 * Created: Thu Feb 01 22:36:01 2001
 *
 * @author <a href="mailto:take@pwave.sig.or.jp "竹本 浩</a>
 * @version 1.0
 */

public class IntegerCount extends Count{
	public IntegerCount (){
		resetValue_ = new Integer(0);
		reset();
	}
	public void increment() { // 以下の部分がラップしている部分
		value_ = new Integer(((Integer)value_).intValue() + 1);
	}
	public void decrement() { // 以下の部分がラップしている部分
		value_ = new Integer(((Integer)value_).intValue() - 1);
	}
}// IntegerCount

どうしても汎用的なクラスにするために、整数値を内部データ型ではなく、Integerクラスを使ってラップしたために、加算と減算の手続きに少し無理がある。

value_ = new Integer(((Integer)value_).intValue() + 1);

のようにvluae_を(Integer)にキャストして、intValue()でその整数値を取得し、1足した後、その値を整数値として持つIntergerオブジェクトを生成してvalue_にセットしている。また、リセット値は0と使用した。

動作確認

動作確認の前に私の環境について、簡単に説明する。(3)

できるだけ、マシンとかOSに依存しない環境にしたいと考え、マウスとGUIが中心の時代に、Emacsというキャラクタペースの環境を使用することにした。幸い、Windows98上で動作するEmacsとしてMeadowがあり、Emacs上の開発環境としてJDEがあることをしったのが、このページを書く契機である。JDK, Meadow, JDEは、すべてフリーであり、自由に利用することができる。

JDEには、beanshell(以下bshと記す)というjavaのインタプリタがあり、先ほど作成したIntegerCounterをメインプログラムを作らずに単体で動作確認をすることができる。JDEのサイトを参照

bsh % IntegerCount c = new IntegerCount(); // IntegerCountのインスタンスを生成
bsh % c.increment();     // incrementメソッドで1増やす
bsh % print(c);          // 単にprint(c)ではクラス名とアドレスしか表示されない
IntegerCount@4f1d0d
bsh % print(c.value_);   // そこで、c.value_のようにメンバーまで指定する
1                        // 値が、0から1になっていることを確認
bsh % c.increment();     // 再度、incrementメソッドで1増やす
bsh % print(c.value_);   // print文で値を出力
2                        // 値が、2になっていることを確認
bsh % c.increment();     // この辺のキー入力はほとんどマウス操作のみ
bsh % print(c.value_);
3
bsh % c.reset();         // resetメソッドのテスト
bsh % print(c.value_);   // 値が0になっていることを確認
0

"bsh" %と表示されている行がコマンド入力行で、その後の行が出力行である(説明文を//の後に付けている)。一見キー入力が多く、かえってメインプログラムを書いた方が楽なように見えるが、JDEと密接に連携しているため、クラス名や変数名の補完とEmacsのシェルモードが利用できるので、キー入力はそんなに多くはならない。

16進カウンタ

次に16進カウンタを作ってみる。16進カウンタと言っても整数のカウント値をどう見せるかと言う問題である。increment, decrement, resetのメソッドは全く同じである。そこで、IntegerCountを継承して、16進カウントを作ることにした。16進数で表示するにはtoStringメソッドを更新する。これまで、IntegerのインスタンスをValue_にセットしてきたが、これはIntegerというラッピングクラスによって整数値を文字列に変換するのを汎用化するためである。

HexCount
import java.lang.*;
/**
 * HexCount.java
 * 16進カウンタ
 *
 * Created: Fri Feb 02 12:48:03 2001
 *
 * @author <a href="mailto:take@pwave.sig.or.jp "竹本 浩</a>
 * @version 1.0
 */

public class HexCount extends IntegerCount{
	public HexCount (){
		// 必要な初期化はIntegerCountが行っており、ここではHexCountのコンストラクタ
		// を定義していることを明記するために定義している。
	}
	public String toString() { // 16進表示は、0xA0のように最初に0xをつけた
		return ("0x" + Integer.toString(((Integer)value_).intValue(), 16).toUpperCase());

	}
}// HexCount

では、bshを使ってできたてのHexCountを実行してみる。bshでは、手順をスクリプトとしてまとめて記述したり、独自の関数を定義してスクリプトを構造化することができる。そこで、テスト処理の一つのファイルhexTest.bshというファイルにまとめて実行する。例のように単なる実行文の羅列ではなく、オブジェクトの状態に応じて制御を変えることができるため、複雑なテストをbsh内で実施することができる。

bshスクリプト例
HexCount c = new HexCount();
print(c);
for (i = 0; i < 12; i++) // for文のループ例
	   c.increment();	 // 12回incrementを実施する
print(c);
c.decrement();
print(c);
c.reset();
print(c);

この結果は、

bsh % source("hexTest.bsh");
0x0
0xC
0xB
0x0

となる。

カレンダカウンタ

カウンタの最後の例題としてカレンダカウンタを実装する。

まず、仕様を決める。

java APIからそれらしいクラスを検索してみる。それらしいDateクラスが見つかったので、bshで動作を調べてみる。

bsh % Date d = new Date();
bsh % print(d);
Fri Feb 02 22:20:53 JST 2001

しかし、Dateクラスでは、日付のカウントができない。Calendarクラスには、addメソッドが用意されおり、日付の増減が可能であり、今回の目的に合致する。Calendarから日付を取り出すには、getTimeメソッドを使うことにする。試しにbshで動作を確認してみる。Java APIの説明からカット&ペーストするだけでAPIの動作を確認することができるのもbshの利点である。

bsh % Calendar now = Calendar.getInstance();  // Calendarのインスタンス生成を変数nowにセット
bsh % print(now.getTime());         // getTime()で時刻を出力
Fri Feb 02 22:43:32 JST 2001
bsh % now.add(Calendar.DATE, 1);    // add()で日付を1日増やす
bsh % print(now.getTime());         // getTime()で時刻を出力
Sat Feb 03 22:43:32 JST 2001

カレンダカウンタは、次のようになる。

CalendarCount
import java.util.*;

/**
 * CalendarCount.java
 * カレンダカウンタ
 *
 * Created: Fri Feb 02 22:46:16 2001
 *
 * @author <a href="mailto:take@pwave.sig.or.jp "竹本 浩 "</a>
 * @version  1.0
 */

public class CalendarCount extends Count{
	public CalendarCount (){
		now_ = Calendar.getInstance(); // Calendarのインスタンスを生成する
		resetValue_ = now_.getTime();  // 現在時刻をリセット値にセットする
		reset();
	}
	public void increment() {
		now_.add(Calendar.DATE, 1);    // 日付を1増やす
		value_ = now_.getTime();       // 時刻を取り出しvalue_にセットする
	}
	public void decrement() {
		now_.add(Calendar.DATE, -1);   // 日付を1減らす
		value_ = now_.getTime();       // 時刻を取り出しvalue_にセットする
	}
	public String toString() {         // value_をDate型で表示する必要があるので
		return (((Date)value_).toString()); // toStringを定義している
	}
	private Calendar now_;
}// CalendarCount

カウンタの値には、Date型を使い、日付のカウンタにCalendar型を使用する。

ここでbshで実行してみる(説明のために実行文の後にコメントを付加した)。

bsh % CalendarCount c = new CalendarCount();  // CalendarCountのインスタンスを生成
bsh % print(c);                     // CalendarCountのtoString()でnowの日付を出力
Fri Feb 02 23:25:38 JST 2001
bsh % c.increment();                // increment()で1日増加する
bsh % print(c);                     // 増加した結果を出力
Sat Feb 03 23:25:38 JST 2001
bsh % c.increment();                // increment()で1日増加する
bsh % print(c);                     // 増加した結果を出力
Sun Feb 04 23:25:38 JST 2001
bsh % c.decrement();                // decrement()で1日減算する
bsh % print(c);                     // 減算した結果を出力
Sat Feb 03 23:25:38 JST 2001
bsh % c.reset();                    // reset()でインスタンス生成の日付に戻す
bsh % print(c);                     // リセット結果を出力
Fri Feb 02 23:25:38 JST 2001

これで、問題領域での実装と動作確認は終了した。

GUIとの結合の前に

問題領域でのカウントの例題にGUIを追加する前に、bshを使ってGUIを追加する方法を紹介する。

値とボタンを含むフレームを作ってみる。

bshによる値モデルGUI例

このフレームを表示するbshスクリプトは次のようになる。

GuiModel.bsh
import javax.swing.*;
import java.awt.event.*;
// モデルを作成
IntegerCount m = new IntegerCount();
// フレームとコンテナパネルを作成する
JFrame f = new JFrame("GUI Model Test");
JPanel p = new JPanel();
// モデルの値を表示するテキストフィールドとアクションを起こすボタンを作成
JTextField l = new JTextField(5);
JButton b = new JButton("increment");
// ボタンのアクションを定義(bshのimplicitメソッドを使用)
actionPerformed( event ) {
    // ここでは無条件にモデルmに対するincrement()呼び出しを
    // を行い、テキストフィールドの値を更新する
    m.increment();
    l.setText(m.value_.toString());
}
// ボタンにアクションリスナーを割り当てる
b.addActionListener( this );
// フレーム構成要素のパッキング
p.add(l);
p.add(b);
f.getContentPane().add(p);
f.pack();
// フレームの表示
f.show();

これを実行するときの例は、

bsh % source("GuiModel.bsh"); // このタイミングでフレームが表示され、incrementボタンで値が変わる
bsh % m.decrement();     // ここからは、bshで動的にモデルmを操作する、まずは2回decrement()する
bsh % m.decrement();
bsh % l.setText(m.value_.toString()); // 表示を更新する(別にincrementボタンを押してもよい)
bsh % m.reset();         // reset()の後に値を出力する、きちんとリセットされている
bsh % print(m);

のようになる。GUIを表示しながら、bshのコンソールで" m.reset() "のように動的にオブジェクトを変更することができる。これがインタプリタの大きな特徴である。

以上のように、javaのインタプリタを使うことによってテストに要する時間が短縮され、テスト方法の自由度も増すことが検証できた。

まとめ

問題領域の段階的プログラミングとして、ここでは

ことを検証できた。

問題領域で作成したクラスの構成をUMLで記述する。

問題領域カウンタクラス図

作成したプログラムの構造や技術的なポイントを後ですぐに取り戻すことができるように整理することが大切である。(4)

  1. このUML図は、フリーのソフトmUMLを使って作成した(http://www.mfcomputers.com/)。 javaで書かれたmUMLは、どの環境でも使え、UMLを書くことに徹しているため、他のUML統合ソフト よりも使い勝手がよい。(後でUMLの早見表を入れる!)
  2. バージョンや入手方法を後で追加する予定。
    • プログラムの構造をUMLで表現してみる
    • プログラムの技術ポイントをドキュメントに残す
    • プログラムにコメントを付加し、javadocでHTMLに変換しておく
    • プログラムやドキュメントはCVS等のバージョン管理ソフトを使って変更履歴をとり、いつでも以前の任意の時点に戻せるようにする

GUI領域での段階的プログラミング

MVCの特徴

MVCの特徴を挙げてみると、

がある。

MVCを使わないGUIカウンタ

最初にGUIカウンタのGUIインタフェースを決めることにする。図[カウンタのGUIインタフェース]のようにresetボタン、incrementボタン、decrementボタンと整数の値を表示するテキストボックスから構成される。

カウンタのGUIインタフェース

MVCを使ったGUIカウンタを作る前に、通常のGUIカウンタを作ってみる。リスト[CountDemo]がGUIカウンタのメインプログラムである。

CountDemo
import javax.swing.*;
import java.awt.event.*;

/**
 * CountDemo.java
 * MVCを使わないGUIカウンタメインプログラム
 *
 * Created: Thu Feb 08 22:36:01 2001
 *
 * @author <a href="mailto:take@pwave.sig.or.jp "竹本 浩</a>
 * @since JDK 1.3
 * @version 1.0
 */

public class CountDemo
{
	// メインプログラム
	public static void main(String[] args)
	{
		IntegerCount model = new IntegerCount();  // IntegerCountモデルを生成

		CountView	view = new CountView(model);  // モデルを持つカウンタビューを生成する

		JFrame frame = new JFrame("CountDemo");   // テスト用のフレームを生成する
		frame.addWindowListener(                  // フレームにウィンドウリスナーを追加する
			new WindowAdapter() {                 
					// フレームのクローズイベント処理のみを定義する
				public void windowClosing(WindowEvent evt) {
					System.exit(0);  // プログラムを終了する
				}
			}
		);
		frame.getContentPane().add(view); // フレームにカウンタビューを追加する
		frame.pack();                     // パッキングして
		frame.show();                     // フレームを表示する
	}
}

IntegerCountのインスタンスをモデルとして作成し、それをCountViewに渡してビューを作成する。後は、おきまりのウィンドウを閉じるためのWindowAdapterを登録してフレームを表示する。

次に、CountViewをリスト[CountView]に示す。

CountView
import javax.swing.*;
import java.awt.event.*;
import java.awt.BorderLayout;

/**
 * CountView.java
 * MVCを使わないカウンタビュー
 *
 * Created: Mon Jan 08 20:41:31 2001
 *
 * @author <a href="mailto:take@pwave.sig.or.jp "竹本 浩</a>
 * @version 1.0
 */

public class CountView extends JPanel {
	/**
	 * CountViewコンストラクタ
	 * @param model 整数カウンタモデル
	 */
	public CountView (IntegerCount model) {	
		JPanel panel = new JPanel(); // パネルを生成する
		model_ = model;              // モデルをビュー内部に保持する
		// resetボタン用のアクション(コールバック)を生成する
		// この部分がJDK1.3で新しく導入された部分!
		Action	resetAction = new AbstractAction("reset") {
				// アクションを実行メソッド
			public void actionPerformed(ActionEvent evt)
			{
				// モデルをリセットし、その値をvalueBoxに表示する
				model_.reset();
				valueBox.setText(model_.value_.toString());
			}
		};
		// アクションresetActionを持つリセットボタンを生成する
		// この部分がJDK1.3で新しく導入された部分!
		resetButton = new JButton(resetAction);
		// resetButtonをパネルに追加する
		panel.add(resetButton);

		// 同様にincrementボタンを生成
		Action	incrementAction = new AbstractAction("increment") {
			public void actionPerformed(ActionEvent evt)
			{
				model_.increment();
				valueBox.setText(model_.value_.toString());
			}
		};
		incrementButton = new JButton(incrementAction);
		panel.add(incrementButton);

		// decrementボタンを生成
		Action	decrementAction = new AbstractAction("decrement") {
			public void actionPerformed(ActionEvent evt)
			{
				model_.decrement();
				valueBox.setText(model_.value_.toString());
			}
		};
		decrementButton = new JButton(decrementAction);
		panel.add(decrementButton);

		// 値を表示するテキストボックスを生成する
		valueBox = new JTextField(model_.value_.toString());
		// テキストボックスの値を編集できないよう属性を変更する
		valueBox.setEditable(false);

		// レイアウトをBorderLayoutに変更し、上部にボタンパネル、中央にテキストボックス
		// を配置する
		setLayout(new BorderLayout());
		add(valueBox, "Center");
		add(panel, "North");
	}
	protected JButton resetButton;		// リセットボタン
	protected JButton incrementButton;  // incrementボタン(カウントアップ)
	protected JButton decrementButton;  // decrementボタン(カウントダウン)
	protected JTextField valueBox;      // 値表示用テキストボックス
	private IntegerCount model_;        // 整数カウンタモデル
}// CountView

ボタンの作成のところで、

Action	resetAction = new AbstractAction("reset") {
	public void actionPerformed(ActionEvent evt)
	{
		model_.reset();
		valueBox.setText(model_.value_.toString());
	}
};
resetButton = new JButton(resetAction);

のようにAbstractActionを無名クラスとして定義し、JButtonのコンストラクタに渡す。これで、ボタンを押したときの動作を登録する。これがJDK 1.3で新たに追加された機能である。この機能によってボタンのコールバックがとてもきれいに記述することができる。resetボタンが押されたときにモデルにresetをコールし、モデルの値をvalueBoxにセットする。しかし、本来リセットボタンの機能(5)としては、ボタンを押された時にモデルにresetコールを実行することであり、値の再表示をどのように行うかはビューの機能である。これがMVCを使っていないプログラムの欠点である。

バリューモデルの実装

GUIカウンタ問題をMVCを使って実装するために、Javaのモデルクラスを調べてみた。JavaのAPIを探しても、値だけを持つシンプルなモデルは見つからない。(6)

そこで、値のみを持つモデルを独自に定義することにする。しかし、何にもないところからモデルを作るのは大変なので、"JavaWorld 2000, jul., p110-119"を元ネタとする。

モデルインタフェース

モデルは、インタフェースの定義とその実装に分かれる。値モデルのインタフェースをに示す。このモデルインタフェースの特徴は、

である。

IValueModel
import java.util.*;

/**
 * IValueModel.java
 *
 * MVCの検証のために、一番単純なValueModelを作成する
 * javaでは、interfaceを使ってうまくMVCを構築している。
 * 特に、モデルの発行するイベントオブジェクトの受け手が
 * 処理する部分をinterfaceに記述している点に注意。
 *
 * 参考文献 JavaWorld 2000, jul., p110-119
 *
 * Created: Tue Jan 09 00:10:09 2001
 *
 * @author <a href="mailto:take@pwave.sig.or.jp">Hiroshi TAKEMOTO</a>
 * @version 1.00
 */

public interface IValueModel {
	// 内部イベントクラスを使って固有の情報をリスナーに渡せるようにしている。
	public static class Event extends EventObject {
		private Object value_;
		
		public Event (IValueModel model, Object value) {
			// イベントの発行元をセットするためにsuper(model)をコール。
			super (model);
			this.value_ = value;
		}
		public Object getValue() {
			return value_;
		}
	}
	
	// この部分をリスナーで実装することに注意!
	public interface Listener extends EventListener {
		public void valueChanged(IValueModel.Event e);
	}

	// ValueModelの基本的なインタフェース
	public void setValue(Object obj);
	public Object getValue();

	// イベントリスナーの処理部分
	public void addListener(IValueModel.Listener l);
	public void removeListener(IValueModel.Listener l);

}// IValueModel

デフォルトモデルクラスの実装

次に値モデルのデフォルトモデルクラスを実装する。

メンバーlisteners_に値モデルのリスナーをVectorとして登録する。adListener, removeListenerでリスナーを管理し、モデルの根幹である値をメンバー変数value_に保持する。value_へのアクセスメソッドgetValueはそのままvalue_の値を返すだけだが、setValueでは値をセットした後にリスナーに値が変化したことを通知するためにfireValueChangedを呼び出す。

リスト[DefaultValueModel]にデフォルトモデルクラスのリスト示す。

DefaultValueModel
import java.util.*;

/**
 * DefaultValueModel.java
 *
 *
 * Created: Tue Jan 16 21:23:25 2001
 *
 * @author <a href="mailto: "</a>
 * @version
 */

public abstract class DefaultValueModel implements IValueModel{
	protected Vector listeners_;
	protected Object value_;

	public DefaultValueModel (){
		listeners_ =  new Vector();
	}

	public void setValue(Object value) {
		value_ = value;
		fireValueChanged(value);
	}

	public Object getValue() {
		return(value_);
	}

	public void addListener(IValueModel.Listener l) {
		listeners_.add(l);
	}

	public void removeListener(IValueModel.Listener l) {
		listeners_.removeElement(l);
	}

	protected void fireValueChanged (Object obj) {
		int size = listeners_.size();
		IValueModel.Event e = new IValueModel.Event(this, obj);

		for (int i = 0; i < size; i++) 
			((IValueModel.Listener)listeners_.elementAt(i)).valueChanged(e);
	}
}// DefaultValueModel

MVCを使ったGUIカウンタ

これで、MVCを使ったGUIカウンタを実装する準備が整った。整数カウンタをMVCで実装するとどのようになるか試してみる(リスト[MVC版Count]参照)。

MVC版Count
/**
 * Count.java
 * カウントのアブストラクトクラス
 *
 * Created: Thu Feb 01 22:28:26 2001
 *
 * @author <a href="mailto:take@pwave.sig.or.jp "竹本 浩</a>
 * @version 1.0
 */

public abstract class Count extends DefaultValueModel {
	static Object resetValue_;

	public abstract void increment();
	public abstract void decrement();
	public void reset() {
		setValue(resetValue_);
	}
}// Count

最初にCountクラスの変更部分を見てみる。

public abstract class Count extends DefaultValueModel {

でCountクラスをDefautValueModelのサブクラスとして定義する。次にresetメソッドを

setValue(resetValue_);

のように直接value_にセットするのではなく、setValueメソッドを使うようにする。これがモデルクラスを使うときのポイントである。

同様にIntegerCountクラスについてもincrement, decrementメソッドでvalue_にセットしていたところをsetValueメソッドを使って値を設定する(リスト[MVC版IntegerCount]参照)。

MVC版IntegerCount
import java.lang.*;
/**
 * IntegerCount.java
 * 整数カウンタ
 *
 * Created: Thu Feb 01 22:36:01 2001
 *
 * @author <a href="mailto:take@pwave.sig.or.jp "竹本 浩</a>
 * @version 1.0
 */

public class IntegerCount extends Count{
	public IntegerCount (){
		resetValue_ = new Integer(0);
		reset();
	}
	public void increment() {
		setValue(new Integer(((Integer)value_).intValue() + 1));
	}
	public void decrement() {
		setValue(new Integer(((Integer)value_).intValue() - 1));
	}
}// IntegerCount

ビューの部分の変更は、MVCモデルのビューを作るときの基本形になるところであり、値モデルのリスナーのアクションメソッドを定義する。

protected IValueModel.Listener modelListener_ =
	new IValueModel.Listener() {
		public void valueChanged (IValueModel.Event e) {
			valueBox.setText(e.getValue().toString());
		}
	};

モデルの値が更新されてイベントがリスナーに届いたら、ここで定義したvalueChangedが呼び出される。

最後に、ビューのインスタンス生成時にモデルのリスナーに自分自身を登録します。

model_.addListener(modelListener_);

メインプログラムは同じなので、これでMVCを使ったカウンタモデルの完成である(整数カウンタビューのソースをリスト[MVC版整数カウンタビュー]に示す)。

MVC版整数カウンタビュー
//  import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
import java.awt.BorderLayout;
import java.awt.*;

/**
 * ValueModelView.java
 *
 *
 * Created: Mon Jan 08 20:41:31 2001
 *
 * @author <a href="mailto: "</a>
 * @version
 */

public class ValueModelView extends JPanel {
	protected IntegerCount model_;
	protected IValueModel.Listener modelListener_ =
		new IValueModel.Listener() {
			public void valueChanged (IValueModel.Event e) {
				valueBox.setText(e.getValue().toString());
			}
		};

	public ValueModelView (IntegerCount model) {
		model_ = model;
		JPanel panel = new JPanel();
	
		model_.addListener(modelListener_);

		Action	resetAction = new AbstractAction("reset") {
			public void actionPerformed(ActionEvent evt)
			{
				// System.out.println("rset button press   ed");
				model_.reset();
			}
		};
		resetButton = new JButton(resetAction);
		panel.add(resetButton);

		Action	incrementAction = new AbstractAction("increment") {
			public void actionPerformed(ActionEvent evt)
			{
				// System.out.println("increment button pressed");
				model_.increment();
			}
		};
		incrementButton = new JButton(incrementAction);
		panel.add(incrementButton);

		Action	decrementAction = new AbstractAction("decrement") {
			public void actionPerformed(ActionEvent evt)
			{
				// System.out.println("decrement button pressed");
				model_.decrement();
			}
		};
		decrementButton = new JButton(decrementAction);
		panel.add(decrementButton);

		setLayout(new BorderLayout());
		add(panel, "North");
		valueBox = new JTextField("0", 5);
		valueBox.setEditable(false);
		add(valueBox, "Center");
	}
	protected JButton resetButton;		
	protected JButton incrementButton;
	protected JButton decrementButton;
	protected JTextField valueBox;
}// ValueModelView

モデルを変える(カレンダカウンタ)

次に、整数カウンタからカレンダカウンタに変更してみる。そのままValueModeDemo.java, ValueModelView.javaを修正してもいいが、classファイルが上書きされてしまうので、CalendarModelDemo.java, CalendarModelView.javaに変えてから修正する。

変更点は、整数カウンタと同様にvalue_への設定をsetValueメソッドを使って設定することと、CalendarModelDemo.java, CalendarModelView.javaのモデルの型をIntegerCountからCalendarCountに変更する点である。これだけだと思って実行したら、初期値が0になっていた。JTextFieldのインスタンス生成コードを

valueBox = new JTextField(model_.getValue().toString);

に修正して完成である。

このカレンダカウンタはアプレットしても動作するようにAppletのサブクラスとして定義し、initメソッドでカウンタモデルの生成とカレンダモデルビューの生成を行うように修正し、mainではアプレットCalendarModelDemoを生成し、

Applet applet = new CalendarModelDemo();
applet.init();
// 中略
applet.start();

それにinitを呼び出し、最後にアプレットにstartを呼び出すように修正した。

CalendarModelDemo
import javax.swing.*;
import java.awt.event.*;
// import java.applet.*; // Applet用のクラス

public class CalendarModelDemo extends JApplet
{
	public void init() {
		CalendarCount counter = new CalendarCount();
		CalendarModelView	view = new CalendarModelView(counter);

		getContentPane().add(view);
	}
		
	public static void main(String[] args)
	{
		JFrame frame = new JFrame("CalendarModelDemo");
		frame.addWindowListener(
			new WindowAdapter() {
				public void windowClosing(WindowEvent evt) {
					System.exit(0);
				}
			}
		);
		JApplet applet = new CalendarModelDemo();
		applet.init();
		frame.getContentPane().add(applet);
		frame.pack();
		frame.setSize(300, 100);
		frame.setVisible(true);
		applet.start();
	}
}
CalendarModelView
//  import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
import java.awt.BorderLayout;
import java.awt.*;

/**
 * CalendarModelView.java
 *
 *
 * Created: Mon Jan 08 20:41:31 2001
 *
 * @author <a href="mailto: "</a>
 * @version
 */

public class CalendarModelView extends JPanel {
	protected CalendarCount model_;
	protected IValueModel.Listener modelListener_ =
		new IValueModel.Listener() {
			public void valueChanged (IValueModel.Event e) {
				valueBox.setText(e.getValue().toString());
			}
		};

	public CalendarModelView (CalendarCount model) {
		model_ = model;
		JPanel panel = new JPanel();
	
		model_.addListener(modelListener_);

		Action	resetAction = new AbstractAction("reset") {
			public void actionPerformed(ActionEvent evt)
			{
				// System.out.println("rset button press   ed");
				model_.reset();
			}
		};
		resetButton = new JButton(resetAction);
		panel.add(resetButton);

		Action	incrementAction = new AbstractAction("increment") {
			public void actionPerformed(ActionEvent evt)
			{
				// System.out.println("increment button pressed");
				model_.increment();
			}
		};
		incrementButton = new JButton(incrementAction);
		panel.add(incrementButton);

		Action	decrementAction = new AbstractAction("decrement") {
			public void actionPerformed(ActionEvent evt)
			{
				// System.out.println("decrement button pressed");
				model_.decrement();
			}
		};
		decrementButton = new JButton(decrementAction);
		panel.add(decrementButton);

		setLayout(new BorderLayout());
		add(panel, "North");
		valueBox = new JTextField(model_.getValue().toString());
		valueBox.setEditable(false);
		add(valueBox, "Center");
	}
	protected JButton resetButton;		
	protected JButton incrementButton;
	protected JButton decrementButton;
	protected JTextField valueBox;
}// CalendarModelView

しかし、アプレットバージョンの実行は、どうもかうまく動かない!!アプレットビューワーでは動いているが、HTMLConverterを使ってもダメである。原因は、今のところわからないが、気にせず先に進む。

JDK1.3に付属のアプレットビューワで表示する場合には、

% appletviewer http://www.st.rim.or.jp/~h_take/MVC/CalendarModelDemo.html

で起動する。

複数ビューに対応

一つモデルを複数のビューから参照する例として1個のカウンタ値を表示するフレームを2個のカウンタが共有する例を挙げる(図[複数ビューカウンタ]参照)。

複数ビューカウンタ

ビューの修正は、値を表示するテキストボックスを取り除く。ただし、モデルはボタンアクションを処理するために保持する。もはや、カウンタビューにはカウンタモデルを表示する必要がないので、モデルの型はアブストラクト・クラスCountとして定義する。

MultiCountView
//  import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
import java.awt.BorderLayout;
import java.awt.*;

/**
 * MultiCountView.java
 *
 *
 * Created: Mon Jan 08 20:41:31 2001
 *
 * @author <a href="mailto: "</a>
 * @version
 */

public class MultiCountView extends JPanel {
	protected Count model_;
	public MultiCountView (Count model) {
		model_ = model;
		JPanel panel = new JPanel();
	
		Action	resetAction = new AbstractAction("reset") {
			public void actionPerformed(ActionEvent evt)
			{
				// System.out.println("rset button press   ed");
				model_.reset();
			}
		};
		resetButton = new JButton(resetAction);
		panel.add(resetButton);

		Action	incrementAction = new AbstractAction("increment") {
			public void actionPerformed(ActionEvent evt)
			{
				// System.out.println("increment button pressed");
				model_.increment();
			}
		};
		incrementButton = new JButton(incrementAction);
		panel.add(incrementButton);

		Action	decrementAction = new AbstractAction("decrement") {
			public void actionPerformed(ActionEvent evt)
			{
				// System.out.println("decrement button pressed");
				model_.decrement();
			}
		};
		decrementButton = new JButton(decrementAction);
		panel.add(decrementButton);

		setLayout(new BorderLayout());
		add(panel, "Center");
	}
	protected JButton resetButton;		
	protected JButton incrementButton;
	protected JButton decrementButton;
}// MultiCountView

次にメイン関数だが、1個のカウンタオブジェクトを2個のMultiCountViewに渡して2個のカウンタビューを生成する。

カウンタモデルのアクションリスナーを生成し、ここでテキストボックスに値をセットする。

後は、2個のフレームにそれぞれのビューを詰めて表示するだけである。プログラムをさぼってクローズ処理をテキストボックスにのみ付けた。

MultiViewDemo
import javax.swing.*;
import java.awt.event.*;

public class MultiViewDemo
{
	/**
	 * 知らなかったが、staticメソッドから使うときにはvalueBox_,
	 * をstaticで定義しないとコンパイルできなかった。
	 * 「static でない 変数 valueBox_ を static コンテキストから参照することはできません」
	 * のエラーメッセージがでる
	 * 後で、言語仕様を調べてる必要がある
	 */
	static JTextField valueBox_;

	public static void main(String[] args)
	{
		// IntegerCount counter = new IntegerCount();
		CalendarCount counter = new CalendarCount();

		MultiCountView	view1 = new MultiCountView(counter);
		MultiCountView	view2 = new MultiCountView(counter);
		
		
		// counter.addListener(modelListener_);

		counter.addListener(
				new IValueModel.Listener() {
					public void valueChanged (IValueModel.Event e) {
						valueBox_.setText(e.getValue().toString());
					}
				}
		);

		JFrame frame = new JFrame("CountValue");
		frame.addWindowListener(
			new WindowAdapter() {
				public void windowClosing(WindowEvent evt) {
					System.exit(0);
				}
			}
		);
		valueBox_ = new JTextField(counter.getValue().toString(), 10);
		valueBox_.setEditable(false);
		frame.getContentPane().add(valueBox_);
		frame.pack();
		frame.show();

		JFrame frame1 = new JFrame("Counter1");
		frame1.getContentPane().add(view1);
		frame1.pack();
		frame1.show();
		JFrame frame2 = new JFrame("Counter2");
		frame2.getContentPane().add(view2);
		frame2.pack();
		frame2.show();
	}
}

まとめ

  1. ここではコントローラとしてのリセットボタンの機能を言っている
  2. 値モデルの場合、それに関連づけるビューが一意に特定できないため、値モデルのイベントリスナーを 内蔵したクラスが用意されなかったのでないかと思われる。