22 fév. 2007

Tony Cox: "Premature Abstraction is the Root of All Evil"

La paraphrase était osée - car reprendre ainsi la célèbre phrase de Donald Knuth et en modifier le principal substantif dans le but de poser un point de vue controversé est osé, quoi qu'on en dise. Le fait est que ce postulat, bien que fort, reste tout de même en partie vrai, du moins dans certaines circonstances.

Avant d'aller plus loin, un mot sur Tony Cox : je ne parle pas ici de l'acteur noir américain qui a connu son heure de gloire pendant les années 80 en jouant un ewok dans Star Wars IV. Mr. Cox fait partie de la population étrange des programmeurs de jeux vidéo. Il a à son actif des jeux respectés, tels que le récent Gears of War (ou il officiait en tant que Directeur du Développement) ou encore Unreal Championship 2, et bien d'autres - on le retrouve dans les crédits de Dogs of War, Unreal Tournament 2004, Jade Empire, etc. Une liste (non exhaustive mais déjà très impressionnante) des jeux auxquels il a participé de près ou de loin peut être consultée sur Moby Games. On peut donc le considérer comme une personne d'expérience, même si l'expérience est une piètre unité de mesure lorsqu'on souhaite qualifier la qualité des propos d'un programmeur.

Pourquoi a-t-il dit cela ? Et surtout, que voulait-il dire par là ?

Une première approche du problème

Il s'agit en fait de combattre l'une des formes prises par le non respect du principe YAGNI : lorsque le programmeur décide que les méthodes de méta-programmation seront utiles pour solutionner un problème qui, en essence, n'existe pas. L'idée de base est la suivante : je programme un jeu vidéo, et je vais avoir besoin d'une classe vector3d qui abstrait un vecteur dans un espace à 3 dimensions. Dans bien des cas, le programmeur confronté à ce besoin va écrire une version générique de la classe voulue - en C++, une classe template qui sera paramétrée sur le type de données contenu (entier, flottant, etc.). Cependant, une telle sur-abstraction est à la fois prématurée (qu'est-ce que qui dit que la classe a besoin d'être générique ?) et ennuyeuse à plus d'un titre : difficulté accrue du déchiffrage du code, implémentation plus longue - car de nouveaux problèmes surgissent - et bien souvent, mauvaise compréhension des problèmes qui peuvent surgir (quid d'un vector3d<std::vector<xxx> >? Est-ce que ça va seulement marcher ? Est-ce que ça a du sens ? Si ça n'a pas de sens, pourquoi l'autoriser ?).

L'implémentation d'une classe template ne doit pas être faite à la légère. James Fristrom (un autre développeur de jeux vidéos) rappelle les propos de Martin Fowler (qu'il attribue malencontreusement à Kent Beck avant de s'auto-corriger) : si le même code est utilisé 3 fois, il devrait subir une factorisation. Concernant les templates, il va encore plus loin : tant que le code n'a pas été réutilisé au moins 4 fois, la transformation en classe template est inutile et plus que probablement néfaste. L'idée est simple, mais peut-être un peu réductrice.

Méta-programmation et programmation générique

En fait, avant d'aborder une solution sous l'angle de la méta-programmation ou de la programmation générique, il convient d'en comprendre ses forces et ses faiblesses.

Le but de la méta-programmation est d'écrire un code source qui va écrire le code source qui sera compilé. L'implémentation qui en est faite en C++[1] permet ainsi de modifier la façon dont un code source sera interprété par le compilateur au moment ou celui-ci l'interprète. C'est la raison d'être des type traits du TR1, déjà traités dans sur ce journal. Lors de la compilation des différents templates utilisés, le compilateur est forcé d'effectuer des choix basés sur ce qu'il compile et sur ce qu'il connaissait avant. Ces choix déterminent le résultat de la compilation.

Bien évidemment, seuls certains problèmes nécessitent l'utilisation des techniques de méta-programmation - on ne saurait conseiller l'utilisation de ce mastodonte pour l'écriture de programmes simples. En fait - c'est le propos de Tony Cox et de James Fristom, et c'est aussi mon avis - seuls les problèmes nécessitant les forces offertes par les techniques de méta-programmation devraient les utiliser. En tout état de cause, l'interface d'un module ne doit pas faire appel à elles, en raison de leur immense complexité qui fait bien souvent barrière à la compréhension du code. J'irais donc dans ce cas encore plus loin que ces deux développeurs, en affirmant que la refactorisation d'un code source pour utiliser les techniques de méta-programmation n'est jamais souhaitable, sauf cas exceptionnels.

L'autre utilisation des templates est d'augmenter la généricité du code. Dans bien des situations, une telle généricité peut être souhaitable, mais il serait dangereux de se laisser aller à ce constat pour l'élargir à toutes les situations ou presque. Dans le cadre de ce billet, on définit la généricité comme étant l'applicabilité d'un comportement à des données de types différents sans nécessiter de spécialisations de ce comportement. Plus le nombre de contraintes implicites appliquées sur ces types est faible, plus le code est générique[2]. Toutefois, dans la plupart des cas, cette simple définition est ignorée - le code est rendu "générique" par l'artifice syntaxique de la création d'une classe template, mais soit il continue de contraindre énormément le type de données qui sera supporté, soit il prétend connaitre la structure des types de données paramètres. Cette généricité n'est donc que supposée - en tout cas, elle n'est pas factuelle. La complexité supplémentaire provenant de la syntaxe utilisée est alors plus une gêne qu'un bonus, puisque sans généricité factuelle est ne fait que rendre difficile la lecture du code source, ainsi que la compréhension des mécanismes de compilation sous-jacent (qui, comme vous l'avez peut être constaté en lisant mes quelques billets sur le langage C++, sont loin d'être simples).

Mais ce n'est pas tout...

Il convient donc d'éviter au maximum les solutions à base de méta-programmation ou de programmation générique, à moins que les conditions ne l'imposent ou que l'utilisation de ces techniques ait une réelle valeur ajoutée par rapport à une solution plus traditionnelle.

Mais je me garderais bien de ne mettre que ces deux techniques au ban des accusés. L'utilisation abusive de concepts abstraits est lui aussi un piège pour l'architecte logiciel. Il est entendu que trop peu d'abstraction mène au chaos - duplication de code, mauvaise délégation des responsabilités, etc. Trop abstraire n'a pas pourtant pas l'effet inverse : les couches et surcouches rajoutées ajoutent de la complexité au logiciel, et toute complexité mal maitrisée est source de problèmes divers. De plus, bien que cela semble paradoxal, trop d'abstraction spécialise le logiciel. La raison de cette étrange paradoxe réside dans le fait que la généricité outrancière des abstractions de plus haut niveau ne définit pas de sémantique en soit. Il convient alors de spécialiser d'autant ces outils - et moins les abstractions ont de sens, plus la spécialisation sera forte - et plus elle risque d'avoir un impact sur les différentes couches du logiciel.

Conclusion

Créer des abstractions est donc une arme à double tranchant. D'un coté, il faut faire attention à ne pas confondre abstraction et sucrage syntaxique, et de l'autre, il faut s'assurer que l'on maitrise les abstractions créées, et qu'elles répondent à un besoin. Ne pas prendre en considération ces deux points primordiaux, c'est s'exposer à l'accroissement non contrôlé de la complexité du programme, avec toutes les mauvaises conséquences que cela implique en termes de gestion et de réussite du projet. En cela, Tony Cox n'a pas forcément tort : Premature Abstraction is the Root of All Evil. CQFD.

Notes

[1] veuillez excuser mon ignorance des autres langages

[2] une bonne généricité ne peut donc être atteinte que si les contraintes liées au type de données paramètres sont faibles. C'est le cas de la plupart des conteneurs de la librairie standard C++ : les contraintes qu'ils imposent sont des concepts (CopyConstructible, DefaultConstructible, etc) qui n'imposent pas de la part des conteneurs une connaissance plus poussée du type des données contenues.

Ajouter un commentaire

Les commentaires peuvent être formatés en utilisant une syntaxe wiki simplifiée.

Fil des commentaires de ce billet