Como edito uma cadeia de instruções if-else if para aderir aos princípios do Código Limpo do tio Bob?


45

Estou tentando seguir as sugestões de código limpo do tio Bob e especificamente para manter os métodos curtos.

Eu me acho incapaz de reduzir essa lógica:

if (checkCondition()) {addAlert(1);}
else if (checkCondition2()) {addAlert(2);}
else if (checkCondition3()) {addAlert(3);}
else if (checkCondition4()) {addAlert(4);}

Não consigo remover os elses e, assim, separar a coisa toda em bits menores, fazer com que o "else" no "else if" ajude o desempenho - avaliar essas condições é caro e se eu puder evitar avaliar as condições abaixo, faça com que uma das primeiras é verdade, eu quero evitá-los.

Mesmo semântica, avaliar a próxima condição se a anterior foi atendida não faz sentido do ponto de vista comercial.


editar: Esta questão foi identificada como uma possível duplicata de maneiras Elegant de lidar com se (se houver) outra coisa .

Eu acredito que essa é uma pergunta diferente (você pode ver isso também comparando as respostas dessas perguntas).


46
O que está realmente errado ou não está claro sobre esse código em seu contexto? Não vejo como isso pode ser reduzido ou simplificado! O código que avalia as condições já parece bem fatorado, como é o método chamado como resultado da decisão. Você só precisa olhar para algumas das respostas abaixo, que apenas complicam o código!
Steve Steve

38
Não há nada errado com este código. É muito legível e fácil de seguir. Qualquer coisa que você faça para reduzir ainda mais isso adicionará indireção e dificultará a compreensão.
17 de 26

20
Seu código está bom. Coloque sua energia restante em algo mais produtivo do que tentar reduzi-la ainda mais.
Robert Harvey

5
Se são realmente apenas 4 condições, tudo bem. Se é realmente algo como 12 ou 50, provavelmente você deseja refatorar em um nível mais alto que este método.
JimmyJames 12/01

9
Deixe seu código exatamente como está. Ouça o que seus pais sempre lhe disseram: não confie em nenhum tio que oferece doces para crianças na rua. @ Harvey Engraçado o bastante, as várias tentativas de "melhorar" o código o tornaram muito maior, mais complicado e menos legível.
precisa saber é o seguinte

Respostas:


81

Idealmente, acho que você deve extrair sua lógica para obter o código / número de alerta em seu próprio método. Portanto, seu código existente é reduzido completamente para

{
    addAlert(GetConditionCode());
}

e você GetConditionCode () encapsula a lógica para verificar as condições. Talvez também seja melhor usar um Enum do que um número mágico.

private AlertCode GetConditionCode() {
    if (CheckCondition1()) return AlertCode.OnFire;
    if (CheckCondition2()) return AlertCode.PlagueOfBees;
    if (CheckCondition3()) return AlertCode.Godzilla;
    if (CheckCondition4()) return AlertCode.ZombieSharkNado;
    return AlertCode.None;
}

2
Se possível encapsular como você descreve (suspeito que possa não ser, acho que o OP está deixando de fora variáveis ​​por simplicidade), ele não altera o código, o que por si só é bom, mas adiciona ergonomia de código e um pouco de legibilidade 1
opa

17
Com esses códigos de alerta, agradeço ao código que apenas um pode ser retornado por vez #
Josh Part

12
Essa também parece uma combinação perfeita para o uso de uma instrução switch - se disponível no idioma do OP.
22818 Frank Hopkins

4
Provavelmente, é apenas uma boa idéia extrair a obtenção do código de erro para um novo método, se isso puder ser escrito, para ser útil em várias situações, sem que seja necessário fornecer um monte de informações sobre a situação específica. Na verdade, há uma troca e um ponto de equilíbrio quando vale a pena. Mas, com frequência, você verá que a sequência de validações é específica para o trabalho em questão e é melhor mantê-los juntos com esse trabalho. Nesses casos, inventar um novo tipo para contar a outra parte do código o que precisa ser feito é um lastro indesejável.
PJTraill

6
Um problema com essa reimplementação é que ela faz com que a função addAlertprecise verificar a condição de alerta falso AlertCode.None.
David Hammen

69

A medida importante é a complexidade do código, não o tamanho absoluto. Supondo que as condições diferentes sejam realmente apenas chamadas de função única, assim como as ações não são mais complexas do que o que você mostrou, eu diria que não há nada errado com o código. Já é tão simples quanto pode ser.

Qualquer tentativa de "simplificar" ainda mais complicará as coisas.

Obviamente, você pode substituir a elsepalavra-chave por uma returncomo outros sugeriram, mas isso é apenas uma questão de estilo, não uma mudança na complexidade.


A parte, de lado:

Meu conselho geral seria: nunca ficar religioso sobre nenhuma regra de código limpo: a maioria dos conselhos de codificação que você vê na Internet é boa se aplicada em um contexto adequado, mas aplicar radicalmente esse mesmo conselho em todos os lugares pode lhe dar uma entrada no o IOCCC . O truque é sempre encontrar um equilíbrio que permita que os seres humanos raciocinem facilmente sobre seu código.

Use métodos muito grandes e você está ferrado. Use funções muito pequenas e você está ferrado. Evite expressões ternárias, e você está ferrado. Use expressões ternárias em todos os lugares e você está ferrado. Perceba que existem locais que exigem funções de uma linha e locais que exigem funções de 50 linhas (sim, eles existem!). Perceba que existem locais que solicitam if()declarações e que locais que solicitam o ?:operador. Use o arsenal completo à sua disposição e tente sempre usar a ferramenta mais adequada que você puder encontrar. E lembre-se, não fique religioso nem mesmo com esse conselho.


2
Eu diria que substituir else ifpor um interno returnseguido por um simples if(remover o else) pode dificultar a leitura do código . Quando o código diz else if, eu sei imediatamente que o código no próximo bloco só será executado se o anterior não. Sem confusão, sem confusão. Se é simples if, pode ser executado ou não, independentemente de o anterior ter sido executado. Agora terei que gastar um pouco de esforço mental para analisar o bloco anterior e observar que ele termina com a return. Prefiro gastar esse esforço mental analisando a lógica de negócios.
um CVn

1
Eu sei, é uma coisa pequena, mas pelo menos para mim, else ifforma uma unidade semântica. (Não é necessariamente uma única unidade para o compilador, mas tudo bem.) ...; return; } if (...Não; muito menos se estiver espalhado em várias linhas. Isso é algo que eu realmente terei que ver para ver o que está fazendo, em vez de ser capaz de entender diretamente, apenas vendo o par de palavras-chave else if.
um CVn

@ MichaelKjörling Full Ack. Eu mesmo preferiria a else ifconstrução, especialmente porque sua forma encadeada é um padrão tão conhecido. No entanto, o código do formulário if(...) return ...;também é um padrão conhecido, então eu não condenaria isso completamente. No entanto, vejo isso como um problema menor: a lógica do fluxo de controle é a mesma nos dois casos, e um único olhar mais atento a uma if(...) { ...; return; }escada me diz que é realmente equivalente a uma else ifescada. Vejo a estrutura de um único termo, deduzo seu significado, percebo que ele se repete em todos os lugares e sei o que se passa.
precisa saber é

Vindo de JavaScript / node.js, alguns usariam o código "cinto e suspensórios" para usar ambos else if e return . por exemploelse if(...) { return alert();}
user949300 15/0118

1
"E lembre-se, não fique religioso nem mesmo com esse conselho." +1
Palavras como Jared

22

É controverso se isso é 'melhor' do que o normal se ... ou seja, para qualquer caso. Mas se você quiser tentar outra coisa, essa é uma maneira comum de fazê-lo.

Coloque suas condições em objetos e coloque esses objetos em uma lista

foreach(var condition in Conditions.OrderBy(i=>i.OrderToRunIn))
{
    if(condition.EvaluatesToTrue())
    {
        addAlert(condition.Alert);
        break;
    }
}

Se várias ações forem necessárias, com a condição de que você possa fazer alguma recursão maluca

void RunConditionalAction(ConditionalActionSet conditions)
{
    foreach(var condition in conditions.OrderBy(i=>i.OrderToRunIn))
    {
        if(condition.EvaluatesToTrue())
        {
            RunConditionalAction(condition);
            break;
        }
    }
}

Obviamente sim. Isso só funciona se você tiver um padrão para sua lógica. Se você tentar fazer uma ação condicional recursiva super genérica, a configuração do objeto será tão complicada quanto a instrução if original. Você estará inventando sua própria nova linguagem / estrutura.

Mas o seu exemplo não têm um padrão

Um caso de uso comum para esse padrão seria a validação. Ao invés de :

bool IsValid()
{
    if(condition1 == false)
    {
        throw new ValidationException("condition1 is wrong!");
    }
    elseif(condition2 == false)
    {
    ....

}

Torna-se

[MustHaveCondition1]
[MustHaveCondition2]
public myObject()
{
    [MustMatchRegExCondition("xyz")]
    public string myProperty {get;set;}
    public bool IsValid()
    {
        conditions = getConditionsFromReflection()
        //loop through conditions
    }
}

27
Isso apenas move a if...elseescada para a construção da Conditionslista. O ganho líquido é negativo, pois a construção de Conditionslevará tanto código quanto o código OP, mas o indireto adicionado vem com um custo de legibilidade. Eu definitivamente preferiria uma escada com código limpo.
cmaster

3
@mastermaster sim, eu acho que disse exatamente isso ", então a configuração do objeto será tão complicada quanto a declaração if original
Ewan

7
Isso é menos legível que o original. Para descobrir qual condição está realmente sendo verificada, você precisa pesquisar em alguma outra área do código. Ele adiciona um nível desnecessário de indireção que torna o código mais difícil de entender.
17 de 26

8
Converter uma cadeia if .. else if .. else .. para uma tabela de predicados e ações faz sentido, mas apenas para exemplos muito maiores. A tabela adiciona alguma complexidade e indireção, portanto, você precisa de entradas suficientes para amortizar essa sobrecarga conceitual. Portanto, para 4 pares de predicado / ação, mantenha o código original simples, mas se você tivesse 100, definitivamente vá com a tabela. O ponto de cruzamento está em algum lugar no meio. @cmaster, a tabela pode ser inicializada estaticamente; portanto, a sobrecarga incremental para adicionar um par de predicado / ação é uma linha que apenas os nomeia: difícil de fazer melhor.
Stephen C. Steel

2
A legibilidade NÃO é pessoal. É um dever para o público da programação. É subjetivo. É exatamente por isso que é importante chegar a lugares como esse e ouvir o que o público de programação tem a dizer sobre isso. Pessoalmente, acho este exemplo incompleto. Mostre-me como conditionsé construído ... ARG! Não há atributos de anotação! Porque Deus? Ow meus olhos!
candied_orange

7

Considere usar return;depois que uma condição for bem-sucedida, ela economiza todos os elses. Você pode até conseguir return addAlert(1)diretamente se esse método tiver um valor de retorno.


3
Obviamente, isso pressupõe que nada mais aconteça após a cadeia de ifs ... Essa pode ser uma suposição razoável e, novamente, pode não ser.
um CVn 14/01

5

Já vi construções como essa consideradas mais limpas às vezes:

switch(true) {
    case cond1(): 
        statement1; break;
    case cond2():
        statement2; break;
    case cond3():
        statement3; break;
    // .. etc
}

O ternário com espaçamento correto também pode ser uma alternativa interessante:

cond1() ? statement1 :
cond2() ? statement2 :
cond3() ? statement3 : (null);

Eu acho que você também pode tentar criar uma matriz com um par contendo condição e função e iterar sobre ela até que a primeira condição seja atendida - o que, como eu vejo, seria igual à primeira resposta de Ewan.


1
ternário é puro
Ewan

6
A depuração de @Ewan em um "ternário profundamente recursivo" quebrado pode ser uma dor desnecessária.
DFRI

5
isso parece pura na tela embora.
Ewan

Uhm, que idioma permite usar funções com caseetiquetas?
undercat

1
@undercat isso é um válido ECMAScript / JavaScript afaik
zworek

1

Como uma variante da resposta de @ Ewan, você pode criar uma cadeia (em vez de uma "lista simples") de condições como esta:

abstract class Condition {
  private static final  Condition LAST = new Condition(){
     public void alertOrPropagate(DisplayInterface display){
        // do nothing;
     }
  }
  private Condition next = Last;

  public Condition setNext(Condition next){
    this.next = next;
    return this; // fluent API
  }

  public void alertOrPropagate(DisplayInterface display){
     if(isConditionMeet()){
         display.alert(getMessage());
     } else {
       next.alertOrPropagate(display);
     }
  }
  protected abstract boolean isConditionMeet();
  protected abstract String getMessage();  
}

Dessa forma, você pode aplicar suas condições em uma ordem definida e a infraestrutura (a classe abstrata mostrada) ignora as verificações restantes depois que a primeira é atendida.

É aqui que é superior à abordagem da "lista simples", na qual é necessário implementar o "pulo" no loop que aplica as condições.

Você simplesmente configura a cadeia de condições:

Condition c1 = new Condition1().setNext(
  new Condition2().setNext(
   new Condition3()
 )
);

E comece a avaliação com uma chamada simples:

c1.alertOrPropagate(display);

Sim, isso é chamado o padrão de Cadeia de responsabilidade
Max

4
Não pretendo falar por mais ninguém, mas, embora o código da pergunta seja imediatamente legível e óbvio em seu comportamento, eu não consideraria isso óbvio quanto ao que ele faz.
um CVn

0

Primeiro de tudo, o código original não é terrível IMO. É bastante compreensível e não há nada inerentemente ruim nisso.

Então, se você não gostar, crie a idéia de @ Ewan de usar uma lista, mas remova seu foreach breakpadrão um tanto antinatural :

public class conditions
{
    private List<Condition> cList;
    private int position;

    public Condition Head
    {
        get { return cList[position];}
    }

    public bool Next()
    {
        return (position++ < cList.Count);
    }
}


while not conditions.head.check() {
  conditions.next()
}
conditions.head.alert()

Agora adapte isso no seu idioma de escolha, torne cada elemento da lista um objeto, uma tupla, o que for, e você será bom.

EDIT: parece que não está tão claro como pensei, então deixe-me explicar melhor. conditionsé uma lista ordenada de algum tipo; headé o elemento atual que está sendo investigado - no início, é o primeiro elemento da lista e, a cada vez que next()é chamado, passa a ser o seguinte; check()e alert()são os checkConditionX()e addAlert(X)do OP.


1
(Não diminuiu o voto, mas) Não posso seguir isso. O que é cabeça ?
Belle-Sophie

@ Belle Eu editei a resposta para explicar melhor. É a mesma ideia que a de Ewan, mas com um ao while notinvés de foreach break.
Nico

A brilhante evolução de uma idéia brilhante
Ewan

0

A questão carece de alguns detalhes. Se as condições forem:

  • sujeito a alterações ou
  • repetido em outras partes do aplicativo ou sistema ou
  • modificado em certos casos (como diferentes compilações, testes, implantações)

ou se o conteúdo addAlertfor mais complicado, uma solução possivelmente melhor, digamos, c # seria:

//in some central spot
IEnumerable<Tuple<Func<bool>, int>> Conditions = new ... {
  Tuple.Create(CheckCondition1, 1),
  Tuple.Create(CheckCondition2, 2),
  ...
}

//at the original place
var matchingCondition = Conditions.Where(c=>c.Item1()).FirstOrDefault();
if(matchingCondition != null) 
  addAlert(matchingCondition.Item2)

As tuplas não são tão bonitas em c # <8, mas são escolhidas por conveniência.

Os profissionais desse método, mesmo que nenhuma das opções acima se aplique, é que a estrutura é digitada estaticamente. Você não pode errar acidentalmente, digamos, perdendo um else.


0

A melhor maneira de reduzir a complexidade ciclomática nos casos em que você tem muito if->then statementsé usar um dicionário ou lista (dependente do idioma) para armazenar o valor da chave (se o valor da instrução ou algum valor de) e, em seguida, o resultado do valor / função.

Por exemplo, em vez de (C #):

if (i > 10) { return "Two"; }
else if (i > 8) { return "Four" }
else if (i > 4) { return "Eight" }
return "Ten";  //etc etc say anything after 3 or 4 values

Eu posso simplesmente

var results = new Dictionary<int, string>
{
  { 10, "Two" },
  { 8, "Four"},
  { 4, "Eight"},
  { 0, "Ten"},
}

foreach(var key in results.Keys)
{
  if (i > results[key]) return results.Values[key];
}

Se você estiver usando linguagens mais modernas, poderá armazenar mais lógica, em seguida, simplesmente valores (c #). Na verdade, são apenas funções inline, mas você também pode apontar para outras funções também se a lógica for apenas colocar em linha.

var results = new Dictionary<Func<int, bool>, Func<int, string>>
{
  { (i) => return i > 10; ,
    (i) => return i.ToString() },
  // etc
};

foreach(var key in results.Keys)
{ 
  if (key(i)) return results.Values[key](i);
}

0

Estou tentando seguir as sugestões de código limpo do tio Bob e especificamente para manter os métodos curtos.

Eu me acho incapaz de reduzir essa lógica:

if (checkCondition()) {addAlert(1);}
else if (checkCondition2()) {addAlert(2);}
else if (checkCondition3()) {addAlert(3);}
else if (checkCondition4()) {addAlert(4);}

Seu código já é muito curto, mas a lógica em si não deve ser alterada. À primeira vista, parece que você está se repetindo com quatro chamadas para checkCondition(), e é aparente que cada uma delas é diferente depois de reler o código com cuidado. Você deve adicionar nomes de formatação e função adequados, por exemplo:

if (is_an_apple()) {
  addAlert(1);
}
else if (is_a_banana()) {
  addAlert(2);
}
else if (is_a_cat()) {
  addAlert(3);
}
else if (is_a_dog()) {
  addAlert(4);
}

Seu código deve ser legível acima de tudo. Depois de ler vários livros do tio Bob, acredito que essa é a mensagem que ele está constantemente tentando transmitir.


0

Supondo que todas as funções sejam implementadas no mesmo componente, você pode fazer com que as funções mantenham algum estado para se livrar das várias ramificações no fluxo.

EG: checkCondition1()se tornaria evaluateCondition1(), no qual verificaria se a condição anterior foi atendida; Nesse caso, ele armazena em cache algum valor a ser recuperado getConditionNumber().

checkCondition2()tornar-se-ia evaluateCondition2(), no qual verificaria se as condições anteriores foram cumpridas. Se a condição anterior não foi atendida, ele verifica o cenário de condição 2, armazenando em cache um valor a ser recuperado getConditionNumber(). E assim por diante.

clearConditions();
evaluateCondition1();
evaluateCondition2();
evaluateCondition3();
evaluateCondition4();
if (anyCondition()) { addAlert(getConditionNumber()); }

EDITAR:

Veja como a verificação de condições caras precisaria ser implementada para que essa abordagem funcionasse.

bool evaluateCondition34() {
    if (!anyCondition() && A && B && C) {
        conditionNumber = 5693;
        return true;
    }
    return false;
}

...

bool evaluateCondition76() {
    if (!anyCondition() && !B && C && D) {
        conditionNumber = 7658;
        return true;
    }
    return false;
}

Portanto, se você tiver muitas verificações caras a serem executadas e o conteúdo deste código permanecer privado, essa abordagem ajudará a mantê-la, permitindo alterar a ordem das verificações, se necessário.

clearConditions();
evaluateCondition10();
evaluateCondition9();
evaluateCondition8();
evaluateCondition7();
...
evaluateCondition34();
...
evaluateCondition76();

if (anyCondition()) { addAlert(getConditionNumber()); }

Essa resposta fornece apenas algumas sugestões alternativas das outras respostas e provavelmente não será melhor que o código original se considerarmos apenas 4 linhas de código. Embora essa não seja uma abordagem terrível (e nem torne a manutenção mais difícil, como já foi dito), devido ao cenário que mencionei (muitas verificações, apenas a principal função exposta como pública, todas as funções são detalhes de implementação da mesma classe).


Não gosto dessa sugestão - ela oculta a lógica de teste dentro de várias funções. Isso pode dificultar a manutenção do código se, por exemplo, você precisar alterar a ordem e executar o item 3 antes do item 2.
Lawrence Lawrence

Não. Você pode verificar se alguma condição anterior foi avaliada se anyCondition() != false.
Emerson Cardoso

1
Ok, eu vejo o que você está chegando. No entanto, se (digamos) as condições 2 e 3 forem avaliadas true, o OP não deseja que a condição 3 seja avaliada.
Lawrence em Lawrence

O que eu quis dizer é que você pode verificar as anyCondition() != falsefunções evaluateConditionXX(). Isso é possível de implementar. Se a abordagem do uso do estado interno não for desejada, eu entendo, mas o argumento de que isso não funciona não é válido.
Emerson Cardoso

1
Sim, minha objeção é que oculta a lógica de teste, sem ajuda, e que não pode funcionar. Na sua resposta (parágrafo 3), a verificação da condição 1 é colocada dentro de cada avaliação ... 2 (). Mas se ele alternar as condições 1 e 2 no nível superior (devido a alterações nos requisitos do cliente, etc.), você terá que entrar em avaliação ... 2 () para remover a verificação da condição 1, além de avaliar. ..1 () para adicionar uma verificação da condição 2. Isso pode ser feito para funcionar, mas pode facilmente levar a problemas com a manutenção.
Lawrence

0

Mais de duas cláusulas "else" obrigam o leitor do código a percorrer toda a cadeia para encontrar a que interessa. Use um método como: void AlertUponCondition (Condição de condição) {switch (condition) {case Condition.Con1: ... break; case Condition.Con2: ... break; etc ...} Onde "Condição" é uma enumeração adequada. Se necessário, retorne um valor ou bool. Chame assim: AlertOnCondition (GetCondition ());

Realmente não pode ser mais simples, E é mais rápido que a cadeia if-else, quando você excede alguns casos.


0

Não posso falar pela sua situação em particular porque o código não é específico, mas ...

código como esse geralmente é um cheiro para um modelo OO ausente. Você realmente tem quatro tipos de coisas, cada uma associada ao seu próprio tipo de alerta, mas, em vez de reconhecer essas entidades e criar uma instância de classe para cada uma, você as trata como uma coisa e tenta compensá-las mais tarde, no momento em que realmente Você precisa saber com o que está lidando para prosseguir.

O polimorfismo pode ter sido melhor para você.

Suspeite de código com métodos longos que contêm construções if-then longas ou complexas. Você geralmente deseja uma árvore de classes com alguns métodos virtuais.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.