t-hom’s diary

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

VBA 循環的複雑度という指標でプロシージャの複雑度を測ってみる

VBAに限らずすべての言語に言えることだが、プロシージャが複雑になればなるほどバグが混入しやすくなる。

そこで今回はプロシージャの複雑度を測る「循環的複雑度」という指標を紹介する。
何がどう循環的なのか知らないけど、名前が醸し出すややこしさとは反対に、とてもシンプルに計算できるので身構える必要はない。

まず押さえておきたいのは、この指標は10以下が理想的で、30を超えるとヤバいということ。

数え方

以下の参考サイトに次の表記がある。

関数の循環的複雑度 = 1 + 関数の中に以下のものが出てきた回数:
if、while、for、foreach、case、default、continue、goto、&&、||、catch、?: (三項演算子)、??

参考:D.O.R.Y. : Dory Offers a Room for You: 循環的複雑度 ( Cyclomatic Complexity ) とは何ぞや


つまりこれをVBAの用語に翻訳すると、

プロシージャの循環的複雑度 = 1 + プロシージャの中に以下のものが出てきた回数:
If、ElseIf、Do、For、For Each、Case、GoTo、And、Or、Wend、IIf、Switch、Choose

ということになる。

※WhileではなくWendとしたのは、Do文のWhileキーワードと、While~Wend文のWhileキーワードが別物だから。
※Caseは、Select Case文の中身のCaseだけ数える。Selectキーワードの直後に来るCaseはノーカウント。

抜け漏れがあるかもしれないけど、主だったものは概ねカバーできてるはず。

勘違いしやすいポイントだが、Elseはカウントしない。
なぜならElseが明示的に書かれようと書かれまいと、分岐経路の数は変わらないから。
f:id:t-hom:20180809013032p:plain

※ElseIfは別途経路が発生するのでカウントする。

ForやDoなどの繰り返しも、終了条件を満たしたらスキップされるという点で、分岐処理の一種といえる。
イメージしにくければ、最初から終了条件を満たしていればループの内部は一切通らないという点を考えてみると良い。

AndやOrは条件を集約しているが、以下のように分解して考えると経路の分岐にカウントされる理由が分かると思う。

↓Andの場合

'元のコード
If A And B Then
    Result1
Else
    Result2
End If

'条件を分解したコード
If A Then
    If B Then
         Result1
    Else
         Result2
    End If
End If

↓Orの場合

'元のコード
If A Or B Then
    Result1
Else
    Result2
End If

'条件を分解したコード
If A Then
    Result1
ElseIf B Then
    Result1
Else
    Result2
End If

具体例

今私が作ってるマクロのプロシージャを例に循環的複雑度を測ってみる。
コードはこちら。

Sub ChangeProcessType(TargetShape As Shape, T As MsoAutoShapeType)
    Dim processConnectors As New Collection
    Dim s As Shape
    
    '現在TargetShapeに接続されたコネクタを一覧化しておく
    For Each s In TargetShape.Parent.Shapes
        If s.Connector Then
            If s.ConnectorFormat.BeginConnected Then
                If s.ConnectorFormat.BeginConnectedShape Is TargetShape Then
                    processConnectors.Add _
                        Array(s, s.ConnectorFormat.BeginConnectionSite, True) 'True=Begin
                End If
            End If
            If s.ConnectorFormat.EndConnected Then
                If s.ConnectorFormat.EndConnectedShape Is TargetShape Then
                    processConnectors.Add _
                        Array(s, s.ConnectorFormat.EndConnectionSite, False) 'False=End
                End If
            End If
        End If
    Next
    
    'シェイプタイプを切り替える。
    TargetShape.AutoShapeType = T
    'このとき、コネクタの接続が外れる
    
    '一覧に登録されたコネクタをTargetShapeに再接続する
    Dim c As Variant
    For Each c In processConnectors
        Dim processConnector As Shape: Set processConnector = c(0)
        If c(2) Then 'True=Begin, False=End
            processConnector.ConnectorFormat.BeginConnect TargetShape, c(1)
        Else
            processConnector.ConnectorFormat.EndConnect TargetShape, c(1)
        End If
    Next
End Sub

赤丸をつけて数えてみると、該当するものが8個。
f:id:t-hom:20180809014517p:plain

初期値が1なので、1+8 = 9
循環的複雑度は9という結果になった。
10未満なのでこのプロシージャの複雑度は、まずまず良好と言える。

参考

複雑度の指標
szk-takanori.hatenablog.com

複雑度の計算方法
saturday-dory-fever.blogspot.com


ちなみにRubberduckというVBEのアドインを使うと、循環的複雑度(Cycromatic Complexity)を自動的に計測してくれる。
Rubberduckは導入するとVBエディタの初回起動がもたつくのと、そもそもPCスペックがそれなりにないと重たいのと、すべて英語なのと、設計思想が本業プログラマー寄りなので、万人にオススメできるものではない。
でもとても便利なツールなので興味があれば導入してみると良いかもしれない。

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