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.

Refatoração de código

post-10513-Code-Refactoring-Cat-in-Bathtu-yRZT

Não precisa acompanhar o “mundo dos testes” para saber o que significa refatorar. Aliás, quem nunca ouviu algum co-worker ou você mesmo tenha feito uma refatoração de um código.

Primeiro, vamos deixar bem claro o que é refatoração. Vamos lá. Você tem até o próximo paragrafo para pensar na sua definição de refatoração de código.

Pronto? Aqui vamos nós!

A milagrosa refatoração de código

Primeiramente é importante explanar de que há dois tipos de refatoração: a) refatoração, terceira etapa do Test-Driven Development (Red, Green, Refactor); b) refatoração de um código de produção já existente. Neste momento vou tratar apenas do item b).

A necessidade de uma refatoração dá-se inicialmente por uma decisão de um (ou mais) programadores sob um determinado trecho de código, por concluir de que aquilo não está construído de uma maneira aceitável. Disto, o cabra deve lembrar de que refatorar quer dizer: não mudar o comportamento da parte a ser refatorada. Apesar de óbvio, essa primeira premissa é a mais violada ao refatorar algo, pois junto da refatoração o programador resolve fazer umas coisinhas a mais (inserir novas features, por exemplo).

Não precisa ir muito longe para provar de que isso não funciona bem, não é mesmo? Ao refatorar você pode quebrar coisas. E, para evitar que essa quebra não vá parar em produção (ou parar o env de produção), você precisa de respostas rápidas a quaisquer mudanças que faça no código, por menores que sejam. (Test-First aqui, alguém?).

Item número dois: evite com todas as forças ficar criando tarefas de refatoração no projeto. A refatoração deve vir com um propósito. Prever o futuro não é um propósito. Não há necessidade de refatorar algo que está em produção há tempos só pelo prazer de refatorar um trecho de código. (você pode fazer isso em casa, para estudar, claro!) Pois você já deveria ter feito isto no terceiro passo do TDD: (Red, Green, Refactor). Se não o fez, espere até que venha precisar trabalhar com aquele código novamente para implementar uma nova feature, daí, divida essa feature em duas etapas: refatorar o código envolvido na nova tarefa e fazer a nova tarefa. Lembre-se: dois passos. Refatorar e somente depois, implementar.

O (semi)deus da refatoração

Lembra que falei que a palavra refatoração você já devia ter ouvido em algum ambiente antes? Sempre ao pegar um código é um costume a gente não entender o que aquilo faz (e por que faz). Por não entender, a gente vai e diz: ah, isso aqui precisa de uma refatoração. Será mesmo?

Esse tipo de atitude pode colocar em xeque os benefícios da refatoração e pior: quebrar algo em produção desnecessariamente, pura e simplesmente porque você sendo novo naquele projeto/equipe não entendeu o código – o que é normal em todo início. Com isso, você faz feio com a equipe e pode destruir todo um processo de amostragem e explicação sobre benefícios de uma refatoração planejada.

É importante que você vá com calma e espere até ter uma certeza mais clara das coisas.

Refatoração nem sempre é a melhor saída

Outro ponto a favor de evitar sair refatorando sem saber quem nem porquê é ter o controle analítico de analisar o cenário em questão e verificar se a refatoração de código solucionaria o problema. Já vi casos em que o código em si não estava ruim, estava aceitável, o verdadeiro problema era um design mal pensando e neste caso, a refatoração não ajudaria em nada. Você precisaria ir além nas decisões.

É preferível manter um código espaguete por mais um tempo do que perder a (única) chance de mostrar os benefícios. No podcast de número 157 do Ruby Rogues, Rebecca Wirfs-Brock afirmou outro ponto importante:

“You’re sort of arguing that refactoring is not necessarily always the best way to clean up a design. Sometimes, you might want to start over.”

Às vezes realmente vale mais a pena fazer um git reset ou um git stash (como disse o Avdi Grimm brincando com ela) mental e partir a fazer uma outra solução do zero. Já tive que fazer isto inúmeras vezes ao longo do tempo. Um grande aliado nesse processo são os Testes de Unidade com Test-First, pois eles dão uma resposta muito rápida sobre seu progresso (ou estagnada) de raciocínio.

Hoje a conclusão, dar-se-á em forma de resumão!

Resumão:

  1. Respostas rápidas ao refatorar. Teste de unidade é a única forma de conseguir isto rapida e isoladamente.
  2. Refatorar. Depois de pronto, volte para implementar o que ia fazer no começo. Não faça os dois juntos por mais que Goku desça da núvem para te pedir isto.
  3. Refatorar ao entrar num projeto: it’s a trap!
  4. Às vezes é melhor um git reset mental e partir para outra solução.
  5. Estar apoiado em teste de unidade e test-first trará a confiança necessária para tomar a decisão de refatorar.
  6. Esforce-se para não tornar a refatoração uma task do seu projeto. Ela deve ser junto de uma task de implementação. Refatorar precisa de propósito.
  7. Red, Green, Refactor (TDD) != Refatorar código de produção.