Comment utiliser les préoccupations dans Rails 4


628

Le générateur de projet par défaut de Rails 4 crée maintenant le répertoire "préoccupations" sous les contrôleurs et les modèles. J'ai trouvé quelques explications sur la façon d'utiliser les problèmes de routage, mais rien sur les contrôleurs ou les modèles.

Je suis à peu près sûr que cela a à voir avec la "tendance DCI" actuelle dans la communauté et je voudrais essayer.

La question est, comment suis-je censé utiliser cette fonctionnalité, existe-t-il une convention sur la façon de définir la hiérarchie de nommage / classe afin de la faire fonctionner? Comment puis-je inclure une préoccupation dans un modèle ou un contrôleur?

Réponses:


617

Je l'ai donc découvert par moi-même. C'est en fait un concept assez simple mais puissant. Cela concerne la réutilisation du code comme dans l'exemple ci-dessous. Fondamentalement, l'idée est d'extraire des morceaux de code communs et / ou spécifiques au contexte afin de nettoyer les modèles et d'éviter qu'ils ne deviennent trop gros et désordonnés.

À titre d'exemple, je vais mettre un modèle bien connu, le modèle 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

Ainsi, en suivant l'exemple de produit, vous pouvez ajouter Taggable à n'importe quelle classe de votre choix et partager ses fonctionnalités.

Ceci est assez bien expliqué par DHH :

Dans Rails 4, nous allons inviter les programmeurs à utiliser des préoccupations avec les répertoires app / models / concern et app / controllers / concern par défaut qui font automatiquement partie du chemin de chargement. Avec l'encapsuleur ActiveSupport :: Concern, c'est juste assez de support pour faire briller ce mécanisme d'affacturage léger.


11
DCI traite d'un contexte, utilise des rôles comme identificateurs pour mapper un modèle mental / cas d'utilisation au code et ne nécessite aucun wrapper (les méthodes sont directement liées à l'objet au moment de l'exécution), donc cela n'a rien à voir avec DCI.
ciscoheat

2
@yagooar, même l'inclure au moment de l'exécution ne ferait pas de DCI. Si vous souhaitez voir un exemple d'implémentation rubis DCI. Jetez un œil à fulloo.info ou aux exemples sur github.com/runefs/Moby ou pour savoir comment utiliser maroon pour faire DCI dans Ruby et ce que DCI est runefs.com (Ce que DCI est. Est une série d' articles que j'ai vient de commencer récemment)
Rune FS

1
@RuneFS && ciscoheat vous aviez raison tous les deux. Je viens d'analyser à nouveau les articles et les faits. Et, je suis allé le week-end dernier à une conférence Ruby où l'on a parlé de DCI et finalement j'ai compris un peu plus sa philosophie. Modification du texte afin qu'il ne mentionne pas du tout DCI.
yagooar

9
Il convient de mentionner (et probablement d'inclure dans un exemple) que les méthodes de classe sont censées être définies dans un module ClassMethods spécialement nommé, et que ce module est étendu par la classe de base ActiveSupport :: Concern, également.
febeling

1
Merci pour cet exemple, principalement parce que j'étais stupide et que je définissais mes méthodes de niveau de classe à l'intérieur du module ClassMethods avec self. quoiqu'il en soit, et cela ne fonctionne pas = P
Ryan Crews

379

J'ai lu comment utiliser les préoccupations des modèles pour maquiller les modèles gras et sécher vos codes de modèle. Voici une explication avec des exemples:

1) SÉCHAGE des codes modèles

Prenons un modèle d'article, un modèle d'événement et un modèle de commentaire. Un article ou un événement a de nombreux commentaires. Un commentaire appartient à un article ou à un événement.

Traditionnellement, les modèles peuvent ressembler à ceci:

Modèle de commentaire:

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

Modèle d'article:

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

Modèle d'événement

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

Comme nous pouvons le remarquer, il existe un morceau de code important commun à la fois à l'événement et à l'article. En utilisant des préoccupations, nous pouvons extraire ce code commun dans un module séparé Commentable.

Pour cela, créez un fichier commentable.rb dans app / models / préoccupations.

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

Et maintenant, vos modèles ressemblent à ceci:

Modèle de commentaire:

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

Modèle d'article:

class Article < ActiveRecord::Base
  include Commentable
end

Modèle d'événement:

class Event < ActiveRecord::Base
  include Commentable
end

2) Modèles Fat Skin-nizing.

Prenons un modèle d'événement. Un événement a de nombreux participants et commentaires.

En règle générale, le modèle d'événement peut ressembler à ceci

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

Les modèles avec de nombreuses associations et autrement ont tendance à accumuler de plus en plus de code et à devenir ingérables. Les préoccupations fournissent un moyen de skin-nize les modules adipeux les rendant plus modularisés et faciles à comprendre.

Le modèle ci-dessus peut être refactorisé en utilisant les préoccupations comme ci-dessous: Créez un fichier attendable.rbet commentable.rbdans le dossier app / models / concern / event

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

Et maintenant, en utilisant Préoccupations, votre modèle d'événement se réduit à

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

* Lors de l'utilisation, il est préférable d'opter pour un groupement basé sur un domaine plutôt que sur un groupement «technique». Le regroupement basé sur le domaine est comme «Commentable», «Photoable», «Attendable». Le regroupement technique signifie «ValidationMethods», «FinderMethods», etc.


6
Les préoccupations ne sont-elles donc qu'un moyen d'utiliser l'héritage ou les interfaces ou l'héritage multiple? Quel est le problème avec la création d'une classe de base commune et le sous-classement à partir de cette classe de base commune?
Chloé

3
En effet @Chloe, j'en ai un peu où le rouge, une application Rails avec un répertoire 'préoccupations' est en fait une 'préoccupation' ...
Ziyan Junaideen

Vous pouvez utiliser le bloc «inclus» pour définir toutes vos méthodes et inclut: les méthodes de classe (avec def self.my_class_method), les méthodes d'instance et les appels et directives de méthode dans la portée de la classe. Pas besoin demodule ClassMethods
A Fader Darkly

1
Le problème que j'ai avec les préoccupations est qu'ils ajoutent des fonctionnalités directement au modèle. Donc, si deux préoccupations implémentent toutes les deux add_item, par exemple, vous êtes foutu. Je me souviens avoir pensé que Rails était cassé lorsque certains validateurs ont cessé de fonctionner, mais quelqu'un s'était implémenté any?dans un souci. Je propose une solution différente: utiliser le souci comme une interface dans une langue différente. Au lieu de définir la fonctionnalité, il définit la référence à une instance de classe distincte qui gère cette fonctionnalité. Ensuite, vous avez des classes plus petites et plus soignées qui font une chose ...
A Fader Darkly

@aaditi_jain: Veuillez corriger une petite modification pour éviter toute idée fausse. c'est-à-dire "Créer un fichier attendable.rd et commentable.rb dans le dossier app / models / issues / event" -> attendable.rd doit être attendable.rb Merci
Rubyist

97

Il convient de mentionner que l'utilisation de préoccupations est considérée comme une mauvaise idée par beaucoup.

  1. comme ce mec
  2. et celui-là

Certaines raisons:

  1. Il y a de la magie noire dans les coulisses - Le souci est la includeméthode de correction , il y a tout un système de gestion des dépendances - beaucoup trop de complexité pour quelque chose qui est un bon vieux modèle de mixage Ruby.
  2. Vos cours ne sont pas moins secs. Si vous bourrez 50 méthodes publiques dans divers modules et les incluez, votre classe a toujours 50 méthodes publiques, c'est juste que vous cachez cette odeur de code, sorte de mettre vos ordures dans les tiroirs.
  3. Codebase est en fait plus difficile à naviguer avec toutes ces préoccupations.
  4. Êtes-vous sûr que tous les membres de votre équipe ont la même compréhension de ce qui devrait réellement remplacer la préoccupation?

Les soucis sont un moyen facile de se tirer une balle dans la jambe, soyez prudent avec eux.


1
Je sais que SO n'est pas le meilleur endroit pour cette discussion, mais quel autre type de mixin Ruby garde vos cours au sec? Il semble que les raisons # 1 et # 2 dans vos arguments soient contraires, à moins que vous ne plaidiez simplement en faveur d'une meilleure conception OO, de la couche services ou de quelque chose d'autre qui me manque? (Je ne suis pas en désaccord - je suggère que l'ajout d'alternatives aide!)
toobulkeh

2
Utiliser github.com/AndyObtiva/super_module est une option, utiliser de bons anciens modèles ClassMethods en est une autre. Et utiliser plus d'objets (comme des services) pour séparer proprement les préoccupations est certainement la voie à suivre.
Dr.Strangelove

4
Downvoting parce que ce n'est pas une réponse à la question. C'est une opinion. C'est une opinion dont je suis sûr qu'elle a ses mérites, mais elle ne devrait pas être une réponse à une question sur StackOverflow.
Adam

2
@Adam C'est une réponse d'opinion. Imaginez que quelqu'un se demande comment utiliser des variables globales dans les rails, mentionne sûrement qu'il existe de meilleures façons de faire les choses (c.-à-d. Redis.current vs $ redis) pourrait être une information utile pour le démarrage du sujet? Le développement logiciel est intrinsèquement une discipline d'opinion, il n'y a pas moyen de le contourner. En fait, je vois les opinions comme des réponses et des discussions dont la réponse est la meilleure de tous les temps sur stackoverflow, et c'est une bonne chose
Dr.Strangelove

2
Bien sûr, le mentionner avec votre réponse à la question semble correct. Cependant, rien dans votre réponse ne répond à la question du PO. Si tout ce que vous souhaitez faire est d'avertir quelqu'un pourquoi il ne devrait pas utiliser des préoccupations ou des variables globales, cela ferait un bon commentaire que vous pourriez ajouter à sa question, mais cela ne fait pas vraiment une bonne réponse.
Adam


46

J'ai senti que la plupart des exemples présentés ici démontraient le pouvoir de modulela ActiveSupport::Concernvaleur ajoutée plutôt que la façon dont elle l' ajoutait module.

Exemple 1: modules plus lisibles.

Donc, sans se soucier de la façon dont ce modulesera typique .

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

Après refactoring avec 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

Vous voyez que les méthodes d'instance, les méthodes de classe et le bloc inclus sont moins compliqués. Les préoccupations les injecteront de manière appropriée pour vous. C'est un avantage de l'utilisation ActiveSupport::Concern.


Exemple 2: Gérez les dépendances des modules avec élégance.

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

Dans cet exemple, Barle module a Hostvraiment besoin. Mais puisque la Bardépendance avec Foola Hostclasse doit le faire include Foo(mais attendez pourquoi Hostveut savoir Foo? Peut-il être évité?).

BarAjoute donc la dépendance partout où il va. Et l' ordre d'inclusion est également important ici. Cela ajoute beaucoup de complexité / dépendance à une énorme base de code.

Après refactoring avec 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

Maintenant, ça a l'air simple.

Si vous pensez pourquoi ne pouvons-nous pas ajouter de Foodépendance dans le Barmodule lui-même? Cela ne fonctionnera pas car method_injected_by_foo_to_host_klassil doit être injecté dans une classe qui n'inclut Barpas le Barmodule lui-même.

Source: Rails ActiveSupport :: Préoccupation


Merci pour ça. Je commençais à me demander quel était leur avantage ...
Hari Karam Singh

FWIW c'est à peu près copier-coller à partir des documents .
Dave Newton

7

En cas de soucis, créez le fichier filename.rb

Par exemple, je veux dans mon application où l'attribut create_by existe mettre à jour la valeur par 1, et 0 pour 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

Si vous voulez passer des arguments en action

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

après cela, incluez dans votre modèle comme ceci:

class Role < ActiveRecord::Base
  include TestConcern
end
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.