Factorisation de code

Factorisation de code - Ruby/Rails - Programmation

Marsh Posté le 20-02-2009 à 14:09:42    

Salut,
 
Une question pour un truc qui marche pour une fois \o/
 
C'est à propos, comme le titre l'indique, de factorisation de code d'une appli Rails. L'idée, c'est que j'ai 3 modèles nestés sur une ressources mère, j'aimerais vos avis sur plusieurs points.
 
Mettons qu'un User has_many Foo, Bar et Baz.
 
1. Couche modèle
 
Foo, Bar et Baz ont en commun un champ datetime, et le fait d'avoir chacun un autre champ, mais de type différent (appelons le value dans tous les cas) : un float, un integer et un string.
J'ai regardé du côté de l'héritage de tables multiples, mais ça parrait overkill pour mon cas. Je prend vos avis sur cette partie, même si c'est clairement celle qui me dérange le moins. Je pourrais aussi créer un unique modèle, mais je vois pas comment faire ça proprement vis à vis des contraintes données plus haut (d'autant que les modèles, même si ils dépendent d'un utilisateur, font l'objet de méthodes, statistiques par exemple, qui impliquent de les distinguer).
 
2. Couche contrôleur
 
Mon souci ici, c'est que le code des actions CRUD dans les contrôleurs Foos, Bars et Bazs est strictement identique, aux détails prêts du nommage des éléments. Genre (pour create) :
 

Code :
  1. @foo = @user.foos.build(params[:foo])


 
et
 

Code :
  1. @bar = @user.bars.build(params[:bar])


 
Pas gros de différence, vous voyez le genre. J'ai bien imaginé un mixin qui implémente les méthodes CRUD, mais autant les nom de variables peuvent être générifiées, autant pour les symboles et helpers (@user.foos par exemple) je suis perplexe.
 
De même que précédemment, si j'avais un unique modèle, ça règlerais le problème, j'en suis conscient mais pas réellement possible.
 
3. Couche vue
 
La cerise sur tout ça, c'est que c'est internationalisé. Donc de la même façon, pour factoriser les vues sans introduire de notions d'affichage dans, disons le modèle, c'est la galère.
 
Dans une des vues de mon user, j'ai 3 div qui correspondent aux "résumé" des Foo, Bar et baz, avec un form pour chaque qui permet d'en ajouter un.
 
La seule méthode pour factoriser le code que j'ai trouvée, c'est un partiel un peu crade sur lequel je passe une tapée d'arguments, aussi bien pour passer les vraies variables que les données de présentation associées (essentiellement des symboles pour l'I18n). L'appel du partiel en question est un truc de ce genre :
 

Code :
  1. <%= render :partial => "user_entry", :locals => {
  2.    :div_id => "user_foos",
  3.    :data_title => :foo_title,
  4.    :last_entries => @last_foos,
  5.    :last_entries_title => :last_foos,
  6.    :data_unit => :foo_unit,
  7.    :new_entry => @new_foo,
  8.    :new_entry_title => :new_foo,
  9.    :entry_value_label => :value_label,
  10.    :entry_value_size => 3,
  11.    :datetime_id => "foo_datetime" }
  12. %>


 
Vous en conviendrez, c'est moche...
 
 
Je suis preneur de vos idées \o/

Reply

Marsh Posté le 20-02-2009 à 14:09:42   

Reply

Marsh Posté le 23-02-2009 à 10:12:56    

Le week-end passé,
et son lot de boissons,
ne m'a pas apporté
de meilleur solution.
 
(up)

Reply

Marsh Posté le 24-02-2009 à 14:24:27    

1. Couche modèle: en fait tu veux réécrire tes 3 modèles simplement parce qu'ils ont "apparemment" un attribut du même nom. La question à se poser c'est: "est-ce que cet attribut qui semble redondant cache derrière le même comportement?". Parce qu'une date dans un modèle, ça peut être une date de fin, alors que dans l'autre c'est une date de début, donc ça peut être dangereux de vouloir refactorer les 2 modèles alors qu'ils traduisent des comportement totalement différents.
 
Perso j'ai eu récemment un cas de figure similaire, et j'ai mergé 3 modèles en un, finalement je m'en mords les doigts.
 
Sinon google "rails concerned_with" tu devrais trouver des idées, et "rails composed_of".
 
2. Couche controlleur: regarde make_resourceful ou resource_controller, l'un est maintenu et pas l'autre je sais plus lequel. Pour faire un blog en 15 minutes ça peut aider, mais quand l'appli va grossir et les controlleurs commencer à se différencier les uns des autres je sais pas si ça va plus t'embêter qu'autre chose. A voir.
 
3. Views: dans les variables que tu passes à ton partial, elles peuvent pas être regroupées dans un seul objet qui aurait tous ces attributs? C'est pas évident de visualiser ce qui passe avec des foo et des bar. Tu peux pas donner les vrais noms ou c'est encore avec des penises et des anuses? Et je comprends pas pourquoi tu passes les symboles de l'i18n, pourquoi ne pas les hard-coder dans le partial?
 
 
Sinon hésite pas à poster dans Blabla@Rails, l'activité est de toute façon faible et avec les drapeaux c'est plus facile à suivre qu'une cat complète ;)

Reply

Marsh Posté le 25-02-2009 à 10:03:58    

On va faire simple, je vais poster plein de code. Je me lance aujourd'hui dans l'analogie Weight Watcher \o/
 
Mettons que mes utilisateurs puissent entrer des repas, des shoots de dope qui fait maigrir et des commentaires. L'idée est de pouvoir retrouver, à partir de l'une des entrée, ce que l'utilisateur a fait autour (je brode mon histoire en live, c'est un vrai bonheur).
 
Genre Monique, 43 ans, a entré un repas à 700 g de graisses (valeur entière) le 06/06/2006 à 12h40, avec quelques heures après une injection de SlimDope © de 0.85 unités (valeur flottante). Dans l'après-midi, elle à faire 1 heure de sport (en fait non, mais elle entre quand même la donnée pour se donner bonne conscience, elle doit croire que l'admin du site surveille et juge ses inscrits, enfin bref), elle entre donc le commentaire "je suis contente, j'ai fait 1 heure de sport" (un string donc).
 
L'ensemble de ces différentes entrées est accompagnée d'un datetime, car un utilisateur peut entrer une action qui s'est déroulée dans le passé (donc pas d'exploitation possible des champs created_at et updated_at dans ce cas).
 
Les modèles ressemble à ça :
 

Code :
  1. class User < ActiveRecord::Base
  2.  has_many :meals
  3.  has_many :dopes
  4.  has_many :comments
  5. end


 

Code :
  1. class Meal < ActiveRecord::Base
  2.  belongs_to :user
  3.  validates_presence_of :lipids, :datetime
  4.  alias_attribute :value, :lipids
  5. end


 

Code :
  1. class Dope < ActiveRecord::Base
  2.  belongs_to :user
  3.  validates_presence_of :units, :datetime
  4.  alias_attribute :value, :units
  5. end


 

Code :
  1. class Comment < ActiveRecord::Base
  2.  belongs_to :user
  3.  validates_presence_of :text, :datetime
  4.  alias_attribute :value, :text
  5. end


 
Les alias_attribute sont là comme ébauche de début de tentative d'unification de l'ensemble. Ça, c'est pour le côté modèle. Je pourrais effectivement garder les trois, ça me pose pas de soucis. En fait, la question de factorisation s'est posée parce que cette structure (qui ne me dérange pas dans le modèle) occasionne pas mal de duplication dans les autres couches.
 
Prenons maintenant l'exemple de l'écran de contrôle Weight Watcher d'un utilisateur, qui regroupe dans une page générale ses infos perso, ainsi que les dernières entrées (des 3 types précédents) et la possibilités d'en ajouter un rapidement. En gros, sur la même page, 3 div (repas, dopes et commentaires) avec les 3 derniers items pour chaque et un formulaire d'ajout. Du point de vue de la vue donc (...), rappelons que le bouzin est internationalisé (même si je sens que c'est premature optimization et que ça va partir à la trappe dans pas longtemps, prenons le cas quand même).
 
Si je ne factorise pas, je me retrouve avec :
 
Pour les repas :
 

Code :
  1. <div id="meals">
  2.    
  3.    <h2><%= t(:meals_title) %></h2>
  4.    
  5.    <% unless @last_meals.empty? %>
  6.    <div class="last_values">
  7.        <h3><%= t(:last_meals_title) %></h3>
  8.        <% @last_meals.each do |meal| %>
  9.        <p>
  10.            <%= meal.value.to_s + "&nbsp;#{t(:meal_unit)}, " + l(meal.datetime, :format => :custom).downcase %>
  11.            <%= button_to "Delete", user_meal_path(@user, meal), :method => :delete %>
  12.        </p>
  13.        <% end %>
  14.    </div>
  15.    <% end %>
  16.    
  17.    <% form_for [@user, @new_meal] do |f| %>
  18.        <fieldset><legend><%= t(new_meal_title) %></legend>
  19.            <p><%= f.label :value, t(meal_value_label) %>
  20.            <%= f.text_field :value, :size => 3 %><%= "&nbsp;#{t(meal_unit)}" %></p>
  21.            <p><%= f.label :datetime, t(:datetime_label) %>
  22.            <span id="<%= datetime_id %>"><%= f.datetime_select :datetime %></span></p>
  23.            <%= submit_tag t(:add_button_label) %>
  24.        </fieldset>
  25.    <% end %>
  26.    
  27. </div>


 
Et les deux même partiels pour les dopes et commentaires. En faisant un partiel commun avec passage de paramètres (voir mon premier post, l'appel du partiel "user_entry" correspond au code précédent factorisé pour les 3 types), je me retrouve avec une tapée d'arguments, et je trouve ça moche.
 
Enfin viens la couche controleur. J'ai regardé tes liens, ça semble être une bonne piste pour mon problème, même si je crains effectivement que le cas particulier (qui arrive toujours un jour ou l'autre, le fourbe) vienne compliquer les choses.
 
Merci de t'attarder sur mes questions en tout cas. Je suis assez pour l'utilisation de blabla, mais dans le cas où je dois pondre un roman d'explications, je préfère quand même garder ça dans un thread séparé (ne serait-ce que si quelqu'un rencontre le soucis un jour).
 
 

Reply

Marsh Posté le 03-03-2009 à 15:30:52    

Je continue  [:mossieurpropre]  
 
Je voudrais quand même être sûr que mon soucis ne vienne pas de la base de tout, et je ne parle pas du big bang mais bien de mes modèles.
 
Reprenons le magnifique exemple de l'appli Weight Watcher. Un User has_many Meals, Dopes et Comments, qui ont tous en commun de présenter en base un datetime et une référence vers user_id, forcément. Le seul truc qui les fait différer, outre leurs noms, c'est le type de valeur qu'ils prennent, respectivement int, float et string. Imaginons donc en terme d'objet que mes modèles soient tous des instances d'un type plus générique, Shakespeare ne m'en voudra pas si je le (le type, pas Shakespeare) nomme Entry. Un User has_many Entries, et une Entry est soit un Meal, un Dope ou un Comment. J'en vois qui dorment dans le fond.
 
J'imagine donc l'héritage de mes tables comme ça :
 
entries
-> user_id
-> datetime
-> entry_id
 
meals
-> value (int)
-> entry_id
 
dopes
-> value (float)
-> entry_id
 
comments
-> value (string)
-> entry_id
 
 
Le souci est qu'ici, on à une relation 1 à 1 entre entry et l'un des sous types, et ça, j'ai pas l'impression que ça soit géré par Rails. Single Inheritance ne gère qu'une seule table (incroyable non ?), et je ne veux pas qu'un modèle hérite de plusieurs mais l'inverse (plusieurs modèles / tables héritent d'une seule). Multiple Inheritance n'a donc pas l'air de convenir sur les exemples que j'ai rencontré, mais je ne demande qu'à me tromper.
 
Vais-je soulever autant de remarques que la dernière fois ?
 

Reply

Marsh Posté le 03-03-2009 à 18:11:00    

Tu devrais pouvoir t'en tirer avec has_one et/ou belongs_to, du coup tu ferais un truc du genre: entry.meal ou inversemment, meal.entry à voir ce qui colle le mieux.
 
Il me semble avoir vu un truc similaire avec Asset ou Media si on veut lui donner un type et des méthodes spécifiques, donc si on prend Media par exemple, il y a un sous-modèle associé Music, Video, Picture, etc. C'est bien ce que tu veux faire.
 
Google voir si tu trouves des trucs, et de mon côté je vais aussi y jeter un oeil je suis en façe du même dilemme.

Reply

Marsh Posté le 03-03-2009 à 18:35:42    

Oué, je réfléchissais à ça.
 
L'idée ici, c'est que je ne vois pas comment définir mes modèles.
 
Entry has_one (ou belongs_to) quoi ? Laquelle de mes sous-classes ? C'est ici que je bloque.
 
Merci de ton intérêt en tout cas ! Je jette un oeil demain sur Asset / Media demain.

Reply

Marsh Posté le 04-03-2009 à 13:43:07    

La nouvelle idée du jour, c'est finalement de partir sur Single Table Inheritance.
 
Disclaimer !
 

Spoiler :

La solution ci-dessous est quelque chose comme très crade.


 
Ici, on a un model Entry, duquel héritent mes modèles Dopes et tout et tout... Mon soucis venait du champ value, qui était de type différent selon l'implé (Meal => int, Dope => float, Comment => string).
 
C'est là que la solution crade entre en scène. Le champ value pour la table entries est un string. elle comporte également un champ type (ici 'meal', 'dope' et 'comment'), requis pour l'utilisation de Single Table Inheritance. Si value est un string partout, celà signifie que je dois convertir (et contrôler le format) les float et int dans le modèle. J'avais prévenu, c'est pas beau.
 
@igarimasho: Asset et Media, j'ai dû mal chercher, je trouve pas de piste. Si tu peux m'en dire plus (si la solution te semble toujours potable)...

Reply

Marsh Posté le 04-03-2009 à 14:21:36    

Je pense avoir retrouvé une piste dans le livre "Ruby for Rails" paru chez Manning.

Reply

Sujets relatifs:

Leave a Replay

Make sure you enter the(*)required information where indicate.HTML code is not allowed