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.
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.
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):
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" ? .
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.
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.
À 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.
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.)
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:
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...
}
Il ne faut pas être compliqué:
public interface ISomeInterface
{
// Returns the amount by which the desired value *exceeds* 5.
uint SomeMethodLess5(string a);
}
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.
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).
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.
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.