ゆるおたノート

Tomorrow is another day.

【VBA】クラスの作り方を整理してみた(移行手順編)

標準モジュールに書いていた処理を、何とかクラスモジュールに移行出来るようになってきました。

まだ世界がごちゃごちゃですが、記憶の新しいうちにメモを残してみます。

基本用語・概念については、こちらからどうぞ。 【VBA】クラスの作り方を整理してみた(基本用語編) - ゆるおたノート

手順

手続きの流れを考える

処理のテーマ

今回は、例として「フォルダ内の複数のブックで、シートの値を置き換えて名前を変えて保存する」場合を考える。

[例]手続きの流れ

だいたいこんな処理を書くと思う。(日本語でごめんなさい)


  • フォルダ内に処理対象のブックがあるか確認する
  • 拡張子付きファイル名を取得
  • パスを取得
  • パスを指定してブックを開く
  • シートを取得
  • テーブルを取得
  • セルの値を取得
  • 文字列に変換
  • 文字列を置き換える
  • ブックの拡張子付きファイル名を取得
  • 現在のファイル名から新しいファイル名を取得
  • 格納先のフォルダを取得
  • ファイル名とフォルダの場所からフルパスを取得
  • 新しいパスでブックを保存する
  • ここまでの処理を同じフォルダのブックの数だけ繰り返す
  • メッセージボックスで「完了」をお知らせ。

処理の分解

処理を区切ってプロシージャを分ける

「〜して、〜して、〜して、…」のように、「『〜して』毎」*1に1つのプロシージャにしてみる。
これを、一般に部品化と呼ぶ。

※詳しくはこちらが参考になります*2 VBA 中級者を悩ませるプロシージャ分割をマスターする極意 - t-hom’s diary

ただし、あまり細かくし過ぎると行数が増えてかえって管理が大変になるばかりなので、内容によっては2~3個の処理を1つのプロシージャにまとめることもある。

目安としては、1プロシージャあたりPCのディスプレイ1枚分(20~30行)くらい。
…区切るサイズや場所には個人差もあると思うので、絶対ではない。

[例]処理をざっくり分割

上記のの場合、ざっくり下記のように分けられると思う。


  • フォルダを確認
  • フォルダ内に処理対象のブックがあるか確認する
  • パスを取得
    • 1つ目のファイル名を取得
    • ファイル名とフォルダの場所からフルパスを取得
    • パスを取得
  • 値を取得
    • パスを指定してブックを開く
    • シートを取得
    • テーブルを取得
    • データ範囲を取得
    • 二次元配列として値を取得
  • 値を置き換える
    • セルの値を取得
    • 文字列に変換
    • 文字列を置き換える
    • セルの値を置き換える
    • シートの数だけ繰り返す
  • ブックを保存する
    • ブックの拡張子付きファイル名を取得
    • 現在のファイル名から新しいファイル名を取得
    • 格納先のフォルダを取得
    • ファイル名とフォルダの場所からフルパスを取得
    • 新しいパスでブックを保存する
  • ブックの数だけ繰り返す
  • メッセージボックスで「完了」をお知らせ。

出来るだけ共通の処理を抽象化する

「他のブックでもこのマクロを使うとしたら?」と考えながら、プロシージャや変数、引数の名前を考える。

[例]処理の抽象化

たとえば…

変更 関数名
⚫️⚫️社Bookの全シートの文字列を▼▼に置き換える
全文字列を置き換える(ワークブック置き換え前置き換え後

処理を分類してメソッド化

似たようなプロシージャをまとめる

今回の例であれば、「ファイル名とフォルダの場所からフルパスを取得」という処理が複数回登場していて、やることは共通している。

その前後にある「フォルダのパスやファイル名を『なんやかんや』する」処理も、「パスの操作」という意味で似ている。
このように、似たものを操作するプロシージャを分類して、いくつかのカテゴリに分けてみる。

さらに、標準モジュール(もしくはシートモジュール)にメイン処理用のプロシージャを作って、こちらから部品化・カテゴリ分けしたプロシージャを呼び出すことにする。

[例]プロシージャの分類

今回の例であれば、ざっくりこんな感じに分けられそう。

  • メイン・プロシージャ
  • 処理対象のブックの判定
  • パスの操作
  • ブックを開く、保存する
  • シートの値の取得
  • 文字列の操作

※「パスの操作」は、プロシージャのサイズによっては「文字列の操作」にまとめても良いかもしれない。

メイン処理以外をオブジェクトモジュール化する

メインのプロシージャ以外を、プロシージャのカテゴリごとに別々のクラスモジュール(やシートモジュール)として独立させる。

プロシージャのスコープを分ける

モジュール内専用の処理外部から呼び出せる処理(=メソッド)をそれぞれ区別し、スコープを設定しておく。
こうすることで、パブリックのものだけ入力補完が効くようになり、この後のコーディングが楽になる。

スコープのキーワード
スコープ キーワード 呼び方
全モジュール共通 Public パブリック・モジュール・レベル
モジュール内専用 Private プライベート・モジュール・レベル
クラス化のメリット

クラスの中で処理や値のスコープをPrivate以下にすると、外部のモジュールからは内部処理やデータの状態が見えなくなる。
このことをオブジェクト指向的には隠蔽(いんぺい)と呼び、特に「クラス」といった枠の中に閉じ込めることカプセル化という。

こうすることで、外部から値の変更などの干渉を受けづらくなり、コードが少し安全になる(はず)。

※「隠蔽」や「カプセル化」の目的について、詳しくはこちらもご参照いただけたら幸いです。 オブジェクト指向って、なんだ? - ゆるおたノート

プロパティの設定

共通の引数を探す

同じものを参照・利用しているのであれば、まとめて「プロパティ」にする。
値や参照はプロパティに代入して、メソッド内ではプロパティから呼び出すようにすることで引数を減らせる。

また、クラスモジュール内でも、メソッドの中からMe.プロパティ名で呼び出せるようになる(後述)。
入力補完も効くので、コーディングの効率や可読性も上がる*3

プロパティの値をモジュールレベル変数とする

プロパティの中身を参照する時はProperty Getプロシージャを呼び出す。
しかし、Property Getプロシージャの戻り値には値を直接代入できないので、下記のような書き方はできない
<2019/10/14追記>
「戻り値に代入」はできます。作例も差し替えております。失礼いたしました…

Property Getプロシージャには値を直接代入できないので、下記のような書き方はできない。

'[クラスモジュール] Exampleクラス
Property Get プロパティ名()As 型
    プロパティ名 = '←直接の代入は出来ない
End Property
'[標準モジュール]
Sub test()
    Dim ex As New Example
    ex.プロパティ名 = 新しい値
End Sub

実行すると、下記のようなコンパイルエラーが発生する。

値の取得のみ可能なプロパティに値を設定することはできません。

このため、Property LetプロシージャProperty Setプロシージャを使って「値を設定」できるようにする(詳細は後述)。

このときに、プロパティではなく「変数」を経由して「値」を代入することで、間接的にプロパティの中身を変更できるようになる。
そんなわけで、今の段階でProperty Getプロシージャも変数の値を返すようにしておく。

[例]変数を経由するよう変更
'[クラスモジュール] Exampleクラス
Private 変数 AsProperty Get プロパティ名()As 型
    プロパティ名 = 変数
End Property
変数のスコープを分ける

この時に、プロシージャと同じように「プロジェクト全体で使用するもの」と「モジュール内で完結するもの」、「プロシージャ内で完結するもの」を整理して、スコープを調整しておく。

プロパティ用の変数でスコープを「プライベート・モジュール・レベル」にしておくことで、「このクラス専用」の変数とすることができる。

つまり、「このクラス専用」の変数は、プライベートなプロシージャと同じように他のモジュールからは呼び出せなくなり、「隠蔽」されていることになる。
(以下、当記事ではこの変数のことを便宜上「隠し変数」と呼ぶことにする。)

スコープのキーワード
スコープ キーワード 呼び方
全モジュール共通 Public パブリック・モジュール・レベル
モジュール内専用 Private/Dim プライベート・モジュール・レベル
プロシージャ内専用 Dim プロシージャレベル
コンストラクで「隠し変数」に代入する

Class_Initializeという特別なプロシージャを作ると、インスタンスの生成時に必ず呼び出され、自動でプロパティに値を設定するように出来る*4

ただし、上述のようにプロパティではなく隠し変数に値を代入するように書いておく。

[例]コンストラクで隠し変数に代入
Private 隠し変数1 AsPrivate 隠し変数2 AsPrivate 隠し変数3 As オブジェクト系の型
Private 隠し変数4 As オブジェクト系の型
 
Public Sub Class_Initialize()
    隠し変数1 =Call 必ず実行する処理 '追加処理を入れても良い
    隠し変数2 =Set 隠し変数3 = オブジェクト
    Set 隠し変数4 = 値の準備とか(引数) '関数も使える
End Sub
コンストラクを使いやすくする

Class_Initializeプロシージャは「引数を設定できない仕様」になっているので、インスタンスの初期値は固定になってしまう。
いつも同じ値にしたいとは限らないので、これでは少し使いづらいことがある。

そこで一工夫。

Initializeメソッド(名前は自分で分かりやすいように変えてもOK)を別途作って、インスタンス生成時に引数として値を渡しつつ呼び出すことにする。
※この場合は、Class_Initializeプロシージャは作成しなくても良い。

これで、インスタンスの生成時にまとめて任意の値を設定できる。
(別の言語では、このような処理をするメソッドを「コンストラク」と言う。)

[例]引数が使えるコンストラク
'[クラスモジュール] Exampleクラス
Private 隠し変数1 AsPrivate 隠し変数2 AsPrivate 隠し変数3 As オブジェクト系の型
Private 隠し変数4 As オブジェクト系の型
Private 隠し変数5 As オブジェクト系の型
 
Public Sub Initialize(ByVal 仮引数1 As, _
                      ByRef 仮引数2 As オブジェクト系の型)
    隠し変数1 = 仮引数1
    
    Call 必ず実行する処理
    隠し変数2 = '引数ではなく値を指定しても大丈夫
    
    Set 隠し変数3 = 仮引数2
    Set 隠し変数4 = 値の準備とか(引数)
    Set 隠し変数5 = オブジェクト 'オブジェクトも大丈夫
    
End Sub
'[標準モジュール]
Public Sub Main()
    
    Dim test As New Example
    'Initializeメソッドを呼び出してプロパティの初期化
    test.Initialize 仮引数1:=引数1, _
                    仮引数2:=引数2    
End Sub
終了処理を追加する

モジュールレベル以上の変数は、マクロの実行が完了しても値が破棄されないので、マクロを複数回実行する時などは注意が必要になってしまう。
Class_Terminateという特別なプロシージャを作ると、インスタンスが不要になった時に自動で終了処理(=Terminate)されるようにできる。

<2019/10/14追記>
当初、上記例の中で変数の初期化・破棄処理を書いておりましたが、ことりちゅん (id:Kotori-ChunChun)さんより下記の通りアドバイスを頂きました。

クラスはインスタンスを破棄した時点でメンバの変数は一緒に消失しますから、サンプルのTerminateのように変数の初期化は意味が無いです。

ことりちゅん (id:Kotori-ChunChun)さんのツイートより

これに従い、作例は下記に修正いたしました。ご指摘ありがとうございます!

[例]終了処理
Public Sub Class_Terminate()
    Call 必ず実行する処理 'ゴミデータのお掃除など
End Sub
Property Getプロシージャでプロパティに隠し変数を代入する

ここまで準備ができたら、プロパティの値を設定していく。

上述のように隠し変数を経由してプロパティに値を設定することで、外部のプロシージャからはインスタンス名.プロパティ名でプロパティの中身を見られるようになる。

[例]値を代入
Private 隠し変数 AsProperty Get プロパティ名()As '~処理~
    隠し変数 = 値
    プロパティ名 = 隠し変数
    
End Property
[例]オブジェクトの参照値を代入
Private 隠し変数 As オブジェクト系の型
 
Property Get プロパティ名()As オブジェクト系の型
    
    '~処理~
    Set 隠し変数 = オブジェクト
    Set プロパティ名 = 隠し変数
    
End Property
ちなみに…

外部ではなく同クラス内で呼び出す時は、Me.プロパティ名と書くことができる。

[例]同クラス内で呼び出し
'[クラスモジュール] Exampleクラス
'▼プロパティ
Private 変数 AsProperty Get プロパティA()As 型
    プロパティA = 変数
End Property
 
'▼メソッド
Public Function test()
    Debug.Print "このインスタンスのプロパティAは、現在" & Me.プロパティA & "の状態です。"
End Function
Property Let/Setプロシージャで、プロパティに代入出来るようにする

プロパティは、中身を見るだけでなく処理の途中で値を変えたくなるときもある。
そういう時は、Property Let/Setプロシージャで値を設定できるようにしておく。

これで、外部のモジュールからプロパティに値を代入できるようになるので、実用に耐える(はず)。

[例]プロパティに代入するプロシージャを追加
Private 隠し変数 AsProperty Let プロパティ名(仮引数 As)
    '処理
    隠し変数 = 仮引数
End Property

繰り返しになるが、プロパティの設定は隠し変数を経由する。

そうしないと、プロパティに代入後も「隠し変数」の値は変わらず、改めてプロパティを呼び出して中身を見たら「元の値に戻ってる?」なんて混乱することに…

あとがき

VBAの神々によれば、ここまでがクラス入門編だそうです。
私はここに至るまで半年以上かかってしまいました…

「分かったような分かんないような…」で何度も書いては消してを繰り返してるので、キレイな文章はもう放棄です。
話の整理は未来の私に託します。笑

慣れてきたら、プロパティを先に作り込んでからメソッドを作り始める方が、入力補完の助けも得られてもっと書きやすいかもですね。
「どの基準・どの単位でクラスにまとめるか?」もまだ感覚が掴めてないです。
数をこなして慣れるしかないんですかね…

このシリーズについて

初心者の頭でVBAでクラスを作成する方法の整理に挑戦しています。

連載目次

  1. オブジェクト指向って、なんだ? - ゆるおたノート
  2. 当記事【VBA】クラスの作り方を整理してみた(移行手順編) - ゆるおたノート
  3. 【VBA】クラスの作り方を整理してみた(基本用語編) - ゆるおたノート

注釈

*1:日本語の文法的に言うと「文節単位」?

*2:いつもお世話になっております!!
この記事に限らず、VBA使いの人は読んで損はないブログです。

*3:入力は補完に任せられるから、多少長い名前をつけても大丈夫。

*4:Initializeは「初期化」のこと