• No results found

Combinatorial keys: a comprehensive solution

This chapter covers: ■ Using dependency injection

2.4 Identifying dependencies for injection

2.4.5 Combinatorial keys: a comprehensive solution

Earlier, I described a sample combinatorial key for an English variant of our favorite email service; it looked like this:

[Emailer.class, "english"]

The ordered pair [Emailer.class, "english"] is one case of a combinatorial key consisting of a type key and a string key. The type key identifies the service that depen- dents rely on, in a safe and consistent manner. Its counterpart, the string key, identi- fies the specific variant that the key is bound to but does so in an abstract manner. Comparing this combinatorial key with a plain string key, the latter clearly has the advantage of type safety. Its second part (the string key) provides a clear advantage over plain type keys because it is able to distinguish between implementations. Let’s examine this with a couple of examples:

Key 1: [Emailer.class, "english"] Key 2: [Emailer.class, "french"]

Key X: [Book.class, "dependencyInjection"] Key Y: [Book.class, "harryPotter"]

Keys 1 and 2 identify object graphs of an email service with English and French spellchecking, respectively. Keys X and Y identify two different books.

Another example we’ve used before is an emailer that depends on a SpellChecker:

[Emailer.class, "withSpellChecker"]

Consider a more complex take on this example, where our emailer depends on a SpellChecker, which itself depends on a dictionary to resolve word corrections (as figure 2.17 models).

The combinatorial key identifying this object graph might look something like:

[Emailer.class, "withSpellCheckerAndDictionary"] Even complex object graphs with many permutations are identified easily with combinatorial keys. They also com- ply with our rules for good key behavior:

Emailer

SpellChecker

- spellChecker

Dictionary

- dictionary

Figure 2.17 A class model of two levels of dependencies below emailer

Keys are unique. The second part to the ordered pair ensures that two imple-

mentations are identified uniquely.

Keys are arbitrary. While the dependency is identified by the first part, any spe-

cific behavior exhibited by a custom variant is easily described by the second part of the key.

Keys are explicit. More so than with pure string keys, combinatorial keys are

able to identify both the service contract they exhibit and any specific variation to the letter of its function.

So, combinatorial keys are safe, explicit, and able to identify any service variants clearly and precisely. They solve the problems of type-safety that pure string keys can’t, and they ease the rigidity of pure type keys. And they do so in an elegant manner. The only drawback is that we have a string part that incurs the problems of misspelling and misuse identified earlier. For example, it is quite easy to see how the following combi- natorial keys, relatively safe though they are, can still end up problematic:

[Emailer.class, "englihs"] [Emailer.class, "English"] [Emailer.class, "francais"] [Emailer.class, "withFaser"] [Emailer.class, "with.SpellChecker"] [Emailer.class, "With.SpellChecker"]

Each of these keys will result in an erroneous injector configuration that won’t be detected until runtime. While there are certainly far fewer possible total errors with this approach, there are nonetheless the same perils and pitfalls so long as we rely on a string key in any consistent and foundational manner. How then can we fix this problem?

It turns out we can still keep all the benefits of combinatorial keys and eliminate the problems that the string part of the key presents. By using an annotation in place of the string part of the combinatorial key, we get the benefit of a safe key and the flexi- bility of an arbitrary and explicit string key.

Consider the following combinatorial keys that are composed of a type and an

annotation type:

[Emailer.class, English.class]

[Emailer.class, WithSpellChecker.class] [Database.class, FileBased.class]

Misspelling the annotation name results in a compiler error, which is an early and clear indication of the problem. This is exactly what we were after. What we have done is effectively replace the string key with a second type key. In combination with the ser- vice’s type key, this retains the qualities of good behavior in well-chosen keys but also eliminates the problems posed by arbitrary, abstract strings. This is quite a compre- hensive and elegant solution. The fact that annotations can be reused also makes them good for more than the one service:

[Emailer.class, English.class] [Dictionary.class, English.class] [SpellChecker.class, English.class]

We use the @English annotation to distinguish variant implementations of not only the Emailer but also the Dictionary and TextEditor services. These keys are self- documenting and still give us the flexibility of a string key. Since annotations are cheap to create (hardly a couple of lines of code) and reuse, this approach scales nicely too.

Guice embodies the use of type/annotation combinatorial keys and was probably the first library to use this strategy. Listing 2.16 and figure 2.18 show what the injector configuration might look like for the spellchecking service I presented in the previ- ous example.

public class SpellingModule extends AbstractModule { @Override

protected void configure() { bind(SpellChecker.class) .annotatedWith(English.class) .to(EnglishSpellChecker.class); bind(SpellChecker.class) .annotatedWith(French.class) .to(FrenchSpellChecker.class); } }

Then using either of these services in a client is as simple as using the appropriate annotation:

Guice.createInjector(new SpellingModule())

.getInstance(Key.get(SpellChecker.class, English.class)) .check("Hello!");

When this code is executed, Guice obtains the SpellChecker instance bound to the combinatorial key represented by [SpellChecker.class, English.class]. Invoking

Listing 2.16 Guice module binding services to combinatorial keys

[SpellChecker.class, English.class] SpellChecker << interface >> English SpellChecker << annotation >> @ English (bound to) [SpellChecker.class, French.class] SpellChecker << interface >> French SpellChecker << annotation >> @ French (bound to) Figure 2.18

Combinatorial type keys use annotations to identify variant implementations of an interface.

the check() method on it runs the service method on the correct (that is, English) implementation.

This is also easily done in any client of SpellChecker via the use of @English anno- tation near the injection point of the dependency. Here’s one such example:

public class SpellCheckerClient { @Inject

public SpellCheckerClient(@English SpellChecker spellChecker) { //use provided spellChecker

} }

This might strike you as odd—while the client does not depend directly on a specific implementation, it seems to couple via the use of @English annotation. Doesn’t this mean SpellCheckerClient is tightly coupled to English spellcheckers? The answer is a surprising no. To understand why, consider the following altered injector configura- tion:

public class SpellingModule extends AbstractModule { @Override

protected void configure() { bind(SpellChecker.class)

.annotatedWith(English.class) .to(FrenchSpellChecker.class); }

}

In this example, I’ve changed the service implementation bound to the combinatorial key [SpellChecker.class, English.class] so that any dependent referring to an @English annotated SpellChecker will actually receive an implementation of type FrenchSpellChecker. There are no errors, and SpellCheckerClient is unaware of any change and more importantly unaffected by any change. So they aren’t really tightly coupled.

No client code needs to change, and we were able to alter configuration transparently. Similarly, you can build such combinatorial bindings in Spring JavaConfig using types and method names:

@Configuration

public class EmailConfig { @Bean

public SpellChecker english() { return new EnglishSpellChecker(); }

@Bean

public Emailer emailer() {

return new Emailer(english()); } } Binding of English spellchecker Create an English emailer

Here, the combinatorial key is [SpellChecker, english()], the latter being the name of the method. It performs a very similar function to the @English annotation we saw just now, though the two approaches differ slightly.

Now let’s look at how to take this practice a step further and separate code by area of concern.