Einfacher Service Locator mit TypeScript

Bei der Arbeit an Frontend-Komponenten haben wir festgestellt, dass Programmiertechniken, die wir normalerweise bei der Backend-Entwicklung in Java einsetzen, die «Developer Experience» erheblich verbessern.

Wir arbeiten zurzeit an einer neuen Administrationsoberfläche, mit der sich die von Paloma ausgeführten Prozesse verfolgen lassen. Das bedeutet auch, dass wir einige bestehende UI-Komponenten von JavaScript / AngularJS nach TypeScript / Vue.js migrieren. Diese Benutzeroberflächen-Komponenten sind stark auf Daten angewiesen, die sie per HTTP von Backend-Systemen erhalten.

Ein Beispiel ist eine Komponente, die einen Stream von Log-Meldungen anzeigt. Wir könnten nun die benötigten Backend-Systeme lokal oder auf einem Entwicklungsserver betreiben und unsere Daten während der Entwicklung von dort beziehen, aber das ist etwas umständlich.

Bei der Entwicklung mit Java / Spring Boot wird so gut wie jede verwendete Komponente über den Konstruktor injiziert. Es ist durchaus üblich, Service-Interfaces zu haben und während der Entwicklung Mock- oder Test-Implementierungen zu verwenden:

interface LogService {
  Stream<LogMessage> stream();
}

class MockLogService implements LogService {
  Stream<LogMessage> stream() {
    return Stream.of(/* mock messages */);
  }
}

Der Spring-Container sorgt dafür, dass Komponenten, die ExampleService benötigen, die passende Implementierung injiziert bekommen. Es gibt auch Inversion-of-Control-Container für TypeScript (wie InversifyJS), aber wir haben uns in unserem Fall für einen Service Locator entschieden.

Der Service Locator

Unser Service Locator ist einfach eine Map, die Service-Instanzen unter einer ID ablegt.

const instances: {[key: string] : any} = {};

export default {

    set<T>(serviceId: string, instance: T) {
        // ... error handling ...
        instances[serviceId] = instance;
    },

    get<T>(serviceId: string): T {
        const instance = instances[serviceId];
        // ... error handling ...
        return instance;
    }
}

Man sollte sich bewusst sein, dass diese Implementierung nicht typsicher ist. Bei falscher Verwendung erhalten Aufrufer von get(serviceId: string) unter Umständen nicht das, was sie erwarten.

Der Log Service

Nun schreiben wir eine Mock-Implementierung für den Service, der unsere UI-Komponente mit Log-Meldungen versorgt:

export interface LogService {
    stream(onUpdate: (statements: LogStatement[]) => void): void;
}

class MockLogService implements LogService {

    stream(onUpdate: (statements: LogStatement[]) => void): void {

        window.setInterval(() => {
            onUpdate(createMockLogStatements());
        }, 200);
    }
}

In unserer Entwicklungs- oder Testkonfiguration können wir die Mock-Implementierung nun beim Service Locator registrieren:

import services from '@/services/ServiceLocator';
import { MockLogService } from '@/services/LogService';

services.set('log_service', new MockLogService());

Die UI-Komponente

Unsere UI-Komponente holt sich eine Instanz des Service. Die konkrete Implementierung spielt für sie keine Rolle, solange diese den Vertrag einhält.

import services from '@/services/ServiceLocator';
import { LogService } from '@/services/LogService';

const logs: LogService = services.get('log_service');

logs.stream((statements: LogStatement[]) => ....);

Und schon können wir unsere UI-Komponente entwickeln, ohne dass ein Backend-Service laufen muss!

Mehr von Astina

Alle Artikel