web-dev-qa-db-fra.com

En C #, existe-t-il un moyen d'imposer le couplage de comportement dans les méthodes d'interface ou le fait que j'essaie de le faire est-il une odeur de conception?

Plusieurs fois, je veux définir une interface avec certaines méthodes qui maintiennent une relation de comportement entre elles.

Cependant, je pense que cette relation est souvent implicite. Dans cet esprit, je me suis demandé: Existe-t-il un moyen d'imposer une relation de comportement entre les méthodes d'interface?

J'ai pensé à définir ce comportement via l'héritage (en définissant une implémentation commune). Mais puisque C # ne permet pas l'héritage multiple, je pense que plusieurs fois une interface serait plus recommandée et que l'héritage n'est pas assez flexible.


Par exemple:

public interface IComponent
{
    void Enable();
    void Disable();
    bool IsEnabled();
}

Pour cette interface, je souhaitais que la relation suivante soit remplie:

  • Si Enable() est appelée, IsEnabled() doit retourner true.
  • Si Disable() est appelée, IsEnabled() doit retourner false.

Dans cet exemple, la contrainte de comportement que je voudrais appliquer est:

  • Lors de l'implémentation de Enable(), l'implémenteur doit s'assurer que IsEnabled() renvoie true
  • Lors de l'implémentation de Disable(), l'implémenteur doit s'assurer que IsEnabled() renvoie false

Existe-t-il un moyen d'appliquer cette contrainte d'implémentation? Ou, le fait que je pense à appliquer ce type de contrainte est en soi un signe qu'il y a une faille dans la conception?

27
Albuquerque

Eh bien, tout d'abord, ajustons un peu votre interface.

public interface IComponent
{
    void Enable();
    void Disable();
    bool IsEnabled { get; }
}

Maintenant. Qu'est-ce qui pourrait mal tourner ici? Par exemple, une exception pourrait-elle être levée dans les méthodes Enable() ou Disable()? Dans quel état IsEnabled serait-il alors?

Même si vous utilisez des contrats de code, je ne vois pas comment IsEnabled peut être corrélé à l'utilisation de vos méthodes Enable ou Disable à moins que ces méthodes ne soient garanties pour réussir. IsEnabled doit représenter l'état réel dans lequel se trouve votre objet, pas un état hypothétique.


Cela dit, tout ce dont vous avez vraiment besoin est

public interface IComponent
{
    bool IsEnabled { get; set; }
}

Désélectionnez-le et le composant se désactive.

33
Robert Harvey

Ce que vous recherchez est une approche bien connue appelée Design by Contract . C'était pris en charge directement dans le cadre de la version 4. .

DIVULGATION: Soyez prudent lorsque vous ajoutez des contrats de code à un nouveau projet en 2019. Le statut actuel de la maintenance ultérieure par Microsoft n'est pas entièrement clair, voir cet article SO =, peut-être à cause de la popularité manquante.

DBC permet de spécifier des conditions préalables, des post-conditions et des invariants pour les fonctions ainsi que pour les interfaces, il vous suffit donc d'écrire un contrat qui force IsEnabled à être vrai après un appel de Enabled().

Comme d'autres réponses l'ont souligné, il peut y avoir des conceptions alternatives où ces contraintes ne sont pas nécessaires, mais pour cet exemple, supposons que ces exigences sont justifiées pour une raison quelconque. Ensuite, utiliser Code Contracts sur variante de Robert Harvey de l'exemple d'interface peut ressembler à ceci:

using System.Diagnostics.Contracts;

[ContractClass(typeof(ComponentContract))]
public interface IComponent
{
    void Enable();
    void Disable();
    bool IsEnabled { get; } 
}

[ContractClassFor(typeof(IComponent))]
sealed class ComponentContract : IComponent
{
    [Pure]
    public bool IsEnabled => Contract.Result<bool>();

    public void Disable()
    {
        Contract.Ensures(IsEnabled == false);
    }

    public void Enable()
    {
        Contract.Ensures(IsEnabled == true);
    }
}

Voir ici pour un court tutoriel sur les contrats de code.

Jetez également un œil à ma deuxième réponse à cette question, qui offre une solution ne dépendant pas des bibliothèques qui pourraient devenir obsolètes à l'avenir.

39
Doc Brown

Vous demandez trop d'interfaces C #.

Les interfaces C # sont des contrats. Ils disent quel pack de méthodes une classe donnée implémente et ils garantissent que ces méthodes seront là si quelqu'un les appelle.

Cela dit, c'est aussi la seule chose que les interfaces C # font.

Ils sont complètement inconscients de ce que font les méthodes mises en œuvre. Ils sont libres de faire ce qu'ils veulent.

Une classe donnée peut implémenter "isEnabled" pour toujours retourner true. Un autre peut lier "Désactiver" à un appel de base de données et refuser de le désactiver si quelque chose de spécifique se produit.

Vous ne pouvez rien contrôler de tout cela. Ce n'est pas le travail de votre interface C #.


Comment puis-je appliquer ce comportement, alors?

Utilisez des tests.

Créez un groupe de tests unitaires pouvant accepter un objet du type en question et testez le comportement.

Si le test réussit, vous êtes prêt à partir. S'ils échouent, quelque chose ne va pas et vous devriez vérifier votre code.

Cela dit, vous n'avez aucun moyen élégant de forcer cela à un tiers si vous développez une API, vous ne devriez pas non plus. Ce n'est pas à cela que servent les interfaces C #.

32
T. Sar

Les transitions d'états peuvent être représentées par des interfaces distinctes par état:

public interface IEnabledComponent
{
    IDisabledComponent ToDisabled();
}

public interface IDisabledComponent
{
    IEnabledComponent ToEnabled();
}

C'est beaucoup plus puissant et sûr, car vous pouvez exposer différentes méthodes en fonction de l'état actuel. En particulier, vous n'auriez même pas besoin de la propriété IsEnabled.

Alternativement, vous pouvez avoir une seule interface vraiment simple:

public interface IComponent
{
    bool IsEnabled {get;set;}
}

Ici, l'état est représenté dans un seul membre, donc pas besoin de coordonner plusieurs membres liés.

Vous choisiriez les premières options si les différents états provoquent un comportement différent, la seconde si l'état activé n'affecte pas les autres comportements.

Le fait que vous puissiez définir une relation comportementale entre différents membres d'une interface indique qu'ils expriment les mêmes informations et sont donc redondants. Pour prendre un exemple plus simple:

interface IComponent 
{
  bool IsEnabled {get;set;}
  bool IsDisabled {get;set;}
}

Ici, vous pouvez définir la relation selon laquelle si IsEnabled est vrai, alors IsDisabled est faux. Mais cela montre simplement que l'un d'eux pourrait être éliminé car il ne représente aucune information ou comportement indépendant.

21
JacquesB

Une option de plus lorsque vous souhaitez que le modèle d'utilisation ait l'air "traditionnel"* mais peut limiter l'interface de sorte qu'il ne nécessite pas de méthodes liées entre elles, vous pouvez utiliser des méthodes d'extension pour "ajouter" des méthodes manquantes.

Dans votre exemple, l'interface peut simplement contenir IsEnabled {get;set;} et Enable et Disable peuvent être des extensions venant dans le cadre de votre bibliothèque définissant l'interface:

public interface IComponent
{
    bool IsEnabled {get;set;}
}

public static class IComponentExtensions
{
    public static void Enable(this IComponent component)
    {
         component.IsEnabled = true;
    } 
    public static void Disable(this IComponent component)
    {
         component.IsEnabled = false;
    } 
}

Notez que puisque les méthodes d'instance ont priorité sur les extensions et que les classes concrètes peuvent implémenter des méthodes avec la même signature pour forcer le compilateur à choisir l'implémentation spécifique à la classe, ce qui peut provoquer une certaine confusion. D'un autre côté, à peu près tout le monde a une expérience significative avec ce modèle dans LINQ - il vaut donc la peine de considérer si votre interface peut être réduite pour le permettre.


* interface "traditionnelle" dans le sens de personnes qui s'attendent à avoir des méthodes particulières sur des types particuliers - par exemple Stream classes dans Framework - on s'attend à ce que le fichier ait "Open" et "Close" lorsque les flux réguliers sont juste "nouveaux"/"Disposer".

5
Alexei Levenkov

Voici une autre idée pour résoudre ce problème en utilisant une approche complètement différente de celle indiquée dans mon autre réponse (d'où je le poste séparément): utilisez le modèle de méthode de modèle . Encore une fois, laissez-nous - pour les besoins de l'exemple, supposer que l'exigence de faire fonctionner IsEnabled comme décrit est justifiée pour une raison quelconque, et vous voulez vous assurer que son état est mis à jour correctement.

Ensuite, vous pouvez remplacer l'interface par une classe abstraite, faire de IsEnabled une propriété booléenne qui est commutée sans condition (même en cas d'exception), et laisser un utilisateur de cette classe abstraite implémenter deux méthodes de modèle au lieu de l'original ceux:

public abstract class Component  // replacement for IComponent 
{
    public bool IsEnabled{get;private set;}

    public void Enable()
    {
       try
       {
          EnableImpl();
       }
       finally
       {
          IsEnabled=true;
       }
    }
    public void Disable()
    {
       try
       {
          DisableImpl();
       }
       finally
       {
          IsEnabled=false;
       }
    }

    protected virtual void EnableImpl();
    protected virtual void DisableImpl();
}

Désormais, les utilisateurs peuvent remplacer EnableImpl et DisableImpl par leurs propres implémentations, tandis que le suivi d'état est garanti.

Cette approche est certainement plus standard que ma première suggestion et je ne m'attendrais pas à ce qu'elle devienne rapidement obsolète par Microsoft.

3
Doc Brown

Une solution alternative (que je recommanderais pas généralement, mais accomplit ce que vous recherchez) serait la suivante.

public class Switch //make sealed if you really want to enforce behaviour
{  

    public bool IsEnabled {get; private set;}

    public void Enable() //make virtual if you don't want to enforce this behaviour
    { 
        IsEnabled = true;
    }

    public void Disable() //make virtual if you don't want to enforce this behaviour
    {
        IsEnabled = false;
    }
}

public interface IComponent {
    Switch Switch{get;}
}
0
Guran