Come sostituire molte dichiarazioni if ​​in Java

1. Panoramica

I costrutti decisionali sono una parte vitale di qualsiasi linguaggio di programmazione. Ma arriviamo alla codifica di un numero enorme di istruzioni if ​​annidate che rendono il nostro codice più complesso e difficile da mantenere.

In questo tutorial, esamineremo i vari modi di sostituire le istruzioni if ​​annidate .

Esploriamo diverse opzioni su come possiamo semplificare il codice.

2. Caso di studio

Spesso ci imbattiamo in una logica aziendale che coinvolge molte condizioni e ognuna di esse necessita di un'elaborazione diversa. Per il bene di una demo, prendiamo l'esempio di una classe Calcolatrice . Avremo un metodo che prende due numeri e un operatore come input e restituisce il risultato in base all'operazione:

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

Possiamo anche implementarlo usando le istruzioni switch :

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

Nello sviluppo tipico, le istruzioni if ​​possono diventare di natura molto più grande e più complessa . Inoltre, le istruzioni switch non si adattano bene in presenza di condizioni complesse .

Un altro effetto collaterale dell'avere costrutti decisionali annidati è che diventano ingestibili. Ad esempio, se dobbiamo aggiungere un nuovo operatore, dobbiamo aggiungere una nuova istruzione if e implementare l'operazione.

3. Refactoring

Esploriamo le opzioni alternative per sostituire le complesse istruzioni if ​​sopra in un codice molto più semplice e gestibile.

3.1. Classe di fabbrica

Molte volte incontriamo costrutti decisionali che finiscono per fare l'operazione simile in ogni ramo. Ciò offre l'opportunità di estrarre un metodo factory che restituisce un oggetto di un determinato tipo ed esegue l'operazione in base al comportamento dell'oggetto concreto .

Per il nostro esempio, definiamo un'interfaccia operativa che abbia un unico metodo di applicazione :

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

Il metodo accetta due numeri come input e restituisce il risultato. Definiamo una classe per eseguire le aggiunte:

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

Ora implementeremo una classe factory che restituisce istanze di Operation basate sull'operatore specificato:

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

Ora, nella classe Calculator , possiamo interrogare la factory per ottenere l'operazione pertinente e applicarla ai numeri di origine:

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

In questo esempio, abbiamo visto come la responsabilità è delegata a oggetti liberamente accoppiati serviti da una classe factory. Ma potrebbero esserci delle possibilità in cui le istruzioni if ​​annidate vengono semplicemente spostate nella classe factory che vanifica il nostro scopo.

In alternativa, possiamo mantenere un repository di oggetti in una mappa che potrebbe essere interrogato per una rapida ricerca . Come abbiamo visto OperatorFactory # operationMap serve al nostro scopo. Possiamo anche inizializzare le mappe in fase di esecuzione e configurarle per la ricerca.

3.2. Uso di enumerazioni

Oltre all'uso di Map, possiamo anche usare Enum per etichettare particolari logiche di business . Dopo di che, possiamo usare sia nel nidificato se le dichiarazioni o le case interruttore dichiarazioni . In alternativa, possiamo anche usarli come una fabbrica di oggetti e strategizzarli per eseguire la relativa logica di business.

Ciò ridurrebbe anche il numero di istruzioni if ​​annidate e delegherebbe la responsabilità ai singoli valori Enum .

Vediamo come possiamo ottenerlo. In un primo momento, dobbiamo definire il nostro Enum :

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

Come possiamo osservare, i valori sono le etichette dei diversi operatori che verranno ulteriormente utilizzati per il calcolo. Abbiamo sempre un'opzione per utilizzare i valori come condizioni diverse nelle istruzioni if ​​annidate o nei casi switch, ma progettiamo un modo alternativo per delegare la logica all'Enum stesso.

Definiremo metodi per ciascuno dei valori Enum e faremo il calcolo. Per esempio:

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

E poi nella classe Calculator , possiamo definire un metodo per eseguire l'operazione:

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

Ora possiamo invocare il metodo convertendo il valore String in Operator utilizzando il metodo Operator # valueOf () :

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

3.3. Modello di comando

Nella discussione precedente, abbiamo visto l'uso della classe factory per restituire l'istanza dell'oggetto business corretto per un dato operatore. Successivamente, l'oggetto di business viene utilizzato per eseguire il calcolo nella calcolatrice .

Possiamo anche progettare un metodo Calculator # calcola per accettare un comando che può essere eseguito sugli input . Questo sarà un altro modo per sostituire le istruzioni if annidate .

Definiremo prima la nostra interfaccia di comando :

public interface Command { Integer execute(); }

Successivamente, implementiamo 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; } }

Infine, introduciamo un nuovo metodo nella Calcolatrice che accetta ed esegue il 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; }

E infine definiamo una classe AddRule personalizzata che valuta solo quando viene specificata l' operazione 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; } }

Ora invocheremo il RuleEngine con un'espressione :

@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. Conclusione

In questo tutorial, abbiamo esplorato una serie di diverse opzioni per semplificare il codice complesso. Abbiamo anche imparato come sostituire le istruzioni if ​​annidate utilizzando modelli di progettazione efficaci.

Come sempre, possiamo trovare il codice sorgente completo nel repository GitHub.