web-dev-qa-db-fra.com

Lancement d'exceptions dans les instructions switch lorsqu'aucun cas spécifié ne peut être géré

Disons que nous avons une fonction qui modifie le mot de passe d'un utilisateur dans un système dans une application MVC:

public JsonResult ChangePassword
    (string username, string currentPassword, string newPassword)
{
    switch (this.membershipService.ValidateLogin(username, currentPassword))
    {
        case UserValidationResult.BasUsername:
        case UserValidationResult.BadPassword:
            // abort: return JsonResult with localized error message        
            // for invalid username/pass combo.
        case UserValidationResult.TrialExpired
            // abort: return JsonResult with localized error message
            // that user cannot login because their trial period has expired
        case UserValidationResult.Success:
            break;
    }

    // NOW change password now that user is validated
}

membershipService.ValidateLogin() renvoie une énumération UserValidationResult définie comme suit:

enum UserValidationResult
{
    BadUsername,
    BadPassword,
    TrialExpired,
    Success
}

Étant un programmeur défensif, je changerais la méthode ChangePassword() ci-dessus pour lever une exception s'il y a une valeur UserValidationResult non reconnue de ValidateLogin():

public JsonResult ChangePassword
    (string username, string currentPassword, string newPassword)
{
    switch (this.membershipService.ValidateLogin(username, currentPassword))
    {
        case UserValidationResult.BasUsername:
        case UserValidationResult.BadPassword:
            // abort: return JsonResult with localized error message        
            // for invalid username/pass combo.
        case UserValidationResult.TrialExpired
            // abort: return JsonResult with localized error message
            // that user cannot login because their trial period has expired
        case UserValidationResult.Success:
            break;
        default:
            throw new NotImplementedException
                ("Unrecognized UserValidationResult value.");
            // or NotSupportedException()
            break;
    }

    // Change password now that user is validated
}

J'ai toujours considéré un modèle comme le dernier extrait au-dessus d'une meilleure pratique. Par exemple, que se passe-t-il si un développeur obtient une exigence selon laquelle maintenant, lorsque les utilisateurs tentent de se connecter, si pour telle ou telle raison commerciale, ils sont censés contacter l'entreprise en premier? La définition de UserValidationResult est donc mise à jour pour être:

enum UserValidationResult
{
    BadUsername,
    BadPassword,
    TrialExpired,
    ContactUs,
    Success
}

Le développeur modifie le corps de la méthode ValidateLogin() pour renvoyer la nouvelle valeur d'énumération (UserValidationResult.ContactUs) Le cas échéant, mais oublie de mettre à jour ChangePassword(). Sans exception dans le commutateur, l'utilisateur est toujours autorisé à changer son mot de passe alors que sa tentative de connexion ne devrait même pas être validée en premier lieu!

Est-ce juste moi, ou est-ce que cette default: throw new Exception() est une bonne idée? Je l'ai vu il y a quelques années et je suppose toujours (après l'avoir poussé) que c'est une meilleure pratique.

57
CantSleepAgain

Je lève toujours une exception dans ce cas. Pensez à utiliser InvalidEnumArgumentException , ce qui donne des informations plus riches dans cette situation.

81
Matt Greer

Avec ce que vous avez, c'est bien, bien que l'instruction break ne soit jamais frappée car l'exécution de ce thread cessera lorsqu'une exception est levée et non gérée.

3
Jesus Ramos

J'ai déjà utilisé cette pratique pour des cas spécifiques où l'énumération vit "loin" de son utilisation, mais dans les cas où l'énumération est vraiment proche et dédiée à une fonctionnalité spécifique, cela semble un peu beaucoup.

Selon toute vraisemblance, je soupçonne qu'un échec d'assertion de débogage est probablement plus approprié.

http://msdn.Microsoft.com/en-us/library/6z60kt1f.aspx

Notez le deuxième exemple de code ...

3
Rob Cooke

J'aime toujours faire exactement ce que vous avez, bien que je jette généralement un ArgumentException si c'est un argument qui a été transmis, mais j'aime mieux NotImplementedException car l'erreur est probablement qu'un nouveau L'instruction case doit être ajoutée plutôt que l'appelant ne doit changer l'argument en argument pris en charge.

2
Davy8

Je n'ajoute jamais de pause après une instruction throw contenue dans une instruction switch. Pas le moindre des soucis n'est l'ennuyeux avertissement "code inaccessible détecté". Alors oui, c'est une bonne idée.

0
ChaosPandion

Je pense que des approches comme celle-ci peuvent être très utiles (par exemple, voir Chemins de code vides par erreur ). C'est encore mieux si vous pouvez faire compiler ce jet par défaut dans le code publié, comme une assertion. De cette façon, vous avez la défense supplémentaire pendant le développement et les tests, mais n'encourez pas la surcharge de code supplémentaire lors de la publication.

0
Ned Batchelder

Vous dites que vous êtes très défensif et je pourrais presque dire que c'est par dessus bord. L'autre développeur ne va-t-il pas tester son code? Assurément, lorsqu'ils effectueront les tests les plus simples, ils verront que l'utilisateur peut toujours se connecter - alors, ils se rendront compte de ce qu'ils doivent corriger. Ce que vous faites n'est pas horrible ou mal, mais si vous passez beaucoup de temps à le faire, cela pourrait être trop.

0
Chad

Pour une application Web, je préférerais une valeur par défaut qui génère un résultat avec un message d'erreur qui demande à l'utilisateur de contacter un administrateur et enregistre une erreur plutôt que de lever une exception qui pourrait entraîner quelque chose de moins significatif pour l'utilisateur. Étant donné que je peux anticiper, dans ce cas, que la valeur de retour pourrait être autre chose que ce que j'attends, je ne considérerais pas ce comportement vraiment exceptionnel.

En outre, votre cas d'utilisation qui entraîne la valeur d'énumération supplémentaire ne doit pas introduire d'erreur dans votre méthode particulière. Je m'attendrais à ce que le cas d'utilisation ne s'applique qu'à la connexion - ce qui se produirait avant que la méthode ChangePassword puisse légalement être appelée, car vous voudriez vous assurer que la personne était connectée avant de changer son mot de passe - vous devriez ne jamais réellement voir la valeur ContactUs comme un retour de la validation du mot de passe. Le cas d'utilisation spécifierait que si un résultat ContactUs est renvoyé, cette authentification échoue jusqu'à ce que la condition résultant du résultat soit effacée. S'il en était autrement, je m'attendrais à ce que vous ayez des exigences sur la façon dont les autres parties du système devraient réagir dans cette condition et vous écririez des tests qui vous forceraient à modifier le code de cette méthode pour vous conformer à ces tests et adresse la nouvelle valeur de retour.

0
tvanfosson

La validation des contraintes d'exécution est généralement une bonne chose, donc oui, la meilleure pratique, aide avec le principe `` échec rapide '' et vous vous arrêtez (ou vous connectez) lors de la détection d'une condition d'erreur plutôt que de continuer en silence. Il y a des cas où ce n'est pas vrai, mais étant donné les déclarations de changement, je pense que nous ne les voyons pas très souvent.

Pour élaborer, les instructions switch peuvent toujours être remplacées par des blocs if/else. En le considérant dans cette perspective, nous voyons que le lancer vs ne pas jeter dans un cas de commutateur par défaut est à peu près équivalent à cet exemple:

    if( i == 0 ) {


    } else { // i > 0


    }

contre

    if( i == 0 ) {


    } else if ( i > 0 ) {


    } else { 
        // i < 0 is now an enforced error
        throw new Illegal(...)
    }

Le deuxième exemple est généralement considéré comme meilleur car vous obtenez un échec lorsqu'une contrainte d'erreur est violée plutôt que de continuer sous une hypothèse potentiellement incorrecte.

Si au contraire nous voulons:

    if( i == 0 ) {


    } else { // i != 0
      // we can handle both positve or negative, just not zero
    }

Ensuite, je soupçonne en pratique que le dernier cas apparaîtra probablement comme une déclaration if/else. En raison de cela, l'instruction switch ressemble si souvent au deuxième bloc if/else, que les lancers sont généralement une meilleure pratique.

Quelques considérations supplémentaires valent la peine: - envisager une approche polymorphe ou une méthode enum pour remplacer complètement l'instruction switch, par exemple: Méthodes à l'intérieur d'énumération en C # - si lancer est le meilleur, comme indiqué dans d'autres réponses, préférez pour utiliser un type d'exception spécifique pour éviter le code de plaque de chaudière, par exemple: InvalidEnumArgumentException

0
LaFayette