menu
  Home  ==>  articles  ==>  bdd  ==>  delphi_business_objects   

Delphi Business Objects - John COLIBRI.


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:

image

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 ClassesDbIbDatabaseIBCustomDataSet;
    
    type c_partsClass(tIbDataSet)
                    PARTNO_tFloatField;
                    VENDORNO_tFloatField;
                    DESCRIPTION_tStringField;
                    ONHAND_tFloatField;
                    ONORDER_tFloatField;
                    COST_tFloatField;
                    LISTPRICE_tFloatField;
                  
                    constructor create_parts(p_c_ownertComponent;
                        p_c_ib_databasetIbDatabasep_select_sqlString);
                    procedure open_dataset();
                  
                    procedure after_open(p_c_datasettDataset);
                  
                    procedure before_edit(p_c_datasettDataset);
                    procedure before_post(p_c_datasettDataset);
                  
                    procedure partno_validate(p_c_fieldtField);
                  end// c_parts
  
  implementation
    uses u_dm_database;

    // -- c_parts
    
    Constructor c_parts.create_parts(p_c_ownertComponent;
        p_c_ib_databasetIbDatabasep_select_sqlString);
      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_datasettDataset);
      var l_field_indexInteger;
      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_indexdo
            if Name'PARTNO'
              then PARTNO_:= Fields[l_field_indexas tFloatField else
            if Name'VENDORNO'
              then VENDORNO_:= Fields[l_field_indexas tFloatField else
            if Name'DESCRIPTION'
              then DESCRIPTION_:= Fields[l_field_indexas tStringField else
            if Name'ONHAND'
              then ONHAND_:= Fields[l_field_indexas tFloatField else
            if Name'ONORDER'
              then ONORDER_:= Fields[l_field_indexas tFloatField else
            if Name'COST'
              then COST_:= Fields[l_field_indexas tFloatField else
            if Name'LISTPRICE'
              then LISTPRICE_:= Fields[l_field_indexas tFloatField else
          
          // -- hook the field_events
          partno_.OnValidate:= partno_validate;
      end// after_open_dataset
    
    procedure c_parts.before_edit(p_c_datasettDataset);
      begin
      end// before_edit
    
    procedure c_parts.before_post(p_c_datasettDataset);
      begin
        // -- get next key
        if StatedsInsert
          then PARTNO_.Value:= dm_database.f_get_next_key('parts_generator');
      end// before_post
    
    procedure c_parts.partno_validate(p_c_fieldtField);
      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:
      • create_xxx()
    • 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 ClassesDbIBCustomDataSetu_c_parts;

    type c_parts_moduleClass(tComponent)
                           _partsc_parts;
                           _parts_datasourcetDataSource;

                           constructor create_parts_module(p_c_ownertComponent;
                             p_select_sqlString);
                           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_ownertComponent;
        p_select_sqlString);
      begin
        Inherited Create(p_c_owner);

        _parts:= c_parts.create_parts(Selfdm_database.IbDataBase1p_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 WindowsMessagesSysUtilsVariantsClassesGraphicsControls,
        , FormsDialogsStdCtrlsMaskExtCtrlsDBCtrlsGridsDBGrids
        , DBIBCustomDataSet
        , u_c_partsu_c_parts_module
        ;

    type
      Tedit_parts_formclass(TForm)
          DBGrid1TDBGrid;
          DBNavigator1TDBNavigator;
          db_PARTNO_editTDBEdit;
          procedure FormCreate(SenderTObject);
        private
        public
          m_c_parts_modulec_parts_module;

          // -- references
          parts_refc_parts;

          procedure initialize_local_references;
          procedure link_controls;
          procedure open_dataset;
          procedure close_dataset;
          procedure open_at(p_PARTNOString);
      end// Tedit_parts_form

    var edit_parts_formTedit_parts_form;

  implementation
    uses u_dm_database;

    {$R *.dfm}

    procedure Tedit_parts_form.FormCreate(SenderTObject);
      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_PARTNOString);
      begin
        open_dataset();
        parts_ref.Locate('PARTNO'p_PARTNO, []);
        ShowModal;
        close_dataset();
      end// open_at

  end.

et la Forme correspondante est la suivante:

image

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:

image

Et le code correspondant ouvre simplement notre Forme d'édition:

procedure Ttest_form.employee_Click(SenderTObject);
  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_generatorclass(c_basic_object)
               m_c_result_listtStringList;
               m_current_lineString;
               m_indentationInteger;

               Constructor create_generator(p_nameString);

               function f_c_selfc_generator;
               procedure clear_generator;

               procedure add_text(p_textString);
               procedure add_line(p_textString);
               procedure add_new_line;

               procedure indent_add_line(p_textString);
               procedure unindent_add_line(p_textString);
               procedure add_line_indent(p_textString);
               procedure add_line_unindent(p_textString);

               procedure append_generator_lines(p_c_generatorc_generator);
               procedure append_generator_lines_indented(p_c_generatorc_generator;
                   p_indentationInteger);

               procedure save_to_file(p_full_file_nameString);

               Destructor DestroyOverride;
             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(SenderTObject);
  var l_itemInteger;
      l_indexInteger;
      l_the_linel_table_nameString;
  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_linel_index);
                f_string_extract_non_blank(l_the_linel_index);
                l_table_name:= f_string_extract_non_blank(l_the_linel_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_moduleclass(c_generator)
                     m_table_nameString;
                     m_dataset_class_namem_dataset_constructor_nameString;
                     m_module_suffixString;

                     _m_datamodule_class_name,
                         _m_datamodule_constructor_nameString;
                     _m_dataset_name_m_datasource_nameString;

                     Constructor create_generate_module(p_table_name,
                         p_dataset_class_namep_dataset_constructor_name,
                         p_module_suffixString);

                       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_indentationInteger;

  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_namem_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_indentationl_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:

image

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" :
    Nom :
    E-mail :
    Commentaires * :
     

  • 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.
Created: jan-04. Last updated: mar-2020 - 250 articles, 620 .ZIP sources, 3303 figures
Contact : John COLIBRI - Tel: 01.42.83.69.36 / 06.87.88.23.91 - email:jcolibri@jcolibri.com
Copyright © J.Colibri   http://www.jcolibri.com - 2001 - 2020
Retour:  Home  Articles  Formations  Développement Delphi  Livres  Pascalissime  Liens  Download
l'Institut Pascal

John COLIBRI

+ Home
  + articles_avec_sources
    + bases_de_donnees
      + programmation_oracle
      + interbase
      + sql_server
      + firebird
      + mysql
      + xml
      – paradox_via_ado
      – mastapp
      – delphi_business_objects
      – clientdataset_xml
      – data_extractor
      – rave_report_tutorial
      – visual_livebindings
      – migration_bde
    + web_internet_sockets
    + services_web_
    + prog_objet_composants
    + office_com_automation
    + colibri_utilities
    + uml_design_patterns
    + graphique
    + delphi
    + outils
    + firemonkey
    + vcl_rtl
    + colibri_helpers
    + colibri_skelettons
    + admin
  + formations
  + developpement_delphi
  + présentations
  + pascalissime
  + livres
  + entre_nous
  – télécharger

contacts
plan_du_site
– chercher :

RSS feed  
Blog

Expert Delphi Résolution de problèmes ponctuels, optimisation, TMA Delphi, audit, migration, réalisation de projets, transfert de technologie - Tél 01.42.83.69.36
Formation Delphi complete L'outil de développpement, le langage de programmation, les composants, les bases de données et la programmation Internet - 5 jours