【実務・中級編】VB.NETによるリフレクションとAppDomainを活用したプラグイン機構の自社開発:コアアプリケーションを変更せずに外部DLLから新機能・帳票レイアウトを動的ロードするアーキテクチャ – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

VB.NETによるリフレクションとAppDomainを活用したプラグイン機構の自社開発:コアアプリケーションを変更せずに拡張性を極限まで高めるアーキテクチャ

こんにちは。エンタープライズ領域のシステムアーキテクトとして、数々の基幹系・業務自動化システムを構築してきた。

業務アプリケーションを開発していて、最も頭を悩ませる瞬間はいつだろうか?
それは、「仕様変更」と「新機能の追加」、そして特に「特定の顧客向け、あるいは法改正に伴う帳票レイアウトの追加」を求められた時だ。

「新しい帳票を1枚追加するだけで、なぜコアシステム全体を再コンパイルし、全端末へのデプロイを行わなければならないのか?」

この泥臭い運用のループから開発チームを解放し、真の疎結合を実現するのが 「プラグイン機構」 である。
今回は、VB.NET(.NET Framework環境)におけるリフレクション(Reflection)と、アセンブリ分離の切り札である AppDomain(アプリケーションドメイン) を駆使し、「実行中のコアを落とさず、外部DLLから安全に機能と帳票を動的ロード・アンロードする」エンタープライズ・アーキテクチャを伝授する。

中途半端なサンプルコードではない。現場のメモリリークやファイルロックの罠をすべて踏み越えてきたプロの設計を、その目に焼き付けてほしい。

—

1. なぜ「単純なリフレクション」だけでは業務システムで破綻するのか?

多くのジュニアプログラマは、動的ロードと聞くとこう実装する。

.net
‘ 【アンチパターン】単なるAssembly.LoadFrom
Dim asm As Assembly = Assembly.LoadFrom(“Plugin.dll”)
Dim type As Type = asm.GetType(“MyPlugin.ReportA”)
Dim instance As IReport = CType(Activator.CreateInstance(type), IReport)
instance.Execute()

一見、これで動くように見える。しかし、業務システムでこれをやると数日後に必ずシステムが死ぬ。

.NETの致命的な罠:アセンブリのアンロード不可能性

.NET Frameworkにおいて、`Assembly.LoadFrom` や `Assembly.Load` で読み込まれたアセンブリは、一度メモリ(AppDomain)にロードされると、アプリケーションが終了するまでメモリから解放(アンロード)されない。

もしユーザーがプラグインのDLLを修正し、コアアプリを再起動せずに「再読み込み」を行いたい場合、古いDLLファイルはロックされ続け、メモリ空間は肥大化(メモリリーク)していく。動的アップデートなど夢のまた夢だ。

解決策:AppDomainによる「隔離空間」の創出

この問題を解決唯一の手段が AppDomain(アプリケーションドメイン) である。
アセンブリをデフォルトのドメインではなく、専用の子ドメイン(Sandbox)にロードし、不要になったらドメインごと破棄(Unload)する。 これにより、DLLファイルのロックは完全に解除され、メモリも綺麗に回収される。

—

2. 全体アーキテクチャの設計

堅牢なプラグイン機構は、以下の3層で構成される。

1. SharedInterface(共有インターフェイスプロジェクト)

  • コアとプラグイン双方が参照する共通の契約(Contract)。
  • ここにはビジネスロジックは書かない。純粋なインターフェイス(`IPlugin`, `IReportPlugin`)のみを定義する。

2. CoreApplication(ホスト・Windows Formsアプリケーション)

  • プラグインをスキャンし、動的にUI(メニューやボタン)を構築する親アプリ。
  • 子AppDomainの生成・管理・破棄を担当。

3. ExternalPlugin(外部DLLプロジェクト)

  • コアとは完全に独立してビルドされるプラグイン群。

—

3. 実装コード:プロダクション・レディな実装

ここから、実際のプロジェクトでそのまま使えるコードを提示する。

Step 1: 共有インターフェイスの定義 (`SharedInterface.vb`)

まずはコアとプラグインの共通言語となる契約を結ぶ。

.net
Namespace Enterprise.Plugin.Contract

‘ マーシャル可能(別AppDomain間で通信するため MarshalByRefObject を継承する)
Public Interface IPlugin
ReadOnly Property PluginName As String
ReadOnly Property Version As String
Sub Initialize(ByVal hostContext As IHostContext)
Sub Execute()
End Interface

‘ ホスト側の機能をプラグインに安全に提供するためのコンテキスト
Public Interface IHostContext
Sub WriteLog(ByVal message As String)
Function GetDatabaseConnectionInfo() As String
End Interface

End Namespace

Step 2: プラグイン本体の実装 (`SampleReportPlugin.vb`)

次に、独立したDLLプロジェクト側の実装。

.net
Imports Enterprise.Plugin.Contract
Imports System.Windows.Forms

_
Public Class SampleReportPlugin
Implements IPlugin

Public ReadOnly Property PluginName As String Implements IPlugin.PluginName
Get
Yield “月次売上帳票出力プラグイン”
End Get
End Property

Public ReadOnly Property Version As String Implements IPlugin.Version
Get
Return “1.0.0.0”
End Get
End Property

Private _context As IHostContext

Public Sub Initialize(hostContext As IHostContext) Implements IPlugin.Initialize
_context = hostContext
_context.WriteLog(“SampleReportPlugin が初期化されました。”)
End Sub

Public Sub Execute() Implements IPlugin.Execute
‘ 実際の業務処理や帳票プレビュー画面の呼び出し
MessageBox.Show(“外部DLLから安全に呼び出された帳票処理です。”, “プラグイン実行”, MessageBoxButtons.OK, MessageBoxIcon.Information)
_context.WriteLog(“月次売上帳票の出力処理が実行完了しました。”)
End Sub
End Class

Step 3: コアアプリケーション側の動的ロード・アンロード機構 (`PluginManager.vb`)

ここが最も重要だ。別AppDomainへの読み込みと、安全なアンロードの仕組みをカプセル化する。

.net
Imports System.IO
Imports System.Reflection
Imports Enterprise.Plugin.Contract

Public Class PluginHostProxy
Inherits MarshalByRefObject

‘ AppDomainを跨いでプラグインを安全に実行するためのプロキシクラス
Private _pluginInstance As IPlugin

Public Function LoadAndInit(ByVal assemblyPath As String, ByVal typeName As String, ByVal context As IHostContext) As Boolean
Try
Dim asm As Assembly = Assembly.LoadFrom(assemblyPath)
Dim t As Type = asm.GetType(typeName)
If t Is Nothing Then Return False

_pluginInstance = CType(Activator.CreateInstance(t), IPlugin)
_pluginInstance.Initialize(context)
Return True
Catch ex As Exception
System.Diagnostics.Debug.WriteLine(ex.Message)
Return False
End Function
End Function

Public Sub ExecutePlugin()
If _pluginInstance IsNot Nothing Then
_pluginInstance.Execute()
End If
End Sub

Public ReadOnly Property GetPluginName() As String
Get
If _pluginInstance IsNot Nothing Then Return _pluginInstance.PluginName
Return String.Empty
End Get
End Property
End Class

‘ プラグインのライフサイクルを管理するマネージャー
Public Class PluginDomainManager
Implements IDisposable

Private _domain As AppDomain
Private _proxy As PluginHostProxy

Public Sub LoadPlugin(ByVal assemblyPath As String, ByVal typeName As String, ByVal context As IHostContext)
‘ 1. セキュリティと分離のための独立したAppDomainを作成
Dim setup As New AppDomainSetup()
setup.ApplicationBase = Path.GetDirectoryName(assemblyPath)

_domain = AppDomain.CreateDomain(“PluginDomain_” & Guid.NewGuid().ToString(), Nothing, setup)

‘ 2. 別ドメイン上にプロキシインスタンスを生成
Dim proxyType = GetType(PluginHostProxy)
_proxy = CType(_domain.CreateInstanceAndUnwrap( _
proxyType.Assembly.FullName, _
proxyType.FullName), PluginHostProxy)

‘ 3. プラグインのロード実行
_proxy.LoadAndInit(assemblyPath, typeName, context)
End Sub

Public Sub Execute()
If _proxy IsNot Nothing Then
_proxy.ExecutePlugin()
End If
End Sub

Public Function GetName() As String
If _proxy IsNot Nothing Then Return _proxy.GetPluginName
Return String.Empty
End Function

‘ ★極めて重要:AppDomainを丸ごと破棄することでDLLのロックを完全に解除する
Public Sub Unload()
If _domain IsNot Nothing Then
AppDomain.Unload(_domain)
_domain = Nothing
_proxy = Nothing
End If
End Sub

Public Sub Dispose() Implements IDisposable.Dispose
Unload()
End Sub
End Class

—

4. 現場で絶対に外してはならない「3つの鉄則」

このアーキテクチャを現場に投入する際、以下のポイントを見落とすと痛い目を見る。

① データベース接続や共通設定は「渡す」な、「インターフェイス経由でアクセスさせよ」

プラグイン側に直接DBの接続文字列(ConnectionStrings)をハードコーディングさせてはならない。必ずコア側が提供する `IHostContext`(ホストコンテキスト)を経由させ、トランザクションやログ出力をコア側で一元管理すること。

② 例外の境界(MarshalByRefObject)を意識する

異なるAppDomain間でオブジェクトをやり取りする場合、そのクラスは `MarshalByRefObject` を継承するか、`` 属性が付与されている必要がある。これを怠ると `SerializationException` が発生し、プロセス間通信でアプリがクラッシュする。

③ ファイルロック問題とバージョンアップ運用

プラグインDLLを新しいバージョンに差し替える際、コアアプリを終了せずにDLLファイルを上書きしたい場合がある。
今回紹介した `AppDomain.Unload` を正しく実装していれば、マネージャーの破棄と同時にファイルハンドルが解放されるため、稼働中のアプリを落とさずにプラグインDLLを上書き・再読み込みすることが可能になる。

—

総括

Visual Basic (VB.NET) は、レガシーな言語と揶揄されることがあるが、それは使い手の設計思想が古い場合に限る。.NET Frameworkの深淵である `AppDomain` と `Reflection` を正しく手懐ければ、C#に劣らない高度で堅牢なエンタープライズ・アーキテクチャを構築できる。

仕様変更に怯える日々は今日で終わりにしよう。
「コアを変えずに、拡張する」――このプラグイン機構をあなたのプロジェクトに組み込み、圧倒的な保守性と開発スピードを手に入れてほしい。

タイトルとURLをコピーしました