【VBA】きれいなコードを書くためのヒント

芝の上のノートパソコン プログラミング

VBAを使用して業務改善いいですよね。コードを書いて思ったような結果が出力されたらそれで満足していませんか。

コードには変更がつきものです。変更しやすいコードはきれいなコードです。きれいなコードを書く方法を学んで、もう一歩先のステージに進んでみませんか。

プログラムの仕様

今回は、FizzBuzz問題を例に説明していきます。

Wikipediaによると、FizzBuzz問題は以下のとおりです。

最初のプレイヤーは「1」と数字を発言する。次のプレイヤーは直前のプレイヤーの発言した数字に1を足した数字を発言していく。ただし、3の倍数の場合は「Fizz」(Bizz Buzzの場合は「Bizz」)、5の倍数の場合は「Buzz」、3の倍数かつ5の倍数の場合(すなわち15の倍数の場合)は「Fizz Buzz」(Bizz Buzzの場合は「Bizz Buzz」)を数の代わりに発言しなければならない。

wikipedia – Fizz Buzz

これを参考に、以下の仕様のコードを書いていきます。

「A1」から「A20」までのそれぞれのセルに、そのセルの行番号を出力せよ。

ただし、

  • 3の倍数のときは、数の代わりに「Fizz」と出力
  • 5の倍数のときは、数の代わりに「Buzz」と出力
  • 3と5の両方の倍数のときは、数の代わりに「FizzBuzz」と出力

コードを実行した結果、期待される出力(シートの状態)は次のようなものです。

fizzbuzz-on-sheet

最初のコード

早速、標準モジュールに以下のコードを書いて、実行します。

Option Explicit

Sub main()
    Cells(1, 1).Value = "1"
    Cells(2, 1).Value = "2"
    Cells(3, 1).Value = "Fizz"
    Cells(4, 1).Value = "4"
    Cells(5, 1).Value = "Buzz"
    Cells(6, 1).Value = "Fizz"
    Cells(7, 1).Value = "7"
    Cells(8, 1).Value = "8"
    Cells(9, 1).Value = "Fizz"
    Cells(10, 1).Value = "Buzz"
    Cells(11, 1).Value = "11"
    Cells(12, 1).Value = "Fizz"
    Cells(13, 1).Value = "13"
    Cells(14, 1).Value = "14"
    Cells(15, 1).Value = "FizzBuzz"
    Cells(16, 1).Value = "16"
    Cells(17, 1).Value = "17"
    Cells(18, 1).Value = "Fizz"
    Cells(19, 1).Value = "19"
    Cells(20, 1).Value = "Buzz"
End Sub
first-fizzbuzz

3,6,9,12,18のときに「Fizz」、5,10,20のときに「Buzz」、15のときに「FizzBuzz」が表示されていますね。期待される出力を得ることができました。

標準モジュールの注意点

標準モジュールにコードを書きましたが、標準モジュールのコードでセルを操作するときシートを指定していない場合はアクティブなシートが対象になってしまいます。

例えば、別のエクセルファイルのシートをアクティブにした状態で、先ほどのコードを実行すれば、その別のエクセルファイルのシートに出力されてしまいます。

以下の例は、「Book2」のA1セルを選択した状態で「Book1」の標準モジュールのコードを実行した場合です。「Book2」のA列にFizzBuzzが出力されてしまいました。

not-select-sheet-fizzbuzz

標準プロシージャにコードを書くときは、特別な理由がない限り、ブックとシートを指定して書くようにしましょう。

ブックとシートを指定する

ブックとシートを指定するには「Cells」の前に「Sheet1」を付けます。「Sheet1」がこのブックのSheet1であると示しています。コードを以下のように変更して、別のシートを選択して実行してみましょう。

Option Explicit

Sub main()
    Sheet1.Cells(1, 1).Value = "1"
    Sheet1.Cells(2, 1).Value = "2"
    Sheet1.Cells(3, 1).Value = "Fizz"
    Sheet1.Cells(4, 1).Value = "4"
    Sheet1.Cells(5, 1).Value = "Buzz"
    Sheet1.Cells(6, 1).Value = "Fizz"
    Sheet1.Cells(7, 1).Value = "7"
    Sheet1.Cells(8, 1).Value = "8"
    Sheet1.Cells(9, 1).Value = "Fizz"
    Sheet1.Cells(10, 1).Value = "Buzz"
    Sheet1.Cells(11, 1).Value = "11"
    Sheet1.Cells(12, 1).Value = "Fizz"
    Sheet1.Cells(13, 1).Value = "13"
    Sheet1.Cells(14, 1).Value = "14"
    Sheet1.Cells(15, 1).Value = "FizzBuzz"
    Sheet1.Cells(16, 1).Value = "16"
    Sheet1.Cells(17, 1).Value = "17"
    Sheet1.Cells(18, 1).Value = "Fizz"
    Sheet1.Cells(19, 1).Value = "19"
    Sheet1.Cells(20, 1).Value = "Buzz"
End Sub

別のシートがアクティブでもSheet1にしか出力されなくなりましたね。

でも、コードがごちゃごちゃしました。「Sheet1」が何回も出てきています。これを「with」を使ってまとめましょう。

Option Explicit

Sub main()
    With Sheet1
        .Cells(1, 1).Value = "1"
        .Cells(2, 1).Value = "2"
        .Cells(3, 1).Value = "Fizz"
        .Cells(4, 1).Value = "4"
        .Cells(5, 1).Value = "Buzz"
        .Cells(6, 1).Value = "Fizz"
        .Cells(7, 1).Value = "7"
        .Cells(8, 1).Value = "8"
        .Cells(9, 1).Value = "Fizz"
        .Cells(10, 1).Value = "Buzz"
        .Cells(11, 1).Value = "11"
        .Cells(12, 1).Value = "Fizz"
        .Cells(13, 1).Value = "13"
        .Cells(14, 1).Value = "14"
        .Cells(15, 1).Value = "FizzBuzz"
        .Cells(16, 1).Value = "16"
        .Cells(17, 1).Value = "17"
        .Cells(18, 1).Value = "Fizz"
        .Cells(19, 1).Value = "19"
        .Cells(20, 1).Value = "Buzz"
    End With
End Sub

いい感じですね。

標準モジュールとシートモジュール

VBAでは、標準モジュールのほかに、シートモジュールがあります。シートモジュールは、ブックやシートを省略した場合、シートモジュールのシートのセルが対象になります。

ということは、シートモジュールにコードを書けば、ブックやシートの指定を省略できますね。一番最初のコードをシートモジュールに書けばよさそうです。

標準プロシージャに書いていたコードを削除して、シートモジュールに以下のコードを書きましょう。

ちなみに、シートモジュールはシートにくっついているので、シートをコピーすればシートモジュールもコピーされ、削除すれば削除されてしまいます。

Option Explicit

Sub main()
    Cells(1, 1).Value = "1"
    Cells(2, 1).Value = "2"
    Cells(3, 1).Value = "Fizz"
    Cells(4, 1).Value = "4"
    Cells(5, 1).Value = "Buzz"
    Cells(6, 1).Value = "Fizz"
    Cells(7, 1).Value = "7"
    Cells(8, 1).Value = "8"
    Cells(9, 1).Value = "Fizz"
    Cells(10, 1).Value = "Buzz"
    Cells(11, 1).Value = "11"
    Cells(12, 1).Value = "Fizz"
    Cells(13, 1).Value = "13"
    Cells(14, 1).Value = "14"
    Cells(15, 1).Value = "FizzBuzz"
    Cells(16, 1).Value = "16"
    Cells(17, 1).Value = "17"
    Cells(18, 1).Value = "Fizz"
    Cells(19, 1).Value = "19"
    Cells(20, 1).Value = "Buzz"
End Sub
fizzbuzz-sheet-module

やったー!できました!!完成です。

いやいや、ちょっと待ってください。仕様を満たすコードはできましたが仕様の変更があった場合、このコードを変更するのは簡単でしょうか?

例えば、

  • 始まりのセルがA5になった。
  • A20までの出力だったものがA100までになった。

A100までコードを書くのは大変ですし、打ち間違いなどの可能性なども高くなります。考えただけで頭が痛いですね。

これで満足せず、このコードをリファクタリングしていきましょう。

ここまでのまとめ
  • 標準モジュールでセルを操作するときは、ブックとシートをきちんと指定する。
  • withステートメントを使えば、同じオブジェクトを書くのを省略することができる。
  • ブックやシートの指定を省略した場合、標準プロシージャはアクティブシートが対象になり、シートプロシージャは、そのシートが対象になる。

リファクタリング

ところで、リファクタリングとは何でしょうか?Wikipediaによると

リファクタリング (refactoring) とは、コンピュータプログラミングにおいて、プログラムの外部から見た動作を変えずにソースコードの内部構造を整理することである。

wikipedia – リファクタリング

プログラムの出力結果を変えずに、コードを変更することですかね。

きれいなコードになるようにリファクタリングをすることで、変更に強いコードにすることができます。

リファクタリングで一番大事なのは、「外部から見た動作を変えずに」という部分です。リファクタリングしたことで、出力結果がおかしくなれば大変です。

今回の場合は、A1からA20までのセルの値が変わらないようにしなければならないですね。

ということで、B列に正しい値を入力しておきましょう。B列と比較すれば出力された結果が正しいかすぐ確認できます。

check-fizzbuzz

リファクタリングの準備はできました。完成していた以下のコードをリファクタリングしていきましょう。

Option Explicit

Sub main()
    Cells(1, 1).Value = "1"
    Cells(2, 1).Value = "2"
    Cells(3, 1).Value = "Fizz"
    Cells(4, 1).Value = "4"
    Cells(5, 1).Value = "Buzz"
    Cells(6, 1).Value = "Fizz"
    Cells(7, 1).Value = "7"
    Cells(8, 1).Value = "8"
    Cells(9, 1).Value = "Fizz"
    Cells(10, 1).Value = "Buzz"
    Cells(11, 1).Value = "11"
    Cells(12, 1).Value = "Fizz"
    Cells(13, 1).Value = "13"
    Cells(14, 1).Value = "14"
    Cells(15, 1).Value = "FizzBuzz"
    Cells(16, 1).Value = "16"
    Cells(17, 1).Value = "17"
    Cells(18, 1).Value = "Fizz"
    Cells(19, 1).Value = "19"
    Cells(20, 1).Value = "Buzz"
End Sub

繰り返し、条件分岐を使う

プログラミングといえば繰り返しと条件分岐ですね。Forとifを使ったら良さそうですが、一気に完成コードまでのイメージが湧きません。

こういう場合は、問題を分解して小さく進みましょう。まずは、Forを使って行数をそのまま出力させるようにしましょう。

次のコードを実行すれば1から20までの数字が出力されます。

Option Explicit

Sub main()
    Dim i As Long
    For i = 1 To 20
        Cells(i, 1).Value = i
    Next
End Sub
fizzbuzz-for

次に、仕様のただし書き部分のFizzBuzzの出力に取り掛かりましょう。以下のような条件でしたね。

  • 3の倍数のときは、数の代わりに「Fizz」と出力。
  • 5の倍数のときは、数の代わりに「Buzz」と出力。
  • 3と5の両方の倍数のときは、数の代わりに「FizzBuzz」と出力。

判定する条件は、倍数かどうかを判断できれば良さそうです。

倍数かどうかは「割ったときの余りが0」という方法で確認ができます。VBAでは、余りを求めるのに「Mod演算子」を使います。

以下のコードを実行すれば期待通りの結果が出力されました。

Option Explicit

Sub main()
    Dim i As Long
    For i = 1 To 20
        If (i Mod 3 = 0) And (i Mod 5 = 0) Then
            Cells(i, 1).Value = "FizzBuzz"
        ElseIf i Mod 3 = 0 Then
            Cells(i, 1).Value = "Fizz"
        ElseIf i Mod 5 = 0 Then
            Cells(i, 1).Value = "Buzz"
        Else
            Cells(i, 1).Value = i
        End If
    Next
End Sub
fizzbuzz-for-2

繰り返しと条件分岐を使って、スマートなコードが書けたのではないのでしょうか。

これでも十分だと思いますが、さらにリファクタリングをしていきましょう。

わかりやすい名前

変数などの名前は、わかりやすいものにしましょう。

コードは、書くよりも読む時間の方が長くなります。できるだけ読みやすいコードを書くように心がけましょう。

あなたは今、難解で複雑なプログラミングの問題に取り掛かっていて、変数名を考える時間なんてもったいないと思って「a」や「b」など意味のない変数名をつけてしまうかもしれません。

今は楽かもしれませんが、そのコードを読み返す未来の自分を苦しめることになります。未来の自分を助けるためにも、わかりやすい名前をつけましょう。

繰り返しの際に使っていた変数名を「i」から「row」に変更してみました。少しわかりやすくなったでしょうか。

Option Explicit

Sub main()
    Dim row As Long
    For row = 1 To 20
        If (row Mod 3 = 0) And (row Mod 5 = 0) Then
            Cells(row, 1).Value = "FizzBuzz"
        ElseIf row Mod 3 = 0 Then
            Cells(row, 1).Value = "Fizz"
        ElseIf row Mod 5 = 0 Then
            Cells(row, 1).Value = "Buzz"
        Else
            Cells(row, 1).Value = row
        End If
    Next
End Sub

メソッドの抽出

コードは短ければ短いほどわかりやすく、理解しやすいです。

短くする方法の1つにメソッドの抽出があります。コードのひとかたまりを抽出することで、コードが短くなり、そのコードで何をしたいのかわかりやすくなります。

メソッドの名前についてもわかりやすいものをつけるように心がけましょう。

以下のコードでは、FizzBuzzの判定部分をメソッドに抽出しました。VBAでは戻り値がある場合は、Functionプロシージャーを使います。

Option Explicit

Sub main()
    Dim row As Long
    For row = 1 To 20
        Cells(row, 1).Value = fizzBuzz(row)
    Next
End Sub

Function fizzBuzz(ByVal number As Long) As String
    If (number Mod 3 = 0) And (number Mod 5 = 0) Then
        fizzBuzz = "FizzBuzz"
    ElseIf number Mod 3 = 0 Then
        fizzBuzz = "Fizz"
    ElseIf number Mod 5 = 0 Then
        fizzBuzz = "Buzz"
    Else
        fizzBuzz = number
    End If
End Function

メソッドを抽出すれば、読みやすくなるというメリットのほかにも、そのメソッドが正しく動いているかテストするのが簡単になるというメリットもあります。

例えば、メソッドの抽出をしていないコードで、「96」で「Fizz」が出力されるか確認しようとすれば、実際にA96のセルに出力させる必要があります。

しかし、22行目以降にすでに何か入力があった場合、コードを実行すればA列に出力されるので元の入力が変わってしまいます。確かめるために、実行前に保存しておいて実行後に保存せずに終了しましょうか?それとも、別のシートを作成し、そこで実行させましょうか?

いずれにせよ、簡単に確かめるのはむずかしそうです。

それに比べて、メソッドの抽出をして独立させれば、以下のように簡単に確認することができます。「testFizzBuzz()」を実行すればイミディエイトウィンドで結果を確認できます。

イミディエイトウィンドウを表示させる方法は、「表示」→「イミディエイトウィンドウ」または「Ctrl+G」です。

Option Explicit

Sub main()
    Dim row As Long
    For row = 1 To 20
        Cells(row, 1).Value = fizzBuzz(row)
    Next
End Sub

Function fizzBuzz(ByVal number As Long) As String
    If (number Mod 3 = 0) And (number Mod 5 = 0) Then
        fizzBuzz = "FizzBuzz"
    ElseIf number Mod 3 = 0 Then
        fizzBuzz = "Fizz"
    ElseIf number Mod 5 = 0 Then
        fizzBuzz = "Buzz"
    Else
        fizzBuzz = number
    End If
End Function

Sub testFizzBuzz()
    Debug.Print fizzBuzz(96) 
End Sub
fizzbuzz-debug-print

ガード節

if文は便利ですが、条件が多くなったりネストが深くなればなるほど理解するのがむずかしくなります。

以下のコードで、「Bの処理」はどういう条件で実行されるのか簡単にはわかりません。

if aのとき
  if bのとき
    if cのとき
      Aの処理
    else
      Bの処理
  else
    Cの処理

条件分岐のコードをわかりやすくする方法に、elseを使わないようにするというものがあります。そして、すぐにリターンすることで脳のリソースの使用量を抑えることができ、わかりやすくすることができます。

elseを使わないで、早期リターンするのをガード節といいます。elseを使わないようにして、「Exit Function」で呼び出し元に戻しています。

Option Explicit

Sub main()
    Dim row As Long
    For row = 1 To 20
        Cells(row, 1).Value = fizzBuzz(row)
    Next
End Sub

Function fizzBuzz(ByVal number As Long) As String
    If (number Mod 3 = 0) And (number Mod 5 = 0) Then
        fizzBuzz = "FizzBuzz"
        Exit Function
    End If
    
    If number Mod 3 = 0 Then
        fizzBuzz = "Fizz"
        Exit Function
    End If
    
    If number Mod 5 = 0 Then
        fizzBuzz = "Buzz"
        Exit Function
    End If
    
    fizzBuzz = number
End Function

条件式をメソッドに抽出

条件式の判定をメソッドに抽出してみましょう。条件の判定なので真偽値を返すメソッドを作成します。真偽値の名前には「isXXX」とつけます。

Option Explicit

Sub main()
    Dim row As Long
    For row = 1 To 20
        Cells(row, 1).Value = fizzBuzz(row)
    Next
End Sub

Function fizzBuzz(ByVal number As Long) As String
    If isFizzBuzz(number) Then
        fizzBuzz = "FizzBuzz"
        Exit Function
    End If
    
    If isFizz(number) Then
        fizzBuzz = "Fizz"
        Exit Function
    End If
    
    If isBuzz(number) Then
        fizzBuzz = "Buzz"
        Exit Function
    End If
    
    fizzBuzz = number
End Function

Function isFizzBuzz(ByVal number As Long) As Boolean
    If number Mod 3 = 0 And number Mod 5 = 0 Then
        isFizzBuzz = True
        Exit Function
    End If
    
    isFizzBuzz = False
End Function

Function isFizz(ByVal number As Long) As Boolean
    If number Mod 3 = 0 Then
        isFizz = True
        Exit Function
    End If
    
    isFizz = False
End Function

Function isBuzz(ByVal number As Long) As Boolean
    If number Mod 5 = 0 Then
        isBuzz = True
        Exit Function
    End If
    
    isBuzz = False
End Function

これでも良さそうですが、もう少しリファクタリングしてみましょう。メソッドの抽出をすれば、そのメソッドが部品として使いまわしができるというメリットもあります。

メソッドとして独立したので、同様の処理をしたいときにそのメソッドを呼び出せばよくなります。

「isFizzBuzz()」の中の条件を「isFizz()」と「isBuzz()」に書き換えましょう。

Option Explicit

Sub main()
    Dim row As Long
    For row = 1 To 20
        Cells(row, 1).Value = fizzBuzz(row)
    Next
End Sub

Function fizzBuzz(ByVal number As Long) As String
    If isFizzBuzz(number) Then
        fizzBuzz = "FizzBuzz"
        Exit Function
    End If
    
    If isFizz(number) Then
        fizzBuzz = "Fizz"
        Exit Function
    End If
    
    If isBuzz(number) Then
        fizzBuzz = "Buzz"
        Exit Function
    End If
    
    fizzBuzz = number
End Function

Function isFizzBuzz(ByVal number As Long) As Boolean
    If isFizz(number) And isBuzz(number) Then
        isFizzBuzz = True
        Exit Function
    End If
    
    isFizzBuzz = False
End Function

Function isFizz(ByVal number As Long) As Boolean
    If number Mod 3 = 0 Then
        isFizz = True
        Exit Function
    End If
    
    isFizz = False
End Function

Function isBuzz(ByVal number As Long) As Boolean
    If number Mod 5 = 0 Then
        isBuzz = True
        Exit Function
    End If
    
    isBuzz = False
End Function

上のコードを実行しても特に出力結果は変わりませんね。

まとめ

同じFizzBuzzを出力するコードですが、全く違うコードが出来上がりました。一番初めと最後のコードを比べてみましょう。

Option Explicit

Sub main()
    Cells(1, 1).Value = "1"
    Cells(2, 1).Value = "2"
    Cells(3, 1).Value = "Fizz"
    Cells(4, 1).Value = "4"
    Cells(5, 1).Value = "Buzz"
    Cells(6, 1).Value = "Fizz"
    Cells(7, 1).Value = "7"
    Cells(8, 1).Value = "8"
    Cells(9, 1).Value = "Fizz"
    Cells(10, 1).Value = "Buzz"
    Cells(11, 1).Value = "11"
    Cells(12, 1).Value = "Fizz"
    Cells(13, 1).Value = "13"
    Cells(14, 1).Value = "14"
    Cells(15, 1).Value = "FizzBuzz"
    Cells(16, 1).Value = "16"
    Cells(17, 1).Value = "17"
    Cells(18, 1).Value = "Fizz"
    Cells(19, 1).Value = "19"
    Cells(20, 1).Value = "Buzz"
End Sub
Option Explicit

Sub main()
    Dim row As Long
    For row = 1 To 20
        Cells(row, 1).Value = fizzBuzz(row)
    Next
End Sub

Function fizzBuzz(ByVal number As Long) As String
    If isFizzBuzz(number) Then
        fizzBuzz = "FizzBuzz"
        Exit Function
    End If
    
    If isFizz(number) Then
        fizzBuzz = "Fizz"
        Exit Function
    End If
    
    If isBuzz(number) Then
        fizzBuzz = "Buzz"
        Exit Function
    End If
    
    fizzBuzz = number
End Function

Function isFizzBuzz(ByVal number As Long) As Boolean
    If isFizz(number) And isBuzz(number) Then
        isFizzBuzz = True
        Exit Function
    End If
    
    isFizzBuzz = False
End Function

Function isFizz(ByVal number As Long) As Boolean
    If number Mod 3 = 0 Then
        isFizz = True
        Exit Function
    End If
    
    isFizz = False
End Function

Function isBuzz(ByVal number As Long) As Boolean
    If number Mod 5 = 0 Then
        isBuzz = True
        Exit Function
    End If
    
    isBuzz = False
End Function

コード量はすごい増えてしまいました。しかし、後者のコードの方がテストがしやすく変更にも強いのではないでしょうか。

きれいなコードを書くために、以下のことを意識してみてください。

  • わかりやすい名前を付ける
  • elseを極力使わないようにして早期リターンする
  • メソッドの抽出をおこなう
  • 真偽値(true/false)は、isXXXなどと名前を付ける

リファクタリングとテスト

今回の記事では、まず期待するコードを書いてから、そのコードをリファクタリングしてきました。

リファクタリングするには、コードがきちんと動作していることを確認する必要があります。今回は、期待される出力結果をB列に書いておいて、動作後にA列とB列が同じことを確認してきちんと動作していることを確認しました。

今回の場合は動いているかどうかの確認が簡単でしたが、もっと複雑なコードの場合はどう確認すればいいでしょうか?特に、リファクタリングをするときにはきちんと動作していることを確認しながら行う必要があります。

そのため、リファクタリングときちんと動作しているかのテストは切っても切れない関係です。

「動作するきれいなコード」。それを教えてくれるのが以下の書籍です。テストコードを書いて、ストレスのない開発ライフを始めてみましょう。

参考

タイトルとURLをコピーしました