Consider the following sample code, adapted
from [1]. It contains something that
practically all programs have, although its often
undesired: a strong coupling between classes, in this case caused
by the dependency of class MovieLister
on class ColonDelimitedMovieFinder
.
The dependency is established in three places:
finder
is declared to be of type ColonDelimitedMovieFinder
;ColonDelimitedMovieFinder
;
andfindAll()
is called on finder
.1 public
class MovieLister {
2 private
ColonDelimitedMovieFinder finder;
3 public
MovieLister() {
4 finder
= new ColonDelimitedMovieFinder("movies1.txt");
5 }
6 public
Movie[] moviesDirectedBy(String arg) {
7 List
allMovies = finder.findAll();
8 for
(Iterator it = allMovies.iterator(); it.hasNext();) {
9 Movie
movie = (Movie) it.next();
10 if
(!movie.getDirector().equals(arg)) it.remove();
11 }
12 return
(Movie[]) allMovies.toArray(new Movie[allMovies.size()]);
13 }
14 }
15 public
class ColonDelimitedMovieFinder {
16 String
filename;
17 public
ColonDelimitedMovieFinder(String filename) {
18 this.filename
= filename;
19 }
20 public
List findAll() {
21 //
TODO Auto-generated method stub
22 return
null;
23 }
24 }
You can download a zip containing the .java files here.
A first step of reducing this dependency is
to introduce a new interface that captures the use of finder
objects in class MovieLister
.
Such an interface could be created with the Extract
Interface refactoring, but todays implementations of
this refactoring (including that of Eclipse) require you, the
programmer, to decide what belongs into that interface. For the
given program this is no problem: a quick inspection of the code
of class MovieLister
shows that findAll()
is the only method called on finder
.
What, however, if finder
were assigned to other variables or passed on (as an actual
parameter) to other classes? How would you know which other
methods are called on finder objects in these new contexts? How
would you decide then what to put into the interface? The answer
requires the collection of all methods that are possibly called
on any variable finder
might eventually get assigned to, and thus a systematic analysis
of the assignments (including parameter passes) in your program:
it requires construction of the transitive closure of the use of finder
under variable
assignment. This is exactly what Infer Type does for you.
To get an impression of the workings of the
refactoring, you best try it out. To invoke Infer Type in
Eclipse, double click on a reference where it is declared (a declaration
element, in the given case finder
in line 2) in the editor pane and select Infer Type from the
context menu, thereby starting the refactoring.
After the refactoring has finished its analysis, it displays the new type (interface or abstract class, depending on the particulars of the specific case) and its position in the type hierarchy in a small UML-style diagram.
The orange colour indicates that interface Finder
is a new type
that the refactoring will create for you.
In order to introduce the new type, you must select it. You can then inspect (but not change!) the members of the new type by clicking the small triangle left of Show methods , giving you the contents of the interface:
In the boxes below, you can enter the new types name and package, as well as decide whether it is to be an interface or an abstract class. In our case, just leave it as it is and press [Finish]. (If you check "Open new type in editor", the newly introduced type will be shown to you in the editor.)
After it has finished, you will find that Infer Type
Finder
,ColonDelimitedMovieFinder
implement Finder
,
andColonDelimitedMovieFinder
with Finder
in the declaration of field finder
.This may appear not a big deal to you, but as you use Infer Type in your own programs, you will find that it can do remarkable jobs for you. For instance,
extends
or implements
clause in the new type;A detailed description of the inner workings of Infer Type can be found in [2].
Returning to the original decoupling
problem, you will have noticed that the dependency of MovieLister
on ColonDelimitedMovieFinder
still persists in line 4, where the constructor of ColonDelimitedMovieFinder
is called. This dependency can be removed by application of a
pattern named Dependency Injection in [1];
it involves replacing the constructor
3 public
MovieLister() {
4 finder
= new ColonDelimitedMovieFinder("movies1.txt");
5 }
with
3 public
MovieLister(ColonDelimitedMovieFinder finder) {
4 this.finder
= finder;
5 }
(so-called Constructor Injection [1]), as well as introduction of an «Assembler
» class that
creates instances of Finder
and MovieLister
and plugs them together (not shown). In Eclipse, you can refactor
the constructor automatically, simply be applying the Introduce
Parameter refactoring on new ColonDelimitedMovieFinder("movies1.txt")
in line 4, as in
However, you might contend that all that is
achieved by this is a shift of the dependency from the method
body to the method header. Fortunately, this dependency can
immediately be removed by applying Infer Type on the newly
introduced parameter, in this case finder
in line 3. This time, analysis of the declaration element results
in the following being displayed:
The inferred type of finder
, the interface Finder
, already
exists; it will automatically be chosen and no new type needs to
be added. After pressing [Finish], the refactoring changes the
method header to
3 public
MovieLister(Finder finder) {
4 this.finder
= finder;
5 }
so that the dependency is completely removed. Note that this second application of Infer Type resembles Eclipses Use Supertype Where Possible refactoring, only that the supertype is chosen automatically and the replacement is localized (to a single declaration element and the references it gets assigned to).
Now if you think that having to apply three
refactorings is not much of a help for such a simple change,
youre probably right. The good thing is that with Infer
Type, you can do it with less. To find out how, restore the
original program, apply the Introduce Parameter refactoring on new ColonDelimitedMovieFinder("movies1.txt")
first, and then Infer Type on the newly introduced parameter. Is
that better?
[1] http://www.martinfowler.com/articles/injection.html
[2] Decoupling Code with Inferred Interfaces submitted to SAC 2006.
© The IntoJ Team, 2005.