和風スパゲティのレシピ

日本語でコーディングするExcelVBA

マジックナンバーを減らす方法6選

マジックナンバーという用語をご存知でしょうか?


簡単に言うと、

Cells(R, 4) = Cells(R, 2) * Cells(R, 3)

この2、3、4など、コード中に直接入力された数値を指す用語です。


この3つの数値は、

  • 改修時に書き換えが面倒かつ危険(全部の2,3,4を見ないといけない)
  • コードだけで意味を読み取れない

という問題点がありますので、しっかりとプログラムを組むなら無くさなければいけない数値です。


マジックナンバーという用語の由来は、

この4という数字は何のことかわからないが、何故かプログラムは動く。
まるで手品だ。

という皮肉です。


こんな皮肉を言われない様、マジックナンバーの減らし方を知っておきましょう。

定数(Const)を使う

最も基本的な方法は、定数にすることです。

定数のサンプルによく上がる「消費税」では、

税込金額 = 金額 * 1.1

' ↓書き換え

Const 消費税率 = 1.1
税込金額 = 金額 * 消費税率

こんな風に定数を使います。


たったこれだけでも、1.1が何を意味する数値なのかが分かるようになりますし、

将来税率が1.05に戻るようなことは残念ながらありえないでしょうが時も、
全部の1.1を書き換える必要がなく、Constの部分だけを変えればいいので簡単です。



注意点として、定数のサンプルコードはサンプルゆえに

Const 消費税率 = 1.1

税込金額 = 金額 * 消費税率

こんな短いコードが多いですが、実際に定数を使うときに、
消費税を使うすべての箇所でConstを作ったらなんの意味もありません。
 

' モジュール最上部
Const 消費税率 = 1.1


Sub 売上を集計するマクロ
    ~~~~~~~~
    総額 = 総額 * 消費税率
    ~~~~~~~~
End Sub

Sub 領収書を出力するマクロ
    ~~~~~~~~
    ws領収書.Range("C3") = 金額 * 消費税率
    ~~~~~~~~
End Sub

このように定数を最上部(Subの外側)で宣言して、
同じ定数をたくさんのプロシージャで共有して初めて定数が活きます。


さらには

Public Const 消費税率 = 1.1

と、パブリック定数をブック内すべてで使いまわすのが重要ですので、
意識しておいてください。


※ Public = 悪 と認識している方がたまにいらっしゃいますが、
 正確には Public変数 = 悪 です。(これも極論ですが)

 Public変数はいろいろと危険なのですが、定数はむしろ逆です。
 ブック内で常に同じ値ならば、「ちゃんと」Publicにしなければいけません。

 

変数を使う(説明変数を作る)

定数の次は変数の活用です。

例えば第1行xから最終行yまでのデータがあった時、その「レコード数」は、

y - x + 1

で計算します。

3行目から10行目までのデータでは、10-3+1=8行になりますね。


この「+1」がマジックナンバーかというとそうではない気がしますが、
いずれにせよ読みにくいことに変わりはありません。


こういったちょっとした計算があるものは、

Dim レコード数 As Long
レコード数 = 最終行 -1+ 1

と、しっかり変数を作りましょう。


そうすると、例えばこのような書き換えができ↓

総額 / (最終行 -1+ 1)

' ↓書き換え

総額 / レコード数

このコードが「平均」であることが一瞬でわかるようになります。


こういった、コードを読みやすくするために作る変数を「説明変数」と呼びます。


説明変数はなくても処理は書けますが、
説明変数があると処理が書きやすく、かつ読みやすくなります。

積極的に使っていきましょう。

列番号に列挙型定数(Enum)を使う

さて冒頭のコードを再掲しましょう。

Cells(R, 4) = Cells(R, 2) * Cells(R, 3)

これでは何のことかわかりませんので、
定数を使って、

Const データの列_単価 = 2
Const データの列_個数 = 3
Const データの列_金額 = 4

Cells(R, データの列_単価) = Cells(R, データの列_個数) * Cells(R, データの列_金額)

こうすることでマジックナンバーが消せます。


もちろんこれでもOKなのですが、ExcelVBAではこの「列番号の定数化」を効率よく行うために、「列挙型定数(Enum)」が用意されています。


列挙型定数(Enum)を使うと上のコードは、

' モジュールの上部
Public Enum データの列
    単価 = 2
    個数
    金額
End Enum

' Subの中身
Cells(R, データの列.単価) = Cells(R, データの列.個数) * Cells(R, データの列.金額)

と書けるようになります。


メリットは大きく3つあり、

◇ Enumの中身はシートの見出しをコピーして作れる
Enumのコピペ


◇ 入力時は選択肢から選べる
入力選択肢


◇ 列を挿入することになったとき、Enumに見出しを挿入するだけで済む
列の挿入時

です。


すべてのシートの列番号をEnumで定義しておき、

Cells(R, 18)

みたいなコードを撲滅しておくと、改修(特に列の挿入)にかかる労力が激減します。


ExcelVBAにおいてマジックナンバーを無くす際、
これが基礎になりますので必ず覚えておきましょう。

シートオブジェクト名を使う

マジック「ナンバー」という用語は、
狭義ではその名の通り「ベタ打ちの数値」を指しますが、
広義では「ベタ打ちされた値すべて」、すなわちベタ打ちの文字列も含みます。


ExcelVBAにおいて、最もメンテナンス性に関わるベタ打ち文字列は、
間違いなく「ワークシート名」でしょう。

Worksheets("売上表").Range("A1")

というシートの指定では、シート名を変更すると全コードの変更を強いられます。


これを防ぐ一番簡単な方法は、シートオブジェクトを設定することです。


シートオブジェクト名
↑ここに名前を設定しておけば

ws売上表.Range("A1")

と、そのオブジェクト名を変数のように指定できるようになります。

これでシート名を変更してもコードを変更する必要がなくなりますね!


シートオブジェクト名は設定も簡単ですので、
積極的に活用していきましょう。



なお、シートオブジェクト名の初期値は「Sheet1」のようになっていますが、
これをそのまま使って

Sheet1.Range("A1")

とやると、どのシートのことかわからなくなって事態が悪化します。
 

Const 小数 = 1.1
税込金額 = 金額 * 小数

みたいな愚行ですので絶対にやめてください

固定セルのアドレスを文字列定数で持っておく

例えばこんな表から↓
固定セルのアドレス

 
データごとの購入日を計算するコードを↓の用に書いたとします。

With ws売上表
   購入日 = DateSerial(.Range("F2"), .Range("F3"), .Cells(R, データの列.購入日))
End With

ここで使ったセルアドレスF2,F3もマジックナンバーですね。

このままでは何のことかわかりませんし、レイアウト変更時の改修も大変です。


これを無くすには↓このように書きます。

' モジュール上部で
Public Const Adrs売上表_年 = "F2"
Public Const Adrs売上表_月 = "F3"

' 実際の使用場面
With ws売上表
   購入日 = DateSerial(.Range(Adrs売上表_年), .Range(Adrs売上表_月), .Cells(R, データの列.購入日))
End With

 
"F2"のようなセルアドレスを文字列の定数にしておいて、
Range(セルアドレス定数)とセルの指定に使う
ことができます。


これで日付を求めるコードもちゃんと読めるようになりますし、
改修が入っても書き換えはもちろん一ヵ所ですね。


ちなみにこの例のように定数名を「Adrsシート名_セル名」みたいにしておくと、
Adrs + Ctrl + Spaceで
アドレスの選択肢

この選択肢が出るのでおすすめです。

表全体などセル範囲のアドレスも同様

もし表が動的ではなく固定の表であれば、

Public Const Adrs売上表_データ部全体 = "B6:F11"

' データのクリア
ws処理シート.Range(Adrs売上表_データ部全体).ClearContents

この書き方も便利です。


ループ処理も、

Dim cell As Range
For Each cell In ws売上表.Range(Adrs売上表_データ部全体)
    ' ここに売上表内の1セルごとの処理を書く
Next

のように書けたりするので、汎用性が高い記述です。

セルアドレス定数はセル範囲にも使えますので、覚えておいてください。

必ず同じオブジェクトを返すFunctionを定義する

最後は少しレベルが上がります。

ワークシートやセルに関するマジックナンバーを減らす際、
わざわざシートオブジェクト名やアドレス定数を用いたのは、
単純に「オブジェクトは定数(Const)で定義できない」からです。


ですが、実務では「常に固定のオブジェクト」はたくさんありますし、
そのオブジェクトを定数で定義したい場面は多いです。


この解決策として、オブジェクトを完璧に定数にすることはできないのですが、

Function 売上表_年度セル As Range
    Set 売上表_年度セル = ws売上表.Range("F2")
End Function

と、必ず同じオブジェクトしか返さない関数を作ることで、
疑似的にオブジェクト定数として扱うことができます。


シートオブジェクト名を使えない「他のブックの固定シート」に対しても、

Function ws商品マスタ As Worksheet
   Set ws商品マスタ = Workbooks("各種マスタ.xlsx").Worksheets("商品マスタ")
End Function

このように疑似的に定数にすることができますし、

オブジェクトと同じくConstに出来ない配列(Array)でも、

Function Array商品リスト
    Array商品リスト = Array("みかん","りんご","いちご")
End Function

と、同様に疑似定数として扱うことができます。


実際に使用したコードのサンプルとして、
疑似定数にした他ブックのシートを、疑似定数にした配列の内容でフィルターする
コードを見てみると、

ws商品マスタ.UsedRange.AutoFilter(CNo商品マスタ.商品名, Array商品リスト)

↑こんな風に書けるようになっています。
 

  • メインコードで呼び出すときは普通のオブジェクト変数のように書ける
  • メインコードでは中身を書き換えることができない
  • 変更したい場合は宣言部分をいじる

という仕様になっていますから、
「これは定数だ」と言い張っても文句は言われないでしょう。


固定のオブジェクトを定数っぽく書くのは意外と使い道があります。

引き出しのひとつとして、持っておいてください。

まとめ

今回は「マジックナンバーを減らす方法6選」ということで、

を紹介しました。


マジックナンバーを減らそうとすると、書き始めは大変かもしれません。

しかし、定数定義さえ終わってしまえば、あとはスムーズにコードが書けます。

さらには改修が発生したときや、あとで発見されたバグを修正するときには、
定数だけいじればよいというメンテナンス性で、すさまじい効果を実感できます。


マジックナンバーを減らすことは「未来への贈り物」です。

贈る相手が自分にせよ他人にせよ、未来の誰かが苦労しないために、
マジックナンバーをがんばって減らしていきましょう。




本記事は千葉ロッテマリーンズ51年ぶりの優勝マジックナンバー点灯を記念して執筆いたしました。

和風スパゲティのレシピは、千葉ロッテマリーンズの47年ぶりリーグ勝率1位優勝を応援しています(´∀`)