Précédent Index Suivant

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()) -. 864000.)) 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 :
  1. définition d'une structure par application du foncteur FGestion aux paramètres.
  2. déclaration d'une signature indiquant les fonctions accessibles dans chacun des cas.
  3. 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.








Précédent Index Suivant