This article is excerpted from the upcoming book Dependency Injection by Dhanji R. Prasanna and published by Manning Publications. It introduces the concept of scope and the role it plays in application design. For the table of contents, the Author Forum, and other resources, go to http://www.manning.com/prasanna/.
In one sentence, scope is a fixed duration of time or method calls in which an object exists. In other words, a scope is a context under which a given key refers to the same instance. Another way to look at this is to think of scope as the amount of time an object's state persists. When the scope context ends, any objects bound under that scope are said to be out of scope and cannot be injected again in other instances.
State is important in any application. It is used to incrementally build up data or responsibility. State is also often used to track the context of certain processes, for instance, to track objects in the same database transaction.
In this article we'll talk about some of the general purpose scopes: singleton and no-scope. These are scopes that are universally applicable in managing state. We'll also take a look at managing state in specific kinds of applications, particularly the Web. Managing user-specific state is a major part of scoping for the web, and this is what the request, session, flash and conversation scopes provide. We'll look at a couple of implementations of these with regard to Guice and Spring, and how they may be applied in building practical web applications. First, a primer on scope.
Understanding Scope
The real power of scope is that it you can model the state of your objects declaratively. By telling the injector that a particular key is bound under a scope, you can move the construction and wiring of objects to the injector's purview. This has some important benefits:
- Let the injector manage the latent state of your objects
- Ensure that your services get new instances of dependencies as needed
- Implicitly separate state by context (for example, two HTTP requests imply different contexts)
- Reduce the necessity for state-aware application logic (which makes code much easier to test and reason about)
Scope properly applied means that code working in a particular context is oblivious to that context. And it is the injector's responsibility to manage these contexts. Not only does this mean that you have an added separation between infrastructure and application logic, but also that the same services can be used for many purposes simply by altering their scopes. Take this example of a family bathroom and their toothpaste:
family.give("Joanie", injector.getInstance(Toothpaste.class));
family.give("Jackie", injector.getInstance(Toothpaste.class));
family.give("Sachin", injector.getInstance(Toothpaste.class));
Simply looking at this code we can say that the Toothpaste is used by Joanie first, then Jackie and finally by Sachin. We might also guess that each family member receives the same tube of toothpaste. If the tube were especially small, Sachin might be left with no paste at all (as per figure 1).
Figure 1: The injector distributes the same instance of toothpaste to all family members
This is an example of context: all three family members use the same bathroom, and therefore have access to the same instance of Toothpaste. It is exactly the same as the following program, using construction by hand:
Toothpaste toothpaste = new FluorideToothpaste();
family.give("Joanie", toothpaste);
family.give("Jackie", toothpaste);
family.give("Sachin", toothpaste);
If this were the whole life of the injector, then only one instance of Toothpaste is ever created and used. In other words, Toothpaste is bound under singleton scope. If however, each family member had their own bathrooms (each with its own tube of toothpaste) then the semantics would change considerably (as shown in figure 2).
Figure 2: The injector now creates a new toothpaste instance for each family member
family.give("Joanie", injector.getInstance(Toothpaste.class));
family.give("Jackie", injector.getInstance(Toothpaste.class));
family.give("Sachin", injector.getInstance(Toothpaste.class));
Nothing has changed in code, but now a new instance of Toothpaste is available to each family member. And now there is no danger of Sachin being deprived of toothpaste by Joanie or Jackie. In this case, the context under which each object operates is unique (that is, their own bathrooms). You can think of this as the opposite of singleton scoping. Technically this is like having no scope at all.
The No-scope (or default scope)
In a sense, no-scope fulfills the functions of scope, as it:
- provides new instances transparently,
- is managed by the injector, and
- associates a service (key) with some context
Or does it? The first two points are indisputable. However, there arises some difficulty in determining exactly what context it represents. To get a better idea of no-scope's semantics, let's dissect the example of the toothpaste from earlier. We saw that it took no change in the use of objects to alter their scope. The family.give() sequence looks exactly the same for both singleton and no-scope:
family.give("Joanie", injector.getInstance(Toothpaste.class));
family.give("Jackie", injector.getInstance(Toothpaste.class));
family.give("Sachin", injector.getInstance(Toothpaste.class));
Or, expanded using construction by hand (modeled in figure 3):
Toothpaste toothpaste = new FluorideToothpaste();
family.give("Joanie", toothpaste);
toothpaste = new FluorideToothpaste();
family.give("Jackie", toothpaste);
toothpaste = new FluorideToothpaste();
family.give("Sachin", toothpaste);
Figure 3: Each member of the family receives their own instance of Toothpaste
In no-scope, every reference to Toothpaste implies a new toothpaste instance. We likened this to the family having three bathrooms, one for each member. However, this is not exactly accurate. For instance, if Sachin brushed his teeth twice:
family.give("Joanie", injector.getInstance(Toothpaste.class));
family.give("Jackie", injector.getInstance(Toothpaste.class));
family.give("Sachin", injector.getInstance(Toothpaste.class));
family.give("Sachin", injector.getInstance(Toothpaste.class));
...you would end up with a total of four Toothpaste instances (see figure 4).
Figure 4: There are now four instances of Toothpaste, for each use
Now, in our conceptual model, there were only three bathrooms. But in practice there were four tubes of toothpaste. This means that no-scope cannot be relied on to adhere to any conceptual context. No-scope means that every time an injector looks for a given key (one bound under no-scope), it will construct and wire a new instance. Furthermore, let's say Sachin took Joanie on vacation and only Jackie was left at home. She would brush her teeth once, as follows:
family.give("Jackie", injector.getInstance(Toothpaste.class));
And this would mean only one instance of Toothpaste was ever created for the life of the application. This was exactly what happened with singleton scoping, but this time it was purely accidental that it did. Given these two extremes, it is difficult to lay down any kind of strict rules for context with no-scope. You could say, perhaps, that no-scope is a split-second scope where the context is entirely tied to referents of a key. This would be a reasonable supposition. Contrast singleton and no-scope in figure 5.
Figure 5: Timeline of contexts, contrasting singleton and no-scopes
No-scope is a very powerful tool for working with injector-managed components. This is partly because it allows a certain amount of flexibility in scaling upward. Dependents that exist for longer times (or in wider scopes) may obtain no-scoped objects as they are required, safely. Granny obtained new instances of an Apple on each use (see listing 1, modeled in figure 6).
Figure 6: Granny obtains instances of Apple (bound to no scope) from a provider
public class Granny {
public Provider appleProvider;
public void eat() {
appleProvider.get().consume(); |#1
appleProvider.get().consume(); |#2
}
}
(Annotation) <#1 Two new apples created>
In listing 1, the eat() method uses a provider to retrieve new instances, just as we did for Toothpaste earlier. Here Apple is no-scoped.
Guice and Spring differ in nomenclature with regard to the no-scope. Spring calls it as the prototype scope. The idea being that a key (and binding) is a kind of template (or prototype) for creating new objects. In the following example, no-scoping enabled multiple threads to see independent instances of an object (modeled in figure 7):
<beans>
<bean id="slippery" class="Slippery" scope="prototype"/>
<bean id="shady" class="Shady" scope="prototype"/>
<bean id="safe" class="UnsafeObject" init-method="init" scope="prototype">
<property name="slippery" ref="slippery">
<property name="shady" ref="shady">
<bean>
<beans>
Figure 7: UnsafeObject and both its dependencies were no-scoped
This object was actually safe, because any dependents of key safe were guaranteed to see new, independent instances of UnsafeObject. Like singleton, the name prototype comes from the Gang of Four book on Design Patterns. I prefer to refer to it as no-scope, largely because it is a more descriptive name. Like Guice, PicoContainer also assumes the no-scope if a key is not explicitly bound to some scope:
PicoContainer injector = new DefaultPicoContainer();
injector.addComponent(Toothpaste.class);
family.give("Joanie", injector.getComponent(Toothpaste.class));
family.give("Jackie", injector.getComponent (Toothpaste.class));
...
Almost no difference at all.
NOTEYou will sometimes also hear no-scope referred to as the default scope. This is a less descriptive name, and typically connotes either Guice or PicoContainer (since they default to the no-scope).