Other posts in the “Delivering high-quality software” series:
Nowadays, we use the software every day and almost everywhere. We require the software to work – and to work fast. I guess we all are annoyed by buggy software. The same is on the other side of a mirror – it is not that comfortable to investigate and fix production bugs under the pressure of time and upper management.
I am going to write a series of posts about making our life easier by producing high-quality software. If you are an experienced developer, I guess most of these good practices will be known to you – and that is awesome. If you are just starting your career, I think you will definitely find something useful here.
Today I will start with the simple rules of a simple code.
General rules of the clean code
Follow language conventions
If you write in Java, follow the well-known conventions of Java. If you move to another language, break your habit and do things differently – like it is done in this particular language. Even if these rules break the conventions of your favorite language. I know it is not always that easy – I have seen too many times C++ish code in Java written by ol’ good:) C++ devs. Looking at such examples can bring our memories back but anyway – try to stick with the well-known rules.
Follow project/company conventions
If the company or the project has different rules, stick with them. Even if they break language conventions. Maybe there is a reason behind that? If you want to fix that state, first discuss this with the team. The overall code consistency is pretty important and different code conventions in the same project are not what we are looking for.
KISS principle (“Keep It Simple, Stupid”) is not that stupid
Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live. Code for readability.
You should be able to understand your code quickly, even after a few months. The same applies to this poor intern that will have to fix bugs years after, often after midnight during a prod issue call. The code is read much more often than written. It shall be like a good book rather than a puzzle. How to achieve that? There are a few simple rules to follow:
- Languages evolve by purpose. Use declarative syntax instead of imperative one if possible to make the code more readable.
- Avoid mutating a state. It is much easier to reason about the code when most variables are final. The same applies to data structures or collections. If they are unmodifiable, understanding and debugging the code is far easier.
- Use meaningful names for variables, functions, classes, and so on. A bigger but more descriptive name is usually better than a shorter but ambiguous one.
- Design your names for searchability. Help the IDE to find your names.
- Avoid magic numbers. Extract them to a well-named variable.
- Declare variables close to their use. It is easier to understand what is going on.
- Be consistent with the naming, code format, indentation, and actually… whatever possible.
- Use proper types for the data. I guess most of us have seen software where almost everything is a
String. A name, ID, URL, even a number or money…
Functions and classes:
- Functions shall be small and pure. They shall do only what they are intended to do without any side effects (logging is also a side effect!).
- Dependent and similar functions shall be close. Try to place functions to flow in the downward direction.
- Classes and methods should follow the SRP (Single-responsibility Principle) and have only one reason to change. If the class or function does too much – divide the logic into separate classes/functions.
- Decrease the Cyclomatic Complexity of your code as much as possible. Avoid using multiple parameters in functions or multiple returns. Do not use flags at all – it is better to write a few functions than one with the flag which modifies its behavior.
- If you need to pass multiple parameters to a method, think about moving them to a separate object.
- Encapsulation really matters. Use a “Demeter Law”. Hide the internal structure of your class, expose only the API which is really necessary.
- Separate data from objects, avoid these “hybrid” objects that store data and behavior.
Fortunately, lots of these checks can be done automatically. See this post if you want to find out more:
Automate your code quality in a few minutes.
- Try to avoid negative conditions. If conditions get too complex, extract them to a separate method.
- Take care of the proper exception handling. At least remember about the proper logging of exceptions – this will pay back significantly in production.
- Apply a proper encapsulation also to classes in packages. The package should ideally expose a simple class with one or a bunch of API methods – and nothing more. This helps in testing. Instead of writing tests for each class, write only tests that check the exposed API. You will gain the freedom to modify the internal structure and logic without the need to rework failing tests.
- Encapsulation also matters in the DI container (like Spring). It is easy to put everything as a bean into a context and autowire things everywhere. Unfortunately, it is not that easy to unwire spaghetti code like this. Put only important components into the DI context, encapsulate everything else.
Apply KISS, SOLID, YAGNI, DRY principles (…did I miss anything else?)
Keep things simple. Do not repeat code in multiple places but extract it to a common method. Do not write a functionality if you are not sure if you really need it (most likely you will not). Apply SOLID principle in action and always recall it before interviews:)
Documentation and comments
- Code shall be self-explanatory therefore avoid using comments. However, use comments if something tricky has to be described and the code itself will not document that enough.
- Javadoc is a good thing – sometimes even a sentence describing a class or a method can be beneficial. Especially considering that you can validate the syntax of the Javadoc by the build tool (like
gradle javadocthanks to the java plugin).
- Documentation can be a life-saver. It is true that it has to be maintained… but the overall effort is worth the price. Not everything can be documented inside Javadoc, comments, or tests. A simple README markdown file (like we use in GitHub) and ADR for the architecture will often suffice yet be very helpful in critical moments.
- It is good idea to place badges in the README. A quick glance will inform us about the code coverage, build status,
used frameworks, and so on.
A VCS history is pretty important in getting an understanding of how the project evolved and at what point in time we can start tracking particular bugs. The structure of commits is often missed in code reviews, though. I have dedicated a separate post to this topic:
Commit to the quality of your commits.
Do not reinvent the wheel
If there is an existing library you can use to solve your problem, most likely it is a good idea to use it. It is also probable that the already written solution will be more optimal and far better tested compared to what we can quickly write. Additionally, if the problem solved by an external solution can be solved by the pure language API, we shall go for it. Unfortunately, everything comes with a price and the same rule applies to the usage of external dependencies. Each library increases security risks with our software and the number of potential issues to occur. So generally speaking, use 3rd party solutions but use them wisely.
Design patterns (and abstractions in general) are cool if not too much
Maybe you have heard a story about a rookie developer who read a book about the design patterns, the next day applied lots of them in the project at work, and was smashed later on by “enlightened” seniors in the code review. I would argue with that story that the problem of over-engineered solutions is not because of the high usage of design patterns but the usage of unnecessary abstractions at all. We like to write abstractions that will make our code “agnostic” (i.e “database-agnostic”), ready to change, decoupled. We always need to think about the trade-offs of such solutions, especially in terms of the simplicity of the code. Do we really need this abstraction? Are we going to replace this layer in the future? Is it worth maintaining this extra code? These are the questions we always have to ask first.
Apply small refactoring (almost) all the time
Always leave the code better than you found it
— Scout rule
Apply the “scout rule” most of the time (unless you work with a legacy code that will be thrown away anyway). With simple code improvements, the overall code quality gets much better over time.
Test your code
Last but not least, test your code. Ideally, use Test Driven Development, but any tests (even written years after the implementation) are better than nothing.
Automate the boring stuff
Lots of things mentioned here can be automated. Code formatters, IDE plugins, and various tools can find places when we break language conventions, use deprecated API, or most likely made a bug. See this post if you want to find out more: Automate your code quality in a few minutes.
By applying these simple rules, we can make our code much more pleasurable to read and work with. There are much more good practices and a Clean Code: A Handbook of Agile Software Craftsmanship from Uncle Bob is a good read. If you have already read this book, this summary can quickly refresh your knowledge: Summary of ‘Clean code’ by Robert C. Martin.