3 O2 : le modèle
Cette section contient tout ce qui concerne la définition d'un
schéma. L'implantation ave O2C fait l'objet de la section
suivante.
3.1 Rappels sur les principaux concepts de l'orienté-objet
La structure de base utilisée pour modéliser l'information est l'objet, équivalent pour les SGBDOO de la table dans les SGBDR ou
de l'enregistrement dans les systèmes de fichiers.
Les différences essentielles entre le modèle orienté objet et les
autres (modèle relationnel, modèle réseau, ...) sont les suivantes:
-
Un objet comprend à la fois des données et les modules (ou
méthodes) qui agissent sur ces données. De plus tout accès ou
manipulation de l'information contenue dans un objet doit en
principe se faire par les méthodes. En d'autres termes l'entité qui
communique avec un objet ne connaît pas la structuration interne des
données de l'objet auquel il s'adresse. On désigne par encapsulation cette technique de protection du contenu d'un
objet.
- Les types des attributs peuvent être d'une structuration
beaucoup plus complexe que celle qu'on trouve dans le modèle
relationnel. De plus un objet peut contenir des attributs
multimédia: image, son, etc... La définition du contenu d'un
objet est une classe. Elle comprend un type (décrivant la
structuration des attributs de l'objet) et des méthodes agissant sur
ce type. Les ensembles d'objets ayant la même structure sont donc
groupés en classes.
- Les classes peuvent avoir des sous-classes définies par un
procédé qu'on appelle l'héritage. Une sous-classe reprend la
définition de sa super-classe et y ajoute des attributs et des
méthodes. Une sous-classe peut également redéfinir les méthodes de
sa super-classe.
- Un objet a une identité, indépendante des valeurs qu'il
contient. Cette notion d'identité est analogue à celle de clé
primaire dans le modèle relationnel. Les deux différences
essentielles sont que
-
le concepteur de la base n'a pas à se soucier de définir
cette clé primaire par un ensemble d'attributs.
- L'identité d'un objet n'est porteuse d'aucune autre
information que celle pour laquelle elle est conçue:
permettre de faire référence à un objet.
En résumé: Un objet est construit par référence à une classe. Les
classes sont définies par un type et des méthodes. Nous allons voir
successivement comment définir un type et une méthode dans O2.
3.2 Types
3.2.1 Définition d'un type
Un type est constitué de types dits "atomiques" auxquels on applique
récursivement des constructeurs. Dans l'ordre, cette partie présente
donc: les types atomiques, les constructeurs, et les types O2. On
trouvera ces informations, avec plus de détails, dans la documentation
O2. Cependant l'ordre de la présentation a été légèrement changé.
3.2.2 Types "atomiques"
Il y en a six :
-
integer
- char
- boolean (ne prend que les valeurs false ou true).
- real
- string (chaîne de caractères).
- bits (chaîne d'octets: peut contenir des caractères
non-ascii).
Comme on le voit, ces types de base correspondent globalement à ce
qu'offrent les SGBD relationnels. L'intérêt du SGBDOO est d'offrir des
constructeurs pour définir des types structurés de façon
complexe.
3.2.3 Constructeurs de types
Il y en a trois:
-
Le constructeur tuple : Il permet de regrouper des attributs de
types distincts, en leur associant un identifiant. Un tuple est
équivalent à la notion d'enregistrement, telle qu'on la trouve par
exemple dans les langages de programmation ou dans le modèle réseau.
Les attributs contenus dans un tuple peuvent être de n'importe quel
type: atomique, structuré, ou classe. Exemple:
type tuple (nom : string,
adresse : tuple (rue : string,
numéro : integer,
ville : string),
Marie_a : Personne)
- Le constructeur set : Il permet de construire un ensemble
constitué d'un nombre indéfini d'éléments du même type. Par défaut
un "set" peut contenir deux fois le même élément. Ce n'est pas le
cas pour les "unique set". Dans un "set", l'ordre des éléments est
indifférent. Exemple :
type tuple (nom : string,
adresse : tuple (rue : string,
numéro : integer,
ville : string),
Marie_a : Personne,
enfants : set(Personne))
- Le constructeur list : Il permet de construire une liste
constituée d'un nombre indéfini d'éléments du même type. Cette fois
l'ordre des éléments est important.
Un type est la définition d'une structure de données bâtie avec les
éléments ci-dessus. Il est donc structuré à l'aide des opérateurs
tuple, set et list, et contient des attributs qui sont
-
Soit des types atomiques
- Soit des types structurés
- Soit des classes d'objet.
Si par exemple une classe bitmap est définie pour contenir une image,
la référence à cette classe dans un type permettra donc d'associer des
données classiques (string, integer) à des données multimédia (image).
On peut définir des types dans O2 par la commande create type. Il faut
bien distinguer le type de la classe, la différence principale étant
qu'un type n'a pas de méthode. Une instance d'un type est un ensemble
de valeurs correspondant aux spécifications du type.
(1, 5, 9) est une instance du type set(integer). ("Dupont", 28,
"Français"), est une instance du type tuple (nom: string, age:
integer, nationalité: string).
Quelques mots sur les sous-types
Un type peut avoir des sous-types, selon la définition suivante:
Un type a est sous-type d'un type b si et seulement si toute instance
de a peut devenir instance de b.
Ce qui revient à dire que a contient plus d'information que b.
Selon le constructeur utilisé, on peut avoir les sous-types suivants:
-
Constructeur tuple: T1 est sous-type de T2 si T1 contient tous
les attributs de T2 et si, pour chaque attribut commun, le type de
l'attribut de T1 est sous-type de l'attribut de T2.
- Constructeur set:si T1 est sous-type de T2, set(T1) est
sous-type de set(T2).
- Constructeur list:si T1 est sous-type de T2, list(T1) est
sous-type de list(T2).
Un type est une définition. L'instanciation d'un type donne selon la
terminologie O2 une valeur, c'est à dire un ensemble de données
structurées.
Il faut bien distinguer type et classe, valeur et objet. Entre le
couple (type, valeur) et le couple (classe, objet), la différence
tient à la présence ou non de méthodes définissant les opérations
applicables à une donnée structurée.
Nous allons donc voir maintenant comment définir des méthodes.
3.3 Méthodes
Une méthode est un module qui implémente une partie du comportement
d'un objet. Les méthodes sont définies au niveau d'une classe, et la
même méthode s'applique à l'ensemble des objets de cette classe. Il
n'est pas possible d'avoir une méthode particulière à un objet.
Il faut bien distinguer la définition d'une méthode et son
implémentation. Quand on définit une méthode, on se contente de
décrire le type des données en sortie et en entrée, sans rien indiquer
sur la façon dont la méthode fonctionne. Le programmeur qui utilise
une classe ne connaît donc que l'appel de la méthode, qu'on désigne
sous le terme signature.
Le fonctionnement est écrit à part à l'aide du langage O2C, qui est
une extension du langage C. De cette façon, on obtient la
programmation modulaire qui une des qualités recherchées dans un SGBD
OO.
Définition d'une méthode:
Une méthode est définie par
-
Son nom
- Son type d'accès, public ou privé
- Sa signature
- Le type de l'objet ou de la valeur ramenée
Si une méthode est privée, elle ne peut être utilisée que par les
autres méthodes de la classe. Une méthode publique peut communiquer
avec une entité extérieure.
La signature est la liste des paramètres de la méthode, avec leur
type. Ce type peut être un type atomique, un type structuré, ou une
classe. Dans ce dernier cas c'est un identificateur d'objet qui sera
passé en paramètre. De plus toute méthode reçoit au moins un paramètre
implicite: l'identificateur de l'objet à partir duquel la méthode a
été appelée. Cet identificateur est désigné par self.
Enfin, une méthode retourne à l'entité appelante une donnée dont le
type est spécifié au moment de la définition de la méthode. Cette
donnée pourra être un objet ou une valeur.
Exemple d'une méthode de la classe Personne, qui teste si une personne
passée en paramètre est parente de l'objet propriétaire:
method public est_parent_de (personne: Personne): boolean;
Dans l'implémentation en O2C, une comparaison sera faite entre la
variable personne et la variable self.
3.3.1 Séparation définition/implémentation
L'explication de cette séparation définition/implémentation est le
respect du principe d'encapsulation. Le contenu d'un objet, que ce
soit la structure des données ou le fonctionnement d'une méthode, doit
rester caché à l'environnement extérieur. Il est donc seulement
nécessaire de connaître le mode de communication avec un objet, c'est
à dire le nom des méthodes, leur signature et le type de l'information
retournée.
3.3.2 Envoi de messages
En orienté-objet, on ne dit pas
"appeler la méthode chose pour l'objet untel " mais "envoyer le
message chose à l'objet untel". La justification de ce jargon est
qu'en raison de l'encapsulation et de l'héritage, la procédure appelée
quand on déclenche une méthode peut changer d'un objet à l'autre.
C'est l'objet qui décide en fait du module à exécuter quand il reçoit
un message. On utilise donc l'expression "envoi de message" pour
éviter l'analogie trompeuse avec "appel de procédure".
Nous savons maintenant définir une classe.
3.4 Classes
3.4.1 Définition d'une classe
La spécification d'une classe comprend quatre parties:
-
Le nom de la classe. Par convention, il commence par une
majuscule.
- La (ou les) classe(s) dont hérite la classe courante. Cet aspect
sera abordé plus loin.
- Le type de la classe. Tous les objets de la classe auront une
information structurée selon ce type.
- La liste des méthodes de la classe.
Exemple de création d'une classe:
class Person
type tuple (nom : string,
prenom : string,
age : integer,
photo : Bitmap,
enfants : list (Person),
salaire : real)
method public maj_salaire , ajout_enfant (enfant: Person)
end;
Ni le type, ni les méthodes ne sont obligatoires.
3.4.2 Propriétés d'une classe
Les attributs et les méthodes sont les propriétés de la classe. Chaque
propriété peut être publique ou privée. Les propriétés privées ne sont
pas accessibles par l'environnement d'un objet de la classe.
Par défaut une propriété est privée. Dans la classe donnée en exemple
ci-dessus, le type est privé. Il est donc impossible d'y accéder
excepté dans une méthode de la classe elle-même.
La méthode ajout_enfant est également privée.
3.4.3 Réutilisabilité
O2 essaie autant que possible d'unifier la syntaxe relative aux
attributs et aux méthodes. Ainsi, si Joe est une référence vers un
objet de la classe Person, l'accès à l'attribut salaire se fait par:
Joe->salaire
et l'appel de la méthode maj_salaire se fait par
Joe->maj_salaire
Pour l'utilisateur extérieur, il est impossible de distinguer, dans ce
cas, ce qui est attribut de ce qui est méthode. Dans la réalité, la
méthode maj_salaire contient certainement des paramètres et la
différence avec un attribut apparaît.
Cette tentative d'unification illustre un des bénéfices attendus de
l'encapsulation: l'évolutivité et la réutilisabilité.
Dans l'exemple ci-dessus, supposons qu'on ait d'abord stocké le
salaire d'une personne sous forme d'un attribut de type real. Les
inconvénients de ce choix apparaissent si il s'avère que la salaire
est en fait la somme de plusieurs attributs (salaire de base +
primes), affectés d'un coefficient (ancienneté). La correction
consiste alors à remplacer l'attribut salaire par une méthode de même
nom qui retournera le salaire de la personne en fonction des autres
attributs. L'appel n'ayant pas changé, tous les programmes utilisant
le salaire d'un individu n'ont pas besoin d'être modifiés.
3.4.4 Compléments sur les classes
Dans O2, toutes les classes héritent implicitement de la classe
Object, et donc des méthodes fournies par le système O2 qui sont
toutes attachées à la classe Object. Il s'agit notamment des méthodes
O2Look qui permettent d'afficher et de manipuler graphiquement des
objets.
De plus, toute classe a au moins une méthode init qui permet
d'initialiser la valeur d'un objet au moment de sa création. Cette
méthode peut être redéfinie.
Une instanciation de la classe est un objet dont le contenu et le
comportement sont définis dans la classe.
3.5 Sous-classes
3.5.1 Définition d'une sous-classe
Une sous-classe apporte des informations supplémentaires par rapport à
la classe dont elle hérite. Ces informations supplémentaires peuvent
être:
-
L'ajout de nouveaux attributs ou la redéfinition des anciens.
- L'ajout de nouvelles méthodes ou la redéfinition des anciennes.
Dans l'exemple donné précédemment, on n'aurait certainement pas fait
figurer un attribut "salaire" dans la classe Personne. L'ensemble des
salariés étant un sous-ensemble de l'ensemble des personnes, voici
comment on peut rendre compte de cette situation dans O2:
class Person
type tuple (nom : string,
prenom : string,
age : integer,
photo : Bitmap,
enfants : list (Person))
method ajout_enfant (enfant: Person)
end;
class Employé inherit Person
type tuple (poste : string,
salaire : real)
method maj_salaire
end;
Toutes les propriétés (attributs et méthodes) de la classe Person
existent toujours dans les objets de la classe Employé. On aurait pu
ne rajouter que des attributs, ou que des méthodes.
Toute classe dans O2 dérive de la classe de plus haut niveau Object.
Dans l'exemple ci-dessus, on a ajouté des attributs et des méthodes.
Mais on peut aussi redéfinir ceux qui existaient déjà dans la
super-classe. Cette redéfinition des propriétés est expliquée
ci-dessous:
3.5.2 Redéfinition d'un attribut
Si un attribut apparaît dans la définition d'une sous-classe avec le
même nom qu'un attribut existant déjà dans la super-classe, ce dernier
est redéfini. Seule condition: le type du nouvel attribut doit être un
sous-type du type de l'ancien attribut.
3.5.3 Redéfinition d'une méthode
On peut rédéfinir une méthode, la seule condition étant que les
signatures soient compatibles
-
Les deux méthodes doivent avoir le même nombre de paramètres.
- Chaque paramètre de la nouvelle méthode doit être un sous-type
du paramètre correspondant dans l'ancienne.
- La valeur retournée dans la nouvelle méthode doit être un
sous-type de la valeur retournée dans l'ancienne.
3.5.4 Résolution tardive
La redéfinition d'une méthode donne lieu à la résolution tardive.
Supposons qu'une routine soit définie pour agir sur des objets de la
classe Person. Elle pourra aussi bien, pendant l'exécution, agir sur
des objets de la classe Employé puisque ces derniers ont les mêmes
propriétés que la classe Person. On a vu d'ailleurs que les méthodes
de la classe Person étaient héritées par la classe Employé.
Si une méthode a été redéfinie sur la classe Employé, ce n'est qu'au
moment de l'exécution que l'on pourra déterminer quelle méthode
utiliser: celle de la classe Employé ou celle de la classe Person.
D'où le terme de résolution tardive.
Quand on "envoie un message" un objet, il s'agit donc d'une opération
plus complexe que d'appeler une routine. Il faut
-
Rechercher la classe de cet objet
- Situer cette classe au sein d'une hiérarchie d'héritage.
- Rechercher dans cette hiérarchie, en partant d'en bas, la
première méthode portant le même nom que le "message" reçu.
- Exécuter cette méthode.
La résolution tardive peut mener à une dégradation des performances.
3.6 Objets
Un objet est à sa classe ce qu'une valeur est à son type: la
réalisation concrète d'une définition abstraite.
Il faut bien distinguer un objet d'une valeur. Un objet encapsule une
valeur, c'est à dire qu'en plus de contenir un ensemble structuré de
données, il impose des règles d'accès (les méthodes) limitant les
possibilités de manipulation de ces données.
Un objet est donc plus qu'une valeur. De fait un objet comprend les
trois éléments suivants:
-
Son identité: information non accessible qu'on peut comparer au
pointeur dans un langage de programmation. Toute référence à un
objet doit passer par son identité. De plus l'identité d'un objet
est indépendante de la valeur de cet objet.
- Sa valeur: ensemble de données structurée selon le type de la
classe à laquelle appartient l'objet.
- Ses méthodes : elles définissent le comportement de l'objet.
Il faut noter que ce qui distingue entre eux les objets d'une classe,
c'est l'identité, qui est unique pour chaque objet, et la valeur. Par
contre les méthodes sont communes à l'ensemble des objets d'une
classe: il ne peut pas y avoir de méthode propre à un objet.
Pour obtenir la valeur d'un objet, on utilise l'opérateur *. C'est
l'opérateur de désencapsulation. Soit par exemple la classe suivante:
class Person
type tuple (nom: string, age: integer).
end;
Si Joe est une variable représentant un objet de cette classe, *Joe
est une valeur de type tuple (nom: string, age: integer).
En principe la valeur d'un objet est encapsulée et on n'y accède que
dans l'un des deux cas suivants:
-
Les méthodes de la classe ont toujours accès aux valeurs des
objets. La manipulation des valeurs par des méthodes est la voie
normale pour le traitement de l'information dans les SGBD OO.
- Certains attributs peuvent être déclarés publics. On y accède
alors par l'opérateur *, ou l'équivalent ->. Par exemple:
*(Joe).nom
Joe->nom.
Les deux expressions ci-dessus sont équivalentes. Elle sont
directement inspirées des opérateurs du langage C.
La création d'un objet se fait par l'opérateur new:
Joe = new Person
Cette instruction revient à appeler la méthode init qui initialise la
valeur de l'objet. Par défaut, l'initialisation se fait de la façon
suivante:
-
Les entiers et réels = 0
- Les chaînes de caractères = "NULL string"
- Les Booléens = false
- Référence à un objet = nil
- Liste ou ensemble = liste ou ensemble vide.
La méthode init peut être redéfinie pour chaque classe de façon à
changer ces initialisations par défaut.
3.7 Le schéma
Toutes les définitions de classes constituant une base de données sont
regroupées dans une entité appelée schéma. Un schéma est créé par la
commande:
create schema nom_schema.
ATTENTION: Dans les version >= 4.6 de O2 il faut exécuter
status TEST in base <nom_base>
dans la fenêtre O2Shell pour pouvoir modifier le schéma ensuite.
Il peut ensuite y avoir plusieurs bases par schémas. Toutes les bases
rattachées à un même schéma auront donc en commun les mêmes classes
Une base est créée par la commande:
create base nom_base
Quand on lance une session O2, on définit le schéma et la base dans
laquelle on souhaite travailler:
set schema nom_schema
set base nom_base
Toutes les commandes suivantes se feront dans le cadre de la base et
du schéma choisi. Les commandes de création de classe viendront
notamment augmenter le schéma.
En plus des définitions de classes, un schéma contient les éléments
suivants: objets et valeurs nommés, transactions, applications.
Objets et valeurs nommés :
La seule façon d'assurer
la persistance des informations dans O2 est de les rattacher à un nom.
Un nom est en fait un identifiant permanent géré par le SGBD. Un nom
peut faire référence à des objets ou à des valeurs. Exemple:
create name Population: set (Person)
create name pi: real
La gestion de la persistance d'un ensemble d'objets en les rattachant
à un nom est la manière courante de stocker des objets dans O2. On
peut effectuer les opérations suivantes sur le nom Population:
-
Ajout d'une personne X: Population += set(X)
- Retrait d'une personne Y: Population -= set(X)
- Réinitialisation: Population = set().
3.8 O2C
Le langage O2C est une extension du langage C. Les ajouts concernent
principalement:
-
La possibilité d'utiliser les types définis dans O2.
- La manipulation de valeurs ou d'objets O2.
- L'appel de méthodes.
- La gestion des transactions.
Pour les deux dernières, voir la documentation O2.
Voici brièvement une description de la gestion des objets en O2C.
Utilisation des types O2 :
La déclaration d'une variable
dont le type est défini par O2 doit être préfixée par le mot réservé
o2. La syntaxe est
o2 type_spec nom_variable [= valeur_initiale].
Exemples:
o2 set(integer) tierce_gagnant = (1, 12, 4);
o2 Person Joe, Tom;
o2 tuple (nom: string, age: integer);
Manipulation d'objets et de valeurs
Les types integer, real et char se manipulent comme leurs équivalents
int, double et char en C.
Par contre les variables de type string ne sont pas des variables C de
type char *. O2 définit un ensemble d'opérateurs sur le type string.
Les variables de type boolean peuvent être employées dans des
expressions conditionnelles du C. Elles prennent les valeurs false ou
true.
Les variables de type tuple sont équivalentes aux variables de type
struct en C.
Les variables qui font référence à un objet sont équivalentes à des
pointeurs vers des structures en C.
Pour les variables de type set et de type list, O2 redéfinit un
ensemble d'opérateurs.