DEV Community

Uiratan Cavalcante
Uiratan Cavalcante

Posted on

Design Orientado a Comportamento em Java: Strategy, Lambdas e Design Moderno em Java

Grande parte dos desenvolvedores Java utiliza lambdas diariamente sem refletir muito sobre o que está acontecendo por trás delas. A sintaxe é simples, elegante e econômica. No entanto, essa simplicidade esconde uma série de decisões de design, conceitos de programação funcional e mecanismos sofisticados da JVM.

Para entender lambdas de verdade, é preciso começar antes delas.

O problema concreto

Considere uma classe de domínio simples que representa uma transação bancária:

public class BankTransaction {

    private final LocalDate date;
    private final double amount;
    private final String description;

    public BankTransaction(LocalDate date, double amount, String description) {
        this.date = date;
        this.amount = amount;
        this.description = description;
    }

    public LocalDate getDate() { return date; }
    public double getAmount() { return amount; }
    public String getDescription() { return description; }
}
Enter fullscreen mode Exit fullscreen mode

Agora imagine que precisamos calcular o total movimentado em um determinado mês:

public double calculateTotalInMonth(Month month) {

    double result = 0;

    for (BankTransaction tx : bankTransactions) {
        if (tx.getDate().getMonth() == month) {
            result += tx.getAmount();
        }
    }

    return result;
}
Enter fullscreen mode Exit fullscreen mode

O parâmetro month é um java.time.Month, um enum introduzido no Java 8 junto com a nova API de datas. Ele representa meses do ano de forma tipada e segura.

Esse método resolve o problema de forma direta. Ele percorre a lista, verifica uma condição e acumula valores. É simples e perfeitamente aceitável. O ponto interessante surge quando começamos a imaginar variações do mesmo cálculo: total por descrição, total acima de determinado valor, contagem de transações, maior valor do mês, e assim por diante.

Se observarmos com atenção, perceberemos que a estrutura do algoritmo — percorrer e acumular — permanece invariável. O que se altera é apenas a regra aplicada a cada elemento.

Esse detalhe é mais importante do que parece.


Separando estrutura de comportamento

Quando a estrutura do algoritmo permanece constante e apenas a regra muda, estamos diante de um cenário clássico para aplicação do padrão Strategy.

O padrão Strategy consiste em encapsular um comportamento variável em um objeto separado e passá-lo para quem executa a lógica estrutural. Em vez de codificar a regra diretamente no método, delegamos essa responsabilidade a um componente externo.

Podemos definir esse contrato da seguinte forma:

@FunctionalInterface
public interface BankTransactionSummarizer {
    double summarize(double accumulator, BankTransaction bankTransaction);
}
Enter fullscreen mode Exit fullscreen mode

Essa interface descreve uma regra de acumulação: dado um valor atual (accumulator) e uma transação, ela retorna o novo acumulador.

Aqui surge o conceito de interface funcional.


Interface funcional e sua origem

Uma interface funcional é simplesmente uma interface com um único método abstrato. O conceito não é novo; o que surgiu no Java 8 foi o reconhecimento formal dele e a anotação @FunctionalInterface, que permite ao compilador validar essa restrição.

Exemplos clássicos da própria JDK incluem Runnable, Callable, Comparator e Function<T,R>. Todas possuem apenas um método abstrato.

O motivo pelo qual isso é relevante é que uma interface com um único método pode representar, conceitualmente, uma função. Isso abre espaço para tratar comportamento como um valor.


Antes do Java 8: classes anônimas

Antes das lambdas, implementar esse contrato exigia uma classe anônima:

public double calculateTotalInMonth(Month month) {

    return summarizeTransactions(
        new BankTransactionSummarizer() {
            @Override
            public double summarize(double acc, BankTransaction tx) {
                if (tx.getDate().getMonth() == month) {
                    return acc + tx.getAmount();
                }
                return acc;
            }
        }
    );
}
Enter fullscreen mode Exit fullscreen mode

Uma classe anônima é uma classe sem nome declarado explicitamente, definida e instanciada no mesmo ponto. Ela existe desde o Java 1.1 e sempre foi o mecanismo padrão para passar comportamento como argumento.

Note que o conceito fundamental já estava presente: comportamento sendo passado como parâmetro. O problema era a verbosidade.


Programação funcional: o ponto central

Quando falamos em programação funcional, não estamos falando apenas de sintaxe enxuta. Estamos falando de um paradigma no qual funções são tratadas como valores.

Em termos práticos, isso significa que podemos:

  • Passar funções como parâmetros
  • Retornar funções
  • Compor funções
  • Trabalhar com estruturas imutáveis

Passar comportamento como parâmetro é central porque eleva o nível de abstração do design. Em vez de escrever múltiplas variações de um mesmo algoritmo, definimos a estrutura uma única vez e variamos apenas a lógica aplicada.

Essa é a essência do que estamos fazendo ao separar o loop da regra.


A chegada das lambdas

Com o Java 8, a classe anônima pode ser substituída por:

public double calculateTotalInMonth(Month month) {
    return summarizeTransactions(
        (acc, tx) ->
            tx.getDate().getMonth() == month
                ? acc + tx.getAmount()
                : acc
    );
}
Enter fullscreen mode Exit fullscreen mode

A expressão (acc, tx) -> ... é apenas uma forma reduzida de implementar o método summarize.

Não é um novo tipo de objeto. Não é um novo conceito de orientação a objetos. É uma forma sintática mais natural de representar algo que já existia: a implementação de uma interface com único método.


Closure: capturando o contexto

Observe que a lambda utiliza a variável month, mesmo ela não sendo parâmetro da função.

Isso é um exemplo de closure.

Closure é uma função que captura variáveis do escopo externo. Em Java, apenas variáveis finais ou efetivamente finais podem ser capturadas. O valor é preservado no momento da criação da lambda.

Internamente, isso significa que o valor capturado é armazenado como um campo do objeto gerado dinamicamente.

Conceitualmente, é como se o compilador produzisse algo equivalente a:

class LambdaImpl implements BankTransactionSummarizer {

    private final Month month;

    LambdaImpl(Month month) {
        this.month = month;
    }

    public double summarize(double acc, BankTransaction tx) {
        if (tx.getDate().getMonth() == month) {
            return acc + tx.getAmount();
        }
        return acc;
    }
}
Enter fullscreen mode Exit fullscreen mode

O que acontece no bytecode

Compile a classe e execute:

javap -c -p NomeDaClasse
Enter fullscreen mode Exit fullscreen mode

Você verá algo semelhante a:

public double calculateTotalInMonth(java.time.Month);
  Code:
     0: aload_0
     1: aload_1
     2: invokedynamic #2,  0
     7: invokevirtual #3
    10: dreturn
Enter fullscreen mode Exit fullscreen mode

A instrução relevante é invokedynamic.

Ela foi introduzida no Java 7, mas passou a ser usada amplamente no Java 8 para dar suporte a lambdas. Diferentemente das classes anônimas, lambdas não geram automaticamente um .class separado. Em vez disso, o compilador gera um método sintético contendo o corpo da lambda e utiliza invokedynamic para que a JVM crie dinamicamente a implementação da interface em tempo de execução, via LambdaMetafactory.

Esse mecanismo permite otimizações mais sofisticadas pelo JIT, além de reduzir o custo estrutural que existia com classes anônimas tradicionais.


Conclusão

Quando você escreve uma lambda simples para somar valores, está aplicando uma sequência de conceitos que atravessam múltiplos níveis da linguagem:

  • Encapsulamento de comportamento (Strategy)
  • Interface funcional
  • Programação funcional
  • Closure
  • Geração dinâmica via invokedynamic

A linha parece simples, mas ela é o ponto de convergência entre design de software e engenharia de linguagem.

Entender isso muda a forma como enxergamos o código. Lambdas deixam de ser apenas uma forma elegante de escrever menos e passam a ser uma ferramenta consciente de modelagem de comportamento.

Top comments (0)