再利用可能なマクロ構造の発見
~設計と処理をつなぐ5つの手法~
Copyright © 2025 LWP 山中 一弘 本資料は、出典を明記いただければ、商用・非商用を問わず、ご自由に複製・改変・再配布していただけます。なお、著作権表示は改変せず、そのまま記載してご利用くださいますようお願いいたします。
要約
マクロはその即効性ゆえに場当たり的なコードが量産されやすく、構造の不統一や可読性の欠如によって保守困難に陥ることが多い。本記事では、そうしたマクロ設計の混乱を避け、処理の分離と再利用を可能にする「構造的な書き方」に焦点を当てる。特に1:N構造の扱い方、関数同士をデータで接続する方法、グローバル変数の適切な抑制、入出力の分離によるパイプライン設計、定義情報の辞書化による構造注入といった5つの視点を取り上げ、実務で応用可能な構造パターンとして提示する。これは「うまく書くため」ではなく、「壊れないように構造化するため」の設計知見である。
第1章 1:Nの処理を辞書とループで支える
1.1 配列より辞書──繰り返し処理の構造化
1:Nの処理構造を実装する際、配列は順序や件数管理に優れるが、データの意味付けや分類構造の明示には不向きである。配列を使った場合、添字番号と業務上の意味との対応が暗黙化し、コードを読む者はその関係を都度解釈しなければならない。これが保守性を低下させ、特に処理対象が複数のカテゴリに分かれる場合には深刻化する。
辞書(Scripting.Dictionary)を用いると、キーに分類条件や識別子を設定でき、値としてそのカテゴリに属するデータの集合を持たせられる。この構造は、配列のような位置依存を避けつつ、処理の意図を直接的にコードに反映できる。
1.2 キーでまとめることの意味と恩恵
辞書構造の利点は、処理対象の分類基準をコード中で直接可視化できる点にある。キーを業務区分やカテゴリに設定することで、処理の流れをデータ構造として外部化できる。これは仕様変更時に定義部分だけを修正すれば済む設計を可能にし、処理本体の安定性を高める。また、キー単位でループ処理が行われるため、1:Nの関係がコード上で直感的に理解できる。
このような構造化は、単なる可読性向上だけでなく、テスト容易性や機能追加時の影響範囲の局所化といった実務的な恩恵ももたらす。
1.3 個別処理関数との接続による分離実現
1:N処理のもう一つの重要なポイントは、「繰り返し構造」と「個別処理」の分離である。辞書を用いた場合、キーごとに同じ処理パターンを適用することが多いが、その処理内容をループの内部に直接書くと、構造の再利用性が下がる。
理想的には、ループ側は「どのキーに対してどの関数を呼ぶか」だけを担当し、実際の処理内容は別の関数に委譲する。この関数は、1件単位のデータ、またはそのカテゴリ全体のデータ集合を引数として受け取り、処理結果を返す純粋関数として設計する。
この分離により、辞書ループの骨格を変更せずに処理内容だけを差し替えられ、構造全体を他の業務やデータ型に転用できる。さらに、処理関数単位でのテストや検証も容易になるため、品質保証の観点でも有効である。
Public Sub processSalesByFruit()Rem @desc 「果物」ごとの売上データを辞書で分類し、個別処理関数に渡して集計を行
Dim xws As Worksheet: Set xws = Worksheets("売上") Dim xtbl As ListObject: Set xtbl = xws.ListObjects("売上") Dim xdic As Object: Set xdic = CreateObject("Scripting.Dictionary") Dim xrow As ListRow Dim xkey, xsum' 1. データを辞書に分類(キー=果物、値=合計金額の配列)
For Each xrow In xtbl.ListRowsxkey = xrow.Range(1, 1).Value ' 列1=果物
If Not xdic.Exists(xkey) Thenxdic(xkey) = Array()
End Ifxsum = xrow.Range(1, 5).Value ' 列5=合計
xdic(xkey) = arrayAppend(xdic(xkey), xsum)
Next' 2. キーごとに個別処理関数を呼び出す
For Each xkey In xdic.KeyscallFruitSummary xkey, xdic(xkey)
NextEnd SubPrivate Sub callFruitSummary(a_key, a_values)Rem @desc 果物カテゴリごとの合計金額を計算し、結果を出力する。
Rem @param a_key 果物名(カテゴリ)
Rem @param a_values 合計金額の配列
Dim total: total = sumArray(a_values) Debug.Print "果物:" & a_key & "/合計:" & totalEnd SubPrivate Function arrayAppend(a_arr, a_val) Dim tmp: tmp = a_arrReDim Preserve tmp(LBound(tmp) To UBound(tmp) + 1)
tmp(UBound(tmp)) = a_val
arrayAppend = tmp
End FunctionPrivate Function sumArray(a_arr) Dim v, total: total = 0 For Each v In a_arrtotal = total + v
NextsumArray = total
End Function第2章 関数を「横並び」にしてつなぐ手法
2.1 関数を直列接続するという発想
マクロ処理の構造化では、1つの関数が全工程を抱え込むのではなく、処理を複数の小さな関数に分割し、それらを直列に接続することで全体を構成する手法が有効である。
直列接続とは、ある関数の出力をそのまま次の関数の入力として渡す設計であり、関数間の受け渡しをデータで統一する。これにより、関数は順番に並んだ部品として再利用可能になり、処理フローが明確化される。
また、直列構造は「途中で関数を差し替える」「一部の関数をスキップする」といった改変を容易にし、保守性を飛躍的に向上させる。
2.2 状態を持たせない純粋処理関数
関数を直列接続する際の重要な前提は、各関数が外部状態に依存せず、入力だけから出力を決定する純粋関数として設計されていることである。
純粋関数は同じ入力に対して常に同じ出力を返すため、テスト容易性が高く、また処理の順序変更や並列化の検討が容易になる。逆に、関数が内部でグローバル変数や外部リソースに依存している場合、関数間の接続は脆弱化し、順序や依存関係の影響が予測困難になる。
実務では、関数の引数と戻り値を厳密に定義し、外部参照を排除することで純粋関数を実現する。
2.3 接続単位としての「中間データ構造」
直列接続の中核となるのが、中間データ構造である。これは、ある関数の出力であり、次の関数の入力となる共通のデータ形式を指す。
中間データ構造を明示的に設計することで、関数同士が統一されたインターフェースで接続され、各関数の役割分担が明確になる。さらに、この構造は処理途中の結果を一時保存する用途にも使えるため、デバッグや部分的な再実行にも有効である。
Excelマクロでは、配列、Collection、Dictionary、カスタム型などが中間データ構造として活用できる。特にDictionaryはキー付きのデータ構造として柔軟性が高く、フィルタ処理や集計処理など複数工程にわたる処理フローに適している。
Public Sub processSalesPipeline()Rem @desc 売上データを取得・加工・集計の3関数で直列接続し、純粋関数と中間データ構造を活用する例。
Dim data As Object Dim processed As Object Dim summary As Object' 関数を直列接続
Set data = loadSalesData() ' Step1: データ取得 Set processed = transformSalesData(data) ' Step2: 加工 Set summary = summarizeSalesData(processed) ' Step3: 集計' 出力
outputSalesSummary summary
End SubPrivate Function loadSalesData()Rem @desc 売上テーブルからDictionary形式でデータを取得する。
Rem @param なし
Rem @return Dictionary(キー=果物、値=合計の配列)
Dim xws As Worksheet: Set xws = Worksheets("売上") Dim xtbl As ListObject: Set xtbl = xws.ListObjects("売上") Dim xdic As Object: Set xdic = CreateObject("Scripting.Dictionary") Dim xrow As ListRow, key, val For Each xrow In xtbl.ListRows key = xrow.Range(1, 1).Value If Not xdic.Exists(key) Thenxdic(key) = Array()
End If val = xrow.Range(1, 5).Valuexdic(key) = arrayAppend(xdic(key), val)
Next Set loadSalesData = xdicEnd FunctionPrivate Function transformSalesData(a_dic)Rem @name transformSalesData
Rem @desc 各果物の売上データを10%増加させる加工処理を行う。
Rem @param a_dic Dictionary(キー=果物、値=売上配列)
Rem @return Dictionary(加工後の同形式データ)
Dim key, arr, i For Each key In a_dic.Keys arr = a_dic(key) For i = LBound(arr) To UBound(arr)arr(i) = arr(i) * 1.1
Nexta_dic(key) = arr
Next Set transformSalesData = a_dicEnd FunctionPrivate Function summarizeSalesData(a_dic)Rem @name summarizeSalesData
Rem @desc 各果物の売上合計をDictionary形式で返す。
Rem @param a_dic Dictionary(キー=果物、値=売上配列)
Rem @return Dictionary(キー=果物、値=売上合計)
Dim key, result As Object Set result = CreateObject("Scripting.Dictionary") For Each key In a_dic.Keysresult(key) = sumArray(a_dic(key))
Next Set summarizeSalesData = resultEnd FunctionPrivate Sub outputSalesSummary(a_dic)Rem @name outputSalesSummary
Rem @desc 集計結果をImmediate Windowに出力する。
Rem @param a_dic Dictionary(キー=果物、値=売上合計)
Dim key For Each key In a_dic.Keys Debug.Print "果物:" & key & "/合計:" & a_dic(key) NextEnd SubPrivate Function arrayAppend(a_arr, a_val) Dim tmp: tmp = a_arrReDim Preserve tmp(LBound(tmp) To UBound(tmp) + 1)
tmp(UBound(tmp)) = a_val
arrayAppend = tmp
End FunctionPrivate Function sumArray(a_arr) Dim v, total: total = 0 For Each v In a_arrtotal = total + v
NextsumArray = total
End Function第3章 グローバル変数を使わないスコープ制御
3.1 便利さと引き換えに失う透明性
グローバル変数は、どこからでも参照・変更できるため短期的には便利である。しかし、この利便性は同時にコードの透明性を失わせる。ある処理がどこで値を書き換えているのか、またその変更がどこに影響しているのかを追跡するのは困難になる。特に、長期運用される業務マクロでは、後から参加した開発者が影響範囲を誤認し、予期せぬ不具合を発生させるリスクが高い。グローバル変数は「誰でも触れる共有領域」であり、責任の所在が不明瞭になる点が構造的な欠点である。
3.2 スコープ設計と変数構造の分離
スコープ制御の第一歩は、変数の寿命と利用範囲を明確にすることである。モジュール単位・プロシージャ単位・ブロック単位といったスコープ階層を意識し、必要最小限の範囲で変数を宣言する。さらに、変数自体の構造(Dictionaryやカスタム型など)とそのスコープを意図的に分離することで、データの受け渡し方法を統一できる。これにより、スコープ外からの直接操作を防ぎ、意図しない変更や依存を抑制できる。
3.3 「引数による構造の受け渡し」への切り替え
グローバル変数の代替として有効なのが、引数による構造体やオブジェクトの受け渡しである。関数やプロシージャは、必要なデータをすべて引数で受け取り、処理結果を戻り値として返す。この方式では、関数が利用するデータの出入りが明示され、外部からの不意な状態変更を防げる。また、構造体やDictionaryなどにまとめて渡せば、引数リストを簡潔に保ちながら柔軟なデータ受け渡しが可能となる。
この切り替えにより、マクロ全体の依存関係が減少し、モジュール間の結合度が下がるため、機能追加や修正時の影響範囲を局所化できる。
Public Sub processSalesNoGlobals()Rem @name processSalesNoGlobals
Rem @desc グローバル変数を使わず、引数による構造受け渡しで売上集計を行う例。
Rem @param なし
Rem @return なし
Dim salesData As Object Set salesData = loadSalesDataNoGlobals() Dim summary As Object Set summary = summarizeSalesNoGlobals(salesData)outputSalesSummaryNoGlobals summary
End SubPrivate Function loadSalesDataNoGlobals()Rem @name loadSalesDataNoGlobals
Rem @desc 売上テーブルからDictionary形式でデータを取得する(グローバル変数不使用)。
Rem @param なし
Rem @return Dictionary(キー=果物、値=売上配列)
Dim xws As Worksheet: Set xws = Worksheets("売上") Dim xtbl As ListObject: Set xtbl = xws.ListObjects("売上") Dim xdic As Object: Set xdic = CreateObject("Scripting.Dictionary") Dim xrow As ListRow, key, val For Each xrow In xtbl.ListRows key = xrow.Range(1, 1).Value If Not xdic.Exists(key) Thenxdic(key) = Array()
End If val = xrow.Range(1, 5).Valuexdic(key) = arrayAppend(xdic(key), val)
Next Set loadSalesDataNoGlobals = xdicEnd FunctionPrivate Function summarizeSalesNoGlobals(a_dic)Rem @name summarizeSalesNoGlobals
Rem @desc 各果物の売上合計を計算する(グローバル変数不使用)。
Rem @param a_dic Dictionary(キー=果物、値=売上配列)
Rem @return Dictionary(キー=果物、値=売上合計)
Dim key, result As Object Set result = CreateObject("Scripting.Dictionary") For Each key In a_dic.Keysresult(key) = sumArray(a_dic(key))
Next Set summarizeSalesNoGlobals = resultEnd FunctionPrivate Sub outputSalesSummaryNoGlobals(a_dic)Rem @name outputSalesSummaryNoGlobals
Rem @desc 集計結果をImmediate Windowに出力する(グローバル変数不使用)。
Rem @param a_dic Dictionary(キー=果物、値=売上合計)
Rem @return なし
Dim key For Each key In a_dic.Keys Debug.Print "果物:" & key & "/合計:" & a_dic(key) NextEnd SubPrivate Function arrayAppend(a_arr, a_val) Dim tmp: tmp = a_arrReDim Preserve tmp(LBound(tmp) To UBound(tmp) + 1)
tmp(UBound(tmp)) = a_val
arrayAppend = tmp
End FunctionPrivate Function sumArray(a_arr) Dim v, total: total = 0 For Each v In a_arrtotal = total + v
NextsumArray = total
End Function第4章 入出力を分離するパイプライン設計
4.1 マクロにも処理段階の役割を持たせる
多くの業務マクロは「入力の取得」「加工処理」「出力」という三つの段階に分けられる。しかし現場では、これらが1つのプロシージャに混在し、構造が不明瞭になっているケースが多い。処理段階を意識的に分離すれば、役割の重複や依存が減り、修正時の影響範囲も局所化できる。
この設計では、マクロ全体を「入力フェーズ(データ取得)」「処理フェーズ(加工・集計)」「出力フェーズ(表示・保存)」の三層に分け、それぞれを専用の関数やモジュールとして設計する。これにより、工程単位での差し替えや並列開発が容易になる。
4.2 「加工する関数」と「出力する関数」の分離
加工処理と出力処理を混在させると、同じデータを別の形で出力する際に加工部分まで重複実装してしまうリスクがある。加工する関数は出力形式を一切意識せず、あくまでデータ構造を整えることだけを責務とする。一方、出力する関数は加工済みデータを受け取り、目的に応じた表示や保存を行う。この役割分離により、同一の加工関数を複数の出力先(シート・CSV・メッセージボックスなど)で再利用できる。
4.3 パイプライン化がもたらす保守性と再利用性
入力・加工・出力を明確に分離すると、それらを順番に接続するだけで全体の処理フローを構築できる。この「パイプライン化」は、工程単位の追加・削除・差し替えが容易で、同じ入力データに対して異なる加工や出力を試す実験的開発にも向く。さらに、工程間のデータ受け渡しが明示されるため、テストやデバッグも局所的に行える。
VBAでも、この考え方を適用すれば、マクロが持つ「一度作ったら使い回しにくい」という弱点を克服し、長期的な保守と拡張に耐える構造を実現できる。
Public Sub runSalesPipeline()Rem @name runSalesPipeline
Rem @desc 入力・加工・出力を分離したパイプライン構造で売上集計を実行する。
Rem @param なし
Dim rawData As Object Dim processedData As Object Dim summaryData As Object' 入力フェーズ
Set rawData = loadSalesDataPipeline()' 加工フェーズ
Set processedData = adjustSalesData(rawData)' 集計フェーズ
Set summaryData = summarizeSalesData(processedData)' 出力フェーズ
outputSalesToImmediate summaryData
' ほかの出力先例:
' outputSalesToSheet summaryData
' outputSalesToCSV summaryData, "C:\temp\sales.csv"End SubPrivate Function loadSalesDataPipeline()Rem @name loadSalesDataPipeline
Rem @desc 売上テーブルからDictionary形式でデータを取得する。
Rem @param なし
Rem @return Dictionary(キー=果物、値=売上配列)
Dim xws As Worksheet: Set xws = Worksheets("売上") Dim xtbl As ListObject: Set xtbl = xws.ListObjects("売上") Dim xdic As Object: Set xdic = CreateObject("Scripting.Dictionary") Dim xrow As ListRow, key, val For Each xrow In xtbl.ListRows key = xrow.Range(1, 1).Value If Not xdic.Exists(key) Thenxdic(key) = Array()
End If val = xrow.Range(1, 5).Valuexdic(key) = arrayAppend(xdic(key), val)
Next Set loadSalesDataPipeline = xdicEnd FunctionPrivate Function adjustSalesData(a_dic)Rem @name adjustSalesData
Rem @desc 売上データに10%の調整を加える。
Rem @param a_dic Dictionary(キー=果物、値=売上配列)
Rem @return Dictionary(調整後データ)
Dim key, arr, i For Each key In a_dic.Keys arr = a_dic(key) For i = LBound(arr) To UBound(arr)arr(i) = arr(i) * 1.1
Nexta_dic(key) = arr
Next Set adjustSalesData = a_dicEnd FunctionPrivate Function summarizeSalesData(a_dic)Rem @name summarizeSalesData
Rem @desc 各果物の売上合計を計算する。
Rem @param a_dic Dictionary(キー=果物、値=売上配列)
Rem @return Dictionary(キー=果物、値=売上合計)
Dim key, result As Object Set result = CreateObject("Scripting.Dictionary") For Each key In a_dic.Keysresult(key) = sumArray(a_dic(key))
Next Set summarizeSalesData = resultEnd FunctionPrivate Sub outputSalesToImmediate(a_dic)Rem @name outputSalesToImmediate
Rem @desc 集計結果をImmediate Windowに出力する。
Rem @param a_dic Dictionary(キー=果物、値=売上合計)
Rem @return なし
Dim key For Each key In a_dic.Keys Debug.Print "果物:" & key & "/合計:" & a_dic(key) NextEnd Sub' 出力先を追加する場合の例
Private Sub outputSalesToSheet(a_dic) Dim ws As Worksheet: Set ws = Worksheets.Add Dim rowIndex As Long: rowIndex = 1 Dim key For Each key In a_dic.Keysws.Cells(rowIndex, 1).Value = key
ws.Cells(rowIndex, 2).Value = a_dic(key)
rowIndex = rowIndex + 1
NextEnd SubPrivate Function arrayAppend(a_arr, a_val) Dim tmp: tmp = a_arrReDim Preserve tmp(LBound(tmp) To UBound(tmp) + 1)
tmp(UBound(tmp)) = a_val
arrayAppend = tmp
End FunctionPrivate Function sumArray(a_arr) Dim v, total: total = 0 For Each v In a_arrtotal = total + v
NextsumArray = total
End Function第5章 マスタ定義の辞書化と構造注入
5.1 定数ではなく構造で渡す設定情報
多くのマクロは、処理条件や閾値、フォーマット指定などをコード中の定数として直接記述している。しかし、これでは条件変更のたびにコードを編集しなければならず、他プロジェクトへの流用も難しい。
この問題を回避するには、設定情報を構造化データとして外部化し、関数に引数として渡す設計が有効である。構造化データは、Dictionaryやカスタム型などを用いて階層的に整理でき、複数の設定を一括管理できる。
5.2 定義辞書と処理関数の役割分離
設定情報を辞書として定義する場合、定義そのものを行うモジュール(または関数)と、それを使って処理を行う関数を明確に分離する。
定義モジュールは、業務仕様やマクロ利用環境に依存した値だけを保持し、処理関数はそれらの設定を受け取って動作する汎用的なロジックに徹する。この役割分離により、異なる業務や環境への展開時には定義部分のみを差し替えればよく、処理本体の改修を最小限にできる。
5.3 「構造注入」による柔軟なマクロ共通化
「構造注入(Dependency Injection of Structures)」とは、関数が利用する設定やマスタ情報を、外部で構築した構造体や辞書として渡す設計を指す。これにより、処理関数はどのようなデータが渡されるかを意識せず、仕様変更時にも設定側で構造を変えるだけで対応できる。
この方法は、特に複数の業務マクロで同じ処理ロジックを共有する場合に効果的であり、テスト時には設定辞書をモックに差し替えることで動作確認も容易になる。
Public Sub runSalesWithMaster()Rem @name runSalesWithMaster
Rem @desc マスタ定義を辞書で構築し、構造注入によって売上集計を実行する。
Rem @param なし
Rem @return なし
Dim masterDef As Object Set masterDef = buildSalesMasterDefinition() Dim rawData As Object Set rawData = loadSalesDataWithMaster(masterDef) Dim summary As Object Set summary = summarizeSalesWithMaster(rawData, masterDef)outputSalesSummaryWithMaster summary, masterDef
End SubPrivate Function buildSalesMasterDefinition()Rem @name buildSalesMasterDefinition
Rem @desc 売上処理に必要なマスタ定義辞書を構築する。
Rem @param なし
Rem @return Dictionary(処理設定)
Rem @procattr std_function
Dim dic As Object: Set dic = CreateObject("Scripting.Dictionary")' 列位置定義
Dim colMap As Object: Set colMap = CreateObject("Scripting.Dictionary")colMap("果物") = 1
colMap("日付") = 2
colMap("価格") = 3
colMap("数量") = 4
colMap("合計") = 5
dic("列マップ") = colMap
' 出力フォーマット定義
Dim fmt As Object: Set fmt = CreateObject("Scripting.Dictionary")fmt("通貨記号") = "¥"
fmt("小数点桁数") = 0
dic("フォーマット") = fmt
Set buildSalesMasterDefinition = dicEnd FunctionPrivate Function loadSalesDataWithMaster(a_master)Rem @name loadSalesDataWithMaster
Rem @desc マスタ定義を参照して売上テーブルからデータを取得する。
Rem @param a_master マスタ定義辞書
Rem @return Dictionary(キー=果物、値=売上配列)
Dim xws As Worksheet: Set xws = Worksheets("売上") Dim xtbl As ListObject: Set xtbl = xws.ListObjects("売上") Dim xdic As Object: Set xdic = CreateObject("Scripting.Dictionary") Dim xrow As ListRow, key, val For Each xrow In xtbl.ListRowskey = xrow.Range(1, a_master("列マップ")("果物")).Value
If Not xdic.Exists(key) Thenxdic(key) = Array()
End Ifval = xrow.Range(1, a_master("列マップ")("合計")).Value
xdic(key) = arrayAppend(xdic(key), val)
Next Set loadSalesDataWithMaster = xdicEnd FunctionPrivate Function summarizeSalesWithMaster(a_dic, a_master)Rem @name summarizeSalesWithMaster
Rem @desc マスタ定義を参照して売上合計を計算する。
Rem @param a_dic Dictionary(キー=果物、値=売上配列)
Rem @param a_master マスタ定義辞書
Rem @return Dictionary(キー=果物、値=売上合計)
Dim key, result As Object Set result = CreateObject("Scripting.Dictionary") For Each key In a_dic.Keysresult(key) = sumArray(a_dic(key))
Next Set summarizeSalesWithMaster = resultEnd FunctionPrivate Sub outputSalesSummaryWithMaster(a_dic, a_master)Rem @name outputSalesSummaryWithMaster
Rem @desc マスタ定義のフォーマットに従い、集計結果をImmediate Windowに出力する。
Rem @param a_dic Dictionary(キー=果物、値=売上合計)
Rem @param a_master マスタ定義辞書
Dim key For Each key In a_dic.Keys Debug.Print "果物:" & key & "/合計:" & _a_master("フォーマット")("通貨記号") & _
Format(a_dic(key), "0." & String(a_master("フォーマット")("小数点桁数"), "0"))
NextEnd SubPrivate Function arrayAppend(a_arr, a_val) Dim tmp: tmp = a_arrReDim Preserve tmp(LBound(tmp) To UBound(tmp) + 1)
tmp(UBound(tmp)) = a_val
arrayAppend = tmp
End FunctionPrivate Function sumArray(a_arr) Dim v, total: total = 0 For Each v In a_arrtotal = total + v
NextsumArray = total
End Function