【テクニカル・上級編】複数画面間における疎結合なデータ共有:WeakEventManagerを活用したメモリリークフリーなオブザーバーパターンの構築 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク
スポンサーリンク

参照の呪縛を解け:WeakEventManagerによるメモリリークフリーなWinForms設計論

Windows Forms(WinForms)を用いた大規模システム開発において、多くのエンジニアが陥る「見えない陥穽」がある。それは、画面間でのデータ同期を実現しようとして、オブジェクト間の結合度を高めすぎ、結果としてメモリリークという名の「ゴースト・オブジェクト」をヒープ領域に増殖させてしまうミスだ。

`AddHandler` を使い、イベントを購読する。一見、至極真っ当な実装に見える。しかし、その背後で「誰が誰を握っているか」という参照のライフサイクルを制御できていなければ、アプリケーションのメモリ消費量は、ユーザーが画面を閉じるたびに右肩上がりに増え続け、最終的には `OutOfMemoryException` という名の死を招く。

本稿では、単なる Observer パターンの実装を超え、`WeakReference` を活用した `WeakEventManager` を自作し、疎結合かつメモリ効率を極限まで高めた、シニアエンジニアのための設計思想を伝授する。

1. 破滅への序曲:なぜ `AddHandler` はメモリを食いつぶすのか

WinForms において、長寿命なオブジェクト(シングルトンのデータ管理クラスや親フォーム)が、短寿命なオブジェクト(詳細表示用のダイアログや子フォーム)のイベントを購読する場合、あるいはその逆で、データモデルのイベントにフォームが `AddHandler` で登録する場合、致命的な事態が発生する。

強参照(Strong Reference)のジレンマ

`.NET` のガベージコレクター(GC)は、あるオブジェクトへの参照が一つでも残っていれば、そのオブジェクトを解放しない。

1. DataModel(長寿命)が `DataChanged` イベントを持つ。
2. FormB(短寿命)が `AddHandler DataModel.DataChanged, AddressOf Me.OnDataChanged` を実行する。
3. この瞬間、`DataModel` 内のデリゲートリストは、`FormB` のメソッドへの参照を保持する。
4. FormB を `Close()` しても、`DataModel` が生きている限り、`DataModel` は `FormB` を「使用中」であると判断する。
5. FormB はメモリ上に残り続け、コントロール(HWND)のリソースも解放されないまま、幽霊のようにメモリを占有し続ける。

これが、大規模な業務アプリケーションにおいて、数時間稼働させただけで動作が重くなる真犯人である。

2. 解決策:Weak Reference(弱参照)による疎結合設計

この問題を回避する唯一の解は、「イベントの発信源が、購読者の生存を強制しない」 構造を作ることだ。

通常の参照(Strong Reference)が「このオブジェクトは絶対に必要だ」という意志表示であるのに対し、`WeakReference` は「もしGCが回収したければ、回収して構わない」という、控えめな参照である。これを利用して、イベントの通知機構を構築する。

アーキテクチャ図

`DataModel` $\rightarrow$ `WeakEventHub` $\leftarrow$ `FormB`

直接 `AddHandler` するのではなく、`WeakEventHub` という仲介役を介し、仲介役は購読者を `WeakReference` としてのみ保持する。

3. 実装:極限の `WeakEventManager`

以下に、VB.NET において堅牢かつ高性能な `WeakEventManager` を実装する。ここでは、スレッドセーフティと、不要になった参照を自動的に掃除するクリーンアップ機構を組み込んでいる。

Imports System.Runtime.CompilerServices

''' <summary>
''' 購読者を弱参照として保持し、メモリリークを防ぐイベント管理クラス
''' </summary>
Public Class WeakEventManager(Of TSubscriber As Class)
    ' 購読者を弱参照として保持するリスト
    Private ReadOnly _subscribers As New List(Of WeakReference(Of TSubscriber))()
    Private ReadOnly _lock As New Object()

    ''' <summary>
    ''' 購読の登録
    ''' </summary>
    ''' <param name="subscriber">購読するオブジェクト</param>
    Public Sub Subscribe(subscriber As TSubscriber)
        SyncLock _lock
            ' すでに登録されていないか確認し、弱参照として追加
            If Not _subscribers.Any(Function(r) r.TryGetTarget(GetType(TSubscriber)).IsSuccess) Then
                _subscribers.Add(New WeakReference(Of TSubscriber)(subscriber))
            End If
        End SyncLock
    End Sub

    ''' <summary>
    ''' 全ての生存している購読者に対して通知を行う
    ''' </summary>
    ''' <param name="sender">イベントの発生源</param>
    ''' <param name="args">イベント引数</param>
    Public Sub Notify(sender As Object, args As EventArgs)
        Dim targetsToNotify As New List(Of TSubscriber)()

        SyncLock _lock
            ' 1. 生きているオブジェクトを抽出、死んでいるものはリストから削除候補にする
            For i As Integer = _subscribers.Count - 1 To 0 Step -1
                Dim target As TSubscriber = Nothing
                If _subscribers(i).TryGetTarget(target) Then
                    If target IsNot Nothing Then
                        targetsToNotify.Add(target)
                    End If
                Else
                    ' GCによって回収されたオブジェクトの参照を削除(クリーンアップ)
                    _subscribers.RemoveAt(i)
                End If
            Next
        End SyncLock

        ' 2. 通知処理(Lockの外で行うことでデッドロックを防止)
        For Each subscriber In targetsToNotify
            ' ここではインターフェースや特定のメソッドを介して通知を行う設計とする
            If TypeOf subscriber Is IWeakObserver(Of EventArgs) Then
                DirectCast(subscriber, IWeakObserver(Of EventArgs)).OnWeakEvent(sender, args)
            End If
        Next
    End Sub
End Class

''' <summary>
''' 弱参照イベントを受けるためのインターフェース
''' </summary>
Public Interface IWeakObserver(Of TEventArgs)
    Sub OnWeakEvent(sender As Object, e As TEventArgs)
End Interface

実践的な利用例:データモデルとフォームの連携

次に、このマネージャーを実際に業務システムへ組み込む例を示す。

''' <summary>
''' 業務データを保持する長寿命なモデル
''' </summary>
Public Class GlobalDataModel
    ' 独自のイベントマネージャーを保持
    Private ReadOnly _eventManager As New WeakEventManager(Of IWeakObserver(Of EventArgs))()

    Public Property CurrentValue As String

    ''' <summary>
    ''' 値を更新し、生存している画面のみに通知する
    ''' </summary>
    Public Sub UpdateValue(newValue As String)
        Me.CurrentValue = newValue
        ' 全ての生存しているUIへ通知
        _eventManager.Notify(Me, EventArgs.Empty)
    End Sub

    Public Sub RegisterObserver(observer As IWeakObserver(Of EventArgs))
        _eventManager.Subscribe(observer)
    End Sub
End Class

''' <summary>
''' データを表示する短寿命なフォーム
''' </summary>
Public Class DisplayForm
    Implements IWeakObserver(Of EventArgs)

    Private ReadOnly _model As GlobalDataModel

    Public Sub New(model As GlobalDataModel)
        InitializeComponent()
        _model = model
        ' マネージャーに自分自身を登録(強参照は作られない)
        _model.RegisterObserver(Me)
    End Sub

    ''' <summary>
    ''' イベント受信時の処理
    ''' </summary>
    Public Sub OnWeakEvent(sender As Object, e As EventArgs) Implements IWeakObserver(Of EventArgs).OnWeakEvent
        ' UIスレッドへのマーシャリングが必要な場合はここで制御
        If Me.InvokeRequired Then
            Me.BeginInvoke(Sub() UpdateUI())
        Else
            UpdateUI()
        End If
    End Sub

    Private Sub UpdateUI()
        ' 実際の表示更新
        Me.Text = $"Current Value: {_model.CurrentValue}"
    End Sub
End Class

4. アーキテクトの視点:パフォーマンスと安全性のトレードオフ

この設計を導入する際、シニアエンジニアとして以下の3点を念頭に置く必要がある。

1. 通知のコストとスレッドセーフティ

`Notify` メソッド内で `SyncLock` を使用しているが、通知自体を `Lock` の中で実行してはならない。通知先のメソッドが再び `Subscribe` を呼び出すような再入(Reentrancy)が発生した場合、デッドロックを引き起こすからだ。上記のコードでは、通知対象のリストを一旦ローカル変数にコピーしてから `Lock` 外で実行することで、このリスクを排除している。

2. GCのタイミングと即時性

`WeakReference` は「いつ回収されるか」を制御できない。フォームを `Close` した直後に `Notify` を呼んでも、GCが走るまでは `OnWeakEvent` が呼ばれる可能性がある。これは致命的なバグではないが、UIのライフサイクル(`Dispose`)と整合性を取るために、フォーム側では `Dispose` 時に明示的に状態を管理する習慣をつけるべきだ。

3. Windows API との境界

WinFormsは内部的に `HWND` を管理している。`WeakEventManager` はマネージド・メモリのリークは防ぐが、もし `CreateFile` や `RegisterWindowMessage` などの Win32 API を用いて未管理リソースを確保している場合、それらは `IDisposable` を通じて確実に解放しなければならない。マネージドな参照が消えても、アンマネージドなハンドルが残れば、それは「リソースリーク」としてシステムを圧迫する。

結語

優れたアーキテクトは、コードの書きやすさよりも、コードが「消えるとき」の振る舞いに腐心する。

`AddHandler` という甘い誘惑に抗い、`WeakReference` を用いた疎結合なイベント駆動設計を選択すること。それは、システムの寿命を決定づける極めて重要な決断である。メモリリークのない、静かで、かつ強靭なアプリケーションを構築せよ。それが、我々プロフェッショナルに課せられた責務である。

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