Skip to main content

Dependency Injection in JavaScript

Artikel von

Jerry Ejonavi

feature buffer overflow

17. November 2022

0 Min. Lesezeit

Techniken zur Inversion of Control (IoC) ermöglichen Entwicklerinnen und Entwicklern, sich von herkömmlichen Programmierabläufen zu lösen. Dadurch erhalten sie mehr Flexibilität und Kontrolle über ihren Code. Dependency Injection ist eine Form von IoC und ein Entwurfsmuster, das die Erstellung von Objekten von ihrer Verwendung trennen soll.

In diesem Artikel erfahren Sie, was Dependency Injection ist, wann Sie sie einsetzen sollten und in welchen beliebten JavaScript-Frameworks sie implementiert ist.

Was ist Dependency Injection?

Dependency Injection ist ein Software-Entwurfsmuster, bei dem ein Objekt oder eine Funktion andere Objekte oder Funktionen (Abhängigkeiten) verwendet, ohne sich um deren Implementierungsdetails kümmern zu müssen. Ein Injector, manchmal auch Assembler, Provider oder Container genannt, stellt Objekten die benötigten Abhängigkeiten bereit.

Denken Sie zum Beispiel daran, dass eine Videospielkonsole nur eine kompatible Disc oder Cartridge benötigt, um zu funktionieren. Verschiedene Discs oder Cartridges enthalten in der Regel Informationen zu unterschiedlichen Spielen. Spielerinnen und Spieler müssen nichts über die internen Abläufe der Konsole wissen und müssen zum Spielen normalerweise lediglich Spieldiscs einlegen oder austauschen.

Wenn Sie Software auf diese Weise entwerfen oder Code schreiben, lassen sich Komponenten (wie im obigen Beispiel) leicht entfernen oder ersetzen. Außerdem können Sie Unit-Tests schreiben und Boilerplate-Code in einer Codebasis reduzieren.

Dependency Injection wird häufig in anderen Sprachen wie Java, C# und Python implementiert und verwendet, ist in JavaScript jedoch seltener anzutreffen. In den folgenden Abschnitten betrachten Sie die Vor- und Nachteile dieses Musters, seine Implementierung in beliebten JavaScript-Frameworks und erfahren, wie Sie ein JavaScript-Projekt von Anfang an auf Dependency Injection ausrichten.

Warum brauchen Sie Dependency Injection?

Um vollständig zu verstehen, warum Dependency Injection hilfreich ist, sollten Sie zunächst wissen, welche Probleme IoC-Techniken lösen. Denken Sie zum Beispiel an die Videospielkonsole aus dem vorherigen Beispiel und versuchen Sie, ihre Funktionalität mit Code abzubilden. In diesem Beispiel nennen Sie Ihre Konsole NoSleepStation und gehen von Folgendem aus:

  • Eine NoSleepStation-Konsole kann nur Spiele spielen, die für die NoSleepStation entwickelt wurden.

  • Die einzige gültige Eingabequelle für die Konsole ist eine Compact Disc.

Mit diesen Informationen könnte die NoSleepStation-Konsole folgendermaßen implementiert werden:

// The GameReader class
class GameReader {
  constructor(input) {
    this.input = input;
  }
  readDisc() {
    console.log("Now playing: ", this.input);
  }
}

// The NoSleepStation Console class
class NSSConsole {
  gameReader = new GameReader("TurboCars Racer");

  play() {
    this.gameReader.readDisc();
  }
}

// use the classes above to play 
const nssConsole = new NSSConsole();
nssConsole.play();

Hier befindet sich die zentrale Konsolenlogik in der Klasse GameReader, die von NSSConsole abhängig ist. Die Methode play der Konsole startet ein Spiel mithilfe einer Instanz von GameReader . Hier zeigen sich jedoch einige Probleme, darunter mangelnde Flexibilität und Schwierigkeiten beim Testen.

Flexibilität

Der zuvor gezeigte Code ist unflexibel. Möchte jemand ein anderes Spiel spielen, müsste die Klasse NSSConsole geändert werden – ähnlich wie bei einer echten Konsole, die dafür auseinandergenommen werden müsste. Der Grund: Eine zentrale Abhängigkeit, die Klasse GameReader, ist fest in die Implementierung von NSSConsole codiert.

Dependency Injection löst dieses Problem, indem Klassen von ihren Abhängigkeiten entkoppelt werden und diese nur bei Bedarf bereitgestellt werden. Im vorherigen Codebeispiel benötigt die Klasse NSSConsole lediglich die Methode readDisc() einer Instanz von GameReader.

Mit Dependency Injection lässt sich der vorherige Code folgendermaßen umschreiben:

class GameReader {
  constructor(input) {
    this.input = input;
  }

  readDisc() {
    console.log("Now playing: ", this.input);
  }

  changeDisc(input) {
    this.input = input;
    this.readDisc();
  }
}

class NSSConsole {
  constructor(gameReader) {
    this.gameReader = gameReader;
  }

  play() {
    this.gameReader.readDisc();
  }

  playAnotherTitle(input) {
    this.gameReader.changeDisc(input);
  }
}

const gameReader = new GameReader("TurboCars Racer");
const nssConsole = new NSSConsole(gameReader);

nssConsole.play();
nssConsole.playAnotherTitle("TurboCars Racer 2");

Die wichtigste Änderung in diesem Code ist die Entkopplung der Klassen NSSConsole und GameReader . Eine Instanz von NSSConsole benötigt zwar weiterhin eine Instanz von GameReader, um zu funktionieren, muss diese aber nicht explizit selbst erstellen. Der Provider für Dependency Injection erstellt die Instanz von GameReader und übergibt sie an NSSConsole.

In einer umfangreichen Codebasis kann dies den Boilerplate-Code erheblich reduzieren, da ein Provider für Dependency Injection die Abhängigkeiten erstellt und miteinander verbindet. Sie müssen sich also nicht darum kümmern, Instanzen der Klassen zu erstellen, die eine bestimmte Klasse benötigt – insbesondere dann nicht, wenn diese Abhängigkeiten ihrerseits weitere Abhängigkeiten haben.

Testen

Einer der wichtigsten Vorteile von Dependency Injection zeigt sich bei Unit-Tests. Wenn ein externer Provider die Abhängigkeiten einer Klasse bereitstellt, können Mocks oder Stubs anstelle der nicht getesteten Objekte verwendet werden:

// nssconsole.test.js
const gameReaderStub = {
  readDisc: () => {
    console.log("Read disc");
  },
  changeDisc: (input) => {
    console.log("Changed disc to: " + input);
  },
};

const nssConsole = new NSSConsole(gameReaderStub);

Solange sich die Schnittstelle der Abhängigkeit (die verfügbaren Methoden und Eigenschaften) nicht ändert, kann beim Unit-Testen eines einfachen Objekts mit derselben Schnittstelle eine tatsächliche Instanz ersetzt werden.

Grenzen von Dependency Injection

Beachten Sie, dass Dependency Injection auch Nachteile hat. Diese hängen meist damit zusammen, wie Dependency Injection das Problem des übermäßigen Boilerplate-Codes zu lösen versucht.

Um Dependency Injection optimal zu nutzen, insbesondere in einer umfangreichen Codebasis, muss ein Provider für Dependency Injection eingerichtet werden. Das kann viel Arbeit verursachen und ist bei einfachen Projekten manchmal unnötiger Aufwand.

Dieses Problem lässt sich durch eine Dependency-Injection-Bibliothek oder ein Framework lösen. Die Nutzung eines Dependency-Injection-Frameworks erfordert jedoch in den meisten Fällen eine umfassende Festlegung darauf, da sich die APIs verschiedener Dependency-Injection-Frameworks bei der Konfiguration kaum ähneln.

Dependency Injection in beliebten JavaScript-Frameworks

Dependency Injection ist eine zentrale Funktion vieler beliebter JavaScript-Frameworks, insbesondere Angular(), NestJS und Vue.js.

Angular

Angular ist ein clientseitiges Webentwicklungs-Framework mit allen Tools, die Sie zum Erstellen, Testen und Warten von Webanwendungen benötigen. Dependency Injection ist in das Framework integriert. Eine Konfiguration ist daher nicht erforderlich, und diese Vorgehensweise wird für die Entwicklung mit Angular empfohlen.

Im Folgenden sehen Sie ein Codebeispiel dafür, wie Dependency Injection in Angular funktioniert (aus der Angular-Dokumentation):

// logger.service.ts
import { Injectable } from '@angular/core';

@Injectable({providedIn: 'root'})
export class Logger {
  writeCount(count: number) {
    console.warn(count);
  }
}

// hello-world-di.component.ts
import { Component } from '@angular/core';
import { Logger } from '../logger.service';

@Component({
  selector: 'hello-world-di',
  templateUrl: './hello-world-di.component.html'
})
export class HelloWorldDependencyInjectionComponent  {
  count = 0;

  constructor(private logger: Logger) { }

  onLogMe() {
    this.logger.writeCount(this.count);
    this.count++;
  }
}

In Angular zeigt der Decorator @Injectable an einer Klasse an, dass diese Klasse injiziert werden kann. Eine injizierbare Klasse kann Abhängigkeiten auf drei Arten zur Verfügung gestellt werden:

  • Auf Komponenten-Ebene über das Feld providers des Decorators @Component

  • Auf NgModule-Ebene über das Feld providers des Decorators @NgModule

  • Auf Ebene des Anwendungsstamms, indem Sie einem Decorator @Injectable die Angabe providedIn: 'root' hinzufügen (wie zuvor gezeigt)

Um in Angular eine Abhängigkeit zu injizieren, deklarieren Sie diese einfach im Konstruktor einer Klasse. Die folgende Zeile aus dem vorherigen Codebeispiel zeigt, wie das funktioniert:

constructor(private logger: Logger) // Angular injects an instance of the LoggerService class

NestJS

NestJS ist ein JavaScript-Framework mit einer Architektur, die sich für die Entwicklung hochleistungsfähiger, zuverlässiger und skalierbarer Backend-Anwendungen eignet. Da sich NestJS beim Design stark an Angular orientiert, funktioniert Dependency Injection in NestJS sehr ähnlich:

// logger.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class Logger {
  writeCount(count: number) {
    console.warn(count);
  }
}

// hello-world-di.component.ts
import { Controller } from '@nestjs/common';
import { Logger } from '../logger.service';

@Controller('')
export class HelloWorldDependencyInjectionController  {
  count = 0;

  constructor(private logger: Logger) { }

  onLogMe() {
    this.logger.writeCount(this.count);
    this.count++;
  }
}

Beachten Sie, dass Abhängigkeiten in NestJS und Angular üblicherweise als Singletons behandelt werden. Das bedeutet: Sobald eine Abhängigkeit gefunden wurde, wird ihr Wert zwischengespeichert und während des gesamten Anwendungslebenszyklus wiederverwendet. Um dieses Verhalten in NestJS zu ändern, konfigurieren Sie die Eigenschaft `scope` in den Optionen des Decorators @Injectable. In Angular konfigurieren Sie die Eigenschaft `providedIn` in den Optionen des Decorators @Injectable.

Vue.js

Vue.js ist ein deklaratives und komponentenbasiertes JavaScript-Framework zum Erstellen von Benutzeroberflächen. Dependency Injection lässt sich in Vue.js beim Erstellen einer Komponente mit den Optionen `provide` und `inject` konfigurieren.

Um festzulegen, welche Daten den Nachkommen einer Komponente zur Verfügung stehen sollen, verwenden Sie die Option provide:

export default {
provide: {
message: 'hello!',
}
}

Anschließend können diese Abhängigkeiten mit der Option inject in den Komponenten injiziert werden, die sie benötigen:

export default {
  inject: ['message'],
  created() {
    console.log(this.message) // injected value
  }
}

Um eine Abhängigkeit auf Anwendungsebene bereitzustellen, ähnlich wie bei providedIn: 'root' in Angular, verwenden Sie die Methode provide der Vue.js-App-Instanz:

import { createApp } from 'vue'

const app = createApp({})

app.provide(/* key */ 'message', /* value */ 'hello!')

JavaScript-Frameworks für Dependency Injection

Bisher haben Sie Dependency Injection im Kontext eines JavaScript-Frameworks betrachtet. Was aber, wenn jemand ein Projekt ohne eines der genannten Frameworks starten und keinen Dependency-Injection-Container oder -Resolver von Grund auf selbst implementieren möchte?

Für diesen Anwendungsfall gibt es einige Bibliotheken, die Dependency-Injection-Funktionen bereitstellen, darunter injection-js, InversifyJS und TSyringe. Im folgenden Abschnitt konzentrieren Sie sich auf InversifyJS. Es kann sich jedoch lohnen, auch die anderen Pakete anzusehen, um herauszufinden, welches am besten zu Ihren Anforderungen passt.

InversifyJS

InversifyJS ist ein Dependency-Injection-Container für JavaScript. Er wurde so entwickelt, dass er den Laufzeitaufwand möglichst gering hält und zugleich gute objektorientierte Programmierung (OOP) sowie IoC-Praktiken fördert und unterstützt. Das Repository des Projekts finden Sie auf GitHub.

Um InversifyJS in einem Projekt zu verwenden, müssen Sie es zu den Abhängigkeiten des Projekts hinzufügen. Hier richten Sie InversifyJS mit dem im Artikel verwendeten Gaming-Beispiel ein. Erstellen Sie zunächst mit npm init ein neues Projekt. Öffnen Sie ein Terminal und führen Sie die folgenden Befehle der Reihe nach aus:

mkdir inversify-di
cd inversify-di
npm init

Fügen Sie anschließend inversify mithilfe von npm oder Yarn zu den Projektabhängigkeiten hinzu:

# Using npm
npm install inversify reflect-metadata

# or if you use yarn: 
# yarn add inversify reflect-metadata

InversifyJS verwendet die Metadata Reflection API. Daher müssen Sie das Paket reflect-metadata als Polyfill installieren und verwenden.

Fügen Sie als Nächstes den Code für die Klassen NSSConsole und GameReader hinzu, indem Sie zunächst zwei leere Dateien erstellen:

touch game-reader.mjs nssconsole.mjs

Fügen Sie anschließend den folgenden Code in die jeweilige Datei ein:

// game-reader.mjs
export default class GameReader {
  constructor(input = "TurboCars Racer") {
    this.input = input;
  }

  readDisc() {
    console.log("Now playing: ", this.input);
  }

  changeDisc(input) {
    this.input = input;
    this.readDisc();
  }
}
// nssconsole.mjs
export default class NSSConsole {
  constructor(gameReader) {
    this.gameReader = gameReader;
  }

  play() {
    this.gameReader.readDisc();
  }

  playAnotherTitle(input) {
    this.gameReader.changeDisc(input);
  }
}

Konfigurieren Sie abschließend das Projekt für die Verwendung von InversifyJS:

touch config.mjs index.mjs
// config.mjs
import { decorate, injectable, inject, Container } from "inversify";
import GameReader from "./game-reader.mjs";
import NSSConsole from "./nssconsole.mjs";

// Declare our dependencies' type identifiers
export const TYPES = {
  GameReader: "GameReader",
  NSSConsole: "NSSConsole",
};

// Declare injectables
decorate(injectable(), GameReader);
decorate(injectable(), NSSConsole);

// Declare the GameReader class as the first dependency of NSSConsole
decorate(inject(TYPES.GameReader), NSSConsole, 0);

// Declare bindings
const container = new Container();
container.bind(TYPES.NSSConsole).to(NSSConsole);
container.bind(TYPES.GameReader).to(GameReader);

export { container };
// index.mjs
// Import reflect-metadata as a polyfill
import "reflect-metadata";
import { container, TYPES } from "./config.mjs";

// Resolve dependencies
// Notice how we do not need to explicitly declare a GameReader instance anymore
const myConsole = container.get(TYPES.NSSConsole);

myConsole.play();
myConsole.playAnotherTitle("Some other game");

Sie können die neue Einrichtung testen, indem Sie im Stammverzeichnis des Projekts node index.mjs ausführen.

Dieses einfache Beispiel eignet sich als guter Ausgangspunkt für die meisten Projekte, die InversifyJS verwenden möchten.

Fazit

In diesem Artikel haben Sie Dependency Injection in JavaScript, ihre Vor- und Nachteile, Beispiele aus beliebten JavaScript-Frameworks und ihre Verwendung in einem JavaScript-Projekt ohne Framework kennengelernt.

Weiterlesen

blog feature snyk container custom base recommendations
Blog

Evo Agentic AppSec: Sicherheit, die mit der KI-Entwicklung Schritt hält

KI-gestützte Entwicklung überholt herkömmliche Security-Workflows. Snyk Agentic AppSec setzt Security-Agenten ein, die Schwachstellen kontinuierlich finden, beheben und überprüfen – während Menschen die Maßstäbe festlegen.

blog feature ai violet
Blog

VulnBump: Können KI-Agenten anfällige Abhängigkeiten beheben, ohne den Build zu beschädigen?

VulnBump testet KI-Agenten anhand von 307 realen Dependency-Upgrades. Eine Lösung zählt nur, wenn die Sicherheitslücke behoben ist und Build sowie Tests weiterhin erfolgreich sind.

Blog

Frontier-Modelle fanden die Schwachstellen. Nur der Angreifer fand die Angriffsketten.

Die statische Analyse fand die Schwachstellen, doch erst Live-Angriffstests bewiesen, wie sie sich zu Angriffen verketten lassen. Ein Vergleich von Evo COS, Claude Security und Claude Code Security.