1 zu 1 Beziehung



  • Es sei folgende Situation gegeben:

    class A{
        B b = null;
        public B B{
            set{b = value;}
            get{return b;}
        }
    }
    
    class B{
        A a = null;
        public A A{
            set{a = value;}
            get{return a;}
        }
    }
    

    Ein A steht also höchstens mit einem B in Verbindung und umgekehrt. Die Verknüpfung soll in beide Richtungen lesbar sein und immer gültig oder null sein. Das heißt, dass immer gilt, dass a.B.a == a für jedes beliebige a (mit einem nicht null B).

    Der Code oben könnte wie folgt verwendet werden.

    A a = new A;
    a.B = new B;
    a.B.a = a;
    

    Die letzte Zeile zeigt auch schon mein Problem. Es ist die Aufgabe des Benutzers sicher zu stellen, dass die Rückreference immer stimmt. Da man dabei doch recht schnell schwer zu findende Flüchtigkeitsfehler macht will ich den Code in die Properties selbst verschieben.

    class A{
        private B b = null;
        public B B{
            set{
                if(b != null){
                    B old_b = b;
                    b = null;
                    if(old_b.A == this)
                        old_b.A = null;
                }
                b = value;
                b.A = this;
            }
            get{return b;}
        }
    }
    

    Die B Klasse ist völlig analog.

    Dies löst zwar mein ursprüngliches Problem allerdings ist die Implementierung selbst anfällig für Flüchtigkeitsfehler und Endlosrekursionen.

    Da dies ein Pattern ist welches in meinem Code immer wieder vor kommt wenn ich komplexere* Datenstrukturen verwende wollte ich fragen wie Ihr das löst und ob es da etwas besseres gibt?

    *komplex im Sinn von komplexeren Daten und nicht im Sinn von algorithmisch komplexeren Strukturen.



  • http://www.c-plusplus.net/forum/viewtopic-var-t-is-220046.html

    In dem Thread wird (glaube ich ;-)) genau dein Problem gelöst!

    Grüße



  • Sobald Du es dem Benutzer erlaubst, diese Objekte selbst zu erstellen, handelst Du Dir potentielle Fehler ein.

    Die Lösung ist, dem Benutzer die Erstellung ganz einfach zu verbieten (Konstruktor internal oder besser noch private machen) und eine Fabrikmethode (entweder die „richtige“, formelle, oder ein ähnliches Konstrukt) zur Erstellung zu verwenden.



  • Ich sehe schon, es scheint kein Allheilmittel für das Problem zu geben. 😉 Ich bring lieber mal ein konkretes Beispiel sonst Reden wir nur aneinander vorbei.

    Ziel ist es ein Karten-Widget zu entwickeln welches mehrere Eingabemodi kennt. Die Logik der Eingabemodi will ich auslagern in eigene Klassen und erweiterbar halten. Der Anwender soll eigene definieren können. Ein Eingabemodus wäre zum Beispiel das Zentrieren der Karte auf den Cursor bei einem Linksclick. Ein andere wäre das Zoomen. Ein weiter könnte ein Auswahlrechteck zeichnen. Die genauen Modi sind uninteressant es geht nur darum, dass es viele sind und die zustandbehaftet sind.

    Im Moment habe ich dies wie folgt gelöst:

    public class Map: ...{
      public abstract class Modus{
        internal Map map;
        public Map Map{
          get{return map;}
        }
    
        public abstract void OnLeftClick(Point p);
        ...
        public virtual void OnCancel(){}
      }
    
      Modus modus = new DefaultModus;
    
      public Modus Modus{
        set{
          modus.OnCancel();
          modus = value;
          modus.map = this;
        }
      }
    
      public void Center(Point p){
        ...
      }
    
      public void Zoom(Point p, double factor){
        ...
      }
    }
    
    public class DefaultModus:Map.Modus{
      public override void OnLeftClick(Point p){
        Map.Center(p);
      }
    }
    
    public class ZoomModus:Map.Modus{
      private double factor;
      ZoomModus(double factor){
        this.factor = factor;
      }
    
      public override void OnLeftClick(Point p){
        Map.Zoom(p, factor);
        Map.Modus = new DefaultModus();
      }
    }
    

    Das Widget kriegt man ganz einfach in den ZoomModus versetzt.

    map.Modus = new ZoomModus(2.0);
    

    Das Problem ist, dass ich allerdings auch folgendes machen kann:

    Map.Modus m = new ZoomModus(2.0);
    map1.Modus = m;
    map2.Modus = m; 
    // map1 ist nun in einem illegalen Zustand
    

    Wenn ich den Factory Ansatz richtig verstehe, dann würde Map gar keinen Modus annehmen, sondern nur eine Modus-erzeugende Klasse und so wäre sicher gestellt, dass der User nie einen Modus in zwei Maps einschleusen könnte. Hierzu muss ich allerdings die Gesamte Modi-Hierarchie duplizieren wenn ich das richtig sehe.

    Als Alternative sehe ich noch, dass map2.Modus = m als Nebeneffekt map1 in den DefaultModus versetzen würde und wahrscheinlich noch eine Art Reset Methode auf m anwenden würde. Dies kämme meinem Ansatz im Ausgabspositing sehr nahe.

    Eventuell könnte man das ganze auch als Automat implementieren. Jeder Zustand würde einem Modus entsprechen und würde bei einer Eingabe eine Referenz auf den Folgezustand zurückgeben. Dadurch werden die Modiklassen selbst zustandslos und können ohne Probleme von zwei Maps geteilt werden.

    Jetzt wo ich nochmal drüber nachdenke, gefällt mir die 3. Alternative immer besser. Allerdings mogelt die sich an der 1 zu 1 Beziehung vorbei. Wie würdest du das anpacken?



  • Hmm, gute Beschreibung, jetzt ist das Problem klar.

    Ich denke, die enfachste oder sauberste Lösung wäre hier, die 'Modus'-Klasse immutable zu machen. D.h. die dazugehörige Map muss per Konstruktor gesetzt werden und kann dann nicht mehr verändert werden. Die Public-Property 'Modus' fiele dann weg (= internal machen):

    public class Map: ...{
      public abstract class Modus{
        private readonly Map map;
        public Map Map{get {return map;}}
    
        protected Modus(Map map){
          this.map = map;
          map.Modus = this;
        }
    
        public abstract void OnLeftClick(Point p);
        ...
        public virtual void OnCancel(){}
      }
    
      // Spezialinstanz zum Resetten.
      private readonly Modus Default = new DefaultModus(this);
    
      Modus modus;
    
      // Wird nur an zwei Stellen aufgerufen:
      // In 'Reset' und im 'Modus'-Kostruktor.
      internal Modus Modus{
        set{
          modus = value;
          modus.OnCancel();
        }
      }
    
      public void ResetModus() {
        Modus = Default;
      }
    
      public void Center(Point p){
        ...
      }
    
      public void Zoom(Point p, double factor){
        ...
      }
    }
    
    public class DefaultModus:Map.Modus{
      public DefaultModus(Map map) : base(map) { }
      public override void OnLeftClick(Point p){
        Map.Center(p);
      }
    }
    
    public class ZoomModus:Map.Modus{
      private readonly double factor;
      public ZoomModus(Map map, double factor) : base(map) {
        this.factor = factor;
      }
    
      public override void OnLeftClick(Point p){
        Map.Zoom(p, factor);
        Map.ResetModus();
      }
    }
    


  • Das Design gefällt mir nicht wirklich. Ich finde den Syntax der notwendig ist um eine Map in einen Modus zu versetzen recht speziell:

    new ZoomModus(map, 2)
    

    Neben dem speziellen Syntax verbietet es dieser Ansatz auch den Modus unabhängig von der Map zu wählen. Code wie folgender geht nicht mehr:

    if(config_foo_bar.is_true())
      return new ZoomModus(2);
    else
      return new PopupModus("No clicking allowed");
    

    Der Code muss die Map kennen obwohl diese an sich nichts mit der Logik zu tun hat. Ein seltener aber problematischer Fall wäre die Situation wo man den Modus auswählt ehe die Map erstellt wurde.

    Wieso ein Modus in deinem Fall immutable sein muss verstehe ich auch nicht. Du hast ja dafür gesorgt, dass es immer eine gültige 1 zu 1 Beziehung gibt dann kannst du doch auch den Zustand des Modus' nach belieben verändern.

    So wie ich den Code versteh muss der Modus sogar unter Umständen veränderlich sein. Um auf den Auswahlrechteck-Modus zurück zu kommen:

    public delegate void RectangleSelectionHandler(Map m, Point start, Point end);
    
    public class RectanleSelectionModus:Map.Modus{
      public RectanleSelectionModus(Map map) : base(map) {
      }
    
      bool dragging = false;
      Point start, end;
    
      public override void OnLeftButtonDown(Point p){
        dragging = true;
        start = p;
        end = p;
      }
    
      public event RectangleSelectionHandler RectangleSelectionEvent;
    
      public override void OnLeftButtonUp(Point p){
        if(dragging){
          dragging = false;
          Map.QueueRedraw();
          if(RectangleSelectionEvent != null)
            RectangleSelectionEvent(Map, start, end);
          Map.ResetModus();
        }
      }
    
      public override void OnMouseMove(Point p){
        if(dragging){
          end = p;
          Map.QueueRedraw();
        }
      }
    
      public override void OnDraw(Surface s){
        if(dragging)
          s.DrawRect(start, end);
      }
    }
    

    Dieser Modus ist auch das Problem mit meinem Automaten-Ansatz. Jede Position des Mauszeigers ist ein eigener Zustand. Jedes Bewegen des Mauszeiger würde also die Allokation eines neuen RectanleSelectionModus zur Folge haben. Ein new ist dank des GC zwar sehr billig aber dennoch nicht umsonst vor allem verglichen mit der Alternative welche nur zwei ints verändert.

    OT Frage: Soll RectangleSelectionHandler in RectanleSelectionModus definiert sein oder so wie es jetzt ist? Sollte ich den delegate in das void Handler(object sender, EventArgs args) Schema pressen oder so lassen?



  • Ben04 schrieb:

    Neben dem speziellen Syntax verbietet es dieser Ansatz auch den Modus unabhängig von der Map zu wählen.

    Das ist ein berechtigter Einwand.

    Wieso ein Modus in deinem Fall immutable sein muss verstehe ich auch nicht.

    Immutable-Strukturen ersparen einem einen Haufen Arbeit, gerade in dem Fall, dass man ungültige Zustände ausschließen muss, denn was einmal gültig ist, wird danach garantiert nicht mehr ungültig.

    Du hast aber recht, das war hier nicht ursächlich.

    Gut, anderer, zu Events analoger Ansatz: 'AttachMap' und 'DetachMap'-Methoden. Dann musst Du die 'Modus'-Eigenschaft der Map folgendermaßen modifizieren:

    public Modus Modus {
        set {
            value.DetachMap();
            modus = value;
            modus.AttachMap(this);
        }
    }
    

    … und die abstrakte Modus-Klasse wird entsprechend erweitert:

    // Convenience property, damit nicht dauernd Instanzen erzeugt werden müssen.
    public static readonly Modus Default = new DefaultModus();
    
    public void AttachMap(Map map) {
        this.map = map;
    }
    
    public void DetachMap() {
        if (map != null)
            map.Modus = Modus.Default;
        map = null;
    }
    

    So. Jetzt nochmal Dein Code:

    Modus zoom = new ZoomModus(2);
    map1.Modus = zoom;
    map2.Modus = zoom;
    Debug.Assert(map1.Modus is DefaultModus);
    

    – Dieses Verhalten ist deferred ownership. Ein anderes Verhalten, das noch eher an die Eventhandler herankommt, wäre, in der Modus-Klasse eine Liste von Maps zu speichern, dann ist das Verhältnis natürlich nicht mehr 1:1. Dafür müssen einfach die Funktionen angepasst werden:

    public void AttachMap(Map map) {
        maps.Add(map);
    }
    
    public void DetachMap(Map map) {
        if (maps.Remove(map))
            map.Modus = Modus.Default;
    }
    

    … analog muss dann natürlich die Aktion auf mehrere Maps angewandt werden.

    Ansonsten sehe ich nicht, wie Du 1:1-Mappings garantieren willst, wenn alle Modifikatoren öffentlich sind.



  • … noch etwas, was mir gerade einfällt. Eventuell wäre es eine Alternative, die Modus als Decorators zu gestalten, die über eine Map rübergelegt werden. Wenn Dir das nichts sagt, lies mal in der Wikipedia nach.


Anmelden zum Antworten