Preciso confessar, até pouco tempo atrás eu achava que era uma mera diferença de termos, e que na verdade os dois eram a mesma coisa! E se tu também acreditava nisso, bem vindo ao clube :)
A imagem acima inclusive deixa bem claro que ambos são muito parecidos pra quem utiliza, tanto que faz parecer que são a mesma coisa, mas por baixo dos panos é que tudo muda!
Nesse artigo aqui vamos explorar as diferenças entre os dois e também vamos aproveitar pra dar uma olhada em como isso se conecta com o conceito de programação orientada a aspectos (AOP), o que são cross-cutting concerns e como isso pode te ajudar a desenhar melhor o teu código!
Anotações
Pelo clubismo, vou começar escrevendo sobre o que temos no Java: as anotações. Anotações são basicamente formas de se adicionar metadados no código. É isso. Poderiamos até parar por aqui que já estaria “respondido” essa questão, mas é claro que isso não responde nada, né?
As anotações por si só não conseguem fazer muita coisa, elas só adicionam metadados mesmo. Nós precisamos ter algum outro componente que seja capaz de processar elas, para que daí sim, a mágica aconteça.
Esse processamento pode ser feito em tempo de compilação, durante o build do projeto, ou em tempo de execução, utilizando reflections.
Exemplos bem famosos de coisas mágicas que processam anotações em tempo de compilação são o Lombok, MapStruct e o Micronaut. Já em tempo de execução, temos como exemplo coisas como o Spring, Hibernate, OpenFeign (E o Retrofit, o “FeignClient do Android”, também!)
Decorators
Os decorators são coisas muito parecidas com anotações para quem está usando, mas pra quem está criando, são bastante diferentes.
Diferente das anotações, decorators são funções de fato, e são “chamados” automaticamente durante a execução do programa. Além de ter a função de adicionar metadados no código, como as anotações, os decorators recebem como parâmetro as informações do elemento que foi decorado com eles, e com isso, eles mesmos são capazes de se “plugar” no elemento anotado e realizar operações em cima da execução desse elemento.
Um exemplo bem interessante são os decorators do Python, que nos permitem executar código “em volta” da execução de uma outra função que tenha sido decorada, e até substituir o valor retornado pela função decorada por alguma outra coisa se desejarmos. Abaixo, um pequeno exemplo de como isso funciona:
def decorator_exemplo(func):
def wrapper(*args, **kwargs):
print("Isso está acontecendo antes da função ser executada.")
result = func(*args, **kwargs)
print("Isso está acontecendo depois da função ser executada.")
return result
return wrapper
@decorator_exemplo
def say_hello(name):
print(f"Hello, {name}!")
say_hello("Sophia")
O resultado desse exemplo no console é o seguinte:
Isso está acontecendo antes da função ser executada.
Hello, Sophia!
Isso está acontecendo depois da função ser executada.
Dessa forma, podemos concluir que os decorators são como as anotações, mas não precisam necessariamente que outro componente processe-os. Mas no dia a dia, geralmente, ambos anotações e decorators servem para o mesmo fim, e normalmente funcionam de maneiras similares. Isso que demonstrei acima com Python tem um equivalente no Java, o qual é chamado de dynamic proxy. Mais abaixo, vamos explorar um pouco mais dessas coisas.
Programação Orientada a Aspectos (AOP)
Decorators e anotações tem tudo a ver com AOP, e vamos aproveitar o embalo para falar um pouquinho sobre isso.
Definição
AOP é um paradigma que nasceu pra resolver um problema clássico no design de software:
Como separar as regras de negócio puras daquelas “features tecnicas” que ficam poluindo o código?
Essas “features tecnicas” são chamadas de aspectos. Se tu não sacou o que isso quer dizer ainda, vamos dar nome aos bois: Cache, autenticação, controle de acesso, logs, configurações, manipulação de erros, monitoramento, concorrência, entre muitos outros. Tudo isso são de fato funcionalidades do software mas são puramente tecnicas, não tem a ver com a lógica de negócio pura. São essas coisas que são chamadas de aspectos.
Aspectos servem para lidar com as chamadas “cross-cutting concerns” (Preocupações transversais em português), elas são o “por quê”, enquanto os aspectos são o “como”. Se precisamos de mais agilidade no retorno de uma informação para melhorar a experiência da aplicação, temos uma preocupação transversal, e para resolver isso, podemos usar um aspecto: @Cacheable
.
Um baita exemplo de AOP é a lib Resilience4J do Java, que nos proporciona vários decorators para lidar com operações comuns relacionadas a tolerância a falhas. Vou deixar um exemplo de código que implementa uma estrutura de retry totalmente transparente, um perfeito exemplo de AOP:
@Retry(name = "buscarUsuarioRetry", maxAttempts = 3)
public User buscarUsuario(String id) {
return httpClient.getUser(id);
}
Simples assim, sem precisar fazer nada a mais. Apenas precisamos colocar uma anotação, e nosso método já conseguirá fazer até três retentativas de execução em caso de falhas. Esse é o poder do AOP, e é assim que AOP, anotações e decorators se conectam.
No geral, AOP é utilizada para nos ajudar a abstrair preocupações que se repetem muito na aplicação. AOP não visa quebrar a separação arquitetural entre “camada de negócio” e “camada de aplicação”, mas sim, limpar ao máximo a camada de aplicação das preocupações transversais e permitindo um maior enfoque na regra de negócio pura.
Indo mais a fundo — Anotações: Compile vs Runtime
A partir desse ponto do artigo, vamos começar um mergulho profundo no assunto anotações, estudando como funcionam no bit e linkando com o que vimos até agora. Se tu leu até aqui, muito obrigado desde já! Aqui tu já vai ter pego a ideia principal do assunto. Mas se tu quiser saber ainda mais, bora continuar!
Anotações podem ser processadas em tempo de compilação ou execução, como já vimos anteriormente. Ambos podem atingir os mesmos objetivos, mas funcionam de maneiras diferentes.
Anotações que são processadas em tempo de compilação geralmente são usadas para geração de código (Lombok literalmente gera o código dos boilerplates, MapStruct literalmente gera o código dos mappers e o Micronaut, esse é um caso a parte… Micronaut incrivelmente faz a maior parte da mágica com código gerado, incluindo uma parte importante da engine de injeção de dependências e até mesmo serialização e deserialização! — e isso isso pra mim é uma maravilha da engenharia de software, absolute cinema)
Já as anotações processadas em tempo de execução fazem a mágica acontecer utilizando reflections. Reflections nos permite ler informações do código de uma aplicação dentro da própria aplicação, e isso nos capacita a fazer uma serie de coisas, incluindo dynamic proxies, que permite adicionar funcionalidade a métodos já existentes sem modificar o código daquele método, fazendo tudo “por fora”, literalmente como um proxy. O Spring Framework faz uso pesado de reflections para uma serie de funcionalidades, que vão desde o container de injeção de dependências até coisas como as anotações @Async
e @Cacheable
, que são implementadas utilizando dynamic proxies.
E por que escolher entre um ou outro? Resumidamente, performance é um grande fator a se levar em conta. Como o próprio site do Micronaut menciona, fazer toda a mágica acontecer em tempo de execução faz as aplicações Java se tornarem mais lentas de inicializar e consumirem muitos recursos de computação, como memória e processamento. No entanto, é muito mais fácil de se desenvolver funcionalidades baseadas em anotações em tempo de execução, geração de código é por si só um desafio extra. No fim das contas, tudo esbarra naquela famosa frase comum na área do desenvolvimento de software: “Tempo de computação é mais barato do que tempo de engenharia”.
Conclusão
Decorators e anotações normalmente servem aos mesmos propósitos, a diferença é que decorators são de fato funções, enquanto anotações são apenas metadados que dependem de um componente externo para agregar funcionalidade a elas.
Ambos são muito usados para programação orientada a aspectos (AOP), um paradigma criado para solucionar o problema dos problemas transversais, isto é, requisitos funcionais de uma aplicação que se repetem muitas vezes no código da aplicação e não podem ser facilmente encapsulados, como requisitos de segurança, logging, cache, tolerância a falhas, entre outros.
E agora, por uma ultima vez: Se tu leu até aqui, muito obrigado!
Top comments (0)