在F#中实现构建器模式(la System.Text.StringBuilder)

时间:2010-02-15 15:58:03

标签: f#

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

这是一个天真的实现:

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#中实现命令式模式时应该是这样。有没有更好的方法来做到这一点,再次,为了命令式语言保留通常的构建器界面?

3 个答案:

答案 0 :(得分:7)

我认为根据您的使用情况,您可能会更好地使用不可变的实现。以下示例 将静态强制执行任何构建器在构建之前将其where,order和group属性设置为恰好一次, 虽然它们可以按任何顺序设置:

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

显然你可能不得不针对你的特定约束调整它,并将它暴露给其他语言你可能想要使用另一个类的成员和委托而不是let-bound函数和F#函数类型,但你得到了图片。

<强>更新

也许值得扩展我已经完成的更多描述 - 代码有点密集。使用记录类型没有什么特别之处;一个普通的不可变类也一样好 - 代码会简洁一点,但与其他语言互操作可能会更好。我的实现基本上有两个重要的功能

  1. 每个添加方法都返回一个表示当前状态的新构建器。这非常简单,但它与Builder模式的正常实现方式明显不同。
  2. 通过使用其他泛型类型参数,您可以强制执行非平凡的不变量,例如在使用Builder之前要求将几个不同属性中的每个属性指定一次。对于某些应用程序来说这可能有点过头了,而且有点棘手。它只能使用不可变的构建器,因为我们可能需要在操作后返回带有不同类型参数的构建器。
  3. 在上面的示例中,类型系统允许这一系列操作:

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

    而这个不会,因为它永远不会设置顺序:

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

    正如我所说,这可能对你的应用程序来说太过分了,但这只是因为我们使用的是不可变的构建器。

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

答案 2 :(得分:1)

一种替代方法是只使用F#记录类型,默认值为None / empty:

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

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

这允许客户端代码使用“with”语法获取新副本:

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

如果您愿意,您可以使用“with”进一步复制“myVal”,从而“构建”更多属性,同时保持原始状态不变:

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