For many years, I was a big fan of beautiful code. With Ruby, I was amazed by what I was able to do in one line of code. With Java, I loved that I could stream, map, and then collect! But as time passed, I read about many impressive optimizations, small tricks, and clever algorithms, and I got less and less excited about these discoveries.
The reason is that they almost always fell into the category of some very elegant expressiveness in a new language (Ruby converts from Java can attest to this) or a technique that I'm not going to ever use. In other words, I was chasing baubles.
When I joined the Ruby community, I heard a lot about code clarity and code readability, as agile methodologies recommended throwing away almost all project documentation to encourage the code itself to be simpler and more intelligible. So my aesthetic sense turned toward code clarity.
Developers must realize that they’re not writing code for themselves, but for others. When there's less documentation, a new developer entering the environment needs to be able to read and understand the code.
Image courtesy of First Class Thoughts
But code readability doesn't only affect newcomers. When developers initially write code, everything is fresh in their minds; their knowledge of the system is detailed because they have read requirements and technical specs and have worked on them for some time. A year later, however, no one knows the system that well anymore, so developers need to read the code again and learn from it.
If you struggle to read the code, how the hell are you meant to fix it? —Ben Hosk
1. Naming
Don’t write complex, beautiful code. The simpler your code is, the fewer bugs it's likely to have, and the less time you'll have to spend debugging. Imagine how hard it would be to debug a data process inside a stream map.
Code should do only what it needs to do without being cluttered by tons of abstractions and comments. Naming is hard, but it’s important. For example, a method that updates some data for an object should have a name that screams "I update x data for x object, and that's all I do!" — but this is not SRP, which I’ll explain at another time.
Names of variables and functions should be distinct and provide a general idea of what they do. The important thing about naming is that the name should mean something to your team. Because of this, it should conform to the conventions chosen for the project, even if you don’t agree with them. For instance, if every request for a record in the database starts with “find," e.g “findUser,” then your team might be confused if you come to the project and name your database function “getUserProfile” because this is what you are used to. Try group naming when possible. For example, if you have many classes for input validation, writing “Validator” as the suffix for the name may quickly provide information as to the purpose of the class.
If a variable or constant might be seen or used in multiple places in a body of code, it is imperative to give it a search-friendly name and avoid comments showing a bad code.
int d; // elapsed time in days
int elapsedTimeInDays;
Good code should be understandable without a line of comments; it should be self-documenting.
Commented code is confusing. Did someone remove it temporarily? Is it important? When was it commented? It’s dead, take it out of its misery. Just remove it.
2. Patterns, my friends, patterns!
Back in 2013, Sandi Metz created some rules for developers:
There are four rules
Classes can be no longer than one hundred lines of code;
Methods can be no longer than five lines of code;
Pass no more than four parameters into a method. Hash options are parameters.
Controllers can instantiate only one object. Therefore, views can only know about one instance variable and views should only send messages to that object (@object.collaborator.value
is not allowed).
For me, the most interesting rule is the five-lines-per-method rule.
We agree that if
, else
, and end
are all lines. So in an if
block with two branches, each branch can only be one line:
public void validateActor() {
if (actor_type == 'Group') {
userMustBelongToGroup()
} else if (actor_type == 'User') {
userMustBeTheSameAsActor()
}
}
Adhering to the five-line rule means that we would never use else
with else if
.
Having only one line per branch urges us to use well-named private methods to get work done. Private methods are great documentation. They need very clear names, which forces us to think about the content of the code we are extracting.
You should break these rules only if you have a good reason or your pair lets you — Sandi Metz
3. Guard Clauses May Be All You Need!
There is as a great discussion happening on how if-statements should be used for better code readability. Usually, the conversation boils down to an opinion that is completely subjective and aesthetic. But it is certain that the most documented case is the guard clause, also known as assert or precondition. The main idea is that when you have something to assert in the beginning of a method, you should do this using the fast return.
Image courtesy of Medium
Let’s start with a snippet of code (not using a guard clause):
public void updateData(Data data) {
if (data == null) {
return;
} else if (data.isPresent()) {
// do stuff here
} else {
trow new Exception(msg);
}
}
Now look at the following one (using a guard clause):
public void updateData(Data data) {
if (data == null) return;
if (!data.isPresent()) trow new Exception(msg);
// do stuff here
}
It’s pretty common to find large if-blocks in a codebase due to the fact that our brains function differently: we tend to think “do stuff if it’s enabled,” not “do stuff if it’s enabled, but if it’s not enabled, do nothing.”
The above tips are just a few examples, albeit important ones, of how we can write readable code. The underlying principle is to always write code that is simple to read and can be easily understood by other developers, because the time and resources you'll spend trying to decipher confusing code is much greater than any gain you'd get from optimizations. I believe that this is the way to make things better, not only for us, but also for those who will read our code later.
You also need to learn when to not use certain patterns: since each one solves a specific problem/issue in the applications, their usefulness can be affected by many different factors like the size of the project, the number of people working on it, the time/cost constraints or the complexity required for the solution. Some patterns have been named anti-patterns, like the Singleton pattern, because even though they provide some solutions, they also introduce some issues in certain cases.
There are many ways to be right. If you are convinced beyond a doubt that you should always use DRY in your applications or that it's better to always keep using only a well-known design pattern, that's okay.
You may choose the path that is right for you and your team; for coding, there are no indisputable truths--the takeaway is to always think about how other developers will read your code.
if (readableCode()) {
beHappy();
} else {
refactor();
}