Antipadrão for-if

9

Eu estava lendo nesta postagem do blog sobre o anti-padrão for-if, e não tenho muita certeza de entender por que é um anti-padrão.

foreach (string filename in Directory.GetFiles("."))
{
    if (filename.Equals("desktop.ini", StringComparison.OrdinalIgnoreCase))
    {
        return new StreamReader(filename);
    }
}

Questão 1:

É por return new StreamReader(filename);dentro for loop? ou o fato de você não precisar de um forloop neste caso?

Como o autor do blog apontou, a versão menos louca disso é:

if (File.Exists("desktop.ini"))
{
    return new StreamReader("desktop.ini");
} 

ambos sofrem de uma condição de corrida porque, se o arquivo for excluído antes da criação do StreamReader, você receberá um File­Not­Found­Exception.

Questão 2:

Para corrigir o segundo exemplo, você o reescreveria sem a instrução if e, em vez disso, o cercaria StreamReadercom um bloco try-catch, e se ele der um, File­Not­Found­Exceptionvocê o manipula no catchbloco de acordo.


fonte
11
consulte Discutir este $ {blog}
gnat
11
@ amon - Obrigado, mas não faço mais comentários, só estou tentando entender o que o artigo está dizendo e como eu o corrigia.
3
Eu não pensaria tanto nisso. O pôster do blog está criando um código idiota que ninguém escreve, dá um nome e chama de antipadrão. Aqui está outro: "Eu chamo isso de padrão de atribuição inútil: x = x; vejo isso sendo feito o tempo todo. Tão estúpido. Acho que vou escrever um blog sobre isso".
Martin Maat
4
To fix the second example, would you re-write it without the if statement, and instead surround the StreamReader with a try-catch block, and if it throws a File­Not­Found­Exception you handle it in the catch block accordingly?- Sim, é exatamente o que eu faria. Resolver a condição de corrida é mais importante do que alguma noção de "exceções como fluxo de controle" e a resolve de maneira elegante e limpa.
Robert Harvey
11
O que ele faz se nenhum dos arquivos atender aos critérios especificados? Isso volta null? Geralmente, você pode usar o LINQ para limpar um código parecido com o seu exemplo: return Directory.GetFiles(".").FirstOrDefault(fileName => fileName.Equals("desktop.ini", StringComparison.OrdinalIgnoreCase))?.Select(fileName => new StreamReader(filename)); Observe o ?.operador entre as duas chamadas do LINQ. Além disso, as pessoas podem argumentar que a criação de objetos como esse não é o uso mais apropriado do LINQ, mas acho que está bem aqui. Esta não é uma resposta para sua pergunta, mas discorda em uma parte dela.
Panzercrisis 18/05/19

Respostas:

7

Este é um antipadrão, pois assume a forma de:

loop over a set of values
   if current value meets a condition
       do something with value
   end
end

e pode ser substituído por

do something with value

Um exemplo clássico disso é um código como:

for (var i=0; i < 5; i++)
{
    switch (i)
        case 1:
            doSomethingWith(1);
            break;
        case 2:
            doSomethingWith(2);
            break;
        case 3:
            doSomethingWith(4);
            break;
        case 4:
            doSomethingWith(4);
            break;
    }
}

Quando o seguinte funciona bem:

doSomethingWith(1);
doSomethingWith(2);
doSomethingWith(3);
doSomethingWith(4);

Se você estiver executando um loop e um ifou switch, pare e pense no que está fazendo. Você está complicando demais as coisas e o loop e o teste completos podem ser substituídos por uma simples linha "just do it". Às vezes, porém, você encontrará que precisa fazer esse loop (mais de um item pode corresponder à única condição, por exemplo); nesse caso, o padrão está correto.

É por isso que é um antipadrão: ele pega o padrão "loop and test" e o abusa.

Em relação à sua segunda pergunta: sim. Um padrão "try do" é mais robusto que um padrão "test then do" em qualquer situação em que seu código não seja o único thread em todo o dispositivo que pode alterar o estado do item em teste.

O problema com este código:

if (File.Exists("desktop.ini"))
{
    return new StreamReader("desktop.ini");
}

é que, no período entre File.Existse StreamReadertentando abrir esse arquivo, outro encadeamento ou processo pode excluir o arquivo. Então você terá uma exceção. Portanto, essa exceção precisa ser protegida contra algo como:

try
{
    return new StreamReader("desktop.ini");
}
catch (File­Not­Found­Exception)
{
    return null; // or whatever
}

@Flater levanta um bom ponto? Isso em si é um antipadrão? Estamos usando exceções como fluxo de controle?

Se o código ler algo como:

try
{
    if (!File.Exists("desktop.ini")
    {
        throw new IniFileMissingException();
        return new StreamReader("desktop.ini");
    }
}
catch (IniFileMissingException)
{
    return null;
}

então eu de fato usaria exceções como um goto glorificado e seria de fato um antipadrão. Mas, neste caso, estamos apenas contornando o comportamento indesejável de criar um novo fluxo, portanto, este não é um exemplo desse antipadrão. Mas é um exemplo de como contornar esse antipadrão.

Obviamente, o que realmente queremos é uma maneira mais elegante de criar o fluxo. Algo como:

return TryCreateStream("desktop.ini", out var stream) ? stream : null;

E eu recomendo agrupar esse try catchcódigo em um método utilitário como este, se você estiver usando muito esse código.

David Arno
fonte
11
@ Flater, tendo a concordar que não é o ideal (embora eu discuta, é um exemplo do antipadrão que essa resposta fala). O código deve mostrar sua intenção, não a mecânica. Então, eu preferiria algo como o Scala Try, por exemplo return Try(() => new StreamReader("desktop.ini")).OrElse(null);, mas o C # não suporta essa construção (sem usar uma biblioteca de terceiros), por isso temos que trabalhar com a versão desajeitada.
David Arno
11
@ Flater, concordo plenamente que as exceções devem ser excepcionais. Mas eles raramente são. Por exemplo, um arquivo que não existe é dificilmente excepcional, mas File.Openlançará um se não conseguir encontrar o arquivo. Como já temos uma exceção excepcional sendo lançada, prendê-la como parte do fluxo de controle é uma necessidade (a menos que alguém queira simplesmente deixar o aplicativo travar).
David Arno
11
@ Flater: o segundo exemplo de código é o caminho certo para abrir o arquivo. Qualquer outra coisa introduz uma condição de corrida. Mesmo se você verificar a existência dos arquivos, entre essa verificação e quando você realmente a abrir, algo poderá tornar esse arquivo indisponível para você, e a chamada Open () explodirá. Então você precisa estar pronto para a exceção. Portanto, é melhor você não se incomodar em verificar e apenas tentar abri-lo. Exceções são ferramentas para nos ajudar a fazer as coisas e seu uso não deve ser visto como dogma religioso.
Whatsisname 18/05/19
11
@Flater: qualquer maneira de evitar operações de tentativa / captura de arquivos introduz condições de corrida. O sistema de arquivos no qual você deseja gravar pode ficar indisponível no instante antes de você chamar File.Create, invalidando qualquer verificação.
Whatsisname 18/05/19
11
@Flater " Você está argumentando efetivamente que toda chamada de método precisa de um tipo de retorno ... que está efetivamente tentando reinventar exceções de uma maneira diferente ". Corrigir. Embora essa reinvenção não seja minha. É usado em linguagens funcionais há anos. Eles geralmente evitam todas as "exceções como questão de fluxo de controle" (que você confunde ser um antipadrão em uma respiração e depois argumenta a favor da próxima) usando tipos de união, por exemplo, neste caso, usaríamos um Maybe<Stream>tipo que retorna nothingse o arquivo não existir e um fluxo, se existir.
David Arno
1

Pergunta 1: (este loop for um antipadrão)

Sim, porque você não precisa fazer sua própria pesquisa por itens armazenados em um subsistema consultável.

Em resumo, o sistema de arquivos, como um banco de dados, é capaz de responder a consultas. Ao interagir com um subsistema passível de consulta, temos uma escolha fundamental sobre se queremos que esse subsistema enumere seu conteúdo para nós, a fim de executar a correspondência fora do subsistema ou usar os recursos de consulta nativos do subsistema.

Vamos fingir por um momento que você está procurando um registro em um banco de dados em vez de um arquivo em um diretório em um sistema de arquivos. Você prefere ver

SELECT * FROM SomeTable;

e depois no loop (por exemplo, em C #) sobre o cursor retornado procurando ID = 100 ou deixando o subsistema consultável fazer o possível para encontrar exatamente o que você está procurando?

SELECT * FROM SomeTable WHERE ID = 100;

Eu acho que a maioria de nós escolheria corretamente deixar o subsistema executar a consulta exata de interesse. A alternativa envolve potencialmente numerosas viagens de ida e volta com o subsistema, teste de igualdade ineficiente e renúncia ao uso de quaisquer índices ou outros aceleradores de pesquisa, fornecidos por bancos de dados e sistemas de arquivos.


Pergunta 2: para corrigir o segundo exemplo, você o escreveria novamente sem a instrução if e, em vez disso, cercaria o StreamReader com um bloco try-catch e, se ele lançar uma FileNotFoundException, você o manipulará no bloco catch de acordo.

Sim, porque é assim que essa API específica funciona - não é realmente a nossa escolha, pois essa é uma função da biblioteca. A verificação if, antes da chamada, não oferece valor adicional: precisamos usar o try / catch de qualquer maneira, pois (1) outros erros além do FileNotFound podem ocorrer e (2) a condição de corrida.

Erik Eidt
fonte
0

Questão 1:

É por causa do retorno novo StreamReader (nome do arquivo); dentro do loop for? ou o fato de você não precisar de um loop for nesse caso?

O leitor de stream não tem nada a ver com isso. O anti-padrão surge devido a um claro conflito de intenção entre o foreache o if:

Qual é o propósito do foreach?

Suponho que sua resposta será algo como: "Quero executar repetidamente um determinado pedaço de código"

Quantos arquivos você espera processar?

Como você pode ter apenas um nome de arquivo específico (incluindo extensão) em uma pasta específica, isso prova que seu código pretende encontrar um arquivo aplicável.

Isso também é confirmado pelo fato de você estar retornando um valor imediatamente. Na verdade, você não se importa com uma segunda partida, mesmo que ela exista.


Há situações em que isso não é um anti-padrão.

  • Se você também procurar nos subdiretórios ( Directory.GetFiles(".", SearchOption.AllDirectories)), é possível encontrar mais de um arquivo com o mesmo nome de arquivo (incluindo extensão)
  • Se você procurar correspondências parciais de nome de arquivo (por exemplo, todo arquivo cujo nome começa com "Test_", ou todo "*.zip"arquivo.

Observe que esses dois casos exigiriam que você processasse várias correspondências e, portanto, não retornasse um valor imediatamente.


Questão 2:

Para corrigir o segundo exemplo, você o reescreveria sem a instrução if e, em vez disso, cercaria o StreamReader com um bloco try-catch e, se ele gerar uma FileNotFoundException, você o manipulará no bloco catch adequadamente.

Exceções são caras. Eles nunca devem ser usados ​​no lugar da lógica de fluxo adequada. Exceções são, como o nome sugere circunstâncias excepcionais .

Por esse motivo, você não deve remover o if.

Conforme esta resposta no SoftwareEngineering.SE :

Geralmente, o uso de exceções para o fluxo de controle é um antipadrão, com exceções notáveis ​​da tosse específica da situação e do idioma.

Como um resumo rápido do motivo, geralmente, é um antipadrão:

  • Exceções são, em essência, sofisticadas declarações GOTO
  • Programar com exceções, portanto, leva a mais difícil leitura e compreensão do código
  • A maioria dos idiomas possui estruturas de controle existentes projetadas para resolver seus problemas sem o uso de exceções
  • Os argumentos de eficiência tendem a ser discutíveis para os compiladores modernos, que tendem a otimizar com a suposição de que as exceções não são usadas para o fluxo de controle.

Leia a discussão no wiki de Ward para obter informações muito mais detalhadas.

Se você precisa agrupar isso em uma tentativa / captura, depende muito da sua situação:

  • Qual a probabilidade de você encontrar uma condição de corrida?
  • Você é realmente capaz de lidar com essa situação ou deseja que esse problema borbulhe para o usuário porque não sabe como lidar com isso?

Nada é sempre uma questão de "sempre usá-lo". Para provar meu argumento:

Estudos separados comprovaram que você tem menos chances de se machucar ao usar capacete, óculos de proteção e coletes à prova de balas.

Então, por que não estamos todos usando esse equipamento de segurança o tempo todo?

A resposta simples é que existem desvantagens em usá-las:

  • Custa dinheiro
  • Isso torna seu movimento mais pesado
  • Pode ser bastante quente para usá-lo.

Agora estamos chegando a algum lugar: existem prós e contras . Em outras palavras, só faz sentido usar este equipamento nos casos em que os profissionais superam os contras.

  • É muito mais provável que os trabalhadores da construção sofram ferimentos durante o trabalho. Eles se beneficiam de um capacete de segurança.
  • Os funcionários de escritório, por outro lado, têm uma chance muito menor de se machucar. Capacetes de segurança não valem a pena.
  • Um membro da equipe da SWAT tem muito mais chances de levar um tiro, comparado a um trabalhador de escritório.

Você deve encerrar a chamada em uma tentativa / captura? Isso depende muito se os benefícios de fazê-lo superam o custo de sua implementação.

Observe que outros podem argumentar que são necessárias apenas algumas teclas para envolvê-lo, portanto, obviamente, isso deve ser feito. Mas esse não é o argumento inteiro:

  • Você precisa decidir o que fazer depois de capturar a exceção.
  • Se houver muitas chamadas diferentes para arquivos diferentes em toda a base de código, a decisão de agrupar uma em uma tentativa / captura geralmente significa que você precisará agrupar todos esses casos. Isso pode ter um efeito dramático na quantidade de esforço necessária para implementá-lo.
  • É perfeitamente possível que você intencionalmente queira não lidar com uma exceção.
    • Observe que seu aplicativo precisará manipular a exceção em algum momento, mas isso não é necessariamente imediatamente após a exceção ter sido gerada.

Então a escolha é sua. Existe algum benefício em fazer isso? Você acha que isso melhora o aplicativo, mais do que o esforço para custear sua implementação?

Atualização - De um comentário que escrevi para a outra resposta, pois acho que é uma consideração relevante para você também:

Depende muito do contexto circundante.

  • Se a abertura do leitor de fluxo for precedida por if(!File.Exists) File.Create(), a ausência do arquivo ao abrir o leitor de fluxo é realmente excepcional .
  • Se o nome do arquivo foi selecionado em uma lista de arquivos existentes, sua repentina ausência é novamente excepcional .
  • Se você estiver trabalhando com uma sequência que ainda não testou no diretório; a ausência do arquivo é um resultado perfeitamente lógico e, portanto, não excepcional .
Flater
fonte