Modèle objet de Smalltalk

From OFSET Wiki

Jump to: navigation, search

Traduction de The Squeak object model. Autorisation de S. Ducasse, voir ses autres chapitres.

Contents

[edit] Les règles du modèle

La conception du modèle objet de Smalltalk est basée sur un ensemble de règles simples qui sont appliquées de manière uniforme.

Les règles sont les suivantes :

  • Règle 1. Tout est un objet qui a des données privées.
  • Règle 2. Tout objet est instance d'une classe.
  • Règle 3. Une classe définit le comportement au moyen de méthodes publiques et la structure de ces instances au moyen de variables d'instances qui sont privées aux instances.
  • Règle 4. Chaque classe hérite de la description de son comportement et de sa structure à partir d'une seule super-classe.
  • Règle 5. Les objets communiquent uniquement au moyen d'envois de messages (i.e., invocation de méthodes). Quand un objet reçoit un message, la méthode correspondante est recherchée dans la classe du receveur, puis si elle n'est pas trouvée dans cette classe dans les super-classes.
  • Règle 6. La classe Object est la racine de l'arbre d'héritage (dans Squeak, c'est ProtoObjet, la classe qui représente les objets qui comprennent le plus petit ensemble de messages).
  • Règle 7. Les classes sont des instances aussi. Elles sont instances de classes, appelées méta-classes.

[edit] Précisions sur le modèle

En Smalltalk, on ne dispose que d'objets qui sont instances de classes. Les classes définissent la structure des instances en terme de variables d'instances et de méthodes.

Variables d'instances. Les variables d'instances sont privées à l'instance elle-même, contrairement à Java ou C++. Même les instances de la même classe ne peuvent pas accéder aux variables d'instance d'un objet si celui-ci ne définit pas de méthode d'accès. Les variables d'instances sont accessibles par toutes les méthodes de la classe et les sous-classes. Elles sont donc protégés suivant le jargon C++. Néanmoins, nous préférons dire qu'elles sont privées, parce que c'est un mauvais style que d'accéder aux variables d'instances à partir d'une sous-classe.

Méthodes. Toutes les méthodes sont publiques. Une méthode retourne toujours une valeur en utilisant le constructeur ^. Lorsque ce n'est pas spécifié explicitement en utilisant le constructeur ^, la valeur retournée par la méthode est le receveur d'un message i.e., self. self (équivalent de this en Java) représente le receveur du message. La recherche d'un message envoyé à self démarre dans la classe du receveur (comme montré dans la figure 1.1).

Lorsque l'on définit une méthode dans une sous-classe, elle peut cacher une méthode dans les super-classes. Pour avoir accès à de telles méthodes cachées d'une super-classe, les messages doivent être envoyés à super et pas à self. super représente aussi le receveur du message mais la recherche des messages s'effectue à partir de la super-classe de la classe de la méthode qui déclenche l'invocation du super. Dans la figure 1.2, la méthode fullPrintOn: est recherchée dans la classe du receveur EllipseMorph qui ne la définit pas, de telle sorte que la recherche se poursuit dans BorderedMorph, la super-classe EllipseMorph. Cette classe définit la méthode qui peut donc être exécutée. L'expression super fullPrintOn: aStream est alors exécutée. La recherche commence alors dans la super-classe de BorderedMorph. Notez que la classe du receveur n'est pas concerné pour déterminer où débute la recherche. La méthode est définie dans la super-classe, elle est donc exécutée.

self est dit dynamique dans le sens où il représente toujours le receveur du message. Cela signifie que tous les messages envoyés à self sont recherchés en commencant par la classe du receveur. Par exemple dans la figure 1.2, le message fullPrintOn: est envoyé à anEllipseMorph, ce qui fait que la recherche de la méthode colorString: invoquée dans la classe Morph dans l'expression self colorString: aStream (B) démarre dans la classe du receveur: EllipseMorph (C).

Notez bien que le modèle présenté est conceptuel dans le sens où les concepteurs de la machine virtuelle utilisent tout un tas d'astuces et d'optimisations afin d'accélérer la recherche des méthodes. Le principal point ici est de comprendre la sémantique de self et super. Cependant, ce modele est celui implémenté dans Smalltalk.

Abstraction Pour en finir avec les précisions, une classe peut être abstraite. Néanmoins il n'y a pas de constructeur dédié à cela. Une classe est considérée comme abstraite si une de ces méthodes contient l'expression self subclassResponsibility établissant le fait que les sous-classes ont la responsabilité de définir cette méthode. Lorsqu'une telle méthode est exécutée, une exception est déclenchée.

aMethodDeclaredAbstract
"Ceci est de la responsabilité de mes sous-classes de définir cette méthode"
   self subclassResponsibility

Notez que rien ne vous empêche de créer une instance d'une classe possédant des méthodes abstraites. Ceci marche tant jusqu'à ce qu'une méthode abstraite soit invoquée.

[edit] Implications de l'uniformité : du côté de la classe

Dans la première partie de cette page, nous avons affirmé que le modèle de Smalltalk est définit par un ensemble de règles simples qui s'appliquent de manière uniforme. Regardons maintenant les implications de cette uniformité et notamment comment ces règles s'appliquent aux classes elles-même. En fait, comme tout est objet, instance d'une classe (règle un et deux), les classes doivent aussi être des objets instances d'autres classes. Vous avez bien deviné : c'est exactement ce qui ce passe !

Règle 7. En Smalltalk, les classes sont instances d'autres classes, appellées méta-classes, .i.e. simplement des classes dont les instances sont d'autres classes.

Les méta-classes sont des classes normales de telle sorte qu'elles définissent la structure (nom, super-classe, sous-classes, dictionnaire de méthodes, ...) et le comportement (méthodes new, allSubclasses, ...) de classes. Comme une ellipseMorph est décrite par la classe EllipseMorph définissant une variable d'instance color et des méthodes pour dessiner une ellipse, la classe EllipseMorph est décrite par une classe qui spécifie sa structure et ses méthodes. Cette classes s'appelle ""EllipseMorph class""

Graphe d'héritage parallèle. En fait pour des raisons de composition, une classe est la seule instance d'une méta-classe anonyme dans le nom est X classX est le nom de la classe. Par exemple EllipseMorph est la seule instance de la classe EllipseMorph class. L'héritage de structure et de comportement suit les mêmes règles pour les classes que les objets de base. Ainsi la description des variables d'instances de la méta-classe et la définition des méthodes est réutilisé par le biais de l'héritage. Toute la gestion des classes est définit dans la classe Behavior qui définit l'essence d'une classe, ClassDescription qui ajoute les noms des variables d'instances, les catégories de méthodes, Class et Metaclass qui s'occupent du fait qu'il n'y a qu'une instance. La figure 1.3 présente la situation : chaque classe est une instance de sa méta-classe et l'héritage de méta-classes suit celui des classes.

La classe Behavior qui hérite de Object définit les méthodes new et new: qui créent les instances et toutes les autres méthodes cruciales pour la gestion des classes et des instances comme il est montré sur la figure 1.4. Ainsi quand le message new est envoyé à la classe EllipseMorph, il est recherché dans la classe EllipseMorph et dans ses super-classes. De manière ultime, la méthode new défini dans la classe Behavior est exécutée pour créer une instance de la classe EllipseMorph.

Règle 8. Une classe X est la seule instance d'une classe anonyme nommé X class. La méta-classe X Class hérite de la méta-classe de la super-classe de la classe X.

[edit] Variables d'instances de classe et méthodes de classe

En fait en Smalltalk, il n'y a qu'un seul modèle d'exécution, qui s'applique à deux niveaux : au niveau des instances et au niveau des classe. Les variables d'instances de classe sont juste des variables d'instances de la métaclasse et les méthodes de classes sont juste des méthodes définies sur les méta-classes.

Variables d'instances de classe. Nous utilisons le terme de variables d'instances de classe pour désigner les variables d'instances définit par une méta-classe qui décrit des classes. Par exemple, la variable d'instance superclass qui décrit la superclasse d'une classe est une variable d'instance de classe (définit dans la classe Behavior). Notez que les variables d'instances de classe ont exactement les mêmes propriétés que les variabes d'instances : elles sont privées à l'instance.

Toutes les méthodes de la classe Point peuvent accéder à la variable d'instance x. De manière similaire toutes les méthodes de classe peuvent accéder la variable d'instance superclass. Les méthodes d'instances de peuvent pas accéder les variables d'instances de classe et vice-versa les méthodes de classe ne peuvent pas accéder aux variables d'instances.

Dans un premier temps, les développeurs Java et C++ peuvent considérer les méthodes et les attributs de classe comme des méthodes (ou membres) statiques. Mais l'uniformité de Smalltalk permet d'aller plus loin en disposant exactement de la même sémantique pour les instances et les classes en terme de résolution de méthodes et de visibilité. Il n'y a aucune contraintes sur les méthodes de classes. Elles peuvent invoquer des méthodes surchargées comme toutes autres méthodes.

Méthodes de classes. L'héritage et la recherche de méthode sont traités exactement de la même façon au niveau de la classe et au niveau de l'instance. Il n'y a pas de règles spéciales. Comme il est établi dans la règle 5, lorsqu'un message est envoyé à un objet, une méthode est recherché dans la classe du receveur. Le fait que l'objet est un objet de base ou une classe ne rentre pas en ligne de compte. La règle s'applique dans tous les cas comme il est montré dans la figure 1.5. Nous utilisons le terme méthode de classe pour parler de méthodes définies du côté des classes mais il n'y a pas de différence. Il y a une seule implémentatioàn des méthodes et de la recherche.

À propos du Browser. Une classe et sa méta-classe sont deux classes séparées. Une est instance de la deuxième. Le Browser nous aide à naviguer dans ces classes comme il est montré sur la figure 1.6 comme s'il s'agissait d'une seule classe avec une partie statique. Cliquer sur le button d'instance permet de naviguer dans les méthodes de la classe EllipseMorph : les méthodes montrées sont celles qui sont envoyés aux instances de la classe EllipseMorph. Presser le button class permet de naviguer dans la classe EllipseMorph class : les méthodes montrées sont celles qui sont envoyés à la classe EllipseMorph elle-même.

Un exemple : un singleton. Pour vous montrer un exemple concret, imaginons que nous voulons implémenter le patron de conception Singleton, i.e. qui permet de s'assurer qu'une classe créée seulement une et une seule instance. Imaginons que nous voulons une classe WebServer qui ne doit avoir qu'une seule instance. Pour implémenter un tel patron, l'idée est de garder une référence vers la première instance et de la retrourner à la demande. L'implémentation est basée sur la définition d'une variable d'instance de classe.

Nous créons la classe WebServer comme montrée sur la définition de classe suivante. Puis du côté de la classe, nous définissons la variable d'instance uniqueInstance. La variable d'instance est alors privé à l'objet qui représente la classe WebServer.

Object subclass: #WebServer
   instanceVariableNames:'sessions '
   classVariableNames:''
   poolDictionaries: ''
   category:'Web'

WebServer class
   instanceVariableNames:'uniqueInstance '

Afin d'interdire la création d'instance, on redéfinit la méthode WebServer class>>new qui déclenche une erreur.

Dans la catégorie instance création :

WebServer class>>new

   self error: 'You should use uniqueInstance to get the unique instance'

Puis nous définissons la méthode WebServer class>>uniqueInstance qui crée seulement une instance et l'assigne à la variable uniqueInstance si aucune instance n'a été précédemment créé.

Dans la catégorie singleton :

WebServer class>>uniqueInstance
   uniqueInstance isNil
      ifTrue:[uniqueInstance := self basicNew initialize].
   ^uniqueInstance

De manière optionnelle nous implémentons la méthode reset qui réinitialize le singleton.

Dans la catégorie singleton :

WebServer class>>reset
   uniqueInstance := nil

[edit] Autres variables partagées

Le modèle objet de Smalltalk propose d'autre façon de partager des variables de manière globale ou entre les classes et entre les instances et les classes. Ces variables sont appellés variables globales, variables de classe et variable de pool.

Variables globales. Dans Squeak, toutes les variables sont stockés dans un espace de nommage unique appellé Smalltalk, une instance de SystemDictionary. Nous n'en dirons pas plus sur les variables globales car vous ne devez pas les utiliser.

Variables de classes : variables partagées. Parfois, il est utile d'avoir des données qui soient partagées par les instance d'une classe et la classe elle-même. Ceci est possible en utilisant des variables de classes, variables partagées dans le jargon Smalltalk. Le terme variable de classe' indique aussi que la durée de vie des variables est le même que celui de la classe. Mais ce que le terme n'indique pas est que ces variables sont partagés par toutes les instance d'une classe et les classes comme le montre la figure 1.7.

Une variable de classe est une variable qui a la durée de vie d'une classe et qui peut être accédée par toutes les méthodes de classe (et des sous-classes) et par toutes les méthodes d'instances. Nous allons illustrer ce type de variable en allant regarder la classe Date qui représentent des dates. Une variable de classe est déclaré en utilisant la définition de classe subclass:instanceVariableNames:classVarianleNames:poolDictionnaries:category:.

Étude de cas : la classe Date. Une date est un objet représentant la date. Date today retourne un objet qui représente la date courante. Si nous demandons à cet objet de s'afficher lui-même, nous obtenons la date d'aujourd'hui par exemple : 27 novembre 2005. La figure 1.8 montre un objet date dans un inspecteur obtenu en utilisant l'expression Date today inspect. Ce que nous voyons est que l'objet date enregistre seulement un nombre de jours. Il n'y pas de variables d'instances représentant le nom des mois, le nom des jours, le nombre de jours par mois et ainsi de suite. En fait, cette information est partagée par toute les instances de la classe et ainsi représentée par les variables de classe DaysInMonth, FirstDayOfMonth, MonthNames, SecondsInDay, WeekDayNames de la définition de classe de Date comme montrée ci-dessous.

Magnitude subclass: #Date
   instanceVariablesNames: 'julianDayNumber '
   classVariableNames: 'DaysInMonth FirstDayOfMonth MonthNames SecondsInDay WeekDayNames'
   poolDictionaries: ''
   category: 'Kernel-Magnitudes'

Toutes les méthodes de classe de la classe Date (ainsi que les sous-classes) peuvent directement accéder aux variables de classes définies par la classe Date comme il est montré par la méthode monthName où l'on aaccède à la variable de classe MonthNames.

Date>>>monthName
"Answer the name of the month in which the receiver fails"
   ^MonthNames at: self monthIndex

De manière similaire, toutes les méthodes de classe peuvent accéder aux variables de classe. La méthode de classe nameOfDay: accède à la variable de classe WeeekDayNames.

Date class>>nameOfDay: dayIndex
   "Answer a symbol representing the name of the day indexed by dayIndex, 1-7."
   
   ^WeekDayNames at: dayIndex

Les variables de pool sont des concepts très statiques et doivent être définis avant qu'une méthode ne les utilise. Nous vous décourageons fortement de les utiliser.

Initialisation de classes. La question naturelle que l'on peut se poser est comment initialiser des variables de classes. Comme les variables de classes ont la durée de vie des classes, elles sont généralement initialisées par les méta-classes. En Smalltalk, les méthodes de classe nommées initialize joue un rôle spécial. Elles sont utilisées pour initialiser les classes. Lorsqu'une méta-classe définit une méthode initialize, cette méthode est automatiquement appellée par le système lorque la classe est chargé en mémoire. La méthode Date class>>initialize montre comme les variables de classes sont initialisées.

Date class>>initialize 
"Initialize class variables representing the names of the months and 
days and the number of seconds, days in each month, and first day 
of each month."
MonthNames := 
   #(January February March April May June July August September 
   October November December ). 
SecondsInDay := 24 * 60 * 60. 
DaysInMonth := #(31 28 31 30 31 30 31 31 30 31 30 31 ). 
FirstDayOfMonth := #(1 32 60 91 121 152 182 213 244 274 305 335 ). 
WeekDayNames := #(Monday Tuesday Wednesday Thursday Friday Saturday Sunday ).

Variables de pool. Le dernier type de variables partagées est appellés variables de pool (ou dictionnaire de pool). Normalement vous n'avez besoin d'elles. Nous vous les présentons juste que vous compreniez un peu de code Smalltalk en les lisant. Les variables de pool sont groupés en dictionnaires de pool. Un dictionnaire de pool défini un groupe de variables qui sont partagées entre des classes (mais pas leur sous-classes).

Déclaration d'un dictionnaire de pool :

Smalltalk at: #MyPoolDict put: (Dictionary new at: #myPoolVar put: 3 ; yourself)

La classe utilisant un pool le déclare en utilisant la méthode subclass:instanceVariableNames:classVariableNames:poolDictionaries:category:.

Object subclass: #MyClass
   instanceVariableNames: ''
   classVariableNames: ''
   poolDictionaries: 'MyPoolDict'
   category: 'MyTest'

Les méthodes de la classe MyClass peuvent alors directement accéder les variables définies dans le dictionnaire.

MyClass>>aMethod 
Transcript show: myPoolVar printString ;cr.
"equivalent to" 
Transcript show: (MyPoolDict at: #myPoolVar) printString; cr

[edit] Ce que vous devez retenir

En Squeak tout est un objet, instance d'une classe. Les classes définissent la structure via des variables d'instances privées et un comportement via des méthodes publiques des instances de la classe. Chaque classe est l'unique instance de sa méta-classe. Les variables de classes sont des variables privées partagées par la classe et toutes les instances de la classe. Les méthodes initialize sont appelées automatiquement lorsqu'une classe est chargée en mémoire.

Personal tools