How to write code so that it grows in height, not in width

Victor Melnik
5 min readNov 10, 2020

Every time we add a new if or for to code, we have to increase the indent of the condition body or loop. The more nested loops and conditions, the broader your code becomes, and the more difficult it becomes to read. In this short article, I’ll show a few techniques by which you can avoid increasing indentation. Your code will be clearer, and you’ll never see horizontal scrolling.

Rule 0. Don’t listen to me, listen to your style guide

First of all, let me remind you of the most important thing – code style. If you’re working in a team, then it’s important to write code so that you don’t switch from style to style when reading it (remember, many developers can work on the same file). Therefore, you should agree on the coding style: how to name variables, what is the indent size in spaces or tabs, where to put curly brackets, and so on. If you write code alone, you can still use the coding style defined by the creators of the programming language or the community. Stick to it.

Rule 1. Put else as close as possible to if block

Sometimes you scroll through the code, jump from one method to another and see this:

    // 20 more lines before this
String[] params = text.split(" ");
members.clear()
members.add(params[1]);
members.add(params[2]);
tournamentEnabled = true;
markup = new Markup();
rows = List.of(members);
markup.setRows(rows);
markup.setAction("tournament");
status = Message.create()
.withText("Let's go!")
.withMarkup(markup)
.toChat(chatId)
.send();
validateStatus(status);
} else {
Log.error("Empty command");
}

And everything seems to be fine, but as soon as your eyes go down to the line } else {, you fall into a stupor and ask yourself: if there is an else, then what was the if? And you have to scroll up to understand the entire block:

void handleMessage(Message message) {
String text = message.getText();
if (text != null) {
// 35 lines of code
} else {
Log.error("Empty command");
}
}

You can invert the condition and swap the body of if and else blocks so that the if body becomes shorter than the else body. This will save us from hanging else statements.

void handleMessage(Message message) {
String text = message.getText();
if (text == null) {
Log.error("Empty command");
} else {
// 20 more lines before this
String[] params = text.split(" ");
members.clear()
members.add(params[1]);
members.add(params[2]);
tournamentEnabled = true;
markup = new Markup();
rows = List.of(members);
markup.setRows(rows);
markup.setAction("tournament");
status = Message.create()
.withText("Let's go!")
.withMarkup(markup)
.toChat(chatId)
.send();
validateStatus(status);
}
}

Rule 2. if/else can be replaced with if-return

In the previous code, you can reduce the indent of the else block if you put return at the end of the if block:

void handleMessage(Message message) {
String text = message.getText();
if (text == null) {
Log.error("Empty command");
return;
}
// 20 more lines before this
String[] params = text.split(" ");
members.clear()
members.add(params[1]);
members.add(params[2]);
tournamentEnabled = true;
markup = new Markup();
rows = List.of(members);
markup.setRows(rows);
markup.setAction("tournament");
status = Message.create()
.withText("Let's go!")
.withMarkup(markup)
.toChat(chatId)
.send();
validateStatus(status);
}

In this case, return doesn’t break anything, on the contrary, it eliminates unnecessary nesting. Of course, it may not be obvious that this is an if/else statement or someone may be against such code, considering that the method should have one input and one output. And they will be right, so the decision should be dictated by your style guide.

The same trick works with if/else if/else:

if (command.startWith("/help")) {
Message.create()
.withText("Help message")
.toChat(chatId)
.send();
} else if (command.startWith("/clear")) {
data.clear();
Message.create()
.withText("Cleared!")
.toChat(chatId)
.send();
} else if (command.startWith("/add") && isAdmin) {
data.add(params[1]);
sendStatusNessage();
} else if // ...

With if-return it would be:

if (command.startWith("/help")) {
Message.create()
.withText("Help message")
.toChat(chatId)
.send();
return;
}
if (command.startWith("/clear")) {
data.clear();
Message.create()
.withText("Cleared!")
.toChat(chatId)
.send();
return;
}
if (command.startWith("/add") && isAdmin) {
data.add(params[1]);
sendStatusNessage();
return;
}
// ...

Although in this case, the indent remains the same, blocks of the same type stand out visually better.

Rule 3. if/else can be replaced with if-continue inside loops

To remove else inside loops, you should use continue instead of return. Before:

for (Members member : members) {
if (member.isBlocked()) {
Log.warning("Member {} is blocked", member);
tournamentMembers.remove(member);
if (tournamentMembers.size() > MIN_MEMBERS_COUNT) {
tournamentEnabled = false;
}
} else {
Member opponent = tournamentMembers.pop();
tournamentEnabled = true;
Message.create()
.withText("Tournament started. Your opponent is {}", opponent)
.toChat(member.getId())
.send();
Message.create()
.withText("Tournament started. Your opponent is {}", member)
.toChat(opponent.getId())
.send();
Message.create()
.withText("Tournament started. {} vs {}", member, opponent)
.toChat(chatId)
.send();
}
}

By adding continue to the end of the if body, we get a working code with less indentation:

for (Members member : members) {
if (member.isBlocked()) {
Log.warning("Member {} is blocked", member);
tournamentMembers.remove(member);
if (tournamentMembers.size() > MIN_MEMBERS_COUNT) {
tournamentEnabled = false;
}
continue;
}

Member opponent = tournamentMembers.pop();
tournamentEnabled = true;
Message.create()
.withText("Tournament started. Your opponent is {}", opponent)
.toChat(member.getId())
.send();
Message.create()
.withText("Tournament started. Your opponent is {}", member)
.toChat(opponent.getId())
.send();
Message.create()
.withText("Tournament started. {} vs {}", member, opponent)
.toChat(chatId)
.send();
}

Rule 4. Avoid unnecessary changes

If the code is clear enough, it’s better to leave it as it is. For example:

if (chatId == config.tournamentChat()) {
processTournamentChat(message);
} else {
processRegularCommand(message);
}

shouldn’t be replaced with:

if (chatId == config.tournamentChat()) {
processTournamentChat(message);
return;
}
processRegularCommand(message);

So readability only gets worse.

Rule 5. If nesting exceeds the allowed level, introduce a new method

If there are so many nested loops and conditions inside one method that the code has moved to the right side due to indentation and horizontal scrolling is about to appear, then it’s time to refactor and replace some parts of the method with another method. For example:

Don’t even try to understand this code. I just made it up as an example. Refactoring is a topic of a separate article, so I won’t consider this process step by step. The point is in the changes:

By applying these rules, we made the code less broad, but longer. This is normal, because it’s better to scroll up/down than up/down and left/right, and a high level of nesting together with curly braces also makes code difficult to understand. Besides, the code has become more understandable due to methods with meaningful names, but this is the merit of refactoring.

--

--

Victor Melnik

I love programming, open source, art, learning and sharing knowledge