Archiv verlassen und diese Seite im Standarddesign anzeigen : Abhängigkeiten in einem Objekt. Automatisch null setzen von Nachfolgewerten.
gereggter Gast
2008-03-09, 20:31:04
Hallo,
momentan stehe ich vor einem Implementierungsproblem, wo mir nix Gescheites einfällt. Im Grunde habe ich auch nur einen einzigen Einfall, aber der wirkt auf mich nicht gerade als Lösung. Vielleicht könnt ihr mir auf die Sprünge helfen.
Es geht darum, dass ich eine Klasse habe, die aus zwei bis drei dutzend Attributen besteht. Diese Attribute hängen meist voneinander ab. Aus Attribut 1 kann man Attribut 2 errechnen. Daraus folgt ein weiteres Attribut, so dass letztendlich ein "Berechnungsbaum" entsteht.
http://img411.imageshack.us/img411/6410/abhngigkeitenou6.gif
Solch ein Berechnungsbaum läßt sich in einem Objekt sehr einfach implementieren. Man nutzt einfach immer die Getter der jeweiligen Attribute.
Für Attribut Nr. 11 wird z.B. Attribut 9 und 4 benötigt. Im Verlaufe dessen wird sich 11 einfach die 9 und 4 holen. Sind 9 oder 4 noch nicht gegeben, werden diese widerum selbst Attribute holen. Dies geht so lange rekursiv den Baum hinunter, bis ein Wert gegeben ist.
Vorteilhaft ist außerdem, dass nicht schon zum Programmstart alle Daten geladen und berechnet werden müssen, sondern immer nur dann, wenn der Nutzer sie wirklich benötigt.
Nun ist es so, dass sich im Verlauf des Programmes diverse Attribute ändern können (je nach Nutzereingabe). Leider nicht nur Werte an der Spitze des Baumes, sondern auch mittendrin. Ändert sich etwas in der Mitte, müssen natürlich auch die daraus berechneten Werte wieder neu errechnet werden.
http://img444.imageshack.us/img444/9596/abhngigkeitenvernderungpv8.gif
Genau hier liegt nun das Problem!
Man könnte jetzt natürlich einfach alle Werte NULL setzen, um eine Neuberechnung zu erzwingen, aber bei vielen Werten erfordert dies unnötig Zeit und damit die Geduld des Nutzers.
Wenn sich "II" (siehe blau im zweiten Bild) ändert, müssen daraufhin alle davon abhängigen Attribute genullt werden. Dies kann sicher auch manuell bei jeder Aktualisierung von "II" erfolgen, aber bei einem wachsenden Objekt kann man irgendwann nicht mehr alle Abhängigkeiten erkennen. Mal ganz davon abgesehen, dass das Programm auch mal von Anderen gewartet wird. Da steigt so schnell keiner durch.
Ein Automatismus, der das nullen an den richtigen Stellen übernimmt, wäre hilfreich.
Ideen? Anregungen? Kommentare?,
registrierter Gast
Monger
2008-03-09, 20:56:57
Bevor ich hier mit Tipps anfange:
Sehe ich das richtig, dass du eine Mesh-artige Struktur hast, und keinen Baum?
Bei einem Baum wäre es relativ simpel: jeder Vaterknoten überwacht seine Töchterknoten, und stößt eine Aktualisierung an, wenn nötig. Aber bei ringförmigen Strukturen wird die ganze Sache nochmal deutlich unangenehmer, weil so ein Parameter um fünf Ecken herum von sich selbst abhängt. Das führt zu seltsamen Anomalien.
Aber ganz grundsätzlich gesagt: wenn so viel komplexes Verhalten in jedem einzelnen Knoten drinsteckt, musst du das Verhalten in den Knoten selbst implementieren. Statt also die Parameter in der großen Klasse zu verwalten, verwaltest du in der Klasse eben nur die Referenz auf die Datenstruktur, modellierst die Datenstruktur separat, und implementierst gegebenenfalls von außen geeignete Zugriffe wie z.B. Suchmechanismen nach bestimmten Knoten.
Gnafoo
2008-03-09, 22:34:38
Hm also ein Baum ist es nicht. Die interessante Frage ist, ob es azyklisch ist. Berechnet sich das im Bild immer von unten nach oben, bis zum Endwert? (Also so ähnlich wie ein geschichtetes neuronales Netzwerk?)
Oder anders herum? Und was soll die blaue Markierung im unteren Teil unter "II" im zweiten Bild genau andeuten? So ganz klar ist mir die Sache auch noch nicht.
Was passiert, wenn ich den Wert eines Knotens in der Mitte setze? Wird dann einfach die standardmäßige Berechnung überschrieben? Sind die Knoten darunter (die markierten?) dann im Prinzip unbenutzt?
registrierter Gast
2008-03-10, 00:55:12
Bevor ich hier mit Tipps anfange:
Sehe ich das richtig, dass du eine Mesh-artige Struktur hast, und keinen Baum?
Bei einem Baum wäre es relativ simpel: jeder Vaterknoten überwacht seine Töchterknoten, und stößt eine Aktualisierung an, wenn nötig. Aber bei ringförmigen Strukturen wird die ganze Sache nochmal deutlich unangenehmer, weil so ein Parameter um fünf Ecken herum von sich selbst abhängt. Das führt zu seltsamen Anomalien.
Aber ganz grundsätzlich gesagt: wenn so viel komplexes Verhalten in jedem einzelnen Knoten drinsteckt, musst du das Verhalten in den Knoten selbst implementieren. Statt also die Parameter in der großen Klasse zu verwalten, verwaltest du in der Klasse eben nur die Referenz auf die Datenstruktur, modellierst die Datenstruktur separat, und implementierst gegebenenfalls von außen geeignete Zugriffe wie z.B. Suchmechanismen nach bestimmten Knoten.
Die Struktur ist ganz leicht meshig angehaucht. Der Graph ist in der Darstellung etwas komplex geworden. Wahrscheinlich kann man diese ringförmigen Strukturen bei meinem Problem durch geeignete, komplexere Klassen umgehen, so dass aus der Struktur ein Baum wird.
Nun ist die Frage, wie du dir vorstellen würdest, dass der Vater seine Töchter überwacht? Wenn der Graph von oben nach unten durchlaufen werden würde, ginge das, aber der Weg ist anders herum. Man benötigt einen Wert unten und die Berechnung führt nach oben. Also von den Töchtern zu den Vätern.
=> Die Tochter weiß, welche ihre Väter sind.
Hm also ein Baum ist es nicht. Die interessante Frage ist, ob es azyklisch ist. Berechnet sich das im Bild immer von unten nach oben, bis zum Endwert? (Also so ähnlich wie ein geschichtetes neuronales Netzwerk?)
Die Berechnung geschieht on demand von unten nach oben. Das Objekt wird mit dem obersten Wert (ANFANGSwert) initialisiert und sobald ein Wert (darunter) benötigt wird, holt man sich dieses per get. Alle davon abhängigen Werte besorgt sich das Objekt selbst.
Oder anders herum? Und was soll die blaue Markierung im unteren Teil unter "II" im zweiten Bild genau andeuten? So ganz klar ist mir die Sache auch noch nicht.
Mit "II" wollte ich den problematischen Fall andeuten. "II" erhält einen neuen Wert. Alle davon abhängigen Attribute müssen daraufhin neu berechnet werden. Dies betrifft in dem Graphen alle darunter liegenden schwarzen Punkte. Darum habe ich die Wege dahin durchgestrichen, weil sie neu gegangen werden müssen (okay, vielleicht etwas eigensinnig meine gedachte Veranschaulichung).
Was passiert, wenn ich den Wert eines Knotens in der Mitte setze? Wird dann einfach die standardmäßige Berechnung überschrieben? Sind die Knoten darunter (die markierten?) dann im Prinzip unbenutzt?
Genau das ist das Problem. Die Knoten darunter sind leider nach wie vor gesetzt. Eine neue Berechnung wird nicht angestoßen, obwohl sie es müsste. Darum müssen alle darunter liegenden Punkte - am besten automatisch - auf NULL gesetzt werden.
Bietchiebatchie
2008-03-10, 00:57:40
Wenn du nicht den kompletten "Baum" neu berechnen willst, musst du irgendwie die Abhängigkeiten der einzelnen Attribute modellieren.
Konkret kann das z.B. heißen du hasst ein Dictionary (bzw eine Map) dass jedem Attribut A eine Liste (L)mit Attributen zuordnet, wobei alle diese Attribute B der Liste direkt von A abhängig sind.
Wenn du jetzt A änderst, musst du alle Attribute B aus L ebenfalls ändern - dieser Schritt muss natürlich rekursiv geschiehen, da von den B's ebenfalls andere Attribute abhängen können.
Edit: Das geht natürlich nur so einfach bei azyklischen Graphen, bei Graphen die u.U. Zyklen enthalten, musst du noch speichern, welches Attribut die Änderungen angestoßen hat und dieses dann nur einmal rekursiv bearbeiten.
ethrandil
2008-03-10, 02:45:23
Beobachter Muster? Das würde gehen. Jedes Objekt trägt sich bei denen in eine Benachrichtigungsliste ein, wo es auf geänderte Werte reagieren will.
Wird es benachrichtigt, ändert es seinen wert und benachrichtigit wieder seine Beobachter. Siehe wikipedia und co.
mfg
- eth
Nun ist die Frage, wie du dir vorstellen würdest, dass der Vater seine Töchter überwacht? Wenn der Graph von oben nach unten durchlaufen werden würde, ginge das, aber der Weg ist anders herum. Man benötigt einen Wert unten und die Berechnung führt nach oben. Also von den Töchtern zu den Vätern.
=> Die Tochter weiß, welche ihre Väter sind.
[...]
Genau das ist das Problem. Die Knoten darunter sind leider nach wie vor gesetzt. Eine neue Berechnung wird nicht angestoßen, obwohl sie es müsste. Darum müssen alle darunter liegenden Punkte - am besten automatisch - auf NULL gesetzt werden.
Die Berechnung führt von unten nach oben, das Rücksetzen von oben nach unten — also ist doch die offensichtliche Lösung, beide Verbindungen zu speichern.
gereggter Gast
2008-03-10, 15:52:00
Konkret kann das z.B. heißen du hasst ein Dictionary (bzw eine Map) dass jedem Attribut A eine Liste (L)mit Attributen zuordnet, wobei alle diese Attribute B der Liste direkt von A abhängig sind.
Wenn du jetzt A änderst, musst du alle Attribute B aus L ebenfalls ändern - dieser Schritt muss natürlich rekursiv geschiehen, da von den B's ebenfalls andere Attribute abhängen können.
Die Berechnung führt von unten nach oben, das Rücksetzen von oben nach unten — also ist doch die offensichtliche Lösung, beide Verbindungen zu speichern.
Das ist Beides leider keine Option. Momentan ist es extra so implementiert, dass ich mich um keinerlei Abhängigkeiten unter den Attributen kümmern muss. Alle benötigten Werte werden automatisch bestimmt. Das ist gut so.
Die beiden Vorschläge laufen letztendlich darauf hinaus, dass man doch darauf achten muss, welche Attribute voneinander abhängig sind.
Es sei denn (@Bietchiebatchie), solch ein Dictionary kann man automatisch beim Berechnen anlegen.
Beobachter Muster? Das würde gehen. Jedes Objekt trägt sich bei denen in eine Benachrichtigungsliste ein, wo es auf geänderte Werte reagieren will.
Wird es benachrichtigt, ändert es seinen wert und benachrichtigit wieder seine Beobachter. Siehe wikipedia und co.
Wie könnte man das automatisieren?
Wenn die Programmiersprache Codegeneration und Introspektion unterstützt kann man den Code für die Aktualisierung der Attribute per Programm generieren.
In Java z.B. mit http://asm.objectweb.org/
Das ist Beides leider keine Option. Momentan ist es extra so implementiert, dass ich mich um keinerlei Abhängigkeiten unter den Attributen kümmern muss. Alle benötigten Werte werden automatisch bestimmt. Das ist gut so.
Die beiden Vorschläge laufen letztendlich darauf hinaus, dass man doch darauf achten muss, welche Attribute voneinander abhängig sind.
Was meinst du mit "darauf achten"? Du musst doch irgendwo für ein Attribut festlegen, von welchen anderen Attributen es abhängt. Wieso kannst du nicht zum gleichen Zeitpunkt die umgekehrte Abhängigkeit speichern?
Die andere Möglichkeit ist dass der Getter jedes Mal nachprüft dass alle Attribute darüber nicht NULL sind.
Monger
2008-03-10, 16:18:11
Die beiden Vorschläge laufen letztendlich darauf hinaus, dass man doch darauf achten muss, welche Attribute voneinander abhängig sind.
Tut mir leid, ich sitz grad auf dem Schlauch. Hängen nun die Parameter voneinander ab, oder eben nicht?
Wenn sie eine Beziehung zueinander haben, musst du diese auch irgendwie modellieren. Du kannst das von hinten durch die Brust ins Auge, oder eben direkt - aber in irgendeiner Weise müssen sie zueinander in Beziehung stehen.
Bitte gib doch mal ein ganz einfaches Beispiel, damit ich mir das besser vorstellen kann: Du hast die Parameter A und B, die miteinander verrechnet werden und auf C einwirken... ?!?
Entweder muss C also A und B kennen um sich die relevanten Werte abzuholen, oder A und B müssen C kennen, um bei Änderungen aktiv C zu benachrichtigen.
Beobachter Muster? Das würde gehen. Jedes Objekt trägt sich bei denen in eine Benachrichtigungsliste ein, wo es auf geänderte Werte reagieren will.
Das war auch mein Gedanke, aber dazu muss man auf jeden Fall diese Mesh-Struktur in einen Baum auflösen, sonst spielen manche Knoten mit sich selbst Benachrichtigungs-Pingpong.
registrierter Gast
2008-03-10, 23:51:14
Hier dann mal ein Beispiel!
Eine Klasse in Java.
public class javaClass {
private Integer two;
private Integer three;
private Integer four;
private Integer five;
private Integer six;
public javaClass(int value) {
// Initialisierung des Startwertes
two = value;
}
public Integer getTwo() {
return two;
}
public Integer getThree() {
if(three == null) {
Integer valueOfTwo = getTwo();
Integer newValue = ..; // mach was mit dem Wert von two
three = newValue;
}
return three;
}
public Integer getFour() {
if(four == null) {
Integer valueOfTwo = getTwo();
Integer newValue = ..; // mach was mit dem Wert von two
four = newValue;
}
return four;
}
public Integer getFive() {
if(five == null) {
Integer valueOfTwo = getFour();
Integer newValue = ..; // mach was mit dem Wert von four
five = newValue;
}
return five;
}
public Integer getSix() {
if(six == null) {
Integer valueOfTwo = getFour();
Integer newValue = ..; // mach was mit dem Wert von four
six = newValue;
}
return six;
}
public void stateOfChange() {
four++;
}
}
Daraus ergibt sich dieser Baum:
http://img373.imageshack.us/img373/3382/beispielyc8.gif
Der Nutzer ruft nun getSix() auf. Das Objekt geht in den Getter, bemerkt, dass das Attribut 'six' noch NULL ist und holt sich 'four' - wieder per get. In getFour() dann das Gleiche. 'four' ist noch NULL, also geht man zu getTwo(), der dann den Wert vom Attribut 'two' zurückliefern kann. Damit wird 'four' berechnet und letztendlich folgt daraus 'six'.
Graphisch sähe das so aus:
http://img373.imageshack.us/img373/4207/beispielverlaufiv9.gif
Nun kommt es zu einer Zustandsänderung von 'four' - stateOfChange() wird aufgerufen.
Alle bereits berechneten, von 'four' abhängigen Attribute sind nun nicht mehr korrekt.
http://img373.imageshack.us/img373/8227/beispielverndertue2.gif
Ruft man nun getSix() auf, wird ein falscher Wert zurückgegeben, dabei müsste eigentlich eine Neuberechnung von 'six' angestoßen werden.
Eine neue Berechnung kann damit erwirkt werden, dass 'six' NULL gesetzt wird.
Wie kann 'six' automatisch NULLified werden, wenn sich 'four' ändert? 'four' - als Vater - weiß nix von seinen Töchtern.
Was meinst du mit "darauf achten"? Du musst doch irgendwo für ein Attribut festlegen, von welchen anderen Attributen es abhängt. Wieso kannst du nicht zum gleichen Zeitpunkt die umgekehrte Abhängigkeit speichern?
Mit steigender Komplexität kann man nicht mehr auf dem ersten Blick erkennen, welches Attribut von welchen Attributen abhängt.
Und was ist z.B. wenn die Tochter der Tochterstochter nun für ein völlig neues Attribut benötigtet wird? Dann muss die komplette Klasse überprüft werden, was nicht ohne Weiteres möglich ist, weil zu komplex und man erkennt auf dem ersten Blick nicht mehr, wer von wem abhängt. ;)
Die andere Möglichkeit ist dass der Getter jedes Mal nachprüft dass alle Attribute darüber nicht NULL sind.
Das Gleiche in grün.
ethrandil
2008-03-11, 00:44:26
Hier mal einfach ins blaue geschrieben und nicht unbedingt schön:
public class State {
private Integer value;
provate List<State> dependentStates;
public State(int value){
this.value=value;
this.dependentStates = new LinkedList<State>();
}
/** Registriert einen neuen von diesem abhängigen State */
public void addDependant(State dep){dependentStates.add(dep);}
/** wird vom vorfahre gerufen, wenn er sich verändert und dieser State sich mit ändern soll */
public void ancestorChanged(){ updateValue(); }
/** updated den State irgendwie */
protected void updateValue(){}
public void getValue() {
if(value == null)
updateValue();
return value;
}
public void setValue(int v) {
this.value = v;
for(State s: this.dependentStates)
s.ancestorChanged();
}
}
public class SumState extends State {
private State s1, s2;
public SumState(State s1, State s2){
this.s1 = s1;
this.s2 = s2;
}
protected void updateValue(){
setValue(s1.getValue() + s2.getValue())
}
}
public class Runner{
public static void main(String[] args){
State one = new State(1);
State two = new State(2);
State three = new SumState(one, two);
State four = new SumState(one, three);
one.addDependant(three);
two.addDependant(three);
one.addDependant(four);
three.addDependant(four);
System.out.println(three.getValue()); // -> 3?
System.out.println(four.getValue()); // -> 4?
one.setValue(4);
System.out.println(three.getValue()); // -> 6?
System.out.println(four.getValue()); // -> 10?
}
}
Es gibt die Klasse State, die einfach einen Integer enthält (das ist nicht unbedingt eine schöne modellierung, ein state muss ja keinen integer halten, ich habs aber mal von dir übernommen). Jeder State hat eine Liste mit von ihm abhängenden anderen States.
Der SumState hat zusätzlich zwei Referenzen auf andere States. Er berechnet seinen Wert aus der Summe deren beiden Werte.
In der main-Methode werden erst alle states erzeugt und dann richtig als Dependants registriert. (Das könnte man auch in den Konstruktor von SumState ziehen.)
Wenn sich dann in einem der States etwas ändert, weil setValue() aufgerufen wird, benachrichtigt er danach alle von ihm abhängigen States! Achtung: Da SumState die set-Methode der Vaterklasse nutzt, benachrichtigt SumState auch alle Nachfolger, genau wie es sein soll.
Der Code ist ungetestet, quasi freihand, aber er soltle die Idee klar machen.
- eth
Bietchiebatchie
2008-03-11, 06:00:52
Alles absolut ok, bis auf
In der main-Methode werden erst alle states erzeugt und dann richtig als Dependants registriert. (Das könnte man auch in den Konstruktor von SumState ziehen.)
Die Dependants würde ich auf jeden fall intern (in der State-Klasse) registrieren. Ansonsten werden das das einfach zuviel addDependant-Aufrufe bei größeren Graphen, da macht man ja zwangsläufig Fehler ;)
Mit steigender Komplexität kann man nicht mehr auf dem ersten Blick erkennen, welches Attribut von welchen Attributen abhängt.
Und was ist z.B. wenn die Tochter der Tochterstochter nun für ein völlig neues Attribut benötigtet wird? Dann muss die komplette Klasse überprüft werden, was nicht ohne Weiteres möglich ist, weil zu komplex und man erkennt auf dem ersten Blick nicht mehr, wer von wem abhängt. ;)
Wieso, du musst doch letztlich nur die direkten Abhängigkeiten modellieren; die indirekten ("Tocher der Tochtersknoten") sind ja automatisch gegeben, da Abhängigkeit sicher transitiv ist. (transitiv heißt: wenn A von B abhängt und B von C dann hängt A auch in jedem Fall von C ab)
ethrandil
2008-03-11, 10:20:38
Die Dependants würde ich auf jeden fall intern (in der State-Klasse) registrieren. Ansonsten werden das das einfach zuviel addDependant-Aufrufe bei größeren Graphen, da macht man ja zwangsläufig Fehler ;)
Joah, ne. Ich habe das nicht gemacht, um diese Registrierung für den Objektersteller explizit zu machen. Genau genommen ergibt sich nämlich ein Problem für den Garbagecollector, wenn man mal ein paar States nicht mehr braucht, die aber noch irgendwie irgendwo in einer der dependant-listen rumlungern. Dann braucht man eigentlich noch ne removeDependant(..) Methode. Wie gesagt dein Argument gilt schon, man muss nur wissen was man tut ^_^. [Oder man macht das mit WeakReference!]
- eth
Monger
2008-03-11, 11:02:35
Wie kann 'six' automatisch NULLified werden, wenn sich 'four' ändert? 'four' - als Vater - weiß nix von seinen Töchtern.
IRGENDJEMAND muss wissen, dass 'four' irgendwas mit 'six' zu tun hat. Wenn nicht 'four' selbst, dann irgendjemand anderes. Wenn du so etwas nicht weißt, musst du auf Verdacht mal alle Knoten durchgehen und suchen - das steht aber im krassen Widerspruch zu deinen Performancebestrebungen, nicht wahr?
Mit steigender Komplexität kann man nicht mehr auf dem ersten Blick erkennen, welches Attribut von welchen Attributen abhängt.
Das ist eins der vielen Probleme hier. Je allgemeiner und generischer dein Code, desto schwerer wird es, Abhängigkeiten darin zu erkennen. Das Problem hast du aber immer, und je besser du dein System kapselst, desto weniger braucht dich das zu interessieren. Was willst du also haben: Flexibilität oder Sicherheit?
Bei deinem Problem stellen sich mir immer noch zu viele ungeklärte Fragen. Wie genau sehen denn deine Berechnungen aus? Ist es z.B. immer eine Addition aller Werte, oder muss man da auch nochmal unterscheiden?
In C# könnte man sich da mit den Delegates viel Arbeit ersparen, in Java wirst du möglicherweise nicht drumherum kommen, einen eigenen Typen für eine Operation zu überlegen. Das kann ganz schön fummlig werden.
Ich finde auch diese Überprüfung auf Null unglaublich hässlich. Besser imho wäre, davon auszugehen dass die Werte gültig sind wenn man auf sie zugreift, und parallel dazu dann sicherzustellen dass sie es auch wirklich sind.
Mein Gedanke geht in eine ganz ähnliche Richtung wie ethrandils:
public class StateNode{
private int basicValue;
private int calculatedValue;
private List<StateNode> changeListeners;
private List<StateNode> operands;
public void addOperand(StateNode n){
n.changeListeners.add(this);
this.operands.add(n);
this.onChange();
// remove und clear analog dazu
}
private void onChange(){
// kann von verschiedenen Stellen aus aufgerufen werden
calculatedValue = basicValue;
for(StateNode n : operands){
calculatedValue += calculatedValue;
}
for(StateNode n : changeListeners){
n.onChange();
}
}
public int getValue(){ return calculatedValue; }
public void setValue(int val){
basicValue = val;
onChange();
}
}
Du baust also deinen Baum auf, indem du Knoten für Knoten erzeugst, und deren Operanden hinzufügst. Jede Operation die die Datenhaltung verändern kann, MUSS auf jeden Fall "onChange" aufrufen!
Dann kannst du davon ausgehen, dass deine Daten immer aktuell sind, und getValue tatsächlich auch den aktuell richtigen Wert ausgibt. Wie deine Berechnungen intern aussehen - naja, das kann beliebig kompliziert werden.
gereggter Gast
2008-03-11, 11:26:00
Hier mal einfach ins blaue geschrieben und nicht unbedingt schön:
public class State {
private Integer value;
provate List<State> dependentStates;
public State(int value){
this.value=value;
this.dependentStates = new LinkedList<State>();
}
/** Registriert einen neuen von diesem abhängigen State */
public void addDependant(State dep){dependentStates.add(dep);}
/** wird vom vorfahre gerufen, wenn er sich verändert und dieser State sich mit ändern soll */
public void ancestorChanged(){ updateValue(); }
/** updated den State irgendwie */
protected void updateValue(){}
public void getValue() {
if(value == null)
updateValue();
return value;
}
public void setValue(int v) {
this.value = v;
for(State s: this.dependentStates)
s.ancestorChanged();
}
}
public class SumState extends State {
private State s1, s2;
public SumState(State s1, State s2){
this.s1 = s1;
this.s2 = s2;
}
protected void updateValue(){
setValue(s1.getValue() + s2.getValue())
}
}
public class Runner{
public static void main(String[] args){
State one = new State(1);
State two = new State(2);
State three = new SumState(one, two);
State four = new SumState(one, three);
one.addDependant(three);
two.addDependant(three);
one.addDependant(four);
three.addDependant(four);
System.out.println(three.getValue()); // -> 3?
System.out.println(four.getValue()); // -> 4?
one.setValue(4);
System.out.println(three.getValue()); // -> 6?
System.out.println(four.getValue()); // -> 10?
}
}
Es gibt die Klasse State, die einfach einen Integer enthält (das ist nicht unbedingt eine schöne modellierung, ein state muss ja keinen integer halten, ich habs aber mal von dir übernommen). Jeder State hat eine Liste mit von ihm abhängenden anderen States.
Der SumState hat zusätzlich zwei Referenzen auf andere States. Er berechnet seinen Wert aus der Summe deren beiden Werte.
In der main-Methode werden erst alle states erzeugt und dann richtig als Dependants registriert. (Das könnte man auch in den Konstruktor von SumState ziehen.)
Wenn sich dann in einem der States etwas ändert, weil setValue() aufgerufen wird, benachrichtigt er danach alle von ihm abhängigen States! Achtung: Da SumState die set-Methode der Vaterklasse nutzt, benachrichtigt SumState auch alle Nachfolger, genau wie es sein soll.
Der Code ist ungetestet, quasi freihand, aber er soltle die Idee klar machen.
- eth
Hi ethrandil, da ich bereits eine Idee für einen Automatismus habe, ist das manuelle Registrieren der Dependants im vornherein leider keine Option. Dennoch danke, deine Idee mit den State-Objekten läßt sich bestimmt übernehmen. :)
Wieso, du musst doch letztlich nur die direkten Abhängigkeiten modellieren; die indirekten ("Tocher der Tochtersknoten") sind ja automatisch gegeben, da Abhängigkeit sicher transitiv ist. (transitiv heißt: wenn A von B abhängt und B von C dann hängt A auch in jedem Fall von C ab)
Was ist mit solchen Fällen? ;)
public Integer getSix() {
if(six == null) {
valueOfFour = getFour();
if(valueOfFour < 0) {
Integer newValue = ..; // mach was mit dem Wert von four
six = newValue;
}
else {
valueOfThree = getThree();
Integer newValue = ..; // mach was mit dem Wert von three
six = newValue;
}
}
return six;
}
Vielleicht hängt 'six' von 'three' ab und nicht von 'four'. Das ist dann aber vom Zustand von 'four' abhängig und stellt sich erst bei der Berechnung von 'six' heraus.
IRGENDJEMAND muss wissen, dass 'four' irgendwas mit 'six' zu tun hat. Wenn nicht 'four' selbst, dann irgendjemand anderes. Wenn du so etwas nicht weißt, musst du auf Verdacht mal alle Knoten durchgehen und suchen - das steht aber im krassen Widerspruch zu deinen Performancebestrebungen, nicht wahr?
Ja, 'six' weiß, dass es von 'four' abhängt. Und zwar merkt 'six' das während seiner Berechnung.
Bei deinem Problem stellen sich mir immer noch zu viele ungeklärte Fragen. Wie genau sehen denn deine Berechnungen aus? Ist es z.B. immer eine Addition aller Werte, oder muss man da auch nochmal unterscheiden?
Zugriffe auf Web-Service, Datenbankabfragen, ...
Ich finde auch diese Überprüfung auf Null unglaublich hässlich. Besser imho wäre, davon auszugehen dass die Werte gültig sind wenn man auf sie zugreift, und parallel dazu dann sicherzustellen dass sie es auch wirklich sind.
Hmm, jetzt wo du das sagst. Stimmt schon. :smile: Vielleicht ließe sich deine Arbeitsweise adaptieren, muss sie mir aber erst nochmal genauer ansehen. :smile:
Du baust also deinen Baum auf, indem du Knoten für Knoten erzeugst, und deren Operanden hinzufügst. Jede Operation die die Datenhaltung verändern kann, MUSS auf jeden Fall "onChange" aufrufen!
Dann kannst du davon ausgehen, dass deine Daten immer aktuell sind, und getValue tatsächlich auch den aktuell richtigen Wert ausgibt. Wie deine Berechnungen intern aussehen - naja, das kann beliebig kompliziert werden.
Meine Idee geht in eine ähnliche Richtung. Ich baue den Baum automatisch während der Berechnung auf. Nur weiß ich noch nicht genau wie. :redface:
Monger
2008-03-11, 11:59:52
Was ist mit solchen Fällen? ;)
...
Vielleicht hängt 'six' von 'three' ab und nicht von 'four'. Das ist dann aber vom Zustand von 'four' abhängig und stellt sich erst bei der Berechnung von 'six' heraus.
So eingeflanschtes Verhalten könnte man über anonyme Klassen lösen, außerdem könnte man dein obiges Beispiel in eigentlich zwei getrennte Beziehungen aufbrechen statt nur einer.
Zugriffe auf Web-Service, Datenbankabfragen, ...
Puh, MUSS das sein? Du schmeißt hier im Prinzip Daten- , Logik- und Interfaceschicht in einen Topf. Das gibt zwangsläufig Kuddelmuddel. Muss z.B. ein Knoten wirklich einen Datenbankzugriff machen, alle anderen dagegen nicht?
Hört sich für mich ziemlich abenteuerlich an.
gereggter Gast
2008-03-11, 12:39:11
So eingeflanschtes Verhalten könnte man über anonyme Klassen lösen, außerdem könnte man dein obiges Beispiel in eigentlich zwei getrennte Beziehungen aufbrechen statt nur einer.
Das war jetzt nur als Beispiel gedacht. Ich glaube nicht, dass dies wirklich vorkommt. :D
Aber du hast Recht, man kann die Beziehungen noch auftrennen (dh, wenn ich dich jetzt richtig verstanden habe :uponder:).
Puh, MUSS das sein? Du schmeißt hier im Prinzip Daten- , Logik- und Interfaceschicht in einen Topf. Das gibt zwangsläufig Kuddelmuddel. Muss z.B. ein Knoten wirklich einen Datenbankzugriff machen, alle anderen dagegen nicht?
Hört sich für mich ziemlich abenteuerlich an.
In dem eigentlichen Datenobjekt befinden sich nur die Werte, sowie die Aufrufe zur Berechnung. Die Aufrufe gehen dann in eine andere Klasse, wo die Logik und die Zugriffe auf andere Datenobjekte erfolgen.
ethrandil
2008-03-11, 17:34:21
Also, dein Beispiel ist kein geeignetes Gegenbeispiel zum vorher registrieren.
Na klar, der State weiß erst zur Laufzeit, welche Eingangsdaten er wirklich braucht. Aber er weiß schon vorher, welche er maximal brauchen wird!
Und dann wäre das einfach so:
protected void updateValue(){
if(s1.getValue() > 0)
setValue(s1.getValue());
else
setValue(s2.getValue());
}
Ist doch kein Problem, das vorher zu registrieren. Und wesentlichen Overhead bedeutet es auch nicht.
mfg
- eth
registrierter Gast
2008-03-17, 12:11:38
Wenn die Programmiersprache Codegeneration und Introspektion unterstützt kann man den Code für die Aktualisierung der Attribute per Programm generieren.
In Java z.B. mit http://asm.objectweb.org/
Mr. Trap hatte die Lösung. :smile: Habe es zwar noch nicht endgültig implementiert, aber schon mal eine Designstudie zum obigen Beispiel angelegt.
Für die Implementation habe ich nicht ASM verwendet, sondern cglib (http://cglib.sourceforge.net/), worauf wohl auch ASM aufbaut (?!).
Alle getter und setter werden nun überwacht. Sobald ein getter aufgerufen wird, wird dessen kompletter Trace über die Vater-getter gespeichert.
Dies tut die Observer.java
package de.example.observation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class Observer implements MethodInterceptor {
private List<String> trace;
private int recursive = 1;
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// getters and 'issers' are the methods which are observed
String methodName = method.getName();
if(methodName.startsWith("get")) {
getCurrentTrace().add(methodName.substring(3));
}
else if(methodName.startsWith("is")) {
getCurrentTrace().add(methodName.substring(2));
}
else {
System.out.println("the method " + methodName + " is not of type getter");
}
// call the method
++recursive;
Object resultSuperInvoked = proxy.invokeSuper(obj, args);
--recursive;
// add final trace to list of traces
if(recursive == 1 && trace != null) {
// do not add trace if it's a single get-call
if(trace.size() > 1) {
WrongCorrect.getTraces().add(trace);
}
trace = null;
}
return resultSuperInvoked;
}
private List<String> getCurrentTrace() {
if(trace == null) {
trace = new ArrayList<String>();
}
return trace;
}
}
Kommt es zu einem set-Aufruf, geht man alle bis dahin vorhandenen Traces durch und schaut, welche Attribute bisher von diesem neu gesetzten Wert abhängig sind.
Das macht die Remover.java
package de.example.observation;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class Remover implements MethodInterceptor {
private boolean onIntercepting;
public static final Method[] BEAN_METHODS = Bean.class.getDeclaredMethods();
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// just intercept if this method doesn't nullify already
if(onIntercepting == false) {
// collect all setters, which has to be called
Set<Method> setMethods = new HashSet<Method>();
String changedValue = method.getName().substring(3);
for(List<String> traces : WrongCorrect.getTraces()) {
if(traces.contains(changedValue) == false) {
continue;
}
// the current trace has the changed value in it
for(String step : traces) {
if(step.equals(changedValue)) {
break;
}
// find matching setter
Method setter = findSetter("set" + step);
setMethods.add(setter);
}
}
// call all collected and nullify those values
onIntercepting = true;
for(Method setter : setMethods) {
setter.invoke(obj, new Object[] {null});
}
onIntercepting = false;
}
// call the method
return proxy.invokeSuper(obj, args);
}
private Method findSetter(String setter) {
for(Method method : BEAN_METHODS) {
if(method.getName().equals(setter)) {
return method;
}
}
return null;
}
}
Damit sieht die Bean.java nach wie vor relativ clean aus (bis auf die Überprüfungen auf null). Wichtig ist allerdings, dass alle Setter und Getter Namen nach der Code-Convention haben!
package de.example.observation;
public class Bean {
private Integer two;
private Integer three;
private Integer four;
private Integer five;
private Integer six;
public Bean(int value) {
// initialize start-value
two = value;
}
public Integer getTwo() {
System.out.println("called: getTwo(), value: " + two);
return two;
}
public Integer getThree() {
if(three == null) {
Integer valueOfTwo = getTwo();
three = valueOfTwo * 2;
}
System.out.println("called: getThree(), value: " + three);
return three;
}
public Integer getFour() {
if(four == null) {
Integer valueOfTwo = getTwo();
four = valueOfTwo * valueOfTwo;
}
System.out.println("called: getFour(), value: " + four);
return four;
}
public Integer getFive() {
if(five == null) {
Integer valueOfFour = getFour();
five = valueOfFour + 3;
}
System.out.println("called: getFive(), value: " + five);
return five;
}
public Integer getSix() {
if(six == null) {
Integer valueOfFour = getFour();
six = valueOfFour * 5;
}
System.out.println("called: getSix(), value: " + six);
return six;
}
public void setTwo(Integer two) {
this.two = two;
}
public void setThree(Integer three) {
this.three = three;
}
public void setFour(Integer four) {
this.four = four;
}
public void setFive(Integer five) {
this.five = five;
}
public void setSix(Integer six) {
this.six = six;
}
}
Der Programmierer muss sich um nix mehr kümmern, wie der JUnit-Test WrongCorrect.java zeigt.
package de.example.observation;
import static org.junit.Assert.assertEquals;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.CallbackFilter;
import net.sf.cglib.proxy.Enhancer;
public class WrongCorrect {
private static List<List<String>> traces = null;
public static List<List<String>> getTraces() {
if(traces == null) {
traces = new ArrayList<List<String>>();
}
return traces;
}
public static Bean newInstance(int initializeValue) {
try {
Enhancer e = new Enhancer();
e.setSuperclass(Bean.class);
Callback[] callbacks = new Callback[] {new Observer(), new Remover()};
e.setCallbacks(callbacks);
e.setCallbackFilter(new CallbackFilter() {
public int accept(Method m) {
return (m.getName().startsWith("set")) ? 1 : 0;
}
});
return (Bean) e.create(new Class[] {int.class}, new Object[] {initializeValue});
}
catch(Throwable e) {
e.printStackTrace();
throw new Error(e.getMessage());
}
}
@Test
public void correct() {
System.out.println("Unit-Test - observed object");
Bean bean = newInstance(2);
System.out.println("calculate value for six...");
Integer six = bean.getSix();
assertEquals(six, 20);
System.out.println("\nchange value of four!");
bean.setFour(10);
System.out.println("\ncalculate value for six...");
six = bean.getSix();
assertEquals(six, 50);
}
@Test
public void wrong() {
System.out.println("Unit-Test - plain object");
Bean bean = new Bean(2);
System.out.println("calculate value for six...");
Integer six = bean.getSix();
assertEquals(six, 20);
System.out.println("\nchange value of four!");
bean.setFour(10);
System.out.println("\ncalculate value for six...");
six = bean.getSix();
assertEquals(six, 50);
}
}
Zu tun ist noch, dass alle Traces bisher noch relativ stupide gespeichert werden. Verändert man zwei mal den Wert von 'four' hat man letztendlich drei Traces in der Liste aller Traces, die alle das selbe aussagen.
Da werde ich wohl Mongers Programmierarbeit übernehmen. :biggrin:
vBulletin®, Copyright ©2000-2024, Jelsoft Enterprises Ltd.