Frage

Ich habe im Laufe von wenigen Projekten entwickelte ein Muster für die Erstellung von unveränderlichen (Read-only) Objekte und unveränderliches Objekt Graphen. Unveränderliche Objekte tragen den Nutzen 100% threadsicher zu sein und kann daher über Threads wieder verwendet werden. In meiner Arbeit verwende ich sehr häufig dieses Muster in Webanwendungen für Konfigurationseinstellungen und andere Objekte, die ich in den Speicher laden und Cache. Cached Objekte sollten immer unveränderlich sein, wie Sie sicherstellen wollen sie nicht unerwartet geändert.

Nun kann man natürlich leicht unveränderliche Objekte entwerfen, wie im folgenden Beispiel:

public class SampleElement
{
  private Guid id;
  private string name;

  public SampleElement(Guid id, string name)
  {
    this.id = id;
    this.name = name;
  }

  public Guid Id
  {
    get { return id; }
  }

  public string Name
  {
    get { return name; }
  }
}

Dies ist für einfache Klassen in Ordnung - aber für komplexere Klassen Phantasie mir nicht das Konzept alle Werte durch einen Konstruktor übergeben. Setter auf den Eigenschaften zu haben, ist wünschenswerter und Ihr Code ein neues Objekt wird zum Lesen einfacher zu konstruieren.

So wie Sie unveränderliche Objekte mit Setter erstellen?

Nun, Objekte in meinem Muster beginnen als vollständig wandelbar, bis Sie sie mit einem einzigen Methodenaufruf einzufrieren. Sobald ein Objekt eingefroren ist, wird es für immer unveränderlich bleiben - es kann nicht wieder in ein veränderbares Objekt gedreht werden. Wenn Sie eine veränderbare Version des Objekts benötigen, können Sie es einfach klonen.

Ok, nun an einigen Code. Ich habe in den folgenden Code-Schnipsel versucht, das Muster zu seiner einfachsten Form einkochen. Die IElement ist die Basis-Schnittstelle, die alle unveränderlichen Objekte letztlich implementieren müssen.

public interface IElement : ICloneable
{
  bool IsReadOnly { get; }
  void MakeReadOnly();
}

Die Element-Klasse ist die Standardimplementierung der IElement Schnittstelle:

public abstract class Element : IElement
{
  private bool immutable;

  public bool IsReadOnly
  {
    get { return immutable; }
  }

  public virtual void MakeReadOnly()
  {
    immutable = true;
  }

  protected virtual void FailIfImmutable()
  {
    if (immutable) throw new ImmutableElementException(this);
  }

  ...
}

Lassen Sie uns die SampleElement Klasse Refactoring über das unveränderliche Objekt Muster zu implementieren:

public class SampleElement : Element
{
  private Guid id;
  private string name;

  public SampleElement() {}

  public Guid Id
  {
    get 
    { 
      return id; 
    }
    set
    {
      FailIfImmutable();
      id = value;
    }
  }

  public string Name
  {
    get 
    { 
      return name; 
    }
    set
    {
      FailIfImmutable();
      name = value;
    }
  }
}

Sie können nun die Id-Eigenschaft ändern und die Eigenschaft Namen, solange das Objekt durch den Aufruf des Makereadonly () -Methode nicht als unveränderlich gekennzeichnet. Sobald es unveränderlich ist, ein Setter Aufruf wird eine ImmutableElementException ergeben.

Schlussbemerkung: Das vollständige Muster ist komplexer als der Code-Schnipsel hier gezeigt. Es enthält auch Unterstützung für Sammlungen von unveränderlichen Objekten und kompletter Objektgraphen von unveränderlichen Objektgraphen. Das vollständige Muster ermöglicht es Ihnen, ein ganzes Objektgraphen unveränderlich durch die Makereadonly () -Methode auf dem äußersten Objekt zu drehen. Sobald Sie anfangen, größere Objektmodelle zu schaffen, dieses Muster mit der Gefahr von undichten Objekten erhöht. Ein Leaky-Objekt ist ein Objekt, das die FailIfImmutable () Methode, bevor sie eine Änderung an dem Objekt Aufruf fehlschlägt. Zur Prüfung auf Lecks habe ich entwickeln auch eine generische Lecksucher Klasse für die Verwendung in Unit-Tests. Es nutzt Reflexion zu testen, ob alle Eigenschaften und Methoden, um die ImmutableElementException im unveränderlichen Zustand werfen. Mit anderen Worten wird TDD verwendet hier.

Ich habe gewachsen viel, um dieses Muster zu mögen und große Vorteile darin finden. So was würde ich gerne wissen, ist, wenn jemand von euch ähnliche Muster verwenden? Wenn ja, wissen Sie, von jedem guten Ressourcen, die sie dokumentieren? Ich bin im Wesentlichen für mögliche Verbesserungen suchen und für alle Standards, die möglicherweise bereits zu diesem Thema existieren.

War es hilfreich?

Lösung

Für Informationen wird der zweite Ansatz „Eis am Stiel Unveränderlichkeit“ genannt.

Eric Lippert hat eine Reihe von Blog-Einträgen auf Unveränderlichkeit Start hier . Ich bin immer noch mit der CTP (C # 4.0) in den Griff, aber es sieht interessant, was optional / benannte Parameter (zum .ctor) könnten hier tun (wenn sie auf Nur-Lese-Felder abgebildet) ... [Update: Ich habe auf dieser hier gebloggt]

Für Informationen, würde ich wahrscheinlich nicht diese Methoden virtual machen - wir wollen wahrscheinlich nicht Subklassen Lage zu sein, sie nicht freezable zu machen. Wenn Sie sie in der Lage sein wollen, zusätzlichen Code hinzufügen, würde ich vorschlagen, so etwas wie:

[public|protected] void Freeze()
{
    if(!frozen)
    {
        frozen = true;
        OnFrozen();
    }
}
protected virtual void OnFrozen() {} // subclass can add code here.

Auch - AOP (wie Postsharp) könnte ein gangbarer Weg sein, alle jene ThrowIfFrozen für das Hinzufügen () prüft

.

(Entschuldigung, wenn ich Terminologie / Methodennamen geändert haben - so halten Sie nicht die Original-Beitrag sichtbar, wenn Antworten Komponieren)

Andere Tipps

Eine andere Möglichkeit wäre eine Art von Builder-Klasse zu erstellen.

Ein Beispiel in Java (und C # und viele anderen Sprachen) String ist unveränderlich. Wenn Sie mehrere Vorgänge erstellen String tun möchten, benutzen Sie einen Stringbuilder. Dies ist wandelbar, und dann, sobald Sie fertig sind Sie es Ihnen Objekt des letzten String zurück. Von da an ist es unveränderlich.

Sie könnten etwas ähnliches für Ihre andere Klassen tun. Sie haben Ihr unveränderliches Element, und dann eine ElementBuilder. Alle der Erbauer tun würde, ist die Optionen speichern eingestellt haben, und wenn Sie es beenden sie konstruiert und gibt das unveränderliche Element.

Es ist ein wenig mehr Code, aber ich denke, es ist sauberer als Einrichter für eine Klasse hat, die seine unveränderlich angenommen hat.

Nach meinen anfänglichen Beschwerden über die Tatsache, dass ich eine neue System.Drawing.Point auf jeder Änderung zu schaffen hatte, habe ich ganz das Konzept vor einigen Jahren angenommen. In der Tat, schaffe ich jetzt jedes Feld als readonly standardmäßig aktiviert und es nur wandelbar sein ändern, wenn es ein zwingender Grund ist -., Die überraschend selten gibt es

Ich interessiere mich nicht sehr viel über Cross-Thread-Probleme, obwohl (ich selten Code verwenden, wo dies relevant ist). Ich finde es einfach viel, viel besser, weil die semantischen Ausdruckskraft. Unveränderlichkeit ist der Inbegriff einer Schnittstelle, die falsch ist schwer zu verwenden.

Sie haben es immer noch mit Staat und damit noch gebissen werden können, wenn Ihre Objekte parallelisiert werden, bevor sie unveränderlich gemacht werden.

Eine funktionelle Art und Weise eine neue Instanz des Objekts mit jedem Setter zurückkehren kann. Oder ein veränderbares Objekt erstellen und an den Konstruktor in.

Das (relativ) neue Software Design Paradigma Domain Driven Design genannt, macht die Unterscheidung zwischen Entitätsobjekten und Wertobjekten.

Entity Objekte werden als etwas definiert, die zu einem Schlüssel angetriebenen Objekt in einem persistenten Datenspeicher abzubilden hat, wie ein Angestellter oder ein Kunde oder eine Rechnung, etc ... wo die Eigenschaften Änderung des Objekts bedeutet, dass Sie müssen, um die Änderung zu einem Datenspeicher speichern irgendwo, und die Existenz von mehreren Instanzen einer Klasse mit dem gleichen „Schlüssel“ imnplies eine Notwendigkeit, sie zu synchronisieren, oder ihre Persistenz in dem Datenspeicher zu koordinieren, so dass eine Instanz‘Änderungen nicht überschreiben der andere. Ändern der Eigenschaften eines Entitätsobjekt bedeutet, dass Sie etwas über das Objekt ändern sich - nicht zu ändern, welches Objekt Sie verweisen ...

Wertobjekte OTOH, sind Objekte, die unveränderlich angesehen werden können, deren Nützlichkeit definiert ist streng durch ihre Eigenschaftswerte und für die mehrere Instanzen, brauchen nicht in irgendeiner Weise koordiniert werden ... wie Adressen oder Telefonnummern, oder die Räder auf einem Auto, oder die Buchstaben in einem Dokument ... diese Dinge sind völlig durch ihre Eigenschaften definiert ... ein Groß ‚A‘ Objekt in einem Texteditor kann transparent mit jedem anderes Groß ausgetauscht werden ‚A‘ Objekt im gesamten das Dokument, Sie keinen Schlüssel benötigen sie von allen anderen zu unterscheiden ‚A ist in diesem Sinne ist es unveränderlich ist, denn wenn man es zu einer Änderung‚B‘(wie die Telefonnummer Zeichenfolge in eine Telefonnummer Objekt zu ändern, Sie sind nicht die Daten mit einem gewissen wandelbaren Entität zugeordnet ändern Sie von einem Wert zu einem anderen wechseln ... genauso wie wenn Sie den Wert eines Strings ändern ...

System.String ein gutes Beispiel für eine unveränderliche Klasse mit Setter und mutiert Methoden, nur dass jeder mutiert Methode gibt eine neue Instanz.

Die Erweiterung auf den Punkt von @Cory Foy und @Charles Bretana in dem eine Differenz zwischen den Entitäten und Werten besteht. Während wert Objekte immer unveränderlich sein sollten, ich glaube wirklich nicht, dass ein Objekt der Lage sein sollte, sich frieren, oder lassen sich in der Codebasis willkürlich eingefroren werden. Es hat einen wirklich schlechten Geruch zu ihm, und ich mache mir Sorgen, dass es schwer bekommen konnte, ausfindig zu machen, wo genau ein Objekt eingefroren wurde, und warum es wurde eingefroren, und die Tatsache, dass zwischen den Anrufen zu einem Objekt es Zustand von gefrorenen aufgetaut ändern könnte .

Das ist nicht zu sagen, dass man manchmal eine (änderbare) Entität etwas geben möchte, und sicherzustellen, dass sie nicht verändert werden sollen.

Also, statt dem Einfrieren des Objekts selbst, ist eine weitere Möglichkeit, die Semantik von Readonlycollection

kopieren
List<int> list = new List<int> { 1, 2, 3};
ReadOnlyCollection<int> readOnlyList = list.AsReadOnly();

Ihr Objekt kann einen Teil als wandelbar nehmen, wenn sie sie braucht, und dann unveränderlich sein, wenn Sie es wünschen sein.

Beachten Sie, dass Readonlycollection auch implementiert ICollection , das eine Add( T item) Methode in der Schnittstelle. Allerdings gibt auch in der Schnittstelle definiert ist bool IsReadOnly { get; } so dass die Verbraucher vor dem Aufruf einer Methode überprüfen, die eine Ausnahme werfen wird.

Der Unterschied besteht darin, dass man nicht nur IsReadOnly auf false gesetzt. Eine Sammlung entweder ist oder nicht nur gelesen, und das ändert sich nie für die gesamte Lebensdauer der Sammlung.

Es wäre an der Zeit schön, die const-Korrektheit zu haben, die C ++ Ihnen bei der Kompilierung gibt, aber das beginnt es ist eigene Probleme zu haben, und ich bin froh, C # dort nicht gehen.


ICloneable - ich dachte, dass ich nur auf das folgende beziehen würde:

  

Sie nicht implementieren ICloneable

     

Verwenden Sie ICloneable nicht in öffentlichen APIs

Brad Abrams - Design-Richtlinien, Managed Code und der. NET Framework

Dies ist ein wichtiges Problem, und ich habe die Liebe direktere Rahmen / Sprachunterstützung, um zu sehen, es zu lösen. Die Lösung, die Sie haben erfordert viel vorformulierten. Es könnte sein, einfach einen Teil des Textvorschlages zu automatisieren, indem die Codegenerierung verwendet wird.

Sie würden eine partielle Klasse erzeugen, die alle freezable Eigenschaften enthält. Es wäre ziemlich einfach, eine wiederverwendbare T4-Vorlage für diese zu machen.

Die Vorlage würde dies für die Eingabe:

  • Namespace
  • Klassenname
  • Liste der Eigenschaftsnamen / Typ Tupel

Und ausgeben würde eine C # Datei enthalten:

  • Namespace-Deklaration
  • partial class
  • jedes der Objekte, mit den entsprechenden Typen, ein Trägerfeld, ein Getter und eine Einstelleinrichtung, die die Methode FailIfFrozen
  • ruft

AOP-Tags auf freezable Eigenschaften könnten auch funktionieren, aber es würde mehr Abhängigkeiten erfordern, während T4 in neuere Versionen von Visual Studio integriert ist.

Ein weiteres Szenario, das sehr ähnlich wie das ist die INotifyPropertyChanged Schnittstelle. Lösungen für dieses Problem sind wahrscheinlich für dieses Problem anwendbar ist.

Mein Problem mit diesem Muster ist, dass Sie keine Kompilierung-Beschränkungen auf Unveränderlichkeit aufzuzwingen. Der Coder ist dafür verantwortlich, dass ein Objekt auf unveränderliche bevor beispielsweise festgelegt ist, es zu einem Cache oder einem anderen nicht-faden sichere Struktur hinzugefügt wird.

Deshalb habe ich dieses Codierungsmuster verlängern würde mit einem Kompilierung-Zurückhaltung in Form einer generischen Klasse, wie folgt aus:

public class Immutable<T> where T : IElement
{
    private T value;

    public Immutable(T mutable) 
    {
        this.value = (T) mutable.Clone();
        this.value.MakeReadOnly();
    }

    public T Value 
    {
        get 
        {
            return this.value;
        }
    }

    public static implicit operator Immutable<T>(T mutable) 
    {
        return new Immutable<T>(mutable);
    }

    public static implicit operator T(Immutable<T> immutable)
    {
        return immutable.value;
    }
}

Hier ist ein Beispiel, wie Sie diese verwenden würden:

// All elements of this list are guaranteed to be immutable
List<Immutable<SampleElement>> elements = 
    new List<Immutable<SampleElement>>();

for (int i = 1; i < 10; i++) 
{
    SampleElement newElement = new SampleElement();
    newElement.Id = Guid.NewGuid();
    newElement.Name = "Sample" + i.ToString();

    // The compiler will automatically convert to Immutable<SampleElement> for you
    // because of the implicit conversion operator
    elements.Add(newElement);
}

foreach (SampleElement element in elements)
    Console.Out.WriteLine(element.Name);

elements[3].Value.Id = Guid.NewGuid();      // This will throw an ImmutableElementException

Zwei weitere Optionen für Ihr spezielles Problem, das nicht diskutiert haben:

  1. Erstellen Sie Ihre eigenen Deserializer, eine, die eine private Eigenschaft Setter aufrufen können. Während die Bemühungen, die Deserializer am Anfang beim Aufbau werden viel mehr sein, macht es die Dinge sauberer. Der Compiler werden Sie hält von selbst versuchen, die Setter und den Code in Ihren Klassen werden rufen leichter zu lesen.

  2. Setzen Sie einen Konstruktor in jeder Klasse, die ein XElement (oder einen anderen Geschmack von XML-Objektmodell) führt und füllt sich von ihm. Offensichtlich als die Anzahl der Klassen zunimmt, wird dies schnell weniger wünschenswert als eine Lösung.

Wie wäre es eine abstrakte Klasse ThingBase hat, mit den Unterklassen MutableThing und ImmutableThing? ThingBase würden alle Daten in einer geschützten Struktur enthalten, öffentliche schreibgeschützte Eigenschaften für die Felder und geschützt schreibgeschützte Eigenschaft für seine Struktur. Es wäre auch ein überwindbaren AsImmutable Verfahren bereitzustellen, die eine ImmutableThing zurückkehren würden.

MutableThing würde die Eigenschaften mit Lese- / Schreibeigenschaften Schatten und bietet sowohl ein Standard-Konstruktor und einen Konstruktor, der eine ThingBase akzeptiert.

Immutable Sache würde eine versiegelte Klasse sein, die AsImmutable überschreibt einfach selbst zurück. Es würde auch einen Konstruktor zur Verfügung stellen, die eine ThingBase akzeptiert.

Ich mag nicht die Idee der Lage zu sein, ein Objekt von einem wandelbaren zu einem unveränderlichen Zustand zu ändern, dass Art scheint der Punkt des Designs mich zu besiegen. Wann benötigen Sie das zu tun? Nur Objekte, die Werte repräsentieren sollte unveränderlich sein

Sie können optional benannte Argumente verwenden zusammen mit nullables mit sehr wenig vorformulierten eine unveränderliche Setter zu machen. Wenn Sie wirklich eine Eigenschaft festlegen wollen auf null, dann können Sie noch einige Probleme haben.

class Foo{ 
    ...
    public Foo 
        Set
        ( double? majorBar=null
        , double? minorBar=null
        , int?        cats=null
        , double?     dogs=null)
    {
        return new Foo
            ( majorBar ?? MajorBar
            , minorBar ?? MinorBar
            , cats     ?? Cats
            , dogs     ?? Dogs);
    }

    public Foo
        ( double R
        , double r
        , int l
        , double e
        ) 
    {
        ....
    }
}

Sie würde es wie so verwenden

var f = new Foo(10,20,30,40);
var g = f.Set(cat:99);
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top