Est-il permis d'utiliser une implémentation d'interface explicite pour masquer les membres en C #?

14

Je comprends comment travailler avec les interfaces et l'implémentation d'interface explicite en C #, mais je me demandais si c'était considéré comme une mauvaise forme pour cacher certains membres qui ne seraient pas utilisés fréquemment. Par exemple:

public interface IMyInterface
{
    int SomeValue { get; set; }
    int AnotherValue { get; set; }
    bool SomeFlag { get; set; }

    event EventHandler SomeValueChanged;
    event EventHandler AnotherValueChanged;
    event EventHandler SomeFlagChanged;
}

Je sais à l'avance que les événements, bien qu'utiles, seraient très rarement utilisés. Ainsi, j'ai pensé qu'il serait logique de les cacher à une classe d'implémentation via une implémentation explicite pour éviter l'encombrement IntelliSense.

Kyle Baran
la source
3
Si vous faites référence à votre instance via l'interface, elle ne les cacherait pas de toute façon, elle ne se cacherait que si votre référence est à un type concret.
Andy
1
J'ai travaillé avec un gars qui faisait ça régulièrement. Cela m'a rendu complètement fou.
Joel Etherton
... et commencez à utiliser l'agrégation.
mikalai

Réponses:

39

Oui, c'est généralement une mauvaise forme.

Ainsi, j'ai pensé qu'il serait logique de les cacher à une classe d'implémentation via une implémentation explicite pour éviter l'encombrement IntelliSense.

Si votre IntelliSense est trop encombré, votre classe est trop grande. Si votre classe est trop grande, elle fait probablement trop de choses et / ou est mal conçue.

Résolvez votre problème, pas votre symptôme.

Telastyn
la source
Est-il approprié de l'utiliser pour supprimer les avertissements du compilateur concernant les membres inutilisés?
Gusdor
11
"Résolvez votre problème, pas votre symptôme." +1
FreeAsInBeer
11

Utilisez la fonction pour ce à quoi elle était destinée. La réduction de l'encombrement des espaces de noms n'en fait pas partie.

Les raisons légitimes ne concernent pas la «dissimulation» ou l'organisation, elles visent à lever l'ambiguïté et à se spécialiser. Si vous implémentez deux interfaces qui définissent "Foo", la sémantique de Foo peut différer. Vraiment pas un meilleur moyen de résoudre ce problème, sauf pour les interfaces explicites. L'alternative consiste à interdire l'implémentation d'interfaces qui se chevauchent, ou à déclarer que les interfaces ne peuvent pas associer la sémantique (ce qui est de toute façon conceptuel). Les deux sont de piètres alternatives. Parfois, la praticité l'emporte sur l'élégance.

Certains auteurs / experts le considèrent comme un kludge d'une fonctionnalité (les méthodes ne font pas vraiment partie du modèle d'objet du type). Mais comme toutes les fonctionnalités, quelque part, quelqu'un en a besoin.

Encore une fois, je ne l' utiliserais pas pour me cacher ou organiser. Il existe des moyens de masquer les membres à Intellisense.

[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
codenheim
la source
"Il y a des auteurs / experts éminents qui le considèrent comme une caresse d'une fonctionnalité." Qui est? Quelle est l'alternative proposée? Il existe au moins un problème (c'est-à-dire la spécialisation des méthodes d'interface dans une sous-interface / classe d'implémentation) qui nécessiterait l'ajout d'une autre fonctionnalité à C # afin de le résoudre en l'absence d'implémentation explicite (voir ADO.NET et collections génériques) .
2014
@jhominal - CLR via C # par Jeffrey Richter utilise spécifiquement le mot kludge. C ++ s'entend sans, le programmeur doit se référer explicitement à l'implémentation de la méthode de base pour lever l'ambiguïté ou utiliser un cast. J'ai déjà mentionné que les alternatives ne sont pas très attrayantes. Mais c'est un aspect de l'héritage unique et des interfaces, pas la POO en général.
codenheim
Vous auriez également pu mentionner que Java n'a pas non plus d'implémentation explicite, malgré la même conception "héritage unique d'implémentation + interfaces". Cependant, en Java, le type de retour d'une méthode remplacée peut être spécialisé, ce qui évite une partie de la nécessité d'une implémentation explicite. (En C #, une décision de conception différente a été prise, probablement en partie à cause de l'inclusion de propriétés).
2014
@jhominal - Bien sûr, quand j'aurai quelques minutes plus tard, je mettrai à jour. Merci.
codenheim
Le masquage peut être tout à fait approprié dans les situations où une classe peut implémenter une méthode d'interface d'une manière conforme au contrat d'interface mais non utile . Par exemple, alors que je n'aime pas l'idée de classes où IDisposable.Disposefait quelque chose mais est donné un nom différent comme Close, je considère qu'il est parfaitement raisonnable pour une classe dont le contrat rejette expressément les états que le code peut créer et abandonner des instances sans appeler Disposeà utiliser l'implémentation d'interface explicite pour définir une IDisposable.Disposeméthode qui ne fait rien.
supercat
5

Je peux être un signe de code en désordre, mais parfois le monde réel est en désordre. Un exemple du framework .NET est ReadOnlyColection qui masque le setter de mutation [], Add et Set.

IE ReadOnlyCollection implémente IList <T>. Voir aussi ma question à SO il y a plusieurs années lorsque j'ai réfléchi à cela.

Esben Skov Pedersen
la source
Eh bien, c'est ma propre interface que j'utiliserai également, donc il n'y a aucun sens à mal le faire dès le départ.
Kyle Baran
2
Je ne dirais pas qu'il cache le passeur mutant mais plutôt qu'il ne le fournit pas du tout. Un meilleur exemple serait ConcurrentDictionary, qui implémente divers membres de IDictionary<T>manière explicite afin que les consommateurs de ConcurrentDictionaryA) ne les appellent pas directement et B) puissent utiliser des méthodes qui nécessitent une implémentation de IDictionary<T>si absolument nécessaire. Par exemple, les utilisateurs de ConcurrentDictionary<T> devraient appeler TryAddplutôt que Addd'éviter d'avoir besoin d'exceptions inutiles.
Brian
Bien sûr, la mise Adden œuvre explicite réduit également l'encombrement. TryAddpeut faire n'importe quoi Add( Addest implémenté comme un appel à TryAdd, donc fournir les deux serait redondant). Cependant, a TryAddnécessairement un nom / signature différent, il n'est donc pas possible de l'implémenter IDictionarysans cette redondance. L'implémentation explicite résout ce problème proprement.
Brian
Du point de vue du consommateur, les méthodes sont cachées.
Esben Skov Pedersen
1
Vous écrivez sur IReadonlyCollection <T> mais vous liez à ReadOnlyCollection <T>. Le premier d'une interface, le second est une classe. IReadonlyCollection <T> n'implémente pas IList <T> même si ReadonlyCollection <T> le fait.
Jørgen Fogh
2

Il est bon de le faire lorsque le député est inutile dans son contexte. Par exemple, si vous créez une collection en lecture seule qui implémente IList<T>en déléguant à un objet interne, _wrappedvous pouvez avoir quelque chose comme:

public T this[int index]
{
  get
  {
     return _wrapped[index];
  }
}
T IList<T>.this[int index]
{
  get
  {
    return this[index];
  }
  set
  {
    throw new NotSupportedException("Collection is read-only.");
  }
}
public int Count
{
  get { return _wrapped.Count; }
}
bool ICollection<T>.IsReadOnly
{
  get
  {
    return true;
  }
}

Ici, nous avons quatre cas différents.

public T this[int index]est défini par notre classe plutôt que par l'interface, et n'est donc bien sûr pas une implémentation explicite, bien que cela se trouve être similaire à la lecture-écriture T this[int index]définie dans l'interface mais est en lecture seule.

T IList<T>.this[int index]est explicite car une partie de celui-ci (le getter) est parfaitement mise en correspondance par la propriété ci-dessus, et l'autre partie lèvera toujours une exception. Bien que vital pour quelqu'un accédant à une instance de cette classe via l'interface, il est inutile pour quelqu'un de l'utiliser via une variable du type de la classe.

De même, parce que bool ICollection<T>.IsReadOnlyva toujours retourner vrai, il est tout à fait inutile de code écrit contre le type de la classe, mais pourrait être vital pour celui qui l'utilise via le type de l'interface, et donc nous l'implémentons explicitement.

Inversement, public int Countn'est pas implémenté explicitement car il pourrait potentiellement être utile à quelqu'un utilisant une instance via son propre type.

Mais avec votre cas "très rarement utilisé", je pencherais très fortement pour ne pas utiliser une implémentation explicite.

Dans les cas où je recommande d'utiliser une implémentation explicite appelant la méthode via une variable du type de la classe, ce serait soit une erreur (essayer d'utiliser le setter indexé) soit inutile (vérifier une valeur qui sera toujours la même) donc en se cachant vous protégez l'utilisateur du buggy ou du code sous-optimal. C'est très différent du code qui, selon vous, est susceptible d'être rarement utilisé. Pour cela, je pourrais envisager d'utiliser l' EditorBrowsableattribut pour cacher le membre d'intellisense, même si je serais fatigué de; le cerveau des gens a déjà son propre logiciel pour filtrer ce qui ne les intéresse pas.

Jon Hanna
la source
Si vous implémentez une interface, implémentez l'interface. Sinon, c'est une violation claire du principe de substitution de Liskov.
Telastyn
@Telastyn l'interface est en effet implémentée.
Jon Hanna
Mais le contrat de celui-ci n'est pas satisfait - spécifiquement, "c'est quelque chose qui représente une collection à accès aléatoire mutable".
Telastyn
1
@Telastyn, il remplit le contrat documenté, en particulier à la lumière de "Une collection en lecture seule ne permet pas l'ajout, la suppression ou la modification d'éléments après la création de la collection." Voir msdn.microsoft.com/en-us/library/0cfatk9t%28v=vs.110%29.aspx
Jon Hanna
@Telastyn: Si le contrat comprenait une mutabilité, alors pourquoi l'interface contiendrait-elle une IsReadOnlypropriété?
nikie