web-dev-qa-db-fra.com

Comment puis-je vous assurer que les implémentations d'interface sont mises en œuvre de la manière dont j'avais été attendues?

Disons qu'il y a un membre SomeMethod dans une interface ISomeInterface comme suit:

public interface ISomeInterface
{
    int SomeMethod(string a);
}

Aux fins de mon programme, tous les consommateurs de ISomeInterface agissent sur l'hypothèse que le RT renvoyé est supérieur à 5.

Trois manières viennent à l'esprit pour résoudre ce problème -

1) Pour chaque objet qui consomme ISomeInterface, ils affirment que l'INT renvoyé> 5.

2) Pour chaque objet qui implémente ISomeInterface, ils affirment que l'int, ils sont sur le point de revenir sont> 5.

Les deux solutions ci-dessus sont encombrantes car elles exigent que le développeur se rappelle de le faire sur chaque mise en œuvre ou une consommation de ISomeInterface. En outre, cela s'appuie sur la mise en œuvre de l'interface qui n'est pas bonne.

3) La seule façon dont je peux penser à faire cela est pratiquement d'avoir une emballage qui implémente également ISomeInterface et renvoie la mise en œuvre sous-jacente comme suit:

public class SomeWrapper : ISomeInterface
{
    private ISomeInterface obj;

    SomeWrapper(ISomeInterface obj)
    {
        this.obj = obj;
    }

    public int SomeMethod(string a)
    {
        int ret = obj.SomeMethod("hello!");
            if (!(ret > 5))
            throw new Exception("ret <= 5");
        else
            return ret;
    }
}

Le problème est tout à fait que nous revenons à nouveau sur un détail de mise en œuvre de ISomeInterface via ce que le class SomeWrapper fait, bien que l'avantage que nous l'avons maintenant confiné à un seul endroit.

Est-ce le meilleur moyen d'assurer une interface est implémentée de la manière prévue, ou existe-t-il une meilleure alternative? Je crois comprendre que les interfaces ne peuvent pas être conçues pour cela, mais quelle est la meilleure pratique d'utiliser un objet sous l'hypothèse qu'il se comporte d'une certaine manière plus que ce que je peux transmettre dans ses signatures membres d'une interface sans avoir à faire des affirmations sur chaque Temps il est instancié? Une interface semble être un bon concept, si seulement je pouvais également spécifier des choses ou des restrictions supplémentaires, elle est censée être mise en œuvre.

60
user4779

Au lieu de renvoyer un int, renvoyez un objet de valeur qui a la validation codée dur. C'est un cas d'obsession primitive et de son correctif.

// should be class, not struct as struct can be created without calling a constructor
public class ValidNumber
{
    public int Number { get; }

    public ValidNumber(int number)
    {
        if (number <= 5)
            throw new ArgumentOutOfRangeException("Number must be greater than 5.")
        Number = number;
    }
}

public class Implementation : ISomeInterface
{
    public ValidNumber SomeMethod(string a)
    {
        return new ValidNumber(int.Parse(a));
    }
}

De cette façon, l'erreur de validation se produirait à l'intérieur de la mise en œuvre. Il devrait donc apparaître lorsque le développeur teste cette mise en œuvre. Avoir la méthode renvoie un objet spécifique rend évident qu'il pourrait y avoir plus que de retourner une valeur branchée.

165
Euphoric

Pour compléter le Autreréponses , j'aimerais faire partiellement commenter la note suivante dans l'OP en fournissant un contexte plus large:

Une interface semble être un bon concept, si seulement je pouvais également spécifier des choses ou des restrictions supplémentaires, elle est censée être mise en œuvre.

Vous faites un bon point ici! Examinons sur lesquels nous pouvons spécifier de telles restrictions (contraintes):

  1. dans le système de type de la langue
  2. via méta-annotations internes ou externes à la langue et aux outils externes (outils d'analyse statique)
  3. via les assertions d'exécution - comme on le voit dans Autreréponses
  4. documentation ciblée chez l'homme

J'élabore sur chaque article ci-dessous. Avant cela, laissez-moi dire que les contraintes deviennent plus faibles et plus faibles, plus vous transmettez de 1 à 4. Par exemple, si vous ne comptez que sur le point 4, vous comptez sur les développeurs en appliquant correctement la documentation et que personne ne soit nullement. capable de vous dire si ces contraintes sont remplies autres que les humains eux-mêmes. Ceci, bien sûr, est beaucoup plus tenu de contenir des bugs de la nature même des humains.

Par conséquent, vous voulez toujours commencer à modéliser votre contrainte au point 1, et seulement si c'est (partiellement) impossible, vous devriez essayer le point 2, etc. En théorie, vous souhaitez toujours compter sur le système de type de la langue. Toutefois, pour que cela soit possible, vous auriez besoin d'avoir des systèmes de type très puissants, qui deviennent alors immobilisables - en termes de vitesse et d'effort de vérification de type et en termes de développeurs pouvant comporter des types. Pour ce dernier, voir est le Scala === Library de collections un cas de "la plus longue note de suicide dans l'histoire" ? .

1. Tapez le système de la langue

Dans la plupart des langues typées (oo-aromatisées) telles que c #, il est facilement possible d'avoir l'interface suivante:

public interface ISomeInterface
{
  int SomeMethod(string a);
}

Ici, le système de type vous permet de spécifier des types tels que int. Ensuite, le composant de type Checker du compilateur garantit heure de compilation que les implémentations renvoient toujours une valeur entière de SomeMethod.

De nombreuses applications peuvent déjà être construites avec les systèmes de type habituels trouvés dans Java et C #. Toutefois, pour la contrainte que vous aviez à l'esprit, à savoir que la valeur de retour est un entier supérieur à 5, ce type Les systèmes sont trop faibles. En effet, certaines langues présentent des systèmes de type plus puissants où au lieu de int vous pouvez écrire {x: int | x > 5}1, c'est-à-dire le type de tous les entiers supérieurs à 5. Dans certaines de ces langues, vous devez également prouver que, comme une implémentation, vous retournez toujours quelque chose de plus grand de 5. Ces preuves sont ensuite vérifiées par le compilateur à l'heure de la compilation!

Étant donné que C # ne présente pas de types, vous devez recourir aux points 2 et 3.

2. Meta Annotations internes/externes à la langue

Cette autre réponse Vous avez déjà fourni un exemple de méta-annotations dans la langue, ce qui est Java ici:

@NotNull
@Size(min = 1)
public List<@NotNull Customer> getAllCustomers() {
  return null;
}

Les outils d'analyse statique peuvent essayer de vérifier si ces contraintes spécifiées dans les méta-annotations sont remplies ou non dans le code. S'ils ne peuvent pas les vérifier, ces outils signalent une erreur.2 Habituellement, on utilise des outils d'analyse statiques avec le compilateur classique au moment de la compilation, ce qui signifie que vous obtenez une vérification de contrainte à Temps de compilation aussi bien ici.

Une autre approche serait d'utiliser des méta-annotations externes à la langue. Par exemple, vous pouvez avoir une base de code dans C, puis prouver l'accomplissement de certaines contraintes dans une langue totalement différente faisant référence à la base C code C. Vous pouvez trouver des exemples sous les mots-clés "Vérification du code C", "Vérification du COQ COQ" parmi d'autres.

3. Assertions d'exécution

À ce niveau de vérification des contraintes, vous externalisez complètement la vérification de la compilation et de l'analyse statique complètement à Runtime . Vous vérifiez au moment de l'exécution si la valeur de retour remplit votre contrainte (E.G. est supérieure à 5), et sinon, vous lancez une exception.

Autreréponses Vous avez déjà montré comment cela semble coeur-sage.

Ce niveau offre une grande flexibilité, cependant, au coût de la vérification de la contrainte de report de la compilation du temps d'exécution. Cela signifie que les insectes pourraient être révélés très tard, éventuellement au client de votre logiciel.

4. Documentation ciblée chez l'homme

J'ai dit que les affirmations d'exécution sont assez flexibles, cependant, ils ne peuvent toujours pas modéliser chaque contrainte que vous pouviez penser. Bien qu'il soit facile de mettre des contraintes sur les valeurs de retour, il est par exemple difficile (lu: immobilable) pour modéliser l'interaction entre les composants de code, car cela nécessiterait une image de "supervision" sur le code.

Par exemple, une méthode int f(void) peut garantir que sa valeur de retour est le score actuel du lecteur dans le jeu, mais aussi longtemps que int g(void) _ n'a pas été appelé remplaçant la valeur de retour de f. Cette contrainte est quelque chose que vous avez probablement besoin de différer la documentation axée sur l'homme.


1: Les mots-clés sont des "types dépendants", "Types de raffinement", "Types de liquide". Les premiers exemples sont des langues pour les proverveurs des théorèmes, par ex. Gallina qui est utilisé dans le Assistant anti-coq .

2: En fonction du type d'expressivité que vous autorisez dans vos contraintes, la vérification de l'épanouissement peut être un problème indécitable . Pratiquement, cela signifie que votre méthode programmée remplit les contraintes que vous avez spécifiées, mais l'outil d'analyse statique est incapable de leur prouver. Ou mettre différemment, il pourrait y avoir de faux négatifs en termes d'erreurs. (Mais jamais de faux positifs si l'outil est sans bug.)

25
ComFreek

Vous pouvez avoir la validation des annotations qui limitent les valeurs retournées acceptables. Voici un exemple dans Java pour printemps pris de Baeldung.com Mais en C #, vous avez un Caractéristique similaire :

   @NotNull
   @Size(min = 1)
   public List<@NotNull Customer> getAllCustomers() {
        return null;
   }

Si vous utilisez cette approche, vous devez considérer que:

  • Vous avez besoin d'un cadre de validation pour votre langue, dans ce cas C #
  • L'ajout de l'annotation n'est qu'une partie. Avoir l'annotation validé et comment ces erreurs de validation sont traitées sont la partie importante. Dans le cas du printemps, il crée un proxy à l'aide d'une injection de dépendance qui lancerait une exécution validationException
  • Certaines annotations peuvent aider votre IDE pour détecter des bogues à l'heure de compilation. Par exemple, si vous utilisez @nonnull, le compilateur peut vérifier que NULL n'est jamais retourné. Les autres validations doivent être appliquées au moment de l'exécution
  • La plupart des cadres de validation vous permettent de créer des validations personnalisées.
  • Il est très utile de traiter les données d'entrée dans lesquelles vous devrez peut-être signaler plus d'une validation cassée en même temps.

Je ne recommande pas cette approche lorsque la validation fait partie du modèle économique. Dans ce cas, le Réponse d'Euphoric est meilleur. L'objet retourné sera un objet de valeur Cela vous aidera à créer un modèle Rich Modèle de domaine . Cet objet devrait avoir un nom significatif avec les restrictions accessibles au type d'entreprise que vous faites. Par exemple, ici, je peux valider que les dates sont des raisons pour nos utilisateurs:

public class YearOfBirth

     private final year; 

     public YearOfBirth(int year){
        this.year = year;    
        if(year < 1880){
            throw new IllegalArgumentException("Are you a vampire or what?");
        }  
        if( year > 2020){
            throw new IllegalArgumentException("No time travelers allowed");     
         }
     }
}

La bonne partie est que ce type d'objet peut attirer de très petites méthodes avec une logique simple et testée. Par exemple:

public String getGeneration(){
      if( year < 1964){
           return "Baby Boomers";
      }
      if( year < 1980 ){
           return "Generation X";
      }
      if( year < 1996){
           return "Millenials";
      }
      // Etc...
}
1
Borjab

Il ne faut pas être compliqué:

public interface ISomeInterface
{
    // Returns the amount by which the desired value *exceeds* 5.
    uint SomeMethodLess5(string a);
}
1
Matt Timmermans

Vous pouvez écrire un test unitaire pour trouver toutes les implémentations d'une interface et exécuter un test contre chacun d'eux.

var iType= typeof(ISomeInterface);
var types = AppDomain.CurrentDomain.GetAssemblies()
    .SelectMany(s => s.GetTypes())
    .Where(p => iType.IsAssignableFrom(p));

foreach(var t in types)
{
    var result = t.SomeMethod("four");
    Assert.IsTrue(result > 4, "SomeMethod implementation result was less than 5");
}

Lorsqu'une nouvelle implémentation d'IsomeInterface est ajoutée à votre projet, ce test devrait pouvoir le tester et l'échec s'il renvoie quelque chose de moins de 5. Cela suppose bien sûr que vous pouvez le tester correctement de l'entrée à cette méthode, qui J'utilise "quatre" ici. Vous devrez peut-être faire d'autres moyens de configurer cet appel.

1
Neil N

Documentation du code Intellisense

La plupart des langues ont une forme de documentation de code d'intellensense. Pour C #, vous pouvez trouver des informations à ce sujet ici . En un mot, c'est la documentation de commentaire que vous IDE Intellisense peut analyser et mettre à la disposition de l'utilisateur quand ils veulent l'utiliser.

Quelle est votre documentation d'interface indique Le comportement d'un appel est le seul contrat réel de la façon dont il devrait se comporter. Après tout, votre interface ne sait pas comment indiquer la différence entre un générateur de nombres aléatoires qui donne une bonne sortie, et celui qui retourne toujours 4 (déterminé par un rouleau de matrice parfaitement aléatoire).

Tests unitaires pour vérifier la documentation est mis en œuvre fidèlement

Après la documentation, vous devez disposer d'une suite de test de l'unité pour votre interface donnée à une instance d'une classe qui implémente l'interface, dégage le comportement attendu lors de la course à travers divers cas d'utilisation. Pendant que vous pourrait cuire les tests de l'unité dans une classe abstraite pour renforcer le comportement, qui est surchargé et causera probablement plus de douleur qu'ausieur.

Méta-annotations

Je remplis que je devrais également mentionner que certaines langues soutiennent également une forme de méta-annotations. Des affirmations efficaces qui sont évaluées à la compilation. Bien qu'ils soient limités dans les types de chèques qu'ils puissent faire, ils peuvent au moins vérifier les fautes de programmation simples à la compilation. Cela devrait être considéré comme plus un compilateur assister avec la documentation de code que d'une exécution de l'interface.

0
Tezra