As great software engineers we’re aspired to be every day in our daily work, I’ve always used to think how we can build amazing products for our clients to use so it helps to achieve their major goals tenfold. Especially when you have the scalability forethought in mind.
Built to scale as they say in the world of startups and venture capital funding.
That product can be anything from a simple portfolio website for an artists/singer, a basic space invaders game for kids to play online, to building high-grade commercial e-commerce system for thousands, if not millions of online customers to interact and use worldwide, or perhaps build the next Facebook-scaled size social media platform!
These atypical software products we’re so used to building can vary in size. A product can do one or several simple things. Or a product that makes up so many moving parts that are, rightfully so, considered as components that do very complex jobs on its own. Thus the same product is a behemoth size project so you got think how a lone developer is going to meander through the layers of architecture ensuring that all of these components can work with each other in which they primarily function or not.
Thus it brings to my attention on this very important subject matter - using dependency injection as one of your core software design principles.
Before we get into that terminology, let’s figure what dependency injection really means based on this wiki quote.
In software engineering, dependency injection is a technique whereby one object supplies the dependencies of another object. A “dependency” is an object that can be used, for example, a service. Instead of a client specifying which service it will use, something tells the client what service to use. The “injection” refers to the passing of a dependency (a service) into the object (a client) that would use it.
To better explain the above, let’s use the following code setup as a way to demonstrate.
When class A uses some functionality of class B, then it’s said that class A has a dependency of class B.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
Now with the above example here, we have two distinct classes that communicate with one another.
Here, A is dependent on B’s methods, inputs and properties etc to do some important tasks. In the majority of cases, this is fine as it stands if these snippets of code are not poised to be tested or changed frequently in the future.
But more often than not, it’s not always the case so the code needs to evolve. Thus, the problem becomes more apparent when you start to change internal behaviour of class B. If any of the properties of class B is modified such
bar are renamed or its methods signature are modified or removed, then the impact of its changes will affect class A for the whole lot as well because everywhere in A has to be changed as we have hardcoded B’s dependency into A.
If you imagine, should we write software like this for every class that depends on each other for performing certain functions or methods such as A is dependent on B, B is dependent on C, C is dependent on D, D is dependent on E etc, etc. This would become an utter nightmare to maintain and change over time thus doing this way leaves you no room for flexibility of swapping or removing dependencies in between, at will.
Thus, what we have gotten ourselves here is an obvious tight coupling of sub-systems with one other. Thus, in usual cases, having tightly coupled systems does not bode for well future incremental changes smoothly over time. By that time, you, as a software engineer, wanted to make several big modifications to the system, you’re more likely going to have to re-architect the entire system inside out just as you first started building it from scratch - day one!
This is not the ideal situation to be in.
Hence, for a long time, in the tech industry, we have a number of battled-tested industry-standard software patterns that deals with this to make our software grown to scale and change incrementally and progressively. And one of them is Dependency injection (DI).
So the question beckons - why does DI matter here?
The long-winded but simple answer to this question is when you have lots of classes that need to talk to each other (such as above), want to keep your code organised, and make it easy to swap different types of objects in/out of your application that gets to communicate with one another at run time.
DI is the main person that does all the work for you.
The key benefit of using this pattern is to encourage loose coupling. Objects can be added(or removed) and tested independently of other objects because they don’t depend on anything other than what you pass onto them. When using traditional dependencies, to test an object you have to create an environment where all of its dependencies exist and are reachable before you can test it. In DI, you can test an object in isolation by passing mock objects for the ones you don’t need or want to create.
At present, our DI pattern strategies come in three types to choose from.
To illustrate these types, let us use Car analogy for this.
First one is that dependencies are provided through a class constructor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
The second type, as the name suggests, the client exposes a setter method that injector uses to inject the dependency. By having injector we mean the DI guy, as mentioned earlier part of this post. To accomplish this, you need some of a decent injector library that does the work underneath whose main task is to register all the dependencies to one or several external client code and supply them on demand during run time.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
Here we have Car and Engine objects use setter methods in injecting their required dependencies and returns their prototype function methods respectively. Why are we doing this? We’re doing this because our imaginary DI library here looks at all available prototype methods provided by various modules or sub-systems it’s fully aware of after registering them within the environment thus inject dependencies based on the setter injection rules we set up in beginning.
Lastly, this is where the dependency provides an injector method that will inject the dependency into any client passed to it. Client must implement an interface that exposes a setter method that accepts the dependency.
Unfortunately, at the time of writing, there’s no such pattern exists in the JS world as JS itself does not do interfaces compared to other traditional object-oriented programming languages such as Java and C#. JS and any other dynamically-type languages do not require interfaces because they’re ‘replaced’ with late binding/duck typing. For actual examples of interface injection done in Java, it would look something like this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
1 2 3 4 5 6 7 8 9 10 11 12 13
Now, at this point, you may be thinking.
Does DI truly still pose relevance to this modern age of software architecture design moving forth, considering how many JS and NodeJS frameworks are made every day of the week?
The answer is yes and no.
It depends on who and what you’ve heard from the software developer veteran community lately and which programming “tribe” you belong to.
For eg, let’s take on two popular JS frameworks; React and Angular.
Let’s start with Angular cause it’s the easier and most likely candidate that benefits from DI.
By in large, Angular is itself, a DI framework if not just the framework to build client-side apps all the time. Because Di is built-in, Angular has the robust structure of how injectors should allow dependencies to be loaded (or removed) for certain components that require such dependencies’ services. Angular provides a mechanism for components to delegate certain tasks for certain services that they should not have any concerns about other than just actioning optimal user experiences. These delegated services are phrased as injectable service classes.
For some examples, here are the common uses written in Typescript.
1 2 3 4 5 6 7
Somewhere in the app, Logger class is used.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Here, you notice it the way Logger service is injected uses the constructor injection strategy as per our examples earlier.
Not much different concept. The difference is that Angular has its built-in injector that looks at all available instances of dependencies during its boot-up time. These dependencies are created through registration which is called a provider. The way providers get registered can be done in several ways due to the hierarchical setup of Angular injector system, ie it can be provided at root, the module level or the component level etc. Which is the core reason why Angular is extremely attractive to back-end developers written in static languages ie Java/C#/etc for a long while now.
For the simplicity and scope of the post, we use the root level approach for this injection, which is simply.
1 2 3 4 5
That’s DI in the Angular in a nutshell!
Now how about React? Does React have common DI concept to embrace as well?
Apparently… it does not! Why? Because it doesn’t need to.
It just handles dependencies on-demand creation differently.
Thank a look at this small snippet React code:
1 2 3 4 5 6 7 8 9 10 11 12
If you’ve worked with React for some time, we always talk about building components and each component that can be made up of other components through parent-child-sibling hierarchy relationship such as
DateField components etc.
In the object-oriented world, we treat components as objects thus in order for components that depend on one another through the use of constructor and injector setup. But in the world of React, this is not necessarily the case, because you’ll often find that the child component does not always depend on the structure of the parent components such as
List components respectively to perform its necessary duties. Here, we’re saying we have
List component has some data that comes through props. But it does not display that same data as a list of reviews. Rather, it delegates the rendering work to the
Datagrid component. We’ve injected our dependencies through the powers of composition without needing for injectors or constructor/setter injectors like other object-oriented languages do.
The beauty with this setup is that you can easily swap dependency around at different places without worrying too much the consequence of changing the hierarchy order of the dependencies between components.
Thus with the
List above, I can replace the
Datagrid component to something like
1 2 3 4 5 6 7 8
All of these are possible thanks to the powerful concepts such as JSX and props patterns, and many other core React patterns that allows you to encapsulate better dependency management in the React world vs more traditionally object-oriented based system design like Angular/C#/Java etc, etc.
So there you have it, folks!
That’s what Dependency Injection is all about.
In summary, the key long-term benefits of using DI are stressed as follows:
- Reduced dependency creation
- Reduced dependency carrying betwen modules
- Unit testing are easier to accomplish with these indepedent modules
- Encourage code reuse
- Encourage loose coupling system
Especially the last point is very important as the software design always keep changing to scale and evolve nowadays. They rarely ever stay in a fixed hence tight-coupling systems has no place for them.
However, one thing I failed to explain earlier in this post is to find out when do we really need not care about using DI when designing our software architecture.
The simple answer lies in the assumption that how many subsystems or modules we know are going to be frequently changed/swapped/configured upfront during its implementation. If they’re not highly configurable, reusable and unit testing has no place for concern or there’s little need to worry about coupling, then we don’t need to hire DI middle guy to do the work at all. It can just work fine on its own.
And it depends on the type of programming languages you work with intimately as you’re probably aware that DI’s inseparably hugely popular for classic object-oriented statically typed programming languages such as Java and C#. But not very much so for dynamically typed languages such as JS and Python. You can read more about it here on this Stackoverflow link.
On a final note, I’m do not claim myself as a DI/IOC designer nor am I subject matter expert on it. I’m just an ordinary software guy who’s got the knack and avid curiosity about the software world and how everything works from reading forums to build things in my own spare time which allows me to share my knowledge and learnings from my past React/NodeJS/Angular project work that has DI built inside with everyone keen to understand how difficult concepts such as these can fully be grasped with simple use case examples people online can relate with. Software development/engineering with the forethoughts of agile principles is always part of the software craftsman journey that truly never ends. That’s the fun part of it. :)
Till then, Happy Coding!
PS: Useful References that inspired me to write up this post to document my learning process.