Livres

Design Patterns (E. Gamma et al.)

Lecture vivement recommandée pour ceux qui ne savent pas encore ce que sont les design patterns (modèles de conception, en français).

Réutilisabilité

Ce terme apparait très souvent, mais je l'ai longtemps mal compris. Il a deux sens. D'une part, il faut essayer de programmer de la manière la plus générale possible, afin de pouvoir réutiliser des morceaux de code (si possible, des fonctions ou des objets, que l'on aura réunis dans une bibliothèque) dans d'autres projets. Il est bon de tendre vers une telle généricité, mais c'est un peu utopique.

D'autre part, il faut essayer de programmer de manière à pouvoir réutiliser le code dans le _même_ projet, après qu'il ait subi d'inévitables remaniements.

C'est ce deuxième sens qui donne toute sa pertinence aux modèles de conceptions, qui consistent simplement à séparer les choses qui changent des choses qui ne changent pas -- c'est expliqué très clairement dans les livres de B. Eckels.

http://www.mindview.net/Books/

MVC (Modèle/Vue/Contrôleur)

C'est une manière de modéliser les interfaces utilisateurs. Le modèle est l'ensemble des données sur lesquelles l'utilisateur peut agir. La vue est la manière dont des données sont représentées (les vues peuvent être multiples : par exemple, un tableau de nombres, un braphique avec des barres, un camembert, etc.) ; c'est la partie en contact direct avec l'utilisateur ; elle reflète les changements du modèle. Le contrôleur est ce qui modifie le modèle.

Héritage

C'est aussi un modèle de conception, qui n'est pas détaillé, car il est présent dans tous les langages de programmation orientés objet. Il modélise la relation « est un ».

L'héritage est parfois utile, mais il a tendance à compliquer les choses quand on s'apperçoit qu'il faut changer la structure du programme. Il ne faut l'utiliser qu'à bon escient.

Délégation

C'est l'implémentation de l'héritage à l'aide de la composition. Il y a deux objets, le récepteur (classe fille), qui reçoit le message et le transmet au délégué ; et le délégué (classe parent), qui le traite. Contrairement à l'héritague, le délégué n'a pas accès aux champs privés du récepteur (contrairement à la classe parent, qui a accès à l'objet this).

C'est par exemple ce que l'on est obligé d'utiliser en Java lorsque l'on veut de l'héritage multiple (par exemple, avec les Threads). Dans d'autres langages, c'est un choix dont on dispose, pas une contrainte.

Composition

C'est la relation « a un ».

Exemple d'application : la délégation (voir plus haut).

Interfaces (Introduction aux modèles de construction)

Il est préférable de manipuler des interfaces plutôt que des types concrets (comme ça, on peut changer le type concret sans rien changer au programme) ; en particulier, le type des variables doit (souvent) être un type abstrait. on instancie alors les variables à l'aide de modèles de création (singleton, fabrique, monteur).

Stratégie

C'est juste un pointeur sur une fonction. De manière plus précise, on définit une interface, AbstractStrategy, qui possède une seule méthode, do_it(). Implémenter cette interface signifie simplement définir une fonction de ce type. [Donc : stratégie abstraite = type d'un pointeur sur une fonction, stratégie concrète = pointeur sur une fonction.] on se retrouve généralement avec une stratégie abstraite et plusieures stratégies concrètes et le programme choisit à l'exécution (éventuellement avec l'aide de l'utilisateur) laquelle appeler.

C'est comme ça que Java s'est débarassé des pointeurs (en particulier, pour la programmation évènementielle, dans les interfaces graphiques).

Exemple : l'état d'un éditeur de texte (vi) : quand on appuie sur une touche, il appelle la fonction correspondante, qui change selon l'état ; cette fonction est stockée dans un "pointeur". De même : l'état d'un logiciel de dession.

Exemple : la fonction de sauvegarde d'un fichier : les différents choix de format de sauvegarde (par exemple *.jpg, *.png, *.xcf, sous Gimp) correspondent à différents pointeurs sur des fonctions.

Exemple : la possibilité de lire des fichiers compressés sous des formats divers.

Exemple : transformer des données en différents types de graphiques (à barres, camembert, etc.)

Exemple : les logiciels d'affichage d'image proposent différents algorithmes pour changer la taille d'une image.

Exemple : un logiciel de streaming (ce qui permet à une radio de diffuser ses programmes sur le net) peut proposer différents formats (selon la vitesse de la connection, selon l'attachement du client aux formats de données ouverts (ogg/wma), etc.).

Commande

Note : je n'ai pas compris la différence entre Commande et Stratégie.

C'est un pointeur sur deux fonctions. Exactement comme avec la stratégie, on définit une interface AbstractCommand, qui possède deux méthodes do_it() et undo_it(). Implémenter cette interface signifie simplement définir deux telles fonctions.

C'est utilisé dans les interfaces graphiques, pour implémenter l'opération « annuler l'action précédente ».

Fabrique abstraite

J'ai dit plus haut qu'il était souvent utile que le type des objets soit un type abstrait. La fabrique abstraite permet de définir ces objets.

Une fabrique abstraite est une interface, contenant des méthodes du genre createA, createB, qui renvoient des objets de type A ou B (ici, A et B sont des types abstraits). On implémente cette fabrique abstraite en définissant des fonctions qui créent des objets dont le type implémente A ou B. Ainsi, si on décide de changer le type concret utilisé pour l'interface A ou B, il suffit de changer la fabrique concrète : il n'y a qu'une seule ligne à modifier.

Exemple : Pouvoir manipuler de la même manière des tableaux, des tables de hachage, des B-arbres, etc.

Monteur

Le Monteur remplit les mêmes services que la fabrique abstraite, mais cette fois-ci on n'a même pas à manipumer explicitement les objets créés. Le monteur implémentera des fonctions addA(), addB() qui ne renvoient pas l'objet créé : il est simplement ajouté au monteur.

Exemple : Un Canvas, i.e., un Widget dans lequel on peut dessiner. On peut disposer de fonction addLine, addPoint, addCircle, etc.

Fabrication

C'est une variante de la fabrique. On a une classe semi-abstraite, avec une méthode abstraite createA() et une méthode concrète do_something() qui utilise createA(). On dérive ensuite cette classe quand on veut implémenter createA().

Exemple : ???

Singleton

Une classe qui n'a qu'une seule instance.

Exemple : une connection à une base de données quand on fait de la programmation Web.

Souvent, l'objet n'est instancié que lors de sa première utilisation (si on n'en a pas besoin, il n'est jamais instancié).

On peut imaginer une variante avec un nombre limité d'instances.

Exemple : file d'attente d'une imprimante.

Exemple : les classes statiques (Math, en Java).

Exemple : les variables globales.

Prototype

Un objet avec une méthode clone() permettant de le dupliquer.

Exemple : Le fonction dclone du module Storable (Perl), la méthode clone() de la classe Object (en Java, pour une copie peu profonde), l'interface Serializable (en Java, pour une copie plus profonde).

Composite

C'est le fait de voir un groupe d'objets comme un seul objet.

Par exemple, dans un logiciel de dessin, on peut « grouper » des éléments pour les manipuler ensemble. Dans un programme qui manipule des arbres (par exemple, un document XML), on peut manipuler un sous-arbre comme si c'était un noeud.

Itérateur

Un objet qui permet de parcourrir les éléments d'une liste, d'un tableau, d'un arbre, etc. (comme en C++).

Patron de methode (method template)

Comme en C++.

Exemple: Une class AbstractSort, avec une méthode sort() (implémentée) et une méthode compare (non omplémentée), et des classes SortIntegers, SortStrings qui en dériveraient.

Décoration

Presque comme un composite, mais il ne contient qu'un seul objet, auquel il se contente de rajouter des fonctionnalités. C'est un peu comme une classe dérivée (implémentée sans héritage).

L'un des intéret, par rapport à l'héritage, c'est qu'on peut ajouter ou retirer ces fonctionnalités à l'exécution.

Exemple : l'ajout d'ascensseurs dans un Widget.

[note : si on veut pouvoir ajouter et retirer plusieures fonctionnalités différentes, dans n'importe quel ordre, c'est peut-être un peu plus compliqué à programmer]

Exemple : en Java, BufferedInputStream est un décorateur autour d'un flux d'entrée (avec un flux d'entrée normal, on peut lire les caractères un par un, avec un BufferedInputStream, on peut lire une ligne à la fois : ce décorateur ajoute une méthode readLine).

Adaptateur

C'est un cast. Il s'agit de changer l'interface d'un objet en gardant sa fonctionnalité.

Prélude à l'exemple : Une bibliothèque (library, en anglais) est un ensemble de fonctions (ou d'objets), réutilisable. Un Framework, c'est pareil, mais en plus on a la structure du programme que l'on peut écrire avec (par exemple, un framework pour écrire des traitements de textes, ou pour écrire des logiciels de dessins, qui donnerait toutes les briques que l'on peut souhaiter, un plan de montage, mais laisserait quand même aux clients le droit de remplacer ou d'ajouter certaines fonctionalités) [dans le monde du logiciel libre, on appellerait ça un logiciel libre].

Voici enfin l'exemple : Si on dispose de deux boites noires, une bibliothèque et un Framework, que l'on veut utiliser ensemble, il arrive que l'interface proposée par la bibliothèque soit incompatible avec celle du Framework : il faut alors on dispose de deux boites noires que l'on veut relier ; l'une est une bibliothèque dont les objets ont une certaine interface, l'autre est un Framework, qui attend une interface complètement différente. On utilisera un adapateur pour les relier.

Pour programmer ça, on peut construire un objet Adaptator qui implémenterait à la fois l'interface du Framework et celle de la bibliothèque.

Exemple : le module Tangram, en Perl, qui s'intercale entre un programme et une base de donnée et transforme cette base de donnée en base de données oriéntée objet.

Exemple : le module DBI, en Perl, qui permet d'accéder de la même manière à différentes bases de données.

Exemple : c'est ce que l'on utilise quand on cherche à faire utiliser Swing à un vieux programme programme en Java 1 qui utilisait AWT.

Exemple : En Java, il y a plein de classes avec Adapter dans le nom, par exemple WindowAdapter (une classe abstraite qui sert à recevoir les divers évènements que l'on peut envoyer à un Widget).

Pont

C'est presque un adaptateur, à ceci près que l'adaptateur transforme l'interface d'une classe pour qu'elle corresponde à celle d'une classe qui existe déjà, alors que le pont se contente de séparer l'interface de l'implémentation, afin de pouvoir ultérieurement changer l'une sans toucher à l'autre.

Facade

Quand on a plein de classes différentes qu'il faut utiliser ensemble, on peut les mettre dans une boite noire. L'entrée de cette boite noire s'appelle une façade.

A FAIRE : DESSIN

Exemples : un compilateur, un système d'exploitation, l'ordonnanceur d'un système d'exploitation, une fabrique abstraite ; de manière générale, n'importe quel objet faisant des choses compliquées avec une interface simple.

Exemple : le module HTTP::Simple, en Perl, est une facade qui permet d'accéder simplement aux modules HTTP::Request::Common et LWP::UserAgent (qui eux-mêmes sont des façades de choses beaucoup plus complexes).

Proxy

Un Proxy (procuration, en français), est un objet qui s'intercale entre un objet utilisateur et un objet utilisé, de manière à différer certaines opérations.

Exemple : quand un traitement de texte rencontre une image, il ne la charge pas tout de suite (c'est trop long), il ne le fait que quand il en a vraiment besoin. Mais comme le traitement de texte parle au Proxy en croyant parler à l'image, il n'en sait rien.

Pour l'implémentation, on peut imaginer une interface Image (contenant les méthodes que le traitement de texte utilisera), implémentée dans des classes RealImage et ProxyImage. L'objet ProxyImage contient (une référence sur) un objet de type RealImage.

Autre exemple : le singleton << connection à une base de données >> : la connection n'est effectuée que lors de la première utilisation.

De manière générale, ce Design Pattern concerne les objets distants, lourd ou dont l'accès est réglementé.

Poids Mouche

Pour des raisons d'efficacité, on ne peut pas faire de tout un objet. Il serait par exemple intéressant, si on programme un traitement de texte, de faire des caractères du texte des objets. Mais pour un texte de quelques milliers de pages, ce n'est pas réaliste. On va donc remplacer les les caractères par des pointeurs vers des objets Caractères, qui seront en nombre réduit (le nombre de caractères, fois le nombre de styles différents).

Pour l'implémentation, on peut avoir une fabrique de poids mouches.

Autre exemple : c'est ainsi que sont implémentées les chaines de caractères en Perl (ou les chaines de caractères constantes définies à la compilation en Java).

Exemple : les icones "Fichier" ou "Répertoire" dans un gestionnaire de fichiers.

Visiteur (suite)

C'est un pointeur sur une fonction (comme la stratégie) qui est appelé lorsqu'on parcourt un objet à l'aide d'un itérateur.

On dispose de plusieures classes (Class1, Class2, Class3), qui implémentent des opérations semblables, mais différentes (operationA, operationB).

                AbstractClass
                operationA()
                operationB()

               ^    ^       ^
              /     |        \
             /      |         \
            /       |          \
           /        |           \

Class1            Class2            Class3
operationA()      operationA()      operationA()
operationB()      operationB()      operationB()

On peut changer la structure du programme comme suit.

AbstractClass
operation(Visitor v)

  ^
  |
  |
  |

Class1                   ...
operation(Visitor v)



Visitor
operationClass1()
operationClass2()
operationClass3()

    ^          ^
    |           \
    |            \
    |             \
    |              \

OperationA          OperationB
operationClass1()   operationClass1()
operationClass2()   operationClass2()
operationClass3()   operationClass3()

On a alors des classes plus simples et les éléments du programme sont mieux séparés.

Exemple : effectuer une opération sur tous les noeuds d'un arbre (alors que ces noeuds sont de type différent).

Par exemple, l'ajout des points de césure dans un logiciel de traitement de texte.

Un visiteur permet d'ajouter une fonctionnalité à une classe sans la modifier. On peut voir cela comme de l'externalisation.

Un visiteur permet d'appeler une même méthode sur des objets de différentes classes.

Chaine de responsabilités

Un message parcourt une chaine d'objets, chacun le passant à l'objet suivant s'il est incapable de le traiter.

Exemple : Les Handlers d'autorisation/identification avec mod_perl, qui sont appelés les uns après les autres, jusqu'à ce que l'un d'eux prenne une décision.

Exemple : Le système d'aide d'un cliquodrome. Si le Widget sur lequel on clique n'a pas d'aide, il la demande à son conteneur, qui fait de même, et ainsi de suite. De manière plus générale, la manière dont un cliquodrome réagit quand on appuie sur une touche ou quand on clique : le Widget sur lequel on est regarde s'il comprend, sinon, il passe le message à son conteneur, et ainsi de suite.

Remarque : cette chaine n'est pas nécessairement linéaire : elle peut avoir la forme d'un arbre -- l'héritage en est un exemple : quand une instance d'une classe reçoit un message (c'est une autre manière de dire << quand on appelle une méthode sur une instance d'une classe >>, message = méthode), si elle ne sait pas le traiter, elle l'envoie aux classes dont elle hérite.

Interpréteur

Une classe abstraite pour manipuler des expressions régulières (l'expression régulière, stockée sous forme d'arbre, constitue un objet héritant de l'expression régulière abstraite ; pour être utilisée, on va la transformer en automate fini).

Exemple : m (Oui, c'est un exemple qui fait une seule lettre : c'est la commande Perl qui permet de vérifier si une expression régulière s'applique à une chaine).

Exemple : la commande eval, dans beaucoup de langages.

Exemple : toute bibliothèque permettant de lire un fichier de configuration -- dans des cas extrèmes, ces fichiers de configuration peuvent être écrit dans un vrai langage de programmation (par exemple : Guile).

Exemple : Jython (un interpréteur Python en Java), que l'on peut utiliser pour proposer au client de configurer ou même carrément étendre l'application qu'on lui propose, que l'on peut utiliser pour écrire des tests de régression plus rapidement, que l'on peut utiliser pour débugguer un programme Java.

Médiateur

Quand on a plein d'objets, avec plein d'interconnections, le programme devient un peu fouilli (car chaque classe est obligée de connaitre (un peu) chaque autre classe avec laquelle elle est en relation). On peut réunir toutes ces relations dans un seul objet central. Ainsi, à part cet objet centrel, les objets n'on plus qu'un seul autre objet à connaître.

A FAIRE : DESSIN 

(5 points interconnectés dans tous les sens, 
puis on rajoute un point au
milieu, qui éclaircit le dessin)

Exemple : un cliquodrome complexe.

Observateur

Quand l'objet observé change, l'observateur le signale à plusieurs autre objets. [c'est donc une variante orientée du médiateur.]

Exemple : La commande tie, en Perl.

Exemple : les cases d'un tableur.

Exemple : La structure MVC (Modèle/Vue/Contrôleur) -- quand le modèle change, il faut mettre les vue à jour.

Mémento

Objet qui permet de stocker l'état d'un objet pour le récupérer plus tard. Typiquement :

my $memento = $self->write_memento();
...
$self->read_memento($memento);

Exemple : la fonction "undo", sous Gimp (ou un autre logiciel de dessin).

Exemple : la commande ROLLBACK dans une base de données, qui permet d'annuller toutes les opérations de la transaction courrante.

Exemple : setjmp en C [Si vous ne connaissez pas, ne regardez pas : c'est comme un goto, mais en pire -- avec un goto, on n'a pas le droit de sauter directement à l'intérieur d'une boucle, ni à l'intérieur d'une fonction -- la commande setjmp lève ces restrictions.]

Exemple : les coroutines. Voir Advanced Programming Language Design, Finkel, Chapter 2

http://cs.engr.uky.edu/~raphael/
http://www.awprofessional.com/catalog/product.asp?product_id={92E30B39-5D91-45F9-9919-D202BE6341F9}&selectDescTypeId={A80972E0-1077-4518-954C-44E43E341DF7}

Exemple : la commande save-excursion, en Emacs Lisp, qui sauvegarde l'état courrant (par exemple, la position du curseur) pour pouvoir le restaurer après avoir effectué certaines opérations.

État

Quand l'état d'un objet change, son comportement change, un peu comme s'il avait changé de classe.

Implémentation :

Object       <>------------      AbstractState
do_it()                          do_it()

                                  ^         ^
                                  |         |
                                  |         |

                               State1     State2
                               do_it()    do_it()

Exemple : la fonction bless, en Perl ? (pour ceux qui ne connaissent pas : perldoc perltoot).

Exemple : l'état d'une connection TCP.

Exemple : logiciels de dessin (un clic de souris correspond à une action différente selon l'état courrant : dessiner au pinceau, dessiner à l'aérographe, remplir avec un dégradé, sélectionner, etc.).

Exemple : vi est un éditeur de texte à états (on peut être en mode insertion ou pas).

Unit Testing

Les tests de régression (i.e., le fait d'écrire des tests pour vérifier qu'une classe (ou une partie du programme, relativement indépendante du reste) se comporte comme documenté, en particulier pour vérifier qu'on ne casse rien quand on en réécrit certaines parties) ne sont pas mentionnés dans le livre, mais on peut les voir comme un << modèle de développement >>.

The Design Patterns Java Companion (J.W. Cooper, AW, 1998)

Le livre est disponible sur Internet.

http://www.patterndepot.com/put/8/JavaPatterns.htm

Ce livre est bourré d'exemples de modèles de conception, en Java (je crois qu'il y a plus d'exemples que dans le livre de E. Gamma et al.). J'ai repris certains de ces exemples plus haut.

Le livre contient aussi une introduction à Swing (le paquetage Java pour créer des interfaces graphiques).

The Smalltalk Companion (Alpert)

Un autre livre sur les Degign Patterns (je ne l'ai pas lu).

The Joy of Patterns

Encore un autre livre sur les Design Patterns (je ne l'ai pas lu).

Modélisation objet avec UML (P.A. Muller)

Quand on fait de la programmation objet, on est amené à faire des petits dessins pour représenter les liens (héritage ou autre) entre nos différentes classes. Le problème, c'est que nous n'adoptons pas tous les mêmes conventions : par exemple, pour l'héritage, certains font une flèche dans un sens, d'autres dans l'autre sens. Si on veut échanger ou comparer ses petits dessins avec ses collègues, c'est génant. C'est là qu'intervient UML : c'est une standardisation de ces petits dessins (et pas uniquement pour les relations entre classes : il y a quelques autres types de petits dessins).

La première partie du livre est une référence d'UML : il ne faut pas l'aborder si on ne connait pas déjà UML (si par exemple on a déjà lu le livre Design Patterns, si on en comprend les diagrammes, si on est capable d'en faire soi-même, ça doit être bon).

Rappelons brièvement quels sont ces diagrammes.

Les diagrammes de classes, représentant les classes, leurs champs et méthodes, leurs relations d'héritage et d'aggrégation.

Les diagrames d'objets : les relations entre les instances des classes (c'est très différent des diagrammes de classes).

Les diagrammes de séquence : ils indiquent ce qui se passe quand le temps s'écoule.

Les diagrammes de collaboration : les différents objets et les messages qu'ils s'échangent. C'est un mélange entre diagramme d'objets et diagramme de séquence.

Les diagrammes d'état-transition : ce sont des automates finis.

Cas d'utilisation : ce sont des ensembles de scénarios. un scénario décrit ce qui se passe du point de vue de l'utilisateur (on ne mentionne pas ce qui lui est caché -- d'ailleurs, ça va peut-être changer au cours du projet). Si les cas d'utilisation sont très importants, je trouve leur représentation graphique inutile : le dessin n'apporte rien de plus par rapport au texte.

Diagramme des composants : les relations entre les différents fichiers source, entre les différentes bibliothèques.

Diagramme de déploiement : répartition du système sur diverses machines (par exemple : le poste client, le/les serveurs Web, le cache, la/les bases de données, etc.).

Diagramme d'activité : organigramme [ne pas rire], généralement divisé en plusieures colonnes, une par objet.

Concevoir des applications Web avec UML

La première partie du livre est bonne à jeter : elle ne contient que du discour marketing. La seconde partie contient un exemple concrêt d'application (une application Web, comme chacun l'aura deviné d'après le titre) d'UML lors de la réalisation d'un projet.

UML, Guide de l'utilisateur (G. Booch)

Pas lu.

UML en action (P. Rocques, F. Vallée)

Le livre décrit une méthode de développement objet, le Two-Track Unified Process. Je n'ai pas du tout aimé : voir plutôt l'autre livre de P. Rocques, UML par la pratique.

UML par la pratique (P. Rocques)

La plupart des livres sur UML, soit se contentent de décrire les diagrammes que l'on peut dessiner, soit présentent un (ou des) projet(s) réel(s) pour lesquels on donne tout de suite les "bons" diagrammes. Ce n'est pas réaliste : quand on programme, on commet des erreurs.

Ce livre, justement, ne présente pas ces travers : il s'agit d'un recueil d'exercices, qui indique les erreurs à ne pas commettre et qui explique aussi comment on peut être amené à modifier ces diagrammes u cours d'un projet.

UML pour l'analyse d'un système d'information (C. Morley et al., Dunod, 2000)

Non.

Introduction au Rational unified Process (P. Krachten)

Ce livre brasse beaucoup de vent : il se contente de brasser des principes abstraits très généraux utiles en génie logiciel et utilisé dans le logiciel de gestion de projet Rational Rose, sans jamais donner d'exemple concret.

L'auteur énonce les « six meilleurs principes de dévbeloppement » :

1. Développement itératif (i.e., même si on sait où on va, il est bon de s'arréter en chemin pour vérifier qu'on va dans la bonne direction)

2. Gérer les exigences (i.e, établir un cahier des charges, des scénarios d'utilisation)

3. Programmer de manière modulaire

4. Modéliser graphiquement le logiciel

5. Faire des tests

6. Contrôler les changements (i.e., faire des tests)

En termes plus simples : savoir où on va (2, 4), savoir qu'on n'y arrivera pas directement (1, 3), vérifier qu'on y est bien arrivé, ou au moins qu'on est dans la bonne direction (5,6).

Voici un extrait du vocabulaire qu'ils utilisent

Travailleur : c'est la casquette que l'on porte (par exemple : architecte logiciel, programmeur, rédacteur de documentation), et dont on peut changer plusieurs fois en cours de journée.

Activité : Ce que l'on fait (réflexion/action/inspection)

Artéfact : le résultat de ce que l'on a fait (lignes de code, tests, documentation, rapports de bug)

Itération : Composée de : mise en route (les cas d'utilisation : que veut-on ?)élaboration (planifier), construction (coder), transition (feedback des utilisateurs).

4+1 vues : vue logique (ce que voit l'utilisateur), vue des processus (ce qui se passe à l'intérieur), vue d'implémentation (répartition des fichiers soources, des bibliothèques, etc.), vue de déploiement (les différentes machines sur lesquelles cela tourne), vue des cas d'utilisation.

Conception Orientée objet et Applications (G. Booch, AW)

Le livre est écrit dans le style des sciences humaines, i.e., avec plein de citation, de références : « comme le dit si bien... », « d'après des études de... », « selon les articles de... », « les expériences menées par ... nous apprennent que... ». C'est très pénible à lire et essentiellement vide. Pour voir à quoi peut mener ce genre de style, lisez

http://www.physics.nyu.edu/faculty/sokal/

Les deux premières partie du livre, si on se débarasse du langage creux des sciences humaines, se résument à ceci. Le développement d'un logiciel passe par les étapes suivantes :

1. Identifier les classes et les objets (à un niveau d'abstraction donné).

2. Identifier leur sémantique (i.e., leurs champs et leurs méthodes).

3. Identifier leurs relations.

4. Implémenter le tout.

A FAIRE : DESSIN

1. Des petits carrés
2. Des petits carrés avec des traits qui en sortent
3. Les traits se prolongent et relient les carrés pour
former un graphe. 

(Utiliser GraphViz pour faire le dessin, 
retirer une partie des arrêtes avec Gimp).

La dernière partie du livre (beaucoup plus lisible !) comporte des exemples. En fait, c'est la seule partie, mais il ne faut pas y voir des exemples, mais plutôt des exercices : il faut les faire et ensuite comparer et critiquer sa solution avec la solution proposée. Il est à noter que le livre suppose qu'on a tout de suite la bonne décomposition en classes et en objets, que l'on ne changera jamais, i.e., qu'on ne commet jamais d'erreur (hypothèse assez peu réaliste pour qui a déjà programmé).

Les solutions proposées soulignent quelques règles de bon sens : faire simple, factoriser le code dès que le besoin s'en fait sentir.

Voici les exemples : un système de chauffage domestique ; modélisation (cliquodrome) d'expérience avec un banc d'optique ; système de gestion de bugs ; cryptoanalyse élémentaire (simple substitution de lettres, comme chez C. Doyle ou E. A. Poe) ; destion de trafic.

Le Processus Unifié de développement logiciel (I. Jacobson et al., Eyrolles)

C'est l'une des méthodes de développement objet à la mode ; elle est basée sur les cas d'utilisation et un développement itératif et incrémental (ça veut dire qu'on rajoute des cas d'utilisation au fur et à mesure). Un cycle se décompose ainsi : décrire les cas d'utilisation, les analyser ; réfléchir à la manière dont on va les implémenter : les implémenter ; tester.

Refactoring: Improving the Design of Existing Code (M. Fowler)

En fait, je n'ai pas lu le livre, mais juste le site Web correspondant, qui présente la même chose avec un peu moins de détails.

http://www.refactoring.com/

La refactorisation, c'est la modification du code : on remplace du code qui marche, par du code différent, plus beau, plus pratique ou plus extensible, qui marche aussi (pour être sûr que ce nouveau code marche, il faut bien sûr procéder à des tests de régression).

Le premier exemple de refactorisation, et le plus important, consiste isoler dans une fonction du code compliqué qui apparait plusieures fois -- ou même du code compliqué qui n'apparaît qu'une seule fois. Les refactorisations sont nécessaires, car d'une part on ne programme pas bien du premier coup, on commet toujours des erreurs, on fait toujours des mauvais choix ; d'autre part, il n'est pas souhaitable que le code soit dès le départ sous sa forme finale (ce serait trop complexe à manipuler). Le livre donne plusieures dizaines d'exemples d'autres types de refactorisation : Quand une méthode renvoie un objet sur lequel on doit systématiquement faire un cast, mettre le cast dans la méthode ; Utiliser des observateurs ; Rendre privés les champs ou les méthodes d'une classes dont on n'a pas besoin directement à l'extérieur ; Séparer présentation, business (i.e., algorithmes, requètes SQL non triviales) et données ; Utiliser des "délégués" (façade ?) dans la couche Business, pour isoler l'implémentation ; Utiliser une classe MachinNull (héritant de la classe Machin) plutôt qu'une valeur nulle pour un objet de type Machin ; Utiliser des proxies (ou proxies/caches) quand c'est nécessaire ; Mettre une classe dans un autre paquetage (au début, on la met là où elle est utilisée, mais quand elle commence à être utilisée ailleurs, il faut la changer de place) ; Réduire la visibilité d'une variable locale ; Eviter les doubles négations ( if(!a.isNotFinished()) ) ; Ne pas hésiter à utiliser break pour sortir d'une boucle ; Utiliser des noms de variable, de méthode ou de classe compréhensibles ; Ne pas mettre des constantes numériques en dur, utiliser plutôt des variables constantes (static final, en Java) ; etc.

Parfois ces conseils ne sont pas à sens unique : on peut, dans certaines circonstances, remplacer une relation bidirectionnelle par une relation unidirectionnelle, et dans dans d'autres, faire le mouvement inverse ; on peut être amené à éclater une classe en plusieures ou au contraire à en fusionner plusieures ; remplacer l'héritage par la composition ou réciproquement ; remplacer une boucle par de la récursivité ou réciproquement, etc.

Ce sont généralement des conseils de bon sens, mais il est parfois nécessaire de les rappeler. Je pense qu'on peut les résumer en le principe suivant : faire en sorte que le programme reste simple, court et facile à lire (court, ça veut dire que les méthodes doivent être courtes : si ça n'est pas le cas, il faut les refactoriser pour les rendre plus lisibles).

On commence à voir apparaître des IDE qui prévoient certaines de ces refactorisations (c'est nécessaire : par exemple, quand on remplace une méthode sans argument par une méthode avec argument, il faut modifier tous les appels de cette méthode -- les IDE peuvent, dans une certaine mesure, automatiser ces changements).

Le chapitre 15, "A longer example" est disponible sur le site Web. Il présente un exemple complet, très détaillé (je ne l'ai pas lu en détails, je me suis essentiellement contenté de regarder les diagrammes UML). Il insiste sur les faits suivants : il faut des tests (il utilise JUnit), pour vérifier que la refactorisation ne change pas le comportement du programme ; il faut faire les changements un par un, de manière à ne pas casser le code, même si on voit tout de suite des dizaines de changements à faire. De surcroit, les premières refactorisations (simples), permettent de mieux comprendre le programme.

La refactorisation comporte aussi un audit du code.

Le livre de Java premier langage (A. Tasso, Eyrolles)

C'est vraiment pour les gens qui n'ont jamais programmé. Le livre comporte suffisemment d'exercices. Sur la fin, le livre parle quand même d'applètes, de servlets, de JSP et de Beans.

Bases de données orientées objets (R.G.G. Cattell, AW)

Quand on veut mettre des objets dans une base de donnée classique (SQL), pour chaque type d'objet, on met ses instances (et leurs propriétés) dans une table, pour chaque type de relation entre objets, on met les relations dans une table.

Avec une base de données à objets, on met directement les objets dans la base, qui se charge (de manière transparente) de tout découper en tables.

De plus, on ne manipule pas les objets directement, mais au travers de méthodes (procédures stockées). Un SGBDOO, c'est donc simplement un langage orienté objet avec de la persistence.

Les SGDBOO sont (suceptibles d'être) utilisés en génie logiciel (gestion des bugs (bugzilla), aide à la conception (rational rose), etc.), en linguistique (hypertexte, XML), etc.

Quelques exigences envers un SGBDOO : il doit permettre de modifier facilement la structure des données (« réutilisabilité ») ; il doit permettre la gestion des versions ; le verrouillage d'un objet

Les méthodes permettent justement la modification de la structure des données : on peut décider de se débarasser d'un champ « date » pour le remplacer par « secondes écoulées depuis le premier janvier 1970 » sans rien changer au code déjà existant -- il suffit d'écrire une méthode « date ».

Le livre distingue différents types de SGBDOO : les langages de programmation auxquels on a ajouté de la persistence (O_2), les SGBD étendus (PostgreSQL), les espaces de stockage d'objets persistents (le module Storable sous Perl ?), des Frameworks pour construire des SGBD dédiés.

XML, langage et applications (A. Michard, Eyrolles)

Le livre est relativement complet (XML, DTD, Schéma, espaces de nommage, XPath, XLink, CSS2 (successeur de DSSSL), XSLT, exemples (RDF, SMIL, MathML, SVG, WebDAV, XMLSignature), SAX, DOM (5 pages), lien avec les bases de données), peut-être même trop : bien qu'il y ait plein d'exemples, il n'y en a pas toujours suffisemment (par exemple, je n'ai toujours pas compris XLink). Il manqerait aussi quemques exercices.

XSLT, développement en XML et HTML (K. Yee Fung)

Je l'ai juste feuilleté (car je connais déjà suffisemment XSLT), mais ça a l'air très clair et très détaillé (beaucoup plus que le livre précédent) : si je l'ouvre au hasard, ou à un passage abordant des points que je comprends mal, je trouve que c'est limpide.

Attention toutefois : d'une part, le livre n'aborde que XSLT, qui est simple mais limité (on a parfois besoin de SAX ou DOM) ; d'autre part, il n'aborde que des tâches « raisonnables » -- voir

http://www.xml.com/pub/a/2002/03/27/templatexslt.html

Extreme Programming

En fait, je n'ai lu aucun livre sur le sujet. (voir toutefois Extreme Programming Explained). C'est une méthode de développement logiciel, particulièrement adaptée aux projets pas trop gros (moins de dix programmeurs, moins de six mois). Si j'ai bien compris, il s'agit d'appliquer les principes suivants.

1. "Release early release often". Quand on écrit un logiciel, on fait cela par cycles (réfléchir/coder/tester). Généralement, ces cycles sont très longs ; l'Extreme Programming suggère de les raccourcir.

2. Factoriser dès que possible

3. Tester le logiciel. Il faut même essayer d'écrire les tests avant de coder (comme ça, si ce que l'on écrit est inutilisable, ou difficile à utiliser, on s'en apperçoit tout de suite ; ça permet aussi (surtout) de vérifier qu'on répond bien aux attentes du client, ou qu'on n'a rien cassé en modifiant telle ou telle partie du programme)

4. Travailler en binômes (deux personnes par clavier) et changer ces binômes assez souvent (toutes les semaines).:w

Vérification de logiciels, techniques et outils du model-checking (Schnoebelen et al., Vuibert)

Le titre laisse suggérer quelque chose d'incompréhensible, mélange de technicité, de langage inutilement spécialisé et de discour marketting -- eh bien non, en fait, c'est très clair.

Il s'agit de vérifier que certains systèmes se comportent bien comme on s'y attent, i.e., de montrer que certains programmes ne comportent pas de bugs. Par exemple : des protocoles de communication, les bus des ordinateurs, les circuits intégrés, les systèmes automatiques (assenceur, aiguillage ferroviaire, etc.)

Le chapitre 1 rappelle ce qu'est un automate fini ou Structure de Kripke (avec les exemples de l'assenceur, du digicode, et du partage d'une imprimante par deux utilisateurs), explique comment faire le produit de plusieurs automates, qui représentent les différentes composantes d'un système (dans les exemples concret, on ne prend pas le produit tout entier, mais un "produit synchronisé", i.e., dans lequel on ne prend pas toutes les transitions). On peut généraliser ces définitions en rajoutant des variables : on n'élargit pas la classe des automates finis, mais ils ont moins d'états et sont plus faciles à manipuler et à comprendre pour des êtres humains.

Le but du livre est de présenter des méthodes et des outils pour vérifier que ces automates satisfont bien les propriétés attendues, du genre "tout fichier envoyé à l'imprimante sera imprimé un jour" ou "la porte de l'assenceur ne s'ouvre que si l'assenceur est derrière". Ca peut sembler trivial, mais :

- même dans des exemples simplistes, ça ne saute pas aux yeux (pour l'imprimante, ils ont donné un automate qui ne marche pas, et je ne l'ai pas vu tout de suite) ;

- dans des exemple réels, les automates s'obtiennent ) l'aide de produits d'automates décrivant des sous-systèmes, et leur nombre énorme d'états impse l'utilisation de méthodes formelles.

- même dans des systèmes simples, les choses se compliquent si on veut que le système soit tolérant à certaines défaillances (il y a eu beaucoup d'accidents d'assenceur, cet été).

Le chapitre 2 aborde la Logique Temporelle. On peut exprimer les propriétés à vérifier à l'aide de quantificateurs, mais on obtient des formules très compliquées.

\forall t \forall n [ app(n,t)  =>  \exists t' > t : dess(n,t') ]

L'idée est simplement d'utiliser un autre langage pour noter la même chose, en remplaçant les quantificateurs \forall, \exists et la variable t par les symboles suivants.

X   l'état suivant
F   un état futur
G   tous les états futurs
GF  un nombre infini de fois (noté F avec in infini au dessus)
FG  à partir d'un certain moment (noté G avec un infini au dessus)
U   jusqu'à ce que (l'évènement limite se produit toujours)
W   jusqu'à ce que (l'évènement limite ne se produit pas nésessairement)

Exemples :

G ( alerte => F arrêt )

Les quantificateurs précédents s'attachaient à une exécution. On peut aussi vouloir regarder toutes les exécutions qui partent de l'état courrant.

A   pour toutes les exécutions qui partent de l'état courrant
E   pour au moins une exécution qui part de l'état courrant

Plus précisémént, cette logique temporelle s'appelle CLT* (Computationnal Tree Logic), mais il y en a plein d'autres : CLT (on exige que tout quantificateur temporel soit immédiatement précéde par un A ou un E -- on montre qu'on obtient alors uniquement des formules d'état) ou PLTL (Propositionnal Linear Temporal Logic, on enlève A et E à CTL*). Ces autres logiques sont plus restrictives (i.e., elles ne permettent pas d'exprimer toute les propriétés suceptibles de nous intéresser), mais elles sont plus faciles à manipuler, et présentent des propriétés plus agréables.

Le chapitre 3 présente un algorithme de model-checking pour CTL (il est linéaire, il utilise le fait qu'il s'agit de formules d'états : l'algorithme peut se contenter de marquer les états) et pour PLTL.

Dans le chapitre 4, on aborde le problème de la taille des automates qu'il faut manipuler. L'idée consiste à ne pas représenter explicitement tous les états.

On peut utiliser pour cela des BDD (Binary Decision Diagrams). A une expression logique (booléenne) à n variables x1, x2,..., xn, on peut associer un arbre de décision binaire (à la racine, on met "est-ce que x1 est vrai ou faux ?", à chacune des deux feuilles on fait pareil avec x2 et ainsi de suite -- on obtient un arbre à 2^n feuilles, dont les feuilles sont étiquetées par "vrai" ou "faux", selon la valeur de l'expression correspondant à ces valeurs des variables). On peut ensuite réduire ce graphe, i.e., retirer les noeuds inutiles (dont les sous-arbres gauche et droit sont identiques) et identifier les noeuds dont les sous-arbres sont identiques -- si bien que ce n'est plus un arbre. Si on choisit un ordre sur les variables x1,...,xn, deux formules sont équivalentes si et seulement si elles ont le même graphe réduit. De plus, on peut implémenter en un temps raisonnable toutes les opérations sur les formules (et, ou, non, projection (i.e., faire "xi=vrai" dans une formule), abstraction (i.e., remplacer "S" par "\exists xi S")).

Si on prend maintenant un automate, on peut représenter ses états par un vecteur booléen (il suffit de nuléroter les états et d'écrire ces numéros en binaire) et considérer la formule P(x1,...,xn) (où P est la propriété qui nous intéresse, et que l'on aimerait bine voir vérifiée par tous les états).

Ce permet juste de représenter les états : restent les transitions. On procède de la même manière : une transition est une paire (état de départ, état d'arrivée), que l'on code en binaire.

Le chapitre 5 aborde les problèmes des systèmes temps réel : on a besoin de pouvoir exprimer des propriétés du genre "l'alarme se déclenche dans moins de cinq secondes". (On pourrait le faire en décrétant que "1 transition = une unité de temps", mais cela rend les automates difficiles à manipuler (transitions instantannées, temporisations, etc.).) On utilise alors un (ou des) chronomètres, et sur les transitions, on rajoute : des conditions sur la valeur de ces chronomètres et éventuellement des actions de remise à zéro de certains chronomètres.

Pour le model-checking, on peut essayer de se ramener à des problèmes d'atteignabilité (par exemple, en rajoutant un automate observateur à notre réseau d'automates, dont le rôle des de regarder si une certaine propriété est satisfaite). Mais il est souvent préférable d'utiliser une logique temporelle temporisée : on ajoute des indices du genre ">3" ou "<4" aux quantificateurs F et G. On essaye ensuite de discrétiser le temps et de regroupper les états pour appliquer les algorithmes.

La seconde partie du livre s'intéresse à la classification des différentes propriétés que l'on attend de notre système -- car on peut développer des algorithmes qui ne marchent que pour certains types de propriétés. On distingue les prorpiétés d'atteignabilité (une certaine situation peut être atteinte), de vivacité (une certaine situation sera atteinte), de sûreté (une certaine situation ne sera jamais atteinte), d'équité (une certaine situatin sera atteinte un nombre infini de fois).

On peut utiliser différentes méthodes d'abstraction, pour simplifier l'automate et le remplacer par un automate équivalent pour la formule à vérifier (en fait, on a besoin que d'une équivalence : Si l'automate simplifie vérifie la propriété, Alors l'automate de départ aussi). Pour cela, on peut utiliser : la fusion d'états ; l'abstraction sur les variables (on peut parfois les supprimer, ou simplement supposer qu'elles sont bornées) ; la restriction (interdire certains comportement, i.e., certaines transitions -- si la restriction est complexes, on peut l'implémenter à l'aide d'un automate observateur).

La troisième partie donne des exemples de logiciels qui font ce genre de choses (certains permettent aussi de faire des simulations) : SMV (Symbolic Model Checking) ; Spin (pour les algorithmes répartis) ; Design/CPN (Colored Petri Nets, outil indistriel) ; Uppaal (systèmes temporisés) ; Kronos (TCTL) ; Hytech (analyse des automates hybrides (les horloges ne vont pas à la même vitesse) linéaires).

Introduction aux algorithmes et structures parallèles (Leighton)

Le livre est très gros mais relativement clair. Il expose les algorithmes qui sont mis en oeuvre à l'intérieur des microprocesseurs (qu'il s'agisse de microprocesseurs construits pour une tâche particulière, par exemple, des attaques cryptographiques ou du Z-buffer, ou des morceaux de microprocesseurs généralistes effectuant des tâches particulières).

La forme du réseau joue un rôle important : linéaire, grille, arbre, tore, multigrille (on prend une grille 2^n x 2^n, on met au dessus une grille 2^{n-1} x 2^{n-1}, et ainsi de suite), X-arbre (comme un arbre binaire, mais avec en plus plein d'arrêtes horizontales), grille d'arbres (on part d'une grille, au dessus de chaque ligne on ajoute un arbre binaire, et on fait pareil en dessous de chaque colonne -- c'est très important, les grilles d'arbres), hypercube (cube de dimension n : il peut servir à simuler tous les réseaux précédents). Certains problèmes sont liés à la forme du réseau : la capacité d'entrée-sortie (un réseau linéaire qui reçoit ses données une par une à une de ses extrémités mettra probablement plus de temps à les traiter qu'un réseau en grille ou en arbre qui les reçoit toutes en même temps) ; le diamètre (le temps qu'il faut à une information pour passer d'un noeud du réseau à un autre plus éloigné) ; la bissection (on recherche les goulots d'étranglement dans la forme du réseau en essayant de le diviser en deux morceaux, connexes, et en comptant le nombre d'arrêtes entre ces deux morceaux -- si ce nombre peut être très bas, il y a un goulot d'étranglement).

Pour analyser un tel algorithme, on peut supposer que le réseau est suffisemment grand, i.e., qu'il a la même taille que les données -- on peut ensuite modifier l'algorithme pour réduire la taille du réseau, mais l'analyse précédente reste valable.

Le livre donne des exemples d'algorithmes dans des domaines variés : tri, calcul numérique, matriciel, traitement d'images, géométrie algorithmique, etc.

Il y a un point sur lequel le livre est confus : la notion de "modèle systolique" du calcul parallèle (qui n'est pas défini -- ou du moins, s'il l'est, je n'ai pas trouvé où) et celle de "modèle semi-systolique" (qui est définie en détail, mais à partir de la définition (inexistante) précédente). Je crois avoir compris que dans le modèle semi-systolique, on a le droit d'avoir des variables "globales", ce qui revient à autoriser certaines transmissions d'information instantannées entre les processeurs. De toutes manières, on peut émuler de modèle à l'aide du modèle systolique, avec un temps d'exécution comparable (double).

Vincent Zoonekynd
<zoonek@math.jussieu.fr>
latest modification on Mon Sep 9 09:02:16 CEST 2002