Java 8 Coding Guide
This coding guide was written for a Java 8 project, but contains principles that are valid in many languages. It is aimed at producing code that is easy to reason about, easy to validate and test, easy to maintain, and easy to avoid common bugs and pitfalls.
Remember that code is read many more times than it is written! We want to optimize for readability, comprehension, and reduced complexity!
null (or, Don't use
Null is famously the "billion dollar mistake" (and that's probably an
under-estimation!). Don't use
null as a way of representing error or failure,
and also don't use
null as a way to avoid passing a certain argument to a
function. Essentially you should never explicitly use a
null value unless
you are checking that something is not
null. Even in those cases, it
should probably be handled better by an
Expressions over statements
Expressions are declarative and evaluate to (and return) a value, while statements are imperative bits of code that perform actions. Because of this, expressions are easier to reason about, debug, and verify correctness for.
A simple example is an
if statement, vs an
If in java
does not return a value, nor does java enforce that an
if have an
Additionally, if there is an
else branch, it does not have to result in
the same type of value as the
if branch. This makes if statements hard to
reason about and understand. The ternary form
predicate ? value : value is
In a language like Java, statements will be required, but we should isolate those to their own, small, methods, and keep their surface area as small as possible to reduce risk and complexity from these parts of the code.
Avoid explicit loops
With Java 8, we have streams, and should no longer need explicit loops. Loops are very repetetive, and error prone, so we should leverage abstractions that handle looping for us to avoid bugs, verbosity, and complexity in our code.
There may be some cases where loops are more desirable (possibly for performance), but they should be avoided until it is clear they are actually needed.
Use immutable data structures
We should leverage
guava's immutable data structures as much as possible to
avoid complicated bugs related to mutating state.
Additionally, we should leverage Lombok's
@Value objects as much as possible
to make our own data objects immutable.
Don’t throw exceptions - use values!
In general, we should never throw an exception of our own. Obviously we will use libraries that throw exceptions. These exceptions should be caught directly in the function/method using the library, and turned into values (to be returned) as much as possible.
This is because exceptions are like
goto statements, which make the flow
of your program much more difficult to reason about. Every function that throws
an exception can return a value in 2 ways, through the "normal" return flow,
or through an exception, and every user of that function has to think about
both cases. It is generally easier to reason about functions that only return
values (something like vavr's Either or
Try types work very well for this). This
also makes it much easier to leverage Java 8's lambda-using utilities, as they
don't like accepting
Functions that throw exceptions.
Fail early, enforce invariants
It is always best to fail as early as possible, at the soonest point that you find something is wrong. This often means that we should validate all of our data up front, to decouple validation from the business logic that needs to use the values in question.
Additionally, the more we can make fail at compile time, rather than run time, the better off we will be at finding bugs earlie. Use Java's typesystem as much as you can to enforce invariants statically!
For our own data objects, we should have strong validation of each object, so that once we've constructed one, we know it will have good data in the rest of our program. Failed construction (on validation errors) should also be expressed by returning values, rather than exceptions. Something like vavr's Validation type is useful for this purpose. Make it impossible to construct a "bad"/invalid data object!
Composition over Inheritance
Inheritance can be very fragile, and make refactoring of complex code involving inheritance hierarchies very difficult to do safely and confidently. Composition (and in java, Interfaces) should be favored over inheritence where possible.
Often inheritance is required by certain frameworks / libraries, but we should limit the depth of inheritance hierarchies that we use as much as possible.
Composition, via composing interfaces or composable types, allows for more design flexibility, and easier, more confident refactoring.