IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Cours complet Pharo par l'exemple


précédentsommaire

5. Chapitre 5 - Le modèle objet de Smalltalk

Le modèle de programmation de Smalltalk est simple et homogène : tout est objet et les objets communiquent les uns avec les autres uniquement par envoi de messages. Cependant, ces caractéristiques de simplicité et d’homogénéité peuvent être source de quelques difficultés pour le développeur habitué à d’autres langages de programmation. Dans ce chapitre, nous présenterons les concepts de base du modèle objet de Smalltalk ; en particulier nous discuterons des conséquences de la représentation des classes comme des objets.

5-1. Les règles du modèle

Le modèle objet de Smalltalk repose sur un ensemble de règles simples qui sont appliquées de manière uniforme. Les règles s’énoncent comme suit :

Règle 1. Tout est objet.

Règle 2. Tout objet est instance de classe.

Règle 3. Toute classe a une super-classe.

Règle 4. Tout se passe par envoi de messages.

Règle 5. La recherche des méthodes suit la chaîne de l’héritage.

Prenons le temps d’étudier ces règles en détail.

5-2. Tout est objet

Attention ! le mantra « tout est objet » est très contagieux. Après seulement quelques heures passées avec Smalltalk, vous serez progressivement surpris par la façon dont cette règle simplifie tout ce que vous faites. Par exemple, les entiers sont véritablement des objets (de la classe Integer). Dès lors vous pouvez leur envoyer des messages, comme vous le feriez avec n’importe quel autre objet.

 
Sélectionnez
3 + 4        −→ 7 "envoie '+ 4' à 3, donnant 7"
20 factorial −→ 2432902008176640000 "envoie factorial, donnant un grand nombre"

La représentation de 20 factorial est certainement différente de la représentation de 7, mais aucune partie du code — pas même l’implémentation de factorial(31) — n’a besoin de le savoir puisque ce sont des objets tous deux.

La conséquence fondamentale de cette règle pourrait s’énoncer ainsi :

Les classes sont aussi des objets.

Plus encore, les classes ne sont pas des objets de seconde zone : elles sont véritablement des objets de premier plan auxquels vous pouvez envoyer des messages, que vous pouvez inspecter, etc. Ainsi Pharo est vraiment un système réflexif offrant une grande expressivité aux développeurs.

Si on regarde plus précisément l’implémentation de Smalltalk, nous trouvons trois sortes différentes d’objets. Il y a (1) les objets ordinaires avec des variables d’instance passées par référence ; également (2) les petits entiers(32) qui sont passés par valeur, et enfin (3) les objets indexés comme les Array (tableaux) qui occupent une portion contigüe de mémoire. La beauté de Smalltalk réside dans le fait que vous n’avez aucunement à vous soucier des différences entre ces trois types d’objets.

5-3. Tout objet est instance de classe

Tout objet a une classe ; pour vous en assurer, vous pouvez envoyer à un objet le message class (classe en anglais).

 
Sélectionnez
1.
2.
3.
4.
5.
6.
1 class            −→ SmallInteger
20 factorial class −→ LargePositiveInteger
'hello' class      −→ ByteString
#(1 2 3) class     −→ Array
(4@5) class        −→ Point
Object new class   −→ Object

Une classe définit la structure pour ses instances via les variables d’instance (instance variables en anglais) et leur comportement (behavior en anglais) via les méthodes. Chaque méthode a un nom. C’est le sélecteur. Il est unique pour chaque classe.

Puisque les classes sont des objets et que tout objet est une instance d’une classe, nous en concluons que les classes doivent aussi être des instances de classes. Les classes dont les instances sont des classes sont nommées des métaclasses. À chaque fois que vous créez une classe, le système crée pour vous une métaclasse automatiquement. La métaclasse définit la structure et le comportement de la classe qui est son instance. La plupart du temps, vous n’aurez pas à penser aux métaclasses et vous pourrez joyeusement les ignorer. (Nous porterons notre attention aux métaclasses dans le chapitre 13.)

5-3-1. Les variables d’instance

Les variables d’instance en Smalltalk sont privées vis-à-vis de l’instance elle-même. Ceci diffère de langages comme Java et C++ qui permettent l’accès aux variables d’instance (aussi connues sous le nom d’« attributs » ou « variables membre ») depuis n’importe qu’elle autre instance de la même classe. Nous disons que la frontière d’encapsulation(33) des objets en Java et en C++ est la classe, là où, en Smalltalk, c’est l’instance.

En Smalltalk, deux instances d’une même classe ne peuvent pas accéder aux variables d’instance l’une de l’autre à moins que la classe ne définisse des « méthodes d’accès » (en anglais, accessor methods). Aucun élément de la syntaxe ne permet l’accès direct à la variable d’instances de n’importe quel autre objet. (En fait, un mécanisme appelé réflexivité offre une possibilité d’interroger un autre objet sur la valeur de ses variables d’instance ; ces facilités de métaprogrammation permettent d’écrire des outils tels que l’inspecteur d’objets (nous utiliserons aussi le terme Inspector). La seule vocation de ce dernier est de regarder le contenu des autres objets.)

Les variables d’instance peuvent être accédées par nom dans toutes les méthodes d’instance de la classe qui les définit ainsi que dans les méthodes définies dans les sous-classes de cette classe. Cela signifie que les variables d’instance en Smalltalk sont semblables aux variables protégées (protected) en C++ et en Java. Cependant, nous préférons dire qu’elles sont privées parce qu’il n’est pas d’usage en Smalltalk d’accéder à une variable d’instance directement depuis une sous-classe.

Exemple

La méthode Point»dist: (méthode 5.1) calcule la distance entre le receveur et un autre point. Les variables d’instance x et y du receveur sont accédées directement par le corps de la méthode. Cependant, les variables d’instance de l’autre point doivent être accédées en lui envoyant les messages x et y.

Méthode 5.1 – La distance entre deux points. le nom arbitraire aPoint est utilisé dans le sens de a point qui, en anglais, signifie « un point »
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
Point»dist: aPoint
  "Retour ne la distance entre aPoint et le receveur."
  | dx dy |
  dx := aPoint x - x.
  dy := aPoint y - y.
  ↑ ((dx * dx) + (dy * dy)) sqrt
  1@1 dist: 4@5 −→ 5.0

La raison-clef de préférer l’encapsulation basée sur l’instance à l’encapsulation basée sur la classe tient au fait qu’elle permet à différentes implémentations d’une même abstraction de coexister. Par exemple, la méthode point» dist: n’a besoin ni de surveiller, ni même de savoir si l’argument aPoint est une instance de la même classe que le receveur. L’argument objet pourrait être représenté par des coordonnées polaires, voire comme un enregistrement dans une base de données ou sur une autre machine d’un réseau distribué ; tant qu’il peut répondre aux messages x et y, le code de la méthode 5.1 fonctionnera toujours.

5-3-2. Les méthodes

Toutes les méthodes sont publiques(34). Les méthodes sont regroupées en protocoles qui indiquent leur objectif. Certains noms de protocoles courants ont été attribués par convention, par exemple, accessing pour les méthodes d’accès, et initialization pour construire un état initial stable pour l’objet. Le protocole private est parfois utilisé pour réunir les méthodes qui ne devraient pas être visibles depuis l’extérieur. Rien ne vous empêche cependant d’envoyer un message qui est implémenté par une telle méthode « privée ».

Les méthodes peuvent accéder à toutes les variables d’instance de l’objet. Certains programmeurs en Smalltalk préfèrent accéder aux variables d’instance uniquement au travers des méthodes d’accès. Cette pratique a un certain avantage, mais elle tend à rendre l’interface de vos classes chaotique, ou pire, à exposer des états privés à tous les regards.

5-3-3. Le côté instance et le côté classe

Puisque les classes sont des objets, elles peuvent avoir leurs propres variables d’instance ainsi que leurs propres méthodes. Nous les appelons variables d’instance de classe (en anglais class instance variables) et méthodes de classe, mais elles ne sont véritablement pas différentes des variables et méthodes d’instances ordinaires : les variables d’instance de classe ne sont seulement que des variables d’instance définies par une métaclasse. Quant aux méthodes de classe, elles correspondent juste aux méthodes définies par une métaclasse.

Une classe et sa métaclasse sont deux classes distinctes, et ce, même si cette première est une instance de l’autre. Pour vous, tout ceci sera somme toute largement trivial : vous n’aurez qu’à vous concentrer sur la définition du comportement de vos objets et des classes qui les créent.

Image non disponible
FIGURE 5.1 – Naviguer dans une classe et sa métaclasse.

De ce fait, le navigateur de classes nommé Browser vous aide à parcourir à la fois classes et métaclasses comme si elles n’étaient qu’une seule entité avec deux « côtés » : le « côté instance » et le « côté classe », comme le montre la figure 5.1. En cliquant sur le bouton instance , vous voyez la présentation de la classe Color et vous donc pouvez naviguer dans les méthodes qui sont exécutées quand les messages de même nom sont envoyés à une instance de Color, comme blue (correspondant à la couleur bleue). En appuyant sur le bouton class (pour classe), vous naviguez dans la classe Color class, autrement dit vous voyez les méthodes qui seront exécutées en envoyant les messages directement à la classe Color elle-même. Par exemple, Color blue envoie le message blue (pour bleu) à la classe Color. Vous trouverez donc la méthode blue définie côté classe de la classe Color et non du côté instance.

 
Sélectionnez
1.
2.
3.
4.
aColor := Color blue.   "Méthode de classe blue"
aColor      −→ Color blue
aColor red  −→ 0.0      "Méthode d'accès red (rouge) côté instance"
aColor blue −→ 1.0      "Méthode d'accès blue (bleu) côté instance"

Vous définissez une classe en remplissant le patron (ou template en anglais) proposé dans le côté instance. Quand vous acceptez ce patron, le système crée non seulement la classe que vous définissez, mais aussi la métaclasse correspondante. Vous pouvez naviguer dans la métaclasse en cliquant sur le bouton class. Du patron employé pour la création de la métaclasse, seule la liste des noms des variables d’instance vous est proposée pour une édition directe.

Une fois que vous avez créé une classe, cliquer sur le bouton instance vous permet d’éditer et de parcourir les méthodes qui seront possédées par les instances de cette classe (et de ses sous-classes). Par exemple, nous pouvons voir dans la figure 5.1 que la méthode hue est définie pour les instances de la classe Color. A contrario, le bouton class vous laisse parcourir et éditer la métaclasse (dans ce cas Color class).

5-3-4. Les méthodes de classe

Les méthodes de classe peuvent être relativement utiles ; naviguez dans Color class pour voir quelques bons exemples. Vous verrez qu’il y a deux sortes de méthodes définies dans une classe : celles qui créent les instances de la classe, comme Color class»blue et celles qui ont une action utilitaire, comme Color class»showColorCube. Ceci est courant, bien que vous trouverez occasionnellement des méthodes de classe utilisées d’une autre manière.

Il est commun de placer des méthodes utilitaires dans le côté classe parce qu’elles peuvent être exécutées sans avoir à créer un objet additionnel dans un premier temps. En fait, beaucoup d’entre elles contiennent un commentaire pour les rendre plus compréhensibles pour l’utilisateur qui les exécute.

Image non disponible

Naviguez dans la méthode Color class»showColorCube, double-cliquez à l’intérieur des guillemets englobant le commentaire « Color showColorCube » et tape au clavier CMD–d.

Vous verrez l’effet de l’exécution de cette méthode. (Sélectionnez World ▷ restore display (r) pour annuler les effets.)

Pour les familiers de Java et C++, les méthodes de classe peuvent être assimilées aux méthodes statiques. Néanmoins, l’homogénéité de Smalltalk induit une différence : les méthodes statiques de Java sont des fonctions résolues de manière statique alors que les méthodes de classe de Smalltalk sont des méthodes à transfert dynamique(35). Ainsi, l’héritage, la surcharge et l’utilisation de super fonctionnent avec les méthodes de classe dans Smalltalk, ce qui n’est pas le cas avec les méthodes statiques en Java.

5-3-5. Les variables d’instance de classe

Dans le cadre des variables d'instance ordinaires, toutes les instances d’une classe partagent le même ensemble de noms de variables et les instances de ses sous-classes héritent de ces noms ; cependant, chaque instance possède son propre jeu de valeurs. C’est exactement la même histoire avec les variables d’instance de classe : chaque classe a ses propres variables d’instance de classe privées. Une sous-classe héritera de ces variables d’instance de classe, mais elle aura ses propres copies privées de ces variables. Aussi vrai que les objets ne partagent pas les variables d’instance, les classes et leurs sous-classes ne partagent pas les variables d’instance de classe.

Vous pouvez utiliser une variable d’instance de classe count(36) afin de suivre le nombre d’instances que vous créez pour une classe donnée. Cependant, les sous-classes ont leur propre variable count, les instances des sous-classes seront comptées séparément.

Exemple : les variables d’instance de classe ne sont pas partagées avec les sous-classes. Soit les classes Dog et Hyena(37) telles que Hyena hérite de la variable d’instance de classe count de la classe Dog.

Classe 5.2 – Créer Dog et Hyena
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
Object subclass: #Dog
  instanceVariableNames: ''
  classVariableNames: ''
  poolDictionaries: ''
  category: 'PBE-CIV'

Dog class
  instanceVariableNames: 'count'

Dog subclass: #Hyena
  instanceVariableNames: ''
  classVariableNames: ''
  poolDictionaries: ''
  category: 'PBE-CIV'

Supposons que nous ayons des méthodes de classe de Dog pour initialiser sa variable count à 0 et pour incrémenter cette dernière quand de nouvelles instances sont créées :

Méthode 5.3 – Comptabiliser les nouvelles instances de Dog via Dog class» count
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
Dog class»initialize
  super initialize.
  count := 0.

Dog class»new
  count := count +1.
  ↑ super new

Dog class»count
  ↑ count

Maintenant, à chaque fois que nous créons un nouveau Dog, son compteur count est incrémenté. Il en est de même pour toute nouvelle instance de Hyena, mais elles sont comptées séparément :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
Dog initialize.
Hyena initialize.
Dog count   −→ 0
Hyena count −→ 0
Dog new.
Dog count   −→ 1
Dog new.
Dog count   −→ 2
Hyena new.
Hyena count −→ 1

Remarquons aussi que les variables d’instance de classe sont privées à la classe tout comme les variables d’instance sont privées à l’instance. Comme les classes et leurs instances sont des objets différents, il en résulte que :

Une classe n’a pas accès aux variables d’instance de ses propres instances.

Une instance d’une classe n’a pas accès aux variables d’instance de classe de sa classe.

C’est pour cette raison que les méthodes d’initialisation d’instance doivent toujours être définies dans le côté instance — le côté classe n’ayant pas accès aux variables d’instance, il ne pourrait y avoir initialisation ! Tout ce que peut faire la classe, c’est d’envoyer des messages d’initialisation à des instances nouvellement créées ; ces messages pouvant bien sûr utiliser les méthodes d’accès.

De même, les instances ne peuvent accéder aux variables d’instance de classe que de manière indirecte en envoyant les messages d’accès à leur classe.

Java n’a rien d’équivalent aux variables d’instance de classe. Les variables statiques en Java et en C++ ont plutôt des similitudes avec les variables de classe de Smalltalk dont nous parlerons dans la section 5.7 : toutes les sous-classes et leurs instances partagent la même variable statique.

Exemple : Définir un Singleton. Le patron de conception(38) nommé Singleton(39) offre un exemple type de l’usage de variables d’instance de classe et de méthodes de classe. Imaginez que nous souhaitions d’une part, créer une classe WebServer et d’autre part, s’assurer qu’il n’a qu’une et une seule instance en faisant appel au patron Singleton. En cliquant sur le bouton instance dans le navigateur de classe, nous définissons la classe WebServer comme suit (classe 5.4).

Classe 5.4 – Une classe Singleton
Sélectionnez
1.
2.
3.
4.
5.
Object subclass: #WebServer
  instanceVariableNames: 'sessions'
  classVariableNames: ''
  poolDictionaries: ''
  category: 'Web'

Ensuite, en cliquant sur le bouton class, nous pouvons ajouter une variable d’instance uniqueInstance au côté classe.

Classe 5.5 – Le côté classe de la classe Singleton
Sélectionnez
1.
2.
WebServer class
instanceVariableNames: 'uniqueInstance'

Par conséquent, la classe WebServer a désormais une autre variable d’instance, en plus des variables héritées telles que superclass et methodDict.

Nous pouvons maintenant définir une méthode de classe que nous appellerons uniqueInstance comme dans la méthode 5.6. Pour commencer, cette méthode vérifie si uniqueInstance a été initialisée ou non : dans ce dernier cas, la méthode crée une instance et l’assigne à la variable d’instance de classe uniqueInstance. In fine, la valeur de uniqueInstance est renvoyée. Puisque uniqueInstance est une variable d’instance de classe, cette méthode peut directement y accéder.

Méthode 5.6 – WebServer class»uniqueInstance (côté classe)
Sélectionnez
1.
2.
3.
WebServer class»uniqueInstance
  uniqueInstance ifNil: [uniqueInstance := self new].
  ↑ uniqueInstance

La première fois que l’expression WebServer uniqueInstance est exécutée, une instance de la classe WebServer sera créée et affectée à la variable uniqueInstance. La seconde fois, l’instance précédemment créée sera renvoyée au lieu d’y avoir une nouvelle création.

Remarquons que la clause conditionnelle à l’intérieur du code de création de la méthode 5.6 est écrite self new et non WebServer new. Quelle en est la différence ? Comme la méthode uniqueInstance est définie dans WebServer class, vous pouvez penser qu’elles sont identiques. En fait, tant que personne ne crée une sous-classe de WebServer, elles sont pareilles. Mais en supposant que ReliableWebServer est une sous-classe de WebServer et qu’elle hérite de la méthode uniqueInstance, nous devrions nous attendre à ce que ReliableWebServer uniqueInstance réponde un ReliableWebServer. L’utilisation de self assure que cela arrivera, car il sera lié à la classe correspondante. Du reste, notez que WebServer et ReliableWebServer ont chacune leur propre variable d’instance de classe nommée uniqueInstance.

Ces deux variables ont, bien entendu, différentes valeurs.

5-4. Toute classe a une super-classe

Chaque classe en Smalltalk hérite de son comportement et de la description de sa structure d’une unique super-classe. Ceci est équivalent à dire que Smalltalk a un héritage simple.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
SmallInteger superclass −→ Integer
Integer superclass      −→ Number
Number superclass       −→ Magnitude
Magnitude superclass    −→ Object
Object superclass       −→ ProtoObject
ProtoObject superclass  −→ nil

Traditionnellement, la racine de la hiérarchie d’héritage en Smalltalk est la classe Object (« Objet » en anglais ; puisque tout est objet). En Pharo, la racine est en fait une classe nommée ProtoObject, mais normalement, vous n’aurez aucune attention à accorder à cette classe. ProtoObject encapsule le jeu de méthodes restreint que tout objet doit avoir. N’importe comment, la plupart des classes héritent de Object qui, pour sa part, définit beaucoup de méthodes supplémentaires que presque tous les objets devraient comprendre et auxquels ils devraient pouvoir répondre. À moins que vous ayez une autre raison de faire autrement, vous devriez normalement générer des classes d’application par l’héritage de la classe Object ou d’une de ses sous-classes lors de la création de classe.

Image non disponible

Une nouvelle classe est normalement créée par l’envoi du message subclass: instanceVariableNames: ... à une classe existante. Il y a d’autres méthodes pour créer des classes. Veuillez jeter un coup d’œil au protocole KernelClasses▷Class▷subclass creation pour voir desquelles il s’agit.

Bien que Pharo ne dispose pas d’héritage multiple, il dispose d’un mécanisme appelé traits(40) pour partager le comportement entre des classes distinctes. Les traits sont des collections de méthodes qui peuvent être réutilisées par plusieurs classes sans lien d’héritage. Employer les traits vous permet de partager du code entre les différentes classes sans reproduire ce code.

5-4-1. Les méthodes abstraites et les classes abstraites

Une classe abstraite est une classe qui n’existe que pour être héritée, au lieu d’être instanciée. Une classe abstraite est habituellement incomplète, dans le sens qu’elle ne définit pas toutes les méthodes qu’elle utilise. Les méthodes « manquantes » — celle que les autres méthodes envoient, mais qui ne sont pas définies elles-mêmes — sont dites méthodes abstraites.

Smalltalk n’a pas de syntaxe dédiée pour dire qu’une méthode ou classe est abstraite. Par convention, le corps d’une méthode abstraite contient l’expression self subclassResponsibility(41). Ceci est connu sous le nom de « marker method » ou marqueur de méthode ; il indique que les sous-classes ont la responsabilité de définir une version concrète de la méthode. Les méthodes self subclassResponsibility devraient toujours être surchargées, et ainsi, ne devraient jamais être exécutées. Si vous oubliez d’en surcharger une et que celle-ci est exécutée, une exception sera levée.


Une classe est considérée comme abstraite si l’une de ses méthodes est abstraite. Rien ne vous empêche de créer une instance d’une classe abstraite ; tout fonctionnera jusqu’à ce qu’une méthode abstraite soit invoquée.

Exemple : la classe Magnitude.

Magnitude est une classe abstraite qui nous aide à définir des objets pouvant être comparables les uns avec les autres. Les sous-classes de Magnitude devraient implémenter les méthodes <, = et hash(42). Grâce à ces messages, Magnitude définit d’autres méthodes telles que >, >=, <=, max:, min:, between:and: et d’autres encore pour comparer des objets. Ces méthodes sont héritées par les sous-classes. La méthode < est abstraite et elle est définie comme dans la méthode 5.7.

Méthode 5.7 – Magnitude»<. Le commentaire dit : « répond si le receveur est inférieur à l’argument »
Sélectionnez
1.
2.
3.
Magnitude»< aMagnitude
  "Answer whether the receiver is less than the argument."self subclassResponsibility

A contrario, la méthode >= est concrète ; elle est définie en fonction de < :

Méthode 5.8 – Magnitude»>=. Le commentaire dit : « répond si le receveur est plus grand ou égal à l’argument »
Sélectionnez
1.
2.
3.
>= aMagnitude
  "Answer whether the receiver is greater than or equal to the argument."
  ↑ (self < aMagnitude) not

Il en va de même d’autres méthodes de comparaison.

Character est une sous-classe de Magnitude; elle surcharge la méthode subclassResponsibility de < avec sa propre version de < (voir méthode 5.9). Character définit aussi les méthodes = et hash; elles héritent entre autres des méthodes >=, <= et ∼= de la classe Magnitude.

Méthode 5.9 – Character»<. Le commentaire dit : « répond vrai si la valeur du receveur est inférieure à la valeur du l’argument »
Sélectionnez
1.
2.
3.
Character»< aCharacter
  "Answer tr ue if the receiver's value < aCharacter's value."self asciiValue < aCharacter asciiValue

5-4-2. Traits

Un trait est une collection de méthodes qui peut être incluse dans le comportement d’une classe sans nécessiter un héritage. Les classes disposent non seulement d’une seule super-classe, mais aussi de la facilité offerte par le partage de méthodes utiles avec d’autres méthodes sans lien de parenté avec l’héritage.

Définir un nouveau trait se fait en remplaçant simplement le patron pour la création de la sous-classe par un message à la classe Trait.

Classe 5.10 – Définir un nouveau trait
Sélectionnez
1.
2.
3.
Trait named: #TAuthor
  uses: { }
  categor y: 'PBE-LightsOut'

Nous définissons ici le trait TAuthor dans la catégorie PBE-LightsOut. Ce trait n’utilise(43) aucun autre trait existant. En général, nous pouvons spécifier l’expression de composition d’un trait par d’autres traits en utilisant le mot-clef uses:. Dans notre cas, nous écrivons un tableau vide ({ }).

Les traits peuvent contenir des méthodes, mais aucune variable d’instance. Supposons que nous voulons ajouter une méthode author (auteur en anglais) à différentes classes sans lien hiérarchique ; nous le ferions ainsi :

Méthode 5.11 – Définir la méthode TAuthor»author
Sélectionnez
1.
2.
3.
TAuthor»author
  "Returns author initials"'on' "oscar nierstrasz"

Maintenant, nous pouvons employer ce trait dans une classe ayant déjà sa propre super-classe, disons, la classe LOGame que nous avons définie dans le chapitre 2. Nous n’avons qu’à modifier le patron de création de la classe LOGame pour y inclure cette fois l’argument clef uses: suivi du trait à utiliser : TAuthor.

Classe 5.12 – Utiliser un trait
Sélectionnez
1.
2.
3.
4.
5.
6.
BorderedMorph subclass: #LOGame
  uses: TAuthor
  instanceVariableNames: 'cells'
  classVariableNames: ''
  poolDictionaries: ''
  category: 'PBE-LightsOut'

Si nous instancions maintenant LOGame, l’instance répondra comme prévu au message author.

 
Sélectionnez
LOGame new author −→ 'on'

Les expressions de composition de trait peuvent combiner plusieurs traits grâce à l’opérateur +. En cas de conflit (c.-à-d.. quand plusieurs traits définissent des méthodes avec le même nom), ces conflits peuvent être résolus en retirant explicitement ces méthodes (avec -) ou en redéfinissant ces méthodes dans la classe ou le trait que vous êtes en train de définir. Il est possible aussi de créer un alias des méthodes (avec @) leur fournissant ainsi un nouveau nom.

Les traits sont employés dans le noyau du système(44). Un bon exemple est la classe Behavior.

Classe 5.13 – Behavior définit par les traits
Sélectionnez
Object subclass: #Behavior
  uses: TPureBehavior @ {#basicAddTraitSelector:withMethod:->
    #addTraitSelector:withMethod:}
  instanceVariableNames: 'superclass methodDict format'
  classVariableNames: 'ObsoleteSubclasses'
  poolDictionaries: ''
  category: 'Kernel-Classes'

Ici, nous voyons que la méthode basicAddTraitSelector:withMethod: définie dans le trait TPureBehavior a été renommée en addTraitSelector:withMethod:. Les traits sont à présent supportés par les navigateurs de classe (ou browsers).

5-5. Tout se passe par envoi de messages

Cette règle résume l’essence même de la programmation en Smalltalk.

Dans la programmation procédurale, lorsqu’une procédure est appelée, l’appelant (caller, en anglais) fait le choix du morceau de code à exécuter ; il choisit la procédure ou la fonction à exécuter statiquement, par nom.

En programmation orientée objet, nous ne faisons pas d’« appel de méthodes ». Nous faisons un « envoi de messages. » Le choix de terminologie est important. Chaque objet a ses propres responsabilités. Nous ne pouvons dire à un objet ce qu’il faut faire en lui imposant une procédure. Au lieu de cela, nous devons lui demander poliment de faire quelque chose en lui envoyant un message. Le message n’est pas un morceau de code : ce n’est rien d’autre qu’un nom (sélecteur) et une liste d’arguments. Le receveur décide alors de comment y répondre en sélectionnant en retour sa propre méthode correspondant à ce qui a été demandé. Puisque des objets distincts peuvent avoir différentes méthodes pour répondre à un même message, le choix de la méthode doit se faire dynamiquement à la réception du message.

 
Sélectionnez
3 + 4     −→ 7    "envoie le message + d'argument 4 à l'entier 3"
(1@2) + 4 −→ 5@6  "envoie le message + d'argument 4 au point (1@2)"

En conséquence, nous pouvons envoyer le même message à différents objets, chacun pouvant avoir sa propre méthode en réponse au message. Nous ne disons pas à SmallInteger 3 ou au Point 1@2 comment répondre au message + 4. Chacun a sa propre méthode pour répondre à cet envoi de message, et répond ainsi selon le cas.

L’une des conséquences du modèle d’envoi de messages de Smalltalk est qu’il encourage un style de programmation dans lequel les objets tendent à avoir des méthodes très compactes en déléguant des tâches aux autres objets, plutôt que d’implémenter de gigantesques méthodes procédurales engendrant trop de responsabilités. Joseph Pelrine dit succinctement le principe suivant :

Ne fais rien que tu ne peux déléguer à quelqu’un d’autre †.
†. Don’t do anything that you can push off onto someone else.

Beaucoup de langages orientés objet disposent à la fois d’opérations statiques et dynamiques pour les objets ; en Smalltalk il n’y a qu’envois de messages dynamiques. Au lieu de fournir des opérations statiques sur les classes, nous leur envoyons simplement des messages, puisque les classes sont aussi des objets.

Pratiquement tout en Smalltalk se passe par envoi de messages. À certains stades, le pragmatisme doit prendre le relais :

  • Les déclarations de variables ne reposent pas sur l’envoi de messages. En fait, les déclarations de variables ne sont même pas exécutables. Déclarer une variable produit simplement l’allocation d’un espace pour la référence de l’objet.
  • Les affectations (ou assignations) ne reposent pas sur l’envoi de messages. L’affectation d’une variable produit une liaison de nom de variable dans le cadre de sa définition.
  • Les retours (ou renvois) ne reposent pas sur l’envoi de messages. Un retour ne produit que le retour à l’envoyeur du résultat calculé.
  • Les primitives ne reposent pas sur l’envoi de messages. Elles sont codées au niveau de la machine virtuelle.

À quelques autres exceptions près, presque tout le reste se déroule véritablement par l’envoi de messages. En particulier, la seule façon de mettre à jour une variable d’instance d’un autre objet est de lui envoyer un message réclamant le changement de son propre attribut (ou champ), car ces derniers ne sont pas des « attributs publics » en Smalltalk. Bien entendu, offrir des méthodes d’accès en lecture dites accesseurs (getter, en anglais, renvoyant l’état de la variable) et mutateurs ou méthodes d’accès en écriture (setter en anglais, changeant la variable) pour chaque variable d’instance d’un objet, n’est pas une bonne méthodologie orientée objet. Joseph Pelrine annonce aussi à juste titre :

Ne laissez jamais personne d’autre jouer avec vos données †.
†. Don’t let anyone else play with your data.

5-6. La recherche de méthode suit la chaîne d’héritage

Qu’arrive-t-il exactement quand un objet reçoit un message ?

Le processus est relativement simple : la classe du receveur cherche la méthode à utiliser pour opérer le message. Si cette classe n’a pas de méthode, elle demande à sa super-classe et remonte ainsi de suite la chaîne d’héritage. Quand la méthode est enfin trouvée, les arguments sont affectés aux paramètres de la méthode et la machine virtuelle l’exécute.

C’est, en essence, aussi simple que cela. Mais il reste quelques questions auxquelles nous devons prendre soin de répondre :

  • Que se passe-t-il lorsqu’une méthode ne renvoie pas explicitement une valeur ?
  • Que se passe-t-il quand une classe réimplémente une méthode d’une super-classe ?
  • Quelle différence y a-t-il entre les envois faits à self et ceux faits à super ?
  • Que se passe-t-il lorsqu’aucune méthode n’est trouvée ?

Les règles pour la recherche par référencement (en anglais lookup) présentées ici sont conceptuelles : des réalisations au sein de la machine virtuelle rusent pour optimiser la vitesse de recherche des méthodes. C’est leur travail, mais tout est fait pour que vous ne remarquiez jamais qu’elles font quelque chose de différent des règles énoncées.

Tout d’abord, penchons-nous sur la stratégie de base de la recherche. Ensuite nous répondrons aux questions.

5-6-1. La recherche de méthode

Supposons la création d’une instance de EllipseMorph.

 
Sélectionnez
anEllipse := EllipseMorph new.

Si nous envoyons à cet objet le message defaultColor, nous obtenons le résultat Color yellow(45) :

 
Sélectionnez
anEllipse defaultColor −→ Color yellow

La classe EllipseMorph implémente defaultColor, donc la méthode adéquate est trouvée immédiatement.

Méthode 5.14 – Une méthode implémentée localement. Le commentaire dit : « retourne la couleur par défaut ; le style de remplissage pour le receveur »
Sélectionnez
EllipseMorph»defaultColor
  "answer the default color/fill style for the receiver"
  ↑ Color yellow

A contrario, si nous envoyons le message openInWorld à anEllipse, la méthode n’est pas trouvée immédiatement parce que la classe EllipseMorph n’implémente pas openInWorld. La recherche continue plus avant dans la super-classe BorderedMorph, puis, ainsi de suite, jusqu’à ce qu’une méthode openInWorld soit trouvée dans la classe Morph (voir la figure 5.2).

Image non disponible
FIGURE 5.2 – Recherche par référencement d’une méthode suivant la hiérarchie d’héritage.
Méthode 5.15 – Une méthode héritée. Le commentaire dit : « Ajoute ce morph dans le monde (world). »
Sélectionnez
Morph»openInWorld
  "Add this mor ph to the wor ld."

self openInWorld: self currentWorld

5-6-2. Renvoyer self

Remarquez que EllipseMorph»defaultColor (méthode 5.14) renvoie explicitement Color yellow alors que Morph»openInWorld (méthode 5.15) semble ne rien retourner.

En réalité, une méthode renvoie toujours une valeur qui est, bien entendu, un objet. La réponse peut être explicitement définie par l’utilisation du symbole ↑ dans la méthode. Si lors de l’exécution, on atteint la fin de la méthode sans avoir rencontré de ↑, la méthode retournera toujours une valeur : l’objet receveur lui-même. On dit habituellement que la méthode « renvoie self », parce qu’en Smalltalk la pseudovariable self représente le receveur du message. En Java, on utilise le mot-clef this.

Ceci induit le constat suivant : la méthode 5.15 est équivalente à la méthode 5.16 :

Méthode 5.16 – Renvoi explicite de self. Le dernier commentaire dit : « Ne faites pas cela à moins d’en être sûr »
Sélectionnez
Morph»openInWorld
  "Add this mor ph to the wor ld."

self openInWorld: self currentWorld.
 ↑ self "Don't do this unless you mean it"

Pourquoi écrire ↑ self explicitement n’est pas une bonne chose à faire ? Parce que, quand vous renvoyez explicitement quelque chose, vous communiquez que vous retournez quelque chose d’importance à l’expéditeur du message. Dès lors vous spécifiez que vous attendez que l’expéditeur fasse quelque chose de la valeur retournée. Puisque ce n’est pas le cas ici, il est préférable de ne pas renvoyer explicitement self.

C’est une convention en Smalltalk, ainsi résumée par Kent Beck se référant à la valeur de retour importante « Interesting return value »(46) :

Renvoyez une valeur seulement quand votre objet expéditeur en a l’usage †.
†. Return a value only when you intend for the sender to use the value.

5-6-3. Surcharge et extension.

Si nous revenons à la hiérarchie de classe EllipseMorph dans la figure 5.2, nous voyons que les classes Morph et EllipseMorph implémentent toutes deux defaultColor. En fait, si nous ouvrons un nouvel élément graphique Morph (Morph new openInWorld), nous constatons que nous obtenons un morph bleu, là où l’ellipse (EllipseMorph) est jaune (yellow) par défaut.

Nous disons que EllipseMorph surcharge la méthode defaultColor qui hérite de Morph. La méthode héritée n’existe plus du point de vue d’anEllipse.

Parfois nous ne voulons pas surcharger les méthodes héritées, mais plutôt les étendre avec de nouvelles fonctionnalités; autrement dit, nous souhaiterions pouvoir invoquer la méthode surchargée complétée par la nouvelle fonctionnalité que nous aurons définie dans la sous-classe. En Smalltalk, comme dans beaucoup de langages orientés objet reposant sur l’héritage simple, nous pouvons le faire à l’aide d’un envoi de message à super.

La méthode initialize est l’exemple le plus important de l’application de ce mécanisme. Quand une nouvelle instance d’une classe est initialisée, il est vital d’initialiser toutes les variables d’instance héritées. Cependant, les méthodes initialize de chacune des super-classes de la chaîne d’héritage fournissent déjà la connaissance nécessaire. La sous-classe n’a pas à s’occuper d’initialiser les variables d’instance héritées !

Envoyer super initialize avant toute autre considération lors de la création d’une méthode d’initialisation est une bonne pratique :

Méthode 5.17 – Initialisation de la super-classe. Le commentaire dit : « initialise l’état du receveur »
Sélectionnez
BorderedMorph»initialize
  "initialize the state of the receiver"
  super initialize.
  self borderInitialize

Une méthode initialize devrait toujours commencer par la ligne super initialize.

5-6-4. Envois à self et à super

Nous avons besoin des envois sur super pour réutiliser le comportement hérité qui pourrait sinon être surchargé. Cependant, la technique habituelle de composition de méthodes, héritées ou non, est basée sur l’envoi à self.

Comment l’envoi à self diffère-t-il de celui à super ? Comme self, super représente le receveur du message. La seule différence est dans la méthode de recherche. Au lieu de faire partir la recherche depuis la classe du receveur, celle-ci démarre dans la super-classe de la méthode dans laquelle l’envoi à super se produit.

Remarquez que super n’est pas la super-classe ! C’est une erreur courante et normale que de le penser. C’est aussi une erreur de penser que la recherche commence dans la super-classe du receveur. Nous allons voir précisément comment cela marche avec l’exemple suivant.

Considérons le message constructorString, que nous pouvons envoyer à n’importe quel morph :

 
Sélectionnez
anEllipse constructorString −→ '(EllipseMorph newBounds: (0@0 corner: 50@40)
    color: Color yellow)'

La valeur de retour est une chaîne de caractères qui peut être évaluée pour recréer un morph.

Comment ce résultat est-il exactement obtenu grâce à l’association de self et de super ? Pour commencer, anEllipse constructorString trouvera la méthode constructorString dans la classe Morph, comme vu dans la figure 5.3.

Image non disponible
FIGURE 5.3 – Les envois à self et super.
Méthode 5.18 – Un envoi à self
Sélectionnez
Morph»constructorString
↑ String streamContents: [:s | self printConstructorOn: s indent: 0]

La méthode Morph»constructorString envoie le message printConstructorOn:indent: à self. La méthode correspondant à ce message est alors recherchée dans la hiérarchie de classes d’abord en en commençant dans la classe EllipseMorph et finalement trouvée dans Morph. Cette méthode en retour envoie à self le message printConstructorOn:indent:nodeDict:, qui, à son tour, envoie fullPrintOn: à self. Encore une fois, fullPrintOn: est recherché depuis la classe EllipseMorph et fullPrintOn: est trouvé dans BorderedMorph (revoir la figure 5.3). Ce qui est crucial à observer est le fait qu’un envoi à self provoque une recherche de méthode qui débute dans la classe du receveur, à savoir la classe de anEllipse.

Un envoi à self déclenche le départ de la recherche dynamique de méthode dans la classe du receveur.

Méthode 5.19 – Combiner l’usage de super et self
Sélectionnez
BorderedMorph»fullPrintOn: aStream
  aStream nextPutAll: '('.
  super fullPrintOn: aStream.
  aStream nextPutAll: ') setBorderWidth: '; print: borderWidth;
  nextPutAll: ' borderColor: ' , (self colorString: borderColor)

Maintenant, BorderedMorph»fullPrintOn: utilise l’envoi à super pour étendre le comportement fullPrintOn: hérité de sa super-classe. Parce qu’il s’agit d’un envoi à super, la recherche démarre alors depuis la super-classe de la classe dans laquelle se produit l’envoi à super, autrement dit, dans Morph. Nous trouvons ainsi immédiatement Morph»fullPrintOn: que nous évaluons.

Notez que la recherche sur super n’a pas commencé dans la super-classe du receveur. Ainsi il en aurait résulté un départ de la recherche depuis BorderedMorph, créant alors une boucle infinie !

Un envoi à super déclenche un départ de recherche statique de méthode dans la super-classe de la classe dont la méthode envoie le message à super.

Si vous regardez attentivement l’envoi à super et la figure 5.3, vous réaliserez que les liens avec super sont statiques : tout ce qui importe est la classe dans laquelle le texte de l’envoi à super est trouvé. A contrario, le sens de self est dynamique : self représente toujours le receveur du message courant exécuté. Ce qui signifie que tout message envoyé à self est recherché en partant de la classe du receveur.

5-6-5. MessageNotUnderstood

Que se passe-t-il si la méthode que nous cherchons n’est pas trouvée ?

Supposons que nous envoyons le message foo à une ellipse anEllipse. Tout d’abord, la recherche normale de cette méthode aurait à parcourir toute la chaîne d’héritage jusqu’à la classe Object (ou plutôt ProtoObject). Comme cette méthode n’est pas trouvée, la machine virtuelle veillera à ce que l’objet envoie self doesNotUnderstand: #foo. (voir la figure 5.4.)

Image non disponible
FIGURE 5.4 – Le message foo n’est pas compris (not understood).

Ceci est un envoi dynamique de message tout à fait normal. Ainsi, la recherche recommence depuis la classe EllipseMorph, mais cette fois-ci en cherchant la méthode doesNotUnderstand:(47). Il apparaît que Object implémente doesNotUnderstand:. Cette méthode créera un nouvel objet MessageNotUnderstood (en français : message incompréhensible) capable de démarrer Debugger, le débogueur, dans le contexte actuel de l’exécution.

Pourquoi prenons-nous ce chemin sinueux pour gérer une erreur si évidente ? Parce qu’en faisant ainsi, le développeur dispose de tous les outils pour agir alternativement grâce à l’interception de ces erreurs. N’importe qui peut surcharger la méthode doesNotUnderstand: dans une sous-classe de Object en étendant ses possibilités en offrant une façon différente de capturer l’erreur.

En fait, nous nous simplifions la vie en implémentant une délégation automatique de messages d’un objet à un autre. Un objet Delegator peut simplement déléguer tous les messages qu’il ne comprend pas à un autre objet dont la responsabilité est de les gérer ou de lever une erreur lui-même !

5-7. Les variables partagées

Maintenant, intéressons-nous à un aspect de Smalltalk que nous n’avons pas couvert par nos cinq règles : les variables partagées.

Smalltalk offre trois sortes de variables partagées : (1) les variables globales ; (2) les variables de classe partagées entre les instances et les classes, et (3) les variables partagées parmi un groupe de classes ou variables de pool. Les noms de toutes ces variables partagées commencent par une lettre capitale (majuscule), pour nous informer qu’elles sont partagées entre plusieurs objets.

5-7-1. Les variables globales

En Pharo, toutes les variables globales sont stockées dans un espace de nommage appelé Smalltalk qui est une instance de la classe SystemDictionary. Les variables globales sont accessibles de partout. Toute classe est nommée par une variable globale ; en plus, quelques variables globales sont utilisées pour nommer des objets spéciaux ou couramment utilisés.

La variable Transcript nomme une instance de TranscriptStream, un flux de données ou stream qui écrit dans une fenêtre à ascenseur (dite aussi scrollable). Le code suivant affiche des informations dans le Transcript en passant une ligne.

 
Sélectionnez
Transcript show: 'Pharo est extra' ; cr

Avant de lancer la commande do it, ouvrez un Transcript en sélectionnant World ▷ Tools… ▷ Transcript.

Écrire dans le Transcript est lent, surtout quand la fenêtre Transcript est ouverte. Ainsi, si vous constatez un manque de réactivité de votre système alors que vous êtes en train d’écrire dans le Transcript, pensez à le minimiser (bouton collapse this window).

D’autres variables globales utiles

  • Smalltalk est une instance de SystemDictionary (Dictionnaire Système) définissant toutes les variables globales — dont l’objet Smalltalk lui-même. Les clefs de ce dictionnaire sont des symboles nommant les objets globaux dans le code Smalltalk. Par exemple,

     
    Sélectionnez
    Smalltalk at: #Boolean −→ Boolean

    Puisque Smalltalk est lui-même aussi une variable globale,

     
    Sélectionnez
    Smalltalk at: #Smalltalk −→ a SystemDictionary(lots of globals)}

    et

     
    Sélectionnez
    (Smalltalk at: #Smalltalk) == Smalltalk −→ true
  • Sensor est une instance de EventSensor. Il représente les entrées interactives ou interfaces de saisie (en anglais, input) dans Pharo. Par exemple, Sensor keyboard retourne le caractère suivant saisi au clavier, et Sensor leftShiftDown répond true (vrai en booléen) si la touche shift gauche est maintenue enfoncée, alors que Sensor mousePoint renvoie un Point indiquant la position actuelle de la souris.

  • World (Monde en anglais) est une instance de PasteUpMorph représentant l’écran. World bounds retourne un rectangle définissant l’espace tout entier de l’écran; tous les morphs (objet Morph) sur l’écran sont des sousmorphs ou submorphs de World.

  • ActiveHand est l’instance courante de HandMorph, la représentation graphique du curseur. Les sous-morphs de ActiveHand tiennent tout ce qui est glissé par la souris.

  • Undeclared(48) est un autre dictionnaire, il contient toutes les variables non déclarées. Si vous écrivez une méthode qui référence une variable non déclarée, le navigateur de classe (Browser) vous l’annoncera normalement pour que vous la déclariez, par exemple, en tant que variable globale ou variable d’instance de la classe. Cependant, si par la suite vous effacez la déclaration, le code référencera une variable non déclarée. Inspecter Undeclared peut parfois expliquer des comportements bizarres !

  • SystemOrganization est une instance de SystemOrganizer : il enregistre l’organisation des classes en paquets. Plus précisément, il catégorise les noms des classes ainsi :
 
Sélectionnez
SystemOrganization categoryOfElement: #Magnitude −→ #'Kernel-Numbers'

Une pratique courante est de limiter fortement l’usage des variables globales ; il est toujours préférable d’utiliser des variables d’instance de classes ou des variables de classes et de fournir des méthodes de classe pour y accéder. En effet, si aujourd’hui Pharo devait être reprogrammé à partir de zéro(49) 19, la plupart des variables globales qui ne sont pas des classes seraient remplacées par des Singletons.

La technique habituellement employée pour définir une variable globale est simplement de faire un do it sur une affectation d’un identifiant non déclaré commençant par une majuscule. Dès lors, l’analyseur syntaxique ou parser vous la déclarera en tant que variable globale. Si vous voulez en définir une de manière programmatique, exécutez Smalltalk at: #AGlobalName put: nil.

Pour l’effacer, exécutez Smalltalk removeKey: #AGlobalName.

5-7-2. Les variables de classe

Nous avons besoin parfois de partager des données entre les instances d’une classe et la classe elle-même. C’est possible grâce aux variables de classe. Le terme variable de classe indique que le cycle de vie de la variable est le même que celui de la classe. Cependant, le terme ne véhicule pas l’idée que ces variables sont partagées aussi bien parmi toutes les instances d’une classe que dans la classe elle-même comme nous pouvons le voir sur la figure 5.5. En fait, variables partagées (ou shared variables, en anglais) aurait été un meilleur nom, car ce dernier exprime plus clairement leur rôle tout en pointant le danger de les utiliser, en particulier si elles sont sujettes aux modifications.

Image non disponible
FIGURE 5.5 – Des méthodes d’instance et de classe accédant à différentes variables.

Sur la figure 5.5, nous voyons que rgb et cachedDepth sont des variables d’instance de Color uniquement accessibles par les instances de Color. Nous remarquons aussi que superclass, subclass, methodDict… etc., sont des variables d’instance de classe, c.-à-d. des variables d’instance accessibles seulement par Color class.

Mais nous pouvons noter quelque chose de nouveau : ColorNames et CachedColormaps sont des variables de classe définies pour Color. La capitalisation du nom de ces variables nous donne un indice sur le fait qu’elles sont partagées. En fait, non seulement toutes les instances de Color peuvent accéder à ces variables partagées, mais aussi la classe Color elle-même, ainsi que toutes ses sous-classes. Les méthodes d’instance et de classe peuvent accéder toutes les deux à ces variables partagées.

Une variable de classe est déclarée dans le patron de définition de la classe. Par exemple, la classe Color définit un grand nombre de variables de classe pour accélérer la création des couleurs ; sa définition est visible ci-dessous (classe 5.20).

Classe 5.20 – Color et ces variables de classe
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
Object subclass: #Color
    instanceVariableNames: 'rgb cachedDepth cachedBitPattern'
    classVariableNames: 'Black Blue BlueShift Brown CachedColormaps ColorChart
    ColorNames ComponentMask ComponentMax Cyan DarkGray Gray
    GrayToIndexMap Green GreenShift HalfComponentMask HighLightBitmaps
    IndexedColors LightBlue LightBrown LightCyan LightGray LightGreen LightMagenta
    LightOrange LightRed LightYellow Magenta MaskingMap Orange PaleBlue
    PaleBuff PaleGreen PaleMagenta PaleOrange PalePeach PaleRed PaleTan
    PaleYellow PureBlue PureCyan PureGreen PureMagenta PureRed PureYellow
    RandomStream Red RedShift TranslucentPatterns Transparent VeryDarkGray
    VeryLightGray VeryPaleRed VeryVeryDarkGray VeryVeryLightGray White Yellow'
    poolDictionaries: ''
    category: 'Graphics-Primitives'

La variable de classe ColorNames est un tableau contenant le nom des couleurs fréquemment utilisées. Ce tableau est partagé par toutes les instances de Color et de sa sous-classe TranslucentColor. Elles sont accessibles via les méthodes d’instance et de classe.

ColorNames est initialisée une fois dans Color class»initializeNames, mais elle est en libre accès depuis les instances de Color. La méthode Color»name utilise la variable pour trouver le nom de la couleur. Il semble en effet inopportun d’ajouter une variable d’instance name à chaque couleur, car la plupart des couleurs n’ont pas de noms.

L’initialisation de classe

La présence de variables de classe soulève une question : comment les initialiser ?

Une solution est l’initialisation dite paresseuse (ou lazy initialization en anglais). Cela est possible avec l’introduction d’une méthode d’accès qui initialise la variable, durant l’exécution, si celle-ci n’a pas été encore initialisée. Ceci nous oblige à utiliser la méthode d’accès tout le temps et à ne jamais faire appel à la variable de classe directement. De plus, notons que le coût de l’envoi d’un accesseur et le test d’initialisation sont à prendre en compte. Ceci va à l’encontre de notre motivation à utiliser une variable de classe, parce qu’en réalité elle n’est plus partagée.

Méthode 5.21 – Color class»colorNames
Sélectionnez
1.
2.
3.
Color class»colorNames
  ColorNames ifNil: [self initializeNames].
  ↑ ColorNames

Une autre solution consiste à surcharger la méthode initialize.

Méthode 5.22 – Color class»initialize
Sélectionnez
1.
2.
3.
Color class»initialize
  ...
  self initializeNames

Si vous adoptez cette solution, vous devez vous rappeler qu’il faut invoquer la méthode initialize après l’avoir définie, par ex., en utilisant Color initialize. Bien que les méthodes côté classe initialize soient exécutées automatiquement lorsque le code est chargé en mémoire, elles ne sont pas exécutées durant leur saisie et leur compilation dans le navigateur Browser ou en phase d’édition et de recompilation.

5-7-3. Les variables de pool

Les variables de pool(50) sont des variables qui sont partagées entre plusieurs classes qui ne sont pas liées par une arborescence d’héritage. À la base, les variables de pool sont stockées dans des dictionnaires de pool ; maintenant, elles devraient être définies comme variables de classe dans des classes dédiées (sous-classes de SharedPool). Notre conseil : évitez-les. Vous n’en aurez besoin qu’en des circonstances exceptionnelles et spécifiques. Ici, notre but est de vous expliquer suffisamment les variables de pool pour comprendre leur fonction quand vous les rencontrez durant la lecture de code.

Une classe qui accède à une variable de pool doit mentionner le pool dans sa définition de classe. Par exemple, la classe Text indique qu’elle emploie le dictionnaire de pool TextConstants qui contient toutes les constantes textuelles telles que CR et LF. Ce dictionnaire a une clef #CR à laquelle est affectée la valeur Character cr, c.-à-d. le caractère retour chariot ou carriage return.

Classe 5.23 – Dictionnaire de pool dans la classe Text
Sélectionnez
1.
2.
3.
4.
5.
ArrayedCollection subclass: #Text
    instanceVariableNames: 'string runs'
    classVariableNames: ''
    poolDictionaries: 'TextConstants'
    categor y: 'Collections-Text'

Ceci permet aux méthodes de la classe Text d’accéder aux clefs du dictionnaire directement dans le corps de la méthode, c.-à-d.. en utilisant la syntaxe de variable plutôt qu’une recherche explicite dans le dictionnaire. Par exemple, nous pouvons écrire la méthode suivante.

Méthode 5.24 – Text»testCR
Sélectionnez
1.
2.
Text»testCR
  ↑ CR == Character cr

Encore une fois, nous recommandons d’éviter d’utiliser les variables et les dictionnaires de pool.

5-8. Résumé du chapitre

Le modèle objet de Pharo est à la fois simple et uniforme. Tout est objet et quasiment tout se passe via l’envoi de messages.

  • Tout est objet. Les entités primitives telles que les entiers sont des objets, ainsi que les classes qui sont des objets comme les autres.
  • Tout objet est instance d’une classe. Les classes définissent la structure de leurs instances via des variables d’instance privées et leur comportement via des méthodes publiques. Chaque classe est l’unique instance de sa métaclasse. Les variables de classe sont des variables privées partagées par la classe et toutes les instances de la classe. Les classes ne peuvent pas accéder directement aux variables d’instance de leurs instances et les instances ne peuvent pas accéder aux variables de classe de leur classe. Des méthodes d’accès (accesseurs et mutateurs) doivent être définies si besoin.
  • Toute classe a une super-classe. La racine de la hiérarchie basée sur l’héritage simple est ProtoObject. Cependant, les classes que vous définissez devraient normalement hériter de la classe Object ou de ses sous-classes. Il n’y a pas d’élément sémantique pour la définition de classes abstraites. Une classe abstraite est simplement une classe avec au moins une méthode abstraite — une dont l’implémentation contient l’expression self subclassResponsibility. Bien que Pharo ne dispose que du principe d’héritage simple, il est facile de partager les implémentations de méthodes en regroupant ces dernières en traits.
  • Tout se passe par envoi de messages. Nous ne faisons pas des « appels de méthodes », nous faisons des « envois de messages ». Le receveur choisit alors sa propre méthode pour répondre au message.
  • La recherche de méthodes suit la chaîne d’héritage ; les envois à self sont dynamiques et la recherche de méthode démarre dans le receveur de la classe, alors que celles à super sont statiques et la recherche commence dans la super-classe de la classe dans laquelle l’envoi à super est écrit.
  • Il y a trois sortes de variables partagées. Les variables globales sont accessibles partout dans le système. Les variables de classe sont partagées entre une classe, ses sous-classes et ses instances. Les variables de pool sont partagées dans un ensemble de classes particulier. Vous devez éviter l’emploi de variables partagées autant que possible.

précédentsommaire
En français, factorielle.
En anglais, small integers.
En anglais, encapsulation boundary.
En fait, presque toutes. En Pharo, des méthodes dont les sélecteurs commencent par la chaîne de caractères pvt sont privées : un message pvt ne peut être envoyé qu’à self uniquement. N’importe comment, les méthodes pvt sont très peu utilisées.
En anglais, dynamically-dispatched methods.
En français, compteur.
En français, chien et hyène.
En anglais, nous parlons de Design Patterns.
Sherman R. Alpert, Kyle Brown et Bobby Woolf, The Design Patterns Smalltalk Companion. Addison Wesley, 1998, ISBN 0–201–18462–1.
Dans le sens de trait de caractères, nous faisons allusion ainsi à la génétique du comportement d’une méthode.
Dans le sens, laissée à la responsabilité de la sous-classe.
Relatif au code de hachage.
Terme anglais : uses : il signifie « utilise ».
En anglais, System kernel.
Yellow est la couleur jaune.
Kent Beck, Smalltalk Best Practice Patterns. Prentice-Hall, 1997.
Le nom du message peut se traduire par : « ne comprend pas ».
Non déclaré, en français.
Le terme anglais est : from scratch, signifiant depuis le début.
Pool signifie piscine en anglais, ces variables sont dans un même bain !

Licence Creative Commons
Le contenu de cet article est rédigé par Andrew Black et est mis à disposition selon les termes de la Licence Creative Commons Attribution 3.0 non transposé.
Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright © 2013 Developpez.com.