Cornwall Peter: Klassenbasierte Programmierung - wie umgehen mit externen, globalen Werten?

Hey,

habe bisher eigentlich immer den move fast and break things Ansatz verfolgt - entsprechend chaotisch ging's auch in meinem Code zu. Möchte mich nun aber endlich mit den verschiedenen Paradigmen anfreunden - daher mein jüngster Vorstoß in die klassenbasierte Programmierung.

Dazu hätte ich eine prinzipielle Frage bzgl. Best Practices:

so habe ich einiges über den Nutzen von self-contained Klassen gelesen, die entweder via Dependency Injection oder Interfaces mit der "Außenwelt" kommunizieren. Stolpere trotzdem immer wieder über Usecases, wo viele Instanzen einer Klasse einen globalen Wert mutieren, sei es auch nur ein Zähler, etc.

Dafür müsste ich innerhalb einer Klasse einen Wert heranziehen, der auf globaler Ebene definiert und mutiert wird - die Klasse würde daher in einem anderen Umfeld nicht mehr "funktionieren".

Beispiel (anhand von 'x'):

class myClass {
  constructor(y) {
  this.y = y;
 }
 mutateExternal() {
  x++; // x in diesem Fall im globalen "Raum", Klasse "funktioniert" in anderem Umfeld nicht mehr!
 }
} 

Ist das zulässig? (...natürlich "funktioniert" es, ich würde aber eben gerne das Paradigma der klassenbasierten Programmierung besser verstehen / richtig anwenden lernen...)

Vielen Dank, euer Cornwall Peter

  1. Hallo Peter,

    du stellst die Frage natürlich allgemein, aber hierfür gibt es keine allgemeine Antwort.

    Die Frage ist hier erstmal: Was ist der Zweck dieses Zählers? Wer braucht diesen Zähler, wer fragt ihn ab, was hängt davon ab, was passiert, wenn bestimmte Zählerstände erreicht werden und so weiter.

    BEISPIELSWEISE könnte es ein Objekt geben, das diesen Zähler verwaltet und auch auf Zählerstände reagiert. Und eine Referenz auf dieses Objekt injizierst Du in myClass.

    Wie dieses Objekt heißt und wie die Methode heißt, die den Zählerstand erhöht, ist stark kontextabhängig. Ich schreibe hier mal JavaScript, keine Ahnung, ob Du für JS fragst oder generell.

    class CounterManager {
    	static #zähler = 0;
    	static count() { this.#zähler++; }
    	static get value() { return this.#zähler; }
    }
    
    class myClass() {
    	#counter;
    	constructor(y, counter) {
    		this.counter = counter;
      	this.y = y;
    	}
    	doCounting() {
    		this.#counter.count();
    	}
    }
    
    let foo = new myClass(17, CounterManager);
    

    Vielleicht verwaltet der CounterManager auch mehrere benannte Zähler und löst eine Aktion aus, wenn ihre Summe einen bestimmten Wert überschreitet? Sowas könnte man darin dann unterbringen. CounterManager kann dann ein falscher Name sein. Kennst Du das Spiel "Can't Stop"? Ein Counter Manager könnte zählen, welche Spielfigur wie weit ist und registrieren, dass ein Spieler seine drei Figuren oben und damit gewonnen hat.

    BEISPIELSWEISE könnte der Zähler aber auch eine statische Eigenschaft von myClass sein, und wer sich für den Zähler interessiert, fragt dort nach.

    class myClass() {
    	static #counter = 0;
      static get counter() { return this.#counter; }
    
    	constructor(y) {
      	this.y = y;
    	}
    	doCounting() {
    		myClass.#counter++;
    	}
    }
    
    let foo = new myClass(17);
    foo.doCounting();
    console.log(myClass.counter);
    

    Es kommt halt darauf an, wo dieser Zähler logisch hingehört. Aber eins ist klar: Wenn dieses "zählen" sich in irgendeiner Form fachlich darstellen lässt, dann ist "doCounting" oder "count" der falsche Methodenname, dann gehört die Fachlichkeit in den Methodennamen. Das könnte "registerCollision()" sein oder "countFailedDebit" - wie es denn passt.

    Eine einfache globale Variable ist deswegen natürlich nicht verboten. Aber in einem objektorientierten Design hat sie ziemlich sicher irgendeine fachliche Zugehörigkeit und kann dann entweder statische Eigenschaft einer Klasse sein oder vielleicht auch normale Eigenschaft eines Singleton-Objekts (also eins, das es nur einmal gibt und mit dem Hinz und Kunz redet. Manchmal auch "Container für globales Zeugs" genannt...)

    Rolf

    --
    sumpsi - posui - obstruxi
    1. Danke für die fundierte Analyse!

      Ich denke, dementsprechend hätten wir es tatsächlich mit einem Singleton zu tun.

      Lässt es sich als Best Practice werten, hier eine entsprechend instanzenlose Singleton-Klasse zu kreieren? Würde ich diese dann einfach an Subklassen "durchreichen", frei nach dem [etwas verbosen] Motto:

      /* ~~~~ ACTION! (GET TO DA CHOPPA) ~~~~ */
      
      const myClassInstance = new MyClass(Singleton);
      myClassInstance.createSubclassInstances(10); // creating 10 instances of SubClass (see below)
      myClassInstance.mutateSharedSingleton("myValue"); // "myValue" could be any value: String | Number | Array | Object | Boolean
      //
      const mySubClassInstance = new SubClass(Singleton);
      // !!!!!!!!!!!!!!!
      // BOTH methods should now point to the SAME "myValue" (declared above):
      myClassInstance.getSharedSingleton();
      mySubClassInstance.getSharedSingleton();
      //
      
      /* ~~~~ CLASSES! ~~~~ */
      
      class Singleton {
        static #singleton = "anyValue"; /* String | Number | Array | Object | Boolean */
        static set mutateSingleton(val) {
          /* do something with #singleton */
          Singleton.#singleton = val; /* | Singleton.#singleton.push(val); | (...) */
        }
        static get getSingleton() {
          return Singleton.#singleton;
        }
      }
      
      class MyClass {
        #sharedSingleton;
        #subclassArray = []; /* dependency with subclasses, to make matters even nicer and crispier 😏 */
        constructor(sharedSingleton) {
          this.#sharedSingleton = sharedSingleton;
        }
        mutateSharedSingleton(val) {
          this.#sharedSingleton.mutateSingleton(val);
        }
        getSharedSingleton() {
          return this.#sharedSingleton.getSingleton();
        }
        /* dependency with subclasses, to make matters even nicer and crispier 😏 */
        createSubclassInstances(num) {
          for (let i = 0; i <= num; i++) {
            const subClassInstance = new SubClass(this.#sharedSingleton);
            this.#subclassArray.push(subClassInstance);
          }
        }
        getSubclassInstances(/* FURTHER... */) {
          /* ...LOGIC */
        }
        treatSubclassInstances(/* FURTHER... */) {
          /* ...LOGIC */
        }
      }
      
      class SubClass {
        #sharedSingleton;
        constructor(sharedSingleton) {
          this.#sharedSingleton = sharedSingleton;
        }
        mutateSharedSingleton(val) {
          this.#sharedSingleton.mutateSingleton(val);
        }
        getSharedSingleton() {
          return this.#sharedSingleton.getSingleton();
        }
        /* More Code */
      }
      
      

      ...? Danke nochmal für eure Gedanken dazu!

      LG, Cornwall Peter

      1. Hallo Peter,

        der Singleton ist das einzelne Objekt, nicht der Wert, der im Singleton-Objekt geführt wird.

        Der Begriff Subklasse ist übrigens irreführend, dabei denkt man in der OOP eher an Vererbung. Das ist hier nicht der Fall. Mir fällt aber auf die Schnelle auch kein allgemeingültiger knackiger Begriff ein. Kindklasse?

        Generell: kann man so machen, muss man aber nicht.

        Wenn MyClass tatsächlich verantwortlich ist für die Erzeugung von SubClass-Instanzen, dann sind die beiden eng verzahnt und es wäre legitim, dass das MyClass-Objekt sich selbst an den Konstruktor von SubClass übergibt. Die Aufgabe, den Zähler zu führen (oder das zu mutierende Dings) könnte dann in MyClass liegen und einfach Teil des Eltern-Kind-Kommunikationsprotokolls sein.

        class MyClass {
           #commonValue;
           #children;
        
           commonValue get() {
              return this.#commomValue;
           }
           commonValue set(newValue) {
              this.#commonValue = newValue;
              // Add common logic here
           }
        
           createChildren(n) {
              this.#children = new Array(n);  // push nur nehmen wenn nötig!
              for (let i=0; i<n; i++)
                 this.#children[i] = new SubClass(this);
           }
        }
        
        class SubClass {
           #parent;
           constructor(parent) {
              this.#parent = parent;
           }
        
           doSomething() {
              this.#parent.commonValue = "drölf";
           }
        }
        

        Wie auch immer der BusinessCase genau dazu aussieht. Jedenfalls ist für eine so enge Koppelung ein separates Counter-Objekt nicht zwingend. Eine best practice kann man, ohne die konkrete Praxis zu kennen, schlecht benennen. Wie ich schon sagte: da ist vieles kontextabhängig.

        Wenn Du Methoden hast, die im Prinzip nichts weiter tun als einen Wert setzen, kannst Du wie bei commonValue gezeigt auch Propertygetter und -setter verwenden.

        Rolf

        --
        sumpsi - posui - obstruxi
        1. Hallo,

          mein Senf zum Ausgangsbeispiel wäre wie folgt. Ja, man kann globale Variablen in einer Klassen verwenden, dafür sollte man aber diese auch als solche mit "globalThis" deklarieren/ansprechen. Und zudem sollte sichergestellt sein, dass es wirklich eine globale Variable ist, d.h. diese muss immer global verfügbar sein, wenn die Klasse verwendet wird. Über Sinn und Unsinn entscheidet das Gesamtdesign.
          Alos konkret:

          globalThis.x = 1;
          
          class myClass {
            constructor(y) {
            this.y = y;
           }
           mutateExternal() {
             x++;
             // oder alternative so ansprechen
             globalThis.x++;
           }
          } 
          
          let mut = new myClass();
          mut.mutateExternal();
          
          console.log(x); // Ausgabe ist 3
          

          globalThis wird inzwischen überall unterstützt.

          Gruss Michael

          1. Hallo Michael_K,

            Ja, man kann globale Variablen in einer Klassen verwenden, dafür sollte man aber diese auch als solche mit "globalThis" deklarieren/ansprechen.

            Jaaaaa...ein.

            Eine mit var deklarierte Variable erreicht man auf diese Weise, weil JavaScript sie zu Eigenschaften des globalen Objekts macht.

            Ist sie mit const oder let deklariert, findet man sie in window[1], self[2], global[3] oder globalThis[4] nicht.

            Rolf

            --
            sumpsi - posui - obstruxi

            1. Browser UI ↩︎

            2. Browser UI oder Worker ↩︎

            3. node.js ↩︎

            4. Der neue Standard ↩︎

            1. Deshalb hatte ich geschrieben, dass man diese dann auch wirklich global deklarieren musss. Variablen, die ich mit const bzw. let definiere, sind für mich keine global definierten Variablen. ;-)

              1. Danke euch,

                aber da denke ich, dass das Setzen einer statischen Eigenschaft der Klasse vielleicht klüger ist - diese kann dann von allen Instanzen dieser Klasse mutiert und abgerufen werden - ähnlich wie das mit globalThis auch ginge.

                LG, Cornwall Peter