データの永続化を「たった一行」で済ませるな:VB.NETにおけるシリアライゼーションの極限設計
Windows Forms(WinForms)を用いた業務アプリケーション開発において、設定ファイルや通信データの保存・復元は避けて通れない工程だ。多くの開発者は、`Newtonsoft.Json` や `System.Text.Json` を使い、まるで魔法のように `JsonConvert.SerializeObject(obj)` と一行書けば済むと考えている。
だが、現場で「動く」ことと、業務システムとして「堅牢である」ことは全く別物だ。
複雑なネスト構造を持つ業務データを扱う際、その「一行」が原因で、循環参照によるスタックオーバーフロー、プロパティの消失、あるいはUIスレッドのフリーズを引き起こし、現場の業務を止める。
今日は、私が数々の炎上案件を鎮火させてきた経験から、VB.NETにおけるシリアライゼーションの「落とし穴」と、プロフェッショナルが実践すべき「設計の鉄則」を伝授する。
1. 循環参照という「静かなる爆弾」
業務データは往々にして、オブジェクト同士が互いを知っている(双方向参照)状態にある。例えば、`部署(Department)` クラスが `社員(Employee)` のリストを持ち、逆に `社員` クラスが所属する `部署` を参照しているような構造だ。
これをそのままシリアライズしようとすると、シリアライザは「部署 → 社員 → 部署 → 社員…」と無限ループに陥り、最終的に `StackOverflowException` を吐いてアプリケーションを強制終了させる。これは `Try…Catch` では捕捉できない、致命的な死だ。
【解決策】
1. DTO(Data Transfer Object)の分離: ドメインモデルを直接シリアライズせず、シリアライズ専用の「平坦なクラス」を用意する。これが最もクリーンな設計だ。
2. 参照設定の利用: `Newtonsoft.Json` の `ReferenceLoopHandling.Ignore` を使う方法もあるが、これはデータの整合性を損なうリスクがある。設計レベルでの解決を優先せよ。
2. 「プロパティ」と「フィールド」の境界線
VB.NETにおいて、`Public MyData As String` と `Public Property MyData As String` は、初心者には同じに見えるだろう。しかし、シリアライザにとっては天と地ほどの差がある。
多くのモダンなシリアライザ(特に `System.Text.Json`)は、デフォルトでは「パブリックなプロパティ」のみを対象とし、フィールドは無視する。
「設定を保存したはずなのに、次回の起動時に値が初期化されている」という不可解なバグの 9 割は、このプロパティとフィールドの混同、あるいは `ReadOnly` プロパティへの書き込み失敗に起因している。
3. Case Sensitivity(大文字・小文字)の罠
`System.Text.Json` は `Newtonsoft.Json` に比べて非常に厳格だ。デフォルトでは、JSONのキーが大文字・小文字を厳密に区別する。
外部APIから受け取った JSON が `userName` なのに、VB.NET 側のプロパティが `UserName` である場合、シリアライズは成功してもデシリアライズ(復元)時に値が `Nothing` になる。これは「バグではないが、仕様として破綻している」状態だ。
4. プロダクションレベルの設計:堅牢なシリアライゼーション・エンジン
では、どのように実装すべきか。単なる関数ではなく、エラーハンドリング、ファイルI/Oの安全性、そしてUIスレッドへの配慮を組み込んだ「サービス層」を構築する設計例を示す。
ここでは、最新の標準である `System.Text.Json` をベースに、実務でそのまま使えるアーキテクチャを提示する。
Imports System.IO
Imports System.Text.Json
Imports System.Text.Json.Serialization
Imports System.Threading.Tasks
''' <summary>
''' 業務データ用DTO。ドメインモデルとは別に定義し、
''' 循環参照や不要なプロパティを排除した「保存専用」の構造にする。
''' </summary>
Public Class AppConfigDto
<JsonPropertyName("app_name")>
Public Property ApplicationName As String = "Default App"
<JsonPropertyName("last_user")>
Public Property LastUserName As String = ""
<JsonPropertyName("window_size")>
Public Property WindowSize As WindowSizeDto = New WindowSizeDto()
End Class
Public Class WindowSizeDto
<JsonPropertyName("width")>
Public Property Width As Integer = 800
<JsonPropertyName("height")>
Public Property Height As Integer = 600
End Class
''' <summary>
''' 設定管理の司令塔。
''' 単一責任の原則に基づき、ファイル操作とシリアライズを隠蔽する。
''' </summary>
Public Class ConfigurationManager
Private ReadOnly _filePath As String
Private ReadOnly _options As JsonSerializerOptions
Public Sub New(fileName As String)
' AppDataフォルダなど、適切な書き込み権限のあるパスを使用すること
Dim appData As String = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)
Dim targetDir = Path.Combine(appData, "MyBusinessApp")
If Not Directory.Exists(targetDir) Then
Directory.CreateDirectory(targetDir)
End If
_filePath = Path.Combine(targetDir, fileName)
' プロフェッショナルな設定:インデント付け、大文字小文字の許容、列挙型の文字列変換
_options = New JsonSerializerOptions() With {
.WriteIndented = True,
.PropertyNameCaseInsensitive = True,
.Converters = { New JsonStringEnumConverter() }
}
End Sub
''' <summary>
''' 非同期で設定を保存する。WinFormsのUIスレッドをブロックしないための必須テクニック。
''' </summary>
Public Async Function SaveConfigAsync(config As AppConfigDto) As Task(Of Boolean)
Try
' 一時ファイルに書き込んでからリネームする手法(Atomic Write)
' これにより、書き込み中のクラッシュによるファイル破損を防ぐ
Dim tempFile = _filePath & ".tmp"
Dim jsonString = JsonSerializer.Serialize(config, _options)
' File.WriteAllTextAsync を使用してI/O待ちによるUIフリーズを回避
Await File.WriteAllTextAsync(tempFile, jsonString)
If File.Exists(_filePath) Then File.Delete(_filePath)
File.Move(tempFile, _filePath)
Return True
Catch ex As Exception
' ここでロギングを行う(EventLogやファイルへの書き出し)
Debug.WriteLine($"[Error] Config Save Failed: {ex.Message}")
Return False
End Try
End Function
''' <summary>
''' 設定を読み込む。ファイルが存在しない場合はデフォルトを返す。
''' </summary>
Public Async Function LoadConfigAsync() As Task(Of AppConfigDto)
If Not File.Exists(_filePath) Then
Return New AppConfigDto() ' デフォルト値を返す
End If
Try
Dim jsonString = Await File.ReadAllTextAsync(_filePath)
Dim config = JsonSerializer.Deserialize(Of AppConfigDto)(jsonString, _options)
Return If(config, New AppConfigDto())
Catch ex As Exception
Debug.WriteLine($"[Error] Config Load Failed: {ex.Message}")
Return New AppConfigDto()
End Try
End Function
End Class
実装のポイント解説
1. DTO(Data Transfer Object)の採用:
`AppConfigDto` を定義することで、ビジネスロジック(ドメイン)と永続化形式(DTO)を完全に切り離している。これにより、将来的にクラス構造が変わっても、JSONの互換性を保つことが容易になる。
2. Atomic Write(原子的な書き込み):
`tempFile` に一度書き出してから `Move` する手法は、極めて重要だ。書き込み中にPCの電源が落ちたり、アプリがクラッシュしたりしても、元の設定ファイルが壊れるのを防げる。
3. Async/Await による UI 非同期化:
WinFormsで `File.WriteAllText` を使うと、ファイルサイズが大きくなった際に「アプリが数秒固まる」現象が発生する。`Async` を使い、I/O待ちの間もメッセージループを回し続けるのがプロの作法だ。
4. JsonSerializerOptions の明示的設定:
`PropertyNameCaseInsensitive = True` を設定することで、JSONの表記揺れに対する耐性を高めている。
結論:設計がバグを未然に防ぐ
シリアライゼーションは、単なる「データの変換」ではない。それは「アプリケーションの状態を、時間を超えて、環境を超えて保存する」という、極めて重要な責任を伴う行為だ。
「とりあえず動く」コードを書くのは誰にでもできる。しかし、数年後の保守担当者が、あるいは過酷な運用環境に置かれたユーザーが、そのツールを安心して使えるかどうかは、君が今、設計の細部にどれだけの知見を注ぎ込めるかにかかっている。
DTOを使い、非同期I/Oを徹底し、不完全な状態での書き込みを避ける。この基本を徹底せよ。それが、伝説的なアーキテクトへの第一歩だ。
