Decoradores de função usando recursos Java8

Recentemente, tenho assistido a algumas palestras de Design de programas de computador em Udacity ministradas por Peter Norvig que tratavam de decoradores de funções em Python. Isso me manteve pensando se eu poderia fazer isso em Java com expressões lambda, então eu estava brincando um pouco …

Comecei com uma implementação simples de Fibonacci para trabalhar. Usei a interface Function para implementar a função Fibonacci e adicionei um pequeno código de teste para calcular o 45º elemento da série.

private static Function<Integer, Long> fib = n -> {
if (n == 0 || n == 1) return 1L;
return Tools.fib.apply(n - 1) + Tools.fib.apply(n - 2);
};

public static void main(String[] args) {
Function<Integer, Long> tfib = fib;
long res = tfib.apply(45);
System.out.println("res = " + res);
}

Por enquanto, tudo bem. Estendi a solução com uma função de temporizador que pega uma outra função como argumento, mede o tempo de execução aproximado e o imprime no console.

private static <T, K> Function<T, K> timer(Function<T, K> f) {
Function<T, K> fun = (args) -> {
long time = System.currentTimeMillis();
K ret
= f.apply(args);
time
= System.currentTimeMillis() - time;
System.out.println("time = " + time);
return ret;
};
return fun;
}

Eu modifiquei ligeiramente a função principal para decorar a função Fibonacci. Portanto, a execução do programa agora imprime o tempo que levou para ser executado. Demorou 16 segundos em minha máquina.

public static void main(String[] args) {
Function<Integer, Long> tfib = timer(fib);
long res = tfib.apply(45);
System.out.println("res = " + res);
}

Também adicionei uma função de cache que também recebe um argumento de função e armazena o resultado da chamada de função em um cache que é um mapa onde a chave é o argumento da função e o valor é o resultado da chamada da função. Se o argumento fornecido já puder ser encontrado no cache, ele retornará esse valor pré-computado; caso contrário, ele chamará a função e armazenará o resultado para que todas as solicitações subsequentes sejam atendidas pelo cache. Decorei a função Fibonacci com a função cache . O código do cliente na função principal permaneceu o mesmo, pois eu apenas alterei a definição da função fib . Agora, quando executo o programa, ele não leva tempo para ser executado, pois a recursão pode depender dos valores no cache.

private static <T, K> Function<T, K> cache(Function<T, K> f) {
Map<T, K> cache = new HashMap<>();
Function<T, K> fun = (args) -> {
K ret
= cache.get(args);
if( ret == null ) {
ret
= f.apply(args);
cache
.put(args, ret);
}
return ret;
};
return fun;
}

private static Function<Integer, Long> fib = cache(n -> {
if (n == 0 || n == 1) return 1L;
return Tools.fib.apply(n - 1) + Tools.fib.apply(n - 2);
});

Como uma etapa final, adicionei dois campos booleanos estáticos para me permitir ligar e desligar as funções de timer e cache e coloquei um teste para eles nas funções de timer e cache .

public static final boolean CACHE_ON = true;
public static final boolean TIMER_ON = true;

if( !TIMER_ON ) return f;
if( !CACHE_ON ) return f;