Bom, esse post é resultado de uma conversa que tivemos no Grupo de Usuários de Ruby de SP. Mas, antes de entrar no que interessa, vamos divagar um pouco sobre “Model” e “Rails”.

Muitos programadores Rails sabem a regra “Controllers magros, Models gordos”. É interessante também saber um pouco sobre o porque dessa regra, mas antes disso, vamos discorrer sobre o que é o “Model” de Rails, comparando com o “Model” da maior parte dos frameworks Java (lembrando que eu não sou programador Java, se eu falar qualquer besteira, me corrijam).

Pegando por exemplo o Hibernate, normalmente há uma classe que mapeia um objeto para uma tabela, e outra classe que faz as buscas (chamada normalmente de Facade). Então, teríamos um diagrama como: JDBC -> Model -> Facade. Já no caso do Rails, o mapeador ActiveRecord já abstrai a parte de “ter que mapear um objeto para uma tabela”, e também já nos oferece formas de buscar esses registros. Resultado, que o “Model” do Rails é meio que uma junção de “Model” e “Facade” do Java, e isso sozinho. Parece então óbvio que regras de negócio vão para ele, não é? Senão, qual o uso de uma classe vazia?

Bom, minha abordagem não é bem essa. E para isso, eu uso o princípio das CRC Cards, da metodologia XP.

CRC – Class, Responsability, Collaboration, é uma forma bem simples de identificar entidades e classes num projeto. Como exemplo, uma classe “Pessoa”:

Classe: Pessoa
Responsabilidade: Representar uma pessoa válida no banco de dados
Colaboração: vamos deixar de fora por enquanto

Digamos agora que eu preciso saber o salário líquido daquela pessoa. É prática comum, infelizmente, a partir do campo (atributo) “salario”, fazer todos os cálculos de imposto, descontos, e então dar o resultado. Mas isso está certo? Olhando para a “responsabilidade” da classe Pessoa, ela deveria “Representar uma pessoa válida no banco de dados”. A responsabilidade da classe são, então, validações, e assegurar que os dados que ela vai retornar são representações de parte daquela pessoa (como exemplo, um método “nome_completo”, que concatena o “nome” e “sobrenome”).

Minha abordagem é a seguinte: No Rails 2, no arquivo environment.rb, tem uma linha:

  # config.load_paths += %W( #{RAILS_ROOT}/extras )

Eu descomento essa linha e adiciono uma lista de diretórios para minhas regras de negócio:

  config.load_paths += ["#{RAILS_ROOT}/app/calculos"]

No rails 3, é necessário adicionar (no caso do meu aplicativo chamar-se Exemplo):

Exemplo::Application.configure do
  config.autoload_paths += ["#{RAILS_ROOT}/app/calculos"]
end

Agora, no diretório “app/calculos”, eu crio um arquivo como “salarios.rb”:

class Salarios
  def initialize(pessoa)
  end
  
  def salario_liquido
  end

  def impostos
  end
end

Enfim, com todas as regras que eu precisaria. Agora, para um exemplo mais real: uma vez, tive que montar um sistema que as regras de validação de um cadastro eram variáveis. Para não expor código que pode estar protegido, vamos inventar aqui uma regra arbitrária: administradores podem alterar todos os dados de uma pessoa. Porém, se o usuário NÃO FOR um administrador, aí ele não pode diminuir o salário de uma pessoa, somente aumentar. O que impacta aqui é o “update” do controller. Primeiramente, vamos fazer uma pequena alteração no controller:

PeopleController < ApplicationController
  def update
    @person_updater = PersonUpdaterRule.return_rule_for current_user, params[:person_id]
    @person = @person_updater.person
    if @person_updater.update_attributes(params[:person])
      redirect_to @person
    else
      render :action => 'edit'
    end
  end
end

Retirei algumas coisas para ficar mais breve. A idéia aqui é que, ao invés de listar os erros do @person, listamos os erros do @person_updater. No rails 2, é possível fazer isso com o helper “error_messages_for :person_updater”. No rails 3, é necessário fazer isso manualmente, infelizmente. Outra possibilidade é o @person ser o próprio PersonUpdater, e delegar todos os métodos desconhecidos (name, age, salary, em resumo, qualquer método que pertença ao model) para o lugar correto, nesse caso o model Person.

Agora, a implementação. Uma possível implementação dos Updaters é:

#arquivo app/rules/person_updater_rule.rb
module PersonUpdaterRule
  def self.return_rule_for(current_user, person_id)
    #Para os que gostam de design pattern, isso é uma Factory :)
    if current_user.admin?
      PersonUpdaterRule::Admin.new person_id
    else
      PersonUpdaterRule::NonAdmin.new person_id
    end
  end
end

#arquivo app/rules/person_updater_rule/admin_rule.rb
module PersonUpdaterRule
  class AdminRule
    include Validatable #se for rails 2. Instalar a gem durran-validatable
    include ActiveModel::Validations #se for rails 3
    validate :valid_person
    attr_reader :person

    def initialize(person_id)
      @person = Person.find person_id
    end

    #Validações padrão da pessoa relacionada a esse modelo
    def valid_person
      @person.valid?
      @person.errors.each { |k, v| errors.add(k, v) }
    end
    private :valid_person

    def update_attributes(params = {})
      return false unless valid?
      @person.update_attributes(params)
    end
  end
end

#arquivo app/rules/person_updater_rule/non_admin_rule.rb
module PersonUpdaterRule
  class NonAdminRule < AdminRule #Herdando as regras de validação do Admin.
    validate :salary_not_lower

    def salary_not_lower
      before, after = @person.changes['salary']
      return if before.nil?
      errors.add(:salary, "can't lower") if(before.to_i > after.to_i)
    end
    private :salary_not_lower
  end
end

A vantagem dessa abordagem é separar, claramente, as validações específicas de cada situação. Esse tipo de validação não tem sentido ser implementado no modelo; afinal, uma pessoa vãlida precisa ter um salário, e essa é a única regra que pertence ao registro. Da mesma forma, não faz sentido ter que passar, sempre que for atualizar o registro, qual o usuário logado atualmente no sistema. Finalmente, essa é uma regra de negócio que pode ser alterada com o tempo, e é ideal que ela esteja separada do resto das regras para que possa ser facilmente atacada quando necessário.

Por fim, mais para frente escreverei um post sobre “Duck Typing” que talvez explique um pouco mais o código acima. Mas a idéia é, SEMPRE, evitar concentrar lógica demais num lugar só. Fica bem mais desacoplado, faz mais sentido e evita o God Anti-Pattern, ou seja, um “super-objeto” que faz tudo.


2 Comments

Diego Chohfi · 2011-08-04 at 10:21

Opa Mauricio, parabéns pelo post cara! Legal ver como implementar design patterns em ruby.

Eu trocaria a variável de instância @person_updater para uma variável no escopo do método no controller.

    Maurício Szabo · 2011-08-04 at 11:07

    Na verdade, eu também não faria isso, eu faria o PersonUpdater delegar todos os métodos desconhecidos para o modelo Person, e nem sequer usaria o @person_updater. Mas usar uma variável no escopo não é possível porque, nesse caso, eu perco a exibição dos erros na view (com o error_messages_for :person_updater, por exemplo). Abraços!

Leave a Reply

Your email address will not be published. Required fields are marked *