Le Blog du Geek Joyeux

Plus moins vite tu codes, moins plus vite ça plante

Décorer ses modèles Rails avec Draper

| Commentaires

TL;DR

Draper permet d’ajouter à un modèle des méthodes spécifiques au formatage de données sans pour autant alourdir le code de ce modèle.

Point de départ

Lorsque l’on développe une application Rails, la façon d’afficher les données issues d’un modèle n’est pas forcément celle dont on les stocke. On peut aussi vouloir utiliser différentes formes d’affichage pour une même donnée.

Prenons pour exemple un numéro de téléphone. Disons que dans notre modèle Contact, il sera stocké sous forme d’une chaine de caractères d’une longueur de 10.

1
2
contact = Contact.new(phone: '0123456789')
contact.phone #=> '0123456789'

Nous voulons maintenant l’afficher de trois façons différentes:

1
2
3
0123456789        # pour les exports de donnees
01 23 45 67 89    # pour l'affichage web
+33 1 23 45 67 89 # pour un cas particulier

Dés lors, quelles solutions s’offrent à nous ?

Écrire des méthodes dans le modèle ?

On pourrait écrire les méthodes dont on a besoin directement dans le modèle:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Contact | ActiveRecord::Base
  attr_accessible :phone

  # Returns phone formatted like: 01 23 45 67 89
  def readable_phone
    phone.gsub /\d\d(?=\d)/, '\\0 '
  end

  # Returns phone formatted like +33 1 23 45 67 89
  def international_phone
    "+33 " + readable_phone[1..-1]
  end
end

Le problème ici est que l’on fait grossir notre modèle pour lui ajouter des méthodes qui sont spécifiques au rendu et non au code métier. On ne respecte pas la séparation des responsabilités au sein de notre code.

Écrire des helpers ?

Puisque le formatage du numéro de téléphone est une problématique de vue, le plus naturel serait d’écrire des helpers pour chaque format.

1
2
3
4
5
6
7
8
9
10
11
module PhoneHelper
  # Returns phone formatted like: 01 23 45 67 89
  def readable_phone(phone)
    phone.gsub /\d\d(?=\d)/, '\\0 '
  end

  # Returns phone formatted like +33 1 23 45 67 89
  def international_phone
    "+33 " + readable_phone(phone)[1..-1]
  end
end

On trouve deux problèmes ici. Le premier est que le code devient un peu moins lisible puisque l’on ajoute un argument à nos méthodes. Le deuxième est que l’on perd l’orientation objet de notre code.

Si l’on regarde le code utilisé dans la vue pour appeler ces méthodes, on se rend compte que l’on ne fait pas du tout de l’objet mais du procédural !

1
Phone: <%= readable_phone(@contact.phone) %>

Utiliser un décorateur

Le principe du décorateur va être d’encapsuler notre modèle et de lui ajouter une couche spécifique au rendu. De cette manière, notre modèle ne contient que des méthodes spécifiques au métier et le décorateur se charge de la vue. Le plus important étant qu’ici, on se sert d’objets de bout en bout.

Il existe un certain nombre de solutions pour implémenter cette architecture. Je parlerai aujourd’hui de Draper.

Pour installer Draper, ajoutez la gem dans votre Gemfile et appelez bundle install.

1
gem 'draper'

Une fois cela fait, vous pouvez lancer la commande rails g draper:install qui va créer le dossier app/decorators ainsi qu’un fichier app/decorators/application_decorator.rb.

Ce dernier contient la définition d’un décorateur global dont vos décorateurs hériteront par la suite.

1
2
3
class ApplicationDecorator < Draper::Base
  # ...
end

Cela vous permet d’y placer des méthodes qui peuvent être utiles à plusieurs (ou à tous les) décorateurs.

Il est maintenant temps de créer un décorateur pour notre modèle Contact:

$ rails g draper:decorator Contact
create  app/decorators/article_decorator.rb

Le fichier généré contient un décorateur qui, pour le moment, ne contient aucune méthode.

1
2
3
class ContactDecorator < ApplicationDecorator
  decorates :contact
end

Nous pouvons dés à présent l’utiliser dans notre contrôleur de la façon suivante:

1
2
3
4
5
6
7
8
9
class ContactsController < ApplicationController
  def index
    @contacts = ContactDecorator.decorate Contact.all
  end

  def show
    @contact = ContactDecorator.decorate Contact.find(params[:id])
  end
end

Par défaut le décorateur délègue tous les appels de méthodes à l’objet qu’il décore. Tout code utilisant à l’origine le modèle Contact continue donc de fonctionner.

Ajoutons maintenant nos méthodes de mise en forme au décorateur:

1
2
3
4
5
6
7
8
9
10
11
class ContactDecorator < ApplicationDecorator
  decorates :contact

  def readable_phone
    model.phone.gsub /\d\d(?=\d)/, '\\0 '
  end

  def international_phone
    "+33 " + readable_phone[1..-1]
  end
end

Nous pouvons appeler ces deux méthodes dans les vues qui ont reçu un contact décoré:

1
2
<%= @contact.readable_phone %>
<%= @contact.international_phone %>

Ce qu’il faut retenir

Nous avons vu trois manières d’obtenir le même résultat. Si la première semble la plus simple, il est important de noter qu’elle est difficilement maintenable au fil des ajouts de méthodes.

L’utilisation de Draper nous permet de garder un accès naturel au données formatées tout en séparant les responsabilités en bonne et due forme.

Commentaires