Make Uncle Bob be proud of your code
Writing clean code is no easy task: it requires a bit of time for analysis, a pinch of care and lots of refactoring.
When talking about clean code and good practices in software development, one of the most well-recognized referents is Robert C. Martin who is also known, colloquially inside tech circles, as Uncle Bob. Martin, among others such as Martin Fowler or Kent Beck, is one of the gurus in that field who promoted SOLID principles back in the early Noughties as a summary of important things to consider while programming. He is also author of the Bible of clean code “Clean code: A Handbook of Agile Software Craftsmanship”.
So, the very first question is… what is considered clean code?
Probably you, as a software developer, already have your own idea of what is good and what is bad in terms of coding, but there are many definitions for clean code.
Although with a nose for this sort of thing, and a bit of common sense, it shouldn’t be that difficult to, at least, catch a few of what are known in the trade as “code smells”. Let’s put all the insights together so here there are some useful tips to bear in mind while programming:
- Keep it simple. A readable and expressive code is more maintainable and always easier to understand. Using descriptive and meaningful variable and function names is a good way to achieve this goal. Horizontal and vertical spaces may help as well to group elements logically and keep code clearer. “Any fool can write code that a computer can understand. Good programmers write code that humans can understand. (M. Fowler)”.
- One thing at a time. Bad code does too much, so we should try to reduce the complexity of our code by doing just one thing at a time. Avoid writing large methods and classes that do the whole thing by themselves, instead try to split them into shorter ones with their own bounded responsibilities.
- Use as few elements as possible. We don’t want our code to have lots of methods, variables, conditions, loops, parameters, etc. It’s preferable to only use elements that are strictly necessary to solve the problem and keep code readable. Fewer lines of code does not necessarily guarantee good code, but the two normally go hand in hand. Smaller is better.
- Do not repeat yourself. Duplicated pieces of code make it redundant and lead to unmaintainable code, which is much more open to bugs or malfunctions.
- Abstraction of similar objects. Try to abstract common logic and implement more complex functionality on top of the abstraction when needed. In this way we can hide lower level details and focus on a higher level logic.
- Minimal dependencies. Keeping fewer dependencies between classes is beneficial in the sense that resultant code becomes more reusable by other modules, because it’s not tightly coupled with anything else.
- Tested. Unit and integration tests are a must in order to guarantee that our code is working properly and meeting good quality gates. Automated tests will always assure that code still works when adding new features or after a refactoring process.
Putting TDD in practice could be a very good approach on the road to writing clean and tested code, since Test Driven Development has unit tests at its core and relies heavily on the refactoring process during development to assure the good quality of code.
Now we all have the same overall idea about what clean code should look like, let’s talk about SOLID principles; the five proposed rules that are highly recommended for developers to follow when writing code.
Which are the SOLID principles and what exactly do they mean?
SOLID is an acronym promoted by Uncle Bob, who tried to establish a bunch of basic concepts, or best practices, in object oriented programming. It’s recommended that software engineers follow them in order to produce better, more legible and reusable code. Each individual letter in the word stands for a principle:
S — stands for “Single Responsibility”. You have probably seen some of those mastodon super large classes with hundreds and hundreds lines of code. To avoid that, this first principle states that every class must conform to a single, easy and concrete purpose and all their methods must be related to that purpose.
O — stands for “Open/Closed”. In order to improve code maintainability one should be able to modify classes by extending their functionality rather than of modifying their code. So, this is a software design that is “Open” to extend functionality but “Closed” to code modifications.
L — stands for “Liskov Substitution”. This principle states that, thanks to polymorphism in OOP, we could replace a parent class with any of its subclasses and everything should keep working the same way. In other words, it should be possible to treat child classes as the parent class itself. Therefore, we have to bear in mind that we can’t override base class methods in a way that could lead to a malfunction if the child class is treated as its base.
Following this principle would be useful, for instance, when we need to pass parameters to a method without knowing its concrete implementation. There we could declare the parameter as a base class reference and actually be passing objects of different underlying child classes.
I — stands for “Interface Segregation”. Again, related to keeping code as simple as possible, this principle states that defining multiple interfaces with a concrete purpose and a few methods is considered a better approach than having a huge single interface with many methods which may be rarely used. So, we don’t want to force the implementation of methods that won’t be used.
D — stands for “Dependency Inversion”. As we mentioned before, an important thing when developing in OOP is to keep classes loosely coupled. This means that classes should have as few dependencies as possible between them, in this way our code becomes more reusable and maintainable. To achieve this, we can make use of abstractions to interact with other classes without really knowing their details. Higher level classes should not depend on lower level classes implementation, instead both should depend on abstractions. If we make use, for instance, of a dependency injection strategy we could take advantage of this by injecting mock objects when testing.
As we touched on before, writing clean code is no easy task especially when we have to face real-life issues. Even though experience could allow senior developers a better understanding of problems, and help them in finding the adequate solution, implementing clean code requires a sixth sense for its flavour. In addition, attempting to do both things at the same time and on the first try is not always such a realistic idea — a better solution would be to make use of refactoring.
Refactoring is a mechanism that lets us join all these concepts together.
Refactoring is one of the pillars of Test Driven Development (TDD) but, even if we are not used to working in that way, often we need to refactor our legacy code. Refactoring means simplifying, standardizing and ultimately leads to better, more understandable and maintainable code.
The refactoring technique was widely described by Martin Fowler in his book “Refactoring: Improving the Design of Existing Code” which also has dozens of refactoring examples. This is another reference book that must be in every developer’s library.
As a software craftsman, during the refactoring process, we should introduce small changes in the code while preserving existing behaviour. Those small changes which may fix, for instance, some code smell or even enable us to apply new design patterns will, all in all, create a significant change in our code’s flavour.
Therefore, the importance of having a widely tested code becomes more clear. This will assure that everything keeps working fine and on the other hand will alert us to any changes that could potentially cause some temporary unexpected behaviour.
To go into a little more detail about unit tests, they should conform to another acronym: FIRST.
- Fast: Unit tests should be fast because we want to execute them frequently.
- Independent: Test conditions shouldn’t rely on any previous tests.
- Repeatable: We should be able to execute them in any environment.
- Self-Validating: No need for further validations to ensure whether the test passed or not.
- Timely: Ideally tests should be written just before writing code.
To summarise, by refactoring our code, we will meet a better software solution in each iteration. And this translates into easier and faster future modifications with less effort and even — if we talk in terms of business costs — to cheaper implementation of new features. In short, cleaner code helps us to develop at a much faster pace.
References
- Clean code: A Handbook of Agile Software Craftsmanship
[www.amazon.es/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882] - Refactoring: Improving the Design of Existing Code
[www.amazon.es/Refactoring-Improving-Existing-Addison-wesley-Signature/dp/0134757599] - SourceMaking
[www.sourcemaking.com] - Tutorials Point
[www.tutorialspoint.com/developers_best_practices/index.htm] - Filippo Buletto’s Blog
[filippobuletto.github.io/solid-java/] - Robert C. Martin
[en.wikipedia.org/wiki/Robert_C._Martin] - Martin Fowler
[en.wikipedia.org/wiki/Martin_Fowler_(software_engineer)] - Kent Beck
[en.wikipedia.org/wiki/Kent_Beck] - Wikipedia
[en.wikipedia.org]