
Attributs de qualité des programmes informatiques : implémenter la robustesse logicielle
- 15 août 2024
- 13 mins de lecture
- Qualité logicielle
Table des matières
Introduction à la robustesse
La robustesse est un attribut de qualité logiciel crucial qui mesure la capacité d’un logiciel à fonctionner correctement dans des conditions défavorables.

Cet article explore les aspects clés à considérer pour rendre votre logiciel robuste.
La norme de vocabulaire de l’ingénierie des systèmes et du logiciel de l’IEEE définit la robustesse comme suit :
degree to which a system or component can function correctly in the presence of invalid inputs or stressful environmental conditions.
ISO/IEC/IEEE 24765:2017 Systems and software engineering — Vocabulary
Cette définition met en évidence la capacité d’un système à résister aux contraintes externes. Pour saisir pleinement le périmètre de la robustesse logicielle, nous la décomposons en deux attributs clés : tolérance aux erreurs et tolérance aux fautes.

Bulletin d'information
Abonnez-vous à notre bulletin d'information et restez informé(e).
Tolérance aux erreurs
Ability of a system or component to continue normal operation despite the presence of erroneous inputs.
ISO/IEC/IEEE 24765:2017 Systems and software engineering — Vocabulary
La tolérance aux erreurs est essentielle pour un logiciel robuste, en garantissant qu’un système continue de fonctionner correctement même lorsqu’il rencontre des entrées invalides ou inattendues.
Un logiciel avec une bonne tolérance aux erreurs ne doit pas échouer ni entrer dans un état inattendu face à des entrées invalides.
Mise en œuvre de la tolérance aux erreurs
Pour rendre un programme tolérant aux erreurs :
- Validation des entrées : validez et assainissez toutes les entrées, y compris les configurations, avant de les traiter. Vérifiez le format, la plage et le type. Cela peut être réalisé à l’aide de la gestion d’erreurs.
- Gestion des erreurs : utilisez des blocs try-except (ou équivalents) pour intercepter et traiter gracieusement les erreurs d’entrée. En cas d’erreur, fournissez un retour clair à l’utilisateur.
- Retour utilisateur : fournissez des messages d’erreur clairs et informatifs, ou des logs. Ensuite, appliquez un mécanisme de sécurité par défaut.
- Mécanismes de sécurité : assurez-vous que le logiciel bascule vers un état sûr lorsque des erreurs d’entrée sont détectées.
Exemple : gestion des erreurs de saisie de score
Considérons un programme simple qui lit le score d’un étudiant (0-100) et affiche la note correspondante (A, B, C, D, F). Il assure la robustesse en gérant les erreurs de saisie comme les valeurs non numériques ou les scores hors plage.
1import bisect
2
3def score_to_grade():
4 while True:
5 # Lire l’entrée
6 score_str = input("Enter the student's score (0-100): ")
7 try:
8 # Convertir l’entrée en flottant
9 score = float(score_str)
10 # Vérifier la plage du score
11 if not (0 <= score <= 100):
12 raise ValueError("Score must be between 0 and 100")
13 except ValueError as e:
14 # Gérer l’erreur de conversion ou de plage
15 print(f"Invalid input: {e}. Please try again.")
16 else:
17 # Tout s’est bien passé (entrée validée). L’entrée assainie est `score`
18 break
19
20 # Seuils pour les notes
21 breakpoints = [60, 70, 80, 90]
22 # Notes correspondantes
23 grades = ["F", "D", "C", "B", "A"]
24 # Trouver la note appropriée avec bisect
25 the_grade = grades[bisect.bisect(breakpoints, score)]
26
27 print(f"The student's grade for {score_str} is {the_grade}")
28
29if __name__ == "__main__":
30 score_to_grade()
Dans cet exemple, la tolérance aux erreurs est mise en œuvre ainsi :
- Convertir l’entrée en flottant et vérifier que le score est compris entre 0 et 100.
- Intercepter toute erreur de conversion ou de plage.
- Informer l’utilisateur et lui demander de réessayer.
Tolérance aux fautes
1. degree to which a system, product or component operates as intended despite the presence of hardware or software faults 1.
2. pertaining to the study of errors, faults, and failures, and of methods for enabling systems to continue normal operation in the presence of faults.ISO/IEC/IEEE 24765:2017 Systems and software engineering — Vocabulary
La tolérance aux fautes désigne la capacité d’un système à continuer de fonctionner correctement même lorsque certains de ses composants échouent. Ce concept est essentiel pour concevoir des systèmes logiciels fiables.
La tolérance aux fautes est liée à la robustesse des composants logiciels non défaillants : la capacité de ces composants à supporter les contraintes imposées par les composants défaillants.
Elle implique la tolérance aux fautes de dépendance, la tolérance environnementale et la tolérance aux défaillances.
Tolérance aux fautes de dépendance
La tolérance aux fautes de dépendance consiste à s’assurer que les fautes dans les composants dépendants (comme les bibliothèques externes) ne provoquent pas de défaillances sur l’ensemble du logiciel.
Stratégies pour améliorer la tolérance aux fautes de dépendance
Pour améliorer la tolérance aux fautes de dépendance, appliquez des stratégies de tolérance aux erreurs aux valeurs retournées par les appels d’API vers vos dépendances.
Exemple : gestion des fautes dans un appel de bibliothèque
Lorsque votre logiciel dépend d’une bibliothèque externe pour effectuer des tâches, il est important de valider les valeurs retournées pour s’assurer qu’elles ont du sens.
Supposons que vous ajoutiez une fonctionnalité au programme ci-dessus « score to grade », où le score est envoyé à une bibliothèque qui récupère le classement de l’école depuis un serveur distant.
1import logging
2
3def get_ranking(api_client, score):
4 try:
5 ranking = api_client.get_school_ranking()
6
7 # Tolérance aux fautes : vérifier que le classement est un entier positif valide
8 if type(ranking) != int or ranking <= 0:
9 raise ValueError("Received an invalid ranking value")
10
11 return ranking
12
13 except (ValueError, ConnectionError) as e:
14 # Gérer la faute : enregistrer l’erreur et retourner une valeur sûre par défaut
15 logging.error(e)
16 return None
Dans cet exemple :
- La fonction
get_rankingutilise un client d’API externe pour récupérer des données de classement. - La tolérance aux fautes de dépendance est mise en œuvre en vérifiant que le classement retourné est un entier positif. Sinon, une
ValueErrorest levée, puis interceptée et traitée. - L’erreur est enregistrée, et une valeur par défaut est retournée, garantissant qu’un problème avec la bibliothèque externe ne fasse pas échouer tout le programme.
Tolérance environnementale
La tolérance environnementale garantit qu’un logiciel continue de fonctionner correctement dans des conditions stressantes ou défavorables, telles que des limitations de ressources, des pannes de ressources ou des défaillances matérielles.
Stratégies pour améliorer la tolérance environnementale
Pour améliorer la tolérance environnementale de votre logiciel, envisagez les approches suivantes :
Approche 1 - Gestion
- Timeouts : lors de l’accès à des ressources externes, utilisez des timeouts pour empêcher les programmes de se bloquer indéfiniment.
- Retries : mettez en œuvre des logiques de retry pour gérer les erreurs transitoires d’accès aux ressources, afin que les problèmes temporaires ne deviennent pas des défaillances permanentes.
- Retour utilisateur : fournissez des messages d’erreur clairs et informatifs ou des logs.
- Redondance et basculement automatique (implémentation au niveau système) : incorporez de la redondance dans la gestion des ressources, par exemple en utilisant des serveurs de secours ou des systèmes de stockage redondants, afin d’assurer la continuité du service même lorsque certaines ressources échouent.
Exemple : gestion
Lorsqu’un programme tente d’accéder à une ressource, telle qu’un fichier ou un service réseau, et que cette ressource est indisponible ou inaccessible, le programme doit gérer cette situation avec élégance.
Cela peut impliquer de retenter l’accès, de basculer vers une ressource de secours ou de fournir un message d’erreur clair. La stratégie réelle dépend du type de logiciel.
Voyons comment nous pourrions implémenter la bibliothèque utilisée dans notre programme « score to grade ». Cette bibliothèque utilise une connexion socket TCP pour communiquer avec un serveur distant afin d’obtenir le classement scolaire en fonction du score.
1import socket
2import time
3import logging
4
5logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
6
7PRIMARY_SERVER = ('primeranking.buildsoftwaresystems.com', 7000)
8BACKUP_SERVER = ('secondranking.buildsoftwaresystems.com', 7000)
9RETRY_COUNT = 3
10RETRY_DELAY = 2 # seconds
11
12def connect_to_server_and_get_rank(server_address, score):
13 """Tentative de connexion au serveur spécifié."""
14 try:
15 with socket.create_connection(server_address, timeout=5) as sock:
16 logging.info(f"Successfully connected to {server_address[0]}:{server_address[1]}")
17 # TODO: Send the score to the remote server and receive the school rank
18 rank = ...
19
20 return rank
21 except (socket.timeout, socket.error) as e:
22 logging.error(f"Failed to connect to {server_address[0]}:{server_address[1]} - {e}")
23 return None
24
25def get_school_rank(score):
26 for attempt in range(RETRY_COUNT):
27 rank = connect_to_server_and_get_rank(PRIMARY_SERVER, score)
28 if rank is not None:
29 return rank
30 logging.info(f"Retrying connection to primary server in {RETRY_DELAY} seconds... ({attempt + 1}/{RETRY_COUNT})")
31 time.sleep(RETRY_DELAY)
32
33 logging.info("Primary server unavailable. Attempting to connect to backup server...")
34 rank = connect_to_server_and_get_rank(BACKUP_SERVER, score)
35 if rank is not None:
36 return rank
37
38 raise ConnectionError("Failed to connect to backup server. Please check your network or server status.")
Dans cet exemple :
La fonction connect_to_server_and_get_rank tente de se connecter au serveur spécifié et d’obtenir le classement scolaire pour un score donné. Si la connexion échoue, elle enregistre l’erreur et renvoie None.
La fonction get_school_rank essaie d’abord d’obtenir le classement en se connectant au serveur principal. Si cela échoue après quelques retries, elle bascule vers le serveur de secours.
Si les deux serveurs sont inaccessibles, elle lève une ConnectionError. Ce dispositif garantit que le programme peut continuer à fonctionner même si le serveur principal est en panne, tout en signalant clairement un problème si les deux serveurs échouent.
Approche 2 - Prévention
- Surveillance des ressources : mettez en place des systèmes de surveillance qui suivent l’utilisation des ressources critiques comme la mémoire, l’espace disque et le CPU. Cela permet au logiciel de prendre des mesures proactives avant que l’épuisement des ressources ne provoque une défaillance.
- Dégradation élégante : concevez votre logiciel pour qu’il se dégrade élégamment en cas de contraintes sur les ressources. Par exemple, lorsque la mémoire est faible, le logiciel peut désactiver les fonctionnalités non essentielles ou réduire sa fonctionnalité tout en maintenant les opérations principales.
- Retour utilisateur : fournissez des messages d’erreur clairs et informatifs ou des logs.
Exemple : prévention
Un serveur qui enregistre son activité dans des fichiers peut surveiller l’utilisation du disque et désactiver automatiquement la journalisation si l’espace disque devient critique.
Avant de désactiver, il doit enregistrer un message d’avertissement. La journalisation peut être réactivée lorsque l’espace disque redevient sûr.
1import shutil
2import time
3import logging
4import threading
5
6logging.basicConfig(filename='server.log', level=logging.INFO, format='%(asctime)s - %(message)s')
7
8def get_disk_usage(path='/'):
9 """Fonction pour vérifier le pourcentage d’utilisation du disque."""
10 total, used, free = shutil.disk_usage(path)
11 return (used / total) * 100
12
13def monitor_disk_usage():
14 """Boucle principale pour surveiller l’utilisation du disque et contrôler la journalisation."""
15 thresholds = {'high': 90.0, 'low': 85.0}
16
17 while True:
18 disk_usage = get_disk_usage()
19 logging_enabled = logging.getLogger().isEnabledFor(logging.CRITICAL)
20 if disk_usage >= thresholds['high'] and logging_enabled:
21 logging.info("Disk usage critically high. Disabling logging.")
22 # Désactiver la journalisation
23 logging.disable()
24 elif disk_usage < thresholds['low'] and not logging_enabled:
25 # Réactiver la journalisation
26 logging.disable(logging.NOTSET)
27 logging.info("Disk usage back to safe level. Enabling logging.")
28 # Attendre un peu avant de vérifier à nouveau
29 time.sleep(5)
30
31def server_logic():
32 # espace réservé pour la logique du serveur
33 while True:
34 logging.info("sleeping 1s")
35 time.sleep(1)
36
37if __name__ == "__main__":
38 threading.Thread(target=monitor_disk_usage).start()
39 threading.Thread(target=server_logic).start()
Tolérance aux défaillances
La tolérance aux défaillances est la capacité d’un logiciel à continuer de fonctionner correctement même lorsque certains composants (processus, threads, tâches async, etc.) ou l’intégralité du logiciel échouent (ne peuvent pas fournir le service requis à l’utilisateur).
Stratégies pour améliorer la tolérance aux défaillances
Pour améliorer la tolérance aux défaillances de votre logiciel, envisagez de mettre en œuvre toutes les approches suivantes :
- Dégradation élégante : permettre au logiciel de continuer à fonctionner avec une fonctionnalité réduite en cas de défaillance partielle. Implémentez cela en scindant le logiciel en tâches indépendantes qui n’interfèrent pas directement les unes avec les autres à l’exécution (par exemple des processus OS).
- Surveillance et alertes : mettez en place une surveillance pour détecter les problèmes tôt et déclencher des alertes pour une réponse rapide.
- Récupération automatique : implémentez des points de contrôle et redémarrez les processus défaillants pour reprendre l’exécution depuis un état sain.
- Redondance et réplication (implémentation au niveau système) : assurez-vous que les services critiques ont des sauvegardes, de sorte que si l’un échoue, un autre puisse prendre le relais.
- Basculement automatique (implémentation au niveau système) : implémentez des systèmes capables de basculer automatiquement vers des composants de secours lorsqu’une défaillance est détectée. Cette mesure complète la redondance et la réplication.
Exemple : processus watchdog
Pour rendre un programme peu fiable plus robuste, un processus watchdog peut le surveiller et le redémarrer automatiquement s’il se plante.
1import subprocess
2import time
3
4def start_unreliable_process(command_list):
5 return subprocess.Popen(command_list)
6
7def watchdog(unreliable_process, command_list):
8 while True:
9 # Vérifier si le processus est toujours en cours
10 if unreliable_process.poll() is not None:
11 # Si le processus s’est terminé, le redémarrer
12 print("Unreliable process terminated. Restarting...")
13 unreliable_process = start_unreliable_process(command_list)
14 time.sleep(5) # Surveiller toutes les 5 secondes
15
16def run_unreliable_process(command_list):
17 # Démarrer le processus peu fiable
18 unreliable_process = start_unreliable_process(command_list)
19
20 # Démarrer le watchdog pour surveiller et redémarrer si nécessaire
21 watchdog(unreliable_process, command_list)
Dans cet exemple :
- La fonction
watchdogsurveille le processus peu fiable en vérifiant s’il est toujours en cours. - Si le processus peu fiable plante, le watchdog le détecte et appelle
start_unreliable_processpour redémarrer le programme.
Bulletin d'information
Abonnez-vous à notre bulletin d'information et restez informé(e).
Conclusion
Un logiciel robuste peut gérer tous les types d’entrées et de réponses des ressources externes, même dans des conditions ou des pannes inattendues.
Pour atteindre la robustesse, le logiciel doit être tolérant aux erreurs et aux fautes, y compris à la tolérance environnementale et à la tolérance aux défaillances. Il doit :
- Gérer toutes les formes d’entrées : valider et assainir les entrées pour éviter des comportements inattendus.
- Vérifier les erreurs d’accès aux ressources : vérifier l’accès aux ressources externes et gérer les problèmes de façon élégante.
- Mettre en œuvre des mécanismes de retry : pour les programmes de service, retenter l’accès aux ressources en cas d’échec. Vous pouvez trouver plus d’informations dans notre guide sur les stratégies de timeout, de retry et de backoff.
- Ignorer les erreurs d’accès non critiques : si un retry n’est pas approprié ou réalisable, gérer les erreurs d’accès aux ressources de manière à minimiser les perturbations.
- Utiliser des timeouts : empêcher les opérations de se bloquer indéfiniment en implémentant des timeouts.
- Retour utilisateur : fournir des messages d’erreur ou des logs clairs et informatifs.
Pour en savoir plus sur la robustesse et les attributs de qualité associés, consultez cet article.
Considérez-vous la robustesse dans votre développement logiciel ? Comment l’implémentez-vous ?
Faites-le nous savoir dans les commentaires ci-dessous.


