Domanda

Nel corso di alcuni progetti ho sviluppato un modello per la creazione di oggetti immutabili (di sola lettura) e grafici di oggetti immutabili. Gli oggetti immutabili offrono il vantaggio di essere sicuri al 100% per i thread e possono quindi essere riutilizzati tra i thread. Nel mio lavoro utilizzo molto spesso questo modello nelle applicazioni Web per le impostazioni di configurazione e altri oggetti che carico e memorizzo nella cache. Gli oggetti memorizzati nella cache dovrebbero essere sempre immutabili in quanto si desidera garantire che non vengano modificati in modo imprevisto.

Ora puoi ovviamente progettare facilmente oggetti immutabili come nell'esempio seguente:

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

Questo va bene per le classi semplici, ma per le classi più complesse non mi piace l'idea di passare tutti i valori attraverso un costruttore. Avere setter sulle proprietà è più desiderabile e il codice che costruisce un nuovo oggetto diventa più facile da leggere.

Quindi, come si creano oggetti immutabili con setter?

Bene, nel mio modello gli oggetti iniziano come completamente mutabili fino a quando non li congeli con una singola chiamata di metodo. Una volta che un oggetto è congelato rimarrà immutabile per sempre - non può più essere trasformato in un oggetto mutabile. Se hai bisogno di una versione mutabile dell'oggetto, devi semplicemente clonarlo.

Ok, ora passiamo al codice. Nel seguente frammento di codice ho provato a ridurre il modello nella sua forma più semplice. IElement è l'interfaccia di base che tutti gli oggetti immutabili devono in definitiva implementare.

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

La classe Element è l'implementazione predefinita dell'interfaccia IElement:

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

  ...
}

Rifattattiamo la classe SampleElement sopra per implementare il modello di oggetti immutabile:

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

Ora puoi modificare la proprietà Id e la proprietà Name purché l'oggetto non sia stato contrassegnato come immutabile chiamando il metodo MakeReadOnly (). Una volta che è immutabile, chiamare un setter produrrà un ImmutableElementException.

Nota finale: Il modello completo è più complesso dei frammenti di codice mostrati qui. Contiene inoltre il supporto per raccolte di oggetti immutabili e grafici di oggetti completi di grafici di oggetti immutabili. Il modello completo consente di trasformare un intero oggetto grafico immutabile chiamando il metodo MakeReadOnly () sull'oggetto più esterno. Quando si inizia a creare modelli di oggetti più grandi utilizzando questo modello, aumenta il rischio di oggetti che perdono. Un oggetto che perde è un oggetto che non riesce a chiamare il metodo FailIfImmutable () prima di apportare una modifica all'oggetto. Per verificare la presenza di perdite, ho anche sviluppato una classe generica di rilevatori di perdite da utilizzare nei test unitari. Utilizza la riflessione per verificare se tutte le proprietà e tutti i metodi generano ImmutableElementException nello stato immutabile. In altre parole, qui viene utilizzato TDD.

Mi è piaciuto molto questo schema e ci sono stati grandi benefici. Quindi quello che vorrei sapere è se qualcuno di voi sta usando schemi simili? Se sì, conosci qualche buona risorsa che lo documenta? Sono essenzialmente alla ricerca di potenziali miglioramenti e di eventuali standard che potrebbero già esistere su questo argomento.

È stato utile?

Soluzione

Per informazioni, il secondo approccio si chiama " immutabilità dei ghiacci " ;.

Eric Lippert ha una serie di post sul blog sull'immutabilità a partire da qui . Sto ancora facendo i conti con il CTP (C # 4.0), ma sembra interessante quali parametri opzionali / nominati (per il .ctor) potrebbero fare qui (se mappati su campi di sola lettura) ... [aggiornamento: ho scritto un blog su questo qui ]

Per informazioni, probabilmente non renderei questi metodi virtual - probabilmente non vogliamo che le sottoclassi siano in grado di renderlo non congelabile. Se vuoi che siano in grado di aggiungere un codice aggiuntivo, suggerirei qualcosa del tipo:

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

Inoltre - AOP (come PostSharp) potrebbe essere un'opzione praticabile per aggiungere tutti quei controlli ThrowIfFrozen ().

(mi scuso se ho cambiato la terminologia / i nomi dei metodi - SO non mantiene visibile il post originale durante la composizione delle risposte)

Altri suggerimenti

Un'altra opzione sarebbe quella di creare una sorta di classe Builder.

Ad esempio, in Java (e C # e molte altre lingue) String è immutabile. Se si desidera eseguire più operazioni per creare una stringa, utilizzare StringBuilder. Questo è mutabile e, una volta terminato, viene restituito l'oggetto String finale. Da allora in poi è immutabile.

Potresti fare qualcosa di simile per le altre tue classi. Hai il tuo elemento immutabile e poi un ElementBuilder. Tutto ciò che il builder farebbe è memorizzare le opzioni impostate, quindi quando lo finalizzi costruisce e restituisce l'elemento immutabile.

È un po 'più di codice, ma penso che sia più pulito che avere setter in una classe che dovrebbe essere immutabile.

Dopo il mio iniziale disagio per il fatto che dovevo creare un nuovo System.Drawing.Point su ogni modifica, alcuni anni fa ho abbracciato del tutto il concetto. In effetti, ora creo ogni campo come readonly per impostazione predefinita e lo cambio in modo che sia mutabile solo se c'è un motivo convincente & # 8211; che è sorprendentemente raro.

Non mi preoccupo molto dei problemi di cross-threading (utilizzo raramente il codice laddove rilevante). Lo trovo molto, molto meglio a causa dell'espressività semantica. L'immutabilità è l'epitome di un'interfaccia che è difficile da usare in modo errato.

Hai ancora a che fare con lo stato, e quindi puoi ancora essere morso se i tuoi oggetti sono parallelizzati prima di essere resi immutabili.

Un modo più funzionale potrebbe essere quello di restituire una nuova istanza dell'oggetto con ciascun setter. Oppure crea un oggetto mutabile e passalo al costruttore.

Il (relativamente) nuovo paradigma di Software Design chiamato Domain Driven design, fa la distinzione tra oggetti entità e oggetti valore.

Gli oggetti entità sono definiti come tutto ciò che deve essere mappato a un oggetto guidato da chiave in un archivio dati persistente, come un dipendente, un cliente o una fattura, ecc ... dove la modifica delle proprietà dell'oggetto implica che è necessario salvare la modifica in un archivio dati da qualche parte e l'esistenza di più istanze di una classe con la stessa " chiave " implica la necessità di sincronizzarli o coordinare la loro persistenza nell'archivio dati in modo che le modifiche di un'istanza non sovrascrivano le altre. Cambiare le proprietà di un oggetto entità implica che stai cambiando qualcosa sull'oggetto - non cambiando QUALE oggetto a cui stai facendo riferimento ...

Gli oggetti valore otoh, sono oggetti che possono essere considerati immutabili, la cui utilità è definita rigorosamente dai loro valori di proprietà e per la quale più istanze non devono essere coordinate in alcun modo ... come indirizzi o numeri di telefono, o le ruote di una macchina, o le lettere in un documento ... queste cose sono totalmente definite dalle loro proprietà ... un oggetto "A" maiuscolo in un editor di testo può essere scambiato in modo trasparente con qualsiasi altro oggetto "A" maiuscolo nel documento, non hai bisogno di una chiave per distinguerlo da tutte le altre "A" In questo senso è immutabile, perché se lo cambi in una "B" (proprio come cambiare la stringa del numero di telefono in un oggetto numero di telefono, non stai modificando i dati associati a qualche entità mutabile, stai passando da un valore a un altro ... proprio come quando cambi il valore di una stringa ...

System.String è un buon esempio di classe immutabile con setter e metodi mutanti, solo che ogni metodo mutante restituisce una nuova istanza.

Espandere sul punto di @Cory Foy e @Charles Bretana dove c'è una differenza tra entità e valori. Mentre gli oggetti valore dovrebbero essere sempre immutabili, non credo davvero che un oggetto dovrebbe essere in grado di congelarsi, o di lasciarsi congelare arbitrariamente nella base di codice. Ha un cattivo odore, e temo che potrebbe essere difficile rintracciare esattamente dove un oggetto è stato congelato, e perché è stato congelato, e il fatto che tra le chiamate a un oggetto potrebbe cambiare stato da scongelato a congelato .

Questo non vuol dire che a volte vuoi dare un'entità (mutevole) a qualcosa e assicurarti che non cambierà.

Quindi, invece di congelare l'oggetto stesso, un'altra possibilità è quella di copiare la semantica di ReadOnlyCollection < T & Gt;

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

Il tuo oggetto può prendere una parte come mutabile quando ne ha bisogno e quindi essere immutabile quando lo desideri.

Nota che ReadOnlyCollection < T & Gt; implementa anche ICollection < T & Gt; che ha un metodo Add( T item) nell'interfaccia. Tuttavia c'è anche bool IsReadOnly { get; } definito nell'interfaccia in modo che i consumatori possano verificare prima di chiamare un metodo che genererà un'eccezione.

La differenza è che non puoi semplicemente impostare IsReadOnly su false. Una raccolta è o non è di sola lettura e non cambia mai per la durata della raccolta.

Sarebbe bello avere la correttezza const che C ++ ti dà in fase di compilazione, ma questo inizia ad avere i suoi problemi e sono contento che C # non ci vada.


ICloneable - Ho pensato di fare riferimento a quanto segue:

  

Non implementare ICloneable

     

Non utilizzare ICloneable nelle API pubbliche

Brad Abrams - Linee guida per la progettazione, codice gestito e codice. NET Framework

Questo è un problema importante e mi piace vedere un supporto più diretto per il framework / linguaggio per risolverlo. La soluzione che hai richiede molta caldaia. Potrebbe essere semplice automatizzare parte del boilerplate usando la generazione del codice.

Genereresti una classe parziale che contiene tutte le proprietà congelabili. Sarebbe abbastanza semplice creare un modello T4 riutilizzabile per questo.

Il modello lo prenderebbe per l'input:

  • namespace
  • nome classe
  • elenco di tuple nome / tipo di proprietà

E produrrebbe un file C #, contenente:

  • dichiarazione dello spazio dei nomi
  • classe parziale
  • ciascuna delle proprietà, con i tipi corrispondenti, un campo di supporto, un getter e un setter che invoca il metodo FailIfFrozen

Anche i tag AOP sulle proprietà congelabili potrebbero funzionare, ma richiederebbero più dipendenze, mentre T4 è integrato nelle versioni più recenti di Visual Studio.

Un altro scenario che è molto simile a questo è l'interfaccia INotifyPropertyChanged. Le soluzioni a questo problema sono probabilmente applicabili a questo problema.

Il mio problema con questo modello è che non stai imponendo alcuna restrizione in fase di compilazione sull'immutabilità. Il programmatore è responsabile di assicurarsi che un oggetto sia impostato su immutabile prima, ad esempio, di aggiungerlo a una cache o a un'altra struttura non thread-safe.

Ecco perché estenderei questo modello di codifica con un limite di compilazione in forma di una classe generica, come questo:

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

Ecco un esempio di come lo useresti:

// 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

Solo un suggerimento per semplificare le proprietà dell'elemento: Usa proprietà automatiche con private set ed evita di dichiarare esplicitamente il campo dati. per es.

public class SampleElement {
  public SampleElement(Guid id, string name) {
    Id = id;
    Name = name;
  }

  public Guid Id {
    get; private set;
  }

  public string Name {
    get; private set;
  }
}

Ecco un nuovo video su Channel 9 in cui Anders Hejlsberg dalle 36:30 nell'intervista inizia a parlare di immutabilità in C #. Fornisce un ottimo caso d'uso per l'immutabilità dei ghiaccioli e spiega come questo è qualcosa che ti viene attualmente richiesto per implementare te stesso. È stata musica per le mie orecchie sentirlo dire che vale la pena pensare a un supporto migliore per la creazione di grafici di oggetti immutabili nelle versioni future di C #

Esperto in esperto: Anders Hejlsberg - Il futuro di C #

Altre due opzioni per il tuo problema particolare che non sono state discusse:

  1. Crea il tuo deserializzatore, uno che può chiamare un setter di proprietà privata. Mentre lo sforzo per costruire il deserializzatore all'inizio sarà molto più, rende le cose più pulite. Il compilatore ti impedirà persino di chiamare i setter e il codice nelle tue classi sarà più facile da leggere.

  2. Inserisce un costruttore in ogni classe che prende un XElement (o qualche altra variante del modello a oggetti XML) e si popola da esso. Ovviamente all'aumentare del numero di classi, questo diventa rapidamente meno desiderabile come soluzione.

Che ne dici di avere una classe astratta ThingBase, con sottoclassi MutableThing e ImmutableThing? ThingBase conterrebbe tutti i dati in una struttura protetta, fornendo proprietà di sola lettura pubbliche per i campi e proprietà di sola lettura protette per la sua struttura. Fornirebbe anche un metodo AsImmutable scavalcabile che restituirebbe ImmutableThing.

MutableThing oscurerebbe le proprietà con proprietà read / write e fornire sia un costruttore predefinito che un costruttore che accetta un ThingBase.

La cosa immutabile sarebbe una classe sigillata che sostituisce AsImmutable per restituire semplicemente se stessa. Fornirebbe anche un costruttore che accetta un ThingBase.

Non mi piace l'idea di essere in grado di cambiare un oggetto da uno stato mutevole a uno immutabile, quel tipo di mi sembra sconfiggere il punto del design. Quando hai bisogno di farlo? Solo gli oggetti che rappresentano VALORI dovrebbero essere immutabili

Puoi usare argomenti nominati opzionali insieme a nullable per creare un setter immutabile con pochissima piastra di caldaia. Se vuoi davvero impostare una proprietà su null, potresti avere qualche altro problema.

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
        ) 
    {
        ....
    }
}

Lo useresti così

var f = new Foo(10,20,30,40);
var g = f.Set(cat:99);
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top