How to become a better programmer?

How to become a better programmer?

Read this article and learn how to use declarative code with a combination of functions to become a better programmer.

In many cases, declarative solutions with a combination of functions provide a degree of code better than traditional imperative code. Read this article and learn how to use declarative code with a combination of functions to become a better programmer.

In this article, we will take a closer look at three problem examples and study two different techniques (imperative and declarative) to solve these problems.

All the source code in this article is open source and can be obtained from it. Finally, we will also see how the learning in this article can be applied to the field of database applications. We will use Speedment Stream as the ORM tool because it provides standard Java Streams corresponding to the tables, views and connections in the database, and supports declarative construction.

In fact, there are countless candidate examples available for code metric evaluation.

1. Examples of problems

In this article, I have selected three common problems that developers may encounter in their daily work:

1.1.SumArray

Iterate the array and perform calculations

1.2.GroupingBy

Parallel aggregation of values

1.3.Rest

Use paging to implement REST interface

2. Solution technology

As described at the beginning of this article, we will use these two encoding techniques to solve the problem:

2.1 Imperative solutions

For an imperative solution, we use traditional code samples with for loops and explicit mutable states.

2.2 Declarative solutions

Declarative solutions, in which we combine various functions to form higher-order composite functions to solve the problem, usually used java.util.stream.Streamor its variants.

3. Code indicators

However, our idea is to use SonarQube (here, SonarQube Community Edition, Version 7.7) to apply static code analysis to different solutions so that we can derive useful and standardized code metrics for problem/solution combinations. These indicators will then be compared.

In this article, we will use the following code metrics:

3.1. LOC

"LOC" stands for "line of code" and is the number of non-blank lines in the code.

3.2. Statements

Is the total number of statements in the code. There may be zero or more statements on each line of code.

3.3. Cyclic complexity

Represents the complexity of the code and is a quantitative measure of the number of linear independent paths through the source code program. For example, a single "if" clause shows two separate paths in the code. Read more on Wikipedia content .

3.4. Cognitive complexity

SonarCube claims:

"Cognitive complexity has changed the practice of using mathematical models to evaluate software maintainability. It starts from the precedent set by Cyclomatic Complexity, but uses human judgment to evaluate how the structure should be calculated and decide what should be added to the model as An overall result, it produces a method complexity score, which makes programmers evaluation of the maintainability model fairer than before.

On SonarCube own page you can read more content .

Normally, a solution needs to be envisaged in which these indicators are small rather than large.

For the record, it should be noted that any solution designed below is only a way to solve any given problem. If you know a better solution, please feel free to pass

4. Iterate the array

We start simple. The object of this problem example is to calculate the sum of the elements in the int array and return the result as a long. The following interface defines the problem:

public interface SumArray {
    long sum(int[] arr);
} 

4.1. Imperative solutions

The following solutions use imperative techniques to implement the SumArray problem:

public class SumArrayImperative implements SumArray {
    @Override
    public long sum(int[] arr) {
        long sum = 0;
        for (int i : arr) {
            sum += i;
        }
        return sum;
    }
} 

4.2 Declarative solutions

This is a solution that uses declarative technology to implement SumArray:

public class SumArrayDeclarative implements SumArray {
    @Override
    public long sum(int[] arr) {
        return IntStream.of(arr)
            .mapToLong(i -> i)
            .sum();
    }
} 

Please note that IntStream::sum only returns an int, so we must add the intermediate operation mapToLong().

4.3. Analysis

SonarQube provides the following analysis:

SumArray's code metrics are shown in the following table (usually lower):

technology
LOC
Statements Cyclic complexity Cognitive complexity
Imperative 12
5
2
1
Functional 11
2
2
0

This is its value in the chart (usually lower):

5. Parallel aggregation of values

The object of this problem example is to group Person objects into different buckets, where each bucket constitutes a unique combination of a person's birth year and a person's working country. For each group, the average salary should be calculated. The aggregation should be calculated in parallel using the public ForkJoin pool.

This is the (immutable) Person class:

public final class Person {
    private final String firstName;
    private final String lastName;
    private final int birthYear;
    private final String country;
    private final double salary;
    public Person(String firstName, 
                  String lastName, 
                  int birthYear, 
                  String country, 
                  double salary) {
        this.firstName = requireNonNull(firstName);
        this.lastName = requireNonNull(lastName);
        this.birthYear = birthYear;
        this.country = requireNonNull(country);
        this.salary = salary;
    }
    public String firstName() { return firstName; }
    public String lastName() { return lastName; }
    public int birthYear() { return birthYear; }
    public String country() { return country; }
    public double salary() { return salary; }
   //equals, hashCode and toString not shown for brevity
} 

We also define another immutable class called YearCountry, and use it as the grouping key:

public final class YearCountry {
    private final int birthYear;
    private final String country;
    public YearCountry(Person person) {
        this.birthYear = person.birthYear();
        this.country = person.country();
    }
    public int birthYear() { return birthYear; }
    public String country() { return country; }
   //equals, hashCode and toString not shown for brevity
} 

After defining these two classes, we can now define this problem example through the interface:

public interface GroupingBy {
    Map<YearCountry, Double> average(Collection<Person> persons);
} 

5.1. Imperative solutions

Implementing an imperative solution to the GroupingBy example problem is not easy. This is a solution to the problem:

public class GroupingByImperative implements GroupingBy {
    @Override
    public Map<YearCountry, Double> average(Collection<Person> persons) {
        final List<Person> personList = new ArrayList<>(persons);
        final int threads = ForkJoinPool.commonPool().getParallelism();
        final int step = personList.size()/threads;
       //Divide the work into smaller work items
        final List<List<Person>> subLists = new ArrayList<>();
        for (int i = 0; i < threads - 1; i++) {
           subLists.add(personList.subList(i * step, (i + 1) * step));
        }
        subLists.add(personList.subList((threads - 1) * step, personList.size()));
        final ConcurrentMap<YearCountry, AverageAccumulator> accumulators = new ConcurrentHashMap<>();
       //Submit the work items to the common ForkJoinPool
        final List<CompletableFuture<Void>> futures = new ArrayList<>();
        for (int i = 0; i < threads; i++) {
            final List<Person> subList = subLists.get(i);
       futures.add(CompletableFuture.runAsync(() -> average(subList, accumulators)));
        }
       //Wait for completion
        for (int i = 0; i < threads; i++) {
            futures.get(i).join();
        }
       //Construct the result
        final Map<YearCountry, Double> result = new HashMap<>();
        accumulators.forEach((k, v) -> result.put(k, v.average()));
        return result;
    }
    private void average(List<Person> subList, ConcurrentMap<YearCountry, AverageAccumulator> accumulators) {
        for (Person person : subList) {
            final YearCountry bc = new YearCountry(person);
          accumulators.computeIfAbsent(bc, unused -> new AverageAccumulator())
                .add(person.salary());
        }
    }
    private final class AverageAccumulator {
        int count;
        double sum;
        synchronized void add(double term) {
            count++;
            sum += term;
        }
        double average() {
            return sum/count;
        }
    }
} 

5.2. Declarative solutions

This is a solution that uses declarative construction to implement GroupingBy:

public class GroupingByDeclarative implements GroupingBy {
    @Override
    public Map<YearCountry, Double> average(Collection<Person> persons) {
        return persons.parallelStream()
            .collect(
             groupingBy(YearCountry::new, averagingDouble(Person::salary))
            );
    }
} 

In the code above, I used some static imports from the Collectors class (eg Collectors::groupingBy). This will not affect the code metrics.

5.3. Analysis

SonarQube provides the following analysis:

GroupingByThe code metrics for is shown in the following table (usually lower):

technology
LOC
Statements Cyclic complexity Cognitive complexity
Imperative 52
27
11
4
Functional 17
1
1
0

This is its value in the chart (usually lower):

6. Implement the REST interface

In this exemplary problem, we will provide paging services for the Person object. The Persons appearing on the page must meet certain (arbitrary) conditions and be sorted in a specific order. The page will be returned as a list of unmodifiable Person objects.

This is an interface to solve the problem:

public interface Rest {

/**

 * Returns an unmodifiable list from the given parameters.
 *
 * @param persons as the raw input list
 * @param predicate to select which elements to include
 * @param order in which to present persons
 * @param page to show. 0 is the first page
 * @return an unmodifiable list from the given parameters
 */

 List<Person> page(List<Person> persons, 
                   Predicate<Person> predicate,
                   Comparator<Person> order,
                   int page);
} 

The size of the page is in a separate utility class called RestUtil:

public final class RestUtil {
    private RestUtil() {}
    public static final int PAGE_SIZE = 50;
} 

6.1. Imperative implementation method

public final class RestImperative implements Rest {
    @Override
    public List<Person> page(List<Person> persons, 
                Predicate<Person> predicate, 
                  Comparator<Person> order, 
                             int page) {
        final List<Person> list = new ArrayList<>();
        for (Person person:persons) {
            if (predicate.test(person)) {
                list.add(person);
            }
        }
        list.sort(order);
        final int from = RestUtil.PAGE_SIZE * page;
        if (list.size() <= from) {
            return Collections.emptyList();
        }
        return unmodifiableList(list.subList(from, Math.min(list.size(), from + RestUtil.PAGE_SIZE)));
    }
} 

6.2. Declarative solutions

public final class RestDeclarative implements Rest {
    @Override
    public List<Person> page(List<Person> persons,
                      Predicate<Person> predicate, 
                        Comparator<Person> order,
                             int page) {
        return persons.stream()
            .filter(predicate)
            .sorted(order)
            .skip(RestUtil.PAGE_SIZE * (long) page)
            .limit(RestUtil.PAGE_SIZE)
           .collect(collectingAndThen(toList(), Collections::unmodifiableList));
    }
} 

6.3. Analysis

SonarQube provides the following analysis:

RestThe code metrics for is shown in the following table (usually lower):

technology
LOC
Statements Cyclic complexity Cognitive complexity
Imperative 27
10
4
4
Functional 21
1
1
0

This is its value in the chart (usually lower):

7. Java 11 improvements

The above example is written in Java 8. With Java 11, we can use LVTI (Local Variable Type Inference) to shorten the declarative code. This will make our code shorter, but will not affect the code metrics.

@Override
public List<Person> page(List<Person> persons,
                         Predicate<Person> predicate, 
                         Comparator<Person> order, 
                         int page) {
    final var list = new ArrayList<Person>();
    ... 

Compared with Java 8, Java 11 includes some new collectors. For example, Collectors.toUnmodifiableList(), it will make our declarative Rest solution shorter:

public final class RestDeclarative implements Rest {
@Override
public List<Person> page(List<Person> persons,
                         Predicate<Person> predicate, 
                         Comparator<Person> order, 
                         int page) {
    return persons.stream()
        .filter(predicate)
        .sorted(order)
        .skip(RestUtil.PAGE_SIZE * (long) page)
        .limit(RestUtil.PAGE_SIZE)
        .collect(toUnmodifiableList());
} 

Again, this will not affect the code metrics.

8. Summary

The average code metric for the three exemplary questions produced the following results (usually lower):

In view of the input requirements in this article, when we go from imperative construction to declarative construction, all code metrics have improved significantly.

8.1. Use declarative constructs in database applications

In order to obtain the benefits of declarative construction in database applications, we use Speedment Stream . Speedment Stream is a stream-based Java ORM tool that can convert any database table/view/connection into a Java stream, allowing you to apply declarative skills in database applications.

Your database application code will become better. In fact, Speedment and Spring Boot's paging REST solution for the database may be expressed as follows:

public Stream<Person> page(Predicate<Person> predicate, 
                     Comparator<Person> order, 
                           int page) {
    return persons.stream()
        .filter(predicate)
        .sorted(order)
        .skip(RestUtil.PAGE_SIZE * (long) page)
        .limit(RestUtil.PAGE_SIZE);
} 

Manager personsIt is provided by Speedment and constitutes the handle of the database table "Person", which can be annotated with @AutoWired through Spring.

9. Summary

Choosing a declarative imperative solution can greatly reduce general code complexity and can provide many benefits, including faster coding, better code quality, higher readability, less testing, and lower maintenance costs and many more.

In order to benefit from the declarative construction in database applications, Speedment Stream is a tool that can provide standard Java Streams directly from the database.

Mastery of declarative construction and functional combination is a must for any contemporary Java developer today.

August benefits come on time, pay attention to the public account Background reply: 003 can receive the July translation collection~ Past benefit reply: 001,002 can be received!