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

Ruby on RailsMé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 rendersocialbookmarks.

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 parseconfig, pas parseconfigetgeneremoimon_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 et Labels développement, ruby, refactoring, objet, rubyonrails

À propos

Frédéric de Villamil

Je m'appelle Frédéric de Villamil, et quand je ne déploie pas ma mauvaise humeur et ma mauvaise foi sur le Web, je suis un super héros chargé de sauver le monde. Vous pouvez me suivre sur Twitter.

  1. 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

Afin de maintenir le niveau global de ce site, les commentaires font l'objet d'une politique de modération qualitative basée sur des critères non écrits et totalement subjectifs, donc injustes.

Les commentaires écrits en langage SMS, inutiles, déplacés, injurieux ou relevant du spam seront systématiquement supprimés sans avertissement préalable.

Les trackbacks sont fermés pour cause de spam.