This chapter covers: ■ Using dependency injection
2.4 Identifying dependencies for injection
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
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
Now compare this with listing 2.13 (and figure 2.13), which presents an improved ver- sion of the same configuration. Much better!
configure() {
define namespace :biz { biz.personnel { ... } biz.user
}
define namespace :data { data.database { ... } data.personnel { ... } }
define namespace :soap {
soap.personnel { ... } } define namespace { security.authentication { ... } security.authorization { ... } } } main { 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 personService - authentication - authorization userService personDataService userAuth security - database - personnel data biz - personnel - user personnelSoapService userAuthz database - personnel soap
Figure 2.13 Services with similar names organized clearly by namespace