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 :
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”.
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 :
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.
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 :
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
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.
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:
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.
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 :
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.
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.