レコード型や選択型のマップを使ったシリアライズ
from ドメイン型を DTO に変換する方法
LT;DR
レコード型 や 判別共用体 の DTO を IDictonary<string,obj> として表現する方法もある
JSON にシリアライズすることが決まっている場合など
これにより 境界づけられたコンテキスト間の契約 が無くなり、コンテキスト 同士が 疎結合 になる
一方、疎結合 過ぎて、「コンシューマ と プロデューサ の期待が一致していない」ことを検知するのが難しくなる
レコード型や選択型のマップを使ったシリアライズ#66b361f775d04f0000653a54
実装の 複雑さ が増す
これだけで radish-miyazaki.icon はこの選択肢は無くなった…
レコード型や選択型のマップを使ったシリアライズ#66b361f775d04f0000653a31
適度な結合が大事
hr.icon
レコード型 や 判別共用体 の別のシリアライズ方法として、すべてキーバリューマップとしてシリアライズする方法がある
JSON にシリアライズする場合はこれが適している
具体的には、DTO を IDictonary<string,obj> として表現する
https://learn.microsoft.com/ja-jp/dotnet/api/system.collections.generic.idictionary-2?view=net-8.0
メリット: 境界づけられたコンテキスト間の契約 が無く、コンテキスト 間のやり取りが 疎結合 に
契約が無く
IDictonary<string,obj> には任意の値を追加できるため
DTO は 境界づけられたコンテキスト 間の結合を 疎結合 にするため必要最低限の 契約 しか設けないが、それすらない状態に radish-miyazaki.icon
シリアライズコードとワークフローの連携#66ad8bbd75d04f0000938f69
デメリット: 契約がまったくないこと
「コンシューマ と プロデューサ の期待が一致していない」ことを検知するのが難しい
ドメイン型を DTO に変換する方法#66b35aaa75d04f00005942a8
適度な結合が大事
契約がまったくないとどうなるのかを見る
選択型を DTO に変換する と同じ題材を利用
再掲
code:domain.fs
type Name =
{ First: String50
Last: String50 }
type Example =
| A
| B of int
| C of string list
| D of Name
Name の シリアライズ
code:fsharp
let nameDtoFromDomain (name: Domain.Name) : IDictionary<string, obj> =
let first = name.First |> String50.value :> obj
let last = name.Last |> String50.value :> obj
("First", first); ("Last", last) |> dict
warning.icon レコード内のすべての値は、アップキャスト 演算子 :> を用いて obj にキャストする必要がある
これが「契約がまったくない」ことを指す
Example のシリアライズ
code:fsharp
let fromDomain (domainObj: Domain.Example) : IDictionary<string, obj> =
match domainObj with
| A -> ("A", null) |> dict
| B i ->
let bdata = Nullable i :> obj
("B", bdata) |> dict
| C strList ->
let cdata = strList |> List.toArray :> obj
("C", cdata) |> dict
| D name ->
let ddata = name |> nameDtoFromDomain :> obj
("D", ddata) |> dict
デシリアライズ はより複雑になる
それぞれのフィールドについて以下が必要
1. 値を調べて存在するかチェックする TryGetValue
https://learn.microsoft.com/ja-jp/dotnet/api/system.collections.generic.dictionary-2.trygetvalue?view=net-8.0
2. 1. で値があれば、正しい型にキャストする :?>(ダウンキャスト 演算子)
http://www.fsharpintro.net/object.html
はじめに、これを実現するヘルパ関数を実装する
code:fsharp
let getValue key (dict: IDictionary<string, obj>) : Result<'a, string> =
match dict.TryGetValue key with
| (true, value) -> // キーが見つかった
try
// 'a にダウンキャストして Ok にラップ
(value :?> 'a) |> Ok
with :? InvalidCastException ->
// キャストに失敗
let typeName = typeof<'a>.Name
let msg = sprintf "Value could not be cast to %s" typeName
Error msg
| (false, _) -> // キーが見つからなかった
let msg = sprintf "Key '%s' not found" key
Error msg
Name の デシリアライズ
code:fsharp
let nameDtoToDomain (nameDto: IDictionary<string, obj>) : Result<Name, string> =
result {
let! firstStr = nameDto |> getValue "First"
let! first = firstStr |> String50.create "First"
let! lastStr = nameDto |> getValue "Last"
let! last = lastStr |> String50.create "Last"
return { First = first; Last = last }
}
VSCode 上だと firstStr の型が string に推測されてる...! radish-miyazaki.icon
https://scrapbox.io/files/66b3604eb977a5001dd88977.png
first の式から再帰的に型が決まってそう ??
Example のデシリアライズ
各ケース毎にキーが存在するかどうかチェックする
code:fsharp
let toDomain (dto: IDictionary<string, obj>) : Result<Example, string> =
if dto.ContainsKey "A" then
Ok A
elif dto.ContainsKey "B" then
result {
let! bdata = dto |> getValue "B" // 失敗する可能性がある
return B bdata
}
elif dto.ContainsKey "C" then
result {
let! cdata = dto |> getValue "C" // 失敗する可能性がある
return cdata |> Array.toList |> C
}
elif dto.ContainsKey "D" then
result {
let! ddata = dto |> getValue "D" // 失敗する可能性がある
let! name = ddata |> nameDtoToDomain // ここも失敗する可能性がある
return name |> D
}
else
let msg = sprintf "Tag not found in dictionary"
Error msg