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」と出力
コードを実行した結果、期待される出力(シートの状態)は次のようなものです。

最初のコード
早速、標準モジュールに以下のコードを書いて、実行します。
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

3,6,9,12,18のときに「Fizz」、5,10,20のときに「Buzz」、15のときに「FizzBuzz」が表示されていますね。期待される出力を得ることができました。
標準モジュールの注意点
標準モジュールにコードを書きましたが、標準モジュールのコードでセルを操作するとき、シートを指定していない場合はアクティブなシートが対象になってしまいます。
例えば、別のエクセルファイルのシートをアクティブにした状態で、先ほどのコードを実行すれば、その別のエクセルファイルのシートに出力されてしまいます。
以下の例は、「Book2」のA1セルを選択した状態で「Book1」の標準モジュールのコードを実行した場合です。「Book2」のA列に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

やったー!できました!!完成です。
いやいや、ちょっと待ってください。仕様を満たすコードはできましたが仕様の変更があった場合、このコードを変更するのは簡単でしょうか?
例えば、
- 始まりのセルがA5になった。
- A20までの出力だったものがA100までになった。
A100までコードを書くのは大変ですし、打ち間違いなどの可能性なども高くなります。考えただけで頭が痛いですね。
これで満足せず、このコードをリファクタリングしていきましょう。
- 標準モジュールでセルを操作するときは、ブックとシートをきちんと指定する。
- withステートメントを使えば、同じオブジェクトを書くのを省略することができる。
- ブックやシートの指定を省略した場合、標準プロシージャはアクティブシートが対象になり、シートプロシージャは、そのシートが対象になる。
リファクタリング
ところで、リファクタリングとは何でしょうか?Wikipediaによると
リファクタリング (refactoring) とは、コンピュータプログラミングにおいて、プログラムの外部から見た動作を変えずにソースコードの内部構造を整理することである。
wikipedia – リファクタリング
プログラムの出力結果を変えずに、コードを変更することですかね。
きれいなコードになるようにリファクタリングをすることで、変更に強いコードにすることができます。
リファクタリングで一番大事なのは、「外部から見た動作を変えずに」という部分です。リファクタリングしたことで、出力結果がおかしくなれば大変です。
今回の場合は、A1からA20までのセルの値が変わらないようにしなければならないですね。
ということで、B列に正しい値を入力しておきましょう。B列と比較すれば出力された結果が正しいかすぐ確認できます。

リファクタリングの準備はできました。完成していた以下のコードをリファクタリングしていきましょう。
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の出力に取り掛かりましょう。以下のような条件でしたね。
- 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

繰り返しと条件分岐を使って、スマートなコードが書けたのではないのでしょうか。
これでも十分だと思いますが、さらにリファクタリングをしていきましょう。
わかりやすい名前
変数などの名前は、わかりやすいものにしましょう。
コードは、書くよりも読む時間の方が長くなります。できるだけ読みやすいコードを書くように心がけましょう。
あなたは今、難解で複雑なプログラミングの問題に取り掛かっていて、変数名を考える時間なんてもったいないと思って「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

ガード節
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列が同じことを確認してきちんと動作していることを確認しました。
今回の場合は動いているかどうかの確認が簡単でしたが、もっと複雑なコードの場合はどう確認すればいいでしょうか?特に、リファクタリングをするときにはきちんと動作していることを確認しながら行う必要があります。
そのため、リファクタリングときちんと動作しているかのテストは切っても切れない関係です。
「動作するきれいなコード」。それを教えてくれるのが以下の書籍です。テストコードを書いて、ストレスのない開発ライフを始めてみましょう。