Les objets métier avec ModelMaker de Delphi 7

Henry Cesbron Lavau

- Titre : Les objets métier avec ModelMaker de Delphi 7

- Auteur : Henry Cesbron Lavau

- Parution : 17 septembre 2002

- Mise à jour : 26 avril 2020

- Licence : Copyright ® 2002 Henry Cesbron Lavau. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Dessin des classes avec ModelMaker

- III. De UML à SQL

- IV. Implémentation des objets métier

- V. Création automatique des déclarations des classes

- VI. Génération automatique de la base de données

- VII. Application de gestion

- VIII. Implémentation des règles métier

- IX. Conclusion

I - Introduction

Nous avons vu dans le tutoriel d'introduction à ModelMaker de Delphi 7 comment utiliser les capacités de modélisation de classes pour produire un code mieux organisé. Nous allons nous centrer maintenant sur les objets métier (en toute rigueur, on devrait parler de classes métier, mais objets métier fait maintenant partie du langage informatique courant).

II - Dessin des classes avec ModelMaker

Nous nous plaçons au moment où les classes ont été repérées (par exemple par la méthode CRC).

Pour illustrer notre propos, nous prendrons le cas d'une application de contacts.

Les contacts sont des personnes ou les entreprises auxquelles elles peuvent appartenir.
Pour chaque contact, on peut disposer de plusieurs numéros de téléphone et d'une adresse.

Les numéros de téléphone et les adresses sont des candidats de classes que l'on retrouvera dans la composition des autres classes.

Les propriétés communes à tous les contacts qu'ils soient des personnes ou des entreprises sont les numéros de téléphone et l'adresse. On fera donc descendre d'une classe contact les personnes et les entreprises. Ceci nous permettra de placer dans les classes descendantes les propriétés qui leur seront propres.

Le diagramme de classes construit dans ModelMaker se présente donc ainsi :

Nous y avons mis les relations d'héritage. Nous pourrions placer maintenant les propriétés des classes. Mais de manière à choisir l'outil ad hoc, nous allons réfléchir d'abord à la façon dont nous implémenterons ces propriétés.

III - De UML à SQL

Plaçons-nous par la pensée en fin de projet, au moment où tourne l'application.
Nos classes seront alors instanciées en objets : pour chaque contact nous aurons au moins son nom. Comment stocker nos objets de manière persistante ? On a pensé, il y a quelques années, que les objets seraient mieux stockés dans des bases de données spécialement conçues pour les objets.

Mais les bases de données orientées objet n'ont pas été adoptées par le marché. La raison en est sans doute d'une part la complexité de leur maniement (l'objet n'est pas encore la panacée dans les applications de gestion, même s'il progresse de jour en jour) et d'autre part les progrès importants effectués dans les bases de données SQL.

En effet, le SGBD doit être rapide, fiable et sûr. Le stockage des données implique de nombreuses servitudes (reprises automatiques sur incidents, disponibilité permanente, synchronisation de bases réparties pour n'en citer que quelques-unes). Les lourds investissements que requiert le développement de tels systèmes n'ont pu être amortis que par le large marché des bases de données classiques. Même parmi celles-ci, certaines ont disparu et l'on assiste à une concentration du secteur qui répond d'ailleurs au besoin général de standardisation.

Donc il se trouve qu'aujourd'hui que les SGBD les plus utilisés pour stocker des objets sont les SGBD classiques.

Mais il reste nécessaire, dans une approche objet, de prendre en compte l'héritage des objets à un moment ou un autre.

Soit l'héritage sera pris en compte dans l'application, soit dans le SGBD lui-même : les grands SGBD classiques ont inclus des extensions objet dans leurs dernières versions. Comme l'emploi de ces extensions n'est pas encore répandu (les évolutions des versions se font plus vite chez les éditeurs qu'en production dans l'entreprise) la tendance aujourd'hui est de mettre la gestion objet dans l'application avec le support externe de la base de données classique. Cela permet d'ajouter une application objet à un SGBD déjà opérationnel sans devoir en changer.

Pour implémenter une classe seule, il n'y a pas de problèmes. La classe contact pourrait être stockée ainsi :

Contact

Id

Name

-

-

-

-

Mais dès qu'il y a héritage se pose une question : pour stocker la table des personnes

Vaut-il mieux une table ayant toutes les propriétés, y compris les propriétés héritées

Person

Id

Name

DateOfBirth

-

-

-

10

Dupont

10/12/1978

ou deux tables, avec un pointeur de chaque rangée de Person vers la rangée correspondante de Contact

Contact

Id

Name

-

-

10

Dupont

Person

Contact.Id

DateOfBirth

-

-

10

10/12/1978

Dans le cas de une table, l'avantage est de se trouver en terrain connu : les requêtes SQL sont simples. Mais il y a au moins deux inconvénients majeurs qui apparaissent dès que l'on a plusieurs descendants de la table contact. Premier inconvénient : au niveau de l'analyse, toute modification de la structure de la classe parent Contact doit être répercutée dans toutes les tables d'implémentations des descendants. Deuxième inconvénient : au niveau de l'exploitation, pour constituer une liste nominative générale, il faut faire l'union de toutes les tables descendantes, et donc modifier les requêtes lorsque l'on ajoute un descendant.

Le cas de deux tables présente au moins deux avantages. Premier avantage : au niveau de l'analyse, nous collons à la structure d'héritage de nos classes et là où nous devions modifier toutes les tables descendantes, il nous suffit maintenant de modifier la seule classe parent. Deuxième avantage : au niveau de l'exploitation, pour constituer la liste nominative générale, une simple requête suffit. Reste cependant l'inconvénient d'avoir à chercher les données dans plusieurs tables : la plupart de nos requêtes seront des jointures. Ce qui pénalisera également les performances.

Compte tenu de la souplesse de plus en plus nécessaire (évoluer avant la concurrence) et du fait que les SGBD sont aujourd'hui très performants sur les jointures, on retiendra de préférence la solution à deux tables.

L'idéal serait de ne pas avoir à nous soucier des jointures lorsque nous voulons extraire un objet.

Si nous pouvions disposer d'un composant d'accès aux données auquel nous passerions simplement le nom de la classe et qui aille chercher l'information dans les différentes tables et nous la présente comme si elle venait d'une seule table, alors nous serions gagnants : les évolutions de structures sont simples ainsi que les requêtes d'extraction d'objets. C'est ce que nous allons voir maintenant.

IV - Implémentation des objets métier

Nous allons voir dans cette partie comment générer automatiquement la base de données à partir du modèle établi dans ModelMaker.

Pour cela, nous allons utiliser un composant développé par Seleqt. Ce composant s'installe dans Delphi et peut y être utilisé directement comme nous le ferons plus loin. Mais, et c'est ce qui nous intéresse ici, il fonctionne également comme Expert dans ModelMaker. Pour effectuer la suite, nous téléchargeons la version d'essai d'InstantObjects. L'installation est on ne peut plus simple. Elle a pour effet d'ajouter :

- dans Delphi, au menu Voir l'option Model Explorer, ainsi que l'onglet InstantObjects dans la palette ;

- dans ModelMaker, la classe TInstantObject, ainsi que l'icône A+ de propriété persistante (Attribut).

Les classes que nous voulons rendre persistantes vont donc descendre de TInstantObject.
Pour cela, nous affichons la hiérarchie des classes (F3), et par glisser-déplacer, nous amenons les classes sur la classe TInstantObject. Le schéma prend alors l'allure suivante :

Le symbole désigne maintenant leur persistance.

Nous pouvons maintenant ajouter les attributs de nos classes, avec les valeurs suivantes :

Cliquer sur Phone dans le diagramme. Puis sur l'icône d'attribut en bas à gauche.
Au lieu de la fenêtre de propriété habituelle dans ModelMaker, nous avons l'éditeur d'attribut qui est géré par InstantObjects.

À noter qu'il faut toujours indiquer la taille maximum des chaînes persistantes.
Répétez l'opération pour les autres attributs.

Cependant pour la classe TContact, nous avons à indiquer que deux objets la composent (en plus de Name) : vous ne saisirez pas l'index PhoneCount, qui sera mis automatiquement, mais Phones de type Parts renvoyant à l'Object Class existante TPhone.
De même, Address est de type Part et renvoie à TAddress.

Dans TCompany, il y a une simple Reference de ContactPerson à TPerson.

Une fois la saisie terminée, revenez sur le diagramme de classe que nous avions tout au début.
Cliquez dedans et faites Ctrl+A pour sélectionner toutes les classes. Puis cliquez droit dedans pour activer parmi les Wizards celui qui s'appelle Visualize Class Relations. Si votre saisie des attributs s'est faite correctement, vous devez obtenir le diagramme suivant :

Vous voyez :

Les relations d'héritage

Les relations de composition, avec ou sans index qui indiquent que la classe visée fait nécessairement partie de la classe d'où part la flèche

et les relations d'agrégation qui indiquent que la classe visée peut faire partie de la classe d'où part la flèche.

La composition, appelée Part(s) dans l'éditeur d'attribut, est une agrégation forte :
les données des objets de la classe visée seront implémentées dans la table de la classe composée.
Donc la suppression d'un contact (Contact) supprimera ses téléphones (Phone) et son adresse (Address).

En revanche, l'implémentation de l'agrégation, appelée Reference(s) dans l'éditeur d'attribut, se fera dans une table distincte : la suppression d'une société (Company) ne supprimera pas la personne (Person), mais seulement la référence à la personne (ContactPerson).

V - Création automatique des déclarations des classes

Avant de générer le code, assurons-nous que les classes sont bien dans l'ordre qu'impose Delphi : TAddress et TPhone doivent précéder TContact. Si tel n'est pas le cas, veuillez effectuer les glisser-déplacer nécessaires dans la fenêtre montrant les classes dans les Units (F4) en haut à gauche de l'écran.

Nous allons associer l'unité Model à nos classes.
Pour cela, nous affichons dans ModelMaker la fenêtre des unités (F4).
Nous cliquons dans cette fenêtre située à gauche en haut. Puis, sur la touche Inser pour insérer une unité. Dans la fenêtre qui s'ouvre alors, nous indiquons le chemin et le nom de l'unité à créer.
Puis nous faisons passer à droite les classes que nous venons de créer. Si nous cochons Auto Generation Enabled, alors, quand nous fermons la fenêtre par Ok, l'unité est créée : elle contient les déclarations de nos classes, comme nous pouvons le constater en ouvrant l'unité dans Delphi.

TAddress = class (TInstantObject)
{  City: String(30);
PostalCode: String(8);
Street: String(30); }
_City: TInstantString;
_PostalCode: TInstantString;
_Street: TInstantString;
private
function GetCity: string;
procedure SetCity(const Value: string);
function GetPostalCode: string;
procedure SetPostalCode(const Value: string);
function GetStreet: string;
procedure SetStreet(const Value: string);
published
property City: string read GetCity write SetCity;
property PostalCode: string read GetPostalCode write SetPostalCode;
property Street: string read GetStreet write SetStreet;
end;

Nous remarquons plusieurs choses de bas en haut.

Les propriétés sont published. En effet, ceci permettra à InstantObjects d'exploiter les RTTI (Run Time Type Information) qui permettront à l'exécution d'avoir des informations sur les types des objets.

Ensuite, nous trouvons des champs dont le nom commence par un tiret souligné : situées sous la classe, avant toute précision de portée, elles sont donc published, conformément aux stipulations du langage Delphi. Contrairement aux objets non persistants dont les champs sont ce qu'il y a de plus privé, les champs sont published, de manière à pouvoir être détectés par RTTI en vue de la mise en base. Bien entendu, l'utilisation de ces champs dans l'application doit être exclue : il faudra s'en tenir aux propriétés de la classe. Leur type appartient aux types prédéfinis par InstantObjects : TInstantString par exemple, et sont donc enregistrables dans la base puisque qu'ils descendent de TInstantStreamable lui-même descendant de TPersistent.

Nous trouvons enfin, tout en haut, un commentaire : il s'agit d'informations qui sont lues dans l'IDE de Delphi par le Model Explorer (menu Voir) auxquelles il ne faut pas toucher. Comme l'unité Model n'est pas visuelle et ne comporte pas d'unité .DFM associée, c'est là qu'InstantObjects conserve et relie les attributs de la classe en cours de conception. Cela permet, en plus du type, d'indiquer par exemple des masques de saisie.
Lorsque l'objet est conservé dans une table, il est dit Stored et le commentaire commence par { stored;, sinon, l'objet est dit Embedded et le commentaire commence immédiatement par le premier champ.

C'est grâce à ces indications qu'InstantObjects va générer la base automatiquement.

VI - Génération automatique de la base de données

Nous choisissons de mettre les données dans InterBase.

Note : en standard, InstantObjects propose aujourd'hui BDE et ADO. dbExpress est prévu dans la prochaine version.

Nous devons d'abord créer le fichier au travers du SGBD.
Ici, nous utilisons IBConsole pour faire un Create Database.

C'est maintenant que va intervenir l'option Model Explorer du menu Voir.

Nous cliquons sur la première icône pour sélectionner les units où InstantObjects va recueillir les classes :

Après validation, les classes apparaissent dans la fenêtre, avec leurs relations.
Nous cliquons alors sur la deuxième icône pour créer une connexion à la base de données.
En cliquant droit dans la fenêtre, nous choisissons la connexion IBX que nous appellerons BO IBX.

Une fois le nom inscrit dans la fenêtre, nous cliquons droit dessus pour éditer la connexion et indiquer le nom de la base que nous avons créée avec IBConsole.

C'est maintenant que nous allons obtenir tout le bénéfice de ce travail : si nous avons bien saisi classes et propriétés, il suffit d'appuyer sur Build pour que la base de données soit instantanément générée.

Nous pouvons aller dans IBConsole pour vérifier que nos tables sont bien créées.

Note : sous certaines versions d'IBConsole, les noms contenant des minuscules doivent être entre guillemets. Mais IBConsole ne suit pas lui-même cette règle quand on demande l'affichage des données par l'onglet Data. Nous nous contenterons donc d'y vérifier les structures des tables.

Il y a trois tables :

Company

Class

VARCHAR(32)

Id

VARCHAR(32)

UpdateCount

Integer

ContactPersonClass

VARCHAR(32)

ContactPersonId

VARCHAR(32)

Contact

Class

VARCHAR(32)

Id

VARCHAR(32)

UpdateCount

Integer

Address

Blob

Name

VARCHAR(30)

Phones

Blob

Person

Class

VARCHAR(32)

Id

VARCHAR(32)

UpdateCount

Integer

DateOfBirth

TIMESTAMP

Bien que la puissance des InstantObjetcs nous affranchira des détails d'implémentation des objets de la base, arrêtons-nous un moment sur ces détails : cela pourra être utile par la suite pour utiliser la base en dehors du composant, comme dans un générateur d'états par exemple.

Chaque classe commence par le même entête :

- le nom de la classe Class ;

- un numéro d'identification global unique (GUID) Id ;

- un compteur de mise à jour UpdateCount.

Le nom de la classe est utile : par exemple Contact contient la partie commune à tous les objets qui en descendent. De savoir si une rangée de Contact porte sur TCompany ou TPerson permettra un traitement plus efficace de la table puisqu'il indiquera à InstantObjects dans quelle table aller chercher le complément de la rangée.

On constatera également qu'il n'y a pas de table Phone ni de table Address : les objets correspondants sont en effet stockés à l'intérieur des classes dans la composition desquelles ils interviennent.

En revanche, il y a une table Person parce que nous avons la possibilité d'enregistrer des personnes directement. Dans la table Company, nous avons seulement ContactPersonId qui pointe vers la rangée correspondante de la table Person où se trouvent les données de la personne.

Note : ContactPersonClass y est également stocké pour permettre un traitement plus efficace des sous-requêtes par InstantObjects.

Revenant au Model Explorer, il reste une 3e icône avec que vous pouvez explorer, mais qui ne nous est pas utile, dans la mesure où nous construisons nos classes avec ModelMaker.

Nous pouvons donc passer maintenant à l'application.

VII - Application de gestion

Lors de l'installation d'InstantObjects, l'onglet suivant a été ajouté à la palette :

Voyons comment nous pouvons en utiliser les composants.
Sur une forme, nous plaçons les composants d'accès à la base de données : IBDataBase et IBTransaction.

Pour une initiation à InterBase avec Delphi, voyez ce tutoriel.

Nous plaçons un DBGrid et son DataSource. Le DataSource est relié à l'IBDataBase par un InstantIBXConnector qui va apporter la logique InstantObjects et par un InstantSelector qui nous permettra d'indiquer la requête d'interrogation de la base.

Dans la propriété Command de l'InstantSelector, nous indiquons à partir de quelle table nous voulons que cet objet soit extrait. Choisissons TContact et ses descendants : la requête est alors construite automatiquement :

SELECT * FROM ANY TContact

Note : les requêtes sont construites en IQL inspiré d'OQL de l'ODMG.

Vous pouvez télécharger les sources et le modèle.
Inspiré par la démo de InstantObjects, j'ai construit le modèle en Model Maker, modifié l'interface et ajouté les règles métier :

VIII - Implémentation des règles métier

Nous disposons maintenant d'objets métier dans lesquels nous allons pouvoir implémenter les règles de gestion. Pour bien organiser cette implémentation, nous allons créer un descendant direct de TInstantObject que nous appellerons TBusinessObject. Tous nos objets de gestion en hériteront :

Cette nouvelle classe TBusinessObject va permettre une gestion simple des règles métier au niveau de chacun des descendants :
il leur suffit de préciser à quelles conditions l'objet est valide.

Par exemple, pour la classe Phone, nous aurons :

function TPhone.IsValid: Boolean;
begin
Result := inherited IsValid;
if not (Length(Number) >= 8) then
Regle := 'Le numéro de téléphone doit comprendre au moins 8 chiffres';

Result := ReglesValides;
end;

pour la classe Contact, nous aurons :

function TContact.IsValid: Boolean;
var
i: Integer;
begin
Result := inherited IsValid;
if not (Name <> '') then
Regle := 'Il faut saisir le nom';

for i := 0 to PhoneCount - 1 do
if not Phones[i].IsValid then
Regle := Phones[i].Regle;

if Assigned(Address) then
if not Address.IsValid then
Regle := Address.Regle;

Result := ReglesValides;
end;

et pour la classe Person :

function TPerson.IsValid: Boolean;
begin
Result := inherited IsValid;
if not ((DateOfBirth = 0) or
(YearOf(Date) - YearOf(DateOfBirth) in [18..75])) then
Regle := 'Il faut avoir entre 18 et 78 ans';

Result := ReglesValides;
end;

Chaque règle est énoncée sous la forme :

if not (expression logique de la règle) then
Regle := 'expression textuelle de la règle';

La beauté de cette implémentation est que chaque règle est contenue dans l'objet qui la concerne.

Nous n'avons plus à répéter des conditions dans plusieurs événements du TDataSet (ni a fortiori, dans les événements de l'interface utilisateur : boutons, zones).

L'implémentation que je vous propose se situe au niveau de l'objet, mais on peut également la faire au niveau de chaque attribut de l'objet. Les règles non observées par l'objet se concatènent dans la propriété Regle (le SetRegle a été écrit en conséquence). Voyez le code source.

TBusinessObject déclenche lui-même la vérification avant tout enregistrement d'objet.
Il affichera, si nécessaire, un message listant les règles non observées par l'objet.

Nous réalisons ainsi notre objectif : un objet à part entière, enregistré dans une base de données classique.

IX - Conclusion

Dans ce tutoriel, avec l'aide de ModelMaker et d'InstantObjects, nous avons vu comment construire des objets métier. La simplicité et la robustesse des développements obtenus nous permettent maintenant de changer de niveau et de nous concentrer sur l'essentiel du métier.

Pour aller plus loin, consultez : ModelMaker de Delphi 7, si vous ne l'avez déjà lu.