Wrap Method
Adding behavior to existing methods is easy to do, but often it isn’t the right
thing to do. When you first create a method, it usually does just one thing for a
client. Any additional code that you add later is sort of suspicious.
Advantages and Disadvantages
Wrap Method is a good way of getting new, tested functionality into an application when we can’t easily write tests for the calling code. Sprout Method and
Sprout Class add code to existing methods and make them longer by at least
one line, but Wrap Method does not increase the size of existing methods.
Another advantage of Wrap Method is that it explicitly makes the new functionality independent of existing functionality. When you wrap, you are not
intertwining code for one purpose with code for another.
The primary disadvantage of Wrap Method is that it can lead to poor names.
In the previous example, we renamed the pay method dispatchPay() just because
we needed a different name for code in the original method. If our code isn’t
terribly brittle or complex, or if we have a refactoring tool that does Extract
Method (415) safely, we can do some further extractions and end up with better
names. However, in many cases, we are wrapping because we don’t have any
tests, the code is brittle and those tools aren’t available.
Wrap Class
The class-level companion to Wrap Method is Wrap Class. Wrap Class uses
pretty much the same concept
...
This technique is called the decorator pattern. We create objects of a class
that wraps another class and pass them around. The class that wraps should
have the same interface as the class it is wrapping so that clients don’t know
that they are working with a wrapper. In the example, LoggingEmployee is a decorator for Employee. It needs to have a pay() method and any other methods on
Employee that are used by the client.
The Decorator Pattern
Decorator allows you to build up complex behaviors by composing objects at runtime.
For example, in an industrial process-control system, we might have a class called
ToolController with methods such as raise(), lower(), step(), on(), and off(). If we
need to have additional things happen whenever we raise() or lower() (things such as
audible alarms to tell people to get out of the way), we could put that functionality
right in those methods in the ToolController class. Chances are, though, that wouldn’t
be the end to the enhancements. Eventually, we might need to log the number of times
we turn the controller on and off. We might also need to notify other controllers that
are close by when we step so that they can avoid stepping at the same time. The list of
things that we can do along with our five simple operations (raise, lower, step, on and
off) is endless, and it won’t do to just create subclasses for each combination of things.
The number of combinations of those behaviors could be endless.
The decorator pattern is an ideal fit for this sort of problem. When you use decorator,
you create an abstract class that defines the set of operations you need to support.
Then you create a subclass that inherits from that abstract class, accepts an instance
of the class in its constructor, and provides a body for each of those methods. Here is
that class for the ToolController problem:
abstract class ToolControllerDecorator extends ToolController
{
protected ToolController controller;
public ToolControllerDecorator(ToolController controller) { this.controller = controller;
}
public void raise() { controller.raise(); }
public void lower() { controller.lower(); }
public void step() { controller.step(); }
public void on() { controller.on(); }
public void off() { controller.off(); }
}
Wrap Class
This is a fine way of adding functionality when you have many existing callers for a method like pay(). However, there is another way of wrapping that is
not so decorator-ish. Let’s look at a case where we need to log calls to pay() in
only one place. Instead of wrapping in the functionality as a decorator, we can
put it in another class that accepts an employee, does payment, and then logs
information about it.
Here is a little class that does this:
class LoggingPayDispatcher
{
private Employee e;
This class might not look very useful, but it is. You can subclass it and override any or
all of the methods to add additional behavior. For example, if we need to notify other
controllers when we step, we could have a StepNotifyingController that looks like
this:
public class StepNotifyingController extends ToolControllerDecorator
{
private List notifyees;
public StepNotifyingController(ToolController controller,
List notifyees) {
super(controller);
this.notifyees = notifyees;
}
public void step() {
// notify all notifyees here
...
controller.step();
}
}
The really neat thing is that we can nest the subclasses of
ToolControllerDecorator:
ToolController controller = new StepNotifyingController(
new AlarmingController
(new ACMEController()), notifyees);
When we perform an operation such as step() on the controller, it notifies all notifyees, issues an alarm, and actually performs the stepping action. That latter part,
actually performing the step action, happens in ACMEController, which is a concrete subclass of ToolController, not ToolControllerDecorator. It doesn’t pass the buck to anyone
else; it just does each of the tool controller actions. When you are using the decorator
pattern, you need to have at least one of these “basic” classes that you wrap around.
Decorator is a nice pattern, but it is good to use it sparingly. Navigating through code
that contains decorators that decorate other decorators is a lot like peeling away the
layers of an onion. It is necessary work, but it does make your eyes water.
...
Summary
In this chapter, I outlined a set of techniques you can use to make changes without getting existing classes under test. From a design point of view, it is hard to
know what to think about them. In many cases, they allow us to put some distance between distinct new responsibilities and old ones. In other words, we
start to move toward better design. But in other cases, we know that the only
reason we’ve created a class is because we wanted to write new code with tests
and we weren’t prepared to take the time to get the existing class under test.
This is a very real situation. When people do this in projects, you start to see
new classes and methods sprouting around the carcasses of the old big classes.
But then an interesting thing happens. After a while, people get tired of sidestepping the old carcasses, and they start to get them under test. Part of this is
familiarity. If you have to look at this big, untested class repeatedly to figure out
where to sprout from it, you get to know it better. It gets less scary. The other
part of it is sheer tiredness. You get tired of looking at the trash in your living
room, and you want to take it out. Chapter 9, I Can’t Get This Class into a Test
Harness, and Chapter 20, This Class Is Too Big and I Don’t Want It to Get Any
Bigger, are good places to start.
Comments
Post a Comment