ebody: Getter / Setter vs. Eigenschaften im Konstruktor

Hallo,

den Sinn von get() kann ich nachvollziehen. Man möchte eine Eigenschaft von außen abfragen können, sie soll aber nicht von außen zu ändern sein.

Die Kombination von get() und set() verstehe ich aber nicht wirklich. Man kann doch im Konstruktor einer Klasse eine Eigenschaft festlegen, welche man ebenfalls von außen lesen/ändern kann.

Warum dann get() und set() verwenden?

Gruß ebody

  1. Hallo ebody,

    es kommt darauf an, ob die Eigenschaften deines Objekts readonly sein sollen oder nicht.

    Für readonly-Eigenschaften reichen eine Festlegung im Konstruktor und öffentliche Getter.

    Wenn die Eigenschaften von außen auch verändert werden können sollen, brauchst Du auch einen Setter.

    Nach der reinen Lehre solltest Du aber auch im Konstruktor die Setter verwenden.

    Solange Du im Setter nur eine private Eigenschaft setzt, scheint er ziemlich zwecklos. Aber Du kannst dort auch prüfen, ob der gesetzte Wert zulässig ist. Du hast ggf. Eigenschaften, deren Veränderung weitere Auswirkungen hat. Einfaches Beispiel: Ein Objekt "Kreis" mit den Eigenschaften "radius" und "fläche". Ändert man den Radius, muss sich auch die Fläche ändern. Natürlich kann man die Fläche bei jeder Abfrage neu aus dem Radius berechnen. Das kostet aber bei vielen Flächenabrufen mehr Laufzeit und es ist effizienter, sie im Setter des Radius zu aktualisieren. Das Beispiel ist natürlich konstruiert, der Nutzen ist minimal.

    Ein anderes Beispiel ist ein Objekt des Datenmodells. Das Objekt wurde aus der Datenbank geladen und hat so etwa drölfzig fachliche Eigenschaften, von denen ein Anwender etliche verändern kann. Wenn Du nun $repository->save($object) aufrufst - woher weiß das Repository nun, welche Eigenschaften geändert sind? Soll es die alte Version vorhalten und Stück für Stück die Eigenschaften vergleichen? Oder wäre es einfacher, wenn die Setter dieser Eigenschaften die Änderung tracken und sich das Update SQL daraus automatisch ergibt?

    Rolf

    --
    sumpsi - posui - obstruxi
  2. Aloha ;)

    den Sinn von get() kann ich nachvollziehen. Man möchte eine Eigenschaft von außen abfragen können, sie soll aber nicht von außen zu ändern sein.

    Die Kombination von get() und set() verstehe ich aber nicht wirklich. Man kann doch im Konstruktor einer Klasse eine Eigenschaft festlegen, welche man ebenfalls von außen lesen/ändern kann.

    Warum dann get() und set() verwenden?

    Dir entgeht da noch etwas wichtiges: Möglichkeiten, einzugreifen!

    Solange dein getter nur den Wert zurückliefert und dein setter nur den Wert setzt, der übergeben wird, kein Thema - in diesem Szenario kann man gut auf getter und setter verzichten (außer, wie du selbst sagtest, im Readonly-Fall.

    Jetzt, was könnte es sein, warum du eine Möglichkeit zum Eingreifen haben möchtest - alles natürlich hypothetisch, aber ist mir alles schonmal passiert.

    Ein paar Beispiele für den getter:

    • du möchtest Zugriffe auf die Eigenschaft loggen
    • du möchtest nicht exakt das liefern, was tatsächlich im Objekt gespeichert ist, sondern etwas davon abgeleitetes (Bsp.: Du speicherst eine ID, lieferst ein Objekt)
    • du möchtest vor dem Ausliefern des Wertes erstmal noch schnell prüfen, ob er noch plausibel ist
    • du initialisierst nur Teile deines Objekts direkt und alle anderen erst "bei Bedarf" - z.B. beim Zugriff via get

    Ein paar Beispiele für den setter:

    • du möchtest den zu setzenden Wert auf Plausibilität prüfen ("hier kommen nur positive Zahlen rein")
    • du möchtest die Veränderung mitloggen ("changelog")
    • du möchtest je nach Wert der Variable andere Objekte/Eigenschaften/... mitverändern
    • du möchtest statt eines übergebenen Objekts nur dessen ID speichern
    • ...

    Natürlich kann man all diese Dinge auch innerhalb der sonstigen Prozeduren tun - aber kommt dann eben ganz schnell in den Bereich DRY - don't repeat yourself.

    Statt 20 Mal eine Log-Anweisung zu schreiben, weil du gerade einen Wert abgerufen hast, kannst du das auch genau einmal tun und in Zukunft den Wert über den Getter aufrufen.

    Ich hoffe die Beispiele waren soweit verständlich...

    Grüße,

    RIDER

    P.S.: Ein gut gemeinter Tipp: Gerade am Anfang denkt man: Ach was, das brauch ich ja nicht, geht schon auch so. Meine Erfahrung sagt mir, dass ich kaum ein Projekt zusammenprogrammiert habe, bei dem ich nicht entweder irgendwann wirklich froh war, getter und setter zu verwenden oder mir irgendwann wirklich in den Arsch gebissen habe, weil ich keine getter und setter verwendet hatte... Projekte wachsen oft mit der Zeit, und die Anforderungen auch. Selbst wenn du jetzt vielleicht noch kein Logging (usw.usf.) vorhast: vielleicht kommt das ja noch, und dann willst du ja nicht alles umschreiben. Jetzt einen Dummy-Getter aufzusetzen und nur den zum Abruf zu verwenden ist nur ein Mehraufwand von einigen Zeichen - und wenn du sie brauchst hast du sie und kannst sie einfach ausbauen. Irgendwann später getter und setter nachzurüsten ist... schmerzhaft, wirklich schmerzhaft.

    P.P.S.: In PHP musst du dir deine getter und setter ja nichtmal selber schreiben. __get und __set sind wirklich tolle Sachen!

    --
    Camping_RIDER a.k.a. Riders Flame a.k.a. Janosch Albers-Zoller
    # Twitter # Steam # YouTube # Self-Wiki # Selfcode: sh:) fo:) ch:| rl:) br:^ n4:? ie:% mo:| va:) js:) de:> zu:} fl:( ss:) ls:[
    1. Hallo Camping_RIDER,

      P.P.S.: In PHP musst du dir deine getter und setter ja nichtmal selber schreiben. __get und __set sind wirklich tolle Sachen!

      Whoa - bis dahin konnte ich alles unterschreiben - aber __get und __set sind magic methods, die nicht an spezielle Properties gebunden sind. Statt dessen muss man darin eine Weiche vorsehen, um für jedes Property das jeweils richtige zu tun.

      Solange es nur eine generische Aktion ist, also das Lesen oder Schreiben loggen oder zu notieren, dass die Eigenschaft foo auf den Wert "bar" gesetzt wurde, geht das. Aber spezifische Plausibilitätsprüfungen oder Fachlichkeiten sind auf diese Weise nicht gut platziert.

      Man kann einen "Router" schreiben, der in __get und __set untergebracht ist und beim Auslesen von $obj->foo automatisch prüft, ob eine Methode get_foo existiert und diese dann aufruft.

      Aber will man das? Der Overengineer ist begeistert von der Magie dieser Methoden und merkt auf seinem Entwicklungsrechner nichts davon. Aber ein solcher Router muss in jedes Lesen und Schreiben einer Objekteigenschaft hineingrätschen, und das kostet Zeit. Irgendwer meinte mal zu mir: Mit dem richtigen Einsatz von OOP macht man das schnellste System langsam. Man muss es natürlich im konkreten Fall anschauen. Aber ein Server, der ohnehin schon auf 99% läuft, kann durch solche Methoden auf 120% kommen - und ein Brüderchen verlangen, damit die Seite bedienbar bleibt. Das schlimmste, was einer ad hoc gebauten Webseite passieren kann, ist Erfolg. Und je mehr solcher abstrakten Konstrukte drin sind, desto eher kann der Erfolg wehtun. Magic Methods sind ein gravierender Klotz am Bein, ein JIT Compiler kann hier nichts optimieren und deshalb müssen sie vorsichtig verwendet werden.

      Rolf

      --
      sumpsi - posui - obstruxi
      1. Aloha ;)

        P.P.S.: In PHP musst du dir deine getter und setter ja nichtmal selber schreiben. __get und __set sind wirklich tolle Sachen!

        Whoa - bis dahin konnte ich alles unterschreiben - aber __get und __set sind magic methods, die nicht an spezielle Properties gebunden sind. Statt dessen muss man darin eine Weiche vorsehen, um für jedes Property das jeweils richtige zu tun.

        Eben - für mich verbessert das die Lesbarkeit ungemein. Statt 22 Methoden für 11 Variablen zu haben habe ich nur 2 Methoden und kann da gezielt drin nachschauen. Und noch besser: ich kann eine default-Aktion vorsehen und muss mich daher im besten Fall um gar nichts mehr kümmern, komme aber gleichzeitig in den Genuss, später ganz einfach Aktionen anflanschen zu können.

        __get und __set ermöglichen mir, ziemlich ähnlich zu programmieren wie wenn ich keine getter und setter verwenden würde, dabei aber gleichzeitig die Vorteile abzugreifen.

        Beispiel:

        Im aktuellen Projekt von @Felix Riesterer und mir hantieren wir viel mit Controller-Objekten, die ihre Daten (mithilfe der zugrunde liegenden Datenbank) selbst verwalten. Die Daten, die aus der Datenbank kommen und wieder in die Datenbank müssen, werden in einem $this->data-Array abgelegt. Via __get und __set wird dann einfach geprüft, ob in $this->data ein passender Schlüssel vorkommt, und dann wird der Wert entweder direkt abgelegt/geliefert (default-Aktion) oder eben vorher noch was gemacht (switch/case).

        Das ist in der Praxis für uns sehr komfortabel. Zum Beispiel haben wir immer mal wieder eine ganze Reihe gleichartiger Variablen. Datumswerte beispielsweise, die vom Setter immer auf die selbe Weise (also ein Datumsformat) geprüft werden müssen.

        Statt jetzt für drei Datumswerte drei gleiche Setter zu schreiben, notiere ich einfach weitere cases an meinem switch in __set:

        public function __set ($name, $value) {
        
            if (array_key_exists($name, $this->data)) {
        		
        		// check value
        		switch ($name) {
        
                    case 'date_of_deregistration':
                    case 'date_of_registration':
        			case 'date_of_birth':
        				// ensure valid date value or null
        				if (!is_null($value) && !preg_match(
        					'~^\d{4}-\d{2}-\d{2}$~',
        					$value
        				)) {
        					// shorten possible datetime values
        					if (preg_match(
        						'~^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$~',
        						$value
        					)) {
        						$value = substr($value, 0, 10);
        					} else {
        						throw new \Exception(
        							$this
        							. ' needs a valid date'
        							. ' or datetime value for '
        							. $name
        							. ' property!'
        						);
        					}
        				}
        			break;
        		}
        
        		$this->data[$name] = $value;
        	} else {
                throw new \Exception('No such property found!');
            }
        }
        

        Aber ein solcher Router muss in jedes Lesen und Schreiben einer Objekteigenschaft hineingrätschen, und das kostet Zeit.

        Abgesehen davon, dass ich tatsächlich eben nicht mit einem Router arbeiten würde (was hätte ich davon? da kann ich ja gleich klassisch auf getter und setter setzen?) riecht das für mich nach Mikrooptimierung.

        Wenn dieser Router tatsächlich nichts tut außer das Vorhandensein einer Methode zu prüfen, dann ist dein performancetechnisches Zahlenbeispiel (wenn du mich fragst) um Größenordnungen zu groß (oder dein Server VIIIIEL zu alt 😉).

        Was aber tatsächlich einen Unterschied macht ist für mich der viel geringere Schreibaufwand, die bessere Übersichtlichkeit und der deutlich geringere Overhead (für mich, nicht für die Maschine) durch weniger eigenständige Methoden bei der Verwendung von __get und __set.

        Aber natürlich: YMMV - wenn du anderes gewohnt bist kann ein anderes Vorgehen für dich übersichtlicher sein. Vieles ist Geschmacks- und Gewöhnungssache.

        Grüße,

        RIDER

        --
        Camping_RIDER a.k.a. Riders Flame a.k.a. Janosch Albers-Zoller
        # Twitter # Steam # YouTube # Self-Wiki # Selfcode: sh:) fo:) ch:| rl:) br:^ n4:? ie:% mo:| va:) js:) de:> zu:} fl:( ss:) ls:[
    2. Hallo,

      vielen Dank für die Beispiele. Die zeigen, wann und warum die Kombination get() und set() Sinn macht.

      Gruß ebody