プログラマーが意識するべきUI設計指針 3つのMVCモデル

数ヶ月前の記事に引き続きMVCモデルについてのまとめです。

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

MVCモデルというものはすごくやっかいで、人によって言っていることが違います。
なのでこれが必ず正解!というものはないと思いますが、個人的に勉強した中でこれが正解であって欲しいなっていうものをまとめてみたいと思います。


今回も実際のコードを例に挙げていきますが、GUIではなくCUIで簡易的な実装で紹介していきたいと思います。
コードの例題としては、ネットショップの商品の商品名、金額、在庫数が表示するページ(というには貧弱過ぎる何か)を想定していきます。

MVCの種類について

MVCモデルには大きく分けて以下の3種類あります。

  1. コントローラが頑張るMVC
  2. 依存性を利用するMVC
  3. プラガブルを利用するMVC



0. MVCを使わない場合

まずはMVCを使わないコードの紹介します。

実行結果
  • 以降すべて同じ実行結果になります。

商品名 = GeekなCamera
価格 = 10000円
在庫数 = 10個

商品名 = GeekなCamera
価格 = 10000円
在庫数 = 9個

コード例

今回のコード例すべてにおいてViewからControllerへのイベントについてはコードが複雑になることを避けるため省略しています。
代わりにmainメソッドからイベント発生時に呼び出されるはずのControllerのメソッドを意図的に呼び出しています。

public class Main {
	public static void main(String[] args) {
		// 初期化
		ItemModel itemModel = new ItemModel("GeekなCamera", 10000, 10);
		Controller controller = new Controller(itemModel);
		// 購入ボタンが押されたことを通知
		controller.sellListener();
	}
}

public class Controller {
	private ItemModel itemModel;
	public Controller(ItemModel itemModel) {
		this.itemModel = itemModel;
	}
	public void sellListener() {
		itemModel.sell();
		itemModel.view();
	}
}

public class ItemModel {
	// 商品名
	private String name;
	// 価格
	private int price;
	// 在庫数
	private int stock;

	public ItemModel(String name, int price, int stock) {
		this.name = name;
		this.price = price;
		this.stock = stock;
		this.view();
	}

	public void sell() {
		stock--;
	}

	public void view() {
		System.out.println("商品名 = " + name);
		System.out.println("価格 = " + price + "円");
		System.out.println("在庫数 = " + stock + "個");
		System.out.println();
	}
}
問題点

ロジック部分と表示部分が1つのクラスに実装されているため、他の表示方法を実装する際にモデル部分を再利用することができません。
また表示部分を修正した場合にも、モデル部分が同一クラスのためコンパイルをしなおす必要があります。
実装時の作業分担も難しくなります。


以上のような問題をMVCに即した実装をすることにより解決していきたいと思います。


1. コントローラが頑張るMVC

これは名前の通りコントローラがすごく頑張ります。
0のMVCを利用しない場合では、表示処理をモデル自身が行っていましたが、今回はViewという専用の表示クラスに処理を任せています。
WebでいうMVCは1もしくは1.5のMVCのことを指すものだと思います。


まず大事なのは、MVCモデルとは、Modelの使い回しを考えた物であって、Viewの使い回しを考えた物ではないと言うことです。
そのためViewがModelへ依存することは問題ありません。あるModelを変更したときに、そのModelに依存するViewが他の用途に使うことができないのは当然です。


具体的には、商品の在庫状況を表示するためのViewをGoogleMapを表示するためのViewとして使うことが出来るわけがありません。これは必要とするModel(データやロジック)が商品の在庫状況表示とGoogleMapの表示ではまったく異なるためです。。
しかし商品の在庫状況を表示するためのModelは、他の表現形式のViewに対して使い回すことが可能です。例えばあるデータを円グラフで表示していたものを棒グラフで表示するということはOffice製品を使っているとよくあることではないでしょうか。
もしExcelで作ったデータを棒グラフで1度表示したら、2度と円グラフに変更できない。と言われたら困ってしまいます。


クラスに直接依存するといことは、依存元のクラスは使い回すことができないというのと同義です。逆に依存される側は、そのしがらみとは関係ありません。
色んなところから存分に依存されちゃっていいわけです。これが再利用ということになります。

点線はイベント通知、実線はメソッドや関数の実行を表します。
イベントとはJavaではインターフェースを越しの間接的なメソッド実行ですが、具体的に言語文法として定義されているものではなく概念の1つです。

コード例

図の番号とコードのコメントの番号が対応しています。

public class Main {
	public static void main(String[] args) {
		// 初期化
		ItemModel itemModel = new ItemModel("GeekなCamera", 10000, 10);
		ItemView itemView = new ItemView(itemModel);
		Controller controller = new Controller(itemModel, itemView);

		// 1 購入ボタンが押されたことを通知
		controller.sellListener();
	}
}

public class Controller {
	private ItemModel itemModel;
	private ItemView itemView;

	public Controller(ItemModel itemModel, ItemView itemView) {
		this.itemModel = itemModel;
		this.itemView = itemView;
	}

	public void sellListener() {
		// 2 在庫数を取得して、在庫数を減らす
		int stock = itemModel.getStock();
		stock--;
		// 3 減らした在庫数を更新
		itemModel.setStock(stock);

		// 4 購入されたことを通知
		itemView.view();
	}
}

public class ItemModel {
	// 商品名
	private String name;
	// 価格
	private int price;
	// 在庫数
	private int stock;

	public ItemModel(String name, int price, int stock) {
		this.name = name;
		this.price = price;
		this.stock = stock;
	}

	// getter省略

	public void setStock(int stock) {
		this.stock = stock;
	}
}

public class ItemView {
	private ItemModel itemModel;

	public ItemView(ItemModel itemModel) {
		this.itemModel = itemModel;
		this.view();
	}
	public void view() {
		// 5 表示する在庫数を取得
		System.out.println("商品名 = " + itemModel.getName());
		System.out.println("価格 = " + itemModel.getPrice() + "円");
		System.out.println("在庫数 = " + itemModel.getStock() + "個");
	}
}
コントローラの頑張り
  • a. 入力の通知を受け取ります。(図の1 購入ボタンが押されたことを通知 V→C)
  • b. 入力に従いモデルに変更を加えます。(図の2 在庫数を取得して、在庫数を減らす 3 減らした在庫数を更新 C→M)←ここがすごく頑張っているポイント
  • c. モデルの変更Viewに通知します。(図の4 購入されたことを通知 C→V)←ここが頑張っているポイント

以上の3つがコントローラのお仕事です。
bではコントローラーが直接モデルの値を変更しています。
オブジェクト指向としては、カプセル化が生かされていないあまり良くない実装です。
そこで私は「1. コントローラが頑張るMVC」と「2. 依存性を利用するMVC」の間にbの問題を解決した「1.5. コントローラーが少し頑張るMVC」というものがあると考えています。



1.5. 少しコントローラが頑張るMVC

これは1. コントローラが頑張るMVCに対して少しだけ、コントローラの頑張りが減った物です。
WebでいうMVCは1もしくは1.5のMVCのことを指すものだと思います。


コード例
public class Main {
	public static void main(String[] args) {
		// 初期化
		ItemModel itemModel = new ItemModel("GeekなCamera", 10000, 10);
		ItemView itemView = new ItemView(itemModel);
		Controller controller = new Controller(itemModel, itemView);

		// 1 購入ボタンが押されたことを通知
		controller.sellListener();
	}
}

public class Controller {
	private ItemModel itemModel;
	private ItemView itemView;

	public Controller(ItemModel itemModel, ItemView itemView) {
		this.itemModel = itemModel;
		this.itemView = itemView;
		// 初期状態を表示
		itemView.view();
	}

	public void sellListener() {
		// 2 購入されたことを通知
		itemModel.sell();
		// 3 購入されたことを通知
		itemView.view();
	}
}

public class ItemModel {
	// 商品名
	private String name;
	// 価格
	private int price;
	// 在庫数
	private int stock;

	public ItemModel(String name, int price, int stock) {
		this.name = name;
		this.price = price;
		this.stock = stock;
	}

	public void sell() {
		stock--;
	}

	// getter省略
}

public class ItemView {
	private ItemModel itemModel;

	public ItemView(ItemModel itemModel) {
		this.itemModel = itemModel;
	}

	public void view() {
		// 4 表示する商品名、価格、在庫数を取得
		System.out.println("商品名 = " + itemModel.getName());
		System.out.println("価格 = " + itemModel.getPrice() + "円");
		System.out.println("在庫数 = " + itemModel.getStock() + "個");
		System.out.println();
	}
}
コントローラの頑張り
  • a. 入力の通知を受け取ります。(図の1購入ボタンが押されたことを通知 V→C)
  • b. 入力があったことを通知します。(図の2購入されたことを通知 C→M)←ここが頑張らなくなったところ
  • c. モデルの変更Viewに通知します。(図の3購入されたことを通知 C→V)←ここが頑張っているポイント

「1. コントローラが頑張るMVC」では、bでコントローラーが直接モデルの値を取得してから変更を加えて設定していましたが、今回の方式ではコントローラはモデルの処理を実行するだけで、直接値は参照せずモデルの変更をモデル自身に行わせるようになりました。
1と1.5の違いはMVCの違いというよりかは、トランザクションスクリプト(手続き型+構造体)かドメインモデル(オブジェクト指向)かという違いです。



2. 依存性を利用するMVC

これは「1.5. 少しコントローラが頑張るMVC」に比べて更にコントローラの頑張りが減ります。
GUIのMVCモデルというとこれを指すことが多いのではないかと思います。もちろんGUIでも1.や1.5のMVCを採用することも可能です。

点線はインターフェースによる間接的な参照

モデルのインターフェースも図示するとこんな感じ

コード例
public class Main {
	public static void main(String[] args) {
		// 初期化
		ItemModel itemModel = new ItemModel("GeekなCamera", 10000, 10);
		ViewObserver itemView = new ItemView();
		((ItemSubject) itemModel).addObserver(itemView);
		Controller controller = new Controller((ItemLogic) itemModel);

		// 1 購入ボタンが押されたことを通知
		controller.sellListener();
	}
}

public class Controller {
	private ItemLogic itemLogic;
	public Controller(ItemLogic itemLogic) {
		this.itemLogic = itemLogic;
	}
	public void sellListener() {
		// 2 購入されたことを通知
		itemLogic.sell();
	}
}

public interface ItemLogic {
	public void sell();
}

public interface ItemSubject {
	public void addObserver(ViewObserver observer);
	public void notifyObservers();
	public String getName();
	public int getPrice();
	public int getStock();
}

public class ItemModel implements ItemSubject, ItemLogic {
	// 商品名
	private String name;
	// 価格
	private int price;
	// 在庫数
	private int stock;
	// 通知先のViewリスト
	private List<ViewObserver> observerList;

	public ItemModel(String name, int price, int stock) {
		this.name = name;
		this.price = price;
		this.stock = stock;
		observerList = new ArrayList<ViewObserver>();
	}

	@Override
	public void sell() {
		stock--;
		// 3 モデルが変更されたことを通知
		this.notifyObservers();
	}

	//getter省略

	@Override
	public void addObserver(ViewObserver observer) {
		observerList.add(observer);
		this.notifyObservers();
	}

	@Override
	public void notifyObservers() {
		for (ViewObserver observer : observerList) {
			observer.update(this);
		}
	}
}

public interface ViewObserver {
	public void update(ItemModel itemModel);
}

public class ItemView implements ViewObserver {
	private ItemModel itemModel;

	public void view() {
		// 4 表示する商品名、価格、在庫数を取得
		System.out.println("商品名 = " + itemModel.getName());
		System.out.println("価格 = " + itemModel.getPrice() + "円");
		System.out.println("在庫数 = " + itemModel.getStock() + "個");
		System.out.println();
	}

	@Override
	public void update(ItemModel itemModel) {
		this.itemModel = itemModel;
		this.view();
	}
}
コントローラの頑張り
  • a. 入力の通知を受け取ります。(図の1購入ボタンが押されたことを通知 V→C)
  • b. 入力があったことを通知します。(図の2購入されたことを通知 C→M)

先ほど1.5では頑張っていた「c. モデルの変更通知をViewに伝えます。」がなくなっています。
この仕事はどこにいったのでしょうか?
答えはモデルさんが代わりにしてくれるようになりました。
モデルは、変更があると登録されているViewに対して変更通知を送るようになっています。


Observerパターン
このモデルがViewに変更通知を送る仕組みはデザインパターンのObserverパターンが利用されています。
まず最初に初期化の段階で、モデルに対して変更があったとき情報を受け取りたいViewを登録します。
そして実際にモデルに変更があった場合、すべてのViewに対して通知を行います。


Observerパターンを利用するメリット
これによるメリットとしては、Controllerが完全にViewに関与しなくなります。
また1.5までは、ControllerからModelに変更を加えたとしても、ControllerからViewにModelが変更したことを伝え忘れてしまうとViewが更新されませんでしたが、
2ではModelが自動的に変更をViewに伝えるため即座にViewが更新されるようになりました。


インターフェースの分離
また2のインターフェースも示した図を見るとわかるとおり、ControllerはModelの売る処理以外は関与していません。(Controller→ItemLogic)
またViewはModelのobserver関連のメソッドと値の取得意外には関与していません。
このようにインターフェースを分けることにより、結合度が下がりより良い設計となるのではないかと思います。
(更に厳密に分けるならObserver関連のメソッドと値取得のメソッドのインターフェースを区別してもよかったかもしれません)



3. プラガブルを利用するMVC

これについてはよくわかりませんでしたorz
下記に挙げている参考サイトでは図やコードで詳しく解説されているのですが、smalltalkを読むことができないため挫折しましたorz



まとめ

Model、View、Controllerとそれぞれ別のクラスとして実装することにより独立性が上がりました。
このようにクラスが独立しているとクラスの責務が明確になり再利用性、拡張性、可動性が向上します。
これにより仕様変更や拡張時の影響範囲を最小限にとどめることができるようになり、さらに作業の分担が容易になるというメリットもあります。
しかしこれらのMVCモデルにはある問題が潜んでいます。
次回はその問題を解決できるMVCモデルから派生したモデルについてまとめてみたいと思います。

参考