21.6. ペットショップの例のデシジョン (アジェンダグループ、グローバル変数、コールバック、および GUI 統合)

ペットショップの例のデシジョンセットでは、ルールでのアジェンダグループとグローバル変数の使用方法と、Red Hat Process Automation Manager ルールとグラフィカルユーザーインターフェイス (GUI) (この場合は、Swing ベースのデスクトップアプリケーション) の統合方法が分かります。また、この例では、コールバックを使用して実行中のデシジョンエンジンと通信し、ランタイム時に加えられたワーキングメモリー内の変更をもとに GUI を更新する方法を例示しています。

以下は、ペットショップの例の概要です。

  • 名前: petstore
  • Main クラス: (src/main/java 内の) org.drools.examples.petstore.PetStoreExample
  • モジュール: drools-examples
  • タイプ: Java アプリケーション
  • ルールファイル: (src/main/resources 内の) org.drools.examples.petstore.PetStore.drl
  • 目的: ルールアジェンダグループ、グローバル変数、コールバック、および GUI 統合を例示します。

ペットショップの例では、PetStoreExample.java クラス例を使用して (Swing イベントを処理する複数のクラスに加え)、以下のクラスを主に定義しています。

  • Petstore には main() メソッドが含まれます。
  • PetStoreUI は Swing ベースの GUI を作成して表示します。このクラスには複数の小さいクラスが含まれており、マウスボタンのクリックなど、さまざまな GUI イベントに主に対応します。
  • TableModel には表データが含まれています。このクラスは基本的に、Swing クラス AbstractTableModel を拡張する JavaBean です。
  • CheckoutCallback により、GUI がルールと対話できるようになります。
  • Ordershow は購入するアイテムを保持します。
  • Purchase には、注文の詳細と、購入する製品が保存されます。
  • Product は、販売可能な商品と価格の詳細を含む JavaBean です。

この例の Java コードはほぼ、プレーンな JavaBean か Swing ベースとなっています。Swing コンポーネントの詳細は、Creating a GUI with JFC/Swing の Java チュートリアルを参照してください。

ペットショップの例でのルール実行動作

他の例のデシジョンセットではファクトがすぐにアサートされて実行されるのに対し、ペットショップの例では、ユーザーの対話をもとに他のファクトが収集されるまでルールが実行します。このルールでは、コンストラクターで作成される PetStoreUI オブジェクトを使用してルールを実行し、Vector オブジェクトの stock を受けれ入れて商品を収集します。次に、この例では、以前の読み込まれたルールベースを含む CheckoutCallback クラスのインスタンスを使用します。

ペットショップの KIE コンテナーおよびファクト実行の設定

// KieServices is the factory for all KIE services.
KieServices ks = KieServices.Factory.get();

// Create a KIE container on the class path.
KieContainer kc = ks.getKieClasspathContainer();

// Create the stock.
Vector<Product> stock = new Vector<Product>();
stock.add( new Product( "Gold Fish", 5 ) );
stock.add( new Product( "Fish Tank", 25 ) );
stock.add( new Product( "Fish Food", 2 ) );

// A callback is responsible for populating the working memory and for firing all rules.
PetStoreUI ui = new PetStoreUI( stock,
                                new CheckoutCallback( kc ) );
ui.createAndShowGUI();

ルールを実行する Java コードは CheckoutCallBack.checkout() メソッドに含まれます。このメソッドは、ユーザーが UI で チェックアウト をクリックするとトリガーされます。

CheckoutCallBack.checkout() からのルール実行

public String checkout(JFrame frame, List<Product> items) {
    Order order = new Order();

    // Iterate through list and add to cart.
    for ( Product p: items ) {
        order.addItem( new Purchase( order, p ) );
    }

    // Add the JFrame to the ApplicationData to allow for user interaction.

    // From the KIE container, a KIE session is created based on
    // its definition and configuration in the META-INF/kmodule.xml file.
    KieSession ksession = kcontainer.newKieSession("PetStoreKS");

    ksession.setGlobal( "frame", frame );
    ksession.setGlobal( "textArea", this.output );

    ksession.insert( new Product( "Gold Fish", 5 ) );
    ksession.insert( new Product( "Fish Tank", 25 ) );
    ksession.insert( new Product( "Fish Food", 2 ) );

    ksession.insert( new Product( "Fish Food Sample", 0 ) );

    ksession.insert( order );

    // Execute rules.
    ksession.fireAllRules();

    // Return the state of the cart
    return order.toString();
}

このコード例では、2 つの要素を CheckoutCallBack.checkout() メソッドに渡します。1 つ目の要素は、GUI の一番下にある出力テキストのフレームを囲む Swing コンポーネント JFrame のハンドルです。2 つ目の要素は注文アイテムのリストで、GUI の右上のセクションにある Table エリアからの情報を保存する TableModel から取得します。

for ループは GUI からの注文アイテム一覧を JavaBean Order に変換します。これは、PetStoreExample.java ファイルにも含まれています。

今回の例では、データはすべて Swing コンポーネントに含まれており、ユーザーが UI の チェックアウト をクリックしない限り実行しないため、ルールはステートレスの KIE セッションで実行します。ユーザーが チェックアウト をクリックするたびに、リストの内容を Swing TableModel から KIE セッションのワーキングメモリーに移動し、ksession.fireAllRules() メソッドで実行します。

このコード内には、KieSession への呼び出しが 9 個あります。1 つ目は、KieContainer から新しい KieSession を作成します (この例では、main() メソッドの CheckoutCallBack クラスから KieContainer に渡されます)。次の 2 つの呼び出しは、ルールでグローバル変数として保持されるオブジェクト を 2 つ渡します (メッセージの書き込みに使用する Swing テキストエリアと Swing フレーム)。より多くの商品の情報を KieSession と注文リストに入力します。最後の呼び出しは、標準の fireAllRules() です。

ペットショップのルールファイルのインポート、グローバル変数、Java 関数

PetStore.drl ファイルには、さまざまな Java クラスをルールで利用できるように、標準のパッケージとインポートステートメントが含まれています。このルールファイルには、frametextArea などのように、ルール内で使用する グローバル変数 が含まれています。グローバル変数では、Swing コンポーネント JFrame と、setGlobal() メソッドを呼び出した Java コードにより以前に渡された JTextArea コンポーネントへの参照を保持します。ルールが実行するとすぐに失効するルールの標準変数とは異なり、グローバル変数は KIE セッションの有効期間中この値を保持します。これは、このグローバル変数の内容が、後続のすべてのルールで評価できることを意味します。

PetStore.drl パッケージ、インポート、およびグローバル変数

package org.drools.examples;

import org.kie.api.runtime.KieRuntime;
import org.drools.examples.petstore.PetStoreExample.Order;
import org.drools.examples.petstore.PetStoreExample.Purchase;
import org.drools.examples.petstore.PetStoreExample.Product;
import java.util.ArrayList;
import javax.swing.JOptionPane;

import javax.swing.JFrame;

global JFrame frame
global javax.swing.JTextArea textArea

PetStore.drl ファイルには、このファイル内のルールが使用する関数が 2 つも含まれています。

PetStore.drl Java 関数

function void doCheckout(JFrame frame, KieRuntime krt) {
        Object[] options = {"Yes",
                            "No"};

        int n = JOptionPane.showOptionDialog(frame,
                                             "Would you like to checkout?",
                                             "",
                                             JOptionPane.YES_NO_OPTION,
                                             JOptionPane.QUESTION_MESSAGE,
                                             null,
                                             options,
                                             options[0]);

       if (n == 0) {
            krt.getAgenda().getAgendaGroup( "checkout" ).setFocus();
       }
}

function boolean requireTank(JFrame frame, KieRuntime krt, Order order, Product fishTank, int total) {
        Object[] options = {"Yes",
                            "No"};

        int n = JOptionPane.showOptionDialog(frame,
                                             "Would you like to buy a tank for your " + total + " fish?",
                                             "Purchase Suggestion",
                                             JOptionPane.YES_NO_OPTION,
                                             JOptionPane.QUESTION_MESSAGE,
                                             null,
                                             options,
                                             options[0]);

       System.out.print( "SUGGESTION: Would you like to buy a tank for your "
                           + total + " fish? - " );

       if (n == 0) {
             Purchase purchase = new Purchase( order, fishTank );
             krt.insert( purchase );
             order.addItem( purchase );
             System.out.println( "Yes" );
       } else {
            System.out.println( "No" );
       }
       return true;
}

この 2 つの関数は以下のアクションを実行します。

  • doCheckout() は、チェックアウトするかどうかユーザーに尋ねるダイアログボックスを表示します。チェックアウトする場合は、フォーカスが checkout アジェンダグループに設定され、そのグループのルールを (今後) 実行できるようにします。
  • requireTank() は、水槽を購入するかどうかを確認するダイアログを表示します。購入する場合は、新しい水槽の Product がワーキングメモリーの注文リストに追加されます。
注記

この例では、効率化を図るため、すべてのルールと関数が同じルールファイルで実行しています。実稼働環境では、通常、ルールと関数を別のファイルに分けるか、静的な Java メソッドを構築して、import function my.package.name.hello などのインポート関数を使用し、ファイルをインポートします。

アジェンダグループを使用したペットショップルール

ペットショップの例のルールはほぼ、アジェンダグループを使用してルールの実行を制御しています。アジェンダグループを使用すると、デシジョンエンジンアジェンダを分割し、ルールのグループの実行を、詳細にわたり制御できるようになります。デフォルトでは、ルールはすべてアジェンダグループ MAIN に含まれています。agenda-group 属性を使用して、ルールに異なるアジェンダグループを指定できます。

最初は、ワーキングメモリーは、アジェンダグループ MAIN にフォーカスを当てます。アジェンダグループのルールは、グループがこのフォーカスを受けた場合のみ実行されます。setFocus() メソッドか、auto-focus ルール属性を使用してフォーカスを設定できます。auto-focus 属性を使用すると、ルールが一致してアクティベートされた場合のみ、ルールにアジェンダグループのフォーカスが自動的に当てられます。

ペットショップの例では、ルールに以下のアジェンダグループを使用します。

  • "init"
  • "evaluate"
  • "show items"
  • "checkout"

たとえば、同じルール "Explode Cart""init" のアジェンダグループを使用して、ショッピングカードのアイテムを起動して KIE セッションのワーキングメモリーに挿入するオプションが提供されるようにします。

ルール "Explode Cart"

// Insert each item in the shopping cart into the working memory.
rule "Explode Cart"
    agenda-group "init"
    auto-focus true
    salience 10
  when
    $order : Order( grossTotal == -1 )
    $item : Purchase() from $order.items
  then
    insert( $item );
    kcontext.getKnowledgeRuntime().getAgenda().getAgendaGroup( "show items" ).setFocus();
    kcontext.getKnowledgeRuntime().getAgenda().getAgendaGroup( "evaluate" ).setFocus();
end

このルールは、grossTotal がまだ計算されていない全注文に対して照合が行われます。購入アイテムごとに、順番に実行がループされます。

ルールは、アジェンダグループに関連する以下の機能を使用します。

  • agenda-group "init" はアジェンダグループの名前を定義します。この例では、グループにはルールが 1 つしかありません。ただし、Java コードもルール結果もこのグループにフォーカスされていないため、auto-focus の属性により、ルールが実行されるかが決まります。
  • このルールはアジェンダグループで唯一のルールですが、auto-focus true を使用して、fireAllRules() が Java コードから呼び出されると、必ず実行されるようにします。
  • kcontext…​.setFocus() は、"show items" および "evaluate" アジェンダグループにフォーカスを設定し、ルールを実行できるようにします。実際には、すべての項目をその順序でループしてメモリーに挿入し、各挿入の後に他のルールを実行します。

"show items" アジェンダグループには "Show Items" というルールだけが含まれます。KIE セッションのワーキングメモリーに現在含まれる注文で購入があるたびに、このルールを使用して、ルールファイルに定義した textArea 変数をもとに、GUI の下の部分にあるテキストエリアに詳細がロギングされます。

ルール "Show Items"

rule "Show Items"
    agenda-group "show items"
  when
    $order : Order()
    $p : Purchase( order == $order )
  then
   textArea.append( $p.product + "\n");
end

また、"evaluate" アジェンダグループにより、"Explode Cart" ルールからフォーカスを取得します。このアジェンダグループには、2 つのルール ("Free Fish Food Sample""Suggest Tank") が含まれます。この順番で実行されます。

ルール "Free Fish Food Sample"

// Free fish food sample when users buy a goldfish if they did not already buy
// fish food and do not already have a fish food sample.
rule "Free Fish Food Sample"
    agenda-group "evaluate" 1
  when
    $order : Order()
    not ( $p : Product( name == "Fish Food") && Purchase( product == $p ) ) 2
    not ( $p : Product( name == "Fish Food Sample") && Purchase( product == $p ) ) 3
    exists ( $p : Product( name == "Gold Fish") && Purchase( product == $p ) ) 4
    $fishFoodSample : Product( name == "Fish Food Sample" );
  then
    System.out.println( "Adding free Fish Food Sample to cart" );
    purchase = new Purchase($order, $fishFoodSample);
    insert( purchase );
    $order.addItem( purchase );
end

ルール "Free Fish Food Sample" は、以下の条件がすべて該当する場合のみ実行されます。

1
アジェンダグループ "evaluate" がルール実行で評価されている
2
ユーザーが魚の餌をまだ持っていない
3
ユーザーが無料の魚の餌サンプルをまだ持っていない
4
ユーザーが金魚を注文している

この注文ファクトが上記の要件すべてを満たす場合は、新しい商品 (Fish Food Sample) が作成され、ワーキングメモリーの注文に追加されます。

ルール "Suggest Tank"

// Suggest a fish tank if users buy more than five goldfish and
// do not already have a tank.
rule "Suggest Tank"
    agenda-group "evaluate"
  when
    $order : Order()
    not ( $p : Product( name == "Fish Tank") && Purchase( product == $p ) ) 1
    ArrayList( $total : size > 5 ) from collect( Purchase( product.name == "Gold Fish" ) ) 2
    $fishTank : Product( name == "Fish Tank" )
  then
    requireTank(frame, kcontext.getKieRuntime(), $order, $fishTank, $total);
end

ルール "Suggest Tank" は以下の条件がすべて該当する場合のみ実行されます。

1
ユーザーが水槽を注文していない
2
ユーザーが 6 匹以上注文した

このルールが実行すると、ルールファイルに定義されている requireTank() 関数が呼び出されます。この関数により、水槽を購入するかどうかを尋ねるダイアログが表示されます。購入する場合は、新しい水槽の Product がワーキングメモリーの注文リストに追加されます。ルールが requireTank() 関数を呼び出した場合は、このルールを使用して、関数に Swing GUI のハンドルが含まれるように、frame のグローバル変数を渡します。

ペットショップの例の "do checkout" ルールにはアジェンダグループや when 条件がないため、ルールは常に実行され、デフォルトの MAIN のアジェンダグループの一部とみなされます。

ルール "do checkout"

rule "do checkout"
  when
  then
    doCheckout(frame, kcontext.getKieRuntime());
end

このルールが実行されると、ルールファイルで定義されている doCheckout() 関数を呼び出します。この関数により、チェックアウトするかどうかをユーザーに尋ねるダイアログボックスが表示されます。チェックアウトする場合は、フォーカスが checkout アジェンダグループに設定され、そのグループのルールを (今後) 実行できるようにします。このルールで doCheckout() 関数を呼び出し、この変数に Swing GUI のハンドルが含まれるように frame グローバル変数を渡します。

注記

この例では、結果が想定どおりに実行されない場合のトラブルシューティングの方法を例示します。ルールの when ステートメントから条件を削除して、then ステートメントのアクションをテストし、アクションが正しく実行されることを検証します。

"checkout" アジェンダグループには、注文のチェックアウト処理、および割引の適用の 3 つのルール ("Gross Total""Apply 5% Discount"、および "Apply 10% Discount") が含まれています。

ルール "Gross Total"、"Apply 5% Discount"、および "Apply 10% Discount"

rule "Gross Total"
    agenda-group "checkout"
  when
    $order : Order( grossTotal == -1)
    Number( total : doubleValue ) from accumulate( Purchase( $price : product.price ),
                                                              sum( $price ) )
  then
    modify( $order ) { grossTotal = total }
    textArea.append( "\ngross total=" + total + "\n" );
end

rule "Apply 5% Discount"
    agenda-group "checkout"
  when
    $order : Order( grossTotal >= 10 && < 20 )
  then
    $order.discountedTotal = $order.grossTotal * 0.95;
    textArea.append( "discountedTotal total=" + $order.discountedTotal + "\n" );
end

rule "Apply 10% Discount"
    agenda-group "checkout"
  when
    $order : Order( grossTotal >= 20 )
  then
    $order.discountedTotal = $order.grossTotal * 0.90;
    textArea.append( "discountedTotal total=" + $order.discountedTotal + "\n" );
end

ユーザーがまだ総計を算出していない場合には、Gross Total で、商品の価格を累積して合計を出し、この合計を KIE セッションに渡して、textArea のグローバル変数を使用し、Swing JTextArea で合計を表示します。

総計が 10 から 20 (通貨単位) の場合は、"Apply 5% Discount" ルールで割引合計を計算し、KIE セッションに追加して、テキストエリアに表示します。

総計が 20 未満の場合は、"Apply 10% Discount" ルールで割引合計を計算し、KIE セッションに追加して、テキストエリアに表示します。

ペットショップ例の実行

他の Red Hat Process Automation Manager のデシジョン例と同じように、お使いの IDE で org.drools.examples.petstore.PetStoreExample クラスを Java アプリケーションとして実行し、ペットショップの例を実行します。

ペットショップの例を実行すると、Pet Store Demo GUI ウィンドウが表示されます。このウィンドウでは、購入可能な商品 (左上)、選択済み商品の空白のリスト (右上)、チェックアウト および リセット ボタン (真ん中)、空白のシステムメッセージエリア (下) が表示されます。

図21.14 起動後のペットショップ例の GUI

1 PetStore Start Screen

この例では、以下のイベントが発生して、この実行動作を確立します。

  1. main() メソッドがルールベースの実行と読み込みを終えていますが、ルールは実行していない。今のところ、これが、実行されたルールに関連する唯一のコードになります。
  2. 新しい PetStoreUI オブジェクトが作成され、後で使用できるようにルールベースにハンドルを渡している。
  3. さまざまな Swing コンポーネントが関数を実行し、最初の UI 画面が表示され、ユーザーの入力を待っている。

リストからさまざまな商品をクリックして、UI 設定をチェックできます。

図21.15 ペットショップ例の GUI のチェック

2 stock added to order list

ルールコードはまだ実行されていません。UI は Swing コードを使用してユーザーによるマウスクリックを検出し、選択済みの商品を TableModel オブジェクトに追加して、UI の右上隅に表示します。この例では、Model-View-Controller 設計パターンを紹介しています。

チェックアウト をクリックすると、ルールが以下の方法で実行されます。

  1. Swing クラスは Checkout がクリックされるまで待機して、(最終的に) CheckOutCallBack.checkout() メソッドを呼び出します。これにより、TableModel オブジェクト (UI の右上隅) から KIE セッションのワーキングメモリーにデータを挿入します。その後に、メソッドによりルールが実行されます。
  2. "Explode Cart" ルールは、auto-focus 属性を true に設定して最初に実行します。このルールは、カートの商品をすべて順にループしていき、商品がワーキングメモリーに含まれていることを確認し、アジェンダグループ "show Items""evaluate" に実行するオプションを提供します。このグループのルールは、カートのコンテンツをテキストエリア (UI の下) に追加して、魚の餌を無料で受け取る資格があるかどうかを評価し、また水槽購入の有無を尋ねるかどうかを決定します。

    図21.16 水槽の資格

    3 purchase suggestion
  3. 現在、他のアジェンダグループにフォーカスが当たっておらず、"do checkout" ルールは、デフォルトの MAIN アジェンダグループに含まれているため、次に実行されます。このルールは常に doCheckout() 関数を呼び出し、この関数によりチェックアウトをするかどうかが尋ねられます。
  4. doCheckout() 関数は、フォーカスを "checkout" アジェンダグループに設定し、そのグループ内のルールに、実行するオプションを提供します。
  5. "checkout" アジェンダグループ内のルールは、カート内の内容を表示し、適切な割引を適用します。
  6. Swing は、別の商品の選択 (およびもう一度ルールを実行) または GUI の終了のいずれかのユーザー入力を待ちます。

    図21.17 全ルールが実行された後のペットショップ例の GUI

    4 Petstore final screen

IDE コンソールでイベントのこのフローを例示するには、他の System.out 呼び出しを追加します。

IDE コンソールの System.out 出力

Adding free Fish Food Sample to cart
SUGGESTION: Would you like to buy a tank for your 6 fish? - Yes