Programmation:OCaml:Objets
Sommaire
Introduction
La programmation orientée objet (OOP Object Oriented Programming) est un paradigme de programmation transversale: il est possible de faire de l'objet tant dans un univers fonctionnel qu'impératif. Historiquement, la programmation objet est plus impérative que fonctionnelle, ce qui explique que les langages objets les plus répandus (C++, Java, C# ... ) soient des langages fortement impératifs.
L'OOP est aussi une discipline de modélisation qui ne nécessite pas forcément d'un support concret: on peut modéliser en terme d'objets, de classes, de spécialisations ou d'agrégations sans pour autant disposer des concepts idoines dans le langage cible. En terme de conception, l'OOP étend et renforce la notion de types abstraits: les données sont maintenant des boites noires (les objets) qui ne sont accessible que par leurs opérations publiques (les méthodes.)
Bien utilisée, l'OOP apporte une valeur ajoutée non négligeable en terme de modularité, d'efficacité (dans la réalisation, pas forcément dans les performances) ou de fiabilité. Combiné avec certains modèles comme UML[1][2] ou certaines approches de modélisation comme les Design Patterns, l'OOP permet diminuer la cassure entre les activités de conceptions et les activités de programmation, assurant ainsi une meilleure adéquation entre les besoins utilisateurs et les logiciels produits.
Bien évidement toutes ces qualités ont des revers: certains trouvent le modèle objet trop contraignant, les indirections induites par les appels de méthodes (notamment en présence de liaison tardive.)
OCaml fournit son propre modèle objet assez classique sur beaucoup d'aspects mais qui a également ses spécificités (typage structurelle, interaction avec l'inférence et le polymorphisme, héritage multiple ... )
Définitions
Voici une série de définitions des notions de base de l'OOP:
- Objet
- l'entité de base de l'OOP. Un objet est une entité opaque qui regroupe les données (sous forme d'attributs ou propriétés) et les opérations pour les manipulées (les méthodes.)
- Classe
- la classe est l'abstraction des objets. Une classe définit une famille d'objets aillant tous la même forme. La classe ne contient pas de donnée et ne peut être utilisée sans être instantiée.
- Héritage
- relation reliant deux classes. Lorsqu'une classe C (dite classe fille) hérite d'une classe B (dite classe mère), elle reprend l'ensemble des définitions de la classe mère en les spécialisant. L'héritage permet de redéfinir une famille d'objets comme étant un cas particulier mais sans avoir à redéfinir les parties conservées.
- Agrégation
- relation reliant plusieurs objets et/ou classes. On dit qu'un objet agrège d'autres objets lorsqu'il intègre dans ses données ces objets.
- Délégation
- reliant reliant plusieurs objets et/ou classes. Un objet O1 fournit la délégation d'une méthode d'un autre objet O2 (qu'il agrège aussi en général) lorsqu'il fournit une méthode qui appelle une méthode de O2.
- Propriété
- partie d'un objet. Une propriété d'un objet est une donnée inclue dans l'objet.
- Attribut
- voir propriété.
- Méthode
- partie d'un objet. Fonction associée à l'objet et qui permet soit d'interroger l'objet sur l'état interne de ses propriétés, soit de modifier ses propriétés ou de déclencher tout comportement que seul l'objet lui même peut réaliser.
- Message
- voir méthode.
- Passage de message
- terme utilisé en général pour désigner l'appel d'une méthode d'un objet. Dans certains modèles objets, les méthodes sont considérés comme des messages et l'exécution du code de la méthode et déclenché par la réception du message idoine.
- État de l'objet
- valeur des propriétés d'un objet à un instant donné.
- Sérialisation
- représentation externe d'un objet et de son état en vue d'une sauvegarde externe ou d'une communication.
- Désérialisation
- construction d'un objet à partir de sa représentation sérialisée.
- Généralisation
- relation symétrique de l'héritage (rarement matérialisée.)
- Spécialisation
- voir héritage.
- Diagramme de classes
- graphe représentant les relations entre classes ou objets dans un programme (conception.)
- Singleton
- classe particulière n'étant instantiée qu'une seule fois.
Modélisation
Une partie de l'impacte de l'OOP tient à sa relation avec de nombreuse méthode de modélisation et de conception. On pense bien sûr à UML, mais également aux design patterns et à tout une collection d'approches basées sur la programmation orientée objet.
Notions de base
Même si les techniques et le discours changent, tous ces modèles partagent de nombreux points communs. Regardons ces différents points:
- Abstraction
- L'abstraction en OOP est un point important. Une bonne partie de la conception objet repose sur l'idée qu'il faut abstraire chaque notion manipulée pour en extraire une vision comportementale. On commence donc souvent une conception objet par décrire des entités (et leur relation) abstraite en dehors de tout contexte informatique.
- Encapsulation
- L'encapsulation (un horrible néologisme) désigne le fait de systématiquement imbriquer un ensemble de données dans un objet pour en cacher la représentation. La notion d'encapsulation en OOP traduit la volonté de restreindre l'usage de l'information.
- Héritage
- L'héritage en terme de modélisation traduit la relation de spécialisation entre deux classes, particulièrement dans les modèles objets usuels, lorsqu'une classe hérite d'une autre, on considère qu'elle peut être vue comme un cas particulier de la classe mère. Cette relation pose parfois des problèmes à l'implantation.
- Agrégation
- L'agrégation traduit la notion que certains objets (enfin classes) sont des conteneurs: ils englobent d'autres objets et délègue parfois l'accès à leur méthode.
Méthodologie
Afin de profiter des bons aspects de l'OOP, un peu de méthodologie est nécessaire. La première étape consiste à lister l'ensemble des entités du programme. On va ensuite relier ces entités par différentes sortes de relations. Pendant le déroulement de ce processus, on sera amener à faire apparaître de nouvelles entités (en général pour des raisons d'abstraction.) Les relations principales reliant les objets sont les suivantes:
- is_a
- la relation de spécialisation qui indique qu'une entité est un cas particulier d'une autre.
- contains
- la relation d'agrégation indiquant qu'une entité englobe une autre entité, cette relation s'accompagne souvent de cardinalités désignant combien d'entités sont englobés (nombre exact, nombre minimal et/ou maximal ... )
Dans le processus de modélisation, il n'est pas rare d'ajouter une entité pour représenter le caractère commun à deux autres entités (ou plus). Cette approche concrétise la vision à revers de la modélisation: on fait apparaître en fonction des besoins le niveau d'abstraction nécessaire pour simplifier le code.
Par exemple, supposons que vous ayez besoin de modéliser dans votre programme des chiens et des chats. En première approche vous allez ajouter à votre modèle une entité chien et une entité chat, mais il apparaîtra probablement que ces deux entités partagent énormément de point commun et qu'il serait pertinent d'ajouter une entité animal et de rajouter les relations: chien is_a animal et chat is_a animal.
Le même raisonnement s'applique également aux relations d'agrégation: supposons que vous modélisiez des véhicules comme des voitures et des motos et que vous ayez besoin d'accéder aux pneus de ses véhicules. La solution la plus pratique consiste à modéliser les pneus (voir la roue de manière générale) et d'ajouter les relations du genre: voiture contains(4) pneus et moto contains(2) pneus.
On prendra l'habitude de représenter ces relations à l'aide de schéma où les cadres représenteront les entités et les flèches les relations.
Généralement, à l'issue de la modélisation vous avez l'ensemble des classes dont vous aurez besoin (les entités dont nous parlions précédemment) avec la bonne hiérarchie d'abstraction (les relations is_a forment en générale les relations d'héritage ou d'implantation d'interface) et les liens de dépendances entre vos classes.
Aspects et interfaces
Les interfaces du monde objet rejoignent les interfaces des modules: elles décrivent se qu'un objet fournit (ou devrait fournir.) Une interface forme une sorte de contrat: les classes qui implanteront une certaine interface, devront fournir toutes les méthodes contenues dans l'interface.
On se sert généralement des interfaces pour décrire un aspect de l'objet: cette entité est affichable car elle fournit les méthodes idoines ... Pour objet respecter (ou implanter) une interface signifie qu'il pourra être utilisé partout où cette interface est attendue.
Comme pour les foncteurs, les interfaces permettent d'écrire du code générique s'appuyant sur une contrainte minimale. Le programmeur décrit au travers d'une interface les aspects que l'objet devra avoir pour pouvoir être utilisé avec son code et tout objet respectant l'interface conviendra. Cette logique étend la notion d'héritage, en ne conservant que la partie extérieure de la relation (existance des méthodes.)
Classes virtuelles
On rencontre également une notion médiane entre implantation d'interface et héritage de classe complète: les classes virtuelles ou abstraites. Il s'agit de classe dont seule une partie des méthodes disposent d'une définition concrète, les autres sont présentées comme dans une interface, uniquement par leur signature. Ces méthodes sont dites virtuelles (ou virtuelles pures.) Lorsqu'une classe hérite d'une classe virtuelle, elle devra soit implanter toutes les méthodes virtuelles de cette classe, soit être elle même virtuelle.
L'objectif des classes virtuelles est de poser un contrat sur l'existence de certaines fonctionnalités de l'objet, afin d'implanter des fonctionnalités génériques reposant dessus.
Considérons, par exemple, la possibilité d'afficher une description textuelle d'un objet. La description de l'objet elle même sera spécifique à l'objet (tout du moins à sa classe) tandis que l'opération d'affichage par elle même sera global pour le programme. Si on préfère intégré la fonctionnalité d'affichage dans les objets directement, plutôt que de faire référence systématiquement à un objet global chargé de l'affichage, on héritera d'une classe définissant cette fonctionnalité. La classe en question ne pouvant pas fournir l'implantation de la méthode construisant la description, celle-ci sera donc virtuelle. Cette exemple de modélisation est illustrée dans le schéma accompagnant ce texte.
Objets en OCaml
Syntaxe basique
Un petit exemple de classe simple:
class point = object val mutable x = 0 val mutable y = 0 method get_x = x method get_y = y method set_x nx = x <- nx method set_y ny = y <- ny end
L'évaluation de cette définition par OCaml donnera le résultat suivant:
class point : object val mutable x : int val mutable y : int method get_x : int method get_y : int method set_x : int -> unit method set_y : int -> unit end
Il s'agit d'une classe disposant de deux attributs privés x et y et des méthodes permettants d'y accéder (ou de les modifier) de l'extérieur.
On peut modifier cette exemple pour que la valeur intiale des attributs soit fixer à l'instantiation:
class point xi yi = object val mutable x:int = xi val mutable y:int = yi method get_x = x method get_y = y method set_x nx = x <- nx method set_y ny = y <- ny end
Dont le typage par OCaml donnera:
class point : int -> int -> object val mutable x : int val mutable y : int method get_x : int method get_y : int method set_x : int -> unit method set_y : int -> unit end
On peut maintenant instantier un objet de classe point:
let p = new point 5 10
On note le résultat de l'évaluation par OCaml:
val p : point = <obj>
On peut utiliser notre objet:
let _ = Printf.printf "(%d,%d)\n" p#get_x p#get_y; p#set_x 42; Printf.printf "(%d,%d)\n" p#get_x p#get_y
Ce qui affichera:
(5,10) (42,10)
On va maintenant utiliser l'héritage d'OCaml pour créer une méthode alternative pour créer un point sur l'origine:
class origin_point = object inherit point 0 0 end let op = new origin_point
La classe origin_point engendre des points initialisés à l'origine.
On décide d'ajouter une méthode to_string et d'y utiliser les méthode interne de l'objet. Pour ça, on va devoit auto-référencer l'objet:
class point xi yi = object (self) val mutable x:int = xi val mutable y:int = yi method get_x = x method get_y = y method set_x nx = x <- nx method set_y ny = y <- ny method to_string = "("^(string_of_int self#get_x)^","^(string_of_int self#get_y)^")" end
On va maintenant étendre notre classe point pour lui ajouter la couleur:
let colored_point x y c = object inherit point x y val mutable color:string = c method get_col = color method set_col c = color <- c end
Ce qui donne à l'évaluation:
class colored_point : int -> int -> string -> object val mutable color : string val mutable x : int val mutable y : int method get_col : string method get_x : int method get_y : int method set_col : string -> unit method set_x : int -> unit method set_y : int -> unit method to_string : string end
On veut maintenant remplacer la méthode to_string en utilisant l'original (celle de la classe point):
class colored_point x y c = object inherit point x y as m val mutable color:string = c method get_col = color method set_col c = color <- c method to_string = color ^ m#to_string end
On peut combiner ce type de construction et l'héritage multiple:
class obj = object method to_string = "objet: " end class obj_colored_point x y c = object inherit obj as m1 inherit colored_point x y c as m2 method to_string = m1#to_string ^ m2#to_string end
Objet singleton
On peut également construire un objet sans classe:
let sp = object (self) val mutable x = 0 val mutable y = 0 method get_x = x method get_y = y method set_x nx = x <- nx method set_y ny = y <- ny method to_string = "singleton("^(string_of_int self#get_x)^","^(string_of_int self#get_y)^")" end
L'évaluation de cette expression donne:
val sp : < get_x : int; get_y : int; set_x : int -> unit; set_y : int -> unit; to_string : string >
Comme il est possible de construire un objet directement, on peut donc construire des fonctions construisant des objets:
let sp xi yi = object (self) val mutable x = xi val mutable y = yi method get_x = x method get_y = y method set_x nx = x <- nx method set_y ny = y <- ny method to_string = "singleton("^(string_of_int self#get_x)^","^(string_of_int self#get_y)^")" end
Dont l'évaluation donnera:
val sp : int -> int -> < get_x : int; get_y : int; set_x : int -> unit; set_y : int -> unit; to_string : string >
Par certain côté, les fonctions créant des objets singletons pourraient être vue comme des classes. À la différence qu'il n'est pas possible d'hériter de ces fonctions et qu'elles ne définissent pas un nom de classe (alias pour le type de l'objet.)
Typage structurel et rangées ouvertes
Le typage des objets en OCaml repose non pas sur la classe dont ils sont instances, mais sur les interfaces qu'ils exposent. Les noms de classes ne sont en OCaml que des alias pour simplifier l'utilisation des types des objets.
On a pu voir un exemple de rangée (le type des objets) dans la section précédante, l'objet singleton sp avait pour type:
< get_x : int; get_y : int; set_x : int -> unit; set_y : int -> unit; to_string : string >
Ce type décrit exactement l'interface de l'objet. Mais il est possible d'écrire une rangée ouverte, c'est à dire un type objet ne décrivant qu'une contrainte minimale. L'exemple suivant illustre ce concept:
let f o = "objet :"^o#to_string
Le type inféré par OCaml sera:
val f : < to_string : string; .. > -> string
Dans la rangée, les .. représente le fait que l'objet peut disposer de nombreuses autres méthodes, tandis que la partie to_string:string impose l'existance de la méthode to_string.
Le typage des objets reposant sur cette approche structurelle, il n'est pas nécessaire de construire des interfaces pour obtenir la même notion de contrat.
Agrégation
Pour réaliser une agrégation d'objet, il suffit en théorie d'ajouter un attribut contenant l'objet en question:
class box p = object (s) val mutable content = p method add np = content <- np method get = content end
Seulement, OCaml ne permet pas que des types soient libres dans une classe, il faut soit imposer le type de l'objet agréger, soit expliciter le polymorphisme de la classe:
class point_box p = object (s) val mutable content : point = p method add np = content <- np method get = content end (* version polymorphe *) class ['a] box p = object (s) val mutable content : 'a = p method add np = content <- np method get = content end
Peut-on mettre un point coloré dans un objet de classe point_box ?
let pb = new point_box (new colored_point 0 0 "red")
Ce qui engendre l'erreur:
Error: This expression has type colored_point but an expression was expected of type point The second object type has no method get_col
La description de la classe point_box est trop rigide, à l'inverse la description de la classe ['a] box est complètement ouverte. On aimerait par exemple, que la classe point_box puisse agréger n'importe quel objet d'une sous-classe de point.
Pour ça on peut utiliser la rangée ouverte suivante:
< get_x : int; get_y : int; set_x : int -> unit; set_y : int -> unit; to_string : string; .. >
Avec le mot clef constraint:
class ['a] point_box p = object (s) constraint 'a = < get_x : int; get_y : int; set_x : int -> unit; set_y : int -> unit; to_string : string; .. > val mutable content : 'a = p method add np = content <- np method get = content end let pb = new point_box (new colored_point 0 0 "red")
On peut maintenant utiliser un objet de classe colored_point avec notre point_box. On aurait pu utiliser la notation #point qui correspond au même type que point mais en version ouverte.
Cours | Partie |
---|---|
Cours de Programmation EPITA/spé | Programmation:OCaml |