¿Cómo edito una cadena de declaraciones if-else if para cumplir con los principios del Código Limpio del Tío Bob?


45

Estoy tratando de seguir las sugerencias de código limpio del tío Bob y específicamente para mantener los métodos cortos.

Sin embargo, no puedo acortar esta lógica:

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

No puedo eliminar los elses y así separar todo en pedazos más pequeños, porque el "else" en el "else if" ayuda al rendimiento: evaluar esas condiciones es costoso y si puedo evitar evaluar las siguientes condiciones, causa una de las primeras Es cierto, quiero evitarlos.

Incluso hablando semánticamente, evaluar la siguiente condición si se cumplió la anterior no tiene sentido desde el punto de vista comercial.


editar: esta pregunta se identificó como un posible duplicado de formas elegantes de manejar if (if else) else .

Creo que esta es una pregunta diferente (puedes ver eso también al comparar las respuestas de esas preguntas).


46
¿Qué tiene de malo o confuso este código en su contexto? ¡No puedo ver cómo se puede acortar o simplificar! El código que evalúa las condiciones ya aparece bien factorizado, como es el método que se llama como resultado de la decisión. ¡Solo tiene que mirar algunas de las respuestas a continuación, que simplemente complican el código!
Steve

38
No hay nada malo con este código. Es muy legible y fácil de seguir. Todo lo que hagas para reducirlo aún más agregará indirección y hará que sea más difícil de entender.
17 de 26

20
Tu código está bien. Ponga su energía restante en algo más productivo que tratar de acortarla aún más.
Robert Harvey

55
Si en realidad son solo 4 condiciones, está bien. Si realmente es algo así como 12 o 50, entonces probablemente desee refactorizar a un nivel más alto que este método.
JimmyJames

99
Deja tu código exactamente como está. Escucha lo que tus padres siempre te dijeron: no confíes en ningún tío que ofrezca dulces a los niños en la calle. @Harvey Curiosamente, los diversos intentos de "mejorar" el código lo hicieron mucho más grande, más complicado y menos legible.
gnasher729

Respuestas:


81

Idealmente, creo que debería extraer su lógica para obtener el código / número de alerta en su propio método. Por lo tanto, su código existente se reduce hasta llegar a

{
    addAlert(GetConditionCode());
}

y tiene GetConditionCode () encapsula la lógica para verificar las condiciones. Quizás también sea mejor usar una enumeración que un 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
Si es posible encapsular como usted describe (sospecho que podría no serlo, creo que OP está omitiendo variables por simplicidad), no cambia el código, lo que en sí mismo está bien, pero agrega ergonomía al código y un poco de legibilidad. 1
opa

17
Con esos códigos de alerta, agradezco que solo se pueda devolver uno a la vez
Josh Part

12
Esto también parece una combinación perfecta para el uso de una declaración de cambio, si está disponible en el lenguaje de OP.
Frank Hopkins el

44
Probablemente sea una buena idea extraer obtener el código de error a un nuevo método si se puede escribir para que sea útil en múltiples situaciones sin tener que dar un montón de información sobre la situación particular. En realidad, hay una compensación y un punto de equilibrio cuando vale la pena. Pero a menudo verá que la secuencia de validaciones es específica para el trabajo en cuestión, y es mejor mantenerlas juntas con ese trabajo. En tales casos, inventar un nuevo tipo para decirle a otra parte del código lo que hay que hacer es un lastre indeseable.
PJTraill

66
Un problema con esta reimplementación es que hace que la función addAlertnecesite verificar la condición de alerta falsa AlertCode.None.
David Hammen

69

La medida importante es la complejidad del código, no el tamaño absoluto. Suponiendo que las diferentes condiciones son realmente solo llamadas de función única, al igual que las acciones no son más complejas de lo que ha mostrado, diría que el código no tiene nada de malo. Ya es tan simple como puede ser.

Cualquier intento de "simplificar" aún más complicará las cosas.

Por supuesto, puede reemplazar la elsepalabra clave con un returncomo otros han sugerido, pero eso es solo una cuestión de estilo, no un cambio en la complejidad.


Aparte:

Mi consejo general sería, nunca volverse religioso sobre ninguna regla para el código limpio: la mayoría de los consejos de codificación que ve en Internet son buenos si se aplican en un contexto adecuado, pero aplicar radicalmente ese mismo consejo en todas partes puede ganarle una entrada en El IOCCC . El truco siempre es lograr un equilibrio que permita a los seres humanos razonar fácilmente sobre su código.

Usa métodos demasiado grandes, y estás jodido. Usa funciones demasiado pequeñas y estás jodido. Evita las expresiones ternarias y estás jodido. Usa expresiones ternarias en todas partes, y estás jodido. Tenga en cuenta que hay lugares que requieren funciones de una línea y lugares que requieren funciones de 50 líneas (¡sí, existen!). Tenga en cuenta que hay lugares que requieren if()declaraciones, y que hay lugares que requieren el ?:operador. Use el arsenal completo que está a su disposición e intente usar siempre la herramienta más adecuada que pueda encontrar. Y recuerde, no se vuelva religioso incluso sobre este consejo también.


2
Yo diría que reemplazarlo else ifcon un interno returnseguido de un simple if(eliminar el else) podría hacer que el código sea más difícil de leer . Cuando el código dice else if, inmediatamente sé que el código en el siguiente bloque solo se ejecutará si el anterior no lo hizo. Sin despeinarse sin problemas. Si es simple, ifentonces podría ejecutarse o no, independientemente de si el anterior se ejecutó. Ahora tendré que gastar un poco de esfuerzo mental para analizar el bloque anterior para notar que termina con a return. Prefiero gastar ese esfuerzo mental en analizar la lógica empresarial.
un CVn el

1
Lo sé, es algo pequeño, pero al menos para mí, else ifforma una unidad semántica. (No es necesariamente una sola unidad para el compilador, pero está bien) ...; return; } if (.... mucho menos si se extiende en varias líneas. Eso es algo que realmente tendré que mirar para ver qué está haciendo, en lugar de poder asimilarlo directamente con solo ver el par de palabras clave else if.
un CVn el

@ MichaelKjörling Full Ack. Yo mismo preferiría la else ifconstrucción, especialmente porque su forma encadenada es un patrón tan conocido. Sin embargo, el código del formulario if(...) return ...;también es un patrón bien conocido, por lo que no lo condenaría por completo. Sin embargo, veo esto realmente como un problema menor: la lógica de flujo de control es la misma en ambos casos, y una simple mirada más cercana a una if(...) { ...; return; }escalera me dirá que de hecho es equivalente a una else ifescalera. Veo la estructura de un solo término, infiero su significado, me doy cuenta de que se repite en todas partes y sé qué pasa.
cmaster

Viniendo de JavaScript / node.js, algunos usarían el código de "cinturón y tirantes" de usar ambos else if y return . por ejemploelse if(...) { return alert();}
user949300 el

1
"Y recuerda, no te vuelvas religioso ni siquiera sobre este consejo". +1
Palabras como Jared

22

Es controvertido si esto es 'mejor' que el simple si ... para cualquier caso dado. Pero si quieres probar algo más, esta es una forma común de hacerlo.

Pon tus condiciones en objetos y pon esos objetos en una lista

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

Si se requieren múltiples acciones con la condición, puede hacer una recursión loca

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

Obviamente, sí. Esto solo funciona si tiene un patrón para su lógica. Si intenta realizar una acción condicional recursiva súper genérica, la configuración del objeto será tan complicada como la instrucción if original. Inventarás tu propio nuevo lenguaje / marco.

Pero su ejemplo hace tener un patrón

Un caso de uso común para este patrón sería la validación. En vez de :

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

}

Se convierte

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

27
Esto solo mueve la if...elseescalera a la construcción de la Conditionslista. La ganancia neta es negativa, ya que la construcción de Conditionstomará tanto código como el código OP, pero la indirección adicional conlleva un costo de legibilidad. Definitivamente preferiría una escalera codificada limpia.
cmaster

3
@cmaster sí, creo que dije exactamente eso ", entonces la configuración del objeto será tan complicada como la declaración if original"
Ewan

77
Esto es menos legible que el original. Para averiguar qué condición se está verificando realmente, debe ir a cavar en otra área del código. Agrega un nivel innecesario de indirección que hace que el código sea más difícil de entender.
17 de 26

8
Convertir una cadena if .. else if .. else .. en una tabla de predicados y acciones tiene sentido, pero solo para ejemplos mucho más grandes. La tabla agrega cierta complejidad e indirección, por lo que necesita suficientes entradas para amortizar esta sobrecarga conceptual. Entonces, para 4 pares predicado / acción, mantenga el código original simple, pero si tenía 100, definitivamente vaya con la tabla. El punto de cruce está en algún punto intermedio. @cmaster, la tabla se puede inicializar estáticamente, por lo que la sobrecarga incremental para agregar un par predicado / acción es una línea que simplemente los nombra: difícil de mejorar.
Stephen C. Steel el

2
La legibilidad NO es personal. Es un deber para el público de programación. Es subjetivo. Por eso es importante venir a lugares como este y escuchar lo que el público de programación tiene que decir al respecto. Personalmente, encuentro este ejemplo incompleto. Muéstrame cómo conditionsse construye ... ARG! ¡No atributos de anotación! ¿Por que Dios? Ow mis ojos!
candied_orange

7

Considere usar return;después de que una condición haya tenido éxito, le ahorra todos los elses. Incluso podría hacerlo return addAlert(1)directamente si ese método tiene un valor de retorno.


3
Por supuesto, esto supone que no sucede nada más después de la cadena de ifs ... Eso podría ser una suposición razonable, y de nuevo podría no serlo.
un CVn el

5

He visto construcciones como esta consideradas más limpias a veces:

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

Ternario con espacio correcto también puede ser una buena alternativa:

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

Supongo que también podría intentar crear una matriz con un par que contenga condición y función e iterar sobre ella hasta que se cumpla la primera condición, lo que, como veo, sería igual a la primera respuesta de Ewan.


1
ternary está ordenado
Ewan

66
@Ewan depurar un "ternario profundamente recursivo" roto puede ser un dolor innecesario.
dfri

55
Sin embargo, se ve bien en la pantalla.
Ewan

¿Qué idioma permite usar funciones con caseetiquetas?
Undercat el

1
@undercat que es un afaik ECMAScript / JavaScript válido
zworek

1

Como una variante de la respuesta de @ Ewan, podría crear una cadena (en lugar de una "lista plana") de condiciones 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();  
}

De esta forma, puede aplicar sus condiciones en un orden definido y la infraestructura (se muestra la clase abstracta) omite las comprobaciones restantes después de que se haya cumplido el primero.

Aquí es donde es superior al enfoque de "lista plana", donde debe implementar el "salto" en el bucle que aplica las condiciones.

Simplemente configura la cadena de condición:

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

Y comience la evaluación con una simple llamada:

c1.alertOrPropagate(display);

Sí, eso se llama el Patrón de
Max

44
No pretendo hablar por nadie más, pero aunque el código en la pregunta es inmediatamente legible y obvio en su comportamiento, no consideraría que esto sea inmediatamente obvio en cuanto a lo que hace.
un CVn

0

En primer lugar, el código original no es terrible IMO. Es bastante comprensible y no hay nada inherentemente malo en ello.

Luego, si no le gusta, desarrolle la idea de @ Ewan de usar una lista pero elimine su foreach breakpatrón poco natural :

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()

Ahora adapte esto en su idioma de elección, haga que cada elemento de la lista sea un objeto, una tupla, lo que sea, y estará bien.

EDITAR: parece que no está tan claro, pensé, así que déjame explicarte más. conditionses una lista ordenada de algún tipo; heades el elemento actual que se está investigando: al principio es el primer elemento de la lista, y cada vez que next()se llama se convierte en el siguiente; check()y alert()son los checkConditionX()y addAlert(X)desde el OP.


1
(No voté en contra pero) No puedo seguir esto. ¿Qué es la cabeza ?
Belle-Sophie

@Belle Edité la respuesta para explicar más. Es la misma idea que la de Ewan pero con un en while notlugar de foreach break.
Nico

Una evolución brillante de una idea brillante
Ewan

0

La pregunta carece de algunos detalles. Si las condiciones son:

  • sujeto a cambios o
  • repetido en otras partes de la aplicación o sistema o
  • modificado en ciertos casos (como diferentes compilaciones, pruebas, implementaciones)

o si el contenido en addAlertes más complicado, entonces una solución posiblemente mejor en digamos c # sería:

//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)

Las tuplas no son tan bellas en c # <8, sino que se eligen por conveniencia.

Las ventajas de este método, incluso si ninguna de las opciones anteriores se aplica, es que la estructura está estáticamente tipada. No puedes equivocarte accidentalmente por, digamos, perder un else.


0

La mejor manera de reducir la complejidad ciclomática en los casos en que tiene mucho if->then statementses usar un diccionario o una lista (depende del idioma) para almacenar el valor clave (si es un valor de declaración o algún valor de) y luego un resultado de valor / función.

Por ejemplo, en lugar 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

Puedo simplemente

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];
}

Si está utilizando lenguajes más modernos, puede almacenar más lógica que simplemente valores también (c #). Esto es realmente solo funciones en línea, pero también puede señalar otras funciones también si la lógica es poner en línea.

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

Estoy tratando de seguir las sugerencias de código limpio del tío Bob y específicamente para mantener los métodos cortos.

Sin embargo, no puedo acortar esta lógica:

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

Su código ya es demasiado corto, pero la lógica en sí no debe cambiarse. A primera vista, parece que te estás repitiendo con cuatro llamadas checkCondition(), y solo es evidente que cada una es diferente después de volver a leer el código cuidadosamente. Debe agregar el formato y los nombres de función adecuados, por ejemplo:

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);
}

Su código debe ser legible por encima de todo. Después de leer varios de los libros del tío Bob, creo que ese es el mensaje que constantemente intenta transmitir.


0

Suponiendo que todas las funciones se implementen en el mismo componente, podría hacer que las funciones conserven algún estado para deshacerse de las múltiples ramas en el flujo.

EG: checkCondition1()se convertiría evaluateCondition1(), en el cual comprobaría si se cumplían las condiciones anteriores; si es así, almacena en caché algún valor para ser recuperado getConditionNumber().

checkCondition2()se convertiría evaluateCondition2(), en el que comprobaría si se cumplían las condiciones anteriores. Si no se cumplió la condición anterior, verifica el escenario de condición 2, almacenando en caché un valor para ser recuperado getConditionNumber(). Y así.

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

EDITAR:

Así es como debería implementarse la verificación de condiciones costosas para que este enfoque funcione.

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;
}

Por lo tanto, si tiene demasiadas verificaciones costosas para realizar y las cosas en este código permanecen privadas, este enfoque ayuda a mantenerlo, permitiendo cambiar el orden de las verificaciones si es necesario.

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

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

Esta respuesta solo proporciona algunas sugerencias alternativas de las otras respuestas, y probablemente no será mejor que el código original si consideramos solo 4 líneas de código. Aunque, este no es un enfoque terrible (y tampoco hace que el mantenimiento sea más difícil como lo han dicho otros) dado el escenario que mencioné (demasiadas verificaciones, solo la función principal expuesta como pública, todas las funciones son detalles de implementación de la misma clase).


No me gusta esta sugerencia: oculta la lógica de prueba dentro de múltiples funciones. Esto puede dificultar el mantenimiento del código si, por ejemplo, necesita cambiar el orden y hacer el # 3 antes del # 2.
Lawrence

No. Puede verificar si alguna condición previa fue evaluada si anyCondition() != false.
Emerson Cardoso

1
Ok, veo a lo que te refieres. Sin embargo, si (por ejemplo) las condiciones 2 y 3 se evalúan true, el OP no quiere evaluar la condición 3.
Lawrence

Lo que quise decir es que puedes verificar anyCondition() != falsedentro de las funciones evaluateConditionXX(). Esto es posible de implementar. Si no se desea utilizar el estado interno, entiendo, pero el argumento de que esto no funciona no es válido.
Emerson Cardoso

1
Sí, mi objeción es que oculta inútilmente la lógica de prueba, no es que no pueda funcionar. En su respuesta (párrafo 3), la verificación de la condición de reunión 1 se coloca dentro de eval ... 2 (). Pero si cambia las condiciones 1 y 2 en el nivel superior (debido a cambios en los requisitos del cliente, etc.), tendría que ingresar a eval ... 2 () para eliminar la verificación de la condición 1, además de ingresar a eval. ..1 () para agregar una verificación para la condición 2. Esto puede hacerse funcionar, pero puede conducir fácilmente a problemas de mantenimiento.
Lawrence

0

Más de dos cláusulas "más" obligan al lector del código a recorrer toda la cadena para encontrar el que le interesa. Utilice un método como: void AlertUponCondition (condición condición) {switch (condición) {case Condition.Con1: ... break; Condición del caso. Con2: ... descanso; etc ...} Donde "Condición" es una enumeración adecuada. Si es necesario, devuelva un valor bool o. Llámalo así: AlertOnCondition (GetCondition ());

Realmente no puede ser más simple, Y es más rápido que la cadena if-else una vez que excede algunos casos.


0

No puedo hablar por su situación particular porque el código no es específico, pero ...

Un código como ese es a menudo un olor para un modelo OO carente. Realmente tiene cuatro tipos de cosas, cada una asociada con su propio tipo de alerta, pero en lugar de reconocer estas entidades y crear una instancia de clase para cada una, las trata como una cosa y trata de compensarlas más tarde, en un momento en que realmente necesita saber con qué está tratando para continuar.

El polimorfismo puede haber sido mejor para ti.

Sospeche del código con métodos largos que contienen construcciones largas o complejas de si-entonces. A menudo quieres un árbol de clases allí con algunos métodos virtuales.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.