Le premier article affirme :
« Le choix de remonter des exceptions, depuis le lieu de la détection de la rupture de contrat, est un choix de programmation défensive. C'est un choix que j'assimile à une déresponsabilisation des véritables responsables. »
« Il est vrai que la programmation défensive permet d'une certaine façon de centraliser et factoriser les vérifications. Mais les vérifications ainsi centralisées ne disposent pas du contexte qui permet de remonter des erreurs correctes. Il est nécessaire d'enrichir les exceptions pauvres en les transformant au niveau du code client, et là on perd les factorisations. »
Cependant, il y a des erreurs dans le code qui illustre les idées ci-dessus :
1 2 3 4
| double my::sqrt(double n) {
if (n<0) throw std::domain_error("Negative number sent to sqrt");
return std::sqrt(n);
} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| void my::process(boost::filesystem::path const& file) {
boost::ifstream f(file);
if (!f) throw std::runtime_error("Cannot open "+file.string());
double d;
while (f >> d) {
double sq = 0;
try {
sq = my::sqrt(d);
}
catch (std::logic_error const&) {
throw std::runtime_error(
"Invalid negative distance " + std::to_string(d)
+" at the "+std::to_string(l)
+"th line in distances file "+file.string());
}
my::memorize(sq);
}
} |
Erreur d'étourderie : La variable
l, qui indique le numéro de ligne, n'est pas déclarée.
Autre erreur : Par convention,
std::logic_error, c'est pour une erreur de programmation, pas pour une erreur d'une donnée en entrée d'un programme (comme un fichier).
Le genre de code critiqué, ce serait plutôt celui-ci :
1 2 3 4
| double my::sqrt(double n) {
if (n<0) throw std::runtime_error("Negative number sent to sqrt: " + std::to_string(n));
return std::sqrt(n);
} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void my::process(boost::filesystem::path const& file) {
boost::ifstream f(file);
if (!f) throw std::runtime_error("Cannot open "+file.string());
double d;
while (f >> d) {
double sq = 0;
try {
sq = my::sqrt(d);
}
catch (std::runtime_error const& e) {
throw std::runtime_error(
"Error in distances file "+file.string()+": "+e.what());
}
my::memorize(sq);
}
} |
Dans le code ci-dessus, on ne perd pas entièrement la factorisation, car une partie du message d'erreur est gérée par
my::sqrt.
Mais il y a bien une déresponsabilisation du code appelant. En effet, l'appelant se dira qu'il n'aura pas toujours besoin de lancer une exception de type
std::runtime_error quand un nombre en entrée du programme attendu comme positif est strictement négatif car, s'il donne ce nombre en argument à
my::sqrt, c'est
my::sqrt qui fera le travail.
Le problème de cette déresponsabilisation, c'est que le jour où
my::sqrt recevra un argument strictement négatif suite à une vraie erreur de programmation, l'erreur ne sera pas signalée sous la forme d'erreur de programmation (par exemple avec une exception de type
std::logic_error).
Alors, le code spécifique à la gestion des erreurs de programmation (par exemple un bloc
catch(std::logic_error const& e)) ne sera pas appelé.
Et le jour où un développeur voudra que le programme détecte mieux les erreurs de programmation en redéfinissant
my::sqrt en :
1 2 3 4 5
| double my::sqrt(double n) {
assert(n>=0 && "sqrt can't process negative numbers");
if (n<0) throw std::logic_error("Negative number sent to sqrt: " + std::to_string(n));
return std::sqrt(n);
} |
il va se heurter à des faux positifs à cause du code
legacy qui partait de l'hypothèse que
my::sqrt pouvait recevoir un nombre strictement négatif.
Au quotidien, les fois où je rencontre le plus cette déresponsabilisation, ce sont les fonctions sans assert qui commencent par un if et qui ne font rien quand la condition est fausse. Comme ça, les fois où la condition est fausse à cause d'une erreur de programmation, l'erreur n'est détectée que beaucoup plus tard voire n'est même pas détectée.
Cela dit, il est dommage que le premier article fasse parfois l'amalgame entre lancer une exception et déresponsabiliser le code appelant. Ce n'est normalement pas le cas quand l'exception lancée dérive de
std::logic_error.
Heureusement, il ne fait pas toujours cet amalgame. Je cite un passage qui ne le fait pas :
« Si la PpC s'intéresse à l'écriture de code correct, la programmation défensive s'intéresse à l'écriture de code robuste. L'objectif premier n'est pas le même (dans un cas on essaie de repérer et éliminer les erreurs de programmation, dans l'autre on essaie de ne pas planter en cas d'erreur de programmation), de fait les deux techniques peuvent se compléter.
[...]
À vrai dire, on peut utiliser simultanément ces deux approches sur de mêmes contrats. En effet, il est possible de modifier la définition d'une assertion en mode Release pour lui faire lancer une exception de logique. En mode Debug elle nous aidera à contrôler les enchaînements d'opérations. »
2 |
1 |