Share
Embed
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
Nando Vieira
September 18, 2015
Transcript
107 slides
-
VOCÊ INICIA SEU PROJETO Tudo começa com uma ideia na
cabeça e o famoso rails new myapp. -
AS VIEWS SE TORNAM COMPLEXAS Difíceis de modificar, é quase
impossível reutilizar o código. -
OS CONTROLLERS SE TORNAM COMPLEXOS Escrever testes ainda é difícil.
Modificar o comportamento de uma action envolve muito esforço. -
OS MODELOS SE TORNAM COMPLEXOS Classes cada vez maiores e
com mais responsabilidade do que deveriam ter. -
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? -
O QUE SIGNIFICA CÓDIGO BEM ESCRITO? A definição de código
bem escrito depende do contexto. -
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 -
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 -
VOCÊ É PAGO PARA ENTREGAR VALOR No fim das contas,
o que manda são as funcionalidades que você entrega para seus clientes. -
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 -
TESTES AUTOMATIZADOS SÃO ESSENCIAIS Eles devem dar a confiança para
que você evolua seu código e não ser um fardo. -
ESCREVA TESTES PARA O QUE IMPORTA Nem tudo precisa ser
testado, mas saber o que precisa é uma tarefa difícil. -
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 -
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 -
It is impossible to test everything. It is suicide to
test nothing. You should test things that might break. Kent Beck @kentbeck -
É impossível testar tudo. É suícidio não testar nada. Você
deve testar coisas que podem quebrar. Kent Beck @kentbeck -
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 -
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 -
TESTES PRECISAM SER RÁPIDOS E SIMPLES Ouça o que seus
testes estão dizendo. -
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 -
desc "Import users into the database" task :import_users => :environment
do UsersCSVImporter.import('./db/data/users.csv') end -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
ORGANIZAÇÃO DE CÓDIGO Todos tem sua própria ideia de qual
é a organização ideal de código. -
FUJA DO RAILS SEMPRE QUE PUDER O Rails não é
sua aplicação, mas pese os benefícios de cada abstração. -
ABSTRAÇÕES DE MAIS OU DE MENOS? O excesso de abstrações
é tão prejudicial quanto à falta de abstrações. -
APLICAÇÕES MONOLÍTICAS SÃO RUINS Dificuldade para testar, dificuldade para modificar
funcionalidades existentes e adicionar novas funcionalidades. -
APLICAÇÕES MONOLÍTICAS NÃO SÃO TÃO RUINS ASSIM O que é
ruim é uma aplicação com alto acoplamento e design pobre. -
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. -
ESTRUTURA DE UM APP app assets helpers jobs mailers models
views controllers -
VOCÊ PODE CRIAR OUTROS DIRETÓRIOS app ... middleware presenters serializers
forms policies services -
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 -
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 -
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 -
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 -
<% if [email protected]? && [email protected]_starter_plan? %> <li> <a href="#followers" class="followers">Followers</a>
</li> <% end %> <!-- ... --> <% if [email protected]? && [email protected]_starter_plan? %> <%= render 'dbs/followers' %> <% end %> -
<% if [email protected]? && [email protected]_starter_plan? %> <li> <a href="#followers" class="followers">Followers</a>
</li> <% end %> <!-- ... --> <% if [email protected]? && [email protected]_starter_plan? %> <%= render 'dbs/followers' %> <% end %> -
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 -
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 -
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 -
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 -
class DbsController < ApplicationController def show @view_object = DbShowPresenter.new(find_db) end
private def find_db; end end -
class DbsController < ApplicationController def show @view_object = DbShowPresenter.new(find_db) end
private def find_db; end end -
<% if @view_object.show_followers? %> <li> <a href="#followers" class="followers">Followers</a> </li> <%
end %> <!-- ... --> <%= render 'dbs/followers' if @view_object.show_followers? %> -
<% if @view_object.show_followers? %> <li> <a href="#followers" class="followers">Followers</a> </li> <%
end %> <!-- ... --> <%= render 'dbs/followers' if @view_object.show_followers? %> -
EXTRAIA A LÓGICA DOS CONTROLLERS O controller deve ser apenas
uma camada de input e output. C -
CONTROLLER Não deve saber sobre a regra de negócio REGRA
DE NEGÓCIO Não deve saber sobre o controller -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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. -
ATUALIZAÇÃO DE DEPENDÊNCIAS Manter suas dependências atualizadas é uma questão
de sobrevivência. É isso, ou deixar seu projeto apodrecer. -
USE SEMPRE A ÚLTIMA VERSÃO DO RAILS Atualizar o framework
não deve ser algo complicado e que despenda muito tempo. -
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 -
USE A ÚLTIMA VERSÃO ESTÁVEL DO RUBY Correções de falhas
de segurança e bugs, novas funcionalidades. -
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 -
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? -
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. -
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 -
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 -
EVITE JANELAS QUEBRADAS Sempre faça commit de códigos mais bem
escritos do que quando você fez o checkout. -
REFACTORING É SEMPRE MELHOR QUE REWRITE. Melhorar a qualidade de
seu projeto incrementalmente é a melhor saída. -
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. -
NÃO COMECE COM UMA ARQUITETURA COMPLICADA Se você ainda precisa
provar o seu negócio, terá que se adaptar rapidamente. 2. -
SUA EXPERIÊNCIA É MAIS IMPORTANTE Não é porque funcionou para
outras pessoas que irá funcionar para você. 3. -
PENSAR FORA DA CAIXA É ESSENCIAL O Rails se estabeleceu
por sugerir padrões, mas não fique preso a este pensamento. 4. -
BOM DESIGN É MELHOR QUE QUALQUER ARQUITETURA Não adianta extrair
seu código se no fim ele continua ruim. 5.