2011-01-29 10 views
5

不変のクラスは素晴らしいですが、サイクルを解決するための賢明な方法を考える大きな問題が1つあります。不変クラスインスタンス間のサイクルをモデル化する方法は?

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

どのように私は、順番に戻って友達として私を持っている友人としてあなたを一つのモデル持っていますか?

IMMUTABILITY 外部からのこのクラスは間違いなく不変である必要があります。内部的に保持される値は、等価チェックのために一定でなければなりません。

+0

事前に必要な双方向関係をすべて知っていますか、一度に1つずつ追加していますか?後者の場合、実際にオブジェクトを変更しているので、不変性の保証を得る方法は実際にはありません。 – templatetypedef

+1

この場合 - いいえ,,これを単純にしています。 –

答えて

8

[[[編集:完全に不変の概念を実証するためのコードを追加しました]]]

ビルダーはimmutablesのためにとてもいいです理由です - 彼らはあなたがそれを "凍結"する前にすべての設定を取得するために建設中の変更を許可します。この場合、サイクルの作成をサポートするFriendビルダーが必要です。

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"); 

編集:以下のアプローチを実証する不変の例を追加しました:

  • 不変バージョンが可能であったかどうかについてのコメントでいくつかの議論がありました。

  • フィールドは最終的なものであり、不変です。コンストラクタでは変更可能なセットが使用されますが、変更後の参照のみがコンストラクタの後に保持されます。

  • は、私が本当に不変のセットではなく、JDKの変更不可能なラッパーにグアバのImmutableSetを使用して別のバージョンがあります。それは同じように動作しますが、Guavaの素晴らしいセットビルダーを使用します。

コード:

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"); 
    } 
} 

出力:

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

ビルダーパターンは、構築後の初期化を行っているという事実を隠すだけです。 Friendクラスは友人を抱くための完全な最終構造を持つことはできませんでした。 –

+2

@ Constantin Komissarchikスタック上の多くの友達(最良の場合はグラフの直径、最悪の場合はすべて)でこれを行うことができ、それでも不変のままです。 –

+0

@Tom私はそれを買わない。最終的にお互いを参照する2つのFriendオブジェクトに終わるために、友人は最終的ではないものを持っていなければなりません。最終的に内部ではないプロキシ(別名ビルダー)への最終参照はカウントされません。これは遅延初期化を行う別の方法です。 –

-1

不変性は、コンパイラ、強制有効architecturalyことである必要はありません。構築後の初期化パラメータを取る正当な不変オブジェクトを持つことができます。例えば···

private Object something; 

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

    this.something = something 
} 

メンバーフィールド "something"は最終的ではありませんが、複数回設定することはできません。

コメントでの議論をもとに、より複雑な変...

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

私はあなたが意味すると思う:if(something == null) – Mnementh

+0

実際には、私は "if(this.something!= null)"を意味しました。 if文は何かを何度か設定しようとする試みをキャッチするためのものです。 –

+0

ああ、そうです。これをクリアしていただきありがとうございます。 – Mnementh

0

サイクルをモデル化するための正しい方法はGraphです。また、単一のソースコード行コメントでは、無人化を実現するのに十分です: "can't touch this"。

どのような変更不可能な施行をお探しですか?あなたvelociraptorが表示されるようにしますwhenever you modify the inmutable Setmutableinmutableの違いは単なる慣例に過ぎません。ただし、RAMのビットは簡単に変更できます。Reflection APIを使用すると、カプセル化とデータ隠蔽の規則を破ることができます。

Velociraptorを無視して、Javaは変更不可能な型をサポートしていません。この問題を回避するには、1つのように動作するデータ型をモデル化する必要があります。

Friend の実装クラスを持つinterfaceを作成する必要があります。オブジェクトの構築は、コンストラクタ内で完全に行われる必要があります。

次に、グラフにサイクルが含まれているため、最終的な変更不可能なインスタンスを作成する前に、グラフノードを変更可能な一時構造に保存する必要があります。また、InmutableFriend.friends()メソッドでunmodifiableSetを返す必要があります。

最後に、グラフをクローン化するには、Breadth-first searchのようなDeep-copyアルゴリズムをMutableグラフに実装する必要があります。しかし、1つの質問は、グラフが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

冷凍庫のパターンは本当に醜いです。私は友人が不変に見えるだろうと思っていますが、ビルダーの奥深くがそれに手を加えて凍結すると、外の世界に与えられます。 –

+0

@mP:フリーザーはパターンではありません。ディープコピーと呼ばれるアルゴリズムです:http://en.wikipedia.org/wiki/Object_copy#Deep_copy – vz0

+0

申し訳ありませんが、私は間違っていますMutableFrield =フィールド:)元のコメントを無視します。 –

関連する問題