Tải bản đầy đủ (.pdf) (26 trang)

The Heart of Spring - inversion of control

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (551.35 KB, 26 trang )

29
■ ■ ■
CHAPTER 3
The Heart of Spring:
Inversion of Control
G
enerally, when you are writing code, your class will accumulate dependencies. Typically,
you will use dozens of other classes: the primitive wrapper classes, the collection classes,
and so on. Some of these will be as general as String or Integer, and others will be
domain specific.
For the most part, nothing changes with inversion of control. The internal implemen-
tation details of your classes should look much the same before and after. However, if
your class has a dependency on a particular external class that could reasonably have
alternative implementations, or if a class represents data that you might reasonably want
to change at some future date, inversion of control frameworks such as Spring allow you
to supply these dependencies at a later date. Because you are providing the dependencies
from outside the class instead of acquiring them directly, this form of inversion of control
is also known as dependency injection (DI).
Spring doesn’t do any magic here—you still need to supply reference variables with
which to manipulate these external dependencies—but it does move the configuration
details of these dependencies from compile-time to runtime.
Benefits and Disadvantages of DI
In principle, you don’t need a framework to inject dependencies into your code; you can
do this from code. In practice, however, most applications built by using inversion of control
use a framework of some type to carry out the dependency injection. Typically, as with
Spring, these read configuration information and then use the Java reflection API or bytecode
manipulation to invoke the appropriate methods on your code to inject the dependencies.
Although this behavior is not innate in the dependency injection approach, it is so
widespread that it might as well be. Unfortunately, it leads directly to the one real disad-
vantage that containers such as Spring have over hard-coding of dependencies: that they
lose some of the advantages of static type checking. The configuration information will


Minter_685-4C03.fm Page 29 Thursday, November 8, 2007 6:03 AM
30
CHAPTER 3

THE HEART OF SPRING: INVERSION OF CONTROL
not be read until runtime; therefore, any incompatible type information given in the
configuration will not cause errors to be produced until runtime.
If you are worried by this sudden loss of an important advantage of Java development,
I can reassure you. This is not as big a deal as you might think. When I first encountered
Spring, I was very resistant to the dynamic type checking aspect of it. However, experience
proved to me that it was not the awful design flaw that I at first took it for. In practice, in
Spring this disadvantage is ameliorated by the tendency for the configuration information
to be processed as early as possible in the life cycle of a Spring-based application. Java is
a strongly typed language, so inappropriate casts will usually cause a conspicuous and
immediate application failure. That in turn means that a reasonable minimum of testing
in your development infrastructure will allow you to detect most type inconsistencies at
build-time, which is very nearly as good as compile-time for most purposes. Additionally,
tools such as the Spring IDE (discussed in the appendix) provide features to perform type
validation during the development process.
TYPE SYSTEMS
Developers are sometimes hazy about the distinctions between static and strong typing, probably because
the specific terminology is unimportant unless you are frequently migrating between languages using
other approaches to typing.
A strongly typed language such as Java does not allow operations to be performed at runtime on
variables of the wrong type. For example, Java does not allow an Integer reference to be assigned to a
String variable. A weakly typed language would perform an implicit type conversion, allowing the assignment.
A statically typed language such as Java determines all (or as many as possible) type incompatibil-
ities at runtime and will not compile until these are eliminated. A dynamically typed language does not
perform any type checking until runtime.
There are a few other minor disadvantages to Spring as a specific framework for depen-

dency injection. The XML-based configuration files typically used can become confusing
if they are not thoughtfully maintained. Expressing relationships between Java components
in XML sometimes feels inelegant. The extensive use of reflection to inject dependencies
can make debugging more complex. These issues are specific to Spring’s implementation,
not to DI itself, but the use of Spring as an implementation more than compensates for these.
Coupling
The big win in using dependency injection is that it allows you to make your applications
loosely coupled. That is to say, any one class of your implementation will tend not to have
any dependencies on any other class’s specific implementation.
Minter_685-4C03.fm Page 30 Thursday, November 8, 2007 6:03 AM
CHAPTER 3

THE HEART OF SPRING: INVERSION OF CONTROL
31
You will still have dependencies on the type system that you’re establishing in your
application, of course, but loose coupling encourages the use of programming to inter-
faces rather than to abstract or concrete implementations.
Tight Coupling
Rather than talking in abstract terms, I will show you an example of some tightly coupled
code to illustrate these concerns (see Listing 3-1).
Listing 3-1.
A Minimal Tightly Coupled Component
package com.apress.coupling;
public class TightlyCoupled {
private Transport transport = new SmtpImpl();
public void sendMessage() {
transport.send();
}
}
This code is a little contrived in its simplicity, but it reflects a real-world scenario in

which tight coupling can cause some problems. The transport mechanism to be used by
this class is obviously SMTP. The implementation has been hard-coded into the class and
cannot be changed except by recompilation. This leads to two major concerns: testing
and reusability.
A unit test written for this class cannot readily separate the behavior of the TightlyCoupled
class from the behavior of SmtpImpl class. If we encounter a bug, narrowing down its loca-
tion will be more difficult. Depending on the contents of SmtpImpl, we may have to set up
a mail server dedicated to the test and write code to determine whether the e-mail was
transmitted successfully. Our unit test has become an integration test.
Because the TightlyCoupled implementation has the SmtpImpl implementation hard-
coded, the SmtpImpl transport cannot readily be replaced it if it becomes necessary to use
a different transport (for example, SOAP) for whatever content is to be transmitted. This
necessarily reduces the reusability of the class in other situations.
Loose Coupling
Loosely coupled code allows the major dependencies to be supplied from external code.
Again, I’ll give a simple example of a class that has a loose coupling with its dependency
(see Listing 3-2).
Minter_685-4C03.fm Page 31 Thursday, November 8, 2007 6:03 AM
32
CHAPTER 3

THE HEART OF SPRING: INVERSION OF CONTROL
Listing 3-2.
A Minimal Loosely Coupled Component
package com.apress.coupling;
public class LooselyCoupled {
private Transport transport;
public LooselyCoupled(final Transport transport) {
this.transport = transport;
}

public void sendMessage() {
transport.send();
}
}
Again this code is somewhat contrived, but it does illustrate clearly the breaking of the
dependency on the specific transport implementation. By allowing the implementation
to be passed via the constructor, we allow alternative implementations to be passed in. By
breaking the tight coupling, we have also removed the hindrances to the development of
external tests and reusability.
For our testing, a mock object can be supplied to the constructor, allowing us to test
that the appropriate method call is made without needing any of the underlying infra-
structure associated with the real transport implementations.
For reusability, we can swap out the SMTP implementation for a SOAP, RMI, or any
other suitable transport.
The only notable disadvantage to the loose coupling approach, as illustrated in Listing 3-2,
is a slight increase in the verbosity of the resulting class implementation, mostly deriving
from the demands of the JavaBean specification when adding properties to classes.
Knowing When to Stop
Not all implementation details need to be exposed to the outside world, even when creating
an application to run within the Spring framework. Only dependencies that you might
want to substitute in order to test the component in isolation are likely to be candidates
for access in this way. Listing 3-3 shows a class with two candidate dependencies.
Listing 3-3.
A Simple Implementation with Two Dependencies
package com.apress.coupling;
import java.util.SortedSet;
import java.util.TreeSet;
Minter_685-4C03.fm Page 32 Thursday, November 8, 2007 6:03 AM
CHAPTER 3


THE HEART OF SPRING: INVERSION OF CONTROL
33
public class Mailinglist {
private SortedSet<String> addresses = new TreeSet<String>();
private Transport transport = new SmtpImpl();

public void addAddress(final String address) {
addresses.add(address);
}

public void send() {
for( final String address : addresses) {
transport.send(address);
}
}
}
In Listing 3-3, our implementation contains a dependency on a specific Transport and
a dependency on a specific SortedSet. It would be reasonable to assume that both of these
dependencies should be provided via injection. In practice, however, I would be inclined
to inject only the Transport implementation. The Transport implementation is a good
candidate because of the following:
• It is likely to be a part of our own code base and thus itself a candidate for unit tests.
• It is reasonable to foresee a circumstance in which we would want to use an alternative
message transport.
• It is likely that the implementation itself has a substantial set of dependencies on
other classes.
• It is likely that the SmtpImpl implementation requires additional infrastructure to
support it.
In my view, the SortedSet implementation is not a good candidate for several reasons:
• TreeSet is a part of the standard class library available in the JDK, and thus unlikely

to be a candidate for unit tests.
• We are unlikely to use an alternative implementation of SortedSet unless we are
involved in minute performance-related debugging concerns.
• TreeSet will have no dependencies beyond the JDK itself. The JDK is generally
assumed to be correct unless proven otherwise and does not require its own unit tests.
Minter_685-4C03.fm Page 33 Thursday, November 8, 2007 6:03 AM
34
CHAPTER 3

THE HEART OF SPRING: INVERSION OF CONTROL
Adding the specific set implementation to the API here would provide very little advantage
in return for the extra complexity added to our MailingList class implementation.
Of course, there are possible counterarguments even for this scenario, but in the end
the choice of when to make a dependency injectable resides with the developer. My advice
is to choose whatever approach will make your unit tests easiest to write, and then refactor
your code as it proves appropriate. Although this won’t give you the correct answer every
time, it will at least be easy to change your architecture without needing to substantially
rework your unit tests, which will in turn reduce the frustration involved in changing APIs
and will keep your code clean and the bug count low.
The Need for a Framework
The framework is not an absolute requirement of dependency injection. Taking our loosely
coupled example from Listing 3-2, it is obvious that the injection of the dependencies can
be carried out from conventional code.
Indeed, as Listing 3-4 shows, this is the sort of code you will have written frequently.
Dependency injection is not some strange abstract new technique; it is one of the normal
tools of the developer.
Listing 3-4.
Dependency Injection from Conventional Code
final Transport smtp = new SmtpImpl();
final LooselyCoupled lc1 = new LooselyCoupled(smtp);

lc1.sendMessage();
final Transport soap = new SoapImpl();
final LooselyCoupled lc2 = new LooselyCoupled(soap);
lc2.sendMessage();
In some ways, this is one of the attractive features of dependency injection. You do not
have to learn a completely new programming style in order to get the associated advantages;
you just have to be a little more disciplined in selecting how and when to apply this technique.
Nonetheless, we do in practice use frameworks, so it is reasonable to ask what these
offer over and above the benefits available from the kind of hard-coded approach used in
Listing 3-4.
The Container
The basic container for your Spring application is a BeanFactory. As the name implies, this
is a class that is responsible for manufacturing bean instances and then configuring their
Minter_685-4C03.fm Page 34 Thursday, November 8, 2007 6:03 AM
CHAPTER 3

THE HEART OF SPRING: INVERSION OF CONTROL
35
dependencies. A Spring bean can be any Java object, although generally we will be refer-
ring to standard Java beans.
Depending on the bean definition information retained by the bean factory, the beans
it instantiates may be created on demand, or may be shared among all clients of the factory.
Table 3-1 shows the methods available on classes implementing the BeanFactory interface.
The implementation of the factory (how it actually goes about acquiring the instances
and configuring their dependencies) is not really our problem. As long as we can acquire
a bean factory that materializes suitable beans, we need inquire no further. The limited
set of methods available should help to illustrate the fact that a BeanFactory really is a
container; the methods provided to you are exclusively about querying the factory about
its contents and obtaining items from it. Listing 3-5 shows the instantiation, configura-
tion, and use of a BeanFactory implementation purely from code.

Table 3-1.
BeanFactory Methods
Method Description
boolean containsBean(String name) Determines whether the factory contains a bean
with the given name.
String[] getAliases(String name) Determines the alternative names (aliases) for a
bean with the given name.
Object getBean(String name) Obtains an instance of the bean with the given name
from the factory. This may be a new instance or a
shared instance.
Object getBean(String name, Class
requiredType)
Obtains an instance of the bean with the given name
and type from the factory. This is used by the auto-
wiring feature described in the “Autowiring” section
of this chapter.
Class getType(String name) Determines the type of a bean with a given name.
boolean isPrototype(String name) Determines whether the bean definition is a proto-
type. A prototype is a named set of bean definition
information that can be used to abbreviate the
configuration of a “real” bean definition. If a bean
definition is a prototype, it cannot be instantiated
with a call to getBean().
boolean isSingleton(String name) Determines whether calls to getBean() for the named
bean will return a new instance with every call, or a
single shared instance (a singleton instance).
boolean isTypeMatch(String name,
Class targetType)
Determines whether the named bean matches the
provided type—essentially determines whether a

call to getBean(String,Class) would be successful.
Minter_685-4C03.fm Page 35 Thursday, November 8, 2007 6:03 AM
36
CHAPTER 3

THE HEART OF SPRING: INVERSION OF CONTROL
Listing 3-5.
Manually Constructing a Bean Factory
// Establish the factory to
// contain the bean definitions
final DefaultListableBeanFactory bf =
new DefaultListableBeanFactory();
// Register the transport implementations
bf.registerBeanDefinition("smtp",
new RootBeanDefinition(SmtpImpl.class,true));
bf.registerBeanDefinition("soap",
new RootBeanDefinition(SoapImpl.class,true));
// Register and configure the SMTP example as
// a bean definition
BeanDefinitionBuilder builder = null;
builder = BeanDefinitionBuilder.
rootBeanDefinition(LooselyCoupled.class);
builder = builder.setSingleton(true);
builder = builder.addConstructorArgReference("smtp");
bf.registerBeanDefinition("looseSmtp",builder.getBeanDefinition());
// Register and configure the SOAP example as
// a bean definition
builder = BeanDefinitionBuilder.
rootBeanDefinition(LooselyCoupled.class);
builder = builder.setSingleton(true);

builder = builder.addConstructorArgReference("soap");
bf.registerBeanDefinition("looseSoap",builder.getBeanDefinition());
// Instantiate the smtp example and invoke it
final LooselyCoupled lc1 = (LooselyCoupled)bf.getBean("looseSmtp");
lc1.sendMessage();
// Instantiate the soap example and invoke it
final LooselyCoupled lc2 = (LooselyCoupled)bf.getBean("looseSoap");
lc2.sendMessage();
The first question that would tend to spring to mind after reading through Listing 3-5
and comparing it to Listing 3-4 is, “Why would I ever want to do something so ungainly?”
You wouldn’t, of course. Listing 3-5 is purely an illustration of what goes on under the
covers of the framework. You might use a few of these classes if you were extending part
of the framework itself, but most developers will never (or at most rarely) need to touch
Minter_685-4C03.fm Page 36 Thursday, November 8, 2007 6:03 AM
CHAPTER 3

THE HEART OF SPRING: INVERSION OF CONTROL
37
upon BeanDefinitionBuilder and the like. I will show you how the equivalent Spring
configuration would really be defined in the discussion of Listing 3-7 later in this chapter.
Listing 3-5 does illustrate some important parts of the architecture that you will be
working with, however, so it is worth taking the time to understand what is involved here.
The first line of the application establishes a DefaultListableBeanFactory instance. This
is a bean factory that provides no direct assistance in preparing the bean definition infor-
mation. The developer must programmatically assign all the bean definition information:
final DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
The next block of the implementation creates two new bean definitions for the trans-
port implementation classes. This is metadata about the implementations, not instances
of the implementations themselves. We declare that two beans should be available from
the factory, we specify the implementation classes that define them, and we specify that

they are both singletons (the second parameter of the RootBeanDefinition constructor).
Multiple calls to the factory’s getBean() method for the bean named smtp will only ever
return one instance of the SmtpImpl class:
bf.registerBeanDefinition("smtp",
new RootBeanDefinition(SmtpImpl.class,true));
bf.registerBeanDefinition("soap",
new RootBeanDefinition(SoapImpl.class,true));
We then configure two bean definitions for one implementation class. These are
configured similarly but not identically:
BeanDefinitionBuilder builder = null;
builder = BeanDefinitionBuilder.
rootBeanDefinition(LooselyCoupled.class);
builder = builder.setSingleton(true);
builder = builder.addConstructorArgReference("smtp");
bf.registerBeanDefinition("looseSmtp",builder.getBeanDefinition());
Both are definitions for the LooselyCoupled class, both are defined as singletons, but
the constructors are defined as taking different bean definitions for their parameters:
builder = BeanDefinitionBuilder.
rootBeanDefinition(LooselyCoupled.class);
builder = builder.setSingleton(true);
builder = builder.addConstructorArgReference("soap");
bf.registerBeanDefinition("looseSoap",builder.getBeanDefinition());
I have chosen my wording carefully here. We have not passed anything to the constructor
of the class; we have merely specified the definitions of these beans (looseSmtp and
looseSoap) in terms of the named definitions of the earlier smtp and soap beans:
Minter_685-4C03.fm Page 37 Thursday, November 8, 2007 6:03 AM
38
CHAPTER 3

THE HEART OF SPRING: INVERSION OF CONTROL

final LooselyCoupled lc1 = (LooselyCoupled)bf.getBean("looseSmtp");
lc1.sendMessage();
// ...
final LooselyCoupled lc2 = (LooselyCoupled)bf.getBean("looseSoap");
lc2.sendMessage();
Only when the factory has been fully populated with all of the relevant bean definitions
do we use it to materialize actual objects. These are normal Java objects quite indistin-
guishable from the objects returned from the calls to the new operators in Listing 3-4.
XML Configuration
Although a Spring application can in principle be configured in any number of different
ways, XML configuration files are by far the most common approach. Indeed, for most
developers, the set of XML files used to configure the factory for a Spring project and the
BeanFactory instance itself are virtually synonymous. The XML is the representation of the
factory that will be available to you at runtime, so this is not a bad way of thinking of them,
but do bear in mind that it is a useful approximation to the reality of the situation.
Ultimately, we need to use a language of some sort to configure our dependencies.
Traditionally, this has been the Java programming language itself, occasionally resorting
to properties files when the problems of tight coupling became too painful. XML files offer
us a better balance of flexibility, readability, verbosity, and expressiveness.
Something to remember in particular is that there is no 1:1 correspondence between
factories and XML files. It is entirely possible (and normal) to use multiple files to configure
a single factory, or to use a single file to instantiate several discrete factories (though this
is unusual).
Listing 3-6 represents the same configuration information that we painstakingly hard-
coded in Listing 3-5 of the previous section.
Listing 3-6.
A Complete but Simple XML Spring Configuration File
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns=" /> xmlns:xsi=" /> xsi:schemaLocation=

" /> /> <bean id="smtp" class="com.apress.coupling.SmtpImpl" />
Minter_685-4C03.fm Page 38 Thursday, November 8, 2007 6:03 AM

×