• No results found

Separating infrastructure and application logic

This chapter covers: ■ Using dependency injection

2.5 Separating infrastructure and application logic

You have seen how object graphs that represent various services may be created, assembled, and referenced. You have also seen the steps to creating and using depen- dency injectors and ultimately the objects that they manage for an application. DI libraries differ slightly in the manner in which these steps are achieved and the rigor with which they are performed, but ultimately they all follow the same principles.

Together, these properties make for lean and focused components with code only to deal with their primary business purpose, be it:

Rendering a web page Sending email

Purchasing stock Checking bad spelling

Logic geared toward such purposes is termed application logic. Everything else is meant only to support and enhance it. Logic for constructing and assembling object graphs, obtaining connections to databases, setting up network sockets, or crunching text through spellcheckers is all peripheral to the core purpose of the application.

While this infrastructure logic is essential to all applications, it is important to distin- guish it from the application logic itself. This not only helps keep application logic clear and focused, but it also prevents tests from being polluted with distracting bugs and errors that may have nothing to do with the code’s purpose. Dependency injec- tion helps in this regard. Figure 2.19 describes the fundamental injector and object composition that forms the basis of modern architectures.

Application Injector

Clients

Services Configuration

Figure 2.19 Injectors assemble clients and services as per configuration, into comprehensive applications.

Good DI libraries enforce and exemplify this core ideology. They are as much about preventing abuse as they are about proffering best practices or flexibility. As such, a DI library that takes extra steps to prevent you from accidentally shooting yourself in the foot (by checking bindings at compile time, for instance) is preferable. On the other hand, DI libraries that offer a great deal of flexibility (and weak type safety) can draw even experienced developers into traps. This is where careful design is important. Don’t be afraid to refactor and redesign your code when you discover violations of good design. A little bit of effort up front to ensure that infrastructure logic remains separate from application logic will go a long way toward keeping your code readable, maintainable, and robust throughout its life.

2.6

Summary

This chapter was a headfirst dive into dependency injectors, their configuration, and their use. You saw how injectors must be bootstrapped with specified configuration and then used to obtain and perform operations on components they build and wire. At first, the injector looks no different to a service locator, and this is roughly correct in the context of obtaining the first, or the root, component from which the rest of an object graph extends.

All services that are used as dependencies are labeled by a key, which the injector uses as a way of referring to them during configuration or service location. The cou- pling of a key to a particular object graph is called a binding. These bindings deter- mine how components are provided with their dependencies and thus how object graphs are ultimately constructed and wired. There are several kinds of keys, the most common being simply string identifiers, which are common in XML-configured DI libraries such as Spring, StructureMap, and HiveMind. String keys are flexible and provide us with the freedom to describe arbitrary and various assemblies of object graphs that portend a service. Different object graphs that conform to the same set of rules, that are the same service, may be thought of as different implementations of that service. String keys allow easy identification of such dependency variants.

However, string keys have many limitations, and the fact that they are unrestricted character strings means that they are prone to human error and to misuse and poor design choices. This leads to the necessity of well-chosen string identifiers that portend good key behavior. We defined the characteristics of well-chosen keys as being unique, arbitrary, and explicit. Unique keys ensure that no two object graphs are identified by the same key accidentally and so ensure that there is no ambiguity when a dependency is sought. Arbitrary keys are useful in supporting a variety of custom variants of services, which a creative user may have cooked up. Finally, these keys must also be explicit if they are to exactly describe what a service implementation does as clearly and concisely as is possible. Well-chosen keys combined with the use of namespaces greatly improve read- ability and also reduce the probability of accidental misuse.

String keys satisfy all of these qualities, but they have serious drawbacks since they lack the knowledge of the type (or class) they represent. This can result in syntactically

correct injector configuration that fails at runtime, sometimes even as late as the first use of a faultily configured object graph. In rare cases, it can even result in incorrect dependency wiring without any explicit errors, which is a very scary situation! In a stat- ically typed language like Java, better solutions are imperative to these problems, espe- cially in large projects that make use of agile software methods that require rapid, iterative execution of partial units.

An alternative is the use of type keys. These are keys that simply refer to the data type of the service. Type keys solve the misspelling and misuse problem of string keys but sacrifice a lot of flexibility and abstraction in doing so. PicoContainer is one DI library that supports the use of type keys. Their primary limitation is the inability to distinguish between various implementations without directly referring to them in cli- ent code. As a result, type keys violate almost all of the requirements of well-chosen keys, though they are safe and help catch errors early, at compile time.

One solution is to combine the two approaches and use type keys to narrow down the service’s type and pair it with a string key that distinguishes the specific variant. These are called combinatorial keys, and they provide all of the flexibility of string keys and retain the rigor of type keys. PicoContainer also provides for these hybrid type/string combinatorial keys. However, the use of string identifiers at all still means that there is a risk of misspelling and accidental misuse. Guice provides a comprehen- sive solution: the use of custom annotation types in place of the string part of a combi- natorial key. The combination of the type key referring to a service and an annotation- type key referring to an abstract identifier to distinguish between implementations provides for a compelling mitigation of the problems of even partial string keys.

Finally, whatever DI library you choose, dependency injectors are geared toward the same goals, that is, separate logic meant for constructing and assembling object graphs, managing external resources and connections, and so on from logic that is intended solely for the core purpose of the application. This is called the separation of infrastructure from application logic and is an important part of good design and architecture. The majority of the concepts presented in this book revolve around this core ideology and in many ways serve to emphasize or enhance the importance of this separation. A successful rendition of dependency injection gives you more time to focus effort on application logic and helps make your code testable, maintainable, and concise.

54

Investigating DI

“The best way to predict the future is to invent it.”

—Alan Kay

Previously we discussed two methods of connecting objects with their dependen- cies. In the main we have written classes that accept their dependencies via construc-

tor. We have also occasionally used a single-argument method called a setter method

to pass in a dependency. As a recap, here are two such examples from chapter 1: public class Emailer {

private SpellChecker spellChecker;

public Emailer(SpellChecker spellChecker) { this.spellChecker = spellChecker; }

}

This chapter covers:

■ Learning about injection by setter and constructor

■ Investigating pros and cons of injection idioms

■ Identifying pitfalls in object graph construction

■ Learning about reinjecting objects

■ Discovering viral injection and cascaded object graphs

The same class accepting dependencies by setter: public class Emailer {

private SpellChecker spellChecker;

public void setSpellChecker(SpellChecker spellChecker) { this.spellChecker = spellChecker;

} }

These are two common forms of wiring. Many dependency injectors also support other varieties and bias toward or away from these idioms.

The choice between them is not always one of taste alone. There are several conse- quences to be considered, ranging from scalability to performance, development rigor, type safety, and even software environments. The use of third-party libraries and certain design patterns can also influence the choice of injection idiom.

DI can make your life easier, but like anything else, it requires careful thought and an understanding of pitfalls and traps and the available strategies for dealing with them. The appropriate choice of injection idiom and accompanying design patterns is significant in any architecture. Even if you were to disavow the use of a DI library alto- gether, you would do well to study the traps, pitfalls, and practices presented in this chapter. They will stand you in good stead for designing and writing healthy code.

In this chapter, we will look at an incarnation of each major idiom and explain when you should choose one over another and why. I provide several strong argu- ments in favor of constructor injection. However, I will show when setter injection is preferable and how to decide between the two. Understanding injection idioms and the nuances behind them is central to a good grasp of dependency injection and architecture in general. Let’s start with the two most common forms.