Критика неизменяемых классов с дизайном круговых ссылок и лучшими вариантами - PullRequest
6 голосов
/ 11 августа 2011

У меня есть фабричный класс, который создает объекты с круговыми ссылками. Я бы хотел, чтобы они тоже были неизменными (в некотором смысле этого слова). Поэтому я использую следующую технику, используя своего рода замыкание:

[<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 ]

9 голосов
/ 12 августа 2011

Вы можете использовать поддержку 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)
4 голосов
/ 12 августа 2011

Я думаю, что ответ Томаса - путь.Однако для полноты картины я покажу, как можно использовать рекурсивные записи для создания циклических неизменяемых объектов.Это может стать довольно уродливым, поэтому я скрыл неизменную реализацию записей за некоторыми более хорошими свойствами:

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
0 голосов
/ 12 августа 2011

Полагаю, что-то подобное тоже можно сделать:

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
...