[技術講座]
(株)オージス総研
福田 直樹
一般的に GUI 画面は、追加や変更の発生しやすい部分であり、その影響は最小限にしたいものです。GUI を表すクラスとデータ処理を行うクラス間は分離し、結合度を弱める必要があります。今回は、変更の影響や再利用性を意識した設計の枠組みと、更新されたデータをリアルタイムに通知するようなメカニズムを提供する Observer パターンを取り上げます。
※雑誌『Java WORLD』 2006 年 8 月号に掲載した記事のオリジナル原稿を Java WORLD 編集部の了解を得て掲載しています。
前回は、Factory Method パターンを取り上げ、オブジェクトを生成するときの柔軟な方法について見ていきました。Factory Method パターンはシンプルなデザインパターンで、そのシンプルさゆえにいろいろなバリエーションがあることも述べました。デザインパターン適用のポイントとして、デザインパターンのクラス構造をそっくりそのまま適用するのではなく、適用の目的や効果を意識しながら基本構造を「くずす」ような視点も必要です。今回取り上げる Observer パターンもいくつかのバリエーションがあるので、適用のメリット、デメリットを捉えながら読み進めてみてください。
X 社のシステムは、セミナー受講者やアンケート情報の管理を行っています。各セミナー開催時には、受講者にアンケートを記入してもらっており、アンケート用紙には受講者の会社名、名前、E メール等の情報を記入する欄と受講したセミナーに関する評価 (満足度) を記入する欄があります。今回取り上げる機能は、評価データの登録や変更を行った際にリアルタイムに画面表示を更新するアンケートデータの表示機能です。(図 1)
図 1 :アンケートデータの表示機能 |
現状は、表示する画面として以下の 2 つがあります。
C さん 「今回取り上げる部分の機能と実装は、A 君の方で見てくれたんだよね。」
A 君 「はい。アンケートデータの活用は企画部の方でも今後充実させたいそうです。実装も確認しておきました。ここの実装は少し他と違う構造になっていて。。通常、GUI から業務ロジックや業務データの管理を行うオブジェクトにオペレーション呼び出しをして、その結果を取得し、画面表示するという形になっています。でも、ここの部分は、逆にデータ処理を行うオブジェクトから GUI 側にオペレーション呼び出しをしているので、ちょっと違和感があるんですよね。(図 2)」
図 1 : GUI クラスとデータ処理クラスの関係 GUI クラスからデータ処理クラスへの呼び出しと、逆にビジネスロジック から GUI クラスの呼び出しが発生している。(一番下の GUI クラス) gui パッケージに含まれるクラスとデータ処理クラスに相互の依存関係 が発生しており、結合度が高くなっている。 そのため、再利用性、拡張性に 乏しい構造となってしまっている。 |
C さん 「なるほど。gui パッケージに含まれる GUI の表示の責務を持つクラス群 (今後「GUI クラス」と呼ぶ) と業務ロジックやデータ処理を行うクラス (今後「データ処理クラス」と呼ぶ) との間に双方向の依存関係があるようだね。違和感と言ったけど、このように双方向の依存関係があると、問題がありそうかな?」
A 君 ごちゃごちゃして変更の影響が捉えにくいですね。GUI クラスの追加が発生した時に、データ処理クラスにも変更が発生してしまいます。
C さん そうだね。画面表示部分とデータ処理部分で明確に役割が分割できている点はいいんだけど、双方向に依存関係があると、再利用性や変更容易性に乏しい構造になるね。まずは、現状で実現しているクラス構造を見てみようか。
図 3 に、既存の設計をクラス図で表現しています。既存のソース・コードは、リスト 1 に示します。ParticipationData クラスの calcDatas メソッドは、アンケート情報が追加されたときに呼び出され、データを集計し分析データを作成します。その際に、dataChanged メソッドが呼び出され、セミナー評価画面を表す PTSeminarEvalFrame オブジェクトと項目毎評価平均画面を表す PTItemEvalFrame オブジェクトに対して update メソッドを呼び出しています。そうすると、各画面のオブジェクトは渡されたデータを表示する処理を行います。
図 3 :既存の設計を表現したクラス図
※ ParticipationData() の引数は省略 |
リスト 1 : Observer パターンを適用する前の既存のソース・コード |
if (index >= 0) { obs// ParticipationData クラス public class ParticipationData { private PTSeminarEvalFrame ptSeminarEvalFrame = null; private PTItemEvalFrame ptItemEvalFrame = null; private double[][] averageDatas; public ParticipationData(PTSeminarEvalFrame ptSeminarEvalFrame, PTItemEvalFrame ptItemEvalFrame) { this.ptSeminarEvalFrame = ptSeminarEvalFrame; this.ptItemEvalFrame = ptItemEvalFrame; } public double[][] getAverageDatas() { return this.averageDatas; } public void dataChanged() { // アンケートの評価平均値を取得する averageDatas = this.getAverageDatas(); // 画面側に変更を通知する ptSeminarEvalFrame.update(averageDatas); ptItemEvalFrame.update(averageDatas); } public void calcDatas() { // アンケートの評価結果を算出する // ...略 ... this.dataChanged(); } // ...略 ... } // PTSeminarEvalFrame クラス public class PTSeminarEvalFrame extends JFrame { // ...略 .... public PTSeminarEvalFrame() { super(); initialize(); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setVisible(true); } public void update(double[][] datas) { // 渡されたデータを表示する for(int i = 0; i < datas.length; i++) { jTable.setValueAt(String.valueOf(datas[i][0]), i, 1); jTable.setValueAt(String.valueOf(datas[i][1]), i, 2); jTable.setValueAt(String.valueOf(datas[i][2]), i, 3); jTable.setValueAt(String.valueOf(datas[i][3]), i, 4); jTable.setValueAt(String.valueOf(datas[i][4]), i, 5); jTable.setValueAt(String.valueOf(datas[i][5]), i, 6); } } // ...略 ... } // PTItemEvalFrame クラス public class PTItemEvalFrame extends JFrame { // ...略 ... public PTItemEvalFrame() { super(); initialize(); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setVisible(true); } public void update(double[][] datas) { // 渡されたデータから評価項目毎の平均値を算出する // ...略 ...(displayDatas を設定) // 画面にデータをセット for(int i = 0; i < displayDatas.length; i++) { jTable.setValueAt(String.valueOf(displayDatas[i]), i, 1); } } // ...略 ... } |
C さん ParticipationData クラスで GUI クラスである PTSeminarEvalFrame と PTItemEvalFrame の update メソッドを呼び出しているから、データ処理クラスから GUI クラスへの依存が発生しているね。GUI クラスが追加された場合はどういう変更が必要になるかな?
A 君 えーっと、ParticipationData.dataChanged メソッドの中で、追加された GUI クラスの update メソッドを呼び出さないといけないですね。メソッドのボリュームが増えるのと、update のメソッド呼び出しは同じようなインタフェースなのに、メソッド呼び出しのコードが増えてしまいますね。
C さん ParticipationData クラスが具象クラスに依存しているから、GUI クラスの追加、削除があった場合には確実に ParticipationData クラスに変更が必要になってしまう。A 君も経験あるんじゃないかな、GUI 画面は見栄えをよくしたり、ちょっとしたカスタマイズをしたくなることが多いよね。それだけ、変更が発生しやすい部分だと言えるんだ。そのため、GUI クラスとデータ処理クラスは、なるべく結合度を弱めておく必要がある。 現状だと、それぞれ独立して再利用することも難しい状態だ。ここまでの問題点を整理しておこうか。
いつものように C さんはノートに問題点をまとめました。
C さん これらの問題点の解決策を見ていこう。ParticipationData.dataChanged メソッドが呼ばれた時に、各 GUI クラスの update メソッドを呼び出す必要があるけど、まずその部分を以下のような指針に変更した方がいいな。
- 各 GUI クラスの共通インタフェースを抽出して、データ処理クラス側ではそのインタフェースに対して呼び出しをする
C さん クラス間の依存関係を弱めるのに、インタフェースを使うのは常套手段だね。インタフェースを抽出してそれを活用することはオブジェクト指向設計の肝とも言っていいことだよ。今まで適用したデザインパターンでも出てきていたよね。
A 君 はい。今回のだと、PTSeminarEvalFrame クラスと PTItemEvalFrame クラスの共通のインタフェースを抽出するのですね?
C さん そうだね。共通のインタフェース名だけど、Observer という名前にしよう。今回適用できそうなデザインパターンは、Observer パターンと言うんだ。Observer とは、「観察者」という意味で、PTSeminarEvalFrame クラスと PTItemEvalFrame クラスがデータを観察するから、それらの共通インタフェースは Observer ということだね。
各 GUI クラスの共通インタフェースとして Observer インタフェースを抽出した結果が図 4 となります。
図 4 : Observer インタフェースを抽出したクラス図 |
C さん さらに、GUI クラスへの更新通知のメカニズムを汎用的なものにしよう。更新メカニズムと各データ処理クラスの独立性を高めて、それぞれで再利用できる形にしたいからね。そのために、以下のような変更を加えてみよう。
C さん こうすることで、GUI クラスが追加されてもデータ処理クラス側では一切変更の必要はなくなるんだ。あと、Subject とは、「被観察者(観察される側)」という意味で、GUI クラスから観察されるわけだ。ここまでの部分は理解できたかい?
A 君 えぇ、なんとか。。でも、ちょっと実装してみないと、いまいちイメージが沸かないですね。。
C さん ちょっと待ってくれるかな。もう一点、検討項目があるんだ。それを検討してから実装に移ることにしよう。
C さん 検討項目とは、表示するデータの送信方法についてなんだ。Observer パターンのデータの受け渡し方は大きく分けて 2 種類のやり方があって、「Push モデル」と「Pull モデル」と呼ばれる。現状は、Subject クラスから Observer.update メソッドを呼び出す際に、引数で渡したいデータを GUI クラスの方に送っているよね。これを Push モデルと言うんだよ。
A 君 データを送り出すということなんでしょうか?もう一つはどういうものなんですか?
C さん もう一つは、update の呼び出し時にデータを送信せずに、update メソッドの中で各 GUI クラスに必要なデータを取得することも可能なんだ。この場合は、update メソッドは更新のタイミングを知らせるだけで、必要な情報は、自ら Subject から引き出さなければならない。これを Pull モデルと言うんだ。(図 5)
図 5 : Push モデル(上)と Pull モデル(下)の処理の流れ |
A 君 なるほど。使い分けの指針はどう考えればいいでしょうか?
C さん Push モデルの場合は、update メソッドの引数の型をすべての Observer クラスで同一にしないといけないから、ある Observer にとっては必要のないデータまで送ってしまう可能性がある。必要なデータだけ送ろうとすると、Subject が各 Observer クラスの処理を意識するようになるため、あまり好ましくないね。しかし、Observer に渡すデータがうまく整理できれば、Push モデルは適しているだろうね。また、Subject 側に Observer からアクセスしないため、Subject に余計なアクセスメソッドは必要なくなるということも言えるかな。Pull モデルの場合は、update メソッドの引数をとらず、各 Observer クラスで自分に必要なデータを取得するメソッドを呼び出せばいいから、update メソッドの汎用性は高いと言えるだろうね。
A 君 Pull モデルの場合は、渡すデータの型に依存しないので、各 Observer クラスで必要に応じてデータの取得、表示の更新ができますね。
C さん 今回の GUI クラスは、必要な情報があまり予測できないし、Pull モデルの方がより単純なモデルになりそうだ。A 君の方で Pull モデルを適用したクラス図とソース・コードを作成してくれるかな。
A 君が書いたクラス図は図 6 に、実装したソース・コードはリスト 2 に示します。また、Subject への Observer の登録から更新までの流れを図 7 にシーケンス図で示しています。
図 6 : Observer パターンを適用したクラス図 |
図 7 : Observer パターンを 適用した際のシーケンス図 |
リスト 2 : Observer パターンを適用したソース・コード |
// Subject クラス public abstract class Subject { private List observers = new ArrayList(); public void addObserver(Observer observer) { observers.add(observer); } public void removeObserver(Observer observer) { int index = observers.indexOf(observer); ervers.remove(index); } } public void notifyObservers() { Iterator iterator = observers.iterator(); while (iterator.hasNext()) { Observer observer = (Observer) iterator.next(); observer.update(); } } } // ParticipationData クラス public class ParticipationData extends Subject { private double[][] averageDatas; public ParticipationData() { } public double[][] getAverageDatas() { return this.averageDatas; } public double[] getItemAverageDatas() { // 評価項目毎の平均値を算出する // ...略 ... } public void dataChanged() { // アンケートの評価平均値を取得する averageDatas = this.getAverageDatas(); // Subject クラスの notifyObservers メソッドを呼び出すように変更 notifyObservers(); } public void calcDatas() { // アンケートの評価結果を算出する // ...略 ... this.dataChanged(); } } // Observer インタフェース public interface Observer { public void update(); } // PTSeminarEvalFrame クラス public class PTSeminarEvalFrame extends JFrame implements Observer { // ...略 ... private ParticipationData participationData = null; public PTSeminarEvalFrame(ParticipationData participationData) { super(); initialize(); this.participationData = participationData; participationData.addObserver(this); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setVisible(true); } public void update() { // 表示データ取得 double[][] datas = this.participationData.getAverageDatas(); // セミナー毎の評価平均値を表示する for (int i = 0; i < datas.length; i++) { jTable.setValueAt(String.valueOf(datas[i][0]), i, 1); jTable.setValueAt(String.valueOf(datas[i][1]), i, 2); jTable.setValueAt(String.valueOf(datas[i][2]), i, 3); jTable.setValueAt(String.valueOf(datas[i][3]), i, 4); jTable.setValueAt(String.valueOf(datas[i][4]), i, 5); jTable.setValueAt(String.valueOf(datas[i][5]), i, 6); } } // ...略 ... } // PTItemEvalFrame クラス public class PTItemEvalFrame extends JFrame implements Observer { // ...略 ... private ParticipationData participationData = null; public PTItemEvalFrame(ParticipationData participationData) { super(); initialize(); this.participationData = participationData; participationData.addObserver(this); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setVisible(true); } public void update() { // 表示データ取得 double[] datas = this.participationData.getItemAverageDatas(); // 画面にデータをセット for (int i = 0; i < displayDatas.length; i++) { jTable.setValueAt(String.valueOf(datas[i]),i, 1); } } // ...略 ... } |
Observer パターンは、あるオブジェクトの状態、データが更新された際に、それに依存するオブジェクトに自動的に更新が通知される仕組みを提供します。 これは、データ処理の部分と表示処理の部分を分割するという基本的な設計指針の一つとして用いられると考えてもよいでしょう。
Observer パターンの基本構造は、図 8 のようになっています。また、同パターンの構成要素 (および、本稿で照会したサンプル・アプリケーションの各クラスとの対応) は表 1 に示す通りです。Observer パターンのメリットをまとめると以下のようになります。
図 8 : Observer パターンの基本クラス構造 |
表 1 : Observer パターンの構成要素 | ||||||||||||||||||||
|
Observer パターンは、MVC(Model 、View 、Controller) アーキテクチャとともに説明されることがよくあります。MVC アーキテクチャは、GUI のアプリケーションを設計する際の設計指針で、デザインパターンより粒度や範囲の大きい指針と言えます。Model は、データ処理クラスのような GUI に依存しないデータを保持するオブジェクトで、View は、各 GUI クラスのような表示に関する情報を持っているものです。既存の実装で見たように、ParticipationData オブジェクトから PTSeminarEvalFrame 、PTItemEvalFrame オブジェクトに対して直接アクセスし、表示を更新させるという方法は、MVC のアーキテクチャにも違反してしまうことにもなるのです。Model オブジェクトから View オブジェクトに対しては直接依存が発生しないようにする必要があります。
Observer パターンは、Subject と Observer のやりとりが少し複雑になっていますが、1 つ 1 つメソッドの意味やシーケンスを追っていくことで理解しやすくなると思います。少し工夫をすることで非常に使い勝手のよい仕組みを作ることができることがお分かりいただけたのではないでしょうか。特に、GUI アプリケーションを作成するときにはよく適用できるデザインパターンなので、そういう場合に重宝する枠組みとなってくれます。
© 2007 OGIS-RI Co., Ltd. |
|