Excelアドイン開発にクリーンアーキテクチャーを導入してみた

クリーンアーキテクチャー採用の動機

ExcelのVBAを使ってシステム開発する場合、ExcelのVBIDEを使うことになると思います。そのエディタの使い勝手は Visual Studio Code など今日的なその他のものと比べてあまり便利とは言えません。しかし、VBA特有の書式化・コンパイルチェック・デバッグを考えるとVBIDEを使わないわけにもいきません。そのため、 Visual Studio Code 等外部エディターである程度編集しておき、VBIDEにコピー、また逆に外部エディターに戻す操作を何度も繰り返していました。

これを解決するため、外部エディターと VBIDE のソースを同期する Excel アドイン(ExcelVBACodeSyncと命名)を C#, VSTO を使って作りました。すなわち、マクロが含まれるブックが保存されたら、指定されたフォルダー配下にソースをエクスポート、フォルダーの変更を監視して外部エディターでファイルが変更されたらインポートするようにしました。

ExcelVBACodeSync はフォルダー監視、Excelの状態変化の両方がコマンドのトリガーとなります。この点をどのように実装するかが問題となります。例えばCRUDが主な動作となるアプリケーションではプレゼンテーション層をUI、データアクセス層をデータベースと位置付けるレイヤードアーキテクチャーを使っています。今回は、フォルダーおよび Excel がストレージであると同時にコマンドトリガーにもなります。

これらを無理なく実装するのにクリーンアーキテクチャーが使えそうだと思い採用してみました。

レイヤードアーキテクチャー各層との比較

レイヤードアーキテクチャー クリーンアーキテクチャー
プレゼンテーション層 アダプター層
Frameworks & Driver 層
アプリケーション層 ユースケース層
ドメイン層 エンティティ層
データアクセス層 アダプター層
Frameworks & Driver 層

普段採用しているレイヤードアーキテクチャーとクリーンアーキテクチャーの各層対応は右表の様になっています。

ユースケース層エンティティ層はそれぞれアプリケーションロジック、ビジネスロジックを記述する部分でありアプリケーションコアと位置づけられます。クリーンアーキテクチャーではプレゼンテーション層もデータアクセス層も技術の詳細に依存する部分としてアプリケーションコアの外部にあるアダプター層Frameworks & Driver 層に配置されます。

出典: The Clean Architecture - Clean Coder Blog

プレゼンテーション層とデータアクセス層がまったく同一視されるかというとそうではなく、その扱いには違いがあり、Primary AdapterSecondary Adapterに分かれます。これらはクリーンアーキテクチャーが影響を受けたヘキサゴナルアーキテクチャーで定義される語彙です。この記事では上記概念図のアダプター層・Frameworks & Driver 層を一体のものとして扱い、まとめたものを指すときはアダプター層と呼びます。

分類 Primary Adapter Secondary Adapter
動作分類 能動的動作(ユースケース層を呼び出す。) 受動的動作(ユースケース層に呼び出される。)
動作例 イベントをトラップしてコマンドを発行 コアロジックの実行結果を永続化
レイヤードアーキテクチャー対応 プレゼンテーション層 データアクセス層

依存性の逆転

クリーンアーキテクチャーでは、依存の方向として外側から内側にアダプター層ユースケース層エンティティ層の方向に依存すべきとの説明があります。問題になりがちなのがデータアクセス等行う Secondary Adapter で、アプリケーションコアからこれをそのまま呼び出したら内側から外側に依存します。これを避けるためにデータアクセスを行う機能をインターフェース化しておき、アプリケーションコアからはインターフェースに対して操作を行います。Secondary Adapter はモックに置き換え可能となります。これについてはレイヤードアーキテクチャーでも行われていますので、クリーンアーキテクチャー独特のものではありません。またこれまで私は行ってこなかったのですが、クリーンアーキテクチャーでは Primary Adapter がアプリケーションコアを呼ぶ際にも同様にユースケース層をインターフェース化しておき、ユースケース層をモックに置き換え可能にします(図右下のUMLを参照)。

Primary Adapter からアプリケーションコア呼び出し時のインターフェースを Input Port、アプリケーションコアから Secondary Adapter 呼び出し時のインターフェースを Output Port と呼びます。

各層への機能割当

ExcelVBACodeSync での機能と層の分類を抜粋したのが下表です。

名前 機能 分類
VbaModuleSync VBA モジュールと外部ファイルの同期を実行するアプリケーションサービス ユースケース
ExcelState Excelの状態を保持 ステート(後述)
MacrobookInfo マクロ付きワークブックの情報を保持 エンティティ
ExcelEventListener Excel のイベントをトラップしてコマンドを発行 Primary Adapter
ExcelOperator Excel に対する操作 Secondary Adapter
ProjectSourceWatcher FileSystemWatcher を使ってファイルの更新を検出 Primary Adapter
ProjectSources VBA ソースコードエクスポート先のディレクトリに対する操作 Secondary Adapter

循環参照

インターフェースと実装を結び付けるのにしばしば DI(Dependency Injection, 依存性の注入) が使われます。

ExcelVBACodeSync で Dependency Injection を使ってインスタンス化しようとすると循環参照を検出して失敗します。

インターフェースによって依存の方向は見かけ上1方向になっているのですが、実装しているオブジェクト同士は、呼び出しの方向がPrimary Adapter → ユースケース層 → Secondary Adapter となるのでこの通りに依存します。ExcelVBACodeSync では上表の通り、Excel側、フォルダー側ともに Primary Adapter、Secondary Adapter に分けているので循環参照にならないように見えます。しかし、完全分離は難しくExcel側、フォルダー側ともにそれぞれ違った理由で Secondary Adapter が Primary Adapter を参照しています。つまり、Primary Adapter → ユースケース層 → Secondary Adapter → Primary Adapter といった循環が生じています。

この解決方法としては、Secondary Adapter が Primary Adapter を呼び出すのではなく、Primary Adapter が Secondary Adapter が発生させるイベントをトラップすることが考えられます。つまり循環参照しないように依存の方向を変えるのです。今回は技術的な問題に引きずられて制御を変えるのを避けたかったのでこの方法を取りませんでした。

DIコンテナーの作成

この問題を解決するため循環参照があってもインスタンス化できるDIコンテナーを作成しました。

このコンテナーは依存チェーンにあるすべてのオブジェクトを生成してからその直後にメソッドインジェクションを実行します。メソッドインジェクションで再び依存チェーンのオブジェクトを生成し、この動作を再帰的に実行して、最終的にすべてのオブジェクトを生成します。使うときには循環チェーン上のいずれかの依存をコンストラクターインジェクションではなく、メソッドインジェクションにすれば問題なく生成してくれます。当初使っていた Microsoft.Extensions.DependencyInjection を含め、メジャーなコンテナーはこのような機能を持っていないようです。

ExcelVBACodeSync への適用

アダプター層とFrameworks & Driver 層を区別しないで一体のものとして実装しました。

クリーンアーキテクチャーで層をまたぐときに渡すデータは DTO を作って渡すことが推奨されているようですが、これは採用しませんでした。詰め替えることにメリットを感じませんでした。ですのでアダプター層からドメインモデルを参照しています。但し余計なプロパティを参照しないように公開メンバーを限定したインターフェースを通じて参照しています。

上記クリーンアーキテクチャー概念図ではユースケース層があります。UI とアプリケーションが継続的に情報をやり取りするシステムではユースケースを受け取るだけでは足りません。アダプター層のオブジェクトの状態の写し(ステート)を持っておくことでアダプター層がシンプルになります。ExcelVBACodeSync ではユースケースとステートの両方を包含するので該当部分をユースケース層とは呼ばず、アプリケーション層と呼んでいます。

Excel 上のリボンのボタンの有効・無効の切り替えはユースケースからリボンに切り替えを指示するのではなく、 ステートのイベントを ExcelEventListener がトラップしたタイミングで更新するようにしました。ステートは MVVM でいうところのView Modelの役割をしていると理解できます。

総括

下記の点が印象に残りました。

  • クリーンアーキテクチャーを採用したことで複数のコマンドトリガー発生源からの要求を無理なく処理する実装となった。
  • 概念図をそのままトレースするのではなくアプリケーション層にステートを導入することでアダプター層を単純化できた。
  • 作成した DIコンテナーは大変使い勝手がよく、インスタンス化のことを気にすることなくクラスを作成できるようになった。

クリーンアーキテクチャーは ExcelVBACodeSync のように、複数のコマンド発生源があり、一つの外部要素がユースケースを呼び出すと同時にユースケースに呼び出される場合に真価を発揮すると思いました。
開発にあたり Claude sonnet, Gemini, ChatGPT 等各種 AI エージェントには助けられました。これらなしにはクリーンアーキテクチャーを適切に実践することは難しかったと思います。

参照