Write Simple and No-surprise Code

And improve your productivity in the long run

Liu Tao
Liu Tao
Jul 3, 2019 · 5 min read
Image for post
Image for post
Photo by Khai Sze Ong on Unsplash

Why should we write simple code

Of course, there are cases that we need to use advanced algorithms, maybe for performance reason. Then as a last resort, we should encapsulate them with boring interfaces, with a meaningful name and minimum parameter.

First thing first, try your best to avoid code

To write less, we have to think much much more. Of course, thinking takes more time for now, but this is only a one-time investment. In the long run, you or your organization will be debtless.

Whenever we get any code working, we should not stop there. Believing the working code is best one prevents us from finding a better solution. Instead, we need to read through our working code many times and think hard about places to improve. Often, StackOverflow is the place to find a more elegant solution. In the beginning, we may not know exactly what better solutions are. But at least, we should develop a sense of code complexity, and that’s our motivation to find a simple solution. Remember, never stopping at your first working code, any working solution is just a start point for a better one.

If possible, use annotation

For example, instead of doing null checks by ourselves, we can use @NonNull annotation. Instead of writing our own get/setter or constructor, we can use @Getter, @Setter and @AllArgsConstructor. Instead of construct log on our own, we can use @Log to construct it.

All of those seem not a big deal by themselves. However, complexity is accumulated over time, and it’s always better to write a little bit less code.
Also, annotation is more declarative than code. The maintenance cost is smaller in the long run.

Use control only when unavoidable

if (a != null && 
a.getItem() != null &&
a.getItem().getValue() != null) {
return process(a.getItem().getValue());
} else {
return false;
}

With Java 8’s Optional, we can write something like the following:

Optional.ofNullable(a)
.map(A::getItem)
.map(Item:getValue)
.map(this::process)
.orElse(false);

Although the second approach has more lines of code, it is still more readable. For cognitive load, the line number doesn’t matter that much, it’s the decision points that make code digestion much harder.

Considering the following example, we can put all logic inside forEach’s lambda:

list.streams().forEach(p -> {
if (p.isValid()) {
process(p);
}
})

Alternatively, we can split the logic and do the filtering in between.

list.streams()
.filter(P::isValid)
.forEach(this::process)

In general, the boring code should have only one execution path and it’s very easy to follow through.

When it comes to OOP, polymorphism is one elegant way to get rid of if/else, we can group logic into subclasses and the compiler or runtime help us pick the expected execution path. So we don’t need to do if/else explicitly in our code. In my option, this is the right use case for inheritance. Inheritance is not the right tool to model complex business domain. Instead, we can use it to replace if/else, or we can use it to share some utility functionality between subclasses.

Extract more data from your code

let caseInSensitive = (val1, val2) => {
val1 = _.capitalize(val1 || “”);
val2 = _.capitalize(val2 || “”);
return val1 === val2;
}
let normal = (val1, val2) => {
return val1 === val2;
}
let properties = [{
“path”: “addressLine1”,
“comparator”: caseInSensitive
},{
“path”: “streetNumber”,
“comparator”: normal
}
]

Of course, there could more comparators and more properties. With all those defined, the implementation of address comparator is much simpler:

let addressComprator = (addr1, addr2) => {
return _.every(properties, prop => {
let val1 = _.get(addr1, prop.path);
let val2 = _.get(addr2, prop.path),
return prop.comparator(val1, val2);
});
}

As you can see, data is more declarative than code, since it doesn’t have any decision points inside. The more data we can get from the logic, the more declarative our code is.

Separate business logic from error handling

if (dto != null) {
result1 = processA(dto);
}

if (result1 != null) {
result2 = processB(result1);
}

if (result2 != null) {
result3 = processC(result2);
}

return result3 != null;

With Java 8’s Optional, we can have something like below:

Optional.ofNullable(dto)
.map(this::processA)
.map(this::processB)
.map(this::processC)
.map(r -> r != null)
.orElse(false)

Clearly, the second approach is much more readable, because there is no decision point between statements, also the intermediate
state is not needed anymore. State and decision point are two sources of complexity.

Summary

The Startup

Medium's largest active publication, followed by +754K people. Follow to join our community.

Liu Tao

Written by

Liu Tao

Microservices developer in payment industry, https://www.linkedin.com/in/tao-liu-241b559/

The Startup

Medium's largest active publication, followed by +754K people. Follow to join our community.

Liu Tao

Written by

Liu Tao

Microservices developer in payment industry, https://www.linkedin.com/in/tao-liu-241b559/

The Startup

Medium's largest active publication, followed by +754K people. Follow to join our community.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store