1. 序章

この記事では、新しいオープンソースのJavaルールエンジンであるEvetteの最初の実践的な概要を説明します。

歴史的に、 Evrete は、 Drools RuleEngineの軽量な代替品としてとして開発されてきました。 Java Rule Engine仕様に完全に準拠しており、大量のデータを処理するためのいくつかの調整と機能を備えた従来の前向き連鎖RETEアルゴリズムを使用しています。

Java 8以降が必要で、依存関係がなく、JSONおよびXMLオブジェクトをシームレスに操作し、ルールの条件およびアクションとして機能インターフェイスを許可します

そのコンポーネントのほとんどは、サービスプロバイダーインターフェイスを介して拡張可能であり、これらのSPI実装の1つは、注釈付きのJavaクラスを実行可能なルールセットに変換します。 今日もやってみます。

2. Mavenの依存関係

Javaコードにジャンプする前に、プロジェクトのpom.xmlevrete-coreMaven依存関係を宣言する必要があります。

<dependency>
    <groupId>org.evrete</groupId>
    <artifactId>evrete-core</artifactId>
    <version>2.1.04</version>
</dependency>

3. ユースケースシナリオ

紹介をわかりやすくするために、私たちが中小企業を経営していて、今日が会計年度の終わりであり、顧客ごとの総売上高を計算したいとします。

ドメインデータモデルには、CustomerInvoiceの2つの単純なクラスが含まれます。

public class Customer {
    private double total = 0.0;
    private final String name;

    public Customer(String name) {
        this.name = name;
    }

    public void addToTotal(double amount) {
        this.total += amount;
    }
    // getters and setters
}
public class Invoice {
    private final Customer customer;
    private final double amount;

    public Invoice(Customer customer, double amount) {
        this.customer = customer;
        this.amount = amount;
    }
    // getters and setters
}

ちなみに、エンジンはそのままで Java Records をサポートし、を使用すると、開発者は任意のクラスプロパティを機能インターフェイスとして宣言できます

この紹介の後半で、請求書と顧客のコレクションが提供されます。ロジックでは、データを処理するために2つのルールが必要であることが示唆されています。

  • 最初のルールは、各顧客の総売上高をクリアします
  • 2番目のルールは、請求書と顧客を照合し、各顧客の合計を更新します。

繰り返しになりますが、これらのルールは、流動的なルールビルダーインターフェイスを使用して、注釈付きのJavaクラスとして実装します。 RuleBuilderAPIから始めましょう。

4. Rule BuilderAPI

ルールビルダーは、ルールのドメイン固有言語(DSL)を開発するための中心的な構成要素です。 開発者は、Excelソース、プレーンテキスト、またはその他のDSL形式をルールに変換する必要があるものを解析するときにこれらを使用します。

ただし、私たちの場合は、主に、開発者のコードにルールを直接埋め込む機能に関心があります。

4.1. ルールセット宣言

ルールビルダーを使用すると、流暢なインターフェイスを使用して2つのルールを宣言できます。

KnowledgeService service = new KnowledgeService();
Knowledge knowledge = service
  .newKnowledge()
  .newRule("Clear total sales")
  .forEach("$c", Customer.class)
  .execute(ctx -> {
      Customer c = ctx.get("$c");
      c.setTotal(0.0);
  })
  .newRule("Compute totals")
  .forEach(
      "$c", Customer.class,
      "$i", Invoice.class
  )
  .where("$i.customer == $c")
  .execute(ctx -> {
      Customer c = ctx.get("$c");
      Invoice i = ctx.get("$i");
      c.addToTotal(i.getAmount());
  });

最初に、 KnowledgeService のインスタンスを作成しました。これは、基本的に共有エグゼキューターサービスです。 通常、アプリケーションごとにKnowledgeServiceのインスタンスが1つ必要です

結果のKnowledgeインスタンスは、2つのルールのコンパイル済みバージョンです。 これは、ソースを一般的にコンパイルするのと同じ理由で行いました。正確性を確保し、コードをより高速に起動するためです。

Droolsルールエンジンに精通している人は、同じロジックの次のDRLバージョンと意味的に同等のルール宣言を見つけるでしょう。

rule "Clear total sales"
  when
    $c: Customer
  then
    $c.setTotal(0.0);
end

rule "Compute totals"
  when
    $c: Customer
    $i: Invoice(customer == $c)
  then
    $c.addToTotal($i.getAmount());
end

4.2. テストデータのモック

ルールセットを3人の顧客と、ランダムな金額で顧客間でランダムに配布される10万件の請求書でテストします。

List<Customer> customers = Arrays.asList(
  new Customer("Customer A"),
  new Customer("Customer B"),
  new Customer("Customer C")
);

Random random = new Random();
Collection<Object> sessionData = new LinkedList<>(customers);
for (int i = 0; i < 100_000; i++) {
    Customer randomCustomer = customers.get(random.nextInt(customers.size()));
    Invoice invoice = new Invoice(randomCustomer, 100 * random.nextDouble());
    sessionData.add(invoice);
}

これで、 sessionData 変数には、ルールセッションに挿入するCustomerインスタンスとInvoiceインスタンスが混在しています。

4.3. ルールの実行

ここで行う必要があるのは、100,003個のオブジェクトすべて(10万件の請求書と3人の顧客)を新しいセッションインスタンスにフィードし、その fire()メソッドを呼び出すことです。

knowledge
  .newStatelessSession()
  .insert(sessionData)
  .fire();

for(Customer c : customers) {
    System.out.printf("%s:\t$%,.2f%n", c.getName(), c.getTotal());
}

最後の行は、各顧客の結果の販売量を印刷します。

Customer A:	$1,664,730.73
Customer B:	$1,666,508.11
Customer C:	$1,672,685.10

5. 注釈付きJavaルール

前の例は期待どおりに機能しますが、ライブラリを仕様に準拠させることはできません。これにより、ルールエンジンは次のことを期待します。

  • 「ビジネスまたはアプリケーションロジックを外部化することにより、宣言型プログラミングを促進します。」
  • 「文書化されたファイル形式またはツールを含めてルールを作成し、アプリケーションの外部でルール実行セットを作成します。」

簡単に言えば、準拠したルールエンジンは、ランタイム外で作成されたルールを実行できなければならないことを意味します。

また、Evreteの注釈付きJavaルール拡張モジュールはこの要件に対応しています。 このモジュールは、実際には「ショーケース」DSLであり、ライブラリのコアAPIのみに依存しています。

それがどのように機能するか見てみましょう。

5.1. インストール

注釈付きのJavaルールは、 Evreteのサービスプロバイダーインターフェイス(SPI)の1つの実装であり、追加の evrete-dsl- javaMaven依存関係が必要です。

<dependency>
    <groupId>org.evrete</groupId>
    <artifactId>evrete-dsl-java</artifactId>
    <version>2.1.04</version>
</dependency>

5.2. ルールセット宣言

アノテーションを使用して同じルールセットを作成しましょう。 クラスやバンドルされたjarではなく、プレーンなJavaソースを選択します。

public class SalesRuleset {

    @Rule
    public void rule1(Customer $c) {
        $c.setTotal(0.0);
    }

    @Rule
    @Where("$i.customer == $c")
    public void rule2(Customer $c, Invoice $i) {
        $c.addToTotal($i.getAmount());
    }
}

このソースファイルには任意の名前を付けることができ、Javaの命名規則に従う必要はありません。 エンジンはソースをそのままコンパイルします。次のことを確認する必要があります。

  • ソースファイルには、必要なすべてのインポートが含まれています
  • サードパーティの依存関係とドメインクラスはエンジンのクラスパスにあります

次に、外部の場所からルールセット定義を読み取るようにエンジンに指示します。

KnowledgeService service = new KnowledgeService();
URL rulesetUrl = new URL("ruleset.java"); // or file.toURI().toURL(), etc
Knowledge knowledge = service.newKnowledge(
  "JAVA-SOURCE",
  rulesetUrl
);

以上です。 コードの残りの部分がそのまま残っている場合、同じ3人の顧客がランダムな販売量とともに印刷されます。

この特定の例に関するいくつかの注意事項:

  • プレーンなJava(“ JAVA -SOURCE” 引数)からルールを構築することを選択したため、エンジンはメソッド引数からファクト名を推測できます。
  • .classまたは.jarソースを選択した場合、メソッド引数には@Factアノテーションが必要でした。
  • エンジンは、メソッド名でルールを自動的に並べ替えました。 名前を入れ替えると、リセットルールは以前に計算されたボリュームをクリアします。 その結果、販売量はゼロになります。

5.3. 使い方

新しいセッションが作成されるたびに、エンジンはそれを注釈付きルールクラスの新しいインスタンスと結合します。 基本的に、これらのクラスのインスタンスはセッション自体と見なすことができます。

したがって、クラス変数が定義されている場合、ルールメソッドにアクセスできるようになります。

条件メソッドを定義するか、新しいフィールドをメソッドとして宣言した場合、それらのメソッドはクラス変数にもアクセスできます。

通常のJavaクラスと同様に、このようなルールセットは拡張、再利用、およびライブラリへのパックが可能です。

5.4. 追加機能

簡単な例は紹介に適していますが、多くの重要なトピックを残しています。 アノテーション付きJavaルールの場合、次のものが含まれます。

  • クラスメソッドとしての条件
  • クラスメソッドとしての任意のプロパティ宣言
  • フェーズリスナー、継承モデル、およびランタイム環境へのアクセス
  • そして、何よりも、条件からアクション、フィールド定義に至るまで、クラスフィールドを全面的に使用します。

6. 結論

この記事では、新しいJavaルールエンジンを簡単にテストしました。 重要なポイントは次のとおりです。

  1. 他のエンジンは、すぐに使用できるDSLソリューションとルールリポジトリを提供するのに優れている場合があります。
  2. Evrete は、代わりに開発者が任意のDSLを構築できるように設計されています。
  3. Javaでルールを作成するために使用される人は、「注釈付きJavaルール」パッケージがより良いオプションであると思うかもしれません。

この記事では取り上げられていないが、ライブラリのAPIで言及されている他の機能について言及する価値があります。

  • 任意のファクトプロパティの宣言
  • Java述語としての条件
  • ルールの条件とアクションをオンザフライで変更する
  • 紛争解決技術
  • ライブセッションに新しいルールを追加する
  • ライブラリの拡張性インターフェイスのカスタム実装

公式ドキュメントはhttps://www.evrete.org/docs/にあります。

コードサンプルと単体テストは、GitHubから入手できます。