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

Criando aplicações mais fáceis de manter com Ruby on Rails

Cb5d9e9095cd41b636764a85e57ade4b?s=47 Nando Vieira
September 18, 2015

Criando aplicações mais fáceis de manter com Ruby on Rails

Desenvolver aplicativos Rails é um prazer. O framework abstrai um bocado de partes chatas e lhe dá as ferramentas para implementar aplicações web complexas rapidamente. Mas não leva muito tempo, até você não poder implementar novas features sem arrancar seus próprios cabelos. E que tal atualizar seu projeto com uma nova versão de Rails ou Ruby? Alguns projetos simplesmente não conseguem.

Nesta palestra nós veremos como você pode se esquivar desta armadilha, criando apps que você pode sobreviver ao longo do tempo.

Palestra dada na Rubyconf Brasil 2015

Cb5d9e9095cd41b636764a85e57ade4b?s=128

Nando Vieira

September 18, 2015
Tweet

Transcript

  1. Criando aplicações mais fáceis de manter @fnando RUBY ON RAILS

  2. VOCÊ INICIA SEU PROJETO Tudo começa com uma ideia na

    cabeça e o famoso rails new myapp.
  3. AS VIEWS SE TORNAM COMPLEXAS Difíceis de modificar, é quase

    impossível reutilizar o código.
  4. OS CONTROLLERS SE TORNAM COMPLEXOS Escrever testes ainda é difícil.

    Modificar o comportamento de uma action envolve muito esforço.
  5. OS MODELOS SE TORNAM COMPLEXOS Classes cada vez maiores e

    com mais responsabilidade do que deveriam ter.
  6. SERÁ QUE O RAILS É A MELHOR FERRAMENTA? Ou será

    que é preciso mudar o modo como o código é escrito, assim como a arquitetura da aplicação?
  7. THE RAILS WAY™

  8. NANDO VIEIRA

  9. None
  10. None
  11. O QUE SIGNIFICA CÓDIGO BEM ESCRITO? A definição de código

    bem escrito depende do contexto.
  12. None
  13. BOM RUIM

  14. https://twitter.com/tenderlove/status/573153754431688704 It’s fun to complain about bad code, but I

    have to remember that “perfect” is the enemy of “shipped”. Aaron Patterson @tenderlove
  15. https://twitter.com/tenderlove/status/573153754431688704 É divertido reclamar de código ruim, mas preciso me

    lembrar que “perfeito” é o inimigo de “entregue”. Aaron Patterson @tenderlove
  16. VOCÊ É PAGO PARA ENTREGAR VALOR No fim das contas,

    o que manda são as funcionalidades que você entrega para seus clientes.
  17. SALÁRIO BRUTO ENCARGOS BENEFÍCIOS TOTAL R$5K R$2644 R$850 R$8494 CUSTO

    MENSAL PARA A EMPRESA http://www.calculador.com.br/calculo/custo-funcionario-empresa
  18. TESTES AUTOMATIZADOS SÃO ESSENCIAIS Eles devem dar a confiança para

    que você evolua seu código e não ser um fardo.
  19. ESCREVA TESTES PARA O QUE IMPORTA Nem tudo precisa ser

    testado, mas saber o que precisa é uma tarefa difícil.
  20. I get paid for code that works, not for tests,

    so my philosophy is to test as little as possible to reach a given level of confidence. Kent Beck @kentbeck
  21. Eu sou pago para escrever código que funciona, não testes,

    então minha filosofia é testar o suficiente para atingir um certo nível de confiança. Kent Beck @kentbeck
  22. It is impossible to test everything. It is suicide to

    test nothing. You should test things that might break. Kent Beck @kentbeck
  23. É impossível testar tudo. É suícidio não testar nada. Você

    deve testar coisas que podem quebrar. Kent Beck @kentbeck
  24. require "rails_helper" RSpec.describe Customer, type: :model do describe "db structure"

    do it { is_expected.to have_db_column(:full_name).of_type(:string) } it { is_expected.to have_db_column(:email).of_type(:string) } it { is_expected.to have_db_column(:phone).of_type(:string) } it { is_expected.to have_db_column(:created_at).of_type(:datetime) } it { is_expected.to have_db_column(:updated_at).of_type(:datetime) } end end
  25. require "rails_helper" RSpec.describe Customer, type: :model do describe "db structure"

    do it { is_expected.to have_db_column(:full_name).of_type(:string) } it { is_expected.to have_db_column(:email).of_type(:string) } it { is_expected.to have_db_column(:phone).of_type(:string) } it { is_expected.to have_db_column(:created_at).of_type(:datetime) } it { is_expected.to have_db_column(:updated_at).of_type(:datetime) } end end
  26. TESTES PRECISAM SER RÁPIDOS E SIMPLES Ouça o que seus

    testes estão dizendo.
  27. desc "Import users into the database" task :import_users => :environment

    do CSV.foreach("./db/data/users.csv") do |row| name, email, password = [*row, SecureRandom.hex] next if User.exist?(email: email) User.create!(name: name, email: email, password: password) end end
  28. desc "Import users into the database" task :import_users => :environment

    do UsersCSVImporter.import('./db/data/users.csv') end
  29. class UsersCvsImporterTest < Minitest::Test setup { DatabaseCleaner.clean } test 'import

    users' do assert_equal 0, User.count UsersCSVImporter.import('./support/users.csv') assert_equal 2, User.count end test 'set default password' do UsersCSVImporter.import('./support/users.csv', default_password: 'test') assert User.first.authenticate('test') end test 'do not import duplicated accounts' do UsersCSVImporter.import('./support/users.csv') UsersCSVImporter.import('./support/users.csv') assert_equal 2, User.count end end
  30. require 'csv' class UsersCSVImporter def self.import(file, default_password: SecureRandom.hex, store: User)

    CSV.foreach(file) do |row| name, email, _ = row next if store.exists?(email: email) store.create!(name: name, email: email, password: default_password) end end end
  31. require 'csv' class UsersCSVImporter def self.import(file, default_password: SecureRandom.hex, store: User)

    CSV.foreach(file) do |row| name, email, _ = row next if store.exists?(email: email) store.create!(name: name, email: email, password: default_password) end end end
  32. require 'csv' class UsersCSVImporter def self.import(file, default_password: SecureRandom.hex, store: User)

    CSV.foreach(file) do |row| name, email, _ = row next if store.exists?(email: email) store.create!(name: name, email: email, password: default_password) end end end
  33. require 'csv' class UsersCSVImporter def self.import(file, default_password: SecureRandom.hex, store: User)

    CSV.foreach(file) do |row| name, email, _ = row next if store.exists?(email: email) store.create!(name: name, email: email, password: default_password) end end end
  34. class UsersCvsImporterTest < Minitest::Test test 'import users' do store =

    FakeStore.new UsersCSVImporter.import('./support/users.csv', store: store) assert_equal 2, store.count end test 'set default password' do store = FakeStore.new UsersCSVImporter.import('./support/users.csv', default_password: 'test', store: store) assert_equal 'test', store.first[:password] end test 'do not import duplicated accounts' do store = FakeStore.new UsersCSVImporter.import('./support/users.csv', store: store) UsersCSVImporter.import('./support/users.csv', store: store) assert_equal 2, store.count end end
  35. class UsersCvsImporterTest < Minitest::Test test 'import users' do store =

    FakeStore.new UsersCSVImporter.import('./support/users.csv', store: store) assert_equal 2, store.count end test 'set default password' do store = FakeStore.new UsersCSVImporter.import('./support/users.csv', default_password: 'test', store: store) assert_equal 'test', store.first[:password] end test 'do not import duplicated accounts' do store = FakeStore.new UsersCSVImporter.import('./support/users.csv', store: store) UsersCSVImporter.import('./support/users.csv', store: store) assert_equal 2, store.count end end
  36. class UsersCvsImporterTest < Minitest::Test test 'import users' do store =

    FakeStore.new UsersCSVImporter.import('./support/users.csv', store: store) assert_equal 2, store.count end test 'set default password' do store = FakeStore.new UsersCSVImporter.import('./support/users.csv', default_password: 'test', store: store) assert_equal 'test', store.first[:password] end test 'do not import duplicated accounts' do store = FakeStore.new UsersCSVImporter.import('./support/users.csv', store: store) UsersCSVImporter.import('./support/users.csv', store: store) assert_equal 2, store.count end end
  37. class UsersCvsImporterTest < Minitest::Test test 'import users' do store =

    FakeStore.new UsersCSVImporter.import('./support/users.csv', store: store) assert_equal 2, store.count end test 'set default password' do store = FakeStore.new UsersCSVImporter.import('./support/users.csv', default_password: 'test', store: store) assert_equal 'test', store.first[:password] end test 'do not import duplicated accounts' do store = FakeStore.new UsersCSVImporter.import('./support/users.csv', store: store) UsersCSVImporter.import('./support/users.csv', store: store) assert_equal 2, store.count end end
  38. class FakeStore def initialize; @store = []; end def first;

    @store.first; end def count; @store.count; end def create!(attrs); @store << attrs; end def exists?(conditions) @store.any? {|u| u[:email] == conditions[:email] } end end
  39. ORGANIZAÇÃO DE CÓDIGO Todos tem sua própria ideia de qual

    é a organização ideal de código.
  40. FUJA DO RAILS SEMPRE QUE PUDER O Rails não é

    sua aplicação, mas pese os benefícios de cada abstração.
  41. ABSTRAÇÕES DE MAIS OU DE MENOS? O excesso de abstrações

    é tão prejudicial quanto à falta de abstrações.
  42. APLICAÇÕES MONOLÍTICAS SÃO RUINS Dificuldade para testar, dificuldade para modificar

    funcionalidades existentes e adicionar novas funcionalidades.
  43. Aplicação Monolítica

  44. Aplicação Monolítica Microservices

  45. APLICAÇÕES MONOLÍTICAS NÃO SÃO TÃO RUINS ASSIM O que é

    ruim é uma aplicação com alto acoplamento e design pobre.
  46. http://shopify.com

  47. ADAPTE A ESTRUTURA DE DIRETÓRIOS DO RAILS Utilize a estrutura

    que você precisa, em vez de ficar preso aos diretórios criados pelo próprio Ruby on Rails.
  48. ESTRUTURA DE UM APP app assets helpers jobs mailers models

    views controllers
  49. VOCÊ PODE CRIAR OUTROS DIRETÓRIOS app ... middleware presenters serializers

    forms policies services
  50. AGRUPE POR CONTEXTOS DE NEGÓCIO app actions signup http://teotti.com/application-directories-named-as-architectural-patterns-antipattern/ checkout

    order_processing order_listing
  51. EXTRAIA A LÓGICA DE MODELOS ACTIVERECORD Não utilize essas classes

    como o lugar responsável por fazer tudo sobre aquele domínio. M
  52. class User < ActiveRecord::Base after_commit(on: :create) { subscribe_to_newsletter('updates') } def

    subscribe_to_newsletter(newsletter_name) newsletter = Newsletter.find_by_name!(newsletter_name) subscription = newsletter.subscriptions.find_or_create_by!(user_id: self) end def unsubscribe_from_newsletter(newsletter_name) newsletter = Newsletter.find_by_name!(newsletter_name) subscription = newsletter.subscriptions.where(user_id: self).first! subscription.destroy! end end
  53. class NewsletterSubscriber def self.call(user, newsletter_name) newsletter = Newsletter.find_by_name!(newsletter_name) newsletter.subscriptions.find_or_create_by!(user_id: user)

    end end class NewsletterUnsubscriber def self.call(user, newsletter_name) newsletter = Newsletter.find_by_name!(newsletter_name) subscription = newsletter.subscriptions.where(user_id: user).first! subscription.destroy! end end
  54. EXTRAIA A LÓGICA DAS VIEWS As views não devem ter

    nada além de loops e lógicas simples de exibição. V
  55. <% if !@db.follower? && !@db.is_starter_plan? %> <li> <a href="#followers" class="followers">Followers</a>

    </li> <% end %> <!-- ... --> <% if !@db.follower? && !@db.is_starter_plan? %> <%= render 'dbs/followers' %> <% end %>
  56. <% if !@db.follower? && !@db.is_starter_plan? %> <li> <a href="#followers" class="followers">Followers</a>

    </li> <% end %> <!-- ... --> <% if !@db.follower? && !@db.is_starter_plan? %> <%= render 'dbs/followers' %> <% end %>
  57. 1. Classes com até 100 linhas 2. Métodos com até

    5 linhas 3. Métodos com até 4 parâmetros 4. Apenas 1 objeto de negócio 5. Apenas 1 objeto de apresentação Sandi Metz @sandimetz
  58. 1. Classes com até 100 linhas 2. Métodos com até

    5 linhas 3. Métodos com até 4 parâmetros 4. Apenas 1 objeto de negócio 5. Apenas 1 objeto de apresentação Sandi Metz @sandimetz
  59. class DbShowPresenter attr_reader :db private :db def initialize(db) @db =

    DbPresenter.new(db) end def show_followers? !db.follower? && !db.is_starter_plan? end end app/presenters/db_show_presenter.rb
  60. class DbShowPresenter attr_reader :db private :db def initialize(db) @db =

    DbPresenter.new(db) end def show_followers? !db.follower? && !db.is_starter_plan? end end app/presenters/db_show_presenter.rb
  61. class DbsController < ApplicationController def show @view_object = DbShowPresenter.new(find_db) end

    private def find_db; end end
  62. class DbsController < ApplicationController def show @view_object = DbShowPresenter.new(find_db) end

    private def find_db; end end
  63. <% if @view_object.show_followers? %> <li> <a href="#followers" class="followers">Followers</a> </li> <%

    end %> <!-- ... --> <%= render 'dbs/followers' if @view_object.show_followers? %>
  64. <% if @view_object.show_followers? %> <li> <a href="#followers" class="followers">Followers</a> </li> <%

    end %> <!-- ... --> <%= render 'dbs/followers' if @view_object.show_followers? %>
  65. EXTRAIA A LÓGICA DOS CONTROLLERS O controller deve ser apenas

    uma camada de input e output. C
  66. CONTROLLER Não deve saber sobre a regra de negócio REGRA

    DE NEGÓCIO Não deve saber sobre o controller
  67. class SignupController < ApplicationController def create @user = User.new(user_params) ActiveRecord::Base.transaction

    do if @user.save Mailer.welcome(@user).deliver_later NewsletterSubscriber.call(@user, 'updates') Scrolls.log(action: 'signup', user: @user.id) redirect_to login_path, notice: t('flash.signup.create.notice') else render :new end end end # ... end
  68. class SignupController < ApplicationController def create @user = User.new(user_params) ActiveRecord::Base.transaction

    do if @user.save Mailer.welcome(@user).deliver_later NewsletterSubscriber.call(@user, 'updates') Scrolls.log(action: 'signup', user: @user.id) redirect_to login_path, notice: t('flash.signup.create.notice') else render :new end end end # ... end
  69. class SignupController < ApplicationController def create @user = User.new(user_params) ActiveRecord::Base.transaction

    do if @user.save Mailer.welcome(@user).deliver_later NewsletterSubscriber.call(@user, 'updates') Scrolls.log(action: 'signup', user: @user.id) redirect_to login_path, notice: t('flash.signup.create.notice') else render :new end end end # ... end
  70. class SignupController < ApplicationController def create @user = User.new(user_params) ActiveRecord::Base.transaction

    do if @user.save Mailer.welcome(@user).deliver_later NewsletterSubscriber.call(@user, 'updates') Scrolls.log(action: 'signup', user: @user.id) redirect_to login_path, notice: t('flash.signup.create.notice') else render :new end end end # ... end
  71. class Signup def initialize(user); @user = user; end def call

    ActiveRecord::Base.transaction do return unless save_user send_welcome_email subscribe_to_newsletter log_action end true end def save_user; @user.save; end def send_welcome_email; end def subscribe_to_newsletter; end def log_action; end end
  72. class Signup def initialize(user); @user = user; end def call

    ActiveRecord::Base.transaction do return unless save_user send_welcome_email subscribe_to_newsletter log_action end true end def save_user; @user.save; end def send_welcome_email; end def subscribe_to_newsletter; end def log_action; end end
  73. class Signup def initialize(user); @user = user; end def call

    ActiveRecord::Base.transaction do return unless save_user send_welcome_email subscribe_to_newsletter log_action end true end def save_user; @user.save; end def send_welcome_email; end def subscribe_to_newsletter; end def log_action; end end
  74. class Signup def initialize(user); @user = user; end def call

    ActiveRecord::Base.transaction do return unless save_user send_welcome_email subscribe_to_newsletter log_action end true end def save_user; @user.save; end def send_welcome_email; end def subscribe_to_newsletter; end def log_action; end end
  75. class Signup def initialize(user); @user = user; end def call

    ActiveRecord::Base.transaction do return unless save_user send_welcome_email subscribe_to_newsletter log_action end true end def save_user; @user.save; end def send_welcome_email; end def subscribe_to_newsletter; end def log_action; end end
  76. class Signup def initialize(user); @user = user; end def call

    ActiveRecord::Base.transaction do return unless save_user send_welcome_email subscribe_to_newsletter log_action end true end def save_user; @user.save; end def send_welcome_email; end def subscribe_to_newsletter; end def log_action; end end
  77. class SignupController < ApplicationController def create @user = User.new(user_params) if

    Signup.new(@user).call redirect_to login_path, notice: t('flash.signup.create.notice') else render :new end end # ... end
  78. class SignupController < ApplicationController def create @user = User.new(user_params) Signup.new(@user)

    .on(:success) { redirect_to login_path, notice: t('flash.signup.create.notice') } .on(:failure) { render :new } .call end # ... end Uma outra alternativa usando observers
  79. class Signup include Signal def initialize(user) @user = user end

    def call ActiveRecord::Base.transaction do return emit(:failure) unless save_user send_welcome_email subscribe_to_newsletter log_action end emit(:success) end # ... end http://rubygems.org/gems/signal
  80. class Signup include Signal def initialize(user) @user = user end

    def call ActiveRecord::Base.transaction do return emit(:failure) unless save_user send_welcome_email subscribe_to_newsletter log_action end emit(:success) end # ... end http://rubygems.org/gems/signal
  81. Injeção de dependências e adapters class Signup # ... def

    send_welcome_email Mailer.welcome(@user).deliver_later end def subscribe_to_newsletter NewsletterSubscriber.call(@user, 'updates') end def log_action Scrolls.log(action: 'signup', user: @user.id) end end
  82. Injeção de dependências e adapters class Signup # ... def

    send_welcome_email Mailer.welcome(@user).deliver_later end def subscribe_to_newsletter NewsletterSubscriber.call(@user, 'updates') end def log_action Scrolls.log(action: 'signup', user: @user.id) end end
  83. class EventTracker def self.track(options) Scrolls.log(options) end end app/adapters/event_tracker.rb

  84. EXTRAIA A LÓGICA DE HELPERS Se o seu helper for

    muito complexo e precisar de métodos utilitários, prefira extrair o comportamento em classes. H
  85. module ApplicationHelper def page_title PageTitle.new( controller.controller_name, controller.action_name ).title end end

  86. class PageTitle ACTION_ALIASES = { "create" => :new, "update" =>

    :edit, "destroy" => :remove } attr_reader :controller def initialize(controller, action) @controller, @action = controller, action end def action ACTION_ALIASES.fetch(@action, @action) end def title I18n.t(action, :scope => [:titles, controller]) end end
  87. EVITE DESIGN PATTERNS ISOLADAMENTE Você leu um novo artigo explicando

    sobre um design pattern e está louco para usá-lo em seu projeto. Por favor, não.
  88. None
  89. ATUALIZAÇÃO DE DEPENDÊNCIAS Manter suas dependências atualizadas é uma questão

    de sobrevivência. É isso, ou deixar seu projeto apodrecer.
  90. USE SEMPRE A ÚLTIMA VERSÃO DO RAILS Atualizar o framework

    não deve ser algo complicado e que despenda muito tempo.
  91. 4.2.0 4.1.0 4.0.0 3.2.0 3.1.0 3.0.0 Dezembro/2014 Abril/2014 Junho/2013 Janeiro/2012

    Agosto/2011 Agosto/2010 DATA DE LANÇAMENTO DO RAILS
  92. USE A ÚLTIMA VERSÃO ESTÁVEL DO RUBY Correções de falhas

    de segurança e bugs, novas funcionalidades.
  93. 2.2.0 2.1.0 2.0.0 1.9.3 1.9.1 1.8.7 1.8.6 1.8.0 Dezembro/2014 Dezembro/2013

    Fevereiro/2013 Outubro/2011 Janeiro/2009 Junho/2008 Março/2007 Agosto/2003 DATA DE LANÇAMENTO DO RUBY
  94. https://github.com/rails/rails/pull/19753

  95. DIMINUA O NÚMERO DE DEPENDÊNCIAS Por que adicionar uma dependência

    complexa quando você pode implementar a mesma funcionalidade com poucas linhas de código?
  96. NÃO MODIFIQUE OS ARQUIVOS GERADOS Em vez de modificar arquivos

    de ambiente e o arquivo config/application.rb, faça a configuração através de initializers.
  97. if Rails.env.production? ActionMailer::Base.tap do |action_mailer| action_mailer.delivery_method = :smtp action_mailer.perform_deliveries =

    true action_mailer.raise_delivery_errors = true action_mailer.smtp_settings = { address: 'smtp.sendgrid.net', port: '587', authentication: :plain, user_name: ENV['SENDGRID_USERNAME'], password: ENV['SENDGRID_PASSWORD'], domain: 'example.com', enable_starttls_auto: true } end end config/initializers/action_mailer.rb
  98. https://github.com/fnando/rails-env Rails.env.on(:production) do config.action_mailer.delivery_method = :smtp config.action_mailer.perform_deliveries = true config.action_mailer.raise_delivery_errors

    = true config.action_mailer.smtp_settings = { address: 'smtp.sendgrid.net', port: '587', authentication: :plain, user_name: ENV['SENDGRID_USERNAME'], password: ENV['SENDGRID_PASSWORD'], domain: 'example.com', enable_starttls_auto: true } end config/initializers/action_mailer.rb
  99. EVITE JANELAS QUEBRADAS Sempre faça commit de códigos mais bem

    escritos do que quando você fez o checkout.
  100. REFACTORING É SEMPRE MELHOR QUE REWRITE. Melhorar a qualidade de

    seu projeto incrementalmente é a melhor saída.
  101. AINDA É CEDO, PODE RESUMIR PARA MIM? tl;dw

  102. PESE PRÓS E CONTRAS DE CADA DECISÃO 1. Não siga

    nada sem saber quais as implicações de uma arquitetura ou pattern.
  103. NÃO COMECE COM UMA ARQUITETURA COMPLICADA Se você ainda precisa

    provar o seu negócio, terá que se adaptar rapidamente. 2.
  104. SUA EXPERIÊNCIA É MAIS IMPORTANTE Não é porque funcionou para

    outras pessoas que irá funcionar para você. 3.
  105. PENSAR FORA DA CAIXA É ESSENCIAL O Rails se estabeleceu

    por sugerir padrões, mas não fique preso a este pensamento. 4.
  106. BOM DESIGN É MELHOR QUE QUALQUER ARQUITETURA Não adianta extrair

    seu código se no fim ele continua ruim. 5.
  107. @fnando OBRIGADO.