O que é um bom design para um método que pode retornar vários resultados logicamente diferentes?

5

O título da pergunta é provavelmente muito abstrato, portanto, deixe-me fornecer um exemplo específico do que tenho em mente:

Existe um webservice que encapsula um processo de alteração de senhas para usuários de um sistema distribuído. O webservice aceita o login do usuário, sua senha antiga e uma nova senha. Com base nessa entrada, ele pode retornar um dos três resultados a seguir:

  1. Caso o usuário não seja encontrado, ou sua senha antiga não corresponda, ele simplesmente retornará com HTTP 403 Forbidden.
  2. Caso contrário, é necessária uma nova senha e a conformidade com uma política de senha (por exemplo, é longa o suficiente, contém uma combinação adequada de letras e números etc.). Caso contrário, retornará um XML descrevendo porque a senha não está em conformidade com a política.
  3. Caso contrário, ele alterará a senha e retornará um XML contendo uma data de expiração da nova senha.

Agora, gostaria de criar uma classe, idealmente com um único método, para encapsular o trabalho com esse serviço da web. Minha primeira foto foi esta:

public class PasswordManagementWebService
{
    public ChangePasswordResult ChangePassword(string login, string oldPassword, string newPassword)
    {
        ChangePasswordResult result;

        // send input to websevice, it's not important how; the httpResponse
        // will contain a response from webservice
        var httpResponse;
        if (HasAuthenticationFailed(httpResponse)
        {
            throw new AuthenticationException();
        }
        else if (WasPasswordSuccessfullyChanged(httpResponse))
        {
            result = new ChangePasswordSuccessfulResult(httpResponse);
        }
        else
        {
            result = new ChangePasswordUnsuccessfulResult(httpResponse);
        }

        return result;
    }
}    

public abstract class ChangePasswordResult
{
    public abstract bool WasSuccessful { get; }
}

public abstract class ChangePasswordSuccessfulResult
{
    public ChangePasswordSuccessfulResult(HttpResponse  httpResponse)
    {
        // initialize the class from the httpResponse
    }

    public override bool WasSuccessful { get { return true; } }
    public DateTime ExpirationDate { get; private set; }
}

public abstract class ChangePasswordUnsuccessfulResult
{
    public ChangePasswordUnsuccessfulResult(HttpResponse  httpResponse)
    {
        // initialize the class from the httpResponse
    }

    public override bool WasSuccessful { get { return false; } }

    public bool WasPasswordLongEnough { get; private set; }        
    public bool DoesPasswordHaveToContainNumbers { get; private set; }
    // ... etc.         
}

Como você pode ver, eu decidi usar classes separadas para casos de retorno # 2 e # 3 - eu poderia ter usado uma única aula com um booleano, mas pareceria um cheiro, a classe não teria um propósito claro . Com duas classes separadas, um usuário da minha classe PasswordManagementWebService agora precisa saber quais classes herdam de ChangePasswordResult e converter para uma classe correta com base na propriedade WasSuccessful . Embora agora eu tenha uma boa aula focada em laser, tornei a vida dos meus usuários mais difícil do que deveria.

Quanto ao caso # 1, decidi lançar uma exceção. Eu poderia ter criado uma exceção separada para o caso # 2, também, e só retornar algo do método quando a senha foi alterada com sucesso. No entanto, isso não parece certo - não acho que uma nova senha sendo inválida seja um estado excepcional o suficiente para justificar uma exceção.

Não sei ao certo como é que eu criaria as coisas se houvesse mais de dois tipos de resultados não excepcionais do serviço Web. Provavelmente, eu alteraria um tipo de propriedade WasSuccessful de booleano para um enum e o renomeia para ResultType , adicionando uma classe dedicada herdada de ChangePasswordResult para cada possível ResultType .

Por último, para a pergunta atual: Essa abordagem de design (ou seja, ter uma classe abstrata e forçar os clientes a converter em um resultado correto com base em uma propriedade) é correta ao lidar com problemas como esse ? Se sim, existe uma maneira de melhorá-lo (talvez com uma estratégia diferente para quando lançar exceções versus resultados de retorno)? Se não, o que você recomendaria?

    
por Nikola Anusev 20.07.2014 / 18:06
fonte

3 respostas

5

Eu não assino a escola de pensamento que diz "exceções devem ser apenas para casos excepcionais!" As pessoas estão com medo de exceções por algum motivo. Se você não pode fazer o que você disse que vai fazer, lance uma exceção - mesmo que seja uma falha comum ou esperada.

Eu gosto disso por alguns motivos. É impossível para o chamador ignorar (alguém poderia facilmente esquecer de inspecionar o valor de retorno de um método que indica falha por um código de retorno.) É simples (sem valores de espaço reservado para resultados perdidos ou hierarquias que precisam de downcasting). minha alteração foi bem-sucedida ou você me diz para me perder; não tenho dúvidas sobre qual é.) Ela é granular (você pode anexar tanta informação quanto desejar à exceção e lançar diferentes exceções para diferentes falhas.)

Então, eu criaria seu método assim:

public void ChangePassword(string password)
{
    HttpResponse response = RequestPasswordChange();
    if (AuthFailed(response))
        throw new AuthorizationException();
    if (PasswordWasNotChanged(response))
        throw new InvalidNewPasswordException(
            WasPasswordLongEnough(response),
            DidPasswordContainNumbers(response)
        );
}

"Sucesso-ou-lance" é uma boa regra de ouro, e as pessoas estão acostumadas com isso, não importa o quanto acenem seus braços. Afinal, Dictionary<TKey, TValue> gera KeyNotFoundException quando você não consegue indexar nele. Um uso não excepcional de exceções.

    
por 21.07.2014 / 01:41
fonte
3

Acho que esta abordagem está usando a herança em excesso, o que adiciona complexidade adicional (desnecessária). Favorecer a composição sobre herança quando possível. Especialmente se for uma coisa de Sucesso / Não-Sucesso que é facilmente representada com um booleano, não justifica a necessidade de subclasses.

Você pode incluir tudo o que precisa em uma única classe e, com base no resultado, pode usar as propriedades de acordo. A classe ainda tem um único objetivo, para retornar informações sobre a alteração da senha. O fato de que existem vários resultados diferentes é uma complexidade essencial da tarefa.

class ChangePasswordResult
{
    public bool Result { get; private set; }
    public DateTime ExpirationDate { get; private set; }  
    public bool WasPasswordLongEnough { get; private set; }        
    public bool DoesPasswordHaveToContainNumbers { get; private set; }
}

Editar:

jhewlett Apresenta um bom ponto também sobre serialização. Com uma única estrutura de resultados sendo retornada, você tem uma interface mais estável com a qual os clientes podem interagir. Ao documentar sua interface, você pode dizer "este ponto de extremidade retorna X", em vez de "este ponto de extremidade retorna X ou Y, dependendo de ..."

    
por 21.07.2014 / 00:55
fonte
0

Suponho que haja ação diferente quando WasSuccessful for true e ação diferente quando WasSuccessful for false . Por que não colocar esse código dentro de ChangePasswordSuccessfulResult e ChangePasswordUnsuccessfulResult e chamá-lo através do método virtual, decidindo assim com base no tipo. Então, você não precisa se preocupar com o elenco.

public abstract class ChangePasswordResult
{
    public abstract SomeResponseData GetResponseData();
}

public class ChangePasswordSuccessfulResult
{
    public ChangePasswordSuccessfulResult(HttpResponse  httpResponse)
    {
        // initialize the class from the httpResponse
    }

    public DateTime ExpirationDate { get; private set; }

    public override SomeResponseData GetResponseData()
    {
        // return data for successful result
    }
}

public abstract class ChangePasswordUnsuccessfulResult
{
    public ChangePasswordUnsuccessfulResult(HttpResponse  httpResponse)
    {
        // initialize the class from the httpResponse
    }

    public bool WasPasswordLongEnough { get; private set; }        
    public bool DoesPasswordHaveToContainNumbers { get; private set; }
    // ... etc.         

    public override SomeResponseData GetResponseData()
    {
        // return data for unsuccessful result
    }
}
    
por 21.07.2014 / 08:19
fonte