Hanami em prod: 2 anos depois – Parte 1

Começamos os protótipos quando o projeto se chamava Lotus-rb. Lá em Outubro de 2015, a ideia era validar se quem-sabe-talvez, Lotus-rb pudesse ser uma alternativa real ao então nosso principal projeto em Ruby on Rails.

Le Prototype

Como todo mundo, fizemos um ToDo app para validar. Experimentamos o model e o validation, pois já tinhamos em mente separar o Input e sua validação do domain-model. Queríamos isolar ao máximo nosso modelo, muito nos moldes do Clean Architecture do Uncle Bob / Ports & Adapters do Alistair Cockburn. A ideia era muito experimental e confiávamos na robustez por baixo do Lotus-rb: Sequel. O Sequel, diferente do Lotus-rb, era estável e bem conhecido no ecosistema Ruby. Pensavamos: se o Lotus cair, ficamos com uma camada de abstração do Sequel e migramos isso depois facilmente para um Sequel-model da vida.

Meses depois, nosso projeto Core foi ao ar, substituindo partes do nosso agora legado Rails app. Já com o nome de Hanami e algumas atualizações feitas, tudo ia bem, para nossas necessidades da época. O projeto é uma API com interface JSON API, com foco em separação em camadas, Domain-Driven Design e arquitetura Hexagonal.

Le Presentation Layer

Ao longo deste processo, testamos outras gems como Roar (do Apotonick), Representable, JSONApi-Serializers e Grape. Como dá para ver, nossa Presentation Layer demorou a ficar do jeito que imaginávamos. É a vida. Ela é assim e boas. Fizemos uma versão básica da Presentation Layer e depois de tudo pronto e algumas utilizações, repensamos como poderíamos deixa-la de facto isolada.

Nossa principal questão foi como lidar com a transformação do Domain-Model para entregar algo “burro” ao nosso Presentation Layer. Outro ponto era o ponto inverso: como transformar o payload do Request em um objeto de transferência para ser encaminhado a Application Layer – a.k.a Use Cases. Idas e vindas, começamos com um modesto fluxo endpoint -> form object -> application service -> domain model até chegarmos em algo com responsabilidades melhor definidas:

# Presentation Layer
endpoint
  form object
    optional triggers: Validation & Relationships (JSON API specifics)

# Application Layer
  use case
    orchestrates to:
      domain model or # domain model
      infrastructure service # application boundaries

Antes de ir para production, tivemos que refazer nossa ideia de Form Object, pois o modelo inicial, não continha uma forma plugável para adicionar Input Validation (Hanami Validation implementation). Era muito manual e repetitivo. Natural neste ponto do processo, pois tivemos que distribuir nosso foco de atenção em toda a arquitetura inicial.

Com o novo modelo, o FormObject delega para a validação usando Template Method, caso o tal FormObject dê include na Validation que deseja utilizar naquele endpoint. Isso deixou muito transparte como validamos as coisas, pois no final, tinhamos uma delegação muito boa entre as classes FormObject e Validation.

Como spoiler, se liga um exemplo de FormObject:

# apps/web/form_object/auto/financing/applications/patch.rb

require_relative 'validations'
require_relative 'relationships'

module FormObject
  module Auto
    module Financing
      module Applications
        class Patch
          include Validations
          include Relationships

          def initialize(params, current_user, overrides = {})
            @raw_params = params
            @current_user = current_user
            @update_application = overrides.fetch(:application_use_case) do
              ::UseCase::Auto::Financing::Application::Update.new
            end
            @repository = overrides.fetch(:applications_repository) do
              ::Auto::Financing::ApplicationsRepository
            end
          end

          def process
            validate_using(@raw_params)
          end

          private

          def validation_succeed(validated_params)
            @update_application.update_application_form_for(
              @current_user,
              entity_with_relationships_updated,
              validated_params
            )
          end

          def entity_with_relationships_updated
            update_relationships(find_application(@raw_params['id']))
          end

          def find_application(id)
            @repository.find(id)
          end
        end
      end
    end
  end
end

# apps/web/form_object/auto/financing/applications/validations.rb
# Classe de Validation citada acima

require 'form_object/definitions/validatable'
require 'validations/auto/financing/application_validator'

module FormObject
  module Auto
    module Financing
      module Applications
        module Validations
          include Definitions::Validatable

          def validator_class(params)
            ::Validations::Auto::Financing::ApplicationValidator.new(params)
          end
        end
      end
    end
  end
end

# apps/web/validations/auto/financing/application_validator.rb
# Hanami Validation class

module Validations
  module Auto
    module Financing
      class ApplicationValidator
        include Hanami::Validations

        attribute :requester_full_name, type: String, size: 0..200
        attribute :requester_cpf, type: CoerceTypes::Cpf, size: 11
        attribute :status_flow, type: String
        attribute :purchase_value, type: CoerceTypes::Nullable::Money
        attribute :down_payment, type: CoerceTypes::Nullable::Money
        attribute :loan_amount, type: CoerceTypes::Nullable::Money
      end
    end
  end
end

Le Application Layer

Nosso próximo desafio foi mudar nossos Application Services. Trocamos o nome de Application Service para Use Case. Isso por si, já deixou objetivo o que era esperado destas classes bem como da Application Layer inteira.

Use Case bem descrito passou a ser prioridade em meados de maio/2016, quando percebemos que a Application Layer precisava sair de um mero detalhe, para assumir a responsabilidade de orquestrar como nossos casos de uso eram controlados. Nosso objetivo era simples: conseguir com um tree boundaries/use_cases entender o que o software é capaz de fazer. Todo aquele papo de sempre sobre nomenclatura de classes, etc., mas ir além e pensar nos contextos (modules/namespaces) que os Use Cases fazem parte.

Como todo o projeto, nossos Use Cases são stateless e recebem suas dependências através de injeção de dependência no initialize. Podem controlar db transactions e em alguns casos, disparar Domain Events, como sugerido pelo Vaugh Verner em seu livro Implementing Domain-Driven Design.

No próximo post, vou descrever como foi lidar com a evolução de nosso domain-model e como conseguimos lidar com isso ao longo destes dois anos.

Arquitetura, Rails e o ecossistema Ruby

Rails acredito que seja a primeira palavra a vir à cabeça quando se fala em Ruby. A coisa é tão intensa, que não é difícil encontrar vagas e programadores entitulando-se: Rails Developer. Nem Engenheiro de Software; Nem (Back|Front)end Developer, tão pouco Ruby Developer.

Que o Rails é o framework mais conhecido dentro (e até fora) do mundo Ruby isso é um fato. Fato também é o quão simplificado o desenvolvimento utilizando ele é; Mas, já parou para pensar no trade-off existente aí?

Architecture: The Lost Years

Keynote apresentado pelo Unclebob lá no distante 2011, sobre o quão ofuscados ficamos com o Rails – e o que isso trouxe como consequência. Não é um ataque direto ao Rails, mas sim um: hey, vamos acordar pra vida e utilizar o Rails de uma forma um pouco mais decente ?

Se você nunca viu esta palestra. Pare de ler agora e veja até o final. Depois, continue lendo (pois vou assumir que você viu ao keynote)

Obviamente que ele não é o único a pensar assim. A apresentação dele gerou diversas threads na Internet sobre como fazer um aplicativo Rails desacoplado e mais sob o controle do desenvolvedor do que do framework.

Acredito que o desejo de muitos seria ter uma especie de Symfony Framework + Doctrine 2.x para Ruby. Desacoplamento. O desenvolvedor escolher as peças; ou como o Unclebob disse na palestra: acessar o diretório do projeto e pensar: “Ah, isso é um software de X; ao invés de: Aaah, isso é um app Rails.”

Moldar o Rails para algo mais Domain-Driven Design

Resolvi apostar. Aposta simples, silenciosa. Aproveitei a (maior) modularidade do Rails 4 para começar a extrair algumas coisas e definir uma estrutura nova de diretórios. Preferir transparência no domínio às convenções do Rails. Movi tudo para o /domain/(modulo)/.

  • O Rails convenciona que Models devem estar dentro de app/models, caso contrário, o ActiveModel não funciona corretamente as relações de ORM.

Ao topar com este empecilho, não quis me alongar nisso e preferi manter todos os “Models” dentro do diretório que se é convencionado. Já viu a sopa de diretórios que isso ficou, né ? /domain; /app/models.

  • O ActiveRecord possui features intrigantes, como por exemplo os scopes, porém os Contras são maiores do que os Prós.

O Avdi Grimm no livro Objects on Rails mostrou passo a passo como ele construiu um software Rails-based postergando relacionar suas entities ao ActiveRecord. No final, ele preciso modificar bastante coisa para tê-las in place. Alguns testes precisarão ser de integração (scopes, olho para vocês) – pois teste de Unidade Comportamental (Unit-Testing Behavioral) não garantirá que o scope está correto mesmo.

Pode parecer xiitismo, mas o ActiveRecord é pesado. E esse peso aparece ao rodar os testes de unidade. Mesmo utilizando Test Double, só de precisar subir toda aquele estrutura do ActiveRecord, o processamento já fica mais lento do que se as entidades fossem livres do meio de persistência.

  • Convention Over Configuration em um framework Arquitetural (One Size Fits All) é nocivo para adotar meios e métodos alternativos.

Seguir o caminho sem o Rails

Optar por deixar o Rails de lado, utilizando somente o que você precisa e quando precisa é uma das alternativas dos Rubistas (outside Brasil) atualmente. Isso explica a popularidade que o Sinatra ganhou nos últimos tempos. Sinatra pois ele fornece uma interface simples entre o Rack e sua aplicação web. Sinatra, pois ele é somente isso, deixando todas as demais decisões para você. Com isto em mente, vale lembrar de que precisará criar suas próprias coisas.

Algumas muito simples outras nem tanto:

  • Criar e configurar seu config.ru para que o Rack o leia e suba um stack;
  • Configurar seu spec_helper.rb ou test_helper.rb para Unit Testing com RSpec ou MiniTest;
  • Criar seu Environment Manager, para conseguir distinguir Development, Testing e Production modes – Dica: ENV['RACK_ENV'] pode ser usado pra isto;
  • Configurar seu Rakefile para manipular Rake Tasks;
  • Definir um Autoloader para ler sua estrutura de diretórios. /domain e /controllers, como é o meu, por exemplo.
  • Migration, Validation, ORM, etc.

Particularmente quando fiz isso pela primeira vez me senti perdido. Não é cuspindo no prato não, mas o Rails o faz criar manias e uma certa dependência nele.

Importante resaltar que você não precisará das coisas da mesma forma que o Rails criou. A vantagem é que você cria as coisas on demand, voltadas às suas necessidades. Por exemplo, meu Environment Manager é muito mais simples do que o do Rails, entretanto, consigo com ele diferenciar os Envs e subir coisas diferentes.

Outra vantagem é que construo um stack muito simples, rápido e customizado. Precisei apenas de 2 dias para ter um sandbox com Sinatra funcionando e meus testes de unidade com RSpec ficam na casa dos 0.00xx sec.

O tempo “gasto” estudando como fazer certas coisas vale muito a pena, pois você entende melhor como funciona a arquitetura por debaixo do Rack. Você no controle!

Desta forma, até agora não poluí meu código com ORM, Validations, etc. Quando realmente preciso de alguma coisa, vou lá e pontualmente a configuro/instalo.

Próximos steps: Virtus gem para Entity Modeling; ActiveModel::Validations ou o ActiveValidators; Sequel ou Rom-rb; Asset Pipeline – Sim, nada me impede de utilizá-lo standalone numa arquitetura onde eu consigo ter o controle 😉

Aos poucos pretendo ir comentando minhas aventuras nessa área. O resultado tem sido bem satisfatório até o momento. Recomendo tentar você também!

Concluindo

Não me entenda errado: o Rails possui seus pontos positivos. Particularmente, gosto do Asset Pipeline e dos Validators (externos à classe, não aqueles validates :field). O propósito aqui é fazer o que muitos developers lá fora já fazem: abrir o olho da comunidade de que há um mundo imenso fora do Rails e que você precisa pensar: eu preciso do Rails ou estou apenas com medo/preguiça de seguir outra solução?

1 – Foto da arquitetura da Ponte de Londres.