Dot.Blog

C#, XAML, Xamarin, UWP/Android/iOS

C# : Comment simuler des implémentations d’Interface par défaut ?

[new:30/11/2014]Les interfaces sont précieuses mais au contraire d’une classe abstraite elle ne peuvent offrir d’implémentations par défaut. Il existe toutefois une astuce…

Interface ou classe abstraite ?

C’est un vieux débat mais il est toujours d’actualité tant ces concepts ne semblent pas toujours clairs à de nombreux développeurs. Je ne m’appesantirai pas non plus sur le sujet, juste un rappel :

Une classe abstraite (abstract) est une classe qui force à la dériver pour l’utiliser. Il faut en effet en hériter dans une nouvelle classe “normale” pour pouvoir ensuite créer des instances. Une classe abstraite propose des méthodes, des propriétés et généralement, au moins pour certaines, des implémentations dites “par défaut”. Si une méthode contient du code et qu’elle n’est pas overridée, la nouvelle classe bénéficiera de ce code déjà écrit.

Une interface ne fait que fixer un contrat, ce n’est qu’une description de ce dernier. Pas moyen d’offrir un code par défaut pour les éléments du contrat. Si dix classes supportent une interface, dix fois il faudra écrire le code pour supporter le contrat.

image

Si on parle de choix entre ces deux approches c’est parce qu’elles semblent partager quelque chose qui les rend, ou tend à les rendre, similaires. Cette “chose”, c’est la notion de contrat. L’interface décrit un contrat sans l’implémenter, un classe abstraite décrit elle aussi un contrat mais en proposant généralement une implémentation. Elle gère aussi d’autres aspects essentiels comme fixer certains mécanismes (ordre d’exécution des méthodes pour accomplir une tâche particulière par exemple). La classe abstraite est bien plus riche qu’une interface dans le sens où elle fixe deux types de contrats à la fois : un contrat public (comme l’interface) et un contrat privé (garantie de certains traitements).

Alors pourquoi choisir ? La classe abstraite ne serait-elle pas “mieux” dans tous les cas ?

Il n’en est rien car s’il existe des similitudes entre les deux approches, il existe aussi une divergence de taille : la nature même du contrat est totalement différente.

L’héritage imposé par la classe abstraite créé un lien de type “est un”. Si A est la classe abstraite, et B une classe dérivée, par la nature même de l’héritage on pourra affirmer que “B est un A”. Prenons la classe Animal et la classe Chien qui en hériterait on peut en effet affirme que “un Chien est un Animal”, ou plus précis “une instance de classe Chien peut être considérée comme une instance de la classe Animal”.

Un interface ne crée pas la même proximité avec les classes qui l’implémentent. Une classe A qui supporte l’interface I, n’a aucune relation particulière avec I en dehors du fait qu’elle supporte I. Sa nature même le fameux “est un” n’est pas impacté par cette relation très distendue entre l’interface et ses implémentations.

imageLe choix entre interface ou classe abstraite ne doit donc pas se faire sur le plan de ce qui serait le “mieux” (ce qui ne veut de toute façon rien dire) sous-entendu qu’une classe abstraite par sa plus grande richesse serait un avantage en vertu du principe du “qui peut le plus peut le moins”.  Le choix doit s’effectue sur les implications de la nature même du contrat imposé par les deux approches. Avec une classe abstraite on impose un héritage et une relation “est un”, avec une interface on s’engage juste à respecter un contrat parmi d’éventuels autres tout en se gardant la possibilité de “être un” quelque chose qui a de la signification (fonctionnelle, métier…).

Bref, si les interfaces sont de plus en plus utilisées c’est bien parce qu’elles n’imposent que peu de choses. Elles permettent ainsi d’éviter de polluer la logique d’héritage d’un code par un héritage technique qui interdira de définir des arborescences chargées de sens pour l’application. Les interfaces offrent aussi un moyen simple d’écrire un code à faible couplage, ce qui est visée par toutes les approches modernes de développement (type MVVM, MvvmCross ou autres).

Dès lors choisir de créer des interfaces est dans 99% des cas une meilleure stratégie que d’imposer un héritage technique en créant des classes abstraites.

Certes… Mais quid des implémentations par défaut ?

Implémentations par défaut

Les interfaces n’ont ainsi que des avantages, sauf un : elles ne permettent pas de proposer des implémentations par défaut.

On voit ainsi parfois des développeurs créer une classe qui supporte une interface en offrant une implémentation de base. Ne reste plus qu’à hériter de cette classe pour bénéficier des implémentations… Mais ce stratagème ne trompe personne, on retrouve ici tous les défauts de l’héritage et de l’utilisation d’une classe abstraite !

Comme il n’y a pas de magie et puisqu’il faut forcément une classe pour implémenter un code, fournir du code par d��faut pour les membres d’une interface oblige à créer une classe pour le faire.

Cette vision des choses nous laisse malgré tout quelques latitudes qu’il ne faudrait pas négliger…

La notion de service

En effet s’il faut une classe pour implémenter le code du contrat d’une interface personne ne dit que c’est “votre classe” qui doit le faire !

C’est tout l’intérêt de l’approche par service. On définit une interface puis on créée une classe d’implémentation qui la supporte et on ajoute une instance de celle-ci dans un système d’injection de dépendances ou dans un conteneur d’IoC (inversion de contrôle) par exemple Unity.

Les classes qui ont besoin des méthodes de l’interface utilisent celle-ci tout simplement, les instances se chargeront de demander ou de recevoir l’instance qui rend le service.

Cette façon de pratiquer permet de créer un code à faible couplage ce qui en garantit une bien meilleureimage maintenabilité et la possibilité de changer à volonté les implémentations du service sans impacter sur les classes qui en font usage.

Largement utilisée par les framework cross-plateformes pour sa souplesse, la notion de “service” est une façon intelligente et efficace de proposer une implémentation pour une interface, implémentation économe puisque codée une fois et utilisable potentiellement à l’infini par autant de classes qu’on désire sans créer aucun lien de dépendance pas même celui de supporter l’interface en question.

Si cette approche est séduisante à plus d’un titre – je ne lui connais pas d’inconvénients même mineurs – elle n’est pas forcément applicable à toutes les situations et puis, vis à vis de la question posée, ce n’est qu’un ersatz, une ficelle un peu grosse car il ne s’agit pas tout à fait de ce qu’on entendait par une implémentation d’interface par défaut. Alors ?

Les méthodes d’extension à la rescousse

Les méthodes d’extensions sont l’une des facettes de la richesse de C#. Ici ce sont plutôt des librairies comme LINQ qui en font grand usage pour s’intégrer plus facilement dans le code. On les retrouve aussi à l’œuvre dans P-LINQ et dans bien d’autres situations.

Le côté pratique des méthodes d’extension c’est de faire croire qu’une instance supporte des méthodes qui en réalité n’existent pas dans sa classe et qui seront traitées par un code qui se trouve “ailleurs”.

Un code “ailleurs” qui implémentent des méthodes ? Cela ressemble beaucoup à la notion de service je vous l’accorde mais aussi à celle d’une implémentation d’interface par défaut …

Avec un peu de créativité on peut détourner cette proximité fonctionnelle pour enfin s’approcher au plus près de ce qu’on attend.

Voici un exemple simplifié de l’utilisation de cette technique ce qui rendra le propos plus éloquent :

interface ITruc
{
}
 
static class ITruc_Extensions 
{
    public static void Machin(this ITruc self) { Console.WriteLine("Truc !"); }
}
 
class ImplementingClass : ITruc
{
 
}
 
 
class MainClass
{
    public static void Main (string[] args)
    {
        var aTruc = new ImplementingClass ();
        aTruc.Machin();  
    }
}

 

Comme on le voit ci-dessus la méthode Machin est en réalité fournie par une classe qui étend l’interface ITruc (et non pas ImplementingClass). Le fait que la classe qui supporte ITruc puisse être vue comme un ITruc permet d’appeler les méthodes d’extension de cette interface. L’instance aTruc peut ainsi appeler Machin uniquement parce que sa classe supporte ITruc… Le tout sans écrire le code correspondant.

C’est une feinte qui peut paraitre un peu grosse puisqu’en réalité ITruc ne définit rien du tout, ce n’est qu’une sorte de marqueur qui permet aux classes qui indiquent la supporter de bénéficier des méthodes d’extension conçue pour elle.

Mais fonctionnellement on a bien l’impression que la classe supporte ITruc et quelle possède un code par défaut pour les méthodes de ITruc (qui n’en a en réalité aucune, le contrat vide et se trouve déporté vers la classe implémentant les méthodes d’extension).

L’avantage est que si la classe Implementing Class veut fournir sa propre implémentation cela reste tout à fait possible puisque ITruc est vide… Fonctionnellement on a bien le support d’une interface offrant des implémentations par défaut qui  peuvent être surchargée ! Rien n’interdit donc de définir une méthode Machin dans la classe ImplementingClass car dans ce cas c’est la méthode locale qui sera appelée car elle a la préséance sur la méthode d’extension. Futé non ?

Mais que se passe-t-il si on souhaite obliger l’implémentation de certaines méthode du contrat ? Avec l’astuce présentée il suffit tout simplement de placer la définition dans l’interface au lieu de la laisser vide…

Imaginons qu’en plus de Machin() nous souhaitions ajouter à l’interface Bidule() mais que son implémentation soit forcée (donc sans implémentation par défaut), il suffit d’ajouter “public void Bidule();” dans ITruc. Et ImplementingClass se verra obligée d’implémenter Bidule() alors que l’implémentation de Machin() sera optionnelle. Krafty, isn’t it ?

Conclusion

Bien entendu on ne peut pas modifier C# pour lui faire faire des choses qu’il ne propose pas. Il n’existe pas de moyen de fournir ce qui serait de “vraies” implémentations par défaut pour les interfaces.

Toutefois en faisant preuve d’un peu d’originalité et de créativité on peut utiliser la très grande richesse du langage pour simuler au plus près ce concept.

Bon développement et …

Stay Tuned !

blog comments powered by Disqus