Daten bearbeiten, welche sich in abgeleiteter Klasse befinden


  • Administrator

    Der Titel ist nicht wirklich gut, aber was kürzeres und besseres kam mir nicht in den Sinn. Ich habe grundsätzlich ein simples Problem, welches immer wieder auftaucht und mir bisher keine meiner Lösungansätze gefallen hat. Daher möchte ich mal in die Runde fragen, wie ihr das machen würdet.

    Gehen wir von einem objektorientierten Beispiel aus: Einem Filtersystem.

    public interface IFilter
    {
      bool Check(String arg);
    }
    
    public class StartsWithFilter
      : IFilter
    {
      public String StartsWith { get; set; }
    
      public StartsWithFilter(string startsWith)
      {
        this.StartWith = startsWith;
      }
    
      public bool Check(String arg)
      {
        return arg.StartsWith(startsWith);
      }
    }
    
    public class LengthFilter
      : IFilter
    {
      public int Length { get; set; }
    
      public LengthFilter(int length)
      {
        this.Length = length;
      }
    
      public bool Check(String arg)
      {
        return arg.Length == this.Length;
      }
    }
    

    Jetzt kann der Benutzer des Programmes per GUI die Filter auswählen, welche er auf einen Bereich anwenden will. Diese Filter werden allerdings gespeichert, da sie immer wieder zum Einsatz kommen. Es gibt somit irgendwo eine List<IFilter> oder sowas ähnliches. Nun möchte der Benutzer aber einen oder mehrere Filter verändern oder austauschen, ohne die anderen auch neu setzen zu müssen. Doch wie soll dies nun geschehen, da wir schliesslich nur Basisklassen haben, bzw. Interfaces.

    Bis jetzt habe ich verschiedene Lösungen angewendet. Allerdings gefallen sie mir eben alle nicht so richtig.

    Visitor-Pattern:
    - Man muss Überladungen für alle Filter anbieten. Falls dies eine Bibliothek ist und jemand einen zusätzlichen Filter anbieten will, ist das recht blöd.
    - Auch wird die Visitor-Klasse mit der Zeit ziemlich überladen, wenn es mehr und mehr Filter werden.

    Doppelte-Speicherung:
    - Irgendwo werden die abgeleiteten Klassenobjekte gespeichert, welche als Filter eingesetzt wurden.
    - Dies heisst aber, dass man alles doppelt speichert.
    - Auch kann man dann meistens schlecht einen Filter auswechseln, sondern muss die Liste der Filter immer neu erstellen, aus den Listen mit den abgeleiteten Klassenobjekten.
    - Und schliesslich muss man dann auch Listen für alle eingesetzen Filterarten führen.

    Testen und Up-Cast:
    - Auf die verschiedenen eingesetzen Filterklassen testen und so den richtigen Up-Cast durchführen. Sieht schrecklich aus.

    Typinformationen mitgeben und Up-Cast machen:
    - Immerhin kein Typ-Testen mehr, sondern eine Switch-Anweisung oder sowas ähnliches.
    - Der Up-Cast stört immer noch.

    Blueprints:
    - Jeder Filter hat ein Bauplan.
    - Dies ist oft nur sehr schwer zu bewerkstelligen, da schliesslich die Parameter unterschiedlich sind, usw. usf. In gewissen speziellen Situationen kann es aber funktionieren.

    Vom objektorientierten Ansatz abrücken und die Super-God-Filter Klasse erstellen:
    - Wird schrecklick unübersichtlich.

    Wie würdet ihr dies bewerkstelligen, wenn das Ziel wäre, so flexibel wie möglich zu bleiben?

    Grüssli



  • Wie wäre es mit einer Id im Interface?



  • Ich verstehe nicht was der Visitor mit der Aufgebenstellung zu tun hat dass der User Filterelemente nachträglich bearbeiten will?
    Kann man nicht den Filtern einen Namen geben, der auch von den Objekten selbst generiert werden kann "Suchtext beginnt mit 'Arg'" und dann die gewählten Filter in einem Dictionary speichern mit Namen als Key? Dann kann der User über diesen namen einzelne Elemente ansprechen.


  • Administrator

    CSL schrieb:

    Wie wäre es mit einer Id im Interface?

    Das verstehe ich unter Typinformationen mit Up-Cast. Jeder Filter hat seine Id oder Typinformation und über die kann man den korrekten Up-Cast machen. Oder meinst du was anderes?

    witte schrieb:

    Ich verstehe nicht was der Visitor mit der Aufgebenstellung zu tun hat dass der User Filterelemente nachträglich bearbeiten will?

    Mit einem Visitor kommst du wieder an die abgeleiteten Klassen heran und kannst somit diese bearbeiten. Auf das obige Beispiel angewandt:

    public interface IFilter
    {
      bool Check(String arg);
      void ApplyVisitor(IFilterVisitor visitor);
    }
    
    public interface IFilterVisitor
    {
      void Set(StartsWithFilter filter);
      void Set(LengthFilter filter);
      // usw.
    }
    
    public class LengthFilter
      : IFilter
    {
      // ... siehe oben ...
    
      public void ApplyVisitor(IFilterVisitor visitor)
      {
        visitor.Set(this);
      }
    }
    
    // usw.
    

    Das GUI kann nun auf den zu bearbeitenden Filter so ein Visitor anwenden. In den Set-Methoden werden dann die richtigen GUI-Komponenten für die Bearbeitung des Filters erstellt/geholt/aufgerufen und zusätzlich hat man auch das richtige Objekt, von welchem man die Daten holen, bzw. setzen, kann.

    witte schrieb:

    Kann man nicht den Filtern einen Namen geben, der auch von den Objekten selbst generiert werden kann "Suchtext beginnt mit 'Arg'" und dann die gewählten Filter in einem Dictionary speichern mit Namen als Key? Dann kann der User über diesen namen einzelne Elemente ansprechen.

    Verstehe deinen Vorschlag nicht ganz. Es geht darum, dass in einer List<IFilter> der Filter an der Stelle x geholt werden soll, überprüft werden soll, was für ein Filter das ist, zum Beispiel ein LengthFilter und dann dessen Attribute verändert werden können, also zum Beispiel Length verändern.

    Grüssli



  • Dravere schrieb:

    Verstehe deinen Vorschlag nicht ganz. Es geht darum, dass in einer List<IFilter> der Filter an der Stelle x geholt werden soll, überprüft werden soll, was für ein Filter das ist, zum Beispiel ein LengthFilter und dann dessen Attribute verändert werden können, also zum Beispiel Length verändern.

    Du gehst also davon aus dass die Filter disjunkt sind, dass es für jeden Typ maximal eine Filterung geben kann. Das war mir nicht klar.

    Text contains 'jupiter' or Text contains 'saturn'
    


  • Wenn die Filteranzahl nicht zu groß ist und sowieso für jeden Filtertyp maximal einen geben kann, könnte man diese Filter doch auch in ein Objekt zusammenfassen welches die verschiedenen Filter als Member enthält.

    class Filterset
    {
      public StartsWithFilter StartsWithFilter  { get; set; }
      public LengthFilter LengthFilter { get; set; }
      ...
    }
    


  • witte schrieb:

    Wenn die Filteranzahl nicht zu groß ist und sowieso für jeden Filtertyp maximal einen geben kann, könnte man diese Filter doch auch in ein Objekt zusammenfassen welches die verschiedenen Filter als Member enthält.

    class Filterset
    {
      public StartsWithFilter StartsWithFilter  { get; set; }
      public LengthFilter LengthFilter { get; set; }
      ...
    }
    

    ist das nicht vom prinzip her das gleiche wie der visitor?


  • Administrator

    witte schrieb:

    Wenn die Filteranzahl nicht zu groß ist und sowieso für jeden Filtertyp maximal einen geben kann, könnte man diese Filter doch auch in ein Objekt zusammenfassen welches die verschiedenen Filter als Member enthält.

    Das ist die Super-God-Filter Klasse 🙂
    Oder zumindest sowas ähnliches.

    witte schrieb:

    Du gehst also davon aus dass die Filter disjunkt sind, dass es für jeden Typ maximal eine Filterung geben kann. Das war mir nicht klar.

    Was du darunter nun verstehst, ist mir wieder nicht klar *sich langsam blöd vorkommt*
    Es kann schon mehrere LengthFilter und mehrere StartsWithFilter haben. Es können aber auch noch andere Filter vorkommen. Die können gleich aufgebaut und verschieden sein. Die können die gleichen Attribute haben und verschiedene. Die einzige Gemeinsamkeit die besteht, ist das Interface IFilter .

    Grüssli



  • Hallo Dravere,

    so ganz verstehe ich dein Problem nicht. Wenn du eine List<IFilter> hast, so sind ja trotzdem dadrin die einzelnen konkreten Objekte der verschiedenen Filterklassen gespeichert, d.h. mittels einer 'is' bzw. 'as' Abfrage kommst du dann an die einzelnen Filter wieder dran (das meinst du mit "Testen und Up-Cast"?). Sicherlich benötigst du dann eine Methode dafür, welche alle Filterklassen der Reihe nach durchgeht.

    Du mußt ja dann dem Benutzer sowieso individuelle GUI-Masken zur Änderung anzeigen. Als Ausweg bliebe sonst nur eine allgemeine Methode über Reflection (d.h. je nach Eigenschaft der Filterklasse wird ein bestimmtes Control angezeigt, z.B. string -> TextBox, int -> NumericUpDown, bool -> CheckBox etc.)
    Oder als Alternative das PropertyGrid?

    ~(Irgendeinen Tod mußt du sterben...)~


  • Administrator

    Th69 schrieb:

    so ganz verstehe ich dein Problem nicht. Wenn du eine List<IFilter> hast, so sind ja trotzdem dadrin die einzelnen konkreten Objekte der verschiedenen Filterklassen gespeichert, d.h. mittels einer 'is' bzw. 'as' Abfrage kommst du dann an die einzelnen Filter wieder dran (das meinst du mit "Testen und Up-Cast"?). Sicherlich benötigst du dann eine Methode dafür, welche alle Filterklassen der Reihe nach durchgeht.

    Verstanden hast du das Problem doch absolut korrekt. Mein Problem ist, dass mir dieser Up-Cast nicht gefällt und ich gerne eine Methode finden würde, wo dies nicht nötig ist.
    Statt mit is oder as zu testen, verwende ich allerdings lieber die Id Möglichkeit, bzw. meistens verwende ich eine Guid , damit ich den Filtertyp über die Programmgrenze hinaus eindeutig identifizieren kann.

    Th69 schrieb:

    ~(Irgendeinen Tod mußt du sterben...)~

    Ich will aber schön sterben :p
    Es ist ja nicht so, dass ich keine Lösung für das Problem habe. Ich suche aktuell nur nach akademisch schöneren Lösungen, als ich bisher gefunden habe 😉

    Grüssli



  • Dravere schrieb:

    Th69 schrieb:

    so ganz verstehe ich dein Problem nicht. Wenn du eine List<IFilter> hast, so sind ja trotzdem dadrin die einzelnen konkreten Objekte der verschiedenen Filterklassen gespeichert, d.h. mittels einer 'is' bzw. 'as' Abfrage kommst du dann an die einzelnen Filter wieder dran (das meinst du mit "Testen und Up-Cast"?). Sicherlich benötigst du dann eine Methode dafür, welche alle Filterklassen der Reihe nach durchgeht.

    Verstanden hast du das Problem doch absolut korrekt. Mein Problem ist, dass mir dieser Up-Cast nicht gefällt und ich gerne eine Methode finden würde, wo dies nicht nötig ist.
    Statt mit is oder as zu testen, verwende ich allerdings lieber die Id Möglichkeit, bzw. meistens verwende ich eine Guid , damit ich den Filtertyp über die Programmgrenze hinaus eindeutig identifizieren kann.

    ??

    public interface IFilter
    {
      bool Check(String arg);
      Control GetConfigControl(); // <-- ??
    }
    

    hand, mogel


  • Administrator

    @mogel,
    Schon mal etwas von Trennung von Daten, GUI und Logik gehört? Eine GUI-Komponente hat definitiv nichts in IFilter zu suchen.
    Was für ein Control soll da zurückgegeben werden? WinForms? WPF? Oder was eigenes? Vielleicht ein Konsolen-Control?

    Grüssli



  • Dravere schrieb:

    Schon mal etwas von Trennung von Daten, GUI und Logik gehört? Eine GUI-Komponente hat definitiv nichts in IFilter zu suchen.
    Was für ein Control soll da zurückgegeben werden? WinForms? WPF? Oder was eigenes? Vielleicht ein Konsolen-Control?

    wenn Du mir jetzt verräts wo ich das MVC zerstört habe ... ein Interface gehört bei mir zum Controler, welcher zwischen Model (Filterimplementierung) und View (Control) vermittelt


  • Administrator

    mogel schrieb:

    Dravere schrieb:

    Schon mal etwas von Trennung von Daten, GUI und Logik gehört? Eine GUI-Komponente hat definitiv nichts in IFilter zu suchen.
    Was für ein Control soll da zurückgegeben werden? WinForms? WPF? Oder was eigenes? Vielleicht ein Konsolen-Control?

    wenn Du mir jetzt verräts wo ich das MVC zerstört habe ... ein Interface gehört bei mir zum Controler, welcher zwischen Model (Filterimplementierung) und View (Control) vermittelt

    😕
    Was ist das denn für eine Ansicht von MVC? Damit bringt dir MVC ja rein gar nichts, weil du immer noch alles fest verkoppelt hast. Du kannst nicht einfach deine View austauschen und zum Beispiel von WinForms auf WPF wechseln, weil du die Filter (also Daten) fix mit der View verkoppelt hast. Und das sogar noch über die stärkste Verbindung überhaupt: Vererbung.

    Grüssli



  • lassen wir mal WPF weg - nutze ich nicht

    IFilter filter = something as IFilter;
    Control control = filter->GetConfigControl();
    

    ich sehe da nur eine Bindung - und zwar zwischen einem konkreten Filter und seiner konkrekten GUI für die Einstellungen


  • Administrator

    mogel schrieb:

    ich sehe da nur eine Bindung - und zwar zwischen einem konkreten Filter und seiner konkrekten GUI für die Einstellungen

    Genau, es ist eine Bindung, nämlich zwischen WinForms und IFilter . Was ist aber nun, wenn jemand IFilter nicht in WinForms anzeigen möchte, sondern in WPF? Oder vielleicht auf der Konsole eine Ausgabe machen möchte. Dann bist du völlig auf WinForms fixiert. Du hast die View eben nicht von den Daten getrennt, sondern völlig zusammengeschweisst. Das ist nicht der Sinn von MVC.

    Mit korrekten MVC kannst du IFilter dann ohne Probleme auch in WPF verwenden oder nur in einem Konsolenprogramm. Dann hast du eine View für WinForms, du hast eine View für WPF, du hast eine View für die Konsole, usw. usf.

    Grüssli



  • Was ist aber nun, wenn jemand IFilter nicht in WinForms anzeigen möchte, sondern in WPF?

    wie schon geschrieben - ich nutze WPF nicht und wolltes erstmal weg lassen ... es ging um grundsätzliche MVC Dinge

    public interface IFilter
    {
      bool Check(String arg);
      IFilterConfig GetConfigControl();
    }
    
    public interface IFilterConfig
    {
      bool OpenFilterConfig();
    }
    
    public class FormsFilterConfig : IFilterConfig
    {
    }
    
    public class WPFFilterConfig : IFilterConfig
    {
    }
    
    public class ConsoleFilterConfig : IFilterConfig
    {
    }
    

    Du musst irgendwo eine Bindung zwischen IFilter und der Konfiguration herstellen ... kannst Du auch in einer statischen Methode machen (was etwas mehr Pflege erfordert) - ist aber prinzipell egal ... es existiert dann aber immer eine feste Bindung zwischen einem konkreten Filter und einer konkreten Oberfläche

    selbst wenn wir nur bei Forms bleiben

    public interface IFilter
    {
      bool Check(String arg);
      Control GetConfigControl();
    }
    

    hier wird noch nicht mal ein Control instanziert welches sinnlos Speicher verschwendet ... nutzen kannst Du den Filter dennoch beliebig ... selbst die Darstellung der Config ist unabhängig vom Filter ... alternativ kannst Du das Ganze auch trennen

    public interface IFilterConfig
    {
      Control GetConfigControl();
    }
    
    public class MyFiler : IFilter, IFilterConfig
    {
    }
    

    egal wie Du es drehst und wendest ... über das C verbindest Du M & V


  • Administrator

    mogel schrieb:

    wie schon geschrieben - ich nutze WPF nicht und wolltes erstmal weg lassen ... es ging um grundsätzliche MVC Dinge

    Wenn du mal meinen Beitrag genau liest, rede ich auch über allgemeine MVC Dinge. Ob WPF, WinForms, Konsole oder sonstwas ist völlig egal, das sind nur Beispiele.

    mogel schrieb:

    Du musst irgendwo eine Bindung zwischen IFilter und der Konfiguration herstellen ... kannst Du auch in einer statischen Methode machen (was etwas mehr Pflege erfordert) - ist aber prinzipell egal ... es existiert dann aber immer eine feste Bindung zwischen einem konkreten Filter und einer konkreten Oberfläche

    Ja, aber die sollte die Filterklasse nicht beeinflussen. Du bindest alles mit Superkleber zusammen, statt es nur über Schnittstellen laufen zu lassen. IFilter sollte das Model sein und nichts über das View oder den Controller wissen.

    Also mal etwas mit Pseudo-Code verdeutlicht:

    public interface IFilterView
    {
      void Display(IFilter filter);
    }
    
    public class FilterWPFView : IFilterView { /* ... */ }
    
    public class FilterWinFormsView : IFilterView { /* ... */ }
    
    public class FilterConsoleView : IFilterView { /* ... */ }
    
    // ...
    
    IFilterView view = new FilterWPFView();
    view.Display(filter);
    

    IFilter hat somit keine Abhängigkeit zur View mehr. Ich kann IFilter in eine eigene DLL auslagern, welche keine Abhängigkeiten zur WPF oder WinForms DLL hat. Hier hast du eine deutlich stärkere Trennung von Daten und Ansicht.

    Vererbung ist immer die stärkste Verbindung, welche es gibt. Im allgemeinen sollte sie als letztes in Betracht gezogen werden.

    mogel schrieb:

    egal wie Du es drehst und wendest ... über das C verbindest Du M & V

    Wie ich schon mehrfach gesagt habe, geht es darum, wie man es verbindet. MVC war ja dazugedacht, dass man die Dinger eben stark entkoppelt. Da ist es nicht sehr sinnvoll, dies mit der stärksten Bindung, welche es gibt, wieder zusammenzukleben.

    Grüssli



  • IFilter sollte das Model sein und nichts über das View oder den Controller wissen.

    ich betrachte Interfaces als Bestandteil von C ... wärend Klassen zu M gehören ... wir haben hier also schon zwei Ansichten

    IFilterView view = new FilterWPFView();
    view.Display(filter);
    

    wenn ich jetzt einen eigenen Filter implementiere, zeigt mir Dein FilterWPFView Details meiner Klasse an?



  • mogel schrieb:

    ich betrachte Interfaces als Bestandteil von C ... wärend Klassen zu M gehören ... wir haben hier also schon zwei Ansichten

    Das hat wenig mit Ansichen zu tun, das was du betreibst widerspricht den Grundprinzipien von MVC, und sollte daher korrekterweise auch nicht so genannt werden.


  • Administrator

    mogel schrieb:

    ich betrachte Interfaces als Bestandteil von C ... wärend Klassen zu M gehören ... wir haben hier also schon zwei Ansichten

    Siehe Antwort von asc. Ich würde dir empfehlen, dich mit dieser Thematik nochmals auseinander zu setzen.

    mogel schrieb:

    IFilterView view = new FilterWPFView();
    view.Display(filter);
    

    wenn ich jetzt einen eigenen Filter implementiere, zeigt mir Dein FilterWPFView Details meiner Klasse an?

    Sofern sich dein Filter an die definierten Konventionen hält, sicher. Aber wer würde dir verbieten, ein eigenes View für deinen Filter zu schreiben, welches von IFilterView ableitet? Danach kannst du einfach diese View für deinen Filter verwenden.

    Ich persönlich erkläre es noch gerne an diesem Beispiel:

    public class ShapeData
    {
      public int X { get; set; }
      public int Y { get; set; }
      public int Width { get; set; }
      public int Height { get; set; }
    }
    
    public interface IShapeView
    {
      void Display(ShapeData data);
    }
    
    public class Shape
    {
      public ShapeData Data { get; set; }
      public IShapeView View { get; set; }
    
      public void Display()
      {
        if(this.Data != null && this.View != null)
        {
          this.View.Display(this.Data);
        }
      }
    }
    

    Perfekt, sollte alles klar sein oder? Nun wollen wir eine Ellipse darstellen?

    public class EllipseView
      : IShapeView
    {
      public void Display(ShapeData data)
      {
        // Das Ding zeichnen oder auf der Konsole ausgeben oder was auch immer.
      }
    }
    
    // ...
    
    Shape shape = new Shape();
    shape.Data = new ShapeData();
    // Werte zuweisen ...
    shape.View = new EllipseView();
    
    shape.Display();
    

    Gut ... jetzt hätte ich aber gerne dieses Rechteck auch als Begrenzung um die Ellipse angezeigt. Kein Problem!

    public class RectView
      : IShapeView
    {
      public void Display(ShapeData data)
      {
        // Einfach DrawRect(data.X, data.Y, data.Width, data.Height) oder sowas ...
      }
    }
    
    // ...
    // Wir können die Variable shape von vorhin gleich wiederverwenden.
    // Die Daten werden belassen, wir verändern nur die View.
    shape.View = new RectView();
    shape.Display();
    

    Was will man mehr? 🙂
    Das kann man nun um beliebige Views erweitern, welche in das einfache Schema mit dem Rechteck passt. Man kann sogar theoretisch Kreis-Views machen, obwohl dies gar nicht nötig ist, da man dies von den Daten her erzwingen könnte mit einer EllipseView. Da wäre zum Beispiel eine BigCircleView und eine SmallCircleView möglich, welche jeweils entweder den grössten oder kleinsten Wert für den Radius von der Länge und Breite heranziehen. Gleiches kann man dann auch für das Quadrat machen, obwohl man es bereits mit RectView und korrekten Daten erreichen kann. Für die korrekten ShapeDaten könnte man sogar entsprechende statische Funktionen anbieten:

    public static ShapeData CreateCircle(int centerX, int centerY, int radius)
    {
      // ...
    }
    
    public static ShapeData CreateSquare(int x, int y, int length)
    {
      // ...
    }
    

    Das ist meiner Meinung nach sehr schönes MVC. So sollte die Objektorientierung erklärt werden und nicht irgendwie probieren einen Kreis von einer Ellipse abzuleiten oder umgekehrt und solchen Unsinn 🙂

    Problematisch wird es eben erst, wenn die Daten auch ein polymorphes Objekt darstellen und man nicht an alle Daten herankommt, an welche man herankommen möchte. Dann wird es grundsätzlich knifflig, aber nicht unmöglich. Man kann zum Beispiel für jeden Filter-Typ eine View im Controller registrieren. Der Controller sucht dann für den Datentyp gleich das richtige View aus. Im View muss dann eben ein Up-Cast durchgeführt werden.

    Ich habe jetzt nicht die vollständige .Net Bibliothek im Kopf und bin zu faul nachzuschauen, aber so ganz grob, würde dies wohl so aussehen:

    public class FilterDisplayController
    {
      private SortedDictionary<Guid, IFilterView> m_views;
    
      public void RegisterView<T>(IFilterView view)
        where T : IFilter
      {
        Guid guid = typeof(T).GUID;
        m_views[guid] = view;
      }
    
      public void Display(IFilter filter)
      {
        Guid guid = filter.GetType().GUID;
    
        IFilterView view;
        if(m_view.TryGetValue(guid, out view))
        {
          view.Display(filter);
        }
      }
    }
    

    Vielleicht könnte man die View-Zuordnung auch statisch setzen ...

    Naja, aber sollte auch nur ein Erläuterungsbeispiel sein, muss ja jetzt nicht gleich perfekt sein. Zudem ist es nun 2 Uhr und ich wollte um 23:30 Uhr ins Nest, damit ich morgen/heute mich noch weiter in Lua einarbeiten kann.

    Herr Dravere, lernen sie endlich rechtzeitig in ihr Nest zu verschwinden! 🙄 😃

    Grüssli


Anmelden zum Antworten