Utilisation de la vérification de type statique pour se protéger contre les erreurs commerciales

13

Je suis un grand fan de la vérification de type statique. Cela vous empêche de faire des erreurs stupides comme ceci:

// java code
Adult a = new Adult();
a.setAge("Roger"); //static type checker would complain
a.setName(42); //and here too

Mais cela ne vous empêche pas de faire des erreurs stupides comme celle-ci:

Adult a = new Adult();
// obviously you've mixed up these fields, but type checker won't complain
a.setAge(150); // nobody's ever lived this old
a.setWeight(42); // a 42lb adult would have serious health issues

Le problème survient lorsque vous utilisez le même type pour représenter des types d'informations évidemment différents. Je pensais qu'une bonne solution à cela serait d'étendre la Integerclasse, juste pour éviter les erreurs de logique métier, mais pas pour ajouter des fonctionnalités. Par exemple:

class Age extends Integer{};
class Pounds extends Integer{};

class Adult{
    ...
    public void setAge(Age age){..}
    public void setWeight(Pounds pounds){...}
}

Adult a = new Adult();
a.setAge(new Age(42));
a.setWeight(new Pounds(150));

Est-ce considéré comme une bonne pratique? Ou y a-t-il des problèmes d'ingénierie imprévus en cours de route avec une conception aussi restrictive?

J-bob
la source
5
Vous ne a.SetAge( new Age(150) )compilerez toujours pas ?
John Wu
1
Le type int de Java a une plage fixe. De toute évidence, vous aimeriez avoir des entiers avec des plages personnalisées, par exemple Integer <18, 110>. Vous recherchez des types de raffinement ou des types dépendants, que certaines langues (non traditionnelles) proposent.
Theodoros Chatzigiannakis
3
Ce dont vous parlez est une excellente façon de faire du design! Malheureusement, Java a un système de type si primitif qu'il est difficile de bien faire les choses. Et même si vous essayez de le faire, cela peut entraîner des effets secondaires tels que des performances inférieures ou une productivité des développeurs inférieure.
Euphoric
@JohnWu oui, votre exemple sera toujours compilé, mais c'est le seul point de défaillance pour cette logique. Une fois que vous avez déclaré votre new Age(...)objet, vous ne pouvez pas l'affecter de manière incorrecte à une variable de type Weightà d'autres endroits. Cela réduit le nombre d'endroits où des erreurs peuvent se produire.
J-bob

Réponses:

12

Vous demandez essentiellement un système unitaire (non, pas des tests unitaires, "unité" comme dans "unité physique", comme les compteurs, les volts, etc.).

Dans votre code Agereprésente le temps et Poundsreprésente la masse. Cela conduit à des choses comme la conversion d'unités, les unités de base, la précision, etc.


Il y a eu / il y a eu des tentatives pour mettre une telle chose en Java, par exemple:

Les deux derniers semblent vivre dans cette chose github: https://github.com/unitsofmeasurement


C ++ a des unités via Boost


LabView est livré avec un tas d'unités .


Il existe d'autres exemples dans d'autres langues. (les modifications sont les bienvenues)


Est-ce considéré comme une bonne pratique?

Comme vous pouvez le voir ci-dessus, plus un langage est utilisé pour gérer des valeurs avec des unités, plus il prend en charge nativement les unités. LabView est souvent utilisé pour interagir avec des appareils de mesure. En tant que tel, il est logique d'avoir une telle fonctionnalité dans la langue et son utilisation serait certainement considérée comme une bonne pratique.

Mais dans tout langage de haut niveau à usage général, où la demande est aussi faible pour une telle rigueur, c'est probablement inattendu.

Ou y a-t-il des problèmes d'ingénierie imprévus en cours de route avec une conception aussi restrictive?

Ma conjecture serait: performance / mémoire. Si vous traitez avec beaucoup de valeurs, la surcharge d'un objet par valeur peut devenir un problème. Mais comme toujours: l' optimisation prématurée est la racine de tout mal .

Je pense que le plus gros "problème" est de s'y habituer, car l'unité est généralement implicitement définie comme suit:

class Adult
{
    ...
    public void setAge(int ageInYears){..}

Les gens seront confus lorsqu'ils devront passer un objet comme valeur pour quelque chose qui pourrait apparemment être décrit avec un simple int, lorsqu'ils ne sont pas familiers avec les systèmes d'unités.

nul
la source
"Les gens seront confus quand ils devront passer un objet comme valeur pour quelque chose qui pourrait apparemment être décrit avec un simple int..." --- pour lequel nous avons ici le principal de Least Surprise comme guide. Bonne prise.
Greg Burghardt
1
Je pense que les plages personnalisées déplacent cela hors du domaine d'une bibliothèque d'unités et dans les types dépendants
jk.
6

Contrairement à la réponse nulle, la définition d'un type pour une "unité" peut être bénéfique si un entier n'est pas suffisant pour décrire la mesure. Par exemple, le poids est souvent mesuré en plusieurs unités dans le même système de mesure. Pensez «livres» et «onces» ou «kilogrammes» et «grammes».

Si vous avez besoin d'un niveau de mesure plus granulaire, définir un type pour l'unité est avantageux:

public struct Weight {
    private int pounds;
    private int ounces;

    public Weight(int pounds, int ounces) {
        // Value range checks go here
        // Throw exception if ounces is greater than 16?
    }

    // Getters go here
}

Pour quelque chose comme "l'âge", je recommande de calculer qu'au moment de l'exécution en fonction de la date de naissance de la personne:

public class Adult {
    private Date birthDate;

    public Interval getCurrentAge() {
        return calculateAge(Date.now());
    }

    public Interval calculateAge(Date date) {
        // Return interval between birthDate and date
    }
}
Greg Burghardt
la source
2

Ce que vous semblez rechercher est connu sous le nom de types balisés . C'est une façon de dire "c'est un entier qui représente l'âge" alors que "c'est aussi un entier mais ça représente du poids" et "tu ne peux pas assigner l'un à l'autre". Notez que cela va plus loin que les unités physiques telles que les mètres ou les kilogrammes: je peux avoir dans mon programme "les hauteurs des gens" et les "distances entre les points sur une carte", mesurés tous les deux en mètres, mais non compatibles les uns avec les autres depuis l'attribution d'un à l'autre n'a pas de sens du point de vue de la logique métier.

Certaines langues, comme Scala, supportent assez facilement les types balisés (voir le lien ci-dessus). Dans d'autres, vous pouvez créer vos propres classes wrapper, mais cela est moins pratique.

La validation, par exemple vérifier que la taille d'une personne est "raisonnable" est un autre problème. Vous pouvez mettre un tel code dans votre Adultclasse (constructeur ou setters), ou dans vos classes de type / wrapper marquées. D'une certaine manière, les classes intégrées telles que URLou UUIDremplissent un tel rôle (entre autres, par exemple, fournir des méthodes utilitaires).

Que l'utilisation de types balisés ou de classes wrapper contribue réellement à améliorer votre code, cela dépendra de plusieurs facteurs. Si vos objets sont simples et ont peu de champs, le risque de les affecter incorrectement est faible et le code supplémentaire nécessaire pour utiliser les types balisés ne vaut pas la peine. Dans les systèmes complexes avec des structures complexes et beaucoup de champs (surtout si beaucoup d'entre eux partagent le même type primitif), cela peut en fait être utile.

Dans le code que j'écris, je crée souvent des classes wrapper si je passe des cartes. Les types tels que Map<String, String>sont très opaques par eux-mêmes, donc les envelopper dans des classes avec des noms significatifs tels que cela NameToAddressaide beaucoup. Bien sûr, avec les types balisés, vous pouvez écrire Map<Name, Address>et ne pas avoir besoin de l'encapsuleur pour toute la carte.

Cependant, pour les types simples tels que Strings ou Integers, j'ai trouvé que les classes wrapper (en Java) étaient trop gênantes. La logique métier habituelle n'était pas si mauvaise, mais il y a eu un certain nombre de problèmes avec la sérialisation de ces types en JSON, leur mappage vers des objets DB, etc. Vous pouvez écrire des mappeurs et des hooks pour tous les grands frameworks (par exemple Jackson et Spring Data), mais le travail supplémentaire et la maintenance liés à ce code compenseront tout ce que vous gagnerez en utilisant ces wrappers. Bien sûr, YMMV et dans un autre système, l'équilibre peut être différent.

Michał Kosmulski
la source