Dans notre précédent billet, nous nous avions constaté que les gems Ruby permettant de décorer des objets ActiveRecord
ne nous conviennent pas à 100%. Chez Level UP, on apprécie les solutions simples, efficaces et facile à maintenir. Voyant que réaliser une solution maison ne serait pas compliqué, nous l'avons développée.
Helpers et décorateurs
Les décorateurs allègent les modèles de tout ce qui est lié à la présentation de données, autrement dit les vues, avec Rails. Ils limitent l'utilisation des helpers, parfois difficiles à organiser et tester. Essentiellement dans les vues, l'appel à une méthode “décorant” un modèle permet également d'alléger les vues de code liées à la construction de structures ou d'itération permettant d'obtenir un résultat consolidé.
Historiquement Rails propose pour cela d'utiliser des helpers
. Ces derniers sont effectivement très pratiques pour extraire de la complexité inutile dans les vues, mais ayant une portée globale ne sont pas toujours pratiques, faciles à tester ou efficaces lorsque l'on travaille sur des objets ActiveRecord
.
SimpleDelegator : un début de solution, natif à Ruby
Ruby propose depuis très longtemps1 la classe SimpleDelegator
qui permet de déléguer l'appel de méthodes à un objet passé en paramètre :
require "date"
require "delegate"
# app/models/user.rb
class User
def born_on
Date.new(1989, 9, 10)
end
end
# app/decorators/user_decorator.rb
class UserDecorator < SimpleDelegator
def birth_year
born_on.year
end
end
decorated_user = UserDecorator.new(User.new)
decorated_user.born_on #=> 1989-09-10
decorated_user.birth_year #=> 1989
C'est ni plus ni moins ce qu'il nous fallait. Notez que ce patron de conception n'est pas réservé aux modèles ActiveRecord
et peut s'appliquer décorer n'importe quel objet Ruby. Nous l'utilisons par exemple sur d'autres projets basés sur Hanami et Sinatra.
Dans une application Rails où le framework apporte beaucoup de “magie”, ce n'est pas suffisant. Pour une utilisation dans les vues de l'application, il manque l'accès aux helpers
de Rails les plus courants : link_to
, content_tag
, url_for
, etc.
Ces helpers sont disponibles via ActionView::Base. Une solution serait de préfixer tous les appels aux helpers, mais nous perdrions une notion essentielle dans Rails, le contexte. Nous perdrions aussi l'accès aux helpers que nous avons définis dans app/helpers
.
Le contrôleur fournit ce contexte aux vues via la méthode view_context
. Par défaut renvoie une instance de ActionView::Base
. Le monde Rails est bien fait !
ApplicationDecorator : notre décorateur adapté à Rails
Notre première implémentation de ressemblait donc à ceci :
# app/decorators/application_decorator.rb
class ApplicationDecorator < SimpleDelegator
DEFAULT_VIEW_CONTEXT = ApplicationController.new.view_context
def initialize(model, view_context = nil)
super(model)
@view_context = view_context || DEFAULT_VIEW_CONTEXT
end
end
# app/decorators/user_decorator.rb
class UserDecorator < ApplicationDecorator
def register_link
@view_context.link_to "Inscrire l'utilisateur", @view_context.register_user_path(user_id: id)
end
end
Un surcharge de SimpleDelegator
qui ajoute l'accès au contexte de vue permettant d'appeler les helpers de Rails avec le préfixe : view_context.link_to
par exemple. Un fallback sur le contexte par défaut fournit par ApplicationController permet de résoudre d'éventuels cas ou le contexte ne serait pas passé ou pas disponible.
L'appel à ce décorateur dans nos vues se déroule donc de la sorte :
<!-- app/views/users/_user_with_test.html.erb -->
<% decorated_user = UserDecorator.new(@user) %>
<p>
<%= decorated_user.fullname %>
<% unless decorated_user.registered? %>
— <%= decorated_user.register_link %>
<% end %>
</p>
La structure de contrôle de l'affichage ou pas du lien d'inscription peut même être gérée au sein de la méthode décorée :
# app/decorators/user_decorator.rb
class UserDecorator < ApplicationDecorator
def register_link
return if registered?
view_context.link_to "Inscrire l'utilisateur", view_context.register_user_path(user_id: id)
end
end
Ce qui permet une réelle simplification de la vue qui n'a pas à se soucier de l'état de l'utilisateur.
<!-- app/views/users/_user_with_test.html.erb -->
<% decorated_user = UserDecorator.new(@user) %>
<p><%= decorated_user.fullname %> <%= decorated_user.register_link %></p>
Tester les décorateurs
Pour tester unitairement les méthodes de nos décorateurs, il suffit d'hériter le test de ActionView::TestCase
, et de passer au décorateur le view_context
pour le test qui est disponible via la variable view
:
require "test_helper"
class UserDecoratorTest < ActionView::TestCase
def setup
@user = User.new(name: "Alice")
@decorated_user = UserDecorator.new(@user, view)
end
def test_register_link
assert_includes @decorated_user.register_link, @user.name
assert_includes @decorated_user.register_link, "Inscrire"
end
end
Fin de la deuxième partie
Grâce à ce petit PORO2, nous avons pu remplacer la gem Draper
et réussi la montée en version d'une grosse application Rails vers la 5.0 dans un temps raisonnable. Dans une dernière partie, nous verrons les petites surprises que nous avons toutefois rencontrées et comment nous les avons contournées, ainsi que les dernières améliorations que nous avons apportées à notre ApplicationDecorator
afin qu'il répondent parfaitement à l'utilisation que nous avions des décorateurs issus de Draper
.
- Première partie : Le problème des gems non maintenues, ou pourquoi nous avons créé notre propre décorateur pour Rails
-
au moins depuis la version 1.8, les documentations des versions précédentes n'étant plus disponibles en ligne. Son utilisation a été grandement simplifiée à partir de Ruby 2.1. ↩
-
Plain Old Ruby Object. On a souvent tendance à vouloir utiliser des bibliothèques ou des fonctions avancées des frameworks, alors que la plupart du temps, Ruby se suffit amplement à lui-même ! ↩