GUIの設計パターン
さて、NetBeansによるGUIの構築方法をある程度把握したので、今回はSwingアプリケーションの設計についてです。
今回作成するアプリケーションは次のようなインターフェイスを持ちます。動きとしては「Up」ボタンをクリックすればカウンターの数字が増加していくだけの単純なアプリケーションです。
これを実装するだけならば簡単かと思いますが、次の制約の元にアプリケーションを構築します。
- ボタンは「ActionPanel」に作成し、カウンターは「CounterPanel」に作成して分割する
- さらに2つのパネルを「AppPanel」に配置した上で、「AppFrame」に配置する
この制限は大きな無駄に思えると思います。しかし、アプリケーションがもっと複雑な構造をとっていると仮定してください。例えばEclipseやNetBeansは様々なビューを持ち、コマンドもメニューだけではなく右クリックからなど様々な方法で実行できます。このように複雑なアプリケーションであると仮定し、どのように設計するべきかを考えます。
依存関係
まず、クラス図を使いながら依存関係を確認しましょう。AppFrameはAppPanelをコンポーネントとして保持し、AppPanelはActionPanelとCounterPanelをコンポーネントとして保持している為、次のようなクラス図になります。
親コンポーネントから子コンポーネントへの依存関係(has-aの関係)は揺るぎようがありません。一般的にクラス設計において依存関係(dependency)は弱い方が良いとされています。したがって、子コンポーネントから親コンポーネントへの依存関係は避けるように設計しなければなりません。
ここで次のように依存関係を無秩序に複雑にするとアプリケーションの拡張やメンテナンスは非常に困難になります。
ボタンにアクションを設定する
Swingではボタンのイベントなどはデザインパターンで言うObserverパターンが用いられています。
JButtonでは、JButton(Subject)に対しActionListener(Observer)を登録しておきます。JButtonではクリックされるというイベントが監視(observe)されており、イベントが発生した時にActionListenerのactionPerformedメソッドが実行される仕組みになっています。言い換えれば実装者はボタンがクリックされた時の処理だけを記述すれば良いわけです。これをJButtonではActionListenerのactionPerformedメソッドに記述します。
簡単な実装サンプルを示します。
JButton button = new JButton(); button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { // クリック時の処理 } });
ここでも無名インナークラスが登場していますね。
NetBeansでは、このようなイベントの登録はGUIから簡単に設定できます。
まず、ActionPanelを作成してJButtonをドラッグ&ドロップします。続けてボタンをダブルクリック(または右クリックして、イベント-アクションを選択)するだけです。次のようなメソッドが追加され、ソースが表示されるはずです。
private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) { // TODO add your handling code here: }
どうやらprivateメソッドのようですが、どのように利用されているかをチェックしましょう。initComponentsを確認すると次のようなコードを確認できます。
jButton1.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jButton1ActionPerformed(evt); } });
ボタンのインスタンスに対し、ActionListenerを無名インナークラスとして登録し、実装は先ほどみたprivateメソッドを呼んでいます。つまり、NetBeansのGUIビルダではドラック&ドロップとダブルクリックという2つの操作で、ボタンの処理のひな形が作成できるのです。後はボタンの処理を記述するだけです。
ボタンの名前が気にくわない
jButton1のままではかっこ悪いので、hitButtonとボタンの名前を意味のある名前に変更してみましょう。
upButton.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { upButtonActionPerformed(evt); } });
private void upButtonActionPerformed(java.awt.event.ActionEvent evt) { // TODO add your handling code here: }
しっかりメソッド名もリファクタリングされている辺りがNetBeansのGUIビルダの素晴らしい所です。
さて、ボタンのイベントの記述方法は解りました。しかし、更新すべきカウントは別のコンポーネントであり、直接触る事ができません。どのようにして設計すべきかを見ていきましょう。
決してMenuPanelにContentsPanelへの参照を持たせてはいけません。絶対にダメです。方針としてはトップレベルのコンポーネント(AppFrame)をMVCのControllerと捉えて実装します。
MVCを再考する
MVC(Model View Controller)と言えばWebアプリケーションにおける設計手法の代名詞となっている感がありますが、本来はGUIを開発する為の設計手法です。勿論、考え方は同じであり、View(表示)に関連するコンポーネントと、Model(処理やデータ)に関連するコンポーネントを分離し、Controllerはそれらの接着剤として機能するように設計します。Swingでアプリケーションを開発する場合もMVCの手法をとります。
尚、Swing内部のMVCとアプリケーション全体でのMVCを混乱しないでください。SwingにはJTableなど複雑なコンポーネントがあります。JTableは内部的なモデルとしてTableModelを扱いますが、これはアプリケーションにおけるモデルではありません。アプリケーション全体からみたならばJTableと関連するコンポーネント(TableModel)は全てViewになります。アプリケーションのモデルには純粋なビジネスロジックやデータモデルのみを含めるようにする事が重要です。
Model
最初にModelを設計します。今回のアプリケーションでは数値をカウントするCounterというクラスが、現在の値の取得と増加などの機能を持てば良いでしょう。
public class Counter { private int count; public int getCount() { return count; } public void increment() { count++; } }
簡単ですね。
ボタンのアクションをモデルに通知する
今回のアプリケーションでは、ボタンをクリックするとカウントが増える仕様です。Modelを分離したので、CounterというModelのincrementメソッドを呼び出せば良い事になります。単純に考えれば、MenuPanelがCounterクラスのインスタンスを保持していれば実現できます。しかし、Counterオブジェクトはアプリケーション全体のModelなので他のコンポーネントでも参照したいはずであり、特定のコンポーネントに結び付けるべきではありません。それでは、どのコンポーネントがModelを保持すべきでしょうか?
答えはAppFrameになります。AppFrameはすべてのコンポーネントのルートとなる為、Modelを持つに相応しいクラスです。また、前述した通り、AppFrameはViewというよりはControllerとして振舞うように設計します。これはViewがModelを直接操作するのではなく、Controllerを仲介して操作する方が望ましいという事でもあります。
Mediator
ModelはAppFrameが持ち、このModelに関する操作をを各コンポーネントに公開する事になります。しかし、AppFrameに対する参照をMenuPanelが持つのは好ましくないと既に述べました。そこで、Javaではインターフェイスを用いて設計します。
public interface CounterMediator { void increment(); }
MenuPanelはAppFrameへの参照ではなく、CounterMediatorへの参照を保持します。AppFrameはCounterMediatorをimplementsするわけであり、実際に参照される実装クラスはAppFrameです。AppFrameへの参照をMenuPanelが持つべきではないのではないか?と思われるかもしれません。ですが、クラス図を参照してください。CounterMediatorは抽象的にincrement機能を提供するだけであり、それがSwing ComponentやModelであるかどうかは関係ありません。インターフェイスに抽出した為、純粋なControllerになっています。
尚、Mediatorは仲介者とも呼ばれ、デザインパターンの1つです。たくさんのコンポーネントからのイベントを受けつけ、それを仲介しやはりたくさんのモデルを使って処理を行います*1。アプリケーションで提供する機能をMediatorとして抽出して主要なクラスが実装すると見通しの良いアプリケーションになります。
実装サンプルです。
public class AppFrame extends javax.swing.JFrame implements CounterMediator { // Counter Model private Counter counter = new Counter(); // CounterMediatorの実装メソッド public void increment() { counter.increment(); } }
これで心おきなくActionPanelにCounterMediator(Controller)の参照を作成できます。
ServiceクラスでModelをまとめる
また、Modelが大量に存在するような場合、AppFrameからモデルを分離すると設計がすっきりします。このとき、同じMediatorをインターフェイスとして使う事がよくあります。
クラス図だと分かり難いですが、コードにすると分かり易いでしょう。
public class AppFrame extends javax.swing.JFrame implements CounterMediator { // AppService private AppService service = new AppService(); // CounterMediatorの実装メソッド public void increment() { service.increment(); } }
AppServiceがモデルをまとめてアプリケーションの機能を提供しています。このAppServiceは自然にCounterMediator を実装するでしょう。
MenuPanelの実装
ここでCounterMediatorをActionPanelに設定する方法としては、大きく2つの方法があります。1つ目はsetterを使って設定する方法で、もう1つはコンストラクタに渡す方法です。堅牢なコードを書くには後者が望ましいのですが、NetBeansのGUIビルダを有効に使うにはsetterを使う方法を取ります。まずはコードを示します。
public class AppFrame extends javax.swing.JFrame implements CounterMediator { // Counter Model private Counter counter = new Counter(); public AppFrame() { initComponents(); this.appPanel.actionPanel.setCounterMediator(this); } // CounterMediatorの実装メソッド public void increment() { counter.increment(); } } public class ActionPanel extends javax.swing.JPanel { private CounterMediator counterMediator; public void setCounterMediator(CounterMediator mediator) { this.counterMediator = mediator; } // private void hitButtonActionPerformed(java.awt.event.ActionEvent evt) { this.counterMediator.increment(): } }
これでActionPanelがAppFrameに依存しない美しいコードになったかと思います。
尚、コンストラクタを使う方法を採用しないのは、NetBeansのGUIビルダとの相性が悪いからです。
カウンターのラベルを更新する
このアプリケーションではイベントはボタンから発生します。したがって、発生した場合の処理はActionPanelのイベントメソッドで記述できました。
次の実装はCounterPanelの更新です。つまり、CounterPanelがどのようにしてイベントの発生を処理するかです。幸いにも処理はトップレベルのコンポーネントで仲介されています。したがって、イベントの処理後に子コンポーネントのメソッドを呼び出す事ができます。
public void increment() { counter.increment(); this.appPanel.actionPanel.counterLabel.setText(Integer.toString(model.getCount())); }