Ce que cachent vos classes

Le 20 Mars 2016
ingénierie

Le monde du développement a beau être très vaste, la paradigme de programmation orientée objet prend beaucoup de place. Bien que je ne sois pas persuadé qu'il mérite autant d'importance, force est de constater que ce paradigme a mis un coup de pied au cul du développement logiciel à la fin des années 90, avec notamment l'apparition de Java et C++.

Mais qu'est ce qu'une classe ?

Le sujet a beau être abordé en long, en large et en travers, je lis toujours les mêmes banalités. Et pourtant, je pense que changer d'angle permet de rafraichir l'idée qu'on se fait de ces fameuses classes.

Qu'est ce qu'une classe ?

Allez, commençons par une définition académique. Dans mes souvenirs, mes profs me disaient :

Avant on codait comme des sauvages… On écrivait plein de fonctions dans tous les sens et à force on ne comprenait plus rien. Bref, il fallait aimer les spaghettis…

Mais maintenant on modélise des classes et on instancie des objets, ça fait du code tout bien rangé. Ça n'a plus rien à voir !

(Ouais je sais… C'est très très raccourci et globalement exagéré. Mais avouez, il y a un peu de ça !)

L'idée de base derrière le développement orienté objet, c'est qu'il est plus facile de modéliser un logiciel sous forme de classes représentant des concepts de notre monde. La définition simpliste d'une classe c'est : une classe est une brique logicielle qui dispose de méthodes et d’attributs. Grossièrement, c'est la définition d'un type d'objet que l'on va pouvoir créer et manipuler. Les attributs sont des variables propres à chaque instance de classe, et les methodes sont des fonctions qui vont pouvoir exploiter les attributs. Ces attributs représentent donc l’état des instances et les méthodes représentent leur comportement.

Qu'est ce que ça cache ?

Une classe c'est à la fois un état et un comportement. Cela veut dire que la classe a la responsabilité de changer cet état tout en garantissant que cet état reste valide. Cette mission peut rapidement devenir compliqué alors qu'il suffit pourtant d'employer les bons outils.

Je connais justement un outil théorique qui sert à modéliser l'état d'un système. Il s'agit de ce qu'on appelle en mathématique discrète : une machine à état. Une machine à états (ou automate) est une machine abstraite qui peut passer d'état en état en suivant des transitions définies.

Et bien, je pense qu'on peut considérer la définition d'une classe comme étant un sur-ensemble de celle d'une machine à états. Car après tout, les attributs d'une classe représentent un ensemble d'état et ses méthodes peuvent définir un ensemble de transitions entre ses états → c'est justement ça un automate.

Cette manoeuvre à deux buts :

Pour comprendre ce que ça peut impliquer, regardons cet exemple de machine à états finis représentant un feu tricolore :

Modification à récupérer

Cette machine définit trois états (feu vert, feu orange et feu rouge) et elle définit aussi les transitions permettant de passer d'un feu à l'autre. On remarque que cet automate ne permet pas de passer du feu vert au feu rouge. On remarque aussi que l'état “feu bleu” n'existe pas. Cela veut dire que si on croise un feu tricolore qui passe du vert au bleu → ce n'est définitivement pas un feu tricolore ! En effet, un automate est un outil mathématiques, il définit ce qu'il étudie et ignore ce qui sort de sa définition.

Malheureusement, si un outil théorique peut se le permettre, ce n'est pas le cas dans notre code. Quand une instance de la classe FeuTricolore passe du vert au bleu, a priori, ça s'appelle un bug ! Si on considère qu'une classe est le sur-ensemble d'un automate, alors essayons de respecter l'aspect automate de notre classe autant que possible.

Mon but ultime est de réduire au maximum la complexité de mon code. Lorsque je développe une classe, je suis toujours ces deux règles d'or :

Réduire l'ensemble des états d'une classe

Il faut se rendre compte de ce que ça veut dire. Quand on regarde l'exemple d'automate plus haut, on distingue très facilement les trois états possibles. Rien d'étonnant, c'est la le but de cette théorie. Pourtant, l'exercise n'est pas trivial quand on regarde une classe. Voici un petit exemple de classe FeuTricolore :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
classe FeuTricolore {

    Attributs :
        - rouge : Entier
        - vert : Entier
        - bleu : Entier

    Méthodes :
        passeAuVert() {
            rouge = 0
            vert = 255
            bleu = 0
        }
}

Quelle est l'ensemble des états possible de cette classe ?

Mathématiquement parlant, cet ensemble est infini dénombrable car il s'agit de la combinaison de 3 nombres entier. Bien sûr, dans les faits, cet ensemble est fini car les ordinateurs on une quantité fini de mémoire. Admettons que j'implémente cette classe en C++. Si j'utilise des unsigned short int pour les composantes de la couleur, le nombre d'état possible pour ma classe sera de :

65 535 × 65 535 × 65 535 = 281 462 092 005 375 → ça fait un paquet d'état possible…

L'idéal serait de ramener ce nombre pour qu'il soit “humainement” compréhensible. L'approche la plus simple pour réduire cette ensemble est probablement de passer par une énumération. Voici un exemple :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
classe FeuTricolore {

    Énumeration CouleurDeFeuTricolore {
        - rouge
        - vert
        - bleu
    }

    Attributs :
        - couleur : CouleurDeFeuTricolore

    Méthodes :
        passeAuVert() {
            couleur = CouleurDeFeuTricolore.vert
        }
}

De cette manière, le nombre d'états possibles est de trois.

Evidemment, avant d'en arriver la, il faut commencer par restreindre au maximum le nombre d'attributs de sa classe. Par exemple, certains attributs peuvent peut être se transformer en paramètre de méthode. Ou alors, il est peut être possible de passer certains attributs en constante. Cela permet de limiter l'aspect combinatoire de la complexité liée à l'état d'un objet.

Il s'agit la d'exemples de la démarche à suivre. Il y a beaucoup d'autres solutions en fonction du language et des techniques d'ingénierie utilisées.

Réduire l'ensemble des transitions d'une classe

Les transitions servent à garantir la cohérence de l'état de notre classe. En réduire le nombre permet de réduire la complexité de notre automate.

Regardons à nouveau l'exemple d'automate plus haut. Encore une fois, on distingue très facilement les trois transitions possibles. Quand on regarde une classe, on peut aussi compter le nombre de transitions existantes assez facilement. Il suffit de compter le nombre de méthodes qui vont provoquer ce qu'on appelle un effet de bords. On dit d'une methode qu'elle provoque un effet de bords lorsqu'à la suite de son appel, au moins un des attributs a été modifié. Il n'y a pas de recette magique pour réduire le nombre de méthodes à effet de bords. C'est avant tout une question de conception de l'interface de la classe.

On peut aussi réduire la complexité lié aux transitions en posant des assertions dans chaque méthode à effet de bord afin de contrôler que l'état dans lequel se trouve l'objet est bien autorisé, et que l'objet fini bien dans l'état souhaité.

1
2
3
4
5
6
7
8
9
classe FeuTricolore {
    Méthodes :
        passeAuVert() {
            verifier que la couleur actuelle est bien rouge
            ** imaginez une opération non-trivial ici **
            couleur = CouleurDeFeuTricolore.vert
            verifier que la couleur actuelle est bien verte
        }
}

L'exemple semble ridicule car dans notre cas la situation est simpliste. Imaginez ce que ça peut donner dans un vrai projet.

Nos classes sont des machines à états refoulées

Quand nos outils montrent leurs limites, il ne faut pas hésiter à en essayer d'autre. En l'occurence mon outils classe montre ses limites dans ce qu'il propose pour gérer son état et son comportement. On peut toujours blinder ses classes avec des tests unitaires, il restera toujours plus intéressant de penser sa classe comme une machine à états. En prenant ce point de vue, on se rend mieux compte de la complexité liée à la définition des états et à la complexité que représente les effets de bords.

Un paradigme de programmation est juste une approche, ça n'apporte aucune garantie sur la qualité du code. Pisser du code orienté objet ne sera pas plus propre que de pisser du code impératif. D'ailleurs, la programmation impérative a permis de bâtir le noyau linux, non pas parce que ce paradigme résout tous les problèmes mais parce que ses développeur on su structurer le projet techniquement et humainement.