Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ActiveModel no Mundo dos Wrappers

Tiago Lima
February 21, 2015

ActiveModel no Mundo dos Wrappers

Adicionando uma camada de ActiveModel entre os Wrappers e as aplicações Rails

Tiago Lima

February 21, 2015
Tweet

More Decks by Tiago Lima

Other Decks in Programming

Transcript

  1. ROTEIRO • SOA, HTTP REST e tudo mais • Renderizando

    recursos da API nas aplicações via POROs • Simplificando as coisas com ActiveModel • Dúvidas • Referências Sobre O Que Vamos Falar?
  2. SOA E HTTP REST • Service-Oriented Architecture • Padrão de

    design que visa a construção de um sistema baseado em serviços que se comunicam através de um protocolo • Protocolo: HTTP • REST: estilo arquitetural utilizado para criar Web Services.
  3. UM POUCO DE CONTEXTO API Dashboard Admin (Web) iremos nos

    concentrar na aplicação Admin Dashboard Público (Web)
  4. CRIAÇÃO DE UM LIVRO Dashboard Admin (Web) Book (Wrapper) API

    Book.create(params) post(url, params) { title: ‘Fundação’ } Book (Object)
  5. { "data": { "id": "1", "name": "Fundação" } } JSON

    Book Object id name EXEMPLO DE ENCAPSULAMENTO
  6. EXEMPLO DE RETORNO DE ERROS { "errors": { "name": ["can't

    be blank"], } } Book Object errors String
  7. CARACTERÍSTICAS • Facilita a manipulação do recurso na aplicação •

    Não possui accessors • uso do method_missing module Library class Book def self.create(params) response = post('books', books.to_hash) new(response['data']) end def initialize(data) @data = JSON.parse(data) end def id @data['id'] || @data['_id'] end def valid? errors.nil? end def method_missing(name, *args, &block) if has_key?(name.to_s) data[name.to_s] else nil end end ... end end Book (Wrapper)
  8. CONSEQUÊNCIAS • Replicado em ambas as aplicações Web • Muitas

    responsabilidades • Sabe como interpretar o recurso • Sabe como requisitar o recurso module Library class Book def self.create(params) response = post('books', books.to_hash) new(response['data']) end def initialize(data) @data = JSON.parse(data) end def id @data['id'] || @data['_id'] end def valid? errors.nil? end def method_missing(name, *args, &block) if has_key?(name.to_s) data[name.to_s] else nil end end ... end end Book (Wrapper)
  9. CONSEQUÊNCIAS • new - não há vantagens em usar Book

    • Os erros não são acoplados ao form, uso do flash message class BooksController < ApplicationController def new end def create @book = Library::Book.create(params[:book]) if @book.valid? flash[:success] = 'Successfully created a book.' redirect_to book_path(@book.id) else flash[:error] = @book.errors @book = Library::Book.new(params[:book].to_json) render :new, status: 400 end end end Dashboard Admin (Web)
  10. CONSEQUÊNCIAS • Não há interação entre Rails e o Objeto

    • Perde-se a facilidade de se usar Rails • Erros não possuem relação com o form <%= form_tag(books_path) do %> <%= field_set_tag 'Book information' do %> <%= label_tag 'book[name]', 'Name' %> <%= text_field_tag 'book[name]', book.name %> <%= label_tag 'book[description]', 'Description' %> <%= text_area_tag 'book[description]', book.description %> <%= label_tag 'book[price]', 'Price' %> <%= text_area_tag 'book[price]', book.price %> <%= submit_tag “Create" %> <% end %> <% end %> Dashboard Admin (Web)
  11. CONSEQUÊNCIAS • Renderização dos erros de forma manual • Erros

    mostrados globalmente. Se fosse necessário mostrar os erros por campo, aumentaria e muito a complexidade <% flash.each do |type, message| %> <div class="alert alert-<%= type %>"> <p> <strong><%= type.to_s.camelize %>!</strong> <%= message %> </p> </div> <% end %> Dashboard Admin (Web)
  12. RESUMO DO PROBLEMA • Muitas responsabilidades • Código duplicado em

    ambas as aplicações • Só é usado parte dos recursos do Rails • Piora sensivelmente a legibilidade • Renderização feita na mão e aumento de complexidade para melhorar UX
  13. CRIAÇÃO DE UM LIVRO Dashboard Admin (Web) BookMapper API post(url,

    params) { title: ‘Fundação’ } Book.new Book Book (ActiveModel) Wrapper BookMapper.create(params)
  14. SEPARAÇÃO DE RESPONSABILIDADES • Model: cria recursos utilizando o ActiveModel

    • Mapper: adapter com interface similar ao ActiveRecord (create, find, etc.) ├── mappers │ ├── mapper.rb │ ├── book_mapper.rb ├── models │ ├── base_model.rb │ ├── book.rb ├── paginated.rb ├── resource │ ├── base.rb │ ├── image.rb │ └── resource.rb ├── responder.rb ├── response.rb └── version.rb Wrapper BookMapper Book
  15. MODELS – BOOK • Simples • Extensível • Se preocupa

    apenas com a manipulação dos atributos module Library class Book < BaseModel def name title end end end
  16. MODELS – BASE MODEL • Usa módulos do ActiveModel para

    interagir com o form e o i18n. • Usa o módulo Errors para se comunicar com Rails • Accessors criados dinamicamente module Fuelzee class BaseModel include ActiveModel::Model extend ActiveModel::Naming include ActiveModel::Conversion attr_reader :errors def initialize(data={}) data_errors = data.delete(:errors) if data_errors @errors = ActiveModel::Errors.new(self) add_errors(data_errors) end self.class.module_eval { attr_accessor *data.keys } super(data) end def method_missing(m, *args, &block) nil end private def add_errors(data_errors) if data_errors.is_a? Hash data_errors.each do |attr, error_arr| error_arr.each do |error| errors.add(attr, error) end end else errors.add(:base, data_errors) end end end end
  17. { "data": { "id": "1", "name": "Fundação" } } JSON

    Book ActiveModel id title EXEMPLO DE ENCAPSULAMENTO id= title=
  18. EXEMPLO DE RETORNO DE ERROS { "errors": { "name": ["can't

    be blank"], } } Book ActiveModel errors ActiveModel::Errors
  19. BOOKMAPPER • Envia requisição para a API • Livre da

    lógica de interpretação do recurso da API • Indica Modelo a ser usado module Library class BookMapper < Mapper def create(params) response = transport.post("books", params) build_response(response) end protected def model_class Book end end end
  20. BENEFÍCIOS • É possível usar o Objeto no form_for •

    As mensagens de erro são automaticamente acopladas ao form pelo Rails class BooksController < ApplicationController def new @book = Library::Book.new end def create book_mapper = Library::BookMapper.create(params[:book]) if book_mapper.valid? flash[:success] = 'Successfully created reward.' redirect_to books_path(book_mapper.id) else @book = Library::Book.new(params[:book]) @book.errors = book_mapper.errors render :new, status: 422 end end end
  21. BENEFÍCIOS <%= form_for book, (..) do |f| %> <% if

    book.errors && book.errors.any? %> <%# renderiza os errors %> <% end %> <%= field_set_tag “Book Information” do %> <%= f.label :name %> <%= f.text_field :name %> <%= f.label :description %> <%= f.text_field :description %> <%= f.label :price %> <%= f.text_field :price %> <%= f.submit “Create" %> <% end %> <% end %> • É possível usar o Objeto no form_for • As mensagens de erro são automaticamente acopladas ao form pelo Rails • Melhora legibilidade
  22. RESUMO DA OBRA • Reusa o wrapper em diversas aplicações

    clientes da API • Extrai responsabilidades de requisição do modelo • Melhora a manipulação do Objeto através dos accessors
  23. RESUMO DA OBRA • Possibilidade de usar o Model como

    parâmetro do form_for • Automatiza a renderização dos erros, através do Rails • Melhor manipulação dos erros
  24. DÚVIDAS • Talvez usar um Representer (roar)/Virtus para inferir atributos

    da API • Ou é melhor inferir dinamicamente? • Separar em gems diferentes as responsabilidades dos Mappers e dos Models
  25. REFERÊNCIAS • ActiveModel: Make Any Ruby Object Feel Like ActiveRecord

    • ActiveModel • Railscasts: #416 Form Objects