どうでもいいプログラム研究所

とある編集者によるIT、Web、ソフトウェア、プログラミングに関する雑記と覚え書き

VBAにおけるクラスモジュールの使い方と必要性をすぐろくプログラムで考えてみる

f:id:tdyu5021:20191019024036p:plain

エクセルVBAの中でも、初学者には非常にわかりづらいイメージのある「クラスモジュール」。今回はすごろくプログラムを例にとり、クラスモジュールと必要性と使い方について限りなく丁寧に解説してみます。

ワークシートで作るすごろく(みたいな)プログラム

まずはじめに以下のGIFをご覧ください。

f:id:tdyu5021:20191020025247g:plain

わけのわからないマクロですが、「ランダムに数字を生成」して、「コマを進めている」ので、ここではこれを一種の「すごろくプログラム」であると仮定してください…笑

「サイコロを人が振らずに勝手にプレーしているじゃん」とか「動物が競争しているだけじゃん」など細かいツッコミはおいておき、なぜこれをお見せしたのかと言うと、このプログラムはクラスモジュールの必要性を説明するのにとても都合が良いからです。

説明のために非常に簡略化していますが、このプログラムでは、以下のような内容を実装しています。

  1. シェイプオブジェクトですごろくのコマを何体か設置する
    (今回は、犬、猫、うさぎの3体)
  2. 数字をランダムに生成し、コマの名前をメッセージで発してマスを進める
  3. これを1体ずつ行う
  4. 上記の(2)~(3)を繰り返す

 ではこれをどのようにプログラムで表現したら良いでしょうか。

どういうコードで実現するか?

今回、クラスモジュールの必要性を際立たせるために、まずはあえてクラスモジュールを使わないやり方をいくつか並べてみます。プログラムをもっと簡略化するため、ゴールまでループさせずに上記の手順の1から4までの手順のうち(2)と(3)を実現する方法を記します。「そんなのはどうでもいいから、クラスモジュールを使った方法が見たい」という方は、「クラスモジュールを用いた実装方法」の見出しから読んでください。

 サンプルソースコード1(超初心者向け)

前提条件として、

・犬の図形オブジェクト名を"dog"に
・猫の図形オブジェクト名を"cat"に
・うさぎの図形オブジェクト名を"rabbit"に
・列の幅を90ピクセルに設定

上記の設定を済ませた上で、以下のコードを実行すると、3体の図形は1回ずつ移動します。

Sub sugoroku1()
    Dim dog As Shape, cat As Shape, rabbit As Shape
    Dim dogName As String, catName As String, rabbitName As String
    Dim n As Integer
    
    Set dog = ActiveSheet.Shapes("dog")
    Set cat = ActiveSheet.Shapes("cat")
    Set rabbit = ActiveSheet.Shapes("rabbit")
    dogName = "ワンコ"
    catName = "ニャンコ"
    rabbitName = "ウサコ"
       
    n = Int(Rnd * 3) + 1
    MsgBox dogName & "は" & n & "進みます"
    dog.IncrementLeft (45 * n)
    Application.Wait [Now() + "0:00:00.2"]
    
    n = Int(Rnd * 3) + 1
    MsgBox catName & "は" & n & "進みます"
    cat.IncrementLeft (45 * n)
    Application.Wait [Now() + "0:00:00.2"]
    
    n = Int(Rnd * 3) + 1
    MsgBox rabbitName & "は" & n & "進みます"
    rabbit.IncrementLeft (45 * n)
    Application.Wait [Now() + "0:00:00.2"]
End Sub

上記は各画像をshapeオブジェクトで変数に格納し、IncrementLeftという図形の左位置を変更するメソッドを用い、その画像を移動させるというプログラムです。Application.waitで0.2秒待っているのは、そうしないと、メッセージボックス→移動→メッセージボックス→移動とステップで実行されないからです。

もちろん、これはかなり汚いコードです。「ランダムに数字を生成して移動する」という処理は犬も猫もうさぎも共通ですので、それらをひとまとめにするのが合理的です。そのように書き換えたのが以下です。

サンプルソースコード2(改良版)

Sub sugoroku2()
    Dim piece(2) As Shape
    Dim pieceName(2) As String
    Dim n As Integer, i As Integer
    
    Set piece(0) = ActiveSheet.Shapes("dog")
    Set piece(1) = ActiveSheet.Shapes("cat")
    Set piece(2) = ActiveSheet.Shapes("rabbit")
    
    pieceName(0) = "ワンコ"
    pieceName(1) = "ニャンコ"
    pieceName(2) = "ウサコ"
    
    For i = 0 To 2
        n = Int(Rnd * 3) + 1
        MsgBox pieceName(i) & "は" & n & "進みます"
        piece(i).IncrementLeft (45 * n)
        Application.Wait [Now() + "0:00:00.2"]
    Next i
End Sub

この改良版のポイントとしては、配列変数を用いることで、各変数に対して行う処理をForループで集約しています。今回はIncrementLeftしかないので、このプロシージャ内に移動の処理を書いていますが、もっと行数の長い複雑な処理になればその部分を別のプロシージャや関数にして部品化してもよいでしょう。

本当にこれがベストなのか? 

しかし、このプログラムはこういう実装が正解なのでしょうか。確かにコードの重複がなくなったので記述こそはシンプルになりましたが、管理性があまり良くありません。その理由は次の通りです。 

●理由1:コマが増えたときが面倒

現在、コマは犬と猫とうさぎなので配列変数の要素数は”2”を入れています。しかし、コマ数を増やしたりすると、その都度、配列変数の要素数も書き換えなければなりません。もちろん、配列変数の要素数の数字も変数化してしまえば一括管理できるので実用に問題はありませんが、少し気持ち悪さが残ります。

●理由2:各コマの処理が増えたときの対処が面倒

今のプログラムでは、各コマの名前(ここではpieceName()という配列変数)だったり各コマを移動させるという処理しかつけていませんが、今後すごろくゲームを拡張していくとき、もっと機能をつけていくことがあるでしょう。

例えば、1回休みの機能をつけることもあるでしょう。その際には、各コマが1回休みかどうかを判定する配列変数が必要になります。このように、コマの状態や属性を判定する機能をつければ、どんどん管理すべき配列変数が増えていく可能性があります。

オブジェクト、プロパティ、メソッドで管理できればもっと便利になる

上記の疑問に対して「別にそれで問題ない」と思われる方もいるかもしれません。しかしこうしたプログラムの場合、すごろくのコマというオブジェクトを中心に、プロパティ、メソッドという概念で管理できるとより便利になります。それを実現するのが「クラスモジュール」なのです

この意図を説明するために、念のため超初心者の方に向けてここでVBA(に限らず多くのプログラミング言語)の基本に立ち返ってみたいと思います。

そもそもオブジェクト、プロパティ、メソッドとは何だ?

VBAを含む多くのプログラミング言語では、オブジェクトがあり、それに対するメソッドやプロパティという概念を持っています。

VBAの場合、セルというオブジェクト(操作の対象)があり、Copyというメソッドがあったり、ValueやInteriorというプロパティが存在します。例えると、人間というオブジェクトに対して、歩くというのがメソッド、年齢、性別というのがプロパティといえるかもしれませんね。

これをすごろくに当てはめるとどうなるか

この考えをすごろくプログラムに取り入れると非常にわかりやすくなります。

すごろくプログラムには、例えばコマに対して「名前(※すごろくゲームに必須ではありませんが・・)」「現在の位置」「1回休みであるかどうか」という属性があれば、「進む」「戻る」「スタートに戻る」などの処理が存在するはずです。言い換えればコマにはプロパティメソッドがあるともいえるでしょう。

つまり次の発想も可能になります。

皆様がCells(1,1).Copy~~とか、Cells(1,1).Value =~~でセルオブジェクトを操作するのとまったく同じように、すごろくの犬のコマをオブジェクトとみなし、dog.move~~とかdog.name=~~と記述するのです。

これにはいろんなメリットがあります。

1つにdog.moveで「ああ犬が動くのだな」という直感的な理解を促せること。もう1つは、プログラムを機能拡張したときにそれをプロパティやメソッドというパーツとして、メインのモジュールとは別に管理できるようになることです。

つまりメインモジュールには影響を与えないため、拡張性やメンテナンス性が高いプログラムを作ることができるのです。

独自のオブジェクトに対してメソッドやプロパティを作れる

いうまでもなく、dogというオブジェクトもなければmoveやnameといったプロパティは存在しません。しかしクラスモジュールを利用すればそれが可能になるのです。つまりクラスは独自のオブジェクトやメソッド、プロパティを定義できるのです。

独自のメソッドやプロパティは、もちろんdogというオブジェクトだけのものではありません。catでもrabbitでも、その他の動物でも構いません。cat.moveとか、rabbit.nameとかでもよいのです。

要するにクラスモジュールでは、後々オブジェクトをdogでもcatでもrabbitでも入れられるように、「入れ物」を定義して「ひな形」を作っておきます(ここでは仮に入れ物の名前を”animal”としておきます。同時にanimal.move、animal.nameというプロパティやメソッドなども定義します)

その後、必要に応じてdogやcatやrabbitといったオブジェクトの「実体」をanimalという入れ物に当てはめていくのです。このとき、作成したひな形を「クラス」とよび、dogやcatやrabbitなどでクラスに入った実体をインスタンスと呼んでいます。

クラスモジュールを用いた実装方法

それではここから先ほど作ったプログラムを、クラスモジュールを使って実現する方法を見ていきましょう。

まず先ほどのanimalという、オブジェクトを入れるためひな形の作り方です。「挿入」「クラスモジュール」を選択し、新たにクラスモジュールができるので、そこを任意の名前(今回はanimalというクラス名にします) 

f:id:tdyu5021:20191020015440p:plain

独自のメソッドを追加する

手始めに独自のメソッドを追加したいと思います。標準モジュールにプロシージャを作るのと同様に、まず独自に作るメソッド名にしたいものをプロシージャ名にして以下のプログラムを書きます(※後述しますが、一部ダミーの文字列あり)。

以下を見ておわかりのとおり、Excelのクラスモジュールにプロシージャを書くと、それがそのクラスのメソッドになるのです。(メッセージボックスの部分は記載していません。ここは後ほど解説します)

'******************
'*クラスモジュール*
'******************
Public Sub move()
    Dim n As Integer
    '//進む数をランダムに生成
    n = Int(Rnd * 3) + 1
    '//図形オブジェクトを進める
    xxxxxxxx.IncrementLeft (45 * n)
    Application.Wait [Now() + "0:00:00.2"]
End Sub

しかし当然ながらこのコードは動きません。incrementLeftするオブジェクトをまだxxxxxxxxのダミー文字列にしており、そこには何も入っていないからです。この部分をどうすればよいのかに触れる前に、ここで作成したanimalクラスに「実体」を当てはめる作業から見ていきます。

animalクラスに犬や猫などの実体をいれる

ここまでanimalクラスを作成し、さらにコマを動かすmoveというメソッド(暫定)まで作成できました。早速それらをワークシート上にある犬や猫やうさぎの図形とひも付けたいと思います。ここで標準モジュールに戻ります。

まずdog.moveなどができるようにするには、”dog”がanimalクラスのオブジェクトだということをプログラムに教えなければなりません。プログラミング的に言うと「クラスを宣言する」という作業です。標準モジュールに以下のように記述します

Dim dog As New animal

クラスの宣言は

dim [変数名] As New [クラス名] 

というかたちになります。

では、本当にdogはanimalというクラスの実体(インスタンス)となることができたのでしょうか。試しに、標準モジュールにdog.まで入力してみると…

f:id:tdyu5021:20191020020610p:plain

メソッドの自動メンバー表示にmoveが出てきた!

つまりdogというオブジェクトが作成され、さらにそれはmoveというメソッドを持っているということを意味しています。

ただし、ここでdog.moveを実行したところで、ワークシート上の犬の図形は動いたりしません。エラーを起こします。当然です。「moveするオブジェクト=犬の画像」だということがまだどこにも書かかれていないからです。

そのため、先ほどのクラスモジュールに書いたxxxxxxxxx.moveのxxxxxxxxの部分(オブジェクト)が犬の画像であるということを定義しましょう。

まずxxxxxxxxを適当な変数名にしておきます。今回moveさせるのは犬なのですが、このmoveはcatとかrabbitとかいろんな動物の実体に対するメソッドなのでオブジェクトは抽象的な命名にするべきです。図形をmoveさせるということで、ここではshpという変数名にしておきましょう。

そしてこの変数shpの中にワークシート上の図形を格納しなければなりません。しかしクラスはひな形であって今後dogでもcatでもrabbitでも何でも当てはめますから

set shp = ActiveSheet.Shapes("dog")

のようにdogという単語を直接記入するわけにもいきません。

そこでどうすればよいかというと、変数shpにオブジェクトを当てはめるという処理を、メソッドとしてanimalクラスに書いておき、それをshpに入れたい好きな図形の引数つきで標準モジュールから呼び出せばよいのです。まずクラスモジュールには以下のように書きます。

'******************
'*クラスモジュール*
'******************
Public Sub setting(animalShape)
    Set shp = animalShape
End Sub

オブジェクトをセットするのでモジュール名(メソッド名)はsettingとかにしておきました、引数をもらってくるので、括弧の中に、適当な引数名を入れてください。動物の図形をもらってくるのでanimalShapeとつけています。

またsettingプロシージャと先ほどのmoveプロシージャ間でshpを使えるようにモジュール間の変数として宣言するのを忘れないようにしましょう。

さてここまでで、クラスモジュールには以下のようなコードができ上がりました。

'******************
'*クラスモジュール*
'******************
Dim shp As Shape
Public Sub setting(animalShape)
    Set shp = animalShape
End Sub
Public Sub move()
    Dim n As Integer
    '//進む数をランダムに生成
    n = Int(Rnd * 3) + 1
    '//図形オブジェクトを進める
    shp.IncrementLeft (45 * n)
    Application.Wait [Now() + "0:00:00.2"]
End Sub

標準モジュールからメソッドを操作する

そして標準モジュールの方に戻りましょう。settingという独自のメソッドを定義したので、今度はdog.まで入力すると、今度は自動メンバー表示にメソッドとしてmoveとsettingの2つができていることがわかります。ここでsettingを選び、その後ろにsettingメソッドに渡したい引数を記入します。

引数の書き方ですが、メソッドから半角スペースを開けて記入します。今回はまず犬の図形を動かしたいので、その情報を引数として渡すためにmoveの後ろにActiveSheet.Shapes("dog")を記入しています。

f:id:tdyu5021:20191020022240p:plain

独自のプロパティをセットする 

クラスモジュールは独自のメソッドと同様に独自のプロパティを持つことができますので、これも解説しましょう。

このすごろくプログラムではコマそれぞれに「名前」をつけていました。そこで、nameというプロパティを定義します(※もちろんプロパティ名は何でも構いません)。それ自体は非常に簡単です。ただnameという変数をPublic変数で宣言すればOKです。

f:id:tdyu5021:20191021021040p:plain

試しに、標準モジュールに戻り先ほどと同じように、dog.まで入力してみてください。自動メンバー表示にnameが出てくるはずです。つまりdogオブジェクトはnameというプロパティを持っていることを意味します。

しかし、プロパティを作っただけでは、当然、まだ中身が何も入っていません。先ほど、shpに犬の図形を入れたように、こちらも犬の名前である「ワンコ」という文字列をセットしましょう。

先ほどはsettingというメソッドを設けて、クラスモジュールで必要な初期値をセットしていきました。別のメソッドを作ってもよいのですが、今回はこのsettingメソッドにてnameという変数に名前を格納する処理を足しておきましょう。

'******************
'*クラスモジュール*
'******************
Public Sub setting(animalShape, animalName)
    Set shp = animalShape
    name = animalName
End Sub

これだけではもちろんダメですね。このメソッドを実行する標準モジュールの方も、動物の名前を渡す引数をもう1つ追加する必要があります( 以下の「ワンコ」の部分です)

dog.setting ActiveSheet.Shapes("dog"), "ワンコ"

これで準備完了です。これでコマの名前をnameというプロパティで表示できるようになったので、まだ書かれていなかったメッセージボックスの記述を以下のようにクラスモジュールに追加しました。

MsgBox name & "は" & n & "進みます"

これで標準モジュールとクラスモジュールはそれぞれ以下のコードができたはずです。 

'******************
'*標準モジュール*
'******************
Option Explicit
Sub sugoroku3()
    '//dogという実体(インスタンス)を生成
    Dim dog As New animal
    '//犬の図形をクラスモジュール内へ渡す
    dog.setting ActiveSheet.Shapes("dog"), "ワンコ"
    '//犬を動かす
    dog.move
End Sub
'******************
'*クラスモジュール*
'******************
Public name As String
Dim shp As Shape
Public Sub setting(animalShape, animalName)
    Set shp = animalShape
    name = animalName
End Sub
Public Sub move()
    Dim n As Integer
    '//進む数をランダムに生成
    n = Int(Rnd * 3) + 1
    MsgBox name & "は" & n & "進みます"
    '//図形オブジェクトを進める
    shp.IncrementLeft (45 * n)
    Application.Wait [Now() + "0:00:00.2"]
End Sub

ここで標準モジュールのsugoroku3というプロシージャを実行してみると、「ワンコは○マス進みます」というメッセージボックスが表示されたあと犬の図形が動いたはずです。

dogという独自のオブジェクトを作成し、moveやnameという独自のメソッドとプロパティが働いていることがわかったでしょう。これこそがクラスの役割です。

猫やうさぎも足してみる

もちろん、まだこれだけでは完成ではありませんよね。冒頭で示したプログラムには猫の図形もうさぎの図形もありましたので、それを追加してみましょう。クラスのメリットでありますが、「動物のコマを処理する」というひな形はもうクラスモジュールに部品化されているので、そちらを触る必要はありません。

・dogという変数をクラスとして宣言し
・そのdogに初期値を与え
・moveというメソッドを書く

ということを、猫とうさぎにも適用すればよいだけです。以下のようになります。

'******************
'*標準モジュール*
'******************
Option Explicit
Sub sugoroku3()
    '//dogという実体(インスタンス)を生成
    Dim dog As New animal
    '//catという実体(インスタンス)を生成
    Dim cat As New animal
    '//rabbitという実体(インスタンス)を生成
    Dim rabbit As New animal
    
    '//犬の図形をクラスモジュール内へ渡す
    dog.setting ActiveSheet.Shapes("dog"), "ワンコ"
    '//猫の図形をクラスモジュール内へ渡す
    cat.setting ActiveSheet.Shapes("cat"), "ニャンコ"
    '//うさぎの図形をクラスモジュール内へ渡す
    rabbit.setting ActiveSheet.Shapes("rabbit"), "ウサコ"
    
    '//犬を動かす
    dog.move
    '//猫を動かす
    cat.move
    '//うさぎを動かす
    rabbit.move
End Sub

本当にこれがメリットになるの? 

あまりにもシンプルすぎるため、これだけ読んでもまだクラスのメリットをお感じになっていない方も多いかもしれません。

確かに、上記のプログラムがまさにそうですが、クラスモジュールを利用すると通常のやり方で書く方法に比べてむしろコードの量が増えることもあります。ただコード量が増える増えないは本質ではありません。

先ほど、「管理」や「拡張性」というキーワードを挙げたかと思いますが、今後上記のプログラムに、例えば「スタートに戻る」というメソッドを追加するときは、同じようにクラスモジュールにプログラムを書き、その機能をいざ呼び出すときは、標準モジュールで例えば、dog.backToStartとかのように書けば済むのです。その他のプロパティについても同様です。

いかがでしょうか。そう考えると少しメリットがあるような気がしないでしょうか。

ちなみに上記のクラスモジュールを使用したサンプルは、実際にはまだ美しいコードではありません。dogという名前でクラスを生成しましたが、そもそもdog、cat、rabbitはすべて同じ処理をするため、それらをループさせなければなりません。

ですので、冒頭の方に記した2個めのサンプルソースコードのように、実際には配列変数を使ってクラスを宣言していくことになるでしょう。

ただ、そうするとサンプルコードが見づらくになるため、今回はそこまでは書きませんでした。

最後に少し言い訳

解説としては以上になります。

私もオブジェクト指向プログラミング言語を触ったことがなく、VBAについてもクラスモジュールを使い慣れているわけでもありません。むしろ今でもよくわかっていないところがあります。

そのため、今回の説明がクラスの普遍的な説明になっていることわけではないことをご承知おきください。詳しい方が読んだら「そこは違う」という部分もあるでしょう。

ただ、私自身クラスモジュールの解説を他の記事で読んで思ったのが、厳密な説明をすればするほど説明や例えが難しいなという印象でした。そこで実用性はともかく、クラスを使うとこんなことができるというのを感覚的に理解できるように今回の記事を書いてみました。

私が苦戦したように、これからクラスモジュールを使う人に少しでも参考になればありがたいなと思います。