Delphi Business Objects - John COLIBRI. |
- résumé : Objets métier et Règles métier en Delphi. Présentation d'un utilitaire de génération pour faciliter l'utilisation d'objets métier
- mots clé : Objets métier - Règles métier - Business Objects - Business
Rules - gestion - bases de données
- logiciel utilisé : Windows XP personnel, Delphi 6.0
- matériel utilisé : Pentium 2.800 Mhz, 512 Meg de mémoire, 250 Giga disque dur
- champ d'application : Delphi 1 à 2006, Turbo Delphi sur Windows
- niveau : développeur Delphi
- plan :
1 - Introduction
Les Objets métier on pour but de fournir un mécanisme qui permet de forcer le respect de certaines règles de gestion. Si une société souhaite que toutes les factures aient un montant de plus de 15 Euros, il est souhaitable que les
logiciels utilisés fassent respecter cette règle.
2 - Quelques solutions existantes Voici quelques solutions pour utiliser des objets métier ou des règles métier en Delphi
2.1 - Raize: crée des composants Ray KONOPKA fut un des premier à proposer une solution dans son livre sur la création de composants Delphi, et dans ses produits commerciaux (Raize).
Elle consiste à créer un composant sur la palette pour chaque table. Ce composant contient les champs statiques, plus les règles métier (codés dans les événements OnSetText, OnValidate, OnBeforePost des composants d'accès du
type tDataSet). La création de ces composants est réalisée par un expert Quelques remarques: - avantage: tout est bien encapsulé par table. Le point de vue technique est très simple
- incovénient:
- il faut acheter son wizard (pas publié dans son livre)
- la palette devient vite surchargée de nouveaux composants. Pour peu que vous gériez plusieurs applications pour des clients différents, l'inflation devient importante
2.2 - RemObjects Alessandro FREDERICI (RemObjects) proposent un outil de développement complet: - cet outil est indépendant du serveur (il contient une couche qui interface
avec IbExpress, Oracle Direct Access etc)
- un expert propose la création de tDataSet, avec des champs provenant d'un dictionnaire de champs, et des règles (séparées pour les client et le
serveur) pour la partie métier. Cet expert gère les affichages, les champs, les règles métier
Et: - avantage: cet outil évite un tDataModule gigantesque ou la surcharge de la Palette
- inconvénient:
- il faut acheter leur mécanique
- l'outil semble assez lourd, avec sa logique propre qu'il faut maîtriser
2.3 - Les couches de persistence objet
Depuis l'avénement des objets, grande fut la tentation de réaliser les applications de gestion qui utiliseraient des objets en mémoire (un objet "client", un objet "facture", un objet "article" etc), en ajoutant au logiciel
une couche qui se charge de communiquer avec le moteur SQL (création des objets lors du SELECT, écriture automatique des modification par génération de requêtes UPDATE )
Ces couches de persistence objet ne recueillent pas l'approbation de tout le monde, à cause de la lourdeur du mapping intermédiaire entre les objets et les tables d'un moteur SQL. Mais il existe de nombreux articles, librairies Open Source, produits
commerciaux qui offrent des solutions de persistence - les articles les plus connus sont ceux de Scott AMBLER
- les premiers codes source Delphi et articles publiés furent ceux de explicatifs de Philipp BROWN
- instant_objects et tIoPf sont deux librairies en source, mais avec une optique plus industrielle et moins pédagogique
- Bold est une société qui réalisa un outil intégré à Delphi, avec des
tutoriaux, et fut par la suite rachetée par Borland pour devenir ECO
2.4 - Utilisation de tClientDataSet Wayne NIDDERY, un dévelolpeur bien connu en Delphi fit remarquer que:
- le flux des données DOIT passer par la couche métier. Par conséquent les contrôles visuels doivent être connectés aux objets métier, qui, eux, contiennent les tDataSets
- propose de réaliser la couche métier en utilisant des tClientDataset
- ils peuvent avoir des champs non directement liés à des champs physiques de tables du serveur SQL
- is assurent la communication avec le Serveur
- ils permettent de conserve la mécanique d'affichage avec des contrôles visuels intacte
Mentionnons qu'à l'arrivée de Midas / DataSnap / dbExpress, nous avions pensé que les paquets de "contraintes" permettraient de faire voyager les règles métier depuis le Serveur vers les Clients. Mais en fait, ces
contraintes sont du type Min et Max, et n'offrent donc pas, à notre avis, toute la richesse que nous pourrions attendre de véritables règles métier.
2.5 - Objets distribués
Depuis l'arrivé de .Net, une nouvelle solution s'appuyant sur la couche d'objets distribués (Remoting) a été proposée. Un livre complet de Rockford LHOTKA a même été écrit sur le sujet. Parmi les arguments:
- Remoting est le seul moyen d'envoyer vers les clients des "règles métier". En utilisant des contrôles visuels, il y a toujours un risque de violer les règles
La technique d'objets distribués semble donc à ce jour une très bonne technique pour fournit au Client de véritables objets contenant à la fois les données et le code de validation. Mais il faut mettre en oeuvre le Remoting .Net, qui
n'est pas utilisable partout.
2.6 - L'analyse du problème 2.6.1 - Emplacement des règles métier Nous sommes tiraillés entre deux extrêmes - la seule solution pour garantir le respect des règles est de les nicher au
niveau du Serveur SQL: personne ne peut écrire ou modifier des données qui ne seraient pas conformes au Triggers qui mettent en oeuvre les règles. Au niveau ergonomie et efficacité cependant, cela oblige un voyage aller
retour, et les violations ne sont détectées et signalées que lors de ces transferts. Les règles au niveau de champs individuels, ou de plusieurs lignes (débit / crédit), ou pour des écriture retardées (BatchUpdate,
travail en mode nomade) ne sont pas possibles
- la solution proche de l'application Client est à l'autre extrémité du spectre: chaque Client utilise des composants qui effectuent la validation
au niveau des applications utilisateur. Les risques sont nombreux:
- certains Clients peuvent court-circuiter ces couches de validation,
- les objets doivent être incorporés, ou pire reécrits pour chaque application
- les règles sont difficile à maintenir en cas de changement
Utiliser des Triggers sur le Serveur ne pose aucun problème au niveau Delphi, et nous n'en dirons pas plus ici.
Concentrons nous sur les règles métier mises en oeuvre au niveau des applications Client
2.6.2 - Règles métier Client Nous souhaitons utiliser des composants liés au composants d'accès aux données
et qui comporteraient une logique métier. Pour simplifier, nous supposerons que notre composant d'accès aux données est un tIbQuery. Parmi les options: - doter le tIbQuery de code de validation. C'est la solution usuelle. Nous
pouvons centraliser ce composant sur un tDataModule, mais nous ne pouvons pas facilement réutiliser ce composant dans d'autres projets. Et nous ne pouvons pas dériver de ce composants différentes variantes qui seraient
adaptées en fonction des applications
- créer un composant dérivé de tIbQuery et le placer sur la Palette: c'est la solution de KONOPKA avec sa multiplication de composants sur la Palette
- utiliser un outil (un Expert, un générateur) qui crée les descendants de tIbQuery. Parmi les inconvénients:
- ce composant ne peut être manipulé par l'Inspecteur d'Objet (c'est un composant créé par code)
- toute modification (correction d'erreur, modifications etc) nécéssite une nouvelle génération
- comme ce nouveau composant n'est pas sur la Palette, nous ne pouvons pas non plus le placer sur un tDataModule
Malgré ses inconvénients, nous allons présenter une solution utilisant un générateur
3 - Objets métier Delphi
3.1 - Le fonctionnement Notre générateur fonctionne ainsi:
- le développeur sélectionne une table de sa base. Supposons que ce soit ORDER
- le générateur créé
- la classe c_ORDER, qui descend de tIbDataSet, et qui contient
- les champs persistants
- les règles métiers, codés dans des événements du tDataSet ou des tFields
- les traitements généraux sur la table (trouver le dernier numéro de commande, faire le total de la commande ... )
- la classe c_ORDER_module qui représente le "datamodule". Comme nous ne pouvons pas placer des descendants de notre c_ORDER sur un tDataModule, autant utiliser une classe pure (non dérivée de tForm).
Le module contient:
- un constructeur, qui utiliser la SELECT pour la table
- un composant d'accès c_ORDER et une source c_ORDER_data_source pour synchroniser tous les contrôles visuels
- à titre de démonstration, une Forme u_f_edit_ORDER, qui contient une dbGrid et un dbEdit, et qui sert de démonstration de l'importation des deux classes précédentes
- il peut aussi générer un projet complet (.DPR, .PAS, .DPR, .DOF) qui contient un bouton pour chaque Table et qui permet d'afficher les différentes Form de démonstration
Les projets qui utiliseront ces classes
- soit utiliseront directement un c_xxx_module
- soit prépareront un conteneur qui leur est propre et qui utilisera, ou héritera des c_xxx_modules de base
Notre exemple va utiliser la base MastApp qui est fournie comme base de démonstration avec Delphi. Nous avons documenté ici la structure et
le code de cette base de données (mais le générateur peut être utilisé avec une autre base que MastApp).
3.2 - Architecture des objets générés Le diagramme de classe UML du résultat est le suivant:
Nous allons présenter les différentes parties pour les articles (PARTS), car il s'agit de la Table ayant le moins de champs (donc les exemples les plus courts).
3.3 - L'objet métier c_PARTNO Voici le code de l'objet de gestion des articles (PARTS)
unit u_c_parts; interface
uses Classes, Db, IbDatabase, IBCustomDataSet;
type c_parts= Class(tIbDataSet)
PARTNO_: tFloatField;
VENDORNO_: tFloatField;
DESCRIPTION_: tStringField;
ONHAND_: tFloatField;
ONORDER_: tFloatField;
COST_: tFloatField;
LISTPRICE_: tFloatField;
constructor create_parts(p_c_owner: tComponent;
p_c_ib_database: tIbDatabase; p_select_sql: String);
procedure open_dataset();
procedure after_open(p_c_dataset: tDataset);
procedure before_edit(p_c_dataset: tDataset);
procedure before_post(p_c_dataset: tDataset);
procedure partno_validate(p_c_field: tField);
end; // c_parts
implementation uses u_dm_database;
// -- c_parts
Constructor c_parts.create_parts(p_c_owner: tComponent;
p_c_ib_database: tIbDatabase; p_select_sql: String);
begin
Inherited Create(p_c_owner);
DataBase:= p_c_ib_database;
SelectSql.Text:= p_select_sql;
ModifySql.Text:= 'UPDATE parts'
+ ' SET'
+ ' VENDORNO= :VENDORNO,'
+ ' DESCRIPTION= :DESCRIPTION,'
+ ' ONHAND= :ONHAND,'
+ ' ONORDER= :ONORDER,'
+ ' COST= :COST,'
+ ' LISTPRICE= :LISTPRICE'
+ ' WHERE'
+ ' PARTNO = :OLD_PARTNO';
InsertSql.Text:= 'INSERT INTO parts'
+ ' (PARTNO,VENDORNO,DESCRIPTION,ONHAND,ONORDER,COST,LISTPRICE)'
+ ' VALUES'
+ ' (:PARTNO,:VENDORNO,:DESCRIPTION,:ONHAND,:ONORDER,:COST,:LISTPRICE)';
DeleteSql.Text:= 'DELETE FROM parts'
+ ' WHERE'
+ ' PARTNO = :OLD_PARTNO';
AfterOpen:= after_open;
// -- hook the events
BeforeEdit:= before_edit;
BeforePost:= before_post;
end; // create_parts
procedure c_parts.open_dataset(); begin
Open; end; // open_dataset
procedure c_parts.after_open(p_c_dataset: tDataset);
var l_field_index: Integer; begin
// -- check which columns are included, and link them to our attributes
for l_field_index:= 0 to FieldCount- 1 do
with FieldDefs[l_field_index] do
if Name= 'PARTNO'
then PARTNO_:= Fields[l_field_index] as tFloatField else
if Name= 'VENDORNO'
then VENDORNO_:= Fields[l_field_index] as tFloatField else
if Name= 'DESCRIPTION'
then DESCRIPTION_:= Fields[l_field_index] as tStringField else
if Name= 'ONHAND'
then ONHAND_:= Fields[l_field_index] as tFloatField else
if Name= 'ONORDER'
then ONORDER_:= Fields[l_field_index] as tFloatField else
if Name= 'COST'
then COST_:= Fields[l_field_index] as tFloatField else
if Name= 'LISTPRICE'
then LISTPRICE_:= Fields[l_field_index] as tFloatField else
// -- hook the field_events
partno_.OnValidate:= partno_validate;
end; // after_open_dataset
procedure c_parts.before_edit(p_c_dataset: tDataset);
begin end; // before_edit
procedure c_parts.before_post(p_c_dataset: tDataset);
begin // -- get next key
if State= dsInsert
then PARTNO_.Value:= dm_database.f_get_next_key('parts_generator');
end; // before_post
procedure c_parts.partno_validate(p_c_field: tField);
begin end; // partno_validate
end. | Notez que: - au niveau de la déclaration:
- nous avons généré un attribut par colonne, afin de pouvoir directement
accéder aux données sans utiliser Fields[nnn] of FieldByName('nnn')
- toutes les classes c_nnn comportent toujours:
- la classe comporte quelques méthodes "typiques":
- before_open(), before_edit() etc
- partno_validate() pour vérifier le contenu du champ PARTNO avant un Post()
Ce sont quelques méthodes typiques que nous avons demandées depuis
l'interface utilisateur de notre générateur. Nous pourrions en demander d'autres - au niveau du code
- nous supposons que l'application qui utilisera nos objets métier
comportera un tDataModule contenant les informations pour la connection. Dans notre cas, un tIbDataBase correctement paramétré
- le CONSTRUCTEUR reçoit une requête SELECT qui sert à initialiser
la propriété SelectSql de notre descendant de tIbDataSet. Nous initialisons de même les autres requêtes (ModifySql, InsertSql et DeleteSql). C'est d'ailleurs à cause de la présence dans la CLASSE
tIbDataSet de toutes ces 4 requêtes que nous avons choisi ce composant plutôt qu'un IbQuery (qui nécessiterait la présence d'un tIbUpdateSql).
- nous faisons aussi pointer les événements que nous avons décidé
d'utiliser (BeforeEdit) vers les méthodes qui figurent dans la CLASSe
- l'utilisateur de notre objet métier n'est pas obligé de travailler sur toutes les colonnes de la Table. Si le SELECT qu'il choisit ne
comporte que les colonnes PARTNO, VENDERNO et DESCRIPTION, il faut n'initialiser que les attributs correspondants de notre CLASSe. C'est ce que fait la méthode AfterOpen(): elle teste si un champ est présent
dans tIbDataSet.FielDefs, et initialise le champ vers le tIbDataSet.Fields[] correspondant. Notez que cette gymnastique n'est là
que pour éviter d'utiliser c_PARTS.Fields[] or c_PARTS.FieldByName()
- BeforePost() montre un exemple d'utilisation de colonne: nous appelons
directement PARTNO.Value. De plus nous supposons que la base SQL contient un générateur Interbase, et BeforePost() est utilisé pour récupérer la nouvelle valeur de la clé de la Table
De plus:
- nous avons suffixé chaque attribut champ par un "_". Ceci évite de les confondre avec le nom des colonnes. Dans une requête, la colonne est DESCRIPTION, dans une instruction Delphi le champ est DESCRIPTION_. Ce
suffixe est optionnel (l'interface de notre générateur permet de l'ajouter ou non)
- les événements à inclure dans la CLASSe sont pilotés par l'interface de notre générateur
- l'utilisation de tIbDataSet est un choix pour notre exemple. Mais un autre composant pourrait remplacer celui-ci
3.4 - Le DataModule Voici le code de notre tDataModule:
unit u_c_parts_module; interface
uses Classes, Db, IBCustomDataSet, u_c_parts;
type c_parts_module= Class(tComponent)
_parts: c_parts;
_parts_datasource: tDataSource;
constructor create_parts_module(p_c_owner: tComponent;
p_select_sql: String);
procedure _open_dataset();
procedure _close_dataset();
end; // c_parts_module
implementation uses u_dm_database;
// -- c_parts_module
Constructor c_parts_module.create_parts_module(p_c_owner: tComponent;
p_select_sql: String); begin
Inherited Create(p_c_owner);
_parts:= c_parts.create_parts(Self, dm_database.IbDataBase1, p_select_sql);
// -- create and link a datasource
_parts_datasource:= tDataSource.Create(Self);
_parts_datasource.DataSet:= _parts;
end; // create_parts_module
procedure c_parts_module._open_dataset(); begin
_parts.open_dataset();
end; // _open_dataset
procedure c_parts_module._close_dataset(); begin
_parts.Close();
end; // _close_dataset end. | Notez que:
- la CLASSe contient note objet métier c_PARTS ainsi qu'un tDataSource
- le CONSTRUCTOR créé ces deux objets
Ici aussi, les préfixes "_" devant le nom de l'objet métier et du tDataSource
sont optionnels
3.5 - Un exemple de Forme Voici le source de c_f_edit_PARTS:
unit u_f_edit_parts; interface
uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls,
, Forms, Dialogs, StdCtrls, Mask, ExtCtrls, DBCtrls, Grids, DBGrids
, DB, IBCustomDataSet
, u_c_parts, u_c_parts_module ;
type Tedit_parts_form= class(TForm)
DBGrid1: TDBGrid;
DBNavigator1: TDBNavigator;
db_PARTNO_edit: TDBEdit;
procedure FormCreate(Sender: TObject);
private public
m_c_parts_module: c_parts_module;
// -- references
parts_ref: c_parts;
procedure initialize_local_references;
procedure link_controls;
procedure open_dataset;
procedure close_dataset;
procedure open_at(p_PARTNO: String);
end; // Tedit_parts_form
var edit_parts_form: Tedit_parts_form; implementation
uses u_dm_database; {$R *.dfm}
procedure Tedit_parts_form.FormCreate(Sender: TObject);
begin m_c_parts_module:=
c_parts_module.create_parts_module(Self, 'SELECT * FROM parts');
end; // FormCreate
procedure Tedit_parts_form.initialize_local_references;
begin
with m_c_parts_module do
begin parts_ref:= _parts;
end; // with m_c_parts_module
end; // initialize_local_references
procedure Tedit_parts_form.link_controls; begin
with m_c_parts_module do
begin // -- link all the db_xxx datasources
db_PARTNO_edit.DataSource:= _parts_datasource;
dbGrid1.DataSource:= _parts_datasource;
dbNavigator1.DataSource:= _parts_datasource;
end; // with m_c_parts_module do
end; // link_controls
procedure Tedit_parts_form.open_dataset; begin
m_c_parts_module._open_dataset();
initialize_local_references; link_controls;
end; // open_dataset
procedure Tedit_parts_form.close_dataset; begin
parts_ref.Close();
end; // close_dataset
procedure Tedit_parts_form.open_at(p_PARTNO: String);
begin open_dataset();
parts_ref.Locate('PARTNO', p_PARTNO, []);
ShowModal; close_dataset();
end; // open_at end. |
et la Forme correspondante est la suivante: Notez que: - la Forme contient un champ c_PARTS_module, qui contient notre objet métier.
- pour éviter d'avoir à référencer l'objet métier à travers son tDataModule:
m_c_PARTS_module._parts.Value := ...
nous avons créé, dans la Forme, un alias parts_ref. Ceci permettrait d'écrire: parts_ref.Value := ... Cette référence est initialisée dans la méthode initialize_local_references()
- nous créons un c_PARTS_datamodule dans l'événement OnFormCreate
- les différentes initialisations ont lieu lors de l'ouverture de la Table:
- l'appel à m_c_PARTS_module initialise les champs de c_PARTS
- les références locales (parts_ref dans notre cas) initialise la référence locale à l'objet métier
- les contrôles visuels (dbGrid, dbEdit, dbNavigator sont reliés à notre Table
Et: - l'utilisation du suffixe "_ref" dans PARTS_ref est optionnelle
- nous aurions pu placer les initialisation dans Tedit_PARTS_form.open_dataset, plutôt que de créer des méthodes séparées
- la méthode open_at() est une fonction qui figure dans MastApp() et que
nous avons reproduite ici, pour l'exercice
3.6 - Le projet de test Pour tester notre générateur, nous générons en plus un projet complet, dont la
fenêtre principale permet d'ouvrir les Formes d'édition décrites ci-dessus. Voici un exemple d'exécution, en ouvrant la table EMPLOYEE:
Et le code correspondant ouvre simplement notre Forme d'édition:
procedure Ttest_form.employee_Click(Sender: TObject);
begin with edit_employee_form do
begin open_dataset(); ShowModal;
edit_employee_form.close_dataset();
end; // with edit_employee_form end; // employee_Click
|
4 - Le générateur d'objets métier 4.1 - Le Générateur Delphi Pour créer nos objets métier, nos DataModules, Formes et projets de test, la
voie la plus simple est la suivante: - écrire un exemple de ce qu'il faut générer
- prendre le source et écrire le code qui placera dans une tStringList le
même texte (les .PAS, le .DFM, le .DPR, et même le fichier d'option Delphi .DOF)
La génération est facilitée par une CLASSe qui gère l'indentation du code généré. Voici la définition de cette CLASSe:
c_generator= class(c_basic_object)
m_c_result_list: tStringList;
m_current_line: String;
m_indentation: Integer;
Constructor create_generator(p_name: String);
function f_c_self: c_generator;
procedure clear_generator;
procedure add_text(p_text: String);
procedure add_line(p_text: String);
procedure add_new_line;
procedure indent_add_line(p_text: String);
procedure unindent_add_line(p_text: String);
procedure add_line_indent(p_text: String);
procedure add_line_unindent(p_text: String);
procedure append_generator_lines(p_c_generator: c_generator);
procedure append_generator_lines_indented(p_c_generator: c_generator;
p_indentation: Integer);
procedure save_to_file(p_full_file_name: String);
Destructor Destroy; Override;
end; // c_generator |
Ce sont les méthodes indent_add_line() et unindent_add_line() qui modifient l'indentation avant d'ajout de texte, et add_line_indent() ainsi que add_line_unindent() qui la modifient après l'ajout de texte.
A titre d'exemple, pour générer la procédure suivante d'ouverture d'un tDataSet:
procedure c_parts.open_dataset(); begin
Open; end; // open_dataset | nous utilisons:
procedure generate_open_body; begin
add_line('');
add_line_indent('procedure '+ m_class_name+ '.open_dataset();');
add_line_indent('begin'); add_line('Open;');
Dec(m_indentation, 2); add_line_unindent('end; // open_dataset');
end; // generate_open_body |
4.2 - Les étapes du générateur
Pour générer tous les objets métier d'une Base, il faut: - lister le nom des Tables de la base
- pour chaque table "xxx" choisie par l'utilisateur
- générer le composant métier c_xxx
- générer le module c_xxx_module
- générer une tForm U_F_EDIT_xxx.PAS et U_F_EDIT_xxx.DFM
- pour toutes les tables ainsi générée, générer le projet de test qui ouvrira ces Formes
4.2.1 - Lister les TABLES d'une base Le générateur commence par présenter le nom des tables d'une application. Pour cela il utilise un composant IbExtract, et récupère dans les lignes commençant
par CREATE TABLE le nom des Tables. Cette technique a déjà été présentée dans l'article Extraction de Script SQL. Voici le code:
procedure TForm1.tables_Click(Sender: TObject);
var l_item: Integer;
l_index: Integer;
l_the_line, l_table_name: String; begin
IbDatabase1.Open;
with IBExtract1 do begin
ExtractObject(eoTable, '');
with Items do
for l_item:= 0 to Count- 1 do
begin
display(Strings[l_item]);
l_the_line:= Strings[l_item];
if Pos('CREATE TABLE', l_the_line)> 0
then begin
l_index:= 1;
f_string_extract_non_blank(l_the_line, l_index);
f_string_extract_non_blank(l_the_line, l_index);
l_table_name:= f_string_extract_non_blank(l_the_line, l_index);
Listbox1.Items.Add(l_table_name);
end else
if POS('PRIMARY KEY', l_the_line)> 0
then begin
// display('*** PRIM');
end;
end; // with Items
end; // with IBExtract1 end; // tables_Click |
4.2.2 - Génération de l'Objet Métier et son module Une fois que l'utilisateur a sélectionné un nom de tables, par exemple EMPLOYEE, nous pouvons calculer le noms des fichiers, des unités, des classes
etc (c_EMPLOYEE qui sera dans U_C_EMPLOYEE.PAS, le module sera c_EMPLOYEE_module et ainsi de suite) Pour les classes métier, nous avions utilisé au début une génération simultanée
de la définition et des méthodes. Cette solution se révéla difficile à maintenir, et le code du générateur actuel appelle des méthodes séparées pour générer toutes les parties d'une unité.
Voici, par exemple, la définition de la CLASSe qui génère le c_xxx_module:
c_generate_module= class(c_generator)
m_table_name: String;
m_dataset_class_name, m_dataset_constructor_name: String;
m_module_suffix: String;
_m_datamodule_class_name,
_m_datamodule_constructor_name: String;
_m_dataset_name, _m_datasource_name: String;
Constructor create_generate_module(p_table_name,
p_dataset_class_name, p_dataset_constructor_name,
p_module_suffix: String);
procedure _generate_header;
procedure _generate_class;
procedure _generate_interface;
procedure _generate_routine_body;
procedure _generate_end;
procedure generate_module;
end; // c_generate_module |
et la procédure qui génère, par exemple, la définition de c_xxx_module:
procedure c_generate_module._generate_class;
var l_datamodule_class_indentation: Integer;
procedure generate_attribute_declaration; begin
add_line(' '+ _m_dataset_name+ ': '+ m_dataset_class_name+ ';');
add_line(' '+ _m_datasource_name+ ': tDataSource;');
end; // generate_attribute_declaration
procedure generate_routine_declaration; begin
// -- add the methods after the fields add_line('');
_m_datamodule_constructor_name:= 'create_'+ m_table_name+ m_module_suffix;
add_line(' constructor '+ _m_datamodule_constructor_name
+ '(p_c_owner: tComponent;');
add_line(' p_select_sql: String);');
add_line(' procedure _open_dataset();');
add_line(' procedure _close_dataset();');
end; // generate_routine_declaration
begin // _generate_class add_line('');
l_datamodule_class_indentation:= Length('type '+ _m_datamodule_class_name)+ 2;
add_line('type '+ _m_datamodule_class_name+ '= Class(tComponent)');
Inc(m_indentation, l_datamodule_class_indentation);
generate_attribute_declaration; generate_routine_declaration;
m_indentation:= Length('type')+ l_datamodule_class_indentation;
add_line('end; // '+ _m_datamodule_class_name);
end; // _generate_class |
Pour la génération de l'objet métier, c'est un peu plus compliqué, car il faut
générer l'initialisation des attributs de champs (PARTNO etc) ainsi que les événements et leur initialisation. Nous utilisons simplement des c_generator différents pour chacune des ces tâches, et assemblons les différentes parties
pour obtenir le résultat final
4.2.3 - Les Formes et le .DPR Pour générer les formes U_F_xxx_EDIT, le .DFM correspondant, le .DPR et le .DOF, la technique est la même, mais appliquée à des fichiers résultat
différents. Voyez le .ZIP pour le détail
4.3 - Le générateur Voici l'image du générateur en pleine action:
Nous avons ici généré les fichiers pour la Table CUSTOMER, et le tNoteBook sur la droite affiche le contenu du .DFM
4.4 - Mini Manuel Voici les étapes pour générer les objets métier d'une nouvelle application:
| dans la page "db_" du classer de gauche, sélectionnez la base de données (le répertoire et le nom des fichiers .GDB pour Interbase) |
| dans la page "generate_" - cliquez "tables_" pour récupérer dans la tListBox située plus bas les noms de toutes les Tables
- cliquez sur les nom des Tables "xxx" pour lesquelles vous souhaitez générer les c_xxx, c_xxx_module et u_f_edit_xxx
| |
si vous souhaitez générer le projet test, cliquez "generate_project_" | Pour utiliser les objets métier dans une application: |
importez les unités contenant les classes métier | | créez des unités regroupant éventuellement plusieurs classes métier, et
ajoutez le code métier proprement dit |
5 - Améliorations 5.1 - Critique de la solution étudiée Nos classes métier répondent-elles aux critères fixés:
- pour la partie code, oui à notre avis:
- les Tables sont bien encapsulées dans une CLASSe à qui nous pouvons ajouter les règles métier que nous souhaitons. De plus les colonnes des
Tables sont accessibles directement (sans passer par Fields[] ou FieldByName
- les modules permettent d'accéder aux objets métier précédent, et, partant de ces classes de base, nous pouvons créer des modules composites
(regroupant plusieurs objets métier) pour mettre en oeuvre des règles portant sur plusieurs Tables. Nous pouvons même hériter de modules métier, en mettant ainsi en oeuvre une hiérarchie (similaire à l'héritage de tDataModule)
- non pour la partie conception: nous ne pouvons pas déposer sur une nouvelle Forme un composant métier, ou un module métier, car nous avons choisi de ne pas placer nos composants sur la Palette. Et nous ne pouvons pas non plus
ajuster les propriétés et le événements de nos objets métier via l'Inspecteur d'Objet
La dernière critique était prévisible. Cette absence d'outil de conception
visuel est surtout gênante en cas de modifications: notre outil fonctionne en une passe, et si le développeur ajoute du code manuel, ces parties sont perdues dans les générations suivantes.
En fait, pour pouvoir utiliser un mécanisme de Palette / Inspecteur d'Objet, il faudrait écrire notre propre mécanique de conception. Ceci est réalisé sous forme de projet pilote, mais non publié à ce jour. Notez que c'est en réalité
ce que RemObjects a effectué (avec, en plus, les couches de généralisation des moteurs SQL) Et compte tenu de l'absence de mécanique de conception visuelle, nous avons
gelé le développement du générateur: tous les événements des tDataSets ne sont pas prévus, nous ne pouvons pas spécifier de propriétés pour les tFields etc. La difficulté n'est pas la génération, mais l'absence d'outil pour modifier le
code généré après sa création. Ceci dit, le générateur a tout de même permis de tester rapidement plusieurs solution métier: - en écrivant différentes organisation dans du code d'essai
- en modifiant le générateur
- en utilisant le code généré pour évaluer la fonctionnalité et l'ergonomie du résultat
5.2 - Ajout de fonctionnalités Nous pourrions ajouter d'autres fonctionnalités "métier" à notre mécanique
- une base de tFields (un dictionnaire de champs) ayant déjà des règles métier (ceci équivaut au concept de DOMAINes au niveau des Serveurs SQL)
- un outil qui facilite le regroupement de plusieurs objets métier dans un
même module
6 - Télécharger le code source Delphi Vous pouvez télécharger: Ce .ZIP qui comprend:
- le .DPR, la forme principale, les formes annexes eventuelles
- les fichiers de paramètres (le schéma et le batch de création)
- dans chaque .ZIP, toutes les librairies nécessaires à chaque projet (chaque .ZIP est autonaume)
Ces .ZIP, pour les projets en Delphi 6, contiennent des chemins RELATIFS. Par conséquent: - créez un répertoire n'importe où sur votre machine
- placez le .ZIP dans ce répertoire
- dézippez et les sous-répertoires nécessaires seront créés
- compilez et exécutez
Ces .ZIP ne modifient pas votre PC (pas de changement de la Base de Registre, de DLL ou autre). Pour supprimer le projet, effacez le répertoire.
La notation utilisée est la notation alsacienne qui consiste à préfixer les identificateurs par la zone de compilation: K_onstant, T_ype, G_lobal,
L_ocal, P_arametre, F_unction, C_lasse. Elle est présentée plus en détail dans l'article La
Notation Alsacienne
Comme d'habitude: - nous vous remercions de nous signaler toute erreur, inexactitude ou
problème de téléchargement en envoyant un e-mail à jcolibri@jcolibri.com. Les corrections qui en résulteront pourront aider les prochains lecteurs
- tous vos commentaires, remarques, questions, critiques, suggestion d'article, ou mentions d'autres sources sur le même sujet seront de même les bienvenus à jcolibri@jcolibri.com.
- plus simplement, vous pouvez taper (anonymement ou en fournissant votre e-mail pour une réponse) vos commentaires ci-dessus et nous les envoyer en cliquant "envoyer" :
- et si vous avez apprécié cet article, faites connaître notre site, ajoutez un lien dans vos listes de liens ou citez-nous dans vos
blogs ou réponses sur les messageries. C'est très simple: plus nous aurons de visiteurs et de références Google, plus nous écrirons d'articles.
7 - Références
Voici quelques références concernant les objets métier en Delphi:
8 - L'auteur John COLIBRI est passionné par le développement
Delphi et les applications de Bases de Données. Il a écrit de nombreux livres et articles, et partage son temps entre le développement de projets (nouveaux projets, maintenance, audit, migration BDE, migration Xe_n, refactoring) pour ses clients, le
conseil (composants, architecture, test) et la
formation. Son site contient des articles
avec code source, ainsi que le programme et le calendrier des stages de formation Delphi, base de données, programmation objet, Services Web, Tcp/Ip et
UML qu'il anime personellement tous les mois, à Paris, en province ou sur site client. |