2011-01-29 6 views
5

변경할 수없는 클래스는 훌륭하지만 해결할 수있는 합리적인 방법을 생각할 수없는 큰 문제가 하나 있습니다.변경 불가능한 클래스 인스턴스 간의 사이클을 모델링하는 방법은 무엇입니까?

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

나를 당신을 친구로 지목 한 친구가 나를 친구로 다시 모으는 모델을 만드는 방법은 무엇입니까?

IMMUTABILITY 외부 세계의이 수업은 분명히 불변이어야합니다. 내부적으로 유지되는 값은 평등성 검사를 위해 일정해야합니다.

+0

사전에 필요한 양방향 관계를 모두 알고 있습니까? 아니면 한 번에 하나씩 추가할까요? 후자의 경우, 당신이 실제로 물건을 바꾸고 있기 때문에, 정말로 불변의 보증을받을 수있는 방법이 없습니다. – 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의 수정 불가능한 래퍼 대신 진정으로 불변 인 세트에 Guava ImmutableSet을 사용하는 다른 버전이 있습니다. 그것은 동일하게 작동하지만 구아바의 훌륭한 세트 빌더를 사용합니다.

코드 :

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

@Konstantin Komissarchik 스택에있는 많은 친구 (최선의 경우 그래프의 직경, 모두 최악의 경우)로이 작업을 수행 할 수 있으며 여전히 변경되지 않습니다. –

+0

@Tom 나는 그것을 사지 않는다. 친구는 결국 서로를 참조하는 두 개의 친구 개체로 끝나기 위해 비 최종적인 무언가를 가져야합니다. 내부적으로 최종이 아닌 프록시 (일명 작성자)에 대한 최종 참조는 중요하지 않습니다. 이것은 지연 초기화를 수행하는 또 다른 방법입니다. –

-1

불변성은 컴파일러가 유효한 아키텍처 일 필요가 없습니다. 사후 구축 초기화 매개 변수를 취하는 정당한 불변 객체를 가질 수 있습니다. 예를 들어 ...

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

어떤 종류의 변경 불가능한 시행을 원하십니까? 벨로시 랩터가 나타나기를 원하십니까 whenever you modify the inmutable Set? mutableinmutable의 차이점은 단지 규칙입니다. 그러나 RAM의 비트는 쉽게 수정할 수 있으며 Reflection API을 사용하면 캡슐화 및 데이터 숨기기 규칙을 위반할 수 있습니다.

잠시 동안 벨로시 랩터를 무시하면 Java는 변경 불가능한 유형을 지원하지 않습니다. 이 문제를 해결하려면 하나의 데이터 유형을 모델링해야합니다.

변경 불가능 속성을 사용하면 이 interface이고 하나는 구현 클래스 인 InmutableFriend이어야하며 개체 생성은 생성자 내부에서 완전히 이루어져야합니다.

그런 다음 그래프에 사이클이 포함되므로 최종 변경 불가능 인스턴스를 만들기 전에 그래프 노드를 변경 가능한 임시 구조에 저장해야합니다. InmutableFriend.friends() 메서드에서 unmodifiableSet을 반환해야합니다.

마지막으로 그래프를 복제하려면 변경 가능 그래프에 Breadth-first search과 같은 Deep-copy 알고리즘을 구현해야합니다. 한 가지 질문은 그래프가 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

냉동실 패턴은 정말 못생긴 imho입니다. 친구는 불변으로 보일 것이라고 생각하기 시작합니다.하지만 건축가 깊숙한 곳에서 건물에 깊숙이 들어가서 얼어 버리면 외부 세계에 줄 수 있습니다. –

+0

@mP : 냉동실은 패턴이 아닙니다. Deep Copy라는 알고리즘입니다. http://en.wikipedia.org/wiki/Object_copy#Deep_copy – vz0

+0

죄송합니다. 내가 잘못 읽었습니다. MutableFrield = 필드 :) 원래 코멘트를 무시합니다. –

관련 문제