Les Interfaces Delphi - John COLIBRI. |
- résumé : les Interfaces Delphi: utilité, définition, mise en oeuvre, exemples
- mots clé : Interface - QueryInterface - iInterface -
tInterfacedObject - gestion mémoire - héritage - génériques - design pattern - coding to the interface - framework
- 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 à 5, Delphi 6, Delphi 7, Delphi 2006 à 2010, Rad Studio, Delphi XE
- niveau : développeur Delphi
- plan :
1 - Delphi et les Interfaces
Le langage objet de Delphi est fondamentalement un langage objet, avec des extensions, comme les références de classe ou les méta classes (RTTI). Pour pouvoir utiliser les objets COM Windows, Delphi a mis en oeuvre les
Interfaces. Comme ce modèle de programmation a été jugé suffisamment utile, Delphi a été doté d'un mécanisme d'Interfaces qui ne dépend pas nécessairement de la mécanique COM.
Cet article a pour but de présenter ce que les Interfaces apportent de plus que le modèle objet de base n'offrait pas. Nous allons examiner - le modèle objet de Delphi
- définition
- les Interfaces comme outil de construction de librairies
- les Interfaces et le polymorphisme
2 - Le modèle objet Delphi
Delphi utilise un modèle objet à héritage unique. Une Classe ne peut hériter que d'une seule Classe.
Prenons le cas d'un traitement de texte. Nous souhaitons pouvoir:
- gérer la manipulation du texte (un tampon, avec ajout, modification, recherche, correction orthographique)
- sauvegarder, recharger les données depuis le disque. Puis renommer, copier, envoyer le fichier sur le réseau etc
La solution simple est de placer toutes ces fonctionalités dans une classe c_editeur:
Mais si nous devons par la suite gérer un tableur il faudra dupliquer une
partie des fonctionalités de gestion de fichier:
L'idée nous vient naturellement de factoriser la partie fichier dans une classe
c_fichier, et hériter de ces fonctionnalités fichier pour notre éditeur et notre tableur :
Mais si nous souhaitons ajouter d'autres possibilités (dessin graphique,
journal, vérification mémoire...), nous ne pourrons pas dériver nos classes "applicatives" de toutes ces classes "utilitaires". Pour ajouter une classe de calcule statistique, nous plaçons l'évaluation
d'expression dans une classe séparée. Toutefois notre tableur ne pourra pas hériter de cette classe, car il hérite déjà de c_fichier:
La solution en Delphi est d'utiliser la composition: la classe c_tableur contiendra un attribut m_c_fichier, un attribut m_c_evaluateur etc.
La composition nous permet donc d'isoler les groupes de fonctionnalités dans des classes séparées, et de composer ces classes pour obtenir une librairie sans redondance excessive.
Nous pouvons alors construire des structures (des listes, par exemple), qui contiennent des objets du type de base ou d'un de ses descendants:
Var ma_list_fichier: Array Of c_fichier;
ma_liste_fichier.ajoute_fichier(c_editeur.create);
ma_liste_fichier.ajoute_fichier(c_tableur.create); |
et nous pourrons appeler des méthodes qui auront comme paramètre un type c_fichier ou un de ses descendants:
Procedure concatene(p_c_fichier: c_fichier); Begin
End;
For indice_fichier:= 0 To Length(ma_liste_fichier)- 1 Do
concatene(ma_liste_fichier[indice_fichier]); |
Toutefois nous ne pouvons pas utiliser le polymorphisme pour les classes qui ne font pas partie de la même hiérarchie:
Var ma_list_fichier: Array Of c_fichier:
ma_liste_fichier.ajoute_fichier(c_editeur.create);
ma_liste_fichier.ajoute_fichier(c_statistiques.create); // <= REFUSE
| Pour construire une liste de c_fichier et de c_statistique, il faut leur trouver un ancêtre commun. Ici c'est tObject. Nous pourrons donc utiliser une
tObjectList. Mais tObject n'a pas les méthodes evalue_expression, ou les attributs de ces Classes, et le moindre traitement exigera force surtypage, Is ou As:
Var ma_list_objet: tObjectList;
ma_liste_objet.Add(c_editeur.create);
ma_liste_objet.Add(c_statistiques.create);
For indice_objet:= 0 To ma_list_objet.Count- 1 Do
If ma_liste_objet[indice_fichier] Is c_fichier
Then concatene(c_fichier(ma_liste_objet[indice_fichier]));
|
Fondamentalement, ce qui manque, c'est la possibilité d'ajouter à c_tableur, en plus des méthodes de c_fichier, les méthodes de c_evaluateur.
C'est ce que permettent les Interfaces Delphi. Nous pourrons alors - créer des Classes ayant différentes fonctionnalités (traitement de texte, calcul matriciel, tableur, )
- ajouter à ces Classes des possibilités de sauvegarde, compression, cryptage, génération d'état
- si nous créons des objets composites, nous savons que nous pourrons effectuer sur ces composites les traitements de sauvegarde, compression,
génération d'état etc (polymorphisme)
3 - Définition des Interfaces Delphi 3.1 - La calculette de base
Commençons par un exemple simple d'une mini-calculette avec des possibilités de log. Supposez qu'un de nos projet utilise une calculette:
Unit u_c_calculator; Interface
Type c_calculator= Class
m_one, m_two: Integer;
m_result: Integer;
Procedure add;
Procedure subtract;
End; Implementation
// -- c_calculator Procedure c_calculator.add;
Begin End;
Procedure c_calculator.subtract; Begin
End; End. |
3.2 - Ajout d'un log
Nous souhaitons à présent doter des classes de notre projet d'un mécanisme de log. Nous définissons les méthodes qui permettront ce log. Pour pouvoir ajouter ces fonctionnalités à n'importe quelle Classe, nous définissons ces méthodes
sous forme d'Interface: Unit u_i_logable;
Interface Type i_logable=
Interface
Procedure initialize_log(p_file_name: String);
Procedure write_to_log(p_message: String);
End; Implementation
End. |
Notez que : - une Interface Delphi se déclare donc avec une syntaxe similaire à une
Classe: dans un Type, entre Interface et End
- Cette Interface définit simplement la syntaxe des procédures et fonctions que nous souhaitons utiliser pour écrire dans notre log. C'est une pure
définition. Elle ne fait aucun traitement. Elle permettra de normaliser les noms et paramètres à utiliser pour notre traitement. L'écriture du log elle-même dépendra des données à écrire, et sera donc mise en oeuvre dans
les différentes Classes qui auront décidé d'employer le log en utilisant ces méthodes avec ces paramètres.
- au niveau de la notation:
- nous avons utilisés les préfixes de la
notation alsacienne, avec un "i_" comme préfixe de l'Interface. En notation standard on utiliserait iLog
- nous avons ajouté le suffixe ABLE qui souligne le fait que cette Interface indiquera qu'une Classe qui utilisera cette Interface sera doté des fonctionnalités de log. Elle sera "capable de" générer des
logs. Ce suffixe "able" est aussi optionnel
3.3 - Une Classe qui implémente l'Interface Modifions à présent notre calculette pour la doter des possibilités de log.
Unit u_c_calculator_2; Interface
Uses Classes, u_i_logable;
Type c_calculator=
Class(tInterfacedObject, i_logable)
m_one, m_two: Integer;
m_result: Integer;
m_c_stream: tStream;
Procedure add;
Procedure subtract;
// -- i_log implementation
Procedure initialize_log(p_file_name: String);
Procedure write_to_log(p_message: String);
End; Implementation
Uses SysUtils; // -- c_calculator
Procedure c_calculator.add; Begin
write_to_log('add '+ IntToStr(m_one)+ '+ '+ IntToStr(m_two));
End; // add
Procedure c_calculator.subtract; Begin
End; // subtract
Procedure c_calculator.initialize_log(p_file_name: String);
Begin
m_c_stream:= tFileStream.Create(p_file_name, fmCreate);
End; // initialize_log
Procedure c_calculator.write_to_log(p_message: String);
Begin
m_c_stream.Write(p_message[1], Length(p_message));
End; // write_to_log End. |
Notez que
4 - Syntaxe 4.1 - L'unité SYSTEM.PAS Les Interfaces sont définies dans l'unité SYSTEM.PAS Nous y trouvons:
Type IInterface =
Interface ['{00000000-0000-0000-C000-000000000046}']
Function QueryInterface(Const IID: TGUID; Out Obj): HResult; Stdcall;
Function _AddRef: Integer; Stdcall;
Function _Release: Integer; Stdcall;
End; IUnknown = IInterface;
TInterfacedObject=
Class(TObject, IInterface)
Protected
FRefCount: Integer;
Function QueryInterface(Const IID: TGUID; Out Obj): HResult; Stdcall;
Function _AddRef: Integer; Stdcall;
Function _Release: Integer; Stdcall;
Public
Procedure AfterConstruction; Override;
Procedure BeforeDestruction; Override;
Class Function NewInstance: TObject; Override;
Property RefCount: Integer read FRefCount;
End;
Function TInterfacedObject.QueryInterface(Const IID: TGUID; Out Obj): HResult;
Begin If GetInterface(IID, Obj)
Then Result := 0
Else Result := E_NOINTERFACE;
End; // QueryInterface
Function TInterfacedObject._AddRef: Integer; Begin
Result := InterlockedIncrement(FRefCount);
End; // _AddRef
Function TInterfacedObject._Release: Integer; Begin
Result := InterlockedDecrement(FRefCount);
If Result = 0 Then Destroy;
End; |
4.2 - iInterface Toutes les Interfaces Delphi descendent de iInterface. Cette Interface a
été définie pour correspondre exactement à une Interface COM Windows, avec 3 méthodes, QueryInterface, _AddRef et _Release. Donc lorsque nous déclarons:
Type i_mon_interface=
Interface
Procedure additionne;
End; | l'héritage de iInterface est implicite. Il est donc équivalent d'écrire:
Type i_mon_interface=
Interface(iInterface)
Procedure additionne;
End; |
4.3 - tInterfacedObject
SYSTEM.PAS contient la Classe tInterfacedObject, qui fournit une implémentation par défaut des 3 méthodes de iInterface. Le code de ces méthodes figure plus haut.
Voyons à présent à quoi servent ces trois méthodes et comment les utiliser.
4.4 - QueryInterface - GUID Cette méthode, définie au coeur de COM, permet de tester si un objet COM
implémente une Interface donnée. Si nous chargeons un objet COM (un ActiveX), nous pouvons tester si cet objet sait analyser une document XML, arrive à afficher une page .HTML, saura jouer une animation Shockwave Flash.
Ces objets COM peuvent avoir été créés par d'autres personnes, et nous ne disposons en général ni des sources, ni même souvent d'une doc. Nous pouvons donc interroger l'objet pour savoir s'il sait effectuer certains traitements en
répondant à certains appels de méthodes.
Pour arriver à effectuer ce test, les Interfaces doivent être dotées d'un identificateur unique, un GUID (Globally Unique Identifier), généré par
Windows. Chaque GUID est GARANTI unique, "in the world". Delphi définit ce Guid par:
Type TGUID = Packed Record
D1: LongWord;
D2: Word;
D3: Word;
D4: Array[0..7] Of Byte;
End; | Pour doter une de nos Interfaces de son propre GUID, il suffit de taper
Shift Ctrl G entre Interface et End. Dans notre cas, nous obtenons, par exemple:
Type i_computable= Interface
['{508B660C-E175-4832-863D-62922D2E4038}'] // <= Shift Ctlr G
Procedure add;
Procedure substract;
End; // i_computable |
Nous pouvons tester si une Classe peut calculer par: Type c_computer=
Class(tInterfacedObject, i_computable)
m_one, m_two: Integer;
m_result: Integer;
Procedure add;
Procedure substract;
Procedure multiply;
End; // c_computer
Procedure c_computer.add; Begin
display('add'); End; // add
Procedure c_computer.substract; Begin
display('subtract'); End; // substract
Procedure c_computer.multiply; Begin
display('mul'); End; // multiply
Const k_guid: tGuid='{508B660C-E175-4832-863D-62922D2E4038}'; // <== same guid litteral
Procedure TForm1.query_k_guid_Click(Sender: TObject);
Var l_c_computer: c_computer;
l_i_computable: i_computable; Begin
l_c_computer:= c_computer.Create;
// -- query and extract the interface
If l_c_computer.QueryInterface(k_guid, l_i_computable)= S_ok
Then Begin
display('ok');
l_i_computable.add; // <== use the interface
End
Else display('not ok');
End; // query_k_guid_Click |
Pour appeler mon_objet.QueryInterface nous avons du créer une constante dont
la valeur correspond au GUID de noter Interface. Delphi permet aussi d'utiliser comme premier paramètre de QueryInterface le nom de l'Interface, ce qui est bien plus pratique:
Procedure TForm1.query_ixxx_Click(Sender: TObject);
Var l_c_computer: c_computer;
l_i_computable: i_computable; Begin
l_c_computer:= c_computer.Create;
If l_c_computer.GetInterface(i_computable, l_i_computable)
Then Begin
display('ok');
l_i_computable.add; End
Else display('not ok');
End; // query_ixxx_ |
Notez que - ATTENTION, il faut appeler
mon_objet.QueryInterface( ... | et pas
qui est la fonction de l'API Windows. Celle-ci fonctionne, évidemment, mais
a ses propres règles d'utilisation. - mon_objet.QueryInterface ne fonctionne QUE SI l'Interface possède un GUID. Si nous appelons mon_objet.QueryInterface(i_xxx ... et que i_xxx
n'a pas de GUID, le compilateur bloque
"the interface i_xxx has no interface identification". - si nous testons une Interface qui n'est pas implémentée par la Classe,
le résultat de la fonction mon_objet.QueryInterface n'est pas S_OK
4.5 - Référence Interface Un Type Interface n'est qu'un Type: c'est une définition, pas une donnée.
Pour utiliser une Interface dans un traitement, il faudra donc - que nous définissions une Classe qui implémente cette Interface
- que nous créions un objet qui est une instance de cette Classe. A cet
instant des données sont allouées en mémoire
- que nous obtenions une référence Interface sur cet objet.
Voici un exemple d'une Interface, un Classe qui l'implémente, avec une
méthode quelconque qui a un paramètre du type de cette Interface:
Type i_printer= Interface
['{D7268B1E-3465-4AA7-B62E-F3BD3856B5C0}']
Procedure print;
End; // i_printer c_letter=
Class(tInterfacedObject, i_printer)
Procedure insert_line;
Procedure print;
End; // c_letter
Procedure c_letter.insert_line; Begin
display('insert_line'); End; // insert_line
Procedure c_letter.print; Begin
display('print'); End; // print
Procedure format_printout(p_i_printer: i_printer); Begin
p_i_printer.print; End; // format_printout |
Nous pouvons obtenir cette référence Interface de 3 manières - en créant directement la référence:
- en affectant un objet à la référence
- en appelant mon_objet.QueryInterface
- en surtypant un objet par AS
4.5.1 - Création directe Nous pouvons directement créer une référence Interface par le code suivant
Procedure TForm1.direct_ref_Click(Sender: TObject);
Var l_i_printer: i_printer; Begin
l_i_printer:= c_letter.create;
// -- appel d'une méthode de l'Interface i_printer l_i_printer.print;
// -- appel d'une procédure ayant un paramètre i_printer
format_printout(l_i_printer); End; // direct_ref_Click |
Notez que - la variable référence l_i_printer est déclarée de type i_printer, mais la création se fait par c_letter.Create
Il s'agit du même mécanisme qui permet de déclarer une variable de type
classe ancêtre et créer un descendant: Type c_ancestor=
Class
m_x, m_y: Integer;
End; c_descendent=
Class(c_ancestor)
m_color: Integer; End;
Var my_c_object: c_ancestor;
// -- crée un objet de type c_descendant
my_c_object:= c_descendent.Create; |
Ce qui détermine le type créé, ce n'est pas la déclaration statique, mais le Type utilisé pour l'appel du Constructor - nous pouvons affecter un c_letter à une i_printer : les références
Interfaces de type i_printer sont donc compatibles avec les Classes qui implémentent i_printer, ici c_letter.
Autrement formulé, nous pourrons remplacer une référence i_printer par une
variable c_letter (compatibilité en affectation, ou encore principe de substitution de Liskov)
4.5.2 - Affectation d'un objet Nous pouvons aussi créer un objet et l'affecter à une référence Interface :
Procedure TForm1.affectation_Click(Sender: TObject);
Var l_c_letter: c_letter;
l_i_printer: i_printer; Begin
l_c_letter:= c_letter.Create;
// -- affactation (compatibilité en affectation) l_i_printer:= l_c_letter;
// -- appel d'une méthode de l'Interface i_printer l_i_printer.print;
// -- appel d'une procédure ayant un paramètre i_printer
format_printout(l_i_printer); // -- appel, avec compatibilité en affactation
format_printout(l_c_letter); End; // affectation_Click |
Notez que - la compatibilité en affectation intervient ici de façon plus visible
l_i_printer:= l_c_letter; format_printout(l_c_letter); |
4.5.3 - QueryInterface Nous pouvons aussi obtenir une référence Interface en appelant mon_objet.QueryInterface, comme nous l'avons montré plus haut.
4.5.4 - Transtypage par As
Nous pouvons finalement convertir un objet en une référence Interface en utilisant une version de As:
Procedure TForm1.conversion_as_Click(Sender: TObject);
Var l_c_letter: c_letter;
l_i_printer: i_printer; Begin
l_c_letter:= c_letter.Create;
// -- affactation (compatibilité en affectation)
l_i_printer:= l_c_letter As i_printer;
l_i_printer.print; format_printout(l_i_printer);
// -- appel, avec compatibilité en affactation
format_printout(l_c_letter As i_printer);
End; // conversion_as_Click |
Notez que
- As ne fonctionne que si notre Interface a été déclarée avec un GUID
4.5.5 - Interface et référence Interface L'Interface ne constitue qu'une définition de fonctionnalités.
Nous ne pouvons utiliser ces fonctionnalités que sur un objet que implémente cette Interface. Nous pouvons, bien sûr, créer un objet et appeler directement les méthodes ce cet objet. Mais si nous ne sommes intéressés que par les méthodes de
l'Interface, ou si nous avons une structure avec différents objets, et ne sommes intéressés que par les fonctionnalités de l'Interface, nous pouvons utiliser une référence Interface en appelant les méthodes de l'Interface.
De façon imagée - nous pouvons considérer les objets comme des icebergs:
- pour certains traitements, nous ne sommes intéressés que par le sommet de ces icebergs.
Pour dessiner, il faut bien que l'objet complet existe, mais nous ne sommes intéressés que par les fonctionnalités de dessin
- les références Interface pointent bien sur de véritables objets, mais en ne dévoilant qu'une partie des traitements disponibles sur cet objet.
En fait, cette image n'est pas très éloignée de la réalité. L'article
Dump Interface permet d'afficher à la fois - le pointeur objet
- les pointeur des différentes référence Interfaces de ce même objet.
Dans ces conditions, l'affectation, ou le surtypage par As ne font que calculer la référence Interface à partir du pointeur vers l'objet.
4.5.6 - Accès aux méthodes
Il est donc acquis que lorsque nous avons une référence Interface, il existe bien un objet qui permet d'appeler ces méthodes. Il existe en général, en plus, des données et d'autres méthodes, propre à cet objet.
Mais lorsque nous utilisons le pointeur Interface, nous ne pouvons PAS y accéder. Nous ne pouvons appeler QUE les méthodes de l'Interface. L'Interface correspond donc à une vue partielle, une facette, de cet objet.
Mais c'est bien là tout le concept: si nous utilisons une référence Interface mon_i_log, nous ne sommes intéressés que par les appels des méthodes de i_log. L'objet sous-jacent fera son traitement propre. Il sait, lui, ce qu'il
doit placer dans le log et comment le faire. Ce comportement est très similaire à Virtual: quand vous appelez mon_triangle.dessine ou mon_cercle.dessine, c'est chaque objet qui sait le
mieux comment dessiner son triangle ou son cercle (et pas la figure ou le programme principal).
4.6 - Gestion Mémoire Pour imiter le fonctionnement COM, Delphi a lié les Interface à une
libération automatique de la mémoire. Les objets qui implémentent une Interface sont dotés d'un champ RefCount. Puis: - ce champ est incrémenté chaque fois que nous créons une référence Interface sur cet objet
- il est décrémenté lorsque cette référence Interface est supprimée.
- lorsque mon_objet.RefCount devient 0, l'objet est libéré.
Notez bien que RefCount est lié à l'OBJET (un Type Interface ne pouvant,
lui, avoir de données).
L'augmentation de RefCount se produit lorsque - nous créons une référence Interface directement
- nous affectons un objet à une Interface
La diminution de RefCount est provoquée par - l'affectation de Nil à une référence Interface
- la réaffectation de la référence vers un autre objet implémentant l'Interface
- la sortie d'une méthode où une référence Interface locale est créée
Voici un projet pour tester RefCount:
Type i_printer= Interface
['{D7268B1E-3465-4AA7-B62E-F3BD3856B5C0}']
Procedure print;
End; // i_printer c_letter=
Class(tInterfacedObject, i_printer)
m_line_count: Integer;
Procedure print;
End; // c_letter
Procedure c_letter.print; Begin
display('print'); End; // print
Var g_c_letter: c_letter= Nil;
Procedure display_letter_refcount(p_text: String); Begin
display(p_text+ ' RefCount= '+ IntToStr(g_c_letter.RefCount));
End; // display_letter_refcount
Procedure format_printout(p_i_printer: i_printer); Begin
display_letter_refcount('in_method'); p_i_printer.print;
End; // format_printout
Procedure TForm1.affectation_Click(Sender: TObject);
Var l_i_printer: i_printer; Begin
g_c_letter:= c_letter.Create;
display_letter_refcount('1_after_create'); l_i_printer:= g_c_letter;
display_letter_refcount('2_after_i_assign'); l_i_printer.print;
format_printout(l_i_printer); display_letter_refcount('3_after_method_1');
format_printout(g_c_letter); display_letter_refcount('4_after_method_2');
End; // affectation_Click
Procedure TForm1.check_object_Click(Sender: TObject);
Begin display_letter_refcount('5_global_click');
g_c_letter.insert_line; g_c_letter.m_line_count:= 5;
If g_c_letter Is c_letter
Then display('ok');
End; // check_object_Click | et un clic sur "affectation" et "check_objet" affichera
1_after_create RefCount= 0 2_after_i_assign RefCount= 1 print in_method RefCount= 2 print 3_after_method_1 RefCount= 1
in_method RefCount= 2 print 4_after_method_2 RefCount= 1 5_global_click RefCount= 24 <==== insert_line ok |
Notez que
- RefCount augment après l'affectation à notre référence Interface
- il monte même à 2 dans la procédure qui a un paramètre i_print (les paramètres valeur sont des copies sur la pile, d'où une seconde référence:
la locale dans la procédure appelante et le paramètre dans la procédure)
- TRES SURPRENANT, RefCount devient incohérent après la fin de la procédure.
La raison est que avant le End, Refcount était 1, et en quittant la
procédure, il tombe à 0, ce qui libère l'objet. Nous pouvons encore dans un autre bouton manipuler l'objet (appeler g_c_letter.print, manipuler ses attributs, tester son type). Dans notre
cas nous n'avons pas eu systématiquement d'exception. En fait le pointeur g_c_letter pointe vers la même adresse. Mais ce comportement est totalement aléatoire, et fort souvent (pas toujours, hélas), une utilisation de
g_c_letter provoquera une exception. La moralité est qu'il ne faut pas mélanger les objets et les références d'Interfaces n'importe comment: - ici nous avons crée un objet global, et ensuite créé une référence
d'Interface qui va imposer son cycle de vie à l'objet. Un autre développeur pourrait penser que l'existence de la globale est régie par Create et Free, et ne soupçonnera pas en général la création d'une
référence d'Interface
- si notre c_letter avait été déclaré localement, la destruction n'aurait pas été un problème
- l'appel d'une procédure ayant un paramètre VALEUR i_printer fonctionne bien
sur la globale: la référence est augmentée à l'entrée et décrémentée à la sortie.
Le même type d'incident serait provoqué par une réaffectation de la référence Interface:
Procedure TForm1.reassign_i_printer_Click(Sender: TObject);
Var l_i_printer: i_printer; Begin
g_c_letter:= c_letter.Create;
l_i_printer:= g_c_letter; // -- REAFFECTATION à un nouvel objet
// -- => libère g_c_letter
l_i_printer:= c_letter.Create;
// -- sortie de la procédure
// -- => libère le second objet utilisé par l_i_printer
End; // reassign_i_printer_Click |
Nous aurions pu compenser la diminution de RefCount, en incrémentant sa valeur
pour qu'elle ne tombe pas à 0. Toutefois - RefCount est une propriété en lecture seule
- _AddRef est Protected au niveau de tInterfacedObject
Plusieurs solutions:
- écrire un descendant de tInterfacedObject qui rendrait _AddRef Public
- utiliser un surtypage par iInterface qui nous permet d'accéder à _AddRef:
Procedure TForm1.call_addref_Click(Sender: TObject);
Var l_i_printer: i_printer; Begin
g_c_letter:= c_letter.Create;
display_letter_refcount('1_after_create'); l_i_printer:= g_c_letter;
display_letter_refcount('2_after_i_assign'); // -- bump RefCount
iInterface(l_i_printer)._AddRef;
display_letter_refcount('3_after_AddRef');
l_i_printer:= c_letter.Create;
display_letter_refcount('4_after_reassign'); End; // call_addref_Click |
- écrire une Classe similaire à tInterfacedObject pour laquelle _AddRef et _Release ne font rien. Les Interfaces ne gèreront donc plus la libération automatique
4.7 - Diagramme de Classe UML Nous pouvons résumer la double structure Interface et Classe par le diagramme de Classe UML suivant:
Nous avons dessiné les Interfaces en pointillé, pour bien souligner que ce sont des éléments différents des Classes. De plus, comme les Classes sont OBLIGEES d'implémenter les méthodes définies
dans les Interfaces, nous pouvons, dans ces diagrammes de classe, supprimer ces méthodes des Classes :
4.8 - Résumé sur la création d'Interfacee Pour définir nos propres Interfaces dans nos projets - nous créons des Types Interfaces qui continenent ou non de GUID
- ces Interfaces seront implémentées par une ou plusieurs Classes qui
- pratiquement toujours, descendront de tInterfacedObject
- listeront les Interfaces dans l'en-tête de la définition de la Classe
- la définition de la Classe contiendra TOUTES les méthodes définies dans ces Interfaces, et ces méthodes seront implémentées dans la partie
Implementation de l'Unité contenant cette définition de Classe
4.9 - Interface et Property En plus des Procedures et Functions, la définition d'une Interface peut
comporter des Propertys. Ces propriétés sont uniquement une facilité de notation, et devront automatiquement comporter les getter et setters. Voici un exemple:
Type i_shape=
Interface
Procedure draw;
Procedure set_size(p_size: Integer);
Function get_size: Integer;
Property Size: Integer read get_size write set_size;
End; // i_shape c_figure=
Class(tInterfacedObject, i_shape)
m_size: Integer;
Procedure draw;
Procedure set_size(p_size: Integer);
Function get_size: Integer;
End; // c_figure
Procedure c_figure.draw; Begin
End; // draw
Procedure c_figure.set_size(p_size: Integer);
Begin display('dans c_figure.set_size');
m_size:= p_size; End; // set_size
Function c_figure.get_size: Integer; Begin
display('dans c_figure.get_size'); Result:= 2* m_size;
End; // get_size
Procedure TForm1.i_shape_Click(Sender: TObject);
Var l_i_shape: i_shape; Begin
l_i_shape:= c_figure.create;
l_i_shape.Size:= 10;
display(IntToStr(l_i_shape.Size));
End; // i_shape_Click |
Des Classes qui implémentent notre Interface pourront même avoir une
propriété qui port le même nom, mais qui pourront correspondre à d'autre méthodes d'accès.
Type i_shape= Interface
Procedure draw;
Procedure set_size(p_size: Integer);
Function get_size: Integer;
Property Size: Integer read get_size write set_size;
End; // i_shape c_figure=
Class(tInterfacedObject, i_shape)
m_size: Integer;
m_figure_size: Integer;
Procedure draw;
Procedure set_size(p_size: Integer);
Function get_size: Integer;
Procedure set_figure_size(p_size: Integer);
Function get_figure_size: Integer;
Property Size: Integer read get_figure_size write set_figure_size;
End; // c_figure
Procedure c_figure.draw; Begin
End; // draw
Procedure c_figure.set_size(p_size: Integer);
Begin display('dans c_figure.set_size');
m_size:= p_size; End; // set_size
Function c_figure.get_size: Integer; Begin
display('dans c_figure.get_size'); Result:= 2* m_size;
End; // get_size
Procedure c_figure.set_figure_size(p_size: Integer);
Begin display('dans c_figure.set_figure_size');
m_figure_size:= p_size; End; // set_figure_size
Function c_figure.get_figure_size: Integer; Begin
display('dans c_figure.get_figure_size');
Result:= 3* m_figure_size; End; // get_figure_size
Procedure TForm1.i_shape_Click(Sender: TObject);
Var l_i_shape: i_shape; Begin
l_i_shape:= c_figure.create;
l_i_shape.Size:= 10;
display(IntToStr(l_i_shape.Size));
End; // i_shape_Click
Procedure TForm1.c_figure_Click(Sender: TObject);
Begin With c_figure.create Do
Begin Size:= 100;
display(IntToStr(Size)); Free;
End; // with c_figure.create End; // c_figure_Click |
Cette pratique est naturellement encline à encourager la confusion et est peu recommandée. Elle existe pour résoudre des conflits si vous ne possédez pas les sources des classes et interfaces ancêtres
4.10 - Héritage d'Interfaces Une Interface peut hériter d'une autre Interface. La classe qui implémentera l'Interface descendante devra implémenter les méthodes des deux niveaux. Voici un exemple:
Type i_one=
Interface
Procedure method_one;
End; // i_one i_two=
Interface(i_one) // <= héritage d'Interface
Procedure method_two;
End; // i_two c_class=
Class(tInterfacedObject, i_two)
Procedure method_one;
Procedure method_two;
End; // c_class
Procedure c_class.method_one; Begin
End; // method_one Procedure c_class.method_two;
Begin End; // method_two |
Une Classe peut implémenter une Interface, et un descendant peut implémenter un descendant de l'Interface:
Type i_one= Interface
Procedure method_one;
End; // i_one c_ancestor_class=
Class(tInterfacedObject, i_one)
Procedure method_one;
End; // c_class
Procedure c_ancestor_class.method_one; Begin
End; // method_one Type i_two=
Interface(i_one) // <= héritage d'Interface
Procedure method_two;
End; // i_two c_descendent_class=
Class(c_ancestor_class, i_two) // <= héritage de classe
Procedure method_two;
End; // c_class
Procedure c_descendent_class.method_two; Begin
End; // method_two | C'est en fait la structure déjà rencontrée pour - iInterface, i_my_interface
- avec tObject, tInterfacedObject (qui implémente iInterface) et tMyObject (qui hérite de tInterfacedObject et implémente i_my_interface).
L'utilisation de l'héritage des Interfaces est contestée par certains. Elle revient à imposer aux Classes qui implémente l'Interfacee descendante toutes les méthodes. Or, dans l'esprit des Interfaces, nous préférerions
laisser chaque Classe implémenter les Interfaces qu'elle souhaite. En fait lorsqu'une Classe implémente une Interface descendante
5 - Interfaces Delphi dans la Vcl, génériques, design patterns 5.1 - Les Interfaces dans la VCL Delphi Une recherche de "= Interface" dans la VCL Delphi 6 fournit environ 1.600
définitions. Sur ces 1.600 définitions - plus de 1.500 sont liés à la mécanique COM de Windows, dans les domaines
- MsXml
- Ado
- ActiveX
- ComSvs
- ShlObj (shell)
- OleDb
- les utilisations Delphi se rencontrent surtout dans les domaines
- dbExpress, DataSnap
- WebSnap
- les variants
- le docking
Pour la partie "purement Delphi" (non lié à COM), nous avons trouvé
- iDockManager définit la spécification pour le tDockTree, qui est la structure polymorphique
- "responsible for inserting and removing controls (and thus zones) from
the tree and associated housekeeping, such as orientation, zone limits, parent zone creation, and painting of controls into zone bounds"
et qui est un exemple de structure polymorphique avec des traitements de
docking sur les éléments de la structure - pratiquement toutes les autres Interfaces sont utilisées
- pour définir un groupe de méthodes. Par exemple iDsCursor, utilisé comme attribut, paramètre, variable locale:
Type IDSCursor=
Interface(IUnknown) ...
Type TCustomClientDataSet= Class(TDataSet)
FFindCursor: IDSCursor; ...
Procedure SortOnFields(Cursor: IDSCursor;
Const Fields: String; CaseInsensitive, Descending: Boolean);
...
Function TCustomClientDataSet.FindRecord(Restart, GoForward: Boolean): Boolean;
Var Cursor: IDSCursor; Begin ... |
- pour "accoller" à une Classe des groupes de fonctionnalités additionnelles : la Classe hérite déjà d'une autre Classe (autre que tObject ou tInterfacedObject) et implémente, en plus, d'autres
Interfaces :
Type ISendDataBlock=
Interface
Function Send(Const Data: IDataBlock;
WaitForResult: Boolean): IDataBlock; Stdcall;
THTTPServer = Class(TWebModule, ISendDataBlock) ...
TSocketDispatcherThread = Class(TServerClientThread, ISendDataBlock) ...
TStreamedConnection = Class(TDispatchConnection, ISendDataBlock) ...
| ou encore - la définition de tDataSet, qui a les anciennes fonctionnalités BDE, mais permet en plus d'être associé à un tDataSetProvider
- la définition d'un tSoapDataModule, qui a le même rôle qu'un datamodule usuel (conteneur non graphique) et peut être utilisé comme un serveur d'application :
Type // -- accole iProviderSupport à tDataSet
tDataset= Class(tcomponent, iprovidersupport)
// -- accole les fonctionnalités iAppServer à un tDataModule
tsoapdatamodule = Class(tdatamodule, iappserver, iprovidercontainer)
|
5.2 - Utilisation des Interfaces Voici un exemple simple où nous implémentons un traitement sur les éléments
d'une structure. Par exemple pouvoir calculer la surface d'une figure. Nous définisoons, par exemple - une Interface de base pour calculer la surface
- une Voici la définition des Interfaces:
- pour calculer la surface, i_surface
- pour gérer et calculer la surface d'un rectangle
- pour gérer une liste de figures en vue d'en calculer la surface
Unit u_i_shape_list; Interface
Type i_shape= Interface
Function f_surface: Integer;
End; i_rectangle=
Interface(i_shape)
End; i_shape_list=
Interface(i_shape)
Procedure add_shape(p_i_shape: i_shape);
End; Implementation
End. |
Nous implémentons un c_rectangle et la liste des figures ainsi:
Unit u_c_shape_list; Interface
Uses Classes, u_i_shape_list;
Type c_rectangle=
Class(tInterfacedObject, i_shape, i_rectangle)
m_width, m_height: Integer;
Constructor create_rectangle(p_width, p_height: Integer);
Function f_surface: Integer;
Function f_width: Integer;
End; c_shape_list=
Class(tInterfacedObject, i_shape, i_shape_list)
m_c_shape_list: tInterfaceList;
Constructor create_shape_list;
Procedure add_shape(p_i_shape: i_shape);
Function f_surface: Integer;
End; Implementation
Uses SysUtils, u_c_display; // -- c_rectangle
Constructor c_rectangle.create_rectangle(p_width, p_height: Integer);
Begin m_width:= p_width;
m_height:= p_height;
End; // create_rectangle
Function c_rectangle.f_surface: Integer;
Begin
Result:= m_width* m_height;
End; // f_surface
Function c_rectangle.f_width: Integer;
Begin End; // -- c_shape_list
Constructor c_shape_list.create_shape_list; Begin
m_c_shape_list:= tInterfaceList.Create;
End; // create_shape_list
Procedure c_shape_list.add_shape(p_i_shape: i_shape);
Begin m_c_shape_list.Add(p_i_shape);
End; // add_shape
Function c_shape_list.f_surface: Integer;
Var l_shape_index: integer; Begin
Result:= 0;
With m_c_shape_list Do
For l_shape_index:= 0 To Count- 1 Do
Begin
Result:= Result+ i_shape(m_c_shape_list[l_shape_index]).f_surface;
End; End; // f_surface
End. |
Et finalement un exemple d'utilisation dans la forme principale:
Unit u_c_shape_list; Interface
Uses Classes, u_i_shape_list;
Type c_rectangle=
Class(tInterfacedObject, i_shape, i_rectangle)
m_width, m_height: Integer;
Constructor create_rectangle(p_width, p_height: Integer);
Function f_surface: Integer;
Function f_width: Integer;
End; c_shape_list=
Class(tInterfacedObject, i_shape, i_shape_list)
m_c_shape_list: tInterfaceList;
Constructor create_shape_list;
Procedure add_shape(p_i_shape: i_shape);
Function f_surface: Integer;
End; Implementation
Uses SysUtils, u_c_display; // -- c_rectangle
Constructor c_rectangle.create_rectangle(p_width, p_height: Integer);
Begin m_width:= p_width;
m_height:= p_height;
End; // create_rectangle
Function c_rectangle.f_surface: Integer;
Begin
Result:= m_width* m_height;
End; // f_surface
Function c_rectangle.f_width: Integer;
Begin End; // -- c_shape_list
Constructor c_shape_list.create_shape_list; Begin
m_c_shape_list:= tInterfaceList.Create;
End; // create_shape_list
Procedure c_shape_list.add_shape(p_i_shape: i_shape);
Begin m_c_shape_list.Add(p_i_shape);
End; // add_shape
Function c_shape_list.f_surface: Integer;
Var l_shape_index: integer; Begin
Result:= 0;
With m_c_shape_list Do
For l_shape_index:= 0 To Count- 1 Do
Begin
Result:= Result+ i_shape(m_c_shape_list[l_shape_index]).f_surface;
End; End; // f_surface
End. |
Cerise sur le gâteau: nous pouvons effectuer le calcul sur tForm1:
Unit u_composite; Interface
Uses Windows, ... u_i_shape_list;
Type TForm1=
Class(TForm, i_shape)
compute_surface_: TButton;
Procedure compute_surface_Click(Sender: TObject);
... Private
Function f_surface: Integer;
End; // tForm1
Var Form1: TForm1; Implementation
Uses u_c_shape_list ; {$R *.DFM}
Procedure TForm1.compute_surface_Click(Sender: TObject);
Var l_c_rectangle: c_rectangle;
Begin
With c_shape_list.create_shape_list Do
Begin
l_c_rectangle:= c_rectangle.create_rectangle(10, 20);
add_shape(l_c_rectangle);
add_shape(Form1);
display('rectangle_surface '+ IntToStr(l_c_rectangle.f_surface));
display('Form1_surface '+ IntToStr(Form1.f_surface));
display('total_surface '+ IntToStr(f_surface));
Free;
End; // with c_shape_list
End; // compute_surface_Click End. |
Nous pouvons ajouter l'Interface i_shape à tForm1, car les tForm implémentent iInterface au niveau de leur ancêtre tComponent
Type TComponent=
Class(TPersistent, IInterface, IInterfaceComponentReference)
... |
Notez que - dans cet exemple, i_rectangle ne sert pas à grand chose
- pour pouvoir utiliser un rectangle dans notre organisation, il a fallu prendre soin d'incorporer i_shape à c_rectangle (sinon nous n'aurions pas pu appeler add_shape, qui attend un i_shape et pas un i_rectangle)
- nous avons évité la libération des données, car nous souhaitons bien que notre rectangle local soit libéré, mais pas que Form1 le soit. En fait il faudrait tenir compte du _Release qui est géré de façon particulier pour
les tForm
5.3 - Interfaces et .Net En .Net (== Java), que nous avons présenté dans nos articles Delphi 2006, les Interfaces sont beaucoup plus utilisés. Il n'y a pas de tList ou
tObjectList, mais des conteneurs dérivés d'Interfaces collection. En fait la hiérarchie commence beaucoup "plus haut" - iEnumerator a la fonction MoveNext et l'élément courant Current (une
propriété, naturellement)
- iEnumerable sait récupérer un énumérateur par la fonction GetEnumérator
- iCollection et iList sont des descendants de plus en plus étoffés
- les véritables conteneurs sont Array, ArrayList, CollectionBase
- nous créons nos propres collections en dérivant de CollectionBase :
L'utilisation des Interfaces permet à .Net de nicher certaines fonctionnalités à un niveau plus élevé dans la hiérarchie des classes. Par
exemple la sensibilité aux données est au niveau d'un Edit (et évite la dichotomie tEdit, tDbEdit)
5.4 - Interfaces et Génériques Les génériques, introduits d'abord en Delphi 2007 pour la partie .Net de
Delphi, on mis en évidence l'utilisation des Interfaces pour définir des groupes de fonctionnalités. Pour créer un conteneur générique avec possibilités de - trier (== comparer)
- dupliquer
- sauvegarder sur disque
nous pourrions déclarer un type:
Type tSortedList<T: iComparable<T>, iCloneable<T>, iSerializable<T> > ...
| L'en-tête du type générique liste les contraintes que devront satisfaire les types actuels. De façons imagée, nous pourrions dire que nous allons faire
notre marché en choisissant "tiens, mettez moi un peu de triable, et que je pourrais aussi dupliquer, et aussi ...". En fait nous définissons une spécification.
5.5 - Interfaces et Design Patterns
Le afficionados des Design Patterns auront reconnu que notre exemple de liste de surfaces était en fait une implémentation du design pattern "composite" Ce pattern est recommandé lorsque nous souhaitons pouvoir effectuer les mêmes
traitements sur les éléments d'une structure et sur la structure. Schématiquement, notre hiérarchie ressemble à:
Un diagramme simplifié de "composite" pourrait être : En définissant i_shape et c_shape_list, nous nous préoccupons de la fonctionnalité pure
- spécifier les fonctionnalités que nous souhaitons utiliser
- mettre en oeuvre le conteneur
Lorsque nous aurons à utiliser cette mécanique, il suffira de créer des classes
qui implémentent nos Interfaces, c_shape_1, c_shape_2 etc.
En fait, toute la mécanique des Design Pattern repose entièrement sur ce principe: "coding to the interface"
- nous définissons des organisations d'Interface et de classes ancêtres, pour obtenir la fonctionnalité désirée.
- puis nous dérivons des classes réelles qui se substitueront aux ancêtres dans l'organisation précédente.
Dans notre exemple - "construire une organisation où les éléments et la structure peuvent effectuer les mêmes traitements", tout est défini au niveau de i_shape et c_shape_list
- le calcul réel est réalisé dans c_rectangle et tForm1
En gros un "framework" définit l'organisation, les classes réelles n'ont plus qu'à effectuer le traitement spécialisé.
En utilisant les Interface et en réfléchissant à l'organisation générale de votre projet plutôt que de coder directement, vous vous élevez du job de programmeur au rang d'architecte, ce qui, naturellement, vous vaudra
immédiatement considération, admiration, et justifiera même une une augmentation immédiate, voire une promotion. Plaisanterie mise à part, ces compétences-là seront transposable dans tout environnement, langage ou développements à venir.
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 autonome)
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 Pour les Interfaces
- Les interfaces d'objet sous Delphi - Laurent Dardenne - Fév 2007. Une introduction détaillé au niveau de la syntaxe des Interfaces Delphi
- dump interface - J Colibri - Mai 2°°4 - la description de l'organisation mémoire des Classes et Interfaces COM sous
Delphi. Démontre que les références d'Interfaces sont un pointeur différent du pointeur de l'objet
- Diagrammes de Classe UML - J Colibri -
présentation des diagrammes de Classe UML
- Uml avec Delphi : comment créer des diagrammes UML
avec Delphi et Together: diagramme de classe, cas d'utilisation et diagramme de séquence. Avec génération de code et synchronisation entre le code Pascal et les diagrammes
- et tous nos articles Design Pattern en général
- les articles sur les génériques
Nous présentons naturellement les règles, les bénéfices et les bonnes pratiques d'utilisation des Interfaces dans nos formations
- Formation Programmation Objet
Delphi présentation et pratique des techniques de la programmation orientée objet. Les techniques de bases et de nombreux exercices permettent de maîtriser les Interfaces
- Formation UML et Design Patterns
Delphi Analyse et Conception Objet - UML et les Design Patterns. Une formation présentant UML (entre autre les diagrammes de classe), et les design patterns, qui peuvent pratiquement tous être définis en utilisant des Interfaces
Nous intervenons aussi en tant qu'expert Delphi pour réorganiser les projets de nos clients, soit sous forme de contrats de
développement, soit sous forme de transfert de technologie. Et pour ce type de mission, UML, les Interfaces et les design pattern font partie de notre boîte à outil de base.
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. |