t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

VBA モジュールのプロシージャは呼び出し順に書く

VBAでは互いに関連するプロシージャをひとつのモジュールに纏めることが多い。
このとき、呼び出す側と呼ばれる側のどちらを先に書けば良いだろうか。

以前の私の考えでは、例えばProc1がProc2を呼び出すとき、呼ばれる側を先に定義しておくという意味で先に書くことがあった。

Sub Proc2()
    MsgBox "Hello"
End Sub

Sub Proc1()
    Call Proc2
End Sub

(ただし、VBAの場合はどの順で書いても動くので、過去のコードで厳密にこの原則を守っていたわけではない)

最近Clean Codeという書籍を読んで、完全に考えが変わった。

Clean Code アジャイルソフトウェア達人の技

Clean Code アジャイルソフトウェア達人の技

※書籍のサンプルコードはJavaで書かれてますが考え方はすべての言語に共通します。

Clean Codeによると、呼び出し側を上に、呼ばれる側が下になるのが望ましいとのこと。
たしかに、物事の説明の流れからしても概要→詳細となるのでコードにおいても粒度の大きいものを先頭に持ってくるというのは理にかなっている。
そして、可能な限り呼ばれる方のプロシージャは、呼び出すプロシージャの直下に置くこと。

マクロが複雑化し、プロシージャが増えれば増えるほど、プロシージャ同士の垂直方向の距離に気を配らないと読みづらくなり、理解に支障をきたす。

例えば次のプロシージャ群をひとつのモジュールに纏めるとする。

Sub Proc1()
    Debug.Print "Proc1"
    Call Proc2
    Call Proc3
    Call Proc4
End Sub
Sub Proc2()
    Debug.Print "Proc2"
    Call Proc5
End Sub
Sub Proc3()
    Debug.Print "Proc3"
    Call Proc6
End Sub
Sub Proc4()
    Debug.Print "Proc4"
    Call Proc7
End Sub
Sub Proc5()
    Debug.Print "Proc5"
End Sub
Sub Proc6()
    Debug.Print "Proc6"
End Sub
Sub Proc7()
    Debug.Print "Proc7"
End Sub

呼び出し関係を図で表すと、こうなる。
f:id:t-hom:20180614000352p:plain

冒頭に述べた「呼ばれる側を先に定義しておく」とは矛盾するが、私は階層順という考えも取り入れていた。
f:id:t-hom:20180614000627p:plain

まさに先ほどのコードは階層順に並べたものだ。

しかしClean Codeを読んで以来、プロシージャは以下のように呼び出し順に並べるのが最も理解しやすいと考えるようになった。
f:id:t-hom:20180614001235p:plain

改善したコードがこちら。

Sub Proc1()
    Debug.Print "Proc1"
    Call Proc2
    Call Proc3
    Call Proc4
End Sub
Sub Proc2()
    Debug.Print "Proc2"
    Call Proc5
End Sub
Sub Proc5()
    Debug.Print "Proc5"
End Sub
Sub Proc3()
    Debug.Print "Proc3"
    Call Proc6
End Sub
Sub Proc6()
    Debug.Print "Proc6"
End Sub
Sub Proc4()
    Debug.Print "Proc4"
    Call Proc7
End Sub
Sub Proc7()
    Debug.Print "Proc7"
End Sub

最近の例では、自動スクリーンショットを取るためのマクロでプロシージャの並び替えを行った。
呼び出しマップ関係図はこちら。
f:id:t-hom:20180613234407p:plain

並び替えのビフォーアフターはこのようになった。
Before

Private Sub clearClipboard()
Private Sub animateCaption()
Public Sub StartAutoScrap()
Public Sub StopAutoScrap()
Public Sub OnTimeScrap(Optional ByRef void = Empty)
Private Sub popUpWindow()
Private Property Get prevWindow() As Long
Private Property Get lowestShapeEdge() As Single

After

Public Sub StartAutoScrap()
Public Sub StopAutoScrap()
Public Sub OnTimeScrap(Optional ByRef void = Empty)
Private Sub animateCaption()
Private Property Get lowestShapeEdge() As Single
Private Sub popUpWindow()
Private Property Get prevWindow() As Long
Private Sub clearClipboard()

OnTimeScrapは4つのプロシージャを呼び出しているが、コードに登場する順に並べた。
f:id:t-hom:20180614003552p:plain

呼び出し関係と並び順だけ取り出して図示すると、こんな感じ。
Before
f:id:t-hom:20180614002623p:plain

After
f:id:t-hom:20180614002644p:plain

変更後は呼び出しの矢印が常に右に伸びていてわかりやすい。
図中のBだけ独立しているのが気になる方がいると思う。
このBはStopAutoScrapを表しており、StartAutoScrapと機能的に対になるマクロなのでStartAutoScrapのすぐ下に置いている。

呼び出しの原則に従うと、こうするか、

Public Sub StopAutoScrap()
Public Sub StartAutoScrap()
Public Sub OnTimeScrap(Optional ByRef void = Empty)
Private Sub animateCaption()
Private Property Get lowestShapeEdge() As Single
Private Sub popUpWindow()
Private Property Get prevWindow() As Long
Private Sub clearClipboard()

こうすることになるけれど、

Public Sub StartAutoScrap()
Public Sub OnTimeScrap(Optional ByRef void = Empty)
Private Sub animateCaption()
Private Property Get lowestShapeEdge() As Single
Private Sub popUpWindow()
Private Property Get prevWindow() As Long
Private Sub clearClipboard()
Public Sub StopAutoScrap()

どちらも微妙なので、今回は呼び出し関係よりも機能的な相対関係を優先した。

以上

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。