Wie zum **** teile ich meine 100k Zeilen JS vernünftig auf? Ein Ansatz.

Diese Seite verwendet Cookies. Durch die Nutzung unserer Seite erklären Sie sich damit einverstanden, dass wir Cookies setzen. Weitere Informationen

  • Wie zum **** teile ich meine 100k Zeilen JS vernünftig auf? Ein Ansatz.

    Ich habe wahrscheinlich, wie viele andere auch, JS in der Praxis gelernt. Mit vielen tollen Tutorials online auf Seiten, wie SelfHTML. Ich habe viel geübt und über die Jahre hinweg, Websites gebaut, mich verbessert, mal mehr JS, dann wieder mehr CSS, dann wieder mehr JS eingesetzt. Ich fühle mich sicher, wenn ich JS schreibe. Doch seit einiger Zeit stehe ich vor einem Dilemma, das mir bisher kein Tutorial und keine Google Suche beantworten konnte. Wie gestalte ich meinen Code und mache ihn gut wiederverwendbar und modular, gut wartbar und einfach zu debuggen, wenn ich viele, viele, ja sogar sehr viele Zeilen JS schreibe?
    Um dieses Problem zu lösen habe ich Abhilfe bei anderen Sprachen gesucht, die ich verwende und die regelmäßig mit einem solchen Problem zu kämpfen haben: C++, Delphi, C#, Java. Wie wird dort jeweils der Code unterteilt?
    Kurz gesagt sind die drei letztgenannten Sprachen identisch: Eine Datei, eine logische Einheit (Klasse). C++ ist ein wenig extravagant und separiert in eine Header und eine Code Datei. Die Header Datei enthält Deklarationen, die Code Datei die zugehörigen Definitionen. Das ist so natürlich sehr grob erklärt, aber für das im folgenden beschriebene Muster reicht es.

    Exkurs: Deklaration vs Definition

    Deklaration: Ich kündige an, dass es etwas gibt, ohne dass ich es einbaue.
    Definition: Ich implementiere etwas (mit Teilen der Deklaration).
    Implementation: Ich schreib die Schritte, wie es gemacht wird, nacheinander auf.


    Nun habe ich mir die Frage gestellt: Kann ich meinen JS Quelltext nicht auch auf diese Weise unterteilen, um die oben genannten Punkte zu verarbeiten?
    JS ist heutzutage so eine Sache. Wir haben zum einen Node.JS und zum anderen JS im Browser. Beide Seiten haben verschiedene Anforderungen.
    Fangen wir mit Node.JS an. Der Browser Teil ist weiter unten in der ersten Antwort zu finden
    • Eine Applikation, die ich für Node.JS schreibe, sollte mMn. zumindest die LTS Version von Node.JS unterstützen. Also steht mir ein Arsenal an JS Harmony Features zur Verfügung (Class, Promise,...).
    • Alle Dateien liegen im lokalen Dateisystem, also ist Traffic und Dateigröße und -masse irrelevant.
    • Ich kann in Node.JS jederzeit Dateien sehr einfach nachladen (quasi ohne Kosten) und scannen, welche Dateien mir überhaupt zur Verfügung stehen.
    • Node.JS arbeitet mit Modulen, die Beschreibungen haben und als separate logische Einheiten betrachtet werden können.

    Ich mag die Unterteilung in Interfaces (Deklarationen) und Implementation (Definitionen), also ist der C++ Weg genau richtig für mich. Allerdings ist mir aufgefallen, dass ich bestimmte Methoden in JS aufgrund der Asynchronität besser in kleine Happen unterteile und diese dann nacheinander abarbeite. Dabei entstehen leicht einige Helfer-Funktionen, die aber insgesamt nur für eine Methode von Bedeutung sind. Ausgestattet mit Fehlertoleranz (selbstheilender Programmierung) ist das ein ganzer Haufen an Zeilen für eine Methode. Und wenn ich dann zehn Methoden in ein Modul einbaue, verliere ich leicht die Übersicht, wer was wo gerade abläuft. Deshalb habe ich für mich eine weitere Unterteilung vorgenommen. Jede Methode bekommt eine eigene Datei. Was ich nun habe ist eine Header Datei und ein paar Code Dateien. Schön. Wir sind aber in JS, nicht in C++. JS versteht den Zusammenhang nicht. Es gibt auch keine Interfaces in JS. Nur Code. Wie verbinde ich also diese Typen von Dateien und packe alles so zusammen, dass ich nur eine Datei einbinden muss, um alles zu haben? Richtig. Ich füge noch einen Typen von Datei hinzu. Ich nenne es die Index-Datei der logischen Einheit (z.B. des Moduls). Quasi der Klebstoff, der alles zusammenhält.
    Mit diesen Überlegungen bewaffnet können wir nun ein Beispiel Modul schreiben:

    JavaScript-Quellcode: package.json

    1. {
    2. "name": "Test",
    3. "version": "0.1.0",
    4. "main": "./index-test.js",
    5. "author": "Maruru",
    6. "website": "https://example.com"
    7. }

    JavaScript-Quellcode: index-test.js

    1. // Die Index Datei
    2. 'use strict';
    3. // Das Interface wird exportiert
    4. module.exports = require('test.h.js');
    5. // Und dann werden die Interface-Methoden mit den Implementationen überschrieben
    6. require('test.foo.c.js');
    7. require('test.bar.c.js');

    JavaScript-Quellcode: test.h.js

    1. // Die Interface Datei
    2. 'use strict';
    3. /**
    4. * Irgendeine Klasse
    5. *
    6. * Hier dokumentieren, worum es bei der Klasse geht
    7. * Dies ist eine Test Klasse!
    8. */
    9. module.exports = class Test {
    10. /**
    11. * Irgendeine Methode
    12. * Das hier ist nur eine Deklaration, also kommt hier keine Implementation hin
    13. * Die Methode wird später überschrieben. Mit dem `throw` können wir nachher einfach Fehler finden, wenn wir vergessen, eine Methode zu überschreiben
    14. *
    15. * @result int
    16. */
    17. foo() { throw 'Not Implemented'; }
    18. /**
    19. * Eine andere Methode
    20. * Mit hübscher Doku. Einfach zu lesen, übersichtlich und perfekt zu verteilen
    21. *
    22. * @param str String
    23. * Dieser Wert wird in die Konsole geschrieben
    24. * @result Promise
    25. * Kein Rückgabewert bei resolve
    26. */
    27. bar(str) { throw 'Not Implemented'; }
    28. };
    Alles anzeigen


    JavaScript-Quellcode: test.foo.c.js

    1. // Eine Code Datei
    2. 'use strict';
    3. var Test = require('test.h.js');
    4. // Diese Funktion könnte ein einzelner Schritt in einer langen Methode sein. Sie ist komplett privat für die Methode und nicht im Modul aufrufbar
    5. var hellow = function () {
    6. console.log('Hellow World!');
    7. };
    8. // Methode überschreiben
    9. // Bitte hier unbedingt eine normale Funktion benutzen, damit `this` weiterhin funktioniert
    10. Test.prototype.foo = function f_test_foo() {
    11. // Mach was
    12. hellow();
    13. return 0;
    14. }
    Alles anzeigen

    JavaScript-Quellcode: test.bar.c.js

    1. // Eine weitere Code Datei
    2. 'use strict';
    3. var Test = require('test.h.js');
    4. // Methode überschreiben
    5. // Bitte hier eine normale Funktion benutzen, damit `this` weiterhin funktioniert
    6. Test.prototype.bar = function f_test_bar(str) {
    7. console.log(str);
    8. return Promise.resolve();
    9. };
    Alles anzeigen

    JavaScript-Quellcode

    1. // Dies ist eine Datei, die unser Test Modul verwendet
    2. // Lasst uns annehmen, dass das Modul Test im Ordner "Test" liegt und dass diese Datei direkt neben dem Ordner liegt.
    3. 'use strict';
    4. // Wir können das Modul wie gewohnt einbinden
    5. var Test = require('./Test');
    6. // Wir können ein Objekt erstellen, das kennt ihr schon!
    7. var t = new Test;
    8. // Und dann könnt ihr lange und komplizierte Methoden aufrufen. Auch hier nichts neues!
    9. t.foo();
    10. t.bar('Wow');
    Alles anzeigen

    Wie ihr seht ist die Vorgehensweise sehr einfach und erzeugt neben übersichtlichen Klassen und Methoden auch eine wunderbare Dokumentation und ist zudem sehr einfach zu debuggen, da nun nicht mehr zwischen tausenden Zeilen Code hin und her gesprungen wird un an dem Dateinamen sofort erkennbar ist, wo man sich denn gerade befindet.
    Durch eine Aufteilung in diese Struktur konnte ich bereits verschiedene Module von der Code Qualität und der Übersichtlichkeit stark verbessern und habe Bugs extrem schnell gefunden und korrigieren können.
    Sicherlich ist diese Aufteilung nicht für jede Situation optimal, aber im Moment experimentiere ich noch damit und bin davon überzeugt.
    Ihr könnt ein vollständiges Modul, das auf diese Weise erstellt wurde, hier betrachten. Es handelt sich dabei um meinen Module Loader (der auch verwendet werden könnte, um die Code Dateien alle mit einer Zeile Quelltext zu laden). Die Anforderung: Ein Dingsi, das gleichzeitig Klasse, Objekt und Funktion ist, um so den größtmöglichen Benutzerkomfort zu ermöglichen. Die Umsetzung: Einfach und übersichtlich. Um das Modul zu benutzen reicht ein Blick in "/src/nml-class.h.js" und die Benutzung sollte kein Problem darstellen.

    Ich hoffe euch hilft dieser Ansatz weiter. Im ersten Post könnt ihr das Pendant dazu für den Browser finden.
    Falls ich neue Erkenntnisse zu diesem Muster finde, werde ich sie nachtragen :) Aber natürlich hoffe ich auf euren Input! Was denkt ihr hiervon? Wo könnte es krachen, wann macht das alles absolut keinen Sinn? Wie kann man das Muster verbessern?
    Look at me, I'm a potato!

    Meine quelloffenen Projekte:

    Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von maruru ()

  • Hab ich mir tatsächlich schon angeschaut, geliebäugelt und dann wieder zur Seite gelegt :D
    TS ist schön und gut, aber wenn ich ein konkretes Problem in JS habe, dann muss ich in TS erst suchen. Und dann das ganze Herumgedudle mit den JS Libs, die keine TS Bindings haben...
    Zumindest damals war TS dann auch noch stark in Entwicklung und etliche Features haben gefehlt, die ich von so nem Ansatz erwartet hätte... also eher nix.
    Außerdem bin ich inzwischen der Meinung, dass TS nur entstand, weil entsprechende Programmier Standards und Guidelines wie was machbar ist fehlen und damals gab es ja auch noch kein Harmony. Also ist es mMn. eigentlich recht sinnfrei, wenn man sich vernünftige Regeln setzt und auf aktuelle Technologie setzen kann.

    tl;dr: Ich bau meine Programme am liebsten in purem "JS2015", wie manche es nennen würden. Und ein vernünftiger Projekt Aufbau gehört einfach dazu.
    Look at me, I'm a potato!

    Meine quelloffenen Projekte:
  • Das ist in der Tat ein interessantes Thema, mit dem ich mich auch schon auseinandergesetzt habe. Clientseitig hat sich das seit AngularJS aber erledigt.

    Von anderen Sprachen würde ich mich aber nicht inspirieren lassen, da sie in der Regel auch andere Arten von Sprachen sind. Java ist klassenbasiert objektorientiert. JavaScript prototypenbasiert objektorientiert. Auch haben andere Sprachen andere Möglichkeiten z. B. Namespaces, Pakete etc., was bei JavaScript einfach fehlt. Es kommen aber endlich bald Module.

    Da JavaScript eine sehr dynamische Sprache ist, hängt die Aufteilung des Quellcodes auch davon ab, wie du JavaScript nutzt. Oder auch welches Framework du benutzt. Teilweise geben Frameworks ja auch eine Ordnerstruktur vor. Oder wenn du RxJS nutzt und eine Anwendung reaktiv programmierst, sind deine Anforderungen an die Ordnerstruktur wieder anders.

    Die Header-Dateien finde ich ehrlich gesagt zu überkompliziert und nicht wirklich notwendig. Das ist aus meiner Sicht einfach viel Code, der nichts tut. Ich würde einfach (wenn du jede Funktion in eine Datei packen willst, was ich jetzt nicht unbedingt für verkehrt halte) alle Funktionen in einer Datei importieren und diese gebündelt als Objekt wieder exportieren. Somit können andere Dateien dieses gebündelte Objekt importieren und die Funktionen daraus nutzen. Diese "Bündeldatei" ist somit in gewisser Weise auch dein Header, da dort ja schon notiert ist, welche Funktionen zur Verfügung stehen.

    Es wird auch von einigen Leuten generell von der Nutzung von Klassen abgeraten. Der Grund kurz zusammengefasst:
    Eine Klasse bzw. ein Konstruktur ist im Grunde nur eine Funktion, die ein Objekt erstellt; allerdings nur auf eine bestimmte Art und Weise. Möchte du diese Art und Weise ändern oder durch eine andere ersetzen, musst du nicht nur die Implementierung ändern, sondern auch alle Stellen, an denen du die Klasse verwendest (das new-Keyword muss weg, evtl. auch der Funktionsaufruf). Stattdessen ist es empfehlenswert, eine Factory-Funktion zu verwenden. Das ist eine simple Funktion, die einfach ein Objekt erstellt und zurückgibt. Wo das Objekt herkommt, ist der Funktion egal (ganz im Gegensatz zum Konstruktur).
    Eine Klasse ist also nichts anderes als:

    JavaScript-Quellcode

    1. function Test() {
    2. return {
    3. property: 42,
    4. method: function () {
    5. return this.property;
    6. }
    7. };
    8. }
    9. var test = Test();
    10. test.method();
    (Das kann man auch noch ein wenig variieren, um mehr Flexibilität zu erhalten.)

    Empfehlenswert ist z. B. auch zu verstehen, was "pure functions" (gibt's dafür einen offiziellen deutschen Begriff?) sind. Wie man eine Funktion implementiert hat ja in gewisser Weise auch etwas mit der Strukturierung zu tun.

    Ein weiterer Anhaltspunkt ist der Begriff "idiomatisches JavaScript". Darunter versteht man, JavaScript so zu verwenden, wie es ursprünglich gedacht war, ohne in "Akzente" aus einer anderen Sprache zu fallen (eben z. B. Klassen).

    Ich hoffe, das hilft zumindest ein wenig.