Utilizando Concerns em Rails

Publicado por Leonardo Risch no dia dev

Distribuição de responsabilidades

Aqui na Resultados Digitais, trabalhamos com diversas integrações entre sistemas. Com isso alguns modelos podem ficar muito complexos, com muitas funções exclusivas de cada integração, dificultando a manutenibilidade do sistema.

O uso de Concerns se aplica quando queremos separar o contexto das funções dentro de Modelos. Foi essa estratégia que resolvemos adotar para reduzir a complexidade. Acompanhe a seguir.

O que é Concern?

Concern é um módulo utilitário do ActiveSupport, suportado pelas versões 3 e 4 do Rails e que permite e facilita o compartilhamento de código entre modelos. Além disso, é possível separar diversas responsabilidades de um Modelo, permitindo assim filtrar funções que são exclusivamente de um contexto do modelo em um Concern e importá-las.

Vamos exemplificar um caso onde temos um blog post e este post pode conter diversos votos (positivos ou negativos) e comentários. Os comentários também podem receber votos (positivos ou negativos).

A modelagem desse sistema ficaria basicamente dessa forma:

1
2
3
4
5
6
7
8
# models/post.rb

class Post < ActiveRecord::Base
  validates :title, :content, presence: true

  has_many :votes
  has_many :comments
end
1
2
3
4
5
6
7
8
# models/comment.rb

class Comment < ActiveRecord::Base
  validates :content, presence: :true

  has_many   :votes
  belongs_to :post
end
1
2
3
4
5
# models/vote.rb

class Vote < ActiveRecord::Base
  enum vote_type: [ :upvote, :downvote ]
end

Neste exemplo fica claro que caso quisermos ter uma função que compute um voto para o comentário ou post, teríamos que criar uma função similar entre o modelo Post e o modelo Comment.

A partir dessa ideia, podemos criar um módulo chamado Votable que possui funções que serão executadas e compartilhadas pelos dois modelos.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# models/concern/votable.rb

module Votable
  extend ActiveSupport::Concern

  included do
    has_many :votes
  end

  def vote!
    votes.create
  end

  def downvote!
    votes.create(vote_type: :downvote)
  end

  def upvote!
    votes.create(vote_type: :upvote)
  end
end

Inserimos uma função genérica chamada vote! e duas outras funções para caso o voto seja específico: upvote! para votos positivos e downvote! para negativos.

E agora? Como conseguimos incorporar o Concern que acabamos de criar para os diversos modelos utilizados?

Basta incluir include Votable em cada modelo pretendido.

Por exemplo, para ser permitido o Post utilizar a função vote!, ficaria assim:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# models/post.rb


class Post < ActiveRecord::Base
  include Votable
  validates :title, :content, presence: true

  has_many :comments
end

# models/comment.rb

class Comment < ActiveRecord::Base
  include Votable
  validates :content, presence: :true

  belongs_to :post
end

Com isso, se entrarmos no console, já conseguimos reproduzir o caso de uso.

1
2
3
irb(main):003:0> post = Post.create(title: "Concern", content: "Post")
=> #<Post id: 1, title: "Concern", content: "Post", timestamp: nil>
irb(main):004:0> post.vote!

É gerado um voto:

1
2
irb(main):005:0> post.votes.last
=> #<Vote id: 1, vote_type: nil, created_at: "2015-11-20 13:44:07", updated_at: "2015-11-20 13:44:07", post_id: 1, comment_id: nil>

Do mesmo modo funciona para o modelo Comment:

1
2
3
4
irb(main):007:0> comment = Comment.create(content: "Concern", post_id: 1)
=> #<Comment id: 1, post_id: 1, created_at: "2015-11-20 14:10:31", updated_at: "2015-11-20 14:10:31", content: "Concern">
irb(main):008:0> comment.vote!
=> #<Vote id: 2, vote_type: nil, created_at: "2015-11-20 14:48:25", updated_at: "2015-11-20 14:48:25", post_id: nil, comment_id: 1>

Sucesso!

Outro exemplo de utilização seria se não tivéssemos comentários, porém muitas funções relacionadas a voto no modelo Post. Nesse caso, poderíamos colocar essas também em um Concern e importá-las para o modelo.

Pontos negativos

Utilizar a técnica de Concern pode trazer alguns malefícios. Por exemplo:

  • Iniciantes em projetos não compreendem o conceito facilmente, deixando a modelagem ainda mais confusa.
  • Se usado de forma errada, o código pode ficar confuso e de difícil manutenibilidade.
  • Confusão no conceito de como a função trabalha para mais de um modelo.
  • A má utilização pode adicionar complexidade desnecessária ao modelo - ao invés de retirá-la.

Lembre-se que existem outros casos de uso para o Concern. Estude bem a técnica para identificar se é a melhor solução para o seu contexto, sempre levando em conta seus pontos negativos.

Conclusão

Concern é um módulo muito interessante, principalmente para separar responsabilidades de classes complexas ou para compartilhar código entre modelos.

Neste post explicamos como utilizar Concern, exemplificando este módulo que foi útil para o nosso projeto em determinado contexto e pode ser interessante para o seu projeto.

Porém, é necessário usá-lo conscientemente e entender o porquê de estar sendo aplicado ao projeto, pois utilizá-lo de maneira errada adicionará mais complexidade ao projeto, dificultando o entendimento do sistema para pessoas que estão começando a mexer no projeto.

Você tem alguma experiência interessante utilizando Concerns? Compartilhe nos comentários abaixo!

Referências

Leonardo Risch

Leonardo Risch

Full Stack Developer

Comentários