Ajoutez un moteur de règles simple à vos applications Spring

Tout projet logiciel non trivial contient une quantité non triviale de soi-disant logique métier. Ce qui constitue exactement la logique métier est discutable. Dans les montagnes de code produites pour une application logicielle typique, des morceaux ici et là font réellement le travail pour lequel le logiciel était demandé: traiter les ordres, contrôler les systèmes d'armes, dessiner des images, etc. Ces bits contrastent fortement avec d'autres qui traitent de la persistance , la journalisation, les transactions, les bizarreries de la langue, les bizarreries du cadre et d'autres bribes d'une application d'entreprise moderne.

Le plus souvent, la logique métier est profondément mêlée à tous ces autres éléments. Lorsque des frameworks lourds et intrusifs (tels que Enterprise JavaBeans) sont utilisés, il devient particulièrement difficile de discerner où la logique métier se termine et où commence le code inspiré du framework.

Il y a une exigence logicielle rarement énoncée dans les documents de définition des exigences, mais qui a le pouvoir de faire ou de casser n'importe quel projet logiciel: l'adaptabilité, la mesure de la facilité avec laquelle il est de changer le logiciel en réponse aux changements de l'environnement commercial.

Les entreprises modernes sont obligées d'être rapides et flexibles, et elles veulent la même chose de leurs logiciels d'entreprise. Les règles métier qui ont été si minutieusement implémentées dans la logique métier de vos classes aujourd'hui deviendront obsolètes demain et devront être modifiées rapidement et avec précision. Lorsque votre code a une logique métier enfouie profondément dans des tonnes et des tonnes de ces autres bits, la modification deviendra rapidement lente, douloureuse et sujette aux erreurs.

Il n'est pas étonnant que certains des domaines les plus à la mode des logiciels d'entreprise aujourd'hui soient les moteurs de règles et divers systèmes de gestion des processus métier (BPM). Une fois que vous avez parcouru le discours marketing, ces outils promettent essentiellement la même chose: le Saint Graal de la logique commerciale capturé dans un référentiel, proprement séparé et existant par lui-même, prêt à être appelé à partir de n'importe quelle application que vous pourriez avoir dans votre éditeur de logiciels.

Bien que les moteurs de règles commerciaux et les systèmes BPM présentent de nombreux avantages, ils comportent également de nombreuses lacunes. Le plus facile à choisir est le prix, qui peut parfois facilement atteindre les sept chiffres. Un autre est le manque de normalisation pratique qui se poursuit aujourd'hui malgré les efforts majeurs de l'industrie et les multiples normes sur papier disponibles. Et, alors que de plus en plus de boutiques de logiciels adaptent des méthodologies de développement agiles, allégées et rapides, ces outils lourds ont du mal à s'intégrer.

Dans cet article, nous construisons un moteur de règles simple qui, d'une part, exploite la séparation claire de la logique métier typique de ces systèmes et, d'autre part, parce qu'il s'appuie sur le framework J2EE populaire et puissant, ne le fait pas souffrent de la complexité et du "non-cool" des offres commerciales.

Le printemps dans l'univers J2EE

Après que la complexité des logiciels d'entreprise soit devenue insupportable et que le problème de logique métier soit entré sous les projecteurs, Spring Framework et d'autres du même genre sont nés. On peut soutenir que Spring est la meilleure chose qui soit arrivée à Java d'entreprise depuis longtemps. Spring fournit la longue liste d'outils et de petits codes pratiques qui rendent la programmation J2EE plus orientée objet, beaucoup plus facile et, bien, plus amusante.

Au cœur de Spring se trouve le principe de l'inversion de contrôle. C'est un nom sophistiqué et surchargé, mais cela se résume à ces idées simples:

  • La fonctionnalité de votre code est divisée en petits morceaux gérables
  • Ces éléments sont représentés par de simples beans Java standard (classes Java simples qui présentent une partie, mais pas la totalité, de la spécification JavaBeans)
  • Vous ne pas vous impliquer à la gestion de ces haricots (créer, détruire, la mise en dépendances)
  • Au lieu de cela, le conteneur Spring le fait pour vous en fonction d'une définition de contexte généralement fournie sous la forme d'un fichier XML

Spring fournit également de nombreuses autres fonctionnalités, telles qu'un framework Model-View-Controller complet et puissant pour les applications Web, des wrappers pratiques pour la programmation Java Database Connectivity, et une douzaine d'autres frameworks. Mais ces sujets dépassent largement le cadre de cet article.

Avant de décrire ce qu'il faut pour créer un moteur de règles simple pour les applications Spring, voyons pourquoi cette approche est une bonne idée.

Les conceptions de moteur de règles ont deux propriétés intéressantes qui en valent la peine:

  • Premièrement, ils séparent le code de logique métier des autres domaines de l'application
  • Deuxièmement, elles sont configurables en externe, ce qui signifie que les définitions des règles métier et comment et dans quel ordre elles se déclenchent sont stockées en externe à l'application et manipulées par le créateur de la règle, pas par l'utilisateur de l'application ni même par un programmeur

Spring convient parfaitement à un moteur de règles. La conception hautement composante d'une application Spring correctement codée favorise le placement de votre code en petits morceaux séparés (beans) gérables , qui sont configurables en externe via les définitions de contexte Spring.

Poursuivez votre lecture pour explorer cette bonne correspondance entre ce dont une conception de moteur de règles a besoin et ce que la conception Spring fournit déjà.

La conception d'un moteur de règles basé sur Spring

Nous basons notre conception sur l'interaction des beans Java contrôlés par Spring, que nous appelons les composants du moteur de règles. Définissons les deux types de composants dont nous pourrions avoir besoin:

  • Une action est un composant qui fait réellement quelque chose d'utile dans notre logique d'application
  • Une règle est un composant qui prend une décision dans un flux logique d'actions

Comme nous sommes de grands fans d'une bonne conception orientée objet, la classe de base suivante capture la fonctionnalité de base de tous nos composants à venir, à savoir la possibilité d'être appelée par d'autres composants avec un argument:

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

Naturellement, la classe de base est abstraite car nous n'en aurons jamais besoin en soi.

Et maintenant, codez pour un AbstractAction, qui sera prolongé par d'autres actions concrètes à venir:

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

}

Comme vous pouvez le voir, AbstractActionfait deux choses: Il stocke la définition du prochain composant à invoquer par notre moteur de règles. Et, dans sa execute()méthode, il appelle une doExecute()méthode à définir par une sous-classe concrète. Après les doExecute()retours, le composant suivant est appelé s'il y en a un.

Notre AbstractRuleest tout aussi simple:

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

Dans sa execute()méthode, le AbstractActionappelle la makeDecision()méthode, qu'une sous-classe implémente, puis, en fonction du résultat de cette méthode, appelle l'un des composants définis comme un résultat positif ou négatif.

Notre conception est terminée lorsque nous introduisons cette 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); } }

C'est tout ce qu'il y a dans la classe principale de notre moteur de règles: la définition d'un premier composant dans notre logique métier et la méthode pour démarrer le traitement.

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

En ce moment, nous avons le squelette en place, c'est donc le moment idéal pour écrire un test JUnit, qui apparaît ci-dessous. Quelques hypothèses sont émises: nous nous attendons à ce que notre société opère dans seulement deux États, le Texas et le Michigan. Et nous n'acceptons que les prêts dont le ratio dépenses / revenus est égal ou supérieur à 70%.

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