"Getter" und "Setter" - Ja oder nein?



  • Cool ist auch, wenn man versucht "Get" und "Set" zu vermeiden, stattdessen zwei Überladungen (non-const und const mit retval) macht und dann bei std::bind die Quittung bekommt -.-

    Dot: Ist Dein GUI-Code irgendwo einsehbar? Ich stelle mir das arg blöde vor, damit bewegbare Fenster und so Zeugs zu implementieren. Wenn das alles natürlich immer nur vom Parent oder wem anders gelayoutet wird, ists natürlich okay...



  • Decimad schrieb:

    Dot: Ist Dein GUI-Code irgendwo einsehbar? Ich stelle mir das arg blöde vor, damit bewegbare Fenster und so Zeugs zu implementieren. Wenn das alles natürlich immer nur vom Parent oder wem anders gelayoutet wird, ists natürlich okay...

    Klingt nach GTK.



  • hustbaer schrieb:

    Das ist ein Statement, das nur mit eine sehr engen Definition von "Getter" bzw. "Setter" Sinn macht.

    Oder nicht einmal da.

    Nehmt als Beispiel die SFML-Klasse sf::Sprite. Da gibts viele set/get-Methoden, um auf einzelne Attribute wie Position, Rotation, Skalierung, Textur, Textur-Rechteck, Farbe, etc. zuzugreifen. Was genau ist daran schlecht, und wie würdet ihr sowas besser designen?



  • @Nexus: Also ich ziehe aus diesen Diskussionen immer die Aussage, dass eben alles gekapselt sein sollte. Seither habe ich den Monitor immer mit Pappe zugedeckt, damit ich nicht Software-Interna sehen muss. Die Tastatur ist natürlich abgestöpselt. Nur dieses Forum liegt mir am Herzen, da mache ich ab und zu eine Ausnahme.


  • Mod

    Irgendwie habe ich das Gefühl, dass hier in diesem Thread absichtlich vieles falsch verstanden oder ausgelegt wird - von beiden allen Seiten.

    Absolute statements are always wrong. Nicht alles wo get oder set dransteht ist ein trivialer Getter/Setter (d.h. eine Memberfunktion in der einfach return membervariable; steht und ihr Setter-Äquivalent). Nicht alle guten Modelle schließen kategorisch aus, dass es auch mal diese trivialen Getter/Setter für die Member einer Klasse gibt.

    Hier kommt nun mein eigener Beitrag zum Thema: Wie volkard bereits so schön verlinkt hat, tendieren viele Anfänger, oftmals aufgrund schlechter Vorbilder, dazu, zu einem Datenmember einer Klasse automatisch einen trivialen Getter und Setter zu schreiben. Manche benutzen dafür sogar Funktionen ihrer IDE, ich erinnere mich sogar an einen Thread, in dem stolz ein Makro a la

    #define MEMBER_VAR(type, name)    \
    private:                          \
     type name;                       \
    public:                           \
     type get_##name () const         \
      { return name ; }               \
     void set_##name (type arg)       \
      { name = arg; }
    

    vorgestellt wurde. Es ist wohl jedem einsichtig, dass dieses zwanghafte Anlegen von Gettern und Settern keine gute Praxis ist, oder? Da brauche ich auch keine weitergehende Begründung und Alternativen zu nennen, denke ich.



  • Der Sinn von Gettern/Settern ist stark kontextabhängig. Man sollte sie so viel wie nötig und so wenig wie möglich einsetzen. Ich finde, das Wichtigste ist mit folgendem Beitrag gesagt:

    Nexus schrieb:

    Man sollte sich eher überlegen, welche Eigenschaften einer Klasse nach aussen relevant sind (Schnittstelle) und wie man auf diese zugreifen möchte. Teilweise will man nämlich gar nicht den Wert direkt setzen, sondern ihn auf eine bestimmte Art verändern. Oder man will nur lesen. Oder gar nichts...



  • Xebov schrieb:

    volkard schrieb:

    Du könntest leicher jagen, welches taube Nüßchen Dir eine 0 in den Nenner geschrieben hat.

    Ich gehe bei solchen Klassen grundsätzlich davon aus das der Nutzer die Regeln kennt. Man kann bei dieser Sache natürlich auch public Variablen und einen Setter nehmen so das der Nutzer der Klasse den Setter nehmen kann wenn er sich unsicher ist ob an dieser Stelle evtl eine 0 kommen kann, und sich damit die Prüfung für die Fälle sparen in denen es nicht passiert.

    Och, ich finde da macht ein Setter immer Sinn und ich würde den Nenner nicht public machen:

    class fraction
    {
    private:
        int numerator;
    
    public:
        int getNumerator() const
        {
            return numerator;
        }
    
        void setNumerator(int newValue)
        {
            assert(newValue != 0);
            numerator = newValue;
        }
    };
    

    SeppJ schrieb:

    ich erinnere mich sogar an einen Thread, in dem stolz ein Makro a la

    #define MEMBER_VAR(type, name)    \
    private:                          \
     type name;                       \
    public:                           \
     type get_##name () const         \
      { return name ; }               \
     void set_##name (type arg)       \
      { name = arg; }
    

    vorgestellt wurde. Es ist wohl jedem einsichtig, dass dieses zwanghafte Anlegen von Gettern und Settern keine gute Praxis ist, oder? Da brauche ich auch keine weitergehende Begründung und Alternativen zu nennen, denke ich.

    Hey, das war ja ich. 😃



  • warum wird hier schon wieder zensiert? warum darf man nicht ironisch zeigen wie dumm die anti getter setter regel ist?



  • a35sw4ed5rf6t7gz8h schrieb:

    dot schrieb:

    a35sw4ed5rf6t7gz8h schrieb:

    dot schrieb:

    Getter und Setter brechen die Kapselung. Wann immer du meinst, einen Getter oder Setter zu brauchen, solltest du darüber nachdenken, was die Variable eigentlich in der Klasse verloren hat.

    Das ist doch nur ein krampfhafter versuch etwas "besser" zu machen, obwohl es nicht sein muss. Wenn ich z.B. ein CAD Programm habe und die Objekte (Kurven/Flächen) haben Namen die in verschiedenen GUI-Elementen angezeigt/umbenannt werden können, warum sollen die dann kein get/setName haben? Was würdest du machen? Warum ist das dann besser?

    Was genau ist der Grund, wieso der Name deiner Meinung nach Teil einer Kurve oder Fläche sein sollte? Imo ist der Name etwas, über das eine bestimmte Instanz einer Kurve oder Fläche von der Anwendung identifiziert werden kann. Ein Name ist aber imo nicht Teil des Konzepts einer Kurve oder Fläche. Folgedessen sollte der Name auch nicht Attribut der Klasse Kurve bzw. Fläche sein, sondern die Anwendung eine Map haben, die Namen auf Instanzen abbildet...

    Der Name ist nicht Teil des Konzepts einer Kurve oder Fläche, sondern eines Objekts. Objekte können auch nur "Gruppen" sein die andere Objekte enthalten. Die Namen der Objekte werden auch in Files gespeichert. Was soll das an deiner Map besser sein? Kennt dann GUI, FileIO usw die Applikation um den Namen zu holen?

    Wofür genau verwendest du Klassen, wenn nicht um Konzepte auszudrücken? Wenn wir uns einig sind, dass ein Name nicht Teil des Konzeptes eine Kurve oder Fläche ist, nenn mir einen Grund, wieso genau der Name dann Teil der Klasse sein sollte. Die Tatsache, dass Objekte im User Interface deiner Anwendung über Namen identifiziert werden, ist für das User Interface deiner Anwendung wesentlich, aber nicht für das mathematische Konzept einer Kurve oder Fläche. Und genau so sollte man das dann auch modellieren. Angenommen du willst nun plötzlich an anderer Stelle in deiner Anwendung mit Kurven und Flächen arbeiten, die aber nicht direkt im User Interface auftauchen. Hinter diesen Kurven und Flächen steckt immer noch um das gleiche Konzept, ein Name macht dort aber keinen Sinn...

    Decimad schrieb:

    Dot: Ist Dein GUI-Code irgendwo einsehbar? Ich stelle mir das arg blöde vor, damit bewegbare Fenster und so Zeugs zu implementieren. Wenn das alles natürlich immer nur vom Parent oder wem anders gelayoutet wird, ists natürlich okay...

    Nope, das ist meine eigene kleine UI Library für Direct3D. Mir fällt grad mir kein sinnvoller Anwendungsfall von "bewegten Fenstern" ein, aber umzusetzen wäre sowas relativ einfach, man braucht lediglich einen entsprechenden Container. Ich finde einer der Kernpunkte von gutem UI ist die Skalierbarkeit des Layouts. Das Design der Library ist ein ganz klein wenig von WPF inspiriert und vor allem auf skalierbares Layout ausgerichtet...

    Xebov schrieb:

    dot schrieb:

    Folgedessen sollte der Name auch nicht Attribut der Klasse Kurve bzw. Fläche sein, sondern die Anwendung eine Map haben, die Namen auf Instanzen abbildet...

    Das finde ich etwas übertrieben. Das bedeutet ja im Grunde das man jedesmal die Map durchsuchen muß um an den Namen zu kommen statt einfach getName() auf die Instanz aufzurufen.

    Wann genau müsste man denn z.B. von der Instanz an den Namen kommen?

    Nexus schrieb:

    hustbaer schrieb:

    Das ist ein Statement, das nur mit eine sehr engen Definition von "Getter" bzw. "Setter" Sinn macht.

    Oder nicht einmal da.

    Nehmt als Beispiel die SFML-Klasse sf::Sprite. Da gibts viele set/get-Methoden, um auf einzelne Attribute wie Position, Rotation, Skalierung, Textur, Textur-Rechteck, Farbe, etc. zuzugreifen. Was genau ist daran schlecht, und wie würdet ihr sowas besser designen?

    Zunächst einmal sind wir uns denk ich einig, dass es keinen Sinn macht, einem sf::Sprite ein getName() bzw. setName() zu verpassen. 😉
    Unter einem Sprite versteht man rein prinzipiell mal ein kleines Bild, das sich irgendwo auf dem Bildschirm befindet und dessen Position verändert werden kann. Dementsprechend wird das Sprite also Methoden brauchen, die seine Position modifizieren, denn das ist für das Konzept eines Sprite wesentlich. Die ganzen Getter sind allerdings imo überflüssig. Ein Spiel verwendet Sprites um Bildchen auf den Bildschirm zu malen. Die Position dieser Bildchen kommt dabei von der Spiellogik und in einem Spiel, das sauber designed ist, geht der Datenfluss da nur in eine Richtung, denn alles andere würde bedeuten, dass die Spiellogik die Sprites als Datenablage missbraucht.

    Ethon schrieb:

    Xebov schrieb:

    volkard schrieb:

    Du könntest leicher jagen, welches taube Nüßchen Dir eine 0 in den Nenner geschrieben hat.

    Ich gehe bei solchen Klassen grundsätzlich davon aus das der Nutzer die Regeln kennt. Man kann bei dieser Sache natürlich auch public Variablen und einen Setter nehmen so das der Nutzer der Klasse den Setter nehmen kann wenn er sich unsicher ist ob an dieser Stelle evtl eine 0 kommen kann, und sich damit die Prüfung für die Fälle sparen in denen es nicht passiert.

    Och, ich finde da macht ein Setter immer Sinn und ich würde den Nenner nicht public machen:

    class fraction
    {
    private:
        int numerator;
    
    public:
        int getNumerator() const
        {
            return numerator;
        }
    
        void setNumerator(int newValue)
        {
            assert(newValue != 0);
            numerator = newValue;
        }
    };
    

    Ich finde, dass es in der Regel keinen Sinn macht, Typen mit Wertsemantik aus einem typischen OO Blickpunkt zu betrachten. In dem konkreten Fall würde ich einfach ein struct mit zwei Membern machen und die entsprechenden Constraints über das Typsystem ausdrücken:

    struct rational
    {
      int numerator;
      nonzero_int denominator;
    };
    


  • strictly_positive_int vernünftig umzusetzen ist so dermaßen aufwendig (die ganzen operatoren, aber schon alleine weil man dann mit traits bei Grenzüberschreitungen arbeiten muss, exceptions, asserts, sonderwerte), dass ich die Idee nicht so gut finde. Der Gewinn steht einfach nicht in Relation zu den Kosten.



  • dot schrieb:

    a35sw4ed5rf6t7gz8h schrieb:

    dot schrieb:

    a35sw4ed5rf6t7gz8h schrieb:

    dot schrieb:

    Getter und Setter brechen die Kapselung. Wann immer du meinst, einen Getter oder Setter zu brauchen, solltest du darüber nachdenken, was die Variable eigentlich in der Klasse verloren hat.

    Das ist doch nur ein krampfhafter versuch etwas "besser" zu machen, obwohl es nicht sein muss. Wenn ich z.B. ein CAD Programm habe und die Objekte (Kurven/Flächen) haben Namen die in verschiedenen GUI-Elementen angezeigt/umbenannt werden können, warum sollen die dann kein get/setName haben? Was würdest du machen? Warum ist das dann besser?

    Was genau ist der Grund, wieso der Name deiner Meinung nach Teil einer Kurve oder Fläche sein sollte? Imo ist der Name etwas, über das eine bestimmte Instanz einer Kurve oder Fläche von der Anwendung identifiziert werden kann. Ein Name ist aber imo nicht Teil des Konzepts einer Kurve oder Fläche. Folgedessen sollte der Name auch nicht Attribut der Klasse Kurve bzw. Fläche sein, sondern die Anwendung eine Map haben, die Namen auf Instanzen abbildet...

    Der Name ist nicht Teil des Konzepts einer Kurve oder Fläche, sondern eines Objekts. Objekte können auch nur "Gruppen" sein die andere Objekte enthalten. Die Namen der Objekte werden auch in Files gespeichert. Was soll das an deiner Map besser sein? Kennt dann GUI, FileIO usw die Applikation um den Namen zu holen?

    Wofür genau verwendest du Klassen, wenn nicht um Konzepte auszudrücken? Wenn wir uns einig sind, dass ein Name nicht Teil des Konzeptes eine Kurve oder Fläche ist, nenn mir einen Grund, wieso genau der Name dann Teil der Klasse sein sollte. Die Tatsache, dass Objekte im User Interface deiner Anwendung über Namen identifiziert werden, ist für das User Interface deiner Anwendung wesentlich, aber nicht für das mathematische Konzept einer Kurve oder Fläche. Und genau so sollte man das dann auch modellieren. Angenommen du willst nun plötzlich an anderer Stelle in deiner Anwendung mit Kurven und Flächen arbeiten, die aber nicht direkt im User Interface auftauchen. Hinter diesen Kurven und Flächen steckt immer noch um das gleiche Konzept, ein Name macht dort aber keinen Sinn...

    Kannst du lesen? Nochmal: Der Name gehört nicht zur Kurve. Ich hab Kurven/Flächen in Klammer geschrieben und du baust deine ganze Argumentation darauf auf.

    Objekte können auch nur "Gruppen" sein die andere Objekte enthalten

    Versuch mal es zu verstehen.
    Ich kann auch Nodes statt Objekt schreiben, vlt kapierst du dann um was es geht. Ist mir aber auch egal.



  • a35sw4ed5rf6t7gz8h schrieb:

    Kannst du lesen? Nochmal: Der Name gehört nicht zur Kurve. Ich hab Kurven/Flächen in Klammer geschrieben und du baust deine ganze Argumentation darauf auf.

    Objekte können auch nur "Gruppen" sein die andere Objekte enthalten

    Versuch mal es zu verstehen.
    Ich kann auch Nodes statt Objekt schreiben, vlt kapierst du dann um was es geht. Ist mir aber auch egal.

    Sry, aber für mich war alles andere als offensichtlich, worauf du mit "Objekte können auch nur "Gruppen" sein die andere Objekte enthalten" genau hinaus wolltest. In dem Fall kannst du von mir aus Nodes haben, die Kurven oder Flächen enthalten und einen Namen haben. Ich persönlich wäre aber auch da eher vorsichtig, da eine Namensänderung eines solchen Node meiner Erfahrung nach nicht nur Auswirkungen auf den Node hat und es in einem solchen Design dann oft sehr schnell sehr kompliziert wird, alle Datenstrukturen konsistent zu halten...



  • dot schrieb:

    Ich finde, dass es in der Regel keinen Sinn macht, Typen mit Wertsemantik aus einem typischen OO Blickpunkt zu betrachten. In dem konkreten Fall würde ich einfach ein struct mit zwei Membern machen und die entsprechenden Constraints über das Typsystem ausdrücken:

    struct rational
    {
      int numerator;
      nonzero_int denominator;
    };
    

    std::string.
    Und die Constraints gehören aus nonzero_int raus und in rational rein. Da wird oft auch drin sein, daß die Brüche nur gekürzt vorliegen und der Zähler nichtnegativ ist.



  • volkard schrieb:

    dot schrieb:

    Ich finde, dass es in der Regel keinen Sinn macht, Typen mit Wertsemantik aus einem typischen OO Blickpunkt zu betrachten. In dem konkreten Fall würde ich einfach ein struct mit zwei Membern machen und die entsprechenden Constraints über das Typsystem ausdrücken:

    struct rational
    {
      int numerator;
      nonzero_int denominator;
    };
    

    std::string.
    Und die Constraints gehören aus nonzero_int raus und in rational rein. Da wird oft auch drin sein, daß die Brüche nur gekürzt vorliegen und der Zähler nichtnegativ ist.

    Das stimmt natürlich, dann aber so machen wie std::complex und nur getter, aber keine setter anbieten...



  • dot schrieb:

    hustbaer schrieb:

    Beispielsweise wird quasi jedes Widget einer GUI Library einen "Setter" für die Position haben. Ob der nun "SetPosition" oder "Move" heisst ist dabei mMn. irrelevant. Das heisst aber nicht dass dabei die Kapselung gebrochen wurde.

    Das hängt wohl stark vom Design der UI Library ab. Bei mir ist die Position eines Widget z.B. kein Attribut des Widget, sondern wird vom Container des Widget bestimmt, welcher sich um das Layout kümmert... 😉

    Dann sagen wir halt Size oder von mir aus der Text eines Buttons/Labels.



  • hustbaer schrieb:

    dot schrieb:

    hustbaer schrieb:

    Beispielsweise wird quasi jedes Widget einer GUI Library einen "Setter" für die Position haben. Ob der nun "SetPosition" oder "Move" heisst ist dabei mMn. irrelevant. Das heisst aber nicht dass dabei die Kapselung gebrochen wurde.

    Das hängt wohl stark vom Design der UI Library ab. Bei mir ist die Position eines Widget z.B. kein Attribut des Widget, sondern wird vom Container des Widget bestimmt, welcher sich um das Layout kümmert... 😉

    Dann sagen wir halt Size oder von mir aus der Text eines Buttons/Labels.

    Die Größe wird ebenfalls vom Layout Container bestimmt, für Text haben meine Steuerelemente nur Setter und keine Getter, da ich intern so weit möglich nur den fertig gesetzten Text bzw. Glyphensequenzen speichere... 😉



  • dot schrieb:

    Die Größe wird ebenfalls vom Layout Container bestimmt

    Und wie reagieren deine Widgets dann darauf wenn sich ihre Grösse ändert?



  • hustbaer schrieb:

    dot schrieb:

    Die Größe wird ebenfalls vom Layout Container bestimmt

    Und wie reagieren deine Widgets dann darauf wenn sich ihre Grösse ändert?

    Das ganze funktioniert nach einem Two Pass Layout Verfahren, wie es z.B. auch WPF verwendet. Wenn ein Layout Container feststellt, dass sein Inhalt neu gemacht werden muss, dann frägt er erst alle Controls wieviel Platz sie jeweils gerne hätten, generiert basierend auf dieser Information das Layout und teilt den Controls dann mit, welchen Platz sie bekommen.



  • Boh - was für ein langer Thread.

    Grundsätzlich stehe ich hinter der Meinung die Nexus vertritt.
    Bei mir haben auch 2D/3D-Vektoren, Brüche oder Komplexe Zahlen keine Set-Methoden und selbstverständlich sind deren Member privat. Ich habe auch nie so eine Set-Methode vermisst. Das heißt nicht, dass ich nicht auch kleine Strukturen mit public-Membern benutze, wo es Sinn macht.
    Aber ein SetNenner bei einem Bruch macht IMHO soviel Sinn, wie ein SetDezimalZiffer-Methode bei einem int oder ein SetSinusWert bei einem Winkel. Zu diesem Thema hatte ich schon mal eine längere Diskussion.

    Noch mal ein kleines Beispiel, um das was Nexus schon gesagt hat, zu unterstreichen. Mal angenommen man soll eine Klasse Statistik erstellen, die Mittelwert und Standardabweichung berechnet. Dann habe ich hier im Forum schon Ansätze gesehen, die in etwa so aufgebaut waren:

    class Statistik
    {
    public:
        std::vector< double >& getDaten();
        void berechne();
        double getMittelwert();
        double getStandardabweichung();
    private:
        double mittelwert;
        double standardabweichung;
        std::vector< double > daten;
    };
    

    Da hat sich einer überlegt, dass man zur Bestimmung von Mittelwert und Standardabweichung alle Werte und zwei Variablen für das Ergebnis benötigt. getDaten() liefert den vector , den kann man dann schön mit Werten füllen, anschließend ruft man berechne() auf und hol dann mit den get-Methoden die Ergebnisse ab. Da alles in einer Klasse 'gekapselt' ist, soll es dann objektorientiert sein.

    Wenn man sich in die Position des Anwenders dieser Klasse versetzt, ist das aber so gar nicht notwendig. Der Anwender will vielleicht nur eine Ausgabe in ein Protokoll über Mittelwert und Standardabweichung. Was er wirklich nur dazu tun muss ist, dem Objekt die einzelnen Werte mitzuteilen. Also vielleicht so:

    class Statistik
    {
    public:
        Statistik();
        void add( double value );
    
        friend std::ostream& operator<<( std::ostream& out, const Statistik& s );
    private:
        std::size_t n_;
        double mean_, m2_; // siehe http://www.c-plusplus.net/forum/p2270075#2270075
    };
    

    Der Anwender hat hier keine Chance das berechne() zu vergessen. Wenn er ein Objekt dieser Klasse auf den std::ostream ausgibt, bekommt er immer das aktuelle Ergebnis. Und intern wird der vector gar nicht benötigt, wie man auch bei Wiki nachlesen kann.

    Der Unterschied ist, dass man mit der ersten Lösung das Innere der Klasse nach außen kehrt und damit auch die Art der Implementierung - zumindest teilweise - fixiert. Beim zweiten Ansatz ist das nicht der Fall. Das Interface ist kondensiert auf das, was der Anwender in diesem Fall benötigt. In einem anderen Projekt kann das natürlich anders aussehen.

    Gruß
    Werner


Anmelden zum Antworten