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!