我目前正在开发领域驱动设计项目的方法存储库。
从存储库获取 DTO 的方法返回以下类型:Async<Result<DTO.Worker option, exn>>
,然后运行以下代码将该 DTO 转换为 Domain.Worker
:
methodResult
|> Async.map ((Result.map (Option.map DTO.Worker.toDomain)))
Domain.Worker
记录类型有一个可区分的联合作为其值之一,我们不能直接在数据库中使用它们,因此在域模型之外有一个额外的步骤,将可区分的联合映射到数字,以便我们可以使用将其保存到数据库中.
module Domain
type WorkerStatus =
| Status1
| Status2
| Status3
type Worker = {
...
Status : WorkerStatus
}
问题在于 DTO.Worker.toDomain
我使用模式匹配将这些数字映射回可识别联合的方法:
module DTO
type Worker = {...}
with
static member toDomain (dbo : Worker) : Domain.Worker = {
...
Domain.Worker.Status =
match dbo.Status with
| 1 -> Domain.WorkerStatus.Status1
| 2 -> Domain.WorkerStatus.Status2
| 3 -> Domain.WorkerStatus.Status3
| _ -> raise (exn "data in the database has the wrong format")
}
在我们启动项目时,我可以避免引发异常,但现在我正在清理代码,我也需要将此方法包装在结果中,这引发了两个问题:
- 如何按照 F# 标准将代码包装在结果中?我仍在学习 F#,所以我能想到的唯一方法是在
toDomain
的开头创建一个可变的 bool 变量方法,如果我们在模式匹配中遇到任何错误,则更改它,然后返回转换为域模型的 DTO 或基于该 bool 值的错误。我还尝试用try...with...
包围该方法阻止并离开raise exn
,但我的同事也说这不是一个好的解决方案。 - 如何展平新的嵌套结果类型?如果我们更改代码,则
toDomain
方法返回 Result 而不是 Domain 对象,我们最终会得到Async<Result<Result<Domain.Worker, exn> option, exn>>
类型,这并不像我们期望的那样有用Async<Result<Domain.Worker option, exn>>
代码中的其他地方。
最佳答案
短期建议
我认为您已经将自己陷入了这些类型签名的困境中,因此没有简单的解决方案,但这是我能想到的侵入性最小的方法,这似乎符合您的要求。
首先修改toDomain
所以它返回 Result
而不是抛出异常:
static member toDomain (dbo : Worker) =
let statusResult =
match dbo.Status with
| 1 -> Ok Domain.WorkerStatus.Status1
| 2 -> Ok Domain.WorkerStatus.Status2
| 3 -> Ok Domain.WorkerStatus.Status3
| _ -> Error (exn "data in the database has the wrong format")
statusResult
|> Result.map (fun status ->
{
//...
Domain.Worker.Status = status
})
现在,正如您所提到的,问题是您有一个 Result<Option<Result<'a, exn>>, exn>
当您想要Result<Option<'a>, exn>
时,所以我们需要一个转换函数:
let convert = function
| Ok (Some (Ok x)) -> Ok (Some x)
| Ok (Some (Error exn)) -> Error exn
| Ok None -> Ok None
| Error exn -> Error exn
当您将 DTO 转换为域值时,您需要:
methodResult
|> Async.map ((Result.map (Option.map DTO.Worker.toDomain)) >> convert)
我并不是说这是一个好主意,但它至少应该让你暂时解开这个结。
长期建议
我认为混合是不明智的Result
和Option
按照您现有的方式键入。如果我正确理解您的设计,则值为 Ok None
当前表示数据访问层执行成功,但没有找到请求的对象。我明白为什么你最终会采用这种设计,但我认为这比它的值(value)更麻烦。相反,我建议每个操作都返回类似 Result<'a, Error>
的内容。相反,Error
类型可以是:
type Error = Exn of exn | NotFound
例如,Ok None
然后将变成Error NotFound
反而。一旦进行了更改,您就会停留在单个 Result monad 内,并且 F# 可以让您的生活变得更加轻松。例如,您可以使用 computation expression像这样:
let getDomainResult queryParams =
result {
let! dto = getDto queryParams
return! convertToDomain dto
}
(注意:为了简单起见,我在这里忽略了 Async
,但是 AsyncResult
可能更适合您。无论哪种情况,只要远离同一工作流程中的 Option
类型即可。您无能为力使用 Option
无法使用 Result
完成。)
关于database - F# 如何展平与其他类型混合的嵌套结果类型,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/74033474/