Aprenda / Ruby on Rails

IntermediárioAula 11 de 2015 min0% da trilha

Concerns e composição

Herança força uma hierarquia rígida — concern deixa você compartilhar comportamento entre models sem isso, desde que não vire uma gaveta de bagunça.

Produto e Pedido não têm nenhuma relação de "é um" entre si — mas os dois precisam poder ser arquivados sem sair do banco (um campo arquivado, um scope pra listar só os ativos, um método pra marcar como arquivado). Copiar esse código nos dois models é duplicação; criar uma superclasse comum só pra compartilhar isso força uma hierarquia que não faz sentido semântico. Um concern resolve exatamente esse caso: comportamento compartilhado sem herança.

Um concern é um módulo com uma DSL pra rodar código na inclusão

app/models/concerns/arquivavel.rb
module Arquivavel
  extend ActiveSupport::Concern

  included do
    scope :ativos, -> { where(arquivado: false) }
    scope :arquivados, -> { where(arquivado: true) }
  end

  def arquivar!
    update!(arquivado: true)
  end
end

ActiveSupport::Concern é o que dá acesso ao bloco included do ... end: tudo dentro dele roda no contexto da classe que inclui o módulo, e é por isso que scope — que só existe em classe ActiveRecord — funciona ali dentro, mesmo estando escrito num módulo solto.

incluindo o concern nos dois models
class Produto < ApplicationRecord
  include Arquivavel
end

class Pedido < ApplicationRecord
  include Arquivavel
end

Produto.ativos          # só os não arquivados
produto.arquivar!        # marca como arquivado, sem duplicar lógica em nenhum dos dois models

Controller também usa concern — não é uma ferramenta exclusiva de model. Um app/controllers/concerns/paginavel.rb compartilhando a mesma lógica de paginação entre ProdutosController e PedidosController segue a mesma ideia: comportamento comum, sem repetir código, sem forçar os dois controllers a herdarem de uma terceira classe intermediária.

Quando um concern vira gaveta de bagunça

O anti-padrão mais comum não é usar concern demais — é criar um concern genérico (Helpers, Util, Shared) e ir empilhando ali qualquer método que não tem lugar óbvio, sem relação lógica entre um método e outro. Isso é pior do que não ter concern nenhum: agora o comportamento de um model está espalhado num arquivo que ninguém sabe o que faz só de olhar o nome.

O teste prático: um concern deveria ter um nome que descreve uma capacidade coesa — Arquivavel, Pesquisavel, Notificavel. Se você não consegue dar ao concern um nome assim, sem ser um substantivo genérico, é sinal de que ele deveria virar duas classes separadas, ou nem existir.

Exercício

Produto e Categoria têm uma coluna nome em comum. Escreva um concern Pesquisavel que adiciona um método de classe buscar_por_termo(termo), usando where com placeholder (nunca interpolando o termo direto na query).

Quiz rápido

Qual o principal motivo pra extrair um comportamento repetido em Produto e Pedido pra um concern, em vez de criar uma superclasse comum da qual os dois herdam?
O que caracteriza um concern que virou 'gaveta de bagunça' (anti-padrão)?