The point of DI is to make the design of an application flexible, to allow it to adapt to future changes as they happen. Figure one is an example of how I coded some JavaScript to create an use an instance of an XMLHttpRequest object to use for AJAX:
function listener () {
@tab;console.log(this.responseText);
}
var request = new XMLHttpRequest();
request.onload = listener;
request.open("get", "data.txt", true);
request.send();
@tab;console.log(this.responseText);
}
var request = new XMLHttpRequest();
request.onload = listener;
request.open("get", "data.txt", true);
request.send();
My focus is not on how XMLHttpRequest is used, you can research that another time. My problem is what happens if someone tells me that we need to use a different object for AJAX? I will have to go back into that code and change what it does. That is dangerous. I might mess something else up and never notice until someone calls the help desk. Changing my code violates the "O" in Uncle Bob's principles: open for extension, closed for modification. The bottom line is that I cannot protect everything from change, but I sure want to try.
It sounds complicated, but it is not. The task is to identify any thing that may change. The solution is to let someone else make the decision about what we need to use, in this case XMLHttpRequest, and then give it to us. Giving it to us is the "injection" part. Now it is their problem if it changes. We simply use whatever they give us! The rule is that whatever we get has to use the same interface we expect; anything that looks like an XMLHttpRequest object should work for us. We are looking for an abstraction, we expect an interface without giving a hoot about the concrete object that provides it. And that is a major piece of what object-oriented programming is all about!
In an Angular application there are several items we can immediately identify as things we want someone else to make the decision about. The $scope a controller uses is one, because it needs to inherit from other scopes or we may need to share it with other controllers. Better to let someone else decide when and how to create a new scope. Don't do it in our controller. Another is the $http used for AJAX. If we let the framework decide what $http is then it isn't our problem when it has to adapt to a different implementation.
So we let Angular provide us with the $scope object that will contain the model elements shared between the controller and the view, and the $http object the controller can use for an AJAX request. The best thing is that we do not need to implement the object-creation-decision-making process: the Angular framework does it for us!
We build controllers using the "controller" method of the application object. We need to create the instance of the application object first. Then we use the controller method to define what the name is, and a function that will be used to create a "controller" object. In this example, the controller function is anonymous:
var myapp = angular.module('myapp', []);
myapp.controller('maincontrollers', function ($scope, $http) {
@tab;$http.get('cloudserver/data').success(function(data) {
@tab;@tab;$scope.mydata = data;
@tab;});
@tab;$scope.dataorder = 'ascending'; });
myapp.controller('maincontrollers', function ($scope, $http) {
@tab;$http.get('cloudserver/data').success(function(data) {
@tab;@tab;$scope.mydata = data;
@tab;});
@tab;$scope.dataorder = 'ascending'; });
To create the controller instance the framework will call this function. The "injection" happens when the framework passes in objects as the $scope and $http parameters in that function call. When I first encountered Angular examples I mistakenly assumed that the parameter names were irrelevant, that the parameters were position dependent. I was wrong.
There are a wide variety of objects the framework can build for us and inject into the new controller, so many in fact that we do not always want define them all just so the ones we want are in the correct position. To simplify this the Angular framework reads the parameter names and then inserts the desired object into each parameter. I could have just as easily declare it this way and the controller will still work:
var myapp = angular.module('myapp', []);
myapp.controller('maincontroller', function ($http, $scope) {
@tab;$http.get('cloudserver/data').success(function(data) {
@tab;@tab;$scope.mydata = data;
@tab;});
@tab;$scope.dataorder = 'ascending'; });
myapp.controller('maincontroller', function ($http, $scope) {
@tab;$http.get('cloudserver/data').success(function(data) {
@tab;@tab;$scope.mydata = data;
@tab;});
@tab;$scope.dataorder = 'ascending'; });
Of course this isn't the only place that Angular relies on dependency injection. In fact, you can even define your own objects and have Angular inject them when they are required. It works pretty much the same everywhere Angular uses it.
And that leaves us with one other problem: if the Angular framework looks at the parameter names to handle dependency injection, then what happens when we use a minification tool on our script that obfuscates those names?
Whoops! That is going to be a problem. Fortunately the framework gives us two ways to deal with that; both of them map the injected object names to the parameters in the function. The first way adds a $inject property to the function object, but in order to do that we need to define the function before we pass it to the application in the controller method. The $inject property is an array of the names of the objects we need injected, and maps by position to the parameters in the function call:
var myapp = angular.module('myapp', []);
var mycontroller = function (cloud, model) {
@tab;cloud.get('cloudserver/data').success(function(data) {
@tab;@tab;model.mydata = data;
@tab;});
@tab;model.dataorder = 'ascending'; });
mycontroller.$inject = [ '$http', '$scope' ];
myapp.controller('maincontroller', mycontroller);
var mycontroller = function (cloud, model) {
@tab;cloud.get('cloudserver/data').success(function(data) {
@tab;@tab;model.mydata = data;
@tab;});
@tab;model.dataorder = 'ascending'; });
mycontroller.$inject = [ '$http', '$scope' ];
myapp.controller('maincontroller', mycontroller);
We changed the names of the function parameters just to show that it works. And by the way, the Angular framework uses dollar signs in front of its identifier names. To keeps things organized, never put a dollar sign in front of an identifier that we declare. Then we know what is Angular's and what is ours.
The second way to define the injection is the more common one: pass an array as the second parameter to the controller method. The array has the names of the objects we need injected (mapping to the parameters in order), and the last element is the controller function itself:
var myapp = angular.module('myapp', []);
myapp.controller('maincontroller', [ '$http', '$scope', function (cloud, model) {
@tab;cloud.get('cloudserver/data').success(function(data) {
@tab;@tab;model.mydata = data;
@tab;});
@tab;model.dataorder = 'ascending'; }]);
myapp.controller('maincontroller', [ '$http', '$scope', function (cloud, model) {
@tab;cloud.get('cloudserver/data').success(function(data) {
@tab;@tab;model.mydata = data;
@tab;});
@tab;model.dataorder = 'ascending'; }]);
Remember, the only magic in Angular's dependency injection is that we let the framework decide when and how to create the objects. We just rely on them being passed in to our code when we declare that we need them!
But wait, I want to know how Angular makes dependency injection work!
OK, what goes on behind the scenes in the framework really is not that difficult. Remember, the framework does its job automatically, we do not have to worry about it. It is just JavaScript...The key is $injector, a component that runs as part of the framework. The $injector is part of a module, and we create at least one module for every Angular application. Every resource that can be injected must be registered so that the $injector can find it: $scope, $http, $log, etc. $scope is a value, and $http and $log are services predefined by Angular.
You can register your own resources (values or services) in your application module, or any other Angular module that you create. I will not go into the details here but you can look for the documentation at http://angularjs.org for the value and factory "recipes". Really they are methods on an Angular module. Once resources are defined they can be injected whenever a controller (or something else) declares it needs them. There can be exactly one resource with any given name:
So there is exactly one $scope value and one $http service that will be provided when the controller declares that it needs them. The controller function object we defined is handed to the $injector, and it is $injector.invoke() method that executes our function to build the new controller object. To do that the invoke method uses a three-step process.
First the invoke method could be passed an array that contains the names of the resources that need to be injected, and the function to execute as the last argument. It looks just like the third form of defining a controller that we looked at above, in fact the invoke method is simply passed that array argument.
If that isn't the form, then the invoke method looks in the function object for the $inject field to get the map of required resources.
Finally if the array is not passed and the $inject field is not prsent, then the invoke method gets complicated. It plays on the fact that when you call the toString() method on a function object what you get is a the original text of the function. So, it uses toString() to peek at the original code and see if the parameter names match the names of registered resources, and then injects those resources.
As we talked about before, minification will wreak havoc on the parameter names and then you need to look at the workarounds we defined. It is better to always use the inline form to declare what resources need to be injected.
So we have cover two pieces here. First we looked at why we need dependency injection and how to use it in the Angular framework. Then just for kicks we took a look at the pieces behind the scenes that make it work, and exposed the fact that you can register your own values and services and cause them to be injected alongside the predefined ones from Angular. Enjoy!
The Creative Commons license requires that I mention the Angular logo above, which has been modified with a surgical mask, is available from the Angular repository at https://github.com/angular/angular.js/tree/master/images/logo.
References
See the references page.
No comments:
Post a Comment