Gibt es eine bestimmte Designstrategie, die angewendet werden kann, um die meisten Henne-Ei-Probleme bei der Verwendung unveränderlicher Objekte zu lösen?

17

Ich komme aus einem OOP-Hintergrund (Java) und lerne Scala alleine. Ich kann die Vorteile der Verwendung unveränderlicher Objekte zwar leicht erkennen, aber es fällt mir schwer, zu sehen, wie man eine ganze Anwendung so gestalten kann. Ich gebe ein Beispiel:

Angenommen, ich habe Objekte, die "Materialien" und ihre Eigenschaften darstellen (ich entwerfe ein Spiel, also habe ich dieses Problem wirklich), wie Wasser und Eis. Ich hätte einen "Manager", der alle derartigen Materialinstanzen besitzt. Eine Eigenschaft wäre der Gefrier- und Schmelzpunkt und was das Material einfriert oder schmilzt.

[BEARBEITEN] Alle materiellen Instanzen sind "Singleton", ähnlich einer Java-Enumeration.

Ich möchte, dass "Wasser" sagt, dass es bei 0 ° C zu "Eis" gefriert und "Eis" sagt, dass es bei 1 ° C zu "Wasser" schmilzt. Wenn jedoch Wasser und Eis unveränderlich sind, können sie nicht als Konstruktorparameter aufeinander verweisen, da einer von ihnen zuerst erstellt werden muss und der andere nicht als Konstruktorparameter auf den noch nicht existierenden anderen verweisen kann. Ich könnte das lösen, indem ich beiden einen Verweis auf den Manager gebe, damit sie ihn abfragen können, um die andere Materialinstanz zu finden, die sie jedes Mal benötigen, wenn sie nach ihren Gefrier- / Schmelzeigenschaften gefragt werden, aber dann habe ich das gleiche Problem zwischen den Managern und die Materialien, die einen Bezug zueinander benötigen, aber nur für eines von ihnen im Konstruktor bereitgestellt werden können, sodass weder der Manager noch das Material unveränderlich sein können.

Kann man dieses Problem nicht umgehen, oder muss ich "funktionale" Programmiertechniken oder ein anderes Muster verwenden, um es zu lösen?

Sebastien Diot
quelle
2
Für mich gibt es weder Wasser noch Eis. Es gibt nur h2oMaterial
Mücke
1
Ich weiß, dass dies "wissenschaftlicher" ist, aber in einem Spiel ist es einfacher, es als zwei verschiedene Materialien mit "festen" Eigenschaften zu modellieren, als je nach Kontext ein Material mit "variablen" Eigenschaften.
Sebastien Diot
Singleton ist eine blöde Idee.
DeadMG
@DeadMG Na gut. Sie sind keine echten Java Singletons. Ich meine nur, dass es keinen Grund gibt, mehr als eine Instanz von jeder zu erstellen, da sie unveränderlich sind und einander gleich wären. Tatsächlich habe ich keine echten statischen Instanzen. Meine "Root" -Objekte sind OSGi-Dienste.
Sebastien Diot
Die Antwort auf diese andere Frage scheint meinen Verdacht zu bestätigen, dass die Dinge mit immutables sehr schnell kompliziert werden: programmers.stackexchange.com/questions/68058/…
Sebastien Diot

Antworten:

2

Die Lösung ist, ein bisschen zu schummeln. Speziell:

  • Erstellen Sie A, aber lassen Sie den Verweis auf B uninitialisiert (da B noch nicht existiert).

  • Erstellen Sie B und lassen Sie es auf A zeigen.

  • Aktualisieren Sie A, um auf B zu zeigen. Aktualisieren Sie danach weder A noch B.

Dies kann entweder explizit erfolgen (Beispiel 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;
}

oder implizit (Beispiel in Haskell):

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

Das Beispiel von Haskell verwendet eine verzögerte Auswertung, um die Illusion von voneinander abhängigen unveränderlichen Werten zu erreichen. Die Werte beginnen mit:

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

aund bsind beide unabhängig voneinander gültige Kopfnormalformen . Die einzelnen Nachteile können konstruiert werden, ohne den Endwert der anderen Variablen zu benötigen. Wenn der Thunk ausgewertet wird, zeigt er auf dieselben Datenpunkte b.

Wenn Sie also möchten, dass zwei unveränderliche Werte aufeinander verweisen, müssen Sie entweder den ersten nach dem Erstellen des zweiten aktualisieren oder einen Mechanismus auf höherer Ebene verwenden, um dasselbe zu tun.


In Ihrem speziellen Beispiel könnte ich es in Haskell so ausdrücken:

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

Ich gehe jedoch aus dem Ruder. Ich würde mir vorstellen, dass bei einem objektorientierten Ansatz, bei dem eine setTemperatureMethode an das Ergebnis jedes MaterialKonstruktors angehängt wird, die Konstruktoren aufeinander zeigen müssen. Wenn die Konstruktoren als unveränderliche Werte behandelt werden, können Sie den oben beschriebenen Ansatz verwenden.

Joey Adams
quelle
(vorausgesetzt, ich habe Haskells Syntax verstanden) Ich denke, meine derzeitige Lösung ist sehr ähnlich, aber ich habe mich gefragt, ob es die "richtige" ist oder ob es etwas Besseres gibt. Zuerst erstelle ich ein "Handle" (Referenz) für jedes (noch nicht erstellte) Objekt, erstelle dann alle Objekte und gebe ihnen die Handles, die sie benötigen, und initialisiere schließlich das Handle für die Objekte. Die Objekte selbst sind unveränderlich, aber nicht die Griffe.
Sebastien Diot
6

In Ihrem Beispiel wenden Sie eine Transformation auf ein Objekt an, sodass ich so etwas wie eine ApplyTransform()Methode verwenden würde, die a zurückgibt, BlockBaseanstatt zu versuchen, das aktuelle Objekt zu ändern.

Wenn Sie zum Beispiel einen IceBlock durch Anwenden von Wärme in einen WaterBlock umwandeln, würde ich so etwas nennen

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

und die IceBlock.ApplyTemperature()Methode würde ungefähr so ​​aussehen:

public class IceBlock() : BlockBase
{
    public BlockBase ApplyTemperature(int temp)
    {
        return (temp > 0 ? new WaterBlock((BlockBase)this) : this);
    }
}
Rachel
quelle
Dies ist eine gute Antwort, aber leider nur, weil ich nicht erwähnt habe, dass meine "Materialien", in der Tat meine "Blöcke", alle Singleton sind, so dass ein neuer WaterBlock () einfach keine Option ist. Das ist der Hauptvorteil von unveränderlichen, Sie können sie unendlich wiederverwenden. Anstatt 500.000 Blöcke im RAM zu haben, habe ich 500.000 Verweise auf 100 Blöcke. Viel billiger!
Sebastien Diot
Wie wäre es also, zurückzukehren, BlockList.WaterBlockanstatt einen neuen Block zu erstellen?
Rachel
Ja, das ist was ich tue, aber wie bekomme ich eine Sperrliste? Offensichtlich müssen die Blöcke vor der Liste der Blöcke erstellt werden. Wenn der Block also wirklich unveränderlich ist, kann er die Liste der Blöcke nicht als Parameter empfangen. Woher bekommt es die Liste? Mein allgemeiner Punkt ist, indem Sie den Code komplizierter machen, das Henne-Ei-Problem auf einer Ebene zu lösen, nur um es auf der nächsten wieder zu bekommen. Grundsätzlich sehe ich keine Möglichkeit, eine ganze Anwendung auf der Basis von Unveränderlichkeit zu erstellen. Es scheint nur auf die "kleinen Objekte" anwendbar zu sein, nicht aber auf Behälter.
Sebastien Diot
@Sebastien Ich denke, dass dies BlockListnur eine staticKlasse ist, die für die einzelnen Instanzen jedes Blocks verantwortlich ist, sodass Sie keine Instanz von erstellen müssen BlockList(ich bin an C #
Rachel
@Sebastien: Wenn du Singeltons verwendest, zahlst du den Preis.
DeadMG
6

Eine andere Möglichkeit, den Kreislauf zu durchbrechen, besteht darin, die Anliegen von Material und Transmutation in einer bestimmten Sprache zu trennen:

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),
]);
SingleNegationElimination
quelle
Huh, es war anfangs schwer für mich, diesen zu lesen, aber ich denke, es ist im Wesentlichen der gleiche Ansatz, den ich vertreten habe.
Aidan Cully
1

Wenn Sie eine funktionale Sprache verwenden und die Vorteile der Unveränderlichkeit nutzen möchten, sollten Sie das Problem unter Berücksichtigung dieser Aspekte angehen. Sie versuchen, einen Objekttyp "Eis" oder "Wasser" zu definieren, der einen Temperaturbereich unterstützen kann. Um die Unveränderlichkeit zu unterstützen, müssen Sie dann jedes Mal ein neues Objekt erstellen, wenn sich die Temperatur ändert. Dies ist verschwenderisch. Versuchen Sie daher, die Konzepte von Blocktyp und Temperatur unabhängiger zu machen. Ich kenne Scala nicht (es steht auf meiner To-Learn-Liste :-)), leihe mir aber Joey Adams aus. Antworte in Haskell , ich schlage Folgendes vor:

data Material = Water | Ice

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

oder vielleicht:

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

(Hinweis: Ich habe nicht versucht, dies auszuführen, und mein Haskell ist ein wenig verrostet.) Nun ist die Übergangslogik vom Materialtyp getrennt, sodass weniger Speicher verschwendet wird, und (meiner Meinung nach) ist es ziemlich ein bisschen mehr funktional ausgerichtet.

Aidan Cully
quelle
Ich versuche eigentlich nicht, "funktionale Sprache" zu verwenden, weil ich es einfach nicht verstehe! Das einzige, was ich normalerweise von einem nicht trivialen funktionalen Programmierbeispiel behalte, ist: "Verdammt, ich, ich war schlauer!" Es ist mir ein Rätsel, wie das für jeden Sinn machen kann. Aus meiner Studienzeit erinnere ich mich, dass Prolog (logikbasiert), Occam (standardmäßig läuft alles parallel) und sogar Assembler Sinn machten, aber Lisp war einfach verrückt. Aber ich verstehe es als sinnvoll, den Code, der eine "Zustandsänderung" verursacht, außerhalb des "Zustands" zu verschieben.
Sebastien Diot