JeuWeb - Crée ton jeu par navigateur
[Ruby on Rails] Les modules, un bon moyen de factoriser son code - Version imprimable

+- JeuWeb - Crée ton jeu par navigateur (https://jeuweb.org)
+-- Forum : Discussions, Aide, Ressources... (https://jeuweb.org/forumdisplay.php?fid=38)
+--- Forum : Programmation, infrastructure (https://jeuweb.org/forumdisplay.php?fid=51)
+--- Sujet : [Ruby on Rails] Les modules, un bon moyen de factoriser son code (/showthread.php?tid=4315)

Pages : 1 2


[Ruby on Rails] Les modules, un bon moyen de factoriser son code - Sephi-Chan - 30-08-2009

Bonsoir,

Je profite d'être dessus pour vous présenter un excellente fonctionnalité offerte par Ruby (et par extension, Ruby on Rails) : les modules. Je marque ce sujet avec [Ruby on Rails] car mon article utilise ça appliqué à une application Rails, mais c'est bien une fonctionnalité native de Ruby.

Les modules sont des conteneurs qui peuvent inclure des classes et des méthodes. On ne peut pas les instancier mais on peut les inclure dans une classe : elle acquiert alors les méthodes définies dans le module.

Je vais vous présenter un exemple d'utilisation d'un module pour factoriser le code et ainsi suivre le principe DRY (Don't Repeat Yourself).

Dans l'application, il existe des modèles pour les annonces (Announce) et les ateliers (Workgroup). Ces ressources peuvent toutes les deux être publiées : elles ont un état de brouillon (indiqué par la colonne is_draft, qui contient 1 si la ressource est un brouillon, 0 si elle est publiée) et une date de publication (si elles sont publiées).

On va donc définir un module qui contient tout le nécessaire pour que la ressource soit publiable.


# The including model is now publishable.
# The model table must have two columns :
# - is_draft:boolean
# - published_at:datetime
module Publishable

def self.included(base)
base.named_scope :published, :conditions => [ 'is_draft = ?', false ]
base.named_scope :unpublished, :conditions => [ 'is_draft = ?', true ]
end

# Set the flag is_draft to true or false.
# Modify the date of publication to the
# current time when resource is published.
def is_draft=(value)
if value == "1"
self.write_attribute(:is_draft, true)
self.published_at = nil
else
self.write_attribute(:is_draft, false)
self.published_at = Time.new
end
end

# Return true if the announce is published.
# Return false else.
def is_published?
return true if published_at
false
end

end

La méthode de classe included (en fait, included est une méthode de la classe Module) permet d'appliquer des modification à la classe (et pas seulement à l'instance) qui inclut le module. En l'occurrence, on définit des named scope.

Les named scope (portées nommées) permettent à Active Record de lancer des requêtes comme : Announce.published.all pour récupérer toutes les annonces publiées. Ainsi, quand un utilisateur lambda cherche à atteindre une annonce (à l'URL http://application.com/announce/1-une-annonce-de-fou), le contrôleur lance la requête : Announce.published.find(params[:id]). On est ainsi sûr que l'utilisateur ne peut pas accéder à une ressource non publiée.

Ensuite, on personnalise l'accesseur qui permet d'affecter une valeur à l'attribut is_draft de notre ressource.
L'accesseur affecte — en plus de son rôle normal — une date de publication égale à l'instant présent quand on publie la ressource.
Cette date de publication est effacée (et donc NULL en base de données) si on (re)met la ressource à l'état de brouillon.

Notez l'utilisation de write_attribute(:is_draft, true) pour que la méthode ne s'appelle pas elle-même indéfiniment quand on fait @announce.is_draft = true.

Enfin, on définit une méthode is_published? qui retourne true si la ressource est publiée et false sinon.


Voilà donc pour les comportements générique de notre module. Plus qu'à définir nos modèles et inclure notre module Publishable dedans. J'épure volontairement les classes des règles de validations, etc. afin de rester assez clair.


class Workgroup < ActiveRecord::Base

include Publishable

@@per_page = 20
cattr_reader :per_page

has_many :participations
has_many :users, :through => :participations

has_many :participants, :through => :participations,
Confusedource => 'User',
:conditions => [ 'participations.is_trainer = ?', false ]

has_many :trainers, :through => :participations,
Confusedource => 'User',
:conditions => [ 'participations.is_trainer = ?', true ]

end


class Announce < ActiveRecord::Base

include Publishable

@@per_page = 20
cattr_reader :per_page

has_many :authorings
has_many :users, :through => :authorings

end

Comme vous pouvez le voir, l'inclusion du module est toute bête !

Voyons le contrôleur de l'une de nos ressources pour comprendre les intérêts de la chose : on utilise de manière transparente les méthodes save et update_attributes puisqu'elles feront appel à notre accesseur is_draft= !


class Administration::AnnouncesController < Administration::AdministrationController

before_filter :find_announce, :only => [ Confusedhow, :edit, :update, :destroy ]

def index
@announces = Announce.paginate(:page => params[:page])
end

def show
end

def new
@announce = Announce.new
end

def create
@announce = Announce.new(params[:announce])

respond_to do |format|
if @announce.save
@announce.users << current_user
format.html { redirect_to [ :administration, @announce ] }
else
format.html { render :action => 'new' }
format.js
end
end
end

def edit
end

def update
respond_to do |format|
if @announce.update_attributes(params[:announce])
@announce.users << current_user unless @announce.users.include?(current_user)
format.html { redirect_to [ :administration, @announce ] }
else
format.html { render :action => 'edit' }
format.js
end
end
end


protected

def find_announce
@announce = Announce.find(params[:id])
end

end

La vue d'édition (dans la partie administration) :


<h2>Modifier une annonce</h2>

<% form_for [ :administration, @announce ] do |form| %>

<div class="field title">
<div><%= form.label :title, 'Titre' %></div>
<div><%= form.text_field :title %></div>
<div class="error"><%= form.error_message_on :title %></div>
</div>

<div class="field content">
<div><%= form.label :content, 'Contenu' %></div>
<div><%= form.text_area :content %></div>
<div class="error"><%= form.error_message_on :content %></div>
</div>

<div class="field draft">
<div>
<%= form.check_box :is_draft %>
<%= form.label :is_draft, 'Brouillon' %>
</div>
<div class="information">Les brouillons ne sont pas publiés directement sur le site.</div>
</div>

<div class="field submit">
<%= form.submit "Modifier l'annonce" %>
</div>

<% end %>

Et une vue d'affichage (la vue partielle pour une annonce) :


<% content_tag_for :div, announce do %>
<h2><%= link_to_unless_current(h(announce.title), announce)%></h2>
<div class="dates">
<%= content_tag Confusedpan, l(announce.created_at), :class => :created_at %>
<%= content_tag Confusedpan, l(announce.updated_at), :class => :updated_at if announce.updated_at %>
<%= content_tag Confusedpan, l(announce.published_at), :class => :published_at if announce.published_at %>
</div>
<div class="authors"><%= links_for_users(announce.users) %></div>
<div class="content">
<%= simple_format(h(announce.content)) %>
</div>
<% end %>


Voilà, j'espère avoir été clair sur l'utilisation des modules.
Une classe peut inclure plusieurs modules (d'ailleurs, il y aura sûrement un module Commentable).

N'hésitez pas à poser vos questions si vous souhaitez en savoir plus sur un point particulier.


Sephi-Chan


RE: [Ruby] Les modules, un bon moyen de factoriser son code - NicoMSEvent - 31-08-2009

Ce que j'aime bien, avec tes messages, c'est que je comprends jamais directement l'intérêt ou la portée...
Il me faut bien 6 mois pour m'y intéresser (et surtout pour comprendre. Je comprends vite, mais faut m'expliquer longtemps => blond inside), et 6 mois après j'arrive a la conclusion que je vais y adhérer, et que c'est la technologie du futur... :p

Bon ben, rdv dans 6 mois sur ce post? ^^ (ça fait quasi 6 mois que tu as posté les premières infos sur ROR, va falloir que je m'y mette alors :p)


RE: [Ruby] Les modules, un bon moyen de factoriser son code - Sephi-Chan - 31-08-2009

Tu ne saisi pas l'intérêt de ça ? Je peux comprendre, je ne crois pas qu'il existe d'équivalence en PHP (ni dans pas mal d'autre langage).

Comme tu peux le voir, ça ressemble à une interface (sauf qu'on peut y implémenter du code) : quand j'inclus mon module Publishable dans un modèle, il gère la publication de manière transparente. C'est assez énorme.

En entreprise, j'ai fais ça pour attacher des medias à des ressources de différente nature : en leur faisant inclure Mediable, elles géraient ça, et sans passer par l'héritage (puisqu'elles héritaient déjà d'une classe) et sans duplication de code !

Toujours pas ? Smile


Sephi-Chan


RE: [Ruby] Les modules, un bon moyen de factoriser son code - pascal - 31-08-2009

ça existe dans des ORM tels que Propel en PHP, ça s'appelle les behaviors / les comportements.

C'est pratique pour gérer plein de trucs répétitifs, genre :
- ajouter des mots-clé à des articles, annonces, profils...
- ajouter des commentaires aux mêmes
- permettre de signaler / modérer n'importe quelle donnée sur un site

Avec des exemples plus parlants, ça pourrait être plus explicite Smile

A+

Pascal


RE: [Ruby] Les modules, un bon moyen de factoriser son code - Sephi-Chan - 31-08-2009

Ouais mais cette notion d'inclusion de module est inconnue en PHP. Vraiment pas de chance pour les développeurs PHP. Big Grin
Les modules servent dans le cas que j'ai indiqué, mais ça n'est qu'un de leur domaine d'utilisation, il y en a d'autres.

C'est effectivement très adapté aux choses polymorphe (ici, on se fout d'avoir affaire à des modèles Announce ou Workgroup) et répétitive : un module Taggable pour ce qui peut recevoir des tags, un module Commentable pour ce qui peut être commenté, etc.


Sephi-Chan


RE: [Ruby] Les modules, un bon moyen de factoriser son code - Shao - 01-09-2009

Le système semble intéressant, mais il faut faire attention avec tout ce qui est générique.
Justement je me posais une question :
Si maintenant dans tes annonces on te demande de gérer une liste d'états de publication ( mardi cette annonce était en mode brouillon et mercredi elle ne l'était plus, puis finalement jeudi elle le redevient parce que l'annonce était finalement pas assez convaincante ), par contre on ne veut pas que ce système d'états soit disponible dans les Ateliers. Est ce qu'il faut étendre le module actuelle pour l'améliorer ? Rajouter un nouveau module qui complète celui là ?

En fait je veux savoir s'il y a moyen simple de compléter les modules sans impacter les utilisateurs de ces derniers ?


RE: [Ruby] Les modules, un bon moyen de factoriser son code - Ter Rowan - 02-09-2009

je me gourre peut être mais

si j'étais en C++, j'appelerais ça du multi héritage (voire mono si ta classe n'a pas de classe mère)

en php, est ce qu'il ne suffit pas de jouer avec un include/require pour géré la même chose ?

genre :

le fichier publishable :
Code PHP :
<?php 
private $published

function is_published()
{
return
$this->published;
}

et dans tes classes

Code PHP :
<?php 
class Workgroup /// avec l'extension qui va bien si besoin, ainsi que les interfaces
{

include(
publishable.php);

function
__construct(.....)
{

}

function
toto()
{
if (
$this->is_published() )
{

}
}
}



RE: [Ruby] Les modules, un bon moyen de factoriser son code - Sephi-Chan - 02-09-2009

Un module ne surcharge donc pas les éléments qui existent déjà.

Documentation Ruby de la classe Module#append_features a écrit :When this module is included in another, Ruby calls append_features in this module, passing it the receiving module in mod. Ruby’s default implementation is to add the constants, methods, and module variables of this module to mod if this module has not already been added to mod or one of its ancestors. See also Module#include.

Partant de ça, tu peux donc définir des méthodes particulières dans ta classe de base puisqu'elles ne seront pas remplacées par celle du module. Ils ont pensé à tout. Smile

C'est donc assez différent de l'héritage multiple. En fait, mais ça y ressemble.

Concernant l'adaptation en PHP, je ne sais pas si ça fonctionne, à voir.


Sephi-Chan


RE: [Ruby] Les modules, un bon moyen de factoriser son code - Ter Rowan - 02-09-2009

mmm en lisant ce que tu dis là,et après avoir découvert les méthodes php d'analyse des classes php (je trouve ca formidable, ca se trouve c'est "normal" pour vous ^^) je me dis qu'on doit être capable d'encapsuler un truc la dessus tel que décrit Pascal.

c'est fou ce qu'on arrive à faire quand même comme "abstraction". Intellectuellement c'est super je trouve


RE: [Ruby] Les modules, un bon moyen de factoriser son code - pascal - 02-09-2009

(02-09-2009, 02:19 PM)Ter Rowan a écrit : mmm en lisant ce que tu dis là,(...) je me dis qu'on doit être capable d'encapsuler un truc la dessus tel que décrit Pascal.

On peut le faire en PHP, je confirme.

A+

Pascal