Come utilizzare le preoccupazioni in Rails 4


628

Il generatore di progetti predefinito di Rails 4 ora crea la directory "preoccupazioni" sotto controller e modelli. Ho trovato alcune spiegazioni su come utilizzare i problemi di routing, ma nulla sui controller o sui modelli.

Sono abbastanza sicuro che abbia a che fare con l'attuale "tendenza DCI" nella comunità e vorrei provarlo.

La domanda è: come dovrei usare questa funzione, esiste una convenzione su come definire la gerarchia di denominazione / classe per farla funzionare? Come posso includere un problema in un modello o controller?

Risposte:


617

Così l'ho scoperto da solo. In realtà è un concetto piuttosto semplice ma potente. Ha a che fare con il riutilizzo del codice come nell'esempio seguente. Fondamentalmente, l'idea è quella di estrarre blocchi di codice comuni e / o specifici al fine di ripulire i modelli ed evitare che diventino troppo grassi e disordinati.

Ad esempio, inserirò uno schema ben noto, lo schema taggabile:

# 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

Quindi, seguendo l'esempio del prodotto, puoi aggiungere Taggable a qualsiasi classe desideri e condividerne le funzionalità.

Questo è abbastanza ben spiegato da DHH :

In Rails 4, inviteremo i programmatori a utilizzare le preoccupazioni con le directory app / modelli / preoccupazioni predefinite e app / controller / preoccupazioni che fanno automaticamente parte del percorso di caricamento. Insieme al wrapper ActiveSupport :: Concern, è sufficiente il supporto per far brillare questo meccanismo di factoring leggero.


11
DCI si occupa di un contesto, utilizza i ruoli come identificatori per mappare un modello mentale / caso d'uso al codice e non richiede l'utilizzo di wrapper (i metodi sono associati direttamente all'oggetto in fase di esecuzione), quindi non ha nulla a che fare con DCI.
ciscoheat,

2
@yagooar anche includendolo in fase di esecuzione non lo renderebbe DCI. Se si desidera vedere un'implementazione di esempio DCI ruby. Dai un'occhiata a fulloo.info o agli esempi su github.com/runefs/Moby o su come usare il marrone per fare DCI in Ruby e cosa DCI è runefs.com (che cos'è DCI. È una serie di post che ho appena iniziato di recente)
Rune FS,

1
@RuneFS && ciscoheat avevi ragione entrambi. Ho appena analizzato di nuovo articoli e fatti. E sono andato lo scorso fine settimana a una conferenza di Ruby in cui si parlava di DCI e alla fine ho capito un po 'di più sulla sua filosofia. Modificato il testo in modo che non menzionasse affatto DCI.
giovedì

9
Vale la pena ricordare (e probabilmente includere in un esempio) che i metodi di classe dovrebbero essere definiti in un modulo ClassMethods con un nome speciale e che questo modulo viene esteso dalla classe di base anche ActiveSupport :: Concern.
febbraio

1
Grazie per questo esempio, principalmente b / c ero stupido e definivo i miei metodi a livello di classe all'interno del modulo ClassMethods con self.whats still, e che non funziona = P
Ryan Crews

379

Ho letto sull'uso delle preoccupazioni dei modelli per skin-nize dei modelli di grasso e per ASCIUGARE i codici del modello. Ecco una spiegazione con esempi:

1) ASCIUGATURA dei codici modello

Considera un modello di articolo, un modello di evento e un modello di commento. Un articolo o un evento ha molti commenti. Un commento appartiene all'articolo o all'evento.

Tradizionalmente, i modelli possono apparire così:

Modello di commento:

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

Modello dell'articolo:

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

Modello di 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

Come possiamo notare, esiste una parte significativa del codice comune sia all'evento che all'articolo. Usando le preoccupazioni possiamo estrarre questo codice comune in un modulo separato Commentable.

Per questo crea un file commentable.rb in app / modelli / preoccupazioni.

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 ora i tuoi modelli si presentano così:

Modello di commento:

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

Modello dell'articolo:

class Article < ActiveRecord::Base
  include Commentable
end

Modello di evento:

class Event < ActiveRecord::Base
  include Commentable
end

2) Modelli di grasso per la pelle.

Prendi in considerazione un modello di evento. Un evento ha molti partecipanti e commenti.

In genere, il modello di eventi potrebbe apparire così

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

Modelli con molte associazioni e altrimenti hanno la tendenza ad accumulare sempre più codice e diventare ingestibili. Le preoccupazioni forniscono un modo per skin-nize i moduli fat rendendoli più modularizzati e di facile comprensione.

Il modello sopra può essere refactored usando i problemi come di seguito: Crea un file attendable.rbe commentable.rbnella cartella app / modelli / preoccupazioni / eventi

attendable.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 ora usando Preoccupazioni, il tuo modello Evento si riduce a

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

* Durante l'utilizzo delle preoccupazioni è consigliabile scegliere un raggruppamento basato su "dominio" anziché raggruppamento "tecnico". Il raggruppamento basato sul dominio è come "Commentable", "Photoable", "Attendable". Raggruppamento tecnico significa "Metodi di convalida", "Metodi di ricerca" ecc


6
Quindi le preoccupazioni sono solo un modo per usare l'ereditarietà o le interfacce o l'ereditarietà multipla? Cosa c'è di sbagliato nella creazione di una classe base comune e nella sottoclasse da quella classe base comune?
Chloe,

3
In effetti @Chloe, in alcuni casi in rosso, un'app di Rails con una directory "preoccupazioni" è in realtà una "preoccupazione" ...
Ziyan Junaideen,

È possibile utilizzare il blocco "incluso" per definire tutti i metodi e include: metodi di classe (con def self.my_class_method), metodi di istanza, chiamate a metodi e direttive nell'ambito della classe. Non c'è bisogno dimodule ClassMethods
A Fader Darkly,

1
Il problema che ho con le preoccupazioni è che aggiungono funzionalità direttamente al modello. Quindi, se due problemi riguardano entrambi add_item, ad esempio, sei fregato. Ricordo di aver pensato che Rails si fosse rotto quando alcuni validatori avevano smesso di funzionare, ma qualcuno si era messo in pratica any?in una preoccupazione. Propongo una soluzione diversa: usare la preoccupazione come un'interfaccia in una lingua diversa. Invece di definire la funzionalità, definisce il riferimento a un'istanza di classe separata che gestisce tale funzionalità. Poi hai classi più piccole e più pulite che fanno una cosa ...
A Fader Darkly,

@aaditi_jain: correggi le piccole modifiche per evitare equivoci. vale a dire "Creare un file attendable.rd e commentable.rb nella cartella app / modelli / preoccupazioni / eventi" -> attendable.rd deve essere attendable.rb Grazie
Rubyist

97

Vale la pena ricordare che l'uso delle preoccupazioni è considerato una cattiva idea da molti.

  1. come questo ragazzo
  2. e questo

Alcuni motivi:

  1. C'è qualche magia oscura che sta accadendo dietro le quinte - La preoccupazione è il includemetodo di patching , c'è un intero sistema di gestione delle dipendenze - troppa complessità per qualcosa che è banale buon vecchio schema di mixaggio Ruby.
  2. Le tue lezioni non sono meno secche. Se riempi 50 metodi pubblici in vari moduli e li includi, la tua classe ha ancora 50 metodi pubblici, è solo che nascondi quell'odore di codice, in un certo senso metti la spazzatura nei cassetti.
  3. Codebase è in realtà più difficile navigare con tutte quelle preoccupazioni in giro.
  4. Sei sicuro che tutti i membri del tuo team abbiano la stessa comprensione di cosa dovrebbe davvero sostituire la preoccupazione?

Le preoccupazioni sono un modo semplice per spararti alla gamba, stai attento con loro.


1
So che SO non è il posto migliore per questa discussione, ma quale altro tipo di mix di Ruby mantiene asciutte le tue lezioni? Sembra che i motivi n. 1 e n. 2 nelle tue argomentazioni siano controproducenti, a meno che tu non stia solo sostenendo una migliore progettazione OO, il livello dei servizi o qualcos'altro che mi manca? (Non sono in disaccordo - sto suggerendo di aggiungere alternative utili!)
toobulkeh

2
L'uso di github.com/AndyObtiva/super_module è un'opzione, l'uso di buoni vecchi schemi ClassMethods è un'altra. E l'uso di più oggetti (come i servizi) per separare in modo chiaro le preoccupazioni è sicuramente la strada da percorrere.
Dr.Strangelove,

4
Downvoting perché questa non è una risposta alla domanda. È un'opinione. È un'opinione che sono sicuro che abbia i suoi meriti, ma non dovrebbe essere una risposta a una domanda su StackOverflow.
Adam,

2
@Adam È una risposta supponente. Immagina che qualcuno potrebbe chiedere come utilizzare le variabili globali nelle rotaie, sicuramente dire che ci sono modi migliori per fare le cose (ad esempio Redis.current vs $ redis) potrebbero essere informazioni utili per iniziare l'argomento? Lo sviluppo del software è intrinsecamente una disciplina presunta, non c'è modo di aggirarla. In effetti, vedo le opinioni come risposte e discussioni, la risposta è la migliore in assoluto su StackOverflow, ed è una buona cosa
Dr.Strangelove,

2
Certo, menzionarlo insieme alla tua risposta alla domanda sembra a posto. Tuttavia, nulla nella risposta risponde alla domanda del PO. Se tutto ciò che desideri fare è avvisare qualcuno perché non dovrebbero usare preoccupazioni o variabili globali, allora sarebbe un buon commento che potresti aggiungere alla loro domanda, ma in realtà non è una buona risposta.
Adam,


46

Ho sentito che la maggior parte degli esempi qui ha dimostrato il potere modulepiuttosto che il modo in cui ActiveSupport::Concernaggiunge valore module.

Esempio 1: moduli più leggibili.

Quindi senza preoccupazioni come modulesarà un tipico .

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

Dopo il refactoring con 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

Vedi i metodi di istanza, i metodi di classe e il blocco incluso sono meno disordinati. Le preoccupazioni le inietteranno in modo appropriato per te. Questo è un vantaggio dell'utilizzo ActiveSupport::Concern.


Esempio 2: gestire le dipendenze del modulo con garbo.

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

In questo esempio Barè il modulo che ha Hostdavvero bisogno. Ma dal momento che la Bardipendenza con Foola Hostclasse deve include Foo(ma aspettare perché Hostvuole saperlo Foo? Può essere evitato?).

Quindi Baraggiunge dipendenza ovunque vada. E l' ordine di inclusione conta anche qui. Ciò aggiunge molta complessità / dipendenza all'enorme base di codice.

Dopo il refactoring con 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

Ora sembra semplice.

Se stai pensando perché non possiamo aggiungere Foodipendenza nel Barmodulo stesso? Ciò non funzionerà poiché method_injected_by_foo_to_host_klassdeve essere iniettato in una classe che Barnon include il Barmodulo stesso.

Fonte: Rails ActiveSupport :: Preoccupazione


grazie per quello. Cominciavo a chiedermi quale fosse il loro vantaggio ...
Hari Karam Singh,

FWIW, questo è approssimativamente copia-incolla dai documenti .
Dave Newton,

7

Per quanto riguarda le preoccupazioni, crea il file nomefile.rb

Ad esempio, voglio nella mia applicazione in cui esiste l'attributo create_by update lì valore di 1 e 0 per 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 si desidera passare argomenti in azione

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

successivamente includi nel tuo modello in questo modo:

class Role < ActiveRecord::Base
  include TestConcern
end
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.