このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。
古くは Smalltalk の時代から、オブジェクト指向における GUI の定石として 知られていたのが MVC(Model, View, Control) です。
通常の逐次型を含み、プログラムは大きく分けて次の三つの部分に分かれるこ とを利用しています。
逐次プログラムでは、入力により変数にデータを格納した後、変数の計算を行 い、結果を表示するという、直線的な三段階に分かれます。
一方、 GUI では、アイコンやボタンなどさまざまなメタファを利 用し、ユーザにボタンのクリックなどのイベントを発生させてユー ザ入力を実現します。 これをオブジェクト指向で実現するにはボタンなどをオブジェクトとして取扱 い、イベントが発生すると特定のメソッドが実行されるように動作します。 ここで、イベントに対応するようなプログラミング手法をイベントドリ ブンと呼びます。
ひとつのプログラミング手法として、このボタンを継承し、イベントに対応す る特定のメソッドをオーバーライドしてデータの処理を考えることができます。 しかし、これではうまくいきません。 この、ボタンに直接データ処理などの複雑な処理をさせてしまう プログラミングスタイルはマジックボタンアンチパターンと呼ば れ、悪いプログラミング手法の代表例になっています。 今までの知識を使うと、ボタンとデータはそれぞれ異なる意味の名詞ですので、 別のオブジェクトになります。 そして、 is-a 関係がないので、継承をしてはまずいです。
このように GUI の入力とデータは分離しなければなりませんが、同じ理由で、 GUI におけるデータそのものとデータの表示も分離しなければなりません。
このような、データ、データ表示、ボタンなどのイベント処理をそれぞれ別オ ブジェクトにして分離し、相互に関連付ける手法を Model(データ)、 View (表示)、Control(入力イベント)の頭文字を取って MVC と呼びます。 さて、 MVC を実現するにはどのようなデザインパターンがあるのでしょうか?
オブザーバデザインパターンは、観測者が観測物に変化が生じた ときに観測者が特定の動作を行うというものです。
例えば、「ボタンを押したら値が増える」というプログラムを考えます。 まず、オブジェクトとして「ボタン」と「値」が考えられます。 すると、「値」が観測者になり、「ボタン」が観測物です。 そして機能として、ボタンが「押される」と観測者の値が「増え」なければな りません。
ここで、オブザーバデザインパターンはストラテジデザインパターンのよ うなオブジェクトの扱いをします。 つまり、観測者は特定のメソッドを持ったオブジェクトを観測物に渡し、イベント が起きたときに観測物にそのメソッドを実行してもらうものです。 但し、ストラテジと違い、観測者に渡すオブジェクトはいくつでもよく、イベ ントの際には登録した全てのオブジェクトに対してメソッドを呼び出します。
Java では java.awt.event.ActionListener という interface があります。 これには public void actionPerformed(ActionEvent e) という抽象メソッド が宣言されています。 Java のクラスライブラリ中のボタンなどは既にできあがっていて修正は容易 ではありませんが、 この ActionListener を実装したクラスを addActionListner メソッドで登録 しておくと、ボタンが押されたときに呼び出されるというオブザーバデザイン パターンがもともと組み込まれています。
import java.awt.*;
import javax.swing.*;
class SimpleFrame extends JFrame {
private static final long serialVersionUID = 264027782111753852L;
public SimpleFrame(){
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(300,200);
Container c = getContentPane();
c.add(new ButtonPanel());
}
}
class ButtonPanel extends JPanel {
private static final long serialVersionUID = 2706302422240708884L;
private JButton getButton(String str){
JButton button = new JButton(str);
button.addActionListener(e->System.out.println(str));
return button;
}
public ButtonPanel(){
add(getButton("X"));
add(getButton("Y"));
add(getButton("Z"));
}
}
class Rei {
public static void main(String[] arg){
SimpleFrame frame = new SimpleFrame();
frame.setVisible(true);
}
}
この ActionListner を使用したオブザーバデザインパターンでは、ひとつの アクションに対してひとつのクラスが必要です。 しかも、そのクラスはひとつのメソッドしか持たず、また ActionListener の 登録以外には全くアクセスされず、一回しか使われません。 このようなクラスを簡便に宣言するために Java では無名クラス という機能があります。 それは「 new interface名(){メソッド宣言}」という構文です。 これを用いると、指定した interface を implements したクラスのインスタン スをひとつ作ることができます。 上記の例をこの無名クラスで書き換えると次のようになります。
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
class SimpleFrame extends JFrame {
Container c;
public SimpleFrame(){
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(300,200);
c = getContentPane();
c.add(new ButtonPanel());
}
}
class ButtonPanel extends JPanel {
public ButtonPanel(){
JButton x = new JButton("X");
JButton y = new JButton("Y");
add(x);
add(y);
x.addActionListener(new ActionListener (){
public void actionPerformed(ActionEvent event){
System.out.println("x");
}
});
y.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent event){
System.out.println("y");
}
});
}
}
class Rei {
public static void main(String[] arg){
SimpleFrame frame = new SimpleFrame();
frame.setVisible(true);
}
}
無名クラスの長所は、記述がシンプルになることと、ActionListner を実装し たクラスを他から全く呼び出しできないようにカプセル化できることです。 しかし、本当にこれは改良なのでしょうか?
ActionListner を実装した無名クラスのメソッドには何が記述されるのでしょ うか? 前にオブジェクト分析したように、 ActionListener を登録される側は Control であり、実際に呼出によりメソッドを実行するのは Model の方です。 したがって、オブジェクト分析からすると、この二つのオブジェクトは分離し ていなければなりません。 また、メソッドの中では Model に含まれるデータを処理する必要があります。
そこで、次の例を考えてみましょう。 「データとして整数値を考え、 GUI で 1 増やしたり、 1 減らしたりをボタン で操作できるようにしたい」というプログラムを考えます。 このとき、どのような手法が使えるでしょうか? ひとつの操作に対して、ひとつのクラスのインスタンスが対応しますので、 「増やすボタン」と「減らすボタン」には異なるオブジェクトが対応しなけれ ばなりません。 ひとつの整数値を持つのはひとつのクラスになります。 そのため、その整数値をいじるボタンのクラスがそれぞれ整数値をいじる権限 が必要です。 安易な解決策としては、整数値をいじる public メソッドを用意して ActionListener のクラスにそれぞれ使わせるということが考えられます。 しかし、値をいじれる public メソッドを作ってしまうとカプセル化ができな くなります。 また、増やす Listener も減らす Listener もクラスですが、なんとなく、整 数値を持っているクラスとだけ深い関連があるように思えます。 そこで Java 言語の機能であるインナークラス という クラス内にクラスを宣言する方法を使います。
インナークラスを使用すると、インナークラスの内部から外部のクラスに対し てはメンバ変数と final 宣言をしたローカル変数にだけ(private であろうと) アクセスできます。 インナークラスのインスタンスを作るには、外部クラスのインスタンスに対し て new を与えます。 詳しくは例15-2を御覧ください。
なお、フレームにボタンを配置するとき、パネルが必要になり、さらにパネル にボタンを貼り付けることになります。 GUI の部品的には階層構造になっており、Java の Swing クラスライブラリに おいても別々のクラスになっています。 一方、外部から見たときは、このような階層構造を意識せずに、各々の部品を 名前で呼び出せると便利です。 そのため、 GUI の一番外部の部品であるフレームのメンバ変数に、全ての GUI の部品を集めるようにします。 すると、フレームのオブジェクトに対して getter などで各部品にアクセスで きます。 そして、フレームのメンバ変数を使うために各 GUI の部品はインナークラス として定義します。
import java.awt.*;
import javax.swing.*;
class SimpleFrame extends JFrame {
private static final long serialVersionUID = 4734380538683265945L;
private JPanel myPanel;
public SimpleFrame(){
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(300,200);
Container c = getContentPane();
myPanel = new JPanel();
c.add(myPanel);
}
public void addButton(JButton button) {
myPanel.add(button);
}
}
class Data {
private int data;
public Data(){
data=0;
}
public JButton getPlusButton(){
JButton button = new JButton("+");
button.addActionListener(e->System.out.println(++data));
return button;
}
public JButton getMinusButton(){
JButton button = new JButton("-");
button.addActionListener(e->System.out.println(--data));
return button;
}
}
class Rei2 {
public static void main(String[] arg){
SimpleFrame frame = new SimpleFrame();
Data data = new Data();
frame.addButton(data.getPlusButton());
frame.addButton(data.getMinusButton());
frame.setVisible(true);
}
}
さて、この例では同じデータに対する出力部が別々の(インナー)クラスにあり、 抽象化されていません。 このようなプログラムでは、例えば出力を GUI の中で行いたいなどの仕様変 更が生じた場合、修正が難しくなります。 そのため、出力もひとつのオブジェクトとして分離しましょう。 ここで、出力オブジェクトの持つべき仕様を考えます。 「Data クラスのオブジェクトは値を持っていて、その値が変更されたら、出 力値を変える」というのが要求される仕様です。 そのため、同様にオブザーバデザインパターンを使用します。 Data クラスに addActionListener メソッドを作り、 OutputData クラスで ActionListener を implements します。
なお、 actionPerformed メソッドで渡す引数は java.awt.event.ActionEvent のオブジェクトです。 ActionEvent オブジェクトは、呼出側のインスタンスの参照と、番号と、文字 列をコンストラクタに与えて作成します。 受け側ではその情報を使用して表示を作成します。
なお import 部分と SimpleFrame クラスは例15-2 と同じため省略してい ます。
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
class SimpleFrame extends JFrame {
private static final long serialVersionUID = 4734380538683265945L;
private JPanel myPanel;
public SimpleFrame(){
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(300,200);
Container c = getContentPane();
myPanel = new JPanel();
c.add(myPanel);
}
public void addButton(JButton button) {
myPanel.add(button);
}
}
class Data {
private int data;
private ActionListener al;
public Data(){
data=0;
}
public void addActionListener(ActionListener a){
al = a; // 手抜き
}
private void update(){
al.actionPerformed(new ActionEvent(this,0,String.valueOf(data)));
}
public JButton getPlusButton(){
JButton button = new JButton("+");
button.addActionListener(e->{++data;update();});
return button;
}
public JButton getMinusButton(){
JButton button = new JButton("-");
button.addActionListener(e->{--data;update();});
return button;
}
}
class Rei {
public static void main(String[] arg){
SimpleFrame frame = new SimpleFrame();
Data data = new Data();
frame.addButton(data.getPlusButton());
frame.addButton(data.getMinusButton());
data.addActionListener(e->System.out.println(e.getActionCommand()));
frame.setVisible(true);
}
}
なお、この出力を GUI の JLabel で行いたいという場合は、 OutputData を JLabel のサブクラスとし、 SimpleFrame のインナークラスとします。
import 部分と Data クラスは変更無しなので、省略しました。
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
class SimpleFrame extends JFrame {
private static final long serialVersionUID = 4734380538683265945L;
private JPanel upperPanel;
private JPanel lowerPanel;
public SimpleFrame(){
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(300,200);
Container c = getContentPane();
upperPanel = new JPanel();
lowerPanel = new JPanel();
c.add(upperPanel,BorderLayout.NORTH);
c.add(lowerPanel,BorderLayout.SOUTH);
}
public void addButton(JButton button) {
upperPanel.add(button);
}
public void addLabel(JLabel label) {
lowerPanel.add(label);
}
}
class Data {
private int data;
private ActionListener al;
public Data(){
data=0;
}
public int getValue(){
return data;
}
public void addActionListener(ActionListener a){
al = a; // 手抜き
}
private void update(){
al.actionPerformed(new ActionEvent(this,0,String.valueOf(data)));
}
public JButton getPlusButton(){
JButton button = new JButton("+");
button.addActionListener(e->{++data;update();});
return button;
}
public JButton getMinusButton(){
JButton button = new JButton("-");
button.addActionListener(e->{--data;update();});
return button;
}
}
class MyLabel extends JLabel{
private static final long serialVersionUID = 6720670150081060445L;
public ActionListener getActionListener(){
return e->this.setText(e.getActionCommand());
}
}
class Rei {
public static void main(String[] arg){
SimpleFrame frame = new SimpleFrame();
Data data = new Data();
MyLabel label = new MyLabel();
frame.addButton(data.getPlusButton());
frame.addButton(data.getMinusButton());
frame.addLabel(label);
data.addActionListener(label.getActionListener());
frame.setVisible(true);
}
}
例15-4において、値を 0 にする Reset ボタンを付けなさい。