Excel VBA - Decorator パターンモデル
Excel VBA - Decorator パターンモデル
概要
ここでは,Decorator パターンモデルという Excel プログラミングに関するアーキテクチャを解説します.これは,Decorator パターンをアーキテクチャレベルにまで拡張したモデルで,継承が使えない VB プログラミングで威力を発揮すると思います.まだ実験段階なので,このモデルが実用化できるかは今後の課題です.
Excel プログラミングとは?
Excel のプログラミングを一言で表すと,VBA プログラミングで Excel を拡張する,ということです.オブジェクト指向の立場で拡張といえば,継承ですね.したがって次のように任意の ExcelObject クラスを継承してプログラミングできればいいですね:
例えば Worksheet クラスから MyWorksheet クラスを継承すればいいわけです.そうすると望みのカスタマイズされたワークシートが使えるようになります.これが本来のオブジェクト指向におけるフレームワークを使ったプログラミングスタイルです.
ところが,Excel プログラミングの場合こうするわけにはいきません.最初から Worksheet オブジェクトが Excel のオブジェクトモデルの一部として提供されており,このオブジェクトを継承を使って拡張できないのです.容易に変更できるのは,Worksheet オブジェクトのプロパティだけです.どうしたらいいのでしょうか?
Worksheet オブジェクトのクラスが最初から変更できない以上,このオブジェクトを拡張するには動的継承のような機能が必要です.動的継承は,すでにインスタンスとして存在するオブジェクトを拡張するということです.もちろんこんな機能は VBA にないので別の方法を考える必要があります.そこで,GoF デザインパターンカタログを調べると,Decorator パターンにこんな記述がされているのがわかります.
「オブジェクトに責任を動的に追加する.Decorator パターンは,サブクラス化よりも柔軟な機能拡張方法を提供する」
これですね.Excel のオブジェクトを拡張するには,この Decorator パターンを使うのがよさそうです.ExcelObject を下図のように Decorator クラスで拡張すればよいのです.
Decorator クラスは,target として参照する ExcelObject を外から拡張します.Worksheet オブジェクトを拡張するには,MyWorksheetDecorator クラスを作ればよいわけです.このように,継承を使って拡張できない Excel プログラミングでは,Decorator パターンによる拡張が一番いいのではないかと思われます.
ところで,Decorator パターンをそのまま適用するとクラス図は次のようになることがわかります.
もちろん VBA でこんなことはできません.この辺をちゃんと実装できないのが VBA プログラミングの問題点でしょう.なお,上の図の MyDecorator が先ほどの Decorator に相当していることに注意してください.
Excel オブジェクトモデル
Excelのオブジェクトモデルをクラス図を使って表すと,次のようになります.
Application クラスが Excel 本体を表し,Application は Excel ブックファイルである Workbook クラスを複数所有しています.Workbook クラスは,シートからなり,シートは Worksheet か Chart のいずれかです.これは,Excel のユーザなら直感的に理解できると思います.なお,図では Sheet クラスを抽象クラスとして書いていますが,本来の Excel オブジェクトモデルにはこのようなクラスは存在しません.あえて理解しやすくするために Sheet クラスを導入しました.ご注意を.
さて,この記事では簡単のために Excel のChart クラスを無視して次のモデルであると考えましょう.実際にグラフシート単独で使うことはあまりないと思います(ちがうのかな?).
前のセクションで書いたとおり,Excel を拡張するのには Decorator パターンを使います.そうすると,Application, Workbook, Worksheet に対応して AppDecorator, BookDecorator, SheetDecorator を用意すればいいですね.
オブジェクトの所有関係も同じにしていることに注意してください.また,SheetDecorator が0以上の多重度に設定されているのは,Decorator によって拡張されない生のままの Excel ワークシートもありうるからです.このように,Excel のオブジェクトモデルの構造とパラレルに Decorator 側のオブジェクト構造を定義するのが, Decorator パターンモデルなのです.上図を合わせて書けば,次のようになります.
Excel 全体を拡張するのに,そのオブジェクト構造がそっくりパラレルなDecoratorクラス郡を用意してやる,というのがミソです.パッケージ図は次のようになりますね.
このように,Decorator 側とExcel 側で分離してプログラムを行うということがお分かりいただけたかと思います.
ところで,SheetDecorator, BookDecorator, AppDecorator の間で所有関係を持たせていることに疑問をもっている方もいらっしゃるかもしれません.その理由にひとつは,Decorator オブジェクトの寿命を管理するためです.Decorator オブジェクトをローカルで作って ExcelObject に引っ付けても,Decorator オブジェクトはすぐに破棄されてしまいます.そのため,AppDecorator をグローバル変数として用意し, この AppDecorator が BookDecorator の寿命を管理します.さらに BookDecorator は,SheetDecorator オブジェクトの寿命を管理します.例えば, BookDecorator がひっついているワークブックが閉じられた場合,それに付随する SheetDecorator もみんな破棄する,という処理などを行います.
さらにユーザのコマンドは,
- アプリケーションレベルのコマンド
- ブックレベルのコマンド
- シートレベルのコマンド
に分類できます.これらのコマンド処理の実装の役割を分担するためにも Decorator をそれぞれのレベルに分けて構成しています.
Decorator パターンモデルの実装
WithEvents キーワード
実際の VBA のプログラミングでは,WithEvents キーワードとクラスモジュールを使って Decorator クラスが実装できます.WithEvents を使えば,ターゲットとなるオブジェクトのイベントをフックすることが可能です.ターゲットオブジェクトのイベントをフックすることによって結果的にそのオブジェクトを拡張できます.VBA では,Decorator に相当するクラスモジュールに次のコードを書くだけでOKです.
Public WithEvents Target As ExcelObject
ExcelObject には,実際には Application, Workbook, Worksheet などが入ります.Decorator のTarget プロパティに Excel のオブジェクトを指定することでそのオブジェクトのイベントをフックできるようになるのです.例えば,
Dim aBookDecorator As New BookDecorator Dim aWorkbook As Workbook ... Set aBookDecorator.Target = aWorkbook
としておき, BookDecorator の方で aWorkbook オブジェクトの Activate イベントをフックするコードは BookDecorator のメソッドとして
Private Sub Target_Activate() 'Activate イベント処理コードをここに書く End Sub
としておきます.このように,WithEvents を使えば VBA でDecoratorパターンが簡単に実装できるわけです.
ここまで読んだ方は,Decorator パターンというより Strategy パターンと呼んだほうがいいのではないか? と思われるかもしれません.そう呼ばない理由は,オブジェクトを参照する方向にあります.Strategy パターンの場合,機能を拡張される側のオブジェクトが Strategy オブジェクトを参照していますが,Decorator パターンでは,機能を拡張される側のオブジェクトは,Decoratorのことは何も知りません.だからここでは Decorator パターンと呼ぶのがふさわしいと考えます.イベントをフックする以外のコードもすべて Decorator 側に書きます.Excelのオブジェクトにメソッドを追加することはできないわけですから.
メタクラス標準モジュール
これは VB 全般の話になりますが,VB のクラスモジュールでは,クラス変数やクラスメソッドを定義することができません.困ったもんですね.これを解決するには,標準モジュールを使って各クラスごとにクラス変数とクラスメソッドを定義するのがよいと思われます.標準モジュールにした場合,自動的にメタクラスの Singleton の要請が満たされるからです.図に書くと次のようになります.
上図のように,XXX というクラスモジュールに対して XXXClass という標準モジュール(メタクラス標準モジュール)を用意するコーディングルールに従うのがよいでしょう.クラス変数,クラスメソッドの使い方は次のようになります.
'クラス変数の使用例 VBObjectClass.ClassVariable = aValue 'クラスメソッドの使用例 VBObjectClass.ClassMethod
本来は VBObjectClass という限定子をつける必要がない場合もありますが,上のように必ずつけるようにします.また,注釈のファイル名は Visual Basic の場合のファイル名です.Excel の場合でもモジュールをエクスポートすればそういうファイル名が自動的につけられます.
したがって Decorator 側の物理的な構成は,次のようになります.
SheetDecorator, BookDecorator, AppDecorator はクラスモジュール,SheetDecoratorClass, BookDecoratorClass, AppDecoratorClass は標準モジュールとして配置します.
ActiveDecorator 変数
Excel プログラミングで次に問題となるのが,ユーザからのマクロ呼び出しです.例えばユーザがメニューを選択してマクロを呼び出す場合,そのマクロは単なるプロシージャでなければなりません.したがってある特定の Decorator オブジェクトのメソッドを呼ぶ,ということはできません.
これを解決するには,ActiveDecorator 変数を作って現在どの Decorator がアクティブになっているかを管理する必要があります.Excel でもすでに ActiveWorkbook と ActiveSheet という変数が用意されていますが,これと同じように,グローバルにアクセスできる変数,ActiveBookDecorator と ActiveSheetDecorator を用意します.
実際には,AppDecoratorClass に
Public ActiveInstance As AppDecorator
というクラス変数を用意しておき,AppDecorator のインスタンス変数として ActiveBookDecorator ,BookDecorator のインスタンス変数として ActiveSheetDecorator を用意しておけば十分です.例えば,SheetDecorator のメソッド DoCommand を呼び出すマクロは 次のように記述できます.
Private Function GetActiveSheetDecorator() As SheetDecorator Set GetActiveSheetDecorator = _ AppDecoratorClass.ActiveInstance.ActiveBookDecorator.ActiveSheetDecorator End Function Sub DoCommand() GetActiveSheetDecorator().DoCommand End Sub
上のコードを SheetDecoratorClass のプロシージャとして定義しておき,メニューに登録する際は
Dim mnuDoCommand As CommandBarControl ... With mnuDoCommand .Caption = "コマンド実行" .OnAction = "SheetDecoratorClass.DoCommand" End With
のように,メタクラス標準モジュールの同名クラスメソッドを OnAction プロパティに登録しておきます(メソッド名を同じにするのはあくまでコーディングルールです).
ActiveDecorator の設定自体は,各 BookDecorator と SheetDecorator が Activate イベントと Deactivate イベント等をフックすることによって実装します.以下のプログラムでは,SheetDecorator が Parent である BookDecorator の ActiveSheetDecorator プロパティを自分自身に設定,または解除しています.
Public Sub OnFocus() Set Parent.ActiveSheetDecorator = Me 'その他,メニューの変更などActiveになった場合の処理を書く End Sub Public Sub OnBlur() Set Parent.ActiveSheetDecorator = Nothing 'その他,メニューの破棄などActiveでなくなった場合の処理を書く End Sub Private Sub Target_Activate() OnFocus End Sub Private Sub Target_Deactivate() OnBlur End Sub
別メソッドとして OnFocus と OnBlur を用意しているのは,別のところでも明示的に呼び出す必要があるためです.
サンプルプログラム
Decorator パターンモデルを使ったサンプルプログラムを用意しました(DecoratorModel.xls).この Excel ファイルは Web ブラウザから直接開くと動作しません.一旦ファイルを保存してから使用してください.
このプログラムは,Excel のメニューバーに DecoratorModel というメニューを追加します.メニューコマンドは,以下のようになっています.
メニュー名 | メソッド名 | コマンドの内容 |
---|---|---|
ブックの新規作成 | AppDecorator.NewBook | BookDecorator によって拡張されたブックを新規作成する |
ブックの数... | AppDecorator.ShowBookDecoratorCount | 現在オープンされている BookDecorator の数を表示する |
シートの追加 | BookDecorator.AddSheet | SheetDecorator によって拡張されたワークシートを追加する |
シートの数... | BookDecorator.ShowSheetDecoratorCount | そのブックの SheetDecorator の数を表示する |
シートコマンド実行 | SheetDecorator.DoCommand | シートの背景色をランダムに変更する |
上のメニューは,常に表示されているわけではありません.ブックを切り替えたり,シートを切り替えたりするタイミングで必要なメニューだけが表示されます.その部分も注意してみてください.
念のため,ソースだけを見たいという方のために各モジュールをファイルとしてエクスポートしておいたものも用意しました(利便性を考えファイルの拡張子をtxtに変えてます).面倒な方はこちらをどうぞ.
- AppDecoratorClass.bas
- AppDecorator.cls
- BookDecoratorClass.bas
- BookDecorator.cls
- SheetDecoratorClass.bas
- SheetAppDecorator.cls