Table des matières

  • Technique du développement «en boîte noire»
  • - Qu'est-ce que cette technique?
  • -- Pourquoi ne l'enseigne-t-on pas plus tôt?
  • -- Quels en sont les avantages?
  • -- Des exemples...
  • --- Tri d'un fichier
  • ---- Étape 0: le problème
  • ---- Étape 1: une solution possible
  • ---- Étape 2
  • ---- Étape 3
  • ---- Étape 4
  • -- Développement par boîte noire et réutilisation du code
  • -- Développement par boîte noire et documentation
  • -- Développement par boîte noire et tests

  • Technique du développement «en boîte noire»

    Une vieille technique de développement en informatique[1] dont nous ferons ici abondamment usage, est la technique dite «de la boîte noire».

    Qu'est-ce que cette technique?

    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:

  • on écrira en langage naturel et simple la tâche accomplie qui sera accomplie lorsque l'algorithme qu'elle représente sera implanté. On dira alors qu'on a abstrait les détails du traitement, faisant semblant que ça marche tout en attendant à plus tard pour effectuer l'implantation détaillée;
  • on définira clairement l'interface (les entrées et les sorties) requise pour qu'elle fonctionne. Ceci nous permettra d'assembler les différentes boîtes noires sans vraiment avoir eu besoin d'implanter l'une ou l'autre d'entre elles.
  • Pourquoi ne l'enseigne-t-on pas plus tôt?

    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.

  • Chaque boîte noire peut aussi être appelée module ou composante du système.
  • 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.

    Quels en sont les avantages?

    On y trouve plusieurs avantages. Par exemple:

  • puisque chaque module est un algorithme en soi, on peut aussi le subdiviser et le simplifier encore plus de par cette même technique;
  • le découpage initial permet d'attribuer à chaque module une tâche précise. Comme vous le savez maintenant, les programmes les plus efficaces sont ceux où chaque fonction fait une et une seule chose; les modules les plus efficaces sont aussi ceux à qui on confie une tâche claire et précise. On obtient généralement, par cette technique, des programmes efficaces;
  • la subdivision en modules et la définition des interfaces tôt dans le cadre du développement fait en sorte de faciliter le partage des tâches, et ainsi le travail d'équipe, et donne une vision d'ensemble immédiate;
  • on peut développer l'une des boîtes faire semblant que chaque boîte noire fait le travail attendu d'elle, en simulant son comportement optimal. Ainsi, on peut développer l'un des modules en communiquant avec des modules fantoches qui donnent toujours la bonne réponse (simulée);
  • finalement, puisqu'on peut développer un module en simulant la bonne exécution des modules avec lesquels il communique, cette technique mène directement à celle du prototypage rapide, fort utile et que nous allons étudier sous peu.
  • Des exemples...

    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.

    Tri d'un fichier

    Étape 0: le problème

    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.

    Étape 1: une solution possible

    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:

  • lire le fichier E dans un tableau T;
  • prendre un tableau (appelons-le TE) et le trier (dans un tableau, nommons-le TS);
  • écrire le contenu d'un tableau T dans le fichier S.
  • L'examen les intrants et des extrants de cette subdivision nous donne:

    Étape 2

    On peut donc dores et déjà définir les interfaces des trois modules, et subdiviser le travail entre coéquipiers:

  • le 1er module aura comme intrant un nom de fichier, et comme extrant un tableau d'enregistrements;
  • le 2ième module aura comme intrant un tableau d'enregistrements, et comme extrant un tableau (trié) d'enregistrements;
  • le 3ième module aura comme intrants un nom de fichier et un tableau d'enregistrements, et n'aura aucun extrant (dans la mesure où le fichier sera correctement rempli du contenu du tableau).
  • 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:

  • (0) une simple chaîne de caractères (de type «string» ou «char*») suivant par exemple le format "Nom,Prénom", ou
  • (1) une structure contenant deux chaînes de caractères; ou encore
  • (2) des instances d'une classe permettant de représenter ces enregistrements...
  • ...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.

    Étape 3

    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.

    Étape 4

    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.

    Développement par boîte noire et réutilisation du code

    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.

    Développement par boîte noire et documentation

    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:

  • le nom et le rôle de chaque module;
  • le nom, le nombre et le type des intrants;
  • le nom, le nombre et le type des extrants;
  • un schéma décrivant l'interconnexion des différents modules de par leurs interfaces respectives.
  • 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.

    Développement par boîte noire et tests

    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

  • Technique du développement «en boîte noire»
  • - Qu'est-ce que cette technique?
  • -- Pourquoi ne l'enseigne-t-on pas plus tôt?
  • -- Quels en sont les avantages?
  • -- Des exemples...
  • --- Tri d'un fichier
  • ---- Étape 0: le problème
  • ---- Étape 1: une solution possible
  • ---- Étape 2
  • ---- Étape 3
  • ---- Étape 4
  • -- Développement par boîte noire et réutilisation du code
  • -- Développement par boîte noire et documentation
  • -- Développement par boîte noire et tests

  • [1] ...elle-même tout droit inspirée de bonnes vieilles techniques mathématiques que vous avez tous déjà survolées au secondaire...