Come si decide se un tipo di oggetto dati deve essere progettato per essere immutabile?


23

Adoro l'immutabile "modello" per i suoi punti di forza, e in passato ho trovato utile progettare sistemi con tipi di dati immutabili (alcuni, quasi tutti o addirittura tutti). Spesso quando lo faccio, mi ritrovo a scrivere meno bug e il debug è molto più semplice.

Tuttavia, i miei colleghi in generale si allontanano da immutabili. Non sono affatto inesperti (tutt'altro), tuttavia scrivono oggetti di dati nel modo classico: membri privati ​​con un getter e un setter per ogni membro. Quindi di solito i loro costruttori non prendono argomenti, o forse prendono solo alcuni argomenti per comodità. Molto spesso, la creazione di un oggetto è simile al seguente:

Foo a = new Foo();
a.setProperty1("asdf");
a.setProperty2("bcde");

Forse lo fanno ovunque. Forse non definiscono nemmeno un costruttore che prende quelle due stringhe, non importa quanto siano importanti. E forse non cambiano il valore di quelle stringhe in seguito e non ne hanno mai bisogno. Chiaramente se quelle cose fossero vere, l'oggetto sarebbe meglio progettato come immutabile, giusto? (il costruttore accetta le due proprietà, nessun setter).

Come decidi se un tipo di oggetto deve essere progettato come immutabile? Esiste un buon insieme di criteri per giudicarlo?

Attualmente sto discutendo se cambiare alcuni tipi di dati nel mio progetto su immutabili, ma dovrei giustificarlo ai miei colleghi, e i dati nei tipi potrebbero (MOLTO raramente) cambiare - a quel punto puoi ovviamente cambiare in modo immutabile (creane uno nuovo, copiando le proprietà dal vecchio oggetto tranne quelle che si desidera modificare). Ma non sono sicuro se questo è solo il mio amore per gli immutabili che si manifestano, o se c'è un reale bisogno / beneficio da loro.


1
Programma a Erlang e l'intero problema è risolto, tutto è immutabile
Zachary K,

1
@ZacharyK In realtà stavo pensando di menzionare qualcosa sulla programmazione funzionale, ma mi sono trattenuto a causa della mia esperienza limitata con i linguaggi di programmazione funzionale.
Ricket,

Risposte:


27

Sembra che ti stai avvicinando all'indietro. Uno dovrebbe essere predefinito immutabile. Rendi mutevole un oggetto solo se devi assolutamente / semplicemente non farlo funzionare come oggetto immutabile.


11

Il vantaggio principale di oggetti immutabili è la sicurezza della filettatura. In un mondo in cui più core e thread sono la norma, questo vantaggio è diventato molto importante.

Ma usare oggetti mutabili è molto conveniente. Si comportano bene e finché non li modifichi da thread separati e hai una buona comprensione di ciò che stai facendo, sono abbastanza affidabili.


15
Il vantaggio di oggetti immutabili è che sono più facili da ragionare. In un ambiente multithread, questo è terribilmente più semplice; in semplici algoritmi a thread singolo, è ancora più semplice. Ad esempio, non devi mai preoccuparti se lo stato è coerente, l'oggetto ha superato tutte le mutazioni necessarie per essere utilizzato in un determinato contesto, ecc. I migliori oggetti mutabili ti permettono anche di preoccuparti poco del loro stato esatto: ad esempio cache.
9000

-1, sono d'accordo con @ 9000. La sicurezza del thread è secondaria (considerare gli oggetti che appaiono immutabili ma che hanno uno stato mutabile interno a causa, ad esempio, della memoizzazione). Inoltre, il miglioramento delle prestazioni mutabili è l'ottimizzazione prematura ed è possibile difendere qualsiasi cosa se si richiede che l'utente "sappia cosa stanno facendo". Se sapessi sempre cosa stavo facendo, non scriverei mai un programma con un bug.
Doval,

4
@Doval: non c'è nulla di cui non essere d'accordo. 9000 è assolutamente corretto; gli oggetti immutabili sono più facili da ragionare. Questo è in parte ciò che li rende così utili per la programmazione concorrente. L'altra parte è che non devi preoccuparti del cambiamento di stato. L'ottimizzazione prematura è irrilevante qui; l'uso di oggetti immutabili riguarda la progettazione, non le prestazioni, e la scarsa scelta delle strutture di dati in anticipo è la pessimizzazione prematura.
Robert Harvey,

@RobertHarvey Non sono sicuro di cosa intendi per "scarsa scelta della struttura dei dati in anticipo". Esistono versioni immutabili della maggior parte delle strutture di dati mutabili che offrono prestazioni simili. Se hai bisogno di un elenco e hai la possibilità di utilizzare un elenco immutabile e un elenco mutabile, vai con quello immutabile fino a quando non sai per certo che è un collo di bottiglia nella tua applicazione e la versione mutabile funzionerà meglio.
Doval,

2
@Doval: ho letto Okasaki. Tali strutture di dati non vengono comunemente utilizzate a meno che non si stia utilizzando un linguaggio che supporti pienamente il paradigma funzionale, come Haskell o Lisp. E contesto l'idea che le strutture immutabili sono la scelta predefinita; la stragrande maggioranza dei sistemi informatici aziendali è ancora progettata attorno a strutture mutabili (ovvero database relazionali). Iniziare con strutture di dati immutabili è una buona idea, ma è ancora una torre molto d'avorio.
Robert Harvey,

6

Esistono due modi principali per decidere se un oggetto è immutabile.

a) Basato sulla natura dell'oggetto

È facile cogliere queste situazioni perché sappiamo che questi oggetti non cambieranno dopo che è stato costruito. Ad esempio se hai unRequestHistory un'entità e per natura le entità della storia non cambiano una volta che è stata costruita. Questi oggetti possono essere progettati direttamente come classi immutabili. Tieni presente che l'oggetto richiesta è modificabile in quanto può cambiare il suo stato e a chi è stato assegnato ecc. Nel tempo ma la cronologia delle richieste non cambia. Ad esempio, c'è stato un elemento storico creato la settimana scorsa quando è passato dallo stato inviato a quello assegnato E questa cronologia non può mai cambiare. Quindi questo è un classico caso immutabile.

b) In base alla scelta del progetto, a fattori esterni

Questo è simile all'esempio java.lang.String. Le stringhe possono effettivamente cambiare nel tempo ma, in base alla progettazione, l'hanno resa immutabile a causa di fattori di cache / pool di stringhe / concorrenza. Similmente la memorizzazione nella cache / concorrenza, ecc., Può svolgere un ruolo importante nel rendere immutabile un oggetto se la memorizzazione nella cache / concorrenza e le prestazioni correlate sono fondamentali nell'applicazione. Ma questa decisione dovrebbe essere presa con molta attenzione dopo aver analizzato tutti gli impatti.

Il vantaggio principale degli oggetti immutabili è che non sono soggetti a un modello di erbaccia. L'oggetto non rileva alcun cambiamento nel corso della vita e rende la codifica e la manutenzione molto più facili.


4

Attualmente sto discutendo se cambiare alcuni tipi di dati nel mio progetto su immutabili, ma dovrei giustificarlo ai miei colleghi, e i dati nei tipi potrebbero (MOLTO raramente) cambiare - a quel punto puoi ovviamente cambiare in modo immutabile (creane uno nuovo, copiando le proprietà dal vecchio oggetto tranne quelle che si desidera modificare).

Ridurre al minimo lo stato di un programma è estremamente vantaggioso.

Chiedi loro se desiderano utilizzare un tipo di valore modificabile in una delle tue classi per l'archiviazione temporanea da una classe client.

Se dicono di sì, chiedi perché? Lo stato mutevole non appartiene a uno scenario come questo. Costringerli a creare lo stato a cui appartiene effettivamente e rendere la statefulness dei tipi di dati il ​​più espliciti possibile sono ottimi motivi.


4

La risposta dipende in qualche modo dalla lingua. Il tuo codice assomiglia a Java, dove questo problema è il più difficile possibile. In Java, gli oggetti possono essere passati solo per riferimento e il clone è completamente rotto.

Non esiste una risposta semplice, ma per certo si desidera rendere immutabili oggetti di valore ridotto. Java ha reso correttamente stringhe immutabili, ma ha erroneamente reso mutabili data e calendario.

Quindi, sicuramente rendere immutabili gli oggetti di piccolo valore e implementare un costruttore di copie. Dimentica tutto di Cloneable, è progettato così male che è inutile.

Per oggetti di valore più grande, se è scomodo renderli immutabili, renderli facili da copiare.


Sembra molto come scegliere tra stack e heap quando si scrive C o C ++ :)
Ricket,

@Ricket: non tanto IMO. Stack / heap dipende dalla durata dell'oggetto. È molto comune in C ++ avere oggetti mutabili nello stack.
Kevin Cline,

1

E forse non cambiano il valore di quelle stringhe in seguito e non ne hanno mai bisogno. Chiaramente se quelle cose fossero vere, l'oggetto sarebbe meglio progettato come immutabile, giusto?

Un po 'controintuitivamente, non aver mai bisogno di cambiare le stringhe in seguito è un argomento abbastanza valido che non importa se gli oggetti sono immutabili o meno. Il programmatore li sta già trattando come effettivamente immutabili indipendentemente dal fatto che il compilatore lo imponga o meno.

L'immutabilità di solito non fa male, ma non aiuta neanche. Il modo semplice per capire se il tuo oggetto potrebbe trarre beneficio dall'immutabilità è se hai mai bisogno di fare una copia dell'oggetto o acquisire un mutex prima di cambiarlo . Se non cambia mai, l'immutabilità non ti compra nulla e talvolta rende le cose più complicate.

È fare avere un buon punto circa il rischio di costruire un oggetto in uno stato non valido, ma che è in realtà una questione separata da immutabilità. Un oggetto può essere sia mutabile che sempre in uno stato valido dopo la costruzione.

L'eccezione a questa regola è che poiché Java non supporta né parametri nominati né parametri predefiniti, a volte può diventare ingombrante progettare una classe che garantisce un oggetto valido solo usando costruttori sovraccarichi. Non tanto con il caso a due proprietà, ma poi c'è qualcosa da dire anche per coerenza, se quel modello è frequente con classi simili ma più grandi in altre parti del codice.


Trovo curioso che le discussioni sulla mutabilità non riescano a riconoscere la relazione tra immutabilità e i modi in cui un oggetto può essere esposto o condiviso in sicurezza. Un riferimento a un oggetto di classe profondamente immutabile può essere tranquillamente condiviso con codice non attendibile. Un riferimento a un'istanza di una classe mutabile può essere condiviso se a tutti coloro che detengono un riferimento è possibile non fidarsi mai di modificare l'oggetto o esporlo a codice che potrebbe farlo. Un riferimento a un'istanza di classe che può essere mutata non dovrebbe in genere essere condiviso affatto. Se esiste un solo riferimento a un oggetto in qualsiasi parte dell'universo ...
supercat

... e nulla ha richiesto il suo "hashcode di identità", lo ha usato per bloccare, o altrimenti ha avuto accesso alle sue " Objectcaratteristiche nascoste ", quindi cambiare direttamente un oggetto non sarebbe semanticamente diverso dalla sovrascrittura del riferimento con un riferimento a un nuovo oggetto che era identico ma per la modifica indicata. Uno dei maggiori punti deboli semantici di Java, IMHO, è che non ha alcun mezzo con cui il codice possa indicare che una variabile dovrebbe essere l'unico riferimento non effimero a qualcosa; i riferimenti dovrebbero essere passabili solo a metodi che non possono possibilmente conservare una copia dopo il loro ritorno.
supercat

0

Potrei avere una visione di livello troppo basso di questo e probabilmente perché sto usando C e C ++ che non rendono esattamente così semplice rendere tutto immutabile, ma vedo i tipi di dati immutabili come un dettaglio di ottimizzazione per scrivere di più funzioni efficienti prive di effetti collaterali e in grado di fornire facilmente funzioni come i sistemi di annullamento e l'editing non distruttivo.

Ad esempio, questo potrebbe essere enormemente costoso:

/// @return A new mesh whose vertices have been transformed
/// by the specified transformation matrix.
Mesh transform(Mesh mesh, Matrix4f matrix);

... Se Mesh non è progettato per essere una struttura di dati persistente ed era, invece, un tipo di dati che richiedeva la copia completa (che poteva estendersi su gigabyte in alcuni scenari) anche se tutto ciò che faremo è cambiare un parte di esso (come nello scenario sopra in cui modificiamo solo le posizioni dei vertici).

Quindi è quando raggiungo l'immutabilità e progetto la struttura dei dati per consentire che parti non modificate di esso vengano copiate in modo superficiale e contate come riferimento, per consentire alla funzione di cui sopra di essere ragionevolmente efficiente senza dover copiare in profondità intere mesh mentre è ancora in grado di scrivere il essere privo di effetti collaterali che semplifica notevolmente la sicurezza dei thread, la sicurezza delle eccezioni, la possibilità di annullare l'operazione, applicarla in modo non distruttivo, ecc.

Nel mio caso è troppo costoso (almeno dal punto di vista della produttività) per rendere tutto immutabile, quindi lo conservo per le classi che sono troppo costose per essere copiate per intero. Quelle classi sono generalmente strutture di dati pesanti come mesh e immagini e generalmente utilizzo un'interfaccia mutabile per esprimere le modifiche ad esse attraverso un oggetto "builder" per ottenere una nuova copia immutabile. E non lo sto facendo così tanto per cercare di ottenere garanzie immutabili a livello centrale della classe, tanto quanto aiutarmi a usare la classe in funzioni che possono essere prive di effetti collaterali. Il mio desiderio di rendere Meshimmutabile sopra non è direttamente quello di rendere immutabili le mesh, ma di consentire di scrivere facilmente funzioni prive di effetti collaterali che introducono una mesh e ne producono una nuova senza pagare una memoria enorme e costi di calcolo in cambio.

Di conseguenza ho solo 4 tipi di dati immutabili in tutta la mia base di codice, e sono tutte strutture di dati pesanti, ma li uso pesantemente per aiutarmi a scrivere funzioni prive di effetti collaterali. Questa risposta potrebbe essere applicabile se, come me, stai lavorando in una lingua che non rende così facile rendere tutto immutabile. In tal caso, puoi spostare la tua attenzione verso il fatto che la maggior parte delle tue funzioni eviti gli effetti collaterali, a quel punto potresti voler rendere immutabili strutture di dati selezionate, tipi di PDS, come dettaglio di ottimizzazione per evitare le costose copie complete. Nel frattempo quando ho una funzione come questa:

/// @return The v1 * v2.
Vector3f vec_mul(Vector3f v1, Vector3f v2);

... quindi non ho la tentazione di rendere immutabili i vettori poiché sono abbastanza economici da copiarli per intero. Non è possibile ottenere vantaggi in termini di prestazioni trasformando i vettori in strutture immutabili in grado di copiare superficialmente parti non modificate. Tali costi supererebbero il costo della sola copia dell'intero vettore.


-1
Foo a = new Foo();
a.setProperty1("asdf");
a.setProperty2("bcde");

Se Foo ha solo due proprietà, allora è facile scrivere un costruttore che accetta due argomenti. Supponiamo di aggiungere un'altra proprietà. Quindi devi aggiungere un altro costruttore:

public Foo(String a, String b, SomethingElse c)

A questo punto, è ancora gestibile. E se ci fossero 10 proprietà? Non vuoi un costruttore con 10 argomenti. È possibile utilizzare il modello builder per costruire istanze, ma ciò aggiunge complessità. A questo punto, penserai "perché non ho semplicemente aggiunto setter per tutte le proprietà come fanno le persone normali"?


4
Un costruttore con dieci argomenti è peggiore di NullPointerException che si verifica quando non è stato impostato property7? Il modello del builder è più complesso del codice per verificare le proprietà non inizializzate in ciascun metodo? Meglio controllare una volta quando l'oggetto è costruito.
Kevin Cline,

2
Come è stato detto decenni fa proprio per questo caso, "Se hai una procedura con dieci parametri, probabilmente ne hai perso alcuni".
9000

1
Perché non vorresti un costruttore con 10 argomenti? A che punto traccia la linea? Penso che un costruttore con 10 argomenti sia un piccolo prezzo da pagare per i benefici dell'immutabilità. Inoltre, o hai una riga con 10 cose separate da virgole (opzionalmente distribuite in più righe o anche 10 righe, se sembra migliore), o hai comunque 10 righe mentre imposti ciascuna proprietà singolarmente ...
Ricket

1
@Ricket: perché aumenta il rischio di mettere gli argomenti nell'ordine sbagliato . Se stai usando setter, o il modello del costruttore, è abbastanza improbabile.
Mike Baranczak,

4
Se hai un costruttore con 10 argomenti, forse è il momento di pensare di incapsulare quegli argomenti in una classe (o classi) di loro e / o dare un'occhiata critica al tuo design di classe.
Adam Lear
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.