Skip to main content

Part 4: Registering services and using the Service Locator

info
This article was originally published in May 2022 at medium.com. Some formatting might have get lost during the migration to this site: If you think you spotted an issue caused by malformed formatting, feel free to open a Pull Request or send me an Email.

This part of the article series focuses on details of the configuration options available when using coon.core.app.Application from the coon.js-package; namely the services-configuration option that lets you define services that can be queried using coon.core.ServiceLocator, providing a central registry for concrete classes mapped to abstracts.

The previous part of this article series provided an extended look at the various configuration options of a coon.js-application. This article assumes that you are familiar with the topics covered therein.

What is a Service Locator (and why)

If you’re not familiar with the concept of Service Locators, Martin Fowler has a very good introduction to this design pattern over at his blog:

Inversion of Control Containers and the Dependency Injection pattern

In Short, a Service Locator allows for separating large sections of a software. It represents a central registry that is known by every section in the software. Decoupled and unaware of each other, those sections are still able to use one and the same functionality during runtime by querying the Service Locator for concrete implementations of abstracts which describe their required functionality.

{title}
Figure 1 The functionality of a Service Locator in a nutshell: Modules in a software that share no links in between them, can still use the same functionality by using a Service Locator: A central registry providing concrete instances of abstracts.

Simple use case — An Image Service

Let’s assume that we have various modules in our application: An email module, an address book and a SignIn-Manager. Each module has the functionality to show an image that is associated with a person — a profile picture. Now, each module understands the entity “person” in a different way:

  • For the email module, persons are identified by the information found in the from-header

  • The address book identifies a person by contact-information stored in various fields: firstname, lastname, address and so on. Of course, an address book’s user-entry also has an email address.

  • The SignIn-Manager identifies a person (a “user”) by an username and a password. For our use-case, the username must be the user’s email-address

So while all modules are aware of an entity representing a “person”, each module models this entity in a different way. However, each module requires the functionality to show a profile picture associated with a person.

Of help is a common identifier that is usually unique for a person — the email address.

The modules do not know where a profile picture for a person comes from. What they do know is

  • a profile picture can be uniquely associated with an email-address (1:1)

  • how to render a profile picture on the screen

  • that a Service Locator exists for querying services

The common functionality for retrieving image data associated with an email address is given by the following interface:

{title}
Figure 2 The interface ImageService provides a contract for retrieving image data: The public method getImage() returns an object providing arbitrary key/value-pairs representing image-information (src, various attributes such as height, width…) when fed with resolvable search criteria.

Using JavaScript and coon.js, the implementation of a Service Locator is toned down compared to strongly typed languages, such as Java. It follows the same purpose however, and implementing APIs can add or remove abstractions and type checking as they like.

For our purpose, the final implementation in each module would look like this:

    const criteria = {emailAddress: "user@domain.com"};

return coon.core.ServiceLocator
.resolve("ImageService")
.getImage(criteria);

Our search criteria is an object the ImageService understands, and we expect the coon.core.ServiceLocator to correctly resolve from ImageService to a concrete instance implementing this interface, properly responding to our API-calls when requesting image data.

The question that remains is: How do we configure the Service Locator so that it resolves to a concrete instance for a given abstract — in our case, a concrete of ImageService?

Configuring Services

Without the help of a DI Container, the scaffolding of services is done in the application configuration file — mapping class names of abstracts to their concrete representants; building up on the previous article about configuration of application instances, you might remember similar approaches regarding plugins, where class names of plugins are specified that get later on created during runtime. Thus, we are now introducing the services property for the application configuration file.

The following code block is part of an application configuration. The services-key denotes the section where abstracts are mapped to concretes — along with additional arguments that will be passed to the constructor of the service. Each service defined in here is treated as a shared instance — that means, for all modules across the application, the ServiceLocator will return one and the same instance of this class.

    "services": {
"coon.core.service.UserImageService": {
"xclass": "coon.core.service.GravatarService",
"args": [{
"parameters" : {"d": "blank"}
}]
}
}

In the given example, the application registers coon.core.service.GravatarService — a specific of coon.core.service.Service for abstracts of the type coon.core.service.UserImageService. Whenever the ServiceLocator needs to resolve coon.core.service.UserImageService , one and the same instance of coon.core.service.GravatarService is returned — which was initially created with the constructor arguments {parameters:{d:"blank"}} :

    // registering the GravatarService with the ServiceLocator
coon.service.ServiceLocator.register(
"coon.core.service.UserImageService",
Ext.create(
"coon.core.service.GravatarService",
{parameters:{d:"blank"}}
)
);

// later on, the GravatarService is returned for calls to:
const imgService = coon.core.ServiceLocator.resolve(
"coon.core.service.UserImageService"
);

Disadvantages of using Service Locators

The service locator is considered to be an anti-pattern. Mark’s arguments are definitely valid. I would not go so far and say that he is wrong — in a world without DI Containers, but with good source code documentation, I’d say it’s an okayish tool for the job, if you can live with the side effects global registries are prone to cause:

Our code must consider the fact that a service is missing. As a possible solution, the ServiceLocator could provide a concrete default (a “Special Case”) of the abstract, where applicabable. On another note, while we can guarantee that services resolved to specific abstracts are of the type of this abstract, the API of those services might not respond to our queries in a way the requesting API can handle the responses properly: Services might become very fine granular with their responses. This could lead to over engineering those services when considering various use cases (our ImageService, for example, might have to provide various binary formats for requested images, or none at all — the src-attribute of an HTML img-tag accepts an URL-string as well as base64-encoded data urls in the form of data:[<mediatype>][;base64],<data>).

Update 05 March 2023

If you feel the Service Locator is too clumsy, you can use an DI-Container for Ext JS, which I've written shortly after I have finished this article: Dependency Injection in JavaScript