Sites


L'art de la programmation

La modélisation

La modélisation est le processus au coeur de la programmation et du
développement logiciel.

En effet, quelque soit le sujet du programme ou du logiciel, ce qui découlera
du développement sera un modèle de celui-ci.

Qu'il s'agisse d'une application informatique telle qu'un éditeur de texte, un
agent de courrier électronique, un logiciel de retouche d'image, une
application web, des outils de gestion, des outils d'analyse mathématique,
scientifique ou financière, qu'il s'agisse de pilotes informatiques de bas
niveau pour le clavier, l'affichage graphique, ou pour des protocoles
d'échanges réseau, qu'il s'agisse de jeux ou de logiciels industriels, ce qui
est programmé décrit un comportement qui est le reflet du modèle qui a été
défini.

C'est ce modèle qui définit ce que le programme fait: ce qu'il produit comme
sorties à partir de telle ou telle entrée. C'est ce qui fait que pour une
action donnée, l'utilisateur s'attend à un résultat obtenu.

Ceci est valable pour les programmes conçus dans une logique déterministe, ce
qui constitue la majorité des cas.

Note: une logique déterministe peut renvoyer un résultat non déterministe,
exemple: renvoyer l'heure courante. La logique est déterministe puisque pour
une action donnée (requête de l'heure), la même fonction est appelée: renvoyer
l'heure courante, c'est simplement la valeur renvoyée qui est fluctuante.

Dans certaines situations par contre, des programmes sont bâtis sur des
automates non déterministes. Ce cas de figure dépasse le sujet de ce guide. A
titre d'informations, voici quelques références du web à ce propos:
 - Les automates:
   http://pauillac.inria.fr/~maranget/X/421/poly/automate.html#sec127
 - Illustration des automates par un exemple:
   http://benhur.teluq.ca/SPIP/inf6104/spip.php?article91

Le point de départ d'un développement informatique est donc la modélisation.

Il existe plusieurs degrés de modélisation. Ils sont décrits dans la section
suivante.

Différents niveaux de modélisation

La modélisation intervient à différents niveaux du logiciel.

En principe, le premier degré est l'analyse de l'ensemble logiciel.

Ensuite, vient l'architecture, qui décrit comment le projet s'articule en
composantes logicielles et matérielles. Elle comporte également la description
des interfaces entre tous les composants.

L'élaboration du projet se fait dans les étapes de conception détaillée
(description des éléments de code) et d'implémentation (écriture du code).

Les tests mettent à l'épreuve le modèle conçu par rapport à un modèle de
référence, implémenté en tant que module de test.

Ce guide commence par aborder le niveau de l'implémentation dans sa forme
d'expression la plus simple. Parce que c'est plus aisé de comprendre les
techniques de modélisation sur des petites réalisation, de bas niveau, avant de
monter vers des projets de plus grande envergure. Il est à noter que le rendu
obtenu ne sera pas forcément le plus simple.

Bien entendu, les technologies choisies, telles que le matériel, le système
d'exploitation, les applicatifs en jeu et, évidemment, le langage de
programmation choisi auront une influence sur la manière de réaliser la
modélisation.

Le corps de programme

Le corps de programme est le niveau d'implémentation le plus direct: c'est
celui du programme qui tient entièrement dans le bloc principal, ou du script
unique.

Il est adapté à toute réalisation simple bien cernée, à tout script utilitaire,
à tout programme qui tient dans un algorithme unique (voir la section
"Algorithmes").

Il correspond au cas de figure où l'on saisit une feuille de papier et un
crayon, que l'on gribouille le schéma général et qu'on se met à le coder
directement. L'étape "papier" est parfois même éludée par le programmeur
expérimenté.

Enfin, dans des situations où les performances sont critiques, et où
chaque appel de fonction pourrait compter, un programme dans son corps unique
sera la solution adaptée pour remplir une tâche simple qui doit être effectuée
un très grand nombre de fois.

C'était le cas autrefois, lorsque les machines étaient beaucoup moins
puissantes et la complexité des programmes nettement moindre, ou aujourd'hui
dans des systèmes où la complexité et les ressources sont limitées expressément
(applications embarquées pour une seule fonction minimaliste).

Ce cas de figure est donné à titre indicatif, en effet, autjourd'hui, la
tendance est d'écrire avant tout de façon lisible et compréhensible. Cet aspect
sera abordé plus loin dans ce guide.

C'est pour son champ d'application que le "corps de programme" est étudié en
premier, c'est l'étape de modélisation qui permet de traduire une
fonctionnalité simple en une réalisation simple, qui marche.

Elle permet de voir concrètement à quoi correspond la modélisation, puisque le
code de programme écrit correspond directement à un modèle de la fonctionnalité.

Dans le cas d'un problème mathématique, la correspondance se fait facilement,
puisque la solution mathématique est déjà un modèle. L'informatique étant bâtie
sur des lois mathématiques, le modèle propre au programme sera très proche du
modèle de la solution mathématique.

Certains langages se prêtent plus à certaines expressions mathématiques que
d'autres. Il y en a même quelques-uns spécifiques aux mathématiques, conçus
pour des logiciels de mathématiques à part entière, comme R, ou scilab (en
logiciels libres), qui permettent de manipuler directement des grands jeux de
données, des matrices et même des fonctions mathématiques. Un parcours des
différentes familles de langages de programmation est abordé dans la section
"Le choix du langage" pour donner un aperçu des grandes spécificités.

Pour l'instant, nous allons nous concentrer sur un cas particulier de la
programmation impérative, celui de la programmation dite "séquentielle"
(https://fr.wikipedia.org/wiki/Programmation_s%C3%A9quentielle).

Il s'agit de tout programme exécutant une suite d'instructions dans un ordre
donné (des conditions peuvent donner lieu à des chemins différents, mais la
séquence d'instruction sera toujours la même).

Voici un exemple de programme en Python très court qui tient directement dans le
corps de programme (source: https://groups.google.com/d/msg/comp.lang.python/xLY96-kdUcc/ylIXBtmPlzIJ):

#! /usr/bin/python
# -*- coding: utf-8-unix; -*-
#
# Code sample: program as a single body
#
# Functionality: act as the dos2unix utility, it converts every DOS end of line
#                in a Unix style end-of-line in the file given as argument.
#
#############################################################################

import sys

filename = sys.argv[1]
text = open(filename, 'rb').read().replace('\r\n', '\n')
open(filename, 'wb').write(text)

Il s'agit bien de programmation séquentielle car le programme est constitué
d'une suite d'instructions les unes après les autres, sans même de branches
conditionnelles !

Ce qu'il fait ?

Il ouvre en lecture le fichier donné en argument, lit tout son contenu en
remplaçant la chaîne de caractère '\r\n' par '\n' pour le stocker dans une
variable "text". Il écrit ensuite tout le contenu de la variable dans le même
fichier, c'est-à-dire qu'il remplace son contenu par le contenu transformé.

Tout ça en quelques instructions Python !

En fait, il fonctionne exactement comme l'utilitaire dos2unix (voir le "guide
sur les systèmes Unix", c'est-à-dire qu'il remplace toutes les fins de ligne qui
suivent la convention DOS (un retour de chariot, "CR" = "Carriage Return",
suivi d'un saut à la ligne, "LF" = "Line Feed). Ces caractères spéciaux (voir
"CR" et "LF" dans http://asciitable.com) sont souvent représentés comme "\r"
(return) et "\n" (newline) dans les chaînes de caractère des langages de
programmation, les shells et les utilitaires du système.

A noter toutefois que la signification de "\n" peut produire "LF" ou "CR LF"
comme résultat selon l'environnements (ex: en C, il produira "LF" sous Unix et
"CR LF" sous MS-DOS/Windows).

A savoir...

La plupart des programmes et outils d'aujourd'hui sont capables de
lire les deux conventions. Cependant, certains éditeurs vous mettrons en
évidence la différence si la convention n'est pas celle du système.

Ainsi, si vous faites passer un programme Python ou une page HTML d'un système
Unix à Windows, ou inversement, l'éditeur de texte pourra vous afficher les
caractères "CR" en plus sous Unix. Par contre, le langage Python comme le
navigateur web étant conçus pour traiter les deux conventions, le programme
Python s'exercera et la page HTML s'affichera correctement.

Analyse

 L'analyse est l'étape préalable qui conduit au choix d'une bonne modélisation.

L'analyse vise à répondre au moins à ces questions:
 - que doit faire mon application ?
 - comment la structurer ?

Une bonne approche pour nous aider à mener cette analyse, est de réaliser une
découpe en "fonctionnalités" et "entrées/sorties".

Les grandes fonctionnalités vont ressortir en répondant à la question "Que doit
faire mon application ?".

Une fois qu'elles sont connues, il faudra établir les interactions entre ces
fonctionnalités et les entrées/sorties.

Cette  revient à poser les questions:
 - Qui fait quoi ? Pour identifier les principaux éléments en jeu.
 - Comment ? Pour décrire la logique, les algorithmes.
 - Avec qui ? Pour déterminer de quoi les éléments ont besoin et quels autres
   éléments peuvent le leur fournir.

C'est là que la structure va commencer à se dégager.

L'analyse papier est un bon support pour concrétiser cette étape et se rendre
compte visuellement des éléments identifiés et de leurs interactions.
Personnellement, je trouve que ça permet de se poser et de laisser venir à soi
les éléments qui entrent en scène.

Si vous préférez travailler directement sur l'ordinateur, je cite quelques
outils dans la section suivante sur "la modélisation et l'architecture du
projet".

En tous cas, je favorise une approche visuelle et schématique qui identifie les
éléments qui interagissent, leurs entrées/sorties et les fonctionnalités en
jeu, je ne suis pas fan du pseudo-code. Un minimum en sera utilisé pour faire
ressortir les logiques essentielles. Je m'insipire ainsi de la modélisation
objet, sans chercher à être fidèle à son formalisme.

Un autre ingrédient qui est important à prendre en compte, c'est que l'analyse
se fait dans la mesure du possible, de façon indépendante des technologies
utilisées.

Néanmoins, dans certains cas, les technologies sont imposées, elles font alors
partie des entrées.

Et dans d'autres, il y a certaines technologies qui se prêtent particulièrement
bien au contexte. En principe, l'analyse doit conduire à ce choix. Cependant,
une fois la technologie connue, elle peut induire une solution, auquel cas, ce
serait un peu une perte de temps de faire l'analyse par deux fois. C'est le bon
sens qui doit parler.

L'analyse est un processus important, alors qu'il y a des bouquins entiers sur
ce sujet, je le décris en peu de mots car pour moi, ce qui est fondamental,
c'est de développer son intuition, parvenir à sentir ce qui colle
bien. Evidemment, cette intuition est renforcée par l'expérience et le
savoir-faire. Ce savoir-faire vous le puiserez dans les rencontres, les
développements en équipe, ou dans ces livres complets. En tous cas, je suis
persuadé qu'en prenant la peine de la laisser s'exprimer cette intuition au
plus tôt, elle s'invitera progressivement à vous plus efficacement que vous ne
le pensez. Elle enrichira le savoir-faire que vous pourrez acquérir.

J'estime que l'analyse est un processus de maturation, vous allez commencer à
faire une ébauche, des choses vous plairont, d'autres seront floues, et puis ça
va gripper à certains endroits. Peut-être qu'il va falloir revoir un point ou
deux, peut-être qu'il va falloir voir les choses complètement différemment.
Parfois, quand quelque chose coince, c'est qu'il faut aborder la question sous
un angle différent.

C'est pour ça que j'aime bien le papier, je peux facilement reprendre un
nouveau schéma, tout en gardant un oeil sur les schémas précédents. Je n'ai pas
besoin de chercher à faire quelque chose de propre, avec des jolies couleurs,
et les polices qui vont bien. Je cherche juste à plaquer un modèle. Je ne
cherche pas à ce qu'il soit fini dès le départ.

Souvent, une idée me vient quelques jours plus tard, quand j'ai laissé
décanter. Simplement, parce que j'aurai lu un article, que je serai tombé sur
un bout de code quelque part, ou parce que j'aurai discuté avec quelqu'un en échangeant toutes
sortes d'idées, qu'il m'aura présenté sa vision des choses.

Bref, comme vous le voyez, l'analyse est un processus.

Alors, pour conclure, les éléments essentiels sont pour moi "s'imprégner du
projet" et "répondre aux questions clés évoquées ci-dessus". Le reste suivra avec
l'expérience de chacun, au fil des intérêts qu'il portera. C'est vous qui
guidez la conduite de développement que vous souhaitez voir grandir.

Modélisation et architecture du projet

Nous avons vu deux premiers cas de modélisation:
 - Réalisation d'un script avec corps unique
 - Découpe en fonctions

Pour aller plus loin, nous devons pousser l'étude de la modélisation:
 - Déterminer les technologies et composants logiciels dont nous aurons besoin:
   cela s'appelle l'architecture et demande une vision d'ensemble du projet à
   réaliser et une bonne compréhension des technologies.
   
 - Utiliser la modélisation dans une approche de développement direct
   en s'appuyant sur ces critères:
   - Séparer interface (affichage et entrée) des fonctions (rendu et fonctionnalités)
   - Modéliser l'application en termes techniques sur la base de l'analyse:
     faire des choix d'implémentation, identifier les principaux objets
     et fonctions, commencer à construire la structure (interface vide)
   - Prévoir les cas d'erreurs et leur traitement
     La bonne pratique est de ne jamais laisser un programme "se vautrer".
     Néanmoins, c'est une problématique complexe et spécifique à chaque
     langage ou framework selon les facilités qu'ils mettent à disposition.
     Nous n'allons pas le développer ici. Il s'agit de retenir que c'est
     un critère important !

 - Structurer son travail en termes d'organisation sur le disque (répertoires
   et fichiers):
   - de la modélisation obtenue en composantes logicielles, se dessine une
     répartition en sous-projets et/ou modules,
   - à partir de cette répartition, nous pouvons élaborer une arborescence pour
     notre projet ; ce sera une étape nécessaire au développement et au suivi
     de version

Représentation visuelle et notions de "programmation orientée objet"

Pour vous aider à visualiser la modélisation, vous pouvez utiliser un support
graphique. Un bon moyen de représentation est la notation UML.

Le langage UML ("Unified Modeling Languages") a été créé pour répondre au
besoin d'exprimer des modèles de façon consistante.

La modélisation UML est un champ d'étude très vaste, un sujet à part
entière. Un de ses champs d'application est la modélisation "orientée objet"
qui est celle qui nous intéresse ici. Elle répond au besoin de conception de la
"programmation orientée objet". Nous en verrons quelques concepts de base ici.

Pour la partie analyse vue précédemment, ce qui convient le mieux pour nos
besoins du moment est le diagramme de classes, dont une explication synthétique
est reprise ici:
 - http://uml.free.fr/cours/i-p14.html
 - https://sourcemaking.com/uml/modeling-it-systems/structural-view/class-diagram

Par rapport aux exemples donnés, sachez qu'outre les attributs (variables
faisant partie d'une classe), s'y réfèrent aussi volontier les méthodes
(fonctions faisant partie d'une classe).

Toute la conception derrière la programmation dite "orientée objet" est que les
variables de notre programme peuvent prendre un type plus complexe que les
types ordinaires (entiers, nombres flottants, chaînes de caractère ou
tableaux). Ces types sont personnalisés et se rapportent à plusieurs variables
et fonctions qui leur sont propres.

Ce nouveau type "personnalisé" s'appelle une "classe".  Ses variables internes
sont appelés "attributs" de la classe et les fonctions propres à celle-ci sont
appelées "méthodes" de la classe.

Lorsqu'une variable de ce nouveau type personnalisé est créé, il se dit que
nous créons une "instance" de la "classe". Le terme "objet" désigne de façon
interchangeable "classe" ou "instance". La "classe" est en quelque sorte la
définition de l'"objet", et une "instance" désignera un "objet" particularisé.

Les liens représentés horizontalement sont des "relations", ce sont elles qui
représentent quels objets sont en interaction entre eux, et à quel titre (le
nom de la relation exprime la nature de la communication). Les interactions ont
lieu au travers des "méthodes" et des "attributs".

Les méthodes et attributs peuvent être "publics" ou internes (on dit "privés")
à la classe.

Dans certains langages, comme Java, la pratique est de ne jamais laisser un
autre objet consulter ou modifier un attribut. L'objet qui interagit avec
l'objet cible doit dans ce cas nécessairement passer par une méthode de l'objet
en question pour y accéder. On parle "d'encapsulation" parce que les attributs
sont cachés de l'extérieur de la classe, seule celle-là a un droit de regard
dessus.

Ce qui signifie que beaucoup d'attributs simples, viennent également avec deux
méthodes, l'une d'accès (getter) et l'autre de modification
(setter). L'idée est de pouvoir éventuellement protéger les accès en
procédant à d'éventuels contrôles.

En Python, par contre, la pratique est de ne jamais prévoir ces deux méthodes
pour des opérations aussi élémentaires, qui reviennent à utiliser l'opérateur
d'affectation et la méthode de consultation d'une donnée. Si des opérations (de
contrôle par exemple) doivent être faites lors de ces deux actions, la classe
viendra surcharger les fonctions par défaut "__getattr__()" et "__setattr__()".

Les méthodes sont alors utilisées pour des manipulations plus complexes.

En C++, les deux approches se rencontrent.

La terminologie "surcharger" désigne le fait de remplacer le fonctionnement
d'une méthode "héritée" d'un objet parent par un autre comportement. Par souci
d'intégrité, la nouvelle fonction fait souvent appel à celle du parent, en
complétant son traitement.

La notion "d'héritage" est une des particularités de la "programmation orientée
objet".

Dans un diagramme UML, les "acteurs" servent à désigner les éléments extérieurs
qui sont en interaction avec le système modélisé. Un utilisateur sera un
acteur, mais un autre système dont nous ne nous intéressons pas au modèle peut
aussi être représenté par un acteur.

Par exemple, si je modélise une voiture sans rentrer dans les détails, mon
acteur sera le conducteur.  Il pourra appuyer sur la pédale d'accélérateur qui
a pour effet indirect d'augmenter l'accélération. Mon modèle pourra alors avoir
une méthode "enfoncer_accelerateur()" avec un paramètre "course" qui ira de 0 à
100% (ou 0 à 1) pour indiquer la course de la pédale d'accélération (jusqu'à
quel point, il a accéléré).

Si je modélise par contre uniquement le moteur, l'acteur sera la pompe à
carburant. Mon moteur pourra proposer une méthode "ajuster_debit()" que la
pompe viendra solliciter (dans notre modélisation, elle s'appelera en réalité
"actualiser()").

Autrement, ce pourrait être la pompe qui soit consultée. Elle pourra offrir un
attribut "debit" qui sera lu par l'objet "moteur" quand il en aura besoin (ou
il appellerait "lire_debit()" si l'approche Java est utilisée). Puisqu'un
acteur n'est pas modélisé, cette analyse sert juste à exprimer l'interface
entre le moteur et l'acteur.

Dans le contexte d'un système industriel, les acteurs peuvent donc se comporter
comme des actuateurs ou des capteurs, pour utiliser la terminologie de cet
univers-là.

En termes de modélisation, il s'agira par contre d'un choix de conception.
L'élément pourra en effet fournir à la fois l'attribut "debit" et déclencher la
méthode "ajuster_debit()" de l'objet avec lequel il interagit.

Enfin, dans les questions qui surgissaient sur le niveau de modélisation, à
savoir si c'est "Voiture" ou "Moteur", il est possible de modéliser à la fois
la voiture et le moteur.

Par exemple, dans la modélisation de la voiture seule, nous aurions:
  - Un acteur: "Conducteur"
  - Un objet "Voiture":
    - Méthodes:
      - actualiser_course_accelerateur(course) ; appelle calculer_vitesse()
      - actualiser_course_freins(course) ; appelle calculer_vitesse()
      - calculer_vitesse() ; calcule la vitesse en fonction de la course
        de l'accélérateur et des freins
    - Attributs:
      - course_accelerateur
      - course_freins
      - vitesse

Et dans la modélisation complète, nous aurions:
 - Un acteur: "Conducteur"
 - Un objet "Voiture": objet "Gestionnaire"
   - Méthodes:
     - actualiser() ; héritée de "Gestionnaire", surchargée pour appeler
       "calculer_vitesse()"
     - calculer_vitesse() ; calcule la vitesse à partir du nombre de tours/min
       du moteur
   - Attributs:
     - nom ; nom initialisé par le constructeur
     - moteur ; objet "Moteur"
     - freins ; objet "Freins"
     - reservoir_carburant ; objet "Reservoir"
     - reservoir_huile_freins ; objet "Reservoir"
     - pompe_carburant ; objet "Pompe"
     - pompe_freins ; objet "Pompe"
     - pedale_accelerateur ; objet "Pedale"
     - pedale_freins ; objet "Pedale"
     - vitesse
 - Un objet "Gestionnaire": objet générique
   - Méthodes:
     - actualiser()
 - Un objet "Pedale":
   - Méthodes:
     - actualiser_course(course) ; a un effet de pression sur "levier" et
       notifie "gestionnaire" de la mise-à-jour
   - Attributs:
     - nom ; nom initialisé par le constructeur
     - gestionnaire ; sera connecté à "voiture"
     - levier ; sera connecté à pompe_freins ou pompe_carburant
     - course ; mesure la course de la pédale
 - Un objet "Pompe":
   - Méthodes:
     - actualiser_pression(niveau) ; calcule la pression en fonction du niveau
       d'ouverture de la valve et des caractéristiques du réservoir en amont
   - Attributs:
     - nom ; nom initialisé par le constructeur
     - p_min ; pression minimum
     - p_max ; presion maximum
     - debit_max ; débit maximum
     - amont ; équipement connecté en amont
     - aval ; équipement connecté en aval, pour notifier un changement
       de débit et/ou de pression
     - debit ; débit actuel
     - pression ; pression actuelle
 - Un objet "Reservoir":
   - Méthodes:
     - ajuster_debit_pression(pompe) ; ajuste le débit en fonction du débit
       tiré par la pompe
   - Attributs:
     - volume ; mesure du volume actuel
     - debit_max ; débit maximum délivré par le réservoir
 - Un objet "Moteur":
   - Méthodes:
     - connecter_entree_carburant() ; connecte l'équipement qui fournit le
       carburant en entrée du moteur
     - ajuster_debit_pression() ; ajuste le débit en fonction du débit fourni
       en entrée
     - calculer_tours() ; calcule la vitesse du moteur en fonction du débit de
       carburant
   - Attributs:
     - entree_carburant ; équipement qui fournit le carburant
     - nb_tours ; nombre de tours/minutes du moteur
 - Un objet "Freins":
   - Méthodes:
     - ajuster_debit_pression() ; ajuste la pressio en fonction de la pression
       fournie en entrée
     - calculer_force_freinage() ; calcule la force de freinage en fonction de
       la pression dans le liquide de freins
   - Attributs:
     - force_freinage


En termes d'instances, il y aura pour les pompes et les réservoirs:
 - pompe_carburant
 - reservoir_carburant
 - pompe_freins
 - reservoir_freins

La représentation orienté objet permet donc d'obtenir un niveau de modélisation
très poussé et très en image de la réalité. C'est toute sa force.

Par contre, il y a lieu de bien évaluer le niveau de représentation. En effet,
dans l'exemple ci-dessus, il n'y aura d'intérêt à décomposer en objets le
système "Voiture" que si les objets individuels ajoutent quelque
chose. Autrement, si les équations du système global suffisent, le modèle, le
développement et les tests en restent quand-même bien plus simples !

C'est exactement cela le travail de modélisation, trouver le niveau de
représentativité qui convient à la situation.

Vous allez voir qu'une représentation visuelle est un véritable plus lorsqu'il
s'agit de décrire une modélisation.

Pour mettre la mettre en forme , nous avons plusieurs outils à disposition,
selon votre préférence:
 - Umbrello (https://umbrello.kde.org/): outil de modélisation UML de KDE, il
   s'agit d'un véritable outil qui prend en compte les spécificités d'UML pour
   les aplliquer au développement
 - Dia (http://dia-installer.de/): outil de tracé de diagrammes qui dispose de
   boîtes de diagrammes spécifiques pour UML (gère uniquement l'aspect visuel)
 - Inkscape (https://inkscape.org): outil de dessin vectoriel, toute la partie
   UML est à faire à la main (il existe des tutoriels, mais pas de /plugins/
   tout prêt)
 - Draw (https://fr.libreoffice.org/discover/draw/): outil de dessin vectoriel
   des suites LibreOffice/OpenOffice.org, qui permet de tracer des boîtes,
   toute la partie UML est à faire à la main

Les logiciels qui intègrent les diagrammes UML comme Umbrello et dia
offrent des boîtes respectant la notation UML. Ces boîtes ont des propriétés
éditables qui reprennent les différentes entrées possibles, telle que nom de
classe, attributs et méthodes.

C'est donc une aide substantielle pour la réalisation d'un diagramme.  Ils
invitent donc à se conformer plus à la norme, ce qui permet aussi de s'appuyer
sur un langage commun. Par ocontre si vous souhaitez plus de liberté
d'expression, vous êtes un peu plus restreint.

A ce titre, Umbrello est un outil de conception informatique, axé sur la
notation UML, alors que dia est avant tout un éditeur de diagrammes qui
intègre la notation UML parmi sa panoplie.

Le premier se prête bien pour se conformer au développement, et même d'établir
un lien avec le code.  Le second est plus léger en termes de modélisation,
puisqu'il s'attache exclusivement à l'aspect visuel. En revanche, il offrira
des possibilités graphiques plus variées. Note: la dimension des boîtes
s'aujstent selon les champs renseignés, il faut donc jouer sur le niveau de
zoom pour avoir un rendu visuellement satisfaisant.

A côté de cela, des outils comme Inkscape et Draw offrent toutes les
facilités de créativité, vous pouvez donc exprimer la représentation qui vous
convienne. L'essentiel est qu'elle soit parlante, compréhensive et que vous
soyez efficace dans la réalisation.

Ci-joint le diagramme de notre système "Voiture-Pompes-Moteur-Freins" modélisé
avec Umbrello et dia. Avec pour comparaison le modèle "Voiture" seul. Dans
le diagramme détaillé, seule la classe voiture a été explicitée à fin
d'illustrations.

La section qui suit donnera la représentation informatique correspondant à ces
diagrammes.

L'objet

L'objet

Diagramme de classe pour le modèle

Diagramme de classe pour le modèle

Représentation informatique et "programmation orientée objet"

Nous allons voir comment écrire en termes informatiques les exemples de
modélisations vus dans la section précédente. Le langage choisi sera Python.

Commençons par le modèle "Voiture" seul:


#! /usr/bin/python
# -*- coding: utf-8-unix; -*-

class Voiture(object):
    VITESSE_MAX=120
    
    def __init__(self, nom):
        self.nom = nom
        self.course_accelerateur = 0.0
        self.course_freins = 0.0
        self.vitesse = 0.0
        return
 
    def actualiser_course_accelerateur(self, course):
        self.course_accelerateur = course
        self.calculer_vitesse()
 
    def actualiser_course_freins(self, course):
        self.course_freins = course
        self.calculer_vitesse()
 
    def calculer_vitesse(self):
        # calcule la vitesse en fonction de la course
        # de l'accélérateur et des freins
        self.vitesse = self.course_accelerateur * self.VITESSE_MAX
        # Formule à élaborer pour le freinage...
        return
 
def main():
    # Création de l'instance "ma_voiture" de l'objet "Voiture"
    ma_voiture = Voiture("Ma Voiture")
    # A la création de l'objet, la voiture est à l'arrêt
    # L'affichage de la vitesse doit donner "0":
    print ma_voiture.vitesse
    # L'acteur conducteur (moi), décide d'appuyer sur l'accélérateur à
    # 15% pour atteindre une petite vitesse de départ (note: une
    # accélération constante est utilisée, l'accélération sur l'appui
    # de la pédale n'est pas prise en compte)
    ma_voiture.actualiser_course_accelerateur(0.15)
    # Affichage de la vitesse après démarrage de la voiture
    print ma_voiture.vitesse
 
if __name__ == "__main__":
    main()

Un brin d'explications...

La ligne "class Voiture(object)" définit l'élément "Voiture" comme étant
une classe qui dérive de la classe générique "object", cela revient à dire
que c'est un objet Python.

Les fonctions définies au sein de la classe (c'est-à-dire, celles indentées par
rapport à celles-ci) sont ses méthodes.

Le première méthode __init__(self, nom) est ce qu'on appelle un constructeur,
c'est la méthode qui est appelée à la création de l'objet, ce qui arrive dans
la fonction main() du programme principal, à la ligne "ma_voiture =
Voiture("Ma Voiture")". A ce moment, je crée une instance de la classe
"Voiture()", que j'appelle "Ma Voiture" et que je choisis de référencer par
la variable "ma_voiture".

J'appelle ensuite les méthodes de la classe relativement à l'instance que je
viens de créer. C'est le mot-clé "self" qui désigne l'instance à laquelle la
fonction se rapporte.

La variable VITESSE_MAX est dite variable de classe car elle est définie au
niveau de la classe et non pas de l'instance. C'est-à-dire que toutes les
instances partageront la même valeur (puisqu'elle n'est pas pas initialisée à
partir de self.VITESSE_MAX. Le fait de l'écrire en majuscules est une
convention qui stipule qu'il s'agit d'une constante, c'est donc une constante
de classe plutôt qu'une variable de classe. Il est à noter que cette nuance est
purement indicative, car hormis la rigueur de programmation, rien n'interdit de
modifier cette valeur.

L'appel à la fonction ~main()~ est encadré par un artifice "if __name__ ==
"__main__"". Cela permet d'exécuter le corps principal (ce qui suit le "if",
donc l'appel à la fonction main ()) uniquement lorsque le script est appelé
en tant que programme et non en tant que module. Dans le premier cas, la
variable ~__name__~ prend la valeur "__main__", tandis que dans le second
cas, la variable prendra pour valeur le nom du module importé (c'est-à-dire le
nom du fichier sans l'extension ".py", à moins qu'il n'en soit précisé
autrement).

Et maintenant, au tour du modèle complet !


#! /usr/bin/python
# -*- coding: utf-8-unix; -*-

class Gestionnaire(object):
    def __init__(self):
        return    

    def actualiser(self):
        return

class Voiture(Gestionnaire):
    # Constante pour évaluer la vitesse en km/h à partir
    # de la vitesse en tours/min
    VITESSE_KMH_PAR_TOURS_MIN = 120.0/3000.0

    def __init__(self, nom):
        super(Voiture, self).__init__()
        self.nom = nom
        self.moteur = Moteur()
        self.freins = Freins()
        self.reservoir_carburant = Reservoir("Reservoir Carburant", 40, 1.8)
        self.reservoir_huile_freins = Reservoir("Reservoir Huile de Freins",
                                                1.0, 0.3)
        self.pompe_carburant = Pompe("Pompe carburant", 0.0, 3.0, 3.0,
                                     self.reservoir_carburant, self.moteur)
        self.moteur.connecter_entree_carburant(self.pompe_carburant)
        self.pompe_freins = Pompe("Pompe freins", 0.0, 1.0, 0.1,
                                  self.reservoir_huile_freins, self.freins)
        self.pedale_accelerateur = Pedale("Accélérateur",
                                          self, self.pompe_carburant)
        self.pedale_freins = Pedale("Freins",
                                    self, self.pompe_freins)
        self.vitesse = 0.0
        return

    # Surcharge de la méthode "actualiser" héritée de "Gestionnaire"
    def actualiser(self):
        super(Voiture, self).actualiser()
        self.calculer_vitesse()
        return

    def calculer_vitesse(self):
        # calcule la vitesse en fonction de la vitesse du moteur
        # et de la force de freinage
        self.vitesse = self.moteur.nb_tours * self.VITESSE_KMH_PAR_TOURS_MIN
        # Formule à élaborer pour le freinage...
        return

class Pedale(object):
    def __init__(self, nom, gestionnaire, levier):
        self.nom = nom
        self.gestionnaire = gestionnaire
        self.levier = levier
        self.course = 0.0
        return

    def actualiser_course(self, course):
        self.course = course
        self.levier.actualiser_pression(self.course)
        self.gestionnaire.actualiser()
        return

class Pompe(object):
    def __init__(self, nom, p_min, p_max, debit_max, amont, aval):
       self.nom = nom
       self.p_min = p_min
       self.p_max = p_max
       self.debit_max = debit_max
       self.amont = amont
       self.aval = aval
       self.pression = 0.0
       self.debit = 0.0
       return

    def actualiser_pression(self, niveau):
       # Calculer le débit et la pression en fonction du niveau
       # d'ouverture de la valve, de p_min et p_max et des
       # caractéristiques du réservoir en amont
       self.pression = self.p_min + niveau * (self.p_max - self.p_min)
       self.debit = (self.pression / self.p_max) * self.debit_max
       self.debit = min(self.debit, self.amont.debit_max)
       self.pression = (self.debit / self.debit_max) * self.p_max
       self.amont.ajuster_debit_pression(self)
       # Faire parvenir l'information à l'équipement en aval
       self.aval.ajuster_debit_pression(self)
       return

class Reservoir(object):
    def __init__(self, nom, volume_init, debit_max):
        self.nom = nom
        self.volume = volume_init
        self.debit_max = debit_max
        return

    def ajuster_debit_pression(self, pompe):
        self.debit = min(self.debit_max, pompe.debit)
        return

    
class Moteur(object):
    NB_TOURS_DEBIT = 3500 / 2.8
    def __init__(self):
        self.entree_carburant = None
        self.nb_tours = 0
        return

    def connecter_entree_carburant(self, entree_carburant):
        self.entree_carburant = entree_carburant
        return

    def ajuster_debit_pression(self, pompe):
        if pompe == self.entree_carburant:
            self.debit_carburant = pompe.debit
        self.calculer_tours()
        return

    def calculer_tours(self):
        # Calculer le nombre de tours/min en fonction du débit du carburant
        if (self.entree_carburant == None):
            self.nb_tours = 0
        else:
            self.nb_tours = self.debit_carburant * self.NB_TOURS_DEBIT
        return        

class Freins(object):
    def __init__(self):
        self.pompe = None
        self.force_freinage = 0
        return

    def ajuster_debit_pression(self, pompe):
        self.pression_freins = pompe.pression
        self.calculer_force_freinage()
        return

    def calculer_force_freinage(self):
        # Calculer la force de freinage en fonction de la pression
        # dans le liquide de freins
        return


def main():
    # Création de l'instance "ma_voiture" de l'objet "Voiture"
    ma_voiture = Voiture("Ma Voiture")
    # A la création de l'objet, la voiture est à l'arrêt
    # L'affichage de la vitesse doit donner "0":
    print ma_voiture.vitesse
    # L'acteur conducteur (moi), décide d'appuyer sur l'accélérateur à
    # 15% pour atteindre une petite vitesse de départ (note: une
    # accélération constante est utilisée, l'accélération sur l'appui
    # de la pédale n'est pas prise en compte)
    ma_voiture.pedale_accelerateur.actualiser_course(0.15)
    # Affichage de la vitesse après démarrage de la voiture
    print ma_voiture.vitesse
 
if __name__ == "__main__":
    main()


En termes de programmation, ce sont exactement les mêmes notions que dans
l'exemple précédent. Elles sont simplement utilisées plus abondamment.

Il y a tout de même quelques particularités supplémentaires.

L'appel au mot-clé super(Voiture, self) sert à appeler la méthode la classe
parente de Voiture(), à savoir Gestionnaire tel que défini dans "class
Voiture(Gestionnaire)".

Enfin, les objets sont associés entre eux en sauvant une référence vers l'un et
l'autre au sein de leurs instances comme dans "self.pompe_freins =
Pompe("Pompe freins", self.freins)".

Pour parvenir à boucler les références circulaires, il faut enregistrer les
variables en deux temps, puisqu'en effet, il faut que l'objet soit créé avant
de pouvoir être référencé à son tour:
"self.moteur.connecter_entree_carburant(self.pompe_carburant)"

Une fois que tous les objets du modèle ont été créés, l'accès au modèle se fait
presque comme sur une véritable voiture. Désormais, pour accélérer, l'acteur
"Conducteur" interagit directement avec la pédale d'accélérateur de la voiture
en tant que l'instance "pedale_accelerateur" de l'objet "Pedale" faisant
partie de l'instance ma_voiture". En termes informatique, l'action
d'accélérer s'écrira donc
"ma_voiture.pedale_accelerateur.actualiser_course(0.15)".

Nous voyons au travers de cette exemple simple que la programmation orientée
objet donne un caractère très vivant à l'informatique.

C'est un avantage, cependant, cela peut parfois être trompeur. En effet,
construire un modèle adéquat demande parfois du recul et une gymnastique
d'esprit particulière, qui n'est pas forcément ce qui nous saute aux yeux en
première analyse, même si la solution à l'arrivée se retrouve plus intuitive
d'usage.

C'est donc un savoir-faire à développer !

Algorithmes

D'après Wikipedia, un algorithme est une suite finie et non ambiguë
d’opérations ou d'instructions permettant de résoudre un problème ou d'obtenir
un résultat donné.

L'algorithme correspond donc à la modélisation, souvent mathématique, d'une
tâche donnée. Il décrit une manière de répondre à la réalisation d'une
spécificté du logiciel en établissant une recette pour obtenir le résultat
souhaité.

La qualité d'un algorithme se mesure à sa fiabilité (sa capacité à obtenir le
bon résultat), il est alors dit "correct" pour sa capacité à produire la bonne
sortie. Elle se mesure aussi à son efficacité (durée de calcul, quantité de ressources
utilisées, précision et comportement au facteur d'échelle).

Lors de l'implémentation d'une solution, il sera souhaitable de trouver
l'algorithme le plus adapaté à la recherche de celle-ci. Bon nombre de
situations courantes ont fait l'objet de recherches et des solutions
algorithmiques peuvent être trouvées pour celles-ci. Il existe souvent même des
librairies qui implémentent un ou plusieurs algorithmes pour des cas de figures
usuels. La librairie offre alors un choix de solutions selon les conditions
d'application qui pourraient privilégier l'un ou l'autre, ou le goût du
programmeur.

L'algorithmique est à la fois une branche spécialisée et à la fois une
compétence que le programmeur sollicite en permanence. En évoluant, le
développeur intègre les recettes élémentaires qui façonnent sa démarche de
programmation.

Au fur-et-à-mesure de sa progression, il apprendra de nouvelles recettes pour
de nouvelles situations, il aiguisera son sens de l'algorithmique. S'il
approdondit ce domaine, il pourra se spécialiser dans cette démarche. Dans tous
les cas, c'est une matière où il est bon de s'appuyer sur les épaules de ceux
qui se sont investis dans la résolution de problèmes spécifiques. C'est la
communauté informatique qui évolue dans son ensemble grâce aux apports des uns
et des autres.

Le choix du langage

La plupart des langages font partie de la programmation de type impérative
(https://fr.wikipedia.org/wiki/Programmation_impérative): langage machine ou
assembleur, Fortran, Algol, Cobol et Basic, Pascal, C. L'évolution vers
l'orienté objet a engendré Ada, Smalltalk, C++. Et la famille des langages
interprétés comprend notamment Perl, Tcl, Python, PHP et Java.

Tous ces langages ont la particularité d'être très proche des
instructions du processeur et des opérations arithmétiques (qui correspondent
d'ailleurs pour bonne part aux instructions du composant "UAL" du processeur,
pour "Unité Arithmétique et Logique", s'écrit "ALU" en anglais).

L'apparition des listes et des fonctions sur celles-ci apportent une petite
partie du concept de la programmation fonctionnelle.

En effet, dans la programmation fonctionnelle
(https://fr.wikipedia.org/wiki/Programmation_fonctionnelle), il n'y a plus
d'assignation de variables. Cette programmation se fait comme un appel en
cascade de fonctions appelées par la précédente. Les langages faisant partie de
cette famille comprennent Lisp et ses dérivés (Scheme, Common Lisp, Emacs
Lisp), ML, Haskell, OCaml et Erlang pour les plus populaires.

Cette approche de programmation se traduit souvent par des listes extensibles
imbriquées comportant des éléments ou d'autres listes. Les éléments d'une liste
peuvent désigner une fonction en premier élément et ses paramètres en
suivant. Chaque paramètre peut à son tour être une liste, dont certains
implique à leur tour l'évaluation d'une fonction.

Prenons un exemple mathématique simple: "(+ 4 7 100 (- 10 2 3))".

Le premier élément de l'expression est la fonction "+", qui agit sur les
éléments "4", "7", "100" et "(- 10 2 3)".

Ce dernier est lui-même une expression à part entière qui s'évalue en
soustrayant (opérateur "-") les éléments "2" et "1" à l'élément "10".
Sa valeurs est donc "10 - 2 - 3" = "5".

Le quatrième élément de la somme est alors "5", et notre somme est donc "4 +
7 + 100 + 5" = "116".

Ce que nous avons là est la modélisation d'une calculatrice en notation
polonaise inverse ("NPI" ou "RPN" en anglais). Comme quoi, chaque langage est
adapté à une modélisation particulière.

Bien entendu, n'importe quelle fonction peut trouver sa place en lieu et place
d'un opérateur arithmétique. Par exemple, pour utiliser une fonction "sort"
plutôt que l'opérateur "+", l'expression remaniée "(sort (list 4 7 100 (- 10 2 3)) '<)"
donne comme résultat "(4 5 7 100)" (fonctionne en "Emacs Lisp").

Voilà pourquoi les listes permettent d'apporter un air de programmation
fonctionnelle dans les langages impératifs. Il suffit de permettre que certains
éléments de la liste soient une fonction. Bien entendu, ils restent des
langages impératifs puisqu'ils ne sont pas conçus dans la logique
fonctionnelle.

A côté de ça, il reste la programmation logique
(https://fr.wikipedia.org/wiki/Programmation_logique), qui est focalisée sur
les assertions logiques et les prédicats. C'est vraiment une approche
particulière, une large part des enchaînements étant pris en charge par le
moteur. Ce type de programmation est très utilisé en intelligence
artificielle. Le premier langage de programmation logique est Prolog.

Enfin, il y a des logiciels spécialisés dans certains domaines, comme
R et Scilab pour les mathématiques. Ces outils disposent d'un langage
de programmation spécifique à leur domaine d'applications. En
l'occurrence, il s'agit de programmation impértative, avec une syntaxe
et des fonctions de travail orientée sur les mathématiques. Ces
langages travaillent avec des objets mathématiques complexes.  Ils
manipulent, par exemple, des matrices et des fonctions comme éléments,
avec une aisance qui n'existent pas dans les autres langages.

En conclusion, chaque domaine d'application qui a fait l'objet d'un
champ d'études, s'est vu développer un langage de programmation
propre. Il convient donc, lors du développement, de porter son choix
sur le langage le plus adapté, qu'il soit le plus spécifique pour le
domaine d'application, car il répond vraiment au domaine cible, ou
plus généraliste, parce qu'il apportera d'autres avangages, comme le
fait d'être connu, ou de présenter une flexibiilté plus grande.

Il y a une erreur de communication avec le serveur Booktype. Nous ne savons pas actuellement où est le problème.

Vous devriez rafraîchir la page.