变异状态是构建器模式的中心。是否有一种惯用的方法来实现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#中实现命令式模式时应该是这样。有没有更好的方法来做到这一点,再次,为了命令式语言保留通常的构建器界面?
答案 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#函数类型,但你得到了图片。
<强>更新强>
也许值得扩展我已经完成的更多描述 - 代码有点密集。使用记录类型没有什么特别之处;一个普通的不可变类也一样好 - 代码会简洁一点,但与其他语言互操作可能会更好。我的实现基本上有两个重要的功能
在上面的示例中,类型系统允许这一系列操作:
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] }