Skip to content

mtumilowicz/java11-lambda-patterns

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

63 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

java11-lambda-patterns

Functional programming patterns in java.

Reference: https://www.youtube.com/watch?v=YnzisJh-ZNI
Reference: https://www.youtube.com/watch?v=e4MT_OguDKg&index=146
Reference: https://www.amazon.com/Modern-Java-Action-functional-programming/dp/1617293563

introduction

This repo is a mix of functional design patterns that we have seen in books or on the internet.

project description

  1. try to design you API in a composable way (package: composable)

    class ShoppingAPI {
        static Function<List<Item>, Cart> buy() {
            return Cart::new;
        }
    
        static Function<Cart, Order> order() {
            return Order::new;
        }
    
        static Function<Order, Delivery> deliver() {
            return Delivery::new;
        }
    
        static Function<List<Item>, Delivery> oneClickBuy() {
            return buy()
                    .andThen(order())
                    .andThen(deliver());
        }
    }
    
    • other used classes are as simple as they can be:
      @Value
      class Cart {
          ImmutableList<Item> items;
      
          Cart(List<Item> items) {
              this.items = ImmutableList.copyOf(items);
          }
      }
      
      @Value
      class Delivery {
          Order order;
      }
      
      @Value
      class Item {
          int id;
      }
      
      @Value
      class Order {
          Cart cart;
      }
      
  2. it's often helpful to use currying(https://github.com/mtumilowicz/groovy-closure-currying) and functional interfaces to design API (package: converter)

    @FunctionalInterface
    interface CurrableDoubleBinaryOperator extends DoubleBinaryOperator {
    
        default DoubleUnaryOperator rate(double u) {
            return t -> applyAsDouble(t, u);
        }
    }
    

    then we can easily implement conversion classes

    class RateConverter implements CurrableDoubleBinaryOperator {
    
        @Override
        public double applyAsDouble(double value, double rate) {
            return value * rate;
        }
    
        static DoubleUnaryOperator milesToKmConverter() {
            return new RateConverter().rate(1.609);
        }
    
        static DoubleUnaryOperator celsiusToFahrenheitConverter() {
            return new RateConverter().rate(1.8).andThen(x -> x + 32);
        }
    }
    
  3. use tuples and know the stream API (package: customer)

    @Value
    @Builder
    public class Customer {
        ImmutableList<Order> orders;
        ImmutableList<Expense> expenses;
        
        // ... methods
    }
    
    @Value
    @Builder
    class Expense {
        Year year;
        ImmutableSet<String> tags;
    
        Stream<String> getTagsStream() {
            return SetUtils.emptyIfNull(tags).stream();
        }
    }
    

    and we want to:

    • find order with max price
      Optional<Order> findOrderWithMaxPrice() {
          return ListUtils.emptyIfNull(orders).stream()
                  .filter(Order::hasPrice)
                  .max(comparing(Order::getPrice));
      
      }
      
    • find top3 orders by price
      Triple<Order, Order, Order> findTop3OrdersByPrice() {
          return ListUtils.emptyIfNull(orders).stream()
                  .filter(Order::hasPrice)
                  .sorted(comparing(Order::getPrice, reverseOrder()))
                  .limit(3)
                  .collect(collectingAndThen(toList(), ListToTripleConverter::convert));
      }
      
    • construct an immutable map with (key, value) = (year, tags from that year)
      ImmutableMap<Year, Set<String>> yearTagsExpensesMap() {
          return ListUtils.emptyIfNull(expenses).stream()
                  .collect(collectingAndThen(groupingBy(Expense::getYear, flatMapping(Expense::getTagsStream, toSet())),
                          ImmutableMap::copyOf)
                  );
      }
      
  4. try to avoid decorator pattern - use function composition instead (package: decorator)

    @Value
    @RequiredArgsConstructor
    class Camera {
        Function<Color, Color> transformColors;
    
        Camera() {
            this.transformColors = Function.identity();
        }
    
        Camera withFilter(Function<Color, Color> transform) {
            return new Camera(transformColors.andThen(transform));
        }
    
        Color snap(Color color) {
            return transformColors.apply(color);
        }
    }
    

    and a library of functions to transform colors

    class ColorTransformers {
        static Color brighten(Color color, int modifier) {
            Preconditions.checkArgument(nonNull(color));
            Preconditions.checkArgument(modifier >= 0);
    
            return new Color(red(color) + modifier,
                    green(color) + modifier,
                    blue(color) + modifier);
        }
    
        static Color negate(Color color) {
            Preconditions.checkArgument(nonNull(color));
    
            return new Color(negate(red(color)), negate(green(color)), negate(blue(color)));
        }
    
        private static int negate(int color) {
            Preconditions.checkArgument(color <= 255);
            Preconditions.checkArgument(color >= 0);
    
            return 255 - color;
        }
    
        private static int red(Color color) {
            return color.getRed();
        }
    
        private static int green(Color color) {
            return color.getGreen();
        }
    
        private static int blue(Color color) {
            return color.getBlue();
        }
    }
    
  5. create complex DSL with hiding creation inside (package: dsl)

    @Value
    @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
    public class Mailer {
        private static final Mailer EMPTY = new Mailer();
    
        String from;
        String to;
    
        private Mailer() {
            this.from = "";
            this.to = "";
        }
    
        Mailer from(String from) {
            return new Mailer(StringUtils.defaultIfEmpty(from, ""), to);
        }
    
        Mailer to(String to) {
            return new Mailer(from, StringUtils.defaultIfEmpty(to, ""));
        }
    
        static void send(UnaryOperator<Mailer> block) {
            System.out.println(block.apply(EMPTY));
        }
    }
    

    and the example of usage:

    Mailer.send(
        mailer -> mailer.from("mtumilowicz01@gmail.com")
                        .to("abc@o2.pl")
    )
    

    note that at any point we don't have direct access to the object, we cannot create object manually and we cannot reuse it (there is NO Mailer object)

  6. know the comparator API (package: person)

    suppose we want to compare person by name, then by surname (if surname is null goes first)

    @Value
    @Builder
    class Person {
        static final Comparator<Person> NAME_SURNAME_COMPARATOR = comparing(Person::getName)
                .thenComparing(Person::getSurname, nullsFirst(naturalOrder()));
    
        String name;
        String surname;
    }
    

    and tests:

    given:
    def B_B = Person.builder().name("B").surname("B_B").build()
    def C_A = Person.builder().name("C").surname("C_A").build()
    def A = Person.builder().name("A").surname("A").build()
    def B_A = Person.builder().name("B").surname("B_A").build()
    def C_null = Person.builder().name("C").surname(null).build()
    def C_null2 = Person.builder().name("C").surname(null).build()
    
    when:
    def list = List.of(B_B, C_A, A, B_A, C_null, C_null2)
            .stream()
            .sorted(Person.NAME_SURNAME_COMPARATOR)
            .collect(toList())
    
    then:
    list == [A, B_A, B_B, C_null, C_null2, C_A]
    
  7. compose behaviours instead of accumulating objects in lists (package: salary)

    suppose we want to calculate salary according to some salary rules

    public enum SalaryRules {
        TAX(new RateConverter().rate(0.81)),
        BONUS(new RateConverter().rate(1.2)),
        ADDITION(salary -> salary + 100);
    
        public final DoubleUnaryOperator operator;
    
        SalaryRules(DoubleUnaryOperator operator) {
            this.operator = operator;
        }
    }
    
    • naive approach
      class NaiveSalaryCalculator {
          final List<SalaryRules> operators = new LinkedList<>();
      
          NaiveSalaryCalculator with(SalaryRules rule) {
              operators.add(rule);
      
              return this;
          }
      
          double calculate(double salary) {
              return operators.stream()
                      .map(rule -> rule.operator)
                      .reduce(DoubleUnaryOperator.identity(), DoubleUnaryOperator::andThen)
                      .applyAsDouble(salary);
      
          }
      }
      
    • better approach - composing functions
      class SalaryCalculator {
          private final DoubleUnaryOperator operator;
      
          SalaryCalculator() {
              this(DoubleUnaryOperator.identity());
          }
      
          private SalaryCalculator(DoubleUnaryOperator operator) {
              this.operator = operator;
          }
      
          SalaryCalculator with(SalaryRules rule) {
              return new SalaryCalculator(operator.andThen(rule.operator));
          }
      
          double calculate(double salary) {
              return operator.applyAsDouble(salary);
      
          }
      }
      
    • tests
      given:
      def calculator = new SalaryCalculator().with(SalaryRules.BONUS)
              .with(SalaryRules.ADDITION)
              .with(SalaryRules.TAX)
      
      expect:
      calculator.calculate(1000) == 1053
      
  8. strategy pattern (library of functions) (package: strategy)

    we have PriceProvider to get current stock price (Stock class is as simple as possible)

    @Value
    class PriceProvider {
        @Getter(AccessLevel.NONE)
        IntUnaryOperator priceSource;
    
        int getPrice(int id) {
            return priceSource.applyAsInt(id);
        }
    }
    
    @Value
    class Stock {
        int id;
    }
    

    and suppose we want to calculate prices for a given stream of stocks (with some custom filtering)

    @Value
    class Calculator {
        PriceProvider priceProvider;
    
        int totalValues(List<Stock> integers, IntPredicate take) {
            return integers.stream()
                    .map(Stock::getId)
                    .mapToInt(priceProvider::getPrice)
                    .filter(take)
                    .sum();
        }
        
        // library of functions
        static IntPredicate priceLessThan(int limit) {
            return it -> it < limit;
        }
    
        static IntPredicate priceEquals(int limit) {
            return it -> it == limit;
        }
    }
    

    suppose we want to sum stocks with prices < 3 or prices == 5

    given:
    def stocks = [new Stock(1),
                  new Stock(2),
                  new Stock(3),
                  new Stock(4),
                  new Stock(5),
                  new Stock(6),
                  new Stock(7)]
    
    def calculator = new Calculator(new PriceProvider(IntUnaryOperator.identity()))
    
    when:
    def sum = calculator.sumPrices(stocks, Calculator.priceLessThan(3) | Calculator.priceEquals(5))
    
    then:
    sum == 8
    

Releases

No releases published

Packages

No packages published