Dodajte jednostavni mehanizam pravila svojim aplikacijama temeljenim na Springu

Bilo koji netrivijalni softverski projekt sadrži netrivijalnu količinu takozvane poslovne logike. Diskutabilno je što točno predstavlja poslovnu logiku. U planinama koda izrađenog za tipičnu softversku aplikaciju, komadići tu i tamo zapravo odrade posao za koji je softver i pozvan - obrađuju narudžbe, kontroliraju sustave oružja, crtaju slike itd. Ti bitovi naglo se razlikuju od ostalih koji se bave upornošću , bilježenje, transakcije, jezične neobičnosti, okviri i druge sitnice moderne poslovne aplikacije.

Češće se poslovna logika duboko miješa sa svim onim ostalim dijelovima. Kada se koriste teški, nametljivi okviri (poput Enterprise JavaBeans), razlučivanje gdje završava poslovna logika i početak koda nadahnutog okvirom postaje posebno teško.

Postoji jedan zahtjev za softverom koji je rijetko naveden u dokumentima za definiciju zahtjeva, ali on ima moć napraviti ili razbiti bilo koji softverski projekt: prilagodljivost, mjera koliko je lako promijeniti softver kao odgovor na promjene poslovnog okruženja.

Moderne tvrtke prisiljene su biti brze i fleksibilne, a to žele i od svog poslovnog softvera. Poslovna pravila koja su danas tako mukotrpno primijenjena u poslovnoj logici vaših predavanja sutra će zastarjeti i trebat će ih brzo i točno promijeniti. Kada vaš kôd ima poslovnu logiku zakopanu duboko u tone i tone onih drugih bitova, modifikacija će brzo postati spora, bolna i sklona pogreškama.

Nije ni čudo što su neka od najmodernijih polja u poslovnom softveru danas mehanizmi za upravljanje i razni sustavi za upravljanje poslovnim procesima (BPM). Jednom kad pogledate marketinški govor, ti alati obećavaju u osnovi isto: Sveti gral poslovne logike zarobljen u spremištu, čisto odvojen i sam za sebe, spreman za poziv iz bilo koje aplikacije koju imate u svojoj softverskoj kući.

Iako motori s komercijalnim pravilima i BPM sustavi imaju brojne prednosti, oni također uključuju mnoge nedostatke. Jednostavno je odabrati cijenu koja ponekad može lako doseći sedam znamenki. Drugi je nedostatak praktične standardizacije koja se nastavlja i danas, usprkos velikim industrijskim naporima i dostupnim višestrukim standardima na papiru. I kako se sve više i više softverskih trgovina prilagođava agilnim, vitkim i brzim metodologijama za razvoj, tako se teški alati teško mogu uklopiti.

U ovom smo članku izgradili jednostavan mehanizam pravila koji, s jedne strane, koristi jasno odvajanje poslovne logike tipične za takve sustave, a s druge strane - jer se oslanja na popularni i snažni J2EE okvir - ne pate od složenosti i "nehladnosti" komercijalnih ponuda.

Proljetno vrijeme u svemiru J2EE

Nakon što je složenost poslovnog softvera postala nepodnošljiva i problem poslovne logike ušao u središte pozornosti, rođeni su Spring Framework i drugi slični njemu. Vjerojatno je Proljeće najbolja stvar koja se dugo dogodila s poslovnom Javom. Spring pruža dugačak popis alata i malih pogodnosti koda koji J2EE programiranje čine objektno orijentiranim, puno lakšim i, zabavnijim.

U srcu Proljeća leži princip Inverzije kontrole. Ovo je otmjeno i preopterećeno ime, ali svodi se na ove jednostavne ideje:

  • Funkcionalnost vašeg koda podijeljena je na male dijelove kojima se može upravljati
  • Ti su dijelovi predstavljeni jednostavnim, standardnim Java grahom (jednostavne Java klase koje pokazuju neke, ali ne sve, specifikacije JavaBeans)
  • Vi ne ne dobiti koji su uključeni u upravljanje tim grah (kreiranje, uništavanje, postavljanje zavisnosti)
  • Umjesto toga, Spring spremnik to čini umjesto vas na temelju neke definicije konteksta koja se obično daje u obliku XML datoteke

Spring također nudi mnoge druge značajke, poput cjelovitog i moćnog okvira Model-View-Controller za web aplikacije, praktične omotnice za programiranje Java Database Connectivity i desetak drugih okvira. Ali ti predmeti dosežu i izvan dosega ovog članka.

Prije nego što opišem što je potrebno za stvaranje jednostavnog mehanizma pravila za aplikacije temeljene na Springu, razmotrimo zašto je ovaj pristup dobra ideja.

Dizajn ručnog motora ima dva zanimljiva svojstva koja ih čine vrijednima:

  • Prvo, oni odvajaju kôd poslovne logike od ostalih područja aplikacije
  • Drugo, oni se mogu konfigurirati izvana, što znači da se definicije poslovnih pravila i kako i kojim redoslijedom aktiviraju pohranjuju izvana u aplikaciju i njima manipulira kreator pravila, a ne korisnik aplikacije ili čak programer

Proljeće dobro odgovara pogonu s pravilima. Visokokomponentni dizajn pravilno kodirane aplikacije Spring promovira postavljanje vašeg koda u male, upravljive, odvojene dijelove (grah), koji se mogu eksterno konfigurirati putem definicija konteksta Spring.

Čitajte dalje kako biste istražili ovo dobro podudaranje između onoga što dizajn dizajna pravila treba i onoga što dizajn Spring već pruža.

Dizajn pokretačkog mehanizma utemeljenog na opruzi

Dizajn zasnivamo na interakciji Java graha kontroliranih proljećem, koje nazivamo komponentama mehanizma pravila. Definirajmo dvije vrste komponenata koje bi nam mogle trebati:

  • Akcija je komponenta koja zapravo radi nešto korisno u našem aplikacijske logike
  • Pravilo je komponenta koja čini rješenje u logičan tijek akcija

Budući da smo veliki ljubitelji dobrog objektno orijentiranog dizajna, sljedeća osnovna klasa bilježi osnovnu funkcionalnost svih naših budućih komponenata, naime mogućnost da ih druge komponente pozivaju s nekim argumentom:

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

Prirodno je da je osnovna klasa apstraktna, jer nam nikada neće trebati sama.

A sada, kod za koji će AbstractActionse proširiti drugim budućim konkretnim radnjama:

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

}

Kao što vidite, AbstractActiončini dvije stvari: pohranjuje definiciju sljedeće komponente na koju se poziva naš mehanizam pravila. I, u svojoj execute()metodi, poziva doExecute()metodu koju treba definirati konkretnom podrazredom. Nakon doExecute()vraćanja poziva se sljedeća komponenta ako je postoji.

Naš AbstractRuleje slično jednostavan:

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

U svojoj execute()metodi AbstractActionpoziva makeDecision()metodu, koju podklasa implementira, a zatim, ovisno o rezultatu te metode, poziva jednu od komponenata definiranih kao pozitivan ili negativan ishod.

Naš dizajn je gotov kada predstavimo ovu SpringRuleEngineklasu:

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

To je sve što postoji u glavnoj klasi našeg mehanizma pravila: definicija prve komponente u našoj poslovnoj logici i metoda za početak obrade.

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

U ovom trenutku imamo kostur na mjestu, pa je savršeno vrijeme za pisanje JUnit testa, koji se pojavljuje u nastavku. Izneseno je nekoliko pretpostavki: Očekujemo da će naša tvrtka poslovati u samo dvije države, Teksasu i Michiganu. A mi prihvaćamo samo zajmove s omjerom troškova i prihoda od 70 posto ili boljim.

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