Estou lendo um livro muito legal do renomado Venkat Subramaniam : "Functional Programming in Java: Harnessing the Power of Java 8 Lambda Expression" e por estar gostando muito, decidi publicar posts com o intuido de fixar meu aprendizado e assim também ajudar quem interessar!
Mude a sua forma de pensar ao escrever seus códigos
Desde sua concepção, o Java tem fornecido uma forma imperativa para escrevermos nossos códigos, isto é: dizemos ao Java, passo a passo, o que queremos que ele faça e assim ter o resultado esperado. Essa forma tem funcionado, porém códigos imperativos tendem a serem verbosos e difíceis de manter. Para melhor descrever esse cenário, vamos programar um pouco! :)
Vamos dizer que temos uma certa necessidade em nossa regra de negócio: precisamos verificar se a cidade "São Paulo" faz parte de uma lista de cidades fornecida, para que assim possamos efetuar outras regras de negócios...
Pois bem, talvez você tenha imaginado construir um código similar conforme abaixo:
boolean found = false;
for (String city : cities) {
if ("São Paulo".equals(city)) {
found = true;
break;
}
}
System.out.println("Found São Paulo? " + found);
No código acima, podemos ver um forma imperativa de busca para verificar se São Paulo está contido na coleção cities.
Mas o que é utilizar o estilo imperativo?
Estilo Imperativo - descrevemos explicitamente COMO o programa deve fazer suas tarefas especificando cada instrução passo a passo.
Apesar de simples, é um código nada intuitivo de se ler e compreender; há variáves que, apesar de serem necessárias para computar nossa busca, acabam ofuscando o que realmente o código tem a intenção de executar, deixando-o distante da linguagem de negócio.
Primeiramente, definimos a varíavel boleana nomeada "found" que será nossa flag que dirá se encontramos ou não a cidade, e então iteramos entre os elementos da coleção cities. Se encontrarmos a cidade que estamos procurando então definimos a flag como "true" e paramos a iteração na lista. Daí então, escrevemos na saída padrão o resultado de nossa busca.
Além de cansativo, traz muitos detalhes de implementação que ofuscam não acham!?
Como desbravadores da linguagem Java, no minuto que olhamos esse código nós rapidamente pensaremos em uma construção mais concisa e fácil de ler, ao parecido com isso:
System.out.println("Found São Paulo?:" + cities.contains("São Paulo"));
Que diferença não é!?
Isso é um exemplo do estilo declarativo: agora podemos rapidamente compreender a intenção do código para o negócio de maneira mais concisa e direta.
Estilo Declarativo - descrevemos O QUE o programa deve fazer sem explicitamente dizer COMO ele deve fazer.
Outras melhorias que podemos perceber em nosso código com essa abordagem:
- Não há bagunça em torno do código com variaveis mutáveis;
- A iteração na lista ocorrem "debaixo do capô";
- Menos desordem;
- Mais clareza; Foco na regra de negócio;
- Menos impedância: o código segue a intenção do negócio;
- Menos propenso a erros;
- Maior facilidade de entendimento e manutenção;
Bom, concordo que estamos olhando um exemplo simples - uma método para checar se um elemento está presente em uma coleção já está faz tempo no Java.
Mas agora, conhecendo esses benefícios na utilização desse estilo "declarativo", por que não utilizar essa abordagem para não escrever códigos imperativos para operações mais avançadas como por exemplo: processamento com dados complexos (oriundos de fontes externas: banco de dados, web, ou arquivos), programação concorrente, etc.?
Calma, mostre-me mais algum exemplo mais avançado!
Além de casos simples
Vamos olhar um outro exemplo:
Vamos definir uma coleção de preços e vamos desenvolver algumas formas para calcular descontos 1:
var currency = Monetary.getCurrency("BRL");
final var prices = Arrays.asList(
Money.of(10, currency),
Money.of(30, currency),
Money.of(17, currency),
Money.of(20, currency),
Money.of(15, currency),
Money.of(18, currency),
Money.of(45, currency),
Money.of(12, currency));
Vamos dizer que queremos o total de preços que sejam maiores que BRL 18.00, aplicando 15% de desconto.
Uma forma habitual
Em uma forma habitual, chegaríamos um código como o abaixo:
var totalOfDiscountedPrices = Money.of(0, currency);
for (MonetaryAmount price : prices) {
if (price.compareTo(Money.of(18, currency)) > 0) {
totalOfDiscountedPrices = totalOfDiscountedPrices.add(price.multiply(0.85));
}
}
System.out.println("Total of discounted prices: " + totalOfDiscountedPrices);
Apesar de ser um código bem familiar, é interessante analisá-lo:
- Primairamente iniciamos a variável para mantermos o total dos preços com desconto;
- Iteramos na coleção de preços, pegando os preços que forem maior que BRL 18, e para cada preço encontrado, computamos o preço com desconto e adicionamos ao valor total;
- E para terminar, escrevemos o total dos preços com desconto na saída da aplicação.
Aqui está a saída que o código produziu:
Total of discounted prices: BRL 80.75
Funcionou conforme esperado, mas a sensação de sugeira ao escrever esse código com certeza veio a tona. Muitos de nós (incluindo eu, é claro) aprendemos assim e utilizavamos o que podíamos de acordo com a ferramentas que temos. Se olharmos esse código, ele está cuidando de coisas bem baixo nível perto do que o negócio em si pede.
Códigos como esse sofre de "Primitive Obsession" 2 além de desafiarem o princípio de responsabilidade única do "SOLID" (listamos, filtramos e calculamos em um só lugar - multipla responsabilidade).
Bom, podemos fazer melhor...realmente bem melhor!
Olhando com calma, podemos deixar o nosso código mais próximo da necessidade do negócio, respeitando o requerimento especificado e, diminuindo assim chances de má interpretação.
Vamos evitar criar variáveis mutáveis e manipulá-las atribuindo valores e vamos trazer as coisas para um nível maior de abstração, assim como no código abaixo:
var totalOfDiscountedPrices = prices
.stream()
.filter(price -> price.compareTo(Money.of(18, currency)) > 0)
.map(price -> price.multiply(0.85))
.reduce(Money.of(0, currency), Money::add);
System.out.println("Total of discounted prices: " + totalOfDiscountedPrices);
E ao executar esse código, temos a mesma saída conforme mostrado no exemplo imperativo:
Total of discounted prices: BRL 80.75
Como em nosso primeiro exemplo, nós reduzimos longas linhas de código com um encadeiamento de métodos a partir do método stream() a partir da lista de preços.
O código está conciso, nós conseguimos até ler em voz alta e assim perceber o quanto próximo da especificação do requerimento estamos:
- filtrar os preços que foram maior que BRL 18.00;
- mapeie o preço atual para um novo preço aplicando o desconto de 15% e...
- reduza os preços com desconto somando os e armazenando na variável **totalOfDiscountedPrices**.
A utilização do método stream() fornecido pelas Collections do Java trazem um tipo especial de iterator com uma variedade de funções especiais que poderemos trazer em próximos posts!
Ao invés de explicitamente iterar entre os preços na lista (de maneira imperativa), nós delegamos essa iteração para a implementação interna do iterator e utilizamos alguns métodos especiais, como o filter, onde definimos o a condição do filtro, e o map, onde dado um valor podemos transformar em outro, e assim conseguir o comportamento desejado de forma declarativa:
Novamente, podemos confirmar essas melhorias:
Melhorias utilizando um estilo declarativo
- Não há bagunça em torno do código com variaveis mutáveis;
- A iteração na lista ocorrem "debaixo do capô";
- Menos desordem;
- Mais clareza; Foco na regra de negócio;
- Menos impedância: o código segue a intenção do negócio;
- Menos propenso a erros;
- Maior facilidade de entendimento e manutenção;
Com isso, compreendo o porque é interessante repensar nossa maneira de programar e procurar utilizar o estilo declarativo em nossos códigos.
Estou lendo um livro muito legal do renomado Venkat Subramaniam : "Functional Programming in Java: Harnessing the Power of Java 8 Lambda Expression" e por estar gostando muito, decidi publicar posts com o intuido de fixar meu aprendizado e assim também ajudar quem interessar!
Source dos exemplos 3:
- https://github.com/dearrudam/learning-notes/blob/main/java/ImperativeSample01.java
https://github.com/dearrudam/learning-notes/blob/main/java/DeclarativeSample01.java
https://github.com/dearrudam/learning-notes/blob/main/java/ImperativeDiscount.java
https://github.com/dearrudam/learning-notes/blob/main/java/DeclarativeDiscount.java
Top comments (10)
Interessante, mas acho que o estilo imperativo é verboso mas muito mais simples de ler e entender. Em uma passada no código você consegue absorver praticamente tudo o que o criador do código quiz fazer. O estilo declarativo é interessante para casos isolados e de baixa complexidade, quando aumenta a complexidade e níveis de condicionais, fica extremamente difícil de ler e compreender. O caso da API de Streams é um exemplo, quando é necessário muita complexidade e lógica condicional, vira um monstrengo que pode te tomar muito tempo até conseguir absorver a intenção do criador. Já peguei alguns códigos com Stream que eram de chorar, se fossem feitos "à moda clássica" teriam ficado mais simples de entender e manter.
Obrigado demais pelo comentário! Eu também tenho esse viés, pois também venho do modo clássico, e sabemos que, tudo depende do contexto que estamos tentando resolver! Sempre temos que validar também a carga cognitiva que é necessária para quem for dar manutenção ao código em si. Respeito muito seu ponto de vista, mas minha opinião é que talvez, dividindo o problemas em pequenos problemas, não colocando muita regra de negócio nas expressões lambdas, o que é uma má prática pois lambdas devem ser glue code (blog.agiledeveloper.com/2015/06/la...), utilizando method reference de maneira consciente, seja possível ter talvez ter um código mais elegante, conciso.
Muito bom! Além de todos os conceitos extremamente válidos e importantes, acrescento o meu ponto de vista onde também tenho uma tendência maior pelo modo clássico, só que é inegável que o modo declarativo facilita muito e aproxima mais da lógica de negócio, o problema é entender bem o que o declarativo faz para ser possível um entendimento rápido e claro.
Então exige um certo aprofundamento, que pode criar uma certa resistência.
A resistência é algo natural nos seres humanos que evitam ao máximo aprender coisas novas, todos nós queremos ficar pela nossa zona de conforto e ir apenas um pouco mais além, mas não muito mais, aí precisamos de uma forte motivação.
Falo por mim, que há bem mais de 10 anos que o Java suporta bem uma programação mais funcional, e ainda estou precisando aprender como utilizar melhor.
Nos exemplos, esta forma de trabalhar com dinheiro eu não conhecia, achei muito legal, então estas dependências vou ter que marcar como referência que vai dar um jeitão, javamoney e moneta. Valeu!
Obrigado pelo comentário Edu!!! Sim, concordo contigo sobre a tendência maior pelo modo clássico, que na verdade é muito mais familiar para nós. Sobre a parada sobre dinheiro, segue o link da especificação: jcp.org/en/jsr/detail?id=354 ... ah... detalhe interessante: um dos líderes da especificação: Otavio Santana!!! Muito massa, não!?! Abraços!!!
Muito bom, Max. Acredito que a API de Streams é a responsável por até hoje termos sistemas em produção usando o Java 8. Foi realmente uma revolução para a linguagem. Parabéns pelo post!!!
Obrigado João! Estou muito feliz que tenha gostado! Sim, também acredito nessa evolução conquistada através das Lambdas Expressions, interfaces funcionais, Streams API! Realmente deixou o Java com um jeito bem mais elegante de trabalhar!
Muito bom o texto. E com referências! Parabéns! :)
Que bom que gostou!!! Valeu, Rodrigo!!!
Muito bom mesmo, gostei!!! s2
Fico feliz que tenha gostado! Vamos aprendendo juntos!