A che punto le classi immutabili diventano un peso?

15

Quando si progettano classi per contenere il modello dati che ho letto, può essere utile creare oggetti immutabili, ma a che punto il carico degli elenchi dei parametri del costruttore e delle copie profonde diventa eccessivo e si deve abbandonare la restrizione immutabile?

Ad esempio, ecco una classe immutabile per rappresentare una cosa con nome (sto usando la sintassi C # ma il principio si applica a tutte le lingue OO)

class NamedThing
{
    private string _name;    
    public NamedThing(string name)
    {
        _name = name;
    }    
    public NamedThing(NamedThing other)
    {
         this._name = other._name;
    }
    public string Name
    {
        get { return _name; }
    }
}

Le cose con nome possono essere costruite, interrogate e copiate in nuove cose nominate, ma il nome non può essere modificato.

Questo è tutto ok ma cosa succede quando voglio aggiungere un altro attributo? Devo aggiungere un parametro al costruttore e aggiornare il costruttore di copie; che non è troppo lavoro ma i problemi iniziano, per quanto posso vedere, quando voglio rendere immutabile un oggetto complesso .

Se la classe contiene attributi e raccolte, contenenti altre classi complesse, mi sembra che l'elenco dei parametri del costruttore diventerebbe un incubo.

Quindi a che punto una classe diventa troppo complessa per essere immutabile?

    
posta Tony 14.04.2011 - 12:13
fonte

7 risposte

24

Quando diventano un peso? Molto rapidamente (specialmente se la lingua scelta non fornisce un supporto sintattico sufficiente per l'immutabilità.)

L'immutabilità viene venduta come pallottola d'argento per il dilemma multi-core e tutto il resto. Ma l'immutabilità nella maggior parte dei linguaggi OO ti obbliga ad aggiungere artefatti e pratiche artificiali nel tuo modello e processo. Per ogni classe immutabile complessa devi avere un costruttore altrettanto complesso (almeno internamente). Indipendentemente dal modo in cui lo si progetta, il fermo immagine introduce un strong accoppiamento (quindi è meglio avere una buona ragione per introdurlo).

Non è necessariamente possibile modellare tutto in piccole classi non complesse. Pertanto, per le classi e le strutture di grandi dimensioni, le partizioniamo artificialmente, non perché che abbia senso nel nostro modello di dominio, ma perché abbiamo a che fare con la loro istanziazione e costruttori complessi nel codice.

È ancora peggio quando le persone prendono l'idea dell'immutabilità troppo in un linguaggio generico come Java o C #, rendendo tutto immutabile. Quindi, come risultato, le persone impongono costrutti di espressioni S in lingue che non supportano tali elementi con facilità.

L'ingegneria è l'atto di modellare attraverso compromessi e compromessi. Rendere tutto immutabile dall'editto perché qualcuno ha letto che tutto è immutabile nel linguaggio funzionale X o Y (un modello di programmazione completamente diverso), che non è accettabile. Non è una buona ingegneria.

Le piccole cose, possibilmente unitarie, possono essere rese immutabili. Cose più complesse possono essere rese immutabili quando ha senso . Ma l'immutabilità non è una pallottola d'argento. La capacità di ridurre i bug, aumentare la scalabilità e le prestazioni, quelli non sono la sola funzione dell'immutabilità. È una funzione delle pratiche ingegneristiche appropriate . Dopo tutto, le persone hanno scritto un software buono e scalabile senza immutabilità.

L'immutabilità diventa un onere molto veloce (si aggiunge alla complessità accidentale) se viene eseguito senza una ragione, quando viene eseguito al di fuori di ciò che ha senso nel contesto di un modello di dominio.

Io, per esempio, cerco di evitarlo (a meno che non stia lavorando in un linguaggio di programmazione con un buon supporto sintattico per questo).

    
risposta data 14.04.2011 - 16:48
fonte
13

Ho attraversato una fase in cui insistevo sul fatto che le classi fossero immutabili ove possibile. Avevo costruttori per praticamente tutto, matrici immutabili, ecc. Ecc. Ho trovato che la risposta alla tua domanda è semplice: A che punto le classi immutabili diventano un peso? Molto rapidamente. Non appena vuoi serializzare qualcosa, devi essere in grado di deserializzare, il che significa che deve essere mutabile; non appena si desidera utilizzare un ORM, molti di loro insistono sulla possibilità di modificare le proprietà. E così via.

Alla fine ho sostituito quella politica con interfacce immutabili a oggetti mutabili.

class NamedThing : INamedThing
{
    private string _name;    
    public NamedThing(string name)
    {
        _name = name;
    }    

    public NamedThing(NamedThing other)
    {
        this._name = other._name;
    }

    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }
}

interface INamedThing
{
    string Name { get; }
}

Ora l'oggetto ha flessibilità, ma puoi ancora dire al codice chiamante che non dovrebbe modificare queste proprietà.

    
risposta data 14.04.2011 - 13:25
fonte
8

Non penso che ci sia una risposta generale a questo. Più una classe è complessa, più è difficile ragionare sui suoi cambiamenti di stato e più è costoso crearne di nuove. Quindi al di sopra di un livello (personale) di complessità diventerà troppo doloroso per rendere / mantenere una classe immutabile.

Tieni presente che una classe troppo complessa o un lungo elenco di parametri del metodo sono odori di progettazione di per sé, indipendentemente dall'immutabilità.

Quindi di solito la soluzione preferita sarebbe quella di suddividere una classe in più classi distinte, ognuna delle quali può essere resa mutevole o immutabile da sola. Se questo non è fattibile, può essere disattivato.

    
risposta data 14.04.2011 - 12:21
fonte
5

Puoi evitare il problema di copia se archivi tutti i tuoi campi immutabili in un struct interno. Questa è fondamentalmente una variazione del modello di memento. Quindi, quando vuoi fare una copia, copia il promemoria:

class MyClass
{
    struct Memento
    {
        public int field1;
        public string field2;
    }

    private readonly Memento memento;

    public MyClass(int field1, string field2)
    {
        this.memento = new Memento()
            {
                field1 = field1,
                field2 = field2
            };
    }

    private MyClass(Memento memento) // for copying
    {
        this.memento = memento;
    }

    public int Field1 { get { return this.memento.field1; } }
    public string Field2 { get { return this.memento.field2; } }

    public MyClass WithNewField1(int newField1)
    {
        Memento newMemento = this.memento;
        newMemento.field1 = newField1;
        return new MyClass(newMemento);
    }
}
    
risposta data 14.04.2011 - 14:05
fonte
3

Hai un paio di cose al lavoro qui. I set di dati immutabili sono ottimi per la scalabilità multithread. In sostanza, puoi ottimizzare la tua memoria un po 'in modo che un set di parametri sia una sola istanza della classe, ovunque. Poiché gli oggetti non cambiano mai, non devi preoccuparti di sincronizzare l'accesso ai suoi membri. È una buona cosa. Tuttavia, come fai notare, più l'oggetto è complesso più hai bisogno di alcune mutabilità. Vorrei iniziare con il ragionamento su queste linee:

  • C'è qualche motivo commerciale per cui un oggetto può cambiare il suo stato? Ad esempio, un oggetto utente memorizzato in un database è univoco in base al suo ID, ma deve essere in grado di cambiare lo stato nel tempo. D'altra parte quando cambi le coordinate su una griglia, cessa di essere la coordinata originale e quindi ha senso rendere le coordinate immutabili. Lo stesso con le stringhe.
  • È possibile calcolare alcuni attributi? In breve, se gli altri valori nella nuova copia di un oggetto sono una funzione di alcuni valori fondamentali che si passano, è possibile calcolarli nel costruttore o su richiesta. Ciò riduce la quantità di manutenzione in quanto è possibile inizializzare tali valori allo stesso modo su copia o creazione.
  • Quanti valori costituiscono il nuovo oggetto immutabile? Ad un certo punto la complessità della creazione di un oggetto diventa non banale e, a quel punto, avere più istanze dell'oggetto può diventare un problema. Gli esempi includono strutture ad albero immutabili, oggetti con più di tre parametri passati, ecc. Più parametri ci sono, maggiore è la possibilità di incasinare l'ordine dei parametri o di azzerare quello sbagliato.

Nelle lingue che supportano solo oggetti immutabili (come Erlang), se c'è qualche operazione che sembra modificare lo stato di un oggetto immutabile, il risultato finale è una nuova copia dell'oggetto con il valore aggiornato. Ad esempio, quando aggiungi un elemento a un vettore / elenco:

myList = lists:append([[1,2,3], [4,5,6]])
% myList is now [1,2,3,4,5,6]

Questo può essere un modo sensato di lavorare con oggetti più complicati. Ad esempio, aggiungendo un nodo ad albero, il risultato è un nuovo albero con il nodo aggiunto. Il metodo nell'esempio precedente restituisce un nuovo elenco. Nell'esempio in questo paragrafo, tree.add(newNode) restituirebbe un nuovo albero con il nodo aggiunto. Per gli utenti, diventa facile lavorare con. Per gli scrittori della biblioteca diventa noioso quando la lingua non supporta la copia implicita. Quella soglia dipende dalla tua stessa pazienza. Per gli utenti della tua libreria, il limite più sano che ho trovato è di circa tre o quattro parametri top.

    
risposta data 14.04.2011 - 14:28
fonte
0

Se hai più membri finali della classe e non vuoi che vengano esposti a tutti gli oggetti che hanno bisogno di crearlo, puoi utilizzare il modello di builder:

class NamedThing
{
    private string _name;    
    private string _value;
    private NamedThing(string name, string value)
    {
        _name = name;
        _value = value;
    }    
    public NamedThing(NamedThing other)
    {
        this._name = other._name;
        this._value = other._value;
    }
    public string Name
    {
        get { return _name; }
    }

    public static class Builder {
        string _name;
        string _value;

        public void setValue(string value) {
            _value = value;
        }
        public void setName(string name) {
            _name = name;
        }
        public NamedThing newObject() {
            return new NamedThing(_name, _value);
        }
    }
}

il vantaggio è che puoi facilmente creare un nuovo oggetto con solo un valore diverso di un nome diffuso.

    
risposta data 14.04.2011 - 13:18
fonte
0

So at what point does a class become too complex to be immutable?

Secondo me non vale la pena preoccuparsi di rendere le classi piccole immutabili in lingue come quella che stai mostrando. Sto usando piccolo qui e non complesso , perché anche se aggiungi dieci campi a quella classe e sono davvero fantasiose operazioni su di essi, dubito che prenderà kilobyte per non parlare dei megabytes, tanto meno dei gigabyte, quindi qualsiasi funzione che usi le istanze della tua classe può semplicemente creare una copia economica dell'intero oggetto per evitare di modificare l'originale se vuole evitare di causare effetti collaterali esterni.

Strutture dati persistenti

Dove trovo l'uso personale per l'immutabilità è per le grandi strutture dati centrali che aggregano un mucchio di dati teeny come le istanze della classe che stai mostrando, come quella che memorizza un milione di% diNamedThings. Appartenendo a una struttura dati persistente che è immutabile e che sta dietro un'interfaccia che consente solo l'accesso di sola lettura, gli elementi che appartengono al contenitore diventano immutabili senza che la classe dell'elemento ( NamedThing ) debba occuparsene.

Copie economiche

La struttura persistente dei dati consente di trasformare e rendere uniche le regioni, evitando modifiche all'originale senza dover copiare la struttura dei dati nella sua interezza. Questa è la vera bellezza di ciò. Se volessi ingenuamente scrivere funzioni che evitassero effetti collaterali che introducono una struttura dati che richiede gigabyte di memoria e modifica solo il valore di un megabyte di memoria, allora dovresti copiare l'intera cosa fottuta per evitare di toccare l'input e restituire un nuovo produzione. È in grado di copiare gigabyte per evitare effetti collaterali o causare effetti collaterali in tale scenario, rendendo necessario scegliere tra due scelte spiacevoli.

Con una struttura dati persistente, ti permette di scrivere una tale funzione ed evitare di fare una copia dell'intera struttura dati, richiedendo solo circa un megabyte di memoria extra per l'output se la tua funzione ha solo trasformato il valore di un megabyte di memoria.

Burden

Per quanto riguarda il peso, ce n'è uno immediato, almeno nel mio caso. Ho bisogno di quei costruttori di cui si parla o di "transitori" come li chiamo per essere in grado di esprimere efficacemente le trasformazioni a quella massiccia struttura di dati senza toccarla. Codice come questo:

void transform_stuff(MutList<Stuff>& stuff, int first, int last)
{
     // Transform stuff in the range, [first, last).
     for (; first != last; ++first)
          transform(stuff[first]);
}

... quindi deve essere scritto in questo modo:

ImmList<Stuff> transform_stuff(ImmList<Stuff> stuff, int first, int last)
{
     // Grab a "transient" (builder) list we can modify:
     TransientList<Stuff> transient(stuff);

     // Transform stuff in the range, [first, last)
     // for the transient list.
     for (; first != last; ++first)
          transform(transient[first]);

     // Commit the modifications to get and return a new
     // immutable list.
     return stuff.commit(transient);
}

Ma in cambio di queste due righe di codice aggiuntive, la funzione ora è sicura per chiamare i thread con lo stesso elenco originale, non causa effetti collaterali, ecc. Inoltre rende davvero facile rendere questa operazione un utente annullabile azione dal momento che l'annullamento può solo memorizzare una copia poco costoso della vecchia lista.

Eccezione: sicurezza o ripristino degli errori

Non tutti potrebbero trarne beneficio quanto da strutture di dati persistenti in contesti come questi (ho trovato così utile per loro nei sistemi di annullamento e nell'editing non distruttivo che sono concetti centrali nel mio dominio VFX), ma una cosa applicabile a quasi tutti quelli da considerare è exception-safety o recovery error .

Se vuoi rendere la funzione di muting originale eccezionalmente sicura, allora ha bisogno della logica di rollback, per la quale l'implementazione più semplice richiede la copia dell'elenco intero :

void transform_stuff(MutList<Stuff>& stuff, int first, int last)
{
    // Make a copy of the whole massive gigabyte-sized list 
    // in case we encounter an exception and need to rollback
    // changes.
    MutList<Stuff> old_stuff = stuff;

    try
    {
         // Transform stuff in the range, [first, last).
         for (; first != last; ++first)
             transform(stuff[first]);
    }
    catch (...)
    {
         // If the operation failed and ran into an exception,
         // swap the original list with the one we modified
         // to "undo" our changes.
         stuff.swap(old_stuff);
         throw;
    }
}

A questo punto la versione mutabile eccezionalmente sicura è ancora più dispendiosa dal punto di vista computazionale e forse anche più difficile da scrivere correttamente rispetto alla versione immutabile che usa un "builder". E molti sviluppatori C ++ trascurano la sicurezza delle eccezioni e forse questo va bene per il loro dominio, ma nel mio caso mi piace assicurarmi che il mio codice funzioni correttamente anche in caso di un'eccezione (anche test di scrittura che deliberatamente generano eccezioni per testare l'eccezione sicurezza), e questo lo rende così che devo essere in grado di eseguire il rollback di qualsiasi effetto collaterale che una funzione causa a metà nella funzione se qualcosa genera.

Quando vuoi essere protetto da eccezioni e recuperare dagli errori con garbo senza il crash e la masterizzazione delle applicazioni, devi annullare / annullare eventuali effetti collaterali che una funzione può causare in caso di errore / eccezione. E lì il costruttore può effettivamente risparmiare più tempo per il programmatore di quanto costi con il tempo di calcolo perché: ...

You don't have to worry about rolling back side effects in a function which doesn't cause any!

Quindi torna alla domanda fondamentale:

At what point do immutable classes become a burden?

Sono sempre un peso nei linguaggi che ruotano maggiormente attorno alla mutabilità rispetto all'immutabilità, motivo per cui penso che dovresti usarli laddove i benefici superino significativamente i costi. Ma a un livello abbastanza ampio per strutture di dati abbastanza grandi, credo che ci siano molti casi in cui è un compromesso degno.

Anche nella mia, ho solo alcuni tipi di dati immutabili, e sono tutte enormi strutture di dati destinate a memorizzare un numero enorme di elementi (pixel di un'immagine / trama, entità e componenti di un ECS e vertici / bordi / poligoni di una mesh).

    
risposta data 14.12.2017 - 02:36
fonte

Leggi altre domande sui tag