Skip to content

Das Factory Pattern in JavaScript - Vorteile, Nachteile und Praxisbeispiele

Published: at 07:00 AMSuggest Changes

Das Factory Pattern zählt zu den bekanntesten Entwurfsmustern in der Softwareentwicklung. Es unterstützt uns dabei, einheitliche Schnittstellen zur Objekterzeugung bereitzustellen und verhindert, dass Aufrufer:innen sich mit den Interna der Instanziierung auseinandersetzen müssen. Während viele Beispiele auf einem klassischen, objektorientierten Ansatz basieren, bietet JavaScript dank seiner funktionalen Eigenschaften auch alternative Wege, zum Beispiel mithilfe von Closures und Currying.


Das objektorientierte Factory Pattern

Im objektorientierten Sinne dient das Factory Pattern dazu, das Erzeugen von Objekten zu kapseln. Dabei muss nicht unbedingt das Schlüsselwort new im aufrufenden Code stehen, sondern zum Beispiel eine statische Methode, die die eigentliche Instanziierung und etwaige zusätzliche Logik übernimmt.

class StarWarsApi {
  constructor() {
    this.baseUrl = 'https://swapi.dev/api/';
  }

  /**
   * Zentrale Methode für den API-Abruf.
   * Nutzt den im Konstruktor übergebenen "baseUrl".
   */
  async fetchResource(endpoint, options = {}) {
    const response = await fetch(this.baseUrl + endpoint, options);

    if (!response.ok) {
      throw new Error(`Request failed with status: ${response.status}`);
    }

    return await response.json();
  }

  /**
   * Statische Factory-Methode, die ein StarWarsApi-Objekt erzeugt.
   * Hier kannst du optional eine alternative "baseUrl" mitgeben.
   */
  static create(baseUrl) {
    return new StarWarsApi(baseUrl);
  }

  /**
   * Lädt Informationen über Personen aus dem SWAPI.
   * @param {string} id - Optional. Wenn angegeben (z.B. '1'), wird diese Person geladen.
   *                      Wenn leer, werden alle Personen geladen.
   */
  async fetchPeople(id = '') {
    // Beispiel: 'people/1' -> Luke Skywalker
    return this.fetchResource(`people/${id}`);
  }

  /**
   * Lädt Informationen über Planeten aus dem SWAPI.
   * @param {string} id - Optional. Wenn angegeben (z.B. '1'), wird dieser Planet geladen.
   *                      Wenn leer, werden alle Planeten geladen.
   */
  async fetchPlanets(id = '') {
    // Beispiel: 'planets/1' -> Tatooine
    return this.fetchResource(`planets/${id}`);
  }
}

// --- Beispielhafte Nutzung ---

(async () => {
  try {
    // Erzeugt ein StarWarsApi-Objekt mit Standard-URL (https://swapi.dev/api/)
    const swApi = StarWarsApi.create();

    // Abruf von Luke Skywalker (ID = 1)
    const luke = await swApi.fetchPeople('1');
    console.log('Luke Skywalker:', luke);
    // { name: 'Luke Skywalker', height: '172', mass: '77', ... }

    // Abruf aller Personen (kein Parameter bei fetchPeople)
    const allPeople = await swApi.fetchPeople();
    console.log('Alle Personen:', allPeople);
    // { count: 82, next: "...", results: [...] }

    // Abruf von Planeten (z.B. Tatooine mit ID = 1)
    const tatooine = await swApi.fetchPlanets('1');
    console.log('Tatooine:', tatooine);
    // { name: 'Tatooine', rotation_period: '23', ... }

    // Abruf aller Planeten
    const allPlanets = await swApi.fetchPlanets();
    console.log('Alle Planeten:', allPlanets);
    // { count: 60, next: "...", results: [...] }

  } catch (error) {
    console.error('Fehler beim Abruf:', error);
  }
})();

Erklärung des objektorientiertes Factory Pattern?

  1. Konstruktor: Legt den baseUrl für sämtliche Abrufe fest, standardmäßig https://swapi.dev/api/.
  2. fetchResource: Eine zentrale Methode, die mithilfe des Fetch-API asynchrone Requests durchführt und bei Fehlern eine Exception wirft. So kann jede spezifischere Methode (z.B. fetchPeople oder fetchPlanets) diese Funktion wiederverwenden.
  3. static create(baseUrl): Die statische Factory-Methode, über die wir ein konfiguriertes Objekt von StarWarsApi anlegen. So können wir das zentrale Instanziierungsverfahren leicht kontrollieren und bei Bedarf erweitern (z.B. standardisierte Logging-Funktionalität, zusätzliche Request-Header etc.).
  4. fetchPeople(id = '') und fetchPlanets(id = ''): Diese Methoden nutzen intern fetchResource. Je nach übergebenem id-Parameter wird entweder ein einzelner Datensatz (people/1) oder eine Liste (people/) abgerufen. Das Gleiche gilt für Planeten (planets/).

So bleibt der Code übersichtlich, flexibel und leicht erweiterbar. Wenn du später etwa fetchStarships oder fetchFilms hinzufügen möchtest, musst du lediglich eine neue Methode analog zu fetchPeople und fetchPlanets schreiben.

Dieser Ansatz eignet sich besonders, wenn dein Team an eine objektorientierte Denkweise gewöhnt ist oder du weitere Klassen-Funktionen wie Vererbung, private Felder oder statische Eigenschaften nutzen möchtest.

Vor- und Nachteile des objektorientierten Factory Patterns

Vorteile:

Nachteile:


Funktionale Ansätze des Factory Patterns

JavaScript ermöglicht dank erstklassiger Funktionen und Closures eine flexible, funktionale Herangehensweise an das Factory Pattern. Zwei gängige Varianten sind hier simple Closures und Currying.

Factory Pattern mit Closures

Unten findest du eine funktionale Factory-Implementierung mit Closures, die analog zum objektorientierten Beispiel um die Methoden fetchPeople und fetchPlanets erweitert wurde. Dabei kapselt eine Hauptfunktion createStarWarsApi sowohl die zentrale Abruf-Logik (fetchResource) als auch spezifische Methoden für die Star-Wars-API. Durch das Closure bleibt die baseUrl erhalten, ohne sie jedes Mal erneut als Parameter übergeben zu müssen.

/**
 * Funktionale Factory für das Star Wars API (SWAPI) mithilfe von Closures.
 * Die zurückgegebene Methoden greifen intern auf die Variable "baseUrl" zu.
 */
function createStarWarsApi() {
  const baseUrl = 'https://swapi.dev/api/';

  // Zentraler Request-Handler für SWAPI-Abfragen
  async function fetchResource(endpoint, options = {}) {
    const response = await fetch(baseUrl + endpoint, options);
    if (!response.ok) {
      throw new Error(`Request failed with status: ${response.status}`);
    }
    return await response.json();
  }

  /**
   * Lädt Informationen über Personen (People) aus der SWAPI.
   * Wenn du eine ID übergibst, wird dieser Datensatz geladen.
   * Ohne ID wird eine Liste aller Personen zurückgegeben.
   */
  async function fetchPeople(id = '') {
    // z.B. 'people/1' -> Luke Skywalker
    return fetchResource(`people/${id}`);
  }

  /**
   * Lädt Informationen über Planeten (Planets) aus der SWAPI.
   * Wenn du eine ID übergibst, wird dieser Datensatz geladen.
   * Ohne ID wird eine Liste aller Planeten zurückgegeben.
   */
  async function fetchPlanets(id = '') {
    // z.B. 'planets/1' -> Tatooine
    return fetchResource(`planets/${id}`);
  }

  // Die Factory gibt ein Objekt mit den verfügbaren Methoden zurück.
  return {
    fetchResource,
    fetchPeople,
    fetchPlanets
  };
}

// --- Beispielhafte Nutzung ---
(async () => {
  try {
    // Erzeugt eine spezialisierte "Instanz" der StarWarsApi mit Default-URL
    const swApi = createStarWarsApi();
    
    // Einzelne Person abrufen (Luke Skywalker mit ID = 1)
    const luke = await swApi.fetchPeople('1');
    console.log('Luke Skywalker:', luke);
    // { name: 'Luke Skywalker', height: '172', mass: '77', ... }

    // Alle Personen abrufen
    const allPeople = await swApi.fetchPeople();
    console.log('Alle Personen:', allPeople);
    // { count: 82, next: "...", results: [...] }

    // Einzelnen Planeten abrufen (Tatooine mit ID = 1)
    const tatooine = await swApi.fetchPlanets('1');
    console.log('Tatooine:', tatooine);
    // { name: 'Tatooine', rotation_period: '23', orbital_period: '304', ... }

    // Alle Planeten abrufen
    const allPlanets = await swApi.fetchPlanets();
    console.log('Alle Planeten:', allPlanets);
    // { count: 60, next: "...", results: [...] }

  } catch (error) {
    console.error('Fehler beim Abruf:', error);
  }
})();

Erklärung des funktionalen Factory Pattern mit Hilfe von Closures?

Diese Herangehensweise bleibt dem funktionalen Paradigma treu und erlaubt es dir, weitere Methoden (z.B. fetchStarships) hinzuzufügen, ohne das zugrundeliegende Konzept zu ändern. Alle Methoden profitieren von der gemeinsamen baseUrl und der gemeinsamen Fehlerbehandlung in fetchResource.

Dieses Muster ist insbesondere nützlich, wenn du in einer Anwendung mehrere APIs oder unterschiedliche Endpunkte mit gemeinsamen Einstellungen (z.B. Header, Timeout, Error-Handling) nutzen möchtest. Das gesamte Handling kannst du einmal in der Factory definieren und anschließend bequem verwenden.

Currying als Factory Pattern

Im folgenden Beispiel setzen wir das Currying-Prinzip ein, um eine Factory für die Star-Wars-API (SWAPI) zu bauen. Wir definieren eine Hauptfunktion createCurriedStarWarsApi, die einen internen curryResource-Helper besitzt. Dieser nimmt den Ressourcentyp (z.B. 'people' oder 'planets') entgegen, gibt eine zweite Funktion für die optionale ID zurück und schließlich eine dritte (asynchrone) Funktion, die einen Fetch-Request ausführt.

Damit können wir gezielt fetchPeople und fetchPlanets definieren, die beide curried sind. Du kannst dann in mehreren Schritten festlegen, ob du eine bestimmte ID abrufen möchtest und ob du spezielle Optionen beim Fetch benötigst.

/**
 * Curried Factory für die Star Wars API.
 * Jeder Aufruf fixiert einen Teil der Parameter, bis schließlich der asynchrone
 * Fetch erfolgt.
 */
function createCurriedStarWarsApi() {
  const baseUrl = 'https://swapi.dev/api/';

  // Hilfsfunktion, um Ressourcen zu fetchen (people, planets, ...)
  // 1. "resource" festlegen (z.B. 'people')
  // 2. Optionale "id" festlegen (z.B. '1')
  // 3. Optionen für den Fetch (z.B. { method: 'GET' }) übergeben und Request ausführen
  const curryResource = (resource) => (id = '') => async (options = {}) => {
    // Zusammenbauen des Endpoints
    // Falls id leer ist, rufen wir alle Datensätze ab (z.B. 'people/')
    const endpoint = `${baseUrl}${resource}/${id}`;

    // Fetch-Aufruf
    const response = await fetch(endpoint, options);
    if (!response.ok) {
      throw new Error(`Request failed with status: ${response.status}`);
    }
    return await response.json();
  };

  // Wir definieren zwei eigens benannte Curried-Funktionen für People und Planets
  // Du kannst hier beliebig weitere Ressourcen hinzufügen (starships, films, etc.)
  return {
    fetchPeople: curryResource('people'),
    fetchPlanets: curryResource('planets')
  };
}

// --- Beispielhafte Nutzung ---
(async () => {
  try {
    // Erzeugt ein "curried" API-Objekt mit Standard-URL
    const swApi = createCurriedStarWarsApi();
    
    // fetchPeople('1') => async (options) => {...}, das heißt:
    // Du kannst hier noch weitere Optionen angeben, oder direkt mit () aufrufen.
    const lukeSkywalker = await swApi.fetchPeople('1')();
    console.log('Luke Skywalker:', lukeSkywalker);
    // { name: 'Luke Skywalker', height: '172', mass: '77', ... }

    // Ohne ID: Abruf aller Personen
    const allPeople = await swApi.fetchPeople()();
    console.log('Alle Personen:', allPeople);
    // { count: 82, next: "...", results: [...] }

    // fetchPlanets('1') => Tatooine
    const tatooine = await swApi.fetchPlanets('1')();
    console.log('Tatooine:', tatooine);
    // { name: 'Tatooine', rotation_period: '23', orbital_period: '304', ... }

    // Auch hier: Alle Planeten abrufen
    const allPlanets = await swApi.fetchPlanets()();
    console.log('Alle Planeten:', allPlanets);
    // { count: 60, next: "...", results: [...] }

  } catch (error) {
    console.error('Fehler beim Abruf:', error);
  }
})();

Erklärung des funktionalen Factory Pattern mit Hilfe von Currying?

Dieses Muster kann zunächst ungewohnt sein, da du mehrere nacheinander zurückgegebene Funktionen aufrufst. Dafür ermöglicht es dir, einen Teil der Parameter „vorkonfiguriert“ abzulegen und an verschiedenen Stellen zu verwenden, ohne diese immer neu angeben zu müssen. Das kann besonders in komplexeren Anwendungen sehr hilfreich sein.

Vorteile dieses Curried-Patterns

Wann Currying sinnvoll ist

Currying lohnt sich insbesondere, wenn du Parameter häufig wiederverwenden oder in Pipelines einbinden möchtest. Falls du z.B. oft mit demselben Endpunkt arbeitest, kannst du diesen Teil fixieren und hast dann quasi eine „vorkonfigurierte“ Funktion, die nur noch die Request-Optionen annehmen muss. Für einfache Anwendungsfälle genügt oft eine klassische Factory-Funktion oder ein objektorientierter Ansatz.

Vorteile:

Nachteile:


Fazit

Das Factory Pattern in JavaScript lässt sich auf vielfältige Weise nutzen: klassisch objektorientiert mit Klassen und statischen Methoden, rein funktional via Closures und Currying. Durch die Verwendung moderner ES6 Ansätze, lässt sich die Lesbarkeit noch erhöhen. Jede Variante hat ihre Daseinsberechtigung und wird besonders von Projektanforderungen, Teampräferenzen und der gewünschten Code-Architektur beeinflusst.


Häufig gestellte Fragen

Kann ich auch für kleine Objekte eine Klassen-basierte Factory verwenden?

Ja, das ist technisch kein Problem. Allerdings kann es Overhead erzeugen, wenn dein Objekt nur wenige Eigenschaften hat und keine Methoden. Dann ist eine einfache Funktions-Factory oft kompakter:

class TinyObject {
  constructor(value) {
    this.value = value;
  }
  static create(value) {
    return new TinyObject(value);
  }
}

const obj = TinyObject.create('Hallo');
console.log(obj.value); 
// "Hallo"

Wann ist ein funktionaler Ansatz mithilfe von Closures sinnvoll?

Vor allem dann, wenn du Konfigurationen kapseln oder wiederverwenden möchtest. Ein bekanntes Beispiel ist das Erzeugen gleichartiger Instanzen mit minimalen Änderungen:

function createLogger(prefix) {
  return function(message) {
    console.log(`[${prefix}] ${message}`);
  };
}

const infoLogger = createLogger('INFO');
infoLogger('Server gestartet');
// "[INFO] Server gestartet"

Wie kann ich mehrere Parameter elegant weiterreichen, ohne den Code zu überfrachten?

Verwende Destrukturierung und ein Options-Objekt. Das steigert die Lesbarkeit und lässt sich leichter erweitern:

function createUser({ name, age, role = 'User' }) {
  return { name, age, role };
}

const user = createUser({ name: 'Alice', age: 25 });
console.log(user);
// { name: 'Alice', age: 25, role: 'User' }

Welche Vorteile bieten private Felder (#) in Klassen in Bezug auf das Factory Pattern?

Private Felder ermöglichen eine echte Datenkapselung innerhalb der Klasse. Du kannst dann sicherstellen, dass intern genutzte Variablen nicht ungewollt von außen modifiziert werden können:

class SecureItem {
  #secret;
  constructor(secret) {
    this.#secret = secret;
  }
  static create(secret) {
    return new SecureItem(secret);
  }
  reveal() {
    console.log(`Geheimnis: ${this.#secret}`);
  }
}

const item = SecureItem.create('Top Secret');
item.reveal(); // "Geheimnis: Top Secret"

Kann ich mit funktionalen Factories auch Prototyp-Methoden nutzen?

Ja, indem du etwa Object.create(proto) verwendest. So kannst du Methoden auf dem Prototyp hinterlegen und pro Instanz nur die spezifischen Eigenschaften setzen:

const userProto = {
  greet() {
    console.log(`Hallo, ich bin ${this.name}`);
  }
};

function createUser(name) {
  const user = Object.create(userProto);
  user.name = name;
  return user;
}

const alice = createUser('Alice');
alice.greet();
// "Hallo, ich bin Alice"

Wie kann ich in einer Factory sicherstellen, dass nur ein bestimmtes Objekt erzeugt wird (Singleton)?

Du kannst in der Factory eine statische oder externe Variable speichern, um zu prüfen, ob bereits eine Instanz existiert, und bei erneutem Aufruf dieselbe Instanz zurückliefern:

 class Singleton {
   static #instance;

   constructor(name) {
     this.name = name;
   }

   static getInstance(name) {
     if (!Singleton.#instance) {
       Singleton.#instance = new Singleton(name);
     }
     return Singleton.#instance;
   }
 }

 const s1 = Singleton.getInstance('Erste Instanz');
 const s2 = Singleton.getInstance('Zweite Instanz');

 console.log(s1.name);   // 'Erste Instanz'
 console.log(s2.name);   // 'Erste Instanz'
 console.log(s1 === s2); // true

Wie sinnvoll ist Currying in der Praxis für das Factory Pattern?

Currying kann sehr hilfreich sein, wenn man häufig dieselben Parameterwerte wiederverwendet oder in Pipelines/Kompositionen denkt. In klassischen Use-Cases kann Currying allerdings die Lesbarkeit mindern, wenn dein Team nicht daran gewöhnt ist:

function curried(a) {
  return (b) => (c) => a + b + c;
}

const addFive = curried(2)(3);
console.log(addFive(10)); 
// 15

Welche Rolle spielen ES-Module für das Factory Pattern?

Durch ES-Module kannst du deine Factories in separaten Dateien definieren und nur das exportieren, was du wirklich benötigst. So bleibt dein Code sauber gekapselt, beispielsweise:

// carFactory.js
export function createCar(brand, model) {
  return { brand, model };
}

// main.js
import { createCar } from './carFactory.js';
const myCar = createCar('Tesla', 'Model 3');

Wie kann ich Asynchronität (z.B. API-Calls) in eine Factory integrieren?

Nutze einfach async/await oder Promises. Dabei kann deine Factory asynchron Daten laden oder verarbeiten, bevor sie ein fertiges Objekt zurückgibt:

async function createUserFromApi(id) {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  return {
    ...data,
    describe() {
      console.log(`User: ${this.name}, Email: ${this.email}`);
    }
  };
}

// Nutzung:
(async () => {
  const user = await createUserFromApi(123);
  user.describe();
})();

Lohnen sich Builder-Patterns als Alternative?

Ja, wenn du sehr komplexe Objekte in mehreren Schritten konfigurieren willst. Das Builder-Pattern (als erweiterte Form des Factory Patterns) kann den Code strukturieren und lesbarer machen. Beispiel:

class CarBuilder {
  constructor() {
    this.brand = '';
    this.model = '';
    this.color = 'white';
  }

  setBrand(brand) {
    this.brand = brand;
    return this;
  }

  setModel(model) {
    this.model = model;
    return this;
  }

  setColor(color) {
    this.color = color;
    return this;
  }

  build() {
    return { brand: this.brand, model: this.model, color: this.color };
  }
}

const myCar = new CarBuilder()
  .setBrand('Tesla')
  .setModel('Model 3')
  .setColor('red')
  .build();

console.log(myCar);
// { brand: 'Tesla', model: 'Model 3', color: 'red' }

Mit diesen Einblicken in objektorientierte und funktionale Umsetzungsmöglichkeiten des Factory Patterns, sowie den modernen Features der Sprache, sollte klar sein, dass es in JavaScript keinen Mangel an Optionen gibt. Die richtige Wahl hängt dabei immer vom konkreten Anwendungsfall, den Teamvorlieben und den Wartungsanforderungen ab.


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


JavaScript Closures: Ein vollständiger Leitfaden


Previous Post
Das Observer Pattern in JavaScript - Vorteile, Nachteile und Praxisbeispiele
Next Post
Das Singleton Pattern in JavaScript - Vorteile, Nachteile und Praxisbeispiele