Conception d'applications prévalentes par Thomas Gil (thomas.gil@dotnetguru.org)

Table des matières

1. Introduction

Depuis quelques années maintenant, nous avons pris l'habitude d'analyser et de concevoir nos applications sous forme d'objets. Les méthodes (UP, Agile), les langages de modélisation (UML) et bien sur de programmation (Java, C#) sont aujourd'hui tout à fait en phase avec cette tendance.

Nous sommes également en train de généraliser la conception et l'architecture multi-couches, supposant par là même que la décomposition suivante est idéale et s'applique à toutes les applications :

Concernant les couches de service et d'objets métier, les langages et méthodes actuels sont adaptés, mâtures et bien acceptés.

La couche de présentation et de visualisation (nous ne parlons ici que du mode client léger) se modernise, en particulier grâce aux ASP.NET et aux JSF soit utilisés tels quels, soit parce les mêmes concepts inspirent un certain nombre de frameworks de présentation.

La couche d'accès aux données reste la plus problématique et fait l'objet de débats sans fin entre les partisans d'outils de mapping objet/relationnel, ceux qui souhaitent continuer à passer par des objets d'accès aux données sur mesure, ceux qui finalement se contentent volontiers de n'invoquer en base de données que des procédures stockées qui implémentent efficacement l'accès aux données physique et qui limitent le couplage entre les couches de service et d'objets métier d'un côté et le stockage physique de l'autre. Comment faire avancer ce débat ? Existe-t-il d'autres solutions non encore complètement explorées ?

La réponse pourrait bien être de rendre nos systèmes prévalents. Laissons-nous aller, oublions quelques instants nos réticences, considérons nos a priori comme des données contestables, et voyons ensemble ce que signifie la notion de prévalence.

2. La prévalence, une révolution orientée objet

Le principe de prévalence a été inventé par Klaus Wuestefeld et a déjà donné lieu à une implémentation en Java, Prevayler, disponible depuis novembre 2001. Son équivalent en .NET est piloté par Rodrigo B. de Oliveira et s'appelle Bamboo.Prevalence.

Afin de nous familiariser avec la notion de prévalence et de bien comprendre ses implications, nous allons nous amuser à citer et à commenter quelques assertions piquantes que l'on peut lire sur la "FAQ pour personnes sceptiques" de Prevayler.

Peut-on simplement conserver les objets métier en mémoire et oublier tout cet embrouillamini autour des bases de données ? Oui.

En effet, un système prévalent conserve tous ses objets en mémoire, et les sérialise de temps en temps. La fréquence de cette sauvegarde est réglable, à définir en fonction des performances souhaitées et de la volumétrie de l'application.

Les modifications apportées au système entre deux sérialisations seront donc perdues ? Non.

Chaque modification de l'état du système doit être implémentée sous la forme d'objets, dans la plus pure forme du Design Pattern Commande. Avant d'exécuter une commande, le mécanisme de prévalence la sérialise tout d'abord sur disque dur. Ainsi, il existe systématiquement une trace de toutes les actions réalisées par les utilisateurs du système; en cas de panne, il suffira de restaurer le dernier cliché, puis de rejouer toutes les commandes exécutées ultérieurement. A nous de bien doser la fréquence des sauvegardes, ou de provoquer des sauvegardes aux moments cruciaux pour l'application.

A quelles contraintes doivent se plier les objets métier ? Il suffit qu'ils soient sérialisables et déterministes.

Par déterministes, on entend ici qu'à partir d'un état donné, l'exécution d'une suite de commandes sur les objets métier doit toujours aboutir au même état final. Cette contrainte est rendue nécessaire par le mécanisme de restauration (par réexécution des commandes) après une panne imprévue.

Finalement, ai-je besoin d'un mécanisme de transactions ? Oui, mais il doit être transparent et naturel.

La notion de transaction telle qu'elle est utilisée lors de l'interaction avec une base de données relationnelle n'est pas naturelle : c'est un artifice technique rendu nécessaire par le fait que les interactions avec la base ne coïncident pas avec les actions utilisateur. Dans un système prévalent, chaque transaction est représentée par une commande. La démarcation transactionnelle est donc entièrement spécifiée lors de la conception du système. Aucun choix technique supplémentaire n'est à faire par la suite.

Qu'en est-il des annulations (rollback) de transactions ? Les rollback ne servent à rien.

C'est probablement la déclaration qui m'a le désarçonné... Voyons l'argumentation (attention un hypothèse s'est silencieusement glissée dans les propos de la FAQ).

Tout d'abord, parlons des annulations faites pour des raisons de pannes (matérielles ou logicielles) : au vu du mécanisme de traitement des commandes, effectivement, puisque chaque commande est sérialisée avant d'être exécutée, il ne peut rien se passer de fâcheux. Au pire, l'état mémoire aura été temporairement incohérent, mais puisque cet état est complètement volatile, cela n'a aucune incidence sur l'état persistant du système.

Ensuite, considérons les annulations souhaitées par l'utilisateur : en exécutant le code d'une commande, on s'aperçoit que cette commande ne peut pas aboutir et on souhaiterait donc procéder à son annulation. Les personnes à l'initiative du mécanisme de prévalence considèrent que chaque commande doit vérifier ses pré-conditions. Si ces dernières ne sont pas remplies, elle ne doit tout simplement pas s'exécuter.

En pratique, la prévalence n'a rien à voir avec les propriétés ACID (bien que la FAQ prétende le contraire). En effet, seule la propriété de Durabilité est implémentée par le mécanisme de prévalence. Rendre les commandes atomiques et isolées est du ressort du concepteur/développeur (rien n'empêche en effet d'user astucieusement du mécanisme des exceptions et des verrous inclus dans les langages Java / C#), et la cohérence est une hypothèse. Bref, rien n'est fait pour rendre transparente l'exécution en parallèle de plusieurs commandes sans effet de bord les unes sur les autres.

Maintenant que le décor est planté, changeons de point de vue. Je vous propose de nous comporter comme de mauvais bougres, et de discuter ouvertement des a priori qui nous suscitent naturellement une réaction à ces assertions tonitruantes.

3. A priori

Au moment de décider d'écrire cet article, j'étais tout de même très sceptique. Ce doit être un réflexe commun puisqu'il existe une Skeptical FAQ. Voici mes réflexions d'alors.

Tout d'abord, concernant le possibilités de montée en charge et en volumétrie de ce genre de système. En effet, si l'on accepte de stocker toutes les informations en mémoire, et que le volume d'informations augmente sans cesse, il arrivera un moment où les contraintes d'occupation deviendront problématiques. Est-ce que ce problème est résolu par les outils tels que Bamboo ? De même, comment répartir la charge d'une application sur plusieurs machines si chaque processus gère lui-même son état persistant ?

Quels langages ou mécanisme de requêtes nous propose Bamboo ? Une grosse partie des applications consiste à rechercher des informations dans leur environnement persistant, il faut donc que cette tâche soit simplifiée au maximum par un langage d'expression puissant.

Sur quels outils d'administration peut-on se baser ? Que se passe-t-il lors d'une panne grave, ou lorsqu'un fichier est partiellement endommagé ?

Nous avons aujourd'hui le réflexe de créer des index sur les informations qui nous servent de clé d'accès aux données, comment se transpose ce réflexe en mode prévalent ?

Comment gère-t-on l'évolution du modèle objet qui sert également de modèle de stockage ? Comment ajouter, supprimer de nouveaux attributs, de nouvelles relations, de nouvelles classes et restaurer les informations dont la structure était compatible avec le modèle objet précédent ?

Enfin, et probablement le plus bloquant, que faire de tous les outils qui supposent que le modèle de stockage des informations est relationnel ? Nombre sont les outils de reporting, d'ETL, d'EAI... qui supposent que les informations sont stockées dans des tables que l'on peut interroger par SQL. Faut-il abandonner ces outils et en réécrire d'autres, compatibles avec la philosophie objet ?

Bref, je me demandais vraiment si cette notion de prévalence n'était pas une simple provocation, un "coup de gueule" contre la mode du mapping O/R. D'où la question : dans quelle situation ce concept de prévalence peut-il avoir sa place ? Comme vous allez le comprendre dans la section suivante, la mise en oeuvre de Bamboo sur un exemple simple mais concret devait remettre en cause mon scepticisme.

4. Développement d'une application prévalente

Dans cette section, nous allons relater notre propre expérience de mise en oeuvre de Bamboo.Prevalence, le clone .NET de l'outil Java Prevayler.

Supposons que nous souhaitions développer une petite application graphique (client riche, pour changer) qui permette de gérer l'ensemble des sujets, des articles et de leurs auteurs sur DotNetGuru. Appelons cette application "GuruBamboo" (vous pouvez en télécharger le code source, sous forme d'un projet VisualStudio.NET à cette adresse : http://www.dotnetguru.org/downloads/GuruBamboo.zip).

Figure 1 . Diagramme de cas d'utilisation de GuruBamboo

De ces quelques cas d'utilisation, on déduit rapidement les objets participants :

Figure 2 . Diagramme de classes d'analyse de GuruBamboo

Ces classes s'implémentent sans aucun problème en C# (ou dans tout autre langage objet, bien entendu), et elles n'ont aucune adhérence vis-à-vis du framework Bamboo.

Vient ensuite le besoin de concevoir et d'implémenter l'aspect dynamique de notre logiciel. Pour cela, le principe de prévalence impose de réifier chaque interaction entre acteurs et système par un objet : une commande. Celle-ci doit implémenter l'interface ICommande du framework Bamboo. Voici quelques exemples :

Figure 3 . Diagramme de classes "Commandes"

Et comme nous le mentionnions plus tôt, chaque commande délimite implicitement une transaction logique. Du coup, il faut se plier à la même discipline pour représenter les interactions de sélection / navigation dans le graphe d'objet : ce sont des "Requêtes", mais qui suivent exactement le même patron structurel :

Figure 4 . Diagramme de classes "Requêtes"

Il ne reste plus qu'à développer une interface graphique pour déclencher ces commandes et ces requêtes, et pour en présenter les résultats. Que les amateurs d'interfaces "sexy" ne soient pas horrifiés, voici une copie d'écran du résultat...

Figure 5 . Fenêtre principale de GuruBamboo

Et voilà... Mais, me direz-vous, tu as complètement oublié d'implémenter la persistance des objets métier : où se trouve la couche d'accès aux données ? Eh bien c'est le plus surprenant dans l'histoire : toute la couche d'accès aux données tient en une classe, dont voici le code source :

01. namespace GuruBamboo.Data {
02.      using System;
03.      using System.IO;
04.      using System.Collections;
05.      using Bamboo.Prevalence;
06.      using Bamboo.Prevalence.Util;
07.      using GuruBamboo.Business;
08.
09.        //Singleton+Prevalenttransparentrootobject
10.      public class PrevalenceRoot {
11.          public static readonly PrevalenceEngine Engine;
12.          public static DotNetGuru DotNetGuruSingleton{
13.              get{ return (DotNetGuru) Engine.PrevalentSystem; }
14.          }
15.
16.          static PrevalenceRoot(){
17.              string prevalenceBase = Path.Combine
18.                  (Environment.CurrentDirectory, "data");
19.              Engine = PrevalenceActivator.CreateEngine
20.                  (typeof(DotNetGuru), prevalenceBase);
21.              new SnapshotTaker
22.                  (Engine,TimeSpan.FromMinutes(1),
23.                  Util.CleanUpAllFilesPolicy.Default);
24.          }
25.      }
26. }
data/PrevalenceRoot.cs

Pour être tout à fait honnête, il faut aussi que ce système de prévalence ait connaissance de l'ensemble des commandes à exécuter. Pour cela, nous avons triché et ajouté à chaque commande une méthode simplificatrice qui prend cela en charge pour ses clients. Pour vous faire une idée, voici à quoi ressemble l'une des commandes :

01. namespace GuruBamboo.Service.Commands {
02.      using System;
03.      using System.Collections;
04.      using Bamboo.Prevalence;
05.      using GuruBamboo.Business;
06.      using GuruBamboo.Data;
07.
08.      [Serializable]
09.      public class AddTopicCommand : ICommand {
10.          private string m_topicName;
11.
12.          public AddTopicCommand(string topicName){
13.              m_topicName = topicName;
14.          }
15.
16.          //ICommandimplementation
17.          public object Execute(object system) {
18.              DotNetGuru dng = system as DotNetGuru;
19.              dng.Add(new Topic(m_topicName));
20.              return null;
21.          }
22.
23.          public object Execute() {
24.              return PrevalenceRoot.Engine.ExecuteCommand(this);
25.          }
26.      }
27. }
service/commands/AddTopicCommand.cs

Nous n'avons pas souhaité aller plus loin dans la conception, mais il est évident que la méthode Execute() possède toujours le même corps, quelle que soit la commande ou la requête. Sur un véritable projet, il faudrait donc créer deux classes abstraites qui mettraient ce code en commun pour toute l'application (et qui en profiteraient pour implémenter un petit module de statistiques pour savoir combien de commandes sont déclenchées par exemple...).

5. Découplage grâce aux proxies dynamiques

Le développement d'une classe commande ou requête par scénario de cas d'utilisation peut sembler lourd à la conception et au développement. Pour cette raison, Bamboo propose une approche allégée utilisant l'infrastructure des dynamic proxies .NET (qui sert également de base à .NET Remoting).

Le principe est le suivant : pour peu qu'une classe hérite de MarshalByRefObject, les invocations de méthodes qui lui parviennent peuvent être interceptés par des objets appelés dynamic proxies. Plusieurs dynamic proxies peuvent intercepter la même invocation de méthode : ils forment donc une chaîne de responsabilités. Dans l'absolu, chacun d'entre eux peut avoir une valeur ajoutée différente comme la vérification d'accréditations, la gestion automatique de transactions...

Dans le cadre de la prévalence, l'interception d'invocations de méthodes sur le système prévalent (l'instance de la classe DotNetGuru dans notre exemple) permettrait de prendre la main avant toute modification ou requétage de l'état du système, de manière à créer un objet de type Commande ou Requête sérialisable qui puisse être écrit dans le journal des commandes.

Seule une question reste sans réponse : "Comment faire comprendre au mécanisme automatique de prévalence qu'une invocation de méthode doit se transformer en Commande ou en Requête" ? Puisque nous sommes en .NET avec Bamboo, il suffit de mieux qualifier les méthodes en s'appuyant sur les méta-données (les "Attributes"). Et en effet, Bamboo permet de préfixer une méthode par [Query] afin de différencier requêtes et commandes.

Ce mécanisme semble tout à fait séduisant a première vue. Mais à bien y réfléchir, on s'aperçoit de plusieurs problèmes potentiels. Le premier est que la classe constituant le système de prévalence DOIT hériter de MarshalByRefObject. Elle ne peut donc pas hériter d'une autre classe métier, à moins que celle-ci ne soit déjà une fille de MarshalByRefObject. Le second problème est plus grave : le mécanisme d'interception de code est relativement coûteux et risque de pénaliser les performances globales de l'application.

Enfin, à l'usage, le fait de faire de chaque interaction une commande finit par devenir un avantage plutôt qu'une lourdeur. En effet, chaque scénario de cas d'utilisation devenant une commande, il devient très simple d'assurer la traçabilité entre le recueil des exigences et l'analyse d'un côté, avec la conception détaillée et l'implémentation d'un autre coté. Corollaire intéressant : le nombre de commandes implémentées (et de tests unitaires associés) peut devenir une métrique intéressante pour mesurer la progression d'un projet de développement.

Pour toutes ces raisons, j'aurais plutôt tendance à laisser de côté la magie des dynamic proxies en faveur d'un développement explicite de toutes les commandes des applications.

6. Bilan sur le développement

J'ai trouvé que la discipline imposée par le concept de prévalence aide (force même un peu) à bien concevoir nos applications. La conception et le développement sont 100% objet, du début à la fin. A aucun moment on ne réfléchit à un concept procédural, tabulaire, arborescent, ni bien entendu à un mapping entre nos objets métier et ces autres types de structures de données.

D'autre part, vous avez pu vous en rendre compte, l'adhérence entre notre application, qu'il s'agisse de la couche de service ou des objets métier, et Bamboo (ou Prevayler en Java) est très faible. En cas de choix contraire, il est toujours temps de se passer d'un tel framework et de revenir à des pratiques moins radicales tel que le mapping Objet/Relationnel. Cela doit nous rassurer : quoi qu'il arrive, l'investissement de temps et d'énergie en conception et en implémentation sur les couches de service et d'objets métier auraient dû être fait. Donc mettre en place la prévalence se fait à sur-coût nul.

Concernant certains des a priori mentionnés ci-dessus, en particulier la fiabilité et la robustesse du système, vous pourrez vous rendre compte en manipulant l'application fournie avec cet article que malgré tous nos efforts pour provoquer de graves pannes lors de l'exécution de l'application prévalente, il est quasiment impossible d'engendrer une situation irrécupérable pour Bamboo. En effet, lorsqu'on provoque une panne violente (l'arrêt brutal du processus par exemple), puisqu'un ensemble de fichiers "command log" aura été écrit sur disque au fur et à mesure de l'exécution des commandes, le mécanisme de prévalence replace l'application dans son dernier état stable, à partir du dernier cliché sérialisé sur disque, et réexécute l'ensemble des commandes postérieures à la prise du cliché. Ce processus rappelle le fonctionnement d'un journal de bases de données, à ceci près que le journal contient ici les actions entreprises par les utilisateurs du système et non une représentation des insertions/suppressions atomiques de données en base.

Les performances du système n'ont pas pu être jugées sur une application aussi simple. Bien entendu, parcourir le graphe d'objet en mémoire est très efficace (et ne nécessite aucune jointure contrairement au mode relationnel), mais la grande inconnue est le parcours de collections d'objets : il semble évident que les collections standard du framework .NET ne sont pas adaptées à contenir de très nombreux objets. Il faudra donc se doter de collections spécifiques, taillées sur mesure pour une telle volumétrie.

Par contre, le grand gagnant dans cette petite expérience est le temps de développement. Si l'on a l'habitude de dire que la mise en place d'un outil de mapping objet/relationnel fait souvent gagner entre 10 et 30 % du temps de développement global d'un projet objet, que dire du développement d'un système prévalent qui n'a même pas le besoin de spécifier de mapping, de le raffiner ni de mettre quoi que ce soit en oeuvre concernant la persistance ! Dans un système prévalent, le temps de développement est complètement consacré à l'implémentation des couches métier et service, ainsi qu'aux tests de ces couches. La notion de framework s'efface, les développeurs se recentrent sur le code métier.

7. Critiques et conclusion

Une petite contrainte de conception est apparue lors du développement de la classe PrevalenceRoot.cs : Bamboo doit être initialisé avec le type du système de prévalence (ici : typeof(DotNetGuru) ). Or cette classe était initialement un singleton, et donc son constructeur n'avait pas un niveau de visibilité suffisant pour permettre à Bamboo de l'initialiser.

Le plus gros regret, au cours du développement, a été de ne pas disposer d'un langage de requêtes puissant sur le graphe d'objets. On se prend à rêver d'un bon vieil OQL, Bamboo semblait prometteur en fournissant une implémentation de XPath sur un graphe d'objets .NET, mais à l'usage cette implémentation s'avère tout à fait insuffisante (si vous souhaitez vous exercer aux requêtes XPath sur les objets métier, lancez l'application GuruBamboo et ouvrez la fenêtre de "Requêtes custom" : vous pourrez évaluer vos requetes XPath à chaud). Cet aspect est primordial à mon sens, et je ne rejoins pas l'avis de Klaus Wuestefeld lorsqu'il dit que nous n'avons pas besoin d'un langage de requêtes et que le seul langage de programmation (Java, C#) suffit à implémenter les requêtes; pour moi, un langage d'expression puissant et efficace permet de simplifier l'application, de diminuer son nombre de lignes de code et de limiter les erreurs de programmation.

A l'issue du développement, une fois bien compris le mécanisme de prévalence, voici ceux des a priori qui persistent (malheureusement) :

En conclusion, on peut dire que certains systèmes peu générateurs d'informations se contenteront sans problème d'un mécanisme de prévalence.

Ceux qui nécessitent une gestion de l'accès concurrent aux ressources partagées pourront également l'utiliser, mais une plus grande attention devra être portée à la synchronisation des commandes exécutées en parallèle (rappelons qu'aucun mécanisme d'isolation automatique n'est proposé par la prévalence).

Par contre, les applications de gestion de données ne peuvent se contenter des fonctionnalités que nous avons présentées ici. Elles auront besoin de compléments pour archiver les informations les moins souvent utilisées, pour recharger ces informations au besoin, pour faire évoluer sans trop de peine le modèle de données, et pour procéder à l'analyse et à l'exploitation de ces données.

Je peux vous l'avouer maintenant, lorsque j'ai commencé à jouer avec Bamboo, je n'étais pas du tout convaincu par cette approche. Après cette petite expérience, je le suis davantage, mais je sens de nombreux points bloquants... Pourtant, j'ai le sentiment qu'il est fondamentalement possible d'étoffer les outils autour de Prevayler ou de Bamboo pour que le mécanisme devienne séduisant pour tous les types d'applications, y compris de gestion. Il faudra par exemple :

Je pense sincèrement que ces quelques mécanismes supplémentaires ne sont pas terriblement complexes à implémenter. Il ne manque donc pas grand chose à la prévalence pour se lancer complètement dans le combat contre les bases de données (relationnelles, objet ou XML d'ailleurs). Un match à suivre dans les mois qui viennent...

8. Références

Notre application "exemple" :

http://www.dotnetguru.org/downloads/GuruBamboo.zip

Le site du projet Bamboo :

http://bbooprevalence.sourceforge.net/

Un très bon article sur le principe de prévalence :

http://www-106.ibm.com/developerworks/web/library/wa-objprev/index.html

Le site de Prevayler, application Java dont s'inspire Bamboo :

http://www.prevayler.org

La fameuse FAQ pour sceptiques :

http://www.prevayler.org/wiki.jsp?topic=PrevalenceSkepticalFAQ