If you've ever filled out a tax return, the concept of Dependency Injection should be fairly easy to comprehend, even if the semantics are vague.
On US tax forms, there's a section to fill out for "dependents", people (usually children) that rely upon the tax payer for their own livelihood. This is an excellent example of how dependency injection works and why it's so important to use in our applications.
Dependency Injection (DI) is simply when one object supplies the dependencies of another object, and a dependency is just an object that can be utilized throughout an app.
CLASSES
There are three very important parts classes in the DI pattern. The service object, to be used, the client object that depends on the service, and the injector, which is responsible for constructing the service and injecting it into the client.
By creating objects directly within the class that requires the objects, our code becomes tightly-coupled and inflexible. Because the class is dependent on specific objects, it becomes impossible to change any instantiation later without having to refactor the entire class.
The class then cannot be reused if other objects are required, finally making it hard to test because of it's heavy reliance on real objects that cannot be mocked.
DI provides a solution to these many issues by creating objects, understanding which classes require those objects, and then providing those classes with the objects.
Wikipedia provides a helpful analogy:
"Service - an electric, gas, hybrid, or diesel car
Client - a driver who uses the car the same way regardless of the engine
Injector - the parent who bought the kid the car and decided which kind"
Why DI
Stay DRY
By following the principles of DI we effectively can reduce boilerplate code since all work to structure and initialize the dependencies is handled in a single component.
Separation of Concerns
One of the most important pros of using DI is the separation of concerns when building components. The biggest trade off here would be that DI can make code difficult to trace because it separates client behavior from construction. However, with good variable and file names, module co-ordination will be far easier to navigate than one file with hundreds to thousands of lines of code.
Parallel Programming & Unit Testing
When components create their own dependencies, there is no way to effective way to test their functionality. In the example below, the Coupe object depends on both the Engine and Wheels Object, while the Engine object is dependent on the Pistons object. This is called "Tightly Coupled Code", and can become very problematic.
function Wheels() {
this.action = () => log("We're rollin' now!");
}
function Pistons() {
this.action = () => log("Pew! Pew! Pew!");
}
function Engine() {
this.pistons = new Pistons();
this.action = () => {
this.pistons.action();
};
log("Hummmmmmmm.");
}
function Coupe() {
this.wheels = new Wheels();
this.engine = new Engine();
this.action = () => {
this.wheels.action();
this.engine.action();
log("Nice car lady!");
};
log("Fresh off the lot.");
}
Should any of Pistons object happen to fail, both the Engine and the Coupe would fall apart, this can lead to spaghetti code and hard to find bugs.
The solution is to loosen the couplings of the code and pass the dependency as a parameter to the client object.
function Engine(pistons) {
this.pistons = pistons;
this.action = () => {
this.pistons.action();
log("Hummmmmmm.");
};
log("Fresh off the lot.");
}
function Car(wheels, engine) {
this.wheels = wheels;
this.engine = engine;
this.action = () => {
this.wheels.action();
this.engine.action();
log("Nice car lady!");
}
log("Fresh off the lot.");
While the code above is a better method, it can still lead to coupling and scope issues. To truly separate our concerns, the best way is to isolate them completely, and keep our clients from knowing anything about eachother, which can be achieved with Inversion of Control.
Inversion of Control (IoC)
DI is a broad technique used to achieve IoC, which a concept that operates on the belief that an object should not configure its dependencies statically, but should be configured by some other class externally.
To make it relatable, let's say you're going on vacation and plan to stay at a nice hotel or resort. It's not your responsibility to bring towels, sheets, the phone in the room etc.
The hotel provides the amenities so you can do the one thing you came to do: enjoy your vacation. IoC follows this same principle to decouple our code, by isolating the dependency and then passing it to client objects on demand. This method maintains those important connections between service and client objects, and effectively extend the app's functionality.
Containers
The use of a service within a DI or 'unity' container is popular in many frameworks, and Angular JS utilizes this method extensively. This method effectively implements IoC by separating the construction concerns and use concerns of this video player app by creating a service, and then passing it to the instance, which uses it throughout the app as props.
angular.module('video-player')
//create a new service to act as an DI container
.service('youTube', function ($http) {
this.search = function (query, cb) {
//utilize the $http object
return $http({
//specify the request method
method: 'GET',
//request the proper endpoint
url: 'https://www.googleapis.com/youtube/v3/search',
//params required for angularjs https requests
params: {
key: `${YOUTUBE_API_KEY}`,
q: `${query}`,
part: 'snippet',
maxResults: 5,
type: 'video',
videoEmbeddable: true,
}
}).then(function successCB(response) {
console.log(response, 'here's that video you asked for!');
cb(response.data.items);
}).catch(function errorCB(response) {
console.error(response, 'sorry, couldn't get that video for you');
});
};
});
DI Containers are so useful because dependencies are defined in a single place instead of throughout your app, and the components that depend on them can easily request data from them without knowledge of any other dependents.
angular.module('video-player')
.component('app', {
//pass the youTube service to the app controller to pass down it's response
controller: function (youTube) {
//mock videos
this.videos = window.exampleVideoData;
//this selects the first video from the response
this.currentVideo = window.exampleVideoData[0];
//onSearched will call the youtube search function on a queried string
this.onSearched = (query) => {
//the youtube search takes a query and a callback
youTube.search(query, this.afterSearch);
//console.log('video queried!');
};
//helper function to re-render the video player and list to the updated queried item's top 5 results
this.afterSearch = (videos) => {
//reassign the queried video results to the videos attribute
this.videos = videos;
//reassign the first video to the current video in the player
this.currentVideo = videos[0];
};
//select video will grab the index from the array and render selected video to the main player
this.selectVideo = (index) => {
//updates the current video to the plucked index video;
this.currentVideo = this.videos[index];
};
},
templateUrl: 'src/templates/app.html'
});
In Conclusion
allow us to stay DRY, separate our concerns, extends and uncouples our code while still effectively passing data to our client objects that need them. Use the DI principle whenever you want to implement parallel programming in your code and look forward to better and more successful unit testing in your next application!
Thanks for reading!
Top comments (0)