Time for injection
2.4 Identifying dependencies for injection
2.4.1 Identifying by string keys
}
Were we to construct them by hand, there would be two possible paths to take. First, for the English email service (the one with the English spellchecker):
Emailer emailer = new Emailer(new EnglishSpellChecker());
emailer.send("Hello!");
Second, the version with French spelling:
Emailer emailer = new Emailer(new FrenchSpellChecker());
emailer.send("Bonjour!");
In both these cases, we want an Emailer but with different dependencies set. Using a simple string identifier such as emailer or a class identifier like Emailer.class is clearly insufficient. As an alternative we might try these approaches:
■ Use a more specific string identifier, for example "englishEmailer" or
"frenchEmailer"
■ Use the class identifier Emailer.class together with a discriminator, for exam-ple, the ordered pair [Emailer.class, "english"]
Existing DI libraries use various methods to solve this problem, usually by falling into one of the two aforementioned paths. Both these options give us an abstract identity over a particular service implementation. Once its object graph has been described, a service implementation is easily identified by this abstract identity (which we refer to as its key).
If you wanted to use one particular implementation instead of another, you’d only need to point the dependent to its key. Keys are also essential when we want to lever-age other features of dependency injectors, such as:
■ Interception —Modifying an object’s behavior
■ Scope —Managing an object’s state
■ Lifecycle —Notifying an object of significant events
Keys provide a binding between a label and the object graph that implements the ser-vice it identifies. The rest of this chapter will focus primarily on the merits and demer-its of approaches to keys and bindings.
2.4.1 Identifying by string keys
A key that explicitly spells out the identity of the service implementation should gen-erally have three properties:
■ It is unique among the set of keys known to an injector; a key must identify only one object graph.
English implementation of SpellChecker
■ It is arbitrary; it must be able to identify particular, arbitrary service implementa-tions that a user has cooked up. In other words, it has to be more flexible than the service name alone.6
■ It is explicit; it must clearly identify the object graph, preferably to the letter of its function.
While these are not hard-and-fast rules, they are good principles to follow. Too often, people dismiss the worth of clear and descriptive keys. Let’s look at real-world examples:
A Set in Java is a data structure that has many implementations. Among other things, the contract of Set disallows duplicate entries. The core Java library ships with the following implementations of Set:
■ java.util.TreeSet—A binary-tree 7 implementation of the Set service
■ java.util.HashSet—A hash-table 8 implementation of the Set service
How should we choose keys for these variants? The names of these implementations give us a good starting point—nominally, "binaryTreeSet" and "hashTableSet."
This certainly isn’t rocket science! These keys are explicit, sufficiently different from one another to be unique, and they clearly identify the behavior of the implementa-tion (either binary-tree or hash-table behavior). While this may seem fairly obvious, it is not often the case. You’d be surprised at how many real-world projects are obfus-cated with unclear, overlapping keys that say little if anything about an object graph. It is as much for your benefit as the developer and maintainer of an application as it is for the dependency injector itself that you follow these guidelines. Lucid, articulate keys are easily identified and self-documenting, and they go a long way toward pre-venting accidental misuse, which can be a real nuisance in big projects.
I also strongly encourage use of namespaces, for example, "set.BinaryTree" and
"set.HashTable," which are nicer to read and comprehend than "binaryTreeSet"
and "hashTableSet." Namespaces are a more elegant and natural nomenclature for your key space and are eminently more readable than strings of grouped capitalized words. An email service with a French bent might be "emailer.French," and its coun-terparts might be "emailer.English," "emailer.Japanese," and so forth.
I am especially fond of namespaces in dependency injection. Their value was imme-diately apparent to me the first time I was on a project with a very large number of XML configuration files. They allowed my team and me to clearly separate services by areas of function and avert the risk of abuse or misapprehension. For instance, data services were prefixed with dao (for Data Access Object) and business services with biz. Helper objects that sat at the periphery of the core business purpose were confined to a util.
6 Recall the example of SpellChecker (the service) being insufficient to distinguish between its English and French implementations.
7 A binary tree is a data structure in which each stored item may have two successors (or children), starting at a single root item.
8 A hash table is a data structure designed to store and look up entries in a single step using a calculated address known as a hash code.
namespace. I can’t emphasize enough how useful this was for us. Consider some of the changes we made in a vast system of objects and dependencies identified solely by string keys (also see figure 2.12):
■ UserDetailsService became dao.User.
■ UserDataUtils became util.Users.
■ DateDataUtils became util.Dates.
■ UserService became biz.Users.
The astute reader will appreciate how much clearer—and more succinct—the latter form is. Of course, there is nothing new or innovative about the concept of namespaces, though one rarely sees it preached in documentation.
Spring and its XML configuration mechanism benefit heavily from this approach.
If we were to declare the variant implementations of a Set, they may look something like this:
<beans ...>
<bean id="set.HashTable" class="java.util.HashSet"/>
<bean id="set.BinaryTree" class="java.util.TreeSet"/>
</beans>
And obtaining these objects from the injector accords to their keys:
BeanFactory injector =
new FileSystemXmlApplicationContext("treesAndHashes.xml");
Set<?> items = (HashSet<?>) injector.getBean("set.HashTable");
For a more complete scenario, consider this pattern for the email service and its two variant spellcheckers (listing 2.10).
<beans>
<bean id="spelling.English" class="EnglishSpellChecker"/>
<bean id="emailer.English" class="Emailer">
Listing 2.10 Variants of an email service using namespaces
UserDetailsService
- Users
UserDataUtils
DateDataUtils UserService
- Dates util
- Users
dao biz
- Users Figure 2.12
Organizing string keys into namespaces
<constructor-arg ref="spelling.English"/>
</bean>
<bean id="spelling.French" class="FrenchSpellChecker"/>
<bean id="emailer.French " class="Emailer">
<constructor-arg ref="spelling.French"/>
</bean>
</beans>
Better yet, listing 2.11 is a more compact, encapsulated version of the same.
<beans ...>
<bean id="emailer.English" class="Emailer">
<constructor-arg><bean class="EnglishSpellChecker"/></constructor-arg>
</bean>
<bean id="emailer.French" class="Emailer">
<constructor-arg><bean class="FrenchSpellChecker"/></constructor-arg>
</bean>
<bean id="emailer.Japanese" class="Emailer">
<constructor-arg><bean class="JapaneseSpellChecker"/></constructor-arg>
</bean>
</beans>
By now, you should be starting to appreciate the value of namespaces, encapsulation, and well-chosen keys. Remember, a well-chosen key is unique, arbitrary, and explicit.
When you must use string keys, choose them wisely. And use namespaces.
To drive the point home, look at the following two setups in pseudocode: one that uses poorly chosen keys and a flat keyspace and a second that conforms to our princi-ples of good key behavior. Listing 2.12 shows an injector configuration with bad keys and no namespaces. Ugly! Wouldn’t you agree?
configure() { personService { ... } personDataService { ... } personnelSoapService { ... } userAuth { ... } userAuthz { ... } userService { ... } database { ... } app {
app = new Application() ...
} }
Listing 2.11 A compact form of listing 2.10
Listing 2.12 Poorly chosen keys for services (in pseudocode configuration)
Start configuration Services for personnel management
Security services
Wiring logic goes here
Now compare this with listing 2.13 (and figure 2.13), which presents an improved ver-sion of the same configuration. Much better!
configure() {
personnelService = injector.biz.personnel personnelDao = injector.data.personnel }
String keys are flexible, and if chosen well they work well. But well chosen or not, string keys have limitations that can be confounding and rather, well, limiting! Let’s talk about some of them.
Listing 2.13 Good-practice version of listing 2.12
Services organized into biz namespace
Data objects are in data namespace
Security object keys named lucidly
Get and use instances from injector
Figure 2.13 Services with similar names organized clearly by namespace