C#Interfaces

On nomme interface un contrat auquel les classes l'implémentant seront assujetties. Ce qu'on nomme l'héritage d'interfaces joue deux grandes rôles en programmation orientée objet :

Un exemple simple d'utilisation d'interfaces avec C# serait :

using System;
using System.Collections.Generic;
namespace z
{
   interface Volant
   {
      void Décoller();
      void Atterrir();
   }
   class Rumeur : Volant
   {
      public void Décoller()
      {
         Console.WriteLine("Cette rumeur décolle!");
      }
      public void Atterrir()
      {
         Console.WriteLine("Cette rumeur tombe à plat...");
      }
   }
   class Oiseau : Volant
   {
      public void Décoller()
      {
         Console.WriteLine("Pit pit, je décolle");
      }
      public void Atterrir()
      {
         Console.WriteLine("Pit pit, j'atterris!");
      }
   }
   class Couteau : Volant
   {
      public void Décoller()
      {
         Console.WriteLine("Les couteaux volent bas...");
      }
      public void Atterrir()
      {
         Console.WriteLine("... c'est bien connu!");
      }
   }
   
   class Program
   {
      static void Main(string[] args)
      {
         List<Volant> volants = new List<Volant>();
         volants.Add(new Rumeur());
         volants.Add(new Oiseau());
         volants.Add(new Couteau());
         foreach (Volant v in volants)
         {
            v.Décoller();
            v.Atterrir();
         }
      }
   }
}

Dans cet exemple, le polymorphisme est appliqué à travers l'interface Volant implémentée par trois classes sans autre « parent » commun.

Interfaces, polymorphisme et particularités de C#

Un réflexe que plusieurs ont (votre humble serviteur inclus) est de voir les interfaces dans un langage comme C# sous la forme de classes abstraites, soumises à des règles connexes, mais il se trouve que C# offre un traitement quelque peu différent aux interfaces de celui que ce langage réserve, à titre d'exemple, aux classes abstraites.

Ce qui suit montre un exemple de telles différences. Examinons tout d'abord un héritage en deux temps, avec MachinAbstrait une classe abstraite exposant un service abstrait f(), et Machin son descendant direct implémentant le service f() :

Exemple avec classe abstraite
using System;
namespace z
{
   abstract class MachinAbstrait
   {
      public abstract void f();
   }
   class Machin : MachinAbstrait
   {
      public override void f()
      {
         Console.WriteLine("Machin");
      }
   }
   class Program
   {
      static void Main(string[] args)
      {
         MachinAbstrait p = new Machin();
         p.f();
      }
   }
}

Remarquez la combinaison des mots clés :

Examinons maintenant un héritage en trois temps, avec MachinAbstrait une classe abstraite exposant un service abstrait f(), Machin son descendant direct implémentant le service f() en question et Truc, un descendant de Machin spécialisant f() un peu plus :

Exemple avec classe abstraite
using System;
namespace z
{
   abstract class MachinAbstrait
   {
      public abstract void f();
   }
   class Machin : MachinAbstrait
   {
      public override void f()
      {
         Console.WriteLine("Machin");
      }
   }
   class Truc : Machin
   {
      public override void f()
      {
         Console.WriteLine("Truc");
      }
   }  
   class Program
   {
      static void Main(string[] args)
      {
         MachinAbstrait p = new Truc();
         p.f();
      }
   }
}

Ce qui doit être remarqué ici est que dans Truc, pour proposer une méthode f(), deux qualifications sont possibles, soit new (pour masquer celle du parent dans l'enfant) et override (pour spécialiser celle du parent dans l'enfant).

Examinons maintenant un autre design en deux temps, avec IMachin une interface exposant un service abstrait f(), Machin son descendant direct implémentant le service f() en question :

Exemple avec classe abstraite
using System;
namespace z
{
   interface IMachin
   {
      void f();
   }
   class Machin : IMachin
   {
      public void f()
      {
         Console.WriteLine("Machin");
      }
   }
   class Program
   {
      static void Main(string[] args)
      {
         IMachin p = new Machin();
         p.f();
      }
   }
}

Remarquez que dans ce cas-ci, le fait pour Machin d'implémenter IMachin lui impose la contrainte d'écrire la méthode f() de même signature que celle déclarée dans IMachin. Ceci impose d'ailleurs à Machin la contrainte (superflue) que qualifier f() de public parce que les méthodes abstraites d'une interface sont elles-mêmes publiques (en C++ par exemple, cet irritant n'existe pas, et il est normal pour une interface d'exposer un service public alors que ses enfants implémentent le service de manière privée; ceci a pour effet de forcer les clients à solliciter les services à travers l'interface, tout simplement).

Notez par ailleurs que dans Machin, la méthode f() n'est pas qualifiée override, même si utiliser un Machin comme un IMachin permet le polymorphisme sur la méthode f(). Cette subtilité met de l'avant une différence clé du modèle de C# avec celui de C++, différence qui amène des surprises (voir plus bas).

Examinons enfin une version en trois temps de ce design, avec IMachin une interface exposant un service abstrait f(), Machin son descendant direct implémentant le service f() en question et Truc, un descendant de Machin spécialisant f() un peu plus :

Exemple avec classe abstraite
using System;
namespace z
{
   interface IMachin
   {
      void f();
   }
   class Machin : IMachin
   {
      public virtual void f()
      {
         Console.WriteLine("Machin");
      }
   }
   class Truc : Machin
   {
      public override void f()
      {
         Console.WriteLine("Truc");
      }
   }  
   class Program
   {
      static void Main(string[] args)
      {
         IMachin p = new Truc();
         p.f();
      }
   }
}

Remarquez que cette fois, la méthode Machin.f() a été qualifiée de virtual. En effet, sans ajouter ce mot, la méthode f() n'était pas considérée par un compilateur C# comme étant polymorphique à travers Machin. Puisque nous souhaitions spécialiser un peu plus le comportement de IMachin.f() dans Truc, il nous a fallu qualifier Machin.f() de virtual et Truc.f() avec la mention override.

Constat : en C#, le polymorphisme qui prend racine à travers une classe est transitif, alors que celui qui prend racine dans une interface ne l'est pas de prime abord, mais cette transitivité peut étre provoquée par une forme d'enchaînement; comme on peut le constater avec le dernier exemple, une fois Machin.f() qualifiée virtual et Truc.f() qualifiée override, l'appel de f() à partir un IMachin tel que p est bel et bien polymorphique.

Il n'existe pas de langage simple...


Valid XHTML 1.0 Transitional

CSS Valide !