Completa immutabilità e programmazione orientata agli oggetti

41

Nella maggior parte dei linguaggi OOP, gli oggetti sono generalmente modificabili con un insieme limitato 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 linguaggi che cercano di sposare entrambi i concetti come ad es. scala dove hai (esplicitamente dichiarato) dati mutabili e immutabili (correggimi se ho torto, la mia conoscenza di scala è più che limitata).

La mia domanda è: Immutabilità completa (sic!) -i.e. nessun oggetto può mutare una volta creato, avere senso in un contesto OOP?

Esistono progetti o implementazioni di tale modello?

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

Motivazione: In OOP normalmente usi i dati on , modificando (mutando) le informazioni sottostanti, mantenendo i riferimenti tra questi oggetti. Per esempio. un oggetto di classe Person con un membro father che fa riferimento a un altro oggetto Person . Se cambi il nome del padre, questo è immediatamente visibile all'oggetto figlio senza necessità di aggiornamento. Essendo immutabile avresti bisogno di costruire nuovi oggetti sia per il padre che per il bambino. Ma avresti molto meno kerfuffle con oggetti condivisi, multi-threading, GIL, ecc.

    
posta Hyperboreus 17.03.2014 - 20:10
fonte

7 risposte

41

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

OOP può essere riassunto da due funzioni principali:

  • Incapsulamento : non accedo direttamente ai contenuti degli oggetti, ma piuttosto comunico tramite un'interfaccia specifica ("metodi") con questo oggetto. Questa interfaccia può nascondere i dati interni da me. Tecnicamente, questo specifico per la programmazione modulare piuttosto che OOP. L'accesso ai dati tramite un'interfaccia definita equivale approssimativamente a un tipo di dati astratto.

  • Dynamic Dispatch : 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 metodo size su un'istanza IList , ma la chiamata potrebbe essere risolta in un'implementazione in una classe LinkedList ). La spedizione dinamica è un modo per consentire il comportamento polimorfico.

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

Un programma imperativo consiste in istruzioni che vengono eseguite in sequenza. Una dichiarazione ha effetti collaterali come cambiare lo stato del programma. Con immutabilità, lo stato non può essere modificato (ovviamente, potrebbe essere creato uno stato nuovo ). Pertanto, la programmazione imperativa è fondamentalmente incompatibile con l'immutabilità.

Ora accade che OOP è sempre stato storicamente connesso alla programmazione imperativa (Simula è basato su Algol), e tutti i principali linguaggi OOP hanno radici imperative (C ++, Java, C #, ... sono tutti radicati in C). Ciò non implica che OOP stesso sia imperativo o mutabile, questo significa solo che l'implementazione di OOP da parte di questi linguaggi consente la mutevolezza.

    
risposta data 17.03.2014 - 20:54
fonte
25

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

Scala è una buona illustrazione che la mutevolezza non è richiesta per l'orientamento all'oggetto. Sebbene Scala supporti la mutabilità , il suo utilizzo è scoraggiato. Idiomatic Scala è molto orientato agli oggetti e anche quasi interamente immutabile. Permette soprattutto la mutabilità per compatibilità con Java, e perché in certe circostanze gli oggetti immutabili sono inefficienti o complicati con cui lavorare.

Confronta un elenco Scala e un elenco Java , ad esempio. L'elenco immutabile di Scala contiene tutti gli stessi metodi degli oggetti dell'elenco mutabile di Java. Di più, infatti, poiché Java utilizza funzioni statiche per operazioni come ordina e Scala aggiunge metodi in stile funzionale come map . Tutti i tratti distintivi di incapsulamento OOP, ereditarietà e polimorfismo sono disponibili in una forma familiare ai programmatori orientati agli oggetti e utilizzati in modo appropriato.

L'unica differenza che vedrai è quando cambi la lista in cui ottieni un nuovo oggetto come risultato. Questo spesso richiede l'utilizzo di schemi di progettazione diversi rispetto a quelli con oggetti mutabili, ma non richiede di abbandonare del tutto l'OOP.

    
risposta data 17.03.2014 - 21:56
fonte
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 potresti perdere alcune funzionalità linguistiche funzionali.

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 all'oggetto comprende una serie di concetti intrinseci (incapsulamento, polimorfismo, ereditarietà, ecc.) Che hanno poco o nulla a che fare con la mutazione e si otterrebbero comunque i benefici di tali caratteristiche, anche se rendeste 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" hanno un modo per specificare i tipi mutabili.

Una domanda migliore potrebbe essere "L'immutabilità completa ha senso nella programmazione imperativa ? direi che la risposta ovvia a questa domanda è no. Per ottenere la completa immutabilità nella programmazione imperativa, dovresti rinunciare a cose come for loops (dato che dovresti mutare una variabile di loop) a favore della ricorsione, e ora stai essenzialmente programmando comunque in modo funzionale.

    
risposta data 17.03.2014 - 20:29
fonte
5

Spesso è utile classificare gli oggetti come valori o entità incapsulanti, con la differenza che se qualcosa è un valore, il codice che contiene un riferimento ad esso 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 diversi dal controllo del detentore del riferimento.

Sebbene sia possibile utilizzare il valore di incapsulamento usando oggetti di tipo mutevole o immutabile, un oggetto può comportarsi come un valore solo se si verifica almeno una delle seguenti condizioni:

  1. Nessun riferimento all'oggetto sarà mai esposto a qualcosa che potrebbe cambiare lo stato in esso incapsulato.

  2. Il detentore di almeno uno dei riferimenti all'oggetto conosce tutti gli usi a cui potrebbe essere messo un riferimento esistente.

Poiché tutte le istanze di tipi immutabili soddisfano automaticamente il primo requisito, utilizzarle come valori è facile. 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 passati in giro come mezzo per incapsulare lo stato incapsulato in essi, passare lo stato memorizzato in tipi mutevoli richiede la costruzione di oggetti wrapper immutabili, oppure la copia dello stato incapsulato da oggetti privati in altri oggetti che sono fornito da o costruito per il destinatario dei dati.

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

Ad esempio, se si ricevono alcuni dati su una connessione TCP, si potrebbe produrre un nuovo oggetto "stato del mondo" che include quei dati nel suo buffer senza influire sui riferimenti al vecchio "stato del mondo", ma le vecchie copie dello stato mondiale che non includono l'ultimo lotto di dati saranno difettose e non dovrebbero essere utilizzate poiché non corrisponderanno più allo stato del socket TCP reale.

    
risposta data 18.03.2014 - 22:11
fonte
4

In c # alcuni tipi sono immutabili come la stringa.

Questo sembra suggerire inoltre che la scelta è stata strongmente presa in considerazione.

Sicuramente è davvero una prestazione impegnativa usare tipi immutabili se devi modificare quel tipo centinaia di migliaia di volte. Questo è il motivo per cui si consiglia di utilizzare la classe StringBuilder anziché la classe string in questo caso.

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

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

    
risposta data 17.03.2014 - 20:34
fonte
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 utile programma ha effetti collaterali.

Un programma che non fa cambiare nulla, è senza valore. Potresti anche non eseguirlo, poiché l'effetto sarà identico.

Anche se pensi di non cambiare nulla, e stai semplicemente riassumendo un elenco di numeri che hai ricevuto in qualche modo, considera che devi fare qualcosa con il risultato: se lo stampi sullo standard output, scrivilo a un file o ovunque. E ciò comporta la modifica 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.

    
risposta data 17.03.2014 - 23:06
fonte
-2

Penso che dipenda dal fatto che la tua definizione di OOP sia che usa 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, comunichi a un oggetto di memorizzare nuovi dati invece di chiedere quali nuovi dati dovresti memorizzare in una nuova variabile.

sameOldObject.changeMe(foo);

È possibile avere oggetti e non mutarli, rendendo i suoi metodi pure funzioni che accadono per vivere all'interno dell'oggetto invece che all'esterno.

var brandNewVariable = nonMutatingObject.askMe(foo);

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

    
risposta data 17.03.2014 - 23:31
fonte

Leggi altre domande sui tag