Immutabilità completa e programmazione orientata agli oggetti


43

Nella maggior parte dei linguaggi OOP, gli oggetti sono generalmente mutabili con una serie limitata di eccezioni (come ad esempio tuple e stringhe in Python). Nella maggior parte dei linguaggi funzionali, i dati sono immutabili.

Sia gli oggetti mutabili che quelli immutabili portano un intero elenco di vantaggi e svantaggi.

Ci sono lingue che provano a sposare entrambi i concetti come ad esempio scala in cui si hanno (dichiarati esplicitamente) dati mutabili e immutabili (per favore correggimi se sbaglio, la mia conoscenza di scala è più che limitata).

La mia domanda è: Ha completa (sic!) Immutabilità -cioè nessun oggetto può mutare una volta che è stato created- alcun senso in un contesto OOP?

Esistono progetti o implementazioni di tale modello?

Fondamentalmente, l'immutabilità (completa) e gli OOP sono opposti o ortogonali?

Motivazione: In OOP normalmente operi sui dati, modificando (mutando) le informazioni sottostanti, mantenendo i riferimenti tra tali oggetti. Ad esempio un oggetto di classe Personcon un membro che fa fatherriferimento a un altro Personoggetto. Se si modifica il nome del padre, questo è immediatamente visibile all'oggetto figlio senza necessità di aggiornamento. Essendo immutabile, dovrai costruire nuovi oggetti per padre e figlio. Ma avresti molto meno kerfuffle con oggetti condivisi, multi-threading, GIL, ecc.


2
L'immutabilità può essere simulata in un linguaggio OOP, esponendo solo i punti di accesso agli oggetti come metodi o proprietà di sola lettura che non mutano i dati. L'immutabilità funziona allo stesso modo nei linguaggi OOP come in qualsiasi linguaggio funzionale, tranne per il fatto che potrebbero mancare alcune funzionalità del linguaggio funzionale.
Robert Harvey,

5
La mutabilità non è una proprietà dei linguaggi OOP come C # e Java, né l'immutabilità. Si specifica la mutabilità o l'immutabilità dal modo in cui si scrive la classe.
Robert Harvey,

9
La tua presunzione sembra essere che la mutabilità sia una caratteristica fondamentale dell'orientamento agli oggetti. Non lo è. La mutabilità è semplicemente una proprietà di oggetti o valori. L'orientamento agli oggetti comprende una serie di concetti intrinseci (incapsulamento, polimorfismo, eredità, ecc.) Che hanno poco o nulla a che fare con la mutazione, e si trarrebbero comunque i benefici di quelle caratteristiche. anche se hai reso tutto immutabile.
Robert Harvey,

2
@MichaelT La domanda non è di rendere mutabili cose specifiche, ma di rendere immutabili tutte le cose.

Risposte:


43

OOP e immutabilità sono quasi completamente ortogonali tra loro. Tuttavia, la programmazione imperativa e l'immutabilità non lo sono.

OOP può essere sintetizzato da due caratteristiche principali:

  • Incapsulamento : non accederò direttamente al contenuto degli oggetti, ma piuttosto comunicherò tramite un'interfaccia specifica ("metodi") con questo oggetto. Questa interfaccia può nascondermi i dati interni. Tecnicamente, questo specifico per la programmazione modulare piuttosto che OOP. L'accesso ai dati tramite un'interfaccia definita equivale all'incirca a un tipo di dati astratto.

  • Invio dinamico : quando chiamo un metodo su un oggetto, il metodo eseguito verrà risolto in fase di esecuzione. (Ad esempio in OOP basato sulla classe, potrei chiamare un sizemetodo su IListun'istanza, ma la chiamata potrebbe essere risolta in un'implementazione in una LinkedListclasse). L'invio dinamico è un modo per consentire il comportamento polimorfico.

L'incapsulamento ha meno senso senza mutabilità (non esiste uno stato interno che potrebbe essere corrotto dall'ingerenza esterna), ma tende ancora a rendere le astrazioni più facili anche quando tutto è immutabile.

Un programma imperativo è costituito da istruzioni eseguite in sequenza. Una dichiarazione ha effetti collaterali come cambiare lo stato del programma. Con l'immutabilità, lo stato non può essere modificato (ovviamente, è possibile creare un nuovo stato). Pertanto, la programmazione imperativa è fondamentalmente incompatibile con l'immutabilità.

Ora succede che OOP è sempre stata storicamente connessa con la programmazione imperativa (Simula è basata su Algol) e tutti i linguaggi OOP tradizionali hanno radici imperative (C ++, Java, C #, ... sono tutti radicati in C). Ciò non implica che la stessa OOP sarebbe imperativa o mutevole, ciò significa solo che l'implementazione della OOP da parte di queste lingue consente la mutabilità.


2
Grazie mille, soprattutto per la definizione delle due caratteristiche principali.
Iperboro

Dynamic Dispatch non è una caratteristica fondamentale di OOP. Né è davvero l'incapsulamento (come hai già ammesso).
Smetti di danneggiare Monica

5
@OrangeDog Sì, non esiste una definizione universalmente accettata di OOP, ma avevo bisogno di una definizione con cui lavorare. Quindi ho scelto qualcosa che fosse il più vicino possibile alla verità senza scrivere un'intera tesi a riguardo. Tuttavia, considero il dispacciamento dinamico come la principale caratteristica distintiva di OOP dagli altri paradigmi. Qualcosa che sembra OOP ma che in realtà ha risolto staticamente tutte le chiamate è in realtà solo una programmazione modulare con polimorfismo ad hoc. Gli oggetti sono una coppia di metodi e dati e sono come tali equivalenti alle chiusure.
amon

2
"Gli oggetti sono un accoppiamento di metodi e dati". Tutto ciò che serve è qualcosa che non ha nulla a che fare con l'immutabilità.
Smetti di fare del male a Monica il

3
@CodeYogi L'occultamento dei dati è il tipo più comune di incapsulamento. Tuttavia, il modo in cui i dati vengono archiviati internamente da un oggetto non è l'unico dettaglio di implementazione che dovrebbe essere nascosto. È altrettanto importante nascondere il modo in cui viene implementata l'interfaccia pubblica, ad esempio se utilizzo metodi di supporto. Tali metodi di supporto dovrebbero anche essere privati, in generale. Riassumendo: l'incapsulamento è un principio, mentre nascondere i dati è una tecnica di incapsulamento.
amon,

25

Nota, c'è una cultura tra i programmatori orientati agli oggetti in cui le persone assumono se stai facendo OOP che la maggior parte dei tuoi oggetti sarà mutabile, ma questo è un problema separato dal fatto che OOP richieda la mutabilità. Inoltre, quella cultura sembra cambiare lentamente verso una maggiore immutabilità, a causa dell'esposizione delle persone alla programmazione funzionale.

Scala è un ottimo esempio del fatto che la mutabilità non è richiesta per l'orientamento agli oggetti. Mentre Scala supporta la mutabilità, il suo uso è scoraggiato. Scala idiomatica è molto orientata agli oggetti e anche quasi del tutto immutabile. Principalmente consente la mutabilità per la compatibilità con Java e perché in determinate circostanze gli oggetti immutabili sono inefficienti o contorti per funzionare.

Confronta un elenco Scala e un elenco Java , ad esempio. L'elenco immutabile di Scala contiene tutti gli stessi metodi oggetto dell'elenco mutabile di Java. Inoltre, poiché Java utilizza funzioni statiche per operazioni come l' ordinamento e Scala aggiunge metodi di tipo funzionale come map. Tutti i tratti distintivi di OOP - incapsulamento, eredità e polimorfismo - sono disponibili in una forma familiare ai programmatori orientati agli oggetti e usati in modo appropriato.

L'unica differenza che vedrai è che quando cambi l'elenco ottieni di conseguenza un nuovo oggetto. Ciò spesso richiede l'utilizzo di modelli di progettazione diversi rispetto a quelli con oggetti mutabili, ma non richiede l'abbandono del tutto OOP.


17

L'immutabilità può essere simulata in un linguaggio OOP, esponendo solo i punti di accesso agli oggetti come metodi o proprietà di sola lettura che non mutano i dati. L'immutabilità funziona allo stesso modo nei linguaggi OOP come in qualsiasi linguaggio funzionale, tranne per il fatto che potrebbero mancare alcune funzionalità del linguaggio funzionale.

La tua presunzione sembra essere che la mutabilità sia una caratteristica fondamentale dell'orientamento agli oggetti. Ma la mutabilità è semplicemente una proprietà di oggetti o valori. L'orientamento agli oggetti comprende una serie di concetti intrinseci (incapsulamento, polimorfismo, eredità, ecc.) Che hanno poco o nulla a che fare con la mutazione, e si trarrebbero comunque i benefici di quelle caratteristiche, anche se si rendesse tutto immutabile.

Non tutti i linguaggi funzionali richiedono l'immutabilità. Clojure ha un'annotazione specifica che consente ai tipi di essere mutabili e la maggior parte dei linguaggi funzionali "pratici" ha un modo per specificare i tipi mutabili.

Una domanda migliore da porsi potrebbe essere "La completa immutabilità ha senso nella programmazione imperativa ?" Direi che l'ovvia risposta a questa domanda è no. Per ottenere la completa immutabilità nella programmazione imperativa, dovresti rinunciare a cose come i forloop (poiché dovresti mutare una variabile loop) a favore della ricorsione, e ora stai essenzialmente programmando in modo funzionale comunque.


Grazie. Potresti per favore elaborare un po 'il tuo ultimo paragrafo ("ovvio" potrebbe essere un po' soggettivo).
Iperboro,

Già fatto ...
Robert Harvey il

1
@Hyperboreus Ci sono molti modi per ottenere il polimorfismo. Sottotipizzazione con invio dinamico, polimorfismo statico ad hoc (aka sovraccarico di funzioni) e polimorfismo parametrico (aka generici) sono i modi più comuni per farlo, e tutti i modi hanno i loro punti di forza e di debolezza. I moderni linguaggi OOP combinano tutti e tre questi modi, mentre Haskell si basa principalmente sul polimorfismo parametrico e sul polimorfismo ad hoc.
amon

3
@RobertHarvey Dici che hai bisogno di mutabilità perché devi fare un ciclo (altrimenti devi usare la ricorsione). Prima di iniziare a usare Haskell due anni fa, pensavo di aver bisogno anche di variabili mutabili. Sto solo dicendo che ci sono altri modi per "eseguire il loop" (mappa, piega, filtro, ecc.). Una volta che prendi il looping dalla tabella, perché altrimenti avresti bisogno di variabili mutabili?
cimmanon,

1
@RobertHarvey Ma questo è esattamente il punto dei linguaggi di programmazione: ciò che ti è esposto e non ciò che sta accadendo sotto il cofano. Quest'ultima è la responsabilità del compilatore o dell'interprete, non dello sviluppatore dell'applicazione. Altrimenti torna all'assemblatore.
Iperboro

5

È spesso utile classificare gli oggetti come valori o entità incapsulanti, con la distinzione che se qualcosa è un valore, il codice che ne contiene un riferimento non dovrebbe mai vedere il suo stato cambiare in alcun modo che il codice stesso non ha avviato. Al contrario, il codice che contiene un riferimento a un'entità può aspettarsi che cambi in modi al di fuori del controllo del detentore del riferimento.

Sebbene sia possibile utilizzare il valore incapsulato utilizzando oggetti di tipo mutabile o immutabile, un oggetto può comportarsi come valore solo se si applica almeno una delle seguenti condizioni:

  1. Nessun riferimento all'oggetto sarà mai esposto a qualsiasi cosa possa cambiare lo stato in esso incapsulato.

  2. Il detentore di almeno uno dei riferimenti all'oggetto conosce tutti gli usi ai quali potrebbe essere assegnato qualsiasi riferimento esistente.

Poiché tutte le istanze di tipi immutabili soddisfano automaticamente il primo requisito, utilizzarle come valori è semplice. Garantire che entrambi i requisiti siano soddisfatti quando si usano tipi mutabili è, al contrario, molto più difficile. Mentre i riferimenti a tipi immutabili possono essere liberamente scambiati come mezzo per incapsulare lo stato in esso incapsulato, passare intorno allo stato memorizzato in tipi mutabili richiede la costruzione di oggetti wrapper immutabili o la copia dello stato incapsulato da oggetti tenuti in privato in altri oggetti che sono fornito o costruito per il destinatario dei dati.

I tipi immutabili funzionano molto bene per il passaggio di valori e spesso sono almeno in qualche modo utilizzabili per manipolarli. Non sono così bravi a gestire entità. La cosa più vicina a un'entità in un sistema con tipi puramente immutabili è una funzione che, dato lo stato del sistema, riporterà gli attributi di una parte di esso o produrrà una nuova istanza di stato del sistema che è come una fornito uno ad eccezione di una parte particolare di esso che sarà diverso in qualche modo selezionabile. Inoltre, se lo scopo di un'entità è di interfacciare del codice con qualcosa che esiste nel mondo reale, potrebbe essere impossibile per l'entità evitare di esporre lo stato mutabile.

Ad esempio, se si ricevono alcuni dati su una connessione TCP, si potrebbe produrre un nuovo oggetto "stato del mondo" che include tali dati nel suo buffer senza influire su alcun riferimento al vecchio "stato del mondo", ma vecchie copie di lo stato mondiale che non include l'ultimo batch di dati sarà difettoso e non dovrebbe essere utilizzato poiché non corrisponderà più allo stato del socket TCP del mondo reale.


4

In c # alcuni tipi sono immutabili come la stringa.

Ciò sembra inoltre suggerire che la scelta è stata fortemente presa in considerazione.

Sicuramente è davvero esigente dalle prestazioni usare tipi immutabili se si deve modificare quel tipo centinaia di migliaia di volte. Questo è il motivo per cui in questi casi si suggerisce di utilizzare la StringBuilderclasse anziché la stringclasse.

Ho fatto un esperimento con un profiler e l'utilizzo del tipo immutabile richiede molta più CPU e RAM.

È anche intuitivo se si considera che per modificare solo una lettera in una stringa di 4000 caratteri è necessario copiare ogni carattere in un'altra area della RAM.


6
La modifica frequente dei dati immutabili non deve essere catastroficamente lenta come con la stringconcatenazione ripetuta . Per praticamente tutti i tipi di dati / casi d'uso, è possibile (spesso è già stata) inventata un'efficace struttura persistente. La maggior parte di questi ha prestazioni pressoché uguali, anche se a volte i fattori costanti sono peggiori.

@delnan Penso anche che l'ultimo paragrafo della risposta riguardi più un dettaglio implementativo che la mutabilità (im).
Hyperboreus,

@Hyperboreus: pensi che dovrei eliminare quella parte? Ma come può cambiare una stringa se è immutabile? Voglio dire .. secondo me, ma di sicuro posso sbagliarmi, questo potrebbe essere il motivo principale per cui gli oggetti non sono immutabili.
Revious

1
@Revious In nessun modo. Lascialo, quindi provoca discussioni e opinioni e punti di vista più interessanti.
Hyperboreus,

1
@Revious Sì, la lettura sarebbe più lenta, anche se non lenta come la modifica di una string(la rappresentazione tradizionale). Una "stringa" (nella rappresentazione di cui sto parlando) dopo 1000 modifiche sarebbe proprio come una stringa appena creata (contenuto del modulo); nessuna struttura di dati persistente utile o ampiamente utilizzata peggiora in termini di qualità dopo le operazioni X. La frammentazione della memoria non è un problema serio (avresti molte allocazioni, sì, ma la frammentazione non è un problema nei moderni garbage collector)

0

La completa immutabilità di tutto non ha molto senso in OOP, o nella maggior parte degli altri paradigmi, per una ragione molto grande:

Ogni programma utile ha effetti collaterali.

Un programma che non fa cambiare nulla è inutile. Potresti anche non averlo nemmeno eseguito, poiché l'effetto sarà identico.

Anche se pensi che non stai cambiando nulla e stai semplicemente riassumendo un elenco di numeri che in qualche modo hai ricevuto, considera che devi fare qualcosa con il risultato: che tu lo stampi sull'output standard, lo scriva in un file, o ovunque. E ciò comporta la mutazione di un buffer e la modifica dello stato del sistema.

Può avere molto senso limitare la mutabilità alle parti che devono essere in grado di cambiare. Ma se assolutamente nulla deve cambiare, allora non stai facendo nulla che valga la pena fare.


4
Non riesco a vedere come la tua risposta si collega alla domanda in quanto non ho affrontato linguaggi funzionali puri. Prendiamo ad esempio erlang: dati immutabili, nessun incarico distruttivo, nessun pericolo per gli effetti collaterali. Inoltre hai lo stato in un linguaggio funzionale, solo che lo stato "scorre" attraverso le funzioni, a differenza delle funzioni che operano sullo stato. Lo stato cambia, ma non cambia in posizione, ma uno stato futuro sostituisce lo stato corrente. L'immutabilità non riguarda la modifica o meno di un buffer di memoria, si tratta di vedere se queste mutazioni sono visibili dall'esterno.
Hyperboreus,

E uno stato futuro sostituisce quello attuale come , esattamente? In un programma OO, quello stato è una proprietà di un oggetto da qualche parte. La sostituzione dello stato richiede la modifica di un oggetto (o la sostituzione con un altro, il che richiede una modifica agli oggetti che si riferiscono ad esso (o la sostituzione con un altro, che ... eh. Ottieni il punto)). Potresti trovare un tipo di hack monadico, in cui ogni azione finisce per creare un'applicazione completamente nuova ... ma anche in questo caso, lo stato corrente del programma deve essere registrato da qualche parte.
cHao,

7
-1. Questo non è corretto Stai confondendo gli effetti collaterali con la mutazione e sebbene siano spesso trattati allo stesso modo dai linguaggi funzionali, sono diversi. Ogni programma utile ha effetti collaterali; non tutti i programmi utili hanno una mutazione.
Michael Shaw,

@Michael: Quando si tratta di OOP, le mutazioni e gli effetti collaterali sono così intrecciati che non possono essere separati realisticamente. Se non hai una mutazione, non puoi avere effetti collaterali senza enormi quantità di hacker.
cHao,


-2

Penso che dipenda dal fatto che la tua definizione di OOP sia che utilizza uno stile di passaggio dei messaggi.

Le funzioni pure non devono mutare nulla perché restituiscono valori che è possibile memorizzare in nuove variabili.

var brandNewVariable = pureFunction(foo);

Con lo stile di passaggio dei messaggi, dici a un oggetto di memorizzare nuovi dati invece di chiedergli quali nuovi dati dovresti memorizzare in una nuova variabile.

sameOldObject.changeMe(foo);

È possibile avere oggetti e non mutarli, rendendo i suoi metodi puri funzioni che vivono all'interno dell'oggetto anziché all'esterno.

var brandNewVariable = nonMutatingObject.askMe(foo);

Ma non è possibile mescolare lo stile di passaggio dei messaggi e oggetti immutabili.

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.