« HE:labs
HE:labs

Extraindo responsabilidade de um classe ruby utilizando polimorfismo

Postado por Mauro George em 06/06/2013

Já falei anteriormente um pouco sobre o uso do pattern decorator no rails neste link. Em algum momento a lógica pode ser tão complexa que quebraríamos os decorators em decorators especializados.

Vamos a um exemplo: dado um PostDecorator com apenas um método público show, responsável por exibir um post, podemos reduzir a sua complexidade e manter a responsabilidade única de cada classe, baseando-se no tipo de status. Mas como?

 1 class PostDecorator
 2 
 3   def initialize(post)
 4     @post = post
 5   end
 6 
 7   def show
 8     if post.status == "draft"
 9       # ... lógica de draft aqui
10     elsif post.status == 'published'
11       # ... lógica de published aqui
12     end
13   end
14 
15   private
16 
17     attr_reader :post
18 end

O primeiro passo que poderíamos fazer seria extrair a lógica de cada um dos status para classes especializadas, delegando as responsabilidades. Exemplo:

 1 class PostDecorator
 2 
 3   def initialize(post)
 4     @post = post
 5   end
 6 
 7   def show
 8     if post.status == "draft"
 9       PostDraftDecorator.new(post).show
10     elsif post.status == 'published'
11       PostPublishedDecorator.new(post).show
12     end
13   end
14 
15   private
16 
17     attr_reader :post
18 end
19 
20 class PostDraftDecorator
21 # ... lógica de draft aqui
22 end
23 
24 class PostPublishedDecorator
25 # ... lógica de published aqui
26 end

Agora ficou melhor, já que a responsabilidade foi dividida.

Um outro pequeno refactory que poderíamos fazer é melhorar essa verificação de if post.status == "draft". Podendo implementar em nosso model Post#draft? e Post#published?.

E assim, encapsulamos a lógica de #draft? e #published?, pois se a complexidade aumentar, teremos que mudar N contextos que fazem a verificação de tal lógica. Como agora elas estão encapsuladas, caso a lógica mude, teremos que alterar apenas no model.

 1 class PostDecorator
 2 
 3   def initialize(post)
 4     @post = post
 5   end
 6 
 7   def show
 8     if post.draft?
 9       PostDraftDecorator.new(post).show
10     elsif post.published?
11       PostPublishedDecorator.new(post).show
12     end
13   end
14 
15   private
16 
17     attr_reader :post
18 end

Para evitarmos criar todos estes métodos na mão, podemos utilizar a gem Jacaranda que foi a minha primeira gem ;)

Mas ainda não é o melhor que podemos fazer para este caso. Vamos agora ao uso de polimorfismo:

Resolvendo com o uso de polimorfismo

Podemos usar polimorfismo para, dependendo do status do post, instanciar e utilizar o decorator correto.

 1 class PostDecorator
 2 
 3   def initialize(post)
 4     @post = post
 5   end
 6 
 7   def show
 8     post_decorator.new(post).show
 9   end
10 
11   private
12 
13     attr_reader :post
14 
15     def post_decorator
16       "Post#{post.status.capitalize}Decorator".constantize
17     end
18 end

Como pode-se notar, criamos o método post_decorator que retorna a classe correta. Em seguida, instanciamos ela e chamamos o método show na classe específica. Caso não seja de seu conhecimento, utilizamos o constantize do ActiveSupport que nos retorna uma constante de mesmo nome baseado na string recebida.

Agora vamos aos testes do nosso PostDecorator:

 1 describe PostDecorator do
 2 
 3   let(:post_decorator) do
 4     PostDecorator.new(post)
 5   end
 6 
 7   describe "#show" do
 8 
 9     context "when post is a draft" do
10 
11       let!(:post) do
12         Post.create(status: "draft")
13       end
14 
15       it "call PostDraftDecorator" do
16         post_draft_decorator = double("PostDraftDecorator")
17         PostDraftDecorator.should_receive(:new).with(post).and_return(post_draft_decorator)
18         post_draft_decorator.should_receive(:show)
19         post_decorator.show
20       end
21     end
22 
23     context "when post is published" do
24 
25       let!(:post) do
26         Post.create(status: "published")
27       end
28 
29       it "call PostPublishedDecorator" do
30         post_published_decorator = double("PostPublishedDecorator")
31         PostPublishedDecorator.should_receive(:new).with(post).and_return(post_published_decorator)
32         post_published_decorator.should_receive(:show)
33         post_decorator.show
34       end
35     end
36   end
37 end

Como visualiza-se acima, nossos testes ficaram bem simples. Testamos apenas se a delegação foi feita corretamente, pois a implementação seria testada unitariamente em cada uma das classes especializadas (PostDraftDecorator e PostPublishedDecorator).

Conclusão

Utilizando polimorfismo, agora nossa classe PostDecorator pode instanciar outros decorators e não precisaremos mais dos ifs e elses. Apenas crie uma nova classe como PostUnpublishedDecorator e todos os posts com o status unpublished usarão esta nova classe, pois segue-se a convenção.

Compartilhe

Sabia que nosso blog agora está no Medium? Confira Aqui!