/note/tech

バリデーションとパースの分離。Goで実装する「変更に強い」CSV 処理の設計

要約:

■ 1. CSV処理における課題

  • 業務アプリケーション開発においてCSVインポート機能は避けて通れない
  • 仕様が複雑になるにつれて以下の課題に直面する:
    • バリデーションとパース処理が混在しエラーの発生箇所が追いづらい
    • Shift_JISなど多様なエンコーディングへの対応でビジネスロジックが複雑になる
    • パースやバリデーションエラーを即座にリターンするとユーザーは「モグラ叩き」のような修正サイクルを繰り返すことになる
    • データが正しい状態か保証されないまま後続の処理に渡されてしまう

■ 2. Parse don't validateの考え方

  • 「Parse don't validate」という考え方を参考にCSV処理のライブラリを実装した
  • これは入力を単に検証して終わるのではなく型のあるデータ構造に変換することでその後の安全性を型システムで保証するという考え方である
  • 処理を以下の3つのフェーズに分割した:
    • Reader:エンコーディングの詳細を隠蔽
    • Parser:CSV行→構造体への変換
    • Validator:中間型→検証済みの型への昇格

■ 3. Reader層の実装

  • 日本の業務システムではExcelから出力したCSVが頻繁に使われShift_JISやBOM付きの場合がある
  • ビジネスロジックの段階ではエンコーディングの違いを意識せずに済むようにしたい
  • 読み込み時にエンコーディングを自動判定・変換し常にUTF-8のReaderを返すように実装した
  • エンコーディング処理の順序:
    • BOM付きUTF-8のCSVファイルをサポート
    • BOMなしUTF-8をサポート
    • Shift_JISをUTF-8へ変換してサポート
  • この層が腐敗防止層として機能することで後続の処理はファイルがどのような形式で保存されていたかを知る必要がなくなる

■ 4. Parser層の実装と中間型の定義

  • 読み込んだ行をGoの構造体にマッピングする
  • ここでは型変換とマッピングのみを行いビジネス的なバリデーションは行わない
  • 中間型Parsed[T]の定義:
    • ColumnCountStatus:カラム数の過不足状態を表す
    • ParsedRecord[T]:1行ごとのパース結果
    • Parsed[T]:パース処理全体の結果
  • カラム数が合わないといった構造的な問題も即座にエラーにするのではなくステータスとして記録する
  • これはエラーの集約を実現するためである
  • パース段階で見つかった不備で即座にエラーを返すとユーザーは修正とアップロードを何度も繰り返すことになる
  • Parserは起きたことの記録に徹し後続のValidatorで他の入力ミスと合わせてファイル内の全エラーをまとめて報告できるようにしてユーザー体験を損なわない設計としている

■ 5. Validator層の設計思想

  • ライブラリには汎用的な検証フローだけを持たせ具体的なビジネスルールは外部から注入する形をとった
  • 型定義:
    • Valid[T]:バリデーション後の安全なデータ
    • ValidateRecordFunc[T]:外部から注入するバリデーション関数の型定義
  • Validate関数の処理フロー:
    • ヘッダー構造の検証
    • レコードごとの検証で注入された関数を実行
    • エラーがなければ有効なレコードとしてリストに追加
    • エラーがあればValid[T]は返さない
    • 全て合格して初めてValid[T]を返す
  • Parsed[T]からValid[T]への変換が成功すればデータは整合性が取れていることが保証される

■ 6. 利用例の実装

  • このライブラリを使う側のコードは非常に宣言的になる
  • 利用手順:
    • 取り込みたいCSVの構造を定義する
    • パースを実行して読み込みと構造化を行う
    • バリデーションルールの定義でドメイン固有のロジックを記述する
    • バリデーション実行でルールの注入を行う
    • エラーがあれば何行目の何がおかしいかをまとめて返却できる
    • 後続処理に来る時点でvalidDataは*Valid[UserCSV]型でありすべてデータがルールに適合していることが保証されている

■ 7. 設計のメリット

  • CSV処理は外部からの入力を扱うため複雑になりがちだが責務を分けることでメンテナビリティを向上させることができた
  • Reader層の利点:
    • エンコーディングの詳細を隠蔽し腐敗防止層として機能
  • Parser層の利点:
    • CSV行を中間型へ変換しエラーは即時リターンせず状態として記録してUX向上に貢献
  • Validator層の利点:
    • 中間型を検証済みの型へ昇格し汎用的な検証フローを提供して具体的なルールは外部から注入する
  • バリデーションロジックを関数として注入する設計にしたことでライブラリ自体を変更することなく様々なCSVに柔軟に対応できる構成となった