Rails 4: utilisation des concerns

Publié le 22 mai 2014 par Jonathan François | back

Cet article est publié sous licence CC BY-NC-SA

Aujourd’hui nous allons nous intéresser à l’utilisation des concerns au sein d’un projet Rails 4.

Nous allons expliquer ce que sont les concerns, pourquoi et comment les utiliser. Tout développeur s’efforce de créer du code fonctionnel mais surtout du code maintenable et répondant aux bonnes “pratiques de développement”. Ces pratiques sont variées et parfois personnelles mais certaines s’adaptent à tous types de développement et font l’unanimité auprès de la communauté, c’est le cas du DRY, “Don’t Repeat Yourself”.

Vous connaissez certainement ce principe (si non, wikipedia est là), qui a pour but d’éviter de dupliquer du code au sein même d’une application, en d’autres terme, essayer de mutualiser intelligemment votre code.

L’utilisation de concerns est l’un des outils de développement permettant de respecter ce principe et de rendre son code plus maintenable et lisible. Depuis Rails 4, le framework à volontairement intégrer cette fonctionnalité afin d’inciter les développeurs Rails aux bonnes pratiques. Vous vous êtes aperçu de l’arrivée du dossier “Concerns” dans l’architecture de votre projet, au sein des controllers ainsi que des models. C’est dans son dossier que nous allons placer nos concerns.

Avec les versions précédentes de Rails, la solution pour mutualiser du code était de concevoir des librairies.

Rentrons maintenant dans le cœur de notre article et dévoilons les différents points que nous allons aborder :

  • Exemple basique d’illustration
  • Conditions d’utilisation:
    • Ou et avec quoi puis-je utiliser les concerns ?
    • Décomposition d’un concern.
    • Partage et espace de nommage d’un concern.
    • Piège à éviter
  • Exemple concret

Exemple d’illustration

Quoi de mieux qu’un exemple pour bien comprendre de quoi nous parlons ?

Prenons le cas classique d’un blog où chaque utilisateur et chaque post doivent avoir un avatar. Nous avons donc les deux models suivants (avec l’utilisation de la gem Dragonfly):

# app/models/user.rb
class User < ActiveRecord::Base
    dragonfly_accessor :avatar
    validates :avatar, presence: true
end
# app/models/post.rb
class Post < ActiveRecord::Base
    dragonfly_accessor :avatar
    validates :avatar, presence: true
end

On s’aperçoit rapidement qu’on duplique du code. Avant Rails 4, nous n’aurions pas crée une librairie juste pour cela on est d’accord, mais maintenant grâce aux concerns, l’on peut avoir cela :

# app/models/concerns/avatar_concern.rb
module AvatarConcern
    extend ActiveSupport::Concern

    included do
        dragonfly_accessor :avatar
        validates :avatar, presence: true
    end
end
# app/models/user.rb
class User < ActiveRecord::Base
    include AvatarConcern
end
# app/models/post.rb
class Post < ActiveRecord::Base
    include AvatarConcern
end

Vous remarquerez que cela ne change rien au fonctionnement de l’application, c’est bien ce que l’on voulait. Si un autre model a besoin d’un avatar obligatoire, nous pourrons juste lui inclure ce concern “include AvatarConcern”.

Conditions d’utilisation

Où et avec quoi puis-je utiser les concerns ?

Nous pouvons utiliser les concerns au sein des controllers, des models mais également des routes. Voici ce que l’on peut retrouver au sein d’un concern :

  • les relations, associations, accessors (model)
  • les scopes (model)
  • les constantes (model)
  • les filtres (controller et model)
  • les méthodes de classes (controller et model)
  • les méthodes d’instances (controller et model)
  • les méthodes privées utilisées par les méthodes publiques du concern lui même.

Concernant les models et controllers, regardons comment les mettre en place:

# app/models/concerns/test_concern.rb
# app/controllers/concerns/test_concern.rb
module TestConcern
    extend ActiveSupport::Concern

    included do
        # Ici nous allons mentionnés nos scopes, relations et paramètres de configuration de celles-ci.
        # ex: has_namy, belongs_to ...
        # ex: validates Rails ou perso
        # ex: default_scope { where(approved: true)}
        # ex: MIN_QUANTITY = 1
    end
    module ClassMethods
        # Ici nous allons mentionnés toutes les méthodes de classe et d'instances pouvant être mutualisées.
        # Les méthodes déclarées dans ce module deviennent des méthodes de classe sur la classe cible ( où l'on inclus notre concern ).

        private
        # les méthodes privées ne pouvant être appelées qu'à l'intérieur de ce concern.
    end
end

Concernant les routes, vous allez définir votre concern directement au sein de votre fichier routes.rb, imaginons l’exemple ci-dessous :

#app/config/routes.rb
Rails.application.routes.draw do
    resources :posts do
      resources :comments
      resources :categories
      resources :tags
    end

    resources :items do
      resources :comments
      resources :categories
      resources :tags
    end
end

En utilisant les concerns, cela devient :

#app/config/routes.rb
Rails.application.routes.draw do

    concern :sociable do |options|
        resources :comments, options
        resources :categories, options
        resources :tags, options
    end

    resources :posts do
        concerns :sociable, only: :create
    end

    resources :items do
        concerns :sociable, only: :update
    end
end

Nous pouvons passer un bloc à notre concern afin de permettre la personnalisation de celui-ci. On voit bien que cela nous permet d’avoir des routes plus clairs et plus faciles à modifier.

Espace de nommage d’un concern

Il est important de respecter le “naming” dans l’utilisation des concerns. Il faut donc que le nom de fichier corresponde au nom de votre concern.

Par exemple le nom de fichier du concern AvatarConcern doit être avatar_concern.rb .

Il est tout à fait possible d’organiser vos concerns en sous-dossier, ce qui est d’ailleurs recommandé pour de gros besoins.

Par exemple :

  • app/models/concerns/posts/calcul_rate_concern.rb
  • app/models/concerns/image/avatar_concern.rb etc…

Lorsque deux concerns ont le même nom, comme app/models/concerns/calcul_rate_concern.rb et app/models/concerns/posts/calcul_rate_concern.rb, Rails va d’abord rechercher le concern relatif à la class qui l’appelle, donc dans le cas :

class Post < ActiveRecord::Base
    include CalculRateConcern
en

C’est bien app/models/concern/posts/calcul_rate_concern.rb qui sera utilisé, mais si celui-ci n’existe pas, il remontera l’architecture pour trouver un concern du même nom, soit app/models/concerns/calcul_rate_concern.rb .

Vous pouvez également utiliser l’espace de nommage pour inclure votre concern :

class Post < ActiveRecord::Base
    include Posts::CalculRateConcern
en

Piége à éviter

On commence souvent par définir les relations et validations de nos models et on se dit que l’on pourrait très rapidement créer des concerns car nous sommes en train de dupliquer du code, mais attention cela n’est pas automatique. Surtout que vos models vont certainement faire l’objet de plusieurs modifications indépendamment des autres models.

Je pense qu’il faut construire les concerns par sujet, thèmes et fonctionnalités et ne pas faire cela de manière automatique.

Exemple Concret

Contexte

Pour cet exemple, nous allons prendre le cas d’une application dédiée à la gestion de location de voiture. Il va valoir gérer le stock du nombre de voiture disponible en fonction de celles déjà louées et celles en panne.

Nous allons donc avoir une classe Car qui aura les attributs suivants:

  • available_quantity
  • hired_quantity
  • damage_quantity

Dans l’interface, l’utilisateur pourra sur la même page et en même temps, renseigner plusieurs informations comme : 2 voitures louées, 3 voitures rentrées et 1 voiture en panne.

aperçu template

Dans cette société ils louent également des motos et des camions avec le même système.

Il est clair que la fonctionnalité requise pour mettre à jour les stocks est une partie du code qui va se répéter sur les attributs mais également sur les différents models (Car, Moto et Truck). Ici, nous devons de suite penser aux concerns afin de simplifier tout cela.

Nous allons donc créer le concern EditableQuantity, dans lequel nous allons créer une méthode de validation en fonction des attributs.

#app/models/concerns/editable_quantity_concern.rb
module EditableQuantityConcern
  extend ActiveSupport::Concern

  module ClassMethods

    def editable_quantity(attribute, boolean_impact_stock)
      attr_accessor "#{attribute}_to_add"
      attr_accessor "#{attribute}_to_remove"

      before_validation do
        # Want to remove from attribute and increase available stock
        if self.send("#{attribute}_to_remove").present?
          if self.send(attribute).to_i >= self.send("#{attribute}_to_remove").to_i
            self.available_quantity += self.send("#{attribute}_to_remove").to_i if boolean_impact_stock
            self.send "#{attribute}=", self.send(attribute).to_i - self.send("#{attribute}_to_remove").to_i
          else
            errors.add :base, :insufficient_stock
          end
        end

        # Want to add to attribute and decrease available stock
        if self.send("#{attribute}_to_add").present?
          if boolean_impact_stock
            if (self.available_quantity + self.send("available_quantity_to_add").to_i) >= self.send("#{attribute}_to_add").to_i
              self.available_quantity -= self.send("#{attribute}_to_add").to_i
              self.send "#{attribute}=", self.send(attribute).to_i + self.send("#{attribute}_to_add").to_i
            else
              errors.add :base, :insufficient_stock
            end
          else
            self.send "#{attribute}=", self.send(attribute).to_i + self.send("#{attribute}_to_add").to_i
          end
        end
      end

    end

  end
end

Et maintenant dans notre model Car :

class Car < ActiveRecord::Base
    include EditableQuantityConcern
    editable_quantity :available_quantity, false
    editable_quantity :hired_quantity, true
    editable_quantity :damage_quantity, true
end

Comme vous pouvez le constater notre méthode editable_quantity prend deux paramètres :

  • l’attribut concerné
  • un boolean qui permet de spécifier si cet attribut influe ou pas sur le stock (ce qui nous permet d’utiliser notre concern même si l’attribut est indépendant du stock).

Nos autres models, Moto et Trunk seront similaires. On pourrait également essayer d’empêcher de dupliquer ce code, en créant une classe Vehicule incluant notre concern et des classes enfants qui serait Car, Moto et Trunk.

Conclusion

Le concern de ce dernier exemple a permis de mutualiser une fonctionnalité à l’ensemble de notre application. N’est-ce pas le comportement d’une gem ?

C’est le cas et c’est la raison pour laquelle le prochain article fera l’objet de la transformation de ce concern en gem.