The presented patterns are meant as a general guide but should not be understood as rules to follow blindly or use as a knockout argument or sledgehammer approach to everything. Each problem deserves to be judged individually considering all its aspects and complexity when choosing a fitting solution.
However, when in doubt which approach might be more fitting the guides provide a reasoning and direction with the goal of a more uniform codebase which can benefit from the advantages that emerge when using the described patterns.
Type | Pattern | Prefer | Over |
---|---|---|---|
Avoid null |
Use @Nonnull in APIs |
annotate bit too much | annotate too little |
Prefer Immutability | Consider collections as unmodifiable | copy before modification | copy before returning |
Using an annotation to communicate expectations (parameters) and guarantees
(return values) in regard to whether or not they can be null
helps both
programmers and static analysis tools to avoid NPEs.
Heavy use of extra information can also clutter the code and make it less
readable. To strike a balance it is recommended to use
javax.annotation.Nonnull
(and javax.annotation.CheckForNull
)
first and foremost in APIs for both method parameters and return values.
Another useful example are private helper methods that deal with mutable
state and low level details, where some methods have to handle null
and
others don't, adding annotations helps to draw the line between the two.
While it is useful to know that return values can be null
by annotating them
with @CheckForNull
the goal for the codebase should be to keep the usage low.
Before using @CheckForNull
it is recommended to consider if null
can be
avoided instead or if the use of Optional
could improve the API.
Goals:
- communicate
null
dependent behaviour - enable static analysis
- avoid adding too much annotation "clutter"
Exceptions:
- constructors of spring injected beans do not benefit much from the annotation as spring will throw exception for missing dependencies
Manipulation of mutable state is hard to follow and opens for permutation of possibilities that programmers need to consider. This unnecessarily creates a larger surface for error. Immutability as a concept should help keeping effects local,easy to follow and think through.
The idea of immutability can take many forms some of which are presented here in greater detail. In Java this usually is not all are black and white banning mutable state altogether. Typically, mutation is limited to a small scope that is easy(er) to think though and on the boundaries immutability is assumed (parameters) or ensured (return values).
When working with collections errors can arise from trying to manipulate an unmodifiable collection. One way to defend against this is to make copies of collections to be sure they can be modified by a consumer. This often results in collections being copied multiple times over as they pass through the layers of abstractions and the application architecture. The other approach to this is to generally consider any collection that wasn't created within or as part of the code that wants to modify the collection as unmodifiable.
In practice this means any collection received from a non-private method call or as parameter to a method should be considered unmodifiable and copied once where the algorithm needs a modifiable collection.
Goals:
- keep copying of collections to a minimum
- make code easy to understand and reason about
- make code about the essential problem (less ceremony/clutter)
Exceptions (known mutable collections):
- collections of hibernate entities in DHIS2 entities
- collections created in or as part of the algorithm (same method,
other
private
helper methods and alike)
Also:
- consider using alternative algorithms that can avoid manipulation,
e.g.
stream().map(...)
and alike.