Simple Service Locator with TypeScript

While working on front-end components we found that applying programming techniques we usually use developing back-end code in Java greatly improves the "developer experience".

We are currently working on a new administration UI to track processes executed by Paloma. This also means migrating some existing UI components from JavaScript / AngularJS to TypeScript / Vue.js. These user interface components rely heavily on data received via HTTP from back-end systems.

One example is a component displaying a stream of log messages. Now we could run the required back-end systems locally or on some development server and fetch our data from there during development, but this is a bit cumbersome.

When developing in Java / Spring Boot, pretty much every component one uses is injected via constructor. It is quite common to have service interfaces and then use mocking or testing implementations during development:

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

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

The Spring container takes care of injecting components that need ExampleService with the proper implementation. There are also inversion of control containers for TypeScript (like InversifyJS) but we decided to go with a Service Locator in our case.

The Service Locator

Our Service Locator is simply a map holding service instances referenced by an ID.

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;
    }
}

Be aware that this implementation is not type safe. If used incorrectly, callers of get(serviceId: string) may not get what they expect.

The Log Service

Now we implement a mock implementation for the service providing our UI component with log messages:

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 our development or testing configuration we can now register the mock implementation with the service locator:

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

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

The UI Component

Our UI component retrieves an instance of the service. It does not care about the implementation as long as it adheres to the contract.

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

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

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

And now we can develop our UI component without the need for a running back-end service!

More from Astina

View all articles