Retrabalhando uma função retornando um código inteiro que representa muitos status diferentes

10

Eu herdei um código terrível que incluí uma pequena amostra abaixo.

  • Existe um nome para esse anti-padrão específico?
  • Quais são algumas recomendações para refatorar isso?

    // 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) { 
    }
    
A_B
fonte
2
O que são "estabelecimento encontrado" e "estabelecimento necessário" ?
Tulains Córdova
4
Isso deveria ser um travessão , leia como "usuário válido encontrado: estabeleça sua sessão".
BJ Myers
2
@A_B Quais desses valores de retorno são logons bem-sucedidos e quais são logons com falha. Nem todos são evidentes.
Tulains Córdova
@A_B "estabelecer sua sessão" significa "sessão estabelecida" ou "precisa estabelecer uma sessão"?
Tulains Córdova
@ TulainsCórdova: "Estabelecer" significa tanto quanto "criar" (neste contexto, pelo menos) - para "estabelecer a sua sessão" é aproximadamente igual a "criar a sua sessão"
hoffmale

Respostas:

22

O código é ruim não apenas pelos números mágicos , mas porque combina vários significados no código de retorno, ocultando dentro de seu significado um erro, um aviso, uma permissão para criar uma sessão ou uma combinação dos três, o que o torna um entrada ruim para tomada de decisão.

Eu sugeriria a seguinte refatoração: retornando uma enumeração com os possíveis resultados (conforme sugerido em outras respostas), mas adicionando à enumeração um atributo indicando se é uma negação, uma renúncia (deixarei você passar desta última vez) ou se estiver 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());           
        }
    }
}

Saída para Test.java mostrando a gravidade de cada 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

Com base no valor da enumeração e na sua gravidade, você pode decidir se a criação da sessão continua ou não.

EDITAR:

Como resposta ao comentário de @T.Sar, alterei os possíveis valores da gravidade para PASS, WAIVER e DENIAL em vez de (OK, WARNING e ERROR). Dessa forma, fica claro que um DENIAL (anteriormente ERRO) não é um erro em si e não deve necessariamente se traduzir em gerar uma exceção. O chamador examina o objeto e decide se deve ou não lançar uma exceção, mas DENIAL é um status de resultado válido resultante da chamada processLogin(...).

  • PASS: vá em frente, crie uma sessão se ainda não existir
  • ISENÇÃO: vá em frente desta vez, mas na próxima vez que você não estiver autorizado a passar
  • NEGAÇÃO: desculpe, o usuário não pode passar, não crie uma sessão
Tulains Córdova
fonte
você também pode criar um enum "complexo" (enum com atributos) para incorporar o nível de erro no enum. No entanto, tenha cuidado porque se você usar ferramentas de serialização de somme, isso pode não ser muito bom.
23317 Walfrat
Lançar exceções nos casos de erro e salvar as enumerações apenas para o sucesso também é uma opção.
T. Sar
@ T.Sar Bem, como eu entendi, eles não são erros em si, mas negações para criar uma sessão por algum motivo. Vou editar a resposta.
Tulains Córdova
@ T.Sar Alterei os valores para PASS, WAIVER e DENIAL para deixar claro que o que eu chamei anteriormente de ERRO é um status válido. Talvez agora eu deva inventar um nome melhor paraSeverity
Tulains Córdova
Eu estava pensando em outra coisa com a minha sugestão, mas gostei muito da sua sugestão! Estou vomitando um +1, com certeza!
T. Sar
15

Este é um exemplo de obsessão primitiva - usando tipos primitivos para tarefas "simples" que acabam se tornando não tão simples.

Isso pode ter começado como um código que retornou a boolpara indicar sucesso ou falha, depois se transformou em intquando havia um terceiro estado e, eventualmente, se tornou uma lista completa de condições de erro quase não documentadas.

A refatoração típica para esse problema é criar uma nova classe / struct / enum / objeto / o que puder representar melhor o valor em questão. Nesse caso, você pode considerar mudar para um enumque contenha as condições do resultado ou mesmo uma classe que possa conter um boolpara êxito ou falha, uma mensagem de erro, informações adicionais etc.

Para obter mais padrões de refatoração que possam ser úteis, dê uma olhada na Folha de dicas Smells to Refactorings da Industrial Logic .

BJ Myers
fonte
7

Eu chamaria isso de um caso de "números mágicos" - números que são especiais e não têm significado óbvio por si mesmos.

A refatoração que eu aplicaria aqui é reestruturar o tipo de retorno em uma enumeração, pois encapsula a preocupação do domínio em um tipo. Lidar com os erros de compilação resultantes disso deve ser possível aos poucos, pois as enumerações java podem ser ordenadas e numeradas. Mesmo se não, não deve ser difícil lidar com eles diretamente, em vez de recair nas ints.

Daenyth
fonte
Não é o que geralmente se entende por 'números mágicos'.
D Drmmr
2
Ele aparecerá como números mágicos nos sites de chamadas, como emif (processLogin(..) == 3)
Daenyth 23/06
@DDrmmr - É exatamente isso que significa o cheiro do código dos 'números mágicos'. Essa assinatura de função quase certamente implica que processLogin () contém linhas como "return 8;" em sua implementação, e praticamente força o código usando processLogin () a se parecer com algo assim "if (resultFromProcessLogin == 7) {".
Stephen C. Aço
3
@ Stephen O valor real dos números é irrelevante aqui. Eles são apenas identificações. O termo números mágicos é geralmente usado para valores no código que têm um significado, mas quem não está documentado (por exemplo, em um nome de variável). Substituir os valores aqui por variáveis ​​inteiras nomeadas não corrigirá o problema.
D Drmmr
2

Esse é um código particularmente desagradável. O antipadrão é conhecido como "código de retorno mágico". Você pode encontrar uma discussão aqui .

Muitos dos valores de retorno indicam estados de erro. Há um debate válido sobre a utilização do tratamento de erros para controle de fluxo, mas no seu caso, acho que existem três casos: sucesso (código 4), sucesso, mas é necessário alterar a senha (código 5) e "não permitido". Portanto, se você não se importa em usar exceções para controle de fluxo, use exceções para indicar esses estados.

Outra abordagem seria refatorar o design para que você retorne um objeto "usuário", com um atributo "perfil" e "sessão" para um login bem-sucedido, um atributo "must_change_password", se necessário, e vários atributos para indicar por que o log -in falhou se esse era o fluxo.

Neville Kuyt
fonte