Warum Ihr Projekt eine eigene Sprache verdient?
\n\nStellen Sie sich vor: Sie schreiben Code zur Berechnung von Versicherungsprämien und sehen anstelle von Dutzenden verschachtelter if-else-Anweisungen und magischen Zahlen eine prägnante Konstruktion:
const rule = new InsuranceRule()\n .when(age > 25)\n .and(experience > 2)\n .then(premium = base * 0.8);\n\nDas ist keine Magie. Es ist ein DSL (Domain Specific Language) – eine domänenspezifische Sprache, eingebettet in TypeScript. In diesem Artikel werden wir uns ansehen, wie man solche Sprachen erstellt, damit Ihr Code die Sprache des Geschäfts spricht, nicht die der Maschinen.
\n\nWas ist ein DSL und warum wird es benötigt?
\n\nEin DSL ist eine Sprache, die zur Lösung von Aufgaben einer bestimmten Fachdomäne entwickelt wurde. Im Gegensatz zu allgemeinen Sprachen (Java, C#, TypeScript) versucht ein DSL nicht, universell zu sein. Es opfert Flexibilität zugunsten der Ausdruckskraft.
\n\nWann sollte man ein DSL verwenden:
\n- \n
- Die Geschäftslogik ändert sich häufig und soll von Analysten bearbeitet werden können \n
- Im Projekt gibt es viele sich wiederholende Konfigurationsmuster \n
- Komplexe Algorithmen müssen vom restlichen Code isoliert werden \n
- Eine Datenvalidierung auf Grammatikebene ist erforderlich \n
TypeScript ist die ideale Wahl für die Erstellung von DSLs. Statische Typisierung, ein leistungsstarkes Typsystem und die Möglichkeit, in JavaScript zu kompilieren, machen es zu einem flexiblen Werkzeug für die Konstruktion sowohl interner als auch externer Sprachen.
\n\nInterne DSLs: Fluent API und Methodenverkettung
\n\nDer einfachste Weg, ein DSL in TypeScript zu erstellen, ist die Verwendung eines Fluent Interface. Dabei gibt jede Methode das Objekt selbst zurück, was die Verkettung von Aufrufen ermöglicht.
\n\nBeispiel: SQL-Abfrage-Builder
\n\nclass QueryBuilder {\n private table: string = '';\n private conditions: string[] = [];\n private selectedFields: string[] = ['*'];\n\n select(...fields: string[]): this {\n this.selectedFields = fields.length ? fields : ['*'];\n return this;\n }\n\n from(table: string): this {\n this.table = table;\n return this;\n }\n\n where(condition: string): this {\n this.conditions.push(condition);\n return this;\n }\n\n and(condition: string): this {\n return this.where(condition);\n }\n\n build(): string {\n let sql = `SELECT ${this.selectedFields.join(', ')} FROM ${this.table}`;\n if (this.conditions.length) {\n sql += ` WHERE ${this.conditions.join(' AND ')}`;\n }\n return sql;\n }\n}\n\n// Verwendung\nconst query = new QueryBuilder()\n .select('name', 'age')\n .from('users')\n .where('age > 18')\n .and('status = "active"')\n .build();\n\nconsole.log(query);\n// SELECT name, age FROM users WHERE age > 18 AND status = "active"\n\nDies ist ein klassisches internes DSL in TypeScript. Es benötigt keinen separaten Parser und funktioniert innerhalb des TypeScript-Codes. Der Hauptvorteil ist die vollständige Unterstützung für Autovervollständigung und Typisierung.
\n\nKomplexer: Typisiertes DSL für Geschäftsregeln
\n\nFügen wir eine strenge Typisierung mit Generics und Union-Typen hinzu:
\n\ntype ComparisonOperator = '>' | '<' | '=' | '!=';\n\nclass RuleBuilder<T extends Record<string, any>> {\n private conditions: ((data: T) => boolean)[] = [];\n\n when<K extends keyof T>(field: K, op: ComparisonOperator, value: T[K]): this {\n this.conditions.push((data: T) => {\n switch (op) {\n case '>': return data[field] > value;\n case '<': return data[field] < value;\n case '=': return data[field] === value;\n case '!=': return data[field] !== value;\n }\n });\n return this;\n }\n\n and<K extends keyof T>(field: K, op: ComparisonOperator, value: T[K]): this {\n return this.when(field, op, value);\n }\n\n evaluate(data: T): boolean {\n return this.conditions.every(c => c(data));\n }\n}\n\n// Datentyp für die Prüfung\ninterface UserData