概要
全ての機能を一つのクラスの中に書くと煩雑になり,メンテナンス性可読性が非常に低下する.そこで機能ごとにクラスを分けて,その間の通信によりアプリケーション全体を機能させる,という設計手法がある.本稿では一般的なModel-View-Controllerに基づく分離・設計手法,Model-ViewControllerに基づく分離・設計手法について述べる.
一番基本的なMVC
概要
一番基本的なMVCとは,「データを保持,処理するModel」,「Modelから受け取ったデータから表示内容を作り表示するView」,「ユーザの入力を受け取り適したデータに変換してModelに送るContoller」の3要素から構成されるプログラムの設計である.
オブジェクト指向プログラミングでは,このModel-View-Contollerの3要素をそれぞれ別のクラスで表現し,お互いに通信(関数を呼んだり,ネットワークメッセージを送る,オブジェクト指向的には「関数を呼ぶ」のも「メッセージを送る」のも同義.)している.基本的にModel, View, Controllerの3要素はデータを「共有」せず,データの中身も「メッセージ」として送ることで,お互いにデータをやり取りする.

コード
以下のコードでは,メインのクラスをModelとし,そのインナークラスとしてViewとControllerを実装し,お互いの通信専用関数を呼ぶことで通信を行っている.(Javaではメッセージは「そのオブジェクトの関数を呼ぶ」ことでのみ表現可能である.Small-TalkやObjective-C等の言語だとそのまま「メッセージを送る」.) BallのModelであるBallModelクラスは外部クラスとして実装している.
アニメーションするかどうかを決めるboolean型のinAnimation変数はModelが保持していることに注意.Viewは常に描画し続けているが,Model側でBallModelのupdateParameter()が実行されないと位置等のパラメータは更新されない.Modelでパラメータが更新されると同時にModelはViewへメッセージを送る.つまりModelがViewに対しパラメータを「プッシュ更新」している状態である.
Model, View, Contollerの全てが独立したframeRateを持ち独立して動いているが,パラメータの更新,すなわちアプリケーション全体を駆動しているframeRateはModelが基準になっている.View側のframeRateはあくまで描画のframeRateであることに注意.具体的にはView側のframeRateは残像にのみ影響してくる.「Step」ボタンで確認すること.

以下の実例コードの動作模式図は上記のとおりである.
//MVC_M_V_C.pde
import processing.awt.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
// in Main Scketch Class, Model is written.
// メインスケッチクラスにはModelを定義する.
MVC_View viewWindow;
MVC_Controller controllerWindow;
BallModel ballModelArray[];
boolean inAnimation;
void setup(){
String args[] = {"MVC_View"};
viewWindow = new MVC_View(640, 480);
PApplet.runSketch(args, viewWindow);
controllerWindow = new MVC_Controller(this);
ballModelArray = new BallModel[10];
for(int i=0; i<ballModelArray.length; i++){
ballModelArray[i] = new BallModel(640, 480);
viewWindow.setMessageFromModelToView
(i, ballModelArray[i].getBallPosition(),
ballModelArray[i].getBallColor(),
ballModelArray[i].getBallSize());
}
// このフレームレートがアプリケーション全体を駆動するフレームレートである
frameRate(10);
inAnimation = false;
}
void draw(){
// 上記のフレームレートで呼ばれるdraw関数が,アプリケーション全体を駆動している
if(inAnimation){
for(int i=0; i<ballModelArray.length; i++){
ballModelArray[i].updateParameters();
viewWindow.setMessageFromModelToView
(i, ballModelArray[i].getBallPosition(),
ballModelArray[i].getBallColor(),
ballModelArray[i].getBallSize());
}
}
}
void setMessageFromControllerToModel(String messageFromController){
if(messageFromController.equals("start"))
inAnimation = true;
else if(messageFromController.equals("stop"))
inAnimation = false;
else if(messageFromController.equals("step")){
for(int i=0; i<ballModelArray.length; i++){
ballModelArray[i].updateParameters();
viewWindow.setMessageFromModelToView
(i, ballModelArray[i].getBallPosition(),
ballModelArray[i].getBallColor(),
ballModelArray[i].getBallSize());
}
}
else if(messageFromController.equals("reset")){
for(int i=0; i<ballModelArray.length; i++){
ballModelArray[i] = new BallModel(640, 480);
viewWindow.setMessageFromModelToView
(i, ballModelArray[i].getBallPosition(),
ballModelArray[i].getBallColor(),
ballModelArray[i].getBallSize());
}
}
}
// View用インナークラス ----------------------------------------------------------
class MVC_View extends PApplet{
// Class for View
// Viewのためのインナークラス
Point pointArray[]; // java.awt.Pointクラス
Color colorArray[]; // java.awt.Colorクラス
int sizeArray[];
int windowWidth, windowHeight;
MVC_View(int windowWidth, int windowHeight){
// Constructor コンストラクタ
// 配列等インスタンス化を伴う変数はここでインスタンス化をしておかないと,エラーになる.
// Modelが先に動き始めてメッセージを送り出すので,
// Viewのsetup()が呼ばれた時には既に遅く,ぬるぽになる.
super();
this.windowWidth = windowWidth;
this.windowHeight = windowHeight;
pointArray = new Point[10];
colorArray = new Color[10];
sizeArray = new int[10];
}
@Override
public void settings(){
this.size(this.windowWidth, this.windowHeight);
}
@Override
void setup(){
// このフレームレートはView側のみ制御している
this.frameRate(10);
this.colorMode(RGB, 255);
}
@Override
void draw(){
this.fill(255, 255, 255, 60);
this.rect(0, 0, windowWidth, windowHeight);
for(int i=0; i<pointArray.length; i++){
if(pointArray[i] != null){
this.fill(colorArray[i].getRed(),
colorArray[i].getGreen(),
colorArray[i].getBlue());
this.ellipse((float)pointArray[i].getX(), (float)pointArray[i].getY(),
sizeArray[i], sizeArray[i]);
}
}
}
void setMessageFromModelToView(int index, Point ballPoint, Color ballColor,
int ballSize){
pointArray[index] = ballPoint;
colorArray[index] = ballColor;
sizeArray[index] = ballSize;
}
}
// Controller用インナークラス ----------------------------------------------------
class MVC_Controller extends JFrame implements ActionListener{
// Class for Controller
// Controllerのためのインナークラス
// JFrameを継承してActionListenerインタフェースを実装したもの.
// 実際のSwingアプリケーションでは,
// このようにJFrameを「継承」してかつActionListenerを「実装」する,
// つまり多重継承したJFrameの子クラスを定義することが多い.
MVC_M_V_C modelWindow;
JPanel panel;
// JPanel is typical content pane class for Swing
// Normally, generate JPanel instance,
// then put the panel instance into JFrame's contentPane
// JPanelというのはSwingの基本となる「台紙」のようなものである.
// このJPanelにレイアウトを指定してGUI部品を貼り付けていき,
// 最後にJFrameやJFrameを継承したクラス(ここではthis)にそのJPanelを貼り付ける.
MVC_Controller(MVC_M_V_C modelWindow){
super("Controller");
this.modelWindow = modelWindow;
this.setSize(320, 240);
panel = new JPanel();
panel.setLayout(new GridLayout(0, 2)); // Grid Layout: *x2
JButton startButton = new JButton("Start");
startButton.setActionCommand("start");
startButton.addActionListener(this);
panel.add(startButton);
JButton stopButton = new JButton("Stop");
stopButton.setActionCommand("stop");
stopButton.addActionListener(this);
panel.add(stopButton);
JButton stepButton = new JButton("Step");
stepButton.setActionCommand("step");
stepButton.addActionListener(this);
panel.add(stepButton);
JButton resetButton = new JButton("Reset");
resetButton.setActionCommand("reset");
resetButton.addActionListener(this);
panel.add(resetButton);
this.add(panel);
this.setVisible(true);
}
@Override
void actionPerformed(ActionEvent e){
String actionCommand = e.getActionCommand();
modelWindow.setMessageFromControllerToModel(actionCommand);
}
}
// BallModel.pde
import java.awt.Point;
import java.awt.Color;
class BallModel {
int windowWidth, windowHeight;
int ballSize;
int xPos, yPos;
int xDirection, yDirection;
int xSpeed, ySpeed;
int rColor, bColor, gColor;
BallModel(int windowWidth, int windowHeight){
//Constructor
this.windowWidth = windowWidth;
this.windowHeight = windowHeight;
ballSize = 30;
xPos = (int)random(windowWidth);
yPos = (int)random(windowHeight);
if(random(100) > 50)
xDirection = 1;
else
xDirection = -1;
if(random(100) > 50)
yDirection = 1;
else
yDirection = -1;
xSpeed = (int)random(10) + 5;
ySpeed = (int)random(10) + 5;
rColor = (int)random(100) + 155;
bColor = (int)random(100) + 155;
gColor = (int)random(100) + 155;
}
void updateParameters(){
xPos += xSpeed * xDirection;
yPos += ySpeed * yDirection;
if(xPos > windowWidth)
xDirection = -1;
else if(xPos < 0)
xDirection = 1;
if(yPos > windowHeight)
yDirection = -1;
else if(yPos < 0)
yDirection = 1;
}
int getBallSize(){
return ballSize;
}
Point getBallPosition(){
return new Point(xPos, yPos);
}
Color getBallColor(){
return new Color(rColor, gColor, bColor);
}
}
Webブラウザアプリに対応したMVC(プル更新)
概要
ところがWebブラウザで動くアプリケーションが主流になり,Modelつまりデータ処理とデータを格納するためのデータベースがWebサーバ上に置かれるようになってくると,HTTPプロトコルの制限からControllerが表示用の処理も担当しViewに命令を送る形に変化することとなる.
HTTPプロトコルの制限とは,Webブラウザはデータをサーバから(データ送信と同時に)GETする指令しか出せず,サーバは任意のタイミングでブラウザにデータを送れない,というものである.このためContollerがModelに向けてデータを送ると同時に,Modelで処理された結果を受け取り,それをViewに転送する形をとる.

コード
BallModelクラスのソースコードは前のものと一緒なので,省略する.
今度はメインスケッチクラスをControllerとし,そこからModelとViewをインナークラスとして生成し,Contollerが主体となって(Controllerの時間で)通信を制御している.一つ前の例との設計の違いをよく考える必要がある.
今度はアニメーションするかどうかを決めるboolean型inAnimation変数はControllerが保持している.またアプリケーション全体を駆動するframeRateはContollerのものが使われる.
つまり今度は,ControllerがModelに対して(Ballのパラメータの)更新を促すメッセージを送り(ControllerがModelのupdateParameters()関数を呼んでいる),そのメッセージの結果としてPosition, Color, Size等のパラメータを(戻り値として)受け取っている.このようにModelに対して働きかけその結果として更新されたModelの情報を得ることを「プル更新」と呼ぶ.前の「プッシュ更新」に対応する言葉である.
プル更新で,戻り値として描画に必要な全ての情報をまとめて一つの戻り値として受け取れるようにするため,BallInfoForViewというクラスを追加しているが,コードで分かる通り,ただ位置情報,色情報,サイズ情報をまとめて(配列として)取り出せるようにしただけである.(クラスではあるがコンストラクタとget系以外のメンバ関数を持たないので,実質的にC言語で言うところの構造体である)

以下の実例コードの動作模式図は上記のようになる.
// MVC_PullMVC.pde
import processing.awt.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
//in Main Scketch, Controller is written
MVC_Model model;
MVC_View viewWindow;
boolean inAnimation;
void settings(){
this.size(320, 240);
}
void setup(){
String args[] = {"MVC_View"};
viewWindow = new MVC_View(640, 480);
PApplet.runSketch(args, viewWindow);
model = new MVC_Model(640, 480);
this.frameRate(10);
inAnimation = false;
JPanel panel = new JPanel();
panel.setBounds(0, 0, width, height);
panel.setLayout(new GridLayout(0, 2));
ControllerActionListener controllerActionListener =
new ControllerActionListener(this);
JButton startButton = new JButton("Start");
startButton.setActionCommand("start");
startButton.addActionListener(controllerActionListener);
panel.add(startButton);
JButton stopButton = new JButton("Stop");
stopButton.setActionCommand("stop");
stopButton.addActionListener(controllerActionListener);
panel.add(stopButton);
JButton stepButton = new JButton("Step");
stepButton.setActionCommand("step");
stepButton.addActionListener(controllerActionListener);
panel.add(stepButton);
JButton resetButton = new JButton("Reset");
resetButton.setActionCommand("reset");
resetButton.addActionListener(controllerActionListener);
panel.add(resetButton);
Canvas canvas = (Canvas)surface.getNative();
JLayeredPane pane = (JLayeredPane)canvas.getParent().getParent();
pane.add(panel);
}
void draw(){
if(inAnimation){
getOneStepUpdatedInfoThenSendToView();
}
}
void getOneStepUpdatedInfoThenSendToView(){
BallInfoForView[] updatedBallInfoForViewArray =
model.updateParametersThenGetBallInfo();
for(int i=0; i<updatedBallInfoForViewArray.length; i++)
viewWindow.setMessageFromControllerToView(i,
updatedBallInfoForViewArray[i].getBallPosition(),
updatedBallInfoForViewArray[i].getBallColor(),
updatedBallInfoForViewArray[i].getBallSize());
}
void sendResetMessageFromControllerToModelThenSendToView(){
BallInfoForView[] resettedBallInfoForViewArray =
model.resetBallModel();
for(int i=0; i<resettedBallInfoForViewArray.length; i++)
viewWindow.setMessageFromControllerToView(i,
resettedBallInfoForViewArray[i].getBallPosition(),
resettedBallInfoForViewArray[i].getBallColor(),
resettedBallInfoForViewArray[i].getBallSize());
}
// メインスケッチクラスからActionListenerを使うためのインナークラス -------------------
class ControllerActionListener implements ActionListener{
MVC_PullMVC controllerWindow;
ControllerActionListener(MVC_PullMVC controllerWindow){
this.controllerWindow = controllerWindow;
}
@Override
public void actionPerformed(ActionEvent e){
if(e.getActionCommand().equals("start"))
inAnimation = true;
else if(e.getActionCommand().equals("stop"))
inAnimation = false;
else if(e.getActionCommand().equals("step"))
this.controllerWindow.getOneStepUpdatedInfoThenSendToView();
else if(e.getActionCommand().equals("reset"))
this.controllerWindow.sendResetMessageFromControllerToModelThenSendToView();
}
}
// Model用インナークラス ---------------------------------------------------------
class MVC_Model{
BallModel ballModelArray[];
int windowWidth, windowHeight;
MVC_Model(int windowWidth, int windowHeight){
this.windowWidth = windowWidth;
this.windowHeight = windowHeight;
ballModelArray = new BallModel[10];
for(int i=0; i<ballModelArray.length; i++)
ballModelArray[i] = new BallModel(this.windowWidth, this.windowHeight);
}
BallInfoForView[] updateParametersThenGetBallInfo(){
BallInfoForView[] ballInfoForViewArray =
new BallInfoForView[ballModelArray.length];
for(int i=0; i<ballModelArray.length; i++){
ballModelArray[i].updateParameters();
ballInfoForViewArray[i] = new BallInfoForView
(ballModelArray[i].getBallPosition(),
ballModelArray[i].getBallColor(),
ballModelArray[i].getBallSize());
}
return ballInfoForViewArray;
}
BallInfoForView[] resetBallModel(){
BallInfoForView[] ballInfoForViewArray =
new BallInfoForView[ballModelArray.length];
for(int i=0; i<ballModelArray.length; i++){
ballModelArray[i] = new BallModel(this.windowWidth, this.windowHeight);
ballInfoForViewArray[i] =
new BallInfoForView(ballModelArray[i].getBallPosition(),
ballModelArray[i].getBallColor(),
ballModelArray[i].getBallSize());
}
return ballInfoForViewArray;
}
}
// View用インナークラス ----------------------------------------------------------
class MVC_View extends PApplet{
int windowWidth, windowHeight;
Point pointArray[];
Color colorArray[];
int sizeArray[];
MVC_View(int windowWidth, int windowHeight){
//Constructor
super();
this.windowWidth = windowWidth;
this.windowHeight = windowHeight;
pointArray = new Point[10];
colorArray = new Color[10];
sizeArray = new int[10];
}
@Override
void settings(){
this.size(this.windowWidth, this.windowHeight);
}
@Override
void setup(){
this.frameRate(10);
this.colorMode(RGB, 255);
}
@Override
void draw(){
this.fill(255, 255, 255, 60);
rect(0, 0, this.windowWidth, this.windowHeight);
for(int i=0; i<pointArray.length; i++){
if(pointArray[i] != null){
Color currentBallColor = colorArray[i];
this.fill(currentBallColor.getRed(),
currentBallColor.getGreen(),
currentBallColor.getBlue());
this.ellipse((float)pointArray[i].getX(), (float)pointArray[i].getY(),
sizeArray[i], sizeArray[i]);
}
}
}
void setMessageFromControllerToView
(int index, Point ballPoint, Color ballColor, int ballSize){
pointArray[index] = ballPoint;
colorArray[index] = ballColor;
sizeArray[index] = ballSize;
}
}
// BallInfoForView.pde
class BallInfoForView{
Point ballPoint;
Color ballColor;
int ballSize;
BallInfoForView(Point ballPoint, Color ballColor, int ballSize){
this.ballPoint = ballPoint;
this.ballColor = ballColor;
this.ballSize = ballSize;
}
Point getBallPosition(){
return ballPoint;
}
Color getBallColor(){
return ballColor;
}
int getBallSize(){
return ballSize;
}
}
M-VC, Document-View
概要
さらにWebブラウザアプリケーションが複雑化してくると,表示と入力の処理を分離すると設計上無理が生じるようになってしまい,ViewとControllerを一体化させたM-VCが登場する.これは別名Document-Viewとも呼ばれる.Processingやタブレット用のアプリケーションではむしろこちらが自然であるとも言える.

コード
DocumentViewのコード例ではModelの部分を外部クラスとして宣言している(とは言っても,内部クラスの時と全く変わらない) BallModelクラスの定義については全く同じなので省略する.
今までは「関数を呼ぶ」ことでメッセージを送ってきたが,今度はString文字列クラスのオブジェクトにコマンドやパラメータを入れて送り,受け取っている.基本的にView側のdraw()でメッセージを送りそれと同時に受け取っているので「プル更新」である.
コマンドやパラメータは一つの文字列で済むように文字列を連結し,その間に「/」を挟んでいる.受け取った側がsplitでそれを分割し,Stringクラスのインスタンス関数equals()を使ってコマンドを判別し,Integerクラスのstatic関数parseInt()を使い,メッセージ文字列中のパラメータを文字列から数字に変換して利用している.

// MVC_DocumentView.pde
// スケッチのメインクラスであり,
// DocumentViewでの表示入力部分を担当する「ViewController」
import processing.awt.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
MVC_DocumentView_Model model;
DocumentViewActionListener documentViewActionListener;
boolean inAnimation;
void setup(){
size(640, 480);
model = new MVC_DocumentView_Model(width, height);
documentViewActionListener = new DocumentViewActionListener(this);
inAnimation = false;
this.frameRate(10);
// Processingのウィンドウの上部50px分の帯のJPanelを作り,そこにボタンを貼り付けていく.
// 先にJPanelでContentPaneを作っているので,レイアウトは自由自在
JPanel panel = new JPanel();
panel.setBounds(0, 0, 640, 50);
panel.setLayout(new GridLayout(0, 4));
JButton startButton = new JButton("Start");
startButton.setActionCommand("start");
startButton.addActionListener(documentViewActionListener);
panel.add(startButton);
JButton stopButton = new JButton("Stop");
stopButton.setActionCommand("stop");
stopButton.addActionListener(documentViewActionListener);
panel.add(stopButton);
JButton stepButton = new JButton("Step");
stepButton.setActionCommand("step");
stepButton.addActionListener(documentViewActionListener);
panel.add(stepButton);
JButton resetButton = new JButton("Reset");
resetButton.setActionCommand("reset");
resetButton.addActionListener(documentViewActionListener);
panel.add(resetButton);
Canvas canvas = (Canvas)surface.getNative();
JLayeredPane pane = (JLayeredPane)canvas.getParent().getParent();
pane.add(panel);
}
void draw(){
fill(255, 255, 255, 60);
rect(0, 0, width, height);
if(inAnimation){
model.messageFromDocumentViewToModel("step");
}
if(!model.messageFromDocumentViewToModel("getNumOfBall").equals("Command not Defined")){
int numOfBall =
Integer.parseInt(model.messageFromDocumentViewToModel("getNumOfBall"));
for(int i=0; i<numOfBall; i++){
String ballInfoString =
model.messageFromDocumentViewToModel("getBallInfo/" + i);
String[] ballInfoArray = ballInfoString.split("/", 0);
int x = 0, y = 0, r = 0, g = 0, b = 0, size = 0;
if(ballInfoArray[0].equals("Pos")){
x = Integer.parseInt(ballInfoArray[1]);
y = Integer.parseInt(ballInfoArray[2]);
}
if(ballInfoArray[3].equals("Color")){
r = Integer.parseInt(ballInfoArray[4]);
g = Integer.parseInt(ballInfoArray[5]);
b = Integer.parseInt(ballInfoArray[6]);
}
if(ballInfoArray[7].equals("Size"))
size = Integer.parseInt(ballInfoArray[8]);
fill(r, g, b);
ellipse(x, y, size, size);
}
}
}
void setActionCommand(String actionCommand){
if(actionCommand.equals("start"))
inAnimation = true;
else if(actionCommand.equals("stop"))
inAnimation = false;
else if(actionCommand.equals("step"))
model.messageFromDocumentViewToModel("step");
else if(actionCommand.equals("reset"))
model.messageFromDocumentViewToModel("reset");
}
class DocumentViewActionListener implements ActionListener{
MVC_DocumentView viewController;
DocumentViewActionListener(MVC_DocumentView viewController){
this.viewController = viewController;
}
public void actionPerformed(ActionEvent e){
viewController.setActionCommand(e.getActionCommand());
}
}
// MVC_DocumentView_Model.pde
// Model用のクラス.今回は外部クラスとして宣言している.
class MVC_DocumentView_Model{
BallModel ballModelArray[];
int windowWidth, windowHeight;
MVC_DocumentView_Model(int windowWidth, int windowHeight){
this.windowWidth = windowWidth;
this.windowHeight = windowHeight;
ballModelArray = new BallModel[10];
for(int i=0; i<ballModelArray.length; i++)
ballModelArray[i] =
new BallModel(this.windowWidth, this.windowHeight);
}
String messageFromDocumentViewToModel(String message){
// 最初にデフォルトで返すメッセージのStringを作っておく
// 受け取ったメッセージStringが用意されているコマンドに該当し途中でreturnされる.
// 途中でreturnされなければ,最後にこの文字列がreturnされる仕組み
String returnString = "Command not Defined";
// メッセージの各コマンドやパラメータは「/」で区切られているので,分離
String[] commandArray = message.split("/", -1);
// 分離したコマンド文字列が,どのコマンドに当たるのかをチェックし,当該の部分を実行
if(commandArray[0].equals("step")){
for(int i=0; i<10; i++){
ballModelArray[i].updateParameters();
}
return new String("Parameter Updated");
}
else if(commandArray[0].equals("reset")){
for(int i=0; i<10; i++){
ballModelArray[i] = new BallModel(windowWidth, windowHeight);
}
return new String("Parameter Resetted");
}
else if(commandArray[0].equals("getBallInfo")){
int index = Integer.parseInt(commandArray[1]);
return new String("Pos/" +
(int)ballModelArray[index].getBallPosition().getX() + "/" +
(int)ballModelArray[index].getBallPosition().getY() + "/" +
"Color/" +
ballModelArray[index].getBallColor().getRed() + "/" +
ballModelArray[index].getBallColor().getGreen() + "/" +
ballModelArray[index].getBallColor().getBlue() + "/" +
"Size/" +
ballModelArray[index].getBallSize());
}
else if(commandArray[0].equals("getNumOfBall")){
return new String(new Integer(ballModelArray.length).toString());
}
return returnString;
}
}
関数チェーン(メソッドチェーン)
とあるオブジェクトインスタンス.その中の関数().そこで飛び出てきたとあるクラスのオブジェクトインスタンスの中の関数().そこで飛び出てきたとあるクラスのオブジェクトインスタンスの中の関数();
という形で関数を繋げて書くことを「関数チェーン」(メソッドチェーン)と呼ぶ.
「可読性が下がらない場合」つまり「飛び出てくるインスタンスのクラス」が明確である場合は,この関数チェーンにより記述量を少なくすることができる.