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


11

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

Questa domanda si occupa della stessa idea, ma nessuna risposta suggerisce quale sia un buon approccio all'immutabilità e quando utilizzarlo effettivamente. Ci sono alcuni buoni modelli di design immutabili? L'idea generale sembra essere "rendere gli oggetti immutabili a meno che tu non ne abbia assolutamente bisogno per cambiare" che è inutile nella pratica.

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

  1. Comincio a necessitare di strutture di dati persistenti (in senso funzionale) come elenchi, mappe, ecc.
  2. È estremamente scomodo lavorare con i riferimenti incrociati (ad esempio il nodo dell'albero che fa riferimento ai suoi figli mentre i bambini fanno riferimento ai loro genitori) che non mi consente affatto di utilizzare i riferimenti incrociati, il che rende le mie strutture di dati e il mio codice più funzionali.
  3. L'ereditarietà si interrompe per dare un senso e inizio a usare la composizione.
  4. Le idee di base di OOP come l'incapsulamento iniziano a sfaldarsi e i miei oggetti iniziano a sembrare funzioni.

A questo punto praticamente non sto più usando nulla del paradigma OOP e posso semplicemente passare a un linguaggio puramente funzionale. Quindi la mia domanda: esiste un approccio coerente al buon design OOP immutabile o è sempre vero che quando porti l'idea immutabile al suo massimo potenziale, finisci sempre per programmare in un linguaggio funzionale che non ha più bisogno del mondo OOP? Ci sono buone linee guida per decidere quali classi dovrebbero essere immutabili e quali dovrebbero rimanere mutabili per garantire che OOP non cada a pezzi?

Per comodità, fornirò un esempio. Facciamo ChessBoarduna collezione immutabile di pezzi degli scacchi immutabili (estendendo la classe astrattaPiece). Dal punto di vista OOP, un pezzo è responsabile della generazione di mosse valide dalla sua posizione sul tabellone. Ma per generare le mosse il pezzo ha bisogno di un riferimento alla sua tavola mentre la tavola deve avere un riferimento ai suoi pezzi. Bene, ci sono alcuni trucchi per creare questi riferimenti incrociati immutabili a seconda del linguaggio OOP, ma sono difficili 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 del tabellone. Quindi il pezzo diventa solo una struttura di dati che contiene il tipo di pezzo e la sua posizione. È quindi possibile utilizzare una funzione polimorfica per generare mosse per tutti i tipi di pezzi. Ciò è perfettamente realizzabile nella programmazione funzionale ma quasi impossibile in OOP senza controlli del tipo di runtime e altre pratiche OOP errate ... Quindi,


3
Mi piace la domanda di base, ma ho dei problemi con i dettagli. Ad esempio, perché l'eredità smette di avere senso quando si massimizza l'immutabilità?
Martin Ba

1
I tuoi oggetti non hanno metodi?
Smetti di fare del male a Monica il


4
"Dal punto di vista OOP, un pezzo è responsabile della generazione di mosse valide dalla sua posizione sul tabellone." - sicuramente no, progettare un pezzo del genere molto probabilmente danneggerebbe SRP.
Doc Brown,

1
@lishaak "Sforzati di spiegare il problema con l'eredità in termini semplici" perché non è un problema; il design scadente è un problema. L'eredità è l'essenza stessa di OOP, la condizione sine qua non se vuoi. Molti dei cosiddetti "problemi di ereditarietà" sono in realtà problemi con le lingue che non hanno una sintassi di sostituzione esplicita e sono molto meno problematiche in linguaggi meglio progettati. Senza metodi virtuali e polimorfismo, non hai OOP; hai una programmazione procedurale con sintassi di oggetti divertenti. Quindi non c'è da meravigliarsi se non lo stai vedendo i benefici di OO!
Mason Wheeler,

Risposte:


24

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

Non capisco perché no. Lo sto facendo da anni prima che Java 8 diventasse comunque tutto funzionale. Hai mai sentito parlare di stringhe? Bello e immutabile sin dall'inizio.

  1. Comincio a necessitare di strutture di dati persistenti (in senso funzionale) come elenchi, mappe, ecc.

Ne ho avuto bisogno anche da sempre. Invalidare i miei iteratori perché hai modificato la raccolta mentre la leggevo è maleducato.

  1. È estremamente scomodo lavorare con i riferimenti incrociati (ad esempio il nodo dell'albero che fa riferimento ai suoi figli mentre i bambini fanno riferimento ai loro genitori) che non mi consente affatto di utilizzare i riferimenti incrociati, il che rende le mie strutture di dati e il mio codice più funzionali.

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

  1. L'ereditarietà si interrompe per dare un senso e inizio a usare la composizione.

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

  1. Le idee di base di OOP come l'incapsulamento iniziano a sfaldarsi e i miei oggetti iniziano a sembrare funzioni.

Rabbrividisco nel pensare quale sia la tua idea di "OOP come l'incapsulamento". Se coinvolge getter e setter, per favore basta smettere di chiamare quell'incapsulamento perché non lo è. Non lo è mai stato. È la programmazione manuale orientata agli aspetti. Un'opportunità per convalidare e un posto dove mettere un breakpoint è bella ma non è incapsulamento. L'incapsulamento sta preservando il mio diritto di non sapere o preoccuparmi di ciò che sta accadendo all'interno.

I tuoi oggetti dovrebbero apparire come funzioni. Sono un sacco di funzioni. Sono un sacco di funzioni che si muovono insieme e si ridefiniscono insieme.

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

  • La programmazione funzionale è formale riguardo agli incarichi.

  • OOP è formale riguardo ai puntatori a funzione.

Davvero. Dykstra ci ha detto che gotoera dannoso, quindi ci siamo formalizzati 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 queste cose fastidiose casualmente.

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 poco nlì sceglie quale implementazionef viene utilizzata E decide quali sono alcune delle costanti utilizzate in quella funzione (che francamente significa la stessa cosa). Per esempio:

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

Questa è la stessa cosa che forniscono le chiusure. Laddove le chiusure si riferiscono al loro ambito di applicazione, i metodi oggetto si riferiscono al loro stato di istanza. Gli oggetti possono fare meglio le chiusure. Una chiusura è una singola funzione restituita da un'altra funzione. Un costruttore restituisce un riferimento a un sacco 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 mettiamo nella stessa borsa. Questo è ciò che un oggetto è davvero. Tenere npremuto (immutabile) significa semplicemente che è più facile prevedere cosa faranno quando li chiami.

Questa è solo la struttura. Il modo in cui penso a OOP è un mucchio di piccole cose che parlano ad altre piccole cose. Spero che un piccolo gruppo selezionato di piccole cose. Quando codice, mi immagino come l'oggetto. Guardo le cose dal punto di vista dell'oggetto. E provo ad essere pigro, quindi non lavoro troppo sull'oggetto. Accolgo messaggi semplici, ci lavoro un po 'e invio messaggi semplici solo ai miei migliori amici. Quando ho finito con quell'oggetto, salto in un altro e guardo le cose dalla sua prospettiva.

Le Class Responsibility Cards sono state le prime a insegnarmi a pensare in quel modo. Amico, allora ero confuso su di loro, ma dannazione se non sono ancora rilevanti oggi.

Let's have a ChessBoard come una raccolta immutabile di pezzi degli scacchi immutabili (estensione del pezzo astratto di classe). Dal punto di vista OOP, un pezzo è responsabile della generazione di mosse valide dalla sua posizione sul tabellone. Ma per generare le mosse il pezzo ha bisogno di un riferimento alla sua tavola mentre la tavola deve avere un riferimento ai suoi pezzi.

Arg! Ancora una volta con inutili riferimenti circolari.

Che ne dici: A ChessBoardDataStructuretrasforma i cavi xy in riferimenti a pezzi. Quei pezzi hanno un metodo che prende x, y e un particolare ChessBoardDataStructuree lo trasforma in una collezione di marchi che sculacciano nuove ChessBoardDataStructures. Quindi lo inserisce in qualcosa che può scegliere la mossa migliore. Ora ChessBoardDataStructurepuò essere immutabile e così anche i pezzi. In questo modo hai sempre e solo una pedina bianca in memoria. Ci sono solo diversi riferimenti ad esso nelle giuste posizioni xy. Orientato agli oggetti, funzionale e immutabile.

Aspetta, non abbiamo già parlato di scacchi?



1
E, ultimo ma non meno importante, il Trattato di Orlando .
Jörg W Mittag,

1
@Euforico: penso che sia una formulazione abbastanza standard che corrisponde alle definizioni standard di "programmazione funzionale", "programmazione imperativa", "programmazione logica", ecc. Altrimenti, C è un linguaggio funzionale perché puoi codificare FP in esso, un linguaggio logico, perché puoi codificare la programmazione logica in esso, un linguaggio dinamico, perché puoi codificare la tipizzazione dinamica in esso, un linguaggio attore, perché puoi codificare un sistema attore in esso e così via. E Haskell è il linguaggio imperativo più grande del mondo poiché puoi effettivamente nominare effetti collaterali, memorizzarli in variabili, passarli come ...
Jörg W Mittag

1
Penso che possiamo usare idee simili a quelle del geniale saggio di Matthias Felleisen sull'espressività del linguaggio. Il trasferimento di un programma OO da, diciamo, Java a C♯ può essere fatto solo con trasformazioni locali, ma spostarlo a C richiede una ristrutturazione globale (in pratica, è necessario introdurre un meccanismo di invio dei messaggi e reindirizzare tutte le funzioni chiamate attraverso di esso), quindi Java e C♯ possono esprimere OO e C può "solo" codificarlo.
Jörg W Mittag,

1
@lishaak Perché lo stai specificando in cose diverse. Ciò lo mantiene a un livello di astrazione e impedisce la duplicazione della memorizzazione di informazioni posizionali che altrimenti potrebbero diventare incoerenti. Se semplicemente rinvii la digitazione aggiuntiva, inseriscila in un metodo in modo da digitarla una sola volta ... Un pezzo non deve ricordare dove si trova se gli dici dove si trova. Ora i pezzi memorizzano solo informazioni come colore, mosse e immagine / abbreviazione, sempre vere e immutabili.
candied_orange,

2

I concetti più utili introdotti nel mainstream da OOP, a mio avviso, sono:

  • Modularizzazione corretta.
  • Incapsulamento dei dati.
  • Chiara separazione tra interfacce pubbliche e private.
  • Meccanismi chiari per l'estensione 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" utilizzava "messaggi" anziché "metodi", ed era più vicina a Erlang che ad esempio C ++. Guarda Go che elimina molti dettagli dell'implementazione OOP tradizionale, ma si sente ancora ragionevolmente orientato agli oggetti.

Se usi oggetti immutabili, puoi comunque usare la maggior parte dei gadget OOP tradizionali: interfacce, invio dinamico, incapsulamento. Non hai bisogno di setter e spesso non hai nemmeno bisogno di getter per oggetti più semplici. Puoi anche raccogliere i benefici dell'immutabilità: sei sempre sicuro che un oggetto non sia cambiato nel frattempo, nessuna copia difensiva, nessuna corsa di dati ed è facile rendere puri i metodi.

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

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

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.