クリーンアーキテクチャー採用の動機
ExcelのVBAを使ってシステム開発する場合、ExcelのVBIDEを使うことになると思います。そのエディタの使い勝手は Visual Studio Code など今日的なその他のものと比べてあまり便利とは言えません。しかし、VBA特有の書式化・コンパイルチェック・デバッグを考えるとVBIDEを使わないわけにもいきません。そのため、 Visual Studio Code 等外部エディターである程度編集しておき、VBIDEにコピー、また逆に外部エディターに戻す操作を何度も繰り返していました。
これを解決するため、外部エディターと VBIDE のソースを同期する Excel アドインを C#, VSTO を使って作りました。すなわち、マクロが含まれるブックが保存されたら、指定されたフォルダー配下にソースをエクスポート、フォルダーの変更を監視して外部エディターでファイルが変更されたらインポートするようにしました。
このアプリケーションはフォルダー監視、Excelの状態変化の両方がコマンドのトリガーとなります。この点をどのように実装するかが問題となります。例えばCRUDが基本のスタイルではプレゼンテーション層をUI、データアクセス層をデータベースと位置付けるレイヤードアーキテクチャーを使っています。今回は、フォルダーはストレージであると同時にコマンドトリガーにもなります。Excel も同様です。
これらを無理なく実装するのにクリーンアーキテクチャー
が使えそうだと思い採用してみました。
レイヤーアーキテクチャー各層との比較
| レイヤードアーキテクチャー | クリーンアーキテクチャー |
|---|---|
| プレゼンテーション層 | アダプター層 |
| アプリケーション層 | ユースケース層 |
| ドメイン層 | エンティティ層 |
| データアクセス層 | アダプター層 |
普段採用しているレイヤーアーキテクチャーとクリーンアーキテクチャーの各層対応は右表の様になっています。
ユースケース層
・エンティティ層
はそれぞれアプリケーションロジック、ビジネスロジックを記述する部分でありアプリケーションコアと位置づけられます。クリーンアーキテクチャーではプレゼンテーション層もデータアクセス層も技術の詳細に依存する部分としてアプリケーションコアの外部にあるアダプター層
に配置されます。
プレゼンテーション層とデータアクセス層がまったく同一視されるかというとそうではなく、その扱いには違いがあり、Primary Adapter
とSecondary Adapter
に分かれます。
| 分類 | Primary Adapter | Secondary Adapter |
|---|---|---|
| 動作分類 | 能動的動作 | 受動的動作 |
| 動作例 | イベントをトラップしてコマンドを発行 | コアロジックの実行結果を永続化 |
| レイヤーアーキテクチャー対応 | プレゼンテーション層 | データアクセス層 |
依存性の逆転
クリーンアーキテクチャーでは、依存の方向として外側から内側にアダプター層
→ユースケース層
→エンティティ層
の方向に依存すべきとの説明があります。問題になりがちなのがデータアクセス等行う Secondary Adapter で、アプリケーションコアからこれをそのまま呼び出したら内側から外側に依存します。これを避けるためにデータアクセスを行う機能をインターフェース化しておき、アプリケーションコアからはインターフェースに対して操作を行います。Secondary Adapter はモックに置き換え可能となります。これについてはレイヤーアーキテクチャーでも行われていますので、クリーンアーキテクチャー独特のものではありません。またこれまで私は行ってこなかったのですが、クリーンアーキテクチャーでは Primary Adapter がアプリケーションコアを呼ぶ際にも同様にユースケース層をインターフェース化しておき、ユースケース層をモックに置き換え可能にします(図右下のUMLを参照)。
Primary Adapter からアプリケーションコア呼び出し時のインターフェースを Input Port、アプリケーションコアから Secondary Adapter 呼び出し時のインターフェースを Output Port と呼びます。
各層への機能割当
このアプリケーションでの機能と層の分類を抜粋したのが下表です。
| 名前 | 機能 | 分類 |
|---|---|---|
| VbaModuleSync | VBA モジュールと外部ファイルの同期を実行するアプリケーションサービス | ユースケース層 |
| ExcelEventListener | Excel のイベントをトラップしてコマンドを発行 | Primary Adapter |
| ExcelOperator | Excel に対する操作 | Secondary Adapter |
| ProjectSourceWatcher | FileSystemWatcher を使ってファイルの更新を検出 | Primary Adapter |
| ProjectSources | VBA ソースコードエクスポート先のディレクトリに対する操作 | Secondary Adapter |
循環参照
インターフェースと実装を結び付けるのにしばしば DI(Dependency Injection, 依存性の注入) が使われます。
このシステムで Dependency Injection を使ってインスタンス化しようとすると循環参照を検出して失敗します。
インターフェースによって依存の方向は見かけ上1方向になっているのですが、実装しているオブジェクト同士は、呼び出しの方向がPrimary Adapter → ユースケース層 → Secondary Adapter となるのでこの通りに依存します。このシステムでは上表の通り、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 を含め、メジャーなコンテナーはこのような機能を持っていないようです。
総括
クリーンアーキテクチャーを採用したことで関心事の分離、疎結合化を実現できたことは成功だったと思います。またはそのなかで作成した新たな DIコンテナーは大変使い勝手がよく、インスタンス化のことを気にすることなくクラスを作成できるようになりました。
開発にあたり Claude sonnet, Gemini, ChatGPT 等各種 AI エージェントには助けられました。これらなしにはクリーンアーキテクチャーを正確に実践することは難しかったと思います。