avatarEmanuel Trandafir

Free AI web copilot to create summaries, insights and extended knowledge, download it at here

6458

Abstract

, <span class="hljs-keyword">new</span> <span class="hljs-title class_">FieldValue</span>(<span class="hljs-number">20L</span>),

    <span class="hljs-keyword">new</span> <span class="hljs-title class_">Aggregator</span>(<span class="hljs-number">3L</span>, Type.SUM), <span class="hljs-keyword">new</span> <span class="hljs-title class_">FieldValue</span>(<span class="hljs-number">30L</span>),
    <span class="hljs-keyword">new</span> <span class="hljs-title class_">Aggregator</span>(<span class="hljs-number">4L</span>, Type.SUM), <span class="hljs-keyword">new</span> <span class="hljs-title class_">FieldValue</span>(<span class="hljs-number">40L</span>),

    <span class="hljs-keyword">new</span> <span class="hljs-title class_">Aggregator</span>(<span class="hljs-number">5L</span>, Type.MAX), <span class="hljs-keyword">new</span> <span class="hljs-title class_">FieldValue</span>(<span class="hljs-number">50L</span>),
    <span class="hljs-keyword">new</span> <span class="hljs-title class_">Aggregator</span>(<span class="hljs-number">6L</span>, Type.MAX), <span class="hljs-keyword">new</span> <span class="hljs-title class_">FieldValue</span>(<span class="hljs-number">60L</span>),

    <span class="hljs-keyword">new</span> <span class="hljs-title class_">Aggregator</span>(<span class="hljs-number">7L</span>, Type.MIN), <span class="hljs-keyword">new</span> <span class="hljs-title class_">FieldValue</span>(<span class="hljs-number">70L</span>),
    <span class="hljs-keyword">new</span> <span class="hljs-title class_">Aggregator</span>(<span class="hljs-number">8L</span>, Type.MIN), <span class="hljs-keyword">new</span> <span class="hljs-title class_">FieldValue</span>(<span class="hljs-number">80L</span>)
);

<span class="hljs-comment">// when</span>
Map&lt;Aggregator.Type, Double&gt; result = aggregate(values);

<span class="hljs-comment">//then</span>
assertThat(result).isEqualTo(Map.of(
    Type.AVG, <span class="hljs-number">15.0</span>,
    Type.SUM, <span class="hljs-number">70.0</span>,
    Type.MAX, <span class="hljs-number">60.0</span>,
    Type.MIN, <span class="hljs-number">70.0</span>
));

}

<span class="hljs-keyword">public</span> Map<Aggregator.Type, Double> aggregate(Map<Aggregator, FieldValue> fields) { <span class="hljs-comment">// <span class="hljs-doctag">TODO:</span> implement me!</span> <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">HashMap</span><>(); }</pre></div><h2 id="db8c">The Challenges</h2><p id="e9a5">Even though it looks like a simple task, there are a few challenges we’ll encounter. First of all, we can try streaming the entry set and then collect it using <a href="https://www.java67.com/2018/11/10-examples-of-collectors-in-java-8.html"><i>Collectors.toMap</i></a> or<a href="https://javarevisited.blogspot.com/2015/07/how-to-do-group-by-in-java-8.html"> <i>Collectors.groupBy</i></a><i>. </i>Unfortunately, it is impossible to group elements using different rules or different collectors<i> (</i>without creating your own custom collector).</p><p id="5509">Secondly, the various out-of-the-box collectors we might be tempted to use have different return types:</p><ul><li><i>Collectors.maxBy() </i>and <i>minBy() — </i>will return <i>Optional<Value></i></li><li><i>Collectors.summingDouble()</i> and <i>averagingDouble()</i> — will return <i>Double</i></li></ul><p id="9793">Even if we will be using a <i>LongStream </i>or <i>DoubleStream </i>and attempt to use <i>max()</i>, <i>min(), average(), </i>and <i>sum(), </i>the return types will be different.</p><h2 id="abab">The Implementation</h2><p id="21d3">Firstly, let’s unwrap the data and discard the useless pieces of information, such as the aggregator id:</p><div id="87b7"><pre>Map<Aggregator.Type, List<Long>> valuesByType = fields.entrySet() .stream() .collect(Collectors.groupingBy( entry -> entry.getKey().type(), Collectors.mapping( entry -> entry.getValue().value(), Collectors.toList()) ));</pre></div><p id="aeab">After this step, the new map should look something like this:</p><div id="7fce"><pre>{ Type<span class="hljs-selector-class">.AVG</span>: <span class="hljs-selector-attr">[10, 20]</span>, Type<span class="hljs-selector-class">.SUM</span>: <span class="hljs-selector-attr">[30, 40]</span>, Type<span class="hljs-selector-class">.MAX</span>: <span class="hljs-selector-attr">[50, 60]</span>, Type<span class="hljs-selector-class">.MIN</span>: <span class="hljs-selector-attr">[70, 80]</span> }</pre></div><p id="76ec">At this point, we can re-iterate through the key-value pairs and try to apply a specific collector to each set of values:</p><div id="ed92"><pre><span class="hljs-keyword">return</span> valuesByType.entrySet() .stream() .collect(Collectors.toMap( entry.getKey(), entry.getKey().aggregate(entry.getValue()) ));</pre></div><p id="53dd">As we can see, we’ll keep the <i>Aggregator.Type </i>as key of our map, and we’ll call a <i>collectValues </i>that will perform the specific collector for each type.</p><p id="82cb"><i>PS: I decided to pass the key and the value as separate arguments instead of passing the whole entry not to couple this function with the internal implementation of java.util.Map</i></p><div id="787a"><pre><span class="hljs-keyword">private</span> <span class="hljs-type">double</span> <span class="hljs-title function_">collectValues</span><span class="hljs-params">(Type aggregator, List<Long> values)</span> { <span class="hljs-type">DoubleStream</span> <span class="hljs-variable">stream</span> <span class="hljs-operator">=</span> values.stream().mapToDouble(x -> x + <span class="hljs-number">0.0d</span>); <span class="hljs-keyword">return</span> <span class="hljs-keyword">switch</span> (aggregator) { <span class="hljs-keyword">case</span> AVG -> stream.average().orElseThrow(); <span class="hljs-keyword">case</span> SUM -> stream.sum(); <span class="hljs-keyword">case</span> MAX -> stream.max().orElseThrow(); <span class="hljs-keyword">case</span> MIN -> stream.min().orElseThrow(); <span class="hljs-keyword">default</span> -> <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span c

Options

lass="hljs-title class_">IllegalArgumentException</span>(); }; }</pre></div><p id="ece3">This new enhanced switch will allow us to call the appropriate aggregator in a declarative manner. In this example, the type is an Enum, but, Starting with Java20, we’ll be able to do it on sealed classes. This pattern is often called “<i>ad-hoc polymorphism</i>”.</p><p id="c0d5">In my opinion, this is not true polymorphism because this method still depends on the various aggregator types. Let’s do a bit of refactoring and add a bit of logic inside the <i>Aggregator.Type:</i></p><div id="870b"><pre><span class="hljs-meta">@RequiredArgsConstructor</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">enum</span> <span class="hljs-title class_">Type</span> { SUM(values -> doubleStream(values).sum()), AVG(values -> doubleStream(values).average().orElseThrow()), MAX(values -> doubleStream(values).max().orElseThrow()), MIN(values -> doubleStream(values).min().orElseThrow());

<span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> Function&lt;List&lt;Long&gt;, Double&gt; aggregator;

<span class="hljs-keyword">public</span> Double <span class="hljs-title function_">aggregate</span><span class="hljs-params">(List&lt;Long&gt; values)</span> {
    <span class="hljs-keyword">return</span> aggregator.apply(values);
}

<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> DoubleStream <span class="hljs-title function_">doubleStream</span><span class="hljs-params">(List&lt;Long&gt; values)</span> {
    <span class="hljs-keyword">return</span>  values.stream().mapToDouble(v -&gt; v + <span class="hljs-number">0.0d</span>);
}

}</pre></div><p id="0e93">Now, we can use the polymorphic <i>aggregate </i>method and remove the switch statement. Let's read through the whole code again and run the tests:</p><div id="af41"><pre><span class="hljs-meta">@Test</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">test</span><span class="hljs-params">()</span> { <span class="hljs-comment">//given</span> Map<Aggregator, FieldValue> values = <span class="hljs-comment">// Map.of( ... );</span>

<span class="hljs-comment">// when</span>
Map&lt;Aggregator.Type, Double&gt; result = aggregate(values);

<span class="hljs-comment">//then</span>
assertThat(result).isEqualTo(Map.of(
    Type.AVG, <span class="hljs-number">15d</span>,
    Type.SUM, <span class="hljs-number">70d</span>,
    Type.MAX, <span class="hljs-number">60d</span>,
    Type.MIN, <span class="hljs-number">70d</span>
));

}

<span class="hljs-keyword">public</span> Map<Aggregator.Type, Double> aggregate(Map<Aggregator, FieldValue> fields) { Map<Aggregator.Type, List<Long>> valuesByType = fields.entrySet() .stream() .collect(Collectors.groupingBy( entry -> entry.getKey().type(), Collectors.mapping( entry -> entry.getValue().value(), Collectors.toList()) ));

<span class="hljs-keyword">return</span> valuesByType.entrySet()
    .stream()
    .collect(Collectors.toMap(
        entry.getKey(),
        entry.getKey().aggregate(entry.getValue())
    ));

}</pre></div><h2 id="ec0d">Code Smells?</h2><p id="ef9f">The test is passing! We followed the question and solved the problem, great!</p><p id="7348">I just want to mention two potential code smells here. Firstly, look at the <i>aggregate </i>method: we collect the data into a local variable, and only after that, we use it to stream again. I believe this is the proper way of doing it. Even though it might be tempting to continue the pipeline right away, this can lead to very large and cluttered functional pipelines. So, in my opinion, we should stay away from this anti-pattern: <code>.collect(…).stream()</code></p><p id="caff">Secondly, the <i>Maps<> </i>we are using here might not be the correct data structure in our case. We can enrich the project with more meaningful classes and types. Lines such as this one: <code>entry.getKey().aggregate(entry.getValue())</code> are a clear manifestation of the <a href="https://levelup.gitconnected.com/the-dark-side-of-primitive-obsession-five-harmful-impacts-b7ea6961e6b5">“primitive obsession” anti-pattern</a>.</p><p id="1543">If you are interested in reading more about this anti-pattern, I have an article dedicated to this topic. Feel free to dive deeper into the subject and let me know what you think:</p><div id="84f8" class="link-block"> <a href="https://levelup.gitconnected.com/the-dark-side-of-primitive-obsession-five-harmful-impacts-b7ea6961e6b5"> <div> <div> <h2>The Dark Side of Primitive Obsession: Five Harmful Impacts</h2> <div><h3>Let’s discuss Functional Interfaces, generic types, coupling, code duplication, and the negative impact of the…</h3></div> <div><p>levelup.gitconnected.com</p></div> </div> <div> <div style="background-image: url(https://miro.readmedium.com/v2/resize:fit:320/1*mgH1vEvksQLkMkxeEH1p8Q.png)"></div> </div> </div> </a> </div><figure id="a5af"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/0*nSgqyNHkIANfK3GO"><figcaption>Photo by <a href="https://unsplash.com/@leunesmedia?utm_source=medium&amp;utm_medium=referral">Michiel Leunens</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><h1 id="1cce">Thank You!</h1><p id="10b4">Thanks for reading the article and please let me know what you think! Any feedback is welcome.</p><p id="6922">If you want to read more about clean code, design, unit testing, object-oriented programming, functional programming, and many others, make sure to check out <a href="https://readmedium.com/start-here-6d2b065a626">my other articles</a>. Do you like the content? Consider <a href="https://medium.com/@emanueltrandafir">following or subscribing</a> to the email list.</p><p id="15a4">Finally, if you consider becoming a Medium member and supporting my blog, here’s my <a href="https://medium.com/@emanueltrandafir/membership">referral</a>.</p><p id="b366">Happy Coding!</p></article></body>

Polymorphic Stream Collector In Java

Let’s answer this Stackoverflow question by implementing a polymorphic stream collector, mixing F.P. and O.O.P. concepts.

Photo by WrongTog on Unsplash

Overview

In this short article, we’ll play lambda expressions and various ways of collecting streams in Java. We’ll start with this StackOverflow question and we’ll solve it by combing functional features such as passing functions as the first-class citizens and O.OP. concepts such as polymorphism.

In my opinion, there is no such thing as “the” silver bullet and all paradigms have interesting things to offer. I believe this combination of F.P. and O.O.P. can lead to an elegant way of solving the problem asked in the question.

The Question

The original question from stack overflow has an example that is perhaps harder to read or understand. This is probably because the author tried to hide the original code due to internal policy reasons. Unfortunately, class names such as A and B, or variable names such as AvsB will make the task at hand even harder, therefore I decided to rephrase a bit the question.

Let’s assume we have a FieldValue class that wraps a long number:

record FieldValue(long value){}

These values, come as input as part of a Map where the key is an Aggregator. An aggregator is an object that has an id and a type:

record Aggregator(long id, Type type) 
    public enum Type {
        SUM, AVG, MAX, MIN;
    }
}

Let’s assume this is the Map<Aggregator, FieldValue> input of our program:

Map<Aggregator, FieldValue> values = Map.of(
    new Aggregator(1L, Type.AVG), new FieldValue(10L),
    new Aggregator(2L, Type.AVG), new FieldValue(20L),
    new Aggregator(3L, Type.SUM), new FieldValue(30L),
    new Aggregator(4L, Type.SUM), new FieldValue(40L),
    new Aggregator(5L, Type.MAX), new FieldValue(50L),
    new Aggregator(6L, Type.MAX), new FieldValue(60L),
    new Aggregator(7L, Type.MIN), new FieldValue(70L),
    new Aggregator(8L, Type.MIN), new FieldValue(80L)
);

Our goal is to group the data by the Aggregator.Type and aggregate their values accordingly. Therefore we would like our function to return a Map<Aggregator.Type, Double> with the key-value pairs:

  • Type.AVG: 15 // avg(10, 20)
  • Type.SUM: 70 // sum(30, 40)
  • Type.MAX: 60 // max(50, 60)
  • Type.MIN: 70 // min(70, 80)

In fact, we can write the unit-test already:

@Test
void test() {
    //given
    Map<Aggregator, FieldValue> values = Map.of(
        new Aggregator(1L, Type.AVG), new FieldValue(10L),
        new Aggregator(2L, Type.AVG), new FieldValue(20L),

        new Aggregator(3L, Type.SUM), new FieldValue(30L),
        new Aggregator(4L, Type.SUM), new FieldValue(40L),

        new Aggregator(5L, Type.MAX), new FieldValue(50L),
        new Aggregator(6L, Type.MAX), new FieldValue(60L),

        new Aggregator(7L, Type.MIN), new FieldValue(70L),
        new Aggregator(8L, Type.MIN), new FieldValue(80L)
    );

    // when
    Map<Aggregator.Type, Double> result = aggregate(values);

    //then
    assertThat(result).isEqualTo(Map.of(
        Type.AVG, 15.0,
        Type.SUM, 70.0,
        Type.MAX, 60.0,
        Type.MIN, 70.0
    ));
}

public Map<Aggregator.Type, Double> aggregate(Map<Aggregator, FieldValue> fields) {
    // TODO: implement me!
    return new HashMap<>();
}

The Challenges

Even though it looks like a simple task, there are a few challenges we’ll encounter. First of all, we can try streaming the entry set and then collect it using Collectors.toMap or Collectors.groupBy. Unfortunately, it is impossible to group elements using different rules or different collectors (without creating your own custom collector).

Secondly, the various out-of-the-box collectors we might be tempted to use have different return types:

  • Collectors.maxBy() and minBy() — will return Optional<Value>
  • Collectors.summingDouble() and averagingDouble() — will return Double

Even if we will be using a LongStream or DoubleStream and attempt to use max(), min(), average(), and sum(), the return types will be different.

The Implementation

Firstly, let’s unwrap the data and discard the useless pieces of information, such as the aggregator id:

Map<Aggregator.Type, List<Long>> valuesByType = fields.entrySet()
    .stream()
    .collect(Collectors.groupingBy(
        entry -> entry.getKey().type(),
        Collectors.mapping(
            entry -> entry.getValue().value(),
            Collectors.toList())
    ));

After this step, the new map should look something like this:

{
  Type.AVG: [10, 20],
  Type.SUM: [30, 40],
  Type.MAX: [50, 60],
  Type.MIN: [70, 80]
}

At this point, we can re-iterate through the key-value pairs and try to apply a specific collector to each set of values:

return valuesByType.entrySet()
    .stream()
    .collect(Collectors.toMap(
        entry.getKey(),
        entry.getKey().aggregate(entry.getValue())
    ));

As we can see, we’ll keep the Aggregator.Type as key of our map, and we’ll call a collectValues that will perform the specific collector for each type.

PS: I decided to pass the key and the value as separate arguments instead of passing the whole entry not to couple this function with the internal implementation of java.util.Map

private double collectValues(Type aggregator, List<Long> values) {
    DoubleStream stream = values.stream().mapToDouble(x -> x + 0.0d);
    return switch (aggregator) {
        case AVG -> stream.average().orElseThrow();
        case SUM -> stream.sum();
        case MAX -> stream.max().orElseThrow();
        case MIN -> stream.min().orElseThrow();
        default -> throw new IllegalArgumentException();
    };
}

This new enhanced switch will allow us to call the appropriate aggregator in a declarative manner. In this example, the type is an Enum, but, Starting with Java20, we’ll be able to do it on sealed classes. This pattern is often called “ad-hoc polymorphism”.

In my opinion, this is not true polymorphism because this method still depends on the various aggregator types. Let’s do a bit of refactoring and add a bit of logic inside the Aggregator.Type:

@RequiredArgsConstructor
public enum Type {
    SUM(values -> doubleStream(values).sum()),
    AVG(values -> doubleStream(values).average().orElseThrow()),
    MAX(values -> doubleStream(values).max().orElseThrow()),
    MIN(values -> doubleStream(values).min().orElseThrow());

    private final Function<List<Long>, Double> aggregator;

    public Double aggregate(List<Long> values) {
        return aggregator.apply(values);
    }

    private static DoubleStream doubleStream(List<Long> values) {
        return  values.stream().mapToDouble(v -> v + 0.0d);
    }
}

Now, we can use the polymorphic aggregate method and remove the switch statement. Let's read through the whole code again and run the tests:

@Test
void test() {
    //given
    Map<Aggregator, FieldValue> values = // Map.of( ... );

    // when
    Map<Aggregator.Type, Double> result = aggregate(values);

    //then
    assertThat(result).isEqualTo(Map.of(
        Type.AVG, 15d,
        Type.SUM, 70d,
        Type.MAX, 60d,
        Type.MIN, 70d
    ));
}

public Map<Aggregator.Type, Double> aggregate(Map<Aggregator, FieldValue> fields) {
    Map<Aggregator.Type, List<Long>> valuesByType = fields.entrySet()
        .stream()
        .collect(Collectors.groupingBy(
            entry -> entry.getKey().type(),
            Collectors.mapping(
                entry -> entry.getValue().value(),
                Collectors.toList())
        ));
    
    return valuesByType.entrySet()
        .stream()
        .collect(Collectors.toMap(
            entry.getKey(),
            entry.getKey().aggregate(entry.getValue())
        ));
}

Code Smells?

The test is passing! We followed the question and solved the problem, great!

I just want to mention two potential code smells here. Firstly, look at the aggregate method: we collect the data into a local variable, and only after that, we use it to stream again. I believe this is the proper way of doing it. Even though it might be tempting to continue the pipeline right away, this can lead to very large and cluttered functional pipelines. So, in my opinion, we should stay away from this anti-pattern: .collect(…).stream()

Secondly, the Maps<> we are using here might not be the correct data structure in our case. We can enrich the project with more meaningful classes and types. Lines such as this one: entry.getKey().aggregate(entry.getValue()) are a clear manifestation of the “primitive obsession” anti-pattern.

If you are interested in reading more about this anti-pattern, I have an article dedicated to this topic. Feel free to dive deeper into the subject and let me know what you think:

Photo by Michiel Leunens on Unsplash

Thank You!

Thanks for reading the article and please let me know what you think! Any feedback is welcome.

If you want to read more about clean code, design, unit testing, object-oriented programming, functional programming, and many others, make sure to check out my other articles. Do you like the content? Consider following or subscribing to the email list.

Finally, if you consider becoming a Medium member and supporting my blog, here’s my referral.

Happy Coding!

Java
Spring Boot
Software Architecture
Programming
Oop
Recommended from ReadMedium