使用循环引用设计的不可变类的批判,以及更好的选项

时间:2011-08-11 19:54:16

标签: f# closures class-design immutability circular-dependency

我有一个工厂类,用于创建带有循环引用的对象。我希望它们也是不变的(在某种意义上说)。所以我使用以下技术,使用一种类型的闭包:

[<AbstractClass>]
type Parent() =
  abstract Children : seq<Child>
and Child(parent) =
  member __.Parent = parent

module Factory =

  let makeParent() =
    let children = ResizeArray()
    let parent = 
      { new Parent() with
        member __.Children = Seq.readonly children }
    [Child(parent); Child(parent); Child(parent)] |> children.AddRange
    parent

我比internal AddChild方法更喜欢这个,因为它有更强的不变性保证。也许它是神经质的,但我更喜欢关闭访问控制。

这个设计有什么陷阱吗?有没有更好的,也许不那么繁琐的方法来做到这一点?

3 个答案:

答案 0 :(得分:9)

即使在创建抽象类的实例时,也可以使用F#的支持进行递归初始化:

let makeParent() =
  let rec children = seq [ Child(parent); Child(parent); Child(parent) ]
  and parent = 
    { new Parent() with
      member __.Children = children }
  parent

编译代码时,F#使用延迟值,因此值children变为惰性值,属性Children访问此延迟计算的值。这很好,因为它可以首先创建Parent的实例(引用惰性值)然后实际构造序列。

对记录做同样的事情不会很好,因为没有计算会被延迟,但是这里工作得很好,因为在创建Parent时实际上并没有访问序列(如果它是一个记录,这将是一个必须评估的领域。)

F#编译器无法判断(通常)这是否正确,因此它会发出警告,可以使用#nowarn "40"禁用。

一般来说,我认为使用let rec .. and ..来初始化递归值是一件好事 - 它有点受限(其中一个引用必须被延迟),但它会强制你保持递归引用被隔离并且我认为,它可以使您的代码更简单。

编辑要在可能出错的情况下添加示例 - 如果Child的构造函数尝试访问其父级的Children集合,则会强制评估在创建它之前的延迟值并且您得到运行时错误(这是警告所说的)。尝试将其添加到Child

的构造函数中
do printfn "%d" (Seq.length parent.Children)

答案 1 :(得分:4)

我认为托马斯的答案是要走的路。但是,为了完整性,我将展示如何使用递归记录来创建循环不可变对象。这可能会非常难看,所以我将不可变记录实现隐藏在一些更好的属性背后:

type Parent = internal { children : Children option }
and internal Children = { first : Child; rest : Children option }
and Child = internal { parent : Parent }

let rec internal listToChildren = function
| [] -> None
| c::cs -> Some { first = c; rest = listToChildren cs }

let rec internal childrenToList = function
| None -> []
| Some { first = c; rest = cs } -> c::(childrenToList cs)

module Factory =
    let makeParent() = 
        let rec parent = { children = children }
        and child1 = { parent = parent }
        and child2 = { parent = parent }
        and child3 = { parent = parent }
        and children = [child1; child2; child3] |> listToChildren
        parent

type Parent with
    member p.Children = childrenToList p.children
type Child with
    member c.Parent = c.parent

答案 2 :(得分:0)

我想这样的事情也可以做到:

type ParentFactory private (n) as X = 
    inherit Parent()
    let childs = [for i=1 to n do yield Child(X :> Parent)]
    override X.Children = childs |> List.toSeq;               
    static member Create n = (new ParentFactory(n)) :> Parent