DEV Community

Cover image for Inversion of Control: Service Locator in Typescript
Cristian-Florin Calina for This is Angular

Posted on • Edited on

Inversion of Control: Service Locator in Typescript

I've been working with Angular for a while now (over 2 years), and since I moved to another workplace, I've switched (mostly) to Stencil.

Stencil is a very nice compiler for Typescript that generates web components. If I were to place it, it would be a mixture between Angular and React. It uses decorators like Angular does (for the compiler to collect metadata and generate the output code based on that), and jsx/tsx with lifecycles just like React. It is a very powerfool tool to generate design systems.

In this design system that I've been working on, there was the problem of how to store the global state. I wanted to have a similar version to Inversion of Control (since I really liked it in Angular). The big advantage of using a IoC system is that you can easily mock the services in tests, instead of needing to overwrite the actual imports in jest (overwriting is fine, but on complicated setups where you want different configurations between iterations, it would get messy because if there is any reference to the module before it is mocked, the mock will not work).

So I came up with a simple solution for a global injectable service that did not care about the actual implementation.


First step would be to think about what the service that you want to write will do. Based on that, you can declare an interface with all the public methods of the service. Let's consider a simple service that sets and gets user information.

Then we would need to declare a UserServiceInstanceResolver static class that will receive an instance of a class implementation of this interface, and store it in order to be able to access it.

Last but not least, we can implement the actual service

And finally, whenever you want to actually instantiate the singleton service, you can just do it as such:

And the actual benefit, whenever you want to test any other component that uses this instance, you can easily instantiate a mock of the app on UserServiceInstanceResolver.Instantiate method.

This was all that I actually needed (just a root level global instance). But this code can be extended to support as well a per component instance (or hierarchy instance) and get a closer experience to Angular's DI.


Thank you for reading my article. If you enjoyed it, please hit that heart button so others can see it.


LE:
After seeing some of the comments, I agree that this is not the best pattern to use, and maybe you should use existing solutions from the implementors of typescript such as tsyringe or even decoration-ioc (which is the extraction of implementation of DI in VSCode). One of the reasons I like writing articles when I explore new ideas is that I can get instant feedback and learn new things so that I can improve those solutions, so thank you for that !

Top comments (15)

Collapse
 
mariocalin profile image
Mario • Edited

This is more of a Service locator pattern than dependency injection pattern. You are not "injecting" anything really.

Collapse
 
cristianflorincalina profile image
Cristian-Florin Calina

Correct ! I will edit it, since you are right

Collapse
 
kralphs profile image
Kevin Ralphs

Just use TSyringe. You'll have a full IoC container that will feel like Angular's DI system. That will allow you to ditch having IUserServiceModel as a class and leave it an interface by using the Inject decorator.

If you were to stick with this method, I recommend marking IUserServiceModel as an abstract class with abstract methods that you want to force subclasses to define.

And maybe it's something Stencil needs or you just trying to make the code feel fuller, but it looks like you're writing Java style code in Typescript.

Collapse
 
cristianflorincalina profile image
Cristian-Florin Calina • Edited

actually that's my bad in the gist that I put it as a class, I corrected it now. It should be an interface. I'll try out TSyringe (but idk how well it works with stencil which has it's own typescript compiler) since I do like DI better than a Service Locator, thank you for the suggestion !

Collapse
 
click2install profile image
click2install

I would just use a container. Newing up a concrete to pass to a resolver to maintain a singleton reference is a lot of indirection for no real benefit over a simpler singleton or container resolution, or both. At least your resolver could be generic but passing a concrete to it could be managed a lot cleaner and simpler.

Collapse
 
cristianflorincalina profile image
Cristian-Florin Calina • Edited

The benefit is the abstractization from implementation. You don't care about the service, you care about the interface being respected (in the components that need the implementation, you import the locator which doesn't have any actual imports to the implementation, just the interface, so no need to mock the import in tests). Can you give me an example of how to simplify the singleton ?

By container resolution you mean the TSyringe container?

Collapse
 
click2install profile image
click2install • Edited

Yes, a DI container like TSyringe or one of the many others. A Service Locator in most situations is considerd an anti-pattern. You're effectively newing up a service so you can have a resolver (which should be generic) maintain a singleton instance. A container will do that for you without having to new anything up.

Similarly, if you implememt a singleton you can just use that and mock the interface. A simpler singleton would maintain it's own single instance an not have to be newed up to pass to a resolver (that you wouldn't need) which returns that instance.

The abstraction you mention is primarily coming from your resolver which isn't required. You already have an interface against your service.

Thread Thread
 
cristianflorincalina profile image
Cristian-Florin Calina • Edited

ok, maybe I don't fully understand, but having the singleton maintain it's own instance wouldn't mean it's imported wherever it is used? So you would have the actual implementation coming up in the import? That means you would be needing to mock the instance on it, but you still need to mock a file import, right?

But I see the point of a container, I will update the conclusion to specify that there are libraries that can do that better such as tsyringe

Thread Thread
 
click2install profile image
click2install

That's correct the singleton instance is cached by the module. If you use a typical singleton implementation, which you generally want to avoid, you would mock the import against the interface - which is technically no different to what you're doing now. You're currently newing up either a concrete or a mock and having your resolver return that instance. Your resolver is a minimally specific container-like structure that is resolving what you pass it. The benefit of using a DI container, as the peferred approach, is its more generic, adheres SOLID and you can specify lifetime.

Collapse
 
stradivario profile image
Kristiqn Tachev

Definetely you should give it a try with @rhtml/di

I tried to make the smallest dependency injection library written in typescript.

Transpiled it is less than 3 kb

github.com/r-html/rhtml/tree/maste...

Great jkb for the article :)

Collapse
 
nausaf profile image
nausaf • Edited

Hi Cristian,

Service Locator, as opposed to Dependency injection, is considered to be an antipattern by many.

My opinion is that it is conceptually easy to understand because we think we can just replace a call to constructor with a call to service locator in the body of our code but still retain the ability to change the implementation (in config, at app startup or perhaps even dynamically, or in tests which seems to be your motivation).

What tends to happen in practice is that if a class/function B is sufficiently decoupled from a piece of code A that it is resolved from an IoC container, it will probabaly also need to be mocked out in unit tests of A. This is where service locator leads to problems:

  1. You have to look into the guts of A to see what will need to be mocked out instead of it being advertised in A's constructor if using Dependency Injection.

    This goes contrary to the idea of test-driven development and that fact that we typically write (or perhaps should write) tests that treat the unit-under-tests as a black box with only its interface visible (include JSDoc comments) and not its implementation. This decoupling of test from internals of code-under-test leads to both ease of refactoring and less brittle tests.

  2. It is easier, and probabaly faster in terms of test execution time, to just create mocks and stubs and then inject them into a unit-under-tests (as you would with Dependency Injection) than to instantiate a full IoC container, put the mocks there, and make sure it's available to the unit-under tests (which is what Service Locator forces you to do).

This is my view anyway. Thanks for the post though. There's some cool stuff there and I really want to look at Stencil for generating design systems.

Collapse
 
cristianflorincalina profile image
Cristian-Florin Calina • Edited

I agree with 1. but on 2. I don't see how it's more difficult to use the service locator and add the mocked instances to it. I think it takes as many lines of code as it does to set the instance to the container for DI. I don't know how the speed is measured as well, it's an assumption that it is faster, if you have any performance measurements between the approaches I'd like to see them.

I added to my article as well that DI is a better approach than this, this is just simpler to implement and easier to understand and get you into IoC (since I assume that most JS FE devs don't know much about IoC solutions -- except for Angular). Writing this article helped me learn a lot as well from all the comments that I've received so thank you as well for contributing to that !

Collapse
 
ecyrbe profile image
ecyrbe

Where is inversion of control ? This is the opposite, you are using singleton pattern.

Collapse
 
cristianflorincalina profile image
Cristian-Florin Calina

It's more like a Service locator pattern, but yea, using singleton

Collapse
 
itechthemeg profile image
itechthemeg

Right