What's up geeks and welcome to the channel!
If you're a software developer or you're studying a programming language
you may have heard of: Dependency Injection. Personally, at first, I thought this must
be very hard and maybe you did as well! But I assure you it's just a fancy term
used to represent a very easy concept. You see, in any object-oriented language classes and objects are the foundations of
any functionality you can think of. Now, the relationships between these classes
and objects like composition or inheritance make it possible to extend and
re-use some of these functionalities. However, there's a catch, the way that we choose
to build those relationships, those dependencies, determine how decoupled and
reusable our code will be, not only in terms of our actual
code, but also in terms of testing. So, suppose we have this Food interface
implemented by the Burger class, and well these two are
being used by the Chef class which has a single method called prepareFood.
In this simple example we can notice that the Chef class is dependent on the Burger class
as without initializing it in the constructor the Chef cannot prepare anything.
Now, imagine another Chef shows up, but this one specializes in pizzas.
So, we created a Pizza class to account for this and made it implement the Food interface.
In the current implementation we have, to include Pizzas in the prepareFood method we will
either have to get rid of the burger option permanently and stick with pizzas,
or we will have to duplicate the code we have, get rid of the method we wrote
entirely, and create two methods, one for each food type we are
trying to prepare in the chef class. Wait! Actually, some might even say we can have
multiple Chef classes that extend the initial one where each one specializes
in a particular type of food. Well, you kind of get where I'm going with
this, all of these solutions are pretty bad, and the best thing we can do
here is pass a food argument to this chef object while
it is being instantiated. So, if we create a chef object
while passing to it a burger object then we have a chef that
specializes in burgers and so on... What we did here is passing the dependency
to the constructor of the dependent object while it is being instantiated,
this is called: Dependency Injection. This dependency was injected into the
object instead of being created inside it. We decoupled the construction of the Chef class
from the construction of its dependencies, the Food class in this example.
Previously, when we created a Chef class, a food object was being instantiated automatically
in its constructor because we needed it, and if a Java class creates an instance
of another class via the new operator it cannot be used and tested
independently from this class, it becomes tied to this class
and open for modification, and this is called a hard dependency.
However, when we used dependency injection, the dependent class was no longer a concern
to us as it is provided from the outside, this way if the object's
implementation changes in the future, it is no longer the dependent class responsibility
to figure out what actually changed. Okay, with that said you need to know that there
are actually three types of dependency injection: the Constructor Injection,
which is the one we just saw, the Setter Injection, and the Field
Injection, let's break them down. In the constructor injection the dependencies
are provided through a class constructor. In the example you see, we provided the
class we depend on via the constructor while initializing our main object
this type of injection is the most recommended and we'll see why in a bit.
For the setter injection the client exposes a setter method that
the injector uses to inject the dependency. So, instead of having the
dependency as a constructor parameter we will have a setter to pass it.
In this example, to create our object we made use of the default constructor
and after that we used our setter to provide that class its dependency.
However, this approach is not really recommended, because you see by doing
that we hided that dependency and by reading the constructor
signature or creating our main object we cannot identify that there is a
dependency right away which might cause a NullPointerException at runtime.
And finally, there is a third way to inject dependencies in Java and
it is called field injection. The only way for field injection to
work is either directly mutate the field because it's a non-private and non-final field,
or modify a final/private field using reflection. This approach has the same problem we
discussed in the setter injection approach, and additionally it adds complexity due
to the mutation or reflection required. Okay, now that we know what is dependency
injection and how to inject our dependencies, let's see why do we even want to do that?
Why would you want to apply dependency injection? Well to answer this question we will have to
take a look at the concept behind dependency injection called: Inversion of Control.
Inversion of control is a principle in software engineering which transfers the
control of objects or portions of a program to a container or framework.
You see, in contrast with traditional programming in which our custom code makes
calls to a library where reusable code sits, inversion of control enables a framework
to take control over the flow of a program and make calls to our custom code.
To enable this, frameworks use abstractions. So, if we want to add our own behavior
we need to extend the classes of the framework or plug-in our own classes.
This is also reflected in the fifth principle of SOLID which are the five basic principles
of object oriented programming and design. This principle states that: A class should
depend on abstractions and not on concretions. And that is exactly what we did when we injected
the Food class to the Chef class if you recall. With a hard dependent code,
our Chef class was dependent on the implementation or the Burger class,
however when we used dependency injection we became dependent on the abstraction or
the interface instead of the concretion. You see a class should concentrate
on fulfilling its responsibilities and not on creating objects that are
required to fulfill those responsibilities and that's where dependency
injection comes into play, it provides the class with these required objects. By doing this you'll be reducing
boilerplate and duplicate code as the initialization of the dependencies
is done by the injector component. Additionally extending the application
and its functionality becomes easier, your classes are now way more open for
extension and closed for modification. Moreover, you will find greater
ease in testing your program because your dependencies can
now be isolated and mocked allowing components to
communicate through contracts. Now, given all of this I would like to point that dependency injection is a
solution to certain problems, so before applying it start by asking yourself
if you even have a problem in the first place. If not then using it will most
likely make the code worse. You have to consider first if you can reduce or
eliminate dependencies or if mocking will really facilitate your testing or if it will obstruct it.
As an example suppose you have a class A which uses another class B.
Internally B is only used by A and therefore fully encapsulated and can
be considered an implementation detail, if you change this to inject
B into the constructor of A, then you have exposed this implementation
detail and B has to exist in some other place in the system separately from A
leading to an overall worse architecture with leaking concerns.
Another problem you may encounter is while testing.
Suppose you have a class B injected into the constructor of A and while testing A you mocked B.
Now, even though your tests are passing, you will never be fully sure
that the actual production code of A will work with the actual production
code of B because all you used was a mock. So, my last words on this topic
are that dependency injection should be used whenever you deem it necessary
as sometimes you might find yourself adding complexity to your code instead of simplifying it.
And that's it for this video, I hope it was helpful thank you guys for watching take
care, and I will see you in the next one!