t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

VBA インターフェースを活用してコードの抽象度を上げるテクニック

以下のような注文票にマクロでデータを記入することを考えてみる。
f:id:t-hom:20160714200309p:plain

人間が記入する場合は、次のように考えながら記入していくだろう。
「注文票の、ひとつ目の商品名は"パソコン"で、単価は10万円、個数は40個。二つ目の商品は"複合機"で、単価は150万円、個数は2個。」

これをVBA風に書くと、こうなる。

Sub 注文記入()
    With 注文票.注文(1)
        .品名 = "パソコン"
        .単価 = 100000
        .個数 = 40
    End With

    With 注文票.注文(2)
        .品名 = "複合機"
        .単価 = 1500000
        .個数 = 2
    End With
End Sub

やりたいことがそのまま表記され、極めてシンプルなコードである。

が、もちろん普通はこんな風には書けない。

それを何とかしてしまおうというのが今回のテーマ。

普通にマクロを組む場合

まず普通にマクロを組む場合について考えてみよう。

VBAのコードに落とし込むには、

「まず記入するのはこのブックのひとつ目のシートだから、ThisWorkbook.Sheets(1)、ひとつ目の商品があるのはシートの5行目、品名はC列だから、Range("C5")に"パソコン"と記入し、単価はD列、個数はE列だからRange("D5")に100000、Range("E5")に40と記入する。」

という風により具体的に考える必要が出てくる。

具体的なコードは次のようになる。

Sub 注文記入()
    Dim RecordRow As Long
    RecordRow = 5
    With ThisWorkbook.Sheets(1)
        .Range("C" & RecordRow).Value = "パソコン"
        .Range("D" & RecordRow).Value = 100000
        .Range("E" & RecordRow).Value = 40
    End With
    
    RecordRow = 6
    With ThisWorkbook.Sheets(1)
        .Range("C" & RecordRow).Value = "複合機"
        .Range("D" & RecordRow).Value = 1500000
        .Range("E" & RecordRow).Value = 2
    End With
End Sub

VBAに慣れた人ならすこぶる簡単なコードに見えるだろう。
しかし実際には脳内では、「人間がやりたいこと」と、「VBAでの具体的なコード」の対応表が作られており、いくらVBAが得意な人でもコードが複雑になってくると脳内の対応表がオーバーフローを起こす。
あるいは久々に読んでみたら、そのコードが何をしたかったものなのかさっぱり思い出せないということがよくある。

コードの抽象度

人間にとって次の指示は非常に具体的である。
「注文票の、ひとつ目の商品名は"パソコン"で、単価は10万円、個数は40個。二つ目の商品は"複合機"で、単価は150万円、個数は2個。」

しかしコンピューターにとっては上記の指示は非常に抽象的である。
もしそんな指示を与えたら、「注文票って何?どのブックの何番目のシートのこと?ひとつ目ってシートの1行目でいいの?、商品名の列は?ってかそもそも列なの?もっと具体的に言って!!!」という具合に混乱(エラー)を起こす。

普通、抽象的な話というのは分かりにくいものなのだが、VBAのコードは人間にとっては具体的過ぎて難しいので、「抽象的なコードのほうがわかりやすい」という反転現象が起こる。

【参考】
thom.hateblo.jp

抽象的な書き方

冒頭で紹介したVBA風のコードであるが、実はちゃんとしたVBAコードである。
これ単体ではもちろん動かないが、他にいろいろとコードを書いて準備すれば、以下は有効なコードになる。

Sub 注文記入()
    With 注文票.注文(1)
        .品名 = "パソコン"
        .単価 = 100000
        .個数 = 40
    End With

    With 注文票.注文(2)
        .品名 = "複合機"
        .単価 = 1500000
        .個数 = 2
    End With
End Sub

以下、その準備について記す。

まずは注文票シートのオブジェクト名を「注文票」に変更する。
f:id:t-hom:20160714203955p:plain

【参考】
thom.hateblo.jp

次に、クラスモジュールを挿入し、オブジェクト名を「I注文レコード」、Instancingを「2 - PublicNotCreatable」にする。
f:id:t-hom:20160714204149p:plain

「I注文レコード」のIはインターフェースの意味である。
インターフェースは、メソッドやプロパティの存在を担保するためのモジュールで、プロシージャの形式だけを記入して中身は記入しない。

I注文レコードには、次のコードを記入しておく。
この時点ではインターフェースを使用する意味が分からないかもしれないが、最後に説明するので今は気にしないでほしい。

Property Let 品名(x As String)
End Property

Property Let 個数(x As Long)
End Property

Property Let 単価(x As Long)
End Property

Sub Clear()
End Sub

次にもう一つクラスモジュールを作成し、こちらはオブジェクト名を「注文レコード」、Instancingはデフォルトの「1 - Private」のままにしておく。

注文レコードのコードは次のとおり。

Implements I注文レコード
Public ws As Worksheet
Public RecordRow As Long

Property Let Number(N As Long)
    RecordRow = N + 4
End Property

Property Let I注文レコード_品名(x As String)
    ws.Range("C" & RecordRow).Value = x
End Property

Property Let I注文レコード_個数(x As Long)
    ws.Range("E" & RecordRow).Value = x
End Property

Property Let I注文レコード_単価(x As Long)
    ws.Range("D" & RecordRow).Value = x
End Property

Sub I注文レコード_Clear()
    Me.I注文レコード_個数 = 0
    Me.I注文レコード_単価 = 0
    Me.I注文レコード_品名 = ""
End Sub

【参考】
thom.hateblo.jp

そして最後に「注文票」シートのコードに次のコードを記入する。

Property Get 注文(N) As I注文レコード
    Dim O As New 注文レコード
    Set O.ws = Me
    O.Number = N
    Set 注文 = O
End Property

ここで注目してほしいのは、注文票シートのプロパティ「注文」が、「I注文レコード」インターフェースを返す点である。
これは、InstancingがPrivateなオブジェクトをパブリックプロパティの引数や戻り値にすることはできず、エラーとなるためである。

具体的なオブジェクトは「注文レコード」型として作成している。
InstancingがPublicNotCreatableなクラスはNewでインスタンスを作ることができない。

そのためこのように自作クラスを引数や戻り値に使おうと思ったらインターフェースを使わざるを得ないのである。

さて、ここまで準備ができたら、あとは標準モジュールに冒頭のコードを書くだけだ。

Sub 注文記入()
    With 注文票.注文(1)
        .品名 = "パソコン"
        .単価 = 100000
        .個数 = 40
    End With

    With 注文票.注文(2)
        .品名 = "複合機"
        .単価 = 1500000
        .個数 = 2
    End With
End Sub

このコードは次のように動作する。

まず「注文票」の「注文」プロパティ(以下のコード)に1が渡される。

Property Get 注文(N) As I注文レコード
    Dim O As New 注文レコード
    Set O.ws = Me
    O.Number = N
    Set 注文 = O
End Property

すると、「注文レコード」オブジェクトが生成され、Numberプロパティが1にセットされる。
注文レコードクラス内では、Numberプロパティは次のように定義されている。

Property Let Number(N As Long)
    RecordRow = N + 4
End Property

つまり1が渡されると、4が足され、5になる。

「ひとつ目の商品があるのはシートの5行目」というのを思い出してほしい。
ここでは、「何個目」という人間の管理する情報を「何行目」というExcelの管理する情報に変更している。

次に「.品名 = "パソコン"」である。
これはWith文の中にあるので、実際は「注文票.注文(1).品名 = "パソコン"」と同じ。

「I注文レコード」インターフェースの品名プロパティが参照されると、具体的にはそれが指す「注文レコード」オブジェクトの次のコードが呼ばれる。

Property Let I注文レコード_品名(x As String)
    ws.Range("C" & RecordRow).Value = x
End Property

ここでwsは「注文票」シートを指す。
※「注文票」の「注文」プロパティで「Set O.ws = Me」として自分自身を渡している。

RecordRowは先ほど5がセットされているので、注文票のRange("C5")に引数xに入った"パソコン"が入力される。

単価や個数についても類似の動作なので割愛する。

以上が抽象的な書き方を実現するための、具体的なコードの準備である。

余計複雑になった気がする?

そのとおり。メインのコードをスッキリ書くために、周辺のコードは相当に複雑になっている。
しかし重要なのは、メインコードのロジックを書く際、その複雑さは脇に置いておけるということだ。

一度仕組みを作ってしまえば、シート番号や記入する行、品名を記入するべき列など、具体的な情報は一切考慮する必要はない。

もしこれから書くコードが50ステップにも満たない簡単なものであれば、こうしたテクニックは余計な複雑さを持ち込むだけであまり役に立たないだろう。

しかし、相当に複雑なロジックの巨大なマクロを書こうと思ったとき、複雑さを分離してメインロジックに集中できる仕組みは有難い。

あとがき

VBAのクラスモジュールは、以下のようなジレンマを抱えているように思う。
「メリットが分からないから使わない→使わないからメリットが分からない」
「使われないから書籍などで紹介されない→書籍などで紹介されないから使われない」

私がオブジェクト指向を理解したのは、実はVBScriptが最初だった。
「なんでまた。。」と思われるかもしれないけど、その当時私はVBSでAccess連携する複雑なコードを書いていた。
複雑で具体的な処理をぐちゃぐちゃ書いていて、ある日それは破綻した。
「どうしてエラーになるのかさっぱり分からない。細部を追っても、全体を見渡しても分からない。これ以上一歩も進めない。」
そう感じて一旦コードを整理しようと思って書き直した際に採用したのが、「クラス」だった。

だから皆さんも、自分のコードが複雑すぎて理解が追い付かなくなってきたら、クラスモジュールのことを思い出してほしい。それはきっと力になってくれるはずだ。

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