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. 2.

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

    cabeça e o famoso rails new myapp.
  2. 4.

    OS CONTROLLERS SE TORNAM COMPLEXOS Escrever testes ainda é difícil.

    Modificar o comportamento de uma action envolve muito esforço.
  3. 5.

    OS MODELOS SE TORNAM COMPLEXOS Classes cada vez maiores e

    com mais responsabilidade do que deveriam ter.
  4. 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?
  5. 9.
  6. 10.
  7. 12.
  8. 13.
  9. 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
  10. 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
  11. 16.

    VOCÊ É PAGO PARA ENTREGAR VALOR No fim das contas,

    o que manda são as funcionalidades que você entrega para seus clientes.
  12. 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
  13. 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.
  14. 19.

    ESCREVA TESTES PARA O QUE IMPORTA Nem tudo precisa ser

    testado, mas saber o que precisa é uma tarefa difícil.
  15. 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
  16. 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
  17. 22.

    It is impossible to test everything. It is suicide to

    test nothing. You should test things that might break. Kent Beck @kentbeck
  18. 23.

    É impossível testar tudo. É suícidio não testar nada. Você

    deve testar coisas que podem quebrar. Kent Beck @kentbeck
  19. 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
  20. 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
  21. 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
  22. 28.

    desc "Import users into the database" task :import_users => :environment

    do UsersCSVImporter.import('./db/data/users.csv') end
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. 39.
  34. 40.

    FUJA DO RAILS SEMPRE QUE PUDER O Rails não é

    sua aplicação, mas pese os benefícios de cada abstração.
  35. 41.

    ABSTRAÇÕES DE MAIS OU DE MENOS? O excesso de abstrações

    é tão prejudicial quanto à falta de abstrações.
  36. 42.

    APLICAÇÕES MONOLÍTICAS SÃO RUINS Dificuldade para testar, dificuldade para modificar

    funcionalidades existentes e adicionar novas funcionalidades.
  37. 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.
  38. 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.
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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 %>
  44. 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 %>
  45. 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
  46. 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
  47. 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
  48. 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
  49. 63.

    <% if @view_object.show_followers? %> <li> <a href="#followers" class="followers">Followers</a> </li> <%

    end %> <!-- ... --> <%= render 'dbs/followers' if @view_object.show_followers? %>
  50. 64.

    <% if @view_object.show_followers? %> <li> <a href="#followers" class="followers">Followers</a> </li> <%

    end %> <!-- ... --> <%= render 'dbs/followers' if @view_object.show_followers? %>
  51. 66.

    CONTROLLER Não deve saber sobre a regra de negócio REGRA

    DE NEGÓCIO Não deve saber sobre o controller
  52. 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
  53. 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
  54. 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
  55. 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
  56. 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
  57. 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
  58. 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
  59. 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
  60. 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
  61. 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
  62. 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
  63. 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
  64. 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
  65. 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
  66. 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
  67. 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
  68. 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
  69. 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
  70. 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.
  71. 88.
  72. 89.

    ATUALIZAÇÃO DE DEPENDÊNCIAS Manter suas dependências atualizadas é uma questão

    de sobrevivência. É isso, ou deixar seu projeto apodrecer.
  73. 90.

    USE SEMPRE A ÚLTIMA VERSÃO DO RAILS Atualizar o framework

    não deve ser algo complicado e que despenda muito tempo.
  74. 91.
  75. 92.

    USE A ÚLTIMA VERSÃO ESTÁVEL DO RUBY Correções de falhas

    de segurança e bugs, novas funcionalidades.
  76. 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
  77. 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?
  78. 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.
  79. 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
  80. 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
  81. 99.

    EVITE JANELAS QUEBRADAS Sempre faça commit de códigos mais bem

    escritos do que quando você fez o checkout.
  82. 100.

    REFACTORING É SEMPRE MELHOR QUE REWRITE. Melhorar a qualidade de

    seu projeto incrementalmente é a melhor saída.
  83. 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.
  84. 103.

    NÃO COMECE COM UMA ARQUITETURA COMPLICADA Se você ainda precisa

    provar o seu negócio, terá que se adaptar rapidamente. 2.
  85. 104.

    SUA EXPERIÊNCIA É MAIS IMPORTANTE Não é porque funcionou para

    outras pessoas que irá funcionar para você. 3.
  86. 105.

    PENSAR FORA DA CAIXA É ESSENCIAL O Rails se estabeleceu

    por sugerir padrões, mas não fique preso a este pensamento. 4.
  87. 106.

    BOM DESIGN É MELHOR QUE QUALQUER ARQUITETURA Não adianta extrair

    seu código se no fim ele continua ruim. 5.