Cómo reemplazar muchas declaraciones if en Java

1. Información general

Las construcciones de decisiones son una parte vital de cualquier lenguaje de programación. Pero aterrizamos codificando una gran cantidad de declaraciones if anidadas que hacen que nuestro código sea más complejo y difícil de mantener.

En este tutorial, veremos las diversas formas de reemplazar declaraciones if anidadas .

Exploremos diferentes opciones sobre cómo podemos simplificar el código.

2. Estudio de caso

A menudo nos encontramos con una lógica empresarial que implica muchas condiciones y cada una de ellas necesita un procesamiento diferente. En aras de una demostración, tomemos el ejemplo de una clase Calculadora . Tendremos un método que toma dos números y un operador como entrada y devuelve el resultado basado en la operación:

public int calculate(int a, int b, String operator) { int result = Integer.MIN_VALUE; if ("add".equals(operator)) { result = a + b; } else if ("multiply".equals(operator)) { result = a * b; } else if ("divide".equals(operator)) { result = a / b; } else if ("subtract".equals(operator)) { result = a - b; } return result; }

También podemos implementar esto usando declaraciones de cambio :

public int calculateUsingSwitch(int a, int b, String operator) { switch (operator) { case "add": result = a + b; break; // other cases } return result; }

En el desarrollo típico, las declaraciones if pueden crecer mucho más y ser de naturaleza más compleja . Además, las declaraciones de cambio no encajan bien cuando hay condiciones complejas .

Otro efecto secundario de tener construcciones de decisión anidadas es que se vuelven inmanejables. Por ejemplo, si necesitamos agregar un nuevo operador, tenemos que agregar una nueva instrucción if e implementar la operación.

3. Refactorización

Exploremos las opciones alternativas para reemplazar las complejas declaraciones if anteriores en un código mucho más simple y manejable.

3.1. Clase de fábrica

Muchas veces nos encontramos con constructos de decisión que terminan haciendo una operación similar en cada rama. Esto brinda la oportunidad de extraer un método de fábrica que devuelve un objeto de un tipo determinado y realiza la operación en función del comportamiento del objeto concreto .

Para nuestro ejemplo, definamos una interfaz de operación que tenga un único método de aplicación :

public interface Operation { int apply(int a, int b); }

El método toma dos números como entrada y devuelve el resultado. Definamos una clase para realizar adiciones:

public class Addition implements Operation { @Override public int apply(int a, int b) { return a + b; } }

Ahora implementaremos una clase de fábrica que devuelve instancias de Operación basadas en el operador dado:

public class OperatorFactory { static Map operationMap = new HashMap(); static { operationMap.put("add", new Addition()); operationMap.put("divide", new Division()); // more operators } public static Optional getOperation(String operator) { return Optional.ofNullable(operationMap.get(operator)); } }

Ahora, en la clase Calculadora , podemos consultar la fábrica para obtener la operación relevante y aplicar en los números de origen:

public int calculateUsingFactory(int a, int b, String operator) { Operation targetOperation = OperatorFactory .getOperation(operator) .orElseThrow(() -> new IllegalArgumentException("Invalid Operator")); return targetOperation.apply(a, b); }

En este ejemplo, hemos visto cómo se delega la responsabilidad a objetos poco acoplados servidos por una clase de fábrica. Pero podría haber posibilidades de que las declaraciones if anidadas simplemente se transfieran a la clase de fábrica, lo que frustra nuestro propósito.

Alternativamente, podemos mantener un repositorio de objetos en un mapa que se podría consultar para una búsqueda rápida . Como hemos visto, OperatorFactory # operationMap sirve para nuestro propósito. También podemos inicializar Map en tiempo de ejecución y configurarlos para la búsqueda.

3.2. Uso de enumeraciones

Además del uso de Map, también podemos usar Enum para etiquetar una lógica empresarial particular . Después de eso, se puede utilizar ya sea en el anidado si las declaraciones o de caso interruptor de declaraciones . Alternativamente, también podemos usarlos como una fábrica de objetos y diseñar estrategias para realizar la lógica comercial relacionada.

Eso también reduciría el número de declaraciones if anidadas y delegaría la responsabilidad a valores individuales de Enum .

Veamos cómo podemos lograrlo. Al principio, necesitamos definir nuestro Enum :

public enum Operator { ADD, MULTIPLY, SUBTRACT, DIVIDE }

Como podemos observar, los valores son las etiquetas de los diferentes operadores que se utilizarán posteriormente para el cálculo. Siempre tenemos la opción de usar los valores como condiciones diferentes en declaraciones if anidadas o casos de cambio, pero diseñemos una forma alternativa de delegar la lógica a la propia Enum .

Definiremos métodos para cada uno de los valores de Enum y haremos el cálculo. Por ejemplo:

ADD { @Override public int apply(int a, int b) { return a + b; } }, // other operators public abstract int apply(int a, int b);

Y luego en la clase Calculadora , podemos definir un método para realizar la operación:

public int calculate(int a, int b, Operator operator) { return operator.apply(a, b); }

Ahora, podemos invocar el método mediante la conversión de la cadena de valor para el operador utilizando el operador # valueOf () método :

@Test public void whenCalculateUsingEnumOperator_thenReturnCorrectResult() { Calculator calculator = new Calculator(); int result = calculator.calculate(3, 4, Operator.valueOf("ADD")); assertEquals(7, result); }

3.3. Patrón de comando

En la discusión anterior, hemos visto el uso de la clase de fábrica para devolver la instancia del objeto comercial correcto para el operador dado. Posteriormente, el objeto comercial se utiliza para realizar el cálculo en la Calculadora .

También podemos diseñar un método de cálculo de Calculadora # para aceptar un comando que se puede ejecutar en las entradas . Esta será otra forma de reemplazar declaraciones if anidadas .

Primero definiremos nuestra interfaz de comandos :

public interface Command { Integer execute(); }

A continuación, implementemos un AddCommand:

public class AddCommand implements Command { // Instance variables public AddCommand(int a, int b) { this.a = a; this.b = b; } @Override public Integer execute() { return a + b; } }

Finalmente, introduzcamos un nuevo método en la Calculadora que acepta y ejecuta el comando :

public int calculate(Command command) { return command.execute(); }

Next, we can invoke the calculation by instantiating an AddCommand and send it to the Calculator#calculate method:

@Test public void whenCalculateUsingCommand_thenReturnCorrectResult() { Calculator calculator = new Calculator(); int result = calculator.calculate(new AddCommand(3, 7)); assertEquals(10, result); }

3.4. Rule Engine

When we end up writing a large number of nested if statements, each of the conditions depicts a business rule which has to be evaluated for the correct logic to be processed. A rule engine takes such complexity out of the main code. A RuleEngine evaluates the Rules and returns the result based on the input.

Let's walk through an example by designing a simple RuleEngine which processes an Expression through a set of Rules and returns the result from the selected Rule. First, we'll define a Rule interface:

public interface Rule { boolean evaluate(Expression expression); Result getResult(); }

Second, let's implement a RuleEngine:

public class RuleEngine { private static List rules = new ArrayList(); static { rules.add(new AddRule()); } public Result process(Expression expression) { Rule rule = rules .stream() .filter(r -> r.evaluate(expression)) .findFirst() .orElseThrow(() -> new IllegalArgumentException("Expression does not matches any Rule")); return rule.getResult(); } }

The RuleEngine accepts an Expression object and returns the Result. Now, let's design the Expression class as a group of two Integer objects with the Operator which will be applied:

public class Expression { private Integer x; private Integer y; private Operator operator; }

Y finalmente definamos una clase AddRule personalizada que se evalúa solo cuando se especifica la operación ADD :

public class AddRule implements Rule { @Override public boolean evaluate(Expression expression) { boolean evalResult = false; if (expression.getOperator() == Operator.ADD) { this.result = expression.getX() + expression.getY(); evalResult = true; } return evalResult; } }

Ahora invocaremos RuleEngine con una expresión :

@Test public void whenNumbersGivenToRuleEngine_thenReturnCorrectResult() { Expression expression = new Expression(5, 5, Operator.ADD); RuleEngine engine = new RuleEngine(); Result result = engine.process(expression); assertNotNull(result); assertEquals(10, result.getValue()); }

4. Conclusión

En este tutorial, exploramos varias opciones diferentes para simplificar el código complejo. También aprendimos cómo reemplazar declaraciones if anidadas mediante el uso de patrones de diseño efectivos.

Como siempre, podemos encontrar el código fuente completo en el repositorio de GitHub.