Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

VOCÊ INICIA SEU PROJETO Tudo começa com uma ideia na cabeça e o famoso rails new myapp.

Slide 3

Slide 3 text

AS VIEWS SE TORNAM COMPLEXAS Difíceis de modificar, é quase impossível reutilizar o código.

Slide 4

Slide 4 text

OS CONTROLLERS SE TORNAM COMPLEXOS Escrever testes ainda é difícil. Modificar o comportamento de uma action envolve muito esforço.

Slide 5

Slide 5 text

OS MODELOS SE TORNAM COMPLEXOS Classes cada vez maiores e com mais responsabilidade do que deveriam ter.

Slide 6

Slide 6 text

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?

Slide 7

Slide 7 text

THE RAILS WAY™

Slide 8

Slide 8 text

NANDO VIEIRA

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

O QUE SIGNIFICA CÓDIGO BEM ESCRITO? A definição de código bem escrito depende do contexto.

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

BOM RUIM

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

VOCÊ É PAGO PARA ENTREGAR VALOR No fim das contas, o que manda são as funcionalidades que você entrega para seus clientes.

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

TESTES AUTOMATIZADOS SÃO ESSENCIAIS Eles devem dar a confiança para que você evolua seu código e não ser um fardo.

Slide 19

Slide 19 text

ESCREVA TESTES PARA O QUE IMPORTA Nem tudo precisa ser testado, mas saber o que precisa é uma tarefa difícil.

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

It is impossible to test everything. It is suicide to test nothing. You should test things that might break. Kent Beck @kentbeck

Slide 23

Slide 23 text

É impossível testar tudo. É suícidio não testar nada. Você deve testar coisas que podem quebrar. Kent Beck @kentbeck

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

TESTES PRECISAM SER RÁPIDOS E SIMPLES Ouça o que seus testes estão dizendo.

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

desc "Import users into the database" task :import_users => :environment do UsersCSVImporter.import('./db/data/users.csv') end

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

ORGANIZAÇÃO DE CÓDIGO Todos tem sua própria ideia de qual é a organização ideal de código.

Slide 40

Slide 40 text

FUJA DO RAILS SEMPRE QUE PUDER O Rails não é sua aplicação, mas pese os benefícios de cada abstração.

Slide 41

Slide 41 text

ABSTRAÇÕES DE MAIS OU DE MENOS? O excesso de abstrações é tão prejudicial quanto à falta de abstrações.

Slide 42

Slide 42 text

APLICAÇÕES MONOLÍTICAS SÃO RUINS Dificuldade para testar, dificuldade para modificar funcionalidades existentes e adicionar novas funcionalidades.

Slide 43

Slide 43 text

Aplicação Monolítica

Slide 44

Slide 44 text

Aplicação Monolítica Microservices

Slide 45

Slide 45 text

APLICAÇÕES MONOLÍTICAS NÃO SÃO TÃO RUINS ASSIM O que é ruim é uma aplicação com alto acoplamento e design pobre.

Slide 46

Slide 46 text

http://shopify.com

Slide 47

Slide 47 text

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.

Slide 48

Slide 48 text

ESTRUTURA DE UM APP app assets helpers jobs mailers models views controllers

Slide 49

Slide 49 text

VOCÊ PODE CRIAR OUTROS DIRETÓRIOS app ... middleware presenters serializers forms policies services

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

<% if [email protected]? && [email protected]_starter_plan? %>
  • Followers
  • <% end %> <% if [email protected]? && [email protected]_starter_plan? %> <%= render 'dbs/followers' %> <% end %>

    Slide 56

    Slide 56 text

    <% if [email protected]? && [email protected]_starter_plan? %>
  • Followers
  • <% end %> <% if [email protected]? && [email protected]_starter_plan? %> <%= render 'dbs/followers' %> <% end %>

    Slide 57

    Slide 57 text

    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

    Slide 58

    Slide 58 text

    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

    Slide 59

    Slide 59 text

    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

    Slide 60

    Slide 60 text

    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

    Slide 61

    Slide 61 text

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

    Slide 62

    Slide 62 text

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

    Slide 63

    Slide 63 text

    <% if @view_object.show_followers? %>
  • Followers
  • <% end %> <%= render 'dbs/followers' if @view_object.show_followers? %>

    Slide 64

    Slide 64 text

    <% if @view_object.show_followers? %>
  • Followers
  • <% end %> <%= render 'dbs/followers' if @view_object.show_followers? %>

    Slide 65

    Slide 65 text

    EXTRAIA A LÓGICA DOS CONTROLLERS O controller deve ser apenas uma camada de input e output. C

    Slide 66

    Slide 66 text

    CONTROLLER Não deve saber sobre a regra de negócio REGRA DE NEGÓCIO Não deve saber sobre o controller

    Slide 67

    Slide 67 text

    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

    Slide 68

    Slide 68 text

    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

    Slide 69

    Slide 69 text

    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

    Slide 70

    Slide 70 text

    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

    Slide 71

    Slide 71 text

    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

    Slide 72

    Slide 72 text

    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

    Slide 73

    Slide 73 text

    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

    Slide 74

    Slide 74 text

    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

    Slide 75

    Slide 75 text

    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

    Slide 76

    Slide 76 text

    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

    Slide 77

    Slide 77 text

    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

    Slide 78

    Slide 78 text

    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

    Slide 79

    Slide 79 text

    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

    Slide 80

    Slide 80 text

    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

    Slide 81

    Slide 81 text

    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

    Slide 82

    Slide 82 text

    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

    Slide 83

    Slide 83 text

    class EventTracker def self.track(options) Scrolls.log(options) end end app/adapters/event_tracker.rb

    Slide 84

    Slide 84 text

    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

    Slide 85

    Slide 85 text

    module ApplicationHelper def page_title PageTitle.new( controller.controller_name, controller.action_name ).title end end

    Slide 86

    Slide 86 text

    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

    Slide 87

    Slide 87 text

    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.

    Slide 88

    Slide 88 text

    No content

    Slide 89

    Slide 89 text

    ATUALIZAÇÃO DE DEPENDÊNCIAS Manter suas dependências atualizadas é uma questão de sobrevivência. É isso, ou deixar seu projeto apodrecer.

    Slide 90

    Slide 90 text

    USE SEMPRE A ÚLTIMA VERSÃO DO RAILS Atualizar o framework não deve ser algo complicado e que despenda muito tempo.

    Slide 91

    Slide 91 text

    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

    Slide 92

    Slide 92 text

    USE A ÚLTIMA VERSÃO ESTÁVEL DO RUBY Correções de falhas de segurança e bugs, novas funcionalidades.

    Slide 93

    Slide 93 text

    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

    Slide 94

    Slide 94 text

    https://github.com/rails/rails/pull/19753

    Slide 95

    Slide 95 text

    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?

    Slide 96

    Slide 96 text

    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.

    Slide 97

    Slide 97 text

    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

    Slide 98

    Slide 98 text

    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

    Slide 99

    Slide 99 text

    EVITE JANELAS QUEBRADAS Sempre faça commit de códigos mais bem escritos do que quando você fez o checkout.

    Slide 100

    Slide 100 text

    REFACTORING É SEMPRE MELHOR QUE REWRITE. Melhorar a qualidade de seu projeto incrementalmente é a melhor saída.

    Slide 101

    Slide 101 text

    AINDA É CEDO, PODE RESUMIR PARA MIM? tl;dw

    Slide 102

    Slide 102 text

    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.

    Slide 103

    Slide 103 text

    NÃO COMECE COM UMA ARQUITETURA COMPLICADA Se você ainda precisa provar o seu negócio, terá que se adaptar rapidamente. 2.

    Slide 104

    Slide 104 text

    SUA EXPERIÊNCIA É MAIS IMPORTANTE Não é porque funcionou para outras pessoas que irá funcionar para você. 3.

    Slide 105

    Slide 105 text

    PENSAR FORA DA CAIXA É ESSENCIAL O Rails se estabeleceu por sugerir padrões, mas não fique preso a este pensamento. 4.

    Slide 106

    Slide 106 text

    BOM DESIGN É MELHOR QUE QUALQUER ARQUITETURA Não adianta extrair seu código se no fim ele continua ruim. 5.

    Slide 107

    Slide 107 text

    @fnando OBRIGADO.