Typunabhaengig arbeiten, abstrakte Klasse?



  • Hallo,

    ich habe folgendes Problem. Moechte eine Diagramm-Bibliothek erstellen und dabei folgenden Sachverhalt in C# umzusetzen. Verschiedene Diagramm-Achsen koennen verschiedene Probleme beschreiben. Beispielsweise eine rein numerische Achse von 0 - 100 oder eben auch eine Zeitachse von 6:00 - 18:00. Unabhaengig vom jeweiligen Achsentyp gibt Gemeinsamkeiten aller Achsen. Beispielsweise

    - Minimalwert
    - Maximalwert
    ...

    Daher folgender Ansatz:

    abstract class Axis
    class AxisNumerical : Axis
    class AxisTime : Axis

    Nun habe ich die Gemeinsamkeiten in die abstrakte Klasse gelegt:

    abstract class Axis
    {
      ...
      public object MinGlobal
      {
        get
        {
          return this.minGlobal; 
        }
        set
        {
          this.minGlobal = value; 
        } 
      }
      protected object minGlobal; 
    }
    

    Den Typ object (beispielsweise fuer minGlobal) habe ich gewaehlt um zu abstrahieren.
    Bedacht habe ich freilich nicht, dass dieser Typ nicht wirklich zum Rechnen dient. Will ich bspw. in der Klasse AxisNumerical mit this.minGlobal Berechnungen (+ - * 😵 durchfuehren, klappt das natuerlich nicht.
    Wie kann ich das Problem loesen? Sollte ich die eigentlich gemeinsamen Elemente Minimalwert, Maximalwert... aus der abstrakten Klasse nehmen und dann typbezogen in die jeweiligen Ableitungen einbinden?

    class AxisNumerical : Axis
    {
    ...
      public float MinGlobal
      {
        get
        {
          return this.minGlobal; 
        }
        set
        {
         this.minGlobal = value; 
        }
      }
      protected float minGlobal;
    ...
    }
    
    class AxisTime : Axis
    {
    ...
      public TimeSpan MinGlobal
      {
        get
        {
          return this.minGlobal; 
        }
        set
        {
         this.minGlobal = value; 
        }
      }
      protected TimeSpan minGlobal;
    ...
    }
    

    Bin fuer jeden Hinweis dankbar um dieses Problem zu loesen. Besten Dank!
    Dr. Goebel



  • Kannst Du mit .NET 2.0 arbeiten?
    Dann würde ich mal über einen Ansatz in ungefähr folgender Richtung nachdenken ( wenn es Deine Anwendung zulässt ).

    public class Axis<T>
    {
        private T minGlobal;
        public T MinGlobal
        {
            get { return this.minGlobal; }
            set { this.minGlobal = value; }
        }
    }
    


  • Schoenen Danke, jedoch kann ich Generics leider nicht verwenden. .NET 2.0 bleibt mir verwehrt.
    Weitere Ansaetze?

    Cheers,
    Dr. Goebel



  • Hallo,

    ich würde in früheren Versionen aus Axis ein Interface
    machen und es einfach explizit implementieren.

    public interface IAxis 
    {
        // ...
    
        object MinGlobal {
            get;
            set;
        }
    
        // ...
    }
    
    public class AxisNumerical : IAxis
    {
        private float minGlobal;
    
        // ...
    
        object IAxis.MinGlobal {
            get { return minGlobal; }
            set {
                if(!(value is float)) {
                    throw new ArgumentException(/* .. */);
                }
    
                minGlobal = (float)value;
            }
        }
    
        float MinGlobal {
            get { return minGlobal;  }
            set { minGlobal = value; }
        }
    
        // ...
    }
    


  • Was ich zu erwähnen vergaß (und bis jetzt hatte ich keine Zeit es nachzuholen):
    Ist es wirklich sinnvoll diesen Minimal- und Maximalwert der Achse zu generalisieren? Kaum willst du ihn benutzen, musst ohnehin immer wieder den refernzierten Typ abfragen.
    Ich würde vorschlagen eine Zwischenschicht einzufügen, die mehr über den Typ mit dem gearbeitet wird verrät (kann natürlich sein, dass du das ohnehin erreichen wolltest)

    // Dieses Interface benutzt man jetzt nur noch wenn es egal ist
    // welcher Typ verwendet wird, typischerweise für Lesezugriffe
    // darum ist daraus auch ein read-only Property geworden.
    public interface IAxis 
    {
        object MinGlobal {
            get;
        }
    }
    
    // Dieses Interface kann man jetzt bereits durchaus zum "normalen" Arbeiten hernehmen für 
    // typische Rechnungen. Man kann so auch  wunderbar in der Signatur angeben, dass mit der 
    // Achse gerechnet werden können soll. Man könnte ansonsten zum Beispiel auch eine Achse 
    // übergeben, die die Monate auflistet, etc.. 
    public interface IFloatAxis : IAxis 
    {
        new float MinGlobal {
            get;
            set;
        }
    }
    
    public class NumericalAxis : IFloatAxis 
    {
        private float minGlobal;
    
        // wieder müssen wir IAxis explizit implementieren ..
        object IAxis.MinGlobal {
            get { return minGlobal;  }
        }
    
        public float MinGlobal {
            get { return minGlobal;  }
            set { minGlobal = value; }
        }
    }
    

    Btw. könnte die Tatsache, dass jede Achse einen Minimal- und Maximalwert hat auch Probleme verursachen. Zum
    Beispiel eben bei einer Achse mit Monatsbeschriftung, oder überhaupt generell bei Text. Kann ja durchaus öfter
    vorkommen, wenn man ein Diagramm macht, dass zum Beispiel die Umsätze eigener Filialen vergleicht, und und und ..

    bis irgendwann mal wieder
    blubb



  • blubb schrieb:

    public interface IAxis 
    {
        // ...
    
        object MinGlobal {
            get;
            set;
        }
    
        // ...
    }
    
    public class AxisNumerical : IAxis
    {
        private float minGlobal;
    
        // ...
    
        object IAxis.MinGlobal {
            get { return minGlobal; }
            set {
                if(!(value is float)) {
                    throw new ArgumentException(/* .. */);
                }
    
                minGlobal = (float)value;
            }
        }
    
        float MinGlobal {
            get { return minGlobal;  }
            set { minGlobal = value; }
        }
    
        // ...
    }
    

    Danke fuer deinen Interface-Ansatz. Hab diesbezueglich eine Frage, da ich bisher keine Erfahrung mit Interfaces habe (auch wenn mir das Konzept bekannt ist).
    Warum tauchen in AxisNumercial : IAxis denn zwei properties auf:

    1. object IAxis.MinGlobal (diese muss implementiert werden)
    2. float MinGlobal (warum das?)

    Cheers,
    Dr. Goebel



  • horc schrieb:

    Warum tauchen in AxisNumercial : IAxis denn zwei properties auf:

    1. object IAxis.MinGlobal (diese muss implementiert werden)
    2. float MinGlobal (warum das?)

    Explicit Interface Implementation Tutorial



  • @blubb:

    Und weitere Fragen zu deinem Ansatz:
    Was meinst du mit "Kaum willst du ihn benutzen, musst ohnehin immer wieder den refernzierten Typ abfragen"?

    Ich hab ueberlegt, wie meine Loesung mit Interfaces harmoniere wuerde. Mir kam folgender Gedanke: Hinter der Achse steht ja eigentlich der Gedanke, Werte aus einem Problembereich in ein Koordinatensystem abzubilden. Diese Werte haengen
    vom Problem ab, also werden bspw. Zeitangaben, Datumsangaben, Numerische Werte auf ein Koordinatensystem abgebildet.
    Aber nicht nur die darzustellenden Werte benoetigen Information darueber, wie sie aufs Koordinatensystem abgebildet werden, auch andere Elemente eines Diagramms benoetigen diese Information auch. So z.B. die Achsenunterteilung, Gridlines, ...
    Alle diese Elemente muessten dann die benoetigen Interfaces implementieren. Das schreckt mich etwas ab.
    Daher ein anderer Ansatz. Ein sog. AxisMapper uebernimmt die Transformation von Problembereich in Koordinatensystem.
    Die Klasse Axis erhaelt z.B. einen solchen AxisMapper, aber auch die Klasse Gridlines, saemtliche Plotklassen (LinePlot, BarPlot...) Der AxisMapper (abstract class AxisMapper) muss jedoch in den Problembereich passen, daher werden verschiedene Axismapper implementiert: NumericalAxisMapper : AxisMapper, TimeAxisMapper : AxisMapper...
    Alle diese AxisMapper bieten eine Methode: ProblemToUnits(object val), die die Transformation von Problembereich in Koordinatensystem uebernimmt.
    Mein anfaengliches Problem (typunabhaengiges Arbeiten) wird dann durch Casts aufgeloest, z.B.:

    abstract class AxisMapper
    {
      ...
    
      public object MinGlobal
      {
        get
        {
          return this.minGlobal; 
        }
        set
        {
          this.minGlobal = value; 
        }
      }
      protected object minGlobal;
    
      abstract float ProblemToUnits(object val); 
      ...
    }
    
    class TimeAxisMapper : AxisMapper
    {
      // hier tauchen nun die casts auf. Von Typ object in die jeweilige Problemdomaene.
    
      float ProblemToUnits(object val)
      {
        temp = (TimeSpan) val; 
        /* Sollte ich nun beispielsweise auf object MinGlobal, object MaxGlobal..
           zugreifen muessen, so werden diese Objekte entsprechend gecastet */
    
        minTemp = (TimeSpan) this.MinGlobal; 
        maxTemp = (TimeSpan) this.MaxGlobal; 
        ...
        return result; 
      }
    }
    

    Der eingefleischte OO/C# - Programmierer moege mich in meinem Ansatz bestaetigen oder kritisieren.
    Vorteil scheint mir der, dass diese Klassen einmalig implementiert werden und dann als Aggregation in anderen
    Klassen auftauchen.
    Besten Dank @blubb: Dein Feedback regt zum Nachdenken an!!

    Cheers,
    Dr. Goebel


Anmelden zum Antworten