Skip to content

Latest commit

 

History

History
913 lines (731 loc) · 21.4 KB

File metadata and controls

913 lines (731 loc) · 21.4 KB

🔝 Retour au Sommaire

3.7.5 Interfaces en Object Pascal

Introduction

Les interfaces sont un concept avancé de la programmation orientée objet qui permet de définir un "contrat" que les classes doivent respecter. Une interface décrit ce qu'un objet peut faire sans spécifier comment il le fait.

Qu'est-ce qu'une interface ?

Une interface est une définition abstraite d'un ensemble de méthodes et de propriétés qu'une classe doit implémenter. C'est comme un cahier des charges ou un contrat.

Analogie du monde réel

Imaginez une prise électrique :

  • L'interface = la forme standardisée de la prise (deux ou trois trous)
  • Les classes qui implémentent = les différents appareils (lampe, ordinateur, téléphone)
  • Tous ces appareils sont différents mais peuvent se brancher sur la même prise car ils respectent le même "contrat"

Différence entre classe et interface

Classe Interface
Contient des données (champs) Ne contient pas de données
Contient l'implémentation des méthodes Définit uniquement la signature des méthodes
Une classe peut hériter d'une seule classe Une classe peut implémenter plusieurs interfaces
Peut être instanciée Ne peut PAS être instanciée

Déclaration d'une interface

Une interface se déclare avec le mot-clé interface dans une définition de type :

type
  IDessinable = interface
    ['{3B8F9C12-5E4D-4A3B-9F2E-1C8D5A7B3F9E}']  // GUID
    procedure Dessiner;
    function ObtenirCouleur: string;
    procedure DefinirCouleur(const ACouleur: string);
  end;

Points importants

  1. Nom : par convention, les interfaces commencent par I (IDessinable, IComparable, etc.)
  2. GUID : un identifiant unique global entre crochets (optionnel mais recommandé)
  3. Pas d'implémentation : uniquement les signatures des méthodes
  4. Pas de visibilité : tout est public par défaut

Générer un GUID

Dans l'IDE Delphi, utilisez Ctrl+Shift+G pour générer automatiquement un GUID. Vous pouvez aussi utiliser cette fonction :

// Pour générer un GUID
uses System.SysUtils;

var
  MonGUID: TGUID;
begin
  CreateGUID(MonGUID);
  ShowMessage(GUIDToString(MonGUID));
end;

Implémenter une interface

Une classe implémente une interface en ajoutant le nom de l'interface après sa déclaration :

type
  // Déclaration de l'interface
  IDessinable = interface
    ['{3B8F9C12-5E4D-4A3B-9F2E-1C8D5A7B3F9E}']
    procedure Dessiner;
    function ObtenirCouleur: string;
    procedure DefinirCouleur(const ACouleur: string);
  end;

  // Classe qui implémente l'interface
  TCercle = class(TInterfacedObject, IDessinable)
  private
    FRayon: Double;
    FCouleur: string;
  public
    constructor Create(ARayon: Double);

    // Implémentation des méthodes de l'interface
    procedure Dessiner;
    function ObtenirCouleur: string;
    procedure DefinirCouleur(const ACouleur: string);

    property Rayon: Double read FRayon write FRayon;
  end;

constructor TCercle.Create(ARayon: Double);  
begin  
  inherited Create;
  FRayon := ARayon;
  FCouleur := 'noir';
end;

procedure TCercle.Dessiner;  
begin  
  ShowMessage(Format('Dessin d''un cercle %s de rayon %.2f',
                     [FCouleur, FRayon]));
end;

function TCercle.ObtenirCouleur: string;  
begin  
  Result := FCouleur;
end;

procedure TCercle.DefinirCouleur(const ACouleur: string);  
begin  
  FCouleur := ACouleur;
end;

TInterfacedObject

TInterfacedObject est une classe de base qui gère automatiquement le comptage de références des interfaces. C'est important pour la gestion mémoire automatique des interfaces.

Utilisation d'une interface

var
  MonCercle: IDessinable;  // Variable de type interface
begin
  MonCercle := TCercle.Create(5);

  // Pas besoin de try...finally avec les interfaces !
  // La mémoire est libérée automatiquement

  MonCercle.DefinirCouleur('rouge');
  MonCercle.Dessiner;

  ShowMessage('Couleur : ' + MonCercle.ObtenirCouleur);

  // MonCercle sera automatiquement libéré à la fin du bloc
end;

Avantage : gestion mémoire automatique

Avec les interfaces, vous n'avez pas besoin d'appeler Free ! La mémoire est libérée automatiquement grâce au comptage de références.

Interfaces multiples

Une classe peut implémenter plusieurs interfaces :

type
  IDessinable = interface
    ['{3B8F9C12-5E4D-4A3B-9F2E-1C8D5A7B3F9E}']
    procedure Dessiner;
  end;

  IRedimensionnable = interface
    ['{7F2A4B8C-1D3E-4F5A-8B9C-2D4E6F8A1B3C}']
    procedure Agrandir(Facteur: Double);
    procedure Retrecir(Facteur: Double);
  end;

  IColorable = interface
    ['{9E4D7C2B-3A5F-4E8D-9C2A-5F7E9D3B8C4A}']
    function ObtenirCouleur: string;
    procedure DefinirCouleur(const ACouleur: string);
  end;

  // Classe implémentant trois interfaces
  TRectangle = class(TInterfacedObject, IDessinable, IRedimensionnable, IColorable)
  private
    FLargeur: Double;
    FHauteur: Double;
    FCouleur: string;
  public
    constructor Create(ALargeur, AHauteur: Double);

    // Méthodes de IDessinable
    procedure Dessiner;

    // Méthodes de IRedimensionnable
    procedure Agrandir(Facteur: Double);
    procedure Retrecir(Facteur: Double);

    // Méthodes de IColorable
    function ObtenirCouleur: string;
    procedure DefinirCouleur(const ACouleur: string);
  end;

constructor TRectangle.Create(ALargeur, AHauteur: Double);  
begin  
  inherited Create;
  FLargeur := ALargeur;
  FHauteur := AHauteur;
  FCouleur := 'bleu';
end;

procedure TRectangle.Dessiner;  
begin  
  ShowMessage(Format('Rectangle %s : %.2f x %.2f',
                     [FCouleur, FLargeur, FHauteur]));
end;

procedure TRectangle.Agrandir(Facteur: Double);  
begin  
  FLargeur := FLargeur * Facteur;
  FHauteur := FHauteur * Facteur;
end;

procedure TRectangle.Retrecir(Facteur: Double);  
begin  
  Agrandir(1 / Facteur);
end;

function TRectangle.ObtenirCouleur: string;  
begin  
  Result := FCouleur;
end;

procedure TRectangle.DefinirCouleur(const ACouleur: string);  
begin  
  FCouleur := ACouleur;
end;

Utilisation avec plusieurs interfaces

var
  MonRectangle: TRectangle;
  Dessinable: IDessinable;
  Redimensionnable: IRedimensionnable;
  Colorable: IColorable;
begin
  MonRectangle := TRectangle.Create(10, 5);
  try
    // Utilisation directe
    MonRectangle.Dessiner;

    // Affectation à une variable d'interface
    Dessinable := MonRectangle;
    Dessinable.Dessiner;

    Redimensionnable := MonRectangle;
    Redimensionnable.Agrandir(2);

    Colorable := MonRectangle;
    Colorable.DefinirCouleur('vert');

    MonRectangle.Dessiner;
  finally
    MonRectangle.Free;
  end;
end;

L'opérateur Supports

L'opérateur Supports vérifie si un objet implémente une interface donnée :

var
  MonObjet: TObject;
  Dessinable: IDessinable;
begin
  MonObjet := TRectangle.Create(10, 5);
  try
    // Vérifier si l'objet supporte l'interface
    if Supports(MonObjet, IDessinable, Dessinable) then
    begin
      ShowMessage('L''objet peut être dessiné');
      Dessinable.Dessiner;
    end
    else
      ShowMessage('L''objet ne peut pas être dessiné');

  finally
    MonObjet.Free;
  end;
end;

L'opérateur as avec les interfaces

On peut aussi utiliser as pour convertir vers une interface :

var
  MonRectangle: TRectangle;
  Dessinable: IDessinable;
begin
  MonRectangle := TRectangle.Create(10, 5);
  try
    // Conversion vers l'interface
    Dessinable := MonRectangle as IDessinable;
    Dessinable.Dessiner;

  finally
    MonRectangle.Free;
  end;
end;

Héritage d'interfaces

Les interfaces peuvent hériter d'autres interfaces :

type
  IAnimal = interface
    ['{1A2B3C4D-5E6F-7A8B-9C0D-1E2F3A4B5C6D}']
    procedure SeNourrir;
    procedure Dormir;
  end;

  IAnimalDomestique = interface(IAnimal)  // Hérite de IAnimal
    ['{7F8E9D0C-1B2A-3D4E-5F6A-7B8C9D0E1F2A}']
    procedure Caresser;
    procedure Nommer(const ANom: string);
  end;

  TChien = class(TInterfacedObject, IAnimalDomestique)
  private
    FNom: string;
  public
    // Méthodes de IAnimal
    procedure SeNourrir;
    procedure Dormir;

    // Méthodes de IAnimalDomestique
    procedure Caresser;
    procedure Nommer(const ANom: string);
  end;

procedure TChien.SeNourrir;  
begin  
  ShowMessage(FNom + ' mange des croquettes');
end;

procedure TChien.Dormir;  
begin  
  ShowMessage(FNom + ' dort dans son panier');
end;

procedure TChien.Caresser;  
begin  
  ShowMessage(FNom + ' remue la queue de joie !');
end;

procedure TChien.Nommer(const ANom: string);  
begin  
  FNom := ANom;
end;

Exemple pratique : Plugin System

Les interfaces sont parfaites pour créer des systèmes de plugins :

type
  // Interface commune à tous les plugins
  IPlugin = interface
    ['{8D3E4F5A-6B7C-8D9E-0F1A-2B3C4D5E6F7A}']
    function ObtenirNom: string;
    function ObtenirVersion: string;
    procedure Initialiser;
    procedure Executer;
    procedure Terminer;
  end;

  // Plugin de sauvegarde
  TPluginSauvegarde = class(TInterfacedObject, IPlugin)
  public
    function ObtenirNom: string;
    function ObtenirVersion: string;
    procedure Initialiser;
    procedure Executer;
    procedure Terminer;
  end;

  // Plugin d'export
  TPluginExport = class(TInterfacedObject, IPlugin)
  public
    function ObtenirNom: string;
    function ObtenirVersion: string;
    procedure Initialiser;
    procedure Executer;
    procedure Terminer;
  end;

// Implémentation TPluginSauvegarde

function TPluginSauvegarde.ObtenirNom: string;  
begin  
  Result := 'Plugin de sauvegarde';
end;

function TPluginSauvegarde.ObtenirVersion: string;  
begin  
  Result := '1.0';
end;

procedure TPluginSauvegarde.Initialiser;  
begin  
  ShowMessage('Initialisation du plugin de sauvegarde');
end;

procedure TPluginSauvegarde.Executer;  
begin  
  ShowMessage('Sauvegarde des données...');
end;

procedure TPluginSauvegarde.Terminer;  
begin  
  ShowMessage('Plugin de sauvegarde terminé');
end;

// Implémentation TPluginExport

function TPluginExport.ObtenirNom: string;  
begin  
  Result := 'Plugin d''export';
end;

function TPluginExport.ObtenirVersion: string;  
begin  
  Result := '2.0';
end;

procedure TPluginExport.Initialiser;  
begin  
  ShowMessage('Initialisation du plugin d''export');
end;

procedure TPluginExport.Executer;  
begin  
  ShowMessage('Export des données vers Excel...');
end;

procedure TPluginExport.Terminer;  
begin  
  ShowMessage('Plugin d''export terminé');
end;

Gestionnaire de plugins

type
  TGestionnairePlugins = class
  private
    FPlugins: TList<IPlugin>;
  public
    constructor Create;
    destructor Destroy; override;
    procedure AjouterPlugin(Plugin: IPlugin);
    procedure ExecuterTousLesPlugins;
  end;

constructor TGestionnairePlugins.Create;  
begin  
  inherited Create;
  FPlugins := TList<IPlugin>.Create;
end;

destructor TGestionnairePlugins.Destroy;  
begin  
  FPlugins.Free;
  inherited Destroy;
end;

procedure TGestionnairePlugins.AjouterPlugin(Plugin: IPlugin);  
begin  
  FPlugins.Add(Plugin);
end;

procedure TGestionnairePlugins.ExecuterTousLesPlugins;  
var  
  Plugin: IPlugin;
begin
  for Plugin in FPlugins do
  begin
    ShowMessage(Format('Plugin : %s (v%s)',
                       [Plugin.ObtenirNom, Plugin.ObtenirVersion]));
    Plugin.Initialiser;
    Plugin.Executer;
    Plugin.Terminer;
  end;
end;

Utilisation

var
  Gestionnaire: TGestionnairePlugins;
begin
  Gestionnaire := TGestionnairePlugins.Create;
  try
    // Ajouter des plugins
    Gestionnaire.AjouterPlugin(TPluginSauvegarde.Create);
    Gestionnaire.AjouterPlugin(TPluginExport.Create);

    // Exécuter tous les plugins
    Gestionnaire.ExecuterTousLesPlugins;
  finally
    Gestionnaire.Free;
  end;
end;

Interfaces et propriétés

Les interfaces peuvent déclarer des propriétés avec des getters et setters :

type
  IPersonne = interface
    ['{2C4D6E8F-1A3B-5C7D-9E0F-1A2B3C4D5E6F}']
    function GetNom: string;
    procedure SetNom(const Value: string);
    function GetAge: Integer;
    procedure SetAge(const Value: Integer);

    property Nom: string read GetNom write SetNom;
    property Age: Integer read GetAge write SetAge;
  end;

  TPersonne = class(TInterfacedObject, IPersonne)
  private
    FNom: string;
    FAge: Integer;
    function GetNom: string;
    procedure SetNom(const Value: string);
    function GetAge: Integer;
    procedure SetAge(const Value: Integer);
  public
    property Nom: string read GetNom write SetNom;
    property Age: Integer read GetAge write SetAge;
  end;

function TPersonne.GetNom: string;  
begin  
  Result := FNom;
end;

procedure TPersonne.SetNom(const Value: string);  
begin  
  FNom := Value;
end;

function TPersonne.GetAge: Integer;  
begin  
  Result := FAge;
end;

procedure TPersonne.SetAge(const Value: Integer);  
begin  
  if (Value >= 0) and (Value <= 150) then
    FAge := Value;
end;

Utilisation

var
  Personne: IPersonne;
begin
  Personne := TPersonne.Create;

  Personne.Nom := 'Marie Dupont';
  Personne.Age := 30;

  ShowMessage(Format('%s a %d ans', [Personne.Nom, Personne.Age]));

  // Libération automatique
end;

Délégation d'interface

Delphi permet de déléguer l'implémentation d'une interface à un champ avec le mot-clé implements :

type
  ICalculatrice = interface
    ['{5A6B7C8D-9E0F-1A2B-3C4D-5E6F7A8B9C0D}']
    function Additionner(A, B: Integer): Integer;
    function Soustraire(A, B: Integer): Integer;
  end;

  TMoteurCalcul = class(TInterfacedObject, ICalculatrice)
  public
    function Additionner(A, B: Integer): Integer;
    function Soustraire(A, B: Integer): Integer;
  end;

  // Classe qui délègue l'interface
  TCalculatriceScientifique = class(TInterfacedObject, ICalculatrice)
  private
    FMoteur: ICalculatrice;  // Délégation à ce champ
  public
    constructor Create;
    property Moteur: ICalculatrice read FMoteur implements ICalculatrice;
  end;

function TMoteurCalcul.Additionner(A, B: Integer): Integer;  
begin  
  Result := A + B;
end;

function TMoteurCalcul.Soustraire(A, B: Integer): Integer;  
begin  
  Result := A - B;
end;

constructor TCalculatriceScientifique.Create;  
begin  
  inherited Create;
  FMoteur := TMoteurCalcul.Create;
end;

Interfaces vs Classes abstraites

Critère Interface Classe abstraite
Héritage multiple ✅ Oui ❌ Non (héritage simple uniquement)
Implémentation ❌ Aucune ✅ Peut avoir des méthodes implémentées
Champs/données ❌ Non ✅ Oui
Gestion mémoire ✅ Automatique ❌ Manuelle (Free)
Flexibilité ✅ Plus flexible ⚠️ Moins flexible
Performance ⚠️ Légèrement plus lent ✅ Plus rapide

Quand utiliser une interface ?

  • Vous avez besoin d'implémenter plusieurs "contrats"
  • Vous voulez la gestion mémoire automatique
  • Vous créez un système de plugins
  • Vous voulez maximiser la flexibilité

Quand utiliser une classe abstraite ?

  • Vous voulez partager du code commun entre classes
  • Vous avez des données à stocker
  • L'héritage simple suffit
  • La performance est critique

Exemple complet : Système de notification

type
  // Interface de notification
  INotification = interface
    ['{9B8C7D6E-5F4A-3B2C-1D0E-9F8A7B6C5D4E}']
    procedure Envoyer(const AMessage: string);
    function EstDisponible: Boolean;
  end;

  // Notification par email
  TNotificationEmail = class(TInterfacedObject, INotification)
  private
    FAdresseEmail: string;
  public
    constructor Create(const AEmail: string);
    procedure Envoyer(const AMessage: string);
    function EstDisponible: Boolean;
  end;

  // Notification par SMS
  TNotificationSMS = class(TInterfacedObject, INotification)
  private
    FNumeroTelephone: string;
  public
    constructor Create(const ANumero: string);
    procedure Envoyer(const AMessage: string);
    function EstDisponible: Boolean;
  end;

  // Notification push
  TNotificationPush = class(TInterfacedObject, INotification)
  private
    FDeviceToken: string;
  public
    constructor Create(const AToken: string);
    procedure Envoyer(const AMessage: string);
    function EstDisponible: Boolean;
  end;

  // Gestionnaire de notifications
  TGestionnaireNotifications = class
  private
    FNotifications: TList<INotification>;
  public
    constructor Create;
    destructor Destroy; override;
    procedure AjouterCanal(Notification: INotification);
    procedure DiffuserMessage(const AMessage: string);
  end;

// Implémentation TNotificationEmail

constructor TNotificationEmail.Create(const AEmail: string);  
begin  
  inherited Create;
  FAdresseEmail := AEmail;
end;

procedure TNotificationEmail.Envoyer(const AMessage: string);  
begin  
  ShowMessage(Format('Email envoyé à %s : %s', [FAdresseEmail, AMessage]));
end;

function TNotificationEmail.EstDisponible: Boolean;  
begin  
  Result := FAdresseEmail <> '';
end;

// Implémentation TNotificationSMS

constructor TNotificationSMS.Create(const ANumero: string);  
begin  
  inherited Create;
  FNumeroTelephone := ANumero;
end;

procedure TNotificationSMS.Envoyer(const AMessage: string);  
begin  
  ShowMessage(Format('SMS envoyé au %s : %s', [FNumeroTelephone, AMessage]));
end;

function TNotificationSMS.EstDisponible: Boolean;  
begin  
  Result := FNumeroTelephone <> '';
end;

// Implémentation TNotificationPush

constructor TNotificationPush.Create(const AToken: string);  
begin  
  inherited Create;
  FDeviceToken := AToken;
end;

procedure TNotificationPush.Envoyer(const AMessage: string);  
begin  
  ShowMessage(Format('Notification push envoyée : %s', [AMessage]));
end;

function TNotificationPush.EstDisponible: Boolean;  
begin  
  Result := FDeviceToken <> '';
end;

// Implémentation TGestionnaireNotifications

constructor TGestionnaireNotifications.Create;  
begin  
  inherited Create;
  FNotifications := TList<INotification>.Create;
end;

destructor TGestionnaireNotifications.Destroy;  
begin  
  FNotifications.Free;
  inherited Destroy;
end;

procedure TGestionnaireNotifications.AjouterCanal(Notification: INotification);  
begin  
  FNotifications.Add(Notification);
end;

procedure TGestionnaireNotifications.DiffuserMessage(const AMessage: string);  
var  
  Notification: INotification;
begin
  for Notification in FNotifications do
  begin
    if Notification.EstDisponible then
      Notification.Envoyer(AMessage);
  end;
end;

Utilisation du système de notification

var
  Gestionnaire: TGestionnaireNotifications;
begin
  Gestionnaire := TGestionnaireNotifications.Create;
  try
    // Ajouter différents canaux de notification
    Gestionnaire.AjouterCanal(TNotificationEmail.Create('user@example.com'));
    Gestionnaire.AjouterCanal(TNotificationSMS.Create('+33612345678'));
    Gestionnaire.AjouterCanal(TNotificationPush.Create('device-token-123'));

    // Diffuser un message sur tous les canaux
    Gestionnaire.DiffuserMessage('Votre commande a été expédiée !');
  finally
    Gestionnaire.Free;
  end;
end;

Bonnes pratiques

1. Préfixez les interfaces par "I"

// ✅ Bon
IDessinable, IComparable, INotification

// ❌ Mauvais
Dessinable, Comparable, Notification

2. Utilisez toujours des GUID

// ✅ Bon
IMonInterface = interface
  ['{3B8F9C12-5E4D-4A3B-9F2E-1C8D5A7B3F9E}']
  ...
end;

// ⚠️ Fonctionne mais déconseillé
IMonInterface = interface
  ...
end;

3. Héritez de TInterfacedObject pour la gestion mémoire automatique

// ✅ Bon
TMaClasse = class(TInterfacedObject, IMonInterface)

// ⚠️ Possible mais nécessite une gestion manuelle
TMaClasse = class(TObject, IMonInterface)

4. Gardez les interfaces simples et focalisées

// ✅ Bon - interfaces simples et spécifiques
ILisible = interface
  procedure Lire;
end;

IEcrivable = interface
  procedure Ecrire;
end;

// ❌ Éviter - interface trop large
IFichier = interface
  procedure Lire;
  procedure Ecrire;
  procedure Copier;
  procedure Deplacer;
  procedure Supprimer;
  procedure Renommer;
  // ... trop de responsabilités
end;

5. Préférez les interfaces pour les dépendances

// ✅ Bon - dépend d'une interface
procedure TraiterDonnees(Source: ISourceDonnees);

// ⚠️ Moins flexible - dépend d'une classe concrète
procedure TraiterDonnees(Source: TBaseDonnees);

Résumé

  • Interface = contrat définissant ce qu'une classe doit faire

    • Syntaxe : interface avec un GUID
    • Nom conventionnel : préfixe I
    • Pas d'implémentation, uniquement des signatures
  • Implémentation

    • Une classe peut implémenter plusieurs interfaces
    • Hériter de TInterfacedObject pour la gestion mémoire automatique
    • Toutes les méthodes de l'interface doivent être implémentées
  • Avantages

    • Héritage multiple
    • Gestion mémoire automatique
    • Flexibilité et découplage
    • Parfait pour les systèmes de plugins
  • Opérateurs

    • Supports : vérifier si un objet implémente une interface
    • as : convertir vers une interface
    • implements : déléguer l'implémentation
  • Utilisation

    • Systèmes de plugins
    • Injection de dépendances
    • Architecture découplée
    • Contrats entre modules

Les interfaces sont un outil puissant pour créer du code flexible, testable et maintenable. Elles favorisent le principe de programmation "contre une abstraction" plutôt que "contre une implémentation".

⏭️ Généricité