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.

Perry the Platypus wants you to subscribe now! Even if you don't visit my site on a regular basis, you can get the latest posts delivered to you for free via Email: