27 juin 2008

La vérification à la compilation, c'est bien

Soit un modèle où l'on diffère, sous forme de commandes, des appels à passer à une certaine classe. Par exemple, dans le cas où l'implémentation finale de la classe est distante, on souhaite passer tous les appels dans une seule connexion réseau : on va donc trouver un moyen de « sérialiser » les intentions d'appels, en général sous forme de texte, on les enverra en lot, puis côté serveur la liste sera détricotée, les appels passés, et on retournera la liste des résultats (dans le cas où tout se passe bien).

Pour illustrer le problème, disons que l'interface de la classe distante s'appelle « Toto », avec les méthodes « doSomething(int, int) » et « doSomethingElse(String) ».

On faisait jusqu'ici un appel réseau pour chaque invocation :


Avec le code Java ci-dessous :
final Toto toto = <proxy>; // proxy pour appel distant

toto.doSomething(2, 5); // 1er appel
toto.doSomething(14, 8); // 2e appel
toto.doSomethingElse("Fantastic!"); // 3e appel
toto.doSomething(0, 1); // 4e appel
Après s'être aperçu qu'on pouvait gagner en performances, nous décidons que du point de vue du réseau il n'y aura qu'un appel, qui agrègera les invocations :


Eh bien je ne sais pas pourquoi, mais pour faire ça certains pensent à écrire la chose suivante :
final Commands commands = new Commands(Toto.class);

commands.storeCommand("doSomething", 2, 5); // 1er
commands.storeCommand("doSomething", 14, 8); // 2e
commands.storeCommand("doSomethingElse", "Fantastic!"); // 3e
commands.storeCommand("doSomething", 0, 1); // 4e

CommandUtils.executeCommands(<proxy>, commands);
C'est tout à fait dommage, car on perd l'avantage de la vérification à la compilation : si on se trompe dans une chaîne de caractères qui contient un nom de méthode, ou dans le type des paramètres d'une méthode, on ne s'en apercevra qu'à l'exécution.

En fait il faut s'arranger pour pouvoir coder ainsi :
final Toto toto = FutureUtils.newFuture(Toto.class);

toto.doSomething(2, 5); // 1er appel
toto.doSomething(14, 8); // 2e appel
toto.doSomethingElse("Fantastic!"); // 3e appel
toto.doSomething(0, 1); // 4e appel

FutureUtils.execute(
<proxy>);
C'est-à-dire qu'il faut s'appuyer le plus possible sur ce qui compile... déjà.

Un très bon modèle pour penser de cette façon, est EasyMock : ce framework d'aide aux tests permet d'enregistrer les appels à contrôler par du code d'appel ! On a donc la vérification de la syntaxe à la compilation, puis évidemment la vérification de la séquence à l'exécution.

2 commentaires:

Oliv a dit…

Tout à fait d'accord.
En particulier on retrouve souvent ce genre de problématique dans des applications client lourd avec le controller du framework MVC implémenter sur le serveur.
Dans ce cas le modèle est souvent sérializer sous forme de clef/valeur.
Il me semble qu'une approche par interface reste la meilleur des solutions.
Il faut alors mettre en place des systèmes de génération de code et de sérialisation générique entre le client et le serveur.
Bon je sais que c'est pas clair, mais en gros ça veut dire que je suis d'accord et qu'il faut dès le départ avoir une approche d'un petit socle de communication prenant en charge la problématique de vérification à la compilation ...
Qui suis je ... y a un petit indice dans le commentaire...
Super blog david, continues comme ça.

David Andrianavalontsalama a dit…

Bonjour Oliv qui fais du Subversion et joues au poker,

oui c'est du délire les mecs qui stockent tout dans des Map<String, Object>... Sans parler de ceux qui n'utilisent même pas Java 5, bien sûr.

Pour la génération de code, je me suis fait un bête plugin Maven qui me crache des JavaBeans et qui commence à être bien rodé, mais Google a cassé le marché cette semaine avec ses Protocol Buffers.

Parce qu'écrire soi-même, ne seraient-ce que des interfaces, avec tous les getXxx() et setXxx(), c'est quand même assez pénible. Sans compter qu'à chaque fois on se dit que « ça serait plus rapide en Ruby ». Donc code generation rulez.

Du coup l'approche par interfaces n'a pas trop d'intérêt : on peut tout à fait produire des classes par génération. La manipulation de bytecode permet de rajouter au runtime tout ce qui manque, éventuellement.

Pour la sérialisation, je suis assez partisan de l'approche des Protocol Buffers, c'est-à-dire un format propriétaire (moi je fais du YAML), transverses aux technologies (Python, C++, Java, PHP...) et déclinable en tout ce qu'on veut de standard (SOAP...).

Et ça va, sinon ?