2011-01-29 10 views
5

Las clases inmutables son geniales, pero hay un gran problema que no puedo pensar en una forma sensata de resolver: los ciclos.Cómo modelar ciclos entre instancias de clases inmutables?

class Friend { 
    Set<Friend> friends(); 
} 

¿Cómo es que uno de los modelos Yo te tiene a ti como amigo y que a su vez me tiene como amigo?

INMUTABILIDAD Esta clase del mundo exterior definitivamente debe ser inmutable. El valor mantenido internamente debe ser constante a los efectos de la verificación de la igualdad.

+0

¿Conoce de antemano todas las relaciones bidireccionales que necesitará, o las está agregando de a una por vez? En este último caso, realmente no hay forma de obtener la garantía de inmutabilidad, ya que realmente está cambiando los objetos. – templatetypedef

+1

En este caso, no, estoy tratando de mantener esto simple. –

Respuesta

8

[[[Editar: Añadido código para demostrar el concepto totalmente inmutable]]]

Es por eso que los constructores son tan agradable para inmutables - permiten la mutabilidad durante la construcción para tener todo listo antes de "congelarlo". En este caso, supongo que necesita un generador de amigos que admita la creación de ciclos.

final FriendBuilder john = new FriendBuilder().setName("john"); 
final FriendBuilder mary = new FriendBuilder().setName("mary"); 
final FriendBuilder susan = new FriendBuilder().setName("susan"); 
john 
    .likes(mary) 
    .likes(susan); 
mary 
    .likes(susan) 
    .likes(john); 
susan 
    .likes(john); 

// okay lets build the immutable Friends 
Map<Friend> friends = FriendsBuilder.createCircleOfFriends(john, mary, susan); 
Friend immutableJohn = friends.get("john"); 

Editar: Añadido inmutable ejemplo siguiente para demostrar enfoque:

  • Hubo cierto debate en los comentarios acerca de si una versión inmutable era posible.

  • Los campos son definitivos e inmutables. Se usa un conjunto modificable en el constructor, pero solo se conserva la referencia no modificable después de la construcción.

  • Tengo otra versión que usa Guava ImmutableSet para un conjunto verdaderamente inmutable en lugar del envoltorio no modificable de JDK. Funciona de la misma manera, pero usa el buen constructor de conjuntos de Guava.

Código:

import java.util.Collections; 
import java.util.HashMap; 
import java.util.HashSet; 
import java.util.IdentityHashMap; 
import java.util.Map; 
import java.util.Set; 

/** 
* Note: potentially cycle graph - be careful of deep equals/hashCode/toString/etc. 
* Immutable 
*/ 
public class Friend { 

    public static class Builder { 

     private final String name; 
     private final Set<Builder> friends = 
      new HashSet<Builder>(); 

     Builder(final String name) { 
      this.name = name; 
     } 

     public String getName() { 
      return name; 
     } 

     public Set<Builder> getFriends() { 
      return friends; 
     } 

     void likes(final Builder... newFriends) { 
      for (final Builder newFriend : newFriends) 
      friends.add(newFriend); 
     } 

     public Map<String, Friend> createCircleOfFriends() { 
      final IdentityHashMap<Builder, Friend> existing = 
       new IdentityHashMap<Builder, Friend>(); 

      // Creating one friend creates the graph 
      new Friend(this, existing); 
      // after the call existingNodes contains all the nodes in the graph 

      // Create map of the all nodes 
      final Map<String, Friend> map = 
       new HashMap<String, Friend>(existing.size(), 1f); 
      for (final Friend current : existing.values()) { 
       map.put(current.getName(), current); 
      } 

      return map; 
     } 
    } 

    final String name; 
    final Set<Friend> friends; 

    private Friend(
      final Builder builder, 
      final Map<Builder, Friend> existingNodes) { 
     this.name = builder.getName(); 

     existingNodes.put(builder, this); 

     final IdentityHashMap<Friend, Friend> friends = 
      new IdentityHashMap<Friend, Friend>(); 
     for (final Builder current : builder.getFriends()) { 
      Friend immutableCurrent = existingNodes.get(current); 
      if (immutableCurrent == null) { 
       immutableCurrent = 
        new Friend(current, existingNodes); 
      } 
      friends.put(immutableCurrent, immutableCurrent); 
     } 

     this.friends = Collections.unmodifiableSet(friends.keySet()); 
    } 

    public String getName() { 
     return name; 
    } 

    public Set<Friend> getFriends() { 
     return friends; 
    } 


    /** Create string - prints links, but does not traverse them */ 
    @Override 
    public String toString() { 
     final StringBuffer sb = new StringBuffer(); 
     sb.append("Friend ").append(System.identityHashCode(this)).append(" {\n"); 
     sb.append(" name = ").append(getName()).append("\n"); 
     sb.append(" links = {").append("\n"); 
     for (final Friend friend : getFriends()) { 
      sb 
      .append("  ") 
      .append(friend.getName()) 
      .append(" (") 
      .append(System.identityHashCode(friend)) 
      .append(")\n"); 
     } 
     sb.append(" }\n"); 
     sb.append("}"); 
     return sb.toString(); 
    } 

    public static void main(final String[] args) { 
     final Friend.Builder john = new Friend.Builder("john"); 
     final Friend.Builder mary = new Friend.Builder("mary"); 
     final Friend.Builder susan = new Friend.Builder("susan"); 
     john 
      .likes(mary, susan); 
     mary 
      .likes(susan, john); 
     susan 
      .likes(john); 

     // okay lets build the immutable Friends 
     final Map<String, Friend> friends = john.createCircleOfFriends(); 

     for(final Friend friend : friends.values()) { 
      System.out.println(friend); 
     } 

     final Friend immutableJohn = friends.get("john"); 
    } 
} 

Salida:

Node 11423854 { 
    value = john 
    links = { 
    susan (19537476) 
    mary (2704014) 
    } 
} 
Node 2704014 { 
    value = mary 
    links = { 
    susan (19537476) 
    john (11423854) 
    } 
} 
Node 19537476 { 
    value = susan 
    links = { 
    john (11423854) 
    } 
} 
+0

Sería útil tener en cuenta que el patrón del generador simplemente oculta el hecho de que está realizando una inicialización posterior a la construcción. La clase Friend no podría tener una estructura final para tener amigos. –

+2

@Konstantin Komissarchik Puede hacer esto con una cantidad de amigos en la pila (el diámetro del gráfico en el mejor de los casos, todos en el peor), y aún así mantener la inmutabilidad. –

+0

@Tom No lo compro. Friend debería tener algo no final para eventualmente terminar con dos objetos Friend que se refieran entre sí. Una referencia final a un proxy (también conocido como generador) que internamente no es definitiva no cuenta. Esa es solo otra forma de hacer una inicialización retrasada. –

-1

La inmutabilidad no necesita ser compilada para ser válida architecturaly. Puede tener un objeto inmutable legítimo que tome los parámetros de inicialización posteriores a la construcción. Por ejemplo ...

private Object something; 

public void init(final Object something) 
{ 
    if(this.something != null) 
    { 
     throw new IllegalStateException(); 
    } 

    this.something = something 
} 

El campo de miembro "algo" no es definitivo, pero tampoco se puede configurar más de una vez.

Una variante más compleja sobre la base de la discusión en los comentarios ...

private boolean initialized; 
private Object a; 
private Object b; 

public void init(final Object a, final Object b) 
{ 
    if(this.initialized) 
    { 
     throw new IllegalStateException(); 
    } 

    this.initialized = true; 
    this.a = a; 
    this.b = b; 
} 

public Object getA() 
{ 
    assertInitialized(); 
    return this.a; 
} 

public Object getB() 
{ 
    assertInitialized(); 
    return this.b; 
} 

private void assertInitialized() 
{ 
    if(this.initialized) 
    { 
     throw new IllegalStateException("not initialized"); 
    } 
} 
+0

Creo que quisiste decir: if (something == null) – Mnementh

+0

En realidad, quise decir "if (this.something! = Null)". La instrucción if está allí para atrapar el intento de configurar algo más de una vez. –

+0

Ah, ya veo. Gracias por aclarar esto. – Mnementh

0

La forma correcta de modelar es un ciclo con un Graph. Y un solo comentario de línea de código fuente puede ser suficiente para forzar la inmutabilidad: "can't touch this".

¿Qué tipo de aplicación inmutable estás buscando? ¿Desea que aparezca un velociraptor whenever you modify the inmutable Set? La diferencia entre mutable y inmutable es solo una convención. Sin embargo, los bits en la RAM se pueden modificar fácilmente y con el Reflection API puede romper cualquier encapsulación y convención de ocultación de datos.

Ignorando el velociraptor por un momento, Java no admite un tipo inmutable. Como solución alternativa, debe modelar un tipo de datos que se comporte como uno.

Y para la propiedad inmutable a tener sentido que necesita para hacer un Friendinterface, que tiene una clase de aplicación: InmutableFriend, y la construcción del objeto debería suceder totalmente dentro del constructor.

Luego, dado que el gráfico contiene ciclos, antes de crear las instancias inmutables finales, necesita almacenar los nodos del gráfico en alguna estructura temporal mutable. También debe devolver un unmodifiableSet en el método InmutableFriend.friends().

Finalmente, para clonar el gráfico necesita implementar un algoritmo Deep-copy como Breadth-first search en el gráfico Mutable. Sin embargo, una pregunta es qué sucede cuando el gráfico no es fully connected.

interface Friend { 
    public Set<Friend> friends(); 
} 

class MutableFriend { 
    private Set<MutableFriend> relations = new HashSet<MutableFriend>(); 

    void connect(MutableFriend otherFiend) { 
     if (!relations.contains(otherFriend)) { 
      relations.add(otherFiend); 
      otherFriend.connect(this); 
     } 
    } 

    Friend freeze() { 
     Map<MutableFriend, InmutableFriend> table = ...; 

     /* 
     * FIXME: Implement a Breadth-first search to clone the graph, 
     * using this node as the starting point. 
     * 
     * TODO: If the graph is not connected this won't work. 
     * 
     */ 
    } 
} 

class InmutableFriend() implements Friend { 
    private Set<Friend> connections; 

    public Set<Friend> friends() { 
     return connections; 
    } 

    public InmutableFriend(Set<Friend> connections) { 
     // Can't touch this. 
     this.connections = Collections.unmodifiableSet(connections); 
    } 
} 
+0

El patrón del congelador es realmente feo, estoy empezando a pensar que Friend se verá inmutable, pero en el interior del constructor se agregará resistencia y luego se congelará, en ese momento se lo puede dar al mundo exterior ... –

+0

@mP: El congelador no es un patrón, es un algoritmo llamado Deep Copy: http://en.wikipedia.org/wiki/Object_copy#Deep_copy – vz0

+0

lo siento compañero y leí mal MutableFrield = Campo :) ignora mi comentario original. –

Cuestiones relacionadas