Créer un simple moteur de workflow dynamique en Groovy

Inspiré en partie par l'article Design Pattern: Design a Simple Workflow using Chain of Responsibility Pattern, cet article décrit la conception d'un simple moteur de workflow en langage Groovy.

Groovy est un langage appréciable car, du fait de sa nature dynamique et de son expressivité, il permet de mettre en oeuvre rapidement des concepts et des idées ! En l'occurrence, nous voulons simplement pouvoir enchaîner l'exécution de code, au travers d'un workflow qui peut être décrit de manière externe grâce à un DSL (Domain-Specific Language) Groovy.

Notre moteur de workflow, dont le code Groovy est proposé en fichier attaché, possède les caractéristiques suivantes :

  • un workflow comprend un certain nombre d'activités écrites en Groovy ou en Java ; identifiée par un nom, une activité indique quelle est l'activité suivante à exécuter si besoin, en renvoyant une chaîne de caractères devant être un nom d'activité ;
  • un workflow peut être défini dans un fichier externe, sous la forme d'un DSL, ou bien par une Closure Groovy ;
  • afin de partager des informations entre les différentes activités, chaque activité peut accéder à un contexte prenant la forme d'une Map Groovy.

S'intégrant parfaitement avec Java, rien n'empêche d'exécuter du code Java à partir d'une activité de workflow, ou même d'invoquer une activité développée en Groovy ou en Java, et présente dans le classpath.

Un premier exemple de définion de workflow

Voici un premier exemple de workflow :

1
2
3
4
5
6
7
8
9
10
11
12
13
workflow {
    "init" {
        println "Démarrage du workflow..."      
        "next"
    }
    "next" { ctx ->
        ctx.name = 'odelia'
        "end"
    }
    "end" { ctx ->
        println "Hello ${ctx.name}!"
    }
}

Ce workflow comporte trois activités nommées init, next et end ; chacune d'elles est implémentée par une Closure Groovy (à la place d'une Closure, on pourrait également trouver une classe, ou une chaîne de caractères). Les deux premières Closures renvoient une chaîne de caractères pour indiquer quelle est l'activité suivante à exécuter, tandis que la troisième retourne null, ce qui mettra fin à l'exécution du workflow.

Exécuter un workflow

Vous pouvez exécuter un workflow, décrit au travers du DSL dans un fichier séparé (j'utiliserai l'extension .swf.groovy pour de tels fichiers), grâce au fichier source SimpleWorkflow.groovy associé à ce billet.
Pour cela, exécutez la commande groovy (venant avec l'installation de Groovy), en passant comme arguments, le fichier Groovy SimpleWorkflow.groovy, le nom du fichier DSL .swf.groovy, ainsi que le nom de l'activité de départ :

1
groovy SimpleWorkflow <fichier.swf.groovy> <nom_activite>

L'exécution du workflow donné plus haut, avec comme activité de départ init, affiche ceci :

1
2
Démarrage du workflow...
Hello odelia!

En Groovy, l'exécution du workflow défini dans le fichier Exemple1.swf.groovy donnerait ceci :

1
2
def swf = SimpleWorkflow.newInstance(new File('Exemple1.swf.groovy') )
swf('init')

Second exemple : trouver un nombre mystérieux

Donnons un nouvel exemple de workflow défini par le fichier NombreMysterieux.swf.groovy (disponible en fin de billet) ; très connu, il s'agit d'un jeu dont but est de vous faire deviner un nombre mystérieux, en vous donnant des indications par rapport à des nombres que vous saisissez dans l'invite de commandes.

Le workflow comporte trois activités : init, qui détermine un nombre aléatoire et le place dans le contexte, puis initialise un compteur de propositions ; input, qui se charge de la saisie utilisateur et teste la valeur soumise afin d'afficher un message d'aide, et d'orienter l'exécution du workflow ; et enfin success, qui correspond à l'activité signalant la réussite du jeu.

Notez que le découpage en activités de ce workflow, ainsi que les activités elles mêmes sont tout à fait arbitraires ; chaque activité devrait avoir un rôle précis et bien identifié, et même pourvoir être réutilisée dans d'autres workflows.

Utiliser des classes comme activités

En plus de définir le code d'une activité sous la forme d'une Closure Groovy, il est aussi possible de fournir des classes, dans le DSL qui décrit un workflow, en tant qu'implémentations d'activités ; de cette manière, des activités prédéfinies pourront être facilement utilisées et réutilisées dans un ou plusieurs workflow(s).

Si dans le DSL, un nom d'activité est suivi d'une classe, SimpleWorkflow tentera d'instancier un objet de cette classe et d'appeler une méthode nommée call sur cet objet, en lui transmettant le contexte du workflow en paramètres. En Groovy, c'est la notion de classe callable.

Examinons un nouvel exemple de workflow qui utilise les classes GroovyActivityTest et JavaActivityTest comme implémentations d'activités :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.odelia.groovy.simpleworkflow.test.activities.*

workflow {
    "init" "1 - Closure"
    "1 - Closure" { ctx ->
        println "Salut d'une Closure!"
        ctx.nextForGroovyActivityTest = "3 - Classe Java"
        ctx.nextForJavaActivityTest = "4 - Closure"
        "2 - Classe Groovy"
    }
    "2 - Classe Groovy" GroovyActivityTest
    "3 - Classe Java" JavaActivityTest
    "4 - Closure" {
        new JavaActivityTest().execute()
    }
}

Ce workflow liste également toutes les manière d'implémenter une activité : par une simple chaîne de caractères, une Closure Groovy, ou bien une classe (Java ou Groovy).

La classe GroovyActivityTest est écrite en Groovy, et possède une méthode call :

1
2
3
4
5
6
7
8
9
10
package com.odelia.groovy.simpleworkflow.test.activities

class GroovyActivityTest {
    
    def call(context) {
        println "Salut depuis GroovyActivityTest, (context : ${context})"
        context.nextForGroovyActivityTest
    }
    
}

Si obj représente un objet d'une telle classe, l'instruction obj(args) en langage Groovy, appellera automatiquement la méthode call de l'objet en transmettant l'argument args.

Cela fonctionne également avec une classe Java, comme ici, avec la classe JavaActivityTest : au moment de l'exécution de l'activité "3 - Classe Java", le moteur de workflow appellera la méthode call de cette classe sur un objet de ce type. Le code de la classe JavaActivityTest est repris ci-dessous :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.odelia.groovy.simpleworkflow.test.activities;

import java.util.Map;

public class JavaActivityTest { 

    String execute() {
        System.out.println("Dans execute (Java)");
        return null;
    }
    
    Object call(Map context) {
        System.out.println("Dans JavaActivityTest.call, context : " + context);     
        return context.get("nextForJavaActivityTest");
    }
    
}

Notez que, comme c'est le cas pour l'activité "4 – Closure" qui utilise une Closure comme implémentation, il reste possible de faire appel à des classes issues de librairies Java.

Tracer l'exécution des activités

SimpleWorkflow propose également un moyen simple de tracer l'exécution des activités : plus précisément, il s'agit de pouvoir exécuter automatiquement une Closure associée au nom beforeActivity avant l'exécution d'une activité, et une Closure associée au nom d'activité afterActivity juste après l'exécution d'une activité.

Pour mettre en œuvre ce comportement, il suffit d'ajouter les activités beforeActivity et afterActivity comme dans l'exemple ci-dessous :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
workflow {
    "init" {
        println "Démarrage du workflow..."      
        "next"
    }
    "next" { ctx ->
        ctx.name = 'odelia'
        "end"
    }
    "end" { ctx ->
        println "Hello ${ctx.name}!"
    }

    "beforeActivity" { ctx, activity ->
        println "/// Prêt à exécuter $activity, avec le contexte $ctx"
    }
    "afterActivity" { ctx, activity, nextActivity ->
        println "/// Exécution de $activity terminée (suivante : ${nextActivity})"
    }
}

L'exécution de ce workfow produira ceci en sortie :

1
2
3
4
5
6
7
8
9
/// Prêt à exécuter init, avec le contexte [:]
Démarrage du workflow...

/// Exécution de init terminée (suivante : next)
/// Prêt à exécuter next, avec le contexte [:]
/// Exécution de next terminée (suivante : end)
/// Prêt à exécuter end, avec le contexte [name:odelia]
Hello odelia!
/// Exécution de end terminée (suivante : null)

Les noms beforeActivity et afterActivity ne sont pas figés : ceux-ci sont utilisés par défaut comme valeurs des propriétés beforeActivityName et afterActivityName d'un objet SimpleWorkflow ; il est bien entendu possible de changer les valeurs de ces propriétés, juste avant l'exécution du workflow, à condition de prévoir des activités portant des noms correspondant dans le workflow.


Fichier(s) :