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)
This is more of a Service locator pattern than dependency injection pattern. You are not "injecting" anything really.
Correct ! I will edit it, since you are right
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.
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 !
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.
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?
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.
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
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.
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 :)
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:
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.
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.
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 !
Where is inversion of control ? This is the opposite, you are using singleton pattern.
It's more like a Service locator pattern, but yea, using singleton
Right