Retravailler une fonction renvoyant un code entier qui représente de nombreux statuts différents

10

J'ai hérité d'un code horrible dont j'ai inclus un court échantillon ci-dessous.

  • Y a-t-il un nom pour cet anti-modèle particulier?
  • Quelles sont quelques recommandations pour refactoriser cela?

    // 0=Need to log in / present username and password
    // 2=Already logged in
    // 3=Inactive User found
    // 4=Valid User found-establish their session
    // 5=Valid User found with password change needed-establish their session
    // 6=Invalid User based on app login
    // 7=Invalid User based on network login
    // 8=User is from an non-approved remote address
    // 9=User account is locked
    // 10=Next failed login, the user account will be locked
    
    public int processLogin(HttpServletRequest request, HttpServletResponse response, 
                            int pwChangeDays, ServletContext ServContext) { 
    }
    
UN B
la source
2
Qu'est-ce que «établir-fondé» et «établir-nécessaire» ?
Tulains Córdova
4
C'est censé être un tiret cadratin , lu comme "utilisateur valide trouvé: établir sa session".
BJ Myers
2
@A_B Lesquelles de ces valeurs de retour sont des connexions réussies sont celles qui ont échoué. Tous ne vont pas de soi.
Tulains Córdova
@A_B Est-ce que "établir sa session" signifie "session établie" ou "doit établir une session"?
Tulains Córdova
@ TulainsCórdova: "Établir" signifie autant que "créer" (dans ce contexte au moins) - donc "établir leur session" est à peu près égal à "créer leur session"
hoffmale

Réponses:

22

Le code est mauvais non seulement parce que les nombres magiques , mais parce qu'il fusionne plusieurs significations dans le code de retour, cachant à l'intérieur de sa signification une erreur, un avertissement, une autorisation de créer une session ou une combinaison des trois, ce qui en fait un mauvaise entrée pour la prise de décision.

Je suggérerais la refactorisation suivante: renvoyer une énumération avec les résultats possibles (comme suggéré dans d'autres réponses), mais en ajoutant à l'énumération un attribut indiquant s'il s'agit d'un déni, d'une renonciation (je vous laisse passer cette dernière fois) ou si c'est OK (PASS):

public LoginResult processLogin(HttpServletRequest request, HttpServletResponse response, 
                            int pwChangeDays, ServletContext ServContext) { 
    }

==> LoginResult.java <==

public enum LoginResult {
    NOT_LOGGED_IN(Severity.DENIAL),
    ALREADY_LOGGED_IN(Severity.PASS),
    INACTIVE_USER(Severity.DENIAL),
    VALID_USER(Severity.PASS),
    NEEDS_PASSWORD_CHANGE(Severity.WAIVER),
    INVALID_APP_USER(Severity.DENIAL),
    INVALID_NETWORK_USER(Severity.DENIAL),
    NON_APPROVED_ADDRESS(Severity.DENIAL),
    ACCOUNT_LOCKED(Severity.DENIAL),
    ACCOUNT_WILL_BE_LOCKED(Severity.WAIVER);

    private Severity severity;

    private LoginResult(Severity severity) {
        this.severity = severity;
    }

    public Severity getSeverity() {
        return this.severity;
    }
}

==> Severity.java <==

public enum Severity {
    PASS,
    WAIVER,
    DENIAL;
}

==> Test.java <==

public class Test {

    public static void main(String[] args) {
        for (LoginResult r: LoginResult.values()){
            System.out.println(r + " " +r.getSeverity());           
        }
    }
}

Sortie pour Test.java montrant la gravité de chaque LoginResult:

NOT_LOGGED_IN : DENIAL
ALREADY_LOGGED_IN : PASS
INACTIVE_USER : DENIAL
VALID_USER : PASS
NEEDS_PASSWORD_CHANGE : WAIVER
INVALID_APP_USER : DENIAL
INVALID_NETWORK_USER : DENIAL
NON_APPROVED_ADDRESS : DENIAL
ACCOUNT_LOCKED : DENIAL
ACCOUNT_WILL_BE_LOCKED : WAIVER

En fonction de la valeur enum et de sa gravité, vous pouvez décider si la création de la session se poursuit ou non.

ÉDITER:

En réponse au commentaire de @ T.Sar, j'ai changé les valeurs possibles de la gravité en PASS, WAIVER et DENIAL au lieu de (OK, WARNING et ERROR). De cette façon, il est clair qu'un REFUS (auparavant ERREUR) n'est pas une erreur en soi et ne devrait pas nécessairement se traduire par le lancement d'une exception. L'appelant examine l'objet et décide de lever ou non une exception, mais DENIAL est un état de résultat valide résultant de l'appel processLogin(...).

  • PASS: allez-y, créez une session si elle n'existe pas déjà
  • RENONCIATION: allez-y cette fois, mais la prochaine fois, vous ne serez peut-être pas autorisé à passer
  • DENIAL: désolé, l'utilisateur ne peut pas passer, ne créez pas de session
Tulains Córdova
la source
vous pouvez également créer une énumération "complexe" (énumération avec attributs) pour incorporer le niveau d'erreur dans l'énumération. Cependant, soyez prudent car si vous utilisez certains outils de sérialisation, cela pourrait ne pas fonctionner très bien.
Walfrat
La levée d'exceptions dans les cas d'erreur et l'enregistrement des énumérations pour le succès uniquement est également une option.
T. Sar
@ T.Sar Eh bien, si j'ai bien compris, ce ne sont pas des erreurs en soi, mais des refus de créer une session pour une raison quelconque. Je vais modifier la réponse.
Tulains Córdova
@ T.Sar J'ai changé les valeurs en PASS, WAIVER et DENIAL pour qu'il soit clair que ce que j'appelais auparavant ERROR est un état valide. Peut-être que maintenant je devrais trouver un meilleur nom pourSeverity
Tulains Córdova
Je pensais à autre chose avec ma suggestion, mais j'ai vraiment aimé votre suggestion! Je lance un +1, c'est sûr!
T. Sar
15

Ceci est un exemple d' obsession primitive - en utilisant des types primitifs pour des tâches "simples" qui finissent par devenir moins simples.

Cela peut avoir commencé comme un code qui renvoyait un boolpour indiquer le succès ou l'échec, puis s'est transformé en un intquand il y avait un troisième état, et est finalement devenu une liste complète de conditions d'erreur presque non documentées.

La refactorisation typique de ce problème consiste à créer une nouvelle classe / struct / enum / objet / tout ce qui peut mieux représenter la valeur en question. Dans ce cas, vous pouvez envisager de basculer vers une enumqui contient les conditions de résultat, ou même une classe qui pourrait contenir une boolréussite ou un échec, un message d'erreur, des informations supplémentaires, etc.

Pour plus de modèles de refactorisation qui pourraient être utiles, jetez un coup d'œil à la feuille de triche Smells to Refactorings d' Industrial Logic .

BJ Myers
la source
7

J'appellerais cela un cas de "nombres magiques" - des nombres qui sont spéciaux et n'ont pas de signification évidente en eux-mêmes.

Le refactoring que j'appliquerais ici est de restructurer le type de retour en une énumération, car il encapsule le domaine concerné dans un type. Traiter les erreurs de compilation qui en découlent devrait être possible au coup par coup, car les énumérations java peuvent être ordonnées et numérotées. Même si ce n'est pas le cas, il ne devrait pas être difficile de les traiter directement au lieu de retomber dans les mœurs.

Daenyth
la source
Ce n'est pas ce que l'on entend généralement par «nombres magiques».
D Drmmr
2
Il apparaîtra comme des nombres magiques sur les sites d'appels, comme dansif (processLogin(..) == 3)
Daenyth
@DDrmmr - C'est exactement ce que l'on entend par l'odeur du code des «nombres magiques». Cette signature de fonction implique presque certainement que processLogin () contient des lignes comme "return 8;" dans son implémentation, et il force à peu près le code utilisant processLogin () à ressembler à ceci "if (resultFromProcessLogin == 7) {".
Stephen C. Steel
3
@Stephen La valeur réelle des nombres n'a pas d'importance ici. Ce ne sont que des pièces d'identité. Le terme nombres magiques est généralement utilisé pour les valeurs dans le code qui ont une signification, mais dont la signification n'est pas documentée (par exemple dans un nom de variable). Remplacer les valeurs ici par des variables entières nommées ne résoudra pas le problème.
D Drmmr
2

C'est un morceau de code particulièrement désagréable. L'antipattern est connu sous le nom de "codes de retour magiques". Vous pouvez trouver une discussion ici .

De nombreuses valeurs de retour indiquent des états d'erreur. Il y a un débat valide sur l'opportunité d'utiliser la gestion des erreurs pour le contrôle de flux, mais dans votre cas, je pense qu'il y a 3 cas: succès (code 4), succès mais besoin de changer de mot de passe (code 5) et "non autorisé". Donc, si vous ne vous souciez pas de l'utilisation d'exceptions pour le contrôle de flux, vous pouvez utiliser des exceptions pour indiquer ces états.

Une autre approche serait de refactoriser la conception afin que vous renvoyiez un objet "utilisateur", avec un attribut "profil" et "session" pour une connexion réussie, un attribut "must_change_password" si nécessaire, et un tas d'attributs pour indiquer pourquoi le journal -in a échoué si tel était le flux.

Neville Kuyt
la source