Retornando um booleano quando o sucesso ou o fracasso é a única preocupação

15

Costumo encontrar-me retornando um booleano de um método, usado em vários locais, para conter toda a lógica em torno desse método em um único local. Todo o método de chamada (interno) precisa saber é se a operação foi bem-sucedida ou não.

Estou usando Python, mas a pergunta não é necessariamente específica para essa linguagem. Existem apenas duas opções em que posso pensar

  1. Crie uma exceção, embora as circunstâncias não sejam excepcionais, e lembre-se de capturar essa exceção em todos os lugares em que a função for chamada
  2. Retorne um booleano como estou fazendo.

Este é realmente um exemplo simples que demonstra do que estou falando.

import os

class DoSomething(object):

    def remove_file(self, filename):

        try:
            os.remove(filename)
        except OSError:
            return False

        return True

    def process_file(self, filename):

        do_something()

        if remove_file(filename):
            do_something_else()

Embora seja funcional, eu realmente não gosto dessa maneira de fazer algo, "cheira" e às vezes pode resultar em muitos ifs aninhados. Mas não consigo pensar em uma maneira mais simples.

Eu poderia recorrer a uma filosofia mais LBYL e usá-lo os.path.exists(filename)antes de tentar excluir, mas não há garantias de que o arquivo não será bloqueado nesse meio tempo (é improvável, mas possível) e ainda preciso determinar se a exclusão foi bem-sucedida ou não.

Esse é um design "aceitável" e, se não, qual seria a melhor maneira de projetar isso?

Ben
fonte

Respostas:

11

Você deve retornar booleanquando o método / função for útil para tomar decisões lógicas.

Você deve lançar um exceptionquando o método / função provavelmente não será usado em decisões lógicas.

Você precisa tomar uma decisão sobre a importância da falha e se ela deve ser tratada ou não. Se você puder classificar a falha como um aviso, retorne boolean. Se o objeto entrar em um estado ruim que tornará instáveis ​​as chamadas futuras, jogue um exception.

Outra prática é retornar em objectsvez de um resultado. Se você ligar open, ele deve retornar um Fileobjeto ou nullse não conseguir abrir. Isso garante que os programadores tenham uma instância de objeto em um estado válido que possa ser usado.

EDITAR:

Lembre-se de que a maioria dos idiomas descartará o resultado de uma função quando seu tipo for booleano ou inteiro. Portanto, é possível chamar a função quando não houver atribuição à esquerda para o resultado. Ao trabalhar com resultados booleanos, sempre assuma que o programador está ignorando o valor retornado e use-o para decidir se deve ser uma exceção.

Reactgular
fonte
É uma validação do que estou fazendo, então gosto da resposta :-). Em objetos, embora eu entenda de onde você é, não vejo como isso ajuda na maioria dos casos em que eu o usaria. Eu quero ficar seco, então vou ajustar novamente o objeto para um único método, pois só quero fazer uma coisa. Sou deixado com o mesmo código que tenho agora, salve com um método adicional. (também para o exemplo dado, estou excluindo o arquivo, para que um objeto de arquivo nulo não diga muito :-) #
Ben Ben
A exclusão é complicada, porque não é garantida. Eu nunca vi um método de exclusão de arquivo lançar uma exceção, mas o que o programador pode fazer se falhar? Repetir continuamente o loop? Não, é um problema de sistema operacional. O código deve registrar o resultado e seguir em frente.
Reactgular
4

Sua intuição está correta, existe uma maneira melhor de fazer isso: mônadas .

O que são mônadas?

Mônadas são (para parafrasear a Wikipedia) uma maneira de encadear operações enquanto ocultam o mecanismo de encadeamento; no seu caso, o mecanismo de encadeamento é o ifs aninhado . Esconda isso e seu código terá um cheiro muito melhor.

Existem algumas mônadas que farão exatamente isso ("Talvez" e "Qualquer uma") e, para sua sorte, elas fazem parte de uma biblioteca de mônadas python realmente agradável!

O que eles podem fazer pelo seu código

Aqui está um exemplo usando a mônada "Qualquer um" ("Disponível" na biblioteca vinculada a), onde uma função pode retornar um Sucesso ou Falha, dependendo do que ocorreu:

import os

class DoSomething(object):

    def remove_file(self, filename):
        try:
            os.remove(filename)
            return Success(None)
        except OSError:
            return Failure("There was an OS Error.")

    @do(Failable)
    def process_file(self, filename):
        do_something()
        yield remove_file(filename)
        do_something_else()
        mreturn(Success("All ok."))

Agora, isso pode não parecer muito diferente do que você tem agora, mas considere como seriam as coisas se você tivesse mais operações que poderiam resultar em uma falha:

    def action_that_might_fail_and_returns_something(self):
        # get some random value between 0 and 1 here
        if value < 0.5:
            return Success(value)
        else:
            return Failure("Bad value! Bad! Go to your room!")

    @do(Failable)
    def process_file(self, filename):
        do_something()
        yield remove_file(filename)
        yield action_that_might_fail(somearg)
        yield another_action_that_might_fail(someotherarg)
        some_val = yield action_that_might_fail_and_returns_something()
        yield something_that_used_the_return_value(some_val)
        do_something_else()
        mreturn(Success("All ok."))

Em cada um dos yields na process_filefunção, se a chamada da função retornar uma falha, a process_filefunção sairá, nesse ponto , retornando o valor de falha da função com falha, em vez de continuar com o restante e retornar o valorSuccess("All ok.")

Agora, imagine fazer o acima com ifs aninhados ! (Como você lidaria com o valor de retorno !?)

Conclusão

Mônadas são legais :)


Notas:

Eu não sou um programador Python - usei a biblioteca de mônada vinculada acima em um script que eu ninja para alguma automação de projeto. Entendo, no entanto, que, em geral, a abordagem idiomática preferida é usar exceções.

IIRC, há um erro de digitação no script lib na página vinculada, embora eu esqueça onde é o caixa eletrônico. Vou atualizar se me lembrar. Eu diferenciei minha versão da página e descobri: def failable_monad_examle():-> def failable_monad_example():- o pin exampleestava ausente.

Para obter o resultado de uma função decorada disponível (como process_file), você deve capturar o resultado em a variablee fazer a variable.valuepara obtê-lo.

Paulo
fonte
2

Uma função é um contrato, e seu nome deve sugerir que contrato ela cumprirá. IMHO, se você o nomear, remove_fileele deverá remover o arquivo e , caso contrário , causará uma exceção. Por outro lado, se você nomear try_remove_file, ele deve "tentar" remover e retornar booleano para saber se o arquivo foi removido ou não.

Isso levaria a outra pergunta - deveria ser remove_fileou try_remove_file? Depende do seu site de chamadas. Na verdade, você pode ter os dois métodos e usá-los em cenários diferentes, mas acho que a remoção do arquivo em si tem grandes chances de sucesso, então prefiro ter apenas remove_fileessa exceção de lançamento quando falha.

tia
fonte
0

Nesse caso específico, pode ser útil pensar sobre por que você talvez não consiga remover o arquivo. Digamos que o problema é que o arquivo pode ou não existir. Então você deve ter uma função doesFileExist()que retorne true ou false e uma função removeFile()que exclua o arquivo.

No seu código, você deve primeiro verificar se o arquivo existe. Se isso acontecer, ligue removeFile. Caso contrário, faça outras coisas.

Nesse caso, você ainda pode querer removeFilelançar uma exceção se o arquivo não puder ser removido por algum outro motivo, como permissões.

Para resumir, exceções devem ser lançadas para coisas que são, bem, excepcionais. Portanto, se é perfeitamente normal que o arquivo que você está tentando excluir possa não existir, isso não é uma exceção. Escreva um predicado booleano para verificar isso. Por outro lado, se você não tiver as permissões de gravação para o arquivo, ou se ele estiver em um sistema de arquivos remoto que de repente está inacessível, essas podem ser condições excepcionais.

Dima
fonte
Isso é muito específico ao exemplo que eu dei, que eu prefiro evitar. Ainda não escrevi isso, ele vai arquivar arquivos e registrar o fato de que isso aconteceu no banco de dados. Os arquivos podem ser recarregados a qualquer momento (embora os arquivos carregados tenham muito menos probabilidade de serem recarregados), é possível que um arquivo possa ser bloqueado por outro processo entre a verificação e a tentativa de exclusão. Não há nada de excepcional em um fracasso. É padrão o Python não se incomodar em verificar primeiro e capturar a exceção quando gerado (se necessário); simplesmente não quero fazer nada com isso dessa vez.
Ben
Se não houver nada de excepcional na falha, verificar se é possível remover um arquivo é uma parte legítima da lógica do seu programa. O princípio de responsabilidade única determina que você deve ter uma função de verificação e uma função removeFile.
Dima 17/05