2010-02-04 16 views
5

So erkennen wir alle die Vorteile von unveränderlichen Typen, insbesondere in Multithread-Szenarien. (Oder zumindest sollten wir alle das realisieren; siehe zum Beispiel System.String.)Unveränderliche Klasse Konstruktion Design

Allerdings, was ich nicht gesehen habe, ist viel Diskussion für Erstellen von unveränderlichen Instanzen, insbesondere Design-Richtlinien.

Angenommen, wir folgende unveränderliche Klasse haben wollen:

class ParagraphStyle { 
    public TextAlignment Alignment {get;} 
    public float FirstLineHeadIndent {get;} 
    // ... 
} 

Der häufigste Ansatz, den ich gesehen habe, ist wandelbar/unveränderlich „Paare“ von Typen zu haben, z.B. die veränderbaren List<T> und unveränderlichen ReadOnlyCollection<T> Arten oder die veränderbaren StringBuilder und unveränderlichen String Arten.

dieses bestehende Muster imitieren würde die Einführung einer Art „wandelbar“ ParagraphStyle Typ, der „Duplikate“ die Mitglieder (um Etter), und geben Sie dann einen ParagraphStyle Konstruktor erfordern, die den wandelbaren Typ als Argument akzeptiert

Also, das funktioniert, unterstützt die Code-Vervollständigung innerhalb der IDE und macht Dinge ziemlich offensichtlich, wie man Dinge konstruiert ... aber es scheint ziemlich duplicative.

Gibt es einen besseren Weg?

Zum Beispiel C# anonyme Typen sind unveränderlich, und lassen Sie „normal“ Eigenschaft Setter für die Initialisierung mit:

var anonymousTypeInstance = new { 
    Foo = "string", 
    Bar = 42; 
}; 
anonymousTypeInstance.Foo = "another-value"; // compiler error 

Leider ist die nächste Möglichkeit, diese Semantik in C# zu duplizieren ist Konstruktorparameter zu verwenden:

// Option 2: 
class ParagraphStyle { 
    public ParagraphStyle (TextAlignment alignment, float firstLineHeadIndent, 
      /* ... */) {...} 
} 

Aber das "skaliert" nicht gut; wenn Ihr Typ z. 15 Eigenschaften, ein Konstruktor mit 15 Parametern ist alles andere als freundlich, und die Bereitstellung "nützlicher" Überladungen für alle 15 Eigenschaften ist ein Rezept für einen Albtraum. Ich lehne es ab.

Wenn wir versuchen, anonyme Typen zu imitieren, so scheint es, dass wir „Set-once“ Eigenschaften in dem „unveränderlich“ Typ, und damit die „wandelbar“ Variante Drop verwenden:

// Option 3: 
class ParagraphStyle { 
    bool alignmentSet; 
    TextAlignment alignment; 

    public TextAlignment Alignment { 
     get {return alignment;} 
     set { 
      if (alignmentSet) throw new InvalidOperationException(); 
      alignment = value; 
      alignmentSet = true; 
     } 
    } 
    // ... 
} 

Das Problem mit das ist, dass es nicht offensichtlich ist, dass Eigenschaften nur einmal gesetzt werden können (der Compiler wird sich sicherlich nicht beschweren), und die Initialisierung ist nicht Thread-sicher. Es wird daher verlockend, eine Commit()-Methode hinzuzufügen, so dass das Objekt wissen kann, dass der Entwickler die Eigenschaften eingestellt hat (wodurch alle Eigenschaften verursacht werden, die zuvor nicht gesetzt wurden, wenn ihr Setter aufgerufen wird), aber dies scheint a zu sein Weg, die Dinge noch schlimmer zu machen, nicht besser.

Gibt es ein besseres Design als die veränderbare/unveränderliche Klassenaufteilung? Oder bin ich dazu verdammt, mit der Mitgliederverdopplung fertig zu werden?

+0

Ich weiß, dass dies schwer zu glauben ist, aber ich arbeite mit einer bestehenden C# -Code-Basis. Die Portierung der gesamten Sache auf F # ist keine Option, und selbst wenn es eine Option wäre, scheint die MSR-SSLA-Lizenz, unter der die F # -Bibliotheken veröffentlicht werden, für meine Verwendung nicht geeignet, insbesondere (ii) (c). – jonp

+0

Dies scheint von: http://stackoverflow.com/questions/263585/immutable-object-pattern-in-c-what-do-you-think – jonp

+0

Kann keine Antwort sein, aber ich habe mit ähnlichen Anforderungen behandelt in der Vergangenheit durch Implementierung von ISupportInitialize. Nach einem Aufruf von EndInit() werden Sie unveränderlich und werfen, wenn Setter aufgerufen werden. Nicht Kompilierzeit sicher, aber Dokumentation und werfen früh und oft hilft. – Will

Antwort

2

In ein paar Projekten verwendete ich fließend Ansatz. I.e. Die meisten generischen Eigenschaften (z. B. Name, Standort, Titel) werden über ctor definiert, während andere mit Set-Methoden aktualisiert werden, die eine neue unveränderliche Instanz zurückgeben.

class ParagraphStyle { 
    public TextAlignment Alignment {get; private set;} 
    public float FirstLineHeadIndent {get; private set;} 
    // ... 
    public ParagraphStyle WithAlignment(TextAlignment ta) { 
     var newStyle = (ParagraphStyle)MemberwiseClone(); 
     newStyle.TextAlignment = ta; 
    } 
    // ... 
} 

MemberwiseClone ist hier in Ordnung, sobald unsere Klasse wirklich tief unveränderlich ist.

+1

Während das funktioniert und nützlich ist, ist es auch keine skalierbare Lösung. Wenn Sie z.B. Die Mutator-Methoden WithFoo() führen zu 15 "temporären" Instanzen, die erstellt werden, bevor Sie Ihr Endergebnis haben. Abhängig davon, wie "schwer" Ihr Typ ist, könnte dies eine beträchtliche Menge an Müll sein. WithFoo() unterstützt auch keine Auflistungsinitialisierer, was kein großer Verlust ist, aber ich liebe sie sehr ;-). Das Foo + FooBuilder-Idiom scheint ein vernünftiger Kompromiss zu sein. – jonp

+0

Ich habe dieses Verfahren ein anderes Mal in einem der letzten Projekte ausprobiert und ich stimme völlig zu, es ist langweilig, all diese Wrapper zu erstellen. Ich stimme zu Builder ist viel eleganter und ziemlich lesbar. Was ist mit tief verschachtelten Strukturen? FP-Datenstrukturen fördern den geringstmöglichen Speicherbedarf durch die Wiederverwendung unveränderter Elemente. – olegz

+0

Sie können auch einen Konstruktor des fließenden Builders angeben, der das unveränderliche Objekt als Argument verwendet, um die internen Felder damit zu initialisieren. So können Sie 'WithFoo()' nur auf diesen 'Foos' verwenden, die Sie ändern müssen :) –