Rendere immutabili le classi mutevoli

3

Quando provo a trasformare una classe con uno stato mutabile in una immutabile, faccio regolarmente fatica a scegliere tra due alternative: 1) o estrae lo stato in un altro oggetto di stato (immutabile) e passare questo stato alla classe con ogni chiamata al metodo (e restituire il nuovo stato) o 2) rendere lo stato parte della classe immutabile e restituire una nuova istanza di classe con stato potenzialmente modificato con ogni chiamata di metodo.

Ecco un esempio in Scala:

Questo è il punto in cui vengo

class SomeApiWrapper(val credentials: ApiCredentials) {

  var nonce = 1L // mutable field

  def apiCall(someParam: String): Unit = {
    // make api call
    // increase nonce
  }

}

Opzione 1 - Stato separato

class SomeApiWrapper(val credentials: ApiCredentials) {

  def apiCall(someParam: String, s: ApiWrapperState) = {
    // get nonce from state
    // api call
    // return new state with increased nonce
  }

}

Opzione 2 - Nuova istanza con ogni chiamata di metodo

class SomeApiWrapper(val credentials: ApiCredentials, val nonce: Long) {

  def apiCall(someParam: String): SomeApiWrapper = {
    // api call with nonce
    SomeApiWrapper(credentials, nonce + 1)
  }

}

Mi piace la chiara separazione dei parametri di configurazione (credenziali) dallo stato nel costruttore per l'opzione 1, tuttavia, è un po 'fastidioso che per ogni classe ora ne ho due (uno in più per lo stato) e io devi passare intorno allo stato tutto il tempo.

Ci sono altri buoni argomenti per scegliere la prima o la seconda opzione? Mi manca un'altra opzione qui? Dovrei farlo completamente un modo differente? C'è qualche letteratura standard su cui leggere questo?

    
posta lex82 18.09.2017 - 12:31
fonte

1 risposta

7

Il punto delle astrazioni è semplificare il tuo codice. Se il design inizia a intralciarti, qualcosa non va.

La mutabilità non locale di solito rende il codice più complicato. Ci sono due strategie generali per affrontare questo:

  • sbarazzarsi dello stato implicito: si scrivono pure funzioni che trasformano lo stato di input ma non lo modificano. Lo stato viene modificato solo al di fuori del tuo sistema puro.

  • incapsula lo stato: inseriscilo in oggetti semplici in cui puoi controllare l'accesso e le modifiche allo stato tramite l'interfaccia pubblica. Mantenere un piccolo pezzo di stato incapsulato coerente è molto più facile che mantenere una grande quantità di stato globale coerente.

Scala supporta intenzionalmente entrambe queste strategie. Quale strategia è migliore dipende interamente dal contesto.

In questo contesto, è probabile che l'utilizzo di un oggetto con campi internamente modificabili sia l'approccio preferito. Il problema è che con ogni chiamata, il vecchio stato diventa non valido e non dovrebbe essere usato di nuovo. Se rendi espliciti gli stati, non c'è modo di impedire che il nuovo stato venga scartato e il vecchio stato venga riutilizzato. Questa gestione dello stato esplicita è più applicabile quando tutti gli stati possono essere utilizzati liberamente e nessuno stato influenzato dallo stato. Qui, effettuare una chiamata API sarebbe un effetto collaterale. Un indicatore strong è che la chiamata API non restituisce alcun valore e pertanto viene utilizzata solo per un effetto collaterale.

Come problema secondario, gli stati espliciti interrompono l'incapsulamento del wrapper e richiedono all'utente di tenere traccia dello stato interno necessario. A meno che non vi sia una necessità urgente per questo progetto, questo complica inutilmente il codice.

A volte è possibile riconciliare parzialmente l'immutabilità con sequenze di stati usando calcoli / futures pigri. Il futuro è costruito con tutte le informazioni necessarie per eseguire la chiamata dell'API in primo piano. Quando si accede al futuro, la chiamata viene eseguita e il risultato memorizzato (questa mutabilità è esternamente invisibile). Quando si accede successivamente al valore, il risultato memorizzato viene nuovamente restituito. Il valore restituito può includere il prossimo futuro. In questo modo, non puoi mai riutilizzare lo stesso stato perché non hai mai accesso diretto allo stato. Tuttavia, questo significa anche che per ottenere i risultati di una chiamata devi fornire i parametri per la prossima chiamata, che probabilmente non è adatta allo scenario.

Le tue due soluzioni a stato esplicativo sono fondamentalmente equivalenti, il che diventa più chiaro se scambiamo l'ordine degli argomenti attorno e vediamo il tuo oggetto come una funzione al curry. La tua soluzione a stati separati ha questo aspetto:

wrapper = Wrapper(...)
initialState = ...
nextState = wrapper(initialState, args)

Correndo il wrapper con lo stato iniziale, e avendo il wrapper restituito al tempo stesso con lo stato successivo, otteniamo:

wrapper = Wrapper(..., initialState)
nextWrapper = wrapper(args)
    
risposta data 18.09.2017 - 14:10
fonte