dedlfix: OOP-Propertys mit Accessor

Tach!

Meine Anliegen ist, zu lernen wie man am besten sowas wie Klassen mit Property-Accessor-Funktionalität mit Javascript aufbaut.

Zunächst eine Hilfsfunktion, die die Eigenschaftswerte des Objekts aus einem übergebenen Objekt nimmt und zuweist. Sie ist nicht Teil des Problems, ich möchte sie nicht weiter erläutern, sie darf aber gern kommentiert werden. (Mir gefällt nicht sonderlich, dass sie global rumliegt. Anderseits, sie an Object.prototype anzuhängen, macht es nicht viel besser.)

/**  
 * @param {object} obj an object to set its values  
 * @param {object} data an object containing the values  
 * @param {string[]} names an array containing allowed names  
 */  
function setData(obj, data, names) {  
	if (data)  
		for (var i = 0; i < names.length; i++) {  
			if (data.hasOwnProperty(names[i]))  
				obj[names[i]] = data[names[i]];  
		}  
}  

Foo ist mein erster Ansatz, nachdem mir Object.defineProperties über den Weg gelaufen ist. Stellvertretend wird hier eine Eigenschaft Name hinzugefügt, die sehr einfach als Getter implementiert ist. In der geplanten Anwendung kommen noch mehr Eigenschaften inklusive Setter hinzu, für die ich zumindest wissen muss, wann sie (via Binding mit AngularJS) beschrieben werden, um daraufhin eine Persistierung im LocalStorage vornehmen zu können.

function Foo(data) {  
	var self = {  
		name: ''  
	};  
  
	setData(self, data, ['name']);  
  
	Object.defineProperties(this, {  
		Name: {  
			enumerable: true,  
			get: function () {  
				return self.name;  
			}  
		}  
	});  
}  
  
var foo1 = new Foo({name: 'Foo1'});  
var foo2 = new Foo({name: 'Foo2'});  
console.log(foo1);  
console.log(foo2);  

Ausgabe:
Foo { Name="Foo1"}
Foo { Name="Foo2"}

Die Variable self ist das, was (zumindest in C#) ein Backing Field ist, die private Eigenschaft, die die Werte aufnimmt, die vom Getter/Setter gelesen/geschrieben wird. setData() initialisiert die Werte aus dem übergebenen Objekt und dann wird die Property hinzugefügt.

Soweit so lala, die Testausgabe zeigt, dass Name eine öffentliche Eigenschaft ist und die richtigen Werte enthält. Unschön ist, dass die Property in jede Instanz (this) neu eingefügt wird.

Nun sah ich, dass man heutzutage gar nicht mehr die Konstruktorfunktion so direkt definiert, weil sie dann global angelegt ist und sich nicht modularisieren lässt. Das wäre in meinem Fall aus meiner Sicht nicht weiter tragisch, weil ich kein Framework sondern eine Anwendung baue. Die verwendeten Frameworks modularisieren sich selbst und kommen nicht mit meinen Dingen ins Gehege.

Nächster Versuch ...

var Bar = (function () {  
	var self = {  
		name: ''  
	};  
  
	function Bar(data) {  
		setData(self, data, ['name']);  
	}  
  
	Object.defineProperties(Bar.prototype, {  
		Name: {  
			enumerable: true,  
			get: function () {  
				return self.name;  
			}  
		}  
	});  
  
	return Bar;  
})();  
  
var bar1 = new Bar({name: 'Bar1'});  
var bar2 = new Bar({name: 'Bar2'});  
console.log(bar1);  
console.log(bar2);  

Ausgabe:
Bar { Name="Bar2"}
Bar { Name="Bar2"}

Das war der zweite Ansatz. Dummerweise ist self in der kapselnden anonymen Funktion angesiedelt und da diese nur einmal existiert, existiert auch self nur einmal und nicht in jeder Objektinstanz separat. Ergebnis ist, dass beide Objekte Bar2 für Name zeigen. Vielleicht lässt sich das ja auch recht einfach beheben, nur weiß ich nicht wie. Die Eingenschaft ist jedenfalls diesmal am Prototypen eingehängt und die Konstruktorfunktionalität schön in der Function Bar zusammengefasst.

var Qux = (function () {  
	function Qux(data) {  
		this.self = {  
			name: ''  
		};  
		setData(this.self, data, ['name']);  
	}  
  
	Object.defineProperties(Qux.prototype, {  
		Name: {  
			enumerable: true,  
			get: function () {  
				return this.self.name;  
			}  
		}  
	});  
  
	return Qux;  
})();  
  
var qux1 = new Qux({name: 'Qux1'});  
var qux2 = new Qux({name: 'Qux2'});  
console.log(qux1);  
console.log(qux2);  

Ausgabe:
Qux { self={...}, Name="Qux1"}
Qux { self={...}, Name="Qux2"}

Mit diesem Ansatz ist das self-Problem behoben. Dies entspricht auch ungefähr dem Code, den TypeScript wählt, wenn es Propertys mit get/set-Accessors nach Javascript übersetzt. Der Preis dafür ist aber, dass self nun öffentlich im Objekt zu sehen ist und nicht mehr privat ist. (Man beachte die hinzugekommenen this im Code.) Das öffentliche self ist sicher nicht kriegsentscheidend, wenn man sich daran hält, es nicht direkt zu verändern. Aber schön geht anders. Was wäre denn empfehlenswert?

Ein JSFiddle gibts dazu auch: http://jsfiddle.net/V5X8b/ (Konsole anzeigen lassen nicht vergessen)

dedlfix.

  1. Hallo,

    Symbols wird es in ECMAScript 6 geben und würden dein Problem elegant lösen:

    https://github.com/lukehoban/es6features#symbols
    https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol
    http://people.mozilla.org/~jorendorff/es6-draft.html#sec-symbol-constructor-class

    function setData(obj, data, names) {
    Mir gefällt nicht sonderlich, dass sie global rumliegt. Anderseits, sie an Object.prototype anzuhängen, macht es nicht viel besser.

    Richtig, deshalb gibt es Object.assign in ES6. Shim:
    https://github.com/paulmillr/es6-shim/
    https://github.com/ljharb/object.assign

    function Foo(data) {
    var self = {
    name: ''
    };

    Ich würde die Variable nicht »self«, sondern z.B. »store«, »storage« oder »attributes« nennen, weil man üblicherweise var self = this; schreibt, also self eine Referenz auf die Instanz ist.

    Object.defineProperties(this, {
    Name: {
    enumerable: true,
    get: function () {
    return self.name;
    (…)
    Unschön ist, dass die Property in jede Instanz (this) neu eingefügt wird.

    Finde ich verschmerzbar. Nur so ist echte Privatheit zu erreichen.

    Siehe Wozu Kapselung gut ist und wann sie nötig ist.

    Nun sah ich, dass man heutzutage gar nicht mehr die Konstruktorfunktion so direkt definiert, weil sie dann global angelegt ist und sich nicht modularisieren lässt.

    Das macht man durchaus noch, wenn es zielführend ist. Die zusätzliche IIFE benutzt man nur, wenn man einen privaten Scope braucht.

    var Bar = (function () { … })();

    Hier legst du letztlich auch eine globale Variable an bzw. eine Variable im entsprechenden Scope.

    Dummerweise ist self in der kapselnden anonymen Funktion angesiedelt und da diese nur einmal existiert, existiert auch self nur einmal und nicht in jeder Objektinstanz separat.

    Japp, das sollte man möglichst vermeiden.

    Object.defineProperties(Qux.prototype, {…});

    Preis ist aber, dass self nun öffentlich im Objekt zu sehen ist und nicht mehr privat ist. … Was wäre denn empfehlenswert?

    Das ist eine Grundfrage von JavaScript, auf die ich in der oben verlinkten Doku auch eingehe:

    Entweder man will effektive Privatheit, dann hilft nur die Closure, weil das in JS die einzige Möglichkeit ist, Sichtbarkeit zu regulieren.

    Oder einem reichen pseudo-private Eigenschaften aus. Dann gibt es die Konvention, die Eigenschaften z.B. mit einem »_«-Präfix zu versehen und sie ggf. als non-enumerable und non-configurable zu markieren.

    Ich halte beides für vertretbar. Pseudo-private Eigenschaften halte ich für ausreichend. Sie sind performanter hinsichtlich der Erzeugung von Instanzen, da Getter/Setter einmal am Prototyp definiert werden. Sie erlauben dem JS-Interpreter eine Optimierung mittels »Hidden Classes«. Und sie erlauben ein einfaches Debugging, da die Eigenschaft im Zweifelsfall von außen zugänglich ist.

    Letztlich sind »private« Eigenschaften in vielen anderen Sprachen nicht wirklich privat. Siehe zum Hintergrund:
    http://dmitrysoshnikov.com/ecmascript/chapter-7-1-oop-general-theory/#encapsulation

    Mathias

    1. Tach!

      Symbols wird es in ECMAScript 6 geben und würden dein Problem elegant lösen:
      https://github.com/lukehoban/es6features#symbols
      https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol
      http://people.mozilla.org/~jorendorff/es6-draft.html#sec-symbol-constructor-class

      Da steig ich jetzt noch nicht so richtig durch. Da sind zwar Beschreibungen zum Implementation, aber ich sehe nicht so recht, für welches Problem dieses Feature erfunden wurde. Ist es so, dass damit Property-Namen nichtmehr durch Strings repräsentiert werden (können), sondern durch einen Identifizierer, der von außerhalb nicht bekannt ist, weil er nur in einer privaten Variable gespeichert ist?

      function setData(obj, data, names) {
      Mir gefällt nicht sonderlich, dass sie global rumliegt. Anderseits, sie an Object.prototype anzuhängen, macht es nicht viel besser.
      Richtig, deshalb gibt es Object.assign in ES6.

      Naja, damit holt man sich Tod und Teufel in sein Objekt rein und überschreibt alles, was das andere Objekt so mitbringt. Ich geb ja noch an, welche Eigenschaften mich interessieren beziehungsweise zugelassen sind.

      Ich halte beides für vertretbar. Pseudo-private Eigenschaften halte ich für ausreichend. Sie sind performanter [...] Und sie erlauben ein einfaches Debugging, da die Eigenschaft im Zweifelsfall von außen zugänglich ist.

      Das sind gute Argumente, vor allem die Sichtbarkeit beim Debugging. Es sind ja in meinem Fall mit dem einen Objekt auch nicht so viele, dass sie die Autovervollständigung zumüllen. Ich nehm dann mal Variante Qux.

      Letztlich sind »private« Eigenschaften in vielen anderen Sprachen nicht wirklich privat.

      Ja, Zugriffsmodifizierer sind für mich auch nur Anwendungshinweise und kein Sicherheitsfeature.

      dedlfix.

      1. Hallo,

        Symbols wird es in ECMAScript 6 geben
        Ist es so, dass damit Property-Namen nichtmehr durch Strings repräsentiert werden (können), sondern durch einen Identifizierer, der von außerhalb nicht bekannt ist, weil er nur in einer privaten Variable gespeichert ist?

        Ja, völlig richtig. Es ist in anderen Sprachen gang und gäbe, dass Objekte die Keys von Hashes sein können. Das wird in ES6 bei Map sowie in Form von Symbols bei sämtlichen Objects möglich sein.

        Es sind ja in meinem Fall mit dem einen Objekt auch nicht so viele, dass sie die Autovervollständigung zumüllen.

        Von der IDE? Dagegen helfen eventuell JSDoc-Kommentare, die die Eigenschaft als private deklarieren.

        Mathias

  2. Meine Herren!

    Meine Anliegen ist, zu lernen wie man am besten sowas wie Klassen mit Property-Accessor-Funktionalität mit Javascript aufbaut.

    Ich habe vor Kurzem mal begonnen einen Artikel darüber zu verfassen. Der Artikel ist in schlechtem Englisch verfasst und man kann noch nicht mal von einer Rohfassung sprechen. Ich tue mich schwer damit ihn hier zu teilen, aber weil ich nicht glaube, dass ich in naher Zukunft die Zeit finde, ihn fertig zu stellen, gibt es hier trotzdem den Link:

    https://docs.google.com/document/d/1ln5eq8INeiZOmk_Ey0rPpfY8vbx2oAEfDCSuWZxwzyU/edit?usp=sharing

    Kommentare und Vorschläge sind willkommen.

    --
    “All right, then, I'll go to hell.” – Huck Finn
    1. Tach!

      Meine Anliegen ist, zu lernen wie man am besten sowas wie Klassen mit Property-Accessor-Funktionalität mit Javascript aufbaut.
      https://docs.google.com/document/d/1ln5eq8INeiZOmk_Ey0rPpfY8vbx2oAEfDCSuWZxwzyU/edit?usp=sharing
      Kommentare und Vorschläge sind willkommen.

      Die Idee, das mit einer Map zu lösen, klingt auf den ersten Blick problemlösend. Was mir daran aber nicht gefällt ist, dass da ein Haufen zusätzlicher Code auf das Problem geworfen wird (selbst wenn der sich in einer nativen Implemenation der Map versteckt). Je Eigenschaft ist eine eigene Map zu führen. Die Handhabung beim Zugreifen wird damit auch nicht einfacher, geschweige denn der Code intuitiver.

      dedlfix.

      1. Meine Herren!

        Die Idee, das mit einer Map zu lösen, klingt auf den ersten Blick problemlösend. Was mir daran aber nicht gefällt ist, dass da ein Haufen zusätzlicher Code auf das Problem geworfen wird (selbst wenn der sich in einer nativen Implemenation der Map versteckt). Je Eigenschaft ist eine eigene Map zu führen. Die Handhabung beim Zugreifen wird damit auch nicht einfacher, geschweige denn der Code intuitiver.

        ACK. Symbols scheinen wirklich der ideale Kandidat für dein Problem zu sein. Die Idee ist eigentlich die gleiche, man wählt einen privaten Gültigkeitsbereich für seine Map oder für sein Symbol. Man achte darauf, dass nur privilegierte Member Zugriff auf diesen Gültigkeitsbereich haben und schon hat man eine adäquate Datenkapselung, die über die bekannten Hürden hinaus geht. Der Unterschied besteht wirklich nur noch in der Art und Weise wie man auf diesen geschützten Bereich zugreift. Und da der Zugriff über ein Symbol genau wie der klassische Zugriff auf eine Eigenschaft erfolgt, ist das wohl der zu bevorzugende Weg. In dieser Erkenntnis soll mein Artikel letztendlich auch münden. BTW: Wie hat er sich gelesen?

        --
        “All right, then, I'll go to hell.” – Huck Finn
        1. Tach!

          [mein Artikel] BTW: Wie hat er sich gelesen?

          Diese dämliche Textverarbeitung versaubeutelt erstmal den Code, weil sie Anführungszeichen nach Gutdünken austauscht. Bis auf einzelne Wörter, die ich noch nicht kannte, hab ich ihn verstanden. Inwieweit man als "native speaker" die Stirn runzeln würde, entzieht sich meiner Kenntnis.

          dedlfix.

  3. hi,

    /**

    • @param {object} obj an object to set its values
    • @param {object} data an object containing the values
    • @param {string[]} names an array containing allowed names
      */
      function setData(obj, data, names) {
      if (data)
      for (var i = 0; i < names.length; i++) {
      if (data.hasOwnProperty(names[i]))
      obj[names[i]] = data[names[i]];
      }
      }
      
    Meine Gedanken zu Getter/Setter habe ich [hier](http://rolfrost.de/getter.html) mal aufgeschrieben. Excerpt: Grundsätzlich spricht nichts dagegen, einen Getter so zu benennen, wie ein Attribut namentlich lautet. Ein Setter jedoch, setzt nicht irgendein Attribut schlechthin, sondern verändert die gesamte Instanz! Setter-Namen like setAttributeName() sind irreführend, weil ein Setter nicht nur ein Attribut ändert sondern die Instanz selbst, wobei diese Veränderungen nicht nur ein sondern auch mehrere Attribute betreffen können (Beispiel siehe Link).  
      
    Es ist ein praktisches Beispiel, weil es mir erstrangig gar nicht darum ging, einen Artikel darüber zu schreiben, sondern weil mir diese Thematik erst beim Modernisieren einer alten Perl-Klasse so richtig bewusst geworden ist (mehr Komfort bei weniger Code).  
      
    Ob sich das sinngemäß auch auf JS anwenden lässt kann ich z.Z. noch nicht manifestieren, aber ich bin dran ;)  
      
    MfG
    
    1. Tach!

      Meine Gedanken zu Getter/Setter habe ich hier mal aufgeschrieben. Excerpt: Grundsätzlich spricht nichts dagegen, einen Getter so zu benennen, wie ein Attribut namentlich lautet. Ein Setter jedoch, setzt nicht irgendein Attribut schlechthin, sondern verändert die gesamte Instanz!

      Ja, und? Wenn meine API vorsieht, hinter einer Wertzuweisung dass ganze schmutzige Geschäft zu verstecken, weil es den Aufrufer nichts weiter angeht, oder er sich nicht darum kümmern muss, dann macht sie das eben so. Da seh ich jetzt nichts generell Verwerfliches drin. Auch Funktionsnamen erzählen nicht sämtliche Einzelheiten, die sie zu tun gedenken. Und das ist auch gut so.

      dedlfix.

    2. Meine Gedanken zu Getter/Setter habe ich hier mal aufgeschrieben. Excerpt: Grundsätzlich spricht nichts dagegen, einen Getter so zu benennen, wie ein Attribut namentlich lautet. Ein Setter jedoch, setzt nicht irgendein Attribut schlechthin, sondern verändert die gesamte Instanz! Setter-Namen like setAttributeName() sind irreführend, weil ein Setter nicht nur ein Attribut ändert sondern die Instanz selbst, wobei diese Veränderungen nicht nur ein sondern auch mehrere Attribute betreffen können (Beispiel siehe Link).

      Als Verfechter funktionaler Programmierung und insbesondere vom Einsatz von Immutabilität finde ich folgendes sehr ungünstig:

      $sca->wochentag; # Samstag
          $sca->add(1);
          $sca->wochentag; # Sonntag
          $sca++;          # overload "++"
          $sca->wochentag; # Montag

      Das macht es sehr schwierig darüber zu argumentieren, wie sich das Programm zu einem bestimmten Zeitpunkt verhält. In so einem Fall sollte man eine neue Scaliger (was immer das ist) Instanz erzeugen, die dann das morgige Datum enthält. also so:

      my $dateTomorrow = //hier Datum von heute zuweisen
      my $scaTomorrow = Scaliger->new( $dateTomorrow);