Interface | la base

Quand on m’a enseigné au cégep qu’est-ce qu’une interface, j’ai compris qu’une fois que l’on implémentait la dite interface dans une classe, c’était comme un contrat. Pour moi, le fait de mettre le nom de l’interface après le nom de la classe séparé par ‘:’ devenait un contrat qui exigeait que l’on implémente tous les éléments de l’interface.

Je n’arrivais pas à comprendre pourquoi est-ce qu’il fallait que je me passe un contrat à moi-même qui exige d’écrire des bouts de code dont je suis l’auteur et que forcément j’allais écrire.

J’étais bien loin du compte et je n’avais encore rien vue ni compris de ce que peux faire une interface et je n’avais surtout pas encore capté toute la beauté de la chose.

Alors voilà, au fil du temps et mes programmes de formations continue m’ont amené à finalement comprendre comment ça fonctionne. Je prendrai ici le temps de vous l’expliquer si vous êtes comme moi il y a un certain temps.

La base

Pour commencer, je commencerai par vous expliquer qu’est-ce qu’une interface en utilisant une analogie.

Imaginez-vous une interface comme un masque représentant un personnage de théâtre qu’un humain peut revêtir. Une fois le masque revêtu, vous devez jouer le rôle, peu importe qui vous êtes vraiment et surtout, peu importe toute les autres choses que vous pouvez penser, dire ou être derrière le masque.

De la même manière que l’humain revêt le masque et joue son rôle, une classe qui implémente une interface se doit de jouer le rôle programmé dans l’interface, peu importe tout ce qu’elle peut faire d’autre de manière publique ou privée.

Ce n’est donc pas une question de contrat comme je pouvais me l’imaginer au départ, mais une question de rôle ou de fonctionnalité si on peut dire.

Alors comment est-ce que cela se concrétise dans le code et pourquoi aurais-je besoin de ça ?

Implémenter une interface

Avant d’implémenter une interface, il faut d’abord la créer et c’est ce que je ferai ici en utilisant des exemples qui seront écrits en C#.

D’abord établissons le rôle que l’on veut attribuer à une classe. Disons que le rôle consiste en une méthode qui additionne deux entiers et à l’existence d’une propriété qui prend la valeur de l’addition. C’est un peu fou comme exemple, mais c’est uniquement pour la démonstration et pour montrer qu’on peut avoir plus que des méthodes dans une interface.

Alors nous créons l’interface comme suit :

namespace InterfacePlay { public interface IFunAddition { int Addition(int x, int y); public int Resultat { get; set; } } }

Maintenant que notre rôle a été déterminé, nous allons faire en sorte qu’une classe le joue de cette manière en ajoutant le nom de l’interface après le nom de la classe et en implémentant les éléments de l’interface comme suit :

public class FunAddition : IFunAddition { public int Resultat { get; set; } public FunAddition(int x, int y) { Addition(x, y); } private void Addition(int x, int y) { Resultat = x + y; } }

Là où je voudrais attirer votre attention, c’est sur la différence entre l’interface et la classe. La classe possède bien l’implémentation des deux éléments de l’interface en plus d’une méthode private qui additionne les deux entiers. Donc, la classe est prête à jouer le rôle, mais comme vous pouvez voir, elle peut le faire à sa manière comme l’existence de la méthode private le démontre, sans que cela change son rôle.

Voilà donc une des choses importantes quand une classe implémente une interface, c’est qu’elle le fait à sa manière tout en cachant bien comment elle le fait ainsi que ses autres éléments, même s’ils sont publics. J’aurais bien pu changer la portée de la méthode Addition pour public une fois le rôle revêtu (ou l’interface implémentée) que cela ne changerais rien. Vous verrez pourquoi un peu plus loin.

Voyons donc comment cela s’utilise en pratique et où cela devient intéressant.

Utiliser une interface

Dans cette partie, pour faire une mise en scène un peu plus vrai, on va simuler que l’on fait une partie de code pour un client REST. On va d’abord utiliser une classe concrète pour le client REST pour fin de comparaison, ensuite nous emploierons une interface pour interagir avec les fonctionnalités de la classe.

Le code utilisé pour les exemples provient d’un projet que j’ai fait sur github. Vous pouvez donc vous référer au projet complet si vous en sentez le besoin.

La classe REST pour le client

La classe REST n’en est pas une vraie. On va simuler que c’est une vraie classe, car à un moment, elle s’identifie en donnant une valeur à un événement comme suit « RestClient » dans une propriété d’un événement RestServerResponded qu’elle retourne. Mais, pour la démonstration, nous la considérerons comme un vrai client REST.

using System.Net; namespace InterfacePlay { public class RestClient { public void AddRestRequest(RestRequest restRequest) { MakeRealCallWithRestServer(restRequest); } public event OnRestServerResponded RestServerResponded; private void MakeRealCallWithRestServer(RestRequest restRequest) { var status = MakeRequest(restRequest); SendStatusToCaller(status); } private void SendStatusToCaller(HttpStatusCode status) { var restEvent = new RestEvent() { HttpStatusCode = status, RestClientName = "RestClient" }; RestServerResponded?.Invoke(this, restEvent); } private HttpStatusCode MakeRequest(RestRequest restRequest) => HttpStatusCode.OK; } }

La classe qui utilise le client REST

Nous avons donc ici une classe qui utilise le client REST afin que ce dernier fasse une requête et retourne quelque chose par la suite par un événement RestServerResponded.

using System.Net;

namespace InterfacePlay
{
    public class ConsumerSansInterface
    {
        private const string request = "http://127.0.0.1/queryThat?option=this";

        private RestClient restClient = new RestClient();

        public ConsumerSansInterface()
        {
            InitializeComponent();
            SendRequest();
        }

        public void SendRequest()
        {
            var restRequest = new RestRequest()
            {
                Request = request
            };
            restClient.AddRestRequest(restRequest);
        }

        public HttpStatusCode HttpStatusCode { get; set; } = HttpStatusCode.BadRequest;

        public string RestClientName { get; set; }

        private void InitializeComponent()
        {
            restClient.RestServerResponded += (sender, restEvent) =>
            {
                HttpStatusCode = restEvent.HttpStatusCode;
                RestClientName = restEvent.RestClientName;
            };
        }
    }
}

La ligne importante à comprendre est la suivante :

        private RestClient restClient = new RestClient();

Nous avons donc un schéma standard d’une classe qui en utilise un autre en déclarant et manipulant directement un objet de cette autre classe. C’est un couplage normal si on peut dire. Si on fait un test comme vous pouvez en trouver sur le projet « ConsumerSansInterfaceTest« , vous pourriez vous rendre compte que la classe utilisée est bien RestClient en débuguant ou en évaluant la valeur de la propriété RestClientName retournée par l’événement RestServerResponded. Maintenant faisons la même chose en utilisant une interface.

Utiliser le client à travers une interface

À présent, nous ferons presque la même chose que dans le schéma standard, mais cette fois-ci, nous implémenterons l’interface IRestClient dans la classe RestClient en premier lieu. Ensuite, à partir de la classe qui utilise le client REST, nous déclarerons une interface de type IRestClient et ce sera avec cette interface que nous interagirons.

Le schéma sera très simple pour la démonstration. Ce que nous ferons, c’est que nous déclarerons une variable de type IRestClient et nous y collerons un objet de la classe RestClient derrière pour jouer le rôle défini par l’interface. À aucun moment, la classe ConsumerAvecInterface n’utilise la classe RestClient directement. ConsumerAvecInterface passe par l’interface IRestClient.

RestClient implémente IRestClient

La classe implémente l’interface IRestClient en ajoutant le nom de l’interface après la déclaration de la classe et parce qu’elle possède et implémente tous les éléments de l’interface.

using System.Net;

namespace InterfacePlay
{
    public class RestClient : IRestClient
    {
        public void AddRestRequest(RestRequest restRequest)
        {
            MakeRealCallWithRestServer(restRequest);
        }

        public void HelloWorld() {string h = "HelloWorld";}

        public event OnRestServerResponded RestServerResponded;

        private void MakeRealCallWithRestServer(RestRequest restRequest)
        {
            var status = MakeRequest(restRequest);
            SendStatusToCaller(status);
        }

        private void SendStatusToCaller(HttpStatusCode status)
        {
            var restEvent = new RestEvent()
            {
                HttpStatusCode = status,
                RestClientName = "RestClient"
            };
            RestServerResponded?.Invoke(this, restEvent);
        }

        private HttpStatusCode MakeRequest(RestRequest restRequest) => HttpStatusCode.OK;
    }
}

Comme vous pouvez le voir, la seule différence entre cette classe RestClient et l’autre que nous avons vu dans l’exemple de schéma standard, c’est le nom de l’interface collé à côté du nom de la classe. C’est pour dire que ce n’est pas difficile d’implémenter une interface « généralement ».

La classe a un élément d’ajouté que l’interface ne possède pas : la méthode public HelloWorld. Cela sert à démontrer que la classe peut posséder ses propres méthodes, variables ou autres éléments qui ne sont pas obligés d’être connus par l’interface et que cela fonctionne quand même. On y reviendra plus tard pour cette méthode.

Classe ConsumerAvecInterface

Ce sera presque exactement la même chose que pour l’autre classe ConsumerSansInterface à l’exception que nous déclarerons une variable de type IRestClient et que nous y collerons un objet RestClient derrière pour jouer le rôle de l’interface IRestClient.

using System.Net;

namespace InterfacePlay
{
    public class ConsumerAvecInterface
    {
        private const string request = "http://127.0.0.1/queryThat?option=this";

        private IRestClient restClient = new RestClient();

        public ConsumerAvecInterface()
        {
            InitializeComponent();
            SendRequest();
        }

        public void SendRequest()
        {
            var restRequest = new RestRequest()
            {
                Request = request
            };
            restClient.AddRestRequest(restRequest);
        }

        public HttpStatusCode HttpStatusCode { get; set; } = HttpStatusCode.BadRequest;

        public string RestClientName { get; set; }

        private void InitializeComponent()
        {
            restClient.RestServerResponded += (sender, restEvent) =>
            {
                HttpStatusCode = restEvent.HttpStatusCode;
                RestClientName = restEvent.RestClientName;
            };
        }
    }
}

Voyez, le détail qui nous importe ici c’est la ligne suivante :

private IRestClient restClient = new RestClient();

On déclare une variable de type IRestClient et on y colle un objet RestClient derrière pour jouer le rôle de l’interface. À aucun moment la classe ConsumerAvecInterface ne sait avec quel objet elle interagit, comment il implémente les éléments de l’interface IRestClient ni si l’objet de la classe RestClient ne possède d’autres éléments publics. D’ailleurs, à ce sujet, si vous essayez d’effectuer d’exécuter la méthode publique HelloWorld de RestClient à partir de ConsumerAvecInterface, vous aurez une erreur de l’Intellisense de Visual Studio.

Au stade où nous en sommes, vous devez probablement trouver que d’implémenter une interface de cette manière n’apporte aucun avantage et ne représente que du code supplémentaire à ajouter. Vous avez raison parce que c’est seulement la base que nous avons vue ici. L’implémentation d’une interface est un tantinet plus complexe dans une solution réelle, mais apporte tellement beaucoup plus d’avantage que de désagrément.

C’est ce que nous verrons donc dans les prochains articles que je mettrai en ligne ici. Nous verrons entre autres un « design pattern »: la fabrique de classe. Ensuite, un des éléments des principes SOLID: l’injection de dépendance. Tout cela se fera en montrant comment ces principes peuvent rendre vos tests unitaires beaucoup plus fiables et facile à écrire.

En conclusion

On a d’abord vu comment créer une interface et comment l’implémenter dans une classe en C#.

Ensuite, on a vue comment utiliser un objet à partir d’une classe dans un schéma standard ou l’on couple un objet à un autre.

C’est après que l’on a vu comment faire la même chose, mais en utilisant une interface au lieu d’un objet et que l’on a compris que l’on interagissait maintenant avec une variable qui contient un rôle plutôt qu’avec un objet de classe réel.

D’autres articles sur les interfaces suivront et, je vous le promets, nous passerons à un autre niveau.

Merci pour votre intérêt. Ne vous gênez pas pour me corriger ou pour me dire combien cet article vous a aidé.

2 thoughts on “Interface | la base”

  1. Ping : My Homepage

Laisser un commentaire

Votre adresse de courriel ne sera pas publiée. Les champs obligatoires sont indiqués avec *