PythonやJavaやC++など、オブジェクト指向プログラミング言語ではクラスの継承がサポートされており、保守性や再利用性の高いコードを書くことができます。
そうなると、VBAではクラスの継承(のようなこと)はできるの?と思う方は少なからずいると思います。結論としては、VBAにはクラスの継承に相当するものはありません。
しかし、それっぽいものを擬似的に作ったり、JavaのInterface構文に似たImplementsという機能があるので、今回はそれらを紹介していきます。
①やりたいことのイメージ
例えば、PythonではABC(Abstract Base Class)で抽象基底クラスを定義し、クラスの継承を組み合わせることで下記のようなことが可能です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
from abc import ABC, abstractmethod # 抽象基底クラス class Job(ABC): def __init__(self, name): self.name = name @abstractmethod def attack(self): pass def defense(self): return f"{self.name}は身を守っている" # Jobを継承したナイトクラス class Knight(Job): def attack(self): return f"{self.name}は剣で攻撃した" # Jobを継承した魔法使いクラス class Mage(Job): def attack(self): return f"{self.name}は杖で攻撃した" def magic(self, spell): return f"{self.name}は{spell}の魔法を使った" # Jobを継承したヒーラークラス class Healer(Job): def attack(self): return f"{self.name}は杖で攻撃した" def heal(self, target): return f"{self.name}は{target}を回復した" # メイン処理実行 if __name__ == "__main__": # 各インスタンスを生成 knight = Knight("nao") mage = Mage("Uta") healer = Healer("re") # 行動の出力 print(knight.attack()) # ナイトの攻撃 print(knight.defense()) # ナイトの防御 print(mage.magic("ファイア")) # 魔法使いの魔法攻撃 print(healer.heal(knight.name)) # ヒーラーの回復 |
このように、基底クラスに各職種で共通の行動や全職で使う内容が異なるものを定義し、継承先クラスには固有の行動を定義するということが簡単に出来てしまいます。
これにより、nameやhpなどのプロパティやdefenseやuse_itemなどの完全に同じメソッドを何度も書く必要がないため、保守性の高いコードが書けることがわかると思います。
本記事では、これに近いことが出来るかをちょっとだけ検証してみた結果を共有します。(もっとうまくやる方法はありそう)
②共通の実装
まず、キャラクターステータス用のCharacterStatusクラスを作ります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
Option Explicit Private name_ As String Private hp_ As Long Public Property Get name() As String name = name_ End Property Public Property Let name(ByVal val As String) name_ = val End Property Public Property Get hp() As Long hp = hp_ End Property Public Property Let hp(ByVal val As Long) hp_ = val End Property |
正直、このアクセサを大量に書くのは面倒ですが、ここは我慢しましょう。
次に親クラスとなるParentClassクラスを作成します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Option Explicit Private status_ As New CharaterStatus Public Property Get status() As CharaterStatus Set status = status_ End Property Public Sub Attack(ByVal weapon As String) Debug.Print status_.name & "は" & weapon & "で攻撃した" End Sub Public Sub Defense() Debug.Print status_.name & "は身を守っている" End Sub |
ポイントは、各職クラスで親クラスのインスタンスを持たせる点です。そのために、このように親クラスにステータスを持たせるときれいなりました。(他に最適な方法はあるかもしれません)
③擬似的なクラスの継承
違いはここからです。
③では普通に職種の子クラスを書いていきます。(KnightClass・MageClass・HealerClass)
・KnightClass
|
1 2 3 4 5 6 7 8 9 10 11 |
Option Explicit Private parent_ As New ParentClass Public Property Get parent() As ParentClass Set parent = parent_ End Property Public Sub Attack() parent_.Attack "剣" End Sub |
・MageClass
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Option Explicit Private parent_ As New ParentClass Public Property Get parent() As ParentClass Set parent = parent_ End Property Public Sub Attack() parent_.Attack "杖" End Sub Public Sub Magic(ByVal spell As String) Debug.Print parent_.status.name & "は" & spell & "の魔法を使った" End Sub |
・HealerClass
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Option Explicit Private parent_ As New ParentClass Public Property Get parent() As ParentClass Set parent = parent_ End Property Public Sub Attack() parent_.Attack "杖" End Sub Public Sub Heal(ByVal target As String) Debug.Print parent_.status.name & "は" & target & "を回復した" End Sub |
最後に呼び出し処理を標準モジュールに作成します。
それぞれステータスを設定した後に行動させます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
Option Explicit Sub test() Dim player1 As New KnightClass Dim player2 As New MageClass Dim player3 As New HealerClass ' ナイト With player1 Call setCharacterStatus(.parent.status, "nao", 100) .Attack .parent.Defense End With ' 魔法使い With player2 Call setCharacterStatus(.parent.status, "Uta", 50) .Attack .Magic "ファイア" End With ' ヒーラー With player3 Call setCharacterStatus(.parent.status, "re", 60) .Attack .Heal player1.parent.status.name End With End Sub Private Sub setCharacterStatus(status As CharaterStatus, _ ByVal name As String, ByVal hp As Long) With status .name = name .hp = hp Debug.Print "name:" & .name & ", hp:" & .hp End With End Sub |
実行結果は次のようになります。

Pythonと比べるとかなり規模が大きくなりますが、概ね似たようなことが出来たかと思います。
一見、もうこの方法で問題ないかのようにも見えますが、実は欠点があります。上記の子クラスであるKnightClassなどを別のプロシージャに渡したい場合、やはりKnightでもMageでも関係なく渡せるようにしたいですね?そうすると引数としてObject型を使って渡すことになりますが、これをすると受け取った側で型が不明な状態になるので、まず自動メンバー表示が使えません。オブジェクトであれば何でも渡せてしまうので、型チェックが効かず、何のエラーが出ているのかわかなくなります。
この辺が悩みどころで、自分しか実装しないから問題なかったり、柔軟に書けるから扱いやすかったりと、メリットもあるので一概にダメと言えないというところですね。
ただ、チームで開発する場合やお客様に納品する場合などは、この実装方法は避けたいところです。
④Implementsを使ってみる
Implementsというのは共通のインターフェースをクラスに持たせる機能です。言葉だけではわかりにくいので、さっそくコードを見てみましょう。下記がIJobインターフェースクラスです。
|
1 2 3 4 5 6 7 |
Option Explicit Public Property Get parent() As ParentClass End Property Public Sub Attack() End Sub |
このように、外部から呼び出すため、publicで宣言だけ書いてあげます。
続いて、各職種の実装クラスを書いていきます。
ここでは、各クラスモジュールにImplements インタフェースクラス(IJob)を記述し、IJobで宣言したプロパティやメソッドは全て実装する必要があります。
その際、メソッド名などの書き方はインターフェースクラス名_メソッド名のような書き方をする必要があります。(書かないとエラーになります)
・JobKnight
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Option Explicit Implements IJob Private parent_ As New ParentClass Private Property Get IJob_parent() As ParentClass Set IJob_parent = parent_ End Property Private Sub IJob_Attack() parent_.Attack "剣" End Sub |
・JobMage 独自メソッドMagicをPublicで追加しておきます
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Option Explicit Implements IJob Private parent_ As New ParentClass Private Property Get IJob_parent() As ParentClass Set IJob_parent = parent_ End Property Private Sub IJob_Attack() parent_.Attack "杖" End Sub Public Sub Magic(ByVal spell As String) Debug.Print parent_.status.name & "は" & spell & "を使った" End Sub |
・JobHealer 独自メソッドHealをPublicで追加しておきます
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Option Explicit Implements IJob Private parent_ As New ParentClass Private Property Get IJob_parent() As ParentClass Set IJob_parent = parent_ End Property Private Sub IJob_Attack() parent_.Attack "杖" End Sub Public Sub Heal(ByVal target As String) Debug.Print parent_.status.name & "は" & target & "を回復した" End Sub |
最後に呼び出し処理を標準モジュールに作成します。
基本的には③と同様ですね。
異なる点は各playerの宣言はインターフェースクラスで行い、インスタンス生成時に実装クラスにしている点と独自メソッドを呼び出す際は、実装クラスで宣言された変数にキャストするという2点です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
Option Explicit Sub test() Dim player1 As IJob Dim player2 As IJob Dim player3 As IJob ' ナイト Set player1 = createInstance(New JobKnight, "nao", 100) With player1 .Attack .parent.Defense End With ' 魔法使い Set player2 = createInstance(New JobMage, "Uta", 50) player2.Attack Dim mage As JobMage: Set mage = player2 mage.Magic "ファイア" ' ヒーラー Set player3 = createInstance(New JobHealer, "re", 60) player3.Attack Dim healer As JobHealer: Set healer = player3 healer.Heal player1.parent.status.name End Sub Private Function createInstance(job As IJob, _ ByVal name As String, ByVal hp As Long) With job.parent.status .name = name .hp = hp Debug.Print "name:" & .name & ", hp:" & .hp End With Set createInstance = job End Function |
実行結果は③と同じですね。
記述内容が増えただけで、一見すると良さがあまりわかりません。しかし、上記のplayer1,2,3はIJobで宣言されているので、③で書いたように他のプロシージャに渡す際には、IJobとしてどれでも渡すことができます。
実はcreateInstanceプロシージャがそのような実装となっています。New JobKnight, JobMage, JobHealer全て渡せて、かつ型安全で、もちろん自動メンバー表示も機能します。
独自メソッドを呼び出す場合にキャストは必要なものの、こちらも安全な実装なので、厳格なコードを書きたい場合は、やはりこの方法がおすすめです。
少し癖があるので、あまり使われていない機能のように感じていますが、十分に活用する価値はあると思っています。
⑤おわりに
いかがでしたか?
いわゆるオブジェクト指向プログラミング言語と呼ばれているJavaやPythonなどと比べると、やや使いにくいのは否めませんが、VBAでもきれいなコードを書きたい方や、クラスの概念について理解を深めたい方にはぜひおすすめしたい内容ということで、紹介させていただきました。
以上、長くなりましたが、最後まで読んでいただき、ありがとうございました。

コメント