2010-02-15 18 views
4

变化状态位于构建器模式的中心。有没有一种惯用的方式来实现F#中这样一个类的内部实现,它将减少/消除可变状态,同时保留通常的接口(该类将主要用于其他.NET语言)?在F#中实现构建器模式(a System.Text.StringBuilder)

这里有一个天真的实现:

type QueryBuilder<'T>() =        //' 
    let where = ref None 
    let orderBy = ref None 
    let groupBy = ref None 
    member x.Where(cond) = 
     match !where with 
     | None -> where := Some(cond) 
     | _ -> invalidOp "Multiple WHERE clauses are not permitted" 
    // members OrderBy and GroupBy implemented similarly 

一个想法是创建一个记录类型来存储的内部结构,并使用复制和更新表达式。

type private QueryBuilderSpec<'T> =      //' 
    { Where : ('T -> bool) option;      //' 
     OrderBy : (('T -> obj) * bool) list;    //' 
     GroupBy : ('T -> obj) list }      //' 

type QueryBuilder<'T>() =        //' 
    let spec = ref None 
    member x.Where(cond) = 
     match !spec with 
     | None -> 
      spec := Some({ Where = Some(cond); OrderBy = []; GroupBy = [] }) 
     | Some({ Where = None; OrderBy = _; GroupBy = _} as s) -> 
      spec := Some({ s with Where = Some(cond) }) 
     | _ -> invalidOp "Multiple WHERE clauses are not permitted" 
    // members OrderBy and GroupBy implemented similarly 

这一切似乎有点笨重,也许应该努力实现F#势在必行模式时,可以预期的。有没有更好的方法来做到这一点,再次,为了命令式语言而保留常用的构建器界面?

+1

我并没有真正看到你的建设者之间有太大的区别。任何一个都可以从外部变化,而不管第一种还是第二种方式都没有实现。 –

+0

没错。我没有看到消除可变状态的任何方法。不同之处在于第一个实现可以包含任意数量的可变变量。第二个实现有一个。是的,也许这是一个微不足道的区别。我讨厌这两个,但这是我需要建立的。我更喜欢F#,但也许我需要回到C#来做到这一点。只是以为在放弃F#之前我会得到更多的意见。 – Daniel

回答

7

我认为这取决于在你的用例中,你可能会用一个不可变的实现更好。下面的例子 将静态执行,任何制造商都有,其中,顺序和正在修建前一次设置完成组属性, 虽然他们可以按任意顺序设置:

type QueryBuilder<'t,'w,'o,'g> = 
    internal { where : 'w; order : 'o; group : 'g } with 

let emptyBuilder = { where =(); order =(); group =() } 

let addGroup (g:'t -> obj) (q:QueryBuilder<'t,_,_,unit>) : QueryBuilder<'t,_,_,_> = 
    { where = q.where; order = q.order; group = g } 

let addOrder (o:'t -> obj * bool) (q:QueryBuilder<'t,_,unit,_>) : QueryBuilder<'t,_,_,_> = 
    { where = q.where; order = o; group = q.group } 

let addWhere (w:'t -> bool) (q:QueryBuilder<'t,unit,_,_>) : QueryBuilder<'t,_,_,_> = 
    { where = w; order = q.order; group = q.group } 

let build (q:QueryBuilder<'t,'t->bool,'t->obj,'t->obj*bool>) = 
    // build query from builder here, knowing that all components have been set 

显然,你可能需要调整这个针对您的特定约束,并将其展示给其他语言,您可能希望使用其他类上的成员和委托,而不是让出界限的函数和F#函数类型,但您可以获得该图片。

UPDATE

也许是值得推广的是什么我多一点的描述做了 - 这个代码是有点密。使用记录类型没有什么特别之处;一个正常的不可变类会一样好 - 代码会稍微简洁一些,但与其他语言交互可能会更好。我的实现基本上有两个重要特性:

  1. 每个用于添加的方法都会返回一个代表当前状态的新构建器。这很简单,虽然它与Builder模式通常实现的方式明显不同。
  2. 通过使用其他泛型类型参数,您可以实施非平凡的不变量,例如在使用Builder之前要求指定几个不同的属性中的每一个。这对于某些应用程序来说可能是过度的,并且有点棘手。它只能用一个不可变的生成器,因为我们可能需要在操作后返回一个具有不同的类型参数的生成器。

在上面的例子中,这样的操作顺序将由型系统被允许:

let query = 
    emtpyBuilder 
    |> addGroup ... 
    |> addOrder ... 
    |> addWhere ... 
    |> build 

而这一个不会,因为它从未设置顺序为:

let query = 
    emptyBuilder 
    |> addGroup ... 
    |> addWhere ... 
    |> build 

正如我所说的,这可能是对你的应用程序的矫枉过正,但它是唯一可能的,因为我们正在使用不可变的构建器。

+0

谢谢你让我感到蠢。 :)而不是每个成员返回“这个”,它应该返回一个新的不变的QueryBuilder。当然!谢谢。这正是我正在寻找的。 – Daniel

+0

顺便说一句 - 我不完全理解你的代码,但我认为我可以放弃记录类型并使类类型不可变(从每个“构建器”方法返回一个新实例),这将很有用。 – Daniel

+0

@丹尼尔 - 查看我的更新,以进一步解释我试图展示的内容。你完全正确,这与非记录不可变类型一样好。 – kvb

2

从内部消除可变性看起来并不像它对我有多大的意义......你可以通过设计使它变得可变 - 在这一点上的任何技巧都不会真正改变任何东西。

至于简洁 - let mutable可能是因为它得到好(这样你就不需要使用!取消引用):

type QueryBuilder<'T>() = 
    let mutable where = None 
    let mutable orderBy = None 
    let mutable groupBy = None 
    member x.Where(cond) = 
     match where with 
     | None -> where <- Some(cond) 
     | _ -> invalidOp "Multiple WHERE clauses are not permitted" 
    // members OrderBy and GroupBy implemented similarly 
+1

使用记录以及复制和更新表达式,或其他功能技术与可变变量(减少可变变量的数量除外)有什么好处? – Daniel

+0

我在这里看不到。 –

1

一个备选是只使用F#记录类型,与这里的一切是默认值无/空:

type QueryBuilderSpec<'T> = 
    { Where : ('T -> bool) option; 
     OrderBy : (('T -> obj) * bool) list; 
     GroupBy : ('T -> obj) list } 

let Default = { Where = None; OrderBy = None; GroupBy = [] } 

这允许客户端代码,以使用“与”语法新副本:

let myVal = { Default with Where = fun _ -> true } 

然后,您可以使用“与”做“设为myVal”的更多副本,如果你愿意的话,因而“打造”了更多的属性,同时保持原有不变:

let myVal' = { myVal with GroupBy = [fun x -> x.Whatever] } 
+0

这正是我要做的,如果这将主要用于F#。不幸的是,它需要看起来像一个典型的.NET类型。虽然,这是我用于内部维护状态的方法。 – Daniel

+0

在这种情况下,你可以添加方法到你的记录类型来创建它的新副本?语法是“... with x.AddWhere whereFunc = ... // create copy”。这些看起来像其他.NET语言的普通.NET方法,记录的行为类似于字符串类。 – Robert

+0

如果我理解你,这是我在OP中的第二次实现中所做的。我非常喜欢kvb的想法,让builder类不可变。然后每个方法都可以返回一个传入更新状态的新实例。 – Daniel