Composite and Test-Driven
Refactorings
Composite
refactorings
are high-level refactorings
composed
of
low-level refactorings. Much of the work performed by low-level
refactorings involves moving code around. For example,
Extract Method
[F] moves code to a new method,
Pull Up Method
[F] moves a method
from a subclass to a superclass,
Extract
Class
[F] moves code to a new class, and
Move Method
[F] moves a method from one class
to another.
Nearly all of the refactorings in this book are
composite refactorings. You begin with a piece of code you want to
change and then incrementally apply various low-level refactorings
until a desired change has occurred. Between applying low-level
refactorings, you run tests to confirm that modified code continues
to behave as expected. Testing is thus an integral part of
composite refactoring; if you don't run tests, you'll have a hard
time applying low-level refactorings with confidence.
Testing also plays an altogether different role
in refactoring; it can be used to rewrite and replace old code. A
test-driven refactoring
involves
applying test-driven development to produce replacement code and
then swap out old code for new code (while retaining and rerunning
the old code's tests).
Composite refactorings are used far more
frequently than test-driven refactorings because a good deal of
refactoring work simply involves relocating existing code. When it
isn't possible to improve a design this way, test-driven
refactorings can help you produce a better design safely and
effectively.
Substitute
Algorithm
[F] is a good example of a refactoring that is
best implemented using test-driven refactorings. It
essentially
involves completely changing an existing algorithm for one that is
simpler and clearer. How do you produce the new algorithm? You
can't produce it by transforming the old algorithm into the new one
because your logic for the new algorithm is different. You can
program the new algorithm, substitute it for the old algorithm, and
then see if the tests pass. But if the tests don't pass, you're
likely to find yourself on a long date with a debugger. A better
way to program the algorithm is to use test-driven development.
This tends to produce simple code, and it also produces tests that
later allow you or others to confidently apply low-level or
composite refactorings.
Encapsulate Composite
with Builder (96)
is another example of a test-driven
refactoring. In this case, you want to make it easier for
clients
to build a Composite by simplifying the build process. A Builder,
which provides a simpler way of building a Composite, is where
you'd like to take the design. Yet if that design is far different
from the existing design, you will likely be unable to use
low-level or composite refactorings to produce the new design. Once
again, test-driven development provides an effective way to
reimplement and replace old code.
The refactoring
Replace Implicit Tree with Composite (178)
is
both a composite refactoring and a test-driven refactoring.
Choosing how to implement this refactoring depends on the nature of
the code you encounter. In general, if it's difficult to implement
the
Extract Class
[F] refactoring
on the code, the test-driven approach may be easier.
Replace Implicit Tree with Composite (178)
includes an example that uses test-driven refactoring.
Move Embellishment to
Decorator (144)
is not a test-driven refactoring; however,
the example for this refactoring shows how test-driven refactoring
is used to move behavior from outside a framework to inside the
framework. This example involves moving code around, so you might
think it would be more
convenient
to use composite refactorings to
implement it. In fact, because the changes involve updating
numerous
classes, it turns out to be easier to use test-driven
development to make the design transformation.
In your practice of refactoring, you're likely
to use low-level and composite refactorings most of the time. Just
remember that the "reimplement and replace" technique, as performed
by using test-driven refactoring, is another useful way to
refactor. While it tends to be most helpful when you're designing a
new algorithm or mechanism, it may also provide an easier
path
than
applying low-level or composite refactorings.
|