Springベースのアプリケーションに簡単なルールエンジンを追加する

重要なソフトウェアプロジェクトには、重要な量のいわゆるビジネスロジックが含まれています。ビジネスロジックを正確に構成するものは議論の余地があります。典型的なソフトウェアアプリケーション用に作成されたコードの山の中で、ソフトウェアが要求された仕事を実際に行っています。注文の処理、武器システムの制御、絵の描画などです。これらのビットは、永続性を扱う他のビットとははっきりと対照的です。 、ロギング、トランザクション、言語の奇妙さ、フレームワークの癖、およびその他の最新のエンタープライズアプリケーションのヒント。

多くの場合、ビジネスロジックは他のすべての要素と深く混ざり合っています。重くて煩わしいフレームワーク(Enterprise JavaBeansなど)を使用すると、ビジネスロジックが終了し、フレームワークに触発されたコードがどこから始まるかを識別することが特に困難になります。

要件定義ドキュメントにほとんど記載されていないソフトウェア要件が1つありますが、ソフトウェアプロジェクトを作成または中断する力があります。それは、適応性、つまりビジネス環境の変化に応じてソフトウェアを変更するのがいかに簡単かを示す尺度です。

現代の企業は迅速かつ柔軟である必要があり、エンタープライズソフトウェアにも同じことを望んでいます。今日のクラスのビジネスロジックに非常に丹念に実装されたビジネスルールは、明日は廃止され、迅速かつ正確に変更する必要があります。コードにビジネスロジックが大量のその他のビットの奥深くに埋め込まれていると、変更はすぐに遅くなり、苦痛になり、エラーが発生しやすくなります。

今日のエンタープライズソフトウェアで最もトレンディな分野のいくつかが、ルールエンジンとさまざまなビジネスプロセス管理(BPM)システムであることは不思議ではありません。マーケティングの話を見ると、これらのツールは本質的に同じことを約束します。リポジトリにキャプチャされ、完全に分離され、それ自体が存在するビジネスロジックの聖杯は、ソフトウェアハウスにある任意のアプリケーションから呼び出す準備ができています。

商用ルールエンジンとBPMシステムには多くの利点がありますが、多くの欠点もあります。簡単に選択できるのは価格で、7桁に達することもあります。もう1つは、主要な業界の取り組みと複数の紙の標準が利用可能であるにもかかわらず、今日も続いている実用的な標準化の欠如です。そして、ますます多くのソフトウェアショップがアジャイル、リーン、そして迅速な開発方法論を採用するにつれて、それらの重量級のツールは適合が困難になっています。

この記事では、そのようなシステムに典型的なビジネスロジックの明確な分離を活用する一方で、人気のある強力なJ2EEフレームワークに便乗しているため、そうではない単純なルールエンジンを構築します。商用製品の複雑さと「冷静さ」に悩まされています。

J2EEユニバースの春の時間

エンタープライズソフトウェアの複雑さが耐えられなくなり、ビジネスロジックの問題が脚光を浴びた後、SpringFrameworkなどが誕生しました。間違いなく、SpringはエンタープライズJavaに長い間起こった最高の出来事です。Springは、J2EEプログラミングをよりオブジェクト指向で、はるかに簡単に、そしてもっと楽しくするためのツールの長いリストと小さなコードの便利さを提供します。

春の中心には、制御の反転の原理があります。これは派手でオーバーロードされた名前ですが、次のような単純なアイデアに帰着します。

  • コードの機能は、管理しやすい小さな部分に分割されています
  • これらの部分は、単純な標準Java Bean(JavaBeans仕様のすべてではなく一部を示す単純なJavaクラス)で表されます。
  • これらのBeanの管理(依存関係の作成、破棄、設定)には関与しません。
  • 代わりに、Springコンテナーは、通常XMLファイルの形式で提供されるコンテキスト定義に基づいてそれを行います。

Springは、Webアプリケーション用の完全で強力なModel-View-Controllerフレームワーク、Java Database Connectivityプログラミング用の便利なラッパー、その他多数のフレームワークなど、他の多くの機能も提供します。しかし、それらの主題はこの記事の範囲をはるかに超えています。

Springベースのアプリケーション用の単純なルールエンジンを作成するために必要なことを説明する前に、このアプローチがなぜ良い考えであるかを考えてみましょう。

ルールエンジンの設計には、価値のある2つの興味深い特性があります。

  • まず、ビジネスロジックコードをアプリケーションの他の領域から分離します
  • 次に、外部で構成可能です。つまり、ビジネスルールの定義と、それらを実行する方法と順序は、アプリケーションの外部に保存され、アプリケーションユーザーやプログラマーではなく、ルール作成者によって操作されます。

Springはルールエンジンに最適です。適切にコーディングされたSpringアプリケーションの高度にコンポーネント化された設計により、Springコンテキスト定義を介して外部で構成可能な、小さくて管理しやすい個別の部分(Bean)へのコードの配置が促進されます。

ルールエンジン設計が必要とするものとSpring設計がすでに提供しているものとの間のこの良い一致を探求するために読んでください。

Springベースのルールエンジンの設計

設計は、ルールエンジンコンポーネントと呼ばれるSpring制御のJavaBeanの相互作用に基づいています。必要になる可能性のある2種類のコンポーネントを定義しましょう。

  • アクションは、実際に我々のアプリケーション・ロジックに便利な何かをするコンポーネントです
  • ルールは作る要素である意思決定を行動の論理的な流れの中で

私たちは優れたオブジェクト指向設計の大ファンであるため、次の基本クラスは、今後登場するすべてのコンポーネントの基本機能、つまり、他のコンポーネントから何らかの引数で呼び出される機能をキャプチャします。

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

当然、基本クラスは抽象的です。それ自体は必要ないからです。

そして今、AbstractAction他の将来の具体的なアクションによって拡張される、のコード:

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

}

ご覧のとおり、AbstractAction2つのことを行います。ルールエンジンによって呼び出される次のコンポーネントの定義を格納します。そして、そのexecute()メソッドではdoExecute()、具象サブクラスによって定義されるメソッドを呼び出します。doExecute()戻った後、次のコンポーネントがあればそれが呼び出されます。

私たちAbstractRuleも同様に単純です:

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

そのexecute()メソッドでは、サブクラスが実装AbstractActionするmakeDecision()メソッドを呼び出し、そのメソッドの結果に応じて、正または負の結果として定義されたコンポーネントの1つを呼び出します。

このSpringRuleEngineクラスを紹介すると、設計が完了します。

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

ルールエンジンのメインクラスにあるのはこれだけです。ビジネスロジックの最初のコンポーネントの定義と、処理を開始するメソッドです。

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

現時点では、スケルトンが配置されているので、以下に示すJUnitテストを作成する絶好のタイミングです。いくつかの仮定があります。当社は、テキサス州とミシガン州の2つの州でのみ事業を展開することを期待しています。また、費用/収入の比率が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()); }