Aggiungi un semplice motore di regole alle tue applicazioni basate su Spring

Qualsiasi progetto software non banale contiene una quantità non banale della cosiddetta logica di business. Ciò che costituisce esattamente la logica aziendale è discutibile. Nelle montagne di codice prodotto per una tipica applicazione software, bit e pezzi qua e là svolgono effettivamente il lavoro per cui il software era richiesto: elaborare ordini, controllare sistemi d'arma, disegnare immagini, ecc. Quei bit sono in netto contrasto con altri che si occupano di persistenza , registrazione, transazioni, stranezze linguistiche, stranezze del framework e altre curiosità di un'applicazione aziendale moderna.

Il più delle volte, la logica aziendale è profondamente mescolata con tutti quegli altri pezzi. Quando vengono utilizzati framework pesanti e intrusivi (come Enterprise JavaBeans), discernere dove finisce la logica aziendale e inizia il codice ispirato al framework diventa particolarmente difficile.

C'è un requisito software raramente enunciato nei documenti di definizione dei requisiti, ma ha il potere di creare o distruggere qualsiasi progetto software: l'adattabilità, la misura di quanto sia facile cambiare il software in risposta ai cambiamenti dell'ambiente aziendale.

Le aziende moderne sono costrette a essere rapide e flessibili e vogliono lo stesso dal loro software aziendale. Le regole aziendali che sono state implementate così scrupolosamente nella logica aziendale delle tue classi oggi diventeranno obsolete domani e dovranno essere modificate rapidamente e accuratamente. Quando il tuo codice ha la logica aziendale sepolta in profondità all'interno di tonnellate e tonnellate di questi altri bit, la modifica diventerà rapidamente lenta, dolorosa e soggetta a errori.

Non c'è da stupirsi che alcuni dei settori più alla moda nel software aziendale oggi siano i motori di regole e vari sistemi di gestione dei processi aziendali (BPM). Una volta esaminato il discorso di marketing, questi strumenti promettono essenzialmente la stessa cosa: il Sacro Graal della Business Logic catturato in un repository, separato in modo pulito ed esistente da solo, pronto per essere chiamato da qualsiasi applicazione possiate avere nella vostra software house.

Sebbene i motori di regole commerciali ei sistemi BPM abbiano molti vantaggi, includono anche molti difetti. Quello più facile da scegliere è il prezzo, che a volte può facilmente raggiungere le sette cifre. Un altro è la mancanza di standardizzazione pratica che continua ancora oggi nonostante i grandi sforzi del settore e i molteplici standard su carta disponibili. E, poiché sempre più negozi di software adattano metodologie di sviluppo agili, snelle e veloci, questi strumenti pesanti trovano difficile adattarsi.

In questo articolo, creiamo un semplice motore di regole che, da un lato, sfrutta la netta separazione della logica di business tipica di tali sistemi e, dall'altro lato, poiché è supportato dal popolare e potente framework J2EE, non lo fa soffre della complessità e della "mancanza di eleganza" delle offerte commerciali.

La primavera nell'universo J2EE

Dopo che la complessità del software aziendale è diventata insopportabile e il problema della logica aziendale è entrato in scena, sono nati lo Spring Framework e altri simili. Probabilmente, la primavera è la cosa migliore che è accaduta a Java aziendale da molto tempo. Spring fornisce il lungo elenco di strumenti e piccole comodità di codice che rendono la programmazione J2EE più orientata agli oggetti, molto più semplice e, beh, più divertente.

Nel cuore della primavera risiede il principio dell'inversione del controllo. Questo è un nome stravagante e sovraccarico, ma si riduce a queste semplici idee:

  • La funzionalità del tuo codice è suddivisa in piccoli pezzi gestibili
  • Questi pezzi sono rappresentati da semplici Java bean standard (semplici classi Java che mostrano alcune, ma non tutte, le specifiche JavaBeans)
  • Tu non essere coinvolti con la gestione di quei fagioli (creando, distruggendo, impostando le dipendenze)
  • Invece, il contenitore Spring lo fa per te in base a una definizione di contesto solitamente fornita sotto forma di file XML

Spring fornisce anche molte altre funzionalità, come un framework Model-View-Controller completo e potente per applicazioni Web, wrapper per la programmazione di Java Database Connectivity e una dozzina di altri framework. Ma questi argomenti vanno ben al di fuori dello scopo di questo articolo.

Prima di descrivere cosa serve per creare un semplice motore di regole per applicazioni basate su Spring, consideriamo perché questo approccio è una buona idea.

I progetti del motore di regole hanno due proprietà interessanti che li rendono utili:

  • In primo luogo, separano il codice della logica di business dalle altre aree dell'applicazione
  • In secondo luogo, sono configurabili esternamente, il che significa che le definizioni delle regole di business e come e in quale ordine vengono attivate vengono memorizzate esternamente all'applicazione e manipolate dal creatore della regola, non dall'utente dell'applicazione o anche da un programmatore

La primavera fornisce una buona misura per un motore di regole. Il design altamente componentizzato di un'applicazione Spring correttamente codificata promuove l'inserimento del codice in parti piccole, gestibili e separate (bean), configurabili esternamente tramite le definizioni del contesto Spring.

Continua a leggere per esplorare questa buona corrispondenza tra ciò di cui ha bisogno un design del motore di regole e ciò che il design Spring fornisce già.

Il design di un motore di regole basato sulla primavera

Basiamo il nostro progetto sull'interazione di Java bean controllati da Spring, che chiamiamo componenti del motore di regole. Definiamo i due tipi di componenti di cui potremmo aver bisogno:

  • L' azione è un componente che fa effettivamente qualcosa di utile nella nostra logica dell'applicazione
  • Una regola è un componente che prende una decisione in un flusso logico di azioni

Poiché siamo grandi fan del buon design orientato agli oggetti, la seguente classe di base cattura la funzionalità di base di tutti i nostri componenti a venire, vale a dire la capacità di essere chiamati da altri componenti con qualche argomento:

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

Naturalmente la classe base è astratta perché non ne avremo mai bisogno da sola.

E ora, codice per un AbstractAction, da estendere da altre future azioni concrete:

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

}

Come puoi vedere, AbstractActionfa due cose: Memorizza la definizione del prossimo componente che verrà richiamato dal nostro motore di regole. E, nel suo execute()metodo, chiama un doExecute()metodo che deve essere definito da una sottoclasse concreta. Dopo i doExecute()ritorni, viene richiamato il componente successivo, se presente.

Il nostro AbstractRuleè altrettanto semplice:

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

Nel suo execute()metodo, AbstractActionchiama il makeDecision()metodo, implementato da una sottoclasse, quindi, a seconda del risultato di quel metodo, chiama uno dei componenti definiti come risultato positivo o negativo.

Il nostro progetto è completo quando introduciamo questa SpringRuleEngineclasse:

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

Questo è tutto ciò che c'è nella classe principale del nostro motore di regole: la definizione di un primo componente nella nostra logica aziendale e il metodo per avviare l'elaborazione.

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 questo momento, abbiamo lo scheletro in posizione, quindi è il momento perfetto per scrivere un test JUnit, che appare di seguito. Vengono fatte alcune ipotesi: ci aspettiamo che la nostra azienda operi solo in due stati, Texas e Michigan. E accettiamo solo prestiti con un rapporto spesa / reddito del 70% o superiore.

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