NEWS
[Diskussion] Adapter-Entwicklung: Klassen / Module
-
Hi,
wir hatten einen sehr guten Austausch in Discord im Kanal "Development > #adapter" zu Klassen, Modulen (also Dateien) und nebenbei auch zu Helper-Klassen mit Einschätzungen auch von sehr erfahrenen Entwicklern.
Hier klicken, falls wer Discord noch nicht kenntHier meine ursprüngliche Frage vom 26.08.2020
Kennt ihr einen Adapter, der sehr umfangreichen Quellcode hat und dessen main.js aufgesplittet ist auf mehrere Module (also Dateien, wie etwa unter lib/.
Hintergrund: Würde mir da gerne was abschauen von der Logik her. Ich mach jetzt grad halt mehrere Dateien (Modules), aktuell 5, mit jeweils einer Klasse in der Datei und dann module.exports. Insbesondere geht es mir um den Umgang mit globalen Vars. Setze diese grad in der main.js in die class <Adapter Name> extends utils.Adapter {} im Constructor. Würde mich interessieren, wie andere das machenGenannt wurden dann:
Diskutiert wurde auch "Alles in der main()" - (Adapter https://github.com/gaudes/ioBroker.fahrplan)
Ich habe dann am 02.09.2020 einen Test-Adapter zur Diskussion gestellt:
Hab mich noch mal mit Adapter und verschiedenen Modulen auseinander gesetzt (siehe obige Diskussion). Hier mal ein quasi leerer Test-Adapter, der nur ein paar Logs zum Testen ausgibt: https://github.com/Mic-M/ioBroker.dev-test_classes
Ich lade da zum Test 2 npms (suncalc2 und node-schedule) und ein paar Dateien aus lib/.
Mein Ansatz: Module - sowie weitere globale Variablen - in der Adapter-Klassen-Instanz verfügbar zu haben, also in 'adapter.x', wo x ein Objekt ist, das z.B. "x.latitude='xyz'" enthält. Dabei müssen Inhalte von 'x' lesbar sowie schreibbar sein und dann überall in der Adapter-Instanz verfügbar.
Link zur main.js: https://github.com/Mic-M/ioBroker.dev-test_classes/blob/master/main.jsHierzu gab es einige Antworten, ab hier nachzulesen.
Hier geht es nun weiter.
Jetzt ein Medien-Wechsel vom Chat zu Forum-Thread, weil spezifische Diskussionen in einem allgemeinen Chat schwierig zu handhaben sind, und um alle teilhaben zu lassen.
@Garfonso schrieb:
Ein Beispiel für Effekte der schlechten Kapselung hab ich auch gerade gefunden:
In den beiden Zeilen https://github.com/Mic-M/ioBroker.dev-test_classes/blob/master/main.js#L78 und https://github.com/Mic-M/ioBroker.dev-test_classes/blob/master/main.js#L79 rufst du deine schöne "Helper Funktion" auf. Die ruft jedesmal this.adapter.getForeignObjectAsync('system.config') auf, liest die gewünschte Variable aus und wirft den Rest weg. Longitude hatte der Adapter also schon einmal, bevor er die zweite ("langwierige", weil potentiell über Netzwerk) Abfrage an die DB für den gleichen Datenpunkt macht!
Wenn du wirklich der Meinung bist, dass du diese komplizierte Filterung benötigst, dann könntest du entweder das Config-Objekt einfach in der main.js einmal laden und als Parameter an die Funktion geben oder wenigstens solltest du das config-Objekt in der Funktion cachen (was aber dann Probleme mit sich bringt, wenn es sich doch einmal ändern sollte).
Guter Punkt, danke
@Garfonso schrieb:
(persönlich würde ich alles, was in generic.js ist löschen und, wenn ich es wirklich benötige, in der entsprechenden Klasse definieren, wie ich es da brauche. In 90% der Fälle wird das ganze durch diesen völlig generischen Ansatz viel komplexer als notwendig.
"isNumber" -> das fängt zwar etwas mehr fälle ab als ein parseInt(x, 10) mit Prüfung auf isNaN (das konvertiert dir z.B. '321blub' eiskalt in 321 -> aber meistens ist das doch völlig in Ordnung, nicht?)
Aber das ist vermutlich Geschmacksache)
@apollon77 antwortete:
ja das generic ist Geschmacksache... wenn man mit solchen Helpern besser klar kommt ok. Nur für andere devs ist’s dann meist komplexer zu verstehen. Über einige der Implementierungen kann man wunderbar streiten
isLikeEmpty oder isNumber wären Beispiele
@Garfonso antwortete:
ja, aber das ist legitim, wenn man noch Javascript lernt. Am Anfang ist das ja durchaus erschreckend, dass man nie weiß, was in so einer variable jetzt drin ist. Die generic.js sieht ein bisschen so aus, wie eine Snipplet Sammlung. :slight_smile:
@AlCalzone antwortete:
Ich bin absolut Fan von Helpern, aber die gehören IMO nicht auf die Adapter-Instanz, insbesondere, wenn sie keinen Zugriff auf die Instanz benötigen. Ganz Node.js-style einfach in ne eigene Datei auslagern und von dort importieren. Wenn die Instanz doch benötigt wird, entweder als Parameter übergeben oder mit factory-Funktion - dann gibts auch keine Probleme im Compact Mode.
@Garfonso - das sehe ich sehe ich tatsächlich anders und wie Apollon schon schreibt ist das sicherlich ein wunderbares Streit-Thema
Mein Ziel: den Entwickler mit Routine-Aufgaben zu entlasten (Prüfung ob Variable Zahl, Prüfung ob number/object/array/string wirklich leer, Duplikate aus Array entfernen, usw.) und das eben in generische Funktionen auslagern.Zu isLikeEmpty() als Beispiel:
Mein Ansatz: Ich kann natürlich eine Prüfung auf ein „leeres“ Array, ein „leeres“ Objekt usw. auch immer so machen, oder ich kipp das eben einfach in diese Function rein und bekomme das verifizierte Resultat. Spart einfach Zeit. Ist für mich auch agiler, auch in früheren Entwicklungsphasen, weil da vielleicht beim ersten Code die Variable ein Array ist, später aber nach Umbau vielleicht ein Objekt oder ein String, etc.
Natürlich muss man sich der Gefahr bewusst sein, was da die Function so macht, aber das muss man auch bei einem(!testvar)
, also was da!
dann je nach Variablentyp zurück gibt.Zu isNumber() als Beispiel:
"isNumber" -> das fängt zwar etwas mehr fälle ab als ein parseInt(x, 10) mit Prüfung auf isNaN (das konvertiert dir z.B. '321blub' eiskalt in 321 -> aber meistens ist das doch völlig in Ordnung, nicht?)
Aber warum soll sich der Entwickler überhaupt darum kümmern müssen, wenn das eine wiederkehrende Function erledigen kann?
Wie seht ihr das?:
Welche Meinungen und Erfahrungen habt ihr in der Adapter-Entwicklung bezüglich Aufteilung der main.js in Module (Dateien)?
Gerne auch Meinungen und Erfahrungen zur Verwendung wiederkehrender Routinen/Methoden...
-
Hier noch eine Ergänzung.
@AggroRalf sagte hier:
Ich hatte ja vor kurzem eine Diskussion mit Klassen angestoßen. Der Ansatz würde damit auch nicht direkt funktionieren. Das Problem ist ein bischen, dass die Adapter-Klasse die Ableitung von utils.Adapter hat, dadurch müsste man bei dem Ansatz die anderen Klassen dann auch ableiten (und im constructor super verwenden), was glaub blöd war.
Ich fänds gut, wenn die Adapter-Klasse einfach nur Basics enthält und man seinen "Kram" dann in eigene Klassen auslagert, aber z.B. über eine vorgegebene Helferklasse oder so Zugriff auf die Objekte in der Adapter-Klasse (Config, Log) hätte
Damit wären Updates in der ADapter-Klasse einfach und man könnte die Helfer-Klasse vlt. auch zentral bereitstellen, z.B. mit Error-Handling, Sentry, ...
Könnte man natürlich auch direkt in utils.Adapter oder so packen
Antwort von @AlCalzone :
Ein klassisches Beispiel wie man sowas konventionell macht wären z.B. factory-Methoden:
// mein-modul.js module.exports = function (adapter) { return { methode1() { adapter.log("1"); }, // ... } } // ------- // main.js let meinModul; class Adapter extends ... { constructor() { meinModul = require("mein-modul.js")(this); // oder alternativ als Instanz-Variable speichern } }
Die Vererbung zu missbrauchen, nur um Zugriff auf die Instanz zu haben ist nicht so ganz der JavaScript-Style.
Spricht was dagegen, stattdessen mit ner Klasse zu arbeiten? Oder ist das nur Geschmackssache? Ergebnis hier im Beispiel ja das gleiche. Oder bläht eine Klasse hier unnötig was auf?
// mein-modul.js class MeineKlasse { constructor(adapter) { this.adapter = adapter; } methode1() { this.adapter.log.info('1'); } // ... } module.exports = MeineKlasse; // ------- // main.js const MeinModul = require('./lib/mein-modul.js'); class Adapter extends ... { constructor() { meinModul = new MeinModul(this); } }
Geht auch
Ist ein bisschen Geschmackssache, bzw. Gewohnheit von Programmiersprachen, wo halt alles in Klassen sein muss.In meinem Gedankenmodell sind Klassen für Dinge...
- von denen es mehrere Instanzen geben wird
- oder wo die Vererbung gewinnbringend eingesetzt wird
- oder die ich als "Dinge" verstehe
Für Helfer (insbesondere bunt zusammengewürfelte) passt es nicht so in mein Gedankenmodell, weil in der Node.js-Runtime auch (fast) alle Methoden alleinstehend importiert werden.
Im Endeffekt sind Klassen aber auch nur Funktionen, die hinter etwas Syntax versteckt sind