I. Introduction▲
Les interfaces ne définissent qu'un comportement (des méthodes) : il s'agit d'une coquille vide. En séparant la définition de l'implémentation, les interfaces permettent de découpler le code, c'est-à-dire de limiter les dépendances entre les modules. L'interface indique ce qu'il est possible de faire, la classe qui l'implémente étant alors en charge du comment le réaliser. Cela permet de travailler avec uniquement la ou les méthodes nécessaires et d'outrepasser les limites du polymorphisme pour les classes ayant une hiérarchie différente.
Les interfaces sont différentes des classes abstraites. Ces dernières sont des classes dont toutes les méthodes n'ont pas été implémentées. De plus, elles autorisent la déclaration de variables. Il est aussi à noter qu'une classe ne peut hériter que d'une seule classe alors qu'elle peut implémenter plusieurs interfaces.
La notion d'interface est différente de celle d'héritage. Un canard hérite de la classe animal, c'est-à-dire qu'il en reprend toutes les caractéristiques pour éventuellement en ajouter d'autres. En revanche, un canard et un avion peuvent voler : l'interface représente alors le point commun entre les deux.
Au premier abord, les interfaces peuvent paraître inutiles et donner l'impression de complexifier le code. Dans la pratique, c'est tout le contraire.
II. Présentation des interfaces▲
Une interface ne peut contenir que des méthodes publiques et des propriétés, mais aucune donnée. Il s'agit seulement d'une définition. Pour établir ou récupérer les données d'une propriété, il faut utiliser les méthodes setter ou getter.
Par convention, les interfaces commencent par la lettre majuscule I, suivie du mot clé interface, et peuvent comporter un GUID (Global Unique IDentifier). La présence de cet identifiant unique permet d'utiliser le transtypage (mot clé as). Il est préférable de déclarer les interfaces dans des unités séparées, toujours pour minimiser le couplage du code.
Le raccourci clavier Ctrl + Shift + G permet de générer automatiquement un GUID.
III. Découpler le code▲
Découpler le code permet d'avoir un code réutilisable et plus facile à maintenir. Une interface peut ainsi être utilisée dans plusieurs projets.
Il est important de garder uniquement les unités nécessaires dans les clauses uses. La clause supérieure (interface) est à utiliser si quelque chose l'utilise dans la partie interface, comme la déclaration d'une classe ou un type. Si l'unité n'est pas utilisée, elle peut être déplacée dans la clause implementation : cela participe à la limitation des dépendances dans un projet.
Si un type est uniquement nécessaire dans une unité, il faut aussi le déclarer dans la partie implementation. Il sera invisible pour le reste du projet : il n'est pas nécessaire de polluer le code avec des types ou méthodes inutiles.
De cette façon, une interface ne doit pas contenir des méthodes dont elle n'aurait pas besoin. Si c'est le cas, il faut sans doute diviser cette interface en plusieurs interfaces.
IV. Utilisation des interfaces▲
IV-A. Déclaration▲
Ci-dessous, voici la déclaration d'une interface pour un objet ayant la capacité de voler :
type
IObjetVolant = interface
['
{CA7FD50F-4A66-4E7A-B838-AA81F3253C62}
'
]
procedure
Voler;
end
;
Nous retrouvons le mot clé interface et le GUID généré automatiquement.
Ajoutons deux classes (un canard et un avion), les deux supportant l'interface IObjetVolant.
Quand une classe implémente une interface, elle doit définir l'ensemble de ses méthodes :
type
TCanard = class
(TInterfacedObject, IObjetVolant)
procedure
Voler;
end
;
TAvion = class
(TInterfacedObject, IObjetVolant)
procedure
Voler;
end
;
{
TCanard
}
procedure
TCanard.Voler;
begin
Writeln('
Le
canard
vole
'
);
end
;
{
TAvion
}
procedure
TAvion.Voler;
begin
Writeln('
L
'
'
avion
vole
'
);
end
;
Quand une classe prend en charge une interface (ou plusieurs séparées par des virgules), il est plus simple d'utiliser TInterfacedObject comme classe de base, car elle implémente les méthodes de IInterface. Elle gère automatiquement le comptage des références et la gestion mémoire.
Les méthodes _AddRef et _Release de IInterface gèrent la durée de vie des interfaces. Elles surveillent et incrémentent le compteur de références de l'objet quand une référence d'interface est passée à un client, et détruisent l'objet quand celui-ci n'a pas plus de référence.
Assigner la valeur nil à une interface va détruire son instance.
En utilisant cette méthode, il ne faut manipuler l'objet que sous la forme d'une référence d'interface, sinon l'objet peut être libéré de manière inopinée.
IV-B. Utilisation▲
Ajoutons une méthode pour faire voler un objet :
procedure
FaireVolerObjet(aObjetVolant: IObjetVolant);
begin
aObjetVolant.Voler;
end
;
Cette méthode attend une variable implémentant l'interface IObjetVolant et fait appel à la procédure Voler. Il s'agit de la seule méthode disponible pour l'interface.
Pour savoir si une classe supporte une interface, il existe la méthode Supports(aClasse, aInterface) qui renvoie un booléen.
Il est possible d'appeler la méthode ainsi définie avec les deux classes créées précédemment :
begin
try
FaireVolerObjet(TCanard.Create);
FaireVolerObjet(TAvion.Create);
Readln;
except
on
E: Exception do
Writeln(E.ClassName, '
:
'
, E.Message
);
end
;
end
.
Du fait du comptage des références, il n'y a pas besoin d'un bloc try…finally pour la libération des objets.
La procédure ne s'occupe pas du type passé en paramètre. Si une méthode FairePleinEssence est ajoutée à l'avion, la procédure qui fait voler un objet n'aura pas accès à celle-ci. Elle a uniquement accès aux méthodes de l'interface attendue. En effet, on n'autorise pas cette méthode à manipuler la totalité de l'objet : cela permet d'éviter les éventuelles erreurs.
Ci-dessous, voici un exemple de Nick Hodges dans son livre Coding in Delphi qui illustre bien cette nécessité de limiter les possibilités de manipulations d'objets :
« Quand une personne réalise un achat chez un commerçant, elle donne sa carte bancaire et le commerçant réalise le paiement. La personne ne donne pas son portefeuille au commerçant qui pourrait réaliser le paiement mais aussi toucher à tout ce qu'il contient, retirer des cartes, en ajouter, etc. »
Ici c'est pareil : la méthode fait voler un objet, elle n'a pas besoin de tout ce qu'il peut y avoir autour de ces objets.
IV-C. Héritage▲
Une classe peut implémenter plusieurs interfaces, alors qu'une interface ne peut hériter que d'une seule autre interface. Par exemple :
IObjetVolantMotorise = interface
(IObjetVolant)
['
{2044B0EE-37D6-4B05-A08D-A7D4B6F243A0}
'
]
procedure
ActiverTurbo;
end
;
La classe qui sera de type IObjetVolantMotorise devra implémenter les méthodes de cette interface et de celles de l'interface héritée.
Attention aux classes qui supportent des interfaces contenant des méthodes ayant le même nom !
Si deux interfaces déclarent une méthode ayant le même nom, dans l'implémentation de la classe il faut préfixer le nom de la méthode par le nom de l'interface et lui assigner une méthode de la classe.
I1 = interface
procedure
Proc;
end
;
I2 = interface
procedure
Proc;
end
;
TTest = class
(TInterfacedObject, I1, I2)
procedure
Proc;
procedure
I2.Proc = ProcInterne;
procedure
ProcInterne;
end
;
IV-D. Délégation▲
Il est courant d'avoir dans une classe une propriété qui est du type d'un autre objet. L'interface peut être utilisée comme type de la propriété.
Le mot clé implements permet de déléguer l'implémentation des méthodes à un sous-objet. La classe « mère » doit alors supporter l'interface.
TTestImplements = class
(TInterfacedObject, IObjetVolant)
strict
private
FObjetVolant: IObjetVolant;
public
property
ObjetVolant: IObjetVolant read
FObjetVolant write
FObjetVolant implements
IObjetVolant;
end
;
Il est à présent possible d'appeler la méthode Voler sur un objet de type TTestImplements alors que celui-ci n'implémente pas les méthodes de l'interface :
Test := TTestImplements.Create;
(Test as
IObjetVolant).Voler;
IV-E. Changement d'implémentation▲
L'utilisation des interfaces permet le changement d'implémentation pendant l'exécution du programme. Une variable est déclarée du type de l'interface (ici, IZip) et l'implémentation est choisie dynamiquement, à l'exécution :
procedure
Compresser(aFichier: TFile; const
IsSuperCompression: boolean
);
var
Zip: IZip;
begin
if
IsSuperCompression then
Zip := TSuperZip.Create
else
Zip := TZip.Create;
Zip.Compresser(aFichier);
end
;
IV-F. Les génériques▲
Les interfaces fonctionnent aussi avec les génériques, de la même manière qu'une classe. Par exemple :
IMammifere<T> = interface
procedure
Manger(aValue: T);
end
;
TTestDemo<T> = class
(TInterfacedObject, IMammifere)
private
procedure
Manger(aValue: T);
public
constructor
Create(aAnimal: TMammifere<T>);
end
;
V. Conclusion▲
Les interfaces doivent être utilisées à peu près tout le temps, dès que cela est possible. Cette utilisation est facile et le découplage du code est quelque chose de très important.
Les interfaces permettent de coder de manière abstraite, au contraire de l'implémentation.
Un projet doit être composé comme un ensemble de modules qui peuvent se connecter les uns aux autres pour former une application. Cette méthode de travail permet de limiter les erreurs et facilite la maintenance. Ainsi, il est possible de travailler en équipe sur plusieurs applications en utilisant les mêmes interfaces. Le fait de les déclarer dans des unités séparées permet de les utiliser sans se soucier des éventuels problèmes de dépendances. Les modifications sont donc centralisées et concernent alors tous les projets.
L'utilisation des interfaces oblige à réfléchir à l'organisation globale du projet et à coder différemment. Par la suite, les interfaces permettent de mettre en place les injections de dépendances et les différents patrons de conception qui pourront faire l'objet d'une présentation future.
VI. Remerciements▲
Je remercie gvasseur58 pour l'aide apportée pour la création et la correction de ce tutoriel, ainsi que Alcatîz pour son retour sur cet article. Merci à genthial pour la relecture orthographique.