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 <pre><code>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 </code></pre> end end #arquivo app/rules/person_updater_rule/non_admin_rule.rb module PersonUpdaterRule class NonAdminRule &lt; AdminRule #Herdando as regras de validação do Admin. validate :salary_not_lower <pre><code>def salary_not_lower before, after = @person.changes['salary'] return if before.nil? errors.add(:salary, &amp;quot;can't lower&amp;quot;) if(before.to_i &amp;gt; after.to_i) end private :salary_not_lower </code></pre> 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!
Comments are closed.