Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

Programmation défensive : faut-il vraiment tester les paramètres des méthodes ?

La programmation défensive c’est bien. Mais faut-il vraiment tester la validité de tous les paramètres des méthodes, n’y-a-t-il pas plus subtile ?

Self-Défense

image

Il y a deux types de code, celui qui ne teste rien et laisse voguer la galère en se disant que les exceptions finiront bien par remonter jusqu’à un message d’erreur, et celui qui s’attache à tout tester en permanence pour éviter les problèmes.

La programmation défensive consiste à anticiper les ennuis. Par exemple vérifier qu’un champ n’est pas null avant de s’en servir, qu’une donnée numérique n’est pas à zéro avant de l’utiliser comme dénominateur dans une division, etc.

Se défendre c’est très bien, anticiper c’est génial, mais à la fin le code est constellé de tests qui en brouillent la compréhension et qui multiplient la maintenance. Pire cela gonfle le code des objets et de leurs méthodes et ralentit systématiquement les traitements là où parfois on préfèrerait plus de vitesse quitte à tester en amont. Par exemple une méthode appelée 100000 fois dans une boucle peut fort bien avoir ses paramètres validés en entrée de boucle si cela est possible, ce qui fera gagner du temps de calcul. Hélas avec la programmation défensive classique le développeur n’a pas ce choix. Si la méthode est “blindée” elle sera 100000 fois appelée avec son blindage même si ponctuellement cela n’était pas nécessaire.

A la place de cette approche classique limitée je vous propose de découvrir les décorateurs de validation.

Blindage de code

Prenons un code typique tel qu’on en écrit tous les jours afin de mieux comprendre le changement de mode de pensée que les décorateurs de validation impliquent.

Par exemple un cas (fictif pour simplifier le code) d’une classe générant un fichier de données sur disque :

public class Report
{
	public void Export(string fileName)
	{
		if (string.IsNullOrWhiteSpace(fileName))
		{
			var msg = "Le nom du fichier ne peut être null, vide ou uniquement fait d'espaces";
			Logger.Log(msg);
			throw new Exception(msg);
		}

		if (File.Exists(fileName))
		{
			var msg = "Le fichier existe déjà !";
			Logger.Log(msg);
			throw new Exception(msg);
		}
		
		// le code intéressant commence ici...
		// ...
	}
}

public static class Logger
{
	public static void Log(string message)
	{
		Console.WriteLine($"** Erreur: {message} **");
	}
}

Voilà un code qui sait se défendre !

Log des erreurs, exceptions avec messages pertinents, tout y est.

Si le nom du fichier est vide, plein d’espaces ou null une erreur spécifique est générée et même loggée ! Mieux si le fichier existe déjà une autre erreur sera déclenchée avec son log aussi.

On pourrait aussi trouver dans un tel code des variantes utilisant ArgumentNullException par exemple.

Bref, c’est bien, c’est protégé mais cela possède plusieurs inconvénients évoqués en début d’article mais dont on peut palper ici la réalité :

  • C’est lourd ! Avant de lire le code utile de la méthode il faut sauter des tas de lignes qui n’ont rien à voir avec le traitement.
  • C’est lent ! Ok il s’agit de l’écriture d’un fichier et les I/O sont lentes par nature, donc c’est peu crucial ici. Mais même ici, si on génère des noms de fichiers sous la forme de GUID assurément uniques dans une boucle d’un million d’appel, quel temps perdu dans tous ces tests qui ne serviront en plus à rien dans ce cas particulier (le nom de fichier est non nul, non vide, et forcément unique donc le test d’existence assez long n’est pas utile).
  • Ce n’est pas modulable. Si dans ma boucle je sais que le nom de fichier sera unique et non vide, je n’ai pas la possibilité d’utiliser une version moins protégée mais plus rapide de la méthode, sauf à en écrire une autre et à commencer du code spaghetti…

Des décorateurs pour valider

C’est là qu’entre en scène les décorateurs de validation.

Dans l’un de mes derniers articles je vous parlais des Design Pattern, notamment celles du Gang Of Four, et de l’importance de toutes les connaitre pour savoir les utiliser avec inventivité. Voici aujourd’hui une mise en pratique très parlante.

Les décorateurs ? Dans la classification du GoF on les trouve dans la catégorie des patterns structurels. Pour rappel : Avec ce pattern on dispose d’un moyen efficace pour accrocher de nouvelles fonctions à un objet dynamiquement lors de l’exécution. Cela ressemble à la fois un peu aux interfaces et à l’héritage mais c’est une troisième voie offrant des avantages différents. Les nouvelles fonctionnalités sont contenues dans des objets, l’objet parent étant dynamiquement mis en relation avec ces derniers pour en utiliser les capacités.

Connaitre et pratiquer les patterns ouvre l’esprit. C’est ce qui fait d’un développeur moyen un bon développeur, entre autres choses.

Car celui qui a pigé ce qu’est un décorateur, pas seulement en comprenant ma petite définition au coin d’un article mais en pratiquant, testant, en intégrant mentalement toute la portée de ce Design pattern, celui-là trouve ici une idée géniale de les utiliser pour simplifier son code, le rendre plus rapide, plus efficace, plus facilement maintenable. Peut-être même que le développeur lui-même en sortira plus beau, plus fort et plus séduisant ! (Bon là j’exagère un peu, mais pas tant que ça… écrire du bon et beau code rend sûr de soi, et les grands séducteurs(rices) sont avant tout des gens sûr d’eux !).

image

 

Aller, un peu d’UML ça fait du bien aussi. Ci-dessus on voit le schéma général d’application du pattern Décorateur.

Mais quel est le rapport avec la validation d’un nom de fichier (ou de n’importe quoi d’autre d’ailleurs) ?

La réponse est dans l’abandon des vieux réflexes de la programmation défensive et l’application inventive des DP : les décorateurs de validation.

Comment ?

Ceux qui suivent le mieux ont compris ou commencent à comprendre… Les décorateurs ajoute dynamiquement au runtime des possibilités nouvelles à des classes. Et on peut décorer un décorateur par un autre décorateur ce qui permet de composer une chaîne selon ses besoins… Ca y est vous y êtes ?

Non ? Pas grave, je vais expliquer !

Dans la version classique c’est la méthode Export de notre exemple qui s’occupe de valider systématiquement les arguments qui lui sont passés, ici un string contenant le nom du fichier. Cette validation, dans notre exemple, s’assure que le nom de fichier est non null, non vide, non constitué d’espaces et que le fichier n’existe pas déjà. On pourrait selon le fonctionnel ajouter d’autres tests comme tester la validité du nom (il peut satisfaire toutes les conditions listées mais être non valide pour l’OS). Et notre méthode s’alourdirait encore plus, devant de moins en lisible, de moins en moins maintenable et surtout de plus en lente systématiquement.

Grâce au DP Décorateur nous allons changer la façon de penser la programmation défensive. Nous allons écrire un code pur sans test, rapide, maintenable, puis nous allons le décorer pour assurer les tests. A l’utilisation nous pourrons composer une chaîne de décorateurs ajoutant chacun un test, chaîne dont la lecture indiquera clairement nos intentions. Et si nous ne voulons appliquer qu’un test ou aucun pour gagner en vitesse, cela sera possible.

D’abord partons d’une Interface. Les interfaces sont partout dans la programmation moderne, c’est normal c’est un concept aussi simple qu’il est génial…

public interface IReport
{
	void Export(string fileName);
}
Rien de compliqué.

Maintenant construisons une classe qui supporte cette interface :

public class DefaultReport : IReport
{
	public void Export(string fileName)
	{
		// Code utile uniquement !
	}
}

Ultra simple. Ce “DefaultReport” sera la version qui fera le travail d’écriture sur disque. Pas de test des paramètres (ici fileName). C’est la version “rapide”.

Maintenant construisons une série de décorateurs pour assurer la validation des paramètres :

public class NoWriteOverReport : IReport
{
	private IReport origin;

	public NoWriteOverReport(IReport report)
	{
		origin = report;
	}

	public void Export(string fileName)
	{
		if (File.Exists(fileName))
		{
			var msg = "Le fichier existe déjà !";
			Logger.Log(msg);
			throw new Exception(msg);
		}
		origin.Export(fileName);
	}
}

public class NoNullReport : IReport
{
	private IReport origin;

	public NoNullReport(IReport report)
	{
		origin = report;
	}

	public void Export(string fileName)
	{
		if (string.IsNullOrWhiteSpace(fileName))
		{
			var msg = "Le nom de fichier ne peut être vide, nul ou fait d'espaces.";
			Logger.Log(msg);
			throw new Exception(msg);
		}
		origin.Export(fileName);
	}
}

Ici nous avons créé deux classes implémentant l’interface IReport. Le principe est celui du décorateur : le constructeur de ces classes prend un IReport en paramètre, celui qui va être décoré. Le décorateur est lui-même un IReport car le décorateur se substitue à la classe décorée de façon transparente.

Un premier décorateur permet de valider le nom du fichier, un second s’occupe de vérifier si le fichier existe ou non.

Chaque classe a une responsabilité clairement définie, elle ne réinvente pas la roue et exploite le savoir faire de la classe décorée en y ajoutant son propre code de portée limitée et facilement maintenable.

Le développeur va ainsi avoir la liberté d’utiliser soit la classe par défaut, rapide et simple mais sans tests, soit les décorateurs, ceux qu’ils veut, quand il le veut, et dans l’ordre qu’il préfère.

Ainsi pourra-t-on écrire :

IReport myReport;
	myReport = new DefaultReport();
	myReport.Export("toto.dat");

	myReport = new NoNullReport(new DefaultReport());
	myReport.Export("toto.dat");

	myReport = new NoWriteOverReport(new DefaultReport());
	myReport.Export("toto.dat");

	myReport = new NoNullReport(new NoWriteOverReport(new DefaultReport()));
	myReport.Export("toto.dat");

	myReport = new NoWriteOverReport(new NoNullReport(new DefaultReport()));
	myReport.Export("toto.dat");

 

Dans le premier cas le développeur assume les tests et préfère la vitesse, dans le deuxième et troisième cas un seul test est choisi, dans les deux derniers les deux tests sont composés mais dans un ordre différent.

Au final le code d’exécution est toujours le même – myReport.Export(filename) – ce qui est clair et simple.

De la même façon la composition des opérateurs rend les intentions du développeur tout aussi claires et simples, lisibles.

Conclusion

La maitrise des Design Pattern est l’une des flèches de l’arc du bon développeur. L’inventivité en est une autre.

Grâce à un simple DP datant du GoF que tout le monde croit connaitre on peut inventer une nouvelle façon d’écrire du code défensif plus léger, plus clair, plus lisible et plus efficace pouvant s’adapter, se moduler selon les besoins. Les intentions du développeur sont tout aussi lisible et accessibles du premier coup d’œil.

Le code résultant est aussi bien plus facile à réutiliser. D’abord parce qu’il utilise une interface permettant un découplage fort entre code et contrat. Mais aussi parce qu’il devient très facile d’ajouter des validations non prévues au départ sans jamais modifier le code du DefaultReport qui fait le travail efficace. On minimise les risques de régression au fil de la maintenance qui est rendue plus simple car les classes sont plus petites, mieux ciblées sur une responsabilité unique.

Voici donc un bon exemple de l’importance des DP et du rôle primordial que l’imagination et la créativité jouent dans notre métier, comme dans tout métier d’art.

Un mauvais développeur sera facilement remplacé un jour par un robot. Un bon développeur possèdera toujours ce petit plus qui nous différencie des machines. Et malgré les délires à la mode, l’avènement d’une IA capable de totalement remplacer l’humain dans ce qu’il a de plus complexe, comme la créativité, est vraiment loin d’arriver. Mais il est vrai que ceux qui se contentent d’être des pions interchangeables ont du souci à se faire. Heureusement les lecteurs de Dot.Blog ne sont pas de cette espèce ! Clignement d'œil

Au passage j’aime toujours rendre à César ce qui lui appartient et cette merveilleuse idée de décorateurs de validation me vient d’un article de Yegor Bugayenko à propos de Java… Comme quoi un bon développeur doit aussi savoir garder son esprit ouvert et ne pas se contenter de son petit monde ! Vive la créativité, vive la diversité du monde … et …

Stay Tuned !

blog comments powered by Disqus