next up previous
Previous: No Title

Sous-sections

Les objets d'O'Caml : partie I




Objectifs :Présentation du noyau objet du langage O'CAML : classes, héritage, classes et méthodes abstraites

Classes et Objets

L'extension objet d'O'Caml s'intègre au noyau fonctionnel et au noyau impératif du langage, et aussi à son système de types. C'est ce dernier point qui en fait son originalité. On obtient ainsi un langage objet, typé statiquement, avec inférence de types. Cette extension permet de définir des classes et des instances, autorise l'héritage entre classes y compris multiple, accepte les classes paramétrées et les classes abstraites. Les interfaces de classes sont engendrées par leur définition mais peuvent être précisées par une signature de modules.

classes

On appelle classe la description du regroupement des données et des procédures (méthodes) qui les manipulent.

On définit l'inévitable classe point qui contient deux champs de données (ou variables d'instance), et six champs de méthodes (ou méthodes d'instance) : deux méthodes d'accès aux champs de données, deux procédures de déplacement absolu et relatif d'un point, un affichage et une fonction de calcul de distance par rapport à l'origine. Cette définition de la classe point possède des méthodes effectuant des modifications physiques des champs de données.

class point (x_init,y_init) = 
object  
  val mutable x = x_init
  val mutable y = y_init

  method get_x = x
  method get_y = y
  method moveto (a,b) = begin x <- a; y <- b end
  method rmoveto (dx,dy) = begin x <- x + dx; y <- y + dy end
  method print () =
    begin
      print_string "( ";
      print_int x;
      print_string " , ";
      print_int y;
      print_string ")";
     end

  method distance () = sqrt(float(x*x + y*y))

end;;

Le système infère l'interface de la classe :

class point :
  int * int ->
  object
    val mutable x : int
    val mutable y : int
    method distance : unit -> float
    method get_x : int
    method get_y : int
    method moveto : int * int -> unit
    method print : unit -> unit
    method rmoveto : int * int -> unit
  end

instances

Un objet est une valeur d'une classe, appelée instance de cette classe. Cette instance est créée par le constructeur d'objets new à qui on indique la classe et les valeurs d'initialisation.

# let p1 = new point (0,0);;
val p1 : point = <obj>
# let p2 = new point (3,4);;
val p2 : point = <obj>

Le type inféré pour les instances p1 et p2 est le type objet (<obj> point). C'est une abréviation du type objet long suivant :

  point =
    < distance : unit -> float; get_x : int; get_y : int;
      moveto : int * int -> unit; print : unit -> unit;
      rmoveto : int * int -> unit >
contenant les méthodes et leur type.

envoi de message

L'envoi d'un message à un objet s'effectue par la notation # (la notation point étant déjà utilisée pour les records et les modules). Ce message correspond à une méthode définie dans la classe de l'objet. L'exemple suivant montre différentes requêtes effectuées sur des objets de la classe point.

# p1#get_x;;
- : int = 0
# p2#get_y;;
- : int = 4
# p1#affiche();;
( 0 , 0)- : unit = ()
# p2#print();;
( 3 , 4)- : unit = ()
# if (p1#distance()) = (p2#distance())
then print_string ("c'est le hasard\n")
else print_string ("on pouvait parier\n");;    
on pouvait parier
- : unit = ()

Du point de vue des types, les objets de type point peuvent être manipulés par les fonctions polymorphes d'O'Caml :

# p1 = p1 ;;
- : bool = true
# p1 == p1;;
- : bool = true
# p1 = p2;;
- : bool = false
# let p3 = new point (0,0);;
val p3 : point = <obj>
# p1 = p3;;
- : bool = true
# p1 == p3;;
- : bool = false

comme n'importe quelle valeur du langage.

Agrégat

Une classe peut contenir des champs de données (ou variables d'instance) appartenant à d'autres classes. C'est la relation "Has-a" (ou "A-un") entre deux classes. Elle est notée par une simple flèche entre les 2 classes dans le sens "C1 Has-a C2" : C1 --> C2. Si C1 a de 0 à n champs de C2 on notera : C1 <>---> C2 la relation.

L'exemple suivant définit une classe picture contenant un tableau de point

class picture n =
object
  val mutable ind = 0
  val tab = Array.create n (new point(0,0))

  method add p = if (ind < n -1) then
                 begin
                   tab.(ind)<-p;
                   ind <- ind + 1
                 end
                 else failwith ("picture.add : ind = "^(string_of_int ind))

  method remove () = if (ind > 0) then ind <-ind-1

  method print () = for i=0 to ind do tab.(i)#print() done
end;;

Le système infère l'interface de la classe :

  class picture :
  int ->
  object
    val mutable ind : int
    val tab : point array
    method add : point -> unit
    method print : unit -> unit
    method remove : unit -> unit
  end

Le champs tab possède le type point array correspondant à un tableau de points.

Héritage

C'est l'avantage majeur de la programmation objet que de pouvoir étendre le comportement d'une classe existante tout en continuant à utiliser le code écrit par la classe originale. Quand on étend une classe, la nouvelle classe hérite de tous les champs, de données et de méthodes, de la classe qu'elle étend.

C'est la relation "Is-a" (ou "Est-un") entre 2 classes. Elle est notée par une flèche remplie entre la sous-classe et la classe ancêtre.

héritage d'une classe

Voici une extension de la classe point qui hérite des coordonnées et des déplacements de point et du calcul de distance :
class point_colore p c =
object
  inherit point p

  val c = c

  method get_color = c
  method print () =
  begin
      print_string "( ";
      print_int x;
      print_string " , ";
      print_int y;
      print_string (") de couleur "^c);
  end
end;;

Le système retourne l'interface de classe suivante :

class point_colore :
  int * int ->
  string ->
  object
    val c : string
    val mutable x : int
    val mutable y : int
    method distance : unit -> float
    method get_color : string
    method get_x : int
    method get_y : int
    method moveto : int * int -> unit
    method print : unit -> unit
    method rmoveto : int * int -> unit
  end

Toutes les méthodes de l'interface peuvent être utilisée :

# let pc = new point_colore (2,3) "blanc";;
val pc : point_colore = <obj>
# pc#get_color;;
- : string = "blanc"
# pc#get_x;;
- : int = 2
# pc#display();;
( 2 , 3) de couleur blanc- : unit = ()
# pc#get_x;;
- : int = 2

La classe point_colore redéfinit la méthode print pour tenir compte du champ couleur. On dit que la méthode print redéfinit celle de son ancêtre. Elle doit avoir le même type. Cela ne suffit pas pour rendre les types point et point_colore compatibles :

# p1 = pc;;
This expression has type
  point_colore =
    < distance : unit -> float; get_color : string; get_x : int; get_y : 
      int; moveto : int * int -> unit; print : unit -> unit;
      rmoveto : int * int -> unit >
but is here used with type
  point =
    < distance : unit -> float; get_x : int; get_y : int;
      moveto : int * int -> unit; print : unit -> unit;
      rmoveto : int * int -> unit >
Only the first object type has a method get_color

référencement : self et super

Il est pratique, dans la définition d'une méthode d'une classe, de pouvoir invoquer une autre méthode de la classe sur soi-même ou de pouvoir invoquer une méthode de la classe ancêtre. Pour cela O'Caml autorise de nommer l'objet soi-même ou la classe ancêtre. On redéfinit la classe point_colore :

class point_colore p c  =
object(self)
  inherit point p as super

  val c = c

  method get_color = c
  method print () =
  begin
      super#print();
      print_string (" de couleur "^ self#get_color);
  end
end;;

Il est possible de donner des noms quelconques pour la classe ancêtre et sa sous-classe, mais autant utiliser la terminologie objet (self ou this pour soi-même et super pour l'ancêtre). Cela est utile dans le cas de l'héritage multiple pour différencier les ancêtres.

liaison retardée

On appelle "liaison retardée" (ou liaisons "dynamique") la détermination à l'exécution de la méthode à utiliser lors de l'envoi d'un message. La liaison "précoce" (ou liaison "statique") effectue cette résolution à la compilation

Quand un programme s'exécute, il doit établir la valeur associée à chaque identificateur rencontré. Cette liaison entre un identificateur et sa valeur peut s'effectuer soit à la compilation (liaison statique ou précoce), soit à l'exécution (liaison dynamique ou retardée). Ce problème se posait avant la programmation par objet (par exemple la majorité des dialectes Lisp interprétés possèdent une liaison dynamique, et les dialectes ML compilés une liaison statique).

En programmation objet les liaisons concernent aussi les méthodes. Les langages à objets utilisent la liaison retardée pour implanter le polymorphisme ad hoc où le même envoi de message peut déclencher différents codes à s'exécuter selon l'objet receveur. C'est l'objet lui-même qui saura le code à exécuter. On appelle surcharge d'une méthode le fait de conserver plusieurs liaisons pour cette méthode (à ne pas confondre avec la redéfinition qui masque une ancienne définition qui n'est plus accessible).

L'exemple de la classe abstraite expr_ar illustre ce propos. La fonction evaluateur de type #expr_ar -> unit prend une expression arithmétique et lui envoie le message eval. C'est l'objet lui-même qui pourra déterminer la méthode à employer selon sa nature (constante ou opération binaire). Il est presque impossible au compilateur de le déterminer. En effet le type de la fonction étant le type d'une classe abstraite sans le corps des méthodes (donc sans code) la liaison ne peut qu'être retardée. Cette détermination pourrait être effectuées pour les classes concrètes, mais limiterait l'intérêt du sous-typage.

On suppose que l'on avait écrit la méthode rmoveto de la classe point en appelant la méthode get_y sur self. Dans la classe point_colore on redéfinit seulement la méthode get_y qui retourne la valeur du champs y fois 100 . C'est un cas d'école, mais cela permet de comprendre le mécanisme.

class point (x_init,y_init) = 
object(self)
...
  method rmoveto (dx,dy) = begin x <- x + dx; y <- self#get_y + dy end
...
end;;

class point_colore p c =
object
  inherit point p

  val c = c
  method get_y = y*100
  method get_color = c
...
end;;

Le programme suivant construit un point et un point_colore, les affiche, les déplace et les réaffiche "

# let p = new point (1,1);;
val p : point = <obj>
# p#print();;
( 1 , 1)- : unit = ()
# p#rmoveto(3,4);;
- : unit = ()
# p#print();;
( 4 , 5)- : unit = ()
# let pc = new point_colore(1,1) "blanc";;
val pc : point_colore = <obj>
# pc#print();;
( 1 , 1) de couleur blanc- : unit = ()
# pc#rmoveto(3,4);;
- : unit = ()
# pc#print();;
( 4 , 104) de couleur blanc- : unit = ()

La méthode rmoveto qui n'a pas été redéfinie a son comportement modifiée par la redéfinition de la méthode get_y. En effet la liaison, c'est à dire le choix de la méthode get_y dans le corps de rmoveto n'est pas déterminé à la compilation de la classe point. Ce choix est effectué en regardant dans la liste des méthodes d'une instance de la classe point ou point_colore. Pour une instance de point l'envoi du message rmoveto déclenchera la méthode get_y définie dans point. Par contre le même envoi de message sur une instance de point_colore, appellera la méthode rmoveto héritée de point et déclenchera la méthode get_y redéfinie dans point_colore.

initialisation

Il est possible d'indiquer dans la définition de la classe, une méthode initializer déclenchée immédiatement après la construction de l'objet. Cet "initialisateur" peut faire n'importe quel calcul et a accès aux champs de l'instance (car celle-ci vient d'être créée). On reprend l'exemple des classe point et point_colore en ajoutant à chaque classe un initialisateur.

class point (x_init,y_init) = 
object
...
initializer print_string "Creation d'un point";
              print_newline(); flush stdout

end;;

class point_colore p c =
object
  inherit point p
...
  initializer print_string "Creation d'un point colore";
              print_newline(); flush stdout

end;;

L'exécution suivante permet de suivre l'ordre de déclenchement de la construction des objets et de leur initialisateur :

# let p = new point;;
val p : int * int -> point = <fun>
# let p = new point (3,4);;
Creation d'un point
val p : point = <obj>
# let pc = new point_colore (3,4) "blanc";;
Creation d'un point
Creation d'un point colore
val pc : point_colore = <obj>

méthodes privées

Une méthode peut être déclarée private. Elle n'apparaîtra pas dans l'interface de la classe et donc dans le type de l'objet. Par contre les méthodes privées sont héritées et pourront donc être utilisée dans la hiérarchie. L'exemple suivant reprend la déclaration de la classe point en rendant private la méthode rmoveto :

class point (x_init,y_init) =
...
  method private rmoveto (dx,dy) = begin x <- x + dx; y <- self#get_y + dy end
  method step1 = self#rmoveto(1,1)
...
end;;

L'interface ne contient donc pas la méthode rmoveto comme le montre l'exemple suivant :

# let p = new point (2,3);;
Creation d'un point
val p : point = <obj>
# p#print();;
( 2 , 3)- : unit = ()
# p#step1;;
- : unit = ()
# p#print();;
( 3 , 4)- : unit = ()
# p#rmoveto(1,1);;
This expression has type point
It has no method rmoveto

Classes et méthodes abstraites

Les classes abstraites sont des classes dont certaines méthodes sont déclarées mais ne possèdent pas de corps. Ces méthodes sont dites alors abstraites. Il n'est pas possible d'instancier une classe abstraite (new est interdit). On utilise le mot clé virtual pour le préciser.

Si une sous-classe, d'une classe abstraite, redéfinit toutes les méthodes abstraite de l'ancêtre, alors elle devient concrète, sinon elle reste abstraite.

Dans cet exemple on construit une classe abstraite graphical_object qui ne contient qu'une seule méthode abstraite print.

class virtual graphical_object () =
object
  method virtual print : unit -> unit
end;;
L'interface calculée est la suivante :
class virtual graphical_object :
  unit -> object method virtual print : unit -> unit end
La sous-classe rectangle suivante hérite de graphical_object et définit de manière concrète toutes les méthodes abstraites de graphical_object.
class rectangle (p1,p2) = 
object
  inherit graphical_object ()
  val mutable llc = (p1 : point)
  val mutable ruc = (p2 : point)
  method print () = begin
   print_string "(";p1#print();
   print_string ",";p2#print();
   print_string ")"
  end
 
end;;
Le système retourne l'interface de classe suivante :
class rectangle :
  point * point ->
  object
    val mutable llc : point
    val mutable ruc : point
    method print : unit -> unit
  end

Autres lectures

Ce cours s'inspire des documents suivants :


next up previous
Previous: No Title
Emmanuel CHAILLOUX
1998-11-15