ModulaTor logo, 7.8KB

The ModulaTor

Oberon-2 and Modula-2 Technical Publication

Ubaye's First Independent Modula-2 & Oberon-2 Journal! Nr. F1, Jun-1998


Extensibilité et persistance

Cathy Berthouzoz 
LATL, Université de Genève, Candolle 2 
CH-1204 Genève
email: berthouzoz@latl.unige.ch 
Cathy's homepage

Petra Fabian et Günter Dotzel, ModulaWare, [email deleted due to spam]

Table des matières


1. Introduction

L'extensibilité et la persistance sont des concepts d'un grand intérêt pour le génie logiciel. L'extensibilité permet d'étendre la fonctionnalité d'un module sans en modifier la source. Cette propriété permet une plus grande réutilisabilité des modules, un des problèmes centraux du développement de logiciel. Elle simplifie aussi l'implémentation des structures de données génériques. La persistance permet de faire survivre des objets au programme qui les a créés. Une nouvelle exécution du même programme, ou d'un autre programme, retrouve les objets dans l'état où il les a laissés. Au contraire de l'extensibilité, qui découle de l'extension de type et du polymorphisme, la persistance ne doit pas être une propriété intrinsèque du langage : elle l'alourdirait inutilement, alors qu'une classe de base supportant la persistance peut être fournie avec le compilateur et être étendue lorsque la persistance est nécessaire, grâce à l'extensibilité.

Un langage de programmation extensible comme Oberon-2 possède toutes les propriétés essentielles qui caractérisent un langage de programmation orienté objet, à savoir les concepts de classes, d'héritage, de méthodes et de messages, même si le vocabulaire est différent (type enregistrement extensible pour classe, variable de ce type pour objet, procédure liée au type pour méthode) [Wirt92]. La différence primordiale d'Oberon-2 par rapport à d'autres langages de programmation objet est qu'il est réduit à l'essentiel et qu'il n'introduit que peu de termes étrangers à la programmation procédurale, plus conventionnelle et bien connue des programmeurs. Ses concepts sont clairs, précis et intuitifs, ce qui facilite son apprentissage et son utilisation (voir [Temp94] pour une comparaison entre Oberon-2 et C++).

Cet article commence par présenter les concepts d'Oberon-2. Puis il montre comment utiliser l'extensibilité pour implémenter des structures de données génériques et hétorogènes. Ensuite une réalisation possible de la persistance est présentée. Finalement, toutes ces notions sont mises en pratique dans un exemple réaliste, un gestionnaire d'arbres AVL, pour montrer les avantages d'Oberon-2 - structuration, rapidité, sûreté, lisibilité - par rapport à un langage traditionnel comme Modula-2.

2 Présentation d'Oberon-2

2.1 De Modula-2 à Oberon-2

Oberon est un langage de programmation extensible, développé par Niklaus Wirth et Jürg Gutknecht entre 1985 et 1987 [Wirth92]. Le point de départ est Modula-2, dont un nombre important de propriétés ont été éliminées. Un petit nombre de concepts ont été introduits, dont le plus important est l'extension de type. Il combine les modules de définition et d'implémentation en un seul module où les objets sont explicitement marqués pour l'exportation. Il n'y a plus de mécanisme explicite de déallocation de mémoire : c'est au support d'exécution (RunTime System) de lancer un récupérateur de mémoire inutilisée (garbage collection).

Oberon-2 est une extension d'Oberon compatible vers le haut, développée par Niklaus Wirth, Jürg Gutknecht et Hanspeter Mössenbock en 1991 [Möss93]. La différence essentielle est l'introduction des procédures liées au type (méthodes), ce qui permet d'implémenter des objets polymorphes : la programmation orientée objet devient plus aisée [Möss93]. Les nouvelles caractéristiques comprennent entre autres les tableaux dynamiques et l'exportation de variables en lecture seulement. Oberon-2, comme Oberon, se distingue par sa petitesse et sa fiabilité.

2.2 Héritage

Le mécanisme d'héritage offre la possibilité d'étendre un type de données abstrait dans un nouveau type qui hérite des propriétés de l'ancien type tout en l'augmantant avec ses propres données et méthodes. Les opérations héritées peuvent être modifiées, donnant ainsi aux types de données une grande flexibilité de comportement. C'est l'héritage principalement qui rend un langage de programmation extensible ou orienté objet.

L'avantage principal de l'héritage réside dans le fait que le type étendu reste compatible avec le type original : tous les algorithmes prévus pour le type original peuvent être utilisés avec le type étendu. Il y a donc la possibilité de fabriquer des produits 'semi- finis', qui seront complétés plus tard, et des structures de données génériques, qui pourront contenir des objets différents.

Oberon-2 ne permet pas l'héritage multiple. Mais cette propriété n'est pas essentielle car on peut construire à l'aide de l'héritage simple des structures de données qui présentent les caractéristiques de l'héritage multiple tout en étant beaucoup plus sûres [Temp93].

2.3 Liaison dynamique

Puisqu'un type de base est compatible avec ses extensions, une variable peut, lors de l'exécution, contenir non seulement un objet du type dont elle a été déclarée, mais aussi des objets appartenant à n'importe quelle extension de ce type. Ce qui veut dire que le type dynamique, à l'exécution, peut être différent du type statique, établi à la compilation. Les appels de méthodes, qui dépendent du type dynamique de l'objet, une peuvent donc pas être générés par le compilateur, mais doivent être établis à l'exécution : c'est ce qu'on appelle liaison dynamique (dynamic binding). Des tests de type sont donc nécessaires lors de l'exécution.

2.4 Illustration des concepts de base Classes, Objets et Méthodes

Dans la terminologie objet, un objet est une instance d'une classe. Traduit en Oberon-2, un objet est une variable d'un type enregistrement (ObjectA) ou d'un type pointeur sur un enregistrement (ObjectB). Si on ne peut pas, ou ne veut pas, déterminer les champs, on peut définir un enregistrement vide comme classe de base. Dans ce cas, on parle de classe abstraite, car elle doit être étendue pour être utilisable. Les méthodes sont des procédures liées aux objets

  TYPE 
    ObjectA* = RECORD (* Note 1 *)  
      key : INTEGER; 
    END; 
    ObjectB* = POINTER TO ObjectDesc; 
    ObjectDesc* = RECORD  END;

  PROCEDURE (VAR obj: ObjectDesc) Operation *; 
  END Operation;

  PROCEDURE (obj: ObjectB) Compare * (item: ObjectB); 
  END Compare;

Les méthodes Compare et Operation sont liées à un objet du type ObjectDesc. Le paramètre obj agit comme récepteur (Receiver). Lorsque le récepteur est un type enregistrement, il doit être passé comme paramètre variable, s'il est d'un type pointeur, il doit être passé comme paramètre valeur.

Lorsque le récepteur est un type pointeur, on ne peut passer le message qu'à une variable de type pointeur; par contre lorsque le récepteur est un type enregistrement, le message peut être passé soit à une variable de type pointeur ou enregistrement :

  VAR ptr1, ptr2 : ObjectB; obj : ObjectDesc; 
  ... 
  ptr1.Operation; (* ok *) 
  ptr1.Compare(ptr2); (* ok *) 
  obj.Operation; (* ok *) 
  obj.Compare(ptr2); (* faux *)

Héritage

  TYPE 
    Object1 = POINTER TO ObjectDesc1; 
    ObjectDesc1 = RECORD (ObjectDesc) 
      att: CHAR; 
    END;

  PROCEDURE (obj: Object1) Write; 
  BEGIN 
    IO.Write(obj.att); 
  END Write;

ObjectDesc1 est une extension du type ObjectDesc, dont elle hérite les méthodes Operation et Compare, et qu'elle étend avec l'attribut att et la méthode Write. Elle peut redéfinir les méthodes héritées. Dans ce cas, le récepteur doit être du type étendu, par contre la liste de paramètres doit être identique à celle de la méthode de la classe de base :

  PROCEDURE (obj: Object1) Compare (item : ObjectB); 
  BEGIN 
    (* ... *) 
  END Compare;

  VAR 
    oPtr1,oPtr2 : Object1; oPtrB : ObjectB; 
    obj1 : ObjectDesc1; objB : ObjectDesc;

  NEW(obj1);NEW(objB); 
  NEW(oPtr1); NEW(oPtrB); 
  oPtrB := oPtr1;  (* a) ok *) 
  oPtr1 := oPtrB;  (* b) faux *) 
  objB := obj1;  (* c) projection *) 
  obj1 := objB;  (* d) faux *)

L'héritage garantit la compatibilité entre un type de base et ses extensions. C'est pourquoi les assignations a) et c) ci-dessus sont possibles. Dans le cas a), oPtrB est du type dynamique Object1, mais du type statique ObjectB. Comme ce sont des types pointeurs, ils référencent le même objet, mais les champs qui ne font pas partie de l'extension ne peuvent pas être référencés par oPtrB. Dans le cas c), objB a le même type dynamique que statique car les champs du type étendu sont perdus lors de l'assignation : on parle alors de projection. C'est pourquoi il est préférable d'utiliser des pointeurs lors d'assignations. Les cas b) et c) sont interdits car ObjectDesc n'étant pas une extension de ObjectDesc1, le champs att n'obtient pas de valeur.

On remarque dans la méthode Write l'appel à la procédure IO.Write pour écrire le champ att de l'objet obj. IO est ici un alias pour l'un des modules standard d'entrées/sorties (pour les types prédéfinis du langage) définis dans la librairie ISO Modula2 qui est fournie avec le compilateur Oberon-2 [ISO94]. La possibilité de donner un alias à un module dans la liste d'importation est une catactéristique intéressante du langage car Oberon-2 exige de préfixer toutes les entités importées avec le nom du module qui les exporte.

Liaison dynamique et vérification de type à l'exécution

Après l'assignation a) on ne peut pas référencer le champs att avec oPtrB, même s'il fait partie de l'objet, car le type statique de oPtrB ne contient pas ce champs. Mais comme il est du type dynamique Object1, on peut référencer att à l'aide d'un gardien de type (type guard), qui vérifie le type dynamique et "fait comme si" le type statique était Object1. Si le gardien de type échoue, il en résulte une erreur à l'exécution. Pour l'éviter, on utilise un test de type :

  IF oPtrB IS Object1 THEN Proc(oPtrB(Object1).att) END;

A l'aide d'un test de type et d'un gardien de type, les assignations b) et d) deviennent possibles :

  IF oPtrB IS Object1 THEN oPtr1 := oPtrB(Object1) END;

On peut aussi utiliser un gardien de type régional, le test de type étant effectué à l'entrée dans l'instruction :

  WITH objB : ObjectDesc1 DO 
    obj1 := objB; 
  ELSE END;
Le message oPtrB.Operation entraîne l'exécution de la méthode Operation du type Object avant l'assignation a) ci-dessus, par contre il entraîne l'exécution de la méthode Operation du type Object1 après. Si après l'assignation on veut exécuter la méthode du type de base, il faut utiliser oPtrB.Operation^.

3 Structures de données génériques

L'utilisation de classes abstraites permet d'implémenter des structures de données génériques, c'est-à-dire qui ont le même comportement quelque soit le type d'objets qui les composent. A partir de la classe de base, on peut dériver selon les besoins n'importe quelles sous-classes. Par exemple, un arbre binaire contenant des chaînes de caractères a les mêmes algorithmes d'insertion, de recherche ou de mise à jour qu'un autre arbre contenant des entiers, des réels ou encore des structures plus complexes. L'avantage de définir un arbre de recherche générique est que pour tous les autres arbres, il n'est pas nécessaire de redéfinir les procédures de traitement. L'unique opération nécessaire pour traiter les noeuds d'un arbre binaire, et qui dépend des différents types d'objets contenu dans l'arbre, est la comparaison entre deux noeuds.

Pour tous les types, elle doit retourner 3 valeurs, selon que l'objet est égal, plus petit ou plus grand. C'est pourquoi elle est définie comme méthode abstraite (son implémentation est vide) attachée au type de noeud. Elle doit être redéfinie pour tout nouveau type, en tenant compte de ses champs propres.

  CONST LESS = -1; EQUAL = 0; GREATER = 1; 
  TYPE 
    Node* = POINTER TO NodeDesc; 
    NodeDesc* = RECORD 
      left,right: Node; 
    END;

    Tree* = POINTER TO TreeDesc; 
    TreeDesc* = RECORD 
      root: Node; 
    END;

  PROCEDURE (no: Node) Compare* (item: Node): INTEGER; 
  BEGIN (* méthode abstraite *) 
  END Compare;

Pour insérer un nouveau noeud dans l'arbre, la méthode Add procède la manière (non récursive) suivante : l'arbre est parcouru depuis la racine pour déterminer la place du nouvel objet. Comme il ne s'agit pas d'un arbre balancé, les nouveaux objets sont toujours insérés à la fin de la branche. La recherche de la place adéquate se fait à l'aide de la méthode d'ordonnancement de l'objet à insérer, item.Compare.

 PROCEDURE (t: Tree) Add* (item: Node); 
 VAR this,father: Node; result : INTEGER; 
 BEGIN 
  IF t.root # NIL THEN t.root := item; 
  ELSE 
    this := t.root; 
    WHILE this # NIL DO 
      father := this;.result := item.Compare(this); 
      IF result = EQUAL THEN.RETURN; (* noeud déjà inséré *) 
      ELSIF result = LESS THEN this := this.left; 
      ELSE this := this.right; 
      END; 
    END; 
    IF item.Compare(father) = LESS THEN.father.left := item; 
    ELSE father.right := thisItem; 
    END; 
  END; 
 END Add;

Pour gérer un arbre contenant des chaînes de caractères, il suffit d'étendre le type Node ci-dessus (en admettant qu'il soit déclaré dans le module Trees) avec le champ spécifique key :

 TYPE  
   Item* = POINTER TO ItemDesc; 
   ItemDesc* = RECORD(Trees.NodeDesc) 
     key * : STRING; 
   END;

La méthode Compare est redéfinie pour ce nouvel objet, c'est pourquoi le récepteur obj est du type étendu Item. Par contre, le paramètre item doit correspondre au paramètre de la méthode de base, donc du type de base Node. Mais pour qu'il puisse être comparé, son type dynamique doit être du type de l'objet étendu Item, c'est pourquoi on utilise un gardien de type. :

 PROCEDURE (obj: Item) Compare* (item:Trees.Node) : INTEGER; 
 BEGIN 
  WITH item: Item DO  (* gardien de type *) 
    IF obj.key < item.key THEN RETURN LESS; 
    ELSIF obj.key > item.key THEN RETURN GREATER; 
    ELSE RETURN EQUAL; 
    END; 
  ELSE error('wrong Type') ; 
  END; 
 END Compare;

Voici un exemple d'insertion dans l'arbre de ces nouveaux objets :

 VAR myTree : Trees.tree; obj : Item; i : INTEGER; 
 BEGIN 
  NEW(myTree); 
  myTree.NewTree; (* création de l'arbre *) 
  FOR i:= 1 TO 5 DO 
    NEW(obj); 
    IO.ReadString(obj.key);  
    myTree.Add(obj); 
  END; 
 END;

4 Structures de données hétérogènes

Le gestionnaire ci-dessus s'applique à des arbres homogènes, dont tous les noeuds contiennent le même type d'objets. Pour traiter des arbres hétérogènes, qui peuvent contenir des objets de types différents, il faut que tous les objets puissent être comparés : ils doivent tous posséder la même clé d'ordonnancement. Le type ItemDesc ci-dessus (en admettant qu'il est déclaré dans le module OrderedTrees) peut servir de type de base pour d'autre types : comme ils héritent tous de la même clé d'ordonnancement (attribut key), ils peuvent tous être insérés et traités dans le même arbre binaire :

 TYPE 
   Item1 = POINTER TO ItemDesc1; 
   ItemDesc1 = RECORD(OrderedTrees.ItemDesc) 
     elem : INTEGER; 
   END; 
   Item2 = POINTER TO ItemDesc2; 
   ItemDesc2 = RECORD(OrderedTrees.ItemDesc) 
     elem : CHAR; 
   END;

Une procédure d'écriture des objets, prend comme paramètre un objet de la classe de base, mais doit tenir compte des différents types dynamiques :

 PROCEDURE Write(VAR i : Trees.Node); 
 BEGIN 
  WITH  
     i : Item1 DO SWholeIO.WriteInt(i.elem,5); 
   | i : Item2 DO STextIO.WriteChar(i.elem); 
  ELSE END; 
 END Write;

Oberon-2 a hérité de Modula-2 le type PROCEDURE, qui permet de passer des procédures en paramètres à d'autres procédures :

 TYPE 
    procType* = PROCEDURE (n : Node);

 PROCEDURE (t: Tree) WalkTreeInOrder *(proc : ProcType); 
 END WalkTreeInOrder;

La méthode WalkTreeInOrder parcout l'arbre binaire et applique la procédure proc à chaque noeud, en commenûant par le noeud le plus à gauche. Pour écrire le contenu de l'arbre, il suffit de passer la procédure Write en paramètre à la méthode de parcours de l'arbre : suivant le type dynamique du noeud, l'une ou l'autre des variantes du gardien de type régional est vraie et la bonne procédure d'écriture est appelée :

  VAR mytree : Trees.Tree; 
  (* ... *) 
  mytree.WalkTreeInOrder(Write)

5 Persistance

Les objets persistants dénotent des objets qui survivent au programme qui les a créés. Une nouvelle exécution du même programme, ou d'un programme différent, les retrouve dans l'état où ils ont été laissés. Une possibilité d'implémentation est de stocker les objets dans un fichier. Mais le programme qui veut les récupérer ne connaît pas leur structure interne, du moins s'il s'agit d'objets génériques, il ne pourra donc pas les reconstruire uniquement sur la base de leur champs.

Pour résoudre ce problème, il faut aussi écrire dans le fichier le type de l'objet. Comme certaines informations sur les objets sont nécessaires à l'exécution, pour les tests et gardiens de types, l'appel des méthodes et la récupération de mémoire, chaque type enregistrement possède en mémoire un descripteur de type, qui est le même pour tous les objets d'une classe. Ce descripteur contient aussi le nom de la classe. Comme il doit être non ambigu, il est formé du nom du module dans lequel le type est déclaré suivi de l'identificateur de type .

Comme les noms de types peuvent prendre une place considérable, ils sont stockés dans le fichier dans une forme compressée [Möss93]. A chaque fichier est associée une table de longueur arbitraire (maxNames) dans laquelle la première occurence des noms de type est inscrite. L'index dans la table et le nom complet sont écrits dans le fichier. Pour les prochaines occurences, seul l'index est écrit. Le champ end indique le prochain index libre dans la table. La classe Stream est fournie avec le compilateur :

 TYPE 
   Stream* = RECORD 
     file : IOChan.ChanId; 
     tab  : ARRAY maxNames OF TypeName; (* Note 3 *) 
     end  : INTEGER; 
   END;

 PROCEDURE (VAR r: Stream) WriteString * (s: TypeName); 
 VAR i: INTEGER; 
 BEGIN 
  i:=0; 
  LOOP (* search s in r.tab *) 
    IF i=r.end THEN (* première occurence de s *) 
      RawIO.Write(r.file,i); RawIO.Write(r.file,s); 
      r.tab[r.end] := s; INC(r.end); EXIT 
    ELSIF s=r.tab[i] THEN         
      RawIO.Write(r.file,i); EXIT; 
    ELSE INC(i); 
    END; 
  END; 
 END WriteString;

 PROCEDURE (VAR r: Stream) ReadString * (VAR s: TypeName); 
 VAR i: INTEGER; 
 BEGIN 
  RawIO.Read(r.file,i); 
  IF i = r.end THEN (* le texte complet suit *) 
    RawIO.Read(r.file,s); 
    COPY(s,r.tab[end]); 
    INC(r.tab[end]); 
  ELSE COPY(r.tab[i],s); 
  END; 
 END ReadString;

La procédure de sauvegarde d'un objet écrit le nom du type (généré par la procédure TypeName du module Types := Objects_Types (Note 4)) puis l'objet lui-même dans un fichier. Le dernier objet est toujours un objet vide (NoName) pour faciliter la lecture, car ainsi le fichier n'a pas besoin de contenir une marque spéciale de fin de fichier :

 PROCEDURE WriteObj (VAR r: Stream; x: Object; VAR ok : BOOLEAN); 
 VAR module,name: TypeName; 
 BEGIN 
  IF x=NIL THEN r.WriteString(NoName); (*NoName = ""*) 
  ELSE  
    Types.TypeName(Types.TypeOf(x),module,name);  
    r.WriteString(module); 
    r.WriteString(name); 
    WriteNBytes(r.file, x, Types.SizeOf(x), ok); 
  END; 
 END WriteObj;

Pour charger un objet qui a été sauvé dans un fichier, le nom du type est d'abord reconstruit, puis l'objet est créé avec le type dynamique correspondant (généré par la procédure This du module Types, qui est le module de base implémentant la persistance et fourni avec le compilateur). Le dernier objet lu est un objet vide (NoName) :

 PROCEDURE ReadObj(VAR r:Stream;VAR x:Object;VAR ok:BOOLEAN); 
 VAR module,name: TypeName; y: Types.Object; 
 BEGIN 
  r.ReadString(module);  
  IF module="" THEN x:=NIL; ok:=TRUE; 
  ELSE r.ReadString(name); 
    Types.NewObj(y,Types.This(module,name)); 
    x:=y(Object); 
    ReadNBytes(r.file,x,Types.SizeOf(x), ok); 
  END; 
 END ReadObj;

Pour une description plus complète de la gestion des objets persistants, voir [Goeb93].

6 Gestionnaire d'arbres AVL

Nous allons voir les avantages d'Oberon-2 par rapport à un langage procédural à travers la réalisation d'un gestionnaire d'arbres AVL (balancés). Modula-2 ne supportant pas la généricité, elle est simulée avec les propriétés de bas niveau (ADDRESS, ADDADR, CAST et LOC ci-dessous sont importés du module SYSTEM). En Oberon-2, la généricité découle de l'extension de type.

6.1 Implémentation Modula-2

Dans la solution Modula-2 un noeud de l'arbre contient une adresse (champ info), ce qui permet de lui passer n'importe quel type pointeur. L'opération de comparaison, dont l'arbre a besoin comme critère d'ordonnancement, est passée comme paramètre procédure et stockée dans le noeud racine de l'arbre AVL :

 TYPE 
   OrderType = PROCEDURE(ADDRESS,ADDRESS): BOOLEAN; 
   TreeHeader = RECORD 
     adr   : ADDRESS; 
     equal : OrderType; 
     order : OrderType; 
     size  : CARDINAL; 
     root  : NodePointer; 
   END; 
   TreeNode = RECORD 
     info  : ADDRESS; 
     left  : NodePointer; 
     right : NodePointer; 
     bal   : [-1..1]; 
   END;

La procédure Define permet de définir les différents paramètres de l'arbre :

 PROCEDURE Define (VAR t : Tree; equal, order : OrderType; item : ARRAY OF BYTE); 
 BEGIN 
  NEW(t); 
  t^.adr  := ADR(t); 
  t^.root := NIL; 
  t^.equal := equal; 
  t^.order := order; 
  t^.size  := HIGH(item) + 1; 
 END Define;

Pour créer un nouveau noeud, la procédure CreateNode doit connaître la taille de l'élément contenu dans le noeud (paramètre size) ainsi que son adresse (item) pour réserver la place mémoire correspondante (avec la procédure ALLOCATE du module Storage). Les données sont ensuite copiées octet par octet :

 PROCEDURE CreateNode (size : CARDINAL; item : ADDRESS): NodePointer; 
 VAR newnode: NodePointer; 
  bytecount : CARDINAL; 
  from,to   : POINTER TO LOC; 
 BEGIN 
  NEW( newnode ); 
  newnode^.left  := NIL; 
  newnode^.right := NIL; 
  newnode^.bal   := 0; 
  ALLOCATE(newnode^.info, size); 
  from:=item; to:=newnode^.info; 
  FOR bytecount := 0 TO size - 1 DO 
    to^:=from^; to:= ADDADR( CAST(ADDRESS, to), 1 ); 
    from := ADDADR( CAST(ADDRESS, from), 1 ); 
  END; 
  RETURN newnode; 
 END CreateNode;

L'utilisation du type ADDRESS permet certes la généricité, mais par contre il empêche toute vérification de type entre les modules d'interface et d'implémentation du gestionnaire : l'utilisateur de ce module doit être très vigilant.

6.2 L'alternative Oberon-2

Une solution complète de ce problème est publiée dans [Dotz93]. Nous ne reprenons ici que les points illustrant les avantages de la programmation extensible.

Puisque la bibliothèque contient un module définissant le type Object et des procédures pour retrouver certaines informations sur le type dynamique (voir le chapitre 5 sur la persistance), un noeud de l'arbre contient un pointeur sur une extension de ce type de base. Il est vide pour que l'arbre soit générique :

 TYPE 
  Object* = POINTER TO ObjectDesc; 
  ObjectDesc* = RECORD (Types.ObjectDesc) END; 
  Type* = Types.Type;

  NodePtr = POINTER TO TreeNode; 
  TreeNode = RECORD 
    left: NodePtr; 
    right: NodePtr; 
    bal : INTEGER ; (* Note 2 *) 
    info : Object; 
  END;

  Tree* = POINTER TO TreeDesc; 
  TreeDesc = RECORD 
    root: NodePtr; 
    objType: Type; 
  END;

Les opérations de comparaison, dont l'arbre a besoin comme critère d'ordonnancement, sont déclarées comme méthodes abstraites de l'objet :

 PROCEDURE (info : Object) Order* (item : Object): BOOLEAN; 
 BEGIN 
  HALT(20); 
 END Order;

 PROCEDURE (info : Object) Equal* (item : Object): BOOLEAN; 
 BEGIN 
  HALT(20); 
 END Equal;

Le champ objType permet de contraindre l'arbre à ne traiter que des objets d'un même type, qui est passé en paramètre à la procédure Define :

 PROCEDURE Define* (VAR t : Tree; type: Type); 
 BEGIN 
  NEW(t); 
  t.Init(type); (* initialise les champs de t *) 
 END Define;

La fonction CreateNode devient plus simple et plus claire :

 PROCEDURE CreateNode (item : Object) : NodePtr; 
 VAR newnode : NodePtr; 
 BEGIN 
  NEW(newnode); 
  newnode^.left := NIL; newnode^.right := NIL; 
  newnode^.bal := 0;                            
  newnode^.info := item; 
  RETURN newnode; 
 END CreateNode;

Les méthodes de sauvegarde et de restoration de l'arbre sont complètement génériques, elles n'ont pas besoin d'être redéfinies pour les extensions de la classe Tree. L'arbre doit exister avant de pouvoir sauver ses éléments ou les récupérer (dans le cas contraire, la variable globale ok prend la valeur FALSE).

La méthode Save sauvegarde les éléments de l'arbre dans le fichier donné en paramètre depuis l'élément le plus à gauche jusqu'à l'élément tout à droite au moyen de la procédure récursive VisitSave. L'écriture proprement dite est effectuée à l'aide de la procédure WriteObj vue au chapitre 5 :

 PROCEDURE (t : Tree) Save* (fn: ARRAY OF CHAR); 
 VAR  s: Stream; 
   ores: StreamFile.OpenResults; 

  PROCEDURE VisitSave( p: NodePtr ); 
  VAR ok : BOOLEAN; 
  BEGIN 
  IF p # NIL THEN 
    VisitSave( p^.left ); 
    WriteObj(s,p.info,ok); 
    IF ~ ok THEN error(writeerror ); 
    END; 
    VisitSave( p^.right ); 
  END; 
  END VisitSave;

 BEGIN 
  InitStream(s); 
  ok := FALSE; 
  IF ( t # NIL ) THEN 
    IF ~ t.Empty() THEN 
      StreamFile.OpenWrite(s.file, fn, StreamFile.raw, ores ); 
      IF ores = StreamFile.opened THEN 
        VisitSave( t^.root ); 
        WriteObj(s,NIL,ok); (* eof *) 
        StreamFile.Close( s.file ); 
      END; 
    END; 
  END; 
 END Save;

La méthode Load récupère les objets du fichier passé en paramètre. La lecture est effectuée à l'aide de la procédure ReadObj vue au chapitre 5 :

 PROCEDURE (t : Tree) Load* (fn: ARRAY OF CHAR); 
 VAR new: Object; 
   s: Stream; 
   ores: StreamFile.OpenResults; 
 BEGIN 
  InitStream(s); 
  ok := FALSE; 
  IF ( t # NIL ) THEN 
    t.MakeEmpty;(* vide l'arbre *) 
    StreamFile.OpenRead( s.file, fn, StreamFile.raw, ores ); 
    IF ores = StreamFile.opened THEN 
      LOOP 
        ReadObj(s,new,ok); 
        IF new = NIL THEN EXIT; 
        ELSIF ok THEN t.Insert( new ); 
        ELSE error( readerror ); 
        END; 
      END; 
      StreamFile.Close( s.file ); 
    ELSE error( readerror ); 
    END; 
  END; 
 END Load;

Pour terminer, voici un exemple d'arbre AVL, qui étend la classe de base Tree avec des éléments de type REAL. Les méthodes Order et Equal ont été redéfinies pour tenir compte du nouvel attribut. La procédure PrintOutRec permet d'écrire les nombres stockés dans l'arbre. Elle est passée en paramètre à la méthode Visit de parcours de l'arbre (voir le chapitre 4) :

 MODULE Exemple; 
 IMPORT AVL, SRealIO;

 CONST 
   max=1000; 
   datafile="x.dat"; 
 TYPE 
   Object = POINTER TO ObjectDesc; 
   ObjectDesc = RECORD (AVL.ObjectDesc) 
      pu: REAL; 
   END;

 VAR  myobj, out: Object; 
   mytype: AVL.Type;                                   
   myTree: AVL.Tree; 
   i: INTEGER;

 PROCEDURE PrintOutRec(VAR obj: AVL.Object); 
 BEGIN 
  WITH obj : Object DO 
    SRealIO.WriteReal(obj^.pu, 13); 
  ELSE HALT(20); 
  END; 
 END PrintOutRec;

 PROCEDURE (obj1 : Object) Equal* (obj2 : AVL.Object) :  
 BOOLEAN; 
 BEGIN 
  WITH obj2 : Object DO 
    RETURN obj1^.pu = obj2^.pu; 
  END; 
 END Equal;

 PROCEDURE (obj1 : Object) Order (obj2 : AVL.Object) : BOOLEAN; 
 BEGIN 
  WITH obj2 : Object DO 
    RETURN obj1^.pu < obj2^.pu; 
  END; 
 END Order;

 BEGIN 
  NEW(myobj); 
  mytype := AVL.TypeOf(myobj); 
  AVL.Define(mytree, mytype); 
  FOR i:=max TO 0 BY -1 DO 
    NEW(out); out^.pu:=i; tree.Insert(out); 
  END; 
  mytree.Visit(PrintOutRec);  
  mytree.Save(datafile); 
  mytree.MakeEmpty; 
  mytree.Load(datafile); 
  IF mytree.type() = mytype THEN 
    mytree.Visit(PrintOutRec); 
  ELSE HALT(20); 
  END; 
 END Exemple;

7 Conclusion

Nous avons vu tout au long de cet article les propriétés d'Oberon-2 qui en font un langage de programmation orienté objet simple, clair et concis. Seul l'essentiel est inclus dans le langage, comme l'extension de type et les procédures liées au type, qui permettent de dériver les autres propriétés d'un langage OOP, l'héritage, le polymorphisme et la persistance par exemple. Un exemple pratique comme un gestionnaire d'arbres AVL a montré la simplicité d'écriture de structures de données génériques par rapport à un langage de programmation traditionnel. Non seulement la clarté des concepts d'Oberon-2 en font un langage destiné à l'enseignement de la programmation de haut niveau structurée et/ou orientée objet, mais sa puissance et sa sûreté en font un langage bien adapté au développement d'applications professionnelles conséquentes.

Bibliographie

[Goeb93] Hartmut Goebel, Günter Dotzel : Persistent Objects in Oberon-2, The ModulaTor, Vol. 3, Nr. 1, ModulaWare, Feb-1993 (2nd ed. Nov-94)

[ISO94] IEEE/IEC : ISO 10154 Modula-2, 1994

[Möss93] Mössenböck, Hanspeter: Object-Oriented Programming in Oberon-2, Springer-Verlag Berlin Heidelberg, 1993

[Temp93] Templ, Josef : A systematic approach to multiple inheritance implementation, in ACM SIGPLAN, Apr-1993

[Temp94] Templ, Josef : Oberon vs C++, The ModulaTor, Vol. 4, Nr. 9, ModulaWare, Oct-1994.

[Wirt92] Reiser, Martin; Wirth, Niklaus: Programming in Oberon. Addison Wesley, 1992.


Notes

Note 1: En Oberon-2, le signe "*" caractérise des constantes, variables, types ou procédures exportées.

Note 2: Le champ de balance (bal) ne peut être déclaré que comme INTEGER car Oberon n'a pas de type énumération.

Note 3: En Oberon-2, on pourrait utiliser un tableau ouvert dynamique : POINTER TO ARRAY OF TypeName

Note 4: Des routines pour dériver un type à partir d'un nom de type, et inversément, sont fournies avec le compilateur:


DEFINITION Objects_Types;

IMPORT SYSTEM;

  CONST 
    QuadwordSize = 8; 
    adrSize = 8; 
    maxIdentLen = 32; 
    tagSize = 8;

  TYPE 
    ADDRESS_64 = SYSTEM.SIGNED_64;

    Name = ARRAY 32 OF CHAR;

    NamePtr = POINTER TO RECORD 
      name: Name; 
    END ;

    Object = POINTER TO ObjectDesc; 
    ObjectDesc = RECORD END ;

    Size = SYSTEM.SIGNED_64;

    Tag = SYSTEM.SIGNED_64;

    Type = POINTER TO TypeDesc; 
    TypeDesc = RECORD 
      module: NamePtr; 
      name: Name; 
    END ;

  VAR 
    Modules-: ARRAY 256 OF ModEntryDesc;

  PROCEDURE DisposeArray (VAR o: SYSTEM.PTR); 
  PROCEDURE DisposeDynArray (VAR o: SYSTEM.PTR; obolete1, obsolete2: LONGINT); 
  PROCEDURE DisposeObj (VAR o: Object); 
  PROCEDURE NewObj (VAR o: Object; t: Type); 
  PROCEDURE SizeOf (o: Object): LONGINT; 
  PROCEDURE StoreModObjects (typeDescBase: TypesArray); 
  PROCEDURE This (module, name: ARRAY OF CHAR): Type; 
  PROCEDURE TypeName (typ: Type; VAR module, name: ARRAY OF CHAR); 
  PROCEDURE TypeOf (o: Object): Type;

END Objects_Types.


This article is a revised edition of "Extensibilité et persistance" published in L'objet, SOL, Paris, Numéro 2, Juin 1995
This Page is http://www.modulaware.com/objetpap.htm was last in revised 24-Jun-1998.

[ Home | Site_index | Legal | OpenVMS_compiler | Alpha_Oberon_System | ModulaTor | Bibliography | Oberon[-2]_links | Modula-2_links | General interesting book recommendations ]

Books Music Video Enter keywords...


Amazon.com logo
Webdesign by www.otolo.com/webworx, 21-Nov-1998