Il y a du Bon C++ et du Mauvais C++
Qu'est-ce qui fait la qualité d'un code C++ ? Si je vous montrais un bout de code, comment reconnaîtriez-vous la griffe d'un expert ?
Il y a ceux qui vous diront que le code doit être correct, c'est-à-dire que les algorithmes doivent produire les bons résultats, etc. Loin de moi l'idée de penser que ces gens ont tort, bien au contraire. C'est tellement évident que le code doit être correct que ça ne vaut même pas la peine de s'attarder dessus.
On peut tout de même noter que tout développeur, expert ou pas, fait des erreurs dans son code, et que la teneur en erreurs est sans doute plus le fruit d'un bon débogage post-codem que d'une maîtrise du langage. Je connais des développeurs très doués mais très distraits, qui font beaucoup d'erreurs dans le feu de l'action mais qui les éliminent par la suite en testant rigoureusement leur code avant de le livrer.
Il y a ceux qui vous diront que le code doit être efficace, qu'il doit s'exécuter vite et qu'il doit être raisonnable dans sa consommation de ressources.
Question anodine : n'est-il pas vrai qu'il vaut toujours mieux avoir un code rapide et compact ? Ca semble tout aussi évident que le fait que le code doive être correct, et pourtant ça ne l'est pas.
D'une part, dans une application classique par exemple, tout le code n'a pas besoin d'être efficace. A vrai dire, du code qui doit s'exécuter vraiment très vite, il n'y en a en général pas plus de 1% - je ne parle pas des jeux ou des applications temps-réel très spécialisées. Dans ce cas-là, si on a du code lent et qu'il y a un impact notable, on utilise un profileur de code, qui dira de manière très objective quelles sont les zones lentes qui méritent une petite réécriture. Idem pour les ressources, il existe des outils pour traquer tout ça.
D'autre part, et c'est presque plus important, dans la vraie vie l'écriture de code n'est pas une activité divine, dégagée de toute contrainte d'espace et de temps. En général, on a des délais à tenir. Prendre deux jours de plus pour écrire une fonction optimisée peut être moins intéressant que de la laisser telle qu'elle est (correcte et raisonnablement efficace) et de passer à la suite. C'est souvent difficile à accepter pour les jeunes développeurs (et j'en viens) mais il faut savoir se montrer pragmatique. Avec l'expérience, on finit par écrire naturellement du code qui n'est pas lent, même s'il n'est pas le plus rapide, et ce sans perdre de temps à faire un peu mieux un peu partout. Le mieux est l'ennemi du bien - c'est précisément ce qui explique le très grand succès des langages de plus haut niveau comme C# ou Visual Basic.
Donc ce n'est pas l'efficacité qui fait du bon code. Continuons.
Il y a ceux qui vous parleront d'indentation, d'alignement, de noms de classes et de variables, etc. Bref, de conventions. C'est sûr, devant un code pas indenté du tout, je ne mets pas ma main à couper que ça vient d'un vieux routier du développement. Mais franchement, les conventions, c'est très surfait. Chacun a les siennes, et une convention différente peut être aussi difficile à comprendre pour un développeur qui reçoit du code que pas de convention du tout. Je vous encourage à choisir votre style et à être cohérent avec vous-même à travers votre code, mais ça s'arrête là.
Des design patterns clairement visibles et identifiés dans le code sont un bon signe, mais ça n'empêche pas un code d'avoir plein de problèmes par ailleurs. L'utilisation de la STL ou d'une autre bibliothèque disponible plutôt que de réinventer la roue est aussi encourageant, mais ce n'est pas tout à fait ça non plus.
Euh... Ah bon ?
Réfléchissons un instant. En C++, on nous encourage à créer des classes et à bien modulariser le code. Bien. Cela signifie que lorsque j'écris du code, il y a neuf chances sur dix pour qu'il soit utilisé par d'autres morceaux de code, et pourquoi pas aussi par d'autres développeurs, sur d'autres projets.
Qu'il soit implémenté sans erreur, c'est le minimum. Qu'il soit hyper-optimisé, la plupart du temps, ce n'est pas ce qu'on lui demande. Par contre, qu'il soit simple à utiliser et qu'il ne me laisse pas le manipuler de façon inappropriée ou erronée, c'est tout de même autrement plus intéressant.
Je vous l'accorde, il aurait pu leur donner des noms, à ces paramètres, mais enfin voilà ce que nous avons sous la main. Chez nous les dates s'écrivent traditionnellement sous la forme JJ/MM/AAAA, alors allons-y, et réglons la date au 7 mars 2006.
Ca avait l'air simple, pourtant. Mais ce que vous ne saviez pas, c'est que Bob est américain, et que les américains écrivent le mois avant le jour, c'est-à-dire MM/JJ/AAAA. Vous venez de changer la date du système au 3 juillet 2006. Et comme, malgré l'erreur, vous avez entré une date parfaitement valide, le système n'a rien dit.
OK, OK, pour corriger le tir, on corrige la déclaration de Bob :
Bon. Un peu plus tard, vous utilisez la fonction à nouveau, mais vous effectuez un calcul pour obtenir la date, et vous avez utilisé les variables
Avec un peu de chance, c'est une date invalide, du genre
Mais alors comment diable faire pour être sûr de ne pas se tromper ?
Le C++ nous offre plein de techniques pour faire en sorte que l'intention soit claire et que le compilateur détecte les mauvais usages. Créons donc trois classes,
Maintenant, faisons un peu attention à nos définitions de ces trois classes. On souhaite pouvoir passer d'une variable entière à l'un de ces trois types. Il nous faut donc un constructeur qui prenne un
Si on écrivait alors
le compilateur construirait un objet
Pas de panique : rendons le constructeur explicite.
En rendant le constructeur explicite, le compilateur ne génère pas de conversion implicite, et donc refuse l'appel à
Comme la fonction
C'est mieux. Notez que vous pouvez toujours vous tromper avec
Parmi les causes les plus fréquentes de code dont il est facile de se tromper en l'utilisant on retrouve un typage trop faible (c'est le cas dans mon exemple) et la redondance : quand vous forcez un développeur à dire deux fois la même chose, vous pouvez être sûr que ça finira par un bug un jour ou l'autre.
Avec l'expérience, le développeur découvre les unes après les autres les différentes causes de code vandalisable et les différentes astuces qui sont à sa disposition pour mettre en place des garde-fou efficaces. Le préprocesseur aussi, bien que les gens du C++ voudraient parfois s'en débarrasser, peut aider à écrire du code qui soit difficile à mal utiliser.
Le plus important est de se demander, à chaque coin de code qu'on écrit et qui va être utilisé ailleurs :
Si vous parvenez à écrire du code qu'on ne peut pas mal utiliser, vous pouvez considérer que c'est du code de qualité.
Chacun cherche son chat
Dès qu'on pose la question sur les forums idoines, chacun y va de sa version et les réponses fusent. Commençons par en éliminer quelques unes.Il y a ceux qui vous diront que le code doit être correct, c'est-à-dire que les algorithmes doivent produire les bons résultats, etc. Loin de moi l'idée de penser que ces gens ont tort, bien au contraire. C'est tellement évident que le code doit être correct que ça ne vaut même pas la peine de s'attarder dessus.
On peut tout de même noter que tout développeur, expert ou pas, fait des erreurs dans son code, et que la teneur en erreurs est sans doute plus le fruit d'un bon débogage post-codem que d'une maîtrise du langage. Je connais des développeurs très doués mais très distraits, qui font beaucoup d'erreurs dans le feu de l'action mais qui les éliminent par la suite en testant rigoureusement leur code avant de le livrer.
Il y a ceux qui vous diront que le code doit être efficace, qu'il doit s'exécuter vite et qu'il doit être raisonnable dans sa consommation de ressources.
Question anodine : n'est-il pas vrai qu'il vaut toujours mieux avoir un code rapide et compact ? Ca semble tout aussi évident que le fait que le code doive être correct, et pourtant ça ne l'est pas.
D'une part, dans une application classique par exemple, tout le code n'a pas besoin d'être efficace. A vrai dire, du code qui doit s'exécuter vraiment très vite, il n'y en a en général pas plus de 1% - je ne parle pas des jeux ou des applications temps-réel très spécialisées. Dans ce cas-là, si on a du code lent et qu'il y a un impact notable, on utilise un profileur de code, qui dira de manière très objective quelles sont les zones lentes qui méritent une petite réécriture. Idem pour les ressources, il existe des outils pour traquer tout ça.
D'autre part, et c'est presque plus important, dans la vraie vie l'écriture de code n'est pas une activité divine, dégagée de toute contrainte d'espace et de temps. En général, on a des délais à tenir. Prendre deux jours de plus pour écrire une fonction optimisée peut être moins intéressant que de la laisser telle qu'elle est (correcte et raisonnablement efficace) et de passer à la suite. C'est souvent difficile à accepter pour les jeunes développeurs (et j'en viens) mais il faut savoir se montrer pragmatique. Avec l'expérience, on finit par écrire naturellement du code qui n'est pas lent, même s'il n'est pas le plus rapide, et ce sans perdre de temps à faire un peu mieux un peu partout. Le mieux est l'ennemi du bien - c'est précisément ce qui explique le très grand succès des langages de plus haut niveau comme C# ou Visual Basic.
Donc ce n'est pas l'efficacité qui fait du bon code. Continuons.
Il y a ceux qui vous parleront d'indentation, d'alignement, de noms de classes et de variables, etc. Bref, de conventions. C'est sûr, devant un code pas indenté du tout, je ne mets pas ma main à couper que ça vient d'un vieux routier du développement. Mais franchement, les conventions, c'est très surfait. Chacun a les siennes, et une convention différente peut être aussi difficile à comprendre pour un développeur qui reçoit du code que pas de convention du tout. Je vous encourage à choisir votre style et à être cohérent avec vous-même à travers votre code, mais ça s'arrête là.
Des design patterns clairement visibles et identifiés dans le code sont un bon signe, mais ça n'empêche pas un code d'avoir plein de problèmes par ailleurs. L'utilisation de la STL ou d'une autre bibliothèque disponible plutôt que de réinventer la roue est aussi encourageant, mais ce n'est pas tout à fait ça non plus.
Le secret révélé
Pour moi, la caractéristique principale d'un code bien écrit est qu'il est difficile de se tromper en l'utilisant.Euh... Ah bon ?
Réfléchissons un instant. En C++, on nous encourage à créer des classes et à bien modulariser le code. Bien. Cela signifie que lorsque j'écris du code, il y a neuf chances sur dix pour qu'il soit utilisé par d'autres morceaux de code, et pourquoi pas aussi par d'autres développeurs, sur d'autres projets.
Qu'il soit implémenté sans erreur, c'est le minimum. Qu'il soit hyper-optimisé, la plupart du temps, ce n'est pas ce qu'on lui demande. Par contre, qu'il soit simple à utiliser et qu'il ne me laisse pas le manipuler de façon inappropriée ou erronée, c'est tout de même autrement plus intéressant.
Exemple
Je vais vous montrer un exemple, qui est un peu canonique, pardonnez-moi si vous l'avez déjà vu ailleurs. Imaginez une fonction qui change la date du système. Elle prend trois entiers en paramètre, un pour le jour, un pour le mois et un pour l'année. Classique, n'est-ce pas ? Dans son infinie sagesse, le développeur (Bob) qui a écrit la fonction l'a déclarée en utilisant une petite bizarrerie du langage : il n'a pas mentionné le nom des paramètres formels. Ca donne ceci :void ChangeDate(int, int, int);Je vous l'accorde, il aurait pu leur donner des noms, à ces paramètres, mais enfin voilà ce que nous avons sous la main. Chez nous les dates s'écrivent traditionnellement sous la forme JJ/MM/AAAA, alors allons-y, et réglons la date au 7 mars 2006.
ChangeDate(07, 03, 2006);Ca avait l'air simple, pourtant. Mais ce que vous ne saviez pas, c'est que Bob est américain, et que les américains écrivent le mois avant le jour, c'est-à-dire MM/JJ/AAAA. Vous venez de changer la date du système au 3 juillet 2006. Et comme, malgré l'erreur, vous avez entré une date parfaitement valide, le système n'a rien dit.
OK, OK, pour corriger le tir, on corrige la déclaration de Bob :
void ChangeDate(int month, int day, int year);Bon. Un peu plus tard, vous utilisez la fonction à nouveau, mais vous effectuez un calcul pour obtenir la date, et vous avez utilisé les variables
i, j et k pour les mois, les jours et les années. Il est 23h17, la quantité de sang dans votre caféine remonte dangereusement et c'est l'accident bête, vous inversez deux des variables en vous embrouillant avec cette histoire de jours et de mois : ChangeDate(j, i, k);Avec un peu de chance, c'est une date invalide, du genre
j = 25 et i = 4, auquel cas le système vous crachera à la figure au moment de l'appel de fonction (si tant est que vous y parveniez, car ça peut être un cas particulier caché dans un code de traitement d'erreur lié au réseau, et ça pour le reproduire, bonne chance).Mais alors comment diable faire pour être sûr de ne pas se tromper ?
Le C++ nous offre plein de techniques pour faire en sorte que l'intention soit claire et que le compilateur détecte les mauvais usages. Créons donc trois classes,
CMonth, CDay et CYear, représentant chacune le numéro de l'unité calendaire correspondante, et modifions la déclaration de ChangeDate : void ChangeDate(const CMonth&, const CDay&, const CYear&);Maintenant, faisons un peu attention à nos définitions de ces trois classes. On souhaite pouvoir passer d'une variable entière à l'un de ces trois types. Il nous faut donc un constructeur qui prenne un
int en argument, comme par exemple : class CMonth
{
CMonth(int month);
...
};Si on écrivait alors
ChangeDate(j, i, k);le compilateur construirait un objet
CMonth à partir de la valeur de j, car la présence du constructeur montré ci-dessus permet une conversion explicite. Mais alors on n'a rien gagné, puisqu'on peut toujours écrire n'importe quoi !Pas de panique : rendons le constructeur explicite.
class CMonth
{
explicit CMonth(int);
...
};En rendant le constructeur explicite, le compilateur ne génère pas de conversion implicite, et donc refuse l'appel à
ChangeDate() comme nous venons de le proposer. Il faut maintenant écrire les noms des classes, ce qui donne, à supposer que vous soyez toujours un peu embrouillé : ChangeDate(CDay(j), CMonth(i), CYear(k));Comme la fonction
ChangeDate() attend un CMonth en premier argument, le compilateur générera une erreur pour vous l'indiquer, et ce quelque soit la teneur en sang de votre caféine ou l'heure de la nuit. ChangeDate(CMonth(i), CDay(j), CYear(k));C'est mieux. Notez que vous pouvez toujours vous tromper avec
i, j et k, mais au moins notre ami Bob qui a écrit la fonction est hors de cause, et c'est tant mieux car c'est son code qui nous intéresse.Le compilateur est votre ami
Ce que je vous montre là est une technique parmi les dizaines qui existent pour limiter les erreurs. Le principe le plus important est que les erreurs sont détectées le plus tôt possible. Par le code lors de l'exécution, et c'est déjà pas mal, mais surtout par le compilateur lors de la compilation, ce qui est le but recherché.Parmi les causes les plus fréquentes de code dont il est facile de se tromper en l'utilisant on retrouve un typage trop faible (c'est le cas dans mon exemple) et la redondance : quand vous forcez un développeur à dire deux fois la même chose, vous pouvez être sûr que ça finira par un bug un jour ou l'autre.
Avec l'expérience, le développeur découvre les unes après les autres les différentes causes de code vandalisable et les différentes astuces qui sont à sa disposition pour mettre en place des garde-fou efficaces. Le préprocesseur aussi, bien que les gens du C++ voudraient parfois s'en débarrasser, peut aider à écrire du code qui soit difficile à mal utiliser.
Le plus important est de se demander, à chaque coin de code qu'on écrit et qui va être utilisé ailleurs :
- Est-il possible de se tromper en utilisant ce code sans que le compilateur ne génère une erreur ?
- Si oui, est-ce qu'au moins le compilateur lance une alerte (warning) ?
- Si oui, est-ce que l'erreur est au moins détectée et rapportée lors de l'exécution ?
Si vous parvenez à écrire du code qu'on ne peut pas mal utiliser, vous pouvez considérer que c'est du code de qualité.




1 Comments:
Pour ma part, j'aurais utiliser des classes de traits pour ton histoire de dates.
By
Alp Mestan, at samedi, 26 janvier, 2008
Enregistrer un commentaire
Links to this post:
Créer un lien
<< Home