読者です 読者をやめる 読者になる 読者になる

t-hom’s diary

主にVBAネタを扱っているブログです。

VBA 私がコードを整理するときの思考プロセスを紹介

VBA VBAについての考え・意見

今回は私が普段どんなことを考えながらコードを整理しているのかを紹介する。
題材としてはなるべくシンプルなものがよかったので、カレンダーのヘッダー部分を作成するマクロを取り上げた。
最初に書いたコードがどういう風に変化していくか、またなぜそのように変更したのかに注目していただければと思う。

最初のコード

まずはよく初心者が書くようなコード。
Book1.xlsmの、標準モジュールに書く想定である。

Sub カレンダー()
    Workbooks("Book1.xlsm").Sheets(1).Activate
    Range("B2").Value = "日"
    Range("C2").Value = "月"
    Range("D2").Value = "火"
    Range("E2").Value = "水"
    Range("F2").Value = "木"
    Range("G2").Value = "金"
    Range("H2").Value = "土"
End Sub

コードを書きながら常に考えるのは変更に強いコードを書くということだ。
変更に強いとは、将来何かが変更された場合にそのままうまく動作する、またはコードの修正が容易であるということだ。

上のコードでまず目につくのはWorkbooks("Book1.xlsm")の部分。
これだとブックの名前が変わっただけで動かなくなる。
他のブックに書き込むのなら別だけれど、今回は同一ブック内にマクロを作る想定なので、これはThisWorkbookに変更しよう。

2番目のコード

さて、自身のブックを指すのにブック名で指定するのをやめて、ThisWorkbookに変更してみた。
これでファイル名が変更されても動作する。ひとつ変更に強くなったわけだ。

Sub カレンダー2()
    ThisWorkbook.Sheets(1).Activate
    Range("B2").Value = "日"
    Range("C2").Value = "月"
    Range("D2").Value = "火"
    Range("E2").Value = "水"
    Range("F2").Value = "木"
    Range("G2").Value = "金"
    Range("H2").Value = "土"
End Sub

次に考えるのが、依存関係を断ち切るということだ。

Rangeへのアクセスは親オブジェクトを指定しない場合、ActiveWorkbookのActiveSheetのRangeという意味になる。
つまり、Rangeへのアクセスはすべて、先頭に書いたThisWorkbook.Sheets(1).Activateに依存している。

Active○○はExcelが管理する超グローバル変数のようなもので、ユーザーもプログラムもここにアクセスできる。便利ではあるが、その時々で指すものが変わるオブジェクトなので、このように特定シートを指す目的で使うべきではない。

つまり「今どのシートがアクティブか」という状況に依存しているので、これを断ち切る。

※ユーザーの領分を侵しているという点でもActive○○の乱用はよろしくない。
thom.hateblo.jp

3番目のコード

さて、状況依存を避けるために親オブジェクトをきちんと指定することにした。

Sub カレンダー3()
    ThisWorkbook.Sheets(1).Range("B2").Value = "日"
    ThisWorkbook.Sheets(1).Range("C2").Value = "月"
    ThisWorkbook.Sheets(1).Range("D2").Value = "火"
    ThisWorkbook.Sheets(1).Range("E2").Value = "水"
    ThisWorkbook.Sheets(1).Range("F2").Value = "木"
    ThisWorkbook.Sheets(1).Range("G2").Value = "金"
    ThisWorkbook.Sheets(1).Range("H2").Value = "土"
End Sub

次に考えるのは、コードの重複をまとめるということ。
親オブジェクトを持ってきたのはいいけれど、これじゃ長ったらしくてみっともない。
また、シートが1から2に変更になったら7箇所の変更が生じる。

4番目のコード

With文を用いて、親オブジェクトを一か所にまとめてみた。

Sub カレンダー4()
    With ThisWorkbook.Sheets(1)
        .Range("B2").Value = "日"
        .Range("C2").Value = "月"
        .Range("D2").Value = "火"
        .Range("E2").Value = "水"
        .Range("F2").Value = "木"
        .Range("G2").Value = "金"
        .Range("H2").Value = "土"
    End With
End Sub

あれ?これも依存なんじゃないの?
はい、そのとおり。

でもユーザー操作に影響を与える・受けるActive○○と違って、Withはプログラム上で一時的に作成される箱庭みたないもの。End Withとともに消滅するので後腐れがない。Active○○が自分で戻すまでActiveにしっぱなしなのとは対照的である。

さて、まだまだ課題はある。
変更に強いコードといいながら、肝心のセル指定がベタ書きだ。
これでは"日"の記入位置をC3にしようと思ったら全部書き換えないといけない。

5番目のコード

セルアドレスのベタ書きでは、記入位置の変更時に面倒なので、Range指定をやめてCellsで行と列を変数で管理してみた。

Sub カレンダー5()
    Dim x As Long
    Dim y As Long
    x = 2
    y = 2
    With ThisWorkbook.Sheets(1)
        .Cells(y, x).Value = "日"
        .Cells(y, x + 1).Value = "月"
        .Cells(y, x + 2).Value = "火"
        .Cells(y, x + 3).Value = "水"
        .Cells(y, x + 4).Value = "木"
        .Cells(y, x + 5).Value = "金"
        .Cells(y, x + 6).Value = "土"
    End With
End Sub

これでxとyを変更すれば、開始位置をコントロールできる。
でもxに足す値が1~6まで順に並んでるんだから、これもFor文でまとめられそうだ。
でも"日"から"土"まではどうすれば良いだろうか。

6番目のコード

実は、"日"~"土"の文字列もFormat関数を使うとループさせることができる。
ここで考えるのは、実験的な小さなマクロを作るということ。

あるロジックに集中したいとき、メインのコードをそのままいじってしまうと動かなくなったり、あるいは他のロジックが邪魔してうまく考えられない。

そんなときは、小さなマクロを別に作って実現したいロジックに集中するのだ。

今回やりたいのは、"日"~"土"の文字列ループ。そこで、以下のように適当なマクロを作り、その中で考える。

Sub hoge()
    'どうにかする
End Sub

失敗してもメインコードを壊さないように、箱庭を作るのだ。

まず曜日のループで使えそうなのは、Format関数である。
Format関数は、数値や日付を書式指定の文字列に整形してくれる関数だ。たとえばFormat(Now, "yyyymmdd")と書くと、"20161223"という風に日付を所定の文字列で返してくれる。

この書式指定に"aaa"を渡すと、曜日の文字列を返してくれるのだ。
つまり適当な日曜の日付を初期値をして選び、1日ずつ足しながらFormat関数で曜日を取り出すという処理が可能。

日付型は1を足すと1日後になるので、以下のように足す数をFor文で増やしていけば"日"~"土"まで出力される。

Sub hoge()
    For i = 0 To 6
        日付 = CDate("2016/12/25") + i
        Debug.Print Format(日付, "aaa")
    Next
End Sub

さらに、そんなことしなくても実はVBAの日付型ってDouble型で管理されているので最初から数値で管理することもできる。
Excelに日付を値貼り付けしたら実数になった経験があると思うけれど、あれと同じ。
整数部が日付、小数部が時刻になっていて、シリアル値と呼ばれる。
今回ほしいのは日付だけなので、整数とみなしてよい。

それで整数1を日付型に変換すると、1899/12/31が取得できる。
これ、都合の良いことに、日曜日なのだ。

ということで、"日"から"土"までループで出力するには、単に1~7をFormatで曜日に変換すれば良いことになる。

Sub hoge()
    For i = 1 To 7
        Debug.Print Format(i, "aaa")
    Next
End Sub

ここまでで箱庭は終了。メインコードに組み込んでいく。
できたのがこちら。

Sub カレンダー6()
    Dim x As Long
    Dim y As Long
    Dim i As Long
    x = 2
    y = 2
    With ThisWorkbook.Sheets(1)
        For i = 1 To 7
            .Cells(y, x + i - 1).Value = Format(i, "aaa")
        Next
    End With
End Sub

7番目のコード

さて、次に考えるのは変数のスコープを縮めるということ。
これについてはこちらの記事で書いたので説明を省略。
thom.hateblo.jp

コードは少しコンパクトになった。

Sub カレンダー7()
    Dim x As Long: x = 2
    Dim y As Long: y = 2
    Dim i As Long
    With ThisWorkbook.Sheets(1)
        For i = 1 To 7
            .Cells(y, x + i - 1).Value = Format(i, "aaa")
        Next
    End With
End Sub

8番目のコード

こうなると、Withも煩わしいので外してしまう。

Sub カレンダー8()
    Dim x As Long: x = 2
    Dim y As Long: y = 2
    Dim i As Long
    For i = 1 To 7
        ThisWorkbook.Sheets(1).Cells(y, x + i - 1).Value = Format(i, "aaa")
    Next
End Sub

もともと親オブジェクトを複数行書くのが面倒だからWithでまとめたので、1行になってしまえばWithのほうが行数が増えてしまう。

9番目のコード

Withを外したら今度は一行がごちゃっとしてしまったので変数shに変更。

Sub カレンダー9()
    Dim x As Long: x = 2
    Dim y As Long: y = 2
    Dim i As Long
    Dim sh As Worksheet: Set sh = ThisWorkbook.Sheets(1)
    For i = 1 To 7
        sh.Cells(y, x + i - 1).Value = Format(i, "aaa")
    Next
End Sub

ただやはり代入の部分がややこしい。
問題はおそらく(y, x + i - 1)である。

10番目のコード

ということで、直観的にわかるRangeプロパティに戻し、オフセットで位置決めすることに。

Sub カレンダー10()
    Dim i As Long
    Dim sh As Worksheet: Set sh = ThisWorkbook.Sheets(1)
    For i = 1 To 7
        sh.Range("B2").Offset(0, i - 1).Value = Format(i, "aaa")
    Next
End Sub

それでもやはり(0, i - 1)が気になる。つまり、曜日のスタートが1なのに対し、オフセット値は0スタートだからそこのズレを埋めるのにマイナス1しているのだ。iを0スタートにすると今度Format関数がi+1になってしまう。

あちらを立てればこちらが立たず。困った。

11番目のコード

どうしてもiの差分調整が気に入らないので、最近よくやるオブジェクト変数の中身を入れ替えていく手法を採用してみた。

Sub カレンダー11()
    Dim i As Long
    Dim sh As Worksheet: Set sh = ThisWorkbook.Sheets(1)
    Dim r As Range: Set r = sh.Range("B2")
    For i = 1 To 7
        r.Value = Format(i, "aaa")
        Set r = r.Offset(0, 1)
    Next
End Sub

変数iは1スタートで曜日だけを管理し、記入先のセルはRangeオブジェクトでSet r = r.Offset(0, 1)を呼ぶ度に右にズレていくという方法。

12番目のコード

記入先セルをオブジェクト変数での管理にしたなら、変数shは要らなくなり、直接変数rに対象セルを入れたら良い。
そのように変更した。

Sub カレンダー12()
    Dim i As Long
    Dim r As Range: Set r = ThisWorkbook.Sheets(1).Range("B2")
    For i = 1 To 7
        r.Value = Format(i, "aaa")   'Format(1~7,"aaa") ⇒ 日~土
        Set r = r.Offset(0, 1)
    Next
End Sub

それから、Format関数が直観的にわかるようにコメントを記入。

13番目のコード

ここでまた、変更に強いコードを考える。
確かに変更箇所は一か所にまとまったけれど、これはヘッダーの開始位置に限った話だ。
カレンダーの中身を書き出すとまたそのスタートセルを考える必要があり、最終的に開始セルの管理が2箇所に散らばってしまう。

そこでヘッダーを作成するコードでは開始セルを管理せず、独立したプロシージャ化して引数で持ってくるという手法を考える。

つまり、曜日ヘッダー作成プロシージャを作り、メインマクロのカレンダー13から呼び出す形に変更するのだ。

Sub 曜日ヘッダー作成(開始セル As Range)
    Dim i As Long
    Dim r As Range: Set r = 開始セル
    For i = 1 To 7
        r.Value = Format(i, "aaa")   'Format(1~7,"aaa") ⇒ 日~土
        Set r = r.Offset(0, 1)
    Next
End Sub

Sub カレンダー13()
    曜日ヘッダー作成 開始セル:=ThisWorkbook.Sheets(1).Range("B2")
End Sub

曜日ヘッダー作成プロシージャは記入位置を直接Range型で受け取るので、メインコードから開始セルをそのまま渡してやれば日~土まで記入される。

曜日ヘッダー作成プロシージャは、わずか6行!

。。。



えっ?



。。。あと1行で曜日の数と一緒やん。

14番目のコード

Sub 曜日ヘッダー作成2(開始セル As Range)
    開始セル.Offset(0, 0) = "日"
    開始セル.Offset(0, 1) = "月"
    開始セル.Offset(0, 2) = "火"
    開始セル.Offset(0, 3) = "水"
    開始セル.Offset(0, 4) = "木"
    開始セル.Offset(0, 5) = "金"
    開始セル.Offset(0, 6) = "土"
End Sub

Sub カレンダー14()
    曜日ヘッダー作成2 開始セル:=ThisWorkbook.Sheets(1).Range("B2")
End Sub

完成~!

いやいや、いままで散々こねくり回したのは何だったのか。
悪い夢でも見ていたようだ。

まとめ

まさにシンプル・イズ・ベスト。
KISSの法則(Keep It Simple, Stupid!!!) について再考させられた。
訳) ややこしいことすな、アホ!の法則

↓まぁ他にもいろいろ試してみたんだけれど、

Sub 曜日ヘッダー作成3(開始セル As Range)
    Dim i As Long
    For i = 0 To 6
        開始セル.Offset(0, i) = Format(i + 1, "aaa")
    Next
End Sub

Sub 曜日ヘッダー作成4(開始セル As Range)
    Dim i As Long
    For i = 0 To 6
        開始セル.Offset(0, i) = Split("日 月 火 水 木 金 土")(i)
    Next
End Sub

コード理解のコストと天秤にかけると、今回のケースでは曜日ヘッダー作成2がベストかなと思う。

経験を積んだプログラマーにとってコードの重複はどうしても気持ち悪く見えてしまうのだが、なんでもかんでも重複排除すれば良いってものではない。

一瞥しただけで処理が把握できるようなコードを書けたらベストだ。とはいえ、なんでもかんでも愚直に書くとそれはそれでコードの肥大化を招いてそもそも長すぎて一瞥できないなんてことにもなるし、修正箇所が多くなって変更にも弱くなる。

逆に少々トリッキーなコードになってもプロシージャや変数の名前をきっちりわかりやすくしておけば中身を詳細に読まなくてもやってることは容易に想像できるし、複雑なテクニックを用いた箇所はコメントで補足すればよい。

このへんのバランス感覚は非常に大事だと思う。

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