MVCの特徴を挙げてみると、
がある。
最初にGUIカウンタのGUIインタフェースを決めることにする。図[カウンタのGUIインタフェース]のようにresetボタン、incrementボタン、decrementボタンと整数の値を表示するテキストボックスから構成される。
リスト[CountDemo.java]がGUIカウンタのメインプログラムである。
package nonmvc; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import javax.swing.JFrame; import model.AbstaractCount; import model.ICount; import model.IntegerCount; /** * CountDemo.java * * @author Hiroshi TAKEMOTO * @version $Id: MVCwithEclipse_c3.html,v 1.2 2005/03/30 13:49:25 take Exp $ */ public class CountDemo { // メインプログラム public static void main(String[] args) { ICount 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のインスタンスをモデル(14)として作成し、それをCountViewに渡してビューを作成する。後は、おきまりのウィンドウを閉じるためのWindowAdapterを登録してフレームを表示する。次に、CountViewをリスト[CountView.java]に示す。
package nonmvc; import java.awt.BorderLayout; import java.awt.event.ActionEvent; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.JButton; import javax.swing.JPanel; import javax.swing.JTextField; import model.AbstaractCount; import model.ICount; import model.IntegerCount; /** * CountView.java * * @author Hiroshi TAKEMOTO * @version $Id: MVCwithEclipse_c3.html,v 1.2 2005/03/30 13:49:25 take Exp $ */ public class CountView extends JPanel { protected JButton resetButton; // リセットボタン protected JButton incrementButton; // incrementボタン(カウントアップ) protected JButton decrementButton; // decrementボタン(カウントダウン) protected JTextField valueBox; // 値表示用テキストボックス private ICount model_; // カウンタモデル /** * CountViewコンストラクタ * @param model 整数カウンタモデル */ public CountView (ICount 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_.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_.toString()); } }; incrementButton = new JButton(incrementAction); panel.add(incrementButton); // decrementボタンを生成 Action decrementAction = new AbstractAction("decrement") { public void actionPerformed(ActionEvent evt) { model_.decrement(); valueBox.setText(model_.toString()); } }; decrementButton = new JButton(decrementAction); panel.add(decrementButton); // 値を表示するテキストボックスを生成する valueBox = new JTextField(model_.toString()); // テキストボックスの値を編集できないよう属性を変更する valueBox.setEditable(false); // レイアウトをBorderLayoutに変更し、上部にボタンパネル、中央にテキストボックス // を配置する setLayout(new BorderLayout()); add(valueBox, "Center"); add(panel, "North"); } }// CountView
ボタンの作成のところで、
Action resetAction = new AbstractAction("reset") { public void actionPerformed(ActionEvent evt) { model_.reset(); valueBox.setText(model_.toString()); } }; resetButton = new JButton(resetAction);
のようにAbstractActionを匿名クラスとして定義し、JButtonのコンストラクタに渡す。これで、ボタンを押したときの動作を登録する。これがJDK 1.3で新たに追加された機能である。この機能によってボタンのコールバックがとてもきれいに記述することができる。resetボタンが押されたときにモデルにresetをコールし、モデルの値をvalueBoxにセットする。しかし、本来リセットボタンの機能(15)としては、ボタンを押された時にモデルにresetコールを実行することであり、値の再表示をどのように行う かはビューの処理である。これがMVCを使っていないプログラムの欠点である。
モデルとビューにObserver, Observableを使ったMVCカウンタを作成する。最初にモデルであるAbstaractCountをObservableのサブクラスに変更する。
public abstract class AbstaractCount extends Observable implements ICount {
次にsetValue_メソッドからObserverに変更を通知するために、setChanged, notifyObserversメソッド呼び出しを追加する。
public void setValue_(Object value_) { this.value_ = value_; setChanged(); notifyObservers(); }
次にビューを修正する。比較のためにCountView.javaをコピーして、ObserverCountView.javaとして変更する。Observerインタフェースの追加を宣言し、updateメソッドを追加する
public class ObserverCountView extends JPanel implements Observer {
public void update(Observable o, Object arg) { if (o instanceof ICount) valueBox.setText(((ICount)o).toString()); else valueBox.setText(""); }
MVCを使わないGUIカウンタで、行っていたsetTextをupdateで行うため、各ActionのactionPerformedからこの処理を削除する。
Action resetAction = new AbstractAction("reset") { public void actionPerformed(ActionEvent evt) { model_.reset(); } };
これで、コントローラはICountで定義した操作を実行するだけでよくなる。最後に、ビューをモデルのObserverとして登録する処理を行えば完了である。
if (model instanceof Observable) ((Observable)model).addObserver(this);
最終のソースは、リスト[AbstractCount.java]とリスト[ObserverCountView.java]に示す。
フレームワークを使った場合、Observableのサブクラスとしてモデルを実装することができない場合がある。このような場合には、
の2つの方法がある。ここでは、独自のバリューモデルインタフェースを定義する方法を使ってGUIカウンタを実装してみる。(16)
モデルは、インタフェースの定義とその実装に分かれる。値モデルのインタフェースをに示す。このモデルインタフェースの特徴は、
である。
package valuemodel; 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:[email protected]">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_(); // イベントリスナーの処理部分 /** * リスナーを追加する * @param l リスナー */ public void addListener(IValueModel.Listener l); /** * リスナーを削除する * @param l リスナー */ public void removeListener(IValueModel.Listener l); }// IValueModel
次に値モデルのAbstractCountクラスを変更してモデルインタフェースを実装する。比較のために、AbstractCountクラスをAbstractValueCountクラスにコピーしてして作業をする。
AbstractCountへの修正は、
である。ソースの修正は、
public abstract class AbstaractValueCount implements ICount, IValueModel { protected Vector listeners_ = new Vector(); 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); }
リスト[AbstractValueCount.java]に完全なリスト示す。
比較のため、AbstractValueCountのサブクラスとしてIntegerValueCountを定義するが、内容はIntegerValueCountと同じです。
リスナーであるビューを修正する。比較のためにCountView.javaをコピーして、ValueCountView.javaとして変更する。
ビューの部分の変更は、バリューモデルのリスナーのアクションメソッドを定義する。モデル(model_)をアクションリスナーのvalueChangedメソッドから見えるようにprotectedに変更する。
protected ICount model_; // カウンタモデル // リスナーの生成 protected IValueModel.Listener modelListener_ = new IValueModel.Listener() { public void valueChanged (IValueModel.Event e) { valueBox.setText(e.getValue_().toString()); } };
モデルの値が更新されてイベントがリスナーに届いたら、ここで定義したvalueChangedが呼び出される。最後に、ビューのインスタンス生成時にモデルのリスナーに自分自身を登録します。
// リスナーの登録 if (model instanceof IValueModel) ((IValueModel)model).addListener(modelListener_);
後は、ObserverCountViewの場合と同様に、
valueBox.setText(model_.toString());
を削除する。最終のソースは、リスト[ValueCountView.java]に示す。
以上のように、独自のバリューインタフェースを追加する方式もObserver, Observableを使った方式と比べ、ソースの修正量はそんなに多くならないことが分かる。Swingのコンポーネントには、MVCを使った部分が多々存在するのでこれらのモデルの実装について時間があれば検証してみたいと考えている。
以下に、MVC対応のGUIへの完全なソースを示す。
package model; import java.util.Observable; /** * カウントのアブストラクトクラス * * @author Hiroshi TAKEMOTO * @version $Id: MVCwithEclipse_c3.html,v 1.2 2005/03/30 13:49:25 take Exp $ */ public abstract class AbstaractCount extends Observable implements ICount { /** * 現在の値 */ private Object value_ = null; /** * リセット値 */ private Object resetValue_ = null; /** * @see model.ICount#increment() */ public abstract void increment(); /** * @see model.ICount#decrement() */ public abstract void decrement(); /** * @see java.lang.Object#toString() */ public abstract String toString(); /** * @see model.ICount#reset() */ public void reset() { setValue_(getResetValue_()); } /** * Returns the resetValue_. * @return Object */ protected Object getResetValue_() { return resetValue_; } /** * Returns the value_. * @return Object */ public Object getValue_() { return value_; } /** * Sets the resetValue_. * @param resetValue_ The resetValue_ to set */ protected void setResetValue_(Object resetValue_) { this.resetValue_ = resetValue_; } /** * Sets the value_. * @param value_ The value_ to set */ public void setValue_(Object value_) { this.value_ = value_; setChanged(); notifyObservers(); } }
package observermvc; import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.util.Observable; import java.util.Observer; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.JButton; import javax.swing.JPanel; import javax.swing.JTextField; import model.AbstaractCount; import model.ICount; import model.IntegerCount; /** * CountView.java * * @author Hiroshi TAKEMOTO * @version $Id: MVCwithEclipse_c3.html,v 1.2 2005/03/30 13:49:25 take Exp $ */ public class ObserverCountView extends JPanel implements Observer { protected JButton resetButton; // リセットボタン protected JButton incrementButton; // incrementボタン(カウントアップ) protected JButton decrementButton; // decrementボタン(カウントダウン) protected JTextField valueBox; // 値表示用テキストボックス private ICount model_; // カウンタモデル /** * CountViewコンストラクタ * @param model 整数カウンタモデル */ public ObserverCountView (ICount model) { JPanel panel = new JPanel(); // パネルを生成する model_ = model; // モデルをビュー内部に保持する // リスナーの登録 if (model instanceof Observable) ((Observable)model).addObserver(this); // resetボタン用のアクション(コールバック)を生成する // この部分がJDK1.3で新しく導入された部分! Action resetAction = new AbstractAction("reset") { // アクションを実行メソッド public void actionPerformed(ActionEvent evt) { // モデルをリセットし、その値をvalueBoxに表示する model_.reset(); } }; // アクションresetActionを持つリセットボタンを生成する // この部分がJDK1.3で新しく導入された部分! resetButton = new JButton(resetAction); // resetButtonをパネルに追加する panel.add(resetButton); // 同様にincrementボタンを生成 Action incrementAction = new AbstractAction("increment") { public void actionPerformed(ActionEvent evt) { model_.increment(); } }; incrementButton = new JButton(incrementAction); panel.add(incrementButton); // decrementボタンを生成 Action decrementAction = new AbstractAction("decrement") { public void actionPerformed(ActionEvent evt) { model_.decrement(); } }; decrementButton = new JButton(decrementAction); panel.add(decrementButton); // 値を表示するテキストボックスを生成する valueBox = new JTextField(model_.toString()); // テキストボックスの値を編集できないよう属性を変更する valueBox.setEditable(false); // レイアウトをBorderLayoutに変更し、上部にボタンパネル、中央にテキストボックス // を配置する setLayout(new BorderLayout()); add(valueBox, "Center"); add(panel, "North"); } /** * @see java.util.Observer#update(Observable, Object) */ public void update(Observable o, Object arg) { if (o instanceof ICount) valueBox.setText(((ICount)o).toString()); else valueBox.setText(""); } }// CountView
package valuemodel; import java.util.Observable; import java.util.Vector; import model.ICount; /** * カウントのアブストラクトクラス * * @author Hiroshi TAKEMOTO * @version $Id: MVCwithEclipse_c3.html,v 1.2 2005/03/30 13:49:25 take Exp $ */ public abstract class AbstaractValueCount implements ICount, IValueModel { /** * リスナーを保持するためベクター */ protected Vector listeners_ = new Vector(); /** * 現在の値 */ private Object value_ = null; /** * リセット値 */ private Object resetValue_ = null; /** * @see valuemodel.IValueModel#addListener(Listener) */ public void addListener(IValueModel.Listener l) { listeners_.add(l); } /** * @see valuemodel.IValueModel#removeListener(Listener) */ public void removeListener(IValueModel.Listener l) { listeners_.removeElement(l); } /** * リスナーにモデルの値が変更したことを通知する * @param obj 変更した値 */ 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); } /** * @see model.ICount#increment() */ public abstract void increment(); /** * @see model.ICount#decrement() */ public abstract void decrement(); /** * @see java.lang.Object#toString() */ public abstract String toString(); /** * @see model.ICount#reset() */ public void reset() { setValue_(getResetValue_()); } /** * Returns the resetValue_. * @return Object */ protected Object getResetValue_() { return resetValue_; } /** * Returns the value_. * @return Object */ public Object getValue_() { return value_; } /** * Sets the resetValue_. * @param resetValue_ The resetValue_ to set */ protected void setResetValue_(Object resetValue_) { this.resetValue_ = resetValue_; } /** * Sets the value_. * @param value_ The value_ to set */ public void setValue_(Object value_) { this.value_ = value_; fireValueChanged(value_); } }
package valuemvc; import java.awt.BorderLayout; import java.awt.event.ActionEvent; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.JButton; import javax.swing.JPanel; import javax.swing.JTextField; import valuemodel.IValueModel; import model.AbstaractCount; import model.ICount; import model.IntegerCount; /** * CountView.java * * @author Hiroshi TAKEMOTO * @version $Id: MVCwithEclipse_c3.html,v 1.2 2005/03/30 13:49:25 take Exp $ */ public class ValueCountView extends JPanel { protected JButton resetButton; // リセットボタン protected JButton incrementButton; // incrementボタン(カウントアップ) protected JButton decrementButton; // decrementボタン(カウントダウン) protected JTextField valueBox; // 値表示用テキストボックス protected ICount model_; // カウンタモデル // リスナーの生成 protected IValueModel.Listener modelListener_ = new IValueModel.Listener() { public void valueChanged (IValueModel.Event e) { valueBox.setText(e.getValue_().toString()); } }; /** * CountViewコンストラクタ * @param model 整数カウンタモデル */ public ValueCountView (ICount model) { JPanel panel = new JPanel(); // パネルを生成する model_ = model; // モデルをビュー内部に保持する // リスナーの登録 if (model instanceof IValueModel) ((IValueModel)model).addListener(modelListener_); // resetボタン用のアクション(コールバック)を生成する // この部分がJDK1.3で新しく導入された部分! Action resetAction = new AbstractAction("reset") { // アクションを実行メソッド public void actionPerformed(ActionEvent evt) { // モデルをリセットし、その値をvalueBoxに表示する model_.reset(); } }; // アクションresetActionを持つリセットボタンを生成する // この部分がJDK1.3で新しく導入された部分! resetButton = new JButton(resetAction); // resetButtonをパネルに追加する panel.add(resetButton); // 同様にincrementボタンを生成 Action incrementAction = new AbstractAction("increment") { public void actionPerformed(ActionEvent evt) { model_.increment(); } }; incrementButton = new JButton(incrementAction); panel.add(incrementButton); // decrementボタンを生成 Action decrementAction = new AbstractAction("decrement") { public void actionPerformed(ActionEvent evt) { model_.decrement(); } }; decrementButton = new JButton(decrementAction); panel.add(decrementButton); // 値を表示するテキストボックスを生成する valueBox = new JTextField(model_.toString()); // テキストボックスの値を編集できないよう属性を変更する valueBox.setEditable(false); // レイアウトをBorderLayoutに変更し、上部にボタンパネル、中央にテキストボックス // を配置する setLayout(new BorderLayout()); add(valueBox, "Center"); add(panel, "North"); } }// CountView