Padrão de design de estratégia em Java 8

Padrão de design de estratégia em Java 8

*1. Introdução *

Neste artigo, veremos como podemos implementar o padrão de design da estratégia no Java 8.

Primeiro, forneceremos uma visão geral do padrão e explicaremos como ele é tradicionalmente implementado em versões mais antigas do Java.

Em seguida, tentaremos o padrão novamente, só que desta vez com o Java 8 lambdas, reduzindo a verbosidade do nosso código.

===* 2. Padrão de Estratégia *

*Essencialmente, o padrão de estratégia nos permite alterar o comportamento de um algoritmo em tempo de execução.*

Normalmente, começamos com uma interface usada para aplicar um algoritmo e, em seguida, implementamos várias vezes para cada algoritmo possível.

Digamos que tenhamos a exigência de aplicar diferentes tipos de descontos a uma compra, com base no Natal, na Páscoa ou no Ano Novo. Primeiro, vamos criar uma interface Discounter que será implementada por cada uma de nossas estratégias:

public interface Discounter {
    BigDecimal applyDiscount(BigDecimal amount);
}

Digamos que queremos aplicar um desconto de 50% na Páscoa e um desconto de 10% no Natal. Vamos implementar nossa interface para cada uma dessas estratégias:

public static class EasterDiscounter implements Discounter {
    @Override
    public BigDecimal applyDiscount(final BigDecimal amount) {
        return amount.multiply(BigDecimal.valueOf(0.5));
    }
}

public static class ChristmasDiscounter implements Discounter {
   @Override
   public BigDecimal applyDiscount(final BigDecimal amount) {
       return amount.multiply(BigDecimal.valueOf(0.9));
   }
}

Por fim, vamos tentar uma estratégia em um teste:

Discounter easterDiscounter = new EasterDiscounter();

BigDecimal discountedValue = easterDiscounter
  .applyDiscount(BigDecimal.valueOf(100));

assertThat(discountedValue)
  .isEqualByComparingTo(BigDecimal.valueOf(50));

Isso funciona muito bem, mas o problema é que pode ser um pouco difícil criar uma classe concreta para cada estratégia. A alternativa seria usar tipos internos anônimos, mas isso ainda é bastante detalhado e pouco prático do que a solução anterior:

Discounter easterDiscounter = new Discounter() {
    @Override
    public BigDecimal applyDiscount(final BigDecimal amount) {
        return amount.multiply(BigDecimal.valueOf(0.5));
    }
};

*3. Aproveitando o Java 8 *

Desde que o Java 8 foi lançado, a introdução de lambdas tornou os tipos internos anônimos mais ou menos redundantes. Isso significa que criar estratégias alinhadas agora é muito mais limpo e fácil.

Além disso, o estilo declarativo de programação funcional nos permite implementar padrões que antes não eram possíveis.

3.1. Reduzindo a verbosidade do código

Vamos tentar criar um _EasterDiscounter embutido, _ apenas desta vez usando uma expressão lambda:

Discounter easterDiscounter = amount -> amount.multiply(BigDecimal.valueOf(0.5));

Como podemos ver, nosso código agora é muito mais limpo e mais sustentável, alcançando o mesmo de antes, mas em uma única linha. Essencialmente,* um lambda pode ser visto como um substituto para um tipo interno anônimo *.

Essa vantagem se torna mais aparente quando queremos declarar ainda mais Discounters na linha:

List<Discounter> discounters = newArrayList(
  amount -> amount.multiply(BigDecimal.valueOf(0.9)),
  amount -> amount.multiply(BigDecimal.valueOf(0.8)),
  amount -> amount.multiply(BigDecimal.valueOf(0.5))
);

Quando queremos definir muitos _Discounters, _ podemos declará-los estaticamente, tudo em um só lugar. O Java 8 até nos permite definir métodos estáticos nas interfaces, se quisermos.

Portanto, em vez de escolher entre classes concretas ou tipos internos anônimos, vamos tentar criar lambdas em uma única classe:

public interface Discounter {
    BigDecimal applyDiscount(BigDecimal amount);

    static Discounter christmasDiscounter() {
        return amount -> amount.multiply(BigDecimal.valueOf(0.9));
    }

    static Discounter newYearDiscounter() {
        return amount -> amount.multiply(BigDecimal.valueOf(0.8));
    }

    static Discounter easterDiscounter() {
        return amount -> amount.multiply(BigDecimal.valueOf(0.5));
    }
}

Como podemos ver, estamos conseguindo muito em um código não muito.

3.2 Aproveitando a composição da função

Vamos modificar nossa interface Discounter para que ela estenda a interface UnaryOperator e, em seguida, adicione um método _combine () _:

public interface Discounter extends UnaryOperator<BigDecimal> {
    default Discounter combine(Discounter after) {
        return value -> after.apply(this.apply(value));
    }
}

Essencialmente, estamos refatorando nosso Discounter e aproveitando o fato de que aplicar um desconto é uma função que converte uma instância BigDecimal em outra instância BigDecimal , , permitindo acessar métodos predefinidos . Como o UnaryOperator vem com um método apply () _, podemos simplesmente substituir _applyDiscount por ele.

O método combine () _ é apenas uma abstração em torno da aplicação de um _Discounter aos resultados de this. Ele usa o funcional _apply () _ interno para conseguir isso.

Agora, vamos tentar aplicar vários Discounters cumulativamente a um valor. Faremos isso usando o funcional _reduce () _ e nossa _combine (): _

Discounter combinedDiscounter = discounters
  .stream()
  .reduce(v -> v, Discounter::combine);

combinedDiscounter.apply(...);

Preste atenção especial ao primeiro argumento reduce. Quando não há descontos, precisamos retornar o valor inalterado. Isso pode ser conseguido fornecendo uma função de identidade como descontos padrão.

Essa é uma alternativa útil e menos detalhada para executar uma iteração padrão. Se considerarmos os métodos que estamos saindo da caixa para composição funcional, isso também nos oferece muito mais funcionalidades gratuitamente.

4. Conclusão

Neste artigo, explicamos o padrão de estratégia e também demonstramos como podemos usar expressões lambda para implementá-lo de uma maneira menos detalhada.

A implementação destes exemplos pode ser encontrada over no GitHub. Este é um projeto baseado em Maven, portanto deve ser fácil de executar como está.