Magazine Focus Emploi

Code défensif et sur-optimisation de code

Publié le 31 juillet 2013 par Abouchard

Récemment, en faisant des revues de code avec mes développeurs, j’ai eu avec eux des discussions intéressantes que j’ai envie de partager sur ce blog.
L’un d’eux utilisait une pratique que j’appelle le code défensif. L’autre avait des idées d’optimisation de code qui étaient de la sur-optimisation. J’ai pris le temps de leur expliquer en quoi ces pratiques sont néfastes.

C’est quoi le code défensif ?

Ce que j’appelle du code défensif, c’est du code qui se défend contre le développeur qui l’a codé et qui va l’utiliser.

Je ne parle pas de l’écriture de bibliothèques qui sont destinées à être (ré-)utilisées dans des contextes variés et par des personnes très différentes. Dans ce cas-là, il faut au contraire prévoir le maximum de cas d’échec, et partir du principe que la bibliothèque est une « boîte noire » à laquelle on est susceptible de fournir des données complètement erronées.

Non, mon propos concerne du code qui « se connait » (si je puis dire). Un contrôleur qui appelle un objet métier dont il est le seul utilisateur. Une méthode publique qui appelle une méthode privée du même objet. Une fonction Javascript qui manipule un élément DOM de la page sur laquelle elle est présente.

C’est quoi le problème avec le code défensif ?

On pourrait se dire que c’est « mieux » de vérifier systématiquement les entrées d’une fonction. C’est plus « propre », c’est plus « sérieux ».

Mais il ne faut oublier que cela a deux impacts.
Le premier impact est peut-être le moins important : Toutes ces vérifications prennent du temps. Oh, pas grand-chose, bien sûr. Mais à force de vérifications inutiles, on finit par dégrader les performances d’une manière qui peut être perceptible.

Le second impact est bien plus grave : Ajouter des vérifications, cela veut dire ajouter du code. Plus de code veut dire plus de maintenance. Le code est moins lisible, il est plus difficile de faire évoluer la partie applicative, car il faut déjà la séparer du reste. Et quand on fait évoluer les fonctionnalités, il faut faire évoluer les vérifications initiales.

Encore une fois, il y a des situations où cela se justifie, et d’autres où c’est complètement inutile.

Quelques exemples

Imaginons un objet qui peut être utilisé de plusieurs manières. On va vérifier les entrées-sorties publiques, mais le développeur est censé savoir ce qu’il fait à l’intérieur de l’objet.

class AjouteEnBaseDeDonnees {
    public function ajoutEntier($i) {
        if (!isset($i) || !is_int($i))
            throw new Exception("Mauvais paramètre.");
        $this->_ajoute($i);
    }
    public function ajouteFlottant($f) {
        if (!isset($f) || !is_float($f))
            throw new Exception("Mauvais paramètre.");
        $this->_ajoute($f);
    }
    private function _ajoute($n) {
        // code défensif complètement inutile
        if (!isset($n) || (!is_int($n) & !is_float($n)))
            throw new Exception("Mauvais paramètre.");
        // ... ajoute la valeur en base de données
    }
}

Dans un cas comme celui-là, on voit bien qu’il est inutile de remettre dans la méthode privée les vérifications qui doivent être faite au plus tôt lors des appels à cet objet.

Autre exemple, une fonction Javascript qui manipule les éléments d’une page.

<html>
<head>
    <script type="text/javascript">
        // on part du principe que jQuery est chargé
        function afficheDate() {
            var panel = $("#panel-date");
            // code défensif inutile
            if (!panel[0]) {
                alert("Je ne peux pas écrire la date !");
                return;
            }
            panel.html((new Date()).toDateString());
            panel.show();
        }
    </script>
</head>
<body>
    <div id="panel-date" style="display: none;"></div>
    <div onclick="afficheDate()">Affiche la date</div>
</body>
</html>

On est d’accord, personne ne code réellement ce genre de chose. On fait des objets Javascripts qui sont proprement rangés dans des fichiers, qui sont eux-même nommés en fonction du namespace de ces objets. Mais ça reste valable dans l’idée.
Dans cet exemple, le code défensif est doublement inutile. Non seulement le développeur qui code la page est censé savoir ce qu’il fait, mais en plus jQuery gère les erreurs silencieusement. Honnêtement, pour que ce code ne fonctionne pas, il faut vraiment que le développeur ne teste pas sa page une seule fois.

La sur-optimisation de code

L’autre cas intéressant est la sur-optimisation. On comprend tous instinctivement qu’il ne faut pas optimiser inutilement. Mais il semblerait qu’on ne réussisse pas tous à sentir quand quelque chose relève de la sur-optimisation.

Pour commencer, revenons rapidement sur les problèmes générés par la sur-optimisation. Ils peuvent être de deux ordres.
Le principal soucis est similaire à celui du code défensif. Écrire plus de code implique de maintenir plus de code ; cela entraîne des difficultés pour le faire évoluer. Le code est plus dur à débugguer et plus lent à améliorer. Ces inconvénients ne peuvent pas être négligés, car à moyens terme ils peuvent devenir particulièrement coûteux.

De manière plus anecdotique, il faut voir qu’une optimisation inutile peut se révéler être contre-efficace. De la même façon qu’un pattern peut se transformer en anti-pattern quand il est mal utilisé, une sur-optimisation peut réduire les performances si elle est mal appliquée ou si elle s’exécute dans un contexte différent de celui qu’on avait en tête quand on l’a codée.

Prenons deux exemples :

  • Ajouter 15% de code en plus, correctement compartimenté, pour multiplier par 25 les performances d’affichage de toutes les pages d’un site web, c’est bien.
  • Ajouter 20% de code en plein milieu de l’existant, pour gagner quelques poussières sur des pages qui représentent moins de 5% du trafic global, c’est inutile.

Mais alors, comment reconnaître une sur-optimisation ?
Posez-vous deux questions simples :

  1. Quel pourcentage de code va être modifié ou ajouté ?
  2. Quel pourcentage d’amélioration puis-je espérer, par rapport au projet global ?

Ensuite il suffit d’appliquer quelques règles :

  • Plus de 33% de code modifié ou ajouté ? Pas bon.
  • Moins de 50% d’amélioration ? Pas bon.
  • Ça prend plus de temps d’optimiser que de coder la fonctionnalité ? Pas bon, sauf si le gain en performance est décuplé (x10 minimum).

Ensuite il ne reste plus qu’à adapter à chaque situation.

Optimiser trop tôt

Ceci est un cas particulier de la sur-optimisation. Comme le disait Donald Knuth :

Premature optimization is the root of all evil.

Les raisons pour lesquelles on peut chercher à optimiser trop tôt sont multiples :

  • L’habitude. On applique une recette telle qu’on l’a déjà appliquée maintes et maintes fois. La force de l’habitude fait qu’on ne réfléchit pas.
  • La volonté de bien faire. Après tout, vouloir optimiser un code pour le rendre plus performant, c’est une bonne chose, non ?
  • La facilité. Quand on a terminé un développement, il est parfois tentant de le peaufiner encore et encore, plutôt que de se pencher sur un autre développement.

N’oubliez pas que ce n’est pas parce qu’on a fait les choses plusieurs fois d’une certaine manière que ça en fait une « bonne pratique » − même si c’était une bonne chose à chaque fois jusque-là.

Mais pourquoi optimiser prématurément est si gênant ?

Tout simplement parce qu’il est plus important de mener un projet à son terme que de l’améliorer. Chaque chose en son temps : développer le projet d’abord, l’affiner ensuite.
À chaque fois qu’un bout de code est complexifié par une optimisation prématurée, on rend plus difficile les ajouts et extensions. Tant qu’un développement n’est pas complet, chaque optimisation qui y est apportée revient à s’ajouter des obstacles dans une course de fond.


Retour à La Une de Logo Paperblog