O framework Ruby on Rails é conhecido pela rapidez que proporciona na prototipagem. Sua arquitetura segue 3 princípios de design:
Apesar das facilidades trazidas por esses princípios, o desenvolvedor ainda poderá se deparar com problemas que surgem conforme uma aplicação Ruby on Rails cresce. Alguns deles são:
Diante dessas dificuldades, o que deve fazer o desenvolvedor? Criar mais classes? Nessas horas, alguns padrões podem ser muito úteis:
Também conhecido como Caso de Uso, um Service object concentra a lógica da aplicação e representa a interação do usuário com ela. Ao olhar para os diferentes Service objects, deve se tornar claro o que a aplicação faz.
Valida e verifica todo dado que entra na aplicação, garantindo que está correto. Permite a remoção de strong parameters do Controller, validação dos parâmetros do Model e parsing/coerção dos parâmetros do Controller.
Além dos citados, podemos empregar outros tipos de objetos, como os View objects, Policy objects, Query objects, Decorators, etc.
Levando em conta esses padrões, podemos ainda utilizar frameworks de alto nível para auxiliar nessa padronização de arquitetura. Para exemplificar essa opção, escolhemos o Trailblazer.
O framework Trailblazer fornece uma arquitetura de alto nível para Ruby on Rails. Foi criado por Nick Sutterer, com o objetivo de adicionar convenções ao modo como agrupamos as abstrações (nossos service objects e form objects).
É de muito simples implementação no projeto: pode ser carregado como uma gem e, com isso, permanece como uma dependência do projeto.
gem "trailblazer" gem "trailblazer-rails" # if you are in rails. |
No Trailblazer, você estrutura seu aplicativo por domínio em componentes reais. Eles são chamados de Concepts (conceitos). Os conceitos são implementados para cada entidade em seu domínio de alto nível.
Esses conceitos ficam em “app/concepts”. Todas as classes e visualizações pertencentes a esse conceito (formas, operações e assim por diante) estão neste diretório.
No Trailblazer, o Model fica com a responsabilidade de ser um local apenas para persistência, tornando-se um objeto de baixo nível com um conjunto de funções muito simples: recuperar e gravar dados em um banco de dados. Ele também pode conter métodos de consulta e escopos para recuperar objetos.
Os Controllers tornam-se endpoints HTTP enxutos. Eles podem lidar com a autenticação, diferenciar entre pedidos e formatos como HTML ou JSON e, em seguida, delegar instantaneamente à respectiva operação (sem lógica de processamento).
Mesmo que o Rails tenha muitas convenções, ainda se trata de um framework muito aberto. Isso permite que os times se organizem da forma que acharem melhor, mas também pode resultar em confusão: muitas vezes, os desenvolvedores adotam práticas de trabalho conflitantes.
Para evitar essas situações, a empresa deve ter protocolos bem definidos e claramente comunicados.
Atuando nesse problema, o Trailblazer auxilia na definição de alguns padrões. Padrões estes que vão além de nomes de tabelas e rake tasks: trazem orientação para questões de arquitetura e padrões com escopos e casos de uso.
O Rails geralmente tem uma resposta padrão: na dúvida, direciona tudo ao Model porque o foco são Controllers com pouco código. Como convenção própria, o Trailblazer identifica claramente as diferentes camadas de aplicações web e fornece abstrações.
O framework Trailblazer conta com uma série de abstrações. Elas são de uso opcional, e aconselha-se restringir seu uso às situações de absoluta necessidade. As principais abstrações desse framework são Operations e Contracts:
Essa abstração é o coração da arquitetura Trailblazer, sua camada de domínio. Ela orquestra validações, Policies, Models, Callbacks e lógica de negócios através de uma pipeline funcional com tratamento de erros integrado.
Além disso, possui uma resposta padronizada para toda a aplicação.
Esse é um exemplo da estrutura padrão de uma Operation:
1. module Boleto:: Operation 2. class Create < Trailblazer:: Operation.version (2) 3. private 4. step Model(Boleto, :new) 5. step :generate_unique_document_number 6. step :save_boleto 7. failure :boleto_already_exists 8. success :generate_in_provider 9. 10. def save_boleto (options) 11. ... 12. end 13. 14. def generate_unique_document number(options) 15. ... 16. end 17. 18. def generate in provider(options) 19. ... 20. end 21. end |
Cada step funciona como um método que pode repassar informação para os steps posteriores. Ao final de cada step, devemos retornar um booleano.
Nessa parte podemos ver com clareza a parte funcional de uma Operation, pois ela segue o padrão chamado de Railway Oriented Programming.
Enquanto os steps retornarem true, a classe continuará no fluxo de sucesso, chegando até o método success na linha 8 do código. Caso algum step retorne false, o circuito será rompido e o método failure (linha 7) será executado.
Através de Contracts são implementadas as validações. É uma abstração para lidar com dados arbitrários ou estados de um objeto, sendo ela própria um objeto independente que, nesse caso, é orquestrado por uma Operation.
Abaixo, temos um exemplo da estrutura padrão de um Contract. Property são os parâmetros esperados e Validates funcionam de modo semelhante à maneira que podemos customizar no ActiveModel.
1. module Boleto::Contract 2. class Create < Reform:: Form 3. property :amount 4. property :document number 5. property :expire_at 6. property :notify_debtor, default: false 7. property :seller 8. property :payer 9. 10. validates :seller, presence: true 11. validates :payer, presence: true 12. validate :maximum_amount 13. 14. private 15. 16. def maximum amount 17. return if amount <= Boleto::MAXIMUM AMOUNT 18. errors.add(: base, 'Retorna mensagem de erro') 19. end 20. end 21. end |
Uma vez que você tenha entendido a estrutura de Contract, já pode chamar essa abstração dentro da Operation:
1. module Boleto:: Operation 2. class Create < Trailblazer:: Operation.version (2) 3. private 4. step Model(Boleto, :new) 5. step Contract::Build(constant: Boleto::Contract::Create) 6. step Contract::Validate() 7. step Contract::Persist() 8. step :generate_unique_document_number 9. failure :boleto_already_exists 10. success :generate in provider |
Entre as linhas 5 e 7 vemos as formas como podemos manipular o objeto de Contract e, a partir de seu resultado, seguir no fluxo de sucesso e criar um objeto na base. A validação também pode falhar. Nesse caso, a classe seguirá no fluxo de falha.
Na hora de escolher entre o Trailblazer e suas alternativas (Dry-rb, Interactor, U-case), lembre-se sempre que devemos escolher de acordo com a real necessidade do projeto ou da necessidade padronização do código fonte da empresa.
Analise os pontos positivos e, principalmente, os negativos de cada abordagem.