Sometimes, when you try to break up a long method, you run into a problem: you have to pass a bunch of local variables into the new method, some of which are both input and output. One solution is the “Replace Method with Method Object” refactoring, described in Fowler’s Refactoring book (1st edition), renamed to “Replace Function with Command” in the second edition.
To do this, extract the method into a class just for it, and promote your variables to field level. Then you can easily extract sub-methods for it.
I don’t use this refactoring super often, but when it applies, it simplifies both the original and the extracted class. I have an example today.
I’ve demonstrated this in a 7-minute video on YouTube.
Example: TuneBuilder
I’m writing a program to compare tunes, and I’m using abc notation (an informal standard) for the input format.
This format is moderately complex, so I have the parser gather information and feed it to the TuneBuilder. At the end, TuneBuilder gathers and transforms the data into a Tune.
TuneBuilder holds three different kinds of information:
- Tune metadata: e.g., title
- Initial conditions for the music: e.g., key and time signature
- Events: e.g., notes, bar lines, key changes
Turning events into measures is the hard part of building a Tune, and the make()
method is a long method.
Fighting the Dragon
I extracted a method from make(), but I was unhappy with the result.
fileprivate func switchToNewMeasure(
_ symbolsInMeasure: inout [Symbol],
_ parts: inout Parts,
_ currentPart: Character)
{
if symbolsInMeasure.count > 0 {
let newMeasure = Measure(timeSignature, symbolsInMeasure)
parts[currentPart]!.append(newMeasure)
symbolsInMeasure = []
}
}
First, it was clear that the new method was very different from the others. The others were about gathering information, not processing it.
Second, the extracted method needed several parameters, some of them in-out parameters (since the variables were changed in the method).
If I promoted those variables to field level, then I’d have some fields whose lifetime was the lifetime of the object, and others that only made sense when running the method.
Furthermore, I had more methods to extract, so the situation would only get worse.
Introducing the Method Object
For “Replace Method with Method Object”, I followed these steps:
- Create a new class, and move the
make()
method to it.
- For fields it needs from the original object, pass those as arguments to the method (or you could pass them to the constructor).
- Have the original make() delegate to a call on the new object.
You could just make the local variables be fields right away, but I tend to change them on a pull basis. (That way, “temporary” local variables don’t end up as fields.)
- Extract a method from the new
make()
.
- If it needs in-out parameters:
- Note which variables that applies to
- Revert the extract
- Promote those variables to be fields
- Re-extract your method
- Repeat
The Result: A Method Object
This results in a much nicer TuneBuilder:
We can freely promote local variables in EventToParts, and extract new methods as needed.
Conclusion
Keep an eye out for methods that are hard to split – they may be candidates for a method object!
References
Refactoring: Improving the Design of Existing Code, by Martin Fowler et al. (This version uses Java for examples; there’s a newer edition that uses JavaScript.)