Esiste una strategia di progettazione specifica che può essere applicata per risolvere la maggior parte dei problemi di galline e uova utilizzando oggetti immutabili?


17

Provenendo da un background OOP (Java), sto imparando Scala da solo. Mentre posso facilmente vedere i vantaggi dell'utilizzo di oggetti immutabili singolarmente, sto facendo fatica a vedere come si può progettare un'intera applicazione del genere. Faccio un esempio:

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

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

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

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


2
secondo me, come dici, non c'è acqua né ghiaccio. C'è solo h2omateriale
moscerino del

1
So che ciò avrebbe più "senso scientifico", ma in un gioco è più facile modellarlo come due materiali diversi con proprietà "fisse", piuttosto che un materiale con proprietà "variabili" a seconda del contesto.
Sebastien Diot,

Singleton è un'idea stupida.
DeadMG

@DeadMG Bene, ok. Non sono veri singleton Java. Intendo solo che non ha senso creare più di un'istanza di ciascuna, poiché sono immutabili e sarebbero uguali tra loro. In effetti, non avrò istanze statiche reali. I miei oggetti "root" sono servizi OSGi.
Sebastien Diot,

La risposta a questa altra domanda sembra confermare il mio sospetto che le cose si complicino molto rapidamente con gli immutabili: programmers.stackexchange.com/questions/68058/…
Sebastien Diot,

Risposte:


2

La soluzione è imbrogliare un po '. In particolare:

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

  • Crea B e fai puntare ad A.

  • Aggiorna A per indicare B. Non aggiornare successivamente A o B.

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 una valutazione pigra per ottenere l'illusione di valori immutabili reciprocamente dipendenti. I valori iniziano come:

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

ae bsono entrambi forme normali normali valide indipendentemente. Ogni contro può essere costruito senza bisogno del valore finale dell'altra variabile. Quando viene valutato, il thunk indicherà gli stessi datib punta.

Pertanto, se si desidera che due valori immutabili siano puntati l'uno verso l'altro, è necessario aggiornare il primo dopo aver costruito il secondo oppure 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 setTemperaturemetodo è attaccato al risultato di ciascun Materialcostruttore, dovresti fare in modo che i costruttori puntino l'uno verso l'altro. Se i costruttori sono trattati come valori immutabili, è possibile utilizzare l'approccio descritto sopra.


(supponendo di aver compreso la sintassi di Haskell) Penso che la mia soluzione attuale sia in realtà molto simile, ma mi chiedevo se fosse quella "giusta" o se esistesse qualcosa di meglio. Per prima cosa creo un "handle" (riferimento) per ogni oggetto (non ancora creato), quindi creo tutti gli oggetti, dando loro gli handle di cui hanno bisogno e infine inizializzano l'handle sugli oggetti. Gli oggetti sono essi stessi immutabili, ma non le maniglie.
Sebastien Diot,

6

Nel tuo esempio, stai applicando una trasformazione a un oggetto in modo da utilizzare qualcosa come un ApplyTransform()metodo che restituisce aBlockBase anziché tentare di modificare l'oggetto corrente.

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

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

e il IceBlock.ApplyTemperature()metodo sarebbe simile a questo:

public class IceBlock() : BlockBase
{
    public BlockBase ApplyTemperature(int temp)
    {
        return (temp > 0 ? new WaterBlock((BlockBase)this) : this);
    }
}

Questa è una buona risposta, ma purtroppo solo perché non ho menzionato il fatto che i miei "materiali", anzi i miei "blocchi", sono tutti singleton, quindi il nuovo WaterBlock () non è un'opzione. Questo è il principale vantaggio di immutabile, puoi riutilizzarli all'infinito. Invece di avere 500.000 blocchi in ram, ho 500.000 riferimenti a 100 blocchi. Più economico!
Sebastien Diot,

Che ne dici di tornare BlockList.WaterBlockinvece di creare un nuovo blocco?
Rachel,

Sì, questo è quello che faccio, ma come posso ottenere la blocklist? Ovviamente, i blocchi devono essere creati prima dell'elenco dei blocchi e, quindi, se il blocco è davvero immutabile, non può ricevere l'elenco di blocchi come parametro. Quindi da dove prende l'elenco? Il mio punto generale è che, rendendo il codice più contorto, risolvi il problema pollo-e-uovo a un livello, solo per riaverlo al prossimo. Fondamentalmente, non vedo alcun modo di creare un'intera applicazione basata sull'immutabilità. Sembra applicabile solo ai "piccoli oggetti", ma non ai contenitori.
Sebastien Diot,

@Sebastien Sto pensando che BlockListsia solo una staticclasse responsabile delle singole istanze di ciascun blocco, quindi non è necessario creare un'istanza di BlockList(sono abituato a C #)
Rachel,

@Sebastien: se usi Singeltons, paghi il prezzo.
DeadMG

6

Un altro modo per interrompere il ciclo è quello di separare le preoccupazioni di materiale e trasmutazione, in un 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),
]);

Eh, è stato difficile per me leggere questo inizialmente, ma penso che sia essenzialmente lo stesso approccio che ho sostenuto.
Aidan Cully,

1

Se hai intenzione di usare un linguaggio funzionale e vuoi realizzare i benefici dell'immutabilità, allora dovresti affrontare il problema con questo in mente. Stai tentando di definire un tipo di oggetto "ghiaccio" o "acqua" in grado di supportare un intervallo di temperature - per supportare l'immutabilità, dovrai quindi creare un nuovo oggetto ogni volta che la temperatura cambia, il che è dispendioso. Quindi cerca di rendere i concetti di tipo di blocco e temperatura più indipendenti. Non conosco Scala (è sulla mia lista di apprendimenti :-)), ma prendendo in 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 eseguire questo, e il mio Haskell è un po 'arrugginito.) Ora, la logica di transizione è separata dal tipo di materiale, quindi non spreca tanta memoria e (secondo me) è abbastanza un po 'più orientato alla funzionalità.


In realtà non sto cercando di usare il "linguaggio funzionale" perché non capisco! L'unica cosa che normalmente conservo da qualsiasi esempio di programmazione funzionale non banale è: "Accidenti, io che ero più intelligente!" È proprio dietro di me come questo possa avere senso per chiunque. Sin dai tempi dei miei studenti, ricordo che Prolog (basato sulla logica), Occam (tutto corre in parallelo per impostazione predefinita) e persino l'assemblatore avevano un senso, ma Lisp era solo roba da matti. Ma ti spingo a spostare il codice che causa un "cambio di stato" al di fuori dello "stato".
Sebastien Diot,
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.