Fügen Sie Ihren Spring-basierten Anwendungen eine einfache Regelengine hinzu

Jedes nichttriviale Softwareprojekt enthält eine nichttriviale Menge an sogenannter Geschäftslogik. Was genau Geschäftslogik ausmacht, ist umstritten. In den Bergen von Code, die für eine typische Softwareanwendung erstellt wurden, erledigen Bits and Pieces hier und da tatsächlich die Aufgabe, für die die Software benötigt wurde - Prozesse verarbeiten, Waffensysteme steuern, Bilder zeichnen usw. Diese Bits stehen in scharfem Kontrast zu anderen, die sich mit Persistenz befassen , Protokollierung, Transaktionen, Kuriositäten, Framework-Macken und andere Leckerbissen einer modernen Unternehmensanwendung.

Meistens ist die Geschäftslogik tief mit all diesen anderen Teilen vermischt. Wenn schwere, aufdringliche Frameworks (wie Enterprise JavaBeans) verwendet werden, wird es besonders schwierig zu erkennen, wo die Geschäftslogik endet und vom Framework inspirierter Code beginnt.

Es gibt eine Softwareanforderung, die in den Anforderungsdefinitionsdokumenten selten aufgeführt ist, die jedoch die Fähigkeit besitzt, ein Softwareprojekt zu erstellen oder zu unterbrechen: Anpassungsfähigkeit, das Maß dafür, wie einfach es ist, die Software als Reaktion auf Änderungen des Geschäftsumfelds zu ändern.

Moderne Unternehmen sind gezwungen, schnell und flexibel zu sein, und sie wollen dasselbe von ihrer Unternehmenssoftware. Geschäftsregeln, die heute so sorgfältig in die Geschäftslogik Ihrer Klassen implementiert wurden, werden morgen veraltet sein und müssen schnell und genau geändert werden. Wenn in Ihrem Code die Geschäftslogik tief in Tonnen und Tonnen dieser anderen Teile vergraben ist, werden Änderungen schnell langsam, schmerzhaft und fehleranfällig.

Kein Wunder, dass einige der derzeit angesagtesten Bereiche in der Unternehmenssoftware Regel-Engines und verschiedene BPM-Systeme (Business Process Management) sind. Sobald Sie die Marketing-Rede durchgesehen haben, versprechen diese Tools im Wesentlichen dasselbe: den Heiligen Gral der Geschäftslogik, der in einem Repository erfasst, sauber getrennt und für sich allein vorhanden ist und von jeder Anwendung in Ihrem Softwarehaus aufgerufen werden kann.

Kommerzielle Regel-Engines und BPM-Systeme haben zwar viele Vorteile, sie weisen jedoch auch viele Mängel auf. Am einfachsten ist der Preis, der manchmal leicht bis in die sieben Ziffern reicht. Ein weiterer Grund ist der Mangel an praktischer Standardisierung, der trotz großer Anstrengungen der Industrie und mehrerer auf dem Papier verfügbarer Standards bis heute anhält. Und da immer mehr Software-Shops agile, schlanke und schnelle Entwicklungsmethoden anpassen, fällt es diesen schwergewichtigen Tools schwer, sich anzupassen.

In diesem Artikel erstellen wir eine einfache Regel-Engine, die einerseits die für solche Systeme typische klare Trennung der Geschäftslogik nutzt und andererseits - weil sie auf dem beliebten und leistungsstarken J2EE-Framework basiert - nicht funktioniert leiden unter der Komplexität und "Uncoolness" der kommerziellen Angebote.

Frühlingszeit im J2EE-Universum

Nachdem die Komplexität der Unternehmenssoftware unerträglich wurde und das Problem der Geschäftslogik ins Rampenlicht geriet, wurden das Spring Framework und ähnliche Unternehmen geboren. Spring ist wohl das Beste, was Enterprise Java seit langem passiert ist. Spring bietet eine lange Liste von Tools und kleinen Code-Annehmlichkeiten, die die J2EE-Programmierung objektorientierter, einfacher und unterhaltsamer machen.

Im Herzen des Frühlings liegt das Prinzip der Umkehrung der Kontrolle. Dies ist ein ausgefallener und überladener Name, aber es kommt auf diese einfachen Ideen an:

  • Die Funktionalität Ihres Codes ist in kleine, überschaubare Teile unterteilt
  • Diese Teile werden durch einfache Standard-Java-Beans dargestellt (einfache Java-Klassen, die einige, aber nicht alle JavaBeans-Spezifikationen aufweisen).
  • Sie beschäftigen sich nicht mit der Verwaltung dieser Beans (Erstellen, Zerstören, Festlegen von Abhängigkeiten).
  • Stattdessen erledigt der Spring-Container dies für Sie basierend auf einer Kontextdefinition, die normalerweise in Form einer XML-Datei bereitgestellt wird

Spring bietet auch viele andere Funktionen, wie ein vollständiges und leistungsstarkes Model-View-Controller-Framework für Webanwendungen, Convenience-Wrapper für die Programmierung der Java Database Connectivity und ein Dutzend anderer Frameworks. Diese Themen reichen jedoch weit über den Rahmen dieses Artikels hinaus.

Bevor ich beschreibe, was zum Erstellen einer einfachen Regelengine für Spring-basierte Anwendungen erforderlich ist, betrachten wir, warum dieser Ansatz eine gute Idee ist.

Regel-Engine-Designs haben zwei interessante Eigenschaften, die sie lohnenswert machen:

  • Erstens trennen sie den Geschäftslogikcode von anderen Bereichen der Anwendung
  • Zweitens sind sie extern konfigurierbar, dh die Definitionen der Geschäftsregeln und wie und in welcher Reihenfolge sie ausgelöst werden, werden extern in der Anwendung gespeichert und vom Regelersteller, nicht vom Anwendungsbenutzer oder sogar einem Programmierer, bearbeitet

Die Feder passt gut zu einer Regelmaschine. Das stark komponentenförmige Design einer ordnungsgemäß codierten Spring-Anwendung fördert die Platzierung Ihres Codes in kleinen, überschaubaren, separaten Teilen (Beans), die über die Spring-Kontextdefinitionen extern konfiguriert werden können.

Lesen Sie weiter, um diese gute Übereinstimmung zwischen den Anforderungen eines Regel-Engine-Designs und dem, was das Spring-Design bereits bietet, zu untersuchen.

Das Design einer Spring-basierten Regel-Engine

Wir stützen unser Design auf das Zusammenspiel von Spring-gesteuerten Java-Beans, die wir als Regelengine-Komponenten bezeichnen. Definieren wir die zwei Arten von Komponenten, die wir möglicherweise benötigen:

  • Eine Aktion ist eine Komponente, die in unserer Anwendungslogik tatsächlich etwas Nützliches bewirkt
  • Eine Regel ist eine Komponente, die eine Entscheidung in einem logischen Ablauf von Aktionen trifft

Da wir große Fans eines guten objektorientierten Designs sind, erfasst die folgende Basisklasse die Basisfunktionalität aller zukünftigen Komponenten, nämlich die Fähigkeit, von anderen Komponenten mit einigen Argumenten aufgerufen zu werden:

public abstract class AbstractComponent { public abstract void execute(Object arg) throws Exception; }

Natürlich ist die Basisklasse abstrakt, weil wir niemals eine für sich brauchen werden.

Und jetzt Code für ein AbstractAction, das durch andere zukünftige konkrete Maßnahmen erweitert werden soll:

public abstract class AbstractAction extends AbstractComponent {

private AbstractComponent nextStep; public void execute(Object arg) throws Exception { this.doExecute(arg); if(nextStep != null) nextStep.execute(arg); } protected abstract void doExecute(Object arg) throws Exception;

public void setNextStep(AbstractComponent nextStep) { this.nextStep = nextStep; }

public AbstractComponent getNextStep() { return nextStep; }

}

Wie Sie sehen, werden AbstractActionzwei Dinge ausgeführt: Es speichert die Definition der nächsten Komponente, die von unserer Regelengine aufgerufen wird. Und in seiner execute()Methode ruft es eine doExecute()Methode auf, die von einer konkreten Unterklasse definiert werden soll. Nach der doExecute()Rückgabe wird die nächste Komponente aufgerufen, falls vorhanden.

Unser AbstractRuleist ähnlich einfach:

public abstract class AbstractRule extends AbstractComponent {

private AbstractComponent positiveOutcomeStep; private AbstractComponent negativeOutcomeStep; public void execute(Object arg) throws Exception { boolean outcome = makeDecision(arg); if(outcome) positiveOutcomeStep.execute(arg); else negativeOutcomeStep.execute(arg);

}

protected abstract boolean makeDecision(Object arg) throws Exception;

// Getters and setters for positiveOutcomeStep and negativeOutcomeStep are omitted for brevity

In seiner execute()Methode AbstractActionruft der die makeDecision()Methode auf, die eine Unterklasse implementiert, und ruft dann abhängig vom Ergebnis dieser Methode eine der Komponenten auf, die entweder als positives oder negatives Ergebnis definiert sind.

Unser Design ist vollständig, wenn wir diese SpringRuleEngineKlasse vorstellen :

public class SpringRuleEngine { private AbstractComponent firstStep; public void setFirstStep(AbstractComponent firstStep) { this.firstStep = firstStep; } public void processRequest(Object arg) throws Exception { firstStep.execute(arg); } }

Das ist alles, was die Hauptklasse unserer Regelengine zu bieten hat: die Definition einer ersten Komponente in unserer Geschäftslogik und die Methode zum Starten der Verarbeitung.

But wait, where is the plumbing that wires all our classes together so they can work? You will next see how the magic of Spring helps us with that task.

Spring-based rule engine in action

Let's look at a concrete example of how this framework might work. Consider this use case: we must develop an application responsible for processing loan applications. We need to satisfy the following requirements:

  • We check the application for completeness and reject it otherwise
  • We check if the application came from an applicant living in a state where we are authorized to do business
  • We check if applicant's monthly income and his/her monthly expenses fit into a ratio we feel comfortable with
  • Incoming applications are stored in a database via a persistence service that we know nothing about, except for its interface (perhaps its development was outsourced to India)
  • Business rules are subject to change, which is why a rule-engine design is required

First, let's design a class representing our loan application:

public class LoanApplication { public static final String INVALID_STATE = "Sorry we are not doing business in your state"; public static final String INVALID_INCOME_EXPENSE_RATIO = "Sorry we cannot provide the loan given this expense/income ratio"; public static final String APPROVED = "Your application has been approved"; public static final String INSUFFICIENT_DATA = "You did not provide enough information on your application"; public static final String INPROGRESS = "in progress"; public static final String[] STATUSES = new String[] { INSUFFICIENT_DATA, INVALID_INCOME_EXPENSE_RATIO, INVALID_STATE, APPROVED, INPROGRESS };

private String firstName; private String lastName; private double income; private double expences; private String stateCode; private String status; public void setStatus(String status) { if(!Arrays.asList(STATUSES).contains(status)) throw new IllegalArgumentException("invalid status:" + status); this.status = status; }

// Bunch of other getters and setters are omitted

}

Our given persistence service is described by the following interface:

public interface LoanApplicationPersistenceInterface { public void recordApproval(LoanApplication application) throws Exception; public void recordRejection(LoanApplication application) throws Exception; public void recordIncomplete(LoanApplication application) throws Exception; }

We quickly mock this interface by developing a MockLoanApplicationPersistence class that does nothing but satisfy the contract defined by the interface.

We use the following subclass of the SpringRuleEngine class to load the Spring context from an XML file and actually begin the processing:

public class LoanProcessRuleEngine extends SpringRuleEngine { public static final SpringRuleEngine getEngine(String name) { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("SpringRuleEngineContext.xml"); return (SpringRuleEngine) context.getBean(name); } }

In diesem Moment haben wir das Skelett an Ort und Stelle, daher ist es der perfekte Zeitpunkt, um einen JUnit-Test zu schreiben, der unten angezeigt wird. Es werden einige Annahmen getroffen: Wir erwarten, dass unser Unternehmen nur in zwei Bundesstaaten tätig ist, Texas und Michigan. Und wir akzeptieren nur Kredite mit einem Kosten-Ertrags-Verhältnis von 70 Prozent oder besser.

public class SpringRuleEngineTest extends TestCase {

public void testSuccessfulFlow() throws Exception { SpringRuleEngine engine = LoanProcessRuleEngine.getEngine("SharkysExpressLoansApplicationProcessor"); LoanApplication application = new LoanApplication(); application.setFirstName("John"); application.setLastName("Doe"); application.setStateCode("TX"); application.setExpences(4500); application.setIncome(7000); engine.processRequest(application); assertEquals(LoanApplication.APPROVED, application.getStatus()); } public void testInvalidState() throws Exception { SpringRuleEngine engine = LoanProcessRuleEngine.getEngine("SharkysExpressLoansApplicationProcessor"); LoanApplication application = new LoanApplication(); application.setFirstName("John"); application.setLastName("Doe"); application.setStateCode("OK"); application.setExpences(4500); application.setIncome(7000); engine.processRequest(application); assertEquals(LoanApplication.INVALID_STATE, application.getStatus()); } public void testInvalidRatio() throws Exception { SpringRuleEngine engine = LoanProcessRuleEngine.getEngine("SharkysExpressLoansApplicationProcessor"); LoanApplication application = new LoanApplication(); application.setFirstName("John"); application.setLastName("Doe"); application.setStateCode("MI"); application.setIncome(7000); application.setExpences(0.80 * 7000); //too high engine.processRequest(application); assertEquals(LoanApplication.INVALID_INCOME_EXPENSE_RATIO, application.getStatus()); } public void testIncompleteApplication() throws Exception { SpringRuleEngine engine = LoanProcessRuleEngine.getEngine("SharkysExpressLoansApplicationProcessor"); LoanApplication application = new LoanApplication(); engine.processRequest(application); assertEquals(LoanApplication.INSUFFICIENT_DATA, application.getStatus()); }