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

31

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é?

Chris Pratt
la source
1
Je me souviens vaguement qu'il existe une classe semi-couramment utilisée en C # qui implémente une interface mais déclare explicitement dans la documentation qu'une certaine méthode n'est pas implémentée. Je vais essayer de voir si je peux le trouver.
Mage Xy
Je serais certainement intéressé de voir cela, si vous pouvez le trouver.
Chris Pratt
23
Je peux signaler plusieurs cas de cela dans la bibliothèque de .NET - ET ILS SONT TOUS RECONNUS COMME DES MAUVAISES ERREURS TERRIBLES . Il s'agit d'une violation emblématique et courante du principe de substitution de Liskov - les raisons de ne pas violer le LSP peuvent être trouvées dans ma réponse ici
Jimmy Hoffa
3
Devez-vous mettre en œuvre cette interface spécifique, ou pourriez-vous introduire une superinterface et l'utiliser?
Christopher Schultz
6
"une propriété qui n'est pas nécessaire pour cette classe" - La question de savoir si une partie d'une interface est nécessaire dépend des clients de l'interface, pas des implémenteurs. Si une classe ne peut pas implémenter raisonnablement un membre d'une interface, alors la classe n'est pas adaptée à l'interface. Cela peut signifier que l'interface est mal conçue - essayant probablement d'en faire trop - mais cela n'aide pas la classe.
Sebastian Redl

Réponses:

51

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. Considérez-le comme une prise électrique où le trou de mise à la terre n'est pas nécessaire pour qu'il ne se fixe pas réellement à 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 vraiment ce que propose l'interface, ne l'implémentez pas.


Voici quelques extraits de Wikipédia:

Le principe de substitution de Liskov peut être formulé simplement 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. Il s'agit d'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 tous besoin d'eux pour vous engager dans les mêmes comportements.


Le 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 qu'une seule installation. Pensez à nouveau à l'interface d'une prise électrique, elle pourrait également avoir un thermostat, 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 d'interface de ségrégation (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 à connaître que les méthodes qui les intéressent.

Jimmy Hoffa
la source
Absolument. C'est probablement pourquoi cela ne me semblait pas juste, en premier lieu. Parfois, vous avez juste besoin d'un rappel doux que vous faites quelque chose de stupide. Merci.
Chris Pratt
@ChrisPratt c'est une erreur très courante à commettre, c'est pourquoi les formalismes peuvent être précieux - la classification des odeurs de code permet de les identifier plus rapidement et de rappeler les solutions précédemment utilisées.
Jimmy Hoffa
4

Ça me va bien, si telle 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.

Avertissement: cela nécessite un héritage multiple pour fonctionner correctement, et je n'ai aucune idée si C # le prend en charge.

Courses de légèreté avec Monica
la source
6
C # ne prend pas en charge l'héritage multiple de classes, mais il le prend en charge pour les interfaces.
mgw854
2
Je pense que tu as raison. L'interface est un contrat après tout, et même si je sais que cette classe particulière ne sera pas utilisée de manière à casser tout ce qui utilise l'interface si cette propriété est désactivée, ce n'est pas évident.
Chris Pratt
@ChrisPratt: Ouais.
Courses de légèreté avec Monica
4

J'ai rencontré cette situation. En fait, comme indiqué ailleurs, la BCL a de tels cas ... 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), bien qu'il ICollection.SyncRootne soit pas obsolète en soi, il lancera NotSupportedException.

  • L'interface contient des membres qui sont documentés comme étant facultatifs et que l'implémentation peut lever l'exception. Par exemple, sur MSDN, IEnumerator.Resetil est 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 qui est attendu maintenant ... Je fais le ICollection<T>.IsReadOnlyretour truepour que vous puissiez leur dire à part. 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 IList<T>mais ma classe a une taille fixe, et ne prend pas en charge l'ajout ni la suppression, donc cela fonctionne plus comme un tableau qu'une liste. Mais il n'y IArray<T>en a pas 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, de sorte 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é de les 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?

C'est parfois InvalidOperationExceptionune 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 vous ne voulez pas permettre à des classes de tiers à introduire par l' injection de dépendance.] Je l' ai fait à backport ThreadLocal parce que la mise en œuvre de suivi et non suivi sont trop loin, et qu'est-ce qui ThreadLocal.Valuesjette sur l'implémentation sans suivi?InvalidOperationExceptionmê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 ce qui est ICollection<T>.IsReadOnlymentionné ci-dessus, il est logique de renvoyer simplement «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 rester avec elle 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.

Theraot
la source
0

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 masquer efficacement * certaines des méthodes IDictionary. Il 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 un IDictionary.

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

user2023861
la source
... ce qui est terrible.
Courses de légèreté avec Monica
Pourquoi le downvote?
user2023861
2
Probablement parce que vous n'avez pas répondu à la question. Ish. Sorte de.
Courses de légèreté avec Monica
@LightnessRacesinOrbit, j'ai mis à jour ma réponse pour la rendre plus claire. J'espère que cela aide le PO.
user2023861
2
On dirait que ça me répond à la question. Le fait qu'il puisse s'agir d'une bonne ou d'une mauvaise idée ne semble pas entrer dans le champ de la question, mais peut tout de même être utile pour les réponses à indiquer.
Ellesedil
-6

Vous pouvez toujours simplement implémenter et renvoyer une valeur bénigne ou une valeur par défaut. Après tout, ce n'est qu'une propriété. Il pourrait renvoyer 0 (la propriété par défaut de int), ou toute valeur ayant un sens dans votre implémentation (int.MinValue, int.MaxValue, 1, 42, etc ...)

//We don't officially implement this property
int IFoo.Bar
{
     { get; }
}

Lancer une exception semble être une mauvaise forme.

Jon Raynor
la source
2
Pourquoi? Comment retourne-t-on mieux des données incorrectes?
Courses de légèreté avec Monica
1
Cela rompt le LSP: voir la réponse de Jimmy Hoffa pour une explication pourquoi.
5
S'il n'y a pas de valeurs de retour correctes possibles, il est préférable de lever une exception que de renvoyer une valeur incorrecte. Quoi que vous fassiez, votre programme va mal fonctionner s'il invoque accidentellement cette propriété. Mais si vous jetez une exception, il sera alors évident pourquoi il fonctionne mal.
Tanner Swett
Je ne sais pas maintenant comment la valeur serait incorrecte lorsque vous implémentez l'interface! C'est votre implémentation, ça peut être ce que vous voulez.
Jon Raynor
Et si la valeur correcte était inconnue au moment de la compilation?
Andy