【テクニカル・上級編】VB.NETでのReadOnlyDictionaryとImmutable Collections:スレッドセーフで変更不可能なコレクションによる堅牢なドメインモデル設計 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

VB.NETにおけるReadOnlyDictionaryとImmutable Collections:スレッドセーフで堅牢なドメインモデル設計の極意

長年、Visual Basicの世界に身を置き、VBAマクロからVB6、そしてVB.NETへと、その進化の波を肌で感じてきた。レガシーシステムの延命、複雑なWindows APIの駆使、そして近年ではAPI連携によるシステム間連携の最適化… 数々の現場で、コードの「堅牢性」と「保守性」が、いかにビジネスの根幹を支えるかを痛感してきた。

特に、マルチスレッド環境や外部からの意図しない改変を防ぐという観点では、コレクションの扱いは永遠の課題と言える。今回は、VB.NETにおける`ReadOnlyDictionary`と`Immutable Collections`に焦点を当て、これらを活用することで、いかにしてスレッドセーフで変更不可能な、文字通り「堅牢」なドメインモデルを構築できるのか、その極意を深掘りしていく。

なぜ「変更不可」が重要なのか?

まず、なぜ「変更不可」(Immutable)なデータ構造が、現代のシステム開発において重要視されるのかを明確にしておこう。

  • 意図しない副作用の排除: プログラムの実行中に、あるデータが意図せず変更されると、その影響は広範囲に及び、デバッグは困難を極める。特に、共有されるコレクションの場合、複数のスレッドから同時にアクセスされた際に競合が発生し、予測不能な結果を招く。
  • スレッドセーフティの実現: マルチスレッド環境では、複数のスレッドが同時に同じデータにアクセス・変更しようとすると、データ破損や競合状態(Race Condition)が発生するリスクがある。`Immutable Collections`は、一度生成されたら変更できないため、スレッド間で安全に共有できる。ロック処理(`SyncLock`など)を別途実装する必要がなくなり、コードがシンプルになるだけでなく、デッドロックのリスクも排除できる。
  • デバッグの容易化: 状態が変更されないということは、ある時点でのデータの状態を追跡するのが容易になる。バグが発生した場合でも、問題の原因となっている状態遷移を特定しやすくなる。
  • ドメインモデルの整合性維持: ドメインモデルは、ビジネスロジックの中核を担う。その状態が容易に変更できてしまうと、ビジネスルールが破綻するリスクが高まる。不変なコレクションは、ドメインオブジェクトの状態を整合性の取れたものに保つ助けとなる。

`ReadOnlyDictionary`と`Immutable Collections`の基本

VB.NET(および.NET Framework/.NET Core)では、これらの「変更不可」なデータ構造を扱うための機能が提供されている。

1. `ReadOnlyDictionary`

これは、`Dictionary`をラップし、外部からの変更操作(追加、削除、更新)を不可能にするインターフェースである。元の`Dictionary`自体は変更可能だが、`ReadOnlyDictionary`を通してアクセスする限り、その変更はできない。

実務的な利用シナリオ:

  • 設定情報や定数リストなど、プログラム実行中に変更されるべきではないデータを、外部のモジュールや関数に渡す場合。
  • APIレスポンスとして受け取ったデータを、アプリケーション内で安全に参照する際。

コード例:

.net
Imports System.Collections.Generic

Public Module ReadOnlyDictionaryExample

Sub Main()
‘ 通常のDictionaryを作成
Dim configurationSettings As New Dictionary(Of String, String)()
configurationSettings.Add(“DatabaseServer”, “localhost”)
configurationSettings.Add(“ApiEndpoint”, “https://api.example.com”)
configurationSettings.Add(“TimeoutSeconds”, “30”)

‘ ReadOnlyDictionaryとして公開
‘ これにより、外部から設定値を変更されるのを防ぐ
Dim readOnlyConfig As IReadOnlyDictionary(Of String, String) = configurationSettings

Console.WriteLine(“— Configuration Settings —“)
‘ 安全に値を参照できる
For Each kvp In readOnlyConfig
Console.WriteLine($”{kvp.Key}: {kvp.Value}”)
Next

‘ 以下の操作は例外を発生させる(またはコンパイルエラー)
‘ readOnlyConfig.Add(“NewSetting”, “SomeValue”) ‘ 例外発生
‘ readOnlyConfig(“DatabaseServer”) = “192.168.1.1” ‘ 例外発生

‘ 元のDictionaryは変更可能なので、必要であればこちらで変更する
configurationSettings(“DatabaseServer”) = “production.db.example.com”
Console.WriteLine(vbCrLf & “— Original Dictionary Updated —“)
Console.WriteLine($”DatabaseServer (via ReadOnly): {readOnlyConfig(“DatabaseServer”)}”) ‘ 更新された値が参照できる
End Sub

End Module

解説:

`configurationSettings`は通常の`Dictionary`なので、`Add`やキーによる値の代入が可能です。しかし、`readOnlyConfig`は`IReadOnlyDictionary(Of String, String)`型として参照されるため、これらの変更操作は試みても例外が発生します。これで、設定値が不用意に書き換えられることを防げます。

2. Immutable Collections (`System.Collections.Immutable`)

.NET Framework 4.5 以降、および .NET Core/.NET 5+ では、`System.Collections.Immutable` 名前空間に、真に不変なコレクションクラス群が用意されています。これらは、`ImmutableArray`、`ImmutableList`、`ImmutableDictionary`、`ImmutableHashSet` など、一般的なコレクションの不変バージョンです。

これらのコレクションは、一度生成されると、その状態を直接変更することはできません。要素の追加や削除などを行うと、元のコレクションはそのままに、新しいコレクションが生成されます。これは、シャローコピーではなく、必要に応じてディープコピーのように振る舞うこともありますが、基本的には「変更操作は常に新しいインスタンスを返す」という設計思想に基づいています。

実務的な利用シナリオ:

  • ドメインモデルのプロパティとして、状態が変更されないべきリストや辞書を保持する場合。
  • イベントソーシングアーキテクチャにおいて、過去のイベント履歴を不変なコレクションとして管理する場合。
  • キャッシュされたデータを、キャッシュ機構外からの変更から保護する場合。

コード例:

.net
Imports System.Collections.Immutable
Imports System.Linq

Public Module ImmutableCollectionsExample

‘ ドメインモデルの例
Public Class UserProfile
Public Property UserId As Integer
Public Property Username As String
‘ 連絡先情報を不変な辞書として保持
Public ReadOnly Property ContactInfo As IImmutableDictionary(Of String, String)

‘ コンストラクタで初期化(変更不可)
Public Sub New(userId As Integer, username As String, contactInfo As IImmutableDictionary(Of String, String))
Me.UserId = userId
Me.Username = username
Me.ContactInfo = contactInfo ‘ 参照をそのまま保持
End Sub

‘ 連絡先情報を追加した新しいUserProfileインスタンスを生成するメソッド
‘ 元のインスタンスは変更されない
Public Function AddContact(key As String, value As String) As UserProfile
‘ ImmutableDictionaryのAddメソッドは新しいインスタンスを返す
Dim newContactInfo = Me.ContactInfo.Add(key, value)
‘ 新しいUserProfileインスタンスを生成して返す
Return New UserProfile(Me.UserId, Me.Username, newContactInfo)
End Function
End Class

Sub Main()
‘ 初期連絡先情報を作成
Dim initialContact As IImmutableDictionary(Of String, String) = ImmutableDictionary.Create(Of String, String)() _
.Add(“Email”, “user@example.com”)

‘ UserProfileインスタンスを生成
Dim user1 As New UserProfile(1, “Alice”, initialContact)

Console.WriteLine($”User: {user1.Username}, Email: {user1.ContactInfo(“Email”)}”)

‘ 連絡先情報を追加する
‘ AddContactメソッドは新しいUserProfileインスタンスを返す
Dim user2 As UserProfile = user1.AddContact(“Phone”, “123-456-7890”)

Console.WriteLine(vbCrLf & “— After adding Phone —“)
‘ user1は変更されていない
Console.WriteLine($”User1’s ContactInfo contains Phone: {user1.ContactInfo.ContainsKey(“Phone”)}”) ‘ False
Console.WriteLine($”User1’s Email: {user1.ContactInfo(“Email”)}”) ‘ user@example.com

‘ user2は新しい連絡先情報を持っている
Console.WriteLine($”User2’s ContactInfo contains Phone: {user2.ContactInfo.ContainsKey(“Phone”)}”) ‘ True
Console.WriteLine($”User2’s Email: {user2.ContactInfo(“Email”)}”) ‘ user@example.com
Console.WriteLine($”User2’s Phone: {user2.ContactInfo(“Phone”)}”) ‘ 123-456-7890

‘ ImmutableDictionary自体も直接変更できない
‘ initialContact.Add(“Fax”, “987-654-3210”) ‘ コンパイルエラーまたは例外
End Sub

End Module

解説:

`UserProfile`クラスの`ContactInfo`プロパティは`ReadOnly`であり、`IImmutableDictionary(Of String, String)`型です。コンストラクタで初期化された後、そのインスタンスの状態は変更できません。

`AddContact`メソッドは、`user1`インスタンスの`ContactInfo`を直接変更するのではなく、`ImmutableDictionary.Add`メソッドを使って新しい連絡先情報を持つ新しい`ImmutableDictionary`を生成しています。そして、その新しい`ImmutableDictionary`と元の`UserId`, `Username`を使って、新しい`UserProfile`インスタンスを生成して返しています。

このパターンは、ドメインモデルの「状態」を安全に管理する上で非常に強力です。元の`user1`インスタンスは一切変更されず、`user2`という全く新しい状態を持つインスタンスが生成されます。これにより、過去の状態への参照が壊れる心配がなく、マルチスレッド環境でも安心して共有できます。

レガシー環境とAPI連携における考慮事項

Windows API呼び出しとの連携

レガシーシステム、特にVB6時代から引き継がれているようなコードベースでは、COMコンポーネントやWin32 APIの呼び出しが頻繁に行われている場合がある。これらのAPIは、しばしばポインタや構造体を直接操作し、メモリ上のデータを書き換える。

`Immutable Collections`で管理されているデータは、APIに渡す際に注意が必要だ。APIがそのデータを変更する可能性がある場合、渡す前にコピーを作成し、そのコピーをAPIに渡すか、APIからの戻り値を受け取った後に新しい`Immutable Collection`を生成し直す必要がある。

例: あるAPIが、渡された`StringBuilder`オブジェクトの内容を変更するとする。

.net
‘ 危険な例:APIがsbの内容を変更する可能性がある
‘ Dim sb As New System.Text.StringBuilder(“Initial Data”)
‘ Call SomeApiThatModifiesStringBuilder(sb)
‘ Console.WriteLine(sb.ToString()) ‘ 予期せぬ内容になっている可能性

‘ 安全な例:Immutableなデータがあれば、それをコピーしてから渡す
‘ (ImmutableDictionary自体は直接APIに渡すより、その内容を一度取り出すべき)
Dim safeData As ImmutableList(Of String) = ImmutableList.Create(“A”, “B”, “C”)

‘ APIに渡す前に、mutableなリストにコピーする
Dim mutableList As New List(Of String)(safeData)
Call SomeApiThatModifiesList(mutableList) ‘ APIはmutableListを変更する

‘ APIからの戻り値で新しいImmutableListを生成する(必要なら)
Dim updatedSafeData As ImmutableList(Of String) = mutableList.ToImmutableList()

メモリ最適化とオブジェクトの明示的解放

`Immutable Collections`は、変更操作のたびに新しいオブジェクトを生成するため、一見するとメモリ使用量が増加するように思えるかもしれない。しかし、これは「不要になった古いオブジェクトはガベージコレクションの対象になる」という.NETのメモリ管理モデルに則っている。

VB6のような手動でのメモリ解放(`Set obj = Nothing`)が一般的だった時代とは異なり、VB.NET(.NET Framework/.NET Core)では、基本的にはガベージコレクタ(GC)に任せるのがセオモデリングになる。オブジェクトのライフサイクルをGCに委ねることで、開発者はより本質的なビジネスロジックに集中できる。

ただし、COMオブジェクトなど、GCの管理外のアンマネージドリソースを扱う場合は、`IDisposable`インターフェースを実装し、`Using`ブロックで適切に解放する必要がある。`Immutable Collections`自体はマネージドリソースなので、通常は明示的な解放は不要である。

システム間連携における堅牢性

外部システムとの連携では、データの整合性や信頼性が極めて重要になる。APIクライアントとして外部APIから取得したデータを、アプリケーション内で安全に扱うために`Immutable Collections`は非常に有効だ。

例えば、外部から取得したユーザーリストをキャッシュしておき、アプリケーション内で参照する場合を考える。

.net
Imports System.Collections.Immutable
Imports System.Net.Http
Imports Newtonsoft.Json ‘ 例としてNewtonsoft.Jsonを使用

‘ 外部APIから取得するユーザーデータの構造体(仮)
Public Class ExternalUser
Public Property Id As Integer
Public Property Name As String
Public Property Email As String
End Class

‘ アプリケーション内でキャッシュするデータ構造
Public Class UserCache
‘ ユーザーデータを不変なリストとして保持
Private ReadOnly _cachedUsers As IImmutableList(Of ExternalUser)

‘ コンストラクタで初期化
Public Sub New(users As IEnumerable(Of ExternalUser))
‘ 取得したデータを不変なリストに変換
Me._cachedUsers = users.ToImmutableList()
End Sub

‘ 特定のユーザーIDで検索するメソッド
Public Function FindUserById(userId As Integer) As ExternalUser
‘ ImmutableListのLINQ拡張メソッドが利用可能
Return _cachedUsers.FirstOrDefault(Function(u) u.Id = userId)
End Function

‘ 全ユーザーリストを取得(参照のみ)
Public Function GetAllUsers() As IImmutableList(Of ExternalUser)
Return _cachedUsers
End Function

‘ ユーザーを追加する(実際には新しいCacheインスタンスを生成すべきだが、ここでは例として)
‘ Public Function AddUser(newUser As ExternalUser) As UserCache
‘ Dim updatedUsers = _cachedUsers.Add(newUser)
‘ Return New UserCache(updatedUsers) ‘ 新しいCacheインスタンスを返す
‘ End Function
End Class

Public Module SystemIntegrationExample

‘ 実際のAPI呼び出しは省略
Function FetchUsersFromApi() As IEnumerable(Of ExternalUser)
‘ ダミーデータ
Return New List(Of ExternalUser) From {
New ExternalUser With {.Id = 1, .Name = “Alice”, .Email = “alice@example.com”},
New ExternalUser With {.Id = 2, .Name = “Bob”, .Email = “bob@example.com”}
}
End Function

Sub Main()
‘ 1. APIからデータを取得
Dim apiUsers = FetchUsersFromApi()

‘ 2. Cacheインスタンスを生成(データはImmutableListに変換される)
Dim userCache As New UserCache(apiUsers)

‘ 3. アプリケーション内で安全にデータを参照
Dim alice = userCache.FindUserById(1)
If alice IsNot Nothing Then
Console.WriteLine($”Found User: {alice.Name}, Email: {alice.Email}”)
End If

‘ ユーザーリストを外部に提供する際も、ImmutableListで提供
Dim allUsers = userCache.GetAllUsers()
‘ allUsers.Add(…) ‘ ここで追加しようとしてもできない
For Each user In allUsers
Console.WriteLine($” – {user.Name}”)
Next

‘ もしAPIがユーザーを追加して更新されたデータを返してきた場合、
‘ 新しいCacheインスタンスを生成して置き換えるべき
‘ Dim updatedApiUsers = FetchUpdatedUsersFromApi()
‘ userCache = New UserCache(updatedApiUsers)
End Sub

End Module

この例では、APIから取得した`ExternalUser`のリストを`UserCache`クラス内で`ImmutableList(Of ExternalUser)`として保持しています。これにより、`UserCache`クラスの外部や、`UserCache`クラス内の他のメソッドから、キャッシュされているユーザーリストが意図せず変更されることを防ぎます。

まとめ:堅牢なシステム設計への道

`ReadOnlyDictionary`や`Immutable Collections`の活用は、単なるコレクションの「変更不可」という表面的な特性に留まらない。それは、システム全体の信頼性、保守性、そしてスケーラビリティを高めるための、アーキテクチャレベルでの重要な設計判断である。

特に、以下のような場面でその真価を発揮する。

  • ドメインモデルの整合性維持: ビジネスロジックの中核を担うエンティティや値オブジェクトの状態を、不変に保つことで、予期せぬ状態変化によるバグを防ぐ。
  • マルチスレッド環境での安全性: ロック機構の複雑さを回避し、シンプルかつ安全にデータを共有できる。
  • レガシーシステムとの連携: 外部APIやCOMオブジェクトなど、状態が変化しやすい要素と、アプリケーション内部の信頼性の高いデータを分離する境界線として機能させる。
  • テスト容易性の向上: 状態が予測可能であるため、単体テストの記述が容易になる。

VB.NETは、その進化の過程で、これらの現代的なプログラミングパラダイムをサポートする強力な機能を提供してきた。私たちが長年培ってきた、リソース管理、API連携、そして堅牢なシステム設計の知見を、これらの新しいツールと組み合わせることで、さらに高品質で、長期にわたって価値を提供し続けられるシステムを構築することが可能になる。

複雑なシステムを「シンプルに、しかし正確に」設計するために、不変性(Immutability)という概念は、今後も我々の武器であり続けるだろう。

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