Possiamo davvero usare l'immutabilità in OOP senza perdere tutte le funzionalità chiave OOP?

10

Vedo i vantaggi di rendere immutabili gli oggetti nel mio programma. Quando penso profondamente a un buon design per la mia applicazione, spesso arrivo a molti dei miei oggetti immutabili. Spesso arriva al punto in cui mi piacerebbe avere tutti i miei oggetti immutabili.

Questa domanda riguarda la stessa idea ma nessuna risposta suggerisce qual è un buon approccio all'immutabilità e quando effettivamente usarlo. Ci sono dei buoni schemi di design immutabili? L'idea generale sembra essere "rendere immutabili gli oggetti se non si ha assolutamente bisogno che cambino", cosa che in pratica è inutile.

La mia esperienza è che l'immutabilità porta il mio codice sempre più al paradigma funzionale e questa progressione accade sempre:

  1. Inizio a richiedere strutture dati persistenti (in senso funzionale) come elenchi, mappe ecc.
  2. È estremamente sconveniente lavorare con riferimenti incrociati (ad esempio il nodo dell'albero che fa riferimento ai suoi figli mentre i bambini fanno riferimento ai loro genitori), il che mi impedisce di utilizzare i riferimenti incrociati, il che rende ancora più funzionali le mie strutture dati e il codice.
  3. L'ereditarietà si interrompe per avere senso e comincio a usare invece la composizione.
  4. Le idee di base di OOP come l'incapsulamento cominciano a cadere a pezzi e i miei oggetti cominciano a somigliare a funzioni.

A questo punto praticamente non utilizzo più nulla del paradigma OOP e posso semplicemente passare a un linguaggio puramente funzionale. Quindi la mia domanda: c'è un approccio coerente al buon design OOP immutabile o è sempre il caso che quando si porta l'idea immutabile al suo massimo potenziale, si finisce sempre per programmare in un linguaggio funzionale che non richiede più nulla dal mondo OOP? Ci sono delle buone linee guida per decidere quali classi dovrebbero essere immutabili e quali dovrebbero rimanere mutabili per assicurare che l'OOP non si disgrega?

Solo per comodità, fornirò un esempio. Abbiamo un ChessBoard come una collezione immutabile di pezzi di scacchi immutabili (estendendo la classe astratta Piece ). Dal punto di vista OOP, un pezzo è responsabile della generazione di mosse valide dalla sua posizione sulla scacchiera. Ma per generare le mosse il pezzo ha bisogno di un riferimento al suo tabellone mentre il tabellone deve fare riferimento ai suoi pezzi. Bene, ci sono alcuni trucchi per creare questi riferimenti incrociati immutabili a seconda del tuo linguaggio OOP ma sono dolorosi da gestire, meglio non avere un pezzo per fare riferimento alla sua scheda. Ma poi il pezzo non può generare le sue mosse poiché non conosce lo stato della scacchiera. Quindi il pezzo diventa solo una struttura dati che tiene il tipo di pezzo e la sua posizione. È quindi possibile utilizzare una funzione polimorfica per generare mosse per tutti i tipi di pezzi. Questo è perfettamente realizzabile nella programmazione funzionale ma quasi impossibile in OOP senza verifiche di tipo runtime e altre cattive pratiche OOP ... Quindi, una mossa è solo una funzione che rende una nuova scheda da una vecchia scheda, di nuovo un'idea funzionale che ha perfettamente senso ma non avendo più nulla a che fare con OOP.

    
posta lishaak 02.06.2017 - 12:08
fonte

2 risposte

23

Can we really use immutability in OOP without losing all key OOP features?

Non capisco perché no. Lo abbiamo fatto per anni prima che Java 8 funzionasse comunque. Hai mai sentito parlare di archi? Bello e immutabile dall'inizio.

  1. I start needing persistent (in the functional sense) data structures like lists, maps etc.

Ne ho avuto bisogno anche da sempre. Invalidare i miei iteratori perché hai mutato la raccolta mentre lo stavo leggendo è solo maleducato.

  1. It is extremely inconvenient to work with cross references (e.g. tree node referencing its children while children referencing their parents) which makes me not use cross references at all, which again makes my data structures and code more functional.

I riferimenti circolari sono un tipo speciale di inferno. Immutabilità non ti salverà da esso.

  1. Inheritance stops to make any sense and I start to use composition instead.

Bene, sono qui con te ma non vedo cosa ha a che fare con l'immutabilità. Il motivo per cui mi piace la composizione non è perché amo il modello dinamico della strategia, è perché mi permette di cambiare il mio livello di astrazione.

  1. The whole basic ideas of OOP like encapsulation start to fall apart and my objects start to look like functions.

Rabbrividisco nel pensare a quale sia la tua idea di "incapsulamento come OOP". Se coinvolge getter e setter, per favore basta smettere di chiamare quell'incapsulamento perché non lo è. Non è mai stato. È una programmazione orientata all'aspetto manuale. Una possibilità di convalidare e un posto dove mettere un breakpoint è buona ma non è incapsulamento. L'incapsulamento sta preservando il mio diritto a non sapere oa preoccuparmi di cosa sta succedendo all'interno.

I tuoi oggetti dovrebbero assomigliare a funzioni. Sono borse di funzioni. Sono borse di funzioni che si muovono insieme e si ridefiniscono insieme.

La programmazione funzionale è di moda in questo momento e le persone stanno perdendo alcune idee sbagliate su OOP. Non lasciare che questo ti confonda nel credere che questa è la fine di OOP. Funzionale e OOP possono convivere abbastanza bene.

  • La programmazione funzionale è formale rispetto ai compiti.

  • OOP è formale rispetto ai puntatori di funzioni.

Davvero questo. Dykstra ci ha detto che goto era dannoso, quindi abbiamo preso una decisione formale e abbiamo creato una programmazione strutturata. Proprio così, questi due paradigmi riguardano la ricerca di modi per fare le cose evitando le insidie che derivano dal fare casualmente queste cose problematiche.

Lascia che ti mostri qualcosa:

f n (x)

Questa è una funzione. In realtà è un continuum di funzioni:

f 1 (x)
f 2 (x)
...
f n (x)

Indovina come lo esprimiamo nelle lingue OOP?

n.f(x)

Quel piccolo n sceglie quale implementazione di f viene usata E decide quali sono alcune delle costanti usate in quella funzione (che francamente significa la stessa cosa). Ad esempio:

f 1 (x) = x + 1
f 2 (x) = x + 2

Questa è la stessa cosa che le chiusure forniscono. Laddove le chiusure si riferiscono al loro ambito di inclusione, i metodi oggetto si riferiscono al loro stato di istanza. Gli oggetti possono fare chiusure una migliore. Una chiusura è la funzione singola restituita da un'altra funzione. Un costruttore restituisce un riferimento a un intero pacchetto di funzioni:

g 1 (x) = x 2 + 1
g 2 (x) = x 2 + 2

Sì, hai indovinato:

n.g(x)

f e g sono funzioni che cambiano insieme e si muovono insieme. Quindi li incolliamo nella stessa borsa. Questo è ciò che è veramente un oggetto. Mantenere n costante (immutabile) significa semplicemente che è più facile prevedere che cosa faranno quando li chiamerai.

Questa è solo la struttura. Il modo in cui penso a OOP è un mucchio di piccole cose che parlano di altre piccole cose. Speriamo in un piccolo gruppo ristretto di piccole cose. Quando codice mi immagino come l'oggetto. Guardo le cose dal punto di vista dell'oggetto. E io cerco di essere pigro, quindi non lavoro troppo sull'oggetto. Prendo semplici messaggi, lavoro un po 'su di loro e inviamo messaggi semplici solo ai miei migliori amici. Quando ho finito con quell'oggetto, salto in un altro e guardo le cose dal suo punto di vista.

Le Class Responsibility Cards sono state le prime a insegnarmi a pensare in questo modo. All'epoca ero un po 'confuso su di loro, ma dannazione se non sono ancora rilevanti oggi.

Let's have a ChessBoard as an immutable collection of immutable chess pieces (extending abstract class Piece). From the OOP point of view, a piece is responsible for generating valid moves from its position on the board. But to generate the moves the piece needs a reference to its board while board needs to have reference to its pieces.

Arg! Ancora con i riferimenti circolari inutili.

Che ne dite di: A ChessBoardDataStructure trasforma x y corde in riferimenti pezzo. Questi pezzi hanno un metodo che prende x, y e un particolare ChessBoardDataStructure e lo trasforma in una raccolta di nuovissima sculacciata ChessBoardDataStructure s. Poi lo spinge in qualcosa che possa scegliere la mossa migliore. Ora ChessBoardDataStructure può essere immutabile e così anche i pezzi. In questo modo hai sempre solo una pedina bianca in memoria. Ci sono solo diversi riferimenti ad esso nelle giuste posizioni x y. Orientato agli oggetti, funzionale e immutabile.

Aspetta, non abbiamo già parlato degli scacchi?

    
risposta data 02.06.2017 - 17:35
fonte
3

I concetti più utili introdotti nel mainstream da OOP, secondo me, sono:

  • Corretta modularizzazione.
  • Incapsulamento dei dati.
  • Chiara separazione tra interfacce private e pubbliche.
  • Cancella i meccanismi per l'estendibilità del codice.

Tutti questi vantaggi possono anche essere realizzati senza i tradizionali dettagli di implementazione, come l'ereditarietà o persino le classi. L'idea originale di Alan Kay di un "sistema orientato agli oggetti" usava "messaggi" invece di "metodi" ed era più vicino a Erlang che ad es. C ++. Guarda su Go che elimina molti dettagli di implementazione OOP tradizionali, ma che comunque si sente ragionevolmente orientato agli oggetti.

Se si utilizzano oggetti immutabili, è comunque possibile utilizzare la maggior parte delle tradizionali funzionalità OOP: interfacce, invio dinamico, incapsulamento. Non hai bisogno di setter e spesso non hai nemmeno bisogno di getter per oggetti più semplici. Puoi anche cogliere i benefici dell'immutabilità: sei sempre sicuro che un oggetto non è cambiato nel frattempo, nessuna copia difensiva, nessuna trasmissione di dati, ed è facile rendere i metodi puri.

Dai un'occhiata a come Scala cerca di combinare immutabilità e approcci FP con OOP. Certo, non è il linguaggio più semplice ed elegante. Tuttavia, ha ragionevolmente successo. Inoltre, dai un'occhiata a Kotlin che fornisce molti strumenti e approcci per un mix simile.

Il solito problema di provare un approccio diverso rispetto a quello che i creatori linguistici avevano in mente N anni fa è la "mancata corrispondenza dell'impedenza" con la libreria standard. OTOH sia gli ecosistemi Java che .NET hanno attualmente un ragionevole supporto per la libreria standard per strutture di dati immutabili; ovviamente esistono anche librerie di terze parti.

    
risposta data 02.06.2017 - 19:56
fonte