An einem früheren Arbeitsplatz musste ich der bestehenden Funktion, die für den Versand verschiedener Benachrichtigungen auf der Grundlage unterschiedlicher Geschäftsregeln zuständig war, neue Funktionen hinzufügen. Für jede Meldung galten unterschiedliche Regeln für verschiedene Kundentypen. Wenn es zum Beispiel einen Kunden gibt, der nicht für ein Upgrade seines Abonnements in Frage kommt, würde die Funktion eine Benachrichtigung an diesen nicht in Frage kommenden Kunden senden.
If (RuleForIneligibleStatus){
// send notification for ineligible customers
sendNotificationForRuleForIneligibleStatus();
} else if (RuleForOpenUpgradeOrderStatus){
// send notification for OpenUpgradeOrder customers
sendNotificationForOpenUpgradeOrderStatus()
} else if (rule3) {
// send notification for rule3
} else if (rule4) {
// send notification for rule4
…
Was ist das Problem mit if/else-Anweisungen?
Stellen Sie sich vor, Sie hätten 20 weitere if-else-Anweisungen wie diese! Jeder, der etwas Erfahrung in der Softwareentwicklung hat, weiß, dass mit der Zeit weitere if-else-Klauseln zu dieser Funktion hinzugefügt werden. Dies ist vor allem dann unvermeidlich, wenn Sie in einem Team arbeiten, in dem es keine Refactoring-Kultur gibt.
Wenn eine neue Meldungsart erforderlich ist, wird ein Entwickler aufgrund des Zeitdrucks eine weitere "if"-Anweisung hinzufügen. Nach einer Weile könnten Sie mit Hunderten von Codezeilen für nur eine Funktion enden, da es für jede Regel eine andere Geschäftslogik geben wird. Aufgrund der zyklometrischen Komplexität der Funktion ist selbst die einfachste Änderung sehr zeitaufwändig.
Außerdem verlieren Sie sich in den if-else-Anweisungen, wenn es einen Fehler bei einer der Meldungen gibt. Es wird auch ein Alptraum sein, Ihre Funktion zu testen.
Eine alternative Lösung: Regel-Entwurfsmuster
Das Regelentwurfsmuster funktioniert durch die Trennung von Geschäftsregeln und Regelverarbeitungslogik (Prinzip der einzigen Verantwortung). Auf diese Weise können neue Regeln leicht hinzugefügt werden, ohne dass der Rest des Systems geändert werden muss (Prinzip der offenen und geschlossenen Struktur). Anstatt eine neue if-else-Anweisung hinzuzufügen, erstellen Sie daher eine neue Klasse für die Regel, die Sie hinzufügen möchten. Das Entwurfsmuster besteht aus 3 Komponenten:
IRegel: Eine Schnittstelle, über die Sie die Regeln definieren. In der Schnittstelle gibt es zwei Methoden, die jede Regel ausführen sollte. Die erste ist die Bedingung, unter der die Regel ausgeführt wird (die Bedingung in der if-Anweisung), und die zweite ist die Aktion, die die Regel ausführen wird (die Aktion, die ausgeführt wird, wenn die if-Anweisung gültig ist).
Rule1, Rule2…: Klassen, die Sie aus IRule implementieren und spezifische Regeln erstellen.
RuleEvaluator: Die Klasse, in der Sie Regeln in der gewünschten Reihenfolge hinzufügen und die Regeln ausführen.
Wie haben wir das Rule Design Pattern implementiert?
Wir wollten eine Struktur schaffen, die leichter zu ändern ist. Am Ende erhalten wir eine Lösung, die im folgenden UML-Diagramm dargestellt ist.
Regel-Schnittstelle: IEligibilityRule
Was Sie hier tun, ist, die Bedingung und die Aktion in der if-Klausel als zwei verschiedene Funktionen zu definieren. Daher können Sie jede if-Struktur in bestimmte Klassen umwandeln, zum Beispiel RuleForEligibleStatus:
if(RuleForIneligibleStatus){ --> isRuleApplicable() function
// send notification for ineligible customers
sendNotificationForRuleForIneligibleStatus(); --> executeEligibilityRule() function
}
Beispiel für eine spezifische Regel: RuleForEligibleStatus
Betrachten Sie jede von IEligibilityRule abgeleitete Klasse als einen if-Block.
isRuleApplicable stellt die Bedingung der if-Anweisung dar. Sie legen fest, unter welchen Bedingungen RuleForEligibleStatus funktionieren soll, indem Sie die Schnittstellenfunktion isRuleApplicable implementieren. executeEligibilityRule stellt den Körper der is a-Anweisung dar. Wenn die Regel gültig ist, definieren Sie die Aktionen von RuleForEligibleStatus mit seiner Funktion executeEligibilityRule.
Kurz gesagt, für jede Klasse erben Sie von der Schnittstelle IEligibilityRule: Die Funktionen isRuleApplicable und executeEligibilityRule enthalten jeweils unterschiedliche Bedingungen und Aktionen. Hier ist das Beispiel für die Klasse RuleForEligibleStatus:
public class RuleForEligibleStatus implements IEligibilityRule {
...
@Override
public boolean isRuleApplicable(Eligibility eligibility) {
return eligibility.getXXX()
.map(Offer::isEligible)
.filter(isEligible -> isEligible.equals(true))
.isPresent();
}
@Override
public void executeEligibilityRule(Eligibility eligibility, Notification notification) {
if (this.isRuleApplicable(eligibility)) {
eligibility.getXXX()
.ifPresent(details -> {
// send the notification here...
});
}
}
}
Ausführen von Regeln: EligibilityRuleEvaluator
In dieser Klasse definieren Sie die Regeln im Konstruktor EligibilityRuleEvaluator(). Wie Sie die Regeln ausführen, hängt von Ihren geschäftlichen Anforderungen ab.
In unserem Projekt hatten die Regeln eine Priorität und nur die erste Regel, die in Frage kommt, würde ausgeführt werden. Dann würden die übrigen Vorschriften wegfallen. Daher prüfen wir jede Regel einzeln in der Funktion evaluateRules und führen die Funktion isRuleApplicable aus, um die anwendbaren Regeln zu filtern.
Hier ist der Codeausschnitt des EligibilityRuleEvaluator.
public class EligibilityRuleEvaluator implements IEligibilityEvaluator {
private final List<IEligibilityRule> rules = new ArrayList<>();
/**
* Add the rules in the order you want to execute
*/
public EligibilityRuleEvaluator() {
...//rules
rules.add(new RuleForIneligibleStatus());
rules.add(new RuleForOpenUpgradeOrderStatus());
rules.add(new RuleForMultipleSubscriptions());
rules.add(new RuleForEligibleStatus());
}
@Override
public void evaluateRules(Eligibility eligibility, Notification notification) {
IEligibilityRule rule = rules.stream()
.filter(r -> r.isRuleApplicable(eligibility))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Expression does not matches any Rule"));
rule.executeEligibilityRule(eligibility, notification);
}
}
Der letzte Schritt
Schließlich müssen Sie die Funktion evaluateRules der Klasse EligibilityRuleEvaluator aufrufen, um die Regeln auszuführen. Die folgende Funktion führt Zulässigkeitsregeln auf der Grundlage der in EligibilityRuleEvaluator definierten Prioritätsliste aus.
Auf diese Weise können wir die Geschäftsregeln modularer, wartbarer und offen für Änderungen implementieren, ohne if-else-Anweisungen zu verwenden.
public void executeEligibilityRule(Notification notification, Eligibility upgradeEligibility) {
var eligibilityRuleEvaluator = new EligibilityRuleEvaluator();
eligibilityRuleEvaluator.evaluateRules(eligibility, notification);
}
Schlussfolgerung
Mit dem Regelentwurfsmuster lassen sich die Geschäftsregeln leicht in separate Klassen übersetzen. Jede if-else-Anweisung steht für die verschiedenen Regeln. Sie müssen lediglich eine Schnittstelle implementieren, die eine Methode zur Feststellung, ob eine Regel erfüllt ist, und eine Methode zur Entscheidung über die zu ergreifende Maßnahme definiert.
Es ist wichtig zu erwähnen, dass jede Regel die gleiche Schnittstelle implementieren sollte. Wenn dies nicht den Anforderungen Ihrer Anwendung entspricht, empfehle ich Ihnen dringend, andere von mir erwähnte Entwurfsmuster zu untersuchen. Wenn Sie ein Regelmuster nicht von Grund auf neu implementieren wollen, können Sie auch die Bibliotheken der Regel-Engine in Betracht ziehen.
Geschrieben von Suleyman Yildirim, einem Oracle Certified Java Software Engineer mit 9 Jahren IT-Erfahrung. Suleyman verfügt über fundierte Kenntnisse in allen Phasen der Softwareentwicklung, einschließlich Softwaredesign, -entwicklung und automatisierte Tests. Er hat an verschiedenen Projekten zur Entwicklung von Unternehmens-Webanwendungen mit Spring Ecosystem teilgenommen. Vor kurzem hat er einen Practical Spring Security Kurs für EC-Council entwickelt. Außerdem gibt er sein Wissen gerne weiter und schreibt in seinem persönlichen Blog über Software Engineering.
Blog: https://suleymanyildirim.org/
GitHub: https://github.com/barrida