Table des matières
Une vieille technique de développement en informatique[1] dont nous ferons ici abondamment usage, est la technique dite «de la boîte noire».
Le développement en boîte noire est aussi ce que certains appellent de la planification «top-down», c'est à dire «de haut en bas», ou--plus exactement--du plus vague au plus précis. C'est une technique qui rend de fiers services lors des travaux volumineux, surtout dans le cas de travail d'équipe.
En procédant par boîte noire, on procède à une subdivision fonctionnelle de l'algorithme propre au problème à résoudre, et donc à un découpage du problème qui nous est soumis en «sous-problèmes» plus simples, comme vous y êtes déjà habitués.
On imagine alors chaque algorithme servant à résoudre ces «sous-problèmes» comme une boîte noire. Pour chacune de ces boîtes noires:
Cette technique demande d'avoir un peu d'expérience de programmation, puisqu'elle demande une compréhension suffisante des techniques de résolution de problèmes pour être capable d'arriver à bien subdiviser un problème en «sous-problèmes» qui, pris individuellement, sont solubles.
C'est pourquoi on ne l'enseigne pas, généralement, aux débutant(e)s. Un découpage qui manque de clarté devient un obstacle plutôt qu'une aide additionnelle.
Note: | Le cycle de développement par boîte noire peut être vu comme la séquence d'opérations suivantes: découpage en modules, simulation du fonctionnement des modules; et intégration des modules. |
On y trouve plusieurs avantages. Par exemple:
En fait, si on se limite au monde des fonctions, vous procédez depuis longtemps à ce genre de découpage. À chaque fois que vous faites usage d'une fonction ou d'un objet rendu disponible par le langage ou venant d'une librairie sans savoir comment cet objet ou cette fonction ont été implantés, vous procédez--en quelque sorte--à une forme de développement en boîte noire.
Lire le contenu d'un fichier contenant, sur chaque ligne, un enregistrement constitué d'un nom de famille et d'un prénom, séparés d'une virgule; puis produire un nouveau fichier contenant tous ces enregistrements mais triés par ordre alphabétique de nom de famille.
On peut dire que l'algorithme suivant est une solution valable au problème:
Algo Solution1 (E: Fichier en entrée, S: Fichier en sortie) Lire le contenu de E dans un tableau T Trier le tableau T Écrire le contenu du tableau T dans le fichier S Fin Solution1 |
On aurait alors besoin de passer à l'algorithme Solution1(E,S) deux fichiers (disons deux noms de fichiers, puisqu'on veut définir immédiatement les interfaces requises).
Nous dirons ici que les paramètres E et S de l'algorithme Solution1() sont des paramètres en entrée, ou des intrants. Nous chercherons à définir les intrants et les extrants (paramètres en sortie) de chacun de nos modules, de façon à clarifier ce que chacun doit aux autres, et sous quelle forme: de façon à connaître à l'avance chaque interface de module à module.
Notre subdivision courante résulte en trois modules, qui sont:
L'examen les intrants et des extrants de cette subdivision nous donne:
On peut donc dores et déjà définir les interfaces des trois modules, et subdiviser le travail entre coéquipiers:
Nos interfaces ne sont pas encore complètes, par contre: il faudra s'entendre entre coéquipiers à savoir comment seront structurés les paramètres, et en particulier ici comment sera représenté un tableau d'enregistrements
Par exemple, en langage C ou C++, il faudra passer la taille du tableau en paramètre avec le tableau à chaque fois, puisque la taille d'un tableau n'est pas encodée dans le tableau lui-même.
On pourrait par exemple représenter un enregistrement comme:
...et on pourrait trouver d'autres représentations, toutes aussi valides.
Cas (0) |
Cas (1) |
Cas (2) |
---|---|---|
typedef char* enreg; // ou typedef string enreg; enreg T[MAX]; |
typedef struct { string nom; string prenom; } enreg; enreg T[MAX]; |
class enreg { // ... string nom; string prenom; // ... }; enreg T[MAX]; |
Chacune de ces options a du bon; l'important est de s'entendre dans l'immédiat, pour que tous puissent commencer à travailler, et pour que la jonction des différentes boîtes soit, en bout de ligne, facile.
Note: | on vous a raconté plusieurs fois que l'un des bénéfices de la programmation orientée objet est la technique de l'encapsulation, selon laquelle on cherchera à envelopper les structures de données de manière à en cacher les détails au monde extérieur. |
La définition d'interfaces entres les systèmes se prête bien à l'application de cette technique: en prétendant qu'on a un type nommé enregistrement (ou, dans le tableau plus haut, enreg), on peut se dire a priori qu'on utilisera comme interface entre les systèmes des tableaux d'enregistrements.
Ainsi, chacun peut travailler à ses algorithmes et à leur implantation, sans avoir à se soucier outre mesure des détails. Lorsque le moment s'y prêtera (cela peut se faire dès la subdivision initiale du travail comme cela peut se faire peu avant l'intégration des différents modules), l'équipe se réunira pour définir ce qu'on attend précisément du type représentant un enregistrement, et les ajustements aux implantations individuelles pourront être apportés avant de procéder à l'intégration officielle de ses différentes composantes.
Chaque équipier peut alors développer son (ou ses) module(s) en simulant le bon fonctionnement des autres.
Par exemple: on peut écrire le module Trier(TE,TS) en supposant se faire donner toujours le même tableau TE, qu'on pourra simuler par une constante:
const int MAX = 5; // Lire(E,T,taille) simulera la lecture d'un fichier // contenant exactement le contenu d'un fichier à // l'aide du tableau FichierSimule[] // Entrée: la chaîne de caractères E (nom du fichier; inutilisé) // Sortie: le tableau d'enregistrements T[] (rempli) // Sortie: le nombre d'enregistrements taille (à jour) void Lire (string E, enreg T[], int& taille) { const enreg FichierSimule[MAX] = { "Tremblay,Roger" , "Casanova,Fidel" , "Cadorette,Jean" , "Biquette,Sheila" , "Fernandel,Gontrane" }; for (taille = 0; taille < MAX; taille++) { T[taille] = TE[taille]; // ou opération équivalente } } // Lire(string,enreg[],int&) |
// Trier(TE,taille,TS) simulera la lecture d'un fichier // contenant exactement le contenu d'un fichier à // l'aide du tableau FichierSimule[] // Entrée: le tableau d'enregistrements TE[] (rempli) // Entrée: le nombre d'enregistrements taille dans TE[] // Sortie: le tableau d'enregistrements TS[] (trié) void Trier (enreg TE[], int taille, enreg TS[]) { // votre code pour le module Trier(TE,taille,TS) } // Trier(enreg[],int,TS[]) |
void main () { enreg T[MAX]; int nbElements; Lire ("NomQuelconque.dat", T, nbElements); // simulé Trier (T, resultat, T); // notre code // petite boucle pour vérifier le résultat } |
En simulant le bon fonctionnement de la fonction devant produire le tableau d'enregistrements requis pour le module Trier(TE,taille,TS), on pourra ainsi faire comme si ce module était présent et opérationnel. Il se trouve que dans notre simulation, ce module produit toujours le même résultat. Et puis? Dans la mesure où la véritable tâche est simulée, notre module pourra être testé de manière véritable.
Chacun des membres de l'équipe de développement peut ainsi procéder à la production et à la mise au point de sa/ses composante(s) en «faisant comme si» les composantes des autres étaient disponibles et opérationnelles.
L'intégration des composantes pourra alors se faire. Si tous les membres de l'équipe ont respecté à la lettre les interfaces définies au tout début, cette phase d'intégration devrait se limiter à remplacer les modules simulés par les modules véritables, et à recompiler.
En pratique, ce n'est jamais si simple. Une phase d'intégration demande pratiquement toujours de légers ajustements, allant du cosmétique (une majuscule au lieu d'une minuscule dans le nom d'un type de données ou de module) au sérieux (un module procédant à une manipulation imprudente de pointeurs et qui fait s'écraser les autres modules, sans qu'on ne parvienne à détecter aisément lequel des modules est fautif).
En général, toutefois, une interface bien définie et respectée au mieux des capacités de chacun diminue singulièrement le temps et la difficulté d'intégration des modules.
Un avantage «caché» du développement par boîte noire est qu'il augmente de beaucoup les probabilités de réutilisation du code. En effet, si un module a été pensé en terme d'intégration éventuelle avec d'autres modules, avec des interfaces claires dès le début, on obtiendra un module qui aura en général tendance à bien s'intégrer avec d'autres modules.
Réutiliser des modules permet de diminuer le temps passé à «réinventer la roue», et permet de commercialiser plus rapidement le fruit de nos efforts. Les anglophones ont un nom pour ce genre de développement basé sur des composantes faciles à assembler entre elles: ils appellent cela le «component-based programming» (ou programmation par composantes). On voit en pratique que cette manière de faire les choses permet de mettre sur pied des programmes efficaces en un tournemain.
Autre avantage significatif du développement par boîte noire: ayant défini dès le début les modules et leurs interfaces respectives, on obtient pratiquement un système «pré-documenté», ou du moins dont la documentation est grandement simplifiée. On a dès le tout début du développement:
Cela permet de produire rapidement et efficacement, pour les clients comme pour l'employeur, une documentation cohérente et utile du système informatique en développement. Les entreprises manquant trop souvent de temps à allouer à la rédaction et à la mise au point de la documentation des différents systèmes, être en mesure de la produire systématiquement est un avantage compétitif indéniable.
De même, connaître dès le début les détails de l'interface entre chaque paire de modules dans un système permet d'en tester efficacement le fonctionnement.
En effet, en offrant à un module une banque d'intrants conçue en conséquence et en vérifiant de manière systématique les extrants correspondants, on peut valider efficacement le bon fonctionnement du module, et on peut produire une liste d'intrants «à risque», ce qui facilite, pour l'équipe de développement, la tâche de maintenance et de mise au point des modules (en sachant lesquels des intrants produisent des résultats incorrects, les informaticien(ne)s sont en mesure de vérifier pourquoi le module ne fonctionne pas correctement pour ces intrants bien précis).
Table des matières
[1] ...elle-même tout droit inspirée de bonnes vieilles techniques mathématiques que vous avez tous déjà survolées au secondaire...