30-08-2009, 11:13 PM
(Modification du message : 10-10-2009, 09:55 AM par Sephi-Chan.)
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.
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.
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= !
La vue d'édition (dans la partie administration) :
Et une vue d'affichage (la vue partielle pour une annonce) :
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
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,
ource => 'User',
:conditions => [ 'participations.is_trainer = ?', false ]
has_many :trainers, :through => :participations,
ource => '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 => [ how, :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 pan, l(announce.created_at), :class => :created_at %>
<%= content_tag pan, l(announce.updated_at), :class => :updated_at if announce.updated_at %>
<%= content_tag pan, 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