Introdução ao Clojure Component

renatoalencar

Renato Alencar

Posted on November 27, 2023

Introdução ao Clojure Component

Se você chegou até aqui já deve saber que Clojure é uma linguagem de programação funcional baseada em Lisp que tem ganhado popularidade no Brasil nos últimos anos, principalmente por causa da Nubank. Na minha experiencia pessoal, Clojure tem sido uma linguagem com uma entrada um pouco difícil, algumas coisas são complicadas de inicio, mas você ganha tração bem fácil à medida que você vai tentando se aprofundar.

Algo que eu tenho notado é a falta de material mais profundo sobre Clojure em português, o que torna difícil para brasileiros em geral se aprofundarem no aprendizado da linguagem e na capacidade de gerar discussões em torno da linguagem e dos padrões que a comunidade cria em volta dela. Parte talvez do sucesso que o JavaScript, em especial no Brasil, tenha sido talvez devido a quantidade crescente de usuários e comunidades em português que surgem. Por isso decidi escrever esse artigo, e talvez outros, sobre como tem sido aprender Clojure, como um hobby nas horas vagas. Em especial, nesse artigo sobre a lib Component do Stuart Sierra.

Por que usar Componentes?

Stuart Sierra costumava trabalhar na Cognitect como consultor e desenvolvedor, e analisando os problemas na forma como comumente desenvolvedores estruturavam o estado de suas aplicações, em especial coisas que tem características de singleton, como um pool de conexão com bancos de dados, caching ou partes separadas de uma aplicação, criou um pequeno framework baseado na ideia de injeção de dependência e gerenciamento de estado da orientação a objetos: O Component.

A questão é que temos o hábito de tentar encaixar tudo no modelo MVC, e tentar ver qualquer aplicação como uma extensão disso, tendo um banco de dados, visualização e seja lá o que você considere como lógica/modelo de negocio, sendo que na realidade as coisas estão mais para um spaghetti em formato de integrações com cache, serviços de terceiros, filas de jobs, etc. Então Stuart Sierra, definiu baseado nisso um framework que poderia criar uma abstração generalizada para lidar com esse problema, onde a ideia é:

  • Criar componentes que guardem a responsabilidade de manter uma parte do estado da aplicação;
  • Definir um limite claro entre esses componentes;
  • Definir como lidar com o ciclo de vida desses componentes, cada componente tem que saber como inicializar a si próprio e e como parar;
  • Definir as dependências entre componentes de forma explícita e que respeite os limites entre os componentes e as partes da aplicação.

Mas como que faz isso? Ajuda aí, cara!

Tudo isso começou semana passada, comigo tentando fazer alguns experimentos com Clojure, montar um web service simples que integrasse com banco de dados e me permitisse fazer algumas operações. Você precisar conectar com o banco e manter a mesma conexão (ou um pool) para usos posteriores, afinal você não vai criar uma nova conexão com o banco toda vez que precisar responder a uma requisição. A forma mais simples de fazer isso pode ser usando um atom mesmo:

(ns example-atom
  (:require [ring.adapter.jetty :refer [run-jetty]]
            [monger.core :as mg]
            [monger.collection :as mc]))

(def database (atom nil))

(defn connect-db! [uri]
  (swap! database (fn [db] (-> uri
                               mg/connect-via-uri
                               :db))))

(defn list-users! []
  (mc/find-maps @database :users))

(defn handler [request]
  {:status 200
   :body (clojure.string/join "\n" (list-users!))})

(defn -main
  [& args]
  (connect-db! (System/getenv "DATABASE_URL"))
  (run-jetty handler {:port 3000}))
Enter fullscreen mode Exit fullscreen mode

Simples, né? Parece até fácil demais para ser verdade. O problema é que quando isso cresce muito, a tendência é que vai ser bem complicado saber que tem acesso à essa conexão, devido ao fato de que essa dependência é implícita, além disso o acesso é a uma variável global e mutável. O outro problema comum é a necessidade de se adicionar cada vez mais dependências externas, que podem ser acessadas por um estado global na aplicação. Stuart Sierra explica isso com detalhes e com bons exemplos em Clojure in the Large, onde ele conta a experiencia dele lidando com esse tipo de problema em aplicações de larga escala.

Isolando o banco de dados

Como o problema inicial nesse exemplo é o banco, vamos começar por ele, esse na verdade é o exemplo clássico incluso no próprio README do projeto Component. Aqui, tudo se baseia em três coisas: o conexão com o banco tem um ciclo de vida, eu devo poder acessar ela na aplicação para poder consultar e inserir objetos e eu devo conseguir gerenciar isso como um estado da aplicação. Para começar vamos definir o que seria exatamente um componente banco de dados, o que devemos manter como estado e como gerenciar o ciclo de vida:

  • Um banco de dados pode ser caracterizado aqui como a URL de conexão, a conexão em si e o objeto banco de dados onde a biblioteca faz as consultas;
  • Eu preciso me conectar ao banco quando a aplicação e iniciar e guardar o banco em si e a conexão para ser fechada depois;
  • Eu preciso fechar a conexão quando a aplicação parar ou pelo menos permitir faze-lo.

Em Clojure podemos definir os dados com um Record, que é um tipo de dados que tem a implementação de map e que são utilizados como tipo para guardar dados em formato de chave-valor.

(defrecord Database [uri database connection])

(defn create-database [uri]
  (let [{:keys [db conn]} (mg/connect-via-uri uri)]
    (map->Database {:uri uri
                    :database db
                    :connection conn})))
Enter fullscreen mode Exit fullscreen mode

Agora que sabemos como encapsular nosso banco de dados, precisamos encapsular o próprio ciclo de vida dele. O Component usa o conceito de Protocolos do Clojure para resolver isso, inclusive protocolo é um conceito tao poderoso por si só que daria um artigo inteiro. O protocolo em questão é o Lifecycle. Usando um protocolo em comum, o framework sabe como iniciar e como parar qualquer componente, assim você só tem que se preocupar com a implementação. Além de definir o componente em si, é interessante definir uma função (pura) que crie o componente, um constructor.

;;
;; Todo o ciclo de vida do banco de dados
;; fica encapsulado em um componente.
;;
(defrecord Database [uri database connection]
  component/Lifecycle

  (start [component]
    (let [{:keys [conn db]} (mg/connect-via-uri uri)]
      (assoc component :database db
                       ::connection conn)))

  (stop [component]
    (-> component ::connection mg/disconnect)
    component))

;;
;; O construtor tem que ser uma funcao pura
;;
(defn new-database [uri]
  (map->Database {:uri uri}))
Enter fullscreen mode Exit fullscreen mode

Mas e o web server?

Bem, nesse momento, você talvez já teve algum tipo de epifania e pensado: "Mas o servidor web também se encaixa nesse padrão". Sim, e vamos encapsular ele também:

;;
;; O web server pode seguir o mesmo protocolo
;; e principios que o banco de dados.
;;
(defrecord WebServer [port app]
  component/Lifecycle

  (start [component]
    (assoc component
      ::jetty
      (run-jetty (-> component :app :handler)
                 {:port port})))

  (stop [component]
    (-> component ::jetty .close)
    component))

(defn new-web-server [port]
  (component/using
    (map->WebServer {:port port})
    ;; Declaramos explicitamente a dependencia
    ;; de um componente `:app`.
    [:app]))
Enter fullscreen mode Exit fullscreen mode

Aqui nós adicionamos algo novo: a função component/using. Ela é responsável poder deixar explicito que um componente depende de outro, nesse caso o app, que possui uma chave handler. Nesse caso o handler deve ser uma função que é responsável por responder a requisições.

Como que junta isso tudo?

Agora vamos precisar de um componente App, que deve depender do banco para fazer as consultas necessárias, usando esse componente vamos passar o banco de dados pelo map da requisição para ser usado internamente pelo handler.

;;
;; Associa um valor `value` a um chave `key` no
;; primeiro argumento da funcao `f`.
;; 
;; Espera-se que `f` seja um handler que possa
;; ser usado com o Ring.
;;
(defn wrap-assoc [f key value]
  (fn [request] (f (assoc request key value))))

(defrecord App [database handler]
  component/Lifecycle

  (start [component]
    (let [database (-> component :database :database)]
      (assoc component :handler
                       (wrap-assoc handler :database database)))))

(defn new-app [handler]
  (component/using
    (map->App {:handler handler})
    [:database]))

(defn list-users-handler [request]
  {:status 200
   :body (clojure.string/join "\n"
           (-> request
               :database ;; Aqui nós acessamos a chave `:database`
                         ;; que foi associada pelo componente `App`.
               (mc/find-maps :users)))})
Enter fullscreen mode Exit fullscreen mode

Como eu inicio a aplicação agora?

Com tudo definido, só precisamos juntar tudo em um map, usando a função component/system-map. Aqui você pode criar uma função pura que é responsável por criar o sistema em si. E então chamar component/start nesse map, dentro da função -main.

;;
;; Apesar de podermos fazer isso diretamente no `-main`, aqui
;; podemos separar a responsabilidade de interpretar as
;; opcoes e passar para os respectivos constructores dos
;; componentes.
;;
(defn system [& options]
  (let [{:keys [database-url port]} options]
    (component/system-map
     :database (new-database database-url)
     :app      (new-app list-users-handler)
     :server   (new-web-server port))))

(defn -main
  [& args]
  (component/start (system :database-url (System/getenv "DATABASE_URL")
                           :port         3000)))
Enter fullscreen mode Exit fullscreen mode

Considerações finais

Você pode estar se perguntando por que eu não usei Compojure ou por que eu usei MongoDB como exemplo. Bem, a intenção desse exemplo é de ser focado no Component, e ao mesmo tempo como ele pode ser usado em conjunto com outras ferramentas. Eu tentei evitar ao máximo adicionar a necessidade de conhecimento sobre outras ferramentas para facilitar o entendimento do exemplo.

O código com as configurações todas no ponto de rodar tá no GitHub em https://github.com/renatoalencar/component-example.

Referências

💖 💪 🙅 🚩
renatoalencar
Renato Alencar

Posted on November 27, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Introdução ao Clojure Component
clojure Introdução ao Clojure Component

November 27, 2023