Advanced Clean Code
15 March 2017, by Chris Arnott
This post is a quick overview of patterns I have picked up over the years that I aim to use when coding and usually end up pointing out in code reviews.
These aren’t hard and fast rules, but to me, this post forms an extension to Uncle Bob’s clean code (which you should read right now if you haven’t already!).
Remove side effects from methods
Side effects make reasoning about code difficult. Any method with a void return type is doing side effects and should be updated to return something.
A common issue I see is a method that takes a list, then returns void. In order to see what happened to that list, a reader now has to go into the method. A better solution is return what needs to happen to the list from the method, then use that to update the list. Now calling code is clearer about what it wants the method to do and the method no longer has to do a null check on its list argument (that’s left up to the calling code).
Make member variables immutable
This again comes down to side effects. It is much more difficult to have side effects in methods if you can’t change things. Instead, you’ll have to return whatever is created (this is what the change above achieved).
Immutable objects are also easier to reason about, as you can be sure that they have not changed since they were created.
Hindrances to following this are large classes with too much responsibility. Refactoring large classes into smaller classes results in this rule being easier to follow. Smaller classes also make code simpler to reason about.
Invert control with callbacks (or functions)
Good unit tests will result in code that is easier to comprehend and understand as they enforce small well thought through units of code.
To produce comprehensive (and useful) unit tests, it is important to pass dependencies into classes. You can extend this pattern by also ensuring you pass dependencies into methods.
Here, dependencies are not just objects that will be called, they are also functions that might exist at a different abstraction level. When writing in a language that supports passing functions as arguments that will solve the problem. In other languages use callbacks to define the path of code that should be executed once a lower level class has finished its calculation.
Move variables to the lowest possible scope
One key thing to look for in classes is member variables which are only being used in one place. These can be pulled out into local variables where they are used and then passed as a method argument rather than into a constructor.
However it is not just member variables that can be moved to lower levels of abstraction, it is often possible to extract a variable into a new class along with the functionality it uses. This functionality can be spread through a class, rather than being all in one place.
This tip is about ensuring that code maintainers only need to understand a subset of all the code in order to appreciate what each part is doing. This is achieved by ensuring related parts of the code are defined together.
Refactor code branches to keep methods small
Small methods are easy to understand (provided they are named well). A good way to keep methods small is to refactor lots. One place to pull functionality out from a method is when code branches.
For example, one method of reducing the size of a method is to extract the code from the truthful path of an if statement into its own method and the code from the else path of an if statement into a separate method. Both of these methods can then be well named, providing clarity to the functional aim of the code that is being executed.
Write tests at the right abstraction level
There are two things you can test in code. It’s functionality, or the wiring that pulls all the functionality together.
I used to believe that testing everything was the correct thing to do, but recently, I’ve started swaying towards only testing business logic.
One question I am often asked is “How do I test this?” There are two key answers to this question that result in cleaner, easier to maintain code.
“Do you need to test it?” This is related to the point above about functionality vs. wiring. If your test is only testing wiring, will it actually be adding much value? What regressions will it catch? Or is it actually just going to prevent rewiring in the future when requirements change?
“You’re not testing at the right level”. This is often the case when functionality is added to a class that is already large and doing too many things. The solution is to extract the new functionality into its own class and call the new class from the existing one. Unit tests can then be written around the new class to ensure that it is working as expected.