Le refactoring pour les nuls : séparer rendu et contrôleurs dans votre application Ruby on Rails

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

Mots clés développement, ruby, objet, refactoring, rubyonrails

Si cet article vous a plu, n'hésitez pas à me suivre sur Twitter.

  1. Avatar

    Par sly le 09 avril 2009 à 10h15 :


    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|

    content_tag(:span, link_to(image_tag('social_bookmark/#{i.image}', :alt => "Add to #{i.name}") + ' ' + i.name, i.url) )
    

    }.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).

Réagir à Le refactoring pour les nuls : séparer rendu et contrôleurs dans votre application Ruby on Rails

Merci de vous exprimer dans un français correct. Les commentaires déplacés, injurieux et le spam seront supprimés.

Les trackbacks sont fermés pour cause de spam.


Abonnez-vous au flux RSS et suivez les nouveaux articles du site Suivez-moi sur Twitter