価格データモデルの強化
4 タスク
45 分
シナリオ
FSGは当初、イベントの見積もりプロセスをサポートするために、Rule-Declare-Expressionとデシジョンテーブルを使用して、イベントやオプションのホテルサービスなど、価格設定可能なさまざまな項目の価格とコストを計算していました。 Rule-Declare-Expressionの数は多すぎると考えられており、駐車場料金の徴収や必要であればシャトルサービスの提供など、他の価格設定可能な項目を追加すると増えてしまうかもしれません。
FSGは、eコマース(ショッピングカート)ソフトウェアで使われているような価格設定モデルのアプローチを導入することにしました。 このアプローチでは、Pricingデータタイプが定義されていて、「価格設定可能」の価格モデルに基づいて価格を計算するための基本的なプロパティが含められます。 最も基本的な2つの価格モデルは、単位と量です。
FSGは、Pricingデータタイプへの入力の変更に呼応するRule-Declare-OnChangeルールを実装し、Rule-Declare-Expressionは廃止しました。 ただし、このソリューションもデシジョンテーブルに依存していました。 Rule-Declare-Expressionを廃止する前に、FSG-Data-Pricingクラスレベルで価格設定モデル固有のルールを持っているという問題がありました。 より適切な設計は、パターン継承とポリモーフィズムを使用して、価格設定モデル固有のルールをそのクラス( FSG-Data-Pricing-Volumeなど)に移動させることでした。 価格設定モデルに基づく特殊化アプローチとして状況設定を使用することは、早い段階で却下されました。 価格とコストの計算方法の違いなど、FSG-Data-Pricingレベルのきめ細かな目的のために状況設定を使用するというオプションはとりあえず保留になりました。
デシジョンテーブルを廃止することが望ましい理由の一つは、新しい価格設定可能項目を追加する際に、価格設定可能項目の価格モデルを指定するデシジョンテーブルに変更を加える必要があることです。 価格設定可能項目の価格設定方法を定義するデシジョンテーブルにも変更を加える必要がありますが、これは開放/閉鎖の原則に違反していました。 価格とコストで異なる価格設定モデルを使用することを指定する方法もありませんでした。 FSG COEは、デシジョンテーブルをさらに追加したくはなく、逆に、デシジョンテーブルの廃止を望んでいました。
また、FSG-Data-Pricingのデータトランスフォーム再計算に入力を渡し、対応する表示プロパティの値を設定し、総価格と総費用のその時点までの合計を更新して、予想利益を計算するという問題もありました。 各価格設定可能項目に固有のRecalculateアクティビティ(たとえばRecalculateHotel)があり、これも開放/閉鎖の原則に違反していました。
FSG COEは、現在の「価格設定エンジン」の状態から、「開放/閉鎖の原則」に沿った方法に移行するには分析が必要であることを知っています。 これらを前提として、今回のチャレンジを開始します。
以下の表は、チャレンジに必要なログイン情報をまとめたものです。
ロール | ユーザー名 | パスワード |
---|---|---|
Admin | COE@FSG | rules |
Admin | Admin@FSGPricingTest | rules |
詳細なタスク
1 Pegaライブデータへの移行
FSF COEでは、ソリューション全体が過剰に複雑にならないように、各パーツをできるだけシンプルにする必要があることを認識していました。 複雑さは、変数を追加するごとに増していきます。 最初の手順は、PricingModelのデシジョンテーブルをPriceableデータタイプに変換することでした。 データタイプ名として「Pricing Model」は使用されませんでした。 このデータタイプの目的は、価格設定モデルが何であるかをカプセル化することではありません。 そうではなく、このデータタイプの目的は、価格設定対象項目のIDと、その価格を導き出すために使用する価格設定モデルをカプセル化することです。
最良の方法は、まず、FSG-Data-Priceableクラスの定義を最小限に留めることでした。 このクラスにはPricingTypeプロパティを付随させることができ、この場合、PricingTypeの値は「Price」または「Cost」です。 この方法では、複数の行が同じItemIDを持つことになり複雑さが増します。 PricingTypeは、ItemIDが渡されるすべてのルールのパラメーターとして渡される必要があります。 単一のキーを持つよりも、Priceableというデータタイプを持つ方が理にかなっています。
ItemIDの値は、価格を表すItemIDに「Cost」を付加することで、コストを表すかどうかを示すことができます。 VolumeIncludesQtyプロパティは、「Volume」という価格設定モデルにしか適用されないにもかかわらず、Priceableクラスに属しています。 プロパティには、「true」の場合は、表示されているとおりの価格になると設定されています。 一方で「false」の場合は、価格を導き出すのに使用する数量と価格との乗算を行います。
Priceable
ItemID(キー) |
PricingModel |
VolumeIncludesQty |
Pricingテーブルはすでにあります。 このテーブルのどの列も、本質的に過去の情報です。 このテーブルには、レコードのオーナーとなるケース(Reference)、Priceableの内容、計算されたPrice、Priceを計算するために使用された入力値、つまりQuantity、Bit、DiscountFactor、PricingModel、そして該当する場合はVolumeIncludesQtyが記録されています。
Pricing
ItemID(キー) |
Reference(キー) |
Price |
Quantity |
Bit |
DiscountFactor |
PricingModel(キー) |
VolumeIncludesQty |
当初「Pricing」と名付けられたデシジョンテーブルは、FSG-Data-Pricing-Unit クラスの「UnitPricing」デシジョンテーブルと、FSG-Data-Pricing-Value クラスの「VolumePricing」デシジョンテーブルに分解されました。 ポリモーフィズムはDerivePricingというデータトランスフォームに対して使用されます。DerivePricingはFSG-Data-Pricingでスタブアウトされ、次に各派生クラスで上書きされます。
最初の手順は、両方のデシジョンテーブルの列をライブデータテーブルに移行させることです。 「Pricing」という名前はすでに使用されているため、ライブデータテーブルの名前として使用することはできません。 このテーブルの名前としてより適しているのは「Price」です。これは、このテーブルにはその種の情報が含まれているためです。
Price
ItemID |
Quantity From |
Quantity To |
PricingModel |
Price |
Priceレコードには4つのキー、つまり左から4つ目までの列があります。 2つのキー(ItemIDとPricingModel)のみを使用した場合、「すべての価格設定可能項目は、PricingModelごとに1つの行しか持つことができない」ということになります。しかし、これは理にかなっていません。なぜならVolume価格設定そのものが、From/Toの数量範囲ごとに複数行を必要とするからです。 唯一の、重なりのない、連続したVolume数量の範囲を表す行の定義を強制しようとすることは不可能です。 このテーブルにAsOfDate列を追加することで、価格は時間の経過とともに新しい値に円滑に移行できます。 一意のキーの強制は複雑であるため、FSG-Data-Price データタイプのクラスルールのボックスにチェックを入れることで、プライマリーキーとしてpyGUIDを自動生成することになりました。
2 開放/閉鎖ソリューションの考案
最初のステージで、開放/閉鎖ソリューションを考案する工程は、シンプルに2つのデシジョンテーブルを2つのライブデータテーブルに変換するだけです。 次のステージは、これよりも難しくなります。
最もシンプルなタスクは、Quotationステージの最初にあるInitPricingアクティビティに代わるものを見つけることです。 このアクティビティは、Bookingアプリケーションで見積もる価格設定可能項目に固有のものです。 より優れた代替方法は、任意のアプリケーションについて価格設定可能項目を初期化する方法を見つけることです。
これについて説明する前に、次のような疑問が生じるかもしれません。「見積もりを行う前にPricingレコードのセットを初期化しなければならない理由は何か。 代わりに、Tableレイアウトを使ってPricingレコードのリストを作成し、一度に1行ずつ追加することをユーザーに強制する方法は採用できないのか。」 端的に言えば、ttは必要ではありません。ただし、Pricingレコードが初期化されていれば、ユーザーにとって格段に使い勝手と生産性が向上します。 ユーザーが毎回Pricingレコードの初期リストを自分で作成することを余儀なくされるとします。 その場合、どの価格設定可能項目から始めるか、その項目が必須か任意かを覚えておく必要があります。 このため、エラーが発生しがちです。 営業担当者は、顧客とのコミュニケーションに集中すべきであり、不要なマウスクリックに時間を費やすべきではありません。 レストランで料理を注文することに例えてみましょう。 ウェイターに記憶を頼りにメニューに載っているものをすべて口述してもらうよりも、お客様にメニューから選んでもらったほうがはるかに簡単で便利です。
Pricingレコード内のPriceプロパティの計算に必要な入力は記録されます。 最初の段階では、DiscountFactorは1(割引なし)に設定されています。 これは、 FSG-Data-Pricing pyDefaultデータトランスフォームで設定できます。 常に必要な項目の場合、Integer Bitレコードは1に事前設定されます。Quantityは0以外が設定されます。 VolumeIncludesQtyがtrueの場合、Quantityは1です。 ただし、オプション項目の場合は、Integer Bitレコードは0に事前設定されます。 Bitの値が0に設定されている場合、Quantityの値は影響しません。
複数のアプリケーションが同じ項目を初期化できます。 より一般的な名称は「Owner」です。これによって、Pricingレコードを初期化できるのがアプリケーションに限定されるわけではありません。どのようなモノでも初期化を行えます。 その「モノ」が何であろうと、その項目のPricingレコードを初期化する方法を「所有」します。 これが、「Owner」という名前が適切である理由です。
PricingInit
Owner |
ItemID |
Bit |
Quantity |
次のタスクは、開放/閉鎖のソリューションを実装できるかどうかを左右するものです。 見積もりのUIで、複数のPricingレコードに影響を与える入力項目に変更を加えることができます。 変更時には、その入力項目の値をクリップボードに貼り付け、ただちに見積もり全体の再計算を要求できます。
Pricingレコードは複数あってもよく、それぞれに複数の入力項目がありますが、それが同じである必要はありません。 ホテルサービスを希望するかどうかを示すチェックボックスは、HotelServiceとHotelServiceCostの値を持つ項目に固有のものです。 一方、「Discount Percentage」フィールドは、価格は計算されるがコストは計算されないすべてのPricingレコードの入力項目として使用できます。
ステージ1では、一貫性はあるが異なる方法で価格を導き出すための既存のソリューションを探し、価格設定モデルのアプローチを使用することになりました。 このソリューションは存在しますが、どこを探せばよいのかを知っている必要があります。 インターネットで「pricing model」と検索して見つかるような直感的なものではありません。
ステージ2では、個別に構成可能な入力項目に基づいて一連の値を再計算できる、既存の一般的な方法を探すことができます。 「再計算」という用語が直接的に「スプレッドシート」という答えに直結します。 スプレッドシートの仕組みと同様のものをPega Platformに実装できるでしょうか。 もちろんできます。
スプレッドシートのセルの値は、Functionを含む式を使って計算されます。 スプレッドシートFunctionへの入力値は、リテラルまたは異なるスプレッドシートセル(A1など)への参照などがあります。 スプレッドシートセルのFunctionは、自身を参照できません。自身を参照すると、回帰的な無限ループになってしまうからです。
スプレッドシートのセルを抽象化すると、"isA" MemoryLocationとなります。 Pega Platform™では、プロパティisA MemoryLocationです。 スプレッドシートの行で横に並んだ列のセットは、Pega Platformのページのようなもので、フィールドグループとも呼ばれます。 スプレッドシートの長方形の領域は、Pega Platformのページリスト/フィールドグループリストに相当します。 Pega Platformのプロパティは、名前付きページ内の一意の名前で識別できます。 データトランスフォームでステップページ名が指定されていない場合、そのページはPrimaryであると見なされます。
この情報を取得するデータタイプの名前は、Pricing Property Sourceが適しています。
PricingPropertySource
Owner |
ItemID |
Page |
Property |
SourceProperty |
Page列は空欄のままでも構いません。 Primaryページがケースの場合に、RecalculateデータトランスフォームがPricingレコードの反復ループ内で呼び出されたときは、SourcePropertyの値はケースレベルのProperty内に存在します。 PricingPropertySourceテーブル内のProperty列の値は、FSG-Data-Pricing Propertyの名前、つまりBit、Quantity、DiscountFactorのいずれかでなければなりません。 Recalculateで使用される他のすべての入力項目は、ライブデータレコードから導き出されたもので、具体的にはPricingModelとVolumeIncludesQtyです。 前述の説明は、Pricing Component内の@baseclass RecalculatePricesデータトランスフォーム内で何が起こっているかについてです。
3 リファクタートランスフォームにページを渡す(PricingSummaryとPricingDisplay)
PricingSummary
ステージ2のクリアは、開放/閉鎖ソリューションを実現するための大きなハードルです。 しかし、それでもまだ最後にやらなければいけないことがあります。 そのひとつは、再計算の際に、総価格、総費用、利益を計算する方法を見つけることです。 最も簡単な方法は、これら3つのプロパティを含む抽象データタイプを定義することです。 そのデータタイプに適した名前は、PricingSummaryです。
Visitor設計パターンは、オブジェクトAがオブジェクトBにコントロールされるのではなく、オブジェクトAが自身を異なるオブジェクトBにプロセスしてもらえるようオファーするというものです。 言い換えれば、オブジェクトAがオブジェクトBのプロセスロジックの異なる領域を「訪問」することで、オブジェクトBがコントロール権を持つということです。
PricingSummaryページタイプは、オブジェクトAと考えることができます。 @baseclass RecalculatePricesデータトランスフォームに渡すパラメーターを確立するデータトランスフォームは、オブジェクトBと考えることができます。 パラメーターは、Page Name
タイプと考えることができます。 PricingSummaryタイプの埋め込みページを持つケースでは、その埋め込みページへの絶対パスは、その関数のステップページがその埋め込みページであるときに@pxGetStepPageReference()を呼び出すことで取得できます。 その関数呼び出しの戻り値は、SummaryPageというパラメーターに設定できます。 次に、このパラメーターを、SummaryPageをページ名パラメーターとして宣言しているWork-WorkRecalculatePricesデータトランスフォームに渡すことができます。 Pages & Classesでは、SummaryPageのクラスがFSG-Data-PricingSummaryと同等に設定されています。
これでPricingSummary「オブジェクト」が定義されたので、@baseclassRecalculatePricesデータトランスフォームからの訪問を受けることができるようになりました。 この訪問は、現在繰り返されているFSG-Data-PricingインスタンスにDerivePricingデータトランスフォームが適用された後に行われます。 項目が価格ではなくコストであることを検出するために、param.ItemIDの末尾がCostであるという、ちょっとしたロジックが必要です。 この場合、項目は別の方法で処理されます。つまり、DiscountFactorが適用されないものと見なされます。 最後にTotalCostには費用が追加され、TotalPriceには価格が追加されます。ProfitはTotalPriceとTotalCostの差異です。
PricingDisplay
より複雑な解決すべき問題は、「価格設定表示プロパティ」の値を設定することです。これは、価格と費用を表示することだけを目的とするプロパティです。 「すべてのPricingインスタンスを単にテーブルレイアウトで表示するだけではなぜいけないのか」という疑問は、検討の余地があります。
1つは、価格設定表示プロパティの制限が少ないことが挙げられます。 プロパティは、App Studioを使用して、Templated View内の好きな場所に配置できます。 App Studioでは、リストの外観にあまり柔軟に変更を加えることはできません。
次に、リストを表示するには、リストデータページをソース(データベースなど)から再初期化するか、リストを埋め込む(フィールドグループリストなど)必要があります。 最初にデータベースからクエリーした行のセットを、その行を再度永続化した直後に再クエリーすると、問題となることがあります。 ケースのBLOB内で永続化される可能性のあるリストをケースのクリップボードメモリ内に維持することは、ケースがQuotationステージを過ぎると無駄になります。 最後に、一連の価格設定表示プロパティの設定と表示の更新が明らかに速くなりました。
表示専用のプロパティを格納する抽象的なデータタイプの名前は、もちろんPricingDisplayです。 表示プロパティの数は、PricingInitで初期化されたPricingレコードの数より少なくても、1対1でも構いません。 繰り返しになりますが、これらのプロパティは表示のみを目的として使用されます。 それ以外の目的はありません。
ここでもVisiterパターンを使用できますが、開放/閉鎖原則の方法で行うことは簡単ではありません。 ケースには、PricingDisplayタイプの埋め込みページを持つことができます。 その埋め込みページへの絶対パスは、その関数のステップページがその埋め込みページであるときに@pxGetStepPageReference()を呼び出すことで取得できます。 その関数呼び出しの戻り値は、PricingDisplayPageというパラメーターに設定できます。 次に、このパラメーターを、PricingDisplayPageをページ名パラメーターとして宣言しているWork-WorkRecalculatePricesデータトランスフォームに渡すことができます。 Pages & Classesでは、PricingDisplayPageのクラスがORG-APP-Data-PricingDisplayと同等に設定されています。
費用となる項目のIDは、末尾にCostが付加されることがすでに想定済みです。 価格を表す価格表示プロパティの名前の末尾にPriceが付加されることは理にかなっているように思えます。 しかし、それを制約条件として追加する理由はありません。 ベストプラクティスは、不必要な制約を使用しないことです。 ここで行えるのは、ItemIDSuffixというパラメーターを定義することです。 項目のIDの末尾がCostでない場合は、そのIDにItemIDSuffixが付加されます。 ItemIDSuffixには、空の文字列を含む任意の値を指定できます。
RecalculatePricesデータトランスフォームをPricingDisplayPageが「訪問した」ことが検出されたら、@setPropertyValue(myStepPage,Param.DisplayProperty,PricingSavable.Price)を使用して、価格表示プロパティの想定名がそのプロパティに決定された時点で、その値を設定すれば作業は完了です。
4 UI構成の簡素化
計算入力として使用されるすべてのフィールドの変更時またはクリック時に、UIがWork-WorkRecalculatePricesデータトランスフォームを直接呼び出すよう強制すると、アプリケーションのメンテナンスを行う人に負担がかかります。 WorkRecalculatePricesには6つのパラメーターがあります。 1つのパラメーターはブール値の「Commit」で、残りの5つのパラメーターは@baseclassRecalculatePricesに渡されます。
すべてのPricingレコードはいずれにしても再計算されるため、最終的に「Pricing Property Source」であるBit、Quantity、DiscountFactorの値を設定するために使用される入力フィールドに必要なのは、フィールドの値をクリップボードにポストし、サーバーに再計算を指示することだけです。 再計算の後、ビューが更新されて再計算の結果が表示されます。
入力フィールドの値がポストされると、Rule-Declare-Expressionを使用して、実際の「Pricing Property Source」の値を導き出すことができます。たとえば、Integer DiscountPercentageプロパティ値が変更され、その値が0から100の間になった場合、対応するDecimal DiscountFactorプロパティには、それぞれ1.0から0の間の値が設定されます。
入力フィールドがサーバーに伝える必要のあるパラメーターは、Commitを発行するかどうかだけです。 データトランスフォームにおいて、コミットが常に実行される必要があるという想定は適切でありません。 データトランスフォームは、ケースの処理中に呼び出されることがあります。
FSGPricingTestアプリケーションには、「Commit」という1つのブール型パラメーターを持つサンプルのFSG-PricingTest-Work FSGSampleRecalculatePricesデータトランスフォームが含まれています。次に、Proxyパターンが採用されます。 プロキシとは、より複雑なものへのインターフェイスをシンプルにするために、オブジェクトが呼び出すものです。 このように、FSGSampleRecalculatePricesデータトランスフォームでは5つのパラメーターの値が設定され、次にWork-WorkRecalculatePricesに渡されます(その後、@baseclass RecalculatePricesに渡されます)。 Referenceパラメーターは.pyIDに、Ownerパラメーターはアプリケーション名と同一のもの(例:Booking)に、ItemIDSuffixパラメーターにPriceに、そして2つのPage Nameパラメーターは前述のように設定されています。