Exemple : gestion de comptes bancaires
Nous terminons ce chapitre par la réalisation d'un petit exemple
illustrant les principaux traits de l'utilisation d'une programmation
modulaire : abstraction de type ; diverses vues d'un module ;
réutilisation de code à l'aide des foncteurs.
Le but de cet exemple est de fournir deux modules de gestion d'un
compte bancaire. L'un est destiné au banquier et l'autre au client.
Le principe est de réaliser un module paramétré général fournissant
toutes les fonctionnalités souhaitables de gestion puis de l'appliquer
à certains paramètres en le contraignant avec la signature
correspondant à sa destination finale : le banquier ou le client.
Organisation de l'application
Figure 14.1 : graphe de dépendances des modules
Les deux modules finals BGestion et CGestion sont
obtenus par contrainte sur la signature du module Gestion. Ce
dernier est obtenu par application du foncteur FGestion aux
modules Compte, Date et deux autres modules
construits également par application des foncteurs FHisto
et FReleve. La figure 14.1 illustre ces
dépendances.
Signatures des modules paramètres
Un module de gestion de compte bancaire est paramétré par quatre
autres modules dont nous donnons et commentons ci-dessous les signatures.
Le compte lui-même
Ce module fournit essentiellement les
opération de calcul sur le contenu du compte.
# module
type
COMPTE
=
sig
type
t
exception
OperationImpossible
val
creer
:
float
->
float
->
t
val
depot
:
float
->
t
->
unit
val
retrait
:
float
->
t
->
unit
val
solde
:
t
->
float
end
;;
Cet ensemble de fonctions fournit les opérations minimales de gestion
du contenu du compte. L'opération de création prend en argument
le solde initial du compte et la valeur du découvert
autorisé. L'opération de retrait peut déclencher l'exception
OperationImpossible.
Des clés ordonnées
Les opérations seront
enregistrées selon un historique décrit au paragraphe
suivant. On associera une clé à chaque enregistrement. Les
fonctions de gestion des clés sont données par la signature :
# module
type
OCLE
=
sig
type
t
val
creer
:
unit
->
t
val
of_string
:
string
->
t
val
to_string
:
t
->
string
val
eq
:
t
->
t
->
bool
val
lt
:
t
->
t
->
bool
val
gt
:
t
->
t
->
bool
end
;;
La fonction creer permet d'engendrer une clé supposée
unique. Les fonction of_string et to_string sont
des opérations de conversion de et vers les chaînes de
caractères. Les trois autres fonctions sont des fonctions de
comparaison.
Historique
Pour garder l'historique de gestion d'un
compte, on se donne l'ensemble de données abstraites et de fonctions
suivant :
# module
type
HISTO
=
sig
type
tcle
type
tinfo
type
t
val
creer
:
unit
->
t
val
add
:
tcle
->
tinfo
->
t
->
unit
val
nth
:
int
->
t
->
tcle*
tinfo
val
get
:
(tcle
->
bool)
->
t
->
(tcle*
tinfo)
list
end
;;
Nous laissons pour l'instant indéterminé la nature des clés
d'enregistrement (type tcle) la nature des informations (type
tinfo) et la structure de stockage (type t). On
supposera que l'ajout de nouvelles informations (fonction add)
est séquentiel. Le module fournit deux fonctions d'accès aux
données archivées : un accès selon leur ordre d'enregistrement
(fonction nth) et un accès selon un critère défini sur
les clés (fonction get).
Les relevés de compte
Le dernier paramètre du module de
gestion fournit deux fonctions d'édition d'un relevé de compte :
# module
type
RELEVE
=
sig
type
tdata
type
tinfo
val
editB
:
tdata
->
tinfo
val
editC
:
tdata
->
tinfo
end
;;
On laisse abstrait le type de données à traiter (tdata)
ainsi que le type des informations extraites (tinfo).
Module général paramétré de gestion
Munis des seules informations que fournissent les signatures
ci-dessus, nous pouvons donner la structure du foncteur général de
gestion d'un compte :
# module
FGestion
=
functor
(C:
COMPTE)
->
functor
(K:
OCLE)
->
functor
(H:
HISTO
with
type
tcle=
K.t
and
type
tinfo=
float)
->
functor
(R:
RELEVE
with
type
tdata=
H.t
and
type
tinfo=
(H.tcle*
H.tinfo)
list)
->
struct
type
t
=
{
mutable
c
:
C.t;
mutable
h
:
H.t
}
let
creer
s
d
=
{
c
=
C.creer
s
d;
h
=
H.creer()
}
let
depot
s
g
=
C.depot
s
g.
c
;
H.add
(K.creer())
s
g.
h
let
retrait
s
g
=
C.retrait
s
g.
c
;
H.add
(K.creer())
(-.
s)
g.
h
let
solde
g
=
C.solde
g.
c
let
releve
edit
g
=
let
f
(d,
i)
=
(K.to_string
d)
^
":"
^
(string_of_float
i)
in
List.map
f
(edit
g.
h)
let
releveB
=
releve
R.editB
let
releveC
=
releve
R.editC
end
;;
module FGestion :
functor(C : COMPTE) ->
functor(K : OCLE) ->
functor
(H : sig
type tcle = K.t
and tinfo = float
and t
val creer : unit -> t
val add : tcle -> tinfo -> t -> unit
val nth : int -> t -> tcle * tinfo
val get : (tcle -> bool) -> t -> (tcle * tinfo) list
end) ->
functor
(R : sig
type tdata = H.t
and tinfo = (H.tcle * H.tinfo) list
val editB : tdata -> tinfo
val editC : tdata -> tinfo
end) ->
sig
type t = { mutable c: C.t; mutable h: H.t }
val creer : float -> float -> t
val depot : H.tinfo -> t -> unit
val retrait : float -> t -> unit
val solde : t -> float
val releve : (H.t -> (K.t * float) list) -> t -> string list
val releveB : t -> string list
val releveC : t -> string list
end
Précision et partage de types
La contrainte de type
appliquée au paramètre H du foncteur FGestion
précise que les clés de l'historique sont celles fournies par le
paramètre K et que les informations stockées sont simplement
des flottants (le montant des transactions). La contrainte de type
appliquée au paramètre R indique que les informations
traitées par le relevé proviennent de l'historique (paramètre
H).
La signature inférée du foncteur FGestion
prend en compte les contraintes de type dans la signature
inférée des arguments.
On définit le type d'un compte comme le contenu du compte
(paramètre C) et son historique.
Opérations
Il est important de remarquer ici que toutes
les opérations de ce foncteur sont définies en terme de
fonctions fournies par les modules paramètres.
Les opérations de création, retrait et dépot affectent le
contenu du compte et son historique. Les deux opérations de
consultation sont la lecture du contenu du compte (fonction
solde) et la lecture d'une partie de l'historique (fonction
releve).
Définition des paramètres
Il faut, avant de créer
les modules finals, définir le contenu des paramètres du foncteur
FGestion.
Contenu
Le contenu d'un compte est essentiellement un flottant représentant
le solde courant. On rajoute l'information d'autorisation de
découvert qui sert à contrôler l'opération de retrait.
# module
Compte:
COMPTE
=
struct
type
t
=
{
mutable
solde:
float;
decouvert:
float
}
exception
OperationImpossible
let
creer
s
d
=
{
solde=
s;
decouvert=
(-.
d)
}
let
depot
s
c
=
c.
solde
<-
c.
solde+.
s
let
solde
c
=
c.
solde
let
retrait
s
c
=
let
ss
=
c.
solde
-.
s
in
if
ss
<
c.
decouvert
then
raise
OperationImpossible
else
c.
solde
<-
ss
end
;;
module Compte : COMPTE
Choix d'une clé
On décide que la clé d'enregistrement
est simplement la date de la transaction exprimée en flottant telle
que la fournit la fonction time du module Unix.
# module
Date:
OCLE
=
struct
type
t
=
float
let
creer()
=
Unix.time()
let
of_string
=
float_of_string
let
to_string
=
string_of_float
let
eq
=
(=
)
let
lt
=
(<
)
let
gt
=
(>
)
end
;;
module Date : OCLE
L'historique
Nous l'avons vu, l'historique d'un compte
dépend du type de clé choisi, c'est pourquoi nous définissons
un foncteur prenant en argument un module fournissant le type des
clés, ce qui permet de définir le type des clés
d'enregistrement.
# module
FHisto
(K:
OCLE)
=
struct
type
tcle
=
K.t
type
tinfo
=
float
type
t
=
{
mutable
content
:
(tcle*
tinfo)
list
}
let
creer()
=
{
content
=
[]
}
let
add
c
i
h
=
h.
content
<-
(c,
i)::h.
content
let
nth
i
h
=
List.nth
h.
content
i
let
get
f
h
=
List.filter
(fun
(c,_
)
->
(f
c))
h.
content
end
;;
module FHisto :
functor(K : OCLE) ->
sig
type tcle = K.t
and tinfo = float
and t = { mutable content: (tcle * tinfo) list }
val creer : unit -> t
val add : tcle -> tinfo -> t -> unit
val nth : int -> t -> tcle * tinfo
val get : (tcle -> bool) -> t -> (tcle * tinfo) list
end
Remarquons que le type des informations doit être cohérent avec
celui donné lors de la définition du foncteur de gestion.
Les relevés
Nous définissons deux types de
relevé. Le premier (editB) donne les cinq dernières
transactions et est destiné au banquier ; le second (editC)
donne les transactions effectuées les dix derniers jours à partir
de la date courante et est destiné au client.
# module
FReleve
(K:
OCLE)
(H:
HISTO
with
type
tcle=
K.t)
=
struct
type
tdata
=
H.t
type
tinfo
=
(H.tcle*
H.tinfo)
list
let
editB
h
=
List.map
(fun
i
->
H.nth
i
h)
[
0
;1
;2
;3
;4
]
let
editC
h
=
let
c0
=
K.of_string
(string_of_float
((Unix.time())
-.
8
6
4
0
0
0
.
))
in
let
f
=
K.lt
c0
in
H.get
f
h
end
;;
module FReleve :
functor(K : OCLE) ->
functor
(H : sig
type tcle = K.t
and tinfo
and t
val creer : unit -> t
val add : tcle -> tinfo -> t -> unit
val nth : int -> t -> tcle * tinfo
val get : (tcle -> bool) -> t -> (tcle * tinfo) list
end) ->
sig
type tdata = H.t
and tinfo = (H.tcle * H.tinfo) list
val editB : H.t -> (H.tcle * H.tinfo) list
val editC : H.t -> (H.tcle * H.tinfo) list
end
Remarquons que pour définir le relevé décadaire, il nous faut
connaître précisément l'implantation des clés
utilisées. C'est sans doute là une entorse au principe des types
abstraits. Cependant la clé correspondant à une date antérieure
de dix jours que nous utilisons est obtenue à partir de sa
représentation textuelle par appel à la fonction
of_string et non pas par calcul direct de la représentation
interne de cette date (même si notre exemple est trop pauvre pour
rendre ce distingo très manifeste).
Les modules finals
Pour construire les modules
GBanque et GClient respectivement destinés au banquier et
au client nous procédons comme suit :
-
définition d'une structure par application du foncteur
FGestion aux paramètres.
- déclaration d'une signature indiquant les fonctions
accessibles dans chacun des cas.
- définition du module final par contrainte de la structure
obtenue en 1 avec la signature déclarée en 2.
# module
Gestion
=
FGestion
(Compte)
(Date)
(FHisto(Date))
(FReleve
(Date)
(FHisto(Date)))
;;
module Gestion :
sig
type t =
FGestion(Compte)(Date)(FHisto(Date))(FReleve(Date)(FHisto(Date))).t =
{ mutable c: Compte.t;
mutable h: FHisto(Date).t }
val creer : float -> float -> t
val depot : FHisto(Date).tinfo -> t -> unit
val retrait : float -> t -> unit
val solde : t -> float
val releve :
(FHisto(Date).t -> (Date.t * float) list) -> t -> string list
val releveB : t -> string list
val releveC : t -> string list
end
# module
type
GESTION_BANQUE
=
sig
type
t
val
creer
:
float
->
float
->
t
val
depot
:
float
->
t
->
unit
val
retrait
:
float
->
t
->
unit
val
solde
:
t
->
float
val
releveB
:
t
->
string
list
end
;;
# module
GBanque
=
(Gestion:
GESTION_BANQUE
with
type
t=
Gestion.t)
;;
module GBanque :
sig
type t = Gestion.t
val creer : float -> float -> t
val depot : float -> t -> unit
val retrait : float -> t -> unit
val solde : t -> float
val releveB : t -> string list
end
# module
type
GESTION_CLIENT
=
sig
type
t
val
depot
:
float
->
t
->
unit
val
retrait
:
float
->
t
->
unit
val
solde
:
t
->
float
val
releveC
:
t
->
string
list
end
;;
# module
GClient
=
(Gestion:
GESTION_CLIENT
with
type
t=
Gestion.t)
;;
module GClient :
sig
type t = Gestion.t
val depot : float -> t -> unit
val retrait : float -> t -> unit
val solde : t -> float
val releveC : t -> string list
end
Pour que les comptes créés par un banquier puissent être manipulés
par un client nous avons utilisé la contrainte de type
Gestion.t dans la définition des modules GBanque et
GClient.