MVCモデルの問題点を解決するPMモデルとMVPモデル

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

前回の続きです。
MVCモデルにはある問題が潜んでいると述べました。



問題点を述べる前に、MVCで作成されたコード例をを見てみましょう。
商品名、価格、在庫数が表示されており、購入を押すごとに在庫が減っていくという簡単なプログラムです。
今回はViewの振る舞いが重要になってくる話なので少しコードは長くなりますが、GUIで説明していきます。

MVCモデル(依存性を利用するMVC)

コード行数を節約するためにObserverは自分で作成するのではなくJavaで用意されているObserverインターフェースを利用します。

MVC


クラス図


実行イメージ


コード例
  • Mainクラス
public class Main {
	public static void main(String[] args) {
		Model model = new Model("GeekなCamera", 100, 10);
		View view = new View(model);
		Controller controller = new Controller(model);
		view.addToButtonActionListener((ActionListener) controller);
		model.addObserver(view);
	}
}
  • Controllerクラス
public class Controller implements ActionListener {
	private Model model;

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

	@Override
	public void actionPerformed(ActionEvent e) {
		model.sell();
	}
}
  • Modelクラス
public class Model extends Observable {
	private String name;
	private int price;
	private int stock;
	public Model(String name, int price, int stock) {
		this.name = name;
		this.price = price;
		this.stock = stock;
	}
	public void sell() {
		if (stock > 0) {
			stock--;
			// 以下の2行でModelに変更があったことをObserverに通知する
			setChanged();
			notifyObservers();
		}
	}
	public String getName() {
		return name;
	}
	public int getPrice() {
		return price;
	}
	public int getStock() {
		return stock;
	}
}
  • Viewクラス
public class View extends JFrame implements Observer {
	private JButton button;
	private JLabel label;
	private Model model;

	/**
	 * View Sampleクラスのコンストラクタ Viewの設定を行う。
	 */
	public View(Model model) {
		this.model = model;
		// ウィンドウが閉じられたときプログラムを終了するように設定します。
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		// コンポーネントの配置のレイアウトを指定します。
		this.setLayout(new FlowLayout());
		// JFrameに追加するラベルを定義します。
		label = new JLabel();
		this.setItemInformation();
		// JFrameに作成したラベルを追加します。
		this.add(label);
		// JFrameに追加するButtonの定義
		button = new JButton("購入");
		// ボタンが押されたとき呼び出される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) {
		this.setItemInformation();
	}

	private void setItemInformation() {
		// 変更されたmodelの情報を取得します。
		String name = model.getName() + " ";
		String price = "価格:$" + Integer.toString(model.getPrice()) + " ";
		String stock = "在庫数:" + Integer.toString(model.getStock());
		// 数値をラベルにセットします。
		label.setText(name + price + stock);
	}
}
MVCの問題点

その問題とは、MVCでは、Modelの値に対応してViewのデザインを変える場合に対応できない」ということです。
できないというと語弊があるかもしれませんが、MVCにはViewのデザインの状態を保持する場所がありません。
前回の通販システムで例えると、「在庫がなくなったら、個数を表示する背景を赤にしたい」ということにMVCでは対応できないということです。
この赤にしたいというものは、ドメインモデルに属するものではないのでModelにしてしまうのは違和感があります。
かといってModelではなくViewでこれを実現してしまうと、Viewがその処理と状態を保持することとなってしまいます。
どちらにしてもMVCとしては相応しくない実装です。

それらの解決するための方法が、PMモデルとMVPモデルになります。
これらはMVCの理想だけを追いかけず、現実的な問題を見据えたうえでの解決策です。妥協案ともいえます。


それぞれの解説はこちらを参考にしてください。

PMモデル

MVCモデルに対して、在庫が0になったら背景を赤で表示するプログラムを作成したいと思います。
MVCではこの振る舞いを記述する場所がありませんが、PMモデルでは、PMがその場所となります。

PM図


クラス図


実行イメージ
  • 在庫あり


  • 在庫切れ

まずViewと対となるPMは、Modelを監視する必要があります。仕組みとしてはModelがPMへ通知する形をとります。
このときModelは複数の表示形式に対応する必要があるためPMへ直接的に依存するわけにはいきません。そうしないとModelを使い回すことができなくなってしまいます。
ここではインターフェースを利用した間接的な参照を利用します。

またViewはPMを監視する必要があります。仕組みとしては、PMがViewへ通知する形をとります。
このときPMは複数の表示画面に対応する必要があるためModelへ直接的に依存するわけにはいきません。
ここではインターフェースを利用した間接的な参照を利用します。

ViewがModelの情報を取得する際には必ずPMを経由します。

コード例

今回のコード例では、CUIではなくGUIを用いて紹介します。
ModelからViewの通知機構は自前で用意するのではなくJavaで用意されているObserverインターフェースを利用します。

  • Mainクラス
public class Main {
	public static void main(String[] args) {
		Model model = new Model("GeekなCameraPM", 100, 10);
		// 通常の背景色は白
		Color normalStockBackground = new Color(255, 255, 255);
		// 在庫切れの背景色は赤
		Color noStockBackground = new Color(255, 0, 0);
		PresentationModel presentationModel = new PresentationModel(model,
				normalStockBackground, noStockBackground);

		View view = new View(presentationModel);

		Controller controller = new Controller(model);

		view.addToButtonActionListener((ActionListener) controller);

		presentationModel.addObserver(view);
		model.addObserver(presentationModel);
	}
}
  • Controllerクラス
public class Controller implements ActionListener {
	private Model model;

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

	@Override
	public void actionPerformed(ActionEvent e) {
		model.sell();
	}
}
  • Modelクラス
public class Model extends Observable {
	private String name;
	private int price;
	private int stock;
	public Model(String name, int price, int stock) {
		this.name = name;
		this.price = price;
		this.stock = stock;
	}
	public void sell() {
		if (stock > 0) {
			stock--;
			// 以下の2行でModelに変更があったことをObserverに通知する
			this.setChanged();
			this.notifyObservers();
		}
	}
	public String getName() {
		return name;
	}
	public int getPrice() {
		return price;
	}
	public int getStock() {
		return stock;
	}
}
  • PresentationModelクラス
public class PresentationModel extends Observable implements Observer {
	// Model
	private Model model;
	// PM
	// 在庫が0になったときの背景色
	private Color noStockBackground;
	// Viewへ返す背景色
	private Color stockBackground;

	public PresentationModel(Model model, Color normalStockBackground,
			Color noStockBackground) {
		this.model = model;
		this.noStockBackground = noStockBackground;
		this.stockBackground = normalStockBackground;
	}

	// PM
	public Color getBackground() {
		return stockBackground;
	}

	// Model
	public String getModelName() {
		return model.getName();
	}

	public int getModelPrice() {
		return model.getPrice();
	}

	public int getModelStock() {
		return model.getStock();
	}

	private void setBackGround() {
		if (model.getStock() < 1) {
			stockBackground = noStockBackground;
		}
		this.setChanged();
		this.notifyObservers();
	}

	@Override
	public void update(Observable arg0, Object arg1) {
		this.setBackGround();
	}
}
  • Viewクラス
public class View extends JFrame implements Observer {
	private JButton button;
	private JLabel label;
	private PresentationModel presentationModel;

	/**
	 * View Sampleクラスのコンストラクタ Viewの設定を行う。
	 */
	public View(PresentationModel presentationModel) {
		this.presentationModel = presentationModel;

		// ウィンドウが閉じられたときプログラムを終了するように設定します。
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		// コンポーネントの配置のレイアウトを指定します。
		this.setLayout(new FlowLayout());
		// JFrameに追加するラベルを定義します。
		label = new JLabel();
		this.setItemInformation();
		// 背景色を指定
		this.setBackGroundColor();
		// JFrameに作成したラベルを追加します。
		this.add(label);
		// JFrameに追加するButtonの定義
		button = new JButton("購入");
		// ボタンが押されたとき呼び出される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) {
		this.setItemInformation();
		this.setBackGroundColor();
	}

	private void setBackGroundColor() {
		label.setOpaque(true);
		label.setBackground(presentationModel.getBackground());
		this.repaint();
	}

	private void setItemInformation() {
		// 変更されたmodelの情報を取得します。
		String name = presentationModel.getModelName() + " ";
		String price = "価格:$"
				+ Integer.toString(presentationModel.getModelPrice()) + " ";
		String stock = "在庫数:"
				+ Integer.toString(presentationModel.getModelStock());
		// 数値をラベルにセットします。
		label.setText(name + price + stock);
	}
}

図の上だけで見ると論理的には綺麗でよさそうだなっと思いましたが、いざコードに落とし込んでみると処理を委譲しているところやobserver部分が少し煩雑ですね。

今回PMモデルをMVC+PMとして紹介しましたが、VCを1つにするという考えたかもあるようです。
私は、VとCは分ける方がクラスの責任が明確に分かれますので好きです。

疑問点

背景色を直接変更したくなった場合はどうするのでしょうか?(ボタンをクリックしたら背景を赤にするみたいな
ControllerからPMに線が伸びて依存するのかな?ん〜なんか更に複雑になってる・・・

PMがMをラップしないで、それぞれがViewの監視対象になるのはいけないのでしょうか(これの方がわかりやすくてコードもすっきりすると思うんですけど)



MVPモデル

続いてMVPモデルです。
MVPモデルはなんか更にPMモデルより更に妥協した感じです。

これの詳細の実装については自信がありませんが、こんな感じかと思われます。

  • Model
    • MVCモデルと同じ
  • View
    • MVCモデルと同じ
  • Presenter
    • MVCモデルのC+PMモデルのPM
MVP図


クラス図


実行イメージ
  • 在庫あり


  • 在庫切れ

コード例
  • Mainクラス
public class Main {
	public static void main(String[] args) {
		Model model = new Model("GeekなCameraPM", 100, 10);
		// 通常の背景色は白
		Color normalStockBackground = new Color(255, 255, 255);
		// 在庫切れの背景色は赤
		Color noStockBackground = new Color(255, 0, 0);

		View view = new View(model);

		Presenter presenter = new Presenter(model, view,
				normalStockBackground, noStockBackground);

		view.addToButtonActionListener((ActionListener) presenter);

		model.addObserver(view);
	}
}
  • Presentatorクラス
public class Presenter implements ActionListener {
	private Model model;
	private View view;
	// 在庫があるときの背景色
	private Color normalStockBackground;
	// 在庫が0になったときの背景色
	private Color noStockBackground;

	public Presenter(Model model, View view, Color normalStockBackground,
			Color noStockBackground) {
		this.model = model;
		this.view = view;
		this.noStockBackground = noStockBackground;
		this.normalStockBackground = normalStockBackground;
		view.setBackGroundColor(getBackGround());
	}

	private Color getBackGround() {
		Color color = normalStockBackground;
		if (model.getStock() < 1) {
			color = noStockBackground;
		}
		return color;
	}

	@Override
	public void actionPerformed(ActionEvent e) {
		model.sell();
		view.setBackGroundColor(getBackGround());
	}
}
  • Modelクラス
public class Model extends Observable {
	private String name;
	private int price;
	private int stock;
	public Model(String name, int price, int stock) {
		this.name = name;
		this.price = price;
		this.stock = stock;
	}
	public void sell() {
		if (stock > 0) {
			stock--;
			// 以下の2行でModelに変更があったことをObserverに通知する
			this.setChanged();
			this.notifyObservers();
		}
	}
	public String getName() {
		return name;
	}
	public int getPrice() {
		return price;
	}
	public int getStock() {
		return stock;
	}
}
  • Viewクラス
public class View extends JFrame implements Observer {
	private JButton button;
	private JLabel label;
	private Model model;

	/**
	 * View Sampleクラスのコンストラクタ Viewの設定を行う。
	 */
	public View(Model model) {
		this.model = model;
		// ウィンドウが閉じられたときプログラムを終了するように設定します。
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		// コンポーネントの配置のレイアウトを指定します。
		this.setLayout(new FlowLayout());
		// JFrameに追加するラベルを定義します。
		label = new JLabel();
		this.setItemInformation();
		// JFrameに作成したラベルを追加します。
		this.add(label);
		// JFrameに追加するButtonの定義
		button = new JButton("購入");
		// ボタンが押されたとき呼び出される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) {
		this.setItemInformation();
	}

	

	private void setItemInformation() {
		// 変更されたmodelの情報を取得します。
		String name = model.getName() + " ";
		String price = "価格:$" + Integer.toString(model.getPrice()) + " ";
		String stock = "在庫数:" + Integer.toString(model.getStock());
		// 数値をラベルにセットします。
		label.setText(name + price + stock);
	}
	
	public void setBackGroundColor(Color backGroundColor) {
		label.setOpaque(true);
		label.setBackground(backGroundColor);
		this.repaint();
	}
}


MVCのCと比べてMVPのPは大忙しです
Presenterのコンストラクタにmodelとviewと背景情報を渡しているのがん〜・・・

まとめ

MVCの問題点を解決する2つの方法を紹介しました。
それぞれの解決策は以下のようなものでした。

  • PMモデルは、振る舞い専用のModelを作成(PresentationModel)
  • MVPモデルは、振る舞いをMVCでいうControllerが持つ(Presenter)


図を見て頂ければわかると思いますが、依存関係や間接参照の線がMVCと比較するとどうしても複雑になってしまっています。
できることならMVCを採用していくことが理想です。
しかし実際問題GUIを作成していく上でModelに対応してViewを変更するなんてことは頻繁に発生すると思います。
そのときはこの記事を思い出しくれると嬉しいです。

おわり

長々と読んで頂いた方には申し訳ないのですが、この認識で正しいかどうか怪しいです。是非勉強した暁にはこちらのコメントで間違いを指摘してください。

参考