疎結合の罠:なぜあなたのVB.NETアプリはメモリを食い尽くすのか
業務アプリケーションを構築していると、必ず直面する壁がある。「画面Aで保存したデータを、画面Bに即座に反映させたい」という要件だ。
多くの開発者はここで、安易に親画面や静的プロパティにイベントを直結させる。あるいは、`AddHandler` でイベントを購読し、そのまま放置する。これが、「メモリリーク」という名の時限爆弾を仕込んでいることに気づかないまま。
VB.NETにおけるイベント購読は、「強い参照(Strong Reference)」だ。購読者が生きている限り、発信者もGC(ガベージコレクション)の対象から外れる。画面を閉じてもインスタンスがメモリに残り続け、操作を繰り返すたびにアプリは重くなり、やがてクラッシュする。
本記事では、この悪夢を断ち切るための「WeakEventManagerによるオブザーバーパターン」という解法を授ける。
—
なぜ従来のイベント設計ではいけないのか
従来の「イベントを購読して、`Dispose`で`RemoveHandler`する」手法は、人間が管理する限り必ず綻びが生じる。
- `Dispose` を呼び忘れた。
- 例外発生時に `RemoveHandler` がスキップされた。
- 複雑なUIツリーの中で、誰が購読終了の責任を持つのか不明確。
これらを「運用」でカバーするのはプロの仕事ではない。構造的に解決すべきだ。
—
解決策:WeakEventManagerの採用
`WeakEventManager` は、イベントソースに対して「弱い参照」を保持する。ソース側が購読者の存在を気にしないため、購読者がGCの対象になれば、自然と購読関係も消滅する。これがメモリリークフリーな疎結合アーキテクチャの核心だ。
実装コード:汎用イベントバス
まずは、アプリケーション全体で使える「メッセージングのハブ」を作成する。
Imports System.Windows
‘ 画面間で受け渡すデータの型
Public Class DataChangedMessage
Public Property Payload As String
End Class
‘ メッセージングの基盤となるイベントソース
Public Class DataService
‘ イベントの定義(通常のDelegate)
Public Shared Event DataChanged As EventHandler(Of DataChangedMessage)
Public Shared Sub NotifyDataChanged(payload As String)
‘ ここでイベントを発行
RaiseEvent DataChanged(Nothing, New DataChangedMessage With {.Payload = payload})
End Sub
End Class
実装コード:購読側(メモリリークフリーな設計)
次に、このイベントを購読する画面側を実装する。ここで `WeakEventManager` の出番だ。
Public Class ReceiverForm
Inherits Form
Implements IWeakEventListener
Public Sub New()
‘ イベントを購読(WeakEventManager経由で登録)
‘ これにより、このフォームが破棄されてもメモリリークしない
WeakEventManager(Of DataService, DataChangedMessage).AddHandler(
AddressOf DataService.NotifyDataChanged, Me)
End Sub
‘ IWeakEventListenerの実装:イベント発生時にここが呼ばれる
Public Function ReceiveWeakEvent(managerType As Type, sender As Object, e As EventArgs) As Boolean Implements IWeakEventListener.ReceiveWeakEvent
If managerType Is GetType(WeakEventManager(Of DataService, DataChangedMessage)) Then
Dim msg = DirectCast(e, DataChangedMessage)
‘ 画面更新処理
Me.Label1.Text = msg.Payload
Return True
End If
Return False
End Function
End Class
—
運用上の注意点とアーキテクチャの真実
1. 「静的」なデータ連携の注意点
上記の例は `Shared` (静的) なイベントソースを使用している。これは非常に強力だが、同時に「どこからでも変更できる」というリスクも孕む。業務ロジック(データベース連携など)を記述する場合は、必ずサービス層を一枚噛ませ、直接UIがデータベースを叩かない設計を徹底すること。
2. スレッドモデルの意識
`WeakEventManager` はUIスレッド以外からの発行も受け付ける。もし `DataService` がバックグラウンドスレッドで動作している場合、`ReceiveWeakEvent` 内でUIコンポーネントを直接操作すると例外(クロススレッドエラー)が発生する。その場合は、`Me.Invoke(Sub() …)` を介してUIスレッドに同期させることを忘れてはならない。
3. なぜ今、このパターンなのか
現代の業務アプリにおいて、スピードよりも優先されるのは「継続的な保守性」だ。画面が10個、20個と増えたとき、従来の強い参照による結合は開発者を地獄へ引きずり込む。WeakEventManagerによる設計は、画面間を「疎」に保つための、いわば「関心の分離」の極致である。
—
結びに:伝説のエンジニアとしてのアドバイス
「とりあえず動くコード」を書くのはジュニアレベルの仕事だ。
我々シニアエンジニアは、「コードが消える瞬間(ライフサイクル)」を設計する。
メモリを浪費するアプリケーションは、美しくない。そして美しくないコードは、必ず後任者に負債として牙を剥く。今日紹介したパターンを自社フレームワークに組み込み、二度と「イベントの解除漏れ」というバグに工数を使わない環境を作ってほしい。
さあ、次は君たちがこの設計を基盤に、より堅牢なシステムを構築する番だ。健闘を祈る。
