シリアライズの全体例
from DMMF: Serialization
LT;DR
ドメインは DTO を知らない ので、ドメインオブジェクト と DTO の相互変換処理は DTO のモジュール内で実装すべき
シリアライザが FP と親和性があるとは限らないので、その場合はラップして使用すると良い
シリアライズ は必ず成功するが、デシリアライズ は失敗する可能性がある
DTO は 境界づけられたコンテキスト間の契約 として機能するため、この契約を違反しないように複数バージョンの DTO を管理しなければならない場合もある
hr.icon
シリアライズの具体例
シリアライズ(JSON -> ドメインオブジェクト)、デシリアライズ(ドメインオブジェクト -> JSON)の具体例を見る
以下の Person を 永続化 したいとする
code:fsharp
module Domain =
open System
type Person =
{ First: String50
Last: String50
Birthdate: Birthdate }
String50 と Birthdate は制約を持つ単純型
各ステップを実装する前に、単純型を実装する
String50 や Birthdate は直接シリアライズできないので、すべてのフィールドが プリミティブ な DTO の型を作成する
code:fsharp
module Dto =
open System
type Person =
{ First: string
Last: string
Birthdate: DateTime }
ヘルパ関数 toDomain と fromDomain の実装
ドメインは DTO を知らないはず なので、Dto モジュール内にドメイン名のサブモジュール(Person)を作成して、そこに実装する
code:fsharp
module Dto =
module Person =
let fromDomain (person: Domain.Person): Dto.Person =
let first = person.First |> String50.value
let last = person.Last |> String50.value
let birthdate = person.Birthdate |> Birthdate.value
{ First = first; Last = last; Birthdate = birthdate }
let toDomain (dto: Dto.Person): Result<Domain.Person, string> =
result {
let! first = dto.First |> String50.create "First"
let! last = dto.Last |> String50.create "Last"
let! birthdate = dto.Birthdate |> Birthdate.create
return { First = first; Last = last; Birthdate = birthdate }
}
fromDomain は常に成功する
toDomain は制約などで失敗する可能性があるので戻り値の型は Result
エラーフローを処理するため、result コンピュテーション式 を利用している
コンピュテーション式の導入#66b06bdf75d04f0000b45b37
JSON シリアライザのラッピング
サードパーティ製の JSON や XML のシリアライザは、戻り値が Result でないなど、FP と親和性が無い場合がある
e.g. .NET JSON シリアライズライブラリ(Newtonsoft.Json)
この場合、以下のようにラップすれば良い
code:fsharp
module Json =
open Newtonsoft.Json
let serialize obj =
JsonConvert.SerializeObject obj
let deserialize<'a> str =
try
JsonConvert.DeserializeObject<'a> str
|> Result.Ok
with
| ex -> Result.Error ex
F# に特化したシリアライザとしては、FsPickler や Chiron がある
シリアライズパイプラインの全体
シリアライズパイプラインは、以下のように実装できる
code:fsharp
let jsonFromDomain (person: Domain.Person) =
person
|> Dto.Person.fromDomain
|> Json.serialize
デシリアライズパイプラインは、Json.deserialize と Dto.Person.toDomain が 異なるエラー型の Result を返すので、エラー型を合わせる必要がある
共通の 選択型 を作成し、Result.mapError で 型を持ち上げる
Result を生成する関数の連鎖(bind と errorMap、map)#66af6a3a75d04f00006fbafd
code:fsharp
type DtoError =
| ValidationError of string
| DeserialiozationException of exn
let jsonToDomain jsonString : Result<Domain.Person, DtoError> =
result {
let! deserializedValue =
jsonString
|> Json.deserialize
|> Result.mapError DeserialiozationException
let! domainValue =
deserializedValue
|> Dto.Person.toDomain
|> Result.mapError ValidationError
return domainValue
}
複数のエラーを返したい場合は、アプリカティブ を適用すると良い
https://fsharpforfunandprofit.com/posts/elevated-world-3/#validation-using-applicative-style
複数バージョンのシリアライズ型を扱う
時間の経過とともに設計は進化する
このとき、ドメインオブジェクト のフィールドの追加・削除、リネームなど変更が必要になる
これにより、DTO 型にも影響が及ぶ可能性がある
DTO は 境界づけられたコンテキスト間の契約 として機能する
ドメインイベント または DTO の定義を変更する場合、影響を受ける他のコンテキストの所有者と協議してから行う必要がある
この契約を違反しないように複数バージョンの DTO を管理しなければならない場合もある