マジックナンバーを避ける
以下のテーブルを VBA で操作するとき、あなたならどうされますか?
番号 | 名前 | 住所 | 電話番号 | 商品 | オプション |
---|---|---|---|---|---|
1 | 佐藤 | 北海道 | 090-0000-000 | おもちゃ | オプション1 |
2 | 田中 | 青森 | 090-1111-111 | おかし | オプション2 |
3 | 本田 | 秋田 | 090-2222-222 | ゲーム | オプション3 |
4 | 岡本 | 新潟 | 090-3333-333 | スポーツ | オプション4 |
5 | 山田 | 山口 | 090-4444-444 | 靴 | オプション5 |
プログラムにおいて、マジックナンバーは原則避けるべきコーディングです。
VBA 以外の言語においては、少し学びさえすればマジックナンバーが現れることはほぼ無いかと思いますが、VBA ではなかなか避けられないケースは多いです。
理由は個人的にですが、主に以下のような理由があるように思います。
- 取り扱うデータのほとんどが、シート内のデータや CSV であるため
- メモリ管理が複雑で、メモリ使用量と可読性が反比例することが多い
- 現在主流の言語と振る舞いが大きく異なる(熟練度の問題)
扱うデータの性質上、何も考えずにコーディングすればマジックナンバーが自然と生まれてしまいます。今回は上記のテーブルの例を使って、私がよく使う対処方法をコードと共に紹介したいと思います。
対処方法
個人的にオススメ、Collection と Dictionary
個人的に最もオススメの方法が、Collection オブジェクトと Dictionary オブジェクトを組み合わせた、連想配列パターンです。javascript のような、json ファイルを扱うことの多い言語を習得されている方には馴染みやすい方法だと思います。
------------------------------------------------
'
' 対象Rangeを連想配列に変換します
'
' @param r 変換するRangeオブジェクト
' @return 連想配列
'
'------------------------------------------------
Public Function GetCollection(ByRef r As Range) As Collection
Dim items As Collection, item As Object
Set items = New Collection
Dim row As Long, col As Long
For row = 2 To r.Rows.Count
Set item = CreateObject("Scripting.Dictionary")
For col = 1 To r.Columns.Count
item.Add r(1, col).Value, r(row, col).Value
Next col
items.Add item
Next row
Set GetCollection = items
End Function
もともと 2 次元配列のように扱うことのできる Range オブジェクトを、わざわざ連想配列に置き換えることは冗長に感じるかもしれませんが、可読性という点ではこの方法が一番気に入っています。
変換をかけた後の Range オブジェクトは、json ファイルで例えると以下のような状態になります。
{
"1": {
"番号": "1",
"名前": "佐藤",
"住所": "北海道"
"電話番号": "090-0000-000",
"商品": "おもちゃ",
"オプション": "オプション1"
},
"2": {
"番号": "2",
"名前": "田中",
"住所": "青森"
"電話番号": "090-1111-111",
"商品": "おかし",
"オプション": "オプション2"
},
"3": {...},
"4": {...},
"5": {...}
}
データを取り出すときは、以下のように行います。
Dim items as Collection
Set items = GetCollection(Range)
items(1)("名前") ' → "佐藤"
イテレータ(for each)を使えば、更にわかりやすくなります。
Dim items as Collection, item as Object
Set items = GetCollection(Range)
For Each item in items
item("名前") ' → "佐藤", "田中"...
next
この対処法の欠点はいくつかありますが、まずは速度です。単純にデータを 2 回ループ(配列作成と実処理)させるので、処理速度が落ちます。もう一つは、ヘッダー行の値に重複がないことが条件というところです。重複がある場合・・・など条件を足してしまうと反ってコードを辿りづらくなるため、素直に別の方法を使いましょう。
マジョリティは Enum
マジックナンバーを避ける方法として、最もメジャーだと思われるのが Enum を使った方法です。最初に紹介した Collection, Dictionary を使った方法より初心者にも分かりやすく、メンテナンス性では最も優秀ではないでしょうか。
Enum TableColumn
番号 = 1
名前 = 2
住所 = 3
電話番号 = 4
商品 = 5
オプション = 6
End Enum
定数群を定義しているだけなので、呼び出しは数字を Enum に置き換えるだけで実装可能です。すでに完成しているプログラムのマジックナンバーを取り除く場合も簡単ですね。
Range(1, TableColumn.名前).value ' → "佐藤"
ループさせる場合は以下のような形です。
Dim i as Long
For i = 1 to Range.Rows.Count
Range(i, TableColumn.名前).value ' → "佐藤", "田中"...
Next
この方法については、特にデメリットはないかと思います。迷ったらこの方法を使いましょう。
最もシンプル、定数
この記事をご覧になっている方は全員ご存知だと思ったので紹介するか迷いましたが、最も処理速度の速い方法として、ヘッダー列を1つずつ定義する方法があります。
Private Const TABLE_COLUMN_番号 As Long = 1
Private Const TABLE_COLUMN_名前 As Long = 2
Private Const TABLE_COLUMN_住所 As Long = 3
Private Const TABLE_COLUMN_電話番号 As Long = 4
Private Const TABLE_COLUMN_商品 As Long = 5
Private Const TABLE_COLUMN_オプション As Long = 6
スコープと型を能動的に指定するため、速度は最速です。ただし、最も重たい部分はループ処理だと思いますので、Enum とはほとんど差はできません。コーディングルールなどに指定がない限りは、Enum で良いかと思います。