Sergiy Yevtushenko
Posted on October 24, 2021
For the last few years, I'm writing articles which describe a new, more functional way to write Java code. But the question of why we should use this new coding style remains largely unanswered. This article is an attempt to fill this gap.
Just like any other language, Java evolves over the time. So does the style in which Java code is written. Code written around Y2K is significantly different from code written after 2004-2006, when Java5 and then Java6 were released. Generics and annotations are so widespread now, that it's hard to even imagine Java code without them.
Then came Java8 with lambdas, Stream<T>
and Optional<T>
. Those functional elements should be revolutionizing Java code, but largely they don't. In a sense, they definitely affected how we write Java code, but there were no revolution. Rather slow evolution. Why? Let's try to find the answer.
I think that there were two main reasons.
The first reason is that even Java authors felt uncertainty how new functional elements fit into existing Java ecosystem. To see this uncertainty, it's enough to read Optional<T>
JavaDoc:
API Note: Optional is primarily intended for use as a method return type where there is a clear need to represent "no result," and where using null is likely to cause errors.
API also shows the same: presence of get()
method (which may throw NPE) as well as a couple of orElseThrow()
methods are clear reverences to traditional imperative Java coding style.
The second reason is that existing Java code, especially libraries and frameworks, was incompatible with functional approaches - null
and business exceptions were the idiomatic Java code.
Fast-forward to present time: Java 17 released few weeks ago, Java 11 is quickly getting wide adoption, replacing Java 8, which was ubiquitous a couple of years ago. Yet, our code looks almost the same as 7 years ago, when Java 8 was released.
Perhaps it's worth to step back and answer another important question: do we need to change the way in which we're writing Java code at all? It served us good enough for long time, we have skills, guides, best practices and tons of books which teach us how to write code in this style. Do we actually need to change that?
I believe the answer to this question could be derived from the answer to another question: do we need to improve the development performance?
I bet we do. Business pushes developers to deliver apps faster. Ideally, projects we're working on should be written, tested and deployed before business even realizes what actually need to be implemented. Just kidding, of course, but delivery date "yesterday" is a dream of many business people.
So, we definitely need to improve development performance. Every single framework, IDE, methodology, design approach, etc., etc., focuses on improving the speed at which software (of course, with necessary quality standards) is implemented and deployed. Nevertheless, despite all these, there is no visible development performance breakthroughs.
Of course, there are many elements which define the pace at which software is delivered. This article focuses only on development performance.
From my perspective, most attempts to improve development performance are assuming that writing less code (and less code in general) automatically means better performance. Popular libraries and frameworks like Spring, Lombok, Feign - all trying to reduce amount of code. Even Kotlin was created with obsession on brevity as opposed to Java "verbosity". History did prove this assumption wrong many times (Perl and APL, perhaps, most notable examples), nevertheless it's still alive and drives most efforts.
Any developer knows that writing code is a tiny portion of the development activities. Most of the time we're reading code. Is reading less code more productive? The first intent is to say yes, but in practice, the amount of code and its readability are barely related. Reading and writing of the same code often has different "impedance" in the form of mental overhead.
Probably the best examples of this difference in the "impedance" are regular expressions. Regular expressions are quite compact and in most cases rather easy to write, especially using countless dedicated tools. But reading regular expressions usually is a pain and consumes much more time. Why? The reason is the lost context. When we're writing regular expression, we know the context: what we want to match, which cases should be considered, how possible input may look like, and so on and so forth. The expression itself is a compressed representation of this context. But when we're reading them, the context is lost or, to be precise, squeezed and packed using very compact syntax. And attempt to "decompress" it from the regular expression is a quite time-consuming task. In some cases, rewriting from scratch takes significantly less time than an attempt to understand existing code.
The example above gives one important hint: reducing the amount of code is meaningful only to the point where context remains preserved. As soon as reducing code causes loss of context, it starts to be counterproductive and harms development performance.
So, if code size is not so relevant, then how we really can improve productivity?
Obviously, by preserving and/or restoring lost context. But when and why, context is getting lost?
Context Eaters
Context Eaters are coding practices or approaches which results to the context loss. Idiomatic Java code has several such context eaters. Popular frameworks often add their context eaters. Let's take a look at the two most ubiquitous context eaters.
Nullable Variables
Yes, you read it correctly. Nullable variables hide part of the context - cases when variable value might be missing. Look at this code example:
String value = service.method(parameter);
Just by looking at this code, you can't tell if value
can be null or not. In other words, part of the context is lost. To restore it, one needs to take a look into the code of the service.method()
and analyze it. Navigation to that method, reading its code, returning - all these are a distraction from the current task. And the constant need to keep in mind that a variable might be null
, causes a mental overhead. Experienced developers are good at keeping such things in mind, but this does not mean that this mental overhead does not affect their development performance.
Let's sum up:
Nullable variables are context eaters, development performance killers and source of run-time errors.
Exceptions
Idiomatic Java uses business exceptions for the error propagation and handling. There are two types of exceptions - checked and unchecked. Use of checked exceptions, usually discouraged and often considered an antipattern because they cause deep code coupling. Although the initial intent of introduction of checked exceptions, by the way, was preserving the context. And compiler even helps to preserve it. Nevertheless, over the time, we've switched to unchecked exceptions. Unchecked exceptions were designed for the technical errors - accessing null variable, attempt to access value outside the array bounds, etc.
Think about this for a moment: we're using technical unchecked exceptions for the business error handling and propagation.
Use of the language feature outside the area it was designed for, results in loss of context and issues similar to ones described for nullable variables. Even reasons are the same - unchecked exceptions require navigation and reading code (often quite deep in the call chain). They also require switching back and forth between current task and error handling. And just like nullable variables, exceptions can be a source of run time errors if not processed correctly.
Summary:
Business exceptions are context eaters, development performance killers and source of bugs.
Frameworks as Context Eaters
Since frameworks are usually specific to a particular project, issues caused by them are also project-specific. Nevertheless, if you got the idea of context loss/preservation, you might notice that popular frameworks like Spring and others, which use class path scan, "convention over configuration" idiom and other "magic", intentionally remove large part of the context and replace it with implicit knowledge of the default setup (i.e. mental overhead). With this approach, the application gets broken into a set of loosely related classes. Without IDE support, it's even hard to navigate between components, so disconnected they are. Besides loss of huge part of context, there is another significant problem, which negatively impacts productivity: significant number of errors are shifted from compile time to run-time. Consequences are devastating:
- more tests are necessary. Famous
contextLoads()
test is a clear sign of this problem - software support and maintenance requires significantly more time and efforts
So, by reducing typing for a few lines of code, we're getting a lot of headache and decreased development performance. This is the real price of the "magic"
Pragmatic Functional Java Way
The Pragmatic Functional Java is an attempt to solve some problems mentioned above. While initial intent was to just preserve context by encoding special states into variable type, practical use did show a number of other benefits of taken approach:
- significantly reduced navigation
- a number of errors are shifted from run-time to compile time which, in turn, improved reliability and reduced number of necessary tests
- removed significant portion of boilerplate and even type declarations - less typing, less code to read, business logic is less cluttered with technical details
- sensibly less mental overhead and need to keep in mind technical things not related to current task
Posted on October 24, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024