Dans l’environnement .NET le rôle des interfaces est typiquement de définir le nom des méthodes et propriété qui seront présents dans une classe. Mais elles ne servent pas qu’à cela.
Implémentation
Comme indiqué, la première et plus utile utilisation d’une interface est de déclarer les membres d’une classe. A cette fin, l’interface doit déclarer les noms des méthodes et propriétés sans pour autant les définir.
public interface IMoney
{
public int PocketMoney { get; set; }
}
Cela a un avantage sur le plan de la conception/modélisation de l’application : on peut ainsi définir le fonctionnement futur de la classe qui implémentera l’interface.
public class Money : IMoney
{
public int _pocketMoney = 10;
public int PocketMoney
{
get {return _pocketMoney;}
set {_pocketMoney = value; }
}
}
A savoir qu’à la déclaration de notre objet on peut indifférement déclarer l’interface ou l’objet qui l’implémente.
Money myMoney = new Money(); IMoney hisMoney = new Money();
Ici myMoney et hisMoney exposeront tout deux les membres de l’interface. Toutefois, hisMoney se contentera de ne montrer que les membres de l’interfaces ; ce qui est déclaré dans la classe ne sera pas visible ; cela annonce une autre utilisation : le masquage.
Le masquage
Imaginons maintenant que Money, qui implémente l’interface IMoney, possède une propriété supplémentaire : FoundMoney.
public class Money : IMoney
{
public int _pocketMoney = 10;
public int PocketMoney
{
get {return _pocketMoney;}
set {_pocketMoney = value; }
}
public int _foundMoney = 10;
public int FoundMoney
{
get {return _foundMoney ;}
set {_foundMoney = value; }
}
}
Et bien le membre FoundMoney ne sera accessible que par la donnée Money myMoney et non par la donnée IMoney hisMoney.
Néanmoins, hisMoney ne fait que masquer une partie de la donnée initialisée.
En réalité, elle contient bien la donnée issue de la classe initialisée et l’ensemble de ses membres.
IMoney hisMoney = new Money(10,20); //PocketMoney, FoundMoney //Console.WriteLine(hisMoney.FoundMoney) IMPOSSIBLE, non accessible Console.WriteLine((hisMoney as Money).FoundMoney);
Ici l’application affichera bien 20, car hisMoney contient bien le membre FoundMoney.
De la même manière, si une classe Income implémente plusieurs interfaces (ex : IMoney, IAccountBank, ICash, ICoins…), l’objet IMoney hisMoney déclaré contiendrait bien l’ensemble d’une instanciation de Income, mais ne pourrait accéder qu’au(x) membre(s) que déclare son interface. Cela peut être utile lorsqu’un objet est partagé par différentes classe qui implémente chacune une interface différente.
Autre exemple : Dans le cas d’un couple qui se partagerait des tâche ménagère, une classe Homme contiendrait donc un membre IVaisselle CorveCuisine et la femme un membre IAspirateur NettoyageSol. Un même objet TacheMenagere tacheMenagere implémentant les deux interfaces pourraient leur servir d’instance commune.
Appels Explicit
Dans le cas d’une implémentation de plusieurs interface exposant le même nom de membre, un appel de objet.MethodeImplemente fera référence aux deux interface. Cela veut dire qu’en castant un objet comme une Interface1, cela apellera la meme méthode que si l’objet était casté en Interface2.
Si l’on veut différencié cela il est nécessaire d’explicitement les déclarer.
Prenons l’exemble d’une classe Mur implémentant IControl et ISurface. Mur devra déclarer des méthodes distinctes préfixées par le nom de l’interface si on désire que le castage soit utile.
void IControl.Paint()
{
System.Console.WriteLine("IControl.Paint");
}
void ISurface.Paint()
{
System.Console.WriteLine("ISurface.Paint");
}
Attention toutefois : l’objet Mur monMur ne pourra accéder à aucune de ces deux méthodes sauf s’il est casté dans l’une de ces deux interface : sans ça, une erreur du compileur aura lieu.
En pratique
Aperçu lors de la redéfinition
Dans une classe enfant, il est tout à fait possible de redéfinir un membre parent avec le mot clé new. Par défaut, le membre parent sera ainsi masqué par le membre enfant.
public Money
{
public Cash PiggyBank;
}
public PreciseMoney : Money
{
public new Euros PiggyBank;
}
Dans cette exemple, on utilse une nouvelle classe Euros pour typé la valeur de PreciseMoney.PiggyBank. De la même manière, on peut la typé à partir d’une interface permettant ainsi d’exploiter les données partielle d’une classe que l’on souhaiterait instancier. De plus, en castant la donnée, il est même possible d’aller stocker l’objet instancié dans le membre parent.
public PreciseMoney : Money
{
public new IDollars PiggyBank{
get{ return (base.PiggyBank as IDollars)}
set{base.PiggyBank = (value as Cash)} }
}
Cela permet de ne l’utiliser en fait que comme accesseur dans la classe enfant qui conserve le nom de l’objet parent correctement nomé. Attention toutefois à bien veiller que l’objet instancier (Dollars) dérive bien de la classe du membre de base (Cash).
Mise en situation
Revenons sur l’analogie d’un couple qui se partage des tâches ménagère ; l’homme ferait la vaisselle et la femme passerait l’aspirateur.
Chacun aurait une tâche ménage à effectuer, comme n’importe quel habitant (si la parité est respecté !). Une personne extérieur pourrait percevoir que chacun effectue une tâche ménagere, bien que chacun des deux ne sait pas nécessairement comme réaliser la tâche de l’autre. Chacun des deux individus auraient conscience qu’une autre tâche est à réaliser (l’homme serait conscient qu’il est possible de passer l’asiprateur et la femme qu’il est possible de faire la vaisselle) mais chacun ne se préoccupe que de la sienne.
Techniquement, considérons les classes Homme et Femmes dérivant finalement de Habitant qui possèderait un membre Menage TacheMenagere. Menage serait une classe simple ayant peu de précision (un int TempsPasse tout au plus).
Dans une classe enfant, imaginons le cas où la classe Menage ne suffise pas mais qu’on souhaiterait utiliser le membre TacheMenagere en le rédefinissant en TacheMenagere (avec, en plus des interface, une dérivation de Menage).
Si on tente de redéfinir dans la classe Homme le membre Menage TacheMenagere en TacheMenagere TacheMenagere, il accèdera à l’interface IAspirateur, ce qui ne l’interesse pas. Et à priori, l’interface IVaisselle n’a pas de lien avec Menage.
Mais en fait il est tout à fait possible de venir y stocker une interface.
//Classe Homme
public new IVaisselle TacheMenagere{
get {return (base.TacheMenagere as IVaisselle}
set {base.TacheMenagere = (value as Menage)}
}
//Classe Femme
public new IAspirateur TacheMenagere{
get {return (base.TacheMenagere as IAspirateur}
set {base.TacheMenagere = (value as Menage)}
}
Ainsi on a tout simplement stocké une instance complète d’une classe TacheMenagere:Menage vers une donnée de type IVaisselle qui la stocke dans une donnée parente, à savoir Menage.
Ainsi, le membre redéfinit dans les classe enfant ne serait qu’un accésseur au membre TacheMenagere correctement défini d’un point de vue conceptuel dans Habitant.
Pour résumer, les deux classes Homme et Femme redefinissent le membre TacheMenagere du parent Habitant. En tentant d’y accéder depuis l’objet parent (en castant l’un des deux par exemple), on n’aurait accès à aucune information précise. Par contre, c’est bien le membre TacheMenagere de la classe parent qui contiendrait, pour chacun des deux cas, une référence vers un objet complet, différent et masqué.
Sources
- https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/interfaces/explicit-interface-implementation