Drools 5.0 入門 3D (agenda-group)

実行

 前の記事で優先度を用いてルールの発火を制御しましたが、こういった割引のどれかひとつが適用されるといった場合、割り引いたときに一番安くなる割引を選択するのが自然でしょう。ここでは、ルールの実行を制御するため、複数のルールをグループに分けるアジェンダグループ(agenda-group)を用いてみましょう。

サンプルの仕様とプログラムの考え方

 前の記事であげた仕様をもう少し拡張してみましょう。

 [仕様]

  会員には、シルバー、ゴールド、プラチナがあり、

  1.シルバー会員は、一律 5%の割引がある

  2.ゴールド会員は、一律 7%の割引がある

  3.プラチナ会員は、一律 10%の割引がある

  4.5000円以上お買い上げの場合一律に500円を割引する

 さて、ここで前と同様12000円の買い物をシルバー会員がしたとしましょう。プログラム的には、最初 12000円の Salesファクトを insert します。次にそのファクトに対して割引を適用します。前は当ファクトを直接 update していましたが、今回は割引を適用した中でもっとも安いファクトを選択する必要があるので、割引を適用した結果を新たなファクトにして insert し、割引候補の Salesファクトを作っておきましょう。そしてその割引候補の中から最安値のファクトを選択して採用し結果を表示します。以上をまとめてみましょう。

 A.定価での Sales ファクトを insert する。

 B.適用可能な割引すべてを適用してみて、それぞれに新たな Salesファクトを insert する。これらが割引の候補となる。

 C.Salesファクトの中で最安値のファクトを採用し、結果を表示する。

となります。すると、ルールとしてプログラムに現われてくるのは、さまざまな割引のルール、最安値のファクトを求めるルール、結果を表示するルールとなるであろうことは容易に想像がつくことと思います。

 ただ、これらルールを単にフラットに並べるだけでは実行順序が制御できないので、たとえば上のB.で割引候補がすべて出尽くさないうちに最安値のルールが発火して結果を出してしまうかもしれません。どうしても実行の制御をする必要があります。どういった方策があるでしょうか。

 もちろん前の記事で出てきた優先度を使うこともできます。割引適用のルールに対して、最安値を求めるルールの優先度を下げておけば先に割引適用のルールが走って、その後に最安値を求めるルールが走るようになります。しかし、今回の場合B.で割引を適用し、C.で最安値を求め結果表示、とフェーズに明確に分かれているので、そのフェーズを意識できる方策の方がよいでしょう。ここで使えるのがアジェンダグループ(agenda-group)です。

 

  アジェンダグループ(agenda-group)

 アジェンダグループは、ルールの実行を制御するためルールをグルーピングします。たとえばここに「A」という名称のアジェンダグループがあるとしましょう。そして仮にその「A」というアジェンダグループが実行対象になっている(フォーカス(focus)されているという)としましょう。その場合、原則としてアジェンダグループ「A」に含まれるルールのみが発火します(ここで「原則として」と付け加えたのは、他のアジェンダグループでも auto-focus という属性がついたルールの場合、条件がマッチした場合自動的に制御を自分のアジェンダグループに移します。したがって、ここでは他のアジェンダグループに auto-focus 属性を持ったルールがないとしておきましょう)
 したがって、アジェンダグループ「A」のルールが発火し尽されないと、次のグループに行かないことになります。この性質を利用してフェーズに分かれたルール実行の制御を行うことができます。

 このアジェンダグループ、あえてルールに設定していないときは「MAIN」というアジェンダグループがデフォルトでついてきます。今までのサンプルプログラムでは、アジェンダグループを設定していませんでしたが、これはすべてアジェンダグループ「MAIN」の中で行われていたことだったのですね。

 さて、アジェンダグループのフォーカスが変わるのはどういった場合でしょうか。

 1.setFocus()で明示的にフォーカスを設定した場合
 2.ルールの属性 auto-focus で自動的にフォーカスを取得する場合
 3.アジェンダグループ内のルールが発火しつくされ、前にフォーカスが当たっていたアジェンダグループに戻る場合

となります。1.については後のサンプルプログラムで具体的なプログラムを参照することとして、ここでは3.について説明しましょう。アジェンダグループのフォーカスは実はスタックの形になっており、setFocus() や auto-focus でアジェンダグループを設定した場合、前のアジェンダグループの上に積み上げられます。Droolsの場合常に「MAIN」という基盤となるアジェンダグループがあり、フォーカスがあたるとその上にアジェンダグループが積みあがっていきます。そしてそのアジェンダグループが発火しつくされたとき初めてそのアジェンダグループは取り出されて、その下のアジェンダグループにフォーカスが移ります。次は、このアジェンダグループを使ったサンプルプログラムを見ていきましょう。

 

  サンプルプログラム

 まずは、上にあげていた手順

 A.定価での Sales ファクトを insert する。

 B.適用可能な割引すべてを適用してみて、それぞれに新たな Salesファクトを insert する。これらが割引の候補となる。

 C.Salesファクトの中で最安値のファクトを採用し、結果を表示する。

に沿って、アジェンダグループを考えていきましょう。A はインサートするだけですので除いておいて、B は「割引候補生成」といった名のアジェンダグループにします。さらに C は、最安値を求めて表示するので「最安値表示」という名前のアジェンダグループにでもしておきましょう。

 アジェンダグループのフォーカスはスタックになっていることは上で書きました。デフォルトのアジェンダグループは「MAIN」ですので、まず最初に「最安値表示」グループにフォーカスを設定し、続いて「割引候補生成」グループにフォーカスを設定しておきましょう。するとアジェンダグループのスタックは上から「割引候補生成」、「最安値表示」、「MAIN」となります。
 これで準備ができましたのでルールを実行してみましょう。最初は「割引候補生成」のアジェンダグループにフォーカスがあたります。「割引候補生成」で候補が出し尽くされると、「割引候補生成」のグループは降ろされて「最安値表示」に制御が戻ります。このようにしてルール実行が進んでいきます。

 では、具体的なプログラムを見ていきます。 メインプログラムは前の記事とほとんど変わりませんが、会員をシルバー、ゴールド、プラチナに分けたことと、ステータスとして、(割引)確定という意味のFIXEDを加えたこと、そして、アジェンダグループのフォーカス「最安値表示」「割引候補生成」を設定しているところでしょうか。

(また、コンソールへの赤字の表示がわずらわしいので、イベントリスナーは今回削除してあります。必要があれば、前のソースを参考に加えて実行してください)

DroolsTest3.java

papackage jp.co.iluminado.example;

import java.util.Collection;

import org.drools.KnowledgeBase;
import org.drools.KnowledgeBaseFactory;
import org.drools.builder.KnowledgeBuilder;
import org.drools.builder.KnowledgeBuilderFactory;
import org.drools.builder.ResourceType;
import org.drools.definition.KnowledgePackage;
import org.drools.io.ResourceFactory;
import org.drools.logger.KnowledgeRuntimeLogger;
import org.drools.logger.KnowledgeRuntimeLoggerFactory;
import org.drools.runtime.StatefulKnowledgeSession;

/**
 * This is a sample file to launch a rule package from a rule source file.
 */
public class DroolsTest3 {

    public static final void main(final String[] args) throws Exception {
        final KnowledgeBuilder kbuilder = KnowledgeBuilderFactory
                .newKnowledgeBuilder();

        // ルールファイルをコンパイルしてKnowledgeBuilderへ追加する
        kbuilder.add(ResourceFactory.newClassPathResource(“DroolsTest3.drl”,
                DroolsTest1.class), ResourceType.DRL);

        // 上記コンパイル時のエラー処理
        if (kbuilder.hasErrors()) {
            System.out.println(kbuilder.getErrors().toString());
            throw new RuntimeException(“Unable to compile \”DroolsTest3.drl\”.”);
        }

        // KnowledgeBuilderからコンパイル済みのパッケージを取得する
        final Collection<KnowledgePackage> pkgs = kbuilder
                .getKnowledgePackages();

        // 取得したパッケージをKnowledgebaseに追加する(パッケージのデプロイ)
        final KnowledgeBase kbase = KnowledgeBaseFactory.newKnowledgeBase();
        kbase.addKnowledgePackages(pkgs);

        // Statefulセッションを作成
        final StatefulKnowledgeSession ksession = kbase
                .newStatefulKnowledgeSession();

        // Auditログのセット
        KnowledgeRuntimeLogger logger = KnowledgeRuntimeLoggerFactory
                .newFileLogger(ksession, “log/DroolsTest3”);

        // ルールの実行
        final Sales sale = new Sales(12000, Sales.NOT_APPLIED, Sales.SILVER);
       
        ksession.insert(sale);
        ksession.getAgenda().getAgendaGroup(“最安値表示”).setFocus();
        ksession.getAgenda().getAgendaGroup(“割引候補生成”).setFocus();
        ksession.fireAllRules();

        // Auditログを閉じる
        logger.close();

        // Statefulセッションなので、実行が終わったところでdisposeして領域を開放する
        ksession.dispose();
    }

    public static class Sales {
       
        public static final int NOT_APPLIED = 0;  // 未適用
        public static final int APPLIED = 1;      // 仮適用
        public static final int FIXED = 2;        // 確定

        public static final int NONMEMBER = 0;    // 非会員
        public static final int SILVER = 1;       // シルバー会員
        public static final int GOLD = 2;         // ゴールド会員
        public static final int PLATINUM = 3;     // プラチナ会員

        private long sales;   // 売り上げ
        private int status;   // 状態(未適用、仮適用、確定)
        private int member;   // 会員

        public Sales() {
        }

        public Sales(long sales,
                int status,
                int member) {
            this.sales = sales;
            this.status = status;
            this.member = member;
        }

        public int getStatus() {
            return this.status;
        }

        public void setStatus(int status) {
            this.status = status;
        }

        public void setSales(long sales) {
            this.sales = sales;
        }

        public long getSales() {
            return sales;
        }

        public int getMember() {
            return member;
        }

        public void setMember(int member) {
            this.member = member;
        }

    }

}

次にルールファイルを見てみましょう。アジェンダグループは MAIN 以外に2つになります。

1.アジェンダグループ「割引候補生成」 (ルール4つ)
 ・ルール 「シルバー会員割引」
 ・ルール 「ゴールド会員割引」
 ・ルール 「プラチナ会員割引」
 ・ルール 「キャンペーン割引(500円引きルール)」

2.アジェンダグループ「最安値表示」 (ルール2つ)
 ・ルール 「最安値」
 ・ルール 「結果表示」

DroolsTest3.drl 

package jp.co.iluminado.example

import jp.co.iluminado.example.DroolsTest3.Sales;

rule “シルバー会員割引”
    agenda-group “割引候補生成”
    when
        $s : Sales( status == Sales.NOT_APPLIED,
                    $salesValue : sales,
                    member == Sales.SILVER )
    then
        insert( new Sales((long)($salesValue * 0.95), Sales.APPLIED, Sales.SILVER ) );
        System.out.println( “シルバー会員割引ルールが発火しました” );
end

rule “ゴールド会員割引”
    agenda-group “割引候補生成”
    when
        $s : Sales( status == Sales.NOT_APPLIED,
                    $salesValue : sales,
                    member == Sales.GOLD )
    then
        insert( new Sales((long)($salesValue * 0.93), Sales.APPLIED, Sales.GOLD ) );
        System.out.println( “ゴールド会員割引ルールが発火しました” );
end

rule “プラチナ会員割引”
    agenda-group “割引候補生成”
    when
        $s : Sales( status == Sales.NOT_APPLIED,
                    $salesValue : sales,
                    member == Sales.PLATINUM )
    then
        insert( new Sales((long)($salesValue * 0.9), Sales.APPLIED, Sales.PLATINUM ) );
        System.out.println( “プラチナ会員割引ルールが発火しました” );
end

rule “キャンペーン割引”
    agenda-group “割引候補生成”
    when
        $s : Sales( status == Sales.NOT_APPLIED,
              $salesValue : sales >= 5000,
              $memberType : member )
    then
        insert( new Sales($salesValue – 500, Sales.APPLIED, $memberType ) );
        System.out.println( “500円引きルールが発火しました” );
end

rule “最安値”
    agenda-group “最安値表示”
    when
        $s1 : Sales( status != Sales.FIXED,
                     $salesValue1 : sales )
        forall ( Sales( sales >= $salesValue1 ))
    then
        $s1.setStatus( Sales.FIXED );
        update ( $s1 );
        System.out.println(  $s1.getSales() );
        System.out.println( “最安値ルールが発火しました” );
end

rule “結果表示”
    agenda-group “最安値表示”
    when
        $s : Sales( status == Sales.FIXED)
    then
        System.out.println(  $s.getSales() );
        System.out.println( “結果表示ルールが発火しました” );
end

このうち、割引候補を作るルールについては、前の記事とほとんど変わらないので説明の必要はないでしょう。ただ候補を生成するということで insert を使って、新たなファクトを作っています。

 さて、「最安値表示」のアジェンダグループを見てみましょう。ルールは2つ。「最安値」と「結果表示」です。このうち「結果表示」は前の記事とほとんど同じなので説明は省略します。「最安値」のルールについては、まず LHS から見ていきましょう。変数 $s1 が bind されている(束縛されている、紐付けられている) Sales ファクトでは、まだ割引が確定していないファクトを選択してきています。次に forall がついている2番目の条件を見てみましょう。forall は、すべてのという意味で、forall がかかっているのは Sales( sales >= $salesValue1 ) ファクトです。これを解釈してみると「 sales >= $salesValue1 であるようなすべての Sales ファクトについて」 となります。ここで $salesValue1 に着目してみましょう。$salesValue1 の値は、2番目の条件からすると、現在ワーキングメモリに存在するすべての Sales ファクトの sales の値よりも小さいかまたは等しいような値です。一方、1番目の条件から $salesValue1 は、(確定していない) Sales ファクトのうちのどれかが持っている sales の値となります。これはすなわち $salesValue1 が Sales ファクトの sales の値の最小値(最安値)をあらわしていることにほかなりません。この LHS は最小値の定義とも言え、Droolsの宣言的な性質をよくあらわしているのではないでしょうか。

(forall について、述語論理を学んだことがある方には、∀(全称記号) にあたると考えてもらえばよいと思います。もちろん ∃(存在記号) にあたるものもあって、次に説明する exists にあたります)

おまけとして、確定した後に残ったいらない割引候補の Sales ファクトを消していく ガーベジコレクター(?) のルールをつけておきましょうか。

DroolsTest3.drl (追加)

 
rule “ガーベジコレクター”
    agenda-group “最安値表示”
    salience 100
    when
        $s : Sales( status == Sales.APPLIED)
        exists Sales( status == Sales.FIXED )
    then
        retract ( $s );   
        System.out.println( “不要な Sales ファクトを削除しました” );
end

 最安値表示のアジェンダグループの中で、優先度( salience )を100として、LHSの条件が合えば最優先で実行されるようにします。LHSは、exists を用いて「確定した Sales ファクトが存在すれば」 ということをあらわしています。また、割引候補として最終的に採用されなかった Sales ファクト(status == Sales.APPLIED) は、RHSで削除( retract )されます。割引が確定した瞬間に当ガーベジコレクタールールが発火し採用されなかった候補を削除していくというようなイメージでしょうか。

 

 それでは、動かしてみましょう。

実行

シルバー会員で、12000円の買い物をすると通常考えるとシルバー会員割引が適用されて、12000円の5%引きの11400円になっていることがわかります。

また、 たとえばシルバー会員が8000円の買い物をすると、500円引きのキャンペーン割引が適用されることも、最初に insert するファクトを変更することで確かめることができるでしょう。

また、さらに割引ルールが追加されたときには、「割引候補生成」グループに追加するだけでよいので、自由に追加して試してみてください。