So ersetzen Sie viele if-Anweisungen in Java

1. Übersicht

Entscheidungskonstrukte sind ein wesentlicher Bestandteil jeder Programmiersprache. Wir landen jedoch in der Codierung einer großen Anzahl verschachtelter if-Anweisungen, die unseren Code komplexer und schwieriger zu pflegen machen.

In diesem Tutorial werden die verschiedenen Möglichkeiten zum Ersetzen verschachtelter if-Anweisungen beschrieben .

Lassen Sie uns verschiedene Optionen untersuchen, wie wir den Code vereinfachen können.

2. Fallstudie

Oft stoßen wir auf eine Geschäftslogik, die viele Bedingungen beinhaltet und für die jeweils eine andere Verarbeitung erforderlich ist. Nehmen wir für eine Demo das Beispiel einer Calculator- Klasse. Wir werden eine Methode haben, die zwei Zahlen und einen Operator als Eingabe verwendet und das Ergebnis basierend auf der Operation zurückgibt:

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

Wir können dies auch mit switch- Anweisungen implementieren :

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

In der typischen Entwicklung können die if-Anweisungen viel größer und komplexer werden . Außerdem passen die switch-Anweisungen bei komplexen Bedingungen nicht gut .

Ein weiterer Nebeneffekt verschachtelter Entscheidungskonstrukte besteht darin, dass sie nicht mehr verwaltet werden können. Wenn wir beispielsweise einen neuen Operator hinzufügen müssen, müssen wir eine neue if-Anweisung hinzufügen und die Operation implementieren.

3. Refactoring

Lassen Sie uns die alternativen Optionen untersuchen, um die oben genannten komplexen if-Anweisungen durch viel einfacheren und verwaltbaren Code zu ersetzen.

3.1. Fabrikklasse

Oft stoßen wir auf Entscheidungskonstrukte, die in jedem Zweig die gleiche Operation ausführen. Dies bietet die Möglichkeit, eine Factory-Methode zu extrahieren, die ein Objekt eines bestimmten Typs zurückgibt und die Operation basierend auf dem konkreten Objektverhalten ausführt .

In unserem Beispiel definieren wir eine Operationsschnittstelle mit einer einzigen Apply- Methode:

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

Die Methode verwendet zwei Zahlen als Eingabe und gibt das Ergebnis zurück. Definieren wir eine Klasse für die Durchführung von Ergänzungen:

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

Wir werden jetzt eine Factory-Klasse implementieren, die Instanzen von Operation basierend auf dem angegebenen Operator zurückgibt :

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

Jetzt können wir in der Calculator- Klasse die Factory abfragen, um die relevante Operation abzurufen und auf die Quellennummern anzuwenden:

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 diesem Beispiel haben wir gesehen, wie die Verantwortung an lose gekoppelte Objekte delegiert wird, die von einer Factory-Klasse bedient werden. Es kann jedoch vorkommen, dass die verschachtelten if-Anweisungen einfach in die Factory-Klasse verschoben werden, was unseren Zweck zunichte macht.

Alternativ können wir ein Repository mit Objekten in einer Karte verwalten, das für eine schnelle Suche abgefragt werden kann . Wie wir gesehen haben, erfüllt OperatorFactory # operationMap unseren Zweck. Wir können Map auch zur Laufzeit initialisieren und für die Suche konfigurieren.

3.2. Verwendung von Enums

Zusätzlich zur Verwendung von Map können wir auch Enum verwenden , um bestimmte Geschäftslogiken zu kennzeichnen . Danach können wir sie entweder in den verschachtelten if-Anweisungen oder in switch case- Anweisungen verwenden . Alternativ können wir sie auch als Fabrik von Objekten verwenden und sie strategisieren, um die zugehörige Geschäftslogik auszuführen.

Dies würde auch die Anzahl der verschachtelten if-Anweisungen verringern und die Verantwortung an einzelne Enum- Werte delegieren .

Mal sehen, wie wir es erreichen können. Zuerst müssen wir unsere Aufzählung definieren :

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

Wie wir beobachten können, sind die Werte die Bezeichnungen der verschiedenen Operatoren, die für die Berechnung weiter verwendet werden. Wir haben immer die Möglichkeit, die Werte als unterschiedliche Bedingungen in verschachtelten if-Anweisungen oder Switch-Fällen zu verwenden. Lassen Sie uns jedoch eine alternative Methode zum Delegieren der Logik an die Enum selbst entwerfen .

Wir definieren Methoden für jeden der Enum- Werte und führen die Berechnung durch. Zum Beispiel:

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

Und dann können wir in der Calculator- Klasse eine Methode definieren, um die Operation auszuführen:

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

Nun können wir die Methode durch Aufrufen Umwandeln des String - Wert an den Operator mit Hilfe der Operator # valueOf () Methode :

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

3.3. Befehlsmuster

In der vorherigen Diskussion haben wir die Verwendung der Factory-Klasse gesehen, um die Instanz des richtigen Geschäftsobjekts für den angegebenen Operator zurückzugeben. Später wird das Geschäftsobjekt verwendet, um die Berechnung im Rechner durchzuführen .

Wir können auch eine Calculator # berechne- Methode entwerfen , um einen Befehl zu akzeptieren, der an den Eingaben ausgeführt werden kann . Dies ist eine weitere Möglichkeit, verschachtelte if-Anweisungen zu ersetzen .

Wir werden zuerst unsere Befehlsschnittstelle definieren:

public interface Command { Integer execute(); }

Als nächstes implementieren wir einen 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; } }

Lassen Sie uns abschließend eine neue Methode in den Rechner einführen , die den Befehl akzeptiert und ausführt :

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

Und schließlich definieren wir eine benutzerdefinierte AddRule- Klasse, die nur ausgewertet wird, wenn die ADD-Operation angegeben wird:

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

Wir werden jetzt die RuleEngine mit einem Ausdruck aufrufen :

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

In diesem Tutorial haben wir verschiedene Optionen untersucht, um komplexen Code zu vereinfachen. Wir haben auch gelernt, wie verschachtelte if-Anweisungen durch effektive Entwurfsmuster ersetzt werden.

Wie immer finden wir den vollständigen Quellcode über das GitHub-Repository.