【问题标题】:Throwing exception when creating F# record with invalid values创建具有无效值的 F# 记录时引发异常
【发布时间】:2019-02-02 05:17:00
【问题描述】:

我是 F# 的新手,如果这是一个新手问题,请原谅我。

我正在学习 Vladimir Khorikov 的 Pluralsight 课程“领域驱动设计实践”。他的示例是使用 C# 实现的,因此为了练习,我尝试在 F# 中实现它们。

他有一个类“Money”,在 F# 中如下所示:

 type Money =
    {
        OneCentCount: int;
        TenCentCount: int;
        QuarterCount: int;
        OneDollarCount: int;
        FiveDollarCount: int;
        TwentyDollarCount: int;
    } 

我对此很好,并且可以在这个类上实现不同的操作,尽管其中一些变得有点笨拙,因为记录类型在 F# 中没有构造函数。 (即,虽然我想说

let money1 = Money(1,2,3,4,5,6)

这会引发一个错误,即 Money 没有构造函数。所以我必须这样做

let money1 = Money { 
    OneCentCount = 1; 
    TenCentCount = 2; 
    QuarterCount = 3; 
    OneDollarCount = 4; 
    FiveDollarCount = 5; 
    TwentyDollarCount = 6}

但是,现在他开始进行测试,如果您尝试创建任何值为负的 Money 记录,他想要一个引发 InvalidOp 异常的测试 - 这是一个合理的要求。

但是由于没有 Money 类型的构造函数,我无法弄清楚将代码放在哪里来测试无效值并抛出异常。

谁能给我一些建议?谢谢。

【问题讨论】:

  • 您认为此 Money 记录的 invalid 值是多少?
  • 正如我试图实现的测试要求中所述,无效值是其中任何值为负的值。例如,您不能有 -10 便士的 Money 金额。

标签: f#


【解决方案1】:

一种典型的方法是在定义为静态成员的类型上使用智能构造函数(简化类型的示例):

type Money =
    {
        OneCentCount: int;
        TenCentCount: int;
    }
    static member Create (oneCent, tenCent) =
        let throwOnNegative field v =
            if v < 0 then invalidOp (sprintf "Negative value for %s" field) else v
        {
            OneCentCount = oneCent |> throwOnNegative "OneCentCount"
            TenCentCount = tenCent |> throwOnNegative "TenCentCount"
        }

任何类型的验证逻辑都可以进入 Create 函数的主体。

【讨论】:

    【解决方案2】:

    现有的两个答案很好地说明了基本思想 - 您需要隐藏数据类型的一些内部结构并提供自定义操作来创建实现检查的值。

    在您的示例中可能有用的一件事是将检查与 Money 类型分开 - 按照现有答案中的方法,您必须对每个非常乏味的字段重复检查。或者,您可以定义一个类型 Count,它使用相同的隐藏技术只允许正值,然后根据 Count 定义您的记录:

    type Count = 
      private { Count : int }
      member x.Value = x.Count
    
    let Count n = 
      if n < 0 then invalidOp "Negative count!"
      else { Count = n }
    

    现在你可以只使用普通的记录了:

    type Money =
      { OneCentCount: Count
        TenCentCount: Count
        QuarterCount: Count
        OneDollarCount: Count
        FiveDollarCount: Count
        TwentyDollarCount: Count } 
    

    创建记录的值时,它作为普通记录工作,但您必须使用执行检查的Count 函数创建所有Count 值:

    let money = 
      { OneCentCount = Count 10
        TenCentCount = Count 10
        QuarterCount = Count -1
        OneDollarCount = Count 10
        FiveDollarCount = Count 10
        TwentyDollarCount = Count 10 } 
    

    【讨论】:

      【解决方案3】:

      你可以做的一个技巧是阴影:

      type Money =
          {
              OneCentCount: int;
              TenCentCount: int;
              QuarterCount: int;
              OneDollarCount: int;
              FiveDollarCount: int;
              TwentyDollarCount: int;
          } 
      
       let Money (a, b, c, d, e, f) =  
         { OneCentCount = a; 
          TenCentCount = b; 
          QuarterCount = c; 
          OneDollarCount = d; 
          FiveDollarCount = e; 
          TwentyDollarCount = f}
      

      然后在您的函数Money(a,b,c,d,e,f) 中,您可以放置​​适当的验证逻辑。例如,我们不能允许 OneCentCount 为负值:

      let Money (a, b, c, d, e, f) =  
          if a < 0 then
              raise(invalidArg("a is required to be a positive value"))
          else
              {  OneCentCount = a; 
                  TenCentCount = b; 
                  QuarterCount = c; 
                  OneDollarCount = d; 
                  FiveDollarCount = e; 
                  TwentyDollarCount = f}
      

      【讨论】:

        【解决方案4】:

        我通常在 F# 中为 DDD 采用的一种方法是为我想要应用的每个不同的业务规则组合创建一个类型:

        [&lt;Struct&gt;] type MonetaryUnitCount = private MonetaryUnitCount of int

        这些类型具有私有构造函数,因此我们可以控制它们的创建方式,只公开一个函数来创建每种类型:

        module MonetaryUnitCount =
            let create count =
                if count < 0
                then Error "Count must be positive"
                else Ok (MonetaryUnitCount count)
        

        然后,每个记录类型还将有一个私有构造函数和一个对应的 create 函数,该函数为每个字段调用正确的 create 函数,并在数据传输时验证数据:

        type Money =
            private {
                OneCentCount: MonetaryUnitCount 
                TenCentCount: MonetaryUnitCount 
                QuarterCount: MonetaryUnitCount 
                OneDollarCount: MonetaryUnitCount 
                FiveDollarCount: MonetaryUnitCount 
                TwentyDollarCount: MonetaryUnitCount 
            } 
        
        module Money =
            let create (a, b, c, d, e, f) =
                MonetaryUnitCount.create a
                |> Result.bind (fun m -> MonetaryUnitCount.create b |> Result.map (fun n -> m, n))
                |> Result.bind (fun (m, n) -> MonetaryUnitCount.create c |> Result.map (fun o -> m, n, o))
                |> Result.bind (fun (m, n, o) -> MonetaryUnitCount.create d |> Result.map (fun p -> m, n, o, p))
                |> Result.bind (fun (m, n, o, p) -> MonetaryUnitCount.create e |> Result.map (fun q -> m, n, o, p, q))
                |> Result.bind (fun (m, n, o, p, q) -> MonetaryUnitCount.create f |> Result.map (fun r -> m, n, o, p, q, r))
                |> Result.map (fun (m, n, o, p, q, r) -> 
                    {
                        OneCentCount = m
                        TenCentCount = n
                        QuarterCount = o
                        OneDollarCount = p
                        FiveDollarCount = q
                        TwentyDollarCount = r
                    })
        

        这样你要么得到一个成功的结果并填充你的Money,要么得到一个验证失败的错误。没有办法用负值构造MonetaryUnitCount的实例,也没有办法用无效的MonetaryUnitCount构造Money的实例,所以任何存在的实例都必须是有效的。

        通过使用计算表达式自动绑定Result 类型可以大大简化此语法,或者通过使用应用程序收集所有验证错误来增强此语法。

        【讨论】:

        • 这个语法可以通过不映射到增加长度的元组MonetaryUnitCount.create a |&gt; Result.bind (fun m -&gt; MonetaryUnitCount.create b |&gt; Result.bind (fun n -&gt; ...来大大简化,并且可能通过使用一元运算符let (&gt;&gt;=) a f = Result.bind f a来简化一点。
        猜你喜欢
        • 1970-01-01
        • 2019-03-11
        • 2016-07-14
        • 2013-11-11
        • 1970-01-01
        • 2015-11-18
        • 1970-01-01
        • 2021-08-16
        • 2012-02-20
        相关资源
        最近更新 更多