Chapitre 8: Raffinement pas à pas


Dans une large mesure, la programmation est la science de la résolution de problèmes par ordinateur. Parce que les problèmes sont souvent difficiles, les solutions - et les programmes qui les implémentent - peuvent l'être également. Afin de faciliter le développement de ces solutions, vous devez adopter une méthodologie et une discipline qui réduisent le niveau de cette complexité à une échelle raisonnable.

Dans les premières années de la programmation, le concept de l'informatique en tant que science était plus ou moins une expérience de vœux pieux. Personne ne connaissait grand-chose à la programmation à cette époque, et peu la considéraient comme une discipline d'ingénierie au sens conventionnel du terme. Cependant, au fur et à mesure que la programmation mûrit, une telle discipline a commencé à émerger. La pierre angulaire de cette discipline est la compréhension que la programmation se fait dans un environnement social dans lequel les programmeurs doivent travailler ensemble. Si vous vous lancez dans l'industrie, vous serez presque certainement l'un des nombreux programmeurs travaillant à développer un grand programme. De plus, il est presque certain que ce programme main à vivre et nécessitera une main au-delà de son application initialement prévue. Quelqu'un voudra que le programme inclue une nouvelle fonctionnalité ou fonctionne d'une manière différente. Lorsque cela se produit, une nouvelle équipe de programmeurs doit entrer et apporter les modifications nécessaires aux programmes. Si les programmes sont écrits dans un style individuel avec peu ou pas de points communs, il est extrêmement difficile d'amener tout le monde à travailler ensemble de manière productive.

Pour lutter contre ce problème, les programmeurs ont commencé à développer un ensemble de méthodologies de programmation appelées collectivement génie logiciel . L'utilisation de bonnes compétences en génie logiciel facilite la lecture et la compréhension de vos programmes par les autres programmeurs, mais facilite également leur écriture. L’un des progrès méthodologiques les plus importants de l’ingénierie logicielle est la stratégie de conception descendante ou raffinement pas à pas , qui consiste à résoudre des problèmes en commençant par le problème dans son ensemble. Vous décomposez tout le problème en morceaux, puis résolvez chaque élément, en les décomposant davantage si nécessaire. Cette stratégie descendante est complétée par test itératif avant de continuer. Assurez-vous que les plus petits éléments de la solution fonctionnent.

Un exercice de raffinement par étapes

Pour illustrer le concept de raffinement par étapes, apprenons à Karel à résoudre un nouveau problème. Imaginez que Karel vit maintenant dans un monde qui ressemble à ceci:

Sur chacune des colonnes, il y a une tour de jetons d'une hauteur inconnue, bien que certaines colonnes (telles que la 7e et la 9e dans le monde de l'échantillon) puissent être vides. Le travail de Karel est de rassembler tous les jetons dans chacune de ces tours, de les remettre sur le coin le plus à l'est de la 1ère rangée, puis de revenir à sa position de départ. Ainsi, lorsque Karel termine son travail dans l'exemple ci-dessus, tous les 25 jetons actuellement dans les tours doivent être empilés au coin de la 9ème colonne et de la 1ère rangée, comme suit:

Surtout, vous pouvez supposer que Karel initialementdépartsavec zéro jetons dans son sac. Chaque jeton ramassé est ajouté à son sac. En mettant jetons dans le coin, Karel peut utiliser le jetons_en_sac() tester. On peut également supposer que les colonnes n'atteignent pas tout le chemin jusqu'au mur le plus au nord.

La clé pour résoudre ce problème est de décomposer le programme de la bonne manière, tout en étant capable de tester au fur et à mesure. Cette tâche est plus complexe que les autres que vous avez vues, ce qui rend le choix des sous-problèmes appropriés plus important pour obtenir une solution réussie.

Le principe de la conception descendante

L'idée clé dans le raffinement par étapes est que vous devez commencer la conception de votre programme par le haut, ce qui fait référence au niveau du programme qui est conceptuellement le plus élevé et le plus abstrait. À ce niveau, le problème de la tour jeton est clairement divisé en trois phases indépendantes. Tout d'abord, Karel doit collecter tous les jetons . Deuxièmement, Karel doit les déposer à la dernière intersection. Troisièmement, Karel doit revenir à sa position d'origine. Cette décomposition conceptuelle du problème suggère que le main() La fonction de ce programme aura la structure suivante:

   def main():
      collectionner_tous_jetons()
      tout_laisser_tomber_jetons()
      rentrer_à_la_maison()

À ce niveau, le problème est facile à comprendre. Bien sûr, il reste quelques détails sous forme de fonctions que vous n'avez pas encore écrites. Néanmoins, il est important de regarder chaque niveau de la décomposition et de se convaincre que, tant que vous pensez que les fonctions que vous êtes sur le point d'écrire résoudront correctement les sous-problèmes, vous aurez alors une solution au problème dans son ensemble. .

Tests itératifs au fur et à mesure

Maintenant que vous avez défini la structure du programme dans son ensemble, il est temps de avancer à avancer au premier sous-problème, qui consiste à collecter tous les jetons . Cette tâche est elle-même plus compliquée que les simples problèmes des chapitres précédents. Collecter tous les jetons signifie que vous devez récupérer les jetons dans chaque tour jusqu'à ce que vous jetons au dernier virage. Le fait que vous deviez répéter une opération pour chaque tour suggère que vous avez besoin d'un while boucle ici. le while boucle répétera le processus de recueillir_une_tour() puis en mouvement.

Mise en garde: Il est dangereux d’essayer d’écrire l’ensemble du programme sans essai comme vous allez. Si vous faites une erreur, il sera difficile de trouver l'erreur. Nous savons que nous allons répéter le processus de collecte d’une tour. Laissez-nous écrire et tester la collecte d'une seule tour avant de mettre le recueillir_une_tour() processus dans un boucle for . Donctemporairementnous pouvons commencer par la définition suivante de collectionner_tous_jetons() :

   def collectionner_tous_jetons() :
      # implémentation temporaire à des fins de test
      recueillir_une_tour()
      avancer()

En principe, si vous avez une boucle complexe, testez lacorps de la boucle avant d'écrire la boucle entière.

Raffinage de la tour de collecte

Quand recueillir_une_tour() est appelé, Karel est soit debout à la base d'une tour de jetons soit debout sur un coin vide. Dans le premier cas, vous devez récupérer le jetons dans la tour. Dans ce dernier, vous pouvez simplement avancer sur. Cette situation ressemble à une application pour le if déclaration, dans laquelle vous écririez quelque chose comme ceci:

   if jetons_présent():
      recueillir_la_tour_réelle()

Avant d'ajouter une telle instruction au code, vous devez vous demander si vous devez effectuer ce test. Souvent, les programmes peuvent être beaucoup plus simples en observant que les cas qui semblent à première vue spéciaux peuvent être traités exactement de la même manière que la situation plus générale. Dans le problème actuel, que se passe-t-il si vous décidez qu'il y a une tour de jetons sur chaque avenue mais que certaines de ces tours ont une jetons zéro jetons ? L'utilisation de ces informations simplifie le programme car vous n'avez plus à tester s'il y a une tour sur une avenue particulière.

le recueillir_une_tour() La fonction est encore suffisamment complexe pour qu'un niveau supplémentaire de décomposition s'impose. Pour rassembler tous les jeton dans une tour, Karel doit entreprendre les étapes suivantes:

  1. Tournez à gauche pour faire face au jetons dans la tour.
  2. Récupérez tous les jetons dans la tour, en vous arrêtant lorsque plus de jetons sont plus trouvés.
  3. Retournez-vous pour faire face au fond du monde.
  4. Retournez au mur qui représente le sol.
  5. Tourner à gauche pour être prêt à avancer au coin suivant.

Encore une fois, ce schéma fournit un modèle pour la recueillir_une_tour() fonction, qui ressemble à ceci:

   defrecueillir_une_tour():
      tourner_gauche()
      collecter_la_ligne_de_jetons()
      tourner_autour()
      avancer_au_mur()
      tourner_gauche()

Conditions préalables et postconditions fonctionnelles

le tourner_gauche() commandes au début et à la fin de recueillir_une_tour() sont tous deux essentiels à l'exactitude de ce programme. Quand recueillir_une_tour() est appelé, Karel est toujours quelque part sur la 1ère rangée face à l'est. Lorsqu'il aura terminé son opération, le programme dans son ensemble ne fonctionnera correctement que si Karel est à nouveau face à l'est dans ce même coin. Les conditions qui doivent être vraies avant qu'une fonction ne soit appelée sont appelées conditions préalables ; les conditions qui doivent s'appliquer après la fin de la fonction sont appelées postconditions .

Lorsque vous définissez une fonction, vous aurez beaucoup moins de problèmes si vous notez exactement quelles sont les pré et postconditions. Une fois que vous avez fait cela, vous devez vous assurer que le code que vous écrivez laisse toujours les postconditions satisfaites, en supposant que les conditions préalables étaient satisfaites au départ. Par exemple, pensez à ce qui se passe si vous appelez recueillir_une_tour() quand Karel est au 1er rang face à l'est. La première tourner_gauche() fonction laisse Karel face au nord, ce qui signifie que Karel est correctement aligné avec la colonne de jetons représentant la tour. le collecter_la_ligne_de_jetons() fonction - qui n'a pas encore été écrite mais qui exécute néanmoins une tâche que vous comprenez conceptuellement - simplement avancer s sans tourner. Ainsi, à la fin de l'appel à collecter_la_ligne_de_jetons() , Karel sera toujours face au nord. le tourner_autour() l'appel laisse donc Karel face au sud. Comme collecter_la_ligne_de_jetons() , les avancer_au_mur() La fonction n'implique aucun virage, mais simplement avancer s jusqu'à ce qu'elle touche le mur de avancer . Parce que Karel est face au sud, ce mur de délimitation sera celui en bas de l'écran, juste en dessous de la 1ère rangée. Le final tourner_gauche() La commande laisse donc Karel au 1er rang face à l'est, ce qui satisfait la post-condition.

Répéter le processus

Vous run votre programme et il efface avec succès une tour et laisse Karel dans la postcondition promise. Wahoo! Vous venez de franchir une étape importante dans la résolution de cette tâche difficile! Nous devons maintenant répéter le processus de nettoyage d'une tour à l'aide d'un while boucle.

Mais qu'est-ce que cela while boucle ressemble à? Tout d'abord, vous devriez penser au test conditionnel. Vous voulez que Karel s'arrête quand il heurte le mur au bout de la rangée. Ainsi, vous voulez que Karel continue aussi longtemps que l'espace devant est dégagé. Ainsi, vous savez que le collectionner_tous_jetons() la fonction comprendra un while boucle qui utilise le l'avant_est_clair() tester. À chaque position, vous voulez que Karel récupère tous les jetons dans la tour en commençant à ce coin. Si vous donnez à cette opération un nom, qui pourrait être quelque chose comme recueillir_une_tour() , vous pouvez continuer et rédiger une définition pour le collectionner_tous_jetons() fonction même si vous n’avez pas encore rempli les détails.

Vous devez cependant être prudent. Le code pour collectionner_tous_jetons() ne ressemble pas à ceci:

   defcollectionner_tous_jetons():
      # boucle buggy!
      while l'avant_est_clair():
         recueillir_une_tour()
         avancer()

Cette implémentation est boguée pour exactement la même raison que la première version du général JetonLigne programme du chapitre 6 n'a pas fait son travail. Il y a une erreur de clôture dans cette version du code, car Karel doit tester la présence d'une tour jeton sur la dernière avenue. La mise en œuvre correcte est:

   defcollectionner_tous_jetons():
      while front_is_clear():
         recueillir_une_tour()
         avancer()
      recueillir_une_tour()

Notez que cette fonction a exactement la même structure que le programme main programme Place Jeton Line présenté au chapitre 6. La seule différence est que ce programme appelle recueillir_une_tour() où l'autre a appelé laisser_jeton() . Ces deux programmes sont chacun des exemples d'une stratégie générale qui ressemble à ceci:

   defcollectionner_tous_jetons():
      while l'avant_est_clair():
          effectuer une opération.
         avancer()
       effectuer la même opération pour le dernier virage.

Vous pouvez utiliser cette stratégie chaque fois que vous devez effectuer une opération sur chaque coin lorsque vous avancer long d'un chemin qui se termine à un mur. Si vous vous souvenez de la structure générale de cette stratégie, vous pouvez l'utiliser chaque fois que vous rencontrez un problème nécessitant une telle opération. Les stratégies réutilisables de ce type sont fréquemment rencontrées dans la programmation et sont appelées idiomes de programmation ou les patrons . Plus vous connaissez de modèles, plus il vous sera facile de trouver celui qui convient à un type de problème particulier.

Finir

Bien que le travail acharné ait été fait, il reste encore plusieurs problèmes à résoudre. Le programme main appelle deux fonctions: tout_laisser_tomber_jetons() et rentrer_à_la_maison() - qui ne sont pas encore écrites. De même, recueillir_une_tour() appels collecter_la_ligne_de_jetons() et avancer_au_mur() . Heureusement, ces quatre fonctions sont assez simples pour coder sans aucune décomposition supplémentaire, en particulier si vous utilisez avancer_au_mur() dans la définition de rentrer_à_la_maison() . Voici l'implémentation complète:


Chapitre suivant