Um dos meus alunos do curso Clojure: Introdução à Programação Funcional fez uma pergunta interessante:
Olá professor, gostaria de informações sobre o deploy de uma aplicação, com um frontend enviando solicitações e obtendo repostas (como no caso do carrinho de compras, por exemplo). Estou gostando do curso, porém acho que uma construção mínima com um front-end ou implementado um service nos aproximaria mais do uso "no mundo real".
Embora a pergunta fuja bastante do escopo do curso (cujo foco são os conceitos do paradigma funcional - a linguagem em si é um detalhe), é uma dúvida bastante compreensível. É natural querer saber como aplicar os conhecimentos na prática.
Respondi o aluno, mas resolvi criar este artigo para poder explicar de forma mais completa. Não irei mostrar o passo a passo de como criar um serviço REST em Clojure, mas vou dar alguns caminhos possíveis, visando facilitar a trajetória daquelas pessoas interessadas neste tema.
Ao longo do artigo mostro como foi a minha linha de raciocínio para implementar um backend REST em Clojure que comecei a desenvolver ano passado e que continuo sustentando e adicionando funcionalidades.
Single Page Application ou Server Side?
Uma das decisões que precisei tomar logo no início foi se iria renderizar o HTML no backend e utilizar um framework Clojure fullstack ou se iria implementar um SPA, utilizando o Clojure apenas no backend (através de serviços REST).
Clojure permite as duas abordagens. E ao escolher desenvolver um SPA, você pode até mesmo utilizar Clojure para implementar também o frontend, utilizando ClojureScript!
Mas optei por utilizar Clojure apenas no backend e desenvolvi o frontend utilizando minha linguagem de programação favorita: Elm.
Queria utilizar apenas linguagens funcionais em toda a stack e como (pelo menos por enquanto) Elm é uma linguagem focada no desenvolvimento de frontends, precisava de outra linguagem para o backend. Como já queria explorar o uso do framework Elm Land e a biblioteca Elm UI, optei por criar um SPA em Elm e utilizar Clojure apenas no backend.
Mas é possível implementar toda a solução em Clojure, caso você ache interessante.
Frameworks ou bibliotecas?
Em muitas linguagens é comum existirem frameworks web que agregam diversas funcionalidades. Quem já programou em Java provavelmente já estudou sobre o Spring ou Quarkus. Já em Ruby temos o famoso Ruby on Rails, no PHP existem soluções como Laravel e Symfony (ou ainda o famigerado Wordpress), entre tantos outros possíveis exemplos. Mas existem também muitas bibliotecas.
É difícil segmentar e definir o que faz com que uma tecnologia seja considerada um frameworks ou uma biblioteca, e não vou tentar fazer isso aqui. Para este artigo, basta considerar que enquanto um framework tenta resolver diversos tipos diferentes de problemas, uma biblioteca costuma ter um escopo bem mais reduzido. Desta forma, em geral, é necessário compor um conjunto de bibliotecas para solucionar um problema maior.
Como exemplos de bibliotecas para o universo web, no Ruby poderia citar o Sinatra, para Java/Kotlin o Spark, entre muitos outros.
E como você deve imaginar, existem também frameworks e bibliotecas para desenvolvimento web em Clojure. E um framework famoso é o Luminus.
Luminus
O Luminus se classifica como um micro-framework Clojure, baseado em um conjunto de bibliotecas leves. Esta é uma característica relativamente comum entre os frameworks web: juntar diversas pequenas bibliotecas de forma coesa e bem integrada que, juntas, possuem tudo que normalmente é necessário para implementar uma solução web completa.
Vale muito a pena conhecer este projeto e ver o que é possível fazer em Clojure. Com ele, você poderá criar facilmente um projeto muito bem organizado e com todas as funcionalidades esperadas atualmente, incluindo um sistema de migrations do schema do banco de dados, testes automatizados, configurações separadas por ambientes, algumas facilidades para interagir com banco de dados, etc.
Mas, embora muito bacana, me parece que esta não é uma abordagem tão popular dentro da comunidade Clojure. O que tenho observado é que, em geral, as pessoas desenvolvedoras Clojure costumam preferir escolher algumas bibliotecas e montar o seu próprio framework, de acordo com suas necessidades.
E este também foi o caminho que escolhi seguir: criar o meu próprio framework customizado para as necessidades do meu projeto. Em parte, segui este caminho pois o que precisava construir não era algo tão complexo assim. Aproveitei então para me aventurar nas diversas bibliotecas do mundo Clojure!
Selecionando as bibliotecas
Automação e gestão de dependências
Escolhi o Leiningen para gerenciar as dependências do projeto e automatizar tarefas como executar testes automatizados, gerar a build (Uberjar) do projeto, executar o terminal do REPL (Read-Eval-Print Loop), etc.
Se você vem do mundo Java, pense no Leiningen como sendo uma alternativa ao Apache Maven ou Gradle.
Existem outras ferramentas similares, mas o Leiningen parece ser a mais utilizada no universo Clojure e atendeu bem minhas necessidades.
Servidor HTTP
Para desenvolver uma aplicação web é necessário, claro, ter um servidor para receber as requisições. Para isso, escolhi a biblioteca Ring.
Para quem conhece Python, pode pensar que Ring seria algo equivalente ao WSGI (Web Server Gateway Interface) e o pessoal acostumado com Ruby pode associá-la ao Rack.
Ring é uma biblioteca simples, mas de baixo nível de abstração. Por isso pode ser um pouco complicada de configurar. Optei então por utilizar a biblioteca Ring-Defaults, que tem como objetivo prover uma configuração inicial segura e que atenda a maioria dos casos. Com ela foi muito fácil configurar o Ring e criar meus primeiros serviços REST.
Gerenciador de rotas
Ring é tão enchuta que não conta nem mesmo com um sistema de gerenciamento de rotas! Então precisei acrescentar uma biblioteca para esta finalidade.
Para isso, escolhi utilizar a Compojure. Trata-se de uma biblioteca bastante simples, que acrescenta algumas facilidades ao Ring relacionadas à criação de rotas.
Criar um endpoint /ola-mundo
utilizando a Compojure é bastante fácil:
(defroutes app
(GET "/ola-mundo" [] "<h1>Olá Mundo</h1>"))
Mas claro que para serviços mais complexos você pode (e deve) delegar o processamento das requisições para outras funções.
Conectando ao banco de dados (PostgreSQL)
Com as bibliotecas listadas acima, já é possível criar um servidor simples e retornar as requisições. Mas a maioria dos projetos vai precisar mais do que isso.
Uma necessidade comum é se comunicar com um banco de dados. No meu caso, precisava acessar um banco PostgreSQL.
Optei por utilizar a biblioteca clojure.java.jdbc. Mas note que, embora esta biblioteca ainda seja mantida (o autor continue aplicacando patchs de correções), ela está descrita no site como sendo "Estável" (não mais "Ativa"). O site ainda recomenda que em seu lugar seja utilizada a next-jdbc.
Portando, em um projeto novo, eu daria prioridade à esta outra biblioteca (e está em meus planos migrar o projeto para ela no futuro).
PostgreSQL
A biblioteca clojure.java.jdbc é agnostica e pode ser usada em qualquer banco de dados com suporte ao JDBC (Java Database Connectivity) e para conseguir me conectar ao banco PostgreSQL, também precisei utilizar a org.postgresql/postgresql.
Pool de conexões
As duas bibliotecas que citei acima são suficientes para conectar e executar queries no banco mas, por questões de performance, é importante utilizar um pool de conexões. Para isso utilizei e recomendo a hikari-cp. Já utilizava ela quando programava em Java e sempre me atendeu muito bem.
Criando as queries
Pessoas que estão acostumadas com Java, C# e similares, devem estar habituadas a utilizar ferramentas de ORM (Object Relational Mapper) como Hibernate ou Entity Framework. Mas dentro da comunidade Clojure, não vejo este tipo de ferramenta sendo muito utilizada. No contexto de Clojure, é mais natural que uma chamada ao banco retorne uma estrutura da dados, como um mapa.
Optei por escrever o código SQL manualmente em algumas poucas queries mais complexas, mas para a maioria dos casos utilizei a biblioteca Honey SQL. Com ela é possível construir as queries como se fossem estruturas de dados Clojure.
Um exemplo seria:
(defn query-select-auditoria-xpto-por-cpf
[cpf]
(-> (h/select :created_at
:remote_address
[[:raw "request_headers->'x-alias'"] :alias])
(h/from :auditoria)
(h/where [:= :cpf cpf]
[:= :path "/api/xpto/"]
[:= :response_status 200]
[:= :method "GET"])
(h/order-by [:id :desc])
sql/format))
Note que esta função não executa a query, apenas gera o comando SQL, sendo portanto uma função pura. Ao executá-la, o seu resultado será:
(query-select-auditoria-xpto-por-cpf "123.345.789-01")
;; Resultado:
["SELECT created_at, remote_address, request_headers->'x-alias' AS alias FROM auditoria WHERE (cpf = ?) AND (path = ?) AND (response_status = ?) AND (method = ?) ORDER BY id DESC" "123.345.789-01" "/api/xpto/" 200 "GET"]
Para processá-la de fato, é necessário chamar a função jdbc/with-db-connection
da biblioteca clojure.java.jdbc, passando a query que deseja executar como parâmetro. Antes disso, claro, precisa configurar usuário, senha e endereço do banco, além do pool de conexões. Mas isso já foge do escopo deste artigo.
JSON
O padrão JSON é utilizado com muita frequência em projetos web. Para facilitar o parsing (e geração) deste formato, utilizo a biblioteca data.json.
Seu uso é bastante simples:
(json/write-str {:a 1 :b 2})
;; Resultado:
"{\"a\":1,\"b\":2}"
(json/read-str "{\"a\":1,\"b\":2}")
;; Resultado:
{"a" 1, "b" 2}
Executando chamadas HTTP
Além de receber requisições HTTP, eu precisava me integrar com outros sistemas através de serviços REST (outra necessidade bastante comum em aplicações web distribuídas).
Para isso utilizei a biblioteca clj-http e seu uso é bastante simples. Para fazer uma chamada GET, basta executar algo como:
(client/get "https://exemplo.com/recurso/1234" {:accept :json})
Caching
O sistema que desenvolvi executa algumas queries pesadas no banco, para geração de relatórios. Mas embora o processamento seja oneroso, o resultado final não é um volume grande de dados e não precisa estar totalmente atualizado o tempo todo. Por isso, algumas destas operações podem ser cacheadas em memória.
Para isso, utilizei a biblioteca clojure.core.cache.
Embora esta biblioteca funcione bem, achei a sua interface um pouco confusa. Para contornar isso criei a minha própria abstração. Depois de alguns ajustes, consegui criar algumas funções que, embora escondam algumas funções mais avançadas da biblioteca, tornam a utilização do cache bastante simples.
Chamadas assíncronas
Em um determinado momento precisei fazer uma integração com o chat do Microsoft Teams (através de uma chamada HTTP). Como já mencionei acima, utilizei a clj-http para isso. Mas queria que esta parte do sistema fosse assíncrona (não bloqueante). Para isso utilizei a biblioteca core.async.
Para quem conhece a linguagem Go, esta biblioteca funciona de forma parecida com as famosas Goroutines. Ou seja, é uma lightweight thread.
Tornar uma função bloqueante em não-bloqueante é muito fácil: basta passar as expressões/funções que deseja executar em paralelo para a função async/go
. No caso de uma chamada HTTP, seria algo como:
(async/go
(try
(client/post url-webhook-teams
{:body (json/write-str {"text" "Uma mensagem para o MS Teams"})
:headers {"content-type" "application/json"}
:socket-timeout 3000
:connection-timeout 3000})
(catch Exception e
(println (str "Falha ao tentar enviar mensagem para o Teams: " e)))
Agendamento de execuções (scheduler)
Outra necessidade comum de uma aplicação web, e que também precisei fazer, é o agendamento de execução de tarefas que devem ser executadas de forma periódicas (no meu caso, executar uma função a cada 10 minutos). Para esta finalidade, utilizei a biblioteca Chime.
Embora seja relativamente simples, assim como ocorreu com a biblioteca de cache, inicialmente tive um pouco de dificuldade para entender como utilizá-la. Não é nada muito complexo, mas achei a documentação um pouco confusa. Novamente consegui isolar esta parte do código em uma função mais específica e deu tudo certo. Não tive mais problemas com ela.
Mocks e testes automatizados
Além do código de produção, queria criar testes automatizados.
Nos primeiros serviços optei por implementar, além dos testes unitários mais básicos, vários testes integrados. Para isso utilizei a biblioteca Ring-Mock para mockar o servidor web. Também criei um dublê da função que fazia acesso ao banco de dados.
Com o tempo comecei a ter dificuldade para manter estes testes, que quebravam de forma não muito trivial. Outro grande problema era que, ao tentar praticar TDD, senti falta de um type system para criar os asserts.
Às vezes criava todo o cenário de ponta a ponta, mas em runtime, quando estava executando a aplicação e me conectando no banco real, o tipo retornado era diferente do que eu estava imaginando. E mais uma vez encontrar exatamente onde estava o problema era um pouco chato às vezes. Nem sempre a mensagem de erro era realmente significativa.
Com o tempo optei por minimizar (pelo menos por enquanto) este tipo de teste. Tento maximizar a quantidade de funções puras e foco meus testes nelas. Sinto falta de testar algumas camadas da aplicação, sendo algo que ainda preciso experimentar e explorar mais.
Separando as funções puras
Não quero entrar muito em detalhes da arquitetura do sistema neste artigo, mas ela está bastante simples. Cada caso de uso (serviço) tem a sua própria pasta, e as funções puras e impuras estão em namespaces distintos. Me esforço para tentar transformar funções impuras em puras e tornar aquelas impuras em apenas orquestradoras.
As funções que causam algum tipo de efeito colateral devem ser o mais "burras" possível. Exemplo: uma função que executa uma query no banco não pode ter nenhuma regra de negócio. O mesmo para uma função que executa uma chamada HTTP, etc.
Já considerava isso uma boa prática quando programava em outras linguagens, mas em Clojure (por ser funcional), torna-se muito fácil e natural fazer isso.
Com o tempo, acabei diminuindo e até eliminando alguns testes integrados e aumentando a quantidade de testes unitários. Para o meu contexto, isso funcionou bem, mas às vezes ainda sinto falta de testes mais abrangentes.
Injeção de dependências
Pessoas acostumadas com Java, .NET e similares talvez queiram buscar alguma biblioteca de injeção de dependência para poder praticar inversão de controle.
Para atingir este objetivo em Clojure, tudo que precisei até agora foi passar referências de funções como parâmetros. Se uma função precisa, por exemplo, executar uma query no banco de dados, eu passo como parâmetro a referência para a função que, ao receber um comando SQL, o executa e retorna o resultado. Faço o mesmo para chamadas HTTP ou outras formas de efeitos colaterais.
No contexto da minha aplicação, isso foi suficiente. Caso precise criar um dublê (mock/spy/stub), basta passar a referência de outra função (usada apenas para testes) como parâmetro. Assim consigo evitar o acoplamento com camadas externas (como banco de dados ou outros sistemas) nos testes, permitindo que tenha total controle sobre o que será retornado nessas chamadas, permitindo criar asserts facilmente, que não irão quebrar com o tempo. Isso permite também que meus testes possam ser executados de forma concorrente, além de serem muito rápidos.
Onde hospedar
Uma das grandes vantagens da linguagem Clojure - e um dos principais motivos de tê-la escolhido - é o fato de ser uma linguagem que roda na JVM (Java Virtual Machine). O resultado final da aplicação é um Uberjar (um arquivo jar com sufixo -standalone.jar) contendo toda a aplicação e dependências do projeto.
E o servidor criado da forma como descrevi neste artigo pode ser iniciado no ambiente produtivo com a seguinte linha de comando:
java -jar target/nome-projeto-0.0.1-SNAPSHOT-standalone.jar
No meu caso, estou utilizando uma nuvem privada on-premise. E um ponto interessante é que esta nuvem já tinha suporte à plataforma Java e eu já havia desenvolvido muitas soluções nesta linguagem. Migrar para o Clojure foi trivial, não precisando fazer nenhuma alteração na infraestrutura. Para o time do centro de dados, é totalmente transparente.
Ou seja, aplicações desenvolvidas da forma como descrevi neste artigo podem ser hospedadas em qualquer servidor que tenha suporte ao Java! 🎉
Bônus: atualizando as dependências
Uma preocupação que sempre tenho em todo projeto é evitar o seu envelhecimento. É um processo natural de toda aplicação, que precisa de atenção constânte do time. E pelo menos para alguns destes tipos de problemas, o processo pode ser automatizado!
O Leiningen possui um comando que verifica se existem dependências desatualizadas e, caso encontre alguma, é capaz de atualizá-la e em seguida executar os testes automatizados. Caso algum teste falhe, a alteração é desfeita e uma mensagem de erro é exibida indicando o problema.
Esta funcionalidade me ajuda diariamente! Criei um script com o comando e normalmente começo meu dia executando-o para garantir que o projeto está com as dependências sempre atualizadas.
O comando em questão é:
lein ancient upgrade :interactive :check-clojure
Mas para poder executá-lo é necessário configurar o plugin lein-ancient.
Meu project.clj atual
A configuração de um projeto que utilize o Leiningen é toda feita no arquivo project.clj
.
Abaixo compartilho como está a configuração do meu projeto no momento em que estou escrevendo este texto. Além das bibliotecas que aprensentei aqui, utilizo algumas outras configurações que não achei relevantes o suficiente para destacar, mas que podem ser úteis para algumas pessoas.
(defproject nome-projeto "1.0.0-SNAPSHOT"
:description "Aqui ficaria a descrição do projeto."
:url "https://endereço-repositorio-git"
:min-lein-version "2.10.0"
:dependencies [[org.clojure/clojure "1.11.1"]
[compojure "1.7.0"]
[ring/ring-defaults "0.3.4"]
[com.github.seancorfield/honeysql "2.4.1045"]
[org.clojure/java.jdbc "0.7.12"]
[org.postgresql/postgresql "42.6.0"]
[hikari-cp "3.0.1"]
[org.clojure/data.json "2.4.0"]
[org.slf4j/slf4j-simple "2.0.7"]
[clj-http "3.12.3"]
[org.clojure/core.cache "1.0.225"]
[jarohen/chime "0.3.3"]
[org.clojure/core.async "1.6.673"]]
:plugins [[lein-ring "0.12.6"]
[lein-ancient "0.7.0"]
[lein-cloverage "1.2.2"]]
:ring {:handler um.namespace.qualquer.routes/app}
:repl-options {:init-ns um.namespace.qualquer.logic}
:profiles
{:dev {:dependencies [[javax.servlet/servlet-api "2.5"]
[ring/ring-mock "0.4.0"]]}})
Conclusões
Clojure, em conjunto com as bibliotecas que mostrei ao longo deste artigo, me atenderam muito bem. Consegui criar uma arquitetura que é ao mesmo tempo simples e fácil de incluir novos serviços. O servidor consome poucos recursos e tem uma performance muito boa.
A escolha por criar o meu próprio framework, selecionando cada biblioteca isoladamente, inicialmente consumiu certo tempo. Tive que testar cada uma delas, descartar as que não atendiam minhas necessidades e construir alguns códigos para "colar" essas bibliotecas.
Usar um framework pronto provavelmente iria aumentar a minha produtividade nas primeiras iterações, mas como é um projeto que espero manter por muitos anos, ter total controle sobre cada parte dele provavelmente é o cenário ideal.
A comunidade Clojure parece ter uma cultura de criar bibliotecas pequenas, com propósitos muito bem definidos. Acredito que esta característica, em conjunto com o fato de Clojure ser uma linguagem funcional, facilitou muito este processo. Essas duas características somadas permitiram que, apenas utilizando composição de funções, eu fosse capaz de criar grandes abstrações, sem adicionar muita complexidade.
Mas é importante deixar claro que este é um projeto pequeno, onde não precisei utilizar nenhuma arquitetura muito avançada. Não abordei nada como Domain-driven design, Arquitetura Hexagonal, Microsserviços, ... Então não consigo afirmar que Clojure (ou mesmo o paradigma funcional) seria uma boa opção para cenários mais complexos. Mas, até o momento, não estou arrependido da escolha e, se depender de mim, não volto a programar em uma linguagem orientada a objetos novamente tão cedo! 😇
E você? Quais tecnologias têm utilizado para desenvolvimento web? Conte um pouco das suas experiências mais recentes nos comentários!
Gostou deste texto? Conheça meus outros artigos, podcasts e vídeos acessando: https://segunda.tech.
Top comments (2)
Boa, otimo guia, vou usar como desculpa pra portar um projetin quarkus pra aprender clojure de vez :)
Fenomenal. Desde a clareza, os vários links e até mesmo o curso (que comecei a fazer!). Que post maravilhoso. Parabéns, adorei