📜 ⬆️ ⬇️

How to make Java code simpler and clearer

You write brilliantly,
and then I'll add roughness.
x / f Trumbo

Write Java code is not easy, but very simple. Difficulties begin when it is launched, or worse, if it needs to be changed. Having opened his code two years ago, everyone at least once wondered: who wrote all this? We at Wrike have been developing the product and have been doing it for more than ten years Similar situations happened to us repeatedly. During this time we have developed a number of principles in writing code that help us make it easier and more intuitive. Although there is nothing extraordinary in our approach, it differs in many ways from how it is customary to write Java code. Nevertheless, someone may find something useful in our approach and for themselves.



')

How to use immutable data models


Practice shows that the code becomes much simpler if for each method both its parameters and the result are immutable.

ImmutableList<Double> multiplyValuesByFactor( final ImmutableList<Double> valueList, final double factor) { … } 

Moreover, it is almost always possible to achieve the same for all local values ​​of methods or class fields. It is logical, moreover, that the class can have only getters. What to do if you need to construct a complex data model: create a builder that has only setters and already build an immutable object inside it using the build () method.

This approach does lead to simpler and more readable code, but you need to try to see it. Here are some examples in support of working with immutable state. Rust language by default defines all "variables" immutable:

 let x = 5; 

and in order to get exactly the variable, you need to make additional efforts:
 let mut y = 6; y=7; 

In the Erlang language, there are no variables at all; absolutely all values ​​are immutable; however, complex applications can be developed on it. Robert C. Martin has an interesting presentation on this topic with a well-laid out theory and not so good examples, but the theory is still worth a look.

It's a trap! How to avoid manual resource management


Opening the file, you need to close it. Having received connection with base, it needs to be released. In general, nothing tricky. But practice shows that when working with resources, Java developers tend to make mistakes, probably because we are deprived of the joy of manually managing memory. Skill is not developed. A simple example:

 try(final ZipInputStream zipInputStream = new ZipInputStream( new FileInputStream(file), charset)) {...} 

In the code above there is an unclear error, due to which FileInputStream may not be closed.

A simple and cheap solution is to deprive a programmer of the need to manage resources in general. For example:

 void processFile(final File file, FileProcessor fileProcessor) throws IOException { try (final FileInputStream fileInputStream = new FileInputStream(file)) { fileProcessor.processFile(fileInputstream); } } 

In this case, even if somewhere in the file processing logic, an error creeps in, at least all resources will be correctly released and closed.

How to return two values


This is especially noticeable in interviews when novice programmers fall into a stupor if they are required to return two values ​​from a method at once, say, the sum and the average. Decisions vary in the degree of indecency from

 return new Pair<>(sum, avg) 

or

 return new AbstractMap.SimpleEntry<>(sum, avg) 

before

 return new Object[]{sum, avg} 

At the same time, a neat solution is quite simple:

 public class MathExt { public static class SumAndAverage { private final long sum; private final double average; public SumAndAverage(final long sum, final double average) {...} public long getSum() {...} public double getAverage() {...} } public static SumAndAverage computeSumAndAverage(final ImmutableList<Integer> valueList) { ... } } 

It is perfectly normal, and moreover, convenient when the auxiliary data models are defined alongside the methods that use them. This is the way to act in absolutely all programming languages, even in C #, which by and large is a clone of Java. However, among Java programmers one may encounter a strange delusion that, they say, “each class must be defined in a separate file, otherwise it is bad code, and period”. In Java, it was really impossible to create inner classes, in the version prior to 1.1, but since February 19, 1997, this problem was solved, and nothing prevents us from using this new language feature, which has been available for twenty years.

How to hide implementation


It happens that in the process of implementing a method, it becomes necessary to break it down into several simpler methods. This inevitably raises the question: where are these auxiliary methods to define? Of course, you can chop logic into private methods, but if the class is large enough, it becomes inconvenient. Private methods inevitably turn out to be scattered throughout the file, and which of them is needed for what and where it is used is not as obvious as we would like. A simple solution that helps both to accurately organize the code, and to scroll the file less when reading it, is to define helper methods inside the main public method.

For example, below is the code to bypass the graph in depth, while for each node of the graph, a callback function is called, of course, exactly once for each node.

 public static void traverseGraphInDepth( final Graph graph, final Consumer<Node> callback ) { new Runnable() { private final HashSet<Node> passedNodeSet = new HashSet<>(); public void run() { for(final Node startNode:graph.listStartNodes()){ stepToNode(startNode); } } void stepToNode(final Node node) { if (passedNodeSet.contains(node)) { return; } else { passedNodeSet.add(node); callback.accept(node); for(final Node nextNode:graph.listNextNodes(node)){ stepToNode(nextNode); } } } }.run(); } 

One could define an auxiliary method alongside:

 private static void stepToNodeImpl( final Graph graph, final Node node, final Consumer<Node> callback, final HashSet<Node> passedNodeSetMutable) {...} 

But such a solution is quite cumbersome, and the auxiliary method itself is lonely. For example, looking only at the name of the method stepToNodeImpl it is not obvious that it is actually used to traverse the graph in depth. To understand this, you must first find all its uses.

There may be a reasonable criticism that a lot of logic will be hidden inside the method and it will not be possible to test it separately, rightly. The question is, are all implementation details, including private methods, covered by separate tests? If yes, the logic will probably have to be decomposed for the sake of tests. If not, it's better to organize the code in a more accurate way.

In practice, depending on the situation, it is convenient to implement the internal logic of the method using the Runnable, Callable, Consumer or Supplier interfaces.

How to calculate the value


Calculating a single value can be a non-trivial task if it is calculated according to complex rules or requires additional intermediate calculations. As a result, the code can get “garbage.” Let's say that the further logic of the method depends on whether the client browser version is supported or not. To do this, we first need to determine the type of browser, then, depending on the type, compare versions, and, possibly, take into account some additional parameters.

 final boolean isSupportedBrowser = ((Supplier<Boolean>)()->{ final BrowserType browserType = … final boolean isMobileBrowser = … final boolean isTabletBrowser = … finalfinalif ( … && … ) { return true; } if ( … || … ) { return true; } else { if ( … ) { return true; … return false; }).get(); 

Note: Of course, in this example it is assumed that the logic of calculating the supported client is very specific, otherwise it should be taken out as a separate auxiliary method.

It would be possible to calculate the value of isSupportedBrowser directly in the code, but then all the auxiliary variables would remain in scope, and the moment where the calculations begin and where they end would be not so obvious. In the example above, the design:

 isSupportedBrowser = ((Supplier<boolean>)()->{ … }).get(); 
it helps to clearly separate the part of the logic responsible for calculating the flag, and to hide all intermediate values ​​that were necessary in the process, but no longer needed.

How to structure a boring code


The code is not always complicated, sometimes it's just a lot of it. The task of transferring one data model to another is more typical. So what if, in the end, the method takes five hundred lines. Still understandable. We take it, put it here, take it, put it there and so on. There are rare if or additional calculations, but they do not do the weather. In this case, if you try to break the code into smaller methods, it may turn out even worse. It becomes necessary to transfer data between methods, define additional models for intermediate results of methods, and so on.

Blank lines can be extremely useful for organizing such a code. By breaking the code into paragraphs, it can be made much more readable. But you can go further, and highlight meaningful moments in {} - curly braces. This will not only help to structure the code in blocks, but also hide out of scope the values ​​needed only for one of the sections.

 OutputReport transformReport(final InputReport input) { final OutputReport output = new OutputReport(); { //transform report header: final boolean someFlagUsedOnlyHere = … … 50 lines of code … } { //transform report body: … 50 lines of code … { //transform section 01: … 50 lines of code … } … { //transform section 04 (end section): … 50 lines of code … } } { //transform report summary: … 50 lines of code … } return output. } 

Of course, this is not always the best approach. It happens that no matter how many {} brackets are added, the code does not get better, and it just needs to be rewritten. It happens that everything is divided into methods is quite simple and wonderful. However, using {} allows you to add expressiveness to code where necessary.

Conclusion


Any application, no matter how large or small, ultimately consists of simple things: conditions, loops, expressions, data models, ultimately lines of code. No matter how beautifully the application architecture is called, no matter what programming patterns and frameworks are used in it, the main thing is how simple or difficult it is to understand how the code that is in front of you on the screen works. The examples above allow you to make the code a little less complicated and a little more expressive. At least they are useful to us.

Source: https://habr.com/ru/post/349652/


All Articles