Implémentation d'un pattern MVC par signaux : class Signal

Date : 17.01.2006
Auteur : Frédéric Logier

Attention

Ce code est encore un brouillon, il pourra être amené à évoluer.

De quoi s’agit-il ?

L’objectif est de proposer une classe de communication permettant d’implémenter le motif MVC à destination de tout type de logiciel, sauf web RoR étant là pour ça. J’ai fais le choix d’utiliser une communication par signaux entre les modules, par simplicité. Ce n’est en aucun cas une implémentation rigoureuse du motif MVC, l’objectif étant de se rapprocher de l’esprit. Les personnes intéressées par un MVC conventionnel peuvent étudier Le motif observer.

Comment cela fonctionne ?

Dans un motif MVC, le Modèle et la Vue sont censés communiquer via le Controleur. L’idée est d’utiliser des signaux pour cette communication. Lorsque l’on développe une interface en RG2 on utilise de fait les signaux. En effet lorsque l’on créé un bouton avec Glade on sélectionne un signal parmis la liste des signaux gérés par le widget GtkButton ou un de ses parents. A ce signal on associe une fonction de traitement (callback) qu’il suffira de créer dans la classe appelant le projet Glade. Il est intéressant de savoir que ces signaux ne sont pas spécifiques à l’interface graphique.

La GLib

En effet les signaux sont proposés par la GLib, une bibliothèque de bas niveau utilisée par GTK+. La GLib implémente les GObject qui sont les objets de GTK+ permettant la programmation object en C. La libglade utilise la GLib pour la gestion des signaux avec l’IHM. Il est donc tout a fait possible d’utiliser directement la GLib pour gérer nos propres signaux. Pour cela, cette classe hérite de la GLib::Object et son instance sera un GObject.

La classe Signal

Cette classe a pour objectif de faciliter la gestion de nos signaux. Elle utilise la passerelle GLib de RG2 vers les signaux. Elle permet :

  • la création
  • l’émission
  • la connexion
  • le blocage et le déblocage
  • ...

Cette classe est un singleton ce qui nous garantie de n’avoir qu’une seule instance. Elle pourra être instancié au tout début de notre programme à l’intérieur d’une constante par exemple. Ainsi en passant par notre constante on pourra accéder à notre object signal depuis n’importe où. Exemple :

SIGNAL = Signal.instance

Utilisation

Dans la classe plus bas 2 signaux sont créés, CrefreshNB et VrefreshYP. Chaque signal que l’on voudra créer devra être défini dans la classe Signal. Pour chaque signal une méthode signal_do_nomdusignal devra être créé même si elle n’est pas utilisée. Une normalisation des noms des signaux permet rapidement de connaitre leur destination, leur action et sur quel objet. Le C majuscule signifie Controleur et le V Vue. refresh est l’action du signal enfin les 2 dernières lettres précisent l’object interne à l’application. Le premier signal est donc à destination du Controleur et a pour but de rafraichir un Notebook. Le deuxième à destination de la vue rafraichit un treeview nommé YP. A vous de créer votre norme de nommage si celle-ci ne vous convient pas.

CrefreshNB

Prenons l’exemple de ce signal. On devine que le Modèle va émettre ce signal pour le Controlleur qui émettra à son tour un signal pour la Vue. Le Controleur devra s’y connecter de manière à l’intercepter. En effet un signal n’est pas émit à destination de quelque chose ! Il est tout simplement émit. A charge à l’objet souhaitant le gérer de s’y connecter.

Il paraît évident, mais autant le préciser, qu’il ne faut pas se connecter au moment de l’émission. En effet le Controleur se connecte au signal au moment de son initialisation, de cette manière :


  SIGNAL.connect("CrefreshNB") {
          puts "CrefreshNB" 
          }

Oui, on fourni à la méthode connect une proc. Cette proc est le code que le Controleur veut exécuter. Ainsi le code est bien défini côté Controleur, même s’il est exécuté par l’object Signal via le yield. Le motif MVC est bien respecté, dans l’esprit tout du moins :).

Le Modèle émet le signal comme ceci :

SIGNAL.emit("CrefreshNB", 1, "toto")

On remarquera que lors de l’emission on peut transmettre un nombre de paramètres illimités. Le type de paramètre est précisé dans le signal_new. Ici ce sont 1 Integer et 1 String mais cela pourrait être un Array comme pour le signal VrefreshYP.

Maintenant on souhaite bien évidement utiliser ce paramètre dans le code du Controleur. C’est ici que la magie de Ruby via le yield intervient :

  SIGNAL.connect("CrefreshNB") {|data1,data2|
          puts data1.to_s + " " + data2
          }

La proc affiche : “1 toto” !

Voici la classe et la fin de cette présentation. En attendant un prochain tutoriel présentant une application RG2 utilisant cette classe.

#  signal.rb - 
#  Copyright (C) 2006 Frédéric Logier

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Library General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.

require 'glib2'
require 'singleton'

  class Signal < GLib::Object
    include Singleton

    type_register

    signal_new("CrefreshNB",            # name
               GLib::Signal::RUN_FIRST, # flags
               nil,                     # accumulator (XXX: not supported yet)
               nil,                     # return type (void == nil)
               Integer, String          # parameter types
               )  

    signal_new("VrefreshYP",            # name
               GLib::Signal::RUN_FIRST, # flags
               nil,                     # accumulator (XXX: not supported yet)
               nil,                     # return type (void == nil)
               Array                    # parameter types
               )  

    def initialize
      super(nil)
    end

    def emit(signal_name, *data)
      begin
        signal_emit(signal_name, *data)
      rescue => e
        puts "Alert : signal_emit exception on the signal named " + signal_name + " !" 
        puts "Exception : " + e.to_str
        puts "Backtrace :" 
        puts e.backtrace
      end
    end

    def connect(signal_name)
      begin
        signal_connect(signal_name){|obj, *signal_param|
           yield *signal_param
        }
      rescue => e
        puts "Alert : signal_connect exception on the signal named " + signal_name + " !" 
        puts "Exception : " + e.to_str
        puts "Backtrace :" 
        puts e.backtrace
      end
     end

    def handler_block(handler_id)
      signal_handler_block(handler_id)
    end

    def handler_unblock(handler_id)
      signal_handler_unblock(handler_id)
    end

    #######
    private
    #######

    def signal_do_CrefreshNB(int,string)
    end
    def signal_do_VrefreshYP(array)
    end

  end