Recently at work I was asked to review some code with a coworker who was concerned with the way her code look. It didn’t seem “right” to her, and she wanted to me to look at it and see if there was a better way.
The code had unit tests and FitNesse tests and the code coverage statistics revealed that it was covered 99%. It was a complex piece of business logic and it did what it was supposed to do according to the tests; they all passed. But still, my coworker didn’t feel like she had written it as well as it could be written.
“It seems too procedural,” she said. So I took a look.
It was a single class that consisted of one method; it’s job was to perform calculations on some values on a given business object. The method consisted of 433 lines of code. Sure, it was broken up into smaller private methods that were a bit more descriptive, but it was still 433 lines of code for a single method call. And there were comments littered throughout the code informing the reader of what it was actually doing. See, despite the dozen or so private methods that had descriptive names, you still couldn’t follow it by just reading the code. It was too complex, a by-product of the code being procedural.
The length and complexity of the code wasn’t the only smell. The unit tests gave off their own oder as well. Some of the unit tests were as many as 30-40 lines of setup code; tons and tons of mocks just to test one path through the code.
To my coworker’s credit she recognized the deficiencies in her code. She knew it was too procedural, she knew the tests were too big and required too much setup, and she knew that the entire solution needed to be more object oriented.
She just wasn’t quite certain how to get there.
We started refactoring by first asking the question: What is this method doing? What is its responsibility? At first glance, it looked like it was supposed to do one thing. But when I prodded my coworker further we both realized it was actually doing six different things. Six different things in one method; that was too much work.
So we left her code behind and started with a new concrete implementation of the main class, implementing the interface that the original class implemented. We began by asking questions about what each responsibility inside that giant method call was, and what might a class look like that would implement just that single responsibility. The outer class and the single method call would then become a simple coordinator between all the smaller classes.
When we finally started coding, we creating those classes, test first. Each little class got its own test fixture and group of unit tests. The logic in the 433 line of code method started to get pushed off into he smaller objects. Since they were small classes responsible for a single responsibility, the code that got pushed off actually became smaller. There was less to do - less branching - so the result was a tight unit of code that did one thing and did it well.
Slowly, piece by piece, bit by bit, the giant 433 line method got broken up and reformed into a new method that called the tiny classes where appropriate.
We didn’t finish the entire refactoring that day, but we were well on our way to a much better design. And the process was a great learning tool. My coworker got to see how procedural code could be turned into an object oriented design, and how certain small classes could be reused in the larger algorithm.
At the end of the day my coworker turned to me and said, “So I guess if a method is larger than my screen, I need to rethink it, huh?”
And I thought: that’s a great rule of thumb. It is one that I have followed for quite some time in a sort of instinctive way. If I find myself writing a method that ends up being larger than my screen in Visual Studio, I know there’s a chance to refactor to a better design. Methods shouldn’t be that big.
When we left work that day I felt a great sense of accomplishment, and I know my coworker did too. She learned a lot, and our code quality improved significantly. The FitNesse tests will still pass when we complete the refactoring, so the end result won’t have changed much. But a big win still occurred: our design was better and would be much more maintainable moving forward.