Como usar preocupações no Rails 4

628

O gerador de projeto padrão do Rails 4 agora cria o diretório "preocupações" em controladores e modelos. Encontrei algumas explicações sobre como usar preocupações de roteamento, mas nada sobre controladores ou modelos.

Tenho certeza de que tem a ver com a atual "tendência DCI" na comunidade e gostaria de experimentá-la.

A questão é: como devo usar esse recurso? Existe uma convenção sobre como definir a hierarquia de nomeação / classe para fazê-lo funcionar? Como posso incluir uma preocupação em um modelo ou controlador?

yagooar
fonte

Respostas:

617

Então eu descobri sozinho. Na verdade, é um conceito bastante simples, mas poderoso. Tem a ver com a reutilização de código, como no exemplo abaixo. Basicamente, a idéia é extrair pedaços de código comuns e / ou específicos do contexto para limpar os modelos e evitar que fiquem muito gordos e sujos.

Como exemplo, vou colocar um padrão bem conhecido, o padrão taggable:

# app/models/product.rb
class Product
  include Taggable

  ...
end

# app/models/concerns/taggable.rb
# notice that the file name has to match the module name 
# (applying Rails conventions for autoloading)
module Taggable
  extend ActiveSupport::Concern

  included do
    has_many :taggings, as: :taggable
    has_many :tags, through: :taggings

    class_attribute :tag_limit
  end

  def tags_string
    tags.map(&:name).join(', ')
  end

  def tags_string=(tag_string)
    tag_names = tag_string.to_s.split(', ')

    tag_names.each do |tag_name|
      tags.build(name: tag_name)
    end
  end

  # methods defined here are going to extend the class, not the instance of it
  module ClassMethods

    def tag_limit(value)
      self.tag_limit_value = value
    end

  end

end

Portanto, seguindo a amostra do produto, você pode adicionar o Taggable a qualquer classe que desejar e compartilhar sua funcionalidade.

Isso é muito bem explicado pelo DHH :

No Rails 4, convidamos os programadores a usar preocupações com os diretórios padrão app / models / preocupações e app / controllers / preocupações que fazem parte automaticamente do caminho de carregamento. Juntamente com o invólucro ActiveSupport :: Concern, é apenas o suporte suficiente para fazer brilhar esse mecanismo leve de fatoração.

yagooar
fonte
11
O DCI lida com um Contexto, usa Funções como identificadores para mapear um modelo / caso de uso mental para codificar e não requer o uso de wrappers (os métodos são vinculados diretamente ao objeto no tempo de execução), portanto, isso não tem nada a ver com o DCI.
Ciscoheat
2
@yagooar, mesmo incluindo-o em tempo de execução, não o tornaria DCI. Se você deseja ver um exemplo de implementação Ruby DCI. Dê uma olhada no fulloo.info ou nos exemplos em github.com/runefs/Moby ou em como usar o maroon para fazer o DCI no Ruby e o que é o DCI runefs.com (O que é o DCI. apenas iniciado recentemente)
Rune FS
1
@RuneFS && ciscoheat vocês dois estavam certos. Acabei de analisar os artigos e fatos novamente. E, fui no fim de semana passado para uma conferência em Ruby, onde uma conversa era sobre a DCI e, finalmente, entendi um pouco mais sobre sua filosofia. O texto foi alterado para não mencionar o DCI.
yagooar
9
Vale mencionar (e provavelmente incluindo em um exemplo) que os métodos de classe devem ser definidos em um módulo chamado ClassMethods especialmente designado, e que esse módulo é estendido pela classe base também seja ActiveSupport :: Concern.
Febeling 17/09/2013
1
Obrigado por este exemplo, principalmente b / c eu estava sendo burra e definindo os meus métodos de nível de classe dentro dos ClassMethods módulo com self.whatever ainda, e isso não faz P trabalho =
Crews Ryan
379

Eu tenho lido sobre o uso de preocupações com modelos para modelar modelos de gordura, bem como secar os códigos de seus modelos. Aqui está uma explicação com exemplos:

1) SECAGEM de códigos de modelo

Considere um modelo de artigo, um modelo de evento e um modelo de comentário. Um artigo ou evento tem muitos comentários. Um comentário pertence ao Artigo ou Evento.

Tradicionalmente, os modelos podem ser assim:

Modelo de comentário:

class Comment < ActiveRecord::Base
  belongs_to :commentable, polymorphic: true
end

Artigo Modelo:

class Article < ActiveRecord::Base
  has_many :comments, as: :commentable 

  def find_first_comment
    comments.first(created_at DESC)
  end

  def self.least_commented
   #return the article with least number of comments
  end
end

Modelo de Evento

class Event < ActiveRecord::Base
  has_many :comments, as: :commentable 

  def find_first_comment
    comments.first(created_at DESC)
  end

  def self.least_commented
   #returns the event with least number of comments
  end
end

Como podemos observar, há um pedaço significativo de código comum ao Evento e ao Artigo. Usando preocupações, podemos extrair esse código comum em um módulo separado.

Para isso, crie um arquivo commentable.rb em app / models / preocupações.

module Commentable
  extend ActiveSupport::Concern

  included do
    has_many :comments, as: :commentable
  end

  # for the given article/event returns the first comment
  def find_first_comment
    comments.first(created_at DESC)
  end

  module ClassMethods
    def least_commented
      #returns the article/event which has the least number of comments
    end
  end
end

E agora seus modelos ficam assim:

Modelo de comentário:

class Comment < ActiveRecord::Base
  belongs_to :commentable, polymorphic: true
end

Artigo Modelo:

class Article < ActiveRecord::Base
  include Commentable
end

Modelo de Evento:

class Event < ActiveRecord::Base
  include Commentable
end

2) Modelos de gordura que desnaturam a pele.

Considere um modelo de evento. Um evento tem muitos participantes e comentários.

Normalmente, o modelo de evento pode ter esta aparência

class Event < ActiveRecord::Base   
  has_many :comments
  has_many :attenders


  def find_first_comment
    # for the given article/event returns the first comment
  end

  def find_comments_with_word(word)
    # for the given event returns an array of comments which contain the given word
  end 

  def self.least_commented
    # finds the event which has the least number of comments
  end

  def self.most_attended
    # returns the event with most number of attendes
  end

  def has_attendee(attendee_id)
    # returns true if the event has the mentioned attendee
  end
end

Modelos com muitas associações e de outra forma tendem a acumular cada vez mais códigos e se tornam incontroláveis. As preocupações fornecem uma maneira de reduzir o tamanho dos módulos de gordura, tornando-os mais modulares e fáceis de entender.

O modelo acima pode ser refatorado usando as preocupações conforme abaixo: Crie um arquivo attendable.rbe commentable.rbna pasta app / models / preocupações / evento

Attable.rb

module Attendable
  extend ActiveSupport::Concern

  included do 
    has_many :attenders
  end

  def has_attender(attender_id)
    # returns true if the event has the mentioned attendee
  end

  module ClassMethods
    def most_attended
      # returns the event with most number of attendes
    end
  end
end

commentable.rb

module Commentable
  extend ActiveSupport::Concern

  included do 
    has_many :comments
  end

  def find_first_comment
    # for the given article/event returns the first comment
  end

  def find_comments_with_word(word)
    # for the given event returns an array of comments which contain the given word
  end

  module ClassMethods
    def least_commented
      # finds the event which has the least number of comments
    end
  end
end

E agora, usando o Preocupações, seu modelo de evento se reduz a

class Event < ActiveRecord::Base
  include Commentable
  include Attendable
end

* Enquanto estiver usando preocupações, é aconselhável optar pelo agrupamento baseado em 'domínio' em vez de agrupamento 'técnico'. O agrupamento baseado em domínio é como 'Comentável', 'Fotoable', 'Attendable'. Agrupamento técnico significa 'ValidationMethods', 'FinderMethods' etc.

Aaditi Jain
fonte
6
Portanto, as preocupações são apenas uma maneira de usar herança ou interfaces ou herança múltipla? O que há de errado em criar uma classe base comum e subclassificar a partir dessa classe base comum?
Chloe
3
Na verdade @Chloe, I alguns onde vermelho, uma aplicação Rails com um diretório 'preocupações' é na verdade uma 'preocupação' ...
Ziyan Junaideen
Você pode usar o bloco 'incluído' para definir todos os seus métodos e inclui: métodos de classe (com def self.my_class_method), métodos de instância e chamadas e diretrizes de método no escopo da classe. Não há necessidade demodule ClassMethods
Um Fader Darkly
1
O problema que tenho com preocupações é que elas adicionam funcionalidade diretamente ao modelo. Portanto, se duas preocupações implementarem add_item, por exemplo, você está ferrado. Lembro-me de pensar que o Rails estava quebrado quando alguns validadores pararam de funcionar, mas alguém havia implementado any?uma preocupação. Proponho uma solução diferente: use a preocupação como uma interface em um idioma diferente. Em vez de definir a funcionalidade, define a referência a uma instância de classe separada que lida com essa funcionalidade. Então você tem menores, as classes mais puro que fazer uma coisa ...
A Fader Darkly
@aaditi_jain: Corrija pequenas alterações para evitar equívocos. ou seja, "Crie um arquivo attendable.rd e commentable.rb na pasta app / models / preocupações / evento" -> Attendable.rd deve ser Attendable.rb Obrigado
Rubyist
97

Vale ressaltar que o uso de preocupações é considerado ruim por muitos.

  1. como esse cara
  2. e este

Algumas razões:

  1. Há alguma magia negra acontecendo nos bastidores - a preocupação é o includemétodo de correção , existe todo um sistema de manipulação de dependências - muita complexidade para algo que é trivial e bom e velho padrão de mixagem Ruby.
  2. Suas aulas não são menos secas. Se você colocar 50 métodos públicos em vários módulos e incluí-los, sua classe ainda terá 50 métodos públicos, apenas oculte o cheiro do código e coloque seu lixo nas gavetas.
  3. A base de código é realmente mais difícil de navegar com todas essas preocupações.
  4. Você tem certeza de que todos os membros de sua equipe têm o mesmo entendimento do que realmente deve substituir a preocupação?

As preocupações são uma maneira fácil de dar um tiro na perna, tenha cuidado com elas.

Dr.Strangelove
fonte
1
Sei que SO não é o melhor lugar para essa discussão, mas que outro tipo de mix de Ruby mantém suas aulas secas? Parece que as razões 1 e 2 dos seus argumentos são contrárias, a menos que você esteja apenas defendendo um melhor design de OO, a camada de serviços ou outra coisa que esteja faltando? (Eu não discordo - Estou sugerindo alternativas acrescentando ajuda!)
toobulkeh
2
O uso de github.com/AndyObtiva/super_module é uma opção, o uso de bons e velhos padrões ClassMethods é outra. E usar mais objetos (como serviços) para separar preocupações de maneira limpa é definitivamente o caminho a percorrer.
Dr.Strangelove
4
Voto negativo, porque essa não é uma resposta para a pergunta. É uma opinião. É uma opinião que tenho certeza que tem seus méritos, mas não deve ser uma resposta para uma pergunta no StackOverflow.
Adam
2
@ Adam É uma resposta opinativa. Imagine que alguém perguntaria como usar variáveis ​​globais em trilhos, certamente mencionaria que existem maneiras melhores de fazer as coisas (por exemplo, Redis.current vs $ redis) poderiam ser informações úteis para iniciantes em tópicos? O desenvolvimento de software é inerentemente uma disciplina opinativa, não há como contornar isso. Na verdade, eu vejo opiniões como respostas e discussões, que são as melhores respostas o tempo todo no stackoverflow, e isso é bom
Dr.Strangelove
2
Claro, mencionar isso junto com sua resposta à pergunta parece bom. Nada na sua resposta realmente responde à pergunta do OP. Se tudo o que você deseja fazer é avisar alguém por que eles não devem usar preocupações ou variáveis ​​globais, isso seria um bom comentário que você poderia adicionar à pergunta deles, mas isso realmente não seria uma boa resposta.
Adam
56

Este post me ajudou a entender as preocupações.

# app/models/trader.rb
class Trader
  include Shared::Schedule
end

# app/models/concerns/shared/schedule.rb
module Shared::Schedule
  extend ActiveSupport::Concern
  ...
end
aminhotob
fonte
1
esta resposta não explica nada.
46

Senti que a maioria dos exemplos aqui demonstrou o poder modulee não como ActiveSupport::Concernagrega valor aomodule .

Exemplo 1: Módulos mais legíveis.

Portanto, sem preocupações, isso moduleserá típico .

module M
  def self.included(base)
    base.extend ClassMethods
    base.class_eval do
      scope :disabled, -> { where(disabled: true) }
    end
  end

  def instance_method
    ...
  end

  module ClassMethods
    ...
  end
end

Após refatorar com ActiveSupport::Concern.

require 'active_support/concern'

module M
  extend ActiveSupport::Concern

  included do
    scope :disabled, -> { where(disabled: true) }
  end

  class_methods do
    ...
  end

  def instance_method
    ...
  end
end

Você vê métodos de instância, métodos de classe e bloco incluído são menos confusos. As preocupações os injetarão adequadamente para você. Essa é uma vantagem do uso ActiveSupport::Concern.


Exemplo 2: Manipule as dependências do módulo normalmente.

module Foo
  def self.included(base)
    base.class_eval do
      def self.method_injected_by_foo_to_host_klass
        ...
      end
    end
  end
end

module Bar
  def self.included(base)
    base.method_injected_by_foo_to_host_klass
  end
end

class Host
  include Foo # We need to include this dependency for Bar
  include Bar # Bar is the module that Host really needs
end

Neste exemplo Baré o módulo que Hostrealmente precisa. Mas como a Bardependência Fooda Hostclasse tem que ser include Foo(mas espere, por que você Hostquer saber Foo? Isso pode ser evitado?).

Então, Baradiciona dependência aonde quer que vá. E ordem de inclusão também importa aqui. Isso adiciona muita complexidade / dependência à enorme base de código.

Após refatorar com ActiveSupport::Concern

require 'active_support/concern'

module Foo
  extend ActiveSupport::Concern
  included do
    def self.method_injected_by_foo_to_host_klass
      ...
    end
  end
end

module Bar
  extend ActiveSupport::Concern
  include Foo

  included do
    self.method_injected_by_foo_to_host_klass
  end
end

class Host
  include Bar # It works, now Bar takes care of its dependencies
end

Agora parece simples.

Se você está pensando, por que não podemos adicionar Foodependência no Barpróprio módulo? Isso não funcionará, pois method_injected_by_foo_to_host_klassprecisa ser injetado em uma classe que Barnão está incluída no Barpróprio módulo.

Fonte: Rails ActiveSupport :: Concern

Siva
fonte
obrigado por isso. Eu estava começando a me perguntar qual é a vantagem deles ...
Hari Karam Singh
FWIW, isto é aproximadamente copiar e colar dos documentos .
Dave Newton
7

Em caso de preocupação, faça o arquivo filename.rb

Por exemplo, eu quero no meu aplicativo onde o atributo create_by existe atualizar o valor lá por 1 e 0 para updated_by

module TestConcern 
  extend ActiveSupport::Concern

  def checkattributes   
    if self.has_attribute?(:created_by)
      self.update_attributes(created_by: 1)
    end
    if self.has_attribute?(:updated_by)
      self.update_attributes(updated_by: 0)
    end
  end

end

Se você deseja passar argumentos em ação

included do
   before_action only: [:create] do
     blaablaa(options)
   end
end

depois disso, inclua no seu modelo assim:

class Role < ActiveRecord::Base
  include TestConcern
end
Sajjad Murtaza
fonte