使用无效值创建F#记录时引发异常

时间:2019-02-01 17:25:09

标签: f#

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

我正在完成弗拉基米尔·霍里科夫(Vladimir Khorikov)的Pluralsight课程“实践中的域驱动设计”。他举的例子是使用C#实现的,所以实践中,我试图实现他们在F#。

他有一类 “钱”,这在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类型的构造函数,因此我无法弄清楚将代码放在哪里以测试无效值并引发异常。

有人可以给我一些建议吗?谢谢。

4 个答案:

答案 0 :(得分:4)

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

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函数的主体中。

答案 1 :(得分: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 } 

答案 2 :(得分:0)

您可以做的一招是遮盖:

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}

答案 3 :(得分:0)

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

[<Struct>] 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实例,也无法构造具有无效Money的{​​{1}}实例,因此,任何存在的实例都必须是有效。

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