TL;DR

内容

第11章 Serializationの一部である。

シリアライズの具体例

ドメインオブジェクトから JSON へのシリアライズと、JSON からドメインオブジェクトへのデシリアライズの具体例を見る。次の Person を永続化したいとする。

module Domain =
    open System
    type Person =
        { First: String50
          Last: String50
          Birthdate: Birthdate }

String50 と Birthdate は制約を持つ単純型である(各ステップを実装する前に、単純型を実装する)。これらは直接シリアライズできないので、すべてのフィールドがプリミティブ型の DTO を作る。

module Dto =
    open System
 
    type Person =
        { First: string
          Last: string
          Birthdate: DateTime }

次に、相互変換のヘルパ関数 toDomain と fromDomain を実装する。ドメインは DTO を知らないはずなので、Dto モジュールの中にドメイン名のサブモジュール(Person)を作り、そこに置く。

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 コンピュテーション式を使っている(コンピュテーション式の導入)。

JSON シリアライザのラップ

サードパーティ製の JSON や XML のシリアライザは、戻り値が Result でないなど、関数型プログラミングと相性が悪いことがある。.NET の JSON ライブラリ Newtonsoft.Json もその 1 つである。この場合は、次のようにラップすればよい。

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 がある。

パイプライン全体

シリアライズのパイプラインは次のように書ける。

let jsonFromDomain (person: Domain.Person) =
    person
    |> Dto.Person.fromDomain
    |> Json.serialize

デシリアライズのパイプラインでは、Json.deserialize と Dto.Person.toDomain が異なるエラー型の Result を返す。そこで、共通の選択型を作り、Result.mapError で型を持ち上げる(Result を生成する関数の連鎖(bind と errorMap、map))。

type DtoError =
    | ValidationError of string
    | DeserializationException of exn
 
let jsonToDomain jsonString : Result<Domain.Person, DtoError> =
    result {
        let! deserializedValue =
            jsonString
            |> Json.deserialize
            |> Result.mapError DeserializationException
 
        let! domainValue =
            deserializedValue
            |> Dto.Person.toDomain
            |> Result.mapError ValidationError
 
        return domainValue
    }

複数のエラーをまとめて返したい場合は、アプリカティブを使うとよい(Validation using applicative style)。

複数バージョンのシリアライズ型を扱う

設計は時間とともに変わる。ドメインオブジェクトにフィールドを追加・削除したり、名前を変えたりすると、DTO の型にも影響が及ぶことがある。DTO は境界づけられたコンテキスト間の契約として機能する。本書は次のように述べている。

ドメインイベントまたは DTO の定義を変更する場合、影響を受ける他のコンテキストの所有者と協議してから行う必要がある

DTO が契約として機能するので、この契約を破らないように、複数バージョンの DTO を管理しなければならない場合もある。

キーワード