9.6. ペットショップの例のデシジョン (アジェンダグループ、グローバル変数、コールバック、および GUI 統合)
ペットショップの例のデシジョンセットでは、ルールでのアジェンダグループとグローバル変数の使用方法と、Red Hat Decision 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 クラスをルールで利用できるように、標準のパッケージとインポートステートメントが含まれています。このルールファイルには、frame
、textArea
などのように、ルール内で使用する グローバル変数 が含まれています。グローバル変数では、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"
は、以下の条件がすべて該当する場合のみ実行されます。
この注文ファクトが上記の要件すべてを満たす場合は、新しい商品 (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"
は以下の条件がすべて該当する場合のみ実行されます。
このルールが実行すると、ルールファイルに定義されている 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 Decision Manager のデシジョン例と同じように、お使いの IDE で org.drools.examples.petstore.PetStoreExample
クラスを Java アプリケーションとして実行し、ペットショップの例を実行します。
ペットショップの例を実行すると、Pet Store Demo
GUI ウィンドウが表示されます。このウィンドウでは、購入可能な商品 (左上)、選択済み商品の空白のリスト (右上)、チェックアウト および リセット ボタン (真ん中)、空白のシステムメッセージエリア (下) が表示されます。
図9.14 起動後のペットショップ例の GUI
この例では、以下のイベントが発生して、この実行動作を確立します。
-
main()
メソッドがルールベースの実行と読み込みを終えていますが、ルールは実行していない。今のところ、これが、実行されたルールに関連する唯一のコードになります。 -
新しい
PetStoreUI
オブジェクトが作成され、後で使用できるようにルールベースにハンドルを渡している。 - さまざまな Swing コンポーネントが関数を実行し、最初の UI 画面が表示され、ユーザーの入力を待っている。
リストからさまざまな商品をクリックして、UI 設定をチェックできます。
図9.15 ペットショップ例の GUI のチェック
ルールコードはまだ実行されていません。UI は Swing コードを使用してユーザーによるマウスクリックを検出し、選択済みの商品を TableModel
オブジェクトに追加して、UI の右上隅に表示します。この例では、Model-View-Controller 設計パターンを紹介しています。
チェックアウト をクリックすると、ルールが以下の方法で実行されます。
-
Swing クラスは Checkout がクリックされるまで待機して、(最終的に)
CheckOutCallBack.checkout()
メソッドを呼び出します。これにより、TableModel
オブジェクト (UI の右上隅) から KIE セッションのワーキングメモリーにデータを挿入します。その後に、メソッドによりルールが実行されます。 "Explode Cart"
ルールは、auto-focus
属性をtrue
に設定して最初に実行します。このルールは、カートの商品をすべて順にループしていき、商品がワーキングメモリーに含まれていることを確認し、アジェンダグループ"show Items"
と"evaluate"
に実行するオプションを提供します。このグループのルールは、カートのコンテンツをテキストエリア (UI の下) に追加して、魚の餌を無料で受け取る資格があるかどうかを評価し、また水槽購入の有無を尋ねるかどうかを決定します。図9.16 水槽の資格
-
現在、他のアジェンダグループにフォーカスが当たっておらず、
"do checkout"
ルールは、デフォルトのMAIN
アジェンダグループに含まれているため、次に実行されます。このルールは常にdoCheckout()
関数を呼び出し、この関数によりチェックアウトをするかどうかが尋ねられます。 -
doCheckout()
関数は、フォーカスを"checkout"
アジェンダグループに設定し、そのグループ内のルールに、実行するオプションを提供します。 -
"checkout"
アジェンダグループ内のルールは、カート内の内容を表示し、適切な割引を適用します。 Swing は、別の商品の選択 (およびもう一度ルールを実行) または GUI の終了のいずれかのユーザー入力を待ちます。
図9.17 全ルールが実行された後のペットショップ例の GUI
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