Étendre les composants
On appelle composant un regroupement de données et de traitements sur ces données. Dans le modèle
fonctionnel/modulaire, un composant comprend la définition d'un type et des fonctions manipulant
ce type. De même un composant dans le modèle objet correspond à une hiérarchie de classes,
héritant d'une même classe et donc possédant tous les comportements de
celle-ci. Le problème de l'extensibilité
des composants consiste à vouloir d'une part augmenter les traitements/comportements sur
ceux-ci et d'autre part d'augmenter les données traitées et cela sans
modifier les programmes sources initiaux. Par exemple un composant image
peut être soit un rectangle, soit un cercle que l'on peut afficher ou déplacer.
|
rectangle |
cercle |
groupe |
affiche |
X |
X |
|
déplace |
X |
X |
|
agrandit |
|
|
|
On peut vouloir étendre le composant image par le traitement agrandir
et créer des groupes d'images. Le comportement des deux modèles diffère selon le sens de
l'extension : données ou traitements. On définit tout d'abord, dans chaque modèle, la partie commune du composant image, puis on cherche à l'étendre.
En fonctionnel
On définit le type image comme un type somme contenant deux cas. Les traitements
prennent en entrée un paramètre de type image et effectuent le travail adéquat.
# type
image
=
Rect
of
float
|
Cercle
of
float
;;
# let
affiche
=
function
Rect
r
->
...
|
Cercle
c
->
...
;;
# let
dé
place
=
...
;;
On encapsule ensuite ces déclarations globales dans un module simple.
Extension des traitements
L'extension des traitements dépend de la représentation du type image dans le
module. Si celui-ci est abstrait, il n'y a plus de possibilité d'extension des traitements. Dans le cas où
le type est resté concret, il est facile d'ajouter une fonction agrandir qui
effectue le changement d'échelle d'une image en sélectionnant un rectangle ou un cercle par
filtrage de motifs.
Extension des données
L'extension des données ne peut pas s'effectuer avec le type image. En effet
les types Objective CAML ne sont pas extensibles, sauf pour le type exn représentant
des exceptions.
Il n'est pas possible d'étendre les données en conservant le même type,
il est donc nécessaire de définir un nouveau type n_image
de la manière
suivante :
type n_image = I of image | G of n_image * n_image;;
On doit redéfinir alors les traitements pour ce nouveau type en simulant une sorte d'héritage.
Cela devient complexe dès qu'il y a plusieurs extensions.
En objet
On définit les classes rectangle et cercle, sous-classes de la classe abstraite
image qui a deux méthodes abstraites affiche et deplace.
# class
virtual
image
()
=
object(self:
'a)
method
virtual
affiche
:
unit
->
unit
method
virtual
deplace
:
float
*
float
->
unit
end;;
# class
rectangle
x
y
w
h
=
object
inherit
image
()
val
mutable
x
=
x
val
mutable
y
=
y
val
mutable
w
=
w
val
mutable
h
=
h
method
affiche
()
=
Printf.printf
"R: (%f,%f) [%f,%f]"
x
y
w
h
method
deplace
(dx,
dy)
=
x
<-
x
+.
dx;
y
<-
y
+.
dy
end;;
# class
cercle
x
y
r
=
object
val
mutable
x
=
x
val
mutable
y
=
y
val
mutable
r
=
r
method
affiche
()
=
Printf.printf
"C: (%f,%f) [%f]"
x
y
r
method
deplace
(dx,
dy)
=
x
<-
x
+.
dx;
y
<-
y
+.
dy
end;;
Le programme suivant construit une liste d'images et l'affiche.
# let
r
=
new
rectangle
1
.
1
.
3
.
4
.
;;
val r : rectangle = <obj>
# let
c
=
new
cercle
1
.
1
.
4
.
;;
val c : cercle = <obj>
# let
l
=
[
(r
:>
image);
(c
:>
image)]
;;
val l : image list = [<obj>; <obj>]
# List.iter
(fun
x
->
x#affiche();
print_newline())
l;;
R: (1.000000,1.000000) [3.000000,4.000000]
C: (1.000000,1.000000) [4.000000]
- : unit = ()
Extension des données
Les données sont facilement extensibles en ajoutant une sous-classe à la classe
image de la manière suivante.
# class
groupe
i1
i2
=
object
val
i1
=
(i1:#
image)
val
i2
=
(i2:#
image)
method
affiche
()
=
i1#affiche();
print_newline
();
i2#affiche()
method
deplace
p
=
i1#deplace
p;
i2#deplace
p
end;;
On s'aperçoit alors que le <<type>> image devient récursif car la classe
groupe dépend hors héritage de la classe image.
# let
g
=
new
groupe
(r:>
image)
(c:>
image);;
val g : groupe = <obj>
# g#affiche();;
R: (1.000000,1.000000) [3.000000,4.000000]
C: (1.000000,1.000000) [4.000000]- : unit = ()
Extension des traitements
On définit une sous-classe abstraite d'image contenant une nouvelle méthode.
# class
virtual
e_image
()
=
object
inherit
image
()
method
virtual
surface
:
unit
->
float
end;;
On peut définir les classes e_rectangle et e_cercle qui
héritent de e_image et de rectangle et cercle respectivement. On peut alors travailler sur ces images étendues pour utiliser cette nouvelle méthode. Il subsiste une difficulté pour la classe groupe. Celle-ci possède
deux champs de type image, donc même en héritant de la classe
e_image il ne sera pas possible d'envoyer le message
agrandit aux champs images. Il est donc possible d'étendre
les traitements sauf pour les cas de sous-classes correspondant à des
types récursifs.
Extension des données et des traitements
Pour réaliser l'extension dans les deux sens, il est nécessaire de définir
les types récursifs sous forme de classe paramétrée. On redéfinit
la classe groupe.
# class
[
'a]
groupe
i1
i2
=
object
val
i1
=
(i1:
'a)
val
i2
=
(i2:
'a)
method
affiche
()
=
i1#affiche();
i2#affiche()
method
deplace
p
=
i1#deplace
p;
i2#deplace
p
end;;
On reprend alors le même principe que pour la classe e_image.
# class
virtual
ext_image
()
=
object
inherit
image
()
method
virtual
surface
:
unit
->
float
end;;
# class
ext_rectangle
x
y
w
h
=
object
inherit
ext_image
()
inherit
rectangle
x
y
w
h
method
surface
()
=
w
*.
h
end;;
# class
ext_cercle
x
y
r=
object
inherit
ext_image
()
inherit
cercle
x
y
r
method
surface
()
=
3
.
1
4
*.
r
*.
r
end;;
L'extension de la classe groupe devient alors
# class
[
'a]
ext_groupe
ei1
ei2
=
object
inherit
image()
inherit
[
'a]
groupe
ei1
ei2
method
surface
()
=
ei1#surface()
+.
ei2#surface
()
end;;
On obtient le programme suivant qui construit une liste le
de type ext_image.
# let
er
=
new
ext_rectangle
1
.
1
.
2
.
4
.
;;
val er : ext_rectangle = <obj>
# let
ec
=
new
ext_cercle
1
.
1
.
8
.
;;
val ec : ext_cercle = <obj>
# let
eg
=
new
ext_groupe
er
ec;;
val eg : ext_rectangle ext_groupe = <obj>
# let
le
=
[
(er:>
ext_image);
(ec
:>
ext_image);
(eg
:>
ext_image)]
;;
val le : ext_image list = [<obj>; <obj>; <obj>]
# List.map
(fun
x
->
x#surface())
le;;
- : float list = [8; 200.96; 208.96]
Généralisation
Pour généraliser l'extension des traitements il est préférable
d'intégrer des fonctions dans une méthode traitement
et de construire une classe paramétrée par le type du résultat du traitement.
Pour cela on définit la classe suivante :
# class
virtual
[
'a]
get_image
(f:
'b
->
unit
->
'a)
=
object(self:
'b)
inherit
image
()
method
traitement
()
=
f(self)
()
end;;
Les classes suivantes possèdent alors un paramètre fonctionnel supplémentaire
pour la construction de leurs instances.
# class
[
'a]
get_rectangle
f
x
y
w
h
=
object(self:
'b)
inherit
[
'a]
get_image
f
inherit
rectangle
x
y
w
h
method
get
=
(x,
y,
w,
h)
end;;
# class
[
'a]
get_cercle
f
x
y
r=
object(self:
'b)
inherit
[
'a]
get_image
f
inherit
cercle
x
y
r
method
get
=
(x,
y,
r)
end;;
L'extension de la classe groupe prend alors deux paramètres de type :
# class
[
'a,
'c]
get_groupe
f
eti1
eti2
=
object
inherit
[
'a]
get_image
f
inherit
[
'c]
groupe
eti1
eti2
method
get
=
(i1,
i2)
end;;
On obtient le programme qui étend le traitement des instances de get_image.
# let
etr
=
new
get_rectangle
(fun
r
()
->
let
(x,
y,
w,
h)
=
r#get
in
w
*.
h)
1
.
1
.
2
.
4
.
;;
val etr : float get_rectangle = <obj>
# let
etc
=
new
get_cercle
(fun
c
()
->
let
(x,
y,
r)
=
c#get
in
3
.
1
4
*.
r
*.
r)
1
.
1
.
8
.
;;
val etc : float get_cercle = <obj>
# let
etg
=
new
get_groupe
(fun
g
()
->
let
(i1,
i2)
=
g#get
in
i1#traitement()
+.
i2#traitement())
(etr
:>
float
get_image)
(etc
:>
float
get_image);;
val etg : (float, float get_image) get_groupe = <obj>
# let
gel
=
[
(etr
:>
float
get_image)
;
(etc
:>
float
get_image)
;
(etg
:>
float
get_image)
]
;;
val gel : float get_image list = [<obj>; <obj>; <obj>]
# List.map
(fun
x
->
x#traitement())
gel;;
- : float list = [8; 200.96; 208.96]
L'extension des données et des traitements est plus facile dans le
modèle objet en s'appuyant sur le modèle fonctionnel.