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.