Esiste una strategia di progettazione specifica che può essere applicata per risolvere la maggior parte dei problemi di gallina e uova durante l'uso di oggetti immutabili?

17

Provenendo da un background OOP (Java), sto imparando Scala da solo. Mentre posso vedere prontamente i vantaggi dell'utilizzo di oggetti immutabili individualmente, sto avendo difficoltà a vedere come si può progettare un'intera applicazione del genere. Darò un esempio:

Dire che ho oggetti che rappresentano "materiali" e le loro proprietà (sto progettando un gioco, quindi effettivamente ho quel problema), come acqua e ghiaccio. Avrei un "manager" che possiede tutte queste istanze di materiali. Una proprietà sarebbe il punto di congelamento e di fusione, e ciò a cui il materiale si congela o si fonde.

[EDIT] Tutte le istanze del materiale sono "singleton", un po 'come un Java Enum.

Voglio "acqua" per dire che si congela a "ghiaccio" a 0C, e "ghiaccio" per dire che si scioglie in "acqua" a 1C. Ma se l'acqua e il ghiaccio sono immutabili, non possono ottenere un riferimento l'uno all'altro come parametri del costruttore, poiché uno di essi deve essere creato per primo e non si può ottenere un riferimento all'altro non ancora esistente come parametro del costruttore. Potrei risolvere questo dando sia un riferimento al gestore in modo che possano interrogarlo per trovare l'altra istanza materiale di cui hanno bisogno ogni volta che viene loro richiesto per le loro proprietà di congelamento / fusione, ma poi ottengo lo stesso problema tra il manager e i materiali, che hanno bisogno di un riferimento tra loro, ma possono essere forniti solo nel costruttore per uno di essi, quindi il gestore o il materiale non può essere immutabile.

Non è proprio il caso di aggirare questo problema, o devo usare tecniche di programmazione "funzionali" o qualche altro schema per risolverlo?

    
posta Sebastien Diot 12.09.2011 - 17:56
fonte

4 risposte

2

La soluzione è di imbrogliare un po '. In particolare:

  • Crea A, ma lascia il suo riferimento a B non inizializzato (poiché B non esiste ancora).

  • Crea B, e fallo puntare su A.

  • Aggiorna A per puntare a B. Non aggiornare A o B dopo questo.

Questo può essere fatto esplicitamente (esempio in C ++):

struct List {
    int n;
    List *next;

    List(int n, List *next)
        : n(n), next(next);
};

// Return a list containing [0,1,0,1,...].
List *binary(void)
{
    List *a = new List(0, NULL);
    List *b = new List(1, a);
    a->next = b; // Evil, but necessary.
    return a;
}

o implicitamente (esempio in Haskell):

binary :: [Int]
binary = a where
    a = 0 : b
    b = 1 : a

L'esempio di Haskell utilizza la valutazione pigra per ottenere l'illusione di valori immutabili reciprocamente dipendenti. I valori iniziano come:

a = 0 : <thunk>
b = 1 : a

a e b sono entrambi validi moduli head-normal in modo indipendente. Ogni contro può essere costruito senza aver bisogno del valore finale dell'altra variabile. Quando il thunk viene valutato, punta quindi allo stesso dato b punti a.

Quindi, se vuoi che due valori immutabili puntino l'un l'altro, devi aggiornare il primo dopo aver costruito il secondo o utilizzare un meccanismo di livello superiore per fare lo stesso.

Nel tuo esempio particolare, potrei esprimerlo in Haskell come:

data Material = Water {temperature :: Double}
              | Ice   {temperature :: Double}

setTemperature :: Double -> Material -> Material
setTemperature newTemp (Water _) | newTemp <= 0.0 = Ice newTemp
                                 | otherwise      = Water newTemp
setTemperature newTemp (Ice _)   | newTemp >= 1.0 = Water newTemp
                                 | otherwise      = Ice newTemp

Tuttavia, sto affrontando il problema. Immagino che in un approccio orientato agli oggetti, in cui un metodo setTemperature è collegato al risultato di ogni costruttore di Material , è necessario che i costruttori puntino l'uno sull'altro. Se i costruttori sono trattati come valori immutabili, puoi utilizzare l'approccio descritto sopra.

    
risposta data 12.09.2011 - 21:24
fonte
6

Nel tuo esempio, stai applicando una trasformazione a un oggetto, quindi utilizzerei qualcosa come un metodo ApplyTransform() che restituisce un BlockBase piuttosto che provare a cambiare l'oggetto corrente.

Ad esempio, per cambiare un IceBlock in un WaterBlock applicando un po 'di calore, chiamerei qualcosa come

BlockBase currentBlock = new IceBlock();
currentBlock = currentBlock.ApplyTemperature(1); 
// currentBlock is now a WaterBlock 

e il metodo IceBlock.ApplyTemperature() sarebbe simile al seguente:

public class IceBlock() : BlockBase
{
    public BlockBase ApplyTemperature(int temp)
    {
        return (temp > 0 ? new WaterBlock((BlockBase)this) : this);
    }
}
    
risposta data 12.09.2011 - 21:14
fonte
6

Un altro modo per spezzare il ciclo è separare le preoccupazioni di materiale e trasmutazione, in qualche linguaggio inventato:

water = new Block("water");
ice = new Block("ice");

transitions = new Transitions([
    new transitions.temperature.Below(0.0, water, ice),
    new transitions.temperature.Above(0.0, ice, water),
]);
    
risposta data 12.09.2011 - 23:12
fonte
1

Se intendi usare un linguaggio funzionale e vuoi realizzare i benefici dell'immutabilità, dovresti affrontare il problema pensando a questo. Stai tentando di definire un tipo di oggetto "ghiaccio" o "acqua" in grado di supportare un intervallo di temperature: per supportare l'immutabilità, dovrai creare un nuovo oggetto ogni volta che la temperatura cambia, il che è uno spreco. Quindi cerca di rendere i concetti di tipo blocco e temperatura più indipendenti. Non conosco Scala (è sulla mia lista di cose da imparare :-)), ma prendendo a prestito da Joey Adams Risposta in Haskell , suggerisco qualcosa del tipo:

data Material = Water | Ice

blockForTemperature :: Double -> Material
blockForTemperature x = 
  if x < 0 then Ice else Water

o forse:

transitionForTemperature :: Material -> Double -> Material
transitionForTemperature oldMaterial newTemp = 
  case (oldMaterial, newTemp) of
    (Ice, _) | newTemp > 0 -> Water
    (Water, _) | newTemp <= 0 -> Ice

(Nota: non ho provato a farlo, e il mio Haskell è un po 'arrugginito.) Ora, la logica di transizione è separata dal tipo di materiale, quindi non spreca più memoria, e (secondo me ) è un po 'più orientato dal punto di vista funzionale.

    
risposta data 13.09.2011 - 18:58
fonte

Leggi altre domande sui tag