Dziś kolejny zestaw idiomów. Tym razem dwa wyrażenia, które zazwyczaj demonstruje się na prezentacjach poświęconych możliwościom jakie dają Streamy. Związane są one z przetwarzaniem kolekcji.

Listing 1. Klasyczny kod

public Collection<MyOtherEntity> filterMapCollect(Collection<MyEntity> myEntities){
   Lists<MyOtherEntity> others = new ArrayList<>();
   for(MyEntity my: myEntities){
      if(my.matchToSomeCondition()){
          others.add(convertToOther(my));
      }
   }
   return others;
}

public Collection<MyOtherEntity> mapFilterCollect(Collection<MyEntity> myEntities){
   Lists<MyOtherEntity> others = new ArrayList<>();
   for(MyEntity my: myEntities){
      MyOtherEntity other = convertToOther(my);
      if(other.matchToSomeCondition()){
          others.add(other);
      }
   }
   return others;
}

Te dwa bardzo podobne kawałki kodu można często spotkać w różnego rodzaju aplikacjach. Zasada działania jest prosta. Mamy kolekcję obiektów jednego typu i zamieniamy ją na kolekcję obiektów innego typu. Przy czym po drodze sprawdzamy czy nasze wejściowe obiekty spełniają pewien warunek, metoda filterMapCollect, albo czy nasze wyjściowe obiekty spełniają jakiś warunek, metoda mapFilterCollect.

Kod ten można oczywiście bardzo ładnie zamienić na:

Listing 2. Kod po zmianach

public Collection<MyOtherEntity> filterMapCollect(Collection<MyEntity> myEntities){
   return myEntities.stream()
             .filter(MyEntity::matchToSomeCondition)
             .map(this::convertToOther)
             .collect(Collectors.toList());
}

public Collection<MyOtherEntity> mapFilterCollect(Collection<MyEntity> myEntities){
   return myEntities.stream()
             .map(this::convertToOther)
             .filter(MyOtherEntity::matchToSomeCondition)
             .collect(Collectors.toList());

}

Jednak to rozwiązanie nie jest jeszcze ostatecznym. W swojej obecnej postaci jest związane z konkretnym typem. Dlatego kolejnym krokiem jest wyekstrahowanie tych metod do osobnego bytu, tu interfejsu

Listing 3. Kod generyczny

public interface FmcMfc {

    default <I, O> Collection<O> fmc(Collection<I> input, Predicate<I> filter, Function<I, O> map) {
        return input.stream()
                .filter(filter)
                .map(map)
                .collect(Collectors.toList());
    }

    default <I, O> Collection<O> mfc(Collection<I> input, Function<I, O> map, Predicate<O> filter) {
        return input.stream()
                .map(map)
                .filter(filter)
                .collect(Collectors.toList());
    }
}

I teraz jak gdzieś potrzebujemy tego typu rozwiązania to implementujemy nasz interfejs i gotowe. Zaletą tego podejścia jest możliwość pisania w pełni deklaratywnego kodu bez wnikania jak będzie zachowywać się on szczegółach. Podajemy kolekcję oraz dwie operacje. Czy pod spodem będzie użyty Stream czy pętla nie ma dla nas znaczenia.

Tylko błagam was, nie róbcie z tego osobnej biblioteki.