В некоторых сценариях невозможно создать неизменяемые графы объектов?

Я знаю, что неизменность не всегда является Святым Граалем. Однако, поскольку я изучаю Scala уже довольно давно, я всегда стараюсь сначала найти неизменное решение, особенно когда речь идет о чистых «объектах данных». В настоящее время я ищу метод создания неизменяемого графа объектов для данного сценария, но я не уверен, что это вообще возможно.

Я просто хочу создать график один раз, изменения после создания не нужны.

Представьте себе следующий сценарий:

  • Есть только один тип: Person.
  • Person objects can have two types of references:
    • there is a unidirected 1-n relationship between a Person and potential children (also of type Person).
    • Кроме того, жены имеют мужей и наоборот

Первая проблема заключается в том, что отношения между двумя супругами цикличны. Поскольку установка ссылок приводит к появлению новых объектов (из-за неизменности), в конечном итоге супруга A указывает на супругу B_old, а супруга B указывает на супругу A_old. Кто-то в другом сообщении сказал, что циклические ссылки и неизменность — это оксюморон. Я не думаю, что это всегда так, поскольку супруг A может создать супруга B в своем собственном конструкторе и передать this, но даже при использовании этого неудобного подхода последующее добавление ссылок на дочерние элементы снова изменит A и B. Наоборот, начиная с детей, а затем соединяя супругов, получается аналогичная ситуация.

На данный момент я думаю, что нет никакого способа сделать это. но, возможно, я ошибаюсь, и есть какие-то шаблоны или обходные пути, о которых я не знаю. Если нет, то является ли изменчивость единственным решением?


person fxlae    schedule 22.10.2015    source источник


Ответы (2)


Я могу представить несколько трюков, как вы можете создать неизменяемый цикл, включая, но не ограничиваясь:

  • частный изменяемый класс, который эффективно неизменен извне
  • отражение

Но то, что мне нравится больше всего (и это действительно Scala-способ), это тщательно смешанная ленивая оценка и параметры по имени:

object DeferredCycle extends App {

  class C(val name:String, _child: => C) {
    lazy val child = _child
    override def toString: String = name + "->" + child.name
  }

  val a:C = new C("A", b)
  val b:C = new C("B", a)

  println(a)
  println(b)
}

Отпечатки:

A->B
B->A
person Aivean    schedule 22.10.2015
comment
Спасибо, очень полезно. Только один вопрос: прямая ссылка, которую вы делаете при создании экземпляра a (ссылка на b), работает только потому, что b является свойством объекта DeferredCycle, верно? Так что это не сработало бы, если бы a и особенно b были просто локальными переменными внутри метода. Или я ошибаюсь в этом вопросе? - person fxlae; 24.10.2015
comment
@DaniW, вы правы, но вы можете использовать бесчисленные приемы, чтобы обойти это ограничение. Вы можете создать локальный объект внутри метода, вы можете определить эти классы как vars (и затем при желании переопределить как vals), вы можете создать их как элементы некоторой коллекции и т.д. - person Aivean; 25.10.2015
comment
@Aivean Интересно, как может выглядеть частно изменяемый класс? Есть ли что-то вроде изменяемого сеттера, который можно использовать только один раз, возможно, управляемый с помощью логического флага? редактировать: что-то вроде set(c: Child): Unit = { if(switch) this.child = c; switch = false } ? - person ceran; 17.03.2016
comment
@ceran, ну, ваш подход тоже жизнеспособен, но он ближе к проверке инварианта. Под приватно изменяемым классом я подразумевал буквально приватный сеттер, который доступен только из того места, где создаются экземпляры, например какая-то фабрика. Когда экземпляр покидает фабрику, его можно считать практически неизменным, поскольку сеттер является закрытым. - person Aivean; 18.03.2016

Чтобы добавить еще одну перспективу, вам не всегда нужно моделировать отношения как сдерживание. Вы можете добавить еще один уровень косвенности, например непрозрачный идентификатор.

case class PersonId(id: Int)
case class Person(id: PersonId, name: String, spouse: Option[PersonId], children: Seq[PersonId])

val people: Map[PersonId, Person] = ...

В качестве альтернативы, отношения даже не должны быть членами Person, они также могут поддерживаться извне:

case class PersonId(id: Int)
case class Person(id: PersonId, name: String)

val people: Map[PersonId, Person] = ...
val spouses: Map[PersonId, PersonId] = ...
val children: Map[PersonId, Seq[PersonId]] = ...
person JimN    schedule 23.10.2015
comment
Это был мой первый подход. Чего я не сказал в вопросе, так это того, что Person имеет несколько подтипов. Думаю, я потерял бы немного безопасности типов, если бы ссылки выполнялись только по идентификаторам. Я хотел бы проголосовать за ваш пост, но сначала мне нужно 15 кредитов :/ - person fxlae; 24.10.2015