Skip to content

Das Strategie Pattern in JavaScript – Ein ausführlicher Leitfaden

Published: at 07:00 AMSuggest Changes

Das Strategie-Pattern ist ein wichtiges Verhaltensmuster (Behavioral Design Pattern) in der objektorientierten Programmierung und wird in vielen JavaScript-Projekten eingesetzt, um den Code flexibler, besser wartbar und leichter testbar zu machen.

Die Grundidee besteht darin, dass eine Hauptkomponente – häufig als „Kontext“ bezeichnet – die eigentliche Ausführung einer Aufgabe an eine austauschbare Strategie delegiert. Jede Strategie implementiert dabei dasselbe Grundinterface, sodass sie beliebig untereinander austauschbar sind. Dieses Muster fördert die Trennung der Belange (separation of concerns), da jede Strategie ihre eigene Logik enthält und unabhängig geändert oder erweitert werden kann.

Was ist das Strategie-Pattern in JavaScript?

Angenommen, du möchtest in einem Bestellprozess verschiedene Versandmethoden berücksichtigen: Standard, Express, vielleicht sogar eine spezielle „Öko“-Variante. Wenn du diesen Prozess ohne Strategie-Pattern baust, müsstest du unter Umständen eine einzige monolithische Funktion oder Klasse haben, in der du mit if-Else-Konstruktionen alle Varianten voneinander trennst. Das wird schnell unübersichtlich, insbesondere wenn weitere Varianten hinzukommen.

Das Strategie-Pattern löst dieses Problem, indem es jeder Variante eine eigene Strategie-Klasse / -Funktion zuweist. Die Hauptkomponente (OrderContext) kennt nur eine gemeinsame Methode (z. B. calculate), ruft diese auf und muss sich nicht darum kümmern, wie die intern ausgestaltet ist. So wird dein Code modularer, besser erweiterbar und lässt sich einfacher pflegen.


Order-Beispiel in “klassischem” JavaScript

Werfen wir einen Blick auf die Umsetzung des oben angerissenen Beispiels einer Order-Funktion, die verschiedene Versandmethoden zur Verfügung stellt. Das Beispiel ist in “klassischem” JavaScript gehalten. Später werden wir das Beispiel noch einmal in einer “modernen” klassenbasierten Form zur Verfügung stellen.

function StandardShippingStrategy() {
  this.calculate = function (weight) {
    return weight * 0.5;
  };
}

function ExpressShippingStrategy() {
  this.calculate = function (weight) {
    return weight * 1.5 + 5;
  };
}

function OrderContext(strategy) {
  this.strategy = strategy;

  this.setStrategy = function (strategy) {
    this.strategy = strategy;
  };

  this.calculateShipping = function (weight) {
    return this.strategy.calculate(weight);
  };
}

// Nutzung:
var standard = new StandardShippingStrategy();
var express = new ExpressShippingStrategy();
var order = new OrderContext(standard);

console.log(order.calculateShipping(10)); // 5
order.setStrategy(express);
console.log(order.calculateShipping(10)); // 20

Der Kontext (OrderContext) kümmert sich lediglich darum, eine Strategie zu verwalten und aufzurufen. Die konkrete Berechnungslogik steckt in den Strategien selbst.


Order-Beispiel im “modernen”, klassenbasierten ES6+ Ansatz

Für viele Entwickler:innen, die aus höheren Programmiersprachen wie Java oder C# kommen oder relativ neu in der Programmierung sind, werden sich wahrscheinlich eher zum “moderneren”, klassenbasierten Ansatz gezogen fühlen. Für sie erhöht der Klassen-Syntax die Lesbarkeit und Struktur, zudem lassen sich Module zum Import und Export einzelner Strategien optimal nutzen.

class StandardShippingStrategy {
  calculate(weight) {
    return weight * 0.5;
  }
}

class ExpressShippingStrategy {
  calculate(weight) {
    return weight * 1.5 + 5;
  }
}

class OrderContext {
  constructor(strategy) {
    this.strategy = strategy;
  }

  setStrategy(strategy) {
    this.strategy = strategy;
  }

  calculateShipping(weight) {
    return this.strategy.calculate(weight);
  }
}

// Nutzung:
const standard = new StandardShippingStrategy();
const express = new ExpressShippingStrategy();
const order = new OrderContext(standard);

console.log(order.calculateShipping(10)); // 5
order.setStrategy(express);
console.log(order.calculateShipping(10)); // 20

Die Strategien sind hier als Klassen angelegt und lassen sich sehr gut in eigenständige Dateien auslagern. Gerade bei größeren Projekten trägt dies maßgeblich zur Übersichtlichkeit bei.


Vorteile und Nachteile

Vorteile

  1. Klarere Struktur und bessere Wartbarkeit

Durch das Auslagern der einzelnen Varianten in separate Funktionen / Klassen - je nach verwendetem Ansatz - entsteht eine klarere Code-Struktur. Dies erleichtert Entwickler:innen das Verständnis für den Code und die Einarbeitung in das Projekt.

Stell dir vor, du hast zwei unterschiedliche Verfahren, um Eingaben zu validieren: eine einfache Textvalidierung und eine erweiterte Validierung mit regulären Ausdrücken. Statt all das in einer Funktion zusammenzufassen, könnte dein Code so aussehen:

// Hauptkomponente (Kontext)
class FormValidator {
  constructor(strategy) {
    this.strategy = strategy;
  }

  setStrategy(strategy) {
    this.strategy = strategy;
  }

  validate(input) {
    return this.strategy.validate(input);
  }
}

// Strategie 1: Einfache Textvalidierung
class SimpleValidationStrategy {
  validate(input) {
    // Prüft beispielsweise nur, ob das Feld nicht leer ist
    return input.trim().length > 0;
  }
}

// Strategie 2: Reguläre Ausdrücke
class RegexValidationStrategy {
  validate(input) {
    // Prüft bestimmte Muster im String
    return /^[a-zA-Z0-9]{3,}$/.test(input);
  }
}

// Nutzung
const validator = new FormValidator(new SimpleValidationStrategy());
console.log(validator.validate("Hello")); // true

validator.setStrategy(new RegexValidationStrategy());
console.log(validator.validate("Hello")); // true

Hier kannst du eine Strategie leicht ändern oder um eine weitere ergänzen, ohne dass du bestehende Logik anpassen musst.

  1. Hohe Flexibilität und Austauschbarkeit

Das Hinzufügen neuer Varianten ist unkompliziert. Ein bestehendes System muss selten stark angepasst werden, wenn du etwa eine dritte oder vierte Strategie implementieren möchtest.

Du könntest für das oben genannte Validierungsbeispiel eine zusätzliche Strategie schreiben, die ein externes API befragt und die Validierung dort ausführen lässt. Dein bestehender Code ändert sich kaum, du fügst einfach eine neue Klasse hinzu und setzt sie bei Bedarf ein:

class ExternalApiValidationStrategy {
  validate(input) {
    // Hier stellst du dir vor, dass ein Request an ein externes API geht
    // und dort überprüft wird, ob der Input gültig ist.
    // Fiktives Beispiel:
    return callApiForValidation(input);
  }
}
  1. Bessere Testbarkeit

Da jede Strategie in einer eigenen Funktion oder Klasse gekapselt ist, kannst du jede Variante separat testen. Das sorgt für mehr Zuverlässigkeit und erleichtert Refactorings.

Mit einem Testframework wie Jest kannst du eine einzelne Strategie isoliert prüfen:

test("SimpleValidationStrategy returns false for empty string", () => {
  const strategy = new SimpleValidationStrategy();
  expect(strategy.validate("")).toBe(false);
});

Nachteile

  1. Zusätzliche Komplexität und mehr Boilerplate

Insbesondere in kleineren Projekten kann das Anlegen eines Kontextes und mehrerer Strategien unnötig erscheinen, wenn du nur eine einzige Variante hast. Dann wirkt die zusätzliche Struktur zunächst eher aufwendig.

Wenn du nur eine einzige Art der Validierung brauchst, könnte dein Code sehr schlank wie folgt aussehen, ganz ohne Strategie-Pattern:

function validate(input) {
  return /^[a-zA-Z0-9]{3,}$/.test(input);
}
// Kein Kontext, keine extra Klassen, einfach eine Funktion

Hier würde es kaum Sinn ergeben, ein vollständiges Strategie-Pattern aufzubauen, da dir nur eine einzige Funktionalität vorliegt.

  1. Erhöhter Pflegeaufwand bei stark veränderlichen Anforderungen

Wenn sich deine Anforderungen ständig ändern und du mehrmals am Tag neue Varianten hinzufügst oder alte löscht, kann es sein, dass der Projektaufbau mit vielen kleinen Strategie-Klassen eher unübersichtlich wird.

Denk an ein System mit dutzenden dynamischen Validierungsregeln, die alle nur minimal abweichen. Manchmal ist es passender, ein flexibles Regelsystem aufzubauen (z. B. mithilfe von Konfigurationsdateien), als für jede Variante eine komplett eigene Klasse zu pflegen.

  1. Erhöhte Einarbeitungszeit

Entwickler:innen, die das Strategie-Pattern nicht kennen, benötigen eine kleine Lernkurve, um den Aufbau zu verstehen. Die Abstraktion kann anfangs verwirren, wenn man sich fragt, warum mehrere Klassen statt einer einzigen verwendet werden.

In einem Team, das noch keine Erfahrung mit Design Patterns hat, kann das Einführen von Strategien-Strukturen zunächst zu vielen Rückfragen führen („Wofür ist das gut?“, „Warum nicht einfach einen Switch-Case nutzen?“). Sobald sich das Team jedoch damit auskennt, wird der Vorteil in Sachen Wartbarkeit und Struktur deutlich.


Fazit

Das Strategie-Pattern ist eine vielseitige Lösung, um unterschiedliche Varianten einer Aufgabe – wie das Validieren von Eingaben, das Sortieren von Daten oder das Berechnen von Versandkosten – gezielt voneinander zu trennen und flexibel einsetzen zu können. Die klar abgegrenzten Klassen oder Funktionen fördern eine gute Struktur, steigern die Wartbarkeit und erleichtern das Testen.

Dennoch solltest du abwägen, ob du wirklich mehrere Varianten benötigst. Sobald nur eine einzige Variante ansteht, kann der Einsatz des Muster zu viel Overhead erzeugen. Wenn jedoch bereits verschiedene Vorgehensweisen nötig sind oder die Anforderungen voraussichtlich wachsen, wird dir das Strategie-Pattern viel Flexibilität und Erweiterbarkeit schenken.


FAQ

1. Wann lohnt sich der Einsatz des Strategie-Patterns besonders?
Vor allem dann, wenn du bereits mehrere oder zumindest potenziell weitere Varianten für ein bestimmtes Vorgehen umsetzen möchtest. Ein kurzer Ausschnitt für unterschiedliche Sortier-Algorithmen könnte zum Beispiel so aussehen:

class QuickSort {
  sort(data) {
    // QuickSort-Logik
  }
}

class MergeSort {
  sort(data) {
    // MergeSort-Logik
  }
}

class SortingContext {
  constructor(strategy) {
    this.strategy = strategy;
  }

  setStrategy(strategy) {
    this.strategy = strategy;
  }

  sortData(data) {
    return this.strategy.sort(data);
  }
}

2. Wie verhält sich das Strategie-Pattern zu anderen Verhaltensmustern?
Das Strategie-Pattern reiht sich bei den Verhaltensmustern (Behavioral Patterns) ein, zu denen z. B. auch Observer, Template Method oder State gehören. Während Observer den Fokus auf Benachrichtigungen legt, sorgt Strategie für austauschbare Algorithmen in demselben Kontext.

3. Kann das Strategie-Pattern mit Arrow Functions genutzt werden?
Absolut. Du kannst deine Strategien als reine Funktionen definieren und dem Kontext übergeben:

// Strategien (als einfache Arrow Functions definiert)
const StandardShippingStrategy = (weight) => weight * 0.5;
const ExpressShippingStrategy = (weight) => weight * 1.5 + 5;

// Kontextklasse, die eine beliebige Strategie entgegennimmt
class OrderContext {
  constructor(strategy) {
    this.strategy = strategy;
  }

  setStrategy(strategy) {
    this.strategy = strategy;
  }

  calculateShipping(weight) {
    return this.strategy(weight);
  }
}

// Nutzung:
const order = new OrderContext(StandardShippingStrategy);

// Mit der Standard-Strategie berechnen
console.log(order.calculateShipping(10)); // 5

// Strategie zur Express-Variante ändern
order.setStrategy(ExpressShippingStrategy);
console.log(order.calculateShipping(10)); // 20

4. Wie teste ich meine Strategien am einfachsten?
Da jede Strategie für sich steht, kannst du sie mit einfachen Unit-Tests überprüfen. Hier ein Beispiel mit Jest:

test("StandardStrategy returns correct value", () => {
  const strategy = new StandardShippingStrategy();
  expect(strategy.calculate(10)).toBe(5);
});

5. Kann ich das Strategie-Pattern mit TypeScript verwenden?
Ja, das funktioniert problemlos. Da TypeScript auf JavaScript basiert, kannst du die Klassen-Syntax und weitere Features nutzen. Du kannst sogar ein Interface für deine Strategien anlegen:

interface ShippingStrategy {
  calculate(weight: number): number;
}

class StandardShippingStrategy implements ShippingStrategy {
  calculate(weight: number) {
    return weight * 0.5;
  }
}

6. Wie viele Strategien sollte ich maximal anlegen?
Das hängt von deinem Anwendungsfall ab. Du kannst beliebig viele Strategien erstellen, solltest aber auf Übersichtlichkeit achten. Eine beliebte Praxis ist es, nur dann neue Strategien anzulegen, wenn sie sich funktional wirklich unterscheiden.

class PremiumShippingStrategy {
  calculate(weight) {
    return weight * 2.0 + 10;
  }
}

7. Kann das Strategie-Pattern eine Factory ersetzen?
Nein. Eine Factory kümmert sich um die Erzeugung von Objekten und kapselt dabei den Prozess der Instanziierung, während das Strategie-Pattern den Austausch von Algorithmen im selben Kontext regelt. Du kannst beide Muster aber kombinieren, um in einer Factory je nach Bedarf eine passende Strategie zurückzugeben:

function createShippingStrategy(type) {
  if (type === "express") return new ExpressShippingStrategy();
  if (type === "premium") return new PremiumShippingStrategy();
  return new StandardShippingStrategy();
}

8. Welche Rolle spielt das Open/Closed-Prinzip beim Strategie-Pattern?
Das Open/Closed-Prinzip besagt, dass Klassen offen für Erweiterungen, aber geschlossen für Veränderungen sein sollen. Beim Strategie-Pattern bedeutet das, dass du neue Strategien hinzufügen kannst, ohne bestehenden Code wesentlich zu verändern.

class EcoShippingStrategy {
  calculate(weight) {
    return weight * 0.3;
  }
}

9. Wie gehe ich vor, wenn ich feststelle, dass ich nur eine einzige Strategie brauche?
Wenn nur eine einzige Variante vorhanden ist oder geplant ist, lohnt sich das Strategie-Pattern in der Regel nicht. Eine einfache Funktion reicht dann völlig aus:

function shippingCost(weight) {
  return weight * 0.5; 
}

10. Welche Best Practices empfehlen sich beim Einsatz des Strategie-Patterns?
Es ist ratsam, jede Strategie in einer eigenen Datei oder einem eigenen Modul zu verwalten und sprechende Methodennamen zu wählen, damit der Code leicht lesbar bleibt. Auch klar gekennzeichnete Import- und Export-Pfade helfen, das Projekt zu strukturieren:

// StandardShippingStrategy.js
export default class StandardShippingStrategy {
  calculate(weight) {
    return weight * 0.5;
  }
}

// OrderContext.js
import StandardShippingStrategy from './StandardShippingStrategy.js';

export default class OrderContext {
  // ...
}

Insgesamt ist das Strategie-Pattern ein äußerst hilfreiches Entwurfsmuster, um deinen JavaScript-Code mit ES6+ clean und flexibel zu gestalten. Es unterstützt dich darin, mehrere Varianten oder Algorithmen unter einem Dach zu verwalten und diese bei Bedarf auszutauschen. Damit bereitest du dein Projekt auf zukünftige Erweiterungen vor und erhöhst die Wartbarkeit, solange du den Umfang des Musters an die tatsächlichen Anforderungen anpasst.


Buy me a coffee

Wenn Dir meine Beiträge gefallen und sie Dir bei Deiner Arbeit helfen, würde ich mich über einen “Kaffee” und ein paar nette Worte von Dir freuen.

Buy me a coffee



Previous Post
Decorator Pattern in JavaScript – Elegantes Erweiteren von Funktionen und Objekten
Next Post
Das Constructor Pattern in JavaScript – Ein ausführlicher Leitfaden