Design Patters Considered Dangerous
I recently played a bit with AngularJS, following the angular tutorial that can be found here:
https://docs.angularjs.org/tutorial/step_07.
Angular makes a good first impression.
I liked the way how you could easily embed JSON into the HTML template, making it a client side templating language. I horribly hacked up a few similar tools myself for a few projects, so it was nice to see someone had actually put in the effort to create a proper one. Midway through the tutorial, I noted that more and more boilerplate code crept into the examples.
I am talking of fragments like these:
angular.
module('phoneList').
component('phoneList', {
templateUrl: 'phone-list/phone-list.template.html',
controller: function PhoneListController($http) {
var self = this;
self.orderProp = 'age';
$http.get('phones/phones.json').then(function(response) {
self.phones = response.data;
});
}
});
At this point, I get the feeling the tutorial writer made a rather simple example awfully complicated, all in order to make a point I really do not get. There is a wonderful rant by Rob Ashton against [AngularJS]
(
http://codeofrob.com/entries/you-have-ruined-javascript.html
)
that voices most of my misgivings.
Ever since the Design Patterns book came out, design patterns have taken over the world by storm, obfuscating code one abstraction layer at a time.
However, I do not want to rant here, but try a more dispassionate exploration of these design patterns and dependency injection in particular.
First I will try to summarise the reasons the angular designers feel they needed to introduce this design pattern. Second, I will explore some of the use cases for which dependency injection is deemed necessary. Third, I evaluate some criteria by which we can judge whether a particular design pattern may be justified.
So, why do the angular designers feel they need to introduce this design pattern? Their motivation (from the angular documentation):
There are only three ways a component (object or function) can get a hold of its dependencies:
- The component can create the dependency, typically using the new operator
- The component can look up the dependency, by referring to a global variable
- The component can have the dependency passed to it where it is needed.
The first two options of creating or looking up dependencies are not optimal because they hard code the dependency to the component. This makes it difficult, if not impossible, to modify the dependencies. This is especially problematic in tests, where it is often desirable to provide mock dependencies for test isolation.
The third option is the most viable, since it removes the responsibility of locating the dependency from the component. The dependency is simply handed to the component.
So the reason dependency injection is needed is because it avoids hard-coding the dependency. They also immediately give a use case, in testing it is “often desirable to provide mock dependencies". This was also about the only valid use case I could find. Most other use cases provided seemed rather like examples designed to specifically to illustrate the blessings of dependency injection and not something that would come up in real life (in Rob Ashton’s terms, “straight from la la land" ).
So, is this use case sufficient to introduce this design pattern? Design patterns have their own disadvantages. To quote a few from the Wikipedia article. They make the code more difficult to read, they increase abstractions and ironically, they introduce new dependencies on the dependency framework that is newly introduced.
How can we evaluate the usage of a particular design pattern? When do the advantages outweigh the disadvantages. It is not possible to provide a generic answer to such a question. Instead we have to weigh this against the concrete particulars of the case at hand.
However, there are some common criteria or questions to evaluate a particular case.
Is it possible to postpone the design pattern until it is actually needed? Hard coding dependencies are really just fine until you really need to change them.
Does the design pattern impose a mandatory framework (i.e. does it introduce a new dependency into the code)?
How much harder does it make the base case as compared to simplification of the special case. If it makes the base case much more complicated at the expense of only a minor simplification of special case, then the design pattern has less justification.
Are there simpler alternatives available?
So let’s answer these questions one by one for the Angular Framework
No, the design pattern is not mandatory. It is perfectly possible to ignore it and ship working code. However, one strongly gets the impression that even though it is not syntactically required, programmers who follow the earlier tutorial style will get laughed out of the office (“you never completed the tutorial, did you?").
Once the design pattern is introduced it does require the very specific angular framework to function, which a programmer will have to learn before they can use it.
For the particular use case presented, (stubbing the test method). It makes the base case much more complicated.
There are more simple methods of achieving the same goal.
The dependency could be placed in a configuration file or a factory object and the configuration file could be made global. For languages that support dynamic class loading, this can be very flexible method.
def __init__(self, context):
database = context.get_database()
In many cases, there is an even simpler solution in just using default arguments: E.g.
def __init__(self, database = None):
self.database = database or MyDatabase()
Although this also introduces some boilerplate code, the abstraction level of both solutions is much lower than the heavy handed framework style approach of the angular documentation.
So in conclusion, the dependency injection in the Angular tutorial does not appear to be worth it. The disadvantages of the solution outweigh the concrete advantages, especially since this is a tutorial, targeted at developers unfamiliar with AngularJS. While evaluating the specific example mentioned in the tutorial, I established a few common criteria that can be used to weigh the advantages of a particular design pattern against its disadvantages.