Odelia>Technologies

Appliquer des règles métiers avec Groovy et l'annotation @Bindable

|

Cet article explore une voix possible de l'utilisation du puissant langage Groovy pour la définition de règles métiers, et montre comment l'annotation Groovy @Bindable peut servir à automatiser leurs applications, par des changements de valeurs de propriétés.

Nous examinons également la possibilité d'exprimer ces règles de manière séparée au moyen d'un Domain Specific Language, et avons aussi recours aux mixins Groovy.

Pour l'implémentation d'un moteur de règles complet compatible JSR 94 Java Rule Engine API, vous pouvez toujours examiner le projet GroovyRules, le but de notre article étant avant tout pédagogique.
Fonctionnant avec Groovy 1.6, le code source de cet article est fourni en pièces jointes et ne contient que peu de code de contrôle.

Définir une règle

Ce qui nous appellerons règle dans cet article est tout simplement la définition d'une condition et d'une action ; lors de l'application de cette règle, l'action associée ne sera exécutée que si la condition est vérifiée.
La classe Groovy Rule permet de créer des règles de manière souple du fait du recours aux Closures Groovy :

class Rule {
    def condition = { source -> true }
    def action = { source -> }
    def run(source) {
        if (condition(source)) action(source)
    }
}

Comme vous pouvez le constater dans le code ci-dessus, la condition et l'action de la règle sont de type Closure avec des valeurs par défaut : pour la condition, celle-ci est vérifiée par défaut, tandis que l'action de fait rien par défaut.
La méthode run permet d'exécuter l'action si la condition est vérifiée.

Déclarons une première règle sans condition, c'est-à-dire sans initialiser la propriété condition d'un objet Rule :

// Règle sans condition
def rule = new Rule(action: { source -> 2*source })
assert rule.run(1) == 2

L'utilisation de l'instruction assert dans cet exemple, ainsi que dans les exemples qui suivent, permet d'indiquer quel est le résultat attendu. Ici, après être instanciée, nous appliquons la règle rule en invoquant sa méthode run.


Voici un second exemple, qui comporte l'affectation de la condition de la règle :

// Règle avec condition
def rule = new Rule(condition: { it > 2 }, action: { source -> 2*source })
assert rule.run(3) == 6
assert rule.run(1) == null
assert rule.action(4) == 8

L'annotation Groovy @Bindable et la classe RuleListener

Dans son article What's New in Groovy 1.6, Guillaume Laforge décrit en particulier l'annotation @Bindable qui est très utile aux développements avec Swing.
Appliquée à une classe ou à une propriété, cette annotation permet d'injecter automatiquement le code technique qui permettra à des écouteurs de surveiller les changements de valeurs de propriétés.

Définissons un nouveau type de règle qui soit un écouteur et qui soit capable d'exécuter son action lorsqu'un changement de valeur de propriété est reçu :

class RuleListener extends Rule implements PropertyChangeListener {
    void propertyChange(PropertyChangeEvent evt) {
        run(evt.source)
    }
}

Ainsi, la classe RuleListener offre un moyen automatique d'application de règles !


Donnons-en maintenant un exemple concret au travers d'une application hypothétique qui traite des commandes ; chaque commande possède en particulier un état (en cours, validée, complète, etc.), ainsi qu'un panier d'article. Simplifiée, une telle classe pourrait se présenter de cette manière :

enum StatutCommande {
    EN_COURS,
    VALIDEE
}

class Commande {
    @Bindable
    StatutCommande statut = StatutCommande.EN_COURS
    @Bindable
    def commandeValidee

    def panier = [
        [code: 1, label: 'Article A', prix: 10.0],
        [code: 2, label: 'Article B', prix: 21.0] ] as ObservableList

    def total

    def calculerSousTotal() {
        panier.prix.sum()
    }

    def calculerFraisDePort // algorithme variable
}

Cette classe possède deux propriétés, statut et commandeValidee, annotées avec @Bindable, susceptibles donc d'être surveillées. La propriété panier est également observable du fait de l'utilisation du type ObservableList (nous y reviendrons plus loin).

Avant de donner un exemple d'utilisation de la classe RuleListener, voyons comment définir le mode de calcul des frais de port en affectant la propriété calculerFraisDePort avec l'action d'une règle de type Rule :

def commande = new Commande()

// Changement de calcul des frais de port
def rule = new Rule(action: { cmd -> cmd.calculerFraisDePort = { 5 + cmd.panier.size() *2 } })
rule.run(commande)
assert commande.calculerFraisDePort() == 9

L'intérêt de cette approche est que l'on peut à tout moment changer le mode de calcul des frais de port, et que l'on pourrait également charger la définition de la règle à partir d'un fichier externe comme nous le verrons par la suite.


Définissons maintenant une règle de type RuleListener qui soit appliquée automatiquement lorsque le statut de la commande passe de la valeur StatutCommande.EN_COURS à la valeur StatutCommande.VALIDEE, de manière à recalculer le total de la commande :

def rule = new RuleListener(
    condition: { cmd -> cmd.statut == StatutCommande.VALIDEE },
    action: { cmd -> cmd.with { total = calculerSousTotal() + calculerFraisDePort() } }
)
commande.addPropertyChangeListener('statut', rule)

// Ceci déclenche l'exécution de la règle de calcul du total
commande.statut = StatutCommande.VALIDEE
assert commande.total == 31 + 9

C'est l'appel de la méthode injectée addPropertyChangeListener (grâce à l'annotation @Bindable) de la classe Commande avec le nom de la propriété à surveiller, ainsi que l'objet écouteur, qui permettra l'application de la règle à l'exécution. Comme le montre l'assertion, après la mise à jour du statut de la commande, le total de la commande aura été recalculé.

Il peut être intéressant de recalculer automatiquement le total de la commande lorsque son panier est modifié par l'ajout ou le retrait d'un article. Du fait que la propriété panier de la commande soit de type ObservableList, nous pouvons également surveiller un changement éventuel dans la liste des articles grâce à la nouvelle règle cartRule :

def cartRule = new RuleListener(
    action: { panier -> rule.run(commande) }
)
commande.panier.addPropertyChangeListener(cartRule)
commande.panier << [code: 3, label: 'Article C', prix: 5.0]
assert commande.total == 36 + 11

Noter la réutilisation que nous faisons de la règle précédemment définie, dans la définition de l'action de la règle cartRule. A la suite de l'ajout d'un article dans le panier, la règle cartRule est appliquée.

Externaliser les règles avec un DSL

Il est possible de décrire un ensemble de règles au moyen d'un Domain Specific Language et que l'on peut placer dans des fichiers externes.
Un tel langage permet de définir des règles de manière plus simple et est normalement destiné à des experts d'un domaine ciblé par le langage. Notre langage de définition de règle est relativement simple ; voici comment une règle est définie avec le langage Groovy :

rule {
    sourceProperty = 'statut'
    condition = { true }
    action = { source -> println 'Salut depuis DSL ! Statut : ' + source.statut }
}

Chaque définition de règle débute par le mot-clé rule (techniquement il s'agit de l'exécution de la Closure rule prenant comme paramètre une Closure...) suivi d'accolades contenant trois affectations : vous reconnaissez celles qui vont définir les propriétés condition et action de la règle, tandis que l'affectation de la propriété sourceProperty permet d'indiquer quelle va être le nom de la propriété d'objet à surveiller.
Nous avons créé la classe RuleListenerBuilder chargée d'analyser et d'instancier un ensemble de règles définies dans notre DSL, que ces règles soient placées dans un fichier ou bien dans une chaîne de caractères ; en voici un exemple avec une chaîne de caractères :

def builder = new RuleListenerBuilder(commande)
builder.addRules(
"""rule {
       sourceProperty = 'statut'
       condition = { true }
       action = { source -> println 'Salut depuis DSL ! Statut : ' + source.statut }
   }
   rule {
       sourceProperty = 'commandeValidee'
       condition = { true }
       action = { source -> println 'Evénement commandeValidee émis !' }
}"
"")


Dans cet exemple, les propriétés à surveiller sont statut et commandeValidee de l'object commande passé en argument du constructeur de RuleListernerBuilder.
Si vous examinez le code source de cette classe, vous verriez que la méthode addRules utilise un objet GroovyShell pour analyser ce qui est passé en argument ; le type de cet argument peut être tout ce qui est permis par la méthode evaluate de la classe GroovyShell, par exemple un fichier ou bien une chaîne de caractères.
Vous pourriez également observer que nous lions la Closure processRule de la classe RuleListenerBuilder au nom de variable rule : ainsi, lors de l'évaluation des règles exprimées dans le DSL, chaque définition de règle est traitée par processRule afin d'instancier un objet RuleListener et d'en faire un écouteur de la propriété d'objet spécifiée.

En ayant ajouté les nouvelles règles au moyen de la classe RuleListenerBuilder, un changement de statut de la commande devrait entraîner l'affichage d'un message de la forme « Salut depuis DSL ! Statut : VALIDEE ».

Plus loin avec les mixins Groovy

Nous savons que c'est uniquement un changement de valeur de propriété d'un objet qui permet une application automatique d'un ensemble de règles. Comment déclencher un tel événement sans devoir opérer un changement de valeur ?
Une solution toute simple consiste à utiliser des propriétés de type java.util.EventObjet : il y aura toujours un changement de valeur pour une propriété de ce type, mais qui consiste à lui affecter une nouvelle instance du type EventObject.
Prenons l'exemple de la propriété commandeValidee de la classe Commande ; un changement de valeur s'effectue grâce au code suivant :

commande.commandeValidee = new EventObjet(commande


Notez que commande est la source de l'événement passé dans le constructeur de EventObject, et que du fait de la redondance de la référence à commande, il serait beaucoup mieux d'ajouter à la classe Commande une méthode utilitaire permettant le changement de valeur de propriété.
Afin d'être plus générale, et d'avoir une méthode qui puisse s'appliquer à toute propriété de la classe, nous pouvons définir une méthode fireEvent ainsi :

def fireEvent(event) {
    assert !this[event] || this[event] instanceof EventObject
    this[event] = new EventObject(this)
}

De cette manière, le déclenchement d'un événement, et donc l'application éventuelle de règles, revient à exécuter le code suivant :

commande.fireEvent('commandeValidee')

Un exemple de règle pouvant être associée à l'événement commandeValidee émis par l'application pourrait très bien être l'envoi d'un mél de confirmation !


Voyons maintenant comment nous pourrions réutiliser le code de la méthode fireEvent placé dans une classe utilitaire EventRuleSupport :

class EventRuleSupport {
    def fireEvent(event) {
        assert !this[event] || this[event] instanceof EventObject
        this[event] = new EventObject(this)
    }
}

Comme dans la cas de la classe Commande, si nous possédons le code source de la classe et que celle-ci n'est pas une super-classe, nous pourrions très bien faire dériver cette dernière de la classe EventRuleSupport afin de bénéficier de la méthode fireEvent par héritage.
Mais supposons que la classe Commande soit une classe ayant une classe parent ; dans ce cas nous pourrions utiliser l'annotation Groovy @Mixin(EventRuleSupport) appliquée à la classe Commande. Cette annotation permet de mixer un nouveau comportement (ici EventRuleSupport) à une classe existante (Commande dans notre exemple).
Dès lors, une classe telle que classe Commande disposerait de la méthode fireEvent sans avoir recours à l'héritage.

L'application de l'annotation @Mixin sur une classe suppose bien sûr que vous avez accès à son code source ; il s'agit d'un mixin statique car le nouveau comportement est introduit dans la classe cible lors de la compilation. Le langage Groovy permet également un mixin dynamique : que vous soyez ou non propriétaire de la classe à enrichir, il est possible, à l'exécution, de lui adjoindre un nouveau comportement.
Dans le cas de la classe Commande, voici comment lui ajouter le comportement de la classe EventRuleSupport :

Commande.mixin EventRuleSupport

Après l'exécution de cette instruction, il vous sera possible d'appeler la méthode EventRuleSupport.fireEvent sur toute nouvelle instance de la classe Commande !

Fichier attachéTaille
Main.groovy3.5 Ko
BindableRules.groovy1.1 Ko

balises dans Langages et systèmes

AJAX cajo Camel DSL Grails GraphicsBuilder Groovy Java JBI prefuse RSS ServiceMix