えくせるちゅんちゅん

ことりがエクセルをちゅんちゅんするブログ

ExcelVBAはどのようにプロシージャを分割すると良いのか(前編)

概要

私がVBAでプログラミングをするときは、プロシージャを3つの階層に分けて作るように意識している。

今回は私のプロシージャ分割の考え方を紹介する。


本記事では自分が説明しやすいようにプロシージャを次の2種類に分けて表記する。

  • マクロ・・・ユーザーの行動によって起動するプロシージャおよび機能のこと。基本的には引数を持たない Subプロシージャ のみが該当する。
  • 関数・・・・ Functionプロシージャ の他、他のVBAコードから呼ばれる一定以上の抽象度を持つサブルーチンSubも含める。


これから説明しようとしていることを満たしている最低限の開発例

f:id:Kotori-ChunChun:20210628003754p:plain


分割単位

私が提唱するプロシージャの分割単位に名前をつけると、次のような感じになる。

f:id:Kotori-ChunChun:20210627231035p:plain

そもそも、ユーザーインターフェース(UI)ってなに?ライブラリって何?って人は、みんな大好き『わわわ用語辞典』で分かった気になってから読み進めて欲しい。

「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典 - ユーザインタフェース (user interface)

「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典 - ロジック (logic)

「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典 - ライブラリ (library)


それぞれを簡単に説明すると

  • ユーザーインターフェース層は、ユーザーの行動(ボタン、シェイプ、ワークシートイベント、ショートカットキー等)によって起動する最初のVBAプロシージャで、MsgBox / ActiveSheet / Selection と言った、現在の画面の状態を読み取って、処理を実行し、ユーザーに操作を求める「マクロ」を書くところ。
  • メインロジック層は、与えられた引数(オブジェクト等)に対して「やりたいことをする関数」を書くところ。要はプログラムの本体。
  • ライブラリ層は、色々なマクロで使える「データを操作する関数」を書くところ。依存するライブラリ(参照設定)毎にモジュールを分けて、複数のプロジェクトで再利用できるようにしたもの。

となる。


これらプロシージャを書くモジュールは、階層ごとに3種類に分けるか、UIとロジックのアプリ専用モジュールとライブラリ用の汎用モジュールの2種類に分ける場合がある。

f:id:Kotori-ChunChun:20210627231058p:plain

呼び出しの原則

これら3階層に分離した時、絶対に守らなければならない原則がある。

上から下へは呼び出してもよいが、下から上を呼んではならないということだ。

同じ階層なら呼んでも良いが、気をつけないと無限呼び出しになるので、やはり関数毎にも順位付けはしておいたほうが良いだろう。

f:id:Kotori-ChunChun:20210627231117p:plain

これが守れない場合は、設計に不備があるので考え直したほうが良いと言える。


ユーザーインターフェース層とメインロジック層を分ける理由

一言で言えば『ユーザーの使い勝手が落ちるから』である。


例えば、VBAでありがちな『特定のシートに対して何らかの処理を行うマクロ』があったとする(右図)。

そして、開発進めていくうちに『複数のシートに対して同じ処理を行うマクロ』も欲しくなって、先に作成したマクロを呼び出すようにしたとする(左図)

さて、この書き方には致命的な問題があるのだが、わかるだろうか? 読者は一度手を止めて考えて欲しい。

f:id:Kotori-ChunChun:20210627231131p:plain

答えは『確認メッセージと結果表示メッセージが何度も表示される』である。

もちろん、毎回『ActiveSheet』を切り替えているのもあまり良い実装とは言えないが、問題の本質は『一括実行マクロなのに、その都度ユーザーがボタンをクリックしないと終わらない=全然一括じゃない』ということである。


この様な場合に有効なのが、プロシージャをユーザーインターフェース層とメインロジック層に分割することである。

f:id:Kotori-ChunChun:20210627231143p:plain

もし確認メッセージまで共通化するのであれば、ユーザーインターフェース層に中間プロシージャを作ってもよい。また、ロジック層の関数の引数をシートコレクションにして、ループ処理を内包させるという方法もある。が、ここで言いたいこととは違うので割愛する。

メインロジックが呼び出される状況を挙げると次のようなパターンが考えられる。

f:id:Kotori-ChunChun:20210627231155p:plain

というわけで、『ユーザーインターフェース層とメインロジック層を分ける理由』は、『共通プロシージャにユーザーと対話するコードを書くと使い勝手が落ちるから』である。


実際には次のような問題が起こりやすい。

  • シートごとに同じロジックのプロシージャが必要になる(共通化できない状態)
  • 異なる文面の実行確認メッセージボックスが表示される
  • ループのたびに同じ確認メッセージボックスが表示される
  • あるシートのエラーでマクロが終了してしまう
  • 完了メッセージが何度も表示される
  • 総合結果メッセージが表示できない

また、このようにロジックを抽象化することで、テストがやり易くなるというメリットも有る。


メインロジック層とライブラリ層を分ける理由

一言で言えば『他のプロジェクトでも使うから』である。

もう一つは、『しっかりテストしたいから』だが、この話をしても経験がないと実感が湧きにくいので後回しにする。


例えば、『特定の名前のブックが開かれているかどうか確認する関数』は、どのプロジェクトでも使える可能性が高い。

Rem 指定したブックが開かれているか確認する。
Public Function BookExists(BookName As String, _
                            Optional ByRef outFindBook As Excel.Workbook) As Boolean
On Error GoTo ExistErr
    Set outFindBook = Excel.Workbooks(BookName)
On Error GoTo 0
    BookExists = True
    Exit Function
ExistErr:
End Function

こういった関数は、メインルーチンとは別のモジュールに溜めていって、モジュール単位で再利用できるようにしたい。種類ごとに整理したモジュールを、必要に応じてプロジェクトへインポートして活用したい。そのためにプロシージャ・モジュールを分離しておく。

f:id:Kotori-ChunChun:20210627231225p:plain

いきなり本格的なライブラリを作るのは非常に難しいので、あまり難しく考えすぎないほうが良いが、今書いているプログラムは「今回限りのものか」「他でも使えるものか」を考えて、実際に分離してみると関数化のコツみたいなのが段々と身についてくるのでお勧めしたい。


ここからはどんなコードを書けば良いのかを具体的に紹介していくつもりだったが、このままだと寝落ちしそうなので、後編へと続く。

以上


何か御座いましたらコメント欄、またはTwitterからどうぞ♪

それではまた来週♪ ちゅんちゅん(・8・)