HE:labs

Postado por Mauro George em 28/01/2013

Extraindo a responsabilidade de Fat Models com o uso de Decorators no Rails

Desde o 15 minutes blog o rails tem evoluido bastante e novos conceitos foram introduzidos com o passar do tempo, um mantra que foi introduzido na comunidade foi o "Skinny Controller, Fat Model" que pode gerar uma confusão aos desenvolvedores novatos e alguns mais experientes também.

Fat model não necessariamente herda de ActiveRecord::Base

Quando lemos o mantra pela primeira vez podemos ficar tentados a fazer algo que seria mover nossas lógicas do controller para o model.

Primeiro temos o código de enfileirar o envio de email no controller.

 1 class PostsController < ApplicationController
 2 
 3   def create
 4     @post = Post.new(params[:post])
 5 
 6     if @post.save
 7       NotifyMailer.delay.notify(@post)
 8       redirect_to(@post, :notice => 'Post was successfully created.') }
 9     else
10       render :action => "new"
11     end
12   end
13 end

Refatorariamos para algo assim em nosso model Post.

1 class Post < ActiveRecord::Base
2   after_save :notify_users
3 
4   private
5 
6     def notify_users
7       NotifyMailer.delay.notify(self)
8     end
9 end

Com certeza melhor que a solução anterior, mas agora o nosso model está sabendo demais. Além de fazer o seu papel em tratar as informações do banco de dados, agora também enfileira emails toda vez que é salvo. Ou seja, temos um problema de alto acoplamento, a operação de salvar sempre ativa o enfileiramento de envio de emails e com isso violamos o SRP.

É aí que chegamos no ponto que fat models não necessariamente são classes que herdam de ActiveRecord::Base, pois como vimos isso nos gera os seguintes problemas:

  • alto acoplamento
  • testes mais lentos
  • falta de coesão

Definindo melhor as responsabilidades e assim deixando tudo mais claro

Para deixar as coisas mais claras e com as responsabilidade melhores definidas podemos criar um decorator.

 1 class PostNotifyUsers
 2 
 3   def initialize(post)
 4     @post = post
 5   end
 6 
 7   def save
 8     @post.save && notify_users
 9   end
10 
11   private
12 
13     def notify_users
14       NotifyMailer.delay.notify(@post)
15     end
16 end

No código acima criamos uma classe em Ruby pura, conhecida também como PORO (Plain Old Ruby Objects). E o papel desta classe é notificar os usuários de um post novo enfileirando o email. Com isso conseguimos:

  • Baixo acoplamento: Post lida com o banco de dados e PostNotifyUsers notifica os usuários. Cada um com sua responsabilidade.
  • Testes mais rápidos: Quando se cria um Post em seus testes ele não envia mais email
  • Alta coesão: Pois agora sabemos de tudo o que ocorre ao salvar um Post, ele apenas é salvo.

Veja agora como ficaria o nosso controller usando o decorator que acabamos de criar:

 1 class PostsController < ApplicationController
 2 
 3   def create
 4     @post = Post.new(params[:post])
 5 
 6     if PostNotifyUsers.new(@post).save
 7       redirect_to(@post, :notice => 'Post was successfully created.') }
 8     else
 9       render :action => "new"
10     end
11   end
12 end

Como pode ver ficou mais claro pois agora sei que nesta action eu salvo o post e notifico os usuários. Outra coisa legal é que o a nossa classe PostNotifyUsers quacks como User sendo assim podemos continuar usando o #save só que agora do PostNotifyUsers em nosso controller.

O Anézio falou recentemente sobre a extração de responsábilidades também no post Cuidados com Observers e callbacks.

O Rails way e os iniciantes no rails

O que é comum são as pessoas iniciarem no rails sem noção nenhuma de Ruby e às vezes até orientação a objetos. O que levam a seguir sempre a abordagem do conhecido Rails Way esquecendo(às vezes nem sabendo) que Ruby é uma linguagem orientada a objetos, e que assim pode resolver muito dos seus problemas.

Comentários
Included file post/disqus_thread.html not found in _includes directory