As regras de Sandi Metz para desenvolvedores

Este post é uma tradução livre em português do artigo Sandi Metz' Rules For Developers. Você pode ler a versão original aqui.

As regras:

  • Classes não devem ter mais que cem linhas de código.
  • Métodos não devem ter mais que cinco linhas de código.
  • Não defina mais que quatro parâmetros na assinatura de um método. Coleções também são parâmetros.
  • Controllers podem instanciar apenas um único objeto. Portanto, as views podem saber apenas sobre uma instância e elas devem enviar mensagens apenas para esses objetos (respeitar a lei de Demeter).

Quando quebrar as regras

Parafraseando Sandi, “Você deve quebrar essas regras somente se tiver um bom motivo ou se seu par permitir.” Seu par ou a pessoa que revisa seu código é a pessoa para quem você deve perguntar. Pense nisso como um regra básica. É imutável.

Cem linhas de código

Apesar do grande número de métodos privados que escrevemos, manter classes pequenas tem sido fácil. Isto nos força a relembra qual é a única responsabilidade da nossa classe e o que devemos extrair dela. Isto se aplica para os testes.

Certa vez, encontramos um arquivo de testes que nos ajudou a perceber que estávamos testando muitas funcionalidades. Dividimos o arquivo em alguns outros mais especializados. Isto também nos fez perceber que git diff não necessariamente nos mostra quando excedemos as cem linhas de código.

Cinco linhas por método

Colocar um limite de cinco linhas por método é a regra mais interessante. Concordamos que if e else são sempre linhas a serem consideradas. Em um bloco condicional, cada condição pode ter apenas uma linha. Por exemplo:

def validate_actor
  if actor_type == 'Group'
    user_must_belong_to_group
  elsif actor_type == 'User'
    user_must_be_the_same_as_actor
  end
end

Cinco linhas de código assegura que nunca devemos usar else com else if.

Tendo apenas uma linha para cada condição, nos incitou a usar bons nomes para métodos privados. Método privado é uma ótima documentação. Eles precisam de nomes claros, que nos forcem a pensar sobre o conteúdo do código que extraímos.

Quatro parâmetros por método

A regra de quatro parâmetros por método foi particularmente desafiante usando Rails, especialmente nas views. Helpers de view como link_to ou form_for podem acabar exigindo vários parâmetros para funcionar corretamente. Enquanto nos esforçamos para não passar muitos parâmetros, voltamos na estaca zero e simplesmente deixamos os parâmetros caso não encontremos uma maneira melhor.

Instancie somente um objeto no controller

Esta regra assustou a maioria antes de começarmos a pôr em prática. Muitas vezes, precisávamos de mais de uma alguma coisa em uma página. Por exemplo, uma página inicial precisa tanto de um feed RSS e um contador de notificação. Resolvemos isto usando o padrão Facade. Ficou assim:

# app/facades/dashboard.rb:

class Dashboard
  def initialize(user)
    @user = user
  end

  def new_status
    @new_status ||= Status.new
  end

  def statuses
    Status.for(user)
  end

  def notifications
    @notifications ||= user.notifications
  end

  private

  attr_reader :user
end
# app/controllers/dashboards_controller.rb:

class DashboardsController < ApplicationController
  before_filter :authorize

  def show
    @dashboard = Dashboard.new(current_user)
  end
end
# app/views/dashboards/show.html.erb:

<%= render 'profile' %>
<%= render 'groups', groups: @dashboard.group %>

<%= render 'statuses/form', status: @dashboard.new_status %>
<%= render 'statuses', statuses: @dashboard.statuses %>

A classe Dashboard forneceu uma interface comum para os objetos de usuários colaboradores e passamos o estado do objeto Dashboard para as views.

Nós não contamos as variáveis de instância no cache do controller. Usamos uma convenção de prefixo para variáveis não utilizadas com um underline a fim de deixar claro o que será usado em uma view:

def calculate
  @_result_of_expensive_calculation ||= SuperCalculator.get_started(thing)
end

Great success!

Concluímos recentemente nossa experiência com sucesso e publicamos os resultados em nossa newsletter de pesquisa. Incorporamos as regras em nosso guia de boas práticas.

Páginas estáticas com Jekyll no Heroku

O Jekyll é um gerenciador de páginas estáticas. Esta fantástica engine oferece uma estrutura para se preocupar somente com os honestos HTML e CSS. Porém, como servir este conteúdo estático usando Jekyll no Heroku?

Existem várias maneiras para isto. Porém, eu separei em cinco pequenas e simples etapas. Veja cada uma delas:

Gemfile

Seu arquivo Gemfile deve conter, pelo menos, as seguintes gems:

source "https://rubygems.org"
ruby "2.1.0"

gem "jekyll"
gem "rack-jekyll"
gem "unicorn"

Estas gems são importantes, pois seus conteúdos serão servidos como uma aplicação Rack e alguns processos do unicorn. Após criar ou modificar seu arquivo, não esqueça de executar o bundle.

Unicorn

Crie o arquivo unicorn.rb com o seguinte conteúdo:

worker_processes 3
timeout 30
preload_app true

Para que entenda melhor o que cada um dos itens representam:

  • worker_processes: Quantidade de processos do unicorn.
  • timeout: Reiniciar os processos do unicorn após 30 segundos sem resposta.
  • preload_app: Evitar a inicialização da aplicação após os processos do unicorn serem estabelecidos.

Procfile

Crie o arquivo Procfile contendo a seguinte instrução:

web: bundle exec unicorn -p $PORT -c ./unicorn.rb

Neste caso, o Heroku será sempre o responsável por ler esta instrução e executá-la no momento do deploy.

Rack

Crie o arquivo config.ru contendo o seguinte conteúdo:

require "bundler/setup"
Bundler.require(:default)

run Rack::Jekyll.new(:destination => "_site")

Este arquivo identifica uma aplicação Rack. Porém, informaremos o path para todo o conteúdo estático gerado. Por padrão, o path para a pasta _site já está definido, mas pode ser alterado.

Conclusão

Nesta etapa final, será necessário modificar o arquivo _config.yml para não considerar todos estes arquivos no processo de inicialização do seu site ou blog, sendo necessário apenas para o Heroku gerar o ambiente da sua aplicação.

exclude:
  - unicorn.rb
  - Procfile
  - Gemfile
  - Gemfile.lock
  - config.ru
  - vendor

Vale lembrar que tudo isto pode rodar em um único e gratuito dyno.

Modificadores de acesso em Ruby

Existem diversos conceitos usados na programação orientada a objetos e os modificadores de acesso são um deles. Basicamente, estamos falando destes: public, protected e private. Para alguns, o uso deles causam certa confusão.

Conceitos

Por padrão, todos os métodos de uma classe são public. Isto quer dizer que os métodos podem ser chamados em qualquer escopo da sua aplicação, diferentemente dos outros dois modificadores.

Quando definimos um método como private, assume-se que este método será acessível apenas no escopo local da classe em que foi definido. Porém, este mesmo método poderá ser usado em uma outra classe por herança. Veja um exemplo:

class Apartment
  private
  def price
    250.50
  end
end

class Kitchen < Apartment
  def total_price
    price + 95.50
  end
end

kitchen = Kitchen.new
kitchen.total_price
#=> 346.0

No entanto, o método Apartment#price não pertence ao self de Kitchen, ou seja, não pertence ao escopo dele. Veja outro exemplo:

class Kitchen < Apartment
  def total_price
    self.price + 95.50
  end
end

kitchen = Kitchen.new
kitchen.total_price
#=> private method `price' called.. (NoMethodError)

Um método private pertence apenas à classe em que foi definido. Este conceito é um pouco diferente do modificador protected. Qualquer método protegido definido em uma classe pertence, além dela mesma, a todas as subclasses da classe em questão. Veja um exemplo:

class Apartment
  protected
  def price
    250.50
  end
end

class Kitchen < Apartment
  def total_price
    self.price + 95.50
  end
end

kitchen = Kitchen.new
kitchen.total_price
#=> 346.0

Além disto, um método protected pode ser chamado, através de uma instância, dentro da própria classe ou de suas subclasses. Veja outro exemplo:

class Person
  def initialize(age)
    @age = age
  end

  def older?(another_person)
    @age > another_person.age
  end

  protected
  def age
    @age
  end
end

john = Person.new(20)
smith = Person.new(22)

john.older?(smith)
#=> false

Diferença entre include e extend

Quando falamos de módulos no Ruby, include e extend são usados para adicionar funcionalidades de um módulo em uma determinada classe. Basicamente, a diferença entre um e outro é:

  • include: Adicionar métodos a uma instância de classe.
  • extend: Adicionar métodos de classe em uma classe.

Exemplo rápido

Vamos considerar, por enquanto, o uso de include.

module Action
  def left
    "to left"
  end
end

class Car
  include Action
end

Car.new.left
#=> "to left"

Car.left
#=> undefined method `left' for Car:Class

Agora, considerando o uso de extend.

class Car
  extend Action
end

Car.new.left
#=> undefined method `left' for #Car:0x007f8d0106c058

Car.left
#=> "to left"

Outros detalhes

Todos os métodos que são adicionados em uma classe através de extend são públicos. É possível, também, adicionar funcionalidades de um módulo em um objeto qualquer através do método extend. No entanto, os métodos disponíveis no módulo serão adicionados somente a instância em questão.

car = Car.new
car.extend Action

car.left
#=> "to left"

car2 = Car.new
car2.left
#=> undefined method `left' for #Car:0x007fd3cc067b68

O método extend pode ser usado para adicionar determinadas funcionalidades em qualquer classe existente. Não é necessário dizer que isto é perigoso, certo?

Object.extend Action

Car.left
#=> "to left"

String.left
#=> "to left"

Array.left
#=> "to left"

Classes abertas e monkey patch

Em uma palestra do Guilherme Silveira e Ricardo Valeriano na RubyConf Brasil 2012, foi comentado que, para se tornar um melhor programador, é importante conhecer as entranhas da linguagem de programação usada.

Partindo desta ideia, é importante entender como são criados os objetos e como ocorrem determinados processos dentro de classes e objetos. O modelo de objetos do Ruby é composto por alguns tópicos. Nesta introdução, comento sobre classes abertas e monkey patch.

Classes abertas

Em um outro post, falei que quase tudo é objeto nesta linguagem. As classes desses objetos podem ser "abertas" em tempo de execução, inclusive aquelas da própria biblioteca padrão. Veja um exemplo simples:

class String
  def remove_numeros
    gsub /[0-9]\s+/
  end
end

Podemos usar este comportamento em objetos do tipo String. Veja o resultado:

"Estão faltando 2 copos no armário".remove_numeros
#=> "Estão faltando copos no armário"

A classe String não recebeu uma nova definição. A linguagem sabe quando existe uma classe definida e, caso exista, ela apenas "reabre" para adicionar ou modificar um comportamento.

Monkey patch

Seguindo o conceito de classes abertas, monkey patch é, em outras palavras, extender ou modificar comportamentos de métodos em classes existentes. Considerando que temos o seguinte comportamento quando mapeamos um objeto, a fim de obter o valor de um atributo qualquer.

class Symbol
  def to_proc
    proc { |target| target.public_send(self) }
  end
end

Com a implementação acima, o output padrão da linguagem será:

countries = [
  Country.new("Argentina"),
  Country.new("Brasil"),
  Country.new("Chile")
]

countries.map(&:capital)
#=> ["Buenos Aires", "Brasilia", "Santiago"]

Agora, usando monkey patch, podemos alterar o comportamento padrão deste método para que toda saída seja com letras maiúsculas. Veja a modificação:

class Symbol
  def to_proc
    proc { |target| target.public_send(self).upcase }
  end
end

Depois de aplicar as modificações, o output será este:

countries = [
  Country.new("Argentina"),
  Country.new("Brasil"),
  Country.new("Chile")
]

countries.map(&:capital)
#=> ["BUENOS AIRES", "BRASILIA", "SANTIAGO"]

Em toda sua aplicação, mapeamento de objetos usando o protocolo to_proc serão afetados pelo novo comportamento.

Por conta disto, muitos desenvolvedores consideram este "poder" perigoso. Visto que, alterar comportamentos de métodos da biblioteca padrão do Ruby não soa normal. Seu uso deve ter uma excelente justificativa!

Com a nova versão (2.0) é possivel aplicar monkey patching em um escopo bem definido. Leia mais sobre o recurso refinements.