MVCモデルについて

MVCモデルについて - GeekなNooblog
プログラマーが意識するべきUI設計指針 3つのMVCモデル - GeekなNooblog
MVCモデルの問題点を解決するPMモデルとMVPモデル - GeekなNooblog
MVCにおけるViewの表示方法 トランザクションスクリプト、ドメインモデル - GeekなNooblog

MVCモデルの概念としては何の疑問もないんだけど、実装となるとどうしていいかわからないところが多々あるので書いてみたいと思います。

MVCとは?

コンピュータ内部のデータをユーザに提示し、それに対してユーザが何らかの指示を出すタイプの、独自のユーザーインタフェースをもつアプリケーションソフトウェアを、以下に述べるようなmodel・view・controllerの3つの部分に分割して設計・実装するという技法、又はそのような構造をいう。

各モジュールが比較的截然と分かれ、プログラムの見通しがよくなるとともに、ユーザインタフェース (UI) 部分を別のモジュールに取り替えることが容易となるのが利点である。

wikipediaMVCを表す画像では、ControllerからViewの実線が伸びていますが、これは可能なら避けるべきものです。


今回はOKボタンをクリックすると表示されている数値がインクリメントされていくというプログラムを作成してみようと思います。

まず最初にMVCでクラス分けを完全に無視したものを作成してみました。
ひとつのクラスで画面の表示、アクションを受け取ったときのモデルへの操作、modelの実際の処理を行っています。

  • 実行イメージ


クリックすると+1された数値が表示される

  • MVCモデルではないコード(import 省略)
public class ButtonIncrement extends JFrame implements ActionListener {
	private JLabel label;
	private JButton button;

	/**
	 * View Sampleクラスのコンストラクタ Viewの設定を行う。
	 */
	public ButtonIncrement() {
		// ウィンドウが閉じられたときプログラムを終了するように設定します。
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		// コンポーネントの配置のレイアウトを指定します。
		this.setLayout(new FlowLayout());
		// JFrameに追加するラベルを初期化します。
		label = new JLabel("0");
		// JFrameに作成したラベルを追加します。
		this.add(label);
		// JFrameに追加するButtonを初期化します。
		button = new JButton("OK");
		// ボタンが押されたとき呼び出されるActionListenerに追加します。
		button.addActionListener(this);
		// JFrameに作成したボタンを貼り付けます。
		this.add(button);
		// JFrameのサイズを追加されたコンポーネント(ボタンやラベル)に合わせて最適化を行います。
		this.pack();
		// 作成したJFrameの表示をします。
		this.setVisible(true);
	}

	/**
	 * Model + View 表示されている数値を1加算する。<br>
	 * ラベルの文字列を取得し、1度数値に変換した後に1を加算する。<br>
	 * 加算した結果を、ラベルに設定する。
	 */
	public void changeTextLabel() {
		// ラベルの文字列を取得します。
		String numberText = label.getText();
		// テキストフィールドの文字列を数値に変換 例外処理は省略
		int number = Integer.parseInt(numberText);
		// 数値を+1します。
		number = increment(number);
		// 数値を文字列に変換します。
		numberText = Integer.toString(number);
		// ラベルに変更されたテキストをセットします。
		label.setText(numberText);
	}

	/**
	 * Model 数値を1加算する。<br>
	 * 
	 * @param number
	 *            インクリメントする数値
	 * @return number + 1 インクリメントした数値
	 */
	public int increment(int number) {
		return number + 1;
	}

	/**
	 * Controller ボタンがクリックされるとこのメソッドが実行される。<br>
	 */
	@Override
	public void actionPerformed(ActionEvent e) {
		changeTextLabel();
	}

	/**
	 * メインメソッド<br>
	 */
	public static void main(String[] args) {
		new ButtonIncrement();
	}
}


MVCパターン的にはこれだと駄目なわけです。Model View Controllerの処理を分割できていません。



次にMVCパターンのアクティブモデルに則った(つもりの)ものを作成してみました。

public class Main {
	public static void main(String[] args) {
		Model model = new Model();
		View view = new View();
		Controller controller = new Controller(model);
		view.addToButtonActionListener((ActionListener) controller);
		model.addObserver(view);
	}
}
public class Controller implements ActionListener {
	private Model model;

	public Controller(Model model) {
		this.model=model;
	}

	@Override
	public void actionPerformed(ActionEvent e) {
		if (e.getActionCommand().equals("OK")) {
			model.increment();
		}
	}
}
public class Model extends Observable {
	private int number;

	public Model() {
		number = 0;
	}

	public void increment() {
		number++;
		// 以下の2行でModelに変更があったことをObserverに通知する
		setChanged();
		notifyObservers();
	}

	public int getNumber() {
		return number;
	}
}
public class View extends JFrame implements Observer {
	private JButton button;
	private JLabel label;

	/**
	 * View Sampleクラスのコンストラクタ Viewの設定を行う。
	 */
	public View() {
		// ウィンドウが閉じられたときプログラムを終了するように設定します。
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		// コンポーネントの配置のレイアウトを指定します。
		this.setLayout(new FlowLayout());
		// JFrameに追加するラベルを定義します。
		label = new JLabel("0");
		// JFrameに作成したラベルを追加します。
		this.add(label);
		// JFrameに追加するButtonの定義
		button = new JButton("OK");
		// ボタンが押されたとき呼び出されるActionListenerに追加します。
		// JFrameに作成したボタンを貼り付けます。
		this.add(button);
		// JFrameのサイズを追加されたコンポーネント(ボタンやラベル)に合わせて最適化を行います。
		this.pack();
		// 作成したJFrameの表示をします。
		this.setVisible(true);
	}

	public void addToButtonActionListener(ActionListener actionListener) {
		button.addActionListener(actionListener);
	}

	/**
	 * Observerインターフェースのupdateメソッド
	 * ModelでnotifyObservers()などが実行されるとupdateメソッドが実行される。
	 * 
	 * @param o
	 *            変更されたモデルのクラス 今回はModelクラス
	 * @param arg
	 *            notifyObservers実行時に引数を与えたものをargとして受け取ることができる。今回はなし
	 *            numberを与えてもよし
	 */
	@Override
	public void update(Observable o, Object arg) {
		Model model = (Model) o;
		// 変更されたmodelの数値を取得します。
		int number = model.getNumber();
		// 数値を文字列に変換します。
		String numberString = Integer.toString(number);
		// 数値をラベルにセットします。
		label.setText(numberString);
	}
}


今回はMVCモデルの実現のために、Observerパターンを利用しています。
Javaには標準でObsererインターフェースとObservableクラスが用意されているのでこれを利用しました。
もちろん自分でObserverパターンを実装しても問題はありませんし、MVCモデルだからといってObserberパターンでなければいけないということはありません。

  • 解説

コントローラーがviewとmodelを管理し、またviewとModelに対してObservable(subject)とObserverとして関連付けています。
またコントローラーには、OKボタンが押されたときに実行されるactionPerformedメソッドがあり、ここではmodelに対しての操作が呼び出されます。(ActionListenerもObserverパターンを用いて実装されています。WikipediaMVCモデルの画像の点線はObserverパターンによる実行を表していて、実線は参照を表していることがわかります。)
このときのmodelのincrementメソッドではnumberという数値をインクリメントしますが、これだけではModelのデータが変更されただけで、画面表示には反映されません。
この結果を画面に反映させたいと思いますが、model自身はViewクラスのインスタンスを保持していないため、直接更新することができません。
そこでデザインパターンであるObserverパターンを用いて、viewクラスにmodelに変更があったことを通知してあげます。setChangedメソッドとnotifyObserversメソッドがこれに当たります。
ViewクラスはModelクラスから変更通知を受け取ると、updateメソッドが実行されます。
ここで変更があったモデルを受け取り、Viewに反映するというのが一連の流れです。
これによりModelとViewが疎結合となりプログラム的にいいよね!という話みたいです。
まとめると、コントローラーはModelとViewを参照でき、ViewはModelを参照できます。


確かにこうなると綺麗ですよね というところで長くなったので、今日はここまで
明日こそはもう少しプログラムが複雑になったときの実装の疑問点について書いていきたいと思います。