web-dev-qa-db-fra.com

Implémenter une interface lorsque vous n'avez pas besoin d'une des propriétés

Assez simple. J'implémente une interface, mais il y a une propriété qui n'est pas nécessaire pour cette classe et, en fait, ne devrait pas être utilisée. Mon idée initiale était de faire quelque chose comme:

int IFoo.Bar
{
    get { raise new NotImplementedException(); }
}

Je suppose qu'il n'y a rien de mal à cela, en soi, mais cela ne semble pas "correct". Quelqu'un d'autre a-t-il déjà rencontré une situation similaire? Si oui, comment l'avez-vous abordé?

31
Chris Pratt

Il s'agit d'un exemple classique de la façon dont les gens décident de violer le principe de substitution de Liskov. Je le décourage fortement mais encouragerais éventuellement une solution différente:

  1. Peut-être que la classe que vous écrivez ne fournit pas les fonctionnalités prescrites par l'interface si elle n'a pas recours à tous les membres de l'interface.
  2. Alternativement, cette interface peut faire plusieurs choses et peut être séparée selon le principe de ségrégation d'interface.

Si le premier est le cas pour vous, n'implémentez simplement pas l'interface sur cette classe. Pensez-y comme une prise électrique où le trou de mise à la terre n'est pas nécessaire afin qu'il ne se fixe pas en fait à la terre. Vous ne branchez rien avec de la terre et ce n'est pas grave! Mais dès que vous utilisez quelque chose qui a besoin d'un terrain - vous pourriez être dans un échec spectaculaire. Mieux vaut ne pas percer un faux trou. Donc, si votre classe ne fait pas en fait faire ce que l'interface a l'intention, ne l'implémentez pas.


Voici quelques extraits de Wikipédia:

Liskov Substitution Principle peut être simplement formulé comme suit: "Ne renforcez pas les conditions préalables et n'affaiblissez pas les conditions postérieures".

Plus formellement, le principe de substitution de Liskov (LSP) est une définition particulière d'une relation de sous-typage, appelée sous-typage comportemental (fort), qui a été initialement introduite par Barbara Liskov dans un discours-programme de conférence de 1987 intitulé Abstraction et hiérarchie des données. C'est une relation sémantique plutôt que simplement syntaxique car elle vise à garantir l'interopérabilité sémantique des types dans une hiérarchie , [...]

Pour l'interopérabilité sémantique et la substituabilité entre différentes implémentations des mêmes contrats - vous avez besoin de tous pour vous engager dans les mêmes comportements.


Principe de ségrégation des interfaces exprime l'idée que les interfaces doivent être séparées en ensembles cohérents de sorte que vous n'avez pas besoin d'une interface qui fait beaucoup de choses disparates lorsque vous ne voulez que un établissement. Pensez à nouveau à l'interface d'une prise électrique, il pourrait avoir un thermostat également, mais cela rendrait plus difficile l'installation d'une prise électrique et pourrait la rendre plus difficile à utiliser à des fins autres que le chauffage. Comme une prise électrique avec un thermostat, les grandes interfaces sont difficiles à mettre en œuvre et difficiles à utiliser.

Le principe de ségrégation d'interface (ISP) stipule qu'aucun client ne devrait être forcé de dépendre de méthodes qu'il n'utilise pas. [1] Le FAI divise les interfaces qui sont très grandes en interfaces plus petites et plus spécifiques afin que les clients n'aient qu'à connaître les méthodes qui les intéressent.

53
Jimmy Hoffa

Ça me va bien, si c'est votre situation.

Cependant, il me semble que votre interface (ou son utilisation) est cassée si une classe dérivée ne l'implémente pas réellement. Pensez à diviser cette interface.

Disclaimer: Cela nécessite plusieurs héritages pour fonctionner correctement, et je n'ai aucune idée si C # le supporte.

4

J'ai rencontré cette situation. En fait, comme indiqué ailleurs la BCL a de telles instances ... Je vais essayer de fournir de meilleurs exemples et de fournir une justification:

Lorsque vous avez une interface déjà livrée que vous conservez pour des raisons de compatibilité et ...

  • L'interface contient des membres obsolètes ou décolorés. Par exemple BlockingCollection<T>.ICollection.SyncRoot (entre autres) tandis que ICollection.SyncRoot n'est pas obsolète en soi, il lancera NotSupportedException.

  • L'interface contient des membres qui sont documentés comme facultatifs et que l'implémentation peut lever l'exception. Par exemple sur MSDN concernant IEnumerator.Reset ça dit:

La méthode de réinitialisation est fournie pour l'interopérabilité COM. Il n'a pas nécessairement besoin d'être mis en œuvre; à la place, l'implémenteur peut simplement lever une exception NotSupportedException.

  • Par une erreur de conception de l'interface, il aurait dû y avoir plus d'une interface en premier lieu. C'est un modèle courant dans BCL d'implémenter des versions en lecture seule des conteneurs avec NotSupportedException. Je l'ai fait moi-même, c'est ce qu'on attend maintenant ... Je fais ICollection<T>.IsReadOnly return true pour que vous puissiez leur dire appart. La conception correcte aurait été d'avoir une version lisible de l'interface, puis l'interface complète en hérite.

  • Il n'y a pas de meilleure interface à utiliser. Par exemple, j'ai une classe qui vous permet d'accéder aux éléments par index, vérifiez s'il contient un élément et sur quel index, il a une certaine taille, vous pouvez le copier dans un tableau ... cela semble un travail pour IList<T> mais ma classe a une taille fixe, et ne supporte ni l'ajout ni la suppression, donc elle fonctionne plus comme un tableau qu'une liste. Mais il n'y a pas de IArray<T> dans la BCL .

  • L'interface appartient à une API qui est portée sur plusieurs plates-formes, et dans la mise en œuvre d'une plate-forme particulière, certaines parties de celle-ci ne sont pas prises en charge. Idéalement, il y aurait un moyen de le détecter, afin que le code portable qui utilise une telle API puisse décider d'appeler ou non ces parties ... mais si vous les appelez, il est tout à fait approprié d'obtenir NotSupportedException. Cela est particulièrement vrai s'il s'agit d'un portage vers une nouvelle plate-forme qui n'était pas prévue dans la conception d'origine.


Considérez également pourquoi il n'est pas pris en charge?

Parfois, InvalidOperationException est une meilleure option. Par exemple, une autre façon d'ajouter du polymorphisme dans une classe est d'avoir différentes implémentations d'une interface interne et votre code choisit celle à instancier en fonction des paramètres donnés dans le constructeur de la classe. [Ceci est particulièrement utile si vous savez que l'ensemble des options est fixe et que vous ne voulez pas autoriser l'introduction de classes tierces par injection de dépendance.] J'ai fait cela pour rétroporter ThreadLocal car l'implémentation de suivi et de non-suivi est trop éloignée, et qu'est-ce que ThreadLocal.Values jeter l'implémentation sans suivi? InvalidOperationException même si cela ne dépend pas de l'état de l'objet. Dans ce cas, j'ai présenté la classe moi-même et je savais que cette méthode devait être implémentée en lançant simplement une exception.

Parfois, une valeur par défaut est logique. Par exemple sur le ICollection<T>.IsReadOnly mentionné ci-dessus, il est logique de renvoyer juste "vrai" ou "faux" selon le cas. Alors ... quelle est la sémantique de IFoo.Bar? il peut y avoir une valeur par défaut raisonnable à retourner.


Addendum: si vous contrôlez l'interface (et que vous n'avez pas besoin de la conserver pour des raisons de compatibilité), il ne devrait pas y avoir de cas où vous devez lancer NotSupportedException. Cependant, vous devrez peut-être diviser l'interface en deux ou plusieurs interfaces plus petites pour avoir le bon ajustement pour votre cas, ce qui peut entraîner une "pollution" dans les situations extrêmes.

4
Theraot

Quelqu'un d'autre a-t-il déjà rencontré une situation similaire?

Oui, les concepteurs de bibliothèques .Net l'ont fait. La classe Dictionary fait ce que vous faites. Il utilise une implémentation explicite pour efficacement masquer * certaines des méthodes IDictionary. C'est mieux expliqué ici , mais pour résumer, afin d'utiliser les méthodes Add, CopyTo ou Remove du dictionnaire qui prennent KeyValuePairs, vous devez d'abord convertir l'objet en IDictionary.

* Il ne s'agit pas de "masquer" les méthodes au sens strict du mot "masquer" car Microsoft l'utilise . Mais je ne connais pas de meilleur terme dans ce cas.

0
user2023861