Le refactoring pour les nuls : séparer rendu et contrôleurs dans votre application Ruby on Rails
Mélanger contrôleurs et rendu est une erreur commune lorsqu’on utilise un framework objet, particulièrement quand on vient du procédural, et qu’on a pris l’habitude de traiter les données et leur utilisation dans une même fonction. Si le paradigme objet a voulu séparer modèle, vue et contrôleur, c’est que c’était mieux pour tout le monde, à commencer par votre successeur.
Dans cet article, je vous propose d’étudier un exemple de ce qu’il ne faut pas faire : un développeur que nous ne nommerons pas a trouvé intelligent de traiter contrôle et formatage dans la même méthode de contrôleur, avant de se rendre compte que ce n’était peut-être pas la meilleure chose à faire.
Controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
module SocialBookmark require 'rexml/document' def parse_config(permalink, title) xml = REXML::Document.new(File.open("#{RAILS_ROOT}/config/sites_EN.xml")) output << "<p id='social'>" REXML::XPath.each(xml, "social_sites/site/") do |elem| bookmark = REXML::XPath.match(elem, "name/text()").first.value url = REXML::XPath.match(elem, "url/text()").first.value.gsub('{link}', permalink).gsub('{title}', CGI::escape(title)) image = REXML::XPath.match(elem, "img/text()").first.value output << "<span>" output << "<img src='/images/social_bookmark/#{image}' alt='Add to #{bookmark}' /> " output << "<a href='#{url}'>#{bookmark}</a>" output << "</span> " end output << "</p>" end end |
Le contrôleur ci-dessus parcourt un fichier XML. Les lignes 14 à 17 génèrent des liens formatés en HTML à partir des données contenues dans ce dernier. Il est ensuite appelé depuis une vue via un helper render_social_bookmarks.
Helper
1 2 3 4 5 |
module SocialBookmarkHelper def render_social_bookmarks(permalink, title) @controller.parse_config(permalink, title) end end |
Cette méthode fait ce qu’on lui demande, certes, mais elle pose plusieurs problèmes.
Les données ne sont pas réutilisables. Si je souhaite récupérer les données contenues dans le fichier de configuration ailleurs, il va me falloir réécrire une méthode de parsing qui retourne des données non formatées. Donc je dois dupliquer le code. Et le code dupliqué, c’est MAL.
Je suis lié au formatage. Conséquence de mon point précédent, jamais je ne pourrai afficher mes liens autrement, par exemple sous forme de liste à puces, à moins de dupliquer la méthode.
Ce n’est pas le travail du contrôleur de générer du HTML. Tout comme un chef est fait pour cheffer
, le contrôleur est fait pour contrôler, et les vues pour voir. Les helpers font le lien entre les deux en permettant de créer des méthodes réutilisables alliant contrôle et formatage. D’ailleurs, si la méthode s’appelle parse_config, pas parse_config_et_genere_moi_mon_html, ce n’est pas pour rien.
Malheureusement, ce type de comportement est très fréquent, car c’est le plus “naturel” quand on n’a pas l’habitude de travailler en objet. Nous allons donc réécrire tout cela en nettoyant le code et en le rendant beaucoup plus réutilisable.
Commençons par créer un objet SocialItem, lequel contiendra simplement les informations extraites du XML :
- URL
- Name
- Image
Social Item
1 2 3 |
class SocialItem < Struct.new(:url, :name, :image) def to_s; title end end |
Nous allons maintenant parcourir le fichier XML et placer chaque noeud dans un objet SocialItem. Chaque objet SocialItem sera placé dans un tableau, lequel sera retourné à la fin de l’itération. Simple non ?
Itération
1 2 3 4 5 6 7 8 9 10 11 12 13 |
items = [] REXML::XPath.each(xml, "social_sites/site/") do |elem| item = SocialItem.new item.name = REXML::XPath.match(elem, "name/text()").first.value rescue "" item.url = REXML::XPath.match(elem, "url/text()").first.value.gsub('{link}', permalink).gsub('{title}', CGI::escape(title)) rescue "" item.image = REXML::XPath.match(elem, "img/text()").first.value rescue "" items << item end items.sort_by { |item| item.name } |
La méthode ainsi réécrite donne :
Controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
module SocialBookmark require 'rexml/document' class SocialItem < Struct.new(:url, :name, :image) def to_s; title end end def parse_config(permalink, title) xml = REXML::Document.new(File.open("#{RAILS_ROOT}/config/sites_EN.xml")) items = [] REXML::XPath.each(xml, "social_sites/site/") do |elem| item = SocialItem.new item.name = REXML::XPath.match(elem, "name/text()").first.value rescue "" item.url = REXML::XPath.match(elem, "url/text()").first.value.gsub('{link}', permalink).gsub('{title}', CGI::escape(title)) rescue "" item.image = REXML::XPath.match(elem, "img/text()").first.value rescue "" items << item end items.sort_by { |item| item.name } end end |
Et le formatage du code ? Il passe tout simplement dans le helper. Nous parcourons le tableau précédemment retourné, et effectuons le formatage dedans.
Helper
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
module SocialBookmarkHelper def render_social_bookmarks(permalink, title) items = @controller.parse_config(permalink, title) output = "<p id='social'>" items.each do |item| output << "<span>" output << "<a href='#{item.url}'>" output << "<img src='/images/social_bookmark/#{item.image}' alt='Add to #{item.name}' /> " output << "#{item.name}" output << "</a>" output << "</span> " end output << "</p>" end end |
Et voilà. En dissociant contrôle et rendu, que ce soit dans les vues ou dans les helpers, vous évitez la duplication de code, retrouvez plus facilement votre formatage, et rendez vos contrôleurs beaucoup plus facilement testables en vous débarrassant des soucis liés à la mise en forme. Votre application est plus simple à maintenir, et votre code beaucoup plus lisible. Vos successeurs vous en remercient d’avance.
Publié le 23 mars 2009 à 18h50 Publié sous Développement
Mots clés développement, ruby, objet, refactoring, rubyonrails
Si cet article vous a plu, n'hésitez pas à me suivre sur Twitter.
1 commentaire sur Le refactoring pour les nuls : séparer rendu et contrôleurs dans votre application Ruby on Rails »
-
Par sly le 09 avril 2009 à 10h15 :
Trackbacks sur Le refactoring pour les nuls : séparer rendu et contrôleurs dans votre application Ruby on Rails
Les trackbacks sont fermés pour cause de spam.
L'ergonomie web, l'utilisabilité et la qualité des logiciels sont trois grandes passions mises au services de ma profession.
Au fait, ça fait un moment que je voulais réagir mais… c’est étrange comme norme de code, on dirait du J2EE non ?
Pourquoi ne pas utiliser les helpers classiques style
on fait comment pour formater du code ?
content_tag(:p, items.map{|i|
}.join :id => ‘social’)
Certes l’id du “p” se retrouve en bas, mais c’est un peu plus clair non ? Ne serait-ce que parce que c’est indenté, et que toute erreur de fermeture de tag te donne un Syntax Error dans le fichier.
Et surtout c’est plus robuste au changement : en se basant sur les fonctions (par exemple de formulaire) il suffit d’une évolution, d’un plugin ou d’un hack pour profiter instantatément de tout ce qu’on peut avoir de nouveau (par exemple les protections CSRF).