Sometimes, during the TDD process, I end up with a constructor that has a long, unwieldy list of arguments. Most likely, I got there by adding the object's peers (other objects it has to communicate with) one at a time, and it got out of hand. This is not dreadful, since the process helped me sort out the design of the object and its neighbours, but now it's time to clean up. The object must still be constructed in a valid state as required by the application. I will still need all the current constructor arguments, so I should see if there's any implicit structure there that I can tease out.
One possibility is that some of the arguments together define a concept that should be packaged up and replaced with a new object that represents it. Here's a small example.
public class MessageProcessor { public MessageProcessor(MessageUnpacker unpacker, AuditTrail auditer, CounterPartyFinder counterpartyFinder, LocationFinder locationFinder, DomesticNotifier domesticNotifier, ImportedNotifier importedNotifier) { // set the fields here } public void onMessage(Message rawMessage) { UnpackedMessage unpacked = unpacker.unpack(rawMessage, counterpartyFinder); auditer.recordReceiptOf(unpacked); // some other activity here if (locationFinder.isDomestic(unpacked)) { domesticNotifier.notify(unpacked.asDomesticMessage()); } else { importedNotifier.notify(unpacked.asImportedMessage()) } } }
Just the thought of writing expectations for all these objects makes me wilt, which suggests that things are too complicated. A first step is to notice that the unpacker
and counterpartyFinder
are always used together, they're fixed at construction and one calls the other. I can remove one argument by pushing the counterpartyFinder
into the unpacker
.
public class MessageProcessor { public MessageProcessor(MessageUnpacker unpacker, AuditTrail auditer, LocationFinder locationFinder, DomesticNotifier domesticNotifier, ImportedNotifier importedNotifier) { … public void onMessage(Message rawMessage) { UnpackedMessage unpacked = unpacker.unpack(rawMessage); …
Then there's the triple of locationFinder
and the notifiers, which seem to go together. It might make sense to package them together into a MessageDispatcher
.
public class MessageProcessor { public MessageProcessor(MessageUnpacker unpacker, AuditTrail auditer, MessageDispatcher dispatcher) { … public void onMessage(Message rawMessage) { UnpackedMessage unpacked = unpacker.unpack(rawMessage); auditer.recordReceiptOf(unpacked); // some other activity here dispatcher.dispatch(unpacked); } }
Although I've forced this example to get it to fit within a blog post, it shows that being sensitive to complexity in the tests can help me clarify my designs. Now I have a message handling object that clearly performs the usual three stages: receive, process, and forward. I've pulled out the message routing code (the MessageDispatcher
), so the MessageProcessor
has fewer responsiblities and I know where to put routing decisions when things get more complicated. You might also notice that this code is easier to test.
When I'm extracting implicit components, I look first for two conditions: arguments that are always used together in the class, and that have the same lifetime. That usually finds me the concept, then I have the harder task of finding a good name.
As an aside, one of the signs that the design is developing nicely is that this kind of change is easy to integrate. All I have to do is find where the MessageProcessor
is created and change this
messageProceesor = new MessageProcessor(new XmlMessageUnpacker(), auditer, counterpartyFinder, locationFinder, domesticNotifier, importedNotifier);
to this
messageProceesor = new MessageProcessor( new XmlMessageUnpacker(counterpartyFinder), auditer new MessageDispatcher(locationFinder, domesticNotifier, importedNotifier));
Later I can cut down the noise by extracting out the creation of the MessageDispatcher
.