経緯
以前作成したそこそこ大きなライブラリ + アプリは Strategy Pattern を採用しており、フットプリントを大きくしないために、各 Strategy を個別 DLL に分割して動的ロードする仕組みにしている。
このアプリを Clean Architecture 化するにあたり、この Strategy DLL 分割の依存関係と動的ロードを改善できないか、再考してみた。
csproj 参照依存の整理
今は Strategy Loader 側 csproj に全 Strategy 側 csproj への <ProjectReference Include="..."> 参照を記述している。
この参照が必要になる理由と排除の仕方がわかったため、改善を図る。
// 現行のアセンブリ取得 var aseembly = Assembly.Load(new AssemblyName(stragegy.AssemblyName)); // 改善案のアセンブリ取得 var aseembly = Assembly.Load(stragegy.DllName));
現行は AssemblyName を通じてアセンブリ取得するのに対し、改善案は DLL 名を通じてアセンブリ取得する。
AssemblyName は .NET VM 水準での管理であるため、当然、Strategy 側の論理情報を必要とし、参照が必要になる。
対して DLL 名指定は、ファイルグロブでファイルを掴みに行くため、Strategy 側の論理情報を必要としない。
ただ、これはファイルグロブで掴めることが前提となるため、当然、物理環境への依存度を高めてしまう。
しかし、この案件はそもそもが DLL 分割しており、csproj で論理情報を先行取得していたとしても、物理的に DLL が存在しなければ同じく失敗するため、実はリスク水準は変わらない。
というわけで、DLL 名指定によるアセンブリ取得に切り替え、csproj の参照記述を排除することにした。
改善したいもう1つのポイント
改善したいポイントがもう1つある。が、実現できそうにない ...
それは、strategy.DllName の情報を、Strategy 側のクラス属性として定義したいということ。
いまは、appsettings.json に記述し、アプリ起動時の初期化において、それを読みこんで辞書を作っている。
この、appsettings.json とクラス定義が離れているのが気持ち悪い。
Strategy クラスに関するメタ情報なのでクラス属性として定義し、ソースジェネレータでビルド時に属性を収集して静的に辞書コードを生成する、というのが理想形。
ここには2つの矛盾があって、実現するのはかなり厄介である。
まず1点目。Strategy 側から見れば、外から自分を呼び出すための情報を、そもそも自分が内包している構造であるということ。本質的な矛盾。
2点目。ソースジェネレーターは csproj 横断で動かない。
プロジェクトの独立性を維持するためなのだが、この制約が煩わしい。もしやるなら、Strategy 側でソースジェネレーターを動かし、cs ファイルではなく json ファイルを出力し、これを Strategy Loader 側のソースジェネレーターで収集して辞書コードを生成する必要がある。そして、ここにはビルドの依存関係がある。加えて、ソースジェネレーターはファイル I/O にも制約があって外部情報をなかなか取り込ませてくれない可能性がある。
Chat GPT に訊くと、他にも T4 (Text Template Transformation Toolkit) を使う方法がある、と言うが、それは .NET の世界では obsolete な手法であるため、採用できず。
訊くたびに回答が揺れ動き、堂々巡りになるところを見ると、確立したうまい方法はないようだ。人間の知恵がまだまだ必要だ、というのを、こういうところに感じる。